Merge pull request #4 from berrnd/master

update grocy-docker-patch fork
This commit is contained in:
Talmai Oliveira 2018-10-19 20:57:32 -04:00 committed by GitHub
commit 94dc6c6a55
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
57 changed files with 1819 additions and 174 deletions

74
composer.lock generated
View File

@ -159,16 +159,16 @@
},
{
"name": "illuminate/container",
"version": "v5.7.5",
"version": "v5.7.8",
"source": {
"type": "git",
"url": "https://github.com/illuminate/container.git",
"reference": "0fc33b14ae6cf9a1e694fd43f2a274e590a824b2"
"reference": "2582a994f2f8a153a4880de757a89ad4eeb083d7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/illuminate/container/zipball/0fc33b14ae6cf9a1e694fd43f2a274e590a824b2",
"reference": "0fc33b14ae6cf9a1e694fd43f2a274e590a824b2",
"url": "https://api.github.com/repos/illuminate/container/zipball/2582a994f2f8a153a4880de757a89ad4eeb083d7",
"reference": "2582a994f2f8a153a4880de757a89ad4eeb083d7",
"shasum": ""
},
"require": {
@ -199,20 +199,20 @@
],
"description": "The Illuminate Container package.",
"homepage": "https://laravel.com",
"time": "2018-05-28T08:50:10+00:00"
"time": "2018-10-03T15:20:19+00:00"
},
{
"name": "illuminate/contracts",
"version": "v5.7.5",
"version": "v5.7.8",
"source": {
"type": "git",
"url": "https://github.com/illuminate/contracts.git",
"reference": "2daf3c078610f744e2a4dc2f44fb5060cce9835b"
"reference": "9532d673de305b0c0028c0ce60c8952b807d7bc3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/illuminate/contracts/zipball/2daf3c078610f744e2a4dc2f44fb5060cce9835b",
"reference": "2daf3c078610f744e2a4dc2f44fb5060cce9835b",
"url": "https://api.github.com/repos/illuminate/contracts/zipball/9532d673de305b0c0028c0ce60c8952b807d7bc3",
"reference": "9532d673de305b0c0028c0ce60c8952b807d7bc3",
"shasum": ""
},
"require": {
@ -243,11 +243,11 @@
],
"description": "The Illuminate Contracts package.",
"homepage": "https://laravel.com",
"time": "2018-09-18T12:50:05+00:00"
"time": "2018-10-03T14:04:39+00:00"
},
{
"name": "illuminate/events",
"version": "v5.7.5",
"version": "v5.7.8",
"source": {
"type": "git",
"url": "https://github.com/illuminate/events.git",
@ -292,7 +292,7 @@
},
{
"name": "illuminate/filesystem",
"version": "v5.7.5",
"version": "v5.7.8",
"source": {
"type": "git",
"url": "https://github.com/illuminate/filesystem.git",
@ -344,16 +344,16 @@
},
{
"name": "illuminate/support",
"version": "v5.7.5",
"version": "v5.7.8",
"source": {
"type": "git",
"url": "https://github.com/illuminate/support.git",
"reference": "f7c68e8c8aab200cc8ad84f974d5511cda58a742"
"reference": "c7583db6703a36b7fa76254073046e0a920ed276"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/illuminate/support/zipball/f7c68e8c8aab200cc8ad84f974d5511cda58a742",
"reference": "f7c68e8c8aab200cc8ad84f974d5511cda58a742",
"url": "https://api.github.com/repos/illuminate/support/zipball/c7583db6703a36b7fa76254073046e0a920ed276",
"reference": "c7583db6703a36b7fa76254073046e0a920ed276",
"shasum": ""
},
"require": {
@ -399,20 +399,20 @@
],
"description": "The Illuminate Support package.",
"homepage": "https://laravel.com",
"time": "2018-09-19T18:36:57+00:00"
"time": "2018-10-04T13:27:30+00:00"
},
{
"name": "illuminate/view",
"version": "v5.7.5",
"version": "v5.7.8",
"source": {
"type": "git",
"url": "https://github.com/illuminate/view.git",
"reference": "3ccd29550afe61eb02ad9e4bae0c2e661aadd7af"
"reference": "86b8c60e502286135d9c91b0836a58445c4998b5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/illuminate/view/zipball/3ccd29550afe61eb02ad9e4bae0c2e661aadd7af",
"reference": "3ccd29550afe61eb02ad9e4bae0c2e661aadd7af",
"url": "https://api.github.com/repos/illuminate/view/zipball/86b8c60e502286135d9c91b0836a58445c4998b5",
"reference": "86b8c60e502286135d9c91b0836a58445c4998b5",
"shasum": ""
},
"require": {
@ -447,7 +447,7 @@
],
"description": "The Illuminate View package.",
"homepage": "https://laravel.com",
"time": "2018-09-18T12:50:05+00:00"
"time": "2018-10-02T13:51:18+00:00"
},
{
"name": "morris/lessql",
@ -1167,16 +1167,16 @@
},
{
"name": "symfony/debug",
"version": "v4.1.4",
"version": "v4.1.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/debug.git",
"reference": "47ead688f1f2877f3f14219670f52e4722ee7052"
"reference": "e3f76ce6198f81994e019bb2b4e533e9de1b9b90"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/debug/zipball/47ead688f1f2877f3f14219670f52e4722ee7052",
"reference": "47ead688f1f2877f3f14219670f52e4722ee7052",
"url": "https://api.github.com/repos/symfony/debug/zipball/e3f76ce6198f81994e019bb2b4e533e9de1b9b90",
"reference": "e3f76ce6198f81994e019bb2b4e533e9de1b9b90",
"shasum": ""
},
"require": {
@ -1219,20 +1219,20 @@
],
"description": "Symfony Debug Component",
"homepage": "https://symfony.com",
"time": "2018-08-03T11:13:38+00:00"
"time": "2018-10-02T16:36:10+00:00"
},
{
"name": "symfony/finder",
"version": "v4.1.4",
"version": "v4.1.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
"reference": "e162f1df3102d0b7472805a5a9d5db9fcf0a8068"
"reference": "1f17195b44543017a9c9b2d437c670627e96ad06"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/finder/zipball/e162f1df3102d0b7472805a5a9d5db9fcf0a8068",
"reference": "e162f1df3102d0b7472805a5a9d5db9fcf0a8068",
"url": "https://api.github.com/repos/symfony/finder/zipball/1f17195b44543017a9c9b2d437c670627e96ad06",
"reference": "1f17195b44543017a9c9b2d437c670627e96ad06",
"shasum": ""
},
"require": {
@ -1268,7 +1268,7 @@
],
"description": "Symfony Finder Component",
"homepage": "https://symfony.com",
"time": "2018-07-26T11:24:31+00:00"
"time": "2018-10-03T08:47:56+00:00"
},
{
"name": "symfony/polyfill-mbstring",
@ -1331,16 +1331,16 @@
},
{
"name": "symfony/translation",
"version": "v4.1.4",
"version": "v4.1.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/translation.git",
"reference": "fa2182669f7983b7aa5f1a770d053f79f0ef144f"
"reference": "9f0b61e339160a466ebcde167a6c5521c810e304"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/translation/zipball/fa2182669f7983b7aa5f1a770d053f79f0ef144f",
"reference": "fa2182669f7983b7aa5f1a770d053f79f0ef144f",
"url": "https://api.github.com/repos/symfony/translation/zipball/9f0b61e339160a466ebcde167a6c5521c810e304",
"reference": "9f0b61e339160a466ebcde167a6c5521c810e304",
"shasum": ""
},
"require": {
@ -1396,7 +1396,7 @@
],
"description": "Symfony Translation Component",
"homepage": "https://symfony.com",
"time": "2018-08-07T12:45:11+00:00"
"time": "2018-10-02T16:36:10+00:00"
},
{
"name": "tuupola/callable-handler",

View File

@ -25,3 +25,20 @@ Setting('STOCK_BARCODE_LOOKUP_PLUGIN', 'DemoBarcodeLookupPlugin');
# If, however, your webserver does not support URL rewriting,
# set this to true
Setting('DISABLE_URL_REWRITING', false);
# Default user settings
# These settings can be changed per user, here the defaults
# are defined which are used when the user has not changed the setting so far
# Night mode related
DefaultUserSetting('night_mode_enabled', false); // If night mode is enabled always
DefaultUserSetting('auto_night_mode_enabled', false); // If night mode is enabled automatically when inside a given time range (see the two settings below)
DefaultUserSetting('auto_night_mode_time_range_from', "20:00"); // Format HH:mm
DefaultUserSetting('auto_night_mode_time_range_to', "07:00"); // Format HH:mm
DefaultUserSetting('auto_night_mode_time_range_goes_over_midnight', true); // If the time range above goes over midnight
DefaultUserSetting('currently_inside_night_mode_range', false); // If we're currently inside of night mode time range (this is not user configurable, but stored as a user setting because it's evaluated client side to be able to use the client time instead of the maybe different server time)
# If the page should be automatically reloaded when there was
# an external change
DefaultUserSetting('auto_reload_on_db_change', true);

View File

@ -5,6 +5,7 @@ namespace Grocy\Controllers;
use \Grocy\Services\DatabaseService;
use \Grocy\Services\ApplicationService;
use \Grocy\Services\LocalizationService;
use \Grocy\Services\UsersService;
class BaseController
{
@ -41,6 +42,15 @@ class BaseController
return $container->UrlManager->ConstructUrl($relativePath, $isResource);
});
try {
$usersService = new UsersService();
$container->view->set('userSettings', $usersService->GetUserSettings(GROCY_USER_ID));
}
catch (\Exception $ex)
{
// Happens when database is not initialised or migrated...
}
$this->AppContainer = $container;
}

View File

@ -0,0 +1,31 @@
<?php
namespace Grocy\Controllers;
class EquipmentController extends BaseController
{
public function Overview(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
return $this->AppContainer->view->render($response, 'equipment', [
'equipment' => $this->Database->equipment()->orderBy('name')
]);
}
public function EditForm(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
if ($args['equipmentId'] == 'new')
{
return $this->AppContainer->view->render($response, 'equipmentform', [
'mode' => 'create'
]);
}
else
{
return $this->AppContainer->view->render($response, 'equipmentform', [
'equipment' => $this->Database->equipment($args['equipmentId']),
'mode' => 'edit'
]);
}
}
}

View File

@ -14,7 +14,7 @@ class FilesApiController extends BaseApiController
protected $FilesService;
public function Upload(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
public function UploadFile(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
try
{
@ -29,6 +29,67 @@ class FilesApiController extends BaseApiController
$data = $request->getBody()->getContents();
file_put_contents($this->FilesService->GetFilePath($args['group'], $fileName), $data);
return $this->ApiResponse(array('success' => true));
}
catch (\Exception $ex)
{
return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage());
}
}
public function ServeFile(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
try
{
if (isset($request->getQueryParams()['file_name']) && !empty($request->getQueryParams()['file_name']) && IsValidFileName($request->getQueryParams()['file_name']))
{
$fileName = $request->getQueryParams()['file_name'];
}
else
{
throw new \Exception('file_name query parameter missing or contains an invalid filename');
}
$filePath = $this->FilesService->GetFilePath($args['group'], $fileName);
if (file_exists($filePath))
{
$response->write(file_get_contents($filePath));
$response = $response->withHeader('Content-Type', mime_content_type($filePath));
return $response->withHeader('Content-Disposition', 'inline; filename="' . $fileName . '"');
}
else
{
return $this->VoidApiActionResponse($response, false, 404, 'File not found');
}
}
catch (\Exception $ex)
{
return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage());
}
}
public function DeleteFile(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
try
{
if (isset($request->getQueryParams()['file_name']) && !empty($request->getQueryParams()['file_name']) && IsValidFileName($request->getQueryParams()['file_name']))
{
$fileName = $request->getQueryParams()['file_name'];
}
else
{
throw new \Exception('file_name query parameter missing or contains an invalid filename');
}
$filePath = $this->FilesService->GetFilePath($args['group'], $fileName);
if (file_exists($filePath))
{
unlink($filePath);
}
return $this->ApiResponse(array('success' => true));
}
catch (\Exception $ex)
{

View File

@ -30,7 +30,7 @@ class LoginController extends BaseController
if ($user !== null && password_verify($inputPassword, $user->password))
{
$sessionKey = $this->SessionService->CreateSession($user->id, $stayLoggedInPermanently);
setcookie($this->SessionCookieName, $sessionKey, time() + 31220640000); // Cookie expires in 999 years, but session validity is up to SessionService
setcookie($this->SessionCookieName, $sessionKey, intval(time() + 31220640000)); // Cookie expires in 999 years, but session validity is up to SessionService
if (password_needs_rehash($user->password, PASSWORD_DEFAULT))
{

View File

@ -126,7 +126,7 @@ class StockApiController extends BaseApiController
$nextXDays = $request->getQueryParams()['expiring_days'];
}
$expiringProducts = $this->StockService->GetExpiringProducts($nextXDays);
$expiringProducts = $this->StockService->GetExpiringProducts($nextXDays, true);
$expiredProducts = $this->StockService->GetExpiringProducts(-1);
$missingProducts = $this->StockService->GetMissingProducts();
return $this->ApiResponse(array(

View File

@ -23,7 +23,8 @@ class StockController extends BaseController
'locations' => $this->Database->locations()->orderBy('name'),
'currentStock' => $this->StockService->GetCurrentStock(),
'missingProducts' => $this->StockService->GetMissingProducts(),
'nextXDays' => 5
'nextXDays' => 5,
'productGroups' => $this->Database->product_groups()->orderBy('name')
]);
}

View File

@ -20,4 +20,22 @@ class SystemApiController extends BaseApiController
'changed_time' => $this->DatabaseService->GetDbChangedTime()
));
}
public function LogMissingLocalization(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
if (GROCY_MODE === 'dev')
{
try
{
$requestBody = $request->getParsedBody();
$this->LocalizationService->LogMissingLocalization(GROCY_CULTURE, $requestBody['text']);
return $this->ApiResponse(array('success' => true));
}
catch (\Exception $ex)
{
return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage());
}
}
}
}

