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:
Travis Raup 2023-04-01 11:05:41 -04:00 committed by GitHub
parent 98469248eb
commit 340832c361
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 499 additions and 2 deletions

View 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
]);
}
}

View File

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

View File

@ -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
View 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')
);

View File

@ -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",

View 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();
});

View File

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

View File

@ -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);

View File

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

View 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>&nbsp;{{ $__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>&nbsp;{{ $__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

View File

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