Compare commits

..

28 Commits

Author SHA1 Message Date
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
34 changed files with 892 additions and 135 deletions

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]

View File

@@ -6,17 +6,21 @@ class Grocy
/**
* @return PDO
*/
public static function GetDbConnectionRaw()
public static function GetDbConnectionRaw($doMigrations = false)
{
if ($doMigrations === true)
{
self::$DbConnectionRaw = null;
}
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)
if ($doMigrations === true)
{
$pdo->exec("CREATE TABLE migrations (migration INTEGER NOT NULL UNIQUE, execution_time_timestamp DATETIME DEFAULT (datetime('now', 'localtime')), PRIMARY KEY(migration)) WITHOUT ROWID");
Grocy::ExecuteDbStatement($pdo, "CREATE TABLE IF NOT EXISTS 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())
@@ -35,22 +39,59 @@ class Grocy
/**
* @return LessQL\Database
*/
public static function GetDbConnection()
public static function GetDbConnection($doMigrations = false)
{
if ($doMigrations === true)
{
self::$DbConnection = null;
}
if (self::$DbConnection == null)
{
self::$DbConnection = new LessQL\Database(self::GetDbConnectionRaw());
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)
@@ -60,4 +101,48 @@ class Grocy
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

@@ -71,18 +71,46 @@ 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 ('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'))
)"
);
}
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)
{
throw new Exception($pdo->errorInfo());
}
$pdo->exec('INSERT INTO migrations (migration) VALUES (' . $migrationId . ')');
Grocy::ExecuteDbStatement($pdo, $sql);
Grocy::ExecuteDbStatement($pdo, 'INSERT INTO migrations (migration) VALUES (' . $migrationId . ')');
}
}
}

View File

@@ -3,6 +3,9 @@
class GrocyDemoDataGenerator
{
public static function PopulateDemoData(PDO $pdo)
{
$rowCount = Grocy::ExecuteDbQuery($pdo, 'SELECT COUNT(*) FROM migrations WHERE migration = -1')->fetchColumn();
if (intval($rowCount) === 0)
{
$sql = "
UPDATE locations SET name = 'Vorratskammer', description = '' WHERE id = 1;
@@ -18,8 +21,8 @@ class GrocyDemoDataGenerator
INSERT INTO quantity_units (name) VALUES ('Bund'); --6
DELETE FROM products WHERE id IN (1, 2);
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('Gummib<69>rchen', 2, 2, 2, 1); --3
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('Chips', 2, 2, 2, 1); --4
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, 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
@@ -31,12 +34,11 @@ class GrocyDemoDataGenerator
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 migrations (migration) VALUES (-1);
";
if ($pdo->exec(utf8_encode($sql)) === false)
{
throw new Exception($pdo->errorInfo());
}
Grocy::ExecuteDbStatement($pdo, $sql);
GrocyLogicStock::AddProduct(3, 5, date('Y-m-d', strtotime('+180 days')), GrocyLogicStock::TRANSACTION_TYPE_PURCHASE);
GrocyLogicStock::AddProduct(4, 5, date('Y-m-d', strtotime('+180 days')), GrocyLogicStock::TRANSACTION_TYPE_PURCHASE);
@@ -51,5 +53,7 @@ class GrocyDemoDataGenerator
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();
}
}
}

View File

@@ -8,8 +8,14 @@ class GrocyLogicStock
public static function GetCurrentStock()
{
$db = Grocy::GetDbConnectionRaw();
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);
$sql = 'SELECT * from stock_current';
return Grocy::ExecuteDbQuery(Grocy::GetDbConnectionRaw(), $sql)->fetchAll(PDO::FETCH_OBJ);
}
public static function GetMissingProducts()
{
$sql = 'SELECT * from stock_missing_products';
return Grocy::ExecuteDbQuery(Grocy::GetDbConnectionRaw(), $sql)->fetchAll(PDO::FETCH_OBJ);
}
public static function GetProductDetails(int $productId)
@@ -18,7 +24,7 @@ class GrocyLogicStock
$product = $db->products($productId);
$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->stock_log()->where('product_id', $productId)->where('transaction_type', self::TRANSACTION_TYPE_CONSUME)->max('used_date');
$quPurchase = $db->quantity_units($product->qu_id_purchase);
$quStock = $db->quantity_units($product->qu_id_stock);
@@ -74,7 +80,7 @@ class GrocyLogicStock
$db = Grocy::GetDbConnection();
$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)
{
@@ -154,4 +160,32 @@ class GrocyLogicStock
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

@@ -46,4 +46,15 @@ class GrocyPhpHelper
return $returnArray;
}
public static function SumArrayValue($array, $propertyName)
{
$sum = 0;
foreach($array as $object)
{
$sum += $object->{$propertyName};
}
return $sum;
}
}

