mirror of
https://github.com/grocy/grocy.git
synced 2025-04-28 17:23:56 +00:00
Feature: Stock Purchase Metrics (#2135)
* Feature: Stock Purchase Metrics * chart update * Refactor to chartjs * More suggestion edits - locale in javascript - global translations - commit migrations sql file * Rename 0215.sql to 0216.sql Fixed merge conflict * Fixed merge conflict * Applied code style * Added missing demo data translations * Removed unused package "canvasjs" * Don't include daterangepicker globally when only needed on a single page / fixed view section imports * Rename this to "Spendings" / name it more generically "Stock reports" * Reuse the existing product_price_history view * Final cleanup * Whitespace fix --------- Co-authored-by: Travis Raup <travis.raup@platform.sh> Co-authored-by: Bernd Bestel <bernd@berrnd.de>
This commit is contained in:
parent
98469248eb
commit
340832c361
71
controllers/StockReportsController.php
Normal file
71
controllers/StockReportsController.php
Normal file
@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace Grocy\Controllers;
|
||||
|
||||
class StockReportsController extends BaseController
|
||||
{
|
||||
public function Spendings(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
|
||||
{
|
||||
if (isset($request->getQueryParams()['start_date']) && isset($request->getQueryParams()['end_date']) && IsIsoDate($request->getQueryParams()['start_date']) && IsIsoDate($request->getQueryParams()['end_date']))
|
||||
{
|
||||
$startDate = $request->getQueryParams()['start_date'];
|
||||
$endDate = $request->getQueryParams()['end_date'];
|
||||
$where = "pph.purchased_date BETWEEN '$startDate' AND '$endDate'";
|
||||
}
|
||||
else
|
||||
{
|
||||
// Default to this month
|
||||
$where = "pph.purchased_date >= DATE(DATE('now', 'localtime'), 'start of month')";
|
||||
}
|
||||
|
||||
|
||||
if (isset($request->getQueryParams()['byGroup']))
|
||||
{
|
||||
$sql = "
|
||||
SELECT
|
||||
pg.id AS id,
|
||||
pg.name AS name,
|
||||
SUM(pph.amount * pph.price) AS total
|
||||
FROM product_price_history pph
|
||||
JOIN products p
|
||||
ON pph.product_id = p.id
|
||||
JOIN product_groups pg
|
||||
ON p.product_group_id = pg.id
|
||||
WHERE $where
|
||||
GROUP BY pg.id
|
||||
ORDER BY pg.NAME COLLATE NOCASE
|
||||
";
|
||||
}
|
||||
else
|
||||
{
|
||||
if (isset($request->getQueryParams()['product_group']) and $request->getQueryParams()['product_group'] != 'all')
|
||||
{
|
||||
$where .= ' AND pg.id = ' . $request->getQueryParams()['product_group'];
|
||||
}
|
||||
|
||||
$sql = "
|
||||
SELECT
|
||||
p.id AS id,
|
||||
p.name AS name,
|
||||
pg.id AS group_id,
|
||||
pg.name AS group_name,
|
||||
SUM(pph.amount * pph.price) AS total
|
||||
FROM product_price_history pph
|
||||
JOIN products p
|
||||
ON pph.product_id = p.id
|
||||
JOIN product_groups pg
|
||||
ON p.product_group_id = pg.id
|
||||
WHERE $where
|
||||
GROUP BY p.id
|
||||
ORDER BY p.NAME COLLATE NOCASE
|
||||
";
|
||||
}
|
||||
|
||||
return $this->renderPage($response, 'stockreportspendings', [
|
||||
'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
|
||||
]);
|
||||
}
|
||||
}
|
@ -388,3 +388,18 @@ msgstr[1] ""
|
||||
|
||||
msgid "Romanian"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pint"
|
||||
msgstr ""
|
||||
|
||||
msgid "Beverages"
|
||||
msgstr ""
|
||||
|
||||
msgid "Ice Cream"
|
||||
msgstr ""
|
||||
|
||||
msgid "Soda"
|
||||
msgstr ""
|
||||
|
||||
msgid "Beer"
|
||||
msgstr ""
|
||||
|
@ -2374,3 +2374,39 @@ msgstr ""
|
||||
|
||||
msgid "Track chore execution now"
|
||||
msgstr ""
|
||||
|
||||
msgid "Total"
|
||||
msgstr ""
|
||||
|
||||
msgid "Apply"
|
||||
msgstr ""
|
||||
|
||||
msgid "Custom range"
|
||||
msgstr ""
|
||||
|
||||
msgid "Yesterday"
|
||||
msgstr ""
|
||||
|
||||
msgid "Last %1$s days"
|
||||
msgstr ""
|
||||
|
||||
msgid "This month"
|
||||
msgstr ""
|
||||
|
||||
msgid "Last month"
|
||||
msgstr ""
|
||||
|
||||
msgid "This year"
|
||||
msgstr ""
|
||||
|
||||
msgid "Last year"
|
||||
msgstr ""
|
||||
|
||||
msgid "Reports"
|
||||
msgstr ""
|
||||
|
||||
msgid "Spendings"
|
||||
msgstr ""
|
||||
|
||||
msgid "Stock report"
|
||||
msgstr ""
|
||||
|
25
migrations/0216.sql
Normal file
25
migrations/0216.sql
Normal file
@ -0,0 +1,25 @@
|
||||
DROP VIEW product_price_history;
|
||||
CREATE VIEW product_price_history
|
||||
AS
|
||||
SELECT
|
||||
sl.product_id AS id, -- Dummy, LessQL needs an id column
|
||||
sl.product_id,
|
||||
sl.price,
|
||||
sl.amount,
|
||||
sl.purchased_date,
|
||||
sl.shopping_location_id
|
||||
FROM stock_log sl
|
||||
WHERE sl.transaction_type IN ('purchase', 'inventory-correction', 'stock-edit-new')
|
||||
AND sl.undone = 0
|
||||
AND IFNULL(sl.price, 0) > 0
|
||||
AND IFNULL(sl.amount, 0) > 0
|
||||
AND sl.id NOT IN (
|
||||
-- These are edited purchase and inventory-correction rows
|
||||
SELECT sl_origin.id
|
||||
FROM stock_log sl_origin
|
||||
JOIN stock_log sl_edit
|
||||
ON sl_origin.stock_id = sl_edit.stock_id
|
||||
AND sl_edit.transaction_type = 'stock-edit-new'
|
||||
AND sl_edit.id > sl_origin.id
|
||||
WHERE sl_origin.transaction_type IN ('purchase', 'inventory-correction')
|
||||
);
|
@ -12,6 +12,9 @@
|
||||
"bootstrap-select": "^1.13.18",
|
||||
"bwip-js": "^3.0.1",
|
||||
"chart.js": "^2.8.0",
|
||||
"chartjs-plugin-colorschemes": "^0.4.0",
|
||||
"chartjs-plugin-doughnutlabel": "^2.0.3",
|
||||
"chartjs-plugin-piechart-outlabels": "^0.1.4",
|
||||
"datatables.net": "^1.10.22",
|
||||
"datatables.net-bs4": "^1.10.22",
|
||||
"datatables.net-colreorder": "^1.5.2",
|
||||
@ -21,6 +24,7 @@
|
||||
"datatables.net-rowgroup-bs4": "^1.1.2",
|
||||
"datatables.net-select": "^1.3.1",
|
||||
"datatables.net-select-bs4": "^1.3.1",
|
||||
"daterangepicker": "^3.1.0",
|
||||
"fullcalendar": "^3.10.1",
|
||||
"gettext-translator": "2.1.0",
|
||||
"jquery": "^3.6.0",
|
||||
|
145
public/viewjs/stockreportspendings.js
Normal file
145
public/viewjs/stockreportspendings.js
Normal file
@ -0,0 +1,145 @@
|
||||
var labels = [];
|
||||
var data = [];
|
||||
var totalAmount = 0.0;
|
||||
$("#metrics-table tbody tr").each(function()
|
||||
{
|
||||
var self = $(this);
|
||||
labels.push(self.find("td:eq(0)").attr("data-chart-label"));
|
||||
var itemTotal = Number.parseFloat(self.find("td:eq(1)").attr("data-chart-value"));
|
||||
data.push(itemTotal);
|
||||
totalAmount += + itemTotal;
|
||||
});
|
||||
totalAmount = totalAmount.toLocaleString(undefined, { style: "currency", currency: Grocy.Currency });
|
||||
|
||||
var backgroundColors = [];
|
||||
var colorChoiceIndex = 0;
|
||||
for (i = 0; i < data.length; i++)
|
||||
{
|
||||
if (i + 1 == Chart.colorschemes.brewer.Paired12.length)
|
||||
{
|
||||
// Restart background color choices
|
||||
colorChoiceIndex = 1;
|
||||
}
|
||||
backgroundColors.push(Chart.colorschemes.brewer.Paired12[colorChoiceIndex]);
|
||||
colorChoiceIndex++;
|
||||
}
|
||||
|
||||
var metricsChart = new Chart("metrics-chart", {
|
||||
"type": "outlabeledDoughnut",
|
||||
"options": {
|
||||
"legend": {
|
||||
"display": false
|
||||
},
|
||||
"tooltips": {
|
||||
"enabled": false
|
||||
},
|
||||
"tooltips": {
|
||||
"enabled": false
|
||||
},
|
||||
"plugins": {
|
||||
"outlabels": {
|
||||
"text": "%l %p",
|
||||
"backgroundColor": "#343a40",
|
||||
"font": {
|
||||
"minSize": 12,
|
||||
"maxSize": 18
|
||||
}
|
||||
},
|
||||
"doughnutlabel": {
|
||||
"labels": [
|
||||
{
|
||||
"text": totalAmount,
|
||||
"font": {
|
||||
"size": 24,
|
||||
"weight": "bold"
|
||||
},
|
||||
},
|
||||
{
|
||||
"text": __t("Total")
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"data": {
|
||||
"labels": labels,
|
||||
"datasets": [{
|
||||
"data": data,
|
||||
"backgroundColor": backgroundColors
|
||||
}]
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
var metricsTable = $("#metrics-table").DataTable({
|
||||
"columnDefs": [
|
||||
{ "type": "num", "targets": 1 }
|
||||
].concat($.fn.dataTable.defaults.columnDefs)
|
||||
});
|
||||
$("#metrics-table tbody").removeClass("d-none");
|
||||
metricsTable.columns.adjust().draw();
|
||||
|
||||
var startDate = moment().startOf("month").format("YYYY-MM-DD");
|
||||
var endDate = moment().endOf("month").format("YYYY-MM-DD");
|
||||
if (GetUriParam("start_date"))
|
||||
{
|
||||
startDate = moment(GetUriParam("start_date"));
|
||||
}
|
||||
if (GetUriParam("end_date"))
|
||||
{
|
||||
endDate = moment(GetUriParam("end_date"));
|
||||
}
|
||||
|
||||
var ranges = {};
|
||||
ranges[__t("Today")] = [moment(), moment()];
|
||||
ranges[__t("Yesterday")] = [moment().subtract(1, "days"), moment().subtract(1, "days")];
|
||||
ranges[__t("Last %1$s days", "7")] = [moment().subtract(6, "days"), moment()];
|
||||
ranges[__t("Last %1$s days", "14")] = [moment().subtract(13, "days"), moment()];
|
||||
ranges[__t("Last %1$s days", "30")] = [moment().subtract(29, "days"), moment()];
|
||||
ranges[__t("This month")] = [moment().startOf("month"), moment().endOf("month")];
|
||||
ranges[__t("Last month")] = [moment().subtract(1, "month").startOf("month"), moment().subtract(1, "month").endOf("month")];
|
||||
ranges[__t("This year")] = [moment().startOf("year"), moment().endOf("year")];
|
||||
ranges[__t("Last year")] = [moment().subtract(1, "year").startOf("year"), moment().subtract(1, "year").endOf("year")];
|
||||
|
||||
$("#daterange-filter").daterangepicker({
|
||||
"showDropdowns": true,
|
||||
"alwaysShowCalendars": true,
|
||||
"buttonClasses": "btn",
|
||||
"applyButtonClasses": "btn-primary",
|
||||
"cancelButtonClasses": "btn-secondary",
|
||||
"startDate": startDate,
|
||||
"endDate": endDate,
|
||||
"showWeekNumbers": Grocy.CalendarShowWeekNumbers,
|
||||
"locale": {
|
||||
"format": "YYYY-MM-DD",
|
||||
"firstDay": Grocy.CalendarFirstDayOfWeek
|
||||
},
|
||||
"applyLabel": __t("Apply"),
|
||||
"cancelLabel": __t("Cancel"),
|
||||
"customRangeLabel": __t("Custom range"),
|
||||
"ranges": ranges
|
||||
}, 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(startDate + " - " + endDate);
|
||||
});
|
||||
|
||||
$("#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();
|
||||
});
|
@ -63,6 +63,8 @@ $app->group('', function (RouteCollectorProxy $group) {
|
||||
$group->get('/stockentry/{entryId}/grocycode', '\Grocy\Controllers\StockController:StockEntryGrocycodeImage');
|
||||
$group->get('/stockentry/{entryId}/label', '\Grocy\Controllers\StockController:StockEntryGrocycodeLabel');
|
||||
$group->get('/quantityunitconversionsresolved', '\Grocy\Controllers\StockController:QuantityUnitConversionsResolved');
|
||||
|
||||
$group->get('/stockreports/spendings', '\Grocy\Controllers\StockReportsController:Spendings');
|
||||
}
|
||||
|
||||
// Stock price tracking
|
||||
|
@ -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);
|
||||
|
@ -46,6 +46,19 @@
|
||||
{{ $__t('Location Content Sheet') }}
|
||||
</a>
|
||||
@endif
|
||||
@if(GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)
|
||||
<div class="dropdown">
|
||||
<a class="btn btn-outline-dark responsive-button m-1 mt-md-0 mb-md-0 float-right dropdown-toggle"
|
||||
href="#"
|
||||
data-toggle="dropdown">
|
||||
{{ $__t('Reports') }}
|
||||
</a>
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item"
|
||||
href="{{ $U('/stockreports/spendings') }}">{{ $__t('Spendings') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="border-top border-bottom my-2 py-1">
|
||||
|
141
views/stockreportspendings.blade.php
Normal file
141
views/stockreportspendings.blade.php
Normal file
@ -0,0 +1,141 @@
|
||||
@extends('layout.default')
|
||||
|
||||
@section('title', $__t('Stock report') . ' / ' . $__t('Spendings'))
|
||||
@section('viewJsName', 'stockreportspendings')
|
||||
|
||||
@push('pageScripts')
|
||||
<script src="{{ $U('/node_modules/chart.js/dist/Chart.min.js?v=', true) }}{{ $version }}"></script>
|
||||
<script src="{{ $U('/node_modules/chartjs-plugin-colorschemes/dist/chartjs-plugin-colorschemes.min.js?v=', true) }}{{ $version}}"></script>
|
||||
<script src="{{ $U('/node_modules/chartjs-plugin-doughnutlabel/dist/chartjs-plugin-doughnutlabel.js?v=', true) }}{{ $version }}"></script>
|
||||
<script src="{{ $U('/node_modules/chartjs-plugin-piechart-outlabels/dist/chartjs-plugin-piechart-outlabels.min.js?v=', true) }}{{ $version}}"></script>
|
||||
<script src="{{ $U('/node_modules/daterangepicker/daterangepicker.js?v=', true) }}{{ $version }}"></script>
|
||||
@endpush
|
||||
|
||||
@push('pageStyles')
|
||||
<link href="{{ $U('/node_modules/daterangepicker/daterangepicker.css?v=', true) }}{{ $version }}"
|
||||
rel="stylesheet">
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="title-related-links">
|
||||
<h2 class="title mr-2 order-0">
|
||||
@yield('title')
|
||||
</h2>
|
||||
<div class="float-right">
|
||||
<button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3"
|
||||
type="button"
|
||||
data-toggle="collapse"
|
||||
data-target="#table-filter-row">
|
||||
<i class="fa-solid fa-filter"></i>
|
||||
</button>
|
||||
<button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3"
|
||||
type="button"
|
||||
data-toggle="collapse"
|
||||
data-target="#related-links">
|
||||
<i class="fa-solid fa-ellipsis-v"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="related-links collapse d-md-flex order-2 width-xs-sm-100"
|
||||
id="related-links">
|
||||
<a class="btn btn-link responsive-button m-1 mt-md-0 mb-md-0 @if(!$byGroup) active @endif discrete-link disabled"
|
||||
href="#">
|
||||
{{ $__t('Group by') }}:
|
||||
</a>
|
||||
<a class="btn btn-outline-dark responsive-button m-1 mt-md-0 mb-md-0 float-right @if(!$byGroup) active @endif"
|
||||
href="{{ $U('/stockreports/spendings') }}">
|
||||
{{ $__t('Product') }}
|
||||
</a>
|
||||
<a class="btn btn-outline-dark responsive-button m-1 mt-md-0 mb-md-0 float-right @if($byGroup) active @endif"
|
||||
href="{{ $U('/stockreports/spendings?byGroup=true') }}">
|
||||
{{ $__t('Product group') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-2">
|
||||
|
||||
<div class="row collapse d-md-flex"
|
||||
id="table-filter-row">
|
||||
<div class="col-sm-12 col-md-6 col-xl-3">
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text"><i class="fa-solid fa-clock"></i> {{ $__t('Date range') }}</span>
|
||||
</div>
|
||||
<input type="text"
|
||||
name="date-filter"
|
||||
id="daterange-filter"
|
||||
class="custom-control custom-select"
|
||||
value="" />
|
||||
</div>
|
||||
</div>
|
||||
@if(!$byGroup)
|
||||
<div class="col-sm-12 col-md-6 col-xl-4">
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text"><i class="fa-solid fa-filter"></i> {{ $__t('Product group') }}</span>
|
||||
</div>
|
||||
<select class="custom-control custom-select"
|
||||
id="product-group-filter">
|
||||
<option value="all">{{ $__t('All') }}</option>
|
||||
@foreach($productGroups as $productGroup)
|
||||
<option @if($productGroup->id == $selectedGroup) selected="selected" @endif
|
||||
value="{{ $productGroup->id }}">{{ $productGroup->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
<div class="col">
|
||||
<div class="float-right">
|
||||
<button id="clear-filter-button"
|
||||
class="btn btn-sm btn-outline-info"
|
||||
data-toggle="tooltip"
|
||||
title="{{ $__t('Clear filter') }}">
|
||||
<i class="fa-solid fa-filter-circle-xmark"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-2">
|
||||
<div class="col-sm-12 col-md-12 col-xl-12">
|
||||
<canvas id="metrics-chart"></canvas>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-12 col-xl-12">
|
||||
<table id="metrics-table"
|
||||
class="table table-sm table-striped nowrap w-100">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ $__t('Name') }}</th>
|
||||
<th>{{ $__t('Total') }}</th>
|
||||
@if(!$byGroup)
|
||||
<th>{{ $__t('Product group') }}</th>
|
||||
@endif
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="d-none">
|
||||
@foreach($metrics as $metric)
|
||||
<tr>
|
||||
<td data-chart-label="{{ $metric->name }}">
|
||||
{{ $metric->name }}
|
||||
</td>
|
||||
<td data-chart-value="{{ $metric->total }}"
|
||||
data-order="{{ $metric->total }}">
|
||||
<span class="locale-number locale-number-currency">{{ $metric->total }}</span>
|
||||
</td>
|
||||
@if(!$byGroup)
|
||||
<td>
|
||||
{{ $metric->group_name }}
|
||||
</td>
|
||||
@endif
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@stop
|
30
yarn.lock
30
yarn.lock
@ -163,6 +163,21 @@ chartjs-color@~2.2.0:
|
||||
chartjs-color-string "^0.5.0"
|
||||
color-convert "^0.5.3"
|
||||
|
||||
chartjs-plugin-colorschemes@^0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/chartjs-plugin-colorschemes/-/chartjs-plugin-colorschemes-0.4.0.tgz#7a310c32411ef0b5135df1f47da4d379e22220f7"
|
||||
integrity sha512-GUAulGFa6igdhi/xF9XopLJknwtXZT0wwEB+e/D4SJcRGY1HdwzX84pVn5BSpnk1idF6RJxZoA7w396R7BOE9A==
|
||||
|
||||
chartjs-plugin-doughnutlabel@^2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/chartjs-plugin-doughnutlabel/-/chartjs-plugin-doughnutlabel-2.0.3.tgz#0aa19b040dc68e12163e3215a01609cb3e0b5a7b"
|
||||
integrity sha512-it815BZSPggTkyhC3b4BVqDwlySKtzO0kZ11DCUs/nD29DGoj6w5luDu4slczg97ml7VRROHawxlS0HvGEIVdw==
|
||||
|
||||
chartjs-plugin-piechart-outlabels@^0.1.4:
|
||||
version "0.1.4"
|
||||
resolved "https://registry.yarnpkg.com/chartjs-plugin-piechart-outlabels/-/chartjs-plugin-piechart-outlabels-0.1.4.tgz#e97e19a12202d74f9040d9e4641987c9d1e458fc"
|
||||
integrity sha512-IaYkh6ab8nLAvgioQ+BwU0awfMbxwmfO2AeBL+S45VVx9AdObovr9+aE+ShUO2Og96y6eJpCxZGJr4zXB7YnRw==
|
||||
|
||||
color-convert@^0.5.3:
|
||||
version "0.5.3"
|
||||
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-0.5.3.tgz#bdb6c69ce660fadffe0b0007cc447e1b9f7282bd"
|
||||
@ -307,6 +322,14 @@ datatables.net@>=1.12.1, datatables.net@^1.10.22:
|
||||
dependencies:
|
||||
jquery ">=1.7"
|
||||
|
||||
daterangepicker@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/daterangepicker/-/daterangepicker-3.1.0.tgz#718d606614331df3e774c9aba82ccd8838d45da1"
|
||||
integrity sha512-DxWXvvPq4srWLCqFugqSV+6CBt/CvQ0dnpXhQ3gl0autcIDAruG1PuGG3gC7yPRNytAD1oU1AcUOzaYhOawhTw==
|
||||
dependencies:
|
||||
jquery ">=1.10"
|
||||
moment "^2.9.0"
|
||||
|
||||
delayed-stream@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
|
||||
@ -479,6 +502,11 @@ jquery@3.3.1:
|
||||
resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.3.1.tgz#958ce29e81c9790f31be7792df5d4d95fc57fbca"
|
||||
integrity sha512-Ubldcmxp5np52/ENotGxlLe6aGMvmF4R8S6tZjsP6Knsaxd/xp3Zrh50cG93lR6nPXyUFwzN3ZSOQI0wRJNdGg==
|
||||
|
||||
jquery@>=1.10:
|
||||
version "3.6.4"
|
||||
resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.6.4.tgz#ba065c188142100be4833699852bf7c24dc0252f"
|
||||
integrity sha512-v28EW9DWDFpzcD9O5iyJXg3R3+q+mET5JhnjJzQUZMHOv67bpSIHq81GEYpPNZHG+XXHsfSme3nxp/hndKEcsQ==
|
||||
|
||||
jquery@>=1.12.0, jquery@>=1.7, jquery@>=1.7.2, jquery@^3.6.0:
|
||||
version "3.6.3"
|
||||
resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.6.3.tgz#23ed2ffed8a19e048814f13391a19afcdba160e6"
|
||||
@ -538,7 +566,7 @@ moment-timezone@^0.5.34:
|
||||
dependencies:
|
||||
moment ">= 2.9.0"
|
||||
|
||||
"moment@>= 2.9.0", moment@^2.10.2, moment@^2.27.0, moment@^2.29.2:
|
||||
"moment@>= 2.9.0", moment@^2.10.2, moment@^2.27.0, moment@^2.29.2, moment@^2.9.0:
|
||||
version "2.29.4"
|
||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108"
|
||||
integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==
|
||||
|
Loading…
x
Reference in New Issue
Block a user