').html(h).text().replace('●', '').trim(),
+ // importi del totale come numeri in Excel; testo e percentuali lasciati com'erano
+ footer: h => {
+ const t = $('
').html(h).text().replace(/\u00a0/g, ' ').trim();
+ if (/^-?[\d.]+(,\d+)?\s*€$/.test(t)) return t.replace(/\./g, '').replace('€', '').replace(',', '.').trim();
+ if (/^[+-]?\d+%$/.test(t)) return parseInt(t, 10) / 100;
+ return t;
+ }
+ }
+ }
+ }]
+ });
+
+ function invOpen() {
+ return $('#invToggle').attr('aria-expanded') === 'true';
+ }
+
+ function invFiltersLabel() {
+ const parts = [];
+ const n = (id, l) => {
+ const v = $(id).val() || [];
+ if (v.length) parts.push(`${l}: ${v.join(', ')}`);
+ };
+ n('#fCustomers', 'clienti');
+ n('#fSalespeople', 'agenti');
+ n('#fAreas', 'aree');
+ if (lastData && lastData.ytd) parts.push(`a pari data ${todayLabel()}`);
+ return parts.length ? ' (' + parts.join(' · ') + ')' : '';
+ }
+
+ function renderInvTables() {
+ if (!lastData || !lastData.invoiced || !lastData.invoiced.available || !invOpen()) return;
+ if (invTab === 'months') renderInvMonthTable(lastData);
+ else loadInvDocs();
+ }
+
+ function renderInvMonthTable(d) {
+ const years = d.years.slice().reverse();
+ const map = seriesByYear(d, d.invoiced.monthly || []);
+ const rows = MONTHS.map((m, i) => ({
+ m: i,
+ label: m
+ }));
+ const cum = {};
+ years.forEach(y => {
+ let s = 0;
+ cum[y] = map[y].map(v => v === null ? null : (s += v));
+ });
+
+ const money = v => v === null || v === undefined ? '
—' : fmtEur(v);
+ const columns = [{
+ title: 'Mese',
+ data: 'label',
+ render: (v, type, r) => type === 'display' || type === 'export' ? v.charAt(0).toUpperCase() + v.slice(1) : r.m
+ }];
+ years.forEach((y, k) => {
+ columns.push({
+ title: `
● ${y}`,
+ data: null,
+ className: 'text-end amount',
+ render: (r, type) => {
+ const v = map[y][r.m];
+ return type === 'display' ? money(v) : (v === null ? '' : Math.round(v * 100) / 100);
+ }
+ }, {
+ title: `${y} progr.`,
+ data: null,
+ className: 'text-end text-muted',
+ render: (r, type) => {
+ const v = cum[y][r.m];
+ return type === 'display' ? money(v) : (v === null ? '' : Math.round(v * 100) / 100);
+ }
+ });
+ const py = years[k + 1];
+ if (k === 0 && py) {
+ columns.push({
+ title: `Var. ${y} vs ${py}`,
+ data: null,
+ className: 'text-end',
+ render: (r, type) => {
+ const a = map[y][r.m],
+ b = map[py][r.m];
+ const p = (a === null || !b) ? null : (a - b) / Math.abs(b) * 100;
+ if (type === 'export') return p === null ? '' : Math.round(p) / 100;
+ if (type !== 'display') return p === null ? -1e9 : p;
+ if (p === null) return '
—';
+ const rr = Math.round(p);
+ return `
${rr >= 0 ? '+' : ''}${rr}%`;
+ }
+ });
+ }
+ });
+
+ // riga totale
+ const tot = years.map(y => map[y].reduce((a, v) => a + (v || 0), 0));
+ let foot = '
| Totale | ';
+ years.forEach((y, k) => {
+ foot += `${fmtEur(tot[k], 2)} | | `;
+ if (k === 0 && years[1]) {
+ const p = tot[1] ? (tot[0] - tot[1]) / Math.abs(tot[1]) * 100 : null;
+ foot += `${p === null ? '' : `${p >= 0 ? '+' : ''}${Math.round(p)}%`} | `;
+ }
+ });
+ foot += '
|---|
';
+
+ if (invMonthDt) {
+ invMonthDt.destroy();
+ $('#invMonthTable').empty();
+ }
+ $('#invMonthTable').html(foot);
+ invMonthDt = $('#invMonthTable').DataTable({
+ data: rows,
+ columns: columns,
+ order: [
+ [0, 'asc']
+ ],
+ paging: false,
+ searching: false,
+ info: false,
+ dom: "<'mb-2'B>rt",
+ buttons: xlsButton('Fatturato per mese' + invFiltersLabel()),
+ language: {
+ url: 'https://cdn.datatables.net/plug-ins/1.13.6/i18n/it-IT.json'
+ }
+ });
+ }
+
+ function loadInvDocs() {
+ const p = new URLSearchParams();
+ p.append('action', 'invdocs');
+ if (lastData.ytd) p.append('ytd', '1');
+ lastData.years.forEach(y => p.append('years[]', y));
+ [
+ ['customers', '#fCustomers'],
+ ['salespeople', '#fSalespeople'],
+ ['areas', '#fAreas']
+ ].forEach(([k, id]) => ($(id).val() || []).forEach(v => p.append(k + '[]', v)));
+ const key = p.toString();
+ if (key === invDocsKey && invDocDt) return; // già caricati con questi filtri
+ invDocsKey = key;
+
+ $('#invPaneDocs').css('opacity', .5);
+ fetch('?' + key).then(r => r.json()).then(res => {
+ $('#invPaneDocs').css('opacity', 1);
+ if (!res.success) {
+ alert(res.message || 'Errore nel caricamento documenti');
+ return;
+ }
+ renderInvDocTable(res.docs || [], res.types || {});
+ }).catch(e => {
+ $('#invPaneDocs').css('opacity', 1);
+ console.error(e);
+ });
+ }
+
+ function renderInvDocTable(docs, types) {
+ const cls = r => +r.is_credit_note ? 't-cn' : (+r.document_type === 3407874 ? 't-inv' : 't-adv');
+ const total = docs.reduce((a, r) => a + (+r.revenue || 0), 0);
+ const nc = docs.filter(r => +r.is_credit_note).length;
+
+ if (invDocDt) {
+ invDocDt.destroy();
+ $('#invDocTable').empty();
+ }
+ $('#invDocTable').html(`
| Totale | ${fmtNum(docs.length - nc)} fatture | ${fmtNum(nc)} note di credito | | | | ${fmtEur(total, 2)} |
|---|
`);
+ invDocDt = $('#invDocTable').DataTable({
+ data: docs,
+ columns: [{
+ title: 'Data',
+ data: 'document_date',
+ render: (v, type) => type === 'display' ? fmtDateIt(v) : v
+ }, {
+ title: 'N° documento',
+ data: 'doc_no',
+ className: 'text-nowrap'
+ }, {
+ title: 'Tipo',
+ data: 'document_type',
+ render: (v, type, r) => {
+ const l = types[v] || v;
+ return type === 'display' ? `
${esc(l)}` : l;
+ }
+ }, {
+ title: 'Cliente',
+ data: null,
+ render: (r, type) => type === 'display' ?
+ `
${esc(r.company_name)}
${esc(r.customer)}
` : (type === 'export' ? `${r.company_name} (${r.customer})` : `${r.company_name} ${r.customer}`)
+ }, {
+ title: 'Agente',
+ data: 'salesperson'
+ }, {
+ title: 'Area',
+ data: 'area'
+ }, {
+ title: 'Imponibile',
+ data: 'revenue',
+ className: 'text-end amount',
+ render: (v, type, r) => {
+ if (type !== 'display') return +v;
+ if (+r.no_amount) return '
—';
+ return `
${fmtEur(+v, 2)}`;
+ }
+ }],
+ order: [
+ [0, 'desc']
+ ],
+ pageLength: 25,
+ lengthMenu: [10, 25, 50, 100, 500],
+ dom: "<'row mb-2'<'col-md-6'B><'col-md-6'f>>rt<'row mt-2'<'col-md-5'i><'col-md-7'p>>",
+ buttons: xlsButton('Fatture e note di credito' + invFiltersLabel()),
+ language: {
+ url: 'https://cdn.datatables.net/plug-ins/1.13.6/i18n/it-IT.json'
+ }
+ });
+ }
+
+ function fmtDateIt(v) {
+ if (!v) return '';
+ const p = String(v).substring(0, 10).split('-');
+ return `${p[2]}/${p[1]}/${p[0]}`;
+ }
+
// fatturato: sempre in € (indipendente dal selettore N° ordini / Valore)
function renderInvoiced(d) {
const inv = d.invoiced || {};
- $('#invRow').toggle(!!inv.available);
+ $('#invRow, #invTableCard').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 ?
'
Fatturato non ancora disponibile: eseguire la migrazione
mago_sale_doc e una sincronizzazione.' :
@@ -1358,6 +1715,49 @@ if ($action === 'data') {
scheduleLoad(0);
});
+ // tabella dati fatturato: apri/chiudi (stato ricordato) e schede
+ $('#invToggle').on('click', function() {
+ const open = !invOpen();
+ $(this).attr('aria-expanded', open ? 'true' : 'false');
+ $('#invTableBody').stop(true, true)[open ? 'slideDown' : 'slideUp'](180, () => {
+ if (open) renderInvTables();
+ });
+ try {
+ localStorage.setItem(STORE_KEY + '_inv', JSON.stringify({
+ open: open,
+ tab: invTab
+ }));
+ } catch (e) {}
+ });
+ $('#invTabs').on('click', 'button', function() {
+ invTab = $(this).data('tab');
+ $('#invTabs button').removeClass('active');
+ $(this).addClass('active');
+ $('#invPaneMonths').toggle(invTab === 'months');
+ $('#invPaneDocs').toggle(invTab === 'docs');
+ renderInvTables();
+ try {
+ localStorage.setItem(STORE_KEY + '_inv', JSON.stringify({
+ open: invOpen(),
+ tab: invTab
+ }));
+ } catch (e) {}
+ });
+ try {
+ const st = JSON.parse(localStorage.getItem(STORE_KEY + '_inv') || 'null');
+ if (st) {
+ invTab = st.tab === 'docs' ? 'docs' : 'months';
+ $('#invTabs button').removeClass('active');
+ $(`#invTabs button[data-tab="${invTab}"]`).addClass('active');
+ $('#invPaneMonths').toggle(invTab === 'months');
+ $('#invPaneDocs').toggle(invTab === 'docs');
+ if (st.open) {
+ $('#invToggle').attr('aria-expanded', 'true');
+ $('#invTableBody').show();
+ }
+ }
+ } catch (e) {}
+
$('#trendChips').on('click', '.chip', function() {
trendFilter = $(this).data('trend') || '';
if (lastData) renderCustomers(lastData);