Compare commits

...

33 Commits

Author SHA1 Message Date
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
Bernd Bestel
4853174d03 Validate all API request as the API is now open for third parties (references #5) 2018-04-22 14:25:08 +02:00
Bernd Bestel
538d789366 Don't expose stock entity directly via API 2018-04-21 21:33:03 +02:00
Bernd Bestel
0751919b82 Add API & data model documentation hint 2018-04-21 20:13:47 +02:00
Bernd Bestel
99b2a84667 Finish API documentation and token auth (references #5) 2018-04-21 19:18:00 +02:00
Bernd Bestel
9bd6aac09c Start working on API documentation and token auth (references #5) 2018-04-20 23:09:18 +02:00
Bernd Bestel
7be35a90c1 Small style changes 2018-04-20 15:21:29 +02:00
Bernd Bestel
eae5b8bad9 Revise session handling to prepare API authentication via token 2018-04-20 11:34:53 +02:00
Bernd Bestel
0c85342404 Updated Composer dependencies 2018-04-20 09:48:06 +02:00
Bernd Bestel
9ddcdb3ab2 Fixed login form didn't respect the configured BASE_URL 2018-04-18 22:38:05 +02:00
Bernd Bestel
1c537cf5da Added some missing translations 2018-04-18 19:37:36 +02:00
Bernd Bestel
607a90cccc Use absolute URLs everywhere, this should fix #3 2018-04-18 19:03:39 +02:00
Bernd Bestel
3d1c6fc5f0 Added update instructions 2018-04-18 07:59:13 +02:00
Bernd Bestel
df1d3677e8 Fixed wrong format info 2018-04-17 20:10:19 +02:00
Bernd Bestel
4da2ac9b35 Typo... 2018-04-17 20:02:09 +02:00
Bernd Bestel
b4ae7d8538 Added some notable things to README 2018-04-17 20:00:00 +02:00
Bernd Bestel
870b679e0e Updated screenshots 2018-04-16 19:31:31 +02:00
103 changed files with 3882 additions and 967 deletions

View File

@@ -1,22 +1,63 @@
# grocy
ERP beyond your fridge
## Give it a try
Public demo of the latest version → [https://demo.grocy.info](https://demo.grocy.info)
## Motivation
A household needs to be managed. I did this so far (almost 10 years) with my first self written software (a C# windows forms application) and with a bunch of Excel sheets. The software is a pain to use and Excel is Excel. So I searched for and tried different things for a (very) long time, nothing 100 % fitted, so this is my aim for a "complete houshold management"-thing.
## What it is about
For now my main focus is on stock management, ERP your fridge!
# Give it a try
Public demo of the latest version → [https://demo.grocy.info](https://demo.grocy.info)
## How to install
Just unpack the [latest release](https://github.com/berrnd/grocy/releases/latest) on your PHP (7.0 or later required) enabled webserver (root is 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. Alternatively clone this repository and install Composer and Bower dependencies manually.
Just unpack the [latest release](https://github.com/berrnd/grocy/releases/latest) on your PHP (currently only tested with PHP 7.2) enabled webserver (webservers root should point to the `/public` directory), copy `config-dist.php` to `data/config.php`, edit it to your needs, ensure that the `data` directory is writable and you're ready to go.
Default login is user `admin` with password `admin` - see the `data/config.php` file. Alternatively clone this repository and install Composer and Bower dependencies manually.
If you use nginx as your webserver, please include `try_files $uri /index.php;` in your location block.
## Notes about barcode readers
Some fields also allow to select a value by scanning a barcode. It works best when your barcode reader prefixes every barcode with a letter this is normally not part of a item name (I use a `$`) and sends a `TAB` after a scan.
If, however, your webserver does not support URL rewriting, set `DISABLE_URL_REWRITING` in `data/config.php`.
## How to update
Just overwrite everything with the latest release while keeping the `/data` directory, check `config-dist.php` for new configuration options and add them to your `data/config.php` (it will show up as an error if something is missing there).
## Things worth to know
### REST API & data model documentation
See the integrated Swagger UI instance on [/api](https://demo-en.grocy.info/api).
### Barcode readers
Some fields also allow to select a value by scanning a barcode. It works best when your barcode reader prefixes every barcode with a letter which is normally not part of a item name (I use a `$`) and sends a `TAB` after a scan.
### Input shorthands for date fields
For (productivity) reasons all date (and time) input fields use the ISO-8601 format regardless of localization.
The following shorthands are available:
- `MMDD` gets expanded to the given day on the current year in proper notation
- Example: `0517` will be converted to `2018-05-17`
- `YYYYMMDD` gets expanded to the proper ISO-8601 notation
- Example: `20190417` will be converted to `2019-04-17`
- `x` gets expanded to `2999-12-31` (which I use for products which never expire)
- Down/up arrow keys will increase/decrease the date by one day
- Right/left arrow keys will increase/decrease the date by 1 week
### Keyboard shorthands for buttons
Wherever a button contains a bold highlighted letter, this is a shortcut key.
Example: Button "Add as new **p**roduct" can be "pressed" by using the `P` key on your keyboard.
### Barcode lookup via external services
Products can be directly added to the database via looking them up against external services by a barcode.
This is currently only possible through the REST API.
There is no plugin included for any service, see the reference implementation in `data/plugins/DemoBarcodeLookupPlugin.php`.
### Database migrations
Database schema migration is automatically done when visiting the root (`/`) route (click on the logo in the left upper edge).
### Demo mode
When the file `data/demo.txt` exists, the application will work in a demo mode which means authentication is disabled and some demo data will be generated during the database schema migration.
### Other things
When the file `data/add_before_end_body.html` exists, the contents of the file be added just before `</body>` on every page, useful for your own JS/CSS without to have to modify the application itself.
## Screenshots
#### Dashboard

52
app.php
View File

@@ -3,42 +3,42 @@
use \Psr\Http\Message\ServerRequestInterface as Request;
use \Psr\Http\Message\ResponseInterface as Response;
use \Grocy\Middleware\SessionAuthMiddleware;
use \Grocy\Services\ApplicationService;
use \Grocy\Helpers\UrlManager;
use \Grocy\Controllers\LoginController;
require_once __DIR__ . '/vendor/autoload.php';
require_once __DIR__ . '/data/config.php';
require_once __DIR__ . '/extensions.php';
// Setup base application
if (PHP_SAPI !== 'cli')
{
$appContainer = new \Slim\Container([
'settings' => [
'displayErrorDetails' => true,
'determineRouteBeforeAppMiddleware' => true
],
'view' => function($container)
{
return new \Slim\Views\Blade(__DIR__ . '/views', __DIR__ . '/data/viewcache');
}
]);
$appContainer = new \Slim\Container([
'settings' => [
'displayErrorDetails' => true,
'determineRouteBeforeAppMiddleware' => true
],
'view' => function($container)
{
return new \Slim\Views\Blade(__DIR__ . '/views', __DIR__ . '/data/viewcache');
},
'LoginControllerInstance' => function($container)
{
return new LoginController($container, 'grocy_session');
},
'UrlManager' => function($container)
{
return new UrlManager(BASE_URL);
},
'ApiKeyHeaderName' => function($container)
{
return 'GROCY-API-KEY';
}
]);
$app = new \Slim\App($appContainer);
$app = new \Slim\App($appContainer);
}
else
if (PHP_SAPI === 'cli')
{
$app = new \Slim\App();
$app->add(\pavlakis\cli\CliRequest::class);
}
// Add session handling if this is not a demo installation
$applicationService = new ApplicationService();
if (!$applicationService->IsDemoInstallation())
{
$app->add(SessionAuthMiddleware::class);
}
require_once __DIR__ . '/routes.php';
$app->run();

View File

@@ -17,6 +17,9 @@
"jquery-timeago": "^1.6.1",
"toastr": "^2.1.3",
"tagmanager": "^3.0.2",
"eonasdan-bootstrap-datetimepicker": "^4.17.47"
"eonasdan-bootstrap-datetimepicker": "^4.17.47",
"swagger-ui": "^3.13.4",
"jquery-ui": "^1.12.1",
"bootstrap-side-navbar": "^1.0.1"
}
}

View File

@@ -1,15 +1,21 @@
{
"require": {
"php": ">=7.2",
"slim/slim": "^3.8",
"morris/lessql": "^0.3.4",
"pavlakis/slim-cli": "^1.0",
"rubellum/slim-blade-view": "^0.1.1"
"rubellum/slim-blade-view": "^0.1.1",
"tuupola/cors-middleware": "^0.7.0"
},
"autoload": {
"psr-4": {
"Grocy\\Services\\": "services/",
"Grocy\\Controllers\\": "controllers/",
"Grocy\\Middleware\\": "middleware/"
}
"Grocy\\Middleware\\": "middleware/",
"Grocy\\Helpers\\": "helpers/"
},
"files": [
"helpers/extensions.php"
]
}
}

430
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
"content-hash": "12ebab60e283dfdab831d1cc22430d05",
"content-hash": "42031c0b205b7ce7efb4b6eb95a0096a",
"packages": [
{
"name": "container-interop/container-interop",
@@ -104,9 +104,61 @@
],
"time": "2018-01-09T20:05:19+00:00"
},
{
"name": "http-interop/http-factory",
"version": "0.3.0",
"source": {
"type": "git",
"url": "https://github.com/http-interop/http-factory.git",
"reference": "c2587cc0a6f74987fefb5b8074acfd32c69a4b0f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/http-interop/http-factory/zipball/c2587cc0a6f74987fefb5b8074acfd32c69a4b0f",
"reference": "c2587cc0a6f74987fefb5b8074acfd32c69a4b0f",
"shasum": ""
},
"require": {
"php": ">=5.3.0",
"psr/http-message": "^1.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Interop\\Http\\Factory\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "http://www.php-fig.org/"
}
],
"description": "Common interface for HTTP message factories",
"keywords": [
"factory",
"http",
"message",
"psr",
"psr-17",
"psr-7",
"request",
"response"
],
"time": "2017-03-24T14:48:51+00:00"
},
{
"name": "illuminate/container",
"version": "v5.6.16",
"version": "v5.6.17",
"source": {
"type": "git",
"url": "https://github.com/illuminate/container.git",
@@ -150,7 +202,7 @@
},
{
"name": "illuminate/contracts",
"version": "v5.6.16",
"version": "v5.6.17",
"source": {
"type": "git",
"url": "https://github.com/illuminate/contracts.git",
@@ -194,7 +246,7 @@
},
{
"name": "illuminate/events",
"version": "v5.6.16",
"version": "v5.6.17",
"source": {
"type": "git",
"url": "https://github.com/illuminate/events.git",
@@ -239,7 +291,7 @@
},
{
"name": "illuminate/filesystem",
"version": "v5.6.16",
"version": "v5.6.17",
"source": {
"type": "git",
"url": "https://github.com/illuminate/filesystem.git",
@@ -291,16 +343,16 @@
},
{
"name": "illuminate/support",
"version": "v5.6.16",
"version": "v5.6.17",
"source": {
"type": "git",
"url": "https://github.com/illuminate/support.git",
"reference": "fad0669f858423679497a17f973261cc32f9a5a8"
"reference": "cc8d6f5cef3a901de6bb7d1b362102a6db001085"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/illuminate/support/zipball/fad0669f858423679497a17f973261cc32f9a5a8",
"reference": "fad0669f858423679497a17f973261cc32f9a5a8",
"url": "https://api.github.com/repos/illuminate/support/zipball/cc8d6f5cef3a901de6bb7d1b362102a6db001085",
"reference": "cc8d6f5cef3a901de6bb7d1b362102a6db001085",
"shasum": ""
},
"require": {
@@ -344,11 +396,11 @@
],
"description": "The Illuminate Support package.",
"homepage": "https://laravel.com",
"time": "2018-04-05T21:19:22+00:00"
"time": "2018-04-17T12:26:47+00:00"
},
{
"name": "illuminate/view",
"version": "v5.6.16",
"version": "v5.6.17",
"source": {
"type": "git",
"url": "https://github.com/illuminate/view.git",
@@ -443,17 +495,72 @@
"time": "2018-01-27T13:18:21+00:00"
},
{
"name": "nesbot/carbon",
"version": "1.25.0",
"name": "neomerx/cors-psr7",
"version": "v1.0.12",
"source": {
"type": "git",
"url": "https://github.com/briannesbitt/Carbon.git",
"reference": "cbcf13da0b531767e39eb86e9687f5deba9857b4"
"url": "https://github.com/neomerx/cors-psr7.git",
"reference": "24944f39483d1a89f66ae9d58cca9f82b8815b35"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/cbcf13da0b531767e39eb86e9687f5deba9857b4",
"reference": "cbcf13da0b531767e39eb86e9687f5deba9857b4",
"url": "https://api.github.com/repos/neomerx/cors-psr7/zipball/24944f39483d1a89f66ae9d58cca9f82b8815b35",
"reference": "24944f39483d1a89f66ae9d58cca9f82b8815b35",
"shasum": ""
},
"require": {
"php": ">=5.6.0",
"psr/http-message": "^1.0",
"psr/log": "^1.0"
},
"require-dev": {
"mockery/mockery": "^0.9.9",
"phpunit/phpunit": "^5.7",
"scrutinizer/ocular": "^1.1",
"squizlabs/php_codesniffer": "^3.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Neomerx\\Cors\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"authors": [
{
"name": "neomerx",
"email": "info@neomerx.com"
}
],
"description": "Framework agnostic (PSR-7) CORS implementation (www.w3.org/TR/cors/)",
"homepage": "https://github.com/neomerx/cors-psr7",
"keywords": [
"Cross Origin Resource Sharing",
"Cross-Origin Resource Sharing",
"cors",
"neomerx",
"psr-7",
"psr7",
"w3.org",
"www.w3.org"
],
"time": "2017-09-03T22:31:57+00:00"
},
{
"name": "nesbot/carbon",
"version": "1.26.4",
"source": {
"type": "git",
"url": "https://github.com/briannesbitt/Carbon.git",
"reference": "e3d9014279133a3cccc01f6a691322a2d5a6a87b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/e3d9014279133a3cccc01f6a691322a2d5a6a87b",
"reference": "e3d9014279133a3cccc01f6a691322a2d5a6a87b",
"shasum": ""
},
"require": {
@@ -465,14 +572,9 @@
"phpunit/phpunit": "^4.8.35 || ^5.7"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.23-dev"
}
},
"autoload": {
"psr-4": {
"Carbon\\": "src/Carbon/"
"": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
@@ -493,7 +595,7 @@
"datetime",
"time"
],
"time": "2018-03-19T15:50:49+00:00"
"time": "2018-04-17T15:35:42+00:00"
},
{
"name": "nikic/fast-route",
@@ -780,6 +882,112 @@
],
"time": "2016-08-06T14:39:51+00:00"
},
{
"name": "psr/http-server-handler",
"version": "1.0.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-server-handler.git",
"reference": "439d92054dc06097f2406ec074a2627839955a02"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/http-server-handler/zipball/439d92054dc06097f2406ec074a2627839955a02",
"reference": "439d92054dc06097f2406ec074a2627839955a02",
"shasum": ""
},
"require": {
"php": ">=7.0",
"psr/http-message": "^1.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Http\\Server\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "http://www.php-fig.org/"
}
],
"description": "Common interface for HTTP server-side request handler",
"keywords": [
"handler",
"http",
"http-interop",
"psr",
"psr-15",
"psr-7",
"request",
"response",
"server"
],
"time": "2018-01-22T17:04:15+00:00"
},
{
"name": "psr/http-server-middleware",
"version": "1.0.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-server-middleware.git",
"reference": "ea17eb1fb2c8df6db919cc578451a8013c6a0ae5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/http-server-middleware/zipball/ea17eb1fb2c8df6db919cc578451a8013c6a0ae5",
"reference": "ea17eb1fb2c8df6db919cc578451a8013c6a0ae5",
"shasum": ""
},
"require": {
"php": ">=7.0",
"psr/http-message": "^1.0",
"psr/http-server-handler": "^1.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Http\\Server\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "http://www.php-fig.org/"
}
],
"description": "Common interface for HTTP server-side middleware",
"keywords": [
"http",
"http-interop",
"middleware",
"psr",
"psr-15",
"psr-7",
"request",
"response"
],
"time": "2018-01-22T17:08:31+00:00"
},
{
"name": "psr/log",
"version": "1.0.2",
@@ -927,16 +1135,16 @@
},
{
"name": "slim/slim",
"version": "3.9.2",
"version": "3.10.0",
"source": {
"type": "git",
"url": "https://github.com/slimphp/Slim.git",
"reference": "4086d0106cf5a7135c69fce4161fe355a8feb118"
"reference": "d8aabeacc3688b25e2f2dd2db91df91ec6fdd748"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/slimphp/Slim/zipball/4086d0106cf5a7135c69fce4161fe355a8feb118",
"reference": "4086d0106cf5a7135c69fce4161fe355a8feb118",
"url": "https://api.github.com/repos/slimphp/Slim/zipball/d8aabeacc3688b25e2f2dd2db91df91ec6fdd748",
"reference": "d8aabeacc3688b25e2f2dd2db91df91ec6fdd748",
"shasum": ""
},
"require": {
@@ -994,7 +1202,7 @@
"micro",
"router"
],
"time": "2017-11-26T19:13:09+00:00"
"time": "2018-04-19T19:29:08+00:00"
},
{
"name": "symfony/debug",
@@ -1227,6 +1435,166 @@
"description": "Symfony Translation Component",
"homepage": "https://symfony.com",
"time": "2018-02-22T10:50:29+00:00"
},
{
"name": "tuupola/callable-handler",
"version": "0.3.0",
"source": {
"type": "git",
"url": "https://github.com/tuupola/callable-handler.git",
"reference": "5141efa1e974687a3fa53338811a988198f50662"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/tuupola/callable-handler/zipball/5141efa1e974687a3fa53338811a988198f50662",
"reference": "5141efa1e974687a3fa53338811a988198f50662",
"shasum": ""
},
"require": {
"php": "^7.0",
"psr/http-server-middleware": "^1.0"
},
"require-dev": {
"codedungeon/phpunit-result-printer": "^0.4.4",
"overtrue/phplint": "^1.0",
"phpunit/phpunit": "^6.5",
"squizlabs/php_codesniffer": "^3.2",
"tuupola/http-factory": "^0.3.0",
"zendframework/zend-diactoros": "^1.6"
},
"type": "library",
"autoload": {
"psr-4": {
"Tuupola\\Middleware\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Mika Tuupola",
"email": "tuupola@appelsiini.net",
"homepage": "https://appelsiini.net/",
"role": "Developer"
}
],
"description": "Compatibility layer for PSR-7 double pass and PSR-15 middlewares.",
"homepage": "https://github.com/tuupola/callable-handler",
"keywords": [
"middleware",
"psr-15",
"psr-7"
],
"time": "2018-01-23T04:07:25+00:00"
},
{
"name": "tuupola/cors-middleware",
"version": "0.7.0",
"source": {
"type": "git",
"url": "https://github.com/tuupola/cors-middleware.git",
"reference": "b0e2b7acacf22acae6ba029ee424fd6c073bb443"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/tuupola/cors-middleware/zipball/b0e2b7acacf22acae6ba029ee424fd6c073bb443",
"reference": "b0e2b7acacf22acae6ba029ee424fd6c073bb443",
"shasum": ""
},
"require": {
"neomerx/cors-psr7": "^1.0",
"php": "^7.1",
"psr/http-server-middleware": "^1.0",
"tuupola/callable-handler": "^0.3.0",
"tuupola/http-factory": "^0.3.0"
},
"require-dev": {
"codedungeon/phpunit-result-printer": "^0.4.4",
"equip/dispatch": "dev-approved-psr15 as 1.0.x-dev",
"overtrue/phplint": "^1.0",
"phpunit/phpunit": "^6.5",
"squizlabs/php_codesniffer": "^3.2",
"zendframework/zend-diactoros": "^1.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Tuupola\\Middleware\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Mika Tuupola",
"email": "tuupola@appelsiini.net",
"homepage": "http://www.appelsiini.net/",
"role": "Developer"
}
],
"description": "PSR-7 and PSR-15 CORS middleware",
"homepage": "https://github.com/tuupola/cors-middleware",
"keywords": [
"cors",
"middleware",
"psr-15",
"psr-7"
],
"time": "2018-01-25T02:29:07+00:00"
},
{
"name": "tuupola/http-factory",
"version": "0.3.0",
"source": {
"type": "git",
"url": "https://github.com/tuupola/http-factory.git",
"reference": "57b2e19ff3f4af0bbee4e31fd282689be351f1ad"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/tuupola/http-factory/zipball/57b2e19ff3f4af0bbee4e31fd282689be351f1ad",
"reference": "57b2e19ff3f4af0bbee4e31fd282689be351f1ad",
"shasum": ""
},
"require": {
"http-interop/http-factory": "^0.3.0"
},
"require-dev": {
"http-interop/http-factory-tests": "^0.3.0",
"overtrue/phplint": "^0.2.1",
"phpunit/phpunit": "^5.7",
"squizlabs/php_codesniffer": "^3.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Tuupola\\Http\\Factory\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Mika Tuupola",
"email": "tuupola@appelsiini.net",
"homepage": "http://www.appelsiini.net/",
"role": "Developer"
}
],
"description": "Lightweight autodiscovering PSR-17 HTTP factories",
"homepage": "https://github.com/tuupola/http-factory",
"keywords": [
"http",
"psr-17",
"psr-7"
],
"time": "2017-07-15T22:03:15+00:00"
}
],
"packages-dev": [],
@@ -1235,6 +1603,8 @@
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": [],
"platform": {
"php": ">=7.0"
},
"platform-dev": []
}

View File

@@ -1,7 +1,26 @@
<?php
define('MODE', 'production'); # Either "production" or "dev"
define('CULTURE', 'en');
# Login credentials
define('HTTP_USER', 'admin');
define('HTTP_PASSWORD', 'admin');
# Either "production" or "dev"
define('MODE', 'production');
# Either "en" or "de" or the filename (without extension) of
# one of the other available localization files in the "/localization" directory
define('CULTURE', 'en');
# The base url of your installation,
# should be just "/" when running directly under the root of a (sub)domain
# or for example "https:/example.com/grocy" when using a subdirectory
define('BASE_URL', '/');
# The plugin to use for external barcode lookups,
# must be the filename without .php extension and must be located in /data/plugins,
# see /data/plugins/DemoBarcodeLookupPlugin.php for an example implementation
define('STOCK_BARCODE_LOOKUP_PLUGIN', 'DemoBarcodeLookupPlugin');
# If, however, your webserver does not support URL rewriting,
# set this to true
define('DISABLE_URL_REWRITING', false);

View File

@@ -4,8 +4,25 @@ namespace Grocy\Controllers;
class BaseApiController extends BaseController
{
protected function ApiResponse($response)
public function __construct(\Slim\Container $container)
{
return json_encode($response);
parent::__construct($container);
$this->OpenApiSpec = json_decode(file_get_contents(__DIR__ . '/../grocy.openapi.json'));
}
protected $OpenApiSpec;
protected function ApiResponse($data)
{
return json_encode($data);
}
protected function VoidApiActionResponse($response, $success = true, $status = 200, $errorMessage = '')
{
return $response->withStatus($status)->withJson(array(
'success' => $success,
'error_message' => $errorMessage
));
}
}

View File

@@ -9,13 +9,13 @@ use \Grocy\Services\LocalizationService;
class BaseController
{
public function __construct(\Slim\Container $container) {
$this->AppContainer = $container;
$databaseService = new DatabaseService();
$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());
@@ -23,6 +23,12 @@ class BaseController
{
return $localizationService->Localize($text, ...$placeholderValues);
});
$container->view->set('U', function($relativePath, $isResource = false) use($container)
{
return $container->UrlManager->ConstructUrl($relativePath, $isResource);
});
$this->AppContainer = $container;
}
protected $AppContainer;

