diff --git a/src/layouts/DefaultLayout/components/AppSidebar/index.js b/src/layouts/DefaultLayout/components/AppSidebar/index.js
index 47f69c8..2c77e79 100644
--- a/src/layouts/DefaultLayout/components/AppSidebar/index.js
+++ b/src/layouts/DefaultLayout/components/AppSidebar/index.js
@@ -27,6 +27,13 @@ const AppSidebar = () => {
id: 2,
enable: intersection(permissions, ['MANAGE_TENDERS']).length
},
+ {
+ label: __('Istruttoria rendicontazioni', 'gepafin'),
+ icon: 'pi pi-check-square',
+ href: '/istruttoria',
+ id: 12,
+ enable: intersection(permissions, ['EVALUATE_APPLICATIONS']).length
+ },
{
label: __('Rendicontazione', 'gepafin'),
icon: 'pi pi-receipt',
diff --git a/src/modules/rendicontazione/pages/IstruttoriaPratica.js b/src/modules/rendicontazione/pages/IstruttoriaPratica.js
new file mode 100644
index 0000000..648aab8
--- /dev/null
+++ b/src/modules/rendicontazione/pages/IstruttoriaPratica.js
@@ -0,0 +1,483 @@
+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 { InputText } from 'primereact/inputtext';
+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 { ConfirmPopup, confirmPopup } from 'primereact/confirmpopup';
+
+import RendicontazioneService from '../service/rendicontazioneService';
+
+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' },
+ UNDER_REVIEW: { severity: 'warning', label: 'In valutazione' },
+ AWAITING_AMENDMENT: { severity: 'warning', label: 'Soccorso aperto' },
+ APPROVED: { severity: 'success', label: 'Approvata' },
+ REJECTED: { severity: 'danger', label: 'Respinta' }
+};
+
+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) => '€ ' + 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); // {practice, gate_check, amendments}
+
+ 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 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]);
+
+ 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 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);
+
+ // ---------- actions ----------
+ const afterOk = (msg) => () => {
+ 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 handleClaim = (e) => {
+ confirmPopup({
+ target: e.currentTarget,
+ message: __('Prendere in carico la pratica? Lo stato passerà a In valutazione.', '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', 'gepafin'), detail: __('Minimo 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 = (e, amendment) => {
+ confirmPopup({
+ target: e.currentTarget,
+ message: __('Chiudi questa richiesta di soccorso? La pratica torna in valutazione.', 'gepafin'),
+ icon: 'pi pi-info-circle',
+ acceptLabel: __('Chiudi', 'gepafin'), rejectLabel: __('Annulla', 'gepafin'),
+ accept: () => RendicontazioneService.closeAmendment(practiceId, amendment.id,
+ afterOk(__('Soccorso chiuso', 'gepafin')), onErr)
+ });
+ };
+
+ // ---------- 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 perCat = totals.per_category || {};
+
+ const invoicesOfCat = (code) => practice.invoices.filter(i => i.category_code === code);
+
+ return (
+
+
+
+
+
+
{__('Istruttoria pratica', 'gepafin')}
+
+
+ {practice.schema_snapshot?.template_label || `Call #${practice.call_id}`} · {__('Pratica', 'gepafin')} #{practice.application_id}
+
+
+
+
+
+
+
+
+
+ {/* ACTIONS */}
+
+
+ navigate('/istruttoria')} />
+
+ {practice.status === 'SUBMITTED' && (
+
+ )}
+
+ {isDecidable && (<>
+ setApproveDialog({ visible: true, amount: totals.remission_due || 0 })} />
+ setRejectDialog({ visible: true, reason: '' })} />
+ 0}
+ onClick={() => setAmendDialog({ visible: true, text: '', deadline: null })} />
+ >)}
+
+
+
+
+
+ {/* RIEPILOGO */}
+
+
{__('Riepilogo', 'gepafin')}
+
+
+
{__('Azienda', 'gepafin')}
+
Company #{practice.company_id}
+
+
+
{__('Erogato', 'gepafin')}
+
{euro(practice.amount_erogato)}
+
+
+
{__('Regime IVA', 'gepafin')}
+
{practice.iva_regime || '—'}
+
+
+
{__('Totale fatture', 'gepafin')}
+
{euro(totals.grand_total || 0)}
+
+
+
{__('Cap remissione', 'gepafin')}
+
{euro(totals.max_remission || 0)}
+
+
+
{__('Remissione calcolata', 'gepafin')}
+
{euro(totals.remission_due || 0)}
+
+ {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 && (
+
closeAmendment(e, a)} />
+ )}
+
+
+
{__('Richiesta istruttore:', 'gepafin')}
+
{a.request_text}
+ {a.response_text && (<>
+
+ {__('Risposta beneficiario', 'gepafin')} ({formatDateTime(a.response_at)}):
+
+
{a.response_text}
+ >)}
+
+
+ );
+ })}
+
+
+
+ >)}
+
+ {/* GATE CHECKS */}
+ {gate && (
+
+
{__('Requisiti di validità', 'gepafin')}
+
+ {gate.checks.map((c, i) => (
+
+
+
+
{c.label}
+
{c.detail}
+
+
+ ))}
+
+
+ )}
+
+
+
+ {/* FATTURE PER CATEGORIA */}
+
+
{__('Fatture rendicontate', 'gepafin')}
+
+ {categories.map(cat => {
+ const invs = invoicesOfCat(cat.code);
+ const total = perCat[cat.code] || 0;
+ return (
+
+
+
+ {cat.code} — {cat.label}
+
+
+
{euro(total)}
+
{invs.length} {__('fatture', 'gepafin')}
+
+
+ {invs.length > 0 ? (
+
+
+ formatDate(r.invoice_date)} />
+ formatDate(r.payment_date)} />
+
+ {r.description.slice(0, 40)}{r.description.length > 40 ? '…' : ''} } />
+ euro(r.taxable)} />
+ euro(r.vat)} />
+ euro(r.total)} />
+
+ ) :
{__('Nessuna fattura', 'gepafin')} }
+
+ );
+ })}
+
+
+
+ {/* ULA */}
+ {ulaSection.enabled && (<>
+
+
+
{__('Dipendenti ULA', 'gepafin')}
+ {practice.ula_employees.length > 0 ? (
+
+
+
+ CONTRACT_TYPES[r.contract_type] || r.contract_type} />
+
+ Number(r.fte_pct).toFixed(2)} />
+ `${formatDate(r.period_start_date)} → ${formatDate(r.period_end_date)}`} />
+ r.supporting_doc_filename
+ ? {r.supporting_doc_filename}
+ : — } />
+
+ ) :
{__('Nessun dipendente caricato', 'gepafin')}
}
+
+ >)}
+
+
+
+ {/* DOCUMENTI */}
+
+
{__('Documenti', 'gepafin')}
+
+ {docsRequired.map(dr => {
+ const existing = practice.documents.find(d => d.doc_code === dr.code);
+ return (
+
+
+
+
{dr.label}
+
{dr.code}
+
+
+ {existing?.filename
+ ? {existing.filename} — {__('caricato il', 'gepafin')} {formatDateTime(existing.uploaded_at)}
+ : {__('Non caricato', 'gepafin')} }
+
+
+ );
+ })}
+
+
+
+ {/* ---------- DIALOG APPROVA ---------- */}
+
setApproveDialog({ visible: false, amount: null })}>
+
+
+
+ {/* ---------- DIALOG RESPINGI ---------- */}
+
setRejectDialog({ visible: false, reason: '' })}>
+
+
+
+ {/* ---------- DIALOG SOCCORSO ---------- */}
+
setAmendDialog({ visible: false, text: '', deadline: null })}>
+
+
+
+ );
+};
+
+export default IstruttoriaPratica;
diff --git a/src/modules/rendicontazione/pages/IstruttoriaQueue.js b/src/modules/rendicontazione/pages/IstruttoriaQueue.js
new file mode 100644
index 0000000..85f46b5
--- /dev/null
+++ b/src/modules/rendicontazione/pages/IstruttoriaQueue.js
@@ -0,0 +1,125 @@
+import React, { useEffect, useState, useRef } from 'react';
+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 RendicontazioneService from '../service/rendicontazioneService';
+
+const STATUS_TAGS = {
+ SUBMITTED: { severity: 'info', label: 'Da prendere in carico' },
+ UNDER_REVIEW: { severity: 'warning', label: 'In valutazione' },
+ AWAITING_AMENDMENT: { severity: 'warning', label: 'Soccorso istruttorio' }
+};
+
+const euro = (v) => '€ ' + Number(v || 0).toLocaleString('it-IT', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
+const formatDate = (d) => d ? new Date(d).toLocaleDateString('it-IT') : '—';
+
+const IstruttoriaQueue = () => {
+ const navigate = useNavigate();
+ const toast = useRef(null);
+ const [items, setItems] = useState([]);
+ const [isManager, setIsManager] = useState(false);
+ const [loading, setLoading] = useState(true);
+
+ const load = () => {
+ setLoading(true);
+ RendicontazioneService.instructorQueue(
+ (resp) => {
+ setItems(resp?.data?.items || []);
+ setIsManager(!!resp?.data?.manager_view);
+ setLoading(false);
+ },
+ (err) => {
+ toast.current?.show({ severity: 'error', summary: __('Errore', 'gepafin'), detail: err?.detail });
+ setLoading(false);
+ }
+ );
+ };
+
+ useEffect(() => { load(); }, []);
+
+ const callTpl = (row) => (
+
+
{row.call_name || `Bando #${row.call_id}`}
+
{row.company_name} · pratica #{row.application_id}
+
+ );
+ const statusTpl = (row) => {
+ const c = STATUS_TAGS[row.status] || { severity: 'secondary', label: row.status };
+ return
+
+ {row.open_amendments > 0 && (
+
+
+
+ )}
+
;
+ };
+ const submittedTpl = (row) => row.submitted_at ? formatDate(row.submitted_at) : '—';
+ const erogatoTpl = (row) => {euro(row.amount_erogato)} ;
+ const remissionTpl = (row) => row.remission_due != null
+ ? {euro(row.remission_due)}
+ : — ;
+ const progressTpl = (row) => (
+
+ {row.invoice_count} {__('fatt.','gepafin')} · {row.ula_count} {__('dip.','gepafin')} · {row.document_count} {__('doc','gepafin')}
+
+ );
+ const actionsTpl = (row) => {
+ const label = row.status === 'SUBMITTED' ? __('Apri e prendi in carico', 'gepafin') : __('Apri', 'gepafin');
+ return navigate(`/istruttoria/${row.id}`)} />;
+ };
+ const assignedTpl = (row) => {
+ if (!row.assigned_instructor_id) return — ;
+ return #{row.assigned_instructor_id} ;
+ };
+
+ return (
+
+
+
+
+
{__('Coda istruttoria', 'gepafin')}
+
+ {isManager
+ ? __('Vista manager: vedi tutte le pratiche in carico a tutti gli istruttori.', 'gepafin')
+ : __('Pool di pratiche da prendere in carico + pratiche assegnate a te.', 'gepafin')}
+
+
+
+
+
+
+ {loading &&
}
+ {!loading && items.length === 0 && (
+
+
+
{__('Nessuna pratica in coda al momento.', 'gepafin')}
+
+ )}
+ {!loading && items.length > 0 && (
+
+
+
+
+
+
+
+
+
+
+ )}
+
+
+ );
+};
+
+export default IstruttoriaQueue;
diff --git a/src/modules/rendicontazione/pages/PraticaRendicontazioneEdit.js b/src/modules/rendicontazione/pages/PraticaRendicontazioneEdit.js
index 2461700..31ae0f6 100644
--- a/src/modules/rendicontazione/pages/PraticaRendicontazioneEdit.js
+++ b/src/modules/rendicontazione/pages/PraticaRendicontazioneEdit.js
@@ -79,6 +79,8 @@ const PraticaRendicontazioneEdit = () => {
const [invDialog, setInvDialog] = useState({ visible: false, data: null });
// modal dipendente ULA
const [empDialog, setEmpDialog] = useState({ visible: false, data: null });
+ // modal risposta soccorso istruttorio
+ const [amendDialog, setAmendDialog] = useState({ visible: false, amendment: null, responseText: '' });
// ---------- load ----------
const load = useCallback(() => {
@@ -225,6 +227,18 @@ const PraticaRendicontazioneEdit = () => {
});
};
+ const submitAmendmentResponse = () => {
+ if (!amendDialog.responseText || amendDialog.responseText.trim().length < 5) {
+ toast.current?.show({ severity: 'warn', summary: __('Risposta troppo corta', 'gepafin') });
+ return;
+ }
+ RendicontazioneService.respondAmendmentBeneficiary(
+ practiceId, amendDialog.amendment.id, amendDialog.responseText,
+ (resp) => { setAmendDialog({ visible: false, amendment: null, responseText: '' });
+ afterMutation(__('Risposta inviata all\'istruttore', 'gepafin'))(resp); },
+ onMutationError);
+ };
+
// ---------- render guards ----------
if (loading) {
return ;
@@ -323,6 +337,53 @@ const PraticaRendicontazioneEdit = () => {
)}
+ {/* SOCCORSO ISTRUTTORIO (se presente) */}
+ {practice.amendments && practice.amendments.length > 0 && (<>
+
+
+
{__('Richieste di soccorso istruttorio', 'gepafin')}
+
+ {__('L\'istruttore ha chiesto integrazioni o chiarimenti. Rispondi al più presto.', 'gepafin')}
+
+
+ {practice.amendments.map(a => {
+ const statusCfg = {
+ AWAITING: { sev: 'warning', label: 'In attesa della tua risposta' },
+ RESPONSE_RECEIVED: { sev: 'info', label: 'Risposta inviata, in attesa di chiusura' },
+ CLOSED: { sev: 'success', label: 'Chiusa' },
+ EXPIRED: { sev: 'danger', label: 'Scaduta' }
+ }[a.status] || { sev: 'secondary', label: a.status };
+ return (
+
+
+
+
+
+ {__('Scadenza:', 'gepafin')} {new Date(a.deadline).toLocaleDateString('it-IT')}
+
+
+ {a.status === 'AWAITING' && (
+
setAmendDialog({ visible: true, amendment: a, responseText: '' })} />
+ )}
+
+
+
{__('Richiesta istruttore:', 'gepafin')}
+
{a.request_text}
+ {a.response_text && (<>
+
{__('Tua risposta:', 'gepafin')}
+
{a.response_text}
+ >)}
+
+
+ );
+ })}
+
+
+ >)}
+
{/* SEZIONE 1: REGIME IVA */}
@@ -567,6 +628,30 @@ const PraticaRendicontazioneEdit = () => {
)}
+ {/* ---------- DIALOG RISPOSTA SOCCORSO ---------- */}
+ setAmendDialog({ visible: false, amendment: null, responseText: '' })}>
+ {amendDialog.amendment && (
+
+ )}
+
+
{/* ---------- DIALOG DIPENDENTE ULA ---------- */}
handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
+ },
+
+ instructorViewPractice(practiceId, onSuccess, onError) {
+ fetch(`${BASE_URL}/api/remission-practices/instructor/${practiceId}`, {
+ method: 'GET', mode: 'cors', headers: buildHeaders()
+ }).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
+ },
+
+ claimPractice(practiceId, onSuccess, onError) {
+ fetch(`${BASE_URL}/api/remission-practices/instructor/${practiceId}/claim`, {
+ method: 'POST', mode: 'cors', headers: buildHeaders()
+ }).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
+ },
+
+ approvePractice(practiceId, body, onSuccess, onError) {
+ fetch(`${BASE_URL}/api/remission-practices/instructor/${practiceId}/approve`, {
+ method: 'POST', mode: 'cors', headers: buildHeaders(),
+ body: JSON.stringify(body || {})
+ }).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
+ },
+
+ rejectPractice(practiceId, reason, onSuccess, onError) {
+ fetch(`${BASE_URL}/api/remission-practices/instructor/${practiceId}/reject`, {
+ method: 'POST', mode: 'cors', headers: buildHeaders(),
+ body: JSON.stringify({ rejection_reason: reason })
+ }).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
+ },
+
+ createAmendment(practiceId, body, onSuccess, onError) {
+ fetch(`${BASE_URL}/api/remission-practices/instructor/${practiceId}/amendment`, {
+ method: 'POST', mode: 'cors', headers: buildHeaders(),
+ body: JSON.stringify(body)
+ }).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
+ },
+
+ closeAmendment(practiceId, amendmentId, onSuccess, onError) {
+ fetch(`${BASE_URL}/api/remission-practices/instructor/${practiceId}/amendment/${amendmentId}/close`, {
+ method: 'POST', mode: 'cors', headers: buildHeaders()
+ }).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
+ },
+
+ respondAmendmentBeneficiary(practiceId, amendmentId, responseText, onSuccess, onError) {
+ fetch(`${BASE_URL}/api/remission-practices/instructor/${practiceId}/amendment/${amendmentId}/respond-beneficiary`, {
+ method: 'POST', mode: 'cors', headers: buildHeaders(),
+ body: JSON.stringify({ response_text: responseText })
+ }).then(r => handleResponse(r, onSuccess, onError)).catch(e => handleError(e, onError));
+ }
+};
+
+Object.assign(RendicontazioneService, extendInstructor);
diff --git a/src/routes.js b/src/routes.js
index d287960..002e772 100644
--- a/src/routes.js
+++ b/src/routes.js
@@ -19,6 +19,8 @@ import RendicontazioneHome from './modules/rendicontazione/pages/Rendicontazione
import RendicontazioniMie from './modules/rendicontazione/pages/RendicontazioniMie';
import PraticaRendicontazioneEdit from './modules/rendicontazione/pages/PraticaRendicontazioneEdit';
import DevSwitchUser from './modules/rendicontazione/pages/DevSwitchUser';
+import IstruttoriaQueue from './modules/rendicontazione/pages/IstruttoriaQueue';
+import IstruttoriaPratica from './modules/rendicontazione/pages/IstruttoriaPratica';
import BandoFlowEdit from './pages/BandoFlowEdit';
import Imieibandi from './pages/Imieibandi';
import BandoApplication from './pages/BandoApplication';
@@ -171,6 +173,20 @@ const routes = ({ role, chosenCompanyId }) => {
{'ROLE_SUPER_ADMIN' === role ? : }
}/>
+
+ {'ROLE_PRE_INSTRUCTOR' === role ? : null}
+ {'ROLE_INSTRUCTOR_MANAGER' === role ? : null}
+ {'ROLE_SUPER_ADMIN' === role ? : null}
+ {'ROLE_BENEFICIARY' === role ? : null}
+ {'ROLE_CONFIDI' === role ? : null}
+ }/>
+
+ {'ROLE_PRE_INSTRUCTOR' === role ? : null}
+ {'ROLE_INSTRUCTOR_MANAGER' === role ? : null}
+ {'ROLE_SUPER_ADMIN' === role ? : null}
+ {'ROLE_BENEFICIARY' === role ? : null}
+ {'ROLE_CONFIDI' === role ? : null}
+ }/>
{'ROLE_SUPER_ADMIN' === role ? : null}
{'ROLE_BENEFICIARY' === role ? : null}