added fatturato

This commit is contained in:
2026-10-14 17:59:06 +02:00
parent b2a2f56383
commit 5e2ec11dbd
6 changed files with 655 additions and 12 deletions
+115 -12
View File
@@ -73,6 +73,8 @@ if ($action === 'data') {
$monthly = [];
$customers = [];
$yearIn = '1=0';
$yp = $params;
if ($years) {
$ph = [];
@@ -110,8 +112,41 @@ if ($action === 'data') {
$customers = array_values($customers);
}
// --- FATTURATO per data documento (fatture + acconti - note di credito) ---
$invoiced = ['available' => false, 'summary' => [], 'monthly' => []];
try {
$iFrom = MagoSalesStats::invoiceFrom();
$iWhere = preg_replace('/\bx\./', 'i.', $where); // stessi filtri cliente/agente/area
$iYtd = $ytd ? " AND DATE_FORMAT(i.document_date, '%m-%d') <= DATE_FORMAT(CURDATE(), '%m-%d')" : '';
$invoiced['summary'] = MagoSalesStats::fetchAllQ($pdo, "
SELECT YEAR(i.document_date) AS y,
COALESCE(SUM(i.revenue), 0) AS value,
SUM(i.is_credit_note = 0) AS docs,
SUM(i.is_credit_note = 1) AS credit_notes,
COALESCE(SUM(CASE WHEN i.is_credit_note = 1 THEN -i.revenue END), 0) AS credit_value,
SUM(i.no_amount) AS missing_amounts
FROM $iFrom
WHERE $iWhere AND i.document_date IS NOT NULL $iYtd
GROUP BY y ORDER BY y
", $params);
if ($years) {
$invoiced['monthly'] = MagoSalesStats::fetchAllQ($pdo, "
SELECT YEAR(i.document_date) AS y, MONTH(i.document_date) AS m, SUM(i.revenue) AS v
FROM $iFrom
WHERE $iWhere AND " . str_replace('x.order_date', 'i.document_date', $yearIn) . " $iYtd
GROUP BY y, m ORDER BY y, m
", $yp);
}
$invoiced['available'] = (bool)$pdo->query("SELECT 1 FROM mago_sale_doc LIMIT 1")->fetchColumn();
} catch (Throwable $e) {
$invoiced['error'] = 'Fatturato non disponibile: eseguire la migrazione e la sincronizzazione';
}
MagoSalesStats::jsonOut([
'success' => true,
'invoiced' => $invoiced,
'metric' => $metric,
'ytd' => $ytd,
'today' => date('Y-m-d'),
@@ -444,6 +479,26 @@ if ($action === 'data') {
text-transform: uppercase;
}
.year-card .inv {
margin-top: 8px;
background: #fff;
border-radius: 10px;
padding: 6px 10px;
font-size: .85rem;
color: var(--c-muted);
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 8px;
flex-wrap: wrap;
}
.year-card .inv b {
color: var(--c-ink);
font-size: 1rem;
font-variant-numeric: tabular-nums;
}
.year-card .deltas {
margin-top: 10px;
font-size: .88rem;
@@ -659,6 +714,25 @@ if ($action === 'data') {
</div>
</div>
<!-- FATTURATO (per data fattura) -->
<div class="row g-3 mb-3" id="invRow">
<div class="col-xl-6">
<div class="chart-card">
<div class="chart-title">Fatturato per mese</div>
<div class="chart-hint">Imponibile fatture + acconti − note di credito, per data fattura</div>
<div id="chInvMonthly" style="min-height:340px;"></div>
</div>
</div>
<div class="col-xl-6">
<div class="chart-card">
<div class="chart-title">Fatturato progressivo dell'anno</div>
<div class="chart-hint">Somma da gennaio del fatturato, anno su anno</div>
<div id="chInvCumul" style="min-height:340px;"></div>
</div>
</div>
</div>
<div class="warn-strip mb-3" id="invMissing" style="display:none;"></div>
<!-- CLIENTI -->
<div class="chart-card">
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mb-2">
@@ -871,6 +945,7 @@ if ($action === 'data') {
renderCards(d);
renderMonthly(d);
renderCumul(d);
renderInvoiced(d);
renderCustomers(d);
}).catch(e => {
$('#content').css('opacity', 1);
@@ -899,6 +974,19 @@ if ($action === 'data') {
$('#missingAmounts').toggle(missing > 0).html(missing > 0 ?
`<i class="fa-solid fa-circle-exclamation me-1"></i> ${fmtNum(missing)} ordini negli anni scelti non hanno ancora gli importi: lancia una sincronizzazione.` : '');
// riga fatturato nella carta anno (se i documenti sono sincronizzati)
const inv = {};
((d.invoiced && d.invoiced.summary) || []).forEach(r => inv[+r.y] = r);
const invLine = y => {
if (!d.invoiced || !d.invoiced.available) return '';
const r = inv[y],
p = inv[y - 1];
const v = r ? +r.value : 0;
const dl = p ? deltaSpan(pct(v, +p.value), `vs ${y - 1}`) : '';
const cn = r && +r.credit_notes ? ` title="${fmtNum(r.docs)} fatture, ${fmtNum(r.credit_notes)} note di credito (−${esc(fmtEur(r.credit_value, 2))})"` : (r ? ` title="${fmtNum(r.docs)} fatture"` : '');
return `<div class="inv"${cn}><span>Fatturato</span><b>${fmtEurShort(v)}</b>${dl}</div>`;
};
$('#yearCards').html(d.years.slice().reverse().map(y => {
const r = byYear[y] || {
orders: 0,
@@ -924,16 +1012,17 @@ if ($action === 'data') {
<div><span>Medio</span><b>${fmtEurShort(avg)}</b></div>
<div><span>Clienti</span><b>${fmtNum(r.customers)}</b></div>
</div>
${invLine(y)}
<div class="deltas">${deltas}</div>
</div>`;
}).join(''));
}
/* ---------------- GRAFICI ---------------- */
function seriesByYear(d) {
function seriesByYear(d, rows) {
const map = {};
d.years.forEach(y => map[y] = Array(12).fill(0));
d.monthly.forEach(r => {
(rows || d.monthly).forEach(r => {
if (map[+r.y]) map[+r.y][+r.m - 1] = +r.v;
});
// mesi futuri: vuoti (non zero) per l'anno in corso, e per tutti se "a pari data"
@@ -964,9 +1053,9 @@ if ($action === 'data') {
}
};
function renderMonthly(d) {
const map = seriesByYear(d);
upsert('monthly', 'chMonthly', {
function renderMonthly(d, key, el, rows, fmtV, fmtA) {
const map = seriesByYear(d, rows);
upsert(key || 'monthly', el || 'chMonthly', {
chart: {
...base,
type: 'line',
@@ -989,14 +1078,14 @@ if ($action === 'data') {
},
yaxis: {
labels: {
formatter: fmtAxis
formatter: fmtA || fmtAxis
}
},
tooltip: {
shared: true,
intersect: false,
y: {
formatter: fmtMetric
formatter: fmtV || fmtMetric
}
},
legend: {
@@ -1009,14 +1098,14 @@ if ($action === 'data') {
});
}
function renderCumul(d) {
const map = seriesByYear(d);
function renderCumul(d, key, el, rows, fmtV, fmtA) {
const map = seriesByYear(d, rows);
const cum = {};
d.years.forEach(y => {
let s = 0;
cum[y] = map[y].map(v => v === null ? null : (s += v));
});
upsert('cumul', 'chCumul', {
upsert(key || 'cumul', el || 'chCumul', {
chart: {
...base,
type: 'area',
@@ -1046,14 +1135,14 @@ if ($action === 'data') {
},
yaxis: {
labels: {
formatter: fmtAxis
formatter: fmtA || fmtAxis
}
},
tooltip: {
shared: true,
intersect: false,
y: {
formatter: fmtMetric
formatter: fmtV || fmtMetric
}
},
legend: {
@@ -1066,6 +1155,20 @@ if ($action === 'data') {
});
}
// fatturato: sempre in € (indipendente dal selettore N° ordini / Valore)
function renderInvoiced(d) {
const inv = d.invoiced || {};
$('#invRow').toggle(!!inv.available);
const miss = (inv.summary || []).filter(r => d.years.includes(+r.y)).reduce((a, r) => a + (+r.missing_amounts || 0), 0);
$('#invMissing').toggle(!inv.available || miss > 0).html(!inv.available ?
'<i class="fa-solid fa-circle-info me-1"></i> Fatturato non ancora disponibile: eseguire la migrazione <b>mago_sale_doc</b> e una sincronizzazione.' :
`<i class="fa-solid fa-circle-exclamation me-1"></i> ${fmtNum(miss)} fatture negli anni scelti non hanno ancora l'importo: lancia una sincronizzazione.`);
if (!inv.available) return;
const fv = v => v === null || v === undefined ? '—' : fmtEur(v);
renderMonthly(d, 'invMonthly', 'chInvMonthly', inv.monthly || [], fv, fmtEurShort);
renderCumul(d, 'invCumul', 'chInvCumul', inv.monthly || [], fv, fmtEurShort);
}
/* ---------------- CLIENTI ---------------- */
function classify(c, last, prev) {
const a = c.v[last] || 0,