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
This commit is contained in:
BFLOWS Sandbox
2026-04-18 09:50:53 +02:00
parent 8888e0326d
commit 9c483ade34
6 changed files with 983 additions and 0 deletions

View File

@@ -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) => (
<div>
<strong>{row.call_name || `Bando #${row.call_id}`}</strong>
<div><small className="text-color-secondary">{row.company_name}</small></div>
</div>
);
const erogatoTpl = (row) => {
const v = Number(row.amount_erogato || 0);
return <strong> {v.toLocaleString('it-IT', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</strong>;
};
const statusTpl = (row) => {
const key = row.isReady ? 'NOT_STARTED' : (row.status || 'DRAFT');
const conf = STATUS_TAGS[key] || { severity: 'secondary', label: key };
return <Tag value={conf.label} severity={conf.severity} />;
};
const progressTpl = (row) => {
if (row.isReady) return <span className="text-color-secondary"></span>;
return (
<span className="text-color-secondary" style={{ fontSize: '0.9em' }}>
{row.invoice_count || 0} {__('fatture','gepafin')} · {row.ula_count || 0} {__('dipendenti','gepafin')} · {row.document_count || 0} {__('doc','gepafin')}
</span>
);
};
const actionsTpl = (row) => {
if (row.isReady) {
return <Button icon="pi pi-play" label={__('Avvia rendicontazione', 'gepafin')}
size="small" severity="success" onClick={() => handleStart(row.application_id)} />;
}
const isEditable = row.status === 'DRAFT';
return <Button icon={isEditable ? 'pi pi-pencil' : 'pi pi-eye'}
label={isEditable ? __('Continua', 'gepafin') : __('Apri', 'gepafin')}
size="small" outlined={!isEditable}
onClick={() => navigate(`/rendicontazioni/${row.id}`)} />;
};
return (
<div className="appPage">
<Toast ref={toast} />
<div className="appPage__pageHeader">
<h1>{__('Le mie rendicontazioni', 'gepafin')}</h1>
<p>{__('Per ogni pratica finanziata puoi avviare la rendicontazione delle spese e il calcolo della remissione del debito.', 'gepafin')}</p>
</div>
<div className="appPage__spacer"></div>
<div className="appPageSection">
{loading && <Skeleton width="100%" height="10rem" />}
{!loading && rows.length === 0 && (
<div style={{ padding: '2rem', textAlign: 'center', width: '100%' }}>
<i className="pi pi-inbox" style={{ fontSize: '2.5rem', color: 'var(--text-color-secondary)', display: 'block', marginBottom: '0.75rem' }} />
<p>{__('Non ci sono rendicontazioni da avviare al momento.', 'gepafin')}</p>
<small className="text-color-secondary">
{__('Le rendicontazioni diventano disponibili dopo la firma del contratto e quando l\'ente ha pubblicato lo schema di rendicontazione per il bando.', 'gepafin')}
</small>
</div>
)}
{!loading && rows.length > 0 && (
<DataTable value={rows} dataKey="id" stripedRows responsiveLayout="scroll" style={{ width: '100%' }}>
<Column header={__('Bando', 'gepafin')} body={callTpl} />
<Column header={__('Importo erogato', 'gepafin')} body={erogatoTpl} style={{ width: '180px' }} />
<Column header={__('Stato', 'gepafin')} body={statusTpl} style={{ width: '180px' }} />
<Column header={__('Avanzamento', 'gepafin')} body={progressTpl} />
<Column header={__('Azione', 'gepafin')} body={actionsTpl} style={{ width: '220px' }} />
</DataTable>
)}
</div>
</div>
);
};
export default RendicontazioniMie;