Compare commits

...

54 Commits

Author SHA1 Message Date
Bernd Bestel
0954b5a741 Add option to not use URL rewriting 2018-06-15 20:50:40 +02:00
Bernd Bestel
02b6c3b721 Make it also possible to directly execute/track battery charge cycles and habits from overview pages 2018-05-13 09:38:22 +02:00
Bernd Bestel
6fa4e13ba2 Fix API version display 2018-05-13 09:00:14 +02:00
Bernd Bestel
9837f79f9c Make it also possible to consume the whole stock of a product from stock overview 2018-05-13 08:42:45 +02:00
Bernd Bestel
6e4cd22118 Make big buttons on overview pages responsive (references #9) 2018-05-12 16:38:21 +02:00
Bernd Bestel
ca00dd8e2d Improved table layout 2018-05-12 16:35:14 +02:00
Bernd Bestel
5455ec7bde Added missing translations 2018-05-12 16:30:10 +02:00
Bernd Bestel
2e7af1b050 Added possibility to consume products directly from stock overview 2018-05-12 16:15:28 +02:00
Bernd Bestel
89bae8d25e Changed how version information is stored and displayed 2018-05-12 15:49:21 +02:00
Bernd Bestel
5b5c272909 Correct and complete documentation 2018-05-12 15:36:23 +02:00
Bernd Bestel
3e394a3840 Also show due/overdue on bateries- and habitoverview 2018-05-12 15:30:13 +02:00
Bernd Bestel
ab8094e1c0 Don't expose username when not logged in 2018-05-12 14:56:51 +02:00
Bernd Bestel
bbb5f1c7c7 Rework general page layout and improve responsiveness (references #9) 2018-05-12 14:25:21 +02:00
Bernd Bestel
b607f188af Correct PHP dependency information (closes #8) 2018-05-11 09:51:30 +02:00
Bernd Bestel
9ab1a674fe Merge pull request #7 from d-Rickyy-b/patch-1
FIX: Add missing translation of "Add" button
2018-05-07 22:38:32 +02:00
Rico
2f0a1391b7 FIX: Add missing translation of "Add" button
The "Add" button was not translated in the 'Quantity units' form.
2018-05-07 22:30:17 +02:00
Bernd Bestel
a9a1358b08 Added a plugin system for looking up products against external services by barcode (references #6) 2018-04-22 19:50:24 +02:00
Bernd Bestel
4853174d03 Validate all API request as the API is now open for third parties (references #5) 2018-04-22 14:25:08 +02:00
Bernd Bestel
538d789366 Don't expose stock entity directly via API 2018-04-21 21:33:03 +02:00
Bernd Bestel
0751919b82 Add API & data model documentation hint 2018-04-21 20:13:47 +02:00
Bernd Bestel
99b2a84667 Finish API documentation and token auth (references #5) 2018-04-21 19:18:00 +02:00
Bernd Bestel
9bd6aac09c Start working on API documentation and token auth (references #5) 2018-04-20 23:09:18 +02:00
Bernd Bestel
7be35a90c1 Small style changes 2018-04-20 15:21:29 +02:00
Bernd Bestel
eae5b8bad9 Revise session handling to prepare API authentication via token 2018-04-20 11:34:53 +02:00
Bernd Bestel
0c85342404 Updated Composer dependencies 2018-04-20 09:48:06 +02:00
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
217 changed files with 9320 additions and 4691 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,191 +0,0 @@
<?php
class GrocyLogicStock
{
const TRANSACTION_TYPE_PURCHASE = 'purchase';
const TRANSACTION_TYPE_CONSUME = 'consume';
const TRANSACTION_TYPE_INVENTORY_CORRECTION = 'inventory-correction';
public static function GetCurrentStock()
{
$sql = 'SELECT * from stock_current';
return Grocy::ExecuteDbQuery(Grocy::GetDbConnectionRaw(), $sql)->fetchAll(PDO::FETCH_OBJ);
}
public static function GetMissingProducts()
{
$sql = 'SELECT * from stock_missing_products';
return Grocy::ExecuteDbQuery(Grocy::GetDbConnectionRaw(), $sql)->fetchAll(PDO::FETCH_OBJ);
}
public static function GetProductDetails(int $productId)
{
$db = Grocy::GetDbConnection();
$product = $db->products($productId);
$productStockAmount = $db->stock()->where('product_id', $productId)->sum('amount');
$productLastPurchased = $db->stock_log()->where('product_id', $productId)->where('transaction_type', self::TRANSACTION_TYPE_PURCHASE)->max('purchased_date');
$productLastUsed = $db->stock_log()->where('product_id', $productId)->where('transaction_type', self::TRANSACTION_TYPE_CONSUME)->max('used_date');
$quPurchase = $db->quantity_units($product->qu_id_purchase);
$quStock = $db->quantity_units($product->qu_id_stock);
return array(
'product' => $product,
'last_purchased' => $productLastPurchased,
'last_used' => $productLastUsed,
'stock_amount' => $productStockAmount,
'quantity_unit_purchase' => $quPurchase,
'quantity_unit_stock' => $quStock
);
}
public static function AddProduct(int $productId, int $amount, string $bestBeforeDate, $transactionType)
{
if ($transactionType === self::TRANSACTION_TYPE_CONSUME || $transactionType === self::TRANSACTION_TYPE_PURCHASE || $transactionType === self::TRANSACTION_TYPE_INVENTORY_CORRECTION)
{
$db = Grocy::GetDbConnection();
$stockId = uniqid();
$logRow = $db->stock_log()->createRow(array(
'product_id' => $productId,
'amount' => $amount,
'best_before_date' => $bestBeforeDate,
'purchased_date' => date('Y-m-d'),
'stock_id' => $stockId,
'transaction_type' => $transactionType
));
$logRow->save();
$stockRow = $db->stock()->createRow(array(
'product_id' => $productId,
'amount' => $amount,
'best_before_date' => $bestBeforeDate,
'purchased_date' => date('Y-m-d'),
'stock_id' => $stockId,
));
$stockRow->save();
return true;
}
else
{
throw new Exception("Transaction type $transactionType is not valid (GrocyLogicStock.AddProduct)");
}
}
public static function ConsumeProduct(int $productId, int $amount, bool $spoiled, $transactionType)
{
if ($transactionType === self::TRANSACTION_TYPE_CONSUME || $transactionType === self::TRANSACTION_TYPE_PURCHASE || $transactionType === self::TRANSACTION_TYPE_INVENTORY_CORRECTION)
{
$db = Grocy::GetDbConnection();
$productStockAmount = $db->stock()->where('product_id', $productId)->sum('amount');
$potentialStockEntries = $db->stock()->where('product_id', $productId)->orderBy('best_before_date', 'ASC')->orderBy('purchased_date', 'ASC')->fetchAll(); //First expiring first, then first in first out
if ($amount > $productStockAmount)
{
return false;
}
foreach ($potentialStockEntries as $stockEntry)
{
if ($amount == 0)
{
break;
}
if ($amount >= $stockEntry->amount) //Take the whole stock entry
{
$logRow = $db->stock_log()->createRow(array(
'product_id' => $stockEntry->product_id,
'amount' => $stockEntry->amount * -1,
'best_before_date' => $stockEntry->best_before_date,
'purchased_date' => $stockEntry->purchased_date,
'used_date' => date('Y-m-d'),
'spoiled' => $spoiled,
'stock_id' => $stockEntry->stock_id,
'transaction_type' => $transactionType
));
$logRow->save();
$amount -= $stockEntry->amount;
$stockEntry->delete();
}
else //Stock entry amount is > than needed amount -> split the stock entry resp. update the amount
{
$logRow = $db->stock_log()->createRow(array(
'product_id' => $stockEntry->product_id,
'amount' => $amount * -1,
'best_before_date' => $stockEntry->best_before_date,
'purchased_date' => $stockEntry->purchased_date,
'used_date' => date('Y-m-d'),
'spoiled' => $spoiled,
'stock_id' => $stockEntry->stock_id,
'transaction_type' => $transactionType
));
$logRow->save();
$restStockAmount = $stockEntry->amount - $amount;
$amount = 0;
$stockEntry->update(array(
'amount' => $restStockAmount
));
}
}
return true;
}
else
{
throw new Exception("Transaction type $transactionType is not valid (GrocyLogicStock.ConsumeProduct)");
}
}
public static function InventoryProduct(int $productId, int $newAmount, string $bestBeforeDate)
{
$db = Grocy::GetDbConnection();
$productStockAmount = $db->stock()->where('product_id', $productId)->sum('amount');
if ($newAmount > $productStockAmount)
{
$amountToAdd = $newAmount - $productStockAmount;
self::AddProduct($productId, $amountToAdd, $bestBeforeDate, self::TRANSACTION_TYPE_INVENTORY_CORRECTION);
}
else if ($newAmount < $productStockAmount)
{
$amountToRemove = $productStockAmount - $newAmount;
self::ConsumeProduct($productId, $amountToRemove, false, self::TRANSACTION_TYPE_INVENTORY_CORRECTION);
}
return true;
}
public static function AddMissingProductsToShoppingList()
{
$db = Grocy::GetDbConnection();
$missingProducts = self::GetMissingProducts();
foreach ($missingProducts as $missingProduct)
{
$product = $db->products()->where('id', $missingProduct->id)->fetch();
$amount = ceil($missingProduct->amount_missing / $product->qu_factor_purchase_to_stock);
$alreadyExistingEntry = $db->shopping_list()->where('product_id', $missingProduct->id)->fetch();
if ($alreadyExistingEntry) //Update
{
$alreadyExistingEntry->update(array(
'amount_autoadded' => $amount
));
}
else //Insert
{
$shoppinglistRow = $db->shopping_list()->createRow(array(
'product_id' => $missingProduct->id,
'amount_autoadded' => $amount
));
$shoppinglistRow->save();
}
}
}
}

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,63 @@
# 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 (currently only tested with PHP 7.2) 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.
If, however, your webserver does not support URL rewriting, set `DISABLE_URL_REWRITING` in `data/config.php`.
## 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
### REST API & data model documentation
See the integrated Swagger UI instance on [/api](https://demo-en.grocy.info/api).
### 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 `2999-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.
### Barcode lookup via external services
Products can be directly added to the database via looking them up against external services by a barcode.
This is currently only possible through the REST API.
There is no plugin included for any service, see the reference implementation in `data/plugins/DemoBarcodeLookupPlugin.php`.
### 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.
### Other things
When the file `data/add_before_end_body.html` exists, the contents of the file be added just before `</body>` on every page, useful for your own JS/CSS without to have to modify the application itself.
## Screenshots
#### Dashboard

44
app.php Normal file
View File

@@ -0,0 +1,44 @@
<?php
use \Psr\Http\Message\ServerRequestInterface as Request;
use \Psr\Http\Message\ResponseInterface as Response;
use \Grocy\Helpers\UrlManager;
use \Grocy\Controllers\LoginController;
require_once __DIR__ . '/vendor/autoload.php';
require_once __DIR__ . '/data/config.php';
// Setup base application
$appContainer = new \Slim\Container([
'settings' => [
'displayErrorDetails' => true,
'determineRouteBeforeAppMiddleware' => true
],
'view' => function($container)
{
return new \Slim\Views\Blade(__DIR__ . '/views', __DIR__ . '/data/viewcache');
},
'LoginControllerInstance' => function($container)
{
return new LoginController($container, 'grocy_session');
},
'UrlManager' => function($container)
{
return new UrlManager(BASE_URL);
},
'ApiKeyHeaderName' => function($container)
{
return 'GROCY-API-KEY';
}
]);
$app = new \Slim\App($appContainer);
if (PHP_SAPI === 'cli')
{
$app->add(\pavlakis\cli\CliRequest::class);
}
require_once __DIR__ . '/routes.php';
$app->run();

View File

@@ -17,6 +17,9 @@
"jquery-timeago": "^1.6.1",
"toastr": "^2.1.3",
"tagmanager": "^3.0.2",
"eonasdan-bootstrap-datetimepicker": "^4.17.47"
"eonasdan-bootstrap-datetimepicker": "^4.17.47",
"swagger-ui": "^3.13.4",
"jquery-ui": "^1.12.1",
"bootstrap-side-navbar": "^1.0.1"
}
}

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,21 @@
{
"require": {
"php": ">=7.2",
"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",
"tuupola/cors-middleware": "^0.7.0"
},
"autoload": {
"psr-4": {
"Grocy\\Services\\": "services/",
"Grocy\\Controllers\\": "controllers/",
"Grocy\\Middleware\\": "middleware/",
"Grocy\\Helpers\\": "helpers/"
},
"files": [
"helpers/extensions.php"
]
}
}

1610
composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,26 @@
<?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', '/');
# The plugin to use for external barcode lookups,
# must be the filename without .php extension and must be located in /data/plugins,
# see /data/plugins/DemoBarcodeLookupPlugin.php for an example implementation
define('STOCK_BARCODE_LOOKUP_PLUGIN', 'DemoBarcodeLookupPlugin');
# If, however, your webserver does not support URL rewriting,
# set this to true
define('DISABLE_URL_REWRITING', false);

View File

@@ -0,0 +1,28 @@
<?php
namespace Grocy\Controllers;
class BaseApiController extends BaseController
{
public function __construct(\Slim\Container $container)
{
parent::__construct($container);
$this->OpenApiSpec = json_decode(file_get_contents(__DIR__ . '/../grocy.openapi.json'));
}
protected $OpenApiSpec;
protected function ApiResponse($data)
{
return json_encode($data);
}
protected function VoidApiActionResponse($response, $success = true, $status = 200, $errorMessage = '')
{
return $response->withStatus($status)->withJson(array(
'success' => $success,
'error_message' => $errorMessage
));
}
}

View File

@@ -0,0 +1,36 @@
<?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();
$versionInfo = $applicationService->GetInstalledVersion();
$container->view->set('version', $versionInfo->Version);
$container->view->set('releaseDate', $versionInfo->ReleaseDate);
$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, $isResource = false) use($container)
{
return $container->UrlManager->ConstructUrl($relativePath, $isResource);
});
$this->AppContainer = $container;
}
protected $AppContainer;
protected $Database;
}

View File

@@ -0,0 +1,47 @@
<?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'];
}
try
{
$this->BatteriesService->TrackChargeCycle($args['batteryId'], $trackedTime);
return $this->VoidApiActionResponse($response);
}
catch (\Exception $ex)
{
return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage());
}
}
public function BatteryDetails(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
try
{
return $this->ApiResponse($this->BatteriesService->GetBatteryDetails($args['batteryId']));
}
catch (\Exception $ex)
{
return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage());
}
}
}

