Compare commits

..

29 Commits

Author SHA1 Message Date
Bernd Bestel
9ddcdb3ab2 Fixed login form didn't respect the configured BASE_URL 2018-04-18 22:38:05 +02:00
Bernd Bestel
1c537cf5da Added some missing translations 2018-04-18 19:37:36 +02:00
Bernd Bestel
607a90cccc Use absolute URLs everywhere, this should fix #3 2018-04-18 19:03:39 +02:00
Bernd Bestel
3d1c6fc5f0 Added update instructions 2018-04-18 07:59:13 +02:00
Bernd Bestel
df1d3677e8 Fixed wrong format info 2018-04-17 20:10:19 +02:00
Bernd Bestel
4da2ac9b35 Typo... 2018-04-17 20:02:09 +02:00
Bernd Bestel
b4ae7d8538 Added some notable things to README 2018-04-17 20:00:00 +02:00
Bernd Bestel
870b679e0e Updated screenshots 2018-04-16 19:31:31 +02:00
Bernd Bestel
4656a85732 Added localization support 2018-04-16 19:11:32 +02:00
Bernd Bestel
4949913ccb Fix usings 2018-04-15 16:02:17 +02:00
Bernd Bestel
580bd5ac0c This is 1.7.0 2018-04-15 15:41:59 +02:00
Bernd Bestel
2bf3448d18 Separate app bootstrapping and routes 2018-04-15 14:51:31 +02:00
Bernd Bestel
e9bc51ca3d Fix session table missing on start with empty database 2018-04-15 14:49:00 +02:00
Bernd Bestel
5ddae116e0 Some style changes 2018-04-15 14:38:42 +02:00
Bernd Bestel
13566bc6fd Modularize more components 2018-04-15 09:41:53 +02:00
Bernd Bestel
642f95a3f8 Finalize project reorganization 2018-04-14 11:10:38 +02:00
Bernd Bestel
5a1d21ef31 Reorganize project part 3 2018-04-12 21:13:38 +02:00
Bernd Bestel
7dcd39f82f Move public stuff into subdirectory 2018-04-11 20:47:03 +02:00
Bernd Bestel
655aa89bd6 Merge branch 'master' of https://github.com/berrnd/grocy 2018-04-11 19:51:22 +02:00
Bernd Bestel
feb88ab685 Reorganize project part 2 2018-04-11 19:51:05 +02:00
Bernd Bestel
79b4bad014 Reorganize project part 2 2018-04-11 19:49:35 +02:00
Bernd Bestel
bcd5092427 Reorganize project part 1 2018-04-10 20:30:11 +02:00
Bernd Bestel
554a83fa01 Show auto added products with blue background 2018-01-04 22:23:24 +01:00
Bernd Bestel
57acb62520 Show note below product name 2018-01-04 21:55:12 +01:00
Bernd Bestel
52ed5f2285 Show relative date in purchase form 2018-01-04 13:02:28 +01:00
Bernd Bestel
dd1d253ea5 Fix form label naming 2018-01-04 12:53:15 +01:00
Bernd Bestel
e40979a874 Allow arbitrary text in shopping list 2018-01-04 12:51:36 +01:00
Bernd Bestel
2ddbc2656b Added >PHP 7.0 requirement (this closes #2) 2017-11-26 10:27:09 +01:00
Bernd Bestel
7351fce395 Form productivity improvements 2017-11-10 22:45:53 +01:00
195 changed files with 6160 additions and 4357 deletions

3
.bowerrc Normal file
View File

@@ -0,0 +1,3 @@
{
"directory": "public/bower_components"
}

201
.gitignore vendored
View File

@@ -1,202 +1,3 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
# User-specific files
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
build/
bld/
[Bb]in/
[Oo]bj/
# Visual Studo 2015 cache/options directory
.vs/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUNIT
*.VisualState.xml
TestResult.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
*_i.c
*_p.c
*_i.h
*.ilk
*.meta
*.obj
*.pch
*.pdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opensdf
*.sdf
*.cachefile
# Visual Studio profiler
*.psess
*.vsp
*.vspx
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# JustCode is a .NET coding addin-in
.JustCode
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# NCrunch
_NCrunch_*
.*crunch*.local.xml
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# TODO: Comment the next line if you want to checkin your web deploy settings
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# NuGet Packages
*.nupkg
# The packages folder can be ignored because of Package Restore
**/packages/*
# except build/, which is used as an MSBuild target.
!**/packages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/packages/repositories.config
# Windows Azure Build Output
csx/
*.build.csdef
# Windows Store app package directory
AppPackages/
# Others
*.[Cc]ache
ClientBin/
[Ss]tyle[Cc]op.*
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.pfx
*.publishsettings
node_modules/
bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
# SQL Server files
*.mdf
*.ldf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
# Microsoft Fakes
FakesAssemblies/
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
/bower_components
/public/bower_components
/vendor
/.release
/composer.phar
/composer.lock

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"phpserver.relativePath": "public"
}

148
Grocy.php
View File

@@ -1,148 +0,0 @@
<?php
class Grocy
{
private static $DbConnectionRaw;
/**
* @return PDO
*/
public static function GetDbConnectionRaw($doMigrations = false)
{
if ($doMigrations === true)
{
self::$DbConnectionRaw = null;
}
if (self::$DbConnectionRaw == null)
{
$pdo = new PDO('sqlite:' . __DIR__ . '/data/grocy.db');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
if ($doMigrations === true)
{
Grocy::ExecuteDbStatement($pdo, "CREATE TABLE IF NOT EXISTS migrations (migration INTEGER NOT NULL PRIMARY KEY UNIQUE, execution_time_timestamp DATETIME DEFAULT (datetime('now', 'localtime')))");
GrocyDbMigrator::MigrateDb($pdo);
if (self::IsDemoInstallation())
{
GrocyDemoDataGenerator::PopulateDemoData($pdo);
}
}
self::$DbConnectionRaw = $pdo;
}
return self::$DbConnectionRaw;
}
private static $DbConnection;
/**
* @return LessQL\Database
*/
public static function GetDbConnection($doMigrations = false)
{
if ($doMigrations === true)
{
self::$DbConnection = null;
}
if (self::$DbConnection == null)
{
self::$DbConnection = new LessQL\Database(self::GetDbConnectionRaw($doMigrations));
}
return self::$DbConnection;
}
/**
* @return boolean
*/
public static function ExecuteDbStatement(PDO $pdo, string $sql)
{
if ($pdo->exec($sql) === false)
{
throw new Exception($pdo->errorInfo());
}
return true;
}
/**
* @return boolean|PDOStatement
*/
public static function ExecuteDbQuery(PDO $pdo, string $sql)
{
if (self::ExecuteDbStatement($pdo, $sql) === true)
{
return $pdo->query($sql);
}
return false;
}
/**
* @return boolean
*/
public static function IsDemoInstallation()
{
return file_exists(__DIR__ . '/data/demo.txt');
}
private static $InstalledVersion;
/**
* @return string
*/
public static function GetInstalledVersion()
{
if (self::$InstalledVersion == null)
{
self::$InstalledVersion = preg_replace("/\r|\n/", '', file_get_contents(__DIR__ . '/version.txt'));
}
return self::$InstalledVersion;
}
/**
* @return boolean
*/
public static function IsValidSession($sessionKey)
{
if ($sessionKey === null || empty($sessionKey))
{
return false;
}
else
{
return file_exists(__DIR__ . "/data/sessions/$sessionKey.txt");
}
}
/**
* @return string
*/
public static function CreateSession()
{
if (!file_exists(__DIR__ . '/data/sessions'))
{
mkdir(__DIR__ . '/data/sessions');
}
$now = time();
foreach (new FilesystemIterator(__DIR__ . '/data/sessions') as $file)
{
if ($now - $file->getCTime() >= 2678400) //31 days
{
unlink(__DIR__ . '/data/sessions/' . $file->getFilename());
}
}
$newSessionKey = uniqid() . uniqid() . uniqid();
file_put_contents(__DIR__ . "/data/sessions/$newSessionKey.txt", '');
return $newSessionKey;
}
public static function RemoveSession($sessionKey)
{
unlink(__DIR__ . "/data/sessions/$sessionKey.txt");
}
}

View File

