From 9c483ade340895c70eca15b0bc97626b9e632c6b Mon Sep 17 00:00:00 2001 From: BFLOWS Sandbox Date: Sat, 18 Apr 2026 09:50:53 +0200 Subject: [PATCH] feat(rendicontazione): lato beneficiario - lista pratiche + compilazione + submit - Nuova pagina RendicontazioniMie: dashboard beneficiario con pratiche esistenti + applications CONTRACT_SIGNED ready_to_start in tabella unificata - Nuova pagina PraticaRendicontazioneEdit: form compilazione completo + riepilogo finanziario live (erogato, totale, cap, remissione spettante) + requisiti per invio con semafori live (gate check refresh on mount) + sezione regime IVA con update inline + fatture per categoria con dialog add + tabella + delete (per B1/B2/B3) + dipendenti ULA con dialog add (CF, contratto, FTE, periodo, allegato) + documenti richiesti con upload simulato (prompt nome file) + submit con confermazione, disabilitato finche' gate non passa - Nuova pagina DevSwitchUser: impersonate sandbox-only per superadmin - Voce sidebar "Le mie rendicontazioni" per ROLE_BENEFICIARY - Voce sidebar "Dev: cambia utente" per ROLE_SUPER_ADMIN - Service esteso con 12 metodi pratiche + impersonate --- .../components/AppSidebar/index.js | 14 + .../rendicontazione/pages/DevSwitchUser.js | 83 +++ .../pages/PraticaRendicontazioneEdit.js | 641 ++++++++++++++++++ .../pages/RendicontazioniMie.js | 133 ++++ .../service/rendicontazioneService.js | 92 +++ src/routes.js | 20 + 6 files changed, 983 insertions(+) create mode 100644 src/modules/rendicontazione/pages/DevSwitchUser.js create mode 100644 src/modules/rendicontazione/pages/PraticaRendicontazioneEdit.js create mode 100644 src/modules/rendicontazione/pages/RendicontazioniMie.js diff --git a/src/layouts/DefaultLayout/components/AppSidebar/index.js b/src/layouts/DefaultLayout/components/AppSidebar/index.js index 28840a8..47f69c8 100644 --- a/src/layouts/DefaultLayout/components/AppSidebar/index.js +++ b/src/layouts/DefaultLayout/components/AppSidebar/index.js @@ -34,6 +34,13 @@ const AppSidebar = () => { id: 21, enable: intersection(permissions, ['MANAGE_TENDERS']).length }, + { + label: __('Dev: cambia utente', 'gepafin'), + icon: 'pi pi-user-edit', + href: '/dev-switch-user', + id: 99, + enable: intersection(permissions, ['MANAGE_USERS']).length + }, { label: __('Domande in lavorazione', 'gepafin'), icon: 'pi pi-file', @@ -90,6 +97,13 @@ const AppSidebar = () => { id: 10, enable: intersection(permissions, ['APPLY_CALLS', 'APPLY_CONFIDI_CALLS']).length }, + { + label: __('Le mie rendicontazioni', 'gepafin'), + icon: 'pi pi-receipt', + href: '/rendicontazioni', + id: 11, + enable: intersection(permissions, ['APPLY_CALLS', 'APPLY_CONFIDI_CALLS']).length + }, { label: __('Archivio domande', 'gepafin'), icon: 'pi pi-briefcase', diff --git a/src/modules/rendicontazione/pages/DevSwitchUser.js b/src/modules/rendicontazione/pages/DevSwitchUser.js new file mode 100644 index 0000000..a75153c --- /dev/null +++ b/src/modules/rendicontazione/pages/DevSwitchUser.js @@ -0,0 +1,83 @@ +import React, { useState, useRef } from 'react'; +import { __ } from '@wordpress/i18n'; +import { useNavigate } from 'react-router-dom'; + +import { Card } from 'primereact/card'; +import { Button } from 'primereact/button'; +import { InputText } from 'primereact/inputtext'; +import { Toast } from 'primereact/toast'; + +import { storeSet } from '../../../store'; +import RendicontazioneService from '../service/rendicontazioneService'; + + +/** + * Pagina sandbox: permette al superadmin di impersonare un altro utente + * (tipicamente beneficiario) senza passare per SPID. Solo per sviluppo. + */ +const DevSwitchUser = () => { + const navigate = useNavigate(); + const toast = useRef(null); + const [email, setEmail] = useState('beneficiario@sandbox.local'); + const [loading, setLoading] = useState(false); + + const doImpersonate = () => { + setLoading(true); + RendicontazioneService.impersonate(email, + (resp) => { + const data = resp?.data; + if (!data?.token) { + toast.current?.show({ severity: 'error', summary: __('Risposta vuota', 'gepafin') }); + setLoading(false); + return; + } + // popola lo store Zustand come dopo il login + storeSet('setAuthData', { + token: data.token, + userData: data.user + }); + toast.current?.show({ severity: 'success', summary: __('Ora sei ', 'gepafin') + data.user.email }); + // aspetta un tick e ricarica a root + setTimeout(() => window.location.replace('/'), 700); + }, + (err) => { + toast.current?.show({ severity: 'error', summary: __('Impersonate fallito', 'gepafin'), detail: err?.detail }); + setLoading(false); + } + ); + }; + + return ( +
+ +
+

