fix(ar1): Ar1Wizard crash su activeQuadro undefined (7 guard)

Bug: TypeError 'Cannot read properties of undefined (reading id)' su Ar1Wizard.js:358
al click Avanti. Causa: activeIndex poteva uscire fuori range quadri.length
(es. dopo re-render con schema_snapshot diverso, o race tra saveQuadro e
setForm+setActiveIndex). Gli onClick/onBlur accedevano a activeQuadro.id
senza controllo null.

Fix:
1. clamp safeIndex = Math.max(0, Math.min(activeIndex, quadri.length - 1))
2. activeQuadro = quadri[safeIndex] (invece di activeIndex diretto)
3. isLastStep usa safeIndex
4. Steps.activeIndex usa safeIndex; onSelect clampa e.index
5. Bottone Indietro: guard 'if (!isReadonly && activeQuadro)' + Math.max(0,...)
6. Bottone Avanti: guard + Math.min(quadri.length-1,...)
7. Card onBlur: guard su activeQuadro
8. submitFinale: return se !activeQuadro, usa activeQuadro invece di quadri[activeIndex]
9. early return se quadri.length === 0 (template senza quadri editabili)

Parse check OK. Webpack compiled 1 warning (vecchio, non nostro).
This commit is contained in:
BFLOWS
2026-04-23 11:11:01 +02:00
parent 7c508e743b
commit dbed5963b2
2 changed files with 60 additions and 212 deletions

View File

