diff --git a/composer.lock b/composer.lock
index 18c86b17..9d9424cf 100644
--- a/composer.lock
+++ b/composer.lock
@@ -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",
diff --git a/config-dist.php b/config-dist.php
index 24c4007a..44bdc278 100644
--- a/config-dist.php
+++ b/config-dist.php
@@ -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);
diff --git a/controllers/BaseController.php b/controllers/BaseController.php
index b0a10d4c..57e81893 100644
--- a/controllers/BaseController.php
+++ b/controllers/BaseController.php
@@ -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;
}
diff --git a/controllers/EquipmentController.php b/controllers/EquipmentController.php
new file mode 100644
index 00000000..a5804a5a
--- /dev/null
+++ b/controllers/EquipmentController.php
@@ -0,0 +1,31 @@
+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'
+ ]);
+ }
+ }
+}
diff --git a/controllers/FilesApiController.php b/controllers/FilesApiController.php
index 91192e47..4bef5531 100644
--- a/controllers/FilesApiController.php
+++ b/controllers/FilesApiController.php
@@ -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)
{
diff --git a/controllers/LoginController.php b/controllers/LoginController.php
index 0d850901..e1691ad4 100644
--- a/controllers/LoginController.php
+++ b/controllers/LoginController.php
@@ -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))
{
diff --git a/controllers/StockApiController.php b/controllers/StockApiController.php
index 350a580f..04f9e04f 100644
--- a/controllers/StockApiController.php
+++ b/controllers/StockApiController.php
@@ -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(
diff --git a/controllers/StockController.php b/controllers/StockController.php
index 9fb222fd..1bafe1d1 100644
--- a/controllers/StockController.php
+++ b/controllers/StockController.php
@@ -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')
]);
}
diff --git a/controllers/SystemApiController.php b/controllers/SystemApiController.php
index b72939a2..23664eb3 100644
--- a/controllers/SystemApiController.php
+++ b/controllers/SystemApiController.php
@@ -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());
+ }
+ }
+ }
}
diff --git a/controllers/UsersApiController.php b/controllers/UsersApiController.php
index 4b1b10e0..9afa7475 100644
--- a/controllers/UsersApiController.php
+++ b/controllers/UsersApiController.php
@@ -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());
+ }
+ }
}
diff --git a/grocy.openapi.json b/grocy.openapi.json
index fcb0883f..4ab5c640 100644
--- a/grocy.openapi.json
+++ b/grocy.openapi.json
@@ -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": {
diff --git a/helpers/extensions.php b/helpers/extensions.php
index 416e2809..06e11b85 100644
--- a/helpers/extensions.php
+++ b/helpers/extensions.php
@@ -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;
}
diff --git a/localization/de.php b/localization/de.php
index aadbc31e..8dc56887 100644
--- a/localization/de.php
+++ b/localization/de.php
@@ -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 ctrl oder ⌘ + C um Tabelle
in Zwischenspeicher zu kopieren.
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'
);
diff --git a/localization/en.php b/localization/en.php
index 84f00ed2..4c951a26 100644
--- a/localization/en.php
+++ b/localization/en.php
@@ -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' => ''
);
diff --git a/localization/it.php b/localization/it.php
index 5b42fdb5..704d62fa 100644
--- a/localization/it.php
+++ b/localization/it.php
@@ -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',
diff --git a/localization/no.php b/localization/no.php
index 07900fa5..e1188d5d 100644
--- a/localization/no.php
+++ b/localization/no.php
@@ -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 ctrl eller ⌘ + C for å kopiere tabell
til utklipptavlen.
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'
);
diff --git a/middleware/JsonMiddleware.php b/middleware/JsonMiddleware.php
index a20f0ed9..9e7cb635 100644
--- a/middleware/JsonMiddleware.php
+++ b/middleware/JsonMiddleware.php
@@ -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');
+ }
}
}
diff --git a/migrations/0038.sql b/migrations/0038.sql
new file mode 100644
index 00000000..3595c9fd
--- /dev/null
+++ b/migrations/0038.sql
@@ -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;
diff --git a/migrations/0039.sql b/migrations/0039.sql
new file mode 100644
index 00000000..7f8e9c80
--- /dev/null
+++ b/migrations/0039.sql
@@ -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)
+);
diff --git a/migrations/0040.sql b/migrations/0040.sql
new file mode 100644
index 00000000..8abf173f
--- /dev/null
+++ b/migrations/0040.sql
@@ -0,0 +1,2 @@
+ALTER TABLE products
+ADD picture_file_name TEXT;
diff --git a/migrations/0041.sql b/migrations/0041.sql
new file mode 100644
index 00000000..78f3afec
--- /dev/null
+++ b/migrations/0041.sql
@@ -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'))
+)
diff --git a/package.json b/package.json
index dbb1cd39..3ce30e1d 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/public/css/grocy.css b/public/css/grocy.css
index 97f4fa8d..179db199 100644
--- a/public/css/grocy.css
+++ b/public/css/grocy.css
@@ -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;
diff --git a/public/css/grocy_night_mode.css b/public/css/grocy_night_mode.css
index 6848a117..34e6a4de 100644
--- a/public/css/grocy_night_mode.css
+++ b/public/css/grocy_night_mode.css
@@ -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;
+}
diff --git a/public/js/extensions.js b/public/js/extensions.js
index 8e3340fe..16419e21 100644
--- a/public/js/extensions.js
+++ b/public/js/extensions.js
@@ -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();
+}
diff --git a/public/js/grocy.js b/public/js/grocy.js
index 24b8fd29..1d925244 100644
--- a/public/js/grocy.js
+++ b/public/js/grocy.js
@@ -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)
+{
+ $("
+@endpush
+
@section('content')