feat(rendicontazione): lato istruttore - queue + review + soccorso istruttorio

Backend (rendicontazione-api):
- 4 nuove colonne su remission_practice: assigned_instructor_id, reviewed_at,
  reviewed_by, rejection_reason, approved_remission
- Nuova tabella remission_amendment_request (id, practice_id, request_text,
  scope jsonb, deadline, status AWAITING/RESPONSE_RECEIVED/CLOSED/EXPIRED,
  response_text, audit cols)
- Router instructor.py con 8 endpoint:
  GET /instructor/queue (SUBMITTED pool + UNDER_REVIEW/AWAITING_AMENDMENT assigned,
  o tutto se manager/superadmin)
  GET /instructor/{id} (practice + gate_check + amendments)
  POST /instructor/{id}/claim (SUBMITTED -> UNDER_REVIEW)
  POST /instructor/{id}/approve (approved_remission opz, default = remission_due calcolato)
  POST /instructor/{id}/reject (rejection_reason min 10 char)
  POST /instructor/{id}/amendment (crea soccorso: request_text + deadline)
  POST /instructor/{id}/amendment/{aid}/close (chiude soccorso, pratica torna UNDER_REVIEW)
  POST /instructor/{id}/amendment/{aid}/respond-beneficiary (benef risponde)
- GET /{id} ora ritorna anche amendments (per beneficiario)

Frontend:
- Pagina IstruttoriaQueue (125 righe): coda pratiche con stato, istruttore
  assegnato, erogato, remission_due calcolata, azioni contestuali
- Pagina IstruttoriaPratica (483 righe): dettaglio pratica readonly per istruttore,
  riepilogo esteso, amendments panel con chiudi, gate check, fatture/ULA/docs,
  3 Dialog per approva/respingi/soccorso
- PraticaRendicontazioneEdit esteso con sezione 'Richieste di soccorso istruttorio'
  visibile al beneficiario + Dialog rispondi con request_text dell'istruttore
- Sidebar: voce 'Istruttoria rendicontazioni' per EVALUATE_APPLICATIONS
  (pre_instructor + instructor_manager)
- Routes /istruttoria e /istruttoria/:id con gate sui tre ruoli

Test end-to-end OK: benef crea+submit, istruttore claim+amendment, benef risponde,
istruttore chiude+approva -> APPROVED remission 8500 EUR su NAPOLI SAS (erogato 17000).

Utenti sandbox creati:
- istruttore@sandbox.local / istruttore123 (ROLE_PRE_INSTRUCTOR)
- manager@sandbox.local / manager123 (ROLE_INSTRUCTOR_MANAGER)
This commit is contained in:
BFLOWS Sandbox
2026-04-18 10:15:22 +02:00
parent 9c483ade34
commit 115f31bdef
6 changed files with 775 additions and 0 deletions

View File

@@ -179,3 +179,62 @@ const extendPractice = {
// Attach to main export
Object.assign(RendicontazioneService, extendPractice);
// ====================== ISTRUTTORE ======================
const extendInstructor = {
instructorQueue(onSuccess, onError) {
fetch(`${BASE_URL}/api/remission-practices/instructor/queue`, {
method: 'GET', mode: 'cors', headers: buildHeaders()
}).then(r => 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);