Compare commits

...

36 Commits

Author SHA1 Message Date
Bernd Bestel
9c2ee58433 This is 1.5 2017-07-25 20:09:41 +02:00
Bernd Bestel
f84593882d Reference most recent major versions instead of specific ones 2017-07-25 20:08:59 +02:00
Bernd Bestel
1241261ca4 Added habit tracking 2017-07-25 20:03:31 +02:00
Bernd Bestel
ebe92335a6 Fix SQLite PDO issue on unix systems 2017-07-25 19:23:08 +02:00
Bernd Bestel
35f2f33ae3 Fix build includes 2017-06-04 18:39:05 +02:00
Bernd Bestel
f0f84b304b Added config/instructions for nginx/Apache URL rewriting - fixes #1 2017-06-04 18:32:34 +02:00
Bernd Bestel
23146417e6 Use session/cookie based authentication with login form instead of basic auth 2017-06-04 18:28:08 +02:00
Bernd Bestel
bd3155d39b Added screenshots 2017-04-23 11:11:13 +02:00
Bernd Bestel
b5fe0a642b Load also last purchased date from stock_log instead of stock 2017-04-22 21:51:07 +02:00
Bernd Bestel
b4b29878db Change FIFO to "First expiring first, then first in first out" 2017-04-22 21:40:22 +02:00
Bernd Bestel
9e68d38df8 Resolve X in date inputs to 2999-12-31 (which is used as "best before date infinite") 2017-04-22 18:04:39 +02:00
Bernd Bestel
574d363d7c Allow date input in form of MMDD and auto append current year 2017-04-22 17:47:27 +02:00
Bernd Bestel
69a011bc86 Little wording changes 2017-04-22 15:47:55 +02:00
Bernd Bestel
fe969c57c4 Changed debug PHP version to 7.1 2017-04-22 12:29:14 +02:00
Bernd Bestel
88d8b72c57 Reorganized sidebar menu 2017-04-22 12:28:43 +02:00
Bernd Bestel
5639797c8d Add note about barcode readers 2017-04-22 11:41:44 +02:00
Bernd Bestel
049a9cee06 Also show quantity unit on dashboard next to amount 2017-04-22 11:38:43 +02:00
Bernd Bestel
14faf57a9e Small productivity improvement (noticed on first own production use :D) 2017-04-22 11:36:05 +02:00
Bernd Bestel
e19b548eff Improved favicon 2017-04-22 09:59:26 +02:00
Bernd Bestel
e3d84c40f7 Added a favicon 2017-04-22 09:37:38 +02:00
Bernd Bestel
50d49219a5 Renamed shopping list item route 2017-04-22 09:27:30 +02:00
Bernd Bestel
96209c852c Add flow to add a new product with prefilled barcode 2017-04-21 22:50:16 +02:00
Bernd Bestel
8e40c50cc1 Validate that best before date is min. today 2017-04-21 19:15:03 +02:00
Bernd Bestel
4b0f0141c9 Fix date input arrow key behavior 2017-04-21 19:10:39 +02:00
Bernd Bestel
d1bd21a601 Finished shopping list feature 2017-04-21 19:02:00 +02:00
Bernd Bestel
c6925ba4c3 Started working on shopping list feature 2017-04-21 15:36:04 +02:00
Bernd Bestel
52e311d847 Show missing products on dashboard 2017-04-21 13:21:09 +02:00
Bernd Bestel
f2f18d260d Show how much is in stock on dashboard 2017-04-21 12:50:53 +02:00
Bernd Bestel
1d293741ba Code review 2017-04-21 12:30:08 +02:00
Bernd Bestel
5db288fc3c Add hint when barcode lookup is disabled 2017-04-21 12:03:56 +02:00
Bernd Bestel
d628f9b3ca Make DB migrations fully automatic 2017-04-21 11:52:24 +02:00
Bernd Bestel
fe8a6d96e4 Added keyboard shortcuts for add product/barcode dialogs 2017-04-20 23:42:06 +02:00
Bernd Bestel
bd16b8c851 Added flow to directly add articles and barcodes form purchase and inventory view 2017-04-20 22:01:14 +02:00
Bernd Bestel
c4a22c18f7 This is 1.0 2017-04-20 17:10:21 +02:00
Bernd Bestel
e38c24f9ed Going straight to 1.0... 2017-04-19 21:09:28 +02:00
Bernd Bestel
83a7534a74 Hide barcode in select dropdown but search in it 2017-04-18 23:04:26 +02:00
57 changed files with 2566 additions and 379 deletions

1
.gitignore vendored
View File

@@ -198,6 +198,5 @@ FakesAssemblies/
/bower_components /bower_components
/vendor /vendor
/.release /.release
/config.php
/composer.phar /composer.phar
/composer.lock /composer.lock

4
.htaccess Normal file
View File

@@ -0,0 +1,4 @@
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.php [QSA,L]

148
Grocy.php Normal file
View File

@@ -0,0 +1,148 @@
<?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(utf8_encode($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(utf8_encode($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 = file_get_contents(__DIR__ . '/version.txt');
}
return self::$InstalledVersion;
}
/**
* @return boolean
*/
public static function IsValidSession($sessionKey)
{
if ($sessionKey === null || empty($sessionKey))
{
return false;
}
else
{
return file_exists(__DIR__ . "/data/sessions/$sessionKey.txt");
}
}
/**
* @return string
*/
public static function CreateSession()
{
if (!file_exists(__DIR__ . '/data/sessions'))
{
mkdir(__DIR__ . '/data/sessions');
}
$now = time();
foreach (new FilesystemIterator(__DIR__ . '/data/sessions') as $file)
{
if ($now - $file->getCTime() >= 2678400) //31 days
{
unlink(__DIR__ . '/data/sessions/' . $file->getFilename());
}
}
$newSessionKey = uniqid() . uniqid() . uniqid();
file_put_contents(__DIR__ . "/data/sessions/$newSessionKey.txt", '');
return $newSessionKey;
}
public static function RemoveSession($sessionKey)
{
unlink(__DIR__ . "/data/sessions/$sessionKey.txt");
}
}

View File

@@ -13,8 +13,10 @@ class GrocyDbMigrator
qu_id_purchase INTEGER NOT NULL, qu_id_purchase INTEGER NOT NULL,
qu_id_stock INTEGER NOT NULL, qu_id_stock INTEGER NOT NULL,
qu_factor_purchase_to_stock REAL NOT NULL, qu_factor_purchase_to_stock REAL NOT NULL,
barcode TEXT UNIQUE, barcode TEXT,
created_timestamp DATETIME DEFAULT (datetime('now', 'localtime')) 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'))
)" )"
); );
@@ -23,7 +25,7 @@ class GrocyDbMigrator
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
name TEXT NOT NULL UNIQUE, name TEXT NOT NULL UNIQUE,
description TEXT, description TEXT,
created_timestamp DATETIME DEFAULT (datetime('now', 'localtime')) row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
)" )"
); );
@@ -32,7 +34,7 @@ class GrocyDbMigrator
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
name TEXT NOT NULL UNIQUE, name TEXT NOT NULL UNIQUE,
description TEXT, description TEXT,
created_timestamp DATETIME DEFAULT (datetime('now', 'localtime')) row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
)" )"
); );
@@ -43,20 +45,23 @@ class GrocyDbMigrator
amount INTEGER NOT NULL, amount INTEGER NOT NULL,
best_before_date DATE, best_before_date DATE,
purchased_date DATE DEFAULT (datetime('now', 'localtime')), purchased_date DATE DEFAULT (datetime('now', 'localtime')),
stock_id TEXT NOT NULL stock_id TEXT NOT NULL,
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
)" )"
); );
self::ExecuteMigrationWhenNeeded($pdo, 5, " self::ExecuteMigrationWhenNeeded($pdo, 5, "
CREATE TABLE consumptions ( CREATE TABLE stock_log (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
product_id INTEGER NOT NULL, product_id INTEGER NOT NULL,
amount INTEGER NOT NULL, amount INTEGER NOT NULL,
best_before_date DATE, best_before_date DATE,
purchased_date DATE, purchased_date DATE,
used_date DATE DEFAULT (datetime('now', 'localtime')), used_date DATE,
spoiled INTEGER NOT NULL DEFAULT 0, spoiled INTEGER NOT NULL DEFAULT 0,
stock_id TEXT NOT NULL stock_id TEXT NOT NULL,
transaction_type TEXT NOT NULL,
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
)" )"
); );
@@ -66,18 +71,75 @@ class GrocyDbMigrator
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 ('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);" 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;"
);
} }
private static function ExecuteMigrationWhenNeeded(PDO $pdo, int $migrationId, string $sql) private static function ExecuteMigrationWhenNeeded(PDO $pdo, int $migrationId, string $sql)
{ {
if ($pdo->query("SELECT COUNT(*) FROM migrations WHERE migration = $migrationId")->fetchColumn() == 0) $rowCount = Grocy::ExecuteDbQuery($pdo, 'SELECT COUNT(*) FROM migrations WHERE migration = ' . $migrationId)->fetchColumn();
if (intval($rowCount) === 0)
{ {
if ($pdo->exec(utf8_encode($sql)) === false) Grocy::ExecuteDbStatement($pdo, $sql);
{ Grocy::ExecuteDbStatement($pdo, 'INSERT INTO migrations (migration) VALUES (' . $migrationId . ')');
throw new Exception($pdo->errorInfo());
}
$pdo->exec('INSERT INTO migrations (migration) VALUES (' . $migrationId . ')');
} }
} }
} }

View File

@@ -3,27 +3,66 @@
class GrocyDemoDataGenerator class GrocyDemoDataGenerator
{ {
public static function PopulateDemoData(PDO $pdo) public static function PopulateDemoData(PDO $pdo)
{
$rowCount = Grocy::ExecuteDbQuery($pdo, 'SELECT COUNT(*) FROM migrations WHERE migration = -1')->fetchColumn();
if (intval($rowCount) === 0)
{ {
$sql = " $sql = "
UPDATE locations SET name = 'Vorratskammer', description = '' WHERE id = 1; UPDATE locations SET name = 'Vorratskammer', description = '' WHERE id = 1;
INSERT INTO locations (name) VALUES ('S<><53>igkeitenschrank'); INSERT INTO locations (name) VALUES ('S<><53>igkeitenschrank'); --2
INSERT INTO locations (name) VALUES ('Konvervenschrank'); INSERT INTO locations (name) VALUES ('Konservenschrank'); --3
INSERT INTO locations (name) VALUES ('K<>hlschrank'); --4
UPDATE quantity_units SET name = 'St<53>ck' WHERE id = 1; UPDATE quantity_units SET name = 'St<53>ck' WHERE id = 1;
INSERT INTO quantity_units (name) VALUES ('Packung'); 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
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('Gummib<69>rchen', 2, 2, 2, 1); 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) VALUES ('Chips', 2, 2, 2, 1); INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, min_stock_amount) VALUES ('Gummib<69>rchen', 2, 2, 2, 1, 8); --3
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('Eier', 1, 2, 1, 10); 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 stock (product_id, amount, best_before_date, stock_id) VALUES (3, 5, date('now', '+180 day'), '".uniqid()."'); INSERT INTO habits (name, period_type, period_days) VALUES ('Changed towels in the bathroom', 'manually', 5); --1
INSERT INTO stock (product_id, amount, best_before_date, stock_id) VALUES (4, 5, date('now', '+180 day'), '".uniqid()."'); INSERT INTO habits (name, period_type, period_days) VALUES ('Cleaned the kitchen floor', 'dynamic-regular', 7); --2
INSERT INTO stock (product_id, amount, best_before_date, stock_id) VALUES (5, 5, date('now', '+25 day'), '".uniqid()."');
INSERT INTO migrations (migration) VALUES (-1);
"; ";
if ($pdo->exec(utf8_encode($sql)) === false) Grocy::ExecuteDbStatement($pdo, $sql);
{
throw new Exception($pdo->errorInfo()); 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')));
} }
} }
} }

59
GrocyLogicHabits.php Normal file
View File

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

View File

@@ -2,10 +2,20 @@
class GrocyLogicStock class GrocyLogicStock
{ {
const TRANSACTION_TYPE_PURCHASE = 'purchase';
const TRANSACTION_TYPE_CONSUME = 'consume';
const TRANSACTION_TYPE_INVENTORY_CORRECTION = 'inventory-correction';
public static function GetCurrentStock() public static function GetCurrentStock()
{ {
$db = Grocy::GetDbConnectionRaw(); $sql = 'SELECT * from stock_current';
return $db->query('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')->fetchAll(PDO::FETCH_OBJ); 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) public static function GetProductDetails(int $productId)
@@ -14,8 +24,8 @@ class GrocyLogicStock
$product = $db->products($productId); $product = $db->products($productId);
$productStockAmount = $db->stock()->where('product_id', $productId)->sum('amount'); $productStockAmount = $db->stock()->where('product_id', $productId)->sum('amount');
$productLastPurchased = $db->stock()->where('product_id', $productId)->max('purchased_date'); $productLastPurchased = $db->stock_log()->where('product_id', $productId)->where('transaction_type', self::TRANSACTION_TYPE_PURCHASE)->max('purchased_date');
$productLastUsed = $db->consumptions()->where('product_id', $productId)->max('used_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); $quPurchase = $db->quantity_units($product->qu_id_purchase);
$quStock = $db->quantity_units($product->qu_id_stock); $quStock = $db->quantity_units($product->qu_id_stock);
@@ -29,12 +39,48 @@ class GrocyLogicStock
); );
} }
public static function ConsumeProduct(int $productId, int $amount, bool $spoiled) 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(); $db = Grocy::GetDbConnection();
$productStockAmount = $db->stock()->where('product_id', $productId)->sum('amount'); $productStockAmount = $db->stock()->where('product_id', $productId)->sum('amount');
$potentialStockEntries = $db->stock()->where('product_id', $productId)->orderBy('purchased_date', 'ASC')->fetchAll(); //FIFO $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) if ($amount > $productStockAmount)
{ {
@@ -50,30 +96,34 @@ class GrocyLogicStock
if ($amount >= $stockEntry->amount) //Take the whole stock entry if ($amount >= $stockEntry->amount) //Take the whole stock entry
{ {
$consumptionRow = $db->consumptions()->createRow(array( $logRow = $db->stock_log()->createRow(array(
'product_id' => $stockEntry->product_id, 'product_id' => $stockEntry->product_id,
'amount' => $stockEntry->amount, 'amount' => $stockEntry->amount * -1,
'best_before_date' => $stockEntry->best_before_date, 'best_before_date' => $stockEntry->best_before_date,
'purchased_date' => $stockEntry->purchased_date, 'purchased_date' => $stockEntry->purchased_date,
'used_date' => date('Y-m-d'),
'spoiled' => $spoiled, 'spoiled' => $spoiled,
'stock_id' => $stockEntry->stock_id 'stock_id' => $stockEntry->stock_id,
'transaction_type' => $transactionType
)); ));
$consumptionRow->save(); $logRow->save();
$amount -= $stockEntry->amount; $amount -= $stockEntry->amount;
$stockEntry->delete(); $stockEntry->delete();
} }
else //Stock entry amount is > than needed amount -> split the stock entry resp. update the amount else //Stock entry amount is > than needed amount -> split the stock entry resp. update the amount
{ {
$consumptionRow = $db->consumptions()->createRow(array( $logRow = $db->stock_log()->createRow(array(
'product_id' => $stockEntry->product_id, 'product_id' => $stockEntry->product_id,
'amount' => $amount, 'amount' => $amount * -1,
'best_before_date' => $stockEntry->best_before_date, 'best_before_date' => $stockEntry->best_before_date,
'purchased_date' => $stockEntry->purchased_date, 'purchased_date' => $stockEntry->purchased_date,
'used_date' => date('Y-m-d'),
'spoiled' => $spoiled, 'spoiled' => $spoiled,
'stock_id' => $stockEntry->stock_id 'stock_id' => $stockEntry->stock_id,
'transaction_type' => $transactionType
)); ));
$consumptionRow->save(); $logRow->save();
$restStockAmount = $stockEntry->amount - $amount; $restStockAmount = $stockEntry->amount - $amount;
$amount = 0; $amount = 0;
@@ -86,4 +136,56 @@ class GrocyLogicStock
return true; return true;
} }
else
{
throw new Exception("Transaction type $transactionType is not valid (GrocyLogicStock.ConsumeProduct)");
}
}
public static function InventoryProduct(int $productId, int $newAmount, string $bestBeforeDate)
{
$db = Grocy::GetDbConnection();
$productStockAmount = $db->stock()->where('product_id', $productId)->sum('amount');
if ($newAmount > $productStockAmount)
{
$amountToAdd = $newAmount - $productStockAmount;
self::AddProduct($productId, $amountToAdd, $bestBeforeDate, self::TRANSACTION_TYPE_INVENTORY_CORRECTION);
}
else if ($newAmount < $productStockAmount)
{
$amountToRemove = $productStockAmount - $newAmount;
self::ConsumeProduct($productId, $amountToRemove, false, self::TRANSACTION_TYPE_INVENTORY_CORRECTION);
}
return true;
}
public static function AddMissingProductsToShoppingList()
{
$db = Grocy::GetDbConnection();
$missingProducts = self::GetMissingProducts();
foreach ($missingProducts as $missingProduct)
{
$product = $db->products()->where('id', $missingProduct->id)->fetch();
$amount = ceil($missingProduct->amount_missing / $product->qu_factor_purchase_to_stock);
$alreadyExistingEntry = $db->shopping_list()->where('product_id', $missingProduct->id)->fetch();
if ($alreadyExistingEntry) //Update
{
$alreadyExistingEntry->update(array(
'amount_autoadded' => $amount
));
}
else //Insert
{
$shoppinglistRow = $db->shopping_list()->createRow(array(
'product_id' => $missingProduct->id,
'amount_autoadded' => $amount
));
$shoppinglistRow->save();
}
}
}
} }