{__('Dev: cambia utente', 'gepafin')}

+

{__('Pagina sandbox. Permette di impersonare un utente (es. beneficiario) senza passare per SPID.', 'gepafin')}

+
+ +
+ +
+ +
{ e.preventDefault(); doImpersonate(); }}> +
+ + setEmail(e.target.value)} /> + + {__('Prova: beneficiario@sandbox.local oppure admin@sandbox.local', 'gepafin')} + +
+
+
+
+
+
+
+ ); +}; + +export default DevSwitchUser; diff --git a/src/modules/rendicontazione/pages/PraticaRendicontazioneEdit.js b/src/modules/rendicontazione/pages/PraticaRendicontazioneEdit.js new file mode 100644 index 0000000..2461700 --- /dev/null +++ b/src/modules/rendicontazione/pages/PraticaRendicontazioneEdit.js @@ -0,0 +1,641 @@ +import React, { useEffect, useState, useRef, useMemo, useCallback } from 'react'; +import { __ } from '@wordpress/i18n'; +import { useNavigate, useParams } from 'react-router-dom'; + +// components +import { Button } from 'primereact/button'; +import { Toast } from 'primereact/toast'; +import { Tag } from 'primereact/tag'; +import { Skeleton } from 'primereact/skeleton'; +import { ConfirmPopup, confirmPopup } from 'primereact/confirmpopup'; +import { Dialog } from 'primereact/dialog'; +import { InputText } from 'primereact/inputtext'; +import { InputNumber } from 'primereact/inputnumber'; +import { Dropdown } from 'primereact/dropdown'; +import { Calendar } from 'primereact/calendar'; +import { InputTextarea } from 'primereact/inputtextarea'; +import { DataTable } from 'primereact/datatable'; +import { Column } from 'primereact/column'; + +// api +import RendicontazioneService from '../service/rendicontazioneService'; + +// ---------- costanti ---------- +const IVA_REGIME_LABELS = { + ORDINARIO: 'Ordinario (IVA non rendicontabile)', + FORFETTARIO: 'Forfettario (IVA rendicontabile)', + ESENTE: 'Esente' +}; + +const CONTRACT_TYPES = [ + { value: 'T_IND', label: 'Tempo indeterminato' }, + { value: 'T_DET', label: 'Tempo determinato' }, + { value: 'APPR', label: 'Apprendistato' }, + { value: 'STAGE', label: 'Tirocinio / Stage' }, + { value: 'COLL', label: 'Collaborazione coordinata' }, + { value: 'ALTRO', label: 'Altro' } +]; + +const STATUS_TAGS = { + DRAFT: { severity: 'warning', label: 'In compilazione' }, + SUBMITTED: { severity: 'info', label: 'Inviata' }, + UNDER_REVIEW: { severity: 'info', label: 'In valutazione' }, + APPROVED: { severity: 'success', label: 'Approvata' }, + REJECTED: { severity: 'danger', label: 'Respinta' }, + AWAITING_AMENDMENT: { severity: 'warning', label: 'Soccorso istruttorio' } +}; + +const euro = (v) => '€ ' + Number(v || 0).toLocaleString('it-IT', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); +const formatDate = (d) => d ? new Date(d).toLocaleDateString('it-IT') : '—'; + +// empty invoice/employee templates +const emptyInvoice = (catCode) => ({ + category_code: catCode || '', + invoice_number: '', invoice_date: null, payment_date: null, + supplier_name: '', supplier_vat: '', + description: '', taxable: null, vat: 0, total: null, + pdf_filename: '' +}); + +const emptyEmployee = () => ({ + codice_fiscale: '', full_name: '', + contract_type: 'T_IND', role_description: '', + fte_pct: 1.0, + period_start_date: null, period_end_date: null, + supporting_doc_type: 'LUL', supporting_doc_filename: '' +}); + + +const PraticaRendicontazioneEdit = () => { + const { id: practiceId } = useParams(); + const navigate = useNavigate(); + const toast = useRef(null); + + const [practice, setPractice] = useState(null); + const [loading, setLoading] = useState(true); + const [gate, setGate] = useState(null); + + // modal fattura + const [invDialog, setInvDialog] = useState({ visible: false, data: null }); + // modal dipendente ULA + const [empDialog, setEmpDialog] = useState({ visible: false, data: null }); + + // ---------- load ---------- + const load = useCallback(() => { + setLoading(true); + RendicontazioneService.getPractice(practiceId, + (resp) => { setPractice(resp?.data); setLoading(false); refreshGate(resp?.data); }, + (err) => { + toast.current?.show({ severity: 'error', summary: __('Errore caricamento', 'gepafin'), detail: err?.detail }); + setLoading(false); + } + ); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [practiceId]); + + const refreshGate = (p) => { + RendicontazioneService.gateCheck(practiceId, + (resp) => setGate(resp?.data), + () => setGate(null)); + }; + + useEffect(() => { load(); }, [load]); + + const readOnly = practice && practice.status !== 'DRAFT'; + + // ---------- derived ---------- + const sections = practice?.schema_snapshot?.sections || []; + const categories = useMemo(() => { + const s = sections.find(x => x.type === 'category_grid') || {}; + return s.categories || []; + }, [sections]); + const ulaSection = useMemo(() => sections.find(x => x.type === 'ula_block') || {}, [sections]); + const docsSection = useMemo(() => sections.find(x => x.type === 'document_checklist') || {}, [sections]); + const docsRequired = useMemo(() => { + const raw = docsSection.required_types || []; + return raw.map(r => typeof r === 'string' ? { code: r, label: r } : r); + }, [docsSection]); + const ivaAllowed = useMemo(() => { + const gen = sections.find(x => x.type === 'static_fields'); + const ivaField = (gen?.fields || []).find(f => f.id === 'iva_regime'); + const opts = ivaField?.options || []; + return opts.map(o => (typeof o === 'string' ? { value: o, label: IVA_REGIME_LABELS[o] || o } + : { value: o.value, label: IVA_REGIME_LABELS[o.value] || o.label || o.value })); + }, [sections]); + + // ---------- actions ---------- + const afterMutation = (successMsg) => (resp) => { + toast.current?.show({ severity: 'success', summary: successMsg }); + load(); + }; + const onMutationError = (err) => { + toast.current?.show({ severity: 'error', summary: __('Operazione fallita', 'gepafin'), detail: err?.detail || JSON.stringify(err?.message || err) }); + }; + + const updateIvaRegime = (regime) => { + RendicontazioneService.updatePractice(practiceId, { iva_regime: regime }, + afterMutation(__('Regime IVA aggiornato', 'gepafin')), onMutationError); + }; + + // invoices + const openAddInvoice = (catCode) => setInvDialog({ visible: true, data: emptyInvoice(catCode) }); + const saveInvoice = () => { + const d = invDialog.data; + // validazione minima + if (!d.invoice_number || !d.invoice_date || !d.payment_date || !d.supplier_name || + !d.supplier_vat || !d.description || d.taxable == null || d.total == null) { + toast.current?.show({ severity: 'warn', summary: __('Campi obbligatori mancanti', 'gepafin'), detail: __('Compila tutti i campi della fattura.', 'gepafin') }); + return; + } + const payload = { + ...d, + invoice_date: typeof d.invoice_date === 'string' ? d.invoice_date : d.invoice_date.toISOString().slice(0, 10), + payment_date: typeof d.payment_date === 'string' ? d.payment_date : d.payment_date.toISOString().slice(0, 10) + }; + RendicontazioneService.addInvoice(practiceId, payload, + (resp) => { setInvDialog({ visible: false, data: null }); afterMutation(__('Fattura aggiunta', 'gepafin'))(resp); }, + onMutationError); + }; + const deleteInvoice = (e, inv) => { + confirmPopup({ + target: e.currentTarget, + message: __('Rimuovere questa fattura?', 'gepafin'), + icon: 'pi pi-exclamation-triangle', + acceptLabel: __('Rimuovi', 'gepafin'), rejectLabel: __('Annulla', 'gepafin'), + acceptClassName: 'p-button-danger', + accept: () => RendicontazioneService.deleteInvoice(practiceId, inv.id, + afterMutation(__('Fattura rimossa', 'gepafin')), onMutationError) + }); + }; + + // ula + const openAddEmployee = () => setEmpDialog({ visible: true, data: emptyEmployee() }); + const saveEmployee = () => { + const d = empDialog.data; + if (!d.codice_fiscale || !d.full_name || !d.contract_type || + !d.period_start_date || !d.period_end_date || d.fte_pct == null) { + toast.current?.show({ severity: 'warn', summary: __('Campi obbligatori mancanti', 'gepafin') }); + return; + } + const payload = { + ...d, + period_start_date: typeof d.period_start_date === 'string' ? d.period_start_date : d.period_start_date.toISOString().slice(0, 10), + period_end_date: typeof d.period_end_date === 'string' ? d.period_end_date : d.period_end_date.toISOString().slice(0, 10) + }; + RendicontazioneService.addUlaEmployee(practiceId, payload, + (resp) => { setEmpDialog({ visible: false, data: null }); afterMutation(__('Dipendente aggiunto', 'gepafin'))(resp); }, + onMutationError); + }; + const deleteEmployee = (e, emp) => { + confirmPopup({ + target: e.currentTarget, + message: __('Rimuovere questo dipendente?', 'gepafin'), + icon: 'pi pi-exclamation-triangle', + acceptLabel: __('Rimuovi', 'gepafin'), rejectLabel: __('Annulla', 'gepafin'), + acceptClassName: 'p-button-danger', + accept: () => RendicontazioneService.deleteUlaEmployee(practiceId, emp.id, + afterMutation(__('Dipendente rimosso', 'gepafin')), onMutationError) + }); + }; + + // documents + const upsertDocument = (docCode, filename) => { + RendicontazioneService.upsertDocument(practiceId, docCode, { doc_code: docCode, filename }, + afterMutation(__('Documento aggiornato', 'gepafin')), onMutationError); + }; + const clearDocument = (docCode) => { + RendicontazioneService.clearDocument(practiceId, docCode, + afterMutation(__('Documento rimosso', 'gepafin')), onMutationError); + }; + + // submit + const handleSubmit = (e) => { + confirmPopup({ + target: e.currentTarget, + message: __('Confermi l\'invio della pratica di rendicontazione? Dopo l\'invio non potrai più modificarla.', 'gepafin'), + icon: 'pi pi-exclamation-triangle', + acceptLabel: __('Invia', 'gepafin'), rejectLabel: __('Annulla', 'gepafin'), + acceptClassName: 'p-button-success', + accept: () => RendicontazioneService.submitPractice(practiceId, + (resp) => { + toast.current?.show({ severity: 'success', summary: __('Pratica inviata', 'gepafin') }); + load(); + }, + onMutationError) + }); + }; + + // ---------- render guards ---------- + if (loading) { + return
; + } + if (!practice) { + return

{__('Pratica non trovata', 'gepafin')}

; + } + + const statusCfg = STATUS_TAGS[practice.status] || { severity: 'secondary', label: practice.status }; + const totals = gate?.totals || {}; + const remissionDue = totals.remission_due || 0; + const grandTotal = totals.grand_total || 0; + const maxRemission = totals.max_remission || 0; + const perCategory = totals.per_category || {}; + + const invoicesOfCat = (code) => practice.invoices.filter(i => i.category_code === code); + + return ( +
+ + + + {/* HEADER */} +
+

{__('Rendicontazione', 'gepafin')}

+

+ + {practice.schema_snapshot?.template_label || `Bando #${practice.call_id}`} + + + + +

+
+ +
+ + {/* ACTIONS */} +
+
+
+
+ +
+ + {/* RIEPILOGO FINANZIARIO */} +
+

{__('Riepilogo', 'gepafin')}

+
+
+ {__('Importo erogato', 'gepafin')} +
{euro(practice.amount_erogato)}
+
+
+ {__('Totale fatture rendicontate', 'gepafin')} +
{euro(grandTotal)}
+
+
+ {__('Cap remissione massimo', 'gepafin')} +
{euro(maxRemission)}
+
+
+ {__('Remissione spettante', 'gepafin')} +
{euro(remissionDue)}
+
+
+
+ +
+ + {/* GATE CHECKS */} + {gate && ( +
+

{__('Requisiti per invio', 'gepafin')}

+
+ {gate.checks.map((c, i) => ( +
+ +
+
{c.label}
+ {c.detail} +
+
+ ))} +
+
+ )} + +
+ + {/* SEZIONE 1: REGIME IVA */} +
+

{__('1. Regime IVA', 'gepafin')}

+
e.preventDefault()}> +
+ + updateIvaRegime(e.value)} + options={ivaAllowed} + placeholder={__('Seleziona...', 'gepafin')} + disabled={readOnly} /> +
+
+
+ +
+ + {/* SEZIONE 2: FATTURE PER CATEGORIA */} +
+

