messaggi di conferma su rimborsi, messaggio per accesso neagato e altro
This commit is contained in:
parent
4ff685557f
commit
59e7343598
12 changed files with 199 additions and 97 deletions
104
README.md
104
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)
|
| Risorsa | URL |
|
||||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
|
|---------|-----|
|
||||||
|
| 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
|
## Funzionalità
|
||||||
export default defineConfig([
|
|
||||||
globalIgnores(['dist']),
|
|
||||||
{
|
|
||||||
files: ['**/*.{ts,tsx}'],
|
|
||||||
extends: [
|
|
||||||
// Other configs...
|
|
||||||
|
|
||||||
// Remove tseslint.configs.recommended and replace with this
|
- Login / registrazione / logout
|
||||||
tseslint.configs.recommendedTypeChecked,
|
- Dashboard differenziata per ruolo
|
||||||
// Alternatively, use this for stricter rules
|
- Dipendente: creazione, elenco, filtri, modifica ed eliminazione richieste in attesa
|
||||||
tseslint.configs.strictTypeChecked,
|
- Responsabile: gestione di tutte le richieste, approvazione, rifiuto, liquidazione
|
||||||
// Optionally, add this for stylistic rules
|
- Statistiche aggregate per mese e categoria
|
||||||
tseslint.configs.stylisticTypeChecked,
|
|
||||||
|
|
||||||
// Other configs...
|
## Avvio locale
|
||||||
],
|
|
||||||
languageOptions: {
|
Configurare il file `.env` (`VITE_API_URL` vuoto in sviluppo per usare il proxy Vite verso il backend locale).
|
||||||
parserOptions: {
|
|
||||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
```bash
|
||||||
tsconfigRootDir: import.meta.dirname,
|
npm install
|
||||||
},
|
npm run dev
|
||||||
// other options...
|
|
||||||
},
|
|
||||||
},
|
|
||||||
])
|
|
||||||
```
|
```
|
||||||
|
|
||||||
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
|
Build di produzione:
|
||||||
// eslint.config.js
|
|
||||||
import reactX from 'eslint-plugin-react-x'
|
|
||||||
import reactDom from 'eslint-plugin-react-dom'
|
|
||||||
|
|
||||||
export default defineConfig([
|
```bash
|
||||||
globalIgnores(['dist']),
|
VITE_API_URL=https://provapraticabackend.andreavillari.it npm run build
|
||||||
{
|
|
||||||
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...
|
|
||||||
},
|
|
||||||
},
|
|
||||||
])
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
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`.
|
||||||
|
|
|
||||||
|
|
@ -219,6 +219,13 @@ input:focus, select:focus, textarea:focus {
|
||||||
color: #2563eb;
|
color: #2563eb;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.table .cell-desc {
|
||||||
|
max-width: 220px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
.filters {
|
.filters {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom";
|
import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom";
|
||||||
|
import GuestRoute from "./components/GuestRoute";
|
||||||
import Layout from "./components/Layout";
|
import Layout from "./components/Layout";
|
||||||
import ProtectedRoute from "./components/ProtectedRoute";
|
import ProtectedRoute from "./components/ProtectedRoute";
|
||||||
import { AuthProvider } from "./context/AuthContext";
|
import { AuthProvider } from "./context/AuthContext";
|
||||||
|
|
@ -17,9 +18,9 @@ export default function App() {
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Home />} />
|
<Route path="/" element={<GuestRoute><Home /></GuestRoute>} />
|
||||||
<Route path="/login" element={<Login />} />
|
<Route path="/login" element={<GuestRoute><Login /></GuestRoute>} />
|
||||||
<Route path="/register" element={<Register />} />
|
<Route path="/register" element={<GuestRoute><Register /></GuestRoute>} />
|
||||||
<Route element={<ProtectedRoute><Layout /></ProtectedRoute>}>
|
<Route element={<ProtectedRoute><Layout /></ProtectedRoute>}>
|
||||||
<Route path="/dashboard" element={<Dashboard />} />
|
<Route path="/dashboard" element={<Dashboard />} />
|
||||||
<Route path="/rimborsi" element={<RimborsiList />} />
|
<Route path="/rimborsi" element={<RimborsiList />} />
|
||||||
|
|
|
||||||
|
|
@ -7,20 +7,23 @@ interface Props {
|
||||||
categorie: CategoriaSpesa[];
|
categorie: CategoriaSpesa[];
|
||||||
dipendenti?: Pick<Utente, "id" | "nome" | "cognome">[];
|
dipendenti?: Pick<Utente, "id" | "nome" | "cognome">[];
|
||||||
showDipendente?: boolean;
|
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 (
|
return (
|
||||||
<div className="filters">
|
<div className="filters">
|
||||||
<label>
|
{showStato && (
|
||||||
Stato
|
<label>
|
||||||
<select value={filtri.stato} onChange={(e) => onChange({ ...filtri, stato: e.target.value })}>
|
Stato
|
||||||
<option value="">Tutti</option>
|
<select value={filtri.stato} onChange={(e) => onChange({ ...filtri, stato: e.target.value })}>
|
||||||
{(Object.keys(STATO_LABELS) as StatoRichiesta[]).map((s) => (
|
<option value="">Tutti</option>
|
||||||
<option key={s} value={s}>{STATO_LABELS[s]}</option>
|
{(Object.keys(STATO_LABELS) as StatoRichiesta[]).map((s) => (
|
||||||
))}
|
<option key={s} value={s}>{STATO_LABELS[s]}</option>
|
||||||
</select>
|
))}
|
||||||
</label>
|
</select>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
<label>
|
<label>
|
||||||
Categoria
|
Categoria
|
||||||
<select value={filtri.categoriaId} onChange={(e) => onChange({ ...filtri, categoriaId: e.target.value })}>
|
<select value={filtri.categoriaId} onChange={(e) => onChange({ ...filtri, categoriaId: e.target.value })}>
|
||||||
|
|
@ -40,7 +43,7 @@ export default function FiltriRimborsi({ filtri, onChange, categorie, dipendenti
|
||||||
<select value={filtri.dipendenteId} onChange={(e) => onChange({ ...filtri, dipendenteId: e.target.value })}>
|
<select value={filtri.dipendenteId} onChange={(e) => onChange({ ...filtri, dipendenteId: e.target.value })}>
|
||||||
<option value="">Tutti</option>
|
<option value="">Tutti</option>
|
||||||
{dipendenti.map((d) => (
|
{dipendenti.map((d) => (
|
||||||
<option key={d.id} value={d.id}>{d.cognome} {d.nome}</option>
|
<option key={d.id} value={String(d.id)}>{d.cognome} {d.nome}</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
|
|
||||||
11
src/components/GuestRoute.tsx
Normal file
11
src/components/GuestRoute.tsx
Normal file
|
|
@ -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 <p className="loading">Caricamento...</p>;
|
||||||
|
if (user) return <Navigate to="/dashboard" replace />;
|
||||||
|
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
||||||
|
|
@ -13,7 +13,15 @@ export default function ProtectedRoute({
|
||||||
|
|
||||||
if (loading) return <p className="loading">Caricamento...</p>;
|
if (loading) return <p className="loading">Caricamento...</p>;
|
||||||
if (!user) return <Navigate to="/login" replace />;
|
if (!user) return <Navigate to="/login" replace />;
|
||||||
if (roles && !roles.includes(user.ruolo)) return <Navigate to="/dashboard" replace />;
|
if (roles && !roles.includes(user.ruolo)) {
|
||||||
|
return (
|
||||||
|
<Navigate
|
||||||
|
to="/dashboard"
|
||||||
|
replace
|
||||||
|
state={{ denied: "Non hai i permessi per accedere a questa sezione." }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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";
|
import { useAuth } from "../context/AuthContext";
|
||||||
|
|
||||||
export default function Dashboard() {
|
export default function Dashboard() {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
const location = useLocation();
|
||||||
|
const navigate = useNavigate();
|
||||||
const isAdmin = user?.ruolo === "RESPONSABILE_AMMINISTRATIVO";
|
const isAdmin = user?.ruolo === "RESPONSABILE_AMMINISTRATIVO";
|
||||||
|
const [deniedMessage, setDeniedMessage] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const denied = (location.state as { denied?: string } | null)?.denied;
|
||||||
|
if (denied) {
|
||||||
|
setDeniedMessage(denied);
|
||||||
|
navigate(location.pathname, { replace: true, state: {} });
|
||||||
|
}
|
||||||
|
}, [location, navigate]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page">
|
<div className="page">
|
||||||
<h1>Dashboard</h1>
|
<h1>Dashboard</h1>
|
||||||
|
{deniedMessage && <p className="error">{deniedMessage}</p>}
|
||||||
<p>Benvenuto, <strong>{user?.nome} {user?.cognome}</strong></p>
|
<p>Benvenuto, <strong>{user?.nome} {user?.cognome}</strong></p>
|
||||||
<p className="muted">Ruolo: {isAdmin ? "Responsabile amministrativo" : "Dipendente"}</p>
|
<p className="muted">Ruolo: {isAdmin ? "Responsabile amministrativo" : "Dipendente"}</p>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,12 @@ export default function Register() {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setError(null);
|
setError(null);
|
||||||
setSuccess(null);
|
setSuccess(null);
|
||||||
|
|
||||||
|
if (form.password !== form.confermaPassword) {
|
||||||
|
setError("Le password non coincidono");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
await register(form);
|
await register(form);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useEffect, useState } from "react";
|
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 { getCategorie, getDipendenti, getRimborsi } from "../api/client";
|
||||||
import FiltriRimborsi from "../components/FiltriRimborsi";
|
import FiltriRimborsi from "../components/FiltriRimborsi";
|
||||||
import StatoBadge from "../components/StatoBadge";
|
import StatoBadge from "../components/StatoBadge";
|
||||||
|
|
@ -16,22 +16,37 @@ function formatEuro(n: number) {
|
||||||
|
|
||||||
export default function RimborsiList() {
|
export default function RimborsiList() {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
const location = useLocation();
|
||||||
|
const navigate = useNavigate();
|
||||||
const isAdmin = user?.ruolo === "RESPONSABILE_AMMINISTRATIVO";
|
const isAdmin = user?.ruolo === "RESPONSABILE_AMMINISTRATIVO";
|
||||||
const [rimborsi, setRimborsi] = useState<RichiestaRimborso[]>([]);
|
const [rimborsi, setRimborsi] = useState<RichiestaRimborso[]>([]);
|
||||||
const [categorie, setCategorie] = useState<CategoriaSpesa[]>([]);
|
const [categorie, setCategorie] = useState<CategoriaSpesa[]>([]);
|
||||||
const [dipendenti, setDipendenti] = useState<Pick<Utente, "id" | "nome" | "cognome">[]>([]);
|
const [dipendenti, setDipendenti] = useState<Pick<Utente, "id" | "nome" | "cognome">[]>([]);
|
||||||
const [filtri, setFiltri] = useState({ stato: "", categoriaId: "", mese: "", dipendenteId: "" });
|
const [filtri, setFiltri] = useState({ stato: "", categoriaId: "", mese: "", dipendenteId: "" });
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [success, setSuccess] = useState<string | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Promise.all([
|
const flash = (location.state as { flash?: string } | null)?.flash;
|
||||||
getCategorie(),
|
if (flash) {
|
||||||
isAdmin ? getDipendenti() : Promise.resolve([]),
|
setSuccess(flash);
|
||||||
]).then(([cats, dips]) => {
|
navigate(location.pathname, { replace: true, state: {} });
|
||||||
setCategorie(cats);
|
}
|
||||||
setDipendenti(dips);
|
}, [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]);
|
}, [isAdmin]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -63,6 +78,7 @@ export default function RimborsiList() {
|
||||||
showDipendente={isAdmin}
|
showDipendente={isAdmin}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{success && <p className="success">{success}</p>}
|
||||||
{error && <p className="error">{error}</p>}
|
{error && <p className="error">{error}</p>}
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<p className="loading">Caricamento...</p>
|
<p className="loading">Caricamento...</p>
|
||||||
|
|
@ -75,8 +91,11 @@ export default function RimborsiList() {
|
||||||
<th>Data spesa</th>
|
<th>Data spesa</th>
|
||||||
<th>Categoria</th>
|
<th>Categoria</th>
|
||||||
{isAdmin && <th>Dipendente</th>}
|
{isAdmin && <th>Dipendente</th>}
|
||||||
|
<th>Descrizione</th>
|
||||||
<th>Importo</th>
|
<th>Importo</th>
|
||||||
<th>Stato</th>
|
<th>Stato</th>
|
||||||
|
<th>Valutazione</th>
|
||||||
|
<th>Liquidazione</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
@ -86,8 +105,11 @@ export default function RimborsiList() {
|
||||||
<td>{formatDate(r.dataSpesa)}</td>
|
<td>{formatDate(r.dataSpesa)}</td>
|
||||||
<td>{r.categoria}</td>
|
<td>{r.categoria}</td>
|
||||||
{isAdmin && <td>{r.dipendenteNome}</td>}
|
{isAdmin && <td>{r.dipendenteNome}</td>}
|
||||||
|
<td className="cell-desc">{r.descrizione}</td>
|
||||||
<td>{formatEuro(r.importo)}</td>
|
<td>{formatEuro(r.importo)}</td>
|
||||||
<td><StatoBadge stato={r.stato} /></td>
|
<td><StatoBadge stato={r.stato} /></td>
|
||||||
|
<td>{r.dataValutazione ? formatDate(r.dataValutazione) : "—"}</td>
|
||||||
|
<td>{r.dataLiquidazione ? formatDate(r.dataLiquidazione) : "—"}</td>
|
||||||
<td><Link to={`/rimborsi/${r.id}`}>Dettaglio</Link></td>
|
<td><Link to={`/rimborsi/${r.id}`}>Dettaglio</Link></td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -33,11 +33,18 @@ export default function RimborsoDetail() {
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [message, setMessage] = useState<string | null>(null);
|
const [message, setMessage] = useState<string | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [initialLoading, setInitialLoading] = useState(true);
|
||||||
|
|
||||||
const load = () => {
|
const load = () => {
|
||||||
|
setInitialLoading(true);
|
||||||
|
setError(null);
|
||||||
getRimborso(Number(id))
|
getRimborso(Number(id))
|
||||||
.then(setRimborso)
|
.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]);
|
useEffect(() => { load(); }, [id]);
|
||||||
|
|
@ -62,14 +69,22 @@ export default function RimborsoDetail() {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
await deleteRimborso(Number(id));
|
await deleteRimborso(Number(id));
|
||||||
navigate("/rimborsi");
|
navigate("/rimborsi", { state: { flash: "Richiesta eliminata con successo." } });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : "Eliminazione fallita");
|
setError(err instanceof Error ? err.message : "Eliminazione fallita");
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!rimborso) return <p className="loading">Caricamento...</p>;
|
if (initialLoading) return <p className="loading">Caricamento...</p>;
|
||||||
|
if (!rimborso) {
|
||||||
|
return (
|
||||||
|
<div className="page">
|
||||||
|
<p className="error">{error ?? "Richiesta non trovata"}</p>
|
||||||
|
<Link to="/rimborsi" className="btn btn-secondary">Torna all'elenco</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const canEdit = !isAdmin && rimborso.stato === "IN_ATTESA" && rimborso.dipendenteId === user?.id;
|
const canEdit = !isAdmin && rimborso.stato === "IN_ATTESA" && rimborso.dipendenteId === user?.id;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
import { useEffect, useState, type FormEvent } from "react";
|
import { useEffect, useState, type FormEvent } from "react";
|
||||||
import { Link, useNavigate, useParams } from "react-router-dom";
|
import { Link, useNavigate, useParams } from "react-router-dom";
|
||||||
import { createRimborso, getCategorie, getRimborso, updateRimborso } from "../api/client";
|
import { createRimborso, getCategorie, getRimborso, updateRimborso } from "../api/client";
|
||||||
|
import { useAuth } from "../context/AuthContext";
|
||||||
|
|
||||||
export default function RimborsoForm() {
|
export default function RimborsoForm() {
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const isEdit = Boolean(id);
|
const isEdit = Boolean(id);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { user } = useAuth();
|
||||||
const [categorie, setCategorie] = useState<{ id: number; descrizione: string }[]>([]);
|
const [categorie, setCategorie] = useState<{ id: number; descrizione: string }[]>([]);
|
||||||
const [form, setForm] = useState({
|
const [form, setForm] = useState({
|
||||||
dataSpesa: "",
|
dataSpesa: "",
|
||||||
|
|
@ -16,6 +18,7 @@ export default function RimborsoForm() {
|
||||||
});
|
});
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [initialLoading, setInitialLoading] = useState(isEdit);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getCategorie().then((cats) => {
|
getCategorie().then((cats) => {
|
||||||
|
|
@ -26,16 +29,27 @@ export default function RimborsoForm() {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
getRimborso(Number(id)).then((r) => {
|
getRimborso(Number(id))
|
||||||
setForm({
|
.then((r) => {
|
||||||
dataSpesa: r.dataSpesa.slice(0, 10),
|
if (r.stato !== "IN_ATTESA") {
|
||||||
categoriaId: String(r.categoriaId),
|
setError("La richiesta non può essere modificata nello stato corrente");
|
||||||
importo: String(r.importo),
|
return;
|
||||||
descrizione: r.descrizione,
|
}
|
||||||
riferimentoGiustificativo: r.riferimentoGiustificativo ?? "",
|
if (r.dipendenteId !== user?.id) {
|
||||||
});
|
setError("Non puoi modificare questa richiesta");
|
||||||
}).catch((err) => setError(err.message));
|
return;
|
||||||
}, [id]);
|
}
|
||||||
|
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) => {
|
const handleSubmit = async (e: FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
@ -51,10 +65,11 @@ export default function RimborsoForm() {
|
||||||
try {
|
try {
|
||||||
if (isEdit) {
|
if (isEdit) {
|
||||||
await updateRimborso(Number(id), payload);
|
await updateRimborso(Number(id), payload);
|
||||||
|
navigate("/rimborsi", { state: { flash: "Richiesta aggiornata con successo." } });
|
||||||
} else {
|
} else {
|
||||||
await createRimborso(payload);
|
await createRimborso(payload);
|
||||||
|
navigate("/rimborsi", { state: { flash: "Richiesta creata con successo." } });
|
||||||
}
|
}
|
||||||
navigate("/rimborsi");
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : "Errore nel salvataggio");
|
setError(err instanceof Error ? err.message : "Errore nel salvataggio");
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -62,9 +77,19 @@ export default function RimborsoForm() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (initialLoading) return <p className="loading">Caricamento...</p>;
|
||||||
|
|
||||||
|
const blocked = isEdit && error && !form.dataSpesa;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page">
|
<div className="page">
|
||||||
<h1>{isEdit ? "Modifica richiesta" : "Nuova richiesta di rimborso"}</h1>
|
<h1>{isEdit ? "Modifica richiesta" : "Nuova richiesta di rimborso"}</h1>
|
||||||
|
{blocked ? (
|
||||||
|
<div className="form-card">
|
||||||
|
<p className="error">{error}</p>
|
||||||
|
<Link to={`/rimborsi/${id}`} className="btn btn-secondary">Torna al dettaglio</Link>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<form className="form-card" onSubmit={handleSubmit}>
|
<form className="form-card" onSubmit={handleSubmit}>
|
||||||
{error && <p className="error">{error}</p>}
|
{error && <p className="error">{error}</p>}
|
||||||
<label>
|
<label>
|
||||||
|
|
@ -98,6 +123,7 @@ export default function RimborsoForm() {
|
||||||
<Link to="/rimborsi" className="btn btn-secondary">Annulla</Link>
|
<Link to="/rimborsi" className="btn btn-secondary">Annulla</Link>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,8 @@ export default function Statistiche() {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Promise.all([getCategorie(), getDipendenti()])
|
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(() => {
|
useEffect(() => {
|
||||||
|
|
@ -44,6 +45,7 @@ export default function Statistiche() {
|
||||||
categorie={categorie}
|
categorie={categorie}
|
||||||
dipendenti={dipendenti}
|
dipendenti={dipendenti}
|
||||||
showDipendente
|
showDipendente
|
||||||
|
showStato={false}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{error && <p className="error">{error}</p>}
|
{error && <p className="error">{error}</p>}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue