longevity: API giornale di bordo per cliente (aggiungi/cancella)

POST unico con campo azione, autore e ruolo sempre da locals.user, redirect
303 alla scheda cliente #giornale.
Refs #16

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-27 22:23:03 +02:00
parent 37b1a316d8
commit cef08aaf9f
@@ -0,0 +1,67 @@
import type { APIRoute } from 'astro';
import { getLongevityDb } from '../../../../lib/longevity/db';
import { aggiungiVoce, cancellaVoce } from '../../../../lib/longevity/giornale';
export const prerender = false;
const REGEX_CODE = /^ISL-\d{4}$/;
// Riservato a admin+trainer da src/lib/auth.ts (RULES, /^\/api\/longevity\/gestionale(\/|$)/).
/**
* Aggiunge o cancella una voce del giornale di bordo (#16, solo staff): un unico endpoint
* per le due azioni, come verifica.ts fa per conferma/scarto — un campo `azione` invece di
* due rotte. Autore e ruolo SEMPRE da `locals.user`, mai dal form (stesso principio di
* checkup.ts e verifica.ts: chi scrive la richiesta non decide chi ne è l'autore).
*/
export const POST: APIRoute = async ({ request, locals }) => {
const form = await request.formData();
const db = getLongevityDb();
const autore = locals.user!.username;
const ruolo = locals.user!.role;
const azione = form.get('azione');
if (azione === 'aggiungi') {
const clientCode = String(form.get('client_code') ?? '').trim();
// Senza un codice valido non c'è una scheda a cui tornare — stesso trattamento di
// checkup.ts: l'unico ritorno sensato è l'indice del gestionale.
if (!REGEX_CODE.test(clientCode)) {
return redirect(`/longevity/gestionale?errore=${encodeURIComponent('Codice cliente non valido.')}`);
}
const data = String(form.get('data') ?? '').trim();
const testo = String(form.get('testo') ?? '');
const esito = aggiungiVoce(db, { clientCode, data, autore, testo });
if (!esito.ok) {
return tornaA(clientCode, `?errore=${encodeURIComponent(esito.errore)}`);
}
return tornaA(clientCode, '?giornale=aggiunta');
}
if (azione === 'cancella') {
const idRaw = form.get('id');
const id = typeof idRaw === 'string' ? Number(idRaw) : NaN;
if (!Number.isInteger(id)) {
return redirect(`/longevity/gestionale?errore=${encodeURIComponent('Voce non valida.')}`);
}
const esito = cancellaVoce(db, { id, utente: autore, ruolo });
if (!esito.ok) {
// Nessuna voce da cui leggere il client_code (id inesistente, o non c'era già): non
// c'è una scheda su cui tornare, come per un id di misura inesistente in verifica.ts.
return redirect(`/longevity/gestionale?errore=${encodeURIComponent(esito.errore)}`);
}
return tornaA(esito.clientCode, '?giornale=cancellata');
}
return redirect(`/longevity/gestionale?errore=${encodeURIComponent('Azione non valida.')}`);
};
const redirect = (location: string): Response => new Response(null, { status: 303, headers: { Location: location } });
// Il redirect torna sempre alla sezione #giornale della scheda, dove il form e l'elenco
// vivono — così chi aggiunge o cancella una nota si ritrova esattamente dove era.
const tornaA = (clientCode: string, query: string): Response =>
redirect(`/longevity/gestionale/cliente/${clientCode}${query}#giornale`);