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 @@ +
+

Dashboard

+ +

Current stock

+
+ + + + + + + + + + + + + + + + + +
ProductAmountNext best before date
+ product_id)->name; ?> + + amount; ?> + + best_before_date; ?> +
+
+
diff --git a/views/layout.php b/views/layout.php new file mode 100644 index 00000000..b7c861a5 --- /dev/null +++ b/views/layout.php @@ -0,0 +1,108 @@ + + + + + + + + + + + + + <?php echo $title; ?> | grocy + + + + + + + + + + + + + + + +
+
+ + + + +
+
+ + + + + + + + + + + + + + + + diff --git a/views/locationform.js b/views/locationform.js new file mode 100644 index 00000000..9216710c --- /dev/null +++ b/views/locationform.js @@ -0,0 +1,36 @@ +$('#save-location-button').on('click', function(e) +{ + e.preventDefault(); + + if (Grocy.EditMode === 'create') + { + Grocy.PostJson('/api/add-object/locations', $('#location-form').serializeJSON(), + function(result) + { + window.location.href = '/locations'; + }, + function(xhr) + { + console.error(xhr); + } + ); + } + else + { + Grocy.PostJson('/api/edit-object/locations/' + Grocy.EditObjectId, $('#location-form').serializeJSON(), + function(result) + { + window.location.href = '/locations'; + }, + function(xhr) + { + console.error(xhr); + } + ); + } +}); + +$(function() +{ + $('#name').focus(); +}); diff --git a/views/locationform.php b/views/locationform.php new file mode 100644 index 00000000..652fadf9 --- /dev/null +++ b/views/locationform.php @@ -0,0 +1,21 @@ +
+

+ + + + + + + +
+
+ + +
+
+ + +
+ +
+
diff --git a/views/locations.js b/views/locations.js new file mode 100644 index 00000000..c41027b5 --- /dev/null +++ b/views/locations.js @@ -0,0 +1,32 @@ +$(document).on('click', '.location-delete-button', function(e) +{ + bootbox.confirm({ + message: 'Delete location ' + $(e.target).attr('data-location-name') + '?', + buttons: { + confirm: { + label: 'Yes', + className: 'btn-success' + }, + cancel: { + label: 'No', + className: 'btn-danger' + } + }, + callback: function(result) + { + if (result == true) + { + Grocy.FetchJson('/api/delete-object/locations/' + $(e.target).attr('data-location-id'), + function(result) + { + window.location.href = '/locations'; + }, + function(xhr) + { + console.error(xhr); + } + ); + } + } + }); +}); diff --git a/views/locations.php b/views/locations.php new file mode 100644 index 00000000..a50a8083 --- /dev/null +++ b/views/locations.php @@ -0,0 +1,40 @@ +
+

+ Locations + +  Add + +

+ +
+ + + + + + + + + + + + + + + + + +
#NameDescription
+ + + + + + + + name; ?> + + description; ?> +
+
+
diff --git a/views/productform.js b/views/productform.js new file mode 100644 index 00000000..266dbf67 --- /dev/null +++ b/views/productform.js @@ -0,0 +1,36 @@ +$('#save-product-button').on('click', function(e) +{ + e.preventDefault(); + + if (Grocy.EditMode === 'create') + { + Grocy.PostJson('/api/add-object/products', $('#product-form').serializeJSON(), + function(result) + { + window.location.href = '/products'; + }, + function(xhr) + { + console.error(xhr); + } + ); + } + else + { + Grocy.PostJson('/api/edit-object/products/' + Grocy.EditObjectId, $('#product-form').serializeJSON(), + function(result) + { + window.location.href = '/products'; + }, + function(xhr) + { + console.error(xhr); + } + ); + } +}); + +$(function() +{ + $('#name').focus(); +}); diff --git a/views/productform.php b/views/productform.php new file mode 100644 index 00000000..2110ff2a --- /dev/null +++ b/views/productform.php @@ -0,0 +1,58 @@ +
+

+ + + + + + + +
+
+ + +
+
+ +
+ +
+ +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+
diff --git a/views/products.js b/views/products.js new file mode 100644 index 00000000..fc2fdf14 --- /dev/null +++ b/views/products.js @@ -0,0 +1,32 @@ +$(document).on('click', '.product-delete-button', function(e) +{ + bootbox.confirm({ + message: 'Delete product ' + $(e.target).attr('data-product-name') + '?', + buttons: { + confirm: { + label: 'Yes', + className: 'btn-success' + }, + cancel: { + label: 'No', + className: 'btn-danger' + } + }, + callback: function(result) + { + if (result == true) + { + Grocy.FetchJson('/api/delete-object/products/' + $(e.target).attr('data-product-id'), + function(result) + { + window.location.href = '/products'; + }, + function(xhr) + { + console.error(xhr); + } + ); + } + } + }); +}); diff --git a/views/products.php b/views/products.php new file mode 100644 index 00000000..50aa6803 --- /dev/null +++ b/views/products.php @@ -0,0 +1,56 @@ +
+

