Compare commits
	
		
			38 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | a9a1358b08 | ||
|  | 4853174d03 | ||
|  | 538d789366 | ||
|  | 0751919b82 | ||
|  | 99b2a84667 | ||
|  | 9bd6aac09c | ||
|  | 7be35a90c1 | ||
|  | eae5b8bad9 | ||
|  | 0c85342404 | ||
|  | 9ddcdb3ab2 | ||
|  | 1c537cf5da | ||
|  | 607a90cccc | ||
|  | 3d1c6fc5f0 | ||
|  | df1d3677e8 | ||
|  | 4da2ac9b35 | ||
|  | b4ae7d8538 | ||
|  | 870b679e0e | ||
|  | 4656a85732 | ||
|  | 4949913ccb | ||
|  | 580bd5ac0c | ||
|  | 2bf3448d18 | ||
|  | e9bc51ca3d | ||
|  | 5ddae116e0 | ||
|  | 13566bc6fd | ||
|  | 642f95a3f8 | ||
|  | 5a1d21ef31 | ||
|  | 7dcd39f82f | ||
|  | 655aa89bd6 | ||
|  | feb88ab685 | ||
|  | 79b4bad014 | ||
|  | bcd5092427 | ||
|  | 554a83fa01 | ||
|  | 57acb62520 | ||
|  | 52ed5f2285 | ||
|  | dd1d253ea5 | ||
|  | e40979a874 | ||
|  | 2ddbc2656b | ||
|  | 7351fce395 | 
							
								
								
									
										201
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,202 +1,3 @@ | ||||
| ## Ignore Visual Studio temporary files, build results, and | ||||
| ## files generated by popular Visual Studio add-ons. | ||||
|  | ||||
| # User-specific files | ||||
| *.suo | ||||
| *.user | ||||
| *.userosscache | ||||
| *.sln.docstates | ||||
|  | ||||
| # User-specific files (MonoDevelop/Xamarin Studio) | ||||
| *.userprefs | ||||
|  | ||||
| # Build results | ||||
| [Dd]ebug/ | ||||
| [Dd]ebugPublic/ | ||||
| [Rr]elease/ | ||||
| [Rr]eleases/ | ||||
| x64/ | ||||
| x86/ | ||||
| build/ | ||||
| bld/ | ||||
| [Bb]in/ | ||||
| [Oo]bj/ | ||||
|  | ||||
| # Visual Studo 2015 cache/options directory | ||||
| .vs/ | ||||
|  | ||||
| # MSTest test Results | ||||
| [Tt]est[Rr]esult*/ | ||||
| [Bb]uild[Ll]og.* | ||||
|  | ||||
| # NUNIT | ||||
| *.VisualState.xml | ||||
| TestResult.xml | ||||
|  | ||||
| # Build Results of an ATL Project | ||||
| [Dd]ebugPS/ | ||||
| [Rr]eleasePS/ | ||||
| dlldata.c | ||||
|  | ||||
| *_i.c | ||||
| *_p.c | ||||
| *_i.h | ||||
| *.ilk | ||||
| *.meta | ||||
| *.obj | ||||
| *.pch | ||||
| *.pdb | ||||
| *.pgc | ||||
| *.pgd | ||||
| *.rsp | ||||
| *.sbr | ||||
| *.tlb | ||||
| *.tli | ||||
| *.tlh | ||||
| *.tmp | ||||
| *.tmp_proj | ||||
| *.log | ||||
| *.vspscc | ||||
| *.vssscc | ||||
| .builds | ||||
| *.pidb | ||||
| *.svclog | ||||
| *.scc | ||||
|  | ||||
| # Chutzpah Test files | ||||
| _Chutzpah* | ||||
|  | ||||
| # Visual C++ cache files | ||||
| ipch/ | ||||
| *.aps | ||||
| *.ncb | ||||
| *.opensdf | ||||
| *.sdf | ||||
| *.cachefile | ||||
|  | ||||
| # Visual Studio profiler | ||||
| *.psess | ||||
| *.vsp | ||||
| *.vspx | ||||
|  | ||||
| # TFS 2012 Local Workspace | ||||
| $tf/ | ||||
|  | ||||
| # Guidance Automation Toolkit | ||||
| *.gpState | ||||
|  | ||||
| # ReSharper is a .NET coding add-in | ||||
| _ReSharper*/ | ||||
| *.[Rr]e[Ss]harper | ||||
| *.DotSettings.user | ||||
|  | ||||
| # JustCode is a .NET coding addin-in | ||||
| .JustCode | ||||
|  | ||||
| # TeamCity is a build add-in | ||||
| _TeamCity* | ||||
|  | ||||
| # DotCover is a Code Coverage Tool | ||||
| *.dotCover | ||||
|  | ||||
| # NCrunch | ||||
| _NCrunch_* | ||||
| .*crunch*.local.xml | ||||
|  | ||||
| # MightyMoose | ||||
| *.mm.* | ||||
| AutoTest.Net/ | ||||
|  | ||||
| # Web workbench (sass) | ||||
| .sass-cache/ | ||||
|  | ||||
| # Installshield output folder | ||||
| [Ee]xpress/ | ||||
|  | ||||
| # DocProject is a documentation generator add-in | ||||
| DocProject/buildhelp/ | ||||
| DocProject/Help/*.HxT | ||||
| DocProject/Help/*.HxC | ||||
| DocProject/Help/*.hhc | ||||
| DocProject/Help/*.hhk | ||||
| DocProject/Help/*.hhp | ||||
| DocProject/Help/Html2 | ||||
| DocProject/Help/html | ||||
|  | ||||
| # Click-Once directory | ||||
| publish/ | ||||
|  | ||||
| # Publish Web Output | ||||
| *.[Pp]ublish.xml | ||||
| *.azurePubxml | ||||
| # TODO: Comment the next line if you want to checkin your web deploy settings  | ||||
| # but database connection strings (with potential passwords) will be unencrypted | ||||
| *.pubxml | ||||
| *.publishproj | ||||
|  | ||||
| # NuGet Packages | ||||
| *.nupkg | ||||
| # The packages folder can be ignored because of Package Restore | ||||
| **/packages/* | ||||
| # except build/, which is used as an MSBuild target. | ||||
| !**/packages/build/ | ||||
| # Uncomment if necessary however generally it will be regenerated when needed | ||||
| #!**/packages/repositories.config | ||||
|  | ||||
| # Windows Azure Build Output | ||||
| csx/ | ||||
| *.build.csdef | ||||
|  | ||||
| # Windows Store app package directory | ||||
| AppPackages/ | ||||
|  | ||||
| # Others | ||||
| *.[Cc]ache | ||||
| ClientBin/ | ||||
| [Ss]tyle[Cc]op.* | ||||
| ~$* | ||||
| *~ | ||||
| *.dbmdl | ||||
| *.dbproj.schemaview | ||||
| *.pfx | ||||
| *.publishsettings | ||||
| node_modules/ | ||||
| bower_components/ | ||||
|  | ||||
| # RIA/Silverlight projects | ||||
| Generated_Code/ | ||||
|  | ||||
| # Backup & report files from converting an old project file | ||||
| # to a newer Visual Studio version. Backup files are not needed, | ||||
| # because we have git ;-) | ||||
| _UpgradeReport_Files/ | ||||
| Backup*/ | ||||
| UpgradeLog*.XML | ||||
| UpgradeLog*.htm | ||||
|  | ||||
| # SQL Server files | ||||
| *.mdf | ||||
| *.ldf | ||||
|  | ||||
| # Business Intelligence projects | ||||
| *.rdl.data | ||||
| *.bim.layout | ||||
| *.bim_*.settings | ||||
|  | ||||
| # Microsoft Fakes | ||||
| FakesAssemblies/ | ||||
|  | ||||
| # Node.js Tools for Visual Studio | ||||
| .ntvs_analysis.dat | ||||
|  | ||||
| # Visual Studio 6 build log | ||||
| *.plg | ||||
|  | ||||
| # Visual Studio 6 workspace options file | ||||
| *.opt | ||||
|  | ||||
| /bower_components | ||||
| /public/bower_components | ||||
| /vendor | ||||
| /.release | ||||
| /composer.phar | ||||
| /composer.lock | ||||
|   | ||||
							
								
								
									
										3
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,3 @@ | ||||
| { | ||||
| 	"phpserver.relativePath": "public" | ||||
| } | ||||
							
								
								
									
										148
									
								
								Grocy.php
									
									
									
									
									
								
							
							
						
						| @@ -1,148 +0,0 @@ | ||||
