inserita la gestiione stabilimenti
All checks were successful
Deploy to VPS / build (push) Successful in 39s
Deploy to VPS / deploy (push) Successful in 27s

This commit is contained in:
AV77web 2026-05-31 17:05:18 +02:00
parent c1f2dfb44d
commit 85e2256022
10 changed files with 891 additions and 25 deletions

View 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>
);
}

View 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 }
);
}
}

View file

@ -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 }
);
}
}

View 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 }
);
}
}

View file

@ -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 }
);
}
}

View file

@ -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"},
];

View 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>
);
}

View file

@ -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,32 +32,48 @@ export function StabilimentoProvider({ children }: { children: ReactNode }) {
const [selectedMagazzino, setSelectedMagazzinoState] = useState<Magazzino | null>(null);
const [loading, setLoading] = useState(true);
const reload = useCallback(async () => {
try {
const [stabRes, magRes] = await Promise.all([
fetch("/api/stabilimenti"),
fetch("/api/magazzini"),
]);
if (stabRes.ok) {
const stabData = await stabRes.json();
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();
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);
}
}, []);
useEffect(() => {
async function fetchData() {
try {
const [stabRes, magRes] = await Promise.all([
fetch("/api/stabilimenti"),
fetch("/api/magazzini"),
]);
if (stabRes.ok) {
const stabData = await stabRes.json();
setStabilimenti(stabData.stabilimenti || []);
}
if (magRes.ok) {
const magData = await magRes.json();
setMagazzini(magData.magazzini || []);
}
} catch (error) {
console.error("Errore caricamento stabilimenti/magazzini:", error);
} finally {
setLoading(false);
}
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}

View file

@ -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 });
}

View file

@ -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;
}