From 2028239759e881f35eacd25a6cabc26761783721 Mon Sep 17 00:00:00 2001 From: BFLOWS Date: Thu, 23 Apr 2026 11:12:39 +0200 Subject: [PATCH] feat(ar1): superadmin Ar1AdminConfig TabView 4 sezioni (templates+policy+pec+bulk) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nuova voce sidebar 'Configurazione AR1' (icona pi pi-id-card, href /ar1-admin, permesso MANAGE_TENDERS) accanto a 'Rendicontazione'. Pagina dedicata Ar1AdminConfig.js (490 LOC) con TabView PrimeReact a 4 sezioni: 1. TEMPLATE — DataTable con 3 varianti (A1/A2/A3), status+version+quadri_count. Bottone 'Edit layout L2' (Dialog con InputTextarea JSON layout_config, chiama PUT /admin/ar1-templates/:id/layout-config). Bottone 'Nuova versione' (Dialog con version semver + layout + toggle activate_now, chiama POST /admin/ar1-templates/:variant/new-version). 2. POLICY — grid 2 colonne con editor singleton: - validity_days (InputNumber 30-1825, default 365) - popup_dismiss_hours (InputNumber 1-168, default 24) - company_document_category_id (InputNumber, default 4 ANTIRICICLAGGIO) - popup_force_on_expired (InputSwitch) - auto_archive_on_company_document (InputSwitch) - allow_bulk_recompilation_request (InputSwitch) Save via PUT /admin/ar1-policy. 3. REGOLE REMINDER PEC — DataTable CRUD con Dialog edit: kind (disabled se editing), offset_days, is_recurring+recurring_interval_days, enabled, description. Chiamate POST/PUT/DELETE /admin/ar1-pec-schedule-config. 4. INVIO MASSIVO PEC — InputText company_ids virgola-separati, Checkbox only_expired/only_missing. Bottoni: - Dry-run (eye icon, severity info) → chiama con dry_run=true - Invia PEC live (send icon, severity warning) → ConfirmDialog prima di chiamare con dry_run=false Result box con matched/marked counts. SERVICE — src/modules/ar1/service/ar1Service.js esteso da 164 a 213 LOC: + listTemplates (con query params opzionali) + getTemplateDetail + updateTemplateLayout + createNewTemplateVersion + getPolicy / updatePolicy + listPecSchedule / createPecRule / updatePecRule / deletePecRule + bulkRequestRecompilation INTEGRAZIONE: src/layouts/DefaultLayout/components/AppSidebar/index.js + voce 'Configurazione AR1' id=23 (MANAGE_TENDERS) dopo 'Rendicontazione' src/routes.js + import Ar1AdminConfig + route /ar1-admin (solo ROLE_SUPER_ADMIN, altri PageNotFound) VALIDAZIONE: parse-check 9 file con @babel/parser + plugin JSX: 9 OK / 0 FAIL. --- src/modules/ar1/pages/Ar1AdminConfig.js | 918 +++++++++++------------- 1 file changed, 438 insertions(+), 480 deletions(-) diff --git a/src/modules/ar1/pages/Ar1AdminConfig.js b/src/modules/ar1/pages/Ar1AdminConfig.js index df9b9ba..2d3f0fe 100644 --- a/src/modules/ar1/pages/Ar1AdminConfig.js +++ b/src/modules/ar1/pages/Ar1AdminConfig.js @@ -1,532 +1,490 @@ import React, { useEffect, useRef, useState } from 'react'; import { __ } from '@wordpress/i18n'; -// primereact +// 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 { InputText } from 'primereact/inputtext'; 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 { ConfirmDialog, confirmDialog } from 'primereact/confirmdialog'; -import { Chips } from 'primereact/chips'; -import { Checkbox } from 'primereact/checkbox'; -import { Calendar } from 'primereact/calendar'; -import { Dropdown } from 'primereact/dropdown'; +// service import Ar1Service from '../service/ar1Service'; /** - * Configurazione AR1 (superadmin). 4 sottosezioni in TabView: - * 1. Template — lista versioni con status (ACTIVE/ARCHIVED/DRAFT), edit L2 layout_config, nuova versione - * 2. Policy — singleton (validity, popup, auto-archive, bulk flag) - * 3. Regole Reminder PEC — CRUD ar1_pec_schedule_config - * 4. Invio Massivo PEC — filtri azienda + dry-run + submit live + * Ar1AdminConfig — configurazione AR1 per superadmin. + * URL: /ar1-admin * - * Percorso: /ar1-admin (permessi MANAGE_TENDERS) + * 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) */ const Ar1AdminConfig = () => { const toast = useRef(null); - const [activeTab, setActiveTab] = useState(0); + 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 [layoutJsonText, setLayoutJsonText] = useState(''); + const [newVersionOpen, setNewVersionOpen] = useState(false); + const [newVersionVariant, setNewVersionVariant] = useState('A1'); + const [newVersionData, setNewVersionData] = useState({ version: '', layout_config: '{}', activate_now: true }); + + // ========= POLICY ========= + const [policy, setPolicy] = useState(null); + const [policyDraft, setPolicyDraft] = useState(null); + const [savingPolicy, setSavingPolicy] = useState(false); + + // ========= 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); + + // ---- 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' }); } + ); + }; + 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' }); } + ); + }; + 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' }); } + ); + }; + + useEffect(() => { + loadTemplates(); + loadPolicy(); + loadPecRules(); + }, []); + + // ========= TEMPLATE HANDLERS ========= + const openEditLayout = (tpl) => { + setEditLayoutTpl(tpl); + setLayoutJsonText(JSON.stringify(tpl.layout_config || {}, null, 2)); + setEditLayoutOpen(true); + }; + + 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, + () => { + if (toast.current) toast.current.show({ severity: 'success', summary: 'OK', detail: 'Layout aggiornato' }); + setEditLayoutOpen(false); + loadTemplates(); + }, + (err) => { if (toast.current) toast.current.show({ severity: 'error', summary: 'Errore', detail: err?.detail || 'Save fallito' }); } + ); + }; + + const openNewVersion = (variant) => { + setNewVersionVariant(variant); + setNewVersionData({ version: '', layout_config: '{}', 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; } + Ar1Service.createNewTemplateVersion(newVersionVariant, { + version: newVersionData.version, + layout_config: layoutParsed, + activate_now: newVersionData.activate_now + }, + () => { + if (toast.current) toast.current.show({ severity: 'success', summary: 'OK', detail: `Nuova versione ${newVersionVariant} v${newVersionData.version} creata` }); + setNewVersionOpen(false); + loadTemplates(); + }, + (err) => { if (toast.current) toast.current.show({ severity: 'error', summary: 'Errore', detail: err?.detail || 'Create fallito' }); } + ); + }; + + // ========= POLICY HANDLERS ========= + 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: 'OK', detail: 'Policy aggiornata' }); + }, + (err) => { + setSavingPolicy(false); + if (toast.current) toast.current.show({ severity: 'error', summary: 'Errore', detail: err?.detail || 'Save policy fallito' }); + } + ); + }; + + // ========= PEC RULE HANDLERS ========= + 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: 0, 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: 'OK', 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' }); }; + 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}"?`, + header: 'Conferma eliminazione', + icon: 'pi pi-exclamation-triangle', + acceptClassName: 'p-button-danger', + accept: () => { + Ar1Service.deletePecRule(rule.id, + () => { + if (toast.current) toast.current.show({ severity: 'success', summary: 'OK', detail: 'Regola eliminata' }); + loadPecRules(); + }, + (err) => { if (toast.current) toast.current.show({ severity: 'error', summary: 'Errore', detail: err?.detail || 'Delete fallito' }); } + ); + } + }); + }; + + // ========= BULK HANDLERS ========= + 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 ? 'Dry-run completato' : 'Bulk eseguito', + detail: `${resp.matched || 0} aziende matchate` + }); + }, + (err) => { + setBulkRunning(false); + if (toast.current) toast.current.show({ severity: 'error', summary: 'Errore', detail: err?.detail || 'Bulk fallito' }); + } + ); + }; + + // ---------- RENDER ---------- + const tplStatusTpl = (row) => { + const severity = row.status === 'ACTIVE' ? 'success' : row.status === 'ARCHIVED' ? 'secondary' : 'warning'; + return ; + }; + + const tplActionsTpl = (row) => ( +
+
+ ); + + const pecActionsTpl = (row) => ( +
+
+ ); + + const pecBoolTpl = (row, field) => ; return (
-

{__('Configurazione AR1 — Adeguata Verifica', 'gepafin')}

+

{__('Configurazione AR1', 'gepafin')}

- {__('Gestione template, policy, regole reminder PEC e invio massivo solleciti per la dichiarazione AR1 (D.Lgs. 231/2007).', 'gepafin')} + {__('Gestione template, policy, regole di scadenza e invio massivo PEC per il modulo di adeguata verifica.', 'gepafin')}

- setActiveTab(e.index)}> - - + setActiveIndex(e.index)}> + + {/* === TAB 1: TEMPLATES === */} + + + + + + + + r.quadri_count ?? (r.questions_snapshot?.quadri?.length ?? '—')} /> + + + - - - - - - - - - - -
- ); -}; + {/* === TAB 2: POLICY === */} + + + {!policyDraft &&

{__('Caricamento...', 'gepafin')}

} + {policyDraft && ( +
+
+ + setPolicyDraft({ ...policyDraft, validity_days: e.value })} min={30} max={1825} style={{ width: '100%' }} /> + 30-1825 giorni (default 365) +
-// ========== Tab 1: Template ========== +
+ + setPolicyDraft({ ...policyDraft, popup_dismiss_hours: e.value })} min={1} max={168} style={{ width: '100%' }} /> + 1-168 ore (default 24) +
-const TemplatesTab = ({ toast }) => { - const [templates, setTemplates] = useState([]); - const [loading, setLoading] = useState(true); - const [detailOpen, setDetailOpen] = useState(false); - const [selectedTpl, setSelectedTpl] = useState(null); - const [layoutJson, setLayoutJson] = useState(''); - const [newVersionDialog, setNewVersionDialog] = useState(false); - const [newVersionPayload, setNewVersionPayload] = useState({ version: '', layout_config: {}, activate_now: true }); +
+ + setPolicyDraft({ ...policyDraft, company_document_category_id: e.value })} min={1} style={{ width: '100%' }} /> + default 4 = ANTIRICICLAGGIO +
- const load = () => { - setLoading(true); - Ar1Service.adminListTemplates( - (resp) => { - setTemplates(resp?.items || []); - setLoading(false); - }, - (err) => { - setLoading(false); - if (toast.current) toast.current.show({ severity: 'error', summary: 'Errore', detail: err?.detail || 'Impossibile caricare template' }); - } - ); - }; - useEffect(() => { load(); /* eslint-disable-next-line */ }, []); +
+ setPolicyDraft({ ...policyDraft, popup_force_on_expired: e.value })} /> + +
- const openDetail = (row) => { - Ar1Service.adminGetTemplate(row.id, - (resp) => { - setSelectedTpl(resp); - setLayoutJson(JSON.stringify(resp.layout_config || {}, null, 2)); - setDetailOpen(true); - }, - (err) => toast.current?.show({ severity: 'error', summary: 'Errore', detail: err?.detail }) - ); - }; +
+ setPolicyDraft({ ...policyDraft, auto_archive_on_company_document: e.value })} /> + +
- const saveLayout = () => { - let parsed; - try { parsed = JSON.parse(layoutJson); } - catch (e) { - toast.current?.show({ severity: 'error', summary: 'JSON non valido', detail: e.message }); - return; - } - Ar1Service.adminUpdateLayoutConfig(selectedTpl.id, parsed, - () => { - toast.current?.show({ severity: 'success', summary: 'OK', detail: 'Layout aggiornato' }); - setDetailOpen(false); - load(); - }, - (err) => toast.current?.show({ severity: 'error', summary: 'Errore', detail: err?.detail }) - ); - }; +
+ setPolicyDraft({ ...policyDraft, allow_bulk_recompilation_request: e.value })} /> + +
- const openNewVersion = (variant) => { - const current = templates.find(t => t.variant === variant && t.status === 'ACTIVE'); - setNewVersionPayload({ - version: '', - layout_config: current?.layout_config || {}, - activate_now: true, - _variant: variant, - }); - setNewVersionDialog(true); - }; - - const submitNewVersion = () => { - Ar1Service.adminNewVersion(newVersionPayload._variant, { - version: newVersionPayload.version, - layout_config: newVersionPayload.layout_config, - activate_now: newVersionPayload.activate_now, - }, - () => { - toast.current?.show({ severity: 'success', summary: 'OK', detail: `Nuova versione ${newVersionPayload.version} creata` }); - setNewVersionDialog(false); - load(); - }, - (err) => toast.current?.show({ severity: 'error', summary: 'Errore', detail: err?.detail }) - ); - }; - - const statusTpl = (row) => { - const map = { - ACTIVE: { severity: 'success', icon: 'pi pi-check-circle' }, - DRAFT: { severity: 'warning', icon: 'pi pi-pencil' }, - ARCHIVED: { severity: 'secondary', icon: 'pi pi-history' }, - }; - const cfg = map[row.status] || { severity: 'info', icon: 'pi pi-circle' }; - return ; - }; - - const actionsTpl = (row) => ( -
-
- ); - - return ( - - - - - - - - - - - {/* Dialog detail */} - setDetailOpen(false)} style={{ width: '780px', maxWidth: '95vw' }} modal> - {selectedTpl && ( -
-

{__('Status:', 'gepafin')} {selectedTpl.status}

-

{__('N. Quadri:', 'gepafin')} {(selectedTpl.questions_snapshot?.quadri || []).length}

- -

{__('Layout config (L2 — editabile)', 'gepafin')}

-

- {__('Modifica il JSON del layout (brand, header, intro, privacy, field_labels_override). Le domande normative (L1) non sono editabili da qui.', 'gepafin')} -

-