{
  "name": "Report settimanale da un foglio di calcolo",
  "nodes": [
    {
      "parameters": {
        "content": "## Report settimanale da un foglio\nOgni lunedì scarica il tuo foglio (vendite, ordini, incassi…) pubblicato come CSV, confronta **ultimi 7 giorni** e **7 giorni precedenti** e ti manda un riepilogo con un breve commento. I numeri li calcola il workflow, l'AI scrive solo il commento.\n\n**Il foglio:** almeno una colonna data (`2026-10-16` o `16/10/2026`) e una colonna importo; la colonna categoria è facoltativa. I nomi delle colonne si impostano in Impostazioni.\n\n**Credenziali da collegare dopo l'importazione**\n- **OpenAI** → nei nodi HTTP Request \"… con AI\": Header Auth con *Name* `Authorization` e *Value* `Bearer LA-TUA-CHIAVE`.\n- **Email (SMTP)** → nei nodi Send Email: l'account da cui partono i messaggi.\n\nPoi apri il nodo **Impostazioni** e cambia solo il blocco iniziale (email, attività, modello).\nFai una prova con *Execute workflow*, poi attivalo con *Publish* (in n8n 1.x: *Test workflow* e interruttore *Active*).",
        "width": 540,
        "height": 430,
        "color": 5
      },
      "id": "6f4ca72c-8e54-51c4-b9e4-b58ea332028b",
      "name": "Leggimi",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        0,
        -250
      ]
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "weeks",
              "weeksInterval": 1,
              "triggerAtDay": [
                1
              ],
              "triggerAtHour": 7,
              "triggerAtMinute": 0
            }
          ]
        }
      },
      "id": "60eadb87-f4cf-56ae-aea7-a189bd5b8b2c",
      "name": "Ogni lunedì",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "// ⚙️ IMPOSTAZIONI — modifica solo questo blocco\nconst cfg = {\n  csvUrl: 'https://docs.google.com/spreadsheets/d/e/IL-TUO-FOGLIO/pub?output=csv',\n  colonnaData: 'data',\n  colonnaImporto: 'importo',\n  colonnaCategoria: 'categoria',        // lascia '' se non c'è\n  model: 'gpt-4o-mini',\n  tuaEmail: 'tu@esempio.it',\n  mittente: 'automazioni@esempio.it',\n  attivita: 'negozio di articoli per la casa',\n};\nreturn [{ json: { cfg } }];\n"
      },
      "id": "02380692-fd20-596f-ba65-41040669f4eb",
      "name": "Impostazioni",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        240,
        300
      ]
    },
    {
      "parameters": {
        "url": "={{ $json.cfg.csvUrl }}",
        "options": {
          "response": {
            "response": {
              "responseFormat": "text"
            }
          }
        }
      },
      "id": "cfe5c417-1bd5-567a-9272-4c1b5006a2cd",
      "name": "Scarica dati",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        480,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "// Legge un CSV (separatore , oppure ;) e restituisce un array di oggetti con\n// le intestazioni in minuscolo come chiavi.\nfunction parseCsv(text) {\n  text = String(text || '').replace(/^﻿/, '');\n  const first = text.split(/\\r?\\n/)[0] || '';\n  const delim = first.split(';').length > first.split(',').length ? ';' : ',';\n  const rows = []; let row = [], cell = '', q = false;\n  for (let i = 0; i < text.length; i++) {\n    const c = text[i];\n    if (q) {\n      if (c === '\"') { if (text[i + 1] === '\"') { cell += '\"'; i++; } else q = false; }\n      else cell += c;\n    } else if (c === '\"') q = true;\n    else if (c === delim) { row.push(cell); cell = ''; }\n    else if (c === '\\n' || c === '\\r') {\n      if (c === '\\r' && text[i + 1] === '\\n') i++;\n      row.push(cell); rows.push(row); row = []; cell = '';\n    } else cell += c;\n  }\n  if (cell !== '' || row.length) { row.push(cell); rows.push(row); }\n  const head = (rows.shift() || []).map(h => h.trim().toLowerCase());\n  return rows\n    .filter(r => r.some(v => v.trim() !== ''))\n    .map(r => Object.fromEntries(head.map((h, i) => [h, (r[i] || '').trim()])));\n}\n\nconst cfg = $('Impostazioni').first().json.cfg;\nfunction numero(v) {\n  let s = String(v || '').replace(/[€\\s]/g, '');\n  if (s.includes(',') && s.lastIndexOf(',') > s.lastIndexOf('.')) s = s.replace(/\\./g, '').replace(',', '.');\n  else s = s.replace(/,/g, '');\n  const n = parseFloat(s);\n  return isFinite(n) ? n : 0;\n}\nfunction data(v) {\n  const s = String(v || '').trim(); let m;\n  if ((m = s.match(/^(\\d{4})-(\\d{1,2})-(\\d{1,2})/))) return new Date(+m[1], m[2] - 1, +m[3]);\n  if ((m = s.match(/^(\\d{1,2})[\\/.-](\\d{1,2})[\\/.-](\\d{2,4})/))) return new Date(m[3].length === 2 ? 2000 + +m[3] : +m[3], m[2] - 1, +m[1]);\n  return null;\n}\nconst oggi = new Date(); oggi.setHours(0, 0, 0, 0);\nconst giorno = 86400000;\nconst inizio = new Date(oggi - 7 * giorno), inizioPrec = new Date(oggi - 14 * giorno);\nconst righe = parseCsv($input.first().json.data);\nconst sett = { totale: 0, righe: 0, categorie: {} }, prec = { totale: 0, righe: 0 };\nlet scartate = 0;\nfor (const r of righe) {\n  const d = data(r[cfg.colonnaData.toLowerCase()]);\n  if (!d) { scartate++; continue; }\n  const imp = numero(r[cfg.colonnaImporto.toLowerCase()]);\n  if (d >= inizio && d < oggi) {\n    sett.totale += imp; sett.righe++;\n    const cat = cfg.colonnaCategoria ? (r[cfg.colonnaCategoria.toLowerCase()] || 'Senza categoria') : null;\n    if (cat) sett.categorie[cat] = (sett.categorie[cat] || 0) + imp;\n  } else if (d >= inizioPrec && d < inizio) { prec.totale += imp; prec.righe++; }\n}\nconst variazione = prec.totale ? (sett.totale - prec.totale) / prec.totale * 100 : null;\nconst top = Object.entries(sett.categorie).sort((a, b) => b[1] - a[1]).slice(0, 5);\nconst stat = {\n  periodo: `${inizio.toLocaleDateString('it-IT')} – ${new Date(oggi - giorno).toLocaleDateString('it-IT')}`,\n  totale: Math.round(sett.totale * 100) / 100, righe: sett.righe,\n  media: sett.righe ? Math.round(sett.totale / sett.righe * 100) / 100 : 0,\n  totalePrecedente: Math.round(prec.totale * 100) / 100, righePrecedenti: prec.righe,\n  variazionePercento: variazione === null ? null : Math.round(variazione * 10) / 10,\n  categoriePrincipali: top.map(([nome, tot]) => ({ nome, totale: Math.round(tot * 100) / 100 })),\n  righeScartate: scartate,\n};\nreturn [{ json: { stat, body: {\n  model: cfg.model, temperature: 0.3,\n  messages: [\n    { role: 'system', content:\n      `Sei il consulente di: ${cfg.attivita}. Scrivi in italiano un commento di 3-5 frasi ai numeri della settimana: ` +\n      `cosa è andato meglio o peggio e una cosa da controllare. Usa solo i dati forniti, non inventare cause né numeri.` },\n    { role: 'user', content: JSON.stringify(stat) },\n  ],\n} } }];\n"
      },
      "id": "09b3ff7b-dd04-50fc-8966-6b12b3962b5e",
      "name": "Calcola",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        720,
        300
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.openai.com/v1/chat/completions",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($json.body) }}",
        "options": {}
      },
      "id": "80eb5314-0921-5cf1-bf4f-9b1542604590",
      "name": "Commenta con AI",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        960,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "const cfg = $('Impostazioni').first().json.cfg;\nconst s = $('Calcola').first().json.stat;\nlet commento = '';\ntry { commento = $input.first().json.choices[0].message.content.trim(); } catch (e) { commento = '(commento non disponibile)'; }\nconst eur = n => n.toLocaleString('it-IT', { style: 'currency', currency: 'EUR' });\nconst nr = n => n === 1 ? '1 riga' : `${n} righe`;\nconst testo = [\n  `Report della settimana ${s.periodo}`, '',\n  `Totale: ${eur(s.totale)} su ${nr(s.righe)} (media ${eur(s.media)})`,\n  `Settimana precedente: ${eur(s.totalePrecedente)} su ${nr(s.righePrecedenti)}` +\n    (s.variazionePercento === null ? '' : ` → ${s.variazionePercento > 0 ? '+' : ''}${s.variazionePercento.toLocaleString('it-IT')}%`),\n  ...(s.categoriePrincipali.length ? ['', 'Categorie principali:', ...s.categoriePrincipali.map(c => `- ${c.nome}: ${eur(c.totale)}`)] : []),\n  '', 'Commento:', commento,\n  ...(s.righeScartate ? ['', `Nota: righe ignorate perché senza una data leggibile: ${s.righeScartate}.`] : []),\n].join('\\n');\nreturn [{ json: { oggetto: `Report settimanale: ${eur(s.totale)}`, testo, a: cfg.tuaEmail, da: cfg.mittente } }];\n"
      },
      "id": "d047af10-f202-597b-adef-4a5247cacc4f",
      "name": "Componi report",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1200,
        300
      ]
    },
    {
      "parameters": {
        "fromEmail": "={{ $json.da }}",
        "toEmail": "={{ $json.a }}",
        "subject": "={{ $json.oggetto }}",
        "emailFormat": "text",
        "text": "={{ $json.testo }}",
        "options": {}
      },
      "id": "cf29a845-296a-552c-b110-e38518ebf77e",
      "name": "Invia report a me",
      "type": "n8n-nodes-base.emailSend",
      "typeVersion": 2.1,
      "position": [
        1440,
        300
      ]
    }
  ],
  "connections": {
    "Ogni lunedì": {
      "main": [
        [
          {
            "node": "Impostazioni",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Impostazioni": {
      "main": [
        [
          {
            "node": "Scarica dati",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Scarica dati": {
      "main": [
        [
          {
            "node": "Calcola",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Calcola": {
      "main": [
        [
          {
            "node": "Commenta con AI",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Commenta con AI": {
      "main": [
        [
          {
            "node": "Componi report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Componi report": {
      "main": [
        [
          {
            "node": "Invia report a me",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "pinData": {},
  "meta": {
    "templateCredsSetupCompleted": false
  },
  "tags": []
}
