431 lines
17 KiB
TypeScript
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>
|
|
);
|
|
}
|