correzione ArticleCard, creazione MacchineTree
This commit is contained in:
parent
33ecedfe33
commit
be228bca5c
5 changed files with 326 additions and 177 deletions
|
|
@ -1,21 +1,56 @@
|
||||||
export default function ArticoliPage() {
|
"use client";
|
||||||
return (
|
|
||||||
<div className="flex flex-1 flex-col bg-zinc-50 p-6 dark:bg-black">
|
|
||||||
<div className="mx-auto w-full max-w-6xl">
|
|
||||||
<header className="mb-8">
|
|
||||||
<h1 className="text-3xl font-semibold text-zinc-950 dark:text-zinc-50">
|
|
||||||
Articoli
|
|
||||||
</h1>
|
|
||||||
<p className="mt-2 text-zinc-600 dark:text-zinc-400">
|
|
||||||
Catalogo ricambi e componenti
|
|
||||||
</p>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div className="rounded-lg border border-zinc-200 bg-white p-6 shadow-sm dark:border-zinc-800 dark:bg-zinc-900">
|
import { useState } from "react";
|
||||||
<p className="text-zinc-600 dark:text-zinc-400">
|
import type { Stabilimento } from "@/src/types/stabilimento";
|
||||||
Contenuto della pagina articoli in arrivo...
|
import StabilimentoSelector from "@/src/components/StabilimentoSelector";
|
||||||
</p>
|
import ArticlesGrid from "@/src/components/ArticlesGrid";
|
||||||
|
|
||||||
|
export default function ArticoliPage() {
|
||||||
|
const [selectedStabilimento, setSelectedStabilimento] = useState<Stabilimento | null>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-black">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 py-6">
|
||||||
|
{/* 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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Selezione stabilimento */}
|
||||||
|
<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}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</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">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -3,72 +3,70 @@
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import type { Stabilimento } from "@/src/types/stabilimento";
|
import type { Stabilimento } from "@/src/types/stabilimento";
|
||||||
import StabilimentoSelector from "@/src/components/StabilimentoSelector";
|
import StabilimentoSelector from "@/src/components/StabilimentoSelector";
|
||||||
import ArticlesGrid from "@/src/components/ArticlesGrid";
|
import ArticlesList from "@/src/components/ArticlesList";
|
||||||
|
import MacchineTree from "@/src/components/MacchineTree";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const [selectedStabilimento, setSelectedStabilimento] = useState<Stabilimento | null>(null);
|
const [selectedStabilimento, setSelectedStabilimento] = useState<Stabilimento | null>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-black">
|
<div className="flex flex-col h-[calc(100vh-64px)] bg-black">
|
||||||
<div className="max-w-7xl mx-auto px-4 py-8">
|
<div className="flex-1 flex flex-col max-w-7xl mx-auto w-full px-4 py-6 overflow-hidden">
|
||||||
{/* Header */}
|
{/* Header + Selezione stabilimento */}
|
||||||
<div className="mb-8">
|
<div className="mb-6 flex-shrink-0">
|
||||||
<h1 className="text-3xl font-bold text-white mb-2">
|
<div className="flex items-center justify-between mb-4">
|
||||||
MagRicambi
|
<div>
|
||||||
</h1>
|
<h1 className="text-2xl font-bold text-white">MagRicambi</h1>
|
||||||
<p className="text-zinc-400">
|
<p className="text-sm text-zinc-500">Gestione magazzino ricambi</p>
|
||||||
Gestione magazzino ricambi
|
</div>
|
||||||
</p>
|
{selectedStabilimento && (
|
||||||
</div>
|
<div className="flex items-center gap-2 text-sm">
|
||||||
|
<span className="text-zinc-500">Stabilimento:</span>
|
||||||
{/* Selezione stabilimento */}
|
<span className="font-medium text-white">{selectedStabilimento.descrizione}</span>
|
||||||
<div className="mb-8">
|
<button
|
||||||
<h2 className="text-lg font-medium text-zinc-300 mb-4">
|
onClick={() => setSelectedStabilimento(null)}
|
||||||
Seleziona stabilimento
|
className="text-zinc-500 hover:text-white transition-colors"
|
||||||
</h2>
|
>
|
||||||
<StabilimentoSelector
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
selectedId={selectedStabilimento?.id ?? null}
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||||
onSelect={setSelectedStabilimento}
|
</svg>
|
||||||
/>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
{/* Contenuto principale */}
|
</div>
|
||||||
{selectedStabilimento ? (
|
|
||||||
<div>
|
{!selectedStabilimento && (
|
||||||
<div className="flex items-center gap-3 mb-6">
|
<div className="p-4 rounded-lg bg-zinc-900 border border-zinc-800">
|
||||||
<h2 className="text-xl font-semibold text-white">
|
<p className="text-sm text-zinc-400 mb-3">Seleziona uno stabilimento per iniziare:</p>
|
||||||
Articoli - {selectedStabilimento.descrizione}
|
<StabilimentoSelector
|
||||||
</h2>
|
selectedId={null}
|
||||||
<button
|
onSelect={setSelectedStabilimento}
|
||||||
onClick={() => setSelectedStabilimento(null)}
|
/>
|
||||||
className="text-sm text-zinc-500 hover:text-white transition-colors"
|
</div>
|
||||||
>
|
)}
|
||||||
(cambia)
|
</div>
|
||||||
</button>
|
|
||||||
|
{/* 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} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Colonna destra: Macchine */}
|
||||||
|
<div className="flex flex-col min-h-0 p-4 rounded-xl bg-zinc-900/50 border border-zinc-800">
|
||||||
|
<MacchineTree stabilimento={selectedStabilimento} />
|
||||||
</div>
|
</div>
|
||||||
<ArticlesGrid stabilimento={selectedStabilimento} />
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-center justify-center py-20 text-center">
|
<div className="flex-1 flex items-center justify-center">
|
||||||
<svg
|
<div className="text-center">
|
||||||
className="w-20 h-20 text-zinc-800 mb-6"
|
<svg className="w-16 h-16 mx-auto text-zinc-800 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
fill="none"
|
<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" />
|
||||||
stroke="currentColor"
|
</svg>
|
||||||
viewBox="0 0 24 24"
|
<p className="text-zinc-500">Seleziona uno stabilimento per visualizzare articoli e macchine</p>
|
||||||
>
|
</div>
|
||||||
<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>
|
|
||||||
<h3 className="text-xl font-medium text-zinc-400 mb-2">
|
|
||||||
Nessuno stabilimento selezionato
|
|
||||||
</h3>
|
|
||||||
<p className="text-zinc-600 max-w-md">
|
|
||||||
Seleziona uno stabilimento dalla lista sopra per visualizzare e gestire gli articoli del magazzino.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -10,30 +10,21 @@ interface ArticleCardProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ArticleCard({ article, onEdit, onDelete }: ArticleCardProps) {
|
export default function ArticleCard({ article, onEdit, onDelete }: ArticleCardProps) {
|
||||||
const [showActions, setShowActions] = useState(false);
|
|
||||||
const [deleting, setDeleting] = useState(false);
|
const [deleting, setDeleting] = useState(false);
|
||||||
|
|
||||||
const isSottoScorta = article.quantita < article.quantita_minima;
|
const isSottoScorta = article.quantita < article.quantita_minima;
|
||||||
const disponibile = article.quantita - article.assegnata_macchine;
|
const disponibile = article.quantita - article.assegnata_macchine;
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
if (!confirm(`Sei sicuro di voler eliminare l'articolo "${article.descrizione}"?`)) {
|
if (!confirm(`Eliminare "${article.descrizione}"?`)) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setDeleting(true);
|
setDeleting(true);
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/articoli/${article.id}`, {
|
const res = await fetch(`/api/articoli/${article.id}`, { method: "DELETE" });
|
||||||
method: "DELETE",
|
if (!res.ok) throw new Error("Errore durante l'eliminazione");
|
||||||
});
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error("Errore durante l'eliminazione");
|
|
||||||
}
|
|
||||||
|
|
||||||
onDelete(article);
|
onDelete(article);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert(error instanceof Error ? error.message : "Errore durante l'eliminazione");
|
alert(error instanceof Error ? error.message : "Errore");
|
||||||
} finally {
|
} finally {
|
||||||
setDeleting(false);
|
setDeleting(false);
|
||||||
}
|
}
|
||||||
|
|
@ -42,98 +33,44 @@ export default function ArticleCard({ article, onEdit, onDelete }: ArticleCardPr
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`
|
className={`
|
||||||
relative bg-zinc-900 rounded-xl border transition-all duration-200
|
flex items-center gap-3 p-3 rounded-lg border bg-zinc-900 transition-colors hover:bg-zinc-800
|
||||||
hover:shadow-lg hover:shadow-black/20 hover:-translate-y-1
|
|
||||||
${isSottoScorta ? "border-red-500/50" : "border-zinc-800"}
|
${isSottoScorta ? "border-red-500/50" : "border-zinc-800"}
|
||||||
`}
|
`}
|
||||||
onMouseEnter={() => setShowActions(true)}
|
|
||||||
onMouseLeave={() => setShowActions(false)}
|
|
||||||
>
|
>
|
||||||
{/* Header */}
|
{/* Indicatore sotto scorta */}
|
||||||
<div className="p-4 border-b border-zinc-800">
|
{isSottoScorta && (
|
||||||
<div className="flex items-start justify-between gap-2">
|
<div className="w-2 h-2 rounded-full bg-red-500 flex-shrink-0" title="Sotto scorta" />
|
||||||
<div className="flex-1 min-w-0">
|
)}
|
||||||
<h3 className="font-semibold text-white truncate" title={article.descrizione}>
|
|
||||||
{article.descrizione}
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm text-zinc-500 font-mono">{article.codice}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Badge famiglia */}
|
{/* Info articolo */}
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="font-medium text-white truncate text-sm" title={article.descrizione}>
|
||||||
|
{article.descrizione}
|
||||||
|
</span>
|
||||||
{article.famiglia && (
|
{article.famiglia && (
|
||||||
<span className="px-2 py-1 text-xs font-medium rounded-full bg-zinc-800 text-zinc-400 whitespace-nowrap">
|
<span className="px-1.5 py-0.5 text-[10px] rounded bg-zinc-800 text-zinc-500 flex-shrink-0">
|
||||||
{article.famiglia}
|
{article.famiglia}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="flex items-center gap-3 text-xs text-zinc-500 mt-0.5">
|
||||||
|
<span className="font-mono">{article.codice}</span>
|
||||||
{/* Body */}
|
<span>
|
||||||
<div className="p-4 space-y-3">
|
Qta: <span className={isSottoScorta ? "text-red-400" : "text-white"}>{article.quantita}</span>
|
||||||
{/* Quantità */}
|
{isSottoScorta && <span className="text-red-400"> (min {article.quantita_minima})</span>}
|
||||||
<div className="grid grid-cols-2 gap-3">
|
</span>
|
||||||
<div>
|
<span>
|
||||||
<p className="text-xs text-zinc-500 uppercase tracking-wider">Quantità</p>
|
Disp: <span className={disponibile <= 0 ? "text-yellow-400" : "text-green-400"}>{disponibile}</span>
|
||||||
<p className={`text-lg font-bold ${isSottoScorta ? "text-red-400" : "text-white"}`}>
|
</span>
|
||||||
{article.quantita}
|
|
||||||
{isSottoScorta && (
|
|
||||||
<span className="ml-2 text-xs font-normal text-red-400">
|
|
||||||
(min: {article.quantita_minima})
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-xs text-zinc-500 uppercase tracking-wider">Disponibile</p>
|
|
||||||
<p className={`text-lg font-bold ${disponibile <= 0 ? "text-yellow-400" : "text-green-400"}`}>
|
|
||||||
{disponibile}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Assegnate a macchine */}
|
|
||||||
{article.assegnata_macchine > 0 && (
|
|
||||||
<div className="flex items-center gap-2 text-sm">
|
|
||||||
<svg className="w-4 h-4 text-zinc-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
|
||||||
</svg>
|
|
||||||
<span className="text-zinc-400">
|
|
||||||
{article.assegnata_macchine} assegnate a macchine
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Prezzo */}
|
|
||||||
{article.prezzo > 0 && (
|
|
||||||
<div className="flex items-center justify-between pt-2 border-t border-zinc-800">
|
|
||||||
<span className="text-sm text-zinc-500">Prezzo</span>
|
|
||||||
<span className="font-semibold text-white">
|
|
||||||
€{article.prezzo.toFixed(2)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Barcode */}
|
|
||||||
{article.barcode && (
|
|
||||||
<div className="flex items-center gap-2 text-xs text-zinc-500">
|
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v1m6 11h2m-6 0h-2v4m0-11v3m0 0h.01M12 12h4.01M16 20h4M4 12h4m12 0h.01M5 8h2a1 1 0 001-1V5a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1zm12 0h2a1 1 0 001-1V5a1 1 0 00-1-1h-2a1 1 0 00-1 1v2a1 1 0 001 1zM5 20h2a1 1 0 001-1v-2a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1z" />
|
|
||||||
</svg>
|
|
||||||
<span className="font-mono">{article.barcode}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Action buttons (hover) */}
|
{/* Azioni */}
|
||||||
<div
|
<div className="flex items-center gap-1 flex-shrink-0">
|
||||||
className={`
|
|
||||||
absolute top-2 right-2 flex gap-1 transition-opacity duration-200
|
|
||||||
${showActions ? "opacity-100" : "opacity-0"}
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
<button
|
<button
|
||||||
onClick={() => onEdit(article)}
|
onClick={() => onEdit(article)}
|
||||||
className="p-2 rounded-lg bg-zinc-800 text-zinc-400 hover:bg-blue-600 hover:text-white transition-colors"
|
className="p-1.5 rounded text-zinc-500 hover:text-blue-400 hover:bg-zinc-700 transition-colors"
|
||||||
title="Modifica"
|
title="Modifica"
|
||||||
>
|
>
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
|
@ -143,13 +80,13 @@ export default function ArticleCard({ article, onEdit, onDelete }: ArticleCardPr
|
||||||
<button
|
<button
|
||||||
onClick={handleDelete}
|
onClick={handleDelete}
|
||||||
disabled={deleting}
|
disabled={deleting}
|
||||||
className="p-2 rounded-lg bg-zinc-800 text-zinc-400 hover:bg-red-600 hover:text-white transition-colors disabled:opacity-50"
|
className="p-1.5 rounded text-zinc-500 hover:text-red-400 hover:bg-zinc-700 transition-colors disabled:opacity-50"
|
||||||
title="Elimina"
|
title="Elimina"
|
||||||
>
|
>
|
||||||
{deleting ? (
|
{deleting ? (
|
||||||
<svg className="w-4 h-4 animate-spin" viewBox="0 0 24 24">
|
<svg className="w-4 h-4 animate-spin" viewBox="0 0 24 24">
|
||||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" />
|
<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 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||||
</svg>
|
</svg>
|
||||||
) : (
|
) : (
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
|
@ -158,15 +95,6 @@ export default function ArticleCard({ article, onEdit, onDelete }: ArticleCardPr
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Sotto scorta indicator */}
|
|
||||||
{isSottoScorta && (
|
|
||||||
<div className="absolute top-0 left-4 -translate-y-1/2">
|
|
||||||
<span className="px-2 py-0.5 text-xs font-bold rounded-full bg-red-500 text-white shadow-lg">
|
|
||||||
SOTTO SCORTA
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
148
src/components/ArticlesList.tsx
Normal file
148
src/components/ArticlesList.tsx
Normal file
|
|
@ -0,0 +1,148 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect, useCallback } from "react";
|
||||||
|
import type { Article, ArticleFamily } from "@/src/types/article";
|
||||||
|
import type { Stabilimento } from "@/src/types/stabilimento";
|
||||||
|
import ArticleCard from "./ArticleCard";
|
||||||
|
import ArticleModal from "./ArticleModal";
|
||||||
|
|
||||||
|
interface ArticlesListProps {
|
||||||
|
stabilimento: Stabilimento;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ArticlesList({ stabilimento }: ArticlesListProps) {
|
||||||
|
const [articles, setArticles] = useState<Article[]>([]);
|
||||||
|
const [families, setFamilies] = useState<ArticleFamily[]>([]);
|
||||||
|
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 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}`);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}, [stabilimento.id, search]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchArticles();
|
||||||
|
}, [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);
|
||||||
|
};
|
||||||
|
|
||||||
|
const sottoScorta = articles.filter((a) => a.quantita < a.quantita_minima).length;
|
||||||
|
|
||||||
|
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">({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>
|
||||||
|
|
||||||
|
{/* 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>
|
||||||
|
) : articles.length === 0 ? (
|
||||||
|
<div className="text-center py-8 text-zinc-500 text-sm">
|
||||||
|
{search ? "Nessun articolo trovato" : "Nessun articolo in questo stabilimento"}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
articles.map((article) => (
|
||||||
|
<ArticleCard
|
||||||
|
key={article.id}
|
||||||
|
article={article}
|
||||||
|
onEdit={handleEdit}
|
||||||
|
onDelete={handleDelete}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Modal */}
|
||||||
|
{showModal && (
|
||||||
|
<ArticleModal
|
||||||
|
article={editingArticle}
|
||||||
|
families={families}
|
||||||
|
stabilimentoId={stabilimento.id}
|
||||||
|
onClose={() => { setShowModal(false); setEditingArticle(null); }}
|
||||||
|
onSave={handleSave}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
40
src/components/MacchineTree.tsx
Normal file
40
src/components/MacchineTree.tsx
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import type { Stabilimento } from "@/src/types/stabilimento";
|
||||||
|
|
||||||
|
interface MacchineTreeProps {
|
||||||
|
stabilimento: Stabilimento;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MacchineTree({ stabilimento }: MacchineTreeProps) {
|
||||||
|
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">Macchine</h2>
|
||||||
|
<p className="text-xs text-zinc-500">Albero macchine e parti</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="p-2 rounded-lg bg-zinc-800 text-zinc-400 hover:bg-zinc-700 hover:text-white transition-colors"
|
||||||
|
title="Nuova macchina"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
{/* Placeholder contenuto */}
|
||||||
|
<div className="flex-1 flex flex-col items-center justify-center text-center p-8 border-2 border-dashed border-zinc-800 rounded-lg">
|
||||||
|
<svg className="w-12 h-12 text-zinc-700 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
||||||
|
</svg>
|
||||||
|
<p className="text-zinc-500 text-sm mb-2">Albero macchine</p>
|
||||||
|
<p className="text-zinc-600 text-xs">
|
||||||
|
Trascina gli articoli qui per assegnarli alle macchine
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue