implementata logica del vecchio gestionale nella gestione delle giacenze per magazzino.
All checks were successful
Deploy to VPS / build (push) Successful in 39s
Deploy to VPS / deploy (push) Successful in 26s

This commit is contained in:
AV77web 2026-05-28 22:39:57 +02:00
parent be228bca5c
commit fa9503a37e
10 changed files with 705 additions and 138 deletions

View file

@ -1,12 +1,122 @@
"use client";
import { useState } from "react";
import type { Stabilimento } from "@/src/types/stabilimento";
import StabilimentoSelector from "@/src/components/StabilimentoSelector";
import ArticlesGrid from "@/src/components/ArticlesGrid";
import { useState, useEffect, useCallback } from "react";
import type { Article, ArticleFamily } from "@/src/types/article";
import type { Stabilimento, Magazzino } from "@/src/types/stabilimento";
import type { GiacenzePerArticolo } from "@/src/types/giacenza";
import ArticleCard from "@/src/components/ArticleCard";
import ArticleModal from "@/src/components/ArticleModal";
import StabilimentoMagazzinoSelector from "@/src/components/StabilimentoMagazzinoSelector";
export default function ArticoliPage() {
const [articles, setArticles] = useState<Article[]>([]);
const [families, setFamilies] = useState<ArticleFamily[]>([]);
const [giacenze, setGiacenze] = useState<GiacenzePerArticolo>({});
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [search, setSearch] = useState("");
const [selectedFamily, setSelectedFamily] = useState<number | null>(null);
const [showModal, setShowModal] = useState(false);
const [editingArticle, setEditingArticle] = useState<Article | null>(null);
const [selectedStabilimento, setSelectedStabilimento] = useState<Stabilimento | null>(null);
const [selectedMagazzino, setSelectedMagazzino] = useState<Magazzino | null>(null);
const fetchArticles = useCallback(async () => {
setLoading(true);
setError(null);
try {
const res = await fetch("/api/articoli");
if (!res.ok) throw new Error("Errore nel caricamento");
const data = await res.json();
setArticles(data.articles);
setFamilies(data.families);
} catch (err) {
setError(err instanceof Error ? err.message : "Errore sconosciuto");
} finally {
setLoading(false);
}
}, []);
const fetchGiacenze = useCallback(async () => {
if (!selectedStabilimento) {
setGiacenze({});
return;
}
try {
const params = new URLSearchParams();
if (selectedMagazzino) {
params.set("magazzinoId", selectedMagazzino.id.toString());
} else {
params.set("stabilimentoId", selectedStabilimento.id.toString());
}
const res = await fetch(`/api/giacenze?${params}`);
if (!res.ok) throw new Error("Errore caricamento giacenze");
const data = await res.json();
setGiacenze(data.giacenze);
} catch (err) {
console.error("Errore giacenze:", err);
setGiacenze({});
}
}, [selectedStabilimento, selectedMagazzino]);
useEffect(() => {
fetchArticles();
}, [fetchArticles]);
useEffect(() => {
fetchGiacenze();
}, [fetchGiacenze]);
const handleSelectionChange = ({ stabilimento, magazzino }: {
stabilimento: Stabilimento | null;
magazzino: Magazzino | null
}) => {
setSelectedStabilimento(stabilimento);
setSelectedMagazzino(magazzino);
};
const handleEdit = (article: Article) => {
setEditingArticle(article);
setShowModal(true);
};
const handleDelete = (deleted: Article) => {
setArticles((prev) => prev.filter((a) => a.id !== deleted.id));
};
const handleSave = (saved: Article) => {
if (editingArticle) {
setArticles((prev) => prev.map((a) => (a.id === saved.id ? saved : a)));
} else {
setArticles((prev) => [saved, ...prev]);
}
setShowModal(false);
setEditingArticle(null);
fetchGiacenze();
};
const filteredArticles = articles.filter((article) => {
const matchesSearch = !search ||
article.descrizione?.toLowerCase().includes(search.toLowerCase()) ||
article.codice?.toLowerCase().includes(search.toLowerCase()) ||
article.barcode?.toLowerCase().includes(search.toLowerCase());
const matchesFamily = !selectedFamily || article.id_famiglia === selectedFamily;
return matchesSearch && matchesFamily;
});
const sottoScorta = articles.filter((a) => a.quantita < a.quantita_minima).length;
const totaleValore = articles.reduce((sum, a) => sum + a.prezzo * a.quantita, 0);
const magazzinoNome = selectedMagazzino?.descrizione ||
(selectedStabilimento ? `${selectedStabilimento.descrizione}` : undefined);
return (
<div className="min-h-screen bg-black">
@ -14,42 +124,124 @@ export default function ArticoliPage() {
{/* Header */}
<div className="mb-6">
<h1 className="text-2xl font-bold text-white">Articoli</h1>
<p className="text-sm text-zinc-500">Catalogo ricambi e componenti</p>
<p className="text-sm text-zinc-500">Catalogo completo ricambi e componenti</p>
</div>
{/* Selezione stabilimento */}
{/* Statistiche */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
<div className="p-4 rounded-xl bg-zinc-900 border border-zinc-800">
<p className="text-sm text-zinc-500">Totale Articoli</p>
<p className="text-2xl font-bold text-white">{articles.length}</p>
</div>
<div className="p-4 rounded-xl bg-zinc-900 border border-zinc-800">
<p className="text-sm text-zinc-500">Famiglie</p>
<p className="text-2xl font-bold text-white">{families.length}</p>
</div>
<div className="p-4 rounded-xl bg-zinc-900 border border-red-500/30">
<p className="text-sm text-zinc-500">Sotto Scorta</p>
<p className="text-2xl font-bold text-red-400">{sottoScorta}</p>
</div>
<div className="p-4 rounded-xl bg-zinc-900 border border-zinc-800">
<p className="text-sm text-zinc-500">Valore Totale</p>
<p className="text-2xl font-bold text-green-400">{totaleValore.toFixed(2)}</p>
</div>
</div>
{/* Selettore Stabilimento/Magazzino */}
<div className="mb-6 p-4 rounded-lg bg-zinc-900 border border-zinc-800">
<div className="flex items-center justify-between mb-3">
<p className="text-sm text-zinc-400">Stabilimento:</p>
{selectedStabilimento && (
<button
onClick={() => setSelectedStabilimento(null)}
className="text-xs text-zinc-500 hover:text-white transition-colors"
>
Cambia
</button>
)}
</div>
{selectedStabilimento ? (
<p className="font-medium text-white">{selectedStabilimento.descrizione}</p>
) : (
<StabilimentoSelector
selectedId={null}
onSelect={setSelectedStabilimento}
/>
)}
<StabilimentoMagazzinoSelector onSelectionChange={handleSelectionChange} />
</div>
{/* Contenuto */}
{selectedStabilimento ? (
<ArticlesGrid stabilimento={selectedStabilimento} />
) : (
<div className="flex flex-col items-center justify-center py-20 text-center">
<svg className="w-16 h-16 text-zinc-800 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
{/* Toolbar */}
<div className="flex flex-col md:flex-row gap-4 items-start md:items-center justify-between mb-6">
<div className="flex flex-1 gap-4 w-full md:w-auto">
{/* Ricerca */}
<div className="relative flex-1 max-w-md">
<svg className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-zinc-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Cerca articoli..."
className="w-full pl-10 pr-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>
{/* Filtro famiglia */}
<select
value={selectedFamily ?? ""}
onChange={(e) => setSelectedFamily(e.target.value ? parseInt(e.target.value, 10) : null)}
className="px-4 py-2 rounded-lg bg-zinc-900 border border-zinc-800 text-white focus:outline-none focus:border-blue-500"
>
<option value="">Tutte le famiglie</option>
{families.map((fam) => (
<option key={fam.id} value={fam.id}>{fam.nome}</option>
))}
</select>
</div>
{/* Pulsante nuovo */}
<button
onClick={() => { setEditingArticle(null); setShowModal(true); }}
className="px-4 py-2 rounded-lg bg-blue-600 text-white font-medium hover:bg-blue-500 transition-colors flex items-center gap-2"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
Nuovo Articolo
</button>
</div>
{/* Lista articoli */}
{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 articoli...
</div>
) : error ? (
<div className="p-6 rounded-xl bg-red-500/10 border border-red-500/30 text-center">
<p className="text-red-400">{error}</p>
<button onClick={fetchArticles} className="mt-4 px-4 py-2 rounded-lg bg-red-600 text-white hover:bg-red-500">
Riprova
</button>
</div>
) : filteredArticles.length === 0 ? (
<div className="p-12 rounded-xl bg-zinc-900 border border-zinc-800 text-center">
<svg className="w-16 h-16 mx-auto text-zinc-700 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
</svg>
<p className="text-zinc-500">Seleziona uno stabilimento per visualizzare gli articoli</p>
<h3 className="text-lg font-medium text-white mb-2">Nessun articolo trovato</h3>
<p className="text-zinc-500">Prova a modificare i filtri di ricerca</p>
</div>
) : (
<div className="space-y-2">
{filteredArticles.map((article) => (
<ArticleCard
key={article.id}
article={article}
giacenzaMagazzino={selectedStabilimento ? giacenze[article.id] : undefined}
magazzinoNome={magazzinoNome}
onEdit={handleEdit}
onDelete={handleDelete}
/>
))}
</div>
)}
{/* Modal */}
{showModal && (
<ArticleModal
article={editingArticle}
families={families}
stabilimentoId={selectedStabilimento?.id || 0}
onClose={() => { setShowModal(false); setEditingArticle(null); }}
onSave={handleSave}
/>
)}
</div>
</div>

View file

@ -1,74 +1,30 @@
"use client";
import { useState } from "react";
import type { Stabilimento } from "@/src/types/stabilimento";
import StabilimentoSelector from "@/src/components/StabilimentoSelector";
import ArticlesList from "@/src/components/ArticlesList";
import MacchineTree from "@/src/components/MacchineTree";
export default function Home() {
const [selectedStabilimento, setSelectedStabilimento] = useState<Stabilimento | null>(null);
return (
<div className="flex flex-col h-[calc(100vh-64px)] bg-black">
<div className="flex-1 flex flex-col max-w-7xl mx-auto w-full px-4 py-6 overflow-hidden">
{/* Header + Selezione stabilimento */}
{/* Header */}
<div className="mb-6 flex-shrink-0">
<div className="flex items-center justify-between mb-4">
<div>
<h1 className="text-2xl font-bold text-white">MagRicambi</h1>
<p className="text-sm text-zinc-500">Gestione magazzino ricambi</p>
</div>
{selectedStabilimento && (
<div className="flex items-center gap-2 text-sm">
<span className="text-zinc-500">Stabilimento:</span>
<span className="font-medium text-white">{selectedStabilimento.descrizione}</span>
<button
onClick={() => setSelectedStabilimento(null)}
className="text-zinc-500 hover:text-white transition-colors"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
)}
</div>
{!selectedStabilimento && (
<div className="p-4 rounded-lg bg-zinc-900 border border-zinc-800">
<p className="text-sm text-zinc-400 mb-3">Seleziona uno stabilimento per iniziare:</p>
<StabilimentoSelector
selectedId={null}
onSelect={setSelectedStabilimento}
/>
</div>
)}
</div>
{/* Contenuto principale: split 50/50 */}
{selectedStabilimento ? (
<div className="flex-1 grid grid-cols-2 gap-6 min-h-0">
{/* Colonna sinistra: Articoli */}
<div className="flex flex-col min-h-0 p-4 rounded-xl bg-zinc-900/50 border border-zinc-800">
<ArticlesList stabilimento={selectedStabilimento} />
<ArticlesList />
</div>
{/* Colonna destra: Macchine */}
{/* Colonna destra: Macchine (placeholder) */}
<div className="flex flex-col min-h-0 p-4 rounded-xl bg-zinc-900/50 border border-zinc-800">
<MacchineTree stabilimento={selectedStabilimento} />
<MacchineTree />
</div>
</div>
) : (
<div className="flex-1 flex items-center justify-center">
<div className="text-center">
<svg className="w-16 h-16 mx-auto text-zinc-800 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</svg>
<p className="text-zinc-500">Seleziona uno stabilimento per visualizzare articoli e macchine</p>
</div>
</div>
)}
</div>
</div>
);

View file

@ -0,0 +1,44 @@
import { NextResponse } from "next/server";
import { auth } from "@/src/auth";
import {
getGiacenzeMapByMagazzino,
getGiacenzeMapByStabilimento
} from "@/src/lib/giacenze";
export async function GET(request: Request) {
const session = await auth();
if (!session?.user) {
return NextResponse.json({ error: "Non autorizzato" }, { status: 401 });
}
try {
const { searchParams } = new URL(request.url);
const magazzinoId = searchParams.get("magazzinoId");
const stabilimentoId = searchParams.get("stabilimentoId");
if (!magazzinoId && !stabilimentoId) {
return NextResponse.json(
{ error: "Specificare magazzinoId o stabilimentoId" },
{ status: 400 }
);
}
let giacenze: Record<number, number>;
if (magazzinoId) {
giacenze = await getGiacenzeMapByMagazzino(parseInt(magazzinoId, 10));
} else {
giacenze = await getGiacenzeMapByStabilimento(parseInt(stabilimentoId!, 10));
}
return NextResponse.json({ giacenze });
} catch (error) {
console.error("[API giacenze] Errore:", error);
return NextResponse.json(
{ error: "Errore nel recupero delle giacenze" },
{ status: 500 }
);
}
}

View file

@ -5,15 +5,24 @@ import type { Article } from "@/src/types/article";
interface ArticleCardProps {
article: Article;
giacenzaMagazzino?: number;
magazzinoNome?: string;
onEdit: (article: Article) => void;
onDelete: (article: Article) => void;
}
export default function ArticleCard({ article, onEdit, onDelete }: ArticleCardProps) {
export default function ArticleCard({
article,
giacenzaMagazzino,
magazzinoNome,
onEdit,
onDelete
}: ArticleCardProps) {
const [deleting, setDeleting] = useState(false);
const isSottoScorta = article.quantita < article.quantita_minima;
const disponibile = article.quantita - article.assegnata_macchine;
const hasGiacenzaMagazzino = giacenzaMagazzino !== undefined;
const handleDelete = async () => {
if (!confirm(`Eliminare "${article.descrizione}"?`)) return;
@ -56,18 +65,50 @@ export default function ArticleCard({ article, onEdit, onDelete }: ArticleCardPr
</div>
<div className="flex items-center gap-3 text-xs text-zinc-500 mt-0.5">
<span className="font-mono">{article.codice}</span>
<span>
Qta: <span className={isSottoScorta ? "text-red-400" : "text-white"}>{article.quantita}</span>
{isSottoScorta && <span className="text-red-400"> (min {article.quantita_minima})</span>}
</span>
<span>
Disp: <span className={disponibile <= 0 ? "text-yellow-400" : "text-green-400"}>{disponibile}</span>
</span>
</div>
</div>
{/* Quantità */}
<div className="flex items-center gap-4 flex-shrink-0">
{/* Quantità totale */}
<div className="text-right">
<div className="text-[10px] text-zinc-600 uppercase">Totale</div>
<div className={`text-sm font-semibold ${isSottoScorta ? "text-red-400" : "text-white"}`}>
{article.quantita}
{isSottoScorta && (
<span className="text-[10px] text-red-400 ml-1">(min {article.quantita_minima})</span>
)}
</div>
</div>
{/* Quantità magazzino (se disponibile) */}
{hasGiacenzaMagazzino && (
<div className="text-right border-l border-zinc-800 pl-4">
<div className="text-[10px] text-zinc-600 uppercase truncate max-w-[80px]" title={magazzinoNome}>
{magazzinoNome || "Mag."}
</div>
<div className={`text-sm font-semibold ${
giacenzaMagazzino === 0 ? "text-zinc-600" :
giacenzaMagazzino! > 0 ? "text-green-400" : "text-white"
}`}>
{giacenzaMagazzino}
</div>
</div>
)}
{/* Disponibile */}
<div className="text-right border-l border-zinc-800 pl-4">
<div className="text-[10px] text-zinc-600 uppercase">Disp.</div>
<div className={`text-sm font-semibold ${
disponibile <= 0 ? "text-yellow-400" : "text-green-400"
}`}>
{disponibile}
</div>
</div>
</div>
{/* Azioni */}
<div className="flex items-center gap-1 flex-shrink-0">
<div className="flex items-center gap-1 flex-shrink-0 border-l border-zinc-800 pl-3">
<button
onClick={() => onEdit(article)}
className="p-1.5 rounded text-zinc-500 hover:text-blue-400 hover:bg-zinc-700 transition-colors"

View file

@ -2,17 +2,16 @@
import { useState, useEffect, useCallback } from "react";
import type { Article, ArticleFamily } from "@/src/types/article";
import type { Stabilimento } from "@/src/types/stabilimento";
import type { Stabilimento, Magazzino } from "@/src/types/stabilimento";
import type { GiacenzePerArticolo } from "@/src/types/giacenza";
import ArticleCard from "./ArticleCard";
import ArticleModal from "./ArticleModal";
import StabilimentoMagazzinoSelector from "./StabilimentoMagazzinoSelector";
interface ArticlesListProps {
stabilimento: Stabilimento;
}
export default function ArticlesList({ stabilimento }: ArticlesListProps) {
export default function ArticlesList() {
const [articles, setArticles] = useState<Article[]>([]);
const [families, setFamilies] = useState<ArticleFamily[]>([]);
const [giacenze, setGiacenze] = useState<GiacenzePerArticolo>({});
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
@ -20,16 +19,15 @@ export default function ArticlesList({ stabilimento }: ArticlesListProps) {
const [showModal, setShowModal] = useState(false);
const [editingArticle, setEditingArticle] = useState<Article | null>(null);
const [selectedStabilimento, setSelectedStabilimento] = useState<Stabilimento | null>(null);
const [selectedMagazzino, setSelectedMagazzino] = useState<Magazzino | null>(null);
const fetchArticles = useCallback(async () => {
setLoading(true);
setError(null);
try {
const params = new URLSearchParams();
params.set("stabilimentoId", stabilimento.id.toString());
if (search) params.set("search", search);
const res = await fetch(`/api/articoli?${params}`);
const res = await fetch("/api/articoli");
if (!res.ok) throw new Error("Errore nel caricamento");
const data = await res.json();
@ -40,12 +38,49 @@ export default function ArticlesList({ stabilimento }: ArticlesListProps) {
} finally {
setLoading(false);
}
}, [stabilimento.id, search]);
}, []);
const fetchGiacenze = useCallback(async () => {
if (!selectedStabilimento) {
setGiacenze({});
return;
}
try {
const params = new URLSearchParams();
if (selectedMagazzino) {
params.set("magazzinoId", selectedMagazzino.id.toString());
} else {
params.set("stabilimentoId", selectedStabilimento.id.toString());
}
const res = await fetch(`/api/giacenze?${params}`);
if (!res.ok) throw new Error("Errore caricamento giacenze");
const data = await res.json();
setGiacenze(data.giacenze);
} catch (err) {
console.error("Errore giacenze:", err);
setGiacenze({});
}
}, [selectedStabilimento, selectedMagazzino]);
useEffect(() => {
fetchArticles();
}, [fetchArticles]);
useEffect(() => {
fetchGiacenze();
}, [fetchGiacenze]);
const handleSelectionChange = ({ stabilimento, magazzino }: {
stabilimento: Stabilimento | null;
magazzino: Magazzino | null
}) => {
setSelectedStabilimento(stabilimento);
setSelectedMagazzino(magazzino);
};
const handleEdit = (article: Article) => {
setEditingArticle(article);
setShowModal(true);
@ -63,9 +98,22 @@ export default function ArticlesList({ stabilimento }: ArticlesListProps) {
}
setShowModal(false);
setEditingArticle(null);
fetchGiacenze();
};
const filteredArticles = articles.filter((article) => {
if (!search) return true;
const searchLower = search.toLowerCase();
return (
article.descrizione?.toLowerCase().includes(searchLower) ||
article.codice?.toLowerCase().includes(searchLower) ||
article.barcode?.toLowerCase().includes(searchLower)
);
});
const sottoScorta = articles.filter((a) => a.quantita < a.quantita_minima).length;
const magazzinoNome = selectedMagazzino?.descrizione ||
(selectedStabilimento ? `${selectedStabilimento.descrizione}` : undefined);
return (
<div className="flex flex-col h-full">
@ -74,7 +122,8 @@ export default function ArticlesList({ stabilimento }: ArticlesListProps) {
<div>
<h2 className="text-lg font-semibold text-white">Articoli</h2>
<p className="text-xs text-zinc-500">
{articles.length} articoli {sottoScorta > 0 && <span className="text-red-400">({sottoScorta} sotto scorta)</span>}
{articles.length} articoli
{sottoScorta > 0 && <span className="text-red-400 ml-1">({sottoScorta} sotto scorta)</span>}
</p>
</div>
<button
@ -88,6 +137,11 @@ export default function ArticlesList({ stabilimento }: ArticlesListProps) {
</button>
</div>
{/* Selettore Stabilimento/Magazzino */}
<div className="mb-4 p-3 rounded-lg bg-zinc-800/50 border border-zinc-800">
<StabilimentoMagazzinoSelector onSelectionChange={handleSelectionChange} />
</div>
{/* Ricerca */}
<div className="relative mb-4">
<svg className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-zinc-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@ -117,15 +171,17 @@ export default function ArticlesList({ stabilimento }: ArticlesListProps) {
<p className="text-red-400 text-sm mb-2">{error}</p>
<button onClick={fetchArticles} className="text-xs text-blue-400 hover:underline">Riprova</button>
</div>
) : articles.length === 0 ? (
) : filteredArticles.length === 0 ? (
<div className="text-center py-8 text-zinc-500 text-sm">
{search ? "Nessun articolo trovato" : "Nessun articolo in questo stabilimento"}
{search ? "Nessun articolo trovato" : "Nessun articolo"}
</div>
) : (
articles.map((article) => (
filteredArticles.map((article) => (
<ArticleCard
key={article.id}
article={article}
giacenzaMagazzino={selectedStabilimento ? giacenze[article.id] : undefined}
magazzinoNome={magazzinoNome}
onEdit={handleEdit}
onDelete={handleDelete}
/>
@ -138,7 +194,7 @@ export default function ArticlesList({ stabilimento }: ArticlesListProps) {
<ArticleModal
article={editingArticle}
families={families}
stabilimentoId={stabilimento.id}
stabilimentoId={selectedStabilimento?.id || 0}
onClose={() => { setShowModal(false); setEditingArticle(null); }}
onSave={handleSave}
/>

View file

@ -1,12 +1,6 @@
"use client";
import type { Stabilimento } from "@/src/types/stabilimento";
interface MacchineTreeProps {
stabilimento: Stabilimento;
}
export default function MacchineTree({ stabilimento }: MacchineTreeProps) {
export default function MacchineTree() {
return (
<div className="flex flex-col h-full">
{/* Header */}

View file

@ -0,0 +1,161 @@
"use client";
import { useState, useEffect } from "react";
import type { Stabilimento, Magazzino } from "@/src/types/stabilimento";
interface Selection {
stabilimento: Stabilimento | null;
magazzino: Magazzino | null;
}
interface StabilimentoMagazzinoSelectorProps {
onSelectionChange: (selection: Selection) => void;
}
export default function StabilimentoMagazzinoSelector({
onSelectionChange,
}: StabilimentoMagazzinoSelectorProps) {
const [stabilimenti, setStabilimenti] = useState<Stabilimento[]>([]);
const [magazzini, setMagazzini] = useState<Magazzino[]>([]);
const [selectedStabilimento, setSelectedStabilimento] = useState<Stabilimento | null>(null);
const [selectedMagazzino, setSelectedMagazzino] = useState<Magazzino | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function fetchData() {
try {
const [stabRes, magRes] = await Promise.all([
fetch("/api/stabilimenti"),
fetch("/api/magazzini"),
]);
if (stabRes.ok && magRes.ok) {
const stabData = await stabRes.json();
const magData = await magRes.json();
setStabilimenti(stabData.stabilimenti);
setMagazzini(magData.magazzini);
}
} catch (error) {
console.error("Errore caricamento dati:", error);
} finally {
setLoading(false);
}
}
fetchData();
}, []);
const handleStabilimentoSelect = (stab: Stabilimento | null) => {
setSelectedStabilimento(stab);
setSelectedMagazzino(null);
onSelectionChange({ stabilimento: stab, magazzino: null });
};
const handleMagazzinoSelect = (mag: Magazzino | null) => {
setSelectedMagazzino(mag);
onSelectionChange({ stabilimento: selectedStabilimento, magazzino: mag });
};
const filteredMagazzini = selectedStabilimento
? magazzini.filter((m) => m.id_stabilimento === selectedStabilimento.id)
: [];
if (loading) {
return (
<div className="flex items-center gap-2 text-zinc-500 text-sm">
<svg className="animate-spin h-4 w-4" 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>
);
}
return (
<div className="flex flex-wrap items-center gap-4">
{/* Stabilimenti */}
<div className="flex items-center gap-2">
<span className="text-xs text-zinc-500 uppercase tracking-wider">Stabilimento:</span>
<div className="flex gap-1">
{stabilimenti.map((stab) => (
<button
key={stab.id}
onClick={() => handleStabilimentoSelect(
selectedStabilimento?.id === stab.id ? null : stab
)}
className={`
px-3 py-1.5 text-sm rounded-lg font-medium transition-all
${selectedStabilimento?.id === stab.id
? "bg-blue-600 text-white"
: "bg-zinc-800 text-zinc-400 hover:bg-zinc-700 hover:text-white"
}
`}
>
{stab.descrizione}
</button>
))}
</div>
</div>
{/* Magazzini (visibile solo se stabilimento selezionato) */}
{selectedStabilimento && filteredMagazzini.length > 0 && (
<div className="flex items-center gap-2">
<span className="text-xs text-zinc-500 uppercase tracking-wider">Magazzino:</span>
<div className="flex gap-1">
<button
onClick={() => handleMagazzinoSelect(null)}
className={`
px-3 py-1.5 text-sm rounded-lg font-medium transition-all
${!selectedMagazzino
? "bg-green-600 text-white"
: "bg-zinc-800 text-zinc-400 hover:bg-zinc-700 hover:text-white"
}
`}
>
Tutti
</button>
{filteredMagazzini.map((mag) => (
<button
key={mag.id}
onClick={() => handleMagazzinoSelect(
selectedMagazzino?.id === mag.id ? null : mag
)}
className={`
px-3 py-1.5 text-sm rounded-lg font-medium transition-all
${selectedMagazzino?.id === mag.id
? "bg-green-600 text-white"
: "bg-zinc-800 text-zinc-400 hover:bg-zinc-700 hover:text-white"
}
`}
>
{mag.descrizione}
</button>
))}
</div>
</div>
)}
{/* Info selezione */}
{selectedStabilimento && (
<div className="flex items-center gap-2 ml-auto">
<span className="text-xs text-zinc-600">
{selectedMagazzino
? `Filtro: ${selectedMagazzino.descrizione}`
: `Tutti i magazzini di ${selectedStabilimento.descrizione}`
}
</span>
<button
onClick={() => handleStabilimentoSelect(null)}
className="p-1 text-zinc-500 hover:text-white transition-colors"
title="Reset selezione"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
)}
</div>
);
}

View file

@ -87,14 +87,15 @@ function parseArticleInput(input: Partial<ArticleInput>): ParsedArticle {
}
export interface ListArticlesOptions {
stabilimentoId?: number;
magazzinoId?: number;
famigliaId?: number;
search?: string;
}
export async function listArticles(options: ListArticlesOptions = {}): Promise<Article[]> {
const { stabilimentoId, magazzinoId, famigliaId, search } = options;
const { famigliaId, search } = options;
const params: Record<string, string | number> = {};
const conditions: string[] = [];
let query = `
SELECT
@ -112,7 +113,6 @@ export async function listArticles(options: ListArticlesOptions = {}): Promise<A
FROM articoli a
LEFT JOIN famiglie f ON f.id = a.id_famiglia
LEFT JOIN giacenze g ON g.articolo_id = a.id
LEFT JOIN magazzini m ON m.id = g.id_magazzino
LEFT JOIN (
SELECT idart, SUM(qtain - qtaout) AS quantita
FROM art_macchine_parti
@ -120,19 +120,6 @@ export async function listArticles(options: ListArticlesOptions = {}): Promise<A
) machine_totals ON machine_totals.idart = a.id
`;
const conditions: string[] = [];
const params: Record<string, string | number> = {};
if (stabilimentoId) {
conditions.push("m.id_stabilimento = :stabilimentoId");
params.stabilimentoId = stabilimentoId;
}
if (magazzinoId) {
conditions.push("g.id_magazzino = :magazzinoId");
params.magazzinoId = magazzinoId;
}
if (famigliaId) {
conditions.push("a.id_famiglia = :famigliaId");
params.famigliaId = famigliaId;

122
src/lib/giacenze/index.ts Normal file
View file

@ -0,0 +1,122 @@
import type { RowDataPacket } from "mysql2";
import { db } from "@/src/lib/db";
import type { Giacenza, GiacenzePerArticolo } from "@/src/types/giacenza";
type GiacenzaRow = Giacenza & RowDataPacket;
export async function getGiacenzeByMagazzino(magazzinoId: number): Promise<Giacenza[]> {
const [rows] = await db.execute<GiacenzaRow[]>(
`SELECT
g.id,
g.articolo_id,
g.id_magazzino,
g.quantita,
a.codice AS articolo_codice,
a.descrizione AS articolo_descrizione,
m.descrizione AS magazzino_descrizione,
s.descrizione AS stabilimento_descrizione
FROM giacenze g
LEFT JOIN articoli a ON g.articolo_id = a.id
LEFT JOIN magazzini m ON g.id_magazzino = m.id
LEFT JOIN stabilimenti s ON m.id_stabilimento = s.id
WHERE g.id_magazzino = :magazzinoId
ORDER BY a.descrizione ASC`,
{ magazzinoId }
);
return rows.map((row) => ({
id: row.id,
articolo_id: row.articolo_id,
id_magazzino: row.id_magazzino,
quantita: Number(row.quantita) || 0,
articolo_codice: row.articolo_codice,
articolo_descrizione: row.articolo_descrizione,
magazzino_descrizione: row.magazzino_descrizione,
stabilimento_descrizione: row.stabilimento_descrizione,
}));
}
export async function getGiacenzeByStabilimento(stabilimentoId: number): Promise<Giacenza[]> {
const [rows] = await db.execute<GiacenzaRow[]>(
`SELECT
g.id,
g.articolo_id,
g.id_magazzino,
g.quantita,
a.codice AS articolo_codice,
a.descrizione AS articolo_descrizione,
m.descrizione AS magazzino_descrizione,
s.descrizione AS stabilimento_descrizione
FROM giacenze g
LEFT JOIN articoli a ON g.articolo_id = a.id
LEFT JOIN magazzini m ON g.id_magazzino = m.id
LEFT JOIN stabilimenti s ON m.id_stabilimento = s.id
WHERE m.id_stabilimento = :stabilimentoId
ORDER BY a.descrizione ASC`,
{ stabilimentoId }
);
return rows.map((row) => ({
id: row.id,
articolo_id: row.articolo_id,
id_magazzino: row.id_magazzino,
quantita: Number(row.quantita) || 0,
articolo_codice: row.articolo_codice,
articolo_descrizione: row.articolo_descrizione,
magazzino_descrizione: row.magazzino_descrizione,
stabilimento_descrizione: row.stabilimento_descrizione,
}));
}
export async function getGiacenzeMapByMagazzino(magazzinoId: number): Promise<GiacenzePerArticolo> {
const giacenze = await getGiacenzeByMagazzino(magazzinoId);
const map: GiacenzePerArticolo = {};
for (const g of giacenze) {
map[g.articolo_id] = g.quantita;
}
return map;
}
export async function getGiacenzeMapByStabilimento(stabilimentoId: number): Promise<GiacenzePerArticolo> {
const giacenze = await getGiacenzeByStabilimento(stabilimentoId);
const map: GiacenzePerArticolo = {};
for (const g of giacenze) {
if (!map[g.articolo_id]) {
map[g.articolo_id] = 0;
}
map[g.articolo_id] += g.quantita;
}
return map;
}
export async function getGiacenzaArticolo(articoloId: number): Promise<Giacenza[]> {
const [rows] = await db.execute<GiacenzaRow[]>(
`SELECT
g.id,
g.articolo_id,
g.id_magazzino,
g.quantita,
m.descrizione AS magazzino_descrizione,
s.descrizione AS stabilimento_descrizione
FROM giacenze g
LEFT JOIN magazzini m ON g.id_magazzino = m.id
LEFT JOIN stabilimenti s ON m.id_stabilimento = s.id
WHERE g.articolo_id = :articoloId
ORDER BY s.descrizione, m.descrizione`,
{ articoloId }
);
return rows.map((row) => ({
id: row.id,
articolo_id: row.articolo_id,
id_magazzino: row.id_magazzino,
quantita: Number(row.quantita) || 0,
magazzino_descrizione: row.magazzino_descrizione,
stabilimento_descrizione: row.stabilimento_descrizione,
}));
}

14
src/types/giacenza.ts Normal file
View file

@ -0,0 +1,14 @@
export interface Giacenza {
id: number;
articolo_id: number;
id_magazzino: number;
quantita: number;
articolo_codice?: string;
articolo_descrizione?: string;
magazzino_descrizione?: string;
stabilimento_descrizione?: string;
}
export interface GiacenzePerArticolo {
[articoloId: number]: number;
}