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:
@@ -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 */}
|
||||
|
||||
Reference in New Issue
Block a user