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
This commit is contained in:
@@ -174,6 +174,10 @@ const NotificationsSidebar = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const connectWebSocket = () => {
|
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, {
|
socket.current = new SockJS(socketUrl, null, {
|
||||||
transports: [
|
transports: [
|
||||||
'websocket',
|
'websocket',
|
||||||
|
|||||||
@@ -27,6 +27,13 @@ const AppSidebar = () => {
|
|||||||
id: 2,
|
id: 2,
|
||||||
enable: intersection(permissions, ['MANAGE_TENDERS']).length
|
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'),
|
label: __('Domande in lavorazione', 'gepafin'),
|
||||||
icon: 'pi pi-file',
|
icon: 'pi pi-file',
|
||||||
|
|||||||
@@ -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 <Tag severity="info" value={__('Non creato','gepafin')} />;
|
||||||
|
if (isPublished) return <Tag severity="success" value={__('Pubblicato','gepafin')} />;
|
||||||
|
return <Tag severity="warning" value={__('Bozza','gepafin')} />;
|
||||||
|
}, [hasSchema, isPublished]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="appPage">
|
||||||
|
<Toast ref={toast} />
|
||||||
|
<ConfirmPopup />
|
||||||
|
<BlockingOverlay isBlocked={isAsyncRequest} />
|
||||||
|
|
||||||
|
{/* HEADER — flex column, border-left */}
|
||||||
|
<div className="appPage__pageHeader">
|
||||||
|
<h1>{__('Schema rendicontazione','gepafin')}</h1>
|
||||||
|
<p>
|
||||||
|
{bandoLoading
|
||||||
|
? <Skeleton width="20rem" height="1.2rem" />
|
||||||
|
: <>
|
||||||
|
<span className="companyName">{(bando && bando.name) || `Bando #${callId}`}</span>
|
||||||
|
<span style={{ marginLeft: '1rem' }}>{statusTag}</span>
|
||||||
|
</>}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="appPage__spacer"></div>
|
||||||
|
|
||||||
|
{/* ACTIONS — torna indietro + salva/pubblica */}
|
||||||
|
<div className="appPageSection">
|
||||||
|
<div className="appPageSection__actions">
|
||||||
|
<Button type="button" outlined icon="pi pi-arrow-left"
|
||||||
|
label={__('Indietro','gepafin')} onClick={() => navigate('/rendicontazione')} />
|
||||||
|
{hasSchema && !isPublished && (
|
||||||
|
<>
|
||||||
|
<Button type="button" icon="pi pi-save" iconPos="right"
|
||||||
|
label={__('Salva bozza','gepafin')} onClick={handleSave} disabled={!dirty} />
|
||||||
|
<Button type="button" icon="pi pi-check-circle" iconPos="right" severity="success"
|
||||||
|
label={__('Pubblica','gepafin')} onClick={handlePublish} disabled={dirty} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="appPage__spacer"></div>
|
||||||
|
|
||||||
|
{/* CONTENT */}
|
||||||
|
{schemaLoading && (
|
||||||
|
<div className="appPageSection">
|
||||||
|
<Skeleton width="100%" height="12rem" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!schemaLoading && !hasSchema && (
|
||||||
|
<div className="appPageSection" style={{ alignItems: 'center', padding: '3rem 2rem' }}>
|
||||||
|
<i className="pi pi-file-edit" style={{ fontSize: '3rem', color: 'var(--text-color-secondary)', marginBottom: '1rem' }} />
|
||||||
|
<h2 style={{ marginBottom: '0.5rem' }}>{__('Nessuno schema di rendicontazione per questo bando','gepafin')}</h2>
|
||||||
|
<p style={{ color: 'var(--text-color-secondary)', marginBottom: '1.5rem', textAlign: 'center' }}>
|
||||||
|
{__('Puoi inizializzarlo con un template predefinito. Per ora è disponibile il template RE-START (fondo prestiti con remissione del debito).','gepafin')}
|
||||||
|
</p>
|
||||||
|
<Button icon="pi pi-plus-circle" iconPos="right"
|
||||||
|
label={__('Inizializza con template RE-START','gepafin')}
|
||||||
|
onClick={handleInitializeRestart} severity="success" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!schemaLoading && hasSchema && form && (
|
||||||
|
<form className="appForm p-fluid" onSubmit={(e) => e.preventDefault()}>
|
||||||
|
|
||||||
|
{/* 1 - IMPORTI E PERIODO */}
|
||||||
|
<div className="appPageSection">
|
||||||
|
<h2>{__('1. Importi ammissibili e periodo','gepafin')}</h2>
|
||||||
|
<div className="appForm__cols">
|
||||||
|
<div className="appForm__field">
|
||||||
|
<label>{__('Importo minimo erogato','gepafin')}</label>
|
||||||
|
<InputNumber value={form.amount_min} onValueChange={(e) => update({amount_min: e.value})}
|
||||||
|
mode="currency" currency="EUR" locale="it-IT" disabled={readOnly} />
|
||||||
|
</div>
|
||||||
|
<div className="appForm__field">
|
||||||
|
<label>{__('Importo massimo erogato','gepafin')}</label>
|
||||||
|
<InputNumber value={form.amount_max} onValueChange={(e) => update({amount_max: e.value})}
|
||||||
|
mode="currency" currency="EUR" locale="it-IT" disabled={readOnly} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="appForm__cols">
|
||||||
|
<div className="appForm__field">
|
||||||
|
<label>{__('Periodo di ammissibilità — inizio','gepafin')}</label>
|
||||||
|
<Dropdown value={form.period_start_rule}
|
||||||
|
onChange={(e) => update({period_start_rule: e.value})}
|
||||||
|
options={PERIOD_START_RULES} disabled={readOnly} />
|
||||||
|
</div>
|
||||||
|
<div className="appForm__field">
|
||||||
|
<label>{__('Periodo di ammissibilità — fine','gepafin')}</label>
|
||||||
|
<Calendar value={form.period_end} onChange={(e) => update({period_end: e.value})}
|
||||||
|
dateFormat="dd/mm/yy" showIcon disabled={readOnly} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="appPage__spacer"></div>
|
||||||
|
|
||||||
|
{/* 2 - IVA */}
|
||||||
|
<div className="appPageSection">
|
||||||
|
<h2>{__('2. Regime IVA','gepafin')}</h2>
|
||||||
|
<div className="appForm__field">
|
||||||
|
<label>{__('Regimi IVA consentiti','gepafin')}</label>
|
||||||
|
<MultiSelect value={form.iva_regimes_allowed} options={IVA_REGIMES}
|
||||||
|
onChange={(e) => update({iva_regimes_allowed: e.value})}
|
||||||
|
disabled={readOnly} display="chip" placeholder={__('Seleziona regimi','gepafin')} />
|
||||||
|
</div>
|
||||||
|
<div className="appForm__field">
|
||||||
|
<div className="appForm__row">
|
||||||
|
<InputSwitch checked={form.iva_ordinario_imponibile_only}
|
||||||
|
onChange={(e) => update({iva_ordinario_imponibile_only: e.value})} disabled={readOnly} />
|
||||||
|
<label style={{ cursor: 'pointer' }}
|
||||||
|
onClick={() => !readOnly && update({iva_ordinario_imponibile_only: !form.iva_ordinario_imponibile_only})}>
|
||||||
|
{__('Regime ordinario: solo imponibile rendicontabile','gepafin')}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<small>{__('Se attivo, in regime ordinario l\'IVA non viene considerata rendicontabile — vale solo la base imponibile della fattura.','gepafin')}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="appPage__spacer"></div>
|
||||||
|
|
||||||
|
{/* 3 - CATEGORIE */}
|
||||||
|
<div className="appPageSection">
|
||||||
|
<h2>{__('3. Categorie di spesa ammissibili','gepafin')} <span style={{fontWeight:400, color:'var(--text-color-secondary)', fontSize:'0.9em'}}>({form.categories.length})</span></h2>
|
||||||
|
|
||||||
|
<div className="fieldsRepeater">
|
||||||
|
{form.categories.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 || `#${i+1}`} — {c.label || __('(senza nome)','gepafin')}</strong>
|
||||||
|
{!readOnly && (
|
||||||
|
<Button type="button" icon="pi pi-trash" severity="danger" outlined
|
||||||
|
size="small" onClick={() => removeCategory(i)}
|
||||||
|
tooltip={__('Rimuovi categoria','gepafin')} tooltipOptions={{position:'top'}} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="appForm__cols">
|
||||||
|
<div className="appForm__field">
|
||||||
|
<label>{__('Codice','gepafin')}</label>
|
||||||
|
<InputText value={c.code} onChange={(e) => updateCategory(i,{code:e.target.value})}
|
||||||
|
placeholder="B1" disabled={readOnly} />
|
||||||
|
</div>
|
||||||
|
<div className="appForm__field">
|
||||||
|
<label>{__('Cap importo (opzionale)','gepafin')}</label>
|
||||||
|
<InputNumber value={c.cap_amount}
|
||||||
|
onValueChange={(e) => updateCategory(i,{cap_amount:e.value})}
|
||||||
|
mode="currency" currency="EUR" locale="it-IT" disabled={readOnly} placeholder="—" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="appForm__field">
|
||||||
|
<label>{__('Nome categoria','gepafin')}</label>
|
||||||
|
<InputText value={c.label}
|
||||||
|
onChange={(e) => updateCategory(i,{label:e.target.value})} disabled={readOnly} />
|
||||||
|
</div>
|
||||||
|
<div className="appForm__field">
|
||||||
|
<label>{__('Descrizione','gepafin')}</label>
|
||||||
|
<InputTextarea value={c.description}
|
||||||
|
onChange={(e) => updateCategory(i,{description:e.target.value})}
|
||||||
|
rows={2} disabled={readOnly} autoResize />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!readOnly && (
|
||||||
|
<div style={{ marginTop: '1rem' }}>
|
||||||
|
<Button type="button" icon="pi pi-plus" iconPos="right" outlined
|
||||||
|
label={__('Aggiungi categoria','gepafin')} onClick={addCategory} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="appPage__spacer"></div>
|
||||||
|
|
||||||
|
{/* 4 - ULA */}
|
||||||
|
<div className="appPageSection">
|
||||||
|
<h2>{__('4. Calcolo ULA (incremento occupazione)','gepafin')}</h2>
|
||||||
|
<div className="appForm__field">
|
||||||
|
<div className="appForm__row">
|
||||||
|
<InputSwitch checked={form.ula_enabled}
|
||||||
|
onChange={(e) => update({ula_enabled: e.value})} disabled={readOnly} />
|
||||||
|
<label style={{ cursor: 'pointer' }}
|
||||||
|
onClick={() => !readOnly && update({ula_enabled: !form.ula_enabled})}>
|
||||||
|
{__('Calcolo ULA richiesto per questo bando','gepafin')}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{form.ula_enabled && (
|
||||||
|
<>
|
||||||
|
<div className="appForm__cols">
|
||||||
|
<div className="appForm__field">
|
||||||
|
<label>{__('Soglia minima di incremento','gepafin')}</label>
|
||||||
|
<InputNumber value={form.ula_threshold}
|
||||||
|
onValueChange={(e) => update({ula_threshold: e.value})}
|
||||||
|
mode="decimal" minFractionDigits={1} maxFractionDigits={2} min={0} disabled={readOnly} />
|
||||||
|
</div>
|
||||||
|
<div className="appForm__field">
|
||||||
|
<label>{__('Periodo fine ULA','gepafin')}</label>
|
||||||
|
<Calendar value={form.ula_period_end}
|
||||||
|
onChange={(e) => update({ula_period_end: e.value})}
|
||||||
|
dateFormat="dd/mm/yy" showIcon disabled={readOnly} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="appForm__field">
|
||||||
|
<div className="appForm__row">
|
||||||
|
<InputSwitch checked={form.ula_supporting_doc_required}
|
||||||
|
onChange={(e) => update({ula_supporting_doc_required: e.value})} disabled={readOnly} />
|
||||||
|
<label style={{ cursor: 'pointer' }}
|
||||||
|
onClick={() => !readOnly && update({ula_supporting_doc_required: !form.ula_supporting_doc_required})}>
|
||||||
|
{__('Allegato di supporto obbligatorio','gepafin')}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{form.ula_supporting_doc_required && (
|
||||||
|
<div className="appForm__field">
|
||||||
|
<label>{__('Tipi di documento ammessi','gepafin')}</label>
|
||||||
|
<MultiSelect value={form.ula_supporting_doc_types} options={ULA_DOC_TYPES}
|
||||||
|
onChange={(e) => update({ula_supporting_doc_types: e.value})}
|
||||||
|
disabled={readOnly} display="chip" placeholder={__('Seleziona tipi','gepafin')} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="appPage__spacer"></div>
|
||||||
|
|
||||||
|
{/* 5 - DOCUMENTI */}
|
||||||
|
<div className="appPageSection">
|
||||||
|
<h2>{__('5. Documenti richiesti','gepafin')} <span style={{fontWeight:400, color:'var(--text-color-secondary)', fontSize:'0.9em'}}>({form.docs_required.length})</span></h2>
|
||||||
|
<p style={{ color:'var(--text-color-secondary)', marginTop: 0 }}>
|
||||||
|
{__('I documenti già in regola nel repository della Company saranno riutilizzati automaticamente. Solo quelli scaduti o mancanti richiederanno caricamento.','gepafin')}
|
||||||
|
</p>
|
||||||
|
<div className="fieldsRepeater">
|
||||||
|
{form.docs_required.map((d, i) => (
|
||||||
|
<div key={i} className="fieldsRepeater__panel" style={{ padding:'0.75rem 1rem', border:'1px solid var(--surface-border)', borderRadius:'6px', background:'var(--surface-50)' }}>
|
||||||
|
<div className="appForm__cols">
|
||||||
|
<div className="appForm__field">
|
||||||
|
<label>{__('Codice','gepafin')}</label>
|
||||||
|
<InputText value={d.code} onChange={(e) => updateDoc(i,{code:e.target.value})}
|
||||||
|
placeholder="DURC" disabled={readOnly} />
|
||||||
|
</div>
|
||||||
|
<div className="appForm__field">
|
||||||
|
<label>{__('Etichetta visibile al beneficiario','gepafin')}</label>
|
||||||
|
<InputText value={d.label} onChange={(e) => updateDoc(i,{label:e.target.value})} disabled={readOnly} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{!readOnly && (
|
||||||
|
<div style={{ textAlign: 'right', marginTop:'0.5rem' }}>
|
||||||
|
<Button type="button" icon="pi pi-trash" severity="danger" outlined size="small"
|
||||||
|
onClick={() => removeDoc(i)} label={__('Rimuovi','gepafin')} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{!readOnly && (
|
||||||
|
<div style={{ marginTop: '1rem' }}>
|
||||||
|
<Button type="button" icon="pi pi-plus" iconPos="right" outlined
|
||||||
|
label={__('Aggiungi documento','gepafin')} onClick={addDoc} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="appPage__spacer"></div>
|
||||||
|
|
||||||
|
{/* 6 - REGOLE */}
|
||||||
|
<div className="appPageSection">
|
||||||
|
<h2>{__('6. Regole di validazione (gate pre-submit)','gepafin')}</h2>
|
||||||
|
<div className="appForm__cols">
|
||||||
|
<div className="appForm__field">
|
||||||
|
<label>{__('Cap remissione (% erogato)','gepafin')}</label>
|
||||||
|
<InputNumber value={form.cap_pct_erogato}
|
||||||
|
onValueChange={(e) => update({cap_pct_erogato: e.value})}
|
||||||
|
suffix=" %" min={0} max={100} disabled={readOnly} />
|
||||||
|
</div>
|
||||||
|
<div className="appForm__field">
|
||||||
|
<label>{__('Cap remissione assoluto','gepafin')}</label>
|
||||||
|
<InputNumber value={form.cap_absolute}
|
||||||
|
onValueChange={(e) => update({cap_absolute: e.value})}
|
||||||
|
mode="currency" currency="EUR" locale="it-IT" disabled={readOnly} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="appForm__field">
|
||||||
|
<div className="appForm__row">
|
||||||
|
<InputSwitch checked={form.require_invoice_per_category}
|
||||||
|
onChange={(e) => update({require_invoice_per_category: e.value})} disabled={readOnly} />
|
||||||
|
<label style={{ cursor: 'pointer' }}
|
||||||
|
onClick={() => !readOnly && update({require_invoice_per_category: !form.require_invoice_per_category})}>
|
||||||
|
{__('Richiedi almeno una fattura per ogni categoria con importo > 0','gepafin')}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="appForm__field">
|
||||||
|
<div className="appForm__row">
|
||||||
|
<InputSwitch checked={form.require_ula_above_threshold}
|
||||||
|
onChange={(e) => update({require_ula_above_threshold: e.value})} disabled={readOnly} />
|
||||||
|
<label style={{ cursor: 'pointer' }}
|
||||||
|
onClick={() => !readOnly && update({require_ula_above_threshold: !form.require_ula_above_threshold})}>
|
||||||
|
{__('Richiedi ULA sopra soglia per validare','gepafin')}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="appForm__field">
|
||||||
|
<div className="appForm__row">
|
||||||
|
<InputSwitch checked={form.require_all_documents_resolved}
|
||||||
|
onChange={(e) => update({require_all_documents_resolved: e.value})} disabled={readOnly} />
|
||||||
|
<label style={{ cursor: 'pointer' }}
|
||||||
|
onClick={() => !readOnly && update({require_all_documents_resolved: !form.require_all_documents_resolved})}>
|
||||||
|
{__('Richiedi che tutti i documenti siano in regola','gepafin')}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="appPage__spacer"></div>
|
||||||
|
|
||||||
|
{/* ACTIONS BOTTOM (copia degli action top per comodità) */}
|
||||||
|
{!isPublished && (
|
||||||
|
<div className="appPageSection">
|
||||||
|
<div className="appPageSection__actions">
|
||||||
|
<Button type="button" icon="pi pi-save" iconPos="right"
|
||||||
|
label={__('Salva bozza','gepafin')} onClick={handleSave} disabled={!dirty} />
|
||||||
|
<Button type="button" icon="pi pi-check-circle" iconPos="right" severity="success"
|
||||||
|
label={__('Pubblica','gepafin')} onClick={handlePublish} disabled={dirty} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BandoRendicontazioneSchemaEdit;
|
||||||
145
src/modules/rendicontazione/pages/RendicontazioneHome.js
Normal file
145
src/modules/rendicontazione/pages/RendicontazioneHome.js
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
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;
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
/**
|
||||||
|
* Client HTTP per rendicontazione-api (microservizio BFLOWS).
|
||||||
|
* Usa fetch nativa come NetworkService. Il microservizio valida lo stesso JWT di GEPAFIN-BE.
|
||||||
|
*
|
||||||
|
* Env var: REACT_APP_RENDICONTAZIONE_API_URL (es. http://78.46.41.91:18090)
|
||||||
|
*/
|
||||||
|
import { storeGet } from '../../../store';
|
||||||
|
|
||||||
|
const BASE_URL = process.env.REACT_APP_RENDICONTAZIONE_API_URL || '';
|
||||||
|
|
||||||
|
const buildHeaders = () => {
|
||||||
|
const token = storeGet('getToken');
|
||||||
|
const h = { 'Content-Type': 'application/json' };
|
||||||
|
if (token) {
|
||||||
|
h['Authorization'] = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
return h;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResponse = async (response, onSuccess, onError) => {
|
||||||
|
let body = null;
|
||||||
|
try {
|
||||||
|
body = await response.json();
|
||||||
|
} catch (e) {
|
||||||
|
body = { detail: response.statusText };
|
||||||
|
}
|
||||||
|
if (response.status >= 200 && response.status < 300) {
|
||||||
|
if (onSuccess) onSuccess(body);
|
||||||
|
} else {
|
||||||
|
if (onError) onError({ status: response.status, ...body });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleError = (err, onError) => {
|
||||||
|
if (onError) onError({ status: 0, detail: err.message });
|
||||||
|
};
|
||||||
|
|
||||||
|
const RendicontazioneService = {
|
||||||
|
getSchemaByCallId(callId, onSuccess, onError) {
|
||||||
|
fetch(`${BASE_URL}/api/rendicontazione-schemas/${callId}`, {
|
||||||
|
method: 'GET', mode: 'cors', headers: buildHeaders()
|
||||||
|
})
|
||||||
|
.then(r => handleResponse(r, onSuccess, onError))
|
||||||
|
.catch(e => handleError(e, onError));
|
||||||
|
},
|
||||||
|
|
||||||
|
initializeRestartTemplate(callId, onSuccess, onError) {
|
||||||
|
fetch(`${BASE_URL}/api/rendicontazione-schemas/${callId}/initialize-restart`, {
|
||||||
|
method: 'POST', mode: 'cors', headers: buildHeaders()
|
||||||
|
})
|
||||||
|
.then(r => handleResponse(r, onSuccess, onError))
|
||||||
|
.catch(e => handleError(e, onError));
|
||||||
|
},
|
||||||
|
|
||||||
|
updateSchema(callId, schemaJson, onSuccess, onError) {
|
||||||
|
fetch(`${BASE_URL}/api/rendicontazione-schemas/${callId}`, {
|
||||||
|
method: 'PUT', mode: 'cors', headers: buildHeaders(),
|
||||||
|
body: JSON.stringify({ schema_json: schemaJson })
|
||||||
|
})
|
||||||
|
.then(r => handleResponse(r, onSuccess, onError))
|
||||||
|
.catch(e => handleError(e, onError));
|
||||||
|
},
|
||||||
|
|
||||||
|
publishSchema(callId, onSuccess, onError) {
|
||||||
|
fetch(`${BASE_URL}/api/rendicontazione-schemas/${callId}/publish`, {
|
||||||
|
method: 'POST', mode: 'cors', headers: buildHeaders()
|
||||||
|
})
|
||||||
|
.then(r => handleResponse(r, onSuccess, onError))
|
||||||
|
.catch(e => handleError(e, onError));
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteSchema(callId, onSuccess, onError) {
|
||||||
|
fetch(`${BASE_URL}/api/rendicontazione-schemas/${callId}`, {
|
||||||
|
method: 'DELETE', mode: 'cors', headers: buildHeaders()
|
||||||
|
})
|
||||||
|
.then(r => handleResponse(r, onSuccess, onError))
|
||||||
|
.catch(e => handleError(e, onError));
|
||||||
|
},
|
||||||
|
|
||||||
|
getRestartTemplatePreview(onSuccess, onError) {
|
||||||
|
fetch(`${BASE_URL}/api/rendicontazione-schemas/templates/restart`, {
|
||||||
|
method: 'GET', mode: 'cors', headers: buildHeaders()
|
||||||
|
})
|
||||||
|
.then(r => handleResponse(r, onSuccess, onError))
|
||||||
|
.catch(e => handleError(e, onError));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RendicontazioneService;
|
||||||
@@ -89,6 +89,10 @@ const BandoEdit = () => {
|
|||||||
navigate(`/bandi/${id}/flow`);
|
navigate(`/bandi/${id}/flow`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const openRendicontazioneSchema = () => {
|
||||||
|
navigate(`/bandi/${id}/rendicontazione-schema`);
|
||||||
|
}
|
||||||
|
|
||||||
const validateBando = () => {
|
const validateBando = () => {
|
||||||
storeSet('setAsyncRequest');
|
storeSet('setAsyncRequest');
|
||||||
bandoMsgs.current.clear();
|
bandoMsgs.current.clear();
|
||||||
@@ -408,6 +412,22 @@ const BandoEdit = () => {
|
|||||||
: <p>{__('Nessun modulo creato ancora', 'gepafin')}</p>}
|
: <p>{__('Nessun modulo creato ancora', 'gepafin')}</p>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="appPageSection">
|
||||||
|
<h2>{__('Schema di rendicontazione', 'gepafin')}</h2>
|
||||||
|
<p className="text-color-secondary">
|
||||||
|
{__('Configura come i beneficiari dovranno rendicontare dopo la firma del contratto: categorie di spesa, ULA, documenti richiesti.', 'gepafin')}
|
||||||
|
</p>
|
||||||
|
<div className="row">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
outlined={data.status === 'PUBLISH'}
|
||||||
|
onClick={openRendicontazioneSchema}
|
||||||
|
icon="pi pi-receipt"
|
||||||
|
iconPos="right"
|
||||||
|
label={__('Crea o modifica schema di rendicontazione', 'gepafin')}/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="appPage__spacer"></div>
|
<div className="appPage__spacer"></div>
|
||||||
|
|
||||||
<div className="appPageSection">
|
<div className="appPageSection">
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import BandoView from './pages/BandoView';
|
|||||||
import BandoFormsEdit from './pages/BandoFormsEdit';
|
import BandoFormsEdit from './pages/BandoFormsEdit';
|
||||||
import BandoForms from './pages/BandoForms';
|
import BandoForms from './pages/BandoForms';
|
||||||
import BandoFormsPreview from './pages/BandoFormsPreview';
|
import BandoFormsPreview from './pages/BandoFormsPreview';
|
||||||
|
import BandoRendicontazioneSchemaEdit from './modules/rendicontazione/pages/BandoRendicontazioneSchemaEdit';
|
||||||
|
import RendicontazioneHome from './modules/rendicontazione/pages/RendicontazioneHome';
|
||||||
import BandoFlowEdit from './pages/BandoFlowEdit';
|
import BandoFlowEdit from './pages/BandoFlowEdit';
|
||||||
import Imieibandi from './pages/Imieibandi';
|
import Imieibandi from './pages/Imieibandi';
|
||||||
import BandoApplication from './pages/BandoApplication';
|
import BandoApplication from './pages/BandoApplication';
|
||||||
@@ -135,6 +137,20 @@ const routes = ({ role, chosenCompanyId }) => {
|
|||||||
{'ROLE_PRE_INSTRUCTOR' === role ? <PageNotFound/> : null}
|
{'ROLE_PRE_INSTRUCTOR' === role ? <PageNotFound/> : null}
|
||||||
{'ROLE_INSTRUCTOR_MANAGER' === role ? <PageNotFound/> : null}
|
{'ROLE_INSTRUCTOR_MANAGER' === role ? <PageNotFound/> : null}
|
||||||
</DefaultLayout>}/>
|
</DefaultLayout>}/>
|
||||||
|
<Route path="/rendicontazione" element={<DefaultLayout>
|
||||||
|
{'ROLE_SUPER_ADMIN' === role ? <RendicontazioneHome/> : <PageNotFound/>}
|
||||||
|
{'ROLE_BENEFICIARY' === role ? <PageNotFound/> : null}
|
||||||
|
{'ROLE_CONFIDI' === role ? <PageNotFound/> : null}
|
||||||
|
{'ROLE_PRE_INSTRUCTOR' === role ? <PageNotFound/> : null}
|
||||||
|
{'ROLE_INSTRUCTOR_MANAGER' === role ? <PageNotFound/> : null}
|
||||||
|
</DefaultLayout>}/>
|
||||||
|
<Route path="/bandi/:id/rendicontazione-schema" element={<DefaultLayout>
|
||||||
|
{'ROLE_SUPER_ADMIN' === role ? <BandoRendicontazioneSchemaEdit/> : <PageNotFound/>}
|
||||||
|
{'ROLE_BENEFICIARY' === role ? <PageNotFound/> : null}
|
||||||
|
{'ROLE_CONFIDI' === role ? <PageNotFound/> : null}
|
||||||
|
{'ROLE_PRE_INSTRUCTOR' === role ? <PageNotFound/> : null}
|
||||||
|
{'ROLE_INSTRUCTOR_MANAGER' === role ? <PageNotFound/> : null}
|
||||||
|
</DefaultLayout>}/>
|
||||||
<Route path="/bandi-osservati" element={<DefaultLayout>
|
<Route path="/bandi-osservati" element={<DefaultLayout>
|
||||||
{'ROLE_SUPER_ADMIN' === role ? <PageNotFound/> : null}
|
{'ROLE_SUPER_ADMIN' === role ? <PageNotFound/> : null}
|
||||||
{'ROLE_BENEFICIARY' === role ? <BandiPreferredBeneficiario/> : null}
|
{'ROLE_BENEFICIARY' === role ? <BandiPreferredBeneficiario/> : null}
|
||||||
|
|||||||
Reference in New Issue
Block a user