service/rendicontazioneService.js: - startPractice(appId, cb, cb, opts) v2: accetta period_label + copy_ula_from_previous - copyUlaOptions(practiceId): preview ULA tranche N-1 per pre-fill - Custom checks: listCustomChecks, declareCustomCheck (form-data + optional file), deleteCustomCheckDocument, verifyCustomCheck, fetchCustomCheckDocumentBlob - Manager: managerAssignments, managerInstructorsList, reassignInstructor B1 BandoRendicontazioneSchemaEdit.js (editor superadmin): - schemaJsonToForm: estrae gate_rules.max_tranches + custom_checks[] top-level - formToSchemaJson: scrive max_tranches e custom_checks + schema_version=2 - Helpers addCheck/removeCheck/updateCheck (pattern fieldsRepeater esistente) - Sezione 7 'Tranches di rendicontazione': InputNumber max_tranches (1-20) - Sezione 8 'Controlli aggiuntivi': array editable con code (snake_case sanitized), label, description, requires_document, required B2 RendicontazioniMie.js (dashboard benef) — RISCRITTA: - Raggruppamento per application_id con card per bando - Riquadro info cumulativo (cap totale, gia approvato, disponibile, tranches N/M) - Elenco tranche con badge stato + bottoni 'Continua' (DRAFT) / 'Apri' (non editable) - Bottone '+ Nuova rendicontazione' con 4 stati: attivo / disabilitato 'Limite raggiunto' / 'Completa prima' / 'Remissione esaurita' - Dialog avvio: InputText period_label + Checkbox copy_ula (solo se sequence > 1) B3 PraticaRendicontazioneEdit.js (beneficiario): - useMemo customChecksDefs da schema_snapshot.custom_checks - State customChecks + loadCustomChecks useCallback - Sezione 5/4 'Controlli aggiuntivi (dichiarazioni)': per ogni check checkbox 'Dichiaro', badge Obbligatorio/Opzionale/status, upload PDF/JPG/PNG 15MB se requires_document, preview filename+size - Bordo rosso su check obbligatori non dichiarati B4 IstruttoriaPratica.js (istruttore): - State customChecks + loadCustomChecks + ccVerifyDialog - Sezione 'Verifica controlli aggiuntivi' (dopo Verifica documenti): lista con label/codice/badge stato beneficiario/validazione/note istruttore - Azioni: preview, download, thumbs-up (VALIDO toggle), thumbs-down (NON_VALIDO) - Dialog motivazione NON_VALIDO con InputTextarea (min 5 char) B5 IstruttoriaQueue.js (manager): - Toggle 'Coda standard' vs 'Vista manager (riassegnazioni)' visibile solo per ROLE_INSTRUCTOR_MANAGER o ROLE_SUPER_ADMIN - Tabella manager con colonne: Bando/Pratica/Tranche, Stato, Istruttore domanda, Assegnato a (o badge 'Da assegnare' se unassigned), Erogato - Azione 'Riassegna' (o 'Assegna' se unassigned): apre Dialog con Dropdown istruttori (pool pre_instructor + manager) + InputTextarea motivazione - Opzione 'Metti in coda (nessuno)' nel Dropdown per unassign Tutti i file validati via @babel/parser JSX. Webpack compila senza errori (solo warning eslint preesistenti non-B).
1269 lines
84 KiB
JavaScript
1269 lines
84 KiB
JavaScript
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 (
|
|
<div className="appPageSection__iconActions">
|
|
<Button icon="pi pi-eye" rounded outlined severity="info"
|
|
onClick={onPreview} disabled={!filename}
|
|
tooltip={__('Anteprima', 'gepafin')} tooltipOptions={{ position: 'top' }}
|
|
aria-label={__('Anteprima', 'gepafin')} />
|
|
<Button icon="pi pi-download" rounded outlined severity="info"
|
|
onClick={onDownload} disabled={!filename}
|
|
tooltip={__('Scarica', 'gepafin')} tooltipOptions={{ position: 'top' }}
|
|
aria-label={__('Scarica', 'gepafin')} />
|
|
{extraButtons}
|
|
<Button icon="pi pi-thumbs-up" rounded outlined
|
|
severity={isUp ? 'success' : 'secondary'}
|
|
disabled={!isVerifiable}
|
|
onClick={onUp}
|
|
tooltip={__('Ammetti', 'gepafin')} tooltipOptions={{ position: 'top' }}
|
|
aria-label={__('Ammetti', 'gepafin')} />
|
|
<Button icon="pi pi-thumbs-down" rounded outlined
|
|
severity={isDown ? 'danger' : 'secondary'}
|
|
disabled={!isVerifiable}
|
|
onClick={onDown}
|
|
tooltip={__('Respingi', 'gepafin')} tooltipOptions={{ position: 'top' }}
|
|
aria-label={__('Respingi', 'gepafin')} />
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// ---------- render ----------
|
|
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 = 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 (
|
|
<div className="appPage">
|
|
<Toast ref={toast} />
|
|
<ConfirmPopup />
|
|
|
|
{/* HEADER */}
|
|
<div className="appPage__pageHeader">
|
|
<h1>{__('Istruttoria pratica rendicontazione', 'gepafin')}</h1>
|
|
<p>
|
|
<span className="companyName">
|
|
{practice.schema_snapshot?.template_label || `Bando #${practice.call_id}`} · {__('Pratica', 'gepafin')} #{practice.application_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 coda', 'gepafin')} onClick={() => navigate('/istruttoria')} />
|
|
|
|
{practice.status === 'SUBMITTED' && (
|
|
<Button type="button" icon="pi pi-user-plus" iconPos="right"
|
|
label={__('Prendi in carico', 'gepafin')} onClick={handleClaim} />
|
|
)}
|
|
|
|
{isDecidable && (<>
|
|
<Button type="button" icon="pi pi-check" iconPos="right" severity="success"
|
|
label={__('Approva', 'gepafin')}
|
|
disabled={!allItemsVerified}
|
|
tooltip={!allItemsVerified ? __('Completa la verifica di tutte le righe prima di approvare', 'gepafin') : null}
|
|
tooltipOptions={{ showOnDisabled: true }}
|
|
onClick={() => setApproveDialog({ visible: true, amount: totals.remission_due || 0 })} />
|
|
<Button type="button" icon="pi pi-times" iconPos="right" severity="danger" outlined
|
|
label={__('Respingi', 'gepafin')}
|
|
onClick={() => setRejectDialog({ visible: true, reason: '' })} />
|
|
<Button type="button" icon="pi pi-comment" iconPos="right" severity="warning" outlined
|
|
label={__('Soccorso istruttorio', 'gepafin')}
|
|
disabled={openAmendments.length > 0}
|
|
onClick={() => setAmendDialog({ visible: true, text: '', deadline: null })} />
|
|
</>)}
|
|
|
|
{/* Verbale: sempre visibile all'istruttore per preview e scarico */}
|
|
{['UNDER_REVIEW', 'AWAITING_AMENDMENT', 'APPROVED', 'REJECTED'].includes(practice.status) && (<>
|
|
<Button type="button" icon="pi pi-eye" iconPos="right" outlined
|
|
label={__('Anteprima verbale', 'gepafin')}
|
|
onClick={openVerbaleHtml} />
|
|
<Button type="button" icon="pi pi-file-pdf" iconPos="right" severity="info"
|
|
label={__('Scarica verbale PDF', 'gepafin')}
|
|
onClick={downloadVerbale} />
|
|
</>)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="appPage__spacer"></div>
|
|
|
|
{/* BANNER pratica non presa in carico */}
|
|
{practice.status === 'SUBMITTED' && (
|
|
<div className="appPageSection">
|
|
<div style={{
|
|
padding: '1rem 1.25rem',
|
|
background: 'var(--orange-50)',
|
|
border: '1px solid var(--orange-300)',
|
|
borderLeft: '4px solid var(--orange-500)',
|
|
borderRadius: '6px',
|
|
display: 'flex', alignItems: 'center', gap: '1rem', width: '100%'
|
|
}}>
|
|
<i className="pi pi-info-circle" style={{ fontSize: '1.5rem', color: 'var(--orange-600)' }} />
|
|
<div style={{ flex: 1 }}>
|
|
<strong>{__("Pratica non ancora presa in carico", 'gepafin')}</strong>
|
|
<div style={{ fontSize: '0.9em', color: 'var(--text-color-secondary)', marginTop: '0.25rem' }}>
|
|
{__("Per poter verificare fatture, dipendenti ULA e documenti, clicca su «Prendi in carico» qui sopra. Lo stato della pratica passerà a «In lavorazione».", 'gepafin')}
|
|
</div>
|
|
</div>
|
|
<Button icon="pi pi-user-plus" iconPos="right"
|
|
label={__('Prendi in carico', 'gepafin')} onClick={handleClaim} />
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{practice.status === 'SUBMITTED' && <div className="appPage__spacer"></div>}
|
|
|
|
{/* RIEPILOGO */}
|
|
<div className="appPageSection" style={{ background: 'var(--surface-50)', padding: '1.25rem', borderRadius: '6px' }}>
|
|
<h2 style={{ margin: '0 0 0.5rem 0' }}>{__('Riepilogo finanziario', 'gepafin')}</h2>
|
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))', gap: '1rem', width: '100%' }}>
|
|
<div>
|
|
<small className="text-color-secondary">{__('Erogato', 'gepafin')}</small>
|
|
<div style={{ fontSize: '1.15rem', fontWeight: 700 }}>{euro(practice.amount_erogato)}</div>
|
|
</div>
|
|
<div>
|
|
<small className="text-color-secondary">{__('Regime IVA', 'gepafin')}</small>
|
|
<div style={{ fontSize: '1rem', fontWeight: 700 }}>{practice.iva_regime || '—'}</div>
|
|
</div>
|
|
<div>
|
|
<small className="text-color-secondary">{__('Totale dichiarato', 'gepafin')}</small>
|
|
<div style={{ fontSize: '1.15rem', fontWeight: 700 }}>{euro(totals.grand_total_declared)}</div>
|
|
</div>
|
|
<div>
|
|
<small className="text-color-secondary">{__('Totale verificato', 'gepafin')}</small>
|
|
<div style={{ fontSize: '1.15rem', fontWeight: 700, color: 'var(--green-700)' }}>{euro(totals.grand_total_verified)}</div>
|
|
</div>
|
|
<div>
|
|
<small className="text-color-secondary">{__('Cap remissione', 'gepafin')}</small>
|
|
<div style={{ fontSize: '1.15rem', fontWeight: 700 }}>{euro(totals.max_remission)}</div>
|
|
</div>
|
|
<div>
|
|
<small className="text-color-secondary">{__('Remissione da riconoscere', 'gepafin')}</small>
|
|
<div style={{ fontSize: '1.4rem', fontWeight: 700, color: 'var(--primary-color)' }}>{euro(totals.remission_due)}</div>
|
|
</div>
|
|
<div>
|
|
<small className="text-color-secondary">{__('Residuo da restituire', 'gepafin')}</small>
|
|
<div style={{ fontSize: '1.4rem', fontWeight: 700, color: 'var(--red-600)' }}>{euro(totals.residuo_da_restituire)}</div>
|
|
</div>
|
|
{practice.approved_remission != null && (
|
|
<div>
|
|
<small className="text-color-secondary">{__('Remissione approvata', 'gepafin')}</small>
|
|
<div style={{ fontSize: '1.4rem', fontWeight: 700, color: 'var(--green-600)' }}>{euro(practice.approved_remission)}</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
{practice.rejection_reason && (
|
|
<div style={{ marginTop: '1rem', padding: '0.75rem', background: 'var(--red-50)', borderRadius: '4px', borderLeft: '4px solid var(--red-500)' }}>
|
|
<strong>{__('Motivo rifiuto:', 'gepafin')}</strong>
|
|
<div style={{ marginTop: '0.25rem' }}>{practice.rejection_reason}</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="appPage__spacer"></div>
|
|
|
|
{/* AMENDMENTS */}
|
|
{amendments.length > 0 && (<>
|
|
<div className="appPageSection">
|
|
<h2>{__('Soccorso istruttorio', 'gepafin')} <span style={{fontWeight:400, color:'var(--text-color-secondary)', fontSize:'0.9em'}}>({amendments.length})</span></h2>
|
|
<div className="fieldsRepeater">
|
|
{amendments.map(a => {
|
|
const cfg = AMENDMENT_STATUS[a.status] || { severity: 'secondary', label: a.status };
|
|
return (
|
|
<div key={a.id} className="fieldsRepeater__panel"
|
|
style={{ border: '1px solid var(--surface-border)', borderRadius: '6px', padding: '1rem', background: 'var(--surface-50)' }}>
|
|
<div className="fieldsRepeater__heading" style={{ marginBottom: '0.5rem' }}>
|
|
<div>
|
|
<Tag severity={cfg.severity} value={cfg.label} />
|
|
<span style={{ marginLeft: '0.75rem', color: 'var(--text-color-secondary)' }}>
|
|
{__('Deadline:', 'gepafin')} {formatDate(a.deadline)} · {__('Creata:', 'gepafin')} {formatDateTime(a.created_at)}
|
|
</span>
|
|
</div>
|
|
{a.status !== 'CLOSED' && isReviewable && (
|
|
<Button icon="pi pi-check" label={__('Chiudi soccorso', 'gepafin')}
|
|
size="small" outlined severity="success"
|
|
onClick={(e) => closeAmendment(e, a)} />
|
|
)}
|
|
</div>
|
|
<div>
|
|
<small className="text-color-secondary">{__('Richiesta istruttore:', 'gepafin')}</small>
|
|
<div style={{ whiteSpace: 'pre-wrap', marginBottom: '0.5rem' }}>{a.request_text}</div>
|
|
{a.response_text && (<>
|
|
<small className="text-color-secondary">{__('Risposta beneficiario', 'gepafin')} ({formatDateTime(a.response_at)}):</small>
|
|
<div style={{ whiteSpace: 'pre-wrap', padding: '0.5rem', background: 'white', borderRadius: '4px', marginTop: '0.25rem' }}>{a.response_text}</div>
|
|
</>)}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
<div className="appPage__spacer"></div>
|
|
</>)}
|
|
|
|
{/* FATTURE — tabella inline unica con raggruppamento per categoria */}
|
|
<div className="appPageSection">
|
|
<h2>{__('Verifica fatture', 'gepafin')}</h2>
|
|
<p className="text-color-secondary" style={{ marginTop: 0 }}>
|
|
{__("Modifica l'importo ammesso direttamente nella riga (salvataggio automatico quando esci dal campo). Clicca ▸ per aprire le note.", 'gepafin')}
|
|
</p>
|
|
|
|
{(() => {
|
|
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 <p className="text-color-secondary">{__('Nessuna fattura caricata', 'gepafin')}</p>;
|
|
}
|
|
|
|
return (
|
|
<DataTable
|
|
value={sortedInvoices}
|
|
dataKey="id"
|
|
size="small"
|
|
rowGroupMode="subheader"
|
|
groupRowsBy="category_code"
|
|
sortMode="single"
|
|
sortField="category_code"
|
|
sortOrder={1}
|
|
tableStyle={{ minWidth: '1100px', width: '100%' }}
|
|
expandedRows={expandedInv}
|
|
onRowToggle={(e) => 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 (
|
|
<div style={{
|
|
display: 'flex', alignItems: 'baseline', justifyContent: 'space-between',
|
|
padding: '0.4rem 0.75rem', background: 'var(--surface-100)',
|
|
borderLeft: '4px solid var(--primary-color)'
|
|
}}>
|
|
<div>
|
|
<strong style={{ color: 'var(--primary-color)', marginRight: '0.5rem' }}>{cat.code}</strong>
|
|
<span>{cat.label}</span>
|
|
</div>
|
|
<div style={{ display: 'flex', gap: '1.5rem', fontSize: '0.9em' }}>
|
|
<div><small className="text-color-secondary">{__('Dichiarato:', 'gepafin')}</small> <strong>{euro(totalDecl)}</strong></div>
|
|
<div><small className="text-color-secondary">{__('Ammesso:', 'gepafin')}</small> <strong style={{ color: 'var(--green-700)' }}>{euro(totalVerif)}</strong></div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}}
|
|
rowExpansionTemplate={(inv) => {
|
|
const draft = invDraft[inv.id] || {};
|
|
return (
|
|
<div style={{ padding: '0.75rem 1rem', background: 'var(--surface-50)' }}>
|
|
<div className="appForm p-fluid" style={{ maxWidth: '800px' }}>
|
|
<div className="appForm__field">
|
|
<label>{__("Note dell'istruttore (motivazione rettifica o rigetto)", 'gepafin')}</label>
|
|
<InputTextarea rows={3} autoResize
|
|
disabled={!isVerifiable}
|
|
value={draft.notes != null ? draft.notes : (inv.verification_notes || '')}
|
|
onChange={(ev) => 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')} />
|
|
</div>
|
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '0.75rem', marginTop: '0.5rem', fontSize: '0.9em' }}>
|
|
<div><small className="text-color-secondary">{__('Dettaglio dichiarato:', 'gepafin')}</small></div>
|
|
<div>{__('Imponibile:', 'gepafin')} <strong>{euro(inv.taxable)}</strong></div>
|
|
<div>IVA: <strong>{euro(inv.vat)}</strong> — {__('Totale:', 'gepafin')} <strong>{euro(inv.total)}</strong></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}}
|
|
>
|
|
<Column expander style={{ width: '3.5rem', minWidth: '3.5rem' }} />
|
|
<Column field="invoice_number" header={__('N°', 'gepafin')} style={{ width: '110px', minWidth: '110px' }} />
|
|
<Column header={__('Data', 'gepafin')} style={{ width: '120px', minWidth: '120px' }}
|
|
body={(r) => {
|
|
const bad = r.date_checks && (r.date_checks.invoice_in_period === false || r.date_checks.payment_in_period === false);
|
|
return <span style={bad ? { color: 'var(--red-600)', fontWeight: 600 } : {}}>
|
|
{formatDate(r.invoice_date)}
|
|
{bad && <i className="pi pi-exclamation-triangle" style={{ marginLeft: 4 }} title={__('Fuori periodo','gepafin')} />}
|
|
</span>;
|
|
}} />
|
|
<Column field="supplier_name" header={__('Fornitore', 'gepafin')} style={{ width: '180px', minWidth: '180px' }} />
|
|
<Column header={__('Descrizione', 'gepafin')}
|
|
body={(r) => <span title={r.description}>{r.description.length > 50 ? r.description.slice(0, 50) + '…' : r.description}</span>} />
|
|
<Column header={declaredLabel} style={{ width: '150px', minWidth: '150px', textAlign: 'right' }}
|
|
body={(r) => <strong>{euro(useTaxable ? r.taxable : r.total)}</strong>} />
|
|
<Column header={ammessoLabel} style={{ width: '170px', minWidth: '170px' }}
|
|
body={(r) => {
|
|
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 (
|
|
<InputNumber
|
|
value={displayVal}
|
|
mode="currency" currency="EUR" locale="it-IT"
|
|
disabled={!isVerifiable}
|
|
inputStyle={{ textAlign: 'right', padding: '0.25rem 0.5rem' }}
|
|
onValueChange={(ev) => 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);
|
|
}
|
|
}}
|
|
/>
|
|
);
|
|
}} />
|
|
<Column header={__('Stato', 'gepafin')} style={{ width: '130px', minWidth: '130px' }}
|
|
body={(r) => {
|
|
const cfg = VERIFICATION_INVOICE_TAG[r.verification_status] || VERIFICATION_INVOICE_TAG.PENDING;
|
|
return <Tag severity={cfg.severity} value={cfg.label} />;
|
|
}} />
|
|
<Column header={__('Azioni', 'gepafin')} style={{ width: '190px', minWidth: '190px' }}
|
|
body={(r) => (
|
|
<div style={{ display: 'flex', gap: '0.25rem' }}>
|
|
<Button icon="pi pi-eye" rounded outlined size="small" severity="info"
|
|
disabled={!r.storage_path}
|
|
onClick={() => openPreview('invoice', r.id, `Fattura ${r.invoice_number}`, r.pdf_filename)}
|
|
tooltip={__('Anteprima', 'gepafin')} tooltipOptions={{ position: 'top' }} />
|
|
<Button icon="pi pi-download" rounded outlined size="small" severity="info"
|
|
disabled={!r.storage_path}
|
|
onClick={() => doDownload('invoice', r.id)}
|
|
tooltip={__('Scarica', 'gepafin')} tooltipOptions={{ position: 'top' }} />
|
|
<Button icon="pi pi-check" rounded outlined size="small"
|
|
severity={r.verification_status === 'AMMESSA' ? 'success' : 'secondary'}
|
|
disabled={!isVerifiable}
|
|
onClick={() => {
|
|
// toggle: se già AMMESSA torno a PENDING
|
|
if (r.verification_status === 'AMMESSA') {
|
|
saveInvoiceInline(r, 'PENDING');
|
|
return;
|
|
}
|
|
const draft = invDraft[r.id];
|
|
if (draft && draft.amount_verified != null) {
|
|
saveInvoiceInline(r);
|
|
} else {
|
|
saveInvoiceInline(r, 'AMMESSA');
|
|
}
|
|
}}
|
|
tooltip={r.verification_status === 'AMMESSA' ? __('Annulla conferma', 'gepafin') : __('Conferma', 'gepafin')}
|
|
tooltipOptions={{ position: 'top' }} />
|
|
<Button icon="pi pi-times" rounded outlined size="small"
|
|
severity={r.verification_status === 'RESPINTA' ? 'danger' : 'secondary'}
|
|
disabled={!isVerifiable}
|
|
onClick={() => {
|
|
if (r.verification_status === 'RESPINTA') {
|
|
saveInvoiceInline(r, 'PENDING');
|
|
} else {
|
|
saveInvoiceInline(r, 'RESPINTA');
|
|
}
|
|
}}
|
|
tooltip={r.verification_status === 'RESPINTA' ? __('Annulla rifiuto', 'gepafin') : __('Respingi', 'gepafin')}
|
|
tooltipOptions={{ position: 'top' }} />
|
|
</div>
|
|
)} />
|
|
</DataTable>
|
|
);
|
|
})()}
|
|
</div>
|
|
|
|
{/* ULA DIPENDENTI — DataTable semplice con box header sopra */}
|
|
{ulaSection.enabled && practice.ula_employees.length > 0 && (<>
|
|
<div className="appPage__spacer"></div>
|
|
<div className="appPageSection">
|
|
<h2>{__('Verifica dipendenti ULA', 'gepafin')}</h2>
|
|
<p className="text-color-secondary" style={{ marginTop: 0 }}>
|
|
{__("Modifica l'FTE ammesso direttamente nella riga (salvataggio automatico quando esci dal campo). Clicca ▸ per aprire le note.", 'gepafin')}
|
|
</p>
|
|
|
|
{(() => {
|
|
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 (
|
|
<div style={{ width: '100%' }}>
|
|
{/* Header-box fuori dalla tabella, stesso stile del subheader delle fatture */}
|
|
<div style={{
|
|
display: 'flex', alignItems: 'baseline', justifyContent: 'space-between',
|
|
padding: '0.5rem 0.75rem', background: 'var(--surface-100)',
|
|
borderLeft: '4px solid var(--primary-color)',
|
|
borderTopLeftRadius: '4px', borderTopRightRadius: '4px',
|
|
borderBottom: '1px solid var(--surface-border)'
|
|
}}>
|
|
<div>
|
|
<strong style={{ color: 'var(--primary-color)', marginRight: '0.5rem' }}>ULA</strong>
|
|
<span>{__('Incremento occupazione (soglia richiesta ≥', 'gepafin')} {Number(ulaSection.threshold || 1).toFixed(2)})</span>
|
|
</div>
|
|
<div style={{ display: 'flex', gap: '1.5rem', fontSize: '0.9em' }}>
|
|
<div><small className="text-color-secondary">{__('FTE dichiarato:', 'gepafin')}</small> <strong>{totalFteDecl.toFixed(2)}</strong></div>
|
|
<div><small className="text-color-secondary">{__('FTE ammesso:', 'gepafin')}</small> <strong style={{ color: thresholdOK ? 'var(--green-700)' : 'var(--red-600)' }}>{totalFteVerif.toFixed(2)}</strong></div>
|
|
</div>
|
|
</div>
|
|
|
|
<DataTable
|
|
value={practice.ula_employees}
|
|
dataKey="id"
|
|
size="small"
|
|
tableStyle={{ minWidth: '1100px', width: '100%' }}
|
|
expandedRows={expandedUla}
|
|
onRowToggle={(e) => setExpandedUla(e.data)}
|
|
rowExpansionTemplate={(emp) => {
|
|
const draft = ulaDraft[emp.id] || {};
|
|
return (
|
|
<div style={{ padding: '0.75rem 1rem', background: 'var(--surface-50)' }}>
|
|
<div className="appForm p-fluid" style={{ maxWidth: '800px' }}>
|
|
<div className="appForm__field">
|
|
<label>{__("Note dell'istruttore (motivazione rettifica)", 'gepafin')}</label>
|
|
<InputTextarea rows={3} autoResize
|
|
disabled={!isVerifiable}
|
|
value={draft.notes != null ? draft.notes : (emp.verification_notes || '')}
|
|
onChange={(ev) => 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')} />
|
|
</div>
|
|
<div style={{ fontSize: '0.9em', marginTop: '0.5rem' }}>
|
|
<small className="text-color-secondary">{__('Dettaglio:', 'gepafin')}</small> {CONTRACT_TYPES[emp.contract_type] || emp.contract_type}
|
|
{emp.role_description && ` · ${emp.role_description}`}
|
|
{' · '}{formatDate(emp.period_start_date)} → {formatDate(emp.period_end_date)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}}
|
|
>
|
|
<Column expander style={{ width: '3.5rem', minWidth: '3.5rem' }} />
|
|
<Column field="codice_fiscale" header="CF" style={{ width: '170px', minWidth: '170px' }} />
|
|
<Column field="full_name" header={__('Nome', 'gepafin')} style={{ width: '150px', minWidth: '150px' }} />
|
|
<Column header={__('Contratto', 'gepafin')} style={{ width: '160px', minWidth: '160px' }}
|
|
body={(r) => <small>{CONTRACT_TYPES[r.contract_type] || r.contract_type}</small>} />
|
|
<Column header={__('Periodo', 'gepafin')}
|
|
body={(r) => <small>{formatDate(r.period_start_date)} → {formatDate(r.period_end_date)}</small>} />
|
|
<Column header={__('FTE dichiarato', 'gepafin')} style={{ width: '150px', minWidth: '150px', textAlign: 'right' }}
|
|
body={(r) => <strong>{Number(r.fte_pct).toFixed(2)}</strong>} />
|
|
<Column header={__('FTE ammesso', 'gepafin')} style={{ width: '170px', minWidth: '170px' }}
|
|
body={(r) => {
|
|
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 (
|
|
<InputNumber
|
|
value={displayVal}
|
|
mode="decimal" minFractionDigits={2} maxFractionDigits={4} min={0} max={1}
|
|
disabled={!isVerifiable}
|
|
inputStyle={{ textAlign: 'right', padding: '0.25rem 0.5rem' }}
|
|
onValueChange={(ev) => 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);
|
|
}
|
|
}}
|
|
/>
|
|
);
|
|
}} />
|
|
<Column header={__('Stato', 'gepafin')} style={{ width: '130px', minWidth: '130px' }}
|
|
body={(r) => {
|
|
const cfg = VERIFICATION_INVOICE_TAG[r.verification_status] || VERIFICATION_INVOICE_TAG.PENDING;
|
|
return <Tag severity={cfg.severity} value={cfg.label} />;
|
|
}} />
|
|
<Column header={__('Azioni', 'gepafin')} style={{ width: '190px', minWidth: '190px' }}
|
|
body={(r) => (
|
|
<div style={{ display: 'flex', gap: '0.25rem' }}>
|
|
<Button icon="pi pi-eye" rounded outlined size="small" severity="info"
|
|
disabled={!r.storage_path}
|
|
onClick={() => openPreview('ula', r.id, `${r.full_name}`, r.supporting_doc_filename)}
|
|
tooltip={__('Anteprima allegato', 'gepafin')} tooltipOptions={{ position: 'top' }} />
|
|
<Button icon="pi pi-download" rounded outlined size="small" severity="info"
|
|
disabled={!r.storage_path}
|
|
onClick={() => doDownload('ula', r.id)}
|
|
tooltip={__('Scarica allegato', 'gepafin')} tooltipOptions={{ position: 'top' }} />
|
|
<Button icon="pi pi-check" rounded outlined size="small"
|
|
severity={r.verification_status === 'AMMESSA' ? 'success' : 'secondary'}
|
|
disabled={!isVerifiable}
|
|
onClick={() => {
|
|
const draft = ulaDraft[r.id];
|
|
if (draft && draft.fte_pct_verified != null) {
|
|
saveUlaInline(r);
|
|
} else {
|
|
saveUlaInline(r, 'AMMESSA');
|
|
}
|
|
}}
|
|
tooltip={__('Conferma', 'gepafin')} tooltipOptions={{ position: 'top' }} />
|
|
<Button icon="pi pi-times" rounded outlined size="small"
|
|
severity={r.verification_status === 'RESPINTA' ? 'danger' : 'secondary'}
|
|
disabled={!isVerifiable}
|
|
onClick={() => saveUlaInline(r, 'RESPINTA')}
|
|
tooltip={__('Respingi', 'gepafin')} tooltipOptions={{ position: 'top' }} />
|
|
</div>
|
|
)} />
|
|
</DataTable>
|
|
</div>
|
|
);
|
|
})()}
|
|
</div>
|
|
</>)}
|
|
|
|
{/* DOCUMENTI */}
|
|
<div className="appPage__spacer"></div>
|
|
<div className="appPageSection">
|
|
<h2>{__('Verifica documenti', 'gepafin')}</h2>
|
|
<ol className="appPageSection__list">
|
|
{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 (
|
|
<li key={dr.code} className="appPageSection__listItem">
|
|
<div className="appPageSection__listItemRow">
|
|
<div style={{ flex: 1 }}>
|
|
<div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center', flexWrap: 'wrap' }}>
|
|
<strong>{dr.label}</strong>
|
|
<code style={{ fontSize: '0.85em' }}>{dr.code}</code>
|
|
{doc.filename ? <Tag severity={cfg.severity} value={cfg.label} /> : <Tag severity="danger" value={__('Non caricato', 'gepafin')} />}
|
|
</div>
|
|
{doc.filename && (
|
|
<div style={{ marginTop: '0.25rem', fontSize: '0.9em', color: 'var(--text-color-secondary)' }}>
|
|
<i className="pi pi-file" /> {doc.filename} · {__('caricato il', 'gepafin')} {formatDateTime(doc.uploaded_at)}
|
|
</div>
|
|
)}
|
|
{doc.verification_notes && (
|
|
<div style={{ marginTop: '0.5rem', padding: '0.4rem 0.6rem', background: 'var(--surface-100)', borderLeft: '3px solid var(--orange-400)', fontSize: '0.9em' }}>
|
|
<i className="pi pi-pencil" style={{ marginRight: '0.4rem' }} />{doc.verification_notes}
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="appPageSection__iconActions">
|
|
<Button icon="pi pi-eye" rounded outlined severity="info"
|
|
disabled={!doc.storage_path}
|
|
onClick={() => openPreview('document', doc.id, `${dr.label}`, doc.filename)}
|
|
tooltip={__('Anteprima', 'gepafin')} tooltipOptions={{ position: 'top' }}
|
|
aria-label={__('Anteprima', 'gepafin')} />
|
|
<Button icon="pi pi-download" rounded outlined severity="info"
|
|
disabled={!doc.storage_path}
|
|
onClick={() => doDownload('document', doc.id)}
|
|
tooltip={__('Scarica', 'gepafin')} tooltipOptions={{ position: 'top' }}
|
|
aria-label={__('Scarica', 'gepafin')} />
|
|
<Button icon="pi pi-clock" rounded outlined severity="warning"
|
|
disabled={!isVerifiable || !doc.filename}
|
|
onClick={() => setDocNoteDialog({ visible: true, doc: { ...doc, verification_notes: doc.verification_notes || '' }, status: 'SCADUTO' })}
|
|
tooltip={__('Segna come scaduto', 'gepafin')} tooltipOptions={{ position: 'top' }}
|
|
aria-label={__('Scaduto', 'gepafin')} />
|
|
<Button icon="pi pi-thumbs-up" rounded outlined
|
|
severity={doc.verification_status === 'VALIDO' ? 'success' : 'secondary'}
|
|
disabled={!isVerifiable || !doc.filename}
|
|
onClick={() => quickVerifyDoc(doc, doc.verification_status === 'VALIDO' ? 'PENDING' : 'VALIDO')}
|
|
tooltip={doc.verification_status === 'VALIDO' ? __('Annulla valido', 'gepafin') : __('Valido', 'gepafin')}
|
|
tooltipOptions={{ position: 'top' }}
|
|
aria-label={__('Valido', 'gepafin')} />
|
|
<Button icon="pi pi-thumbs-down" rounded outlined
|
|
severity={doc.verification_status === 'NON_VALIDO' ? 'danger' : 'secondary'}
|
|
disabled={!isVerifiable || !doc.filename}
|
|
onClick={() => {
|
|
if (doc.verification_status === 'NON_VALIDO') {
|
|
quickVerifyDoc(doc, 'PENDING');
|
|
} else {
|
|
setDocNoteDialog({ visible: true, doc: { ...doc, verification_notes: doc.verification_notes || '' }, status: 'NON_VALIDO' });
|
|
}
|
|
}}
|
|
tooltip={doc.verification_status === 'NON_VALIDO' ? __('Annulla (rimuovi stato)', 'gepafin') : __('Non valido', 'gepafin')}
|
|
tooltipOptions={{ position: 'top' }}
|
|
aria-label={__('Non valido', 'gepafin')} />
|
|
</div>
|
|
</div>
|
|
</li>
|
|
);
|
|
})}
|
|
</ol>
|
|
</div>
|
|
|
|
{/* VERIFICA CONTROLLI AGGIUNTIVI (v2) */}
|
|
{customChecksDefs.length > 0 && (<>
|
|
<div className="appPage__spacer"></div>
|
|
<div className="appPageSection">
|
|
<h2>{__('Verifica controlli aggiuntivi', 'gepafin')}</h2>
|
|
<p className="text-color-secondary" style={{ marginTop: 0 }}>
|
|
{__('Dichiarazioni aggiuntive del beneficiario. Valida ciascun controllo con VALIDO o NON_VALIDO (richiede motivazione). I controlli obbligatori non dichiarati impediscono l\'approvazione.', 'gepafin')}
|
|
</p>
|
|
<ol className="appPageSection__list">
|
|
{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 (
|
|
<li key={def.code} className="appPageSection__listItem">
|
|
<div className="appPageSection__listItemRow">
|
|
<div style={{ flex: 1 }}>
|
|
<div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center', flexWrap: 'wrap' }}>
|
|
<strong>{def.label}</strong>
|
|
<code style={{ fontSize: '0.85em' }}>{def.code}</code>
|
|
{def.required && <Tag severity="warning" value={__('Obbligatorio', 'gepafin')} />}
|
|
{declared
|
|
? <Tag severity="success" value={__('Dichiarato', 'gepafin')} />
|
|
: <Tag severity={missingRequired ? 'danger' : 'secondary'} value={__('Non dichiarato', 'gepafin')} />}
|
|
<Tag severity={cfg.severity} value={cfg.label} />
|
|
</div>
|
|
{def.description && (
|
|
<div style={{ marginTop: '0.3rem', fontSize: '0.9em', color: 'var(--text-color-secondary)', whiteSpace: 'pre-wrap' }}>
|
|
{def.description}
|
|
</div>
|
|
)}
|
|
{def.requires_document && (
|
|
<div style={{ marginTop: '0.4rem', fontSize: '0.9em' }}>
|
|
{hasDoc ? (
|
|
<span>
|
|
<i className="pi pi-file-pdf" style={{ color: 'var(--primary-color)', marginRight: '0.3rem' }} />
|
|
<strong>{val.filename_original}</strong>
|
|
{val.size_bytes && <small className="text-color-secondary"> ({(val.size_bytes/1024).toFixed(1)} KB)</small>}
|
|
</span>
|
|
) : (
|
|
<span className="text-color-secondary">
|
|
<i className="pi pi-file" style={{ marginRight: '0.3rem' }} />
|
|
{__('Nessun documento allegato', 'gepafin')}
|
|
</span>
|
|
)}
|
|
</div>
|
|
)}
|
|
{val.verification_notes && (
|
|
<div style={{ marginTop: '0.5rem', padding: '0.4rem 0.6rem', background: 'var(--surface-100)', borderLeft: '3px solid var(--orange-400)', fontSize: '0.9em' }}>
|
|
<i className="pi pi-pencil" style={{ marginRight: '0.4rem' }} />{val.verification_notes}
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="appPageSection__iconActions">
|
|
<Button icon="pi pi-eye" rounded outlined severity="info"
|
|
disabled={!hasDoc}
|
|
onClick={() => previewCustomCheckDoc(val)}
|
|
tooltip={__('Anteprima', 'gepafin')} tooltipOptions={{ position: 'top' }} />
|
|
<Button icon="pi pi-download" rounded outlined severity="info"
|
|
disabled={!hasDoc}
|
|
onClick={() => downloadCustomCheckDoc(val)}
|
|
tooltip={__('Scarica', 'gepafin')} tooltipOptions={{ position: 'top' }} />
|
|
<Button icon="pi pi-thumbs-up" rounded outlined
|
|
severity={stat === 'VALIDO' ? 'success' : 'secondary'}
|
|
disabled={!isVerifiable}
|
|
onClick={() => {
|
|
if (stat === 'VALIDO') {
|
|
verifyCustomCheckInline({ code: def.code }, 'PENDING', null);
|
|
} else {
|
|
verifyCustomCheckInline({ code: def.code }, 'VALIDO', val.verification_notes);
|
|
}
|
|
}}
|
|
tooltip={stat === 'VALIDO' ? __('Annulla valido', 'gepafin') : __('Valido', 'gepafin')}
|
|
tooltipOptions={{ position: 'top' }} />
|
|
<Button icon="pi pi-thumbs-down" rounded outlined
|
|
severity={stat === 'NON_VALIDO' ? 'danger' : 'secondary'}
|
|
disabled={!isVerifiable}
|
|
onClick={() => {
|
|
if (stat === 'NON_VALIDO') {
|
|
verifyCustomCheckInline({ code: def.code }, 'PENDING', null);
|
|
} else {
|
|
openCcVerifyDialog({ code: def.code, verification_notes: val.verification_notes }, 'NON_VALIDO');
|
|
}
|
|
}}
|
|
tooltip={stat === 'NON_VALIDO' ? __('Annulla non valido', 'gepafin') : __('Non valido', 'gepafin')}
|
|
tooltipOptions={{ position: 'top' }} />
|
|
</div>
|
|
</div>
|
|
</li>
|
|
);
|
|
})}
|
|
</ol>
|
|
</div>
|
|
</>)}
|
|
|
|
{/* VERBALE ISTRUTTORIA */}
|
|
{isVerifiable && (<>
|
|
<div className="appPage__spacer"></div>
|
|
<div className="appPageSection" style={{ background: 'var(--surface-50)', padding: '1.25rem', borderRadius: '6px' }}>
|
|
<h2 style={{ margin: '0 0 0.5rem 0' }}>{__('Verbale istruttoria', 'gepafin')}</h2>
|
|
|
|
<div style={{ marginBottom: '1rem' }}>
|
|
<h3 style={{ fontSize: '1rem', margin: '0.5rem 0' }}>{__('Checklist finale', 'gepafin')}</h3>
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
|
|
{[
|
|
{ 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 => (
|
|
<div key={item.id} style={{ display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
|
|
<Checkbox inputId={item.id} checked={!!checklist[item.id]}
|
|
onChange={(e) => saveFinalNotes({
|
|
instructor_checklist: { ...checklist, [item.id]: e.checked }
|
|
})} />
|
|
<label htmlFor={item.id}>{item.label}</label>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="appForm p-fluid">
|
|
<div className="appForm__field">
|
|
<label>{__('Note sintetiche di istruttoria', 'gepafin')}</label>
|
|
<InputTextarea rows={4} autoResize
|
|
defaultValue={practice.instructor_final_notes || ''}
|
|
onBlur={(e) => {
|
|
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')} />
|
|
<small className="text-color-secondary">{__('Le note si salvano quando esci dal campo.', 'gepafin')}</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</>)}
|
|
|
|
{/* ============ DIALOGS ============ */}
|
|
|
|
{/* Preview PDF reale — iframe con blob autenticato */}
|
|
<FilePreviewDialog
|
|
visible={previewDialog.visible}
|
|
onHide={closePreview}
|
|
entityType={previewDialog.entityType}
|
|
entityId={previewDialog.entityId}
|
|
title={previewDialog.title}
|
|
filename={previewDialog.filename}
|
|
/>
|
|
|
|
{/* Rettifica fattura */}
|
|
|
|
|
|
{/* Rettifica ULA */}
|
|
|
|
|
|
{/* Note documento (scaduto/non valido) */}
|
|
<Dialog visible={docNoteDialog.visible} style={{ width: '520px' }}
|
|
header={docNoteDialog.status === 'SCADUTO' ? __('Segna documento scaduto', 'gepafin') : __('Marca documento non valido', 'gepafin')}
|
|
modal onHide={() => setDocNoteDialog({ visible: false, doc: null, status: null })}>
|
|
{docNoteDialog.doc && (
|
|
<form className="appForm p-fluid" onSubmit={(e) => { e.preventDefault(); saveDocNote(); }}>
|
|
<div className="appForm__field">
|
|
<label>{__('Motivazione', 'gepafin')}</label>
|
|
<InputTextarea rows={4} autoResize
|
|
value={docNoteDialog.doc.verification_notes}
|
|
onChange={(e) => 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')} />
|
|
</div>
|
|
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '0.75rem', marginTop: '1rem' }}>
|
|
<Button type="button" outlined label={__('Annulla', 'gepafin')} onClick={() => setDocNoteDialog({ visible: false, doc: null, status: null })} />
|
|
<Button type="submit" label={__('Conferma', 'gepafin')} icon="pi pi-check" severity={docNoteDialog.status === 'SCADUTO' ? 'warning' : 'danger'} />
|
|
</div>
|
|
</form>
|
|
)}
|
|
</Dialog>
|
|
|
|
{/* DIALOG APPROVA */}
|
|
<Dialog visible={approveDialog.visible} style={{ width: '480px' }}
|
|
header={__('Approva pratica', 'gepafin')} modal
|
|
onHide={() => setApproveDialog({ visible: false, amount: null })}>
|
|
<form className="appForm p-fluid" onSubmit={(e) => { e.preventDefault(); doApprove(); }}>
|
|
<div className="appForm__field">
|
|
<label>{__('Remissione da approvare (€)', 'gepafin')}</label>
|
|
<InputNumber value={approveDialog.amount} mode="currency" currency="EUR" locale="it-IT"
|
|
onValueChange={(e) => setApproveDialog(d => ({ ...d, amount: e.value }))} />
|
|
<small className="text-color-secondary">
|
|
{__('Valore calcolato da verificati:', 'gepafin')} {euro(totals.remission_due)}
|
|
</small>
|
|
</div>
|
|
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '0.75rem', marginTop: '1rem' }}>
|
|
<Button type="button" outlined label={__('Annulla', 'gepafin')} onClick={() => setApproveDialog({ visible: false, amount: null })} />
|
|
<Button type="submit" label={__('Conferma approvazione', 'gepafin')} icon="pi pi-check" severity="success" />
|
|
</div>
|
|
</form>
|
|
</Dialog>
|
|
|
|
{/* DIALOG RESPINGI */}
|
|
<Dialog visible={rejectDialog.visible} style={{ width: '560px' }}
|
|
header={__('Respingi pratica', 'gepafin')} modal
|
|
onHide={() => setRejectDialog({ visible: false, reason: '' })}>
|
|
<form className="appForm p-fluid" onSubmit={(e) => { e.preventDefault(); doReject(); }}>
|
|
<div className="appForm__field">
|
|
<label>{__('Motivazione del rifiuto', 'gepafin')}</label>
|
|
<InputTextarea value={rejectDialog.reason} rows={4} autoResize
|
|
onChange={(e) => setRejectDialog(d => ({ ...d, reason: e.target.value }))} />
|
|
<small className="text-color-secondary">{__('Minimo 10 caratteri. Verrà inviata al beneficiario.', 'gepafin')}</small>
|
|
</div>
|
|
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '0.75rem', marginTop: '1rem' }}>
|
|
<Button type="button" outlined label={__('Annulla', 'gepafin')} onClick={() => setRejectDialog({ visible: false, reason: '' })} />
|
|
<Button type="submit" label={__('Conferma rifiuto', 'gepafin')} icon="pi pi-times" severity="danger" />
|
|
</div>
|
|
</form>
|
|
</Dialog>
|
|
|
|
{/* DIALOG SOCCORSO */}
|
|
<Dialog visible={amendDialog.visible} style={{ width: '560px' }}
|
|
header={__('Avvia soccorso istruttorio', 'gepafin')} modal
|
|
onHide={() => setAmendDialog({ visible: false, text: '', deadline: null })}>
|
|
<form className="appForm p-fluid" onSubmit={(e) => { e.preventDefault(); doAmend(); }}>
|
|
<div className="appForm__field">
|
|
<label>{__('Richiesta al beneficiario', 'gepafin')}</label>
|
|
<InputTextarea value={amendDialog.text} rows={5} autoResize
|
|
onChange={(e) => setAmendDialog(d => ({ ...d, text: e.target.value }))}
|
|
placeholder={__('Descrivi le integrazioni richieste...', 'gepafin')} />
|
|
</div>
|
|
<div className="appForm__field">
|
|
<label>{__('Scadenza risposta', 'gepafin')}</label>
|
|
<Calendar value={amendDialog.deadline} dateFormat="dd/mm/yy" showIcon
|
|
onChange={(e) => setAmendDialog(d => ({ ...d, deadline: e.value }))} />
|
|
</div>
|
|
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '0.75rem', marginTop: '1rem' }}>
|
|
<Button type="button" outlined label={__('Annulla', 'gepafin')} onClick={() => setAmendDialog({ visible: false, text: '', deadline: null })} />
|
|
<Button type="submit" label={__('Invia richiesta', 'gepafin')} icon="pi pi-send" severity="warning" />
|
|
</div>
|
|
</form>
|
|
</Dialog>
|
|
{/* DIALOG VERIFICA CUSTOM CHECK (motivazione NON_VALIDO) */}
|
|
<Dialog visible={ccVerifyDialog.visible} style={{ width: '520px' }}
|
|
header={__('Marca controllo come non valido', 'gepafin')} modal
|
|
onHide={() => setCcVerifyDialog({ visible: false, cc: null, status: null, notes: '' })}>
|
|
<form className="appForm p-fluid" onSubmit={(e) => { e.preventDefault(); confirmCcVerify(); }}>
|
|
<div className="appForm__field">
|
|
<label>{__('Motivazione (obbligatoria)', 'gepafin')}</label>
|
|
<InputTextarea value={ccVerifyDialog.notes} rows={4} autoResize
|
|
onChange={(e) => setCcVerifyDialog(d => ({ ...d, notes: e.target.value }))}
|
|
placeholder={__('Es: dichiarazione non coerente con il bando...', 'gepafin')} />
|
|
</div>
|
|
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '0.75rem', marginTop: '1rem' }}>
|
|
<Button type="button" outlined label={__('Annulla', 'gepafin')}
|
|
onClick={() => setCcVerifyDialog({ visible: false, cc: null, status: null, notes: '' })} />
|
|
<Button type="submit" label={__('Conferma', 'gepafin')} icon="pi pi-times" severity="danger"
|
|
disabled={!ccVerifyDialog.notes || ccVerifyDialog.notes.trim().length < 5} />
|
|
</div>
|
|
</form>
|
|
</Dialog>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default IstruttoriaPratica;
|