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'; import FileUploadCell from '../components/FileUploadCell'; import FilePreviewDialog from '../components/FilePreviewDialog'; // ---------- 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 }); // modal risposta soccorso istruttorio const [amendDialog, setAmendDialog] = useState({ visible: false, amendment: null, responseText: '' }); // preview file const [previewDialog, setPreviewDialog] = useState({ visible: false, entityType: null, entityId: null, filename: null, title: null }); const openPreview = (entityType, entityId, title, filename) => setPreviewDialog({ visible: true, entityType, entityId, title, filename }); const closePreview = () => setPreviewDialog({ visible: false, entityType: null, entityId: null, filename: null, title: null }); // update locale riga dopo upload/delete const updateInvoiceFile = (invoiceId, fileMeta) => { setPractice(p => p ? { ...p, invoices: p.invoices.map(i => i.id === invoiceId ? { ...i, pdf_filename: fileMeta ? fileMeta.filename_original : null, storage_path: fileMeta ? fileMeta.storage_path : null, size_bytes: fileMeta ? fileMeta.size_bytes : null, } : i) } : p); }; const updateUlaFile = (empId, fileMeta) => { setPractice(p => p ? { ...p, ula_employees: p.ula_employees.map(e => e.id === empId ? { ...e, supporting_doc_filename: fileMeta ? fileMeta.filename_original : null, storage_path: fileMeta ? fileMeta.storage_path : null, size_bytes: fileMeta ? fileMeta.size_bytes : null, } : e) } : p); }; const updateDocFile = (docCode, docId, fileMeta) => { setPractice(p => { if (!p) return p; const exists = p.documents.find(d => d.doc_code === docCode); const newDocs = exists ? p.documents.map(d => d.doc_code === docCode ? { ...d, filename: fileMeta ? fileMeta.filename_original : null, storage_path: fileMeta ? fileMeta.storage_path : null, size_bytes: fileMeta ? fileMeta.size_bytes : null, } : d) : p.documents; return { ...p, documents: newDocs }; }); }; // ensure doc record exists (returns id via callback) const ensureDocRecord = (docCode, onReady) => { const existing = practice?.documents?.find(d => d.doc_code === docCode); if (existing && existing.id) { onReady(existing.id); return; } RendicontazioneService.upsertDocument(practiceId, docCode, { filename: null }, (resp) => { const newDoc = resp?.data; if (newDoc && newDoc.id) { setPractice(p => p ? { ...p, documents: [...p.documents.filter(d => d.doc_code !== docCode), newDoc] } : p); onReady(newDoc.id); } }, (err) => toast.current?.show({ severity: 'error', summary: __('Errore preparazione documento', 'gepafin'), detail: err?.detail }) ); }; // ---------- 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) }); }; const submitAmendmentResponse = () => { if (!amendDialog.responseText || amendDialog.responseText.trim().length < 5) { toast.current?.show({ severity: 'warn', summary: __('Risposta troppo corta', 'gepafin') }); return; } RendicontazioneService.respondAmendmentBeneficiary( practiceId, amendDialog.amendment.id, amendDialog.responseText, (resp) => { setAmendDialog({ visible: false, amendment: null, responseText: '' }); afterMutation(__('Risposta inviata all\'istruttore', 'gepafin'))(resp); }, onMutationError); }; // ---------- render guards ---------- if (loading) { return
{__('Pratica non trovata', 'gepafin')}
{practice.schema_snapshot?.template_label || `Bando #${practice.call_id}`}
{__('L\'istruttore ha chiesto integrazioni o chiarimenti. Rispondi al più presto.', 'gepafin')}
{__('Carica le fatture assegnandole alla categoria di spesa appropriata. I totali si aggiornano in tempo reale.', 'gepafin')}
{__('Inserisci i dipendenti che contano per l\'incremento occupazionale. Soglia minima richiesta:', 'gepafin')} {ulaSection.threshold}.
{practice.ula_employees.length > 0 && ({__('Carica un file per ciascun documento richiesto. In questa sandbox viene registrato solo il nome del file (upload reale al prossimo sprint).', 'gepafin')}
{dr.code}