From 4a719ded5b149582e21e25883a2417e1082d47ee Mon Sep 17 00:00:00 2001 From: BFLOWS Date: Thu, 23 Apr 2026 14:32:34 +0200 Subject: [PATCH] feat(ar1-admin): riscrittura italiana + 5 tab con Testi PEC + editor form-based MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Riscrittura completa di Ar1AdminConfig.js (490 -> 888 LOC) con UI italianizzata, labels parlanti, editor form-based per layout template, tab 'Testi comunicazioni' con editor dei 5 template email + anteprima server-side. CAMBIAMENTI FUNZIONALI: Tab 1 Template: - 'In uso' (ACTIVE/DRAFT) vs 'Archiviati' ora in DUE Card separate, non mescolati - Nomi varianti in italiano ('A1 — Persona Giuridica (societa, ente)', ecc) - Status tag italiano ('In uso', 'Archiviato', 'Bozza') - Editor layout: DEFAULT modalita form (Brand/Header/Intro/Privacy con campi espliciti nome, logo_url, colori primario+accento, titoli, saluto, corpo introduttivo, URL privacy, testo piede). Toggle 'Modalita avanzata (JSON raw)' per chi vuole editare tutto il layout_config. - Bottone 'Anteprima PDF' presente (placeholder toast TODO — endpoint BE da wirare) - Bottone 'Nuova versione' eredita automaticamente layout_config da ACTIVE corrente Tab 2 Policy: - Tutti i label tradotti in italiano con help text inline per ogni campo - Dropdown 'Categoria documento aziendale' da GET /admin/document-categories (cross-schema read a gepafin_schema.document_category) invece di InputNumber raw. Mostra 'DURC — Documento Unico...', 'ANTIRICICLAGGIO — Dichiarazione...' - Switch con descrizioni espanse (cosa fa, quando si attiva) - Divider visivo tra campi numerici e switch booleani Tab 3 Regole reminder: - Colonna 'Regola' con label italiano parlante + kind tecnico in sottotitolo - Colonna 'Quando parte' calcolata dinamicamente: '30 giorni PRIMA', 'Il giorno della scadenza', '5 giorni DOPO', ecc - Colonna 'Ricorrenza' formattata ('una tantum' vs 'ogni 30 giorni') - Dialog edit: Dropdown PEC_KIND_OPTIONS con 5 etichette italiane (kind disabled se editing esistente), help text inline sul campo offset_days che cambia live ('3 giorni prima della scadenza' / 'giorno della scadenza' / '3 giorni dopo la scadenza') Tab 4 Invio massivo: - Label italiano 'Solo aziende con AR1 scaduta' / 'Solo aziende senza AR1' - Pulsante 'Anteprima (non invia)' con toast descrittivo - Pulsante 'Invia PEC' richiede ConfirmDialog - Messaggio warning giallo chiarisce che la PEC sara dispatchata dal BE Gepafin - Box esito con matched / marked_for_pec / company_ids (trimmato a 30+…) Tab 5 Testi comunicazioni (NUOVO): - Banner info + elenco variabili supportate come Tag cliccabili (7 variabili) - DataTable 5 righe: Tipo comunicazione (label IT + kind mono) / Oggetto / Versione (Tag 'v1', 'v2', ...) / Aggiornato il / Azione 'Modifica' - Dialog edit massimizzabile: subject + body_html (textarea monospace 10 righe) + body_text fallback (5 righe) + note interne - Bottone 'Anteprima (dati di esempio)' chiama POST /admin/ar1-email-templates/ {kind}/preview e mostra rendering HTML interpolato (dangerouslySetInnerHTML) con subject renderizzato + body in box stile email - Save bump version lato BE (toast 'Testo aggiornato (version N)') SERVICE: ar1Service.js esteso da 213 -> 247 LOC: + listDocumentCategories (GET /admin/document-categories) + listEmailTemplates / getEmailTemplate / updateEmailTemplate / previewEmailTemplate (4 metodi admin email) VALIDAZIONE: Parse-check @babel/parser plugin JSX: 2/2 OK (service + Ar1AdminConfig). Hot-reload CRA webpack compiled with 1 warning (solo unused-vars pre-esistenti). COSE NON ANCORA FATTE (next): - Endpoint BE POST /admin/ar1-templates/:id/preview per anteprima PDF (wiring FE: rimuovere toast TODO dentro openEditLayout/tplActiveActionsTpl) - Test manuale dal browser con hard-refresh --- src/modules/ar1/pages/Ar1AdminConfig.js | 704 +++++++++++++++++++----- src/modules/ar1/service/ar1Service.js | 34 ++ 2 files changed, 585 insertions(+), 153 deletions(-) diff --git a/src/modules/ar1/pages/Ar1AdminConfig.js b/src/modules/ar1/pages/Ar1AdminConfig.js index 2d3f0fe..981fe86 100644 --- a/src/modules/ar1/pages/Ar1AdminConfig.js +++ b/src/modules/ar1/pages/Ar1AdminConfig.js @@ -16,20 +16,58 @@ import { Checkbox } from 'primereact/checkbox'; import { Dialog } from 'primereact/dialog'; import { Tag } from 'primereact/tag'; import { Message } from 'primereact/message'; +import { Dropdown } from 'primereact/dropdown'; +import { Divider } from 'primereact/divider'; import { ConfirmDialog, confirmDialog } from 'primereact/confirmdialog'; // service import Ar1Service from '../service/ar1Service'; +// ================================================== +// Mappe label in italiano — SINGLE SOURCE OF TRUTH +// ================================================== + +// Stati template +const TEMPLATE_STATUS_LABEL = { + ACTIVE: 'In uso', + ARCHIVED: 'Archiviato', + DRAFT: 'Bozza' +}; + +// Varianti template +const VARIANT_LABEL = { + A1: 'A1 — Persona Giuridica (societa, ente)', + A2: 'A2 — Ditta Individuale (P.IVA persona fisica)', + A3: 'A3 — Persona Fisica (senza P.IVA)' +}; + +// Kind regole reminder PEC +const PEC_KIND_LABEL = { + AR1_REMINDER_30D: 'Promemoria 30 giorni prima della scadenza', + AR1_REMINDER_7D: 'Promemoria 7 giorni prima della scadenza', + AR1_EXPIRED: 'Notifica alla scadenza (giorno 0)', + AR1_POST_EXPIRED_RECURRING: 'Sollecito ricorrente dopo la scadenza', + AR1_BULK_MANUAL: 'Invio manuale massivo (superadmin)' +}; + +const PEC_KIND_OPTIONS = [ + { label: PEC_KIND_LABEL.AR1_REMINDER_30D, value: 'AR1_REMINDER_30D' }, + { label: PEC_KIND_LABEL.AR1_REMINDER_7D, value: 'AR1_REMINDER_7D' }, + { label: PEC_KIND_LABEL.AR1_EXPIRED, value: 'AR1_EXPIRED' }, + { label: PEC_KIND_LABEL.AR1_POST_EXPIRED_RECURRING, value: 'AR1_POST_EXPIRED_RECURRING' }, + { label: PEC_KIND_LABEL.AR1_BULK_MANUAL, value: 'AR1_BULK_MANUAL' } +]; + /** * Ar1AdminConfig — configurazione AR1 per superadmin. * URL: /ar1-admin * - * 4 sezioni (TabView): - * 1. Template — lista + layout editor L2 + nuova versione - * 2. Policy — singleton (validity, popup, auto-archive, category) - * 3. Regole reminder PEC — CRUD pec-schedule-config - * 4. Invio massivo PEC — bulk-request-recompilation (dry-run + live) + * 5 sezioni (TabView): + * 1. Template — lista (In uso / Archiviati separati) + anteprima PDF + editor form-based layout + * 2. Policy — singleton con dropdown categoria documento + labels in italiano + * 3. Regole — CRUD regole reminder PEC con kind parlante + help inline + * 4. Invio massivo — bulk-request-recompilation (dry-run + live) + * 5. Testi PEC — editor dei 5 template email AR1 (sync BE Gepafin via pull /internal/ar1-email-templates) */ const Ar1AdminConfig = () => { const toast = useRef(null); @@ -40,15 +78,18 @@ const Ar1AdminConfig = () => { const [loadingTpl, setLoadingTpl] = useState(false); const [editLayoutOpen, setEditLayoutOpen] = useState(false); const [editLayoutTpl, setEditLayoutTpl] = useState(null); - const [layoutJsonText, setLayoutJsonText] = useState(''); + const [layoutForm, setLayoutForm] = useState({}); // form strutturato + const [layoutAdvancedJson, setLayoutAdvancedJson] = useState(''); // modalita JSON raw + const [useAdvancedEditor, setUseAdvancedEditor] = useState(false); const [newVersionOpen, setNewVersionOpen] = useState(false); const [newVersionVariant, setNewVersionVariant] = useState('A1'); - const [newVersionData, setNewVersionData] = useState({ version: '', layout_config: '{}', activate_now: true }); + const [newVersionData, setNewVersionData] = useState({ version: '', activate_now: true }); // ========= POLICY ========= const [policy, setPolicy] = useState(null); const [policyDraft, setPolicyDraft] = useState(null); const [savingPolicy, setSavingPolicy] = useState(false); + const [docCategories, setDocCategories] = useState([]); // ========= PEC RULES ========= const [pecRules, setPecRules] = useState([]); @@ -63,24 +104,50 @@ const Ar1AdminConfig = () => { const [bulkResult, setBulkResult] = useState(null); const [bulkRunning, setBulkRunning] = useState(false); + // ========= EMAIL TEMPLATES ========= + const [emailTemplates, setEmailTemplates] = useState([]); + const [loadingEmail, setLoadingEmail] = useState(false); + const [availableVariables, setAvailableVariables] = useState([]); + const [editEmailOpen, setEditEmailOpen] = useState(false); + const [editEmailData, setEditEmailData] = useState(null); + const [previewHtml, setPreviewHtml] = useState(null); + const [previewSubject, setPreviewSubject] = useState(null); + // ---- load all ---- const loadTemplates = () => { setLoadingTpl(true); Ar1Service.listTemplates( (resp) => { setTemplates(resp?.items || resp || []); setLoadingTpl(false); }, - (err) => { setLoadingTpl(false); if (toast.current) toast.current.show({ severity: 'error', summary: 'Errore', detail: err?.detail || 'Load templates fallito' }); } + (err) => { setLoadingTpl(false); if (toast.current) toast.current.show({ severity: 'error', summary: 'Errore', detail: err?.detail || 'Caricamento template fallito' }); } ); }; const loadPolicy = () => { Ar1Service.getPolicy( (resp) => { setPolicy(resp); setPolicyDraft(resp); }, - (err) => { if (toast.current) toast.current.show({ severity: 'error', summary: 'Errore', detail: err?.detail || 'Load policy fallito' }); } + (err) => { if (toast.current) toast.current.show({ severity: 'error', summary: 'Errore', detail: err?.detail || 'Caricamento policy fallito' }); } ); }; const loadPecRules = () => { Ar1Service.listPecSchedule( (resp) => setPecRules(resp?.items || resp || []), - (err) => { if (toast.current) toast.current.show({ severity: 'error', summary: 'Errore', detail: err?.detail || 'Load pec rules fallito' }); } + (err) => { if (toast.current) toast.current.show({ severity: 'error', summary: 'Errore', detail: err?.detail || 'Caricamento regole fallito' }); } + ); + }; + const loadDocCategories = () => { + Ar1Service.listDocumentCategories( + (resp) => setDocCategories(resp?.items || []), + (err) => console.warn('Categorie non caricate:', err) + ); + }; + const loadEmailTemplates = () => { + setLoadingEmail(true); + Ar1Service.listEmailTemplates( + (resp) => { + setEmailTemplates(resp?.items || []); + setAvailableVariables(resp?.available_variables || []); + setLoadingEmail(false); + }, + (err) => { setLoadingEmail(false); if (toast.current) toast.current.show({ severity: 'error', summary: 'Errore', detail: err?.detail || 'Caricamento testi fallito' }); } ); }; @@ -88,54 +155,106 @@ const Ar1AdminConfig = () => { loadTemplates(); loadPolicy(); loadPecRules(); + loadDocCategories(); + loadEmailTemplates(); }, []); - // ========= TEMPLATE HANDLERS ========= + // ================================================== + // SEZIONE 1: TEMPLATE + // ================================================== + + // Divido active vs archived per UI separata + const activeTemplates = templates.filter(t => t.status === 'ACTIVE' || t.status === 'DRAFT'); + const archivedTemplates = templates.filter(t => t.status === 'ARCHIVED'); + const openEditLayout = (tpl) => { setEditLayoutTpl(tpl); - setLayoutJsonText(JSON.stringify(tpl.layout_config || {}, null, 2)); + const lc = tpl.layout_config || {}; + // popolo form con chiavi comuni; altre restano in "advanced JSON" + setLayoutForm({ + brand_name: lc.brand?.name || 'Gepafin S.p.A.', + brand_logo_url: lc.brand?.logo_url || '', + brand_color_primary: lc.brand?.color_primary || '#003d7a', + brand_color_accent: lc.brand?.color_accent || '#e65100', + header_title: lc.header?.title || 'Modulo AR1 — Adeguata Verifica', + header_subtitle: lc.header?.subtitle || 'D.Lgs. 231/2007', + intro_salutation: lc.intro?.salutation || 'Gentile Cliente,', + intro_body: lc.intro?.body || '', + privacy_url: lc.privacy?.url || '', + privacy_body: lc.privacy?.body || '' + }); + setLayoutAdvancedJson(JSON.stringify(lc, null, 2)); + setUseAdvancedEditor(false); setEditLayoutOpen(true); }; + const buildLayoutFromForm = () => ({ + brand: { + name: layoutForm.brand_name, + logo_url: layoutForm.brand_logo_url, + color_primary: layoutForm.brand_color_primary, + color_accent: layoutForm.brand_color_accent + }, + header: { + title: layoutForm.header_title, + subtitle: layoutForm.header_subtitle + }, + intro: { + salutation: layoutForm.intro_salutation, + body: layoutForm.intro_body + }, + privacy: { + url: layoutForm.privacy_url, + body: layoutForm.privacy_body + } + }); + const saveLayout = () => { - let parsed; - try { parsed = JSON.parse(layoutJsonText); } - catch (e) { if (toast.current) toast.current.show({ severity: 'error', summary: 'JSON invalido', detail: e.message }); return; } - Ar1Service.updateTemplateLayout(editLayoutTpl.id, parsed, + let payload; + if (useAdvancedEditor) { + try { payload = JSON.parse(layoutAdvancedJson); } + catch (e) { if (toast.current) toast.current.show({ severity: 'error', summary: 'JSON non valido', detail: e.message }); return; } + } else { + payload = buildLayoutFromForm(); + } + Ar1Service.updateTemplateLayout(editLayoutTpl.id, payload, () => { - if (toast.current) toast.current.show({ severity: 'success', summary: 'OK', detail: 'Layout aggiornato' }); + if (toast.current) toast.current.show({ severity: 'success', summary: 'Salvato', detail: 'Layout aggiornato' }); setEditLayoutOpen(false); loadTemplates(); }, - (err) => { if (toast.current) toast.current.show({ severity: 'error', summary: 'Errore', detail: err?.detail || 'Save fallito' }); } + (err) => { if (toast.current) toast.current.show({ severity: 'error', summary: 'Errore', detail: err?.detail || 'Salvataggio fallito' }); } ); }; const openNewVersion = (variant) => { setNewVersionVariant(variant); - setNewVersionData({ version: '', layout_config: '{}', activate_now: true }); + setNewVersionData({ version: '', activate_now: true }); setNewVersionOpen(true); }; const saveNewVersion = () => { - let layoutParsed; - try { layoutParsed = JSON.parse(newVersionData.layout_config); } - catch (e) { if (toast.current) toast.current.show({ severity: 'error', summary: 'Layout JSON invalido', detail: e.message }); return; } + // Eredita layout_config dalla versione ACTIVE corrente + const active = templates.find(t => t.variant === newVersionVariant && t.status === 'ACTIVE'); + const layoutConfig = active?.layout_config || {}; Ar1Service.createNewTemplateVersion(newVersionVariant, { version: newVersionData.version, - layout_config: layoutParsed, + layout_config: layoutConfig, activate_now: newVersionData.activate_now }, () => { - if (toast.current) toast.current.show({ severity: 'success', summary: 'OK', detail: `Nuova versione ${newVersionVariant} v${newVersionData.version} creata` }); + if (toast.current) toast.current.show({ severity: 'success', summary: 'Creata', detail: `Nuova versione ${newVersionVariant} v${newVersionData.version}` }); setNewVersionOpen(false); loadTemplates(); }, - (err) => { if (toast.current) toast.current.show({ severity: 'error', summary: 'Errore', detail: err?.detail || 'Create fallito' }); } + (err) => { if (toast.current) toast.current.show({ severity: 'error', summary: 'Errore', detail: err?.detail || 'Creazione fallita' }); } ); }; - // ========= POLICY HANDLERS ========= + // ================================================== + // SEZIONE 2: POLICY + // ================================================== + const savePolicy = () => { setSavingPolicy(true); const patch = { @@ -151,16 +270,19 @@ const Ar1AdminConfig = () => { setSavingPolicy(false); setPolicy(resp); setPolicyDraft(resp); - if (toast.current) toast.current.show({ severity: 'success', summary: 'OK', detail: 'Policy aggiornata' }); + if (toast.current) toast.current.show({ severity: 'success', summary: 'Salvata', detail: 'Policy aggiornata' }); }, (err) => { setSavingPolicy(false); - if (toast.current) toast.current.show({ severity: 'error', summary: 'Errore', detail: err?.detail || 'Save policy fallito' }); + if (toast.current) toast.current.show({ severity: 'error', summary: 'Errore', detail: err?.detail || 'Salvataggio fallito' }); } ); }; - // ========= PEC RULE HANDLERS ========= + // ================================================== + // SEZIONE 3: REGOLE PEC + // ================================================== + const openPecDialog = (rule) => { if (rule) { setPecEditing(rule); @@ -174,7 +296,7 @@ const Ar1AdminConfig = () => { }); } else { setPecEditing(null); - setPecDraft({ kind: '', offset_days: 0, is_recurring: false, recurring_interval_days: null, enabled: true, description: '' }); + setPecDraft({ kind: '', offset_days: 30, is_recurring: false, recurring_interval_days: null, enabled: true, description: '' }); } setPecDialogOpen(true); }; @@ -182,307 +304,583 @@ const Ar1AdminConfig = () => { const savePecRule = () => { const payload = { ...pecDraft }; const onOk = () => { - if (toast.current) toast.current.show({ severity: 'success', summary: 'OK', detail: pecEditing ? 'Regola aggiornata' : 'Regola creata' }); + if (toast.current) toast.current.show({ severity: 'success', summary: 'Salvata', detail: pecEditing ? 'Regola aggiornata' : 'Regola creata' }); setPecDialogOpen(false); loadPecRules(); }; - const onKo = (err) => { if (toast.current) toast.current.show({ severity: 'error', summary: 'Errore', detail: err?.detail || 'Save fallito' }); }; + const onKo = (err) => { if (toast.current) toast.current.show({ severity: 'error', summary: 'Errore', detail: err?.detail || 'Salvataggio fallito' }); }; if (pecEditing) Ar1Service.updatePecRule(pecEditing.id, payload, onOk, onKo); else Ar1Service.createPecRule(payload, onOk, onKo); }; const deletePecRule = (rule) => { confirmDialog({ - message: `Eliminare la regola "${rule.kind}"?`, + message: `Eliminare la regola "${PEC_KIND_LABEL[rule.kind] || rule.kind}"? Non sara piu inviata la PEC corrispondente.`, header: 'Conferma eliminazione', icon: 'pi pi-exclamation-triangle', + acceptLabel: 'Elimina', + rejectLabel: 'Annulla', acceptClassName: 'p-button-danger', accept: () => { Ar1Service.deletePecRule(rule.id, () => { - if (toast.current) toast.current.show({ severity: 'success', summary: 'OK', detail: 'Regola eliminata' }); + if (toast.current) toast.current.show({ severity: 'success', summary: 'Eliminata', detail: 'Regola eliminata' }); loadPecRules(); }, - (err) => { if (toast.current) toast.current.show({ severity: 'error', summary: 'Errore', detail: err?.detail || 'Delete fallito' }); } + (err) => { if (toast.current) toast.current.show({ severity: 'error', summary: 'Errore', detail: err?.detail || 'Eliminazione fallita' }); } ); } }); }; - // ========= BULK HANDLERS ========= + // ================================================== + // SEZIONE 4: BULK + // ================================================== + const runBulk = (dryRun) => { setBulkRunning(true); const companyIds = bulkCompanyIds.trim() ? bulkCompanyIds.split(',').map(s => parseInt(s.trim(), 10)).filter(n => !isNaN(n)) : null; - const payload = { - dry_run: dryRun, - only_expired: bulkOnlyExpired, - only_missing: bulkOnlyMissing, - company_ids: companyIds, - }; + const payload = { dry_run: dryRun, only_expired: bulkOnlyExpired, only_missing: bulkOnlyMissing, company_ids: companyIds }; Ar1Service.bulkRequestRecompilation(payload, (resp) => { setBulkRunning(false); setBulkResult({ ...resp, was_dry_run: dryRun }); if (toast.current) toast.current.show({ severity: 'success', - summary: dryRun ? 'Dry-run completato' : 'Bulk eseguito', + summary: dryRun ? 'Anteprima completata' : 'PEC inviate', detail: `${resp.matched || 0} aziende matchate` }); }, (err) => { setBulkRunning(false); - if (toast.current) toast.current.show({ severity: 'error', summary: 'Errore', detail: err?.detail || 'Bulk fallito' }); + if (toast.current) toast.current.show({ severity: 'error', summary: 'Errore', detail: err?.detail || 'Invio fallito' }); } ); }; - // ---------- RENDER ---------- - const tplStatusTpl = (row) => { - const severity = row.status === 'ACTIVE' ? 'success' : row.status === 'ARCHIVED' ? 'secondary' : 'warning'; - return ; + // ================================================== + // SEZIONE 5: TESTI PEC + // ================================================== + + const openEditEmail = (tpl) => { + setEditEmailData({ ...tpl }); + setPreviewHtml(null); + setPreviewSubject(null); + setEditEmailOpen(true); }; - const tplActionsTpl = (row) => ( + const saveEmail = () => { + if (!editEmailData) return; + Ar1Service.updateEmailTemplate(editEmailData.kind, { + subject: editEmailData.subject, + body_html: editEmailData.body_html, + body_text: editEmailData.body_text, + description: editEmailData.description || null + }, + (resp) => { + if (toast.current) toast.current.show({ + severity: 'success', + summary: 'Salvato', + detail: `Testo "${PEC_KIND_LABEL[resp.kind] || resp.kind}" aggiornato (version ${resp.version})` + }); + setEditEmailOpen(false); + loadEmailTemplates(); + }, + (err) => { if (toast.current) toast.current.show({ severity: 'error', summary: 'Errore', detail: err?.detail || 'Salvataggio fallito' }); } + ); + }; + + const runPreview = () => { + if (!editEmailData) return; + Ar1Service.previewEmailTemplate(editEmailData.kind, {}, // usa default mock del BE + (resp) => { + setPreviewSubject(resp.subject); + setPreviewHtml(resp.body_html); + }, + (err) => { if (toast.current) toast.current.show({ severity: 'error', summary: 'Errore', detail: err?.detail || 'Anteprima fallita' }); } + ); + }; + + // ================================================== + // RENDER + // ================================================== + + const tplStatusTpl = (row) => { + const severity = row.status === 'ACTIVE' ? 'success' : row.status === 'ARCHIVED' ? 'secondary' : 'warning'; + return ; + }; + + const variantTpl = (row) => VARIANT_LABEL[row.variant] || row.variant; + + const tplActiveActionsTpl = (row) => (
-
); + const pecKindTpl = (row) => ( +
+
{PEC_KIND_LABEL[row.kind] || row.kind}
+ {row.kind} +
+ ); + + const pecWhenTpl = (row) => { + if (row.offset_days > 0) return `${row.offset_days} giorni PRIMA della scadenza`; + if (row.offset_days === 0) return 'Il giorno della scadenza'; + return `${Math.abs(row.offset_days)} giorni DOPO la scadenza`; + }; + + const pecRecurringTpl = (row) => { + if (!row.is_recurring) return una tantum; + return ogni {row.recurring_interval_days} giorni; + }; + const pecActionsTpl = (row) => (
-
); - const pecBoolTpl = (row, field) => ; + const pecEnabledTpl = (row) => ( + + ); + + const emailActionsTpl = (row) => ( +