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 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'))
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user