diff --git a/src/modules/rendicontazione/pages/RendicontazioniMie.js b/src/modules/rendicontazione/pages/RendicontazioniMie.js
index d8b8548..804cf49 100644
--- a/src/modules/rendicontazione/pages/RendicontazioniMie.js
+++ b/src/modules/rendicontazione/pages/RendicontazioniMie.js
@@ -3,37 +3,43 @@ import { __ } from '@wordpress/i18n';
import { useNavigate } from 'react-router-dom';
import { Button } from 'primereact/button';
-import { DataTable } from 'primereact/datatable';
-import { Column } from 'primereact/column';
import { Tag } from 'primereact/tag';
import { Toast } from 'primereact/toast';
import { Skeleton } from 'primereact/skeleton';
+import { Dialog } from 'primereact/dialog';
+import { InputText } from 'primereact/inputtext';
+import { Checkbox } from 'primereact/checkbox';
import RendicontazioneService from '../service/rendicontazioneService';
const STATUS_TAGS = {
- NOT_STARTED: { severity: 'info', label: 'Da avviare' },
- DRAFT: { severity: 'warning', label: 'In compilazione' },
- SUBMITTED: { severity: 'info', label: 'Inviata' },
- UNDER_REVIEW: { severity: 'info', label: 'In valutazione' },
- APPROVED: { severity: 'success', label: 'Approvata' },
- REJECTED: { severity: 'danger', label: 'Respinta' },
+ DRAFT: { severity: 'warning', label: 'In compilazione' },
+ SUBMITTED: { severity: 'info', label: 'Inviata' },
+ UNDER_REVIEW: { severity: 'info', label: 'In valutazione' },
+ APPROVED: { severity: 'success', label: 'Approvata' },
+ REJECTED: { severity: 'danger', label: 'Respinta' },
AWAITING_AMENDMENT: { severity: 'warning', label: 'Soccorso istruttorio' }
};
+const fmtEur = (v) => {
+ const n = Number(v || 0);
+ return `€ ${n.toLocaleString('it-IT', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
+};
+
const RendicontazioniMie = () => {
const navigate = useNavigate();
const toast = useRef(null);
- const [rows, setRows] = useState([]);
+ const [apps, setApps] = useState([]);
const [loading, setLoading] = useState(true);
+ const [startDialog, setStartDialog] = useState(null); // { application_id, max_tranches, next_seq, show_copy_ula }
+ const [startForm, setStartForm] = useState({ period_label: '', copy_ula: true });
+ const [starting, setStarting] = useState(false);
const load = () => {
setLoading(true);
RendicontazioneService.listMine(
(resp) => {
- const practices = (resp?.data?.practices || []).map(p => ({ ...p, isReady: false }));
- const ready = (resp?.data?.ready_to_start || []).map(r => ({ ...r, isReady: true }));
- setRows([...practices, ...ready]);
+ setApps(resp?.data?.applications || []);
setLoading(false);
},
(err) => {
@@ -45,53 +51,137 @@ const RendicontazioniMie = () => {
useEffect(() => { load(); /* eslint-disable-next-line */ }, []);
- const handleStart = (applicationId) => {
- RendicontazioneService.startPractice(applicationId,
+ const openStartDialog = (app) => {
+ const nextSeq = (app.tranches?.length || 0) + 1;
+ setStartDialog({
+ application_id: app.application_id,
+ call_name: app.call_name,
+ max_tranches: app.max_tranches,
+ next_seq: nextSeq,
+ show_copy_ula: nextSeq > 1,
+ max_remission_next: app.max_remission_next_tranche,
+ });
+ setStartForm({ period_label: '', copy_ula: nextSeq > 1 });
+ };
+
+ const confirmStart = () => {
+ if (!startDialog) return;
+ setStarting(true);
+ RendicontazioneService.startPractice(
+ startDialog.application_id,
(resp) => {
- toast.current?.show({ severity: 'success', summary: __('Rendicontazione avviata', 'gepafin') });
+ setStarting(false);
+ setStartDialog(null);
+ toast.current?.show({ severity: 'success', summary: resp?.message || __('Tranche avviata', 'gepafin') });
navigate(`/rendicontazioni/${resp.data.id}`);
},
- (err) => toast.current?.show({ severity: 'error', summary: __('Avvio fallito', 'gepafin'), detail: err?.detail })
+ (err) => {
+ setStarting(false);
+ toast.current?.show({ severity: 'error', summary: __('Avvio fallito', 'gepafin'), detail: err?.detail });
+ },
+ {
+ period_label: startForm.period_label?.trim() || null,
+ copy_ula_from_previous: startForm.copy_ula,
+ }
);
};
- const callTpl = (row) => (
-
-
{row.call_name || `Bando #${row.call_id}`}
-
{row.company_name}
-
- );
+ const renderApplicationCard = (app) => {
+ const hasTranches = (app.tranches?.length || 0) > 0;
+ const nextSeq = (app.tranches?.length || 0) + 1;
+ const canStart = !!app.can_start_new;
+ const blockReason = app.start_blocked_reason;
- const erogatoTpl = (row) => {
- const v = Number(row.amount_erogato || 0);
- return
€ {v.toLocaleString('it-IT', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} ;
- };
-
- const statusTpl = (row) => {
- const key = row.isReady ? 'NOT_STARTED' : (row.status || 'DRAFT');
- const conf = STATUS_TAGS[key] || { severity: 'secondary', label: key };
- return
;
- };
-
- const progressTpl = (row) => {
- if (row.isReady) return
— ;
return (
-
- {row.invoice_count || 0} {__('fatture','gepafin')} · {row.ula_count || 0} {__('dipendenti','gepafin')} · {row.document_count || 0} {__('doc','gepafin')}
-
- );
- };
+
+ {/* HEADER CARD */}
+
+
+
+ {app.call_name || `Bando #${app.call_id}`}
+
+
+ {app.company_name || ''} · {__('Domanda', 'gepafin')} #{app.application_id}
+
+
+
+
{__('Finanziamento erogato','gepafin')}
+
{fmtEur(app.amount_erogato)}
+
+
- const actionsTpl = (row) => {
- if (row.isReady) {
- return
handleStart(row.application_id)} />;
- }
- const isEditable = row.status === 'DRAFT';
- return navigate(`/rendicontazioni/${row.id}`)} />;
+ {/* CAP INFO */}
+
+
+
{__('Cap remissione totale', 'gepafin')}
+
{fmtEur(app.max_remission_global)}
+
+
+
{__('Già approvato', 'gepafin')}
+
0 ? '#22543d' : 'inherit' }}>{fmtEur(app.already_approved_sum)}
+
+
+
{__('Disponibile prossima tranche', 'gepafin')}
+
{fmtEur(app.max_remission_next_tranche)}
+
+
+
{__('Tranches', 'gepafin')}
+
{app.tranches?.length || 0} / {app.max_tranches}
+
+
+
+ {/* TRANCHES LIST */}
+ {hasTranches && (
+
+ {app.tranches.map((t) => {
+ const tag = STATUS_TAGS[t.status] || { severity: 'secondary', label: t.status };
+ const isEditable = t.status === 'DRAFT';
+ return (
+
+
+ T{t.sequence_number}
+
+
+
{t.period_label || {__('nessun periodo indicato','gepafin')} }
+
+ {t.invoice_count || 0} {__('fatture','gepafin')} · {t.ula_count || 0} {__('dipendenti','gepafin')} · {t.document_count || 0} {__('doc','gepafin')}
+
+
+
+
navigate(`/rendicontazioni/${t.id}`)} />
+
+ );
+ })}
+
+ )}
+
+ {/* BOTTONE NUOVA TRANCHE */}
+
+ {!canStart && blockReason && (
+
+
+ {blockReason}
+
+ )}
+ openStartDialog(app)} />
+
+
+ );
};
return (
@@ -100,32 +190,79 @@ const RendicontazioniMie = () => {
{__('Le mie rendicontazioni', 'gepafin')}
-
{__('Per ogni pratica finanziata puoi avviare la rendicontazione delle spese e il calcolo della remissione del debito.', 'gepafin')}
+
{__('Per ogni pratica finanziata puoi avviare la rendicontazione delle spese e il calcolo della remissione del debito. I bandi che prevedono piu tranches permettono rendicontazioni multi-fase.', 'gepafin')}
-
- {loading &&
}
- {!loading && rows.length === 0 && (
-
-
-
{__('Non ci sono rendicontazioni da avviare al momento.', 'gepafin')}
-
- {__('Le rendicontazioni diventano disponibili dopo la firma del contratto e quando l\'ente ha pubblicato lo schema di rendicontazione per il bando.', 'gepafin')}
-
+ {loading &&
}
+
+ {!loading && apps.length === 0 && (
+
+
+
{__('Non ci sono rendicontazioni disponibili al momento.', 'gepafin')}
+
+ {__('Le rendicontazioni diventano disponibili dopo la firma del contratto e quando l\'ente ha pubblicato lo schema di rendicontazione per il bando.', 'gepafin')}
+
+
+ )}
+
+ {!loading && apps.length > 0 && apps.map(renderApplicationCard)}
+
+ {/* START DIALOG */}
+
!starting && setStartDialog(null)}
+ modal
+ footer={(
+
+ setStartDialog(null)} outlined disabled={starting} />
+
+
+ )}>
+ {startDialog && (
+
+
+ {__('Stai per avviare la tranche', 'gepafin')}
+ {' '}T{startDialog.next_seq} / {startDialog.max_tranches}
+ {' '}{__('del bando', 'gepafin')} {startDialog.call_name} .
+
+
+ {__('Cap remissione disponibile per questa tranche', 'gepafin')}:
+ {' '}{fmtEur(startDialog.max_remission_next)}
+
+
+
+ {__('Periodo / fase (opzionale)', 'gepafin')}
+ setStartForm(f => ({ ...f, period_label: e.target.value }))}
+ placeholder={__('es. "I trimestre 2021", "Stato avanzamento II"', 'gepafin')}
+ disabled={starting} />
+
+ {__('Descrizione libera per identificare la tranche. Apparirà sul verbale.', 'gepafin')}
+
+
+
+ {startDialog.show_copy_ula && (
+
+
+ setStartForm(f => ({ ...f, copy_ula: e.checked }))}
+ disabled={starting} />
+
+ {__('Copia i dipendenti ULA dalla tranche precedente', 'gepafin')}
+
+
+
+ {__('Se attivo, i dipendenti censiti nella tranche precedente saranno precaricati. Potrai modificarli o rimuoverli prima di inviare.', 'gepafin')}
+
+
+ )}
)}
- {!loading && rows.length > 0 && (
-
-
-
-
-
-
-
- )}
-
+
);
};
diff --git a/src/modules/rendicontazione/service/rendicontazioneService.js b/src/modules/rendicontazione/service/rendicontazioneService.js
index 0a4cad7..782b415 100644
--- a/src/modules/rendicontazione/service/rendicontazioneService.js
+++ b/src/modules/rendicontazione/service/rendicontazioneService.js
@@ -97,10 +97,21 @@ const extendPractice = {
}).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
},
- startPractice(applicationId, onSuccess, onError) {
+ startPractice(applicationId, onSuccess, onError, opts = {}) {
+ // opts: { period_label?: string, copy_ula_from_previous?: bool }
fetch(`${BASE_URL}/api/remission-practices/start`, {
method: 'POST', mode: 'cors', headers: buildHeaders(),
- body: JSON.stringify({ application_id: applicationId })
+ body: JSON.stringify({
+ application_id: applicationId,
+ period_label: opts.period_label ?? null,
+ copy_ula_from_previous: opts.copy_ula_from_previous !== false,
+ })
+ }).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
+ },
+
+ copyUlaOptions(practiceId, onSuccess, onError) {
+ fetch(`${BASE_URL}/api/remission-practices/${practiceId}/copy-ula-options`, {
+ method: 'GET', mode: 'cors', headers: buildHeaders()
}).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
},
@@ -400,3 +411,84 @@ const extendVerbale = {
};
Object.assign(RendicontazioneService, extendVerbale);
+
+// ====================== v2 CUSTOM CHECKS ======================
+const extendCustomChecks = {
+ listCustomChecks(practiceId, onSuccess, onError) {
+ fetch(`${BASE_URL}/api/remission-practices/${practiceId}/custom-checks`, {
+ method: 'GET', mode: 'cors', headers: buildHeaders()
+ }).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
+ },
+
+ declareCustomCheck(practiceId, code, declared, file, onSuccess, onError) {
+ const fd = new FormData();
+ fd.append('beneficiary_declared', declared ? 'true' : 'false');
+ if (file) fd.append('file', file);
+ fetch(`${BASE_URL}/api/remission-practices/${practiceId}/custom-checks/${code}/declare`, {
+ method: 'PUT', mode: 'cors',
+ headers: _buildBearerOnly(), // no Content-Type: boundary auto
+ body: fd
+ }).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
+ },
+
+ deleteCustomCheckDocument(practiceId, code, onSuccess, onError) {
+ fetch(`${BASE_URL}/api/remission-practices/${practiceId}/custom-checks/${code}/document`, {
+ method: 'DELETE', mode: 'cors', headers: buildHeaders()
+ }).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
+ },
+
+ verifyCustomCheck(practiceId, code, body, onSuccess, onError) {
+ fetch(`${BASE_URL}/api/remission-practices/${practiceId}/custom-checks/${code}/verify`, {
+ method: 'PUT', mode: 'cors', headers: buildHeaders(),
+ body: JSON.stringify(body)
+ }).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
+ },
+
+ fetchCustomCheckDocumentBlob(practiceId, code, inline, onSuccess, onError) {
+ fetch(`${BASE_URL}/api/remission-practices/${practiceId}/custom-checks/${code}/document?inline=${inline ? 1 : 0}`, {
+ method: 'GET', mode: 'cors', headers: _buildBearerOnly()
+ }).then(async r => {
+ if (r.status < 200 || r.status >= 300) {
+ let detail = r.statusText;
+ try { const j = await r.json(); detail = j.detail || detail; } catch(e){}
+ if (onError) onError({ status: r.status, detail });
+ return;
+ }
+ let filename = 'file';
+ const cd = r.headers.get('Content-Disposition') || '';
+ const m = cd.match(/filename="([^"]+)"/);
+ if (m) filename = m[1];
+ const blob = await r.blob();
+ const objectUrl = URL.createObjectURL(blob);
+ if (onSuccess) onSuccess({ blob, objectUrl, filename });
+ }).catch(e => handleError(e, onError));
+ }
+};
+Object.assign(RendicontazioneService, extendCustomChecks);
+
+
+// ====================== v2 MANAGER ISTRUTTORE ======================
+const extendAssignmentManager = {
+ managerAssignments(onSuccess, onError) {
+ fetch(`${BASE_URL}/api/remission-practices/instructor-manager/assignments`, {
+ method: 'GET', mode: 'cors', headers: buildHeaders()
+ }).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
+ },
+
+ managerInstructorsList(onSuccess, onError) {
+ fetch(`${BASE_URL}/api/remission-practices/instructor-manager/instructors`, {
+ method: 'GET', mode: 'cors', headers: buildHeaders()
+ }).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
+ },
+
+ reassignInstructor(practiceId, newInstructorId, reason, onSuccess, onError) {
+ fetch(`${BASE_URL}/api/remission-practices/instructor/${practiceId}/reassign`, {
+ method: 'POST', mode: 'cors', headers: buildHeaders(),
+ body: JSON.stringify({
+ new_instructor_id: newInstructorId,
+ reassignment_reason: reason || null,
+ })
+ }).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
+ }
+};
+Object.assign(RendicontazioneService, extendAssignmentManager);