diff --git a/src/modules/rendicontazione/pages/IstruttoriaPratica.js b/src/modules/rendicontazione/pages/IstruttoriaPratica.js index 3405423..9af38b0 100644 --- a/src/modules/rendicontazione/pages/IstruttoriaPratica.js +++ b/src/modules/rendicontazione/pages/IstruttoriaPratica.js @@ -14,6 +14,8 @@ import { DataTable } from 'primereact/datatable'; import { Column } from 'primereact/column'; import { Checkbox } from 'primereact/checkbox'; import { ConfirmPopup, confirmPopup } from 'primereact/confirmpopup'; +import { Editor } from 'primereact/editor'; +import { FileUpload } from 'primereact/fileupload'; import RendicontazioneService from '../service/rendicontazioneService'; import FilePreviewDialog from '../components/FilePreviewDialog'; @@ -48,6 +50,7 @@ const VERIFICATION_DOC_TAG = { }; const AMENDMENT_STATUS = { + DRAFT: { severity: 'secondary', label: 'Bozza (non inviata)' }, AWAITING: { severity: 'warning', label: 'Attesa risposta' }, RESPONSE_RECEIVED: { severity: 'info', label: 'Risposta ricevuta' }, CLOSED: { severity: 'success', label: 'Chiusa' }, @@ -81,7 +84,13 @@ const IstruttoriaPratica = () => { const [approveDialog, setApproveDialog] = useState({ visible: false, amount: null }); const [rejectDialog, setRejectDialog] = useState({ visible: false, reason: '' }); - const [amendDialog, setAmendDialog] = useState({ visible: false, text: '', deadline: null }); + const [amendDialog, setAmendDialog] = useState({ + visible: false, mode: 'create', amendmentId: null, + text: '', deadline: null, response_days: 15, internal_note: '', + instructor_file: null, current_doc_path: null + }); + const [extendDialog, setExtendDialog] = useState({ visible: false, amendment: null, extended_days: 7, motivation: '' }); + const amendFileRef = useRef(null); // v2: custom_checks (merge schema+values dal BE) const [customChecks, setCustomChecks] = useState([]); const [ccVerifyDialog, setCcVerifyDialog] = useState({ visible: false, cc: null, status: null, notes: '' }); @@ -125,7 +134,7 @@ const IstruttoriaPratica = () => { return practice?.schema_snapshot?.custom_checks || []; }, [practice]); - const openAmendments = amendments.filter(a => a.status === 'AWAITING' || a.status === 'RESPONSE_RECEIVED'); + const openAmendments = amendments.filter(a => ['DRAFT','AWAITING','RESPONSE_RECEIVED'].includes(a.status)); 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); @@ -353,18 +362,123 @@ const IstruttoriaPratica = () => { 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; + const _stripHtml = (html) => { + if (!html) return ''; + const tmp = document.createElement('div'); + tmp.innerHTML = html; + return (tmp.textContent || tmp.innerText || '').trim(); + }; + + const resetAmendDialog = () => setAmendDialog({ + visible: false, mode: 'create', amendmentId: null, + text: '', deadline: null, response_days: 15, internal_note: '', + instructor_file: null, current_doc_path: null + }); + + const openCreateAmendDialog = () => setAmendDialog({ + visible: true, mode: 'create', amendmentId: null, + text: '', deadline: null, response_days: 15, internal_note: '', + instructor_file: null, current_doc_path: null + }); + + const openEditAmendDialog = (a) => setAmendDialog({ + visible: true, mode: 'edit', amendmentId: a.id, + text: a.request_text || '', + deadline: a.deadline ? new Date(a.deadline) : null, + response_days: a.response_days || 15, + internal_note: a.internal_note || '', + instructor_file: null, current_doc_path: a.amendment_document_path || null + }); + + const doAmend = (sendAfterSave = false) => { + const plainText = _stripHtml(amendDialog.text); + if (plainText.length < 10) { + toast.current?.show({ severity: 'warn', summary: __('Testo troppo corto (min 10 caratteri)', '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 deadlineStr = typeof amendDialog.deadline === 'string' + ? amendDialog.deadline + : amendDialog.deadline.toISOString().slice(0, 10); + const body = { + request_text: amendDialog.text, deadline: deadlineStr, + response_days: amendDialog.response_days, + internal_note: amendDialog.internal_note || null + }; + + const uploadIfNeeded = (savedAmendment, then) => { + if (amendDialog.instructor_file) { + RendicontazioneService.uploadAmendmentDocument(practiceId, savedAmendment.id, amendDialog.instructor_file, + () => then(savedAmendment), onErr); + } else { then(savedAmendment); } + }; + + const finalStep = (savedAmendment) => { + if (sendAfterSave) { + RendicontazioneService.sendAmendment(practiceId, savedAmendment.id, + (resp) => { resetAmendDialog(); afterOk(__('Soccorso inviato al beneficiario', 'gepafin'))(resp); }, onErr); + } else { + resetAmendDialog(); + afterOk(__('Bozza salvata', 'gepafin'))({ data: savedAmendment }); + } + }; + + if (amendDialog.mode === 'create') { + RendicontazioneService.createAmendment(practiceId, body, + (resp) => uploadIfNeeded(resp.data, finalStep), onErr); + } else { + RendicontazioneService.updateAmendment(practiceId, amendDialog.amendmentId, body, + (resp) => uploadIfNeeded(resp.data, finalStep), onErr); + } }; + + const sendDraftAmendment = (ev, a) => { + confirmPopup({ + target: ev.currentTarget, + message: __('Inviare il soccorso al beneficiario? Dopo l invio non sara piu modificabile.', 'gepafin'), + icon: 'pi pi-send', + acceptLabel: __('Invia', 'gepafin'), rejectLabel: __('Annulla', 'gepafin'), + accept: () => RendicontazioneService.sendAmendment(practiceId, a.id, + afterOk(__('Soccorso inviato', 'gepafin')), onErr) + }); + }; + + const deleteDraftAmendment = (ev, a) => { + confirmPopup({ + target: ev.currentTarget, + message: __('Eliminare questa bozza di soccorso?', 'gepafin'), + icon: 'pi pi-exclamation-triangle', + acceptLabel: __('Elimina', 'gepafin'), rejectLabel: __('Annulla', 'gepafin'), + acceptClassName: 'p-button-danger', + accept: () => RendicontazioneService.deleteAmendment(practiceId, a.id, + afterOk(__('Bozza eliminata', 'gepafin')), onErr) + }); + }; + + const doExtendAmendment = () => { + if (!extendDialog.extended_days || extendDialog.extended_days < 1) { + toast.current?.show({ severity: 'warn', summary: __('Indicare giorni di proroga', 'gepafin') }); return; + } + RendicontazioneService.extendAmendment(practiceId, extendDialog.amendment.id, + extendDialog.extended_days, extendDialog.motivation || null, + (resp) => { + setExtendDialog({ visible: false, amendment: null, extended_days: 7, motivation: '' }); + afterOk(__('Scadenza prorogata', 'gepafin'))(resp); + }, onErr); + }; + + const sendReminder = (ev, a) => { + confirmPopup({ + target: ev.currentTarget, + message: __('Inviare un reminder al beneficiario? Il backend accodera una PEC di sollecito.', 'gepafin'), + icon: 'pi pi-bell', + acceptLabel: __('Invia reminder', 'gepafin'), rejectLabel: __('Annulla', 'gepafin'), + accept: () => RendicontazioneService.sendAmendmentReminder(practiceId, a.id, + afterOk(__('Reminder accodato', 'gepafin')), onErr) + }); + }; + const closeAmendment = (ev, a) => { confirmPopup({ target: ev.currentTarget, @@ -469,7 +583,9 @@ const IstruttoriaPratica = () => {