View File

@@ -13,5 +13,20 @@ Public demo of the latest version &rarr; [https://grocy.projectdemos.berrnd.org]
## How to install
Just unpack the [latest release](https://github.com/berrnd/grocy/releases/latest) on your PHP enabled webserver, copy `config-dist.php` to `data/config.php`, edit it to your needs, ensure that the `data` directory is writable and you're ready to go. Alternatively clone this repository and install Composer and Bower dependencies manually.
If you use nginx as your webserver, please include `try_files $uri /index.php;` in your location block.
## Notes about barcode readers
Some fields also allow to select a value by scanning a barcode. It works best when your barcode reader prefixes every barcode with a letter this is normally not part of a item name (I use a `$`) and sends a `TAB` after a scan.
## 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
The MIT License (MIT)

View File

@@ -7,5 +7,6 @@ mkdir "%releasePath%"
for /f "tokens=*" %%a in ('type version.txt') do set version=%%a
del "%releasePath%\grocy_%version%.zip"
"build_tools\7za.exe" a -r "%releasePath%\grocy_%version%.zip" "%projectPath%\*" -xr!.* -xr!build_tools -xr!build.bat -xr!composer.json -xr!composer.lock -xr!composer.phar -xr!grocy.phpproj -xr!grocy.phpproj.user -xr!grocy.sln
"build_tools\7za.exe" d "%releasePath%\grocy_%version%.zip" data\add_before_end_body.html data\demo.txt data\config.php data\grocy.db data\.gitignore bower.json
"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" 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": {
"slim/slim": "^3.8",
"slim/php-view": "^2.2",
"morris/lessql": "^0.3.4",
"tuupola/slim-basic-auth": "^2.2"
"morris/lessql": "^0.3.4"
}
}

View File

@@ -86,7 +86,8 @@ Grocy.GetUriParam = function(key)
var currentUri = decodeURIComponent(window.location.search.substring(1));
var vars = currentUri.split('&');
for (i = 0; i < vars.length; i++) {
for (i = 0; i < vars.length; i++)
{
var currentParam = vars[i].split('=');
if (currentParam[0] === key)
@@ -95,3 +96,8 @@ Grocy.GetUriParam = function(key)
}
}
};
Grocy.Wait = function(ms)
{
return new Promise(resolve => setTimeout(resolve, ms));
}

View File

