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')
@@ -28,6 +36,15 @@ @endforeach
+
+ + +