View File

@@ -0,0 +1,68 @@
<?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);
}
$nextXDays = 5;
$countDueNextXDays = count(FindAllItemsInArrayByValue($nextChargeTimes, date('Y-m-d', strtotime("+$nextXDays days")), '<'));
$countOverdue = count(FindAllItemsInArrayByValue($nextChargeTimes, date('Y-m-d', strtotime('-1 days')), '<'));
return $this->AppContainer->view->render($response, 'batteriesoverview', [
'batteries' => $this->Database->batteries(),
'current' => $this->BatteriesService->GetCurrent(),
'nextChargeTimes' => $nextChargeTimes,
'nextXDays' => $nextXDays,
'countDueNextXDays' => $countDueNextXDays - $countOverdue,
'countOverdue' => $countOverdue
]);
}
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,80 @@
<?php
namespace Grocy\Controllers;
class GenericEntityApiController extends BaseApiController
{
public function GetObjects(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
if ($this->IsValidEntity($args['entity']))
{
return $this->ApiResponse($this->Database->{$args['entity']}());
}
else
{
return $this->VoidApiActionResponse($response, false, 400, 'Entity does not exist or is not exposed');
}
}
public function GetObject(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
if ($this->IsValidEntity($args['entity']))
{
return $this->ApiResponse($this->Database->{$args['entity']}($args['objectId']));
}
else
{
return $this->VoidApiActionResponse($response, false, 400, 'Entity does not exist or is not exposed');
}
}
public function AddObject(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
if ($this->IsValidEntity($args['entity']))
{
$newRow = $this->Database->{$args['entity']}()->createRow($request->getParsedBody());
$newRow->save();
$success = $newRow->isClean();
return $this->ApiResponse(array('success' => $success));
}
else
{
return $this->VoidApiActionResponse($response, false, 400, 'Entity does not exist or is not exposed');
}
}
public function EditObject(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
if ($this->IsValidEntity($args['entity']))
{
$row = $this->Database->{$args['entity']}($args['objectId']);
$row->update($request->getParsedBody());
$success = $row->isClean();
return $this->ApiResponse(array('success' => $success));
}
else
{
return $this->VoidApiActionResponse($response, false, 400, 'Entity does not exist or is not exposed');
}
}
public function DeleteObject(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
if ($this->IsValidEntity($args['entity']))
{
$row = $this->Database->{$args['entity']}($args['objectId']);
$row->delete();
$success = $row->isClean();
return $this->ApiResponse(array('success' => $success));
}
else
{
return $this->VoidApiActionResponse($response, false, 400, 'Entity does not exist or is not exposed');
}
}
private function IsValidEntity($entity)
{
return in_array($entity, $this->OpenApiSpec->components->internalSchemas->ExposedEntity->enum);
}
}

