added scheda cliente

This commit is contained in:
2026-10-15 10:20:41 +02:00
parent c3234e3160
commit abd09769f2
6 changed files with 2023 additions and 31 deletions
+49 -26
View File
@@ -95,32 +95,16 @@ class MagoSalesStats
$by[$c]['orders'][] = $r;
}
$today = new DateTime('today');
$out = [];
foreach ($by as $c) {
$dates = array_keys($c['dates']);
sort($dates);
$n = count($dates);
if ($n < self::DORMANT_MIN_ORDERS) continue;
$gaps = [];
for ($k = 1; $k < $n; $k++) {
$gaps[] = (int)(new DateTime($dates[$k - 1]))->diff(new DateTime($dates[$k]))->days;
}
sort($gaps);
$m = count($gaps);
$median = $m % 2 ? $gaps[intdiv($m, 2)] : ($gaps[$m / 2 - 1] + $gaps[$m / 2]) / 2;
$rhythm = max(7, (float)$median);
$last = end($dates);
$since = (int)(new DateTime($last))->diff($today)->days;
if ($since < self::DORMANT_MIN_DAYS) continue;
$ratio = $since / $rhythm;
if ($ratio >= self::DORMANT_LOST || $since >= 365) $level = 'lost';
elseif ($ratio >= self::DORMANT_SLEEP) $level = 'dormant';
elseif ($ratio >= self::DORMANT_WATCH) $level = 'watch';
else continue;
$info = self::rhythmInfo(array_keys($c['dates']));
if (!$info || !in_array($info['level'], ['watch', 'dormant', 'lost'], true)) continue;
$last = $info['last_date'];
$since = $info['days_since'];
$rhythm = $info['rhythm'];
$ratio = $info['ratio'];
$level = $info['level'];
$n = $info['orders'];
// ordinato nei 12 mesi fino all'ultimo ordine = quanto vale il cliente "a regime"
$from12 = (new DateTime($last))->modify('-365 days')->format('Y-m-d');
@@ -142,8 +126,8 @@ class MagoSalesStats
'last_date' => $last,
'last_no' => $lastOrder['ord_no'],
'days_since' => $since,
'rhythm' => (int)round($rhythm),
'ratio' => round($ratio, 1),
'rhythm' => $rhythm,
'ratio' => $ratio,
'orders' => $n,
'value12' => round($v12, 2),
'level' => $level,
@@ -155,6 +139,45 @@ class MagoSalesStats
return $out;
}
/**
* Ritmo d'ordine di un cliente a partire dalle date d'ordine (Y-m-d) degli ultimi DORMANT_WINDOW_YEARS anni.
* level: 'ok' | 'watch' | 'dormant' | 'lost' | 'few' (troppo pochi ordini per stimare un ritmo)
*/
public static function rhythmInfo(array $dates)
{
$dates = array_values(array_unique($dates));
sort($dates);
$n = count($dates);
if (!$n) return null;
$today = new DateTime('today');
$last = end($dates);
$since = (int)(new DateTime($last))->diff($today)->days;
$out = ['orders' => $n, 'last_date' => $last, 'days_since' => $since, 'rhythm' => null, 'ratio' => null, 'level' => 'few'];
if ($n < self::DORMANT_MIN_ORDERS) return $out;
$gaps = [];
for ($k = 1; $k < $n; $k++) {
$gaps[] = (int)(new DateTime($dates[$k - 1]))->diff(new DateTime($dates[$k]))->days;
}
sort($gaps);
$m = count($gaps);
$median = $m % 2 ? $gaps[intdiv($m, 2)] : ($gaps[$m / 2 - 1] + $gaps[$m / 2]) / 2;
$rhythm = max(7, (float)$median);
$ratio = $since / $rhythm;
if ($since < self::DORMANT_MIN_DAYS) $level = 'ok';
elseif ($ratio >= self::DORMANT_LOST || $since >= 365) $level = 'lost';
elseif ($ratio >= self::DORMANT_SLEEP) $level = 'dormant';
elseif ($ratio >= self::DORMANT_WATCH) $level = 'watch';
else $level = 'ok';
$out['rhythm'] = (int)round($rhythm);
$out['ratio'] = round($ratio, 1);
$out['level'] = $level;
return $out;
}
/**
* Previsione di fine anno: valore dell'anno in corso ad oggi diviso la quota che, negli anni
* precedenti (max 3), si era raggiunta alla stessa data. $rows: [y, full, ytd].
+5
View File
@@ -235,6 +235,11 @@
<i class='bx bx-radio-circle'></i>Confronto anni
</a>
</li>
<li>
<a href="sales_customer.php">
<i class='fa-solid fa-address-card'></i>Scheda cliente
</a>
</li>
<li>
<a href="sales_orders_deliveries.php">
<i class='bx bx-radio-circle'></i>Consegne
File diff suppressed because it is too large Load Diff
+13 -1
View File
@@ -589,6 +589,17 @@ if ($action === 'data') {
.order-link:hover {
text-decoration: underline;
}
.sc-link {
color: #198754;
margin-left: 6px;
opacity: .75;
text-decoration: none;
}
.sc-link:hover {
opacity: 1;
}
</style>
</head>
@@ -620,6 +631,7 @@ if ($action === 'data') {
<?php if ($canStats) : ?>
<a href="sales_orders_stats.php"><i class="fa-solid fa-list-check me-1"></i> Andamento ordini</a>
<a href="sales_orders_years.php"><i class="fa-solid fa-chart-column me-1"></i> Confronto anni</a>
<a href="sales_customer.php"><i class="fa-solid fa-address-card me-1"></i> Scheda cliente</a>
<?php endif; ?>
<a href="sales_orders_deliveries.php" class="active"><i class="fa-solid fa-truck-fast me-1"></i> Consegne</a>
</div>
@@ -1154,7 +1166,7 @@ if ($action === 'data') {
(CAN_STATS ?
`<a class="order-link" href="sales_orders_stats.php?q=${encodeURIComponent(r.internal_ord_no)}" target="_blank">${esc(r.internal_ord_no)}</a>` :
`<b>${esc(r.internal_ord_no)}</b>`) + `<div class="small text-muted">${fmtDate(r.order_date)}</div>`,
`<div class="fw-semibold">${esc(r.company_name)}</div><div class="small text-muted">${esc(r.customer)}</div>`,
`<div class="fw-semibold">${esc(r.company_name)}</div><div class="small text-muted">${esc(r.customer)}${CAN_STATS ? `<a class="sc-link" href="sales_customer.php?c=${encodeURIComponent(r.customer)}" title="Apri la scheda cliente" onclick="event.stopPropagation();"><i class="fa-solid fa-address-card"></i></a>` : ''}</div>`,
`<b>${esc(r.item)}</b><div class="small text-muted" style="white-space:normal;max-width:300px;">${esc(r.description)}</div>`,
`<span data-order="${+r.residual_qty}" class="amount"><b>${fmtQty(r.residual_qty)}</b> ${esc(r.uom)}</span><div class="small text-muted">di ${fmtQty(r.qty)}</div>`,
`<span data-order="${esc(r.due_date || '9999-12-31')}">${dateCell(r.zibo_date, r.client_date)}</span>`,
+14 -2
View File
@@ -875,6 +875,17 @@ if ($action === 'order') {
grid-template-columns: 1fr;
}
}
.sc-link {
color: #198754;
margin-left: 6px;
opacity: .75;
text-decoration: none;
}
.sc-link:hover {
opacity: 1;
}
</style>
</head>
@@ -923,6 +934,7 @@ if ($action === 'order') {
<div class="stats-tabs">
<a href="sales_orders_stats.php" class="active"><i class="fa-solid fa-list-check me-1"></i> Andamento ordini</a>
<a href="sales_orders_years.php"><i class="fa-solid fa-chart-column me-1"></i> Confronto anni</a>
<a href="sales_customer.php"><i class="fa-solid fa-address-card me-1"></i> Scheda cliente</a>
<?php if (userCan('sales.deliveries.view')) : ?>
<a href="sales_orders_deliveries.php"><i class="fa-solid fa-truck-fast me-1"></i> Consegne</a>
<?php endif; ?>
@@ -2006,7 +2018,7 @@ if ($action === 'order') {
const data = rows.map(r => [
`<b>${esc(r.internal_ord_no)}</b>${r.external_ord_no ? `<div class="small text-muted">${esc(r.external_ord_no)}</div>` : ''}`,
`<span data-order="${esc(r.order_date || '')}">${fmtDate(r.order_date)}</span>`,
`<div class="fw-semibold">${esc(r.company_name)}</div><div class="small text-muted">${esc(r.customer)}</div>`,
`<div class="fw-semibold">${esc(r.company_name)}</div><div class="small text-muted">${esc(r.customer)}<a class="sc-link" href="sales_customer.php?c=${encodeURIComponent(r.customer)}" title="Apri la scheda cliente" onclick="event.stopPropagation();"><i class="fa-solid fa-address-card"></i></a></div>`,
esc(r.your_reference),
`<span data-order="${esc(r.due_date || '')}">${dateCell(r.zibo_date, r.client_date, {
open: r.status === 'aperto' || r.status === 'bloccato',
@@ -2119,7 +2131,7 @@ if ($action === 'order') {
<div class="d-flex justify-content-between align-items-start flex-wrap gap-2">
<div>
<h4 class="mb-1">Ordine ${esc(o.internal_ord_no)}</h4>
<div>${esc(o.company_name)} <span style="opacity:.8;">(${esc(o.customer)})</span></div>
<div><a href="sales_customer.php?c=${encodeURIComponent(o.customer)}" style="color:inherit;" title="Apri la scheda cliente">${esc(o.company_name)} <span style="opacity:.8;">(${esc(o.customer)})</span> <i class="fa-solid fa-address-card"></i></a></div>
</div>
<div class="text-end">
<span class="st-badge st-${esc(o.status)}" style="border:2px solid rgba(255,255,255,.7);">${STATUS_LABEL[o.status] || esc(o.status)}</span>
+14 -2
View File
@@ -933,6 +933,17 @@ if ($action === 'data') {
padding: 60px 20px;
color: var(--c-muted);
}
.sc-link {
color: #198754;
margin-left: 6px;
opacity: .75;
text-decoration: none;
}
.sc-link:hover {
opacity: 1;
}
</style>
</head>
@@ -964,6 +975,7 @@ if ($action === 'data') {
<div class="stats-tabs">
<a href="sales_orders_stats.php"><i class="fa-solid fa-list-check me-1"></i> Andamento ordini</a>
<a href="sales_orders_years.php" class="active"><i class="fa-solid fa-chart-column me-1"></i> Confronto anni</a>
<a href="sales_customer.php"><i class="fa-solid fa-address-card me-1"></i> Scheda cliente</a>
<?php if (userCan('sales.deliveries.view')) : ?>
<a href="sales_orders_deliveries.php"><i class="fa-solid fa-truck-fast me-1"></i> Consegne</a>
<?php endif; ?>
@@ -1774,7 +1786,7 @@ if ($action === 'data') {
title: 'Cliente',
data: null,
render: (c, type) => type === 'display' ?
`<div class="fw-semibold">${esc(c.name)}</div><div class="small text-muted">${esc(c.code)}</div>` : (type === 'export' ? `${c.name} (${c.code})` : `${c.name} ${c.code}`)
`<div class="fw-semibold">${esc(c.name)}</div><div class="small text-muted">${esc(c.code)}<a class="sc-link" href="sales_customer.php?c=${encodeURIComponent(c.code)}" title="Apri la scheda cliente" onclick="event.stopPropagation();"><i class="fa-solid fa-address-card"></i></a></div>` : (type === 'export' ? `${c.name} (${c.code})` : `${c.name} ${c.code}`)
}, {
title: 'Agente',
data: 'salesperson'
@@ -2178,7 +2190,7 @@ if ($action === 'data') {
title: 'Cliente',
data: null,
render: (c, type) => type === 'display' ?
`<div class="fw-semibold">${esc(c.name)}</div><div class="small text-muted">${esc(c.code)}</div>` : (type === 'export' ? `${c.name} (${c.code})` : `${c.name} ${c.code}`)
`<div class="fw-semibold">${esc(c.name)}</div><div class="small text-muted">${esc(c.code)}<a class="sc-link" href="sales_customer.php?c=${encodeURIComponent(c.code)}" title="Apri la scheda cliente" onclick="event.stopPropagation();"><i class="fa-solid fa-address-card"></i></a></div>` : (type === 'export' ? `${c.name} (${c.code})` : `${c.name} ${c.code}`)
}];
years.slice().reverse().forEach(y => columns.push({
title: `<span style="color:${yearColor(y)};">●</span> ${y}`,