Files
bflows-bandi-fe/src/modules/rendicontazione/pages/RendicontazioneHome.js
BFLOWS Sandbox 8888e0326d feat(rendicontazione): editor schema con form strutturato + dashboard + integrazione microservizio
- Aggiunta voce 'Rendicontazione' in AppSidebar (id 21, icon pi-receipt)
- Nuova pagina RendicontazioneHome: dashboard con tabella bandi + stato schema
  (Non creato / Bozza / Pubblicato) + azioni Crea/Modifica per ciascuno
- Nuova pagina BandoRendicontazioneSchemaEdit: form strutturato 6 sezioni
  (importi/periodo, IVA, categorie, ULA, documenti, regole gate) con
  salva bozza + pubblica, read-only dopo pubblicazione
- Nuovo service modules/rendicontazione/service/rendicontazioneService.js
  (client fetch verso rendicontazione-api, JWT dallo store Zustand)
- 2 nuove route /rendicontazione e /bandi/:id/rendicontazione-schema
  (gate su ROLE_SUPER_ADMIN)
- Bottone 'Schema rendicontazione' aggiunto in BandoEdit come shortcut
- Patch NotificationsSidebar per disabilitare WSS se REACT_APP_ENABLE_WEBSOCKET=0
  (evita errori CORS in sandbox senza RabbitMQ)

UI coerente col codebase: appPage/appPageSection/appForm/appForm__cols/
fieldsRepeater, p-fluid per width input, h1+p in header con border-left
2026-04-18 09:37:08 +02:00

146 lines
5.9 KiB
JavaScript

import React, { useEffect, useState, useRef } from 'react';
import { __ } from '@wordpress/i18n';
import { useNavigate } from 'react-router-dom';
// components
import { Button } from 'primereact/button';
import { DataTable } from 'primereact/datatable';
import { Column } from 'primereact/column';
import { Card } from 'primereact/card';
import { Tag } from 'primereact/tag';
import { Skeleton } from 'primereact/skeleton';
import { Toast } from 'primereact/toast';
// api
import BandoService from '../../../service/bando-service';
import RendicontazioneService from '../service/rendicontazioneService';
const SCHEMA_STATUS_CONFIG = {
null: { severity: 'info', label: __('Non creato', 'gepafin'), icon: 'pi pi-circle' },
DRAFT: { severity: 'warning', label: __('Bozza', 'gepafin'), icon: 'pi pi-pencil' },
PUBLISHED: { severity: 'success', label: __('Pubblicato', 'gepafin'), icon: 'pi pi-check-circle' }
};
const RendicontazioneHome = () => {
const navigate = useNavigate();
const toast = useRef(null);
const [rows, setRows] = useState([]); // {bando, schema}
const [loading, setLoading] = useState(true);
const loadData = () => {
setLoading(true);
BandoService.getBandiPaginated({ page: 0, size: 100 },
(resp) => {
const bandi = resp?.data?.body || [];
// per ogni bando, tento di caricare lo schema di rendicontazione
const baseRows = bandi.map(b => ({ bando: b, schema: null, schemaLoaded: false }));
setRows(baseRows);
setLoading(false);
// Caricamento schemi in parallelo — update progressivo
bandi.forEach((b, idx) => {
RendicontazioneService.getSchemaByCallId(b.id,
(schemaResp) => {
setRows(prev => prev.map((r, i) => i === idx
? { ...r, schema: schemaResp?.data || null, schemaLoaded: true }
: r));
},
(err) => {
// 404 = schema non ancora creato, tutto ok
setRows(prev => prev.map((r, i) => i === idx
? { ...r, schema: null, schemaLoaded: true }
: r));
}
);
});
},
(err) => {
setLoading(false);
if (toast.current) {
toast.current.show({ severity: 'error', summary: __('Errore', 'gepafin'), detail: err?.message || __('Impossibile caricare i bandi', 'gepafin') });
}
}
);
};
useEffect(() => {
loadData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const editSchema = (bandoId) => {
navigate(`/bandi/${bandoId}/rendicontazione-schema`);
};
// --- column templates ---
const bandoNameTpl = (row) => (
<div>
<strong>{row.bando.name || `Bando #${row.bando.id}`}</strong>
{row.bando.descriptionShort && (
<div><small className="text-color-secondary">{row.bando.descriptionShort.slice(0, 80)}{row.bando.descriptionShort.length > 80 ? '…' : ''}</small></div>
)}
</div>
);
const bandoStatusTpl = (row) => (
<Tag value={row.bando.status || '—'} severity={row.bando.status === 'PUBLISH' ? 'success' : 'secondary'} />
);
const schemaStatusTpl = (row) => {
if (!row.schemaLoaded) return <Skeleton width="6rem" height="1.5rem" />;
const key = row.schema ? row.schema.status : null;
const conf = SCHEMA_STATUS_CONFIG[key] || SCHEMA_STATUS_CONFIG[null];
return <Tag icon={conf.icon} value={conf.label} severity={conf.severity} />;
};
const actionsTpl = (row) => {
if (!row.schemaLoaded) return <Skeleton width="8rem" height="2rem" />;
const hasSchema = !!row.schema;
return (
<Button
icon={hasSchema ? 'pi pi-pencil' : 'pi pi-plus-circle'}
label={hasSchema ? __('Modifica', 'gepafin') : __('Crea schema', 'gepafin')}
className={hasSchema ? 'p-button-outlined p-button-sm' : 'p-button-sm'}
severity={hasSchema ? null : 'success'}
onClick={() => editSchema(row.bando.id)}
/>
);
};
return (
<div className="page-wrapper">
<Toast ref={toast} />
<div className="mb-3">
<h2 className="mb-1">{__('Gestione rendicontazione', 'gepafin')}</h2>
<p className="m-0 text-color-secondary">
{__('Configura per ciascun bando lo schema di rendicontazione che i beneficiari vedranno dopo la firma del contratto. Ogni bando ha uno schema: categorie di spesa, regole ULA, documenti richiesti.', 'gepafin')}
</p>
</div>
<Card>
<DataTable
value={rows}
loading={loading}
dataKey="bando.id"
emptyMessage={__('Nessun bando disponibile', 'gepafin')}
paginator={rows.length > 15}
rows={15}
stripedRows
>
<Column field="bando.id" header="ID" style={{ width: '60px' }} />
<Column field="bando.name" header={__('Bando', 'gepafin')} body={bandoNameTpl} />
<Column field="bando.status" header={__('Stato bando', 'gepafin')} body={bandoStatusTpl} style={{ width: '140px' }} />
<Column header={__('Schema rendicontazione', 'gepafin')} body={schemaStatusTpl} style={{ width: '180px' }} />
<Column header={__('Azioni', 'gepafin')} body={actionsTpl} style={{ width: '180px' }} />
</DataTable>
</Card>
</div>
);
};
export default RendicontazioneHome;