View File

@@ -0,0 +1,47 @@
<?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'];
}
try
{
$this->HabitsService->TrackHabit($args['habitId'], $trackedTime);
return $this->VoidApiActionResponse($response);
}
catch (\Exception $ex)
{
return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage());
}
}
public function HabitDetails(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
try
{
return $this->ApiResponse($this->HabitsService->GetHabitDetails($args['habitId']));
}
catch (\Exception $ex)
{
return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage());
}
}
}

View File

@@ -0,0 +1,70 @@
<?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);
}
$nextXDays = 5;
$countDueNextXDays = count(FindAllItemsInArrayByValue($nextHabitTimes, date('Y-m-d', strtotime("+$nextXDays days")), '<'));
$countOverdue = count(FindAllItemsInArrayByValue($nextHabitTimes, date('Y-m-d', strtotime('-1 days')), '<'));
return $this->AppContainer->view->render($response, 'habitsoverview', [
'habits' => $this->Database->habits(),
'currentHabits' => $this->HabitsService->GetCurrentHabits(),
'nextHabitTimes' => $nextHabitTimes,
'nextXDays' => $nextXDays,
'countDueNextXDays' => $countDueNextXDays - $countOverdue,
'countOverdue' => $countOverdue
]);
}
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,76 @@
<?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, string $sessionCookieName)
{
parent::__construct($container);
$this->SessionService = new SessionService();
$this->SessionCookieName = $sessionCookieName;
}
protected $SessionService;
protected $SessionCookieName;
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($this->SessionCookieName, $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[$this->SessionCookieName]);
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'));
}
public function GetSessionCookieName()
{
return $this->SessionCookieName;
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace Grocy\Controllers;
use \Grocy\Services\ApplicationService;
use \Grocy\Services\ApiKeyService;
class OpenApiController extends BaseApiController
{
public function __construct(\Slim\Container $container)
{
parent::__construct($container);
$this->ApiKeyService = new ApiKeyService();
}
protected $ApiKeyService;
public function DocumentationUi(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
return $this->AppContainer->view->render($response, 'openapiui');
}
public function DocumentationSpec(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
$applicationService = new ApplicationService();
$versionInfo = $applicationService->GetInstalledVersion();
$this->OpenApiSpec->info->version = $versionInfo->Version;
$this->OpenApiSpec->info->description = str_replace('PlaceHolderManageApiKeysUrl', $this->AppContainer->UrlManager->ConstructUrl('/manageapikeys'), $this->OpenApiSpec->info->description);
$this->OpenApiSpec->servers[0]->url = $this->AppContainer->UrlManager->ConstructUrl('/api');
return $this->ApiResponse($this->OpenApiSpec);
}
public function ApiKeysList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
return $this->AppContainer->view->render($response, 'manageapikeys', [
'apiKeys' => $this->Database->api_keys()
]);
}
public function CreateNewApiKey(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
$newApiKey = $this->ApiKeyService->CreateApiKey();
$newApiKeyId = $this->ApiKeyService->GetApiKeyId($newApiKey);
return $response->withRedirect($this->AppContainer->UrlManager->ConstructUrl("/manageapikeys?CreatedApiKeyId=$newApiKeyId"));
}
}

