Compare commits

...

21 Commits

Author SHA1 Message Date
Bernd Bestel
ecf96252b9 Add possibility to filter products by location (references #10) 2018-07-08 15:16:24 +02:00
Bernd Bestel
92e648490a Sort all dropdowns 2018-07-08 13:59:42 +02:00
Bernd Bestel
6dd3c26ddd Fix Bower (for now, need to switch to Yarn soon) 2018-07-08 13:51:29 +02:00
Bernd Bestel
02ea26b090 Disable pagination for data tables 2018-07-08 13:50:52 +02:00
Bernd Bestel
0954b5a741 Add option to not use URL rewriting 2018-06-15 20:50:40 +02:00
Bernd Bestel
02b6c3b721 Make it also possible to directly execute/track battery charge cycles and habits from overview pages 2018-05-13 09:38:22 +02:00
Bernd Bestel
6fa4e13ba2 Fix API version display 2018-05-13 09:00:14 +02:00
Bernd Bestel
9837f79f9c Make it also possible to consume the whole stock of a product from stock overview 2018-05-13 08:42:45 +02:00
Bernd Bestel
6e4cd22118 Make big buttons on overview pages responsive (references #9) 2018-05-12 16:38:21 +02:00
Bernd Bestel
ca00dd8e2d Improved table layout 2018-05-12 16:35:14 +02:00
Bernd Bestel
5455ec7bde Added missing translations 2018-05-12 16:30:10 +02:00
Bernd Bestel
2e7af1b050 Added possibility to consume products directly from stock overview 2018-05-12 16:15:28 +02:00
Bernd Bestel
89bae8d25e Changed how version information is stored and displayed 2018-05-12 15:49:21 +02:00
Bernd Bestel
5b5c272909 Correct and complete documentation 2018-05-12 15:36:23 +02:00
Bernd Bestel
3e394a3840 Also show due/overdue on bateries- and habitoverview 2018-05-12 15:30:13 +02:00
Bernd Bestel
ab8094e1c0 Don't expose username when not logged in 2018-05-12 14:56:51 +02:00
Bernd Bestel
bbb5f1c7c7 Rework general page layout and improve responsiveness (references #9) 2018-05-12 14:25:21 +02:00
Bernd Bestel
b607f188af Correct PHP dependency information (closes #8) 2018-05-11 09:51:30 +02:00
Bernd Bestel
9ab1a674fe Merge pull request #7 from d-Rickyy-b/patch-1
FIX: Add missing translation of "Add" button
2018-05-07 22:38:32 +02:00
Rico
2f0a1391b7 FIX: Add missing translation of "Add" button
The "Add" button was not translated in the 'Quantity units' form.
2018-05-07 22:30:17 +02:00
Bernd Bestel
a9a1358b08 Added a plugin system for looking up products against external services by barcode (references #6) 2018-04-22 19:50:24 +02:00
75 changed files with 1352 additions and 864 deletions

View File

@@ -1,3 +1,8 @@
{
"directory": "public/bower_components"
"directory": "public/bower_components",
"registry": {
"search": [
"https://registry.bower.io"
]
}
}

View File

@@ -11,12 +11,14 @@ A household needs to be managed. I did this so far (almost 10 years) with my fir
For now my main focus is on stock management, ERP your fridge!
## How to install
Just unpack the [latest release](https://github.com/berrnd/grocy/releases/latest) on your PHP (7.0 or later required) enabled webserver (webservers root should point to the `/public` directory), copy `config-dist.php` to `data/config.php`, edit it to your needs, ensure that the `data` directory is writable and you're ready to go.
Just unpack the [latest release](https://github.com/berrnd/grocy/releases/latest) on your PHP (currently only tested with PHP 7.2) enabled webserver (webservers root should point to the `/public` directory), copy `config-dist.php` to `data/config.php`, edit it to your needs, ensure that the `data` directory is writable and you're ready to go.
Default login is user `admin` with password `admin` - see the `data/config.php` file. Alternatively clone this repository and install Composer and Bower dependencies manually.
If you use nginx as your webserver, please include `try_files $uri /index.php;` in your location block.
If, however, your webserver does not support URL rewriting, set `DISABLE_URL_REWRITING` in `data/config.php`.
## How to update
Just overwrite everything with the latest release while keeping the `/data` directory, check `config-dist.php` for new configuration options and add them to your `data/config.php` (it will show up as an error if something is missing there).
@@ -35,7 +37,7 @@ The following shorthands are available:
- Example: `0517` will be converted to `2018-05-17`
- `YYYYMMDD` gets expanded to the proper ISO-8601 notation
- Example: `20190417` will be converted to `2019-04-17`
- `x` gets expanded to `2099-12-31` (which I use for products which never expire)
- `x` gets expanded to `2999-12-31` (which I use for products which never expire)
- Down/up arrow keys will increase/decrease the date by one day
- Right/left arrow keys will increase/decrease the date by 1 week
@@ -43,12 +45,20 @@ The following shorthands are available:
Wherever a button contains a bold highlighted letter, this is a shortcut key.
Example: Button "Add as new **p**roduct" can be "pressed" by using the `P` key on your keyboard.
### Barcode lookup via external services
Products can be directly added to the database via looking them up against external services by a barcode.
This is currently only possible through the REST API.
There is no plugin included for any service, see the reference implementation in `data/plugins/DemoBarcodeLookupPlugin.php`.
### Database migrations
Database schema migration is automatically done when visiting the root (`/`) route (click on the logo in the left upper edge).
### Demo mode
When the file `data/demo.txt` exists, the application will work in a demo mode which means authentication is disabled and some demo data will be generated during the database schema migration.
### Other things
When the file `data/add_before_end_body.html` exists, the contents of the file be added just before `</body>` on every page, useful for your own JS/CSS without to have to modify the application itself.
## Screenshots
#### Dashboard
![Dashboard](https://github.com/berrnd/grocy/raw/master/publication_assets/dashboard.png "Dashboard")

View File

@@ -19,6 +19,7 @@
"tagmanager": "^3.0.2",
"eonasdan-bootstrap-datetimepicker": "^4.17.47",
"swagger-ui": "^3.13.4",
"jquery-ui": "^1.12.1"
"jquery-ui": "^1.12.1",
"bootstrap-side-navbar": "^1.0.1"
}
}

View File

@@ -1,6 +1,6 @@
{
"require": {
"php": ">=7.0",
"php": ">=7.2",
"slim/slim": "^3.8",
"morris/lessql": "^0.3.4",
"pavlakis/slim-cli": "^1.0",

View File

@@ -15,3 +15,12 @@ define('CULTURE', 'en');
# should be just "/" when running directly under the root of a (sub)domain
# or for example "https:/example.com/grocy" when using a subdirectory
define('BASE_URL', '/');
# The plugin to use for external barcode lookups,
# must be the filename without .php extension and must be located in /data/plugins,
# see /data/plugins/DemoBarcodeLookupPlugin.php for an example implementation
define('STOCK_BARCODE_LOOKUP_PLUGIN', 'DemoBarcodeLookupPlugin');
# If, however, your webserver does not support URL rewriting,
# set this to true
define('DISABLE_URL_REWRITING', false);

View File

@@ -13,7 +13,9 @@ class BaseController
$this->Database = $databaseService->GetDbConnection();
$applicationService = new ApplicationService();
$container->view->set('version', $applicationService->GetInstalledVersion());
$versionInfo = $applicationService->GetInstalledVersion();
$container->view->set('version', $versionInfo->Version);
$container->view->set('releaseDate', $versionInfo->ReleaseDate);
$localizationService = new LocalizationService(CULTURE);
$container->view->set('localizationStrings', $localizationService->GetCurrentCultureLocalizations());
@@ -21,9 +23,9 @@ class BaseController
{
return $localizationService->Localize($text, ...$placeholderValues);
});
$container->view->set('U', function($relativePath) use($container)
$container->view->set('U', function($relativePath, $isResource = false) use($container)
{
return $container->UrlManager->ConstructUrl($relativePath);
return $container->UrlManager->ConstructUrl($relativePath, $isResource);
});
$this->AppContainer = $container;

View File

@@ -22,24 +22,30 @@ class BatteriesController extends BaseController
$nextChargeTimes[$battery->id] = $this->BatteriesService->GetNextChargeTime($battery->id);
}
$nextXDays = 5;
$countDueNextXDays = count(FindAllItemsInArrayByValue($nextChargeTimes, date('Y-m-d', strtotime("+$nextXDays days")), '<'));
$countOverdue = count(FindAllItemsInArrayByValue($nextChargeTimes, date('Y-m-d', strtotime('-1 days')), '<'));
return $this->AppContainer->view->render($response, 'batteriesoverview', [
'batteries' => $this->Database->batteries(),
'batteries' => $this->Database->batteries()->orderBy('name'),
'current' => $this->BatteriesService->GetCurrent(),
'nextChargeTimes' => $nextChargeTimes
'nextChargeTimes' => $nextChargeTimes,
'nextXDays' => $nextXDays,
'countDueNextXDays' => $countDueNextXDays - $countOverdue,
'countOverdue' => $countOverdue
]);
}
public function TrackChargeCycle(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
return $this->AppContainer->view->render($response, 'batterytracking', [
'batteries' => $this->Database->batteries()
'batteries' => $this->Database->batteries()->orderBy('name')
]);
}
public function BatteriesList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
return $this->AppContainer->view->render($response, 'batteries', [
'batteries' => $this->Database->batteries()
'batteries' => $this->Database->batteries()->orderBy('name')
]);
}

View File

@@ -22,24 +22,30 @@ class HabitsController extends BaseController
$nextHabitTimes[$habit->id] = $this->HabitsService->GetNextHabitTime($habit->id);
}
$nextXDays = 5;
$countDueNextXDays = count(FindAllItemsInArrayByValue($nextHabitTimes, date('Y-m-d', strtotime("+$nextXDays days")), '<'));
$countOverdue = count(FindAllItemsInArrayByValue($nextHabitTimes, date('Y-m-d', strtotime('-1 days')), '<'));
return $this->AppContainer->view->render($response, 'habitsoverview', [
'habits' => $this->Database->habits(),
'habits' => $this->Database->habits()->orderBy('name'),
'currentHabits' => $this->HabitsService->GetCurrentHabits(),
'nextHabitTimes' => $nextHabitTimes
'nextHabitTimes' => $nextHabitTimes,
'nextXDays' => $nextXDays,
'countDueNextXDays' => $countDueNextXDays - $countOverdue,
'countOverdue' => $countOverdue
]);
}
public function TrackHabitExecution(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
return $this->AppContainer->view->render($response, 'habittracking', [
'habits' => $this->Database->habits()
'habits' => $this->Database->habits()->orderBy('name')
]);
}
public function HabitsList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
return $this->AppContainer->view->render($response, 'habits', [
'habits' => $this->Database->habits()
'habits' => $this->Database->habits()->orderBy('name')
]);
}

View File

@@ -24,7 +24,8 @@ class OpenApiController extends BaseApiController
{
$applicationService = new ApplicationService();
$this->OpenApiSpec->info->version = $applicationService->GetInstalledVersion();
$versionInfo = $applicationService->GetInstalledVersion();
$this->OpenApiSpec->info->version = $versionInfo->Version;
$this->OpenApiSpec->info->description = str_replace('PlaceHolderManageApiKeysUrl', $this->AppContainer->UrlManager->ConstructUrl('/manageapikeys'), $this->OpenApiSpec->info->description);
$this->OpenApiSpec->servers[0]->url = $this->AppContainer->UrlManager->ConstructUrl('/api');

View File

@@ -105,4 +105,22 @@ class StockApiController extends BaseApiController
$this->StockService->AddMissingProductsToShoppingList();
return $this->VoidApiActionResponse($response);
}
public function ExternalBarcodeLookup(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
try
{
$addFoundProduct = false;
if (isset($request->getQueryParams()['add']) && ($request->getQueryParams()['add'] === 'true' || $request->getQueryParams()['add'] === 1))
{
$addFoundProduct = true;
}
return $this->ApiResponse($this->StockService->ExternalBarcodeLookup($args['barcode'], $addFoundProduct));
}
catch (\Exception $ex)
{
return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage());
}
}
}

View File

