feat(ar1-admin): editor unificato template (layout L2 + struttura quadri L1)

Strada A: il superadmin puo modificare TUTTO via UI (layout grafico + struttura
dei quadri). Se tocca solo il layout -> PUT in place. Se tocca la struttura ->
il BE auto-bumpa la patch e archivia la versione precedente. I form gia compilati
continuano a usare il loro snapshot.

Nuovo componente:
  src/modules/ar1/components/QuadriStructureEditor.js (438 LOC)
  - Metadati snapshot: variant_label, legal_ref, normative_frame, variant_description
  - Lista quadri in Accordion, per ogni quadro:
      * id / title / description modificabili
      * reorder su/giu, elimina quadro, aggiungi quadro
      * Warning 'NORMATIVO' per Quadro G (is_legal_frame=true)
      * Fields normali: editor per-campo con id, label, tipo (7 types), required,
        max_length, pattern regex, options (per enum/radio), tag prefill_from
      * Row fields (row_type, Quadro B titolari): sezione separata con warning
      * Nested_full fields (Quadro C/D): sezione separata
      * Upload slots (Quadro F): tag readonly (edit avanzato tbd)
  - FIELD_TYPE_OPTIONS: text, email, date, checkbox, radio, enum, yes_no_with_note
  - Usa Accordion multi-open per navigare piu quadri, Tag per metadati visuali

Cambiamenti in Ar1AdminConfig.js:
  - Rimossi: openEditLayout, openNewVersion, saveNewVersion, stati newVersionOpen/
    Data/Variant, Dialog 'Nuova versione' manuale (user sceglieva version semver)
  - Aggiunti: openEditTemplate (carica template completo via GET detail),
    saveEditTemplate (fa diff questions_snapshot, se cambiato chiama
    createNewTemplateVersion senza version -> BE auto-bump, se invariato chiama
    updateTemplateLayout in place), questionsStructureChanged helper (deep-equal
    via JSON.stringify su clone deep fatto al load)
  - Service: + getTemplate + getNextVersion (per preview numero versione)
  - Bottoni azioni tab Template: solo 'Anteprima' + 'Modifica' (rimosso '+ Nuova vers.')
  - Dialog unificato 1100px maximizable:
      * Bar top con Tag variante/version/status + Message warning se struttura
        modificata (mostra prossima versione preview es. v1.2.968)
      * 2 tab interni (pulsanti custom): 'Layout grafico' vs 'Struttura quadri'
        con indicatore • se struttura ha modifiche
      * Sezione Layout: form come prima (brand/header/intro/privacy) + toggle JSON raw
      * Sezione Struttura: rende QuadriStructureEditor
      * Footer sticky: tag stato ('update in place' verde vs 'nuova versione' giallo)
        + bottone Salva che cambia label e severity: 'Salva layout' default vs
        'Crea versione v1.2.968' warning quando struttura cambiata
  - Dialog 'Nuova versione' rimosso (mai piu input manuale di semver)
This commit is contained in:
BFLOWS
2026-04-23 15:46:43 +02:00
parent ac1c18c737
commit ec0e7397e6
2 changed files with 694 additions and 159 deletions

View File

@@ -23,6 +23,9 @@ 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
// ==================================================
@@ -142,14 +145,20 @@ const Ar1AdminConfig = () => {
// ========= 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 [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);
const [newVersionOpen, setNewVersionOpen] = useState(false);
const [newVersionVariant, setNewVersionVariant] = useState('A1');
const [newVersionData, setNewVersionData] = useState({ version: '', activate_now: true });
// 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);
@@ -278,25 +287,51 @@ const Ar1AdminConfig = () => {
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 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.getTemplate(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 = () => ({
@@ -320,46 +355,64 @@ const Ar1AdminConfig = () => {
}
});
const saveLayout = () => {
let payload;
// 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 { payload = JSON.parse(layoutAdvancedJson); }
try { layoutConfig = JSON.parse(layoutAdvancedJson); }
catch (e) { if (toast.current) toast.current.show({ severity: 'error', summary: 'JSON non valido', detail: e.message }); return; }
} else {
payload = buildLayoutFromForm();
layoutConfig = 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: formatErrorDetail(err?.detail, 'Salvataggio fallito') }); }
);
};
const openNewVersion = (variant) => {
setNewVersionVariant(variant);
setNewVersionData({ version: '', activate_now: true });
setNewVersionOpen(true);
};
const structureChanged = questionsStructureChanged();
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();
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
},
(err) => { if (toast.current) toast.current.show({ severity: 'error', summary: 'Errore', detail: formatErrorDetail(err?.detail, 'Creazione fallita') }); }
);
(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') });
}
);
}
};
// ==================================================
@@ -541,11 +594,8 @@ const Ar1AdminConfig = () => {
}
}} />
<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)} />
tooltip="Modifica (layout + struttura quadri)" tooltipOptions={{ position: 'top' }}
onClick={() => openEditTemplate(row)} disabled={row.status === 'ARCHIVED'} />
</div>
);
@@ -787,108 +837,155 @@ const Ar1AdminConfig = () => {
{/* ==================== DIALOGS ==================== */}
{/* Edit layout template */}
{/* Edit template unificato: layout + struttura quadri */}
<Dialog
header={editLayoutTpl ? `Modifica layout ${editLayoutTpl.variant} v${editLayoutTpl.version}` : ''}
visible={editLayoutOpen}
onHide={() => setEditLayoutOpen(false)}
style={{ width: '780px', maxWidth: '95vw' }}
header={editTpl ? `Modifica template ${editTpl.variant} v${editTpl.version}` : 'Caricamento...'}
visible={editOpen}
onHide={() => setEditOpen(false)}
style={{ width: '1100px', maxWidth: '95vw', height: '90vh' }}
contentStyle={{ maxHeight: 'calc(90vh - 100px)', overflow: 'auto' }}
modal
maximizable
>
<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>
{editLoading && <p>Caricamento template...</p>}
{!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 && (
{!editLoading && editTpl && (
<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 }} />
{/* Bar info + cambio sezione */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 14, padding: '8px 12px', background: '#f5f5f5', borderRadius: 4 }}>
<div>
<Tag value={editTpl.variant} severity="info" />{' '}
<Tag value={`v${editTpl.version}`} severity="secondary" />{' '}
<Tag value={TEMPLATE_STATUS_LABEL[editTpl.status] || editTpl.status} severity={editTpl.status === 'ACTIVE' ? 'success' : 'warning'} />
</div>
{questionsStructureChanged() && nextVersion && (
<Message severity="warn" text={`Struttura modificata → verra creata la versione v${nextVersion}`} />
)}
</div>
{/* Tab switch: Layout vs Struttura */}
<div style={{ display: 'flex', gap: 0, borderBottom: '2px solid #e0e0e0', marginBottom: 16 }}>
<Button
label="Layout grafico"
icon="pi pi-palette"
text={editSection !== 'layout'}
onClick={() => setEditSection('layout')}
style={{ borderRadius: 0, borderBottom: editSection === 'layout' ? '3px solid var(--primary-color)' : 'none' }}
/>
<Button
label={'Struttura quadri' + (questionsStructureChanged() ? ' •' : '')}
icon="pi pi-list"
text={editSection !== 'struttura'}
severity={questionsStructureChanged() ? 'warning' : undefined}
onClick={() => setEditSection('struttura')}
style={{ borderRadius: 0, borderBottom: editSection === 'struttura' ? '3px solid var(--primary-color)' : 'none' }}
/>
</div>
{/* SEZIONE LAYOUT */}
{editSection === 'layout' && (
<div>
<Message severity="info" style={{ marginBottom: 10 }}
text="Modifiche al layout grafico (brand, colori, testi introduttivi) si applicano in place alla versione corrente, senza creare una nuova versione." />
<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={4} 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 pie 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={20} style={{ width: '100%', fontFamily: 'monospace', fontSize: 12 }} />
</div>
)}
</div>
)}
{/* SEZIONE STRUTTURA QUADRI */}
{editSection === 'struttura' && questionsSnapshot && (
<QuadriStructureEditor
value={questionsSnapshot}
onChange={setQuestionsSnapshot}
/>
)}
{/* Footer azioni */}
<div style={{ position: 'sticky', bottom: 0, background: 'white', paddingTop: 14, borderTop: '1px solid #eee', marginTop: 20, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
{questionsStructureChanged() && nextVersion && (
<Tag severity="warning" value={`Al salvataggio: nuova versione v${nextVersion}`} />
)}
{!questionsStructureChanged() && (
<Tag severity="info" value={`Al salvataggio: update in place su v${editTpl.version}`} />
)}
</div>
<div style={{ display: 'flex', gap: 8 }}>
<Button label="Annulla" severity="secondary" outlined onClick={() => setEditOpen(false)} disabled={saving} />
<Button label={questionsStructureChanged() ? `Crea versione v${nextVersion || '...'}` : 'Salva layout'}
icon="pi pi-save"
severity={questionsStructureChanged() ? 'warning' : undefined}
onClick={saveEditTemplate}
loading={saving} />
</div>
</div>
</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 */}