Compare commits
6 Commits
fba47c6e77
...
feature/re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
abf415487d | ||
|
|
05cf15cad7 | ||
|
|
6dc715d997 | ||
|
|
6e5e8c61f5 | ||
|
|
7289113a8f | ||
| 548556d4fc |
@@ -6,10 +6,15 @@ import { Button } from 'primereact/button';
|
|||||||
import { Message } from 'primereact/message';
|
import { Message } from 'primereact/message';
|
||||||
import Ar1Service from '../service/ar1Service';
|
import Ar1Service from '../service/ar1Service';
|
||||||
import Ar1StatusTag from './Ar1StatusTag';
|
import Ar1StatusTag from './Ar1StatusTag';
|
||||||
|
import { useStoreValue } from '../../../store';
|
||||||
|
|
||||||
const DISMISS_SESSION_KEY_PREFIX = 'ar1-compliance-dismissed-';
|
const DISMISS_SESSION_KEY_PREFIX = 'ar1-compliance-dismissed-';
|
||||||
const DISMISS_WINDOW_HOURS = 24;
|
const DISMISS_WINDOW_HOURS = 24;
|
||||||
|
|
||||||
|
// AR1 (D.Lgs.231/2007) si applica solo alle aziende beneficiarie.
|
||||||
|
// Admin / istruttore / direttore NON devono ricevere il popup.
|
||||||
|
const AR1_POPUP_ALLOWED_ROLES = ['ROLE_BENEFICIARY', 'ROLE_CONFIDI'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dialog AR1 mostrato al login se l'azienda ha AR1 MISSING/EXPIRED/APPROACHING.
|
* Dialog AR1 mostrato al login se l'azienda ha AR1 MISSING/EXPIRED/APPROACHING.
|
||||||
* - dismissable=false (EXPIRED/MISSING): bloccante, solo CTA "Compila ora"
|
* - dismissable=false (EXPIRED/MISSING): bloccante, solo CTA "Compila ora"
|
||||||
@@ -20,11 +25,17 @@ const DISMISS_WINDOW_HOURS = 24;
|
|||||||
*/
|
*/
|
||||||
const Ar1ComplianceModal = ({ companyId }) => {
|
const Ar1ComplianceModal = ({ companyId }) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const role = useStoreValue('getRole');
|
||||||
const [status, setStatus] = useState(null);
|
const [status, setStatus] = useState(null);
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// Gate ruolo: solo aziende vedono il popup AR1.
|
||||||
|
if (!AR1_POPUP_ALLOWED_ROLES.includes(role)) {
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!companyId) return;
|
if (!companyId) return;
|
||||||
const dismissKey = DISMISS_SESSION_KEY_PREFIX + companyId;
|
const dismissKey = DISMISS_SESSION_KEY_PREFIX + companyId;
|
||||||
const dismissed = sessionStorage.getItem(dismissKey);
|
const dismissed = sessionStorage.getItem(dismissKey);
|
||||||
@@ -50,7 +61,7 @@ const Ar1ComplianceModal = ({ companyId }) => {
|
|||||||
console.warn('Ar1ComplianceModal: status check failed', err);
|
console.warn('Ar1ComplianceModal: status check failed', err);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}, [companyId]);
|
}, [companyId, role]);
|
||||||
|
|
||||||
const handleDismiss = () => {
|
const handleDismiss = () => {
|
||||||
if (!status?.is_popup_dismissible) return;
|
if (!status?.is_popup_dismissible) return;
|
||||||
|
|||||||
@@ -106,7 +106,6 @@ const Ar1Home = () => {
|
|||||||
|
|
||||||
const renderStatusCard = () => {
|
const renderStatusCard = () => {
|
||||||
if (!status) return null;
|
if (!status) return null;
|
||||||
const isUrgent = ['MISSING', 'EXPIRED'].includes(status.status);
|
|
||||||
const canCompile = ['MISSING', 'EXPIRED', 'APPROACHING', 'VALID'].includes(status.status);
|
const canCompile = ['MISSING', 'EXPIRED', 'APPROACHING', 'VALID'].includes(status.status);
|
||||||
const hasActive = status.form_id && ['DRAFT', 'AWAITING_SIGNATURE'].includes(status.status);
|
const hasActive = status.form_id && ['DRAFT', 'AWAITING_SIGNATURE'].includes(status.status);
|
||||||
|
|
||||||
@@ -119,13 +118,7 @@ const Ar1Home = () => {
|
|||||||
{__('Variante:', 'gepafin')} <strong>{status.variant}</strong>
|
{__('Variante:', 'gepafin')} <strong>{status.variant}</strong>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{status.days_to_expiry !== null && status.days_to_expiry !== undefined && (
|
{/* "Scade tra X giorni" / "Scaduta da X giorni" rimosso su richiesta Leonardo 6/7/2026 */}
|
||||||
<span style={{ color: isUrgent ? '#b71c1c' : '#444', fontWeight: 600 }}>
|
|
||||||
{status.days_to_expiry < 0
|
|
||||||
? __(`Scaduta da ${Math.abs(status.days_to_expiry)} giorni`, 'gepafin')
|
|
||||||
: __(`Scade tra ${status.days_to_expiry} giorni`, 'gepafin')}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{status.must_recompile_reason && (
|
{status.must_recompile_reason && (
|
||||||
|
|||||||
@@ -76,7 +76,10 @@ const Ar1Signature = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const [uploadRecap, setUploadRecap] = useState(null);
|
||||||
|
|
||||||
const handleUploadSigned = (event) => {
|
const handleUploadSigned = (event) => {
|
||||||
|
setUploadRecap(null);
|
||||||
const file = event.files?.[0];
|
const file = event.files?.[0];
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
|
|
||||||
@@ -96,27 +99,30 @@ const Ar1Signature = () => {
|
|||||||
resetUploadInput();
|
resetUploadInput();
|
||||||
refreshForm();
|
refreshForm();
|
||||||
const outcome = resp?.outcome;
|
const outcome = resp?.outcome;
|
||||||
if (outcome === 'VERIFIED') {
|
const sig = resp?.signature || {};
|
||||||
if (toast.current) toast.current.show({ severity: 'success', summary: 'Firma verificata!', detail: 'La dichiarazione e stata archiviata nei tuoi documenti aziendali.' });
|
const recapByOutcome = {
|
||||||
setTimeout(() => navigate('/ar1'), 1500);
|
VERIFIED: { severity: 'success', title: __('Firma verificata', 'gepafin'), body: __('La dichiarazione e stata archiviata automaticamente nei tuoi documenti aziendali.', 'gepafin') },
|
||||||
} else if (outcome === 'SIGNED_NOT_VERIFIED') {
|
SIGNED_NOT_VERIFIED: { severity: 'warn', title: __('Firma accettata — verifica manuale', 'gepafin'), body: __('La firma e presente ma richiede verifica manuale da parte dell\'istruttore.', 'gepafin') },
|
||||||
if (toast.current) toast.current.show({ severity: 'warn', summary: 'Firma accettata', detail: 'La firma e presente ma richiede verifica manuale da parte dell\'istruttore.' });
|
SIGNED_DOCVERIFY_UNAVAILABLE: { severity: 'warn', title: __('Firma accettata — verifica rimandata', 'gepafin'), body: __('Servizio di verifica momentaneamente non disponibile. L\'istruttore verifichera la firma manualmente.', 'gepafin') }
|
||||||
} else if (outcome === 'SIGNED_DOCVERIFY_UNAVAILABLE') {
|
};
|
||||||
if (toast.current) toast.current.show({ severity: 'warn', summary: 'Verifica rimandata', detail: 'Servizio di verifica momentaneamente non disponibile. L\'istruttore verifichera la firma manualmente.' });
|
const r = recapByOutcome[outcome];
|
||||||
}
|
if (r) setUploadRecap({ ...r, outcome, signerName: sig.signer_name, signerCf: sig.signer_cf, method: sig.method });
|
||||||
},
|
},
|
||||||
(err) => {
|
(err) => {
|
||||||
setUploading(false);
|
setUploading(false);
|
||||||
resetUploadInput();
|
resetUploadInput();
|
||||||
if (err?.detail?.code === 'NO_SIGNATURE_DETECTED') {
|
if (err?.detail?.code === 'NO_SIGNATURE_DETECTED') {
|
||||||
if (toast.current) toast.current.show({
|
setUploadRecap({
|
||||||
severity: 'error',
|
severity: 'error', outcome: 'NO_SIGNATURE_DETECTED',
|
||||||
summary: 'Firma non rilevata',
|
title: __('Firma non rilevata', 'gepafin'),
|
||||||
detail: 'Il file caricato non contiene una firma digitale valida. Firmare il PDF con il proprio strumento FEQ e ricaricarlo.',
|
body: __('Il file caricato non contiene una firma digitale valida. Firmare il PDF con il proprio strumento FEQ (CAdES .p7m o PAdES) e ricaricarlo.', 'gepafin')
|
||||||
life: 6000
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
if (toast.current) toast.current.show({ severity: 'error', summary: 'Errore', detail: typeof err?.detail === 'string' ? err.detail : (err?.detail?.message || 'Upload fallito') });
|
setUploadRecap({
|
||||||
|
severity: 'error', outcome: 'UPLOAD_ERROR',
|
||||||
|
title: __('Errore', 'gepafin'),
|
||||||
|
body: typeof err?.detail === 'string' ? err.detail : (err?.detail?.message || __('Upload fallito', 'gepafin'))
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -136,6 +142,27 @@ const Ar1Signature = () => {
|
|||||||
<h1>{__('Firma AR1', 'gepafin')} — {form.variant}</h1>
|
<h1>{__('Firma AR1', 'gepafin')} — {form.variant}</h1>
|
||||||
<div style={{ marginBottom: 20 }}><Ar1StatusTag status={form.status} /></div>
|
<div style={{ marginBottom: 20 }}><Ar1StatusTag status={form.status} /></div>
|
||||||
|
|
||||||
|
{uploadRecap && (
|
||||||
|
<Message severity={uploadRecap.severity} style={{ marginBottom: 14, display: 'block' }} content={
|
||||||
|
<div style={{ width: '100%' }}>
|
||||||
|
<b>{uploadRecap.title}</b>
|
||||||
|
<div style={{ marginTop: 4 }}>{uploadRecap.body}</div>
|
||||||
|
{(uploadRecap.signerName || uploadRecap.signerCf || uploadRecap.method) && (
|
||||||
|
<div style={{ marginTop: 6, fontSize: '0.9em' }}>
|
||||||
|
{uploadRecap.signerName && <span>{__('Firmatario', 'gepafin')}: <b>{uploadRecap.signerName}</b>{' '}</span>}
|
||||||
|
{uploadRecap.signerCf && <span>({uploadRecap.signerCf}){' '}</span>}
|
||||||
|
{uploadRecap.method && <span>— {uploadRecap.method}</span>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{uploadRecap.outcome === 'VERIFIED' && (
|
||||||
|
<div style={{ marginTop: 10 }}>
|
||||||
|
<Button label={__('Torna ai documenti', 'gepafin')} icon="pi pi-arrow-left" size="small" onClick={() => navigate('/ar1')} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
} />
|
||||||
|
)}
|
||||||
|
|
||||||
<Card title={__('1. Scarica il modulo AR1 da firmare', 'gepafin')} style={{ marginBottom: 14 }}>
|
<Card title={__('1. Scarica il modulo AR1 da firmare', 'gepafin')} style={{ marginBottom: 14 }}>
|
||||||
{!hasUnsignedPdf && (
|
{!hasUnsignedPdf && (
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -41,12 +41,121 @@ const Ar1Wizard = () => {
|
|||||||
[form]
|
[form]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [stepErrors, setStepErrors] = useState([]);
|
||||||
|
const [authExpired, setAuthExpired] = useState(false);
|
||||||
|
const authExpiredRef = useRef(false);
|
||||||
|
const [draftNotice, setDraftNotice] = useState(null);
|
||||||
|
|
||||||
|
// --- Validazione client obbligatori (specchio di app/validation.py BE) ---
|
||||||
|
const isEmptyVal = (v) => {
|
||||||
|
if (v === null || v === undefined) return true;
|
||||||
|
if (typeof v === 'string') return v.trim() === '';
|
||||||
|
if (Array.isArray(v)) return v.length === 0;
|
||||||
|
if (typeof v === 'object') {
|
||||||
|
if ('value' in v) return isEmptyVal(v.value);
|
||||||
|
return Object.values(v).every(isEmptyVal);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// show_if formati seed: "campo == 'VALORE'", "campo == true", "campo != true"
|
||||||
|
const evalShowIf = (expr, quadroData) => {
|
||||||
|
if (!expr) return true;
|
||||||
|
const m = String(expr).match(/^\s*(\w+)\s*(==|!=)\s*(?:'(.*)'|(true|false))\s*$/);
|
||||||
|
if (!m) return true; // formato ignoto: non nascondere mai per errore
|
||||||
|
const raw = (quadroData || {})[m[1]];
|
||||||
|
const val = raw && typeof raw === 'object' && 'value' in raw ? raw.value : raw;
|
||||||
|
const target = m[4] !== undefined ? (m[4] === 'true') : m[3];
|
||||||
|
const eq = m[4] !== undefined ? (!!val === target) : (String(val ?? '') === target);
|
||||||
|
return m[2] === '==' ? eq : !eq;
|
||||||
|
};
|
||||||
|
const isFieldVisible = (field, quadroData) => evalShowIf(field.show_if, quadroData);
|
||||||
|
|
||||||
|
const validateQuadro = (quadro, data) => {
|
||||||
|
const errs = [];
|
||||||
|
const d = data || {};
|
||||||
|
if (!quadro || quadro.is_legal_frame) return errs;
|
||||||
|
if (quadro.row_type) {
|
||||||
|
const rows = d.rows || [];
|
||||||
|
if (rows.length === 0) {
|
||||||
|
errs.push(`Quadro ${quadro.id}: obbligatoria almeno una riga di dati`);
|
||||||
|
return errs;
|
||||||
|
}
|
||||||
|
rows.forEach((row, i) => {
|
||||||
|
(quadro.row_fields || []).forEach(f => {
|
||||||
|
if (f.required && !f.show_if && isEmptyVal((row || {})[f.id])) {
|
||||||
|
errs.push(`Quadro ${quadro.id} (riga ${i + 1}): ${f.label}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return errs;
|
||||||
|
}
|
||||||
|
(quadro.upload_slots || []).forEach(sl => {
|
||||||
|
if (sl.required && isEmptyVal(d[sl.id])) errs.push(`Quadro ${quadro.id}: allegato obbligatorio — ${sl.label}`);
|
||||||
|
});
|
||||||
|
(quadro.fields || []).forEach(f => {
|
||||||
|
if (f.required && isFieldVisible(f, d) && isEmptyVal(d[f.id])) errs.push(`Quadro ${quadro.id}: ${f.label}`);
|
||||||
|
});
|
||||||
|
if (quadro.nested_full && evalShowIf(quadro.nested_full.show_if, d)) {
|
||||||
|
const nd = d.nested || {};
|
||||||
|
(quadro.nested_full.fields || []).forEach(f => {
|
||||||
|
if (f.required && isFieldVisible(f, nd) && isEmptyVal(nd[f.id])) errs.push(`Quadro ${quadro.id} (dettaglio): ${f.label}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return errs;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Valida tutti i quadri in [from, to): al salto in avanti nessun quadro
|
||||||
|
// intermedio puo essere incompleto (report test 4/7/2026 P1)
|
||||||
|
const validateRange = (fromIdx, toIdx) => {
|
||||||
|
for (let i = fromIdx; i < toIdx; i++) {
|
||||||
|
const q = quadri[i];
|
||||||
|
const errs = validateQuadro(q, quadriValues[q.id]);
|
||||||
|
if (errs.length) return { errs, badIndex: i };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// detail BE puo essere stringa o {message, errors[]} (validazione 422)
|
||||||
|
const errDetail = (err) => {
|
||||||
|
const d = err?.detail;
|
||||||
|
if (d && typeof d === 'object') {
|
||||||
|
const list = (d.errors || []).slice(0, 5).join('; ');
|
||||||
|
return `${d.message || 'Errore'}${list ? ': ' + list : ''}`;
|
||||||
|
}
|
||||||
|
return d || 'Errore';
|
||||||
|
};
|
||||||
|
|
||||||
|
// JWT scaduto durante la compilazione: bozza in localStorage + avviso persistente
|
||||||
|
const backupDraft = () => {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(`ar1_draft_${formId}`, JSON.stringify({ ts: Date.now(), quadri: quadriValues }));
|
||||||
|
} catch (e) { /* quota o private mode: pazienza */ }
|
||||||
|
};
|
||||||
|
const handleAuthExpired = () => {
|
||||||
|
backupDraft();
|
||||||
|
if (authExpiredRef.current) return; // banner unico, niente pile di avvisi (P6)
|
||||||
|
authExpiredRef.current = true;
|
||||||
|
setAuthExpired(true);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!formId) return;
|
if (!formId) return;
|
||||||
Ar1Service.getForm(formId,
|
Ar1Service.getForm(formId,
|
||||||
(resp) => {
|
(resp) => {
|
||||||
setForm(resp);
|
setForm(resp);
|
||||||
setQuadriValues(resp.quadri || {});
|
let initial = resp.quadri || {};
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(`ar1_draft_${formId}`);
|
||||||
|
if (raw && resp.status === 'DRAFT') {
|
||||||
|
const draft = JSON.parse(raw);
|
||||||
|
if (draft && draft.quadri) {
|
||||||
|
initial = { ...initial, ...draft.quadri };
|
||||||
|
setDraftNotice(__('Bozza recuperata: ripristinati i dati non salvati della sessione precedente.', 'gepafin'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) { /* draft corrotto: ignora */ }
|
||||||
|
setQuadriValues(initial);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
},
|
},
|
||||||
(err) => {
|
(err) => {
|
||||||
@@ -60,22 +169,33 @@ const Ar1Wizard = () => {
|
|||||||
|
|
||||||
const saveQuadro = (quadroId) => {
|
const saveQuadro = (quadroId) => {
|
||||||
if (isReadonly) return;
|
if (isReadonly) return;
|
||||||
|
if (authExpiredRef.current) { backupDraft(); return; } // 401 gia noto: solo backup locale
|
||||||
const patch = { [quadroId]: quadriValues[quadroId] || {} };
|
const patch = { [quadroId]: quadriValues[quadroId] || {} };
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
Ar1Service.updateQuadri(formId, patch,
|
Ar1Service.updateQuadri(formId, patch,
|
||||||
(resp) => { setSaving(false); setForm(resp); },
|
(resp) => {
|
||||||
|
setSaving(false); setForm(resp);
|
||||||
|
try { localStorage.removeItem(`ar1_draft_${formId}`); } catch (e) { /* noop */ }
|
||||||
|
},
|
||||||
(err) => {
|
(err) => {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
if (toast.current) toast.current.show({ severity: 'warn', summary: 'Save fallito', detail: err?.detail || 'Riprovare' });
|
if (err?.status === 401) { handleAuthExpired(); return; }
|
||||||
|
backupDraft();
|
||||||
|
if (toast.current) toast.current.show({ severity: 'warn', summary: 'Save fallito', detail: errDetail(err) });
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFieldChange = (quadroId, fieldId, value) => {
|
const handleFieldChange = (quadroId, fieldId, value) => {
|
||||||
setQuadriValues(prev => ({
|
setQuadriValues(prev => {
|
||||||
...prev,
|
const next = { ...prev, [quadroId]: { ...(prev[quadroId] || {}), [fieldId]: value } };
|
||||||
[quadroId]: { ...(prev[quadroId] || {}), [fieldId]: value }
|
// Autosave sospeso (post-401): il draft locale segue OGNI modifica, non solo
|
||||||
}));
|
// lo snapshot iniziale (fix R6 — niente perdita dei dati inseriti dopo il banner)
|
||||||
|
if (authExpiredRef.current) {
|
||||||
|
try { localStorage.setItem(`ar1_draft_${formId}`, JSON.stringify({ ts: Date.now(), quadri: next })); } catch (e) { /* quota */ }
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRowFieldChange = (quadroId, rowIndex, fieldId, value) => {
|
const handleRowFieldChange = (quadroId, rowIndex, fieldId, value) => {
|
||||||
@@ -105,6 +225,21 @@ const Ar1Wizard = () => {
|
|||||||
|
|
||||||
const submitFinale = () => {
|
const submitFinale = () => {
|
||||||
if (!activeQuadro) return;
|
if (!activeQuadro) return;
|
||||||
|
// Gate client: tutti i quadri obbligatori completi prima della firma
|
||||||
|
const allErrs = [];
|
||||||
|
let firstBadIndex = -1;
|
||||||
|
quadri.forEach((q, i) => {
|
||||||
|
const errs = validateQuadro(q, quadriValues[q.id]);
|
||||||
|
if (errs.length && firstBadIndex === -1) firstBadIndex = i;
|
||||||
|
allErrs.push(...errs);
|
||||||
|
});
|
||||||
|
if (allErrs.length) {
|
||||||
|
setStepErrors(allErrs);
|
||||||
|
if (firstBadIndex >= 0) setActiveIndex(firstBadIndex);
|
||||||
|
if (toast.current) toast.current.show({ severity: 'warn', summary: __('Quadri obbligatori incompleti', 'gepafin'), detail: allErrs.slice(0, 4).join(' — '), life: 8000 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setStepErrors([]);
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
const patch = { [activeQuadro.id]: quadriValues[activeQuadro.id] || {} };
|
const patch = { [activeQuadro.id]: quadriValues[activeQuadro.id] || {} };
|
||||||
Ar1Service.updateQuadri(formId, patch,
|
Ar1Service.updateQuadri(formId, patch,
|
||||||
@@ -112,18 +247,23 @@ const Ar1Wizard = () => {
|
|||||||
Ar1Service.submitForSignature(formId,
|
Ar1Service.submitForSignature(formId,
|
||||||
() => {
|
() => {
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
|
try { localStorage.removeItem(`ar1_draft_${formId}`); } catch (e) { /* noop */ }
|
||||||
if (toast.current) toast.current.show({ severity: 'success', summary: 'OK', detail: 'Modulo pronto per la firma' });
|
if (toast.current) toast.current.show({ severity: 'success', summary: 'OK', detail: 'Modulo pronto per la firma' });
|
||||||
setTimeout(() => navigate(`/ar1/signature/${formId}`), 600);
|
setTimeout(() => navigate(`/ar1/signature/${formId}`), 600);
|
||||||
},
|
},
|
||||||
(err) => {
|
(err) => {
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
if (toast.current) toast.current.show({ severity: 'error', summary: 'Errore', detail: err?.detail || 'Submit fallito' });
|
if (err?.status === 401) { handleAuthExpired(); return; }
|
||||||
|
const beErrs = (err?.detail && typeof err.detail === 'object' && err.detail.errors) || [];
|
||||||
|
if (beErrs.length) setStepErrors(beErrs);
|
||||||
|
if (toast.current) toast.current.show({ severity: 'error', summary: 'Errore', detail: errDetail(err) });
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
(err) => {
|
(err) => {
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
if (toast.current) toast.current.show({ severity: 'error', summary: 'Errore', detail: err?.detail || 'Save fallito' });
|
if (err?.status === 401) { handleAuthExpired(); return; }
|
||||||
|
if (toast.current) toast.current.show({ severity: 'error', summary: 'Errore', detail: errDetail(err) });
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -163,7 +303,7 @@ const Ar1Wizard = () => {
|
|||||||
return (
|
return (
|
||||||
<div key={key} style={{ marginBottom: 14 }}>
|
<div key={key} style={{ marginBottom: 14 }}>
|
||||||
{commonLabel}
|
{commonLabel}
|
||||||
<Calendar id={key} value={value ? new Date(value) : null} onChange={(e) => onChange(field.id, e.value ? e.value.toISOString().slice(0, 10) : null)} disabled={disabled} dateFormat="dd/mm/yy" showIcon style={{ width: '100%' }} />
|
<Calendar id={key} value={value ? new Date(value + 'T00:00:00') : null} onChange={(e) => onChange(field.id, e.value ? `${e.value.getFullYear()}-${String(e.value.getMonth() + 1).padStart(2, '0')}-${String(e.value.getDate()).padStart(2, '0')}` : null)} disabled={disabled} minDate={field.min_today ? (() => { const t = new Date(); t.setHours(0,0,0,0); return t; })() : undefined} dateFormat="dd/mm/yy" showIcon style={{ width: '100%' }} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
case 'checkbox':
|
case 'checkbox':
|
||||||
@@ -211,7 +351,7 @@ const Ar1Wizard = () => {
|
|||||||
<label htmlFor={`${key}-yes`}>{__('Si', 'gepafin')}</label>
|
<label htmlFor={`${key}-yes`}>{__('Si', 'gepafin')}</label>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||||
<RadioButton inputId={`${key}-no`} checked={no} onChange={() => onChange(field.id, { ...v, value: 'no' })} disabled={disabled} />
|
<RadioButton inputId={`${key}-no`} checked={no} onChange={() => onChange(field.id, { value: 'no' })} disabled={disabled} />
|
||||||
<label htmlFor={`${key}-no`}>{__('No', 'gepafin')}</label>
|
<label htmlFor={`${key}-no`}>{__('No', 'gepafin')}</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -238,6 +378,43 @@ const Ar1Wizard = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Nominativi titolari effettivi dal Quadro B (per prefill coincidenza LR/esecutore)
|
||||||
|
const titolariB = useMemo(() => {
|
||||||
|
const b = quadriValues['B'] || {};
|
||||||
|
const rows = b.rows || [];
|
||||||
|
return rows.map(r => (r && r.cognome_nome ? String(r.cognome_nome).trim() : '')).filter(Boolean);
|
||||||
|
}, [quadriValues]);
|
||||||
|
|
||||||
|
// Campo nominativo di coincidenza (Quadro C: lr_te_reference, D: esecutore_lr_reference).
|
||||||
|
// Reso SOLO se la spunta di coincidenza e attiva; il valore e sempre persistito
|
||||||
|
// cosi da comparire nel PDF (fix regressione R7, modulo 231/2007 mai senza nominativo).
|
||||||
|
const renderCoincidenceRef = (quadro, field, q) => {
|
||||||
|
if (!isFieldVisible(field, q)) return null; // spunta OFF: se ne occupa il nested_full
|
||||||
|
const key = `q-${quadro.id}-${field.id}`;
|
||||||
|
const val = q[field.id] || '';
|
||||||
|
const onSet = (v) => handleFieldChange(quadro.id, field.id, v);
|
||||||
|
return (
|
||||||
|
<div key={key} style={{ marginBottom: 14 }}>
|
||||||
|
<label htmlFor={key} style={{ display: 'block', marginBottom: 4, fontWeight: 500 }}>{field.label}{field.required ? ' *' : ''}</label>
|
||||||
|
{titolariB.length > 0 ? (
|
||||||
|
<Dropdown
|
||||||
|
id={key}
|
||||||
|
value={val}
|
||||||
|
options={titolariB.map(n => ({ label: n, value: n }))}
|
||||||
|
onChange={(e) => onSet(e.value)}
|
||||||
|
disabled={isReadonly}
|
||||||
|
placeholder={__('Seleziona dal Quadro B', 'gepafin')}
|
||||||
|
editable
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<InputText id={key} value={val} onChange={(e) => onSet(e.target.value)} disabled={isReadonly} maxLength={field.max_length} placeholder={__('Cognome e Nome', 'gepafin')} style={{ width: '100%' }} />
|
||||||
|
)}
|
||||||
|
<small style={{ color: '#888' }}>{__('Compilare con uno dei titolari effettivi indicati al Quadro B.', 'gepafin')}</small>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const renderQuadro = (quadro) => {
|
const renderQuadro = (quadro) => {
|
||||||
const q = quadriValues[quadro.id] || {};
|
const q = quadriValues[quadro.id] || {};
|
||||||
|
|
||||||
@@ -313,8 +490,9 @@ const Ar1Wizard = () => {
|
|||||||
<div>
|
<div>
|
||||||
<h3>{quadro.title}</h3>
|
<h3>{quadro.title}</h3>
|
||||||
{quadro.description && <p style={{ color: '#666' }}>{quadro.description}</p>}
|
{quadro.description && <p style={{ color: '#666' }}>{quadro.description}</p>}
|
||||||
{(quadro.fields || []).map(field => renderField(field, q[field.id], (fid, val) => handleFieldChange(quadro.id, fid, val), `q-${quadro.id}`))}
|
{(quadro.fields || []).filter(field => isFieldVisible(field, q) && !field.coincidence_ref).map(field => renderField(field, q[field.id], (fid, val) => handleFieldChange(quadro.id, fid, val), `q-${quadro.id}`))}
|
||||||
{quadro.nested_full && (
|
{(quadro.fields || []).filter(field => field.coincidence_ref).map(field => renderCoincidenceRef(quadro, field, q))}
|
||||||
|
{quadro.nested_full && evalShowIf(quadro.nested_full.show_if, q) && (
|
||||||
<div style={{ marginTop: 16, padding: 12, background: '#f5f5f5', borderRadius: 4 }}>
|
<div style={{ marginTop: 16, padding: 12, background: '#f5f5f5', borderRadius: 4 }}>
|
||||||
<h4 style={{ marginTop: 0 }}>{__('Dettaglio aggiuntivo', 'gepafin')}</h4>
|
<h4 style={{ marginTop: 0 }}>{__('Dettaglio aggiuntivo', 'gepafin')}</h4>
|
||||||
{(quadro.nested_full.fields || []).map(field => renderField(
|
{(quadro.nested_full.fields || []).map(field => renderField(
|
||||||
@@ -351,14 +529,52 @@ const Ar1Wizard = () => {
|
|||||||
model={steps}
|
model={steps}
|
||||||
activeIndex={safeIndex}
|
activeIndex={safeIndex}
|
||||||
onSelect={(e) => {
|
onSelect={(e) => {
|
||||||
if (!isReadonly && activeQuadro) saveQuadro(activeQuadro.id);
|
|
||||||
const next = Math.max(0, Math.min(e.index, quadri.length - 1));
|
const next = Math.max(0, Math.min(e.index, quadri.length - 1));
|
||||||
|
if (!isReadonly && next > safeIndex) {
|
||||||
|
const bad = validateRange(safeIndex, next);
|
||||||
|
if (bad) {
|
||||||
|
setStepErrors(bad.errs);
|
||||||
|
setActiveIndex(bad.badIndex);
|
||||||
|
if (toast.current) toast.current.show({ severity: 'warn', summary: __('Campi obbligatori mancanti', 'gepafin'), detail: bad.errs.slice(0, 3).join(' — '), life: 6000 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setStepErrors([]);
|
||||||
|
}
|
||||||
|
if (!isReadonly && activeQuadro) saveQuadro(activeQuadro.id);
|
||||||
|
setStepErrors([]);
|
||||||
setActiveIndex(next);
|
setActiveIndex(next);
|
||||||
}}
|
}}
|
||||||
readOnly={false}
|
readOnly={false}
|
||||||
style={{ marginBottom: 20 }}
|
style={{ marginBottom: 20 }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{authExpired && (
|
||||||
|
<Message severity="warn" style={{ marginBottom: 14, display: 'block' }} content={
|
||||||
|
<div>
|
||||||
|
<b>{__('Sessione scaduta', 'gepafin')}</b>
|
||||||
|
<div style={{ marginTop: 4 }}>{__('Il salvataggio automatico e sospeso: i dati compilati sono conservati in questo browser. Effettua di nuovo il login e riapri il modulo per recuperarli e continuare.', 'gepafin')}</div>
|
||||||
|
</div>
|
||||||
|
} />
|
||||||
|
)}
|
||||||
|
{draftNotice && !authExpired && (
|
||||||
|
<Message severity="info" style={{ marginBottom: 14, display: 'block' }} content={
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 12 }}>
|
||||||
|
<span>{draftNotice}</span>
|
||||||
|
<Button icon="pi pi-times" text size="small" onClick={() => setDraftNotice(null)} />
|
||||||
|
</div>
|
||||||
|
} />
|
||||||
|
)}
|
||||||
|
{stepErrors.length > 0 && (
|
||||||
|
<Message severity="warn" style={{ marginBottom: 14, display: 'block' }} content={
|
||||||
|
<div>
|
||||||
|
<b>{__('Campi obbligatori mancanti', 'gepafin')}:</b>
|
||||||
|
<ul style={{ margin: '6px 0 0 0', paddingLeft: 18 }}>
|
||||||
|
{stepErrors.slice(0, 10).map((e, i) => <li key={i}>{e}</li>)}
|
||||||
|
{stepErrors.length > 10 && <li>{__('...e altri', 'gepafin')} {stepErrors.length - 10}</li>}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
} />
|
||||||
|
)}
|
||||||
<Card style={{ marginBottom: 14 }} onBlur={() => { if (!isReadonly && activeQuadro) saveQuadro(activeQuadro.id); }}>
|
<Card style={{ marginBottom: 14 }} onBlur={() => { if (!isReadonly && activeQuadro) saveQuadro(activeQuadro.id); }}>
|
||||||
{activeQuadro && renderQuadro(activeQuadro)}
|
{activeQuadro && renderQuadro(activeQuadro)}
|
||||||
</Card>
|
</Card>
|
||||||
@@ -366,6 +582,7 @@ const Ar1Wizard = () => {
|
|||||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||||
<Button label={__('Indietro', 'gepafin')} icon="pi pi-arrow-left" severity="secondary" outlined disabled={activeIndex === 0}
|
<Button label={__('Indietro', 'gepafin')} icon="pi pi-arrow-left" severity="secondary" outlined disabled={activeIndex === 0}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
setStepErrors([]);
|
||||||
if (!isReadonly && activeQuadro) saveQuadro(activeQuadro.id);
|
if (!isReadonly && activeQuadro) saveQuadro(activeQuadro.id);
|
||||||
setActiveIndex(Math.max(0, activeIndex - 1));
|
setActiveIndex(Math.max(0, activeIndex - 1));
|
||||||
}}
|
}}
|
||||||
@@ -375,7 +592,17 @@ const Ar1Wizard = () => {
|
|||||||
{!isLastStep && (
|
{!isLastStep && (
|
||||||
<Button label={__('Avanti', 'gepafin')} icon="pi pi-arrow-right" iconPos="right"
|
<Button label={__('Avanti', 'gepafin')} icon="pi pi-arrow-right" iconPos="right"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!isReadonly && activeQuadro) saveQuadro(activeQuadro.id);
|
if (!isReadonly) {
|
||||||
|
const errs = validateQuadro(activeQuadro, quadriValues[activeQuadro.id]);
|
||||||
|
if (errs.length) {
|
||||||
|
setStepErrors(errs);
|
||||||
|
if (toast.current) toast.current.show({ severity: 'warn', summary: __('Campi obbligatori mancanti', 'gepafin'), detail: errs.slice(0, 3).join(' — '), life: 6000 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setStepErrors([]);
|
||||||
|
saveQuadro(activeQuadro.id);
|
||||||
|
}
|
||||||
|
setStepErrors([]);
|
||||||
setActiveIndex(Math.min(quadri.length - 1, activeIndex + 1));
|
setActiveIndex(Math.min(quadri.length - 1, activeIndex + 1));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user