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:
@@ -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',
|
||||
|
||||
83
src/modules/rendicontazione/pages/DevSwitchUser.js
Normal file
83
src/modules/rendicontazione/pages/DevSwitchUser.js
Normal file
@@ -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 (
|
||||
<div className="appPage">
|
||||
<Toast ref={toast} />
|
||||
<div className="appPage__pageHeader">
|
||||
<h1>{__('Dev: cambia utente', 'gepafin')}</h1>
|
||||
<p>{__('Pagina sandbox. Permette di impersonare un utente (es. beneficiario) senza passare per SPID.', 'gepafin')}</p>
|
||||
</div>
|
||||
|
||||
<div className="appPage__spacer"></div>
|
||||
|
||||
<div className="appPageSection">
|
||||
<Card style={{ width: '100%', maxWidth: '500px' }}>
|
||||
<form className="appForm p-fluid" onSubmit={(e) => { e.preventDefault(); doImpersonate(); }}>
|
||||
<div className="appForm__field">
|
||||
<label>{__('Email utente da impersonare', 'gepafin')}</label>
|
||||
<InputText value={email} onChange={(e) => setEmail(e.target.value)} />
|
||||
<small className="text-color-secondary">
|
||||
{__('Prova: beneficiario@sandbox.local oppure admin@sandbox.local', 'gepafin')}
|
||||
</small>
|
||||
</div>
|
||||
<div style={{ marginTop: '1rem', display: 'flex', gap: '0.75rem' }}>
|
||||
<Button type="button" outlined label={__('Indietro', 'gepafin')}
|
||||
onClick={() => navigate('/')} />
|
||||
<Button type="submit" label={__('Impersona', 'gepafin')} icon="pi pi-user-edit"
|
||||
loading={loading} severity="warning" />
|
||||
</div>
|
||||
</form>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DevSwitchUser;
|
||||
641
src/modules/rendicontazione/pages/PraticaRendicontazioneEdit.js
Normal file
641
src/modules/rendicontazione/pages/PraticaRendicontazioneEdit.js
Normal file
@@ -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 <div className="appPage"><div className="appPageSection"><Skeleton width="100%" height="15rem" /></div></div>;
|
||||
}
|
||||
if (!practice) {
|
||||
return <div className="appPage"><div className="appPageSection"><p>{__('Pratica non trovata', 'gepafin')}</p></div></div>;
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="appPage">
|
||||
<Toast ref={toast} />
|
||||
<ConfirmPopup />
|
||||
|
||||
{/* HEADER */}
|
||||
<div className="appPage__pageHeader">
|
||||
<h1>{__('Rendicontazione', 'gepafin')}</h1>
|
||||
<p>
|
||||
<span className="companyName">
|
||||
{practice.schema_snapshot?.template_label || `Bando #${practice.call_id}`}
|
||||
</span>
|
||||
<span style={{ marginLeft: '1rem' }}>
|
||||
<Tag severity={statusCfg.severity} value={statusCfg.label} />
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="appPage__spacer"></div>
|
||||
|
||||
{/* ACTIONS */}
|
||||
<div className="appPageSection">
|
||||
<div className="appPageSection__actions">
|
||||
<Button type="button" outlined icon="pi pi-arrow-left"
|
||||
label={__('Torna alla lista', 'gepafin')} onClick={() => navigate('/rendicontazioni')} />
|
||||
{!readOnly && (
|
||||
<Button type="button" icon="pi pi-send" iconPos="right" severity="success"
|
||||
label={__('Invia rendicontazione', 'gepafin')}
|
||||
onClick={handleSubmit}
|
||||
disabled={!gate?.passed}
|
||||
tooltip={!gate?.passed ? __('Completa tutti i requisiti prima di inviare', 'gepafin') : null} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="appPage__spacer"></div>
|
||||
|
||||
{/* RIEPILOGO FINANZIARIO */}
|
||||
<div className="appPageSection" style={{ background: 'var(--surface-50)', padding: '1.25rem', borderRadius: '6px' }}>
|
||||
<h2 style={{ margin: '0 0 0.5rem 0' }}>{__('Riepilogo', 'gepafin')}</h2>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(160px, 1fr))', gap: '1rem', width: '100%' }}>
|
||||
<div>
|
||||
<small className="text-color-secondary">{__('Importo erogato', 'gepafin')}</small>
|
||||
<div style={{ fontSize: '1.25rem', fontWeight: 700 }}>{euro(practice.amount_erogato)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<small className="text-color-secondary">{__('Totale fatture rendicontate', 'gepafin')}</small>
|
||||
<div style={{ fontSize: '1.25rem', fontWeight: 700 }}>{euro(grandTotal)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<small className="text-color-secondary">{__('Cap remissione massimo', 'gepafin')}</small>
|
||||
<div style={{ fontSize: '1.25rem', fontWeight: 700 }}>{euro(maxRemission)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<small className="text-color-secondary">{__('Remissione spettante', 'gepafin')}</small>
|
||||
<div style={{ fontSize: '1.5rem', fontWeight: 700, color: 'var(--primary-color)' }}>{euro(remissionDue)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="appPage__spacer"></div>
|
||||
|
||||
{/* GATE CHECKS */}
|
||||
{gate && (
|
||||
<div className="appPageSection">
|
||||
<h2>{__('Requisiti per invio', 'gepafin')}</h2>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem', width: '100%' }}>
|
||||
{gate.checks.map((c, i) => (
|
||||
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
|
||||
<i className={c.passed ? 'pi pi-check-circle' : 'pi pi-times-circle'}
|
||||
style={{ color: c.passed ? 'var(--green-500)' : 'var(--orange-500)', fontSize: '1.25rem' }} />
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontWeight: 600 }}>{c.label}</div>
|
||||
<small className="text-color-secondary">{c.detail}</small>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="appPage__spacer"></div>
|
||||
|
||||
{/* SEZIONE 1: REGIME IVA */}
|
||||
<div className="appPageSection">
|
||||
<h2>{__('1. Regime IVA', 'gepafin')}</h2>
|
||||
<form className="appForm p-fluid" onSubmit={(e) => e.preventDefault()}>
|
||||
<div className="appForm__field" style={{ maxWidth: '500px' }}>
|
||||
<label>{__('Seleziona il tuo regime IVA', 'gepafin')}</label>
|
||||
<Dropdown value={practice.iva_regime}
|
||||
onChange={(e) => updateIvaRegime(e.value)}
|
||||
options={ivaAllowed}
|
||||
placeholder={__('Seleziona...', 'gepafin')}
|
||||
disabled={readOnly} />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className="appPage__spacer"></div>
|
||||
|
||||
{/* SEZIONE 2: FATTURE PER CATEGORIA */}
|
||||
<div className="appPageSection">
|
||||
<h2>{__('2. Fatture per categoria', 'gepafin')}</h2>
|
||||
<p className="text-color-secondary" style={{ marginTop: 0 }}>
|
||||
{__('Carica le fatture assegnandole alla categoria di spesa appropriata. I totali si aggiornano in tempo reale.', 'gepafin')}
|
||||
</p>
|
||||
|
||||
<div className="fieldsRepeater">
|
||||
{categories.map((cat) => {
|
||||
const invs = invoicesOfCat(cat.code);
|
||||
const catTotal = perCategory[cat.code] || 0;
|
||||
return (
|
||||
<div key={cat.code} className="fieldsRepeater__panel"
|
||||
style={{ border: '1px solid var(--surface-border)', borderRadius: '6px', padding: '1rem' }}>
|
||||
<div className="fieldsRepeater__heading" style={{ marginBottom: '0.75rem' }}>
|
||||
<div>
|
||||
<strong style={{ color: 'var(--primary-color)' }}>{cat.code}</strong> — {cat.label}
|
||||
<div><small className="text-color-secondary">{cat.description}</small></div>
|
||||
</div>
|
||||
<div style={{ textAlign: 'right' }}>
|
||||
<div><strong>{euro(catTotal)}</strong></div>
|
||||
<small className="text-color-secondary">{invs.length} {__('fatture', 'gepafin')}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{invs.length > 0 && (
|
||||
<DataTable value={invs} dataKey="id" size="small" responsiveLayout="scroll">
|
||||
<Column field="invoice_number" header={__('N°', 'gepafin')} />
|
||||
<Column field="invoice_date" header={__('Data', 'gepafin')}
|
||||
body={(r) => formatDate(r.invoice_date)} />
|
||||
<Column field="supplier_name" header={__('Fornitore', 'gepafin')} />
|
||||
<Column field="description" header={__('Descrizione', 'gepafin')}
|
||||
body={(r) => <span title={r.description}>{r.description.slice(0, 40)}{r.description.length > 40 ? '…' : ''}</span>} />
|
||||
<Column field="taxable" header={__('Imponibile', 'gepafin')} body={(r) => euro(r.taxable)} />
|
||||
<Column field="total" header={__('Totale', 'gepafin')} body={(r) => euro(r.total)} />
|
||||
{!readOnly && (
|
||||
<Column header="" body={(r) => (
|
||||
<Button icon="pi pi-trash" severity="danger" outlined size="small"
|
||||
onClick={(e) => deleteInvoice(e, r)} />
|
||||
)} style={{ width: '60px' }} />
|
||||
)}
|
||||
</DataTable>
|
||||
)}
|
||||
|
||||
{!readOnly && (
|
||||
<div style={{ marginTop: '0.75rem' }}>
|
||||
<Button type="button" icon="pi pi-plus" outlined size="small"
|
||||
label={__('Aggiungi fattura', 'gepafin') + ' ' + cat.code}
|
||||
onClick={() => openAddInvoice(cat.code)} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* SEZIONE 3: ULA */}
|
||||
{ulaSection.enabled && (<>
|
||||
<div className="appPage__spacer"></div>
|
||||
<div className="appPageSection">
|
||||
<h2>{__('3. Calcolo ULA — Dipendenti', 'gepafin')}</h2>
|
||||
<p className="text-color-secondary" style={{ marginTop: 0 }}>
|
||||
{__('Inserisci i dipendenti che contano per l\'incremento occupazionale. Soglia minima richiesta:', 'gepafin')} <strong>{ulaSection.threshold}</strong>.
|
||||
</p>
|
||||
|
||||
{practice.ula_employees.length > 0 && (
|
||||
<DataTable value={practice.ula_employees} dataKey="id" size="small" responsiveLayout="scroll" style={{ width: '100%', marginBottom: '0.75rem' }}>
|
||||
<Column field="codice_fiscale" header="CF" />
|
||||
<Column field="full_name" header={__('Nome', 'gepafin')} />
|
||||
<Column field="contract_type" header={__('Contratto', 'gepafin')}
|
||||
body={(r) => (CONTRACT_TYPES.find(c => c.value === r.contract_type)?.label || r.contract_type)} />
|
||||
<Column field="fte_pct" header="FTE" body={(r) => Number(r.fte_pct).toFixed(2)} />
|
||||
<Column header={__('Periodo', 'gepafin')}
|
||||
body={(r) => `${formatDate(r.period_start_date)} → ${formatDate(r.period_end_date)}`} />
|
||||
<Column field="supporting_doc_filename" header={__('Allegato', 'gepafin')}
|
||||
body={(r) => r.supporting_doc_filename ? <span><i className="pi pi-file" /> {r.supporting_doc_filename}</span> : <span className="text-color-secondary">—</span>} />
|
||||
{!readOnly && (
|
||||
<Column header="" body={(r) => (
|
||||
<Button icon="pi pi-trash" severity="danger" outlined size="small"
|
||||
onClick={(e) => deleteEmployee(e, r)} />
|
||||
)} style={{ width: '60px' }} />
|
||||
)}
|
||||
</DataTable>
|
||||
)}
|
||||
|
||||
{!readOnly && (
|
||||
<Button type="button" icon="pi pi-plus" outlined size="small"
|
||||
label={__('Aggiungi dipendente', 'gepafin')} onClick={openAddEmployee} />
|
||||
)}
|
||||
</div>
|
||||
</>)}
|
||||
|
||||
<div className="appPage__spacer"></div>
|
||||
|
||||
{/* SEZIONE 4: DOCUMENTI */}
|
||||
<div className="appPageSection">
|
||||
<h2>{__((ulaSection.enabled ? '4.' : '3.') + ' Documenti richiesti', 'gepafin')}</h2>
|
||||
<p className="text-color-secondary" style={{ marginTop: 0 }}>
|
||||
{__('Carica un file per ciascun documento richiesto. In questa sandbox viene registrato solo il nome del file (upload reale al prossimo sprint).', 'gepafin')}
|
||||
</p>
|
||||
<div className="fieldsRepeater">
|
||||
{docsRequired.map((dr) => {
|
||||
const existing = practice.documents.find(d => d.doc_code === dr.code);
|
||||
return (
|
||||
<div key={dr.code} className="fieldsRepeater__panel"
|
||||
style={{ border: '1px solid var(--surface-border)', borderRadius: '6px', padding: '0.75rem 1rem',
|
||||
display: 'flex', alignItems: 'center', gap: '1rem', flexWrap: 'wrap' }}>
|
||||
<i className={existing?.filename ? 'pi pi-check-circle' : 'pi pi-circle'}
|
||||
style={{ color: existing?.filename ? 'var(--green-500)' : 'var(--text-color-secondary)', fontSize: '1.25rem' }} />
|
||||
<div style={{ flex: 1, minWidth: '200px' }}>
|
||||
<strong>{dr.label}</strong>
|
||||
<div><small className="text-color-secondary"><code>{dr.code}</code></small></div>
|
||||
</div>
|
||||
<div style={{ flex: 2, minWidth: '220px' }}>
|
||||
{existing?.filename
|
||||
? <span><i className="pi pi-file" /> {existing.filename}</span>
|
||||
: <span className="text-color-secondary">{__('Nessun file', 'gepafin')}</span>}
|
||||
</div>
|
||||
{!readOnly && (
|
||||
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
||||
<Button type="button" icon="pi pi-upload" size="small"
|
||||
label={existing?.filename ? __('Sostituisci', 'gepafin') : __('Carica', 'gepafin')}
|
||||
outlined={!!existing?.filename}
|
||||
onClick={() => {
|
||||
const fname = prompt(__('Nome del file (simulato)', 'gepafin'),
|
||||
existing?.filename || `${dr.code}.pdf`);
|
||||
if (fname) upsertDocument(dr.code, fname);
|
||||
}} />
|
||||
{existing?.filename && (
|
||||
<Button type="button" icon="pi pi-trash" severity="danger" outlined size="small"
|
||||
onClick={() => clearDocument(dr.code)} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="appPage__spacer"></div>
|
||||
|
||||
{/* BOTTOM ACTIONS */}
|
||||
{!readOnly && (
|
||||
<div className="appPageSection">
|
||||
<div className="appPageSection__actions">
|
||||
<Button type="button" icon="pi pi-send" iconPos="right" severity="success"
|
||||
label={__('Invia rendicontazione', 'gepafin')}
|
||||
onClick={handleSubmit}
|
||||
disabled={!gate?.passed} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ---------- DIALOG FATTURA ---------- */}
|
||||
<Dialog visible={invDialog.visible} style={{ width: '640px', maxWidth: '95vw' }}
|
||||
header={__('Aggiungi fattura', 'gepafin') + (invDialog.data?.category_code ? ` — ${invDialog.data.category_code}` : '')}
|
||||
modal onHide={() => setInvDialog({ visible: false, data: null })}>
|
||||
{invDialog.data && (
|
||||
<form className="appForm p-fluid" onSubmit={(e) => { e.preventDefault(); saveInvoice(); }}>
|
||||
<div className="appForm__cols">
|
||||
<div className="appForm__field">
|
||||
<label>{__('Numero fattura', 'gepafin')}</label>
|
||||
<InputText value={invDialog.data.invoice_number}
|
||||
onChange={(e) => setInvDialog(d => ({ ...d, data: { ...d.data, invoice_number: e.target.value } }))} />
|
||||
</div>
|
||||
<div className="appForm__field">
|
||||
<label>{__('Data fattura', 'gepafin')}</label>
|
||||
<Calendar value={invDialog.data.invoice_date} dateFormat="dd/mm/yy" showIcon
|
||||
onChange={(e) => setInvDialog(d => ({ ...d, data: { ...d.data, invoice_date: e.value } }))} />
|
||||
</div>
|
||||
<div className="appForm__field">
|
||||
<label>{__('Data pagamento', 'gepafin')}</label>
|
||||
<Calendar value={invDialog.data.payment_date} dateFormat="dd/mm/yy" showIcon
|
||||
onChange={(e) => setInvDialog(d => ({ ...d, data: { ...d.data, payment_date: e.value } }))} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="appForm__cols">
|
||||
<div className="appForm__field">
|
||||
<label>{__('Fornitore', 'gepafin')}</label>
|
||||
<InputText value={invDialog.data.supplier_name}
|
||||
onChange={(e) => setInvDialog(d => ({ ...d, data: { ...d.data, supplier_name: e.target.value } }))} />
|
||||
</div>
|
||||
<div className="appForm__field">
|
||||
<label>{__('P. IVA fornitore', 'gepafin')}</label>
|
||||
<InputText value={invDialog.data.supplier_vat}
|
||||
onChange={(e) => setInvDialog(d => ({ ...d, data: { ...d.data, supplier_vat: e.target.value } }))} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="appForm__field">
|
||||
<label>{__('Descrizione', 'gepafin')}</label>
|
||||
<InputTextarea value={invDialog.data.description} rows={2} autoResize
|
||||
onChange={(e) => setInvDialog(d => ({ ...d, data: { ...d.data, description: e.target.value } }))} />
|
||||
</div>
|
||||
<div className="appForm__cols">
|
||||
<div className="appForm__field">
|
||||
<label>{__('Imponibile (€)', 'gepafin')}</label>
|
||||
<InputNumber value={invDialog.data.taxable} mode="currency" currency="EUR" locale="it-IT"
|
||||
onValueChange={(e) => setInvDialog(d => ({ ...d, data: { ...d.data, taxable: e.value } }))} />
|
||||
</div>
|
||||
<div className="appForm__field">
|
||||
<label>{__('IVA (€)', 'gepafin')}</label>
|
||||
<InputNumber value={invDialog.data.vat} mode="currency" currency="EUR" locale="it-IT"
|
||||
onValueChange={(e) => setInvDialog(d => ({ ...d, data: { ...d.data, vat: e.value } }))} />
|
||||
</div>
|
||||
<div className="appForm__field">
|
||||
<label>{__('Totale (€)', 'gepafin')}</label>
|
||||
<InputNumber value={invDialog.data.total} mode="currency" currency="EUR" locale="it-IT"
|
||||
onValueChange={(e) => setInvDialog(d => ({ ...d, data: { ...d.data, total: e.value } }))} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="appForm__field">
|
||||
<label>{__('Nome file PDF (simulato)', 'gepafin')}</label>
|
||||
<InputText value={invDialog.data.pdf_filename}
|
||||
onChange={(e) => setInvDialog(d => ({ ...d, data: { ...d.data, pdf_filename: e.target.value } }))} />
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '0.75rem', marginTop: '1rem' }}>
|
||||
<Button type="button" outlined label={__('Annulla', 'gepafin')} onClick={() => setInvDialog({ visible: false, data: null })} />
|
||||
<Button type="submit" label={__('Aggiungi', 'gepafin')} icon="pi pi-check" />
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</Dialog>
|
||||
|
||||
{/* ---------- DIALOG DIPENDENTE ULA ---------- */}
|
||||
<Dialog visible={empDialog.visible} style={{ width: '620px', maxWidth: '95vw' }}
|
||||
header={__('Aggiungi dipendente', 'gepafin')}
|
||||
modal onHide={() => setEmpDialog({ visible: false, data: null })}>
|
||||
{empDialog.data && (
|
||||
<form className="appForm p-fluid" onSubmit={(e) => { e.preventDefault(); saveEmployee(); }}>
|
||||
<div className="appForm__cols">
|
||||
<div className="appForm__field">
|
||||
<label>{__('Codice fiscale', 'gepafin')}</label>
|
||||
<InputText value={empDialog.data.codice_fiscale}
|
||||
onChange={(e) => setEmpDialog(d => ({ ...d, data: { ...d.data, codice_fiscale: e.target.value.toUpperCase() } }))} />
|
||||
</div>
|
||||
<div className="appForm__field">
|
||||
<label>{__('Nome e cognome', 'gepafin')}</label>
|
||||
<InputText value={empDialog.data.full_name}
|
||||
onChange={(e) => setEmpDialog(d => ({ ...d, data: { ...d.data, full_name: e.target.value } }))} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="appForm__cols">
|
||||
<div className="appForm__field">
|
||||
<label>{__('Tipo contratto', 'gepafin')}</label>
|
||||
<Dropdown value={empDialog.data.contract_type} options={CONTRACT_TYPES}
|
||||
onChange={(e) => setEmpDialog(d => ({ ...d, data: { ...d.data, contract_type: e.value } }))} />
|
||||
</div>
|
||||
<div className="appForm__field">
|
||||
<label>{__('Mansione (opzionale)', 'gepafin')}</label>
|
||||
<InputText value={empDialog.data.role_description}
|
||||
onChange={(e) => setEmpDialog(d => ({ ...d, data: { ...d.data, role_description: e.target.value } }))} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="appForm__cols">
|
||||
<div className="appForm__field">
|
||||
<label>{__('Percentuale tempo (0-1)', 'gepafin')}</label>
|
||||
<InputNumber value={empDialog.data.fte_pct} mode="decimal" minFractionDigits={2} maxFractionDigits={4} min={0} max={1}
|
||||
onValueChange={(e) => setEmpDialog(d => ({ ...d, data: { ...d.data, fte_pct: e.value } }))} />
|
||||
</div>
|
||||
<div className="appForm__field">
|
||||
<label>{__('Inizio periodo', 'gepafin')}</label>
|
||||
<Calendar value={empDialog.data.period_start_date} dateFormat="dd/mm/yy" showIcon
|
||||
onChange={(e) => setEmpDialog(d => ({ ...d, data: { ...d.data, period_start_date: e.value } }))} />
|
||||
</div>
|
||||
<div className="appForm__field">
|
||||
<label>{__('Fine periodo', 'gepafin')}</label>
|
||||
<Calendar value={empDialog.data.period_end_date} dateFormat="dd/mm/yy" showIcon
|
||||
onChange={(e) => setEmpDialog(d => ({ ...d, data: { ...d.data, period_end_date: e.value } }))} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="appForm__cols">
|
||||
<div className="appForm__field">
|
||||
<label>{__('Tipo documento di supporto', 'gepafin')}</label>
|
||||
<Dropdown value={empDialog.data.supporting_doc_type}
|
||||
options={(ulaSection.supporting_doc_types || []).map(t => typeof t === 'string' ? { value: t, label: t } : { value: t.code, label: t.label })}
|
||||
onChange={(e) => setEmpDialog(d => ({ ...d, data: { ...d.data, supporting_doc_type: e.value } }))} />
|
||||
</div>
|
||||
<div className="appForm__field">
|
||||
<label>{__('Nome file allegato (simulato)', 'gepafin')}</label>
|
||||
<InputText value={empDialog.data.supporting_doc_filename}
|
||||
onChange={(e) => setEmpDialog(d => ({ ...d, data: { ...d.data, supporting_doc_filename: e.target.value } }))} />
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '0.75rem', marginTop: '1rem' }}>
|
||||
<Button type="button" outlined label={__('Annulla', 'gepafin')} onClick={() => setEmpDialog({ visible: false, data: null })} />
|
||||
<Button type="submit" label={__('Aggiungi', 'gepafin')} icon="pi pi-check" />
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PraticaRendicontazioneEdit;
|
||||
133
src/modules/rendicontazione/pages/RendicontazioniMie.js
Normal file
133
src/modules/rendicontazione/pages/RendicontazioniMie.js
Normal 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;
|
||||
@@ -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);
|
||||
|
||||
@@ -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 ? <PageNotFound/> : null}
|
||||
{'ROLE_INSTRUCTOR_MANAGER' === role ? <PageNotFound/> : null}
|
||||
</DefaultLayout>}/>
|
||||
<Route path="/rendicontazioni" element={<DefaultLayout>
|
||||
{'ROLE_BENEFICIARY' === role ? <RendicontazioniMie/> : null}
|
||||
{'ROLE_SUPER_ADMIN' === role ? <RendicontazioniMie/> : null}
|
||||
{'ROLE_CONFIDI' === role ? <PageNotFound/> : null}
|
||||
{'ROLE_PRE_INSTRUCTOR' === role ? <PageNotFound/> : null}
|
||||
{'ROLE_INSTRUCTOR_MANAGER' === role ? <PageNotFound/> : null}
|
||||
</DefaultLayout>}/>
|
||||
<Route path="/rendicontazioni/:id" element={<DefaultLayout>
|
||||
{'ROLE_BENEFICIARY' === role ? <PraticaRendicontazioneEdit/> : null}
|
||||
{'ROLE_SUPER_ADMIN' === role ? <PraticaRendicontazioneEdit/> : null}
|
||||
{'ROLE_CONFIDI' === role ? <PageNotFound/> : null}
|
||||
{'ROLE_PRE_INSTRUCTOR' === role ? <PageNotFound/> : null}
|
||||
{'ROLE_INSTRUCTOR_MANAGER' === role ? <PageNotFound/> : null}
|
||||
</DefaultLayout>}/>
|
||||
<Route path="/dev-switch-user" element={<DefaultLayout>
|
||||
{'ROLE_SUPER_ADMIN' === role ? <DevSwitchUser/> : <PageNotFound/>}
|
||||
</DefaultLayout>}/>
|
||||
<Route path="/bandi-osservati" element={<DefaultLayout>
|
||||
{'ROLE_SUPER_ADMIN' === role ? <PageNotFound/> : null}
|
||||
{'ROLE_BENEFICIARY' === role ? <BandiPreferredBeneficiario/> : null}
|
||||
|
||||
Reference in New Issue
Block a user