View File

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

View File

@@ -11,10 +11,22 @@ For now my main focus is on stock management, ERP your fridge!
Public demo of the latest version &rarr; [https://grocy.projectdemos.berrnd.org](https://grocy.projectdemos.berrnd.org) Public demo of the latest version &rarr; [https://grocy.projectdemos.berrnd.org](https://grocy.projectdemos.berrnd.org)
## How to install ## 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 `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 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.
## Todo If you use nginx as your webserver, please include `try_files $uri /index.php;` in your location block.
A lot...
## 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.
## Screenshots
#### Dashboard
![Dashboard](https://github.com/berrnd/grocy/raw/master/publication_assets/dashboard.png "Dashboard")
#### Purchase - with barcode scan
![Purchase - with barcode scan](https://github.com/berrnd/grocy/raw/master/publication_assets/purchase-with-barcode.gif "purchase-with-barcode")
#### Consume - with manual search
![Consume - with manual search](https://github.com/berrnd/grocy/raw/master/publication_assets/consume.gif "consume")
## License ## License
The MIT License (MIT) The MIT License (MIT)

View File

@@ -2,20 +2,21 @@
"name": "asp.net", "name": "asp.net",
"private": true, "private": true,
"dependencies": { "dependencies": {
"bootstrap": "3.3.7", "bootstrap": "^3.3.7",
"font-awesome": "4.7.0", "font-awesome": "^4.7.0",
"bootbox": "4.4.0", "bootbox": "^4.4.0",
"jquery.serializeJSON": "2.7.2", "jquery.serializeJSON": "^2.8.1",
"bootstrap-validator": "0.11.9", "bootstrap-validator": "^0.11.9",
"bootstrap-datepicker": "1.6.4", "bootstrap-datepicker": "^1.7.1",
"moment": "2.18.1", "moment": "^2.18.1",
"bootstrap-combobox": "1.1.8", "bootstrap-combobox": "^1.1.8",
"datatables.net": "1.10.13", "datatables.net": "^1.10.15",
"datatables.net-bs": "2.1.1", "datatables.net-bs": "^2.1.1",
"datatables.net-responsive": "2.1.1", "datatables.net-responsive": "^2.1.1",
"datatables.net-responsive-bs": "2.1.1", "datatables.net-responsive-bs": "^2.1.1",
"jquery-timeago": "1.5.4", "jquery-timeago": "^1.6.1",
"toastr": "2.1.3", "toastr": "^2.1.3",
"tagmanager": "3.0.2" "tagmanager": "^3.0.2",
"eonasdan-bootstrap-datetimepicker": "^4.17.47"
} }
} }

View File

@@ -7,5 +7,6 @@ mkdir "%releasePath%"
for /f "tokens=*" %%a in ('type version.txt') do set version=%%a for /f "tokens=*" %%a in ('type version.txt') do set version=%%a
del "%releasePath%\grocy_%version%.zip" 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!composer.phar -xr!grocy.phpproj -xr!grocy.phpproj.user -xr!grocy.sln "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!composer.phar -xr!grocy.phpproj -xr!grocy.phpproj.user -xr!grocy.sln -xr!bower.json -xr!publication_assets
"build_tools\7za.exe" d "%releasePath%\grocy_%version%.zip" data\add_before_end_body.html data\demo.txt data\grocy.db data\.gitignore config.php bower.json "build_tools\7za.exe" a -r "%releasePath%\grocy_%version%.zip" "%projectPath%\.htaccess"
"build_tools\7za.exe" d "%releasePath%\grocy_%version%.zip" data\*.* data\sessions

View File

@@ -2,7 +2,6 @@
"require": { "require": {
"slim/slim": "^3.8", "slim/slim": "^3.8",
"slim/php-view": "^2.2", "slim/php-view": "^2.2",
"morris/lessql": "^0.3.4", "morris/lessql": "^0.3.4"
"tuupola/slim-basic-auth": "^2.2"
} }
} }

View File

@@ -75,3 +75,29 @@ Grocy.EmptyElementWhenMatches = function(selector, text)
$(selector).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));
}

View File