@@ -104,9 +104,9 @@ const Ar1Wizard = () => {
};
const submitFinale = () => {
if (!activeQuadro) return;
setSubmitting(true);
const currentQuadro = quadri[activeIndex];
const patch = { [currentQuadro.id]: quadriValues[currentQuadro.id] || {} };
const patch = { [activeQuadro.id]: quadriValues[activeQuadro.id] || {} };
Ar1Service.updateQuadri(formId, patch,
() => {
Ar1Service.submitForSignature(formId,
@@ -315,10 +315,13 @@ const Ar1Wizard = () => {
if (loading) return <div style={{ textAlign: 'center', padding: 40 }}><ProgressSpinner /></div>;
if (!form) return <div style={{ padding: 20 }}><Message severity="error" text={__('Form non trovato', 'gepafin')} /></div>;
if (quadri.length === 0) return <div style={{ padding: 20 }}><Message severity="warn" text={__('Nessun quadro editabile nel template. Contattare il supporto.', 'gepafin')} /></div>;
const steps = quadri.map(q => ({ label: q.id }));
const activeQuadro = quadri[activeIndex];
const isLastStep = activeIndex === quadri.length - 1;
// clamp activeIndex: difensivo se quadri cambia lunghezza o e fuori range
const safeIndex = quadri.length === 0 ? 0 : Math.max(0, Math.min(activeIndex, quadri.length - 1));
const activeQuadro = quadri[safeIndex];
const isLastStep = safeIndex === quadri.length - 1;
return (
<div style={{ padding: 16 }}>
@@ -330,24 +333,25 @@ const Ar1Wizard = () => {
<Steps
model={steps}
activeIndex={activeIndex}
activeIndex={safeIndex}
onSelect={(e) => {
if (!isReadonly && activeQuadro) saveQuadro(activeQuadro.id);
setActiveIndex(e.index);
const next = Math.max(0, Math.min(e.index, quadri.length - 1));
setActiveIndex(next);
}}
readOnly={false}
style={{ marginBottom: 20 }}
/>
<Card style={{ marginBottom: 14 }} onBlur={() => !isReadonly && saveQuadro(activeQuadro.id)}>
<Card style={{ marginBottom: 14 }} onBlur={() => { if (!isReadonly && activeQuadro) saveQuadro(activeQuadro.id); }}>
{activeQuadro && renderQuadro(activeQuadro)}
</Card>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<Button label={__('Indietro', 'gepafin')} icon="pi pi-arrow-left" severity="secondary" outlined disabled={activeIndex === 0}
onClick={() => {
if (!isReadonly) saveQuadro(activeQuadro.id);
setActiveIndex(activeIndex - 1);
if (!isReadonly && activeQuadro) saveQuadro(activeQuadro.id);
setActiveIndex(Math.max(0, activeIndex - 1));
}}
/>
<div style={{ display: 'flex', gap: 8 }}>
@@ -355,8 +359,8 @@ const Ar1Wizard = () => {
{!isLastStep && (
<Button label={__('Avanti', 'gepafin')} icon="pi pi-arrow-right" iconPos="right"
onClick={() => {
if (!isReadonly) saveQuadro(activeQuadro.id);
setActiveIndex(activeIndex + 1);
if (!isReadonly && activeQuadro) saveQuadro(activeQuadro.id);
setActiveIndex(Math.min(quadri.length - 1, activeIndex + 1));
}}
/>
)}

View File

@@ -21,7 +21,6 @@ const buildHeadersMultipart = () => {
const token = storeGet('getToken');
const h = {};
if (token) h['Authorization'] = `Bearer ${token}`;
// niente Content-Type: fetch imposta boundary per multipart/form-data
return h;
};
@@ -40,13 +39,11 @@ const handleError = (err, onError) => {
};
const Ar1Service = {
// ---------- Status pubblico (per compliance modal al login) ----------
// ---------- Status pubblico (per compliance modal) ----------
getStatusForCompany(companyId, onSuccess, onError) {
fetch(`${BASE_URL}/public/ar1-status/${companyId}`, {
method: 'GET', mode: 'cors', headers: buildHeaders()
})
.then(r => handleResponse(r, onSuccess, onError))
.catch(e => handleError(e, onError));
}).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
},
// ---------- CRUD form beneficiario ----------
@@ -54,83 +51,60 @@ const Ar1Service = {
fetch(`${BASE_URL}/api/ar1-forms`, {
method: 'POST', mode: 'cors', headers: buildHeaders(),
body: JSON.stringify({ company_id: companyId, variant })
})
.then(r => handleResponse(r, onSuccess, onError))
.catch(e => handleError(e, onError));
}).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
},
getForm(formId, onSuccess, onError) {
fetch(`${BASE_URL}/api/ar1-forms/${formId}`, {
method: 'GET', mode: 'cors', headers: buildHeaders()
})
.then(r => handleResponse(r, onSuccess, onError))
.catch(e => handleError(e, onError));
}).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
},
listFormsForCompany(companyId, onSuccess, onError) {
fetch(`${BASE_URL}/api/ar1-forms/company/${companyId}`, {
method: 'GET', mode: 'cors', headers: buildHeaders()
})
.then(r => handleResponse(r, onSuccess, onError))
.catch(e => handleError(e, onError));
}).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
},
updateQuadri(formId, quadriPatch, onSuccess, onError) {
fetch(`${BASE_URL}/api/ar1-forms/${formId}/quadri`, {
method: 'PUT', mode: 'cors', headers: buildHeaders(),
body: JSON.stringify({ quadri: quadriPatch })
})
.then(r => handleResponse(r, onSuccess, onError))
.catch(e => handleError(e, onError));
}).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
},
submitForSignature(formId, onSuccess, onError) {
fetch(`${BASE_URL}/api/ar1-forms/${formId}/submit-for-signature`, {
method: 'PUT', mode: 'cors', headers: buildHeaders()
})
.then(r => handleResponse(r, onSuccess, onError))
.catch(e => handleError(e, onError));
}).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
},
deleteForm(formId, onSuccess, onError) {
fetch(`${BASE_URL}/api/ar1-forms/${formId}`, {
method: 'DELETE', mode: 'cors', headers: buildHeaders()
})
.then(r => {
if (r.status === 204) {
if (onSuccess) onSuccess({});
} else {
handleResponse(r, onSuccess, onError);
}
})
.catch(e => handleError(e, onError));
}).then(r => {
if (r.status === 204) { if (onSuccess) onSuccess({}); }
else handleResponse(r, onSuccess, onError);
}).catch(e => handleError(e, onError));
},
// ---------- PDF ----------
generatePdf(formId, onSuccess, onError) {
fetch(`${BASE_URL}/api/ar1-forms/${formId}/generate-pdf`, {
method: 'POST', mode: 'cors', headers: buildHeaders()
})
.then(r => handleResponse(r, onSuccess, onError))
.catch(e => handleError(e, onError));
}).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
},
downloadPdfUnsigned(formId) {
return fetch(`${BASE_URL}/api/ar1-forms/${formId}/pdf-unsigned`, {
method: 'GET', mode: 'cors', headers: buildHeadersMultipart()
}).then(r => {
if (!r.ok) throw new Error(`HTTP ${r.status}`);
return r.blob();
});
}).then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.blob(); });
},
downloadPdfSigned(formId) {
return fetch(`${BASE_URL}/api/ar1-forms/${formId}/pdf-signed`, {
method: 'GET', mode: 'cors', headers: buildHeadersMultipart()
}).then(r => {
if (!r.ok) throw new Error(`HTTP ${r.status}`);
return r.blob();
});
}).then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.blob(); });
},
// ---------- Firma ----------
@@ -140,230 +114,100 @@ const Ar1Service = {
fetch(`${BASE_URL}/api/ar1-forms/${formId}/upload-signature`, {
method: 'POST', mode: 'cors', headers: buildHeadersMultipart(),
body: formData
})
.then(r => handleResponse(r, onSuccess, onError))
.catch(e => handleError(e, onError));
}).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
},
reVerifySignature(formId, onSuccess, onError) {
fetch(`${BASE_URL}/api/ar1-forms/${formId}/verify`, {
method: 'POST', mode: 'cors', headers: buildHeaders()
})
.then(r => handleResponse(r, onSuccess, onError))
.catch(e => handleError(e, onError));
}).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
},
// ---------- ADMIN: Templates ----------
listTemplates(onSuccess, onError, queryStr = '') {
fetch(`${BASE_URL}/admin/ar1-templates${queryStr}`, {
archiveToCompanyDocument(formId, onSuccess, onError) {
fetch(`${BASE_URL}/api/ar1-forms/${formId}/archive-to-company-document`, {
method: 'POST', mode: 'cors', headers: buildHeaders()
}).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
},
// ---------- ADMIN: templates ----------
listTemplates(onSuccess, onError, queryParams) {
const qs = queryParams ? ('?' + new URLSearchParams(queryParams).toString()) : '';
fetch(`${BASE_URL}/admin/ar1-templates${qs}`, {
method: 'GET', mode: 'cors', headers: buildHeaders()
})
.then(r => handleResponse(r, onSuccess, onError))
.catch(e => handleError(e, onError));
}).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
},
getTemplateDetail(templateId, onSuccess, onError) {
fetch(`${BASE_URL}/admin/ar1-templates/${templateId}`, {
method: 'GET', mode: 'cors', headers: buildHeaders()
})
.then(r => handleResponse(r, onSuccess, onError))
.catch(e => handleError(e, onError));
}).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
},
updateTemplateLayout(templateId, layoutConfig, onSuccess, onError) {
fetch(`${BASE_URL}/admin/ar1-templates/${templateId}/layout-config`, {
method: 'PUT', mode: 'cors', headers: buildHeaders(),
body: JSON.stringify({ layout_config: layoutConfig })
})
.then(r => handleResponse(r, onSuccess, onError))
.catch(e => handleError(e, onError));
}).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
},
createNewTemplateVersion(variant, payload, onSuccess, onError) {
fetch(`${BASE_URL}/admin/ar1-templates/${variant}/new-version`, {
method: 'POST', mode: 'cors', headers: buildHeaders(),
body: JSON.stringify(payload)
})
.then(r => handleResponse(r, onSuccess, onError))
.catch(e => handleError(e, onError));
}).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
},
// ---------- ADMIN: Policy ----------
// ---------- ADMIN: policy ----------
getPolicy(onSuccess, onError) {
fetch(`${BASE_URL}/admin/ar1-policy`, {
method: 'GET', mode: 'cors', headers: buildHeaders()
})
.then(r => handleResponse(r, onSuccess, onError))
.catch(e => handleError(e, onError));
}).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
},
updatePolicy(payload, onSuccess, onError) {
updatePolicy(patch, onSuccess, onError) {
fetch(`${BASE_URL}/admin/ar1-policy`, {
method: 'PUT', mode: 'cors', headers: buildHeaders(),
body: JSON.stringify(payload)
})
.then(r => handleResponse(r, onSuccess, onError))
.catch(e => handleError(e, onError));
body: JSON.stringify(patch)
}).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
},
// ---------- ADMIN: PEC Schedule Config (CRUD) ----------
// ---------- ADMIN: pec-schedule-config ----------
listPecSchedule(onSuccess, onError) {
fetch(`${BASE_URL}/admin/ar1-pec-schedule-config`, {
method: 'GET', mode: 'cors', headers: buildHeaders()
})
.then(r => handleResponse(r, onSuccess, onError))
.catch(e => handleError(e, onError));
}).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
},
createPecRule(payload, onSuccess, onError) {
fetch(`${BASE_URL}/admin/ar1-pec-schedule-config`, {
method: 'POST', mode: 'cors', headers: buildHeaders(),
body: JSON.stringify(payload)
})
.then(r => handleResponse(r, onSuccess, onError))
.catch(e => handleError(e, onError));
}).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
},
updatePecRule(ruleId, payload, onSuccess, onError) {
fetch(`${BASE_URL}/admin/ar1-pec-schedule-config/${ruleId}`, {
method: 'PUT', mode: 'cors', headers: buildHeaders(),
body: JSON.stringify(payload)
})
.then(r => handleResponse(r, onSuccess, onError))
.catch(e => handleError(e, onError));
}).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
},
deletePecRule(ruleId, onSuccess, onError) {
fetch(`${BASE_URL}/admin/ar1-pec-schedule-config/${ruleId}`, {
method: 'DELETE', mode: 'cors', headers: buildHeaders()
})
.then(r => {
if (r.status === 204) {
if (onSuccess) onSuccess({});
} else {
handleResponse(r, onSuccess, onError);
}
})
.catch(e => handleError(e, onError));
}).then(r => {
if (r.status === 204) { if (onSuccess) onSuccess({}); }
else handleResponse(r, onSuccess, onError);
}).catch(e => handleError(e, onError));
},
// ---------- ADMIN: Bulk PEC ----------
// ---------- ADMIN: bulk PEC ----------
bulkRequestRecompilation(payload, onSuccess, onError) {
fetch(`${BASE_URL}/admin/ar1-forms/bulk-request-recompilation`, {
method: 'POST', mode: 'cors', headers: buildHeaders(),
body: JSON.stringify(payload)
})
.then(r => handleResponse(r, onSuccess, onError))
.catch(e => handleError(e, onError));
},
// ---------- Archive manuale (di solito automatico) ----------
archiveToCompanyDocument(formId, onSuccess, onError) {
fetch(`${BASE_URL}/api/ar1-forms/${formId}/archive-to-company-document`, {
method: 'POST', mode: 'cors', headers: buildHeaders()
})
.then(r => handleResponse(r, onSuccess, onError))
.catch(e => handleError(e, onError));
}).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
},
};
export default Ar1Service;
// ========== ADMIN METHODS (aggiunti fase admin) ==========
Ar1Service.adminListTemplates = function (onSuccess, onError) {
fetch(`${BASE_URL}/admin/ar1-templates`, {
method: 'GET', mode: 'cors', headers: buildHeaders()
})
.then(r => handleResponse(r, onSuccess, onError))
.catch(e => handleError(e, onError));
};
Ar1Service.adminGetTemplate = function (templateId, onSuccess, onError) {
fetch(`${BASE_URL}/admin/ar1-templates/${templateId}`, {
method: 'GET', mode: 'cors', headers: buildHeaders()
})
.then(r => handleResponse(r, onSuccess, onError))
.catch(e => handleError(e, onError));
};
Ar1Service.adminUpdateLayoutConfig = function (templateId, layoutConfig, onSuccess, onError) {
fetch(`${BASE_URL}/admin/ar1-templates/${templateId}/layout-config`, {
method: 'PUT', mode: 'cors', headers: buildHeaders(),
body: JSON.stringify({ layout_config: layoutConfig })
})
.then(r => handleResponse(r, onSuccess, onError))
.catch(e => handleError(e, onError));
};
Ar1Service.adminNewVersion = function (variant, payload, onSuccess, onError) {
fetch(`${BASE_URL}/admin/ar1-templates/${variant}/new-version`, {
method: 'POST', mode: 'cors', headers: buildHeaders(),
body: JSON.stringify(payload)
})
.then(r => handleResponse(r, onSuccess, onError))
.catch(e => handleError(e, onError));
};
Ar1Service.adminGetPolicy = function (onSuccess, onError) {
fetch(`${BASE_URL}/admin/ar1-policy`, {
method: 'GET', mode: 'cors', headers: buildHeaders()
})
.then(r => handleResponse(r, onSuccess, onError))
.catch(e => handleError(e, onError));
};
Ar1Service.adminUpdatePolicy = function (payload, onSuccess, onError) {
fetch(`${BASE_URL}/admin/ar1-policy`, {
method: 'PUT', mode: 'cors', headers: buildHeaders(),
body: JSON.stringify(payload)
})
.then(r => handleResponse(r, onSuccess, onError))
.catch(e => handleError(e, onError));
};
Ar1Service.adminListPecSchedule = function (onSuccess, onError) {
fetch(`${BASE_URL}/admin/ar1-pec-schedule-config`, {
method: 'GET', mode: 'cors', headers: buildHeaders()
})
.then(r => handleResponse(r, onSuccess, onError))
.catch(e => handleError(e, onError));
};
Ar1Service.adminCreatePecRule = function (payload, onSuccess, onError) {
fetch(`${BASE_URL}/admin/ar1-pec-schedule-config`, {
method: 'POST', mode: 'cors', headers: buildHeaders(),
body: JSON.stringify(payload)
})
.then(r => handleResponse(r, onSuccess, onError))
.catch(e => handleError(e, onError));
};
Ar1Service.adminUpdatePecRule = function (ruleId, payload, onSuccess, onError) {
fetch(`${BASE_URL}/admin/ar1-pec-schedule-config/${ruleId}`, {
method: 'PUT', mode: 'cors', headers: buildHeaders(),
body: JSON.stringify(payload)
})
.then(r => handleResponse(r, onSuccess, onError))
.catch(e => handleError(e, onError));
};
Ar1Service.adminDeletePecRule = function (ruleId, onSuccess, onError) {
fetch(`${BASE_URL}/admin/ar1-pec-schedule-config/${ruleId}`, {
method: 'DELETE', mode: 'cors', headers: buildHeaders()
})
.then(r => {
if (r.status === 204) { if (onSuccess) onSuccess({}); }
else handleResponse(r, onSuccess, onError);
})
.catch(e => handleError(e, onError));
};
Ar1Service.adminBulkRecompilation = function (payload, onSuccess, onError) {
fetch(`${BASE_URL}/admin/ar1-forms/bulk-request-recompilation`, {
method: 'POST', mode: 'cors', headers: buildHeaders(),
body: JSON.stringify(payload)
})
.then(r => handleResponse(r, onSuccess, onError))
.catch(e => handleError(e, onError));
};