fix(ar1): esiti test UI 4/7 — barra Steps, banner 401, recap firma, note su NO
Report test UI 4/7/2026 (P1, P2a, P3, P6, P7): - P1: salto in avanti dalla barra Steps valida TUTTI i quadri intermedi (validateRange) e posiziona sul primo incompleto; indietro sempre libero - P2a: selezione No azzera la nota nel payload (niente note orfane persistite) - P3: Firma non rilevata come banner fisso in pagina (pattern recap), non toast - P6: banner sessione scaduta unico (ref dedup) + autosave sospeso dopo il primo 401 (solo backup locale, niente raffiche di PUT) - P7: Bozza recuperata come Message in pagina con dismiss, non toast volatile
This commit is contained in:
@@ -79,6 +79,7 @@ const Ar1Signature = () => {
|
|||||||
const [uploadRecap, setUploadRecap] = useState(null);
|
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;
|
||||||
|
|
||||||
@@ -111,14 +112,17 @@ const Ar1Signature = () => {
|
|||||||
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'))
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -42,6 +42,9 @@ const Ar1Wizard = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const [stepErrors, setStepErrors] = useState([]);
|
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) ---
|
// --- Validazione client obbligatori (specchio di app/validation.py BE) ---
|
||||||
const isEmptyVal = (v) => {
|
const isEmptyVal = (v) => {
|
||||||
@@ -102,6 +105,17 @@ const Ar1Wizard = () => {
|
|||||||
return errs;
|
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)
|
// detail BE puo essere stringa o {message, errors[]} (validazione 422)
|
||||||
const errDetail = (err) => {
|
const errDetail = (err) => {
|
||||||
const d = err?.detail;
|
const d = err?.detail;
|
||||||
@@ -120,11 +134,9 @@ const Ar1Wizard = () => {
|
|||||||
};
|
};
|
||||||
const handleAuthExpired = () => {
|
const handleAuthExpired = () => {
|
||||||
backupDraft();
|
backupDraft();
|
||||||
if (toast.current) toast.current.show({
|
if (authExpiredRef.current) return; // banner unico, niente pile di avvisi (P6)
|
||||||
severity: 'warn', sticky: true,
|
authExpiredRef.current = true;
|
||||||
summary: __('Sessione scaduta', 'gepafin'),
|
setAuthExpired(true);
|
||||||
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(() => {
|
useEffect(() => {
|
||||||
@@ -139,7 +151,7 @@ const Ar1Wizard = () => {
|
|||||||
const draft = JSON.parse(raw);
|
const draft = JSON.parse(raw);
|
||||||
if (draft && draft.quadri) {
|
if (draft && draft.quadri) {
|
||||||
initial = { ...initial, ...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 });
|
setDraftNotice(__('Bozza recuperata: ripristinati i dati non salvati della sessione precedente.', 'gepafin'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) { /* draft corrotto: ignora */ }
|
} catch (e) { /* draft corrotto: ignora */ }
|
||||||
@@ -157,6 +169,7 @@ 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,
|
||||||
@@ -333,7 +346,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>
|
||||||
@@ -475,10 +488,11 @@ const Ar1Wizard = () => {
|
|||||||
onSelect={(e) => {
|
onSelect={(e) => {
|
||||||
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) {
|
if (!isReadonly && next > safeIndex) {
|
||||||
const errs = validateQuadro(activeQuadro, quadriValues[activeQuadro.id]);
|
const bad = validateRange(safeIndex, next);
|
||||||
if (errs.length) {
|
if (bad) {
|
||||||
setStepErrors(errs);
|
setStepErrors(bad.errs);
|
||||||
if (toast.current) toast.current.show({ severity: 'warn', summary: __('Campi obbligatori mancanti', 'gepafin'), detail: errs.slice(0, 3).join(' — '), life: 6000 });
|
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;
|
return;
|
||||||
}
|
}
|
||||||
setStepErrors([]);
|
setStepErrors([]);
|
||||||
@@ -490,6 +504,22 @@ const Ar1Wizard = () => {
|
|||||||
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 && (
|
{stepErrors.length > 0 && (
|
||||||
<Message severity="warn" style={{ marginBottom: 14, display: 'block' }} content={
|
<Message severity="warn" style={{ marginBottom: 14, display: 'block' }} content={
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
Reference in New Issue
Block a user