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:
438
src/modules/ar1/components/QuadriStructureEditor.js
Normal file
438
src/modules/ar1/components/QuadriStructureEditor.js
Normal file
@@ -0,0 +1,438 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
// prime
|
||||
import { Card } from 'primereact/card';
|
||||
import { Button } from 'primereact/button';
|
||||
import { InputText } from 'primereact/inputtext';
|
||||
import { InputTextarea } from 'primereact/inputtextarea';
|
||||
import { Dropdown } from 'primereact/dropdown';
|
||||
import { InputNumber } from 'primereact/inputnumber';
|
||||
import { Checkbox } from 'primereact/checkbox';
|
||||
import { Tag } from 'primereact/tag';
|
||||
import { Divider } from 'primereact/divider';
|
||||
import { Message } from 'primereact/message';
|
||||
import { Accordion, AccordionTab } from 'primereact/accordion';
|
||||
import { confirmDialog } from 'primereact/confirmdialog';
|
||||
|
||||
const FIELD_TYPE_OPTIONS = [
|
||||
{ label: 'Testo libero', value: 'text' },
|
||||
{ label: 'Email', value: 'email' },
|
||||
{ label: 'Data', value: 'date' },
|
||||
{ label: 'Checkbox (si/no)', value: 'checkbox' },
|
||||
{ label: 'Scelta singola (radio)', value: 'radio' },
|
||||
{ label: 'Menu a tendina (enum)', value: 'enum' },
|
||||
{ label: 'Si/No con nota', value: 'yes_no_with_note' }
|
||||
];
|
||||
|
||||
/**
|
||||
* Editor della struttura questions_snapshot di un template AR1.
|
||||
*
|
||||
* Props:
|
||||
* value: { quadri: [...], variant, legal_ref, ... }
|
||||
* onChange: (newValue) => void
|
||||
*
|
||||
* Permette:
|
||||
* - Modificare metadati snapshot (variant_label, variant_description, legal_ref, normative_frame)
|
||||
* - Modificare per ogni quadro: id, title, description
|
||||
* - Aggiungere/rimuovere/modificare fields con tipo, label, required, pattern, max_length, options, prefill_from
|
||||
* - Per Quadro G (is_legal_frame=true): warning visuale + editor del description
|
||||
* - Per Quadri B (row_type=titolare): edit row_fields separato
|
||||
* - Per Quadri C/D (nested_full): edit fields nested
|
||||
* - Aggiungere/rimuovere quadri interi
|
||||
*/
|
||||
const QuadriStructureEditor = ({ value, onChange }) => {
|
||||
const qs = value || { quadri: [] };
|
||||
const quadri = qs.quadri || [];
|
||||
|
||||
const update = (newQs) => onChange(newQs);
|
||||
|
||||
// --- metadati snapshot ---
|
||||
const updateMeta = (key, v) => update({ ...qs, [key]: v });
|
||||
|
||||
// --- quadri ---
|
||||
const updateQuadro = (idx, partial) => {
|
||||
const newQuadri = quadri.map((q, i) => i === idx ? { ...q, ...partial } : q);
|
||||
update({ ...qs, quadri: newQuadri });
|
||||
};
|
||||
|
||||
const moveQuadro = (idx, direction) => {
|
||||
const newIdx = idx + direction;
|
||||
if (newIdx < 0 || newIdx >= quadri.length) return;
|
||||
const newQuadri = [...quadri];
|
||||
[newQuadri[idx], newQuadri[newIdx]] = [newQuadri[newIdx], newQuadri[idx]];
|
||||
update({ ...qs, quadri: newQuadri });
|
||||
};
|
||||
|
||||
const removeQuadro = (idx) => {
|
||||
const q = quadri[idx];
|
||||
confirmDialog({
|
||||
message: `Eliminare il quadro "${q.id} - ${q.title}"? Sara creata una nuova versione.`,
|
||||
header: 'Conferma eliminazione quadro',
|
||||
icon: 'pi pi-exclamation-triangle',
|
||||
acceptLabel: 'Elimina',
|
||||
rejectLabel: 'Annulla',
|
||||
acceptClassName: 'p-button-danger',
|
||||
accept: () => {
|
||||
const newQuadri = quadri.filter((_, i) => i !== idx);
|
||||
update({ ...qs, quadri: newQuadri });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const addQuadro = () => {
|
||||
const nextLetter = String.fromCharCode(65 + quadri.length); // A, B, C...
|
||||
const newQuadro = {
|
||||
id: nextLetter,
|
||||
title: `Quadro ${nextLetter} - Nuovo quadro`,
|
||||
description: '',
|
||||
fields: []
|
||||
};
|
||||
update({ ...qs, quadri: [...quadri, newQuadro] });
|
||||
};
|
||||
|
||||
// --- fields dentro quadro ---
|
||||
const updateField = (qIdx, fIdx, partial) => {
|
||||
const q = quadri[qIdx];
|
||||
const newFields = (q.fields || []).map((f, i) => i === fIdx ? { ...f, ...partial } : f);
|
||||
updateQuadro(qIdx, { fields: newFields });
|
||||
};
|
||||
|
||||
const addField = (qIdx) => {
|
||||
const q = quadri[qIdx];
|
||||
const fields = q.fields || [];
|
||||
const newField = {
|
||||
id: `campo_${fields.length + 1}`,
|
||||
type: 'text',
|
||||
label: 'Nuovo campo',
|
||||
required: false
|
||||
};
|
||||
updateQuadro(qIdx, { fields: [...fields, newField] });
|
||||
};
|
||||
|
||||
const removeField = (qIdx, fIdx) => {
|
||||
const q = quadri[qIdx];
|
||||
const newFields = (q.fields || []).filter((_, i) => i !== fIdx);
|
||||
updateQuadro(qIdx, { fields: newFields });
|
||||
};
|
||||
|
||||
const moveField = (qIdx, fIdx, direction) => {
|
||||
const q = quadri[qIdx];
|
||||
const fields = q.fields || [];
|
||||
const newIdx = fIdx + direction;
|
||||
if (newIdx < 0 || newIdx >= fields.length) return;
|
||||
const newFields = [...fields];
|
||||
[newFields[fIdx], newFields[newIdx]] = [newFields[newIdx], newFields[fIdx]];
|
||||
updateQuadro(qIdx, { fields: newFields });
|
||||
};
|
||||
|
||||
// --- row_fields (es. Quadro B titolari) ---
|
||||
const updateRowField = (qIdx, fIdx, partial) => {
|
||||
const q = quadri[qIdx];
|
||||
const newRowFields = (q.row_fields || []).map((f, i) => i === fIdx ? { ...f, ...partial } : f);
|
||||
updateQuadro(qIdx, { row_fields: newRowFields });
|
||||
};
|
||||
|
||||
const addRowField = (qIdx) => {
|
||||
const q = quadri[qIdx];
|
||||
const rowFields = q.row_fields || [];
|
||||
updateQuadro(qIdx, {
|
||||
row_fields: [...rowFields, { id: `campo_${rowFields.length + 1}`, type: 'text', label: 'Nuovo campo', required: false }]
|
||||
});
|
||||
};
|
||||
|
||||
const removeRowField = (qIdx, fIdx) => {
|
||||
const q = quadri[qIdx];
|
||||
updateQuadro(qIdx, { row_fields: (q.row_fields || []).filter((_, i) => i !== fIdx) });
|
||||
};
|
||||
|
||||
// --- nested fields (es. Quadro C/D) ---
|
||||
const updateNestedField = (qIdx, fIdx, partial) => {
|
||||
const q = quadri[qIdx];
|
||||
const nested = q.nested_full || {};
|
||||
const newNestedFields = (nested.fields || []).map((f, i) => i === fIdx ? { ...f, ...partial } : f);
|
||||
updateQuadro(qIdx, { nested_full: { ...nested, fields: newNestedFields } });
|
||||
};
|
||||
|
||||
const addNestedField = (qIdx) => {
|
||||
const q = quadri[qIdx];
|
||||
const nested = q.nested_full || { fields: [] };
|
||||
const fields = nested.fields || [];
|
||||
updateQuadro(qIdx, {
|
||||
nested_full: {
|
||||
...nested,
|
||||
fields: [...fields, { id: `campo_${fields.length + 1}`, type: 'text', label: 'Nuovo campo', required: false }]
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const removeNestedField = (qIdx, fIdx) => {
|
||||
const q = quadri[qIdx];
|
||||
const nested = q.nested_full || {};
|
||||
updateQuadro(qIdx, {
|
||||
nested_full: { ...nested, fields: (nested.fields || []).filter((_, i) => i !== fIdx) }
|
||||
});
|
||||
};
|
||||
|
||||
// ----- render field editor (atom) -----
|
||||
const renderFieldRow = (field, idx, onUpdate, onRemove, onMoveUp, onMoveDown, total) => {
|
||||
const isEnum = field.type === 'enum' || field.type === 'radio';
|
||||
return (
|
||||
<div key={idx} style={{
|
||||
border: '1px solid #ddd', borderRadius: 4, padding: 10,
|
||||
marginBottom: 8, background: '#fafafa'
|
||||
}}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '160px 1fr 160px auto', gap: 8, alignItems: 'end' }}>
|
||||
<div>
|
||||
<label style={{ fontSize: 11, color: '#555' }}>ID (kebab, no spazi)</label>
|
||||
<InputText value={field.id || ''}
|
||||
onChange={(e) => onUpdate({ id: e.target.value.replace(/\s+/g, '_') })}
|
||||
style={{ width: '100%', fontFamily: 'monospace', fontSize: 12 }} />
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ fontSize: 11, color: '#555' }}>Label visualizzato</label>
|
||||
<InputText value={field.label || ''}
|
||||
onChange={(e) => onUpdate({ label: e.target.value })}
|
||||
style={{ width: '100%' }} />
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ fontSize: 11, color: '#555' }}>Tipo</label>
|
||||
<Dropdown value={field.type || 'text'} options={FIELD_TYPE_OPTIONS}
|
||||
onChange={(e) => onUpdate({ type: e.value })}
|
||||
style={{ width: '100%' }} />
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
<Button icon="pi pi-arrow-up" rounded outlined size="small"
|
||||
disabled={idx === 0} onClick={() => onMoveUp()} tooltip="Sposta su" />
|
||||
<Button icon="pi pi-arrow-down" rounded outlined size="small"
|
||||
disabled={idx === total - 1} onClick={() => onMoveDown && onMoveDown()} tooltip="Sposta giu" />
|
||||
<Button icon="pi pi-trash" rounded outlined size="small" severity="danger"
|
||||
onClick={() => onRemove()} tooltip="Elimina campo" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: 16, marginTop: 8, alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<Checkbox inputId={`req_${idx}_${field.id}`} checked={!!field.required}
|
||||
onChange={(e) => onUpdate({ required: e.checked })} />
|
||||
<label htmlFor={`req_${idx}_${field.id}`} style={{ fontSize: 12 }}>Obbligatorio</label>
|
||||
</div>
|
||||
|
||||
{field.type === 'text' && (
|
||||
<>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<label style={{ fontSize: 11, color: '#555' }}>Max lunghezza:</label>
|
||||
<InputNumber value={field.max_length || null}
|
||||
onValueChange={(e) => onUpdate({ max_length: e.value })}
|
||||
style={{ width: 90 }} inputStyle={{ fontSize: 12 }} />
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<label style={{ fontSize: 11, color: '#555' }}>Pattern regex:</label>
|
||||
<InputText value={field.pattern || ''}
|
||||
onChange={(e) => onUpdate({ pattern: e.target.value || undefined })}
|
||||
style={{ width: 180, fontFamily: 'monospace', fontSize: 11 }}
|
||||
placeholder="es. ^[0-9]{11}$" />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{field.prefill_from && (
|
||||
<Tag severity="info" value={`prefill: ${field.prefill_from}`} style={{ fontSize: 11 }} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isEnum && (
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<label style={{ fontSize: 11, color: '#555', display: 'block', marginBottom: 2 }}>
|
||||
Opzioni (una per riga)
|
||||
</label>
|
||||
<InputTextarea rows={3}
|
||||
value={(field.options || []).map(o => typeof o === 'string' ? o : (o.label || o.value || '')).join('\n')}
|
||||
onChange={(e) => {
|
||||
const opts = e.target.value.split('\n').filter(Boolean);
|
||||
onUpdate({ options: opts });
|
||||
}}
|
||||
style={{ width: '100%', fontSize: 12 }} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// ----- render -----
|
||||
return (
|
||||
<div>
|
||||
<Message severity="warn" style={{ marginBottom: 14 }}
|
||||
text="Attenzione: modifiche alla struttura dei quadri generano automaticamente una nuova versione del template. I form gia compilati continuano a usare la versione precedente grazie allo snapshot." />
|
||||
|
||||
{/* Metadati */}
|
||||
<Card title="Metadati template" style={{ marginBottom: 14 }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}>
|
||||
<div>
|
||||
<label style={{ fontSize: 11, color: '#555' }}>Etichetta variante</label>
|
||||
<InputText value={qs.variant_label || ''}
|
||||
onChange={(e) => updateMeta('variant_label', e.target.value)} style={{ width: '100%' }} />
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ fontSize: 11, color: '#555' }}>Riferimento normativo</label>
|
||||
<InputText value={qs.legal_ref || ''}
|
||||
onChange={(e) => updateMeta('legal_ref', e.target.value)} style={{ width: '100%' }}
|
||||
placeholder="es. D.Lgs. 231/2007" />
|
||||
</div>
|
||||
<div style={{ gridColumn: '1 / -1' }}>
|
||||
<label style={{ fontSize: 11, color: '#555' }}>Descrizione variante</label>
|
||||
<InputTextarea rows={2} value={qs.variant_description || ''}
|
||||
onChange={(e) => updateMeta('variant_description', e.target.value)} style={{ width: '100%' }} />
|
||||
</div>
|
||||
<div style={{ gridColumn: '1 / -1' }}>
|
||||
<label style={{ fontSize: 11, color: '#555' }}>Cornice normativa (testo completo)</label>
|
||||
<InputTextarea rows={3} value={qs.normative_frame || ''}
|
||||
onChange={(e) => updateMeta('normative_frame', e.target.value)}
|
||||
style={{ width: '100%', fontSize: 12 }} />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Quadri */}
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
|
||||
<h3 style={{ margin: 0 }}>Quadri ({quadri.length})</h3>
|
||||
<Button label="Aggiungi quadro" icon="pi pi-plus" size="small" outlined onClick={addQuadro} />
|
||||
</div>
|
||||
|
||||
<Accordion multiple>
|
||||
{quadri.map((q, qIdx) => (
|
||||
<AccordionTab key={qIdx}
|
||||
header={
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, width: '100%' }}>
|
||||
<Tag value={q.id || '?'} severity={q.is_legal_frame ? 'warning' : 'info'} />
|
||||
<strong style={{ flex: 1 }}>{q.title || '(senza titolo)'}</strong>
|
||||
{q.is_legal_frame && <Tag value="NORMATIVO" severity="warning" />}
|
||||
{q.row_type && <Tag value={`ripetuto (${q.row_type})`} severity="secondary" />}
|
||||
{q.nested_full && <Tag value="annidato" severity="secondary" />}
|
||||
<small style={{ color: '#888' }}>
|
||||
{(q.fields || []).length} campi
|
||||
{q.row_fields ? ` +${q.row_fields.length} riga` : ''}
|
||||
{q.nested_full?.fields ? ` +${q.nested_full.fields.length} nidif.` : ''}
|
||||
</small>
|
||||
</div>
|
||||
}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '140px 1fr 1fr auto', gap: 8, alignItems: 'end', marginBottom: 10 }}>
|
||||
<div>
|
||||
<label style={{ fontSize: 11, color: '#555' }}>ID quadro</label>
|
||||
<InputText value={q.id || ''}
|
||||
onChange={(e) => updateQuadro(qIdx, { id: e.target.value.toUpperCase() })}
|
||||
style={{ width: '100%', fontFamily: 'monospace' }} maxLength={4} />
|
||||
</div>
|
||||
<div style={{ gridColumn: 'span 2' }}>
|
||||
<label style={{ fontSize: 11, color: '#555' }}>Titolo</label>
|
||||
<InputText value={q.title || ''}
|
||||
onChange={(e) => updateQuadro(qIdx, { title: e.target.value })}
|
||||
style={{ width: '100%' }} />
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
<Button icon="pi pi-arrow-up" rounded outlined size="small"
|
||||
disabled={qIdx === 0} onClick={() => moveQuadro(qIdx, -1)} tooltip="Su" />
|
||||
<Button icon="pi pi-arrow-down" rounded outlined size="small"
|
||||
disabled={qIdx === quadri.length - 1} onClick={() => moveQuadro(qIdx, 1)} tooltip="Giu" />
|
||||
<Button icon="pi pi-trash" rounded outlined size="small" severity="danger"
|
||||
onClick={() => removeQuadro(qIdx)} tooltip="Elimina quadro" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 10 }}>
|
||||
<label style={{ fontSize: 11, color: '#555' }}>Descrizione</label>
|
||||
<InputTextarea rows={2} value={q.description || ''}
|
||||
onChange={(e) => updateQuadro(qIdx, { description: e.target.value })}
|
||||
style={{ width: '100%' }} />
|
||||
</div>
|
||||
|
||||
{q.is_legal_frame && (
|
||||
<Message severity="error" style={{ marginBottom: 10 }}
|
||||
text="Quadro normativo (D.Lgs. 231/2007). Il testo dichiara responsabilita legali del firmatario. Modifica con massima cautela: il contenuto viene inserito nel PDF firmato dal beneficiario." />
|
||||
)}
|
||||
|
||||
{/* Fields normali */}
|
||||
{(q.fields !== undefined) && (
|
||||
<>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 6 }}>
|
||||
<strong style={{ fontSize: 13 }}>Campi ({(q.fields || []).length})</strong>
|
||||
<Button label="Aggiungi campo" icon="pi pi-plus" size="small" outlined
|
||||
onClick={() => addField(qIdx)} />
|
||||
</div>
|
||||
{(q.fields || []).map((f, fIdx) =>
|
||||
renderFieldRow(f, fIdx,
|
||||
(p) => updateField(qIdx, fIdx, p),
|
||||
() => removeField(qIdx, fIdx),
|
||||
() => moveField(qIdx, fIdx, -1),
|
||||
() => moveField(qIdx, fIdx, 1),
|
||||
(q.fields || []).length
|
||||
)
|
||||
)}
|
||||
{(!q.fields || q.fields.length === 0) && !q.row_type && !q.nested_full && (
|
||||
<p style={{ color: '#888', fontStyle: 'italic', textAlign: 'center' }}>Nessun campo. Aggiungine uno.</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Row fields (Quadro B titolari) */}
|
||||
{q.row_type && (
|
||||
<>
|
||||
<Divider />
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 6 }}>
|
||||
<strong style={{ fontSize: 13 }}>Campi riga (per ogni {q.row_type}): ({(q.row_fields || []).length})</strong>
|
||||
<Button label="Aggiungi campo riga" icon="pi pi-plus" size="small" outlined
|
||||
onClick={() => addRowField(qIdx)} />
|
||||
</div>
|
||||
<Message severity="info" style={{ marginBottom: 8, fontSize: 12 }}
|
||||
text={`Questi campi si ripetono per ogni ${q.row_type} aggiunto dal beneficiario. Non eliminare il row_type se ci sono form gia compilati.`} />
|
||||
{(q.row_fields || []).map((f, fIdx) =>
|
||||
renderFieldRow(f, fIdx,
|
||||
(p) => updateRowField(qIdx, fIdx, p),
|
||||
() => removeRowField(qIdx, fIdx),
|
||||
() => {}, () => {},
|
||||
(q.row_fields || []).length
|
||||
)
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Nested full (Quadro C/D rappresentante/esecutore) */}
|
||||
{q.nested_full && (
|
||||
<>
|
||||
<Divider />
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 6 }}>
|
||||
<strong style={{ fontSize: 13 }}>Campi annidati: ({(q.nested_full.fields || []).length})</strong>
|
||||
<Button label="Aggiungi campo annidato" icon="pi pi-plus" size="small" outlined
|
||||
onClick={() => addNestedField(qIdx)} />
|
||||
</div>
|
||||
{(q.nested_full.fields || []).map((f, fIdx) =>
|
||||
renderFieldRow(f, fIdx,
|
||||
(p) => updateNestedField(qIdx, fIdx, p),
|
||||
() => removeNestedField(qIdx, fIdx),
|
||||
() => {}, () => {},
|
||||
(q.nested_full.fields || []).length
|
||||
)
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Upload slots info (Quadro F) */}
|
||||
{q.upload_slots && (
|
||||
<>
|
||||
<Divider />
|
||||
<strong style={{ fontSize: 13 }}>Slot upload:</strong>
|
||||
<div style={{ marginTop: 4 }}>
|
||||
{q.upload_slots.map((slot, i) => (
|
||||
<Tag key={i} value={slot.label || slot.id} severity="secondary" style={{ marginRight: 4 }} />
|
||||
))}
|
||||
</div>
|
||||
<small style={{ color: '#888' }}>Gli slot upload sono gestiti dal modello: modifica avanzata non ancora via UI.</small>
|
||||
</>
|
||||
)}
|
||||
</AccordionTab>
|
||||
))}
|
||||
</Accordion>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default QuadriStructureEditor;
|
||||
@@ -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