View File

@@ -22,11 +22,26 @@ class BatteriesApiController extends BaseApiController
$trackedTime = $request->getQueryParams()['tracked_time'];
}
return $this->ApiResponse(array('success' => $this->BatteriesService->TrackChargeCycle($args['batteryId'], $trackedTime)));
try
{
$this->BatteriesService->TrackChargeCycle($args['batteryId'], $trackedTime);
return $this->VoidApiActionResponse($response);
}
catch (\Exception $ex)
{
return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage());
}
}
public function BatteryDetails(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
return $this->ApiResponse($this->BatteriesService->GetBatteryDetails($args['batteryId']));
try
{
return $this->ApiResponse($this->BatteriesService->GetBatteryDetails($args['batteryId']));
}
catch (\Exception $ex)
{
return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage());
}
}
}

View File

@@ -22,10 +22,16 @@ 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(),
'current' => $this->BatteriesService->GetCurrent(),
'nextChargeTimes' => $nextChargeTimes
'nextChargeTimes' => $nextChargeTimes,
'nextXDays' => $nextXDays,
'countDueNextXDays' => $countDueNextXDays - $countOverdue,
'countOverdue' => $countOverdue
]);
}

View File

@@ -6,35 +6,75 @@ class GenericEntityApiController extends BaseApiController
{
public function GetObjects(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
return $this->ApiResponse($this->Database->{$args['entity']}());
if ($this->IsValidEntity($args['entity']))
{
return $this->ApiResponse($this->Database->{$args['entity']}());
}
else
{
return $this->VoidApiActionResponse($response, false, 400, 'Entity does not exist or is not exposed');
}
}
public function GetObject(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
return $this->ApiResponse($this->Database->{$args['entity']}($args['objectId']));
if ($this->IsValidEntity($args['entity']))
{
return $this->ApiResponse($this->Database->{$args['entity']}($args['objectId']));
}
else
{
return $this->VoidApiActionResponse($response, false, 400, 'Entity does not exist or is not exposed');
}
}
public function AddObject(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
$newRow = $this->Database->{$args['entity']}()->createRow($request->getParsedBody());
$newRow->save();
$success = $newRow->isClean();
return $this->ApiResponse(array('success' => $success));
if ($this->IsValidEntity($args['entity']))
{
$newRow = $this->Database->{$args['entity']}()->createRow($request->getParsedBody());
$newRow->save();
$success = $newRow->isClean();
return $this->ApiResponse(array('success' => $success));
}
else
{
return $this->VoidApiActionResponse($response, false, 400, 'Entity does not exist or is not exposed');
}
}
public function EditObject(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
$row = $this->Database->{$args['entity']}($args['objectId']);
$row->update($request->getParsedBody());
$success = $row->isClean();
return $this->ApiResponse(array('success' => $success));
if ($this->IsValidEntity($args['entity']))
{
$row = $this->Database->{$args['entity']}($args['objectId']);
$row->update($request->getParsedBody());
$success = $row->isClean();
return $this->ApiResponse(array('success' => $success));
}
else
{
return $this->VoidApiActionResponse($response, false, 400, 'Entity does not exist or is not exposed');
}
}
public function DeleteObject(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
$row = $this->Database->{$args['entity']}($args['objectId']);
$row->delete();
$success = $row->isClean();
return $this->ApiResponse(array('success' => $success));
if ($this->IsValidEntity($args['entity']))
{
$row = $this->Database->{$args['entity']}($args['objectId']);
$row->delete();
$success = $row->isClean();
return $this->ApiResponse(array('success' => $success));
}
else
{
return $this->VoidApiActionResponse($response, false, 400, 'Entity does not exist or is not exposed');
}
}
private function IsValidEntity($entity)
{
return in_array($entity, $this->OpenApiSpec->components->internalSchemas->ExposedEntity->enum);
}
}