@@ -1,174 +0,0 @@
<?php
class GrocyDbMigrator
{
public static function MigrateDb(PDO $pdo)
{
self::ExecuteMigrationWhenNeeded($pdo, 1, "
CREATE TABLE products (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
name TEXT NOT NULL UNIQUE,
description TEXT,
location_id INTEGER NOT NULL,
qu_id_purchase INTEGER NOT NULL,
qu_id_stock INTEGER NOT NULL,
qu_factor_purchase_to_stock REAL NOT NULL,
barcode TEXT,
min_stock_amount INTEGER NOT NULL DEFAULT 0,
default_best_before_days INTEGER NOT NULL DEFAULT 0,
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
)"
);
self::ExecuteMigrationWhenNeeded($pdo, 2, "
CREATE TABLE locations (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
name TEXT NOT NULL UNIQUE,
description TEXT,
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
)"
);
self::ExecuteMigrationWhenNeeded($pdo, 3, "
CREATE TABLE quantity_units (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
name TEXT NOT NULL UNIQUE,
description TEXT,
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
)"
);
self::ExecuteMigrationWhenNeeded($pdo, 4, "
CREATE TABLE stock (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
product_id INTEGER NOT NULL,
amount INTEGER NOT NULL,
best_before_date DATE,
purchased_date DATE DEFAULT (datetime('now', 'localtime')),
stock_id TEXT NOT NULL,
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
)"
);
self::ExecuteMigrationWhenNeeded($pdo, 5, "
CREATE TABLE stock_log (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
product_id INTEGER NOT NULL,
amount INTEGER NOT NULL,
best_before_date DATE,
purchased_date DATE,
used_date DATE,
spoiled INTEGER NOT NULL DEFAULT 0,
stock_id TEXT NOT NULL,
transaction_type TEXT NOT NULL,
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
)"
);
self::ExecuteMigrationWhenNeeded($pdo, 6, "
INSERT INTO locations (name, description) VALUES ('DefaultLocation', 'This is the first default location, edit or delete it');
INSERT INTO quantity_units (name, description) VALUES ('DefaultQuantityUnit', 'This is the first default quantity unit, edit or delete it');
INSERT INTO products (name, description, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('DefaultProduct1', 'This is the first default product, edit or delete it', 1, 1, 1, 1);
INSERT INTO products (name, description, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('DefaultProduct2', 'This is the second default product, edit or delete it', 1, 1, 1, 1);"
);
self::ExecuteMigrationWhenNeeded($pdo, 7, "
CREATE VIEW stock_missing_products
AS
SELECT p.id, MAX(p.name) AS name, p.min_stock_amount - IFNULL(SUM(s.amount), 0) AS amount_missing
FROM products p
LEFT JOIN stock s
ON p.id = s.product_id
WHERE p.min_stock_amount != 0
GROUP BY p.id
HAVING IFNULL(SUM(s.amount), 0) < p.min_stock_amount;"
);
self::ExecuteMigrationWhenNeeded($pdo, 8, "
CREATE VIEW stock_current
AS
SELECT product_id, SUM(amount) AS amount, MIN(best_before_date) AS best_before_date
FROM stock
GROUP BY product_id
ORDER BY MIN(best_before_date) ASC;"
);
self::ExecuteMigrationWhenNeeded($pdo, 9, "
CREATE TABLE shopping_list (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
product_id INTEGER NOT NULL UNIQUE,
amount INTEGER NOT NULL DEFAULT 0,
amount_autoadded INTEGER NOT NULL DEFAULT 0,
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
)"
);
self::ExecuteMigrationWhenNeeded($pdo, 10, "
CREATE TABLE habits (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
name TEXT NOT NULL UNIQUE,
description TEXT,
period_type TEXT NOT NULL,
period_days INTEGER,
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
)"
);
self::ExecuteMigrationWhenNeeded($pdo, 11, "
CREATE TABLE habits_log (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
habit_id INTEGER NOT NULL,
tracked_time DATETIME,
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
)"
);
self::ExecuteMigrationWhenNeeded($pdo, 12, "
CREATE VIEW habits_current
AS
SELECT habit_id, MAX(tracked_time) AS last_tracked_time
FROM habits_log
GROUP BY habit_id
ORDER BY MAX(tracked_time) DESC;"
);
self::ExecuteMigrationWhenNeeded($pdo, 13, "
CREATE TABLE batteries (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
name TEXT NOT NULL UNIQUE,
description TEXT,
used_in TEXT,
charge_interval_days INTEGER NOT NULL DEFAULT 0,
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
)"
);
self::ExecuteMigrationWhenNeeded($pdo, 14, "
CREATE TABLE battery_charge_cycles (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
battery_id TEXT NOT NULL,
tracked_time DATETIME,
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
)"
);
self::ExecuteMigrationWhenNeeded($pdo, 15, "
CREATE VIEW batteries_current
AS
SELECT battery_id, MAX(tracked_time) AS last_tracked_time
FROM battery_charge_cycles
GROUP BY battery_id
ORDER BY MAX(tracked_time) DESC;"
);
}
private static function ExecuteMigrationWhenNeeded(PDO $pdo, int $migrationId, string $sql)
{
$rowCount = Grocy::ExecuteDbQuery($pdo, 'SELECT COUNT(*) FROM migrations WHERE migration = ' . $migrationId)->fetchColumn();
if (intval($rowCount) === 0)
{
Grocy::ExecuteDbStatement($pdo, $sql);
Grocy::ExecuteDbStatement($pdo, 'INSERT INTO migrations (migration) VALUES (' . $migrationId . ')');
}
}
}

View File

@@ -1,90 +0,0 @@
<?php
class GrocyDemoDataGenerator
{
public static function PopulateDemoData(PDO $pdo)
{
$rowCount = Grocy::ExecuteDbQuery($pdo, 'SELECT COUNT(*) FROM migrations WHERE migration = -1')->fetchColumn();
if (intval($rowCount) === 0)
{
$sql = "
UPDATE locations SET name = 'Vorratskammer', description = '' WHERE id = 1;
INSERT INTO locations (name) VALUES ('Süßigkeitenschrank'); --2
INSERT INTO locations (name) VALUES ('Konservenschrank'); --3
INSERT INTO locations (name) VALUES ('Kühlschrank'); --4
UPDATE quantity_units SET name = 'Stück' WHERE id = 1;
INSERT INTO quantity_units (name) VALUES ('Packung'); --2
INSERT INTO quantity_units (name) VALUES ('Glas'); --3
INSERT INTO quantity_units (name) VALUES ('Dose'); --4
INSERT INTO quantity_units (name) VALUES ('Becher'); --5
INSERT INTO quantity_units (name) VALUES ('Bund'); --6
DELETE FROM products WHERE id IN (1, 2);
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, min_stock_amount) VALUES ('Gummibärchen', 2, 2, 2, 1, 8); --3
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, min_stock_amount) VALUES ('Chips', 2, 2, 2, 1, 10); --4
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('Eier', 1, 2, 1, 10); --5
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('Nudeln', 1, 2, 2, 1); --6
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('Essiggurken', 3, 3, 3, 1); --7
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('Gulaschsuppe', 3, 4, 4, 1); --8
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('Joghurt', 4, 5, 5, 1); --9
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('Käse', 4, 2, 2, 1); --10
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('Aufschnitt', 4, 2, 2, 1); --11
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('Paprika', 4, 1, 1, 1); --12
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('Gurke', 4, 1, 1, 1); --13
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('Radieschen', 4, 6, 6, 1); --14
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('Tomate', 4, 1, 1, 1); --15
INSERT INTO habits (name, period_type, period_days) VALUES ('Changed towels in the bathroom', 'manually', 5); --1
INSERT INTO habits (name, period_type, period_days) VALUES ('Cleaned the kitchen floor', 'dynamic-regular', 7); --2
INSERT INTO batteries (name, description, used_in) VALUES ('Battery1', 'Warranty ends 2022', 'TV remote control'); --1
INSERT INTO batteries (name, description, used_in) VALUES ('Battery2', 'Warranty ends 2022', 'Alarm clock'); --2
INSERT INTO batteries (name, description, used_in, charge_interval_days) VALUES ('Battery3', 'Warranty ends 2022', 'Heat remote control', 60); --3
INSERT INTO migrations (migration) VALUES (-1);
";
Grocy::ExecuteDbStatement($pdo, $sql);
GrocyLogicStock::AddProduct(3, 5, date('Y-m-d', strtotime('+180 days')), GrocyLogicStock::TRANSACTION_TYPE_PURCHASE);
GrocyLogicStock::AddProduct(4, 5, date('Y-m-d', strtotime('+180 days')), GrocyLogicStock::TRANSACTION_TYPE_PURCHASE);
GrocyLogicStock::AddProduct(5, 5, date('Y-m-d', strtotime('+20 days')), GrocyLogicStock::TRANSACTION_TYPE_PURCHASE);
GrocyLogicStock::AddProduct(6, 5, date('Y-m-d', strtotime('+600 days')), GrocyLogicStock::TRANSACTION_TYPE_PURCHASE);
GrocyLogicStock::AddProduct(7, 5, date('Y-m-d', strtotime('+800 days')), GrocyLogicStock::TRANSACTION_TYPE_PURCHASE);
GrocyLogicStock::AddProduct(8, 5, date('Y-m-d', strtotime('+900 days')), GrocyLogicStock::TRANSACTION_TYPE_PURCHASE);
GrocyLogicStock::AddProduct(9, 5, date('Y-m-d', strtotime('+14 days')), GrocyLogicStock::TRANSACTION_TYPE_PURCHASE);
GrocyLogicStock::AddProduct(10, 5, date('Y-m-d', strtotime('+21 days')), GrocyLogicStock::TRANSACTION_TYPE_PURCHASE);
GrocyLogicStock::AddProduct(11, 5, date('Y-m-d', strtotime('+10 days')), GrocyLogicStock::TRANSACTION_TYPE_PURCHASE);
GrocyLogicStock::AddProduct(12, 5, date('Y-m-d', strtotime('+2 days')), GrocyLogicStock::TRANSACTION_TYPE_PURCHASE);
GrocyLogicStock::AddProduct(13, 5, date('Y-m-d', strtotime('-2 days')), GrocyLogicStock::TRANSACTION_TYPE_PURCHASE);
GrocyLogicStock::AddProduct(14, 5, date('Y-m-d', strtotime('+2 days')), GrocyLogicStock::TRANSACTION_TYPE_PURCHASE);
GrocyLogicStock::AddProduct(15, 5, date('Y-m-d', strtotime('-2 days')), GrocyLogicStock::TRANSACTION_TYPE_PURCHASE);
GrocyLogicStock::AddMissingProductsToShoppingList();
GrocyLogicHabits::TrackHabit(1, date('Y-m-d H:i:s', strtotime('-5 days')));
GrocyLogicHabits::TrackHabit(1, date('Y-m-d H:i:s', strtotime('-10 days')));
GrocyLogicHabits::TrackHabit(1, date('Y-m-d H:i:s', strtotime('-15 days')));
GrocyLogicHabits::TrackHabit(2, date('Y-m-d H:i:s', strtotime('-10 days')));
GrocyLogicHabits::TrackHabit(2, date('Y-m-d H:i:s', strtotime('-20 days')));
GrocyLogicBatteries::TrackChargeCycle(1, date('Y-m-d H:i:s', strtotime('-200 days')));
GrocyLogicBatteries::TrackChargeCycle(1, date('Y-m-d H:i:s', strtotime('-150 days')));
GrocyLogicBatteries::TrackChargeCycle(1, date('Y-m-d H:i:s', strtotime('-100 days')));
GrocyLogicBatteries::TrackChargeCycle(1, date('Y-m-d H:i:s', strtotime('-50 days')));
GrocyLogicBatteries::TrackChargeCycle(2, date('Y-m-d H:i:s', strtotime('-200 days')));
GrocyLogicBatteries::TrackChargeCycle(2, date('Y-m-d H:i:s', strtotime('-150 days')));
GrocyLogicBatteries::TrackChargeCycle(2, date('Y-m-d H:i:s', strtotime('-100 days')));
GrocyLogicBatteries::TrackChargeCycle(2, date('Y-m-d H:i:s', strtotime('-50 days')));
GrocyLogicBatteries::TrackChargeCycle(3, date('Y-m-d H:i:s', strtotime('-65 days')));
}
}
public static function RecreateDemo()
{
unlink(__DIR__ . '/data/grocy.db');
$db = Grocy::GetDbConnectionRaw(true);
self::PopulateDemoData($db);
}
}

View File

@@ -1,57 +0,0 @@
<?php
class GrocyLogicBatteries
{
public static function GetCurrent()
{
$sql = 'SELECT * from batteries_current';
return Grocy::ExecuteDbQuery(Grocy::GetDbConnectionRaw(), $sql)->fetchAll(PDO::FETCH_OBJ);
}
public static function GetNextChargeTime(int $batteryId)
{
$db = Grocy::GetDbConnection();
$battery = $db->batteries($batteryId);
$batteryLastLogRow = Grocy::ExecuteDbQuery(Grocy::GetDbConnectionRaw(), "SELECT * from batteries_current WHERE battery_id = $batteryId LIMIT 1")->fetch(PDO::FETCH_OBJ);
if ($battery->charge_interval_days > 0)
{
return date('Y-m-d H:i:s', strtotime('+' . $battery->charge_interval_days . ' day', strtotime($batteryLastLogRow->last_tracked_time)));
}
else
{
return date('Y-m-d H:i:s');
}
return null;
}
public static function GetBatteryDetails(int $batteryId)
{
$db = Grocy::GetDbConnection();
$battery = $db->batteries($batteryId);
$batteryChargeCylcesCount = $db->battery_charge_cycles()->where('battery_id', $batteryId)->count();
$batteryLastChargedTime = $db->battery_charge_cycles()->where('battery_id', $batteryId)->max('tracked_time');
return array(
'battery' => $battery,
'last_charged' => $batteryLastChargedTime,
'charge_cycles_count' => $batteryChargeCylcesCount
);
}
public static function TrackChargeCycle(int $batteryId, string $trackedTime)
{
$db = Grocy::GetDbConnection();
$logRow = $db->battery_charge_cycles()->createRow(array(
'battery_id' => $batteryId,
'tracked_time' => $trackedTime
));
$logRow->save();
return true;
}
}

View File

@@ -1,59 +0,0 @@
<?php
class GrocyLogicHabits
{
const HABIT_TYPE_MANUALLY = 'manually';
const HABIT_TYPE_DYNAMIC_REGULAR = 'dynamic-regular';
public static function GetCurrentHabits()
{
$sql = 'SELECT * from habits_current';
return Grocy::ExecuteDbQuery(Grocy::GetDbConnectionRaw(), $sql)->fetchAll(PDO::FETCH_OBJ);
}
public static function GetNextHabitTime(int $habitId)
{
$db = Grocy::GetDbConnection();
$habit = $db->habits($habitId);
$habitLastLogRow = Grocy::ExecuteDbQuery(Grocy::GetDbConnectionRaw(), "SELECT * from habits_current WHERE habit_id = $habitId LIMIT 1")->fetch(PDO::FETCH_OBJ);
switch ($habit->period_type)
{
case self::HABIT_TYPE_MANUALLY:
return date('Y-m-d H:i:s');
case self::HABIT_TYPE_DYNAMIC_REGULAR:
return date('Y-m-d H:i:s', strtotime('+' . $habit->period_days . ' day', strtotime($habitLastLogRow->last_tracked_time)));
}
return null;
}
public static function GetHabitDetails(int $habitId)
{
$db = Grocy::GetDbConnection();
$habit = $db->habits($habitId);
$habitTrackedCount = $db->habits_log()->where('habit_id', $habitId)->count();
$habitLastTrackedTime = $db->habits_log()->where('habit_id', $habitId)->max('tracked_time');
return array(
'habit' => $habit,
'last_tracked' => $habitLastTrackedTime,
'tracked_count' => $habitTrackedCount
);
}
public static function TrackHabit(int $habitId, string $trackedTime)
{
$db = Grocy::GetDbConnection();
$logRow = $db->habits_log()->createRow(array(
'habit_id' => $habitId,
'tracked_time' => $trackedTime
));
$logRow->save();
return true;
}
}

View File

@@ -1,66 +0,0 @@
<?php
class GrocyPhpHelper
{
public static function FindObjectInArrayByPropertyValue($array, $propertyName, $propertyValue)
{
foreach($array as $object)
{
if($object->{$propertyName} == $propertyValue)
{
return $object;
}
}
return null;
}
public static function FindAllObjectsInArrayByPropertyValue($array, $propertyName, $propertyValue, $operator = '==')
{
$returnArray = array();
foreach($array as $object)
{
switch($operator)
{
case '==':
if($object->{$propertyName} == $propertyValue)
{
$returnArray[] = $object;
}
break;
case '>':
if($object->{$propertyName} > $propertyValue)
{
$returnArray[] = $object;
}
break;
case '<':
if($object->{$propertyName} < $propertyValue)
{
$returnArray[] = $object;
}
break;
}
}
return $returnArray;
}
public static function SumArrayValue($array, $propertyName)
{
$sum = 0;
foreach($array as $object)
{
$sum += $object->{$propertyName};
}
return $sum;
}
public static function GetClassConstants($className)
{
$r = new ReflectionClass($className);
return $r->getConstants();
}
}

View File

