added scheda cliente
This commit is contained in:
@@ -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].
|
||||
|
||||
@@ -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
@@ -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>`,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}`,
|
||||
|
||||
Reference in New Issue
Block a user