import React, { useEffect, useState, useRef, useCallback, useMemo } from 'react'; import { __ } from '@wordpress/i18n'; import { useNavigate, useParams } from 'react-router-dom'; import { Button } from 'primereact/button'; import { Toast } from 'primereact/toast'; import { Tag } from 'primereact/tag'; import { Skeleton } from 'primereact/skeleton'; import { Dialog } from 'primereact/dialog'; import { InputNumber } from 'primereact/inputnumber'; import { InputTextarea } from 'primereact/inputtextarea'; import { Calendar } from 'primereact/calendar'; import { DataTable } from 'primereact/datatable'; import { Column } from 'primereact/column'; import { Checkbox } from 'primereact/checkbox'; import { ConfirmPopup, confirmPopup } from 'primereact/confirmpopup'; import RendicontazioneService from '../service/rendicontazioneService'; import FilePreviewDialog from '../components/FilePreviewDialog'; const CONTRACT_TYPES = { T_IND: 'Tempo indeterminato', T_DET: 'Tempo determinato', APPR: 'Apprendistato', STAGE: 'Tirocinio / Stage', COLL: 'Collaborazione coordinata', ALTRO: 'Altro' }; const PRACTICE_STATUS = { DRAFT: { severity: 'warning', label: 'Bozza beneficiario' }, SUBMITTED: { severity: 'info', label: 'Inviata — da prendere in carico' }, UNDER_REVIEW: { severity: 'warning', label: 'In lavorazione' }, AWAITING_AMENDMENT: { severity: 'warning', label: 'Soccorso aperto' }, APPROVED: { severity: 'success', label: 'Approvata' }, REJECTED: { severity: 'danger', label: 'Respinta' } }; const VERIFICATION_INVOICE_TAG = { PENDING: { severity: 'secondary', label: 'Da verificare' }, AMMESSA: { severity: 'success', label: 'Ammessa' }, PARZIALE: { severity: 'warning', label: 'Parziale' }, RESPINTA: { severity: 'danger', label: 'Respinta' } }; const VERIFICATION_DOC_TAG = { PENDING: { severity: 'secondary', label: 'Da verificare' }, VALIDO: { severity: 'success', label: 'Valido' }, NON_VALIDO: { severity: 'danger', label: 'Non valido' }, SCADUTO: { severity: 'warning', label: 'Scaduto' } }; const AMENDMENT_STATUS = { AWAITING: { severity: 'warning', label: 'Attesa risposta' }, RESPONSE_RECEIVED: { severity: 'info', label: 'Risposta ricevuta' }, CLOSED: { severity: 'success', label: 'Chiusa' }, EXPIRED: { severity: 'danger', label: 'Scaduta' } }; const euro = (v) => v == null ? '—' : '€ ' + Number(v || 0).toLocaleString('it-IT', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); const formatDate = (d) => d ? new Date(d).toLocaleDateString('it-IT') : '—'; const formatDateTime = (d) => d ? new Date(d).toLocaleString('it-IT') : '—'; const IstruttoriaPratica = () => { const { id: practiceId } = useParams(); const navigate = useNavigate(); const toast = useRef(null); const [loading, setLoading] = useState(true); const [bundle, setBundle] = useState(null); // dialoghi const [previewDialog, setPreviewDialog] = useState({ visible: false, entityType: null, entityId: null, filename: null, title: null }); const [docNoteDialog, setDocNoteDialog] = useState({ visible: false, doc: null, status: null }); // tabelle: expanded rows + buffer modifiche inline const [expandedInv, setExpandedInv] = useState({}); const [expandedUla, setExpandedUla] = useState({}); const [invDraft, setInvDraft] = useState({}); // { invoiceId: { amount_verified, notes } } const [ulaDraft, setUlaDraft] = useState({}); // { employeeId: { fte_pct_verified, notes } } const [approveDialog, setApproveDialog] = useState({ visible: false, amount: null }); const [rejectDialog, setRejectDialog] = useState({ visible: false, reason: '' }); const [amendDialog, setAmendDialog] = useState({ visible: false, text: '', deadline: null }); // v2: custom_checks (merge schema+values dal BE) const [customChecks, setCustomChecks] = useState([]); const [ccVerifyDialog, setCcVerifyDialog] = useState({ visible: false, cc: null, status: null, notes: '' }); const loadCustomChecks = useCallback(() => { if (!practiceId) return; RendicontazioneService.listCustomChecks(practiceId, (resp) => setCustomChecks(resp?.data?.custom_checks || []), () => {}); }, [practiceId]); const load = useCallback(() => { setLoading(true); RendicontazioneService.instructorViewPractice(practiceId, (resp) => { setBundle(resp?.data); setLoading(false); }, (err) => { toast.current?.show({ severity: 'error', summary: __('Errore', 'gepafin'), detail: err?.detail }); setLoading(false); } ); }, [practiceId]); useEffect(() => { load(); }, [load]); useEffect(() => { loadCustomChecks(); }, [loadCustomChecks]); const practice = bundle?.practice; const gate = bundle?.gate_check; const amendments = bundle?.amendments || []; 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 docsRequired = useMemo(() => { const s = sections.find(x => x.type === 'document_checklist') || {}; const raw = s.required_types || []; return raw.map(r => typeof r === 'string' ? { code: r, label: r } : r); }, [sections]); const customChecksDefs = useMemo(() => { return practice?.schema_snapshot?.custom_checks || []; }, [practice]); const openAmendments = amendments.filter(a => a.status === 'AWAITING' || a.status === 'RESPONSE_RECEIVED'); const isReviewable = practice && ['SUBMITTED', 'UNDER_REVIEW', 'AWAITING_AMENDMENT'].includes(practice.status); const isDecidable = practice && ['UNDER_REVIEW', 'AWAITING_AMENDMENT'].includes(practice.status); const isVerifiable = practice && ['UNDER_REVIEW', 'AWAITING_AMENDMENT'].includes(practice.status); // ---------- actions ---------- const afterOk = (msg) => (resp) => { toast.current?.show({ severity: 'success', summary: msg }); load(); }; const onErr = (err) => { toast.current?.show({ severity: 'error', summary: __('Operazione fallita', 'gepafin'), detail: typeof err?.detail === 'object' ? JSON.stringify(err.detail) : err?.detail }); }; 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 }); const downloadVerbale = () => { RendicontazioneService.downloadVerbale(practiceId, (err) => toast.current?.show({ severity: 'error', summary: __('Errore', 'gepafin'), detail: err?.detail || __('Verbale non disponibile', 'gepafin') }) ); }; const openVerbaleHtml = () => { RendicontazioneService.openVerbaleHtml(practiceId).catch(() => toast.current?.show({ severity: 'error', summary: __('Errore', 'gepafin'), detail: __('Verbale non disponibile', 'gepafin') }) ); }; const doDownload = (entityType, entityId) => { RendicontazioneService.downloadEntityFile(entityType, entityId, (err) => toast.current?.show({ severity: 'error', summary: __('Errore', 'gepafin'), detail: err?.detail || __('Download non riuscito', 'gepafin') }) ); }; // Quick verify (thumbs up/down) senza rettifica const quickVerifyInvoice = (invoice, newStatus) => { RendicontazioneService.verifyInvoice(practiceId, invoice.id, { verification_status: newStatus, verification_notes: null }, afterOk(__(`Fattura ${newStatus.toLowerCase()}`, 'gepafin')), onErr); }; const quickVerifyUla = (employee, newStatus) => { RendicontazioneService.verifyUlaEmployee(practiceId, employee.id, { verification_status: newStatus, verification_notes: null }, afterOk(__(`Dipendente ${newStatus.toLowerCase()}`, 'gepafin')), onErr); }; const quickVerifyDoc = (doc, newStatus) => { RendicontazioneService.verifyDocument(practiceId, doc.doc_code, { verification_status: newStatus, verification_notes: null }, (resp) => { const updated = resp?.data || {}; setBundle(b => { if (!b) return b; const exists = b.practice.documents.find(d => d.doc_code === doc.doc_code); const newDocs = exists ? b.practice.documents.map(d => d.doc_code === doc.doc_code ? { ...d, ...updated } : d) : [...b.practice.documents, updated]; return { ...b, practice: { ...b.practice, documents: newDocs } }; }); toast.current?.show({ severity: 'success', summary: __(`Documento ${newStatus.toLowerCase()}`, 'gepafin') }); }, onErr); }; // Refresh solo gate_check (totali) senza rileggere tutta la pratica const refreshGateOnly = () => { RendicontazioneService.gateCheck(practiceId, (resp) => setBundle(b => b ? { ...b, gate_check: resp?.data } : b), () => {}); }; // Save INLINE fattura con update LOCALE (no reload pagina) const saveInvoiceInline = (invoice, explicitStatus = null) => { const draft = invDraft[invoice.id] || {}; const useTaxable = (gate?.totals?.use_taxable_only ?? true); const declared = Number(useTaxable ? invoice.taxable : invoice.total); const verified = draft.amount_verified != null ? Number(draft.amount_verified) : declared; const notes = draft.notes != null ? draft.notes : invoice.verification_notes; let status = explicitStatus; if (!status) { if (verified <= 0) status = 'RESPINTA'; else if (Math.abs(verified - declared) < 0.005) status = 'AMMESSA'; else status = 'PARZIALE'; } const body = { verification_status: status, verification_notes: notes || null }; if (status !== 'RESPINTA' && status !== 'PENDING') { if (useTaxable) { body.taxable_verified = verified; body.vat_verified = invoice.vat; body.total_verified = Number(verified) + Number(invoice.vat || 0); } else { body.total_verified = verified; body.vat_verified = invoice.vat; body.taxable_verified = Number(verified) - Number(invoice.vat || 0); } } RendicontazioneService.verifyInvoice(practiceId, invoice.id, body, (resp) => { setInvDraft(prev => { const n = {...prev}; delete n[invoice.id]; return n; }); const updated = resp?.data || {}; // update LOCALE della singola fattura (no full page reload!) setBundle(b => { if (!b) return b; const newInvoices = b.practice.invoices.map(i => i.id === invoice.id ? { ...i, ...updated } : i); return { ...b, practice: { ...b.practice, invoices: newInvoices } }; }); refreshGateOnly(); toast.current?.show({ severity: 'success', summary: __(`Fattura ${status.toLowerCase()}`, 'gepafin') }); }, onErr); }; // Save INLINE ULA con update LOCALE (no reload pagina) const saveUlaInline = (emp, explicitStatus = null) => { const draft = ulaDraft[emp.id] || {}; const declared = Number(emp.fte_pct); const verified = draft.fte_pct_verified != null ? Number(draft.fte_pct_verified) : declared; const notes = draft.notes != null ? draft.notes : emp.verification_notes; let status = explicitStatus; if (!status) { if (verified <= 0) status = 'RESPINTA'; else if (Math.abs(verified - declared) < 0.0005) status = 'AMMESSA'; else status = 'PARZIALE'; } const body = { verification_status: status, verification_notes: notes || null }; if (status !== 'RESPINTA' && status !== 'PENDING') { body.fte_pct_verified = verified; } RendicontazioneService.verifyUlaEmployee(practiceId, emp.id, body, (resp) => { setUlaDraft(prev => { const n = {...prev}; delete n[emp.id]; return n; }); const updated = resp?.data || {}; setBundle(b => { if (!b) return b; const newUlas = b.practice.ula_employees.map(x => x.id === emp.id ? { ...x, ...updated } : x); return { ...b, practice: { ...b.practice, ula_employees: newUlas } }; }); refreshGateOnly(); toast.current?.show({ severity: 'success', summary: __(`Dipendente ${status.toLowerCase()}`, 'gepafin') }); }, onErr); }; const saveDocNote = () => { const d = docNoteDialog.doc; const status = docNoteDialog.status; RendicontazioneService.verifyDocument(practiceId, d.doc_code, { verification_status: status, verification_notes: d.verification_notes }, (resp) => { setDocNoteDialog({ visible: false, doc: null, status: null }); const updated = resp?.data || {}; setBundle(b => { if (!b) return b; const exists = b.practice.documents.find(x => x.doc_code === d.doc_code); const newDocs = exists ? b.practice.documents.map(x => x.doc_code === d.doc_code ? { ...x, ...updated } : x) : [...b.practice.documents, updated]; return { ...b, practice: { ...b.practice, documents: newDocs } }; }); toast.current?.show({ severity: 'success', summary: __('Documento aggiornato', 'gepafin') }); }, onErr); }; // v2: verify custom_check const verifyCustomCheckInline = (cc, status, notes) => { RendicontazioneService.verifyCustomCheck(practiceId, cc.code, { verification_status: status, verification_notes: notes || null }, (resp) => { toast.current?.show({ severity: 'success', summary: __('Controllo aggiornato', 'gepafin') }); loadCustomChecks(); }, onErr); }; const openCcVerifyDialog = (cc, status) => { setCcVerifyDialog({ visible: true, cc, status, notes: cc.verification_notes || '' }); }; const confirmCcVerify = () => { const { cc, status, notes } = ccVerifyDialog; verifyCustomCheckInline(cc, status, notes); setCcVerifyDialog({ visible: false, cc: null, status: null, notes: '' }); }; const downloadCustomCheckDoc = (cc) => { RendicontazioneService.fetchCustomCheckDocumentBlob(practiceId, cc.code, false, ({ objectUrl, filename }) => { const a = document.createElement('a'); a.href = objectUrl; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); setTimeout(() => URL.revokeObjectURL(objectUrl), 60000); }, (err) => toast.current?.show({ severity: 'error', summary: __('Errore', 'gepafin'), detail: err?.detail })); }; const previewCustomCheckDoc = (cc) => { RendicontazioneService.fetchCustomCheckDocumentBlob(practiceId, cc.code, true, ({ objectUrl }) => { const w = window.open(objectUrl, '_blank'); if (w) setTimeout(() => URL.revokeObjectURL(objectUrl), 120000); }, (err) => toast.current?.show({ severity: 'error', summary: __('Errore', 'gepafin'), detail: err?.detail })); }; // Final notes + checklist (debounced inline save) const saveFinalNotes = (patch) => { RendicontazioneService.setInstructorFinalNotes(practiceId, patch, afterOk(__('Verbale aggiornato', 'gepafin')), onErr); }; // Claim / approve / reject / amendment / close amendment — come prima const handleClaim = (ev) => { confirmPopup({ target: ev.currentTarget, message: __('Prendere in carico la pratica? Lo stato passerà a "In lavorazione".', 'gepafin'), icon: 'pi pi-info-circle', acceptLabel: __('Prendi in carico', 'gepafin'), rejectLabel: __('Annulla', 'gepafin'), accept: () => RendicontazioneService.claimPractice(practiceId, afterOk(__('Pratica presa in carico', 'gepafin')), onErr) }); }; const doApprove = () => { const body = approveDialog.amount != null ? { approved_remission: approveDialog.amount } : {}; RendicontazioneService.approvePractice(practiceId, body, (resp) => { setApproveDialog({ visible: false, amount: null }); afterOk(__('Pratica approvata', 'gepafin'))(resp); }, onErr); }; const doReject = () => { if (!rejectDialog.reason || rejectDialog.reason.trim().length < 10) { toast.current?.show({ severity: 'warn', summary: __('Motivazione troppo corta (min 10 caratteri)', 'gepafin') }); return; } RendicontazioneService.rejectPractice(practiceId, rejectDialog.reason, (resp) => { setRejectDialog({ visible: false, reason: '' }); afterOk(__('Pratica respinta', 'gepafin'))(resp); }, onErr); }; const doAmend = () => { if (!amendDialog.text || amendDialog.text.trim().length < 10) { toast.current?.show({ severity: 'warn', summary: __('Testo troppo corto', 'gepafin') }); return; } if (!amendDialog.deadline) { toast.current?.show({ severity: 'warn', summary: __('Deadline obbligatoria', 'gepafin') }); return; } const body = { request_text: amendDialog.text, deadline: typeof amendDialog.deadline === 'string' ? amendDialog.deadline : amendDialog.deadline.toISOString().slice(0, 10) }; RendicontazioneService.createAmendment(practiceId, body, (resp) => { setAmendDialog({ visible: false, text: '', deadline: null }); afterOk(__('Soccorso avviato', 'gepafin'))(resp); }, onErr); }; const closeAmendment = (ev, a) => { confirmPopup({ target: ev.currentTarget, message: __('Chiudi questa richiesta di soccorso? La pratica torna in lavorazione.', 'gepafin'), icon: 'pi pi-info-circle', acceptLabel: __('Chiudi', 'gepafin'), rejectLabel: __('Annulla', 'gepafin'), accept: () => RendicontazioneService.closeAmendment(practiceId, a.id, afterOk(__('Soccorso chiuso', 'gepafin')), onErr) }); }; // ---------- render helpers ---------- const renderThumbsRow = (currentStatus, onUp, onDown, onPreview, onDownload, filename, extraButtons) => { const isUp = currentStatus === 'AMMESSA' || currentStatus === 'VALIDO'; const isDown = currentStatus === 'RESPINTA' || currentStatus === 'NON_VALIDO' || currentStatus === 'SCADUTO'; return (
); }; // ---------- render ---------- if (loading) { return
; } if (!practice) { return

{__('Pratica non trovata', 'gepafin')}

; } const statusCfg = PRACTICE_STATUS[practice.status] || { severity: 'secondary', label: practice.status }; const totals = gate?.totals || {}; const perDecl = totals.per_category_declared || {}; const perVerif = totals.per_category_verified || {}; const invoicesOfCat = (code) => practice.invoices.filter(i => i.category_code === code); const allItemsVerified = practice.invoices.every(i => i.verification_status !== 'PENDING') && practice.ula_employees.every(e => e.verification_status !== 'PENDING') && practice.documents.every(d => d.verification_status !== 'PENDING'); const checklist = practice.instructor_checklist || {}; return (
{/* HEADER */}

{__('Istruttoria pratica rendicontazione', 'gepafin')}

{practice.schema_snapshot?.template_label || `Bando #${practice.call_id}`} · {__('Pratica', 'gepafin')} #{practice.application_id}

{/* ACTIONS */}
{/* BANNER pratica non presa in carico */} {practice.status === 'SUBMITTED' && (
{__("Pratica non ancora presa in carico", 'gepafin')}
{__("Per poter verificare fatture, dipendenti ULA e documenti, clicca su «Prendi in carico» qui sopra. Lo stato della pratica passerà a «In lavorazione».", 'gepafin')}
)} {practice.status === 'SUBMITTED' &&
} {/* RIEPILOGO */}

{__('Riepilogo finanziario', 'gepafin')}

{__('Erogato', 'gepafin')}
{euro(practice.amount_erogato)}
{__('Regime IVA', 'gepafin')}
{practice.iva_regime || '—'}
{__('Totale dichiarato', 'gepafin')}
{euro(totals.grand_total_declared)}
{__('Totale verificato', 'gepafin')}
{euro(totals.grand_total_verified)}
{__('Cap remissione', 'gepafin')}
{euro(totals.max_remission)}
{__('Remissione da riconoscere', 'gepafin')}
{euro(totals.remission_due)}
{__('Residuo da restituire', 'gepafin')}
{euro(totals.residuo_da_restituire)}
{practice.approved_remission != null && (
{__('Remissione approvata', 'gepafin')}
{euro(practice.approved_remission)}
)}
{practice.rejection_reason && (
{__('Motivo rifiuto:', 'gepafin')}
{practice.rejection_reason}
)}
{/* AMENDMENTS */} {amendments.length > 0 && (<>

{__('Soccorso istruttorio', 'gepafin')} ({amendments.length})

{amendments.map(a => { const cfg = AMENDMENT_STATUS[a.status] || { severity: 'secondary', label: a.status }; return (
{__('Deadline:', 'gepafin')} {formatDate(a.deadline)} · {__('Creata:', 'gepafin')} {formatDateTime(a.created_at)}
{a.status !== 'CLOSED' && isReviewable && (
{__('Richiesta istruttore:', 'gepafin')}
{a.request_text}
{a.response_text && (<> {__('Risposta beneficiario', 'gepafin')} ({formatDateTime(a.response_at)}):
{a.response_text}
)}
); })}
)} {/* FATTURE — tabella inline unica con raggruppamento per categoria */}

{__('Verifica fatture', 'gepafin')}

{__("Modifica l'importo ammesso direttamente nella riga (salvataggio automatico quando esci dal campo). Clicca ▸ per aprire le note.", 'gepafin')}

{(() => { const useTaxable = totals.use_taxable_only !== false; const declaredLabel = useTaxable ? __('Imponibile dichiarato', 'gepafin') : __('Totale dichiarato', 'gepafin'); const ammessoLabel = useTaxable ? __('Imponibile ammesso', 'gepafin') : __('Totale ammesso', 'gepafin'); // Ordino le fatture secondo l'ordine definito dallo schema (B1 < B2 < B3) const catOrder = Object.fromEntries(categories.map((c, i) => [c.code, i])); const sortedInvoices = [...practice.invoices].sort((a, b) => { const oa = catOrder[a.category_code] ?? 999; const ob = catOrder[b.category_code] ?? 999; if (oa !== ob) return oa - ob; return (a.invoice_number || '').localeCompare(b.invoice_number || ''); }); if (sortedInvoices.length === 0) { return

{__('Nessuna fattura caricata', 'gepafin')}

; } return ( setExpandedInv(e.data)} rowGroupHeaderTemplate={(row) => { const cat = categories.find(c => c.code === row.category_code) || { code: row.category_code, label: '—' }; const totalDecl = perDecl[row.category_code] || 0; const totalVerif = perVerif[row.category_code] || 0; return (
{cat.code} {cat.label}
{__('Dichiarato:', 'gepafin')} {euro(totalDecl)}
{__('Ammesso:', 'gepafin')} {euro(totalVerif)}
); }} rowExpansionTemplate={(inv) => { const draft = invDraft[inv.id] || {}; return (
setInvDraft(d => ({ ...d, [inv.id]: { ...(d[inv.id]||{}), notes: ev.target.value } }))} onBlur={() => { const d = invDraft[inv.id] || {}; if (d.notes != null && d.notes !== (inv.verification_notes || '')) { saveInvoiceInline(inv); } }} placeholder={__('Es: decurtata quota di 400€ per assicurazione accessoria non ammissibile...', 'gepafin')} />
{__('Dettaglio dichiarato:', 'gepafin')}
{__('Imponibile:', 'gepafin')} {euro(inv.taxable)}
IVA: {euro(inv.vat)} — {__('Totale:', 'gepafin')} {euro(inv.total)}
); }} > { const bad = r.date_checks && (r.date_checks.invoice_in_period === false || r.date_checks.payment_in_period === false); return {formatDate(r.invoice_date)} {bad && } ; }} /> {r.description.length > 50 ? r.description.slice(0, 50) + '…' : r.description}} /> {euro(useTaxable ? r.taxable : r.total)}} /> { const draft = invDraft[r.id] || {}; const declared = Number(useTaxable ? r.taxable : r.total); const currentVerified = r.verification_status === 'PENDING' ? declared : (useTaxable ? (r.taxable_verified != null ? Number(r.taxable_verified) : declared) : (r.total_verified != null ? Number(r.total_verified) : declared)); const displayVal = draft.amount_verified != null ? draft.amount_verified : currentVerified; return ( setInvDraft(d => ({ ...d, [r.id]: { ...(d[r.id]||{}), amount_verified: ev.value } }))} onBlur={() => { const d = invDraft[r.id]; if (d && d.amount_verified != null && Math.abs(d.amount_verified - currentVerified) > 0.005) { saveInvoiceInline(r); } }} /> ); }} /> { const cfg = VERIFICATION_INVOICE_TAG[r.verification_status] || VERIFICATION_INVOICE_TAG.PENDING; return ; }} /> (
)} />
); })()}
{/* ULA DIPENDENTI — DataTable semplice con box header sopra */} {ulaSection.enabled && practice.ula_employees.length > 0 && (<>

{__('Verifica dipendenti ULA', 'gepafin')}

{__("Modifica l'FTE ammesso direttamente nella riga (salvataggio automatico quando esci dal campo). Clicca ▸ per aprire le note.", 'gepafin')}

{(() => { const totalFteDecl = practice.ula_employees.reduce((a, e) => a + Number(e.fte_pct || 0), 0); const totalFteVerif = practice.ula_employees .filter(e => ['AMMESSA', 'PARZIALE'].includes(e.verification_status)) .reduce((a, e) => a + Number((e.fte_pct_verified != null ? e.fte_pct_verified : e.fte_pct) || 0), 0); const thresholdOK = totalFteVerif >= Number(ulaSection.threshold || 1); return (
{/* Header-box fuori dalla tabella, stesso stile del subheader delle fatture */}
ULA {__('Incremento occupazione (soglia richiesta ≥', 'gepafin')} {Number(ulaSection.threshold || 1).toFixed(2)})
{__('FTE dichiarato:', 'gepafin')} {totalFteDecl.toFixed(2)}
{__('FTE ammesso:', 'gepafin')} {totalFteVerif.toFixed(2)}
setExpandedUla(e.data)} rowExpansionTemplate={(emp) => { const draft = ulaDraft[emp.id] || {}; return (
setUlaDraft(d => ({ ...d, [emp.id]: { ...(d[emp.id]||{}), notes: ev.target.value } }))} onBlur={() => { const d = ulaDraft[emp.id] || {}; if (d.notes != null && d.notes !== (emp.verification_notes || '')) { saveUlaInline(emp); } }} placeholder={__('Es: dipendente verificato part-time 50% su LUL, non full-time come dichiarato...', 'gepafin')} />
{__('Dettaglio:', 'gepafin')} {CONTRACT_TYPES[emp.contract_type] || emp.contract_type} {emp.role_description && ` · ${emp.role_description}`} {' · '}{formatDate(emp.period_start_date)} → {formatDate(emp.period_end_date)}
); }} > {CONTRACT_TYPES[r.contract_type] || r.contract_type}} /> {formatDate(r.period_start_date)} → {formatDate(r.period_end_date)}} /> {Number(r.fte_pct).toFixed(2)}} /> { const draft = ulaDraft[r.id] || {}; const declared = Number(r.fte_pct); const currentVerified = r.verification_status === 'PENDING' ? declared : (r.fte_pct_verified != null ? Number(r.fte_pct_verified) : declared); const displayVal = draft.fte_pct_verified != null ? draft.fte_pct_verified : currentVerified; return ( setUlaDraft(d => ({ ...d, [r.id]: { ...(d[r.id]||{}), fte_pct_verified: ev.value } }))} onBlur={() => { const d = ulaDraft[r.id]; if (d && d.fte_pct_verified != null && Math.abs(d.fte_pct_verified - currentVerified) > 0.0005) { saveUlaInline(r); } }} /> ); }} /> { const cfg = VERIFICATION_INVOICE_TAG[r.verification_status] || VERIFICATION_INVOICE_TAG.PENDING; return ; }} /> (
)} />
); })()}
)} {/* DOCUMENTI */}