View File

@@ -0,0 +1,126 @@
<?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)
{
try
{
return $this->ApiResponse($this->StockService->GetProductDetails($args['productId']));
}
catch (\Exception $ex)
{
return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage());
}
}
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'];
}
try
{
$this->StockService->AddProduct($args['productId'], $args['amount'], $bestBeforeDate, $transactionType);
return $this->VoidApiActionResponse($response);
}
catch (\Exception $ex)
{
return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage());
}
}
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'];
}
try
{
$this->StockService->ConsumeProduct($args['productId'], $args['amount'], $spoiled, $transactionType);
return $this->VoidApiActionResponse($response);
}
catch (\Exception $ex)
{
return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage());
}
}
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'];
}
try
{
$this->StockService->InventoryProduct($args['productId'], $args['newAmount'], $bestBeforeDate);
return $this->VoidApiActionResponse($response);
}
catch (\Exception $ex)
{
return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage());
}
}
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->VoidApiActionResponse($response);
}
public function ExternalBarcodeLookup(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
try
{
$addFoundProduct = false;
if (isset($request->getQueryParams()['add']) && ($request->getQueryParams()['add'] === 'true' || $request->getQueryParams()['add'] === 1))
{
$addFoundProduct = true;
}
return $this->ApiResponse($this->StockService->ExternalBarcodeLookup($args['barcode'], $addFoundProduct));
}
catch (\Exception $ex)
{
return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage());
}
}
}

