PrimeReact leftIcon non ha margin-right di default, le icone erano incollate
al testo ('Template', 'Policy', etc). Aggiunta classe utility mr-2
(margin-right 0.5rem) su tutti e 5 i TabPanel, pattern identico a quello
usato in rendicontazione (es. IstruttoriaPratica.js linea con
leftIcon='pi pi-file mr-2').
990 lines
55 KiB
JavaScript
990 lines
55 KiB
JavaScript
import React, { useEffect, useRef, useState } from 'react';
|
|
import { __ } from '@wordpress/i18n';
|
|
|
|
// prime
|
|
import { Card } from 'primereact/card';
|
|
import { Button } from 'primereact/button';
|
|
import { Toast } from 'primereact/toast';
|
|
import { DataTable } from 'primereact/datatable';
|
|
import { Column } from 'primereact/column';
|
|
import { TabView, TabPanel } from 'primereact/tabview';
|
|
import { InputNumber } from 'primereact/inputnumber';
|
|
import { InputSwitch } from 'primereact/inputswitch';
|
|
import { InputText } from 'primereact/inputtext';
|
|
import { InputTextarea } from 'primereact/inputtextarea';
|
|
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. (build 1776950352)
|
|
* URL: /ar1-admin
|
|
*
|
|
* 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);
|
|
const [activeIndex, setActiveIndex] = useState(0);
|
|
|
|
// ========= TEMPLATES =========
|
|
const [templates, setTemplates] = useState([]);
|
|
const [loadingTpl, setLoadingTpl] = useState(false);
|
|
const [editLayoutOpen, setEditLayoutOpen] = useState(false);
|
|
const [editLayoutTpl, setEditLayoutTpl] = useState(null);
|
|
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: '', 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([]);
|
|
const [pecDialogOpen, setPecDialogOpen] = useState(false);
|
|
const [pecEditing, setPecEditing] = useState(null);
|
|
const [pecDraft, setPecDraft] = useState({ kind: '', offset_days: 0, is_recurring: false, recurring_interval_days: null, enabled: true, description: '' });
|
|
|
|
// ========= BULK =========
|
|
const [bulkCompanyIds, setBulkCompanyIds] = useState('');
|
|
const [bulkOnlyExpired, setBulkOnlyExpired] = useState(false);
|
|
const [bulkOnlyMissing, setBulkOnlyMissing] = useState(false);
|
|
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);
|
|
// Tracking campo attivo per inserimento variabili con click
|
|
const [activeEmailField, setActiveEmailField] = useState('body_html'); // 'subject' | 'body_html' | 'body_text'
|
|
const subjectInputRef = useRef(null);
|
|
const bodyHtmlInputRef = useRef(null);
|
|
const bodyTextInputRef = useRef(null);
|
|
|
|
// Inserisce {{variabile}} nel campo attivo, al cursore se possibile
|
|
const insertVariable = (varName) => {
|
|
if (!editEmailData) return;
|
|
const token = '{{' + varName + '}}';
|
|
const fieldName = activeEmailField;
|
|
const refMap = { subject: subjectInputRef, body_html: bodyHtmlInputRef, body_text: bodyTextInputRef };
|
|
const ref = refMap[fieldName];
|
|
const currentValue = editEmailData[fieldName] || '';
|
|
|
|
// InputText/InputTextarea PrimeReact espone l'elemento nativo via .getElement() o .current.element in v10+
|
|
// fallback sicuro: appendo in coda
|
|
let insertAt = currentValue.length;
|
|
let el = null;
|
|
try {
|
|
el = ref?.current?.getInput ? ref.current.getInput() : (ref?.current?.element || ref?.current);
|
|
if (el && typeof el.selectionStart === 'number') {
|
|
insertAt = el.selectionStart;
|
|
}
|
|
} catch (e) {
|
|
// ignore, insertAt rimane a length
|
|
}
|
|
|
|
const before = currentValue.slice(0, insertAt);
|
|
const after = currentValue.slice(insertAt);
|
|
const newValue = before + token + after;
|
|
|
|
setEditEmailData({ ...editEmailData, [fieldName]: newValue });
|
|
|
|
// riposiziono focus e cursor dopo il token appena inserito
|
|
setTimeout(() => {
|
|
try {
|
|
if (el && typeof el.setSelectionRange === 'function') {
|
|
el.focus();
|
|
const pos = insertAt + token.length;
|
|
el.setSelectionRange(pos, pos);
|
|
}
|
|
} catch (e) { /* ignore */ }
|
|
}, 0);
|
|
};
|
|
|
|
// ---- 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 || '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 || '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 || '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' }); }
|
|
);
|
|
};
|
|
|
|
useEffect(() => {
|
|
loadTemplates();
|
|
loadPolicy();
|
|
loadPecRules();
|
|
loadDocCategories();
|
|
loadEmailTemplates();
|
|
}, []);
|
|
|
|
// ==================================================
|
|
// 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);
|
|
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 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: 'Salvato', detail: 'Layout aggiornato' });
|
|
setEditLayoutOpen(false);
|
|
loadTemplates();
|
|
},
|
|
(err) => { if (toast.current) toast.current.show({ severity: 'error', summary: 'Errore', detail: err?.detail || 'Salvataggio fallito' }); }
|
|
);
|
|
};
|
|
|
|
const openNewVersion = (variant) => {
|
|
setNewVersionVariant(variant);
|
|
setNewVersionData({ version: '', activate_now: true });
|
|
setNewVersionOpen(true);
|
|
};
|
|
|
|
const saveNewVersion = () => {
|
|
// 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: layoutConfig,
|
|
activate_now: newVersionData.activate_now
|
|
},
|
|
() => {
|
|
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 || 'Creazione fallita' }); }
|
|
);
|
|
};
|
|
|
|
// ==================================================
|
|
// SEZIONE 2: POLICY
|
|
// ==================================================
|
|
|
|
const savePolicy = () => {
|
|
setSavingPolicy(true);
|
|
const patch = {
|
|
validity_days: policyDraft.validity_days,
|
|
popup_dismiss_hours: policyDraft.popup_dismiss_hours,
|
|
popup_force_on_expired: policyDraft.popup_force_on_expired,
|
|
auto_archive_on_company_document: policyDraft.auto_archive_on_company_document,
|
|
company_document_category_id: policyDraft.company_document_category_id,
|
|
allow_bulk_recompilation_request: policyDraft.allow_bulk_recompilation_request,
|
|
};
|
|
Ar1Service.updatePolicy(patch,
|
|
(resp) => {
|
|
setSavingPolicy(false);
|
|
setPolicy(resp);
|
|
setPolicyDraft(resp);
|
|
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 || 'Salvataggio fallito' });
|
|
}
|
|
);
|
|
};
|
|
|
|
// ==================================================
|
|
// SEZIONE 3: REGOLE PEC
|
|
// ==================================================
|
|
|
|
const openPecDialog = (rule) => {
|
|
if (rule) {
|
|
setPecEditing(rule);
|
|
setPecDraft({
|
|
kind: rule.kind,
|
|
offset_days: rule.offset_days,
|
|
is_recurring: rule.is_recurring,
|
|
recurring_interval_days: rule.recurring_interval_days,
|
|
enabled: rule.enabled,
|
|
description: rule.description || ''
|
|
});
|
|
} else {
|
|
setPecEditing(null);
|
|
setPecDraft({ kind: '', offset_days: 30, is_recurring: false, recurring_interval_days: null, enabled: true, description: '' });
|
|
}
|
|
setPecDialogOpen(true);
|
|
};
|
|
|
|
const savePecRule = () => {
|
|
const payload = { ...pecDraft };
|
|
const onOk = () => {
|
|
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 || '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 "${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: 'Eliminata', detail: 'Regola eliminata' });
|
|
loadPecRules();
|
|
},
|
|
(err) => { if (toast.current) toast.current.show({ severity: 'error', summary: 'Errore', detail: err?.detail || 'Eliminazione fallita' }); }
|
|
);
|
|
}
|
|
});
|
|
};
|
|
|
|
// ==================================================
|
|
// 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 };
|
|
Ar1Service.bulkRequestRecompilation(payload,
|
|
(resp) => {
|
|
setBulkRunning(false);
|
|
setBulkResult({ ...resp, was_dry_run: dryRun });
|
|
if (toast.current) toast.current.show({
|
|
severity: 'success',
|
|
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 || 'Invio fallito' });
|
|
}
|
|
);
|
|
};
|
|
|
|
// ==================================================
|
|
// SEZIONE 5: TESTI PEC
|
|
// ==================================================
|
|
|
|
const openEditEmail = (tpl) => {
|
|
setEditEmailData({ ...tpl });
|
|
setPreviewHtml(null);
|
|
setPreviewSubject(null);
|
|
setEditEmailOpen(true);
|
|
};
|
|
|
|
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 <Tag severity={severity} value={TEMPLATE_STATUS_LABEL[row.status] || row.status} />;
|
|
};
|
|
|
|
const variantTpl = (row) => VARIANT_LABEL[row.variant] || row.variant;
|
|
|
|
const tplActiveActionsTpl = (row) => (
|
|
<div style={{ display: 'flex', gap: '0.25rem' }}>
|
|
<Button icon="pi pi-eye" rounded outlined size="small" severity="info"
|
|
tooltip="Anteprima PDF (dati di esempio)" tooltipOptions={{ position: 'top' }}
|
|
onClick={async () => {
|
|
try {
|
|
if (toast.current) toast.current.show({ severity: 'info', summary: 'Generazione anteprima...', detail: 'Attendere qualche secondo', life: 3000 });
|
|
const blob = await Ar1Service.previewTemplatePdf(row.id);
|
|
const url = URL.createObjectURL(blob);
|
|
window.open(url, '_blank');
|
|
setTimeout(() => URL.revokeObjectURL(url), 60000);
|
|
} catch (e) {
|
|
if (toast.current) toast.current.show({ severity: 'error', summary: 'Errore anteprima', detail: e.message || 'Generazione PDF fallita' });
|
|
}
|
|
}} />
|
|
<Button icon="pi pi-pencil" rounded outlined size="small"
|
|
tooltip="Modifica layout grafico" tooltipOptions={{ position: 'top' }}
|
|
onClick={() => openEditLayout(row)} disabled={row.status === 'ARCHIVED'} />
|
|
<Button icon="pi pi-plus" rounded outlined size="small" severity="warning"
|
|
tooltip="Nuova versione" tooltipOptions={{ position: 'top' }}
|
|
onClick={() => openNewVersion(row.variant)} />
|
|
</div>
|
|
);
|
|
|
|
const pecKindTpl = (row) => (
|
|
<div>
|
|
<div style={{ fontWeight: 500 }}>{PEC_KIND_LABEL[row.kind] || row.kind}</div>
|
|
<small style={{ color: '#888' }}>{row.kind}</small>
|
|
</div>
|
|
);
|
|
|
|
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 <span style={{ color: '#888' }}>una tantum</span>;
|
|
return <span>ogni <strong>{row.recurring_interval_days}</strong> giorni</span>;
|
|
};
|
|
|
|
const pecActionsTpl = (row) => (
|
|
<div style={{ display: 'flex', gap: '0.25rem' }}>
|
|
<Button icon="pi pi-pencil" rounded outlined size="small" severity="info"
|
|
tooltip="Modifica" tooltipOptions={{ position: 'top' }}
|
|
onClick={() => openPecDialog(row)} />
|
|
<Button icon="pi pi-trash" rounded outlined size="small" severity="danger"
|
|
tooltip="Elimina" tooltipOptions={{ position: 'top' }}
|
|
onClick={() => deletePecRule(row)} />
|
|
</div>
|
|
);
|
|
|
|
const pecEnabledTpl = (row) => (
|
|
<i className={row.enabled ? 'pi pi-check-circle' : 'pi pi-times-circle'}
|
|
style={{ color: row.enabled ? '#2e7d32' : '#999', fontSize: 18 }} />
|
|
);
|
|
|
|
const emailActionsTpl = (row) => (
|
|
<div style={{ display: 'flex', gap: '0.25rem' }}>
|
|
<Button icon="pi pi-pencil" rounded outlined size="small" severity="info"
|
|
tooltip="Modifica testo" tooltipOptions={{ position: 'top' }}
|
|
onClick={() => openEditEmail(row)} />
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<div style={{ padding: 16 }}>
|
|
<Toast ref={toast} />
|
|
<ConfirmDialog />
|
|
|
|
<h1>Configurazione AR1 — Adeguata Verifica</h1>
|
|
<p style={{ color: '#666' }}>
|
|
Gestione template, policy, regole di scadenza, invio massivo e testi PEC per il modulo antiriciclaggio (D.Lgs. 231/2007).
|
|
</p>
|
|
|
|
<TabView activeIndex={activeIndex} onTabChange={(e) => setActiveIndex(e.index)}>
|
|
|
|
{/* ================ TAB 1: TEMPLATE ================ */}
|
|
<TabPanel header="Template" leftIcon="pi pi-file-edit mr-2">
|
|
<Card title="Template in uso" style={{ marginBottom: 14 }}>
|
|
<Message severity="info" style={{ marginBottom: 14 }} text="Questi sono i template attualmente usati per la compilazione AR1. Le modifiche al layout grafico sono immediate per tutte le nuove bozze. Per cambiamenti strutturali, crea una nuova versione." />
|
|
<DataTable value={activeTemplates} loading={loadingTpl} emptyMessage="Nessun template in uso">
|
|
<Column field="variant" header="Variante" body={variantTpl} />
|
|
<Column field="version" header="Versione" style={{ width: 120 }} body={(r) => <strong>v{r.version}</strong>} />
|
|
<Column field="status" header="Stato" body={tplStatusTpl} style={{ width: 140 }} />
|
|
<Column field="quadri_count" header="N. quadri" style={{ width: 110 }} body={(r) => r.quadri_count ?? (r.questions_snapshot?.quadri?.length ?? '—')} />
|
|
<Column header="Azioni" body={tplActiveActionsTpl} style={{ width: 170 }} />
|
|
</DataTable>
|
|
</Card>
|
|
|
|
{archivedTemplates.length > 0 && (
|
|
<Card title={`Template archiviati (${archivedTemplates.length})`}>
|
|
<DataTable value={archivedTemplates} paginator={archivedTemplates.length > 10} rows={10}>
|
|
<Column field="variant" header="Variante" body={variantTpl} />
|
|
<Column field="version" header="Versione" body={(r) => `v${r.version}`} />
|
|
<Column field="status" header="Stato" body={tplStatusTpl} />
|
|
<Column field="created_at" header="Creato il" body={(r) => r.created_at ? new Date(r.created_at).toLocaleDateString('it-IT') : '—'} />
|
|
</DataTable>
|
|
</Card>
|
|
)}
|
|
</TabPanel>
|
|
|
|
{/* ================ TAB 2: POLICY ================ */}
|
|
<TabPanel header="Policy" leftIcon="pi pi-cog mr-2">
|
|
<Card>
|
|
{!policyDraft && <p>Caricamento…</p>}
|
|
{policyDraft && (
|
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 20 }}>
|
|
<div>
|
|
<label style={{ display: 'block', marginBottom: 4, fontWeight: 500 }}>Validita dichiarazione (giorni)</label>
|
|
<InputNumber value={policyDraft.validity_days} onValueChange={(e) => setPolicyDraft({ ...policyDraft, validity_days: e.value })} min={30} max={1825} style={{ width: '100%' }} />
|
|
<small style={{ color: '#888' }}>Giorni dopo la firma prima che la dichiarazione scada (default 365, min 30, max 1825).</small>
|
|
</div>
|
|
|
|
<div>
|
|
<label style={{ display: 'block', marginBottom: 4, fontWeight: 500 }}>Pausa pop-up (ore)</label>
|
|
<InputNumber value={policyDraft.popup_dismiss_hours} onValueChange={(e) => setPolicyDraft({ ...policyDraft, popup_dismiss_hours: e.value })} min={1} max={168} style={{ width: '100%' }} />
|
|
<small style={{ color: '#888' }}>Ore di silenzio dopo che l'utente clicca "Ricordamelo piu tardi" sul pop-up (default 24).</small>
|
|
</div>
|
|
|
|
<div style={{ gridColumn: '1 / -1' }}>
|
|
<label style={{ display: 'block', marginBottom: 4, fontWeight: 500 }}>Categoria documento aziendale</label>
|
|
<Dropdown
|
|
value={policyDraft.company_document_category_id}
|
|
options={docCategories.map(c => ({ label: `${c.category_name} — ${c.description || ''}`, value: c.id }))}
|
|
onChange={(e) => setPolicyDraft({ ...policyDraft, company_document_category_id: e.value })}
|
|
style={{ width: '100%' }}
|
|
placeholder="Seleziona categoria…"
|
|
/>
|
|
<small style={{ color: '#888' }}>Categoria in cui l'AR1 firmato viene archiviato nei documenti aziendali (visibile in "I miei documenti").</small>
|
|
</div>
|
|
|
|
<Divider style={{ gridColumn: '1 / -1' }} />
|
|
|
|
<div style={{ gridColumn: '1 / -1', display: 'flex', flexDirection: 'column', gap: 14 }}>
|
|
<label style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
|
<InputSwitch checked={!!policyDraft.popup_force_on_expired} onChange={(e) => setPolicyDraft({ ...policyDraft, popup_force_on_expired: e.value })} />
|
|
<span>
|
|
<strong>Pop-up bloccante se scaduta</strong><br />
|
|
<small style={{ color: '#888' }}>Se attivo, al login con AR1 scaduta il pop-up non puo essere chiuso (solo tramite compilazione).</small>
|
|
</span>
|
|
</label>
|
|
|
|
<label style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
|
<InputSwitch checked={!!policyDraft.auto_archive_on_company_document} onChange={(e) => setPolicyDraft({ ...policyDraft, auto_archive_on_company_document: e.value })} />
|
|
<span>
|
|
<strong>Archiviazione automatica</strong><br />
|
|
<small style={{ color: '#888' }}>Alla verifica della firma (VERIFIED), il PDF viene archiviato automaticamente nei documenti aziendali nella categoria selezionata sopra.</small>
|
|
</span>
|
|
</label>
|
|
|
|
<label style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
|
<InputSwitch checked={!!policyDraft.allow_bulk_recompilation_request} onChange={(e) => setPolicyDraft({ ...policyDraft, allow_bulk_recompilation_request: e.value })} />
|
|
<span>
|
|
<strong>Invio massivo PEC abilitato</strong><br />
|
|
<small style={{ color: '#888' }}>Se attivo, il superadmin puo richiedere via PEC la ricompilazione a piu aziende contemporaneamente (tab "Invio massivo").</small>
|
|
</span>
|
|
</label>
|
|
</div>
|
|
|
|
<div style={{ gridColumn: '1 / -1', display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 12 }}>
|
|
<Button label="Annulla" severity="secondary" outlined onClick={() => setPolicyDraft(policy)} disabled={savingPolicy} />
|
|
<Button label="Salva policy" icon="pi pi-save" onClick={savePolicy} loading={savingPolicy} />
|
|
</div>
|
|
</div>
|
|
)}
|
|
</Card>
|
|
</TabPanel>
|
|
|
|
{/* ================ TAB 3: REGOLE ================ */}
|
|
<TabPanel header="Regole reminder" leftIcon="pi pi-bell mr-2">
|
|
<Card>
|
|
<Message severity="info" style={{ marginBottom: 14 }} text="Le regole definiscono QUANDO partono automaticamente le PEC di sollecito. Per il contenuto dei testi, vai al tab 'Testi comunicazioni'." />
|
|
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: 10 }}>
|
|
<Button label="Nuova regola" icon="pi pi-plus" onClick={() => openPecDialog(null)} />
|
|
</div>
|
|
<DataTable value={pecRules} emptyMessage="Nessuna regola configurata">
|
|
<Column field="kind" header="Regola" body={pecKindTpl} />
|
|
<Column field="offset_days" header="Quando parte" body={pecWhenTpl} style={{ width: 240 }} />
|
|
<Column field="is_recurring" header="Ricorrenza" body={pecRecurringTpl} style={{ width: 160 }} />
|
|
<Column field="enabled" header="Attiva" body={pecEnabledTpl} style={{ width: 90 }} />
|
|
<Column header="Azioni" body={pecActionsTpl} style={{ width: 130 }} />
|
|
</DataTable>
|
|
</Card>
|
|
</TabPanel>
|
|
|
|
{/* ================ TAB 4: BULK PEC ================ */}
|
|
<TabPanel header="Invio massivo" leftIcon="pi pi-send mr-2">
|
|
<Card>
|
|
<Message severity="warn" style={{ marginBottom: 14 }} text="Questa azione segna le aziende selezionate per l'invio di una PEC di sollecito AR1. Esegui sempre prima l'anteprima per verificare il numero di destinatari." />
|
|
|
|
<div style={{ marginBottom: 12 }}>
|
|
<label style={{ display: 'block', marginBottom: 4, fontWeight: 500 }}>ID aziende (virgola-separati, vuoto = tutte)</label>
|
|
<InputText value={bulkCompanyIds} onChange={(e) => setBulkCompanyIds(e.target.value)} placeholder="es. 1, 7, 42" style={{ width: '100%' }} />
|
|
</div>
|
|
|
|
<div style={{ display: 'flex', gap: 20, marginBottom: 14 }}>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
|
<Checkbox inputId="bulk-expired" checked={bulkOnlyExpired} onChange={(e) => setBulkOnlyExpired(e.checked)} />
|
|
<label htmlFor="bulk-expired">Solo aziende con AR1 scaduta</label>
|
|
</div>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
|
<Checkbox inputId="bulk-missing" checked={bulkOnlyMissing} onChange={(e) => setBulkOnlyMissing(e.checked)} />
|
|
<label htmlFor="bulk-missing">Solo aziende senza AR1</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div style={{ display: 'flex', gap: 8 }}>
|
|
<Button label="Anteprima (non invia)" icon="pi pi-eye" severity="info" outlined onClick={() => runBulk(true)} loading={bulkRunning} />
|
|
<Button label="Invia PEC" icon="pi pi-send" severity="warning" onClick={() => {
|
|
confirmDialog({
|
|
message: 'Confermi l\'invio reale delle PEC? Il BE Gepafin dispatchera le comunicazioni alle aziende matchate.',
|
|
header: 'Conferma invio',
|
|
icon: 'pi pi-exclamation-triangle',
|
|
acceptLabel: 'Invia',
|
|
rejectLabel: 'Annulla',
|
|
accept: () => runBulk(false)
|
|
});
|
|
}} loading={bulkRunning} />
|
|
</div>
|
|
|
|
{bulkResult && (
|
|
<Card title={bulkResult.was_dry_run ? 'Anteprima' : 'Esito invio'} style={{ marginTop: 14 }}>
|
|
<p><strong>Aziende matchate:</strong> {bulkResult.matched ?? 0}</p>
|
|
{!bulkResult.was_dry_run && (
|
|
<p><strong>Form segnati per invio PEC:</strong> {bulkResult.marked_for_pec ?? bulkResult.marked ?? 0}</p>
|
|
)}
|
|
{bulkResult.company_ids && bulkResult.company_ids.length > 0 && (
|
|
<p><strong>ID aziende:</strong> {bulkResult.company_ids.slice(0, 30).join(', ')}{bulkResult.company_ids.length > 30 ? '…' : ''}</p>
|
|
)}
|
|
</Card>
|
|
)}
|
|
</Card>
|
|
</TabPanel>
|
|
|
|
{/* ================ TAB 5: TESTI PEC ================ */}
|
|
<TabPanel header="Testi comunicazioni" leftIcon="pi pi-envelope mr-2">
|
|
<Card>
|
|
<Message severity="info" style={{ marginBottom: 10 }} text="Qui modifichi oggetto e corpo delle PEC inviate dal sistema. Ogni modifica incrementa la versione: il backend Gepafin sincronizza automaticamente i testi nei suoi template per-hub (PEC Massiva / Mailgun)." />
|
|
|
|
<Message severity="info" style={{ marginBottom: 10 }} text="Clicca su 'Modifica' per editare un testo. Dentro il dialog potrai inserire le variabili con un click." />
|
|
|
|
<DataTable value={emailTemplates} loading={loadingEmail} emptyMessage="Nessun testo configurato">
|
|
<Column field="kind" header="Tipo comunicazione" body={(r) => (
|
|
<div>
|
|
<div style={{ fontWeight: 500 }}>{r.label || PEC_KIND_LABEL[r.kind] || r.kind}</div>
|
|
<small style={{ color: '#888' }}>{r.kind}</small>
|
|
</div>
|
|
)} />
|
|
<Column field="subject" header="Oggetto" body={(r) => <span style={{ fontSize: 13 }}>{r.subject}</span>} />
|
|
<Column field="version" header="Ver." style={{ width: 70 }} body={(r) => <Tag value={`v${r.version}`} severity="secondary" />} />
|
|
<Column field="updated_at" header="Aggiornato il" style={{ width: 150 }} body={(r) => r.updated_at ? new Date(r.updated_at).toLocaleDateString('it-IT') : '—'} />
|
|
<Column header="Azioni" body={emailActionsTpl} style={{ width: 150 }} />
|
|
</DataTable>
|
|
</Card>
|
|
</TabPanel>
|
|
|
|
</TabView>
|
|
|
|
{/* ==================== DIALOGS ==================== */}
|
|
|
|
{/* Edit layout template */}
|
|
<Dialog
|
|
header={editLayoutTpl ? `Modifica layout ${editLayoutTpl.variant} v${editLayoutTpl.version}` : ''}
|
|
visible={editLayoutOpen}
|
|
onHide={() => setEditLayoutOpen(false)}
|
|
style={{ width: '780px', maxWidth: '95vw' }}
|
|
modal
|
|
>
|
|
<div style={{ marginBottom: 10, display: 'flex', alignItems: 'center', gap: 10 }}>
|
|
<InputSwitch checked={useAdvancedEditor} onChange={(e) => setUseAdvancedEditor(e.value)} />
|
|
<label>Modalita avanzata (JSON raw)</label>
|
|
</div>
|
|
|
|
{!useAdvancedEditor && (
|
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
|
|
<div style={{ gridColumn: '1 / -1' }}><strong>Brand</strong></div>
|
|
<div>
|
|
<label>Nome brand</label>
|
|
<InputText value={layoutForm.brand_name || ''} onChange={(e) => setLayoutForm({ ...layoutForm, brand_name: e.target.value })} style={{ width: '100%' }} />
|
|
</div>
|
|
<div>
|
|
<label>URL logo</label>
|
|
<InputText value={layoutForm.brand_logo_url || ''} onChange={(e) => setLayoutForm({ ...layoutForm, brand_logo_url: e.target.value })} style={{ width: '100%' }} placeholder="https://..." />
|
|
</div>
|
|
<div>
|
|
<label>Colore primario</label>
|
|
<InputText value={layoutForm.brand_color_primary || ''} onChange={(e) => setLayoutForm({ ...layoutForm, brand_color_primary: e.target.value })} style={{ width: '100%' }} placeholder="#003d7a" />
|
|
</div>
|
|
<div>
|
|
<label>Colore accento</label>
|
|
<InputText value={layoutForm.brand_color_accent || ''} onChange={(e) => setLayoutForm({ ...layoutForm, brand_color_accent: e.target.value })} style={{ width: '100%' }} placeholder="#e65100" />
|
|
</div>
|
|
|
|
<Divider style={{ gridColumn: '1 / -1' }} />
|
|
<div style={{ gridColumn: '1 / -1' }}><strong>Intestazione documento</strong></div>
|
|
<div>
|
|
<label>Titolo</label>
|
|
<InputText value={layoutForm.header_title || ''} onChange={(e) => setLayoutForm({ ...layoutForm, header_title: e.target.value })} style={{ width: '100%' }} />
|
|
</div>
|
|
<div>
|
|
<label>Sottotitolo</label>
|
|
<InputText value={layoutForm.header_subtitle || ''} onChange={(e) => setLayoutForm({ ...layoutForm, header_subtitle: e.target.value })} style={{ width: '100%' }} />
|
|
</div>
|
|
|
|
<Divider style={{ gridColumn: '1 / -1' }} />
|
|
<div style={{ gridColumn: '1 / -1' }}><strong>Introduzione</strong></div>
|
|
<div style={{ gridColumn: '1 / -1' }}>
|
|
<label>Saluto</label>
|
|
<InputText value={layoutForm.intro_salutation || ''} onChange={(e) => setLayoutForm({ ...layoutForm, intro_salutation: e.target.value })} style={{ width: '100%' }} />
|
|
</div>
|
|
<div style={{ gridColumn: '1 / -1' }}>
|
|
<label>Testo introduttivo</label>
|
|
<InputTextarea value={layoutForm.intro_body || ''} onChange={(e) => setLayoutForm({ ...layoutForm, intro_body: e.target.value })} rows={3} style={{ width: '100%' }} />
|
|
</div>
|
|
|
|
<Divider style={{ gridColumn: '1 / -1' }} />
|
|
<div style={{ gridColumn: '1 / -1' }}><strong>Privacy</strong></div>
|
|
<div>
|
|
<label>URL informativa</label>
|
|
<InputText value={layoutForm.privacy_url || ''} onChange={(e) => setLayoutForm({ ...layoutForm, privacy_url: e.target.value })} style={{ width: '100%' }} />
|
|
</div>
|
|
<div>
|
|
<label>Testo piè di pagina</label>
|
|
<InputText value={layoutForm.privacy_body || ''} onChange={(e) => setLayoutForm({ ...layoutForm, privacy_body: e.target.value })} style={{ width: '100%' }} />
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{useAdvancedEditor && (
|
|
<div>
|
|
<Message severity="warn" text="Modalita avanzata: modifichi il JSON layout_config direttamente. Attenzione alla sintassi." style={{ marginBottom: 8 }} />
|
|
<InputTextarea value={layoutAdvancedJson} onChange={(e) => setLayoutAdvancedJson(e.target.value)} rows={18} style={{ width: '100%', fontFamily: 'monospace', fontSize: 12 }} />
|
|
</div>
|
|
)}
|
|
|
|
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 14 }}>
|
|
<Button label="Annulla" severity="secondary" outlined onClick={() => setEditLayoutOpen(false)} />
|
|
<Button label="Salva layout" icon="pi pi-save" onClick={saveLayout} />
|
|
</div>
|
|
</Dialog>
|
|
|
|
{/* Nuova versione */}
|
|
<Dialog
|
|
header={`Nuova versione ${VARIANT_LABEL[newVersionVariant] || newVersionVariant}`}
|
|
visible={newVersionOpen}
|
|
onHide={() => setNewVersionOpen(false)}
|
|
style={{ width: '540px', maxWidth: '95vw' }}
|
|
modal
|
|
>
|
|
<Message severity="info" text="La nuova versione eredita il layout grafico corrente. La versione precedente viene archiviata automaticamente se attivi subito quella nuova." style={{ marginBottom: 10 }} />
|
|
<div style={{ marginBottom: 10 }}>
|
|
<label style={{ display: 'block', marginBottom: 4, fontWeight: 500 }}>Versione (formato semver, maggiore dell'attuale)</label>
|
|
<InputText value={newVersionData.version} onChange={(e) => setNewVersionData({ ...newVersionData, version: e.target.value })} placeholder="es. 1.1.0" style={{ width: '100%' }} />
|
|
</div>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 10 }}>
|
|
<InputSwitch checked={newVersionData.activate_now} onChange={(e) => setNewVersionData({ ...newVersionData, activate_now: e.value })} />
|
|
<label>Attiva subito (archivia la versione precedente)</label>
|
|
</div>
|
|
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 12 }}>
|
|
<Button label="Annulla" severity="secondary" outlined onClick={() => setNewVersionOpen(false)} />
|
|
<Button label="Crea versione" icon="pi pi-plus" onClick={saveNewVersion} />
|
|
</div>
|
|
</Dialog>
|
|
|
|
{/* Edit regola PEC */}
|
|
<Dialog
|
|
header={pecEditing ? 'Modifica regola reminder' : 'Nuova regola reminder'}
|
|
visible={pecDialogOpen}
|
|
onHide={() => setPecDialogOpen(false)}
|
|
style={{ width: '620px', maxWidth: '95vw' }}
|
|
modal
|
|
>
|
|
<div style={{ marginBottom: 10 }}>
|
|
<label style={{ display: 'block', marginBottom: 4, fontWeight: 500 }}>Tipo di regola</label>
|
|
<Dropdown
|
|
value={pecDraft.kind}
|
|
options={PEC_KIND_OPTIONS}
|
|
onChange={(e) => setPecDraft({ ...pecDraft, kind: e.value })}
|
|
style={{ width: '100%' }}
|
|
disabled={!!pecEditing}
|
|
placeholder="Seleziona tipo…"
|
|
/>
|
|
<small style={{ color: '#888' }}>{pecEditing ? 'Il tipo non e modificabile dopo la creazione' : 'Seleziona quale evento fa partire la PEC'}</small>
|
|
</div>
|
|
|
|
<div style={{ marginBottom: 10 }}>
|
|
<label style={{ display: 'block', marginBottom: 4, fontWeight: 500 }}>Quando parte la PEC</label>
|
|
<div style={{ display: 'flex', gap: 10, alignItems: 'center' }}>
|
|
<InputNumber value={pecDraft.offset_days} onValueChange={(e) => setPecDraft({ ...pecDraft, offset_days: e.value })} style={{ flex: 1 }} />
|
|
<span style={{ fontStyle: 'italic', color: '#555' }}>
|
|
{pecDraft.offset_days > 0 && `giorni PRIMA della scadenza`}
|
|
{pecDraft.offset_days === 0 && `giorno della scadenza`}
|
|
{pecDraft.offset_days < 0 && `giorni DOPO la scadenza`}
|
|
</span>
|
|
</div>
|
|
<small style={{ color: '#888' }}>Numero positivo = prima della scadenza; 0 = il giorno stesso; negativo = dopo la scadenza.</small>
|
|
</div>
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 10 }}>
|
|
<InputSwitch checked={pecDraft.is_recurring} onChange={(e) => setPecDraft({ ...pecDraft, is_recurring: e.value })} />
|
|
<label>Ricorrente (si ripete ogni N giorni)</label>
|
|
</div>
|
|
|
|
{pecDraft.is_recurring && (
|
|
<div style={{ marginBottom: 10 }}>
|
|
<label style={{ display: 'block', marginBottom: 4, fontWeight: 500 }}>Intervallo ricorrenza (giorni)</label>
|
|
<InputNumber value={pecDraft.recurring_interval_days} onValueChange={(e) => setPecDraft({ ...pecDraft, recurring_interval_days: e.value })} min={1} style={{ width: '100%' }} />
|
|
<small style={{ color: '#888' }}>Esempio: 30 = la PEC si ripete ogni 30 giorni finche la dichiarazione non viene aggiornata.</small>
|
|
</div>
|
|
)}
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 10 }}>
|
|
<InputSwitch checked={pecDraft.enabled} onChange={(e) => setPecDraft({ ...pecDraft, enabled: e.value })} />
|
|
<label>Regola attiva</label>
|
|
</div>
|
|
|
|
<div style={{ marginBottom: 10 }}>
|
|
<label style={{ display: 'block', marginBottom: 4, fontWeight: 500 }}>Descrizione (opzionale, uso interno)</label>
|
|
<InputTextarea value={pecDraft.description} onChange={(e) => setPecDraft({ ...pecDraft, description: e.target.value })} rows={2} style={{ width: '100%' }} />
|
|
</div>
|
|
|
|
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 12 }}>
|
|
<Button label="Annulla" severity="secondary" outlined onClick={() => setPecDialogOpen(false)} />
|
|
<Button label="Salva" icon="pi pi-save" onClick={savePecRule} />
|
|
</div>
|
|
</Dialog>
|
|
|
|
{/* Edit testo email */}
|
|
<Dialog
|
|
header={editEmailData ? `Modifica testo: ${PEC_KIND_LABEL[editEmailData.kind] || editEmailData.kind}` : ''}
|
|
visible={editEmailOpen}
|
|
onHide={() => setEditEmailOpen(false)}
|
|
style={{ width: '900px', maxWidth: '95vw' }}
|
|
modal
|
|
maximizable
|
|
>
|
|
{editEmailData && (
|
|
<div>
|
|
<Message severity="info" style={{ marginBottom: 10 }} text="Clicca un campo per attivarlo, poi clicca una variabile qui sotto per inserirla al cursore." />
|
|
|
|
{availableVariables.length > 0 && (
|
|
<div style={{ marginBottom: 14, padding: 10, background: '#eef6ff', border: '1px solid #cfe0f7', borderRadius: 4 }}>
|
|
<strong style={{ fontSize: 13 }}>Variabili disponibili </strong>
|
|
<small style={{ color: '#555' }}>
|
|
(inserite in: <strong>{activeEmailField === 'subject' ? 'Oggetto' : activeEmailField === 'body_html' ? 'Corpo HTML' : 'Corpo testo'}</strong>)
|
|
</small>
|
|
<div style={{ marginTop: 8, display: 'flex', flexWrap: 'wrap', gap: 6 }}>
|
|
{availableVariables.map(v => (
|
|
<Tag
|
|
key={v}
|
|
value={'{{' + v + '}}'}
|
|
severity="info"
|
|
style={{ cursor: 'pointer', userSelect: 'none' }}
|
|
onClick={() => insertVariable(v)}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div style={{ marginBottom: 10 }}>
|
|
<label style={{ display: 'block', marginBottom: 4, fontWeight: 500 }}>Oggetto</label>
|
|
<InputText
|
|
ref={subjectInputRef}
|
|
value={editEmailData.subject}
|
|
onChange={(e) => setEditEmailData({ ...editEmailData, subject: e.target.value })}
|
|
onFocus={() => setActiveEmailField('subject')}
|
|
style={{ width: '100%' }}
|
|
/>
|
|
</div>
|
|
|
|
<div style={{ marginBottom: 10 }}>
|
|
<label style={{ display: 'block', marginBottom: 4, fontWeight: 500 }}>Corpo HTML</label>
|
|
<InputTextarea
|
|
ref={bodyHtmlInputRef}
|
|
value={editEmailData.body_html}
|
|
onChange={(e) => setEditEmailData({ ...editEmailData, body_html: e.target.value })}
|
|
onFocus={() => setActiveEmailField('body_html')}
|
|
rows={10}
|
|
style={{ width: '100%', fontFamily: 'monospace', fontSize: 12 }}
|
|
/>
|
|
</div>
|
|
|
|
<div style={{ marginBottom: 10 }}>
|
|
<label style={{ display: 'block', marginBottom: 4, fontWeight: 500 }}>Corpo testo semplice (fallback)</label>
|
|
<InputTextarea
|
|
ref={bodyTextInputRef}
|
|
value={editEmailData.body_text}
|
|
onChange={(e) => setEditEmailData({ ...editEmailData, body_text: e.target.value })}
|
|
onFocus={() => setActiveEmailField('body_text')}
|
|
rows={5}
|
|
style={{ width: '100%', fontSize: 13 }}
|
|
/>
|
|
<small style={{ color: '#888' }}>Usato dai client email che non supportano HTML.</small>
|
|
</div>
|
|
|
|
<div style={{ marginBottom: 10 }}>
|
|
<label style={{ display: 'block', marginBottom: 4, fontWeight: 500 }}>Note interne (opzionale)</label>
|
|
<InputText value={editEmailData.description || ''} onChange={(e) => setEditEmailData({ ...editEmailData, description: e.target.value })} style={{ width: '100%' }} />
|
|
</div>
|
|
|
|
<Divider />
|
|
|
|
<div style={{ display: 'flex', gap: 8, marginBottom: 10 }}>
|
|
<Button label="Anteprima (dati di esempio)" icon="pi pi-eye" severity="info" outlined onClick={runPreview} />
|
|
</div>
|
|
|
|
{previewSubject && (
|
|
<div style={{ padding: 14, background: '#f9f9f9', border: '1px solid #ddd', borderRadius: 4, marginBottom: 10 }}>
|
|
<div style={{ marginBottom: 8 }}><strong>Oggetto:</strong> {previewSubject}</div>
|
|
<div style={{ background: 'white', padding: 10, border: '1px solid #eee' }} dangerouslySetInnerHTML={{ __html: previewHtml }} />
|
|
</div>
|
|
)}
|
|
|
|
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 14 }}>
|
|
<Button label="Annulla" severity="secondary" outlined onClick={() => setEditEmailOpen(false)} />
|
|
<Button label="Salva testo" icon="pi pi-save" onClick={saveEmail} />
|
|
</div>
|
|
</div>
|
|
)}
|
|
</Dialog>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default Ar1AdminConfig;
|