diff --git a/src/components/NotificationsSidebar/index.js b/src/components/NotificationsSidebar/index.js
index b4ec5e2..7489b26 100644
--- a/src/components/NotificationsSidebar/index.js
+++ b/src/components/NotificationsSidebar/index.js
@@ -174,6 +174,10 @@ const NotificationsSidebar = () => {
}
const connectWebSocket = () => {
+ // BFLOWS: consenti di disabilitare WSS via env (sandbox senza RabbitMQ)
+ if (process.env.REACT_APP_ENABLE_WEBSOCKET === '0') {
+ return;
+ }
socket.current = new SockJS(socketUrl, null, {
transports: [
'websocket',
diff --git a/src/layouts/DefaultLayout/components/AppSidebar/index.js b/src/layouts/DefaultLayout/components/AppSidebar/index.js
index 89b5903..28840a8 100644
--- a/src/layouts/DefaultLayout/components/AppSidebar/index.js
+++ b/src/layouts/DefaultLayout/components/AppSidebar/index.js
@@ -27,6 +27,13 @@ const AppSidebar = () => {
id: 2,
enable: intersection(permissions, ['MANAGE_TENDERS']).length
},
+ {
+ label: __('Rendicontazione', 'gepafin'),
+ icon: 'pi pi-receipt',
+ href: '/rendicontazione',
+ id: 21,
+ enable: intersection(permissions, ['MANAGE_TENDERS']).length
+ },
{
label: __('Domande in lavorazione', 'gepafin'),
icon: 'pi pi-file',
diff --git a/src/modules/rendicontazione/pages/BandoRendicontazioneSchemaEdit.js b/src/modules/rendicontazione/pages/BandoRendicontazioneSchemaEdit.js
new file mode 100644
index 0000000..3fd8546
--- /dev/null
+++ b/src/modules/rendicontazione/pages/BandoRendicontazioneSchemaEdit.js
@@ -0,0 +1,589 @@
+import React, { useEffect, useState, useRef, useMemo } from 'react';
+import { __ } from '@wordpress/i18n';
+import { useNavigate, useParams } from 'react-router-dom';
+
+// store
+import { useStoreValue } from '../../../store';
+
+// 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 { InputText } from 'primereact/inputtext';
+import { InputNumber } from 'primereact/inputnumber';
+import { InputSwitch } from 'primereact/inputswitch';
+import { MultiSelect } from 'primereact/multiselect';
+import { Dropdown } from 'primereact/dropdown';
+import { Calendar } from 'primereact/calendar';
+import { InputTextarea } from 'primereact/inputtextarea';
+import BlockingOverlay from '../../../components/BlockingOverlay';
+
+// api
+import RendicontazioneService from '../service/rendicontazioneService';
+import BandoService from '../../../service/bando-service';
+
+// ---------- costanti ----------
+const IVA_REGIMES = [
+ { value: 'ORDINARIO', label: 'Ordinario' },
+ { value: 'FORFETTARIO', label: 'Forfettario' },
+ { value: 'ESENTE', label: 'Esente' }
+];
+
+const PERIOD_START_RULES = [
+ { value: 'erogato_date', label: 'Data di erogazione del finanziamento' },
+ { value: 'contract_signed_date', label: 'Data firma contratto' },
+ { value: 'custom', label: 'Data personalizzata' }
+];
+
+const ULA_DOC_TYPES = [
+ { value: 'LUL', label: 'Libro Unico del Lavoro (LUL)' },
+ { value: 'GESTIONALE_PAGHE', label: 'Estratto gestionale paghe' },
+ { value: 'DICHIARAZIONE_CDL', label: 'Dichiarazione Consulente del Lavoro' },
+ { value: 'ALTRO', label: 'Altro documento di supporto' }
+];
+
+// ---------- helpers JSON <-> form ----------
+const schemaJsonToForm = (j) => {
+ if (!j || !j.sections) return null;
+ const general = j.sections.find(s => s.type === 'static_fields') || {};
+ const expenses = j.sections.find(s => s.type === 'category_grid') || {};
+ const ula = j.sections.find(s => s.type === 'ula_block') || {};
+ const docs = j.sections.find(s => s.type === 'document_checklist') || {};
+ const gate = j.gate_rules || {};
+ const ivaField = (general.fields || []).find(f => f.id === 'iva_regime');
+ const ivaAllowed = ivaField && ivaField.options
+ ? ivaField.options.map(o => typeof o === 'string' ? o : o.value)
+ : ['ORDINARIO','FORFETTARIO','ESENTE'];
+ const parseList = (list) => (list || []).map(x =>
+ typeof x === 'string' ? { code: x, label: x } : { code: x.code || '', label: x.label || x.code || '' });
+ return {
+ amount_min: gate.amount_range?.min ?? 5000,
+ amount_max: gate.amount_range?.max ?? 25000,
+ period_end: gate.period_end ? new Date(gate.period_end) : null,
+ period_start_rule: gate.period_start_rule ?? 'erogato_date',
+ iva_regimes_allowed: ivaAllowed,
+ iva_ordinario_imponibile_only: gate.iva_ordinario_imponibile_only ?? true,
+ categories: (expenses.categories || []).map(c => ({
+ code: c.code || '', label: c.label || '',
+ description: c.description || '', cap_amount: c.cap_amount ?? null
+ })),
+ ula_enabled: ula.enabled ?? false,
+ ula_threshold: ula.threshold ?? 1.0,
+ ula_period_start_rule: ula.period_start_rule ?? 'erogato_date',
+ ula_period_end: ula.period_end ? new Date(ula.period_end) : null,
+ ula_supporting_doc_required: ula.supporting_doc_required ?? true,
+ ula_supporting_doc_types: (ula.supporting_doc_types || []).map(t => typeof t === 'string' ? t : t.code),
+ docs_required: parseList(docs.required_types),
+ cap_pct_erogato: gate.cap_pct_erogato != null ? Math.round(gate.cap_pct_erogato * 100) : 50,
+ 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
+ };
+};
+
+const formToSchemaJson = (f, base = null) => {
+ const orig = base || {};
+ const fmtDate = (d) => d ? (typeof d === 'string' ? d : d.toISOString().slice(0, 10)) : null;
+ return {
+ version: orig.version || '1.0',
+ template_id: orig.template_id || 'CUSTOM',
+ template_label: orig.template_label || 'Schema personalizzato',
+ sections: [
+ {
+ type: 'static_fields', id: 'general', label: 'Dati generali',
+ fields: [{
+ id: 'iva_regime', type: 'select', label: 'Regime IVA', required: true,
+ options: IVA_REGIMES.filter(o => f.iva_regimes_allowed.includes(o.value))
+ }]
+ },
+ {
+ type: 'category_grid', id: 'expenses', label: 'Spese ammissibili per categoria',
+ categories: f.categories,
+ invoice_schema: { required_fields: ['invoice_number','invoice_date','payment_date','supplier_name','supplier_vat','description','taxable','vat','total','pdf'] }
+ },
+ {
+ type: 'ula_block', id: 'ula', label: 'Calcolo ULA',
+ enabled: f.ula_enabled, threshold: f.ula_threshold,
+ period_start_rule: f.ula_period_start_rule,
+ period_end: fmtDate(f.ula_period_end),
+ supporting_doc_required: f.ula_supporting_doc_required,
+ supporting_doc_types: ULA_DOC_TYPES
+ .filter(t => f.ula_supporting_doc_types.includes(t.value))
+ .map(t => ({ code: t.value, label: t.label }))
+ },
+ {
+ type: 'document_checklist', id: 'docs', label: 'Documenti richiesti',
+ required_types: f.docs_required
+ }
+ ],
+ gate_rules: {
+ amount_range: { min: f.amount_min, max: f.amount_max },
+ cap_pct_erogato: f.cap_pct_erogato / 100,
+ cap_absolute: f.cap_absolute,
+ iva_ordinario_imponibile_only: f.iva_ordinario_imponibile_only,
+ period_start_rule: f.period_start_rule,
+ period_end: fmtDate(f.period_end),
+ 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
+ }
+ };
+};
+
+
+const BandoRendicontazioneSchemaEdit = () => {
+ const { id } = useParams();
+ const navigate = useNavigate();
+ const isAsyncRequest = useStoreValue('isAsyncRequest');
+ const callId = parseInt(id);
+
+ const [bando, setBando] = useState(null);
+ const [bandoLoading, setBandoLoading] = useState(true);
+ const [schemaRecord, setSchemaRecord] = useState(null);
+ const [schemaLoading, setSchemaLoading] = useState(true);
+ const [form, setForm] = useState(null);
+ const [dirty, setDirty] = useState(false);
+ const toast = useRef(null);
+
+ // ---------- load ----------
+ const loadBando = () => {
+ setBandoLoading(true);
+ BandoService.getBando(callId,
+ (r) => { setBando(r?.data || null); setBandoLoading(false); },
+ () => setBandoLoading(false));
+ };
+
+ const loadSchema = () => {
+ setSchemaLoading(true);
+ RendicontazioneService.getSchemaByCallId(callId,
+ (resp) => {
+ const rec = resp?.data || null;
+ setSchemaRecord(rec);
+ setForm(rec ? schemaJsonToForm(rec.schema_json) : null);
+ setDirty(false);
+ setSchemaLoading(false);
+ },
+ (err) => {
+ if (err?.status === 404) { setSchemaRecord(null); setForm(null); }
+ else toast.current?.show({ severity: 'error', summary: __('Errore caricamento schema','gepafin'), detail: err?.detail });
+ setSchemaLoading(false);
+ });
+ };
+
+ useEffect(() => {
+ if (!isNaN(callId)) { loadBando(); loadSchema(); }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [callId]);
+
+ // ---------- updates ----------
+ const update = (patch) => { setForm(p => ({ ...p, ...patch })); setDirty(true); };
+ const updateCategory = (idx, patch) => {
+ setForm(p => ({ ...p, categories: p.categories.map((c,i) => i===idx ? {...c, ...patch} : c) }));
+ setDirty(true);
+ };
+ const addCategory = () => {
+ setForm(p => ({ ...p, categories: [...p.categories, { code:'', label:'', description:'', cap_amount:null }] }));
+ setDirty(true);
+ };
+ const removeCategory = (idx) => {
+ setForm(p => ({ ...p, categories: p.categories.filter((_,i) => i!==idx) }));
+ setDirty(true);
+ };
+ const updateDoc = (idx, patch) => {
+ setForm(p => ({ ...p, docs_required: p.docs_required.map((d,i) => i===idx ? {...d,...patch} : d) }));
+ setDirty(true);
+ };
+ const addDoc = () => {
+ setForm(p => ({ ...p, docs_required: [...p.docs_required, { code:'', label:'' }] }));
+ setDirty(true);
+ };
+ const removeDoc = (idx) => {
+ setForm(p => ({ ...p, docs_required: p.docs_required.filter((_,i) => i!==idx) }));
+ setDirty(true);
+ };
+
+ // ---------- actions ----------
+ const handleInitializeRestart = (e) => {
+ confirmPopup({
+ target: e.currentTarget,
+ message: __('Inizializzo lo schema con il template RE-START? Sarà modificabile finché non verrà pubblicato.','gepafin'),
+ icon: 'pi pi-info-circle',
+ acceptLabel: __('Inizializza','gepafin'), rejectLabel: __('Annulla','gepafin'),
+ accept: () => RendicontazioneService.initializeRestartTemplate(callId,
+ () => { toast.current?.show({severity:'success', summary: __('Schema inizializzato','gepafin')}); loadSchema(); },
+ (err) => toast.current?.show({severity:'error', summary:__('Inizializzazione fallita','gepafin'), detail: err?.detail}))
+ });
+ };
+
+ const handleSave = () => {
+ const newJson = formToSchemaJson(form, schemaRecord?.schema_json);
+ RendicontazioneService.updateSchema(callId, newJson,
+ (resp) => {
+ toast.current?.show({severity:'success', summary: __('Schema salvato','gepafin')});
+ setSchemaRecord(resp?.data);
+ setForm(schemaJsonToForm(resp?.data?.schema_json));
+ setDirty(false);
+ },
+ (err) => toast.current?.show({severity:'error', summary:__('Salvataggio fallito','gepafin'), detail: err?.detail}));
+ };
+
+ const handlePublish = (e) => {
+ confirmPopup({
+ target: e.currentTarget,
+ message: __('Dopo la pubblicazione lo schema non sarà più modificabile e diventerà visibile ai beneficiari. Confermi?','gepafin'),
+ icon: 'pi pi-exclamation-triangle',
+ acceptLabel: __('Pubblica','gepafin'), rejectLabel: __('Annulla','gepafin'),
+ acceptClassName: 'p-button-success',
+ accept: () => RendicontazioneService.publishSchema(callId,
+ (resp) => { toast.current?.show({severity:'success', summary:__('Schema pubblicato','gepafin')}); setSchemaRecord(resp?.data); },
+ (err) => toast.current?.show({severity:'error', summary:__('Pubblicazione fallita','gepafin'), detail: err?.detail}))
+ });
+ };
+
+ // ---------- render ----------
+ const isPublished = schemaRecord?.status === 'PUBLISHED';
+ const readOnly = isPublished;
+ const hasSchema = !!schemaRecord;
+
+ const statusTag = useMemo(() => {
+ if (!hasSchema) return
+ {bandoLoading
+ ?
+ {__('Puoi inizializzarlo con un template predefinito. Per ora è disponibile il template RE-START (fondo prestiti con remissione del debito).','gepafin')} +
++ {__('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')} +
+{__('Nessun modulo creato ancora', 'gepafin')}
} ++ {__('Configura come i beneficiari dovranno rendicontare dopo la firma del contratto: categorie di spesa, ULA, documenti richiesti.', 'gepafin')} +
+