Compare commits

...

13 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
34 changed files with 907 additions and 65 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

@@ -20,7 +20,7 @@ class Grocy
if ($doMigrations === true)
{
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");
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())
@@ -101,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

@@ -88,7 +88,7 @@ class GrocyDbMigrator
CREATE VIEW stock_current
AS
SELECT product_id, SUM(amount) AS amount, MIN(best_before_date) AS best_before_date
from stock
FROM stock
GROUP BY product_id
ORDER BY MIN(best_before_date) ASC;"
);
@@ -102,6 +102,35 @@ class GrocyDbMigrator
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)

View File

@@ -35,6 +35,9 @@ class GrocyDemoDataGenerator
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('Radieschen', 4, 6, 6, 1); --14
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('Tomate', 4, 1, 1, 1); --15
INSERT INTO habits (name, period_type, period_days) VALUES ('Changed towels in the bathroom', 'manually', 5); --1
INSERT INTO habits (name, period_type, period_days) VALUES ('Cleaned the kitchen floor', 'dynamic-regular', 7); --2
INSERT INTO migrations (migration) VALUES (-1);
";
@@ -54,6 +57,12 @@ class GrocyDemoDataGenerator
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

@@ -24,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);
@@ -80,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)
{

View File

@@ -57,4 +57,10 @@ class GrocyPhpHelper
return $sum;
}
public static function GetClassConstants($className)
{
$r = new ReflectionClass($className);
return $r->getConstants();
}
}

View File

@@ -13,8 +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

@@ -2,20 +2,21 @@
"name": "asp.net",
"private": true,
"dependencies": {
"bootstrap": "3.3.7",
"font-awesome": "4.7.0",
"bootbox": "4.4.0",
"jquery.serializeJSON": "2.7.2",
"bootstrap-validator": "0.11.9",
"bootstrap-datepicker": "1.6.4",
"moment": "2.18.1",
"bootstrap-combobox": "1.1.8",
"datatables.net": "1.10.13",
"datatables.net-bs": "2.1.1",
"datatables.net-responsive": "2.1.1",
"datatables.net-responsive-bs": "2.1.1",
"jquery-timeago": "1.5.4",
"toastr": "2.1.3",
"tagmanager": "3.0.2"
"bootstrap": "^3.3.7",
"font-awesome": "^4.7.0",
"bootbox": "^4.4.0",
"jquery.serializeJSON": "^2.8.1",
"bootstrap-validator": "^0.11.9",
"bootstrap-datepicker": "^1.7.1",
"moment": "^2.18.1",
"bootstrap-combobox": "^1.1.8",
"datatables.net": "^1.10.15",
"datatables.net-bs": "^2.1.1",
"datatables.net-responsive": "^2.1.1",
"datatables.net-responsive-bs": "^2.1.1",
"jquery-timeago": "^1.6.1",
"toastr": "^2.1.3",
"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
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 -xr!bower.json
"build_tools\7za.exe" d "%releasePath%\grocy_%version%.zip" data\*.*
"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

@@ -24,13 +24,18 @@
<ItemGroup>
<Compile Include="config-dist.php" />
<Compile Include="Grocy.php" />
<Compile Include="GrocyLogicHabits.php" />
<Compile Include="GrocyLogicStock.php" />
<Compile Include="GrocyDemoDataGenerator.php" />
<Compile Include="GrocyPhpHelper.php" />
<Compile Include="GrocyDbMigrator.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" />
@@ -40,7 +45,8 @@
<Compile Include="views\locations.php" />
<Compile Include="views\quantityunits.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" />
</ItemGroup>
<ItemGroup>
@@ -57,9 +63,14 @@
<Content Include="robots.txt" />
<Content Include="style.css" />
<Content Include="version.txt" />
<Content Include="views\consumption.js" />
<Content Include="views\dashboard.js" />
<Content Include="views\habittracking.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" />

148
index.php
View File

@@ -10,11 +10,13 @@ require_once __DIR__ . '/Grocy.php';
require_once __DIR__ . '/GrocyDbMigrator.php';
require_once __DIR__ . '/GrocyDemoDataGenerator.php';
require_once __DIR__ . '/GrocyLogicStock.php';
require_once __DIR__ . '/GrocyLogicHabits.php';
require_once __DIR__ . '/GrocyPhpHelper.php';
$app = new \Slim\App(new \Slim\Container([
'settings' => [
'displayErrorDetails' => true,
'determineRouteBeforeAppMiddleware' => true
],
]));
$container = $app->getContainer();
@@ -22,25 +24,77 @@ $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 $response->withRedirect('/stockoverview');
});
$app->get('/stockoverview', function(Request $request, Response $response) use($db)
{
return $this->renderer->render($response, '/layout.php', [
'title' => 'Dashboard',
'contentPage' => 'dashboard.php',
'title' => 'Stock overview',
'contentPage' => 'stockoverview.php',
'products' => $db->products(),
'quantityunits' => $db->quantity_units(),
'currentStock' => GrocyLogicStock::GetCurrentStock(),
@@ -48,6 +102,16 @@ $app->get('/', function(Request $request, Response $response) use($db)
]);
});
$app->get('/habitsoverview', function(Request $request, Response $response) use($db)
{
return $this->renderer->render($response, '/layout.php', [
'title' => 'Habits overview',
'contentPage' => 'habitsoverview.php',
'habits' => $db->habits(),
'currentHabits' => GrocyLogicHabits::GetCurrentHabits(),
]);
});
$app->get('/purchase', function(Request $request, Response $response) use($db)
{
return $this->renderer->render($response, '/layout.php', [
@@ -57,11 +121,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()
]);
});
@@ -87,6 +151,15 @@ $app->get('/shoppinglist', function(Request $request, Response $response) use($d
]);
});
$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', [
@@ -116,6 +189,16 @@ $app->get('/quantityunits', function(Request $request, Response $response) use($
]);
});
$app->get('/habits', function(Request $request, Response $response) use($db)
{
return $this->renderer->render($response, '/layout.php', [
'title' => 'Habits',
'contentPage' => 'habits.php',
'habits' => $db->habits()
]);
});
$app->get('/product/{productId}', function(Request $request, Response $response, $args) use($db)
{
if ($args['productId'] == 'new')
@@ -183,6 +266,29 @@ $app->get('/quantityunit/{quantityunitId}', function(Request $request, Response
}
});
$app->get('/habit/{habitId}', function(Request $request, Response $response, $args) use($db)
{
if ($args['habitId'] == 'new')
{
return $this->renderer->render($response, '/layout.php', [
'title' => 'Create habit',
'contentPage' => 'habitform.php',
'periodTypes' => GrocyPhpHelper::GetClassConstants('GrocyLogicHabits'),
'mode' => 'create'
]);
}
else
{
return $this->renderer->render($response, '/layout.php', [
'title' => 'Edit habit',
'contentPage' => 'habitform.php',
'habit' => $db->habits($args['habitId']),
'periodTypes' => GrocyPhpHelper::GetClassConstants('GrocyLogicHabits'),
'mode' => 'edit'
]);
}
});
$app->get('/shoppinglistitem/{itemId}', function(Request $request, Response $response, $args) use($db)
{
if ($args['itemId'] == 'new')
@@ -302,6 +408,22 @@ $app->group('/api', function() use($db)
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);

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

@@ -1 +1 @@
1.3.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();
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,7 @@ $('#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');
@@ -114,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)
{
@@ -125,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,8 +1,8 @@
<div class="col-sm-3 col-sm-offset-3 col-md-3 col-md-offset-2 main">
<h1 class="page-header">Consumption</h1>
<h1 class="page-header">Consume</h1>
<form id="consumption-form">
<form id="consume-form">
<div class="form-group">
<label for="product_id">Product&nbsp;&nbsp;<i class="fa fa-barcode"></i></label>
@@ -27,7 +27,7 @@
</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>

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>

View File

@@ -275,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');

View File

@@ -20,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/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/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" />
<script src="/bower_components/jquery/dist/jquery.min.js?v=<?php echo Grocy::GetInstalledVersion(); ?>"></script>
@@ -38,17 +39,31 @@
<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">
<li data-nav-for-page="dashboard.php">
<a class="discrete-link" href="/"><i class="fa fa-tachometer fa-fw"></i>&nbsp;Dashboard</a>
<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="consumption.php">
<a class="discrete-link" href="/consumption"><i class="fa fa-cutlery fa-fw"></i>&nbsp;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>
@@ -56,6 +71,9 @@
<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">
@@ -69,6 +87,15 @@
<li data-nav-for-page="quantityunits.php">
<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>
</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>
@@ -81,14 +108,20 @@
<div class="col-sm-3 col-md-2 sidebar">
<ul class="nav nav-sidebar">
<li data-nav-for-page="dashboard.php">
<a class="discrete-link" href="/"><i class="fa fa-tachometer fa-fw"></i>&nbsp;Dashboard</a>
<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 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="consumption.php">
<a class="discrete-link" href="/consumption"><i class="fa fa-cutlery fa-fw"></i>&nbsp;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>
@@ -96,6 +129,9 @@
<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">
@@ -109,6 +145,9 @@
<li data-nav-for-page="quantityunits.php">
<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>
</ul>
<div class="nav-copyright nav nav-sidebar">
@@ -148,6 +187,7 @@
<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/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))) : ?>
<script src="/views/<?php echo str_replace('.php', '.js', $contentPage) . '?v=' . Grocy::GetInstalledVersion(); ?>"></script>

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

@@ -274,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');

View File

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

View File

@@ -1,8 +1,6 @@
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
<h1 class="page-header">Dashboard</h1>
<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>
<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">
@@ -15,7 +13,7 @@
<div class="discrete-content-separator-2x"></div>
<div class="table-responsive">
<table id="current-stock-table" class="table table-striped">
<table id="stock-overview-table" class="table table-striped">
<thead>
<tr>
<th>Product</th>