View File

@@ -0,0 +1,162 @@
<?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)
{
$currentStock = $this->StockService->GetCurrentStock();
$nextXDays = 5;
$countExpiringNextXDays = count(FindAllObjectsInArrayByPropertyValue($currentStock, 'best_before_date', date('Y-m-d', strtotime('+5 days')), '<'));
$countAlreadyExpired = count(FindAllObjectsInArrayByPropertyValue($currentStock, 'best_before_date', date('Y-m-d', strtotime('-1 days')), '<'));
return $this->AppContainer->view->render($response, 'stockoverview', [
'products' => $this->Database->products(),
'quantityunits' => $this->Database->quantity_units(),
'currentStock' => $currentStock,
'missingProducts' => $this->StockService->GetMissingProducts(),
'nextXDays' => $nextXDays,
'countExpiringNextXDays' => $countExpiringNextXDays,
'countAlreadyExpired' => $countAlreadyExpired
]);
}
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'
]);
}
}
}

2
data/.gitignore vendored
View File

@@ -1,2 +1,4 @@
*
!.gitignore
!viewcache
!plugins

3
data/plugins/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
*
!.gitignore
!DemoBarcodeLookupPlugin.php

View File

@@ -0,0 +1,78 @@
<?php
use \Grocy\Helpers\BaseBarcodeLookupPlugin;
/*
This class must extend BaseBarcodeLookupPlugin (in namespace \Grocy\Helpers)
*/
class DemoBarcodeLookupPlugin extends BaseBarcodeLookupPlugin
{
/*
To use this plugin, configure it in data/config.php like this:
define('STOCK_BARCODE_LOOKUP_PLUGIN', 'DemoBarcodeLookupPlugin');
*/
/*
To try it:
Call the API function at /api/stock/external-barcode-lookup/{barcode}
When you also add ?add=true as a query parameter to the API call,
on a successful lookup the product is added to the database and in the output
the new product id is included (automatically, nothing to do here in the plugin)
*/
/*
Provided references:
$this->Locations contains all locations
$this->QuantityUnits contains all quantity units
*/
/*
Useful hints:
Get a quantity unit by name:
$quantityUnit = FindObjectInArrayByPropertyValue($this->QuantityUnits, 'name', 'Piece');
Get a location by name:
$location = FindObjectInArrayByPropertyValue($this->Locations, 'name', 'Fridge');
*/
/*
This class must implement the protected abstract function ExecuteLookup($barcode),
which is called with the barcode that needs to be looked up and must return an
associative array of the product model or null, when nothing was found for the barcode.
The returned array must contain at least these properties:
array(
'name' => '',
'location_id' => 1, // A valid id of a location object, check against $this->Locations
'qu_id_purchase' => 1, // A valid id of quantity unit object, check against $this->QuantityUnits
'qu_id_stock' => 1, // A valid id of quantity unit object, check against $this->QuantityUnits
'qu_factor_purchase_to_stock' => 1, // Normally 1 when quantity unit stock and purchase is the same
'barcode' => $barcode // The barcode of the product, maybe just pass through $barcode or manipulate it if necessary
)
*/
protected function ExecuteLookup($barcode)
{
if ($barcode === 'x') // Demonstration when nothing is found
{
return null;
}
elseif ($barcode === 'e') // Demonstration when an error occurred
{
throw new \Exception('This is the error message from the plugin...');
}
else
{
return array(
'name' => 'LookedUpProduct_' . RandomString(5),
'location_id' => $this->Locations[0]->id,
'qu_id_purchase' => $this->QuantityUnits[0]->id,
'qu_id_stock' => $this->QuantityUnits[0]->id,
'qu_factor_purchase_to_stock' => 1,
'barcode' => $barcode
);
}
}
}

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));
}

