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:
BFLOWS
2026-04-23 10:36:17 +02:00
parent 1116f96acf
commit 46ee801bd0
9 changed files with 1192 additions and 0 deletions

View 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;