magricambi/src/components/StabilimentiManager.tsx
AV77web 85e2256022
All checks were successful
Deploy to VPS / build (push) Successful in 39s
Deploy to VPS / deploy (push) Successful in 27s
inserita la gestiione stabilimenti
2026-05-31 17:05:18 +02:00

431 lines
17 KiB
TypeScript

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