+ Products + +  Add + +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
#NameLocationQU purchaseQU stockQU factorDescription
+ + + + + + + + name; ?> + + location_id)->name; ?> + + qu_id_purchase)->name; ?> + + qu_id_stock)->name; ?> + + qu_factor_purchase_to_stock; ?> + + description; ?> +
+
+
diff --git a/views/purchase.js b/views/purchase.js new file mode 100644 index 00000000..f8d4dd94 --- /dev/null +++ b/views/purchase.js @@ -0,0 +1,75 @@ +$('#save-purchase-button').on('click', function(e) +{ + e.preventDefault(); + + var jsonForm = $('#purchase-form').serializeJSON(); + delete jsonForm.barcode; + + Grocy.FetchJson('/api/get-object/products/' + jsonForm.product_id, + function(product) + { + jsonForm.amount = jsonForm.amount * product.qu_factor_purchase_to_stock; + + Grocy.PostJson('/api/add-object/stock', jsonForm, + function(result) + { + $('#product_id').val(null); + $('#amount').val(1); + $('#product_id').focus(); + $('#purchase-form').validator('validate'); + }, + function(xhr) + { + console.error(xhr); + } + ); + }, + 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-purchase-qu-name').text(productStatistics.quantity_unit_purchase.name); + $('#selected-product-last-purchased').text(productStatistics.last_purchased || 'never'); + $('#selected-product-last-used').text(productStatistics.last_used || 'never'); + }, + function(xhr) + { + console.error(xhr); + } + ); +}); + +$(function() +{ + $('.datepicker').datepicker( + { + format: 'yyyy-mm-dd', + startDate: '+7d', + 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/purchase.php b/views/purchase.php new file mode 100644 index 00000000..7becac08 --- /dev/null +++ b/views/purchase.php @@ -0,0 +1,49 @@ +
+

Record purchase

+ +
+
+ + +
+ +
+ +
+
+
+
+
+ + +
+
+
+ +
+ +
+ +
+
+
+
+ +
+
+ +
+

Product overview

+

Purchase quantity:

+ +

+ Stock amount:
+ Last purchased:
+ Last used: +

+
\ No newline at end of file diff --git a/views/quantityunitform.js b/views/quantityunitform.js new file mode 100644 index 00000000..c571e08f --- /dev/null +++ b/views/quantityunitform.js @@ -0,0 +1,36 @@ +$('#save-quantityunit-button').on('click', function(e) +{ + e.preventDefault(); + + if (Grocy.EditMode === 'create') + { + Grocy.PostJson('/api/add-object/quantity_units', $('#quantityunit-form').serializeJSON(), + function(result) + { + window.location.href = '/quantityunits'; + }, + function(xhr) + { + console.error(xhr); + } + ); + } + else + { + Grocy.PostJson('/api/edit-object/quantity_units/' + Grocy.EditObjectId, $('#quantityunit-form').serializeJSON(), + function(result) + { + window.location.href = '/quantityunits'; + }, + function(xhr) + { + console.error(xhr); + } + ); + } +}); + +$(function() +{ + $('#name').focus(); +}); diff --git a/views/quantityunitform.php b/views/quantityunitform.php new file mode 100644 index 00000000..0f330673 --- /dev/null +++ b/views/quantityunitform.php @@ -0,0 +1,21 @@ +
+

+ + + + + + + +
+
+ + +
+
+ + +
+ +
+
diff --git a/views/quantityunits.js b/views/quantityunits.js new file mode 100644 index 00000000..9788c925 --- /dev/null +++ b/views/quantityunits.js @@ -0,0 +1,32 @@ +$(document).on('click', '.quantityunit-delete-button', function(e) +{ + bootbox.confirm({ + message: 'Delete quantity unit ' + $(e.target).attr('data-quantityunit-name') + '?', + buttons: { + confirm: { + label: 'Yes', + className: 'btn-success' + }, + cancel: { + label: 'No', + className: 'btn-danger' + } + }, + callback: function(result) + { + if (result == true) + { + Grocy.FetchJson('/api/delete-object/quantity_units/' + $(e.target).attr('data-quantityunit-id'), + function(result) + { + window.location.href = '/quantityunits'; + }, + function(xhr) + { + console.error(xhr); + } + ); + } + } + }); +}); diff --git a/views/quantityunits.php b/views/quantityunits.php new file mode 100644 index 00000000..40815edf --- /dev/null +++ b/views/quantityunits.php @@ -0,0 +1,40 @@ +
+

+ Quantity units + +  Add + +

+ +
+ + + + + + + + + + + + + + + + + +
#NameDescription
+ + + + + + + + name; ?> + + description; ?> +
+
+