selettore stabilimento unico per macchine e articoli
All checks were successful
Deploy to VPS / build (push) Successful in 38s
Deploy to VPS / deploy (push) Successful in 28s

This commit is contained in:
AV77web 2026-05-29 16:08:27 +02:00
parent 50c18f0913
commit c66ee44f36
6 changed files with 252 additions and 146 deletions

View file

@ -3,6 +3,7 @@ import { redirect } from "next/navigation";
import { auth } from "@/src/auth"; import { auth } from "@/src/auth";
import SignOutButton from "@/src/components/auth/SignOutButton"; import SignOutButton from "@/src/components/auth/SignOutButton";
import Navbar from "@/src/components/Navbar"; import Navbar from "@/src/components/Navbar";
import { StabilimentoProvider } from "@/src/contexts/StabilimentoContext";
export default async function MainLayout({ export default async function MainLayout({
children, children,
@ -17,9 +18,11 @@ export default async function MainLayout({
} }
return ( return (
<StabilimentoProvider>
<div> <div>
<Navbar actions={<SignOutButton />} /> <Navbar actions={<SignOutButton />} />
<main>{children}</main> <main>{children}</main>
</div> </div>
</StabilimentoProvider>
); );
} }

View file

@ -2,13 +2,12 @@
import { useState, useEffect, useCallback } from "react"; import { useState, useEffect, useCallback } from "react";
import type { Article, ArticleFamily } from "@/src/types/article"; import type { Article, ArticleFamily } from "@/src/types/article";
import type { Stabilimento, Magazzino } from "@/src/types/stabilimento";
import type { GiacenzePerArticolo } from "@/src/types/giacenza"; import type { GiacenzePerArticolo } from "@/src/types/giacenza";
import type { TipoMovimento } from "@/src/types/movimento"; import type { TipoMovimento } from "@/src/types/movimento";
import ArticleCard from "./ArticleCard"; import ArticleCard from "./ArticleCard";
import ArticleModal from "./ArticleModal"; import ArticleModal from "./ArticleModal";
import MovimentoModal from "./MovimentoModal"; import MovimentoModal from "./MovimentoModal";
import StabilimentoMagazzinoSelector from "./StabilimentoMagazzinoSelector"; import { useStabilimento } from "@/src/contexts/StabilimentoContext";
export default function ArticlesList() { export default function ArticlesList() {
const [articles, setArticles] = useState<Article[]>([]); const [articles, setArticles] = useState<Article[]>([]);
@ -25,8 +24,7 @@ export default function ArticlesList() {
const [movimentoArticle, setMovimentoArticle] = useState<Article | null>(null); const [movimentoArticle, setMovimentoArticle] = useState<Article | null>(null);
const [movimentoTipo, setMovimentoTipo] = useState<TipoMovimento>("carico"); const [movimentoTipo, setMovimentoTipo] = useState<TipoMovimento>("carico");
const [selectedStabilimento, setSelectedStabilimento] = useState<Stabilimento | null>(null); const { selectedStabilimento, selectedMagazzino } = useStabilimento();
const [selectedMagazzino, setSelectedMagazzino] = useState<Magazzino | null>(null);
const fetchArticles = useCallback(async () => { const fetchArticles = useCallback(async () => {
setLoading(true); setLoading(true);
@ -79,14 +77,6 @@ export default function ArticlesList() {
fetchGiacenze(); fetchGiacenze();
}, [fetchGiacenze]); }, [fetchGiacenze]);
const handleSelectionChange = ({ stabilimento, magazzino }: {
stabilimento: Stabilimento | null;
magazzino: Magazzino | null
}) => {
setSelectedStabilimento(stabilimento);
setSelectedMagazzino(magazzino);
};
const handleEdit = (article: Article) => { const handleEdit = (article: Article) => {
setEditingArticle(article); setEditingArticle(article);
setShowModal(true); setShowModal(true);
@ -156,10 +146,15 @@ export default function ArticlesList() {
</button> </button>
</div> </div>
{/* Selettore Stabilimento/Magazzino */} {/* Info selezione corrente */}
<div className="mb-4 p-3 rounded-lg bg-zinc-800/50 border border-zinc-800"> {selectedStabilimento && (
<StabilimentoMagazzinoSelector onSelectionChange={handleSelectionChange} /> <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> </div>
)}
{/* Ricerca */} {/* Ricerca */}
<div className="relative mb-4"> <div className="relative mb-4">

View file

@ -0,0 +1,105 @@
"use client";
import { useStabilimento } from "@/src/contexts/StabilimentoContext";
export default function GlobalStabilimentoSelector() {
const {
stabilimenti,
selectedStabilimento,
selectedMagazzino,
filteredMagazzini,
loading,
setSelectedStabilimento,
setSelectedMagazzino,
resetSelection,
} = useStabilimento();
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>
</div>
);
}
return (
<div className="flex items-center gap-3">
{/* Stabilimenti */}
<div className="flex items-center gap-1.5">
<button
onClick={() => resetSelection()}
className={`
px-2.5 py-1 text-xs rounded-md font-medium transition-all
${!selectedStabilimento
? "bg-blue-600 text-white"
: "bg-zinc-800 text-zinc-400 hover:bg-zinc-700 hover:text-white"
}
`}
>
Tutti
</button>
{stabilimenti.map((stab) => (
<button
key={stab.id}
onClick={() => setSelectedStabilimento(
selectedStabilimento?.id === stab.id ? null : stab
)}
className={`
px-2.5 py-1 text-xs rounded-md 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>
{/* Separatore */}
{selectedStabilimento && filteredMagazzini.length > 0 && (
<div className="w-px h-5 bg-zinc-700" />
)}
{/* Magazzini */}
{selectedStabilimento && filteredMagazzini.length > 0 && (
<div className="flex items-center gap-1.5">
<span className="text-xs text-zinc-500 mr-1">Mag:</span>
<button
onClick={() => setSelectedMagazzino(null)}
className={`
px-2.5 py-1 text-xs rounded-md 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={() => setSelectedMagazzino(
selectedMagazzino?.id === mag.id ? null : mag
)}
className={`
px-2.5 py-1 text-xs rounded-md 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>
);
}

View file

@ -2,18 +2,17 @@
import { useState, useEffect, useCallback, useMemo } from "react"; import { useState, useEffect, useCallback, useMemo } from "react";
import type { MacchinaWithDetails, ArticoliPerMacchina } from "@/src/types/macchina"; import type { MacchinaWithDetails, ArticoliPerMacchina } from "@/src/types/macchina";
import type { Stabilimento } from "@/src/types/stabilimento";
import TreeNode from "./TreeNode"; import TreeNode from "./TreeNode";
import MacchinaModal from "./MacchinaModal"; import MacchinaModal from "./MacchinaModal";
import { useStabilimento } from "@/src/contexts/StabilimentoContext";
export default function MacchineTree() { export default function MacchineTree() {
const [nodes, setNodes] = useState<MacchinaWithDetails[]>([]); const [nodes, setNodes] = useState<MacchinaWithDetails[]>([]);
const [stabilimenti, setStabilimenti] = useState<Stabilimento[]>([]);
const [articlesByMacchina, setArticlesByMacchina] = useState<ArticoliPerMacchina>({}); const [articlesByMacchina, setArticlesByMacchina] = useState<ArticoliPerMacchina>({});
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [selectedStabilimento, setSelectedStabilimento] = useState<Stabilimento | null>(null); const { selectedStabilimento, stabilimenti } = useStabilimento();
const [copiedNodeId, setCopiedNodeId] = useState<number | null>(null); const [copiedNodeId, setCopiedNodeId] = useState<number | null>(null);
const [isCloning, setIsCloning] = useState(false); const [isCloning, setIsCloning] = useState(false);
@ -38,23 +37,12 @@ export default function MacchineTree() {
} }
}, []); }, []);
const fetchStabilimenti = useCallback(async () => {
try {
const res = await fetch("/api/stabilimenti");
if (!res.ok) throw new Error("Errore nel caricamento stabilimenti");
const data = await res.json();
setStabilimenti(data.stabilimenti || []);
} catch (err) {
console.error("Errore stabilimenti:", err);
}
}, []);
const loadInitialData = useCallback(async () => { const loadInitialData = useCallback(async () => {
setLoading(true); setLoading(true);
setError(null); setError(null);
await Promise.all([fetchNodes(), fetchStabilimenti()]); await fetchNodes();
setLoading(false); setLoading(false);
}, [fetchNodes, fetchStabilimenti]); }, [fetchNodes]);
useEffect(() => { useEffect(() => {
loadInitialData(); loadInitialData();
@ -238,39 +226,12 @@ export default function MacchineTree() {
</button> </button>
</div> </div>
{/* Filtro Stabilimento */} {/* Info selezione corrente */}
<div className="mb-4 p-3 rounded-lg bg-zinc-800/50 border border-zinc-800"> {selectedStabilimento && (
<label className="block text-xs font-medium text-zinc-400 mb-2"> <div className="mb-3 px-3 py-2 rounded-lg bg-blue-900/20 border border-blue-700/30 text-xs text-blue-300">
Filtra per stabilimento Filtro attivo: {selectedStabilimento.descrizione}
</label> </div>
<div className="flex flex-wrap gap-2">
<button
onClick={() => setSelectedStabilimento(null)}
className={`px-3 py-1.5 text-xs font-medium rounded-lg transition-all
${!selectedStabilimento
? "bg-blue-600 text-white"
: "bg-zinc-700 text-zinc-300 hover:bg-zinc-600"
}`}
>
Tutti
</button>
{stabilimenti.map((stab) => (
<button
key={stab.id}
onClick={() => setSelectedStabilimento(
selectedStabilimento?.id === stab.id ? null : stab
)} )}
className={`px-3 py-1.5 text-xs font-medium rounded-lg transition-all
${selectedStabilimento?.id === stab.id
? "bg-blue-600 text-white"
: "bg-zinc-700 text-zinc-300 hover:bg-zinc-600"
}`}
>
{stab.descrizione}
</button>
))}
</div>
</div>
{/* Indicatore clonazione */} {/* Indicatore clonazione */}
{isCloning && ( {isCloning && (

View file

@ -7,46 +7,26 @@
"use client" "use client"
import { useState , MouseEvent, ReactNode} from "react"; import { useState , MouseEvent, ReactNode} from "react";
import Link from "next/link"; import Link from "next/link";
import GlobalStabilimentoSelector from "./GlobalStabilimentoSelector";
interface NavLink { interface NavLink {
href: string; href: string;
label: string; label: string;
} }
interface ServiceLink {
href: string;
label: string;
}
const navLinks: NavLink[] = [ const navLinks: NavLink[] = [
{ href: "/", label: "Home"}, { href: "/", label: "Home"},
{ href: "/movimenti", label: "Movimenti"}, { href: "/movimenti", label: "Movimenti"},
{ href: "/articoli", label: "Articoli"}, { href: "/articoli", label: "Articoli"},
{ href: "/macchine", label: "Macchine"}, { href: "/macchine", label: "Macchine"},
{ href: "/utenti", label: "Utenti"}, { href: "/utenti", label: "Utenti"},
]; ];
const serviceLinks: ServiceLink[] = [
{ href: "/services/web", label: "Web Development"},
{ href: "/services/mobile", label: "Mobile Development"},
{ href: "/services/consulting", label: "Consulting"},
]
export default function Navbar({ actions }: { actions?: ReactNode }) { export default function Navbar({ actions }: { actions?: ReactNode }) {
const [ isServicesOpen, setIsServicesOpen] = useState<boolean>(false);
const toggleServices = ( e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
setIsServicesOpen(!isServicesOpen);
}
return ( return (
<nav className="border-b border-zinc-800 bg-black"> <nav className="border-b border-zinc-800 bg-black">
<div className="max-w-4xl mx-auto px-4"> <div className="max-w-7xl mx-auto px-4">
<div className="flex justify-between h-16"> <div className="flex items-center justify-between h-14">
{/* Logo */} {/* Logo */}
<Link <Link
href="/" href="/"
@ -55,69 +35,25 @@ export default function Navbar({ actions }: { actions?: ReactNode }) {
LOGO LOGO
</Link> </Link>
{/* Navigation */} {/* Navigation Links */}
<div className="flex items-center space-x-8"> <div className="flex items-center space-x-6">
{/* Link normali */}
{navLinks.map((link) => ( {navLinks.map((link) => (
<Link <Link
key={link.href} key={link.href}
href={link.href} href={link.href}
className="flex items-center text-zinc-300 hover:text-zinc-100 transition-colors" className="text-sm text-zinc-300 hover:text-zinc-100 transition-colors"
> >
{link.label} {link.label}
</Link> </Link>
))} ))}
{/* Dropdown menu */}
<div className="relative flex items-center">
<button
className="flex items-center text-zinc-300 hover:text-zinc-100 transition-colors"
onClick={toggleServices}
aria-expanded={isServicesOpen}
aria-label="Toggle services menu"
>
Services
<svg
className={`ml-1 h-4 w-4 transition-transform duration-300 ${isServicesOpen ? "rotate-180" : ""
}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 9l-7 7-7-7"
/>
</svg>
</button>
{/* Menu dropdown */}
{isServicesOpen && (
<div
className="absolute top-full mt-2 w-48 bg-zinc-800 rounded-md shadow-lg py-1 border border-zinc-700"
role="menu"
aria-orientation="vertical"
>
{serviceLinks.map((service) => (
<Link
key={service.href}
href={service.href}
className="block px-4 py-2 text-zinc-300 hover:bg-zinc-700 hover:text-zinc-100 transition-colors"
>
{service.label}
</Link>
))}
</div>
)}
</div>
</div> </div>
{/* Stabilimento Selector */}
<div className="flex-1 flex justify-center px-4">
<GlobalStabilimentoSelector />
</div>
{/* Actions */}
{actions ? ( {actions ? (
<div className="flex items-center"> <div className="flex items-center">
{actions} {actions}

View file

@ -0,0 +1,106 @@
"use client";
import {
createContext,
useContext,
useState,
useEffect,
useCallback,
type ReactNode,
} from "react";
import type { Stabilimento, Magazzino } from "@/src/types/stabilimento";
interface StabilimentoContextValue {
stabilimenti: Stabilimento[];
magazzini: Magazzino[];
selectedStabilimento: Stabilimento | null;
selectedMagazzino: Magazzino | null;
loading: boolean;
setSelectedStabilimento: (stabilimento: Stabilimento | null) => void;
setSelectedMagazzino: (magazzino: Magazzino | null) => void;
filteredMagazzini: Magazzino[];
resetSelection: () => void;
}
const StabilimentoContext = createContext<StabilimentoContextValue | null>(null);
export function StabilimentoProvider({ children }: { children: ReactNode }) {
const [stabilimenti, setStabilimenti] = useState<Stabilimento[]>([]);
const [magazzini, setMagazzini] = useState<Magazzino[]>([]);
const [selectedStabilimento, setSelectedStabilimentoState] = useState<Stabilimento | null>(null);
const [selectedMagazzino, setSelectedMagazzinoState] = 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) {
const stabData = await stabRes.json();
setStabilimenti(stabData.stabilimenti || []);
}
if (magRes.ok) {
const magData = await magRes.json();
setMagazzini(magData.magazzini || []);
}
} catch (error) {
console.error("Errore caricamento stabilimenti/magazzini:", error);
} finally {
setLoading(false);
}
}
fetchData();
}, []);
const setSelectedStabilimento = useCallback((stabilimento: Stabilimento | null) => {
setSelectedStabilimentoState(stabilimento);
setSelectedMagazzinoState(null);
}, []);
const setSelectedMagazzino = useCallback((magazzino: Magazzino | null) => {
setSelectedMagazzinoState(magazzino);
}, []);
const resetSelection = useCallback(() => {
setSelectedStabilimentoState(null);
setSelectedMagazzinoState(null);
}, []);
const filteredMagazzini = selectedStabilimento
? magazzini.filter((m) => m.id_stabilimento === selectedStabilimento.id)
: [];
return (
<StabilimentoContext.Provider
value={{
stabilimenti,
magazzini,
selectedStabilimento,
selectedMagazzino,
loading,
setSelectedStabilimento,
setSelectedMagazzino,
filteredMagazzini,
resetSelection,
}}
>
{children}
</StabilimentoContext.Provider>
);
}
export function useStabilimento() {
const context = useContext(StabilimentoContext);
if (!context) {
throw new Error("useStabilimento deve essere usato dentro StabilimentoProvider");
}
return context;
}