{__('2. Fatture per categoria', 'gepafin')}

+

+ {__('Carica le fatture assegnandole alla categoria di spesa appropriata. I totali si aggiornano in tempo reale.', 'gepafin')} +

+ +
+ {categories.map((cat) => { + const invs = invoicesOfCat(cat.code); + const catTotal = perCategory[cat.code] || 0; + return ( +
+
+
+ {cat.code} — {cat.label} +
{cat.description}
+
+
+
{euro(catTotal)}
+ {invs.length} {__('fatture', 'gepafin')} +
+
+ + {invs.length > 0 && ( + + + formatDate(r.invoice_date)} /> + + {r.description.slice(0, 40)}{r.description.length > 40 ? '…' : ''}} /> + euro(r.taxable)} /> + euro(r.total)} /> + {!readOnly && ( + ( +
+ )} +
+ ); + })} +
+
+ + {/* SEZIONE 3: ULA */} + {ulaSection.enabled && (<> +
+
+

{__('3. Calcolo ULA — Dipendenti', 'gepafin')}

+

+ {__('Inserisci i dipendenti che contano per l\'incremento occupazionale. Soglia minima richiesta:', 'gepafin')} {ulaSection.threshold}. +

+ + {practice.ula_employees.length > 0 && ( + + + + (CONTRACT_TYPES.find(c => c.value === r.contract_type)?.label || r.contract_type)} /> + Number(r.fte_pct).toFixed(2)} /> + `${formatDate(r.period_start_date)} → ${formatDate(r.period_end_date)}`} /> + r.supporting_doc_filename ? {r.supporting_doc_filename} : } /> + {!readOnly && ( + ( +
+ )} + +
+ + {/* SEZIONE 4: DOCUMENTI */} +
+

{__((ulaSection.enabled ? '4.' : '3.') + ' Documenti richiesti', 'gepafin')}

+

+ {__('Carica un file per ciascun documento richiesto. In questa sandbox viene registrato solo il nome del file (upload reale al prossimo sprint).', 'gepafin')} +

+
+ {docsRequired.map((dr) => { + const existing = practice.documents.find(d => d.doc_code === dr.code); + return ( +
+ +
+ {dr.label} +
{dr.code}
+
+
+ {existing?.filename + ? {existing.filename} + : {__('Nessun file', 'gepafin')}} +
+ {!readOnly && ( +
+
+ )} +
+ ); + })} +
+
+ +
+ + {/* BOTTOM ACTIONS */} + {!readOnly && ( +
+
+
+
+ )} + + {/* ---------- DIALOG FATTURA ---------- */} + setInvDialog({ visible: false, data: null })}> + {invDialog.data && ( +
{ e.preventDefault(); saveInvoice(); }}> +
+
+ + setInvDialog(d => ({ ...d, data: { ...d.data, invoice_number: e.target.value } }))} /> +
+
+ + setInvDialog(d => ({ ...d, data: { ...d.data, invoice_date: e.value } }))} /> +
+
+ + setInvDialog(d => ({ ...d, data: { ...d.data, payment_date: e.value } }))} /> +
+
+
+
+ + setInvDialog(d => ({ ...d, data: { ...d.data, supplier_name: e.target.value } }))} /> +
+
+ + setInvDialog(d => ({ ...d, data: { ...d.data, supplier_vat: e.target.value } }))} /> +
+
+
+ + setInvDialog(d => ({ ...d, data: { ...d.data, description: e.target.value } }))} /> +
+
+
+ + setInvDialog(d => ({ ...d, data: { ...d.data, taxable: e.value } }))} /> +
+
+ + setInvDialog(d => ({ ...d, data: { ...d.data, vat: e.value } }))} /> +
+
+ + setInvDialog(d => ({ ...d, data: { ...d.data, total: e.value } }))} /> +
+
+
+ + setInvDialog(d => ({ ...d, data: { ...d.data, pdf_filename: e.target.value } }))} /> +
+
+
+
+ )} +
+ + {/* ---------- DIALOG DIPENDENTE ULA ---------- */} + setEmpDialog({ visible: false, data: null })}> + {empDialog.data && ( +
{ e.preventDefault(); saveEmployee(); }}> +
+
+ + setEmpDialog(d => ({ ...d, data: { ...d.data, codice_fiscale: e.target.value.toUpperCase() } }))} /> +
+
+ + setEmpDialog(d => ({ ...d, data: { ...d.data, full_name: e.target.value } }))} /> +
+
+
+
+ + setEmpDialog(d => ({ ...d, data: { ...d.data, contract_type: e.value } }))} /> +
+
+ + setEmpDialog(d => ({ ...d, data: { ...d.data, role_description: e.target.value } }))} /> +
+
+
+
+ + setEmpDialog(d => ({ ...d, data: { ...d.data, fte_pct: e.value } }))} /> +
+
+ + setEmpDialog(d => ({ ...d, data: { ...d.data, period_start_date: e.value } }))} /> +
+
+ + setEmpDialog(d => ({ ...d, data: { ...d.data, period_end_date: e.value } }))} /> +
+
+
+
+ + typeof t === 'string' ? { value: t, label: t } : { value: t.code, label: t.label })} + onChange={(e) => setEmpDialog(d => ({ ...d, data: { ...d.data, supporting_doc_type: e.value } }))} /> +
+
+ + setEmpDialog(d => ({ ...d, data: { ...d.data, supporting_doc_filename: e.target.value } }))} /> +
+
+
+
+
+ )} +
+ + ); +}; + +export default PraticaRendicontazioneEdit; diff --git a/src/modules/rendicontazione/pages/RendicontazioniMie.js b/src/modules/rendicontazione/pages/RendicontazioniMie.js new file mode 100644 index 0000000..d8b8548 --- /dev/null +++ b/src/modules/rendicontazione/pages/RendicontazioniMie.js @@ -0,0 +1,133 @@ +import React, { useEffect, useState, useRef } from 'react'; +import { __ } from '@wordpress/i18n'; +import { useNavigate } from 'react-router-dom'; + +import { Button } from 'primereact/button'; +import { DataTable } from 'primereact/datatable'; +import { Column } from 'primereact/column'; +import { Tag } from 'primereact/tag'; +import { Toast } from 'primereact/toast'; +import { Skeleton } from 'primereact/skeleton'; + +import RendicontazioneService from '../service/rendicontazioneService'; + +const STATUS_TAGS = { + NOT_STARTED: { severity: 'info', label: 'Da avviare' }, + DRAFT: { severity: 'warning', label: 'In compilazione' }, + SUBMITTED: { severity: 'info', label: 'Inviata' }, + UNDER_REVIEW: { severity: 'info', label: 'In valutazione' }, + APPROVED: { severity: 'success', label: 'Approvata' }, + REJECTED: { severity: 'danger', label: 'Respinta' }, + AWAITING_AMENDMENT: { severity: 'warning', label: 'Soccorso istruttorio' } +}; + +const RendicontazioniMie = () => { + const navigate = useNavigate(); + const toast = useRef(null); + const [rows, setRows] = useState([]); + const [loading, setLoading] = useState(true); + + const load = () => { + setLoading(true); + RendicontazioneService.listMine( + (resp) => { + const practices = (resp?.data?.practices || []).map(p => ({ ...p, isReady: false })); + const ready = (resp?.data?.ready_to_start || []).map(r => ({ ...r, isReady: true })); + setRows([...practices, ...ready]); + setLoading(false); + }, + (err) => { + toast.current?.show({ severity: 'error', summary: __('Errore', 'gepafin'), detail: err?.detail }); + setLoading(false); + } + ); + }; + + useEffect(() => { load(); /* eslint-disable-next-line */ }, []); + + const handleStart = (applicationId) => { + RendicontazioneService.startPractice(applicationId, + (resp) => { + toast.current?.show({ severity: 'success', summary: __('Rendicontazione avviata', 'gepafin') }); + navigate(`/rendicontazioni/${resp.data.id}`); + }, + (err) => toast.current?.show({ severity: 'error', summary: __('Avvio fallito', 'gepafin'), detail: err?.detail }) + ); + }; + + const callTpl = (row) => ( +
+ {row.call_name || `Bando #${row.call_id}`} +
{row.company_name}
+
+ ); + + const erogatoTpl = (row) => { + const v = Number(row.amount_erogato || 0); + return € {v.toLocaleString('it-IT', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}; + }; + + const statusTpl = (row) => { + const key = row.isReady ? 'NOT_STARTED' : (row.status || 'DRAFT'); + const conf = STATUS_TAGS[key] || { severity: 'secondary', label: key }; + return ; + }; + + const progressTpl = (row) => { + if (row.isReady) return ; + return ( + + {row.invoice_count || 0} {__('fatture','gepafin')} · {row.ula_count || 0} {__('dipendenti','gepafin')} · {row.document_count || 0} {__('doc','gepafin')} + + ); + }; + + const actionsTpl = (row) => { + if (row.isReady) { + return