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';
// components
import QuadriStructureEditor from '../components/QuadriStructureEditor';
// ==================================================
// 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' }
];
// Mapping field name -> label italiano umano
const FIELD_LABELS_IT = {
version: 'Versione',
layout_config: 'Configurazione layout',
activate_now: 'Attiva subito',
kind: 'Tipo regola',
offset_days: 'Giorni offset',
recurring_interval_days: 'Intervallo ricorrenza',
enabled: 'Attiva',
description: 'Descrizione',
subject: 'Oggetto',
body_html: 'Corpo HTML',
body_text: 'Corpo testo',
validity_days: 'Validita',
popup_dismiss_hours: 'Ore dismiss pop-up',
company_document_category_id: 'Categoria documento',
company_ids: 'ID aziende',
only_expired: 'Solo scadute',
only_missing: 'Solo senza AR1',
dry_run: 'Simulazione',
};
// Traduce messaggi Pydantic comuni in italiano umano
const translatePydanticMsg = (msg, pydanticType, ctx) => {
if (!msg) return '';
const m = String(msg);
// semver pattern
if (m.includes("should match pattern") && ctx?.pattern?.includes('\\d+\\.\\d+\\.\\d+')) {
return 'deve essere nel formato X.Y.Z (esempio: 1.1.0)';
}
// string pattern generico
if (pydanticType === 'string_pattern_mismatch') return `formato non valido (atteso: ${ctx?.pattern || 'pattern specifico'})`;
// tipi base
if (pydanticType === 'missing') return 'campo obbligatorio mancante';
if (pydanticType === 'string_type') return 'deve essere una stringa';
if (pydanticType === 'int_type' || pydanticType === 'int_parsing') return 'deve essere un numero intero';
if (pydanticType === 'bool_type') return 'deve essere vero o falso';
// range
if (pydanticType === 'greater_than_equal') return `deve essere almeno ${ctx?.ge}`;
if (pydanticType === 'less_than_equal') return `deve essere al massimo ${ctx?.le}`;
if (pydanticType === 'string_too_short') return `troppo corto (minimo ${ctx?.min_length || ''} caratteri)`;
if (pydanticType === 'string_too_long') return `troppo lungo (massimo ${ctx?.max_length || ''} caratteri)`;
// value error (validator custom)
if (pydanticType === 'value_error') return m.replace(/^Value error,\s*/, '');
// fallback: lascio msg originale
return m;
};
// Normalizza il campo detail che puo essere string o array Pydantic [{loc, msg, type, ctx}]
const formatErrorDetail = (detail, fallback) => {
if (!detail) return fallback || 'Errore';
if (typeof detail === 'string') return detail;
if (Array.isArray(detail)) {
return detail.map(e => {
const locArr = Array.isArray(e.loc) ? e.loc.filter(x => x !== 'body') : [];
const lastLoc = locArr[locArr.length - 1];
const fieldLabel = FIELD_LABELS_IT[lastLoc] || lastLoc || '';
const msg = translatePydanticMsg(e.msg, e.type, e.ctx);
return fieldLabel ? `${fieldLabel}: ${msg}` : msg;
}).join('; ');
}
if (typeof detail === 'object') return JSON.stringify(detail);
return String(detail);
};
/**
* Ar1AdminConfig — configurazione AR1 per superadmin. (build 1776950842)
* 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 [editOpen, setEditOpen] = useState(false);
const [editTpl, setEditTpl] = useState(null); // template completo (caricato via GET /admin/ar1-templates/:id)
const [editLoading, setEditLoading] = useState(false);
const [editSection, setEditSection] = useState('layout'); // 'layout' | 'struttura'
// Layout form fields
const [layoutForm, setLayoutForm] = useState({});
const [layoutAdvancedJson, setLayoutAdvancedJson] = useState('');
const [useAdvancedEditor, setUseAdvancedEditor] = useState(false);
// Questions snapshot editor
const [questionsSnapshot, setQuestionsSnapshot] = useState(null);
const [originalQuestionsSnapshot, setOriginalQuestionsSnapshot] = useState(null);
// Prossima versione auto-bump (preview)
const [nextVersion, setNextVersion] = useState(null);
const [saving, setSaving] = useState(false);
// ========= 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: formatErrorDetail(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: formatErrorDetail(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: formatErrorDetail(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: formatErrorDetail(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 openEditTemplate = (tplListRow) => {
// carico il template completo (questions_snapshot incluso) via GET detail
setEditOpen(true);
setEditLoading(true);
setEditTpl(null);
setQuestionsSnapshot(null);
setOriginalQuestionsSnapshot(null);
setNextVersion(null);
setEditSection('layout');
Ar1Service.getTemplateDetail(tplListRow.id,
(tpl) => {
setEditTpl(tpl);
const lc = tpl.layout_config || {};
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);
const qs = tpl.questions_snapshot || { quadri: [] };
setQuestionsSnapshot(qs);
setOriginalQuestionsSnapshot(JSON.parse(JSON.stringify(qs))); // deep clone per diff
setEditLoading(false);
// carico anche la prossima versione possibile (preview)
Ar1Service.getNextVersion(tpl.variant,
(resp) => setNextVersion(resp.next_version),
() => {}
);
},
(err) => {
setEditLoading(false);
setEditOpen(false);
if (toast.current) toast.current.show({ severity: 'error', summary: 'Errore', detail: formatErrorDetail(err?.detail, 'Caricamento template fallito') });
}
);
};
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
}
});
// Deep-equal semplice via JSON.stringify con key order (sufficiente perche le key provengono dalla stessa struttura)
const questionsStructureChanged = () => {
if (!originalQuestionsSnapshot || !questionsSnapshot) return false;
return JSON.stringify(questionsSnapshot) !== JSON.stringify(originalQuestionsSnapshot);
};
const saveEditTemplate = () => {
if (!editTpl) return;
let layoutConfig;
if (useAdvancedEditor) {
try { layoutConfig = JSON.parse(layoutAdvancedJson); }
catch (e) { if (toast.current) toast.current.show({ severity: 'error', summary: 'JSON non valido', detail: e.message }); return; }
} else {
layoutConfig = buildLayoutFromForm();
}
const structureChanged = questionsStructureChanged();
setSaving(true);
if (structureChanged) {
// Nuova versione: il BE auto-bumpa la patch
Ar1Service.createNewTemplateVersion(editTpl.variant, {
// version omessa -> BE auto-bump
layout_config: layoutConfig,
questions_snapshot: questionsSnapshot,
activate_now: true
},
(resp) => {
setSaving(false);
if (toast.current) toast.current.show({
severity: 'success',
summary: `Nuova versione creata: v${resp.version}`,
detail: `La versione precedente e stata archiviata. I form gia compilati continuano a usare quella. Nuove bozze useranno v${resp.version}.`,
life: 6000
});
setEditOpen(false);
loadTemplates();
},
(err) => {
setSaving(false);
if (toast.current) toast.current.show({ severity: 'error', summary: 'Errore creazione versione', detail: formatErrorDetail(err?.detail, 'Creazione fallita') });
}
);
} else {
// Solo layout cambiato -> PUT in place sulla versione corrente
Ar1Service.updateTemplateLayout(editTpl.id, layoutConfig,
() => {
setSaving(false);
if (toast.current) toast.current.show({ severity: 'success', summary: 'Layout aggiornato', detail: `Template ${editTpl.variant} v${editTpl.version}` });
setEditOpen(false);
loadTemplates();
},
(err) => {
setSaving(false);
if (toast.current) toast.current.show({ severity: 'error', summary: 'Errore', detail: formatErrorDetail(err?.detail, 'Salvataggio fallito') });
}
);
}
};
// ==================================================
// 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: formatErrorDetail(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: formatErrorDetail(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: formatErrorDetail(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: formatErrorDetail(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: formatErrorDetail(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: formatErrorDetail(err?.detail, 'Anteprima fallita') }); }
);
};
// ==================================================
// RENDER
// ==================================================
const tplStatusTpl = (row) => {
const severity = row.status === 'ACTIVE' ? 'success' : row.status === 'ARCHIVED' ? 'secondary' : 'warning';
return
Gestione template, policy, regole di scadenza, invio massivo e testi PEC per il modulo antiriciclaggio (D.Lgs. 231/2007).
Caricamento…
} {policyDraft && (Aziende matchate: {bulkResult.matched ?? 0}
{!bulkResult.was_dry_run && (Form segnati per invio PEC: {bulkResult.marked_for_pec ?? bulkResult.marked ?? 0}
)} {bulkResult.company_ids && bulkResult.company_ids.length > 0 && (ID aziende: {bulkResult.company_ids.slice(0, 30).join(', ')}{bulkResult.company_ids.length > 30 ? '…' : ''}
)}