Sistemato attività
This commit is contained in:
@@ -1,3 +1,9 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "StatoTassonomia" AS ENUM ('CONFERMATA', 'DA_APPROVARE');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "TipoNotifica" AS ENUM ('TASSONOMIA_PROPOSTA', 'ATTIVITA_IN_ATTESA', 'ATTIVITA_PUBBLICATA', 'ATTIVITA_BOZZA_NOTA');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "stato_attivita" (
|
||||
"id" VARCHAR(2) NOT NULL,
|
||||
@@ -56,6 +62,8 @@ CREATE TABLE "branca" (
|
||||
"data_creazione" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"data_modifica" TIMESTAMP(3) NOT NULL,
|
||||
"utente_modifica" TEXT NOT NULL,
|
||||
"creato_da_id" TEXT,
|
||||
"stato" "StatoTassonomia" NOT NULL DEFAULT 'CONFERMATA',
|
||||
|
||||
CONSTRAINT "branca_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
@@ -81,6 +89,8 @@ CREATE TABLE "periodo_anno" (
|
||||
"data_creazione" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"data_modifica" TIMESTAMP(3) NOT NULL,
|
||||
"utente_modifica" TEXT NOT NULL,
|
||||
"creato_da_id" TEXT,
|
||||
"stato" "StatoTassonomia" NOT NULL DEFAULT 'CONFERMATA',
|
||||
|
||||
CONSTRAINT "periodo_anno_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
@@ -121,6 +131,8 @@ CREATE TABLE "categoria" (
|
||||
"data_creazione" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"data_modifica" TIMESTAMP(3) NOT NULL,
|
||||
"utente_modifica" TEXT NOT NULL,
|
||||
"creato_da_id" TEXT,
|
||||
"stato" "StatoTassonomia" NOT NULL DEFAULT 'CONFERMATA',
|
||||
|
||||
CONSTRAINT "categoria_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
@@ -174,6 +186,34 @@ CREATE TABLE "categoria_materiale" (
|
||||
CONSTRAINT "categoria_materiale_pkey" PRIMARY KEY ("materiale_id","categoria_id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "notifica" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"tipo" "TipoNotifica" NOT NULL,
|
||||
"messaggio" TEXT NOT NULL,
|
||||
"link" TEXT,
|
||||
"destinatario_id" TEXT,
|
||||
"letta" BOOLEAN NOT NULL DEFAULT false,
|
||||
"data_creazione" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "notifica_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "notifica_destinatario_id_idx" ON "notifica"("destinatario_id");
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "nota_attivita" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"attivita_id" INTEGER NOT NULL,
|
||||
"testo" VARCHAR(1000) NOT NULL,
|
||||
"autore" TEXT NOT NULL,
|
||||
"autore_id" TEXT NOT NULL,
|
||||
"data_creazione" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "nota_attivita_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "attivita" ADD CONSTRAINT "attivita_padre_fkey" FOREIGN KEY ("padre") REFERENCES "attivita"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
@@ -222,3 +262,6 @@ ALTER TABLE "categoria_materiale" ADD CONSTRAINT "categoria_materiale_materiale_
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "categoria_materiale" ADD CONSTRAINT "categoria_materiale_categoria_id_fkey" FOREIGN KEY ("categoria_id") REFERENCES "categoria"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "nota_attivita" ADD CONSTRAINT "nota_attivita_attivita_id_fkey" FOREIGN KEY ("attivita_id") REFERENCES "attivita"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (i.e. Git)
|
||||
provider = "postgresql"
|
||||
provider = "postgresql"
|
||||
@@ -7,6 +7,33 @@ datasource db {
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
enum StatoTassonomia {
|
||||
CONFERMATA
|
||||
DA_APPROVARE
|
||||
}
|
||||
|
||||
enum TipoNotifica {
|
||||
TASSONOMIA_PROPOSTA
|
||||
ATTIVITA_IN_ATTESA
|
||||
ATTIVITA_PUBBLICATA
|
||||
ATTIVITA_BOZZA_NOTA
|
||||
}
|
||||
|
||||
// destinatarioId null = notifica "broadcast" visibile a chiunque abbia ruolo admin/moderatore
|
||||
// (usata per segnalare nuove proposte/attività da revisionare); altrimenti è personale.
|
||||
model Notifica {
|
||||
id Int @id @default(autoincrement())
|
||||
tipo TipoNotifica
|
||||
messaggio String
|
||||
link String?
|
||||
destinatarioId String? @map("destinatario_id")
|
||||
letta Boolean @default(false)
|
||||
dataCreazione DateTime @default(now()) @map("data_creazione")
|
||||
|
||||
@@index([destinatarioId])
|
||||
@@map("notifica")
|
||||
}
|
||||
|
||||
model StatoAttivita {
|
||||
id String @id @db.VarChar(2)
|
||||
nome String
|
||||
@@ -63,16 +90,32 @@ model Attivita {
|
||||
categoriaLinks CategoriaAttivita[]
|
||||
materialeLinks MaterialeAttivita[]
|
||||
periodoAnnoLinks PeriodoAnnoAttivita[]
|
||||
noteList NotaAttivita[]
|
||||
|
||||
@@map("attivita")
|
||||
}
|
||||
|
||||
model NotaAttivita {
|
||||
id Int @id @default(autoincrement())
|
||||
attivitaId Int @map("attivita_id")
|
||||
testo String @db.VarChar(1000)
|
||||
autore String
|
||||
autoreId String @map("autore_id")
|
||||
dataCreazione DateTime @default(now()) @map("data_creazione")
|
||||
|
||||
attivita Attivita @relation(fields: [attivitaId], references: [id])
|
||||
|
||||
@@map("nota_attivita")
|
||||
}
|
||||
|
||||
model Branca {
|
||||
id Int @id @default(autoincrement())
|
||||
nome String
|
||||
inizioEta Int? @map("inizio_eta")
|
||||
fineEta Int? @map("fine_eta")
|
||||
colore String? @db.VarChar(10)
|
||||
stato StatoTassonomia @default(CONFERMATA)
|
||||
creatoDaId String? @map("creato_da_id")
|
||||
dataCreazione DateTime @default(now()) @map("data_creazione")
|
||||
dataModifica DateTime @updatedAt @map("data_modifica")
|
||||
utenteModifica String @map("utente_modifica")
|
||||
@@ -102,6 +145,8 @@ model PeriodoAnno {
|
||||
nome String
|
||||
inizioMese Int? @map("inizio_mese")
|
||||
fineMese Int? @map("fine_mese")
|
||||
stato StatoTassonomia @default(CONFERMATA)
|
||||
creatoDaId String? @map("creato_da_id")
|
||||
dataCreazione DateTime @default(now()) @map("data_creazione")
|
||||
dataModifica DateTime @updatedAt @map("data_modifica")
|
||||
utenteModifica String @map("utente_modifica")
|
||||
@@ -148,6 +193,8 @@ model Categoria {
|
||||
nome String
|
||||
padreId Int? @map("padre")
|
||||
tipoId String @map("tipo")
|
||||
stato StatoTassonomia @default(CONFERMATA)
|
||||
creatoDaId String? @map("creato_da_id")
|
||||
dataCreazione DateTime @default(now()) @map("data_creazione")
|
||||
dataModifica DateTime @updatedAt @map("data_modifica")
|
||||
utenteModifica String @map("utente_modifica")
|
||||
|
||||
@@ -10,6 +10,7 @@ async function main() {
|
||||
{ id: "PU", nome: "Pubblicato" },
|
||||
{ id: "BO", nome: "Bozza" },
|
||||
{ id: "PR", nome: "Privato" },
|
||||
{ id: "IA", nome: "In attesa di approvazione" },
|
||||
].map((stato) =>
|
||||
prisma.statoAttivita.upsert({
|
||||
where: { id: stato.id },
|
||||
@@ -102,6 +103,19 @@ async function main() {
|
||||
create: { ...categoria, utenteModifica: UTENTE_MODIFICA },
|
||||
});
|
||||
}
|
||||
|
||||
// Branca/Categoria/PeriodoAnno sopra sono inserite con id espliciti: l'upsert non passa
|
||||
// mai dal DEFAULT della colonna, quindi la sequence Postgres dell'id non avanza e resta
|
||||
// disallineata rispetto al MAX(id) reale. Se non la si risincronizza qui, la prima riga
|
||||
// creata in seguito con id auto-generato (es. una tassonomia proposta da un utente) va in
|
||||
// conflitto su un id già esistente.
|
||||
await Promise.all(
|
||||
["branca", "categoria", "periodo_anno", "materiale"].map((tabella) =>
|
||||
prisma.$executeRawUnsafe(
|
||||
`SELECT setval(pg_get_serial_sequence('"${tabella}"', 'id'), COALESCE((SELECT MAX(id) FROM "${tabella}"), 1))`
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
main()
|
||||
|
||||
@@ -3,9 +3,12 @@ import cors from 'cors';
|
||||
import { env } from './config/env';
|
||||
import { authenticate } from './middlewares/authenticate';
|
||||
import { errorHandler } from './middlewares/errorHandler';
|
||||
import { optionalAuthenticate } from './middlewares/optionalAuthenticate';
|
||||
import { attivitaPublicRouter } from './modules/attivita/attivita.router.public';
|
||||
import { attivitaPrivateRouter } from './modules/attivita/attivita.router.private';
|
||||
import { autocompleteRouter } from './modules/autocomplete/autocomplete.router';
|
||||
import { notificheRouter } from './modules/notifiche/notifiche.router';
|
||||
import { tassonomieRouter } from './modules/tassonomie/tassonomie.router';
|
||||
|
||||
export const app = express();
|
||||
|
||||
@@ -19,9 +22,11 @@ app.get('/health', (req, res) => {
|
||||
res.json({ status: 'ok' });
|
||||
});
|
||||
|
||||
app.use('/public/attivita', attivitaPublicRouter);
|
||||
app.use('/public/attivita', optionalAuthenticate, attivitaPublicRouter);
|
||||
app.use('/private/attivita', authenticate, attivitaPrivateRouter);
|
||||
app.use('/public/autocomplete', autocompleteRouter);
|
||||
app.use('/private/tassonomie', authenticate, tassonomieRouter);
|
||||
app.use('/private/notifiche', authenticate, notificheRouter);
|
||||
|
||||
app.use((req, res) => {
|
||||
res.status(404).json({ message: 'not found' });
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import jwt, { JwtPayload } from 'jsonwebtoken';
|
||||
import jwksClient from 'jwks-rsa';
|
||||
import { env } from '../config/env';
|
||||
import { AuthContext } from './auth.types';
|
||||
|
||||
const client = jwksClient({
|
||||
jwksUri: `${env.keycloak.baseUrl}/realms/${env.keycloak.realm}/protocol/openid-connect/certs`,
|
||||
cache: true,
|
||||
rateLimit: true,
|
||||
});
|
||||
|
||||
interface KeycloakTokenPayload extends JwtPayload {
|
||||
sub: string;
|
||||
email?: string;
|
||||
name?: string;
|
||||
preferred_username?: string;
|
||||
realm_access?: { roles?: string[] };
|
||||
}
|
||||
|
||||
function extractBearerToken(req: Request): string | null {
|
||||
const header = req.headers.authorization;
|
||||
if (!header) {
|
||||
return null;
|
||||
}
|
||||
const [scheme, token] = header.split(' ');
|
||||
if (scheme !== 'Bearer' || !token) {
|
||||
return null;
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
function buildAuthContext(payload: KeycloakTokenPayload): AuthContext {
|
||||
return {
|
||||
userId: payload.sub,
|
||||
email: payload.email ?? null,
|
||||
name: payload.name ?? payload.preferred_username ?? payload.email ?? payload.sub,
|
||||
roles: payload.realm_access?.roles ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
// Come authenticate.ts, ma se il token manca o non è valido lascia proseguire la richiesta
|
||||
// senza req.auth invece di rispondere 401: serve sulle route pubbliche che devono comunque
|
||||
// sapere chi è l'utente quando è loggato (es. per mostrargli le proprie tassonomie in attesa
|
||||
// di approvazione), senza per questo richiedere l'autenticazione a chi non è loggato.
|
||||
export async function optionalAuthenticate(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
const token = extractBearerToken(req);
|
||||
if (!token) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const decoded = jwt.decode(token, { complete: true });
|
||||
if (!decoded || !decoded.header.kid) {
|
||||
throw new Error("Header del token privo di 'kid'");
|
||||
}
|
||||
|
||||
const signingKey = await client.getSigningKey(decoded.header.kid);
|
||||
const payload = jwt.verify(token, signingKey.getPublicKey(), { algorithms: ['RS256'] });
|
||||
|
||||
if (typeof payload === 'string') {
|
||||
throw new Error('Payload del token non valido');
|
||||
}
|
||||
|
||||
req.auth = buildAuthContext(payload as KeycloakTokenPayload);
|
||||
} catch {
|
||||
// token presente ma non valido: procediamo comunque come utente anonimo
|
||||
}
|
||||
next();
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
|
||||
export function requireRole(...ruoliAmmessi: string[]) {
|
||||
return (req: Request, res: Response, next: NextFunction): void => {
|
||||
const roles = req.auth?.roles ?? [];
|
||||
const autorizzato = ruoliAmmessi.some((ruolo) => roles.includes(ruolo));
|
||||
|
||||
if (!autorizzato) {
|
||||
res.status(403).json({ message: 'Ruolo non autorizzato' });
|
||||
return;
|
||||
}
|
||||
next();
|
||||
};
|
||||
}
|
||||
@@ -1,8 +1,11 @@
|
||||
import { Request, Response, NextFunction, Router } from 'express';
|
||||
import { HttpError } from '../../errors';
|
||||
import { attivitaSaveSchema } from '../../types/validation';
|
||||
import { requireRole } from '../../middlewares/requireRole';
|
||||
import { attivitaSaveSchema, notaAttivitaSchema } from '../../types/validation';
|
||||
import * as attivitaService from './attivita.service';
|
||||
|
||||
const moderazione = requireRole('admin', 'moderatore');
|
||||
|
||||
function asyncHandler(
|
||||
handler: (req: Request, res: Response, next: NextFunction) => Promise<void>,
|
||||
) {
|
||||
@@ -11,12 +14,21 @@ function asyncHandler(
|
||||
};
|
||||
}
|
||||
|
||||
function parseIntParam(value: string, next: NextFunction, message: string): number | undefined {
|
||||
const parsed = Number(value);
|
||||
if (!Number.isInteger(parsed)) {
|
||||
next(new HttpError(400, message));
|
||||
return undefined;
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
export const attivitaPrivateRouter = Router();
|
||||
|
||||
attivitaPrivateRouter.get(
|
||||
'/get/lista/my',
|
||||
asyncHandler(async (req, res) => {
|
||||
const lista = await attivitaService.getListMy(req.auth!.userId);
|
||||
const lista = await attivitaService.getListMy(req.auth!.userId, req.auth);
|
||||
res.status(200).json(lista);
|
||||
}),
|
||||
);
|
||||
@@ -49,3 +61,54 @@ attivitaPrivateRouter.post(
|
||||
res.status(200).end();
|
||||
}),
|
||||
);
|
||||
|
||||
attivitaPrivateRouter.get(
|
||||
'/get/lista/moderazione',
|
||||
moderazione,
|
||||
asyncHandler(async (req, res) => {
|
||||
const lista = await attivitaService.getListaModerazione(req.auth!);
|
||||
res.status(200).json(lista);
|
||||
}),
|
||||
);
|
||||
|
||||
attivitaPrivateRouter.post(
|
||||
'/:idAttivita/approva',
|
||||
moderazione,
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const idAttivita = parseIntParam(req.params.idAttivita, next, "l'idAttivita deve essere un numero intero");
|
||||
if (idAttivita === undefined) return;
|
||||
|
||||
await attivitaService.approva(idAttivita, req.auth!);
|
||||
res.status(200).end();
|
||||
}),
|
||||
);
|
||||
|
||||
attivitaPrivateRouter.post(
|
||||
'/:idAttivita/commenta',
|
||||
moderazione,
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const idAttivita = parseIntParam(req.params.idAttivita, next, "l'idAttivita deve essere un numero intero");
|
||||
if (idAttivita === undefined) return;
|
||||
|
||||
const parsed = notaAttivitaSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
next(new HttpError(400, JSON.stringify(parsed.error.flatten())));
|
||||
return;
|
||||
}
|
||||
|
||||
await attivitaService.commenta(idAttivita, parsed.data.testo, req.auth!);
|
||||
res.status(200).end();
|
||||
}),
|
||||
);
|
||||
|
||||
attivitaPrivateRouter.delete(
|
||||
'/note/:idNota',
|
||||
moderazione,
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const idNota = parseIntParam(req.params.idNota, next, "l'idNota deve essere un numero intero");
|
||||
if (idNota === undefined) return;
|
||||
|
||||
await attivitaService.eliminaNota(idNota);
|
||||
res.status(204).end();
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -24,7 +24,7 @@ export const attivitaPublicRouter = Router();
|
||||
attivitaPublicRouter.get(
|
||||
'/get/lista/home',
|
||||
asyncHandler(async (req, res) => {
|
||||
const lista = await attivitaService.getListaHome();
|
||||
const lista = await attivitaService.getListaHome(req.auth);
|
||||
res.status(200).json(lista);
|
||||
}),
|
||||
);
|
||||
@@ -38,7 +38,7 @@ attivitaPublicRouter.post(
|
||||
return;
|
||||
}
|
||||
|
||||
const lista = await attivitaService.getListaSearch(parsed.data);
|
||||
const lista = await attivitaService.getListaSearch(parsed.data, req.auth);
|
||||
res.status(200).json(lista);
|
||||
}),
|
||||
);
|
||||
@@ -52,7 +52,7 @@ attivitaPublicRouter.get(
|
||||
return;
|
||||
}
|
||||
|
||||
const attivita = await attivitaService.getOne(id);
|
||||
const attivita = await attivitaService.getOne(id, req.auth);
|
||||
if (!attivita) {
|
||||
next(new HttpError(404, 'attività non trovata'));
|
||||
return;
|
||||
|
||||
@@ -2,11 +2,13 @@ import { Prisma, PrismaClient } from '@prisma/client';
|
||||
import { prisma } from '../../db/prisma';
|
||||
import { HttpError } from '../../errors';
|
||||
import { AuthContext } from '../../middlewares/auth.types';
|
||||
import * as notificheService from '../notifiche/notifiche.service';
|
||||
import {
|
||||
AttivitaDto,
|
||||
BrancaDto,
|
||||
CategoriaDto,
|
||||
MaterialeDto,
|
||||
NotaDto,
|
||||
ParagrafoDto,
|
||||
PeriodoAnnoDto,
|
||||
SearchObjectDto,
|
||||
@@ -14,6 +16,10 @@ import {
|
||||
} from '../../types/dto';
|
||||
import { AttivitaSaveInput } from '../../types/validation';
|
||||
|
||||
const STATO_IN_ATTESA = 'IA';
|
||||
const STATO_PUBBLICATO = 'PU';
|
||||
const STATO_BOZZA = 'BO';
|
||||
|
||||
const attivitaInclude = {
|
||||
stato: true,
|
||||
brancaLinks: { include: { branca: true } },
|
||||
@@ -21,6 +27,7 @@ const attivitaInclude = {
|
||||
materialeLinks: { include: { materiale: true } },
|
||||
periodoAnnoLinks: { include: { periodoAnno: true } },
|
||||
paragrafi: { include: { tipo: true } },
|
||||
noteList: { orderBy: { dataCreazione: 'desc' } },
|
||||
} satisfies Prisma.AttivitaInclude;
|
||||
|
||||
type AttivitaWithRelations = Prisma.AttivitaGetPayload<{ include: typeof attivitaInclude }>;
|
||||
@@ -29,15 +36,55 @@ function toTipologicaDto(entity: { id: string; nome: string }): TipologicaDto {
|
||||
return { id: entity.id, nome: entity.nome };
|
||||
}
|
||||
|
||||
function toAttivitaDto(entity: AttivitaWithRelations): AttivitaDto {
|
||||
// Usata sia per decidere se mostrare branca/categoria/periodo ancora in stato DA_APPROVARE,
|
||||
// sia per le note di moderazione e per la visibilità delle attività non ancora pubblicate:
|
||||
// in tutti i casi il perimetro di chi può "gestire" l'attività è lo stesso (autore, admin,
|
||||
// moderatore).
|
||||
function puoGestire(entity: AttivitaWithRelations, auth?: AuthContext): boolean {
|
||||
if (!auth) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
auth.userId === entity.autoreId ||
|
||||
auth.roles.includes('admin') ||
|
||||
auth.roles.includes('moderatore')
|
||||
);
|
||||
}
|
||||
|
||||
// Ogni volta che l'autore porta un'attività in stato Pubblicato (da save o da changeStato)
|
||||
// il valore persistito è in realtà "in attesa di approvazione": diventa Pubblicato per davvero
|
||||
// solo dopo l'approvazione di admin/moderatore (vedi approva()).
|
||||
function risolviStatoPersistito(idStato: string): string {
|
||||
return idStato === STATO_PUBBLICATO ? STATO_IN_ATTESA : idStato;
|
||||
}
|
||||
|
||||
function toAttivitaDto(entity: AttivitaWithRelations, auth?: AuthContext): AttivitaDto {
|
||||
const mostraProposte = puoGestire(entity, auth);
|
||||
|
||||
// Le note servono solo a far sistemare l'attività all'autore mentre è in revisione: una
|
||||
// volta tornata Pubblicato (approvazione definitiva) non vanno più mostrate, anche se non
|
||||
// sono ancora state cancellate esplicitamente (vedi eliminaNota).
|
||||
const mostraNote = mostraProposte && entity.statoId !== STATO_PUBBLICATO;
|
||||
|
||||
const noteList: NotaDto[] = mostraNote
|
||||
? entity.noteList.map((nota) => ({
|
||||
id: nota.id,
|
||||
attivitaId: nota.attivitaId,
|
||||
testo: nota.testo,
|
||||
autore: nota.autore,
|
||||
dataCreazione: nota.dataCreazione,
|
||||
}))
|
||||
: [];
|
||||
|
||||
const brancaList: BrancaDto[] = entity.brancaLinks
|
||||
.filter((link) => !link.cancellato)
|
||||
.filter((link) => !link.cancellato && (link.branca.stato === 'CONFERMATA' || mostraProposte))
|
||||
.map((link) => ({
|
||||
id: link.branca.id,
|
||||
nome: link.branca.nome,
|
||||
inizioEta: link.branca.inizioEta,
|
||||
fineEta: link.branca.fineEta,
|
||||
colore: link.branca.colore,
|
||||
stato: link.branca.stato,
|
||||
cancellato: false,
|
||||
dataCreazione: link.branca.dataCreazione,
|
||||
dataModifica: link.branca.dataModifica,
|
||||
@@ -45,12 +92,13 @@ function toAttivitaDto(entity: AttivitaWithRelations): AttivitaDto {
|
||||
}));
|
||||
|
||||
const categoriaList: CategoriaDto[] = entity.categoriaLinks
|
||||
.filter((link) => !link.cancellato)
|
||||
.filter((link) => !link.cancellato && (link.categoria.stato === 'CONFERMATA' || mostraProposte))
|
||||
.map((link) => ({
|
||||
id: link.categoria.id,
|
||||
nome: link.categoria.nome,
|
||||
padre: link.categoria.padreId,
|
||||
tipo: toTipologicaDto(link.categoria.tipo),
|
||||
stato: link.categoria.stato,
|
||||
cancellato: false,
|
||||
dataCreazione: link.categoria.dataCreazione,
|
||||
dataModifica: link.categoria.dataModifica,
|
||||
@@ -70,12 +118,15 @@ function toAttivitaDto(entity: AttivitaWithRelations): AttivitaDto {
|
||||
}));
|
||||
|
||||
const periodoAnnoList: PeriodoAnnoDto[] = entity.periodoAnnoLinks
|
||||
.filter((link) => !link.cancellato)
|
||||
.filter(
|
||||
(link) => !link.cancellato && (link.periodoAnno.stato === 'CONFERMATA' || mostraProposte),
|
||||
)
|
||||
.map((link) => ({
|
||||
id: link.periodoAnno.id,
|
||||
nome: link.periodoAnno.nome,
|
||||
inizioMese: link.periodoAnno.inizioMese,
|
||||
fineMese: link.periodoAnno.fineMese,
|
||||
stato: link.periodoAnno.stato,
|
||||
cancellato: false,
|
||||
dataCreazione: link.periodoAnno.dataCreazione,
|
||||
dataModifica: link.periodoAnno.dataModifica,
|
||||
@@ -107,42 +158,56 @@ function toAttivitaDto(entity: AttivitaWithRelations): AttivitaDto {
|
||||
materialeList,
|
||||
paragrafoList,
|
||||
periodoAnnoList,
|
||||
noteList,
|
||||
dataCreazione: entity.dataCreazione,
|
||||
dataModifica: entity.dataModifica,
|
||||
utenteModifica: entity.utenteModifica,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getListaHome(): Promise<AttivitaDto[]> {
|
||||
export async function getListaHome(auth?: AuthContext): Promise<AttivitaDto[]> {
|
||||
const entities = await prisma.attivita.findMany({
|
||||
where: { statoId: 'PU' },
|
||||
include: attivitaInclude,
|
||||
orderBy: { dataModifica: 'desc' },
|
||||
});
|
||||
|
||||
return entities.map(toAttivitaDto);
|
||||
return entities.map((entity) => toAttivitaDto(entity, auth));
|
||||
}
|
||||
|
||||
export async function getOne(id: number): Promise<AttivitaDto | null> {
|
||||
export async function getOne(id: number, auth?: AuthContext): Promise<AttivitaDto | null> {
|
||||
const entity = await prisma.attivita.findUnique({
|
||||
where: { id },
|
||||
include: attivitaInclude,
|
||||
});
|
||||
|
||||
return entity ? toAttivitaDto(entity) : null;
|
||||
if (!entity) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Un'attività non ancora pubblicata (bozza, privata, o in attesa di approvazione) è
|
||||
// visibile solo a chi può gestirla: chiunque altro la vede come inesistente.
|
||||
if (entity.statoId !== STATO_PUBBLICATO && !puoGestire(entity, auth)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return toAttivitaDto(entity, auth);
|
||||
}
|
||||
|
||||
export async function getListMy(autoreId: string): Promise<AttivitaDto[]> {
|
||||
export async function getListMy(autoreId: string, auth?: AuthContext): Promise<AttivitaDto[]> {
|
||||
const entities = await prisma.attivita.findMany({
|
||||
where: { autoreId },
|
||||
include: attivitaInclude,
|
||||
orderBy: { dataModifica: 'desc' },
|
||||
});
|
||||
|
||||
return entities.map(toAttivitaDto);
|
||||
return entities.map((entity) => toAttivitaDto(entity, auth));
|
||||
}
|
||||
|
||||
export async function getListaSearch(dtoList: SearchObjectDto[]): Promise<AttivitaDto[]> {
|
||||
export async function getListaSearch(
|
||||
dtoList: SearchObjectDto[],
|
||||
auth?: AuthContext,
|
||||
): Promise<AttivitaDto[]> {
|
||||
const brancaIds = dtoList.filter((d) => d.gruppo === 'branca').map((d) => d.id as number);
|
||||
const categoriaIds = dtoList.filter((d) => d.gruppo === 'categoria').map((d) => d.id as number);
|
||||
const materialeIds = dtoList.filter((d) => d.gruppo === 'materiale').map((d) => d.id as number);
|
||||
@@ -154,7 +219,7 @@ export async function getListaSearch(dtoList: SearchObjectDto[]): Promise<Attivi
|
||||
.map((d) => d.nome)
|
||||
.filter((nome): nome is string => !!nome);
|
||||
|
||||
const and: Prisma.AttivitaWhereInput[] = [];
|
||||
const and: Prisma.AttivitaWhereInput[] = [{ statoId: STATO_PUBBLICATO }];
|
||||
|
||||
if (brancaIds.length > 0) {
|
||||
and.push({
|
||||
@@ -195,7 +260,7 @@ export async function getListaSearch(dtoList: SearchObjectDto[]): Promise<Attivi
|
||||
orderBy: { dataModifica: 'desc' },
|
||||
});
|
||||
|
||||
return entities.map(toAttivitaDto);
|
||||
return entities.map((entity) => toAttivitaDto(entity, auth));
|
||||
}
|
||||
|
||||
export async function changeStato(
|
||||
@@ -211,18 +276,115 @@ export async function changeStato(
|
||||
throw new HttpError(403, 'non sei autore di questa attività');
|
||||
}
|
||||
|
||||
const nuovoStatoId = risolviStatoPersistito(idStato);
|
||||
|
||||
const updated = await prisma.attivita.update({
|
||||
where: { id: idAttivita },
|
||||
data: {
|
||||
statoId: idStato,
|
||||
statoId: nuovoStatoId,
|
||||
utenteModifica: auth.name,
|
||||
},
|
||||
include: { stato: true },
|
||||
});
|
||||
|
||||
if (nuovoStatoId === STATO_IN_ATTESA && existing.statoId !== STATO_IN_ATTESA) {
|
||||
await notificheService.creaBroadcastModerazione(
|
||||
'ATTIVITA_IN_ATTESA',
|
||||
`Attività da approvare: "${updated.nome}"`,
|
||||
`/attivita/dettaglio/${updated.id}`,
|
||||
);
|
||||
}
|
||||
|
||||
return toTipologicaDto(updated.stato);
|
||||
}
|
||||
|
||||
export async function getListaModerazione(auth: AuthContext): Promise<AttivitaDto[]> {
|
||||
const entities = await prisma.attivita.findMany({
|
||||
where: { statoId: STATO_IN_ATTESA },
|
||||
include: attivitaInclude,
|
||||
orderBy: { dataModifica: 'asc' },
|
||||
});
|
||||
|
||||
return entities.map((entity) => toAttivitaDto(entity, auth));
|
||||
}
|
||||
|
||||
async function trovaAttivitaInAttesa(idAttivita: number): Promise<AttivitaWithRelations> {
|
||||
const entity = await prisma.attivita.findUnique({
|
||||
where: { id: idAttivita },
|
||||
include: attivitaInclude,
|
||||
});
|
||||
if (!entity) {
|
||||
throw new HttpError(404, 'attività non trovata');
|
||||
}
|
||||
if (entity.statoId !== STATO_IN_ATTESA) {
|
||||
throw new HttpError(409, 'attività non in attesa di approvazione');
|
||||
}
|
||||
return entity;
|
||||
}
|
||||
|
||||
export async function approva(idAttivita: number, auth: AuthContext): Promise<void> {
|
||||
const entity = await trovaAttivitaInAttesa(idAttivita);
|
||||
|
||||
await prisma.attivita.update({
|
||||
where: { id: idAttivita },
|
||||
data: {
|
||||
statoId: STATO_PUBBLICATO,
|
||||
utenteModifica: auth.name,
|
||||
},
|
||||
});
|
||||
|
||||
await notificheService.creaPersonale(
|
||||
'ATTIVITA_PUBBLICATA',
|
||||
`La tua attività "${entity.nome}" è stata pubblicata`,
|
||||
entity.autoreId,
|
||||
`/attivita/dettaglio/${entity.id}`,
|
||||
);
|
||||
}
|
||||
|
||||
export async function commenta(idAttivita: number, testo: string, auth: AuthContext): Promise<void> {
|
||||
const entity = await trovaAttivitaInAttesa(idAttivita);
|
||||
|
||||
await prisma.$transaction([
|
||||
prisma.notaAttivita.create({
|
||||
data: {
|
||||
attivitaId: idAttivita,
|
||||
testo,
|
||||
autore: auth.name,
|
||||
autoreId: auth.userId,
|
||||
},
|
||||
}),
|
||||
prisma.attivita.update({
|
||||
where: { id: idAttivita },
|
||||
data: {
|
||||
statoId: STATO_BOZZA,
|
||||
utenteModifica: auth.name,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
await notificheService.creaPersonale(
|
||||
'ATTIVITA_BOZZA_NOTA',
|
||||
`La tua attività "${entity.nome}" è tornata in bozza con una nota di moderazione`,
|
||||
entity.autoreId,
|
||||
`/modifica-attivita/${entity.id}`,
|
||||
);
|
||||
}
|
||||
|
||||
export async function eliminaNota(idNota: number): Promise<void> {
|
||||
const nota = await prisma.notaAttivita.findUnique({
|
||||
where: { id: idNota },
|
||||
include: { attivita: true },
|
||||
});
|
||||
if (!nota) {
|
||||
throw new HttpError(404, 'nota non trovata');
|
||||
}
|
||||
if (nota.attivita.statoId !== STATO_PUBBLICATO) {
|
||||
throw new HttpError(409, 'le note si possono cancellare solo su attività pubblicate');
|
||||
}
|
||||
|
||||
await prisma.notaAttivita.delete({ where: { id: idNota } });
|
||||
}
|
||||
|
||||
type Tx = Omit<PrismaClient, '$connect' | '$disconnect' | '$on' | '$transaction' | '$use' | '$extends'>;
|
||||
|
||||
async function upsertBranca(
|
||||
@@ -439,8 +601,12 @@ async function upsertPeriodoAnno(
|
||||
}
|
||||
|
||||
export async function save(dto: AttivitaSaveInput, auth: AuthContext): Promise<void> {
|
||||
let entraInAttesa = false;
|
||||
let savedAttivitaId!: number;
|
||||
|
||||
await prisma.$transaction(async (tx) => {
|
||||
let attivitaId: number;
|
||||
const nuovoStatoId = risolviStatoPersistito(dto.stato.id);
|
||||
|
||||
if (dto.id) {
|
||||
const existing = await tx.attivita.findUnique({ where: { id: dto.id } });
|
||||
@@ -455,23 +621,26 @@ export async function save(dto: AttivitaSaveInput, auth: AuthContext): Promise<v
|
||||
where: { id: dto.id },
|
||||
data: {
|
||||
nome: dto.nome,
|
||||
statoId: dto.stato.id,
|
||||
statoId: nuovoStatoId,
|
||||
utenteModifica: auth.name,
|
||||
},
|
||||
});
|
||||
attivitaId = dto.id;
|
||||
entraInAttesa = nuovoStatoId === STATO_IN_ATTESA && existing.statoId !== STATO_IN_ATTESA;
|
||||
} else {
|
||||
const created = await tx.attivita.create({
|
||||
data: {
|
||||
nome: dto.nome,
|
||||
autore: auth.name,
|
||||
autoreId: auth.userId,
|
||||
statoId: dto.stato.id,
|
||||
statoId: nuovoStatoId,
|
||||
utenteModifica: auth.name,
|
||||
},
|
||||
});
|
||||
attivitaId = created.id;
|
||||
entraInAttesa = nuovoStatoId === STATO_IN_ATTESA;
|
||||
}
|
||||
savedAttivitaId = attivitaId;
|
||||
|
||||
const brancaIds = new Set<number>();
|
||||
for (const branca of dto.brancaList) {
|
||||
@@ -557,4 +726,12 @@ export async function save(dto: AttivitaSaveInput, auth: AuthContext): Promise<v
|
||||
where: { attivitaId, id: { notIn: [...paragrafoIds] } },
|
||||
});
|
||||
});
|
||||
|
||||
if (entraInAttesa) {
|
||||
await notificheService.creaBroadcastModerazione(
|
||||
'ATTIVITA_IN_ATTESA',
|
||||
`Attività da approvare: "${dto.nome}"`,
|
||||
`/attivita/dettaglio/${savedAttivitaId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { SearchGroupDto, SearchObjectDto } from '../../types/dto';
|
||||
|
||||
export async function getBranca(keyword?: string | null): Promise<SearchObjectDto[]> {
|
||||
const entities = await prisma.branca.findMany({
|
||||
where: { nome: { contains: keyword ?? '', mode: 'insensitive' } },
|
||||
where: { nome: { contains: keyword ?? '', mode: 'insensitive' }, stato: 'CONFERMATA' },
|
||||
});
|
||||
|
||||
return entities.map((entity) => ({ id: entity.id, nome: entity.nome, gruppo: 'branca' }));
|
||||
@@ -11,7 +11,7 @@ export async function getBranca(keyword?: string | null): Promise<SearchObjectDt
|
||||
|
||||
export async function getCategoria(keyword?: string | null): Promise<SearchObjectDto[]> {
|
||||
const entities = await prisma.categoria.findMany({
|
||||
where: { nome: { contains: keyword ?? '', mode: 'insensitive' } },
|
||||
where: { nome: { contains: keyword ?? '', mode: 'insensitive' }, stato: 'CONFERMATA' },
|
||||
});
|
||||
|
||||
return entities.map((entity) => ({ id: entity.id, nome: entity.nome, gruppo: 'categoria' }));
|
||||
@@ -27,7 +27,7 @@ export async function getMateriale(keyword?: string | null): Promise<SearchObjec
|
||||
|
||||
export async function getPeriodoAnno(keyword?: string | null): Promise<SearchObjectDto[]> {
|
||||
const entities = await prisma.periodoAnno.findMany({
|
||||
where: { nome: { contains: keyword ?? '', mode: 'insensitive' } },
|
||||
where: { nome: { contains: keyword ?? '', mode: 'insensitive' }, stato: 'CONFERMATA' },
|
||||
});
|
||||
|
||||
return entities.map((entity) => ({ id: entity.id, nome: entity.nome, gruppo: 'periodoAnno' }));
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { Request, Response, NextFunction, Router } from 'express';
|
||||
import { HttpError } from '../../errors';
|
||||
import * as notificheService from './notifiche.service';
|
||||
|
||||
function asyncHandler(
|
||||
handler: (req: Request, res: Response, next: NextFunction) => Promise<void>,
|
||||
) {
|
||||
return (req: Request, res: Response, next: NextFunction): void => {
|
||||
handler(req, res, next).catch(next);
|
||||
};
|
||||
}
|
||||
|
||||
function parseId(req: Request, next: NextFunction): number | undefined {
|
||||
const id = Number(req.params.id);
|
||||
if (!Number.isInteger(id)) {
|
||||
next(new HttpError(400, "l'id deve essere un numero intero"));
|
||||
return undefined;
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
export const notificheRouter = Router();
|
||||
|
||||
notificheRouter.get(
|
||||
'/',
|
||||
asyncHandler(async (req, res) => {
|
||||
res.status(200).json(await notificheService.listNotifiche(req.auth!));
|
||||
}),
|
||||
);
|
||||
|
||||
notificheRouter.get(
|
||||
'/non-lette/count',
|
||||
asyncHandler(async (req, res) => {
|
||||
res.status(200).json({ count: await notificheService.countNonLette(req.auth!) });
|
||||
}),
|
||||
);
|
||||
|
||||
notificheRouter.put(
|
||||
'/letta-tutte',
|
||||
asyncHandler(async (req, res) => {
|
||||
await notificheService.segnaTutteLette(req.auth!);
|
||||
res.status(204).end();
|
||||
}),
|
||||
);
|
||||
|
||||
notificheRouter.put(
|
||||
'/:id/letta',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const id = parseId(req, next);
|
||||
if (id === undefined) return;
|
||||
|
||||
await notificheService.segnaLetta(id, req.auth!);
|
||||
res.status(204).end();
|
||||
}),
|
||||
);
|
||||
|
||||
export default notificheRouter;
|
||||
@@ -0,0 +1,95 @@
|
||||
import { TipoNotifica } from '@prisma/client';
|
||||
import { prisma } from '../../db/prisma';
|
||||
import { HttpError } from '../../errors';
|
||||
import { AuthContext } from '../../middlewares/auth.types';
|
||||
import { NotificaDto } from '../../types/dto';
|
||||
|
||||
const RUOLI_MODERAZIONE = ['admin', 'moderatore'];
|
||||
|
||||
function isModeratore(auth: AuthContext): boolean {
|
||||
return RUOLI_MODERAZIONE.some((ruolo) => auth.roles.includes(ruolo));
|
||||
}
|
||||
|
||||
function toDto(entity: {
|
||||
id: number;
|
||||
tipo: TipoNotifica;
|
||||
messaggio: string;
|
||||
link: string | null;
|
||||
letta: boolean;
|
||||
dataCreazione: Date;
|
||||
}): NotificaDto {
|
||||
return {
|
||||
id: entity.id,
|
||||
tipo: entity.tipo,
|
||||
messaggio: entity.messaggio,
|
||||
link: entity.link,
|
||||
letta: entity.letta,
|
||||
dataCreazione: entity.dataCreazione,
|
||||
};
|
||||
}
|
||||
|
||||
// Le notifiche "broadcast" (destinatarioId null) sono la coda di moderazione condivisa tra
|
||||
// admin/moderatore: sono visibili solo a chi ha uno di questi ruoli, oltre alle proprie
|
||||
// notifiche personali.
|
||||
function whereVisibili(auth: AuthContext) {
|
||||
return isModeratore(auth)
|
||||
? { OR: [{ destinatarioId: auth.userId }, { destinatarioId: null }] }
|
||||
: { destinatarioId: auth.userId };
|
||||
}
|
||||
|
||||
export async function listNotifiche(auth: AuthContext): Promise<NotificaDto[]> {
|
||||
const entities = await prisma.notifica.findMany({
|
||||
where: whereVisibili(auth),
|
||||
orderBy: { dataCreazione: 'desc' },
|
||||
take: 50,
|
||||
});
|
||||
return entities.map(toDto);
|
||||
}
|
||||
|
||||
export async function countNonLette(auth: AuthContext): Promise<number> {
|
||||
return prisma.notifica.count({ where: { ...whereVisibili(auth), letta: false } });
|
||||
}
|
||||
|
||||
export async function segnaLetta(id: number, auth: AuthContext): Promise<void> {
|
||||
const entity = await prisma.notifica.findUnique({ where: { id } });
|
||||
if (!entity) {
|
||||
throw new HttpError(404, 'notifica non trovata');
|
||||
}
|
||||
const visibile =
|
||||
entity.destinatarioId === auth.userId || (entity.destinatarioId === null && isModeratore(auth));
|
||||
if (!visibile) {
|
||||
throw new HttpError(403, 'non puoi accedere a questa notifica');
|
||||
}
|
||||
|
||||
await prisma.notifica.update({ where: { id }, data: { letta: true } });
|
||||
}
|
||||
|
||||
export async function segnaTutteLette(auth: AuthContext): Promise<void> {
|
||||
await prisma.notifica.updateMany({
|
||||
where: { ...whereVisibili(auth), letta: false },
|
||||
data: { letta: true },
|
||||
});
|
||||
}
|
||||
|
||||
// Notifica destinata a chiunque abbia ruolo admin/moderatore (coda di moderazione condivisa,
|
||||
// vedi whereVisibili): usata per segnalare nuove proposte/attività in attesa di revisione.
|
||||
export async function creaBroadcastModerazione(
|
||||
tipo: TipoNotifica,
|
||||
messaggio: string,
|
||||
link?: string,
|
||||
): Promise<void> {
|
||||
await prisma.notifica.create({
|
||||
data: { tipo, messaggio, link: link ?? null, destinatarioId: null },
|
||||
});
|
||||
}
|
||||
|
||||
export async function creaPersonale(
|
||||
tipo: TipoNotifica,
|
||||
messaggio: string,
|
||||
destinatarioId: string,
|
||||
link?: string,
|
||||
): Promise<void> {
|
||||
await prisma.notifica.create({
|
||||
data: { tipo, messaggio, link: link ?? null, destinatarioId },
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
import { Request, Response, NextFunction, Router } from 'express';
|
||||
import { HttpError } from '../../errors';
|
||||
import { requireRole } from '../../middlewares/requireRole';
|
||||
import {
|
||||
brancaAdminSchema,
|
||||
categoriaAdminSchema,
|
||||
periodoAnnoAdminSchema,
|
||||
proponiTassonomiaSchema,
|
||||
} from '../../types/validation';
|
||||
import * as tassonomieService from './tassonomie.service';
|
||||
|
||||
const moderazione = requireRole('admin', 'moderatore');
|
||||
|
||||
function asyncHandler(
|
||||
handler: (req: Request, res: Response, next: NextFunction) => Promise<void>,
|
||||
) {
|
||||
return (req: Request, res: Response, next: NextFunction): void => {
|
||||
handler(req, res, next).catch(next);
|
||||
};
|
||||
}
|
||||
|
||||
function parseId(req: Request, next: NextFunction): number | undefined {
|
||||
const id = Number(req.params.id);
|
||||
if (!Number.isInteger(id)) {
|
||||
next(new HttpError(400, "l'id deve essere un numero intero"));
|
||||
return undefined;
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
export const tassonomieRouter = Router();
|
||||
|
||||
tassonomieRouter.get(
|
||||
'/branca',
|
||||
moderazione,
|
||||
asyncHandler(async (req, res) => {
|
||||
res.status(200).json(await tassonomieService.listBranca());
|
||||
}),
|
||||
);
|
||||
|
||||
tassonomieRouter.post(
|
||||
'/branca',
|
||||
moderazione,
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const parsed = brancaAdminSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
next(new HttpError(400, JSON.stringify(parsed.error.flatten())));
|
||||
return;
|
||||
}
|
||||
const created = await tassonomieService.createBranca(parsed.data, req.auth!);
|
||||
res.status(201).json(created);
|
||||
}),
|
||||
);
|
||||
|
||||
tassonomieRouter.put(
|
||||
'/branca/:id',
|
||||
moderazione,
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const id = parseId(req, next);
|
||||
if (id === undefined) return;
|
||||
|
||||
const parsed = brancaAdminSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
next(new HttpError(400, JSON.stringify(parsed.error.flatten())));
|
||||
return;
|
||||
}
|
||||
const updated = await tassonomieService.updateBranca(id, parsed.data, req.auth!);
|
||||
res.status(200).json(updated);
|
||||
}),
|
||||
);
|
||||
|
||||
tassonomieRouter.delete(
|
||||
'/branca/:id',
|
||||
moderazione,
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const id = parseId(req, next);
|
||||
if (id === undefined) return;
|
||||
|
||||
await tassonomieService.deleteBranca(id);
|
||||
res.status(204).end();
|
||||
}),
|
||||
);
|
||||
|
||||
tassonomieRouter.post(
|
||||
'/branca/:id/approva',
|
||||
moderazione,
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const id = parseId(req, next);
|
||||
if (id === undefined) return;
|
||||
|
||||
const approvata = await tassonomieService.approvaBranca(id);
|
||||
res.status(200).json(approvata);
|
||||
}),
|
||||
);
|
||||
|
||||
tassonomieRouter.post(
|
||||
'/branca/:id/rifiuta',
|
||||
moderazione,
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const id = parseId(req, next);
|
||||
if (id === undefined) return;
|
||||
|
||||
await tassonomieService.rifiutaBranca(id);
|
||||
res.status(204).end();
|
||||
}),
|
||||
);
|
||||
|
||||
tassonomieRouter.get(
|
||||
'/categoria',
|
||||
moderazione,
|
||||
asyncHandler(async (req, res) => {
|
||||
res.status(200).json(await tassonomieService.listCategoria());
|
||||
}),
|
||||
);
|
||||
|
||||
tassonomieRouter.get(
|
||||
'/tipo-categoria',
|
||||
moderazione,
|
||||
asyncHandler(async (req, res) => {
|
||||
res.status(200).json(await tassonomieService.listTipoCategoria());
|
||||
}),
|
||||
);
|
||||
|
||||
tassonomieRouter.post(
|
||||
'/categoria',
|
||||
moderazione,
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const parsed = categoriaAdminSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
next(new HttpError(400, JSON.stringify(parsed.error.flatten())));
|
||||
return;
|
||||
}
|
||||
const created = await tassonomieService.createCategoria(parsed.data, req.auth!);
|
||||
res.status(201).json(created);
|
||||
}),
|
||||
);
|
||||
|
||||
tassonomieRouter.put(
|
||||
'/categoria/:id',
|
||||
moderazione,
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const id = parseId(req, next);
|
||||
if (id === undefined) return;
|
||||
|
||||
const parsed = categoriaAdminSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
next(new HttpError(400, JSON.stringify(parsed.error.flatten())));
|
||||
return;
|
||||
}
|
||||
const updated = await tassonomieService.updateCategoria(id, parsed.data, req.auth!);
|
||||
res.status(200).json(updated);
|
||||
}),
|
||||
);
|
||||
|
||||
tassonomieRouter.delete(
|
||||
'/categoria/:id',
|
||||
moderazione,
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const id = parseId(req, next);
|
||||
if (id === undefined) return;
|
||||
|
||||
await tassonomieService.deleteCategoria(id);
|
||||
res.status(204).end();
|
||||
}),
|
||||
);
|
||||
|
||||
tassonomieRouter.post(
|
||||
'/categoria/:id/approva',
|
||||
moderazione,
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const id = parseId(req, next);
|
||||
if (id === undefined) return;
|
||||
|
||||
const approvata = await tassonomieService.approvaCategoria(id);
|
||||
res.status(200).json(approvata);
|
||||
}),
|
||||
);
|
||||
|
||||
tassonomieRouter.post(
|
||||
'/categoria/:id/rifiuta',
|
||||
moderazione,
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const id = parseId(req, next);
|
||||
if (id === undefined) return;
|
||||
|
||||
await tassonomieService.rifiutaCategoria(id);
|
||||
res.status(204).end();
|
||||
}),
|
||||
);
|
||||
|
||||
tassonomieRouter.get(
|
||||
'/periodoAnno',
|
||||
moderazione,
|
||||
asyncHandler(async (req, res) => {
|
||||
res.status(200).json(await tassonomieService.listPeriodoAnno());
|
||||
}),
|
||||
);
|
||||
|
||||
tassonomieRouter.post(
|
||||
'/periodoAnno',
|
||||
moderazione,
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const parsed = periodoAnnoAdminSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
next(new HttpError(400, JSON.stringify(parsed.error.flatten())));
|
||||
return;
|
||||
}
|
||||
const created = await tassonomieService.createPeriodoAnno(parsed.data, req.auth!);
|
||||
res.status(201).json(created);
|
||||
}),
|
||||
);
|
||||
|
||||
tassonomieRouter.put(
|
||||
'/periodoAnno/:id',
|
||||
moderazione,
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const id = parseId(req, next);
|
||||
if (id === undefined) return;
|
||||
|
||||
const parsed = periodoAnnoAdminSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
next(new HttpError(400, JSON.stringify(parsed.error.flatten())));
|
||||
return;
|
||||
}
|
||||
const updated = await tassonomieService.updatePeriodoAnno(id, parsed.data, req.auth!);
|
||||
res.status(200).json(updated);
|
||||
}),
|
||||
);
|
||||
|
||||
tassonomieRouter.delete(
|
||||
'/periodoAnno/:id',
|
||||
moderazione,
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const id = parseId(req, next);
|
||||
if (id === undefined) return;
|
||||
|
||||
await tassonomieService.deletePeriodoAnno(id);
|
||||
res.status(204).end();
|
||||
}),
|
||||
);
|
||||
|
||||
tassonomieRouter.post(
|
||||
'/periodoAnno/:id/approva',
|
||||
moderazione,
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const id = parseId(req, next);
|
||||
if (id === undefined) return;
|
||||
|
||||
const approvato = await tassonomieService.approvaPeriodoAnno(id);
|
||||
res.status(200).json(approvato);
|
||||
}),
|
||||
);
|
||||
|
||||
tassonomieRouter.post(
|
||||
'/periodoAnno/:id/rifiuta',
|
||||
moderazione,
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const id = parseId(req, next);
|
||||
if (id === undefined) return;
|
||||
|
||||
await tassonomieService.rifiutaPeriodoAnno(id);
|
||||
res.status(204).end();
|
||||
}),
|
||||
);
|
||||
|
||||
// Route di proposta: qualunque utente autenticato può proporre una nuova tassonomia,
|
||||
// che viene creata con stato DA_APPROVARE (vedi tassonomie.service.ts).
|
||||
tassonomieRouter.post(
|
||||
'/proposte/branca',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const parsed = proponiTassonomiaSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
next(new HttpError(400, JSON.stringify(parsed.error.flatten())));
|
||||
return;
|
||||
}
|
||||
const created = await tassonomieService.proponiBranca(parsed.data.nome, req.auth!);
|
||||
res.status(201).json(created);
|
||||
}),
|
||||
);
|
||||
|
||||
tassonomieRouter.post(
|
||||
'/proposte/categoria',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const parsed = proponiTassonomiaSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
next(new HttpError(400, JSON.stringify(parsed.error.flatten())));
|
||||
return;
|
||||
}
|
||||
const created = await tassonomieService.proponiCategoria(parsed.data.nome, req.auth!);
|
||||
res.status(201).json(created);
|
||||
}),
|
||||
);
|
||||
|
||||
tassonomieRouter.post(
|
||||
'/proposte/periodoAnno',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const parsed = proponiTassonomiaSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
next(new HttpError(400, JSON.stringify(parsed.error.flatten())));
|
||||
return;
|
||||
}
|
||||
const created = await tassonomieService.proponiPeriodoAnno(parsed.data.nome, req.auth!);
|
||||
res.status(201).json(created);
|
||||
}),
|
||||
);
|
||||
@@ -0,0 +1,301 @@
|
||||
import { Branca, Categoria, PeriodoAnno, TipoCategoria } from '@prisma/client';
|
||||
import { prisma } from '../../db/prisma';
|
||||
import { HttpError } from '../../errors';
|
||||
import { AuthContext } from '../../middlewares/auth.types';
|
||||
import * as notificheService from '../notifiche/notifiche.service';
|
||||
import { BrancaAdminInput, CategoriaAdminInput, PeriodoAnnoAdminInput } from '../../types/validation';
|
||||
|
||||
export async function listBranca(): Promise<Branca[]> {
|
||||
return prisma.branca.findMany({ orderBy: { nome: 'asc' } });
|
||||
}
|
||||
|
||||
export async function createBranca(input: BrancaAdminInput, auth: AuthContext): Promise<Branca> {
|
||||
return prisma.branca.create({
|
||||
data: {
|
||||
nome: input.nome,
|
||||
inizioEta: input.inizioEta ?? null,
|
||||
fineEta: input.fineEta ?? null,
|
||||
colore: input.colore ?? null,
|
||||
utenteModifica: auth.name,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateBranca(
|
||||
id: number,
|
||||
input: BrancaAdminInput,
|
||||
auth: AuthContext,
|
||||
): Promise<Branca> {
|
||||
await assertExists(prisma.branca.findUnique({ where: { id } }), 'Branca non trovata');
|
||||
|
||||
return prisma.branca.update({
|
||||
where: { id },
|
||||
data: {
|
||||
nome: input.nome,
|
||||
inizioEta: input.inizioEta ?? null,
|
||||
fineEta: input.fineEta ?? null,
|
||||
colore: input.colore ?? null,
|
||||
utenteModifica: auth.name,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteBranca(id: number): Promise<void> {
|
||||
await assertExists(prisma.branca.findUnique({ where: { id } }), 'Branca non trovata');
|
||||
|
||||
const linkCount = await prisma.brancaAttivita.count({ where: { brancaId: id } });
|
||||
if (linkCount > 0) {
|
||||
throw new HttpError(409, `In uso da ${linkCount} attività, impossibile eliminare`);
|
||||
}
|
||||
|
||||
await prisma.branca.delete({ where: { id } });
|
||||
}
|
||||
|
||||
export async function proponiBranca(nome: string, auth: AuthContext): Promise<Branca> {
|
||||
const created = await prisma.branca.create({
|
||||
data: {
|
||||
nome,
|
||||
stato: 'DA_APPROVARE',
|
||||
creatoDaId: auth.userId,
|
||||
utenteModifica: auth.name,
|
||||
},
|
||||
});
|
||||
|
||||
await notificheService.creaBroadcastModerazione(
|
||||
'TASSONOMIA_PROPOSTA',
|
||||
`Nuova branca proposta da approvare: "${created.nome}"`,
|
||||
'/tassonomie',
|
||||
);
|
||||
|
||||
return created;
|
||||
}
|
||||
|
||||
export async function approvaBranca(id: number): Promise<Branca> {
|
||||
const entity = await assertExists(prisma.branca.findUnique({ where: { id } }), 'Branca non trovata');
|
||||
if (entity.stato !== 'DA_APPROVARE') {
|
||||
throw new HttpError(409, 'Branca già confermata');
|
||||
}
|
||||
|
||||
return prisma.branca.update({ where: { id }, data: { stato: 'CONFERMATA' } });
|
||||
}
|
||||
|
||||
export async function rifiutaBranca(id: number): Promise<void> {
|
||||
const entity = await assertExists(prisma.branca.findUnique({ where: { id } }), 'Branca non trovata');
|
||||
if (entity.stato !== 'DA_APPROVARE') {
|
||||
throw new HttpError(409, 'Solo le proposte in attesa possono essere rifiutate');
|
||||
}
|
||||
|
||||
await prisma.$transaction([
|
||||
prisma.brancaAttivita.deleteMany({ where: { brancaId: id } }),
|
||||
prisma.branca.delete({ where: { id } }),
|
||||
]);
|
||||
}
|
||||
|
||||
export async function listCategoria(): Promise<Categoria[]> {
|
||||
return prisma.categoria.findMany({ orderBy: { nome: 'asc' } });
|
||||
}
|
||||
|
||||
export async function listTipoCategoria(): Promise<TipoCategoria[]> {
|
||||
return prisma.tipoCategoria.findMany({ orderBy: { nome: 'asc' } });
|
||||
}
|
||||
|
||||
export async function createCategoria(
|
||||
input: CategoriaAdminInput,
|
||||
auth: AuthContext,
|
||||
): Promise<Categoria> {
|
||||
return prisma.categoria.create({
|
||||
data: {
|
||||
nome: input.nome,
|
||||
padreId: input.padreId ?? null,
|
||||
tipoId: input.tipoId,
|
||||
utenteModifica: auth.name,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateCategoria(
|
||||
id: number,
|
||||
input: CategoriaAdminInput,
|
||||
auth: AuthContext,
|
||||
): Promise<Categoria> {
|
||||
await assertExists(prisma.categoria.findUnique({ where: { id } }), 'Categoria non trovata');
|
||||
|
||||
if (input.padreId === id) {
|
||||
throw new HttpError(400, 'Una categoria non può essere padre di se stessa');
|
||||
}
|
||||
|
||||
return prisma.categoria.update({
|
||||
where: { id },
|
||||
data: {
|
||||
nome: input.nome,
|
||||
padreId: input.padreId ?? null,
|
||||
tipoId: input.tipoId,
|
||||
utenteModifica: auth.name,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteCategoria(id: number): Promise<void> {
|
||||
await assertExists(prisma.categoria.findUnique({ where: { id } }), 'Categoria non trovata');
|
||||
|
||||
const [linkCount, figliCount, materialeLinkCount] = await Promise.all([
|
||||
prisma.categoriaAttivita.count({ where: { categoriaId: id } }),
|
||||
prisma.categoria.count({ where: { padreId: id } }),
|
||||
prisma.categoriaMateriale.count({ where: { categoriaId: id } }),
|
||||
]);
|
||||
|
||||
const usi = linkCount + figliCount + materialeLinkCount;
|
||||
if (usi > 0) {
|
||||
throw new HttpError(409, `In uso da ${usi} elementi collegati, impossibile eliminare`);
|
||||
}
|
||||
|
||||
await prisma.categoria.delete({ where: { id } });
|
||||
}
|
||||
|
||||
export async function proponiCategoria(nome: string, auth: AuthContext): Promise<Categoria> {
|
||||
const created = await prisma.categoria.create({
|
||||
data: {
|
||||
nome,
|
||||
tipoId: 'A',
|
||||
stato: 'DA_APPROVARE',
|
||||
creatoDaId: auth.userId,
|
||||
utenteModifica: auth.name,
|
||||
},
|
||||
});
|
||||
|
||||
await notificheService.creaBroadcastModerazione(
|
||||
'TASSONOMIA_PROPOSTA',
|
||||
`Nuova categoria proposta da approvare: "${created.nome}"`,
|
||||
'/tassonomie',
|
||||
);
|
||||
|
||||
return created;
|
||||
}
|
||||
|
||||
export async function approvaCategoria(id: number): Promise<Categoria> {
|
||||
const entity = await assertExists(prisma.categoria.findUnique({ where: { id } }), 'Categoria non trovata');
|
||||
if (entity.stato !== 'DA_APPROVARE') {
|
||||
throw new HttpError(409, 'Categoria già confermata');
|
||||
}
|
||||
|
||||
return prisma.categoria.update({ where: { id }, data: { stato: 'CONFERMATA' } });
|
||||
}
|
||||
|
||||
export async function rifiutaCategoria(id: number): Promise<void> {
|
||||
const entity = await assertExists(prisma.categoria.findUnique({ where: { id } }), 'Categoria non trovata');
|
||||
if (entity.stato !== 'DA_APPROVARE') {
|
||||
throw new HttpError(409, 'Solo le proposte in attesa possono essere rifiutate');
|
||||
}
|
||||
|
||||
const figliCount = await prisma.categoria.count({ where: { padreId: id } });
|
||||
if (figliCount > 0) {
|
||||
throw new HttpError(409, 'Ha sotto-categorie collegate, impossibile rifiutare');
|
||||
}
|
||||
|
||||
await prisma.$transaction([
|
||||
prisma.categoriaAttivita.deleteMany({ where: { categoriaId: id } }),
|
||||
prisma.categoriaMateriale.deleteMany({ where: { categoriaId: id } }),
|
||||
prisma.categoria.delete({ where: { id } }),
|
||||
]);
|
||||
}
|
||||
|
||||
export async function listPeriodoAnno(): Promise<PeriodoAnno[]> {
|
||||
return prisma.periodoAnno.findMany({ orderBy: { nome: 'asc' } });
|
||||
}
|
||||
|
||||
export async function createPeriodoAnno(
|
||||
input: PeriodoAnnoAdminInput,
|
||||
auth: AuthContext,
|
||||
): Promise<PeriodoAnno> {
|
||||
return prisma.periodoAnno.create({
|
||||
data: {
|
||||
nome: input.nome,
|
||||
inizioMese: input.inizioMese ?? null,
|
||||
fineMese: input.fineMese ?? null,
|
||||
utenteModifica: auth.name,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function updatePeriodoAnno(
|
||||
id: number,
|
||||
input: PeriodoAnnoAdminInput,
|
||||
auth: AuthContext,
|
||||
): Promise<PeriodoAnno> {
|
||||
await assertExists(prisma.periodoAnno.findUnique({ where: { id } }), 'Periodo anno non trovato');
|
||||
|
||||
return prisma.periodoAnno.update({
|
||||
where: { id },
|
||||
data: {
|
||||
nome: input.nome,
|
||||
inizioMese: input.inizioMese ?? null,
|
||||
fineMese: input.fineMese ?? null,
|
||||
utenteModifica: auth.name,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function deletePeriodoAnno(id: number): Promise<void> {
|
||||
await assertExists(prisma.periodoAnno.findUnique({ where: { id } }), 'Periodo anno non trovato');
|
||||
|
||||
const linkCount = await prisma.periodoAnnoAttivita.count({ where: { periodoAnnoId: id } });
|
||||
if (linkCount > 0) {
|
||||
throw new HttpError(409, `In uso da ${linkCount} attività, impossibile eliminare`);
|
||||
}
|
||||
|
||||
await prisma.periodoAnno.delete({ where: { id } });
|
||||
}
|
||||
|
||||
export async function proponiPeriodoAnno(nome: string, auth: AuthContext): Promise<PeriodoAnno> {
|
||||
const created = await prisma.periodoAnno.create({
|
||||
data: {
|
||||
nome,
|
||||
stato: 'DA_APPROVARE',
|
||||
creatoDaId: auth.userId,
|
||||
utenteModifica: auth.name,
|
||||
},
|
||||
});
|
||||
|
||||
await notificheService.creaBroadcastModerazione(
|
||||
'TASSONOMIA_PROPOSTA',
|
||||
`Nuovo periodo dell'anno proposto da approvare: "${created.nome}"`,
|
||||
'/tassonomie',
|
||||
);
|
||||
|
||||
return created;
|
||||
}
|
||||
|
||||
export async function approvaPeriodoAnno(id: number): Promise<PeriodoAnno> {
|
||||
const entity = await assertExists(
|
||||
prisma.periodoAnno.findUnique({ where: { id } }),
|
||||
'Periodo anno non trovato',
|
||||
);
|
||||
if (entity.stato !== 'DA_APPROVARE') {
|
||||
throw new HttpError(409, 'Periodo anno già confermato');
|
||||
}
|
||||
|
||||
return prisma.periodoAnno.update({ where: { id }, data: { stato: 'CONFERMATA' } });
|
||||
}
|
||||
|
||||
export async function rifiutaPeriodoAnno(id: number): Promise<void> {
|
||||
const entity = await assertExists(
|
||||
prisma.periodoAnno.findUnique({ where: { id } }),
|
||||
'Periodo anno non trovato',
|
||||
);
|
||||
if (entity.stato !== 'DA_APPROVARE') {
|
||||
throw new HttpError(409, 'Solo le proposte in attesa possono essere rifiutate');
|
||||
}
|
||||
|
||||
await prisma.$transaction([
|
||||
prisma.periodoAnnoAttivita.deleteMany({ where: { periodoAnnoId: id } }),
|
||||
prisma.periodoAnno.delete({ where: { id } }),
|
||||
]);
|
||||
}
|
||||
|
||||
async function assertExists<T>(promise: Promise<T | null>, message: string): Promise<T> {
|
||||
const entity = await promise;
|
||||
if (!entity) {
|
||||
throw new HttpError(404, message);
|
||||
}
|
||||
return entity;
|
||||
}
|
||||
@@ -3,6 +3,8 @@ export interface TipologicaDto {
|
||||
nome: string;
|
||||
}
|
||||
|
||||
export type StatoTassonomiaDto = 'CONFERMATA' | 'DA_APPROVARE';
|
||||
|
||||
export interface BaseDto {
|
||||
dataCreazione: Date;
|
||||
dataModifica: Date;
|
||||
@@ -15,6 +17,7 @@ export interface BrancaDto extends BaseDto {
|
||||
inizioEta: number | null;
|
||||
fineEta: number | null;
|
||||
colore: string | null;
|
||||
stato: StatoTassonomiaDto;
|
||||
cancellato: boolean;
|
||||
}
|
||||
|
||||
@@ -23,6 +26,7 @@ export interface CategoriaDto extends BaseDto {
|
||||
nome: string;
|
||||
padre: number | null;
|
||||
tipo: TipologicaDto;
|
||||
stato: StatoTassonomiaDto;
|
||||
cancellato: boolean;
|
||||
}
|
||||
|
||||
@@ -48,9 +52,18 @@ export interface PeriodoAnnoDto extends BaseDto {
|
||||
nome: string;
|
||||
inizioMese: number | null;
|
||||
fineMese: number | null;
|
||||
stato: StatoTassonomiaDto;
|
||||
cancellato: boolean;
|
||||
}
|
||||
|
||||
export interface NotaDto {
|
||||
id: number;
|
||||
attivitaId: number;
|
||||
testo: string;
|
||||
autore: string;
|
||||
dataCreazione: Date;
|
||||
}
|
||||
|
||||
export interface AttivitaDto extends BaseDto {
|
||||
id: number | null;
|
||||
nome: string;
|
||||
@@ -62,6 +75,18 @@ export interface AttivitaDto extends BaseDto {
|
||||
materialeList: MaterialeDto[];
|
||||
paragrafoList: ParagrafoDto[];
|
||||
periodoAnnoList: PeriodoAnnoDto[];
|
||||
// Popolata solo se il richiedente è l'autore o ha ruolo admin/moderatore (vedi puoGestire
|
||||
// in attivita.service.ts); per chiunque altro arriva sempre vuota, anche se esistono note.
|
||||
noteList: NotaDto[];
|
||||
}
|
||||
|
||||
export interface NotificaDto {
|
||||
id: number;
|
||||
tipo: string;
|
||||
messaggio: string;
|
||||
link: string | null;
|
||||
letta: boolean;
|
||||
dataCreazione: Date;
|
||||
}
|
||||
|
||||
export interface SearchObjectDto {
|
||||
|
||||
@@ -49,6 +49,39 @@ export const attivitaSaveSchema = z.object({
|
||||
paragrafoList: z.array(paragrafoInputSchema),
|
||||
});
|
||||
|
||||
export const brancaAdminSchema = z.object({
|
||||
nome: z.string().min(1),
|
||||
inizioEta: z.number().int().nullable().optional(),
|
||||
fineEta: z.number().int().nullable().optional(),
|
||||
colore: z.string().max(10).nullable().optional(),
|
||||
});
|
||||
|
||||
export const categoriaAdminSchema = z.object({
|
||||
nome: z.string().min(1),
|
||||
padreId: z.number().int().nullable().optional(),
|
||||
tipoId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const periodoAnnoAdminSchema = z.object({
|
||||
nome: z.string().min(1),
|
||||
inizioMese: z.number().int().min(1).max(12).nullable().optional(),
|
||||
fineMese: z.number().int().min(1).max(12).nullable().optional(),
|
||||
});
|
||||
|
||||
export const proponiTassonomiaSchema = z.object({
|
||||
nome: z.string().min(1),
|
||||
});
|
||||
|
||||
export const notaAttivitaSchema = z.object({
|
||||
testo: z.string().min(1).max(1000),
|
||||
});
|
||||
|
||||
export type BrancaAdminInput = z.infer<typeof brancaAdminSchema>;
|
||||
export type CategoriaAdminInput = z.infer<typeof categoriaAdminSchema>;
|
||||
export type PeriodoAnnoAdminInput = z.infer<typeof periodoAnnoAdminSchema>;
|
||||
export type ProponiTassonomiaInput = z.infer<typeof proponiTassonomiaSchema>;
|
||||
export type NotaAttivitaInput = z.infer<typeof notaAttivitaSchema>;
|
||||
|
||||
export type TipologicaInput = z.infer<typeof tipologicaSchema>;
|
||||
export type ParagrafoInput = z.infer<typeof paragrafoInputSchema>;
|
||||
export type BrancaInput = z.infer<typeof brancaInputSchema>;
|
||||
|
||||
Reference in New Issue
Block a user