@@ -17,32 +17,40 @@ class StockController extends BaseController
public function Overview(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
$currentStock = $this->StockService->GetCurrentStock();
$nextXDays = 5;
$countExpiringNextXDays = count(FindAllObjectsInArrayByPropertyValue($currentStock, 'best_before_date', date('Y-m-d', strtotime('+5 days')), '<'));
$countAlreadyExpired = count(FindAllObjectsInArrayByPropertyValue($currentStock, 'best_before_date', date('Y-m-d', strtotime('-1 days')), '<'));
return $this->AppContainer->view->render($response, 'stockoverview', [
'products' => $this->Database->products(),
'quantityunits' => $this->Database->quantity_units(),
'currentStock' => $this->StockService->GetCurrentStock(),
'missingProducts' => $this->StockService->GetMissingProducts()
'products' => $this->Database->products()->orderBy('name'),
'quantityunits' => $this->Database->quantity_units()->orderBy('name'),
'locations' => $this->Database->locations()->orderBy('name'),
'currentStock' => $currentStock,
'missingProducts' => $this->StockService->GetMissingProducts(),
'nextXDays' => $nextXDays,
'countExpiringNextXDays' => $countExpiringNextXDays,
'countAlreadyExpired' => $countAlreadyExpired
]);
}
public function Purchase(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
return $this->AppContainer->view->render($response, 'purchase', [
'products' => $this->Database->products()
'products' => $this->Database->products()->orderBy('name')
]);
}
public function Consume(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
return $this->AppContainer->view->render($response, 'consume', [
'products' => $this->Database->products()
'products' => $this->Database->products()->orderBy('name')
]);
}
public function Inventory(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
return $this->AppContainer->view->render($response, 'inventory', [
'products' => $this->Database->products()
'products' => $this->Database->products()->orderBy('name')
]);
}
@@ -50,8 +58,8 @@ class StockController extends BaseController
{
return $this->AppContainer->view->render($response, 'shoppinglist', [
'listItems' => $this->Database->shopping_list(),
'products' => $this->Database->products(),
'quantityunits' => $this->Database->quantity_units(),
'products' => $this->Database->products()->orderBy('name'),
'quantityunits' => $this->Database->quantity_units()->orderBy('name'),
'missingProducts' => $this->StockService->GetMissingProducts()
]);
}
@@ -59,23 +67,23 @@ class StockController extends BaseController
public function ProductsList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
return $this->AppContainer->view->render($response, 'products', [
'products' => $this->Database->products(),
'locations' => $this->Database->locations(),
'quantityunits' => $this->Database->quantity_units()
'products' => $this->Database->products()->orderBy('name'),
'locations' => $this->Database->locations()->orderBy('name'),
'quantityunits' => $this->Database->quantity_units()->orderBy('name')
]);
}
public function LocationsList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
return $this->AppContainer->view->render($response, 'locations', [
'locations' => $this->Database->locations()
'locations' => $this->Database->locations()->orderBy('name')
]);
}
public function QuantityUnitsList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
return $this->AppContainer->view->render($response, 'quantityunits', [
'quantityunits' => $this->Database->quantity_units()
'quantityunits' => $this->Database->quantity_units()->orderBy('name')
]);
}
@@ -84,8 +92,8 @@ class StockController extends BaseController
if ($args['productId'] == 'new')
{
return $this->AppContainer->view->render($response, 'productform', [
'locations' => $this->Database->locations(),
'quantityunits' => $this->Database->quantity_units(),
'locations' => $this->Database->locations()->orderBy('name'),
'quantityunits' => $this->Database->quantity_units()->orderBy('name'),
'mode' => 'create'
]);
}
@@ -93,8 +101,8 @@ class StockController extends BaseController
{
return $this->AppContainer->view->render($response, 'productform', [
'product' => $this->Database->products($args['productId']),
'locations' => $this->Database->locations(),
'quantityunits' => $this->Database->quantity_units(),
'locations' => $this->Database->locations()->orderBy('name'),
'quantityunits' => $this->Database->quantity_units()->orderBy('name'),
'mode' => 'edit'
]);
}
@@ -139,7 +147,7 @@ class StockController extends BaseController
if ($args['itemId'] == 'new')
{
return $this->AppContainer->view->render($response, 'shoppinglistform', [
'products' => $this->Database->products(),
'products' => $this->Database->products()->orderBy('name'),
'mode' => 'create'
]);
}
@@ -147,7 +155,7 @@ class StockController extends BaseController
{
return $this->AppContainer->view->render($response, 'shoppinglistform', [
'listItem' => $this->Database->shopping_list($args['itemId']),
'products' => $this->Database->products(),
'products' => $this->Database->products()->orderBy('name'),
'mode' => 'edit'
]);
}

1
data/.gitignore vendored
View File

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

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

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

View File

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

View File

@@ -650,6 +650,57 @@
}
}
},
"/stock/external-barcode-lookup/{barcode}": {
"get": {
"description": "Executes an external barcode lookoup via the configured plugin with the given barcode",
"tags": [
"Stock"
],
"parameters": [
{
"in": "path",
"name": "barcode",
"required": true,
"description": "The barcode to lookup up",
"schema": {
"type": "string"
}
},
{
"in": "query",
"name": "add",
"required": false,
"description": "When true, the product is added to the database on a successful lookup and the new product id is in included in output",
"schema": {
"type": "boolean",
"default": false
}
}
],
"responses": {
"200": {
"description": "An ExternalBarcodeLookupResponse object or null, when nothing was found for the given barcode",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ExternalBarcodeLookupResponse"
}
}
}
},
"400": {
"description": "A VoidApiActionResponse object (possible errors are: Plugin error)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorExampleVoidApiActionResponse"
}
}
}
}
}
}
},
"/habits/track-habit-execution/{habitId}": {
"get": {
"description": "Tracks an execution of the given habit",
@@ -992,6 +1043,35 @@
}
}
},
"ExternalBarcodeLookupResponse": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"location_id": {
"type": "integer"
},
"qu_id_purchase": {
"type": "integer"
},
"qu_id_stock": {
"type": "integer"
},
"qu_factor_purchase_to_stock": {
"type": "number",
"format": "double"
},
"barcode": {
"type": "string",
"description": "Can contain multiple barcodes separated by comma"
},
"id": {
"type": "integer",
"description": "The id of the added product, only included when the producted was added to the database"
}
}
},
"HabitDetailsResponse": {
"type": "object",
"properties": {

View File

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

View File

@@ -18,9 +18,16 @@ class UrlManager
protected $BasePath;
public function ConstructUrl($relativePath)
public function ConstructUrl($relativePath, $isResource = false)
{
return rtrim($this->BasePath, '/') . $relativePath;
if (DISABLE_URL_REWRITING === false || $isResource === true)
{
return rtrim($this->BasePath, '/') . $relativePath;
}
else // Is not a resource and URL rewriting is disabled
{
return rtrim($this->BasePath, '/') . '/index.php' . $relativePath;
}
}
private function GetBaseUrl()

View File

@@ -45,6 +45,38 @@ function FindAllObjectsInArrayByPropertyValue($array, $propertyName, $propertyVa
return $returnArray;
}
function FindAllItemsInArrayByValue($array, $value, $operator = '==')
{
$returnArray = array();
foreach($array as $item)
{
switch($operator)
{
case '==':
if($item == $value)
{
$returnArray[] = $item;
}
break;
case '>':
if($item > $value)
{
$returnArray[] = $item;
}
break;
case '<':
if($item < $value)
{
$returnArray[] = $item;
}
break;
}
}
return $returnArray;
}
function SumArrayValue($array, $propertyName)
{
$sum = 0;
@@ -72,3 +104,9 @@ function RandomString($length, $allowedChars = '0123456789abcdefghijklmnopqrstuv
return $randomString;
}
function IsAssociativeArray(array $array)
{
$keys = array_keys($array);
return array_keys($keys) !== $keys;
}

View File

@@ -112,6 +112,24 @@ return array(
'This means #1 will be added to stock' => 'Das bedeutet #1 wird dem Bestand hinzugefügt',
'This means #1 will be removed from stock' => 'Das bedeutet #1 wird aus dem Bestand entfernt',
'This means it is estimated that a new execution of this habit is tracked #1 days after the last was tracked' => 'Das bedeutet, dass eine erneute Ausführung der Gewohnheit #1 Tage nach der letzten Ausführung geplant wird',
'Removed #1 #2 of #3 from stock' => '#1 #2 #3 aus dem Bestand entfernt',
'About grocy' => 'Über grocy',
'Close' => 'Schließen',
'#1 batteries are due to be charged within the next #2 days' => '#1 Batterien müssen in den nächsten #2 Tagen geladen werden',
'#1 batteries are overdue to be charged' => '#1 Batterien sind überfällig',
'#1 habits are due to be done within the next #2 days' => '#1 Gewohnheiten stehen in den nächsten #2 Tagen an',
'#1 habits are overdue to be done' => '#1 Gewohnheiten sind überfällig',
'Released on' => 'Veröffentlicht am',
'Consume #3 #1 of #2' => 'Verbrauche #3 #1 #2',
'Added #1 #2 of #3 to stock' => '#1 #2 #3 dem Bestand hinzugefügt',
'Stock amount of #1 is now #2 #3' => 'Es sind nun #2 #3 #1 im Bestand',
'Tracked execution of habit #1 on #2' => 'Ausführung von #1 am #2 erfasst',
'Tracked charge cylce of battery #1 on #2' => 'Ladezyklus für Batterie #1 am #2 erfasst',
'Consume all #1 which are currently in stock' => 'Verbrauche den kompletten Bestand von #1',
'All' => 'Alle',
'Track charge cycle of battery #1' => 'Erfasse einen Ladezyklus für Batterie #1',
'Track execution of habit #1' => 'Erfasse eine Ausführung von #1',
'Filter by location' => 'Nach Standort filtern',
//Constants
'manually' => 'Manuell',

View File

@@ -21,6 +21,7 @@ class SessionAuthMiddleware extends BaseMiddleware
if ($routeName === 'root' || $this->ApplicationService->IsDemoInstallation())
{
define('AUTHENTICATED', $this->ApplicationService->IsDemoInstallation());
$response = $next($request, $response);
}
else
@@ -28,10 +29,12 @@ class SessionAuthMiddleware extends BaseMiddleware
$sessionService = new SessionService();
if ((!isset($_COOKIE[$this->SessionCookieName]) || !$sessionService->IsValidSession($_COOKIE[$this->SessionCookieName])) && $routeName !== 'login')
{
define('AUTHENTICATED', false);
$response = $response->withRedirect($this->AppContainer->UrlManager->ConstructUrl('/login'));
}
else
{
define('AUTHENTICATED', $routeName !== 'login');
$response = $next($request, $response);
}
}

View File

@@ -4,82 +4,78 @@
}
.navbar-fixed-top {
border: 0;
background-color: #e5e5e5;
border-bottom: 2px solid;
border-color: #d6d6d6;
}
.sidebar {
display: none;
.navbar-brand {
font-weight: bold;
letter-spacing: -5px;
font-size: 2.2em;
color: #0b024c !important;
margin-left: 0 !important;
padding-left: 5px !important;
}
.navbar-fixed-side {
top: 51px;
padding-top: 20px;
margin-bottom: 20px;
background-color: #e5e5e5;
border-right: 2px solid #d6d6d6;
max-width: 260px;
border-left: 0;
}
#sidebar {
overflow-y: auto;
}
@media (min-width: 768px) {
.sidebar {
position: fixed;
top: 51px;
bottom: 0;
left: 0;
z-index: 1000;
display: block;
padding: 20px;
overflow-x: hidden;
overflow-y: auto;
background-color: #e5e5e5;
border-right: 2px solid #d6d6d6;
min-width: 220px;
max-width: 260px;
}
#navbar-mobile {
display: none !important;
}
.nav-copyright {
padding-bottom: 100px;
}
}
.nav-sidebar {
margin-right: -21px;
margin-bottom: 20px;
margin-left: -20px;
@media (max-width: 768px) {
.navbar-brand {
margin-left: 25px !important;
}
}
.nav-sidebar > li > a {
.sidebar-nav > li > a {
padding-right: 20px;
padding-left: 20px;
transition: all 0.3s;
}
.nav-sidebar > li > a:hover {
.sidebar-nav > li > a:hover {
box-shadow: inset 5px 0 0 #337ab7;
transition: all 0.3s;
}
.nav-sidebar > li > a:focus {
.sidebar-nav > li > a:focus {
box-shadow: inset 5px 0 0 #ab2230;
transition: all 0.3s;
}
.nav-sidebar > .active > a,
.nav-sidebar > .active > a:hover,
.nav-sidebar > .active > a:focus {
.sidebar-nav > .active > a,
.sidebar-nav > .active > a:hover,
.sidebar-nav > .active > a:focus {
background-color: #d6d6d6;
box-shadow: inset 5px 0 0 #ab2230;
transition: all 0.3s;
}
.navbar-default {
background-color: #e5e5e5;
}
.main {
padding: 20px;
}
@media (min-width: 768px) {
.main {
padding-right: 40px;
padding-left: 40px;
}
}
.main .page-header {
margin-top: 0;
.nav > li.disabled > a,
.navbar-default .navbar-nav > .disabled > a {
color: #a7a7a7;
}
.nav-copyright {
@@ -88,6 +84,34 @@
text-align: center;
}
.nav-copyright > li > a {
padding-top: 0 !important;
padding-bottom: 0 !important;
}
.navbar-default .navbar-nav > .open > a {
background-color: #d6d6d6 !important;
}
.dropdown-menu > li > a:hover,
.dropdown-menu > li > a:focus {
background-color: #e5e5e5 !important;
}
.well {
background-color: #e5e5e5;
padding-right: 25px;
padding-left: 25px;
}
td {
vertical-align: middle !important;
}
.responsive-button {
white-space: normal;
}
.discrete-link {
color: inherit !important;
transition: all 0.3s !important;
@@ -105,21 +129,6 @@ a.discrete-link:focus {
transition: all 0.3s !important;
}
.navbar-fixed-top {
border-bottom: 2px solid;
border-color: #d6d6d6;
}
.navbar-brand {
font-weight: bold;
letter-spacing: -5px;
font-size: 2.2em;
color: #0b024c !important;
margin-left: 0 !important;
padding-left: 5px !important;
}
.table td.fit-content,
.table th.fit-content {
white-space: nowrap;
@@ -144,6 +153,11 @@ a.discrete-link:focus {
margin-bottom: 2px;
}
.discrete-content-separator-2x {
padding-top: 10px;
padding-bottom: 10px;
}
.warning-bg {
background-color: #fcf8e3 !important;
}
@@ -156,44 +170,15 @@ a.discrete-link:focus {
background-color: #afd9ee !important;
}
.discrete-content-separator {
padding-top: 5px;
padding-bottom: 5px;
}
.discrete-content-separator-2x {
padding-top: 10px;
padding-bottom: 10px;
}
.well {
background-color: #e5e5e5;
}
.nav > li.disabled > a,
.navbar-default .navbar-nav > .disabled > a
{
color: #a7a7a7;
}
#toast-container > div {
opacity: 1;
filter: alpha(opacity=100);
}
.toast-success {
.toast-success {
background-color: #4c994c;
}
#toast-container > div {
box-shadow: none;
}
.navbar-default .navbar-nav > .open > a {
background-color: #d6d6d6 !important;
}
.dropdown-menu > li > a:hover,
.dropdown-menu > li > a:focus {
background-color: #e5e5e5 !important;
}

View File

@@ -26,7 +26,11 @@ if (!Grocy.ActiveNav.isEmpty())
}
$.timeago.settings.allowFuture = true;
$('time.timeago').timeago();
RefreshContextualTimeago = function()
{
$('time.timeago').timeago();
}
RefreshContextualTimeago();
toastr.options = {
toastClass: 'alert',

View File

@@ -35,7 +35,7 @@
});
$('#batteries-table').DataTable({
'pageLength': 50,
'bPaginate': false,
'order': [[1, 'asc']],
'columnDefs': [
{ 'orderable': false, 'targets': 0 }

View File

@@ -1,5 +1,33 @@
$('#batteries-overview-table').DataTable({
'pageLength': 50,
'order': [[1, 'desc']],
'bPaginate': false,
'order': [[2, 'desc']],
'columnDefs': [
{ 'orderable': false, 'targets': 0 }
],
'language': JSON.parse(L('datatables_localization'))
});
$(document).on('click', '.track-charge-cycle-button', function(e)
{
var batteryId = $(e.currentTarget).attr('data-battery-id');
var batteryName = $(e.currentTarget).attr('data-battery-name');
var trackedTime = moment().format('YYYY-MM-DD HH:mm:ss');
Grocy.Api.Get('batteries/track-charge-cycle/' + batteryId + '?tracked_time=' + trackedTime,
function(result)
{
$('#battery-' + batteryId + '-last-tracked-time').parent().effect('highlight', {}, 500);
$('#battery-' + batteryId + '-last-tracked-time').fadeOut(500, function () {
$(this).text(trackedTime).fadeIn(500);
});
$('#battery-' + batteryId + '-last-tracked-time-timeago').attr('datetime', trackedTime);
RefreshContextualTimeago();
toastr.success(L('Tracked charge cylce of battery #1 on #2', batteryName, trackedTime));
},
function(xhr)
{
console.error(xhr);
}
);
});

View File

@@ -10,7 +10,7 @@
Grocy.Api.Get('batteries/track-charge-cycle/' + jsonForm.battery_id + '?tracked_time=' + $('#tracked_time').val(),
function(result)
{
toastr.success('Tracked charge cylce of battery ' + batteryDetails.battery.name + ' on ' + $('#tracked_time').val());
toastr.success(L('Tracked charge cylce of battery #1 on #2', batteryDetails.battery.name, $('#tracked_time').val()));
$('#battery_id').val('');
$('#battery_id_text_input').focus();

View File

@@ -16,7 +16,7 @@
Grocy.Api.Get('stock/consume-product/' + jsonForm.product_id + '/' + jsonForm.amount + '?spoiled=' + spoiled,
function(result)
{
toastr.success('Removed ' + jsonForm.amount + ' ' + productDetails.quantity_unit_stock.name + ' of ' + productDetails.product.name + ' from stock');
toastr.success(L('Removed #1 #2 of #3 from stock', jsonForm.amount, productDetails.quantity_unit_stock.name, productDetails.product.name));
$('#amount').val(1);
$('#product_id').val('');

View File

@@ -35,7 +35,7 @@
});
$('#habits-table').DataTable({
'pageLength': 50,
'bPaginate': false,
'order': [[1, 'asc']],
'columnDefs': [
{ 'orderable': false, 'targets': 0 }

View File

@@ -1,5 +1,33 @@
$('#habits-overview-table').DataTable({
'pageLength': 50,
'order': [[1, 'desc']],
'bPaginate': false,
'order': [[2, 'desc']],
'columnDefs': [
{ 'orderable': false, 'targets': 0 }
],
'language': JSON.parse(L('datatables_localization'))
});
$(document).on('click', '.track-habit-button', function(e)
{
var habitId = $(e.currentTarget).attr('data-habit-id');
var habitName = $(e.currentTarget).attr('data-habit-name');
var trackedTime = moment().format('YYYY-MM-DD HH:mm:ss');
Grocy.Api.Get('habits/track-habit-execution/' + habitId + '?tracked_time=' + trackedTime,
function(result)
{
$('#habit-' + habitId + '-last-tracked-time').parent().effect('highlight', {}, 500);
$('#habit-' + habitId + '-last-tracked-time').fadeOut(500, function () {
$(this).text(trackedTime).fadeIn(500);
});
$('#habit-' + habitId + '-last-tracked-time-timeago').attr('datetime', trackedTime);
RefreshContextualTimeago();
toastr.success(L('Tracked execution of habit #1 on #2', habitName, trackedTime));
},
function(xhr)
{
console.error(xhr);
}
);
});

View File

@@ -10,7 +10,7 @@
Grocy.Api.Get('habits/track-habit-execution/' + jsonForm.habit_id + '?tracked_time=' + $('#tracked_time').val(),
function(result)
{
toastr.success('Tracked execution of habit ' + habitDetails.habit.name + ' on ' + $('#tracked_time').val());
toastr.success(L('Tracked execution of habit #1 on #2', habitDetails.habit.name, $('#tracked_time').val()));
$('#habit_id').val('');
$('#habit_id_text_input').focus();

View File

@@ -32,7 +32,7 @@
);
}
toastr.success('Stock amount of ' + productDetails.product.name + ' is now ' + jsonForm.new_amount.toString() + ' ' + productDetails.quantity_unit_stock.name);
toastr.success(L('Stock amount of #1 is now #2 #3', productDetails.product.name, jsonForm.new_amount, productDetails.quantity_unit_stock.name));
if (addBarcode !== undefined)
{

View File

@@ -35,7 +35,7 @@
});
$('#locations-table').DataTable({
'pageLength': 50,
'bPaginate': false,
'order': [[1, 'asc']],
'columnDefs': [
{ 'orderable': false, 'targets': 0 }

View File

@@ -35,7 +35,7 @@
});
$('#apikeys-table').DataTable({
'pageLength': 50,
'bPaginate': false,
'order': [[4, 'desc']],
'columnDefs': [
{ 'orderable': false, 'targets': 0 }

View File

@@ -35,7 +35,7 @@
});
$('#products-table').DataTable({
'pageLength': 50,
'bPaginate': false,
'order': [[1, 'asc']],
'columnDefs': [
{ 'orderable': false, 'targets': 0 }

View File

@@ -34,7 +34,7 @@
);
}
toastr.success('Added ' + amount + ' ' + productDetails.quantity_unit_stock.name + ' of ' + productDetails.product.name + ' to stock');
toastr.success(L('Added #1 #2 of #3 to stock', amount, productDetails.quantity_unit_stock.name, productDetails.product.name));
if (addBarcode !== undefined)
{

View File

@@ -35,7 +35,7 @@
});
$('#quantityunits-table').DataTable({
'pageLength': 50,
'bPaginate': false,
'order': [[1, 'asc']],
'columnDefs': [
{ 'orderable': false, 'targets': 0 }

View File

@@ -27,7 +27,7 @@ $(document).on('click', '#add-products-below-min-stock-amount', function(e)
});
$('#shoppinglist-table').DataTable({
'pageLength': 50,
'bPaginate': false,
'order': [[1, 'asc']],
'columnDefs': [
{ 'orderable': false, 'targets': 0 }

View File

@@ -1,5 +1,82 @@
$('#stock-overview-table').DataTable({
'pageLength': 50,
'order': [[2, 'asc']],
'language': JSON.parse(L('datatables_localization'))
var stockOverviewTable = $('#stock-overview-table').DataTable({
'bPaginate': false,
'order': [[3, 'asc']],
'columnDefs': [
{ 'orderable': false, 'targets': 0 },
{ 'visible': false, 'targets': 4 }
],
'language': JSON.parse(L('datatables_localization')),
"dom": '<"filter-by-location">f'
});
$("div.filter-by-location").html('<div class="dataTables_filter"><label>' + L('Filter by location') + ':<select id="location-filter" class="form-control input-sm" style="margin-left: 0.5em;"></label></div>');
$('#stock-overview-table_wrapper').on("DOMSubtreeModified", function()
{
$('#stock-overview-table_wrapper').off("DOMSubtreeModified");
Grocy.Api.Get('get-objects/locations',
function(locations)
{
$('#location-filter').append($('<option></option>').val("all").html(L("All")));
$.each(locations, function(index)
{
var locationName = locations[index].name;
$('#location-filter').append($('<option></option>').val(locationName).html(locationName));
});
},
function(xhr)
{
console.error(xhr);
}
);
$("#location-filter").on("change", function()
{
var value = $(this).val();
if (value === "all")
{
value = "";
}
stockOverviewTable.column(4).search(value).draw();
});
});
$(document).on('click', '.product-consume-button', function(e)
{
var productId = $(e.currentTarget).attr('data-product-id');
var productName = $(e.currentTarget).attr('data-product-name');
var productQuName = $(e.currentTarget).attr('data-product-qu-name');
var consumeAmount = $(e.currentTarget).attr('data-consume-amount');
Grocy.Api.Get('stock/consume-product/' + productId + '/' + consumeAmount,
function(result)
{
var oldAmount = parseInt($('#product-' + productId + '-amount').text());
var newAmount = oldAmount - consumeAmount;
if (newAmount === 0)
{
$('#product-' + productId + '-row').fadeOut(500, function()
{
$(this).remove();
});
}
else
{
$('#product-' + productId + '-amount').parent().effect('highlight', { }, 500);
$('#product-' + productId + '-amount').fadeOut(500, function()
{
$(this).text(newAmount).fadeIn(500);
});
$('#product-' + productId + '-consume-all-button').attr('data-consume-amount', newAmount);
}
toastr.success(L('Removed #1 #2 of #3 from stock', consumeAmount, productQuName, productName));
},
function(xhr)
{
console.error(xhr);
}
);
});

View File

@@ -70,6 +70,7 @@ $app->group('/api', function()
$this->get('/stock/get-product-details/{productId}', 'Grocy\Controllers\StockApiController:ProductDetails');
$this->get('/stock/get-current-stock', 'Grocy\Controllers\StockApiController:CurrentStock');
$this->get('/stock/add-missing-products-to-shoppinglist', 'Grocy\Controllers\StockApiController:AddMissingProductsToShoppingList');
$this->get('/stock/external-barcode-lookup/{barcode}', 'Grocy\Controllers\StockApiController:ExternalBarcodeLookup');
$this->get('/habits/track-habit-execution/{habitId}', 'Grocy\Controllers\HabitsApiController:TrackHabitExecution');
$this->get('/habits/get-habit-details/{habitId}', 'Grocy\Controllers\HabitsApiController:HabitDetails');

View File

@@ -13,14 +13,11 @@ class ApplicationService extends BaseService
}
private $InstalledVersion;
/**
* @return string
*/
public function GetInstalledVersion()
{
if ($this->InstalledVersion == null)
{
$this->InstalledVersion = preg_replace("/\r|\n/", '', file_get_contents(__DIR__ . '/../version.txt'));
$this->InstalledVersion = json_decode(file_get_contents(__DIR__ . '/../version.json'));
}
return $this->InstalledVersion;

View File

@@ -26,7 +26,7 @@ class BatteriesService extends BaseService
}
else
{
return date('Y-m-d H:i:s');
return date('2999-12-31 23:59:59');
}
return null;

View File

@@ -26,7 +26,7 @@ class HabitsService extends BaseService
switch($habit->period_type)
{
case self::HABIT_TYPE_MANUALLY:
return date('Y-m-d H:i:s');
return date('2999-12-31 23:59:59');
case self::HABIT_TYPE_DYNAMIC_REGULAR:
return date('Y-m-d H:i:s', strtotime('+' . $habit->period_days . ' day', strtotime($habitLastLogRow->last_tracked_time)));
}

View File

@@ -208,4 +208,44 @@ class StockService extends BaseService
$productRow = $this->Database->products()->where('id = :1', $productId)->fetch();
return $productRow !== null;
}
private function LoadBarcodeLookupPlugin()
{
$pluginName = defined('STOCK_BARCODE_LOOKUP_PLUGIN') ? STOCK_BARCODE_LOOKUP_PLUGIN : '';
if (empty($pluginName))
{
throw new \Exception('No barcode lookup plugin defined');
}
$path = __DIR__ . "/../data/plugins/$pluginName.php";
if (file_exists($path))
{
require_once $path;
return new $pluginName($this->Database->locations()->fetchAll(), $this->Database->quantity_units()->fetchAll());
}
else
{
throw new \Exception("Plugin $pluginName was not found");
}
}
public function ExternalBarcodeLookup($barcode, $addFoundProduct)
{
$plugin = $this->LoadBarcodeLookupPlugin();
$pluginOutput = $plugin->Lookup($barcode);
if ($pluginOutput !== null) // Lookup was successful
{
if ($addFoundProduct === true)
{
// Add product to database and include new product id in output
$newRow = $this->Database->products()->createRow($pluginOutput);
$newRow->save();
$pluginOutput['id'] = $newRow->id;
}
}
return $pluginOutput;
}
}

4
version.json Normal file
View File

@@ -0,0 +1,4 @@
{
"Version": "1.12.0",
"ReleaseDate": "2018-07-08"
}

View File

@@ -1 +0,0 @@
1.9.1

View File

@@ -5,50 +5,46 @@
@section('viewJsName', 'batteries')
@section('content')
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2">
<h1 class="page-header">
@yield('title')
<a class="btn btn-default" href="{{ $U('/battery/new') }}" role="button">
<i class="fa fa-plus"></i>&nbsp;{{ $L('Add') }}
</a>
</h1>
<div class="table-responsive">
<table id="batteries-table" class="table table-striped">
<thead>
<tr>
<th>#</th>
<th>{{ $L('Name') }}</th>
<th>{{ $L('Description') }}</th>
<th>{{ $L('Used in') }}</th>
</tr>
</thead>
<tbody>
@foreach($batteries as $battery)
<tr>
<td class="fit-content">
<a class="btn btn-info" href="{{ $U('/battery/') }}{{ $battery->id }}" role="button">
<i class="fa fa-pencil"></i>
</a>
<a class="btn btn-danger battery-delete-button" href="#" role="button" data-battery-id="{{ $battery->id }}" data-battery-name="{{ $battery->name }}">
<i class="fa fa-trash"></i>
</a>
</td>
<td>
{{ $battery->name }}
</td>
<td>
{{ $battery->description }}
</td>
<td>
{{ $battery->used_in }}
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
<h1 class="page-header">
@yield('title')
<a class="btn btn-default" href="{{ $U('/battery/new') }}" role="button">
<i class="fa fa-plus"></i>&nbsp;{{ $L('Add') }}
</a>
</h1>
<div class="table-responsive">
<table id="batteries-table" class="table table-striped">
<thead>
<tr>
<th>#</th>
<th>{{ $L('Name') }}</th>
<th>{{ $L('Description') }}</th>
<th>{{ $L('Used in') }}</th>
</tr>
</thead>
<tbody>
@foreach($batteries as $battery)
<tr>
<td class="fit-content">
<a class="btn btn-info" href="{{ $U('/battery/') }}{{ $battery->id }}" role="button">
<i class="fa fa-pencil"></i>
</a>
<a class="btn btn-danger battery-delete-button" href="#" role="button" data-battery-id="{{ $battery->id }}" data-battery-name="{{ $battery->name }}">
<i class="fa fa-trash"></i>
</a>
</td>
<td>
{{ $battery->name }}
</td>
<td>
{{ $battery->description }}
</td>
<td>
{{ $battery->used_in }}
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@stop

View File

@@ -4,43 +4,60 @@
@section('activeNav', 'batteriesoverview')
@section('viewJsName', 'batteriesoverview')
@push('pageScripts')
<script src="{{ $U('/bower_components/jquery-ui/jquery-ui.min.js?v=', true) }}{{ $version }}"></script>
@endpush
@section('content')
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2">
<h1 class="page-header">@yield('title')</h1>
<h1 class="page-header">@yield('title')</h1>
<div class="table-responsive">
<table id="batteries-overview-table" class="table table-striped">
<thead>
<tr>
<th>{{ $L('Battery') }}</th>
<th>{{ $L('Last charged') }}</th>
<th>{{ $L('Next planned charge cycle') }}</th>
</tr>
</thead>
<tbody>
@foreach($current as $curentBatteryEntry)
<tr class="@if(FindObjectInArrayByPropertyValue($batteries, 'id', $curentBatteryEntry->battery_id)->charge_interval_days > 0 && $nextChargeTimes[$curentBatteryEntry->battery_id] < date('Y-m-d H:i:s')) error-bg @endif">
<td>
{{ FindObjectInArrayByPropertyValue($batteries, 'id', $curentBatteryEntry->battery_id)->name }}
</td>
<td>
{{ $curentBatteryEntry->last_tracked_time }}
<time class="timeago timeago-contextual" datetime="{{ $curentBatteryEntry->last_tracked_time }}"></time>
</td>
<td>
@if(FindObjectInArrayByPropertyValue($batteries, 'id', $curentBatteryEntry->battery_id)->charge_interval_days > 0)
{{ $nextChargeTimes[$curentBatteryEntry->battery_id] }}
<time class="timeago timeago-contextual" datetime="{{ $nextChargeTimes[$curentBatteryEntry->battery_id] }}"></time>
@else
...
@endif
</td>
</tr>
@endforeach
</tbody>
</table>
<div class="container-fluid">
<div class="row">
<p class="btn btn-lg btn-warning no-real-button responsive-button">{{ $L('#1 batteries are due to be charged within the next #2 days', $countDueNextXDays, $nextXDays) }}</p>
<p class="btn btn-lg btn-danger no-real-button responsive-button">{{ $L('#1 batteries are overdue to be charged', $countOverdue) }}</p>
</div>
</div>
<div class="discrete-content-separator-2x"></div>
<div class="table-responsive">
<table id="batteries-overview-table" class="table table-striped">
<thead>
<tr>
<th>#</th>
<th>{{ $L('Battery') }}</th>
<th>{{ $L('Last charged') }}</th>
<th>{{ $L('Next planned charge cycle') }}</th>
</tr>
</thead>
<tbody>
@foreach($current as $curentBatteryEntry)
<tr class="@if(FindObjectInArrayByPropertyValue($batteries, 'id', $curentBatteryEntry->battery_id)->charge_interval_days > 0 && $nextChargeTimes[$curentBatteryEntry->battery_id] < date('Y-m-d H:i:s')) error-bg @endif">
<td class="fit-content">
<a class="btn btn-success btn-xs track-charge-cycle-button" href="#" title="{{ $L('Track charge cycle of battery #1', FindObjectInArrayByPropertyValue($batteries, 'id', $curentBatteryEntry->battery_id)->name) }}"
data-battery-id="{{ $curentBatteryEntry->battery_id }}"
data-battery-name="{{ FindObjectInArrayByPropertyValue($batteries, 'id', $curentBatteryEntry->battery_id)->name }}">
<i class="fa fa-fire"></i>
</a>
</td>
<td>
{{ FindObjectInArrayByPropertyValue($batteries, 'id', $curentBatteryEntry->battery_id)->name }}
</td>
<td>
<span id="battery-{{ $curentBatteryEntry->battery_id }}-last-tracked-time">{{ $curentBatteryEntry->last_tracked_time }}</span>
<time id="battery-{{ $curentBatteryEntry->battery_id }}-last-tracked-time-timeago" class="timeago timeago-contextual" datetime="{{ $curentBatteryEntry->last_tracked_time }}"></time>
</td>
<td>
@if(FindObjectInArrayByPropertyValue($batteries, 'id', $curentBatteryEntry->battery_id)->charge_interval_days > 0)
{{ $nextChargeTimes[$curentBatteryEntry->battery_id] }}
<time class="timeago timeago-contextual" datetime="{{ $nextChargeTimes[$curentBatteryEntry->battery_id] }}"></time>
@else
...
@endif
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@stop

View File

@@ -9,8 +9,7 @@
@section('viewJsName', 'batteryform')
@section('content')
<div class="col-sm-3 col-sm-offset-3 col-md-4 col-md-offset-2">
<div class="col-lg-4 col-xs-12">
<h1 class="page-header">@yield('title')</h1>
<script>Grocy.EditMode = '{{ $mode }}';</script>
@@ -40,6 +39,5 @@
<button id="save-battery-button" type="submit" class="btn btn-default">{{ $L('Save') }}</button>
</form>
</div>
@stop

View File

@@ -5,8 +5,7 @@
@section('viewJsName', 'batterytracking')
@section('content')
<div class="col-sm-3 col-sm-offset-3 col-md-3 col-md-offset-2">
<div class="col-lg-4 col-xs-12">
<h1 class="page-header">@yield('title')</h1>
<form id="batterytracking-form">
@@ -36,10 +35,9 @@
<button id="save-batterytracking-button" type="submit" class="btn btn-default">{{ $L('OK') }}</button>
</form>
</div>
<div class="col-sm-6 col-md-5 col-lg-3">
<div class="col-lg-4 col-xs-12">
@include('components.batterycard')
</div>
@stop

View File

@@ -1,5 +1,5 @@
@push('componentScripts')
<script src="{{ $U('/viewjs/components/batterycard.js') }}?v={{ $version }}"></script>
<script src="{{ $U('/viewjs/components/batterycard.js', true) }}?v={{ $version }}"></script>
@endpush
<div class="main well">

View File

@@ -1,5 +1,5 @@
@push('componentScripts')
<script src="{{ $U('/viewjs/components/datepicker.js') }}?v={{ $version }}"></script>
<script src="{{ $U('/viewjs/components/datepicker.js', true) }}?v={{ $version }}"></script>
@endpush
<div class="form-group">

View File

@@ -1,5 +1,5 @@
@push('componentScripts')
<script src="{{ $U('/viewjs/components/datetimepicker.js') }}?v={{ $version }}"></script>
<script src="{{ $U('/viewjs/components/datetimepicker.js', true) }}?v={{ $version }}"></script>
@endpush
<div class="form-group">

View File

@@ -1,5 +1,5 @@
@push('componentScripts')
<script src="{{ $U('/viewjs/components/habitcard.js') }}?v={{ $version }}"></script>
<script src="{{ $U('/viewjs/components/habitcard.js', true) }}?v={{ $version }}"></script>
@endpush
<div class="main well">

View File

@@ -0,0 +1,66 @@
<ul class="nav navbar-nav sidebar-nav">
<li data-nav-for-page="stockoverview">
<a class="discrete-link" href="{{ $U('/stockoverview') }}"><i class="fa fa-tachometer fa-fw"></i>&nbsp;{{ $L('Stock overview') }}</a>
</li>
<li data-nav-for-page="habitsoverview">
<a class="discrete-link" href="{{ $U('/habitsoverview') }}"><i class="fa fa-tachometer fa-fw"></i>&nbsp;{{ $L('Habits overview') }}</a>
</li>
<li data-nav-for-page="batteriesoverview">
<a class="discrete-link" href="{{ $U('/batteriesoverview') }}"><i class="fa fa-tachometer fa-fw"></i>&nbsp;{{ $L('Batteries overview') }}</a>
</li>
</ul>
<div class="discrete-content-separator-2x"></div>
<ul class="nav navbar-nav sidebar-nav">
<li class="disabled"><a href="#"><strong>{{ $L('Record data') }}</strong></a></li>
<li data-nav-for-page="purchase">
<a class="discrete-link" href="{{ $U('/purchase') }}"><i class="fa fa-shopping-cart fa-fw"></i>&nbsp;{{ $L('Purchase') }}</a>
</li>
<li data-nav-for-page="consume">
<a class="discrete-link" href="{{ $U('/consume') }}"><i class="fa fa-cutlery fa-fw"></i>&nbsp;{{ $L('Consume') }}</a>
</li>
<li data-nav-for-page="shoppinglist">
<a class="discrete-link" href="{{ $U('/shoppinglist') }}"><i class="fa fa-shopping-bag fa-fw"></i>&nbsp;{{ $L('Shopping list') }}</a>
</li>
<li data-nav-for-page="inventory">
<a class="discrete-link" href="{{ $U('/inventory') }}"><i class="fa fa-list fa-fw"></i>&nbsp;{{ $L('Inventory') }}</a>
</li>
<li data-nav-for-page="habittracking">
<a class="discrete-link" href="{{ $U('/habittracking') }}"><i class="fa fa-play fa-fw"></i>&nbsp;{{ $L('Habit tracking') }}</a>
</li>
<li data-nav-for-page="batterytracking">
<a class="discrete-link" href="{{ $U('/batterytracking') }}"><i class="fa fa-fire fa-fw"></i>&nbsp;{{ $L('Battery tracking') }}</a>
</li>
</ul>
<div class="discrete-content-separator-2x"></div>
<ul class="nav navbar-nav sidebar-nav">
<li class="disabled"><a href="#"><strong>{{ $L('Manage master data') }}</strong></a></li>
<li data-nav-for-page="products">
<a class="discrete-link" href="{{ $U('/products') }}"><i class="fa fa-product-hunt fa-fw"></i>&nbsp;{{ $L('Products') }}</a>
</li>
<li data-nav-for-page="locations">
<a class="discrete-link" href="{{ $U('/locations') }}"><i class="fa fa-map-marker fa-fw"></i>&nbsp;{{ $L('Locations') }}</a>
</li>
<li data-nav-for-page="quantityunits">
<a class="discrete-link" href="{{ $U('/quantityunits') }}"><i class="fa fa-balance-scale fa-fw"></i>&nbsp;{{ $L('Quantity units') }}</a>
</li>
<li data-nav-for-page="habits">
<a class="discrete-link" href="{{ $U('/habits') }}"><i class="fa fa-refresh fa-fw"></i>&nbsp;{{ $L('Habits') }}</a>
</li>
<li data-nav-for-page="batteries">
<a class="discrete-link" href="{{ $U('/batteries') }}"><i class="fa fa-battery-three-quarters fa-fw"></i>&nbsp;{{ $L('Batteries') }}</a>
</li>
</ul>
<div class="discrete-content-separator-2x hidden-xs"></div>
<ul class="nav navbar-nav sidebar-nav nav-copyright">
<li>
Version {{ $version }}<br>
<a class="discrete-link" href="#" data-toggle="modal" data-target="#about-modal">{{ $L('About grocy') }}</a>
</li>
</ul>

View File

@@ -1,5 +1,5 @@
@push('componentScripts')
<script src="{{ $U('/viewjs/components/productcard.js') }}?v={{ $version }}"></script>
<script src="{{ $U('/viewjs/components/productcard.js', true) }}?v={{ $version }}"></script>
@endpush
<div class="main well">

View File

@@ -0,0 +1,17 @@
<ul class="nav navbar-nav navbar-right">
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">@if(AUTHENTICATED === true){{ HTTP_USER }}@endif <span class="caret"></span></a>
<ul class="dropdown-menu">
<li>
<a class="discrete-link logout-button" href="{{ $U('/logout') }}"><i class="fa fa-sign-out fa-fw"></i>&nbsp;{{ $L('Logout') }}</a>
</li>
<li class="divider logout-button"></li>
<li>
<a class="discrete-link" href="{{ $U('/manageapikeys') }}"><i class="fa fa-handshake-o fa-fw"></i>&nbsp;{{ $L('Manage API keys') }}</a>
</li>
<li>
<a class="discrete-link" target="_blank" href="{{ $U('/api') }}"><i class="fa fa-book"></i>&nbsp;{{ $L('REST API & data model documentation') }}</a>
</li>
</ul>
</li>
</ul>

View File

@@ -5,8 +5,7 @@
@section('viewJsName', 'consume')
@section('content')
<div class="col-sm-3 col-sm-offset-3 col-md-3 col-md-offset-2">
<div class="col-lg-4 col-xs-12">
<h1 class="page-header">@yield('title')</h1>
<form id="consume-form">
@@ -37,10 +36,9 @@
<button id="save-consume-button" type="submit" class="btn btn-default">{{ $L('OK') }}</button>
</form>
</div>
<div class="col-sm-6 col-md-5 col-lg-3">
<div class="col-lg-4 col-xs-12">
@include('components.productcard')
</div>
@stop

View File

@@ -9,8 +9,7 @@
@section('viewJsName', 'habitform')
@section('content')
<div class="col-sm-3 col-sm-offset-3 col-md-4 col-md-offset-2">
<div class="col-lg-4 col-xs-12">
<h1 class="page-header">@yield('title')</h1>
<script>Grocy.EditMode = '{{ $mode }}';</script>
@@ -53,6 +52,5 @@
<button id="save-habit-button" type="submit" class="btn btn-default">{{ $L('Save') }}</button>
</form>
</div>
@stop

View File

@@ -5,54 +5,50 @@
@section('viewJsName', 'habits')
@section('content')
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2">
<h1 class="page-header">
@yield('title')
<a class="btn btn-default" href="{{ $U('/habit/new') }}" role="button">
<i class="fa fa-plus"></i>&nbsp;{{ $L('Add') }}
</a>
</h1>
<div class="table-responsive">
<table id="habits-table" class="table table-striped">
<thead>
<tr>
<th>#</th>
<th>{{ $L('Name') }}</th>
<th>{{ $L('Period type') }}</th>
<th>{{ $L('Period days') }}</th>
<th>{{ $L('Description') }}</th>
</tr>
</thead>
<tbody>
@foreach($habits as $habit)
<tr>
<td class="fit-content">
<a class="btn btn-info" href="{{ $U('/habit/') }}{{ $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="{{ $habit->id }}" data-habit-name="{{ $habit->name }}">
<i class="fa fa-trash"></i>
</a>
</td>
<td>
{{ $habit->name }}
</td>
<td>
{{ $L($habit->period_type) }}
</td>
<td>
{{ $habit->period_days }}
</td>
<td>
{{ $habit->description }}
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
<h1 class="page-header">
@yield('title')
<a class="btn btn-default" href="{{ $U('/habit/new') }}" role="button">
<i class="fa fa-plus"></i>&nbsp;{{ $L('Add') }}
</a>
</h1>
<div class="table-responsive">
<table id="habits-table" class="table table-striped">
<thead>
<tr>
<th>#</th>
<th>{{ $L('Name') }}</th>
<th>{{ $L('Period type') }}</th>
<th>{{ $L('Period days') }}</th>
<th>{{ $L('Description') }}</th>
</tr>
</thead>
<tbody>
@foreach($habits as $habit)
<tr>
<td class="fit-content">
<a class="btn btn-info" href="{{ $U('/habit/') }}{{ $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="{{ $habit->id }}" data-habit-name="{{ $habit->name }}">
<i class="fa fa-trash"></i>
</a>
</td>
<td>
{{ $habit->name }}
</td>
<td>
{{ $L($habit->period_type) }}
</td>
<td>
{{ $habit->period_days }}
</td>
<td>
{{ $habit->description }}
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@stop

View File

@@ -4,43 +4,60 @@
@section('activeNav', 'habitsoverview')
@section('viewJsName', 'habitsoverview')
@push('pageScripts')
<script src="{{ $U('/bower_components/jquery-ui/jquery-ui.min.js?v=', true) }}{{ $version }}"></script>
@endpush
@section('content')
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2">
<h1 class="page-header">@yield('title')</h1>
<h1 class="page-header">@yield('title')</h1>
<div class="table-responsive">
<table id="habits-overview-table" class="table table-striped">
<thead>
<tr>
<th>{{ $L('Habit') }}</th>
<th>{{ $L('Next estimated tracking') }}</th>
<th>{{ $L('Last tracked') }}</th>
</tr>
</thead>
<tbody>
@foreach($currentHabits as $curentHabitEntry)
<tr class="@if(FindObjectInArrayByPropertyValue($habits, 'id', $curentHabitEntry->habit_id)->period_type === \Grocy\Services\HabitsService::HABIT_TYPE_DYNAMIC_REGULAR && $nextHabitTimes[$curentHabitEntry->habit_id] < date('Y-m-d H:i:s')) error-bg @endif">
<td>
{{ FindObjectInArrayByPropertyValue($habits, 'id', $curentHabitEntry->habit_id)->name }}
</td>
<td>
@if(FindObjectInArrayByPropertyValue($habits, 'id', $curentHabitEntry->habit_id)->period_type === \Grocy\Services\HabitsService::HABIT_TYPE_DYNAMIC_REGULAR)
{{ $nextHabitTimes[$curentHabitEntry->habit_id] }}
<time class="timeago timeago-contextual" datetime="{{ $nextHabitTimes[$curentHabitEntry->habit_id] }}"></time>
@else
...
@endif
</td>
<td>
{{ $curentHabitEntry->last_tracked_time }}
<time class="timeago timeago-contextual" datetime="{{ $curentHabitEntry->last_tracked_time }}"></time>
</td>
</tr>
@endforeach
</tbody>
</table>
<div class="container-fluid">
<div class="row">
<p class="btn btn-lg btn-warning no-real-button responsive-button">{{ $L('#1 habits are due to be done within the next #2 days', $countDueNextXDays, $nextXDays) }}</p>
<p class="btn btn-lg btn-danger no-real-button responsive-button">{{ $L('#1 habits are overdue to be done', $countOverdue) }}</p>
</div>
</div>
<div class="discrete-content-separator-2x"></div>
<div class="table-responsive">
<table id="habits-overview-table" class="table table-striped">
<thead>
<tr>
<th>#</th>
<th>{{ $L('Habit') }}</th>
<th>{{ $L('Next estimated tracking') }}</th>
<th>{{ $L('Last tracked') }}</th>
</tr>
</thead>
<tbody>
@foreach($currentHabits as $curentHabitEntry)
<tr class="@if(FindObjectInArrayByPropertyValue($habits, 'id', $curentHabitEntry->habit_id)->period_type === \Grocy\Services\HabitsService::HABIT_TYPE_DYNAMIC_REGULAR && $nextHabitTimes[$curentHabitEntry->habit_id] < date('Y-m-d H:i:s')) error-bg @endif">
<td class="fit-content">
<a class="btn btn-success btn-xs track-habit-button" href="#" title="{{ $L('Track execution of habit #1', FindObjectInArrayByPropertyValue($habits, 'id', $curentHabitEntry->habit_id)->name) }}"
data-habit-id="{{ $curentHabitEntry->habit_id }}"
data-habit-name="{{ FindObjectInArrayByPropertyValue($habits, 'id', $curentHabitEntry->habit_id)->name }}">
<i class="fa fa-play"></i>
</a>
</td>
<td>
{{ FindObjectInArrayByPropertyValue($habits, 'id', $curentHabitEntry->habit_id)->name }}
</td>
<td>
@if(FindObjectInArrayByPropertyValue($habits, 'id', $curentHabitEntry->habit_id)->period_type === \Grocy\Services\HabitsService::HABIT_TYPE_DYNAMIC_REGULAR)
{{ $nextHabitTimes[$curentHabitEntry->habit_id] }}
<time class="timeago timeago-contextual" datetime="{{ $nextHabitTimes[$curentHabitEntry->habit_id] }}"></time>
@else
...
@endif
</td>
<td>
<span id="habit-{{ $curentHabitEntry->habit_id }}-last-tracked-time">{{ $curentHabitEntry->last_tracked_time }}</span>
<time id="habit-{{ $curentHabitEntry->habit_id }}-last-tracked-time-timeago" class="timeago timeago-contextual" datetime="{{ $curentHabitEntry->last_tracked_time }}"></time>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@stop

View File

@@ -5,8 +5,7 @@
@section('viewJsName', 'habittracking')
@section('content')
<div class="col-sm-3 col-sm-offset-3 col-md-3 col-md-offset-2">
<div class="col-lg-4 col-xs-12">
<h1 class="page-header">@yield('title')</h1>
<form id="habittracking-form">
@@ -30,10 +29,9 @@
<button id="save-habittracking-button" type="submit" class="btn btn-default">{{ $L('OK') }}</button>
</form>
</div>
<div class="col-sm-6 col-md-5 col-lg-3">
<div class="col-lg-4 col-xs-12">
@include('components.habitcard')
</div>
@stop

View File

@@ -5,8 +5,7 @@
@section('viewJsName', 'inventory')
@section('content')
<div class="col-sm-4 col-sm-offset-3 col-md-3 col-md-offset-2">
<div class="col-lg-4 col-xs-12">
<h1 class="page-header">@yield('title')</h1>
<form id="inventory-form">
@@ -39,10 +38,9 @@
<button id="save-inventory-button" type="submit" class="btn btn-default">{{ $L('OK') }}</button>
</form>
</div>
<div class="col-sm-6 col-md-5 col-lg-3">
<div class="col-lg-4 col-xs-12">
@include('components.productcard')
</div>
@stop

View File

@@ -9,21 +9,22 @@
<meta name="format-detection" content="telephone=no">
<meta name="author" content="Bernd Bestel (bernd@berrnd.de)">
<link rel="icon" type="image/png" sizes="200x200" href="{{ $U('/img/grocy.png?v=') }}{{ $version }}">
<link rel="icon" type="image/png" sizes="200x200" href="{{ $U('/img/grocy.png?v=', true) }}{{ $version }}">
<title>@yield('title') | grocy</title>
<link href="{{ $U('/bower_components/bootstrap/dist/css/bootstrap.min.css?v=') }}{{ $version }}" rel="stylesheet">
<link href="{{ $U('/bower_components/font-awesome/css/font-awesome.min.css?v=') }}{{ $version }}" rel="stylesheet">
<link href="{{ $U('/bower_components/bootstrap-datepicker/dist/css/bootstrap-datepicker3.min.css?v=') }}{{ $version }}" rel="stylesheet">
<link href="{{ $U('/bower_components/bootstrap-combobox/css/bootstrap-combobox.css?v=') }}{{ $version }}" rel="stylesheet">
<link href="{{ $U('/bower_components/datatables.net-bs/css/dataTables.bootstrap.min.css?v=') }}{{ $version }}" rel="stylesheet">
<link href="{{ $U('/bower_components/datatables.net-responsive-bs/css/responsive.bootstrap.min.css?v=') }}{{ $version }}" rel="stylesheet">
<link href="{{ $U('/bower_components/toastr/toastr.min.css?v=') }}{{ $version }}" rel="stylesheet">
<link href="{{ $U('/bower_components/tagmanager/tagmanager.css?v=') }}{{ $version }}" rel="stylesheet">
<link href="{{ $U('/bower_components/eonasdan-bootstrap-datetimepicker/build/css/bootstrap-datetimepicker.min.css?v=') }}{{ $version }}" rel="stylesheet">
<link href="{{ $U('/components_unmanaged/noto-sans-v6-latin/noto-sans-v6-latin.css?v=') }}{{ $version }}" rel="stylesheet">
<link href="{{ $U('/css/grocy.css?v=') }}{{ $version }}" rel="stylesheet">
<link href="{{ $U('/bower_components/bootstrap/dist/css/bootstrap.min.css?v=', true) }}{{ $version }}" rel="stylesheet">
<link href="{{ $U('/bower_components/font-awesome/css/font-awesome.min.css?v=', true) }}{{ $version }}" rel="stylesheet">
<link href="{{ $U('/bower_components/bootstrap-datepicker/dist/css/bootstrap-datepicker3.min.css?v=', true) }}{{ $version }}" rel="stylesheet">
<link href="{{ $U('/bower_components/bootstrap-combobox/css/bootstrap-combobox.css?v=', true) }}{{ $version }}" rel="stylesheet">
<link href="{{ $U('/bower_components/datatables.net-bs/css/dataTables.bootstrap.min.css?v=', true) }}{{ $version }}" rel="stylesheet">
<link href="{{ $U('/bower_components/datatables.net-responsive-bs/css/responsive.bootstrap.min.css?v=', true) }}{{ $version }}" rel="stylesheet">
<link href="{{ $U('/bower_components/toastr/toastr.min.css?v=', true) }}{{ $version }}" rel="stylesheet">
<link href="{{ $U('/bower_components/tagmanager/tagmanager.css?v=', true) }}{{ $version }}" rel="stylesheet">
<link href="{{ $U('/bower_components/eonasdan-bootstrap-datetimepicker/build/css/bootstrap-datetimepicker.min.css?v=', true) }}{{ $version }}" rel="stylesheet">
<link href="{{ $U('/bower_components/bootstrap-side-navbar/source/assets/stylesheets/navbar-fixed-side.css?v=', true) }}{{ $version }}" rel="stylesheet">
<link href="{{ $U('/components_unmanaged/noto-sans-v6-latin/noto-sans-v6-latin.css?v=', true) }}{{ $version }}" rel="stylesheet">
<link href="{{ $U('/css/grocy.css?v=', true) }}{{ $version }}" rel="stylesheet">
@stack('pageStyles')
<script>
@@ -48,113 +49,12 @@
</div>
<div id="navbar" class="navbar-collapse collapse">
<ul class="nav navbar-nav navbar-right">
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">{{ HTTP_USER }} <span class="caret"></span></a>
<ul class="dropdown-menu">
<li>
<a class="discrete-link logout-button" href="{{ $U('/logout') }}"><i class="fa fa-sign-out fa-fw"></i>&nbsp;{{ $L('Logout') }}</a>
</li>
<li role="separator" class="divider"></li>
<li>
<a class="discrete-link" href="{{ $U('/manageapikeys') }}"><i class="fa fa-handshake-o fa-fw"></i>&nbsp;{{ $L('Manage API keys') }}</a>
</li>
<li>
<a class="discrete-link" target="_blank" href="{{ $U('/api') }}"><i class="fa fa-book"></i>&nbsp;{{ $L('REST API & data model documentation') }}</a>
</li>
</ul>
</li>
</ul>
@include('components.usermenu')
</div>
<div id="navbar-mobile" class="navbar-collapse collapse">
<ul class="nav navbar-nav navbar-right">
<li data-nav-for-page="stockoverview">
<a class="discrete-link" href="{{ $U('/stockoverview') }}"><i class="fa fa-tachometer fa-fw"></i>&nbsp;{{ $L('Stock overview') }}</a>
</li>
<li data-nav-for-page="habitsoverview">
<a class="discrete-link" href="{{ $U('/habitsoverview') }}"><i class="fa fa-tachometer fa-fw"></i>&nbsp;{{ $L('Habits overview') }}</a>
</li>
<li data-nav-for-page="batteriesoverview">
<a class="discrete-link" href="{{ $U('/batteriesoverview') }}"><i class="fa fa-tachometer fa-fw"></i>&nbsp;{{ $L('Batteries overview') }}</a>
</li>
</ul>
<ul class="nav navbar-nav navbar-right">
<li class="disabled"><a href="#"><strong>{{ $L('Record data') }}</strong></a></li>
<li data-nav-for-page="purchase">
<a class="discrete-link" href="{{ $U('/purchase') }}"><i class="fa fa-shopping-cart fa-fw"></i>&nbsp;{{ $L('Purchase') }}</a>
</li>
<li data-nav-for-page="consume">
<a class="discrete-link" href="{{ $U('/consume') }}"><i class="fa fa-cutlery fa-fw"></i>&nbsp;{{ $L('Consume') }}</a>
</li>
<li data-nav-for-page="shoppinglist">
<a class="discrete-link" href="{{ $U('/shoppinglist') }}"><i class="fa fa-shopping-bag fa-fw"></i>&nbsp;{{ $L('Shopping list') }}</a>
</li>
<li data-nav-for-page="inventory">
<a class="discrete-link" href="{{ $U('/inventory') }}"><i class="fa fa-list fa-fw"></i>&nbsp;{{ $L('Inventory') }}</a>
</li>
<li data-nav-for-page="habittracking">
<a class="discrete-link" href="{{ $U('/habittracking') }}"><i class="fa fa-play fa-fw"></i>&nbsp;{{ $L('Habit tracking') }}</a>
</li>
<li data-nav-for-page="batterytracking">
<a class="discrete-link" href="{{ $U('/batterytracking') }}"><i class="fa fa-fire fa-fw"></i>&nbsp;{{ $L('Battery tracking') }}</a>
</li>
</ul>
<ul class="nav navbar-nav navbar-right">
<li class="disabled"><a href="#"><strong>{{ $L('Manage master data') }}</strong></a></li>
<li data-nav-for-page="products">
<a class="discrete-link" href="{{ $U('/products') }}"><i class="fa fa-product-hunt fa-fw"></i>&nbsp;{{ $L('Products') }}</a>
</li>
<li data-nav-for-page="locations">
<a class="discrete-link" href="{{ $U('/locations') }}"><i class="fa fa-map-marker fa-fw"></i>&nbsp;{{ $L('Locations') }}</a>
</li>
<li data-nav-for-page="quantityunits">
<a class="discrete-link" href="{{ $U('/quantityunits') }}"><i class="fa fa-balance-scale fa-fw"></i>&nbsp;{{ $L('Quantity units') }}</a>
</li>
<li data-nav-for-page="habits">
<a class="discrete-link" href="{{ $U('/habits') }}"><i class="fa fa-refresh fa-fw"></i>&nbsp;{{ $L('Habits') }}</a>
</li>
<li data-nav-for-page="batteries">
<a class="discrete-link" href="{{ $U('/batteries') }}"><i class="fa fa-battery-three-quarters fa-fw"></i>&nbsp;{{ $L('Batteries') }}</a>
</li>
</ul>
<ul class="nav navbar-nav navbar-right">
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">{{ HTTP_USER }} <span class="caret"></span></a>
<ul class="dropdown-menu">
<li>
<a class="discrete-link logout-button" href="{{ $U('/logout') }}"><i class="fa fa-sign-out fa-fw"></i>&nbsp;{{ $L('Logout') }}</a>
</li>
<li role="separator" class="divider"></li>
<li>
<a class="discrete-link" href="{{ $U('/manageapikeys') }}"><i class="fa fa-handshake-o fa-fw"></i>&nbsp;{{ $L('Manage API keys') }}</a>
</li>
<li>
<a class="discrete-link" target="_blank" href="{{ $U('/api') }}"><i class="fa fa-book"></i>&nbsp;{{ $L('REST API & data model documentation') }}</a>
</li>
</ul>
</li>
</ul>
<div class="nav-copyright nav nav-sidebar">
grocy is a project by
<a class="discrete-link" href="https://berrnd.de" target="_blank">Bernd Bestel</a>
<br>
Created with passion since 2017
<br>
Version {{ $version }}
<br>
Life runs on code
<br>
<a class="discrete-link" href="https://github.com/berrnd/grocy" target="_blank">
<i class="fa fa-github"></i>
</a>
</div>
@include('components.menu')
@include('components.usermenu')
</div>
</div>
</nav>
@@ -162,108 +62,73 @@
<div class="container-fluid">
<div class="row">
<div class="col-sm-3 col-md-2 sidebar">
<ul class="nav nav-sidebar">
<li data-nav-for-page="stockoverview">
<a class="discrete-link" href="{{ $U('/stockoverview') }}"><i class="fa fa-tachometer fa-fw"></i>&nbsp;{{ $L('Stock overview') }}</a>
</li>
<li data-nav-for-page="habitsoverview">
<a class="discrete-link" href="{{ $U('/habitsoverview') }}"><i class="fa fa-tachometer fa-fw"></i>&nbsp;{{ $L('Habits overview') }}</a>
</li>
<li data-nav-for-page="batteriesoverview">
<a class="discrete-link" href="{{ $U('/batteriesoverview') }}"><i class="fa fa-tachometer fa-fw"></i>&nbsp;{{ $L('Batteries overview') }}</a>
</li>
</ul>
<ul class="nav nav-sidebar">
<li class="disabled"><a href="#"><strong>{{ $L('Record data') }}</strong></a></li>
<li data-nav-for-page="purchase">
<a class="discrete-link" href="{{ $U('/purchase') }}"><i class="fa fa-shopping-cart fa-fw"></i>&nbsp;{{ $L('Purchase') }}</a>
</li>
<li data-nav-for-page="consume">
<a class="discrete-link" href="{{ $U('/consume') }}"><i class="fa fa-cutlery fa-fw"></i>&nbsp;{{ $L('Consume') }}</a>
</li>
<li data-nav-for-page="shoppinglist">
<a class="discrete-link" href="{{ $U('/shoppinglist') }}"><i class="fa fa-shopping-bag fa-fw"></i>&nbsp;{{ $L('Shopping list') }}</a>
</li>
<li data-nav-for-page="inventory">
<a class="discrete-link" href="{{ $U('/inventory') }}"><i class="fa fa-list fa-fw"></i>&nbsp;{{ $L('Inventory') }}</a>
</li>
<li data-nav-for-page="habittracking">
<a class="discrete-link" href="{{ $U('/habittracking') }}"><i class="fa fa-play fa-fw"></i>&nbsp;{{ $L('Habit tracking') }}</a>
</li>
<li data-nav-for-page="batterytracking">
<a class="discrete-link" href="{{ $U('/batterytracking') }}"><i class="fa fa-fire fa-fw"></i>&nbsp;{{ $L('Battery tracking') }}</a>
</li>
</ul>
<ul class="nav nav-sidebar">
<li class="disabled"><a href="#"><strong>{{ $L('Manage master data') }}</strong></a></li>
<li data-nav-for-page="products">
<a class="discrete-link" href="{{ $U('/products') }}"><i class="fa fa-product-hunt fa-fw"></i>&nbsp;{{ $L('Products') }}</a>
</li>
<li data-nav-for-page="locations">
<a class="discrete-link" href="{{ $U('/locations') }}"><i class="fa fa-map-marker fa-fw"></i>&nbsp;{{ $L('Locations') }}</a>
</li>
<li data-nav-for-page="quantityunits">
<a class="discrete-link" href="{{ $U('/quantityunits') }}"><i class="fa fa-balance-scale fa-fw"></i>&nbsp;{{ $L('Quantity units') }}</a>
</li>
<li data-nav-for-page="habits">
<a class="discrete-link" href="{{ $U('/habits') }}"><i class="fa fa-refresh fa-fw"></i>&nbsp;{{ $L('Habits') }}</a>
</li>
<li data-nav-for-page="batteries">
<a class="discrete-link" href="{{ $U('/batteries') }}"><i class="fa fa-battery-three-quarters fa-fw"></i>&nbsp;{{ $L('Batteries') }}</a>
</li>
</ul>
<div class="nav-copyright nav nav-sidebar">
grocy is a project by
<a class="discrete-link" href="https://berrnd.de" target="_blank">Bernd Bestel</a>
<br>
Created with passion since 2017
<br>
Version {{ $version }}
<br>
Life runs on code
<br>
<a class="discrete-link" href="https://github.com/berrnd/grocy" target="_blank">
<i class="fa fa-github"></i>
</a>
</div>
<div id="sidebar" class="col-sm-3 col-lg-2">
<nav class="navbar navbar-default navbar-fixed-side hidden-xs">
<div class="navbar-collapse collapse">
@include('components.menu')
</div>
</nav>
</div>
@yield('content')
<div class="col-sm-9 col-lg-10">
@yield('content')
</div>
</div>
</div>
<script src="{{ $U('/bower_components/jquery/dist/jquery.min.js?v=') }}{{ $version }}"></script>
<script src="{{ $U('/bower_components/bootstrap/dist/js/bootstrap.min.js?v=') }}{{ $version }}"></script>
<script src="{{ $U('/bower_components/bootbox/bootbox.js?v=') }}{{ $version }}"></script>
<script src="{{ $U('/bower_components/jquery.serializeJSON/jquery.serializejson.min.js?v=') }}{{ $version }}"></script>
<script src="{{ $U('/bower_components/bootstrap-datepicker/dist/js/bootstrap-datepicker.min.js?v=') }}{{ $version }}"></script>
@if(!empty($L('bootstrap_datepicker_locale')))<script src="{{ $U('/bower_components') }}/bootstrap-datepicker/dist/locales/bootstrap-datepicker.{{ $L('bootstrap_datepicker_locale') }}.min.js?v={{ $version }}"></script>@endif
<script src="{{ $U('/bower_components/moment/min/moment.min.js?v=') }}{{ $version }}"></script>
@if(!empty($L('moment_locale')))<script src="{{ $U('/bower_components') }}/moment/locale/{{ $L('moment_locale') }}.js?v={{ $version }}"></script>@endif
<script src="{{ $U('/bower_components/bootstrap-validator/dist/validator.min.js?v=') }}{{ $version }}"></script>
<script src="{{ $U('/bower_components/bootstrap-combobox/js/bootstrap-combobox.js?v=') }}{{ $version }}"></script>
<script src="{{ $U('/bower_components/datatables.net/js/jquery.dataTables.min.js?v=') }}{{ $version }}"></script>
<script src="{{ $U('/bower_components/datatables.net-bs/js/dataTables.bootstrap.min.js?v=') }}{{ $version }}"></script>
<script src="{{ $U('/bower_components/datatables.net-responsive/js/dataTables.responsive.min.js?v=') }}{{ $version }}"></script>
<script src="{{ $U('/bower_components/datatables.net-responsive-bs/js/responsive.bootstrap.min.js?v=') }}{{ $version }}"></script>
<script src="{{ $U('/bower_components/jquery-timeago/jquery.timeago.js?v=') }}{{ $version }}"></script>
<script src="{{ $U('/bower_components') }}/jquery-timeago/locales/jquery.timeago.{{ $L('timeago_locale') }}.js?v={{ $version }}"></script>
<script src="{{ $U('/bower_components/toastr/toastr.min.js?v=') }}{{ $version }}"></script>
<script src="{{ $U('/bower_components/tagmanager/tagmanager.js?v=') }}{{ $version }}"></script>
<script src="{{ $U('/bower_components/eonasdan-bootstrap-datetimepicker/build/js/bootstrap-datetimepicker.min.js?v=') }}{{ $version }}"></script>
<div class="modal fade" id="about-modal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content text-center">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h4 class="modal-title">{{ $L('About grocy') }}</h4>
</div>
<div class="modal-body">
grocy is a project by
<a href="https://berrnd.de" target="_blank">Bernd Bestel</a><br>
Created with passion since 2017<br>
<br>
Version {{ $version }}<br>
{{ $L('Released on') }} {{ $releaseDate }} <time class="timeago timeago-contextual" datetime="{{ $releaseDate }}"></time><br>
<br>
Life runs on code<br>
<a href="https://github.com/berrnd/grocy" target="_blank">
<i class="fa fa-github"></i>
</a>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">{{ $L('Close') }}</button>
</div>
</div>
</div>
</div>
<script src="{{ $U('/js/extensions.js?v=') }}{{ $version }}"></script>
<script src="{{ $U('/js/grocy.js?v=') }}{{ $version }}"></script>
<script src="{{ $U('/bower_components/jquery/dist/jquery.min.js?v=', true) }}{{ $version }}"></script>
<script src="{{ $U('/bower_components/bootstrap/dist/js/bootstrap.min.js?v=', true) }}{{ $version }}"></script>
<script src="{{ $U('/bower_components/bootbox/bootbox.js?v=', true) }}{{ $version }}"></script>
<script src="{{ $U('/bower_components/jquery.serializeJSON/jquery.serializejson.min.js?v=', true) }}{{ $version }}"></script>
<script src="{{ $U('/bower_components/bootstrap-datepicker/dist/js/bootstrap-datepicker.min.js?v=', true) }}{{ $version }}"></script>
@if(!empty($L('bootstrap_datepicker_locale')))<script src="{{ $U('/bower_components', true) }}/bootstrap-datepicker/dist/locales/bootstrap-datepicker.{{ $L('bootstrap_datepicker_locale') }}.min.js?v={{ $version }}"></script>@endif
<script src="{{ $U('/bower_components/moment/min/moment.min.js?v=', true) }}{{ $version }}"></script>
@if(!empty($L('moment_locale')))<script src="{{ $U('/bower_components', true) }}/moment/locale/{{ $L('moment_locale') }}.js?v={{ $version }}"></script>@endif
<script src="{{ $U('/bower_components/bootstrap-validator/dist/validator.min.js?v=', true) }}{{ $version }}"></script>
<script src="{{ $U('/bower_components/bootstrap-combobox/js/bootstrap-combobox.js?v=', true) }}{{ $version }}"></script>
<script src="{{ $U('/bower_components/datatables.net/js/jquery.dataTables.min.js?v=', true) }}{{ $version }}"></script>
<script src="{{ $U('/bower_components/datatables.net-bs/js/dataTables.bootstrap.min.js?v=', true) }}{{ $version }}"></script>
<script src="{{ $U('/bower_components/datatables.net-responsive/js/dataTables.responsive.min.js?v=', true) }}{{ $version }}"></script>
<script src="{{ $U('/bower_components/datatables.net-responsive-bs/js/responsive.bootstrap.min.js?v=', true) }}{{ $version }}"></script>
<script src="{{ $U('/bower_components/jquery-timeago/jquery.timeago.js?v=', true) }}{{ $version }}"></script>
<script src="{{ $U('/bower_components', true) }}/jquery-timeago/locales/jquery.timeago.{{ $L('timeago_locale') }}.js?v={{ $version }}"></script>
<script src="{{ $U('/bower_components/toastr/toastr.min.js?v=', true) }}{{ $version }}"></script>
<script src="{{ $U('/bower_components/tagmanager/tagmanager.js?v=', true) }}{{ $version }}"></script>
<script src="{{ $U('/bower_components/eonasdan-bootstrap-datetimepicker/build/js/bootstrap-datetimepicker.min.js?v=', true) }}{{ $version }}"></script>
<script src="{{ $U('/js/extensions.js?v=', true) }}{{ $version }}"></script>
<script src="{{ $U('/js/grocy.js?v=', true) }}{{ $version }}"></script>
@stack('pageScripts')
@stack('componentScripts')
<script src="{{ $U('/viewjs') }}/@yield('viewJsName').js?v={{ $version }}"></script>
<script src="{{ $U('/viewjs', true) }}/@yield('viewJsName').js?v={{ $version }}"></script>
@if(file_exists(__DIR__ . '/../../data/add_before_end_body.html'))
@php include __DIR__ . '/../../data/add_before_end_body.html' @endphp

View File

@@ -9,8 +9,7 @@
@section('viewJsName', 'locationform')
@section('content')
<div class="col-sm-3 col-sm-offset-3 col-md-4 col-md-offset-2">
<div class="col-lg-4 col-xs-12">
<h1 class="page-header">@yield('title')</h1>
<script>Grocy.EditMode = '{{ $mode }}';</script>
@@ -35,6 +34,5 @@
<button id="save-location-button" type="submit" class="btn btn-default">{{ $L('Save') }}</button>
</form>
</div>
@stop

View File

@@ -5,46 +5,42 @@
@section('viewJsName', 'locations')
@section('content')
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2">
<h1 class="page-header">
@yield('title')
<a class="btn btn-default" href="{{ $U('/location/new') }}" role="button">
<i class="fa fa-plus"></i>&nbsp;{{ $L('Add') }}
</a>
</h1>
<div class="table-responsive">
<table id="locations-table" class="table table-striped">
<thead>
<tr>
<th>#</th>
<th>{{ $L('Name') }}</th>
<th>{{ $L('Description') }}</th>
</tr>
</thead>
<tbody>
@foreach($locations as $location)
<tr>
<td class="fit-content">
<a class="btn btn-info" href="{{ $U('/location/') }}{{ $location->id }}" role="button">
<i class="fa fa-pencil"></i>
</a>
<a class="btn btn-danger location-delete-button" href="#" role="button" data-location-id="{{ $location->id }}" data-location-name="{{ $location->name }}">
<i class="fa fa-trash"></i>
</a>
</td>
<td>
{{ $location->name }}
</td>
<td>
{{ $location->description }}
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
<h1 class="page-header">
@yield('title')
<a class="btn btn-default" href="{{ $U('/location/new') }}" role="button">
<i class="fa fa-plus"></i>&nbsp;{{ $L('Add') }}
</a>
</h1>
<div class="table-responsive">
<table id="locations-table" class="table table-striped">
<thead>
<tr>
<th>#</th>
<th>{{ $L('Name') }}</th>
<th>{{ $L('Description') }}</th>
</tr>
</thead>
<tbody>
@foreach($locations as $location)
<tr>
<td class="fit-content">
<a class="btn btn-info" href="{{ $U('/location/') }}{{ $location->id }}" role="button">
<i class="fa fa-pencil"></i>
</a>
<a class="btn btn-danger location-delete-button" href="#" role="button" data-location-id="{{ $location->id }}" data-location-name="{{ $location->name }}">
<i class="fa fa-trash"></i>
</a>
</td>
<td>
{{ $location->name }}
</td>
<td>
{{ $location->description }}
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@stop

View File

@@ -4,27 +4,25 @@
@section('viewJsName', 'login')
@section('content')
<div class="col-md-4 col-md-offset-5">
<div class="col-md-6 col-md-offset-3 col-xs-12">
<h1 class="page-header text-center">@yield('title')</h1>
<h1 class="page-header text-center">@yield('title')</h1>
<form method="post" action="{{ $U('/login') }}" id="login-form">
<form method="post" action="{{ $U('/login') }}" id="login-form">
<div class="form-group">
<label for="name">{{ $L('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">{{ $L('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">{{ $L('Password') }}</label>
<input type="password" class="form-control" required id="password" name="password">
<div id="login-error" class="help-block with-errors"></div>
</div>
<div class="form-group">
<label for="name">{{ $L('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">{{ $L('OK') }}</button>
<button id="login-button" type="submit" class="btn btn-default">{{ $L('OK') }}</button>
</form>
</div>
</form>
</div>
@stop

View File

@@ -5,60 +5,56 @@
@section('viewJsName', 'manageapikeys')
@push('pageScripts')
<script src="{{ $U('/bower_components/jquery-ui/jquery-ui.min.js?v=') }}{{ $version }}"></script>
<script src="{{ $U('/bower_components/jquery-ui/jquery-ui.min.js?v=', true) }}{{ $version }}"></script>
@endpush
@section('content')
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2">
<h1 class="page-header">
@yield('title')
<a class="btn btn-default" href="{{ $U('/manageapikeys/new') }}" role="button">
<i class="fa fa-plus"></i>&nbsp;{{ $L('Create new API key') }}
</a>
</h1>
<h1 class="page-header">
@yield('title')
<a class="btn btn-default" href="{{ $U('/manageapikeys/new') }}" role="button">
<i class="fa fa-plus"></i>&nbsp;{{ $L('Create new API key') }}
</a>
</h1>
<p class="lead"><a href="{{ $U('/api') }}" target="_blank">{{ $L('REST API & data model documentation') }}</a></p>
<p class="lead"><a href="{{ $U('/api') }}" target="_blank">{{ $L('REST API & data model documentation') }}</a></p>
<div class="table-responsive">
<table id="apikeys-table" class="table table-striped">
<thead>
<tr>
<th>#</th>
<th>{{ $L('API key') }}</th>
<th>{{ $L('Expires') }}</th>
<th>{{ $L('Last used') }}</th>
<th>{{ $L('Created') }}</th>
</tr>
</thead>
<tbody>
@foreach($apiKeys as $apiKey)
<div class="table-responsive">
<table id="apikeys-table" class="table table-striped">
<thead>
<tr>
<th>#</th>
<th>{{ $L('API key') }}</th>
<th>{{ $L('Expires') }}</th>
<th>{{ $L('Last used') }}</th>
<th>{{ $L('Created') }}</th>
</tr>
</thead>
<tbody>
@foreach($apiKeys as $apiKey)
<tr id="apiKeyRow_{{ $apiKey->id }}">
<td class="fit-content">
<a class="btn btn-danger apikey-delete-button" href="#" role="button" data-apikey-id="{{ $apiKey->id }}" data-apikey-apikey="{{ $apiKey->api_key }}">
<i class="fa fa-trash"></i>
</a>
</td>
<td>
{{ $apiKey->api_key }}
</td>
<td>
{{ $apiKey->expires }}
<time class="timeago timeago-contextual" datetime="{{ $apiKey->expires }}"></time>
</td>
<td>
@if(empty($apiKey->last_used)){{ $L('never') }}@else{{ $apiKey->last_used }}@endif
<time class="timeago timeago-contextual" datetime="{{ $apiKey->last_used }}"></time>
</td>
<td>
{{ $apiKey->row_created_timestamp }}
<time class="timeago timeago-contextual" datetime="{{ $apiKey->row_created_timestamp }}"></time>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
<td class="fit-content">
<a class="btn btn-danger apikey-delete-button" href="#" role="button" data-apikey-id="{{ $apiKey->id }}" data-apikey-apikey="{{ $apiKey->api_key }}">
<i class="fa fa-trash"></i>
</a>
</td>
<td>
{{ $apiKey->api_key }}
</td>
<td>
{{ $apiKey->expires }}
<time class="timeago timeago-contextual" datetime="{{ $apiKey->expires }}"></time>
</td>
<td>
@if(empty($apiKey->last_used)){{ $L('never') }}@else{{ $apiKey->last_used }}@endif
<time class="timeago timeago-contextual" datetime="{{ $apiKey->last_used }}"></time>
</td>
<td>
{{ $apiKey->row_created_timestamp }}
<time class="timeago timeago-contextual" datetime="{{ $apiKey->row_created_timestamp }}"></time>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@stop

View File

@@ -9,11 +9,11 @@
<meta name="format-detection" content="telephone=no">
<meta name="author" content="Bernd Bestel (bernd@berrnd.de)">
<link rel="icon" type="image/png" sizes="200x200" href="{{ $U('/img/grocy.png?v=') }}{{ $version }}">
<link rel="icon" type="image/png" sizes="200x200" href="{{ $U('/img/grocy.png?v=', true) }}{{ $version }}">
<title>{{ $L('REST API & data model documentation') }} | grocy</title>
<link href="{{ $U('/bower_components/swagger-ui/dist/swagger-ui.css?v=') }}{{ $version }}" rel="stylesheet">
<link href="{{ $U('/bower_components/swagger-ui/dist/swagger-ui.css?v=', true) }}{{ $version }}" rel="stylesheet">
<script>
var Grocy = { };
@@ -25,9 +25,9 @@
<body>
<div id="swagger-ui"></div>
<script src="{{ $U('/bower_components/swagger-ui/dist/swagger-ui-bundle.js?v=') }}{{ $version }}"></script>
<script src="{{ $U('/bower_components/swagger-ui/dist/swagger-ui-standalone-preset.js?v=') }}{{ $version }}"></script>
<script src="{{ $U('/viewjs') }}/openapiui.js?v={{ $version }}"></script>
<script src="{{ $U('/bower_components/swagger-ui/dist/swagger-ui-bundle.js?v=', true) }}{{ $version }}"></script>
<script src="{{ $U('/bower_components/swagger-ui/dist/swagger-ui-standalone-preset.js?v=', true) }}{{ $version }}"></script>
<script src="{{ $U('/viewjs', true) }}/openapiui.js?v={{ $version }}"></script>
@if(file_exists(__DIR__ . '/../../data/add_before_end_body.html'))
@php include __DIR__ . '/../../data/add_before_end_body.html' @endphp

View File

@@ -9,8 +9,7 @@
@section('viewJsName', 'productform')
@section('content')
<div class="col-sm-3 col-sm-offset-3 col-md-4 col-md-offset-2">
<div class="col-lg-4 col-xs-12">
<h1 class="page-header">@yield('title')</h1>
<script>Grocy.EditMode = '{{ $mode }}';</script>
@@ -90,6 +89,5 @@
<button id="save-product-button" type="submit" class="btn btn-default">{{ $L('Save') }}</button>
</form>
</div>
@stop

View File

@@ -5,66 +5,62 @@
@section('viewJsName', 'products')
@section('content')
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2">
<h1 class="page-header">
@yield('title')
<a class="btn btn-default" href="{{ $U('/product/new') }}" role="button">
<i class="fa fa-plus"></i>&nbsp;{{ $L('Add') }}
</a>
</h1>
<div class="table-responsive">
<table id="products-table" class="table table-striped">
<thead>
<tr>
<th>#</th>
<th>{{ $L('Name') }}</th>
<th>{{ $L('Location') }}</th>
<th>{{ $L('Min. stock amount') }}</th>
<th>{{ $L('QU purchase') }}</th>
<th>{{ $L('QU stock') }}</th>
<th>{{ $L('QU factor') }}</th>
<th>{{ $L('Description') }}</th>
</tr>
</thead>
<tbody>
@foreach($products as $product)
<tr>
<td class="fit-content">
<a class="btn btn-info" href="{{ $U('/product/') }}{{ $product->id }}" role="button">
<i class="fa fa-pencil"></i>
</a>
<a class="btn btn-danger product-delete-button" href="#" role="button" data-product-id="{{ $product->id }}" data-product-name="{{ $product->name }}">
<i class="fa fa-trash"></i>
</a>
</td>
<td>
{{ $product->name }}
</td>
<td>
{{ FindObjectInArrayByPropertyValue($locations, 'id', $product->location_id)->name }}
</td>
<td>
{{ $product->min_stock_amount }}
</td>
<td>
{{ FindObjectInArrayByPropertyValue($quantityunits, 'id', $product->qu_id_purchase)->name }}
</td>
<td>
{{ FindObjectInArrayByPropertyValue($quantityunits, 'id', $product->qu_id_stock)->name }}
</td>
<td>
{{ $product->qu_factor_purchase_to_stock }}
</td>
<td>
{{ $product->description }}
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
<h1 class="page-header">
@yield('title')
<a class="btn btn-default" href="{{ $U('/product/new') }}" role="button">
<i class="fa fa-plus"></i>&nbsp;{{ $L('Add') }}
</a>
</h1>
<div class="table-responsive">
<table id="products-table" class="table table-striped">
<thead>
<tr>
<th>#</th>
<th>{{ $L('Name') }}</th>
<th>{{ $L('Location') }}</th>
<th>{{ $L('Min. stock amount') }}</th>
<th>{{ $L('QU purchase') }}</th>
<th>{{ $L('QU stock') }}</th>
<th>{{ $L('QU factor') }}</th>
<th>{{ $L('Description') }}</th>
</tr>
</thead>
<tbody>
@foreach($products as $product)
<tr>
<td class="fit-content">
<a class="btn btn-info" href="{{ $U('/product/') }}{{ $product->id }}" role="button">
<i class="fa fa-pencil"></i>
</a>
<a class="btn btn-danger product-delete-button" href="#" role="button" data-product-id="{{ $product->id }}" data-product-name="{{ $product->name }}">
<i class="fa fa-trash"></i>
</a>
</td>
<td>
{{ $product->name }}
</td>
<td>
{{ FindObjectInArrayByPropertyValue($locations, 'id', $product->location_id)->name }}
</td>
<td>
{{ $product->min_stock_amount }}
</td>
<td>
{{ FindObjectInArrayByPropertyValue($quantityunits, 'id', $product->qu_id_purchase)->name }}
</td>
<td>
{{ FindObjectInArrayByPropertyValue($quantityunits, 'id', $product->qu_id_stock)->name }}
</td>
<td>
{{ $product->qu_factor_purchase_to_stock }}
</td>
<td>
{{ $product->description }}
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@stop

View File

@@ -5,8 +5,7 @@
@section('viewJsName', 'purchase')
@section('content')
<div class="col-sm-4 col-sm-offset-3 col-md-3 col-md-offset-2">
<div class="col-lg-4 col-xs-12">
<h1 class="page-header">@yield('title')</h1>
<form id="purchase-form">
@@ -37,10 +36,9 @@
<button id="save-purchase-button" type="submit" class="btn btn-default">{{ $L('OK') }}</button>
</form>
</div>
<div class="col-sm-6 col-md-5 col-lg-3">
<div class="col-lg-4 col-xs-12">
@include('components.productcard')
</div>
@stop

View File

@@ -9,8 +9,7 @@
@section('viewJsName', 'quantityunitform')
@section('content')
<div class="col-sm-3 col-sm-offset-3 col-md-4 col-md-offset-2">
<div class="col-lg-4 col-xs-12">
<h1 class="page-header">@yield('title')</h1>
<script>Grocy.EditMode = '{{ $mode }}';</script>
@@ -35,6 +34,5 @@
<button id="save-quantityunit-button" type="submit" class="btn btn-default">{{ $L('Save') }}</button>
</form>
</div>
@stop

View File

@@ -5,46 +5,42 @@
@section('viewJsName', 'quantityunits')
@section('content')
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2">
<h1 class="page-header">
@yield('title')
<a class="btn btn-default" href="{{ $U('/quantityunit/new') }}" role="button">
<i class="fa fa-plus"></i>&nbsp;Add
</a>
</h1>
<div class="table-responsive">
<table id="quantityunits-table" class="table table-striped">
<thead>
<tr>
<th>#</th>
<th>{{ $L('Name') }}</th>
<th>{{ $L('Description') }}</th>
</tr>
</thead>
<tbody>
@foreach($quantityunits as $quantityunit)
<tr>
<td class="fit-content">
<a class="btn btn-info" href="{{ $U('/quantityunit/') }}{{ $quantityunit->id }}" role="button">
<i class="fa fa-pencil"></i>
</a>
<a class="btn btn-danger quantityunit-delete-button" href="#" role="button" data-quantityunit-id="{{ $quantityunit->id }}" data-quantityunit-name="{{ $quantityunit->name }}">
<i class="fa fa-trash"></i>
</a>
</td>
<td>
{{ $quantityunit->name }}
</td>
<td>
{{ $quantityunit->description }}
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
<h1 class="page-header">
@yield('title')
<a class="btn btn-default" href="{{ $U('/quantityunit/new') }}" role="button">
<i class="fa fa-plus"></i>&nbsp;{{ $L('Add') }}
</a>
</h1>
<div class="table-responsive">
<table id="quantityunits-table" class="table table-striped">
<thead>
<tr>
<th>#</th>
<th>{{ $L('Name') }}</th>
<th>{{ $L('Description') }}</th>
</tr>
</thead>
<tbody>
@foreach($quantityunits as $quantityunit)
<tr>
<td class="fit-content">
<a class="btn btn-info" href="{{ $U('/quantityunit/') }}{{ $quantityunit->id }}" role="button">
<i class="fa fa-pencil"></i>
</a>
<a class="btn btn-danger quantityunit-delete-button" href="#" role="button" data-quantityunit-id="{{ $quantityunit->id }}" data-quantityunit-name="{{ $quantityunit->name }}">
<i class="fa fa-trash"></i>
</a>
</td>
<td>
{{ $quantityunit->name }}
</td>
<td>
{{ $quantityunit->description }}
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@stop

View File

@@ -5,49 +5,45 @@
@section('viewJsName', 'shoppinglist')
@section('content')
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2">
<h1 class="page-header">
@yield('title')
<a class="btn btn-default" href="{{ $U('/shoppinglistitem/new') }}" role="button">
<i class="fa fa-plus"></i>&nbsp;{{ $L('Add') }}
</a>
<a id="add-products-below-min-stock-amount" class="btn btn-info" href="#" role="button">
<i class="fa fa-plus"></i>&nbsp;{{ $L('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>{{ $L('Product') }} / <em>{{ $L('Note') }}</em></th>
<th>{{ $L('Amount') }}</th>
</tr>
</thead>
<tbody>
@foreach($listItems as $listItem)
<tr class="@if($listItem->amount_autoadded > 0) info-bg @endif">
<td class="fit-content">
<a class="btn btn-info" href="{{ $U('/shoppinglistitem/') }}{{ $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="{{ $listItem->id }}">
<i class="fa fa-trash"></i>
</a>
</td>
<td>
@if(!empty($listItem->product_id)) {{ FindObjectInArrayByPropertyValue($products, 'id', $listItem->product_id)->name }}<br>@endif<em>{{ $listItem->note }}</em>
</td>
<td>
{{ $listItem->amount + $listItem->amount_autoadded }} @if(!empty($listItem->product_id)) {{ FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $listItem->product_id)->qu_id_purchase)->name }}@endif
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
<h1 class="page-header">
@yield('title')
<a class="btn btn-default" href="{{ $U('/shoppinglistitem/new') }}" role="button">
<i class="fa fa-plus"></i>&nbsp;{{ $L('Add') }}
</a>
<a id="add-products-below-min-stock-amount" class="btn btn-info" href="#" role="button">
<i class="fa fa-plus"></i>&nbsp;{{ $L('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>{{ $L('Product') }} / <em>{{ $L('Note') }}</em></th>
<th>{{ $L('Amount') }}</th>
</tr>
</thead>
<tbody>
@foreach($listItems as $listItem)
<tr class="@if($listItem->amount_autoadded > 0) info-bg @endif">
<td class="fit-content">
<a class="btn btn-info" href="{{ $U('/shoppinglistitem/') }}{{ $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="{{ $listItem->id }}">
<i class="fa fa-trash"></i>
</a>
</td>
<td>
@if(!empty($listItem->product_id)) {{ FindObjectInArrayByPropertyValue($products, 'id', $listItem->product_id)->name }}<br>@endif<em>{{ $listItem->note }}</em>
</td>
<td>
{{ $listItem->amount + $listItem->amount_autoadded }} @if(!empty($listItem->product_id)) {{ FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $listItem->product_id)->qu_id_purchase)->name }}@endif
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@stop

View File

@@ -9,8 +9,7 @@
@section('viewJsName', 'shoppinglistform')
@section('content')
<div class="col-sm-3 col-sm-offset-3 col-md-3 col-md-offset-2">
<div class="col-lg-4 col-xs-12">
<h1 class="page-header">@yield('title')</h1>
<script>Grocy.EditMode = '{{ $mode }}';</script>
@@ -46,10 +45,9 @@
<button id="save-shoppinglist-button" type="submit" class="btn btn-default">{{ $L('Save') }}</button>
</form>
</div>
<div class="col-sm-6 col-md-5 col-lg-3">
<div class="col-lg-4 col-xs-12">
@include('components.productcard')
</div>
@stop

View File

@@ -4,48 +4,69 @@
@section('activeNav', 'stockoverview')
@section('viewJsName', 'stockoverview')
@push('pageScripts')
<script src="{{ $U('/bower_components/jquery-ui/jquery-ui.min.js?v=', true) }}{{ $version }}"></script>
@endpush
@section('content')
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2">
<h1 class="page-header">{{ $L('Stock overview') }} <span class="text-muted small">{{ $L('#1 products with #2 units in stock', count($currentStock), SumArrayValue($currentStock, 'amount')) }}</span></h1>
<h1 class="page-header">{{ $L('Stock overview') }} <span class="text-muted small">{{ $L('#1 products with #2 units in stock', count($currentStock), SumArrayValue($currentStock, 'amount')) }}</span></h1>
<div class="container-fluid">
<div class="row">
<p class="btn btn-lg btn-warning no-real-button">{{ $L('#1 products expiring within the next #2 days', count(FindAllObjectsInArrayByPropertyValue($currentStock, 'best_before_date', date('Y-m-d', strtotime('+5 days')), '<')), 5) }}</p>
<p class="btn btn-lg btn-danger no-real-button">{{ $L('#1 products are already expired', count(FindAllObjectsInArrayByPropertyValue($currentStock, 'best_before_date', date('Y-m-d', strtotime('-1 days')), '<'))) }}</p>
<p class="btn btn-lg btn-info no-real-button">{{ $L('#1 products are below defined min. stock amount', count($missingProducts)) }}</p>
</div>
<div class="container-fluid">
<div class="row">
<p class="btn btn-lg btn-warning no-real-button responsive-button">{{ $L('#1 products expiring within the next #2 days', $countExpiringNextXDays, $nextXDays) }}</p>
<p class="btn btn-lg btn-danger no-real-button responsive-button">{{ $L('#1 products are already expired', $countAlreadyExpired) }}</p>
<p class="btn btn-lg btn-info no-real-button responsive-button">{{ $L('#1 products are below defined min. stock amount', count($missingProducts)) }}</p>
</div>
</div>
<div class="discrete-content-separator-2x"></div>
<div class="table-responsive">
<table id="stock-overview-table" class="table table-striped">
<thead>
<tr>
<th>{{ $L('Product') }}</th>
<th>{{ $L('Amount') }}</th>
<th>{{ $L('Next best before date') }}</th>
</tr>
</thead>
<tbody>
@foreach($currentStock as $currentStockEntry)
<tr class="@if($currentStockEntry->best_before_date < date('Y-m-d', strtotime('-1 days'))) error-bg @elseif($currentStockEntry->best_before_date < date('Y-m-d', strtotime('+5 days'))) warning-bg @elseif (FindObjectInArrayByPropertyValue($missingProducts, 'id', $currentStockEntry->product_id) !== null) info-bg @endif">
<td>
{{ FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name }}
</td>
<td>
{{ $currentStockEntry->amount . ' ' . FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_stock)->name }}
</td>
<td>
{{ $currentStockEntry->best_before_date }}
<time class="timeago timeago-contextual" datetime="{{ $currentStockEntry->best_before_date }}"></time>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
<div class="discrete-content-separator-2x"></div>
<div class="table-responsive">
<table id="stock-overview-table" class="table table-striped">
<thead>
<tr>
<th>#</th>
<th>{{ $L('Product') }}</th>
<th>{{ $L('Amount') }}</th>
<th>{{ $L('Next best before date') }}</th>
<th class="hidden">Hidden location</th>
</tr>
</thead>
<tbody>
@foreach($currentStock as $currentStockEntry)
<tr id="product-{{ $currentStockEntry->product_id }}-row" class="@if($currentStockEntry->best_before_date < date('Y-m-d', strtotime('-1 days'))) error-bg @elseif($currentStockEntry->best_before_date < date('Y-m-d', strtotime('+5 days'))) warning-bg @elseif (FindObjectInArrayByPropertyValue($missingProducts, 'id', $currentStockEntry->product_id) !== null) info-bg @endif">
<td class="fit-content">
<a class="btn btn-success btn-xs product-consume-button" href="#" title="{{ $L('Consume #3 #1 of #2', FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_stock)->name, FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name, 1) }}"
data-product-id="{{ $currentStockEntry->product_id }}"
data-product-name="{{ FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name }}"
data-product-qu-name="{{ FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_stock)->name }}"
data-consume-amount="1">
<i class="fa fa-cutlery"></i> 1
</a>
<a id="product-{{ $currentStockEntry->product_id }}-consume-all-button" class="btn btn-danger btn-xs product-consume-button" href="#" title="{{ $L('Consume all #1 which are currently in stock', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name) }}"
data-product-id="{{ $currentStockEntry->product_id }}"
data-product-name="{{ FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name }}"
data-product-qu-name="{{ FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_stock)->name }}"
data-consume-amount="{{ $currentStockEntry->amount }}">
<i class="fa fa-cutlery"></i> {{ $L('All') }}
</a>
</td>
<td>
{{ FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name }}
</td>
<td>
<span id="product-{{ $currentStockEntry->product_id }}-amount">{{ $currentStockEntry->amount }}</span> {{ FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_stock)->name }}
</td>
<td>
{{ $currentStockEntry->best_before_date }}
<time class="timeago timeago-contextual" datetime="{{ $currentStockEntry->best_before_date }}"></time>
</td>
<td class="hidden">
{{ FindObjectInArrayByPropertyValue($locations, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->location_id)->name }}
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@stop