View File

@@ -22,11 +22,26 @@ class HabitsApiController extends BaseApiController
$trackedTime = $request->getQueryParams()['tracked_time'];
}
return $this->ApiResponse(array('success' => $this->HabitsService->TrackHabit($args['habitId'], $trackedTime)));
try
{
$this->HabitsService->TrackHabit($args['habitId'], $trackedTime);
return $this->VoidApiActionResponse($response);
}
catch (\Exception $ex)
{
return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage());
}
}
public function HabitDetails(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
return $this->ApiResponse($this->HabitsService->GetHabitDetails($args['habitId']));
try
{
return $this->ApiResponse($this->HabitsService->GetHabitDetails($args['habitId']));
}
catch (\Exception $ex)
{
return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage());
}
}
}

View File

@@ -22,10 +22,16 @@ 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(),
'currentHabits' => $this->HabitsService->GetCurrentHabits(),
'nextHabitTimes' => $nextHabitTimes
'nextHabitTimes' => $nextHabitTimes,
'nextXDays' => $nextXDays,
'countDueNextXDays' => $countDueNextXDays - $countOverdue,
'countOverdue' => $countOverdue
]);
}

View File

@@ -9,13 +9,15 @@ use \Grocy\Services\DemoDataGeneratorService;
class LoginController extends BaseController
{
public function __construct(\Slim\Container $container)
public function __construct(\Slim\Container $container, string $sessionCookieName)
{
parent::__construct($container);
$this->SessionService = new SessionService();
$this->SessionCookieName = $sessionCookieName;
}
protected $SessionService;
protected $SessionCookieName;
public function ProcessLogin(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
@@ -25,18 +27,18 @@ class LoginController extends BaseController
if ($postParams['username'] === HTTP_USER && $postParams['password'] === HTTP_PASSWORD)
{
$sessionKey = $this->SessionService->CreateSession();
setcookie('grocy_session', $sessionKey, time() + 31536000); // Cookie expires in 1 year, but session validity is up to SessionService
setcookie($this->SessionCookieName, $sessionKey, time() + 31536000); // Cookie expires in 1 year, but session validity is up to SessionService
return $response->withRedirect('/');
return $response->withRedirect($this->AppContainer->UrlManager->ConstructUrl('/'));
}
else
{
return $response->withRedirect('/login?invalid=true');
return $response->withRedirect($this->AppContainer->UrlManager->ConstructUrl('/login?invalid=true'));
}
}
else
{
return $response->withRedirect('/login?invalid=true');
return $response->withRedirect($this->AppContainer->UrlManager->ConstructUrl('/login?invalid=true'));
}
}
@@ -47,8 +49,8 @@ class LoginController extends BaseController
public function Logout(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
$this->SessionService->RemoveSession($_COOKIE['grocy_session']);
return $response->withRedirect('/');
$this->SessionService->RemoveSession($_COOKIE[$this->SessionCookieName]);
return $response->withRedirect($this->AppContainer->UrlManager->ConstructUrl('/'));
}
public function Root(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
@@ -64,6 +66,11 @@ class LoginController extends BaseController
$demoDataGeneratorService->PopulateDemoData();
}
return $response->withRedirect('/stockoverview');
return $response->withRedirect($this->AppContainer->UrlManager->ConstructUrl('/stockoverview'));
}
public function GetSessionCookieName()
{
return $this->SessionCookieName;
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace Grocy\Controllers;
use \Grocy\Services\ApplicationService;
use \Grocy\Services\ApiKeyService;
class OpenApiController extends BaseApiController
{
public function __construct(\Slim\Container $container)
{
parent::__construct($container);
$this->ApiKeyService = new ApiKeyService();
}
protected $ApiKeyService;
public function DocumentationUi(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
return $this->AppContainer->view->render($response, 'openapiui');
}
public function DocumentationSpec(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
$applicationService = new ApplicationService();
$versionInfo = $applicationService->GetInstalledVersion();
$this->OpenApiSpec->info->version = $versionInfo->Version;
$this->OpenApiSpec->info->description = str_replace('PlaceHolderManageApiKeysUrl', $this->AppContainer->UrlManager->ConstructUrl('/manageapikeys'), $this->OpenApiSpec->info->description);
$this->OpenApiSpec->servers[0]->url = $this->AppContainer->UrlManager->ConstructUrl('/api');
return $this->ApiResponse($this->OpenApiSpec);
}
public function ApiKeysList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
return $this->AppContainer->view->render($response, 'manageapikeys', [
'apiKeys' => $this->Database->api_keys()
]);
}
public function CreateNewApiKey(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
$newApiKey = $this->ApiKeyService->CreateApiKey();
$newApiKeyId = $this->ApiKeyService->GetApiKeyId($newApiKey);
return $response->withRedirect($this->AppContainer->UrlManager->ConstructUrl("/manageapikeys?CreatedApiKeyId=$newApiKeyId"));
}
}

View File

@@ -16,7 +16,14 @@ class StockApiController extends BaseApiController
public function ProductDetails(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
return $this->ApiResponse($this->StockService->GetProductDetails($args['productId']));
try
{
return $this->ApiResponse($this->StockService->GetProductDetails($args['productId']));
}
catch (\Exception $ex)
{
return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage());
}
}
public function AddProduct(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
@@ -33,7 +40,15 @@ class StockApiController extends BaseApiController
$transactionType = $request->getQueryParams()['transactiontype'];
}
return $this->ApiResponse(array('success' => $this->StockService->AddProduct($args['productId'], $args['amount'], $bestBeforeDate, $transactionType)));
try
{
$this->StockService->AddProduct($args['productId'], $args['amount'], $bestBeforeDate, $transactionType);
return $this->VoidApiActionResponse($response);
}
catch (\Exception $ex)
{
return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage());
}
}
public function ConsumeProduct(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
@@ -50,7 +65,15 @@ class StockApiController extends BaseApiController
$transactionType = $request->getQueryParams()['transactiontype'];
}
return $this->ApiResponse(array('success' => $this->StockService->ConsumeProduct($args['productId'], $args['amount'], $spoiled, $transactionType)));
try
{
$this->StockService->ConsumeProduct($args['productId'], $args['amount'], $spoiled, $transactionType);
return $this->VoidApiActionResponse($response);
}
catch (\Exception $ex)
{
return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage());
}
}
public function InventoryProduct(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
@@ -61,7 +84,15 @@ class StockApiController extends BaseApiController
$bestBeforeDate = $request->getQueryParams()['bestbeforedate'];
}
return $this->ApiResponse(array('success' => $this->StockService->InventoryProduct($args['productId'], $args['newAmount'], $bestBeforeDate)));
try
{
$this->StockService->InventoryProduct($args['productId'], $args['newAmount'], $bestBeforeDate);
return $this->VoidApiActionResponse($response);
}
catch (\Exception $ex)
{
return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage());
}
}
public function CurrentStock(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
@@ -69,9 +100,27 @@ class StockApiController extends BaseApiController
return $this->ApiResponse($this->StockService->GetCurrentStock());
}
public function AddmissingProductsToShoppingList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
public function AddMissingProductsToShoppingList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
$this->StockService->AddMissingProductsToShoppingList();
return $this->ApiResponse(array('success' => true));
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,11 +17,18 @@ 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()
'currentStock' => $currentStock,
'missingProducts' => $this->StockService->GetMissingProducts(),
'nextXDays' => $nextXDays,
'countExpiringNextXDays' => $countExpiringNextXDays,
'countAlreadyExpired' => $countAlreadyExpired
]);
}

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
);
}
}
}

1384
grocy.openapi.json Normal file

File diff suppressed because it is too large Load Diff

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;
}
}

37
helpers/UrlManager.php Normal file
View File

