mirror of
				https://github.com/grocy/grocy.git
				synced 2025-10-31 10:46:36 +00:00 
			
		
		
		
	Compare commits
	
		
			151 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 5a0b862d22 | ||
|  | bb5fd8360b | ||
|  | d7180bd7b2 | ||
|  | 8c9b0dedb2 | ||
|  | 9c2c2c1fa2 | ||
|  | 596dc9e36d | ||
|  | b2019ba42d | ||
|  | 003d4a567a | ||
|  | 5112e0f551 | ||
|  | 8008fcdc65 | ||
|  | 8d41dcc650 | ||
|  | 037d024862 | ||
|  | 03ca5cd45b | ||
|  | 60d47bef84 | ||
|  | 98a7bcb044 | ||
|  | 7401971884 | ||
|  | 067a10e1b2 | ||
|  | ddfe33fab6 | ||
|  | 2a0ec30bb0 | ||
|  | 8540fc44f3 | ||
|  | 66095738e3 | ||
|  | e472711d23 | ||
|  | 8e054a4981 | ||
|  | feb28211d8 | ||
|  | 06f25b7006 | ||
|  | f85a67a1ff | ||
|  | 6fe0100927 | ||
|  | bcb359e317 | ||
|  | 4075067a10 | ||
|  | bd3c63218b | ||
|  | 27daf384da | ||
|  | 905fc0f357 | ||
|  | 9cd0e4ab2d | ||
|  | 6b38cd450f | ||
|  | bb60f5f043 | ||
|  | e777be4d3b | ||
|  | 8a71d55f0f | ||
|  | b01b49d10c | ||
|  | 496594d898 | ||
|  | 1d5e82c341 | ||
|  | a9b696f41c | ||
|  | e50b1eb359 | ||
|  | 92e0245387 | ||
|  | 67d0d3c3d6 | ||
|  | 23bcbc23e9 | ||
|  | 085d9a0bc7 | ||
|  | 368df142cf | ||
|  | d38edabb14 | ||
|  | 4426a10e2e | ||
|  | 931dc9d243 | ||
|  | c5b8893008 | ||
|  | c27f41aee4 | ||
|  | ef043b38ce | ||
|  | bb261f99c4 | ||
|  | 48ca0f2ac7 | ||
|  | b7f0b06684 | ||
|  | 324487d395 | ||
|  | 9a8c61497b | ||
|  | bc7afe4bdd | ||
|  | bb5dcb2434 | ||
|  | 71b9d11ff5 | ||
|  | 3e73a44576 | ||
|  | dedfe3a854 | ||
|  | c4b0ef4d49 | ||
|  | 339d81318f | ||
|  | 282ee0885b | ||
|  | 5833364e51 | ||
|  | 525f1705d1 | ||
|  | 5a13cb5ffe | ||
|  | e830805443 | ||
|  | ca3f28b615 | ||
|  | 6081b8ee67 | ||
|  | 7eef4acd81 | ||
|  | 678579e933 | ||
|  | 4cc2d39063 | ||
|  | 14cc153422 | ||
|  | f5b5c4c7e1 | ||
|  | 88b76a52a5 | ||
|  | a4a25af460 | ||
|  | 41a72d11da | ||
|  | c8236b101b | ||
|  | ef1df0a446 | ||
|  | 5c4953b9b2 | ||
|  | ccaf2411fe | ||
|  | bce8bd6b35 | ||
|  | 66c07887cb | ||
|  | be99880ce4 | ||
|  | e026609972 | ||
|  | 3474f55866 | ||
|  | f583810d5c | ||
|  | 419445f5ae | ||
|  | c64eb27ca1 | ||
|  | f4eb5196f7 | ||
|  | 9e493430d8 | ||
|  | 7690eedd70 | ||
|  | aaa270a52f | ||
|  | 6f47a5415c | ||
|  | 42c1709633 | ||
|  | 4685ff4145 | ||
|  | 249b01d7a8 | ||
|  | bcbdf58376 | ||
|  | 7f8540ff4e | ||
|  | b52ab91606 | ||
|  | 7246ac55b6 | ||
|  | 848931da21 | ||
|  | bf4092e746 | ||
|  | 7cee18c926 | ||
|  | e9a4b43268 | ||
|  | b1522742cc | ||
|  | ecdaaab789 | ||
|  | 3379942086 | ||
|  | 12eaa8c074 | ||
|  | c6310d636d | ||
|  | 9bedc6a138 | ||
|  | 70dbc6018f | ||
|  | 3afeb44b1d | ||
|  | 3131b8965e | ||
|  | bbc2fc9e42 | ||
|  | 3b4141eb4d | ||
|  | 5f826be82c | ||
|  | db9ee93d2b | ||
|  | 1eabd29105 | ||
|  | dc05c56440 | ||
|  | cb88ab2080 | ||
|  | 254e1a9bc1 | ||
|  | 0fc7c297bf | ||
|  | 82bfb6a3c3 | ||
|  | 277c622475 | ||
|  | 091a0f3efe | ||
|  | 823c76aa08 | ||
|  | 37dee2a50b | ||
|  | ea0f5101ec | ||
|  | be650d093d | ||
|  | 734814d96b | ||
|  | d9246b9b42 | ||
|  | 70e7e630c3 | ||
|  | 71fc49252f | ||
|  | aa0771877f | ||
|  | e5a4d11c0b | ||
|  | 909949a9e1 | ||
|  | f018696219 | ||
|  | 347a47d0d2 | ||
|  | c3de4b86b0 | ||
|  | 594e77ca41 | ||
|  | 31ce7a13ea | ||
|  | 5d762001c8 | ||
|  | bc3d339d9c | ||
|  | 33e5ed9ddc | ||
|  | 2d712b0ef7 | ||
|  | 13d81a4e4b | ||
|  | 8d917aee12 | 
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,3 +1,4 @@ | ||||
| /public/node_modules | ||||
| /vendor | ||||
| /.release | ||||
| embedded.txt | ||||
							
								
								
									
										46
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										46
									
								
								README.md
									
									
									
									
									
								
							| @@ -2,25 +2,37 @@ | ||||
| ERP beyond your fridge | ||||
|  | ||||
| ## Give it a try | ||||
| Public demo of the latest version → [https://demo.grocy.info](https://demo.grocy.info)  | ||||
| - Public demo of the latest stable version → [https://demo.grocy.info](https://demo.grocy.info) | ||||
| - Public demo of the latest pre-release version (current master branch) → [https://demo-prerelease.grocy.info](https://demo-prerelease.grocy.info) | ||||
|  | ||||
| ## Motivation | ||||
| A household needs to be managed. I did this so far (almost 10 years) with my first self written software (a C# windows forms application) and with a bunch of Excel sheets. The software is a pain to use and Excel is Excel. So I searched for and tried different things for a (very) long time, nothing 100 % fitted, so this is my aim for a "complete houshold management"-thing. | ||||
|  | ||||
| ## What it is about | ||||
| For now my main focus is on stock management, ERP your fridge! | ||||
| A household needs to be managed. I did this so far (almost 10 years) with my first self written software (a C# windows forms application) and with a bunch of Excel sheets. The software is a pain to use and Excel is Excel. So I searched for and tried different things for a (very) long time, nothing 100 % fitted, so this is my aim for a "complete houshold management"-thing. ERP your fridge! | ||||
|  | ||||
| ## How to install | ||||
| Just unpack the [latest release](https://github.com/berrnd/grocy/releases/latest) on your PHP (SQLite extension required, currently only tested with PHP 7.2) enabled webserver (webservers root should point to the `/public` directory), copy `config-dist.php` to `data/config.php`, edit it to your needs, ensure that the `data` directory is writable and you're ready to go. | ||||
| > **NEW** | ||||
| > | ||||
| > There is now grocy-desktop if you want to run grocy without a webserver just like a normal (windows) desktop application. | ||||
| > | ||||
| > See https://github.com/berrnd/grocy-desktop or directly download the [latest release](https://releases.grocy.info/latest-desktop) - the installation is nothing more than just clicking 2 times "next"... | ||||
|  | ||||
| Default login is user `admin` with password `admin` - see the `data/config.php` file. Alternatively clone this repository and install Composer and Yarn dependencies manually. | ||||
| Just unpack the [latest release](https://releases.grocy.info/latest) on your PHP (SQLite extension required, currently only tested with PHP 7.2) enabled webserver (webservers root should point to the `public` directory), copy `config-dist.php` to `data/config.php`, edit it to your needs, ensure that the `data` directory is writable and you're ready to go, (to make writable `chown -R www-data:www-data data/`). Default login is user `admin` with password `admin`, please change the password immediately (see user menu). | ||||
|  | ||||
| Alternatively clone this repository and install Composer and Yarn dependencies manually. | ||||
|  | ||||
| If you use nginx as your webserver, please include `try_files $uri /index.php;` in your location block. | ||||
|  | ||||
| If, however, your webserver does not support URL rewriting, set `DISABLE_URL_REWRITING` in `data/config.php` (`Setting('DISABLE_URL_REWRITING', true);`). | ||||
|  | ||||
| ## How to update | ||||
| Just overwrite everything with the latest release while keeping the `/data` directory, check `config-dist.php` for new configuration options and add them to your `data/config.php` (the default from values `config-dist.php` will be used for not in `data/config.php` defined settings). | ||||
| Just overwrite everything with the latest release while keeping the `data` directory, check `config-dist.php` for new configuration options and add them to your `data/config.php` (the default from values `config-dist.php` will be used for not in `data/config.php` defined settings). Just to be sure, please empty `data/viewcache`. | ||||
|  | ||||
| If you run grocy on Linux, there is also `update.sh` (remember to make the script executable, `chmod +x update.sh` and ensure that you have `unzip` installed) which does exactly this and additionally creates a backup (`.tgz` archive) of the current installation in `data/backups` (backups older than 60 days will be deleted during the update). | ||||
|  | ||||
| ## Localization | ||||
| grocy is fully localizable - the default language is English (integrated into code), a German localization is always maintained by me. There is one file per language in the `localization` directory, if you want to create a translation, it's best to copy `localization/de.php` to a new one (e. g. `localization/it.php`) and translating all strings there. (Language can be changed in `data/config.php`, e. g. `Setting('CULTURE', 'it');`) | ||||
|  | ||||
| ### Maintaining your own localization | ||||
| As the German translation will always be the most complete one, for maintaining your localization it would be easiest when you compare your localization with the German one with a diff tool of your choice. | ||||
|  | ||||
| ## Things worth to know | ||||
|  | ||||
| @@ -33,10 +45,12 @@ Some fields also allow to select a value by scanning a barcode. It works best wh | ||||
| ### Input shorthands for date fields | ||||
| For (productivity) reasons all date (and time) input fields use the ISO-8601 format regardless of localization. | ||||
| The following shorthands are available: | ||||
| - `MMDD` gets expanded to the given day on the current year in proper notation | ||||
| - `MMDD` gets expanded to the given day on the current year, if > today, or to the given day next year, if < today, in proper notation | ||||
|   - Example: `0517` will be converted to `2018-05-17` | ||||
| - `YYYYMMDD` gets expanded to the proper ISO-8601 notation | ||||
|   - Example: `20190417` will be converted to `2019-04-17` | ||||
| - `YYYYMMe` or `YYYYMM+` gets expanded to the end of the given month in the given year in proper notation | ||||
|   - Example: `201807e` will be converted to `2018-07-31` | ||||
| - `x` gets expanded to `2999-12-31` (which I use for products which never expire) | ||||
| - Down/up arrow keys will increase/decrease the date by one day | ||||
| - Right/left arrow keys will increase/decrease the date by 1 week | ||||
| @@ -50,18 +64,20 @@ Products can be directly added to the database via looking them up against exter | ||||
| This is currently only possible through the REST API. | ||||
| There is no plugin included for any service, see the reference implementation in `data/plugins/DemoBarcodeLookupPlugin.php`. | ||||
|  | ||||
| ### Localization | ||||
| grocy is fully localizable - the default language is English (integrated into code), a German localization is always maintained by me. There is one file per language in the `localization` directory, if you want to create a translation, it's best to copy `localization/de.php` to a new one (e. g. `localization/it.php`) and translating all strings there. (Language can be changed in `data/config.php`, e. g. `Setting('CULTURE', 'it');`) | ||||
|  | ||||
| ### Database migrations | ||||
| Database schema migration is automatically done when visiting the root (`/`) route (click on the logo in the left upper edge). | ||||
|  | ||||
| ### Adding your own CSS or JS without to have to modify the application itself | ||||
| - When the file `data/custom_js.html` exists, the contents of the file will be added just before `</body>` (end of body) on every page | ||||
| - When the file `data/custom_css.html` exists, the contents of the file will be added just before `</head>` (end of head) on every page | ||||
|  | ||||
| ### Demo mode | ||||
| When the file `data/demo.txt` exists, the application will work in a demo mode which means authentication is disabled and some demo data will be generated during the database schema migration. | ||||
|  | ||||
| ### Adding your own CSS or JS without to have to modify the application itself | ||||
| - When the file `data/custom.js` exists, the contents of the file be added just before `</body>` on every page | ||||
| - When the file `data/custom.css` exists, the contents of the file be added just before `</head>` on every page | ||||
| ### Embedded mode | ||||
| When the file `embedded.txt` exists, it must contain a valid and writable path which will be used as the data directory instead of `data` and authentication will be disabled (used in [grocy-desktop](https://github.com/berrnd/grocy-desktop)). | ||||
|  | ||||
| In embedded mode, settings can be overridden by text files in `data/settingoverrides`, the file name must be `<SettingName>.txt` (e. g. `BASE_URL.txt`) and the content must be the setting value (normally one single line). | ||||
|  | ||||
| ## Screenshots | ||||
| #### Dashboard | ||||
|   | ||||
							
								
								
									
										42
									
								
								app.php
									
									
									
									
									
								
							
							
						
						
									
										42
									
								
								app.php
									
									
									
									
									
								
							| @@ -6,8 +6,38 @@ use \Psr\Http\Message\ResponseInterface as Response; | ||||
| use \Grocy\Helpers\UrlManager; | ||||
| use \Grocy\Controllers\LoginController; | ||||
|  | ||||
| // Definitions for embedded mode | ||||
| if (file_exists(__DIR__ . '/embedded.txt')) | ||||
| { | ||||
| 	define('GROCY_IS_EMBEDDED_INSTALL', true); | ||||
| 	define('GROCY_DATAPATH', file_get_contents(__DIR__ . '/embedded.txt')); | ||||
| 	define('GROCY_USER_ID', 1); | ||||
| } | ||||
| else | ||||
| { | ||||
| 	define('GROCY_IS_EMBEDDED_INSTALL', false); | ||||
| 	define('GROCY_DATAPATH', __DIR__ . '/data'); | ||||
| } | ||||
|  | ||||
| // Definitions for demo mode | ||||
| if (file_exists(GROCY_DATAPATH . '/demo.txt')) | ||||
| { | ||||
| 	define('GROCY_IS_DEMO_INSTALL', true); | ||||
| 	if (!defined('GROCY_USER_ID')) | ||||
| 	{ | ||||
| 		define('GROCY_USER_ID', 1); | ||||
| 	} | ||||
| } | ||||
| else | ||||
| { | ||||
| 	define('GROCY_IS_DEMO_INSTALL', false); | ||||
| } | ||||
|  | ||||
| // Load composer dependencies | ||||
| require_once __DIR__ . '/vendor/autoload.php'; | ||||
| require_once __DIR__ . '/data/config.php'; | ||||
|  | ||||
| // Load config files | ||||
| require_once GROCY_DATAPATH . '/config.php'; | ||||
| require_once __DIR__ . '/config-dist.php'; //For not in own config defined values we use the default ones | ||||
|  | ||||
| // Setup base application | ||||
| @@ -18,7 +48,7 @@ $appContainer = new \Slim\Container([ | ||||
| 	], | ||||
| 	'view' => function($container) | ||||
| 	{ | ||||
| 		return new \Slim\Views\Blade(__DIR__ . '/views', __DIR__ . '/data/viewcache'); | ||||
| 		return new \Slim\Views\Blade(__DIR__ . '/views', GROCY_DATAPATH . '/viewcache'); | ||||
| 	}, | ||||
| 	'LoginControllerInstance' => function($container) | ||||
| 	{ | ||||
| @@ -26,7 +56,7 @@ $appContainer = new \Slim\Container([ | ||||
| 	}, | ||||
| 	'UrlManager' => function($container) | ||||
| 	{ | ||||
| 		return new UrlManager(BASE_URL); | ||||
| 		return new UrlManager(GROCY_BASE_URL); | ||||
| 	}, | ||||
| 	'ApiKeyHeaderName' => function($container) | ||||
| 	{ | ||||
| @@ -35,11 +65,7 @@ $appContainer = new \Slim\Container([ | ||||
| ]); | ||||
| $app = new \Slim\App($appContainer); | ||||
|  | ||||
| if (PHP_SAPI === 'cli') | ||||
| { | ||||
| 	$app->add(\pavlakis\cli\CliRequest::class); | ||||
| } | ||||
|  | ||||
| // Load routes from separate file | ||||
| require_once __DIR__ . '/routes.php'; | ||||
|  | ||||
| $app->run(); | ||||
|   | ||||
| @@ -10,4 +10,4 @@ del "%releasePath%\grocy_%version%.zip" | ||||
| "build_tools\7za.exe" a -r "%releasePath%\grocy_%version%.zip" "%projectPath%\*" -xr!.* -xr!build_tools -xr!build.bat -xr!composer.json -xr!composer.lock -xr!package.json -xr!yarn.lock -xr!publication_assets | ||||
| "build_tools\7za.exe" a "%releasePath%\grocy_%version%.zip" "%projectPath%\public\.htaccess" | ||||
| "build_tools\7za.exe" rn "%releasePath%\grocy_%version%.zip" .htaccess public\.htaccess | ||||
| "build_tools\7za.exe" d "%releasePath%\grocy_%version%.zip" data\*.* data\sessions data\viewcache\* | ||||
| "build_tools\7za.exe" d "%releasePath%\grocy_%version%.zip" data\*.* data\storage data\viewcache\* | ||||
|   | ||||
| @@ -3,7 +3,6 @@ | ||||
| 		"php": ">=7.2", | ||||
| 		"slim/slim": "^3.8", | ||||
| 		"morris/lessql": "^0.3.4", | ||||
| 		"pavlakis/slim-cli": "^1.0", | ||||
| 		"rubellum/slim-blade-view": "^0.1.1", | ||||
| 		"tuupola/cors-middleware": "^0.7.0" | ||||
| 	}, | ||||
|   | ||||
							
								
								
									
										241
									
								
								composer.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										241
									
								
								composer.lock
									
									
									
										generated
									
									
									
								
							| @@ -4,7 +4,7 @@ | ||||
|         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", | ||||
|         "This file is @generated automatically" | ||||
|     ], | ||||
|     "content-hash": "131ab83ecb1ea3d1a431cc70b5092448", | ||||
|     "content-hash": "c1bc4c17739e9d0ee8b33628f6d4b9a4", | ||||
|     "packages": [ | ||||
|         { | ||||
|             "name": "container-interop/container-interop", | ||||
| @@ -154,31 +154,32 @@ | ||||
|                 "request", | ||||
|                 "response" | ||||
|             ], | ||||
|             "abandoned": "psr/http-factory", | ||||
|             "time": "2017-03-24T14:48:51+00:00" | ||||
|         }, | ||||
|         { | ||||
|             "name": "illuminate/container", | ||||
|             "version": "v5.6.27", | ||||
|             "version": "v5.7.5", | ||||
|             "source": { | ||||
|                 "type": "git", | ||||
|                 "url": "https://github.com/illuminate/container.git", | ||||
|                 "reference": "1f0757cae8749400aeda730f6438a081fc3c082d" | ||||
|                 "reference": "0fc33b14ae6cf9a1e694fd43f2a274e590a824b2" | ||||
|             }, | ||||
|             "dist": { | ||||
|                 "type": "zip", | ||||
|                 "url": "https://api.github.com/repos/illuminate/container/zipball/1f0757cae8749400aeda730f6438a081fc3c082d", | ||||
|                 "reference": "1f0757cae8749400aeda730f6438a081fc3c082d", | ||||
|                 "url": "https://api.github.com/repos/illuminate/container/zipball/0fc33b14ae6cf9a1e694fd43f2a274e590a824b2", | ||||
|                 "reference": "0fc33b14ae6cf9a1e694fd43f2a274e590a824b2", | ||||
|                 "shasum": "" | ||||
|             }, | ||||
|             "require": { | ||||
|                 "illuminate/contracts": "5.6.*", | ||||
|                 "illuminate/contracts": "5.7.*", | ||||
|                 "php": "^7.1.3", | ||||
|                 "psr/container": "~1.0" | ||||
|                 "psr/container": "^1.0" | ||||
|             }, | ||||
|             "type": "library", | ||||
|             "extra": { | ||||
|                 "branch-alias": { | ||||
|                     "dev-master": "5.6-dev" | ||||
|                     "dev-master": "5.7-dev" | ||||
|                 } | ||||
|             }, | ||||
|             "autoload": { | ||||
| @@ -198,31 +199,31 @@ | ||||
|             ], | ||||
|             "description": "The Illuminate Container package.", | ||||
|             "homepage": "https://laravel.com", | ||||
|             "time": "2018-05-24T13:16:56+00:00" | ||||
|             "time": "2018-05-28T08:50:10+00:00" | ||||
|         }, | ||||
|         { | ||||
|             "name": "illuminate/contracts", | ||||
|             "version": "v5.6.27", | ||||
|             "version": "v5.7.5", | ||||
|             "source": { | ||||
|                 "type": "git", | ||||
|                 "url": "https://github.com/illuminate/contracts.git", | ||||
|                 "reference": "3dc639feabe0f302f574157a782ede323881a944" | ||||
|                 "reference": "2daf3c078610f744e2a4dc2f44fb5060cce9835b" | ||||
|             }, | ||||
|             "dist": { | ||||
|                 "type": "zip", | ||||
|                 "url": "https://api.github.com/repos/illuminate/contracts/zipball/3dc639feabe0f302f574157a782ede323881a944", | ||||
|                 "reference": "3dc639feabe0f302f574157a782ede323881a944", | ||||
|                 "url": "https://api.github.com/repos/illuminate/contracts/zipball/2daf3c078610f744e2a4dc2f44fb5060cce9835b", | ||||
|                 "reference": "2daf3c078610f744e2a4dc2f44fb5060cce9835b", | ||||
|                 "shasum": "" | ||||
|             }, | ||||
|             "require": { | ||||
|                 "php": "^7.1.3", | ||||
|                 "psr/container": "~1.0", | ||||
|                 "psr/simple-cache": "~1.0" | ||||
|                 "psr/container": "^1.0", | ||||
|                 "psr/simple-cache": "^1.0" | ||||
|             }, | ||||
|             "type": "library", | ||||
|             "extra": { | ||||
|                 "branch-alias": { | ||||
|                     "dev-master": "5.6-dev" | ||||
|                     "dev-master": "5.7-dev" | ||||
|                 } | ||||
|             }, | ||||
|             "autoload": { | ||||
| @@ -242,32 +243,32 @@ | ||||
|             ], | ||||
|             "description": "The Illuminate Contracts package.", | ||||
|             "homepage": "https://laravel.com", | ||||
|             "time": "2018-05-11T23:38:58+00:00" | ||||
|             "time": "2018-09-18T12:50:05+00:00" | ||||
|         }, | ||||
|         { | ||||
|             "name": "illuminate/events", | ||||
|             "version": "v5.6.27", | ||||
|             "version": "v5.7.5", | ||||
|             "source": { | ||||
|                 "type": "git", | ||||
|                 "url": "https://github.com/illuminate/events.git", | ||||
|                 "reference": "b6e73ed40478cef2ef98d5ddb27f333291606cea" | ||||
|                 "reference": "4cf622acc05592f86d4a5c77ad1a544d38e58dee" | ||||
|             }, | ||||
|             "dist": { | ||||
|                 "type": "zip", | ||||
|                 "url": "https://api.github.com/repos/illuminate/events/zipball/b6e73ed40478cef2ef98d5ddb27f333291606cea", | ||||
|                 "reference": "b6e73ed40478cef2ef98d5ddb27f333291606cea", | ||||
|                 "url": "https://api.github.com/repos/illuminate/events/zipball/4cf622acc05592f86d4a5c77ad1a544d38e58dee", | ||||
|                 "reference": "4cf622acc05592f86d4a5c77ad1a544d38e58dee", | ||||
|                 "shasum": "" | ||||
|             }, | ||||
|             "require": { | ||||
|                 "illuminate/container": "5.6.*", | ||||
|                 "illuminate/contracts": "5.6.*", | ||||
|                 "illuminate/support": "5.6.*", | ||||
|                 "illuminate/container": "5.7.*", | ||||
|                 "illuminate/contracts": "5.7.*", | ||||
|                 "illuminate/support": "5.7.*", | ||||
|                 "php": "^7.1.3" | ||||
|             }, | ||||
|             "type": "library", | ||||
|             "extra": { | ||||
|                 "branch-alias": { | ||||
|                     "dev-master": "5.6-dev" | ||||
|                     "dev-master": "5.7-dev" | ||||
|                 } | ||||
|             }, | ||||
|             "autoload": { | ||||
| @@ -287,39 +288,39 @@ | ||||
|             ], | ||||
|             "description": "The Illuminate Events package.", | ||||
|             "homepage": "https://laravel.com", | ||||
|             "time": "2018-02-26T19:00:55+00:00" | ||||
|             "time": "2018-07-26T15:27:42+00:00" | ||||
|         }, | ||||
|         { | ||||
|             "name": "illuminate/filesystem", | ||||
|             "version": "v5.6.27", | ||||
|             "version": "v5.7.5", | ||||
|             "source": { | ||||
|                 "type": "git", | ||||
|                 "url": "https://github.com/illuminate/filesystem.git", | ||||
|                 "reference": "2677365f61c66fad13ff12a37cd4fa8aaeb048d2" | ||||
|                 "reference": "a09fae4470494dc9867609221b46fe844f2f3b70" | ||||
|             }, | ||||
|             "dist": { | ||||
|                 "type": "zip", | ||||
|                 "url": "https://api.github.com/repos/illuminate/filesystem/zipball/2677365f61c66fad13ff12a37cd4fa8aaeb048d2", | ||||
|                 "reference": "2677365f61c66fad13ff12a37cd4fa8aaeb048d2", | ||||
|                 "url": "https://api.github.com/repos/illuminate/filesystem/zipball/a09fae4470494dc9867609221b46fe844f2f3b70", | ||||
|                 "reference": "a09fae4470494dc9867609221b46fe844f2f3b70", | ||||
|                 "shasum": "" | ||||
|             }, | ||||
|             "require": { | ||||
|                 "illuminate/contracts": "5.6.*", | ||||
|                 "illuminate/support": "5.6.*", | ||||
|                 "illuminate/contracts": "5.7.*", | ||||
|                 "illuminate/support": "5.7.*", | ||||
|                 "php": "^7.1.3", | ||||
|                 "symfony/finder": "~4.0" | ||||
|                 "symfony/finder": "^4.1" | ||||
|             }, | ||||
|             "suggest": { | ||||
|                 "league/flysystem": "Required to use the Flysystem local and FTP drivers (~1.0).", | ||||
|                 "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (~1.0).", | ||||
|                 "league/flysystem-cached-adapter": "Required to use the Flysystem cache (~1.0).", | ||||
|                 "league/flysystem-rackspace": "Required to use the Flysystem Rackspace driver (~1.0).", | ||||
|                 "league/flysystem-sftp": "Required to use the Flysystem SFTP driver (~1.0)." | ||||
|                 "league/flysystem": "Required to use the Flysystem local and FTP drivers (^1.0).", | ||||
|                 "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^1.0).", | ||||
|                 "league/flysystem-cached-adapter": "Required to use the Flysystem cache (^1.0).", | ||||
|                 "league/flysystem-rackspace": "Required to use the Flysystem Rackspace driver (^1.0).", | ||||
|                 "league/flysystem-sftp": "Required to use the Flysystem SFTP driver (^1.0)." | ||||
|             }, | ||||
|             "type": "library", | ||||
|             "extra": { | ||||
|                 "branch-alias": { | ||||
|                     "dev-master": "5.6-dev" | ||||
|                     "dev-master": "5.7-dev" | ||||
|                 } | ||||
|             }, | ||||
|             "autoload": { | ||||
| @@ -339,42 +340,43 @@ | ||||
|             ], | ||||
|             "description": "The Illuminate Filesystem package.", | ||||
|             "homepage": "https://laravel.com", | ||||
|             "time": "2018-07-07T14:54:27+00:00" | ||||
|             "time": "2018-08-14T19:42:44+00:00" | ||||
|         }, | ||||
|         { | ||||
|             "name": "illuminate/support", | ||||
|             "version": "v5.6.27", | ||||
|             "version": "v5.7.5", | ||||
|             "source": { | ||||
|                 "type": "git", | ||||
|                 "url": "https://github.com/illuminate/support.git", | ||||
|                 "reference": "97ca44c95392ce0a41749fa47b953734d88b94b1" | ||||
|                 "reference": "f7c68e8c8aab200cc8ad84f974d5511cda58a742" | ||||
|             }, | ||||
|             "dist": { | ||||
|                 "type": "zip", | ||||
|                 "url": "https://api.github.com/repos/illuminate/support/zipball/97ca44c95392ce0a41749fa47b953734d88b94b1", | ||||
|                 "reference": "97ca44c95392ce0a41749fa47b953734d88b94b1", | ||||
|                 "url": "https://api.github.com/repos/illuminate/support/zipball/f7c68e8c8aab200cc8ad84f974d5511cda58a742", | ||||
|                 "reference": "f7c68e8c8aab200cc8ad84f974d5511cda58a742", | ||||
|                 "shasum": "" | ||||
|             }, | ||||
|             "require": { | ||||
|                 "doctrine/inflector": "~1.1", | ||||
|                 "doctrine/inflector": "^1.1", | ||||
|                 "ext-mbstring": "*", | ||||
|                 "illuminate/contracts": "5.6.*", | ||||
|                 "nesbot/carbon": "^1.24.1", | ||||
|                 "illuminate/contracts": "5.7.*", | ||||
|                 "nesbot/carbon": "^1.26.3", | ||||
|                 "php": "^7.1.3" | ||||
|             }, | ||||
|             "conflict": { | ||||
|                 "tightenco/collect": "<5.5.33" | ||||
|             }, | ||||
|             "suggest": { | ||||
|                 "illuminate/filesystem": "Required to use the composer class (5.6.*).", | ||||
|                 "illuminate/filesystem": "Required to use the composer class (5.7.*).", | ||||
|                 "moontoast/math": "Required to use ordered UUIDs (^1.1).", | ||||
|                 "ramsey/uuid": "Required to use Str::uuid() (^3.7).", | ||||
|                 "symfony/process": "Required to use the composer class (~4.0).", | ||||
|                 "symfony/var-dumper": "Required to use the dd function (~4.0)." | ||||
|                 "symfony/process": "Required to use the composer class (^4.1).", | ||||
|                 "symfony/var-dumper": "Required to use the dd function (^4.1)." | ||||
|             }, | ||||
|             "type": "library", | ||||
|             "extra": { | ||||
|                 "branch-alias": { | ||||
|                     "dev-master": "5.6-dev" | ||||
|                     "dev-master": "5.7-dev" | ||||
|                 } | ||||
|             }, | ||||
|             "autoload": { | ||||
| @@ -397,35 +399,35 @@ | ||||
|             ], | ||||
|             "description": "The Illuminate Support package.", | ||||
|             "homepage": "https://laravel.com", | ||||
|             "time": "2018-07-04T01:23:57+00:00" | ||||
|             "time": "2018-09-19T18:36:57+00:00" | ||||
|         }, | ||||
|         { | ||||
|             "name": "illuminate/view", | ||||
|             "version": "v5.6.27", | ||||
|             "version": "v5.7.5", | ||||
|             "source": { | ||||
|                 "type": "git", | ||||
|                 "url": "https://github.com/illuminate/view.git", | ||||
|                 "reference": "625c35e8942f0ecd467acb8db8daf8449390d559" | ||||
|                 "reference": "3ccd29550afe61eb02ad9e4bae0c2e661aadd7af" | ||||
|             }, | ||||
|             "dist": { | ||||
|                 "type": "zip", | ||||
|                 "url": "https://api.github.com/repos/illuminate/view/zipball/625c35e8942f0ecd467acb8db8daf8449390d559", | ||||
|                 "reference": "625c35e8942f0ecd467acb8db8daf8449390d559", | ||||
|                 "url": "https://api.github.com/repos/illuminate/view/zipball/3ccd29550afe61eb02ad9e4bae0c2e661aadd7af", | ||||
|                 "reference": "3ccd29550afe61eb02ad9e4bae0c2e661aadd7af", | ||||
|                 "shasum": "" | ||||
|             }, | ||||
|             "require": { | ||||
|                 "illuminate/container": "5.6.*", | ||||
|                 "illuminate/contracts": "5.6.*", | ||||
|                 "illuminate/events": "5.6.*", | ||||
|                 "illuminate/filesystem": "5.6.*", | ||||
|                 "illuminate/support": "5.6.*", | ||||
|                 "illuminate/container": "5.7.*", | ||||
|                 "illuminate/contracts": "5.7.*", | ||||
|                 "illuminate/events": "5.7.*", | ||||
|                 "illuminate/filesystem": "5.7.*", | ||||
|                 "illuminate/support": "5.7.*", | ||||
|                 "php": "^7.1.3", | ||||
|                 "symfony/debug": "~4.0" | ||||
|                 "symfony/debug": "^4.1" | ||||
|             }, | ||||
|             "type": "library", | ||||
|             "extra": { | ||||
|                 "branch-alias": { | ||||
|                     "dev-master": "5.6-dev" | ||||
|                     "dev-master": "5.7-dev" | ||||
|                 } | ||||
|             }, | ||||
|             "autoload": { | ||||
| @@ -445,7 +447,7 @@ | ||||
|             ], | ||||
|             "description": "The Illuminate View package.", | ||||
|             "homepage": "https://laravel.com", | ||||
|             "time": "2018-07-06T14:55:12+00:00" | ||||
|             "time": "2018-09-18T12:50:05+00:00" | ||||
|         }, | ||||
|         { | ||||
|             "name": "morris/lessql", | ||||
| @@ -552,16 +554,16 @@ | ||||
|         }, | ||||
|         { | ||||
|             "name": "nesbot/carbon", | ||||
|             "version": "1.32.0", | ||||
|             "version": "1.34.0", | ||||
|             "source": { | ||||
|                 "type": "git", | ||||
|                 "url": "https://github.com/briannesbitt/Carbon.git", | ||||
|                 "reference": "64563e2b9f69e4db1b82a60e81efa327a30ff343" | ||||
|                 "reference": "1dbd3cb01c5645f3e7deda7aa46ef780d95fcc33" | ||||
|             }, | ||||
|             "dist": { | ||||
|                 "type": "zip", | ||||
|                 "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/64563e2b9f69e4db1b82a60e81efa327a30ff343", | ||||
|                 "reference": "64563e2b9f69e4db1b82a60e81efa327a30ff343", | ||||
|                 "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/1dbd3cb01c5645f3e7deda7aa46ef780d95fcc33", | ||||
|                 "reference": "1dbd3cb01c5645f3e7deda7aa46ef780d95fcc33", | ||||
|                 "shasum": "" | ||||
|             }, | ||||
|             "require": { | ||||
| @@ -603,7 +605,7 @@ | ||||
|                 "datetime", | ||||
|                 "time" | ||||
|             ], | ||||
|             "time": "2018-07-05T06:59:26+00:00" | ||||
|             "time": "2018-09-20T19:36:25+00:00" | ||||
|         }, | ||||
|         { | ||||
|             "name": "nikic/fast-route", | ||||
| @@ -651,55 +653,6 @@ | ||||
|             ], | ||||
|             "time": "2018-02-13T20:26:39+00:00" | ||||
|         }, | ||||
|         { | ||||
|             "name": "pavlakis/slim-cli", | ||||
|             "version": "1.0.4", | ||||
|             "source": { | ||||
|                 "type": "git", | ||||
|                 "url": "https://github.com/pavlakis/slim-cli.git", | ||||
|                 "reference": "603933a54e391b3c70c573206cce543b75d8b1db" | ||||
|             }, | ||||
|             "dist": { | ||||
|                 "type": "zip", | ||||
|                 "url": "https://api.github.com/repos/pavlakis/slim-cli/zipball/603933a54e391b3c70c573206cce543b75d8b1db", | ||||
|                 "reference": "603933a54e391b3c70c573206cce543b75d8b1db", | ||||
|                 "shasum": "" | ||||
|             }, | ||||
|             "require": { | ||||
|                 "php": "^5.5|^5.6|^7.0|^7.1" | ||||
|             }, | ||||
|             "require-dev": { | ||||
|                 "phpunit/phpunit": "^4.0", | ||||
|                 "slim/slim": "^3.0" | ||||
|             }, | ||||
|             "type": "library", | ||||
|             "autoload": { | ||||
|                 "psr-4": { | ||||
|                     "pavlakis\\cli\\tests\\": "tests/phpunit", | ||||
|                     "pavlakis\\cli\\": "src" | ||||
|                 } | ||||
|             }, | ||||
|             "notification-url": "https://packagist.org/downloads/", | ||||
|             "license": [ | ||||
|                 "BSD-3-Clause" | ||||
|             ], | ||||
|             "authors": [ | ||||
|                 { | ||||
|                     "name": "Antonis Pavlakis", | ||||
|                     "email": "adoni@pavlakis.info", | ||||
|                     "homepage": "http://pavlakis.info" | ||||
|                 } | ||||
|             ], | ||||
|             "description": "Making a mock GET request through the CLI and enabling the same application entry point on CLI scripts.", | ||||
|             "homepage": "http://github.com/pavlakis/slim-cli", | ||||
|             "keywords": [ | ||||
|                 "cli", | ||||
|                 "framework", | ||||
|                 "middleware", | ||||
|                 "slim" | ||||
|             ], | ||||
|             "time": "2017-01-30T22:50:06+00:00" | ||||
|         }, | ||||
|         { | ||||
|             "name": "philo/laravel-blade", | ||||
|             "version": "v3.1", | ||||
| @@ -1143,16 +1096,16 @@ | ||||
|         }, | ||||
|         { | ||||
|             "name": "slim/slim", | ||||
|             "version": "3.10.0", | ||||
|             "version": "3.11.0", | ||||
|             "source": { | ||||
|                 "type": "git", | ||||
|                 "url": "https://github.com/slimphp/Slim.git", | ||||
|                 "reference": "d8aabeacc3688b25e2f2dd2db91df91ec6fdd748" | ||||
|                 "reference": "d378e70431e78ee92ee32ddde61ecc72edf5dc0a" | ||||
|             }, | ||||
|             "dist": { | ||||
|                 "type": "zip", | ||||
|                 "url": "https://api.github.com/repos/slimphp/Slim/zipball/d8aabeacc3688b25e2f2dd2db91df91ec6fdd748", | ||||
|                 "reference": "d8aabeacc3688b25e2f2dd2db91df91ec6fdd748", | ||||
|                 "url": "https://api.github.com/repos/slimphp/Slim/zipball/d378e70431e78ee92ee32ddde61ecc72edf5dc0a", | ||||
|                 "reference": "d378e70431e78ee92ee32ddde61ecc72edf5dc0a", | ||||
|                 "shasum": "" | ||||
|             }, | ||||
|             "require": { | ||||
| @@ -1210,20 +1163,20 @@ | ||||
|                 "micro", | ||||
|                 "router" | ||||
|             ], | ||||
|             "time": "2018-04-19T19:29:08+00:00" | ||||
|             "time": "2018-09-16T10:54:21+00:00" | ||||
|         }, | ||||
|         { | ||||
|             "name": "symfony/debug", | ||||
|             "version": "v4.1.1", | ||||
|             "version": "v4.1.4", | ||||
|             "source": { | ||||
|                 "type": "git", | ||||
|                 "url": "https://github.com/symfony/debug.git", | ||||
|                 "reference": "dbe0fad88046a755dcf9379f2964c61a02f5ae3d" | ||||
|                 "reference": "47ead688f1f2877f3f14219670f52e4722ee7052" | ||||
|             }, | ||||
|             "dist": { | ||||
|                 "type": "zip", | ||||
|                 "url": "https://api.github.com/repos/symfony/debug/zipball/dbe0fad88046a755dcf9379f2964c61a02f5ae3d", | ||||
|                 "reference": "dbe0fad88046a755dcf9379f2964c61a02f5ae3d", | ||||
|                 "url": "https://api.github.com/repos/symfony/debug/zipball/47ead688f1f2877f3f14219670f52e4722ee7052", | ||||
|                 "reference": "47ead688f1f2877f3f14219670f52e4722ee7052", | ||||
|                 "shasum": "" | ||||
|             }, | ||||
|             "require": { | ||||
| @@ -1266,20 +1219,20 @@ | ||||
|             ], | ||||
|             "description": "Symfony Debug Component", | ||||
|             "homepage": "https://symfony.com", | ||||
|             "time": "2018-06-08T09:39:36+00:00" | ||||
|             "time": "2018-08-03T11:13:38+00:00" | ||||
|         }, | ||||
|         { | ||||
|             "name": "symfony/finder", | ||||
|             "version": "v4.1.1", | ||||
|             "version": "v4.1.4", | ||||
|             "source": { | ||||
|                 "type": "git", | ||||
|                 "url": "https://github.com/symfony/finder.git", | ||||
|                 "reference": "84714b8417d19e4ba02ea78a41a975b3efaafddb" | ||||
|                 "reference": "e162f1df3102d0b7472805a5a9d5db9fcf0a8068" | ||||
|             }, | ||||
|             "dist": { | ||||
|                 "type": "zip", | ||||
|                 "url": "https://api.github.com/repos/symfony/finder/zipball/84714b8417d19e4ba02ea78a41a975b3efaafddb", | ||||
|                 "reference": "84714b8417d19e4ba02ea78a41a975b3efaafddb", | ||||
|                 "url": "https://api.github.com/repos/symfony/finder/zipball/e162f1df3102d0b7472805a5a9d5db9fcf0a8068", | ||||
|                 "reference": "e162f1df3102d0b7472805a5a9d5db9fcf0a8068", | ||||
|                 "shasum": "" | ||||
|             }, | ||||
|             "require": { | ||||
| @@ -1315,20 +1268,20 @@ | ||||
|             ], | ||||
|             "description": "Symfony Finder Component", | ||||
|             "homepage": "https://symfony.com", | ||||
|             "time": "2018-06-19T21:38:16+00:00" | ||||
|             "time": "2018-07-26T11:24:31+00:00" | ||||
|         }, | ||||
|         { | ||||
|             "name": "symfony/polyfill-mbstring", | ||||
|             "version": "v1.8.0", | ||||
|             "version": "v1.9.0", | ||||
|             "source": { | ||||
|                 "type": "git", | ||||
|                 "url": "https://github.com/symfony/polyfill-mbstring.git", | ||||
|                 "reference": "3296adf6a6454a050679cde90f95350ad604b171" | ||||
|                 "reference": "d0cd638f4634c16d8df4508e847f14e9e43168b8" | ||||
|             }, | ||||
|             "dist": { | ||||
|                 "type": "zip", | ||||
|                 "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/3296adf6a6454a050679cde90f95350ad604b171", | ||||
|                 "reference": "3296adf6a6454a050679cde90f95350ad604b171", | ||||
|                 "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/d0cd638f4634c16d8df4508e847f14e9e43168b8", | ||||
|                 "reference": "d0cd638f4634c16d8df4508e847f14e9e43168b8", | ||||
|                 "shasum": "" | ||||
|             }, | ||||
|             "require": { | ||||
| @@ -1340,7 +1293,7 @@ | ||||
|             "type": "library", | ||||
|             "extra": { | ||||
|                 "branch-alias": { | ||||
|                     "dev-master": "1.8-dev" | ||||
|                     "dev-master": "1.9-dev" | ||||
|                 } | ||||
|             }, | ||||
|             "autoload": { | ||||
| @@ -1374,20 +1327,20 @@ | ||||
|                 "portable", | ||||
|                 "shim" | ||||
|             ], | ||||
|             "time": "2018-04-26T10:06:28+00:00" | ||||
|             "time": "2018-08-06T14:22:27+00:00" | ||||
|         }, | ||||
|         { | ||||
|             "name": "symfony/translation", | ||||
|             "version": "v4.1.1", | ||||
|             "version": "v4.1.4", | ||||
|             "source": { | ||||
|                 "type": "git", | ||||
|                 "url": "https://github.com/symfony/translation.git", | ||||
|                 "reference": "b6d8164085ee0b6debcd1b7a131fd6f63bb04854" | ||||
|                 "reference": "fa2182669f7983b7aa5f1a770d053f79f0ef144f" | ||||
|             }, | ||||
|             "dist": { | ||||
|                 "type": "zip", | ||||
|                 "url": "https://api.github.com/repos/symfony/translation/zipball/b6d8164085ee0b6debcd1b7a131fd6f63bb04854", | ||||
|                 "reference": "b6d8164085ee0b6debcd1b7a131fd6f63bb04854", | ||||
|                 "url": "https://api.github.com/repos/symfony/translation/zipball/fa2182669f7983b7aa5f1a770d053f79f0ef144f", | ||||
|                 "reference": "fa2182669f7983b7aa5f1a770d053f79f0ef144f", | ||||
|                 "shasum": "" | ||||
|             }, | ||||
|             "require": { | ||||
| @@ -1443,7 +1396,7 @@ | ||||
|             ], | ||||
|             "description": "Symfony Translation Component", | ||||
|             "homepage": "https://symfony.com", | ||||
|             "time": "2018-06-22T08:59:39+00:00" | ||||
|             "time": "2018-08-07T12:45:11+00:00" | ||||
|         }, | ||||
|         { | ||||
|             "name": "tuupola/callable-handler", | ||||
|   | ||||
| @@ -1,16 +1,17 @@ | ||||
| <?php | ||||
|  | ||||
| # Login credentials | ||||
| Setting('HTTP_USER', 'admin'); | ||||
| Setting('HTTP_PASSWORD', 'admin'); | ||||
|  | ||||
| # Either "production" or "dev" | ||||
| # Either "production", "dev" or "prerelease" | ||||
| Setting('MODE', 'production'); | ||||
|  | ||||
| # Either "en" or "de" or the filename (without extension) of | ||||
| # one of the other available localization files in the "/localization" directory | ||||
| Setting('CULTURE', 'en'); | ||||
|  | ||||
| # To keep it simpel, grocy does not handle any currency conversions, | ||||
| # this here is used to format all money values, | ||||
| # so can be anything (e. g. "USD" OR "$", doesn't matter...) | ||||
| Setting('CURRENCY', '$'); | ||||
|  | ||||
| # The base url of your installation, | ||||
| # should be just "/" when running directly under the root of a (sub)domain | ||||
| # or for example "https:/example.com/grocy" when using a subdirectory | ||||
|   | ||||
| @@ -11,13 +11,26 @@ class BaseController | ||||
| 	public function __construct(\Slim\Container $container) { | ||||
| 		$databaseService = new DatabaseService(); | ||||
| 		$this->Database = $databaseService->GetDbConnection(); | ||||
| 		 | ||||
| 		$localizationService = new LocalizationService(GROCY_CULTURE); | ||||
| 		$this->LocalizationService = $localizationService; | ||||
|  | ||||
| 		$applicationService = new ApplicationService(); | ||||
| 		$versionInfo = $applicationService->GetInstalledVersion(); | ||||
| 		$container->view->set('version', $versionInfo->Version); | ||||
| 		$container->view->set('releaseDate', $versionInfo->ReleaseDate); | ||||
| 		if (GROCY_MODE === 'prerelease') | ||||
| 		{ | ||||
| 			$commitHash = trim(exec('git log --pretty="%h" -n1 HEAD')); | ||||
| 			$commitDate = trim(exec('git log --date=iso --pretty="%cd" -n1 HEAD')); | ||||
| 			 | ||||
| 			$container->view->set('version', "pre-release-$commitHash"); | ||||
| 			$container->view->set('releaseDate', \substr($commitDate, 0, 19)); | ||||
| 		} | ||||
| 		else | ||||
| 		{ | ||||
| 			$applicationService = new ApplicationService(); | ||||
| 			$versionInfo = $applicationService->GetInstalledVersion(); | ||||
| 			$container->view->set('version', $versionInfo->Version); | ||||
| 			$container->view->set('releaseDate', $versionInfo->ReleaseDate); | ||||
| 		} | ||||
|  | ||||
| 		$localizationService = new LocalizationService(CULTURE); | ||||
| 		$container->view->set('localizationStrings', $localizationService->GetCurrentCultureLocalizations()); | ||||
| 		$container->view->set('L', function($text, ...$placeholderValues) use($localizationService) | ||||
| 		{ | ||||
| @@ -33,4 +46,5 @@ class BaseController | ||||
|  | ||||
| 	protected $AppContainer; | ||||
| 	protected $Database; | ||||
| 	protected $LocalizationService; | ||||
| } | ||||
|   | ||||
| @@ -44,4 +44,9 @@ class BatteriesApiController extends BaseApiController | ||||
| 			return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage()); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	public function Current(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		return $this->ApiResponse($this->BatteriesService->GetCurrent()); | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -16,22 +16,10 @@ class BatteriesController extends BaseController | ||||
|  | ||||
| 	public function Overview(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		$nextChargeTimes = array(); | ||||
| 		foreach($this->Database->batteries() as $battery) | ||||
| 		{ | ||||
| 			$nextChargeTimes[$battery->id] = $this->BatteriesService->GetNextChargeTime($battery->id); | ||||
| 		} | ||||
|  | ||||
| 		$nextXDays = 5; | ||||
| 		$countDueNextXDays = count(FindAllItemsInArrayByValue($nextChargeTimes, date('Y-m-d', strtotime("+$nextXDays days")), '<')); | ||||
| 		$countOverdue = count(FindAllItemsInArrayByValue($nextChargeTimes, date('Y-m-d', strtotime('-1 days')), '<')); | ||||
| 		return $this->AppContainer->view->render($response, 'batteriesoverview', [ | ||||
| 			'batteries' => $this->Database->batteries()->orderBy('name'), | ||||
| 			'current' => $this->BatteriesService->GetCurrent(), | ||||
| 			'nextChargeTimes' => $nextChargeTimes, | ||||
| 			'nextXDays' => $nextXDays, | ||||
| 			'countDueNextXDays' => $countDueNextXDays - $countOverdue, | ||||
| 			'countOverdue' => $countOverdue | ||||
| 			'nextXDays' => 5 | ||||
| 		]); | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -2,19 +2,19 @@ | ||||
| 
 | ||||
| namespace Grocy\Controllers; | ||||
| 
 | ||||
| use \Grocy\Services\HabitsService; | ||||
| use \Grocy\Services\ChoresService; | ||||
| 
 | ||||
| class HabitsApiController extends BaseApiController | ||||
| class ChoresApiController extends BaseApiController | ||||
| { | ||||
| 	public function __construct(\Slim\Container $container) | ||||
| 	{ | ||||
| 		parent::__construct($container); | ||||
| 		$this->HabitsService = new HabitsService(); | ||||
| 		$this->ChoresService = new ChoresService(); | ||||
| 	} | ||||
| 
 | ||||
| 	protected $HabitsService; | ||||
| 	protected $ChoresService; | ||||
| 
 | ||||
| 	public function TrackHabitExecution(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	public function TrackChoreExecution(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		$trackedTime = date('Y-m-d H:i:s'); | ||||
| 		if (isset($request->getQueryParams()['tracked_time']) && !empty($request->getQueryParams()['tracked_time']) && IsIsoDateTime($request->getQueryParams()['tracked_time'])) | ||||
| @@ -22,9 +22,15 @@ class HabitsApiController extends BaseApiController | ||||
| 			$trackedTime = $request->getQueryParams()['tracked_time']; | ||||
| 		} | ||||
| 
 | ||||
| 		$doneBy = GROCY_USER_ID; | ||||
| 		if (isset($request->getQueryParams()['done_by']) && !empty($request->getQueryParams()['done_by'])) | ||||
| 		{ | ||||
| 			$doneBy = $request->getQueryParams()['done_by']; | ||||
| 		} | ||||
| 
 | ||||
| 		try | ||||
| 		{ | ||||
| 			$this->HabitsService->TrackHabit($args['habitId'], $trackedTime); | ||||
| 			$this->ChoresService->TrackChore($args['choreId'], $trackedTime, $doneBy); | ||||
| 			return $this->VoidApiActionResponse($response); | ||||
| 		} | ||||
| 		catch (\Exception $ex) | ||||
| @@ -33,15 +39,20 @@ class HabitsApiController extends BaseApiController | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	public function HabitDetails(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	public function ChoreDetails(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		try | ||||
| 		{ | ||||
| 			return $this->ApiResponse($this->HabitsService->GetHabitDetails($args['habitId'])); | ||||
| 			return $this->ApiResponse($this->ChoresService->GetChoreDetails($args['choreId'])); | ||||
| 		} | ||||
| 		catch (\Exception $ex) | ||||
| 		{ | ||||
| 			return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage()); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	public function Current(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		return $this->ApiResponse($this->ChoresService->GetCurrent()); | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										68
									
								
								controllers/ChoresController.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								controllers/ChoresController.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,68 @@ | ||||
| <?php | ||||
|  | ||||
| namespace Grocy\Controllers; | ||||
|  | ||||
| use \Grocy\Services\ChoresService; | ||||
|  | ||||
| class ChoresController extends BaseController | ||||
| { | ||||
| 	public function __construct(\Slim\Container $container) | ||||
| 	{ | ||||
| 		parent::__construct($container); | ||||
| 		$this->ChoresService = new ChoresService(); | ||||
| 	} | ||||
|  | ||||
| 	protected $ChoresService; | ||||
|  | ||||
| 	public function Overview(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		return $this->AppContainer->view->render($response, 'choresoverview', [ | ||||
| 			'chores' => $this->Database->chores()->orderBy('name'), | ||||
| 			'currentChores' => $this->ChoresService->GetCurrent(), | ||||
| 			'nextXDays' => 5 | ||||
| 		]); | ||||
| 	} | ||||
|  | ||||
| 	public function TrackChoreExecution(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		return $this->AppContainer->view->render($response, 'choretracking', [ | ||||
| 			'chores' => $this->Database->chores()->orderBy('name'), | ||||
| 			'users' => $this->Database->users()->orderBy('username') | ||||
| 		]); | ||||
| 	} | ||||
|  | ||||
| 	public function ChoresList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		return $this->AppContainer->view->render($response, 'chores', [ | ||||
| 			'chores' => $this->Database->chores()->orderBy('name') | ||||
| 		]); | ||||
| 	} | ||||
|  | ||||
| 	public function Analysis(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		return $this->AppContainer->view->render($response, 'choresanalysis', [ | ||||
| 			'choresLog' => $this->Database->chores_log()->orderBy('tracked_time', 'DESC'), | ||||
| 			'chores' => $this->Database->chores()->orderBy('name'), | ||||
| 			'users' => $this->Database->users()->orderBy('username') | ||||
| 		]); | ||||
| 	} | ||||
|  | ||||
| 	public function ChoreEditForm(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		if ($args['choreId'] == 'new') | ||||
| 		{ | ||||
| 			return $this->AppContainer->view->render($response, 'choreform', [ | ||||
| 				'periodTypes' => GetClassConstants('\Grocy\Services\ChoresService'), | ||||
| 				'mode' => 'create' | ||||
| 			]); | ||||
| 		} | ||||
| 		else | ||||
| 		{ | ||||
| 			return $this->AppContainer->view->render($response, 'choreform', [ | ||||
| 				'chore' =>  $this->Database->chores($args['choreId']), | ||||
| 				'periodTypes' => GetClassConstants('\Grocy\Services\ChoresService'), | ||||
| 				'mode' => 'edit' | ||||
| 			]); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -1,19 +0,0 @@ | ||||
| <?php | ||||
|  | ||||
| namespace Grocy\Controllers; | ||||
|  | ||||
| use \Grocy\Services\ApplicationService; | ||||
| use \Grocy\Services\DatabaseMigrationService; | ||||
|  | ||||
| class CliController extends BaseController | ||||
| { | ||||
| 	public function RecreateDemo(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		$applicationService = new ApplicationService(); | ||||
| 		if ($applicationService->IsDemoInstallation()) | ||||
| 		{ | ||||
| 			$databaseMigrationService = new DatabaseMigrationService(); | ||||
| 			$databaseMigrationService->RecreateDemo(); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										38
									
								
								controllers/FilesApiController.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								controllers/FilesApiController.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | ||||
| <?php | ||||
|  | ||||
| namespace Grocy\Controllers; | ||||
|  | ||||
| use \Grocy\Services\FilesService; | ||||
|  | ||||
| class FilesApiController extends BaseApiController | ||||
| { | ||||
| 	public function __construct(\Slim\Container $container) | ||||
| 	{ | ||||
| 		parent::__construct($container); | ||||
| 		$this->FilesService = new FilesService(); | ||||
| 	} | ||||
|  | ||||
| 	protected $FilesService; | ||||
|  | ||||
| 	public function Upload(\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'); | ||||
| 			} | ||||
|  | ||||
| 			$data = $request->getBody()->getContents(); | ||||
| 			file_put_contents($this->FilesService->GetFilePath($args['group'], $fileName), $data); | ||||
| 		} | ||||
| 		catch (\Exception $ex) | ||||
| 		{ | ||||
| 			return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage()); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -1,70 +0,0 @@ | ||||
| <?php | ||||
|  | ||||
| namespace Grocy\Controllers; | ||||
|  | ||||
| use \Grocy\Services\HabitsService; | ||||
|  | ||||
| class HabitsController extends BaseController | ||||
| { | ||||
| 	public function __construct(\Slim\Container $container) | ||||
| 	{ | ||||
| 		parent::__construct($container); | ||||
| 		$this->HabitsService = new HabitsService(); | ||||
| 	} | ||||
|  | ||||
| 	protected $HabitsService; | ||||
|  | ||||
| 	public function Overview(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		$nextHabitTimes = array(); | ||||
| 		foreach($this->Database->habits() as $habit) | ||||
| 		{ | ||||
| 			$nextHabitTimes[$habit->id] = $this->HabitsService->GetNextHabitTime($habit->id); | ||||
| 		} | ||||
|  | ||||
| 		$nextXDays = 5; | ||||
| 		$countDueNextXDays = count(FindAllItemsInArrayByValue($nextHabitTimes, date('Y-m-d', strtotime("+$nextXDays days")), '<')); | ||||
| 		$countOverdue = count(FindAllItemsInArrayByValue($nextHabitTimes, date('Y-m-d', strtotime('-1 days')), '<')); | ||||
| 		return $this->AppContainer->view->render($response, 'habitsoverview', [ | ||||
| 			'habits' => $this->Database->habits()->orderBy('name'), | ||||
| 			'currentHabits' => $this->HabitsService->GetCurrentHabits(), | ||||
| 			'nextHabitTimes' => $nextHabitTimes, | ||||
| 			'nextXDays' => $nextXDays, | ||||
| 			'countDueNextXDays' => $countDueNextXDays - $countOverdue, | ||||
| 			'countOverdue' => $countOverdue | ||||
| 		]); | ||||
| 	} | ||||
|  | ||||
| 	public function TrackHabitExecution(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		return $this->AppContainer->view->render($response, 'habittracking', [ | ||||
| 			'habits' => $this->Database->habits()->orderBy('name') | ||||
| 		]); | ||||
| 	} | ||||
|  | ||||
| 	public function HabitsList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		return $this->AppContainer->view->render($response, 'habits', [ | ||||
| 			'habits' => $this->Database->habits()->orderBy('name') | ||||
| 		]); | ||||
| 	} | ||||
|  | ||||
| 	public function HabitEditForm(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		if ($args['habitId'] == 'new') | ||||
| 		{ | ||||
| 			return $this->AppContainer->view->render($response, 'habitform', [ | ||||
| 				'periodTypes' => GetClassConstants('\Grocy\Services\HabitsService'), | ||||
| 				'mode' => 'create' | ||||
| 			]); | ||||
| 		} | ||||
| 		else | ||||
| 		{ | ||||
| 			return $this->AppContainer->view->render($response, 'habitform', [ | ||||
| 				'habit' =>  $this->Database->habits($args['habitId']), | ||||
| 				'periodTypes' => GetClassConstants('\Grocy\Services\HabitsService'), | ||||
| 				'mode' => 'edit' | ||||
| 			]); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -3,7 +3,6 @@ | ||||
| namespace Grocy\Controllers; | ||||
|  | ||||
| use \Grocy\Services\SessionService; | ||||
| use \Grocy\Services\ApplicationService; | ||||
| use \Grocy\Services\DatabaseMigrationService; | ||||
| use \Grocy\Services\DemoDataGeneratorService; | ||||
|  | ||||
| @@ -24,10 +23,21 @@ class LoginController extends BaseController | ||||
| 		$postParams = $request->getParsedBody(); | ||||
| 		if (isset($postParams['username']) && isset($postParams['password'])) | ||||
| 		{ | ||||
| 			if ($postParams['username'] === HTTP_USER && $postParams['password'] === HTTP_PASSWORD) | ||||
| 			$user = $this->Database->users()->where('username', $postParams['username'])->fetch(); | ||||
| 			$inputPassword = $postParams['password']; | ||||
| 			$stayLoggedInPermanently = $postParams['stay_logged_in'] == 'on'; | ||||
|  | ||||
| 			if ($user !== null && password_verify($inputPassword, $user->password)) | ||||
| 			{ | ||||
| 				$sessionKey = $this->SessionService->CreateSession(); | ||||
| 				setcookie($this->SessionCookieName, $sessionKey, time() + 31536000); // Cookie expires in 1 year, but session validity is up to SessionService | ||||
| 				$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 | ||||
|  | ||||
| 				if (password_needs_rehash($user->password, PASSWORD_DEFAULT)) | ||||
| 				{ | ||||
| 					$user->update(array( | ||||
| 						'password' => password_hash($inputPassword, PASSWORD_DEFAULT) | ||||
| 					)); | ||||
| 				} | ||||
|  | ||||
| 				return $response->withRedirect($this->AppContainer->UrlManager->ConstructUrl('/')); | ||||
| 			} | ||||
| @@ -59,8 +69,7 @@ class LoginController extends BaseController | ||||
| 		$databaseMigrationService = new DatabaseMigrationService(); | ||||
| 		$databaseMigrationService->MigrateDatabase(); | ||||
|  | ||||
| 		$applicationService = new ApplicationService(); | ||||
| 		if ($applicationService->IsDemoInstallation()) | ||||
| 		if (GROCY_IS_DEMO_INSTALL) | ||||
| 		{ | ||||
| 			$demoDataGeneratorService = new DemoDataGeneratorService(); | ||||
| 			$demoDataGeneratorService->PopulateDemoData(); | ||||
|   | ||||
| @@ -35,7 +35,8 @@ class OpenApiController extends BaseApiController | ||||
| 	public function ApiKeysList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		return $this->AppContainer->view->render($response, 'manageapikeys', [ | ||||
| 			'apiKeys' => $this->Database->api_keys() | ||||
| 			'apiKeys' => $this->Database->api_keys(), | ||||
| 			'users' => $this->Database->users() | ||||
| 		]); | ||||
| 	} | ||||
|  | ||||
|   | ||||
							
								
								
									
										35
									
								
								controllers/RecipesApiController.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								controllers/RecipesApiController.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | ||||
| <?php | ||||
|  | ||||
| namespace Grocy\Controllers; | ||||
|  | ||||
| use \Grocy\Services\RecipesService; | ||||
|  | ||||
| class RecipesApiController extends BaseApiController | ||||
| { | ||||
| 	public function __construct(\Slim\Container $container) | ||||
| 	{ | ||||
| 		parent::__construct($container); | ||||
| 		$this->RecipesService = new RecipesService(); | ||||
| 	} | ||||
|  | ||||
| 	protected $RecipesService; | ||||
|  | ||||
| 	public function AddNotFulfilledProductsToShoppingList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		$this->RecipesService->AddNotFulfilledProductsToShoppingList($args['recipeId']); | ||||
| 		return $this->VoidApiActionResponse($response); | ||||
| 	} | ||||
|  | ||||
| 	public function ConsumeRecipe(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		try | ||||
| 		{ | ||||
| 			$this->RecipesService->ConsumeRecipe($args['recipeId']); | ||||
| 			return $this->VoidApiActionResponse($response); | ||||
| 		} | ||||
| 		catch (\Exception $ex) | ||||
| 		{ | ||||
| 			return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage()); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										95
									
								
								controllers/RecipesController.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								controllers/RecipesController.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,95 @@ | ||||
| <?php | ||||
|  | ||||
| namespace Grocy\Controllers; | ||||
|  | ||||
| use \Grocy\Services\RecipesService; | ||||
|  | ||||
| class RecipesController extends BaseController | ||||
| { | ||||
| 	public function __construct(\Slim\Container $container) | ||||
| 	{ | ||||
| 		parent::__construct($container); | ||||
| 		$this->RecipesService = new RecipesService(); | ||||
| 	} | ||||
|  | ||||
| 	protected $RecipesService; | ||||
|  | ||||
| 	public function Overview(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		$recipes = $this->Database->recipes()->orderBy('name'); | ||||
|  | ||||
| 		$selectedRecipe = null; | ||||
| 		$selectedRecipePositions = null; | ||||
| 		if (isset($request->getQueryParams()['recipe'])) | ||||
| 		{ | ||||
| 			$selectedRecipe = $this->Database->recipes($request->getQueryParams()['recipe']); | ||||
| 			$selectedRecipePositions = $this->Database->recipes_pos()->where('recipe_id', $request->getQueryParams()['recipe']); | ||||
| 		} | ||||
| 		else | ||||
| 		{ | ||||
| 			foreach ($recipes as $recipe) | ||||
| 			{ | ||||
| 				$selectedRecipe = $recipe; | ||||
| 				$selectedRecipePositions = $this->Database->recipes_pos()->where('recipe_id', $recipe->id); | ||||
| 				break; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		return $this->AppContainer->view->render($response, 'recipes', [ | ||||
| 			'recipes' => $recipes, | ||||
| 			'recipesFulfillment' => $this->RecipesService->GetRecipesFulfillment(), | ||||
| 			'recipesSumFulfillment' => $this->RecipesService->GetRecipesSumFulfillment(), | ||||
| 			'selectedRecipe' => $selectedRecipe, | ||||
| 			'selectedRecipePositions' => $selectedRecipePositions, | ||||
| 			'products' => $this->Database->products(), | ||||
| 			'quantityunits' => $this->Database->quantity_units() | ||||
| 		]); | ||||
| 	} | ||||
|  | ||||
| 	public function RecipeEditForm(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		$recipeId = $args['recipeId']; | ||||
| 		if ($recipeId  == 'new') | ||||
| 		{ | ||||
| 			$newRecipe = $this->Database->recipes()->createRow(array( | ||||
| 				'name' => $this->LocalizationService->Localize('New recipe') | ||||
| 			)); | ||||
| 			$newRecipe->save(); | ||||
|  | ||||
| 			$recipeId = $this->Database->lastInsertId(); | ||||
| 		} | ||||
| 		 | ||||
| 		return $this->AppContainer->view->render($response, 'recipeform', [ | ||||
| 			'recipe' =>  $this->Database->recipes($recipeId), | ||||
| 			'recipePositions' =>  $this->Database->recipes_pos()->where('recipe_id', $recipeId), | ||||
| 			'mode' => 'edit', | ||||
| 			'products' => $this->Database->products(), | ||||
| 			'quantityunits' => $this->Database->quantity_units(), | ||||
| 			'recipesFulfillment' => $this->RecipesService->GetRecipesFulfillment(), | ||||
| 			'recipesSumFulfillment' => $this->RecipesService->GetRecipesSumFulfillment() | ||||
| 		]); | ||||
| 	} | ||||
|  | ||||
| 	public function RecipePosEditForm(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		if ($args['recipePosId'] == 'new') | ||||
| 		{ | ||||
| 			return $this->AppContainer->view->render($response, 'recipeposform', [ | ||||
| 				'mode' => 'create', | ||||
| 				'recipe' => $this->Database->recipes($args['recipeId']), | ||||
| 				'products' => $this->Database->products()->orderBy('name'), | ||||
| 				'quantityUnits' => $this->Database->quantity_units()->orderBy('name') | ||||
| 			]); | ||||
| 		} | ||||
| 		else | ||||
| 		{ | ||||
| 			return $this->AppContainer->view->render($response, 'recipeposform', [ | ||||
| 				'mode' => 'edit', | ||||
| 				'recipe' =>  $this->Database->recipes($args['recipeId']), | ||||
| 				'recipePos' => $this->Database->recipes_pos($args['recipePosId']), | ||||
| 				'products' => $this->Database->products()->orderBy('name'), | ||||
| 				'quantityUnits' => $this->Database->quantity_units()->orderBy('name') | ||||
| 			]); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -26,6 +26,18 @@ class StockApiController extends BaseApiController | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	public function ProductPriceHistory(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		try | ||||
| 		{ | ||||
| 			return $this->ApiResponse($this->StockService->GetProductPriceHistory($args['productId'])); | ||||
| 		} | ||||
| 		catch (\Exception $ex) | ||||
| 		{ | ||||
| 			return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage()); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	public function AddProduct(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		$bestBeforeDate = date('Y-m-d'); | ||||
| @@ -34,6 +46,12 @@ class StockApiController extends BaseApiController | ||||
| 			$bestBeforeDate = $request->getQueryParams()['bestbeforedate']; | ||||
| 		} | ||||
|  | ||||
| 		$price = null; | ||||
| 		if (isset($request->getQueryParams()['price']) && !empty($request->getQueryParams()['price']) && is_numeric($request->getQueryParams()['price'])) | ||||
| 		{ | ||||
| 			$price = $request->getQueryParams()['price']; | ||||
| 		} | ||||
|  | ||||
| 		$transactionType = StockService::TRANSACTION_TYPE_PURCHASE; | ||||
| 		if (isset($request->getQueryParams()['transactiontype']) && !empty($request->getQueryParams()['transactiontype'])) | ||||
| 		{ | ||||
| @@ -42,7 +60,7 @@ class StockApiController extends BaseApiController | ||||
|  | ||||
| 		try | ||||
| 		{ | ||||
| 			$this->StockService->AddProduct($args['productId'], $args['amount'], $bestBeforeDate, $transactionType); | ||||
| 			$this->StockService->AddProduct($args['productId'], $args['amount'], $bestBeforeDate, $transactionType, date('Y-m-d'), $price); | ||||
| 			return $this->VoidApiActionResponse($response); | ||||
| 		} | ||||
| 		catch (\Exception $ex) | ||||
| @@ -100,12 +118,36 @@ class StockApiController extends BaseApiController | ||||
| 		return $this->ApiResponse($this->StockService->GetCurrentStock()); | ||||
| 	} | ||||
|  | ||||
| 	public function CurrentVolatilStock(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		$nextXDays = 5; | ||||
| 		if (isset($request->getQueryParams()['expiring_days']) && !empty($request->getQueryParams()['expiring_days']) && is_numeric($request->getQueryParams()['expiring_days'])) | ||||
| 		{ | ||||
| 			$nextXDays = $request->getQueryParams()['expiring_days']; | ||||
| 		} | ||||
|  | ||||
| 		$expiringProducts = $this->StockService->GetExpiringProducts($nextXDays); | ||||
| 		$expiredProducts = $this->StockService->GetExpiringProducts(-1); | ||||
| 		$missingProducts = $this->StockService->GetMissingProducts(); | ||||
| 		return $this->ApiResponse(array( | ||||
| 			 'expiring_products' => $expiringProducts, | ||||
| 			 'expired_products' => $expiredProducts, | ||||
| 			 'missing_products' => $missingProducts | ||||
| 		)); | ||||
| 	} | ||||
|  | ||||
| 	public function AddMissingProductsToShoppingList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		$this->StockService->AddMissingProductsToShoppingList(); | ||||
| 		return $this->VoidApiActionResponse($response); | ||||
| 	} | ||||
|  | ||||
| 	public function ClearShoppingList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		$this->StockService->ClearShoppingList(); | ||||
| 		return $this->VoidApiActionResponse($response); | ||||
| 	} | ||||
|  | ||||
| 	public function ExternalBarcodeLookup(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		try | ||||
|   | ||||
| @@ -17,19 +17,13 @@ class StockController extends BaseController | ||||
|  | ||||
| 	public function Overview(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		$currentStock = $this->StockService->GetCurrentStock(); | ||||
| 		$nextXDays = 5; | ||||
| 		$countExpiringNextXDays = count(FindAllObjectsInArrayByPropertyValue($currentStock, 'best_before_date', date('Y-m-d', strtotime('+5 days')), '<')); | ||||
| 		$countAlreadyExpired = count(FindAllObjectsInArrayByPropertyValue($currentStock, 'best_before_date', date('Y-m-d', strtotime('-1 days')), '<')); | ||||
| 		return $this->AppContainer->view->render($response, 'stockoverview', [ | ||||
| 			'products' => $this->Database->products()->orderBy('name'), | ||||
| 			'quantityunits' => $this->Database->quantity_units()->orderBy('name'), | ||||
| 			'locations' => $this->Database->locations()->orderBy('name'), | ||||
| 			'currentStock' => $currentStock, | ||||
| 			'currentStock' => $this->StockService->GetCurrentStock(), | ||||
| 			'missingProducts' => $this->StockService->GetMissingProducts(), | ||||
| 			'nextXDays' => $nextXDays, | ||||
| 			'countExpiringNextXDays' => $countExpiringNextXDays, | ||||
| 			'countAlreadyExpired' => $countAlreadyExpired | ||||
| 			'nextXDays' => 5 | ||||
| 		]); | ||||
| 	} | ||||
|  | ||||
| @@ -60,7 +54,8 @@ class StockController extends BaseController | ||||
| 			'listItems' => $this->Database->shopping_list(), | ||||
| 			'products' => $this->Database->products()->orderBy('name'), | ||||
| 			'quantityunits' => $this->Database->quantity_units()->orderBy('name'), | ||||
| 			'missingProducts' => $this->StockService->GetMissingProducts() | ||||
| 			'missingProducts' => $this->StockService->GetMissingProducts(), | ||||
| 			'productGroups' => $this->Database->product_groups()->orderBy('name') | ||||
| 		]); | ||||
| 	} | ||||
|  | ||||
| @@ -69,7 +64,8 @@ class StockController extends BaseController | ||||
| 		return $this->AppContainer->view->render($response, 'products', [ | ||||
| 			'products' => $this->Database->products()->orderBy('name'), | ||||
| 			'locations' => $this->Database->locations()->orderBy('name'), | ||||
| 			'quantityunits' => $this->Database->quantity_units()->orderBy('name') | ||||
| 			'quantityunits' => $this->Database->quantity_units()->orderBy('name'), | ||||
| 			'productGroups' => $this->Database->product_groups()->orderBy('name') | ||||
| 		]); | ||||
| 	} | ||||
|  | ||||
| @@ -80,6 +76,13 @@ class StockController extends BaseController | ||||
| 		]); | ||||
| 	} | ||||
|  | ||||
| 	public function ProductGroupsList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		return $this->AppContainer->view->render($response, 'productgroups', [ | ||||
| 			'productGroups' => $this->Database->product_groups()->orderBy('name') | ||||
| 		]); | ||||
| 	} | ||||
|  | ||||
| 	public function QuantityUnitsList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		return $this->AppContainer->view->render($response, 'quantityunits', [ | ||||
| @@ -94,6 +97,7 @@ class StockController extends BaseController | ||||
| 			return $this->AppContainer->view->render($response, 'productform', [ | ||||
| 				'locations' =>  $this->Database->locations()->orderBy('name'), | ||||
| 				'quantityunits' =>  $this->Database->quantity_units()->orderBy('name'), | ||||
| 				'productgroups' => $this->Database->product_groups()->orderBy('name'), | ||||
| 				'mode' => 'create' | ||||
| 			]); | ||||
| 		} | ||||
| @@ -103,6 +107,7 @@ class StockController extends BaseController | ||||
| 				'product' =>  $this->Database->products($args['productId']), | ||||
| 				'locations' =>  $this->Database->locations()->orderBy('name'), | ||||
| 				'quantityunits' =>  $this->Database->quantity_units()->orderBy('name'), | ||||
| 				'productgroups' => $this->Database->product_groups()->orderBy('name'), | ||||
| 				'mode' => 'edit' | ||||
| 			]); | ||||
| 		} | ||||
| @@ -125,6 +130,23 @@ class StockController extends BaseController | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	public function ProductGroupEditForm(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		if ($args['productGroupId'] == 'new') | ||||
| 		{ | ||||
| 			return $this->AppContainer->view->render($response, 'productgroupform', [ | ||||
| 				'mode' => 'create' | ||||
| 			]); | ||||
| 		} | ||||
| 		else | ||||
| 		{ | ||||
| 			return $this->AppContainer->view->render($response, 'productgroupform', [ | ||||
| 				'group' =>  $this->Database->product_groups($args['productGroupId']), | ||||
| 				'mode' => 'edit' | ||||
| 			]); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	public function QuantityUnitEditForm(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		if ($args['quantityunitId'] == 'new') | ||||
|   | ||||
							
								
								
									
										23
									
								
								controllers/SystemApiController.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								controllers/SystemApiController.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| <?php | ||||
|  | ||||
| namespace Grocy\Controllers; | ||||
|  | ||||
| use \Grocy\Services\DatabaseService; | ||||
|  | ||||
| class SystemApiController extends BaseApiController | ||||
| { | ||||
| 	public function __construct(\Slim\Container $container) | ||||
| 	{ | ||||
| 		parent::__construct($container); | ||||
| 		$this->DatabaseService = new DatabaseService(); | ||||
| 	} | ||||
|  | ||||
| 	protected $DatabaseService; | ||||
|  | ||||
| 	public function GetDbChangedTime(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		return $this->ApiResponse(array( | ||||
| 			'changed_time' => $this->DatabaseService->GetDbChangedTime() | ||||
| 		)); | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										40
									
								
								controllers/TasksApiController.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								controllers/TasksApiController.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | ||||
| <?php | ||||
|  | ||||
| namespace Grocy\Controllers; | ||||
|  | ||||
| use \Grocy\Services\TasksService; | ||||
|  | ||||
| class TasksApiController extends BaseApiController | ||||
| { | ||||
| 	public function __construct(\Slim\Container $container) | ||||
| 	{ | ||||
| 		parent::__construct($container); | ||||
| 		$this->TasksService = new TasksService(); | ||||
| 	} | ||||
|  | ||||
| 	protected $TasksService; | ||||
|  | ||||
| 	public function Current(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		return $this->ApiResponse($this->TasksService->GetCurrent()); | ||||
| 	} | ||||
|  | ||||
| 	public function MarkTaskAsCompleted(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		$doneTime = date('Y-m-d H:i:s'); | ||||
| 		if (isset($request->getQueryParams()['done_time']) && !empty($request->getQueryParams()['done_time']) && IsIsoDateTime($request->getQueryParams()['done_time'])) | ||||
| 		{ | ||||
| 			$doneTime = $request->getQueryParams()['done_time']; | ||||
| 		} | ||||
|  | ||||
| 		try | ||||
| 		{ | ||||
| 			$this->TasksService->MarkTaskAsCompleted($args['taskId'], $doneTime); | ||||
| 			return $this->VoidApiActionResponse($response); | ||||
| 		} | ||||
| 		catch (\Exception $ex) | ||||
| 		{ | ||||
| 			return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage()); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										80
									
								
								controllers/TasksController.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								controllers/TasksController.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,80 @@ | ||||
| <?php | ||||
|  | ||||
| namespace Grocy\Controllers; | ||||
|  | ||||
| use \Grocy\Services\TasksService; | ||||
|  | ||||
| class TasksController extends BaseController | ||||
| { | ||||
| 	public function __construct(\Slim\Container $container) | ||||
| 	{ | ||||
| 		parent::__construct($container); | ||||
| 		$this->TasksService = new TasksService(); | ||||
| 	} | ||||
|  | ||||
| 	protected $TasksService; | ||||
|  | ||||
| 	public function Overview(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		if (isset($request->getQueryParams()['include_done'])) | ||||
| 		{ | ||||
| 			$tasks = $this->Database->tasks()->orderBy('name'); | ||||
| 		} | ||||
| 		else | ||||
| 		{ | ||||
| 			$tasks = $this->TasksService->GetCurrent(); | ||||
| 		} | ||||
|  | ||||
| 		return $this->AppContainer->view->render($response, 'tasks', [ | ||||
| 			'tasks' => $tasks, | ||||
| 			'nextXDays' => 5, | ||||
| 			'taskCategories' => $this->Database->task_categories()->orderBy('name'), | ||||
| 			'users' => $this->Database->users() | ||||
| 		]); | ||||
| 	} | ||||
|  | ||||
| 	public function TaskEditForm(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		if ($args['taskId'] == 'new') | ||||
| 		{ | ||||
| 			return $this->AppContainer->view->render($response, 'taskform', [ | ||||
| 				'mode' => 'create', | ||||
| 				'taskCategories' => $this->Database->task_categories()->orderBy('name'), | ||||
| 				'users' => $this->Database->users()->orderBy('username') | ||||
| 			]); | ||||
| 		} | ||||
| 		else | ||||
| 		{ | ||||
| 			return $this->AppContainer->view->render($response, 'taskform', [ | ||||
| 				'task' =>  $this->Database->tasks($args['taskId']), | ||||
| 				'mode' => 'edit', | ||||
| 				'taskCategories' => $this->Database->task_categories()->orderBy('name'), | ||||
| 				'users' => $this->Database->users()->orderBy('username') | ||||
| 			]); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	public function TaskCategoriesList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		return $this->AppContainer->view->render($response, 'taskcategories', [ | ||||
| 			'taskCategories' => $this->Database->task_categories()->orderBy('name') | ||||
| 		]); | ||||
| 	} | ||||
|  | ||||
| 	public function TaskCategoryEditForm(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		if ($args['categoryId'] == 'new') | ||||
| 		{ | ||||
| 			return $this->AppContainer->view->render($response, 'taskcategoryform', [ | ||||
| 				'mode' => 'create' | ||||
| 			]); | ||||
| 		} | ||||
| 		else | ||||
| 		{ | ||||
| 			return $this->AppContainer->view->render($response, 'taskcategoryform', [ | ||||
| 				'category' =>  $this->Database->task_categories($args['categoryId']), | ||||
| 				'mode' => 'edit' | ||||
| 			]); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										71
									
								
								controllers/UsersApiController.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								controllers/UsersApiController.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,71 @@ | ||||
| <?php | ||||
|  | ||||
| namespace Grocy\Controllers; | ||||
|  | ||||
| use \Grocy\Services\UsersService; | ||||
|  | ||||
| class UsersApiController extends BaseApiController | ||||
| { | ||||
| 	public function __construct(\Slim\Container $container) | ||||
| 	{ | ||||
| 		parent::__construct($container); | ||||
| 		$this->UsersService = new UsersService(); | ||||
| 	} | ||||
|  | ||||
| 	protected $UsersService; | ||||
|  | ||||
| 	public function GetUsers(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		try | ||||
| 		{ | ||||
| 			return $this->ApiResponse($this->UsersService->GetUsersAsDto()); | ||||
| 		} | ||||
| 		catch (\Exception $ex) | ||||
| 		{ | ||||
| 			return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage()); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	public function CreateUser(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		$requestBody = $request->getParsedBody(); | ||||
|  | ||||
| 		try | ||||
| 		{ | ||||
| 			$this->UsersService->CreateUser($requestBody['username'], $requestBody['first_name'], $requestBody['last_name'], $requestBody['password']); | ||||
| 			return $this->ApiResponse(array('success' => true)); | ||||
| 		} | ||||
| 		catch (\Exception $ex) | ||||
| 		{ | ||||
| 			return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage()); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	public function DeleteUser(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		try | ||||
| 		{ | ||||
| 			$this->UsersService->DeleteUser($args['userId']); | ||||
| 			return $this->ApiResponse(array('success' => true)); | ||||
| 		} | ||||
| 		catch (\Exception $ex) | ||||
| 		{ | ||||
| 			return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage()); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	public function EditUser(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		$requestBody = $request->getParsedBody(); | ||||
|  | ||||
| 		try | ||||
| 		{ | ||||
| 			$this->UsersService->EditUser($args['userId'], $requestBody['username'], $requestBody['first_name'], $requestBody['last_name'], $requestBody['password']); | ||||
| 			return $this->ApiResponse(array('success' => true)); | ||||
| 		} | ||||
| 		catch (\Exception $ex) | ||||
| 		{ | ||||
| 			return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage()); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										30
									
								
								controllers/UsersController.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								controllers/UsersController.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| <?php | ||||
|  | ||||
| namespace Grocy\Controllers; | ||||
|  | ||||
| class UsersController extends BaseController | ||||
| { | ||||
| 	public function UsersList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		return $this->AppContainer->view->render($response, 'users', [ | ||||
| 			'users' => $this->Database->users()->orderBy('username') | ||||
| 		]); | ||||
| 	} | ||||
|  | ||||
| 	public function UserEditForm(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		if ($args['userId'] == 'new') | ||||
| 		{ | ||||
| 			return $this->AppContainer->view->render($response, 'userform', [ | ||||
| 				'mode' => 'create' | ||||
| 			]); | ||||
| 		} | ||||
| 		else | ||||
| 		{ | ||||
| 			return $this->AppContainer->view->render($response, 'userform', [ | ||||
| 				'user' =>  $this->Database->users($args['userId']), | ||||
| 				'mode' => 'edit' | ||||
| 			]); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -9,7 +9,7 @@ class DemoBarcodeLookupPlugin extends BaseBarcodeLookupPlugin | ||||
| { | ||||
| 	/* | ||||
| 		To use this plugin, configure it in data/config.php like this: | ||||
| 		define('STOCK_BARCODE_LOOKUP_PLUGIN', 'DemoBarcodeLookupPlugin'); | ||||
| 		Setting('STOCK_BARCODE_LOOKUP_PLUGIN', 'DemoBarcodeLookupPlugin'); | ||||
| 	*/ | ||||
|  | ||||
| 	/* | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -20,7 +20,7 @@ class UrlManager | ||||
|  | ||||
| 	public function ConstructUrl($relativePath, $isResource = false) | ||||
| 	{ | ||||
| 		if (DISABLE_URL_REWRITING === false || $isResource === true) | ||||
| 		if (GROCY_DISABLE_URL_REWRITING === false || $isResource === true) | ||||
| 		{ | ||||
| 			return rtrim($this->BasePath, '/') . $relativePath; | ||||
| 		} | ||||
| @@ -32,6 +32,11 @@ class UrlManager | ||||
|  | ||||
| 	private function GetBaseUrl() | ||||
| 	{ | ||||
| 		if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && strpos($_SERVER['HTTP_X_FORWARDED_PROTO'], 'https') !== false) | ||||
| 		{ | ||||
| 			$_SERVER['HTTPS'] = 'on'; | ||||
| 		} | ||||
|  | ||||
| 		return (isset($_SERVER['HTTPS']) ? "https" : "http") . "://$_SERVER[HTTP_HOST]"; | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -128,10 +128,63 @@ function BoolToString(bool $bool) | ||||
| 	return $bool ? 'true' : 'false'; | ||||
| } | ||||
|  | ||||
| function Setting(string $name, string $value) | ||||
| function Setting(string $name, $value) | ||||
| { | ||||
| 	if (!defined($name)) | ||||
| 	if (!defined('GROCY_' . $name)) | ||||
| 	{ | ||||
| 		define($name, $value); | ||||
| 		// The content of a $name.txt file in /data/settingoverrides can overwrite the given setting (for embedded mode) | ||||
| 		$settingOverrideFile = GROCY_DATAPATH . '/settingoverrides/' . $name . '.txt'; | ||||
| 		if (file_exists($settingOverrideFile)) | ||||
| 		{ | ||||
| 			define('GROCY_' . $name, file_get_contents($settingOverrideFile)); | ||||
| 		} | ||||
| 		else | ||||
| 		{ | ||||
| 			define('GROCY_' . $name, $value); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| function GetUserDisplayName($user) | ||||
| { | ||||
| 	$displayName = ''; | ||||
| 	 | ||||
| 	if (empty($user->first_name) && !empty($user->last_name)) | ||||
| 	{ | ||||
| 		$displayName = $user->last_name; | ||||
| 	} | ||||
| 	elseif (empty($user->last_name) && !empty($user->first_name)) | ||||
| 	{ | ||||
| 		$displayName = $user->first_name; | ||||
| 	} | ||||
| 	elseif (!empty($user->last_name) && !empty($user->first_name)) | ||||
| 	{ | ||||
| 		$displayName = $user->first_name . ' ' . $user->last_name; | ||||
| 	} | ||||
| 	else | ||||
| 	{ | ||||
| 		$displayName = $user->username; | ||||
| 	} | ||||
|  | ||||
| 	return $displayName; | ||||
| } | ||||
|  | ||||
| function Pluralize($number, $singularForm, $pluralForm) | ||||
| { | ||||
| 	$text = $singularForm; | ||||
| 	if ($number != 1 && $pluralForm !== null && !empty($pluralForm)) | ||||
| 	{ | ||||
| 		$text = $pluralForm; | ||||
| 	} | ||||
| 	return $text; | ||||
| } | ||||
|  | ||||
| function IsValidFileName($fileName) | ||||
| { | ||||
| 	if(preg_match('#^[a-z0-9]+\.[a-z]+?$#i', $fileName)) | ||||
| 	{ | ||||
| 		return true; | ||||
| 	} | ||||
|  | ||||
| 	return false; | ||||
| } | ||||
|   | ||||
| @@ -2,7 +2,6 @@ | ||||
|  | ||||
| return array( | ||||
| 	'Stock overview' => 'Bestand', | ||||
| 	'#1 products with #2 units in stock' => '#1 Produkte (#2 Einheiten) vorrätig', | ||||
| 	'#1 products expiring within the next #2 days' => '#1 Produkte laufen innerhalb der nächsten #2 Tage ab', | ||||
| 	'#1 products are already expired' => '#1 Produkte sind bereits abgelaufen', | ||||
| 	'#1 products are below defined min. stock amount' => '#1 Produkte sind unter Mindestbestand', | ||||
| @@ -10,20 +9,20 @@ return array( | ||||
| 	'Amount' => 'Menge', | ||||
| 	'Next best before date' => 'Nächstes MHD', | ||||
| 	'Logout' => 'Abmelden', | ||||
| 	'Habits overview' => 'Gewohnheiten', | ||||
| 	'Chores overview' => 'Hausarbeiten', | ||||
| 	'Batteries overview' => 'Batterien', | ||||
| 	'Purchase' => 'Einkauf', | ||||
| 	'Consume' => 'Verbrauch', | ||||
| 	'Inventory' => 'Inventur', | ||||
| 	'Shopping list' => 'Einkaufszettel', | ||||
| 	'Habit tracking' => 'Gewohnheit-Ausführung', | ||||
| 	'Chore tracking' => 'Hausarbeiten-Ausführung', | ||||
| 	'Battery tracking' => 'Batterie-Ladzyklus', | ||||
| 	'Products' => 'Produkte', | ||||
| 	'Locations' => 'Standorte', | ||||
| 	'Quantity units' => 'Mengeneinheiten', | ||||
| 	'Habits' => 'Gewohnheiten', | ||||
| 	'Chores' => 'Hausarbeiten', | ||||
| 	'Batteries' => 'Batterien', | ||||
| 	'Habit' => 'Gewohnheit', | ||||
| 	'Chore' => 'Hausarbeit', | ||||
| 	'Next estimated tracking' => 'Nächste geplante Ausführung', | ||||
| 	'Last tracked' => 'Zuletzt ausgeführt', | ||||
| 	'Battery' => 'Batterie', | ||||
| @@ -42,7 +41,7 @@ return array( | ||||
| 	'New amount' => 'Neue Menge', | ||||
| 	'Note' => 'Notiz', | ||||
| 	'Tracked time' => 'Ausführungszeit', | ||||
| 	'Habit overview' => 'Gewohnheit Übersicht', | ||||
| 	'Chore overview' => 'Hausarbeit Übersicht', | ||||
| 	'Tracked count' => 'Ausführungsanzahl', | ||||
| 	'Battery overview' => 'Batterie Übersicht', | ||||
| 	'Charge cycles count' => 'Ladezyklen', | ||||
| @@ -69,11 +68,11 @@ return array( | ||||
| 	'Create quantity unit' => 'Mengeneinheit erstellen', | ||||
| 	'Period type' => 'Periodentyp', | ||||
| 	'Period days' => 'Tage/Periode', | ||||
| 	'Create habit' => 'Gewohnheit erstellen', | ||||
| 	'Create chore' => 'Hausarbeit erstellen', | ||||
| 	'Used in' => 'Benutzt in', | ||||
| 	'Create battery' => 'Batterie erstellen', | ||||
| 	'Edit battery' => 'Batterie bearbeiten', | ||||
| 	'Edit habit' => 'Gewohnheit bearbeiten', | ||||
| 	'Edit chore' => 'Hausarbeit bearbeiten', | ||||
| 	'Edit quantity unit' => 'Mengeneinheit bearbeiten', | ||||
| 	'Edit product' => 'Produkt bearbeiten', | ||||
| 	'Edit location' => 'Standort bearbeiten', | ||||
| @@ -91,7 +90,7 @@ return array( | ||||
| 	'Are you sure to delete battery "#1"?' => 'Battery "#1" wirklich löschen?', | ||||
| 	'Yes' => 'Ja', | ||||
| 	'No' => 'Nein', | ||||
| 	'Are you sure to delete habit "#1"?' => 'Gewohnheit "#1" wirklich löschen?', | ||||
| 	'Are you sure to delete chore "#1"?' => 'Hausarbeit "#1" wirklich löschen?', | ||||
| 	'"#1" could not be resolved to a product, how do you want to proceed?' => '"#1" konnte nicht zu einem Produkt aufgelöst werden, wie möchtest du weiter machen?', | ||||
| 	'Create or assign product' => 'Produkt erstellen oder verknüpfen', | ||||
| 	'Cancel' => 'Abbrechen', | ||||
| @@ -111,29 +110,29 @@ return array( | ||||
| 	'This product is not in stock' => 'Dieses Produkt ist nicht vorrätig', | ||||
| 	'This means #1 will be added to stock' => 'Das bedeutet #1 wird dem Bestand hinzugefügt', | ||||
| 	'This means #1 will be removed from stock' => 'Das bedeutet #1 wird aus dem Bestand entfernt', | ||||
| 	'This means it is estimated that a new execution of this habit is tracked #1 days after the last was tracked' => 'Das bedeutet, dass eine erneute Ausführung der Gewohnheit #1 Tage nach der letzten Ausführung geplant wird', | ||||
| 	'This means it is estimated that a new execution of this chore is tracked #1 days after the last was tracked' => 'Das bedeutet, dass eine erneute Ausführung der Hausarbeit #1 Tage nach der letzten Ausführung geplant wird', | ||||
| 	'Removed #1 #2 of #3 from stock' => '#1 #2 #3 aus dem Bestand entfernt', | ||||
| 	'About grocy' => 'Über grocy', | ||||
| 	'Close' => 'Schließen', | ||||
| 	'#1 batteries are due to be charged within the next #2 days' => '#1 Batterien müssen in den nächsten #2 Tagen geladen werden', | ||||
| 	'#1 batteries are overdue to be charged' => '#1 Batterien sind überfällig', | ||||
| 	'#1 habits are due to be done within the next #2 days' => '#1 Gewohnheiten stehen in den nächsten #2 Tagen an', | ||||
| 	'#1 habits are overdue to be done' => '#1 Gewohnheiten sind überfällig', | ||||
| 	'#1 chores are due to be done within the next #2 days' => '#1 Hausarbeiten stehen in den nächsten #2 Tagen an', | ||||
| 	'#1 chores are overdue to be done' => '#1 Hausarbeiten sind überfällig', | ||||
| 	'Released on' => 'Veröffentlicht am', | ||||
| 	'Consume #3 #1 of #2' => 'Verbrauche #3 #1 #2', | ||||
| 	'Added #1 #2 of #3 to stock' => '#1 #2 #3 dem Bestand hinzugefügt', | ||||
| 	'Stock amount of #1 is now #2 #3' => 'Es sind nun #2 #3 #1 im Bestand', | ||||
| 	'Tracked execution of habit #1 on #2' => 'Ausführung von #1 am #2 erfasst', | ||||
| 	'Tracked charge cylce of battery #1 on #2' => 'Ladezyklus für Batterie #1 am #2 erfasst', | ||||
| 	'Tracked execution of chore #1 on #2' => 'Ausführung von #1 am #2 erfasst', | ||||
| 	'Tracked charge cycle of battery #1 on #2' => 'Ladezyklus für Batterie #1 am #2 erfasst', | ||||
| 	'Consume all #1 which are currently in stock' => 'Verbrauche den kompletten Bestand von #1', | ||||
| 	'All' => 'Alle', | ||||
| 	'Track charge cycle of battery #1' => 'Erfasse einen Ladezyklus für Batterie #1', | ||||
| 	'Track execution of habit #1' => 'Erfasse eine Ausführung von #1', | ||||
| 	'Track execution of chore #1' => 'Erfasse eine Ausführung von #1', | ||||
| 	'Filter by location' => 'Nach Standort filtern', | ||||
| 	'Search' => 'Suche', | ||||
| 	'Not logged in' => 'Nicht angemeldet', | ||||
| 	'You have to select a product' => 'Ein Produkt muss ausgewählt werden', | ||||
| 	'You have to select a habit' => 'Eine Gewohnheit muss ausgewählt werden', | ||||
| 	'You have to select a chore' => 'Eine Hausarbeit muss ausgewählt werden', | ||||
| 	'You have to select a battery' => 'Eine Batterie muss ausgewählt werden', | ||||
| 	'A name is required' => 'Ein Name ist erforderlich', | ||||
| 	'A location is required' => 'Ein Standort ist erforderlich', | ||||
| @@ -144,6 +143,112 @@ return array( | ||||
| 	'A best before date is required and must be later than today' => 'Ein Mindesthaltbarkeitsdatum ist erforderlich und muss später als heute sein', | ||||
| 	'Settings' => 'Einstellungen', | ||||
| 	'This can only be before now' => 'Dies kann nur vor jetzt sein', | ||||
| 	'Calendar' => 'Kalender', | ||||
| 	'Recipes' => 'Rezepte', | ||||
| 	'Edit recipe' => 'Rezept bearbeiten', | ||||
| 	'New recipe' => 'Neues Rezept', | ||||
| 	'Ingredients list' => 'Zutatenliste', | ||||
| 	'Add recipe ingredient' => 'Rezeptzutat hinzufügen', | ||||
| 	'Edit recipe ingredient' => 'Rezeptzutat bearbeiten', | ||||
| 	'Are you sure to delete recipe "#1"?' => 'Rezept "#1" wirklich löschen?', | ||||
| 	'Are you sure to delete recipe ingredient "#1"?' => 'Rezeptzutat "#1" wirklich löschen?', | ||||
| 	'Are you sure to empty the shopping list?' => 'Sicher, dass den Einkaufszettel geleert werden soll?', | ||||
| 	'Clear list' => 'Liste leeren', | ||||
| 	'Requirements fulfilled' => 'Bedarf im Bestand', | ||||
| 	'Put missing products on shopping list' => 'Fehlende Produkte auf den Einkaufszettel setzen', | ||||
| 	'Not enough in stock, #1 ingredients missing' => 'Nicht ausreichend im Bestand, #1 Zutaten fehlen', | ||||
| 	'Enough in stock' => 'Bestand reicht aus', | ||||
| 	'Not enough in stock, #1 ingredients missing but already on the shopping list' => 'Bestand nicht ausreichend, #1 Zutaten fehlen, stehen aber bereits auf dem Einkaufszettel', | ||||
| 	'Expand to fullscreen' => 'Auf ganzen Bildschirm vergrößern', | ||||
| 	'Ingredients' => 'Zutaten', | ||||
| 	'Preparation' => 'Zubereitung', | ||||
| 	'Recipe' => 'Rezept', | ||||
| 	'Not enough in stock, #1 missing, #2 already on shopping list' => 'Nicht ausreichend im Bestand, #1 fehlen, #2 stehen bereits auf dem Einkaufszettel', | ||||
| 	'Show notes' => 'Notizen anzeigen', | ||||
| 	'Put missing amount on shopping list' => 'Fehlende Menge auf den Einkaufszettel setzen', | ||||
| 	'Are you sure to put all missing ingredients for recipe "#1" on the shopping list?' => 'Sicher alle fehlenden Zutaten für Rezept "#1" auf die Einkaufsliste zu setzen?', | ||||
| 	'Added for recipe #1' => 'Hinzugefügt für Rezept #1', | ||||
| 	'Manage users' => 'Benutzer verwalten', | ||||
| 	'User' => 'Benutzer', | ||||
| 	'Users' => 'Benutzer', | ||||
| 	'Are you sure to delete user "#1"?' => 'Benutzer "#1" wirklich löschen?', | ||||
| 	'Create user' => 'Benutzer erstellen', | ||||
| 	'Edit user' => 'Benutzer bearbeiten', | ||||
| 	'First name' => 'Vorname', | ||||
| 	'Last name' => 'Nachname', | ||||
| 	'A username is required' => 'Ein Benutzername ist erforderlich', | ||||
| 	'Confirm password' => 'Passwort bestätigen', | ||||
| 	'Passwords do not match' => 'Passwörter stimmen nicht überein', | ||||
| 	'Change password' => 'Passwort ändern', | ||||
| 	'Done by' => 'Ausgeführt von', | ||||
| 	'Last done by' => 'Zuletzt ausgeführt von', | ||||
| 	'Unknown' => 'Unbekannt', | ||||
| 	'Filter by chore' => 'Nach Hausarbeit filtern', | ||||
| 	'Chores analysis' => 'Hausarbeiten Analyse', | ||||
| 	'0 means suggestions for the next charge cycle are disabled' => '0 bedeutet dass Vorschläge für den nächsten Ladezyklus deaktiviert sind', | ||||
| 	'Charge cycle interval (days)' => 'Ladezyklusintervall (Tage)', | ||||
| 	'Last price' => 'Letzter Preis', | ||||
| 	'Price history' => 'Preisentwicklung', | ||||
| 	'No price history available' => 'Keine Preisdaten verfügbar', | ||||
| 	'Price' => 'Preis', | ||||
| 	'in #1 per purchase quantity unit' => 'in #1 pro Einkaufsmengeneinheit', | ||||
| 	'The price cannot be lower than #1' => 'Der Preis darf nicht niedriger als #1 sein', | ||||
| 	'#1 product expires within the next #2 days' => '#1 Produkt läuft innerhalb der nächsten #2 Tage ab', | ||||
| 	'#1 product is already expired' => '#1 Produkt ist bereits abgelaufen', | ||||
| 	'#1 product is below defined min. stock amount' => '#1 Produkt ist unter Mindestbestand', | ||||
| 	'Unit' => 'Einheit', | ||||
| 	'Units' => 'Einheiten', | ||||
| 	'#1 chore is due to be done within the next #2 days' => '#1 Hausarbeit steht in den nächsten #2 Tagen an', | ||||
| 	'#1 chore is overdue to be done' => '#1 Hausarbeit ist überfällig', | ||||
| 	'#1 battery is due to be charged within the next #2 days' => '#1 Batterie muss in den nächsten #2 Tagen geladen werden', | ||||
| 	'#1 battery is overdue to be charged' => '#1 Batterie ist überfällig', | ||||
| 	'#1 unit was automatically added and will apply in addition to the amount entered here' => '#1 Einheit wurde automatisch hinzugefügt und gilt zusätzlich der hier eingegebenen Menge', | ||||
| 	'in singular form' => 'in der Einzahl', | ||||
| 	'in plural form' => 'in der Mehrzahl', | ||||
| 	'Never expires' => 'Läuft nie ab', | ||||
| 	'This cannot be lower than #1' => 'Dies darf nicht kleiner als #1 sein', | ||||
| 	'-1 means that this product never expires' => '-1 bedeuet, dass dieses Produkt niemals abläuft', | ||||
| 	'Quantity unit' => 'Mengeneinheit', | ||||
| 	'Only check if a single unit is in stock (a different quantity can then be used above)' => 'Nur prüfen, ob eine einzelne Einheit vorrätig ist (eine abweichende Mengeneinheit kann dann oben verwendet werden)', | ||||
| 	'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)?' => 'Sicher, dass alle Zutaten die vom Rezept "#1" benötigt werden aus dem Bestand entfernt werden sollen (Zutaten markiert mit "nur prüfen, ob eine einzelne Einheit vorrätig ist" werden ignoriert)?', | ||||
| 	'Removed all ingredients of recipe "#1" from stock' => 'Alle Zutaten, die vom Rezept "#1" benötigt werden, wurdem aus dem Bestand entfernt', | ||||
| 	'Consume all ingredients needed by this recipe' => 'Alle Zutaten, die von diesem Rezept benötigt werden, aus dem Bestand enternen', | ||||
| 	'Click to show technical details' => 'Klick um technische Details anzuzeigen', | ||||
| 	'Error while saving, probably this item already exists' => 'Fehler beim Speichern, möglicherweise existiert das Element bereits', | ||||
| 	'Error details' => 'Fehlerdetails', | ||||
| 	'Tasks' => 'Aufgaben', | ||||
| 	'Show done tasks' => 'Erledigte Aufgaben anzeigen', | ||||
| 	'Task' => 'Aufgabe', | ||||
| 	'Due' => 'Fällig', | ||||
| 	'Assigned to' => 'Zugewiesen an', | ||||
| 	'Mark task "#1" as completed' => 'Aufgabe "#1" als erledigt markieren', | ||||
| 	'Uncategorized' => 'Nicht kategorisiert', | ||||
| 	'Task categories' => 'Aufgabenkategorien', | ||||
| 	'Create task' => 'Aufgabe erstellen', | ||||
| 	'A due date is required' => 'Ein Fälligkeitsdatum ist erforderlich', | ||||
| 	'Category' => 'Kategorie', | ||||
| 	'Edit task' => 'Aufgabe bearbeiten', | ||||
| 	'Are you sure to delete task "#1"?' => 'Aufgabe "#1" wirklich löschen?', | ||||
| 	'#1 task is due to be done within the next #2 days' => '#1 Aufgabe steht in den nächsten #2 Tagen an', | ||||
| 	'#1 tasks are due to be done within the next #2 days' => '#1 Aufgaben stehen in den nächsten #2 Tagen an', | ||||
| 	'#1 task is overdue to be done' => '#1 Aufgabe ist überfällig', | ||||
| 	'#1 tasks are overdue to be done' => '#1 Aufgaben sind überfällig', | ||||
| 	'Edit task category' => 'Aufgabenkategorie bearbeiten', | ||||
| 	'Create task category' => 'Aufgabenkategorie erstellen', | ||||
| 	'Product groups' => 'Produktgruppen', | ||||
| 	'Ungrouped' => 'Ungruppiert', | ||||
| 	'Create product group' => 'Produktgruppe erstellen', | ||||
| 	'Edit product group' => 'Produktgruppe bearbeiten', | ||||
| 	'Product group' => 'Produktgruppe', | ||||
| 	'Are you sure to delete product group "#1"?' => 'Produktgruppe "#1" wirklich löschen?', | ||||
| 	'Stay logged in permanently' => 'Dauerhaft angemeldet bleiben', | ||||
| 	'When not set, you will get logged out at latest after 30 days' => 'Wenn nicht gesetzt, wirst du spätestens nach 30 Tagen automatisch abgemeldet', | ||||
| 	'Filter by status' => 'Nach Status filtern', | ||||
| 	'Below min. stock amount' => 'Unter Mindestbestand', | ||||
| 	'Expiring soon' => 'Bald ablaufend', | ||||
| 	'Already expired' => 'Bereits abgelaufen', | ||||
| 	'Due soon' => 'Bald fällig', | ||||
| 	'Overdue' => 'Überfällig', | ||||
| 	 | ||||
| 	//Constants | ||||
| 	'manually' => 'Manuell', | ||||
| @@ -163,11 +268,17 @@ return array( | ||||
| 	'Tinned food cupboard' => 'Konservenschrank', | ||||
| 	'Fridge' => 'Kühlschrank', | ||||
| 	'Piece' => 'Stück', | ||||
| 	'Pieces' => 'Stücke', | ||||
| 	'Pack' => 'Packung', | ||||
| 	'Packs' => 'Packungen', | ||||
| 	'Glass' => 'Glas', | ||||
| 	'Glasses' => 'Gläser', | ||||
| 	'Tin' => 'Dose', | ||||
| 	'Tins' => 'Dosen', | ||||
| 	'Can' => 'Becher', | ||||
| 	'Cans' => 'Becher', | ||||
| 	'Bunch' => 'Bund', | ||||
| 	'Bunches' => 'Bunde', | ||||
| 	'Gummy bears' => 'Gummibärchen', | ||||
| 	'Crisps' => 'Chips', | ||||
| 	'Eggs' => 'Eier', | ||||
| @@ -186,5 +297,38 @@ return array( | ||||
| 	'Warranty ends' => 'Garantie endet', | ||||
| 	'TV remote control' => 'TV Fernbedienung', | ||||
| 	'Alarm clock' => 'Wecker', | ||||
| 	'Heat remote control' => 'Fernbedienung Heizung' | ||||
| 	'Heat remote control' => 'Fernbedienung Heizung', | ||||
| 	'Lawn mowed in the garden' => 'Rasen im Garten gemäht', | ||||
| 	'Some good snacks' => 'Paar gute Snacks', | ||||
| 	'Pizza dough' => 'Pizzateig', | ||||
| 	'Sieved tomatoes' => 'Passierte Tomaten', | ||||
| 	'Salami' => 'Salami', | ||||
| 	'Toast' => 'Toast', | ||||
| 	'Minced meat' => 'Hackfleisch', | ||||
| 	'Pizza' => 'Pizza', | ||||
| 	'Spaghetti bolognese' => 'Spaghetti Bolognese', | ||||
| 	'Sandwiches' => 'Belegte Toasts', | ||||
| 	'English' => 'Englisch', | ||||
| 	'German' => 'Deutsch', | ||||
| 	'Italian' => 'Italienisch', | ||||
| 	'Demo in different language' => 'Demo in anderer Sprache', | ||||
| 	'This is the note content of the recipe ingredient' => 'Dies ist der Inhalt der Notiz der Zutat', | ||||
| 	'Demo User' => 'Demo Benutzer', | ||||
| 	'Gram' => 'Gramm', | ||||
|     'Grams' => 'Gramm', | ||||
|     'Flour' => 'Mehl', | ||||
|     'Pancakes' => 'Pfannkuchen', | ||||
| 	'Sugar' => 'Zucker', | ||||
| 	'Home' => 'Zuhause', | ||||
| 	'Life' => 'Leben', | ||||
| 	'Projects' => 'Projekte', | ||||
| 	'Repair the garage door' => 'Garagentor reparieren', | ||||
| 	'Fork and improve grocy' => 'grocy forken und verbessern', | ||||
| 	'Find a solution for what to do when I forget the door keys' => 'Eine Lösung für "Haustürschlüssel vergessen" finden', | ||||
| 	'Sweets' => 'Süßigkeiten', | ||||
| 	'Bakery products' => 'Bäckerei Produkte', | ||||
| 	'Tinned food' => 'Konservern', | ||||
| 	'Butchery products' => 'Metzgerei', | ||||
| 	'Vegetables/Fruits' => 'Obst/Gemüse', | ||||
| 	'Refrigerated products' => 'Kühlregal' | ||||
| ); | ||||
|   | ||||
| @@ -2,7 +2,6 @@ | ||||
|  | ||||
| return array( | ||||
| 	'Stock overview' => 'Dispensa', | ||||
| 	'#1 products with #2 units in stock' => '#1 prodotti stano per finire(#2 unità)', | ||||
| 	'#1 products expiring within the next #2 days' => '#1 prodotti scadranno tra #2 giorni', | ||||
| 	'#1 products are already expired' => '#1 prodotti scaduti', | ||||
| 	'#1 products are below defined min. stock amount' => '#1 prodotti sotto il limite minimo', | ||||
| @@ -10,20 +9,20 @@ return array( | ||||
| 	'Amount' => 'quantità', | ||||
| 	'Next best before date' => 'Prossima data di scadenza', | ||||
| 	'Logout' => 'Logout', | ||||
| 	'Habits overview' => 'Riepilogo delle abitudini', | ||||
| 	'Chores overview' => 'Riepilogo delle abitudini', | ||||
| 	'Batteries overview' => 'Riepilogo delle batterie', | ||||
| 	'Purchase' => 'Acquisti', | ||||
| 	'Consume' => 'Consumi', | ||||
| 	'Inventory' => 'Inventario', | ||||
| 	'Shopping list' => 'Lista della spesa', | ||||
| 	'Habit tracking' => 'Dati abitudini', | ||||
| 	'Chore tracking' => 'Dati abitudini', | ||||
| 	'Battery tracking' => 'Dati batterie', | ||||
| 	'Products' => 'Prodotti', | ||||
| 	'Locations' => 'Posizioni', | ||||
| 	'Quantity units' => 'Unità di misura', | ||||
| 	'Habits' => 'Abitudini', | ||||
| 	'Chores' => 'Abitudini', | ||||
| 	'Batteries' => 'Batterie', | ||||
| 	'Habit' => 'Abitudine', | ||||
| 	'Chore' => 'Abitudine', | ||||
| 	'Next estimated tracking' => 'Prossima esecuzione', | ||||
| 	'Last tracked' => 'Ultima esecuzione', | ||||
| 	'Battery' => 'Batterie', | ||||
| @@ -42,7 +41,7 @@ return array( | ||||
| 	'New amount' => 'Nuova quantità', | ||||
| 	'Note' => 'Nota', | ||||
| 	'Tracked time' => 'Ora di esecuzione', | ||||
| 	'Habit overview' => 'Riepilogo dell\'abitudine', | ||||
| 	'Chore overview' => 'Riepilogo dell\'abitudine', | ||||
| 	'Tracked count' => 'Numero di esecuzioni', | ||||
| 	'Battery overview' => 'Riepilogo della batteria', | ||||
| 	'Charge cycles count' => 'Numero di ricariche', | ||||
| @@ -69,11 +68,11 @@ return array( | ||||
| 	'Create quantity unit' => 'Aggiungi unità di misura', | ||||
| 	'Period type' => 'Tipo di ripetizione', | ||||
| 	'Period days' => 'Periodo in giorni', | ||||
| 	'Create habit' => 'Aggiungi abitudine', | ||||
| 	'Create chore' => 'Aggiungi abitudine', | ||||
| 	'Used in' => 'Usato in', | ||||
| 	'Create battery' => 'Aggiungi batteria', | ||||
| 	'Edit battery' => 'Modifica batteria', | ||||
| 	'Edit habit' => 'Modifica abitudine', | ||||
| 	'Edit chore' => 'Modifica abitudine', | ||||
| 	'Edit quantity unit' => 'Modifica unità di misura', | ||||
| 	'Edit product' => 'Modifica prodotto', | ||||
| 	'Edit location' => 'Modifica posizione', | ||||
| @@ -91,7 +90,7 @@ return array( | ||||
| 	'Are you sure to delete battery "#1"?' => 'Sei sicuro di voler eliminare la batteria "#1"?', | ||||
| 	'Yes' => 'Si', | ||||
| 	'No' => 'No', | ||||
| 	'Are you sure to delete habit "#1"?' => 'Sei sicuro di voler eliminare l\'abitudine "#1"?', | ||||
| 	'Are you sure to delete chore "#1"?' => 'Sei sicuro di voler eliminare l\'abitudine "#1"?', | ||||
| 	'"#1" could not be resolved to a product, how do you want to proceed?' => '"#1" non è stato associato a nessun prodotto, vuoi procedere?', | ||||
| 	'Create or assign product' => 'Aggiungi o assegna prodotto', | ||||
| 	'Cancel' => 'Annulla', | ||||
| @@ -111,29 +110,29 @@ return array( | ||||
| 	'This product is not in stock' => 'Questo prodotto non è in dispensa', | ||||
| 	'This means #1 will be added to stock' => '#1 sarà aggiunto alla dispensa', | ||||
| 	'This means #1 will be removed from stock' => '#1 sarà rimosso dalla dispensa', | ||||
| 	'This means it is estimated that a new execution of this habit is tracked #1 days after the last was tracked' => 'L\'esecuzione dell\'abitudine è #1 giorni dopo la precedente', | ||||
| 	'This means it is estimated that a new execution of this chore is tracked #1 days after the last was tracked' => 'L\'esecuzione dell\'abitudine è #1 giorni dopo la precedente', | ||||
| 	'Removed #1 #2 of #3 from stock' => '#1 #2 su #3 rimossi dalla dispensa', | ||||
| 	'About grocy' => 'Riguardo grocy', | ||||
| 	'Close' => 'Chiudi', | ||||
| 	'#1 batteries are due to be charged within the next #2 days' => '#1 batterie da ricaricare entro #2 giorni', | ||||
| 	'#1 batteries are overdue to be charged' => '#1 batterie devono essere ricaricate', | ||||
| 	'#1 habits are due to be done within the next #2 days' => '#1 abitudini da eseguire entro #2 giorni', | ||||
| 	'#1 habits are overdue to be done' => '#1 abitudini da eseguire', | ||||
| 	'#1 chores are due to be done within the next #2 days' => '#1 abitudini da eseguire entro #2 giorni', | ||||
| 	'#1 chores are overdue to be done' => '#1 abitudini da eseguire', | ||||
| 	'Released on' => 'Rilasciato il', | ||||
| 	'Consume #3 #1 of #2' => 'Consumati #3 #1 di #2', | ||||
| 	'Added #1 #2 of #3 to stock' => 'Aggiunti #1 #2 di #3', | ||||
| 	'Stock amount of #1 is now #2 #3' => 'La quantità in dispensa di #1 è ora #2 #3', | ||||
| 	'Tracked execution of habit #1 on #2' => 'Esecuzione dell\'abitudine #1 registrata il #2', | ||||
| 	'Tracked charge cylce of battery #1 on #2' => 'Ricarica della batteria #1 effettuata il #2', | ||||
| 	'Tracked execution of chore #1 on #2' => 'Esecuzione dell\'abitudine #1 registrata il #2', | ||||
| 	'Tracked charge cycle of battery #1 on #2' => 'Ricarica della batteria #1 effettuata il #2', | ||||
| 	'Consume all #1 which are currently in stock' => 'Consuma tutto #1 in dispensa', | ||||
| 	'All' => 'Tutto', | ||||
| 	'Track charge cycle of battery #1' => 'Registra la ricarica della batteria #1', | ||||
| 	'Track execution of habit #1' => 'Registra l\'esecuzione dell\'abitudine #1', | ||||
| 	'Track execution of chore #1' => 'Registra l\'esecuzione dell\'abitudine #1', | ||||
| 	'Filter by location' => 'Filtra per posizione', | ||||
| 	'Search' => 'Cerca', | ||||
| 	'Not logged in' => 'Non autenticato', | ||||
| 	'You have to select a product' => 'Devi selezionare un prodotto', | ||||
| 	'You have to select a habit' => 'Devi selezionare un\'abitudine', | ||||
| 	'You have to select a chore' => 'Devi selezionare un\'abitudine', | ||||
| 	'You have to select a battery' => 'Devi selezionare una batteria', | ||||
| 	'A name is required' => 'Inserisci un nome', | ||||
| 	'A location is required' => 'Inserisci la posizione', | ||||
| @@ -150,7 +149,6 @@ return array( | ||||
| 	'timeago_locale' => 'it', | ||||
| 	'timeago_nan' => 'NaN anni fa', | ||||
| 	'moment_locale' => 'it', | ||||
| 	'bootstrap_datepicker_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"}}', | ||||
| 	 | ||||
| 	//Demo data | ||||
| @@ -161,11 +159,17 @@ return array( | ||||
| 	'Tinned food cupboard' => 'Konservenschrank', | ||||
| 	'Fridge' => 'Kühlschrank', | ||||
| 	'Piece' => 'Pezzo', | ||||
| 	'Pieces' => 'Pezzi', | ||||
| 	'Pack' => 'Pacco', | ||||
| 	'Packs' => 'Pacchi', | ||||
| 	'Glass' => 'Bicchiere', | ||||
| 	'Glasses' => 'Bicchieri', | ||||
| 	'Tin' => 'Scatola', | ||||
| 	'Tins' => 'Scatole', | ||||
| 	'Can' => 'Lattina', | ||||
| 	'Cans' => 'Lattine', | ||||
| 	'Bunch' => 'Cespo', | ||||
| 	'Bunches' => 'Cespi', | ||||
| 	'Gummy bears' => 'Caramelle', | ||||
| 	'Crisps' => 'Patatine', | ||||
| 	'Eggs' => 'Uova', | ||||
|   | ||||
							
								
								
									
										334
									
								
								localization/no.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										334
									
								
								localization/no.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,334 @@ | ||||
| <?php | ||||
|  | ||||
| return array( | ||||
| 	'Stock overview' => 'Husholdning', | ||||
| 	'#1 products expiring within the next #2 days' => '#1 Produkt som går ut på dato innen de neste #2 dagene', | ||||
| 	'#1 products are already expired' => '#1 Produkt som har gått ut på dato', | ||||
| 	'#1 products are below defined min. stock amount' => '#1 Produkt under minimum husholdningsnivå', | ||||
| 	'Product' => 'Produkt', | ||||
| 	'Amount' => 'Antall', | ||||
| 	'Next best before date' => 'Kommende best før dato', | ||||
| 	'Logout' => 'Logg ut', | ||||
| 	'Chores overview' => 'Oversikt Husarbeid', | ||||
| 	'Batteries overview' => 'Oversikt Batteri', | ||||
| 	'Purchase' => 'Innkjøp', | ||||
| 	'Consume' => 'Forbrukt', | ||||
| 	'Inventory' => 'Endre Husholdning', | ||||
| 	'Shopping list' => 'Handleliste', | ||||
| 	'Chore tracking' => 'Logge Husarbeid', | ||||
| 	'Battery tracking' => 'Batteri Ladesyklus', | ||||
| 	'Products' => 'Produkter', | ||||
| 	'Locations' => 'Lokasjoner', | ||||
| 	'Quantity units' => 'Forpakning', | ||||
| 	'Chores' => 'Husarbeid', | ||||
| 	'Batteries' => 'Batterier', | ||||
| 	'Chore' => 'Husarbeid', | ||||
| 	'Next estimated tracking' => 'Neste handling', | ||||
| 	'Last tracked' => 'Sist logget', | ||||
| 	'Battery' => 'Batteri', | ||||
| 	'Last charged' => 'Sist ladet', | ||||
| 	'Next planned charge cycle' => 'Neste planlagte ladesyklus', | ||||
| 	'Best before' => 'Best før', | ||||
| 	'OK' => 'OK', | ||||
| 	'Product overview' => 'Produkt oversikt', | ||||
| 	'Stock quantity unit' => 'Forpakningstype i husholdningen', | ||||
| 	'Stock amount' => 'Husholdning', | ||||
| 	'Last purchased' => 'Sist kjøpt', | ||||
| 	'Last used' => 'Sist brukt', | ||||
| 	'Spoiled' => 'Produkt har gått ut på dato', | ||||
| 	'Barcode lookup is disabled' => 'Strekkodesøk deaktivert', | ||||
| 	'will be added to the list of barcodes for the selected product on submit' => 'Blir lagt til liste over strekkoder når produkt blir lagt inn.', | ||||
| 	'New amount' => 'Nytt antall', | ||||
| 	'Note' => 'Info', | ||||
| 	'Tracked time' => 'Tid utført/ ladet', | ||||
| 	'Chore overview' => 'Oversikt Husarbeid', | ||||
| 	'Tracked count' => 'Antall utførelser/ ladninger', | ||||
| 	'Battery overview' => 'Batteri Oversikt', | ||||
| 	'Charge cycles count' => 'Antall ladesykluser', | ||||
| 	'Create shopping list item' => 'Opprett handelisteoppføring', | ||||
| 	'Edit shopping list item' => 'Endre på handlelistoppføring', | ||||
| 	'#1 units were automatically added and will apply in addition to the amount entered here' => '#1 enheter ble automatisk lagt til i tillegg til hva som blir skrevet inn her', | ||||
| 	'Save' => 'Lagre', | ||||
| 	'Add' => 'Legg til', | ||||
| 	'Name' => 'Navn', | ||||
| 	'Location' => 'Lokasjon', | ||||
| 	'Min. stock amount' => 'Minimums antall for husholdingen', | ||||
| 	'QU purchase' => 'FPK innkjøp', | ||||
| 	'QU stock' => 'FPK husholdning', | ||||
| 	'QU factor' => 'FPK faktor', | ||||
| 	'Description' => 'Beskrivelse', | ||||
| 	'Create product' => 'Opprett produkt', | ||||
| 	'Barcode(s)' => 'Strekkode(r)', | ||||
| 	'Minimum stock amount' => 'Minimums antall for husholdningen', | ||||
| 	'Default best before days' => 'Standard antall dager best før', | ||||
| 	'Quantity unit purchase' => 'Forpakning kjøpt', | ||||
| 	'Quantity unit stock' => 'Forpakning husholdning', | ||||
| 	'Factor purchase to stock quantity unit' => 'Innkjøpsfaktor for forpakning', | ||||
| 	'Create location' => 'Opprett lokasjon', | ||||
| 	'Create quantity unit' => 'Opprett forpakning', | ||||
| 	'Period type' => 'Gjentakelse', | ||||
| 	'Period days' => 'Antall dager for gjentakelse', | ||||
| 	'Create chore' => 'Opprett husarbeid oppgave', | ||||
| 	'Used in' => 'Brukt', | ||||
| 	'Create battery' => 'Opprett batteri', | ||||
| 	'Edit battery' => 'Endre batteri', | ||||
| 	'Edit chore' => 'Endre husarbeid oppgave', | ||||
| 	'Edit quantity unit' => 'Endre forpakning', | ||||
| 	'Edit product' => 'Endre produkt', | ||||
| 	'Edit location' => 'Endre lokasjon', | ||||
| 	'Record data' => 'Logg handlinger', | ||||
| 	'Manage master data' => 'Administrer masterdata', | ||||
| 	'This will apply to added products' => 'Dette vil gjelde for produkt som blir lagt til', | ||||
| 	'never' => 'aldri', | ||||
| 	'Add products that are below defined min. stock amount' => 'Legg til produkt som er under minimumsnivå for husholdningen', | ||||
| 	'For purchases this amount of days will be added to today for the best before date suggestion' => 'For innkjøp vil dette antallet dager legges til bestfør forslaget', | ||||
| 	'This means 1 #1 purchased will be converted into #2 #3 in stock' => 'Dette betyr at 1 #1 innkjøp vil bli omgjort til #2 #3 husholdning', | ||||
| 	'Login' => 'Logg inn', | ||||
| 	'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"?', | ||||
| 	'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', | ||||
| 	'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', | ||||
| 	'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"?', | ||||
| 	'Manage API keys' => 'Administrer API-Keys', | ||||
|     'REST API & data model documentation' => 'REST-API & Datamodell Dokumentasjon', | ||||
|     'API keys' => 'API-Keys', | ||||
|     'Create new API key' => 'Opprett ny API-Key', | ||||
|     'API key' => 'API-Key', | ||||
|     'Expires' => 'Går ut', | ||||
|     'Created' => 'Opprettet', | ||||
| 	'This product is not in stock' => 'Dette produktet er ikke i husholdningen', | ||||
| 	'This means #1 will be added to stock' => 'Dette betyr at #1 vil bli lagt til i husholdningen', | ||||
| 	'This means #1 will be removed from stock' => 'Dette betyr at #1 vil bli fjernet fra husholdningen', | ||||
| 	'This means it is estimated that a new execution of this chore is tracked #1 days after the last was tracked' => 'Dette betyr at det er estimert at den nye utførelsen av denne husarbeid oppgaven er logget #1 dag etter den sist var logget', | ||||
| 	'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 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', | ||||
| 	'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', | ||||
| 	'All' => 'Alle', | ||||
| 	'Track charge cycle of battery #1' => '#1 ladet', | ||||
| 	'Track execution of chore #1' => 'Utfør husarbeid oppgave #1', | ||||
| 	'Filter by location' => 'Filtrér etter lokasjon', | ||||
| 	'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 battery' => 'Du må velge et batteri', | ||||
| 	'A name is required' => 'Vennligst fyll inn et navn', | ||||
| 	'A location is required' => 'En lokasjon kreves', | ||||
| 	'The amount cannot be lower than #1' => 'Antallet kan ikke være lavere enn #1', | ||||
| 	'This cannot be negative' => 'Dette kan ikke være negativt', | ||||
| 	'A quantity unit is required' => 'Forpakning antall/størrelse kreves', | ||||
| 	'A period type is required' => 'En periodetype kreves', | ||||
| 	'A best before date is required and must be later than today' => 'En best før dato kreves, denne må være senere enn i dag', | ||||
| 	'Settings' => 'Innstillinger', | ||||
| 	'This can only be before now' => 'Dette kan kun være før nå', | ||||
|         'Calendar' => 'Kalender', | ||||
| 	'Recipes' => 'Oppskrifter', | ||||
| 	'Edit recipe' => 'Endre oppskrift', | ||||
| 	'New recipe' => 'Ny oppskrift', | ||||
| 	'Ingredients list' => 'Liste over ingredienser', | ||||
| 	'Add recipe ingredient' => 'Legg ingrediens til oppskrift', | ||||
| 	'Edit recipe ingredient' => 'Endre ingrediens i oppskrift', | ||||
| 	'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', | ||||
| 	'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', | ||||
| 	'Enough in stock' => 'Nok i husholdningen', | ||||
| 	'Not enough in stock, #1 ingredients missing but already on the shopping list' => 'Ikke nok i husholdningen, #1 ingrediens mangler, men denne er på handelisten', | ||||
| 	'Expand to fullscreen' => 'Full skjerm', | ||||
| 	'Ingredients' => 'Ingredienser', | ||||
| 	'Preparation' => 'Forberedelse / Slik gjør du', | ||||
| 	'Recipe' => 'Oppskrift', | ||||
| 	'Not enough in stock, #1 missing, #2 already on shopping list' => 'Ikke nok i husholdningen, mangler #1, er #2 på handlelisten', | ||||
| 	'Show notes' => 'Vis notater', | ||||
| 	'Put missing amount on shopping list' => 'Legg manglende til handlelisten', | ||||
| 	'Are you sure to put all missing ingredients for recipe "#1" on the shopping list?' => 'Er du sikker du ønsker å legge alle manglende ingredienser til oppskrift "#1"?', | ||||
| 	'Added for recipe #1' => 'Lagt til fra oppskrift "#1"', | ||||
| 	'Manage users' => 'Administrer brukere', | ||||
| 	'User' => 'Bruker', | ||||
| 	'Users' => 'Brukere', | ||||
| 	'Are you sure to delete user "#1"?' => 'Er du sikker på du ønsker å slette bruker, "#1"?', | ||||
| 	'Create user' => 'Legg til bruker', | ||||
| 	'Edit user' => 'Endre på bruker', | ||||
| 	'First name' => 'Fornavn', | ||||
| 	'Last name' => 'Etternavn', | ||||
| 	'A username is required' => 'Et brukernavn er nødvendig', | ||||
| 	'Confirm password' => 'Bekreft passord', | ||||
| 	'Passwords do not match' => 'Passord er ikke like', | ||||
| 	'Change password' => 'Endre passord', | ||||
| 	'Done by' => 'Utført av', | ||||
| 	'Last done by' => 'Sist utført av', | ||||
| 	'Unknown' => 'Ukjent', | ||||
| 	'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)', | ||||
|  	'Last price' => 'Siste pris', | ||||
|  	'Price history' => 'Prishistorikk', | ||||
|  	'No price history available' => 'Ingen prishistorikk tilgjengelig', | ||||
| 	'Price' => 'Pris', | ||||
| 	'in #1 per purchase quantity unit' => 'I #1 per kjøpt forpakning ', | ||||
| 	'The price cannot be lower than #1' => 'Prisen kan ikke være lavere enn #1', | ||||
| 	'#1 product expires within the next #2 days' => '#1 Produkt går ut på dato innen de #2  neste dagene', | ||||
| 	'#1 product is already expired' => '#1 Produkt er allerede gått ut på dato', | ||||
| 	'#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 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', | ||||
| 	'in singular form' => 'I entall', | ||||
| 	'in plural form' => 'I flertall', | ||||
| 	'Never expires' => 'Går ikke ut på dato', | ||||
| 	'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', | ||||
| 	'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', | ||||
| 	'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', | ||||
| 	'Tasks' => 'Oppgaver', | ||||
| 	'Show done tasks' => 'Vis ferdige oppgaver', | ||||
| 	'Task' => 'Oppgave', | ||||
| 	'Due' => 'Forfall', | ||||
| 	'Assigned to' => 'Tildelt', | ||||
| 	'Mark task "#1" as completed' => 'Merk oppgave "#1" som ferdig', | ||||
| 	'Uncategorized' => 'Mangler kategori', | ||||
| 	'Task categories' => 'Oppgave kategorier', | ||||
| 	'Create task' => 'Opprett en oppgave', | ||||
| 	'A due date is required' => 'En forfallsdato kreves', | ||||
| 	'Category' => 'Kategori', | ||||
| 	'Edit task' => 'Endre oppgave', | ||||
| 	'Are you sure to delete task "#1"?' => 'Er du sikker du ønsker slette oppgave "#1"?', | ||||
| 	'#1 task is due to be done within the next #2 days' => '#1 oppgave har utførelse forfall innen de neste #2 dagene', | ||||
| 	'#1 tasks are due to be done within the next #2 days' => '#1 oppgaver har utførelse forfall innen de neste #2 dagene', | ||||
| 	'#1 task is overdue to be done' => '#1 oppgave har forfalt utførelse dato', | ||||
| 	'#1 tasks are overdue to be done' => '#1 oppgaver har forfalt utførelse dato', | ||||
| 	'Edit task category' => 'Endre oppgave kategori', | ||||
| 	'Create task category' => 'Opprett oppgave kategori', | ||||
| 	'Product groups' => 'Produktgrupper', | ||||
| 	'Ungrouped' => 'Ikke i grupper', | ||||
| 	'Create product group' => 'Opprett produkt gruppe', | ||||
| 	'Edit product group' => 'Endre produkt gruppe', | ||||
| 	'Product group' => 'Produktgruppe', | ||||
| 	'Are you sure to delete product group "#1"?' => 'Er du sikker du ønsker å slette produktgruppe "#1"?', | ||||
| 	'Stay logged in permanently' => 'Alltid være innlogget', | ||||
| 	'When not set, you will get logged out at latest after 30 days' => 'Når den ikke er satt vil du bli logget ut etter 30 dager', | ||||
| 	'Filter by status' => 'Filtrér etter status', | ||||
| 	'Below min. stock amount' => 'Under under minimum husholdningsnivå', | ||||
| 	'Expiring soon' => 'Går snart ut på dato', | ||||
| 	'Already expired' => 'Utgått på dato', | ||||
| 	'Due soon' => 'Forfaller snart', | ||||
| 	'Overdue' => 'Forfalt', | ||||
| 	 | ||||
| 	//Constants | ||||
| 	'manually' => 'Manuel', | ||||
| 	'dynamic-regular' => 'Automatisk', | ||||
| 	 | ||||
| 	//Technical component translations | ||||
| 	'timeago_locale' => 'no', | ||||
| 	'timeago_nan' => 'for NaN År', | ||||
| 	'moment_locale' => 'nb', | ||||
| 	'datatables_localization' => '{"sEmptyTable":"Det finnes ingen data i tabellen","sInfo":"_START_ fra _END_ til _TOTAL_ skriv","sInfoEmpty":"Ingen data tilgjengelign","sInfoFiltered":"(filtrert fra _MAX_ skriv)","sInfoPostFix":"","sInfoThousands":".","sLengthMenu":"_MENU_ registrer deg","sLoadingRecords":"Laster ..","sProcessing":"Vennligst vent ..","sSearch":"Søk","sZeroRecords":"Ingen oppføringer tilgjengelig","oPaginate":{"sFirst":"Første","sPrevious":"Bakover","sNext":"Neste","sLast":"Siste"},"oAria":{"sSortAscending":": Sortér stigende","sSortDescending":": Sortér synkende"},"select":{"rows":{"0":"klikk på en linje for å velge","1":"1 linje valgt","_":"%d linger valgt"}},"buttons":{"print":"Print","colvis":"Søyle","copy":"Kopi","copyTitle":"Kopier til utklippstavlen","copyKeys":"Trykk <i>ctrl</i> eller <i>⌘</i> + <i>C</i> for å kopiere tabell<br> til utklipptavlen.<br><br>For å avbryte, klikke på meldingen eller trykk på ESC.","copySuccess":{"1":"1 Kolonne kopiert","_":"%d kolonne kopiert"}}}', | ||||
| 	 | ||||
| 	//Demo data | ||||
| 	'Cookies' => 'Cookies', | ||||
| 	'Chocolate' => 'Sjokolade', | ||||
| 	'Pantry' => 'Spiskammers', | ||||
| 	'Candy cupboard' => 'Godteriskapet', | ||||
| 	'Tinned food cupboard' => 'Boksematskapet', | ||||
| 	'Fridge' => 'Kjøleskapet', | ||||
| 	'Piece' => 'Ett', | ||||
| 	'Pieces' => 'Flere', | ||||
| 	'Pack' => 'Pakke', | ||||
| 	'Packs' => 'Pakker', | ||||
| 	'Glass' => 'Glass', | ||||
| 	'Glasses' => 'Glass', | ||||
| 	'Tin' => 'Hermetikkboks', | ||||
| 	'Tins' => 'Hermetikkbokser', | ||||
| 	'Can' => 'Boks', | ||||
| 	'Cans' => 'Bokser', | ||||
| 	'Bunch' => 'Klase', | ||||
| 	'Bunches' => 'Klaser',	 | ||||
| 	'Gummy bears' => 'Vingummibjørner', | ||||
| 	'Crisps' => 'Chips', | ||||
| 	'Eggs' => 'Egg', | ||||
| 	'Noodles' => 'Nuddler', | ||||
| 	'Pickles' => 'Sur agurk', | ||||
| 	'Gulash soup' => 'Gulasj suppe', | ||||
| 	'Yogurt' => 'Yoghurt', | ||||
| 	'Cheese' => 'Ost', | ||||
| 	'Cold cuts' => 'Kjøttpålegg', | ||||
| 	'Paprika' => 'Paprika', | ||||
| 	'Cucumber' => 'Agurk', | ||||
| 	'Radish' => 'Reddik', | ||||
| 	'Tomato' => 'Tomat', | ||||
| 	'Changed towels in the bathroom' => 'Bytt handklær på badet', | ||||
| 	'Cleaned the kitchen floor' => 'Vasket kjøkkengulvet', | ||||
| 	'Warranty ends' => 'Garanti utgår', | ||||
| 	'TV remote control' => 'Fjernkontroll for TV', | ||||
| 	'Alarm clock' => 'Alarmklokke', | ||||
| 	'Heat remote control' => 'Fjernkontroll for termostat', | ||||
|         'Lawn mowed in the garden' => 'Kuttet gresset i hagen', | ||||
| 	'Some good snacks' => 'Noen gode snacks', | ||||
| 	'Pizza dough' => 'Pizzadeig', | ||||
| 	'Sieved tomatoes' => 'Tomatpuré', | ||||
| 	'Salami' => 'Salami', | ||||
| 	'Toast' => 'Ristet brød', | ||||
| 	'Minced meat' => 'Kjøttdeig', | ||||
| 	'Pizza' => 'Pizza', | ||||
| 	'Spaghetti bolognese' => 'Spaghetti Bolognese', | ||||
| 	'Sandwiches' => 'Smørbrød', | ||||
| 	'English' => 'Engelsk', | ||||
| 	'German' => 'Tysk', | ||||
| 	'Italian' => 'Italiensk', | ||||
| 	'Demo in different language' => 'Demo i annet språk', | ||||
| 	'This is the note content of the recipe ingredient' => 'Dette er notisen for ingrediensen i oppskriften', | ||||
| 	'Demo User' => 'Demo Bruker', | ||||
| 	'Gram' => 'Gram', | ||||
|     'Grams' => 'Gram', | ||||
|     'Flour' => 'Mel', | ||||
|     'Pancakes' => 'Pannekaker', | ||||
| 	'Sugar' => 'Sukker', | ||||
| 	'Home' => 'Hus', | ||||
| 	'Life' => 'Livstil', | ||||
| 	'Projects' => 'Projekter', | ||||
| 	'Repair the garage door' => 'Reparere garasjedøren', | ||||
| 	'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 ', | ||||
| 	'Tinned food' => 'Boksemat', | ||||
| 	'Butchery products' => 'Produkt fra slakteren', | ||||
| 	'Vegetables/Fruits' => 'Frukt/ Grønnsaker', | ||||
| 	'Refrigerated products' => 'Kjølte produkter' | ||||
| ); | ||||
| @@ -22,8 +22,9 @@ class ApiKeyAuthMiddleware extends BaseMiddleware | ||||
| 		$route = $request->getAttribute('route'); | ||||
| 		$routeName = $route->getName(); | ||||
|  | ||||
| 		if ($this->ApplicationService->IsDemoInstallation()) | ||||
| 		if (GROCY_IS_DEMO_INSTALL || GROCY_IS_EMBEDDED_INSTALL) | ||||
| 		{ | ||||
| 			define('GROCY_AUTHENTICATED', true); | ||||
| 			$response = $next($request, $response); | ||||
| 		} | ||||
| 		else | ||||
| @@ -45,10 +46,23 @@ class ApiKeyAuthMiddleware extends BaseMiddleware | ||||
|  | ||||
| 			if (!$validSession && !$validApiKey) | ||||
| 			{ | ||||
| 				define('GROCY_AUTHENTICATED', false); | ||||
| 				$response = $response->withStatus(401); | ||||
| 			} | ||||
| 			else | ||||
| 			elseif ($validApiKey) | ||||
| 			{ | ||||
| 				$user = $apiKeyService->GetUserByApiKey($request->getHeaderLine($this->ApiKeyHeaderName)); | ||||
| 				define('GROCY_AUTHENTICATED', true); | ||||
| 				define('GROCY_USER_ID', $user->id); | ||||
|  | ||||
| 				$response = $next($request, $response); | ||||
| 			} | ||||
| 			elseif ($validSession) | ||||
| 			{ | ||||
| 				$user = $sessionService->GetUserBySessionKey($_COOKIE[$this->SessionCookieName]); | ||||
| 				define('GROCY_AUTHENTICATED', true); | ||||
| 				define('GROCY_USER_ID', $user->id); | ||||
|  | ||||
| 				$response = $next($request, $response); | ||||
| 			} | ||||
| 		} | ||||
|   | ||||
| @@ -1,20 +0,0 @@ | ||||
| <?php | ||||
|  | ||||
| namespace Grocy\Middleware; | ||||
|  | ||||
| class CliMiddleware extends BaseMiddleware | ||||
| { | ||||
| 	public function __invoke(\Slim\Http\Request $request, \Slim\Http\Response $response, callable $next) | ||||
| 	{ | ||||
| 		if (PHP_SAPI !== 'cli') | ||||
| 		{ | ||||
| 			$response->write('Please call this only from CLI'); | ||||
| 			return $response->withHeader('Content-Type', 'text/plain')->withStatus(400); | ||||
| 		} | ||||
| 		else | ||||
| 		{ | ||||
| 			$response = $next($request, $response); | ||||
| 			return $response->withHeader('Content-Type', 'text/plain'); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -3,6 +3,7 @@ | ||||
| namespace Grocy\Middleware; | ||||
|  | ||||
| use \Grocy\Services\SessionService; | ||||
| use \Grocy\Services\LocalizationService; | ||||
|  | ||||
| class SessionAuthMiddleware extends BaseMiddleware | ||||
| { | ||||
| @@ -18,23 +19,41 @@ class SessionAuthMiddleware extends BaseMiddleware | ||||
| 	{ | ||||
| 		$route = $request->getAttribute('route'); | ||||
| 		$routeName = $route->getName(); | ||||
| 		$sessionService = new SessionService(); | ||||
|  | ||||
| 		if ($routeName === 'root' || $this->ApplicationService->IsDemoInstallation()) | ||||
| 		if ($routeName === 'root') | ||||
| 		{ | ||||
| 			define('AUTHENTICATED', $this->ApplicationService->IsDemoInstallation()); | ||||
| 			$response = $next($request, $response); | ||||
| 		} | ||||
| 		elseif (GROCY_IS_DEMO_INSTALL || GROCY_IS_EMBEDDED_INSTALL) | ||||
| 		{ | ||||
| 			$user = $sessionService->GetDefaultUser(); | ||||
| 			define('GROCY_AUTHENTICATED', true); | ||||
| 			define('GROCY_USER_USERNAME', $user->username); | ||||
|  | ||||
| 			$response = $next($request, $response); | ||||
| 		} | ||||
| 		else | ||||
| 		{ | ||||
| 			$sessionService = new SessionService(); | ||||
| 			if ((!isset($_COOKIE[$this->SessionCookieName]) || !$sessionService->IsValidSession($_COOKIE[$this->SessionCookieName])) && $routeName !== 'login') | ||||
| 			{ | ||||
| 				define('AUTHENTICATED', false); | ||||
| 				define('GROCY_AUTHENTICATED', false); | ||||
| 				$response = $response->withRedirect($this->AppContainer->UrlManager->ConstructUrl('/login')); | ||||
| 			} | ||||
| 			else | ||||
| 			{ | ||||
| 				define('AUTHENTICATED', $routeName !== 'login'); | ||||
| 				if ($routeName !== 'login') | ||||
| 				{ | ||||
| 					$user = $sessionService->GetUserBySessionKey($_COOKIE[$this->SessionCookieName]); | ||||
| 					define('GROCY_AUTHENTICATED', true); | ||||
| 					define('GROCY_USER_USERNAME', $user->username); | ||||
| 					define('GROCY_USER_ID', $user->id); | ||||
| 				} | ||||
| 				else | ||||
| 				{ | ||||
| 					define('GROCY_AUTHENTICATED', false); | ||||
| 				} | ||||
|  | ||||
| 				$response = $next($request, $response); | ||||
| 			} | ||||
| 		} | ||||
|   | ||||
| @@ -3,4 +3,3 @@ AS | ||||
| SELECT product_id, SUM(amount) AS amount, MIN(best_before_date) AS best_before_date | ||||
| FROM stock | ||||
| GROUP BY product_id | ||||
| ORDER BY MIN(best_before_date) ASC | ||||
|   | ||||
| @@ -3,4 +3,3 @@ AS | ||||
| SELECT habit_id, MAX(tracked_time) AS last_tracked_time | ||||
| FROM habits_log | ||||
| GROUP BY habit_id | ||||
| ORDER BY MAX(tracked_time) DESC | ||||
|   | ||||
| @@ -3,4 +3,3 @@ AS | ||||
| SELECT battery_id, MAX(tracked_time) AS last_tracked_time | ||||
| FROM battery_charge_cycles | ||||
| GROUP BY battery_id | ||||
| ORDER BY MAX(tracked_time) DESC | ||||
|   | ||||
							
								
								
									
										50
									
								
								migrations/0025.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								migrations/0025.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,50 @@ | ||||
| CREATE TABLE recipes ( | ||||
| 	id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, | ||||
| 	name TEXT NOT NULL, | ||||
| 	description TEXT, | ||||
| 	row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime')) | ||||
| ); | ||||
|  | ||||
| CREATE TABLE recipes_pos ( | ||||
| 	id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, | ||||
| 	recipe_id INTEGER NOT NULL, | ||||
| 	product_id INTEGER NOT NULL, | ||||
| 	amount INTEGER NOT NULL DEFAULT 0, | ||||
| 	note TEXT, | ||||
| 	row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime')) | ||||
| ); | ||||
|  | ||||
| CREATE VIEW recipes_fulfillment | ||||
| AS | ||||
| SELECT | ||||
| 	r.id AS recipe_id, | ||||
| 	rp.id AS recipe_pos_id, | ||||
| 	rp.product_id AS product_id, | ||||
| 	rp.amount AS recipe_amount, | ||||
| 	IFNULL(sc.amount, 0) AS stock_amount, | ||||
| 	CASE WHEN IFNULL(sc.amount, 0) >= rp.amount THEN 1 ELSE 0 END AS need_fulfilled, | ||||
| 	CASE WHEN IFNULL(sc.amount, 0) - IFNULL(rp.amount, 0) < 0 THEN ABS(IFNULL(sc.amount, 0) - IFNULL(rp.amount, 0)) ELSE 0 END AS missing_amount, | ||||
| 	IFNULL(sl.amount, 0) AS amount_on_shopping_list, | ||||
| 	CASE WHEN IFNULL(sc.amount, 0) + IFNULL(sl.amount, 0) >= rp.amount THEN 1 ELSE 0 END AS need_fulfilled_with_shopping_list | ||||
| FROM recipes r | ||||
| JOIN recipes_pos rp | ||||
| 	ON r.id = rp.recipe_id | ||||
| LEFT JOIN ( | ||||
| 	SELECT product_id, SUM(amount + amount_autoadded) AS amount | ||||
| 	FROM shopping_list | ||||
| 	GROUP BY product_id) sl | ||||
| 	ON rp.product_id = sl.product_id | ||||
| LEFT JOIN stock_current sc | ||||
| 	ON rp.product_id = sc.product_id; | ||||
|  | ||||
| CREATE VIEW recipes_fulfillment_sum | ||||
| AS | ||||
| SELECT | ||||
| 	r.id AS recipe_id, | ||||
| 	IFNULL(MIN(rf.need_fulfilled), 1) AS need_fulfilled, | ||||
| 	IFNULL(MIN(rf.need_fulfilled_with_shopping_list), 1) AS need_fulfilled_with_shopping_list, | ||||
| 	(SELECT COUNT(*) FROM recipes_fulfillment WHERE recipe_id = rf.recipe_id AND need_fulfilled = 0 AND recipe_pos_id IS NOT NULL) AS missing_products_count | ||||
| FROM recipes r | ||||
| LEFT JOIN recipes_fulfillment rf | ||||
| 	ON rf.recipe_id = r.id | ||||
| GROUP BY r.id; | ||||
							
								
								
									
										20
									
								
								migrations/0026.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								migrations/0026.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| CREATE TABLE users ( | ||||
| 	id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, | ||||
| 	username TEXT NOT NULL UNIQUE, | ||||
| 	first_name TEXT, | ||||
| 	last_name TEXT, | ||||
| 	password TEXT NOT NULL, | ||||
| 	row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime')) | ||||
| ); | ||||
|  | ||||
| DROP TABLE sessions; | ||||
|  | ||||
| CREATE TABLE sessions ( | ||||
| 	id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, | ||||
| 	session_key TEXT NOT NULL UNIQUE, | ||||
| 	user_id INTEGER NOT NULL, | ||||
| 	expires DATETIME, | ||||
| 	last_used DATETIME, | ||||
| 	row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime')) | ||||
| ) | ||||
|  | ||||
							
								
								
									
										24
									
								
								migrations/0027.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								migrations/0027.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| <?php | ||||
|  | ||||
| // This is executed inside DatabaseMigrationService class/context | ||||
|  | ||||
| $db = $this->DatabaseService->GetDbConnection(); | ||||
|  | ||||
| if (defined('GROCY_HTTP_USER')) | ||||
| { | ||||
| 	// Migrate old user defined in config file to database | ||||
| 	$newUserRow = $db->users()->createRow(array( | ||||
| 		'username' => GROCY_HTTP_USER, | ||||
| 		'password' => password_hash(GROCY_HTTP_PASSWORD, PASSWORD_DEFAULT) | ||||
| 	)); | ||||
| 	$newUserRow->save(); | ||||
| } | ||||
| else | ||||
| { | ||||
| 	// Create default user "admin" with password "admin" | ||||
| 	$newUserRow = $db->users()->createRow(array( | ||||
| 		'username' => 'admin', | ||||
| 		'password' => password_hash('admin', PASSWORD_DEFAULT) | ||||
| 	)); | ||||
| 	$newUserRow->save(); | ||||
| } | ||||
							
								
								
									
										13
									
								
								migrations/0028.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								migrations/0028.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| ALTER TABLE habits_log | ||||
| ADD done_by_user_id INTEGER; | ||||
|  | ||||
| DROP TABLE api_keys; | ||||
|  | ||||
| CREATE TABLE api_keys ( | ||||
| 	id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, | ||||
| 	api_key TEXT NOT NULL UNIQUE, | ||||
| 	user_id INTEGER NOT NULL, | ||||
| 	expires DATETIME, | ||||
| 	last_used DATETIME, | ||||
| 	row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime')) | ||||
| ); | ||||
							
								
								
									
										5
									
								
								migrations/0029.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								migrations/0029.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| ALTER TABLE stock | ||||
| ADD price DECIMAL(15, 2); | ||||
|  | ||||
| ALTER TABLE stock_log | ||||
| ADD price DECIMAL(15, 2); | ||||
							
								
								
									
										2
									
								
								migrations/0030.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								migrations/0030.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| ALTER TABLE quantity_units | ||||
| ADD name_plural TEXT; | ||||
							
								
								
									
										32
									
								
								migrations/0031.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								migrations/0031.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| <?php | ||||
|  | ||||
| // This is executed inside DatabaseMigrationService class/context | ||||
|  | ||||
| use \Grocy\Services\LocalizationService; | ||||
| $localizationService = new LocalizationService(GROCY_CULTURE); | ||||
|  | ||||
| $db = $this->DatabaseService->GetDbConnection(); | ||||
|  | ||||
| if ($db->quantity_units()->count() === 0) | ||||
| { | ||||
| 	// Create 2 default quantity units | ||||
| 	$newRow = $db->quantity_units()->createRow(array( | ||||
| 		'name' => $localizationService->Localize('Piece'), | ||||
| 		'name_plural' => $localizationService->Localize('Pieces') | ||||
| 	)); | ||||
| 	$newRow->save(); | ||||
| 	$newRow = $db->quantity_units()->createRow(array( | ||||
| 		'name' => $localizationService->Localize('Pack'), | ||||
| 		'name_plural' => $localizationService->Localize('Packs') | ||||
| 	)); | ||||
| 	$newRow->save(); | ||||
| } | ||||
|  | ||||
| if ($db->locations()->count() === 0) | ||||
| { | ||||
| 	// Create a default location | ||||
| 	$newRow = $db->locations()->createRow(array( | ||||
| 		'name' => $localizationService->Localize('Fridge') | ||||
| 	)); | ||||
| 	$newRow->save(); | ||||
| } | ||||
							
								
								
									
										20
									
								
								migrations/0032.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								migrations/0032.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| 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; | ||||
|  | ||||
| DROP VIEW habits_current; | ||||
| CREATE VIEW habits_current | ||||
| AS | ||||
| SELECT habit_id, MAX(tracked_time) AS last_tracked_time | ||||
| FROM habits_log | ||||
| GROUP BY habit_id; | ||||
|  | ||||
| DROP VIEW batteries_current; | ||||
| CREATE VIEW batteries_current | ||||
| AS | ||||
| SELECT battery_id, MAX(tracked_time) AS last_tracked_time | ||||
| FROM battery_charge_cycles | ||||
| GROUP BY battery_id; | ||||
							
								
								
									
										29
									
								
								migrations/0033.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								migrations/0033.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | ||||
| DROP VIEW habits_current; | ||||
| CREATE VIEW habits_current | ||||
| AS | ||||
| SELECT | ||||
| 	h.id AS habit_id, | ||||
| 	MAX(l.tracked_time) AS last_tracked_time, | ||||
| 	CASE h.period_type | ||||
| 		WHEN 'manually' THEN '2999-12-31 23:59:59' | ||||
| 		WHEN 'dynamic-regular' THEN datetime(MAX(l.tracked_time), '+' || CAST(h.period_days AS TEXT) || ' day') | ||||
| 	END AS next_estimated_execution_time | ||||
| FROM habits h | ||||
| LEFT JOIN habits_log l | ||||
| 	ON h.id = l.habit_id | ||||
| GROUP BY h.id, h.period_days; | ||||
|  | ||||
| DROP VIEW batteries_current; | ||||
| CREATE VIEW batteries_current | ||||
| AS | ||||
| SELECT | ||||
| 	b.id AS battery_id, | ||||
| 	MAX(l.tracked_time) AS last_tracked_time, | ||||
| 	CASE WHEN b.charge_interval_days = 0 | ||||
| 		THEN '2999-12-31 23:59:59' | ||||
| 		ELSE datetime(MAX(l.tracked_time), '+' || CAST(b.charge_interval_days AS TEXT) || ' day') | ||||
| 	END AS next_estimated_charge_time | ||||
| FROM batteries b | ||||
| LEFT JOIN battery_charge_cycles l | ||||
| 	ON b.id = l.battery_id | ||||
| GROUP BY b.id, b.charge_interval_days; | ||||
							
								
								
									
										41
									
								
								migrations/0034.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								migrations/0034.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | ||||
| ALTER TABLE recipes_pos | ||||
| ADD qu_id INTEGER; | ||||
|  | ||||
| UPDATE recipes_pos | ||||
| SET qu_id = (SELECT qu_id_stock FROM products where id = product_id); | ||||
|  | ||||
| CREATE TRIGGER recipes_pos_qu_id_default AFTER INSERT ON recipes_pos | ||||
| BEGIN | ||||
| 	UPDATE recipes_pos | ||||
| 	SET qu_id = (SELECT qu_id_stock FROM products where id = product_id) | ||||
| 	WHERE qu_id IS NULL | ||||
| 		AND id = NEW.id; | ||||
| END; | ||||
|  | ||||
| ALTER TABLE recipes_pos | ||||
| ADD only_check_single_unit_in_stock TINYINT NOT NULL DEFAULT 0; | ||||
|  | ||||
| DROP VIEW recipes_fulfillment; | ||||
| CREATE VIEW recipes_fulfillment | ||||
| AS | ||||
| SELECT | ||||
| 	r.id AS recipe_id, | ||||
| 	rp.id AS recipe_pos_id, | ||||
| 	rp.product_id AS product_id, | ||||
| 	rp.amount AS recipe_amount, | ||||
| 	IFNULL(sc.amount, 0) AS stock_amount, | ||||
| 	CASE WHEN IFNULL(sc.amount, 0) >= CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 1 ELSE IFNULL(rp.amount, 0) END THEN 1 ELSE 0 END AS need_fulfilled, | ||||
| 	CASE WHEN IFNULL(sc.amount, 0) - CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 1 ELSE IFNULL(rp.amount, 0) END < 0 THEN ABS(IFNULL(sc.amount, 0) - CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 1 ELSE IFNULL(rp.amount, 0) END) ELSE 0 END AS missing_amount, | ||||
| 	IFNULL(sl.amount, 0) AS amount_on_shopping_list, | ||||
| 	CASE WHEN IFNULL(sc.amount, 0) + IFNULL(sl.amount, 0) >= CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 1 ELSE IFNULL(rp.amount, 0) END THEN 1 ELSE 0 END AS need_fulfilled_with_shopping_list, | ||||
| 	rp.qu_id | ||||
| FROM recipes r | ||||
| JOIN recipes_pos rp | ||||
| 	ON r.id = rp.recipe_id | ||||
| LEFT JOIN ( | ||||
| 	SELECT product_id, SUM(amount + amount_autoadded) AS amount | ||||
| 	FROM shopping_list | ||||
| 	GROUP BY product_id) sl | ||||
| 	ON rp.product_id = sl.product_id | ||||
| LEFT JOIN stock_current sc | ||||
| 	ON rp.product_id = sc.product_id; | ||||
							
								
								
									
										31
									
								
								migrations/0035.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								migrations/0035.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | ||||
| ALTER TABLE habits RENAME TO chores; | ||||
|  | ||||
| CREATE TABLE chores_log ( | ||||
| 	id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, | ||||
| 	chore_id INTEGER NOT NULL, | ||||
| 	tracked_time DATETIME, | ||||
| 	done_by_user_id INTEGER, | ||||
| 	row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime')) | ||||
| ); | ||||
|  | ||||
| INSERT INTO chores_log | ||||
| 	(chore_id, tracked_time, done_by_user_id, row_created_timestamp) | ||||
| SELECT habit_id, tracked_time, done_by_user_id, row_created_timestamp | ||||
| FROM habits_log; | ||||
|  | ||||
| DROP TABLE habits_log; | ||||
|  | ||||
| DROP VIEW habits_current; | ||||
| CREATE VIEW chores_current | ||||
| AS | ||||
| SELECT | ||||
| 	h.id AS chore_id, | ||||
| 	MAX(l.tracked_time) AS last_tracked_time, | ||||
| 	CASE h.period_type | ||||
| 		WHEN 'manually' THEN '2999-12-31 23:59:59' | ||||
| 		WHEN 'dynamic-regular' THEN datetime(MAX(l.tracked_time), '+' || CAST(h.period_days AS TEXT) || ' day') | ||||
| 	END AS next_estimated_execution_time | ||||
| FROM chores h | ||||
| LEFT JOIN chores_log l | ||||
| 	ON h.id = l.chore_id | ||||
| GROUP BY h.id, h.period_days; | ||||
							
								
								
									
										24
									
								
								migrations/0036.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								migrations/0036.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| CREATE TABLE tasks ( | ||||
| 	id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, | ||||
| 	name TEXT NOT NULL UNIQUE, | ||||
| 	description TEXT, | ||||
| 	due_date DATETIME, | ||||
| 	done TINYINT NOT NULL DEFAULT 0 CHECK(done IN (0, 1)), | ||||
| 	done_timestamp DATETIME, | ||||
| 	category_id INTEGER, | ||||
| 	assigned_to_user_id INTEGER, | ||||
| 	row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime')) | ||||
| ); | ||||
|  | ||||
| CREATE TABLE task_categories ( | ||||
| 	id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, | ||||
| 	name TEXT NOT NULL UNIQUE, | ||||
| 	description TEXT, | ||||
| 	row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime')) | ||||
| ); | ||||
|  | ||||
| CREATE VIEW tasks_current | ||||
| AS | ||||
| SELECT * | ||||
| FROM tasks | ||||
| WHERE done = 0; | ||||
							
								
								
									
										9
									
								
								migrations/0037.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								migrations/0037.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| ALTER TABLE products | ||||
| ADD product_group_id INTEGER; | ||||
|  | ||||
| CREATE TABLE product_groups ( | ||||
| 	id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, | ||||
| 	name TEXT NOT NULL UNIQUE, | ||||
| 	description TEXT, | ||||
| 	row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime')) | ||||
| ); | ||||
							
								
								
									
										10
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								package.json
									
									
									
									
									
								
							| @@ -2,16 +2,22 @@ | ||||
| 	"name": "grocy", | ||||
| 	"private": true, | ||||
| 	"dependencies": { | ||||
| 		"@danielfarrell/bootstrap-combobox": "https://github.com/pallidus-fintech/bootstrap-combobox.git#enhance/boostrap_4", | ||||
| 		"@danielfarrell/bootstrap-combobox": "https://github.com/berrnd/bootstrap-combobox.git#master", | ||||
| 		"@fortawesome/fontawesome-free": "^5.1.0", | ||||
| 		"TagManager": "https://github.com/max-favilli/tagmanager.git#3.0.2", | ||||
| 		"bootbox": "https://github.com/makeusabrew/bootbox.git#v5.x", | ||||
| 		"bootstrap": "^4.1.1", | ||||
| 		"bootstrap-side-navbar": "https://github.com/samrayner/bootstrap-side-navbar.git#1.0.1", | ||||
| 		"chart.js": "^2.7.2", | ||||
| 		"datatables.net": "^1.10.19", | ||||
| 		"datatables.net-bs4": "^1.10.19", | ||||
| 		"datatables.net-colreorder": "^1.5.1", | ||||
| 		"datatables.net-colreorder-bs4": "^1.5.1", | ||||
| 		"datatables.net-responsive": "^2.2.3", | ||||
| 		"datatables.net-responsive-bs4": "^2.2.3", | ||||
| 		"datatables.net-rowgroup": "^1.0.4", | ||||
| 		"datatables.net-rowgroup-bs4": "^1.0.4", | ||||
| 		"datatables.net-select": "^1.2.7", | ||||
| 		"datatables.net-select-bs4": "^1.2.7", | ||||
| 		"jquery": "^3.3.1", | ||||
| 		"jquery-serializejson": "^2.8.1", | ||||
| 		"jquery-ui-dist": "^1.12.1", | ||||
|   | ||||
| @@ -11,10 +11,6 @@ body { | ||||
|     white-space: normal; | ||||
| } | ||||
|  | ||||
| .no-real-button { | ||||
| 	pointer-events: none; | ||||
| } | ||||
|  | ||||
| .timeago-contextual { | ||||
| 	font-style: italic; | ||||
| 	font-size: 0.8em; | ||||
| @@ -45,10 +41,45 @@ a.discrete-link:focus { | ||||
| 	background-color: #e5e5e5; | ||||
| } | ||||
|  | ||||
| .card-body { | ||||
| 	flex-grow: 0; | ||||
| } | ||||
|  | ||||
| .content-text .invalid-feedback { | ||||
| 	font-size: 95%; | ||||
| } | ||||
|  | ||||
| .fullscreen { | ||||
| 	z-index: 9999;  | ||||
| 	width: 100%;  | ||||
| 	height: 100%;  | ||||
| 	position: fixed;  | ||||
| 	top: 0;  | ||||
| 	left: 0;  | ||||
|  } | ||||
|  | ||||
|  .form-check-input.is-valid ~ .form-check-label, | ||||
|  .was-validated .form-check-input:valid ~ .form-check-label { | ||||
|     color: inherit; | ||||
| } | ||||
|  | ||||
| .text-strike-through { | ||||
| 	text-decoration: line-through; | ||||
| } | ||||
|  | ||||
| button.disabled { | ||||
| 	pointer-events: none; | ||||
| } | ||||
|  | ||||
| /* Hide the default up/down arrow buttons for number inputs because we use our own buttons in numberpicker */ | ||||
| input[type='number'] { | ||||
| 	-moz-appearance: textfield; | ||||
| } | ||||
| input::-webkit-outer-spin-button, | ||||
| input::-webkit-inner-spin-button { | ||||
| 	-webkit-appearance: none; | ||||
| } | ||||
|  | ||||
| /* Navigation style customizations */ | ||||
| #mainNav { | ||||
| 	background-color: #e5e5e5 !important; | ||||
| @@ -57,14 +88,13 @@ a.discrete-link:focus { | ||||
| } | ||||
|  | ||||
| .navbar-sidenav { | ||||
| 	overflow-y: auto; | ||||
| 	overflow-x: hidden; | ||||
| 	overflow: hidden; | ||||
| 	border-top: 2px solid !important; | ||||
| } | ||||
|  | ||||
| .navbar-sidenav, | ||||
| .sidenav-second-level { | ||||
| 	background-color: #e5e5e5 !important; | ||||
| 	border-top: 2px solid !important; | ||||
| 	border-right: 2px solid !important; | ||||
| 	border-color: #d6d6d6 !important; | ||||
| } | ||||
| @@ -156,3 +186,18 @@ td { | ||||
| 		padding: 0.8em; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| /* Third party component customizations - Tempus Dominus */ | ||||
| .date-only-datetimepicker .bootstrap-datetimepicker-widget.dropdown-menu { | ||||
| 	width: auto !important; | ||||
| } | ||||
|  | ||||
| /* Third party component customizations - Bootstrap Combobox */ | ||||
| .typeahead .active { | ||||
| 	background-color: #e5e5e5; | ||||
| } | ||||
|  | ||||
| /* Third party component customizations - Popper.js */ | ||||
| .tooltip { | ||||
| 	pointer-events: none; | ||||
| } | ||||
|   | ||||
| @@ -31,3 +31,13 @@ GetUriParam = function(key) | ||||
| 		} | ||||
| 	} | ||||
| }; | ||||
|  | ||||
| IsTouchInputDevice = function() | ||||
| { | ||||
| 	if (("ontouchstart" in window) || window.DocumentTouch && document instanceof DocumentTouch) | ||||
| 	{ | ||||
| 		return true; | ||||
| 	} | ||||
|  | ||||
| 	return false; | ||||
| } | ||||
|   | ||||
| @@ -19,16 +19,64 @@ U = function(relativePath) | ||||
| 	return Grocy.BaseUrl.replace(/\/$/, '') + relativePath; | ||||
| } | ||||
|  | ||||
| Pluralize = function(number, singularForm, pluralForm) | ||||
| { | ||||
| 	var text = singularForm; | ||||
| 	if (number != 1 && pluralForm !== null && !pluralForm.isEmpty()) | ||||
| 	{ | ||||
| 		text = pluralForm; | ||||
| 	} | ||||
| 	return text; | ||||
| } | ||||
|  | ||||
| if (!Grocy.ActiveNav.isEmpty()) | ||||
| { | ||||
| 	var menuItem = $('#sidebarResponsive').find("[data-nav-for-page='" + Grocy.ActiveNav + "']"); | ||||
| 	menuItem.addClass('active-page'); | ||||
| }	 | ||||
|  | ||||
| 	var parentMenuSelector = menuItem.data("sub-menu-of"); | ||||
| 	if (typeof parentMenuSelector !== "undefined") | ||||
| 	{ | ||||
| 		$(parentMenuSelector).collapse("show"); | ||||
| 		$(parentMenuSelector).prev(".nav-link-collapse").addClass("active-page"); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| var observer = new MutationObserver(function(mutations) | ||||
| { | ||||
| 	mutations.forEach(function(mutation) | ||||
| 	{ | ||||
| 		if (mutation.attributeName === "class") | ||||
| 		{ | ||||
| 			var attributeValue = $(mutation.target).prop(mutation.attributeName); | ||||
| 			if (attributeValue.contains("sidenav-toggled")) | ||||
| 			{ | ||||
| 				window.localStorage.setItem("sidebar_state", "collapsed"); | ||||
| 			} | ||||
| 			else | ||||
| 			{ | ||||
| 				window.localStorage.setItem("sidebar_state", "expanded"); | ||||
| 			} | ||||
| 		} | ||||
| 	}); | ||||
| }); | ||||
| observer.observe(document.body, { | ||||
| 	attributes: true | ||||
| }); | ||||
| if (window.localStorage.getItem("sidebar_state") === "collapsed") | ||||
| { | ||||
| 	$("#sidenavToggler").click(); | ||||
| } | ||||
|  | ||||
| $.timeago.settings.allowFuture = true; | ||||
| RefreshContextualTimeago = function() | ||||
| {	 | ||||
| 	$('time.timeago').timeago(); | ||||
| 	$("time.timeago").each(function() | ||||
| 	{ | ||||
| 		var element = $(this); | ||||
| 		var timestamp = element.attr("datetime"); | ||||
| 		element.timeago("update", timestamp); | ||||
| 	}); | ||||
| } | ||||
| RefreshContextualTimeago(); | ||||
|  | ||||
| @@ -43,6 +91,14 @@ window.FontAwesomeConfig = { | ||||
| 	searchPseudoElements: true | ||||
| } | ||||
|  | ||||
| // Don't show tooltips on touch input devices | ||||
| if (IsTouchInputDevice()) | ||||
| { | ||||
| 	var css = document.createElement("style"); | ||||
| 	css.innerHTML = ".tooltip { display: none; }"; | ||||
| 	document.body.appendChild(css); | ||||
| } | ||||
|  | ||||
| Grocy.Api = { }; | ||||
| Grocy.Api.Get = function(apiFunction, success, error) | ||||
| { | ||||
| @@ -120,3 +176,18 @@ Grocy.FrontendHelpers.ValidateForm = function(formId) | ||||
|  | ||||
| 	$(form).addClass('was-validated'); | ||||
| } | ||||
|  | ||||
| Grocy.FrontendHelpers.ShowGenericError = function(message, exception) | ||||
| { | ||||
| 	toastr.error(L(message) + '<br><br>' + L('Click to show technical details'), '', { | ||||
| 		onclick: function() | ||||
| 		{ | ||||
| 			bootbox.alert({ | ||||
| 				title: L('Error details'), | ||||
| 				message: JSON.stringify(exception, null, 4) | ||||
| 			}); | ||||
| 		} | ||||
| 	}); | ||||
| 	 | ||||
| 	console.error(exception); | ||||
| } | ||||
|   | ||||
							
								
								
									
										53
									
								
								public/js/grocy_dbchangedhandling.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								public/js/grocy_dbchangedhandling.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | ||||
| Grocy.Api.Get('system/get-db-changed-time', | ||||
| 	function(result) | ||||
| 	{ | ||||
| 		Grocy.DatabaseChangedTime = moment(result.changed_time); | ||||
| 	}, | ||||
| 	function(xhr) | ||||
| 	{ | ||||
| 		console.error(xhr); | ||||
| 	} | ||||
| ); | ||||
|  | ||||
| // Check if the database has changed once a minute | ||||
| // If a change is detected, reload the current page, but only if already idling for at least 50 seconds | ||||
| setInterval(function() | ||||
| { | ||||
| 	Grocy.Api.Get('system/get-db-changed-time', | ||||
| 		function(result) | ||||
| 		{ | ||||
| 			var newDbChangedTime = moment(result.changed_time); | ||||
| 			if (newDbChangedTime.isAfter(Grocy.DatabaseChangedTime)) | ||||
| 			{ | ||||
| 				if (Grocy.IdleTime >= 50) | ||||
| 				{ | ||||
| 					window.location.reload(); | ||||
| 				} | ||||
|  | ||||
| 				Grocy.DatabaseChangedTime = newDbChangedTime; | ||||
| 			} | ||||
| 		}, | ||||
| 		function(xhr) | ||||
| 		{ | ||||
| 			console.error(xhr); | ||||
| 		} | ||||
| 	); | ||||
| }, 60000); | ||||
|  | ||||
| Grocy.IdleTime = 0; | ||||
| Grocy.ResetIdleTime = function() | ||||
| { | ||||
| 	Grocy.IdleTime = 0; | ||||
| } | ||||
| window.onmousemove = Grocy.ResetIdleTime; | ||||
| window.onmousedown = Grocy.ResetIdleTime; | ||||
| window.onclick = Grocy.ResetIdleTime; | ||||
| window.onscroll = Grocy.ResetIdleTime; | ||||
| window.onkeypress = Grocy.ResetIdleTime; | ||||
|  | ||||
| // Increase the idle time once every second | ||||
| // On any interaction it will be reset to 0 (see above) | ||||
| setInterval(function() | ||||
| { | ||||
| 	Grocy.IdleTime += 1; | ||||
| }, 1000); | ||||
| @@ -5,7 +5,18 @@ | ||||
| 		{ 'orderable': false, 'targets': 0 } | ||||
| 	], | ||||
| 	'language': JSON.parse(L('datatables_localization')), | ||||
| 	'scrollY': false | ||||
| 	'scrollY': false, | ||||
| 	'colReorder': true, | ||||
| 	'stateSave': true, | ||||
| 	'stateSaveParams': function(settings, data) | ||||
| 	{ | ||||
| 		data.search.search = ""; | ||||
|  | ||||
| 		data.columns.forEach(column => | ||||
| 		{ | ||||
| 			column.search.search = ""; | ||||
| 		}); | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| $("#search").on("keyup", function() | ||||
|   | ||||
| @@ -5,7 +5,18 @@ | ||||
| 		{ 'orderable': false, 'targets': 0 } | ||||
| 	], | ||||
| 	'language': JSON.parse(L('datatables_localization')), | ||||
| 	'scrollY': false | ||||
| 	'scrollY': false, | ||||
| 	'colReorder': true, | ||||
| 	'stateSave': true, | ||||
| 	'stateSaveParams': function(settings, data) | ||||
| 	{ | ||||
| 		data.search.search = ""; | ||||
|  | ||||
| 		data.columns.forEach(column => | ||||
| 		{ | ||||
| 			column.search.search = ""; | ||||
| 		}); | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| $("#search").on("keyup", function() | ||||
| @@ -19,23 +30,87 @@ $("#search").on("keyup", function() | ||||
| 	batteriesOverviewTable.search(value).draw(); | ||||
| }); | ||||
|  | ||||
| $("#status-filter").on("change", function() | ||||
| { | ||||
| 	var value = $(this).val(); | ||||
| 	if (value === "all") | ||||
| 	{ | ||||
| 		value = ""; | ||||
| 	} | ||||
|  | ||||
| 	// Transfer CSS classes of selected element to dropdown element (for background) | ||||
| 	$(this).attr("class", $("#" + $(this).attr("id") + " option[value='" + value + "']").attr("class") + " form-control"); | ||||
| 	 | ||||
| 	batteriesOverviewTable.column(4).search(value).draw(); | ||||
| }); | ||||
|  | ||||
| $(".status-filter-button").on("click", function() | ||||
| { | ||||
| 	var value = $(this).data("status-filter"); | ||||
| 	$("#status-filter").val(value); | ||||
| 	$("#status-filter").trigger("change"); | ||||
| }); | ||||
|  | ||||
| $(document).on('click', '.track-charge-cycle-button', function(e) | ||||
| { | ||||
| 	e.preventDefault(); | ||||
|  | ||||
| 	// Remove the focus from the current button | ||||
| 	// to prevent that the tooltip stays until clicked anywhere else | ||||
| 	document.activeElement.blur(); | ||||
| 	 | ||||
| 	var batteryId = $(e.currentTarget).attr('data-battery-id'); | ||||
| 	var batteryName = $(e.currentTarget).attr('data-battery-name'); | ||||
| 	var trackedTime = moment().format('YYYY-MM-DD HH:mm:ss'); | ||||
|  | ||||
| 	Grocy.Api.Get('batteries/track-charge-cycle/' + batteryId + '?tracked_time=' + trackedTime, | ||||
| 		function(result) | ||||
| 		function() | ||||
| 		{ | ||||
| 			$('#battery-' + batteryId + '-last-tracked-time').parent().effect('highlight', {}, 500); | ||||
| 			$('#battery-' + batteryId + '-last-tracked-time').fadeOut(500, function () { | ||||
| 				$(this).text(trackedTime).fadeIn(500); | ||||
| 			}); | ||||
| 			$('#battery-' + batteryId + '-last-tracked-time-timeago').attr('datetime', trackedTime); | ||||
| 			RefreshContextualTimeago(); | ||||
| 			Grocy.Api.Get('batteries/get-battery-details/' + batteryId, | ||||
| 				function(result) | ||||
| 				{ | ||||
| 					var batteryRow = $('#battery-' + batteryId + '-row'); | ||||
| 					var nextXDaysThreshold = moment().add($("#info-due-batteries").data("next-x-days"), "days"); | ||||
| 					var now = moment(); | ||||
| 					var nextExecutionTime = moment(result.next_estimated_charge_time); | ||||
|  | ||||
| 			toastr.success(L('Tracked charge cylce of battery #1 on #2', batteryName, trackedTime)); | ||||
| 					batteryRow.removeClass("table-warning"); | ||||
| 					batteryRow.removeClass("table-danger"); | ||||
| 					if (nextExecutionTime.isBefore(now)) | ||||
| 					{ | ||||
| 						batteryRow.addClass("table-danger"); | ||||
| 					} | ||||
| 					else if (nextExecutionTime.isBefore(nextXDaysThreshold)) | ||||
| 					{ | ||||
| 						batteryRow.addClass("table-warning"); | ||||
| 					} | ||||
|  | ||||
| 					$('#battery-' + batteryId + '-last-tracked-time').parent().effect('highlight', { }, 500); | ||||
| 					$('#battery-' + batteryId + '-last-tracked-time').fadeOut(500, function() | ||||
| 					{ | ||||
| 						$(this).text(trackedTime).fadeIn(500); | ||||
| 					}); | ||||
| 					$('#battery-' + batteryId + '-last-tracked-time-timeago').attr('datetime', trackedTime); | ||||
|  | ||||
| 					if (result.battery.charge_interval_days != 0) | ||||
| 					{ | ||||
| 						$('#battery-' + batteryId + '-next-charge-time').parent().effect('highlight', { }, 500); | ||||
| 						$('#battery-' + batteryId + '-next-charge-time').fadeOut(500, function() | ||||
| 						{ | ||||
| 							$(this).text(result.next_estimated_charge_time).fadeIn(500); | ||||
| 						}); | ||||
| 						$('#battery-' + batteryId + '-next-charge-time-timeago').attr('datetime', result.next_estimated_charge_time); | ||||
| 					} | ||||
|  | ||||
| 					toastr.success(L('Tracked charge cycle of battery #1 on #2', batteryName, trackedTime)); | ||||
| 					RefreshContextualTimeago(); | ||||
| 					RefreshStatistics(); | ||||
| 				}, | ||||
| 				function(xhr) | ||||
| 				{ | ||||
| 					console.error(xhr); | ||||
| 				} | ||||
| 			); | ||||
| 		}, | ||||
| 		function(xhr) | ||||
| 		{ | ||||
| @@ -43,3 +118,37 @@ $(document).on('click', '.track-charge-cycle-button', function(e) | ||||
| 		} | ||||
| 	); | ||||
| }); | ||||
|  | ||||
| function RefreshStatistics() | ||||
| { | ||||
| 	var nextXDays = $("#info-due-batteries").data("next-x-days"); | ||||
| 	Grocy.Api.Get('batteries/get-current', | ||||
| 		function(result) | ||||
| 		{ | ||||
| 			var dueCount = 0; | ||||
| 			var overdueCount = 0; | ||||
| 			var now = moment(); | ||||
| 			var nextXDaysThreshold = moment().add(nextXDays, "days"); | ||||
| 			result.forEach(element => { | ||||
| 				var date = moment(element.next_estimated_charge_time); | ||||
| 				if (date.isBefore(now)) | ||||
| 				{ | ||||
| 					overdueCount++; | ||||
| 				} | ||||
| 				else if (date.isBefore(nextXDaysThreshold)) | ||||
| 				{ | ||||
| 					dueCount++; | ||||
| 				} | ||||
| 			}); | ||||
| 			 | ||||
| 			$("#info-due-batteries").text(Pluralize(dueCount, L('#1 battery is due to be charged within the next #2 days', dueCount, nextXDays), L('#1 batteries are due to be charged within the next #2 days', dueCount, nextXDays))); | ||||
| 			$("#info-overdue-batteries").text(Pluralize(overdueCount, L('#1 battery is overdue to be charged', overdueCount), L('#1 batteries are overdue to be charged', overdueCount))); | ||||
| 		}, | ||||
| 		function(xhr) | ||||
| 		{ | ||||
| 			console.error(xhr); | ||||
| 		} | ||||
| 	); | ||||
| } | ||||
|  | ||||
| RefreshStatistics(); | ||||
|   | ||||
| @@ -11,7 +11,7 @@ | ||||
| 			}, | ||||
| 			function(xhr) | ||||
| 			{ | ||||
| 				console.error(xhr); | ||||
| 				Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response) | ||||
| 			} | ||||
| 		); | ||||
| 	} | ||||
| @@ -24,7 +24,7 @@ | ||||
| 			}, | ||||
| 			function(xhr) | ||||
| 			{ | ||||
| 				console.error(xhr); | ||||
| 				Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response) | ||||
| 			} | ||||
| 		); | ||||
| 	} | ||||
| @@ -39,9 +39,10 @@ $('#battery-form input').keydown(function(event) | ||||
| { | ||||
| 	if (event.keyCode === 13) //Enter | ||||
| 	{ | ||||
| 		event.preventDefault(); | ||||
| 		 | ||||
| 		if (document.getElementById('battery-form').checkValidity() === false) //There is at least one validation error | ||||
| 		{ | ||||
| 			event.preventDefault(); | ||||
| 			return false; | ||||
| 		} | ||||
| 		else | ||||
|   | ||||
| @@ -35,17 +35,22 @@ | ||||
|  | ||||
| $('#battery_id').on('change', function(e) | ||||
| { | ||||
| 	var batteryId = $(e.target).val(); | ||||
| 	var input = $('#battery_id_text_input').val().toString(); | ||||
| 	$('#battery_id_text_input').val(input); | ||||
| 	$('#battery_id').data('combobox').refresh(); | ||||
|  | ||||
| 	var batteryId = $(e.target).val(); | ||||
| 	if (batteryId) | ||||
| 	{ | ||||
| 		Grocy.Components.BatteryCard.Refresh(batteryId); | ||||
| 		$('#tracked_time').find('input').focus(); | ||||
| 		Grocy.FrontendHelpers.ValidateForm('batterytracking-form'); | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| $('.combobox').combobox({ | ||||
| 	appendId: '_text_input' | ||||
| 	appendId: '_text_input', | ||||
| 	bsVersion: '4' | ||||
| }); | ||||
|  | ||||
| $('#battery_id').val(''); | ||||
| @@ -63,9 +68,10 @@ $('#batterytracking-form input').keydown(function(event) | ||||
| { | ||||
| 	if (event.keyCode === 13) //Enter | ||||
| 	{ | ||||
| 		event.preventDefault(); | ||||
| 		 | ||||
| 		if (document.getElementById('batterytracking-form').checkValidity() === false) //There is at least one validation error | ||||
| 		{ | ||||
| 			event.preventDefault(); | ||||
| 			return false; | ||||
| 		} | ||||
| 		else | ||||
|   | ||||
							
								
								
									
										72
									
								
								public/viewjs/choreform.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								public/viewjs/choreform.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,72 @@ | ||||
| $('#save-chore-button').on('click', function(e) | ||||
| { | ||||
| 	e.preventDefault(); | ||||
|  | ||||
| 	if (Grocy.EditMode === 'create') | ||||
| 	{ | ||||
| 		Grocy.Api.Post('add-object/chores', $('#chore-form').serializeJSON(), | ||||
| 			function(result) | ||||
| 			{ | ||||
| 				window.location.href = U('/chores'); | ||||
| 			}, | ||||
| 			function(xhr) | ||||
| 			{ | ||||
| 				Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response) | ||||
| 			} | ||||
| 		); | ||||
| 	} | ||||
| 	else | ||||
| 	{ | ||||
| 		Grocy.Api.Post('edit-object/chores/' + Grocy.EditObjectId, $('#chore-form').serializeJSON(), | ||||
| 			function(result) | ||||
| 			{ | ||||
| 				window.location.href = U('/chores'); | ||||
| 			}, | ||||
| 			function(xhr) | ||||
| 			{ | ||||
| 				Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response) | ||||
| 			} | ||||
| 		); | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| $('#chore-form input').keyup(function(event) | ||||
| { | ||||
| 	Grocy.FrontendHelpers.ValidateForm('chore-form'); | ||||
| }); | ||||
|  | ||||
| $('#chore-form input').keydown(function(event) | ||||
| { | ||||
| 	if (event.keyCode === 13) //Enter | ||||
| 	{ | ||||
| 		event.preventDefault(); | ||||
|  | ||||
| 		if (document.getElementById('chore-form').checkValidity() === false) //There is at least one validation error | ||||
| 		{ | ||||
| 			return false; | ||||
| 		} | ||||
| 		else | ||||
| 		{ | ||||
| 			$('#save-chore-button').click(); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| $('#name').focus(); | ||||
| Grocy.FrontendHelpers.ValidateForm('chore-form'); | ||||
|  | ||||
| $('.input-group-chore-period-type').on('change', function(e) | ||||
| { | ||||
| 	var periodType = $('#period_type').val(); | ||||
| 	var periodDays = $('#period_days').val(); | ||||
|  | ||||
| 	if (periodType === 'dynamic-regular') | ||||
| 	{ | ||||
| 		$('#chore-period-type-info').text(L('This means it is estimated that a new execution of this chore is tracked #1 days after the last was tracked', periodDays.toString())); | ||||
| 		$('#chore-period-type-info').removeClass('d-none'); | ||||
| 	} | ||||
| 	else | ||||
| 	{ | ||||
| 		$('#chore-period-type-info').addClass('d-none'); | ||||
| 	} | ||||
| }); | ||||
							
								
								
									
										67
									
								
								public/viewjs/chores.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								public/viewjs/chores.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,67 @@ | ||||
| var choresTable  = $('#chores-table').DataTable({ | ||||
| 	'paginate': false, | ||||
| 	'order': [[1, 'asc']], | ||||
| 	'columnDefs': [ | ||||
| 		{ 'orderable': false, 'targets': 0 } | ||||
| 	], | ||||
| 	'language': JSON.parse(L('datatables_localization')), | ||||
| 	'scrollY': false, | ||||
| 	'colReorder': true, | ||||
| 	'stateSave': true, | ||||
| 	'stateSaveParams': function(settings, data) | ||||
| 	{ | ||||
| 		data.search.search = ""; | ||||
|  | ||||
| 		data.columns.forEach(column => | ||||
| 		{ | ||||
| 			column.search.search = ""; | ||||
| 		}); | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| $("#search").on("keyup", function() | ||||
| { | ||||
| 	var value = $(this).val(); | ||||
| 	if (value === "all") | ||||
| 	{ | ||||
| 		value = ""; | ||||
| 	} | ||||
| 	 | ||||
| 	choresTable.search(value).draw(); | ||||
| }); | ||||
|  | ||||
| $(document).on('click', '.chore-delete-button', function (e) | ||||
| { | ||||
| 	var objectName = $(e.currentTarget).attr('data-chore-name'); | ||||
| 	var objectId = $(e.currentTarget).attr('data-chore-id'); | ||||
|  | ||||
| 	bootbox.confirm({ | ||||
| 		message: L('Are you sure to delete chore "#1"?', objectName), | ||||
| 		buttons: { | ||||
| 			confirm: { | ||||
| 				label: L('Yes'), | ||||
| 				className: 'btn-success' | ||||
| 			}, | ||||
| 			cancel: { | ||||
| 				label: L('No'), | ||||
| 				className: 'btn-danger' | ||||
| 			} | ||||
| 		}, | ||||
| 		callback: function(result) | ||||
| 		{ | ||||
| 			if (result === true) | ||||
| 			{ | ||||
| 				Grocy.Api.Get('delete-object/chores/' + objectId, | ||||
| 					function(result) | ||||
| 					{ | ||||
| 						window.location.href = U('/chores'); | ||||
| 					}, | ||||
| 					function(xhr) | ||||
| 					{ | ||||
| 						console.error(xhr); | ||||
| 					} | ||||
| 				); | ||||
| 			} | ||||
| 		} | ||||
| 	}); | ||||
| }); | ||||
							
								
								
									
										46
									
								
								public/viewjs/choresanalysis.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								public/viewjs/choresanalysis.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,46 @@ | ||||
| var choresAnalysisTable = $('#chores-analysis-table').DataTable({ | ||||
| 	'paginate': false, | ||||
| 	'order': [[1, 'desc']], | ||||
| 	'language': JSON.parse(L('datatables_localization')), | ||||
| 	'scrollY': false, | ||||
| 	'colReorder': true, | ||||
| 	'stateSave': true, | ||||
| 	'stateSaveParams': function(settings, data) | ||||
| 	{ | ||||
| 		data.search.search = ""; | ||||
|  | ||||
| 		data.columns.forEach(column => | ||||
| 		{ | ||||
| 			column.search.search = ""; | ||||
| 		}); | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| $("#chore-filter").on("change", function() | ||||
| { | ||||
| 	var value = $(this).val(); | ||||
| 	var text = $("#chore-filter option:selected").text(); | ||||
| 	if (value === "all") | ||||
| 	{ | ||||
| 		text = ""; | ||||
| 	} | ||||
| 	 | ||||
| 	choresAnalysisTable.column(0).search(text).draw(); | ||||
| }); | ||||
|  | ||||
| $("#search").on("keyup", function() | ||||
| { | ||||
| 	var value = $(this).val(); | ||||
| 	if (value === "all") | ||||
| 	{ | ||||
| 		value = ""; | ||||
| 	} | ||||
| 	 | ||||
| 	choresAnalysisTable.search(value).draw(); | ||||
| }); | ||||
|  | ||||
| if (typeof GetUriParam("chore") !== "undefined") | ||||
| { | ||||
| 	$("#chore-filter").val(GetUriParam("chore")); | ||||
| 	$("#chore-filter").trigger("change"); | ||||
| } | ||||
							
								
								
									
										154
									
								
								public/viewjs/choresoverview.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										154
									
								
								public/viewjs/choresoverview.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,154 @@ | ||||
| var choresOverviewTable = $('#chores-overview-table').DataTable({ | ||||
| 	'paginate': false, | ||||
| 	'order': [[2, 'desc']], | ||||
| 	'columnDefs': [ | ||||
| 		{ 'orderable': false, 'targets': 0 } | ||||
| 	], | ||||
| 	'language': JSON.parse(L('datatables_localization')), | ||||
| 	'scrollY': false, | ||||
| 	'colReorder': true, | ||||
| 	'stateSave': true, | ||||
| 	'stateSaveParams': function(settings, data) | ||||
| 	{ | ||||
| 		data.search.search = ""; | ||||
|  | ||||
| 		data.columns.forEach(column => | ||||
| 		{ | ||||
| 			column.search.search = ""; | ||||
| 		}); | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| $("#search").on("keyup", function() | ||||
| { | ||||
| 	var value = $(this).val(); | ||||
| 	if (value === "all") | ||||
| 	{ | ||||
| 		value = ""; | ||||
| 	} | ||||
| 	 | ||||
| 	choresOverviewTable.search(value).draw(); | ||||
| }); | ||||
|  | ||||
| $("#status-filter").on("change", function() | ||||
| { | ||||
| 	var value = $(this).val(); | ||||
| 	if (value === "all") | ||||
| 	{ | ||||
| 		value = ""; | ||||
| 	} | ||||
|  | ||||
| 	// Transfer CSS classes of selected element to dropdown element (for background) | ||||
| 	$(this).attr("class", $("#" + $(this).attr("id") + " option[value='" + value + "']").attr("class") + " form-control"); | ||||
| 	 | ||||
| 	choresOverviewTable.column(4).search(value).draw(); | ||||
| }); | ||||
|  | ||||
| $(".status-filter-button").on("click", function() | ||||
| { | ||||
| 	var value = $(this).data("status-filter"); | ||||
| 	$("#status-filter").val(value); | ||||
| 	$("#status-filter").trigger("change"); | ||||
| }); | ||||
|  | ||||
| $(document).on('click', '.track-chore-button', function(e) | ||||
| { | ||||
| 	e.preventDefault(); | ||||
|  | ||||
| 	// Remove the focus from the current button | ||||
| 	// to prevent that the tooltip stays until clicked anywhere else | ||||
| 	document.activeElement.blur(); | ||||
| 	 | ||||
| 	var choreId = $(e.currentTarget).attr('data-chore-id'); | ||||
| 	var choreName = $(e.currentTarget).attr('data-chore-name'); | ||||
| 	var trackedTime = moment().format('YYYY-MM-DD HH:mm:ss'); | ||||
|  | ||||
| 	Grocy.Api.Get('chores/track-chore-execution/' + choreId + '?tracked_time=' + trackedTime, | ||||
| 		function() | ||||
| 		{ | ||||
| 			Grocy.Api.Get('chores/get-chore-details/' + choreId, | ||||
| 				function(result) | ||||
| 				{ | ||||
| 					var choreRow = $('#chore-' + choreId + '-row'); | ||||
| 					var nextXDaysThreshold = moment().add($("#info-due-chores").data("next-x-days"), "days"); | ||||
| 					var now = moment(); | ||||
| 					var nextExecutionTime = moment(result.next_estimated_execution_time); | ||||
|  | ||||
| 					choreRow.removeClass("table-warning"); | ||||
| 					choreRow.removeClass("table-danger"); | ||||
| 					if (nextExecutionTime.isBefore(now)) | ||||
| 					{ | ||||
| 						choreRow.addClass("table-danger"); | ||||
| 					} | ||||
| 					else if (nextExecutionTime.isBefore(nextXDaysThreshold)) | ||||
| 					{ | ||||
| 						choreRow.addClass("table-warning"); | ||||
| 					} | ||||
|  | ||||
| 					$('#chore-' + choreId + '-last-tracked-time').parent().effect('highlight', { }, 500); | ||||
| 					$('#chore-' + choreId + '-last-tracked-time').fadeOut(500, function() | ||||
| 					{ | ||||
| 						$(this).text(trackedTime).fadeIn(500); | ||||
| 					}); | ||||
| 					$('#chore-' + choreId + '-last-tracked-time-timeago').attr('datetime', trackedTime); | ||||
|  | ||||
| 					if (result.chore.period_type == "dynamic-regular") | ||||
| 					{ | ||||
| 						$('#chore-' + choreId + '-next-execution-time').parent().effect('highlight', { }, 500); | ||||
| 						$('#chore-' + choreId + '-next-execution-time').fadeOut(500, function() | ||||
| 						{ | ||||
| 							$(this).text(result.next_estimated_execution_time).fadeIn(500); | ||||
| 						}); | ||||
| 						$('#chore-' + choreId + '-next-execution-time-timeago').attr('datetime', result.next_estimated_execution_time); | ||||
| 					} | ||||
|  | ||||
| 					toastr.success(L('Tracked execution of chore #1 on #2', choreName, trackedTime)); | ||||
| 					RefreshContextualTimeago(); | ||||
| 					RefreshStatistics(); | ||||
| 				}, | ||||
| 				function(xhr) | ||||
| 				{ | ||||
| 					console.error(xhr); | ||||
| 				} | ||||
| 			); | ||||
| 		}, | ||||
| 		function(xhr) | ||||
| 		{ | ||||
| 			console.error(xhr); | ||||
| 		} | ||||
| 	); | ||||
| }); | ||||
|  | ||||
| function RefreshStatistics() | ||||
| { | ||||
| 	var nextXDays = $("#info-due-chores").data("next-x-days"); | ||||
| 	Grocy.Api.Get('chores/get-current', | ||||
| 		function(result) | ||||
| 		{ | ||||
| 			var dueCount = 0; | ||||
| 			var overdueCount = 0; | ||||
| 			var now = moment(); | ||||
| 			var nextXDaysThreshold = moment().add(nextXDays, "days"); | ||||
| 			result.forEach(element => { | ||||
| 				var date = moment(element.next_estimated_execution_time); | ||||
| 				if (date.isBefore(now)) | ||||
| 				{ | ||||
| 					overdueCount++; | ||||
| 				} | ||||
| 				else if (date.isBefore(nextXDaysThreshold)) | ||||
| 				{ | ||||
| 					dueCount++; | ||||
| 				} | ||||
| 			}); | ||||
| 			 | ||||
| 			$("#info-due-chores").text(Pluralize(dueCount, L('#1 chore is due to be done within the next #2 days', dueCount, nextXDays), L('#1 chores are due to be done within the next #2 days', dueCount, nextXDays))); | ||||
| 			$("#info-overdue-chores").text(Pluralize(overdueCount, L('#1 chore is overdue to be done', overdueCount), L('#1 chores are overdue to be done', overdueCount))); | ||||
| 		}, | ||||
| 		function(xhr) | ||||
| 		{ | ||||
| 			console.error(xhr); | ||||
| 		} | ||||
| 	); | ||||
| } | ||||
|  | ||||
| RefreshStatistics(); | ||||
							
								
								
									
										84
									
								
								public/viewjs/choretracking.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								public/viewjs/choretracking.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,84 @@ | ||||
| $('#save-choretracking-button').on('click', function(e) | ||||
| { | ||||
| 	e.preventDefault(); | ||||
|  | ||||
| 	var jsonForm = $('#choretracking-form').serializeJSON(); | ||||
|  | ||||
| 	Grocy.Api.Get('chores/get-chore-details/' + jsonForm.chore_id, | ||||
| 		function (choreDetails) | ||||
| 		{ | ||||
| 			Grocy.Api.Get('chores/track-chore-execution/' + jsonForm.chore_id + '?tracked_time=' + Grocy.Components.DateTimePicker.GetValue() + "&done_by=" + Grocy.Components.UserPicker.GetValue(), | ||||
| 				function(result) | ||||
| 				{ | ||||
| 					toastr.success(L('Tracked execution of chore #1 on #2', choreDetails.chore.name, Grocy.Components.DateTimePicker.GetValue())); | ||||
|  | ||||
| 					$('#chore_id').val(''); | ||||
| 					$('#chore_id_text_input').focus(); | ||||
| 					$('#chore_id_text_input').val(''); | ||||
| 					Grocy.Components.DateTimePicker.SetValue(moment().format('YYYY-MM-DD HH:mm:ss')); | ||||
| 					$('#chore_id_text_input').trigger('change'); | ||||
| 					Grocy.FrontendHelpers.ValidateForm('choretracking-form'); | ||||
| 				}, | ||||
| 				function(xhr) | ||||
| 				{ | ||||
| 					console.error(xhr); | ||||
| 				} | ||||
| 			); | ||||
| 		}, | ||||
| 		function(xhr) | ||||
| 		{ | ||||
| 			console.error(xhr); | ||||
| 		} | ||||
| 	); | ||||
| }); | ||||
|  | ||||
| $('#chore_id').on('change', function(e) | ||||
| { | ||||
| 	var input = $('#chore_id_text_input').val().toString(); | ||||
| 	$('#chore_id_text_input').val(input); | ||||
| 	$('#chore_id').data('combobox').refresh(); | ||||
|  | ||||
| 	var choreId = $(e.target).val(); | ||||
| 	if (choreId) | ||||
| 	{ | ||||
| 		Grocy.Components.ChoreCard.Refresh(choreId); | ||||
| 		Grocy.Components.DateTimePicker.GetInputElement().focus(); | ||||
| 		Grocy.FrontendHelpers.ValidateForm('choretracking-form'); | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| $('.combobox').combobox({ | ||||
| 	appendId: '_text_input', | ||||
| 	bsVersion: '4' | ||||
| }); | ||||
|  | ||||
| $('#chore_id_text_input').focus(); | ||||
| $('#chore_id_text_input').trigger('change'); | ||||
| Grocy.FrontendHelpers.ValidateForm('choretracking-form'); | ||||
|  | ||||
| $('#choretracking-form input').keyup(function (event) | ||||
| { | ||||
| 	Grocy.FrontendHelpers.ValidateForm('choretracking-form'); | ||||
| }); | ||||
|  | ||||
| $('#choretracking-form input').keydown(function(event) | ||||
| { | ||||
| 	if (event.keyCode === 13) //Enter | ||||
| 	{ | ||||
| 		event.preventDefault(); | ||||
|  | ||||
| 		if (document.getElementById('choretracking-form').checkValidity() === false) //There is at least one validation error | ||||
| 		{ | ||||
| 			return false; | ||||
| 		} | ||||
| 		else | ||||
| 		{ | ||||
| 			$('#save-choretracking-button').click(); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| Grocy.Components.DateTimePicker.GetInputElement().on('keypress', function(e) | ||||
| { | ||||
| 	Grocy.FrontendHelpers.ValidateForm('choretracking-form'); | ||||
| }); | ||||
| @@ -7,7 +7,7 @@ Grocy.Components.BatteryCard.Refresh = function(batteryId) | ||||
| 		{ | ||||
| 			$('#batterycard-battery-name').text(batteryDetails.battery.name); | ||||
| 			$('#batterycard-battery-used_in').text(batteryDetails.battery.used_in); | ||||
| 			$('#batterycard-battery-last-charged').text((batteryDetails.last_charged || 'never')); | ||||
| 			$('#batterycard-battery-last-charged').text((batteryDetails.last_charged || L('never'))); | ||||
| 			$('#batterycard-battery-last-charged-timeago').text($.timeago(batteryDetails.last_charged || '')); | ||||
| 			$('#batterycard-battery-charge-cycles-count').text((batteryDetails.charge_cycles_count || '0')); | ||||
|  | ||||
|   | ||||
							
								
								
									
										40
									
								
								public/viewjs/components/calendarcard.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								public/viewjs/components/calendarcard.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | ||||
| $('#calendar').datetimepicker( | ||||
| { | ||||
| 	format: 'L', | ||||
| 	buttons: { | ||||
| 		showToday: true, | ||||
| 		showClose: false | ||||
| 	}, | ||||
| 	calendarWeeks: true, | ||||
| 	locale: moment.locale(), | ||||
| 	icons: { | ||||
| 		time: 'far fa-clock', | ||||
| 		date: 'far fa-calendar', | ||||
| 		up: 'fas fa-arrow-up', | ||||
| 		down: 'fas fa-arrow-down', | ||||
| 		previous: 'fas fa-chevron-left', | ||||
| 		next: 'fas fa-chevron-right', | ||||
| 		today: 'fas fa-calendar-check', | ||||
| 		clear: 'far fa-trash-alt', | ||||
| 		close: 'far fa-times-circle' | ||||
| 	}, | ||||
| 	keepOpen: true, | ||||
| 	inline: true, | ||||
| 	keyBinds: { | ||||
| 		up: function(widget) { }, | ||||
| 		down: function(widget) { }, | ||||
| 		'control up': function(widget) { }, | ||||
| 		'control down': function(widget) { }, | ||||
| 		left: function(widget) { }, | ||||
| 		right: function(widget) { }, | ||||
| 		pageUp: function(widget) { }, | ||||
| 		pageDown: function(widget) { }, | ||||
| 		enter: function(widget) { }, | ||||
| 		escape: function(widget) { }, | ||||
| 		'control space': function(widget) { }, | ||||
| 		t: function(widget) { }, | ||||
| 		'delete': function(widget) { } | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| $('#calendar').datetimepicker('show'); | ||||
							
								
								
									
										21
									
								
								public/viewjs/components/chorecard.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								public/viewjs/components/chorecard.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| Grocy.Components.ChoreCard = { }; | ||||
|  | ||||
| Grocy.Components.ChoreCard.Refresh = function(choreId) | ||||
| { | ||||
| 	Grocy.Api.Get('chores/get-chore-details/' + choreId, | ||||
| 		function(choreDetails) | ||||
| 		{ | ||||
| 			$('#chorecard-chore-name').text(choreDetails.chore.name); | ||||
| 			$('#chorecard-chore-last-tracked').text((choreDetails.last_tracked || L('never'))); | ||||
| 			$('#chorecard-chore-last-tracked-timeago').text($.timeago(choreDetails.last_tracked || '')); | ||||
| 			$('#chorecard-chore-tracked-count').text((choreDetails.tracked_count || '0')); | ||||
| 			$('#chorecard-chore-last-done-by').text((choreDetails.last_done_by.display_name || L('Unknown'))); | ||||
|  | ||||
| 			EmptyElementWhenMatches('#chorecard-chore-last-tracked-timeago', L('timeago_nan')); | ||||
| 		}, | ||||
| 		function(xhr) | ||||
| 		{ | ||||
| 			console.error(xhr); | ||||
| 		} | ||||
| 	); | ||||
| }; | ||||
| @@ -2,7 +2,7 @@ Grocy.Components.DateTimePicker = { }; | ||||
|  | ||||
| Grocy.Components.DateTimePicker.GetInputElement = function() | ||||
| { | ||||
| 	return $('.datetimepicker').find('input'); | ||||
| 	return $('.datetimepicker').find('input').not(".form-check-input"); | ||||
| } | ||||
|  | ||||
| Grocy.Components.DateTimePicker.GetValue = function() | ||||
| @@ -14,6 +14,14 @@ Grocy.Components.DateTimePicker.SetValue = function(value) | ||||
| { | ||||
| 	Grocy.Components.DateTimePicker.GetInputElement().val(value); | ||||
| 	Grocy.Components.DateTimePicker.GetInputElement().trigger('change'); | ||||
|  | ||||
| 	// "Click" the shortcut checkbox when the desired value is | ||||
| 	// not the shortcut value and it is currently set | ||||
| 	var shortcutValue = $("#datetimepicker-shortcut").data("datetimepicker-shortcut-value"); | ||||
| 	if (value != shortcutValue && $("#datetimepicker-shortcut").is(":checked")) | ||||
| 	{ | ||||
| 		$("#datetimepicker-shortcut").click(); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| var startDate = null; | ||||
| @@ -21,6 +29,10 @@ if (Grocy.Components.DateTimePicker.GetInputElement().data('init-with-now') === | ||||
| { | ||||
| 	startDate = moment().format(Grocy.Components.DateTimePicker.GetInputElement().data('format')); | ||||
| } | ||||
| if (Grocy.Components.DateTimePicker.GetInputElement().data('init-value').length > 0) | ||||
| { | ||||
| 	startDate = moment(Grocy.Components.DateTimePicker.GetInputElement().data('init-value')).format(Grocy.Components.DateTimePicker.GetInputElement().data('format')); | ||||
| } | ||||
|  | ||||
| var limitDate = moment('2999-12-31 23:59:59'); | ||||
| if (Grocy.Components.DateTimePicker.GetInputElement().data('limit-end-to-now') === true) | ||||
| @@ -79,7 +91,7 @@ Grocy.Components.DateTimePicker.GetInputElement().on('keyup', function(e) | ||||
| 	var centuryEnd = Number.parseInt(now.getFullYear().toString().substring(0, 2) + '99'); | ||||
| 	var format = Grocy.Components.DateTimePicker.GetInputElement().data('format'); | ||||
| 	var nextInputElement = $(Grocy.Components.DateTimePicker.GetInputElement().data('next-input-selector')); | ||||
|  | ||||
| 	 | ||||
| 	//If input is empty and any arrow key is pressed, set date to today | ||||
| 	if (value.length === 0 && (e.keyCode === 38 || e.keyCode === 40 || e.keyCode === 37 || e.keyCode === 39)) | ||||
| 	{ | ||||
| @@ -93,7 +105,12 @@ Grocy.Components.DateTimePicker.GetInputElement().on('keyup', function(e) | ||||
| 	} | ||||
| 	else if (value.length === 4 && !(Number.parseInt(value) > centuryStart && Number.parseInt(value) < centuryEnd)) | ||||
| 	{ | ||||
| 		Grocy.Components.DateTimePicker.SetValue((new Date()).getFullYear().toString() + value); | ||||
| 		var date = moment((new Date()).getFullYear().toString() + value); | ||||
| 		if (date.isBefore(moment())) | ||||
| 		{ | ||||
| 			date.add(1, "year"); | ||||
| 		} | ||||
| 		Grocy.Components.DateTimePicker.SetValue(date.format(format)); | ||||
| 		nextInputElement.focus(); | ||||
| 	} | ||||
| 	else if (value.length === 8 && $.isNumeric(value)) | ||||
| @@ -101,6 +118,12 @@ Grocy.Components.DateTimePicker.GetInputElement().on('keyup', function(e) | ||||
| 		Grocy.Components.DateTimePicker.SetValue(value.replace(/(\d{4})(\d{2})(\d{2})/, '$1-$2-$3')); | ||||
| 		nextInputElement.focus(); | ||||
| 	} | ||||
| 	else if (value.length === 7 && $.isNumeric(value.substring(0, 6)) && (value.substring(6, 7).toLowerCase() === "e" || value.substring(6, 7).toLowerCase() === "+")) | ||||
| 	{ | ||||
| 		var date = moment(value.substring(0, 4) + "-" + value.substring(4, 6) + "-01").endOf("month"); | ||||
| 		Grocy.Components.DateTimePicker.SetValue(date.format(format)); | ||||
| 		nextInputElement.focus(); | ||||
| 	} | ||||
| 	else | ||||
| 	{ | ||||
| 		var dateObj = moment(value, format, true); | ||||
| @@ -148,6 +171,14 @@ Grocy.Components.DateTimePicker.GetInputElement().on('keyup', function(e) | ||||
| 			element.setCustomValidity(""); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// "Click" the shortcut checkbox when the shortcut value was | ||||
| 	// entered manually and it is currently not set | ||||
| 	var shortcutValue = $("#datetimepicker-shortcut").data("datetimepicker-shortcut-value"); | ||||
| 	if (value == shortcutValue && !$("#datetimepicker-shortcut").is(":checked")) | ||||
| 	{ | ||||
| 		$("#datetimepicker-shortcut").click(); | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| Grocy.Components.DateTimePicker.GetInputElement().on('input', function(e) | ||||
| @@ -160,3 +191,20 @@ $('.datetimepicker').on('update.datetimepicker', function(e) | ||||
| { | ||||
| 	Grocy.Components.DateTimePicker.GetInputElement().trigger('input'); | ||||
| }); | ||||
|  | ||||
| $("#datetimepicker-shortcut").on("click", function() | ||||
| { | ||||
| 	if (this.checked) | ||||
| 	{ | ||||
| 		var value = $("#datetimepicker-shortcut").data("datetimepicker-shortcut-value"); | ||||
| 		Grocy.Components.DateTimePicker.SetValue(value); | ||||
| 		Grocy.Components.DateTimePicker.GetInputElement().attr("readonly", ""); | ||||
| 		$(Grocy.Components.DateTimePicker.GetInputElement().data('next-input-selector')).focus(); | ||||
| 	} | ||||
| 	else | ||||
| 	{ | ||||
| 		Grocy.Components.DateTimePicker.SetValue(""); | ||||
| 		Grocy.Components.DateTimePicker.GetInputElement().removeAttr("readonly"); | ||||
| 		Grocy.Components.DateTimePicker.GetInputElement().focus(); | ||||
| 	} | ||||
| }); | ||||
|   | ||||
| @@ -1,20 +0,0 @@ | ||||
| Grocy.Components.HabitCard = { }; | ||||
|  | ||||
| Grocy.Components.HabitCard.Refresh = function(habitId) | ||||
| { | ||||
| 	Grocy.Api.Get('habits/get-habit-details/' + habitId, | ||||
| 		function(habitDetails) | ||||
| 		{ | ||||
| 			$('#habitcard-habit-name').text(habitDetails.habit.name); | ||||
| 			$('#habitcard-habit-last-tracked').text((habitDetails.last_tracked || 'never')); | ||||
| 			$('#habitcard-habit-last-tracked-timeago').text($.timeago(habitDetails.last_tracked || '')); | ||||
| 			$('#habitcard-habit-tracked-count').text((habitDetails.tracked_count || '0')); | ||||
|  | ||||
| 			EmptyElementWhenMatches('#habitcard-habit-last-tracked-timeago', L('timeago_nan')); | ||||
| 		}, | ||||
| 		function(xhr) | ||||
| 		{ | ||||
| 			console.error(xhr); | ||||
| 		} | ||||
| 	); | ||||
| }; | ||||
							
								
								
									
										15
									
								
								public/viewjs/components/numberpicker.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								public/viewjs/components/numberpicker.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| $(".numberpicker-down-button").unbind('click').on("click", function () | ||||
| { | ||||
| 	var inputElement = $(this).parent().parent().find('input[type="number"]')[0]; | ||||
| 	inputElement.stepDown(); | ||||
| 	$(inputElement).trigger('keyup'); | ||||
| 	$(inputElement).trigger('change'); | ||||
| }); | ||||
|  | ||||
| $(".numberpicker-up-button").unbind('click').on("click", function() | ||||
| { | ||||
| 	var inputElement = $(this).parent().parent().find('input[type="number"]')[0]; | ||||
| 	inputElement.stepUp(); | ||||
| 	$(inputElement).trigger('keyup'); | ||||
| 	$(inputElement).trigger('change'); | ||||
| }); | ||||
| @@ -5,15 +5,25 @@ Grocy.Components.ProductCard.Refresh = function(productId) | ||||
| 	Grocy.Api.Get('stock/get-product-details/' + productId, | ||||
| 		function(productDetails) | ||||
| 		{ | ||||
| 			var stockAmount = productDetails.stock_amount || '0'; | ||||
| 			$('#productcard-product-name').text(productDetails.product.name); | ||||
| 			$('#productcard-product-stock-amount').text(productDetails.stock_amount || '0'); | ||||
| 			$('#productcard-product-stock-amount').text(stockAmount); | ||||
| 			$('#productcard-product-stock-qu-name').text(productDetails.quantity_unit_stock.name); | ||||
| 			$('#productcard-product-stock-qu-name2').text(productDetails.quantity_unit_stock.name); | ||||
| 			$('#productcard-product-stock-qu-name2').text(Pluralize(stockAmount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural)); | ||||
| 			$('#productcard-product-last-purchased').text((productDetails.last_purchased || L('never')).substring(0, 10)); | ||||
| 			$('#productcard-product-last-purchased-timeago').text($.timeago(productDetails.last_purchased || '')); | ||||
| 			$('#productcard-product-last-used').text((productDetails.last_used || L('never')).substring(0, 10)); | ||||
| 			$('#productcard-product-last-used-timeago').text($.timeago(productDetails.last_used || '')); | ||||
|  | ||||
| 			if (productDetails.last_price !== null) | ||||
| 			{ | ||||
| 				$('#productcard-product-last-price').text(Number.parseFloat(productDetails.last_price).toLocaleString() + ' ' + Grocy.Currency); | ||||
| 			} | ||||
| 			else | ||||
| 			{ | ||||
| 				$('#productcard-product-last-price').text(L('Unknown')); | ||||
| 			} | ||||
|  | ||||
| 			EmptyElementWhenMatches('#productcard-product-last-purchased-timeago', L('timeago_nan')); | ||||
| 			EmptyElementWhenMatches('#productcard-product-last-used-timeago', L('timeago_nan')); | ||||
| 		}, | ||||
| @@ -22,4 +32,88 @@ Grocy.Components.ProductCard.Refresh = function(productId) | ||||
| 			console.error(xhr); | ||||
| 		} | ||||
| 	); | ||||
|  | ||||
| 	Grocy.Api.Get('stock/get-product-price-history/' + productId, | ||||
| 		function(priceHistoryDataPoints) | ||||
| 		{ | ||||
| 			if (priceHistoryDataPoints.length > 0) | ||||
| 			{ | ||||
| 				$("#productcard-product-price-history-chart").removeClass("d-none"); | ||||
| 				$("#productcard-no-price-data-hint").addClass("d-none"); | ||||
|  | ||||
| 				Grocy.Components.ProductCard.ReInitPriceHistoryChart(); | ||||
| 				priceHistoryDataPoints.forEach((dataPoint) => | ||||
| 				{ | ||||
| 					Grocy.Components.ProductCard.PriceHistoryChart.data.labels.push(moment(dataPoint.date).toDate()); | ||||
|  | ||||
| 					var dataset = Grocy.Components.ProductCard.PriceHistoryChart.data.datasets[0]; | ||||
| 					dataset.data.push(dataPoint.price); | ||||
| 				}); | ||||
| 				Grocy.Components.ProductCard.PriceHistoryChart.update(); | ||||
| 			} | ||||
| 			else | ||||
| 			{ | ||||
| 				$("#productcard-product-price-history-chart").addClass("d-none"); | ||||
| 				$("#productcard-no-price-data-hint").removeClass("d-none"); | ||||
| 			} | ||||
| 		}, | ||||
| 		function(xhr) | ||||
| 		{ | ||||
| 			console.error(xhr); | ||||
| 		} | ||||
| 	); | ||||
| }; | ||||
|  | ||||
| Grocy.Components.ProductCard.ReInitPriceHistoryChart = function() | ||||
| { | ||||
| 	if (typeof Grocy.Components.ProductCard.PriceHistoryChart !== "undefined") | ||||
| 	{ | ||||
| 		Grocy.Components.ProductCard.PriceHistoryChart.destroy(); | ||||
| 	} | ||||
|  | ||||
| 	var format = 'YYYY-MM-DD'; | ||||
| 	Grocy.Components.ProductCard.PriceHistoryChart = new Chart(document.getElementById("productcard-product-price-history-chart"), { | ||||
| 		type: "line", | ||||
| 		data: { | ||||
| 			labels: [ //Date objects | ||||
| 				// Will be populated in Grocy.Components.ProductCard.Refresh | ||||
| 			], | ||||
| 			datasets: [{ | ||||
| 				data: [ | ||||
| 					// Will be populated in Grocy.Components.ProductCard.Refresh | ||||
| 				], | ||||
| 				fill: false, | ||||
| 				borderColor: '#17a2b8' | ||||
| 			}] | ||||
| 		}, | ||||
| 		options: { | ||||
| 			scales: { | ||||
| 				xAxes: [{ | ||||
| 					type: 'time', | ||||
| 					time: { | ||||
| 						parser: format, | ||||
| 						round: 'day', | ||||
| 						tooltipFormat: format, | ||||
| 						unit: 'day', | ||||
| 						unitStepSize: 10, | ||||
| 						displayFormats: { | ||||
| 							'day': format | ||||
| 						} | ||||
| 					}, | ||||
| 					ticks: { | ||||
| 						autoSkip: true, | ||||
| 						maxRotation: 0 | ||||
| 					} | ||||
| 				}], | ||||
| 				yAxes: [{ | ||||
| 					ticks: { | ||||
| 						beginAtZero: true | ||||
| 					} | ||||
| 				}] | ||||
| 			}, | ||||
| 			legend: { | ||||
| 				display: false | ||||
| 			} | ||||
| 		} | ||||
| 	}); | ||||
| } | ||||
|   | ||||
							
								
								
									
										172
									
								
								public/viewjs/components/productpicker.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										172
									
								
								public/viewjs/components/productpicker.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,172 @@ | ||||
| Grocy.Components.ProductPicker = { }; | ||||
|  | ||||
| Grocy.Components.ProductPicker.GetPicker = function() | ||||
| { | ||||
| 	return $('#product_id'); | ||||
| } | ||||
|  | ||||
| Grocy.Components.ProductPicker.GetInputElement = function() | ||||
| { | ||||
| 	return $('#product_id_text_input'); | ||||
| } | ||||
|  | ||||
| Grocy.Components.ProductPicker.GetValue = function() | ||||
| { | ||||
| 	return $('#product_id').val(); | ||||
| } | ||||
|  | ||||
| Grocy.Components.ProductPicker.SetValue = function(value) | ||||
| { | ||||
| 	Grocy.Components.ProductPicker.GetInputElement().val(value); | ||||
| 	Grocy.Components.ProductPicker.GetInputElement().trigger('change'); | ||||
| } | ||||
|  | ||||
| Grocy.Components.ProductPicker.InProductAddWorkflow = function() | ||||
| { | ||||
| 	return typeof GetUriParam('createdproduct') !== "undefined"; | ||||
| } | ||||
|  | ||||
| Grocy.Components.ProductPicker.InProductModifyWorkflow = function() | ||||
| { | ||||
| 	return typeof GetUriParam('addbarcodetoselection') !== "undefined"; | ||||
| } | ||||
|  | ||||
| Grocy.Components.ProductPicker.ShowCustomError = function(text) | ||||
| { | ||||
| 	var element = $("#custom-productpicker-error"); | ||||
| 	element.text(text); | ||||
| 	element.removeClass("d-none"); | ||||
| } | ||||
|  | ||||
| Grocy.Components.ProductPicker.HideCustomError = function() | ||||
| { | ||||
| 	$("#custom-productpicker-error").addClass("d-none"); | ||||
| } | ||||
|  | ||||
| $('.product-combobox').combobox({ | ||||
| 	appendId: '_text_input', | ||||
| 	bsVersion: '4', | ||||
| 	clearIfNoMatch: false | ||||
| }); | ||||
|  | ||||
| var prefillProduct = GetUriParam('createdproduct'); | ||||
| var prefillProduct2 = Grocy.Components.ProductPicker.GetPicker().parent().data('prefill-by-name').toString(); | ||||
| if (!prefillProduct2.isEmpty()) | ||||
| { | ||||
| 	prefillProduct = prefillProduct2; | ||||
| } | ||||
| if (typeof prefillProduct !== "undefined") | ||||
| { | ||||
| 	var possibleOptionElement = $("#product_id option[data-additional-searchdata*='" + prefillProduct + "']").first(); | ||||
| 	if (possibleOptionElement.length === 0) | ||||
| 	{ | ||||
| 		possibleOptionElement = $("#product_id option:contains('" + prefillProduct + "')").first(); | ||||
| 	} | ||||
|  | ||||
| 	if (possibleOptionElement.length > 0) | ||||
| 	{ | ||||
| 		$('#product_id').val(possibleOptionElement.val()); | ||||
| 		$('#product_id').data('combobox').refresh(); | ||||
| 		$('#product_id').trigger('change'); | ||||
|  | ||||
| 		var nextInputElement = $(Grocy.Components.ProductPicker.GetPicker().parent().data('next-input-selector').toString()); | ||||
| 		nextInputElement.focus(); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| var addBarcode = GetUriParam('addbarcodetoselection'); | ||||
| if (addBarcode !== undefined) | ||||
| { | ||||
| 	$('#addbarcodetoselection').text(addBarcode); | ||||
| 	$('#flow-info-addbarcodetoselection').removeClass('d-none'); | ||||
| 	$('#barcode-lookup-disabled-hint').removeClass('d-none'); | ||||
| } | ||||
|  | ||||
| $('#product_id_text_input').on('blur', function(e) | ||||
| { | ||||
| 	if (Grocy.Components.ProductPicker.GetPicker().hasClass("combobox-menu-visible")) | ||||
| 	{ | ||||
| 		return;	 | ||||
| 	} | ||||
|  | ||||
| 	var input = $('#product_id_text_input').val().toString(); | ||||
| 	var possibleOptionElement = $("#product_id option[data-additional-searchdata*='" + input + "']").first(); | ||||
| 	 | ||||
| 	if (GetUriParam('addbarcodetoselection') === undefined && possibleOptionElement.length > 0) | ||||
| 	{ | ||||
| 		$('#product_id').val(possibleOptionElement.val()); | ||||
| 		$('#product_id').data('combobox').refresh(); | ||||
| 		$('#product_id').trigger('change'); | ||||
| 	} | ||||
| 	else | ||||
| 	{ | ||||
| 		var optionElement = $("#product_id option:contains('" + input + "')").first(); | ||||
| 		if (input.length > 0 && optionElement.length === 0 && typeof GetUriParam('addbarcodetoselection') === "undefined") | ||||
| 		{ | ||||
| 			var addProductWorkflowsAdditionalCssClasses = ""; | ||||
| 			if (Grocy.Components.ProductPicker.GetPicker().parent().data('disallow-add-product-workflows').toString() === "true") | ||||
| 			{ | ||||
| 				addProductWorkflowsAdditionalCssClasses = "d-none"; | ||||
| 			} | ||||
|  | ||||
| 			bootbox.dialog({ | ||||
| 				message: L('"#1" could not be resolved to a product, how do you want to proceed?', input), | ||||
| 				title: L('Create or assign product'), | ||||
| 				onEscape: function() | ||||
| 				{ | ||||
| 					Grocy.Components.ProductPicker.SetValue(''); | ||||
| 				}, | ||||
| 				size: 'large', | ||||
| 				backdrop: true, | ||||
| 				buttons: { | ||||
| 					cancel: { | ||||
| 						label: L('Cancel'), | ||||
| 						className: 'btn-default responsive-button', | ||||
| 						callback: function() | ||||
| 						{ | ||||
| 							Grocy.Components.ProductPicker.SetValue(''); | ||||
| 						} | ||||
| 					}, | ||||
| 					addnewproduct: { | ||||
| 						label: '<strong>P</strong> ' + L('Add as new product'), | ||||
| 						className: 'btn-success add-new-product-dialog-button responsive-button ' + addProductWorkflowsAdditionalCssClasses, | ||||
| 						callback: function() | ||||
| 						{ | ||||
| 							window.location.href = U('/product/new?prefillname=' + encodeURIComponent(input) + '&returnto=' + encodeURIComponent(window.location.pathname)); | ||||
| 						} | ||||
| 					}, | ||||
| 					addbarcode: { | ||||
| 						label: '<strong>B</strong> ' + L('Add as barcode to existing product'), | ||||
| 						className: 'btn-info add-new-barcode-dialog-button responsive-button', | ||||
| 						callback: function() | ||||
| 						{ | ||||
| 							window.location.href = U(window.location.pathname + '?addbarcodetoselection=' + encodeURIComponent(input)); | ||||
| 						} | ||||
| 					}, | ||||
| 					addnewproductwithbarcode: { | ||||
| 						label: '<strong>A</strong> ' + L('Add as new product and prefill barcode'), | ||||
| 						className: 'btn-warning add-new-product-with-barcode-dialog-button responsive-button ' + addProductWorkflowsAdditionalCssClasses, | ||||
| 						callback: function() | ||||
| 						{ | ||||
| 							window.location.href = U('/product/new?prefillbarcode=' + encodeURIComponent(input) + '&returnto=' + encodeURIComponent(window.location.pathname)); | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
| 			}).on('keypress', function(e) | ||||
| 			{ | ||||
| 				if (e.key === 'B' || e.key === 'b') | ||||
| 				{ | ||||
| 					$('.add-new-barcode-dialog-button').not(".d-none").click(); | ||||
| 				} | ||||
| 				if (e.key === 'p' || e.key === 'P') | ||||
| 				{ | ||||
| 					$('.add-new-product-dialog-button').not(".d-none").click(); | ||||
| 				} | ||||
| 				if (e.key === 'a' || e.key === 'A') | ||||
| 				{ | ||||
| 					$('.add-new-product-with-barcode-dialog-button').not(".d-none").click(); | ||||
| 				} | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
							
								
								
									
										62
									
								
								public/viewjs/components/userpicker.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								public/viewjs/components/userpicker.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,62 @@ | ||||
| Grocy.Components.UserPicker = { }; | ||||
|  | ||||
| Grocy.Components.UserPicker.GetPicker = function() | ||||
| { | ||||
| 	return $('#user_id'); | ||||
| } | ||||
|  | ||||
| Grocy.Components.UserPicker.GetInputElement = function() | ||||
| { | ||||
| 	return $('#user_id_text_input'); | ||||
| } | ||||
|  | ||||
| Grocy.Components.UserPicker.GetValue = function() | ||||
| { | ||||
| 	return $('#user_id').val(); | ||||
| } | ||||
|  | ||||
| Grocy.Components.UserPicker.SetValue = function(value) | ||||
| { | ||||
| 	Grocy.Components.UserPicker.GetInputElement().val(value); | ||||
| 	Grocy.Components.UserPicker.GetInputElement().trigger('change'); | ||||
| } | ||||
|  | ||||
| $('.user-combobox').combobox({ | ||||
| 	appendId: '_text_input', | ||||
| 	bsVersion: '4' | ||||
| }); | ||||
|  | ||||
| var prefillUser = Grocy.Components.UserPicker.GetPicker().parent().data('prefill-by-username').toString(); | ||||
| if (typeof prefillUser !== "undefined") | ||||
| { | ||||
| 	var possibleOptionElement = $("#user_id option[data-additional-searchdata*='" + prefillUser + "']").first(); | ||||
| 	if (possibleOptionElement.length === 0) | ||||
| 	{ | ||||
| 		possibleOptionElement = $("#user_id option:contains('" + prefillUser + "')").first(); | ||||
| 	} | ||||
|  | ||||
| 	if (possibleOptionElement.length > 0) | ||||
| 	{ | ||||
| 		$('#user_id').val(possibleOptionElement.val()); | ||||
| 		$('#user_id').data('combobox').refresh(); | ||||
| 		$('#user_id').trigger('change'); | ||||
|  | ||||
| 		var nextInputElement = $(Grocy.Components.UserPicker.GetPicker().parent().data('next-input-selector').toString()); | ||||
| 		nextInputElement.focus(); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| var prefillUserId = Grocy.Components.UserPicker.GetPicker().parent().data('prefill-by-user-id').toString(); | ||||
| if (typeof prefillUserId !== "undefined") | ||||
| { | ||||
| 	var possibleOptionElement = $("#user_id option[value='" + prefillUserId + "']").first(); | ||||
| 	if (possibleOptionElement.length > 0) | ||||
| 	{ | ||||
| 		$('#user_id').val(possibleOptionElement.val()); | ||||
| 		$('#user_id').data('combobox').refresh(); | ||||
| 		$('#user_id').trigger('change'); | ||||
|  | ||||
| 		var nextInputElement = $(Grocy.Components.UserPicker.GetPicker().parent().data('next-input-selector').toString()); | ||||
| 		nextInputElement.focus(); | ||||
| 	} | ||||
| } | ||||
| @@ -19,10 +19,8 @@ | ||||
| 					toastr.success(L('Removed #1 #2 of #3 from stock', jsonForm.amount, productDetails.quantity_unit_stock.name, productDetails.product.name)); | ||||
|  | ||||
| 					$('#amount').val(1); | ||||
| 					$('#product_id').val(''); | ||||
| 					$('#product_id_text_input').focus(); | ||||
| 					$('#product_id_text_input').val(''); | ||||
| 					$('#product_id_text_input').trigger('change'); | ||||
| 					Grocy.Components.ProductPicker.SetValue(''); | ||||
| 					Grocy.Components.ProductPicker.GetInputElement().focus(); | ||||
| 					Grocy.FrontendHelpers.ValidateForm('consume-form'); | ||||
| 				}, | ||||
| 				function(xhr) | ||||
| @@ -38,7 +36,7 @@ | ||||
| 	); | ||||
| }); | ||||
|  | ||||
| $('#product_id').on('change', function(e) | ||||
| Grocy.Components.ProductPicker.GetPicker().on('change', function(e) | ||||
| { | ||||
| 	var productId = $(e.target).val(); | ||||
|  | ||||
| @@ -54,14 +52,14 @@ $('#product_id').on('change', function(e) | ||||
|  | ||||
| 				if ((productDetails.stock_amount || 0) === 0) | ||||
| 				{ | ||||
| 					$('#product_id').val(''); | ||||
| 					$('#product_id_text_input').val(''); | ||||
| 					Grocy.Components.ProductPicker.SetValue(''); | ||||
| 					Grocy.FrontendHelpers.ValidateForm('consume-form'); | ||||
| 					$('#product-error').text(L('This product is not in stock')); | ||||
| 					$('#product_id_text_input').focus(); | ||||
| 					Grocy.Components.ProductPicker.ShowCustomError(L('This product is not in stock')); | ||||
| 					Grocy.Components.ProductPicker.GetInputElement().focus(); | ||||
| 				} | ||||
| 				else | ||||
| 				{ | ||||
| 					Grocy.Components.ProductPicker.HideCustomError(); | ||||
| 					Grocy.FrontendHelpers.ValidateForm('consume-form'); | ||||
| 					$('#amount').focus(); | ||||
| 				} | ||||
| @@ -74,28 +72,8 @@ $('#product_id').on('change', function(e) | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| $('.combobox').combobox({ | ||||
| 	appendId: '_text_input' | ||||
| }); | ||||
|  | ||||
| $('#product_id_text_input').on('change', function(e) | ||||
| { | ||||
| 	var input = $('#product_id_text_input').val().toString(); | ||||
| 	var possibleOptionElement = $("#product_id option[data-additional-searchdata*='" + input + "']").first(); | ||||
| 	 | ||||
| 	if (possibleOptionElement.length > 0) | ||||
| 	{ | ||||
| 		$('#product_id').val(possibleOptionElement.val()); | ||||
| 		$('#product_id').data('combobox').refresh(); | ||||
| 		$('#product_id').trigger('change'); | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| $('#amount').val(1); | ||||
| $('#product_id').val(''); | ||||
| $('#product_id_text_input').focus(); | ||||
| $('#product_id_text_input').val(''); | ||||
| $('#product_id_text_input').trigger('change'); | ||||
| Grocy.Components.ProductPicker.GetInputElement().focus(); | ||||
| Grocy.FrontendHelpers.ValidateForm('consume-form'); | ||||
|  | ||||
| $('#amount').on('focus', function(e) | ||||
| @@ -112,9 +90,10 @@ $('#consume-form input').keydown(function(event) | ||||
| { | ||||
| 	if (event.keyCode === 13) //Enter | ||||
| 	{ | ||||
| 		event.preventDefault(); | ||||
| 		 | ||||
| 		if (document.getElementById('consume-form').checkValidity() === false) //There is at least one validation error | ||||
| 		{ | ||||
| 			event.preventDefault(); | ||||
| 			return false; | ||||
| 		} | ||||
| 		else | ||||
|   | ||||
| @@ -1,71 +0,0 @@ | ||||
| $('#save-habit-button').on('click', function(e) | ||||
| { | ||||
| 	e.preventDefault(); | ||||
|  | ||||
| 	if (Grocy.EditMode === 'create') | ||||
| 	{ | ||||
| 		Grocy.Api.Post('add-object/habits', $('#habit-form').serializeJSON(), | ||||
| 			function(result) | ||||
| 			{ | ||||
| 				window.location.href = U('/habits'); | ||||
| 			}, | ||||
| 			function(xhr) | ||||
| 			{ | ||||
| 				console.error(xhr); | ||||
| 			} | ||||
| 		); | ||||
| 	} | ||||
| 	else | ||||
| 	{ | ||||
| 		Grocy.Api.Post('edit-object/habits/' + Grocy.EditObjectId, $('#habit-form').serializeJSON(), | ||||
| 			function(result) | ||||
| 			{ | ||||
| 				window.location.href = U('/habits'); | ||||
| 			}, | ||||
| 			function(xhr) | ||||
| 			{ | ||||
| 				console.error(xhr); | ||||
| 			} | ||||
| 		); | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| $('#habit-form input').keyup(function(event) | ||||
| { | ||||
| 	Grocy.FrontendHelpers.ValidateForm('habit-form'); | ||||
| }); | ||||
|  | ||||
| $('#habit-form input').keydown(function(event) | ||||
| { | ||||
| 	if (event.keyCode === 13) //Enter | ||||
| 	{ | ||||
| 		if (document.getElementById('habit-form').checkValidity() === false) //There is at least one validation error | ||||
| 		{ | ||||
| 			event.preventDefault(); | ||||
| 			return false; | ||||
| 		} | ||||
| 		else | ||||
| 		{ | ||||
| 			$('#save-habit-button').click(); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| $('#name').focus(); | ||||
| Grocy.FrontendHelpers.ValidateForm('habit-form'); | ||||
|  | ||||
| $('.input-group-habit-period-type').on('change', function(e) | ||||
| { | ||||
| 	var periodType = $('#period_type').val(); | ||||
| 	var periodDays = $('#period_days').val(); | ||||
|  | ||||
| 	if (periodType === 'dynamic-regular') | ||||
| 	{ | ||||
| 		$('#habit-period-type-info').text(L('This means it is estimated that a new execution of this habit is tracked #1 days after the last was tracked', periodDays.toString())); | ||||
| 		$('#habit-period-type-info').removeClass('d-none'); | ||||
| 	} | ||||
| 	else | ||||
| 	{ | ||||
| 		$('#habit-period-type-info').addClass('d-none'); | ||||
| 	} | ||||
| }); | ||||
| @@ -1,45 +0,0 @@ | ||||
| var habitsOverviewTable = $('#habits-overview-table').DataTable({ | ||||
| 	'paginate': false, | ||||
| 	'order': [[2, 'desc']], | ||||
| 	'columnDefs': [ | ||||
| 		{ 'orderable': false, 'targets': 0 } | ||||
| 	], | ||||
| 	'language': JSON.parse(L('datatables_localization')), | ||||
| 	'scrollY': false | ||||
| }); | ||||
|  | ||||
| $("#search").on("keyup", function() | ||||
| { | ||||
| 	var value = $(this).val(); | ||||
| 	if (value === "all") | ||||
| 	{ | ||||
| 		value = ""; | ||||
| 	} | ||||
| 	 | ||||
| 	habitsOverviewTable.search(value).draw(); | ||||
| }); | ||||
|  | ||||
| $(document).on('click', '.track-habit-button', function(e) | ||||
| { | ||||
| 	var habitId = $(e.currentTarget).attr('data-habit-id'); | ||||
| 	var habitName = $(e.currentTarget).attr('data-habit-name'); | ||||
| 	var trackedTime = moment().format('YYYY-MM-DD HH:mm:ss'); | ||||
|  | ||||
| 	Grocy.Api.Get('habits/track-habit-execution/' + habitId + '?tracked_time=' + trackedTime, | ||||
| 		function(result) | ||||
| 		{ | ||||
| 			$('#habit-' + habitId + '-last-tracked-time').parent().effect('highlight', {}, 500); | ||||
| 			$('#habit-' + habitId + '-last-tracked-time').fadeOut(500, function () { | ||||
| 				$(this).text(trackedTime).fadeIn(500); | ||||
| 			}); | ||||
| 			$('#habit-' + habitId + '-last-tracked-time-timeago').attr('datetime', trackedTime); | ||||
| 			RefreshContextualTimeago(); | ||||
|  | ||||
| 			toastr.success(L('Tracked execution of habit #1 on #2', habitName, trackedTime)); | ||||
| 		}, | ||||
| 		function(xhr) | ||||
| 		{ | ||||
| 			console.error(xhr); | ||||
| 		} | ||||
| 	); | ||||
| }); | ||||
| @@ -1,78 +0,0 @@ | ||||
| $('#save-habittracking-button').on('click', function(e) | ||||
| { | ||||
| 	e.preventDefault(); | ||||
|  | ||||
| 	var jsonForm = $('#habittracking-form').serializeJSON(); | ||||
|  | ||||
| 	Grocy.Api.Get('habits/get-habit-details/' + jsonForm.habit_id, | ||||
| 		function (habitDetails) | ||||
| 		{ | ||||
| 			Grocy.Api.Get('habits/track-habit-execution/' + jsonForm.habit_id + '?tracked_time=' + Grocy.Components.DateTimePicker.GetValue(), | ||||
| 				function(result) | ||||
| 				{ | ||||
| 					toastr.success(L('Tracked execution of habit #1 on #2', habitDetails.habit.name, Grocy.Components.DateTimePicker.GetValue())); | ||||
|  | ||||
| 					$('#habit_id').val(''); | ||||
| 					$('#habit_id_text_input').focus(); | ||||
| 					$('#habit_id_text_input').val(''); | ||||
| 					Grocy.Components.DateTimePicker.SetValue(moment().format('YYYY-MM-DD HH:mm:ss')); | ||||
| 					$('#habit_id_text_input').trigger('change'); | ||||
| 					Grocy.FrontendHelpers.ValidateForm('habittracking-form'); | ||||
| 				}, | ||||
| 				function(xhr) | ||||
| 				{ | ||||
| 					console.error(xhr); | ||||
| 				} | ||||
| 			); | ||||
| 		}, | ||||
| 		function(xhr) | ||||
| 		{ | ||||
| 			console.error(xhr); | ||||
| 		} | ||||
| 	); | ||||
| }); | ||||
|  | ||||
| $('#habit_id').on('change', function(e) | ||||
| { | ||||
| 	var habitId = $(e.target).val(); | ||||
|  | ||||
| 	if (habitId) | ||||
| 	{ | ||||
| 		Grocy.Components.HabitCard.Refresh(habitId); | ||||
| 		Grocy.Components.DateTimePicker.GetInputElement().focus(); | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| $('.combobox').combobox({ | ||||
| 	appendId: '_text_input' | ||||
| }); | ||||
|  | ||||
| $('#habit_id_text_input').focus(); | ||||
| $('#habit_id_text_input').trigger('change'); | ||||
| Grocy.FrontendHelpers.ValidateForm('habittracking-form'); | ||||
|  | ||||
| $('#habittracking-form input').keyup(function (event) | ||||
| { | ||||
| 	Grocy.FrontendHelpers.ValidateForm('habittracking-form'); | ||||
| }); | ||||
|  | ||||
| $('#habittracking-form input').keydown(function(event) | ||||
| { | ||||
| 	if (event.keyCode === 13) //Enter | ||||
| 	{ | ||||
| 		if (document.getElementById('habittracking-form').checkValidity() === false) //There is at least one validation error | ||||
| 		{ | ||||
| 			event.preventDefault(); | ||||
| 			return false; | ||||
| 		} | ||||
| 		else | ||||
| 		{ | ||||
| 			$('#save-habittracking-button').click(); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| Grocy.Components.DateTimePicker.GetInputElement().on('keypress', function(e) | ||||
| { | ||||
| 	Grocy.FrontendHelpers.ValidateForm('habittracking-form'); | ||||
| }); | ||||
| @@ -43,10 +43,8 @@ | ||||
| 						$('#inventory-change-info').addClass('d-none'); | ||||
| 						$('#new_amount').val(''); | ||||
| 						Grocy.Components.DateTimePicker.SetValue(''); | ||||
| 						$('#product_id').val(''); | ||||
| 						$('#product_id_text_input').focus(); | ||||
| 						$('#product_id_text_input').val(''); | ||||
| 						$('#product_id_text_input').trigger('change'); | ||||
| 						Grocy.Components.ProductPicker.SetValue(''); | ||||
| 						Grocy.Components.ProductPicker.GetInputElement().focus(); | ||||
| 						Grocy.FrontendHelpers.ValidateForm('inventory-form'); | ||||
| 					} | ||||
| 				}, | ||||
| @@ -63,7 +61,7 @@ | ||||
| 	); | ||||
| }); | ||||
|  | ||||
| $('#product_id').on('change', function(e) | ||||
| Grocy.Components.ProductPicker.GetPicker().on('change', function(e) | ||||
| { | ||||
| 	var productId = $(e.target).val(); | ||||
|  | ||||
| @@ -87,94 +85,23 @@ $('#product_id').on('change', function(e) | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| $('.combobox').combobox({ | ||||
| 	appendId: '_text_input' | ||||
| }); | ||||
|  | ||||
| $('#product_id_text_input').on('change', function(e) | ||||
| { | ||||
| 	var input = $('#product_id_text_input').val().toString(); | ||||
| 	var possibleOptionElement = $("#product_id option[data-additional-searchdata*='" + input + "']").first(); | ||||
| 	 | ||||
| 	if (GetUriParam('addbarcodetoselection') === undefined && possibleOptionElement.length > 0) | ||||
| 	{ | ||||
| 		$('#product_id').val(possibleOptionElement.val()); | ||||
| 		$('#product_id').data('combobox').refresh(); | ||||
| 		$('#product_id').trigger('change'); | ||||
| 	} | ||||
| 	else | ||||
| 	{ | ||||
| 		var optionElement = $("#product_id option:contains('" + input + "')").first(); | ||||
| 		if (input.length > 0 && optionElement.length === 0 && GetUriParam('addbarcodetoselection') === undefined	) | ||||
| 		{ | ||||
| 			bootbox.dialog({ | ||||
| 				message: L('#1 could not be resolved to a product, how do you want to proceed?', input), | ||||
| 				title: L('Create or assign product'), | ||||
| 				onEscape: function() { }, | ||||
| 				size: 'large', | ||||
| 				backdrop: true, | ||||
| 				buttons: { | ||||
| 					cancel: { | ||||
| 						label: L('Cancel'), | ||||
| 						className: 'btn-default', | ||||
| 						callback: function() { } | ||||
| 					}, | ||||
| 					addnewproduct: { | ||||
| 						label: '<strong>P</strong> ' + L('Add as new product'), | ||||
| 						className: 'btn-success add-new-product-dialog-button', | ||||
| 						callback: function() | ||||
| 						{ | ||||
| 							window.location.href = U('/product/new?prefillname=' + encodeURIComponent(input) + '&returnto=' + encodeURIComponent(window.location.pathname)); | ||||
| 						} | ||||
| 					}, | ||||
| 					addbarcode: { | ||||
| 						label: '<strong>B</strong> ' + L('Add as barcode to existing product'), | ||||
| 						className: 'btn-info add-new-barcode-dialog-button', | ||||
| 						callback: function() | ||||
| 						{ | ||||
| 							window.location.href = U('/inventory?addbarcodetoselection=' + encodeURIComponent(input)); | ||||
| 						} | ||||
| 					}, | ||||
| 					addnewproductwithbarcode: { | ||||
| 						label: '<strong>A</strong> ' + L('Add as new product and prefill barcode'), | ||||
| 						className: 'btn-warning add-new-product-with-barcode-dialog-button', | ||||
| 						callback: function() | ||||
| 						{ | ||||
| 							window.location.href = U('/product/new?prefillbarcode=' + encodeURIComponent(input) + '&returnto=' + encodeURIComponent(window.location.pathname)); | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
| 			}).on('keypress', function(e) | ||||
| 			{ | ||||
| 				if (e.key === 'B' || e.key === 'b') | ||||
| 				{ | ||||
| 					$('.add-new-barcode-dialog-button').click(); | ||||
| 				} | ||||
| 				if (e.key === 'p' || e.key === 'P') | ||||
| 				{ | ||||
| 					$('.add-new-product-dialog-button').click(); | ||||
| 				} | ||||
| 				if (e.key === 'a' || e.key === 'A') | ||||
| 				{ | ||||
| 					$('.add-new-product-with-barcode-dialog-button').click(); | ||||
| 				} | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| $('#new_amount').val(''); | ||||
| $('#product_id').val(''); | ||||
| $('#product_id_text_input').focus(); | ||||
| $('#product_id_text_input').val(''); | ||||
| $('#product_id_text_input').trigger('change'); | ||||
| Grocy.FrontendHelpers.ValidateForm('inventory-form'); | ||||
|  | ||||
| if (Grocy.Components.ProductPicker.InProductAddWorkflow() === false) | ||||
| { | ||||
| 	Grocy.Components.ProductPicker.GetInputElement().focus(); | ||||
| } | ||||
| else | ||||
| { | ||||
| 	Grocy.Components.ProductPicker.GetPicker().trigger('change'); | ||||
| } | ||||
|  | ||||
| $('#new_amount').on('focus', function(e) | ||||
| { | ||||
| 	if ($('#product_id_text_input').val().length === 0) | ||||
| 	if (Grocy.Components.ProductPicker.GetValue().length === 0) | ||||
| 	{ | ||||
| 		$('#product_id_text_input').focus(); | ||||
| 		Grocy.Components.ProductPicker.GetInputElement().focus(); | ||||
| 	} | ||||
| 	else | ||||
| 	{ | ||||
| @@ -191,9 +118,10 @@ $('#inventory-form input').keydown(function(event) | ||||
| { | ||||
| 	if (event.keyCode === 13) //Enter | ||||
| 	{ | ||||
| 		event.preventDefault(); | ||||
| 		 | ||||
| 		if (document.getElementById('inventory-form').checkValidity() === false) //There is at least one validation error | ||||
| 		{ | ||||
| 			event.preventDefault(); | ||||
| 			return false; | ||||
| 		} | ||||
| 		else | ||||
| @@ -203,32 +131,6 @@ $('#inventory-form input').keydown(function(event) | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| var prefillProduct = GetUriParam('createdproduct'); | ||||
| if (prefillProduct !== undefined) | ||||
| { | ||||
| 	var possibleOptionElement = $("#product_id option[data-additional-searchdata*='" + prefillProduct + "']").first(); | ||||
| 	if (possibleOptionElement.length === 0) | ||||
| 	{ | ||||
| 		possibleOptionElement = $("#product_id option:contains('" + prefillProduct + "')").first(); | ||||
| 	} | ||||
|  | ||||
| 	if (possibleOptionElement.length > 0) | ||||
| 	{ | ||||
| 		$('#product_id').val(possibleOptionElement.val()); | ||||
| 		$('#product_id').data('combobox').refresh(); | ||||
| 		$('#product_id').trigger('change'); | ||||
| 		$('#new_amount').focus(); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| var addBarcode = GetUriParam('addbarcodetoselection'); | ||||
| if (addBarcode !== undefined) | ||||
| { | ||||
| 	$('#addbarcodetoselection').text(addBarcode); | ||||
| 	$('#flow-info-addbarcodetoselection').removeClass('d-none'); | ||||
| 	$('#barcode-lookup-disabled-hint').removeClass('d-none'); | ||||
| } | ||||
|  | ||||
| $('#new_amount').on('keypress', function(e) | ||||
| { | ||||
| 	$('#new_amount').trigger('change'); | ||||
| @@ -246,7 +148,7 @@ Grocy.Components.DateTimePicker.GetInputElement().on('keypress', function(e) | ||||
|  | ||||
| $('#new_amount').on('keyup', function(e) | ||||
| { | ||||
| 	var productId = $('#product_id').val(); | ||||
| 	var productId = Grocy.Components.ProductPicker.GetValue(); | ||||
| 	var newAmount = parseInt($('#new_amount').val()); | ||||
| 	 | ||||
| 	if (productId) | ||||
|   | ||||
| @@ -11,7 +11,7 @@ | ||||
| 			}, | ||||
| 			function(xhr) | ||||
| 			{ | ||||
| 				console.error(xhr); | ||||
| 				Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response) | ||||
| 			} | ||||
| 		); | ||||
| 	} | ||||
| @@ -24,7 +24,7 @@ | ||||
| 			}, | ||||
| 			function(xhr) | ||||
| 			{ | ||||
| 				console.error(xhr); | ||||
| 				Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response) | ||||
| 			} | ||||
| 		); | ||||
| 	} | ||||
| @@ -39,9 +39,10 @@ $('#location-form input').keydown(function (event) | ||||
| { | ||||
| 	if (event.keyCode === 13) //Enter | ||||
| 	{ | ||||
| 		event.preventDefault(); | ||||
| 		 | ||||
| 		if (document.getElementById('location-form').checkValidity() === false) //There is at least one validation error | ||||
| 		{ | ||||
| 			event.preventDefault(); | ||||
| 			return false; | ||||
| 		} | ||||
| 		else | ||||
|   | ||||
| @@ -5,7 +5,18 @@ | ||||
| 		{ 'orderable': false, 'targets': 0 } | ||||
| 	], | ||||
| 	'language': JSON.parse(L('datatables_localization')), | ||||
| 	'scrollY': false | ||||
| 	'scrollY': false, | ||||
| 	'colReorder': true, | ||||
| 	'stateSave': true, | ||||
| 	'stateSaveParams': function(settings, data) | ||||
| 	{ | ||||
| 		data.search.search = ""; | ||||
|  | ||||
| 		data.columns.forEach(column => | ||||
| 		{ | ||||
| 			column.search.search = ""; | ||||
| 		}); | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| $("#search").on("keyup", function() | ||||
|   | ||||
| @@ -5,7 +5,18 @@ | ||||
| 		{ 'orderable': false, 'targets': 0 } | ||||
| 	], | ||||
| 	'language': JSON.parse(L('datatables_localization')), | ||||
| 	'scrollY': false | ||||
| 	'scrollY': false, | ||||
| 	'colReorder': true, | ||||
| 	'stateSave': true, | ||||
| 	'stateSaveParams': function(settings, data) | ||||
| 	{ | ||||
| 		data.search.search = ""; | ||||
|  | ||||
| 		data.columns.forEach(column => | ||||
| 		{ | ||||
| 			column.search.search = ""; | ||||
| 		}); | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| var createdApiKeyId = GetUriParam('CreatedApiKeyId'); | ||||
|   | ||||
| @@ -18,7 +18,7 @@ | ||||
| 			}, | ||||
| 			function(xhr) | ||||
| 			{ | ||||
| 				console.error(xhr); | ||||
| 				Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response) | ||||
| 			} | ||||
| 		); | ||||
| 	} | ||||
| @@ -31,7 +31,7 @@ | ||||
| 			}, | ||||
| 			function(xhr) | ||||
| 			{ | ||||
| 				console.error(xhr); | ||||
| 				Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response) | ||||
| 			} | ||||
| 		); | ||||
| 	} | ||||
| @@ -99,9 +99,10 @@ $('#product-form input').keydown(function(event) | ||||
| { | ||||
| 	if (event.keyCode === 13) //Enter | ||||
| 	{ | ||||
| 		event.preventDefault(); | ||||
| 		 | ||||
| 		if (document.getElementById('product-form').checkValidity() === false) //There is at least one validation error | ||||
| 		{ | ||||
| 			event.preventDefault(); | ||||
| 			return false; | ||||
| 		} | ||||
| 		else | ||||
|   | ||||
							
								
								
									
										56
									
								
								public/viewjs/productgroupform.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								public/viewjs/productgroupform.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,56 @@ | ||||
| $('#save-product-group-button').on('click', function(e) | ||||
| { | ||||
| 	e.preventDefault(); | ||||
|  | ||||
| 	if (Grocy.EditMode === 'create') | ||||
| 	{ | ||||
| 		Grocy.Api.Post('add-object/product_groups', $('#product-group-form').serializeJSON(), | ||||
| 			function(result) | ||||
| 			{ | ||||
| 				window.location.href = U('/productgroups'); | ||||
| 			}, | ||||
| 			function(xhr) | ||||
| 			{ | ||||
| 				Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response) | ||||
| 			} | ||||
| 		); | ||||
| 	} | ||||
| 	else | ||||
| 	{ | ||||
| 		Grocy.Api.Post('edit-object/product_groups/' + Grocy.EditObjectId, $('#product-group-form').serializeJSON(), | ||||
| 			function(result) | ||||
| 			{ | ||||
| 				window.location.href = U('/productgroups'); | ||||
| 			}, | ||||
| 			function(xhr) | ||||
| 			{ | ||||
| 				Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response) | ||||
| 			} | ||||
| 		); | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| $('#product-group-form input').keyup(function (event) | ||||
| { | ||||
| 	Grocy.FrontendHelpers.ValidateForm('product-group-form'); | ||||
| }); | ||||
|  | ||||
| $('#product-group-form input').keydown(function (event) | ||||
| { | ||||
| 	if (event.keyCode === 13) //Enter | ||||
| 	{ | ||||
| 		event.preventDefault(); | ||||
| 		 | ||||
| 		if (document.getElementById('product-group-form').checkValidity() === false) //There is at least one validation error | ||||
| 		{ | ||||
| 			return false; | ||||
| 		} | ||||
| 		else | ||||
| 		{ | ||||
| 			$('#save-product-group-button').click(); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| $('#name').focus(); | ||||
| Grocy.FrontendHelpers.ValidateForm('product-group-form'); | ||||
							
								
								
									
										67
									
								
								public/viewjs/productgroups.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								public/viewjs/productgroups.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,67 @@ | ||||
| var groupsTable = $('#productgroups-table').DataTable({ | ||||
| 	'paginate': false, | ||||
| 	'order': [[1, 'asc']], | ||||
| 	'columnDefs': [ | ||||
| 		{ 'orderable': false, 'targets': 0 } | ||||
| 	], | ||||
| 	'language': JSON.parse(L('datatables_localization')), | ||||
| 	'scrollY': false, | ||||
| 	'colReorder': true, | ||||
| 	'stateSave': true, | ||||
| 	'stateSaveParams': function(settings, data) | ||||
| 	{ | ||||
| 		data.search.search = ""; | ||||
|  | ||||
| 		data.columns.forEach(column => | ||||
| 		{ | ||||
| 			column.search.search = ""; | ||||
| 		}); | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| $("#search").on("keyup", function() | ||||
| { | ||||
| 	var value = $(this).val(); | ||||
| 	if (value === "all") | ||||
| 	{ | ||||
| 		value = ""; | ||||
| 	} | ||||
| 	 | ||||
| 	groupsTable.search(value).draw(); | ||||
| }); | ||||
|  | ||||
| $(document).on('click', '.product-group-delete-button', function(e) | ||||
| { | ||||
| 	var objectName = $(e.currentTarget).attr('data-group-name'); | ||||
| 	var objectId = $(e.currentTarget).attr('data-group-id'); | ||||
|  | ||||
| 	bootbox.confirm({ | ||||
| 		message: L('Are you sure to delete product group "#1"?', objectName), | ||||
| 		buttons: { | ||||
| 			confirm: { | ||||
| 				label: L('Yes'), | ||||
| 				className: 'btn-success' | ||||
| 			}, | ||||
| 			cancel: { | ||||
| 				label: L('No'), | ||||
| 				className: 'btn-danger' | ||||
| 			} | ||||
| 		}, | ||||
| 		callback: function(result) | ||||
| 		{ | ||||
| 			if (result === true) | ||||
| 			{ | ||||
| 				Grocy.Api.Get('delete-object/product_groups/' + objectId, | ||||
| 					function(result) | ||||
| 					{ | ||||
| 						window.location.href = U('/productgroups'); | ||||
| 					}, | ||||
| 					function(xhr) | ||||
| 					{ | ||||
| 						console.error(xhr); | ||||
| 					} | ||||
| 				); | ||||
| 			} | ||||
| 		} | ||||
| 	}); | ||||
| }); | ||||
| @@ -5,7 +5,18 @@ | ||||
| 		{ 'orderable': false, 'targets': 0 } | ||||
| 	], | ||||
| 	'language': JSON.parse(L('datatables_localization')), | ||||
| 	'scrollY': false | ||||
| 	'scrollY': false, | ||||
| 	'colReorder': true, | ||||
| 	'stateSave': true, | ||||
| 	'stateSaveParams': function(settings, data) | ||||
| 	{ | ||||
| 		data.search.search = ""; | ||||
|  | ||||
| 		data.columns.forEach(column => | ||||
| 		{ | ||||
| 			column.search.search = ""; | ||||
| 		}); | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| $("#search").on("keyup", function() | ||||
|   | ||||
| @@ -9,7 +9,13 @@ | ||||
| 		{ | ||||
| 			var amount = jsonForm.amount * productDetails.product.qu_factor_purchase_to_stock; | ||||
|  | ||||
| 			Grocy.Api.Get('stock/add-product/' + jsonForm.product_id + '/' + amount + '?bestbeforedate=' + Grocy.Components.DateTimePicker.GetValue(), | ||||
| 			var price = ""; | ||||
| 			if (!jsonForm.price.toString().isEmpty()) | ||||
| 			{ | ||||
| 				price = parseFloat(jsonForm.price).toFixed(2); | ||||
| 			} | ||||
|  | ||||
| 			Grocy.Api.Get('stock/add-product/' + jsonForm.product_id + '/' + amount + '?bestbeforedate=' + Grocy.Components.DateTimePicker.GetValue() + '&price=' + price, | ||||
| 				function(result) | ||||
| 				{ | ||||
| 					var addBarcode = GetUriParam('addbarcodetoselection'); | ||||
| @@ -43,11 +49,10 @@ | ||||
| 					else | ||||
| 					{ | ||||
| 						$('#amount').val(0); | ||||
| 						$('#price').val(''); | ||||
| 						Grocy.Components.DateTimePicker.SetValue(''); | ||||
| 						$('#product_id').val(''); | ||||
| 						$('#product_id_text_input').focus(); | ||||
| 						$('#product_id_text_input').val(''); | ||||
| 						$('#product_id_text_input').trigger('change'); | ||||
| 						Grocy.Components.ProductPicker.SetValue(''); | ||||
| 						Grocy.Components.ProductPicker.GetInputElement().focus(); | ||||
| 						Grocy.FrontendHelpers.ValidateForm('purchase-form'); | ||||
| 					} | ||||
| 				}, | ||||
| @@ -64,7 +69,7 @@ | ||||
| 	); | ||||
| }); | ||||
|  | ||||
| $('#product_id').on('change', function(e) | ||||
| Grocy.Components.ProductPicker.GetPicker().on('change', function(e) | ||||
| { | ||||
| 	var productId = $(e.target).val(); | ||||
|  | ||||
| @@ -76,10 +81,21 @@ $('#product_id').on('change', function(e) | ||||
| 			function(productDetails) | ||||
| 			{ | ||||
| 				$('#amount_qu_unit').text(productDetails.quantity_unit_purchase.name); | ||||
| 				$('#price').val(productDetails.last_price); | ||||
| 				 | ||||
| 				if (productDetails.product.default_best_before_days.toString() !== '0') | ||||
| 				{ | ||||
| 					Grocy.Components.DateTimePicker.SetValue(moment().add(productDetails.product.default_best_before_days, 'days').format('YYYY-MM-DD')); | ||||
| 					if (productDetails.product.default_best_before_days == -1) | ||||
| 					{ | ||||
| 						if (!$("#datetimepicker-shortcut").is(":checked")) | ||||
| 						{ | ||||
| 							$("#datetimepicker-shortcut").click(); | ||||
| 						} | ||||
| 					} | ||||
| 					else | ||||
| 					{ | ||||
| 						Grocy.Components.DateTimePicker.SetValue(moment().add(productDetails.product.default_best_before_days, 'days').format('YYYY-MM-DD')); | ||||
| 					} | ||||
| 					$('#amount').focus(); | ||||
| 				} | ||||
| 				else | ||||
| @@ -95,95 +111,23 @@ $('#product_id').on('change', function(e) | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| $('.combobox').combobox({ | ||||
| 	appendId: '_text_input', | ||||
| 	bsVersion: '4' | ||||
| }); | ||||
|  | ||||
| $('#product_id_text_input').on('change', function(e) | ||||
| { | ||||
| 	var input = $('#product_id_text_input').val().toString(); | ||||
| 	var possibleOptionElement = $("#product_id option[data-additional-searchdata*='" + input + "']").first(); | ||||
| 	 | ||||
| 	if (GetUriParam('addbarcodetoselection') === undefined && possibleOptionElement.length > 0) | ||||
| 	{ | ||||
| 		$('#product_id').val(possibleOptionElement.val()); | ||||
| 		$('#product_id').data('combobox').refresh(); | ||||
| 		$('#product_id').trigger('change'); | ||||
| 	} | ||||
| 	else | ||||
| 	{ | ||||
| 		var optionElement = $("#product_id option:contains('" + input + "')").first(); | ||||
| 		if (input.length > 0 && optionElement.length === 0 && GetUriParam('addbarcodetoselection') === undefined	) | ||||
| 		{ | ||||
| 			bootbox.dialog({ | ||||
| 				message: L('"#1" could not be resolved to a product, how do you want to proceed?', input), | ||||
| 				title: L('Create or assign product'), | ||||
| 				onEscape: function() { }, | ||||
| 				size: 'large', | ||||
| 				backdrop: true, | ||||
| 				buttons: { | ||||
| 					cancel: { | ||||
| 						label: 'Cancel', | ||||
| 						className: 'btn-default', | ||||
| 						callback: function() { } | ||||
| 					}, | ||||
| 					addnewproduct: { | ||||
| 						label: '<strong>P</strong> ' + L('Add as new product'), | ||||
| 						className: 'btn-success add-new-product-dialog-button', | ||||
| 						callback: function() | ||||
| 						{ | ||||
| 							window.location.href = U('/product/new?prefillname=' + encodeURIComponent(input) + '&returnto=' + encodeURIComponent(window.location.pathname)); | ||||
| 						} | ||||
| 					}, | ||||
| 					addbarcode: { | ||||
| 						label: '<strong>B</strong> ' + L('Add as barcode to existing product'), | ||||
| 						className: 'btn-info add-new-barcode-dialog-button', | ||||
| 						callback: function() | ||||
| 						{ | ||||
| 							window.location.href = U('/purchase?addbarcodetoselection=' + encodeURIComponent(input)); | ||||
| 						} | ||||
| 					}, | ||||
| 					addnewproductwithbarcode: { | ||||
| 						label: '<strong>A</strong> ' + L('Add as new product and prefill barcode'), | ||||
| 						className: 'btn-warning add-new-product-with-barcode-dialog-button', | ||||
| 						callback: function() | ||||
| 						{ | ||||
| 							window.location.href = U('/product/new?prefillbarcode=' + encodeURIComponent(input) + '&returnto=' + encodeURIComponent(window.location.pathname)); | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
| 			}).on('keypress', function(e) | ||||
| 			{ | ||||
| 				if (e.key === 'B' || e.key === 'b') | ||||
| 				{ | ||||
| 					$('.add-new-barcode-dialog-button').click(); | ||||
| 				} | ||||
| 				if (e.key === 'p' || e.key === 'P') | ||||
| 				{ | ||||
| 					$('.add-new-product-dialog-button').click(); | ||||
| 				} | ||||
| 				if (e.key === 'a' || e.key === 'A') | ||||
| 				{ | ||||
| 					$('.add-new-product-with-barcode-dialog-button').click(); | ||||
| 				} | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| $('#amount').val(0); | ||||
| $('#product_id').val(''); | ||||
| $('#product_id_text_input').focus(); | ||||
| $('#product_id_text_input').val(''); | ||||
| $('#product_id_text_input').trigger('change'); | ||||
| Grocy.FrontendHelpers.ValidateForm('purchase-form'); | ||||
|  | ||||
| if (Grocy.Components.ProductPicker.InProductAddWorkflow() === false) | ||||
| { | ||||
| 	Grocy.Components.ProductPicker.GetInputElement().focus(); | ||||
| } | ||||
| else | ||||
| { | ||||
| 	Grocy.Components.ProductPicker.GetPicker().trigger('change'); | ||||
| } | ||||
|  | ||||
| $('#amount').on('focus', function(e) | ||||
| { | ||||
| 	if ($('#product_id_text_input').val().length === 0) | ||||
| 	if (Grocy.Components.ProductPicker.GetValue().length === 0) | ||||
| 	{ | ||||
| 		$('#product_id_text_input').focus(); | ||||
| 		Grocy.Components.ProductPicker.GetInputElement().focus(); | ||||
| 	} | ||||
| 	else | ||||
| 	{ | ||||
| @@ -200,9 +144,10 @@ $('#purchase-form input').keydown(function(event) | ||||
| { | ||||
| 	if (event.keyCode === 13) //Enter | ||||
| 	{ | ||||
| 		event.preventDefault(); | ||||
| 		 | ||||
| 		if (document.getElementById('purchase-form').checkValidity() === false) //There is at least one validation error | ||||
| 		{ | ||||
| 			event.preventDefault(); | ||||
| 			return false; | ||||
| 		} | ||||
| 		else | ||||
| @@ -212,32 +157,6 @@ $('#purchase-form input').keydown(function(event) | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| var prefillProduct = GetUriParam('createdproduct'); | ||||
| if (prefillProduct !== undefined) | ||||
| { | ||||
| 	var possibleOptionElement = $("#product_id option[data-additional-searchdata*='" + prefillProduct + "']").first(); | ||||
| 	if (possibleOptionElement.length === 0) | ||||
| 	{ | ||||
| 		possibleOptionElement = $("#product_id option:contains('" + prefillProduct + "')").first(); | ||||
| 	} | ||||
|  | ||||
| 	if (possibleOptionElement.length > 0) | ||||
| 	{ | ||||
| 		$('#product_id').val(possibleOptionElement.val()); | ||||
| 		$('#product_id').data('combobox').refresh(); | ||||
| 		$('#product_id').trigger('change'); | ||||
| 		Grocy.Components.DateTimePicker.GetInputElement().focus(); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| var addBarcode = GetUriParam('addbarcodetoselection'); | ||||
| if (addBarcode !== undefined) | ||||
| { | ||||
| 	$('#addbarcodetoselection').text(addBarcode); | ||||
| 	$('#flow-info-addbarcodetoselection').removeClass('d-none'); | ||||
| 	$('#barcode-lookup-disabled-hint').removeClass('d-none'); | ||||
| } | ||||
|  | ||||
| Grocy.Components.DateTimePicker.GetInputElement().on('change', function(e) | ||||
| { | ||||
| 	Grocy.FrontendHelpers.ValidateForm('purchase-form'); | ||||
|   | ||||
| @@ -11,7 +11,7 @@ | ||||
| 			}, | ||||
| 			function(xhr) | ||||
| 			{ | ||||
| 				console.error(xhr); | ||||
| 				Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response) | ||||
| 			} | ||||
| 		); | ||||
| 	} | ||||
| @@ -24,7 +24,7 @@ | ||||
| 			}, | ||||
| 			function(xhr) | ||||
| 			{ | ||||
| 				console.error(xhr); | ||||
| 				Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response) | ||||
| 			} | ||||
| 		); | ||||
| 	} | ||||
| @@ -39,9 +39,10 @@ $('#quantityunit-form input').keydown(function(event) | ||||
| { | ||||
| 	if (event.keyCode === 13) //Enter | ||||
| 	{ | ||||
| 		event.preventDefault(); | ||||
| 		 | ||||
| 		if (document.getElementById('quantityunit-form').checkValidity() === false) //There is at least one validation error | ||||
| 		{ | ||||
| 			event.preventDefault(); | ||||
| 			return false; | ||||
| 		} | ||||
| 		else | ||||
|   | ||||
| @@ -5,7 +5,18 @@ | ||||
| 		{ 'orderable': false, 'targets': 0 } | ||||
| 	], | ||||
| 	'language': JSON.parse(L('datatables_localization')), | ||||
| 	'scrollY': false | ||||
| 	'scrollY': false, | ||||
| 	'colReorder': true, | ||||
| 	'stateSave': true, | ||||
| 	'stateSaveParams': function(settings, data) | ||||
| 	{ | ||||
| 		data.search.search = ""; | ||||
|  | ||||
| 		data.columns.forEach(column => | ||||
| 		{ | ||||
| 			column.search.search = ""; | ||||
| 		}); | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| $("#search").on("keyup", function() | ||||
|   | ||||
							
								
								
									
										171
									
								
								public/viewjs/recipeform.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										171
									
								
								public/viewjs/recipeform.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,171 @@ | ||||
| $('#save-recipe-button').on('click', function(e) | ||||
| { | ||||
| 	e.preventDefault(); | ||||
|  | ||||
| 	Grocy.Api.Post('edit-object/recipes/' + Grocy.EditObjectId, $('#recipe-form').serializeJSON(), | ||||
| 		function(result) | ||||
| 		{ | ||||
| 			window.location.href = U('/recipes'); | ||||
| 		}, | ||||
| 		function(xhr) | ||||
| 		{ | ||||
| 			console.error(xhr); | ||||
| 		} | ||||
| 	); | ||||
| }); | ||||
|  | ||||
| var recipesPosTables = $('#recipes-pos-table').DataTable({ | ||||
| 	'paginate': false, | ||||
| 	'order': [[1, 'asc']], | ||||
| 	'columnDefs': [ | ||||
| 		{ 'orderable': false, 'targets': 0 } | ||||
| 	], | ||||
| 	'language': JSON.parse(L('datatables_localization')), | ||||
| 	'scrollY': false, | ||||
| 	'colReorder': true, | ||||
| 	'stateSave': true, | ||||
| 	'stateSaveParams': function(settings, data) | ||||
| 	{ | ||||
| 		data.search.search = ""; | ||||
|  | ||||
| 		data.columns.forEach(column => | ||||
| 		{ | ||||
| 			column.search.search = ""; | ||||
| 		}); | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| $("#search").on("keyup", function () | ||||
| { | ||||
| 	var value = $(this).val(); | ||||
| 	if (value === "all") | ||||
| 	{ | ||||
| 		value = ""; | ||||
| 	} | ||||
|  | ||||
| 	recipesPosTables.search(value).draw(); | ||||
| }); | ||||
|  | ||||
| Grocy.FrontendHelpers.ValidateForm('recipe-form'); | ||||
| $("#name").focus(); | ||||
|  | ||||
| $('#recipe-form input').keyup(function (event) | ||||
| { | ||||
| 	Grocy.FrontendHelpers.ValidateForm('recipe-form'); | ||||
| }); | ||||
|  | ||||
| $('#recipe-form input').keydown(function (event) | ||||
| { | ||||
| 	if (event.keyCode === 13) //Enter | ||||
| 	{ | ||||
| 		event.preventDefault(); | ||||
| 		 | ||||
| 		if (document.getElementById('recipe-form').checkValidity() === false) //There is at least one validation error | ||||
| 		{ | ||||
| 			return false; | ||||
| 		} | ||||
| 		else | ||||
| 		{ | ||||
| 			$('#save-recipe-button').click(); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| $(document).on('click', '.recipe-pos-delete-button', function(e) | ||||
| { | ||||
| 	var objectName = $(e.currentTarget).attr('data-recipe-pos-name'); | ||||
| 	var objectId = $(e.currentTarget).attr('data-recipe-pos-id'); | ||||
|  | ||||
| 	bootbox.confirm({ | ||||
| 		message: L('Are you sure to delete recipe ingredient "#1"?', objectName), | ||||
| 		buttons: { | ||||
| 			confirm: { | ||||
| 				label: L('Yes'), | ||||
| 				className: 'btn-success' | ||||
| 			}, | ||||
| 			cancel: { | ||||
| 				label: L('No'), | ||||
| 				className: 'btn-danger' | ||||
| 			} | ||||
| 		}, | ||||
| 		callback: function(result) | ||||
| 		{ | ||||
| 			if (result === true) | ||||
| 			{ | ||||
| 				Grocy.Api.Post('edit-object/recipes/' + Grocy.EditObjectId, $('#recipe-form').serializeJSON(), function() { }, function() { }); | ||||
| 				Grocy.Api.Get('delete-object/recipes_pos/' + objectId, | ||||
| 					function(result) | ||||
| 					{ | ||||
| 						window.location.href = U('/recipe/' + Grocy.EditObjectId); | ||||
| 					}, | ||||
| 					function(xhr) | ||||
| 					{ | ||||
| 						console.error(xhr); | ||||
| 					} | ||||
| 				); | ||||
| 			} | ||||
| 		} | ||||
| 	}); | ||||
| }); | ||||
|  | ||||
| $(document).on('click', '.recipe-pos-order-missing-button', function(e) | ||||
| { | ||||
| 	var productName = $(e.currentTarget).attr('data-product-name'); | ||||
| 	var productId = $(e.currentTarget).attr('data-product-id'); | ||||
| 	var productAmount = $(e.currentTarget).attr('data-product-amount'); | ||||
| 	var recipeName = $(e.currentTarget).attr('data-recipe-name'); | ||||
|  | ||||
| 	var jsonData = {}; | ||||
| 	jsonData.product_id = productId; | ||||
| 	jsonData.amount = productAmount; | ||||
| 	jsonData.note = L('Added for recipe #1', recipeName); | ||||
|  | ||||
| 	Grocy.Api.Post('add-object/shopping_list', jsonData, | ||||
| 		function(result) | ||||
| 		{ | ||||
| 			Grocy.Api.Post('edit-object/recipes/' + Grocy.EditObjectId, $('#recipe-form').serializeJSON(), function () { }, function () { }); | ||||
| 			window.location.href = U('/recipe/' + Grocy.EditObjectId); | ||||
| 		}, | ||||
| 		function(xhr) | ||||
| 		{ | ||||
| 			console.error(xhr); | ||||
| 		} | ||||
| 	); | ||||
| }); | ||||
|  | ||||
| $(document).on('click', '.recipe-pos-show-note-button', function(e) | ||||
| { | ||||
| 	var note = $(e.currentTarget).attr('data-recipe-pos-note'); | ||||
|  | ||||
| 	bootbox.alert(note); | ||||
| }); | ||||
|  | ||||
| $(document).on('click', '.recipe-pos-edit-button', function (e) | ||||
| { | ||||
| 	var recipePosId = $(e.currentTarget).attr('data-recipe-pos-id'); | ||||
|  | ||||
| 	Grocy.Api.Post('edit-object/recipes/' + Grocy.EditObjectId, $('#recipe-form').serializeJSON(), | ||||
| 		function(result) | ||||
| 		{ | ||||
| 			window.location.href = U('/recipe/' + Grocy.EditObjectId + '/pos/' + recipePosId); | ||||
| 		}, | ||||
| 		function(xhr) | ||||
| 		{ | ||||
| 			console.error(xhr); | ||||
| 		} | ||||
| 	); | ||||
| }); | ||||
|  | ||||
| $("#recipe-pos-add-button").on("click", function(e) | ||||
| { | ||||
| 	Grocy.Api.Post('edit-object/recipes/' + Grocy.EditObjectId, $('#recipe-form').serializeJSON(), | ||||
| 		function(result) | ||||
| 		{ | ||||
| 			window.location.href = U('/recipe/' + Grocy.EditObjectId + '/pos/new'); | ||||
| 		}, | ||||
| 		function(xhr) | ||||
| 		{ | ||||
| 			console.error(xhr); | ||||
| 		} | ||||
| 	); | ||||
| }); | ||||
							
								
								
									
										115
									
								
								public/viewjs/recipeposform.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								public/viewjs/recipeposform.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,115 @@ | ||||
| $('#save-recipe-pos-button').on('click', function(e) | ||||
| { | ||||
| 	e.preventDefault(); | ||||
|  | ||||
| 	var jsonData = $('#recipe-pos-form').serializeJSON({ checkboxUncheckedValue: "0" }); | ||||
| 	jsonData.recipe_id = Grocy.EditObjectParentId; | ||||
| 	console.log(jsonData); | ||||
| 	if (Grocy.EditMode === 'create') | ||||
| 	{ | ||||
| 		Grocy.Api.Post('add-object/recipes_pos', jsonData, | ||||
| 			function(result) | ||||
| 			{ | ||||
| 				window.location.href = U('/recipe/' + Grocy.EditObjectParentId); | ||||
| 			}, | ||||
| 			function(xhr) | ||||
| 			{ | ||||
| 				Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response) | ||||
| 			} | ||||
| 		); | ||||
| 	} | ||||
| 	else | ||||
| 	{ | ||||
| 		Grocy.Api.Post('edit-object/recipes_pos/' + Grocy.EditObjectId, jsonData, | ||||
| 			function(result) | ||||
| 			{ | ||||
| 				window.location.href = U('/recipe/' + Grocy.EditObjectParentId); | ||||
| 			}, | ||||
| 			function(xhr) | ||||
| 			{ | ||||
| 				Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response) | ||||
| 			} | ||||
| 		); | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| Grocy.Components.ProductPicker.GetPicker().on('change', function(e) | ||||
| { | ||||
| 	var productId = $(e.target).val(); | ||||
|  | ||||
| 	if (productId) | ||||
| 	{ | ||||
| 		Grocy.Components.ProductCard.Refresh(productId); | ||||
| 		 | ||||
| 		Grocy.Api.Get('stock/get-product-details/' + productId, | ||||
| 			function (productDetails) | ||||
| 			{ | ||||
| 				if (!$("#only_check_single_unit_in_stock").is(":checked")) | ||||
| 				{ | ||||
| 					$("#qu_id").val(productDetails.quantity_unit_stock.id); | ||||
| 				} | ||||
| 				$('#amount').focus(); | ||||
| 				Grocy.FrontendHelpers.ValidateForm('recipe-pos-form'); | ||||
| 			}, | ||||
| 			function(xhr) | ||||
| 			{ | ||||
| 				console.error(xhr); | ||||
| 			} | ||||
| 		); | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| Grocy.FrontendHelpers.ValidateForm('recipe-pos-form'); | ||||
|  | ||||
| if (Grocy.Components.ProductPicker.InProductAddWorkflow() === false) | ||||
| { | ||||
| 	Grocy.Components.ProductPicker.GetInputElement().focus(); | ||||
| } | ||||
| Grocy.Components.ProductPicker.GetPicker().trigger('change'); | ||||
|  | ||||
| $('#amount').on('focus', function(e) | ||||
| { | ||||
| 	if (Grocy.Components.ProductPicker.GetValue().length === 0) | ||||
| 	{ | ||||
| 		Grocy.Components.ProductPicker.GetInputElement().focus(); | ||||
| 	} | ||||
| 	else | ||||
| 	{ | ||||
| 		$(this).select(); | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| $('#recipe-pos-form input').keyup(function(event) | ||||
| { | ||||
| 	Grocy.FrontendHelpers.ValidateForm('recipe-pos-form'); | ||||
| }); | ||||
|  | ||||
| $('#recipe-pos-form input').keydown(function(event) | ||||
| { | ||||
| 	if (event.keyCode === 13) //Enter | ||||
| 	{ | ||||
| 		event.preventDefault(); | ||||
| 		 | ||||
| 		if (document.getElementById('recipe-pos-form').checkValidity() === false) //There is at least one validation error | ||||
| 		{ | ||||
| 			return false; | ||||
| 		} | ||||
| 		else | ||||
| 		{ | ||||
| 			$('#save-recipe-pos-button').click(); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| $("#only_check_single_unit_in_stock").on("click", function() | ||||
| { | ||||
| 	if (this.checked) | ||||
| 	{ | ||||
| 		$("#qu_id").removeAttr("disabled"); | ||||
| 	} | ||||
| 	else | ||||
| 	{ | ||||
| 		$("#qu_id").attr("disabled", ""); | ||||
| 		Grocy.Components.ProductPicker.GetPicker().trigger("change"); | ||||
| 	} | ||||
| }); | ||||
							
								
								
									
										161
									
								
								public/viewjs/recipes.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										161
									
								
								public/viewjs/recipes.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,161 @@ | ||||
| var recipesTables = $('#recipes-table').DataTable({ | ||||
| 	'paginate': false, | ||||
| 	'order': [[0, 'asc']], | ||||
| 	'language': JSON.parse(L('datatables_localization')), | ||||
| 	'scrollY': false, | ||||
| 	'colReorder': true, | ||||
| 	'stateSave': true, | ||||
| 	'stateSaveParams': function(settings, data) | ||||
| 	{ | ||||
| 		data.search.search = ""; | ||||
|  | ||||
| 		data.columns.forEach(column => | ||||
| 		{ | ||||
| 			column.search.search = ""; | ||||
| 		}); | ||||
| 	}, | ||||
| 	'select': 'single', | ||||
| 	'initComplete': function() | ||||
| 	{ | ||||
| 		this.api().row({ order: 'current' }, 0).select(); | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| var rowSelect = GetUriParam("row"); | ||||
| if (typeof rowSelect !== "undefined") | ||||
| { | ||||
| 	recipesTables.row(rowSelect).select(); | ||||
| } | ||||
|  | ||||
| $("#search").on("keyup", function() | ||||
| { | ||||
| 	var value = $(this).val(); | ||||
| 	if (value === "all") | ||||
| 	{ | ||||
| 		value = ""; | ||||
| 	} | ||||
| 	 | ||||
| 	recipesTables.search(value).draw(); | ||||
| }); | ||||
|  | ||||
| $("#selectedRecipeDeleteButton").on('click', function(e) | ||||
| { | ||||
| 	var objectName = $(e.currentTarget).attr('data-recipe-name'); | ||||
| 	var objectId = $(e.currentTarget).attr('data-recipe-id'); | ||||
|  | ||||
| 	bootbox.confirm({ | ||||
| 		message: L('Are you sure to delete recipe "#1"?', objectName), | ||||
| 		buttons: { | ||||
| 			confirm: { | ||||
| 				label: L('Yes'), | ||||
| 				className: 'btn-success' | ||||
| 			}, | ||||
| 			cancel: { | ||||
| 				label: L('No'), | ||||
| 				className: 'btn-danger' | ||||
| 			} | ||||
| 		}, | ||||
| 		callback: function(result) | ||||
| 		{ | ||||
| 			if (result === true) | ||||
| 			{ | ||||
| 				Grocy.Api.Get('delete-object/recipes/' + objectId, | ||||
| 					function(result) | ||||
| 					{ | ||||
| 						window.location.href = U('/recipes'); | ||||
| 					}, | ||||
| 					function(xhr) | ||||
| 					{ | ||||
| 						console.error(xhr); | ||||
| 					} | ||||
| 				); | ||||
| 			} | ||||
| 		} | ||||
| 	}); | ||||
| }); | ||||
|  | ||||
| $(document).on('click', '.recipe-order-missing-button', function(e) | ||||
| { | ||||
| 	var objectName = $(e.currentTarget).attr('data-recipe-name'); | ||||
| 	var objectId = $(e.currentTarget).attr('data-recipe-id'); | ||||
|  | ||||
| 	bootbox.confirm({ | ||||
| 		message: L('Are you sure to put all missing ingredients for recipe "#1" on the shopping list?', objectName), | ||||
| 		buttons: { | ||||
| 			confirm: { | ||||
| 				label: L('Yes'), | ||||
| 				className: 'btn-success' | ||||
| 			}, | ||||
| 			cancel: { | ||||
| 				label: L('No'), | ||||
| 				className: 'btn-danger' | ||||
| 			} | ||||
| 		}, | ||||
| 		callback: function(result) | ||||
| 		{ | ||||
| 			if (result === true) | ||||
| 			{ | ||||
| 				Grocy.Api.Get('recipes/add-not-fulfilled-products-to-shopping-list/' + objectId, | ||||
| 					function(result) | ||||
| 					{ | ||||
| 						window.location.href = U('/recipes'); | ||||
| 					}, | ||||
| 					function(xhr) | ||||
| 					{ | ||||
| 						console.error(xhr); | ||||
| 					} | ||||
| 				); | ||||
| 			} | ||||
| 		} | ||||
| 	}); | ||||
| }); | ||||
|  | ||||
| $("#selectedRecipeConsumeButton").on('click', function(e) | ||||
| { | ||||
| 	var objectName = $(e.currentTarget).attr('data-recipe-name'); | ||||
| 	var objectId = $(e.currentTarget).attr('data-recipe-id'); | ||||
|  | ||||
| 	bootbox.confirm({ | ||||
| 		message: L('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)?', objectName), | ||||
| 		buttons: { | ||||
| 			confirm: { | ||||
| 				label: L('Yes'), | ||||
| 				className: 'btn-success' | ||||
| 			}, | ||||
| 			cancel: { | ||||
| 				label: L('No'), | ||||
| 				className: 'btn-danger' | ||||
| 			} | ||||
| 		}, | ||||
| 		callback: function(result) | ||||
| 		{ | ||||
| 			if (result === true) | ||||
| 			{ | ||||
| 				Grocy.Api.Get('recipes/consume-recipe/' + objectId, | ||||
| 					function(result) | ||||
| 					{ | ||||
| 						toastr.success(L('Removed all ingredients of recipe "#1" from stock', objectName)); | ||||
| 					}, | ||||
| 					function(xhr) | ||||
| 					{ | ||||
| 						console.error(xhr); | ||||
| 					} | ||||
| 				); | ||||
| 			} | ||||
| 		} | ||||
| 	}); | ||||
| }); | ||||
|  | ||||
| recipesTables.on('select', function(e, dt, type, indexes) | ||||
| { | ||||
| 	if (type === 'row') | ||||
| 	{ | ||||
| 		var selectedRecipeId = $(recipesTables.row(indexes[0]).node()).data("recipe-id"); | ||||
| 		window.location.href = U('/recipes?recipe=' + selectedRecipeId.toString() + "&row=" + indexes[0].toString()); | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| $("#selectedRecipeToggleFullscreenButton").on('click', function(e) | ||||
| { | ||||
| 	$("#selectedRecipeCard").toggleClass("fullscreen"); | ||||
| }); | ||||
| @@ -1,11 +1,27 @@ | ||||
| var shoppingListTable = $('#shoppinglist-table').DataTable({ | ||||
| 	'paginate': false, | ||||
| 	'order': [[1, 'asc']], | ||||
| 	"orderFixed": [[3, 'asc']], | ||||
| 	'columnDefs': [ | ||||
| 		{ 'orderable': false, 'targets': 0 } | ||||
| 		{ 'orderable': false, 'targets': 0 }, | ||||
| 		{ 'visible': false, 'targets': 3 } | ||||
| 	], | ||||
| 	'language': JSON.parse(L('datatables_localization')), | ||||
| 	'scrollY': false | ||||
| 	'scrollY': false, | ||||
| 	'colReorder': true, | ||||
| 	'stateSave': true, | ||||
| 	'stateSaveParams': function(settings, data) | ||||
| 	{ | ||||
| 		data.search.search = ""; | ||||
|  | ||||
| 		data.columns.forEach(column => | ||||
| 		{ | ||||
| 			column.search.search = ""; | ||||
| 		}); | ||||
| 	}, | ||||
| 	'rowGroup': { | ||||
| 		dataSrc: 3 | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| $("#search").on("keyup", function() | ||||
| @@ -19,12 +35,40 @@ $("#search").on("keyup", function() | ||||
| 	shoppingListTable.search(value).draw(); | ||||
| }); | ||||
|  | ||||
| $("#status-filter").on("change", function() | ||||
| { | ||||
| 	var value = $(this).val(); | ||||
| 	if (value === "all") | ||||
| 	{ | ||||
| 		value = ""; | ||||
| 	} | ||||
|  | ||||
| 	// Transfer CSS classes of selected element to dropdown element (for background) | ||||
| 	$(this).attr("class", $("#" + $(this).attr("id") + " option[value='" + value + "']").attr("class") + " form-control"); | ||||
| 	 | ||||
| 	shoppingListTable.column(4).search(value).draw(); | ||||
| }); | ||||
|  | ||||
| $(".status-filter-button").on("click", function() | ||||
| { | ||||
| 	var value = $(this).data("status-filter"); | ||||
| 	$("#status-filter").val(value); | ||||
| 	$("#status-filter").trigger("change"); | ||||
| }); | ||||
|  | ||||
| $(document).on('click', '.shoppinglist-delete-button', function (e) | ||||
| { | ||||
| 	Grocy.Api.Get('delete-object/shopping_list/' + $(e.currentTarget).attr('data-shoppinglist-id'), | ||||
| 	e.preventDefault(); | ||||
| 	 | ||||
| 	var shoppingListItemId = $(e.currentTarget).attr('data-shoppinglist-id'); | ||||
|  | ||||
| 	Grocy.Api.Get('delete-object/shopping_list/' + shoppingListItemId, | ||||
| 		function(result) | ||||
| 		{ | ||||
| 			window.location.href = U('/shoppinglist'); | ||||
| 			$('#shoppinglistitem-' + shoppingListItemId + '-row').fadeOut(500, function() | ||||
| 			{ | ||||
| 				$(this).remove(); | ||||
| 			}); | ||||
| 		}, | ||||
| 		function(xhr) | ||||
| 		{ | ||||
| @@ -46,3 +90,39 @@ $(document).on('click', '#add-products-below-min-stock-amount', function(e) | ||||
| 		} | ||||
| 	); | ||||
| }); | ||||
|  | ||||
| $(document).on('click', '#clear-shopping-list', function(e) | ||||
| { | ||||
| 	bootbox.confirm({ | ||||
| 		message: L('Are you sure to empty the shopping list?'), | ||||
| 		buttons: { | ||||
| 			confirm: { | ||||
| 				label: L('Yes'), | ||||
| 				className: 'btn-success' | ||||
| 			}, | ||||
| 			cancel: { | ||||
| 				label: L('No'), | ||||
| 				className: 'btn-danger' | ||||
| 			} | ||||
| 		}, | ||||
| 		callback: function(result) | ||||
| 		{ | ||||
| 			if (result === true) | ||||
| 			{ | ||||
| 				Grocy.Api.Get('stock/clear-shopping-list', | ||||
| 					function(result) | ||||
| 					{ | ||||
| 						$('#shoppinglist-table tbody tr').fadeOut(500, function() | ||||
| 						{ | ||||
| 							$(this).remove(); | ||||
| 						}); | ||||
| 					}, | ||||
| 					function(xhr) | ||||
| 					{ | ||||
| 						console.error(xhr); | ||||
| 					} | ||||
| 				); | ||||
| 			} | ||||
| 		} | ||||
| 	}); | ||||
| }); | ||||
|   | ||||
| @@ -30,7 +30,7 @@ | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| $('#product_id').on('change', function(e) | ||||
| Grocy.Components.ProductPicker.GetPicker().on('change', function(e) | ||||
| { | ||||
| 	var productId = $(e.target).val(); | ||||
|  | ||||
| @@ -43,6 +43,7 @@ $('#product_id').on('change', function(e) | ||||
| 			{ | ||||
| 				$('#amount_qu_unit').text(productDetails.quantity_unit_purchase.name); | ||||
| 				$('#amount').focus(); | ||||
| 				Grocy.FrontendHelpers.ValidateForm('shoppinglist-form'); | ||||
| 			}, | ||||
| 			function(xhr) | ||||
| 			{ | ||||
| @@ -52,39 +53,22 @@ $('#product_id').on('change', function(e) | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| $('.combobox').combobox({ | ||||
| 	appendId: '_text_input' | ||||
| }); | ||||
|  | ||||
| $('#product_id_text_input').on('change', function(e) | ||||
| { | ||||
| 	var input = $('#product_id_text_input').val().toString(); | ||||
| 	var possibleOptionElement = $("#product_id option[data-additional-searchdata*='" + input + "']").first(); | ||||
|  | ||||
| 	if (possibleOptionElement.length > 0 && possibleOptionElement.text().length > 0) | ||||
| 	{ | ||||
| 		$('#product_id').val(possibleOptionElement.val()); | ||||
| 		$('#product_id').data('combobox').refresh(); | ||||
| 		$('#product_id').trigger('change'); | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| $('#product_id_text_input').focus(); | ||||
| $('#product_id_text_input').trigger('change'); | ||||
|  | ||||
| if (Grocy.EditMode === 'edit') | ||||
| { | ||||
| 	$('#product_id').addClass('suppress-next-custom-validate-event'); | ||||
| 	$('#product_id').trigger('change'); | ||||
| } | ||||
|  | ||||
| Grocy.FrontendHelpers.ValidateForm('shoppinglist-form'); | ||||
|  | ||||
| if (Grocy.Components.ProductPicker.InProductAddWorkflow() === false) | ||||
| { | ||||
| 	Grocy.Components.ProductPicker.GetInputElement().focus(); | ||||
| } | ||||
| else | ||||
| { | ||||
| 	Grocy.Components.ProductPicker.GetPicker().trigger('change'); | ||||
| } | ||||
|  | ||||
| $('#amount').on('focus', function(e) | ||||
| { | ||||
| 	if ($('#product_id_text_input').val().length === 0) | ||||
| 	if (Grocy.Components.ProductPicker.GetValue().length === 0) | ||||
| 	{ | ||||
| 		$('#product_id_text_input').focus(); | ||||
| 		Grocy.Components.ProductPicker.GetInputElement().focus(); | ||||
| 	} | ||||
| 	else | ||||
| 	{ | ||||
| @@ -101,9 +85,10 @@ $('#shoppinglist-form input').keydown(function (event) | ||||
| { | ||||
| 	if (event.keyCode === 13) //Enter | ||||
| 	{ | ||||
| 		event.preventDefault(); | ||||
| 		 | ||||
| 		if (document.getElementById('shoppinglist-form').checkValidity() === false) //There is at least one validation error | ||||
| 		{ | ||||
| 			event.preventDefault(); | ||||
| 			return false; | ||||
| 		} | ||||
| 		else | ||||
|   | ||||
| @@ -6,7 +6,18 @@ | ||||
| 		{ 'visible': false, 'targets': 4 } | ||||
| 	], | ||||
| 	'language': JSON.parse(L('datatables_localization')), | ||||
| 	'scrollY': false | ||||
| 	'scrollY': false, | ||||
| 	'colReorder': true, | ||||
| 	'stateSave': true, | ||||
| 	'stateSaveParams': function(settings, data) | ||||
| 	{ | ||||
| 		data.search.search = ""; | ||||
|  | ||||
| 		data.columns.forEach(column => | ||||
| 		{ | ||||
| 			column.search.search = ""; | ||||
| 		}); | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| $("#location-filter").on("change", function() | ||||
| @@ -20,6 +31,27 @@ $("#location-filter").on("change", function() | ||||
| 	stockOverviewTable.column(4).search(value).draw(); | ||||
| }); | ||||
|  | ||||
| $("#status-filter").on("change", function() | ||||
| { | ||||
| 	var value = $(this).val(); | ||||
| 	if (value === "all") | ||||
| 	{ | ||||
| 		value = ""; | ||||
| 	} | ||||
|  | ||||
| 	// Transfer CSS classes of selected element to dropdown element (for background) | ||||
| 	$(this).attr("class", $("#" + $(this).attr("id") + " option[value='" + value + "']").attr("class") + " form-control"); | ||||
| 	 | ||||
| 	stockOverviewTable.column(5).search(value).draw(); | ||||
| }); | ||||
|  | ||||
| $(".status-filter-button").on("click", function() | ||||
| { | ||||
| 	var value = $(this).data("status-filter"); | ||||
| 	$("#status-filter").val(value); | ||||
| 	$("#status-filter").trigger("change"); | ||||
| }); | ||||
|  | ||||
| $("#search").on("keyup", function() | ||||
| { | ||||
| 	var value = $(this).val(); | ||||
| @@ -33,34 +65,74 @@ $("#search").on("keyup", function() | ||||
|  | ||||
| $(document).on('click', '.product-consume-button', function(e) | ||||
| { | ||||
| 	e.preventDefault(); | ||||
|  | ||||
| 	// Remove the focus from the current button | ||||
| 	// to prevent that the tooltip stays until clicked anywhere else | ||||
| 	document.activeElement.blur(); | ||||
| 	 | ||||
| 	var productId = $(e.currentTarget).attr('data-product-id'); | ||||
| 	var productName = $(e.currentTarget).attr('data-product-name'); | ||||
| 	var productQuName = $(e.currentTarget).attr('data-product-qu-name'); | ||||
| 	var consumeAmount = $(e.currentTarget).attr('data-consume-amount'); | ||||
|  | ||||
| 	Grocy.Api.Get('stock/consume-product/' + productId + '/' + consumeAmount, | ||||
| 		function(result) | ||||
| 		function() | ||||
| 		{ | ||||
| 			var oldAmount = parseInt($('#product-' + productId + '-amount').text()); | ||||
| 			var newAmount = oldAmount - consumeAmount; | ||||
| 			if (newAmount === 0) | ||||
| 			{ | ||||
| 				$('#product-' + productId + '-row').fadeOut(500, function() | ||||
| 			Grocy.Api.Get('stock/get-product-details/' + productId, | ||||
| 				function(result) | ||||
| 				{ | ||||
| 					$(this).remove(); | ||||
| 				}); | ||||
| 			}	 | ||||
| 			else | ||||
| 			{ | ||||
| 				$('#product-' + productId + '-amount').parent().effect('highlight', { }, 500); | ||||
| 				$('#product-' + productId + '-amount').fadeOut(500, function() | ||||
| 				{ | ||||
| 					$(this).text(newAmount).fadeIn(500); | ||||
| 				}); | ||||
| 				$('#product-' + productId + '-consume-all-button').attr('data-consume-amount', newAmount); | ||||
| 			}	 | ||||
| 					var productRow = $('#product-' + productId + '-row'); | ||||
| 					var expiringThreshold = moment().add("-" + $("#info-expiring-products").data("next-x-days"), "days"); | ||||
| 					var now = moment(); | ||||
| 					var nextBestBeforeDate = moment(result.next_best_before_date); | ||||
|  | ||||
| 			toastr.success(L('Removed #1 #2 of #3 from stock', consumeAmount, productQuName, productName)); | ||||
| 					productRow.removeClass("table-warning"); | ||||
| 					productRow.removeClass("table-danger"); | ||||
| 					if (now.isAfter(nextBestBeforeDate)) | ||||
| 					{ | ||||
| 						productRow.addClass("table-danger"); | ||||
| 					} | ||||
| 					if (expiringThreshold.isAfter(nextBestBeforeDate)) | ||||
| 					{ | ||||
| 						productRow.addClass("table-warning"); | ||||
| 					} | ||||
|  | ||||
| 					var oldAmount = parseInt($('#product-' + productId + '-amount').text()); | ||||
| 					var newAmount = oldAmount - consumeAmount; | ||||
| 					if (newAmount === 0) | ||||
| 					{ | ||||
| 						$('#product-' + productId + '-row').fadeOut(500, function() | ||||
| 						{ | ||||
| 							$(this).remove(); | ||||
| 						}); | ||||
| 					}	 | ||||
| 					else | ||||
| 					{ | ||||
| 						$('#product-' + productId + '-amount').parent().effect('highlight', { }, 500); | ||||
| 						$('#product-' + productId + '-amount').fadeOut(500, function() | ||||
| 						{ | ||||
| 							$(this).text(newAmount).fadeIn(500); | ||||
| 						}); | ||||
| 						$('#product-' + productId + '-consume-all-button').attr('data-consume-amount', newAmount); | ||||
|  | ||||
| 						$('#product-' + productId + '-next-best-before-date').parent().effect('highlight', { }, 500); | ||||
| 						$('#product-' + productId + '-next-best-before-date').fadeOut(500, function() | ||||
| 						{ | ||||
| 							$(this).text(result.next_best_before_date).fadeIn(500); | ||||
| 						}); | ||||
| 						$('#product-' + productId + '-next-best-before-date-timeago').attr('datetime', result.next_best_before_date); | ||||
| 					}	 | ||||
|  | ||||
| 					toastr.success(L('Removed #1 #2 of #3 from stock', consumeAmount, productQuName, productName)); | ||||
| 					RefreshContextualTimeago(); | ||||
| 					RefreshStatistics(); | ||||
| 				}, | ||||
| 				function(xhr) | ||||
| 				{ | ||||
| 					console.error(xhr); | ||||
| 				} | ||||
| 			); | ||||
| 		}, | ||||
| 		function(xhr) | ||||
| 		{ | ||||
| @@ -68,3 +140,37 @@ $(document).on('click', '.product-consume-button', function(e) | ||||
| 		} | ||||
| 	); | ||||
| }); | ||||
|  | ||||
| function RefreshStatistics() | ||||
| { | ||||
| 	Grocy.Api.Get('stock/get-current-stock', | ||||
| 		function(result) | ||||
| 		{ | ||||
| 			var amountSum = 0; | ||||
| 			result.forEach(element => { | ||||
| 				amountSum += parseInt(element.amount); | ||||
| 			}); | ||||
| 			$("#info-current-stock").text(result.length + " " + Pluralize(result.length, L('Product'), L('Products')) + ", " + amountSum.toString() + " " + Pluralize(amountSum, L('Unit'), L('Units'))); | ||||
| 		}, | ||||
| 		function(xhr) | ||||
| 		{ | ||||
| 			console.error(xhr); | ||||
| 		} | ||||
| 	); | ||||
|  | ||||
| 	var nextXDays = $("#info-expiring-products").data("next-x-days"); | ||||
| 	Grocy.Api.Get('stock/get-current-volatil-stock?expiring_days=' + nextXDays, | ||||
| 		function(result) | ||||
| 		{ | ||||
| 			$("#info-expiring-products").text(Pluralize(result.expiring_products.length, L('#1 product expires within the next #2 days', result.expiring_products.length, nextXDays), L('#1 products expiring within the next #2 days', result.expiring_products.length, nextXDays))); | ||||
| 			$("#info-expired-products").text(Pluralize(result.expired_products.length, L('#1 product is already expired', result.expired_products.length), L('#1 products are already expired', result.expired_products.length))); | ||||
| 			$("#info-missing-products").text(Pluralize(result.missing_products.length, L('#1 product is below defined min. stock amount', result.missing_products.length), L('#1 products are below defined min. stock amount', result.missing_products.length))); | ||||
| 		}, | ||||
| 		function(xhr) | ||||
| 		{ | ||||
| 			console.error(xhr); | ||||
| 		} | ||||
| 	); | ||||
| } | ||||
|  | ||||
| RefreshStatistics(); | ||||
|   | ||||
							
								
								
									
										67
									
								
								public/viewjs/taskcategories.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								public/viewjs/taskcategories.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,67 @@ | ||||
| var categoriesTable = $('#taskcategories-table').DataTable({ | ||||
| 	'paginate': false, | ||||
| 	'order': [[1, 'asc']], | ||||
| 	'columnDefs': [ | ||||
| 		{ 'orderable': false, 'targets': 0 } | ||||
| 	], | ||||
| 	'language': JSON.parse(L('datatables_localization')), | ||||
| 	'scrollY': false, | ||||
| 	'colReorder': true, | ||||
| 	'stateSave': true, | ||||
| 	'stateSaveParams': function(settings, data) | ||||
| 	{ | ||||
| 		data.search.search = ""; | ||||
|  | ||||
| 		data.columns.forEach(column => | ||||
| 		{ | ||||
| 			column.search.search = ""; | ||||
| 		}); | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| $("#search").on("keyup", function() | ||||
| { | ||||
| 	var value = $(this).val(); | ||||
| 	if (value === "all") | ||||
| 	{ | ||||
| 		value = ""; | ||||
| 	} | ||||
| 	 | ||||
| 	categoriesTable.search(value).draw(); | ||||
| }); | ||||
|  | ||||
| $(document).on('click', '.task-category-delete-button', function (e) | ||||
| { | ||||
| 	var objectName = $(e.currentTarget).attr('data-category-name'); | ||||
| 	var objectId = $(e.currentTarget).attr('data-category-id'); | ||||
|  | ||||
| 	bootbox.confirm({ | ||||
| 		message: L('Are you sure to delete task category "#1"?', objectName), | ||||
| 		buttons: { | ||||
| 			confirm: { | ||||
| 				label: L('Yes'), | ||||
| 				className: 'btn-success' | ||||
| 			}, | ||||
| 			cancel: { | ||||
| 				label: L('No'), | ||||
| 				className: 'btn-danger' | ||||
| 			} | ||||
| 		}, | ||||
| 		callback: function(result) | ||||
| 		{ | ||||
| 			if (result === true) | ||||
| 			{ | ||||
| 				Grocy.Api.Get('delete-object/task_categories/' + objectId, | ||||
| 					function(result) | ||||
| 					{ | ||||
| 						window.location.href = U('/taskcategories'); | ||||
| 					}, | ||||
| 					function(xhr) | ||||
| 					{ | ||||
| 						console.error(xhr); | ||||
| 					} | ||||
| 				); | ||||
| 			} | ||||
| 		} | ||||
| 	}); | ||||
| }); | ||||
							
								
								
									
										56
									
								
								public/viewjs/taskcategoryform.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								public/viewjs/taskcategoryform.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,56 @@ | ||||
| $('#save-task-category-button').on('click', function(e) | ||||
| { | ||||
| 	e.preventDefault(); | ||||
|  | ||||
| 	if (Grocy.EditMode === 'create') | ||||
| 	{ | ||||
| 		Grocy.Api.Post('add-object/task_categories', $('#task-category-form').serializeJSON(), | ||||
| 			function(result) | ||||
| 			{ | ||||
| 				window.location.href = U('/taskcategories'); | ||||
| 			}, | ||||
| 			function(xhr) | ||||
| 			{ | ||||
| 				Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response) | ||||
| 			} | ||||
| 		); | ||||
| 	} | ||||
| 	else | ||||
| 	{ | ||||
| 		Grocy.Api.Post('edit-object/task_categories/' + Grocy.EditObjectId, $('#task-category-form').serializeJSON(), | ||||
| 			function(result) | ||||
| 			{ | ||||
| 				window.location.href = U('/taskcategories'); | ||||
| 			}, | ||||
| 			function(xhr) | ||||
| 			{ | ||||
| 				Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response) | ||||
| 			} | ||||
| 		); | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| $('#task-category-form input').keyup(function (event) | ||||
| { | ||||
| 	Grocy.FrontendHelpers.ValidateForm('task-category-form'); | ||||
| }); | ||||
|  | ||||
| $('#task-category-form input').keydown(function (event) | ||||
| { | ||||
| 	if (event.keyCode === 13) //Enter | ||||
| 	{ | ||||
| 		event.preventDefault(); | ||||
| 		 | ||||
| 		if (document.getElementById('task-category-form').checkValidity() === false) //There is at least one validation error | ||||
| 		{ | ||||
| 			return false; | ||||
| 		} | ||||
| 		else | ||||
| 		{ | ||||
| 			$('#save-task-category-button').click(); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| $('#name').focus(); | ||||
| Grocy.FrontendHelpers.ValidateForm('task-category-form'); | ||||
							
								
								
									
										61
									
								
								public/viewjs/taskform.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								public/viewjs/taskform.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,61 @@ | ||||
| $('#save-task-button').on('click', function(e) | ||||
| { | ||||
| 	e.preventDefault(); | ||||
|  | ||||
| 	var jsonData = $('#task-form').serializeJSON(); | ||||
| 	jsonData.assigned_to_user_id = jsonData.user_id; | ||||
| 	delete jsonData.user_id; | ||||
| 	jsonData.due_date = Grocy.Components.DateTimePicker.GetValue(); | ||||
|  | ||||
| 	if (Grocy.EditMode === 'create') | ||||
| 	{ | ||||
| 		Grocy.Api.Post('add-object/tasks', jsonData, | ||||
| 			function(result) | ||||
| 			{ | ||||
| 				window.location.href = U('/tasks'); | ||||
| 			}, | ||||
| 			function(xhr) | ||||
| 			{ | ||||
| 				Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response) | ||||
| 			} | ||||
| 		); | ||||
| 	} | ||||
| 	else | ||||
| 	{ | ||||
| 		Grocy.Api.Post('edit-object/tasks/' + Grocy.EditObjectId, jsonData, | ||||
| 			function(result) | ||||
| 			{ | ||||
| 				window.location.href = U('/tasks'); | ||||
| 			}, | ||||
| 			function(xhr) | ||||
| 			{ | ||||
| 				Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response) | ||||
| 			} | ||||
| 		); | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| $('#task-form input').keyup(function(event) | ||||
| { | ||||
| 	Grocy.FrontendHelpers.ValidateForm('task-form'); | ||||
| }); | ||||
|  | ||||
| $('#task-form input').keydown(function(event) | ||||
| { | ||||
| 	if (event.keyCode === 13) //Enter | ||||
| 	{ | ||||
| 		event.preventDefault(); | ||||
| 		 | ||||
| 		if (document.getElementById('task-form').checkValidity() === false) //There is at least one validation error | ||||
| 		{ | ||||
| 			return false; | ||||
| 		} | ||||
| 		else | ||||
| 		{ | ||||
| 			$('#save-task-button').click(); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| $('#name').focus(); | ||||
| Grocy.FrontendHelpers.ValidateForm('task-form'); | ||||
							
								
								
									
										188
									
								
								public/viewjs/tasks.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										188
									
								
								public/viewjs/tasks.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,188 @@ | ||||
| var tasksTable = $('#tasks-table').DataTable({ | ||||
| 	'paginate': false, | ||||
| 	'order': [[2, 'desc']], | ||||
| 	'columnDefs': [ | ||||
| 		{ 'orderable': false, 'targets': 0 }, | ||||
| 		{ 'visible': false, 'targets': 3 } | ||||
| 	], | ||||
| 	'language': JSON.parse(L('datatables_localization')), | ||||
| 	'scrollY': false, | ||||
| 	'colReorder': true, | ||||
| 	'stateSave': true, | ||||
| 	'stateSaveParams': function(settings, data) | ||||
| 	{ | ||||
| 		data.search.search = ""; | ||||
|  | ||||
| 		data.columns.forEach(column => | ||||
| 		{ | ||||
| 			column.search.search = ""; | ||||
| 		}); | ||||
| 	}, | ||||
| 	'rowGroup': { | ||||
| 		dataSrc: 3 | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| $("#search").on("keyup", function() | ||||
| { | ||||
| 	var value = $(this).val(); | ||||
| 	if (value === "all") | ||||
| 	{ | ||||
| 		value = ""; | ||||
| 	} | ||||
| 	 | ||||
| 	tasksTable.search(value).draw(); | ||||
| }); | ||||
|  | ||||
| $("#status-filter").on("change", function() | ||||
| { | ||||
| 	var value = $(this).val(); | ||||
| 	if (value === "all") | ||||
| 	{ | ||||
| 		value = ""; | ||||
| 	} | ||||
|  | ||||
| 	// Transfer CSS classes of selected element to dropdown element (for background) | ||||
| 	$(this).attr("class", $("#" + $(this).attr("id") + " option[value='" + value + "']").attr("class") + " form-control"); | ||||
| 	 | ||||
| 	tasksTable.column(5).search(value).draw(); | ||||
| }); | ||||
|  | ||||
| $(".status-filter-button").on("click", function() | ||||
| { | ||||
| 	var value = $(this).data("status-filter"); | ||||
| 	$("#status-filter").val(value); | ||||
| 	$("#status-filter").trigger("change"); | ||||
| }); | ||||
|  | ||||
| $(document).on('click', '.do-task-button', function(e) | ||||
| { | ||||
| 	e.preventDefault(); | ||||
|  | ||||
| 	// Remove the focus from the current button | ||||
| 	// to prevent that the tooltip stays until clicked anywhere else | ||||
| 	document.activeElement.blur(); | ||||
| 	 | ||||
| 	var taskId = $(e.currentTarget).attr('data-task-id'); | ||||
| 	var taskName = $(e.currentTarget).attr('data-task-name'); | ||||
| 	var doneTime = moment().format('YYYY-MM-DD HH:mm:ss'); | ||||
|  | ||||
| 	Grocy.Api.Get('tasks/mark-task-as-completed/' + taskId + '?done_time=' + doneTime, | ||||
| 		function() | ||||
| 		{ | ||||
| 			if (!$("#show-done-tasks").is(":checked")) | ||||
| 			{ | ||||
| 				$('#task-' + taskId + '-row').fadeOut(500, function () | ||||
| 				{ | ||||
| 					$(this).remove(); | ||||
| 				}); | ||||
| 			} | ||||
| 			else | ||||
| 			{ | ||||
| 				$('#task-' + taskId + '-row').addClass("text-muted"); | ||||
| 				$('#task-' + taskId + '-name').addClass("text-strike-through"); | ||||
| 				$('.do-task-button[data-task-id="' + taskId + '"]').addClass("disabled"); | ||||
| 			} | ||||
|  | ||||
| 			toastr.success(L('Marked task #1 as completed on #2', taskName, doneTime)); | ||||
| 			RefreshContextualTimeago(); | ||||
| 			RefreshStatistics(); | ||||
| 		}, | ||||
| 		function(xhr) | ||||
| 		{ | ||||
| 			console.error(xhr); | ||||
| 		} | ||||
| 	); | ||||
| }); | ||||
|  | ||||
| $(document).on('click', '.delete-task-button', function (e) | ||||
| { | ||||
| 	e.preventDefault(); | ||||
|  | ||||
| 	var objectName = $(e.currentTarget).attr('data-task-name'); | ||||
| 	var objectId = $(e.currentTarget).attr('data-task-id'); | ||||
|  | ||||
| 	bootbox.confirm({ | ||||
| 		message: L('Are you sure to delete task "#1"?', objectName), | ||||
| 		buttons: { | ||||
| 			confirm: { | ||||
| 				label: L('Yes'), | ||||
| 				className: 'btn-success' | ||||
| 			}, | ||||
| 			cancel: { | ||||
| 				label: L('No'), | ||||
| 				className: 'btn-danger' | ||||
| 			} | ||||
| 		}, | ||||
| 		callback: function(result) | ||||
| 		{ | ||||
| 			if (result === true) | ||||
| 			{ | ||||
| 				Grocy.Api.Get('delete-object/tasks/' + objectId, | ||||
| 					function(result) | ||||
| 					{ | ||||
| 						$('#task-' + objectId + '-row').fadeOut(500, function () | ||||
| 						{ | ||||
| 							$(this).remove(); | ||||
| 						}); | ||||
| 					}, | ||||
| 					function(xhr) | ||||
| 					{ | ||||
| 						console.error(xhr); | ||||
| 					} | ||||
| 				); | ||||
| 			} | ||||
| 		} | ||||
| 	}); | ||||
| }); | ||||
|  | ||||
| $("#show-done-tasks").change(function() | ||||
| { | ||||
| 	if (this.checked) | ||||
| 	{ | ||||
| 		window.location.href = U('/tasks?include_done'); | ||||
| 	} | ||||
| 	else | ||||
| 	{ | ||||
| 		window.location.href = U('/tasks'); | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| if (GetUriParam('include_done')) | ||||
| { | ||||
| 	$("#show-done-tasks").prop('checked', true); | ||||
| } | ||||
|  | ||||
| function RefreshStatistics() | ||||
| { | ||||
| 	var nextXDays = $("#info-due-tasks").data("next-x-days"); | ||||
| 	Grocy.Api.Get('tasks/get-current', | ||||
| 		function(result) | ||||
| 		{ | ||||
| 			var dueCount = 0; | ||||
| 			var overdueCount = 0; | ||||
| 			var now = moment(); | ||||
| 			var nextXDaysThreshold = moment().add(nextXDays, "days"); | ||||
| 			result.forEach(element => { | ||||
| 				var date = moment(element.due_date); | ||||
| 				if (date.isBefore(now)) | ||||
| 				{ | ||||
| 					overdueCount++; | ||||
| 				} | ||||
| 				else if (date.isBefore(nextXDaysThreshold)) | ||||
| 				{ | ||||
| 					dueCount++; | ||||
| 				} | ||||
| 			}); | ||||
| 			 | ||||
| 			$("#info-due-tasks").text(Pluralize(dueCount, L('#1 task is due to be done within the next #2 days', dueCount, nextXDays), L('#1 tasks are due to be done within the next #2 days', dueCount, nextXDays))); | ||||
| 			$("#info-overdue-tasks").text(Pluralize(overdueCount, L('#1 task is overdue to be done', overdueCount), L('#1 tasks are overdue to be done', overdueCount))); | ||||
| 		}, | ||||
| 		function(xhr) | ||||
| 		{ | ||||
| 			console.error(xhr); | ||||
| 		} | ||||
| 	); | ||||
| } | ||||
|  | ||||
| RefreshStatistics(); | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user