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:
BFLOWS
2026-04-18 17:53:04 +02:00
parent fca18de751
commit 381fd64fef
6 changed files with 960 additions and 105 deletions

View File

@@ -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">