View File

@ -68,4 +68,32 @@ class UsersApiController extends BaseApiController
return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage());
}
}
public function GetUserSetting(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
try
{
$value = $this->UsersService->GetUserSetting(GROCY_USER_ID, $args['settingKey']);
return $this->ApiResponse(array('value' => $value));
}
catch (\Exception $ex)
{
return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage());
}
}
public function SetUserSetting(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{
try
{
$requestBody = $request->getParsedBody();
$value = $this->UsersService->SetUserSetting(GROCY_USER_ID, $args['settingKey'], $requestBody['value']);
return $this->ApiResponse(array('success' => true));
}
catch (\Exception $ex)
{
return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage());
}
}
}

View File

@ -44,6 +44,47 @@
}
}
},
"/system/log-missing-localization": {
"post": {
"description": "Logs a missing localization string (only when MODE == 'dev', so should only be called then)",
"tags": [
"System"
],
"requestBody": {
"description": "A valid MissingLocalizationRequest object",
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/MissingLocalizationRequest"
}
}
}
},
"responses": {
"200": {
"description": "A VoidApiActionResponse object",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/VoidApiActionResponse"
}
}
}
},
"400": {
"description": "A VoidApiActionResponse object",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorExampleVoidApiActionResponse"
}
}
}
}
}
}
},
"/get-objects/{entity}": {
"get": {
"description": "Returns all objects of the given entity",
@ -390,9 +431,58 @@
}
}
},
"/files/upload/{group}": {
"post": {
"description": "Uploads a single file to /data/storage/{group}/{file_name}",
"/file/{group}": {
"get": {
"description": "Serves the given file (with proper Content-Type header)",
"tags": [
"Files"
],
"parameters": [
{
"in": "path",
"name": "group",
"required": true,
"description": "The file group",
"schema": {
"type": "string"
}
},
{
"in": "query",
"name": "file_name",
"required": true,
"description": "The file name (including extension)",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "The binary file contents (Content-Type header is automatically set based on the file type)",
"content": {
"application/octet-stream": {
"schema": {
"type": "string",
"format": "binary"
}
}
}
},
"400": {
"description": "A VoidApiActionResponse object",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorExampleVoidApiActionResponse"
}
}
}
}
}
},
"put": {
"description": "Uploads a single file to /data/storage/{group}/{file_name} (you need to remember the group and file name to get or delete it again)",
"tags": [
"Files"
],
@ -448,6 +538,54 @@
}
}
}
},
"delete": {
"description": "Deletes the given file",
"tags": [
"Files"
],
"parameters": [
{
"in": "path",
"name": "group",
"required": true,
"description": "The file group",
"schema": {
"type": "string"
}
},
{
"in": "query",
"name": "file_name",
"required": true,
"description": "The file name (including extension)",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "A VoidApiActionResponse object",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/VoidApiActionResponse"
}
}
}
},
"400": {
"description": "A VoidApiActionResponse object",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorExampleVoidApiActionResponse"
}
}
}
}
}
}
},
"/users/get": {
@ -617,6 +755,97 @@
}
}
},
"/user/settings/{settingKey}": {
"get": {
"description": "Gets the given setting of the currently logged on user",
"tags": [
"User settings"
],
"parameters": [
{
"in": "path",
"name": "settingKey",
"required": true,
"description": "The key of the user setting",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "A UserSetting object",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserSetting"
}
}
}
},
"400": {
"description": "A VoidApiActionResponse object",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorExampleVoidApiActionResponse"
}
}
}
}
}
},
"post": {
"description": "Sets the given setting of the currently logged on user",
"tags": [
"User settings"
],
"requestBody": {
"description": "A valid UserSetting object",
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserSetting"
}
}
}
},
"parameters": [
{
"in": "path",
"name": "settingKey",
"required": true,
"description": "The key of the user setting",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "A VoidApiActionResponse object",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/VoidApiActionResponse"
}
}
}
},
"400": {
"description": "A VoidApiActionResponse object",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorExampleVoidApiActionResponse"
}
}
}
}
}
}
},
"/stock/add-product/{productId}/{amount}": {
"get": {
"description": "Adds the the given amount of the given product to stock",
@ -1448,7 +1677,8 @@
"recipes_pos",
"tasks",
"task_categories",
"product_groups"
"product_groups",
"equipment"
]
},
"StockTransactionType": {
@ -1500,6 +1730,9 @@
"minimum": 0,
"default": 0
},
"picture_file_name": {
"type": "string"
},
"row_created_timestamp": {
"type": "string",
"format": "date-time"
@ -2098,6 +2331,22 @@
"format": "date-time"
}
}
},
"UserSetting": {
"type": "object",
"properties": {
"value": {
"type": "string"
}
}
},
"MissingLocalizationRequest": {
"type": "object",
"properties": {
"text": {
"type": "string"
}
}
}
},
"examples": {

View File

@ -145,6 +145,17 @@ function Setting(string $name, $value)
}
}
global $GROCY_DEFAULT_USER_SETTINGS;
$GROCY_DEFAULT_USER_SETTINGS = array();
function DefaultUserSetting(string $name, $value)
{
global $GROCY_DEFAULT_USER_SETTINGS;
if (!array_key_exists($name, $GROCY_DEFAULT_USER_SETTINGS))
{
$GROCY_DEFAULT_USER_SETTINGS[$name] = $value;
}
}
function GetUserDisplayName($user)
{
$displayName = '';
@ -181,7 +192,7 @@ function Pluralize($number, $singularForm, $pluralForm)
function IsValidFileName($fileName)
{
if(preg_match('#^[a-z0-9]+\.[a-z]+?$#i', $fileName))
if(preg_match('=^[^/?*;:{}\\\\]+\.[^/?*;:{}\\\\]+$=', $fileName))
{
return true;
}

View File

@ -249,6 +249,36 @@ return array(
'Already expired' => 'Bereits abgelaufen',
'Due soon' => 'Bald fällig',
'Overdue' => 'Überfällig',
'View settings' => 'Ansichtseinstellungen',
'Auto reload on external changes' => 'Autom. akt. bei externen Änderungen',
'Enable night mode' => 'Nachtmodus aktivieren',
'Auto enable in time range' => 'Autom. akt. in diesem Zeitraum',
'From' => 'Von',
'in format' => 'im Format',
'To' => 'Bis',
'Time range goes over midnight' => 'Zeitraum geht über Mitternacht',
'Product picture' => 'Produktbild',
'No file selected' => 'Keine Datei ausgewählt',
'If you don\'t select a file, the current picture will not be altered' => 'Wenn du keine Datei auswählst, wird das aktuelle Bild nicht verändert',
'Current picture' => 'Aktuelles Bild',
'Delete' => 'Löschen',
'The current picture will be deleted when you save the product' => 'Das aktuelle Bild wird beim Speichern des Produkts gelöscht',
'Select file' => 'Datei auswählen',
'Image of product #1' => 'Bild des Produkts #1',
'This product cannot be deleted because it is in stock, please remove the stock amount first.' => 'Dieses Produkt kann nicht gelöscht werden, da es auf Lager ist, bitte zuerst den Bestand entfernen.',
'Delete not possible' => 'Löschen nicht möglich',
'Equipment' => 'Ausstattung',
'Instruction manual' => 'Bedienungsanleitung',
'The selected equipment has no instruction manual' => 'Das ausgewählte Gerät hat keine Bedienungsanleitung',
'Notes' => 'Notizen',
'Edit equipment' => 'Geräte bearbeiten',
'Create equipment' => 'Geräte erstellen',
'If you don\'t select a file, the current instruction manual will not be altered' => 'Wenn du keine Datei auswählst, wird die aktuelle Bedienungsanleitung nicht verändert',
'Current instruction manual' => 'Aktuelle Bedienungsanleitung',
'No instruction manual available' => 'Keine Bedienungsanleitung vorhanden',
'The current instruction manual will be deleted when you save the equipment' => 'Die aktuelle Bedienungsanleitung wird beim Speichern des Geräts gelöscht',
'No picture available' => 'Kein Bild vorhanden',
'Filter by product group' => 'Nach Produktgruppe filtern',
//Constants
'manually' => 'Manuell',
@ -259,6 +289,7 @@ return array(
'timeago_nan' => 'vor NaN Jahren',
'moment_locale' => 'de',
'datatables_localization' => '{"sEmptyTable":"Keine Daten in der Tabelle vorhanden","sInfo":"_START_ bis _END_ von _TOTAL_ Einträgen","sInfoEmpty":"Keine Daten vorhanden","sInfoFiltered":"(gefiltert von _MAX_ Einträgen)","sInfoPostFix":"","sInfoThousands":".","sLengthMenu":"_MENU_ Einträge anzeigen","sLoadingRecords":"Wird geladen ..","sProcessing":"Bitte warten ..","sSearch":"Suchen","sZeroRecords":"Keine Einträge vorhanden","oPaginate":{"sFirst":"Erste","sPrevious":"Zurück","sNext":"Nächste","sLast":"Letzte"},"oAria":{"sSortAscending":": aktivieren, um Spalte aufsteigend zu sortieren","sSortDescending":": aktivieren, um Spalte absteigend zu sortieren"},"select":{"rows":{"0":"Zum Auswählen auf eine Zeile klicken","1":"1 Zeile ausgewählt","_":"%d Zeilen ausgewählt"}},"buttons":{"print":"Drucken","colvis":"Spalten","copy":"Kopieren","copyTitle":"In Zwischenablage kopieren","copyKeys":"Taste <i>ctrl</i> oder <i>⌘</i> + <i>C</i> um Tabelle<br>in Zwischenspeicher zu kopieren.<br><br>Um abzubrechen die Nachricht anklicken oder Escape drücken.","copySuccess":{"1":"1 Spalte kopiert","_":"%d Spalten kopiert"}}}',
'summernote_locale' => 'de-DE',
//Demo data
'Cookies' => 'Cookies',
@ -330,5 +361,7 @@ return array(
'Tinned food' => 'Konservern',
'Butchery products' => 'Metzgerei',
'Vegetables/Fruits' => 'Obst/Gemüse',
'Refrigerated products' => 'Kühlregal'
'Refrigerated products' => 'Kühlregal',
'Coffee machine' => 'Kaffeemaschine',
'Dishwasher' => 'Spülmaschine'
);

View File

@ -9,5 +9,6 @@ return array(
'timeago_locale' => 'en',
'timeago_nan' => 'NaN years ago',
'moment_locale' => '',
'datatables_localization' => '{"sEmptyTable":"No data available in table","sInfo":"Showing _START_ to _END_ of _TOTAL_ entries","sInfoEmpty":"Showing 0 to 0 of 0 entries","sInfoFiltered":"(filtered from _MAX_ total entries)","sInfoPostFix":"","sInfoThousands":",","sLengthMenu":"Show _MENU_ entries","sLoadingRecords":"Loading...","sProcessing":"Processing...","sSearch":"Search:","sZeroRecords":"No matching records found","oPaginate":{"sFirst":"First","sLast":"Last","sNext":"Next","sPrevious":"Previous"},"oAria":{"sSortAscending":": activate to sort column ascending","sSortDescending":": activate to sort column descending"}}'
'datatables_localization' => '{"sEmptyTable":"No data available in table","sInfo":"Showing _START_ to _END_ of _TOTAL_ entries","sInfoEmpty":"Showing 0 to 0 of 0 entries","sInfoFiltered":"(filtered from _MAX_ total entries)","sInfoPostFix":"","sInfoThousands":",","sLengthMenu":"Show _MENU_ entries","sLoadingRecords":"Loading...","sProcessing":"Processing...","sSearch":"Search:","sZeroRecords":"No matching records found","oPaginate":{"sFirst":"First","sLast":"Last","sNext":"Next","sPrevious":"Previous"},"oAria":{"sSortAscending":": activate to sort column ascending","sSortDescending":": activate to sort column descending"}}',
'summernote_locale' => ''
);

View File

@ -150,6 +150,7 @@ return array(
'timeago_nan' => 'NaN anni fa',
'moment_locale' => 'it',
'datatables_localization' => '{"sEmptyTable":"Nessun dato disponibile","sInfo":"Mostrando da _START_ a _END_ di _TOTAL_ voci","sInfoEmpty":"Mostrando da 0 a 0 di 0 voci","sInfoFiltered":"(Filtrato da _MAX_ voci totali)","sInfoPostFix":"","sInfoThousands":",","sLengthMenu":"Mostra _MENU_ voci","sLoadingRecords":"Caricando...","sProcessing":"Calcolando...","sSearch":"Cerca:","sZeroRecords":"Nessun risultato trovato","oPaginate":{"sFirst":"Prima","sLast":"Ultima","sNext":"Prossima","sPrevious":"Precedente"},"oAria":{"sSortAscending":": ordine crescente","sSortDescending":": ordine decrescente"}}',
'summernote_locale' => 'it-IT',
//Demo data
'Cookies' => 'Biscotti',

View File

@ -9,14 +9,14 @@ return array(
'Amount' => 'Antall',
'Next best before date' => 'Kommende best før dato',
'Logout' => 'Logg ut',
'Chores overview' => 'Oversikt Husarbeid',
'Batteries overview' => 'Oversikt Batteri',
'Chores overview' => 'Oversikt husarbeid',
'Batteries overview' => 'Oversikt batteri',
'Purchase' => 'Innkjøp',
'Consume' => 'Forbrukt',
'Inventory' => 'Endre Husholdning',
'Consume' => 'Forbruk produkt',
'Inventory' => 'Endre husholdning',
'Shopping list' => 'Handleliste',
'Chore tracking' => 'Logge Husarbeid',
'Battery tracking' => 'Batteri Ladesyklus',
'Chore tracking' => 'Logge husarbeid',
'Battery tracking' => 'Batteri ladesyklus',
'Products' => 'Produkter',
'Locations' => 'Lokasjoner',
'Quantity units' => 'Forpakning',
@ -41,9 +41,9 @@ return array(
'New amount' => 'Nytt antall',
'Note' => 'Info',
'Tracked time' => 'Tid utført/ ladet',
'Chore overview' => 'Oversikt Husarbeid',
'Chore overview' => 'Oversikt husarbeid',
'Tracked count' => 'Antall utførelser/ ladninger',
'Battery overview' => 'Batteri Oversikt',
'Battery overview' => 'Batteri oversikt',
'Charge cycles count' => 'Antall ladesykluser',
'Create shopping list item' => 'Opprett handelisteoppføring',
'Edit shopping list item' => 'Endre på handlelistoppføring',
@ -53,9 +53,9 @@ return array(
'Name' => 'Navn',
'Location' => 'Lokasjon',
'Min. stock amount' => 'Minimums antall for husholdingen',
'QU purchase' => 'FPK innkjøp',
'QU stock' => 'FPK husholdning',
'QU factor' => 'FPK faktor',
'QU purchase' => 'Forpakingsfaktor innkjøp',
'QU stock' => 'Forpakingsfaktor husholdning',
'QU factor' => 'Forpakingsfaktor',
'Description' => 'Beskrivelse',
'Create product' => 'Opprett produkt',
'Barcode(s)' => 'Strekkode(r)',
@ -87,16 +87,16 @@ return array(
'Username' => 'Brukernavn',
'Password' => 'Passord',
'Invalid credentials, please try again' => 'Feil brukernavn og/eller passord, prøv igjen',
'Are you sure to delete battery "#1"?' => 'Er du sikker du ønsker å slette Batteri "#1"?',
'Are you sure to delete battery "#1"?' => 'Er du sikker du ønsker å slette batteri "#1"?',
'Yes' => 'Ja',
'No' => 'Nei',
'Are you sure to delete chore "#1"?' => 'Er du sikker på du ønsker å slette husarbeid oppgave "#1"?',
'"#1" could not be resolved to a product, how do you want to proceed?' => '"#1" kunne ikke bli tildelt et produkt, hvordan ønsker du å fortsette?',
'Create or assign product' => 'Opprett eller tildel til produkt',
'Create or assign product' => 'Opprett eller tildel til et produkt',
'Cancel' => 'Avbryt',
'Add as new product' => 'Legg til som nytt produkt',
'Add as barcode to existing product' => 'Legg til strekkode til allerede eksisterende produkt',
'Add as new product and prefill barcode' => 'Legg til som nytt produkt med forhåndsutfylt strekkode',
'Add as new product and prefill barcode' => 'Legg til som nytt produkt med forhåndsfylt strekkode',
'Are you sure to delete quantity unit "#1"?' => 'Er du sikker du ønsker å slette forpakning "#1"?',
'Are you sure to delete product "#1"?' => 'Er du sikker du ønsker å slette produkt "#1"?',
'Are you sure to delete location "#1"?' => 'Er du sikker du ønsker å slette lokasjon "#1"?',
@ -114,17 +114,17 @@ return array(
'Removed #1 #2 of #3 from stock' => 'Fjernet #1 #2 #3 fra husholdningen',
'About grocy' => 'Om Grocy',
'Close' => 'Lukk',
'#1 batteries are due to be charged within the next #2 days' => '#1 Batteri må lades innen de #2 neste dagene',
'#1 batteries are due to be charged within the next #2 days' => '#1 batteri må lades innen de #2 neste dagene',
'#1 batteries are overdue to be charged' => '#1 Batteri har gått over fristen for å bli ladet opp',
'#1 chores are due to be done within the next #2 days' => '#1 husarbeid(s) oppgave(r) skal gjøres inne de #2 neste dagene',
'#1 chores are overdue to be done' => '#1 husarbeid(s) oppgave(r) har gått over fristen for utførelse',
'#1 chores are due to be done within the next #2 days' => '#1 husarbeids oppgaver skal gjøres inne de #2 neste dagene',
'#1 chores are overdue to be done' => '#1 husarbeids oppgaver har gått over fristen for utførelse',
'Released on' => 'Utgitt',
'Consume #3 #1 of #2' => 'Forbruk #3 #1 #2',
'Added #1 #2 of #3 to stock' => '#1 #2 #3 lagt til i husholdningen',
'Stock amount of #1 is now #2 #3' => 'Husholdning antall #1 er nå #2 #3',
'Tracked execution of chore #1 on #2' => 'Utførte husarbeid oppgave "#1" den #2',
'Tracked charge cycle of battery #1 on #2' => 'Ladet #1 den #2',
'Consume all #1 which are currently in stock' => 'Konsumér alle #1 som er i husholdningen',
'Consume all #1 which are currently in stock' => 'Forbruk alle #1 som er i husholdningen',
'All' => 'Alle',
'Track charge cycle of battery #1' => '#1 ladet',
'Track execution of chore #1' => 'Utfør husarbeid oppgave #1',
@ -132,7 +132,7 @@ return array(
'Search' => 'Søk',
'Not logged in' => 'Ikke logget inn',
'You have to select a product' => 'Du må velge et produkt',
'You have to select a chore' => 'Du må velge en husarbeid oppgave',
'You have to select a chore' => 'Du må velge en husarbeids oppgave',
'You have to select a battery' => 'Du må velge et batteri',
'A name is required' => 'Vennligst fyll inn et navn',
'A location is required' => 'En lokasjon kreves',
@ -153,7 +153,7 @@ return array(
'Are you sure to delete recipe "#1"?' => 'Er du sikker du ønsker å slette oppskrift "#1"?',
'Are you sure to delete recipe ingredient "#1"?' => 'Er du sikker du ønsker å slette ingrediens "#1" fra oppskriften?',
'Are you sure to empty the shopping list?' => 'Er du sikker du ønsker å slette handlelisten?',
'Clear list' => 'Tøm liste',
'Clear list' => 'Slett handleliste',
'Requirements fulfilled' => 'Har jeg alt jeg trenger for denne oppskriften?',
'Put missing products on shopping list' => 'Legg manglende produkter til handlelisten',
'Not enough in stock, #1 ingredients missing' => 'Ikke nok i husholdningen, #1 ingredienser mangler',
@ -186,7 +186,7 @@ return array(
'Filter by chore' => 'Filtrér husarbeid',
'Chores analysis' => 'Statistikk husarbeid',
'0 means suggestions for the next charge cycle are disabled' => '0 betyr neste ladesyklus er avslått',
'Charge cycle interval (days)' => 'Ladesyklysintervall (Dager)',
'Charge cycle interval (days)' => 'Ladesyklysintervall (dager)',
'Last price' => 'Siste pris',
'Price history' => 'Prishistorikk',
'No price history available' => 'Ingen prishistorikk tilgjengelig',
@ -198,8 +198,8 @@ return array(
'#1 product is below defined min. stock amount' => '#1 Produkt er under minimums husholdningsnivå',
'Unit' => 'Enhet',
'Units' => 'Enheter',
'#1 chore is due to be done within the next #2 days' => '#1 husarbeid oppgave(r) skal gjøres inne de #2 neste dagene',
'#1 chore is overdue to be done' => '#1 husarbeid(s) oppgave(r) har gått over fristen for utførelse',
'#1 chore is due to be done within the next #2 days' => '#1 husarbeid oppgave skal gjøres inne de #2 neste dagene',
'#1 chore is overdue to be done' => '#1 husarbeid oppgave har gått over fristen for utførelse',
'#1 battery is due to be charged within the next #2 days' => '#1 Batteri må lades innen #2 dager',
'#1 battery is overdue to be charged' => '#1 Batteri har gått over fristen for å lades',
'#1 unit was automatically added and will apply in addition to the amount entered here' => '#1 enhet ble automatisk lagt til i tillegg til hva som blir skrevet inn her',
@ -209,10 +209,10 @@ return array(
'This cannot be lower than #1' => 'Dette kan ikke være lavere enn #1',
'-1 means that this product never expires' => '-1 Betyr at dette produktet aldri går ut på dato',
'Quantity unit' => 'Forpakning',
'Only check if a single unit is in stock (a different quantity can then be used above)' => 'Huk av hvis du ønsker å bruke mindre enn forpakningsstørrelse i husholdningen',
'Are you sure to consume all ingredients needed by recipe "#1" (ingredients marked with "check only if a single unit is in stock" will be ignored)?' => 'Er du sikker du ønsker å forbruke alle ingredienser for "#1" oppskriften? (Ingredienser merket med "bruke mindre enn forpakningsstørrelse i husholdningen" blir ignorert',
'Only check if a single unit is in stock (a different quantity can then be used above)' => 'Ønsker du å bruke mindre enn forpakningsstørrelse?',
'Are you sure to consume all ingredients needed by recipe "#1" (ingredients marked with "check only if a single unit is in stock" will be ignored)?' => 'Er du sikker du ønsker å forbruke alle ingredienser for "#1" oppskriften? (Ingredienser merket med "Ønsker du å bruke mindre enn forpakningsstørrelse?" blir ignorert',
'Removed all ingredients of recipe "#1" from stock' => 'Fjern alle ingredienser for "#1" oppskriften fra husholdningen.',
'Consume all ingredients needed by this recipe' => 'Konsumer alle ingredienser for denne oppskriften',
'Consume all ingredients needed by this recipe' => 'Forbruk alle ingredienser for denne oppskriften',
'Click to show technical details' => 'Klikk for å vise teknisk informasjon',
'Error while saving, probably this item already exists' => 'Kunne ikke lagre, produkt er lagt til fra før',
'Error details' => 'Detaljer om feil',
@ -236,7 +236,7 @@ return array(
'Edit task category' => 'Endre oppgave kategori',
'Create task category' => 'Opprett oppgave kategori',
'Product groups' => 'Produktgrupper',
'Ungrouped' => 'Ikke i grupper',
'Ungrouped' => 'Mangler gruppe',
'Create product group' => 'Opprett produkt gruppe',
'Edit product group' => 'Endre produkt gruppe',
'Product group' => 'Produktgruppe',
@ -249,7 +249,36 @@ return array(
'Already expired' => 'Utgått på dato',
'Due soon' => 'Forfaller snart',
'Overdue' => 'Forfalt',
'View settings' => 'Se instillinger',
'Auto reload on external changes' => 'Automatisk fornying ved ekstern endring',
'Enable night mode' => 'Aktiver nattmodus',
'Auto enable in time range' => 'Automatisk aktivering i tidsrommet',
'From' => 'Fra',
'in format' => 'format',
'To' => 'Til',
'Time range goes over midnight' => 'Tidsrommet går over midnatt',
'Product picture' => 'Produktbilde',
'No file selected' => 'Ingen fil merket',
'If you don\'t select a file, the current picture will not be altered' => 'Hvis du ikke velger et bilde, vil nåværende bilde ikke bli endret',
'Current picture' => 'Nåværende bilde',
'Delete' => 'Slett',
'The current picture will be deleted when you save the product' => 'Nåværende bilde vil bli slettet når du lagrer produktet',
'Select file' => 'Velg fil',
'Image of product #1' => 'Bilde av produkt #1',
'This product cannot be deleted because it is in stock, please remove the stock amount first.' => 'Dette produktet kan ikke slettes fordi det er gjenværende produkter i husholdningen',
'Delete not possible' => 'Ikke mulig å slette',
'Equipment' => 'Utstyr',
'Instruction manual' => 'Instruksjonsmanual',
'The selected equipment has no instruction manual' => 'Merket utstyr har ingen instruksjonsmanual',
'Notes' => 'Notater',
'Edit equipment' => 'Endre utstyr',
'Create equipment' => 'Opprett utstyr',
'If you don\'t select a file, the current instruction manual will not be altered' => 'Hvis du ikke velger en instruksjonsmanual, vil nåværende instruksjonsmanual ikke bli endret',
'Current instruction manual' => 'Nåværende instruksjonsmanual',
'No instruction manual available' => 'Ingen instruksjonsmanual tilgjengelig',
'The current instruction manual will be deleted when you save the equipment' => 'Nåværende instruksjonsmanual vil bli slettet når du lagrer utstyret',
'No picture available' => 'Ingen bilde tilgjengelig',
//Constants
'manually' => 'Manuel',
'dynamic-regular' => 'Automatisk',
@ -259,6 +288,7 @@ return array(
'timeago_nan' => 'for NaN År',
'moment_locale' => 'nb',
'datatables_localization' => '{"sEmptyTable":"Det finnes ingen data i tabellen","sInfo":"_START_ fra _END_ til _TOTAL_ skriv","sInfoEmpty":"Ingen data tilgjengelign","sInfoFiltered":"(filtrert fra _MAX_ skriv)","sInfoPostFix":"","sInfoThousands":".","sLengthMenu":"_MENU_ registrer deg","sLoadingRecords":"Laster ..","sProcessing":"Vennligst vent ..","sSearch":"Søk","sZeroRecords":"Ingen oppføringer tilgjengelig","oPaginate":{"sFirst":"Første","sPrevious":"Bakover","sNext":"Neste","sLast":"Siste"},"oAria":{"sSortAscending":": Sortér stigende","sSortDescending":": Sortér synkende"},"select":{"rows":{"0":"klikk på en linje for å velge","1":"1 linje valgt","_":"%d linger valgt"}},"buttons":{"print":"Print","colvis":"Søyle","copy":"Kopi","copyTitle":"Kopier til utklippstavlen","copyKeys":"Trykk <i>ctrl</i> eller <i>⌘</i> + <i>C</i> for å kopiere tabell<br> til utklipptavlen.<br><br>For å avbryte, klikke på meldingen eller trykk på ESC.","copySuccess":{"1":"1 Kolonne kopiert","_":"%d kolonne kopiert"}}}',
'summernote_locale' => 'nb-NO',
//Demo data
'Cookies' => 'Cookies',
@ -326,9 +356,11 @@ return array(
'Fork and improve grocy' => 'Fork og forbedre grocy',
'Find a solution for what to do when I forget the door keys' => 'Finne på løsning for hva jeg skal gjøre når jeg mister dørnøklene',
'Sweets' => 'Godteri',
'Bakery products' => 'Produkt fra bakeren ',
'Bakery products' => 'Bakevarer',
'Tinned food' => 'Boksemat',
'Butchery products' => 'Produkt fra slakteren',
'Butchery products' => 'Kjøtt/ Ferskvare',
'Vegetables/Fruits' => 'Frukt/ Grønnsaker',
'Refrigerated products' => 'Kjølte produkter'
'Refrigerated products' => 'Frysedisk',
'Coffee machine' => 'Kaffetrakter',
'Dishwasher' => 'Oppvaskmaskin'
);

View File

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

27
migrations/0038.sql Normal file
View File

@ -0,0 +1,27 @@
DROP VIEW stock_missing_products;
CREATE VIEW stock_missing_products
AS
SELECT
p.id,
MAX(p.name) AS name,
p.min_stock_amount - IFNULL(SUM(s.amount), 0) AS amount_missing,
CASE WHEN s.id IS NOT NULL THEN 1 ELSE 0 END AS is_partly_in_stock
FROM products p
LEFT JOIN stock s
ON p.id = s.product_id
WHERE p.min_stock_amount != 0
GROUP BY p.id
HAVING IFNULL(SUM(s.amount), 0) < p.min_stock_amount;
DROP VIEW stock_current;
CREATE VIEW stock_current
AS
SELECT product_id, SUM(amount) AS amount, MIN(best_before_date) AS best_before_date
FROM stock
GROUP BY product_id
UNION
SELECT id, 0, null
FROM stock_missing_products
WHERE is_partly_in_stock = 0;

10
migrations/0039.sql Normal file
View File

@ -0,0 +1,10 @@
CREATE TABLE user_settings (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
user_id INTEGER NOT NULL,
key TEXT NOT NULL,
value TEXT,
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime')),
row_updated_timestamp DATETIME DEFAULT (datetime('now', 'localtime')),
UNIQUE(user_id, key)
);

2
migrations/0040.sql Normal file
View File

@ -0,0 +1,2 @@
ALTER TABLE products
ADD picture_file_name TEXT;

7
migrations/0041.sql Normal file
View File

@ -0,0 +1,7 @@
CREATE TABLE equipment (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
name TEXT NOT NULL UNIQUE,
description TEXT,
instruction_manual_file_name TEXT,
row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime'))
)

View File

@ -23,6 +23,7 @@
"jquery-ui-dist": "^1.12.1",
"moment": "^2.22.2",
"startbootstrap-sb-admin": "^4.0.0",
"summernote": "^0.8.10",
"swagger-ui-dist": "^3.17.3",
"tagmanager": "https://github.com/max-favilli/tagmanager.git#3.0.2",
"tempusdominus-bootstrap-4": "^5.0.1",

View File

@ -50,12 +50,13 @@ a.discrete-link:focus {
}
.fullscreen {
z-index: 9999;
width: 100%;
height: 100%;
position: fixed;
top: 0;
left: 0;
z-index: 9999;
width: 100%;
height: 100%;
position: fixed;
top: 0;
left: 0;
overflow: auto;
}
.form-check-input.is-valid ~ .form-check-label,
@ -80,6 +81,12 @@ input::-webkit-inner-spin-button {
-webkit-appearance: none;
}
.centered-dialog .modal-title,
.centered-dialog .modal-body {
margin-left: auto;
margin-right: auto;
}
/* Navigation style customizations */
#mainNav {
background-color: #e5e5e5 !important;

View File

@ -197,3 +197,25 @@
.night-mode .typeahead .active {
background-color: #333131;
}
.night-mode .note-editor.note-frame .note-editing-area .note-editable {
color: #c1c1c1;
background-color: #333131;
}
.night-mode .bootstrap-datetimepicker-widget table td.day {
background-color: #333131;
}
.night-mode .bootstrap-datetimepicker-widget table td {
background-color: #333131;
}
.night-mode .bootstrap-datetimepicker-widget table td,
.night-mode .bootstrap-datetimepicker-widget table th {
background-color: #333131;
}
.night-mode .dropdown-menu {
background-color: #333131;
}

View File

@ -41,3 +41,26 @@ IsTouchInputDevice = function()
return false;
}
BoolVal = function(test)
{
var anything = test.toString().toLowerCase();
if (anything === true || anything === "true" || anything === "1" || anything === "on")
{
return true;
}
else
{
return false;
}
}
GetFileNameFromPath = function(path)
{
return path.split("/").pop().split("\\").pop();
}
GetFileExtension = function(pathOrFileName)
{
return pathOrFileName.split(".").pop();
}

View File

@ -3,6 +3,22 @@
var localizedText = Grocy.LocalizationStrings[text];
if (localizedText === undefined)
{
if (Grocy.Mode === 'dev')
{
jsonData = {};
jsonData.text = text;
Grocy.Api.Post('system/log-missing-localization', jsonData,
function(result)
{
// Nothing to do...
},
function(xhr)
{
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response)
}
);
}
localizedText = text;
}
@ -161,6 +177,68 @@ Grocy.Api.Post = function(apiFunction, jsonData, success, error)
xhr.send(JSON.stringify(jsonData));
};
Grocy.Api.UploadFile = function(file, group, fileName, success, error)
{
var xhr = new XMLHttpRequest();
var url = U('/api/file/' + group + '?file_name=' + encodeURIComponent(fileName));
xhr.onreadystatechange = function()
{
if (xhr.readyState === XMLHttpRequest.DONE)
{
if (xhr.status === 200)
{
if (success)
{
success(JSON.parse(xhr.responseText));
}
}
else
{
if (error)
{
error(xhr);
}
}
}
};
xhr.open('PUT', url, true);
xhr.setRequestHeader('Content-type', 'application/octet-stream');
xhr.send(file);
};
Grocy.Api.DeleteFile = function(fileName, group, success, error)
{
var xhr = new XMLHttpRequest();
var url = U('/api/file/' + group + '?file_name=' + encodeURIComponent(fileName));
xhr.onreadystatechange = function()
{
if (xhr.readyState === XMLHttpRequest.DONE)
{
if (xhr.status === 200)
{
if (success)
{
success(JSON.parse(xhr.responseText));
}
}
else
{
if (error)
{
error(xhr);
}
}
}
};
xhr.open('DELETE', url, true);
xhr.setRequestHeader('Content-type', 'application/json');
xhr.send();
};
Grocy.FrontendHelpers = { };
Grocy.FrontendHelpers.ValidateForm = function(formId)
{
@ -191,3 +269,74 @@ Grocy.FrontendHelpers.ShowGenericError = function(message, exception)
console.error(exception);
}
$("form").on("keyup paste", "input, textarea", function()
{
$(this).closest("form").addClass("is-dirty");
});
$("form").on("click", "select", function()
{
$(this).closest("form").addClass("is-dirty");
});
// Auto saving user setting controls
$(".user-setting-control").on("change", function()
{
var element = $(this);
var inputType = element.attr("type").toLowerCase();
var settingKey = element.attr("data-setting-key");
if (inputType === "checkbox")
{
value = element.is(":checked");
}
else
{
var value = element.val();
}
Grocy.UserSettings[settingKey] = value;
jsonData = { };
jsonData.value = value;
Grocy.Api.Post('user/settings/' + settingKey, jsonData,
function(result)
{
// Nothing to do...
},
function(xhr)
{
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response)
}
);
});
// Show file name Bootstrap custom file input
$('input.custom-file-input').on('change', function()
{
$(this).next('.custom-file-label').html(GetFileNameFromPath($(this).val()));
});
// Translation of "Browse"-button of Bootstrap custom file input
if ($(".custom-file-label").length > 0)
{
$("<style>").html('.custom-file-label::after { content: "' + L("Select file") + '"; }').appendTo("head");
}
ResizeResponsiveEmbeds = function(fillEntireViewport = false)
{
if (!fillEntireViewport)
{
var maxHeight = $("body").height() - $("#mainNav").outerHeight() - 62;
}
else
{
var maxHeight = $("body").height();
}
$(".embed-responsive").attr("height", maxHeight.toString() + "px");
}
$(window).on('resize', function()
{
ResizeResponsiveEmbeds($("body").hasClass("fullscreen-card"));
});

View File

@ -10,7 +10,8 @@
);
// Check if the database has changed once a minute
// If a change is detected, reload the current page, but only if already idling for at least 50 seconds
// If a change is detected, reload the current page, but only if already idling for at least 50 seconds,
// when there is no unsaved form data and when the user enabled auto reloading
setInterval(function()
{
Grocy.Api.Get('system/get-db-changed-time',
@ -21,7 +22,10 @@ setInterval(function()
{
if (Grocy.IdleTime >= 50)
{
window.location.reload();
if (BoolVal(Grocy.UserSettings.auto_reload_on_db_change) && $("form.is-dirty").length === 0 && !$("body").hasClass("fullscreen-card"))
{
window.location.reload();
}
}
Grocy.DatabaseChangedTime = newDbChangedTime;
@ -51,3 +55,8 @@ setInterval(function()
{
Grocy.IdleTime += 1;
}, 1000);
if (BoolVal(Grocy.UserSettings.auto_reload_on_db_change))
{
$("#auto-reload-enabled").prop("checked", true);
}

View File

@ -1,8 +1,6 @@
$("#night-mode-enabled").on("change", function()
{
var value = $(this).is(":checked");
window.localStorage.setItem("night_mode", value);
if (value)
{
$("body").addClass("night-mode");
@ -13,8 +11,96 @@
}
});
if (window.localStorage.getItem("night_mode") === "true")
$("#auto-night-mode-enabled").on("change", function()
{
$("body").addClass("night-mode");
$("#night-mode-enabled").prop("checked", true);
var value = $(this).is(":checked");
$("#auto-night-mode-time-range-from").prop("readonly", !value);
$("#auto-night-mode-time-range-to").prop("readonly", !value);
if (!value && !BoolVal(Grocy.UserSettings.night_mode_enabled))
{
$("body").removeClass("night-mode");
}
});
$(document).on("keyup", "#auto-night-mode-time-range-from, #auto-night-mode-time-range-to", function()
{
var value = $(this).val();
var valueIsValid = moment(value, "HH:mm", true).isValid();
if (valueIsValid)
{
$(this).removeClass("bg-danger");
}
else
{
$(this).addClass("bg-danger");
}
CheckNightMode();
});
$("#auto-night-mode-time-range-goes-over-midgnight").on("change", function()
{
CheckNightMode();
});
$("#night-mode-enabled").prop("checked", BoolVal(Grocy.UserSettings.night_mode_enabled));
$("#auto-night-mode-enabled").prop("checked", BoolVal(Grocy.UserSettings.auto_night_mode_enabled));
$("#auto-night-mode-time-range-goes-over-midgnight").prop("checked", BoolVal(Grocy.UserSettings.auto_night_mode_time_range_goes_over_midnight));
$("#auto-night-mode-enabled").trigger("change");
$("#auto-night-mode-time-range-from").val(Grocy.UserSettings.auto_night_mode_time_range_from);
$("#auto-night-mode-time-range-from").trigger("keyup");
$("#auto-night-mode-time-range-to").val(Grocy.UserSettings.auto_night_mode_time_range_to);
$("#auto-night-mode-time-range-to").trigger("keyup");
function CheckNightMode()
{
if (!BoolVal(Grocy.UserSettings.auto_night_mode_enabled))
{
return;
}
var start = moment(Grocy.UserSettings.auto_night_mode_time_range_from, "HH:mm", true);
var end = moment(Grocy.UserSettings.auto_night_mode_time_range_to, "HH:mm", true);
var now = moment();
if (!start.isValid() || !end.isValid)
{
return;
}
if (BoolVal(Grocy.UserSettings.auto_night_mode_time_range_goes_over_midnight))
{
end.add(1, "day");
}
if (start.isSameOrBefore(now) && end.isSameOrAfter(now)) // We're INSIDE of night mode time range
{
if (!$("body").hasClass("night-mode"))
{
$("body").addClass("night-mode");
$("#currently-inside-night-mode-range").prop("checked", true);
$("#currently-inside-night-mode-range").trigger("change");
}
}
else // We're OUTSIDE of night mode time range
{
if ($("body").hasClass("night-mode"))
{
$("body").removeClass("night-mode");
$("#currently-inside-night-mode-range").prop("checked", false);
$("#currently-inside-night-mode-range").trigger("change");
}
}
}
CheckNightMode();
if (Grocy.Mode === "production")
{
setInterval(CheckNightMode, 60000);
}
else
{
setInterval(CheckNightMode, 4000);
}

View File

@ -24,6 +24,18 @@ Grocy.Components.ProductCard.Refresh = function(productId)
$('#productcard-product-last-price').text(L('Unknown'));
}
if (productDetails.product.picture_file_name !== null && !productDetails.product.picture_file_name.isEmpty())
{
$("#productcard-no-product-picture").addClass("d-none");
$("#productcard-product-picture").removeClass("d-none");
$("#productcard-product-picture").attr("src", U('/api/file/productpictures?file_name=' + productDetails.product.picture_file_name));
}
else
{
$("#productcard-no-product-picture").removeClass("d-none");
$("#productcard-product-picture").addClass("d-none");
}
EmptyElementWhenMatches('#productcard-product-last-purchased-timeago', L('timeago_nan'));
EmptyElementWhenMatches('#productcard-product-last-used-timeago', L('timeago_nan'));
},

128
public/viewjs/equipment.js Normal file
View File

@ -0,0 +1,128 @@
var equipmentTable = $('#equipment-table').DataTable({
'paginate': false,
'order': [[0, 'asc']],
'language': JSON.parse(L('datatables_localization')),
'scrollY': false,
'colReorder': true,
'stateSave': true,
'stateSaveParams': function(settings, data)
{
data.search.search = "";
data.columns.forEach(column =>
{
column.search.search = "";
});
},
'select': 'single',
'initComplete': function()
{
this.api().row({ order: 'current' }, 0).select();
DisplayEquipment($('#equipment-table tbody tr:eq(0)').data("equipment-id"));
}
});
equipmentTable.on('select', function(e, dt, type, indexes)
{
if (type === 'row')
{
var selectedEquipmentId = $(equipmentTable.row(indexes[0]).node()).data("equipment-id");
DisplayEquipment(selectedEquipmentId)
}
});
function DisplayEquipment(id)
{
Grocy.Api.Get('get-object/equipment/' + id,
function(equipmentItem)
{
$(".selected-equipment-name").text(equipmentItem.name);
$("#description-tab-content").html(equipmentItem.description);
if (equipmentItem.instruction_manual_file_name !== null && !equipmentItem.instruction_manual_file_name.isEmpty())
{
var pdfUrl = U('/api/file/equipmentmanuals?file_name=' + equipmentItem.instruction_manual_file_name);
$("#selected-equipment-instruction-manual").attr("src", pdfUrl);
$("#selected-equipment-instruction-manual").removeClass("d-none");
$("#selected-equipment-has-no-instruction-manual-hint").addClass("d-none");
$("a[href='#instruction-manual-tab']").tab("show");
ResizeResponsiveEmbeds();
}
else
{
$("#selected-equipment-instruction-manual").addClass("d-none");
$("#selected-equipment-has-no-instruction-manual-hint").removeClass("d-none");
$("a[href='#description-tab']").tab("show");
}
},
function(xhr)
{
console.error(xhr);
}
);
}
$("#search").on("keyup", function()
{
var value = $(this).val();
if (value === "all")
{
value = "";
}
equipmentTable.search(value).draw();
});
$(document).on('click', '.equipment-delete-button', function (e)
{
var objectName = $(e.currentTarget).attr('data-equipment-name');
var objectId = $(e.currentTarget).attr('data-equipment-id');
bootbox.confirm({
message: L('Are you sure to delete equipment "#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/equipment/' + objectId,
function(result)
{
window.location.href = U('/equipment');
},
function(xhr)
{
console.error(xhr);
}
);
}
}
});
});
$("#selectedEquipmentInstructionManualToggleFullscreenButton").on('click', function(e)
{
$("#selectedEquipmentInstructionManualCard").toggleClass("fullscreen");
$("#selectedEquipmentInstructionManualCard .card-header").toggleClass("fixed-top");
$("#selectedEquipmentInstructionManualCard .card-body").toggleClass("mt-5");
$("body").toggleClass("fullscreen-card");
ResizeResponsiveEmbeds(true);
});
$("#selectedEquipmentDescriptionToggleFullscreenButton").on('click', function(e)
{
$("#selectedEquipmentDescriptionCard").toggleClass("fullscreen");
$("#selectedEquipmentDescriptionCard .card-header").toggleClass("fixed-top");
$("#selectedEquipmentDescriptionCard .card-body").toggleClass("mt-5");
});

View File

@ -0,0 +1,130 @@
$('#save-equipment-button').on('click', function(e)
{
e.preventDefault();
var jsonData = $('#equipment-form').serializeJSON();
if ($("#instruction-manual")[0].files.length > 0)
{
var someRandomStuff = Math.random().toString(36).substring(2, 100) + Math.random().toString(36).substring(2, 100);
jsonData.instruction_manual_file_name = someRandomStuff + $("#instruction-manual")[0].files[0].name;
}
if (Grocy.DeleteInstructionManualOnSave)
{
jsonData.instruction_manual_file_name = null;
}
if (Grocy.EditMode === 'create')
{
Grocy.Api.Post('add-object/equipment', jsonData,
function(result)
{
if (jsonData.hasOwnProperty("instruction_manual_file_name") && !Grocy.DeleteInstructionManualOnSave)
{
Grocy.Api.UploadFile($("#instruction-manual")[0].files[0], 'equipmentmanuals', jsonData.instruction_manual_file_name,
function(result)
{
window.location.href = U('/equipment');
},
function(xhr)
{
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response)
}
);
}
else
{
window.location.href = U('/equipment');
}
},
function(xhr)
{
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response)
}
);
}
else
{
if (Grocy.DeleteInstructionManualOnSave)
{
Grocy.Api.DeleteFile(Grocy.InstructionManualFileNameName, 'equipmentmanuals',
function(result)
{
// Nothing to do
},
function(xhr)
{
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response)
}
);
};
Grocy.Api.Post('edit-object/equipment/' + Grocy.EditObjectId, jsonData,
function(result)
{
if (jsonData.hasOwnProperty("instruction_manual_file_name") && !Grocy.DeleteInstructionManualOnSave)
{
Grocy.Api.UploadFile($("#instruction-manual")[0].files[0], 'equipmentmanuals', jsonData.instruction_manual_file_name,
function(result)
{
window.location.href = U('/equipment');;
},
function(xhr)
{
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response)
}
);
}
else
{
window.location.href = U('/equipment');;
}
},
function(xhr)
{
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response)
}
);
}
});
$('#equipment-form input').keyup(function(event)
{
Grocy.FrontendHelpers.ValidateForm('equipment-form');
});
$('#equipment-form input').keydown(function(event)
{
if (event.keyCode === 13) //Enter
{
event.preventDefault();
if (document.getElementById('equipment-form').checkValidity() === false) //There is at least one validation error
{
return false;
}
else
{
$('#save-equipment-button').click();
}
}
});
Grocy.DeleteInstructionManualOnSave = false;
$('#delete-current-instruction-manual-button').on('click', function (e)
{
Grocy.DeleteInstructionManualOnSave = true;
$("#current-equipment-instruction-manual").addClass("d-none");
$("#delete-current-instruction-manual-on-save-hint").removeClass("d-none");
$("#delete-current-instruction-manual-button").addClass("disabled");
});
$('#description').summernote({
minHeight: '300px',
lang: L('summernote_locale')
});
ResizeResponsiveEmbeds();
$('#name').focus();
Grocy.FrontendHelpers.ValidateForm('equipment-form');

View File

@ -1,4 +1,4 @@
$('#save-product-button').on('click', function(e)
$('#save-product-button').on('click', function (e)
{
e.preventDefault();
@ -9,14 +9,42 @@
redirectDestination = returnTo + '?createdproduct=' + encodeURIComponent($('#name').val());
}
var jsonData = $('#product-form').serializeJSON();
if ($("#product-picture")[0].files.length > 0)
{
var someRandomStuff = Math.random().toString(36).substring(2, 100) + Math.random().toString(36).substring(2, 100);
jsonData.picture_file_name = someRandomStuff + $("#product-picture")[0].files[0].name;
}
if (Grocy.DeleteProductPictureOnSave)
{
jsonData.picture_file_name = null;
}
if (Grocy.EditMode === 'create')
{
Grocy.Api.Post('add-object/products', $('#product-form').serializeJSON(),
function(result)
Grocy.Api.Post('add-object/products', jsonData,
function (result)
{
window.location.href = redirectDestination;
if (jsonData.hasOwnProperty("picture_file_name") && !Grocy.DeleteProductPictureOnSave)
{
Grocy.Api.UploadFile($("#product-picture")[0].files[0], 'productpictures', jsonData.picture_file_name,
function (result)
{
window.location.href = redirectDestination;
},
function (xhr)
{
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response)
}
);
}
else
{
window.location.href = redirectDestination;
}
},
function(xhr)
function (xhr)
{
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response)
}
@ -24,10 +52,40 @@
}
else
{
Grocy.Api.Post('edit-object/products/' + Grocy.EditObjectId, $('#product-form').serializeJSON(),
if (Grocy.DeleteProductPictureOnSave)
{
Grocy.Api.DeleteFile(Grocy.ProductPictureFileName, 'productpictures',
function(result)
{
// Nothing to do
},
function(xhr)
{
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response)
}
);
};
Grocy.Api.Post('edit-object/products/' + Grocy.EditObjectId, jsonData,
function(result)
{
window.location.href = redirectDestination;
if (jsonData.hasOwnProperty("picture_file_name") && !Grocy.DeleteProductPictureOnSave)
{
Grocy.Api.UploadFile($("#product-picture")[0].files[0], 'productpictures', jsonData.picture_file_name,
function(result)
{
window.location.href = redirectDestination;
},
function(xhr)
{
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response)
}
);
}
else
{
window.location.href = redirectDestination;
}
},
function(xhr)
{
@ -39,7 +97,8 @@
$('#barcode-taginput').tagsManager({
'hiddenTagListName': 'barcode',
'tagsContainer': '#barcode-taginput-container'
'tagsContainer': '#barcode-taginput-container',
'tagClass': 'badge badge-secondary'
});
if (Grocy.EditMode === 'edit')
@ -78,7 +137,21 @@ if (prefillBarcode !== undefined)
$('.input-group-qu').on('change', function(e)
{
var quIdPurchase = $("#qu_id_purchase").val();
var quIdStock = $("#qu_id_stock").val();
var factor = $('#qu_factor_purchase_to_stock').val();
if (quIdPurchase != quIdStock)
{
$('#qu_factor_purchase_to_stock').attr("min", 2);
$("#qu_factor_purchase_to_stock").parent().find(".invalid-feedback").text(L('The amount cannot be lower than #1', '2'));
}
else
{
$('#qu_factor_purchase_to_stock').attr("min", 1);
$("#qu_factor_purchase_to_stock").parent().find(".invalid-feedback").text(L('The amount cannot be lower than #1', '1'));
}
if (factor > 1)
{
$('#qu-conversion-info').text(L('This means 1 #1 purchased will be converted into #2 #3 in stock', $("#qu_id_purchase option:selected").text(), (1 * factor).toString(), $("#qu_id_stock option:selected").text()));
@ -88,6 +161,8 @@ $('.input-group-qu').on('change', function(e)
{
$('#qu-conversion-info').addClass('d-none');
}
Grocy.FrontendHelpers.ValidateForm('product-form');
});
$('#product-form input').keyup(function(event)
@ -112,6 +187,15 @@ $('#product-form input').keydown(function(event)
}
});
Grocy.DeleteProductPictureOnSave = false;
$('#delete-current-product-picture-button').on('click', function (e)
{
Grocy.DeleteProductPictureOnSave = true;
$("#current-product-picture").addClass("d-none");
$("#delete-current-product-picture-on-save-hint").removeClass("d-none");
$("#delete-current-product-picture-button").addClass("disabled");
});
$('#name').focus();
$('.input-group-qu').trigger('change');
Grocy.FrontendHelpers.ValidateForm('product-form');

View File

@ -35,33 +35,54 @@ $(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: L('Are you sure to delete product "#1"?', objectName),
buttons: {
confirm: {
label: L('Yes'),
className: 'btn-success'
},
cancel: {
label: L('No'),
className: 'btn-danger'
Grocy.Api.Get('stock/get-product-details/' + objectId,
function(productDetails)
{
var stockAmount = productDetails.stock_amount || '0';
if (stockAmount.toString() == "0")
{
bootbox.confirm({
message: L('Are you sure to delete product "#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/products/' + objectId,
function (result)
{
window.location.href = U('/products');
},
function (xhr)
{
console.error(xhr);
}
);
}
}
});
}
else
{
bootbox.alert({
title: L('Delete not possible'),
message: L('This product cannot be deleted because it is in stock, please remove the stock amount first.') + '<br><br>' + L('Stock amount') + ': ' + stockAmount + ' ' + Pluralize(stockAmount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural)
});
}
},
callback: function(result)
function(xhr)
{
if (result === true)
{
Grocy.Api.Get('delete-object/products/' + objectId,
function(result)
{
window.location.href = U('/products');
},
function(xhr)
{
console.error(xhr);
}
);
}
console.error(xhr);
}
});
);
});

View File

@ -169,3 +169,8 @@ $("#recipe-pos-add-button").on("click", function(e)
}
);
});
$('#description').summernote({
minHeight: '300px',
lang: L('summernote_locale')
});

View File

@ -4,7 +4,6 @@
var jsonData = $('#recipe-pos-form').serializeJSON({ checkboxUncheckedValue: "0" });
jsonData.recipe_id = Grocy.EditObjectParentId;
console.log(jsonData);
if (Grocy.EditMode === 'create')
{
Grocy.Api.Post('add-object/recipes_pos', jsonData,

View File

@ -158,4 +158,7 @@ recipesTables.on('select', function(e, dt, type, indexes)
$("#selectedRecipeToggleFullscreenButton").on('click', function(e)
{
$("#selectedRecipeCard").toggleClass("fullscreen");
$("body").toggleClass("fullscreen-card");
$("#selectedRecipeCard .card-header").toggleClass("fixed-top");
$("#selectedRecipeCard .card-body").toggleClass("mt-5");
});

View File

@ -31,6 +31,17 @@ $("#location-filter").on("change", function()
stockOverviewTable.column(4).search(value).draw();
});
$("#product-group-filter").on("change", function()
{
var value = $(this).val();
if (value === "all")
{
value = "";
}
stockOverviewTable.column(6).search(value).draw();
});
$("#status-filter").on("change", function()
{
var value = $(this).val();
@ -141,6 +152,45 @@ $(document).on('click', '.product-consume-button', function(e)
);
});
$(document).on("click", ".product-name-cell", function(e)
{
var productHasPicture = BoolVal($(e.currentTarget).attr("data-product-has-picture"));
if (productHasPicture)
{
var pictureUrl = $(e.currentTarget).attr("data-picture-url");
var productName = $(e.currentTarget).attr("data-product-name");
var productId = $(e.currentTarget).attr("data-product-id");
bootbox.dialog({
title: L("Image of product #1", productName),
message: "<img src='" + pictureUrl + "' class='img-fluid img-thumbnail'>",
backdrop: false,
onEscape: true,
closeButton: false,
className: 'centered-dialog',
buttons: {
editproduct: {
label: '<i class="fas fa-edit"></i> ' + L('Edit product'),
className: 'btn-info responsive-button',
callback: function ()
{
window.location.href = U('/product/' + productId + '?returnto=' + encodeURIComponent(window.location.pathname) + '#product-picture');
}
},
close: {
label: L('Close'),
className: 'btn-default responsive-button',
callback: function()
{
bootbox.hideAll();
}
}
}
});
}
});
function RefreshStatistics()
{
Grocy.Api.Get('stock/get-current-stock',

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 166 KiB

View File

@ -61,6 +61,10 @@ $app->group('', function()
$this->get('/taskcategories', '\Grocy\Controllers\TasksController:TaskCategoriesList');
$this->get('/taskcategory/{categoryId}', '\Grocy\Controllers\TasksController:TaskCategoryEditForm');
// Equipment routes
$this->get('/equipment', '\Grocy\Controllers\EquipmentController:Overview');
$this->get('/equipment/{equipmentId}', '\Grocy\Controllers\EquipmentController:EditForm');
// OpenAPI routes
$this->get('/api', '\Grocy\Controllers\OpenApiController:DocumentationUi');
$this->get('/manageapikeys', '\Grocy\Controllers\OpenApiController:ApiKeysList');
@ -81,9 +85,12 @@ $app->group('/api', function()
// System
$this->get('/system/get-db-changed-time', '\Grocy\Controllers\SystemApiController:GetDbChangedTime');
$this->post('/system/log-missing-localization', '\Grocy\Controllers\SystemApiController:LogMissingLocalization');
// Files
$this->post('/files/upload/{group}', '\Grocy\Controllers\FilesApiController:Upload');
$this->put('/file/{group}', '\Grocy\Controllers\FilesApiController:UploadFile');
$this->get('/file/{group}', '\Grocy\Controllers\FilesApiController:ServeFile');
$this->delete('/file/{group}', '\Grocy\Controllers\FilesApiController:DeleteFile');
// Users
$this->get('/users/get', '\Grocy\Controllers\UsersApiController:GetUsers');
@ -91,6 +98,10 @@ $app->group('/api', function()
$this->post('/users/edit/{userId}', '\Grocy\Controllers\UsersApiController:EditUser');
$this->get('/users/delete/{userId}', '\Grocy\Controllers\UsersApiController:DeleteUser');
// User
$this->get('/user/settings/{settingKey}', '\Grocy\Controllers\UsersApiController:GetUserSetting');
$this->post('/user/settings/{settingKey}', '\Grocy\Controllers\UsersApiController:SetUserSetting');
// Stock
$this->get('/stock/add-product/{productId}/{amount}', '\Grocy\Controllers\StockApiController:AddProduct');
$this->get('/stock/consume-product/{productId}/{amount}', '\Grocy\Controllers\StockApiController:ConsumeProduct');

View File

@ -14,6 +14,7 @@ class DemoDataGeneratorService extends BaseService
if (intval($rowCount) === 0)
{
$loremIpsum = 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.';
$loremIpsumWithHtmlFormattings = "<h1>Lorem ipsum</h1><p>Lorem ipsum <b>dolor sit</b> amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur <span style=\"background-color: rgb(255, 255, 0);\">sadipscing elitr</span>, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.</p><ul><li>At vero eos et accusam et justo duo dolores et ea rebum.</li><li>Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.</li></ul><h1>Lorem ipsum</h1><p>Lorem ipsum <b>dolor sit</b> amet, consetetur \r\nsadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et \r\ndolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et\r\n justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea \r\ntakimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit \r\namet, consetetur <span style=\"background-color: rgb(255, 255, 0);\">sadipscing elitr</span>,\r\n sed diam nonumy eirmod tempor invidunt ut labore et dolore magna \r\naliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo \r\ndolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus \r\nest Lorem ipsum dolor sit amet.</p>";
$sql = "
UPDATE users SET username = '{$localizationService->Localize('Demo User')}' WHERE id = 1;
@ -39,9 +40,9 @@ class DemoDataGeneratorService extends BaseService
INSERT INTO product_groups(name) VALUES ('06 {$localizationService->Localize('Refrigerated products')}'); --6
DELETE FROM sqlite_sequence WHERE name = 'products'; --Just to keep IDs in order as mentioned here...
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, min_stock_amount, product_group_id) VALUES ('{$localizationService->Localize('Cookies')}', 3, 3, 3, 1, 8, 1); --1
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, min_stock_amount, product_group_id, picture_file_name) VALUES ('{$localizationService->Localize('Cookies')}', 3, 3, 3, 1, 8, 1, 'cookies.jpg'); --1
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, min_stock_amount, product_group_id) VALUES ('{$localizationService->Localize('Chocolate')}', 3, 3, 3, 1, 8, 1); --2
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, min_stock_amount, product_group_id) VALUES ('{$localizationService->Localize('Gummy bears')}', 3, 3, 3, 1, 8, 1); --3
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, min_stock_amount, product_group_id, picture_file_name) VALUES ('{$localizationService->Localize('Gummy bears')}', 3, 3, 3, 1, 8, 1, 'gummybears.jpg'); --3
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, min_stock_amount, product_group_id) VALUES ('{$localizationService->Localize('Crisps')}', 3, 3, 3, 1, 10, 1); --4
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$localizationService->Localize('Eggs')}', 2, 3, 2, 10, 5); --5
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$localizationService->Localize('Noodles')}', 3, 3, 3, 1, 6); --6
@ -50,10 +51,10 @@ class DemoDataGeneratorService extends BaseService
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$localizationService->Localize('Yogurt')}', 2, 6, 6, 1, 6); --9
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$localizationService->Localize('Cheese')}', 2, 3, 3, 1, 6); --10
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$localizationService->Localize('Cold cuts')}', 2, 3, 3, 1, 6); --11
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$localizationService->Localize('Paprika')}', 2, 2, 2, 1, 5); --12
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$localizationService->Localize('Cucumber')}', 2, 2, 2, 1, 5); --13
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id, picture_file_name) VALUES ('{$localizationService->Localize('Paprika')}', 2, 2, 2, 1, 5, 'paprika.jpg'); --12
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id, picture_file_name) VALUES ('{$localizationService->Localize('Cucumber')}', 2, 2, 2, 1, 5, 'cucumber.jpg'); --13
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$localizationService->Localize('Radish')}', 2, 7, 7, 1, 5); --14
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$localizationService->Localize('Tomato')}', 2, 2, 2, 1, 5); --15
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id, picture_file_name) VALUES ('{$localizationService->Localize('Tomato')}', 2, 2, 2, 1, 5, 'tomato.jpg'); --15
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$localizationService->Localize('Pizza dough')}', 3, 3, 3, 1, 6); --16
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$localizationService->Localize('Sieved tomatoes')}', 4, 5, 5, 1, 3); --17
INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, product_group_id) VALUES ('{$localizationService->Localize('Salami')}', 2, 3, 3, 1, 6); --18
@ -66,10 +67,10 @@ class DemoDataGeneratorService extends BaseService
INSERT INTO shopping_list (product_id, amount) VALUES (20, 1);
INSERT INTO shopping_list (product_id, amount) VALUES (17, 1);
INSERT INTO recipes (name, description) VALUES ('{$localizationService->Localize('Pizza')}', '{$loremIpsum}'); --1
INSERT INTO recipes (name, description) VALUES ('{$localizationService->Localize('Spaghetti bolognese')}', '{$loremIpsum}'); --2
INSERT INTO recipes (name, description) VALUES ('{$localizationService->Localize('Sandwiches')}', '{$loremIpsum}'); --3
INSERT INTO recipes (name, description) VALUES ('{$localizationService->Localize('Pancakes')}', '{$loremIpsum}'); --4
INSERT INTO recipes (name, description) VALUES ('{$localizationService->Localize('Pizza')}', '{$loremIpsumWithHtmlFormattings}'); --1
INSERT INTO recipes (name, description) VALUES ('{$localizationService->Localize('Spaghetti bolognese')}', '{$loremIpsumWithHtmlFormattings}'); --2
INSERT INTO recipes (name, description) VALUES ('{$localizationService->Localize('Sandwiches')}', '{$loremIpsumWithHtmlFormattings}'); --3
INSERT INTO recipes (name, description) VALUES ('{$localizationService->Localize('Pancakes')}', '{$loremIpsumWithHtmlFormattings}'); --4
INSERT INTO recipes_pos (recipe_id, product_id, amount) VALUES (1, 16, 1);
INSERT INTO recipes_pos (recipe_id, product_id, amount) VALUES (1, 17, 1);
@ -105,6 +106,9 @@ class DemoDataGeneratorService extends BaseService
INSERT INTO tasks (name, due_date, assigned_to_user_id) VALUES ('{$localizationService->Localize('Find a solution for what to do when I forget the door keys')}', date(datetime('now', 'localtime'), '+3 day'), 1);
INSERT INTO tasks (name, due_date, assigned_to_user_id) VALUES ('{$localizationService->Localize('Task')}3', date(datetime('now', 'localtime'), '+4 day'), 1);
INSERT INTO equipment (name, description, instruction_manual_file_name) VALUES ('{$localizationService->Localize('Coffee machine')}', '{$loremIpsumWithHtmlFormattings}', 'loremipsum.pdf'); --1
INSERT INTO equipment (name, description) VALUES ('{$localizationService->Localize('Dishwasher')}', '{$loremIpsumWithHtmlFormattings}'); --2
INSERT INTO migrations (migration) VALUES (-1);
";
@ -201,6 +205,25 @@ class DemoDataGeneratorService extends BaseService
$batteriesService->TrackChargeCycle(2, date('Y-m-d H:i:s', strtotime('-50 days')));
$batteriesService->TrackChargeCycle(3, date('Y-m-d H:i:s', strtotime('-65 days')));
$batteriesService->TrackChargeCycle(4, date('Y-m-d H:i:s', strtotime('-56 days')));
// Download demo storage data
$productPicturesFolder = GROCY_DATAPATH . '/storage/productpictures';
$equipmentManualsFolder = GROCY_DATAPATH . '/storage/equipmentmanuals';
mkdir(GROCY_DATAPATH . '/storage');
mkdir(GROCY_DATAPATH . '/storage/productpictures');
mkdir(GROCY_DATAPATH . '/storage/equipmentmanuals');
$sslOptions = array(
'ssl' => array(
'verify_peer' => false,
'verify_peer_name' => false,
),
);
file_put_contents("$productPicturesFolder/cookies.jpg", file_get_contents('https://releases.grocy.info/demoresources/cookies.jpg', false, stream_context_create($sslOptions)));
file_put_contents("$productPicturesFolder/cucumber.jpg", file_get_contents('https://releases.grocy.info/demoresources/cucumber.jpg', false, stream_context_create($sslOptions)));
file_put_contents("$productPicturesFolder/gummybears.jpg", file_get_contents('https://releases.grocy.info/demoresources/gummybears.jpg', false, stream_context_create($sslOptions)));
file_put_contents("$productPicturesFolder/paprika.jpg", file_get_contents('https://releases.grocy.info/demoresources/paprika.jpg', false, stream_context_create($sslOptions)));
file_put_contents("$productPicturesFolder/tomato.jpg", file_get_contents('https://releases.grocy.info/demoresources/tomato.jpg', false, stream_context_create($sslOptions)));
file_put_contents("$equipmentManualsFolder/loremipsum.pdf", file_get_contents('https://releases.grocy.info/demoresources/loremipsum.pdf', false, stream_context_create($sslOptions)));
}
}

