inserita la gestiione stabilimenti
This commit is contained in:
parent
c1f2dfb44d
commit
85e2256022
10 changed files with 891 additions and 25 deletions
11
src/app/(management)/stabilimenti/page.tsx
Normal file
11
src/app/(management)/stabilimenti/page.tsx
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
"use client";
|
||||
|
||||
import StabilimentiManager from "@/src/components/StabilimentiManager";
|
||||
|
||||
export default function StabilimentiPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-black -mx-4 -my-6 px-4 py-6">
|
||||
<StabilimentiManager />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
79
src/app/api/magazzini/[id]/route.ts
Normal file
79
src/app/api/magazzini/[id]/route.ts
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
import { NextResponse } from "next/server";
|
||||
|
||||
import { auth } from "@/src/auth";
|
||||
import { getMagazzinoById, updateMagazzino, deleteMagazzino } from "@/src/lib/stabilimenti";
|
||||
|
||||
interface RouteContext {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
async function readId(params: Promise<{ id: string }>): Promise<number> {
|
||||
const { id } = await params;
|
||||
const magazzinoId = Number(id);
|
||||
|
||||
if (!Number.isInteger(magazzinoId) || magazzinoId <= 0) {
|
||||
throw new Error("ID magazzino non valido.");
|
||||
}
|
||||
|
||||
return magazzinoId;
|
||||
}
|
||||
|
||||
export async function GET(_request: Request, context: RouteContext) {
|
||||
const session = await auth();
|
||||
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: "Non autorizzato" }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const magazzino = await getMagazzinoById(await readId(context.params));
|
||||
if (!magazzino) {
|
||||
return NextResponse.json({ error: "Magazzino non trovato" }, { status: 404 });
|
||||
}
|
||||
return NextResponse.json({ magazzino });
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : "Errore recupero magazzino" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function PUT(request: Request, context: RouteContext) {
|
||||
const session = await auth();
|
||||
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: "Non autorizzato" }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const magazzino = await updateMagazzino(
|
||||
await readId(context.params),
|
||||
await request.json()
|
||||
);
|
||||
return NextResponse.json({ magazzino });
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : "Errore modifica magazzino" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(_request: Request, context: RouteContext) {
|
||||
const session = await auth();
|
||||
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: "Non autorizzato" }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
await deleteMagazzino(await readId(context.params));
|
||||
return NextResponse.json({ ok: true });
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : "Errore eliminazione magazzino" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { NextResponse } from "next/server";
|
||||
|
||||
import { auth } from "@/src/auth";
|
||||
import { listMagazzini } from "@/src/lib/stabilimenti";
|
||||
import { listMagazzini, createMagazzino } from "@/src/lib/stabilimenti";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const session = await auth();
|
||||
|
|
@ -27,3 +27,21 @@ export async function GET(request: Request) {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const session = await auth();
|
||||
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: "Non autorizzato" }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const magazzino = await createMagazzino(await request.json());
|
||||
return NextResponse.json({ magazzino }, { status: 201 });
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : "Errore creazione magazzino" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
83
src/app/api/stabilimenti/[id]/route.ts
Normal file
83
src/app/api/stabilimenti/[id]/route.ts
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import { NextResponse } from "next/server";
|
||||
|
||||
import { auth } from "@/src/auth";
|
||||
import {
|
||||
getStabilimentoById,
|
||||
updateStabilimento,
|
||||
deleteStabilimento,
|
||||
} from "@/src/lib/stabilimenti";
|
||||
|
||||
interface RouteContext {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
async function readId(params: Promise<{ id: string }>): Promise<number> {
|
||||
const { id } = await params;
|
||||
const stabilimentoId = Number(id);
|
||||
|
||||
if (!Number.isInteger(stabilimentoId) || stabilimentoId <= 0) {
|
||||
throw new Error("ID stabilimento non valido.");
|
||||
}
|
||||
|
||||
return stabilimentoId;
|
||||
}
|
||||
|
||||
export async function GET(_request: Request, context: RouteContext) {
|
||||
const session = await auth();
|
||||
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: "Non autorizzato" }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const stabilimento = await getStabilimentoById(await readId(context.params));
|
||||
if (!stabilimento) {
|
||||
return NextResponse.json({ error: "Stabilimento non trovato" }, { status: 404 });
|
||||
}
|
||||
return NextResponse.json({ stabilimento });
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : "Errore recupero stabilimento" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function PUT(request: Request, context: RouteContext) {
|
||||
const session = await auth();
|
||||
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: "Non autorizzato" }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const stabilimento = await updateStabilimento(
|
||||
await readId(context.params),
|
||||
await request.json()
|
||||
);
|
||||
return NextResponse.json({ stabilimento });
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : "Errore modifica stabilimento" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(_request: Request, context: RouteContext) {
|
||||
const session = await auth();
|
||||
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: "Non autorizzato" }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
await deleteStabilimento(await readId(context.params));
|
||||
return NextResponse.json({ ok: true });
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : "Errore eliminazione stabilimento" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,11 @@
|
|||
import { NextResponse } from "next/server";
|
||||
|
||||
import { auth } from "@/src/auth";
|
||||
import { listStabilimenti, getStabilimentiWithMagazzini } from "@/src/lib/stabilimenti";
|
||||
import {
|
||||
listStabilimenti,
|
||||
getStabilimentiWithMagazzini,
|
||||
createStabilimento,
|
||||
} from "@/src/lib/stabilimenti";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const session = await auth();
|
||||
|
|
@ -29,3 +33,21 @@ export async function GET(request: Request) {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const session = await auth();
|
||||
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: "Non autorizzato" }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const stabilimento = await createStabilimento(await request.json());
|
||||
return NextResponse.json({ stabilimento }, { status: 201 });
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : "Errore creazione stabilimento" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ const navLinks: NavLink[] = [
|
|||
{ href: "/movimenti", label: "Movimenti"},
|
||||
{ href: "/articoli", label: "Articoli"},
|
||||
{ href: "/macchine", label: "Macchine"},
|
||||
{ href: "/stabilimenti", label: "Stabilimenti"},
|
||||
{ href: "/utenti", label: "Utenti"},
|
||||
];
|
||||
|
||||
|
|
|
|||
431
src/components/StabilimentiManager.tsx
Normal file
431
src/components/StabilimentiManager.tsx
Normal file
|
|
@ -0,0 +1,431 @@
|
|||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import type { Stabilimento, Magazzino, StabilimentoWithMagazzini } from "@/src/types/stabilimento";
|
||||
import { useStabilimento } from "@/src/contexts/StabilimentoContext";
|
||||
|
||||
function FormModal({
|
||||
title,
|
||||
onClose,
|
||||
onSubmit,
|
||||
children,
|
||||
submitLabel,
|
||||
}: {
|
||||
title: string;
|
||||
onClose: () => void;
|
||||
onSubmit: (e: React.FormEvent) => void;
|
||||
children: React.ReactNode;
|
||||
submitLabel: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
<div className="absolute inset-0 bg-black/70 backdrop-blur-sm" onClick={onClose} />
|
||||
<div className="relative w-full max-w-md rounded-xl bg-zinc-900 border border-zinc-700 p-6 shadow-xl">
|
||||
<h3 className="text-lg font-semibold text-white mb-4">{title}</h3>
|
||||
<form onSubmit={onSubmit} className="space-y-4">
|
||||
{children}
|
||||
<div className="flex justify-end gap-3 pt-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 rounded-lg border border-zinc-700 text-zinc-300 hover:bg-zinc-800"
|
||||
>
|
||||
Annulla
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="px-4 py-2 rounded-lg bg-blue-600 text-white hover:bg-blue-500"
|
||||
>
|
||||
{submitLabel}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function StabilimentiManager() {
|
||||
const { reload } = useStabilimento();
|
||||
const [stabilimenti, setStabilimenti] = useState<StabilimentoWithMagazzini[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
const [stabModal, setStabModal] = useState<"create" | "edit" | null>(null);
|
||||
const [magModal, setMagModal] = useState<"create" | "edit" | null>(null);
|
||||
const [editingStabilimento, setEditingStabilimento] = useState<Stabilimento | null>(null);
|
||||
const [editingMagazzino, setEditingMagazzino] = useState<Magazzino | null>(null);
|
||||
const [presetStabilimentoId, setPresetStabilimentoId] = useState<number | null>(null);
|
||||
|
||||
const [stabDescrizione, setStabDescrizione] = useState("");
|
||||
const [magDescrizione, setMagDescrizione] = useState("");
|
||||
const [magStabilimentoId, setMagStabilimentoId] = useState("");
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const res = await fetch("/api/stabilimenti?includeMagazzini=true");
|
||||
if (!res.ok) throw new Error("Errore nel caricamento");
|
||||
const data = await res.json();
|
||||
setStabilimenti(data.stabilimenti || []);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Errore sconosciuto");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, [loadData]);
|
||||
|
||||
const afterMutation = async () => {
|
||||
await loadData();
|
||||
await reload();
|
||||
};
|
||||
|
||||
const openCreateStabilimento = () => {
|
||||
setEditingStabilimento(null);
|
||||
setStabDescrizione("");
|
||||
setStabModal("create");
|
||||
};
|
||||
|
||||
const openEditStabilimento = (stab: Stabilimento) => {
|
||||
setEditingStabilimento(stab);
|
||||
setStabDescrizione(stab.descrizione);
|
||||
setStabModal("edit");
|
||||
};
|
||||
|
||||
const openCreateMagazzino = (stabilimentoId?: number) => {
|
||||
setEditingMagazzino(null);
|
||||
setMagDescrizione("");
|
||||
setMagStabilimentoId(stabilimentoId?.toString() || "");
|
||||
setPresetStabilimentoId(stabilimentoId ?? null);
|
||||
setMagModal("create");
|
||||
};
|
||||
|
||||
const openEditMagazzino = (mag: Magazzino) => {
|
||||
setEditingMagazzino(mag);
|
||||
setMagDescrizione(mag.descrizione);
|
||||
setMagStabilimentoId(mag.id_stabilimento.toString());
|
||||
setMagModal("edit");
|
||||
};
|
||||
|
||||
const handleStabilimentoSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
try {
|
||||
const url =
|
||||
stabModal === "edit" && editingStabilimento
|
||||
? `/api/stabilimenti/${editingStabilimento.id}`
|
||||
: "/api/stabilimenti";
|
||||
const method = stabModal === "edit" ? "PUT" : "POST";
|
||||
const res = await fetch(url, {
|
||||
method,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ descrizione: stabDescrizione }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error || "Operazione fallita");
|
||||
setStabModal(null);
|
||||
await afterMutation();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Errore");
|
||||
}
|
||||
};
|
||||
|
||||
const handleMagazzinoSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
try {
|
||||
const url =
|
||||
magModal === "edit" && editingMagazzino
|
||||
? `/api/magazzini/${editingMagazzino.id}`
|
||||
: "/api/magazzini";
|
||||
const method = magModal === "edit" ? "PUT" : "POST";
|
||||
const res = await fetch(url, {
|
||||
method,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
descrizione: magDescrizione,
|
||||
id_stabilimento: parseInt(magStabilimentoId, 10),
|
||||
}),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error || "Operazione fallita");
|
||||
setMagModal(null);
|
||||
await afterMutation();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Errore");
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteStabilimento = async (id: number, nome: string) => {
|
||||
if (!confirm(`Eliminare lo stabilimento "${nome}"?`)) return;
|
||||
setError(null);
|
||||
try {
|
||||
const res = await fetch(`/api/stabilimenti/${id}`, { method: "DELETE" });
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error || "Eliminazione fallita");
|
||||
await afterMutation();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Errore");
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteMagazzino = async (id: number, nome: string) => {
|
||||
if (!confirm(`Eliminare il magazzino "${nome}"?`)) return;
|
||||
setError(null);
|
||||
try {
|
||||
const res = await fetch(`/api/magazzini/${id}`, { method: "DELETE" });
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error || "Eliminazione fallita");
|
||||
await afterMutation();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Errore");
|
||||
}
|
||||
};
|
||||
|
||||
const filtered = stabilimenti.filter((s) =>
|
||||
s.descrizione.toLowerCase().includes(search.toLowerCase())
|
||||
);
|
||||
|
||||
const totalMagazzini = stabilimenti.reduce((n, s) => n + s.magazzini.length, 0);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-bold text-white">Stabilimenti e Magazzini</h1>
|
||||
<p className="text-sm text-zinc-500">
|
||||
Anagrafica sedi produttive e magazzini associati
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 mb-6">
|
||||
<div className="p-4 rounded-xl bg-zinc-900 border border-zinc-800">
|
||||
<p className="text-sm text-zinc-500">Stabilimenti</p>
|
||||
<p className="text-2xl font-bold text-white">{stabilimenti.length}</p>
|
||||
</div>
|
||||
<div className="p-4 rounded-xl bg-zinc-900 border border-zinc-800">
|
||||
<p className="text-sm text-zinc-500">Magazzini totali</p>
|
||||
<p className="text-2xl font-bold text-teal-400">{totalMagazzini}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="mb-4 p-4 rounded-lg bg-red-500/10 border border-red-500/30 text-red-400 text-sm flex justify-between items-center">
|
||||
<span>{error}</span>
|
||||
<button type="button" onClick={() => setError(null)} className="text-red-300 hover:text-white">
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col md:flex-row gap-4 items-start md:items-center justify-between mb-6">
|
||||
<input
|
||||
type="text"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
placeholder="Cerca stabilimenti..."
|
||||
className="w-full md:max-w-md px-4 py-2 rounded-lg bg-zinc-900 border border-zinc-800 text-white placeholder-zinc-500 focus:outline-none focus:border-blue-500"
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={openCreateStabilimento}
|
||||
className="px-4 py-2 rounded-lg bg-blue-600 text-white font-medium hover:bg-blue-500"
|
||||
>
|
||||
Nuovo stabilimento
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openCreateMagazzino()}
|
||||
className="px-4 py-2 rounded-lg bg-teal-700 text-white font-medium hover:bg-teal-600"
|
||||
>
|
||||
Nuovo magazzino
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-20 text-zinc-500">
|
||||
<svg className="w-6 h-6 animate-spin mr-3" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" />
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||
</svg>
|
||||
Caricamento...
|
||||
</div>
|
||||
) : filtered.length === 0 ? (
|
||||
<div className="p-12 rounded-xl bg-zinc-900 border border-zinc-800 text-center">
|
||||
<h3 className="text-lg font-medium text-white mb-2">Nessuno stabilimento</h3>
|
||||
<p className="text-zinc-500 mb-4">
|
||||
{search ? "Nessun risultato per la ricerca" : "Aggiungi il primo stabilimento"}
|
||||
</p>
|
||||
{!search && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={openCreateStabilimento}
|
||||
className="px-4 py-2 rounded-lg bg-blue-600 text-white hover:bg-blue-500"
|
||||
>
|
||||
Crea stabilimento
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{filtered.map((stab) => (
|
||||
<div
|
||||
key={stab.id}
|
||||
className="rounded-xl bg-zinc-900 border border-zinc-800 overflow-hidden"
|
||||
>
|
||||
<div className="flex items-center justify-between p-4 border-b border-zinc-800">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-2xl">🏭</span>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-white">{stab.descrizione}</h2>
|
||||
<p className="text-xs text-zinc-500">
|
||||
{stab.magazzini.length} magazzin{stab.magazzini.length === 1 ? "o" : "i"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openCreateMagazzino(stab.id)}
|
||||
className="px-3 py-1.5 text-xs rounded-lg bg-teal-900/50 text-teal-300 border border-teal-800 hover:bg-teal-900"
|
||||
title="Aggiungi magazzino"
|
||||
>
|
||||
+ Magazzino
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openEditStabilimento(stab)}
|
||||
className="p-2 rounded-lg text-zinc-400 hover:text-white hover:bg-zinc-800"
|
||||
title="Modifica"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleDeleteStabilimento(stab.id, stab.descrizione)}
|
||||
className="p-2 rounded-lg text-red-400 hover:text-red-300 hover:bg-red-500/10"
|
||||
title="Elimina"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4">
|
||||
{stab.magazzini.length === 0 ? (
|
||||
<p className="text-sm text-zinc-500 italic">Nessun magazzino associato</p>
|
||||
) : (
|
||||
<ul className="space-y-2">
|
||||
{stab.magazzini.map((mag) => (
|
||||
<li
|
||||
key={mag.id}
|
||||
className="flex items-center justify-between py-2 px-3 rounded-lg bg-zinc-950 border border-zinc-800"
|
||||
>
|
||||
<span className="text-sm text-zinc-200 flex items-center gap-2">
|
||||
<span>📦</span>
|
||||
{mag.descrizione}
|
||||
</span>
|
||||
<div className="flex gap-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openEditMagazzino(mag)}
|
||||
className="p-1.5 rounded text-zinc-400 hover:text-white hover:bg-zinc-800"
|
||||
title="Modifica"
|
||||
>
|
||||
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleDeleteMagazzino(mag.id, mag.descrizione)}
|
||||
className="p-1.5 rounded text-red-400 hover:text-red-300 hover:bg-red-500/10"
|
||||
title="Elimina"
|
||||
>
|
||||
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{stabModal && (
|
||||
<FormModal
|
||||
title={stabModal === "edit" ? "Modifica stabilimento" : "Nuovo stabilimento"}
|
||||
onClose={() => setStabModal(null)}
|
||||
onSubmit={handleStabilimentoSubmit}
|
||||
submitLabel={stabModal === "edit" ? "Salva" : "Crea"}
|
||||
>
|
||||
<div>
|
||||
<label className="block text-xs text-zinc-400 mb-1">Descrizione</label>
|
||||
<input
|
||||
type="text"
|
||||
value={stabDescrizione}
|
||||
onChange={(e) => setStabDescrizione(e.target.value)}
|
||||
required
|
||||
className="w-full px-3 py-2 rounded-lg bg-zinc-950 border border-zinc-700 text-white focus:outline-none focus:border-blue-500"
|
||||
placeholder="Es. Stabilimento Gonzaga"
|
||||
/>
|
||||
</div>
|
||||
</FormModal>
|
||||
)}
|
||||
|
||||
{magModal && (
|
||||
<FormModal
|
||||
title={magModal === "edit" ? "Modifica magazzino" : "Nuovo magazzino"}
|
||||
onClose={() => setMagModal(null)}
|
||||
onSubmit={handleMagazzinoSubmit}
|
||||
submitLabel={magModal === "edit" ? "Salva" : "Crea"}
|
||||
>
|
||||
<div>
|
||||
<label className="block text-xs text-zinc-400 mb-1">Stabilimento</label>
|
||||
<select
|
||||
value={magStabilimentoId}
|
||||
onChange={(e) => setMagStabilimentoId(e.target.value)}
|
||||
required
|
||||
disabled={magModal === "create" && presetStabilimentoId !== null}
|
||||
className="w-full px-3 py-2 rounded-lg bg-zinc-950 border border-zinc-700 text-white focus:outline-none focus:border-blue-500 disabled:opacity-60"
|
||||
>
|
||||
<option value="">Seleziona stabilimento...</option>
|
||||
{stabilimenti.map((s) => (
|
||||
<option key={s.id} value={s.id}>
|
||||
{s.descrizione}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-zinc-400 mb-1">Descrizione</label>
|
||||
<input
|
||||
type="text"
|
||||
value={magDescrizione}
|
||||
onChange={(e) => setMagDescrizione(e.target.value)}
|
||||
required
|
||||
className="w-full px-3 py-2 rounded-lg bg-zinc-950 border border-zinc-700 text-white focus:outline-none focus:border-blue-500"
|
||||
placeholder="Es. Magazzino ricambi"
|
||||
/>
|
||||
</div>
|
||||
</FormModal>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -20,6 +20,7 @@ interface StabilimentoContextValue {
|
|||
setSelectedMagazzino: (magazzino: Magazzino | null) => void;
|
||||
filteredMagazzini: Magazzino[];
|
||||
resetSelection: () => void;
|
||||
reload: () => Promise<void>;
|
||||
}
|
||||
|
||||
const StabilimentoContext = createContext<StabilimentoContextValue | null>(null);
|
||||
|
|
@ -31,8 +32,7 @@ export function StabilimentoProvider({ children }: { children: ReactNode }) {
|
|||
const [selectedMagazzino, setSelectedMagazzinoState] = useState<Magazzino | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchData() {
|
||||
const reload = useCallback(async () => {
|
||||
try {
|
||||
const [stabRes, magRes] = await Promise.all([
|
||||
fetch("/api/stabilimenti"),
|
||||
|
|
@ -41,22 +41,39 @@ export function StabilimentoProvider({ children }: { children: ReactNode }) {
|
|||
|
||||
if (stabRes.ok) {
|
||||
const stabData = await stabRes.json();
|
||||
setStabilimenti(stabData.stabilimenti || []);
|
||||
const list: Stabilimento[] = stabData.stabilimenti || [];
|
||||
setStabilimenti(list);
|
||||
|
||||
setSelectedStabilimentoState((prev) => {
|
||||
if (!prev) return prev;
|
||||
return list.find((s) => s.id === prev.id) ?? null;
|
||||
});
|
||||
}
|
||||
|
||||
if (magRes.ok) {
|
||||
const magData = await magRes.json();
|
||||
setMagazzini(magData.magazzini || []);
|
||||
const list: Magazzino[] = magData.magazzini || [];
|
||||
setMagazzini(list);
|
||||
|
||||
setSelectedMagazzinoState((prev) => {
|
||||
if (!prev) return prev;
|
||||
return list.find((m) => m.id === prev.id) ?? null;
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Errore caricamento stabilimenti/magazzini:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchData() {
|
||||
setLoading(true);
|
||||
await reload();
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
fetchData();
|
||||
}, []);
|
||||
}, [reload]);
|
||||
|
||||
const setSelectedStabilimento = useCallback((stabilimento: Stabilimento | null) => {
|
||||
setSelectedStabilimentoState(stabilimento);
|
||||
|
|
@ -88,6 +105,7 @@ export function StabilimentoProvider({ children }: { children: ReactNode }) {
|
|||
setSelectedMagazzino,
|
||||
filteredMagazzini,
|
||||
resetSelection,
|
||||
reload,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,13 @@
|
|||
import type { RowDataPacket } from "mysql2";
|
||||
import type { ResultSetHeader, RowDataPacket } from "mysql2";
|
||||
|
||||
import { db } from "@/src/lib/db";
|
||||
import type { Stabilimento, Magazzino, StabilimentoWithMagazzini } from "@/src/types/stabilimento";
|
||||
import type {
|
||||
Stabilimento,
|
||||
Magazzino,
|
||||
StabilimentoWithMagazzini,
|
||||
StabilimentoInput,
|
||||
MagazzinoInput,
|
||||
} from "@/src/types/stabilimento";
|
||||
|
||||
type StabilimentoRow = Stabilimento & RowDataPacket;
|
||||
type MagazzinoRow = Magazzino & RowDataPacket;
|
||||
|
|
@ -87,3 +93,191 @@ export async function getMagazzinoById(id: number): Promise<Magazzino | null> {
|
|||
stabilimento: row.stabilimento,
|
||||
};
|
||||
}
|
||||
|
||||
function cleanText(value: unknown): string {
|
||||
return String(value ?? "").trim();
|
||||
}
|
||||
|
||||
function parseStabilimentoInput(input: Partial<StabilimentoInput>): StabilimentoInput {
|
||||
const descrizione = cleanText(input.descrizione);
|
||||
if (!descrizione) {
|
||||
throw new Error("La descrizione dello stabilimento è obbligatoria.");
|
||||
}
|
||||
return { descrizione };
|
||||
}
|
||||
|
||||
function parseMagazzinoInput(input: Partial<MagazzinoInput>): MagazzinoInput {
|
||||
const descrizione = cleanText(input.descrizione);
|
||||
const id_stabilimento = Number(input.id_stabilimento);
|
||||
|
||||
if (!descrizione) {
|
||||
throw new Error("La descrizione del magazzino è obbligatoria.");
|
||||
}
|
||||
if (!Number.isInteger(id_stabilimento) || id_stabilimento <= 0) {
|
||||
throw new Error("Seleziona uno stabilimento valido per il magazzino.");
|
||||
}
|
||||
|
||||
return { descrizione, id_stabilimento };
|
||||
}
|
||||
|
||||
export async function createStabilimento(input: Partial<StabilimentoInput>): Promise<Stabilimento> {
|
||||
const { descrizione } = parseStabilimentoInput(input);
|
||||
|
||||
const [result] = await db.execute<ResultSetHeader>(
|
||||
"INSERT INTO stabilimenti (descrizione) VALUES (:descrizione)",
|
||||
{ descrizione }
|
||||
);
|
||||
|
||||
const created = await getStabilimentoById(result.insertId);
|
||||
if (!created) {
|
||||
throw new Error("Errore nella creazione dello stabilimento.");
|
||||
}
|
||||
return created;
|
||||
}
|
||||
|
||||
export async function updateStabilimento(
|
||||
id: number,
|
||||
input: Partial<StabilimentoInput>
|
||||
): Promise<Stabilimento> {
|
||||
const existing = await getStabilimentoById(id);
|
||||
if (!existing) {
|
||||
throw new Error("Stabilimento non trovato.");
|
||||
}
|
||||
|
||||
const { descrizione } = parseStabilimentoInput(input);
|
||||
|
||||
await db.execute(
|
||||
"UPDATE stabilimenti SET descrizione = :descrizione WHERE id = :id",
|
||||
{ descrizione, id }
|
||||
);
|
||||
|
||||
const updated = await getStabilimentoById(id);
|
||||
if (!updated) {
|
||||
throw new Error("Errore nell'aggiornamento dello stabilimento.");
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
|
||||
export async function deleteStabilimento(id: number): Promise<void> {
|
||||
const existing = await getStabilimentoById(id);
|
||||
if (!existing) {
|
||||
throw new Error("Stabilimento non trovato.");
|
||||
}
|
||||
|
||||
const [magRows] = await db.execute<RowDataPacket[]>(
|
||||
"SELECT COUNT(*) AS cnt FROM magazzini WHERE id_stabilimento = :id",
|
||||
{ id }
|
||||
);
|
||||
if (Number(magRows[0]?.cnt) > 0) {
|
||||
throw new Error(
|
||||
"Impossibile eliminare: esistono magazzini associati. Elimina prima i magazzini."
|
||||
);
|
||||
}
|
||||
|
||||
const [macRows] = await db.execute<RowDataPacket[]>(
|
||||
"SELECT COUNT(*) AS cnt FROM macchine_parti WHERE id_stabilimento = :id",
|
||||
{ id }
|
||||
);
|
||||
if (Number(macRows[0]?.cnt) > 0) {
|
||||
throw new Error(
|
||||
"Impossibile eliminare: esistono macchine associate a questo stabilimento."
|
||||
);
|
||||
}
|
||||
|
||||
await db.execute("DELETE FROM stabilimenti WHERE id = :id", { id });
|
||||
}
|
||||
|
||||
export async function createMagazzino(input: Partial<MagazzinoInput>): Promise<Magazzino> {
|
||||
const parsed = parseMagazzinoInput(input);
|
||||
|
||||
const stabilimento = await getStabilimentoById(parsed.id_stabilimento);
|
||||
if (!stabilimento) {
|
||||
throw new Error("Stabilimento non trovato.");
|
||||
}
|
||||
|
||||
const [result] = await db.execute<ResultSetHeader>(
|
||||
"INSERT INTO magazzini (descrizione, id_stabilimento) VALUES (:descrizione, :id_stabilimento)",
|
||||
{
|
||||
descrizione: parsed.descrizione,
|
||||
id_stabilimento: parsed.id_stabilimento,
|
||||
}
|
||||
);
|
||||
|
||||
const created = await getMagazzinoById(result.insertId);
|
||||
if (!created) {
|
||||
throw new Error("Errore nella creazione del magazzino.");
|
||||
}
|
||||
return created;
|
||||
}
|
||||
|
||||
export async function updateMagazzino(
|
||||
id: number,
|
||||
input: Partial<MagazzinoInput>
|
||||
): Promise<Magazzino> {
|
||||
const existing = await getMagazzinoById(id);
|
||||
if (!existing) {
|
||||
throw new Error("Magazzino non trovato.");
|
||||
}
|
||||
|
||||
const parsed = parseMagazzinoInput(input);
|
||||
|
||||
const stabilimento = await getStabilimentoById(parsed.id_stabilimento);
|
||||
if (!stabilimento) {
|
||||
throw new Error("Stabilimento non trovato.");
|
||||
}
|
||||
|
||||
await db.execute(
|
||||
"UPDATE magazzini SET descrizione = :descrizione, id_stabilimento = :id_stabilimento WHERE id = :id",
|
||||
{
|
||||
descrizione: parsed.descrizione,
|
||||
id_stabilimento: parsed.id_stabilimento,
|
||||
id,
|
||||
}
|
||||
);
|
||||
|
||||
const updated = await getMagazzinoById(id);
|
||||
if (!updated) {
|
||||
throw new Error("Errore nell'aggiornamento del magazzino.");
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
|
||||
export async function deleteMagazzino(id: number): Promise<void> {
|
||||
const existing = await getMagazzinoById(id);
|
||||
if (!existing) {
|
||||
throw new Error("Magazzino non trovato.");
|
||||
}
|
||||
|
||||
const [giacRows] = await db.execute<RowDataPacket[]>(
|
||||
"SELECT COUNT(*) AS cnt FROM giacenze WHERE id_magazzino = :id",
|
||||
{ id }
|
||||
);
|
||||
if (Number(giacRows[0]?.cnt) > 0) {
|
||||
throw new Error(
|
||||
"Impossibile eliminare: il magazzino contiene giacenze di articoli."
|
||||
);
|
||||
}
|
||||
|
||||
const [movRows] = await db.execute<RowDataPacket[]>(
|
||||
`SELECT COUNT(*) AS cnt FROM movimenti
|
||||
WHERE id_magazzino_origine = :id OR id_magazzino_destinazione = :id`,
|
||||
{ id }
|
||||
);
|
||||
if (Number(movRows[0]?.cnt) > 0) {
|
||||
throw new Error(
|
||||
"Impossibile eliminare: il magazzino è referenziato in movimenti di magazzino."
|
||||
);
|
||||
}
|
||||
|
||||
const [artRows] = await db.execute<RowDataPacket[]>(
|
||||
"SELECT COUNT(*) AS cnt FROM articoli WHERE id_magazzino = :id",
|
||||
{ id }
|
||||
);
|
||||
if (Number(artRows[0]?.cnt) > 0) {
|
||||
throw new Error(
|
||||
"Impossibile eliminare: articoli del catalogo sono collegati a questo magazzino."
|
||||
);
|
||||
}
|
||||
|
||||
await db.execute("DELETE FROM magazzini WHERE id = :id", { id });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,3 +13,12 @@ export interface Magazzino {
|
|||
export interface StabilimentoWithMagazzini extends Stabilimento {
|
||||
magazzini: Magazzino[];
|
||||
}
|
||||
|
||||
export interface StabilimentoInput {
|
||||
descrizione: string;
|
||||
}
|
||||
|
||||
export interface MagazzinoInput {
|
||||
descrizione: string;
|
||||
id_stabilimento: number;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue