diff --git a/src/modules/ar1/components/QuadriStructureEditor.js b/src/modules/ar1/components/QuadriStructureEditor.js
new file mode 100644
index 0000000..13c8190
--- /dev/null
+++ b/src/modules/ar1/components/QuadriStructureEditor.js
@@ -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 (
+
+
+
+
+ onUpdate({ id: e.target.value.replace(/\s+/g, '_') })}
+ style={{ width: '100%', fontFamily: 'monospace', fontSize: 12 }} />
+
+
+
+ onUpdate({ label: e.target.value })}
+ style={{ width: '100%' }} />
+
+
+
+ onUpdate({ type: e.value })}
+ style={{ width: '100%' }} />
+
+
+
+
+
+
+
+ onUpdate({ required: e.checked })} />
+
+
+
+ {field.type === 'text' && (
+ <>
+
+
+ onUpdate({ max_length: e.value })}
+ style={{ width: 90 }} inputStyle={{ fontSize: 12 }} />
+
+
+
+ onUpdate({ pattern: e.target.value || undefined })}
+ style={{ width: 180, fontFamily: 'monospace', fontSize: 11 }}
+ placeholder="es. ^[0-9]{11}$" />
+
+ >
+ )}
+
+ {field.prefill_from && (
+
+ )}
+
+
+ {isEnum && (
+
+
+ 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 }} />
+
+ )}
+
+ );
+ };
+
+ // ----- render -----
+ return (
+
+
+
+ {/* Metadati */}
+
+
+
+
+ updateMeta('variant_label', e.target.value)} style={{ width: '100%' }} />
+
+
+
+ updateMeta('legal_ref', e.target.value)} style={{ width: '100%' }}
+ placeholder="es. D.Lgs. 231/2007" />
+
+
+
+ updateMeta('variant_description', e.target.value)} style={{ width: '100%' }} />
+
+
+
+ updateMeta('normative_frame', e.target.value)}
+ style={{ width: '100%', fontSize: 12 }} />
+
+
+
+
+ {/* Quadri */}
+
+
Quadri ({quadri.length})
+
+
+
+
+ {quadri.map((q, qIdx) => (
+
+
+ {q.title || '(senza titolo)'}
+ {q.is_legal_frame && }
+ {q.row_type && }
+ {q.nested_full && }
+
+ {(q.fields || []).length} campi
+ {q.row_fields ? ` +${q.row_fields.length} riga` : ''}
+ {q.nested_full?.fields ? ` +${q.nested_full.fields.length} nidif.` : ''}
+
+
+ }>
+
+
+
+ updateQuadro(qIdx, { id: e.target.value.toUpperCase() })}
+ style={{ width: '100%', fontFamily: 'monospace' }} maxLength={4} />
+
+
+
+ updateQuadro(qIdx, { title: e.target.value })}
+ style={{ width: '100%' }} />
+
+
+ moveQuadro(qIdx, -1)} tooltip="Su" />
+ moveQuadro(qIdx, 1)} tooltip="Giu" />
+ removeQuadro(qIdx)} tooltip="Elimina quadro" />
+
+
+
+
+
+ updateQuadro(qIdx, { description: e.target.value })}
+ style={{ width: '100%' }} />
+
+
+ {q.is_legal_frame && (
+
+ )}
+
+ {/* Fields normali */}
+ {(q.fields !== undefined) && (
+ <>
+
+ Campi ({(q.fields || []).length})
+ addField(qIdx)} />
+
+ {(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 && (
+ Nessun campo. Aggiungine uno.
+ )}
+ >
+ )}
+
+ {/* Row fields (Quadro B titolari) */}
+ {q.row_type && (
+ <>
+
+
+ Campi riga (per ogni {q.row_type}): ({(q.row_fields || []).length})
+ addRowField(qIdx)} />
+
+
+ {(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 && (
+ <>
+
+
+ Campi annidati: ({(q.nested_full.fields || []).length})
+ addNestedField(qIdx)} />
+
+ {(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 && (
+ <>
+
+ Slot upload:
+
+ {q.upload_slots.map((slot, i) => (
+
+ ))}
+
+ Gli slot upload sono gestiti dal modello: modifica avanzata non ancora via UI.
+ >
+ )}
+
+ ))}
+
+
+ );
+};
+
+export default QuadriStructureEditor;
diff --git a/src/modules/ar1/pages/Ar1AdminConfig.js b/src/modules/ar1/pages/Ar1AdminConfig.js
index 3d12427..1c5bfa7 100644
--- a/src/modules/ar1/pages/Ar1AdminConfig.js
+++ b/src/modules/ar1/pages/Ar1AdminConfig.js
@@ -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 = () => {
}
}} />
openEditLayout(row)} disabled={row.status === 'ARCHIVED'} />
- openNewVersion(row.variant)} />
+ tooltip="Modifica (layout + struttura quadri)" tooltipOptions={{ position: 'top' }}
+ onClick={() => openEditTemplate(row)} disabled={row.status === 'ARCHIVED'} />
);
@@ -787,108 +837,155 @@ const Ar1AdminConfig = () => {
{/* ==================== DIALOGS ==================== */}
- {/* Edit layout template */}
+ {/* Edit template unificato: layout + struttura quadri */}
-
- {/* Nuova versione */}
-
{/* Edit regola PEC */}