1384
grocy.openapi.json Normal file

File diff suppressed because it is too large Load Diff

BIN
grocy.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

View File

@@ -0,0 +1,80 @@
<?php
namespace Grocy\Helpers;
abstract class BaseBarcodeLookupPlugin
{
final public function __construct($locations, $quantityUnits)
{
$this->Locations = $locations;
$this->QuantityUnits = $quantityUnits;
}
protected $Locations;
protected $QuantityUnits;
abstract protected function ExecuteLookup($barcode);
final public function Lookup($barcode)
{
$pluginOutput = $this->ExecuteLookup($barcode);
if ($pluginOutput === null)
{
return $pluginOutput;
}
// Plugin must return an associative array
if (!is_array($pluginOutput))
{
throw new \Exception('Plugin output must be an associative array');
}
if (!IsAssociativeArray($pluginOutput)) // $pluginOutput is at least an indexed array here
{
throw new \Exception('Plugin output must be an associative array');
}
// Check for minimum needed properties
$minimunNeededProperties = array(
'name',
'location_id',
'qu_id_purchase',
'qu_id_stock',
'qu_factor_purchase_to_stock',
'barcode'
);
foreach ($minimunNeededProperties as $prop)
{
if (!array_key_exists($prop, $pluginOutput))
{
throw new \Exception("Plugin output does not provide needed property $prop");
}
}
// $pluginOutput contains all needed properties here
// Check referenced entity ids are valid
$locationId = $pluginOutput['location_id'];
if (FindObjectInArrayByPropertyValue($this->Locations, 'id', $locationId) === null)
{
throw new \Exception("Location $locationId is not a valid location id");
}
$quIdPurchase = $pluginOutput['qu_id_purchase'];
if (FindObjectInArrayByPropertyValue($this->QuantityUnits, 'id', $quIdPurchase) === null)
{
throw new \Exception("Location $quIdPurchase is not a valid quantity unit id");
}
$quIdStock = $pluginOutput['qu_id_stock'];
if (FindObjectInArrayByPropertyValue($this->QuantityUnits, 'id', $quIdStock) === null)
{
throw new \Exception("Location $quIdStock is not a valid quantity unit id");
}
$quFactor = $pluginOutput['qu_factor_purchase_to_stock'];
if (empty($quFactor) || !is_numeric($quFactor))
{
throw new \Exception('Quantity unit factor is empty or not a number');
}
return $pluginOutput;
}
}

37
helpers/UrlManager.php Normal file
View File

@@ -0,0 +1,37 @@
<?php
namespace Grocy\Helpers;
class UrlManager
{
public function __construct(string $basePath)
{
if ($basePath === '/')
{
$this->BasePath = $this->GetBaseUrl();
}
else
{
$this->BasePath = $basePath;
}
}
protected $BasePath;
public function ConstructUrl($relativePath, $isResource = false)
{
if (DISABLE_URL_REWRITING === false || $isResource === true)
{
return rtrim($this->BasePath, '/') . $relativePath;
}
else // Is not a resource and URL rewriting is disabled
{
return rtrim($this->BasePath, '/') . '/index.php' . $relativePath;
}
}
private function GetBaseUrl()
{
return (isset($_SERVER['HTTPS']) ? "https" : "http") . "://$_SERVER[HTTP_HOST]";
}
}

112
helpers/extensions.php Normal file
View File

@@ -0,0 +1,112 @@
<?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 FindAllItemsInArrayByValue($array, $value, $operator = '==')
{
$returnArray = array();
foreach($array as $item)
{
switch($operator)
{
case '==':
if($item == $value)
{
$returnArray[] = $item;
}
break;
case '>':
if($item > $value)
{
$returnArray[] = $item;
}
break;
case '<':
if($item < $value)
{
$returnArray[] = $item;
}
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();
}
function RandomString($length, $allowedChars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ')
{
$randomString = '';
for ($i = 0; $i < $length; $i++)
{
$randomString .= $allowedChars[rand(0, strlen($allowedChars) - 1)];
}
return $randomString;
}
function IsAssociativeArray(array $array)
{
$keys = array_keys($array);
return array_keys($keys) !== $keys;
}

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();

176
localization/de.php Normal file
View File

@@ -0,0 +1,176 @@
<?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?',
'Manage API keys' => 'API-Keys verwalten',
'REST API & data model documentation' => 'REST-API & Datenmodell Dokumentation',
'API keys' => 'API-Keys',
'Create new API key' => 'Neuen API-Key erstellen',
'API key' => 'API-Key',
'Expires' => 'Läuft ab',
'Created' => 'Erstellt',
'This product is not in stock' => 'Dieses Produkt ist nicht vorrätig',
'This means #1 will be added to stock' => 'Das bedeutet #1 wird dem Bestand hinzugefügt',
'This means #1 will be removed from stock' => 'Das bedeutet #1 wird aus dem Bestand entfernt',
'This means it is estimated that a new execution of this habit is tracked #1 days after the last was tracked' => 'Das bedeutet, dass eine erneute Ausführung der Gewohnheit #1 Tage nach der letzten Ausführung geplant wird',
'Removed #1 #2 of #3 from stock' => '#1 #2 #3 aus dem Bestand entfernt',
'About grocy' => 'Über grocy',
'Close' => 'Schließen',
'#1 batteries are due to be charged within the next #2 days' => '#1 Batterien müssen in den nächsten #2 Tagen geladen werden',
'#1 batteries are overdue to be charged' => '#1 Batterien sind überfällig',
'#1 habits are due to be done within the next #2 days' => '#1 Gewohnheiten stehen in den nächsten #2 Tagen an',
'#1 habits are overdue to be done' => '#1 Gewohnheiten sind überfällig',
'Released on' => 'Veröffentlicht am',
'Consume #3 #1 of #2' => 'Verbrauche #3 #1 #2',
'Added #1 #2 of #3 to stock' => '#1 #2 #3 dem Bestand hinzugefügt',
'Stock amount of #1 is now #2 #3' => 'Es sind nun #2 #3 #1 im Bestand',
'Tracked execution of habit #1 on #2' => 'Ausführung von #1 am #2 erfasst',
'Tracked charge cylce of battery #1 on #2' => 'Ladezyklus für Batterie #1 am #2 erfasst',
'Consume all #1 which are currently in stock' => 'Verbrauche den kompletten Bestand von #1',
'All' => 'Alle',
'Track charge cycle of battery #1' => 'Erfasse einen Ladezyklus für Batterie #1',
'Track execution of habit #1' => 'Erfasse eine Ausführung von #1',
//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,58 @@
<?php
namespace Grocy\Middleware;
use \Grocy\Services\SessionService;
use \Grocy\Services\ApiKeyService;
class ApiKeyAuthMiddleware extends BaseMiddleware
{
public function __construct(\Slim\Container $container, string $sessionCookieName, string $apiKeyHeaderName)
{
parent::__construct($container);
$this->SessionCookieName = $sessionCookieName;
$this->ApiKeyHeaderName = $apiKeyHeaderName;
}
protected $SessionCookieName;
protected $ApiKeyHeaderName;
public function __invoke(\Slim\Http\Request $request, \Slim\Http\Response $response, callable $next)
{
$route = $request->getAttribute('route');
$routeName = $route->getName();
if ($this->ApplicationService->IsDemoInstallation())
{
$response = $next($request, $response);
}
else
{
$validSession = true;
$validApiKey = true;
$sessionService = new SessionService();
if (!isset($_COOKIE[$this->SessionCookieName]) || !$sessionService->IsValidSession($_COOKIE[$this->SessionCookieName]))
{
$validSession = false;
}
$apiKeyService = new ApiKeyService();
if (!$request->hasHeader($this->ApiKeyHeaderName) || !$apiKeyService->IsValidApiKey($request->getHeaderLine($this->ApiKeyHeaderName)))
{
$validApiKey = false;
}
if (!$validSession && !$validApiKey)
{
$response = $response->withStatus(401);
}
else
{
$response = $next($request, $response);
}
}
return $response;
}
}

View File

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

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);
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);
return $response->withHeader('Content-Type', 'application/json');
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace Grocy\Middleware;
use \Grocy\Services\SessionService;
class SessionAuthMiddleware extends BaseMiddleware
{
public function __construct(\Slim\Container $container, string $sessionCookieName)
{
parent::__construct($container);
$this->SessionCookieName = $sessionCookieName;
}
protected $SessionCookieName;
public function __invoke(\Slim\Http\Request $request, \Slim\Http\Response $response, callable $next)
{
$route = $request->getAttribute('route');
$routeName = $route->getName();
if ($routeName === 'root' || $this->ApplicationService->IsDemoInstallation())
{
define('AUTHENTICATED', $this->ApplicationService->IsDemoInstallation());
$response = $next($request, $response);
}
else
{
$sessionService = new SessionService();
if ((!isset($_COOKIE[$this->SessionCookieName]) || !$sessionService->IsValidSession($_COOKIE[$this->SessionCookieName])) && $routeName !== 'login')
{
define('AUTHENTICATED', false);
$response = $response->withRedirect($this->AppContainer->UrlManager->ConstructUrl('/login'));
}
else
{
define('AUTHENTICATED', $routeName !== 'login');
$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';

7
migrations/0022.sql Normal file
View File

@@ -0,0 +1,7 @@
CREATE TABLE api_keys (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
api_key TEXT NOT NULL UNIQUE,
expires DATETIME,
last_used DATETIME,
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
)

1
migrations/0023.sql Normal file
View File

@@ -0,0 +1 @@
DELETE FROM sessions

2
migrations/0024.sql Normal file
View File

@@ -0,0 +1,2 @@
ALTER TABLE sessions
ADD COLUMN last_used DATETIME

View File

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

184
public/css/grocy.css Normal file
View File

@@ -0,0 +1,184 @@
body {
padding-top: 50px;
font-family: 'Noto Sans', sans-serif;
}
.navbar-fixed-top {
background-color: #e5e5e5;
border-bottom: 2px solid;
border-color: #d6d6d6;
}
.navbar-brand {
font-weight: bold;
letter-spacing: -5px;
font-size: 2.2em;
color: #0b024c !important;
margin-left: 0 !important;
padding-left: 5px !important;
}
.navbar-fixed-side {
top: 51px;
padding-top: 20px;
margin-bottom: 20px;
background-color: #e5e5e5;
border-right: 2px solid #d6d6d6;
max-width: 260px;
border-left: 0;
}
#sidebar {
overflow-y: auto;
}
@media (min-width: 768px) {
#navbar-mobile {
display: none !important;
}
.nav-copyright {
padding-bottom: 100px;
}
}
@media (max-width: 768px) {
.navbar-brand {
margin-left: 25px !important;
}
}
.sidebar-nav > li > a {
padding-right: 20px;
padding-left: 20px;
transition: all 0.3s;
}
.sidebar-nav > li > a:hover {
box-shadow: inset 5px 0 0 #337ab7;
transition: all 0.3s;
}
.sidebar-nav > li > a:focus {
box-shadow: inset 5px 0 0 #ab2230;
transition: all 0.3s;
}
.sidebar-nav > .active > a,
.sidebar-nav > .active > a:hover,
.sidebar-nav > .active > a:focus {
background-color: #d6d6d6;
box-shadow: inset 5px 0 0 #ab2230;
transition: all 0.3s;
}
.nav > li.disabled > a,
.navbar-default .navbar-nav > .disabled > a {
color: #a7a7a7;
}
.nav-copyright {
color: #a7a7a7;
font-size: 11px;
text-align: center;
}
.nav-copyright > li > a {
padding-top: 0 !important;
padding-bottom: 0 !important;
}
.navbar-default .navbar-nav > .open > a {
background-color: #d6d6d6 !important;
}
.dropdown-menu > li > a:hover,
.dropdown-menu > li > a:focus {
background-color: #e5e5e5 !important;
}
.well {
background-color: #e5e5e5;
padding-right: 25px;
padding-left: 25px;
}
td {
vertical-align: middle !important;
}
.responsive-button {
white-space: normal;
}
.discrete-link {
color: inherit !important;
transition: all 0.3s !important;
}
a.discrete-link:hover {
color: #337ab7 !important;
text-decoration: none !important;
transition: all 0.3s !important;
}
a.discrete-link:focus {
color: #ab2230 !important;
text-decoration: none !important;
transition: all 0.3s !important;
}
.table td.fit-content,
.table th.fit-content {
white-space: nowrap;
width: 1%;
}
.dataTables_info,
.dataTables_length,
.dataTables_filter {
font-style: italic;
}
.timeago-contextual {
font-style: italic;
font-size: 0.8em;
}
.disabled,
.no-real-button {
pointer-events: none;
margin-top: 2px;
margin-bottom: 2px;
}
.discrete-content-separator-2x {
padding-top: 10px;
padding-bottom: 10px;
}
.warning-bg {
background-color: #fcf8e3 !important;
}
.error-bg {
background-color: #f2dede !important;
}
.info-bg {
background-color: #afd9ee !important;
}
#toast-container > div {
opacity: 1;
filter: alpha(opacity=100);
}
.toast-success {
background-color: #4c994c;
}
#toast-container > div {
box-shadow: none;
}

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];
}
}
};

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

@@ -0,0 +1,102 @@
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;
RefreshContextualTimeago = function()
{
$('time.timeago').timeago();
}
RefreshContextualTimeago();
toastr.options = {
toastClass: 'alert',
closeButton: true,
timeOut: 20000,
extendedTimeOut: 5000
};
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,44 @@
$(document).on('click', '.battery-delete-button', function(e)
{
var objectName = $(e.currentTarget).attr('data-battery-name');
var objectId = $(e.currentTarget).attr('data-battery-id');
bootbox.confirm({
message: L('Are you sure to delete battery "#1"?', objectName),
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/' + objectId,
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'))
});

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