diff --git a/src/modules/ar1/pages/Ar1Signature.js b/src/modules/ar1/pages/Ar1Signature.js index 6613545..cb7e9af 100644 --- a/src/modules/ar1/pages/Ar1Signature.js +++ b/src/modules/ar1/pages/Ar1Signature.js @@ -76,6 +76,8 @@ const Ar1Signature = () => { } }; + const [uploadRecap, setUploadRecap] = useState(null); + const handleUploadSigned = (event) => { const file = event.files?.[0]; if (!file) return; @@ -96,14 +98,14 @@ const Ar1Signature = () => { resetUploadInput(); refreshForm(); const outcome = resp?.outcome; - if (outcome === 'VERIFIED') { - if (toast.current) toast.current.show({ severity: 'success', summary: 'Firma verificata!', detail: 'La dichiarazione e stata archiviata nei tuoi documenti aziendali.' }); - setTimeout(() => navigate('/ar1'), 1500); - } else if (outcome === 'SIGNED_NOT_VERIFIED') { - if (toast.current) toast.current.show({ severity: 'warn', summary: 'Firma accettata', detail: 'La firma e presente ma richiede verifica manuale da parte dell\'istruttore.' }); - } 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 sig = resp?.signature || {}; + const recapByOutcome = { + VERIFIED: { severity: 'success', title: __('Firma verificata', 'gepafin'), body: __('La dichiarazione e stata archiviata automaticamente nei tuoi documenti aziendali.', 'gepafin') }, + 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') }, + 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') } + }; + const r = recapByOutcome[outcome]; + if (r) setUploadRecap({ ...r, outcome, signerName: sig.signer_name, signerCf: sig.signer_cf, method: sig.method }); }, (err) => { setUploading(false); @@ -136,6 +138,27 @@ const Ar1Signature = () => {

{__('Firma AR1', 'gepafin')} — {form.variant}

+ {uploadRecap && ( + + {uploadRecap.title} +
{uploadRecap.body}
+ {(uploadRecap.signerName || uploadRecap.signerCf || uploadRecap.method) && ( +
+ {uploadRecap.signerName && {__('Firmatario', 'gepafin')}: {uploadRecap.signerName}{' '}} + {uploadRecap.signerCf && ({uploadRecap.signerCf}){' '}} + {uploadRecap.method && — {uploadRecap.method}} +
+ )} + {uploadRecap.outcome === 'VERIFIED' && ( +
+
+ )} + + } /> + )} + {!hasUnsignedPdf && (
diff --git a/src/modules/ar1/pages/Ar1Wizard.js b/src/modules/ar1/pages/Ar1Wizard.js index 2a8b83c..5c09ccb 100644 --- a/src/modules/ar1/pages/Ar1Wizard.js +++ b/src/modules/ar1/pages/Ar1Wizard.js @@ -41,12 +41,90 @@ const Ar1Wizard = () => { [form] ); + const [stepErrors, setStepErrors] = useState([]); + + // --- 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; + }; + + 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 && !f.show_if && isEmptyVal(d[f.id])) errs.push(`Quadro ${quadro.id}: ${f.label}`); + }); + return errs; + }; + + // 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 (toast.current) toast.current.show({ + severity: 'warn', sticky: true, + summary: __('Sessione scaduta', 'gepafin'), + detail: __('Il salvataggio non e riuscito perche la sessione e scaduta. I dati compilati sono conservati in questo browser: effettua di nuovo il login e riapri il modulo per recuperarli.', 'gepafin') + }); + }; + useEffect(() => { if (!formId) return; Ar1Service.getForm(formId, (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 }; + if (toast.current) toast.current.show({ severity: 'info', summary: 'Bozza recuperata', detail: 'Ripristinati i dati non salvati della sessione precedente.', life: 6000 }); + } + } + } catch (e) { /* draft corrotto: ignora */ } + setQuadriValues(initial); setLoading(false); }, (err) => { @@ -63,10 +141,15 @@ const Ar1Wizard = () => { const patch = { [quadroId]: quadriValues[quadroId] || {} }; setSaving(true); Ar1Service.updateQuadri(formId, patch, - (resp) => { setSaving(false); setForm(resp); }, + (resp) => { + setSaving(false); setForm(resp); + try { localStorage.removeItem(`ar1_draft_${formId}`); } catch (e) { /* noop */ } + }, (err) => { 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) }); } ); }; @@ -105,6 +188,21 @@ const Ar1Wizard = () => { const submitFinale = () => { 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); const patch = { [activeQuadro.id]: quadriValues[activeQuadro.id] || {} }; Ar1Service.updateQuadri(formId, patch, @@ -112,18 +210,23 @@ const Ar1Wizard = () => { Ar1Service.submitForSignature(formId, () => { 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' }); setTimeout(() => navigate(`/ar1/signature/${formId}`), 600); }, (err) => { 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) => { 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) }); } ); }; @@ -351,14 +454,34 @@ const Ar1Wizard = () => { model={steps} activeIndex={safeIndex} onSelect={(e) => { - if (!isReadonly && activeQuadro) saveQuadro(activeQuadro.id); const next = Math.max(0, Math.min(e.index, quadri.length - 1)); + if (!isReadonly && next > safeIndex) { + 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([]); + } + if (!isReadonly && activeQuadro) saveQuadro(activeQuadro.id); setActiveIndex(next); }} readOnly={false} style={{ marginBottom: 20 }} /> + {stepErrors.length > 0 && ( + + {__('Campi obbligatori mancanti', 'gepafin')}: +
    + {stepErrors.slice(0, 10).map((e, i) =>
  • {e}
  • )} + {stepErrors.length > 10 &&
  • {__('...e altri', 'gepafin')} {stepErrors.length - 10}
  • } +
+
+ } /> + )} { if (!isReadonly && activeQuadro) saveQuadro(activeQuadro.id); }}> {activeQuadro && renderQuadro(activeQuadro)} @@ -366,6 +489,7 @@ const Ar1Wizard = () => {