feat(v2): FE multi-tranche + custom_checks + manager view
service/rendicontazioneService.js: - startPractice(appId, cb, cb, opts) v2: accetta period_label + copy_ula_from_previous - copyUlaOptions(practiceId): preview ULA tranche N-1 per pre-fill - Custom checks: listCustomChecks, declareCustomCheck (form-data + optional file), deleteCustomCheckDocument, verifyCustomCheck, fetchCustomCheckDocumentBlob - Manager: managerAssignments, managerInstructorsList, reassignInstructor B1 BandoRendicontazioneSchemaEdit.js (editor superadmin): - schemaJsonToForm: estrae gate_rules.max_tranches + custom_checks[] top-level - formToSchemaJson: scrive max_tranches e custom_checks + schema_version=2 - Helpers addCheck/removeCheck/updateCheck (pattern fieldsRepeater esistente) - Sezione 7 'Tranches di rendicontazione': InputNumber max_tranches (1-20) - Sezione 8 'Controlli aggiuntivi': array editable con code (snake_case sanitized), label, description, requires_document, required B2 RendicontazioniMie.js (dashboard benef) — RISCRITTA: - Raggruppamento per application_id con card per bando - Riquadro info cumulativo (cap totale, gia approvato, disponibile, tranches N/M) - Elenco tranche con badge stato + bottoni 'Continua' (DRAFT) / 'Apri' (non editable) - Bottone '+ Nuova rendicontazione' con 4 stati: attivo / disabilitato 'Limite raggiunto' / 'Completa prima' / 'Remissione esaurita' - Dialog avvio: InputText period_label + Checkbox copy_ula (solo se sequence > 1) B3 PraticaRendicontazioneEdit.js (beneficiario): - useMemo customChecksDefs da schema_snapshot.custom_checks - State customChecks + loadCustomChecks useCallback - Sezione 5/4 'Controlli aggiuntivi (dichiarazioni)': per ogni check checkbox 'Dichiaro', badge Obbligatorio/Opzionale/status, upload PDF/JPG/PNG 15MB se requires_document, preview filename+size - Bordo rosso su check obbligatori non dichiarati B4 IstruttoriaPratica.js (istruttore): - State customChecks + loadCustomChecks + ccVerifyDialog - Sezione 'Verifica controlli aggiuntivi' (dopo Verifica documenti): lista con label/codice/badge stato beneficiario/validazione/note istruttore - Azioni: preview, download, thumbs-up (VALIDO toggle), thumbs-down (NON_VALIDO) - Dialog motivazione NON_VALIDO con InputTextarea (min 5 char) B5 IstruttoriaQueue.js (manager): - Toggle 'Coda standard' vs 'Vista manager (riassegnazioni)' visibile solo per ROLE_INSTRUCTOR_MANAGER o ROLE_SUPER_ADMIN - Tabella manager con colonne: Bando/Pratica/Tranche, Stato, Istruttore domanda, Assegnato a (o badge 'Da assegnare' se unassigned), Erogato - Azione 'Riassegna' (o 'Assegna' se unassigned): apre Dialog con Dropdown istruttori (pool pre_instructor + manager) + InputTextarea motivazione - Opzione 'Metti in coda (nessuno)' nel Dropdown per unassign Tutti i file validati via @babel/parser JSX. Webpack compila senza errori (solo warning eslint preesistenti non-B).
This commit is contained in:
@@ -76,6 +76,14 @@ const PraticaRendicontazioneEdit = () => {
|
||||
const [practice, setPractice] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [gate, setGate] = useState(null);
|
||||
const [customChecks, setCustomChecks] = useState([]); // v2: merge schema+values dal BE
|
||||
const loadCustomChecks = useCallback(() => {
|
||||
if (!practiceId) return;
|
||||
RendicontazioneService.listCustomChecks(practiceId,
|
||||
(resp) => setCustomChecks(resp?.data?.custom_checks || []),
|
||||
() => {});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [practiceId]);
|
||||
|
||||
// modal fattura
|
||||
const [invDialog, setInvDialog] = useState({ visible: false, data: null });
|
||||
@@ -155,6 +163,7 @@ const PraticaRendicontazioneEdit = () => {
|
||||
};
|
||||
|
||||
useEffect(() => { load(); }, [load]);
|
||||
useEffect(() => { loadCustomChecks(); }, [loadCustomChecks]);
|
||||
|
||||
const readOnly = practice && practice.status !== 'DRAFT';
|
||||
|
||||
@@ -170,6 +179,10 @@ const PraticaRendicontazioneEdit = () => {
|
||||
const raw = docsSection.required_types || [];
|
||||
return raw.map(r => typeof r === 'string' ? { code: r, label: r } : r);
|
||||
}, [docsSection]);
|
||||
// v2: custom_checks definition + values (state separato con fetch dedicato)
|
||||
const customChecksDefs = useMemo(() => {
|
||||
return practice?.schema_snapshot?.custom_checks || [];
|
||||
}, [practice]);
|
||||
const ivaAllowed = useMemo(() => {
|
||||
const gen = sections.find(x => x.type === 'static_fields');
|
||||
const ivaField = (gen?.fields || []).find(f => f.id === 'iva_regime');
|
||||
@@ -253,6 +266,18 @@ const PraticaRendicontazioneEdit = () => {
|
||||
});
|
||||
};
|
||||
|
||||
// v2: custom_checks
|
||||
const declareCustomCheck = (code, declared, file) => {
|
||||
RendicontazioneService.declareCustomCheck(practiceId, code, declared, file,
|
||||
(resp) => { toast.current?.show({ severity: 'success', summary: __('Controllo aggiornato','gepafin') }); loadCustomChecks(); },
|
||||
onMutationError);
|
||||
};
|
||||
const deleteCustomCheckDoc = (code) => {
|
||||
RendicontazioneService.deleteCustomCheckDocument(practiceId, code,
|
||||
(resp) => { toast.current?.show({ severity: 'success', summary: __('Documento rimosso','gepafin') }); loadCustomChecks(); },
|
||||
onMutationError);
|
||||
};
|
||||
|
||||
// documents
|
||||
const upsertDocument = (docCode, filename) => {
|
||||
RendicontazioneService.upsertDocument(practiceId, docCode, { doc_code: docCode, filename },
|
||||
@@ -617,7 +642,102 @@ const PraticaRendicontazioneEdit = () => {
|
||||
|
||||
<div className="appPage__spacer"></div>
|
||||
|
||||
{/* BOTTOM ACTIONS */}
|
||||
{/* SEZIONE 5: CONTROLLI AGGIUNTIVI (v2) */}
|
||||
{customChecksDefs.length > 0 && (<>
|
||||
<div className="appPageSection">
|
||||
<h2>{__((ulaSection.enabled ? '5.' : '4.') + ' Controlli aggiuntivi (dichiarazioni)', 'gepafin')}</h2>
|
||||
<p className="text-color-secondary" style={{ marginTop: 0 }}>
|
||||
{__('Dichiarazioni richieste dal bando oltre ai documenti standard. I controlli obbligatori devono essere tutti dichiarati prima di poter inviare la pratica.', 'gepafin')}
|
||||
</p>
|
||||
|
||||
<div className="fieldsRepeater">
|
||||
{customChecksDefs.map((def) => {
|
||||
const val = customChecks.find(c => c.code === def.code) || {};
|
||||
const declared = !!val.beneficiary_declared;
|
||||
const hasDoc = !!val.filename_original;
|
||||
const isMissing = def.required && !declared;
|
||||
return (
|
||||
<div key={def.code} className="fieldsRepeater__panel"
|
||||
style={{ border: '1px solid var(--surface-border)', borderRadius: '6px', padding: '1rem',
|
||||
background: isMissing ? 'var(--red-50)' : 'white' }}>
|
||||
<div style={{ display:'flex', alignItems:'flex-start', gap:'0.75rem' }}>
|
||||
<div style={{ flex:'0 0 auto', paddingTop:'4px' }}>
|
||||
<input type="checkbox"
|
||||
checked={declared}
|
||||
disabled={readOnly}
|
||||
onChange={(e) => declareCustomCheck(def.code, e.target.checked, null)}
|
||||
style={{ width: '20px', height: '20px', cursor: readOnly ? 'default' : 'pointer' }} />
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', flexWrap: 'wrap' }}>
|
||||
<strong>{__('Dichiaro', 'gepafin')}: {def.label}</strong>
|
||||
{def.required && (
|
||||
<Tag severity={isMissing ? 'danger' : 'success'} value={isMissing ? __('Obbligatorio', 'gepafin') : __('OK', 'gepafin')} />
|
||||
)}
|
||||
{!def.required && (
|
||||
<Tag severity="info" value={__('Opzionale', 'gepafin')} />
|
||||
)}
|
||||
{val.verification_status && val.verification_status !== 'PENDING' && (
|
||||
<Tag severity={val.verification_status === 'VALIDO' ? 'success' : 'danger'}
|
||||
value={val.verification_status} />
|
||||
)}
|
||||
</div>
|
||||
{def.description && (
|
||||
<div className="text-color-secondary" style={{ fontSize: '0.9em', marginTop: '0.35rem', whiteSpace: 'pre-wrap' }}>
|
||||
{def.description}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{def.requires_document && (
|
||||
<div style={{ marginTop: '0.75rem', padding: '0.6rem', background: 'var(--surface-50)', borderRadius: '4px' }}>
|
||||
{hasDoc ? (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', flexWrap: 'wrap' }}>
|
||||
<i className="pi pi-file-pdf" style={{ color: 'var(--primary-color)' }} />
|
||||
<span style={{ flex: 1, minWidth: '150px' }}>
|
||||
<strong>{val.filename_original}</strong>
|
||||
{val.size_bytes && <small className="text-color-secondary"> ({(val.size_bytes/1024).toFixed(1)} KB)</small>}
|
||||
</span>
|
||||
{!readOnly && (
|
||||
<Button icon="pi pi-trash" severity="danger" outlined size="small"
|
||||
label={__('Rimuovi','gepafin')}
|
||||
onClick={() => deleteCustomCheckDoc(def.code)} />
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
!readOnly && (
|
||||
<div>
|
||||
<small className="text-color-secondary">{__('Allega documento (PDF, JPG, PNG — max 15MB):', 'gepafin')}</small>
|
||||
<input type="file"
|
||||
accept="application/pdf,image/jpeg,image/png"
|
||||
disabled={readOnly}
|
||||
onChange={(e) => {
|
||||
const f = e.target.files?.[0];
|
||||
if (f) declareCustomCheck(def.code, declared, f);
|
||||
e.target.value = '';
|
||||
}}
|
||||
style={{ display: 'block', marginTop: '0.4rem' }} />
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{val.verification_notes && (
|
||||
<div style={{ marginTop: '0.5rem', padding: '0.5rem 0.75rem', background: val.verification_status === 'NON_VALIDO' ? 'var(--red-50)' : 'var(--surface-50)', borderRadius: '4px', fontSize: '0.85em' }}>
|
||||
<strong>{__('Note istruttore', 'gepafin')}:</strong> {val.verification_notes}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="appPage__spacer"></div>
|
||||
</>)}
|
||||
|
||||
{/* BOTTOM ACTIONS */}
|
||||
{!readOnly && (
|
||||
<div className="appPageSection">
|
||||
<div className="appPageSection__actions">
|
||||
|
||||
Reference in New Issue
Block a user