diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..3c60286a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,203 @@
+## 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
+/vendor
+/.release
+/config.php
+/composer.phar
+/composer.lock
diff --git a/README.md b/README.md
index f5741de3..66f54e5d 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,20 @@
# grocy
ERP beyond your fridge
+
+## 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://grocy.projectdemos.berrnd.org](https://grocy.projectdemos.berrnd.org)
+
+## 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 `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.
+
+## Todo
+A lot...
+
+## License
+The MIT License (MIT)
diff --git a/bower.json b/bower.json
new file mode 100644
index 00000000..a6ae2ccb
--- /dev/null
+++ b/bower.json
@@ -0,0 +1,14 @@
+{
+ "name": "asp.net",
+ "private": true,
+ "dependencies": {
+ "bootstrap": "3.3.7",
+ "font-awesome": "4.7.0",
+ "bootbox": "4.4.0",
+ "jquery.serializeJSON": "2.7.2",
+ "bootstrap-validator": "0.11.9",
+ "bootstrap-datepicker": "1.6.4",
+ "moment": "2.18.1",
+ "bootstrap-combobox": "1.1.8"
+ }
+}
diff --git a/build.bat b/build.bat
new file mode 100644
index 00000000..97dc1a10
--- /dev/null
+++ b/build.bat
@@ -0,0 +1,11 @@
+set projectPath=%~dp0
+if %projectPath:~-1%==\ set projectPath=%projectPath:~0,-1%
+
+set releasePath=%projectPath%\.release
+mkdir "%releasePath%"
+
+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!composer.phar -xr!grocy.phpproj -xr!grocy.phpproj.user -xr!grocy.sln
+"build_tools\7za.exe" d "%releasePath%\grocy_%version%.zip" data\add_before_end_body.html data\demo.txt data\grocy.db data\.gitignore config.php bower.json
diff --git a/build_tools/7za.exe b/build_tools/7za.exe
new file mode 100644
index 00000000..8a545980
Binary files /dev/null and b/build_tools/7za.exe differ
diff --git a/composer.json b/composer.json
new file mode 100644
index 00000000..095e0937
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,8 @@
+{
+ "require": {
+ "slim/slim": "^3.8",
+ "slim/php-view": "^2.2",
+ "morris/lessql": "^0.3.4",
+ "tuupola/slim-basic-auth": "^2.2"
+ }
+}
diff --git a/config-dist.php b/config-dist.php
new file mode 100644
index 00000000..58ea7b6f
--- /dev/null
+++ b/config-dist.php
@@ -0,0 +1,4 @@
+exec('PRAGMA encoding = "UTF-8"');
+ $pdo->exec("CREATE TABLE migrations (migration INTEGER NOT NULL UNIQUE, execution_time_timestamp DATETIME DEFAULT (datetime('now', 'localtime')), PRIMARY KEY(migration)) WITHOUT ROWID");
+ self::MigrateDb();
+
+ if (self::IsDemoInstallation())
+ {
+ self::PopulateDemoData();
+ }
+ }
+
+ self::$DbConnectionRaw = $pdo;
+ }
+
+ return self::$DbConnectionRaw;
+ }
+
+ /**
+ * @return LessQL\Database
+ */
+ public static function GetDbConnection()
+ {
+ if (self::$DbConnection == null)
+ {
+ self::$DbConnection = new LessQL\Database(self::GetDbConnectionRaw());
+ }
+
+ return self::$DbConnection;
+ }
+
+ public static function MigrateDb()
+ {
+ $pdo = self::GetDbConnectionRaw();
+
+ 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 UNIQUE,
+ 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,
+ 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,
+ 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'))
+ )"
+ );
+
+ self::ExecuteMigrationWhenNeeded($pdo, 5, "
+ CREATE TABLE consumptions (
+ id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
+ product_id INTEGER NOT NULL,
+ amount INTEGER NOT NULL,
+ used_date DATE DEFAULT (datetime('now', 'localtime')),
+ best_before_date DATE,
+ purchased_date DATE,
+ spoiled INTEGER NOT NULL DEFAULT 0
+ )"
+ );
+
+ 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);"
+ );
+ }
+
+ public static function PopulateDemoData()
+ {
+ $pdo = self::GetDbConnectionRaw();
+
+ self::ExecuteMigrationWhenNeeded($pdo, -1, utf8_encode("
+ UPDATE locations SET name = 'Vorratskammer', description = '' WHERE id = 1;
+ INSERT INTO locations (name) VALUES ('Süßigkeitenschrank');
+ INSERT INTO locations (name) VALUES ('Konvervenschrank');
+
+ UPDATE quantity_units SET name = 'Stück' WHERE id = 1;
+ INSERT INTO quantity_units (name) VALUES ('Packung');
+
+ INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('Gummibärchen', 2, 2, 2, 1);
+ INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('Chips', 2, 2, 2, 1);
+ INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('Eier', 1, 2, 1, 10);
+
+ INSERT INTO stock (product_id, amount, best_before_date) VALUES (3, 5, date('now', '+180 day'));
+ INSERT INTO stock (product_id, amount, best_before_date) VALUES (4, 5, date('now', '+180 day'));
+ INSERT INTO stock (product_id, amount, best_before_date) VALUES (5, 5, date('now', '+25 day'));
+ "));
+ }
+
+ private static function ExecuteMigrationWhenNeeded(PDO $pdo, int $migrationId, string $sql)
+ {
+ if ($pdo->query("SELECT COUNT(*) FROM migrations WHERE migration = $migrationId")->fetchColumn() == 0)
+ {
+ $pdo->exec($sql);
+ $pdo->exec('INSERT INTO migrations (migration) VALUES (' . $migrationId . ')');
+ }
+ }
+
+ public static function FindObjectInArrayByPropertyValue($array, $propertyName, $propertyValue)
+ {
+ foreach($array as $object)
+ {
+ if($object->{$propertyName} == $propertyValue)
+ {
+ return $object;
+ }
+ }
+
+ return null;
+ }
+
+ public static function GetCurrentStock()
+ {
+ $db = self::GetDbConnectionRaw();
+ return $db->query('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) DESC')->fetchAll(PDO::FETCH_OBJ);
+ }
+
+ public static function IsDemoInstallation()
+ {
+ return file_exists('data/demo.txt');
+ }
+}
diff --git a/grocy.phpproj b/grocy.phpproj
new file mode 100644
index 00000000..517a3f87
--- /dev/null
+++ b/grocy.phpproj
@@ -0,0 +1,58 @@
+
+
+ Debug
+ grocy
+ edb77631-5196-4860-baeb-bca8900a4b6d
+ Library
+
+
+ {A0786B88-2ADB-4C21-ABE8-AA2D79766269}
+ grocy
+
+
+ true
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Content
+ README.md
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/grocy.sln b/grocy.sln
new file mode 100644
index 00000000..c9b6ea7e
--- /dev/null
+++ b/grocy.sln
@@ -0,0 +1,20 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 15
+VisualStudioVersion = 15.0.26403.3
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{A0786B88-2ADB-4C21-ABE8-AA2D79766269}") = "grocy", "grocy.phpproj", "{EDB77631-5196-4860-BAEB-BCA8900A4B6D}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {EDB77631-5196-4860-BAEB-BCA8900A4B6D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {EDB77631-5196-4860-BAEB-BCA8900A4B6D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+EndGlobal
diff --git a/index.php b/index.php
new file mode 100644
index 00000000..f34b0466
--- /dev/null
+++ b/index.php
@@ -0,0 +1,313 @@
+getContainer();
+$container['renderer'] = new PhpRenderer('./views');
+
+if (!Grocy::IsDemoInstallation())
+{
+ $isHttpsReverseProxied = !empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https';
+ $app->add(new \Slim\Middleware\HttpBasicAuthentication([
+ 'realm' => 'grocy',
+ 'secure' => !$isHttpsReverseProxied,
+ 'users' => [
+ HTTP_USER => HTTP_PASSWORD
+ ]
+ ]));
+}
+
+$app->get('/', function(Request $request, Response $response)
+{
+ $db = Grocy::GetDbConnection();
+
+ return $this->renderer->render($response, '/layout.php', [
+ 'title' => 'Dashboard',
+ 'contentPage' => 'dashboard.php',
+ 'products' => $db->products(),
+ 'currentStock' => Grocy::GetCurrentStock()
+ ]);
+});
+
+$app->get('/purchase', function(Request $request, Response $response)
+{
+ $db = Grocy::GetDbConnection();
+
+ return $this->renderer->render($response, '/layout.php', [
+ 'title' => 'Purchase',
+ 'contentPage' => 'purchase.php',
+ 'products' => $db->products()
+ ]);
+});
+
+$app->get('/consumption', function(Request $request, Response $response)
+{
+ $db = Grocy::GetDbConnection();
+
+ return $this->renderer->render($response, '/layout.php', [
+ 'title' => 'Consumption',
+ 'contentPage' => 'consumption.php',
+ 'products' => $db->products()
+ ]);
+});
+
+$app->get('/products', function(Request $request, Response $response)
+{
+ $db = Grocy::GetDbConnection();
+
+ 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)
+{
+ $db = Grocy::GetDbConnection();
+
+ return $this->renderer->render($response, '/layout.php', [
+ 'title' => 'Locations',
+ 'contentPage' => 'locations.php',
+ 'locations' => $db->locations()
+ ]);
+});
+
+$app->get('/quantityunits', function(Request $request, Response $response)
+{
+ $db = Grocy::GetDbConnection();
+
+ return $this->renderer->render($response, '/layout.php', [
+ 'title' => 'Quantity units',
+ 'contentPage' => 'quantityunits.php',
+ 'quantityunits' => $db->quantity_units()
+ ]);
+});
+
+$app->get('/product/{productId}', function(Request $request, Response $response, $args)
+{
+ $db = Grocy::GetDbConnection();
+
+ 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)
+{
+ $db = Grocy::GetDbConnection();
+
+ 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)
+{
+ $db = Grocy::GetDbConnection();
+
+ 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->group('/api', function()
+{
+ $this->get('/get-objects/{entity}', function(Request $request, Response $response, $args)
+ {
+ $db = Grocy::GetDbConnection();
+ echo json_encode($db->{$args['entity']}());
+
+ return $response->withHeader('Content-Type', 'application/json');
+ });
+
+ $this->get('/get-object/{entity}/{objectId}', function(Request $request, Response $response, $args)
+ {
+ $db = Grocy::GetDbConnection();
+ echo json_encode($db->{$args['entity']}($args['objectId']));
+
+ return $response->withHeader('Content-Type', 'application/json');
+ });
+
+ $this->post('/add-object/{entity}', function(Request $request, Response $response, $args)
+ {
+ $db = Grocy::GetDbConnection();
+ $newRow = $db->{$args['entity']}()->createRow($request->getParsedBody());
+ $newRow->save();
+ $success = $newRow->isClean();
+ echo json_encode(array('success' => $success));
+
+ return $response->withHeader('Content-Type', 'application/json');
+ });
+
+ $this->post('/edit-object/{entity}/{objectId}', function(Request $request, Response $response, $args)
+ {
+ $db = Grocy::GetDbConnection();
+ $row = $db->{$args['entity']}($args['objectId']);
+ $row->update($request->getParsedBody());
+ $success = $row->isClean();
+ echo json_encode(array('success' => $success));
+
+ return $response->withHeader('Content-Type', 'application/json');
+ });
+
+ $this->get('/delete-object/{entity}/{objectId}', function(Request $request, Response $response, $args)
+ {
+ $db = Grocy::GetDbConnection();
+ $row = $db->{$args['entity']}($args['objectId']);
+ $row->delete();
+ $success = $row->isClean();
+ echo json_encode(array('success' => $success));
+
+ return $response->withHeader('Content-Type', 'application/json');
+ });
+
+ $this->get('/get-product-statistics/{productId}', function(Request $request, Response $response, $args)
+ {
+ $db = Grocy::GetDbConnection();
+ $product = $db->products($args['productId']);
+ $productStockAmount = $db->stock()->where('product_id', $args['productId'])->sum('amount');
+ $productLastPurchased = $db->stock()->where('product_id', $args['productId'])->max('purchased_date');
+ $productLastUsed = $db->consumptions()->where('product_id', $args['productId'])->max('used_date');
+ $quPurchase = $db->quantity_units($product->qu_id_purchase);
+ $quStock = $db->quantity_units($product->qu_id_stock);
+
+ echo json_encode(array(
+ 'product' => $product,
+ 'last_purchased' => $productLastPurchased,
+ 'last_used' => $productLastUsed,
+ 'stock_amount' => $productStockAmount,
+ 'quantity_unit_purchase' => $quPurchase,
+ 'quantity_unit_stock' => $quStock
+ ));
+
+ return $response->withHeader('Content-Type', 'application/json');
+ });
+
+ $this->get('/get-current-stock', function(Request $request, Response $response)
+ {
+ echo json_encode(Grocy::GetCurrentStock());
+
+ return $response->withHeader('Content-Type', 'application/json');
+ });
+
+ $this->get('/consume-product/{productId}/{amount}', function(Request $request, Response $response, $args)
+ {
+ $db = Grocy::GetDbConnection();
+ $productStockAmount = $db->stock()->where('product_id', $args['productId'])->sum('amount');
+ $potentialStockEntries = $db->stock()->where('product_id', $args['productId'])->orderBy('purchased_date', 'ASC')->fetchAll(); //FIFO
+ $amount = $args['amount'];
+
+ if ($amount > $productStockAmount)
+ {
+ echo json_encode(array('success' => false));
+ return $response->withStatus(400)->withHeader('Content-Type', 'application/json');
+ }
+
+ $spoiled = 0;
+ if (isset($request->getQueryParams()['spoiled']) && !empty($request->getQueryParams()['spoiled']) && $request->getQueryParams()['spoiled'] == '1')
+ {
+ $spoiled = 1;
+ }
+
+ foreach ($potentialStockEntries as $stockEntry)
+ {
+ if ($amount == 0)
+ {
+ break;
+ }
+
+ if ($amount >= $stockEntry->amount) //Take the whole stock entry
+ {
+ $newRow = $db->consumptions()->createRow(array(
+ 'product_id' => $stockEntry->product_id,
+ 'amount' => $stockEntry->amount,
+ 'best_before_date' => $stockEntry->best_before_date,
+ 'purchased_date' => $stockEntry->purchased_date,
+ 'spoiled' => $spoiled
+ ));
+ $newRow->save();
+
+ $stockEntry->delete();
+ }
+ else //Split the stock entry resp. update the amount
+ {
+ $newRow = $db->consumptions()->createRow(array(
+ 'product_id' => $stockEntry->product_id,
+ 'amount' => $amount,
+ 'best_before_date' => $stockEntry->best_before_date,
+ 'purchased_date' => $stockEntry->purchased_date,
+ 'spoiled' => $spoiled
+ ));
+ $newRow->save();
+
+ $restStockAmount = $stockEntry->amount - $amount;
+ $stockEntry->update(array(
+ 'amount' => $restStockAmount
+ ));
+ }
+
+ $amount -= $stockEntry->amount;
+ }
+
+ echo json_encode(array('success' => true));
+ return $response->withHeader('Content-Type', 'application/json');
+ });
+});
+
+$app->run();
diff --git a/robots.txt b/robots.txt
new file mode 100644
index 00000000..1f53798b
--- /dev/null
+++ b/robots.txt
@@ -0,0 +1,2 @@
+User-agent: *
+Disallow: /
diff --git a/style.css b/style.css
new file mode 100644
index 00000000..83815612
--- /dev/null
+++ b/style.css
@@ -0,0 +1,92 @@
+body {
+ padding-top: 50px;
+}
+
+.navbar-fixed-top {
+ border: 0;
+}
+
+.sidebar {
+ display: none;
+}
+
+@media (min-width: 768px) {
+ .sidebar {
+ position: fixed;
+ top: 51px;
+ bottom: 0;
+ left: 0;
+ z-index: 1000;
+ display: block;
+ padding: 20px;
+ overflow-x: hidden;
+ overflow-y: auto;
+ background-color: #f5f5f5;
+ border-right: 1px solid #5e5e5e;
+ min-width: 210px;
+ max-width: 260px;
+ }
+}
+
+.nav-sidebar {
+ margin-right: -21px;
+ margin-bottom: 20px;
+ margin-left: -20px;
+}
+
+.nav-sidebar > li > a {
+ padding-right: 20px;
+ padding-left: 20px;
+}
+
+.nav-sidebar > .active > a,
+.nav-sidebar > .active > a:hover,
+.nav-sidebar > .active > a:focus {
+ color: #fff;
+ background-color: #5e5e5e;
+}
+
+.main {
+ padding: 20px;
+}
+
+@media (min-width: 768px) {
+ .main {
+ padding-right: 40px;
+ padding-left: 40px;
+ }
+}
+
+.main .page-header {
+ margin-top: 0;
+}
+
+.nav-copyright {
+ position: absolute;
+ bottom: 0;
+ width: 100%;
+ color: #b3b3b1;
+ font-size: 0.85em;
+ text-align: center;
+}
+
+a.discrete-link {
+ color: inherit;
+}
+
+.navbar-fixed-top {
+ border-bottom: solid;
+ border-color: #5e5e5e;
+}
+
+.navbar-brand {
+ font-weight: bold;
+ letter-spacing: -2px;
+ font-size: 2.2em;
+}
+
+.table td.fit-content,
+.table th.fit-content {
+ white-space: nowrap;
+ width: 1%;
+}
diff --git a/version.txt b/version.txt
new file mode 100644
index 00000000..6c6aa7cb
--- /dev/null
+++ b/version.txt
@@ -0,0 +1 @@
+0.1.0
\ No newline at end of file
diff --git a/views/consumption.js b/views/consumption.js
new file mode 100644
index 00000000..134fd2e6
--- /dev/null
+++ b/views/consumption.js
@@ -0,0 +1,70 @@
+$('#save-consumption-button').on('click', function(e)
+{
+ e.preventDefault();
+
+ var jsonForm = $('#consumption-form').serializeJSON();
+
+ var spoiled = 0;
+ if ($('#spoiled').is(':checked'))
+ {
+ spoiled = 1;
+ }
+
+ Grocy.FetchJson('/api/consume-product/' + jsonForm.product_id + '/' + jsonForm.amount + '?spoiled=' + spoiled,
+ function(result)
+ {
+ $('#product_id').val(null);
+ $('#amount').val(1);
+ $('#product_name').focus();
+ $('#consumption-form').validator('validate');
+ },
+ function(xhr)
+ {
+ console.error(xhr);
+ }
+ );
+});
+
+$('#product_id').on('change', function(e)
+{
+ var productId = $(e.target).val();
+
+ Grocy.FetchJson('/api/get-product-statistics/' + productId,
+ function(productStatistics)
+ {
+ $('#selected-product-name').text(productStatistics.product.name);
+ $('#selected-product-stock-amount').text(productStatistics.stock_amount || '0');
+ $('#selected-product-stock-qu-name').text(productStatistics.quantity_unit_stock.name);
+ $('#selected-product-stock-qu-name2').text(productStatistics.quantity_unit_stock.name);
+ $('#selected-product-last-purchased').text(productStatistics.last_purchased || 'never');
+ $('#selected-product-last-used').text(productStatistics.last_used || 'never');
+ $('#amount').attr('max', productStatistics.stock_amount);
+ },
+ function(xhr)
+ {
+ console.error(xhr);
+ }
+ );
+});
+
+$(function()
+{
+ $('.datepicker').datepicker(
+ {
+ format: 'yyyy-mm-dd',
+ startDate: '-3d',
+ todayHighlight: true,
+ autoclose: true,
+ calendarWeeks: true,
+ orientation: 'bottom auto'
+ });
+ $('.datepicker').val(moment().format('YYYY-MM-DD'));
+ $('.datepicker').trigger('change');
+
+ $('.combobox').combobox();
+ $('#product_id').focus();
+ $('#product_id').val(null);
+ $('#product_name').trigger('change');
+ $('#purchase-form').validator();
+ $('#purchase-form').validator('validate');
+});
diff --git a/views/consumption.php b/views/consumption.php
new file mode 100644
index 00000000..bbc42d89
--- /dev/null
+++ b/views/consumption.php
@@ -0,0 +1,44 @@
+
+
Record consumption
+
+
+
+
+
+
Product overview
+
Stock quantity unit:
+
+
+ Stock amount:
+ Last purchased:
+ Last used:
+
+
\ No newline at end of file
diff --git a/views/dashboard.php b/views/dashboard.php
new file mode 100644
index 00000000..6cfd429d
--- /dev/null
+++ b/views/dashboard.php
@@ -0,0 +1,31 @@
+