@@ -8,6 +8,12 @@
</RootNamespace>
<ProjectTypeGuids>{A0786B88-2ADB-4C21-ABE8-AA2D79766269}</ProjectTypeGuids>
<AssemblyName>grocy</AssemblyName>
<SaveServerSettingsInUserFile>false</SaveServerSettingsInUserFile>
<Runtime>PHP</Runtime>
<RuntimeVersion>7.1</RuntimeVersion>
<EnvName>PHPDev</EnvName>
<PHPDevHostName>localhost</PHPDevHostName>
<PHPDevAutoPort>true</PHPDevAutoPort>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<IncludeDebugInformation>true</IncludeDebugInformation>
@@ -23,8 +29,11 @@
<Compile Include="GrocyPhpHelper.php" />
<Compile Include="GrocyDbMigrator.php" />
<Compile Include="index.php" />
<Compile Include="views\consumption.php" />
<Compile Include="views\consume.php" />
<Compile Include="views\login.php" />
<Compile Include="views\inventory.php" />
<Compile Include="views\shoppinglistform.php" />
<Compile Include="views\shoppinglist.php" />
<Compile Include="views\purchase.php" />
<Compile Include="views\quantityunitform.php" />
<Compile Include="views\locationform.php" />
@@ -39,6 +48,7 @@
<Content Include="bower.json" />
<None Include="build.bat" />
<Content Include="composer.json" />
<Content Include="grocy.png" />
<Content Include="grocy.js" />
<None Include="README.md" />
<Content Include="README.html">
@@ -48,9 +58,12 @@
<Content Include="robots.txt" />
<Content Include="style.css" />
<Content Include="version.txt" />
<Content Include="views\consumption.js" />
<Content Include="views\consume.js" />
<Content Include="views\dashboard.js" />
<Content Include="views\inventory.js" />
<Content Include="views\login.js" />
<Content Include="views\shoppinglistform.js" />
<Content Include="views\shoppinglist.js" />
<Content Include="views\purchase.js" />
<Content Include="views\quantityunitform.js" />
<Content Include="views\locationform.js" />

BIN
grocy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

119
index.php
View File