@@ -0,0 +1,37 @@
<?php
namespace Grocy\Helpers;
class UrlManager
{
public function __construct(string $basePath)
{
if ($basePath === '/')
{
$this->BasePath = $this->GetBaseUrl();
}
else
{
$this->BasePath = $basePath;
}
}
protected $BasePath;
public function ConstructUrl($relativePath, $isResource = false)
{
if (DISABLE_URL_REWRITING === false || $isResource === true)
{
return rtrim($this->BasePath, '/') . $relativePath;
}
else // Is not a resource and URL rewriting is disabled
{
return rtrim($this->BasePath, '/') . '/index.php' . $relativePath;
}
}
private function GetBaseUrl()
{
return (isset($_SERVER['HTTPS']) ? "https" : "http") . "://$_SERVER[HTTP_HOST]";
}
}

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;
@@ -61,3 +93,20 @@ function GetClassConstants($className)
$r = new ReflectionClass($className);
return $r->getConstants();
}
function RandomString($length, $allowedChars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ')
{
$randomString = '';
for ($i = 0; $i < $length; $i++)
{
$randomString .= $allowedChars[rand(0, strlen($allowedChars) - 1)];
}
return $randomString;
}
function IsAssociativeArray(array $array)
{
$keys = array_keys($array);
return array_keys($keys) !== $keys;
}

View File

@@ -88,6 +88,47 @@ return array(
'Username' => 'Benutzername',
'Password' => 'Passwort',
'Invalid credentials, please try again' => 'Ungültige Zugangsdaten, bitte versuche es erneut',
'Are you sure to delete battery "#1"?' => 'Battery "#1" wirklich löschen?',
'Yes' => 'Ja',
'No' => 'Nein',
'Are you sure to delete habit "#1"?' => 'Gewohnheit "#1" wirklich löschen?',
'"#1" could not be resolved to a product, how do you want to proceed?' => '"#1" konnte nicht zu einem Produkt aufgelöst werden, wie möchtest du weiter machen?',
'Create or assign product' => 'Produkt erstellen oder verknüpfen',
'Cancel' => 'Abbrechen',
'Add as new product' => 'Als neues Produkt hinzufügen',
'Add as barcode to existing product' => 'Barcode vorhandenem Produkt zuweisen',
'Add as new product and prefill barcode' => 'Neues Produkt erstellen und Barcode vorbelegen',
'Are you sure to delete quantity unit "#1"?' => 'Mengeneinheit "#1" wirklich löschen?',
'Are you sure to delete product "#1"?' => 'Produkt "#1" wirklich löschen?',
'Are you sure to delete location "#1"?' => 'Standort "#1" wirklich löschen?',
'Manage API keys' => 'API-Keys verwalten',
'REST API & data model documentation' => 'REST-API & Datenmodell Dokumentation',
'API keys' => 'API-Keys',
'Create new API key' => 'Neuen API-Key erstellen',
'API key' => 'API-Key',
'Expires' => 'Läuft ab',
'Created' => 'Erstellt',
'This product is not in stock' => 'Dieses Produkt ist nicht vorrätig',
'This means #1 will be added to stock' => 'Das bedeutet #1 wird dem Bestand hinzugefügt',
'This means #1 will be removed from stock' => 'Das bedeutet #1 wird aus dem Bestand entfernt',
'This means it is estimated that a new execution of this habit is tracked #1 days after the last was tracked' => 'Das bedeutet, dass eine erneute Ausführung der Gewohnheit #1 Tage nach der letzten Ausführung geplant wird',
'Removed #1 #2 of #3 from stock' => '#1 #2 #3 aus dem Bestand entfernt',
'About grocy' => 'Über grocy',
'Close' => 'Schließen',
'#1 batteries are due to be charged within the next #2 days' => '#1 Batterien müssen in den nächsten #2 Tagen geladen werden',
'#1 batteries are overdue to be charged' => '#1 Batterien sind überfällig',
'#1 habits are due to be done within the next #2 days' => '#1 Gewohnheiten stehen in den nächsten #2 Tagen an',
'#1 habits are overdue to be done' => '#1 Gewohnheiten sind überfällig',
'Released on' => 'Veröffentlicht am',
'Consume #3 #1 of #2' => 'Verbrauche #3 #1 #2',
'Added #1 #2 of #3 to stock' => '#1 #2 #3 dem Bestand hinzugefügt',
'Stock amount of #1 is now #2 #3' => 'Es sind nun #2 #3 #1 im Bestand',
'Tracked execution of habit #1 on #2' => 'Ausführung von #1 am #2 erfasst',
'Tracked charge cylce of battery #1 on #2' => 'Ladezyklus für Batterie #1 am #2 erfasst',
'Consume all #1 which are currently in stock' => 'Verbrauche den kompletten Bestand von #1',
'All' => 'Alle',
'Track charge cycle of battery #1' => 'Erfasse einen Ladezyklus für Batterie #1',
'Track execution of habit #1' => 'Erfasse eine Ausführung von #1',
//Constants
'manually' => 'Manuell',

View File

@@ -0,0 +1,58 @@
<?php
namespace Grocy\Middleware;
use \Grocy\Services\SessionService;
use \Grocy\Services\ApiKeyService;
class ApiKeyAuthMiddleware extends BaseMiddleware
{
public function __construct(\Slim\Container $container, string $sessionCookieName, string $apiKeyHeaderName)
{
parent::__construct($container);
$this->SessionCookieName = $sessionCookieName;
$this->ApiKeyHeaderName = $apiKeyHeaderName;
}
protected $SessionCookieName;
protected $ApiKeyHeaderName;
public function __invoke(\Slim\Http\Request $request, \Slim\Http\Response $response, callable $next)
{
$route = $request->getAttribute('route');
$routeName = $route->getName();
if ($this->ApplicationService->IsDemoInstallation())
{
$response = $next($request, $response);
}
else
{
$validSession = true;
$validApiKey = true;
$sessionService = new SessionService();
if (!isset($_COOKIE[$this->SessionCookieName]) || !$sessionService->IsValidSession($_COOKIE[$this->SessionCookieName]))
{
$validSession = false;
}
$apiKeyService = new ApiKeyService();
if (!$request->hasHeader($this->ApiKeyHeaderName) || !$apiKeyService->IsValidApiKey($request->getHeaderLine($this->ApiKeyHeaderName)))
{
$validApiKey = false;
}
if (!$validSession && !$validApiKey)
{
$response = $response->withStatus(401);
}
else
{
$response = $next($request, $response);
}
}
return $response;
}
}

View File

@@ -2,11 +2,16 @@
namespace Grocy\Middleware;
use \Grocy\Services\ApplicationService;
class BaseMiddleware
{
public function __construct(\Slim\Container $container) {
$this->container = $container;
public function __construct(\Slim\Container $container)
{
$this->AppContainer = $container;
$this->ApplicationService = new ApplicationService();
}
protected $container;
protected $AppContainer;
protected $ApplicationService;
}

View File

@@ -13,7 +13,7 @@ class CliMiddleware extends BaseMiddleware
}
else
{
$response = $next($request, $response, $next);
$response = $next($request, $response);
return $response->withHeader('Content-Type', 'text/plain');
}
}

View File

@@ -6,7 +6,7 @@ class JsonMiddleware extends BaseMiddleware
{
public function __invoke(\Slim\Http\Request $request, \Slim\Http\Response $response, callable $next)
{
$response = $next($request, $response, $next);
$response = $next($request, $response);
return $response->withHeader('Content-Type', 'application/json');
}
}

View File

@@ -6,24 +6,35 @@ use \Grocy\Services\SessionService;
class SessionAuthMiddleware extends BaseMiddleware
{
public function __construct(\Slim\Container $container, string $sessionCookieName)
{
parent::__construct($container);
$this->SessionCookieName = $sessionCookieName;
}
protected $SessionCookieName;
public function __invoke(\Slim\Http\Request $request, \Slim\Http\Response $response, callable $next)
{
$route = $request->getAttribute('route');
$routeName = $route->getName();
if ($routeName === 'root')
if ($routeName === 'root' || $this->ApplicationService->IsDemoInstallation())
{
define('AUTHENTICATED', $this->ApplicationService->IsDemoInstallation());
$response = $next($request, $response);
}
else
{
$sessionService = new SessionService();
if ((!isset($_COOKIE['grocy_session']) || !$sessionService->IsValidSession($_COOKIE['grocy_session'])) && $routeName !== 'login')
if ((!isset($_COOKIE[$this->SessionCookieName]) || !$sessionService->IsValidSession($_COOKIE[$this->SessionCookieName])) && $routeName !== 'login')
{
$response = $response->withRedirect('/login');
define('AUTHENTICATED', false);
$response = $response->withRedirect($this->AppContainer->UrlManager->ConstructUrl('/login'));
}
else
{
define('AUTHENTICATED', $routeName !== 'login');
$response = $next($request, $response);
}
}

7
migrations/0022.sql Normal file
View File

@@ -0,0 +1,7 @@
CREATE TABLE api_keys (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
api_key TEXT NOT NULL UNIQUE,
expires DATETIME,
last_used DATETIME,
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
)

1
migrations/0023.sql Normal file
View File

@@ -0,0 +1 @@
DELETE FROM sessions

2
migrations/0024.sql Normal file
View File

@@ -0,0 +1,2 @@
ALTER TABLE sessions
ADD COLUMN last_used DATETIME

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,16 +170,15 @@ a.discrete-link:focus {
background-color: #afd9ee !important;
}
.discrete-content-separator {
padding-top: 5px;
padding-bottom: 5px;
#toast-container > div {
opacity: 1;
filter: alpha(opacity=100);
}
.discrete-content-separator-2x {
padding-top: 10px;
padding-bottom: 10px;
.toast-success {
background-color: #4c994c;
}
.well {
background-color: #e5e5e5;
#toast-container > div {
box-shadow: none;
}

View File

@@ -14,6 +14,11 @@
return localizedText;
}
U = function(relativePath)
{
return Grocy.BaseUrl.replace(/\/$/, '') + relativePath;
}
if (!Grocy.ActiveNav.isEmpty())
{
var menuItem = $('.nav').find("[data-nav-for-page='" + Grocy.ActiveNav + "']");
@@ -21,11 +26,24 @@ if (!Grocy.ActiveNav.isEmpty())
}
$.timeago.settings.allowFuture = true;
$('time.timeago').timeago();
RefreshContextualTimeago = function()
{
$('time.timeago').timeago();
}
RefreshContextualTimeago();
Grocy.FetchJson = function(url, success, error)
toastr.options = {
toastClass: 'alert',
closeButton: true,
timeOut: 20000,
extendedTimeOut: 5000
};
Grocy.Api = { };
Grocy.Api.Get = function(apiFunction, success, error)
{
var xhr = new XMLHttpRequest();
var url = U('/api/' + apiFunction);
xhr.onreadystatechange = function()
{
@@ -52,9 +70,10 @@ Grocy.FetchJson = function(url, success, error)
xhr.send();
};
Grocy.PostJson = function(url, jsonData, success, error)
Grocy.Api.Post = function(apiFunction, jsonData, success, error)
{
var xhr = new XMLHttpRequest();
var url = U('/api/' + apiFunction);
xhr.onreadystatechange = function()
{

View File

@@ -1,14 +1,17 @@
$(document).on('click', '.battery-delete-button', function(e)
{
var objectName = $(e.currentTarget).attr('data-battery-name');
var objectId = $(e.currentTarget).attr('data-battery-id');
bootbox.confirm({
message: 'Delete battery <strong>' + $(e.target).attr('data-battery-name') + '</strong>?',
message: L('Are you sure to delete battery "#1"?', objectName),
buttons: {
confirm: {
label: 'Yes',
label: L('Yes'),
className: 'btn-success'
},
cancel: {
label: 'No',
label: L('No'),
className: 'btn-danger'
}
},
@@ -16,10 +19,10 @@
{
if (result === true)
{
Grocy.FetchJson('/api/delete-object/batteries/' + $(e.target).attr('data-battery-id'),
Grocy.Api.Get('delete-object/batteries/' + objectId,
function(result)
{
window.location.href = '/batteries';
window.location.href = U('/batteries');
},
function(xhr)
{

View File

@@ -1,5 +1,33 @@
$('#batteries-overview-table').DataTable({
'pageLength': 50,
'order': [[1, 'desc']],
'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

@@ -4,10 +4,10 @@
if (Grocy.EditMode === 'create')
{
Grocy.PostJson('/api/add-object/batteries', $('#battery-form').serializeJSON(),
Grocy.Api.Post('add-object/batteries', $('#battery-form').serializeJSON(),
function(result)
{
window.location.href = '/batteries';
window.location.href = U('/batteries');
},
function(xhr)
{
@@ -17,10 +17,10 @@
}
else
{
Grocy.PostJson('/api/edit-object/batteries/' + Grocy.EditObjectId, $('#battery-form').serializeJSON(),
Grocy.Api.Post('edit-object/batteries/' + Grocy.EditObjectId, $('#battery-form').serializeJSON(),
function(result)
{
window.location.href = '/batteries';
window.location.href = U('/batteries');
},
function(xhr)
{

View File

@@ -4,13 +4,13 @@
var jsonForm = $('#batterytracking-form').serializeJSON();
Grocy.FetchJson('/api/batteries/get-battery-details/' + jsonForm.battery_id,
Grocy.Api.Get('batteries/get-battery-details/' + jsonForm.battery_id,
function (batteryDetails)
{
Grocy.FetchJson('/api/batteries/track-charge-cycle/' + jsonForm.battery_id + '?tracked_time=' + $('#tracked_time').val(),
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

@@ -2,7 +2,7 @@ Grocy.Components.BatteryCard = { };
Grocy.Components.BatteryCard.Refresh = function(batteryId)
{
Grocy.FetchJson('/api/batteries/get-battery-details/' + batteryId,
Grocy.Api.Get('batteries/get-battery-details/' + batteryId,
function(batteryDetails)
{
$('#batterycard-battery-name').text(batteryDetails.battery.name);

View File

@@ -1,8 +1,8 @@
Grocy.Components.HabitCard = { };
Grocy.Components.HabitCard.Refresh = function (habitId)
Grocy.Components.HabitCard.Refresh = function(habitId)
{
Grocy.FetchJson('/api/habits/get-habit-details/' + habitId,
Grocy.Api.Get('habits/get-habit-details/' + habitId,
function(habitDetails)
{
$('#habitcard-habit-name').text(habitDetails.habit.name);

View File

@@ -2,7 +2,7 @@ Grocy.Components.ProductCard = { };
Grocy.Components.ProductCard.Refresh = function(productId)
{
Grocy.FetchJson('/api/stock/get-product-details/' + productId,
Grocy.Api.Get('stock/get-product-details/' + productId,
function(productDetails)
{
$('#productcard-product-name').text(productDetails.product.name);

View File

@@ -10,13 +10,13 @@
spoiled = 1;
}
Grocy.FetchJson('/api/stock/get-product-details/' + jsonForm.product_id,
Grocy.Api.Get('stock/get-product-details/' + jsonForm.product_id,
function (productDetails)
{
Grocy.FetchJson('/api/stock/consume-product/' + jsonForm.product_id + '/' + jsonForm.amount + '?spoiled=' + spoiled,
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('');
@@ -46,7 +46,7 @@ $('#product_id').on('change', function(e)
{
Grocy.Components.ProductCard.Refresh(productId);
Grocy.FetchJson('/api/stock/get-product-details/' + productId,
Grocy.Api.Get('stock/get-product-details/' + productId,
function (productDetails)
{
$('#amount').attr('max', productDetails.stock_amount);
@@ -60,7 +60,7 @@ $('#product_id').on('change', function(e)
$('#product_id_text_input').addClass('has-error');
$('#product_id_text_input').parent('.input-group').addClass('has-error');
$('#product_id_text_input').closest('.form-group').addClass('has-error');
$('#product-error').text('This product is not in stock.');
$('#product-error').text(L('This product is not in stock'));
$('#product-error').show();
$('#product_id_text_input').focus();
}

View File

@@ -4,10 +4,10 @@
if (Grocy.EditMode === 'create')
{
Grocy.PostJson('/api/add-object/habits', $('#habit-form').serializeJSON(),
Grocy.Api.Post('add-object/habits', $('#habit-form').serializeJSON(),
function(result)
{
window.location.href = '/habits';
window.location.href = U('/habits');
},
function(xhr)
{
@@ -17,10 +17,10 @@
}
else
{
Grocy.PostJson('/api/edit-object/habits/' + Grocy.EditObjectId, $('#habit-form').serializeJSON(),
Grocy.Api.Post('edit-object/habits/' + Grocy.EditObjectId, $('#habit-form').serializeJSON(),
function(result)
{
window.location.href = '/habits';
window.location.href = U('/habits');
},
function(xhr)
{
@@ -41,7 +41,7 @@ $('.input-group-habit-period-type').on('change', function(e)
if (periodType === 'dynamic-regular')
{
$('#habit-period-type-info').text('This means it is estimated that a new "execution" of this habit is tracked ' + periodDays.toString() + ' days after the last was tracked.');
$('#habit-period-type-info').text(L('This means it is estimated that a new execution of this habit is tracked #1 days after the last was tracked', periodDays.toString()));
$('#habit-period-type-info').show();
}
else

View File

@@ -1,14 +1,17 @@
$(document).on('click', '.habit-delete-button', function(e)
{
var objectName = $(e.currentTarget).attr('data-habit-name');
var objectId = $(e.currentTarget).attr('data-habit-id');
bootbox.confirm({
message: 'Delete habit <strong>' + $(e.target).attr('data-habit-name') + '</strong>?',
message: L('Are you sure to delete habit "#1"?', objectName),
buttons: {
confirm: {
label: 'Yes',
label: L('Yes'),
className: 'btn-success'
},
cancel: {
label: 'No',
label: L('No'),
className: 'btn-danger'
}
},
@@ -16,10 +19,10 @@
{
if (result === true)
{
Grocy.FetchJson('/api/delete-object/habits/' + $(e.target).attr('data-habit-id'),
Grocy.Api.Get('delete-object/habits/' + objectId,
function(result)
{
window.location.href = '/habits';
window.location.href = U('/habits');
},
function(xhr)
{

View File

@@ -1,5 +1,33 @@
$('#habits-overview-table').DataTable({
'pageLength': 50,
'order': [[1, 'desc']],
'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

@@ -4,13 +4,13 @@
var jsonForm = $('#habittracking-form').serializeJSON();
Grocy.FetchJson('/api/habits/get-habit-details/' + jsonForm.habit_id,
Grocy.Api.Get('habits/get-habit-details/' + jsonForm.habit_id,
function (habitDetails)
{
Grocy.FetchJson('/api/habits/track-habit-execution/' + jsonForm.habit_id + '?tracked_time=' + $('#tracked_time').val(),
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

@@ -4,10 +4,10 @@
var jsonForm = $('#inventory-form').serializeJSON();
Grocy.FetchJson('/api/stock/get-product-details/' + jsonForm.product_id,
Grocy.Api.Get('stock/get-product-details/' + jsonForm.product_id,
function (productDetails)
{
Grocy.FetchJson('/api/stock/inventory-product/' + jsonForm.product_id + '/' + jsonForm.new_amount + '?bestbeforedate=' + $('#best_before_date').val(),
Grocy.Api.Get('stock/inventory-product/' + jsonForm.product_id + '/' + jsonForm.new_amount + '?bestbeforedate=' + $('#best_before_date').val(),
function(result)
{
var addBarcode = GetUriParam('addbarcodetoselection');
@@ -23,7 +23,7 @@
productDetails.product.barcode += ',' + addBarcode;
}
Grocy.PostJson('/api/edit-object/products/' + productDetails.product.id, productDetails.product,
Grocy.Api.Get('edit-object/products/' + productDetails.product.id, productDetails.product,
function (result) { },
function(xhr)
{
@@ -32,11 +32,11 @@
);
}
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)
{
window.location.href = '/inventory';
window.location.href = U('/inventory');
}
else
{
@@ -71,7 +71,7 @@ $('#product_id').on('change', function(e)
{
Grocy.Components.ProductCard.Refresh(productId);
Grocy.FetchJson('/api/stock/get-product-details/' + productId,
Grocy.Api.Get('stock/get-product-details/' + productId,
function(productDetails)
{
$('#new_amount').attr('not-equal', productDetails.stock_amount);
@@ -108,39 +108,39 @@ $('#product_id_text_input').on('change', function(e)
if (input.length > 0 && optionElement.length === 0 && GetUriParam('addbarcodetoselection') === undefined )
{
bootbox.dialog({
message: '<strong>' + input + '</strong> could not be resolved to a product, how do you want to proceed?',
title: 'Create or assign product',
message: L('#1 could not be resolved to a product, how do you want to proceed?', input),
title: L('Create or assign product'),
onEscape: function() { },
size: 'large',
backdrop: true,
buttons: {
cancel: {
label: 'Cancel',
label: L('Cancel'),
className: 'btn-default',
callback: function() { }
},
addnewproduct: {
label: 'Add as new <u><strong>p</strong></u>roduct',
label: '<strong>P</strong> ' + L('Add as new product'),
className: 'btn-success add-new-product-dialog-button',
callback: function()
{
window.location.href = '/product/new?prefillname=' + encodeURIComponent(input) + '&returnto=' + encodeURIComponent(window.location.pathname);
window.location.href = U('/product/new?prefillname=' + encodeURIComponent(input) + '&returnto=' + encodeURIComponent(window.location.pathname));
}
},
addbarcode: {
label: 'Add as <u><strong>b</strong></u>arcode to existing product',
label: '<strong>B</strong> ' + L('Add as barcode to existing product'),
className: 'btn-info add-new-barcode-dialog-button',
callback: function()
{
window.location.href = '/inventory?addbarcodetoselection=' + encodeURIComponent(input);
window.location.href = U('/inventory?addbarcodetoselection=' + encodeURIComponent(input));
}
},
addnewproductwithbarcode: {
label: '<u><strong>A</strong></u>dd as new product + prefill barcode',
label: '<strong>A</strong> ' + L('Add as new product and prefill barcode'),
className: 'btn-warning add-new-product-with-barcode-dialog-button',
callback: function()
{
window.location.href = '/product/new?prefillbarcode=' + encodeURIComponent(input) + '&returnto=' + encodeURIComponent(window.location.pathname);
window.location.href = U('/product/new?prefillbarcode=' + encodeURIComponent(input) + '&returnto=' + encodeURIComponent(window.location.pathname));
}
}
}
@@ -283,7 +283,7 @@ $('#new_amount').on('change', function(e)
if (productId)
{
Grocy.FetchJson('/api/stock/get-product-details/' + productId,
Grocy.Api.Get('stock/get-product-details/' + productId,
function(productDetails)
{
var productStockAmount = productDetails.stock_amount || '0';
@@ -291,14 +291,14 @@ $('#new_amount').on('change', function(e)
if (newAmount > productStockAmount)
{
var amountToAdd = newAmount - productDetails.stock_amount;
$('#inventory-change-info').text('This means ' + amountToAdd.toString() + ' ' + productDetails.quantity_unit_stock.name + ' will be added to stock');
$('#inventory-change-info').text(L('This means #1 will be added to stock', amountToAdd.toString() + ' ' + productDetails.quantity_unit_stock.name));
$('#inventory-change-info').show();
$('#best_before_date').attr('required', 'required');
}
else if (newAmount < productStockAmount)
{
var amountToRemove = productStockAmount - newAmount;
$('#inventory-change-info').text('This means ' + amountToRemove.toString() + ' ' + productDetails.quantity_unit_stock.name + ' will be removed from stock');
$('#inventory-change-info').text(L('This means #1 will be removed from stock', amountToRemove.toString() + ' ' + productDetails.quantity_unit_stock.name));
$('#inventory-change-info').show();
$('#best_before_date').removeAttr('required');
}

View File

@@ -4,10 +4,10 @@
if (Grocy.EditMode === 'create')
{
Grocy.PostJson('/api/add-object/locations', $('#location-form').serializeJSON(),
Grocy.Api.Post('add-object/locations', $('#location-form').serializeJSON(),
function(result)
{
window.location.href = '/locations';
window.location.href = U('/locations');
},
function(xhr)
{
@@ -17,10 +17,10 @@
}
else
{
Grocy.PostJson('/api/edit-object/locations/' + Grocy.EditObjectId, $('#location-form').serializeJSON(),
Grocy.Api.Post('edit-object/locations/' + Grocy.EditObjectId, $('#location-form').serializeJSON(),
function(result)
{
window.location.href = '/locations';
window.location.href = U('/locations');
},
function(xhr)
{

View File

@@ -1,14 +1,17 @@
$(document).on('click', '.location-delete-button', function(e)
{
var objectName = $(e.currentTarget).attr('data-location-name');
var objectId = $(e.currentTarget).attr('data-location-id');
bootbox.confirm({
message: 'Delete location <strong>' + $(e.target).attr('data-location-name') + '</strong>?',
message: L('Are you sure to delete location "#1"?', objectName),
buttons: {
confirm: {
label: 'Yes',
label: L('Yes'),
className: 'btn-success'
},
cancel: {
label: 'No',
label: L('No'),
className: 'btn-danger'
}
},
@@ -16,10 +19,10 @@
{
if (result === true)
{
Grocy.FetchJson('/api/delete-object/locations/' + $(e.target).attr('data-location-id'),
Grocy.Api.Get('delete-object/locations/' + objectId,
function(result)
{
window.location.href = '/locations';
window.location.href = U('/locations');
},
function(xhr)
{

View File

@@ -0,0 +1,50 @@
$(document).on('click', '.apikey-delete-button', function(e)
{
var objectName = $(e.currentTarget).attr('data-apikey-apikey');
var objectId = $(e.currentTarget).attr('data-apikey-id');
bootbox.confirm({
message: L('Are you sure to delete API key "#1"?', objectName),
buttons: {
confirm: {
label: L('Yes'),
className: 'btn-success'
},
cancel: {
label: L('No'),
className: 'btn-danger'
}
},
callback: function(result)
{
if (result === true)
{
Grocy.Api.Get('delete-object/api_keys/' + objectId,
function(result)
{
window.location.href = U('/manageapikeys');
},
function(xhr)
{
console.error(xhr);
}
);
}
}
});
});
$('#apikeys-table').DataTable({
'pageLength': 50,
'order': [[4, 'desc']],
'columnDefs': [
{ 'orderable': false, 'targets': 0 }
],
'language': JSON.parse(L('datatables_localization'))
});
var createdApiKeyId = GetUriParam('CreatedApiKeyId');
if (createdApiKeyId !== undefined)
{
$('#apiKeyRow_' + createdApiKeyId).effect('highlight', { }, 3000);
}

View File

@@ -0,0 +1,26 @@
function HideTopbarPlugin()
{
return {
components: {
Topbar: function () { return null }
}
}
}
const swaggerUi = SwaggerUIBundle({
url: Grocy.OpenApi.SpecUrl,
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl,
HideTopbarPlugin
],
layout: 'StandaloneLayout',
docExpansion: "list"
});
window.ui = swaggerUi;

View File

@@ -2,7 +2,7 @@
{
e.preventDefault();
var redirectDestination = '/products';
var redirectDestination = U('/products');
var returnTo = GetUriParam('returnto');
if (returnTo !== undefined)
{
@@ -11,7 +11,7 @@
if (Grocy.EditMode === 'create')
{
Grocy.PostJson('/api/add-object/products', $('#product-form').serializeJSON(),
Grocy.Api.Post('add-object/products', $('#product-form').serializeJSON(),
function(result)
{
window.location.href = redirectDestination;
@@ -24,7 +24,7 @@
}
else
{
Grocy.PostJson('/api/edit-object/products/' + Grocy.EditObjectId, $('#product-form').serializeJSON(),
Grocy.Api.Post('edit-object/products/' + Grocy.EditObjectId, $('#product-form').serializeJSON(),
function(result)
{
window.location.href = redirectDestination;
@@ -44,7 +44,7 @@ $('#barcode-taginput').tagsManager({
if (Grocy.EditMode === 'edit')
{
Grocy.FetchJson('/api/get-object/products/' + Grocy.EditObjectId,
Grocy.Api.Get('get-object/products/' + Grocy.EditObjectId,
function (product)
{
if (product.barcode !== null && product.barcode.length > 0)

View File

@@ -1,14 +1,17 @@
$(document).on('click', '.product-delete-button', function(e)
{
var objectName = $(e.currentTarget).attr('data-product-name');
var objectId = $(e.currentTarget).attr('data-product-id');
bootbox.confirm({
message: 'Delete product <strong>' + $(e.target).attr('data-product-name') + '</strong>?',
message: L('Are you sure to delete product "#1"?', objectName),
buttons: {
confirm: {
label: 'Yes',
label: L('Yes'),
className: 'btn-success'
},
cancel: {
label: 'No',
label: L('No'),
className: 'btn-danger'
}
},
@@ -16,10 +19,10 @@
{
if (result === true)
{
Grocy.FetchJson('/api/delete-object/products/' + $(e.target).attr('data-product-id'),
Grocy.Api.Get('delete-object/products/' + objectId,
function(result)
{
window.location.href = '/products';
window.location.href = U('/products');
},
function(xhr)
{

View File

@@ -4,12 +4,12 @@
var jsonForm = $('#purchase-form').serializeJSON();
Grocy.FetchJson('/api/stock/get-product-details/' + jsonForm.product_id,
Grocy.Api.Get('stock/get-product-details/' + jsonForm.product_id,
function (productDetails)
{
var amount = jsonForm.amount * productDetails.product.qu_factor_purchase_to_stock;
Grocy.FetchJson('/api/stock/add-product/' + jsonForm.product_id + '/' + amount + '?bestbeforedate=' + $('#best_before_date').val(),
Grocy.Api.Get('stock/add-product/' + jsonForm.product_id + '/' + amount + '?bestbeforedate=' + $('#best_before_date').val(),
function(result)
{
var addBarcode = GetUriParam('addbarcodetoselection');
@@ -25,7 +25,7 @@
productDetails.product.barcode += ',' + addBarcode;
}
Grocy.PostJson('/api/edit-object/products/' + productDetails.product.id, productDetails.product,
Grocy.Api.Post('edit-object/products/' + productDetails.product.id, productDetails.product,
function (result) { },
function(xhr)
{
@@ -34,11 +34,11 @@
);
}
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)
{
window.location.href = '/purchase';
window.location.href = U('/purchase');
}
else
{
@@ -72,7 +72,7 @@ $('#product_id').on('change', function(e)
{
Grocy.Components.ProductCard.Refresh(productId);
Grocy.FetchJson('/api/stock/get-product-details/' + productId,
Grocy.Api.Get('stock/get-product-details/' + productId,
function(productDetails)
{
$('#amount_qu_unit').text(productDetails.quantity_unit_purchase.name);
@@ -117,8 +117,8 @@ $('#product_id_text_input').on('change', function(e)
if (input.length > 0 && optionElement.length === 0 && GetUriParam('addbarcodetoselection') === undefined )
{
bootbox.dialog({
message: '<strong>' + input + '</strong> could not be resolved to a product, how do you want to proceed?',
title: 'Create or assign product',
message: L('"#1" could not be resolved to a product, how do you want to proceed?', input),
title: L('Create or assign product'),
onEscape: function() { },
size: 'large',
backdrop: true,
@@ -129,27 +129,27 @@ $('#product_id_text_input').on('change', function(e)
callback: function() { }
},
addnewproduct: {
label: 'Add as new <u><strong>p</strong></u>roduct',
label: '<strong>P</strong> ' + L('Add as new product'),
className: 'btn-success add-new-product-dialog-button',
callback: function()
{
window.location.href = '/product/new?prefillname=' + encodeURIComponent(input) + '&returnto=' + encodeURIComponent(window.location.pathname);
window.location.href = U('/product/new?prefillname=' + encodeURIComponent(input) + '&returnto=' + encodeURIComponent(window.location.pathname));
}
},
addbarcode: {
label: 'Add as <u><strong>b</strong></u>arcode to existing product',
label: '<strong>B</strong> ' + L('Add as barcode to existing product'),
className: 'btn-info add-new-barcode-dialog-button',
callback: function()
{
window.location.href = '/purchase?addbarcodetoselection=' + encodeURIComponent(input);
window.location.href = U('/purchase?addbarcodetoselection=' + encodeURIComponent(input));
}
},
addnewproductwithbarcode: {
label: '<u><strong>A</strong></u>dd as new product + prefill barcode',
label: '<strong>A</strong> ' + L('Add as new product and prefill barcode'),
className: 'btn-warning add-new-product-with-barcode-dialog-button',
callback: function()
{
window.location.href = '/product/new?prefillbarcode=' + encodeURIComponent(input) + '&returnto=' + encodeURIComponent(window.location.pathname);
window.location.href = U('/product/new?prefillbarcode=' + encodeURIComponent(input) + '&returnto=' + encodeURIComponent(window.location.pathname));
}
}
}

View File

@@ -4,10 +4,10 @@
if (Grocy.EditMode === 'create')
{
Grocy.PostJson('/api/add-object/quantity_units', $('#quantityunit-form').serializeJSON(),
Grocy.Api.Post('add-object/quantity_units', $('#quantityunit-form').serializeJSON(),
function(result)
{
window.location.href = '/quantityunits';
window.location.href = U('/quantityunits');
},
function(xhr)
{
@@ -17,10 +17,10 @@
}
else
{
Grocy.PostJson('/api/edit-object/quantity_units/' + Grocy.EditObjectId, $('#quantityunit-form').serializeJSON(),
Grocy.Api.Post('edit-object/quantity_units/' + Grocy.EditObjectId, $('#quantityunit-form').serializeJSON(),
function(result)
{
window.location.href = '/quantityunits';
window.location.href = U('/quantityunits');
},
function(xhr)
{

View File

@@ -1,7 +1,10 @@
$(document).on('click', '.quantityunit-delete-button', function(e)
{
var objectName = $(e.currentTarget).attr('data-quantityunit-name');
var objectId = $(e.currentTarget).attr('data-quantityunit-id');
bootbox.confirm({
message: 'Delete quantity unit <strong>' + $(e.target).attr('data-quantityunit-name') + '</strong>?',
message: L('Are you sure to delete quantity unit "#1"?', objectName),
buttons: {
confirm: {
label: 'Yes',
@@ -16,10 +19,10 @@
{
if (result === true)
{
Grocy.FetchJson('/api/delete-object/quantity_units/' + $(e.target).attr('data-quantityunit-id'),
Grocy.Api.Get('delete-object/quantity_units/' + objectId,
function(result)
{
window.location.href = '/quantityunits';
window.location.href = U('/quantityunits');
},
function(xhr)
{

View File

@@ -1,9 +1,9 @@
$(document).on('click', '.shoppinglist-delete-button', function(e)
{
Grocy.FetchJson('/api/delete-object/shopping_list/' + $(e.target).attr('data-shoppinglist-id'),
Grocy.Api.Get('delete-object/shopping_list/' + $(e.currentTarget).attr('data-shoppinglist-id'),
function(result)
{
window.location.href = '/shoppinglist';
window.location.href = U('/shoppinglist');
},
function(xhr)
{
@@ -14,10 +14,10 @@
$(document).on('click', '#add-products-below-min-stock-amount', function(e)
{
Grocy.FetchJson('/api/stock/add-missing-products-to-shoppinglist',
Grocy.Api.Get('stock/add-missing-products-to-shoppinglist',
function(result)
{
window.location.href = '/shoppinglist';
window.location.href = U('/shoppinglist');
},
function(xhr)
{

View File

@@ -4,10 +4,10 @@
if (Grocy.EditMode === 'create')
{
Grocy.PostJson('/api/add-object/shopping_list', $('#shoppinglist-form').serializeJSON(),
Grocy.Api.Post('add-object/shopping_list', $('#shoppinglist-form').serializeJSON(),
function(result)
{
window.location.href = '/shoppinglist';
window.location.href = U('/shoppinglist');
},
function(xhr)
{
@@ -17,10 +17,10 @@
}
else
{
Grocy.PostJson('/api/edit-object/shopping_list/' + Grocy.EditObjectId, $('#shoppinglist-form').serializeJSON(),
Grocy.Api.Post('edit-object/shopping_list/' + Grocy.EditObjectId, $('#shoppinglist-form').serializeJSON(),
function(result)
{
window.location.href = '/shoppinglist';
window.location.href = U('/shoppinglist');
},
function(xhr)
{
@@ -38,7 +38,7 @@ $('#product_id').on('change', function(e)
{
Grocy.Components.ProductCard.Refresh(productId);
Grocy.FetchJson('/api/stock/get-product-details/' + productId,
Grocy.Api.Get('stock/get-product-details/' + productId,
function (productDetails)
{
$('#amount_qu_unit').text(productDetails.quantity_unit_purchase.name);
@@ -49,7 +49,7 @@ $('#product_id').on('change', function(e)
}
else
{
Grocy.FetchJson('/api/get-objects/shopping_list',
Grocy.Api.Get('get-objects/shopping_list',
function (currentShoppingListItems)
{
if (currentShoppingListItems.filter(function (e) { return e.product_id === productId; }).length > 0)

View File

@@ -1,5 +1,46 @@
$('#stock-overview-table').DataTable({
'pageLength': 50,
'order': [[2, 'asc']],
'order': [[3, 'asc']],
'columnDefs': [
{ 'orderable': false, 'targets': 0 }
],
'language': JSON.parse(L('datatables_localization'))
});
$(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);
}
);
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 84 KiB

View File

@@ -2,51 +2,62 @@
use \Grocy\Middleware\JsonMiddleware;
use \Grocy\Middleware\CliMiddleware;
use \Grocy\Middleware\SessionAuthMiddleware;
use \Grocy\Middleware\ApiKeyAuthMiddleware;
use \Tuupola\Middleware\CorsMiddleware;
// Base route
$app->get('/', 'Grocy\Controllers\LoginController:Root')->setName('root');
$app->group('', function()
{
// Base route
$this->get('/', 'LoginControllerInstance:Root')->setName('root');
// Login routes
$app->get('/login', 'Grocy\Controllers\LoginController:LoginPage')->setName('login');
$app->post('/login', 'Grocy\Controllers\LoginController:ProcessLogin')->setName('login');
$app->get('/logout', 'Grocy\Controllers\LoginController:Logout');
// Login routes
$this->get('/login', 'LoginControllerInstance:LoginPage')->setName('login');
$this->post('/login', 'LoginControllerInstance:ProcessLogin')->setName('login');
$this->get('/logout', 'LoginControllerInstance:Logout');
// Stock routes
$app->get('/stockoverview', 'Grocy\Controllers\StockController:Overview');
$app->get('/purchase', 'Grocy\Controllers\StockController:Purchase');
$app->get('/consume', 'Grocy\Controllers\StockController:Consume');
$app->get('/inventory', 'Grocy\Controllers\StockController:Inventory');
// Stock routes
$this->get('/stockoverview', 'Grocy\Controllers\StockController:Overview');
$this->get('/purchase', 'Grocy\Controllers\StockController:Purchase');
$this->get('/consume', 'Grocy\Controllers\StockController:Consume');
$this->get('/inventory', 'Grocy\Controllers\StockController:Inventory');
$app->get('/products', 'Grocy\Controllers\StockController:ProductsList');
$app->get('/product/{productId}', 'Grocy\Controllers\StockController:ProductEditForm');
$this->get('/products', 'Grocy\Controllers\StockController:ProductsList');
$this->get('/product/{productId}', 'Grocy\Controllers\StockController:ProductEditForm');
$app->get('/locations', 'Grocy\Controllers\StockController:LocationsList');
$app->get('/location/{locationId}', 'Grocy\Controllers\StockController:LocationEditForm');
$this->get('/locations', 'Grocy\Controllers\StockController:LocationsList');
$this->get('/location/{locationId}', 'Grocy\Controllers\StockController:LocationEditForm');
$app->get('/quantityunits', 'Grocy\Controllers\StockController:QuantityUnitsList');
$app->get('/quantityunit/{quantityunitId}', 'Grocy\Controllers\StockController:QuantityUnitEditForm');
$this->get('/quantityunits', 'Grocy\Controllers\StockController:QuantityUnitsList');
$this->get('/quantityunit/{quantityunitId}', 'Grocy\Controllers\StockController:QuantityUnitEditForm');
$app->get('/shoppinglist', 'Grocy\Controllers\StockController:ShoppingList');
$app->get('/shoppinglistitem/{itemId}', 'Grocy\Controllers\StockController:ShoppingListItemEditForm');
$this->get('/shoppinglist', 'Grocy\Controllers\StockController:ShoppingList');
$this->get('/shoppinglistitem/{itemId}', 'Grocy\Controllers\StockController:ShoppingListItemEditForm');
// Habit routes
$this->get('/habitsoverview', 'Grocy\Controllers\HabitsController:Overview');
$this->get('/habittracking', 'Grocy\Controllers\HabitsController:TrackHabitExecution');
// Habit routes
$app->get('/habitsoverview', 'Grocy\Controllers\HabitsController:Overview');
$app->get('/habittracking', 'Grocy\Controllers\HabitsController:TrackHabitExecution');
$this->get('/habits', 'Grocy\Controllers\HabitsController:HabitsList');
$this->get('/habit/{habitId}', 'Grocy\Controllers\HabitsController:HabitEditForm');
$app->get('/habits', 'Grocy\Controllers\HabitsController:HabitsList');
$app->get('/habit/{habitId}', 'Grocy\Controllers\HabitsController:HabitEditForm');
// Batterry routes
$this->get('/batteriesoverview', 'Grocy\Controllers\BatteriesController:Overview');
$this->get('/batterytracking', 'Grocy\Controllers\BatteriesController:TrackChargeCycle');
// Batterry routes
$app->get('/batteriesoverview', 'Grocy\Controllers\BatteriesController:Overview');
$app->get('/batterytracking', 'Grocy\Controllers\BatteriesController:TrackChargeCycle');
$app->get('/batteries', 'Grocy\Controllers\BatteriesController:BatteriesList');
$app->get('/battery/{batteryId}', 'Grocy\Controllers\BatteriesController:BatteryEditForm');
$this->get('/batteries', 'Grocy\Controllers\BatteriesController:BatteriesList');
$this->get('/battery/{batteryId}', 'Grocy\Controllers\BatteriesController:BatteryEditForm');
// Other routes
$this->get('/api', 'Grocy\Controllers\OpenApiController:DocumentationUi');
$this->get('/manageapikeys', 'Grocy\Controllers\OpenApiController:ApiKeysList');
$this->get('/manageapikeys/new', 'Grocy\Controllers\OpenApiController:CreateNewApiKey');
})->add(new SessionAuthMiddleware($appContainer, $appContainer->LoginControllerInstance->GetSessionCookieName()));
$app->group('/api', function()
{
$this->get('/get-openapi-specification', 'Grocy\Controllers\OpenApiController:DocumentationSpec');
$this->get('/get-objects/{entity}', 'Grocy\Controllers\GenericEntityApiController:GetObjects');
$this->get('/get-object/{entity}/{objectId}', 'Grocy\Controllers\GenericEntityApiController:GetObject');
$this->post('/add-object/{entity}', 'Grocy\Controllers\GenericEntityApiController:AddObject');
@@ -59,13 +70,23 @@ $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');
$this->get('/batteries/track-charge-cycle/{batteryId}', 'Grocy\Controllers\BatteriesApiController:TrackChargeCycle');
$this->get('/batteries/get-battery-details/{batteryId}', 'Grocy\Controllers\BatteriesApiController:BatteryDetails');
})->add(JsonMiddleware::class);
})->add(new ApiKeyAuthMiddleware($appContainer, $appContainer->LoginControllerInstance->GetSessionCookieName(), $appContainer->ApiKeyHeaderName))
->add(JsonMiddleware::class)
->add(new CorsMiddleware([
'origin' => ["*"],
'methods' => ["GET", "POST"],
'headers.allow' => [ $appContainer->ApiKeyHeaderName ],
'headers.expose' => [ ],
'credentials' => false,
'cache' => 0,
]));
$app->group('/cli', function()
{

View File

@@ -0,0 +1,64 @@
<?php
namespace Grocy\Services;
class ApiKeyService extends BaseService
{
/**
* @return boolean
*/
public function IsValidApiKey($apiKey)
{
if ($apiKey === null || empty($apiKey))
{
return false;
}
else
{
$apiKeyRow = $this->Database->api_keys()->where('api_key = :1 AND expires > :2', $apiKey, date('Y-m-d H:i:s', time()))->fetch();
if ($apiKeyRow !== null)
{
$apiKeyRow->update(array(
'last_used' => date('Y-m-d H:i:s', time())
));
return true;
}
else
{
return false;
}
}
}
/**
* @return string
*/
public function CreateApiKey()
{
$newApiKey = $this->GenerateApiKey();
$apiKeyRow = $this->Database->api_keys()->createRow(array(
'api_key' => $newApiKey,
'expires' => '2999-12-31 23:59:59' // Default is that API keys expire never
));
$apiKeyRow->save();
return $newApiKey;
}
public function RemoveApiKey($apiKey)
{
$this->Database->api_keys()->where('api_key', $apiKey)->delete();
}
public function GetApiKeyId($apiKey)
{
$apiKey = $this->Database->api_keys()->where('api_key', $apiKey)->fetch();
return $apiKey->id;
}
private function GenerateApiKey()
{
return RandomString(50);
}
}

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

@@ -12,6 +12,11 @@ class BatteriesService extends BaseService
public function GetNextChargeTime(int $batteryId)
{
if (!$this->BatteryExists($batteryId))
{
throw new \Exception('Battery does not exist');
}
$battery = $this->Database->batteries($batteryId);
$batteryLastLogRow = $this->DatabaseService->ExecuteDbQuery("SELECT * from batteries_current WHERE battery_id = $batteryId LIMIT 1")->fetch(\PDO::FETCH_OBJ);
@@ -21,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;
@@ -29,6 +34,11 @@ class BatteriesService extends BaseService
public function GetBatteryDetails(int $batteryId)
{
if (!$this->BatteryExists($batteryId))
{
throw new \Exception('Battery does not exist');
}
$battery = $this->Database->batteries($batteryId);
$batteryChargeCylcesCount = $this->Database->battery_charge_cycles()->where('battery_id', $batteryId)->count();
$batteryLastChargedTime = $this->Database->battery_charge_cycles()->where('battery_id', $batteryId)->max('tracked_time');
@@ -42,6 +52,11 @@ class BatteriesService extends BaseService
public function TrackChargeCycle(int $batteryId, string $trackedTime)
{
if (!$this->BatteryExists($batteryId))
{
throw new \Exception('Battery does not exist');
}
$logRow = $this->Database->battery_charge_cycles()->createRow(array(
'battery_id' => $batteryId,
'tracked_time' => $trackedTime
@@ -50,4 +65,10 @@ class BatteriesService extends BaseService
return true;
}
private function BatteryExists($batteryId)
{
$batteryRow = $this->Database->batteries()->where('id = :1', $batteryId)->fetch();
return $batteryRow !== null;
}
}

View File

@@ -15,13 +15,18 @@ class HabitsService extends BaseService
public function GetNextHabitTime(int $habitId)
{
if (!$this->HabitExists($habitId))
{
throw new \Exception('Habit does not exist');
}
$habit = $this->Database->habits($habitId);
$habitLastLogRow = $this->DatabaseService->ExecuteDbQuery("SELECT * from habits_current WHERE habit_id = $habitId LIMIT 1")->fetch(\PDO::FETCH_OBJ);
switch($habit->period_type)
{
case self::HABIT_TYPE_MANUALLY:
return date('Y-m-d H:i:s');
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)));
}
@@ -31,6 +36,11 @@ class HabitsService extends BaseService
public function GetHabitDetails(int $habitId)
{
if (!$this->HabitExists($habitId))
{
throw new \Exception('Habit does not exist');
}
$habit = $this->Database->habits($habitId);
$habitTrackedCount = $this->Database->habits_log()->where('habit_id', $habitId)->count();
$habitLastTrackedTime = $this->Database->habits_log()->where('habit_id', $habitId)->max('tracked_time');
@@ -44,6 +54,11 @@ class HabitsService extends BaseService
public function TrackHabit(int $habitId, string $trackedTime)
{
if (!$this->HabitExists($habitId))
{
throw new \Exception('Habit does not exist');
}
$logRow = $this->Database->habits_log()->createRow(array(
'habit_id' => $habitId,
'tracked_time' => $trackedTime
@@ -52,4 +67,10 @@ class HabitsService extends BaseService
return true;
}
private function HabitExists($habitId)
{
$habitRow = $this->Database->habits()->where('id = :1', $habitId)->fetch();
return $habitRow !== null;
}
}

View File

@@ -15,7 +15,18 @@ class SessionService extends BaseService
}
else
{
return $this->Database->sessions()->where('session_key = :1 AND expires > :2', $sessionKey, time())->count() === 1;
$sessionRow = $this->Database->sessions()->where('session_key = :1 AND expires > :2', $sessionKey, date('Y-m-d H:i:s', time()))->fetch();
if ($sessionRow !== null)
{
$sessionRow->update(array(
'last_used' => date('Y-m-d H:i:s', time())
));
return true;
}
else
{
return false;
}
}
}
@@ -24,11 +35,11 @@ class SessionService extends BaseService
*/
public function CreateSession()
{
$newSessionKey = uniqid() . uniqid() . uniqid();
$newSessionKey = $this->GenerateSessionKey();
$sessionRow = $this->Database->sessions()->createRow(array(
'session_key' => $newSessionKey,
'expires' => time() + 2592000 //30 days
'expires' => date('Y-m-d H:i:s', time() + 2592000) // Default is that sessions expire in 30 days
));
$sessionRow->save();
@@ -39,4 +50,9 @@ class SessionService extends BaseService
{
$this->Database->sessions()->where('session_key', $sessionKey)->delete();
}
private function GenerateSessionKey()
{
return RandomString(50);
}
}

View File

@@ -22,6 +22,11 @@ class StockService extends BaseService
public function GetProductDetails(int $productId)
{
if (!$this->ProductExists($productId))
{
throw new \Exception('Product does not exist');
}
$product = $this->Database->products($productId);
$productStockAmount = $this->Database->stock()->where('product_id', $productId)->sum('amount');
$productLastPurchased = $this->Database->stock_log()->where('product_id', $productId)->where('transaction_type', self::TRANSACTION_TYPE_PURCHASE)->max('purchased_date');
@@ -41,6 +46,11 @@ class StockService extends BaseService
public function AddProduct(int $productId, int $amount, string $bestBeforeDate, $transactionType)
{
if (!$this->ProductExists($productId))
{
throw new \Exception('Product does not exist');
}
if ($transactionType === self::TRANSACTION_TYPE_CONSUME || $transactionType === self::TRANSACTION_TYPE_PURCHASE || $transactionType === self::TRANSACTION_TYPE_INVENTORY_CORRECTION)
{
$stockId = uniqid();
@@ -68,12 +78,17 @@ class StockService extends BaseService
}
else
{
throw new Exception("Transaction type $transactionType is not valid (StockService.AddProduct)");
throw new \Exception("Transaction type $transactionType is not valid (StockService.AddProduct)");
}
}
public function ConsumeProduct(int $productId, int $amount, bool $spoiled, $transactionType)
{
if (!$this->ProductExists($productId))
{
throw new \Exception('Product does not exist');
}
if ($transactionType === self::TRANSACTION_TYPE_CONSUME || $transactionType === self::TRANSACTION_TYPE_PURCHASE || $transactionType === self::TRANSACTION_TYPE_INVENTORY_CORRECTION)
{
$productStockAmount = $this->Database->stock()->where('product_id', $productId)->sum('amount');
@@ -141,6 +156,11 @@ class StockService extends BaseService
public function InventoryProduct(int $productId, int $newAmount, string $bestBeforeDate)
{
if (!$this->ProductExists($productId))
{
throw new \Exception('Product does not exist');
}
$productStockAmount = $this->Database->stock()->where('product_id', $productId)->sum('amount');
if ($newAmount > $productStockAmount)
@@ -182,4 +202,50 @@ class StockService extends BaseService
}
}
}
private function ProductExists($productId)
{
$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.11.0",
"ReleaseDate": "2018-06-15"
}

View File

@@ -1 +0,0 @@
1.8.0

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="/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="/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="/viewjs/components/batterycard.js"></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="/viewjs/components/datepicker.js"></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="/viewjs/components/datetimepicker.js"></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="/viewjs/components/habitcard.js"></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="/viewjs/components/productcard.js"></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="/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="/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

@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en">
<html lang="{{ CULTURE }}">
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
@@ -9,25 +9,28 @@
<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="/img/grocy.png">
<link rel="icon" type="image/png" sizes="200x200" href="{{ $U('/img/grocy.png?v=', true) }}{{ $version }}">
<title>@yield('title') | grocy</title>
<link href="/bower_components/bootstrap/dist/css/bootstrap.min.css?v={{ $version }}" rel="stylesheet">
<link href="/bower_components/font-awesome/css/font-awesome.min.css?v={{ $version }}" rel="stylesheet">
<link href="/bower_components/bootstrap-datepicker/dist/css/bootstrap-datepicker3.min.css?v={{ $version }}" rel="stylesheet">
<link href="/bower_components/bootstrap-combobox/css/bootstrap-combobox.css?v={{ $version }}" rel="stylesheet">
<link href="/bower_components/datatables.net-bs/css/dataTables.bootstrap.min.css?v={{ $version }}" rel="stylesheet">
<link href="/bower_components/datatables.net-responsive-bs/css/responsive.bootstrap.min.css?v={{ $version }}" rel="stylesheet">
<link href="/bower_components/toastr/toastr.min.css?v={{ $version }}" rel="stylesheet">
<link href="/bower_components/tagmanager/tagmanager.css?v={{ $version }}" rel="stylesheet">
<link href="/bower_components/eonasdan-bootstrap-datetimepicker/build/css/bootstrap-datetimepicker.min.css?v={{ $version }}" rel="stylesheet">
<link href="/components_unmanaged/noto-sans-v6-latin/noto-sans-v6-latin.css?v={{ $version }}" rel="stylesheet">
<link href="/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>
var Grocy = { };
Grocy.Components = { };
Grocy.BaseUrl = '{{ $U('/') }}';
Grocy.LocalizationStrings = {!! json_encode($localizationStrings) !!};
Grocy.ActiveNav = '@yield('activeNav', '')';
</script>
@@ -42,93 +45,16 @@
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">grocy</a>
<a class="navbar-brand" href="{{ $U('/') }}">grocy</a>
</div>
<div id="navbar" class="navbar-collapse collapse">
<ul class="nav navbar-nav navbar-right">
<li>
<a class="discrete-link logout-button" href="/logout"><i class="fa fa-sign-out fa-fw"></i>&nbsp;{{ $L('Logout') }}</a>
</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="/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="/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="/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="/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="/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="/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="/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="/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="/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="/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="/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="/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="/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="/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>
<a class="discrete-link logout-button" href="/logout"><i class="fa fa-sign-out fa-fw"></i>&nbsp;{{ $L('Logout') }}</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>
@include('components.menu')
@include('components.usermenu')
</div>
</div>
</nav>
@@ -136,107 +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="/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="/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="/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="/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="/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="/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="/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="/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="/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="/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="/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="/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="/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="/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="/bower_components/jquery/dist/jquery.min.js?v={{ $version }}"></script>
<script src="/bower_components/bootstrap/dist/js/bootstrap.min.js?v={{ $version }}"></script>
<script src="/bower_components/bootbox/bootbox.js?v={{ $version }}"></script>
<script src="/bower_components/jquery.serializeJSON/jquery.serializejson.min.js?v={{ $version }}"></script>
<script src="/bower_components/bootstrap-datepicker/dist/js/bootstrap-datepicker.min.js?v={{ $version }}"></script>
@if(!empty($L('bootstrap_datepicker_locale')))<script src="/bower_components/bootstrap-datepicker/dist/locales/bootstrap-datepicker.{{ $L('bootstrap_datepicker_locale') }}.min.js?v={{ $version }}"></script>@endif
<script src="/bower_components/moment/min/moment.min.js?v={{ $version }}"></script>
@if(!empty($L('moment_locale')))<script src="/bower_components/moment/locale/{{ $L('moment_locale') }}.js?v={{ $version }}"></script>@endif
<script src="/bower_components/bootstrap-validator/dist/validator.min.js?v={{ $version }}"></script>
<script src="/bower_components/bootstrap-combobox/js/bootstrap-combobox.js?v={{ $version }}"></script>
<script src="/bower_components/datatables.net/js/jquery.dataTables.min.js?v={{ $version }}"></script>
<script src="/bower_components/datatables.net-bs/js/dataTables.bootstrap.min.js?v={{ $version }}"></script>
<script src="/bower_components/datatables.net-responsive/js/dataTables.responsive.min.js?v={{ $version }}"></script>
<script src="/bower_components/datatables.net-responsive-bs/js/responsive.bootstrap.min.js?v={{ $version }}"></script>
<script src="/bower_components/jquery-timeago/jquery.timeago.js?v={{ $version }}"></script>
<script src="/bower_components/jquery-timeago/locales/jquery.timeago.{{ $L('timeago_locale') }}.js?v={{ $version }}"></script>
<script src="/bower_components/toastr/toastr.min.js?v={{ $version }}"></script>
<script src="/bower_components/tagmanager/tagmanager.js?v={{ $version }}"></script>
<script src="/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="/js/extensions.js?v={{ $version }}"></script>
<script src="/js/grocy.js?v={{ $version }}"></script>
<script src="/viewjs/@yield('viewJsName').js"></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', 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="/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="/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="/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

@@ -0,0 +1,60 @@
@extends('layout.default')
@section('title', $L('API keys'))
@section('activeNav', '')
@section('viewJsName', 'manageapikeys')
@push('pageScripts')
<script src="{{ $U('/bower_components/jquery-ui/jquery-ui.min.js?v=', true) }}{{ $version }}"></script>
@endpush
@section('content')
<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>
<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>
@stop

36
views/openapiui.blade.php Normal file
View File

@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="robots" content="noindex,nofollow">
<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=', true) }}{{ $version }}">
<title>{{ $L('REST API & data model documentation') }} | grocy</title>
<link href="{{ $U('/bower_components/swagger-ui/dist/swagger-ui.css?v=', true) }}{{ $version }}" rel="stylesheet">
<script>
var Grocy = { };
Grocy.OpenApi = { };
Grocy.OpenApi.SpecUrl = '{{ $U('/api/get-openapi-specification') }}';
</script>
</head>
<body>
<div id="swagger-ui"></div>
<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
@endif
</body>
</html>

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="/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="/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">
@@ -20,7 +19,7 @@
@endforeach
</select>
<div class="help-block with-errors"></div>
<div id="flow-info-addbarcodetoselection" class="text-muted small hide"><strong><span id="addbarcodetoselection"></span></strong> will be added to the list of barcodes for the selected product on submit.</div>
<div id="flow-info-addbarcodetoselection" class="text-muted small hide"><strong><span id="addbarcodetoselection"></span></strong> {{ $L('will be added to the list of barcodes for the selected product on submit') }}</div>
</div>
@include('components.datepicker', array(
@@ -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="/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="/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

Some files were not shown because too many files have changed in this diff Show More