selettore stabilimento unico per macchine e articoli
This commit is contained in:
parent
50c18f0913
commit
c66ee44f36
6 changed files with 252 additions and 146 deletions
|
|
@ -3,6 +3,7 @@ import { redirect } from "next/navigation";
|
|||
import { auth } from "@/src/auth";
|
||||
import SignOutButton from "@/src/components/auth/SignOutButton";
|
||||
import Navbar from "@/src/components/Navbar";
|
||||
import { StabilimentoProvider } from "@/src/contexts/StabilimentoContext";
|
||||
|
||||
export default async function MainLayout({
|
||||
children,
|
||||
|
|
@ -17,9 +18,11 @@ export default async function MainLayout({
|
|||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Navbar actions={<SignOutButton />} />
|
||||
<main>{children}</main>
|
||||
</div>
|
||||
<StabilimentoProvider>
|
||||
<div>
|
||||
<Navbar actions={<SignOutButton />} />
|
||||
<main>{children}</main>
|
||||
</div>
|
||||
</StabilimentoProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,13 +2,12 @@
|
|||
|
||||
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 type { TipoMovimento } from "@/src/types/movimento";
|
||||
import ArticleCard from "./ArticleCard";
|
||||
import ArticleModal from "./ArticleModal";
|
||||
import MovimentoModal from "./MovimentoModal";
|
||||
import StabilimentoMagazzinoSelector from "./StabilimentoMagazzinoSelector";
|
||||
import { useStabilimento } from "@/src/contexts/StabilimentoContext";
|
||||
|
||||
export default function ArticlesList() {
|
||||
const [articles, setArticles] = useState<Article[]>([]);
|
||||
|
|
@ -25,8 +24,7 @@ export default function ArticlesList() {
|
|||
const [movimentoArticle, setMovimentoArticle] = useState<Article | null>(null);
|
||||
const [movimentoTipo, setMovimentoTipo] = useState<TipoMovimento>("carico");
|
||||
|
||||
const [selectedStabilimento, setSelectedStabilimento] = useState<Stabilimento | null>(null);
|
||||
const [selectedMagazzino, setSelectedMagazzino] = useState<Magazzino | null>(null);
|
||||
const { selectedStabilimento, selectedMagazzino } = useStabilimento();
|
||||
|
||||
const fetchArticles = useCallback(async () => {
|
||||
setLoading(true);
|
||||
|
|
@ -79,14 +77,6 @@ export default function ArticlesList() {
|
|||
fetchGiacenze();
|
||||
}, [fetchGiacenze]);
|
||||
|
||||
const handleSelectionChange = ({ stabilimento, magazzino }: {
|
||||
stabilimento: Stabilimento | null;
|
||||
magazzino: Magazzino | null
|
||||
}) => {
|
||||
setSelectedStabilimento(stabilimento);
|
||||
setSelectedMagazzino(magazzino);
|
||||
};
|
||||
|
||||
const handleEdit = (article: Article) => {
|
||||
setEditingArticle(article);
|
||||
setShowModal(true);
|
||||
|
|
@ -156,10 +146,15 @@ export default function ArticlesList() {
|
|||
</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>
|
||||
{/* 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">
|
||||
|
|
|
|||
105
src/components/GlobalStabilimentoSelector.tsx
Normal file
105
src/components/GlobalStabilimentoSelector.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -2,18 +2,17 @@
|
|||
|
||||
import { useState, useEffect, useCallback, useMemo } from "react";
|
||||
import type { MacchinaWithDetails, ArticoliPerMacchina } from "@/src/types/macchina";
|
||||
import type { Stabilimento } from "@/src/types/stabilimento";
|
||||
import TreeNode from "./TreeNode";
|
||||
import MacchinaModal from "./MacchinaModal";
|
||||
import { useStabilimento } from "@/src/contexts/StabilimentoContext";
|
||||
|
||||
export default function MacchineTree() {
|
||||
const [nodes, setNodes] = useState<MacchinaWithDetails[]>([]);
|
||||
const [stabilimenti, setStabilimenti] = useState<Stabilimento[]>([]);
|
||||
const [articlesByMacchina, setArticlesByMacchina] = useState<ArticoliPerMacchina>({});
|
||||
const [loading, setLoading] = useState(true);
|
||||
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 [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 () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
await Promise.all([fetchNodes(), fetchStabilimenti()]);
|
||||
await fetchNodes();
|
||||
setLoading(false);
|
||||
}, [fetchNodes, fetchStabilimenti]);
|
||||
}, [fetchNodes]);
|
||||
|
||||
useEffect(() => {
|
||||
loadInitialData();
|
||||
|
|
@ -238,39 +226,12 @@ export default function MacchineTree() {
|
|||
</button>
|
||||
</div>
|
||||
|
||||
{/* Filtro Stabilimento */}
|
||||
<div className="mb-4 p-3 rounded-lg bg-zinc-800/50 border border-zinc-800">
|
||||
<label className="block text-xs font-medium text-zinc-400 mb-2">
|
||||
Filtra per stabilimento
|
||||
</label>
|
||||
<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>
|
||||
))}
|
||||
{/* 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 attivo: {selectedStabilimento.descrizione}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Indicatore clonazione */}
|
||||
{isCloning && (
|
||||
|
|
|
|||
|
|
@ -7,46 +7,26 @@
|
|||
"use client"
|
||||
import { useState , MouseEvent, ReactNode} from "react";
|
||||
import Link from "next/link";
|
||||
import GlobalStabilimentoSelector from "./GlobalStabilimentoSelector";
|
||||
|
||||
interface NavLink {
|
||||
href: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface ServiceLink {
|
||||
href: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
const navLinks: NavLink[] = [
|
||||
{ href: "/", label: "Home"},
|
||||
{ href: "/movimenti", label: "Movimenti"},
|
||||
{ href: "/articoli", label: "Articoli"},
|
||||
{ href: "/macchine", label: "Macchine"},
|
||||
{ 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 }) {
|
||||
|
||||
const [ isServicesOpen, setIsServicesOpen] = useState<boolean>(false);
|
||||
|
||||
const toggleServices = ( e: MouseEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault();
|
||||
setIsServicesOpen(!isServicesOpen);
|
||||
}
|
||||
return (
|
||||
<nav className="border-b border-zinc-800 bg-black">
|
||||
<div className="max-w-4xl mx-auto px-4">
|
||||
<div className="flex justify-between h-16">
|
||||
|
||||
return (
|
||||
<nav className="border-b border-zinc-800 bg-black">
|
||||
<div className="max-w-7xl mx-auto px-4">
|
||||
<div className="flex items-center justify-between h-14">
|
||||
{/* Logo */}
|
||||
<Link
|
||||
href="/"
|
||||
|
|
@ -55,69 +35,25 @@ export default function Navbar({ actions }: { actions?: ReactNode }) {
|
|||
LOGO
|
||||
</Link>
|
||||
|
||||
{/* Navigation */}
|
||||
<div className="flex items-center space-x-8">
|
||||
|
||||
{/* Link normali */}
|
||||
{/* Navigation Links */}
|
||||
<div className="flex items-center space-x-6">
|
||||
{navLinks.map((link) => (
|
||||
<Link
|
||||
key={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>
|
||||
))}
|
||||
|
||||
{/* 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>
|
||||
|
||||
{/* Stabilimento Selector */}
|
||||
<div className="flex-1 flex justify-center px-4">
|
||||
<GlobalStabilimentoSelector />
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
{actions ? (
|
||||
<div className="flex items-center">
|
||||
{actions}
|
||||
|
|
|
|||
106
src/contexts/StabilimentoContext.tsx
Normal file
106
src/contexts/StabilimentoContext.tsx
Normal 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;
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue