diff --git a/src/app/(main)/articoli/page.tsx b/src/app/(main)/articoli/page.tsx index 7f91365..dcd7a57 100644 --- a/src/app/(main)/articoli/page.tsx +++ b/src/app/(main)/articoli/page.tsx @@ -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([]); + const [families, setFamilies] = useState([]); + const [giacenze, setGiacenze] = useState({}); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const [search, setSearch] = useState(""); + const [selectedFamily, setSelectedFamily] = useState(null); + const [showModal, setShowModal] = useState(false); + const [editingArticle, setEditingArticle] = useState
(null); + const [selectedStabilimento, setSelectedStabilimento] = useState(null); + const [selectedMagazzino, setSelectedMagazzino] = useState(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 (
@@ -14,42 +124,124 @@ export default function ArticoliPage() { {/* Header */}

Articoli

-

Catalogo ricambi e componenti

+

Catalogo completo ricambi e componenti

- {/* Selezione stabilimento */} -
-
-

Stabilimento:

- {selectedStabilimento && ( - - )} + {/* Statistiche */} +
+
+

Totale Articoli

+

{articles.length}

+
+
+

Famiglie

+

{families.length}

+
+
+

Sotto Scorta

+

{sottoScorta}

+
+
+

Valore Totale

+

€{totaleValore.toFixed(2)}

- {selectedStabilimento ? ( -

{selectedStabilimento.descrizione}

- ) : ( - - )}
- {/* Contenuto */} - {selectedStabilimento ? ( - - ) : ( -
- + {/* Selettore Stabilimento/Magazzino */} +
+ +
+ + {/* Toolbar */} +
+
+ {/* Ricerca */} +
+ + + + 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" + /> +
+ + {/* Filtro famiglia */} + +
+ + {/* Pulsante nuovo */} + +
+ + {/* Lista articoli */} + {loading ? ( +
+ + + + + Caricamento articoli... +
+ ) : error ? ( +
+

{error}

+ +
+ ) : filteredArticles.length === 0 ? ( +
+ -

Seleziona uno stabilimento per visualizzare gli articoli

+

Nessun articolo trovato

+

Prova a modificare i filtri di ricerca

+ ) : ( +
+ {filteredArticles.map((article) => ( + + ))} +
+ )} + + {/* Modal */} + {showModal && ( + { setShowModal(false); setEditingArticle(null); }} + onSave={handleSave} + /> )}
diff --git a/src/app/(main)/page.tsx b/src/app/(main)/page.tsx index b7b7ece..f8b2475 100644 --- a/src/app/(main)/page.tsx +++ b/src/app/(main)/page.tsx @@ -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(null); - return (
- {/* Header + Selezione stabilimento */} + {/* Header */}
-
-
-

MagRicambi

-

Gestione magazzino ricambi

-
- {selectedStabilimento && ( -
- Stabilimento: - {selectedStabilimento.descrizione} - -
- )} -
- - {!selectedStabilimento && ( -
-

Seleziona uno stabilimento per iniziare:

- -
- )} +

MagRicambi

+

Gestione magazzino ricambi

{/* Contenuto principale: split 50/50 */} - {selectedStabilimento ? ( -
- {/* Colonna sinistra: Articoli */} -
- -
+
+ {/* Colonna sinistra: Articoli */} +
+ +
- {/* Colonna destra: Macchine */} -
- -
+ {/* Colonna destra: Macchine (placeholder) */} +
+
- ) : ( -
-
- - - -

Seleziona uno stabilimento per visualizzare articoli e macchine

-
-
- )} +
); diff --git a/src/app/api/giacenze/route.ts b/src/app/api/giacenze/route.ts new file mode 100644 index 0000000..d10e169 --- /dev/null +++ b/src/app/api/giacenze/route.ts @@ -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; + + 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 } + ); + } +} diff --git a/src/components/ArticleCard.tsx b/src/components/ArticleCard.tsx index fa5b15f..106c073 100644 --- a/src/components/ArticleCard.tsx +++ b/src/components/ArticleCard.tsx @@ -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
{article.codice} - - Qta: {article.quantita} - {isSottoScorta && (min {article.quantita_minima})} - - - Disp: {disponibile} - +
+
+ + {/* Quantità */} +
+ {/* Quantità totale */} +
+
Totale
+
+ {article.quantita} + {isSottoScorta && ( + (min {article.quantita_minima}) + )} +
+
+ + {/* Quantità magazzino (se disponibile) */} + {hasGiacenzaMagazzino && ( +
+
+ {magazzinoNome || "Mag."} +
+
0 ? "text-green-400" : "text-white" + }`}> + {giacenzaMagazzino} +
+
+ )} + + {/* Disponibile */} +
+
Disp.
+
+ {disponibile} +
{/* Azioni */} -
+
+ {/* Selettore Stabilimento/Magazzino */} +
+ +
+ {/* Ricerca */}
@@ -117,15 +171,17 @@ export default function ArticlesList({ stabilimento }: ArticlesListProps) {

{error}

- ) : articles.length === 0 ? ( + ) : filteredArticles.length === 0 ? (
- {search ? "Nessun articolo trovato" : "Nessun articolo in questo stabilimento"} + {search ? "Nessun articolo trovato" : "Nessun articolo"}
) : ( - articles.map((article) => ( + filteredArticles.map((article) => ( @@ -138,7 +194,7 @@ export default function ArticlesList({ stabilimento }: ArticlesListProps) { { setShowModal(false); setEditingArticle(null); }} onSave={handleSave} /> diff --git a/src/components/MacchineTree.tsx b/src/components/MacchineTree.tsx index a5018f9..d991136 100644 --- a/src/components/MacchineTree.tsx +++ b/src/components/MacchineTree.tsx @@ -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 (
{/* Header */} diff --git a/src/components/StabilimentoMagazzinoSelector.tsx b/src/components/StabilimentoMagazzinoSelector.tsx new file mode 100644 index 0000000..87a4b48 --- /dev/null +++ b/src/components/StabilimentoMagazzinoSelector.tsx @@ -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([]); + const [magazzini, setMagazzini] = useState([]); + const [selectedStabilimento, setSelectedStabilimento] = useState(null); + const [selectedMagazzino, setSelectedMagazzino] = useState(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 ( +
+ + + + + Caricamento... +
+ ); + } + + return ( +
+ {/* Stabilimenti */} +
+ Stabilimento: +
+ {stabilimenti.map((stab) => ( + + ))} +
+
+ + {/* Magazzini (visibile solo se stabilimento selezionato) */} + {selectedStabilimento && filteredMagazzini.length > 0 && ( +
+ Magazzino: +
+ + {filteredMagazzini.map((mag) => ( + + ))} +
+
+ )} + + {/* Info selezione */} + {selectedStabilimento && ( +
+ + {selectedMagazzino + ? `Filtro: ${selectedMagazzino.descrizione}` + : `Tutti i magazzini di ${selectedStabilimento.descrizione}` + } + + +
+ )} +
+ ); +} diff --git a/src/lib/articles/index.ts b/src/lib/articles/index.ts index 5c27e71..7d9539a 100644 --- a/src/lib/articles/index.ts +++ b/src/lib/articles/index.ts @@ -87,14 +87,15 @@ function parseArticleInput(input: Partial): ParsedArticle { } export interface ListArticlesOptions { - stabilimentoId?: number; - magazzinoId?: number; famigliaId?: number; search?: string; } export async function listArticles(options: ListArticlesOptions = {}): Promise { - const { stabilimentoId, magazzinoId, famigliaId, search } = options; + const { famigliaId, search } = options; + + const params: Record = {}; + const conditions: string[] = []; let query = ` SELECT @@ -112,7 +113,6 @@ export async function listArticles(options: ListArticlesOptions = {}): Promise = {}; - - 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; diff --git a/src/lib/giacenze/index.ts b/src/lib/giacenze/index.ts new file mode 100644 index 0000000..b7f0698 --- /dev/null +++ b/src/lib/giacenze/index.ts @@ -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 { + const [rows] = await db.execute( + `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 { + const [rows] = await db.execute( + `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 { + 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 { + 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 { + const [rows] = await db.execute( + `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, + })); +} diff --git a/src/types/giacenza.ts b/src/types/giacenza.ts new file mode 100644 index 0000000..b2d71a5 --- /dev/null +++ b/src/types/giacenza.ts @@ -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; +}