{__('Verifica documenti', 'gepafin')}

    {docsRequired.map(dr => { const doc = practice.documents.find(d => d.doc_code === dr.code) || { doc_code: dr.code }; const cfg = VERIFICATION_DOC_TAG[doc.verification_status || 'PENDING']; return (
  1. {dr.label} {dr.code} {doc.filename ? : }
    {doc.filename && (
    {doc.filename} · {__('caricato il', 'gepafin')} {formatDateTime(doc.uploaded_at)}
    )} {doc.verification_notes && (
    {doc.verification_notes}
    )}
  2. ); })}
{/* VERIFICA CONTROLLI AGGIUNTIVI (v2) */} {customChecksDefs.length > 0 && (<>

{__('Verifica controlli aggiuntivi', 'gepafin')}

{__('Dichiarazioni aggiuntive del beneficiario. Valida ciascun controllo con VALIDO o NON_VALIDO (richiede motivazione). I controlli obbligatori non dichiarati impediscono l\'approvazione.', 'gepafin')}

    {customChecksDefs.map(def => { const val = customChecks.find(c => c.code === def.code) || {}; const stat = val.verification_status || 'PENDING'; const declared = !!val.beneficiary_declared; const hasDoc = !!val.filename_original; const missingRequired = def.required && !declared; const sevMap = { PENDING: { severity: 'secondary', label: __('Da verificare', 'gepafin') }, VALIDO: { severity: 'success', label: __('Valido', 'gepafin') }, NON_VALIDO: { severity: 'danger', label: __('Non valido', 'gepafin') } }; const cfg = sevMap[stat] || sevMap.PENDING; return (
  1. {def.label} {def.code} {def.required && } {declared ? : }
    {def.description && (
    {def.description}
    )} {def.requires_document && (
    {hasDoc ? ( {val.filename_original} {val.size_bytes && ({(val.size_bytes/1024).toFixed(1)} KB)} ) : ( {__('Nessun documento allegato', 'gepafin')} )}
    )} {val.verification_notes && (
    {val.verification_notes}
    )}
  2. ); })}
)} {/* VERBALE ISTRUTTORIA */} {isVerifiable && (<>

{__('Verbale istruttoria', 'gepafin')}

{__('Checklist finale', 'gepafin')}

{[ { id: 'domanda_completa', label: __('Documentazione completa e coerente', 'gepafin') }, { id: 'ula_ok', label: __('Incremento ULA > 1 verificato', 'gepafin') }, { id: 'erogato_in_range', label: __('Importo erogato entro il range bando', 'gepafin') } ].map(item => (
saveFinalNotes({ instructor_checklist: { ...checklist, [item.id]: e.checked } })} />
))}
{ if (e.target.value !== (practice.instructor_final_notes || '')) { saveFinalNotes({ instructor_final_notes: e.target.value }); } }} placeholder={__('Note di sintesi che saranno incluse nel verbale finale...', 'gepafin')} /> {__('Le note si salvano quando esci dal campo.', 'gepafin')}
)} {/* ============ DIALOGS ============ */} {/* Preview PDF reale — iframe con blob autenticato */} {/* Rettifica fattura */} {/* Rettifica ULA */} {/* Note documento (scaduto/non valido) */} setDocNoteDialog({ visible: false, doc: null, status: null })}> {docNoteDialog.doc && (
{ e.preventDefault(); saveDocNote(); }}>
setDocNoteDialog(d => ({ ...d, doc: { ...d.doc, verification_notes: e.target.value } }))} placeholder={docNoteDialog.status === 'SCADUTO' ? __('Esempio: DURC scaduto il 15/10/2021, non valido al momento della rendicontazione...', 'gepafin') : __('Esempio: visura camerale non corrisponde alla ragione sociale del beneficiario...', 'gepafin')} />
)}
{/* DIALOG APPROVA */} setApproveDialog({ visible: false, amount: null })}>
{ e.preventDefault(); doApprove(); }}>
setApproveDialog(d => ({ ...d, amount: e.value }))} /> {__('Valore calcolato da verificati:', 'gepafin')} {euro(totals.remission_due)}
{/* DIALOG RESPINGI */} setRejectDialog({ visible: false, reason: '' })}>
{ e.preventDefault(); doReject(); }}>
setRejectDialog(d => ({ ...d, reason: e.target.value }))} /> {__('Minimo 10 caratteri. Verrà inviata al beneficiario.', 'gepafin')}
{/* DIALOG SOCCORSO */} setAmendDialog({ visible: false, text: '', deadline: null })}>
{ e.preventDefault(); doAmend(); }}>
setAmendDialog(d => ({ ...d, text: e.target.value }))} placeholder={__('Descrivi le integrazioni richieste...', 'gepafin')} />
setAmendDialog(d => ({ ...d, deadline: e.value }))} />
{/* DIALOG VERIFICA CUSTOM CHECK (motivazione NON_VALIDO) */} setCcVerifyDialog({ visible: false, cc: null, status: null, notes: '' })}>
{ e.preventDefault(); confirmCcVerify(); }}>
setCcVerifyDialog(d => ({ ...d, notes: e.target.value }))} placeholder={__('Es: dichiarazione non coerente con il bando...', 'gepafin')} />
); }; export default IstruttoriaPratica;