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')}
+
+
+
+
+
+
+ );
+};
+
+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 */}
+
+
+ navigate('/rendicontazioni')} />
+ {!readOnly && (
+
+ )}
+
+
+
+
+
+ {/* 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')}
+
+
+
+
+
+ {/* 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 && (
+ (
+ deleteInvoice(e, r)} />
+ )} style={{ width: '60px' }} />
+ )}
+
+ )}
+
+ {!readOnly && (
+
+ openAddInvoice(cat.code)} />
+
+ )}
+
+ );
+ })}
+
+
+
+ {/* 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 && (
+ (
+ deleteEmployee(e, r)} />
+ )} style={{ width: '60px' }} />
+ )}
+
+ )}
+
+ {!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 && (
+
+ {
+ const fname = prompt(__('Nome del file (simulato)', 'gepafin'),
+ existing?.filename || `${dr.code}.pdf`);
+ if (fname) upsertDocument(dr.code, fname);
+ }} />
+ {existing?.filename && (
+ clearDocument(dr.code)} />
+ )}
+
+ )}
+
+ );
+ })}
+
+
+
+
+
+ {/* BOTTOM ACTIONS */}
+ {!readOnly && (
+
+ )}
+
+ {/* ---------- DIALOG FATTURA ---------- */}
+
setInvDialog({ visible: false, data: null })}>
+ {invDialog.data && (
+
+ )}
+
+
+ {/* ---------- DIALOG DIPENDENTE ULA ---------- */}
+
setEmpDialog({ visible: false, data: null })}>
+ {empDialog.data && (
+
+ )}
+
+
+ );
+};
+
+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 handleStart(row.application_id)} />;
+ }
+ const isEditable = row.status === 'DRAFT';
+ return navigate(`/rendicontazioni/${row.id}`)} />;
+ };
+
+ return (
+
+
+
+
+
{__('Le mie rendicontazioni', 'gepafin')}
+
{__('Per ogni pratica finanziata puoi avviare la rendicontazione delle spese e il calcolo della remissione del debito.', 'gepafin')}
+
+
+
+
+
+ {loading &&
}
+ {!loading && rows.length === 0 && (
+
+
+
{__('Non ci sono rendicontazioni da avviare al momento.', 'gepafin')}
+
+ {__('Le rendicontazioni diventano disponibili dopo la firma del contratto e quando l\'ente ha pubblicato lo schema di rendicontazione per il bando.', 'gepafin')}
+
+
+ )}
+ {!loading && rows.length > 0 && (
+
+
+
+
+
+
+
+ )}
+
+
+ );
+};
+
+export default RendicontazioniMie;
diff --git a/src/modules/rendicontazione/service/rendicontazioneService.js b/src/modules/rendicontazione/service/rendicontazioneService.js
index 0fe1349..57fdcd5 100644
--- a/src/modules/rendicontazione/service/rendicontazioneService.js
+++ b/src/modules/rendicontazione/service/rendicontazioneService.js
@@ -87,3 +87,95 @@ const RendicontazioneService = {
};
export default RendicontazioneService;
+
+// ====================== PRATICHE BENEFICIARIO ======================
+
+const extendPractice = {
+ listMine(onSuccess, onError) {
+ fetch(`${BASE_URL}/api/remission-practices/mine`, {
+ method: 'GET', mode: 'cors', headers: buildHeaders()
+ }).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
+ },
+
+ startPractice(applicationId, onSuccess, onError) {
+ fetch(`${BASE_URL}/api/remission-practices/start`, {
+ method: 'POST', mode: 'cors', headers: buildHeaders(),
+ body: JSON.stringify({ application_id: applicationId })
+ }).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
+ },
+
+ getPractice(practiceId, onSuccess, onError) {
+ fetch(`${BASE_URL}/api/remission-practices/${practiceId}`, {
+ method: 'GET', mode: 'cors', headers: buildHeaders()
+ }).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
+ },
+
+ updatePractice(practiceId, patch, onSuccess, onError) {
+ fetch(`${BASE_URL}/api/remission-practices/${practiceId}`, {
+ method: 'PUT', mode: 'cors', headers: buildHeaders(),
+ body: JSON.stringify(patch)
+ }).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
+ },
+
+ addInvoice(practiceId, invoice, onSuccess, onError) {
+ fetch(`${BASE_URL}/api/remission-practices/${practiceId}/invoices`, {
+ method: 'POST', mode: 'cors', headers: buildHeaders(),
+ body: JSON.stringify(invoice)
+ }).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
+ },
+
+ deleteInvoice(practiceId, invoiceId, onSuccess, onError) {
+ fetch(`${BASE_URL}/api/remission-practices/${practiceId}/invoices/${invoiceId}`, {
+ method: 'DELETE', mode: 'cors', headers: buildHeaders()
+ }).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
+ },
+
+ addUlaEmployee(practiceId, emp, onSuccess, onError) {
+ fetch(`${BASE_URL}/api/remission-practices/${practiceId}/ula-employees`, {
+ method: 'POST', mode: 'cors', headers: buildHeaders(),
+ body: JSON.stringify(emp)
+ }).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
+ },
+
+ deleteUlaEmployee(practiceId, empId, onSuccess, onError) {
+ fetch(`${BASE_URL}/api/remission-practices/${practiceId}/ula-employees/${empId}`, {
+ method: 'DELETE', mode: 'cors', headers: buildHeaders()
+ }).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
+ },
+
+ upsertDocument(practiceId, docCode, payload, onSuccess, onError) {
+ fetch(`${BASE_URL}/api/remission-practices/${practiceId}/documents/${docCode}`, {
+ method: 'PUT', mode: 'cors', headers: buildHeaders(),
+ body: JSON.stringify({ doc_code: docCode, ...payload })
+ }).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
+ },
+
+ clearDocument(practiceId, docCode, onSuccess, onError) {
+ fetch(`${BASE_URL}/api/remission-practices/${practiceId}/documents/${docCode}`, {
+ method: 'DELETE', mode: 'cors', headers: buildHeaders()
+ }).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
+ },
+
+ gateCheck(practiceId, onSuccess, onError) {
+ fetch(`${BASE_URL}/api/remission-practices/${practiceId}/gate-check`, {
+ method: 'GET', mode: 'cors', headers: buildHeaders()
+ }).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
+ },
+
+ submitPractice(practiceId, onSuccess, onError) {
+ fetch(`${BASE_URL}/api/remission-practices/${practiceId}/submit`, {
+ method: 'POST', mode: 'cors', headers: buildHeaders()
+ }).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
+ },
+
+ // dev-only: impersonation per test beneficiary
+ impersonate(email, onSuccess, onError) {
+ fetch(`${BASE_URL}/api/debug/impersonate`, {
+ method: 'POST', mode: 'cors', headers: buildHeaders(),
+ body: JSON.stringify({ email })
+ }).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
+ }
+};
+
+// Attach to main export
+Object.assign(RendicontazioneService, extendPractice);
diff --git a/src/routes.js b/src/routes.js
index ffb196e..d287960 100644
--- a/src/routes.js
+++ b/src/routes.js
@@ -16,6 +16,9 @@ import BandoForms from './pages/BandoForms';
import BandoFormsPreview from './pages/BandoFormsPreview';
import BandoRendicontazioneSchemaEdit from './modules/rendicontazione/pages/BandoRendicontazioneSchemaEdit';
import RendicontazioneHome from './modules/rendicontazione/pages/RendicontazioneHome';
+import RendicontazioniMie from './modules/rendicontazione/pages/RendicontazioniMie';
+import PraticaRendicontazioneEdit from './modules/rendicontazione/pages/PraticaRendicontazioneEdit';
+import DevSwitchUser from './modules/rendicontazione/pages/DevSwitchUser';
import BandoFlowEdit from './pages/BandoFlowEdit';
import Imieibandi from './pages/Imieibandi';
import BandoApplication from './pages/BandoApplication';
@@ -151,6 +154,23 @@ const routes = ({ role, chosenCompanyId }) => {
{'ROLE_PRE_INSTRUCTOR' === role ? : null}
{'ROLE_INSTRUCTOR_MANAGER' === role ? : null}
}/>
+
+ {'ROLE_BENEFICIARY' === role ? : null}
+ {'ROLE_SUPER_ADMIN' === role ? : null}
+ {'ROLE_CONFIDI' === role ? : null}
+ {'ROLE_PRE_INSTRUCTOR' === role ? : null}
+ {'ROLE_INSTRUCTOR_MANAGER' === role ? : null}
+ }/>
+
+ {'ROLE_BENEFICIARY' === role ? : null}
+ {'ROLE_SUPER_ADMIN' === role ? : null}
+ {'ROLE_CONFIDI' === role ? : null}
+ {'ROLE_PRE_INSTRUCTOR' === role ? : null}
+ {'ROLE_INSTRUCTOR_MANAGER' === role ? : null}
+ }/>
+
+ {'ROLE_SUPER_ADMIN' === role ? : }
+ }/>
{'ROLE_SUPER_ADMIN' === role ? : null}
{'ROLE_BENEFICIARY' === role ? : null}