| <?php | ||||
|  | ||||
| class Grocy | ||||
| { | ||||
| 	private static $DbConnectionRaw; | ||||
| 	/** | ||||
| 	 * @return PDO | ||||
| 	 */ | ||||
| 	public static function GetDbConnectionRaw($doMigrations = false) | ||||
| 	{ | ||||
| 		if ($doMigrations === true) | ||||
| 		{ | ||||
| 			self::$DbConnectionRaw = null; | ||||
| 		} | ||||
|  | ||||
| 		if (self::$DbConnectionRaw == null) | ||||
| 		{ | ||||
| 			$pdo = new PDO('sqlite:' . __DIR__ . '/data/grocy.db'); | ||||
| 			$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); | ||||
|  | ||||
| 			if ($doMigrations === true) | ||||
| 			{ | ||||
| 				Grocy::ExecuteDbStatement($pdo, "CREATE TABLE IF NOT EXISTS migrations (migration INTEGER NOT NULL PRIMARY KEY UNIQUE, execution_time_timestamp DATETIME DEFAULT (datetime('now', 'localtime')))"); | ||||
| 				GrocyDbMigrator::MigrateDb($pdo); | ||||
|  | ||||
| 				if (self::IsDemoInstallation()) | ||||
| 				{ | ||||
| 					GrocyDemoDataGenerator::PopulateDemoData($pdo); | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			self::$DbConnectionRaw = $pdo; | ||||
| 		} | ||||
|  | ||||
| 		return self::$DbConnectionRaw; | ||||
| 	} | ||||
|  | ||||
| 	private static $DbConnection; | ||||
| 	/** | ||||
| 	 * @return LessQL\Database | ||||
| 	 */ | ||||
| 	public static function GetDbConnection($doMigrations = false) | ||||
| 	{ | ||||
| 		if ($doMigrations === true) | ||||
| 		{ | ||||
| 			self::$DbConnection = null; | ||||
| 		} | ||||
|  | ||||
| 		if (self::$DbConnection == null) | ||||
| 		{ | ||||
| 			self::$DbConnection = new LessQL\Database(self::GetDbConnectionRaw($doMigrations)); | ||||
| 		} | ||||
|  | ||||
| 		return self::$DbConnection; | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * @return boolean | ||||
| 	 */ | ||||
| 	public static function ExecuteDbStatement(PDO $pdo, string $sql) | ||||
| 	{ | ||||
| 		if ($pdo->exec($sql) === false) | ||||
| 		{ | ||||
| 			throw new Exception($pdo->errorInfo()); | ||||
| 		} | ||||
|  | ||||
| 		return true; | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * @return boolean|PDOStatement | ||||
| 	 */ | ||||
| 	public static function ExecuteDbQuery(PDO $pdo, string $sql) | ||||
| 	{ | ||||
| 		if (self::ExecuteDbStatement($pdo, $sql) === true) | ||||
| 		{ | ||||
| 			return $pdo->query($sql); | ||||
| 		} | ||||
|  | ||||
| 		return false; | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * @return boolean | ||||
| 	 */ | ||||
| 	public static function IsDemoInstallation() | ||||
| 	{ | ||||
| 		return file_exists(__DIR__ . '/data/demo.txt'); | ||||
| 	} | ||||
|  | ||||
| 	private static $InstalledVersion; | ||||
| 	/** | ||||
| 	 * @return string | ||||
| 	 */ | ||||
| 	public static function GetInstalledVersion() | ||||
| 	{ | ||||
| 		if (self::$InstalledVersion == null) | ||||
| 		{ | ||||
| 			self::$InstalledVersion = preg_replace("/\r|\n/", '', file_get_contents(__DIR__ . '/version.txt')); | ||||
| 		} | ||||
|  | ||||
| 		return self::$InstalledVersion; | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * @return boolean | ||||
| 	 */ | ||||
| 	public static function IsValidSession($sessionKey) | ||||
| 	{ | ||||
| 		if ($sessionKey === null || empty($sessionKey)) | ||||
| 		{ | ||||
| 			return false; | ||||
| 		} | ||||
| 		else | ||||
| 		{ | ||||
| 			return file_exists(__DIR__ . "/data/sessions/$sessionKey.txt"); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * @return string | ||||
| 	 */ | ||||
| 	public static function CreateSession() | ||||
| 	{ | ||||
| 		if (!file_exists(__DIR__ . '/data/sessions')) | ||||
| 		{ | ||||
| 			mkdir(__DIR__ . '/data/sessions'); | ||||
| 		} | ||||
|  | ||||
| 		$now = time(); | ||||
| 		foreach (new FilesystemIterator(__DIR__ . '/data/sessions') as $file) | ||||
| 		{ | ||||
| 			if ($now - $file->getCTime() >= 2678400) //31 days | ||||
| 			{ | ||||
| 				unlink(__DIR__ . '/data/sessions/' . $file->getFilename()); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		$newSessionKey = uniqid() . uniqid() . uniqid(); | ||||
| 		file_put_contents(__DIR__ . "/data/sessions/$newSessionKey.txt", ''); | ||||
| 		return $newSessionKey; | ||||
| 	} | ||||
|  | ||||
| 	public static function RemoveSession($sessionKey) | ||||
| 	{ | ||||
| 		unlink(__DIR__ . "/data/sessions/$sessionKey.txt"); | ||||
| 	} | ||||
| } | ||||
| @@ -1,174 +0,0 @@ | ||||
| <?php | ||||
|  | ||||
| class GrocyDbMigrator | ||||
| { | ||||
| 	public static function MigrateDb(PDO $pdo) | ||||
| 	{ | ||||
| 		self::ExecuteMigrationWhenNeeded($pdo, 1, " | ||||
| 			CREATE TABLE products ( | ||||
| 				id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, | ||||
| 				name TEXT NOT NULL UNIQUE, | ||||
| 				description TEXT, | ||||
| 				location_id INTEGER NOT NULL, | ||||
| 				qu_id_purchase INTEGER NOT NULL, | ||||
| 				qu_id_stock INTEGER NOT NULL, | ||||
| 				qu_factor_purchase_to_stock REAL NOT NULL, | ||||
| 				barcode TEXT, | ||||
| 				min_stock_amount INTEGER NOT NULL DEFAULT 0, | ||||
| 				default_best_before_days INTEGER NOT NULL DEFAULT 0, | ||||
| 				row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime')) | ||||
| 			)" | ||||
| 		); | ||||
|  | ||||
| 		self::ExecuteMigrationWhenNeeded($pdo, 2, " | ||||
| 			CREATE TABLE locations ( | ||||
| 				id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, | ||||
| 				name TEXT NOT NULL UNIQUE, | ||||
| 				description TEXT, | ||||
| 				row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime')) | ||||
| 			)" | ||||
| 		); | ||||
|  | ||||
| 		self::ExecuteMigrationWhenNeeded($pdo, 3, " | ||||
| 			CREATE TABLE quantity_units ( | ||||
| 				id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, | ||||
| 				name TEXT NOT NULL UNIQUE, | ||||
| 				description TEXT, | ||||
| 				row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime')) | ||||
| 			)" | ||||
| 		); | ||||
|  | ||||
| 		self::ExecuteMigrationWhenNeeded($pdo, 4, " | ||||
| 			CREATE TABLE stock ( | ||||
| 				id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, | ||||
| 				product_id INTEGER NOT NULL, | ||||
| 				amount INTEGER NOT NULL, | ||||
| 				best_before_date DATE, | ||||
| 				purchased_date DATE DEFAULT (datetime('now', 'localtime')), | ||||
| 				stock_id TEXT NOT NULL, | ||||
| 				row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime')) | ||||
| 			)" | ||||
| 		); | ||||
|  | ||||
| 		self::ExecuteMigrationWhenNeeded($pdo, 5, " | ||||
| 			CREATE TABLE stock_log ( | ||||
| 				id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, | ||||
| 				product_id INTEGER NOT NULL, | ||||
| 				amount INTEGER NOT NULL, | ||||
| 				best_before_date DATE, | ||||
| 				purchased_date DATE, | ||||
| 				used_date DATE, | ||||
| 				spoiled INTEGER NOT NULL DEFAULT 0, | ||||
| 				stock_id TEXT NOT NULL, | ||||
| 				transaction_type TEXT NOT NULL, | ||||
| 				row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime')) | ||||
| 			)" | ||||
| 		); | ||||
|  | ||||
| 		self::ExecuteMigrationWhenNeeded($pdo, 6, " | ||||
| 			INSERT INTO locations (name, description) VALUES ('DefaultLocation', 'This is the first default location, edit or delete it'); | ||||
| 			INSERT INTO quantity_units (name, description) VALUES ('DefaultQuantityUnit', 'This is the first default quantity unit, edit or delete it'); | ||||
| 			INSERT INTO products (name, description, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('DefaultProduct1', 'This is the first default product, edit or delete it', 1, 1, 1, 1); | ||||
| 			INSERT INTO products (name, description, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('DefaultProduct2', 'This is the second default product, edit or delete it', 1, 1, 1, 1);" | ||||
| 		); | ||||
|  | ||||
| 		self::ExecuteMigrationWhenNeeded($pdo, 7, " | ||||
| 			CREATE VIEW stock_missing_products | ||||
| 			AS | ||||
| 			SELECT p.id, MAX(p.name) AS name, p.min_stock_amount - IFNULL(SUM(s.amount), 0) AS amount_missing | ||||
| 			FROM products p | ||||
| 			LEFT JOIN stock s | ||||
| 				ON p.id = s.product_id | ||||
| 			WHERE p.min_stock_amount != 0 | ||||
| 			GROUP BY p.id | ||||
| 			HAVING IFNULL(SUM(s.amount), 0) < p.min_stock_amount;" | ||||
| 		); | ||||
|  | ||||
| 		self::ExecuteMigrationWhenNeeded($pdo, 8, " | ||||
| 			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 | ||||
| 			ORDER BY MIN(best_before_date) ASC;" | ||||
| 		); | ||||
|  | ||||
| 		self::ExecuteMigrationWhenNeeded($pdo, 9, " | ||||
| 			CREATE TABLE shopping_list ( | ||||
| 				id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, | ||||
| 				product_id INTEGER NOT NULL UNIQUE, | ||||
| 				amount INTEGER NOT NULL DEFAULT 0, | ||||
| 				amount_autoadded INTEGER NOT NULL DEFAULT 0, | ||||
| 				row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime')) | ||||
| 			)" | ||||
| 		); | ||||
|  | ||||
| 		self::ExecuteMigrationWhenNeeded($pdo, 10, " | ||||
| 			CREATE TABLE habits ( | ||||
| 				id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, | ||||
| 				name TEXT NOT NULL UNIQUE, | ||||
| 				description TEXT, | ||||
| 				period_type TEXT NOT NULL, | ||||
| 				period_days INTEGER, | ||||
| 				row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime')) | ||||
| 			)" | ||||
| 		); | ||||
|  | ||||
| 		self::ExecuteMigrationWhenNeeded($pdo, 11, " | ||||
| 			CREATE TABLE habits_log ( | ||||
| 				id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, | ||||
| 				habit_id INTEGER NOT NULL, | ||||
| 				tracked_time DATETIME, | ||||
| 				row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime')) | ||||
| 			)" | ||||
| 		); | ||||
|  | ||||
| 		self::ExecuteMigrationWhenNeeded($pdo, 12, " | ||||
| 			CREATE VIEW habits_current | ||||
| 			AS | ||||
| 			SELECT habit_id, MAX(tracked_time) AS last_tracked_time | ||||
| 			FROM habits_log | ||||
| 			GROUP BY habit_id | ||||
| 			ORDER BY MAX(tracked_time) DESC;" | ||||
| 		); | ||||
|  | ||||
| 		self::ExecuteMigrationWhenNeeded($pdo, 13, " | ||||
| 			CREATE TABLE batteries ( | ||||
| 				id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, | ||||
| 				name TEXT NOT NULL UNIQUE, | ||||
| 				description TEXT, | ||||
| 				used_in TEXT, | ||||
| 				charge_interval_days INTEGER NOT NULL DEFAULT 0, | ||||
| 				row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime')) | ||||
| 			)" | ||||
| 		); | ||||
|  | ||||
| 		self::ExecuteMigrationWhenNeeded($pdo, 14, " | ||||
| 			CREATE TABLE battery_charge_cycles ( | ||||
| 				id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, | ||||
| 				battery_id TEXT NOT NULL, | ||||
| 				tracked_time DATETIME, | ||||
| 				row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime')) | ||||
| 			)" | ||||
| 		); | ||||
|  | ||||
| 		self::ExecuteMigrationWhenNeeded($pdo, 15, " | ||||
| 			CREATE VIEW batteries_current | ||||
| 			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;" | ||||
| 		); | ||||
| 	} | ||||
|  | ||||
| 	private static function ExecuteMigrationWhenNeeded(PDO $pdo, int $migrationId, string $sql) | ||||
| 	{ | ||||
| 		$rowCount = Grocy::ExecuteDbQuery($pdo, 'SELECT COUNT(*) FROM migrations WHERE migration = ' . $migrationId)->fetchColumn(); | ||||
| 		if (intval($rowCount) === 0) | ||||
| 		{ | ||||
| 			Grocy::ExecuteDbStatement($pdo, $sql); | ||||
| 			Grocy::ExecuteDbStatement($pdo, 'INSERT INTO migrations (migration) VALUES (' . $migrationId . ')'); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -1,90 +0,0 @@ | ||||
| <?php | ||||
|  | ||||
| class GrocyDemoDataGenerator | ||||
| { | ||||
| 	public static function PopulateDemoData(PDO $pdo) | ||||
| 	{ | ||||
| 		$rowCount = Grocy::ExecuteDbQuery($pdo, 'SELECT COUNT(*) FROM migrations WHERE migration = -1')->fetchColumn(); | ||||
| 		if (intval($rowCount) === 0) | ||||
| 		{ | ||||
| 			$sql = " | ||||
| 				UPDATE locations SET name = 'Vorratskammer', description = '' WHERE id = 1; | ||||
| 				INSERT INTO locations (name) VALUES ('Süßigkeitenschrank'); --2 | ||||
| 				INSERT INTO locations (name) VALUES ('Konservenschrank'); --3 | ||||
| 				INSERT INTO locations (name) VALUES ('Kühlschrank'); --4 | ||||
|  | ||||
| 				UPDATE quantity_units SET name = 'Stück' WHERE id = 1; | ||||
| 				INSERT INTO quantity_units (name) VALUES ('Packung'); --2 | ||||
| 				INSERT INTO quantity_units (name) VALUES ('Glas'); --3 | ||||
| 				INSERT INTO quantity_units (name) VALUES ('Dose'); --4 | ||||
| 				INSERT INTO quantity_units (name) VALUES ('Becher'); --5 | ||||
| 				INSERT INTO quantity_units (name) VALUES ('Bund'); --6 | ||||
|  | ||||
| 				DELETE FROM products WHERE id IN (1, 2); | ||||
| 				INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, min_stock_amount) VALUES ('Gummibärchen', 2, 2, 2, 1, 8); --3 | ||||
| 				INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, min_stock_amount) VALUES ('Chips', 2, 2, 2, 1, 10); --4 | ||||
| 				INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('Eier', 1, 2, 1, 10); --5 | ||||
| 				INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('Nudeln', 1, 2, 2, 1); --6 | ||||
| 				INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('Essiggurken', 3, 3, 3, 1); --7 | ||||
| 				INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('Gulaschsuppe', 3, 4, 4, 1); --8 | ||||
| 				INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('Joghurt', 4, 5, 5, 1); --9 | ||||
| 				INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('Käse', 4, 2, 2, 1); --10 | ||||
| 				INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('Aufschnitt', 4, 2, 2, 1); --11 | ||||
| 				INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('Paprika', 4, 1, 1, 1); --12 | ||||
| 				INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('Gurke', 4, 1, 1, 1); --13 | ||||
| 				INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('Radieschen', 4, 6, 6, 1); --14 | ||||
| 				INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('Tomate', 4, 1, 1, 1); --15 | ||||
|  | ||||
| 				INSERT INTO habits (name, period_type, period_days) VALUES ('Changed towels in the bathroom', 'manually', 5); --1 | ||||
| 				INSERT INTO habits (name, period_type, period_days) VALUES ('Cleaned the kitchen floor', 'dynamic-regular', 7); --2 | ||||
|  | ||||
| 				INSERT INTO batteries (name, description, used_in) VALUES ('Battery1', 'Warranty ends 2022', 'TV remote control'); --1 | ||||
| 				INSERT INTO batteries (name, description, used_in) VALUES ('Battery2', 'Warranty ends 2022', 'Alarm clock'); --2 | ||||
| 				INSERT INTO batteries (name, description, used_in, charge_interval_days) VALUES ('Battery3', 'Warranty ends 2022', 'Heat remote control', 60); --3 | ||||
|  | ||||
| 				INSERT INTO migrations (migration) VALUES (-1); | ||||
| 			"; | ||||
|  | ||||
| 			Grocy::ExecuteDbStatement($pdo, $sql); | ||||
|  | ||||
| 			GrocyLogicStock::AddProduct(3, 5, date('Y-m-d', strtotime('+180 days')), GrocyLogicStock::TRANSACTION_TYPE_PURCHASE); | ||||
| 			GrocyLogicStock::AddProduct(4, 5, date('Y-m-d', strtotime('+180 days')), GrocyLogicStock::TRANSACTION_TYPE_PURCHASE); | ||||
| 			GrocyLogicStock::AddProduct(5, 5, date('Y-m-d', strtotime('+20 days')), GrocyLogicStock::TRANSACTION_TYPE_PURCHASE); | ||||
| 			GrocyLogicStock::AddProduct(6, 5, date('Y-m-d', strtotime('+600 days')), GrocyLogicStock::TRANSACTION_TYPE_PURCHASE); | ||||
| 			GrocyLogicStock::AddProduct(7, 5, date('Y-m-d', strtotime('+800 days')), GrocyLogicStock::TRANSACTION_TYPE_PURCHASE); | ||||
| 			GrocyLogicStock::AddProduct(8, 5, date('Y-m-d', strtotime('+900 days')), GrocyLogicStock::TRANSACTION_TYPE_PURCHASE); | ||||
| 			GrocyLogicStock::AddProduct(9, 5, date('Y-m-d', strtotime('+14 days')), GrocyLogicStock::TRANSACTION_TYPE_PURCHASE); | ||||
| 			GrocyLogicStock::AddProduct(10, 5, date('Y-m-d', strtotime('+21 days')), GrocyLogicStock::TRANSACTION_TYPE_PURCHASE); | ||||
| 			GrocyLogicStock::AddProduct(11, 5, date('Y-m-d', strtotime('+10 days')), GrocyLogicStock::TRANSACTION_TYPE_PURCHASE); | ||||
| 			GrocyLogicStock::AddProduct(12, 5, date('Y-m-d', strtotime('+2 days')), GrocyLogicStock::TRANSACTION_TYPE_PURCHASE); | ||||
| 			GrocyLogicStock::AddProduct(13, 5, date('Y-m-d', strtotime('-2 days')), GrocyLogicStock::TRANSACTION_TYPE_PURCHASE); | ||||
| 			GrocyLogicStock::AddProduct(14, 5, date('Y-m-d', strtotime('+2 days')), GrocyLogicStock::TRANSACTION_TYPE_PURCHASE); | ||||
| 			GrocyLogicStock::AddProduct(15, 5, date('Y-m-d', strtotime('-2 days')), GrocyLogicStock::TRANSACTION_TYPE_PURCHASE); | ||||
| 			GrocyLogicStock::AddMissingProductsToShoppingList(); | ||||
|  | ||||
| 			GrocyLogicHabits::TrackHabit(1, date('Y-m-d H:i:s', strtotime('-5 days'))); | ||||
| 			GrocyLogicHabits::TrackHabit(1, date('Y-m-d H:i:s', strtotime('-10 days'))); | ||||
| 			GrocyLogicHabits::TrackHabit(1, date('Y-m-d H:i:s', strtotime('-15 days'))); | ||||
| 			GrocyLogicHabits::TrackHabit(2, date('Y-m-d H:i:s', strtotime('-10 days'))); | ||||
| 			GrocyLogicHabits::TrackHabit(2, date('Y-m-d H:i:s', strtotime('-20 days'))); | ||||
|  | ||||
| 			GrocyLogicBatteries::TrackChargeCycle(1, date('Y-m-d H:i:s', strtotime('-200 days'))); | ||||
| 			GrocyLogicBatteries::TrackChargeCycle(1, date('Y-m-d H:i:s', strtotime('-150 days'))); | ||||
| 			GrocyLogicBatteries::TrackChargeCycle(1, date('Y-m-d H:i:s', strtotime('-100 days'))); | ||||
| 			GrocyLogicBatteries::TrackChargeCycle(1, date('Y-m-d H:i:s', strtotime('-50 days'))); | ||||
| 			GrocyLogicBatteries::TrackChargeCycle(2, date('Y-m-d H:i:s', strtotime('-200 days'))); | ||||
| 			GrocyLogicBatteries::TrackChargeCycle(2, date('Y-m-d H:i:s', strtotime('-150 days'))); | ||||
| 			GrocyLogicBatteries::TrackChargeCycle(2, date('Y-m-d H:i:s', strtotime('-100 days'))); | ||||
| 			GrocyLogicBatteries::TrackChargeCycle(2, date('Y-m-d H:i:s', strtotime('-50 days'))); | ||||
| 			GrocyLogicBatteries::TrackChargeCycle(3, date('Y-m-d H:i:s', strtotime('-65 days'))); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	public static function RecreateDemo() | ||||
| 	{ | ||||
| 		unlink(__DIR__ . '/data/grocy.db'); | ||||
|  | ||||
| 		$db = Grocy::GetDbConnectionRaw(true); | ||||
| 		self::PopulateDemoData($db); | ||||
| 	} | ||||
| } | ||||
| @@ -1,57 +0,0 @@ | ||||
| <?php | ||||
|  | ||||
| class GrocyLogicBatteries | ||||
| { | ||||
| 	public static function GetCurrent() | ||||
| 	{ | ||||
| 		$sql = 'SELECT * from batteries_current'; | ||||
| 		return Grocy::ExecuteDbQuery(Grocy::GetDbConnectionRaw(), $sql)->fetchAll(PDO::FETCH_OBJ); | ||||
| 	} | ||||
|  | ||||
| 	public static function GetNextChargeTime(int $batteryId) | ||||
| 	{ | ||||
| 		$db = Grocy::GetDbConnection(); | ||||
|  | ||||
| 		$battery = $db->batteries($batteryId); | ||||
| 		$batteryLastLogRow = Grocy::ExecuteDbQuery(Grocy::GetDbConnectionRaw(), "SELECT * from batteries_current WHERE battery_id = $batteryId LIMIT 1")->fetch(PDO::FETCH_OBJ); | ||||
|  | ||||
| 		if ($battery->charge_interval_days > 0) | ||||
| 		{ | ||||
| 			return date('Y-m-d H:i:s', strtotime('+' . $battery->charge_interval_days . ' day', strtotime($batteryLastLogRow->last_tracked_time))); | ||||
| 		} | ||||
| 		else | ||||
| 		{ | ||||
| 			return date('Y-m-d H:i:s'); | ||||
| 		} | ||||
|  | ||||
| 		return null; | ||||
| 	} | ||||
|  | ||||
| 	public static function GetBatteryDetails(int $batteryId) | ||||
| 	{ | ||||
| 		$db = Grocy::GetDbConnection(); | ||||
|  | ||||
| 		$battery = $db->batteries($batteryId); | ||||
| 		$batteryChargeCylcesCount = $db->battery_charge_cycles()->where('battery_id', $batteryId)->count(); | ||||
| 		$batteryLastChargedTime = $db->battery_charge_cycles()->where('battery_id', $batteryId)->max('tracked_time'); | ||||
|  | ||||
| 		return array( | ||||
| 			'battery' => $battery, | ||||
| 			'last_charged' => $batteryLastChargedTime, | ||||
| 			'charge_cycles_count' => $batteryChargeCylcesCount | ||||
| 		); | ||||
| 	} | ||||
|  | ||||
| 	public static function TrackChargeCycle(int $batteryId, string $trackedTime) | ||||
| 	{ | ||||
| 		$db = Grocy::GetDbConnection(); | ||||
|  | ||||
| 		$logRow = $db->battery_charge_cycles()->createRow(array( | ||||
| 			'battery_id' => $batteryId, | ||||
| 			'tracked_time' => $trackedTime | ||||
| 		)); | ||||
| 		$logRow->save(); | ||||
|  | ||||
| 		return true; | ||||
| 	} | ||||
| } | ||||
| @@ -1,59 +0,0 @@ | ||||
| <?php | ||||
|  | ||||
| class GrocyLogicHabits | ||||
| { | ||||
| 	const HABIT_TYPE_MANUALLY = 'manually'; | ||||
| 	const HABIT_TYPE_DYNAMIC_REGULAR = 'dynamic-regular'; | ||||
|  | ||||
| 	public static function GetCurrentHabits() | ||||
| 	{ | ||||
| 		$sql = 'SELECT * from habits_current'; | ||||
| 		return Grocy::ExecuteDbQuery(Grocy::GetDbConnectionRaw(), $sql)->fetchAll(PDO::FETCH_OBJ); | ||||
| 	} | ||||
|  | ||||
| 	public static function GetNextHabitTime(int $habitId) | ||||
| 	{ | ||||
| 		$db = Grocy::GetDbConnection(); | ||||
|  | ||||
| 		$habit = $db->habits($habitId); | ||||
| 		$habitLastLogRow = Grocy::ExecuteDbQuery(Grocy::GetDbConnectionRaw(), "SELECT * from habits_current WHERE habit_id = $habitId LIMIT 1")->fetch(PDO::FETCH_OBJ); | ||||
|  | ||||
| 		switch ($habit->period_type) | ||||
| 		{ | ||||
| 			case self::HABIT_TYPE_MANUALLY: | ||||
| 				return date('Y-m-d H:i:s'); | ||||
| 			case self::HABIT_TYPE_DYNAMIC_REGULAR: | ||||
| 				return date('Y-m-d H:i:s', strtotime('+' . $habit->period_days . ' day', strtotime($habitLastLogRow->last_tracked_time))); | ||||
| 		} | ||||
|  | ||||
| 		return null; | ||||
| 	} | ||||
|  | ||||
| 	public static function GetHabitDetails(int $habitId) | ||||
| 	{ | ||||
| 		$db = Grocy::GetDbConnection(); | ||||
|  | ||||
| 		$habit = $db->habits($habitId); | ||||
| 		$habitTrackedCount = $db->habits_log()->where('habit_id', $habitId)->count(); | ||||
| 		$habitLastTrackedTime = $db->habits_log()->where('habit_id', $habitId)->max('tracked_time'); | ||||
|  | ||||
| 		return array( | ||||
| 			'habit' => $habit, | ||||
| 			'last_tracked' => $habitLastTrackedTime, | ||||
| 			'tracked_count' => $habitTrackedCount | ||||
| 		); | ||||
| 	} | ||||
|  | ||||
| 	public static function TrackHabit(int $habitId, string $trackedTime) | ||||
| 	{ | ||||
| 		$db = Grocy::GetDbConnection(); | ||||
|  | ||||
| 		$logRow = $db->habits_log()->createRow(array( | ||||
| 			'habit_id' => $habitId, | ||||
| 			'tracked_time' => $trackedTime | ||||
| 		)); | ||||
| 		$logRow->save(); | ||||
|  | ||||
| 		return true; | ||||
| 	} | ||||
| } | ||||
| @@ -1,191 +0,0 @@ | ||||
| <?php | ||||
|  | ||||
| class GrocyLogicStock | ||||
| { | ||||
| 	const TRANSACTION_TYPE_PURCHASE = 'purchase'; | ||||
| 	const TRANSACTION_TYPE_CONSUME = 'consume'; | ||||
| 	const TRANSACTION_TYPE_INVENTORY_CORRECTION = 'inventory-correction'; | ||||
|  | ||||
| 	public static function GetCurrentStock() | ||||
| 	{ | ||||
| 		$sql = 'SELECT * from stock_current'; | ||||
| 		return Grocy::ExecuteDbQuery(Grocy::GetDbConnectionRaw(), $sql)->fetchAll(PDO::FETCH_OBJ); | ||||
| 	} | ||||
|  | ||||
| 	public static function GetMissingProducts() | ||||
| 	{ | ||||
| 		$sql = 'SELECT * from stock_missing_products'; | ||||
| 		return Grocy::ExecuteDbQuery(Grocy::GetDbConnectionRaw(), $sql)->fetchAll(PDO::FETCH_OBJ); | ||||
| 	} | ||||
|  | ||||
| 	public static function GetProductDetails(int $productId) | ||||
| 	{ | ||||
| 		$db = Grocy::GetDbConnection(); | ||||
|  | ||||
| 		$product = $db->products($productId); | ||||
| 		$productStockAmount = $db->stock()->where('product_id', $productId)->sum('amount'); | ||||
| 		$productLastPurchased = $db->stock_log()->where('product_id', $productId)->where('transaction_type', self::TRANSACTION_TYPE_PURCHASE)->max('purchased_date'); | ||||
| 		$productLastUsed = $db->stock_log()->where('product_id', $productId)->where('transaction_type', self::TRANSACTION_TYPE_CONSUME)->max('used_date'); | ||||
| 		$quPurchase = $db->quantity_units($product->qu_id_purchase); | ||||
| 		$quStock = $db->quantity_units($product->qu_id_stock); | ||||
|  | ||||
| 		return array( | ||||
| 			'product' => $product, | ||||
| 			'last_purchased' => $productLastPurchased, | ||||
| 			'last_used' => $productLastUsed, | ||||
| 			'stock_amount' => $productStockAmount, | ||||
| 			'quantity_unit_purchase' => $quPurchase, | ||||
| 			'quantity_unit_stock' => $quStock | ||||
| 		); | ||||
| 	} | ||||
|  | ||||
| 	public static function AddProduct(int $productId, int $amount, string $bestBeforeDate, $transactionType) | ||||
| 	{ | ||||
| 		if ($transactionType === self::TRANSACTION_TYPE_CONSUME || $transactionType === self::TRANSACTION_TYPE_PURCHASE || $transactionType === self::TRANSACTION_TYPE_INVENTORY_CORRECTION) | ||||
| 		{ | ||||
| 			$db = Grocy::GetDbConnection(); | ||||
| 			$stockId = uniqid(); | ||||
|  | ||||
| 			$logRow = $db->stock_log()->createRow(array( | ||||
| 				'product_id' => $productId, | ||||
| 				'amount' => $amount, | ||||
| 				'best_before_date' => $bestBeforeDate, | ||||
| 				'purchased_date' => date('Y-m-d'), | ||||
| 				'stock_id' => $stockId, | ||||
| 				'transaction_type' => $transactionType | ||||
| 			)); | ||||
| 			$logRow->save(); | ||||
|  | ||||
| 			$stockRow = $db->stock()->createRow(array( | ||||
| 				'product_id' => $productId, | ||||
| 				'amount' => $amount, | ||||
| 				'best_before_date' => $bestBeforeDate, | ||||
| 				'purchased_date' => date('Y-m-d'), | ||||
| 				'stock_id' => $stockId, | ||||
| 			)); | ||||
| 			$stockRow->save(); | ||||
|  | ||||
| 			return true; | ||||
| 		} | ||||
| 		else | ||||
| 		{ | ||||
| 			throw new Exception("Transaction type $transactionType is not valid (GrocyLogicStock.AddProduct)"); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	public static function ConsumeProduct(int $productId, int $amount, bool $spoiled, $transactionType) | ||||
| 	{ | ||||
| 		if ($transactionType === self::TRANSACTION_TYPE_CONSUME || $transactionType === self::TRANSACTION_TYPE_PURCHASE || $transactionType === self::TRANSACTION_TYPE_INVENTORY_CORRECTION) | ||||
| 		{ | ||||
| 			$db = Grocy::GetDbConnection(); | ||||
|  | ||||
| 			$productStockAmount = $db->stock()->where('product_id', $productId)->sum('amount'); | ||||
| 			$potentialStockEntries = $db->stock()->where('product_id', $productId)->orderBy('best_before_date', 'ASC')->orderBy('purchased_date', 'ASC')->fetchAll(); //First expiring first, then first in first out | ||||
|  | ||||
| 			if ($amount > $productStockAmount) | ||||
| 			{ | ||||
| 				return false; | ||||
| 			} | ||||
|  | ||||
| 			foreach ($potentialStockEntries as $stockEntry) | ||||
| 			{ | ||||
| 				if ($amount == 0) | ||||
| 				{ | ||||
| 					break; | ||||
| 				} | ||||
|  | ||||
| 				if ($amount >= $stockEntry->amount) //Take the whole stock entry | ||||
| 				{ | ||||
| 					$logRow = $db->stock_log()->createRow(array( | ||||
| 						'product_id' => $stockEntry->product_id, | ||||
| 						'amount' => $stockEntry->amount * -1, | ||||
| 						'best_before_date' => $stockEntry->best_before_date, | ||||
| 						'purchased_date' => $stockEntry->purchased_date, | ||||
| 						'used_date' => date('Y-m-d'), | ||||
| 						'spoiled' => $spoiled, | ||||
| 						'stock_id' => $stockEntry->stock_id, | ||||
| 						'transaction_type' => $transactionType | ||||
| 					)); | ||||
| 					$logRow->save(); | ||||
|  | ||||
| 					$amount -= $stockEntry->amount; | ||||
| 					$stockEntry->delete(); | ||||
| 				} | ||||
| 				else //Stock entry amount is > than needed amount -> split the stock entry resp. update the amount | ||||
| 				{ | ||||
| 					$logRow = $db->stock_log()->createRow(array( | ||||
| 						'product_id' => $stockEntry->product_id, | ||||
| 						'amount' => $amount * -1, | ||||
| 						'best_before_date' => $stockEntry->best_before_date, | ||||
| 						'purchased_date' => $stockEntry->purchased_date, | ||||
| 						'used_date' => date('Y-m-d'), | ||||
| 						'spoiled' => $spoiled, | ||||
| 						'stock_id' => $stockEntry->stock_id, | ||||
| 						'transaction_type' => $transactionType | ||||
| 					)); | ||||
| 					$logRow->save(); | ||||
|  | ||||
| 					$restStockAmount = $stockEntry->amount - $amount; | ||||
| 					$amount = 0; | ||||
|  | ||||
| 					$stockEntry->update(array( | ||||
| 						'amount' => $restStockAmount | ||||
| 					)); | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			return true; | ||||
| 		} | ||||
| 		else | ||||
| 		{ | ||||
| 			throw new Exception("Transaction type $transactionType is not valid (GrocyLogicStock.ConsumeProduct)"); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	public static function InventoryProduct(int $productId, int $newAmount, string $bestBeforeDate) | ||||
| 	{ | ||||
| 		$db = Grocy::GetDbConnection(); | ||||
| 		$productStockAmount = $db->stock()->where('product_id', $productId)->sum('amount'); | ||||
|  | ||||
| 		if ($newAmount > $productStockAmount) | ||||
| 		{ | ||||
| 			$amountToAdd = $newAmount - $productStockAmount; | ||||
| 			self::AddProduct($productId, $amountToAdd, $bestBeforeDate, self::TRANSACTION_TYPE_INVENTORY_CORRECTION); | ||||
| 		} | ||||
| 		else if ($newAmount < $productStockAmount) | ||||
| 		{ | ||||
| 			$amountToRemove = $productStockAmount - $newAmount; | ||||
| 			self::ConsumeProduct($productId, $amountToRemove, false, self::TRANSACTION_TYPE_INVENTORY_CORRECTION); | ||||
| 		} | ||||
|  | ||||
| 		return true; | ||||
| 	} | ||||
|  | ||||
| 	public static function AddMissingProductsToShoppingList() | ||||
| 	{ | ||||
| 		$db = Grocy::GetDbConnection(); | ||||
|  | ||||
| 		$missingProducts = self::GetMissingProducts(); | ||||
| 		foreach ($missingProducts as $missingProduct) | ||||
| 		{ | ||||
| 			$product = $db->products()->where('id', $missingProduct->id)->fetch(); | ||||
| 			$amount = ceil($missingProduct->amount_missing / $product->qu_factor_purchase_to_stock); | ||||
|  | ||||
| 			$alreadyExistingEntry = $db->shopping_list()->where('product_id', $missingProduct->id)->fetch(); | ||||
| 			if ($alreadyExistingEntry) //Update | ||||
| 			{ | ||||
| 				$alreadyExistingEntry->update(array( | ||||
| 					'amount_autoadded' => $amount | ||||
| 				)); | ||||
| 			} | ||||
| 			else //Insert | ||||
| 			{ | ||||
| 				$shoppinglistRow = $db->shopping_list()->createRow(array( | ||||
| 					'product_id' => $missingProduct->id, | ||||
| 					'amount_autoadded' => $amount | ||||
| 				)); | ||||
| 				$shoppinglistRow->save(); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -1,66 +0,0 @@ | ||||
| <?php | ||||
|  | ||||
| class GrocyPhpHelper | ||||
| { | ||||
| 	public static function FindObjectInArrayByPropertyValue($array, $propertyName, $propertyValue) | ||||
| 	{ | ||||
| 		foreach($array as $object) | ||||
| 		{ | ||||
| 			if($object->{$propertyName} == $propertyValue) | ||||
| 			{ | ||||
| 				return $object; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		return null; | ||||
| 	} | ||||
|  | ||||
| 	public static function FindAllObjectsInArrayByPropertyValue($array, $propertyName, $propertyValue, $operator = '==') | ||||
| 	{ | ||||
| 		$returnArray = array(); | ||||
|  | ||||
| 		foreach($array as $object) | ||||
| 		{ | ||||
| 			switch($operator) | ||||
| 			{ | ||||
| 				case '==': | ||||
| 					if($object->{$propertyName} == $propertyValue) | ||||
| 					{ | ||||
| 						$returnArray[] = $object; | ||||
| 					} | ||||
| 					break; | ||||
| 				case '>': | ||||
| 					if($object->{$propertyName} > $propertyValue) | ||||
| 					{ | ||||
| 						$returnArray[] = $object; | ||||
| 					} | ||||
| 					break; | ||||
| 				case '<': | ||||
| 					if($object->{$propertyName} < $propertyValue) | ||||
| 					{ | ||||
| 						$returnArray[] = $object; | ||||
| 					} | ||||
| 					break; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		return $returnArray; | ||||
| 	} | ||||
|  | ||||
| 	public static function SumArrayValue($array, $propertyName) | ||||
| 	{ | ||||
| 		$sum = 0; | ||||
| 		foreach($array as $object) | ||||
| 		{ | ||||
| 			$sum += $object->{$propertyName}; | ||||
| 		} | ||||
|  | ||||
| 		return $sum; | ||||
| 	} | ||||
|  | ||||
| 	public static function GetClassConstants($className) | ||||
| 	{ | ||||
| 		$r = new ReflectionClass($className); | ||||
| 		return $r->getConstants(); | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										48
									
								
								README.md
									
									
									
									
									
								
							
							
						
						| @@ -1,22 +1,58 @@ | ||||
| # grocy | ||||
| ERP beyond your fridge | ||||
|  | ||||
| ## Give it a try | ||||
| Public demo of the latest version → [https://demo.grocy.info](https://demo.grocy.info)  | ||||
|  | ||||
| ## Motivation | ||||
| A household needs to be managed. I did this so far (almost 10 years) with my first self written software (a C# windows forms application) and with a bunch of Excel sheets. The software is a pain to use and Excel is Excel. So I searched for and tried different things for a (very) long time, nothing 100 % fitted, so this is my aim for a "complete houshold management"-thing. | ||||
|  | ||||
| ## What it is about | ||||
| For now my main focus is on stock management, ERP your fridge! | ||||
|  | ||||
| # Give it a try | ||||
| Public demo of the latest version → [https://demo.grocy.info](https://demo.grocy.info)  | ||||
|  | ||||
| ## How to install | ||||
| Just unpack the [latest release](https://github.com/berrnd/grocy/releases/latest) on your PHP enabled webserver, copy `config-dist.php` to `data/config.php`, edit it to your needs, ensure that the `data` directory is writable and you're ready to go. Alternatively clone this repository and install Composer and Bower dependencies manually. | ||||
| Just unpack the [latest release](https://github.com/berrnd/grocy/releases/latest) on your PHP (7.0 or later required) enabled webserver (webservers root should point to the `/public` directory), copy `config-dist.php` to `data/config.php`, edit it to your needs, ensure that the `data` directory is writable and you're ready to go. | ||||
|  | ||||
| Default login is user `admin` with password `admin` - see the `data/config.php` file. Alternatively clone this repository and install Composer and Bower dependencies manually. | ||||
|  | ||||
| If you use nginx as your webserver, please include `try_files $uri /index.php;` in your location block. | ||||
|  | ||||
| ## Notes about barcode readers | ||||
| Some fields also allow to select a value by scanning a barcode. It works best when your barcode reader prefixes every barcode with a letter this is normally not part of a item name (I use a `$`) and sends a `TAB` after a scan. | ||||
| ## How to update | ||||
| Just overwrite everything with the latest release while keeping the `/data` directory, check `config-dist.php` for new configuration options and add them to your `data/config.php` (it will show up as an error if something is missing there). | ||||
|  | ||||
| ## Things worth to know | ||||
|  | ||||
| ### REST API & data model documentation | ||||
| See the integrated Swagger UI instance on [/api](https://demo-en.grocy.info/api). | ||||
|  | ||||
| ### Barcode readers | ||||
| Some fields also allow to select a value by scanning a barcode. It works best when your barcode reader prefixes every barcode with a letter which is normally not part of a item name (I use a `$`) and sends a `TAB` after a scan. | ||||
|  | ||||
| ### Input shorthands for date fields | ||||
| For (productivity) reasons all date (and time) input fields use the ISO-8601 format regardless of localization. | ||||
| The following shorthands are available: | ||||
| - `MMDD` gets expanded to the given day on the current year in proper notation | ||||
|   - Example: `0517` will be converted to `2018-05-17` | ||||
| - `YYYYMMDD` gets expanded to the proper ISO-8601 notation | ||||
|   - Example: `20190417` will be converted to `2019-04-17` | ||||
| - `x` gets expanded to `2099-12-31` (which I use for products which never expire) | ||||
| - Down/up arrow keys will increase/decrease the date by one day | ||||
| - Right/left arrow keys will increase/decrease the date by 1 week | ||||
|  | ||||
| ### Keyboard shorthands for buttons | ||||
| Wherever a button contains a bold highlighted letter, this is a shortcut key. | ||||
| Example: Button "Add as new **p**roduct" can be "pressed" by using the `P` key on your keyboard. | ||||
|  | ||||
| ### Barcode lookup via external services | ||||
| Products can be directly added to the database via looking them up against external services by a barcode. | ||||
| This is currently only possible through the REST API. | ||||
| There is no plugin included for any service, see the reference implementation in `data/plugins/DemoBarcodeLookupPlugin.php`. | ||||
|  | ||||
| ### Database migrations | ||||
| Database schema migration is automatically done when visiting the root (`/`) route (click on the logo in the left upper edge). | ||||
|  | ||||
| ### Demo mode | ||||
| When the file `data/demo.txt` exists, the application will work in a demo mode which means authentication is disabled and some demo data will be generated during the database schema migration. | ||||
|  | ||||
| ## Screenshots | ||||
| #### Dashboard | ||||
|   | ||||
							
								
								
									
										44
									
								
								app.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,44 @@ | ||||
| <?php | ||||
|  | ||||
| use \Psr\Http\Message\ServerRequestInterface as Request; | ||||
| use \Psr\Http\Message\ResponseInterface as Response; | ||||
|  | ||||
| use \Grocy\Helpers\UrlManager; | ||||
| use \Grocy\Controllers\LoginController; | ||||
|  | ||||
| require_once __DIR__ . '/vendor/autoload.php'; | ||||
| require_once __DIR__ . '/data/config.php'; | ||||
|  | ||||
| // Setup base application | ||||
| $appContainer = new \Slim\Container([ | ||||
| 	'settings' => [ | ||||
| 		'displayErrorDetails' => true, | ||||
| 		'determineRouteBeforeAppMiddleware' => true | ||||
| 	], | ||||
| 	'view' => function($container) | ||||
| 	{ | ||||
| 		return new \Slim\Views\Blade(__DIR__ . '/views', __DIR__ . '/data/viewcache'); | ||||
| 	}, | ||||
| 	'LoginControllerInstance' => function($container) | ||||
| 	{ | ||||
| 		return new LoginController($container, 'grocy_session'); | ||||
| 	}, | ||||
| 	'UrlManager' => function($container) | ||||
| 	{ | ||||
| 		return new UrlManager(BASE_URL); | ||||
| 	}, | ||||
| 	'ApiKeyHeaderName' => function($container) | ||||
| 	{ | ||||
| 		return 'GROCY-API-KEY'; | ||||
| 	} | ||||
| ]); | ||||
| $app = new \Slim\App($appContainer); | ||||
|  | ||||
| if (PHP_SAPI === 'cli') | ||||
| { | ||||
| 	$app->add(\pavlakis\cli\CliRequest::class); | ||||
| } | ||||
|  | ||||
| require_once __DIR__ . '/routes.php'; | ||||
|  | ||||
| $app->run(); | ||||
| @@ -17,6 +17,8 @@ | ||||
| 		"jquery-timeago": "^1.6.1", | ||||
| 		"toastr": "^2.1.3", | ||||
| 		"tagmanager": "^3.0.2", | ||||
| 		"eonasdan-bootstrap-datetimepicker": "^4.17.47" | ||||
| 		"eonasdan-bootstrap-datetimepicker": "^4.17.47", | ||||
| 		"swagger-ui": "^3.13.4", | ||||
| 		"jquery-ui": "^1.12.1" | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -8,5 +8,6 @@ for /f "tokens=*" %%a in ('type version.txt') do set version=%%a | ||||
|  | ||||
| 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!bower.json -xr!publication_assets | ||||
| "build_tools\7za.exe" a -r "%releasePath%\grocy_%version%.zip" "%projectPath%\.htaccess" | ||||
| "build_tools\7za.exe" d "%releasePath%\grocy_%version%.zip" data\*.* data\sessions | ||||
| "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\* | ||||
|   | ||||
| @@ -1,8 +1,21 @@ | ||||
| { | ||||
| 	"require": { | ||||
| 		"php": ">=7.0", | ||||
| 		"slim/slim": "^3.8", | ||||
| 		"slim/php-view": "^2.2", | ||||
| 		"morris/lessql": "^0.3.4", | ||||
| 		"pavlakis/slim-cli": "^1.0" | ||||
| 		"pavlakis/slim-cli": "^1.0", | ||||
| 		"rubellum/slim-blade-view": "^0.1.1", | ||||
| 		"tuupola/cors-middleware": "^0.7.0" | ||||
| 	}, | ||||
| 	"autoload": { | ||||
| 		"psr-4": { | ||||
| 			"Grocy\\Services\\": "services/", | ||||
| 			"Grocy\\Controllers\\": "controllers/", | ||||
| 			"Grocy\\Middleware\\": "middleware/", | ||||
| 			"Grocy\\Helpers\\": "helpers/" | ||||
| 		}, | ||||
| 		"files": [ | ||||
| 			"helpers/extensions.php" | ||||
| 		] | ||||
| 	} | ||||
| } | ||||
|   | ||||
							
								
								
									
										1610
									
								
								composer.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						| @@ -1,4 +1,22 @@ | ||||
| <?php | ||||
|  | ||||
| # Login credentials | ||||
| define('HTTP_USER', 'admin'); | ||||
| define('HTTP_PASSWORD', 'admin'); | ||||
|  | ||||
| # Either "production" or "dev" | ||||
| define('MODE', 'production'); | ||||
|  | ||||
| # Either "en" or "de" or the filename (without extension) of | ||||
| # one of the other available localization files in the "/localization" directory | ||||
| define('CULTURE', 'en'); | ||||
|  | ||||
| # The base url of your installation, | ||||
| # should be just "/" when running directly under the root of a (sub)domain | ||||
| # or for example "https:/example.com/grocy" when using a subdirectory | ||||
| define('BASE_URL', '/'); | ||||
|  | ||||
| # The plugin to use for external barcode lookups, | ||||
| # must be the filename without .php extension and must be located in /data/plugins, | ||||
| # see /data/plugins/DemoBarcodeLookupPlugin.php for an example implementation | ||||
| define('STOCK_BARCODE_LOOKUP_PLUGIN', 'DemoBarcodeLookupPlugin'); | ||||
|   | ||||
							
								
								
									
										28
									
								
								controllers/BaseApiController.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,28 @@ | ||||
| <?php | ||||
|  | ||||
| namespace Grocy\Controllers; | ||||
|  | ||||
| class BaseApiController extends BaseController | ||||
| { | ||||
|  | ||||
| 	public function __construct(\Slim\Container $container) | ||||
| 	{ | ||||
| 		parent::__construct($container); | ||||
| 		$this->OpenApiSpec = json_decode(file_get_contents(__DIR__ . '/../grocy.openapi.json')); | ||||
| 	} | ||||
|  | ||||
| 	protected $OpenApiSpec; | ||||
|  | ||||
| 	protected function ApiResponse($data) | ||||
| 	{ | ||||
| 		return json_encode($data); | ||||
| 	} | ||||
|  | ||||
| 	protected function VoidApiActionResponse($response, $success = true, $status = 200, $errorMessage = '') | ||||
| 	{ | ||||
| 		return $response->withStatus($status)->withJson(array( | ||||
| 			'success' => $success, | ||||
| 			'error_message' => $errorMessage | ||||
| 		)); | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										34
									
								
								controllers/BaseController.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,34 @@ | ||||
| <?php | ||||
|  | ||||
| namespace Grocy\Controllers; | ||||
|  | ||||
| use \Grocy\Services\DatabaseService; | ||||
| use \Grocy\Services\ApplicationService; | ||||
| use \Grocy\Services\LocalizationService; | ||||
|  | ||||
| class BaseController | ||||
| { | ||||
| 	public function __construct(\Slim\Container $container) { | ||||
| 		$databaseService = new DatabaseService(); | ||||
| 		$this->Database = $databaseService->GetDbConnection(); | ||||
|  | ||||
| 		$applicationService = new ApplicationService(); | ||||
| 		$container->view->set('version', $applicationService->GetInstalledVersion()); | ||||
|  | ||||
| 		$localizationService = new LocalizationService(CULTURE); | ||||
| 		$container->view->set('localizationStrings', $localizationService->GetCurrentCultureLocalizations()); | ||||
| 		$container->view->set('L', function($text, ...$placeholderValues) use($localizationService) | ||||
| 		{ | ||||
| 			return $localizationService->Localize($text, ...$placeholderValues); | ||||
| 		}); | ||||
| 		$container->view->set('U', function($relativePath) use($container) | ||||
| 		{ | ||||
| 			return $container->UrlManager->ConstructUrl($relativePath); | ||||
| 		}); | ||||
|  | ||||
| 		$this->AppContainer = $container; | ||||
| 	} | ||||
|  | ||||
| 	protected $AppContainer; | ||||
| 	protected $Database; | ||||
| } | ||||
							
								
								
									
										47
									
								
								controllers/BatteriesApiController.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,47 @@ | ||||
| <?php | ||||
|  | ||||
| namespace Grocy\Controllers; | ||||
|  | ||||
| use \Grocy\Services\BatteriesService; | ||||
|  | ||||
| class BatteriesApiController extends BaseApiController | ||||
| { | ||||
| 	public function __construct(\Slim\Container $container) | ||||
| 	{ | ||||
| 		parent::__construct($container); | ||||
| 		$this->BatteriesService = new BatteriesService(); | ||||
| 	} | ||||
|  | ||||
| 	protected $BatteriesService; | ||||
|  | ||||
| 	public function TrackChargeCycle(\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'])) | ||||
| 		{ | ||||
| 			$trackedTime = $request->getQueryParams()['tracked_time']; | ||||
| 		} | ||||
|  | ||||
| 		try | ||||
| 		{ | ||||
| 			$this->BatteriesService->TrackChargeCycle($args['batteryId'], $trackedTime); | ||||
| 			return $this->VoidApiActionResponse($response); | ||||
| 		} | ||||
| 		catch (\Exception $ex) | ||||
| 		{ | ||||
| 			return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage()); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	public function BatteryDetails(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		try | ||||
| 		{ | ||||
| 			return $this->ApiResponse($this->BatteriesService->GetBatteryDetails($args['batteryId'])); | ||||
| 		} | ||||
| 		catch (\Exception $ex) | ||||
| 		{ | ||||
| 			return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage()); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										62
									
								
								controllers/BatteriesController.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,62 @@ | ||||
| <?php | ||||
|  | ||||
| namespace Grocy\Controllers; | ||||
|  | ||||
| use \Grocy\Services\BatteriesService; | ||||
|  | ||||
| class BatteriesController extends BaseController | ||||
| { | ||||
| 	public function __construct(\Slim\Container $container) | ||||
| 	{ | ||||
| 		parent::__construct($container); | ||||
| 		$this->BatteriesService = new BatteriesService(); | ||||
| 	} | ||||
|  | ||||
| 	protected $BatteriesService; | ||||
|  | ||||
| 	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); | ||||
| 		} | ||||
|  | ||||
| 		return $this->AppContainer->view->render($response, 'batteriesoverview', [ | ||||
| 			'batteries' => $this->Database->batteries(), | ||||
| 			'current' => $this->BatteriesService->GetCurrent(), | ||||
| 			'nextChargeTimes' => $nextChargeTimes | ||||
| 		]); | ||||
| 	} | ||||
|  | ||||
| 	public function TrackChargeCycle(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		return $this->AppContainer->view->render($response, 'batterytracking', [ | ||||
| 			'batteries' =>  $this->Database->batteries() | ||||
| 		]); | ||||
| 	} | ||||
|  | ||||
| 	public function BatteriesList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		return $this->AppContainer->view->render($response, 'batteries', [ | ||||
| 			'batteries' => $this->Database->batteries() | ||||
| 		]); | ||||
| 	} | ||||
|  | ||||
| 	public function BatteryEditForm(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		if ($args['batteryId'] == 'new') | ||||
| 		{ | ||||
| 			return $this->AppContainer->view->render($response, 'batteryform', [ | ||||
| 				'mode' => 'create' | ||||
| 			]); | ||||
| 		} | ||||
| 		else | ||||
| 		{ | ||||
| 			return $this->AppContainer->view->render($response, 'batteryform', [ | ||||
| 				'battery' =>  $this->Database->batteries($args['batteryId']), | ||||
| 				'mode' => 'edit' | ||||
| 			]); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										19
									
								
								controllers/CliController.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,19 @@ | ||||
| <?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(); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										80
									
								
								controllers/GenericEntityApiController.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,80 @@ | ||||
| <?php | ||||
|  | ||||
| namespace Grocy\Controllers; | ||||
|  | ||||
| class GenericEntityApiController extends BaseApiController | ||||
| { | ||||
| 	public function GetObjects(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		if ($this->IsValidEntity($args['entity'])) | ||||
| 		{ | ||||
| 			return $this->ApiResponse($this->Database->{$args['entity']}()); | ||||
| 		} | ||||
| 		else | ||||
| 		{ | ||||
| 			return $this->VoidApiActionResponse($response, false, 400, 'Entity does not exist or is not exposed'); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	public function GetObject(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		if ($this->IsValidEntity($args['entity'])) | ||||
| 		{ | ||||
| 			return $this->ApiResponse($this->Database->{$args['entity']}($args['objectId'])); | ||||
| 		} | ||||
| 		else | ||||
| 		{ | ||||
| 			return $this->VoidApiActionResponse($response, false, 400, 'Entity does not exist or is not exposed'); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	public function AddObject(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		if ($this->IsValidEntity($args['entity'])) | ||||
| 		{ | ||||
| 			$newRow = $this->Database->{$args['entity']}()->createRow($request->getParsedBody()); | ||||
| 			$newRow->save(); | ||||
| 			$success = $newRow->isClean(); | ||||
| 			return $this->ApiResponse(array('success' => $success)); | ||||
| 		} | ||||
| 		else | ||||
| 		{ | ||||
| 			return $this->VoidApiActionResponse($response, false, 400, 'Entity does not exist or is not exposed'); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	public function EditObject(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		if ($this->IsValidEntity($args['entity'])) | ||||
| 		{ | ||||
| 			$row = $this->Database->{$args['entity']}($args['objectId']); | ||||
| 			$row->update($request->getParsedBody()); | ||||
| 			$success = $row->isClean(); | ||||
| 			return $this->ApiResponse(array('success' => $success)); | ||||
| 		} | ||||
| 		else | ||||
| 		{ | ||||
| 			return $this->VoidApiActionResponse($response, false, 400, 'Entity does not exist or is not exposed'); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	public function DeleteObject(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		if ($this->IsValidEntity($args['entity'])) | ||||
| 		{ | ||||
| 			$row = $this->Database->{$args['entity']}($args['objectId']); | ||||
| 			$row->delete(); | ||||
| 			$success = $row->isClean(); | ||||
| 			return $this->ApiResponse(array('success' => $success)); | ||||
| 		} | ||||
| 		else | ||||
| 		{ | ||||
| 			return $this->VoidApiActionResponse($response, false, 400, 'Entity does not exist or is not exposed'); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	private function IsValidEntity($entity) | ||||
| 	{ | ||||
| 		return in_array($entity, $this->OpenApiSpec->components->internalSchemas->ExposedEntity->enum); | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										47
									
								
								controllers/HabitsApiController.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,47 @@ | ||||
| <?php | ||||
|  | ||||
| namespace Grocy\Controllers; | ||||
|  | ||||
| use \Grocy\Services\HabitsService; | ||||
|  | ||||
| class HabitsApiController extends BaseApiController | ||||
| { | ||||
| 	public function __construct(\Slim\Container $container) | ||||
| 	{ | ||||
| 		parent::__construct($container); | ||||
| 		$this->HabitsService = new HabitsService(); | ||||
| 	} | ||||
|  | ||||
| 	protected $HabitsService; | ||||
|  | ||||
| 	public function TrackHabitExecution(\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'])) | ||||
| 		{ | ||||
| 			$trackedTime = $request->getQueryParams()['tracked_time']; | ||||
| 		} | ||||
|  | ||||
| 		try | ||||
| 		{ | ||||
| 			$this->HabitsService->TrackHabit($args['habitId'], $trackedTime); | ||||
| 			return $this->VoidApiActionResponse($response); | ||||
| 		} | ||||
| 		catch (\Exception $ex) | ||||
| 		{ | ||||
| 			return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage()); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	public function HabitDetails(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		try | ||||
| 		{ | ||||
| 			return $this->ApiResponse($this->HabitsService->GetHabitDetails($args['habitId'])); | ||||
| 		} | ||||
| 		catch (\Exception $ex) | ||||
| 		{ | ||||
| 			return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage()); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										64
									
								
								controllers/HabitsController.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,64 @@ | ||||
| <?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); | ||||
| 		} | ||||
|  | ||||
| 		return $this->AppContainer->view->render($response, 'habitsoverview', [ | ||||
| 			'habits' => $this->Database->habits(), | ||||
| 			'currentHabits' => $this->HabitsService->GetCurrentHabits(), | ||||
| 			'nextHabitTimes' => $nextHabitTimes | ||||
| 		]); | ||||
| 	} | ||||
|  | ||||
| 	public function TrackHabitExecution(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		return $this->AppContainer->view->render($response, 'habittracking', [ | ||||
| 			'habits' => $this->Database->habits() | ||||
| 		]); | ||||
| 	} | ||||
|  | ||||
| 	public function HabitsList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		return $this->AppContainer->view->render($response, 'habits', [ | ||||
| 			'habits' => $this->Database->habits() | ||||
| 		]); | ||||
| 	} | ||||
|  | ||||
| 	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' | ||||
| 			]); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										76
									
								
								controllers/LoginController.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,76 @@ | ||||
| <?php | ||||
|  | ||||
| namespace Grocy\Controllers; | ||||
|  | ||||
| use \Grocy\Services\SessionService; | ||||
| use \Grocy\Services\ApplicationService; | ||||
| use \Grocy\Services\DatabaseMigrationService; | ||||
| use \Grocy\Services\DemoDataGeneratorService; | ||||
|  | ||||
| class LoginController extends BaseController | ||||
| { | ||||
| 	public function __construct(\Slim\Container $container, string $sessionCookieName) | ||||
| 	{ | ||||
| 		parent::__construct($container); | ||||
| 		$this->SessionService = new SessionService(); | ||||
| 		$this->SessionCookieName = $sessionCookieName; | ||||
| 	} | ||||
|  | ||||
| 	protected $SessionService; | ||||
| 	protected $SessionCookieName; | ||||
|  | ||||
| 	public function ProcessLogin(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		$postParams = $request->getParsedBody(); | ||||
| 		if (isset($postParams['username']) && isset($postParams['password'])) | ||||
| 		{ | ||||
| 			if ($postParams['username'] === HTTP_USER && $postParams['password'] === HTTP_PASSWORD) | ||||
| 			{ | ||||
| 				$sessionKey = $this->SessionService->CreateSession(); | ||||
| 				setcookie($this->SessionCookieName, $sessionKey, time() + 31536000); // Cookie expires in 1 year, but session validity is up to SessionService | ||||
|  | ||||
| 				return $response->withRedirect($this->AppContainer->UrlManager->ConstructUrl('/')); | ||||
| 			} | ||||
| 			else | ||||
| 			{ | ||||
| 				return $response->withRedirect($this->AppContainer->UrlManager->ConstructUrl('/login?invalid=true')); | ||||
| 			} | ||||
| 		} | ||||
| 		else | ||||
| 		{ | ||||
| 			return $response->withRedirect($this->AppContainer->UrlManager->ConstructUrl('/login?invalid=true')); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	public function LoginPage(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		return $this->AppContainer->view->render($response, 'login'); | ||||
| 	} | ||||
|  | ||||
| 	public function Logout(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		$this->SessionService->RemoveSession($_COOKIE[$this->SessionCookieName]); | ||||
| 		return $response->withRedirect($this->AppContainer->UrlManager->ConstructUrl('/')); | ||||
| 	} | ||||
|  | ||||
| 	public function Root(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		// Schema migration is done here | ||||
| 		$databaseMigrationService = new DatabaseMigrationService(); | ||||
| 		$databaseMigrationService->MigrateDatabase(); | ||||
|  | ||||
| 		$applicationService = new ApplicationService(); | ||||
| 		if ($applicationService->IsDemoInstallation()) | ||||
| 		{ | ||||
| 			$demoDataGeneratorService = new DemoDataGeneratorService(); | ||||
| 			$demoDataGeneratorService->PopulateDemoData(); | ||||
| 		} | ||||
|  | ||||
| 		return $response->withRedirect($this->AppContainer->UrlManager->ConstructUrl('/stockoverview')); | ||||
| 	} | ||||
|  | ||||
| 	public function GetSessionCookieName() | ||||
| 	{ | ||||
| 		return $this->SessionCookieName; | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										47
									
								
								controllers/OpenApiController.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,47 @@ | ||||
| <?php | ||||
|  | ||||
| namespace Grocy\Controllers; | ||||
|  | ||||
| use \Grocy\Services\ApplicationService; | ||||
| use \Grocy\Services\ApiKeyService; | ||||
|  | ||||
| class OpenApiController extends BaseApiController | ||||
| { | ||||
| 	public function __construct(\Slim\Container $container) | ||||
| 	{ | ||||
| 		parent::__construct($container); | ||||
| 		$this->ApiKeyService = new ApiKeyService(); | ||||
| 	} | ||||
|  | ||||
| 	protected $ApiKeyService; | ||||
|  | ||||
| 	public function DocumentationUi(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		return $this->AppContainer->view->render($response, 'openapiui'); | ||||
| 	} | ||||
|  | ||||
| 	public function DocumentationSpec(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		$applicationService = new ApplicationService(); | ||||
|  | ||||
| 		$this->OpenApiSpec->info->version = $applicationService->GetInstalledVersion(); | ||||
| 		$this->OpenApiSpec->info->description = str_replace('PlaceHolderManageApiKeysUrl', $this->AppContainer->UrlManager->ConstructUrl('/manageapikeys'), $this->OpenApiSpec->info->description); | ||||
| 		$this->OpenApiSpec->servers[0]->url = $this->AppContainer->UrlManager->ConstructUrl('/api'); | ||||
|  | ||||
| 		return $this->ApiResponse($this->OpenApiSpec); | ||||
| 	} | ||||
|  | ||||
| 	public function ApiKeysList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		return $this->AppContainer->view->render($response, 'manageapikeys', [ | ||||
| 			'apiKeys' => $this->Database->api_keys() | ||||
| 		]); | ||||
| 	} | ||||
|  | ||||
| 	public function CreateNewApiKey(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		$newApiKey = $this->ApiKeyService->CreateApiKey(); | ||||
| 		$newApiKeyId = $this->ApiKeyService->GetApiKeyId($newApiKey); | ||||
| 		return $response->withRedirect($this->AppContainer->UrlManager->ConstructUrl("/manageapikeys?CreatedApiKeyId=$newApiKeyId")); | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										126
									
								
								controllers/StockApiController.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,126 @@ | ||||
| <?php | ||||
|  | ||||
| namespace Grocy\Controllers; | ||||
|  | ||||
| use \Grocy\Services\StockService; | ||||
|  | ||||
| class StockApiController extends BaseApiController | ||||
| { | ||||
| 	public function __construct(\Slim\Container $container) | ||||
| 	{ | ||||
| 		parent::__construct($container); | ||||
| 		$this->StockService = new StockService(); | ||||
| 	} | ||||
|  | ||||
| 	protected $StockService; | ||||
|  | ||||
| 	public function ProductDetails(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		try | ||||
| 		{ | ||||
| 			return $this->ApiResponse($this->StockService->GetProductDetails($args['productId'])); | ||||
| 		} | ||||
| 		catch (\Exception $ex) | ||||
| 		{ | ||||
| 			return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage()); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	public function AddProduct(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		$bestBeforeDate = date('Y-m-d'); | ||||
| 		if (isset($request->getQueryParams()['bestbeforedate']) && !empty($request->getQueryParams()['bestbeforedate'])) | ||||
| 		{ | ||||
| 			$bestBeforeDate = $request->getQueryParams()['bestbeforedate']; | ||||
| 		} | ||||
|  | ||||
| 		$transactionType = StockService::TRANSACTION_TYPE_PURCHASE; | ||||
| 		if (isset($request->getQueryParams()['transactiontype']) && !empty($request->getQueryParams()['transactiontype'])) | ||||
| 		{ | ||||
| 			$transactionType = $request->getQueryParams()['transactiontype']; | ||||
| 		} | ||||
|  | ||||
| 		try | ||||
| 		{ | ||||
| 			$this->StockService->AddProduct($args['productId'], $args['amount'], $bestBeforeDate, $transactionType); | ||||
| 			return $this->VoidApiActionResponse($response); | ||||
| 		} | ||||
| 		catch (\Exception $ex) | ||||
| 		{ | ||||
| 			return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage()); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	public function ConsumeProduct(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		$spoiled = false; | ||||
| 		if (isset($request->getQueryParams()['spoiled']) && !empty($request->getQueryParams()['spoiled']) && $request->getQueryParams()['spoiled'] == '1') | ||||
| 		{ | ||||
| 			$spoiled = true; | ||||
| 		} | ||||
|  | ||||
| 		$transactionType = StockService::TRANSACTION_TYPE_CONSUME; | ||||
| 		if (isset($request->getQueryParams()['transactiontype']) && !empty($request->getQueryParams()['transactiontype'])) | ||||
| 		{ | ||||
| 			$transactionType = $request->getQueryParams()['transactiontype']; | ||||
| 		} | ||||
|  | ||||
| 		try | ||||
| 		{ | ||||
| 			$this->StockService->ConsumeProduct($args['productId'], $args['amount'], $spoiled, $transactionType); | ||||
| 			return $this->VoidApiActionResponse($response); | ||||
| 		} | ||||
| 		catch (\Exception $ex) | ||||
| 		{ | ||||
| 			return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage()); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	public function InventoryProduct(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		$bestBeforeDate = date('Y-m-d'); | ||||
| 		if (isset($request->getQueryParams()['bestbeforedate']) && !empty($request->getQueryParams()['bestbeforedate'])) | ||||
| 		{ | ||||
| 			$bestBeforeDate = $request->getQueryParams()['bestbeforedate']; | ||||
| 		} | ||||
|  | ||||
| 		try | ||||
| 		{ | ||||
| 			$this->StockService->InventoryProduct($args['productId'], $args['newAmount'], $bestBeforeDate); | ||||
| 			return $this->VoidApiActionResponse($response); | ||||
| 		} | ||||
| 		catch (\Exception $ex) | ||||
| 		{ | ||||
| 			return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage()); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	public function CurrentStock(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		return $this->ApiResponse($this->StockService->GetCurrentStock()); | ||||
| 	} | ||||
|  | ||||
| 	public function AddMissingProductsToShoppingList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		$this->StockService->AddMissingProductsToShoppingList(); | ||||
| 		return $this->VoidApiActionResponse($response); | ||||
| 	} | ||||
|  | ||||
| 	public function ExternalBarcodeLookup(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		try | ||||
| 		{ | ||||
| 			$addFoundProduct = false; | ||||
| 			if (isset($request->getQueryParams()['add']) && ($request->getQueryParams()['add'] === 'true' || $request->getQueryParams()['add'] === 1)) | ||||
| 			{ | ||||
| 				$addFoundProduct = true; | ||||
| 			} | ||||
| 			 | ||||
| 			return $this->ApiResponse($this->StockService->ExternalBarcodeLookup($args['barcode'], $addFoundProduct)); | ||||
| 		} | ||||
| 		catch (\Exception $ex) | ||||
| 		{ | ||||
| 			return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage()); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										155
									
								
								controllers/StockController.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,155 @@ | ||||
| <?php | ||||
|  | ||||
| namespace Grocy\Controllers; | ||||
|  | ||||
| use \Grocy\Services\StockService; | ||||
|  | ||||
| class StockController extends BaseController | ||||
| { | ||||
|  | ||||
| 	public function __construct(\Slim\Container $container) | ||||
| 	{ | ||||
| 		parent::__construct($container); | ||||
| 		$this->StockService = new StockService(); | ||||
| 	} | ||||
|  | ||||
| 	protected $StockService; | ||||
|  | ||||
| 	public function Overview(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		return $this->AppContainer->view->render($response, 'stockoverview', [ | ||||
| 			'products' => $this->Database->products(), | ||||
| 			'quantityunits' => $this->Database->quantity_units(), | ||||
| 			'currentStock' => $this->StockService->GetCurrentStock(), | ||||
| 			'missingProducts' => $this->StockService->GetMissingProducts() | ||||
| 		]); | ||||
| 	} | ||||
|  | ||||
| 	public function Purchase(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		return $this->AppContainer->view->render($response, 'purchase', [ | ||||
| 			'products' => $this->Database->products() | ||||
| 		]); | ||||
| 	} | ||||
|  | ||||
| 	public function Consume(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		return $this->AppContainer->view->render($response, 'consume', [ | ||||
| 			'products' => $this->Database->products() | ||||
| 		]); | ||||
| 	} | ||||
|  | ||||
| 	public function Inventory(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		return $this->AppContainer->view->render($response, 'inventory', [ | ||||
| 			'products' => $this->Database->products() | ||||
| 		]); | ||||
| 	} | ||||
|  | ||||
| 	public function ShoppingList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		return $this->AppContainer->view->render($response, 'shoppinglist', [ | ||||
| 			'listItems' => $this->Database->shopping_list(), | ||||
| 			'products' => $this->Database->products(), | ||||
| 			'quantityunits' => $this->Database->quantity_units(), | ||||
| 			'missingProducts' => $this->StockService->GetMissingProducts() | ||||
| 		]); | ||||
| 	} | ||||
|  | ||||
| 	public function ProductsList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		return $this->AppContainer->view->render($response, 'products', [ | ||||
| 			'products' => $this->Database->products(), | ||||
| 			'locations' => $this->Database->locations(), | ||||
| 			'quantityunits' => $this->Database->quantity_units() | ||||
| 		]); | ||||
| 	} | ||||
|  | ||||
| 	public function LocationsList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		return $this->AppContainer->view->render($response, 'locations', [ | ||||
| 			'locations' => $this->Database->locations() | ||||
| 		]); | ||||
| 	} | ||||
|  | ||||
| 	public function QuantityUnitsList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		return $this->AppContainer->view->render($response, 'quantityunits', [ | ||||
| 			'quantityunits' => $this->Database->quantity_units() | ||||
| 		]); | ||||
| 	} | ||||
|  | ||||
| 	public function ProductEditForm(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		if ($args['productId'] == 'new') | ||||
| 		{ | ||||
| 			return $this->AppContainer->view->render($response, 'productform', [ | ||||
| 				'locations' =>  $this->Database->locations(), | ||||
| 				'quantityunits' =>  $this->Database->quantity_units(), | ||||
| 				'mode' => 'create' | ||||
| 			]); | ||||
| 		} | ||||
| 		else | ||||
| 		{ | ||||
| 			return $this->AppContainer->view->render($response, 'productform', [ | ||||
| 				'product' =>  $this->Database->products($args['productId']), | ||||
| 				'locations' =>  $this->Database->locations(), | ||||
| 				'quantityunits' =>  $this->Database->quantity_units(), | ||||
| 				'mode' => 'edit' | ||||
| 			]); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	public function LocationEditForm(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		if ($args['locationId'] == 'new') | ||||
| 		{ | ||||
| 			return $this->AppContainer->view->render($response, 'locationform', [ | ||||
| 				'mode' => 'create' | ||||
| 			]); | ||||
| 		} | ||||
| 		else | ||||
| 		{ | ||||
| 			return $this->AppContainer->view->render($response, 'locationform', [ | ||||
| 				'location' =>  $this->Database->locations($args['locationId']), | ||||
| 				'mode' => 'edit' | ||||
| 			]); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	public function QuantityUnitEditForm(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		if ($args['quantityunitId'] == 'new') | ||||
| 		{ | ||||
| 			return $this->AppContainer->view->render($response, 'quantityunitform', [ | ||||
| 				'mode' => 'create' | ||||
| 			]); | ||||
| 		} | ||||
| 		else | ||||
| 		{ | ||||
| 			return $this->AppContainer->view->render($response, 'quantityunitform', [ | ||||
| 				'quantityunit' =>  $this->Database->quantity_units($args['quantityunitId']), | ||||
| 				'mode' => 'edit' | ||||
| 			]); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	public function ShoppingListItemEditForm(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) | ||||
| 	{ | ||||
| 		if ($args['itemId'] == 'new') | ||||
| 		{ | ||||
| 			return $this->AppContainer->view->render($response, 'shoppinglistform', [ | ||||
| 				'products' =>  $this->Database->products(), | ||||
| 				'mode' => 'create' | ||||
| 			]); | ||||
| 		} | ||||
| 		else | ||||
| 		{ | ||||
| 			return $this->AppContainer->view->render($response, 'shoppinglistform', [ | ||||
| 				'listItem' =>  $this->Database->shopping_list($args['itemId']), | ||||
| 				'products' =>  $this->Database->products(), | ||||
| 				'mode' => 'edit' | ||||
| 			]); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										2
									
								
								data/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,2 +1,4 @@ | ||||
| * | ||||
| !.gitignore | ||||
| !viewcache | ||||
| !plugins | ||||
|   | ||||
							
								
								
									
										3
									
								
								data/plugins/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,3 @@ | ||||
| * | ||||
| !.gitignore | ||||
| !DemoBarcodeLookupPlugin.php | ||||
							
								
								
									
										78
									
								
								data/plugins/DemoBarcodeLookupPlugin.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,78 @@ | ||||
| <?php | ||||
|  | ||||
| use \Grocy\Helpers\BaseBarcodeLookupPlugin; | ||||
|  | ||||
| /* | ||||
| 	This class must extend BaseBarcodeLookupPlugin (in namespace \Grocy\Helpers) | ||||
| */ | ||||
| class DemoBarcodeLookupPlugin extends BaseBarcodeLookupPlugin | ||||
| { | ||||
| 	/* | ||||
| 		To use this plugin, configure it in data/config.php like this: | ||||
| 		define('STOCK_BARCODE_LOOKUP_PLUGIN', 'DemoBarcodeLookupPlugin'); | ||||
| 	*/ | ||||
|  | ||||
| 	/* | ||||
| 		To try it: | ||||
| 		Call the API function at /api/stock/external-barcode-lookup/{barcode} | ||||
|  | ||||
| 		When you also add ?add=true as a query parameter to the API call, | ||||
| 		on a successful lookup the product is added to the database and in the output | ||||
| 		the new product id is included (automatically, nothing to do here in the plugin) | ||||
| 	*/ | ||||
|  | ||||
| 	/* | ||||
| 		Provided references: | ||||
|  | ||||
| 		$this->Locations contains all locations | ||||
| 		$this->QuantityUnits contains all quantity units | ||||
| 	*/ | ||||
|  | ||||
| 	/* | ||||
| 		Useful hints: | ||||
|  | ||||
| 		Get a quantity unit by name: | ||||
| 		$quantityUnit = FindObjectInArrayByPropertyValue($this->QuantityUnits, 'name', 'Piece'); | ||||
|  | ||||
| 		Get a location by name: | ||||
| 		$location = FindObjectInArrayByPropertyValue($this->Locations, 'name', 'Fridge'); | ||||
| 	*/ | ||||
|  | ||||
| 	/* | ||||
| 		This class must implement the protected abstract function ExecuteLookup($barcode), | ||||
| 		which is called with the barcode that needs to be looked up and must return an | ||||
| 		associative array of the product model or null, when nothing was found for the barcode. | ||||
|  | ||||
| 		The returned array must contain at least these properties: | ||||
| 		array( | ||||
| 			'name' => '', | ||||
| 			'location_id' => 1, // A valid id of a location object, check against $this->Locations | ||||
| 			'qu_id_purchase' => 1, // A valid id of quantity unit object, check against $this->QuantityUnits | ||||
| 			'qu_id_stock' => 1, // A valid id of quantity unit object, check against $this->QuantityUnits | ||||
| 			'qu_factor_purchase_to_stock' => 1, // Normally 1 when quantity unit stock and purchase is the same | ||||
| 			'barcode' => $barcode // The barcode of the product, maybe just pass through $barcode or manipulate it if necessary | ||||
| 		) | ||||
| 	*/ | ||||
| 	protected function ExecuteLookup($barcode) | ||||
| 	{ | ||||
| 		if ($barcode === 'x') // Demonstration when nothing is found | ||||
| 		{ | ||||
| 			return null; | ||||
| 		} | ||||
| 		elseif ($barcode === 'e') // Demonstration when an error occurred | ||||
| 		{ | ||||
| 			throw new \Exception('This is the error message from the plugin...'); | ||||
| 		} | ||||
| 		else | ||||
| 		{ | ||||
| 			return array( | ||||
| 				'name' => 'LookedUpProduct_' . RandomString(5), | ||||
| 				'location_id' => $this->Locations[0]->id, | ||||
| 				'qu_id_purchase' => $this->QuantityUnits[0]->id, | ||||
| 				'qu_id_stock' => $this->QuantityUnits[0]->id, | ||||
| 				'qu_factor_purchase_to_stock' => 1, | ||||
| 				'barcode' => $barcode | ||||
| 			); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										2
									
								
								data/viewcache/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,2 @@ | ||||
| * | ||||
| !.gitignore | ||||
							
								
								
									
										103
									
								
								grocy.js
									
									
									
									
									
								
							
							
						
						| @@ -1,103 +0,0 @@ | ||||
| var Grocy = { }; | ||||
|  | ||||
| $(function() | ||||
| { | ||||
| 	var menuItem = $('.nav').find("[data-nav-for-page='" + Grocy.ContentPage + "']"); | ||||
| 	menuItem.addClass('active'); | ||||
|  | ||||
| 	$.timeago.settings.allowFuture = true; | ||||
| 	$('time.timeago').timeago(); | ||||
| }); | ||||
|  | ||||
| Grocy.FetchJson = function(url, success, error) | ||||
| { | ||||
| 	var xhr = new XMLHttpRequest(); | ||||
|  | ||||
| 	xhr.onreadystatechange = function() | ||||
| 	{ | ||||
| 		if (xhr.readyState === XMLHttpRequest.DONE) | ||||
| 		{ | ||||
| 			if (xhr.status === 200) | ||||
| 			{ | ||||
| 				if (success) | ||||
| 				{ | ||||
| 					success(JSON.parse(xhr.responseText)); | ||||
| 				} | ||||
| 			} | ||||
| 			else | ||||
| 			{ | ||||
| 				if (error) | ||||
| 				{ | ||||
| 					error(xhr); | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	}; | ||||
|  | ||||
| 	xhr.open('GET', url, true); | ||||
| 	xhr.send(); | ||||
| }; | ||||
|  | ||||
| Grocy.PostJson = function(url, jsonData, success, error) | ||||
| { | ||||
| 	var xhr = new XMLHttpRequest(); | ||||
|  | ||||
| 	xhr.onreadystatechange = function() | ||||
| 	{ | ||||
| 		if (xhr.readyState === XMLHttpRequest.DONE) | ||||
| 		{ | ||||
| 			if (xhr.status === 200) | ||||
| 			{ | ||||
| 				if (success) | ||||
| 				{ | ||||
| 					success(JSON.parse(xhr.responseText)); | ||||
| 				} | ||||
| 			} | ||||
| 			else | ||||
| 			{ | ||||
| 				if (error) | ||||
| 				{ | ||||
| 					error(xhr); | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	}; | ||||
|  | ||||
| 	xhr.open('POST', url, true); | ||||
| 	xhr.setRequestHeader('Content-type', 'application/json'); | ||||
| 	xhr.send(JSON.stringify(jsonData)); | ||||
| }; | ||||
|  | ||||
| Grocy.EmptyElementWhenMatches = function(selector, text) | ||||
| { | ||||
| 	if ($(selector).text() === text) | ||||
| 	{ | ||||
| 		$(selector).text(''); | ||||
| 	} | ||||
| }; | ||||
|  | ||||
| String.prototype.contains = function(search) | ||||
| { | ||||
| 	return this.toLowerCase().indexOf(search.toLowerCase()) !== -1; | ||||
| }; | ||||
|  | ||||
| Grocy.GetUriParam = function(key) | ||||
| { | ||||
| 	var currentUri = decodeURIComponent(window.location.search.substring(1)); | ||||
| 	var vars = currentUri.split('&'); | ||||
|  | ||||
| 	for (i = 0; i < vars.length; i++) | ||||
| 	{ | ||||
| 		var currentParam = vars[i].split('='); | ||||
|  | ||||
| 		if (currentParam[0] === key) | ||||
| 		{ | ||||
| 			return currentParam[1] === undefined ? true : currentParam[1]; | ||||
| 		} | ||||
| 	} | ||||
| }; | ||||
|  | ||||
| Grocy.Wait = function(ms) | ||||
| { | ||||
| 	return new Promise(resolve => setTimeout(resolve, ms)); | ||||
| } | ||||
							
								
								
									
										1384
									
								
								grocy.openapi.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										80
									
								
								helpers/BaseBarcodeLookupPlugin.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,80 @@ | ||||
| <?php | ||||
|  | ||||
| namespace Grocy\Helpers; | ||||
|  | ||||
| abstract class BaseBarcodeLookupPlugin | ||||
| { | ||||
| 	final public function __construct($locations, $quantityUnits) | ||||
| 	{ | ||||
| 		$this->Locations = $locations; | ||||
| 		$this->QuantityUnits = $quantityUnits; | ||||
| 	} | ||||
|  | ||||
| 	protected $Locations; | ||||
| 	protected $QuantityUnits; | ||||
|  | ||||
| 	abstract protected function ExecuteLookup($barcode); | ||||
|  | ||||
| 	final public function Lookup($barcode) | ||||
| 	{ | ||||
| 		$pluginOutput = $this->ExecuteLookup($barcode); | ||||
|  | ||||
| 		if ($pluginOutput === null) | ||||
| 		{ | ||||
| 			return $pluginOutput; | ||||
| 		} | ||||
|  | ||||
| 		// Plugin must return an associative array | ||||
| 		if (!is_array($pluginOutput)) | ||||
| 		{ | ||||
| 			throw new \Exception('Plugin output must be an associative array'); | ||||
| 		} | ||||
| 		if (!IsAssociativeArray($pluginOutput)) // $pluginOutput is at least an indexed array here | ||||
| 		{ | ||||
| 			throw new \Exception('Plugin output must be an associative array'); | ||||
| 		} | ||||
|  | ||||
| 		// Check for minimum needed properties | ||||
| 		$minimunNeededProperties = array( | ||||
| 			'name', | ||||
| 			'location_id', | ||||
| 			'qu_id_purchase', | ||||
| 			'qu_id_stock', | ||||
| 			'qu_factor_purchase_to_stock', | ||||
| 			'barcode' | ||||
| 		); | ||||
| 		foreach ($minimunNeededProperties as $prop) | ||||
| 		{ | ||||
| 			if (!array_key_exists($prop, $pluginOutput)) | ||||
| 			{ | ||||
| 				throw new \Exception("Plugin output does not provide needed property $prop"); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		// $pluginOutput contains all needed properties here | ||||
|  | ||||
| 		// Check referenced entity ids are valid | ||||
| 		$locationId = $pluginOutput['location_id']; | ||||
| 		if (FindObjectInArrayByPropertyValue($this->Locations, 'id', $locationId) === null) | ||||
| 		{ | ||||
| 			throw new \Exception("Location $locationId is not a valid location id"); | ||||
| 		} | ||||
| 		$quIdPurchase = $pluginOutput['qu_id_purchase']; | ||||
| 		if (FindObjectInArrayByPropertyValue($this->QuantityUnits, 'id', $quIdPurchase) === null) | ||||
| 		{ | ||||
| 			throw new \Exception("Location $quIdPurchase is not a valid quantity unit id"); | ||||
| 		} | ||||
| 		$quIdStock = $pluginOutput['qu_id_stock']; | ||||
| 		if (FindObjectInArrayByPropertyValue($this->QuantityUnits, 'id', $quIdStock) === null) | ||||
| 		{ | ||||
| 			throw new \Exception("Location $quIdStock is not a valid quantity unit id"); | ||||
| 		} | ||||
| 		$quFactor = $pluginOutput['qu_factor_purchase_to_stock']; | ||||
| 		if (empty($quFactor) || !is_numeric($quFactor)) | ||||
| 		{ | ||||
| 			throw new \Exception('Quantity unit factor is empty or not a number'); | ||||
| 		} | ||||
|  | ||||
| 		return $pluginOutput; | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										30
									
								
								helpers/UrlManager.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,30 @@ | ||||
| <?php | ||||
|  | ||||
| namespace Grocy\Helpers; | ||||
|  | ||||
| class UrlManager | ||||
| { | ||||
| 	public function __construct(string $basePath) | ||||
| 	{ | ||||
| 		if ($basePath === '/') | ||||
| 		{ | ||||
| 			$this->BasePath = $this->GetBaseUrl(); | ||||
| 		} | ||||
| 		else | ||||
| 		{ | ||||
| 			$this->BasePath = $basePath; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	protected $BasePath; | ||||
|  | ||||
| 	public function ConstructUrl($relativePath) | ||||
| 	{ | ||||
| 		return rtrim($this->BasePath, '/') . $relativePath; | ||||
| 	} | ||||
|  | ||||
| 	private function GetBaseUrl() | ||||
| 	{ | ||||
| 		return (isset($_SERVER['HTTPS']) ? "https" : "http") . "://$_SERVER[HTTP_HOST]"; | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										80
									
								
								helpers/extensions.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,80 @@ | ||||
| <?php | ||||
|  | ||||
| function FindObjectInArrayByPropertyValue($array, $propertyName, $propertyValue) | ||||
| { | ||||
| 	foreach($array as $object) | ||||
| 	{ | ||||
| 		if($object->{$propertyName} == $propertyValue) | ||||
| 		{ | ||||
| 			return $object; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return null; | ||||
| } | ||||
|  | ||||
| function FindAllObjectsInArrayByPropertyValue($array, $propertyName, $propertyValue, $operator = '==') | ||||
| { | ||||
| 	$returnArray = array(); | ||||
|  | ||||
| 	foreach($array as $object) | ||||
| 	{ | ||||
| 		switch($operator) | ||||
| 		{ | ||||
| 			case '==': | ||||
| 				if($object->{$propertyName} == $propertyValue) | ||||
| 				{ | ||||
| 					$returnArray[] = $object; | ||||
| 				} | ||||
| 				break; | ||||
| 			case '>': | ||||
| 				if($object->{$propertyName} > $propertyValue) | ||||
| 				{ | ||||
| 					$returnArray[] = $object; | ||||
| 				} | ||||
| 				break; | ||||
| 			case '<': | ||||
| 				if($object->{$propertyName} < $propertyValue) | ||||
| 				{ | ||||
| 					$returnArray[] = $object; | ||||
| 				} | ||||
| 				break; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return $returnArray; | ||||
| } | ||||
|  | ||||
| function SumArrayValue($array, $propertyName) | ||||
| { | ||||
| 	$sum = 0; | ||||
| 	foreach($array as $object) | ||||
| 	{ | ||||
| 		$sum += $object->{$propertyName}; | ||||
| 	} | ||||
|  | ||||
| 	return $sum; | ||||
| } | ||||
|  | ||||
| function GetClassConstants($className) | ||||
| { | ||||
| 	$r = new ReflectionClass($className); | ||||
| 	return $r->getConstants(); | ||||
| } | ||||
|  | ||||
| function RandomString($length, $allowedChars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ') | ||||
| { | ||||
| 	$randomString = ''; | ||||
| 	for ($i = 0; $i < $length; $i++) | ||||
| 	{ | ||||
| 		$randomString .= $allowedChars[rand(0, strlen($allowedChars) - 1)]; | ||||
| 	} | ||||
|  | ||||
| 	return $randomString; | ||||
| } | ||||
|  | ||||
| function IsAssociativeArray(array $array) | ||||
| { | ||||
| 	$keys = array_keys($array); | ||||
| 	return array_keys($keys) !== $keys; | ||||
| } | ||||
							
								
								
									
										531
									
								
								index.php
									
									
									
									
									
								
							
							
						
						| @@ -1,531 +0,0 @@ | ||||
| <?php | ||||
|  | ||||
| use \Psr\Http\Message\ServerRequestInterface as Request; | ||||
| use \Psr\Http\Message\ResponseInterface as Response; | ||||
| use Slim\Views\PhpRenderer; | ||||
|  | ||||
| require_once __DIR__ . '/vendor/autoload.php'; | ||||
| require_once __DIR__ . '/data/config.php'; | ||||
| require_once __DIR__ . '/Grocy.php'; | ||||
| require_once __DIR__ . '/GrocyDbMigrator.php'; | ||||
| require_once __DIR__ . '/GrocyDemoDataGenerator.php'; | ||||
| require_once __DIR__ . '/GrocyLogicStock.php'; | ||||
| require_once __DIR__ . '/GrocyLogicHabits.php'; | ||||
| require_once __DIR__ . '/GrocyLogicBatteries.php'; | ||||
| require_once __DIR__ . '/GrocyPhpHelper.php'; | ||||
|  | ||||
| $app = new \Slim\App; | ||||
|  | ||||
| if (PHP_SAPI !== 'cli') | ||||
| { | ||||
| 	$app = new \Slim\App(new \Slim\Container([ | ||||
| 		'settings' => [ | ||||
| 			'displayErrorDetails' => true, | ||||
| 			'determineRouteBeforeAppMiddleware' => true | ||||
| 		], | ||||
| 	])); | ||||
| 	$container = $app->getContainer(); | ||||
| 	$container['renderer'] = new PhpRenderer('./views'); | ||||
| } | ||||
|  | ||||
| if (PHP_SAPI === 'cli') | ||||
| { | ||||
| 	$app->add(new \pavlakis\cli\CliRequest()); | ||||
| } | ||||
|  | ||||
| if (!Grocy::IsDemoInstallation()) | ||||
| { | ||||
| 	$sessionMiddleware = function(Request $request, Response $response, callable $next) | ||||
| 	{ | ||||
| 		$route = $request->getAttribute('route'); | ||||
| 		$routeName = $route->getName(); | ||||
|  | ||||
| 		if ((!isset($_COOKIE['grocy_session']) || !Grocy::IsValidSession($_COOKIE['grocy_session'])) && $routeName !== 'login') | ||||
| 		{ | ||||
| 			$response = $response->withRedirect('/login'); | ||||
| 		} | ||||
| 		else | ||||
| 		{ | ||||
| 			$response = $next($request, $response); | ||||
| 		} | ||||
|  | ||||
| 		return $response; | ||||
| 	}; | ||||
|  | ||||
| 	$app->add($sessionMiddleware); | ||||
| } | ||||
|  | ||||
| $db = Grocy::GetDbConnection(); | ||||
|  | ||||
| $app->get('/login', function(Request $request, Response $response) | ||||
| { | ||||
| 	return $this->renderer->render($response, '/layout.php', [ | ||||
| 		'title' => 'Login', | ||||
| 		'contentPage' => 'login.php' | ||||
| 	]); | ||||
| })->setName('login'); | ||||
|  | ||||
| $app->post('/login', function(Request $request, Response $response) | ||||
| { | ||||
| 	$postParams = $request->getParsedBody(); | ||||
| 	if (isset($postParams['username']) && isset($postParams['password'])) | ||||
| 	{ | ||||
| 		if ($postParams['username'] === HTTP_USER && $postParams['password'] === HTTP_PASSWORD) | ||||
| 		{ | ||||
| 			$sessionKey = Grocy::CreateSession(); | ||||
| 			setcookie('grocy_session', $sessionKey, time()+2592000); //30 days | ||||
|  | ||||
| 			return $response->withRedirect('/'); | ||||
| 		} | ||||
| 		else | ||||
| 		{ | ||||
| 			return $response->withRedirect('/login?invalid=true'); | ||||
| 		} | ||||
| 	} | ||||
| 	else | ||||
| 	{ | ||||
| 		return $response->withRedirect('/login?invalid=true'); | ||||
| 	} | ||||
| })->setName('login'); | ||||
|  | ||||
| $app->get('/logout', function(Request $request, Response $response) | ||||
| { | ||||
| 	Grocy::RemoveSession($_COOKIE['grocy_session']); | ||||
| 	return $response->withRedirect('/'); | ||||
| }); | ||||
|  | ||||
| $app->get('/', function(Request $request, Response $response) use($db) | ||||
| { | ||||
| 	$db = Grocy::GetDbConnection(true); //For database schema migration | ||||
|  | ||||
| 	return $response->withRedirect('/stockoverview'); | ||||
| }); | ||||
|  | ||||
| $app->get('/stockoverview', function(Request $request, Response $response) use($db) | ||||
| { | ||||
| 	return $this->renderer->render($response, '/layout.php', [ | ||||
| 		'title' => 'Stock overview', | ||||
| 		'contentPage' => 'stockoverview.php', | ||||
| 		'products' => $db->products(), | ||||
| 		'quantityunits' => $db->quantity_units(), | ||||
| 		'currentStock' => GrocyLogicStock::GetCurrentStock(), | ||||
| 		'missingProducts' => GrocyLogicStock::GetMissingProducts() | ||||
| 	]); | ||||
| }); | ||||
|  | ||||
| $app->get('/habitsoverview', function(Request $request, Response $response) use($db) | ||||
| { | ||||
| 	return $this->renderer->render($response, '/layout.php', [ | ||||
| 		'title' => 'Habits overview', | ||||
| 		'contentPage' => 'habitsoverview.php', | ||||
| 		'habits' => $db->habits(), | ||||
| 		'currentHabits' => GrocyLogicHabits::GetCurrentHabits(), | ||||
| 	]); | ||||
| }); | ||||
|  | ||||
| $app->get('/batteriesoverview', function(Request $request, Response $response) use($db) | ||||
| { | ||||
| 	return $this->renderer->render($response, '/layout.php', [ | ||||
| 		'title' => 'Batteries overview', | ||||
| 		'contentPage' => 'batteriesoverview.php', | ||||
| 		'batteries' => $db->batteries(), | ||||
| 		'current' => GrocyLogicBatteries::GetCurrent(), | ||||
| 	]); | ||||
| }); | ||||
|  | ||||
| $app->get('/purchase', function(Request $request, Response $response) use($db) | ||||
| { | ||||
| 	return $this->renderer->render($response, '/layout.php', [ | ||||
| 		'title' => 'Purchase', | ||||
| 		'contentPage' => 'purchase.php', | ||||
| 		'products' => $db->products() | ||||
| 	]); | ||||
| }); | ||||
|  | ||||
| $app->get('/consume', function(Request $request, Response $response) use($db) | ||||
| { | ||||
| 	return $this->renderer->render($response, '/layout.php', [ | ||||
| 		'title' => 'Consume', | ||||
| 		'contentPage' => 'consume.php', | ||||
| 		'products' => $db->products() | ||||
| 	]); | ||||
| }); | ||||
|  | ||||
| $app->get('/inventory', function(Request $request, Response $response) use($db) | ||||
| { | ||||
| 	return $this->renderer->render($response, '/layout.php', [ | ||||
| 		'title' => 'Inventory', | ||||
| 		'contentPage' => 'inventory.php', | ||||
| 		'products' => $db->products() | ||||
| 	]); | ||||
| }); | ||||
|  | ||||
| $app->get('/shoppinglist', function(Request $request, Response $response) use($db) | ||||
| { | ||||
| 	return $this->renderer->render($response, '/layout.php', [ | ||||
| 		'title' => 'Shopping list', | ||||
| 		'contentPage' => 'shoppinglist.php', | ||||
| 		'listItems' => $db->shopping_list(), | ||||
| 		'products' => $db->products(), | ||||
| 		'quantityunits' => $db->quantity_units(), | ||||
| 		'missingProducts' => GrocyLogicStock::GetMissingProducts() | ||||
| 	]); | ||||
| }); | ||||
|  | ||||
| $app->get('/habittracking', function(Request $request, Response $response) use($db) | ||||
| { | ||||
| 	return $this->renderer->render($response, '/layout.php', [ | ||||
| 		'title' => 'Habit tracking', | ||||
| 		'contentPage' => 'habittracking.php', | ||||
| 		'habits' => $db->habits() | ||||
| 	]); | ||||
| }); | ||||
|  | ||||
| $app->get('/batterytracking', function(Request $request, Response $response) use($db) | ||||
| { | ||||
| 	return $this->renderer->render($response, '/layout.php', [ | ||||
| 		'title' => 'Battery tracking', | ||||
| 		'contentPage' => 'batterytracking.php', | ||||
| 		'batteries' => $db->batteries() | ||||
| 	]); | ||||
| }); | ||||
|  | ||||
| $app->get('/products', function(Request $request, Response $response) use($db) | ||||
| { | ||||
| 	return $this->renderer->render($response, '/layout.php', [ | ||||
| 		'title' => 'Products', | ||||
| 		'contentPage' => 'products.php', | ||||
| 		'products' => $db->products(), | ||||
| 		'locations' => $db->locations(), | ||||
| 		'quantityunits' => $db->quantity_units() | ||||
| 	]); | ||||
| }); | ||||
|  | ||||
| $app->get('/locations', function(Request $request, Response $response) use($db) | ||||
| { | ||||
| 	return $this->renderer->render($response, '/layout.php', [ | ||||
| 		'title' => 'Locations', | ||||
| 		'contentPage' => 'locations.php', | ||||
| 		'locations' => $db->locations() | ||||
| 	]); | ||||
| }); | ||||
|  | ||||
| $app->get('/quantityunits', function(Request $request, Response $response) use($db) | ||||
| { | ||||
| 	return $this->renderer->render($response, '/layout.php', [ | ||||
| 		'title' => 'Quantity units', | ||||
| 		'contentPage' => 'quantityunits.php', | ||||
| 		'quantityunits' => $db->quantity_units() | ||||
| 	]); | ||||
| }); | ||||
|  | ||||
| $app->get('/habits', function(Request $request, Response $response) use($db) | ||||
| { | ||||
| 	return $this->renderer->render($response, '/layout.php', [ | ||||
| 		'title' => 'Habits', | ||||
| 		'contentPage' => 'habits.php', | ||||
| 		'habits' => $db->habits() | ||||
| 	]); | ||||
| }); | ||||
|  | ||||
| $app->get('/batteries', function(Request $request, Response $response) use($db) | ||||
| { | ||||
| 	return $this->renderer->render($response, '/layout.php', [ | ||||
| 		'title' => 'Batteries', | ||||
| 		'contentPage' => 'batteries.php', | ||||
| 		'batteries' => $db->batteries() | ||||
| 	]); | ||||
| }); | ||||
|  | ||||
|  | ||||
| $app->get('/product/{productId}', function(Request $request, Response $response, $args) use($db) | ||||
| { | ||||
| 	if ($args['productId'] == 'new') | ||||
| 	{ | ||||
| 		return $this->renderer->render($response, '/layout.php', [ | ||||
| 			'title' => 'Create product', | ||||
| 			'contentPage' => 'productform.php', | ||||
| 			'locations' => $db->locations(), | ||||
| 			'quantityunits' => $db->quantity_units(), | ||||
| 			'mode' => 'create' | ||||
| 		]); | ||||
| 	} | ||||
| 	else | ||||
| 	{ | ||||
| 		return $this->renderer->render($response, '/layout.php', [ | ||||
| 			'title' => 'Edit product', | ||||
| 			'contentPage' => 'productform.php', | ||||
| 			'product' => $db->products($args['productId']), | ||||
| 			'locations' => $db->locations(), | ||||
| 			'quantityunits' => $db->quantity_units(), | ||||
| 			'mode' => 'edit' | ||||
| 		]); | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| $app->get('/location/{locationId}', function(Request $request, Response $response, $args) use($db) | ||||
| { | ||||
| 	if ($args['locationId'] == 'new') | ||||
| 	{ | ||||
| 		return $this->renderer->render($response, '/layout.php', [ | ||||
| 			'title' => 'Create location', | ||||
| 			'contentPage' => 'locationform.php', | ||||
| 			'mode' => 'create' | ||||
| 		]); | ||||
| 	} | ||||
| 	else | ||||
| 	{ | ||||
| 		return $this->renderer->render($response, '/layout.php', [ | ||||
| 			'title' => 'Edit location', | ||||
| 			'contentPage' => 'locationform.php', | ||||
| 			'location' => $db->locations($args['locationId']), | ||||
| 			'mode' => 'edit' | ||||
| 		]); | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| $app->get('/quantityunit/{quantityunitId}', function(Request $request, Response $response, $args) use($db) | ||||
| { | ||||
| 	if ($args['quantityunitId'] == 'new') | ||||
| 	{ | ||||
| 		return $this->renderer->render($response, '/layout.php', [ | ||||
| 			'title' => 'Create quantity unit', | ||||
| 			'contentPage' => 'quantityunitform.php', | ||||
| 			'mode' => 'create' | ||||
| 		]); | ||||
| 	} | ||||
| 	else | ||||
| 	{ | ||||
| 		return $this->renderer->render($response, '/layout.php', [ | ||||
| 			'title' => 'Edit quantity unit', | ||||
| 			'contentPage' => 'quantityunitform.php', | ||||
| 			'quantityunit' => $db->quantity_units($args['quantityunitId']), | ||||
| 			'mode' => 'edit' | ||||
| 		]); | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| $app->get('/habit/{habitId}', function(Request $request, Response $response, $args) use($db) | ||||
| { | ||||
| 	if ($args['habitId'] == 'new') | ||||
| 	{ | ||||
| 		return $this->renderer->render($response, '/layout.php', [ | ||||
| 			'title' => 'Create habit', | ||||
| 			'contentPage' => 'habitform.php', | ||||
| 			'periodTypes' => GrocyPhpHelper::GetClassConstants('GrocyLogicHabits'), | ||||
| 			'mode' => 'create' | ||||
| 		]); | ||||
| 	} | ||||
| 	else | ||||
| 	{ | ||||
| 		return $this->renderer->render($response, '/layout.php', [ | ||||
| 			'title' => 'Edit habit', | ||||
| 			'contentPage' => 'habitform.php', | ||||
| 			'habit' => $db->habits($args['habitId']), | ||||
| 			'periodTypes' => GrocyPhpHelper::GetClassConstants('GrocyLogicHabits'), | ||||
| 			'mode' => 'edit' | ||||
| 		]); | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| $app->get('/battery/{batteryId}', function(Request $request, Response $response, $args) use($db) | ||||
| { | ||||
| 	if ($args['batteryId'] == 'new') | ||||
| 	{ | ||||
| 		return $this->renderer->render($response, '/layout.php', [ | ||||
| 			'title' => 'Create battery', | ||||
| 			'contentPage' => 'batteryform.php', | ||||
| 			'mode' => 'create' | ||||
| 		]); | ||||
| 	} | ||||
| 	else | ||||
| 	{ | ||||
| 		return $this->renderer->render($response, '/layout.php', [ | ||||
| 			'title' => 'Edit battery', | ||||
| 			'contentPage' => 'batteryform.php', | ||||
| 			'battery' => $db->batteries($args['batteryId']), | ||||
| 			'mode' => 'edit' | ||||
| 		]); | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| $app->get('/shoppinglistitem/{itemId}', function(Request $request, Response $response, $args) use($db) | ||||
| { | ||||
| 	if ($args['itemId'] == 'new') | ||||
| 	{ | ||||
| 		return $this->renderer->render($response, '/layout.php', [ | ||||
| 			'title' => 'Add shopping list item', | ||||
| 			'contentPage' => 'shoppinglistform.php', | ||||
| 			'products' => $db->products(), | ||||
| 			'mode' => 'create' | ||||
| 		]); | ||||
| 	} | ||||
| 	else | ||||
| 	{ | ||||
| 		return $this->renderer->render($response, '/layout.php', [ | ||||
| 			'title' => 'Edit shopping list item', | ||||
| 			'contentPage' => 'shoppinglistform.php', | ||||
| 			'listItem' => $db->shopping_list($args['itemId']), | ||||
| 			'products' => $db->products(), | ||||
| 			'mode' => 'edit' | ||||
| 		]); | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| $app->group('/api', function() use($db) | ||||
| { | ||||
| 	$this->get('/get-objects/{entity}', function(Request $request, Response $response, $args) use($db) | ||||
| 	{ | ||||
| 		echo json_encode($db->{$args['entity']}()); | ||||
| 	}); | ||||
|  | ||||
| 	$this->get('/get-object/{entity}/{objectId}', function(Request $request, Response $response, $args) use($db) | ||||
| 	{ | ||||
| 		echo json_encode($db->{$args['entity']}($args['objectId'])); | ||||
| 	}); | ||||
|  | ||||
| 	$this->post('/add-object/{entity}', function(Request $request, Response $response, $args) use($db) | ||||
| 	{ | ||||
| 		$newRow = $db->{$args['entity']}()->createRow($request->getParsedBody()); | ||||
| 		$newRow->save(); | ||||
| 		$success = $newRow->isClean(); | ||||
| 		echo json_encode(array('success' => $success)); | ||||
| 	}); | ||||
|  | ||||
| 	$this->post('/edit-object/{entity}/{objectId}', function(Request $request, Response $response, $args) use($db) | ||||
| 	{ | ||||
| 		$row = $db->{$args['entity']}($args['objectId']); | ||||
| 		$row->update($request->getParsedBody()); | ||||
| 		$success = $row->isClean(); | ||||
| 		echo json_encode(array('success' => $success)); | ||||
| 	}); | ||||
|  | ||||
| 	$this->get('/delete-object/{entity}/{objectId}', function(Request $request, Response $response, $args) use($db) | ||||
| 	{ | ||||
| 		$row = $db->{$args['entity']}($args['objectId']); | ||||
| 		$row->delete(); | ||||
| 		$success = $row->isClean(); | ||||
| 		echo json_encode(array('success' => $success)); | ||||
| 	}); | ||||
|  | ||||
| 	$this->get('/stock/add-product/{productId}/{amount}', function(Request $request, Response $response, $args) | ||||
| 	{ | ||||
| 		$bestBeforeDate = date('Y-m-d'); | ||||
| 		if (isset($request->getQueryParams()['bestbeforedate']) && !empty($request->getQueryParams()['bestbeforedate'])) | ||||
| 		{ | ||||
| 			$bestBeforeDate = $request->getQueryParams()['bestbeforedate']; | ||||
| 		} | ||||
|  | ||||
| 		$transactionType = GrocyLogicStock::TRANSACTION_TYPE_PURCHASE; | ||||
| 		if (isset($request->getQueryParams()['transactiontype']) && !empty($request->getQueryParams()['transactiontype'])) | ||||
| 		{ | ||||
| 			$transactionType = $request->getQueryParams()['transactiontype']; | ||||
| 		} | ||||
|  | ||||
| 		echo json_encode(array('success' => GrocyLogicStock::AddProduct($args['productId'], $args['amount'], $bestBeforeDate, $transactionType))); | ||||
| 	}); | ||||
|  | ||||
| 	$this->get('/stock/consume-product/{productId}/{amount}', function(Request $request, Response $response, $args) | ||||
| 	{ | ||||
| 		$spoiled = false; | ||||
| 		if (isset($request->getQueryParams()['spoiled']) && !empty($request->getQueryParams()['spoiled']) && $request->getQueryParams()['spoiled'] == '1') | ||||
| 		{ | ||||
| 			$spoiled = true; | ||||
| 		} | ||||
|  | ||||
| 		$transactionType = GrocyLogicStock::TRANSACTION_TYPE_CONSUME; | ||||
| 		if (isset($request->getQueryParams()['transactiontype']) && !empty($request->getQueryParams()['transactiontype'])) | ||||
| 		{ | ||||
| 			$transactionType = $request->getQueryParams()['transactiontype']; | ||||
| 		} | ||||
|  | ||||
| 		echo json_encode(array('success' => GrocyLogicStock::ConsumeProduct($args['productId'], $args['amount'], $spoiled, $transactionType))); | ||||
| 	}); | ||||
|  | ||||
| 	$this->get('/stock/inventory-product/{productId}/{newAmount}', function(Request $request, Response $response, $args) | ||||
| 	{ | ||||
| 		$bestBeforeDate = date('Y-m-d'); | ||||
| 		if (isset($request->getQueryParams()['bestbeforedate']) && !empty($request->getQueryParams()['bestbeforedate'])) | ||||
| 		{ | ||||
| 			$bestBeforeDate = $request->getQueryParams()['bestbeforedate']; | ||||
| 		} | ||||
|  | ||||
| 		echo json_encode(array('success' => GrocyLogicStock::InventoryProduct($args['productId'], $args['newAmount'], $bestBeforeDate))); | ||||
| 	}); | ||||
|  | ||||
| 	$this->get('/stock/get-product-details/{productId}', function(Request $request, Response $response, $args) | ||||
| 	{ | ||||
| 		echo json_encode(GrocyLogicStock::GetProductDetails($args['productId'])); | ||||
| 	}); | ||||
|  | ||||
| 	$this->get('/stock/get-current-stock', function(Request $request, Response $response) | ||||
| 	{ | ||||
| 		echo json_encode(GrocyLogicStock::GetCurrentStock()); | ||||
| 	}); | ||||
|  | ||||
| 	$this->get('/stock/add-missing-products-to-shoppinglist', function(Request $request, Response $response) | ||||
| 	{ | ||||
| 		GrocyLogicStock::AddMissingProductsToShoppingList(); | ||||
| 		echo json_encode(array('success' => true)); | ||||
| 	}); | ||||
|  | ||||
| 	$this->get('/habits/track-habit/{habitId}', function(Request $request, Response $response, $args) | ||||
| 	{ | ||||
| 		$trackedTime = date('Y-m-d H:i:s'); | ||||
| 		if (isset($request->getQueryParams()['tracked_time']) && !empty($request->getQueryParams()['tracked_time'])) | ||||
| 		{ | ||||
| 			$trackedTime = $request->getQueryParams()['tracked_time']; | ||||
| 		} | ||||
|  | ||||
| 		echo json_encode(array('success' => GrocyLogicHabits::TrackHabit($args['habitId'], $trackedTime))); | ||||
| 	}); | ||||
|  | ||||
| 	$this->get('/habits/get-habit-details/{habitId}', function(Request $request, Response $response, $args) | ||||
| 	{ | ||||
| 		echo json_encode(GrocyLogicHabits::GetHabitDetails($args['habitId'])); | ||||
| 	}); | ||||
|  | ||||
| 	$this->get('/batteries/track-charge-cycle/{batteryId}', function(Request $request, Response $response, $args) | ||||
| 	{ | ||||
| 		$trackedTime = date('Y-m-d H:i:s'); | ||||
| 		if (isset($request->getQueryParams()['tracked_time']) && !empty($request->getQueryParams()['tracked_time'])) | ||||
| 		{ | ||||
| 			$trackedTime = $request->getQueryParams()['tracked_time']; | ||||
| 		} | ||||
|  | ||||
| 		echo json_encode(array('success' => GrocyLogicBatteries::TrackChargeCycle($args['batteryId'], $trackedTime))); | ||||
| 	}); | ||||
|  | ||||
| 	$this->get('/batteries/get-battery-details/{batteryId}', function(Request $request, Response $response, $args) | ||||
| 	{ | ||||
| 		echo json_encode(GrocyLogicBatteries::GetBatteryDetails($args['batteryId'])); | ||||
| 	}); | ||||
| })->add(function($request, $response, $next) | ||||
| { | ||||
| 	$response = $next($request, $response); | ||||
| 	return $response->withHeader('Content-Type', 'application/json'); | ||||
| }); | ||||
|  | ||||
| $app->group('/cli', function() | ||||
| { | ||||
| 	$this->get('/recreatedemo', function(Request $request, Response $response) | ||||
| 	{ | ||||
| 		if (Grocy::IsDemoInstallation()) | ||||
| 		{ | ||||
| 			GrocyDemoDataGenerator::RecreateDemo(); | ||||
| 		} | ||||
| 	}); | ||||
| })->add(function($request, $response, $next) | ||||
| { | ||||
| 	$response = $next($request, $response); | ||||
|  | ||||
| 	if (PHP_SAPI !== 'cli') | ||||
| 	{ | ||||
| 		echo 'Please call this only from CLI'; | ||||
| 		return $response->withHeader('Content-Type', 'text/plain')->withStatus(400); | ||||
| 	} | ||||
|  | ||||
| 	return $response->withHeader('Content-Type', 'text/plain'); | ||||
| }); | ||||
|  | ||||
| $app->run(); | ||||
							
								
								
									
										159
									
								
								localization/de.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,159 @@ | ||||
| <?php | ||||
|  | ||||
| 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', | ||||
| 	'Product' => 'Produkt', | ||||
| 	'Amount' => 'Menge', | ||||
| 	'Next best before date' => 'Nächstes MHD', | ||||
| 	'Logout' => 'Abmelden', | ||||
| 	'Habits overview' => 'Gewohnheiten', | ||||
| 	'Batteries overview' => 'Batterien', | ||||
| 	'Purchase' => 'Einkauf', | ||||
| 	'Consume' => 'Verbrauch', | ||||
| 	'Inventory' => 'Inventur', | ||||
| 	'Shopping list' => 'Einkaufszettel', | ||||
| 	'Habit tracking' => 'Gewohnheit-Ausführung', | ||||
| 	'Battery tracking' => 'Batterie-Ladzyklus', | ||||
| 	'Products' => 'Produkte', | ||||
| 	'Locations' => 'Standorte', | ||||
| 	'Quantity units' => 'Mengeneinheiten', | ||||
| 	'Habits' => 'Gewohnheiten', | ||||
| 	'Batteries' => 'Batterien', | ||||
| 	'Habit' => 'Gewohnheit', | ||||
| 	'Next estimated tracking' => 'Nächste geplante Ausführung', | ||||
| 	'Last tracked' => 'Zuletzt ausgeführt', | ||||
| 	'Battery' => 'Batterie', | ||||
| 	'Last charged' => 'Zuletzt geladen', | ||||
| 	'Next planned charge cycle' => 'Nächster geplanter Ladezyklus', | ||||
| 	'Best before' => 'MHD', | ||||
| 	'OK' => 'OK', | ||||
| 	'Product overview' => 'Produktübersicht', | ||||
| 	'Stock quantity unit' => 'Mengeneinheit Bestand', | ||||
| 	'Stock amount' => 'Bestand', | ||||
| 	'Last purchased' => 'Zuletzt gekauft', | ||||
| 	'Last used' => 'Zuletzt benutzt', | ||||
| 	'Spoiled' => 'Verdorben', | ||||
| 	'Barcode lookup is disabled' => 'Barcode-Suche ist deaktiviert', | ||||
| 	'will be added to the list of barcodes for the selected product on submit' => 'wird der Liste der Barcodes für das ausgewählte Produkt beim Speichern hinzugefügt', | ||||
| 	'New amount' => 'Neue Menge', | ||||
| 	'Note' => 'Notiz', | ||||
| 	'Tracked time' => 'Ausführungszeit', | ||||
| 	'Habit overview' => 'Gewohnheit Übersicht', | ||||
| 	'Tracked count' => 'Ausführungsanzahl', | ||||
| 	'Battery overview' => 'Batterie Übersicht', | ||||
| 	'Charge cycles count' => 'Ladezyklen', | ||||
| 	'Create shopping list item' => 'Einkaufszettel Eintrag erstellen', | ||||
| 	'Edit shopping list item' => 'Einkaufszettel Eintrag bearbeiten', | ||||
| 	'#1 units were automatically added and will apply in addition to the amount entered here' => '#1 Einheiten wurden automatisch hinzugefügt und gelten zusätzlich der hier eingegebenen Menge', | ||||
| 	'Save' => 'Speichern', | ||||
| 	'Add' => 'Hinzufügen', | ||||
| 	'Name' => 'Name', | ||||
| 	'Location' => 'Standort', | ||||
| 	'Min. stock amount' => 'Mindestbestand', | ||||
| 	'QU purchase' => 'ME Einkauf', | ||||
| 	'QU stock' => 'ME Bestand', | ||||
| 	'QU factor' => 'ME-Faktor', | ||||
| 	'Description' => 'Beschreibung', | ||||
| 	'Create product' => 'Produkt erstellen', | ||||
| 	'Barcode(s)' => 'Barcode(s)', | ||||
| 	'Minimum stock amount' => 'Mindestbestand', | ||||
| 	'Default best before days' => 'Standard Haltbarkeit in Tagen', | ||||
| 	'Quantity unit purchase' => 'Mengeneinheit Einkauf', | ||||
| 	'Quantity unit stock' => 'Mengeneinheit Bestand', | ||||
| 	'Factor purchase to stock quantity unit' => 'Faktor Mengeneinheit Einkauf zu Mengeneinheit Bestand', | ||||
| 	'Create location' => 'Standort erstellen', | ||||
| 	'Create quantity unit' => 'Mengeneinheit erstellen', | ||||
| 	'Period type' => 'Periodentyp', | ||||
| 	'Period days' => 'Tage/Periode', | ||||
| 	'Create habit' => 'Gewohnheit erstellen', | ||||
| 	'Used in' => 'Benutzt in', | ||||
| 	'Create battery' => 'Batterie erstellen', | ||||
| 	'Edit battery' => 'Batterie bearbeiten', | ||||
| 	'Edit habit' => 'Gewohnheit bearbeiten', | ||||
| 	'Edit quantity unit' => 'Mengeneinheit bearbeiten', | ||||
| 	'Edit product' => 'Produkt bearbeiten', | ||||
| 	'Edit location' => 'Standort bearbeiten', | ||||
| 	'Record data' => 'Daten erfassen', | ||||
| 	'Manage master data' => 'Stammdaten verwalten', | ||||
| 	'This will apply to added products' => 'Dies gilt für hinzugefügte Produkte', | ||||
| 	'never' => 'nie', | ||||
| 	'Add products that are below defined min. stock amount' => 'Produkte unter Mindestbestand hinzufügen', | ||||
| 	'For purchases this amount of days will be added to today for the best before date suggestion' => 'Bei Einkäufen wird hierauf basierend das MHD vorausgefüllt', | ||||
| 	'This means 1 #1 purchased will be converted into #2 #3 in stock' => 'Das bedeutet 1 #1 im Einkauf entsprechen #2 #3 im Bestand', | ||||
| 	'Login' => 'Anmelden', | ||||
| 	'Username' => 'Benutzername', | ||||
| 	'Password' => 'Passwort', | ||||
| 	'Invalid credentials, please try again' => 'Ungültige Zugangsdaten, bitte versuche es erneut', | ||||
| 	'Are you sure to delete battery "#1"?' => 'Battery "#1" wirklich löschen?', | ||||
| 	'Yes' => 'Ja', | ||||
| 	'No' => 'Nein', | ||||
| 	'Are you sure to delete habit "#1"?' => 'Gewohnheit "#1" wirklich löschen?', | ||||
| 	'"#1" could not be resolved to a product, how do you want to proceed?' => '"#1" konnte nicht zu einem Produkt aufgelöst werden, wie möchtest du weiter machen?', | ||||
| 	'Create or assign product' => 'Produkt erstellen oder verknüpfen', | ||||
| 	'Cancel' => 'Abbrechen', | ||||
| 	'Add as new product' => 'Als neues Produkt hinzufügen', | ||||
| 	'Add as barcode to existing product' => 'Barcode vorhandenem Produkt zuweisen', | ||||
| 	'Add as new product and prefill barcode' => 'Neues Produkt erstellen und Barcode vorbelegen', | ||||
| 	'Are you sure to delete quantity unit "#1"?' => 'Mengeneinheit "#1" wirklich löschen?', | ||||
| 	'Are you sure to delete product "#1"?' => 'Produkt "#1" wirklich löschen?', | ||||
| 	'Are you sure to delete location "#1"?' => 'Standort "#1" wirklich löschen?', | ||||
| 	'Manage API keys' => 'API-Keys verwalten', | ||||
|     'REST API & data model documentation' => 'REST-API & Datenmodell Dokumentation', | ||||
|     'API keys' => 'API-Keys', | ||||
|     'Create new API key' => 'Neuen API-Key erstellen', | ||||
|     'API key' => 'API-Key', | ||||
|     'Expires' => 'Läuft ab', | ||||
|     'Created' => 'Erstellt', | ||||
| 	'This product is not in stock' => 'Dieses Produkt ist nicht vorrätig', | ||||
| 	'This means #1 will be added to stock' => 'Das bedeutet #1 wird dem Bestand hinzugefügt', | ||||
| 	'This means #1 will be removed from stock' => 'Das bedeutet #1 wird aus dem Bestand entfernt', | ||||
| 	'This means it is estimated that a new execution of this habit is tracked #1 days after the last was tracked' => 'Das bedeutet, dass eine erneute Ausführung der Gewohnheit #1 Tage nach der letzten Ausführung geplant wird', | ||||
| 	 | ||||
| 	//Constants | ||||
| 	'manually' => 'Manuell', | ||||
| 	'dynamic-regular' => 'Dynamisch regelmäßig', | ||||
| 	 | ||||
| 	//Technical component translations | ||||
| 	'timeago_locale' => 'de', | ||||
| 	'timeago_nan' => 'vor NaN Jahren', | ||||
| 	'moment_locale' => 'de', | ||||
| 	'bootstrap_datepicker_locale' => 'de', | ||||
| 	'datatables_localization' => '{"sEmptyTable":"Keine Daten in der Tabelle vorhanden","sInfo":"_START_ bis _END_ von _TOTAL_ Einträgen","sInfoEmpty":"Keine Daten vorhanden","sInfoFiltered":"(gefiltert von _MAX_ Einträgen)","sInfoPostFix":"","sInfoThousands":".","sLengthMenu":"_MENU_ Einträge anzeigen","sLoadingRecords":"Wird geladen ..","sProcessing":"Bitte warten ..","sSearch":"Suchen","sZeroRecords":"Keine Einträge vorhanden","oPaginate":{"sFirst":"Erste","sPrevious":"Zurück","sNext":"Nächste","sLast":"Letzte"},"oAria":{"sSortAscending":": aktivieren, um Spalte aufsteigend zu sortieren","sSortDescending":": aktivieren, um Spalte absteigend zu sortieren"},"select":{"rows":{"0":"Zum Auswählen auf eine Zeile klicken","1":"1 Zeile ausgewählt","_":"%d Zeilen ausgewählt"}},"buttons":{"print":"Drucken","colvis":"Spalten","copy":"Kopieren","copyTitle":"In Zwischenablage kopieren","copyKeys":"Taste <i>ctrl</i> oder <i>⌘</i> + <i>C</i> um Tabelle<br>in Zwischenspeicher zu kopieren.<br><br>Um abzubrechen die Nachricht anklicken oder Escape drücken.","copySuccess":{"1":"1 Spalte kopiert","_":"%d Spalten kopiert"}}}', | ||||
| 	 | ||||
| 	//Demo data | ||||
| 	'Cookies' => 'Cookies', | ||||
| 	'Chocolate' => 'Schokolade', | ||||
| 	'Pantry' => 'Vorratskammer', | ||||
| 	'Candy cupboard' => 'Süßigkeitenschrank', | ||||
| 	'Tinned food cupboard' => 'Konservenschrank', | ||||
| 	'Fridge' => 'Kühlschrank', | ||||
| 	'Piece' => 'Stück', | ||||
| 	'Pack' => 'Packung', | ||||
| 	'Glass' => 'Glas', | ||||
| 	'Tin' => 'Dose', | ||||
| 	'Can' => 'Becher', | ||||
| 	'Bunch' => 'Bund', | ||||
| 	'Gummy bears' => 'Gummibärchen', | ||||
| 	'Crisps' => 'Chips', | ||||
| 	'Eggs' => 'Eier', | ||||
| 	'Noodles' => 'Nudeln', | ||||
| 	'Pickles' => 'Essiggurken', | ||||
| 	'Gulash soup' => 'Gulaschsuppe', | ||||
| 	'Yogurt' => 'Joghurt', | ||||
| 	'Cheese' => 'Käse', | ||||
| 	'Cold cuts' => 'Aufschnitt', | ||||
| 	'Paprika' => 'Paprika', | ||||
| 	'Cucumber' => 'Gurke', | ||||
| 	'Radish' => 'Radieschen', | ||||
| 	'Tomato' => 'Tomaten', | ||||
| 	'Changed towels in the bathroom' => 'Handtücher im Bad gewechselt', | ||||
| 	'Cleaned the kitchen floor' => 'Küchenboden gewischt', | ||||
| 	'Warranty ends' => 'Garantie endet', | ||||
| 	'TV remote control' => 'TV Fernbedienung', | ||||
| 	'Alarm clock' => 'Wecker', | ||||
| 	'Heat remote control' => 'Fernbedienung Heizung' | ||||
| ); | ||||
							
								
								
									
										14
									
								
								localization/en.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,14 @@ | ||||
| <?php | ||||
|  | ||||
| return array( | ||||
| 	//Constants | ||||
| 	'manually' => 'Manually', | ||||
| 	'dynamic-regular' => 'Dynamic regular', | ||||
| 	 | ||||
| 	//Technical component translations | ||||
| 	'timeago_locale' => 'en', | ||||
| 	'timeago_nan' => 'NaN years ago', | ||||
| 	'moment_locale' => '', | ||||
| 	'bootstrap_datepicker_locale' => '', | ||||
| 	'datatables_localization' => '{"sEmptyTable":"No data available in table","sInfo":"Showing _START_ to _END_ of _TOTAL_ entries","sInfoEmpty":"Showing 0 to 0 of 0 entries","sInfoFiltered":"(filtered from _MAX_ total entries)","sInfoPostFix":"","sInfoThousands":",","sLengthMenu":"Show _MENU_ entries","sLoadingRecords":"Loading...","sProcessing":"Processing...","sSearch":"Search:","sZeroRecords":"No matching records found","oPaginate":{"sFirst":"First","sLast":"Last","sNext":"Next","sPrevious":"Previous"},"oAria":{"sSortAscending":": activate to sort column ascending","sSortDescending":": activate to sort column descending"}}' | ||||
| ); | ||||
							
								
								
									
										58
									
								
								middleware/ApiKeyAuthMiddleware.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,58 @@ | ||||
| <?php | ||||
|  | ||||
| namespace Grocy\Middleware; | ||||
|  | ||||
| use \Grocy\Services\SessionService; | ||||
| use \Grocy\Services\ApiKeyService; | ||||
|  | ||||
| class ApiKeyAuthMiddleware extends BaseMiddleware | ||||
| { | ||||
| 	public function __construct(\Slim\Container $container, string $sessionCookieName, string $apiKeyHeaderName) | ||||
| 	{ | ||||
| 		parent::__construct($container); | ||||
| 		$this->SessionCookieName = $sessionCookieName; | ||||
| 		$this->ApiKeyHeaderName = $apiKeyHeaderName; | ||||
| 	} | ||||
|  | ||||
| 	protected $SessionCookieName; | ||||
| 	protected $ApiKeyHeaderName; | ||||
|  | ||||
| 	public function __invoke(\Slim\Http\Request $request, \Slim\Http\Response $response, callable $next) | ||||
| 	{ | ||||
| 		$route = $request->getAttribute('route'); | ||||
| 		$routeName = $route->getName(); | ||||
|  | ||||
| 		if ($this->ApplicationService->IsDemoInstallation()) | ||||
| 		{ | ||||
| 			$response = $next($request, $response); | ||||
| 		} | ||||
| 		else | ||||
| 		{ | ||||
| 			$validSession = true; | ||||
| 			$validApiKey = true; | ||||
| 			 | ||||
| 			$sessionService = new SessionService(); | ||||
| 			if (!isset($_COOKIE[$this->SessionCookieName]) || !$sessionService->IsValidSession($_COOKIE[$this->SessionCookieName])) | ||||
| 			{ | ||||
| 				$validSession = false; | ||||
| 			} | ||||
|  | ||||
| 			$apiKeyService = new ApiKeyService(); | ||||
| 			if (!$request->hasHeader($this->ApiKeyHeaderName) || !$apiKeyService->IsValidApiKey($request->getHeaderLine($this->ApiKeyHeaderName))) | ||||
| 			{ | ||||
| 				$validApiKey = false; | ||||
| 			} | ||||
|  | ||||
| 			if (!$validSession && !$validApiKey) | ||||
| 			{ | ||||
| 				$response = $response->withStatus(401); | ||||
| 			} | ||||
| 			else | ||||
| 			{ | ||||
| 				$response = $next($request, $response); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		return $response; | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										17
									
								
								middleware/BaseMiddleware.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,17 @@ | ||||
| <?php | ||||
|  | ||||
| namespace Grocy\Middleware; | ||||
|  | ||||
| use \Grocy\Services\ApplicationService; | ||||
|  | ||||
| class BaseMiddleware | ||||
| { | ||||
| 	public function __construct(\Slim\Container $container) | ||||
| 	{ | ||||
| 		$this->AppContainer = $container; | ||||
| 		$this->ApplicationService = new ApplicationService(); | ||||
| 	} | ||||
|  | ||||
| 	protected $AppContainer; | ||||
| 	protected $ApplicationService; | ||||
| } | ||||
							
								
								
									
										20
									
								
								middleware/CliMiddleware.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,20 @@ | ||||
| <?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'); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										12
									
								
								middleware/JsonMiddleware.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,12 @@ | ||||
| <?php | ||||
|  | ||||
| namespace Grocy\Middleware; | ||||
|  | ||||
| class JsonMiddleware extends BaseMiddleware | ||||
| { | ||||
| 	public function __invoke(\Slim\Http\Request $request, \Slim\Http\Response $response, callable $next) | ||||
| 	{ | ||||
| 		$response = $next($request, $response); | ||||
| 		return $response->withHeader('Content-Type', 'application/json'); | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										41
									
								
								middleware/SessionAuthMiddleware.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,41 @@ | ||||
| <?php | ||||
|  | ||||
| namespace Grocy\Middleware; | ||||
|  | ||||
| use \Grocy\Services\SessionService; | ||||
|  | ||||
| class SessionAuthMiddleware extends BaseMiddleware | ||||
| { | ||||
| 	public function __construct(\Slim\Container $container, string $sessionCookieName) | ||||
| 	{ | ||||
| 		parent::__construct($container); | ||||
| 		$this->SessionCookieName = $sessionCookieName; | ||||
| 	} | ||||
|  | ||||
| 	protected $SessionCookieName; | ||||
|  | ||||
| 	public function __invoke(\Slim\Http\Request $request, \Slim\Http\Response $response, callable $next) | ||||
| 	{ | ||||
| 		$route = $request->getAttribute('route'); | ||||
| 		$routeName = $route->getName(); | ||||
|  | ||||
| 		if ($routeName === 'root' || $this->ApplicationService->IsDemoInstallation()) | ||||
| 		{ | ||||
| 			$response = $next($request, $response); | ||||
| 		} | ||||
| 		else | ||||
| 		{ | ||||
| 			$sessionService = new SessionService(); | ||||
| 			if ((!isset($_COOKIE[$this->SessionCookieName]) || !$sessionService->IsValidSession($_COOKIE[$this->SessionCookieName])) && $routeName !== 'login') | ||||
| 			{ | ||||
| 				$response = $response->withRedirect($this->AppContainer->UrlManager->ConstructUrl('/login')); | ||||
| 			} | ||||
| 			else | ||||
| 			{ | ||||
| 				$response = $next($request, $response); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		return $response; | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										13
									
								
								migrations/0001.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,13 @@ | ||||
| CREATE TABLE products ( | ||||
| 	id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, | ||||
| 	name TEXT NOT NULL UNIQUE, | ||||
| 	description TEXT, | ||||
| 	location_id INTEGER NOT NULL, | ||||
| 	qu_id_purchase INTEGER NOT NULL, | ||||
| 	qu_id_stock INTEGER NOT NULL, | ||||
| 	qu_factor_purchase_to_stock REAL NOT NULL, | ||||
| 	barcode TEXT, | ||||
| 	min_stock_amount INTEGER NOT NULL DEFAULT 0, | ||||
| 	default_best_before_days INTEGER NOT NULL DEFAULT 0, | ||||
| 	row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime')) | ||||
| ) | ||||
							
								
								
									
										6
									
								
								migrations/0002.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,6 @@ | ||||
| CREATE TABLE locations ( | ||||
| 	id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, | ||||
| 	name TEXT NOT NULL UNIQUE, | ||||
| 	description TEXT, | ||||
| 	row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime')) | ||||
| ) | ||||
							
								
								
									
										6
									
								
								migrations/0003.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,6 @@ | ||||
| CREATE TABLE quantity_units ( | ||||
| 	id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, | ||||
| 	name TEXT NOT NULL UNIQUE, | ||||
| 	description TEXT, | ||||
| 	row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime')) | ||||
| ) | ||||
							
								
								
									
										9
									
								
								migrations/0004.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,9 @@ | ||||
| CREATE TABLE stock ( | ||||
| 	id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, | ||||
| 	product_id INTEGER NOT NULL, | ||||
| 	amount INTEGER NOT NULL, | ||||
| 	best_before_date DATE, | ||||
| 	purchased_date DATE DEFAULT (datetime('now', 'localtime')), | ||||
| 	stock_id TEXT NOT NULL, | ||||
| 	row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime')) | ||||
| ) | ||||
							
								
								
									
										12
									
								
								migrations/0005.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,12 @@ | ||||
| CREATE TABLE stock_log ( | ||||
| 	id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, | ||||
| 	product_id INTEGER NOT NULL, | ||||
| 	amount INTEGER NOT NULL, | ||||
| 	best_before_date DATE, | ||||
| 	purchased_date DATE, | ||||
| 	used_date DATE, | ||||
| 	spoiled INTEGER NOT NULL DEFAULT 0, | ||||
| 	stock_id TEXT NOT NULL, | ||||
| 	transaction_type TEXT NOT NULL, | ||||
| 	row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime')) | ||||
| ) | ||||
							
								
								
									
										19
									
								
								migrations/0006.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,19 @@ | ||||
| INSERT INTO locations | ||||
| 	(name, description) | ||||
| VALUES | ||||
| 	('DefaultLocation', 'This is the first default location, edit or delete it'); | ||||
|  | ||||
| INSERT INTO quantity_units | ||||
| 	(name, description) | ||||
| VALUES | ||||
| 	('DefaultQuantityUnit', 'This is the first default quantity unit, edit or delete it'); | ||||
|  | ||||
| INSERT INTO products | ||||
| 	(name, description, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) | ||||
| VALUES | ||||
| 	('DefaultProduct1', 'This is the first default product, edit or delete it', 1, 1, 1, 1); | ||||
|  | ||||
| INSERT INTO products | ||||
| 	(name, description, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) | ||||
| VALUES | ||||
| 	('DefaultProduct2', 'This is the second default product, edit or delete it', 1, 1, 1, 1); | ||||
							
								
								
									
										9
									
								
								migrations/0007.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,9 @@ | ||||
| CREATE VIEW stock_missing_products | ||||
| AS | ||||
| SELECT p.id, MAX(p.name) AS name, p.min_stock_amount - IFNULL(SUM(s.amount), 0) AS amount_missing | ||||
| FROM products p | ||||
| LEFT JOIN stock s | ||||
| 	ON p.id = s.product_id | ||||
| WHERE p.min_stock_amount != 0 | ||||
| GROUP BY p.id | ||||
| HAVING IFNULL(SUM(s.amount), 0) < p.min_stock_amount | ||||
							
								
								
									
										6
									
								
								migrations/0008.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,6 @@ | ||||
| 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 | ||||
| ORDER BY MIN(best_before_date) ASC | ||||
							
								
								
									
										7
									
								
								migrations/0009.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,7 @@ | ||||
| CREATE TABLE shopping_list ( | ||||
| 	id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, | ||||
| 	product_id INTEGER NOT NULL UNIQUE, | ||||
| 	amount INTEGER NOT NULL DEFAULT 0, | ||||
| 	amount_autoadded INTEGER NOT NULL DEFAULT 0, | ||||
| 	row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime')) | ||||
| ) | ||||
							
								
								
									
										8
									
								
								migrations/0010.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,8 @@ | ||||
| CREATE TABLE habits ( | ||||
| 	id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, | ||||
| 	name TEXT NOT NULL UNIQUE, | ||||
| 	description TEXT, | ||||
| 	period_type TEXT NOT NULL, | ||||
| 	period_days INTEGER, | ||||
| 	row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime')) | ||||
| ) | ||||
							
								
								
									
										6
									
								
								migrations/0011.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,6 @@ | ||||
| CREATE TABLE habits_log ( | ||||
| 	id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, | ||||
| 	habit_id INTEGER NOT NULL, | ||||
| 	tracked_time DATETIME, | ||||
| 	row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime')) | ||||
| ) | ||||
							
								
								
									
										6
									
								
								migrations/0012.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,6 @@ | ||||
| CREATE VIEW habits_current | ||||
| AS | ||||
| SELECT habit_id, MAX(tracked_time) AS last_tracked_time | ||||
| FROM habits_log | ||||
| GROUP BY habit_id | ||||
| ORDER BY MAX(tracked_time) DESC | ||||
							
								
								
									
										8
									
								
								migrations/0013.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,8 @@ | ||||
| CREATE TABLE batteries ( | ||||
| 	id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, | ||||
| 	name TEXT NOT NULL UNIQUE, | ||||
| 	description TEXT, | ||||
| 	used_in TEXT, | ||||
| 	charge_interval_days INTEGER NOT NULL DEFAULT 0, | ||||
| 	row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime')) | ||||
| ) | ||||
							
								
								
									
										6
									
								
								migrations/0014.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,6 @@ | ||||
| CREATE TABLE battery_charge_cycles ( | ||||
| 	id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, | ||||
| 	battery_id TEXT NOT NULL, | ||||
| 	tracked_time DATETIME, | ||||
| 	row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime')) | ||||
| ) | ||||
							
								
								
									
										6
									
								
								migrations/0015.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,6 @@ | ||||
| CREATE VIEW batteries_current | ||||
| 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 | ||||
							
								
								
									
										1
									
								
								migrations/0016.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | ||||
| ALTER TABLE shopping_list RENAME TO shopping_list_old | ||||
							
								
								
									
										8
									
								
								migrations/0017.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,8 @@ | ||||
| CREATE TABLE shopping_list ( | ||||
| 	id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, | ||||
| 	product_id INTEGER, | ||||
| 	note TEXT, | ||||
| 	amount INTEGER NOT NULL DEFAULT 0, | ||||
| 	amount_autoadded INTEGER NOT NULL DEFAULT 0, | ||||
| 	row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime')) | ||||
| ) | ||||
							
								
								
									
										4
									
								
								migrations/0018.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,4 @@ | ||||
| INSERT INTO shopping_list | ||||
| 	(product_id, amount, amount_autoadded, row_created_timestamp) | ||||
| SELECT product_id, amount, amount_autoadded, row_created_timestamp | ||||
| FROM shopping_list_old | ||||
							
								
								
									
										1
									
								
								migrations/0019.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | ||||
| DROP TABLE shopping_list_old | ||||
							
								
								
									
										6
									
								
								migrations/0020.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,6 @@ | ||||
| CREATE TABLE sessions ( | ||||
| 	id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, | ||||
| 	session_key TEXT NOT NULL UNIQUE, | ||||
| 	expires DATETIME, | ||||
| 	row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime')) | ||||
| ) | ||||
							
								
								
									
										11
									
								
								migrations/0021.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,11 @@ | ||||
| DELETE FROM locations | ||||
| WHERE name = 'DefaultLocation'; | ||||
|  | ||||
| DELETE FROM quantity_units | ||||
| WHERE name = 'DefaultQuantityUnit'; | ||||
|  | ||||
| DELETE FROM products | ||||
| WHERE name = 'DefaultProduct1'; | ||||
|  | ||||
| DELETE FROM products | ||||
| WHERE name = 'DefaultProduct2'; | ||||
							
								
								
									
										7
									
								
								migrations/0022.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,7 @@ | ||||
| CREATE TABLE api_keys ( | ||||
| 	id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, | ||||
| 	api_key TEXT NOT NULL UNIQUE, | ||||
| 	expires DATETIME, | ||||
| 	last_used DATETIME, | ||||
| 	row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime')) | ||||
| ) | ||||
							
								
								
									
										1
									
								
								migrations/0023.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | ||||
| DELETE FROM sessions | ||||
							
								
								
									
										2
									
								
								migrations/0024.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,2 @@ | ||||
| ALTER TABLE sessions | ||||
| ADD COLUMN last_used DATETIME | ||||
| Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB | 
| Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB | 
| Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB | 
| Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB | 
| @@ -22,8 +22,8 @@ | ||||
| 		padding: 20px; | ||||
| 		overflow-x: hidden; | ||||
| 		overflow-y: auto; | ||||
| 		background-color: #f5f5f5; | ||||
| 		border-right: 1px solid #5e5e5e; | ||||
| 		background-color: #e5e5e5; | ||||
| 		border-right: 2px solid #d6d6d6; | ||||
| 		min-width: 220px; | ||||
| 		max-width: 260px; | ||||
| 	} | ||||
| @@ -42,13 +42,29 @@ | ||||
| .nav-sidebar > li > a { | ||||
| 	padding-right: 20px; | ||||
| 	padding-left: 20px; | ||||
| 	transition: all 0.3s; | ||||
| } | ||||
| 
 | ||||
| .nav-sidebar > li > a:hover { | ||||
| 	box-shadow: inset 5px 0 0 #337ab7; | ||||
| 	transition: all 0.3s; | ||||
| } | ||||
| 
 | ||||
| .nav-sidebar > li > a:focus { | ||||
| 	box-shadow: inset 5px 0 0 #ab2230; | ||||
| 	transition: all 0.3s; | ||||
| } | ||||
| 
 | ||||
| .nav-sidebar > .active > a, | ||||
| .nav-sidebar > .active > a:hover, | ||||
| .nav-sidebar > .active > a:focus { | ||||
| 	color: #fff; | ||||
| 	background-color: #5e5e5e; | ||||
| 	background-color: #d6d6d6; | ||||
| 	box-shadow: inset 5px 0 0 #ab2230; | ||||
| 	transition: all 0.3s; | ||||
| } | ||||
| 
 | ||||
| .navbar-default { | ||||
| 	background-color: #e5e5e5; | ||||
| } | ||||
| 
 | ||||
| .main { | ||||
| @@ -67,36 +83,41 @@ | ||||
| } | ||||
| 
 | ||||
| .nav-copyright { | ||||
| 	color: #b3b3b1; | ||||
| 	color: #a7a7a7; | ||||
| 	font-size: 11px; | ||||
| 	text-align: center; | ||||
| 	font-family: 'Arial', sans-serif; | ||||
| } | ||||
| 
 | ||||
| .discrete-link { | ||||
| 	color: inherit; | ||||
| 	color: inherit !important; | ||||
| 	transition: all 0.3s !important; | ||||
| } | ||||
| 
 | ||||
| a.discrete-link:hover { | ||||
| 	color: #5cb85c; | ||||
| 	text-decoration: none; | ||||
| 	color: #337ab7 !important; | ||||
| 	text-decoration: none !important; | ||||
| 	transition: all 0.3s !important; | ||||
| } | ||||
| 
 | ||||
| a.discrete-link:focus { | ||||
| 	color: #337ab7; | ||||
| 	text-decoration: none; | ||||
| 	color: #ab2230 !important; | ||||
| 	text-decoration: none !important; | ||||
| 	transition: all 0.3s !important; | ||||
| } | ||||
| 
 | ||||
| .navbar-fixed-top { | ||||
| 	border-bottom: solid; | ||||
| 	border-color: #5e5e5e; | ||||
| 	border-bottom: 2px solid; | ||||
| 	border-color: #d6d6d6; | ||||
| } | ||||
| 
 | ||||
| .navbar-brand { | ||||
| 	font-weight: bold; | ||||
| 	letter-spacing: -2px; | ||||
| 	letter-spacing: -5px; | ||||
| 	font-size: 2.2em; | ||||
| 	font-family: 'Arial', sans-serif; | ||||
| 	color: #0b024c !important; | ||||
| 	margin-left: 0 !important; | ||||
| 	padding-left: 5px !important; | ||||
| 	 | ||||
| } | ||||
| 
 | ||||
| .table td.fit-content, | ||||
| @@ -144,3 +165,35 @@ a.discrete-link:focus { | ||||
| 	padding-top: 10px; | ||||
| 	padding-bottom: 10px; | ||||
| } | ||||
| 
 | ||||
| .well { | ||||
| 	background-color: #e5e5e5; | ||||
| } | ||||
| 
 | ||||
| .nav > li.disabled > a, | ||||
| .navbar-default .navbar-nav > .disabled > a | ||||
| { | ||||
| 	color: #a7a7a7; | ||||
| } | ||||
| 
 | ||||
| #toast-container > div { | ||||
| 	opacity: 1; | ||||
| 	filter: alpha(opacity=100); | ||||
| } | ||||
| 
 | ||||
|  .toast-success { | ||||
| 	background-color: #4c994c; | ||||
| } | ||||
| 
 | ||||
| #toast-container > div { | ||||
| 	box-shadow: none; | ||||
| } | ||||
| 
 | ||||
| .navbar-default .navbar-nav > .open > a { | ||||
| 	background-color: #d6d6d6 !important; | ||||
| } | ||||
| 
 | ||||
| .dropdown-menu > li > a:hover, | ||||
| .dropdown-menu > li > a:focus { | ||||
| 	background-color: #e5e5e5 !important; | ||||
| } | ||||
							
								
								
									
										
											BIN
										
									
								
								public/img/grocy.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 2.2 KiB | 
							
								
								
									
										3
									
								
								public/index.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,3 @@ | ||||
| <?php | ||||
|  | ||||
| require_once __DIR__ . '/../app.php'; | ||||
							
								
								
									
										33
									
								
								public/js/extensions.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,33 @@ | ||||
| EmptyElementWhenMatches = function(selector, text) | ||||
| { | ||||
| 	if ($(selector).text() === text) | ||||
| 	{ | ||||
| 		$(selector).text(''); | ||||
| 	} | ||||
| }; | ||||
|  | ||||
| String.prototype.contains = function(search) | ||||
| { | ||||
| 	return this.toLowerCase().indexOf(search.toLowerCase()) !== -1; | ||||
| }; | ||||
|  | ||||
| String.prototype.isEmpty = function() | ||||
| { | ||||
| 	return (this.length === 0 || !this.trim()); | ||||
| }; | ||||
|  | ||||
| GetUriParam = function(key) | ||||
| { | ||||
| 	var currentUri = decodeURIComponent(window.location.search.substring(1)); | ||||
| 	var vars = currentUri.split('&'); | ||||
|  | ||||
| 	for (i = 0; i < vars.length; i++) | ||||
| 	{ | ||||
| 		var currentParam = vars[i].split('='); | ||||
|  | ||||
| 		if (currentParam[0] === key) | ||||
| 		{ | ||||
| 			return currentParam[1] === undefined ? true : currentParam[1]; | ||||
| 		} | ||||
| 	} | ||||
| }; | ||||
							
								
								
									
										98
									
								
								public/js/grocy.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,98 @@ | ||||
| L = function(text, ...placeholderValues) | ||||
| { | ||||
| 	var localizedText = Grocy.LocalizationStrings[text]; | ||||
| 	if (localizedText === undefined) | ||||
| 	{ | ||||
| 		localizedText = text; | ||||
| 	} | ||||
| 	 | ||||
| 	for (var i = 0; i < placeholderValues.length; i++) | ||||
| 	{ | ||||
| 		localizedText = localizedText.replace('#' + (i + 1), placeholderValues[i]); | ||||
| 	} | ||||
| 	 | ||||
| 	return localizedText; | ||||
| } | ||||
|  | ||||
| U = function(relativePath) | ||||
| { | ||||
| 	return Grocy.BaseUrl.replace(/\/$/, '') + relativePath; | ||||
| } | ||||
|  | ||||
| if (!Grocy.ActiveNav.isEmpty()) | ||||
| { | ||||
| 	var menuItem = $('.nav').find("[data-nav-for-page='" + Grocy.ActiveNav + "']"); | ||||
| 	menuItem.addClass('active'); | ||||
| }	 | ||||
|  | ||||
| $.timeago.settings.allowFuture = true; | ||||
| $('time.timeago').timeago(); | ||||
|  | ||||
| toastr.options = { | ||||
| 	toastClass: 'alert', | ||||
| 	closeButton: true, | ||||
| 	timeOut: 20000, | ||||
| 	extendedTimeOut: 5000 | ||||
| }; | ||||
|  | ||||
| Grocy.Api = { }; | ||||
| Grocy.Api.Get = function(apiFunction, success, error) | ||||
| { | ||||
| 	var xhr = new XMLHttpRequest(); | ||||
| 	var url = U('/api/' + apiFunction); | ||||
|  | ||||
| 	xhr.onreadystatechange = function() | ||||
| 	{ | ||||
| 		if (xhr.readyState === XMLHttpRequest.DONE) | ||||
| 		{ | ||||
| 			if (xhr.status === 200) | ||||
| 			{ | ||||
| 				if (success) | ||||
| 				{ | ||||
| 					success(JSON.parse(xhr.responseText)); | ||||
| 				} | ||||
| 			} | ||||
| 			else | ||||
| 			{ | ||||
| 				if (error) | ||||
| 				{ | ||||
| 					error(xhr); | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	}; | ||||
|  | ||||
| 	xhr.open('GET', url, true); | ||||
| 	xhr.send(); | ||||
| }; | ||||
|  | ||||
| Grocy.Api.Post = function(apiFunction, jsonData, success, error) | ||||
| { | ||||
| 	var xhr = new XMLHttpRequest(); | ||||
| 	var url = U('/api/' + apiFunction); | ||||
|  | ||||
| 	xhr.onreadystatechange = function() | ||||
| 	{ | ||||
| 		if (xhr.readyState === XMLHttpRequest.DONE) | ||||
| 		{ | ||||
| 			if (xhr.status === 200) | ||||
| 			{ | ||||
| 				if (success) | ||||
| 				{ | ||||
| 					success(JSON.parse(xhr.responseText)); | ||||
| 				} | ||||
| 			} | ||||
| 			else | ||||
| 			{ | ||||
| 				if (error) | ||||
| 				{ | ||||
| 					error(xhr); | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	}; | ||||
|  | ||||
| 	xhr.open('POST', url, true); | ||||
| 	xhr.setRequestHeader('Content-type', 'application/json'); | ||||
| 	xhr.send(JSON.stringify(jsonData)); | ||||
| }; | ||||
							
								
								
									
										44
									
								
								public/viewjs/batteries.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,44 @@ | ||||
| $(document).on('click', '.battery-delete-button', function(e) | ||||
| { | ||||
| 	var objectName = $(e.currentTarget).attr('data-battery-name'); | ||||
| 	var objectId = $(e.currentTarget).attr('data-battery-id'); | ||||
|  | ||||
| 	bootbox.confirm({ | ||||
| 		message: L('Are you sure to delete battery "#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/batteries/' + objectId, | ||||
| 					function(result) | ||||
| 					{ | ||||
| 						window.location.href = U('/batteries'); | ||||
| 					}, | ||||
| 					function(xhr) | ||||
| 					{ | ||||
| 						console.error(xhr); | ||||
| 					} | ||||
| 				); | ||||
| 			} | ||||
| 		} | ||||
| 	}); | ||||
| }); | ||||
|  | ||||
| $('#batteries-table').DataTable({ | ||||
| 	'pageLength': 50, | ||||
| 	'order': [[1, 'asc']], | ||||
| 	'columnDefs': [ | ||||
| 		{ 'orderable': false, 'targets': 0 } | ||||
| 	], | ||||
| 	'language': JSON.parse(L('datatables_localization')) | ||||
| }); | ||||