239 lines
8.3 KiB
TypeScript
239 lines
8.3 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect, useCallback } from "react";
|
|
import type { Article, ArticleFamily } from "@/src/types/article";
|
|
import type { GiacenzePerArticolo } from "@/src/types/giacenza";
|
|
import type { TipoMovimento } from "@/src/types/movimento";
|
|
import ArticleCard from "./ArticleCard";
|
|
import ArticleModal from "./ArticleModal";
|
|
import MovimentoModal from "./MovimentoModal";
|
|
import { useStabilimento } from "@/src/contexts/StabilimentoContext";
|
|
|
|
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);
|
|
|
|
const [search, setSearch] = useState("");
|
|
const [showModal, setShowModal] = useState(false);
|
|
const [editingArticle, setEditingArticle] = useState<Article | null>(null);
|
|
|
|
const [showMovimentoModal, setShowMovimentoModal] = useState(false);
|
|
const [movimentoArticle, setMovimentoArticle] = useState<Article | null>(null);
|
|
const [movimentoTipo, setMovimentoTipo] = useState<TipoMovimento>("carico");
|
|
|
|
const { selectedStabilimento, selectedMagazzino } = useStabilimento();
|
|
|
|
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]);
|
|
|
|
useEffect(() => {
|
|
const handleArticoliUpdated = () => {
|
|
fetchArticles();
|
|
};
|
|
|
|
window.addEventListener("articoli-updated", handleArticoliUpdated);
|
|
return () => {
|
|
window.removeEventListener("articoli-updated", handleArticoliUpdated);
|
|
};
|
|
}, [fetchArticles]);
|
|
|
|
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 handleMovimento = (article: Article, tipo: TipoMovimento) => {
|
|
setMovimentoArticle(article);
|
|
setMovimentoTipo(tipo);
|
|
setShowMovimentoModal(true);
|
|
};
|
|
|
|
const handleMovimentoSuccess = () => {
|
|
setShowMovimentoModal(false);
|
|
setMovimentoArticle(null);
|
|
fetchArticles();
|
|
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">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between gap-4 mb-4">
|
|
<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 ml-1">({sottoScorta} sotto scorta)</span>}
|
|
</p>
|
|
</div>
|
|
<button
|
|
onClick={() => { setEditingArticle(null); setShowModal(true); }}
|
|
className="p-2 rounded-lg bg-blue-600 text-white hover:bg-blue-500 transition-colors"
|
|
title="Nuovo articolo"
|
|
>
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
{/* Info selezione corrente */}
|
|
{selectedStabilimento && (
|
|
<div className="mb-3 px-3 py-2 rounded-lg bg-blue-900/20 border border-blue-700/30 text-xs text-blue-300">
|
|
Filtro: {selectedMagazzino
|
|
? `${selectedStabilimento.descrizione} → ${selectedMagazzino.descrizione}`
|
|
: `${selectedStabilimento.descrizione} (tutti i magazzini)`
|
|
}
|
|
</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">
|
|
<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-9 pr-3 py-2 text-sm rounded-lg bg-zinc-900 border border-zinc-800 text-white placeholder-zinc-500 focus:outline-none focus:border-blue-500"
|
|
/>
|
|
</div>
|
|
|
|
{/* Lista articoli */}
|
|
<div className="flex-1 overflow-y-auto space-y-2 pr-1">
|
|
{loading ? (
|
|
<div className="flex items-center justify-center py-8 text-zinc-500 text-sm">
|
|
<svg className="w-5 h-5 animate-spin mr-2" 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>
|
|
) : error ? (
|
|
<div className="text-center py-8">
|
|
<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>
|
|
) : filteredArticles.length === 0 ? (
|
|
<div className="text-center py-8 text-zinc-500 text-sm">
|
|
{search ? "Nessun articolo trovato" : "Nessun articolo"}
|
|
</div>
|
|
) : (
|
|
filteredArticles.map((article) => (
|
|
<ArticleCard
|
|
key={article.id}
|
|
article={article}
|
|
giacenzaMagazzino={selectedStabilimento ? giacenze[article.id] : undefined}
|
|
magazzinoNome={magazzinoNome}
|
|
onEdit={handleEdit}
|
|
onDelete={handleDelete}
|
|
onMovimento={handleMovimento}
|
|
/>
|
|
))
|
|
)}
|
|
</div>
|
|
|
|
{/* Modal Articolo */}
|
|
{showModal && (
|
|
<ArticleModal
|
|
article={editingArticle}
|
|
families={families}
|
|
onClose={() => { setShowModal(false); setEditingArticle(null); }}
|
|
onSave={handleSave}
|
|
/>
|
|
)}
|
|
|
|
{/* Modal Movimento */}
|
|
{showMovimentoModal && movimentoArticle && (
|
|
<MovimentoModal
|
|
article={movimentoArticle}
|
|
tipo={movimentoTipo}
|
|
onClose={() => { setShowMovimentoModal(false); setMovimentoArticle(null); }}
|
|
onSuccess={handleMovimentoSuccess}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|