Compare commits
54 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
0954b5a741 | ||
|
02b6c3b721 | ||
|
6fa4e13ba2 | ||
|
9837f79f9c | ||
|
6e4cd22118 | ||
|
ca00dd8e2d | ||
|
5455ec7bde | ||
|
2e7af1b050 | ||
|
89bae8d25e | ||
|
5b5c272909 | ||
|
3e394a3840 | ||
|
ab8094e1c0 | ||
|
bbb5f1c7c7 | ||
|
b607f188af | ||
|
9ab1a674fe | ||
|
2f0a1391b7 | ||
|
a9a1358b08 | ||
|
4853174d03 | ||
|
538d789366 | ||
|
0751919b82 | ||
|
99b2a84667 | ||
|
9bd6aac09c | ||
|
7be35a90c1 | ||
|
eae5b8bad9 | ||
|
0c85342404 | ||
|
9ddcdb3ab2 | ||
|
1c537cf5da | ||
|
607a90cccc | ||
|
3d1c6fc5f0 | ||
|
df1d3677e8 | ||
|
4da2ac9b35 | ||
|
b4ae7d8538 | ||
|
870b679e0e | ||
|
4656a85732 | ||
|
4949913ccb | ||
|
580bd5ac0c | ||
|
2bf3448d18 | ||
|
e9bc51ca3d | ||
|
5ddae116e0 | ||
|
13566bc6fd | ||
|
642f95a3f8 | ||
|
5a1d21ef31 | ||
|
7dcd39f82f | ||
|
655aa89bd6 | ||
|
feb88ab685 | ||
|
79b4bad014 | ||
|
bcd5092427 | ||
|
554a83fa01 | ||
|
57acb62520 | ||
|
52ed5f2285 | ||
|
dd1d253ea5 | ||
|
e40979a874 | ||
|
2ddbc2656b | ||
|
7351fce395 |
201
.gitignore
vendored
@@ -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
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"phpserver.relativePath": "public"
|
||||
}
|
148
Grocy.php
@@ -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");
|
||||
}
|
||||
}
|
@@ -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 . ')');
|
||||
}
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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();
|
||||
}
|
||||
}
|
53
README.md
@@ -1,22 +1,63 @@
|
||||
# grocy
|
||||
ERP beyond your fridge
|
||||
|
||||
## Give it a try
|
||||
Public demo of the latest version → [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 → [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
@@ -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();
|
@@ -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"
|
||||
}
|
||||
}
|
||||
|
@@ -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\*
|
||||
|
@@ -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
@@ -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);
|
||||
|
28
controllers/BaseApiController.php
Normal 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
|
||||
));
|
||||
}
|
||||
}
|
36
controllers/BaseController.php
Normal 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;
|
||||
}
|
47
controllers/BatteriesApiController.php
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
68
controllers/BatteriesController.php
Normal 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'
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
19
controllers/CliController.php
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
80
controllers/GenericEntityApiController.php
Normal 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);
|
||||
}
|
||||
}
|
47
controllers/HabitsApiController.php
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
70
controllers/HabitsController.php
Normal 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'
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
76
controllers/LoginController.php
Normal 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;
|
||||
}
|
||||
}
|
48
controllers/OpenApiController.php
Normal 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"));
|
||||
}
|
||||
}
|
126
controllers/StockApiController.php
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
162
controllers/StockController.php
Normal 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
@@ -1,2 +1,4 @@
|
||||
*
|
||||
!.gitignore
|
||||
!viewcache
|
||||
!plugins
|
||||
|
3
data/plugins/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
*
|
||||
!.gitignore
|
||||
!DemoBarcodeLookupPlugin.php
|
78
data/plugins/DemoBarcodeLookupPlugin.php
Normal 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
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
103
grocy.js
@@ -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
80
helpers/BaseBarcodeLookupPlugin.php
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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"}}'
|
||||
);
|
58
middleware/ApiKeyAuthMiddleware.php
Normal 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;
|
||||
}
|
||||
}
|
17
middleware/BaseMiddleware.php
Normal 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;
|
||||
}
|
20
middleware/CliMiddleware.php
Normal 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');
|
||||
}
|
||||
}
|
||||
}
|
12
middleware/JsonMiddleware.php
Normal 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');
|
||||
}
|
||||
}
|
44
middleware/SessionAuthMiddleware.php
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1 @@
|
||||
ALTER TABLE shopping_list RENAME TO shopping_list_old
|
8
migrations/0017.sql
Normal 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
@@ -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
@@ -0,0 +1 @@
|
||||
DROP TABLE shopping_list_old
|
6
migrations/0020.sql
Normal 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
@@ -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
@@ -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
@@ -0,0 +1 @@
|
||||
DELETE FROM sessions
|
2
migrations/0024.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE sessions
|
||||
ADD COLUMN last_used DATETIME
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB |
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB |
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
184
public/css/grocy.css
Normal 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
After Width: | Height: | Size: 2.2 KiB |
3
public/index.php
Normal file
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../app.php';
|
33
public/js/extensions.js
Normal 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
@@ -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));
|
||||
};
|
44
public/viewjs/batteries.js
Normal 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'))
|
||||
});
|