Skip to content

Commit 8f677ff

Browse files
committed
commerce dashboard
1 parent a2fff91 commit 8f677ff

File tree

2 files changed

+344
-0
lines changed

2 files changed

+344
-0
lines changed
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
<?php
2+
3+
/**
4+
* SiteBase
5+
* PHP Version 8.3
6+
*
7+
* @category CMS / Framework
8+
* @package Degami\Sitebase
9+
* @author Mirko De Grandis <degami@github.com>
10+
* @license MIT https://opensource.org/licenses/mit-license.php
11+
* @link https://github.com/degami/sitebase
12+
*/
13+
14+
namespace App\Base\Controllers\Admin\Commerce;
15+
16+
use App\Base\Abstracts\Controllers\AdminPage;
17+
use App\Base\Interfaces\Model\PhysicalProductInterface;
18+
use DateInterval;
19+
use DateTime;
20+
21+
/**
22+
* "Dashboard" Admin Page
23+
*/
24+
class Dashboard extends AdminPage
25+
{
26+
/**
27+
* @var string page title
28+
*/
29+
protected ?string $page_title = 'Commerce Dashboard';
30+
31+
/**
32+
* {@inheritdoc}
33+
*/
34+
public function getTemplateName(): string
35+
{
36+
return 'commerce/dashboard';
37+
}
38+
39+
/**
40+
* {@inheritdoc}
41+
*/
42+
public static function getAccessPermission(): string
43+
{
44+
return 'administer_orders';
45+
}
46+
47+
/**
48+
* {@inheritdoc}
49+
*/
50+
public static function getAdminPageLink(): ?array
51+
{
52+
return [
53+
'permission_name' => '',
54+
'route_name' => static::getPageRouteName(),
55+
'icon' => 'bar-chart',
56+
'text' => 'Dashboard',
57+
'section' => 'commerce',
58+
'order' => 0,
59+
];
60+
}
61+
62+
/**
63+
* {@inheritdoc}
64+
*/
65+
public function getTemplateData(): array
66+
{
67+
$now = new DateTime();
68+
69+
$this->template_data = [
70+
'lifetime' => $this->collectData(clone $now, null),
71+
'last_year' => $this->collectData(clone $now, (clone $now)->sub(DateInterval::createFromDateString('1 year'))),
72+
'last_month' => $this->collectData(clone $now, (clone $now)->sub(DateInterval::createFromDateString('1 month'))),
73+
'last_week' => $this->collectData(clone $now, (clone $now)->sub(DateInterval::createFromDateString('1 week'))),
74+
];
75+
76+
return $this->template_data;
77+
}
78+
79+
/**
80+
* Returns sales data for a given date range
81+
*/
82+
protected function collectData(DateTime $to, ?DateTime $from = null): array
83+
{
84+
$args = [];
85+
$where = " WHERE 1";
86+
87+
if ($from) {
88+
$where .= ' AND o.created_at >= :from';
89+
$args['from'] = $from->format('Y-m-d 00:00:00');
90+
}
91+
92+
if ($to) {
93+
$where .= ' AND o.created_at <= :to';
94+
$args['to'] = $to->format('Y-m-d 23:59:59');
95+
}
96+
97+
// === 1️⃣ Statistiche generali (solo tabella `order`) ===
98+
$qOrders = "
99+
SELECT
100+
COUNT(o.id) AS total_sales,
101+
SUM(o.admin_total_incl_tax) AS total_income,
102+
SUM(o.discount_amount) AS total_discounts,
103+
SUM(o.tax_amount) AS total_tax,
104+
MAX(o.admin_currency_code) AS admin_currency_code
105+
FROM `order` o
106+
$where
107+
";
108+
109+
$stmt1 = $this->getPdo()->prepare($qOrders);
110+
$stmt1->execute($args);
111+
$orders = $stmt1->fetch(\PDO::FETCH_ASSOC) ?: [];
112+
113+
// === 2️⃣ Totale prodotti venduti ===
114+
$qItems = "
115+
SELECT SUM(oi.quantity) AS total_products
116+
FROM `order_item` oi
117+
INNER JOIN `order` o ON o.id = oi.order_id
118+
$where
119+
";
120+
121+
$stmt2 = $this->getPdo()->prepare($qItems);
122+
$stmt2->execute($args);
123+
$items = $stmt2->fetch(\PDO::FETCH_ASSOC) ?: [];
124+
125+
// === 3️⃣ Prodotti più venduti (TOP 10) ===
126+
$qTop = "
127+
SELECT
128+
CONCAT(oi.product_class, ':', oi.product_id) AS product_key,
129+
SUM(oi.quantity) AS total_qty
130+
FROM `order` o
131+
INNER JOIN `order_item` oi ON oi.order_id = o.id
132+
$where
133+
GROUP BY product_key
134+
ORDER BY total_qty DESC
135+
LIMIT 5
136+
";
137+
138+
$stmt3 = $this->getPdo()->prepare($qTop);
139+
$stmt3->execute($args);
140+
141+
$most_sold = [];
142+
foreach ($stmt3->fetchAll(\PDO::FETCH_ASSOC) as $row) {
143+
try {
144+
$p = explode(':', $row['product_key']);
145+
$product = $this->containerCall([$p[0], 'load'], ['id' => $p[1]]);
146+
$label = method_exists($product, 'getSku')
147+
? ($product->getSku() ?: $product->getName())
148+
: $product->getName();
149+
$stock = ($product instanceof PhysicalProductInterface) ?
150+
$product->getProductStock()->getCurrentQuantity() :
151+
$this->getUtils()->translate('unlimited', locale: $this->getCurrentLocale());
152+
} catch (\Exception $e) {
153+
$label = 'n/a';
154+
$stock = 'n/a';
155+
}
156+
157+
$most_sold[] = [
158+
'product' => $label,
159+
'total_qty' => (int) $row['total_qty'],
160+
'stock' => $stock,
161+
];
162+
}
163+
164+
// === 4️⃣ Metodo di pagamento più usato ===
165+
$qPay = "
166+
SELECT op.payment_method, COUNT(*) AS total
167+
FROM `order_payment` op
168+
INNER JOIN `order` o ON o.id = op.order_id
169+
$where
170+
GROUP BY op.payment_method
171+
ORDER BY total DESC
172+
LIMIT 1
173+
";
174+
175+
$stmt4 = $this->getPdo()->prepare($qPay);
176+
$stmt4->execute($args);
177+
$payment = $stmt4->fetch(\PDO::FETCH_ASSOC);
178+
179+
// === 5️⃣ Calcoli e formattazione ===
180+
$utils = $this->getUtils();
181+
$currency = $orders['admin_currency_code'] ?? 'EUR';
182+
183+
$total_sales = (int) ($orders['total_sales'] ?? 0);
184+
$total_income = (float) ($orders['total_income'] ?? 0);
185+
$total_tax = (float) ($orders['total_tax'] ?? 0);
186+
$total_discount = (float) ($orders['total_discounts'] ?? 0);
187+
$total_products = (int) ($items['total_products'] ?? 0);
188+
189+
$aov = $total_sales > 0 ? $total_income / $total_sales : 0;
190+
191+
return [
192+
'total_sales' => $total_sales,
193+
'total_income' => $utils->formatPrice($total_income, $currency),
194+
'total_tax' => $utils->formatPrice($total_tax, $currency),
195+
'total_discount' => $utils->formatPrice($total_discount, $currency),
196+
'total_products' => $total_products,
197+
'average_order' => $utils->formatPrice($aov, $currency),
198+
'most_sold' => $most_sold,
199+
'top_payment' => $payment['payment_method'] ?? 'n/a',
200+
];
201+
}
202+
}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
<?php
2+
/**
3+
* @var $controller \App\Base\Controllers\Admin\Commerce\Dashboard
4+
* @var $current_user \App\Base\Abstracts\Models\AccountModel
5+
*/
6+
7+
$this->layout('admin::layout', ['title' => $controller->getPageTitle()] + get_defined_vars()) ?>
8+
9+
<div class="container-fluid my-4">
10+
<!-- Tabs navigation -->
11+
<?php $tabs = [
12+
'lifetime' => $this->sitebase()->translate('Lifetime'),
13+
'last_year' => $this->sitebase()->translate('Last year'),
14+
'last_month' => $this->sitebase()->translate('Last month'),
15+
'last_week' => $this->sitebase()->translate('Last week'),
16+
]; ?>
17+
18+
<ul class="nav nav-tabs" id="dashboardTab" role="tablist">
19+
<?php $first = true; foreach ($tabs as $tab_id => $tab_label): ?>
20+
<li class="nav-item">
21+
<a
22+
class="nav-link <?= $first ? 'active' : '' ?>"
23+
id="<?= $tab_id ?>-tab"
24+
data-toggle="tab"
25+
href="#<?= $tab_id ?>"
26+
role="tab"
27+
aria-controls="<?= $tab_id ?>"
28+
aria-selected="<?= $first ? 'true' : 'false' ?>"
29+
>
30+
<?= htmlspecialchars($tab_label) ?>
31+
</a>
32+
</li>
33+
<?php $first = false; endforeach; ?>
34+
</ul>
35+
36+
<!-- Tabs content -->
37+
<div class="tab-content mt-3" id="dashboardTabContent">
38+
<?php $first = true; foreach ($tabs as $tab_id => $tab_label):
39+
$data = ${$tab_id};
40+
?>
41+
<div
42+
class="tab-pane fade <?= $first ? 'show active' : '' ?>"
43+
id="<?= $tab_id ?>"
44+
role="tabpanel"
45+
aria-labelledby="<?= $tab_id ?>-tab"
46+
>
47+
<!-- Summary cards -->
48+
<div class="row mt-3">
49+
<div class="col-md-3">
50+
<div class="card text-center">
51+
<div class="card-header"><h6 class="text-muted mb-2"><?= $this->sitebase()->translate('Orders') ?></h6></div>
52+
<div class="card-body">
53+
<p class="h3 mb-0"><?= (int)$data['total_sales'] ?></p>
54+
</div>
55+
</div>
56+
</div>
57+
58+
<div class="col-md-3">
59+
<div class="card text-center">
60+
<div class="card-header"><h6 class="text-muted mb-2"><?= $this->sitebase()->translate('Revenue') ?></h6></div>
61+
<div class="card-body">
62+
<p class="h3 mb-0"><?= htmlspecialchars($data['total_income']) ?></p>
63+
</div>
64+
</div>
65+
</div>
66+
67+
<div class="col-md-3">
68+
<div class="card text-center">
69+
<div class="card-header"><h6 class="text-muted mb-2"><?= $this->sitebase()->translate('Average order value') ?></h6></div>
70+
<div class="card-body">
71+
<p class="h3 mb-0"><?= htmlspecialchars($data['average_order']) ?></p>
72+
</div>
73+
</div>
74+
</div>
75+
76+
<div class="col-md-3">
77+
<div class="card text-center">
78+
<div class="card-header"><h6 class="text-muted mb-2"><?= $this->sitebase()->translate('Products sold') ?></h6></div>
79+
<div class="card-body">
80+
<p class="h3 mb-0"><?= (int)$data['total_products'] ?></p>
81+
</div>
82+
</div>
83+
</div>
84+
</div>
85+
86+
<!-- More stats -->
87+
<div class="row mt-3">
88+
<div class="col-md-4">
89+
<div class="card text-center">
90+
<div class="card-header"><h6 class="text-muted mb-2"><?= $this->sitebase()->translate('Tax collected') ?></h6></div>
91+
<div class="card-body">
92+
<p class="h4 mb-0"><?= htmlspecialchars($data['total_tax']) ?></p>
93+
</div>
94+
</div>
95+
</div>
96+
97+
<div class="col-md-4">
98+
<div class="card text-center">
99+
<div class="card-header"><h6 class="text-muted mb-2"><?= $this->sitebase()->translate('Discounts given') ?></h6></div>
100+
<div class="card-body">
101+
<p class="h4 mb-0"><?= htmlspecialchars($data['total_discount']) ?></p>
102+
</div>
103+
</div>
104+
</div>
105+
106+
<div class="col-md-4">
107+
<div class="card text-center">
108+
<div class="card-header"><h6 class="text-muted mb-2"><?= $this->sitebase()->translate('Most used payment method') ?></h6></div>
109+
<div class="card-body">
110+
<p class="h4 mb-0"><?= htmlspecialchars($data['top_payment']) ?></p>
111+
</div>
112+
</div>
113+
</div>
114+
</div>
115+
116+
<!-- Top sellers -->
117+
<h4 class="mt-4 mb-3"><?= $this->sitebase()->translate('Top %d best sellers', [count($data['most_sold'])]); ?></h4>
118+
<div class="table-responsive">
119+
<table class="table table-striped table-bordered table-sm">
120+
<thead class="thead-dark">
121+
<tr>
122+
<th><?= $this->sitebase()->translate('Product'); ?></th>
123+
<th class="text-right"><?= $this->sitebase()->translate('Sold quantity'); ?></th>
124+
<th class="text-right"><?= $this->sitebase()->translate('In Stock quantity'); ?></th>
125+
</tr>
126+
</thead>
127+
<tbody>
128+
<?php foreach ($data['most_sold'] as $item): ?>
129+
<tr>
130+
<td><?= htmlspecialchars($item['product']) ?></td>
131+
<td class="text-right"><?= (int)$item['total_qty'] ?></td>
132+
<td class="text-right"><?= $item['stock'] ?></td>
133+
</tr>
134+
<?php endforeach; ?>
135+
</tbody>
136+
</table>
137+
</div>
138+
139+
</div>
140+
<?php $first = false; endforeach; ?>
141+
</div>
142+
</div>

0 commit comments

Comments
 (0)