feat(ar1): modulo Dichiarazione AR1 Adeguata Verifica D.Lgs.231/2007
Nuovo modulo FE speculare a rendicontazione. Integrazione con microservizio
ar1-compiler (AX41:18091, 26 endpoint live, JWT HS512 condiviso con GEPAFIN-BE).
FILE CREATI (1159 LOC):
src/modules/ar1/service/ar1Service.js (166 LOC)
Client HTTP pattern 1:1 da rendicontazioneService.js. Metodi:
- getStatusForCompany (pubblico, per compliance modal)
- createDraft / getForm / listFormsForCompany / updateQuadri
- submitForSignature / deleteForm
- generatePdf / downloadPdfUnsigned / downloadPdfSigned
- uploadSignature (multipart) / reVerifySignature
- archiveToCompanyDocument (manuale, solitamente auto)
src/modules/ar1/components/Ar1StatusTag.js (26 LOC)
Badge PrimeReact Tag per 9 stati (MISSING/DRAFT/AWAITING_SIGNATURE/SIGNED/
VERIFIED/VALID/APPROACHING/EXPIRED/SUPERSEDED) con severity+icon specifici.
src/modules/ar1/components/Ar1ComplianceModal.js (137 LOC)
Dialog al login se azienda ha AR1 MISSING/EXPIRED (bloccante, no dismiss)
o APPROACHING (dismissable 24h via sessionStorage). CTA 'Compila ora'
naviga a /ar1. Da montare nel layout principale con <Ar1ComplianceModal
companyId={userCompanyId} />.
src/modules/ar1/pages/Ar1Home.js (248 LOC)
Pagina principale beneficiario. Card status con countdown + CTA dinamici
(Compila/Riprendi/Firma/Rinnova). DataTable storico con azioni per riga
(riprendi, firma, elimina, scarica firmato). Dialog scelta variante per
nuovo form (A1/A2/A3).
src/modules/ar1/pages/Ar1Wizard.js (372 LOC)
Wizard data-driven: legge schema_snapshot del form e genera step/field
dinamicamente. Un step PrimeReact Steps per ogni quadro. Auto-save onBlur
via PUT /quadri. 7 renderer type-aware:
- text/email (uppercase CF regex)
- textarea
- date (Calendar it-IT)
- checkbox
- radio (opzioni string o {label,value})
- enum (Dropdown)
- yes_no_with_note (RadioButton SI/NO + textarea condizionale)
Handler row_type per Quadro B titolari effettivi (array fino a max_rows).
Handler upload_slots per Quadro F allegati. Nested_full per Quadro C LR
e D esecutore con sezione 'Dettaglio aggiuntivo'.
Solo DRAFT editabile, AWAITING_SIGNATURE+ in sola lettura.
Submit finale invia PUT /quadri + PUT /submit-for-signature e naviga
a /ar1/signature/:id.
src/modules/ar1/pages/Ar1Signature.js (210 LOC)
Pagina firma:
Step 1: genera PDF + download unsigned (filename AR1_A1_da-firmare.pdf)
Step 2: FileUpload PDF firmato (.pdf PAdES o .p7m CAdES, 50MB max)
→ DocVerify call (toast 'Verifica in corso, fino a 60s')
→ 4 outcome con toast specifici:
VERIFIED → success + redirect Home
SIGNED_NOT_VERIFIED → warn 'verifica manuale'
SIGNED_DOCVERIFY_UNAVAILABLE → warn 'DocVerify down'
NO_SIGNATURE_DETECTED → error 'Firmare prima il PDF'
Card 'Dettagli verifica' con firmatario/CF/metodo/scadenza se VERIFIED.
INTEGRAZIONE (pattern identico a rendicontazione):
src/layouts/DefaultLayout/components/AppSidebar/index.js
Aggiunta voce sidebar:
label: 'Dichiarazione AR1', icon: 'pi pi-id-card', href: '/ar1', id: 22,
enable: intersection(permissions, ['APPLY_CALLS', 'APPLY_CONFIDI_CALLS']).length
src/routes.js
Import Ar1Home/Ar1Wizard/Ar1Signature.
3 route con pattern ruoli:
/ar1 → BENEFICIARY/SUPER_ADMIN: Ar1Home, altri: PageNotFound
/ar1/wizard/:formId → BENEFICIARY/SUPER_ADMIN: Ar1Wizard
/ar1/signature/:formId → BENEFICIARY/SUPER_ADMIN: Ar1Signature
.env
+ REACT_APP_AR1_API_URL=http://78.46.41.91:18091
+ REACT_APP_RENDICONTAZIONE_API_URL=http://78.46.41.91:18090
VALIDAZIONE:
8 file @babel/parser parse-check con plugin JSX: 8 OK / 0 FAIL.
PROSSIMI STEP (non in questo commit):
- Rinaldo integra Ar1ComplianceModal nel layout principale post-login
- Rinaldo deploya DocVerify sul server BFLOWS/Gepafin e configura
AR1_DOCVERIFY_URL nel microservizio ar1-compiler (senza DocVerify,
degrada gracefully a SIGNED senza VERIFIED)
- BE Spring Ar1AmendmentPoller (4.5h, bundle in /tmp/rinaldo-bundle-ar1.zip)
This commit is contained in:
166
src/modules/ar1/service/ar1Service.js
Normal file
166
src/modules/ar1/service/ar1Service.js
Normal file
@@ -0,0 +1,166 @@
|
||||
/**
|
||||
* Client HTTP per ar1-compiler (microservizio BFLOWS).
|
||||
* Il microservizio valida lo stesso JWT di GEPAFIN-BE (HS512 shared secret).
|
||||
*
|
||||
* Env var: REACT_APP_AR1_API_URL (es. http://78.46.41.91:18091)
|
||||
*
|
||||
* Pattern replicato 1:1 da rendicontazioneService.js.
|
||||
*/
|
||||
import { storeGet } from '../../../store';
|
||||
|
||||
const BASE_URL = process.env.REACT_APP_AR1_API_URL || '';
|
||||
|
||||
const buildHeaders = () => {
|
||||
const token = storeGet('getToken');
|
||||
const h = { 'Content-Type': 'application/json' };
|
||||
if (token) h['Authorization'] = `Bearer ${token}`;
|
||||
return h;
|
||||
};
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
const handleResponse = async (response, onSuccess, onError) => {
|
||||
let body = null;
|
||||
try { body = await response.json(); } catch (e) { body = { detail: response.statusText }; }
|
||||
if (response.status >= 200 && response.status < 300) {
|
||||
if (onSuccess) onSuccess(body);
|
||||
} else {
|
||||
if (onError) onError({ status: response.status, ...body });
|
||||
}
|
||||
};
|
||||
|
||||
const handleError = (err, onError) => {
|
||||
if (onError) onError({ status: 0, detail: err.message });
|
||||
};
|
||||
|
||||
const Ar1Service = {
|
||||
// ---------- Status pubblico (per compliance modal al login) ----------
|
||||
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));
|
||||
},
|
||||
|
||||
// ---------- CRUD form beneficiario ----------
|
||||
createDraft(companyId, variant, onSuccess, onError) {
|
||||
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));
|
||||
},
|
||||
|
||||
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));
|
||||
},
|
||||
|
||||
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));
|
||||
},
|
||||
|
||||
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));
|
||||
},
|
||||
|
||||
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));
|
||||
},
|
||||
|
||||
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));
|
||||
},
|
||||
|
||||
// ---------- 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));
|
||||
},
|
||||
|
||||
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();
|
||||
});
|
||||
},
|
||||
|
||||
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();
|
||||
});
|
||||
},
|
||||
|
||||
// ---------- Firma ----------
|
||||
uploadSignature(formId, fileObject, onSuccess, onError) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', fileObject);
|
||||
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));
|
||||
},
|
||||
|
||||
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));
|
||||
},
|
||||
|
||||
// ---------- 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));
|
||||
},
|
||||
};
|
||||
|
||||
export default Ar1Service;
|
||||
Reference in New Issue
Block a user