From e3a51b56538daba680dbbf136e0c8d47eab17368 Mon Sep 17 00:00:00 2001 From: cammo22 Date: Wed, 16 Sep 2026 12:22:02 +0200 Subject: [PATCH] Modello Excel scaricabile per caricare i dati di un'azienda nuova MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Provando la versione portatile, un'azienda appena creata restava bloccata: l'unica porta d'ingresso dei dati è l'import, che vuole un file nel formato del consulente, e chi non ce l'ha non sapeva da dove partire. - Nuovo generatore del modello (src/main/import/template.ts): stesse intestazioni e sezioni che l'import riconosce, TIPO a tendina, colonne di sotto-classificazione a "X" solo dove la sezione le prevede, foglio ISTRUZIONI. Se l'azienda ha già conti, il modello li riporta con sezione e sotto-classificazione: resta solo da scrivere i valori. - Il modello si rilegge senza perdite: un test genera il file dalle 24 sezioni della migrazione 002, lo compila e lo reimporta (zero righe da sistemare, tag intatti, valori in centesimi corretti). - Endpoint POST /import/chart-of-accounts/template (solo consulente), dialogo "Salva con nome" e apertura automatica del file in Excel. - La schermata Import spiega i tre passi (scarica, compila, importa); la schermata vuota di un'azienda senza bilancio offre subito il modello. Co-Authored-By: Claude Opus 5 --- src/main/db/migrations/002_financial_model.ts | 4 +- src/main/import/chart-of-accounts.ts | 23 +++ src/main/import/template.test.ts | 98 ++++++++++ src/main/import/template.ts | 169 ++++++++++++++++++ src/main/index.ts | 16 ++ src/main/server/routes/analysis.routes.ts | 13 +- src/main/server/services/import.service.ts | 27 ++- src/preload/index.ts | 9 +- src/renderer/src/pages/CompanyPage.tsx | 13 +- .../src/pages/business/ImportPanel.tsx | 86 +++++++++ 10 files changed, 448 insertions(+), 10 deletions(-) create mode 100644 src/main/import/template.test.ts create mode 100644 src/main/import/template.ts diff --git a/src/main/db/migrations/002_financial_model.ts b/src/main/db/migrations/002_financial_model.ts index d7b24cc..3627cbb 100644 --- a/src/main/db/migrations/002_financial_model.ts +++ b/src/main/db/migrations/002_financial_model.ts @@ -24,7 +24,7 @@ import type { Database } from 'better-sqlite3-multiple-ciphers' /** §1 — i cinque TIPI di conto. */ const ACCOUNT_TYPES = ["RICAVO", "COSTO", "ATTIVITA'", "ATTIVITA' NEGATIVO", "PASSIVITA'"] -interface SectionSeed { +export interface SectionSeed { code: string label: string /** Prospetto di destinazione: conto economico o stato patrimoniale. */ @@ -48,7 +48,7 @@ interface SectionSeed { detail_tags: string[] } -const SECTIONS: SectionSeed[] = [ +export const SECTIONS: SectionSeed[] = [ // --- §2.1 Ricavi --------------------------------------------------------- { code: 'ricavi_operativi', diff --git a/src/main/import/chart-of-accounts.ts b/src/main/import/chart-of-accounts.ts index bb0bc93..fa9e21d 100644 --- a/src/main/import/chart-of-accounts.ts +++ b/src/main/import/chart-of-accounts.ts @@ -97,6 +97,29 @@ const DETAIL_TAG_BY_HEADER: Record = { 'debiti verso enti previdenziali': 'Debiti v/Enti Previdenziali' } +/** + * Intestazione con cui il modello scaricabile scrive ogni sotto-classificazione. + * Ognuna deve essere una chiave di DETAIL_TAG_BY_HEADER, una volta normalizzata: + * il modello si deve rileggere senza perdite (lo verifica un test). + */ +export const DETAIL_TAG_TEMPLATE_HEADERS: Record = { + 'Crediti Commerciali': 'Crediti Commerciali', + 'Crediti Diversi': 'Crediti Diversi', + 'Erario c/IVA': 'Erario c/IVA', + 'Utile a nuovo': 'Utile a nuovo', + Utile: 'Utile', + 'Fondo TFR': 'Fondo TFR', + 'Debiti Diversi': 'Debiti Diversi', + 'Debiti v/Fornitori (costi variabili)': 'Debiti verso Fornitori costi variabili', + 'Debiti v/Fornitori (costi fissi)': 'Debiti verso Fornitori costi fissi', + 'Debiti v/Enti Previdenziali': 'Debiti v/Enti Previdenziali' +} + +/** Riconosce una sotto-classificazione dall'intestazione di colonna. */ +export function detailTagFromHeader(header: string): string | null { + return DETAIL_TAG_BY_HEADER[norm(header)] ?? null +} + /** Intestazioni delle colonne che servono, con le varianti già incontrate. */ const COLUMN_ALIASES: Record = { type: ['tipo'], diff --git a/src/main/import/template.test.ts b/src/main/import/template.test.ts new file mode 100644 index 0000000..ad081f0 --- /dev/null +++ b/src/main/import/template.test.ts @@ -0,0 +1,98 @@ +import ExcelJS from 'exceljs' +import { mkdtemp, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterAll, beforeAll, describe, expect, it } from 'vitest' +import { SECTIONS } from '../db/migrations/002_financial_model' +import { DETAIL_TAG_TEMPLATE_HEADERS, detailTagFromHeader, readPreview } from './chart-of-accounts' +import { buildTemplate, type TemplateAccount } from './template' + +/** + * Il modello scaricabile ha senso solo se l'import lo rilegge senza perdite: + * tutte le sezioni riconosciute, nessuna riga da sistemare, sotto-classificazioni + * intatte. + */ + +const ACCOUNTS: TemplateAccount[] = [ + { code: '60.01', name: 'Vendite', section_code: 'ricavi_operativi', account_type: 'RICAVO', detail_tag: null, direct_cost_pct: null }, + { code: '60.20', name: 'Rimanenze finali CE', section_code: 'rimanenze_finali_ricavo', account_type: 'RICAVO', detail_tag: null, direct_cost_pct: null }, + { code: '70.40', name: 'Stipendi', section_code: 'costi_personale', account_type: 'COSTO', detail_tag: null, direct_cost_pct: 60 }, + { code: '11.01', name: 'Impianti', section_code: 'immobilizzazioni_materiali', account_type: "ATTIVITA'", detail_tag: null, direct_cost_pct: null }, + { code: '11.91', name: 'Fondo ammortamento impianti', section_code: 'immobilizzazioni_materiali', account_type: "ATTIVITA' NEGATIVO", detail_tag: null, direct_cost_pct: null }, + { code: '20.01', name: 'Magazzino', section_code: 'rimanenze_finali_magazzino', account_type: "ATTIVITA'", detail_tag: null, direct_cost_pct: null }, + { code: '21.01', name: 'Clienti', section_code: 'liquidita_differite', account_type: "ATTIVITA'", detail_tag: 'Crediti Commerciali', direct_cost_pct: null }, + { code: '40.01', name: 'Fornitori merci', section_code: 'debiti_breve', account_type: "PASSIVITA'", detail_tag: 'Debiti v/Fornitori (costi variabili)', direct_cost_pct: null }, + { code: '32.01', name: 'TFR', section_code: 'debiti_medio_lungo', account_type: "PASSIVITA'", detail_tag: 'Fondo TFR', direct_cost_pct: null } +] + +let dir: string +let path: string + +beforeAll(async () => { + dir = await mkdtemp(join(tmpdir(), 'daprodfinanza-template-')) + path = join(dir, 'modello.xlsx') + await writeFile(path, await buildTemplate(SECTIONS, ACCOUNTS, { companyName: 'Prova' })) +}) + +afterAll(async () => { + await rm(dir, { recursive: true, force: true }) +}) + +describe('modello Excel scaricabile', () => { + it('ogni sotto-classificazione ha un\'intestazione che l\'import riconosce', () => { + for (const [tag, header] of Object.entries(DETAIL_TAG_TEMPLATE_HEADERS)) { + expect(detailTagFromHeader(header)).toBe(tag) + } + const allDetailTags = new Set(SECTIONS.flatMap((s) => s.detail_tags)) + for (const tag of allDetailTags) expect(DETAIL_TAG_TEMPLATE_HEADERS[tag]).toBeDefined() + }) + + it('si rilegge con tutte le sezioni e nessuna riga da sistemare', async () => { + const preview = await readPreview(path) + expect(preview.sections).toHaveLength(SECTIONS.length) + expect(preview.sections.every((s) => s.section_code !== null)).toBe(true) + expect(preview.unmapped).toEqual([]) + expect(preview.duplicates).toEqual([]) + expect(preview.valueColumn).toBe('VALORE T') + expect(preview.templateRows).toBe(SECTIONS.length * 3) + }) + + it('riporta i conti esistenti con sezione, tipo e sotto-classificazione', async () => { + const preview = await readPreview(path) + expect(preview.accounts).toHaveLength(ACCOUNTS.length) + for (const expected of ACCOUNTS) { + const found = preview.accounts.find((a) => a.code === expected.code) + expect(found, expected.code).toMatchObject({ + section_code: expected.section_code, + account_type: expected.account_type, + detail_tag: expected.detail_tag, + amount_cents: null + }) + } + expect(preview.accounts.find((a) => a.code === '70.40')?.direct_cost_pct).toBe(60) + }) + + it('una volta compilato, i valori arrivano all\'import', async () => { + const workbook = new ExcelJS.Workbook() + await workbook.xlsx.readFile(path) + const sheet = workbook.getWorksheet('PIANO DEI CONTI')! + let aggiunto = false + sheet.eachRow((row) => { + if (row.getCell(2).value === '60.01') row.getCell(4).value = 12345.67 + // Un conto nuovo su una riga vuota della sezione. + if (row.getCell(1).value === 'COSTO' && !row.getCell(2).value && !aggiunto) { + row.getCell(2).value = '70.99' + row.getCell(3).value = 'Consulenze' + row.getCell(4).value = 800 + aggiunto = true + } + }) + const filled = join(dir, 'compilato.xlsx') + await workbook.xlsx.writeFile(filled) + + const preview = await readPreview(filled) + expect(preview.accounts.find((a) => a.code === '60.01')?.amount_cents).toBe(1_234_567) + expect(preview.accounts.find((a) => a.code === '70.99')?.amount_cents).toBe(80_000) + expect(preview.unmapped).toEqual([]) + }) +}) diff --git a/src/main/import/template.ts b/src/main/import/template.ts new file mode 100644 index 0000000..717b551 --- /dev/null +++ b/src/main/import/template.ts @@ -0,0 +1,169 @@ +import ExcelJS from 'exceljs' +import type { AccountType } from '@shared/types' +import { DETAIL_TAG_TEMPLATE_HEADERS } from './chart-of-accounts' + +/** + * Modello Excel scaricabile — la porta d'ingresso per chi non ha già il file + * del consulente. + * + * Produce esattamente la forma che `readPreview()` sa leggere: stesse + * intestazioni, sezioni riconosciute dal nome, sotto-classificazioni a "X". + * Se l'azienda ha già un piano dei conti, lo riporta: il consulente deve solo + * scrivere i valori del periodo, non ricostruire la struttura ogni mese. + */ + +export interface TemplateSection { + code: string + label: string + statement: 'CE' | 'SP' + account_type: string + detail_tags: string[] +} + +export interface TemplateAccount { + code: string + name: string + section_code: string + account_type: AccountType + detail_tag: string | null + direct_cost_pct: number | null +} + +/** Righe vuote per sezione, col TIPO già scritto, dove aggiungere conti. */ +const EMPTY_ROWS_PER_SECTION = 3 + +const TYPES: AccountType[] = ['RICAVO', 'COSTO', "ATTIVITA'", "ATTIVITA' NEGATIVO", "PASSIVITA'"] + +/** + * Nel file del consulente "Rimanenze Finali" compare due volte, una nel conto + * economico e una nell'attivo, con la stessa etichetta: la distingue il TIPO. + * Le etichette del database portano la precisazione fra parentesi, che il + * file non ha. + */ +function fileLabel(label: string): string { + return label.replace(/\s*\(.*\)\s*$/, '') +} + +export async function buildTemplate( + sections: TemplateSection[], + accounts: TemplateAccount[], + options: { companyName?: string } = {} +): Promise { + const workbook = new ExcelJS.Workbook() + workbook.creator = 'DaProdFinanza' + workbook.created = new Date() + + const sheet = workbook.addWorksheet('PIANO DEI CONTI', { + views: [{ state: 'frozen', ySplit: 1, xSplit: 3 }] + }) + + const detailTags = Object.keys(DETAIL_TAG_TEMPLATE_HEADERS) + const headers = [ + 'TIPO', + 'Codice Conto Mastro', + 'Descrizione', + 'VALORE T', + '% COSTO DIRETTO', + ...detailTags.map((tag) => DETAIL_TAG_TEMPLATE_HEADERS[tag]!) + ] + const valueCol = 4 + const pctCol = 5 + const firstDetailCol = 6 + + sheet.columns = headers.map((_, i) => ({ + width: i === 2 ? 42 : i === 1 ? 20 : i < 5 ? 16 : 14 + })) + + const header = sheet.addRow(headers) + header.font = { bold: true, color: { argb: 'FFFFFFFF' } } + header.alignment = { vertical: 'middle', wrapText: true } + header.height = 32 + header.eachCell((cell) => { + cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF1E293B' } } + }) + + const typeList = `"${TYPES.join(',')}"` + + const addAccountRow = ( + type: string, + values: { code?: string; name?: string; pct?: number | null; tag?: string | null }, + allowedTags: string[] + ): void => { + const row = sheet.addRow([type, values.code ?? null, values.name ?? null]) + row.getCell(valueCol).numFmt = '#,##0.00' + if (values.pct !== null && values.pct !== undefined) row.getCell(pctCol).value = values.pct + + row.getCell(1).dataValidation = { + type: 'list', + allowBlank: false, + formulae: [typeList], + showErrorMessage: true, + errorTitle: 'TIPO non valido', + error: 'Scegli uno dei cinque tipi di conto.' + } + + // Le colonne di sotto-classificazione hanno senso solo dove la sezione le + // prevede: le altre si lasciano grigie, per non invitare a marcarle. + detailTags.forEach((tag, i) => { + const cell = row.getCell(firstDetailCol + i) + if (allowedTags.includes(tag)) { + cell.alignment = { horizontal: 'center' } + cell.dataValidation = { type: 'list', allowBlank: true, formulae: ['"X"'] } + if (values.tag === tag) cell.value = 'X' + } else { + cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFE2E8F0' } } + } + }) + } + + for (const section of sections) { + const titleRow = sheet.addRow([null, null, fileLabel(section.label)]) + titleRow.font = { bold: true } + titleRow.getCell(3).fill = { + type: 'pattern', + pattern: 'solid', + fgColor: { argb: section.statement === 'CE' ? 'FFDBEAFE' : 'FFDCFCE7' } + } + + for (const account of accounts.filter((a) => a.section_code === section.code)) { + addAccountRow( + account.account_type, + { + code: account.code, + name: account.name, + pct: account.direct_cost_pct, + tag: account.detail_tag + }, + section.detail_tags + ) + } + for (let i = 0; i < EMPTY_ROWS_PER_SECTION; i++) { + addAccountRow(section.account_type, {}, section.detail_tags) + } + } + + const guide = workbook.addWorksheet('ISTRUZIONI') + guide.getColumn(1).width = 110 + const lines = [ + `Modello del piano dei conti${options.companyName ? ` — ${options.companyName}` : ''}`, + '', + 'Come si compila', + '1. Nel foglio PIANO DEI CONTI ogni riga è un conto. Le righe in grassetto sono le sezioni: non vanno modificate.', + '2. Per aggiungere un conto usa le righe vuote sotto la sezione giusta: scrivi codice e descrizione.', + '3. Nella colonna VALORE T scrivi il saldo del periodo. Il periodo (mese o anno) e lo scenario si scelgono al momento dell\'import.', + "4. I costi si scrivono positivi. I fondi (ammortamento, svalutazione) vanno nella loro sezione con TIPO \"ATTIVITA' NEGATIVO\".", + '5. Nelle colonne a destra marca con una X la sotto-classificazione del conto, dove la sezione lo prevede (celle bianche).', + ' Servono a distinguere crediti commerciali, debiti verso fornitori, TFR: senza, giorni di incasso e pagamento e posizione finanziaria non si calcolano.', + '6. La colonna % COSTO DIRETTO è facoltativa: se vuota vale quella della sezione.', + '', + 'Le righe vuote vengono ignorate. Prima di scrivere qualsiasi cosa, DaProdFinanza mostra un riepilogo di quello che ha letto.', + 'Lo stesso file si può riusare: basta cambiare i valori e importarlo su un altro periodo.' + ] + lines.forEach((text, i) => { + const row = guide.addRow([text]) + if (i === 0) row.font = { bold: true, size: 14 } + if (text === 'Come si compila') row.font = { bold: true } + }) + + return Buffer.from(await workbook.xlsx.writeBuffer()) +} diff --git a/src/main/index.ts b/src/main/index.ts index c0c822f..18ac86f 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -73,6 +73,22 @@ function registerIpc(): void { }) return result.canceled ? null : (result.filePaths[0] ?? null) }) + + // "Salva con nome" per il modello Excel da compilare. + ipcMain.handle('dialog:save-excel', async (_event, suggestedName: string) => { + const result = await dialog.showSaveDialog({ + title: 'Salva il modello Excel', + defaultPath: join(app.getPath('documents'), suggestedName), + filters: [{ name: 'Cartella di lavoro Excel', extensions: ['xlsx'] }] + }) + return result.canceled ? null : (result.filePath ?? null) + }) + + // Apre un file appena creato con il programma predefinito (Excel). + ipcMain.handle('shell:open-file', async (_event, path: string) => { + if (typeof path !== 'string' || !path.toLowerCase().endsWith('.xlsx')) return + await shell.openPath(path) + }) } app.whenReady().then(async () => { diff --git a/src/main/server/routes/analysis.routes.ts b/src/main/server/routes/analysis.routes.ts index f4afa72..7d75649 100644 --- a/src/main/server/routes/analysis.routes.ts +++ b/src/main/server/routes/analysis.routes.ts @@ -2,7 +2,11 @@ import { Router, type Request } from 'express' import type { Scheme } from '@shared/engine' import type { Scenario } from '@shared/types' import { analyse, listPeriods, series } from '../services/analysis.service' -import { applyChartOfAccounts, previewChartOfAccounts } from '../services/import.service' +import { + applyChartOfAccounts, + previewChartOfAccounts, + writeChartOfAccountsTemplate +} from '../services/import.service' import { requireAuth, requireRole } from '../middleware/auth' import { HttpError } from '../http-error' @@ -71,3 +75,10 @@ analysisRouter.post('/import/chart-of-accounts', requireRole('consultant'), asyn }) ) }) + +/** Modello Excel da compilare, salvato dove il consulente ha scelto. */ +analysisRouter.post('/import/chart-of-accounts/template', requireRole('consultant'), async (req, res) => { + const { filePath } = req.body ?? {} + if (!filePath) throw new HttpError(400, 'Manca il percorso dove salvare il modello.') + res.status(201).json(await writeChartOfAccountsTemplate(param(req, 'uuid'), filePath)) +}) diff --git a/src/main/server/services/import.service.ts b/src/main/server/services/import.service.ts index 354b5c3..e578e33 100644 --- a/src/main/server/services/import.service.ts +++ b/src/main/server/services/import.service.ts @@ -1,12 +1,14 @@ -import { copyFile } from 'node:fs/promises' +import { copyFile, writeFile } from 'node:fs/promises' import { join } from 'node:path' import type { AccountType } from '@shared/types' import { getDatabase } from '../../db' import { newUuid, nowIso } from '../../lib/ids' import { companyFolder } from '../../lib/paths' import { readPreview, type ImportPreview } from '../../import/chart-of-accounts' +import { buildTemplate, type TemplateAccount } from '../../import/template' import { HttpError } from '../http-error' import { getCompany } from './companies.service' +import { listSections } from './reference.service' /** * Import del piano dei conti: anteprima, poi scrittura — AGENTS.md §11.1. @@ -277,3 +279,26 @@ export async function applyChartOfAccounts( return run() } + +/** + * Modello Excel da compilare, con il piano dei conti già presente + * dell'azienda. È la porta d'ingresso per chi non ha il file del consulente. + */ +export async function writeChartOfAccountsTemplate( + companyUuid: string, + filePath: string +): Promise<{ path: string; accounts: number }> { + const company = getCompany(companyUuid) + const accounts = getDatabase() + .prepare( + `SELECT code, name, section_code, account_type, detail_tag, direct_cost_pct + FROM accounts + WHERE company_uuid = ? AND deleted = 0 + ORDER BY code` + ) + .all(companyUuid) as TemplateAccount[] + + const buffer = await buildTemplate(listSections(), accounts, { companyName: company.name }) + await writeFile(filePath, buffer) + return { path: filePath, accounts: accounts.length } +} diff --git a/src/preload/index.ts b/src/preload/index.ts index 67e2a65..83d25ed 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -21,7 +21,14 @@ const api = { openDataFolder: (): Promise => ipcRenderer.invoke('shell:open-data-folder'), /** Apre il dialogo di sistema per scegliere il file da importare. */ - pickExcelFile: (): Promise => ipcRenderer.invoke('dialog:pick-excel') + pickExcelFile: (): Promise => ipcRenderer.invoke('dialog:pick-excel'), + + /** Dialogo "Salva con nome" per un file Excel. */ + saveExcelFile: (suggestedName: string): Promise => + ipcRenderer.invoke('dialog:save-excel', suggestedName), + + /** Apre un file .xlsx con il programma predefinito. */ + openExcelFile: (path: string): Promise => ipcRenderer.invoke('shell:open-file', path) } export type DaProdApi = typeof api diff --git a/src/renderer/src/pages/CompanyPage.tsx b/src/renderer/src/pages/CompanyPage.tsx index f27abb4..cc5d727 100644 --- a/src/renderer/src/pages/CompanyPage.tsx +++ b/src/renderer/src/pages/CompanyPage.tsx @@ -6,7 +6,7 @@ import { api } from '../lib/api' import type { Vista } from '../components/Sidebar' import { Alert, Button, Card, EmptyState, Select } from '../components/ui' import { BalanceSheetView } from './business/BalanceSheetView' -import { ImportPanel } from './business/ImportPanel' +import { ImportPanel, TemplateButton } from './business/ImportPanel' import { IncomeStatementView } from './business/IncomeStatementView' import { OverviewView } from './business/OverviewView' @@ -174,7 +174,7 @@ export function CompanyPage({ description={ periods.length === 0 ? canImport - ? "Questa azienda esiste in anagrafica ma non ha ancora un bilancio. Importa il piano dei conti e le analisi compaiono da sole." + ? "Questa azienda non ha ancora un bilancio. Scarica il modello Excel, compilalo con i saldi e importalo: le analisi compaiono da sole." : 'Il consulente non ha ancora caricato un bilancio per questa azienda.' : `Il periodo selezionato non ha saldi per lo scenario "${ SCENARI.find((s) => s.id === scenario)?.label @@ -183,9 +183,12 @@ export function CompanyPage({ action={ periods.length === 0 && canImport && ( - +
+ + +
) } /> diff --git a/src/renderer/src/pages/business/ImportPanel.tsx b/src/renderer/src/pages/business/ImportPanel.tsx index 43a631b..5177357 100644 --- a/src/renderer/src/pages/business/ImportPanel.tsx +++ b/src/renderer/src/pages/business/ImportPanel.tsx @@ -25,6 +25,73 @@ interface Preview { alreadyImported: { filename: string; imported_at: string } | null } +/** + * Scarica il modello Excel da compilare. Se l'azienda ha già conti, il modello + * li contiene: resta solo da scrivere i valori. + */ +export function TemplateButton({ + company, + variant = 'ghost' +}: { + company: Company + variant?: 'primary' | 'ghost' +}): React.JSX.Element { + const [busy, setBusy] = useState(false) + const [msg, setMsg] = useState<{ ok: boolean; text: string } | null>(null) + + const scarica = async (): Promise => { + const nome = `Piano dei conti - ${company.name.replace(/[\\/:*?"<>|]/g, '')}.xlsx` + const path = await window.daprod.saveExcelFile(nome) + if (!path) return + setBusy(true) + setMsg(null) + try { + const result = await api.post<{ path: string; accounts: number }>( + `/api/companies/${company.uuid}/import/chart-of-accounts/template`, + { filePath: path } + ) + setMsg({ + ok: true, + text: + result.accounts > 0 + ? `Modello salvato con i ${result.accounts} conti già presenti.` + : 'Modello salvato: aggiungi i conti nelle righe vuote di ogni sezione.' + }) + await window.daprod.openExcelFile(result.path) + } catch (err) { + setMsg({ ok: false, text: err instanceof Error ? err.message : 'Salvataggio non riuscito.' }) + } finally { + setBusy(false) + } + } + + return ( +
+ + {msg && ( + {msg.text} + )} +
+ ) +} + +const PASSI = [ + { + titolo: 'Scarica il modello', + testo: "Un file Excel con tutte le sezioni del bilancio già pronte, e i conti che l'azienda ha già." + }, + { + titolo: 'Compilalo', + testo: 'Un conto per riga, il saldo nella colonna VALORE T. Il foglio ISTRUZIONI spiega il resto.' + }, + { + titolo: 'Importalo', + testo: 'Scegli il file, controlla il riepilogo e indica mese, anno e scenario. Nulla è scritto prima.' + } +] + const MESI = [ 'Gennaio', 'Febbraio', 'Marzo', 'Aprile', 'Maggio', 'Giugno', 'Luglio', 'Agosto', 'Settembre', 'Ottobre', 'Novembre', 'Dicembre' @@ -117,6 +184,25 @@ export function ImportPanel({ {error && {error}} {esito && {esito}} + +
    + {PASSI.map((passo, i) => ( +
  1. +

    + + {i + 1} + + {passo.titolo} +

    +

    {passo.testo}

    +
  2. + ))} +
+
+ +
+
+