mirror of
https://github.com/grocy/grocy.git
synced 2025-08-20 12:20:22 +00:00
First commit
This commit is contained in:
203
.gitignore
vendored
Normal file
203
.gitignore
vendored
Normal file
@@ -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
|
18
README.md
18
README.md
@@ -1,2 +1,20 @@
|
|||||||
# grocy
|
# grocy
|
||||||
ERP beyond your fridge
|
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)
|
||||||
|
14
bower.json
Normal file
14
bower.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
11
build.bat
Normal file
11
build.bat
Normal file
@@ -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
|
BIN
build_tools/7za.exe
Normal file
BIN
build_tools/7za.exe
Normal file
Binary file not shown.
8
composer.json
Normal file
8
composer.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
4
config-dist.php
Normal file
4
config-dist.php
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
define('HTTP_USER', 'admin');
|
||||||
|
define('HTTP_PASSWORD', 'admin');
|
2
data/.gitignore
vendored
Normal file
2
data/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
*
|
||||||
|
!.gitignore
|
66
grocy.js
Normal file
66
grocy.js
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
var Grocy = {};
|
||||||
|
|
||||||
|
$(function()
|
||||||
|
{
|
||||||
|
var menuItem = $('.nav-sidebar').find("[data-nav-for-page='" + Grocy.ContentPage + "']");
|
||||||
|
menuItem.addClass('active');
|
||||||
|
});
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
169
grocy.php
Normal file
169
grocy.php
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class Grocy
|
||||||
|
{
|
||||||
|
private static $DbConnection;
|
||||||
|
private static $DbConnectionRaw;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return PDO
|
||||||
|
*/
|
||||||
|
private static function GetDbConnectionRaw()
|
||||||
|
{
|
||||||
|
if (self::$DbConnectionRaw == null)
|
||||||
|
{
|
||||||
|
$newDb = !file_exists('data/grocy.db');
|
||||||
|
$pdo = new PDO('sqlite:data/grocy.db');
|
||||||
|
|
||||||
|
if ($newDb)
|
||||||
|
{
|
||||||
|
$pdo->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<><53>igkeitenschrank');
|
||||||
|
INSERT INTO locations (name) VALUES ('Konvervenschrank');
|
||||||
|
|
||||||
|
UPDATE quantity_units SET name = 'St<53>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<69>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');
|
||||||
|
}
|
||||||
|
}
|
58
grocy.phpproj
Normal file
58
grocy.phpproj
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
|
<PropertyGroup>
|
||||||
|
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||||
|
<Name>grocy</Name>
|
||||||
|
<ProjectGuid>edb77631-5196-4860-baeb-bca8900a4b6d</ProjectGuid>
|
||||||
|
<OutputType>Library</OutputType>
|
||||||
|
<RootNamespace>
|
||||||
|
</RootNamespace>
|
||||||
|
<ProjectTypeGuids>{A0786B88-2ADB-4C21-ABE8-AA2D79766269}</ProjectTypeGuids>
|
||||||
|
<AssemblyName>grocy</AssemblyName>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||||
|
<IncludeDebugInformation>true</IncludeDebugInformation>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||||
|
<IncludeDebugInformation>false</IncludeDebugInformation>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="config-dist.php" />
|
||||||
|
<Compile Include="grocy.php" />
|
||||||
|
<Compile Include="index.php" />
|
||||||
|
<Compile Include="views\consumption.php" />
|
||||||
|
<Compile Include="views\purchase.php" />
|
||||||
|
<Compile Include="views\quantityunitform.php" />
|
||||||
|
<Compile Include="views\locationform.php" />
|
||||||
|
<Compile Include="views\productform.php" />
|
||||||
|
<Compile Include="views\locations.php" />
|
||||||
|
<Compile Include="views\quantityunits.php" />
|
||||||
|
<Compile Include="views\products.php" />
|
||||||
|
<Compile Include="views\dashboard.php" />
|
||||||
|
<Compile Include="views\layout.php" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Include="bower.json" />
|
||||||
|
<None Include="build.bat" />
|
||||||
|
<Content Include="composer.json" />
|
||||||
|
<Content Include="grocy.js" />
|
||||||
|
<None Include="README.md" />
|
||||||
|
<Content Include="README.html">
|
||||||
|
<SubType>Content</SubType>
|
||||||
|
<DependentUpon>README.md</DependentUpon>
|
||||||
|
</Content>
|
||||||
|
<Content Include="robots.txt" />
|
||||||
|
<Content Include="style.css" />
|
||||||
|
<Content Include="version.txt" />
|
||||||
|
<Content Include="views\consumption.js" />
|
||||||
|
<Content Include="views\purchase.js" />
|
||||||
|
<Content Include="views\quantityunitform.js" />
|
||||||
|
<Content Include="views\locationform.js" />
|
||||||
|
<Content Include="views\productform.js" />
|
||||||
|
<Content Include="views\locations.js" />
|
||||||
|
<Content Include="views\quantityunits.js" />
|
||||||
|
<Content Include="views\products.js" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="views\" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
20
grocy.sln
Normal file
20
grocy.sln
Normal file
@@ -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
|
313
index.php
Normal file
313
index.php
Normal file
@@ -0,0 +1,313 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use \Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
|
use \Psr\Http\Message\ResponseInterface as Response;
|
||||||
|
use Slim\Views\PhpRenderer;
|
||||||
|
|
||||||
|
require_once 'vendor/autoload.php';
|
||||||
|
require_once 'config.php';
|
||||||
|
require_once 'grocy.php';
|
||||||
|
|
||||||
|
$app = new \Slim\App;
|
||||||
|
$container = $app->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();
|
2
robots.txt
Normal file
2
robots.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
User-agent: *
|
||||||
|
Disallow: /
|
92
style.css
Normal file
92
style.css
Normal file
@@ -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%;
|
||||||
|
}
|
1
version.txt
Normal file
1
version.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
0.1.0
|
70
views/consumption.js
Normal file
70
views/consumption.js
Normal file
@@ -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');
|
||||||
|
});
|
44
views/consumption.php
Normal file
44
views/consumption.php
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<div class="col-sm-3 col-sm-offset-3 col-md-3 col-md-offset-2 main">
|
||||||
|
<h1 class="page-header">Record consumption</h1>
|
||||||
|
|
||||||
|
<form id="consumption-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="product_id">Product</label>
|
||||||
|
<select class="form-control combobox" id="product_id" name="product_id" required>
|
||||||
|
<option value=""></option>
|
||||||
|
<?php foreach ($products as $product) : ?>
|
||||||
|
<option value="<?php echo $product->id; ?>"><?php echo $product->name; ?></option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
<div class="input-group date">
|
||||||
|
<input type="text" class="form-control" id="barcode" name="barcode" />
|
||||||
|
<div class="input-group-addon">
|
||||||
|
<i class="fa fa-barcode"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="help-block with-errors"></div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="amount">Amount</label>
|
||||||
|
<input type="number" class="form-control" id="amount" name="amount" value="1" min="1" required>
|
||||||
|
<div class="help-block with-errors"></div>
|
||||||
|
</div>
|
||||||
|
<div class="checkbox">
|
||||||
|
<label for="spoiled">
|
||||||
|
<input type="checkbox" id="spoiled" name="spoiled"> Spoiled
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<button id="save-consumption-button" type="submit" class="btn btn-default">OK</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-sm-3 col-md-3 main well">
|
||||||
|
<h3>Product overview <strong><span id="selected-product-name"></span></strong></h3>
|
||||||
|
<h4><strong>Stock quantity unit:</strong> <span id="selected-product-stock-qu-name"></span></h4>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<strong>Stock amount:</strong> <span id="selected-product-stock-amount"></span> <span id="selected-product-stock-qu-name2"></span><br />
|
||||||
|
<strong>Last purchased:</strong> <span id="selected-product-last-purchased"></span><br />
|
||||||
|
<strong>Last used:</strong> <span id="selected-product-last-used"></span>
|
||||||
|
</p>
|
||||||
|
</div>
|
31
views/dashboard.php
Normal file
31
views/dashboard.php
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
|
||||||
|
<h1 class="page-header">Dashboard</h1>
|
||||||
|
|
||||||
|
<h3>Current stock</h3>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Product</th>
|
||||||
|
<th>Amount</th>
|
||||||
|
<th>Next best before date</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($currentStock as $currentStockEntry) : ?>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<?php echo Grocy::FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name; ?>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<?php echo $currentStockEntry->amount; ?>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<?php echo $currentStockEntry->best_before_date; ?>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
108
views/layout.php
Normal file
108
views/layout.php
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
|
||||||
|
<meta name="robots" content="noindex,nofollow" />
|
||||||
|
|
||||||
|
<meta name="author" content="Bernd Bestel (bernd@berrnd.de)" />
|
||||||
|
|
||||||
|
<title>
|
||||||
|
<?php echo $title; ?> | grocy
|
||||||
|
</title>
|
||||||
|
|
||||||
|
<link href="/bower_components/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet" />
|
||||||
|
<link href="/bower_components/font-awesome/css/font-awesome.min.css" rel="stylesheet" />
|
||||||
|
<link href="/bower_components/bootstrap-datepicker/dist/css/bootstrap-datepicker3.min.css" rel="stylesheet" />
|
||||||
|
<link href="/bower_components/bootstrap-combobox/css/bootstrap-combobox.css" rel="stylesheet" />
|
||||||
|
<link href="/style.css" rel="stylesheet" />
|
||||||
|
|
||||||
|
<script src="/bower_components/jquery/dist/jquery.min.js"></script>
|
||||||
|
<script src="/grocy.js"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<nav class="navbar navbar-default navbar-fixed-top">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="navbar-header">
|
||||||
|
<!--<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" >
|
||||||
|
<span class="icon-bar"></span>
|
||||||
|
<span class="icon-bar"></span>
|
||||||
|
<span class="icon-bar"></span>
|
||||||
|
</button>-->
|
||||||
|
<a class="navbar-brand" href="/">grocy</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="navbar" class="navbar-collapse collapse">
|
||||||
|
<!--<ul class="nav navbar-nav navbar-right">
|
||||||
|
<li>
|
||||||
|
<a href="#">About</a>
|
||||||
|
</li>
|
||||||
|
</ul>-->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-3 col-md-2 sidebar">
|
||||||
|
<ul class="nav nav-sidebar">
|
||||||
|
<li data-nav-for-page="dashboard.php">
|
||||||
|
<a class="discrete-link" href="/"><i class="fa fa-tachometer fa-fw"></i> Dashboard</a>
|
||||||
|
</li>
|
||||||
|
<li data-nav-for-page="purchase.php">
|
||||||
|
<a class="discrete-link" href="/purchase"><i class="fa fa-shopping-cart fa-fw"></i> Record purchase</a>
|
||||||
|
</li>
|
||||||
|
<li data-nav-for-page="consumption.php">
|
||||||
|
<a class="discrete-link" href="/consumption"><i class="fa fa-cutlery fa-fw"></i> Record consumption</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<ul class="nav nav-sidebar">
|
||||||
|
<li data-nav-for-page="products.php">
|
||||||
|
<a class="discrete-link" href="/products"><i class="fa fa-product-hunt fa-fw"></i> Manage products</a>
|
||||||
|
</li>
|
||||||
|
<li data-nav-for-page="locations.php">
|
||||||
|
<a class="discrete-link" href="/locations"><i class="fa fa-map-marker fa-fw"></i> Manage locations</a>
|
||||||
|
</li>
|
||||||
|
<li data-nav-for-page="quantityunits.php">
|
||||||
|
<a class="discrete-link" href="/quantityunits"><i class="fa fa-balance-scale fa-fw"></i> Manage quantity units</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div class="nav-copyright nav nav-sidebar">
|
||||||
|
grocy is a project by
|
||||||
|
<a class="discrete-link" href="https://berrnd.de" target="_blank">Bernd Bestel</a>
|
||||||
|
<br />
|
||||||
|
Created with passion since 2017
|
||||||
|
<br />
|
||||||
|
Version <?php echo file_get_contents('version.txt'); ?>
|
||||||
|
<br />
|
||||||
|
Life runs on code
|
||||||
|
<br />
|
||||||
|
<a class="discrete-link" href="https://github.com/berrnd/grocy" target="_blank">
|
||||||
|
<i class="fa fa-github"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>Grocy.ContentPage = '<?php echo $contentPage; ?>';</script>
|
||||||
|
<?php include $contentPage; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="/bower_components/bootstrap/dist/js/bootstrap.min.js"></script>
|
||||||
|
<script src="/bower_components/bootbox/bootbox.js"></script>
|
||||||
|
<script src="/bower_components/jquery.serializeJSON/jquery.serializejson.min.js"></script>
|
||||||
|
<script src="/bower_components/bootstrap-datepicker/dist/js/bootstrap-datepicker.min.js"></script>
|
||||||
|
<script src="/bower_components/moment/min/moment.min.js"></script>
|
||||||
|
<script src="/bower_components/bootstrap-validator/dist/validator.min.js"></script>
|
||||||
|
<script src="/bower_components/bootstrap-combobox/js/bootstrap-combobox.js"></script>
|
||||||
|
|
||||||
|
<?php if (file_exists('views/' . str_replace('.php', '.js', $contentPage))) : ?>
|
||||||
|
<script src="/views/<?php echo str_replace('.php', '.js', $contentPage); ?>"></script>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (file_exists('data/add_before_end_body.html')) include 'data/add_before_end_body.html' ?>
|
||||||
|
</body>
|
||||||
|
</html>
|
36
views/locationform.js
Normal file
36
views/locationform.js
Normal file
@@ -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();
|
||||||
|
});
|
21
views/locationform.php
Normal file
21
views/locationform.php
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<div class="col-sm-3 col-sm-offset-3 col-md-4 col-md-offset-2 main">
|
||||||
|
<h1 class="page-header"><?php echo $title; ?></h1>
|
||||||
|
|
||||||
|
<script>Grocy.EditMode = '<?php echo $mode; ?>';</script>
|
||||||
|
|
||||||
|
<?php if ($mode == 'edit') : ?>
|
||||||
|
<script>Grocy.EditObjectId = <?php echo $location->id; ?>;</script>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<form id="location-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="name">Name</label>
|
||||||
|
<input type="text" class="form-control" id="name" name="name" value="<?php if ($mode == 'edit') echo $location->name; ?>" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="description">Description</label>
|
||||||
|
<textarea class="form-control" rows="2" id="description" name="description"><?php if ($mode == 'edit') echo $location->description; ?></textarea>
|
||||||
|
</div>
|
||||||
|
<button id="save-location-button" type="submit" class="btn btn-default">Save</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
32
views/locations.js
Normal file
32
views/locations.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
$(document).on('click', '.location-delete-button', function(e)
|
||||||
|
{
|
||||||
|
bootbox.confirm({
|
||||||
|
message: 'Delete location <strong>' + $(e.target).attr('data-location-name') + '</strong>?',
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
40
views/locations.php
Normal file
40
views/locations.php
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
|
||||||
|
<h1 class="page-header">
|
||||||
|
Locations
|
||||||
|
<a class="btn btn-default" href="/location/new" role="button">
|
||||||
|
<i class="fa fa-plus"></i> Add
|
||||||
|
</a>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>#</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Description</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($locations as $location) : ?>
|
||||||
|
<tr>
|
||||||
|
<td class="fit-content">
|
||||||
|
<a class="btn btn-info" href="/location/<?php echo $location->id; ?>" role="button">
|
||||||
|
<i class="fa fa-pencil"></i>
|
||||||
|
</a>
|
||||||
|
<a class="btn btn-danger location-delete-button" href="#" role="button" data-location-id="<?php echo $location->id; ?>" data-location-name="<?php echo $location->name; ?>">
|
||||||
|
<i class="fa fa-trash"></i>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<?php echo $location->name; ?>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<?php echo $location->description; ?>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
36
views/productform.js
Normal file
36
views/productform.js
Normal file
@@ -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();
|
||||||
|
});
|
58
views/productform.php
Normal file
58
views/productform.php
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<div class="col-sm-3 col-sm-offset-3 col-md-4 col-md-offset-2 main">
|
||||||
|
<h1 class="page-header"><?php echo $title; ?></h1>
|
||||||
|
|
||||||
|
<script>Grocy.EditMode = '<?php echo $mode; ?>';</script>
|
||||||
|
|
||||||
|
<?php if ($mode == 'edit') : ?>
|
||||||
|
<script>Grocy.EditObjectId = <?php echo $product->id; ?>;</script>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<form id="product-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="name">Name</label>
|
||||||
|
<input type="text" class="form-control" id="name" name="name" value="<?php if ($mode == 'edit') echo $product->name; ?>">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="barcode">Barcode</label>
|
||||||
|
<div class="input-group date">
|
||||||
|
<input type="text" class="form-control" id="barcode" name="barcode" value="<?php if ($mode == 'edit') echo $product->barcode; ?>">
|
||||||
|
<div class="input-group-addon">
|
||||||
|
<i class="fa fa-barcode"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="description">Description</label>
|
||||||
|
<textarea class="form-control" rows="2" id="description" name="description"><?php if ($mode == 'edit') echo $product->description; ?></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="location_id">Location</label>
|
||||||
|
<select class="form-control" id="location_id" name="location_id">
|
||||||
|
<?php foreach ($locations as $location) : ?>
|
||||||
|
<option <?php if ($mode == 'edit' && $location->id == $product->location_id) echo 'selected="selected"'; ?> value="<?php echo $location->id; ?>"><?php echo $location->name; ?></option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="qu_id_purchase">Quantity unit purchase</label>
|
||||||
|
<select class="form-control" id="qu_id_purchase" name="qu_id_purchase">
|
||||||
|
<?php foreach ($quantityunits as $quantityunit) : ?>
|
||||||
|
<option <?php if ($mode == 'edit' && $quantityunit->id == $product->qu_id_purchase) echo 'selected="selected"'; ?> value="<?php echo $quantityunit->id; ?>"><?php echo $quantityunit->name; ?></option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="qu_id_stock">Quantity unit stock</label>
|
||||||
|
<select class="form-control" id="qu_id_stock" name="qu_id_stock">
|
||||||
|
<?php foreach ($quantityunits as $quantityunit) : ?>
|
||||||
|
<option <?php if ($mode == 'edit' && $quantityunit->id == $product->qu_id_stock) echo 'selected="selected"'; ?> value="<?php echo $quantityunit->id; ?>"><?php echo $quantityunit->name; ?></option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="qu_factor_purchase_to_stock">Factor purchase to stock quantity unit</label>
|
||||||
|
<input type="text" class="form-control" id="qu_factor_purchase_to_stock" name="qu_factor_purchase_to_stock" value="<?php if ($mode == 'edit') echo $product->qu_factor_purchase_to_stock; else echo '1'; ?>">
|
||||||
|
</div>
|
||||||
|
<button id="save-product-button" type="submit" class="btn btn-default">Save</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
32
views/products.js
Normal file
32
views/products.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
$(document).on('click', '.product-delete-button', function(e)
|
||||||
|
{
|
||||||
|
bootbox.confirm({
|
||||||
|
message: 'Delete product <strong>' + $(e.target).attr('data-product-name') + '</strong>?',
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
56
views/products.php
Normal file
56
views/products.php
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
|
||||||
|
<h1 class="page-header">
|
||||||
|
Products
|
||||||
|
<a class="btn btn-default" href="/product/new" role="button">
|
||||||
|
<i class="fa fa-plus"></i> Add
|
||||||
|
</a>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>#</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Location</th>
|
||||||
|
<th>QU purchase</th>
|
||||||
|
<th>QU stock</th>
|
||||||
|
<th>QU factor</th>
|
||||||
|
<th>Description</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($products as $product) : ?>
|
||||||
|
<tr>
|
||||||
|
<td class="fit-content">
|
||||||
|
<a class="btn btn-info" href="/product/<?php echo $product->id; ?>" role="button">
|
||||||
|
<i class="fa fa-pencil"></i>
|
||||||
|
</a>
|
||||||
|
<a class="btn btn-danger product-delete-button" href="#" role="button" data-product-id="<?php echo $product->id; ?>" data-product-name="<?php echo $product->name; ?>">
|
||||||
|
<i class="fa fa-trash"></i>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<?php echo $product->name; ?>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<?php echo Grocy::FindObjectInArrayByPropertyValue($locations, 'id', $product->location_id)->name; ?>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<?php echo Grocy::FindObjectInArrayByPropertyValue($quantityunits, 'id', $product->qu_id_purchase)->name; ?>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<?php echo Grocy::FindObjectInArrayByPropertyValue($quantityunits, 'id', $product->qu_id_stock)->name; ?>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<?php echo $product->qu_factor_purchase_to_stock; ?>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<?php echo $product->description; ?>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
75
views/purchase.js
Normal file
75
views/purchase.js
Normal file
@@ -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');
|
||||||
|
});
|
49
views/purchase.php
Normal file
49
views/purchase.php
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<div class="col-sm-3 col-sm-offset-3 col-md-3 col-md-offset-2 main">
|
||||||
|
<h1 class="page-header">Record purchase</h1>
|
||||||
|
|
||||||
|
<form id="purchase-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="product_id">Product</label>
|
||||||
|
<select class="form-control combobox" id="product_id" name="product_id" required>
|
||||||
|
<option value=""></option>
|
||||||
|
<?php foreach ($products as $product) : ?>
|
||||||
|
<option value="<?php echo $product->id; ?>"><?php echo $product->name; ?></option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
<div class="input-group date">
|
||||||
|
<input type="text" class="form-control" id="barcode" name="barcode" />
|
||||||
|
<div class="input-group-addon">
|
||||||
|
<i class="fa fa-barcode"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="help-block with-errors"></div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="amount">Amount</label>
|
||||||
|
<input type="number" class="form-control" id="amount" name="amount" value="1" min="1" required>
|
||||||
|
<div class="help-block with-errors"></div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="best_before_date">Best before</label>
|
||||||
|
<div class="input-group date">
|
||||||
|
<input type="text" class="form-control datepicker" id="best_before_date" name="best_before_date" required>
|
||||||
|
<div class="input-group-addon">
|
||||||
|
<i class="fa fa-calendar"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="help-block with-errors"></div>
|
||||||
|
</div>
|
||||||
|
<button id="save-purchase-button" type="submit" class="btn btn-default">OK</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-sm-3 col-md-3 main well">
|
||||||
|
<h3>Product overview <strong><span id="selected-product-name"></span></strong></h3>
|
||||||
|
<h4><strong>Purchase quantity:</strong> <span id="selected-product-purchase-qu-name"></span></h4>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<strong>Stock amount:</strong> <span id="selected-product-stock-amount"></span> <span id="selected-product-stock-qu-name"></span><br />
|
||||||
|
<strong>Last purchased:</strong> <span id="selected-product-last-purchased"></span><br />
|
||||||
|
<strong>Last used:</strong> <span id="selected-product-last-used"></span>
|
||||||
|
</p>
|
||||||
|
</div>
|
36
views/quantityunitform.js
Normal file
36
views/quantityunitform.js
Normal file
@@ -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();
|
||||||
|
});
|
21
views/quantityunitform.php
Normal file
21
views/quantityunitform.php
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<div class="col-sm-3 col-sm-offset-3 col-md-4 col-md-offset-2 main">
|
||||||
|
<h1 class="page-header"><?php echo $title; ?></h1>
|
||||||
|
|
||||||
|
<script>Grocy.EditMode = '<?php echo $mode; ?>';</script>
|
||||||
|
|
||||||
|
<?php if ($mode == 'edit') : ?>
|
||||||
|
<script>Grocy.EditObjectId = <?php echo $quantityunit->id; ?>;</script>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<form id="quantityunit-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="name">Name</label>
|
||||||
|
<input type="text" class="form-control" id="name" name="name" value="<?php if ($mode == 'edit') echo $quantityunit->name; ?>" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="description">Description</label>
|
||||||
|
<textarea class="form-control" rows="2" id="description" name="description"><?php if ($mode == 'edit') echo $quantityunit->description; ?></textarea>
|
||||||
|
</div>
|
||||||
|
<button id="save-quantityunit-button" type="submit" class="btn btn-default">Save</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
32
views/quantityunits.js
Normal file
32
views/quantityunits.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
$(document).on('click', '.quantityunit-delete-button', function(e)
|
||||||
|
{
|
||||||
|
bootbox.confirm({
|
||||||
|
message: 'Delete quantity unit <strong>' + $(e.target).attr('data-quantityunit-name') + '</strong>?',
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
40
views/quantityunits.php
Normal file
40
views/quantityunits.php
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
|
||||||
|
<h1 class="page-header">
|
||||||
|
Quantity units
|
||||||
|
<a class="btn btn-default" href="/quantityunit/new" role="button">
|
||||||
|
<i class="fa fa-plus"></i> Add
|
||||||
|
</a>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>#</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Description</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($quantityunits as $quantityunit) : ?>
|
||||||
|
<tr>
|
||||||
|
<td class="fit-content">
|
||||||
|
<a class="btn btn-info" href="/quantityunit/<?php echo $quantityunit->id; ?>" role="button">
|
||||||
|
<i class="fa fa-pencil"></i>
|
||||||
|
</a>
|
||||||
|
<a class="btn btn-danger quantityunit-delete-button" href="#" role="button" data-quantityunit-id="<?php echo $quantityunit->id; ?>" data-quantityunit-name="<?php echo $quantityunit->name; ?>">
|
||||||
|
<i class="fa fa-trash"></i>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<?php echo $quantityunit->name; ?>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<?php echo $quantityunit->description; ?>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
Reference in New Issue
Block a user