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:
Kitzanos
2026-07-04 08:43:54 +02:00
parent 6e5e8c61f5
commit 6dc715d997
2 changed files with 51 additions and 17 deletions

View File

@@ -79,6 +79,7 @@ const Ar1Signature = () => {
const [uploadRecap, setUploadRecap] = useState(null);
const handleUploadSigned = (event) => {
setUploadRecap(null);
const file = event.files?.[0];
if (!file) return;
@@ -111,14 +112,17 @@ const Ar1Signature = () => {
setUploading(false);
resetUploadInput();
if (err?.detail?.code === 'NO_SIGNATURE_DETECTED') {
if (toast.current) toast.current.show({
severity: 'error',
summary: 'Firma non rilevata',
detail: 'Il file caricato non contiene una firma digitale valida. Firmare il PDF con il proprio strumento FEQ e ricaricarlo.',
life: 6000
setUploadRecap({
severity: 'error', outcome: 'NO_SIGNATURE_DETECTED',
title: __('Firma non rilevata', 'gepafin'),
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')
});
} 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'))
});
}
}
);

View File

@@ -42,6 +42,9 @@ const Ar1Wizard = () => {
);
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) => {
@@ -102,6 +105,17 @@ const Ar1Wizard = () => {
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;
@@ -120,11 +134,9 @@ const Ar1Wizard = () => {
};
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')
});
if (authExpiredRef.current) return; // banner unico, niente pile di avvisi (P6)
authExpiredRef.current = true;
setAuthExpired(true);
};
useEffect(() => {
@@ -139,7 +151,7 @@ const Ar1Wizard = () => {
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 });
setDraftNotice(__('Bozza recuperata: ripristinati i dati non salvati della sessione precedente.', 'gepafin'));
}
}
} catch (e) { /* draft corrotto: ignora */ }
@@ -157,6 +169,7 @@ const Ar1Wizard = () => {
const saveQuadro = (quadroId) => {
if (isReadonly) return;
if (authExpiredRef.current) { backupDraft(); return; } // 401 gia noto: solo backup locale
const patch = { [quadroId]: quadriValues[quadroId] || {} };
setSaving(true);
Ar1Service.updateQuadri(formId, patch,
@@ -333,7 +346,7 @@ const Ar1Wizard = () => {
<label htmlFor={`${key}-yes`}>{__('Si', 'gepafin')}</label>
</div>
<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>
</div>
</div>
@@ -475,10 +488,11 @@ const Ar1Wizard = () => {
onSelect={(e) => {
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 });
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([]);
@@ -490,6 +504,22 @@ const Ar1Wizard = () => {
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>