@@ -1,22 +1,50 @@
# grocy
ERP beyond your fridge
## Give it a try
Public demo of the latest version &rarr; [https://demo.grocy.info](https://demo.grocy.info)
## Motivation
A household needs to be managed. I did this so far (almost 10 years) with my first self written software (a C# windows forms application) and with a bunch of Excel sheets. The software is a pain to use and Excel is Excel. So I searched for and tried different things for a (very) long time, nothing 100 % fitted, so this is my aim for a "complete houshold management"-thing.
## What it is about
For now my main focus is on stock management, ERP your fridge!
# Give it a try
Public demo of the latest version &rarr; [https://demo.grocy.info](https://demo.grocy.info)
## How to install
Just unpack the [latest release](https://github.com/berrnd/grocy/releases/latest) on your PHP enabled webserver, copy `config-dist.php` to `data/config.php`, edit it to your needs, ensure that the `data` directory is writable and you're ready to go. Alternatively clone this repository and install Composer and Bower dependencies manually.
Just unpack the [latest release](https://github.com/berrnd/grocy/releases/latest) on your PHP (7.0 or later required) enabled webserver (webservers root should point to the `/public` directory), copy `config-dist.php` to `data/config.php`, edit it to your needs, ensure that the `data` directory is writable and you're ready to go.
Default login is user `admin` with password `admin` - see the `data/config.php` file. Alternatively clone this repository and install Composer and Bower dependencies manually.
If you use nginx as your webserver, please include `try_files $uri /index.php;` in your location block.
## Notes about barcode readers
Some fields also allow to select a value by scanning a barcode. It works best when your barcode reader prefixes every barcode with a letter this is normally not part of a item name (I use a `$`) and sends a `TAB` after a scan.
## How to update
Just overwrite everything with the latest release while keeping the `/data` directory, check `config-dist.php` for new configuration options and add them to your `data/config.php` (it will show up as an error if something is missing there).
## Things worth to know
### Barcode readers
Some fields also allow to select a value by scanning a barcode. It works best when your barcode reader prefixes every barcode with a letter which is normally not part of a item name (I use a `$`) and sends a `TAB` after a scan.
### Input shorthands for date fields
For (productivity) reasons all date (and time) input fields use the ISO-8601 format regardless of localization.
The following shorthands are available:
- `MMDD` gets expanded to the given day on the current year in proper notation
- Example: `0517` will be converted to `2018-05-17`
- `YYYYMMDD` gets expanded to the proper ISO-8601 notation
- Example: `20190417` will be converted to `2019-04-17`
- `x` gets expanded to `2099-12-31` (which I use for products which never expire)
- Down/up arrow keys will increase/decrease the date by one day
- Right/left arrow keys will increase/decrease the date by 1 week
### Keyboard shorthands for buttons
Wherever a button contains a bold highlighted letter, this is a shortcut key.
Example: Button "Add as new **p**roduct" can be "pressed" by using the `P` key on your keyboard.
### Database migrations
Database schema migration is automatically done when visiting the root (`/`) route (click on the logo in the left upper edge).
### Demo mode
When the file `data/demo.txt` exists, the application will work in a demo mode which means authentication is disabled and some demo data will be generated during the database schema migration.
## Screenshots
#### Dashboard

48
app.php Normal file
View File

@@ -0,0 +1,48 @@
<?php
use \Psr\Http\Message\ServerRequestInterface as Request;
use \Psr\Http\Message\ResponseInterface as Response;
use \Grocy\Middleware\SessionAuthMiddleware;
use \Grocy\Helpers\UrlManager;
use \Grocy\Services\ApplicationService;
require_once __DIR__ . '/vendor/autoload.php';
require_once __DIR__ . '/data/config.php';
// Setup base application
if (PHP_SAPI !== 'cli')
{
$appContainer = new \Slim\Container([
'settings' => [
'displayErrorDetails' => true,
'determineRouteBeforeAppMiddleware' => true
],
'view' => function($container)
{
return new \Slim\Views\Blade(__DIR__ . '/views', __DIR__ . '/data/viewcache');
},
'UrlManager' => function($container)
{
return new UrlManager(BASE_URL);
}
]);
$app = new \Slim\App($appContainer);
}
else
{
$app = new \Slim\App();
$app->add(\pavlakis\cli\CliRequest::class);
}
// Add session handling if this is not a demo installation
$applicationService = new ApplicationService();
if (!$applicationService->IsDemoInstallation())
{
$app->add(SessionAuthMiddleware::class);
}
require_once __DIR__ . '/routes.php';
$app->run();

View File

@@ -8,5 +8,6 @@ for /f "tokens=*" %%a in ('type version.txt') do set version=%%a
del "%releasePath%\grocy_%version%.zip"
"build_tools\7za.exe" a -r "%releasePath%\grocy_%version%.zip" "%projectPath%\*" -xr!.* -xr!build_tools -xr!build.bat -xr!composer.json -xr!composer.lock -xr!bower.json -xr!publication_assets
"build_tools\7za.exe" a -r "%releasePath%\grocy_%version%.zip" "%projectPath%\.htaccess"
"build_tools\7za.exe" d "%releasePath%\grocy_%version%.zip" data\*.* data\sessions
"build_tools\7za.exe" a "%releasePath%\grocy_%version%.zip" "%projectPath%\public\.htaccess"
"build_tools\7za.exe" rn "%releasePath%\grocy_%version%.zip" .htaccess public\.htaccess
"build_tools\7za.exe" d "%releasePath%\grocy_%version%.zip" data\*.* data\sessions data\viewcache\*

View File

@@ -1,8 +1,19 @@
{
"require": {
"slim/slim": "^3.8",
"slim/php-view": "^2.2",
"morris/lessql": "^0.3.4",
"pavlakis/slim-cli": "^1.0"
"pavlakis/slim-cli": "^1.0",
"rubellum/slim-blade-view": "^0.1.1"
},
"autoload": {
"psr-4": {
"Grocy\\Services\\": "services/",
"Grocy\\Controllers\\": "controllers/",
"Grocy\\Middleware\\": "middleware/",
"Grocy\\Helpers\\": "helpers/"
},
"files": [
"helpers/extensions.php"
]
}
}

1240
composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,17 @@
<?php
# Login credentials
define('HTTP_USER', 'admin');
define('HTTP_PASSWORD', 'admin');
# Either "production" or "dev"
define('MODE', 'production');
# Either "en" or "de" or the filename (without extension) of
# one of the other available localization files in the "/localization" directory
define('CULTURE', 'en');
# The base url of your installation,
# should be just "/" when running directly under the root of a (sub)domain
# or for example "https:/example.com/grocy" when using a subdirectory
define('BASE_URL', '/');

View File

@@ -0,0 +1,11 @@
<?php
namespace Grocy\Controllers;
class BaseApiController extends BaseController
{
protected function ApiResponse($response)
{
return json_encode($response);
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace Grocy\Controllers;
use \Grocy\Services\DatabaseService;
use \Grocy\Services\ApplicationService;
use \Grocy\Services\LocalizationService;
class BaseController
{
public function __construct(\Slim\Container $container) {
$databaseService = new DatabaseService();
$this->Database = $databaseService->GetDbConnection();
$applicationService = new ApplicationService();
$container->view->set('version', $applicationService->GetInstalledVersion());
$localizationService = new LocalizationService(CULTURE);
$container->view->set('localizationStrings', $localizationService->GetCurrentCultureLocalizations());
$container->view->set('L', function($text, ...$placeholderValues) use($localizationService)
{
return $localizationService->Localize($text, ...$placeholderValues);
});
$container->view->set('U', function($relativePath) use($container)
{
return $container->UrlManager->ConstructUrl($relativePath);
});
$this->AppContainer = $container;
}
protected $AppContainer;
protected $Database;
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Grocy\Controllers;
use \Grocy\Services\BatteriesService;
class BatteriesApiController extends BaseApiController
{
public function __construct(\Slim\Container $container)
{
parent::__construct($container);
$this->BatteriesService = new BatteriesService();
}
protected $BatteriesService;
public function TrackChargeCycle(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
$trackedTime = date('Y-m-d H:i:s');
if (isset($request->getQueryParams()['tracked_time']) && !empty($request->getQueryParams()['tracked_time']))
{
$trackedTime = $request->getQueryParams()['tracked_time'];
}
return $this->ApiResponse(array('success' => $this->BatteriesService->TrackChargeCycle($args['batteryId'], $trackedTime)));
}
public function BatteryDetails(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
return $this->ApiResponse($this->BatteriesService->GetBatteryDetails($args['batteryId']));
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace Grocy\Controllers;
use \Grocy\Services\BatteriesService;
class BatteriesController extends BaseController
{
public function __construct(\Slim\Container $container)
{
parent::__construct($container);
$this->BatteriesService = new BatteriesService();
}
protected $BatteriesService;
public function Overview(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
$nextChargeTimes = array();
foreach($this->Database->batteries() as $battery)
{
$nextChargeTimes[$battery->id] = $this->BatteriesService->GetNextChargeTime($battery->id);
}
return $this->AppContainer->view->render($response, 'batteriesoverview', [
'batteries' => $this->Database->batteries(),
'current' => $this->BatteriesService->GetCurrent(),
'nextChargeTimes' => $nextChargeTimes
]);
}
public function TrackChargeCycle(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
return $this->AppContainer->view->render($response, 'batterytracking', [
'batteries' => $this->Database->batteries()
]);
}
public function BatteriesList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
return $this->AppContainer->view->render($response, 'batteries', [
'batteries' => $this->Database->batteries()
]);
}
public function BatteryEditForm(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
if ($args['batteryId'] == 'new')
{
return $this->AppContainer->view->render($response, 'batteryform', [
'mode' => 'create'
]);
}
else
{
return $this->AppContainer->view->render($response, 'batteryform', [
'battery' => $this->Database->batteries($args['batteryId']),
'mode' => 'edit'
]);
}
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace Grocy\Controllers;
use \Grocy\Services\ApplicationService;
use \Grocy\Services\DatabaseMigrationService;
class CliController extends BaseController
{
public function RecreateDemo(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
$applicationService = new ApplicationService();
if ($applicationService->IsDemoInstallation())
{
$databaseMigrationService = new DatabaseMigrationService();
$databaseMigrationService->RecreateDemo();
}
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Grocy\Controllers;
class GenericEntityApiController extends BaseApiController
{
public function GetObjects(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
return $this->ApiResponse($this->Database->{$args['entity']}());
}
public function GetObject(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
return $this->ApiResponse($this->Database->{$args['entity']}($args['objectId']));
}
public function AddObject(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
$newRow = $this->Database->{$args['entity']}()->createRow($request->getParsedBody());
$newRow->save();
$success = $newRow->isClean();
return $this->ApiResponse(array('success' => $success));
}
public function EditObject(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
$row = $this->Database->{$args['entity']}($args['objectId']);
$row->update($request->getParsedBody());
$success = $row->isClean();
return $this->ApiResponse(array('success' => $success));
}
public function DeleteObject(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
$row = $this->Database->{$args['entity']}($args['objectId']);
$row->delete();
$success = $row->isClean();
return $this->ApiResponse(array('success' => $success));
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Grocy\Controllers;
use \Grocy\Services\HabitsService;
class HabitsApiController extends BaseApiController
{
public function __construct(\Slim\Container $container)
{
parent::__construct($container);
$this->HabitsService = new HabitsService();
}
protected $HabitsService;
public function TrackHabitExecution(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
$trackedTime = date('Y-m-d H:i:s');
if (isset($request->getQueryParams()['tracked_time']) && !empty($request->getQueryParams()['tracked_time']))
{
$trackedTime = $request->getQueryParams()['tracked_time'];
}
return $this->ApiResponse(array('success' => $this->HabitsService->TrackHabit($args['habitId'], $trackedTime)));
}
public function HabitDetails(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
return $this->ApiResponse($this->HabitsService->GetHabitDetails($args['habitId']));
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace Grocy\Controllers;
use \Grocy\Services\HabitsService;
class HabitsController extends BaseController
{
public function __construct(\Slim\Container $container)
{
parent::__construct($container);
$this->HabitsService = new HabitsService();
}
protected $HabitsService;
public function Overview(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
$nextHabitTimes = array();
foreach($this->Database->habits() as $habit)
{
$nextHabitTimes[$habit->id] = $this->HabitsService->GetNextHabitTime($habit->id);
}
return $this->AppContainer->view->render($response, 'habitsoverview', [
'habits' => $this->Database->habits(),
'currentHabits' => $this->HabitsService->GetCurrentHabits(),
'nextHabitTimes' => $nextHabitTimes
]);
}
public function TrackHabitExecution(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
return $this->AppContainer->view->render($response, 'habittracking', [
'habits' => $this->Database->habits()
]);
}
public function HabitsList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
return $this->AppContainer->view->render($response, 'habits', [
'habits' => $this->Database->habits()
]);
}
public function HabitEditForm(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
if ($args['habitId'] == 'new')
{
return $this->AppContainer->view->render($response, 'habitform', [
'periodTypes' => GetClassConstants('\Grocy\Services\HabitsService'),
'mode' => 'create'
]);
}
else
{
return $this->AppContainer->view->render($response, 'habitform', [
'habit' => $this->Database->habits($args['habitId']),
'periodTypes' => GetClassConstants('\Grocy\Services\HabitsService'),
'mode' => 'edit'
]);
}
}
}

View File

@@ -0,0 +1,69 @@
<?php
namespace Grocy\Controllers;
use \Grocy\Services\SessionService;
use \Grocy\Services\ApplicationService;
use \Grocy\Services\DatabaseMigrationService;
use \Grocy\Services\DemoDataGeneratorService;
class LoginController extends BaseController
{
public function __construct(\Slim\Container $container)
{
parent::__construct($container);
$this->SessionService = new SessionService();
}
protected $SessionService;
public function ProcessLogin(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
$postParams = $request->getParsedBody();
if (isset($postParams['username']) && isset($postParams['password']))
{
if ($postParams['username'] === HTTP_USER && $postParams['password'] === HTTP_PASSWORD)
{
$sessionKey = $this->SessionService->CreateSession();
setcookie('grocy_session', $sessionKey, time() + 31536000); // Cookie expires in 1 year, but session validity is up to SessionService
return $response->withRedirect($this->AppContainer->UrlManager->ConstructUrl('/'));
}
else
{
return $response->withRedirect($this->AppContainer->UrlManager->ConstructUrl('/login?invalid=true'));
}
}
else
{
return $response->withRedirect($this->AppContainer->UrlManager->ConstructUrl('/login?invalid=true'));
}
}
public function LoginPage(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
return $this->AppContainer->view->render($response, 'login');
}
public function Logout(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
$this->SessionService->RemoveSession($_COOKIE['grocy_session']);
return $response->withRedirect($this->AppContainer->UrlManager->ConstructUrl('/'));
}
public function Root(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
// Schema migration is done here
$databaseMigrationService = new DatabaseMigrationService();
$databaseMigrationService->MigrateDatabase();
$applicationService = new ApplicationService();
if ($applicationService->IsDemoInstallation())
{
$demoDataGeneratorService = new DemoDataGeneratorService();
$demoDataGeneratorService->PopulateDemoData();
}
return $response->withRedirect($this->AppContainer->UrlManager->ConstructUrl('/stockoverview'));
}
}

View File

@@ -0,0 +1,77 @@
<?php
namespace Grocy\Controllers;
use \Grocy\Services\StockService;
class StockApiController extends BaseApiController
{
public function __construct(\Slim\Container $container)
{
parent::__construct($container);
$this->StockService = new StockService();
}
protected $StockService;
public function ProductDetails(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
return $this->ApiResponse($this->StockService->GetProductDetails($args['productId']));
}
public function AddProduct(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
$bestBeforeDate = date('Y-m-d');
if (isset($request->getQueryParams()['bestbeforedate']) && !empty($request->getQueryParams()['bestbeforedate']))
{
$bestBeforeDate = $request->getQueryParams()['bestbeforedate'];
}
$transactionType = StockService::TRANSACTION_TYPE_PURCHASE;
if (isset($request->getQueryParams()['transactiontype']) && !empty($request->getQueryParams()['transactiontype']))
{
$transactionType = $request->getQueryParams()['transactiontype'];
}
return $this->ApiResponse(array('success' => $this->StockService->AddProduct($args['productId'], $args['amount'], $bestBeforeDate, $transactionType)));
}
public function ConsumeProduct(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
$spoiled = false;
if (isset($request->getQueryParams()['spoiled']) && !empty($request->getQueryParams()['spoiled']) && $request->getQueryParams()['spoiled'] == '1')
{
$spoiled = true;
}
$transactionType = StockService::TRANSACTION_TYPE_CONSUME;
if (isset($request->getQueryParams()['transactiontype']) && !empty($request->getQueryParams()['transactiontype']))
{
$transactionType = $request->getQueryParams()['transactiontype'];
}
return $this->ApiResponse(array('success' => $this->StockService->ConsumeProduct($args['productId'], $args['amount'], $spoiled, $transactionType)));
}
public function InventoryProduct(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
$bestBeforeDate = date('Y-m-d');
if (isset($request->getQueryParams()['bestbeforedate']) && !empty($request->getQueryParams()['bestbeforedate']))
{
$bestBeforeDate = $request->getQueryParams()['bestbeforedate'];
}
return $this->ApiResponse(array('success' => $this->StockService->InventoryProduct($args['productId'], $args['newAmount'], $bestBeforeDate)));
}
public function CurrentStock(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
return $this->ApiResponse($this->StockService->GetCurrentStock());
}
public function AddmissingProductsToShoppingList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
$this->StockService->AddMissingProductsToShoppingList();
return $this->ApiResponse(array('success' => true));
}
}

View File

@@ -0,0 +1,155 @@
<?php
namespace Grocy\Controllers;
use \Grocy\Services\StockService;
class StockController extends BaseController
{
public function __construct(\Slim\Container $container)
{
parent::__construct($container);
$this->StockService = new StockService();
}
protected $StockService;
public function Overview(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
return $this->AppContainer->view->render($response, 'stockoverview', [
'products' => $this->Database->products(),
'quantityunits' => $this->Database->quantity_units(),
'currentStock' => $this->StockService->GetCurrentStock(),
'missingProducts' => $this->StockService->GetMissingProducts()
]);
}
public function Purchase(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
return $this->AppContainer->view->render($response, 'purchase', [
'products' => $this->Database->products()
]);
}
public function Consume(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
return $this->AppContainer->view->render($response, 'consume', [
'products' => $this->Database->products()
]);
}
public function Inventory(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
return $this->AppContainer->view->render($response, 'inventory', [
'products' => $this->Database->products()
]);
}
public function ShoppingList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
return $this->AppContainer->view->render($response, 'shoppinglist', [
'listItems' => $this->Database->shopping_list(),
'products' => $this->Database->products(),
'quantityunits' => $this->Database->quantity_units(),
'missingProducts' => $this->StockService->GetMissingProducts()
]);
}
public function ProductsList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
return $this->AppContainer->view->render($response, 'products', [
'products' => $this->Database->products(),
'locations' => $this->Database->locations(),
'quantityunits' => $this->Database->quantity_units()
]);
}
public function LocationsList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
return $this->AppContainer->view->render($response, 'locations', [
'locations' => $this->Database->locations()
]);
}
public function QuantityUnitsList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
return $this->AppContainer->view->render($response, 'quantityunits', [
'quantityunits' => $this->Database->quantity_units()
]);
}
public function ProductEditForm(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
if ($args['productId'] == 'new')
{
return $this->AppContainer->view->render($response, 'productform', [
'locations' => $this->Database->locations(),
'quantityunits' => $this->Database->quantity_units(),
'mode' => 'create'
]);
}
else
{
return $this->AppContainer->view->render($response, 'productform', [
'product' => $this->Database->products($args['productId']),
'locations' => $this->Database->locations(),
'quantityunits' => $this->Database->quantity_units(),
'mode' => 'edit'
]);
}
}
public function LocationEditForm(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
if ($args['locationId'] == 'new')
{
return $this->AppContainer->view->render($response, 'locationform', [
'mode' => 'create'
]);
}
else
{
return $this->AppContainer->view->render($response, 'locationform', [
'location' => $this->Database->locations($args['locationId']),
'mode' => 'edit'
]);
}
}
public function QuantityUnitEditForm(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
if ($args['quantityunitId'] == 'new')
{
return $this->AppContainer->view->render($response, 'quantityunitform', [
'mode' => 'create'
]);
}
else
{
return $this->AppContainer->view->render($response, 'quantityunitform', [
'quantityunit' => $this->Database->quantity_units($args['quantityunitId']),
'mode' => 'edit'
]);
}
}
public function ShoppingListItemEditForm(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
if ($args['itemId'] == 'new')
{
return $this->AppContainer->view->render($response, 'shoppinglistform', [
'products' => $this->Database->products(),
'mode' => 'create'
]);
}
else
{
return $this->AppContainer->view->render($response, 'shoppinglistform', [
'listItem' => $this->Database->shopping_list($args['itemId']),
'products' => $this->Database->products(),
'mode' => 'edit'
]);
}
}
}

1
data/.gitignore vendored
View File

@@ -1,2 +1,3 @@
*
!.gitignore
!viewcache

2
data/viewcache/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
*
!.gitignore

103
grocy.js
View File

@@ -1,103 +0,0 @@
var Grocy = { };
$(function()
{
var menuItem = $('.nav').find("[data-nav-for-page='" + Grocy.ContentPage + "']");
menuItem.addClass('active');
$.timeago.settings.allowFuture = true;
$('time.timeago').timeago();
});
Grocy.FetchJson = function(url, success, error)
{
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function()
{
if (xhr.readyState === XMLHttpRequest.DONE)
{
if (xhr.status === 200)
{
if (success)
{
success(JSON.parse(xhr.responseText));
}
}
else
{
if (error)
{
error(xhr);
}
}
}
};
xhr.open('GET', url, true);
xhr.send();
};
Grocy.PostJson = function(url, jsonData, success, error)
{
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function()
{
if (xhr.readyState === XMLHttpRequest.DONE)
{
if (xhr.status === 200)
{
if (success)
{
success(JSON.parse(xhr.responseText));
}
}
else
{
if (error)
{
error(xhr);
}
}
}
};
xhr.open('POST', url, true);
xhr.setRequestHeader('Content-type', 'application/json');
xhr.send(JSON.stringify(jsonData));
};
Grocy.EmptyElementWhenMatches = function(selector, text)
{
if ($(selector).text() === text)
{
$(selector).text('');
}
};
String.prototype.contains = function(search)
{
return this.toLowerCase().indexOf(search.toLowerCase()) !== -1;
};
Grocy.GetUriParam = function(key)
{
var currentUri = decodeURIComponent(window.location.search.substring(1));
var vars = currentUri.split('&');
for (i = 0; i < vars.length; i++)
{
var currentParam = vars[i].split('=');
if (currentParam[0] === key)
{
return currentParam[1] === undefined ? true : currentParam[1];
}
}
};
Grocy.Wait = function(ms)
{
return new Promise(resolve => setTimeout(resolve, ms));
}

BIN
grocy.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

17
helpers/UrlManager.php Normal file
View File

@@ -0,0 +1,17 @@
<?php
namespace Grocy\Helpers;
class UrlManager
{
public function __construct(string $basePath) {
$this->BasePath = $basePath;
}
protected $BasePath;
public function ConstructUrl($relativePath)
{
return rtrim($this->BasePath, '/') . $relativePath;
}
}

63
helpers/extensions.php Normal file
View File

@@ -0,0 +1,63 @@
<?php
function FindObjectInArrayByPropertyValue($array, $propertyName, $propertyValue)
{
foreach($array as $object)
{
if($object->{$propertyName} == $propertyValue)
{
return $object;
}
}
return null;
}
function FindAllObjectsInArrayByPropertyValue($array, $propertyName, $propertyValue, $operator = '==')
{
$returnArray = array();
foreach($array as $object)
{
switch($operator)
{
case '==':
if($object->{$propertyName} == $propertyValue)
{
$returnArray[] = $object;
}
break;
case '>':
if($object->{$propertyName} > $propertyValue)
{
$returnArray[] = $object;
}
break;
case '<':
if($object->{$propertyName} < $propertyValue)
{
$returnArray[] = $object;
}
break;
}
}
return $returnArray;
}
function SumArrayValue($array, $propertyName)
{
$sum = 0;
foreach($array as $object)
{
$sum += $object->{$propertyName};
}
return $sum;
}
function GetClassConstants($className)
{
$r = new ReflectionClass($className);
return $r->getConstants();
}

531
index.php
View File

@@ -1,531 +0,0 @@
<?php
use \Psr\Http\Message\ServerRequestInterface as Request;
use \Psr\Http\Message\ResponseInterface as Response;
use Slim\Views\PhpRenderer;
require_once __DIR__ . '/vendor/autoload.php';
require_once __DIR__ . '/data/config.php';
require_once __DIR__ . '/Grocy.php';
require_once __DIR__ . '/GrocyDbMigrator.php';
require_once __DIR__ . '/GrocyDemoDataGenerator.php';
require_once __DIR__ . '/GrocyLogicStock.php';
require_once __DIR__ . '/GrocyLogicHabits.php';
require_once __DIR__ . '/GrocyLogicBatteries.php';
require_once __DIR__ . '/GrocyPhpHelper.php';
$app = new \Slim\App;
if (PHP_SAPI !== 'cli')
{
$app = new \Slim\App(new \Slim\Container([
'settings' => [
'displayErrorDetails' => true,
'determineRouteBeforeAppMiddleware' => true
],
]));
$container = $app->getContainer();
$container['renderer'] = new PhpRenderer('./views');
}
if (PHP_SAPI === 'cli')
{
$app->add(new \pavlakis\cli\CliRequest());
}
if (!Grocy::IsDemoInstallation())
{
$sessionMiddleware = function(Request $request, Response $response, callable $next)
{
$route = $request->getAttribute('route');
$routeName = $route->getName();
if ((!isset($_COOKIE['grocy_session']) || !Grocy::IsValidSession($_COOKIE['grocy_session'])) && $routeName !== 'login')
{
$response = $response->withRedirect('/login');
}
else
{
$response = $next($request, $response);
}
return $response;
};
$app->add($sessionMiddleware);
}
$db = Grocy::GetDbConnection();
$app->get('/login', function(Request $request, Response $response)
{
return $this->renderer->render($response, '/layout.php', [
'title' => 'Login',
'contentPage' => 'login.php'
]);
})->setName('login');
$app->post('/login', function(Request $request, Response $response)
{
$postParams = $request->getParsedBody();
if (isset($postParams['username']) && isset($postParams['password']))
{
if ($postParams['username'] === HTTP_USER && $postParams['password'] === HTTP_PASSWORD)
{
$sessionKey = Grocy::CreateSession();
setcookie('grocy_session', $sessionKey, time()+2592000); //30 days
return $response->withRedirect('/');
}
else
{
return $response->withRedirect('/login?invalid=true');
}
}
else
{
return $response->withRedirect('/login?invalid=true');
}
})->setName('login');
$app->get('/logout', function(Request $request, Response $response)
{
Grocy::RemoveSession($_COOKIE['grocy_session']);
return $response->withRedirect('/');
});
$app->get('/', function(Request $request, Response $response) use($db)
{
$db = Grocy::GetDbConnection(true); //For database schema migration
return $response->withRedirect('/stockoverview');
});
$app->get('/stockoverview', function(Request $request, Response $response) use($db)
{
return $this->renderer->render($response, '/layout.php', [
'title' => 'Stock overview',
'contentPage' => 'stockoverview.php',
'products' => $db->products(),
'quantityunits' => $db->quantity_units(),
'currentStock' => GrocyLogicStock::GetCurrentStock(),
'missingProducts' => GrocyLogicStock::GetMissingProducts()
]);
});
$app->get('/habitsoverview', function(Request $request, Response $response) use($db)
{
return $this->renderer->render($response, '/layout.php', [
'title' => 'Habits overview',
'contentPage' => 'habitsoverview.php',
'habits' => $db->habits(),
'currentHabits' => GrocyLogicHabits::GetCurrentHabits(),
]);
});
$app->get('/batteriesoverview', function(Request $request, Response $response) use($db)
{
return $this->renderer->render($response, '/layout.php', [
'title' => 'Batteries overview',
'contentPage' => 'batteriesoverview.php',
'batteries' => $db->batteries(),
'current' => GrocyLogicBatteries::GetCurrent(),
]);
});
$app->get('/purchase', function(Request $request, Response $response) use($db)
{
return $this->renderer->render($response, '/layout.php', [
'title' => 'Purchase',
'contentPage' => 'purchase.php',
'products' => $db->products()
]);
});
$app->get('/consume', function(Request $request, Response $response) use($db)
{
return $this->renderer->render($response, '/layout.php', [
'title' => 'Consume',
'contentPage' => 'consume.php',
'products' => $db->products()
]);
});
$app->get('/inventory', function(Request $request, Response $response) use($db)
{
return $this->renderer->render($response, '/layout.php', [
'title' => 'Inventory',
'contentPage' => 'inventory.php',
'products' => $db->products()
]);
});
$app->get('/shoppinglist', function(Request $request, Response $response) use($db)
{
return $this->renderer->render($response, '/layout.php', [
'title' => 'Shopping list',
'contentPage' => 'shoppinglist.php',
'listItems' => $db->shopping_list(),
'products' => $db->products(),
'quantityunits' => $db->quantity_units(),
'missingProducts' => GrocyLogicStock::GetMissingProducts()
]);
});
$app->get('/habittracking', function(Request $request, Response $response) use($db)
{
return $this->renderer->render($response, '/layout.php', [
'title' => 'Habit tracking',
'contentPage' => 'habittracking.php',
'habits' => $db->habits()
]);
});
$app->get('/batterytracking', function(Request $request, Response $response) use($db)
{
return $this->renderer->render($response, '/layout.php', [
'title' => 'Battery tracking',
'contentPage' => 'batterytracking.php',
'batteries' => $db->batteries()
]);
});
$app->get('/products', function(Request $request, Response $response) use($db)
{
return $this->renderer->render($response, '/layout.php', [
'title' => 'Products',
'contentPage' => 'products.php',
'products' => $db->products(),
'locations' => $db->locations(),
'quantityunits' => $db->quantity_units()
]);
});
$app->get('/locations', function(Request $request, Response $response) use($db)
{
return $this->renderer->render($response, '/layout.php', [
'title' => 'Locations',
'contentPage' => 'locations.php',
'locations' => $db->locations()
]);
});
$app->get('/quantityunits', function(Request $request, Response $response) use($db)
{
return $this->renderer->render($response, '/layout.php', [
'title' => 'Quantity units',
'contentPage' => 'quantityunits.php',
'quantityunits' => $db->quantity_units()
]);
});
$app->get('/habits', function(Request $request, Response $response) use($db)
{
return $this->renderer->render($response, '/layout.php', [
'title' => 'Habits',
'contentPage' => 'habits.php',
'habits' => $db->habits()
]);
});
$app->get('/batteries', function(Request $request, Response $response) use($db)
{
return $this->renderer->render($response, '/layout.php', [
'title' => 'Batteries',
'contentPage' => 'batteries.php',
'batteries' => $db->batteries()
]);
});
$app->get('/product/{productId}', function(Request $request, Response $response, $args) use($db)
{
if ($args['productId'] == 'new')
{
return $this->renderer->render($response, '/layout.php', [
'title' => 'Create product',
'contentPage' => 'productform.php',
'locations' => $db->locations(),
'quantityunits' => $db->quantity_units(),
'mode' => 'create'
]);
}
else
{
return $this->renderer->render($response, '/layout.php', [
'title' => 'Edit product',
'contentPage' => 'productform.php',
'product' => $db->products($args['productId']),
'locations' => $db->locations(),
'quantityunits' => $db->quantity_units(),
'mode' => 'edit'
]);
}
});
$app->get('/location/{locationId}', function(Request $request, Response $response, $args) use($db)
{
if ($args['locationId'] == 'new')
{
return $this->renderer->render($response, '/layout.php', [
'title' => 'Create location',
'contentPage' => 'locationform.php',
'mode' => 'create'
]);
}
else
{
return $this->renderer->render($response, '/layout.php', [
'title' => 'Edit location',
'contentPage' => 'locationform.php',
'location' => $db->locations($args['locationId']),
'mode' => 'edit'
]);
}
});
$app->get('/quantityunit/{quantityunitId}', function(Request $request, Response $response, $args) use($db)
{
if ($args['quantityunitId'] == 'new')
{
return $this->renderer->render($response, '/layout.php', [
'title' => 'Create quantity unit',
'contentPage' => 'quantityunitform.php',
'mode' => 'create'
]);
}
else
{
return $this->renderer->render($response, '/layout.php', [
'title' => 'Edit quantity unit',
'contentPage' => 'quantityunitform.php',
'quantityunit' => $db->quantity_units($args['quantityunitId']),
'mode' => 'edit'
]);
}
});
$app->get('/habit/{habitId}', function(Request $request, Response $response, $args) use($db)
{
if ($args['habitId'] == 'new')
{
return $this->renderer->render($response, '/layout.php', [
'title' => 'Create habit',
'contentPage' => 'habitform.php',
'periodTypes' => GrocyPhpHelper::GetClassConstants('GrocyLogicHabits'),
'mode' => 'create'
]);
}
else
{
return $this->renderer->render($response, '/layout.php', [
'title' => 'Edit habit',
'contentPage' => 'habitform.php',
'habit' => $db->habits($args['habitId']),
'periodTypes' => GrocyPhpHelper::GetClassConstants('GrocyLogicHabits'),
'mode' => 'edit'
]);
}
});
$app->get('/battery/{batteryId}', function(Request $request, Response $response, $args) use($db)
{
if ($args['batteryId'] == 'new')
{
return $this->renderer->render($response, '/layout.php', [
'title' => 'Create battery',
'contentPage' => 'batteryform.php',
'mode' => 'create'
]);
}
else
{
return $this->renderer->render($response, '/layout.php', [
'title' => 'Edit battery',
'contentPage' => 'batteryform.php',
'battery' => $db->batteries($args['batteryId']),
'mode' => 'edit'
]);
}
});
$app->get('/shoppinglistitem/{itemId}', function(Request $request, Response $response, $args) use($db)
{
if ($args['itemId'] == 'new')
{
return $this->renderer->render($response, '/layout.php', [
'title' => 'Add shopping list item',
'contentPage' => 'shoppinglistform.php',
'products' => $db->products(),
'mode' => 'create'
]);
}
else
{
return $this->renderer->render($response, '/layout.php', [
'title' => 'Edit shopping list item',
'contentPage' => 'shoppinglistform.php',
'listItem' => $db->shopping_list($args['itemId']),
'products' => $db->products(),
'mode' => 'edit'
]);
}
});
$app->group('/api', function() use($db)
{
$this->get('/get-objects/{entity}', function(Request $request, Response $response, $args) use($db)
{
echo json_encode($db->{$args['entity']}());
});
$this->get('/get-object/{entity}/{objectId}', function(Request $request, Response $response, $args) use($db)
{
echo json_encode($db->{$args['entity']}($args['objectId']));
});
$this->post('/add-object/{entity}', function(Request $request, Response $response, $args) use($db)
{
$newRow = $db->{$args['entity']}()->createRow($request->getParsedBody());
$newRow->save();
$success = $newRow->isClean();
echo json_encode(array('success' => $success));
});
$this->post('/edit-object/{entity}/{objectId}', function(Request $request, Response $response, $args) use($db)
{
$row = $db->{$args['entity']}($args['objectId']);
$row->update($request->getParsedBody());
$success = $row->isClean();
echo json_encode(array('success' => $success));
});
$this->get('/delete-object/{entity}/{objectId}', function(Request $request, Response $response, $args) use($db)
{
$row = $db->{$args['entity']}($args['objectId']);
$row->delete();
$success = $row->isClean();
echo json_encode(array('success' => $success));
});
$this->get('/stock/add-product/{productId}/{amount}', function(Request $request, Response $response, $args)
{
$bestBeforeDate = date('Y-m-d');
if (isset($request->getQueryParams()['bestbeforedate']) && !empty($request->getQueryParams()['bestbeforedate']))
{
$bestBeforeDate = $request->getQueryParams()['bestbeforedate'];
}
$transactionType = GrocyLogicStock::TRANSACTION_TYPE_PURCHASE;
if (isset($request->getQueryParams()['transactiontype']) && !empty($request->getQueryParams()['transactiontype']))
{
$transactionType = $request->getQueryParams()['transactiontype'];
}
echo json_encode(array('success' => GrocyLogicStock::AddProduct($args['productId'], $args['amount'], $bestBeforeDate, $transactionType)));
});
$this->get('/stock/consume-product/{productId}/{amount}', function(Request $request, Response $response, $args)
{
$spoiled = false;
if (isset($request->getQueryParams()['spoiled']) && !empty($request->getQueryParams()['spoiled']) && $request->getQueryParams()['spoiled'] == '1')
{
$spoiled = true;
}
$transactionType = GrocyLogicStock::TRANSACTION_TYPE_CONSUME;
if (isset($request->getQueryParams()['transactiontype']) && !empty($request->getQueryParams()['transactiontype']))
{
$transactionType = $request->getQueryParams()['transactiontype'];
}
echo json_encode(array('success' => GrocyLogicStock::ConsumeProduct($args['productId'], $args['amount'], $spoiled, $transactionType)));
});
$this->get('/stock/inventory-product/{productId}/{newAmount}', function(Request $request, Response $response, $args)
{
$bestBeforeDate = date('Y-m-d');
if (isset($request->getQueryParams()['bestbeforedate']) && !empty($request->getQueryParams()['bestbeforedate']))
{
$bestBeforeDate = $request->getQueryParams()['bestbeforedate'];
}
echo json_encode(array('success' => GrocyLogicStock::InventoryProduct($args['productId'], $args['newAmount'], $bestBeforeDate)));
});
$this->get('/stock/get-product-details/{productId}', function(Request $request, Response $response, $args)
{
echo json_encode(GrocyLogicStock::GetProductDetails($args['productId']));
});
$this->get('/stock/get-current-stock', function(Request $request, Response $response)
{
echo json_encode(GrocyLogicStock::GetCurrentStock());
});
$this->get('/stock/add-missing-products-to-shoppinglist', function(Request $request, Response $response)
{
GrocyLogicStock::AddMissingProductsToShoppingList();
echo json_encode(array('success' => true));
});
$this->get('/habits/track-habit/{habitId}', function(Request $request, Response $response, $args)
{
$trackedTime = date('Y-m-d H:i:s');
if (isset($request->getQueryParams()['tracked_time']) && !empty($request->getQueryParams()['tracked_time']))
{
$trackedTime = $request->getQueryParams()['tracked_time'];
}
echo json_encode(array('success' => GrocyLogicHabits::TrackHabit($args['habitId'], $trackedTime)));
});
$this->get('/habits/get-habit-details/{habitId}', function(Request $request, Response $response, $args)
{
echo json_encode(GrocyLogicHabits::GetHabitDetails($args['habitId']));
});
$this->get('/batteries/track-charge-cycle/{batteryId}', function(Request $request, Response $response, $args)
{
$trackedTime = date('Y-m-d H:i:s');
if (isset($request->getQueryParams()['tracked_time']) && !empty($request->getQueryParams()['tracked_time']))
{
$trackedTime = $request->getQueryParams()['tracked_time'];
}
echo json_encode(array('success' => GrocyLogicBatteries::TrackChargeCycle($args['batteryId'], $trackedTime)));
});
$this->get('/batteries/get-battery-details/{batteryId}', function(Request $request, Response $response, $args)
{
echo json_encode(GrocyLogicBatteries::GetBatteryDetails($args['batteryId']));
});
})->add(function($request, $response, $next)
{
$response = $next($request, $response);
return $response->withHeader('Content-Type', 'application/json');
});
$app->group('/cli', function()
{
$this->get('/recreatedemo', function(Request $request, Response $response)
{
if (Grocy::IsDemoInstallation())
{
GrocyDemoDataGenerator::RecreateDemo();
}
});
})->add(function($request, $response, $next)
{
$response = $next($request, $response);
if (PHP_SAPI !== 'cli')
{
echo 'Please call this only from CLI';
return $response->withHeader('Content-Type', 'text/plain')->withStatus(400);
}
return $response->withHeader('Content-Type', 'text/plain');
});
$app->run();

148
localization/de.php Normal file
View File

@@ -0,0 +1,148 @@
<?php
return array(
'Stock overview' => 'Bestand',
'#1 products with #2 units in stock' => '#1 Produkte (#2 Einheiten) vorrätig',
'#1 products expiring within the next #2 days' => '#1 Produkte laufen innerhalb der nächsten #2 Tage ab',
'#1 products are already expired' => '#1 Produkte sind bereits abgelaufen',
'#1 products are below defined min. stock amount' => '#1 Produkte sind unter Mindestbestand',
'Product' => 'Produkt',
'Amount' => 'Menge',
'Next best before date' => 'Nächstes MHD',
'Logout' => 'Abmelden',
'Habits overview' => 'Gewohnheiten',
'Batteries overview' => 'Batterien',
'Purchase' => 'Einkauf',
'Consume' => 'Verbrauch',
'Inventory' => 'Inventur',
'Shopping list' => 'Einkaufszettel',
'Habit tracking' => 'Gewohnheit-Ausführung',
'Battery tracking' => 'Batterie-Ladzyklus',
'Products' => 'Produkte',
'Locations' => 'Standorte',
'Quantity units' => 'Mengeneinheiten',
'Habits' => 'Gewohnheiten',
'Batteries' => 'Batterien',
'Habit' => 'Gewohnheit',
'Next estimated tracking' => 'Nächste geplante Ausführung',
'Last tracked' => 'Zuletzt ausgeführt',
'Battery' => 'Batterie',
'Last charged' => 'Zuletzt geladen',
'Next planned charge cycle' => 'Nächster geplanter Ladezyklus',
'Best before' => 'MHD',
'OK' => 'OK',
'Product overview' => 'Produktübersicht',
'Stock quantity unit' => 'Mengeneinheit Bestand',
'Stock amount' => 'Bestand',
'Last purchased' => 'Zuletzt gekauft',
'Last used' => 'Zuletzt benutzt',
'Spoiled' => 'Verdorben',
'Barcode lookup is disabled' => 'Barcode-Suche ist deaktiviert',
'will be added to the list of barcodes for the selected product on submit' => 'wird der Liste der Barcodes für das ausgewählte Produkt beim Speichern hinzugefügt',
'New amount' => 'Neue Menge',
'Note' => 'Notiz',
'Tracked time' => 'Ausführungszeit',
'Habit overview' => 'Gewohnheit Übersicht',
'Tracked count' => 'Ausführungsanzahl',
'Battery overview' => 'Batterie Übersicht',
'Charge cycles count' => 'Ladezyklen',
'Create shopping list item' => 'Einkaufszettel Eintrag erstellen',
'Edit shopping list item' => 'Einkaufszettel Eintrag bearbeiten',
'#1 units were automatically added and will apply in addition to the amount entered here' => '#1 Einheiten wurden automatisch hinzugefügt und gelten zusätzlich der hier eingegebenen Menge',
'Save' => 'Speichern',
'Add' => 'Hinzufügen',
'Name' => 'Name',
'Location' => 'Standort',
'Min. stock amount' => 'Mindestbestand',
'QU purchase' => 'ME Einkauf',
'QU stock' => 'ME Bestand',
'QU factor' => 'ME-Faktor',
'Description' => 'Beschreibung',
'Create product' => 'Produkt erstellen',
'Barcode(s)' => 'Barcode(s)',
'Minimum stock amount' => 'Mindestbestand',
'Default best before days' => 'Standard Haltbarkeit in Tagen',
'Quantity unit purchase' => 'Mengeneinheit Einkauf',
'Quantity unit stock' => 'Mengeneinheit Bestand',
'Factor purchase to stock quantity unit' => 'Faktor Mengeneinheit Einkauf zu Mengeneinheit Bestand',
'Create location' => 'Standort erstellen',
'Create quantity unit' => 'Mengeneinheit erstellen',
'Period type' => 'Periodentyp',
'Period days' => 'Tage/Periode',
'Create habit' => 'Gewohnheit erstellen',
'Used in' => 'Benutzt in',
'Create battery' => 'Batterie erstellen',
'Edit battery' => 'Batterie bearbeiten',
'Edit habit' => 'Gewohnheit bearbeiten',
'Edit quantity unit' => 'Mengeneinheit bearbeiten',
'Edit product' => 'Produkt bearbeiten',
'Edit location' => 'Standort bearbeiten',
'Record data' => 'Daten erfassen',
'Manage master data' => 'Stammdaten verwalten',
'This will apply to added products' => 'Dies gilt für hinzugefügte Produkte',
'never' => 'nie',
'Add products that are below defined min. stock amount' => 'Produkte unter Mindestbestand hinzufügen',
'For purchases this amount of days will be added to today for the best before date suggestion' => 'Bei Einkäufen wird hierauf basierend das MHD vorausgefüllt',
'This means 1 #1 purchased will be converted into #2 #3 in stock' => 'Das bedeutet 1 #1 im Einkauf entsprechen #2 #3 im Bestand',
'Login' => 'Anmelden',
'Username' => 'Benutzername',
'Password' => 'Passwort',
'Invalid credentials, please try again' => 'Ungültige Zugangsdaten, bitte versuche es erneut',
'Are you sure to delete battery "#1"?' => 'Battery "#1" wirklich löschen?',
'Yes' => 'Ja',
'No' => 'Nein',
'Are you sure to delete habit "#1"?' => 'Gewohnheit "#1" wirklich löschen?',
'"#1" could not be resolved to a product, how do you want to proceed?' => '"#1" konnte nicht zu einem Produkt aufgelöst werden, wie möchtest du weiter machen?',
'Create or assign product' => 'Produkt erstellen oder verknüpfen',
'Cancel' => 'Abbrechen',
'Add as new product' => 'Als neues Produkt hinzufügen',
'Add as barcode to existing product' => 'Barcode vorhandenem Produkt zuweisen',
'Add as new product and prefill barcode' => 'Neues Produkt erstellen und Barcode vorbelegen',
'Are you sure to delete quantity unit "#1"?' => 'Mengeneinheit "#1" wirklich löschen?',
'Are you sure to delete product "#1"?' => 'Produkt "#1" wirklich löschen?',
'Are you sure to delete location "#1"?' => 'Standort "#1" wirklich löschen?',
//Constants
'manually' => 'Manuell',
'dynamic-regular' => 'Dynamisch regelmäßig',
//Technical component translations
'timeago_locale' => 'de',
'timeago_nan' => 'vor NaN Jahren',
'moment_locale' => 'de',
'bootstrap_datepicker_locale' => 'de',
'datatables_localization' => '{"sEmptyTable":"Keine Daten in der Tabelle vorhanden","sInfo":"_START_ bis _END_ von _TOTAL_ Einträgen","sInfoEmpty":"Keine Daten vorhanden","sInfoFiltered":"(gefiltert von _MAX_ Einträgen)","sInfoPostFix":"","sInfoThousands":".","sLengthMenu":"_MENU_ Einträge anzeigen","sLoadingRecords":"Wird geladen ..","sProcessing":"Bitte warten ..","sSearch":"Suchen","sZeroRecords":"Keine Einträge vorhanden","oPaginate":{"sFirst":"Erste","sPrevious":"Zurück","sNext":"Nächste","sLast":"Letzte"},"oAria":{"sSortAscending":": aktivieren, um Spalte aufsteigend zu sortieren","sSortDescending":": aktivieren, um Spalte absteigend zu sortieren"},"select":{"rows":{"0":"Zum Auswählen auf eine Zeile klicken","1":"1 Zeile ausgewählt","_":"%d Zeilen ausgewählt"}},"buttons":{"print":"Drucken","colvis":"Spalten","copy":"Kopieren","copyTitle":"In Zwischenablage kopieren","copyKeys":"Taste <i>ctrl</i> oder <i>⌘</i> + <i>C</i> um Tabelle<br>in Zwischenspeicher zu kopieren.<br><br>Um abzubrechen die Nachricht anklicken oder Escape drücken.","copySuccess":{"1":"1 Spalte kopiert","_":"%d Spalten kopiert"}}}',
//Demo data
'Cookies' => 'Cookies',
'Chocolate' => 'Schokolade',
'Pantry' => 'Vorratskammer',
'Candy cupboard' => 'Süßigkeitenschrank',
'Tinned food cupboard' => 'Konservenschrank',
'Fridge' => 'Kühlschrank',
'Piece' => 'Stück',
'Pack' => 'Packung',
'Glass' => 'Glas',
'Tin' => 'Dose',
'Can' => 'Becher',
'Bunch' => 'Bund',
'Gummy bears' => 'Gummibärchen',
'Crisps' => 'Chips',
'Eggs' => 'Eier',
'Noodles' => 'Nudeln',
'Pickles' => 'Essiggurken',
'Gulash soup' => 'Gulaschsuppe',
'Yogurt' => 'Joghurt',
'Cheese' => 'Käse',
'Cold cuts' => 'Aufschnitt',
'Paprika' => 'Paprika',
'Cucumber' => 'Gurke',
'Radish' => 'Radieschen',
'Tomato' => 'Tomaten',
'Changed towels in the bathroom' => 'Handtücher im Bad gewechselt',
'Cleaned the kitchen floor' => 'Küchenboden gewischt',
'Warranty ends' => 'Garantie endet',
'TV remote control' => 'TV Fernbedienung',
'Alarm clock' => 'Wecker',
'Heat remote control' => 'Fernbedienung Heizung'
);

14
localization/en.php Normal file
View File

@@ -0,0 +1,14 @@
<?php
return array(
//Constants
'manually' => 'Manually',
'dynamic-regular' => 'Dynamic regular',
//Technical component translations
'timeago_locale' => 'en',
'timeago_nan' => 'NaN years ago',
'moment_locale' => '',
'bootstrap_datepicker_locale' => '',
'datatables_localization' => '{"sEmptyTable":"No data available in table","sInfo":"Showing _START_ to _END_ of _TOTAL_ entries","sInfoEmpty":"Showing 0 to 0 of 0 entries","sInfoFiltered":"(filtered from _MAX_ total entries)","sInfoPostFix":"","sInfoThousands":",","sLengthMenu":"Show _MENU_ entries","sLoadingRecords":"Loading...","sProcessing":"Processing...","sSearch":"Search:","sZeroRecords":"No matching records found","oPaginate":{"sFirst":"First","sLast":"Last","sNext":"Next","sPrevious":"Previous"},"oAria":{"sSortAscending":": activate to sort column ascending","sSortDescending":": activate to sort column descending"}}'
);

View File

@@ -0,0 +1,12 @@
<?php
namespace Grocy\Middleware;
class BaseMiddleware
{
public function __construct(\Slim\Container $container) {
$this->AppContainer = $container;
}
protected $AppContainer;
}

View File

@@ -0,0 +1,20 @@
<?php
namespace Grocy\Middleware;
class CliMiddleware extends BaseMiddleware
{
public function __invoke(\Slim\Http\Request $request, \Slim\Http\Response $response, callable $next)
{
if (PHP_SAPI !== 'cli')
{
$response->write('Please call this only from CLI');
return $response->withHeader('Content-Type', 'text/plain')->withStatus(400);
}
else
{
$response = $next($request, $response, $next);
return $response->withHeader('Content-Type', 'text/plain');
}
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace Grocy\Middleware;
class JsonMiddleware extends BaseMiddleware
{
public function __invoke(\Slim\Http\Request $request, \Slim\Http\Response $response, callable $next)
{
$response = $next($request, $response, $next);
return $response->withHeader('Content-Type', 'application/json');
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace Grocy\Middleware;
use \Grocy\Services\SessionService;
class SessionAuthMiddleware extends BaseMiddleware
{
public function __invoke(\Slim\Http\Request $request, \Slim\Http\Response $response, callable $next)
{
$route = $request->getAttribute('route');
$routeName = $route->getName();
if ($routeName === 'root')
{
$response = $next($request, $response);
}
else
{
$sessionService = new SessionService();
if ((!isset($_COOKIE['grocy_session']) || !$sessionService->IsValidSession($_COOKIE['grocy_session'])) && $routeName !== 'login')
{
$response = $response->withRedirect($this->AppContainer->UrlManager->ConstructUrl('/login'));
}
else
{
$response = $next($request, $response);
}
}
return $response;
}
}

13
migrations/0001.sql Normal file
View File

@@ -0,0 +1,13 @@
CREATE TABLE products (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
name TEXT NOT NULL UNIQUE,
description TEXT,
location_id INTEGER NOT NULL,
qu_id_purchase INTEGER NOT NULL,
qu_id_stock INTEGER NOT NULL,
qu_factor_purchase_to_stock REAL NOT NULL,
barcode TEXT,
min_stock_amount INTEGER NOT NULL DEFAULT 0,
default_best_before_days INTEGER NOT NULL DEFAULT 0,
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
)

6
migrations/0002.sql Normal file
View File

@@ -0,0 +1,6 @@
CREATE TABLE locations (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
name TEXT NOT NULL UNIQUE,
description TEXT,
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
)

6
migrations/0003.sql Normal file
View File

@@ -0,0 +1,6 @@
CREATE TABLE quantity_units (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
name TEXT NOT NULL UNIQUE,
description TEXT,
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
)

9
migrations/0004.sql Normal file
View File

@@ -0,0 +1,9 @@
CREATE TABLE stock (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
product_id INTEGER NOT NULL,
amount INTEGER NOT NULL,
best_before_date DATE,
purchased_date DATE DEFAULT (datetime('now', 'localtime')),
stock_id TEXT NOT NULL,
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
)

12
migrations/0005.sql Normal file
View File

@@ -0,0 +1,12 @@
CREATE TABLE stock_log (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
product_id INTEGER NOT NULL,
amount INTEGER NOT NULL,
best_before_date DATE,
purchased_date DATE,
used_date DATE,
spoiled INTEGER NOT NULL DEFAULT 0,
stock_id TEXT NOT NULL,
transaction_type TEXT NOT NULL,
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
)

19
migrations/0006.sql Normal file
View File

@@ -0,0 +1,19 @@
INSERT INTO locations
(name, description)
VALUES
('DefaultLocation', 'This is the first default location, edit or delete it');
INSERT INTO quantity_units
(name, description)
VALUES
('DefaultQuantityUnit', 'This is the first default quantity unit, edit or delete it');
INSERT INTO products
(name, description, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock)
VALUES
('DefaultProduct1', 'This is the first default product, edit or delete it', 1, 1, 1, 1);
INSERT INTO products
(name, description, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock)
VALUES
('DefaultProduct2', 'This is the second default product, edit or delete it', 1, 1, 1, 1);

9
migrations/0007.sql Normal file
View File

@@ -0,0 +1,9 @@
CREATE VIEW stock_missing_products
AS
SELECT p.id, MAX(p.name) AS name, p.min_stock_amount - IFNULL(SUM(s.amount), 0) AS amount_missing
FROM products p
LEFT JOIN stock s
ON p.id = s.product_id
WHERE p.min_stock_amount != 0
GROUP BY p.id
HAVING IFNULL(SUM(s.amount), 0) < p.min_stock_amount

6
migrations/0008.sql Normal file
View File

@@ -0,0 +1,6 @@
CREATE VIEW stock_current
AS
SELECT product_id, SUM(amount) AS amount, MIN(best_before_date) AS best_before_date
FROM stock
GROUP BY product_id
ORDER BY MIN(best_before_date) ASC

7
migrations/0009.sql Normal file
View File

@@ -0,0 +1,7 @@
CREATE TABLE shopping_list (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
product_id INTEGER NOT NULL UNIQUE,
amount INTEGER NOT NULL DEFAULT 0,
amount_autoadded INTEGER NOT NULL DEFAULT 0,
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
)

8
migrations/0010.sql Normal file
View File

@@ -0,0 +1,8 @@
CREATE TABLE habits (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
name TEXT NOT NULL UNIQUE,
description TEXT,
period_type TEXT NOT NULL,
period_days INTEGER,
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
)

6
migrations/0011.sql Normal file
View File

@@ -0,0 +1,6 @@
CREATE TABLE habits_log (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
habit_id INTEGER NOT NULL,
tracked_time DATETIME,
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
)

6
migrations/0012.sql Normal file
View File

@@ -0,0 +1,6 @@
CREATE VIEW habits_current
AS
SELECT habit_id, MAX(tracked_time) AS last_tracked_time
FROM habits_log
GROUP BY habit_id
ORDER BY MAX(tracked_time) DESC

8
migrations/0013.sql Normal file
View File

@@ -0,0 +1,8 @@
CREATE TABLE batteries (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
name TEXT NOT NULL UNIQUE,
description TEXT,
used_in TEXT,
charge_interval_days INTEGER NOT NULL DEFAULT 0,
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
)

6
migrations/0014.sql Normal file
View File

@@ -0,0 +1,6 @@
CREATE TABLE battery_charge_cycles (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
battery_id TEXT NOT NULL,
tracked_time DATETIME,
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
)

6
migrations/0015.sql Normal file
View File

@@ -0,0 +1,6 @@
CREATE VIEW batteries_current
AS
SELECT battery_id, MAX(tracked_time) AS last_tracked_time
FROM battery_charge_cycles
GROUP BY battery_id
ORDER BY MAX(tracked_time) DESC

1
migrations/0016.sql Normal file
View File

@@ -0,0 +1 @@
ALTER TABLE shopping_list RENAME TO shopping_list_old

8
migrations/0017.sql Normal file
View File

@@ -0,0 +1,8 @@
CREATE TABLE shopping_list (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
product_id INTEGER,
note TEXT,
amount INTEGER NOT NULL DEFAULT 0,
amount_autoadded INTEGER NOT NULL DEFAULT 0,
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
)

4
migrations/0018.sql Normal file
View File

@@ -0,0 +1,4 @@
INSERT INTO shopping_list
(product_id, amount, amount_autoadded, row_created_timestamp)
SELECT product_id, amount, amount_autoadded, row_created_timestamp
FROM shopping_list_old

1
migrations/0019.sql Normal file
View File

@@ -0,0 +1 @@
DROP TABLE shopping_list_old

6
migrations/0020.sql Normal file
View File

@@ -0,0 +1,6 @@
CREATE TABLE sessions (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
session_key TEXT NOT NULL UNIQUE,
expires DATETIME,
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
)

11
migrations/0021.sql Normal file
View File

@@ -0,0 +1,11 @@
DELETE FROM locations
WHERE name = 'DefaultLocation';
DELETE FROM quantity_units
WHERE name = 'DefaultQuantityUnit';
DELETE FROM products
WHERE name = 'DefaultProduct1';
DELETE FROM products
WHERE name = 'DefaultProduct2';

View File

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

View File

@@ -22,8 +22,8 @@
padding: 20px;
overflow-x: hidden;
overflow-y: auto;
background-color: #f5f5f5;
border-right: 1px solid #5e5e5e;
background-color: #e5e5e5;
border-right: 2px solid #d6d6d6;
min-width: 220px;
max-width: 260px;
}
@@ -42,13 +42,29 @@
.nav-sidebar > li > a {
padding-right: 20px;
padding-left: 20px;
transition: all 0.3s;
}
.nav-sidebar > li > a:hover {
box-shadow: inset 5px 0 0 #337ab7;
transition: all 0.3s;
}
.nav-sidebar > li > a:focus {
box-shadow: inset 5px 0 0 #ab2230;
transition: all 0.3s;
}
.nav-sidebar > .active > a,
.nav-sidebar > .active > a:hover,
.nav-sidebar > .active > a:focus {
color: #fff;
background-color: #5e5e5e;
background-color: #d6d6d6;
box-shadow: inset 5px 0 0 #ab2230;
transition: all 0.3s;
}
.navbar-default {
background-color: #e5e5e5;
}
.main {
@@ -67,36 +83,41 @@
}
.nav-copyright {
color: #b3b3b1;
color: #a7a7a7;
font-size: 11px;
text-align: center;
font-family: 'Arial', sans-serif;
}
.discrete-link {
color: inherit;
color: inherit !important;
transition: all 0.3s !important;
}
a.discrete-link:hover {
color: #5cb85c;
text-decoration: none;
color: #337ab7 !important;
text-decoration: none !important;
transition: all 0.3s !important;
}
a.discrete-link:focus {
color: #337ab7;
text-decoration: none;
color: #ab2230 !important;
text-decoration: none !important;
transition: all 0.3s !important;
}
.navbar-fixed-top {
border-bottom: solid;
border-color: #5e5e5e;
border-bottom: 2px solid;
border-color: #d6d6d6;
}
.navbar-brand {
font-weight: bold;
letter-spacing: -2px;
letter-spacing: -5px;
font-size: 2.2em;
font-family: 'Arial', sans-serif;
color: #0b024c !important;
margin-left: 0 !important;
padding-left: 5px !important;
}
.table td.fit-content,
@@ -144,3 +165,7 @@ a.discrete-link:focus {
padding-top: 10px;
padding-bottom: 10px;
}
.well {
background-color: #e5e5e5;
}

BIN
public/img/grocy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

3
public/index.php Normal file
View File

@@ -0,0 +1,3 @@
<?php
require_once __DIR__ . '/../app.php';

33
public/js/extensions.js Normal file
View File

@@ -0,0 +1,33 @@
EmptyElementWhenMatches = function(selector, text)
{
if ($(selector).text() === text)
{
$(selector).text('');
}
};
String.prototype.contains = function(search)
{
return this.toLowerCase().indexOf(search.toLowerCase()) !== -1;
};
String.prototype.isEmpty = function()
{
return (this.length === 0 || !this.trim());
};
GetUriParam = function(key)
{
var currentUri = decodeURIComponent(window.location.search.substring(1));
var vars = currentUri.split('&');
for (i = 0; i < vars.length; i++)
{
var currentParam = vars[i].split('=');
if (currentParam[0] === key)
{
return currentParam[1] === undefined ? true : currentParam[1];
}
}
};

91
public/js/grocy.js Normal file
View File

@@ -0,0 +1,91 @@
L = function(text, ...placeholderValues)
{
var localizedText = Grocy.LocalizationStrings[text];
if (localizedText === undefined)
{
localizedText = text;
}
for (var i = 0; i < placeholderValues.length; i++)
{
localizedText = localizedText.replace('#' + (i + 1), placeholderValues[i]);
}
return localizedText;
}
U = function(relativePath)
{
return Grocy.BaseUrl.replace(/\/$/, '') + relativePath;
}
if (!Grocy.ActiveNav.isEmpty())
{
var menuItem = $('.nav').find("[data-nav-for-page='" + Grocy.ActiveNav + "']");
menuItem.addClass('active');
}
$.timeago.settings.allowFuture = true;
$('time.timeago').timeago();
Grocy.Api = { };
Grocy.Api.Get = function(apiFunction, success, error)
{
var xhr = new XMLHttpRequest();
var url = U('/api/' + apiFunction);
xhr.onreadystatechange = function()
{
if (xhr.readyState === XMLHttpRequest.DONE)
{
if (xhr.status === 200)
{
if (success)
{
success(JSON.parse(xhr.responseText));
}
}
else
{
if (error)
{
error(xhr);
}
}
}
};
xhr.open('GET', url, true);
xhr.send();
};
Grocy.Api.Post = function(apiFunction, jsonData, success, error)
{
var xhr = new XMLHttpRequest();
var url = U('/api/' + apiFunction);
xhr.onreadystatechange = function()
{
if (xhr.readyState === XMLHttpRequest.DONE)
{
if (xhr.status === 200)
{
if (success)
{
success(JSON.parse(xhr.responseText));
}
}
else
{
if (error)
{
error(xhr);
}
}
}
};
xhr.open('POST', url, true);
xhr.setRequestHeader('Content-type', 'application/json');
xhr.send(JSON.stringify(jsonData));
};

View File

@@ -0,0 +1,41 @@
$(document).on('click', '.battery-delete-button', function(e)
{
bootbox.confirm({
message: L('Are you sure to delete battery "#1"?', $(e.currentTarget).attr('data-battery-name')),
buttons: {
confirm: {
label: L('Yes'),
className: 'btn-success'
},
cancel: {
label: L('No'),
className: 'btn-danger'
}
},
callback: function(result)
{
if (result === true)
{
Grocy.Api.Get('delete-object/batteries/' + $(e.currentTarget).attr('data-battery-id'),
function(result)
{
window.location.href = U('/batteries');
},
function(xhr)
{
console.error(xhr);
}
);
}
}
});
});
$('#batteries-table').DataTable({
'pageLength': 50,
'order': [[1, 'asc']],
'columnDefs': [
{ 'orderable': false, 'targets': 0 }
],
'language': JSON.parse(L('datatables_localization'))
});

View File

@@ -0,0 +1,5 @@
$('#batteries-overview-table').DataTable({
'pageLength': 50,
'order': [[1, 'desc']],
'language': JSON.parse(L('datatables_localization'))
});

View File

@@ -0,0 +1,35 @@
$('#save-battery-button').on('click', function(e)
{
e.preventDefault();
if (Grocy.EditMode === 'create')
{
Grocy.Api.Post('add-object/batteries', $('#battery-form').serializeJSON(),
function(result)
{
window.location.href = U('/batteries');
},
function(xhr)
{
console.error(xhr);
}
);
}
else
{
Grocy.Api.Post('edit-object/batteries/' + Grocy.EditObjectId, $('#battery-form').serializeJSON(),
function(result)
{
window.location.href = U('/batteries');
},
function(xhr)
{
console.error(xhr);
}
);
}
});
$('#name').focus();
$('#battery-form').validator();
$('#battery-form').validator('validate');

View File

@@ -4,10 +4,10 @@
var jsonForm = $('#batterytracking-form').serializeJSON();
Grocy.FetchJson('/api/batteries/get-battery-details/' + jsonForm.battery_id,
Grocy.Api.Get('batteries/get-battery-details/' + jsonForm.battery_id,
function (batteryDetails)
{
Grocy.FetchJson('/api/batteries/track-charge-cycle/' + jsonForm.battery_id + '?tracked_time=' + $('#tracked_time').val(),
Grocy.Api.Get('batteries/track-charge-cycle/' + jsonForm.battery_id + '?tracked_time=' + $('#tracked_time').val(),
function(result)
{
toastr.success('Tracked charge cylce of battery ' + batteryDetails.battery.name + ' on ' + $('#tracked_time').val());
@@ -39,70 +39,52 @@ $('#battery_id').on('change', function(e)
if (batteryId)
{
Grocy.FetchJson('/api/batteries/get-battery-details/' + batteryId,
function(batteryDetails)
{
$('#selected-battery-name').text(batteryDetails.battery.name);
$('#selected-battery-last-charged').text((batteryDetails.last_charged || 'never'));
$('#selected-battery-last-charged-timeago').text($.timeago(batteryDetails.last_charged || ''));
$('#selected-battery-charge-cycles-count').text((batteryDetails.charge_cycles_count || '0'));
$('#tracked_time').focus();
Grocy.EmptyElementWhenMatches('#selected-battery-last-charged-timeago', 'NaN years ago');
},
function(xhr)
{
console.error(xhr);
}
);
Grocy.Components.BatteryCard.Refresh(batteryId);
$('#tracked_time').focus();
}
});
$(function()
$('.datetimepicker').datetimepicker(
{
$('.datetimepicker').datetimepicker(
{
format: 'YYYY-MM-DD HH:mm:ss',
showTodayButton: true,
calendarWeeks: true,
maxDate: moment()
});
format: 'YYYY-MM-DD HH:mm:ss',
showTodayButton: true,
calendarWeeks: true,
maxDate: moment()
});
$('#tracked_time').val(moment().format('YYYY-MM-DD HH:mm:ss'));
$('#tracked_time').trigger('change');
$('#tracked_time').val(moment().format('YYYY-MM-DD HH:mm:ss'));
$('#tracked_time').trigger('change');
$('#tracked_time').on('focus', function(e)
$('#tracked_time').on('focus', function(e)
{
if ($('#battery_id_text_input').val().length === 0)
{
if ($('#battery_id_text_input').val().length === 0)
$('#battery_id_text_input').focus();
}
});
$('.combobox').combobox({
appendId: '_text_input'
});
$('#battery_id').val('');
$('#battery_id_text_input').focus();
$('#battery_id_text_input').val('');
$('#battery_id_text_input').trigger('change');
$('#batterytracking-form').validator();
$('#batterytracking-form').validator('validate');
$('#batterytracking-form input').keydown(function(event)
{
if (event.keyCode === 13) //Enter
{
if ($('#batterytracking-form').validator('validate').has('.has-error').length !== 0) //There is at least one validation error
{
$('#battery_id_text_input').focus();
event.preventDefault();
return false;
}
});
$('.combobox').combobox({
appendId: '_text_input'
});
$('#battery_id').val('');
$('#battery_id_text_input').focus();
$('#battery_id_text_input').val('');
$('#battery_id_text_input').trigger('change');
$('#batterytracking-form').validator();
$('#batterytracking-form').validator('validate');
$('#batterytracking-form input').keydown(function(event)
{
if (event.keyCode === 13) //Enter
{
if ($('#batterytracking-form').validator('validate').has('.has-error').length !== 0) //There is at least one validation error
{
event.preventDefault();
return false;
}
}
});
}
});
$('#tracked_time').on('change', function(e)

View File

@@ -0,0 +1,21 @@
Grocy.Components.BatteryCard = { };
Grocy.Components.BatteryCard.Refresh = function(batteryId)
{
Grocy.Api.Get('batteries/get-battery-details/' + batteryId,
function(batteryDetails)
{
$('#batterycard-battery-name').text(batteryDetails.battery.name);
$('#batterycard-battery-used_in').text(batteryDetails.battery.used_in);
$('#batterycard-battery-last-charged').text((batteryDetails.last_charged || 'never'));
$('#batterycard-battery-last-charged-timeago').text($.timeago(batteryDetails.last_charged || ''));
$('#batterycard-battery-charge-cycles-count').text((batteryDetails.charge_cycles_count || '0'));
EmptyElementWhenMatches('#batterycard-battery-last-charged-timeago', L('timeago_nan'));
},
function(xhr)
{
console.error(xhr);
}
);
};

View File

@@ -0,0 +1,92 @@
$(function()
{
$('.datepicker').datepicker(
{
format: 'yyyy-mm-dd',
startDate: '+0d',
todayHighlight: true,
autoclose: true,
calendarWeeks: true,
orientation: 'bottom auto',
weekStart: 1,
showOnFocus: false,
language: L('bootstrap_datepicker_locale')
});
$('.datepicker').trigger('change');
EmptyElementWhenMatches('#datepicker-timeago', L('timeago_nan'));
});
$('.datepicker').on('keydown', function(e)
{
if (e.keyCode === 13) //Enter
{
$('.datepicker').trigger('change');
}
});
$('.datepicker').on('keypress', function(e)
{
var element = $(e.target);
var value = element.val();
var dateObj = moment(element.val(), 'YYYY-MM-DD', true);
$('.datepicker').datepicker('hide');
//If input is empty and any arrow key is pressed, set date to today
if (value.length === 0 && (e.keyCode === 38 || e.keyCode === 40 || e.keyCode === 37 || e.keyCode === 39))
{
dateObj = moment(new Date(), 'YYYY-MM-DD', true);
}
if (dateObj.isValid())
{
if (e.keyCode === 38) //Up
{
element.val(dateObj.add(-1, 'days').format('YYYY-MM-DD'));
}
else if (e.keyCode === 40) //Down
{
element.val(dateObj.add(1, 'days').format('YYYY-MM-DD'));
}
else if (e.keyCode === 37) //Left
{
element.val(dateObj.add(-1, 'weeks').format('YYYY-MM-DD'));
}
else if (e.keyCode === 39) //Right
{
element.val(dateObj.add(1, 'weeks').format('YYYY-MM-DD'));
}
}
});
$('.datepicker').on('change', function(e)
{
var value = $('.datepicker').val();
var now = new Date();
var centuryStart = Number.parseInt(now.getFullYear().toString().substring(0, 2) + '00');
var centuryEnd = Number.parseInt(now.getFullYear().toString().substring(0, 2) + '99');
if (value === 'x' || value === 'X') {
value = '29991231';
}
if (value.length === 4 && !(Number.parseInt(value) > centuryStart && Number.parseInt(value) < centuryEnd))
{
value = (new Date()).getFullYear().toString() + value;
}
if (value.length === 8 && $.isNumeric(value))
{
value = value.replace(/(\d{4})(\d{2})(\d{2})/, '$1-$2-$3');
$('.datepicker').val(value);
}
$('#datepicker-timeago').text($.timeago($('.datepicker').val()));
EmptyElementWhenMatches('#datepicker-timeago', L('timeago_nan'));
});
$('#datepicker-button').on('click', function(e)
{
$('.datepicker').datepicker('show');
});

View File

@@ -0,0 +1,11 @@
$(function()
{
$('.datetimepicker').datetimepicker(
{
format: 'YYYY-MM-DD HH:mm:ss',
showTodayButton: true,
calendarWeeks: true,
maxDate: moment(),
locale: moment.locale('de')
});
});

View File

@@ -0,0 +1,20 @@
Grocy.Components.HabitCard = { };
Grocy.Components.HabitCard.Refresh = function (habitId)
{
Grocy.Api.Get('habits/get-habit-details/' + habitId,
function(habitDetails)
{
$('#habitcard-habit-name').text(habitDetails.habit.name);
$('#habitcard-habit-last-tracked').text((habitDetails.last_tracked || 'never'));
$('#habitcard-habit-last-tracked-timeago').text($.timeago(habitDetails.last_tracked || ''));
$('#habitcard-habit-tracked-count').text((habitDetails.tracked_count || '0'));
EmptyElementWhenMatches('#habitcard-habit-last-tracked-timeago', L('timeago_nan'));
},
function(xhr)
{
console.error(xhr);
}
);
};

View File

@@ -0,0 +1,25 @@
Grocy.Components.ProductCard = { };
Grocy.Components.ProductCard.Refresh = function(productId)
{
Grocy.Api.Get('stock/get-product-details/' + productId,
function(productDetails)
{
$('#productcard-product-name').text(productDetails.product.name);
$('#productcard-product-stock-amount').text(productDetails.stock_amount || '0');
$('#productcard-product-stock-qu-name').text(productDetails.quantity_unit_stock.name);
$('#productcard-product-stock-qu-name2').text(productDetails.quantity_unit_stock.name);
$('#productcard-product-last-purchased').text((productDetails.last_purchased || L('never')).substring(0, 10));
$('#productcard-product-last-purchased-timeago').text($.timeago(productDetails.last_purchased || ''));
$('#productcard-product-last-used').text((productDetails.last_used || L('never')).substring(0, 10));
$('#productcard-product-last-used-timeago').text($.timeago(productDetails.last_used || ''));
EmptyElementWhenMatches('#productcard-product-last-purchased-timeago', L('timeago_nan'));
EmptyElementWhenMatches('#productcard-product-last-used-timeago', L('timeago_nan'));
},
function(xhr)
{
console.error(xhr);
}
);
};

132
public/viewjs/consume.js Normal file
View File

@@ -0,0 +1,132 @@
$('#save-consume-button').on('click', function(e)
{
e.preventDefault();
var jsonForm = $('#consume-form').serializeJSON();
var spoiled = 0;
if ($('#spoiled').is(':checked'))
{
spoiled = 1;
}
Grocy.Api.Get('stock/get-product-details/' + jsonForm.product_id,
function (productDetails)
{
Grocy.Api.Get('stock/consume-product/' + jsonForm.product_id + '/' + jsonForm.amount + '?spoiled=' + spoiled,
function(result)
{
toastr.success('Removed ' + jsonForm.amount + ' ' + productDetails.quantity_unit_stock.name + ' of ' + productDetails.product.name + ' from stock');
$('#amount').val(1);
$('#product_id').val('');
$('#product_id_text_input').focus();
$('#product_id_text_input').val('');
$('#product_id_text_input').trigger('change');
$('#consume-form').validator('validate');
},
function(xhr)
{
console.error(xhr);
}
);
},
function(xhr)
{
console.error(xhr);
}
);
});
$('#product_id').on('change', function(e)
{
var productId = $(e.target).val();
if (productId)
{
Grocy.Components.ProductCard.Refresh(productId);
Grocy.Api.Get('stock/get-product-details/' + productId,
function (productDetails)
{
$('#amount').attr('max', productDetails.stock_amount);
$('#consume-form').validator('update');
$('#amount_qu_unit').text(productDetails.quantity_unit_stock.name);
if ((productDetails.stock_amount || 0) === 0)
{
$('#product_id').val('');
$('#product_id_text_input').val('');
$('#product_id_text_input').addClass('has-error');
$('#product_id_text_input').parent('.input-group').addClass('has-error');
$('#product_id_text_input').closest('.form-group').addClass('has-error');
$('#product-error').text('This product is not in stock.');
$('#product-error').show();
$('#product_id_text_input').focus();
}
else
{
$('#product_id_text_input').removeClass('has-error');
$('#product_id_text_input').parent('.input-group').removeClass('has-error');
$('#product_id_text_input').closest('.form-group').removeClass('has-error');
$('#product-error').hide();
$('#amount').focus();
}
},
function(xhr)
{
console.error(xhr);
}
);
}
});
$('.combobox').combobox({
appendId: '_text_input'
});
$('#product_id_text_input').on('change', function(e)
{
var input = $('#product_id_text_input').val().toString();
var possibleOptionElement = $("#product_id option[data-additional-searchdata*='" + input + "']").first();
if (possibleOptionElement.length > 0)
{
$('#product_id').val(possibleOptionElement.val());
$('#product_id').data('combobox').refresh();
$('#product_id').trigger('change');
}
});
$('#amount').val(1);
$('#product_id').val('');
$('#product_id_text_input').focus();
$('#product_id_text_input').val('');
$('#product_id_text_input').trigger('change');
$('#consume-form').validator();
$('#consume-form').validator('validate');
$('#amount').on('focus', function(e)
{
if ($('#product_id_text_input').val().length === 0)
{
$('#product_id_text_input').focus();
}
else
{
$(this).select();
}
});
$('#consume-form input').keydown(function(event)
{
if (event.keyCode === 13) //Enter
{
if ($('#consume-form').validator('validate').has('.has-error').length !== 0) //There is at least one validation error
{
event.preventDefault();
return false;
}
}
});

View File

@@ -4,10 +4,10 @@
if (Grocy.EditMode === 'create')
{
Grocy.PostJson('/api/add-object/habits', $('#habit-form').serializeJSON(),
Grocy.Api.Post('add-object/habits', $('#habit-form').serializeJSON(),
function(result)
{
window.location.href = '/habits';
window.location.href = U('/habits');
},
function(xhr)
{
@@ -17,10 +17,10 @@
}
else
{
Grocy.PostJson('/api/edit-object/habits/' + Grocy.EditObjectId, $('#habit-form').serializeJSON(),
Grocy.Api.Post('edit-object/habits/' + Grocy.EditObjectId, $('#habit-form').serializeJSON(),
function(result)
{
window.location.href = '/habits';
window.location.href = U('/habits');
},
function(xhr)
{
@@ -30,12 +30,9 @@
}
});
$(function()
{
$('#name').focus();
$('#habit-form').validator();
$('#habit-form').validator('validate');
});
$('#name').focus();
$('#habit-form').validator();
$('#habit-form').validator('validate');
$('.input-group-habit-period-type').on('change', function(e)
{

41
public/viewjs/habits.js Normal file
View File

@@ -0,0 +1,41 @@
$(document).on('click', '.habit-delete-button', function(e)
{
bootbox.confirm({
message: L('Are you sure to delete habit "#1"?', $(e.currentTarget).attr('data-habit-name')),
buttons: {
confirm: {
label: L('Yes'),
className: 'btn-success'
},
cancel: {
label: L('No'),
className: 'btn-danger'
}
},
callback: function(result)
{
if (result === true)
{
Grocy.Api.Get('delete-object/habits/' + $(e.currentTarget).attr('data-habit-id'),
function(result)
{
window.location.href = U('/habits');
},
function(xhr)
{
console.error(xhr);
}
);
}
}
});
});
$('#habits-table').DataTable({
'pageLength': 50,
'order': [[1, 'asc']],
'columnDefs': [
{ 'orderable': false, 'targets': 0 }
],
'language': JSON.parse(L('datatables_localization'))
});

Some files were not shown because too many files have changed in this diff Show More