@@ -15,6 +15,7 @@ require_once __DIR__ . '/GrocyPhpHelper.php';
$app = new \Slim\App(new \Slim\Container([
'settings' => [
'displayErrorDetails' => true,
'determineRouteBeforeAppMiddleware' => true
],
]));
$container = $app->getContainer();
@@ -22,25 +23,76 @@ $container['renderer'] = new PhpRenderer('./views');
if (!Grocy::IsDemoInstallation())
{
$isHttpsReverseProxied = !empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https';
$app->add(new \Slim\Middleware\HttpBasicAuthentication([
'realm' => 'grocy',
'secure' => !$isHttpsReverseProxied,
'users' => [
HTTP_USER => HTTP_PASSWORD
]
]));
$sessionMiddleware = function(Request $request, Response $response, callable $next)
{
$route = $request->getAttribute('route');
$routeName = $route->getName();
if (!Grocy::IsValidSession($_COOKIE['grocy_session']) && $routeName !== 'login')
{
$response = $response->withRedirect('/login');
}
else
{
$response = $next($request, $response);
}
return $response;
};
$app->add($sessionMiddleware);
}
$db = Grocy::GetDbConnection();
$app->get('/login', function(Request $request, Response $response)
{
return $this->renderer->render($response, '/layout.php', [
'title' => 'Login',
'contentPage' => 'login.php'
]);
})->setName('login');
$app->post('/login', function(Request $request, Response $response)
{
$postParams = $request->getParsedBody();
if (isset($postParams['username']) && isset($postParams['password']))
{
if ($postParams['username'] === HTTP_USER && $postParams['password'] === HTTP_PASSWORD)
{
$sessionKey = Grocy::CreateSession();
setcookie('grocy_session', $sessionKey, time()+2592000); //30 days
return $response->withRedirect('/');
}
else
{
return $response->withRedirect('/login?invalid=true');
}
}
else
{
return $response->withRedirect('/login?invalid=true');
}
})->setName('login');
$app->get('/logout', function(Request $request, Response $response)
{
Grocy::RemoveSession($_COOKIE['grocy_session']);
return $response->withRedirect('/');
});
$app->get('/', function(Request $request, Response $response) use($db)
{
$db = Grocy::GetDbConnection(true); //For database schema migration
return $this->renderer->render($response, '/layout.php', [
'title' => 'Dashboard',
'contentPage' => 'dashboard.php',
'products' => $db->products(),
'currentStock' => GrocyLogicStock::GetCurrentStock()
'quantityunits' => $db->quantity_units(),
'currentStock' => GrocyLogicStock::GetCurrentStock(),
'missingProducts' => GrocyLogicStock::GetMissingProducts()
]);
});
@@ -53,11 +105,11 @@ $app->get('/purchase', function(Request $request, Response $response) use($db)
]);
});
$app->get('/consumption', function(Request $request, Response $response) use($db)
$app->get('/consume', function(Request $request, Response $response) use($db)
{
return $this->renderer->render($response, '/layout.php', [
'title' => 'Consumption',
'contentPage' => 'consumption.php',
'title' => 'Consume',
'contentPage' => 'consume.php',
'products' => $db->products()
]);
});
@@ -71,6 +123,18 @@ $app->get('/inventory', function(Request $request, Response $response) use($db)
]);
});
$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('/products', function(Request $request, Response $response) use($db)
{
return $this->renderer->render($response, '/layout.php', [
@@ -167,7 +231,30 @@ $app->get('/quantityunit/{quantityunitId}', function(Request $request, Response
}
});
$app->group('/api', function() use($db, $app)
$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)
{
@@ -257,6 +344,12 @@ $app->group('/api', function() use($db, $app)
{
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));
});
})->add(function($request, $response, $next)
{
$response = $next($request, $response);

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

@@ -118,3 +118,17 @@
.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 @@
1.0.1
1.4.0

View File

@@ -1,8 +1,8 @@
$('#save-consumption-button').on('click', function(e)
$('#save-consume-button').on('click', function(e)
{
e.preventDefault();
var jsonForm = $('#consumption-form').serializeJSON();
var jsonForm = $('#consume-form').serializeJSON();
var spoiled = 0;
if ($('#spoiled').is(':checked'))
@@ -23,7 +23,7 @@
$('#product_id_text_input').focus();
$('#product_id_text_input').val('');
$('#product_id_text_input').trigger('change');
$('#consumption-form').validator('validate');
$('#consume-form').validator('validate');
},
function(xhr)
{
@@ -56,7 +56,8 @@ $('#product_id').on('change', function(e)
$('#selected-product-last-used').text((productDetails.last_used || 'never').substring(0, 10));
$('#selected-product-last-used-timeago').text($.timeago(productDetails.last_used || ''));
$('#amount').attr('max', productDetails.stock_amount);
$('#consumption-form').validator('update');
$('#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-used-timeago', 'NaN years ago');
@@ -113,8 +114,8 @@ $(function()
$('#product_id_text_input').val('');
$('#product_id_text_input').trigger('change');
$('#consumption-form').validator();
$('#consumption-form').validator('validate');
$('#consume-form').validator();
$('#consume-form').validator('validate');
$('#amount').on('focus', function(e)
{
@@ -124,11 +125,11 @@ $(function()
}
});
$('#consumption-form input').keydown(function(event)
$('#consume-form input').keydown(function(event)
{
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();
return false;

View File

@@ -1,11 +1,12 @@
<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">
<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>
<?php foreach ($products as $product) : ?>
<option data-additional-searchdata="<?php echo $product->barcode; ?>" value="<?php echo $product->id; ?>"><?php echo $product->name; ?></option>
@@ -15,7 +16,7 @@
</div>
<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>
<div class="help-block with-errors"></div>
</div>
@@ -26,9 +27,10 @@
</label>
</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>
</div>
<div class="col-sm-6 col-md-5 col-lg-3 main well">

View File

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

View File

@@ -2,12 +2,17 @@
<h1 class="page-header">Dashboard</h1>
<h3>Stock overview</h3>
<h3>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></h3>
<div>
<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="current-stock-table" class="table table-striped">
@@ -20,12 +25,12 @@
</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'; ?>">
<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; ?>
<?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; ?>

View File

@@ -33,6 +33,7 @@
}
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)
{
@@ -133,6 +134,8 @@ $(function()
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',
@@ -140,22 +143,44 @@ $(function()
callback: function() { }
},
addnewproduct: {
label: 'Add as new product',
className: 'btn-success',
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 barcode to existing product',
className: 'btn-info',
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();
}
});
}
}
@@ -176,6 +201,13 @@ $(function()
{
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)
{
@@ -231,6 +263,7 @@ $(function()
{
$('#addbarcodetoselection').text(addBarcode);
$('#flow-info-addbarcodetoselection').removeClass('hide');
$('#barcode-lookup-disabled-hint').removeClass('hide');
}
});
@@ -242,6 +275,20 @@ $('#best_before_date-datepicker-button').on('click', function(e)
$('#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');
@@ -258,11 +305,13 @@ $('#best_before_date').on('keypress', function(e)
$('.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
{

View File

@@ -1,10 +1,11 @@
<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></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>
<option value=""></option>
<?php foreach ($products as $product) : ?>
@@ -36,6 +37,7 @@
<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">

View File

@@ -8,6 +8,7 @@
<meta name="robots" content="noindex,nofollow" />
<meta name="author" content="Bernd Bestel (bernd@berrnd.de)" />
<link rel="icon" href="/grocy.png" />
<title><?php echo $title; ?> | grocy</title>
@@ -37,6 +38,14 @@
<a class="navbar-brand" href="/">grocy</a>
</div>
<div id="navbar" class="navbar-collapse collapse">
<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 id="navbar-mobile" class="navbar-collapse collapse">
<ul class="nav navbar-nav navbar-right">
@@ -44,25 +53,35 @@
<a class="discrete-link" href="/"><i class="fa fa-tachometer fa-fw"></i>&nbsp;Dashboard</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>
<a class="discrete-link" href="/purchase"><i class="fa fa-shopping-cart fa-fw"></i>&nbsp;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 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>
</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">
<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 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 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>
</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>
@@ -80,25 +99,29 @@
<a class="discrete-link" href="/"><i class="fa fa-tachometer fa-fw"></i>&nbsp;Dashboard</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>
<a class="discrete-link" href="/purchase"><i class="fa fa-shopping-cart fa-fw"></i>&nbsp;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 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>
</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">
<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 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 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>
</ul>

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

@@ -75,6 +75,13 @@ $(function()
$('#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)

View File

@@ -35,6 +35,7 @@
}
toastr.success('Added ' + amount + ' ' + productDetails.quantity_unit_stock.name + ' of ' + productDetails.product.name + ' to stock');
Grocy.Wait(1000);
if (addBarcode !== undefined)
{
@@ -42,7 +43,7 @@
}
else
{
$('#amount').val(1);
$('#amount').val(0);
$('#best_before_date').val('');
$('#product_id').val('');
$('#product_id_text_input').focus();
@@ -81,11 +82,12 @@ $('#product_id').on('change', function(e)
$('#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_qu_unit').text(productDetails.quantity_unit_stock.name);
$('#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');
@@ -138,6 +140,8 @@ $(function()
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',
@@ -145,28 +149,50 @@ $(function()
callback: function() { }
},
addnewproduct: {
label: 'Add as new product',
className: 'btn-success',
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 barcode to existing product',
className: 'btn-info',
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(1);
$('#amount').val(0);
$('#best_before_date').val('');
$('#product_id').val('');
$('#product_id_text_input').focus();
@@ -181,6 +207,13 @@ $(function()
{
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.';
}
}
}
}
});
@@ -229,6 +262,7 @@ $(function()
{
$('#addbarcodetoselection').text(addBarcode);
$('#flow-info-addbarcodetoselection').removeClass('hide');
$('#barcode-lookup-disabled-hint').removeClass('hide');
}
});
@@ -240,6 +274,19 @@ $('#best_before_date-datepicker-button').on('click', function(e)
$('#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');
@@ -256,11 +303,13 @@ $('#best_before_date').on('keypress', function(e)
$('.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
{

View File

@@ -1,10 +1,11 @@
<div class="col-sm-4 col-sm-offset-3 col-md-3 col-md-offset-2 main">
<h1 class="page-header">Purchase</h1>
<form id="purchase-form">
<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>
<option value=""></option>
<?php foreach ($products as $product) : ?>
@@ -27,7 +28,7 @@
</div>
<div class="form-group">
<label for="amount">Amount&nbsp;&nbsp;<span id="new_amount_qu_unit" class="small text-muted"></span></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>
<div class="help-block with-errors"></div>
</div>
@@ -35,6 +36,7 @@
<button id="save-purchase-button" type="submit" class="btn btn-default">OK</button>
</form>
</div>
<div class="col-sm-6 col-md-5 col-lg-3 main well">

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>