View File

@ -34,7 +34,7 @@ class LocalizationService
}
}
private function LogMissingLocalization(string $culture, string $text)
public function LogMissingLocalization(string $culture, string $text)
{
$file = GROCY_DATAPATH . "/missing_translations_$culture.json";

View File

@ -37,10 +37,10 @@ class SessionService extends BaseService
{
$newSessionKey = $this->GenerateSessionKey();
$expires = date('Y-m-d H:i:s', time() + 2592000); // Default is that sessions expire in 30 days
$expires = date('Y-m-d H:i:s', intval(time() + 2592000)); // Default is that sessions expire in 30 days
if ($stayLoggedInPermanently === true)
{
$expires = date('Y-m-d H:i:s', time() + 31220640000); // 999 years aka forever
$expires = date('Y-m-d H:i:s', intval(time() + 31220640000)); // 999 years aka forever
}
$sessionRow = $this->Database->sessions()->createRow(array(

View File

@ -8,9 +8,14 @@ class StockService extends BaseService
const TRANSACTION_TYPE_CONSUME = 'consume';
const TRANSACTION_TYPE_INVENTORY_CORRECTION = 'inventory-correction';
public function GetCurrentStock()
public function GetCurrentStock($includeNotInStockButMissingProducts = false)
{
$sql = 'SELECT * from stock_current';
if ($includeNotInStockButMissingProducts)
{
$sql = 'SELECT * from stock_current WHERE best_before_date IS NOT NULL';
}
return $this->DatabaseService->ExecuteDbQuery($sql)->fetchAll(\PDO::FETCH_OBJ);
}
@ -20,10 +25,17 @@ class StockService extends BaseService
return $this->DatabaseService->ExecuteDbQuery($sql)->fetchAll(\PDO::FETCH_OBJ);
}
public function GetExpiringProducts(int $days = 5)
public function GetExpiringProducts(int $days = 5, bool $excludeExpired = false)
{
$currentStock = $this->GetCurrentStock();
return FindAllObjectsInArrayByPropertyValue($currentStock, 'best_before_date', date('Y-m-d', strtotime("+$days days")), '<');
$currentStock = $this->GetCurrentStock(true);
$currentStock = FindAllObjectsInArrayByPropertyValue($currentStock, 'best_before_date', date('Y-m-d', strtotime("+$days days")), '<');
if ($excludeExpired)
{
$currentStock = FindAllObjectsInArrayByPropertyValue($currentStock, 'best_before_date', date('Y-m-d', strtotime('now')), '>');
}
return $currentStock;
}
public function GetProductDetails(int $productId)

View File

@ -50,6 +50,55 @@ class UsersService extends BaseService
return $returnUsers;
}
public function GetUserSetting($userId, $settingKey)
{
$settingRow = $this->Database->user_settings()->where('user_id = :1 AND key = :2', $userId, $settingKey)->fetch();
if ($settingRow !== null)
{
return $settingRow->value;
}
else
{
return null;
}
}
public function GetUserSettings($userId)
{
$settings = array();
$settingRows = $this->Database->user_settings()->where('user_id = :1', $userId)->fetchAll();
foreach ($settingRows as $settingRow)
{
$settings[$settingRow->key] = $settingRow->value;
}
// Use the configured default values for all missing settings
global $GROCY_DEFAULT_USER_SETTINGS;
return array_merge($GROCY_DEFAULT_USER_SETTINGS, $settings);
}
public function SetUserSetting($userId, $settingKey, $settingValue)
{
$settingRow = $this->Database->user_settings()->where('user_id = :1 AND key = :2', $userId, $settingKey)->fetch();
if ($settingRow !== null)
{
$settingRow->update(array(
'value' => $settingValue,
'row_updated_timestamp' => date('Y-m-d H:i:s')
));
}
else
{
$settingRow = $this->Database->user_settings()->createRow(array(
'user_id' => $userId,
'key' => $settingKey,
'value' => $settingValue
));
$settingRow->save();
}
}
private function UserExists($userId)
{
$userRow = $this->Database->users()->where('id = :1', $userId)->fetch();

View File

@ -1,4 +1,4 @@
{
"Version": "1.19.2",
"ReleaseDate": "2018-09-29"
"Version": "1.21.0",
"ReleaseDate": "2018-10-06"
}

View File

@ -15,6 +15,10 @@
<strong>{{ $L('Last used') }}:</strong> <span id="productcard-product-last-used"></span> <time id="productcard-product-last-used-timeago" class="timeago timeago-contextual"></time><br>
<strong>{{ $L('Last price') }}:</strong> <span id="productcard-product-last-price"></span>
<h5 class="mt-3">{{ $L('Product picture') }}</h5>
<img id="productcard-product-picture" src="" class="img-fluid img-thumbnail d-none">
<span id="productcard-no-product-picture" class="font-italic d-none">{{ $L('No picture available') }}</span>
<h5 class="mt-3">{{ $L('Price history') }}</h5>
<canvas id="productcard-product-price-history-chart" class="w-100 d-none"></canvas>
<span id="productcard-no-price-data-hint" class="font-italic d-none">{{ $L('No price history available') }}</span>

91
views/equipment.blade.php Normal file
View File

@ -0,0 +1,91 @@
@extends('layout.default')
@section('title', $L('Equipment'))
@section('activeNav', 'equipment')
@section('viewJsName', 'equipment')
@section('content')
<div class="row">
<div class="col-xs-12 col-md-4 pb-3">
<h1>
@yield('title')
<a class="btn btn-outline-dark" href="{{ $U('/equipment/new') }}">
<i class="fas fa-plus"></i>&nbsp;{{ $L('Add') }}
</a>
</h1>
<label for="search">{{ $L('Search') }}</label> <i class="fas fa-search"></i>
<input type="text" class="form-control" id="search">
<table id="equipment-table" class="table table-striped dt-responsive">
<thead>
<tr>
<th>{{ $L('Name') }}</th>
</tr>
</thead>
<tbody>
@foreach($equipment as $equipmentItem)
<tr data-equipment-id="{{ $equipmentItem->id }}">
<td>
{{ $equipmentItem->name }}
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
<div class="col-xs-12 col-md-8">
<ul class="nav nav-tabs">
<li class="nav-item">
<a class="nav-link active" data-toggle="tab" href="#instruction-manual-tab">{{ $L('Instruction manual') }}</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#description-tab">{{ $L('Notes') }}</a>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane fade show active" id="instruction-manual-tab">
<div id="selectedEquipmentInstructionManualCard" class="card">
<div class="card-header">
<i class="fas fa-toolbox"></i> <span class="selected-equipment-name"></span>&nbsp;&nbsp;
<a class="btn btn-info btn-sm btn-outline-info py-0" href="{{ $U('/equipment/') }}{{ $equipmentItem->id }}">
<i class="fas fa-edit"></i>
</a>
<a class="btn btn-danger btn-sm btn-outline-danger py-0 equipment-delete-button" href="#" data-equipment-id="{{ $equipmentItem->id }}" data-equipment-name="{{ $equipmentItem->name }}">
<i class="fas fa-trash"></i>
</a>
<a id="selectedEquipmentInstructionManualToggleFullscreenButton" class="btn btn-sm btn-outline-secondary py-0 float-right" href="#" data-toggle="tooltip" title="{{ $L('Expand to fullscreen') }}">
<i class="fas fa-expand-arrows-alt"></i>
</a>
</div>
<div class="card-body py-0 px-0">
<p id="selected-equipment-has-no-instruction-manual-hint" class="text-muted font-italic d-none">{{ $L('The selected equipment has no instruction manual') }}</p>
<embed id="selected-equipment-instruction-manual" class="embed-responsive embed-responsive-4by3" src="" type="application/pdf">
</div>
</div>
</div>
<div class="tab-pane fade" id="description-tab">
<div id="selectedEquipmentDescriptionCard" class="card">
<div class="card-header">
<i class="fas fa-toolbox"></i> <span class="selected-equipment-name"></span>&nbsp;&nbsp;
<a class="btn btn-info btn-sm btn-outline-info py-0" href="{{ $U('/equipment/') }}{{ $equipmentItem->id }}">
<i class="fas fa-edit"></i>
</a>
<a class="btn btn-danger btn-sm btn-outline-danger py-0 equipment-delete-button" href="#" data-equipment-id="{{ $equipmentItem->id }}" data-equipment-name="{{ $equipmentItem->name }}">
<i class="fas fa-trash"></i>
</a>
<a id="selectedEquipmentDescriptionToggleFullscreenButton" class="btn btn-sm btn-outline-secondary py-0 float-right" href="#" data-toggle="tooltip" title="{{ $L('Expand to fullscreen') }}">
<i class="fas fa-expand-arrows-alt"></i>
</a>
</div>
<div class="card-body">
<div id="description-tab-content" class="mb-0"></div>
</div>
</div>
</div>
</div>
</div>
</div>
@stop

View File

@ -0,0 +1,74 @@
@extends('layout.default')
@if($mode == 'edit')
@section('title', $L('Edit equipment'))
@else
@section('title', $L('Create equipment'))
@endif
@section('viewJsName', 'equipmentform')
@push('pageScripts')
<script src="{{ $U('/node_modules/summernote/dist/summernote-bs4.js?v=', true) }}{{ $version }}"></script>
@if(!empty($L('summernote_locale')))<script src="{{ $U('/node_modules', true) }}/summernote/dist/lang/summernote-{{ $L('summernote_locale') }}.js?v={{ $version }}"></script>@endif
@endpush
@push('pageStyles')
<link href="{{ $U('/node_modules/summernote/dist/summernote-bs4.css?v=', true) }}{{ $version }}" rel="stylesheet">
@endpush
@section('content')
<div class="row">
<div class="col-lg-6 col-xs-12">
<h1>@yield('title')</h1>
<script>Grocy.EditMode = '{{ $mode }}';</script>
@if($mode == 'edit')
<script>Grocy.EditObjectId = {{ $equipment->id }};</script>
@if(!empty($equipment->instruction_manual_file_name))
<script>Grocy.InstructionManualFileNameName = '{{ $equipment->instruction_manual_file_name }}';</script>
@endif
@endif
<form id="equipment-form" novalidate>
<div class="form-group">
<label for="name">{{ $L('Name') }}</label>
<input type="text" class="form-control" required id="name" name="name" value="@if($mode == 'edit'){{ $equipment->name }}@endif">
<div class="invalid-feedback">{{ $L('A name is required') }}</div>
</div>
<div class="form-group">
<label for="instruction-manual">{{ $L('Instruction manual') }} (PDF)</label>
<div class="custom-file">
<input type="file" class="custom-file-input" id="instruction-manual" accept="application/pdf">
<label class="custom-file-label" for="instruction-manual">{{ $L('No file selected') }}</label>
</div>
<p class="form-text text-muted small">{{ $L('If you don\'t select a file, the current instruction manual will not be altered') }}</p>
</div>
<div class="form-group">
<label for="description">{{ $L('Notes') }}</label>
<textarea class="form-control" id="description" name="description">@if($mode == 'edit'){{ $equipment->description }}@endif</textarea>
</div>
<button id="save-equipment-button" class="btn btn-success">{{ $L('Save') }}</button>
</form>
</div>
<div class="col-lg-6 col-xs-12">
<label class="mt-2">{{ $L('Current instruction manual') }}</label>
<button id="delete-current-instruction-manual-button" class="btn btn-sm btn-danger @if(empty($equipment->instruction_manual_file_name)) disabled @endif"><i class="fas fa-trash"></i> {{ $L('Delete') }}</button>
@if(!empty($equipment->instruction_manual_file_name))
<embed id="current-equipment-instruction-manual" class="embed-responsive embed-responsive-4by3" src="{{ $U('/api/file/equipmentmanuals?file_name=' . $equipment->instruction_manual_file_name) }}" type="application/pdf">
<p id="delete-current-instruction-manual-on-save-hint" class="form-text text-muted font-italic d-none">{{ $L('The current instruction manual will be deleted when you save the equipment') }}</p>
@else
<p id="no-current-instruction-manual-hint" class="form-text text-muted font-italic">{{ $L('No instruction manual available') }}</p>
@endif
</div>
</div>
@stop

View File

@ -36,15 +36,17 @@
<script>
var Grocy = { };
Grocy.Components = { };
Grocy.Mode = '{{ GROCY_MODE }}';
Grocy.BaseUrl = '{{ $U('/') }}';
Grocy.LocalizationStrings = {!! json_encode($localizationStrings) !!};
Grocy.ActiveNav = '@yield('activeNav', '')';
Grocy.Culture = '{{ GROCY_CULTURE }}';
Grocy.Currency = '{{ GROCY_CURRENCY }}';
Grocy.UserSettings = {!! json_encode($userSettings) !!};
</script>
</head>
<body class="fixed-nav">
<body class="fixed-nav @if(boolval($userSettings['night_mode_enabled']) || (boolval($userSettings['auto_night_mode_enabled']) && boolval($userSettings['currently_inside_night_mode_range']))) night-mode @endif">
<nav id="mainNav" class="navbar navbar-expand-lg navbar-light fixed-top">
<a class="navbar-brand py-0" href="{{ $U('/') }}"><img src="{{ $U('/img/grocy_logo.svg?v=', true) }}{{ $version }}" height="30"></a>
@ -91,6 +93,12 @@
<span class="nav-link-text">{{ $L('Batteries overview') }}</span>
</a>
</li>
<li class="nav-item" data-toggle="tooltip" data-placement="right" title="{{ $L('Equipment') }}" data-nav-for-page="equipment">
<a class="nav-link discrete-link" href="{{ $U('/equipment') }}">
<i class="fas fa-toolbox"></i>
<span class="nav-link-text">{{ $L('Equipment') }}</span>
</a>
</li>
<li class="nav-item mt-4" data-toggle="tooltip" data-placement="right" title="{{ $L('Purchase') }}" data-nav-for-page="purchase">
<a class="nav-link discrete-link" href="{{ $U('/purchase') }}">
@ -191,16 +199,52 @@
<div class="dropdown-menu dropdown-menu-right">
<a class="dropdown-item logout-button discrete-link" href="{{ $U('/logout') }}"><i class="fas fa-sign-out-alt"></i>&nbsp;{{ $L('Logout') }}</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item logout-button discrete-link" href="{{ $U('/user/' . GROCY_USER_ID . '?changepw=true') }}"><i class="fas fa-key"></i>&nbsp;{{ $L('Change password') }}</a>
</div>
</li>
@endif
@if(GROCY_AUTHENTICATED === true)
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle discrete-link" href="#" data-toggle="dropdown"><i class="fas fa-sliders-h"></i> <span class="d-inline d-lg-none">{{ $L('View settings') }}</span></a>
<div class="dropdown-menu dropdown-menu-right">
<div class="dropdown-item">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="night-mode-enabled">
<input class="form-check-input user-setting-control" type="checkbox" id="auto-reload-enabled" data-setting-key="auto_reload_on_db_change">
<label class="form-check-label" for="auto-reload-enabled">
{{ $L('Auto reload on external changes') }}
</label>
</div>
</div>
<div class="dropdown-divider"></div>
<div class="dropdown-item">
<div class="form-check">
<input class="form-check-input user-setting-control" type="checkbox" id="night-mode-enabled" data-setting-key="night_mode_enabled">
<label class="form-check-label" for="night-mode-enabled">
{{ $L('Enable night mode') }}
</label>
</div>
</div>
<div class="dropdown-divider"></div>
<a class="dropdown-item logout-button discrete-link" href="{{ $U('/user/' . GROCY_USER_ID . '?changepw=true') }}"><i class="fas fa-key"></i>&nbsp;{{ $L('Change password') }}</a>
<div class="dropdown-item">
<div class="form-check">
<input class="form-check-input user-setting-control" type="checkbox" id="auto-night-mode-enabled" data-setting-key="auto_night_mode_enabled">
<label class="form-check-label" for="auto-night-mode-enabled">
{{ $L('Auto enable in time range') }}
</label>
</div>
<div class="form-inline">
<input type="text" class="form-control my-1 user-setting-control" readonly id="auto-night-mode-time-range-from" placeholder="{{ $L('From') }} ({{ $L('in format') }} HH:mm)" data-setting-key="auto_night_mode_time_range_from">
<input type="text" class="form-control user-setting-control" readonly id="auto-night-mode-time-range-to" placeholder="{{ $L('To') }} ({{ $L('in format') }} HH:mm)" data-setting-key="auto_night_mode_time_range_to">
</div>
<div class="form-check mt-1">
<input class="form-check-input user-setting-control" type="checkbox" id="auto-night-mode-time-range-goes-over-midgnight" data-setting-key="auto_night_mode_time_range_goes_over_midnight">
<label class="form-check-label" for="auto-night-mode-time-range-goes-over-midgnight">
{{ $L('Time range goes over midnight') }}
</label>
</div>
<input class="form-check-input d-none user-setting-control" type="checkbox" id="currently-inside-night-mode-range" data-setting-key="currently_inside_night_mode_range">
</div>
</div>
</li>
@endif

View File

@ -10,6 +10,7 @@
@section('content')
<div class="row">
<div class="col-lg-6 col-xs-12">
<h1>@yield('title')</h1>
@ -17,6 +18,10 @@
@if($mode == 'edit')
<script>Grocy.EditObjectId = {{ $product->id }};</script>
@if(!empty($product->picture_file_name))
<script>Grocy.ProductPictureFileName = '{{ $product->picture_file_name }}';</script>
@endif
@endif
<form id="product-form" novalidate>
@ -108,8 +113,28 @@
'additionalHtmlElements' => '<p id="qu-conversion-info" class="form-text text-muted small d-none"></p>'
))
<div class="form-group">
<label for="product-picture">{{ $L('Product picture') }}</label>
<div class="custom-file">
<input type="file" class="custom-file-input" id="product-picture" accept="image/*">
<label class="custom-file-label" for="product-picture">{{ $L('No file selected') }}</label>
</div>
<p class="form-text text-muted small">{{ $L('If you don\'t select a file, the current picture will not be altered') }}</p>
</div>
<button id="save-product-button" class="btn btn-success">{{ $L('Save') }}</button>
</form>
</div>
<div class="col-lg-6 col-xs-12">
<label class="mt-2">{{ $L('Current picture') }}</label>
<button id="delete-current-product-picture-button" class="btn btn-sm btn-danger @if(empty($product->picture_file_name)) disabled @endif"><i class="fas fa-trash"></i> {{ $L('Delete') }}</button>
@if(!empty($product->picture_file_name))
<p><img id="current-product-picture" src="{{ $U('/api/file/productpictures?file_name=' . $product->picture_file_name) }}" class="img-fluid img-thumbnail mt-2"></p>
<p id="delete-current-product-picture-on-save-hint" class="form-text text-muted font-italic d-none">{{ $L('The current picture will be deleted when you save the product') }}</p>
@else
<p id="no-current-product-picture-hint" class="form-text text-muted font-italic">{{ $L('No picture available') }}</p>
@endif
</div>
</div>
@stop

View File

@ -50,7 +50,7 @@
</a>
</td>
<td>
{{ $product->name }}
{{ $product->name }}@if(!empty($product->picture_file_name)) <i class="fas fa-image text-muted"></i>@endif
</td>
<td>
{{ FindObjectInArrayByPropertyValue($locations, 'id', $product->location_id)->name }}

View File

@ -8,6 +8,15 @@
@section('viewJsName', 'recipeform')
@push('pageScripts')
<script src="{{ $U('/node_modules/summernote/dist/summernote-bs4.js?v=', true) }}{{ $version }}"></script>
@if(!empty($L('summernote_locale')))<script src="{{ $U('/node_modules', true) }}/summernote/dist/lang/summernote-{{ $L('summernote_locale') }}.js?v={{ $version }}"></script>@endif
@endpush
@push('pageStyles')
<link href="{{ $U('/node_modules/summernote/dist/summernote-bs4.css?v=', true) }}{{ $version }}" rel="stylesheet">
@endpush
@section('content')
<div class="row">
<div class="col">
@ -33,7 +42,7 @@
<div class="form-group">
<label for="description">{{ $L('Preparation') }}</label>
<textarea id="description" class="form-control" name="description" rows="25">@if($mode == 'edit'){{ $recipe->description }}@endif</textarea>
<textarea id="description" class="form-control" name="description">@if($mode == 'edit'){{ $recipe->description }}@endif</textarea>
</div>
<button id="save-recipe-button" class="btn btn-success">{{ $L('Save') }}</button>

View File

@ -6,18 +6,15 @@
@section('content')
<div class="row">
<div class="col">
<div class="col-xs-12 col-md-6 pb-3">
<h1>
@yield('title')
<a class="btn btn-outline-dark" href="{{ $U('/recipe/new') }}">
<i class="fas fa-plus"></i> {{ $L('Add') }}
</a>
</h1>
</div>
</div>
<div class="row">
<div class="col-xs-12 col-md-6 pb-3">
<label for="search">{{ $L('Search') }}</label> <i class="fas fa-search"></i>
<input type="text" class="form-control" id="search">
@ -82,7 +79,7 @@
</ul>
<div class="card-body">
<h5>{{ $L('Preparation') }}</h5>
{!! nl2br(htmlentities($selectedRecipe->description)) !!}
{!! $selectedRecipe->description !!}
</div>
</div>
</div>

View File

@ -8,6 +8,14 @@
<script src="{{ $U('/node_modules/jquery-ui-dist/jquery-ui.min.js?v=', true) }}{{ $version }}"></script>
@endpush
@push('pageStyles')
<style>
.product-name-cell[data-product-has-picture='true'] {
cursor: pointer;
}
</style>
@endpush
@section('content')
<div class="row">
<div class="col">
@ -28,6 +36,15 @@
@endforeach
</select>
</div>
<div class="col-xs-12 col-md-6 col-xl-3">
<label for="location-filter">{{ $L('Filter by product group') }}</label> <i class="fas fa-filter"></i>
<select class="form-control" id="product-group-filter">
<option value="all">{{ $L('All') }}</option>
@foreach($productGroups as $productGroup)
<option value="{{ $productGroup->name }}">{{ $productGroup->name }}</option>
@endforeach
</select>
</div>
<div class="col-xs-12 col-md-6 col-xl-3">
<label for="status-filter">{{ $L('Filter by status') }}</label> <i class="fas fa-filter"></i>
<select class="form-control" id="status-filter">
@ -54,20 +71,21 @@
<th>{{ $L('Next best before date') }}</th>
<th class="d-none">Hidden location</th>
<th class="d-none">Hidden status</th>
<th class="d-none">Hidden product group</th>
</tr>
</thead>
<tbody>
@foreach($currentStock as $currentStockEntry)
<tr id="product-{{ $currentStockEntry->product_id }}-row" class="@if($currentStockEntry->best_before_date < date('Y-m-d', strtotime('-1 days'))) table-danger @elseif($currentStockEntry->best_before_date < date('Y-m-d', strtotime("+$nextXDays days"))) table-warning @elseif (FindObjectInArrayByPropertyValue($missingProducts, 'id', $currentStockEntry->product_id) !== null) table-info @endif">
<tr id="product-{{ $currentStockEntry->product_id }}-row" class="@if($currentStockEntry->best_before_date < date('Y-m-d', strtotime('-1 days')) && $currentStockEntry->amount > 0) table-danger @elseif($currentStockEntry->best_before_date < date('Y-m-d', strtotime("+$nextXDays days")) && $currentStockEntry->amount > 0) table-warning @elseif (FindObjectInArrayByPropertyValue($missingProducts, 'id', $currentStockEntry->product_id) !== null) table-info @endif">
<td class="fit-content">
<a class="btn btn-success btn-sm product-consume-button" href="#" data-toggle="tooltip" data-placement="left" title="{{ $L('Consume #3 #1 of #2', FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_stock)->name, FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name, 1) }}"
<a class="btn btn-success btn-sm product-consume-button @if($currentStockEntry->amount == 0) disabled @endif" href="#" data-toggle="tooltip" data-placement="left" title="{{ $L('Consume #3 #1 of #2', FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_stock)->name, FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name, 1) }}"
data-product-id="{{ $currentStockEntry->product_id }}"
data-product-name="{{ FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name }}"
data-product-qu-name="{{ FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_stock)->name }}"
data-consume-amount="1">
<i class="fas fa-utensils"></i> 1
</a>
<a id="product-{{ $currentStockEntry->product_id }}-consume-all-button" class="btn btn-danger btn-sm product-consume-button" href="#" data-toggle="tooltip" data-placement="right" title="{{ $L('Consume all #1 which are currently in stock', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name) }}"
<a id="product-{{ $currentStockEntry->product_id }}-consume-all-button" class="btn btn-danger btn-sm product-consume-button @if($currentStockEntry->amount == 0) disabled @endif" href="#" data-toggle="tooltip" data-placement="right" title="{{ $L('Consume all #1 which are currently in stock', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name) }}"
data-product-id="{{ $currentStockEntry->product_id }}"
data-product-name="{{ FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name }}"
data-product-qu-name="{{ FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_stock)->name }}"
@ -75,8 +93,12 @@
<i class="fas fa-utensils"></i> {{ $L('All') }}
</a>
</td>
<td>
{{ FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name }}
<td class="product-name-cell"
data-picture-url="{{ $U('/api/file/productpictures?file_name=' . FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->picture_file_name) }}"
data-product-id="{{ $currentStockEntry->product_id }}"
data-product-name="{{ FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name }}"
data-product-has-picture="{{ BoolToString(!empty(FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->picture_file_name)) }}">
{{ FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name }}@if(!empty(FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->picture_file_name)) <i class="fas fa-image text-muted"></i>@endif
</td>
<td>
<span id="product-{{ $currentStockEntry->product_id }}-amount">{{ $currentStockEntry->amount }}</span> {{ Pluralize($currentStockEntry->amount, FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_stock)->name, FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_stock)->name_plural) }}
@ -89,7 +111,11 @@
{{ FindObjectInArrayByPropertyValue($locations, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->location_id)->name }}
</td>
<td class="d-none">
@if($currentStockEntry->best_before_date < date('Y-m-d', strtotime('-1 days'))) expired @elseif($currentStockEntry->best_before_date < date('Y-m-d', strtotime("+$nextXDays days"))) expiring @elseif (FindObjectInArrayByPropertyValue($missingProducts, 'id', $currentStockEntry->product_id) !== null) belowminstockamount @endif
@if($currentStockEntry->best_before_date < date('Y-m-d', strtotime('-1 days')) && $currentStockEntry->amount > 0) expired @elseif($currentStockEntry->best_before_date < date('Y-m-d', strtotime("+$nextXDays days")) && $currentStockEntry->amount > 0) expiring @elseif (FindObjectInArrayByPropertyValue($missingProducts, 'id', $currentStockEntry->product_id) !== null) belowminstockamount @endif
</td>
@php $productGroup = FindObjectInArrayByPropertyValue($productGroups, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->product_group_id) @endphp
<td class="d-none">
@if($productGroup !== null){{ $productGroup->name }}@endif
</td>
</tr>
@endforeach