@@ -1,63 +0,0 @@
<?php
class Grocy
{
private static $DbConnectionRaw;
/**
* @return PDO
*/
public static function GetDbConnectionRaw()
{
if (self::$DbConnectionRaw == null)
{
$newDb = !file_exists(__DIR__ . '/data/grocy.db');
$pdo = new PDO('sqlite:' . __DIR__ . '/data/grocy.db');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
if ($newDb)
{
$pdo->exec("CREATE TABLE migrations (migration INTEGER NOT NULL UNIQUE, execution_time_timestamp DATETIME DEFAULT (datetime('now', 'localtime')), PRIMARY KEY(migration)) WITHOUT ROWID");
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()
{
if (self::$DbConnection == null)
{
self::$DbConnection = new LessQL\Database(self::GetDbConnectionRaw());
}
return self::$DbConnection;
}
public static function IsDemoInstallation()
{
return file_exists(__DIR__ . '/data/demo.txt');
}
private static $InstalledVersion;
public static function GetInstalledVersion()
{
if (self::$InstalledVersion == null)
{
self::$InstalledVersion = file_get_contents(__DIR__ . '/version.txt');
}
return self::$InstalledVersion;
}
}

View File

@@ -8,6 +8,12 @@
</RootNamespace> </RootNamespace>
<ProjectTypeGuids>{A0786B88-2ADB-4C21-ABE8-AA2D79766269}</ProjectTypeGuids> <ProjectTypeGuids>{A0786B88-2ADB-4C21-ABE8-AA2D79766269}</ProjectTypeGuids>
<AssemblyName>grocy</AssemblyName> <AssemblyName>grocy</AssemblyName>
<SaveServerSettingsInUserFile>false</SaveServerSettingsInUserFile>
<Runtime>PHP</Runtime>
<RuntimeVersion>7.1</RuntimeVersion>
<EnvName>PHPDev</EnvName>
<PHPDevHostName>localhost</PHPDevHostName>
<PHPDevAutoPort>true</PHPDevAutoPort>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<IncludeDebugInformation>true</IncludeDebugInformation> <IncludeDebugInformation>true</IncludeDebugInformation>
@@ -18,12 +24,20 @@
<ItemGroup> <ItemGroup>
<Compile Include="config-dist.php" /> <Compile Include="config-dist.php" />
<Compile Include="Grocy.php" /> <Compile Include="Grocy.php" />
<Compile Include="GrocyLogicHabits.php" />
<Compile Include="GrocyLogicStock.php" /> <Compile Include="GrocyLogicStock.php" />
<Compile Include="GrocyDemoDataGenerator.php" /> <Compile Include="GrocyDemoDataGenerator.php" />
<Compile Include="GrocyPhpHelper.php" /> <Compile Include="GrocyPhpHelper.php" />
<Compile Include="GrocyDbMigrator.php" /> <Compile Include="GrocyDbMigrator.php" />
<Compile Include="index.php" /> <Compile Include="index.php" />
<Compile Include="views\consumption.php" /> <Compile Include="views\habittracking.php" />
<Compile Include="views\consume.php" />
<Compile Include="views\login.php" />
<Compile Include="views\inventory.php" />
<Compile Include="views\habits.php" />
<Compile Include="views\habitform.php" />
<Compile Include="views\shoppinglistform.php" />
<Compile Include="views\shoppinglist.php" />
<Compile Include="views\purchase.php" /> <Compile Include="views\purchase.php" />
<Compile Include="views\quantityunitform.php" /> <Compile Include="views\quantityunitform.php" />
<Compile Include="views\locationform.php" /> <Compile Include="views\locationform.php" />
@@ -31,13 +45,15 @@
<Compile Include="views\locations.php" /> <Compile Include="views\locations.php" />
<Compile Include="views\quantityunits.php" /> <Compile Include="views\quantityunits.php" />
<Compile Include="views\products.php" /> <Compile Include="views\products.php" />
<Compile Include="views\dashboard.php" /> <Compile Include="views\habitsoverview.php" />
<Compile Include="views\stockoverview.php" />
<Compile Include="views\layout.php" /> <Compile Include="views\layout.php" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Content Include="bower.json" /> <Content Include="bower.json" />
<None Include="build.bat" /> <None Include="build.bat" />
<Content Include="composer.json" /> <Content Include="composer.json" />
<Content Include="grocy.png" />
<Content Include="grocy.js" /> <Content Include="grocy.js" />
<None Include="README.md" /> <None Include="README.md" />
<Content Include="README.html"> <Content Include="README.html">
@@ -47,8 +63,16 @@
<Content Include="robots.txt" /> <Content Include="robots.txt" />
<Content Include="style.css" /> <Content Include="style.css" />
<Content Include="version.txt" /> <Content Include="version.txt" />
<Content Include="views\consumption.js" /> <Content Include="views\habittracking.js" />
<Content Include="views\dashboard.js" /> <Content Include="views\consume.js" />
<Content Include="views\habitsoverview.js" />
<Content Include="views\stockoverview.js" />
<Content Include="views\inventory.js" />
<Content Include="views\login.js" />
<Content Include="views\habits.js" />
<Content Include="views\habitform.js" />
<Content Include="views\shoppinglistform.js" />
<Content Include="views\shoppinglist.js" />
<Content Include="views\purchase.js" /> <Content Include="views\purchase.js" />
<Content Include="views\quantityunitform.js" /> <Content Include="views\quantityunitform.js" />
<Content Include="views\locationform.js" /> <Content Include="views\locationform.js" />

BIN
grocy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

326
index.php
View File

@@ -4,17 +4,19 @@ use \Psr\Http\Message\ServerRequestInterface as Request;
use \Psr\Http\Message\ResponseInterface as Response; use \Psr\Http\Message\ResponseInterface as Response;
use Slim\Views\PhpRenderer; use Slim\Views\PhpRenderer;
require_once 'vendor/autoload.php'; require_once __DIR__ . '/vendor/autoload.php';
require_once 'config.php'; require_once __DIR__ . '/data/config.php';
require_once 'Grocy.php'; require_once __DIR__ . '/Grocy.php';
require_once 'GrocyDbMigrator.php'; require_once __DIR__ . '/GrocyDbMigrator.php';
require_once 'GrocyDemoDataGenerator.php'; require_once __DIR__ . '/GrocyDemoDataGenerator.php';
require_once 'GrocyLogicStock.php'; require_once __DIR__ . '/GrocyLogicStock.php';
require_once 'GrocyPhpHelper.php'; require_once __DIR__ . '/GrocyLogicHabits.php';
require_once __DIR__ . '/GrocyPhpHelper.php';
$app = new \Slim\App(new \Slim\Container([ $app = new \Slim\App(new \Slim\Container([
'settings' => [ 'settings' => [
'displayErrorDetails' => true, 'displayErrorDetails' => true,
'determineRouteBeforeAppMiddleware' => true
], ],
])); ]));
$container = $app->getContainer(); $container = $app->getContainer();
@@ -22,32 +24,96 @@ $container['renderer'] = new PhpRenderer('./views');
if (!Grocy::IsDemoInstallation()) if (!Grocy::IsDemoInstallation())
{ {
$isHttpsReverseProxied = !empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https'; $sessionMiddleware = function(Request $request, Response $response, callable $next)
$app->add(new \Slim\Middleware\HttpBasicAuthentication([ {
'realm' => 'grocy', $route = $request->getAttribute('route');
'secure' => !$isHttpsReverseProxied, $routeName = $route->getName();
'users' => [
HTTP_USER => HTTP_PASSWORD if (!Grocy::IsValidSession($_COOKIE['grocy_session']) && $routeName !== 'login')
] {
])); $response = $response->withRedirect('/login');
}
else
{
$response = $next($request, $response);
}
return $response;
};
$app->add($sessionMiddleware);
} }
$app->get('/', function(Request $request, Response $response)
{
$db = Grocy::GetDbConnection(); $db = Grocy::GetDbConnection();
$app->get('/login', function(Request $request, Response $response)
{
return $this->renderer->render($response, '/layout.php', [ return $this->renderer->render($response, '/layout.php', [
'title' => 'Dashboard', 'title' => 'Login',
'contentPage' => 'dashboard.php', '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(), 'products' => $db->products(),
'currentStock' => GrocyLogicStock::GetCurrentStock() 'quantityunits' => $db->quantity_units(),
'currentStock' => GrocyLogicStock::GetCurrentStock(),
'missingProducts' => GrocyLogicStock::GetMissingProducts()
]); ]);
}); });
$app->get('/purchase', function(Request $request, Response $response) $app->get('/habitsoverview', function(Request $request, Response $response) use($db)
{ {
$db = Grocy::GetDbConnection(); return $this->renderer->render($response, '/layout.php', [
'title' => 'Habits overview',
'contentPage' => 'habitsoverview.php',
'habits' => $db->habits(),
'currentHabits' => GrocyLogicHabits::GetCurrentHabits(),
]);
});
$app->get('/purchase', function(Request $request, Response $response) use($db)
{
return $this->renderer->render($response, '/layout.php', [ return $this->renderer->render($response, '/layout.php', [
'title' => 'Purchase', 'title' => 'Purchase',
'contentPage' => 'purchase.php', 'contentPage' => 'purchase.php',
@@ -55,21 +121,47 @@ $app->get('/purchase', function(Request $request, Response $response)
]); ]);
}); });
$app->get('/consumption', function(Request $request, Response $response) $app->get('/consume', function(Request $request, Response $response) use($db)
{ {
$db = Grocy::GetDbConnection();
return $this->renderer->render($response, '/layout.php', [ return $this->renderer->render($response, '/layout.php', [
'title' => 'Consumption', 'title' => 'Consume',
'contentPage' => 'consumption.php', 'contentPage' => 'consume.php',
'products' => $db->products() 'products' => $db->products()
]); ]);
}); });
$app->get('/products', function(Request $request, Response $response) $app->get('/inventory', function(Request $request, Response $response) use($db)
{ {
$db = Grocy::GetDbConnection(); 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('/products', function(Request $request, Response $response) use($db)
{
return $this->renderer->render($response, '/layout.php', [ return $this->renderer->render($response, '/layout.php', [
'title' => 'Products', 'title' => 'Products',
'contentPage' => 'products.php', 'contentPage' => 'products.php',
@@ -79,10 +171,8 @@ $app->get('/products', function(Request $request, Response $response)
]); ]);
}); });
$app->get('/locations', function(Request $request, Response $response) $app->get('/locations', function(Request $request, Response $response) use($db)
{ {
$db = Grocy::GetDbConnection();
return $this->renderer->render($response, '/layout.php', [ return $this->renderer->render($response, '/layout.php', [
'title' => 'Locations', 'title' => 'Locations',
'contentPage' => 'locations.php', 'contentPage' => 'locations.php',
@@ -90,10 +180,8 @@ $app->get('/locations', function(Request $request, Response $response)
]); ]);
}); });
$app->get('/quantityunits', function(Request $request, Response $response) $app->get('/quantityunits', function(Request $request, Response $response) use($db)
{ {
$db = Grocy::GetDbConnection();
return $this->renderer->render($response, '/layout.php', [ return $this->renderer->render($response, '/layout.php', [
'title' => 'Quantity units', 'title' => 'Quantity units',
'contentPage' => 'quantityunits.php', 'contentPage' => 'quantityunits.php',
@@ -101,10 +189,18 @@ $app->get('/quantityunits', function(Request $request, Response $response)
]); ]);
}); });
$app->get('/product/{productId}', function(Request $request, Response $response, $args) $app->get('/habits', function(Request $request, Response $response) use($db)
{ {
$db = Grocy::GetDbConnection(); return $this->renderer->render($response, '/layout.php', [
'title' => 'Habits',
'contentPage' => 'habits.php',
'habits' => $db->habits()
]);
});
$app->get('/product/{productId}', function(Request $request, Response $response, $args) use($db)
{
if ($args['productId'] == 'new') if ($args['productId'] == 'new')
{ {
return $this->renderer->render($response, '/layout.php', [ return $this->renderer->render($response, '/layout.php', [
@@ -128,10 +224,8 @@ $app->get('/product/{productId}', function(Request $request, Response $response,
} }
}); });
$app->get('/location/{locationId}', function(Request $request, Response $response, $args) $app->get('/location/{locationId}', function(Request $request, Response $response, $args) use($db)
{ {
$db = Grocy::GetDbConnection();
if ($args['locationId'] == 'new') if ($args['locationId'] == 'new')
{ {
return $this->renderer->render($response, '/layout.php', [ return $this->renderer->render($response, '/layout.php', [
@@ -151,10 +245,8 @@ $app->get('/location/{locationId}', function(Request $request, Response $respons
} }
}); });
$app->get('/quantityunit/{quantityunitId}', function(Request $request, Response $response, $args) $app->get('/quantityunit/{quantityunitId}', function(Request $request, Response $response, $args) use($db)
{ {
$db = Grocy::GetDbConnection();
if ($args['quantityunitId'] == 'new') if ($args['quantityunitId'] == 'new')
{ {
return $this->renderer->render($response, '/layout.php', [ return $this->renderer->render($response, '/layout.php', [
@@ -174,67 +266,103 @@ $app->get('/quantityunit/{quantityunitId}', function(Request $request, Response
} }
}); });
$app->group('/api', function() $app->get('/habit/{habitId}', function(Request $request, Response $response, $args) use($db)
{ {
$this->get('/get-objects/{entity}', function(Request $request, Response $response, $args) 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('/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)
{ {
$db = Grocy::GetDbConnection();
echo json_encode($db->{$args['entity']}()); echo json_encode($db->{$args['entity']}());
return $response->withHeader('Content-Type', 'application/json');
}); });
$this->get('/get-object/{entity}/{objectId}', function(Request $request, Response $response, $args) $this->get('/get-object/{entity}/{objectId}', function(Request $request, Response $response, $args) use($db)
{ {
$db = Grocy::GetDbConnection();
echo json_encode($db->{$args['entity']}($args['objectId'])); echo json_encode($db->{$args['entity']}($args['objectId']));
return $response->withHeader('Content-Type', 'application/json');
}); });
$this->post('/add-object/{entity}', function(Request $request, Response $response, $args) $this->post('/add-object/{entity}', function(Request $request, Response $response, $args) use($db)
{ {
$db = Grocy::GetDbConnection();
$newRow = $db->{$args['entity']}()->createRow($request->getParsedBody()); $newRow = $db->{$args['entity']}()->createRow($request->getParsedBody());
$newRow->save(); $newRow->save();
$success = $newRow->isClean(); $success = $newRow->isClean();
echo json_encode(array('success' => $success)); echo json_encode(array('success' => $success));
return $response->withHeader('Content-Type', 'application/json');
}); });
$this->post('/edit-object/{entity}/{objectId}', function(Request $request, Response $response, $args) $this->post('/edit-object/{entity}/{objectId}', function(Request $request, Response $response, $args) use($db)
{ {
$db = Grocy::GetDbConnection();
$row = $db->{$args['entity']}($args['objectId']); $row = $db->{$args['entity']}($args['objectId']);
$row->update($request->getParsedBody()); $row->update($request->getParsedBody());
$success = $row->isClean(); $success = $row->isClean();
echo json_encode(array('success' => $success)); echo json_encode(array('success' => $success));
return $response->withHeader('Content-Type', 'application/json');
}); });
$this->get('/delete-object/{entity}/{objectId}', function(Request $request, Response $response, $args) $this->get('/delete-object/{entity}/{objectId}', function(Request $request, Response $response, $args) use($db)
{ {
$db = Grocy::GetDbConnection();
$row = $db->{$args['entity']}($args['objectId']); $row = $db->{$args['entity']}($args['objectId']);
$row->delete(); $row->delete();
$success = $row->isClean(); $success = $row->isClean();
echo json_encode(array('success' => $success)); echo json_encode(array('success' => $success));
return $response->withHeader('Content-Type', 'application/json');
}); });
$this->get('/stock/get-product-details/{productId}', function(Request $request, Response $response, $args) $this->get('/stock/add-product/{productId}/{amount}', function(Request $request, Response $response, $args)
{ {
echo json_encode(GrocyLogicStock::GetProductDetails($args['productId'])); $bestBeforeDate = date('Y-m-d');
return $response->withHeader('Content-Type', 'application/json'); if (isset($request->getQueryParams()['bestbeforedate']) && !empty($request->getQueryParams()['bestbeforedate']))
}); {
$bestBeforeDate = $request->getQueryParams()['bestbeforedate'];
}
$this->get('/stock/get-current-stock', function(Request $request, Response $response) $transactionType = GrocyLogicStock::TRANSACTION_TYPE_PURCHASE;
if (isset($request->getQueryParams()['transactiontype']) && !empty($request->getQueryParams()['transactiontype']))
{ {
echo json_encode(GrocyLogicStock::GetCurrentStock()); $transactionType = $request->getQueryParams()['transactiontype'];
return $response->withHeader('Content-Type', 'application/json'); }
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) $this->get('/stock/consume-product/{productId}/{amount}', function(Request $request, Response $response, $args)
@@ -245,15 +373,61 @@ $app->group('/api', function()
$spoiled = true; $spoiled = true;
} }
echo json_encode(array('success' => GrocyLogicStock::ConsumeProduct($args['productId'], $args['amount'], $spoiled))); $transactionType = GrocyLogicStock::TRANSACTION_TYPE_CONSUME;
return $response->withHeader('Content-Type', 'application/json'); 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('/helper/uniqid', function(Request $request, Response $response) $this->get('/stock/inventory-product/{productId}/{newAmount}', function(Request $request, Response $response, $args)
{ {
echo json_encode(array('uniqid' => uniqid())); $bestBeforeDate = date('Y-m-d');
return $response->withHeader('Content-Type', 'application/json'); 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']));
});
})->add(function($request, $response, $next)
{
$response = $next($request, $response);
return $response->withHeader('Content-Type', 'application/json');
}); });
$app->run(); $app->run();

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

View File

@@ -106,6 +106,29 @@
font-size: 0.8em; font-size: 0.8em;
} }
.disabled { .disabled,
.no-real-button {
pointer-events: none; pointer-events: none;
} }
.warning-bg {
background-color: #fcf8e3 !important;
}
.error-bg {
background-color: #f2dede !important;
}
.info-bg {
background-color: #afd9ee !important;
}
.discrete-content-separator {
padding-top: 5px;
padding-bottom: 5px;
}
.discrete-content-separator-2x {
padding-top: 10px;
padding-bottom: 10px;
}

View File

@@ -1 +1 @@
0.4.0 1.5.0

View File

@@ -1,8 +1,8 @@
$('#save-consumption-button').on('click', function(e) $('#save-consume-button').on('click', function(e)
{ {
e.preventDefault(); e.preventDefault();
var jsonForm = $('#consumption-form').serializeJSON(); var jsonForm = $('#consume-form').serializeJSON();
var spoiled = 0; var spoiled = 0;
if ($('#spoiled').is(':checked')) if ($('#spoiled').is(':checked'))
@@ -23,7 +23,7 @@
$('#product_id_text_input').focus(); $('#product_id_text_input').focus();
$('#product_id_text_input').val(''); $('#product_id_text_input').val('');
$('#product_id_text_input').trigger('change'); $('#product_id_text_input').trigger('change');
$('#consumption-form').validator('validate'); $('#consume-form').validator('validate');
}, },
function(xhr) function(xhr)
{ {
@@ -45,22 +45,24 @@ $('#product_id').on('change', function(e)
if (productId) if (productId)
{ {
Grocy.FetchJson('/api/stock/get-product-details/' + productId, Grocy.FetchJson('/api/stock/get-product-details/' + productId,
function(productStatistics) function (productDetails)
{ {
$('#selected-product-name').text(productStatistics.product.name); $('#selected-product-name').text(productDetails.product.name);
$('#selected-product-stock-amount').text(productStatistics.stock_amount || '0'); $('#selected-product-stock-amount').text(productDetails.stock_amount || '0');
$('#selected-product-stock-qu-name').text(productStatistics.quantity_unit_stock.name); $('#selected-product-stock-qu-name').text(productDetails.quantity_unit_stock.name);
$('#selected-product-stock-qu-name2').text(productStatistics.quantity_unit_stock.name); $('#selected-product-stock-qu-name2').text(productDetails.quantity_unit_stock.name);
$('#selected-product-last-purchased').text((productStatistics.last_purchased || 'never').substring(0, 10)); $('#selected-product-last-purchased').text((productDetails.last_purchased || 'never').substring(0, 10));
$('#selected-product-last-purchased-timeago').text($.timeago(productStatistics.last_purchased || '')); $('#selected-product-last-purchased-timeago').text($.timeago(productDetails.last_purchased || ''));
$('#selected-product-last-used').text((productStatistics.last_used || 'never').substring(0, 10)); $('#selected-product-last-used').text((productDetails.last_used || 'never').substring(0, 10));
$('#selected-product-last-used-timeago').text($.timeago(productStatistics.last_used || '')); $('#selected-product-last-used-timeago').text($.timeago(productDetails.last_used || ''));
$('#amount').attr('max', productStatistics.stock_amount); $('#amount').attr('max', productDetails.stock_amount);
$('#consume-form').validator('update');
$('#amount_qu_unit').text(productDetails.quantity_unit_stock.name);
Grocy.EmptyElementWhenMatches('#selected-product-last-purchased-timeago', 'NaN years ago'); Grocy.EmptyElementWhenMatches('#selected-product-last-purchased-timeago', 'NaN years ago');
Grocy.EmptyElementWhenMatches('#selected-product-last-used-timeago', 'NaN years ago'); Grocy.EmptyElementWhenMatches('#selected-product-last-used-timeago', 'NaN years ago');
if ((productStatistics.stock_amount || 0) === 0) if ((productDetails.stock_amount || 0) === 0)
{ {
$('#product_id').val(''); $('#product_id').val('');
$('#product_id_text_input').val(''); $('#product_id_text_input').val('');
@@ -69,6 +71,7 @@ $('#product_id').on('change', function(e)
$('#product_id_text_input').closest('.form-group').addClass('has-error'); $('#product_id_text_input').closest('.form-group').addClass('has-error');
$('#product-error').text('This product is not in stock.'); $('#product-error').text('This product is not in stock.');
$('#product-error').show(); $('#product-error').show();
$('#product_id_text_input').focus();
} }
else else
{ {
@@ -88,7 +91,22 @@ $('#product_id').on('change', function(e)
$(function() $(function()
{ {
$('.combobox').combobox({ appendId: '_text_input' }); $('.combobox').combobox({
appendId: '_text_input'
});
$('#product_id_text_input').on('change', function(e)
{
var input = $('#product_id_text_input').val().toString();
var possibleOptionElement = $("#product_id option[data-additional-searchdata*='" + input + "']").first();
if (possibleOptionElement.length > 0)
{
$('#product_id').val(possibleOptionElement.val());
$('#product_id').data('combobox').refresh();
$('#product_id').trigger('change');
}
});
$('#amount').val(1); $('#amount').val(1);
$('#product_id').val(''); $('#product_id').val('');
@@ -96,14 +114,22 @@ $(function()
$('#product_id_text_input').val(''); $('#product_id_text_input').val('');
$('#product_id_text_input').trigger('change'); $('#product_id_text_input').trigger('change');
$('#consumption-form').validator(); $('#consume-form').validator();
$('#consumption-form').validator('validate'); $('#consume-form').validator('validate');
$('#consumption-form input').keydown(function(event) $('#amount').on('focus', function(e)
{
if ($('#product_id_text_input').val().length === 0)
{
$('#product_id_text_input').focus();
}
});
$('#consume-form input').keydown(function(event)
{ {
if (event.keyCode === 13) //Enter if (event.keyCode === 13) //Enter
{ {
if ($('#consumption-form').validator('validate').has('.has-error').length !== 0) //There is at least one validation error if ($('#consume-form').validator('validate').has('.has-error').length !== 0) //There is at least one validation error
{ {
event.preventDefault(); event.preventDefault();
return false; return false;

View File

@@ -1,29 +1,36 @@
<div class="col-sm-3 col-sm-offset-3 col-md-3 col-md-offset-2 main"> <div class="col-sm-3 col-sm-offset-3 col-md-3 col-md-offset-2 main">
<h1 class="page-header">Consumption</h1>
<form id="consumption-form"> <h1 class="page-header">Consume</h1>
<form id="consume-form">
<div class="form-group"> <div class="form-group">
<label for="product_id">Product&nbsp;&nbsp;<i class="fa fa-barcode"></i></label> <label for="product_id">Product&nbsp;&nbsp;<i class="fa fa-barcode"></i></label>
<select data-instockproduct="instockproduct" class="form-control combobox" id="product_id" name="product_id" required> <select class="form-control combobox" id="product_id" name="product_id" required>
<option value=""></option> <option value=""></option>
<?php foreach ($products as $product) : ?> <?php foreach ($products as $product) : ?>
<option value="<?php echo $product->id; ?>"><?php echo $product->name; ?><?php if (!empty($product->barcode)) echo ' [' . $product->barcode . ']'; ?></option> <option data-additional-searchdata="<?php echo $product->barcode; ?>" value="<?php echo $product->id; ?>"><?php echo $product->name; ?></option>
<?php endforeach; ?> <?php endforeach; ?>
</select> </select>
<div id="product-error" class="help-block with-errors"></div> <div id="product-error" class="help-block with-errors"></div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="amount">Amount</label> <label for="amount">Amount&nbsp;&nbsp;<span id="amount_qu_unit" class="small text-muted"></span></label>
<input type="number" class="form-control" id="amount" name="amount" value="1" min="1" required> <input type="number" class="form-control" id="amount" name="amount" value="1" min="1" required>
<div class="help-block with-errors"></div> <div class="help-block with-errors"></div>
</div> </div>
<div class="checkbox"> <div class="checkbox">
<label for="spoiled"> <label for="spoiled">
<input type="checkbox" id="spoiled" name="spoiled"> Spoiled <input type="checkbox" id="spoiled" name="spoiled"> Spoiled
</label> </label>
</div> </div>
<button id="save-consumption-button" type="submit" class="btn btn-default">OK</button>
<button id="save-consume-button" type="submit" class="btn btn-default">OK</button>
</form> </form>
</div> </div>
<div class="col-sm-6 col-md-5 col-lg-3 main well"> <div class="col-sm-6 col-md-5 col-lg-3 main well">

View File

@@ -1,7 +0,0 @@
$(function()
{
$('#current-stock-table').DataTable({
'paging': false,
'order': [[2, 'asc']]
});
});

View File

@@ -1,31 +0,0 @@
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
<h1 class="page-header">Dashboard</h1>
<h3>Current stock</h3>
<div class="table-responsive">
<table id="current-stock-table" class="table table-striped">
<thead>
<tr>
<th>Product</th>
<th>Amount</th>
<th>Next best before date</th>
</tr>
</thead>
<tbody>
<?php foreach ($currentStock as $currentStockEntry) : ?>
<tr>
<td>
<?php echo GrocyPhpHelper::FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name; ?>
</td>
<td>
<?php echo $currentStockEntry->amount; ?>
</td>
<td>
<?php echo $currentStockEntry->best_before_date; ?> <time class="timeago timeago-contextual" datetime="<?php echo $currentStockEntry->best_before_date; ?>"></time>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>

54
views/habitform.js Normal file
View File

@@ -0,0 +1,54 @@
$('#save-habit-button').on('click', function(e)
{
e.preventDefault();
if (Grocy.EditMode === 'create')
{
Grocy.PostJson('/api/add-object/habits', $('#habit-form').serializeJSON(),
function(result)
{
window.location.href = '/habits';
},
function(xhr)
{
console.error(xhr);
}
);
}
else
{
Grocy.PostJson('/api/edit-object/habits/' + Grocy.EditObjectId, $('#habit-form').serializeJSON(),
function(result)
{
window.location.href = '/habits';
},
function(xhr)
{
console.error(xhr);
}
);
}
});
$(function()
{
$('#name').focus();
$('#habit-form').validator();
$('#habit-form').validator('validate');
});
$('.input-group-habit-period-type').on('change', function(e)
{
var periodType = $('#period_type').val();
var periodDays = $('#period_days').val();
if (periodType === 'dynamic-regular')
{
$('#habit-period-type-info').text('This means it is estimated that a new "execution" of this habit is tracked ' + periodDays.toString() + ' days after the last was tracked.');
$('#habit-period-type-info').show();
}
else
{
$('#habit-period-type-info').hide();
}
});

46
views/habitform.php Normal file
View File

@@ -0,0 +1,46 @@
<div class="col-sm-3 col-sm-offset-3 col-md-4 col-md-offset-2 main">
<h1 class="page-header"><?php echo $title; ?></h1>
<script>Grocy.EditMode = '<?php echo $mode; ?>';</script>
<?php if ($mode == 'edit') : ?>
<script>Grocy.EditObjectId = <?php echo $habit->id; ?>;</script>
<?php endif; ?>
<form id="habit-form">
<div class="form-group">
<label for="name">Name</label>
<input type="text" class="form-control" required id="name" name="name" value="<?php if ($mode == 'edit') echo $habit->name; ?>" />
<div class="help-block with-errors"></div>
</div>
<div class="form-group">
<label for="description">Description</label>
<textarea class="form-control" rows="2" id="description" name="description"><?php if ($mode == 'edit') echo $habit->description; ?></textarea>
</div>
<div class="form-group">
<label for="period_type">Location</label>
<select required class="form-control input-group-habit-period-type" id="period_type" name="period_type">
<?php foreach ($periodTypes as $periodType) : ?>
<option <?php if ($mode == 'edit' && $periodType == $habit->period_type) echo 'selected="selected"'; ?> value="<?php echo $periodType; ?>"><?php echo $periodType; ?></option>
<?php endforeach; ?>
</select>
<div class="help-block with-errors"></div>
</div>
<div class="form-group">
<label for="period_days">Period days</label>
<input type="number" class="form-control input-group-habit-period-type" id="period_days" name="period_days" value="<?php if ($mode == 'edit') echo $habit->period_days; ?>" />
<div class="help-block with-errors"></div>
</div>
<p id="habit-period-type-info" class="help-block text-muted"></p>
<button id="save-habit-button" type="submit" class="btn btn-default">Save</button>
</form>
</div>

43
views/habits.js Normal file
View File

@@ -0,0 +1,43 @@
$(document).on('click', '.habit-delete-button', function(e)
{
bootbox.confirm({
message: 'Delete habit <strong>' + $(e.target).attr('data-habit-name') + '</strong>?',
buttons: {
confirm: {
label: 'Yes',
className: 'btn-success'
},
cancel: {
label: 'No',
className: 'btn-danger'
}
},
callback: function(result)
{
if (result === true)
{
Grocy.FetchJson('/api/delete-object/habits/' + $(e.target).attr('data-habit-id'),
function(result)
{
window.location.href = '/habits';
},
function(xhr)
{
console.error(xhr);
}
);
}
}
});
});
$(function()
{
$('#habits-table').DataTable({
'pageLength': 50,
'order': [[1, 'asc']],
'columnDefs': [
{ 'orderable': false, 'targets': 0 }
]
});
});

50
views/habits.php Normal file
View File

@@ -0,0 +1,50 @@
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
<h1 class="page-header">
Habits
<a class="btn btn-default" href="/habit/new" role="button">
<i class="fa fa-plus"></i>&nbsp;Add
</a>
</h1>
<div class="table-responsive">
<table id="habits-table" class="table table-striped">
<thead>
<tr>
<th>#</th>
<th>Name</th>
<th>Period type</th>
<th>Period days</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<?php foreach ($habits as $habit) : ?>
<tr>
<td class="fit-content">
<a class="btn btn-info" href="/habit/<?php echo $habit->id; ?>" role="button">
<i class="fa fa-pencil"></i>
</a>
<a class="btn btn-danger habit-delete-button" href="#" role="button" data-habit-id="<?php echo $habit->id; ?>" data-habit-name="<?php echo $habit->name; ?>">
<i class="fa fa-trash"></i>
</a>
</td>
<td>
<?php echo $habit->name; ?>
</td>
<td>
<?php echo $habit->period_type; ?>
</td>
<td>
<?php echo $habit->period_days; ?>
</td>
<td>
<?php echo $habit->description; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>

7
views/habitsoverview.js Normal file
View File

@@ -0,0 +1,7 @@
$(function()
{
$('#habits-overview-table').DataTable({
'pageLength': 50,
'order': [[1, 'desc']]
});
});

38
views/habitsoverview.php Normal file
View File

@@ -0,0 +1,38 @@
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
<h1 class="page-header">Habits overview</h1>
<div class="table-responsive">
<table id="habits-overview-table" class="table table-striped">
<thead>
<tr>
<th>Habit</th>
<th>Next estimated tracking</th>
<th>Last tracked</th>
</tr>
</thead>
<tbody>
<?php foreach ($currentHabits as $curentHabitEntry) : ?>
<tr class="<?php if (GrocyPhpHelper::FindObjectInArrayByPropertyValue($habits, 'id', $curentHabitEntry->habit_id)->period_type === GrocyLogicHabits::HABIT_TYPE_DYNAMIC_REGULAR && GrocyLogicHabits::GetNextHabitTime($curentHabitEntry->habit_id) < date('Y-m-d H:i:s')) echo 'error-bg'; ?>">
<td>
<?php echo GrocyPhpHelper::FindObjectInArrayByPropertyValue($habits, 'id', $curentHabitEntry->habit_id)->name; ?>
</td>
<td>
<?php if (GrocyPhpHelper::FindObjectInArrayByPropertyValue($habits, 'id', $curentHabitEntry->habit_id)->period_type === GrocyLogicHabits::HABIT_TYPE_DYNAMIC_REGULAR): ?>
<?php echo GrocyLogicHabits::GetNextHabitTime($curentHabitEntry->habit_id); ?>
<time class="timeago timeago-contextual" datetime="<?php echo GrocyLogicHabits::GetNextHabitTime($curentHabitEntry->habit_id); ?>"></time>
<?php else: ?>
Whenever you want...
<?php endif; ?>
</td>
<td>
<?php echo $curentHabitEntry->last_tracked_time; ?>
<time class="timeago timeago-contextual" datetime="<?php echo $curentHabitEntry->last_tracked_time; ?>"></time>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>

165
views/habittracking.js Normal file
View File

@@ -0,0 +1,165 @@
$('#save-habittracking-button').on('click', function(e)
{
e.preventDefault();
var jsonForm = $('#habittracking-form').serializeJSON();
Grocy.FetchJson('/api/habits/get-habit-details/' + jsonForm.habit_id,
function (habitDetails)
{
Grocy.FetchJson('/api/habits/track-habit/' + jsonForm.habit_id + '?tracked_time=' + $('#tracked_time').val(),
function(result)
{
toastr.success('Tracked execution of habit ' + habitDetails.habit.name + ' on ' + $('#tracked_time').val());
$('#habit_id').val('');
$('#habit_id_text_input').focus();
$('#habit_id_text_input').val('');
$('#tracked_time').val(moment().format('YYYY-MM-DD HH:mm:ss'));
$('#tracked_time').trigger('change');
$('#habit_id_text_input').trigger('change');
$('#habittracking-form').validator('validate');
},
function(xhr)
{
console.error(xhr);
}
);
},
function(xhr)
{
console.error(xhr);
}
);
});
$('#habit_id').on('change', function(e)
{
var habitId = $(e.target).val();
if (habitId)
{
Grocy.FetchJson('/api/habits/get-habit-details/' + habitId,
function(habitDetails)
{
$('#selected-habit-name').text(habitDetails.habit.name);
$('#selected-habit-last-tracked').text((habitDetails.last_tracked || 'never'));
$('#selected-habit-last-tracked-timeago').text($.timeago(habitDetails.last_tracked || ''));
$('#selected-habit-tracked-count').text((habitDetails.tracked_count || '0'));
Grocy.EmptyElementWhenMatches('#selected-habit-last-tracked-timeago', 'NaN years ago');
},
function(xhr)
{
console.error(xhr);
}
);
}
});
$(function()
{
$('.datetimepicker').datetimepicker(
{
format: 'YYYY-MM-DD HH:mm:ss',
showTodayButton: true,
calendarWeeks: true,
maxDate: moment()
});
$('#tracked_time').val(moment().format('YYYY-MM-DD HH:mm:ss'));
$('#tracked_time').trigger('change');
$('#tracked_time').on('focus', function(e)
{
if ($('#habit_id_text_input').val().length === 0)
{
$('#habit_id_text_input').focus();
}
});
$('.combobox').combobox({
appendId: '_text_input'
});
$('#habit_id').val('');
$('#habit_id_text_input').focus();
$('#habit_id_text_input').val('');
$('#habit_id_text_input').trigger('change');
$('#habittracking-form').validator();
$('#habittracking-form').validator('validate');
$('#habittracking-form input').keydown(function(event)
{
if (event.keyCode === 13) //Enter
{
if ($('#habittracking-form').validator('validate').has('.has-error').length !== 0) //There is at least one validation error
{
event.preventDefault();
return false;
}
}
});
});
$('#tracked_time').on('change', function(e)
{
var value = $('#tracked_time').val();
var now = new Date();
var centuryStart = Number.parseInt(now.getFullYear().toString().substring(0, 2) + '00');
var centuryEnd = Number.parseInt(now.getFullYear().toString().substring(0, 2) + '99');
if (value === 'x' || value === 'X') {
value = '29991231';
}
if (value.length === 4 && !(Number.parseInt(value) > centuryStart && Number.parseInt(value) < centuryEnd))
{
value = (new Date()).getFullYear().toString() + value;
}
if (value.length === 8 && $.isNumeric(value))
{
value = value.replace(/(\d{4})(\d{2})(\d{2})/, '$1-$2-$3');
$('#tracked_time').val(value);
$('#habittracking-form').validator('validate');
}
});
$('#tracked_time').on('keypress', function(e)
{
var element = $(e.target);
var value = element.val();
var dateObj = moment(element.val(), 'YYYY-MM-DD', true);
$('.datepicker').datepicker('hide');
//If input is empty and any arrow key is pressed, set date to today
if (value.length === 0 && (e.keyCode === 38 || e.keyCode === 40 || e.keyCode === 37 || e.keyCode === 39))
{
dateObj = moment(new Date(), 'YYYY-MM-DD', true);
}
if (dateObj.isValid())
{
if (e.keyCode === 38) //Up
{
element.val(dateObj.add(-1, 'days').format('YYYY-MM-DD'));
}
else if (e.keyCode === 40) //Down
{
element.val(dateObj.add(1, 'days').format('YYYY-MM-DD'));
}
else if (e.keyCode === 37) //Left
{
element.val(dateObj.add(-1, 'weeks').format('YYYY-MM-DD'));
}
else if (e.keyCode === 39) //Right
{
element.val(dateObj.add(1, 'weeks').format('YYYY-MM-DD'));
}
}
$('#habittracking-form').validator('validate');
});

42
views/habittracking.php Normal file
View File

@@ -0,0 +1,42 @@
<div class="col-sm-3 col-sm-offset-3 col-md-3 col-md-offset-2 main">
<h1 class="page-header">Habit tracking</h1>
<form id="habittracking-form">
<div class="form-group">
<label for="habit_id">Habit</label>
<select class="form-control combobox" id="habit_id" name="habit_id" required>
<option value=""></option>
<?php foreach ($habits as $habit) : ?>
<option value="<?php echo $habit->id; ?>"><?php echo $habit->name; ?></option>
<?php endforeach; ?>
</select>
<div id="product-error" class="help-block with-errors"></div>
</div>
<div class="form-group">
<label for="tracked_time">Tracked time</label>
<div class="input-group date datetimepicker">
<input type="text" class="form-control" id="tracked_time" name="tracked_time" required >
<span class="input-group-addon">
<span class="fa fa-calendar"></span>
</span>
</div>
<div class="help-block with-errors"></div>
</div>
<button id="save-habittracking-button" type="submit" class="btn btn-default">OK</button>
</form>
</div>
<div class="col-sm-6 col-md-5 col-lg-3 main well">
<h3>Habit overview <strong><span id="selected-habit-name"></span></strong></h3>
<p>
<strong>Tracked count:</strong> <span id="selected-habit-tracked-count"></span><br />
<strong>Last tracked:</strong> <span id="selected-habit-last-tracked"></span> <time id="selected-habit-last-tracked-timeago" class="timeago timeago-contextual"></time><br />
</p>
</div>

383
views/inventory.js Normal file
View File

@@ -0,0 +1,383 @@
$('#save-inventory-button').on('click', function(e)
{
e.preventDefault();
var jsonForm = $('#inventory-form').serializeJSON();
Grocy.FetchJson('/api/stock/get-product-details/' + jsonForm.product_id,
function (productDetails)
{
Grocy.FetchJson('/api/stock/inventory-product/' + jsonForm.product_id + '/' + jsonForm.new_amount + '?bestbeforedate=' + $('#best_before_date').val(),
function(result)
{
var addBarcode = Grocy.GetUriParam('addbarcodetoselection');
if (addBarcode !== undefined)
{
var existingBarcodes = productDetails.product.barcode || '';
if (existingBarcodes.length === 0)
{
productDetails.product.barcode = addBarcode;
}
else
{
productDetails.product.barcode += ',' + addBarcode;
}
Grocy.PostJson('/api/edit-object/products/' + productDetails.product.id, productDetails.product,
function (result) { },
function(xhr)
{
console.error(xhr);
}
);
}
toastr.success('Stock amount of ' + productDetails.product.name + ' is now ' + jsonForm.new_amount.toString() + ' ' + productDetails.quantity_unit_stock.name);
Grocy.Wait(1000);
if (addBarcode !== undefined)
{
window.location.href = '/inventory';
}
else
{
$('#inventory-change-info').hide();
$('#new_amount').val('');
$('#best_before_date').val('');
$('#product_id').val('');
$('#product_id_text_input').focus();
$('#product_id_text_input').val('');
$('#product_id_text_input').trigger('change');
$('#inventory-form').validator('validate');
}
},
function(xhr)
{
console.error(xhr);
}
);
},
function(xhr)
{
console.error(xhr);
}
);
});
$('#product_id').on('change', function(e)
{
var productId = $(e.target).val();
if (productId)
{
Grocy.FetchJson('/api/stock/get-product-details/' + productId,
function(productDetails)
{
$('#selected-product-name').text(productDetails.product.name);
$('#selected-product-stock-amount').text(productDetails.stock_amount || '0');
$('#selected-product-stock-qu-name').text(productDetails.quantity_unit_stock.name);
$('#selected-product-purchase-qu-name').text(productDetails.quantity_unit_purchase.name);
$('#selected-product-last-purchased').text((productDetails.last_purchased || 'never').substring(0, 10));
$('#selected-product-last-purchased-timeago').text($.timeago(productDetails.last_purchased || ''));
$('#selected-product-last-used').text((productDetails.last_used || 'never').substring(0, 10));
$('#selected-product-last-used-timeago').text($.timeago(productDetails.last_used || ''));
$('#new_amount').attr('not-equal', productDetails.stock_amount);
$('#new_amount_qu_unit').text(productDetails.quantity_unit_stock.name);
Grocy.EmptyElementWhenMatches('#selected-product-last-purchased-timeago', 'NaN years ago');
Grocy.EmptyElementWhenMatches('#selected-product-last-used-timeago', 'NaN years ago');
},
function(xhr)
{
console.error(xhr);
}
);
}
});
$(function()
{
$('.datepicker').datepicker(
{
format: 'yyyy-mm-dd',
startDate: '+0d',
todayHighlight: true,
autoclose: true,
calendarWeeks: true,
orientation: 'bottom auto',
weekStart: 1,
showOnFocus: false
});
$('.datepicker').trigger('change');
$('.combobox').combobox({
appendId: '_text_input'
});
$('#product_id_text_input').on('change', function(e)
{
var input = $('#product_id_text_input').val().toString();
var possibleOptionElement = $("#product_id option[data-additional-searchdata*='" + input + "']").first();
if (Grocy.GetUriParam('addbarcodetoselection') === undefined && possibleOptionElement.length > 0)
{
$('#product_id').val(possibleOptionElement.val());
$('#product_id').data('combobox').refresh();
$('#product_id').trigger('change');
}
else
{
var optionElement = $("#product_id option:contains('" + input + "')").first();
if (input.length > 0 && optionElement.length === 0 && Grocy.GetUriParam('addbarcodetoselection') === undefined )
{
bootbox.dialog({
message: '<strong>' + input + '</strong> could not be resolved to a product, how do you want to proceed?',
title: 'Create or assign product',
onEscape: function() { },
size: 'large',
backdrop: true,
buttons: {
cancel: {
label: 'Cancel',
className: 'btn-default',
callback: function() { }
},
addnewproduct: {
label: 'Add as new <u><strong>p</strong></u>roduct',
className: 'btn-success add-new-product-dialog-button',
callback: function()
{
window.location.href = '/product/new?prefillname=' + encodeURIComponent(input) + '&returnto=' + encodeURIComponent(window.location.pathname);
}
},
addbarcode: {
label: 'Add as <u><strong>b</strong></u>arcode to existing product',
className: 'btn-info add-new-barcode-dialog-button',
callback: function()
{
window.location.href = '/inventory?addbarcodetoselection=' + encodeURIComponent(input);
}
},
addnewproductwithbarcode: {
label: '<u><strong>A</strong></u>dd as new product + prefill barcode',
className: 'btn-warning add-new-product-with-barcode-dialog-button',
callback: function()
{
window.location.href = '/product/new?prefillbarcode=' + encodeURIComponent(input) + '&returnto=' + encodeURIComponent(window.location.pathname);
}
}
}
}).on('keypress', function(e)
{
if (e.key === 'B' || e.key === 'b')
{
$('.add-new-barcode-dialog-button').click();
}
if (e.key === 'p' || e.key === 'P')
{
$('.add-new-product-dialog-button').click();
}
if (e.key === 'a' || e.key === 'A')
{
$('.add-new-product-with-barcode-dialog-button').click();
}
});
}
}
});
$('#new_amount').val('');
$('#best_before_date').val('');
$('#product_id').val('');
$('#product_id_text_input').focus();
$('#product_id_text_input').val('');
$('#product_id_text_input').trigger('change');
$('#inventory-form').validator({
custom: {
'isodate': function($el)
{
if ($el.val().length !== 0 && !moment($el.val(), 'YYYY-MM-DD', true).isValid())
{
return 'Wrong date format, needs to be YYYY-MM-DD';
}
else if (moment($el.val()).isValid())
{
if (moment($el.val()).isBefore(moment(), 'day'))
{
return 'This value cannot be before today.';
}
}
},
'notequal': function($el)
{
if ($el.val().length !== 0 && $el.val().toString() === $el.attr('not-equal').toString())
{
return 'This value cannot be equal to ' + $el.attr('not-equal').toString();
}
}
}
});
$('#inventory-form').validator('validate');
$('#new_amount').on('focus', function(e)
{
if ($('#product_id_text_input').val().length === 0)
{
$('#product_id_text_input').focus();
}
});
$('#inventory-form input').keydown(function(event)
{
if (event.keyCode === 13) //Enter
{
if ($('#inventory-form').validator('validate').has('.has-error').length !== 0) //There is at least one validation error
{
event.preventDefault();
return false;
}
}
});
var prefillProduct = Grocy.GetUriParam('createdproduct');
if (prefillProduct !== undefined)
{
var possibleOptionElement = $("#product_id option[data-additional-searchdata*='" + prefillProduct + "']").first();
if (possibleOptionElement.length === 0)
{
possibleOptionElement = $("#product_id option:contains('" + prefillProduct + "')").first();
}
if (possibleOptionElement.length > 0)
{
$('#product_id').val(possibleOptionElement.val());
$('#product_id').data('combobox').refresh();
$('#product_id').trigger('change');
$('#new_amount').focus();
}
}
var addBarcode = Grocy.GetUriParam('addbarcodetoselection');
if (addBarcode !== undefined)
{
$('#addbarcodetoselection').text(addBarcode);
$('#flow-info-addbarcodetoselection').removeClass('hide');
$('#barcode-lookup-disabled-hint').removeClass('hide');
}
});
$('#best_before_date-datepicker-button').on('click', function(e)
{
$('.datepicker').datepicker('show');
});
$('#best_before_date').on('change', function(e)
{
var value = $('#best_before_date').val();
var now = new Date();
var centuryStart = Number.parseInt(now.getFullYear().toString().substring(0, 2) + '00');
var centuryEnd = Number.parseInt(now.getFullYear().toString().substring(0, 2) + '99');
if (value === 'x' || value === 'X')
{
value = '29991231';
}
if (value.length === 4 && !(Number.parseInt(value) > centuryStart && Number.parseInt(value) < centuryEnd))
{
value = (new Date()).getFullYear().toString() + value;
}
if (value.length === 8 && $.isNumeric(value))
{
value = value.replace(/(\d{4})(\d{2})(\d{2})/, '$1-$2-$3');
$('#best_before_date').val(value);
$('#inventory-form').validator('validate');
}
});
$('#best_before_date').on('keypress', function(e)
{
var element = $(e.target);
var value = element.val();
var dateObj = moment(element.val(), 'YYYY-MM-DD', true);
$('.datepicker').datepicker('hide');
//If input is empty and any arrow key is pressed, set date to today
if (value.length === 0 && (e.keyCode === 38 || e.keyCode === 40 || e.keyCode === 37 || e.keyCode === 39))
{
dateObj = moment(new Date(), 'YYYY-MM-DD', true);
}
if (dateObj.isValid())
{
if (e.keyCode === 38) //Up
{
element.val(dateObj.add(-1, 'days').format('YYYY-MM-DD'));
}
else if (e.keyCode === 40) //Down
{
element.val(dateObj.add(1, 'days').format('YYYY-MM-DD'));
}
else if (e.keyCode === 37) //Left
{
element.val(dateObj.add(-1, 'weeks').format('YYYY-MM-DD'));
}
else if (e.keyCode === 39) //Right
{
element.val(dateObj.add(1, 'weeks').format('YYYY-MM-DD'));
}
}
$('#inventory-form').validator('validate');
});
$('#new_amount').on('change', function(e)
{
if ($('#product_id').parent().hasClass('has-error'))
{
$('#inventory-change-info').hide();
return;
}
var productId = $('#product_id').val();
var newAmount = $('#new_amount').val();
if (productId)
{
Grocy.FetchJson('/api/stock/get-product-details/' + productId,
function(productDetails)
{
var productStockAmount = productDetails.stock_amount || '0';
if (newAmount > productStockAmount)
{
var amountToAdd = newAmount - productDetails.stock_amount;
$('#inventory-change-info').text('This means ' + amountToAdd.toString() + ' ' + productDetails.quantity_unit_stock.name + ' will be added to stock');
$('#inventory-change-info').show();
$('#best_before_date').attr('required', 'required');
}
else if (newAmount < productStockAmount)
{
var amountToRemove = productStockAmount - newAmount;
$('#inventory-change-info').text('This means ' + amountToRemove.toString() + ' ' + productDetails.quantity_unit_stock.name + ' will be removed from stock');
$('#inventory-change-info').show();
$('#best_before_date').removeAttr('required');
}
else
{
$('#inventory-change-info').hide();
}
$('#inventory-form').validator('update');
$('#inventory-form').validator('validate');
},
function(xhr)
{
console.error(xhr);
}
);
}
});

52
views/inventory.php Normal file
View File

@@ -0,0 +1,52 @@
<div class="col-sm-4 col-sm-offset-3 col-md-3 col-md-offset-2 main">
<h1 class="page-header">Inventory</h1>
<form id="inventory-form">
<div class="form-group">
<label for="product_id">Product&nbsp;&nbsp;<i class="fa fa-barcode"></i><span id="barcode-lookup-disabled-hint" class="small text-muted hide">&nbsp;&nbsp;Barcode lookup is disabled</span></label>
<select class="form-control combobox" id="product_id" name="product_id" required>
<option value=""></option>
<?php foreach ($products as $product) : ?>
<option data-additional-searchdata="<?php echo $product->barcode; ?>" value="<?php echo $product->id; ?>"><?php echo $product->name; ?></option>
<?php endforeach; ?>
</select>
<div class="help-block with-errors"></div>
<div id="flow-info-addbarcodetoselection" class="text-muted small hide"><strong><span id="addbarcodetoselection"></span></strong> will be added to the list of barcodes for the selected product on submit.</div>
</div>
<div class="form-group">
<label for="new_amount">New amount&nbsp;&nbsp;<span id="new_amount_qu_unit" class="small text-muted"></span></label>
<input type="number" data-notequal="notequal" class="form-control" id="new_amount" name="new_amount" min="0" not-equal="-1" required>
<div class="help-block with-errors"></div>
<div id="inventory-change-info" class="help-block text-muted"></div>
</div>
<div class="form-group">
<label for="best_before_date">Best before&nbsp;&nbsp;<span class="small text-muted">This will apply to added products</span></label>
<div class="input-group date">
<input type="text" data-isodate="isodate" class="form-control datepicker" id="best_before_date" name="best_before_date" autocomplete="off">
<div id="best_before_date-datepicker-button" class="input-group-addon">
<i class="fa fa-calendar"></i>
</div>
</div>
<div class="help-block with-errors"></div>
</div>
<button id="save-inventory-button" type="submit" class="btn btn-default">OK</button>
</form>
</div>
<div class="col-sm-6 col-md-5 col-lg-3 main well">
<h3>Product overview <strong><span id="selected-product-name"></span></strong></h3>
<h4><strong>Purchase quantity:</strong> <span id="selected-product-purchase-qu-name"></span></h4>
<p>
<strong>Stock amount:</strong> <span id="selected-product-stock-amount"></span> <span id="selected-product-stock-qu-name"></span><br />
<strong>Last purchased:</strong> <span id="selected-product-last-purchased"></span> <time id="selected-product-last-purchased-timeago" class="timeago timeago-contextual"></time><br />
<strong>Last used:</strong> <span id="selected-product-last-used"></span> <time id="selected-product-last-used-timeago" class="timeago timeago-contextual"></time>
</p>
</div>

View File

@@ -8,6 +8,7 @@
<meta name="robots" content="noindex,nofollow" /> <meta name="robots" content="noindex,nofollow" />
<meta name="author" content="Bernd Bestel (bernd@berrnd.de)" /> <meta name="author" content="Bernd Bestel (bernd@berrnd.de)" />
<link rel="icon" href="/grocy.png" />
<title><?php echo $title; ?> | grocy</title> <title><?php echo $title; ?> | grocy</title>
@@ -19,6 +20,7 @@
<link href="/bower_components/datatables.net-responsive-bs/css/responsive.bootstrap.min.css?v=<?php echo Grocy::GetInstalledVersion(); ?>" rel="stylesheet" /> <link href="/bower_components/datatables.net-responsive-bs/css/responsive.bootstrap.min.css?v=<?php echo Grocy::GetInstalledVersion(); ?>" rel="stylesheet" />
<link href="/bower_components/toastr/toastr.min.css?v=<?php echo Grocy::GetInstalledVersion(); ?>" rel="stylesheet" /> <link href="/bower_components/toastr/toastr.min.css?v=<?php echo Grocy::GetInstalledVersion(); ?>" rel="stylesheet" />
<link href="/bower_components/tagmanager/tagmanager.css?v=<?php echo Grocy::GetInstalledVersion(); ?>" rel="stylesheet" /> <link href="/bower_components/tagmanager/tagmanager.css?v=<?php echo Grocy::GetInstalledVersion(); ?>" rel="stylesheet" />
<link href="/bower_components/eonasdan-bootstrap-datetimepicker/build/css/bootstrap-datetimepicker.min.css?v=<?php echo Grocy::GetInstalledVersion(); ?>" rel="stylesheet" />
<link href="/style.css?v=<?php echo Grocy::GetInstalledVersion(); ?>" rel="stylesheet" /> <link href="/style.css?v=<?php echo Grocy::GetInstalledVersion(); ?>" rel="stylesheet" />
<script src="/bower_components/jquery/dist/jquery.min.js?v=<?php echo Grocy::GetInstalledVersion(); ?>"></script> <script src="/bower_components/jquery/dist/jquery.min.js?v=<?php echo Grocy::GetInstalledVersion(); ?>"></script>
@@ -37,58 +39,117 @@
<a class="navbar-brand" href="/">grocy</a> <a class="navbar-brand" href="/">grocy</a>
</div> </div>
<div id="navbar-mobile" class="navbar-collapse collapse"> <div id="navbar" class="navbar-collapse collapse">
<ul class="nav navbar-nav navbar-right"> <ul class="nav navbar-nav navbar-right">
<li data-nav-for-page="dashboard.php"> <li>
<a class="discrete-link" href="/"><i class="fa fa-tachometer fa-fw"></i>&nbsp;Dashboard</a> <a class="discrete-link logout-button" href="/logout"><i class="fa fa-sign-out fa-fw"></i>&nbsp;Logout</a>
</li>
<li data-nav-for-page="purchase.php">
<a class="discrete-link" href="/purchase"><i class="fa fa-shopping-cart fa-fw"></i>&nbsp;Record purchase</a>
</li>
<li data-nav-for-page="consumption.php">
<a class="discrete-link" href="/consumption"><i class="fa fa-cutlery fa-fw"></i>&nbsp;Record consumption</a>
</li> </li>
</ul> </ul>
</div>
<div id="navbar-mobile" class="navbar-collapse collapse">
<ul class="nav navbar-nav navbar-right"> <ul class="nav navbar-nav navbar-right">
<li data-nav-for-page="stockoverview.php">
<a class="discrete-link" href="/stockoverview"><i class="fa fa-tachometer fa-fw"></i>&nbsp;Stock overview</a>
</li>
<li data-nav-for-page="habitsoverview.php">
<a class="discrete-link" href="/habitsoverview"><i class="fa fa-tachometer fa-fw"></i>&nbsp;Habits overview</a>
</li>
</ul>
<ul class="nav navbar-nav navbar-right">
<li data-nav-for-page="purchase.php">
<a class="discrete-link" href="/purchase"><i class="fa fa-shopping-cart fa-fw"></i>&nbsp;Purchase</a>
</li>
<li data-nav-for-page="consume.php">
<a class="discrete-link" href="/consume"><i class="fa fa-cutlery fa-fw"></i>&nbsp;Consume</a>
</li>
<li data-nav-for-page="inventory.php">
<a class="discrete-link" href="/inventory"><i class="fa fa-list fa-fw"></i>&nbsp;Inventory</a>
</li>
<li data-nav-for-page="shoppinglist.php">
<a class="discrete-link" href="/shoppinglist"><i class="fa fa-shopping-bag fa-fw"></i>&nbsp;Shopping list</a>
</li>
<li data-nav-for-page="habittracking.php">
<a class="discrete-link" href="/habittracking"><i class="fa fa-play fa-fw"></i>&nbsp;Habit tracking</a>
</li>
</ul>
<ul class="nav navbar-nav navbar-right">
<li class="disabled"><a href="#"><strong>Manage master data</strong></a></li>
<li data-nav-for-page="products.php"> <li data-nav-for-page="products.php">
<a class="discrete-link" href="/products"><i class="fa fa-product-hunt fa-fw"></i>&nbsp;Manage products</a> <a class="discrete-link" href="/products"><i class="fa fa-product-hunt fa-fw"></i>&nbsp;Products</a>
</li> </li>
<li data-nav-for-page="locations.php"> <li data-nav-for-page="locations.php">
<a class="discrete-link" href="/locations"><i class="fa fa-map-marker fa-fw"></i>&nbsp;Manage locations</a> <a class="discrete-link" href="/locations"><i class="fa fa-map-marker fa-fw"></i>&nbsp;Locations</a>
</li> </li>
<li data-nav-for-page="quantityunits.php"> <li data-nav-for-page="quantityunits.php">
<a class="discrete-link" href="/quantityunits"><i class="fa fa-balance-scale fa-fw"></i>&nbsp;Manage quantity units</a> <a class="discrete-link" href="/quantityunits"><i class="fa fa-balance-scale fa-fw"></i>&nbsp;Quantity units</a>
</li>
<li data-nav-for-page="habits.php">
<a class="discrete-link" href="/habits"><i class="fa fa-refresh fa-fw"></i>&nbsp;Habits</a>
</li> </li>
</ul> </ul>
<ul class="nav navbar-nav navbar-right">
<li>
<a class="discrete-link logout-button" href="/logout"><i class="fa fa-sign-out fa-fw"></i>&nbsp;Logout</a>
</li>
</ul>
</div> </div>
</div> </div>
</nav> </nav>
<div class="container-fluid"> <div class="container-fluid">
<div class="row"> <div class="row">
<div class="col-sm-3 col-md-2 sidebar"> <div class="col-sm-3 col-md-2 sidebar">
<ul class="nav nav-sidebar"> <ul class="nav nav-sidebar">
<li data-nav-for-page="dashboard.php"> <li data-nav-for-page="stockoverview.php">
<a class="discrete-link" href="/"><i class="fa fa-tachometer fa-fw"></i>&nbsp;Dashboard</a> <a class="discrete-link" href="/stockoverview"><i class="fa fa-tachometer fa-fw"></i>&nbsp;Stock overview</a>
</li> </li>
<li data-nav-for-page="purchase.php"> <li data-nav-for-page="habitsoverview.php">
<a class="discrete-link" href="/purchase"><i class="fa fa-shopping-cart fa-fw"></i>&nbsp;Record purchase</a> <a class="discrete-link" href="/habitsoverview"><i class="fa fa-tachometer fa-fw"></i>&nbsp;Habits overview</a>
</li>
<li data-nav-for-page="consumption.php">
<a class="discrete-link" href="/consumption"><i class="fa fa-cutlery fa-fw"></i>&nbsp;Record consumption</a>
</li> </li>
</ul> </ul>
<ul class="nav nav-sidebar"> <ul class="nav nav-sidebar">
<li data-nav-for-page="purchase.php">
<a class="discrete-link" href="/purchase"><i class="fa fa-shopping-cart fa-fw"></i>&nbsp;Purchase</a>
</li>
<li data-nav-for-page="consume.php">
<a class="discrete-link" href="/consume"><i class="fa fa-cutlery fa-fw"></i>&nbsp;Consume</a>
</li>
<li data-nav-for-page="inventory.php">
<a class="discrete-link" href="/inventory"><i class="fa fa-list fa-fw"></i>&nbsp;Inventory</a>
</li>
<li data-nav-for-page="shoppinglist.php">
<a class="discrete-link" href="/shoppinglist"><i class="fa fa-shopping-bag fa-fw"></i>&nbsp;Shopping list</a>
</li>
<li data-nav-for-page="habittracking.php">
<a class="discrete-link" href="/habittracking"><i class="fa fa-play fa-fw"></i>&nbsp;Habit tracking</a>
</li>
</ul>
<ul class="nav nav-sidebar">
<li class="disabled"><a href="#"><strong>Manage master data</strong></a></li>
<li data-nav-for-page="products.php"> <li data-nav-for-page="products.php">
<a class="discrete-link" href="/products"><i class="fa fa-product-hunt fa-fw"></i>&nbsp;Manage products</a> <a class="discrete-link" href="/products"><i class="fa fa-product-hunt fa-fw"></i>&nbsp;Products</a>
</li> </li>
<li data-nav-for-page="locations.php"> <li data-nav-for-page="locations.php">
<a class="discrete-link" href="/locations"><i class="fa fa-map-marker fa-fw"></i>&nbsp;Manage locations</a> <a class="discrete-link" href="/locations"><i class="fa fa-map-marker fa-fw"></i>&nbsp;Locations</a>
</li> </li>
<li data-nav-for-page="quantityunits.php"> <li data-nav-for-page="quantityunits.php">
<a class="discrete-link" href="/quantityunits"><i class="fa fa-balance-scale fa-fw"></i>&nbsp;Manage quantity units</a> <a class="discrete-link" href="/quantityunits"><i class="fa fa-balance-scale fa-fw"></i>&nbsp;Quantity units</a>
</li>
<li data-nav-for-page="habits.php">
<a class="discrete-link" href="/habits"><i class="fa fa-refresh fa-fw"></i>&nbsp;Habits</a>
</li> </li>
</ul> </ul>
<div class="nav-copyright nav nav-sidebar"> <div class="nav-copyright nav nav-sidebar">
grocy is a project by grocy is a project by
<a class="discrete-link" href="https://berrnd.de" target="_blank">Bernd Bestel</a> <a class="discrete-link" href="https://berrnd.de" target="_blank">Bernd Bestel</a>
@@ -103,10 +164,12 @@
<i class="fa fa-github"></i> <i class="fa fa-github"></i>
</a> </a>
</div> </div>
</div> </div>
<script>Grocy.ContentPage = '<?php echo $contentPage; ?>';</script> <script>Grocy.ContentPage = '<?php echo $contentPage; ?>';</script>
<?php include $contentPage; ?> <?php include $contentPage; ?>
</div> </div>
</div> </div>
@@ -124,6 +187,7 @@
<script src="/bower_components/jquery-timeago/jquery.timeago.js?v=<?php echo Grocy::GetInstalledVersion(); ?>"></script> <script src="/bower_components/jquery-timeago/jquery.timeago.js?v=<?php echo Grocy::GetInstalledVersion(); ?>"></script>
<script src="/bower_components/toastr/toastr.min.js?v=<?php echo Grocy::GetInstalledVersion(); ?>"></script> <script src="/bower_components/toastr/toastr.min.js?v=<?php echo Grocy::GetInstalledVersion(); ?>"></script>
<script src="/bower_components/tagmanager/tagmanager.js?v=<?php echo Grocy::GetInstalledVersion(); ?>"></script> <script src="/bower_components/tagmanager/tagmanager.js?v=<?php echo Grocy::GetInstalledVersion(); ?>"></script>
<script src="/bower_components/eonasdan-bootstrap-datetimepicker/build/js/bootstrap-datetimepicker.min.js?v=<?php echo Grocy::GetInstalledVersion(); ?>"></script>
<?php if (file_exists(__DIR__ . '/' . str_replace('.php', '.js', $contentPage))) : ?> <?php if (file_exists(__DIR__ . '/' . str_replace('.php', '.js', $contentPage))) : ?>
<script src="/views/<?php echo str_replace('.php', '.js', $contentPage) . '?v=' . Grocy::GetInstalledVersion(); ?>"></script> <script src="/views/<?php echo str_replace('.php', '.js', $contentPage) . '?v=' . Grocy::GetInstalledVersion(); ?>"></script>

View File

@@ -1,4 +1,5 @@
<div class="col-sm-3 col-sm-offset-3 col-md-4 col-md-offset-2 main"> <div class="col-sm-3 col-sm-offset-3 col-md-4 col-md-offset-2 main">
<h1 class="page-header"><?php echo $title; ?></h1> <h1 class="page-header"><?php echo $title; ?></h1>
<script>Grocy.EditMode = '<?php echo $mode; ?>';</script> <script>Grocy.EditMode = '<?php echo $mode; ?>';</script>
@@ -8,15 +9,20 @@
<?php endif; ?> <?php endif; ?>
<form id="location-form"> <form id="location-form">
<div class="form-group"> <div class="form-group">
<label for="name">Name</label> <label for="name">Name</label>
<input type="text" class="form-control" required id="name" name="name" value="<?php if ($mode == 'edit') echo $location->name; ?>" /> <input type="text" class="form-control" required id="name" name="name" value="<?php if ($mode == 'edit') echo $location->name; ?>" />
<div class="help-block with-errors"></div> <div class="help-block with-errors"></div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="description">Description</label> <label for="description">Description</label>
<textarea class="form-control" rows="2" id="description" name="description"><?php if ($mode == 'edit') echo $location->description; ?></textarea> <textarea class="form-control" rows="2" id="description" name="description"><?php if ($mode == 'edit') echo $location->description; ?></textarea>
</div> </div>
<button id="save-location-button" type="submit" class="btn btn-default">Save</button> <button id="save-location-button" type="submit" class="btn btn-default">Save</button>
</form> </form>
</div> </div>

View File

@@ -14,7 +14,7 @@
}, },
callback: function(result) callback: function(result)
{ {
if (result == true) if (result === true)
{ {
Grocy.FetchJson('/api/delete-object/locations/' + $(e.target).attr('data-location-id'), Grocy.FetchJson('/api/delete-object/locations/' + $(e.target).attr('data-location-id'),
function(result) function(result)
@@ -41,4 +41,3 @@ $(function()
] ]
}); });
}); });

View File

@@ -1,4 +1,5 @@
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main"> <div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
<h1 class="page-header"> <h1 class="page-header">
Locations Locations
<a class="btn btn-default" href="/location/new" role="button"> <a class="btn btn-default" href="/location/new" role="button">
@@ -37,4 +38,5 @@
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>

12
views/login.js Normal file
View File

@@ -0,0 +1,12 @@
$(function()
{
$('.logout-button').hide();
$('#username').focus();
if (Grocy.GetUriParam('invalid') === 'true')
{
$('#login-error').text('Invalid credentials, please try again.');
$('#login-error').show();
}
});

23
views/login.php Normal file
View File

@@ -0,0 +1,23 @@
<div class="col-md-4 col-md-offset-5 main">
<h1 class="page-header text-center">Login</h1>
<form method="post" action="/login" id="login-form">
<div class="form-group">
<label for="name">Username</label>
<input type="text" class="form-control" required id="username" name="username" />
<div class="help-block with-errors"></div>
</div>
<div class="form-group">
<label for="name">Password</label>
<input type="password" class="form-control" required id="password" name="password" />
<div id="login-error" class="help-block with-errors"></div>
</div>
<button id="login-button" type="submit" class="btn btn-default">Login</button>
</form>
</div>

View File

@@ -2,12 +2,19 @@
{ {
e.preventDefault(); e.preventDefault();
var redirectDestination = '/products';
var returnTo = Grocy.GetUriParam('returnto');
if (returnTo !== undefined)
{
redirectDestination = returnTo + '?createdproduct=' + encodeURIComponent($('#name').val());
}
if (Grocy.EditMode === 'create') if (Grocy.EditMode === 'create')
{ {
Grocy.PostJson('/api/add-object/products', $('#product-form').serializeJSON(), Grocy.PostJson('/api/add-object/products', $('#product-form').serializeJSON(),
function(result) function(result)
{ {
window.location.href = '/products'; window.location.href = redirectDestination;
}, },
function(xhr) function(xhr)
{ {
@@ -20,7 +27,7 @@
Grocy.PostJson('/api/edit-object/products/' + Grocy.EditObjectId, $('#product-form').serializeJSON(), Grocy.PostJson('/api/edit-object/products/' + Grocy.EditObjectId, $('#product-form').serializeJSON(),
function(result) function(result)
{ {
window.location.href = '/products'; window.location.href = redirectDestination;
}, },
function(xhr) function(xhr)
{ {
@@ -42,7 +49,7 @@ $(function()
Grocy.FetchJson('/api/get-object/products/' + Grocy.EditObjectId, Grocy.FetchJson('/api/get-object/products/' + Grocy.EditObjectId,
function (product) function (product)
{ {
if (product.barcode.length > 0) if (product.barcode !== null && product.barcode.length > 0)
{ {
product.barcode.split(',').forEach(function(item) product.barcode.split(',').forEach(function(item)
{ {
@@ -61,6 +68,20 @@ $(function()
$('#name').focus(); $('#name').focus();
$('#product-form').validator(); $('#product-form').validator();
$('#product-form').validator('validate'); $('#product-form').validator('validate');
var prefillName = Grocy.GetUriParam('prefillname');
if (prefillName !== undefined)
{
$('#name').val(prefillName);
$('#name').focus();
}
var prefillBarcode = Grocy.GetUriParam('prefillbarcode');
if (prefillBarcode !== undefined)
{
$('#barcode-taginput').tagsManager('pushTag', prefillBarcode);
$('#name').focus();
}
}); });
$('.input-group-qu').on('change', function(e) $('.input-group-qu').on('change', function(e)

View File

@@ -1,4 +1,5 @@
<div class="col-sm-3 col-sm-offset-3 col-md-4 col-md-offset-2 main"> <div class="col-sm-3 col-sm-offset-3 col-md-4 col-md-offset-2 main">
<h1 class="page-header"><?php echo $title; ?></h1> <h1 class="page-header"><?php echo $title; ?></h1>
<script>Grocy.EditMode = '<?php echo $mode; ?>';</script> <script>Grocy.EditMode = '<?php echo $mode; ?>';</script>
@@ -8,20 +9,24 @@
<?php endif; ?> <?php endif; ?>
<form id="product-form"> <form id="product-form">
<div class="form-group"> <div class="form-group">
<label for="name">Name</label> <label for="name">Name</label>
<input type="text" class="form-control" required id="name" name="name" value="<?php if ($mode == 'edit') echo $product->name; ?>"> <input type="text" class="form-control" required id="name" name="name" value="<?php if ($mode == 'edit') echo $product->name; ?>">
<div class="help-block with-errors"></div> <div class="help-block with-errors"></div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="description">Description</label> <label for="description">Description</label>
<textarea class="form-control" rows="2" id="description" name="description"><?php if ($mode == 'edit') echo $product->description; ?></textarea> <textarea class="form-control" rows="2" id="description" name="description"><?php if ($mode == 'edit') echo $product->description; ?></textarea>
</div> </div>
<div class="form-group tm-group"> <div class="form-group tm-group">
<label for="barcode-taginput">Barcode(s)</label> <label for="barcode-taginput">Barcode(s)&nbsp;&nbsp;<i class="fa fa-barcode"></i></label>
<input type="text" class="form-control tm-input" id="barcode-taginput" placeholder="Add (scan) a barcode here to add one..."> <input type="text" class="form-control tm-input" id="barcode-taginput">
<div id="barcode-taginput-container"></div> <div id="barcode-taginput-container"></div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="location_id">Location</label> <label for="location_id">Location</label>
<select required class="form-control" id="location_id" name="location_id"> <select required class="form-control" id="location_id" name="location_id">
@@ -31,6 +36,19 @@
</select> </select>
<div class="help-block with-errors"></div> <div class="help-block with-errors"></div>
</div> </div>
<div class="form-group">
<label for="min_stock_amount">Minimum stock amount</label>
<input required min="0" type="number" class="form-control" id="min_stock_amount" name="min_stock_amount" value="<?php if ($mode == 'edit') echo $product->min_stock_amount; else echo '0'; ?>">
<div class="help-block with-errors"></div>
</div>
<div class="form-group">
<label for="default_best_before_days">Default best before days<br /><span class="small text-muted">For purchases this amount of days will be added to today for the best before date suggestion</span></label>
<input required min="0" type="number" class="form-control" id="default_best_before_days" name="default_best_before_days" value="<?php if ($mode == 'edit') echo $product->default_best_before_days; else echo '0'; ?>">
<div class="help-block with-errors"></div>
</div>
<div class="form-group"> <div class="form-group">
<label for="qu_id_purchase">Quantity unit purchase</label> <label for="qu_id_purchase">Quantity unit purchase</label>
<select required class="form-control input-group-qu" id="qu_id_purchase" name="qu_id_purchase"> <select required class="form-control input-group-qu" id="qu_id_purchase" name="qu_id_purchase">
@@ -40,6 +58,7 @@
</select> </select>
<div class="help-block with-errors"></div> <div class="help-block with-errors"></div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="qu_id_stock">Quantity unit stock</label> <label for="qu_id_stock">Quantity unit stock</label>
<select required class="form-control input-group-qu" id="qu_id_stock" name="qu_id_stock"> <select required class="form-control input-group-qu" id="qu_id_stock" name="qu_id_stock">
@@ -49,12 +68,16 @@
</select> </select>
<div class="help-block with-errors"></div> <div class="help-block with-errors"></div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="qu_factor_purchase_to_stock">Factor purchase to stock quantity unit</label> <label for="qu_factor_purchase_to_stock">Factor purchase to stock quantity unit</label>
<input required min="1" type="number" class="form-control input-group-qu" id="qu_factor_purchase_to_stock" name="qu_factor_purchase_to_stock" value="<?php if ($mode == 'edit') echo $product->qu_factor_purchase_to_stock; else echo '1'; ?>"> <input required min="1" type="number" class="form-control input-group-qu" id="qu_factor_purchase_to_stock" name="qu_factor_purchase_to_stock" value="<?php if ($mode == 'edit') echo $product->qu_factor_purchase_to_stock; else echo '1'; ?>">
<div class="help-block with-errors"></div> <div class="help-block with-errors"></div>
</div> </div>
<p id="qu-conversion-info" class="help-block text-muted"></p> <p id="qu-conversion-info" class="help-block text-muted"></p>
<button id="save-product-button" type="submit" class="btn btn-default">Save</button> <button id="save-product-button" type="submit" class="btn btn-default">Save</button>
</form> </form>
</div> </div>

View File

@@ -14,7 +14,7 @@
}, },
callback: function(result) callback: function(result)
{ {
if (result == true) if (result === true)
{ {
Grocy.FetchJson('/api/delete-object/products/' + $(e.target).attr('data-product-id'), Grocy.FetchJson('/api/delete-object/products/' + $(e.target).attr('data-product-id'),
function(result) function(result)

View File

@@ -1,4 +1,5 @@
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main"> <div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
<h1 class="page-header"> <h1 class="page-header">
Products Products
<a class="btn btn-default" href="/product/new" role="button"> <a class="btn btn-default" href="/product/new" role="button">
@@ -13,6 +14,7 @@
<th>#</th> <th>#</th>
<th>Name</th> <th>Name</th>
<th>Location</th> <th>Location</th>
<th>Min. stock amount</th>
<th>QU purchase</th> <th>QU purchase</th>
<th>QU stock</th> <th>QU stock</th>
<th>QU factor</th> <th>QU factor</th>
@@ -36,6 +38,9 @@
<td> <td>
<?php echo GrocyPhpHelper::FindObjectInArrayByPropertyValue($locations, 'id', $product->location_id)->name; ?> <?php echo GrocyPhpHelper::FindObjectInArrayByPropertyValue($locations, 'id', $product->location_id)->name; ?>
</td> </td>
<td>
<?php echo $product->min_stock_amount; ?>
</td>
<td> <td>
<?php echo GrocyPhpHelper::FindObjectInArrayByPropertyValue($quantityunits, 'id', $product->qu_id_purchase)->name; ?> <?php echo GrocyPhpHelper::FindObjectInArrayByPropertyValue($quantityunits, 'id', $product->qu_id_purchase)->name; ?>
</td> </td>
@@ -53,4 +58,5 @@
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>

View File

@@ -7,31 +7,50 @@
Grocy.FetchJson('/api/stock/get-product-details/' + jsonForm.product_id, Grocy.FetchJson('/api/stock/get-product-details/' + jsonForm.product_id,
function (productDetails) function (productDetails)
{ {
jsonForm.amount = jsonForm.amount * productDetails.product.qu_factor_purchase_to_stock; var amount = jsonForm.amount * productDetails.product.qu_factor_purchase_to_stock;
Grocy.FetchJson('/api/helper/uniqid', Grocy.FetchJson('/api/stock/add-product/' + jsonForm.product_id + '/' + amount + '?bestbeforedate=' + $('#best_before_date').val(),
function(uniqidResponse)
{
jsonForm.stock_id = uniqidResponse.uniqid;
Grocy.PostJson('/api/add-object/stock', jsonForm,
function(result) function(result)
{ {
toastr.success('Added ' + jsonForm.amount + ' ' + productDetails.quantity_unit_stock.name + ' of ' + productDetails.product.name + ' to stock'); var addBarcode = Grocy.GetUriParam('addbarcodetoselection');
if (addBarcode !== undefined)
{
var existingBarcodes = productDetails.product.barcode || '';
if (existingBarcodes.length === 0)
{
productDetails.product.barcode = addBarcode;
}
else
{
productDetails.product.barcode += ',' + addBarcode;
}
$('#amount').val(1); Grocy.PostJson('/api/edit-object/products/' + productDetails.product.id, productDetails.product,
function (result) { },
function(xhr)
{
console.error(xhr);
}
);
}
toastr.success('Added ' + amount + ' ' + productDetails.quantity_unit_stock.name + ' of ' + productDetails.product.name + ' to stock');
Grocy.Wait(1000);
if (addBarcode !== undefined)
{
window.location.href = '/purchase';
}
else
{
$('#amount').val(0);
$('#best_before_date').val(''); $('#best_before_date').val('');
$('#product_id').val(''); $('#product_id').val('');
$('#product_id_text_input').focus(); $('#product_id_text_input').focus();
$('#product_id_text_input').val(''); $('#product_id_text_input').val('');
$('#product_id_text_input').trigger('change'); $('#product_id_text_input').trigger('change');
$('#purchase-form').validator('validate'); $('#purchase-form').validator('validate');
},
function(xhr)
{
console.error(xhr);
} }
);
}, },
function(xhr) function(xhr)
{ {
@@ -63,6 +82,13 @@ $('#product_id').on('change', function(e)
$('#selected-product-last-purchased-timeago').text($.timeago(productDetails.last_purchased || '')); $('#selected-product-last-purchased-timeago').text($.timeago(productDetails.last_purchased || ''));
$('#selected-product-last-used').text((productDetails.last_used || 'never').substring(0, 10)); $('#selected-product-last-used').text((productDetails.last_used || 'never').substring(0, 10));
$('#selected-product-last-used-timeago').text($.timeago(productDetails.last_used || '')); $('#selected-product-last-used-timeago').text($.timeago(productDetails.last_used || ''));
$('#amount_qu_unit').text(productDetails.quantity_unit_purchase.name);
if (productDetails.product.default_best_before_days.toString() !== '0')
{
$('#best_before_date').val(moment().add(productDetails.product.default_best_before_days, 'days').format('YYYY-MM-DD'));
$('#best_before_date').trigger('change');
}
Grocy.EmptyElementWhenMatches('#selected-product-last-purchased-timeago', 'NaN years ago'); Grocy.EmptyElementWhenMatches('#selected-product-last-purchased-timeago', 'NaN years ago');
Grocy.EmptyElementWhenMatches('#selected-product-last-used-timeago', 'NaN years ago'); Grocy.EmptyElementWhenMatches('#selected-product-last-used-timeago', 'NaN years ago');
@@ -90,9 +116,83 @@ $(function()
}); });
$('.datepicker').trigger('change'); $('.datepicker').trigger('change');
$('.combobox').combobox({ appendId: '_text_input' }); $('.combobox').combobox({
appendId: '_text_input'
});
$('#amount').val(1); $('#product_id_text_input').on('change', function(e)
{
var input = $('#product_id_text_input').val().toString();
var possibleOptionElement = $("#product_id option[data-additional-searchdata*='" + input + "']").first();
if (Grocy.GetUriParam('addbarcodetoselection') === undefined && possibleOptionElement.length > 0)
{
$('#product_id').val(possibleOptionElement.val());
$('#product_id').data('combobox').refresh();
$('#product_id').trigger('change');
}
else
{
var optionElement = $("#product_id option:contains('" + input + "')").first();
if (input.length > 0 && optionElement.length === 0 && Grocy.GetUriParam('addbarcodetoselection') === undefined )
{
bootbox.dialog({
message: '<strong>' + input + '</strong> could not be resolved to a product, how do you want to proceed?',
title: 'Create or assign product',
onEscape: function() { },
size: 'large',
backdrop: true,
buttons: {
cancel: {
label: 'Cancel',
className: 'btn-default',
callback: function() { }
},
addnewproduct: {
label: 'Add as new <u><strong>p</strong></u>roduct',
className: 'btn-success add-new-product-dialog-button',
callback: function()
{
window.location.href = '/product/new?prefillname=' + encodeURIComponent(input) + '&returnto=' + encodeURIComponent(window.location.pathname);
}
},
addbarcode: {
label: 'Add as <u><strong>b</strong></u>arcode to existing product',
className: 'btn-info add-new-barcode-dialog-button',
callback: function()
{
window.location.href = '/purchase?addbarcodetoselection=' + encodeURIComponent(input);
}
},
addnewproductwithbarcode: {
label: '<u><strong>A</strong></u>dd as new product + prefill barcode',
className: 'btn-warning add-new-product-with-barcode-dialog-button',
callback: function()
{
window.location.href = '/product/new?prefillbarcode=' + encodeURIComponent(input) + '&returnto=' + encodeURIComponent(window.location.pathname);
}
}
}
}).on('keypress', function(e)
{
if (e.key === 'B' || e.key === 'b')
{
$('.add-new-barcode-dialog-button').click();
}
if (e.key === 'p' || e.key === 'P')
{
$('.add-new-product-dialog-button').click();
}
if (e.key === 'a' || e.key === 'A')
{
$('.add-new-product-with-barcode-dialog-button').click();
}
});
}
}
});
$('#amount').val(0);
$('#best_before_date').val(''); $('#best_before_date').val('');
$('#product_id').val(''); $('#product_id').val('');
$('#product_id_text_input').focus(); $('#product_id_text_input').focus();
@@ -107,11 +207,26 @@ $(function()
{ {
return 'Wrong date format, needs to be YYYY-MM-DD'; return 'Wrong date format, needs to be YYYY-MM-DD';
} }
else if (moment($el.val()).isValid())
{
if (moment($el.val()).isBefore(moment(), 'day'))
{
return 'This value cannot be before today.';
}
}
} }
} }
}); });
$('#purchase-form').validator('validate'); $('#purchase-form').validator('validate');
$('#best_before_date').on('focus', function(e)
{
if ($('#product_id_text_input').val().length === 0)
{
$('#product_id_text_input').focus();
}
});
$('#purchase-form input').keydown(function(event) $('#purchase-form input').keydown(function(event)
{ {
if (event.keyCode === 13) //Enter if (event.keyCode === 13) //Enter
@@ -123,6 +238,32 @@ $(function()
} }
} }
}); });
var prefillProduct = Grocy.GetUriParam('createdproduct');
if (prefillProduct !== undefined)
{
var possibleOptionElement = $("#product_id option[data-additional-searchdata*='" + prefillProduct + "']").first();
if (possibleOptionElement.length === 0)
{
possibleOptionElement = $("#product_id option:contains('" + prefillProduct + "')").first();
}
if (possibleOptionElement.length > 0)
{
$('#product_id').val(possibleOptionElement.val());
$('#product_id').data('combobox').refresh();
$('#product_id').trigger('change');
$('#best_before_date').focus();
}
}
var addBarcode = Grocy.GetUriParam('addbarcodetoselection');
if (addBarcode !== undefined)
{
$('#addbarcodetoselection').text(addBarcode);
$('#flow-info-addbarcodetoselection').removeClass('hide');
$('#barcode-lookup-disabled-hint').removeClass('hide');
}
}); });
$('#best_before_date-datepicker-button').on('click', function(e) $('#best_before_date-datepicker-button').on('click', function(e)
@@ -133,6 +274,19 @@ $('#best_before_date-datepicker-button').on('click', function(e)
$('#best_before_date').on('change', function(e) $('#best_before_date').on('change', function(e)
{ {
var value = $('#best_before_date').val(); var value = $('#best_before_date').val();
var now = new Date();
var centuryStart = Number.parseInt(now.getFullYear().toString().substring(0, 2) + '00');
var centuryEnd = Number.parseInt(now.getFullYear().toString().substring(0, 2) + '99');
if (value === 'x' || value === 'X') {
value = '29991231';
}
if (value.length === 4 && !(Number.parseInt(value) > centuryStart && Number.parseInt(value) < centuryEnd))
{
value = (new Date()).getFullYear().toString() + value;
}
if (value.length === 8 && $.isNumeric(value)) if (value.length === 8 && $.isNumeric(value))
{ {
value = value.replace(/(\d{4})(\d{2})(\d{2})/, '$1-$2-$3'); value = value.replace(/(\d{4})(\d{2})(\d{2})/, '$1-$2-$3');
@@ -149,11 +303,13 @@ $('#best_before_date').on('keypress', function(e)
$('.datepicker').datepicker('hide'); $('.datepicker').datepicker('hide');
if (value.length === 0) //If input is empty and any arrow key is pressed, set date to today
if (value.length === 0 && (e.keyCode === 38 || e.keyCode === 40 || e.keyCode === 37 || e.keyCode === 39))
{ {
element.val(moment().format('YYYY-MM-DD')); dateObj = moment(new Date(), 'YYYY-MM-DD', true);
} }
else if (dateObj.isValid())
if (dateObj.isValid())
{ {
if (e.keyCode === 38) //Up if (e.keyCode === 38) //Up
{ {

View File

@@ -1,17 +1,21 @@
<div class="col-sm-4 col-sm-offset-3 col-md-3 col-md-offset-2 main"> <div class="col-sm-4 col-sm-offset-3 col-md-3 col-md-offset-2 main">
<h1 class="page-header">Purchase</h1> <h1 class="page-header">Purchase</h1>
<form id="purchase-form"> <form id="purchase-form">
<div class="form-group"> <div class="form-group">
<label for="product_id">Product&nbsp;&nbsp;<i class="fa fa-barcode"></i></label> <label for="product_id">Product&nbsp;&nbsp;<i class="fa fa-barcode"></i><span id="barcode-lookup-disabled-hint" class="small text-muted hide">&nbsp;&nbsp;Barcode lookup is disabled</span></label>
<select class="form-control combobox" id="product_id" name="product_id" required> <select class="form-control combobox" id="product_id" name="product_id" required>
<option value=""></option> <option value=""></option>
<?php foreach ($products as $product) : ?> <?php foreach ($products as $product) : ?>
<option value="<?php echo $product->id; ?>"><?php echo $product->name; ?><?php if (!empty($product->barcode)) echo ' [' . $product->barcode . ']'; ?></option> <option data-additional-searchdata="<?php echo $product->barcode; ?>" value="<?php echo $product->id; ?>"><?php echo $product->name; ?></option>
<?php endforeach; ?> <?php endforeach; ?>
</select> </select>
<div class="help-block with-errors"></div> <div class="help-block with-errors"></div>
<div id="flow-info-addbarcodetoselection" class="text-muted small hide"><strong><span id="addbarcodetoselection"></span></strong> will be added to the list of barcodes for the selected product on submit.</div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="best_before_date">Best before</label> <label for="best_before_date">Best before</label>
<div class="input-group date"> <div class="input-group date">
@@ -22,13 +26,17 @@
</div> </div>
<div class="help-block with-errors"></div> <div class="help-block with-errors"></div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="amount">Amount</label> <label for="amount">Amount&nbsp;&nbsp;<span id="amount_qu_unit" class="small text-muted"></span></label>
<input type="number" class="form-control" id="amount" name="amount" value="1" min="1" required> <input type="number" class="form-control" id="amount" name="amount" value="1" min="1" required>
<div class="help-block with-errors"></div> <div class="help-block with-errors"></div>
</div> </div>
<button id="save-purchase-button" type="submit" class="btn btn-default">OK</button> <button id="save-purchase-button" type="submit" class="btn btn-default">OK</button>
</form> </form>
</div> </div>
<div class="col-sm-6 col-md-5 col-lg-3 main well"> <div class="col-sm-6 col-md-5 col-lg-3 main well">

View File

@@ -1,4 +1,5 @@
<div class="col-sm-3 col-sm-offset-3 col-md-4 col-md-offset-2 main"> <div class="col-sm-3 col-sm-offset-3 col-md-4 col-md-offset-2 main">
<h1 class="page-header"><?php echo $title; ?></h1> <h1 class="page-header"><?php echo $title; ?></h1>
<script>Grocy.EditMode = '<?php echo $mode; ?>';</script> <script>Grocy.EditMode = '<?php echo $mode; ?>';</script>
@@ -8,15 +9,20 @@
<?php endif; ?> <?php endif; ?>
<form id="quantityunit-form"> <form id="quantityunit-form">
<div class="form-group"> <div class="form-group">
<label for="name">Name</label> <label for="name">Name</label>
<input type="text" class="form-control" required id="name" name="name" value="<?php if ($mode == 'edit') echo $quantityunit->name; ?>" /> <input type="text" class="form-control" required id="name" name="name" value="<?php if ($mode == 'edit') echo $quantityunit->name; ?>" />
<div class="help-block with-errors"></div> <div class="help-block with-errors"></div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="description">Description</label> <label for="description">Description</label>
<textarea class="form-control" rows="2" id="description" name="description"><?php if ($mode == 'edit') echo $quantityunit->description; ?></textarea> <textarea class="form-control" rows="2" id="description" name="description"><?php if ($mode == 'edit') echo $quantityunit->description; ?></textarea>
</div> </div>
<button id="save-quantityunit-button" type="submit" class="btn btn-default">Save</button> <button id="save-quantityunit-button" type="submit" class="btn btn-default">Save</button>
</form> </form>
</div> </div>

View File

@@ -14,7 +14,7 @@
}, },
callback: function(result) callback: function(result)
{ {
if (result == true) if (result === true)
{ {
Grocy.FetchJson('/api/delete-object/quantity_units/' + $(e.target).attr('data-quantityunit-id'), Grocy.FetchJson('/api/delete-object/quantity_units/' + $(e.target).attr('data-quantityunit-id'),
function(result) function(result)

View File

@@ -1,4 +1,5 @@
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main"> <div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
<h1 class="page-header"> <h1 class="page-header">
Quantity units Quantity units
<a class="btn btn-default" href="/quantityunit/new" role="button"> <a class="btn btn-default" href="/quantityunit/new" role="button">
@@ -37,4 +38,5 @@
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>

38
views/shoppinglist.js Normal file
View File

@@ -0,0 +1,38 @@
$(document).on('click', '.shoppinglist-delete-button', function(e)
{
Grocy.FetchJson('/api/delete-object/shopping_list/' + $(e.target).attr('data-shoppinglist-id'),
function(result)
{
window.location.href = '/shoppinglist';
},
function(xhr)
{
console.error(xhr);
}
);
});
$(document).on('click', '#add-products-below-min-stock-amount', function(e)
{
Grocy.FetchJson('/api/stock/add-missing-products-to-shoppinglist',
function(result)
{
window.location.href = '/shoppinglist';
},
function(xhr)
{
console.error(xhr);
}
);
});
$(function()
{
$('#shoppinglist-table').DataTable({
'pageLength': 50,
'order': [[1, 'asc']],
'columnDefs': [
{ 'orderable': false, 'targets': 0 }
]
});
});

45
views/shoppinglist.php Normal file
View File

@@ -0,0 +1,45 @@
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
<h1 class="page-header">
Shopping list
<a class="btn btn-default" href="/shoppinglistitem/new" role="button">
<i class="fa fa-plus"></i>&nbsp;Add
</a>
<a id="add-products-below-min-stock-amount" class="btn btn-info" href="#" role="button">
<i class="fa fa-plus"></i>&nbsp;Add products that are below defined min. stock amount
</a>
</h1>
<div class="table-responsive">
<table id="shoppinglist-table" class="table table-striped">
<thead>
<tr>
<th>#</th>
<th>Product</th>
<th>Amount</th>
</tr>
</thead>
<tbody>
<?php foreach ($listItems as $listItem) : ?>
<tr>
<td class="fit-content">
<a class="btn btn-info" href="/shoppinglistitem/<?php echo $listItem->id; ?>" role="button">
<i class="fa fa-pencil"></i>
</a>
<a class="btn btn-danger shoppinglist-delete-button" href="#" role="button" data-shoppinglist-id="<?php echo $listItem->id; ?>">
<i class="fa fa-trash"></i>
</a>
</td>
<td>
<?php echo GrocyPhpHelper::FindObjectInArrayByPropertyValue($products, 'id', $listItem->product_id)->name; ?>
</td>
<td>
<?php echo $listItem->amount + $listItem->amount_autoadded . ' ' . GrocyPhpHelper::FindObjectInArrayByPropertyValue($quantityunits, 'id', GrocyPhpHelper::FindObjectInArrayByPropertyValue($products, 'id', $listItem->product_id)->qu_id_purchase)->name; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>

147
views/shoppinglistform.js Normal file
View File

@@ -0,0 +1,147 @@
$('#save-shoppinglist-button').on('click', function(e)
{
e.preventDefault();
if (Grocy.EditMode === 'create')
{
Grocy.PostJson('/api/add-object/shopping_list', $('#shoppinglist-form').serializeJSON(),
function(result)
{
window.location.href = '/shoppinglist';
},
function(xhr)
{
console.error(xhr);
}
);
}
else
{
Grocy.PostJson('/api/edit-object/shopping_list/' + Grocy.EditObjectId, $('#shoppinglist-form').serializeJSON(),
function(result)
{
window.location.href = '/shoppinglist';
},
function(xhr)
{
console.error(xhr);
}
);
}
});
$('#product_id').on('change', function(e)
{
var productId = $(e.target).val();
if (productId)
{
Grocy.FetchJson('/api/stock/get-product-details/' + productId,
function (productDetails)
{
$('#selected-product-name').text(productDetails.product.name);
$('#selected-product-stock-amount').text(productDetails.stock_amount || '0');
$('#selected-product-stock-qu-name').text(productDetails.quantity_unit_stock.name);
$('#selected-product-stock-qu-name2').text(productDetails.quantity_unit_stock.name);
$('#selected-product-last-purchased').text((productDetails.last_purchased || 'never').substring(0, 10));
$('#selected-product-last-purchased-timeago').text($.timeago(productDetails.last_purchased || ''));
$('#selected-product-last-used').text((productDetails.last_used || 'never').substring(0, 10));
$('#selected-product-last-used-timeago').text($.timeago(productDetails.last_used || ''));
$('#amount_qu_unit').text(productDetails.quantity_unit_purchase.name);
Grocy.EmptyElementWhenMatches('#selected-product-last-purchased-timeago', 'NaN years ago');
Grocy.EmptyElementWhenMatches('#selected-product-last-used-timeago', 'NaN years ago');
if ($('#product_id').hasClass('suppress-next-custom-validate-event'))
{
$('#product_id').removeClass('suppress-next-custom-validate-event');
}
else
{
Grocy.FetchJson('/api/get-objects/shopping_list',
function (currentShoppingListItems)
{
if (currentShoppingListItems.filter(function (e) { return e.product_id === productId; }).length > 0)
{
$('#product_id').val('');
$('#product_id_text_input').val('');
$('#product_id_text_input').addClass('has-error');
$('#product_id_text_input').parent('.input-group').addClass('has-error');
$('#product_id_text_input').closest('.form-group').addClass('has-error');
$('#product-error').text('This product is already on the shopping list.');
$('#product-error').show();
$('#product_id_text_input').focus();
}
else
{
$('#product_id_text_input').removeClass('has-error');
$('#product_id_text_input').parent('.input-group').removeClass('has-error');
$('#product_id_text_input').closest('.form-group').removeClass('has-error');
$('#product-error').hide();
}
},
function(xhr)
{
console.error(xhr);
}
);
}
},
function(xhr)
{
console.error(xhr);
}
);
}
});
$(function()
{
$('.combobox').combobox({
appendId: '_text_input'
});
$('#product_id_text_input').on('change', function(e)
{
var input = $('#product_id_text_input').val().toString();
var possibleOptionElement = $("#product_id option[data-additional-searchdata*='" + input + "']").first();
if (possibleOptionElement.length > 0 && possibleOptionElement.text().length > 0) {
$('#product_id').val(possibleOptionElement.val());
$('#product_id').data('combobox').refresh();
$('#product_id').trigger('change');
}
});
$('#product_id_text_input').focus();
$('#product_id_text_input').trigger('change');
if (Grocy.EditMode === 'edit')
{
$('#product_id').addClass('suppress-next-custom-validate-event');
$('#product_id').trigger('change');
}
$('#shoppinglist-form').validator();
$('#shoppinglist-form').validator('validate');
$('#amount').on('focus', function(e)
{
if ($('#product_id_text_input').val().length === 0)
{
$('#product_id_text_input').focus();
}
});
$('#shoppinglist-form input').keydown(function(event)
{
if (event.keyCode === 13) //Enter
{
if ($('#shoppinglist-form').validator('validate').has('.has-error').length !== 0) //There is at least one validation error
{
event.preventDefault();
return false;
}
}
});
});

View File

@@ -0,0 +1,45 @@
<div class="col-sm-3 col-sm-offset-3 col-md-3 col-md-offset-2 main">
<h1 class="page-header"><?php echo $title; ?></h1>
<script>Grocy.EditMode = '<?php echo $mode; ?>';</script>
<?php if ($mode == 'edit') : ?>
<script>Grocy.EditObjectId = <?php echo $listItem->id; ?>;</script>
<?php endif; ?>
<form id="shoppinglist-form">
<div class="form-group">
<label for="product_id">Product&nbsp;&nbsp;<i class="fa fa-barcode"></i></label>
<select class="form-control combobox" id="product_id" name="product_id" value="<?php if ($mode == 'edit') echo $listItem->product_id; ?>" required>
<option value=""></option>
<?php foreach ($products as $product) : ?>
<option <?php if ($mode == 'edit' && $product->id == $listItem->product_id) echo 'selected="selected"'; ?> data-additional-searchdata="<?php echo $product->barcode; ?>" value="<?php echo $product->id; ?>"><?php echo $product->name; ?></option>
<?php endforeach; ?>
</select>
<div id="product-error" class="help-block with-errors"></div>
</div>
<div class="form-group">
<label for="amount">Amount&nbsp;&nbsp;<span id="amount_qu_unit" class="small text-muted"></span></label>
<input type="number" class="form-control" id="amount" name="amount" value="<?php if ($mode == 'edit') echo $listItem->amount; else echo '1'; ?>" min="1" required>
<div class="help-block with-errors"></div>
</div>
<button id="save-shoppinglist-button" type="submit" class="btn btn-default">Save</button>
</form>
</div>
<div class="col-sm-6 col-md-5 col-lg-3 main well">
<h3>Product overview <strong><span id="selected-product-name"></span></strong></h3>
<h4><strong>Stock quantity unit:</strong> <span id="selected-product-stock-qu-name"></span></h4>
<p>
<strong>Stock amount:</strong> <span id="selected-product-stock-amount"></span> <span id="selected-product-stock-qu-name2"></span><br />
<strong>Last purchased:</strong> <span id="selected-product-last-purchased"></span> <time id="selected-product-last-purchased-timeago" class="timeago timeago-contextual"></time><br />
<strong>Last used:</strong> <span id="selected-product-last-used"></span> <time id="selected-product-last-used-timeago" class="timeago timeago-contextual"></time>
</p>
</div>

7
views/stockoverview.js Normal file
View File

@@ -0,0 +1,7 @@
$(function()
{
$('#stock-overview-table').DataTable({
'pageLength': 50,
'order': [[2, 'asc']]
});
});

43
views/stockoverview.php Normal file
View File

@@ -0,0 +1,43 @@
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
<h1 class="page-header">Stock overview <span class="text-muded small"><strong><?php echo count($currentStock) ?></strong> products with <strong><?php echo GrocyPhpHelper::SumArrayValue($currentStock, 'amount'); ?></strong> units in stock</span></h1>
<div class="container-fluid">
<div class="row">
<p class="btn btn-lg btn-warning no-real-button"><strong><?php echo count(GrocyPhpHelper::FindAllObjectsInArrayByPropertyValue($currentStock, 'best_before_date', date('Y-m-d', strtotime('+5 days')), '<')); ?></strong> products expiring within the next 5 days</p>
<p class="btn btn-lg btn-danger no-real-button"><strong><?php echo count(GrocyPhpHelper::FindAllObjectsInArrayByPropertyValue($currentStock, 'best_before_date', date('Y-m-d', strtotime('-1 days')), '<')); ?></strong> products are already expired</p>
<p class="btn btn-lg btn-info no-real-button"><strong><?php echo count($missingProducts); ?></strong> products are below defined min. stock amount</p>
</div>
</div>
<div class="discrete-content-separator-2x"></div>
<div class="table-responsive">
<table id="stock-overview-table" class="table table-striped">
<thead>
<tr>
<th>Product</th>
<th>Amount</th>
<th>Next best before date</th>
</tr>
</thead>
<tbody>
<?php foreach ($currentStock as $currentStockEntry) : ?>
<tr class="<?php if ($currentStockEntry->best_before_date < date('Y-m-d', strtotime('-1 days'))) echo 'error-bg'; else if ($currentStockEntry->best_before_date < date('Y-m-d', strtotime('+5 days'))) echo 'warning-bg'; else if (GrocyPhpHelper::FindObjectInArrayByPropertyValue($missingProducts, 'id', $currentStockEntry->product_id) !== null) echo 'info-bg'; ?>">
<td>
<?php echo GrocyPhpHelper::FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name; ?>
</td>
<td>
<?php echo $currentStockEntry->amount . ' ' . GrocyPhpHelper::FindObjectInArrayByPropertyValue($quantityunits, 'id', GrocyPhpHelper::FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_stock)->name; ?>
</td>
<td>
<?php echo $currentStockEntry->best_before_date; ?>
<time class="timeago timeago-contextual" datetime="<?php echo $currentStockEntry->best_before_date; ?>"></time>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>