From 59e73435984088339493f366931d7e26a25f28ca Mon Sep 17 00:00:00 2001 From: AV77web Date: Tue, 9 Jun 2026 16:28:25 +0200 Subject: [PATCH] messaggi di conferma su rimborsi, messaggio per accesso neagato e altro --- README.md | 104 +++++++++++++----------------- src/App.css | 7 ++ src/App.tsx | 7 +- src/components/FiltriRimborsi.tsx | 25 +++---- src/components/GuestRoute.tsx | 11 ++++ src/components/ProtectedRoute.tsx | 10 ++- src/pages/Dashboard.tsx | 15 ++++- src/pages/Register.tsx | 6 ++ src/pages/RimborsiList.tsx | 38 ++++++++--- src/pages/RimborsoDetail.tsx | 21 +++++- src/pages/RimborsoForm.tsx | 48 ++++++++++---- src/pages/Statistiche.tsx | 4 +- 12 files changed, 199 insertions(+), 97 deletions(-) create mode 100644 src/components/GuestRoute.tsx diff --git a/README.md b/README.md index 7dbf7eb..587b11b 100644 --- a/README.md +++ b/README.md @@ -1,73 +1,61 @@ -# React + TypeScript + Vite +# Frontend – Gestione Rimborsi Spese Aziendali (Prova S5) -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. +Applicazione web CSR (React + Vite + TypeScript) per la gestione dei rimborsi spese. -Currently, two official plugins are available: +## URL produzione -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) +| Risorsa | URL | +|---------|-----| +| Applicazione | https://provapraticafrontend.andreavillari.it | +| API backend | https://provapraticabackend.andreavillari.it | +| Documentazione API (Swagger) | https://provapraticabackend.andreavillari.it/api/docs | -## React Compiler +## Credenziali di test -The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). +Password comune: `Password123!` -## Expanding the ESLint configuration +| Ruolo | Email | +|-------|-------| +| Responsabile amministrativo | `admin@azienda.it` | +| Dipendente | `mario.rossi@azienda.it` | +| Dipendente | `marco.bianchi@azienda.it` | +| Dipendente | `anna.verdi@azienda.it` | -If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: +Dalla pagina **Registrati** è possibile creare nuovi account scegliendo il ruolo. -```js -export default defineConfig([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - // Other configs... +## Funzionalità - // Remove tseslint.configs.recommended and replace with this - tseslint.configs.recommendedTypeChecked, - // Alternatively, use this for stricter rules - tseslint.configs.strictTypeChecked, - // Optionally, add this for stylistic rules - tseslint.configs.stylisticTypeChecked, +- Login / registrazione / logout +- Dashboard differenziata per ruolo +- Dipendente: creazione, elenco, filtri, modifica ed eliminazione richieste in attesa +- Responsabile: gestione di tutte le richieste, approvazione, rifiuto, liquidazione +- Statistiche aggregate per mese e categoria - // Other configs... - ], - languageOptions: { - parserOptions: { - project: ['./tsconfig.node.json', './tsconfig.app.json'], - tsconfigRootDir: import.meta.dirname, - }, - // other options... - }, - }, -]) +## Avvio locale + +Configurare il file `.env` (`VITE_API_URL` vuoto in sviluppo per usare il proxy Vite verso il backend locale). + +```bash +npm install +npm run dev ``` -You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: +Con `VITE_API_URL` vuoto, Vite inoltra le chiamate `/api` al backend su `http://localhost:3003`. -```js -// eslint.config.js -import reactX from 'eslint-plugin-react-x' -import reactDom from 'eslint-plugin-react-dom' +Build di produzione: -export default defineConfig([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - // Other configs... - // Enable lint rules for React - reactX.configs['recommended-typescript'], - // Enable lint rules for React DOM - reactDom.configs.recommended, - ], - languageOptions: { - parserOptions: { - project: ['./tsconfig.node.json', './tsconfig.app.json'], - tsconfigRootDir: import.meta.dirname, - }, - // other options... - }, - }, -]) +```bash +VITE_API_URL=https://provapraticabackend.andreavillari.it npm run build ``` + +Con Docker: + +```bash +docker compose up -d --build +``` + +L'app è servita da nginx sulla porta `3002`. + +## Deploy + +Push su `main` → Forgejo Actions → SSH sul VPS → `docker compose up -d --build` in `/root/provapraticafrontend`. diff --git a/src/App.css b/src/App.css index dde569d..18997d6 100644 --- a/src/App.css +++ b/src/App.css @@ -219,6 +219,13 @@ input:focus, select:focus, textarea:focus { color: #2563eb; } +.table .cell-desc { + max-width: 220px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + .filters { display: flex; flex-wrap: wrap; diff --git a/src/App.tsx b/src/App.tsx index 6ceb9dc..ac0f115 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,5 @@ import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom"; +import GuestRoute from "./components/GuestRoute"; import Layout from "./components/Layout"; import ProtectedRoute from "./components/ProtectedRoute"; import { AuthProvider } from "./context/AuthContext"; @@ -17,9 +18,9 @@ export default function App() { - } /> - } /> - } /> + } /> + } /> + } /> }> } /> } /> diff --git a/src/components/FiltriRimborsi.tsx b/src/components/FiltriRimborsi.tsx index 7685ecc..15e4041 100644 --- a/src/components/FiltriRimborsi.tsx +++ b/src/components/FiltriRimborsi.tsx @@ -7,20 +7,23 @@ interface Props { categorie: CategoriaSpesa[]; dipendenti?: Pick[]; showDipendente?: boolean; + showStato?: boolean; } -export default function FiltriRimborsi({ filtri, onChange, categorie, dipendenti, showDipendente }: Props) { +export default function FiltriRimborsi({ filtri, onChange, categorie, dipendenti, showDipendente, showStato = true }: Props) { return (
- + {showStato && ( + + )} diff --git a/src/components/GuestRoute.tsx b/src/components/GuestRoute.tsx new file mode 100644 index 0000000..87de3db --- /dev/null +++ b/src/components/GuestRoute.tsx @@ -0,0 +1,11 @@ +import { Navigate } from "react-router-dom"; +import { useAuth } from "../context/AuthContext"; + +export default function GuestRoute({ children }: { children: React.ReactNode }) { + const { user, loading } = useAuth(); + + if (loading) return

Caricamento...

; + if (user) return ; + + return <>{children}; +} diff --git a/src/components/ProtectedRoute.tsx b/src/components/ProtectedRoute.tsx index 87eb323..a1971be 100644 --- a/src/components/ProtectedRoute.tsx +++ b/src/components/ProtectedRoute.tsx @@ -13,7 +13,15 @@ export default function ProtectedRoute({ if (loading) return

Caricamento...

; if (!user) return ; - if (roles && !roles.includes(user.ruolo)) return ; + if (roles && !roles.includes(user.ruolo)) { + return ( + + ); + } return <>{children}; } diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index 1a1ca1e..078b3db 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -1,13 +1,26 @@ -import { Link } from "react-router-dom"; +import { useEffect, useState } from "react"; +import { Link, useLocation, useNavigate } from "react-router-dom"; import { useAuth } from "../context/AuthContext"; export default function Dashboard() { const { user } = useAuth(); + const location = useLocation(); + const navigate = useNavigate(); const isAdmin = user?.ruolo === "RESPONSABILE_AMMINISTRATIVO"; + const [deniedMessage, setDeniedMessage] = useState(null); + + useEffect(() => { + const denied = (location.state as { denied?: string } | null)?.denied; + if (denied) { + setDeniedMessage(denied); + navigate(location.pathname, { replace: true, state: {} }); + } + }, [location, navigate]); return (

Dashboard

+ {deniedMessage &&

{deniedMessage}

}

Benvenuto, {user?.nome} {user?.cognome}

Ruolo: {isAdmin ? "Responsabile amministrativo" : "Dipendente"}

diff --git a/src/pages/Register.tsx b/src/pages/Register.tsx index dba3ad6..749af2b 100644 --- a/src/pages/Register.tsx +++ b/src/pages/Register.tsx @@ -21,6 +21,12 @@ export default function Register() { e.preventDefault(); setError(null); setSuccess(null); + + if (form.password !== form.confermaPassword) { + setError("Le password non coincidono"); + return; + } + setLoading(true); try { await register(form); diff --git a/src/pages/RimborsiList.tsx b/src/pages/RimborsiList.tsx index fffc84b..78e384d 100644 --- a/src/pages/RimborsiList.tsx +++ b/src/pages/RimborsiList.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from "react"; -import { Link } from "react-router-dom"; +import { Link, useLocation, useNavigate } from "react-router-dom"; import { getCategorie, getDipendenti, getRimborsi } from "../api/client"; import FiltriRimborsi from "../components/FiltriRimborsi"; import StatoBadge from "../components/StatoBadge"; @@ -16,22 +16,37 @@ function formatEuro(n: number) { export default function RimborsiList() { const { user } = useAuth(); + const location = useLocation(); + const navigate = useNavigate(); const isAdmin = user?.ruolo === "RESPONSABILE_AMMINISTRATIVO"; const [rimborsi, setRimborsi] = useState([]); const [categorie, setCategorie] = useState([]); const [dipendenti, setDipendenti] = useState[]>([]); const [filtri, setFiltri] = useState({ stato: "", categoriaId: "", mese: "", dipendenteId: "" }); const [error, setError] = useState(null); + const [success, setSuccess] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { - Promise.all([ - getCategorie(), - isAdmin ? getDipendenti() : Promise.resolve([]), - ]).then(([cats, dips]) => { - setCategorie(cats); - setDipendenti(dips); - }); + const flash = (location.state as { flash?: string } | null)?.flash; + if (flash) { + setSuccess(flash); + navigate(location.pathname, { replace: true, state: {} }); + } + }, [location, navigate]); + + useEffect(() => { + if (!isAdmin) { + setDipendenti([]); + getCategorie().then(setCategorie).catch(() => {}); + return; + } + Promise.all([getCategorie(), getDipendenti()]) + .then(([cats, dips]) => { + setCategorie(cats); + setDipendenti(dips); + }) + .catch((err) => setError(err instanceof Error ? err.message : "Errore caricamento filtri")); }, [isAdmin]); useEffect(() => { @@ -63,6 +78,7 @@ export default function RimborsiList() { showDipendente={isAdmin} /> + {success &&

{success}

} {error &&

{error}

} {loading ? (

Caricamento...

@@ -75,8 +91,11 @@ export default function RimborsiList() { Data spesa Categoria {isAdmin && Dipendente} + Descrizione Importo Stato + Valutazione + Liquidazione @@ -86,8 +105,11 @@ export default function RimborsiList() { {formatDate(r.dataSpesa)} {r.categoria} {isAdmin && {r.dipendenteNome}} + {r.descrizione} {formatEuro(r.importo)} + {r.dataValutazione ? formatDate(r.dataValutazione) : "—"} + {r.dataLiquidazione ? formatDate(r.dataLiquidazione) : "—"} Dettaglio ))} diff --git a/src/pages/RimborsoDetail.tsx b/src/pages/RimborsoDetail.tsx index e75a98a..8d85771 100644 --- a/src/pages/RimborsoDetail.tsx +++ b/src/pages/RimborsoDetail.tsx @@ -33,11 +33,18 @@ export default function RimborsoDetail() { const [error, setError] = useState(null); const [message, setMessage] = useState(null); const [loading, setLoading] = useState(false); + const [initialLoading, setInitialLoading] = useState(true); const load = () => { + setInitialLoading(true); + setError(null); getRimborso(Number(id)) .then(setRimborso) - .catch((err) => setError(err.message)); + .catch((err) => { + setRimborso(null); + setError(err instanceof Error ? err.message : "Errore nel caricamento"); + }) + .finally(() => setInitialLoading(false)); }; useEffect(() => { load(); }, [id]); @@ -62,14 +69,22 @@ export default function RimborsoDetail() { setLoading(true); try { await deleteRimborso(Number(id)); - navigate("/rimborsi"); + navigate("/rimborsi", { state: { flash: "Richiesta eliminata con successo." } }); } catch (err) { setError(err instanceof Error ? err.message : "Eliminazione fallita"); setLoading(false); } }; - if (!rimborso) return

Caricamento...

; + if (initialLoading) return

Caricamento...

; + if (!rimborso) { + return ( +
+

{error ?? "Richiesta non trovata"}

+ Torna all'elenco +
+ ); + } const canEdit = !isAdmin && rimborso.stato === "IN_ATTESA" && rimborso.dipendenteId === user?.id; diff --git a/src/pages/RimborsoForm.tsx b/src/pages/RimborsoForm.tsx index 423343c..7b43a46 100644 --- a/src/pages/RimborsoForm.tsx +++ b/src/pages/RimborsoForm.tsx @@ -1,11 +1,13 @@ import { useEffect, useState, type FormEvent } from "react"; import { Link, useNavigate, useParams } from "react-router-dom"; import { createRimborso, getCategorie, getRimborso, updateRimborso } from "../api/client"; +import { useAuth } from "../context/AuthContext"; export default function RimborsoForm() { const { id } = useParams(); const isEdit = Boolean(id); const navigate = useNavigate(); + const { user } = useAuth(); const [categorie, setCategorie] = useState<{ id: number; descrizione: string }[]>([]); const [form, setForm] = useState({ dataSpesa: "", @@ -16,6 +18,7 @@ export default function RimborsoForm() { }); const [error, setError] = useState(null); const [loading, setLoading] = useState(false); + const [initialLoading, setInitialLoading] = useState(isEdit); useEffect(() => { getCategorie().then((cats) => { @@ -26,16 +29,27 @@ export default function RimborsoForm() { useEffect(() => { if (!id) return; - getRimborso(Number(id)).then((r) => { - setForm({ - dataSpesa: r.dataSpesa.slice(0, 10), - categoriaId: String(r.categoriaId), - importo: String(r.importo), - descrizione: r.descrizione, - riferimentoGiustificativo: r.riferimentoGiustificativo ?? "", - }); - }).catch((err) => setError(err.message)); - }, [id]); + getRimborso(Number(id)) + .then((r) => { + if (r.stato !== "IN_ATTESA") { + setError("La richiesta non può essere modificata nello stato corrente"); + return; + } + if (r.dipendenteId !== user?.id) { + setError("Non puoi modificare questa richiesta"); + return; + } + setForm({ + dataSpesa: r.dataSpesa.slice(0, 10), + categoriaId: String(r.categoriaId), + importo: String(r.importo), + descrizione: r.descrizione, + riferimentoGiustificativo: r.riferimentoGiustificativo ?? "", + }); + }) + .catch((err) => setError(err instanceof Error ? err.message : "Errore nel caricamento")) + .finally(() => setInitialLoading(false)); + }, [id, user?.id]); const handleSubmit = async (e: FormEvent) => { e.preventDefault(); @@ -51,10 +65,11 @@ export default function RimborsoForm() { try { if (isEdit) { await updateRimborso(Number(id), payload); + navigate("/rimborsi", { state: { flash: "Richiesta aggiornata con successo." } }); } else { await createRimborso(payload); + navigate("/rimborsi", { state: { flash: "Richiesta creata con successo." } }); } - navigate("/rimborsi"); } catch (err) { setError(err instanceof Error ? err.message : "Errore nel salvataggio"); } finally { @@ -62,9 +77,19 @@ export default function RimborsoForm() { } }; + if (initialLoading) return

Caricamento...

; + + const blocked = isEdit && error && !form.dataSpesa; + return (

{isEdit ? "Modifica richiesta" : "Nuova richiesta di rimborso"}

+ {blocked ? ( +
+

{error}

+ Torna al dettaglio +
+ ) : (
{error &&

{error}

}
+ )}
); } diff --git a/src/pages/Statistiche.tsx b/src/pages/Statistiche.tsx index 099e46a..1693233 100644 --- a/src/pages/Statistiche.tsx +++ b/src/pages/Statistiche.tsx @@ -17,7 +17,8 @@ export default function Statistiche() { useEffect(() => { Promise.all([getCategorie(), getDipendenti()]) - .then(([cats, dips]) => { setCategorie(cats); setDipendenti(dips); }); + .then(([cats, dips]) => { setCategorie(cats); setDipendenti(dips); }) + .catch((err) => setError(err instanceof Error ? err.message : "Errore caricamento filtri")); }, []); useEffect(() => { @@ -44,6 +45,7 @@ export default function Statistiche() { categorie={categorie} dipendenti={dipendenti} showDipendente + showStato={false} /> {error &&

{error}

}