generazine backend
This commit is contained in:
parent
712d4add3c
commit
3c2694d607
19 changed files with 896 additions and 21 deletions
34
dist/index.js
vendored
34
dist/index.js
vendored
|
|
@ -1,23 +1,31 @@
|
|||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const express_1 = __importDefault(require("express"));
|
||||
const cors_1 = __importDefault(require("cors"));
|
||||
const dotenv_1 = __importDefault(require("dotenv"));
|
||||
dotenv_1.default.config();
|
||||
const app = (0, express_1.default)();
|
||||
import { ExpressAuth } from "@auth/express";
|
||||
import cors from "cors";
|
||||
import dotenv from "dotenv";
|
||||
import express from "express";
|
||||
import { authConfig } from "./auth/config.js";
|
||||
import { errorHandler } from "./middleware/errorHandler.js";
|
||||
import categorieRouter from "./routes/categorie.js";
|
||||
import rimborsiRouter from "./routes/rimborsi.js";
|
||||
import statisticheRouter from "./routes/statistiche.js";
|
||||
import utentiRouter from "./routes/utenti.js";
|
||||
dotenv.config();
|
||||
const app = express();
|
||||
const port = Number(process.env.PORT) || 3003;
|
||||
app.set("trust proxy", 1);
|
||||
app.use((0, cors_1.default)({
|
||||
origin: process.env.FRONTEND_URL ?? "https://provapraticafrontend.andreavillari.it",
|
||||
app.use(cors({
|
||||
origin: process.env.FRONTEND_URL ?? "http://localhost:5173",
|
||||
credentials: true,
|
||||
}));
|
||||
app.use(express_1.default.json());
|
||||
app.use(express.json());
|
||||
app.get("/api/health", (_req, res) => {
|
||||
res.json({ status: "ok" });
|
||||
});
|
||||
app.use("/api/auth/*", ExpressAuth(authConfig));
|
||||
app.use("/api/utenti", utentiRouter);
|
||||
app.use("/api/categorie-spesa", categorieRouter);
|
||||
app.use("/api/rimborsi", rimborsiRouter);
|
||||
app.use("/api/statistiche", statisticheRouter);
|
||||
app.use(errorHandler);
|
||||
app.listen(port, "0.0.0.0", () => {
|
||||
console.log(`Backend in ascolto sulla porta ${port}`);
|
||||
});
|
||||
|
|
|
|||
2
dist/index.js.map
vendored
2
dist/index.js.map
vendored
|
|
@ -1 +1 @@
|
|||
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;AAAA,sDAA8B;AAC9B,gDAAwB;AACxB,oDAA4B;AAE5B,gBAAM,CAAC,MAAM,EAAE,CAAC;AAEhB,MAAM,GAAG,GAAG,IAAA,iBAAO,GAAE,CAAC;AACtB,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC;AAE9C,GAAG,CAAC,GAAG,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC;AAC1B,GAAG,CAAC,GAAG,CAAC,IAAA,cAAI,EAAC;IACX,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,YAAY,IAAI,+CAA+C;IACnF,WAAW,EAAE,IAAI;CAClB,CAAC,CAAC,CAAC;AACJ,GAAG,CAAC,GAAG,CAAC,iBAAO,CAAC,IAAI,EAAE,CAAC,CAAC;AAExB,GAAG,CAAC,GAAG,CAAC,aAAa,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE;IACnC,GAAG,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;AAC7B,CAAC,CAAC,CAAC;AAEH,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE;IAC/B,OAAO,CAAC,GAAG,CAAC,kCAAkC,IAAI,EAAE,CAAC,CAAC;AACxD,CAAC,CAAC,CAAC"}
|
||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,MAAM,MAAM,QAAQ,CAAC;AAC5B,OAAO,OAAO,MAAM,SAAS,CAAC;AAC9B,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAC5D,OAAO,eAAe,MAAM,uBAAuB,CAAC;AACpD,OAAO,cAAc,MAAM,sBAAsB,CAAC;AAClD,OAAO,iBAAiB,MAAM,yBAAyB,CAAC;AACxD,OAAO,YAAY,MAAM,oBAAoB,CAAC;AAE9C,MAAM,CAAC,MAAM,EAAE,CAAC;AAEhB,MAAM,GAAG,GAAG,OAAO,EAAE,CAAC;AACtB,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC;AAE9C,GAAG,CAAC,GAAG,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC;AAC1B,GAAG,CAAC,GAAG,CACL,IAAI,CAAC;IACH,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,YAAY,IAAI,uBAAuB;IAC3D,WAAW,EAAE,IAAI;CAClB,CAAC,CACH,CAAC;AACF,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;AAExB,GAAG,CAAC,GAAG,CAAC,aAAa,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE;IACnC,GAAG,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;AAC7B,CAAC,CAAC,CAAC;AAEH,GAAG,CAAC,GAAG,CAAC,aAAa,EAAE,WAAW,CAAC,UAAU,CAAC,CAAC,CAAC;AAChD,GAAG,CAAC,GAAG,CAAC,aAAa,EAAE,YAAY,CAAC,CAAC;AACrC,GAAG,CAAC,GAAG,CAAC,sBAAsB,EAAE,eAAe,CAAC,CAAC;AACjD,GAAG,CAAC,GAAG,CAAC,eAAe,EAAE,cAAc,CAAC,CAAC;AACzC,GAAG,CAAC,GAAG,CAAC,kBAAkB,EAAE,iBAAiB,CAAC,CAAC;AAE/C,GAAG,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;AAEtB,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE;IAC/B,OAAO,CAAC,GAAG,CAAC,kCAAkC,IAAI,EAAE,CAAC,CAAC;AACxD,CAAC,CAAC,CAAC"}
|
||||
25
initdb/02_seed.sql
Normal file
25
initdb/02_seed.sql
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
USE provapratica;
|
||||
|
||||
INSERT INTO categorie_spesa (descrizione) VALUES
|
||||
('Trasferta'),
|
||||
('Pasto'),
|
||||
('Pedaggio'),
|
||||
('Parcheggio'),
|
||||
('Acquisto autorizzato');
|
||||
|
||||
-- Password per tutti: Password123!
|
||||
INSERT INTO utenti (nome, cognome, email, password_hash, ruolo) VALUES
|
||||
('Marco', 'Rossi', 'marco.rossi@azienda.it', '$2b$10$xJ9shj7EP1XZzcKztutcs.K4np19OVExwZcSGlBmS/5WTZSt9W.c6', 'DIPENDENTE'),
|
||||
('Laura', 'Bianchi', 'laura.bianchi@azienda.it', '$2b$10$xJ9shj7EP1XZzcKztutcs.K4np19OVExwZcSGlBmS/5WTZSt9W.c6', 'DIPENDENTE'),
|
||||
('Giulia', 'Verdi', 'giulia.verdi@azienda.it', '$2b$10$xJ9shj7EP1XZzcKztutcs.K4np19OVExwZcSGlBmS/5WTZSt9W.c6', 'DIPENDENTE'),
|
||||
('Paolo', 'Admin', 'admin@azienda.it', '$2b$10$xJ9shj7EP1XZzcKztutcs.K4np19OVExwZcSGlBmS/5WTZSt9W.c6', 'RESPONSABILE_AMMINISTRATIVO');
|
||||
|
||||
INSERT INTO richieste_rimborso
|
||||
(data_inserimento, data_spesa, categoria_id, importo, descrizione, riferimento_giustificativo, stato, dipendente_id, data_valutazione, responsabile_valutazione_id, motivazione_rifiuto, data_liquidazione)
|
||||
VALUES
|
||||
('2026-05-02 09:00:00', '2026-05-01', 1, 120.50, 'Trasferta Milano-Bologna', 'BIGL-001', 'IN_ATTESA', 1, NULL, NULL, NULL, NULL),
|
||||
('2026-05-03 10:00:00', '2026-05-02', 2, 35.00, 'Pranzo con cliente', 'SCONTR-102', 'IN_ATTESA', 1, NULL, NULL, NULL, NULL),
|
||||
('2026-05-05 11:00:00', '2026-05-04', 3, 18.50, 'Pedaggio autostrada A1', 'PED-445', 'APPROVATA', 2, '2026-05-06 14:00:00', 4, NULL, NULL),
|
||||
('2026-05-07 08:30:00', '2026-05-06', 4, 12.00, 'Parcheggio centro storico', NULL, 'LIQUIDATA', 2, '2026-05-08 09:00:00', 4, NULL, '2026-05-10 16:00:00'),
|
||||
('2026-05-08 15:00:00', '2026-05-07', 5, 45.00, 'Cancelleria per ufficio', 'FATT-778', 'RIFIUTATA', 3, '2026-05-09 10:00:00', 4, 'Spesa non autorizzata preventivamente', NULL),
|
||||
('2026-05-10 09:00:00', '2026-05-09', 1, 89.00, 'Taxi aeroporto', 'TAXI-22', 'APPROVATA', 3, '2026-05-11 11:00:00', 4, NULL, NULL);
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
"name": "provapraticabackend",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"type": "commonjs",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "tsx watch src/index.ts",
|
||||
"build": "tsc",
|
||||
|
|
|
|||
61
src/auth/config.ts
Normal file
61
src/auth/config.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import type { ExpressAuthConfig } from "@auth/express";
|
||||
import Credentials from "@auth/express/providers/credentials";
|
||||
import bcrypt from "bcryptjs";
|
||||
import type { RowDataPacket } from "mysql2";
|
||||
import pool from "../db/pool.js";
|
||||
import type { Ruolo, Utente } from "../types/index.js";
|
||||
|
||||
export const authConfig: ExpressAuthConfig = {
|
||||
trustHost: true,
|
||||
secret: process.env.AUTH_SECRET,
|
||||
session: { strategy: "jwt" },
|
||||
pages: { signIn: "/login" },
|
||||
providers: [
|
||||
Credentials({
|
||||
credentials: {
|
||||
email: { label: "Email", type: "email" },
|
||||
password: { label: "Password", type: "password" },
|
||||
},
|
||||
async authorize(credentials) {
|
||||
const email = credentials?.email?.toString().trim().toLowerCase();
|
||||
const password = credentials?.password?.toString();
|
||||
|
||||
if (!email || !password) return null;
|
||||
|
||||
const [rows] = await pool.execute<RowDataPacket[]>(
|
||||
"SELECT utente_id, nome, cognome, email, password_hash, ruolo FROM utenti WHERE email = ?",
|
||||
[email]
|
||||
);
|
||||
|
||||
const user = rows[0] as Utente | undefined;
|
||||
if (!user) return null;
|
||||
|
||||
const valid = await bcrypt.compare(password, user.password_hash);
|
||||
if (!valid) return null;
|
||||
|
||||
return {
|
||||
id: String(user.utente_id),
|
||||
email: user.email,
|
||||
name: `${user.nome} ${user.cognome}`,
|
||||
ruolo: user.ruolo as Ruolo,
|
||||
};
|
||||
},
|
||||
}),
|
||||
],
|
||||
callbacks: {
|
||||
async jwt({ token, user }) {
|
||||
if (user) {
|
||||
token.id = user.id;
|
||||
token.ruolo = user.ruolo;
|
||||
}
|
||||
return token;
|
||||
},
|
||||
async session({ session, token }) {
|
||||
if (session.user) {
|
||||
session.user.id = token.id as string;
|
||||
session.user.ruolo = token.ruolo as Ruolo;
|
||||
}
|
||||
return session;
|
||||
},
|
||||
},
|
||||
};
|
||||
14
src/db/pool.ts
Normal file
14
src/db/pool.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import mysql from "mysql2/promise";
|
||||
|
||||
const pool = mysql.createPool({
|
||||
host: process.env.DB_HOST ?? "127.0.0.1",
|
||||
port: Number(process.env.DB_PORT ?? 3307),
|
||||
database: process.env.DB_NAME ?? "provapratica",
|
||||
user: process.env.DB_USER ?? "provapratica_user",
|
||||
password: process.env.DB_PASSWORD ?? "",
|
||||
waitForConnections: true,
|
||||
connectionLimit: 10,
|
||||
dateStrings: false,
|
||||
});
|
||||
|
||||
export default pool;
|
||||
25
src/index.ts
25
src/index.ts
|
|
@ -1,6 +1,13 @@
|
|||
import express from "express";
|
||||
import { ExpressAuth } from "@auth/express";
|
||||
import cors from "cors";
|
||||
import dotenv from "dotenv";
|
||||
import express from "express";
|
||||
import { authConfig } from "./auth/config.js";
|
||||
import { errorHandler } from "./middleware/errorHandler.js";
|
||||
import categorieRouter from "./routes/categorie.js";
|
||||
import rimborsiRouter from "./routes/rimborsi.js";
|
||||
import statisticheRouter from "./routes/statistiche.js";
|
||||
import utentiRouter from "./routes/utenti.js";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
|
|
@ -8,16 +15,26 @@ const app = express();
|
|||
const port = Number(process.env.PORT) || 3003;
|
||||
|
||||
app.set("trust proxy", 1);
|
||||
app.use(cors({
|
||||
origin: process.env.FRONTEND_URL ?? "https://provapraticafrontend.andreavillari.it",
|
||||
app.use(
|
||||
cors({
|
||||
origin: process.env.FRONTEND_URL ?? "http://localhost:5173",
|
||||
credentials: true,
|
||||
}));
|
||||
})
|
||||
);
|
||||
app.use(express.json());
|
||||
|
||||
app.get("/api/health", (_req, res) => {
|
||||
res.json({ status: "ok" });
|
||||
});
|
||||
|
||||
app.use("/api/auth/*", ExpressAuth(authConfig));
|
||||
app.use("/api/utenti", utentiRouter);
|
||||
app.use("/api/categorie-spesa", categorieRouter);
|
||||
app.use("/api/rimborsi", rimborsiRouter);
|
||||
app.use("/api/statistiche", statisticheRouter);
|
||||
|
||||
app.use(errorHandler);
|
||||
|
||||
app.listen(port, "0.0.0.0", () => {
|
||||
console.log(`Backend in ascolto sulla porta ${port}`);
|
||||
});
|
||||
42
src/middleware/auth.ts
Normal file
42
src/middleware/auth.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import { getSession } from "@auth/express";
|
||||
import type { NextFunction, Request, Response } from "express";
|
||||
import { authConfig } from "../auth/config.js";
|
||||
import type { Ruolo } from "../types/index.js";
|
||||
|
||||
export async function requireAuth(req: Request, res: Response, next: NextFunction) {
|
||||
const session = await getSession(req, authConfig);
|
||||
if (!session?.user?.id) {
|
||||
res.status(401).json({ error: "Autenticazione richiesta" });
|
||||
return;
|
||||
}
|
||||
req.user = {
|
||||
id: Number(session.user.id),
|
||||
email: session.user.email,
|
||||
name: session.user.name,
|
||||
ruolo: session.user.ruolo,
|
||||
};
|
||||
next();
|
||||
}
|
||||
|
||||
export function requireRole(...ruoli: Ruolo[]) {
|
||||
return (req: Request, res: Response, next: NextFunction) => {
|
||||
if (!req.user || !ruoli.includes(req.user.ruolo)) {
|
||||
res.status(403).json({ error: "Operazione non consentita per il ruolo corrente" });
|
||||
return;
|
||||
}
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
declare global {
|
||||
namespace Express {
|
||||
interface Request {
|
||||
user?: {
|
||||
id: number;
|
||||
email: string;
|
||||
name: string;
|
||||
ruolo: Ruolo;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
6
src/middleware/errorHandler.ts
Normal file
6
src/middleware/errorHandler.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import type { NextFunction, Request, Response } from "express";
|
||||
|
||||
export function errorHandler(err: Error, _req: Request, res: Response, _next: NextFunction) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: "Errore interno del server" });
|
||||
}
|
||||
17
src/routes/categorie.ts
Normal file
17
src/routes/categorie.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { Router } from "express";
|
||||
import { requireAuth } from "../middleware/auth.js";
|
||||
import { listCategorie } from "../services/rimborsiService.js";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get("/", requireAuth, async (_req, res) => {
|
||||
const categorie = await listCategorie();
|
||||
res.json(
|
||||
categorie.map((c) => ({
|
||||
id: c.categoria_id,
|
||||
descrizione: c.descrizione,
|
||||
}))
|
||||
);
|
||||
});
|
||||
|
||||
export default router;
|
||||
168
src/routes/rimborsi.ts
Normal file
168
src/routes/rimborsi.ts
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
import { Router } from "express";
|
||||
import { requireAuth, requireRole } from "../middleware/auth.js";
|
||||
import {
|
||||
approvaRimborso,
|
||||
categoriaExists,
|
||||
createRimborso,
|
||||
deleteRimborso,
|
||||
getRimborsoById,
|
||||
liquidaRimborso,
|
||||
listRimborsi,
|
||||
rifiutaRimborso,
|
||||
updateRimborso,
|
||||
validateRichiestaBody,
|
||||
} from "../services/rimborsiService.js";
|
||||
import { formatRimborso } from "../utils/format.js";
|
||||
import type { StatoRichiesta } from "../types/index.js";
|
||||
|
||||
const router = Router();
|
||||
const STATI: StatoRichiesta[] = ["IN_ATTESA", "APPROVATA", "RIFIUTATA", "LIQUIDATA"];
|
||||
|
||||
router.use(requireAuth);
|
||||
|
||||
router.get("/", async (req, res) => {
|
||||
const stato = req.query.stato?.toString() as StatoRichiesta | undefined;
|
||||
if (stato && !STATI.includes(stato)) {
|
||||
res.status(400).json({ error: "Stato non valido" });
|
||||
return;
|
||||
}
|
||||
|
||||
const rimborsi = await listRimborsi(
|
||||
{
|
||||
stato,
|
||||
categoriaId: req.query.categoriaId ? Number(req.query.categoriaId) : undefined,
|
||||
mese: req.query.mese?.toString(),
|
||||
dipendenteId: req.query.dipendenteId ? Number(req.query.dipendenteId) : undefined,
|
||||
},
|
||||
req.user!.ruolo,
|
||||
req.user!.id
|
||||
);
|
||||
|
||||
res.json(rimborsi.map(formatRimborso));
|
||||
});
|
||||
|
||||
router.get("/:id", async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
const rimborso = await getRimborsoById(id);
|
||||
if (!rimborso) {
|
||||
res.status(404).json({ error: "Richiesta non trovata" });
|
||||
return;
|
||||
}
|
||||
if (req.user!.ruolo === "DIPENDENTE" && rimborso.dipendente_id !== req.user!.id) {
|
||||
res.status(403).json({ error: "Accesso non consentito" });
|
||||
return;
|
||||
}
|
||||
res.json(formatRimborso(rimborso));
|
||||
});
|
||||
|
||||
router.post("/", async (req, res) => {
|
||||
if (req.user!.ruolo !== "DIPENDENTE") {
|
||||
res.status(403).json({ error: "Solo i dipendenti possono creare richieste" });
|
||||
return;
|
||||
}
|
||||
|
||||
const { error, data } = validateRichiestaBody(req.body);
|
||||
if (error || !data) {
|
||||
res.status(400).json({ error });
|
||||
return;
|
||||
}
|
||||
if (!(await categoriaExists(data.categoriaId))) {
|
||||
res.status(400).json({ error: "Categoria non esistente" });
|
||||
return;
|
||||
}
|
||||
|
||||
const id = await createRimborso(req.user!.id, data);
|
||||
const created = await getRimborsoById(id);
|
||||
res.status(201).json(formatRimborso(created!));
|
||||
});
|
||||
|
||||
router.put("/:id", async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
const existing = await getRimborsoById(id);
|
||||
if (!existing) {
|
||||
res.status(404).json({ error: "Richiesta non trovata" });
|
||||
return;
|
||||
}
|
||||
if (req.user!.ruolo === "DIPENDENTE" && existing.dipendente_id !== req.user!.id) {
|
||||
res.status(403).json({ error: "Accesso non consentito" });
|
||||
return;
|
||||
}
|
||||
if (existing.stato !== "IN_ATTESA") {
|
||||
res.status(400).json({ error: "La richiesta non può essere modificata nello stato corrente" });
|
||||
return;
|
||||
}
|
||||
|
||||
const { error, data } = validateRichiestaBody(req.body);
|
||||
if (error || !data) {
|
||||
res.status(400).json({ error });
|
||||
return;
|
||||
}
|
||||
if (!(await categoriaExists(data.categoriaId))) {
|
||||
res.status(400).json({ error: "Categoria non esistente" });
|
||||
return;
|
||||
}
|
||||
|
||||
await updateRimborso(id, data);
|
||||
const updated = await getRimborsoById(id);
|
||||
res.json(formatRimborso(updated!));
|
||||
});
|
||||
|
||||
router.delete("/:id", async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
const existing = await getRimborsoById(id);
|
||||
if (!existing) {
|
||||
res.status(404).json({ error: "Richiesta non trovata" });
|
||||
return;
|
||||
}
|
||||
if (req.user!.ruolo === "DIPENDENTE" && existing.dipendente_id !== req.user!.id) {
|
||||
res.status(403).json({ error: "Accesso non consentito" });
|
||||
return;
|
||||
}
|
||||
if (existing.stato !== "IN_ATTESA") {
|
||||
res.status(400).json({ error: "La richiesta non può essere eliminata nello stato corrente" });
|
||||
return;
|
||||
}
|
||||
|
||||
const deleted = await deleteRimborso(id);
|
||||
if (!deleted) {
|
||||
res.status(400).json({ error: "Impossibile eliminare la richiesta" });
|
||||
return;
|
||||
}
|
||||
res.json({ message: "Richiesta eliminata" });
|
||||
});
|
||||
|
||||
router.put("/:id/approva", requireRole("RESPONSABILE_AMMINISTRATIVO"), async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
const ok = await approvaRimborso(id, req.user!.id);
|
||||
if (!ok) {
|
||||
res.status(400).json({ error: "Impossibile approvare la richiesta" });
|
||||
return;
|
||||
}
|
||||
const updated = await getRimborsoById(id);
|
||||
res.json(formatRimborso(updated!));
|
||||
});
|
||||
|
||||
router.put("/:id/rifiuta", requireRole("RESPONSABILE_AMMINISTRATIVO"), async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
const motivazione = req.body.motivazione?.toString().trim() || null;
|
||||
const ok = await rifiutaRimborso(id, req.user!.id, motivazione);
|
||||
if (!ok) {
|
||||
res.status(400).json({ error: "Impossibile rifiutare la richiesta" });
|
||||
return;
|
||||
}
|
||||
const updated = await getRimborsoById(id);
|
||||
res.json(formatRimborso(updated!));
|
||||
});
|
||||
|
||||
router.put("/:id/liquida", requireRole("RESPONSABILE_AMMINISTRATIVO"), async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
const ok = await liquidaRimborso(id);
|
||||
if (!ok) {
|
||||
res.status(400).json({ error: "Impossibile liquidare la richiesta" });
|
||||
return;
|
||||
}
|
||||
const updated = await getRimborsoById(id);
|
||||
res.json(formatRimborso(updated!));
|
||||
});
|
||||
|
||||
export default router;
|
||||
16
src/routes/statistiche.ts
Normal file
16
src/routes/statistiche.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { Router } from "express";
|
||||
import { requireAuth, requireRole } from "../middleware/auth.js";
|
||||
import { getStatisticheRimborsi } from "../services/statisticheService.js";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get("/rimborsi", requireAuth, requireRole("RESPONSABILE_AMMINISTRATIVO"), async (req, res) => {
|
||||
const mese = req.query.mese?.toString();
|
||||
const categoriaId = req.query.categoriaId ? Number(req.query.categoriaId) : undefined;
|
||||
const dipendenteId = req.query.dipendenteId ? Number(req.query.dipendenteId) : undefined;
|
||||
|
||||
const stats = await getStatisticheRimborsi({ mese, categoriaId, dipendenteId });
|
||||
res.json(stats);
|
||||
});
|
||||
|
||||
export default router;
|
||||
65
src/routes/utenti.ts
Normal file
65
src/routes/utenti.ts
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import { getSession } from "@auth/express";
|
||||
import { Router } from "express";
|
||||
import { authConfig } from "../auth/config.js";
|
||||
import { requireAuth, requireRole } from "../middleware/auth.js";
|
||||
import { listDipendenti } from "../services/rimborsiService.js";
|
||||
import { createUtente, findByEmail, validateRegistration } from "../services/utentiService.js";
|
||||
import type { Ruolo } from "../types/index.js";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.post("/register", async (req, res) => {
|
||||
const validationError = validateRegistration(req.body);
|
||||
if (validationError) {
|
||||
res.status(400).json({ error: validationError });
|
||||
return;
|
||||
}
|
||||
|
||||
const existing = await findByEmail(req.body.email);
|
||||
if (existing) {
|
||||
res.status(409).json({ error: "Email già registrata" });
|
||||
return;
|
||||
}
|
||||
|
||||
const ruolo = (req.body.ruolo as Ruolo) ?? "DIPENDENTE";
|
||||
const id = await createUtente({
|
||||
nome: req.body.nome,
|
||||
cognome: req.body.cognome,
|
||||
email: req.body.email,
|
||||
password: req.body.password,
|
||||
ruolo,
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
message: "Registrazione completata",
|
||||
utente: { id, nome: req.body.nome.trim(), cognome: req.body.cognome.trim(), email: req.body.email.trim().toLowerCase(), ruolo },
|
||||
});
|
||||
});
|
||||
|
||||
router.get("/dipendenti", requireAuth, requireRole("RESPONSABILE_AMMINISTRATIVO"), async (_req, res) => {
|
||||
const dipendenti = await listDipendenti();
|
||||
res.json(
|
||||
dipendenti.map((d) => ({
|
||||
id: d.utente_id,
|
||||
nome: d.nome,
|
||||
cognome: d.cognome,
|
||||
}))
|
||||
);
|
||||
});
|
||||
|
||||
router.get("/me", async (req, res) => {
|
||||
const session = await getSession(req, authConfig);
|
||||
if (!session?.user?.id) {
|
||||
res.status(401).json({ error: "Non autenticato" });
|
||||
return;
|
||||
}
|
||||
res.json({
|
||||
id: Number(session.user.id),
|
||||
email: session.user.email,
|
||||
nome: session.user.name.split(" ")[0],
|
||||
cognome: session.user.name.split(" ").slice(1).join(" "),
|
||||
ruolo: session.user.ruolo,
|
||||
});
|
||||
});
|
||||
|
||||
export default router;
|
||||
225
src/services/rimborsiService.ts
Normal file
225
src/services/rimborsiService.ts
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
import type { ResultSetHeader, RowDataPacket } from "mysql2";
|
||||
import pool from "../db/pool.js";
|
||||
import type { RichiestaRimborsoDettaglio, Ruolo, StatoRichiesta } from "../types/index.js";
|
||||
|
||||
const SELECT_BASE = `
|
||||
SELECT r.*,
|
||||
c.descrizione AS categoria_descrizione,
|
||||
d.nome AS dipendente_nome, d.cognome AS dipendente_cognome,
|
||||
resp.nome AS responsabile_nome, resp.cognome AS responsabile_cognome
|
||||
FROM richieste_rimborso r
|
||||
JOIN categorie_spesa c ON c.categoria_id = r.categoria_id
|
||||
JOIN utenti d ON d.utente_id = r.dipendente_id
|
||||
LEFT JOIN utenti resp ON resp.utente_id = r.responsabile_valutazione_id
|
||||
`;
|
||||
|
||||
function mapRow(row: RowDataPacket): RichiestaRimborsoDettaglio {
|
||||
return {
|
||||
richiesta_id: row.richiesta_id,
|
||||
data_inserimento: row.data_inserimento,
|
||||
data_spesa: row.data_spesa,
|
||||
categoria_id: row.categoria_id,
|
||||
importo: Number(row.importo),
|
||||
descrizione: row.descrizione,
|
||||
riferimento_giustificativo: row.riferimento_giustificativo,
|
||||
stato: row.stato,
|
||||
dipendente_id: row.dipendente_id,
|
||||
data_valutazione: row.data_valutazione,
|
||||
responsabile_valutazione_id: row.responsabile_valutazione_id,
|
||||
motivazione_rifiuto: row.motivazione_rifiuto,
|
||||
data_liquidazione: row.data_liquidazione,
|
||||
categoria_descrizione: row.categoria_descrizione,
|
||||
dipendente_nome: row.dipendente_nome,
|
||||
dipendente_cognome: row.dipendente_cognome,
|
||||
responsabile_nome: row.responsabile_nome,
|
||||
responsabile_cognome: row.responsabile_cognome,
|
||||
};
|
||||
}
|
||||
|
||||
export interface FiltriRimborsi {
|
||||
stato?: StatoRichiesta;
|
||||
categoriaId?: number;
|
||||
mese?: string;
|
||||
dipendenteId?: number;
|
||||
}
|
||||
|
||||
function buildFilters(
|
||||
filtri: FiltriRimborsi,
|
||||
ruolo: Ruolo,
|
||||
userId: number
|
||||
): { where: string; params: (string | number)[] } {
|
||||
const clauses: string[] = [];
|
||||
const params: (string | number)[] = [];
|
||||
|
||||
if (ruolo === "DIPENDENTE") {
|
||||
clauses.push("r.dipendente_id = ?");
|
||||
params.push(userId);
|
||||
} else if (filtri.dipendenteId) {
|
||||
clauses.push("r.dipendente_id = ?");
|
||||
params.push(filtri.dipendenteId);
|
||||
}
|
||||
|
||||
if (filtri.stato) {
|
||||
clauses.push("r.stato = ?");
|
||||
params.push(filtri.stato);
|
||||
}
|
||||
if (filtri.categoriaId) {
|
||||
clauses.push("r.categoria_id = ?");
|
||||
params.push(filtri.categoriaId);
|
||||
}
|
||||
if (filtri.mese && /^\d{4}-\d{2}$/.test(filtri.mese)) {
|
||||
clauses.push("DATE_FORMAT(r.data_spesa, '%Y-%m') = ?");
|
||||
params.push(filtri.mese);
|
||||
}
|
||||
|
||||
const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
|
||||
return { where, params };
|
||||
}
|
||||
|
||||
export async function listRimborsi(
|
||||
filtri: FiltriRimborsi,
|
||||
ruolo: Ruolo,
|
||||
userId: number
|
||||
): Promise<RichiestaRimborsoDettaglio[]> {
|
||||
const { where, params } = buildFilters(filtri, ruolo, userId);
|
||||
const [rows] = await pool.execute<RowDataPacket[]>(
|
||||
`${SELECT_BASE} ${where} ORDER BY r.data_inserimento DESC`,
|
||||
params
|
||||
);
|
||||
return rows.map(mapRow);
|
||||
}
|
||||
|
||||
export async function getRimborsoById(id: number): Promise<RichiestaRimborsoDettaglio | null> {
|
||||
const [rows] = await pool.execute<RowDataPacket[]>(
|
||||
`${SELECT_BASE} WHERE r.richiesta_id = ?`,
|
||||
[id]
|
||||
);
|
||||
return rows[0] ? mapRow(rows[0]) : null;
|
||||
}
|
||||
|
||||
export async function categoriaExists(id: number): Promise<boolean> {
|
||||
const [rows] = await pool.execute<RowDataPacket[]>(
|
||||
"SELECT 1 FROM categorie_spesa WHERE categoria_id = ?",
|
||||
[id]
|
||||
);
|
||||
return rows.length > 0;
|
||||
}
|
||||
|
||||
export function validateRichiestaBody(
|
||||
body: Record<string, unknown>,
|
||||
isUpdate = false
|
||||
): { error: string | null; data?: { dataSpesa: string; categoriaId: number; importo: number; descrizione: string; riferimentoGiustificativo: string | null } } {
|
||||
const dataSpesa = body.dataSpesa?.toString().trim();
|
||||
const categoriaId = Number(body.categoriaId);
|
||||
const importo = Number(body.importo);
|
||||
const descrizione = body.descrizione?.toString().trim();
|
||||
const riferimento = body.riferimentoGiustificativo?.toString().trim() ?? null;
|
||||
|
||||
if (!isUpdate || dataSpesa !== undefined) {
|
||||
if (!dataSpesa) return { error: "La data della spesa è obbligatoria" };
|
||||
}
|
||||
if (!isUpdate || body.categoriaId !== undefined) {
|
||||
if (!categoriaId || Number.isNaN(categoriaId)) return { error: "Categoria non valida" };
|
||||
}
|
||||
if (!isUpdate || body.importo !== undefined) {
|
||||
if (!importo || importo <= 0) return { error: "L'importo deve essere maggiore di zero" };
|
||||
}
|
||||
if (!isUpdate || body.descrizione !== undefined) {
|
||||
if (!descrizione) return { error: "La descrizione è obbligatoria" };
|
||||
}
|
||||
if (riferimento !== null && riferimento !== "" && !riferimento.trim()) {
|
||||
return { error: "Il riferimento al giustificativo non può essere composto solo da spazi" };
|
||||
}
|
||||
|
||||
return {
|
||||
error: null,
|
||||
data: {
|
||||
dataSpesa: dataSpesa!,
|
||||
categoriaId,
|
||||
importo,
|
||||
descrizione: descrizione!,
|
||||
riferimentoGiustificativo: riferimento || null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function createRimborso(
|
||||
dipendenteId: number,
|
||||
data: { dataSpesa: string; categoriaId: number; importo: number; descrizione: string; riferimentoGiustificativo: string | null }
|
||||
): Promise<number> {
|
||||
const [result] = await pool.execute<ResultSetHeader>(
|
||||
`INSERT INTO richieste_rimborso
|
||||
(data_spesa, categoria_id, importo, descrizione, riferimento_giustificativo, stato, dipendente_id)
|
||||
VALUES (?, ?, ?, ?, ?, 'IN_ATTESA', ?)`,
|
||||
[data.dataSpesa, data.categoriaId, data.importo, data.descrizione, data.riferimentoGiustificativo, dipendenteId]
|
||||
);
|
||||
return result.insertId;
|
||||
}
|
||||
|
||||
export async function updateRimborso(
|
||||
id: number,
|
||||
data: { dataSpesa: string; categoriaId: number; importo: number; descrizione: string; riferimentoGiustificativo: string | null }
|
||||
): Promise<void> {
|
||||
await pool.execute(
|
||||
`UPDATE richieste_rimborso
|
||||
SET data_spesa = ?, categoria_id = ?, importo = ?, descrizione = ?, riferimento_giustificativo = ?
|
||||
WHERE richiesta_id = ? AND stato = 'IN_ATTESA'`,
|
||||
[data.dataSpesa, data.categoriaId, data.importo, data.descrizione, data.riferimentoGiustificativo, id]
|
||||
);
|
||||
}
|
||||
|
||||
export async function deleteRimborso(id: number): Promise<boolean> {
|
||||
const [result] = await pool.execute<ResultSetHeader>(
|
||||
"DELETE FROM richieste_rimborso WHERE richiesta_id = ? AND stato = 'IN_ATTESA'",
|
||||
[id]
|
||||
);
|
||||
return result.affectedRows > 0;
|
||||
}
|
||||
|
||||
export async function approvaRimborso(id: number, responsabileId: number): Promise<boolean> {
|
||||
const [result] = await pool.execute<ResultSetHeader>(
|
||||
`UPDATE richieste_rimborso
|
||||
SET stato = 'APPROVATA', data_valutazione = NOW(), responsabile_valutazione_id = ?, motivazione_rifiuto = NULL
|
||||
WHERE richiesta_id = ? AND stato = 'IN_ATTESA'`,
|
||||
[responsabileId, id]
|
||||
);
|
||||
return result.affectedRows > 0;
|
||||
}
|
||||
|
||||
export async function rifiutaRimborso(
|
||||
id: number,
|
||||
responsabileId: number,
|
||||
motivazione: string | null
|
||||
): Promise<boolean> {
|
||||
const [result] = await pool.execute<ResultSetHeader>(
|
||||
`UPDATE richieste_rimborso
|
||||
SET stato = 'RIFIUTATA', data_valutazione = NOW(), responsabile_valutazione_id = ?, motivazione_rifiuto = ?
|
||||
WHERE richiesta_id = ? AND stato = 'IN_ATTESA'`,
|
||||
[responsabileId, motivazione, id]
|
||||
);
|
||||
return result.affectedRows > 0;
|
||||
}
|
||||
|
||||
export async function liquidaRimborso(id: number): Promise<boolean> {
|
||||
const [result] = await pool.execute<ResultSetHeader>(
|
||||
`UPDATE richieste_rimborso
|
||||
SET stato = 'LIQUIDATA', data_liquidazione = NOW()
|
||||
WHERE richiesta_id = ? AND stato = 'APPROVATA'`,
|
||||
[id]
|
||||
);
|
||||
return result.affectedRows > 0;
|
||||
}
|
||||
|
||||
export async function listCategorie(): Promise<{ categoria_id: number; descrizione: string }[]> {
|
||||
const [rows] = await pool.execute<RowDataPacket[]>(
|
||||
"SELECT categoria_id, descrizione FROM categorie_spesa ORDER BY descrizione"
|
||||
);
|
||||
return rows as { categoria_id: number; descrizione: string }[];
|
||||
}
|
||||
|
||||
export async function listDipendenti(): Promise<{ utente_id: number; nome: string; cognome: string }[]> {
|
||||
const [rows] = await pool.execute<RowDataPacket[]>(
|
||||
"SELECT utente_id, nome, cognome FROM utenti WHERE ruolo = 'DIPENDENTE' ORDER BY cognome, nome"
|
||||
);
|
||||
return rows as { utente_id: number; nome: string; cognome: string }[];
|
||||
}
|
||||
54
src/services/statisticheService.ts
Normal file
54
src/services/statisticheService.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import type { RowDataPacket } from "mysql2";
|
||||
import pool from "../db/pool.js";
|
||||
import type { StatisticaRimborso } from "../types/index.js";
|
||||
|
||||
export interface FiltriStatistiche {
|
||||
mese?: string;
|
||||
categoriaId?: number;
|
||||
dipendenteId?: number;
|
||||
}
|
||||
|
||||
export async function getStatisticheRimborsi(filtri: FiltriStatistiche): Promise<StatisticaRimborso[]> {
|
||||
const clauses: string[] = [];
|
||||
const params: (string | number)[] = [];
|
||||
|
||||
if (filtri.mese && /^\d{4}-\d{2}$/.test(filtri.mese)) {
|
||||
clauses.push("DATE_FORMAT(r.data_spesa, '%Y-%m') = ?");
|
||||
params.push(filtri.mese);
|
||||
}
|
||||
if (filtri.categoriaId) {
|
||||
clauses.push("r.categoria_id = ?");
|
||||
params.push(filtri.categoriaId);
|
||||
}
|
||||
if (filtri.dipendenteId) {
|
||||
clauses.push("r.dipendente_id = ?");
|
||||
params.push(filtri.dipendenteId);
|
||||
}
|
||||
|
||||
const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
|
||||
|
||||
const [rows] = await pool.execute<RowDataPacket[]>(
|
||||
`SELECT
|
||||
DATE_FORMAT(r.data_spesa, '%Y-%m') AS mese,
|
||||
c.descrizione AS categoria,
|
||||
COUNT(*) AS numeroRichieste,
|
||||
SUM(r.importo) AS totaleRichiesto,
|
||||
SUM(CASE WHEN r.stato IN ('APPROVATA', 'LIQUIDATA') THEN r.importo ELSE 0 END) AS totaleApprovato,
|
||||
SUM(CASE WHEN r.stato = 'LIQUIDATA' THEN r.importo ELSE 0 END) AS totaleLiquidato
|
||||
FROM richieste_rimborso r
|
||||
JOIN categorie_spesa c ON c.categoria_id = r.categoria_id
|
||||
${where}
|
||||
GROUP BY DATE_FORMAT(r.data_spesa, '%Y-%m'), c.descrizione
|
||||
ORDER BY mese DESC, categoria`,
|
||||
params
|
||||
);
|
||||
|
||||
return rows.map((row) => ({
|
||||
mese: row.mese,
|
||||
categoria: row.categoria,
|
||||
numeroRichieste: Number(row.numeroRichieste),
|
||||
totaleRichiesto: Number(row.totaleRichiesto),
|
||||
totaleApprovato: Number(row.totaleApprovato),
|
||||
totaleLiquidato: Number(row.totaleLiquidato),
|
||||
}));
|
||||
}
|
||||
54
src/services/utentiService.ts
Normal file
54
src/services/utentiService.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import bcrypt from "bcryptjs";
|
||||
import type { ResultSetHeader, RowDataPacket } from "mysql2";
|
||||
import pool from "../db/pool.js";
|
||||
import type { Ruolo, Utente } from "../types/index.js";
|
||||
|
||||
export async function findByEmail(email: string): Promise<Utente | null> {
|
||||
const [rows] = await pool.execute<RowDataPacket[]>(
|
||||
"SELECT * FROM utenti WHERE email = ?",
|
||||
[email.toLowerCase().trim()]
|
||||
);
|
||||
return (rows[0] as Utente) ?? null;
|
||||
}
|
||||
|
||||
export async function createUtente(data: {
|
||||
nome: string;
|
||||
cognome: string;
|
||||
email: string;
|
||||
password: string;
|
||||
ruolo: Ruolo;
|
||||
}): Promise<number> {
|
||||
const passwordHash = await bcrypt.hash(data.password, 10);
|
||||
const [result] = await pool.execute<ResultSetHeader>(
|
||||
`INSERT INTO utenti (nome, cognome, email, password_hash, ruolo)
|
||||
VALUES (?, ?, ?, ?, ?)`,
|
||||
[
|
||||
data.nome.trim(),
|
||||
data.cognome.trim(),
|
||||
data.email.toLowerCase().trim(),
|
||||
passwordHash,
|
||||
data.ruolo,
|
||||
]
|
||||
);
|
||||
return result.insertId;
|
||||
}
|
||||
|
||||
export function validateRegistration(body: Record<string, unknown>): string | null {
|
||||
const nome = body.nome?.toString().trim();
|
||||
const cognome = body.cognome?.toString().trim();
|
||||
const email = body.email?.toString().trim().toLowerCase();
|
||||
const password = body.password?.toString();
|
||||
const confermaPassword = body.confermaPassword?.toString();
|
||||
const ruolo = body.ruolo?.toString();
|
||||
|
||||
if (!nome) return "Il nome è obbligatorio";
|
||||
if (!cognome) return "Il cognome è obbligatorio";
|
||||
if (!email) return "L'email è obbligatoria";
|
||||
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) return "Formato email non valido";
|
||||
if (!password) return "La password è obbligatoria";
|
||||
if (password !== confermaPassword) return "Le password non coincidono";
|
||||
if (ruolo && ruolo !== "DIPENDENTE" && ruolo !== "RESPONSABILE_AMMINISTRATIVO") {
|
||||
return "Ruolo non valido";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
22
src/types/auth.d.ts
vendored
Normal file
22
src/types/auth.d.ts
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import type { Ruolo } from "./index.js";
|
||||
|
||||
declare module "@auth/core/types" {
|
||||
interface User {
|
||||
ruolo?: Ruolo;
|
||||
}
|
||||
interface Session {
|
||||
user: {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
ruolo: Ruolo;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
declare module "@auth/core/jwt" {
|
||||
interface JWT {
|
||||
id?: string;
|
||||
ruolo?: Ruolo;
|
||||
}
|
||||
}
|
||||
57
src/types/index.ts
Normal file
57
src/types/index.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
export type Ruolo = "DIPENDENTE" | "RESPONSABILE_AMMINISTRATIVO";
|
||||
export type StatoRichiesta = "IN_ATTESA" | "APPROVATA" | "RIFIUTATA" | "LIQUIDATA";
|
||||
|
||||
export interface Utente {
|
||||
utente_id: number;
|
||||
nome: string;
|
||||
cognome: string;
|
||||
email: string;
|
||||
password_hash: string;
|
||||
ruolo: Ruolo;
|
||||
}
|
||||
|
||||
export interface UtentePublic {
|
||||
id: number;
|
||||
nome: string;
|
||||
cognome: string;
|
||||
email: string;
|
||||
ruolo: Ruolo;
|
||||
}
|
||||
|
||||
export interface CategoriaSpesa {
|
||||
categoria_id: number;
|
||||
descrizione: string;
|
||||
}
|
||||
|
||||
export interface RichiestaRimborso {
|
||||
richiesta_id: number;
|
||||
data_inserimento: Date;
|
||||
data_spesa: Date;
|
||||
categoria_id: number;
|
||||
importo: number;
|
||||
descrizione: string;
|
||||
riferimento_giustificativo: string | null;
|
||||
stato: StatoRichiesta;
|
||||
dipendente_id: number;
|
||||
data_valutazione: Date | null;
|
||||
responsabile_valutazione_id: number | null;
|
||||
motivazione_rifiuto: string | null;
|
||||
data_liquidazione: Date | null;
|
||||
}
|
||||
|
||||
export interface RichiestaRimborsoDettaglio extends RichiestaRimborso {
|
||||
categoria_descrizione: string;
|
||||
dipendente_nome: string;
|
||||
dipendente_cognome: string;
|
||||
responsabile_nome: string | null;
|
||||
responsabile_cognome: string | null;
|
||||
}
|
||||
|
||||
export interface StatisticaRimborso {
|
||||
mese: string;
|
||||
categoria: string;
|
||||
numeroRichieste: number;
|
||||
totaleRichiesto: number;
|
||||
totaleApprovato: number;
|
||||
totaleLiquidato: number;
|
||||
}
|
||||
24
src/utils/format.ts
Normal file
24
src/utils/format.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import type { RichiestaRimborsoDettaglio } from "../types/index.js";
|
||||
|
||||
export function formatRimborso(r: RichiestaRimborsoDettaglio) {
|
||||
return {
|
||||
id: r.richiesta_id,
|
||||
dataInserimento: r.data_inserimento,
|
||||
dataSpesa: r.data_spesa,
|
||||
categoriaId: r.categoria_id,
|
||||
categoria: r.categoria_descrizione,
|
||||
importo: r.importo,
|
||||
descrizione: r.descrizione,
|
||||
riferimentoGiustificativo: r.riferimento_giustificativo,
|
||||
stato: r.stato,
|
||||
dipendenteId: r.dipendente_id,
|
||||
dipendenteNome: `${r.dipendente_nome} ${r.dipendente_cognome}`,
|
||||
dataValutazione: r.data_valutazione,
|
||||
responsabileValutazioneId: r.responsabile_valutazione_id,
|
||||
responsabileNome: r.responsabile_nome
|
||||
? `${r.responsabile_nome} ${r.responsabile_cognome}`
|
||||
: null,
|
||||
motivazioneRifiuto: r.motivazione_rifiuto,
|
||||
dataLiquidazione: r.data_liquidazione,
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue