diff --git a/controllers/StockController.php b/controllers/StockController.php index 26ec8527..f75e6d0a 100644 --- a/controllers/StockController.php +++ b/controllers/StockController.php @@ -515,6 +515,53 @@ class StockController extends BaseController ]); } + public function StockMetricsPurchases(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) + { + if (isset($request->getQueryParams()['start_date']) AND isset($request->getQueryParams()['end_date'])) + { + $start_date = $request->getQueryParams()['start_date']; + $end_date = $request->getQueryParams()['end_date']; + $where = "purchased_date >= '$start_date' AND purchased_date <= '$end_date'"; + } + else + { + // Default this month + $where = "purchased_date >= DATE(DATE('now', 'localtime'), 'start of month')"; + } + + + if (isset($request->getQueryParams()['byGroup'])) + { + $sql = " + SELECT product_group_id as id, product_group as name, sum(quantity * price) as total + FROM product_purchase_history + where $where + GROUP BY product_group + ORDER BY product_group + "; + } else { + if (isset($request->getQueryParams()['product_group']) AND $request->getQueryParams()['product_group'] != 'all') + { + $where = $where . ' AND product_group_id = ' . $request->getQueryParams()['product_group']; + } + + $sql = " + SELECT product_id as id, product_name as name, product_group_id as group_id, product_group as group_name, sum(quantity * price) as total + FROM product_purchase_history + WHERE $where + GROUP BY product_name + ORDER BY product_name + "; + } + + return $this->renderPage($response, 'stockmetricspurchases', [ + 'metrics' => $this->getDatabaseService()->ExecuteDbQuery($sql)->fetchAll(\PDO::FETCH_OBJ), + 'productGroups' => $this->getDatabase()->product_groups()->orderBy('name', 'COLLATE NOCASE'), + 'selectedGroup' => isset($request->getQueryParams()['product_group']) ? $request->getQueryParams()['product_group'] : null, + 'byGroup' => isset($request->getQueryParams()['byGroup']) ? $request->getQueryParams()['byGroup'] : null + ]); + } + public function Transfer(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) { return $this->renderPage($response, 'transfer', [ diff --git a/localization/strings.pot b/localization/strings.pot index 92d05e62..64cb762d 100644 --- a/localization/strings.pot +++ b/localization/strings.pot @@ -2371,3 +2371,18 @@ msgstr "" msgid "Quick open amount" msgstr "" + +msgid "Metrics" +msgstr "" + +msgid "Stock Metrics: Purchases" +msgstr "" + +msgid "by Product" +msgstr "" + +msgid "by Group" +msgstr "" + +msgid "Total" +msgstr "" diff --git a/package.json b/package.json index f9233c4c..312f77ee 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "bootstrap": "^4.5.2", "bootstrap-select": "^1.13.18", "bwip-js": "^3.0.1", + "canvasjs": "^1.8.3", "chart.js": "^2.8.0", "datatables.net": "^1.10.22", "datatables.net-bs4": "^1.10.22", @@ -21,6 +22,7 @@ "datatables.net-rowgroup-bs4": "^1.1.2", "datatables.net-select": "^1.3.1", "datatables.net-select-bs4": "^1.3.1", + "daterangepicker": "dangrossman/daterangepicker", "fullcalendar": "^3.10.1", "gettext-translator": "2.1.0", "jquery": "^3.6.0", diff --git a/public/viewjs/metrics.js b/public/viewjs/metrics.js new file mode 100644 index 00000000..3dc85807 --- /dev/null +++ b/public/viewjs/metrics.js @@ -0,0 +1,96 @@ +/* + * Metrics Javascript + */ + +/* Charting */ +var dataPoints = []; +$("#metrics-table tbody tr").each(function () { + var self = $(this); + var label = self.find("td:eq(0)").attr('data-chart-label'); + var value = Number(self.find("td:eq(1)").attr('data-chart-value')); + var dataPoint = { label: label, y: parseFloat((Math.round(value * 100) / 100).toFixed(2))}; + dataPoints.push(dataPoint); +}); + +var options = { + exportEnabled: true, + legend:{ + horizontalAlign: "center", + verticalAlign: "bottom" + }, + data: [{ + type: "pie", + showInLegend: true, + toolTipContent: "{label}: ${y} (#percent%)", + indexLabel: "{label}", + legendText: "{label} (#percent%)", + indexLabelPlacement: "outside", + valueFormatSTringt: "#,##0.##", + dataPoints: dataPoints + }] +}; + +// needed for recursionCount error +recursionCount=0; +$("#metrics-chart").CanvasJSChart(options); + +/* DataTables */ +var metricsTable = $('#metrics-table').DataTable({ + "columnDefs": [ + { "type": "num", "targets": 1 } + ] +}); +$('#metrics-table tbody').removeClass("d-none"); +metricsTable.columns.adjust().draw(); + +/* DateRangePicker */ +const urlParams = new URLSearchParams(window.location.search); + +var start_date = moment().startOf("month").format('MM/DD/YYYY'); +var end_date = moment().endOf("month").format('MM/DD/YYYY'); + +if (urlParams.get('start_date')) start_date = moment(urlParams.get('start_date')) ; +if (urlParams.get('end_date')) end_date = moment(urlParams.get('end_date')); + +$('#daterange-filter').daterangepicker({ + showDropdowns: true, + startDate: start_date, + endDate: end_date, + locale: { + "format": 'MM/DD/YYYY', + "firstDay": 1 + }, + ranges: { + 'Today': [moment(), moment()], + 'Yesterday': [moment().subtract(1, 'days'), moment().subtract(1, 'days')], + 'Last 7 Days': [moment().subtract(6, 'days'), moment()], + 'Last 14 Days': [moment().subtract(13, 'days'), moment()], + 'Last 30 Days': [moment().subtract(29, 'days'), moment()], + 'This Month': [moment().startOf('month'), moment().endOf('month')], + 'Last Month': [moment().subtract(1, 'month').startOf('month'), moment().subtract(1, 'month').endOf('month')], + 'This Year': [moment().startOf('year'), moment().endOf('year')], + 'Last Year': [moment().subtract(1, 'year').startOf('year'), moment().subtract(1, 'year').endOf('year')] + }}, function(start, end, label) { + UpdateUriParam("start_date", start.format('YYYY-MM-DD')); + UpdateUriParam("end_date", end.format('YYYY-MM-DD')) + window.location.reload(); +}); + +$('#daterange-filter').on('cancel.daterangepicker', function(ev, picker) +{ + $(this).val(start_date + ' - ' + end_date); +}); + +$("#clear-filter-button").on("click", function() +{ + RemoveUriParam("start_date"); + RemoveUriParam("end_date"); + RemoveUriParam("product_group"); + window.location.reload(); +}); + +$("#product-group-filter").on("change", function() +{ + UpdateUriParam("product_group", $(this).val()); + window.location.reload(); +}); diff --git a/routes.php b/routes.php index b0b2710d..00c1ad81 100644 --- a/routes.php +++ b/routes.php @@ -56,6 +56,7 @@ $app->group('', function (RouteCollectorProxy $group) { $group->get('/locations', '\Grocy\Controllers\StockController:LocationsList'); $group->get('/location/{locationId}', '\Grocy\Controllers\StockController:LocationEditForm'); $group->get('/stockjournal', '\Grocy\Controllers\StockController:Journal'); + $group->get('/stockmetricspurchases', '\Grocy\Controllers\StockController:StockMetricsPurchases'); $group->get('/locationcontentsheet', '\Grocy\Controllers\StockController:LocationContentSheet'); $group->get('/quantityunitpluraltesting', '\Grocy\Controllers\StockController:QuantityUnitPluralFormTesting'); $group->get('/stockjournal/summary', '\Grocy\Controllers\StockController:JournalSummary'); diff --git a/services/DemoDataGeneratorService.php b/services/DemoDataGeneratorService.php index c057b7b6..e202d793 100644 --- a/services/DemoDataGeneratorService.php +++ b/services/DemoDataGeneratorService.php @@ -66,6 +66,8 @@ class DemoDataGeneratorService extends BaseService INSERT INTO quantity_units (id, name, name_plural) VALUES (12, '{$this->__n_sql(1, 'Slice', 'Slices')}', '{$this->__n_sql(2, 'Slice', 'Slices')}'); --12 DELETE FROM quantity_units WHERE name = '{$this->__t_sql('Kilogram')}'; INSERT INTO quantity_units (id, name, name_plural) VALUES (13, '{$this->__n_sql(1, 'Kilogram', 'Kilograms')}', '{$this->__n_sql(2, 'Kilogram', 'Kilograms')}'); --13 + DELETE FROM quantity_units WHERE name = '{$this->__t_sql('pint')}'; + INSERT INTO quantity_units (id, name, name_plural) VALUES (14, '{$this->__n_sql(1, 'Pint', 'Pints')}', '{$this->__n_sql(2, 'Pint', 'Pint')}'); --14 INSERT INTO product_groups(name) VALUES ('01 {$this->__t_sql('Sweets')}'); --1 INSERT INTO product_groups(name) VALUES ('02 {$this->__t_sql('Bakery products')}'); --2 @@ -73,13 +75,14 @@ class DemoDataGeneratorService extends BaseService INSERT INTO product_groups(name) VALUES ('04 {$this->__t_sql('Butchery products')}'); --4 INSERT INTO product_groups(name) VALUES ('05 {$this->__t_sql('Vegetables/Fruits')}'); --5 INSERT INTO product_groups(name) VALUES ('06 {$this->__t_sql('Refrigerated products')}'); --6 + INSERT INTO product_groups(name) VALUES ('07 {$this->__t_sql('Beverages')}'); --7' DELETE FROM sqlite_sequence WHERE name = 'products'; --Just to keep IDs in order as mentioned here... INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, min_stock_amount, product_group_id, picture_file_name) VALUES ('{$this->__t_sql('Cookies')}', 4, 3, 3, 8, 1, 'cookies.jpg'); --1 INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, min_stock_amount, product_group_id, cumulate_min_stock_amount_of_sub_products) VALUES ('{$this->__t_sql('Chocolate')}', 4, 3, 3, 8, 1, 1); --2 INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, min_stock_amount, product_group_id, picture_file_name) VALUES ('{$this->__t_sql('Gummy bears')}', 4, 3, 3, 8, 1, 'gummybears.jpg'); --3 INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, min_stock_amount, product_group_id) VALUES ('{$this->__t_sql('Crisps')}', 4, 3, 3, 10, 1); --4 - INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, product_group_id) VALUES ('{$this->__t_sql('Eggs')}', 2, 3, 2, 5); --5 + INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, product_group_id) VALUES ('{$this->__t_sql('Eggs')}', 2, 3, 2, 6); --5 INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, product_group_id) VALUES ('{$this->__t_sql('Noodles')}', 3, 3, 3, 6); --6 INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, product_group_id) VALUES ('{$this->__t_sql('Pickles')}', 5, 4, 4, 3); --7 INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, product_group_id) VALUES ('{$this->__t_sql('Gulash soup')}', 5, 5, 5, 3); --8 @@ -101,6 +104,10 @@ class DemoDataGeneratorService extends BaseService INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, product_group_id, parent_product_id) VALUES ('{$this->__t_sql('Milk Chocolate')}', 4, 3, 3, 1, 2); --24 INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, product_group_id, parent_product_id) VALUES ('{$this->__t_sql('Dark Chocolate')}', 4, 3, 3, 1, 2); --25 INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, product_group_id) VALUES ('{$this->__t_sql('Waffle rolls')}', 4, 3, 3, 1); --26 + INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, product_group_id) VALUES ('{$this->__t_sql('Ice Cream')}', 6, 14, 14, 1); --27 + INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, product_group_id) VALUES ('{$this->__t_sql('Soda')}', 2, 6, 6, 7); --28 + INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, product_group_id) VALUES ('{$this->__t_sql('Beer')}', 2, 6, 6, 7); --29 + UPDATE products SET calories = 123 WHERE IFNULL(calories, 0) = 0; INSERT INTO product_barcodes (product_id, barcode) VALUES (8, '22111968'); @@ -289,6 +296,16 @@ class DemoDataGeneratorService extends BaseService $stockService->AddProduct(24, 2, date('Y-m-d', strtotime('+180 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice(), null, $this->NextSupermarketId(), $stockTransactionId); $stockService->AddProduct(25, 2, date('Y-m-d', strtotime('+180 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice(), null, $this->NextSupermarketId(), $stockTransactionId); $stockService->AddProduct(2, 1, date('Y-m-d', strtotime('+180 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice(), null, $this->NextSupermarketId(), $stockTransactionId); + $stockService->AddProduct(27, 1, date('Y-m-d', strtotime('+30 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('now')), $this->RandomPrice(), null, $this->NextSupermarketId(), $stockTransactionId); + $stockService->AddProduct(23, 1, date('Y-m-d', strtotime('+60 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('now')), $this->RandomPrice(), null, $this->NextSupermarketId(), $stockTransactionId); + $stockService->AddProduct(27, 1, date('Y-m-d', strtotime('+30 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-2 weeks')), $this->RandomPrice(), null, $this->NextSupermarketId(), $stockTransactionId); + $stockService->AddProduct(27, 1, date('Y-m-d', strtotime('+30 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-3 weeks')), $this->RandomPrice(), null, $this->NextSupermarketId(), $stockTransactionId); + $stockService->AddProduct(28, 12, date('Y-m-d', strtotime('+180 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-1 weeks')), $this->RandomPrice(), null, $this->NextSupermarketId(), $stockTransactionId); + $stockService->AddProduct(29, 12, date('Y-m-d', strtotime('+365 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-2 weeks')), $this->RandomPrice(), null, $this->NextSupermarketId(), $stockTransactionId); + $stockService->AddProduct(5, 1, date('Y-m-d', strtotime('+1 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-1 days')), $this->RandomPrice(), null, $this->NextSupermarketId(), $stockTransactionId); + $stockService->AddProduct(1, 12, date('Y-m-d', strtotime('+180 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-1 days')), $this->RandomPrice(), null, $this->NextSupermarketId(), $stockTransactionId); + $stockService->AddProduct(2, 12, date('Y-m-d', strtotime('+365 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-1 days')), $this->RandomPrice(), null, $this->NextSupermarketId(), $stockTransactionId); + $stockService->AddMissingProductsToShoppingList(); $stockService->OpenProduct(3, 1); $stockService->OpenProduct(6, 1); diff --git a/views/layout/default.blade.php b/views/layout/default.blade.php index f4acb23f..4ac27c16 100644 --- a/views/layout/default.blade.php +++ b/views/layout/default.blade.php @@ -71,6 +71,8 @@ rel="stylesheet"> + @endif + + diff --git a/views/stockmetricspurchases.blade.php b/views/stockmetricspurchases.blade.php new file mode 100644 index 00000000..6a3f326f --- /dev/null +++ b/views/stockmetricspurchases.blade.php @@ -0,0 +1,121 @@ +@extends('layout.default') + +@section('title', $__t('Stock Metrics: Purchases')) +@section('activeNav', 'stockmetricspurchases') +@section('viewJsName', 'metrics') + +@section('content') +
| {{ $__t('Name') }} | +{{ $__t('Total') }} | + @if(!$byGroup) +{{ $__t('Product group') }} | + @endif +
|---|---|---|
| + {{ $metric->name }} + | ++ {{ $metric->total }} + | + @if(!$byGroup) ++ {{ $metric->group_name }} + | + @endif +