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:
@@ -88,7 +88,16 @@ const schemaJsonToForm = (j) => {
|
||||
cap_absolute: gate.cap_absolute ?? 12500,
|
||||
require_invoice_per_category: gate.require_at_least_one_invoice_per_nonzero_category ?? true,
|
||||
require_ula_above_threshold: gate.require_ula_above_threshold ?? true,
|
||||
require_all_documents_resolved: gate.require_all_documents_resolved ?? true
|
||||
require_all_documents_resolved: gate.require_all_documents_resolved ?? true,
|
||||
// v2 multi-tranche + custom_checks
|
||||
max_tranches: gate.max_tranches ?? 1,
|
||||
custom_checks: (j.custom_checks || []).map(cc => ({
|
||||
code: cc.code || '',
|
||||
label: cc.label || '',
|
||||
description: cc.description || '',
|
||||
requires_document: !!cc.requires_document,
|
||||
required: !!cc.required,
|
||||
}))
|
||||
};
|
||||
};
|
||||
|
||||
@@ -138,8 +147,17 @@ const formToSchemaJson = (f, base = null) => {
|
||||
amount_basis: f.amount_basis,
|
||||
require_at_least_one_invoice_per_nonzero_category: f.require_invoice_per_category,
|
||||
require_ula_above_threshold: f.require_ula_above_threshold,
|
||||
require_all_documents_resolved: f.require_all_documents_resolved
|
||||
}
|
||||
require_all_documents_resolved: f.require_all_documents_resolved,
|
||||
max_tranches: f.max_tranches || 1
|
||||
},
|
||||
custom_checks: (f.custom_checks || []).map(cc => ({
|
||||
code: cc.code,
|
||||
label: cc.label,
|
||||
description: cc.description,
|
||||
requires_document: !!cc.requires_document,
|
||||
required: !!cc.required,
|
||||
})),
|
||||
schema_version: 2
|
||||
};
|
||||
};
|
||||
|
||||
@@ -215,6 +233,20 @@ const BandoRendicontazioneSchemaEdit = () => {
|
||||
setDirty(true);
|
||||
};
|
||||
|
||||
// v2 custom_checks
|
||||
const updateCheck = (idx, patch) => {
|
||||
setForm(p => ({ ...p, custom_checks: p.custom_checks.map((c,i) => i===idx ? {...c, ...patch} : c) }));
|
||||
setDirty(true);
|
||||
};
|
||||
const addCheck = () => {
|
||||
setForm(p => ({ ...p, custom_checks: [...(p.custom_checks || []), { code:'', label:'', description:'', requires_document:false, required:false }] }));
|
||||
setDirty(true);
|
||||
};
|
||||
const removeCheck = (idx) => {
|
||||
setForm(p => ({ ...p, custom_checks: p.custom_checks.filter((_,i) => i!==idx) }));
|
||||
setDirty(true);
|
||||
};
|
||||
|
||||
// ---------- actions ----------
|
||||
const handleInitializeRestart = (e) => {
|
||||
confirmPopup({
|
||||
@@ -594,6 +626,100 @@ const BandoRendicontazioneSchemaEdit = () => {
|
||||
|
||||
<div className="appPage__spacer"></div>
|
||||
|
||||
{/* 7 - TRANCHES + CUSTOM CHECKS (v2) */}
|
||||
<div className="appPageSection">
|
||||
<h2>{__('7. Tranches di rendicontazione','gepafin')}</h2>
|
||||
<p style={{ color:'var(--text-color-secondary)', marginTop: 0 }}>
|
||||
{__('Numero massimo di tranche che il beneficiario puo aprire per questo bando. Il default 1 mantiene il comportamento classico a rendicontazione unica. Aumenta il numero per permettere rendicontazioni multi-fase (es. stati di avanzamento).','gepafin')}
|
||||
</p>
|
||||
<div className="appForm__field" style={{maxWidth:'300px'}}>
|
||||
<label>{__('Tranches massime','gepafin')}</label>
|
||||
<InputNumber value={form.max_tranches}
|
||||
onValueChange={(e) => update({max_tranches: e.value})}
|
||||
min={1} max={20} showButtons disabled={readOnly} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="appPage__spacer"></div>
|
||||
|
||||
<div className="appPageSection">
|
||||
<h2>{__('8. Controlli aggiuntivi (dichiarazioni beneficiario)','gepafin')} <span style={{fontWeight:400, color:'var(--text-color-secondary)', fontSize:'0.9em'}}>({(form.custom_checks || []).length})</span></h2>
|
||||
<p style={{ color:'var(--text-color-secondary)', marginTop: 0 }}>
|
||||
{__('Dichiarazioni aggiuntive richieste al beneficiario, oltre ai documenti standard. Ogni controllo puo richiedere o meno un documento allegato e puo essere obbligatorio o opzionale. Esempi: dichiarazione antiriciclaggio (senza doc, obbligatoria), polizza fidejussoria (con doc, opzionale).','gepafin')}
|
||||
</p>
|
||||
<div className="fieldsRepeater">
|
||||
{(form.custom_checks || []).map((c, i) => (
|
||||
<div key={i} className="fieldsRepeater__panel" style={{ padding:'1rem', border:'1px solid var(--surface-border)', borderRadius:'6px', background:'var(--surface-50)' }}>
|
||||
<div className="fieldsRepeater__heading" style={{ marginBottom:'0.5rem' }}>
|
||||
<strong style={{ color:'var(--primary-color)' }}>{c.code || `check #${i+1}`} — {c.label || __('(senza etichetta)','gepafin')}</strong>
|
||||
{!readOnly && (
|
||||
<Button type="button" icon="pi pi-trash" severity="danger" outlined
|
||||
size="small" onClick={() => removeCheck(i)}
|
||||
tooltip={__('Rimuovi controllo','gepafin')} tooltipOptions={{position:'top'}} />
|
||||
)}
|
||||
</div>
|
||||
<div className="appForm__cols">
|
||||
<div className="appForm__field">
|
||||
<label>{__('Codice (snake_case)','gepafin')}</label>
|
||||
<InputText value={c.code}
|
||||
onChange={(e) => updateCheck(i,{code:e.target.value.toLowerCase().replace(/[^a-z0-9_]/g,'_')})}
|
||||
placeholder="antiriciclaggio" disabled={readOnly} />
|
||||
</div>
|
||||
<div className="appForm__field">
|
||||
<label>{__('Etichetta visibile','gepafin')}</label>
|
||||
<InputText value={c.label}
|
||||
onChange={(e) => updateCheck(i,{label:e.target.value})}
|
||||
placeholder={__('Dichiarazione antiriciclaggio','gepafin')} disabled={readOnly} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="appForm__field">
|
||||
<label>{__('Descrizione (testo mostrato al beneficiario)','gepafin')}</label>
|
||||
<InputTextarea value={c.description}
|
||||
onChange={(e) => updateCheck(i,{description:e.target.value})}
|
||||
rows={3} autoResize disabled={readOnly}
|
||||
placeholder={__('Dichiaro che il beneficiario rispetta...','gepafin')} />
|
||||
</div>
|
||||
<div className="appForm__cols">
|
||||
<div className="appForm__field">
|
||||
<div className="appForm__row">
|
||||
<InputSwitch checked={c.requires_document}
|
||||
onChange={(e) => updateCheck(i,{requires_document:e.value})} disabled={readOnly} />
|
||||
<label style={{ cursor: 'pointer' }}
|
||||
onClick={() => !readOnly && updateCheck(i,{requires_document: !c.requires_document})}>
|
||||
{__('Richiede documento allegato','gepafin')}
|
||||
</label>
|
||||
</div>
|
||||
<small className="text-color-secondary">
|
||||
{__("Se attivo, il beneficiario puo allegare un PDF (max 15MB).",'gepafin')}
|
||||
</small>
|
||||
</div>
|
||||
<div className="appForm__field">
|
||||
<div className="appForm__row">
|
||||
<InputSwitch checked={c.required}
|
||||
onChange={(e) => updateCheck(i,{required:e.value})} disabled={readOnly} />
|
||||
<label style={{ cursor: 'pointer' }}
|
||||
onClick={() => !readOnly && updateCheck(i,{required: !c.required})}>
|
||||
{__('Obbligatorio','gepafin')}
|
||||
</label>
|
||||
</div>
|
||||
<small className="text-color-secondary">
|
||||
{__("Se attivo, il beneficiario deve dichiararlo prima di poter inviare la pratica.",'gepafin')}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{!readOnly && (
|
||||
<div style={{ marginTop: '1rem' }}>
|
||||
<Button type="button" icon="pi pi-plus" iconPos="right" outlined
|
||||
label={__('Aggiungi controllo aggiuntivo','gepafin')} onClick={addCheck} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="appPage__spacer"></div>
|
||||
|
||||
{/* ACTIONS BOTTOM (copia degli action top per comodità) */}
|
||||
{!isPublished && (
|
||||
<div className="appPageSection">
|
||||
|
||||
Reference in New Issue
Block a user