View File

@ -4,13 +4,13 @@
"@danielfarrell/bootstrap-combobox@https://github.com/berrnd/bootstrap-combobox.git#master":
version "1.1.8"
resolved "https://github.com/berrnd/bootstrap-combobox.git#d5a43b011d4d2c86537df26e15d2caa51be6a15f"
resolved "https://github.com/berrnd/bootstrap-combobox.git#fcf0110146f4daab94888234c57d198b4ca5f129"
"@fortawesome/fontawesome-free@^5.1.0":
version "5.3.1"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.3.1.tgz#5466b8f31c1f493a96754c1426c25796d0633dd9"
"TagManager@https://github.com/max-favilli/tagmanager.git#3.0.2", "tagmanager@https://github.com/max-favilli/tagmanager.git#3.0.2":
"TagManager@https://github.com/max-favilli/tagmanager.git#3.0.2":
version "3.0.1"
resolved "https://github.com/max-favilli/tagmanager.git#df9eb9935c8585a392dfc00602f890caf233fa94"
dependencies:
@ -108,16 +108,16 @@ datatables.net-responsive@2.2.3, datatables.net-responsive@^2.2.3:
jquery ">=1.7"
datatables.net-rowgroup-bs4@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/datatables.net-rowgroup-bs4/-/datatables.net-rowgroup-bs4-1.0.4.tgz#602f056f9a60bab1b3ac3a36088636f40156b05a"
version "1.1.0"
resolved "https://registry.yarnpkg.com/datatables.net-rowgroup-bs4/-/datatables.net-rowgroup-bs4-1.1.0.tgz#bcaa9842bc9cf70eeba19e8af6edad190c7b896e"
dependencies:
datatables.net-bs4 "^1.10.15"
datatables.net-rowgroup "1.0.4"
datatables.net-rowgroup "1.1.0"
jquery ">=1.7"
datatables.net-rowgroup@1.0.4, datatables.net-rowgroup@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/datatables.net-rowgroup/-/datatables.net-rowgroup-1.0.4.tgz#2caf979f28747be7d9ab66725b639b73099d8eb0"
datatables.net-rowgroup@1.1.0, datatables.net-rowgroup@^1.0.4:
version "1.1.0"
resolved "https://registry.yarnpkg.com/datatables.net-rowgroup/-/datatables.net-rowgroup-1.1.0.tgz#638efb37a1a15f5b3402b7dbce89b3bcdc286f1a"
dependencies:
datatables.net "^1.10.15"
jquery ">=1.7"
@ -202,9 +202,13 @@ startbootstrap-sb-admin@^4.0.0:
jquery "3.3.1"
jquery.easing "^1.4.1"
summernote@^0.8.10:
version "0.8.10"
resolved "https://registry.yarnpkg.com/summernote/-/summernote-0.8.10.tgz#21a5d7f18a3b07500b58b60d5907417a54897520"
swagger-ui-dist@^3.17.3:
version "3.19.0"
resolved "https://registry.yarnpkg.com/swagger-ui-dist/-/swagger-ui-dist-3.19.0.tgz#95942ce1a556e7fe2705d7c92c6004a628d53207"
version "3.19.2"
resolved "https://registry.yarnpkg.com/swagger-ui-dist/-/swagger-ui-dist-3.19.2.tgz#3218f205e7cbc9f0c7c11fabbee07340173ae939"
tempusdominus-bootstrap-4@^5.0.1:
version "5.1.1"