creati: fujnzioni CRUD per le macchine, le API routes TreeNode e MacchineTree
This commit is contained in:
parent
3e3f05136c
commit
723d1d88b7
8 changed files with 1395 additions and 15 deletions
|
|
@ -1,3 +1,5 @@
|
||||||
|
import MacchineTree from "@/src/components/MacchineTree";
|
||||||
|
|
||||||
export default function MachinePage() {
|
export default function MachinePage() {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-1 flex-col bg-zinc-50 p-6 dark:bg-black">
|
<div className="flex flex-1 flex-col bg-zinc-50 p-6 dark:bg-black">
|
||||||
|
|
@ -11,10 +13,8 @@ export default function MachinePage() {
|
||||||
</p>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="rounded-lg border border-zinc-200 bg-white p-6 shadow-sm dark:border-zinc-800 dark:bg-zinc-900">
|
<div className="rounded-lg border border-zinc-200 bg-white p-6 shadow-sm dark:border-zinc-800 dark:bg-zinc-900 min-h-[500px]">
|
||||||
<p className="text-zinc-600 dark:text-zinc-400">
|
<MacchineTree />
|
||||||
Contenuto della pagina macchine in arrivo...
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
122
src/app/api/macchine/[id]/route.ts
Normal file
122
src/app/api/macchine/[id]/route.ts
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
|
import { auth } from "@/src/auth";
|
||||||
|
import {
|
||||||
|
getMacchinaById,
|
||||||
|
updateMacchina,
|
||||||
|
deleteMacchina,
|
||||||
|
} from "@/src/lib/macchine";
|
||||||
|
import type { MacchinaUpdateInput } from "@/src/types/macchina";
|
||||||
|
|
||||||
|
interface RouteParams {
|
||||||
|
params: Promise<{ id: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET(request: Request, { params }: RouteParams) {
|
||||||
|
const session = await auth();
|
||||||
|
|
||||||
|
if (!session?.user) {
|
||||||
|
return NextResponse.json({ error: "Non autorizzato" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id } = await params;
|
||||||
|
const macchinaId = parseInt(id, 10);
|
||||||
|
|
||||||
|
if (isNaN(macchinaId)) {
|
||||||
|
return NextResponse.json({ error: "ID non valido" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const macchina = await getMacchinaById(macchinaId);
|
||||||
|
|
||||||
|
if (!macchina) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Macchina non trovata" },
|
||||||
|
{ status: 404 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ macchina });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[API macchine/:id] Errore GET:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Errore nel recupero della macchina" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function PUT(request: Request, { params }: RouteParams) {
|
||||||
|
const session = await auth();
|
||||||
|
|
||||||
|
if (!session?.user) {
|
||||||
|
return NextResponse.json({ error: "Non autorizzato" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id } = await params;
|
||||||
|
const macchinaId = parseInt(id, 10);
|
||||||
|
|
||||||
|
if (isNaN(macchinaId)) {
|
||||||
|
return NextResponse.json({ error: "ID non valido" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const body = await request.json();
|
||||||
|
|
||||||
|
const input: MacchinaUpdateInput = {};
|
||||||
|
if (body.nome !== undefined) input.nome = body.nome;
|
||||||
|
if (body.tipo !== undefined) input.tipo = body.tipo;
|
||||||
|
if (body.id_padre !== undefined) input.id_padre = body.id_padre;
|
||||||
|
if (body.id_stabilimento !== undefined) input.id_stabilimento = body.id_stabilimento;
|
||||||
|
if (body.colore_testo !== undefined) input.colore_testo = body.colore_testo;
|
||||||
|
|
||||||
|
const macchina = await updateMacchina(macchinaId, input);
|
||||||
|
|
||||||
|
if (!macchina) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Macchina non trovata" },
|
||||||
|
{ status: 404 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ macchina });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[API macchine/:id] Errore PUT:", error);
|
||||||
|
const message = error instanceof Error ? error.message : "Errore nell'aggiornamento";
|
||||||
|
return NextResponse.json({ error: message }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function DELETE(request: Request, { params }: RouteParams) {
|
||||||
|
const session = await auth();
|
||||||
|
|
||||||
|
if (!session?.user) {
|
||||||
|
return NextResponse.json({ error: "Non autorizzato" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id } = await params;
|
||||||
|
const macchinaId = parseInt(id, 10);
|
||||||
|
|
||||||
|
if (isNaN(macchinaId)) {
|
||||||
|
return NextResponse.json({ error: "ID non valido" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const deleted = await deleteMacchina(macchinaId);
|
||||||
|
|
||||||
|
if (!deleted) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Macchina non trovata" },
|
||||||
|
{ status: 404 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[API macchine/:id] Errore DELETE:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Errore nell'eliminazione" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
73
src/app/api/macchine/route.ts
Normal file
73
src/app/api/macchine/route.ts
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
|
import { auth } from "@/src/auth";
|
||||||
|
import {
|
||||||
|
listMacchine,
|
||||||
|
createMacchina,
|
||||||
|
cloneMacchina,
|
||||||
|
} from "@/src/lib/macchine";
|
||||||
|
import type { MacchinaCreateInput } from "@/src/types/macchina";
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
const session = await auth();
|
||||||
|
|
||||||
|
if (!session?.user) {
|
||||||
|
return NextResponse.json({ error: "Non autorizzato" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const macchine = await listMacchine();
|
||||||
|
return NextResponse.json({ macchine });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[API macchine] Errore GET:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Errore nel recupero delle macchine" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
const session = await auth();
|
||||||
|
|
||||||
|
if (!session?.user) {
|
||||||
|
return NextResponse.json({ error: "Non autorizzato" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const body = await request.json();
|
||||||
|
|
||||||
|
if (body.action === "clone" && body.id) {
|
||||||
|
const macchina = await cloneMacchina(body.id, body.newParentId);
|
||||||
|
if (!macchina) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Macchina non trovata" },
|
||||||
|
{ status: 404 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return NextResponse.json({ macchina });
|
||||||
|
}
|
||||||
|
|
||||||
|
const input: MacchinaCreateInput = {
|
||||||
|
nome: body.nome,
|
||||||
|
tipo: body.tipo || "macchina",
|
||||||
|
id_padre: body.id_padre || null,
|
||||||
|
id_stabilimento: body.id_stabilimento || null,
|
||||||
|
colore_testo: body.colore_testo || "#000000",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!input.nome?.trim()) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Nome obbligatorio" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const macchina = await createMacchina(input);
|
||||||
|
return NextResponse.json({ macchina }, { status: 201 });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[API macchine] Errore POST:", error);
|
||||||
|
const message = error instanceof Error ? error.message : "Errore nella creazione";
|
||||||
|
return NextResponse.json({ error: message }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
254
src/components/MacchinaModal.tsx
Normal file
254
src/components/MacchinaModal.tsx
Normal file
|
|
@ -0,0 +1,254 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import type { MacchinaWithDetails, TipoMacchina } from "@/src/types/macchina";
|
||||||
|
import type { Stabilimento } from "@/src/types/stabilimento";
|
||||||
|
|
||||||
|
interface FormData {
|
||||||
|
nome: string;
|
||||||
|
tipo: TipoMacchina;
|
||||||
|
id_stabilimento: string;
|
||||||
|
colore_testo: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MacchinaModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
editingMacchina: MacchinaWithDetails | null;
|
||||||
|
stabilimenti: Stabilimento[];
|
||||||
|
parentId: number | null;
|
||||||
|
parentStabilimento: number | null;
|
||||||
|
onClose: () => void;
|
||||||
|
onConfirm: (data: FormData & { id_padre?: number | null }) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MacchinaModal({
|
||||||
|
isOpen,
|
||||||
|
editingMacchina,
|
||||||
|
stabilimenti,
|
||||||
|
parentId,
|
||||||
|
parentStabilimento,
|
||||||
|
onClose,
|
||||||
|
onConfirm,
|
||||||
|
}: MacchinaModalProps) {
|
||||||
|
const [formData, setFormData] = useState<FormData>({
|
||||||
|
nome: "",
|
||||||
|
tipo: "macchina",
|
||||||
|
id_stabilimento: "",
|
||||||
|
colore_testo: "#3b82f6",
|
||||||
|
});
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
|
const isEditing = !!editingMacchina?.id;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
if (isEditing && editingMacchina) {
|
||||||
|
setFormData({
|
||||||
|
nome: editingMacchina.nome,
|
||||||
|
tipo: editingMacchina.tipo,
|
||||||
|
id_stabilimento: editingMacchina.id_stabilimento?.toString() || "",
|
||||||
|
colore_testo: editingMacchina.colore_testo || "#3b82f6",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setFormData({
|
||||||
|
nome: "",
|
||||||
|
tipo: parentId ? "gruppo" : "macchina",
|
||||||
|
id_stabilimento: parentStabilimento?.toString() || "",
|
||||||
|
colore_testo: "#3b82f6",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setErrors({});
|
||||||
|
}
|
||||||
|
}, [isOpen, editingMacchina, parentId, parentStabilimento, isEditing]);
|
||||||
|
|
||||||
|
const handleFieldChange = (field: keyof FormData, value: string) => {
|
||||||
|
setFormData((prev) => ({ ...prev, [field]: value }));
|
||||||
|
if (errors[field]) {
|
||||||
|
setErrors((prev) => ({ ...prev, [field]: "" }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateForm = (): boolean => {
|
||||||
|
const newErrors: Record<string, string> = {};
|
||||||
|
|
||||||
|
if (!formData.nome.trim()) {
|
||||||
|
newErrors.nome = "Il nome è obbligatorio";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (formData.tipo === "macchina" && !parentId && !formData.id_stabilimento) {
|
||||||
|
newErrors.id_stabilimento = "Stabilimento obbligatorio per macchine di livello root";
|
||||||
|
}
|
||||||
|
|
||||||
|
setErrors(newErrors);
|
||||||
|
return Object.keys(newErrors).length === 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!validateForm()) return;
|
||||||
|
|
||||||
|
setIsSubmitting(true);
|
||||||
|
try {
|
||||||
|
await onConfirm({
|
||||||
|
...formData,
|
||||||
|
id_padre: isEditing ? undefined : parentId,
|
||||||
|
});
|
||||||
|
onClose();
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : "Errore sconosciuto";
|
||||||
|
setErrors({ submit: message });
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
const getTitle = () => {
|
||||||
|
const action = isEditing ? "Modifica" : "Nuova";
|
||||||
|
const typeLabel = formData.tipo === "macchina" ? "Macchina" : formData.tipo === "gruppo" ? "Gruppo" : "Parte";
|
||||||
|
return `${action} ${typeLabel}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||||
|
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={onClose} />
|
||||||
|
|
||||||
|
<div className="relative w-full max-w-md bg-zinc-900 rounded-xl border border-zinc-800 shadow-2xl">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between px-5 py-4 border-b border-zinc-800">
|
||||||
|
<h3 className="text-lg font-semibold text-white">{getTitle()}</h3>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className="p-1 rounded-lg text-zinc-400 hover:text-white hover:bg-zinc-800 transition-colors"
|
||||||
|
>
|
||||||
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="px-5 py-4 space-y-4">
|
||||||
|
{/* Nome */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-zinc-300 mb-1.5">
|
||||||
|
Nome <span className="text-red-400">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={formData.nome}
|
||||||
|
onChange={(e) => handleFieldChange("nome", e.target.value)}
|
||||||
|
placeholder="Inserisci nome..."
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className={`w-full px-3 py-2 text-sm rounded-lg bg-zinc-800 border text-white placeholder-zinc-500 focus:outline-none focus:ring-2 focus:ring-blue-500/50
|
||||||
|
${errors.nome ? "border-red-500" : "border-zinc-700"}`}
|
||||||
|
/>
|
||||||
|
{errors.nome && <p className="mt-1 text-xs text-red-400">{errors.nome}</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tipo */}
|
||||||
|
{!isEditing && (
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-zinc-300 mb-1.5">Tipo</label>
|
||||||
|
<select
|
||||||
|
value={formData.tipo}
|
||||||
|
onChange={(e) => handleFieldChange("tipo", e.target.value as TipoMacchina)}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className="w-full px-3 py-2 text-sm rounded-lg bg-zinc-800 border border-zinc-700 text-white focus:outline-none focus:ring-2 focus:ring-blue-500/50"
|
||||||
|
>
|
||||||
|
<option value="macchina">Macchina</option>
|
||||||
|
<option value="gruppo">Gruppo</option>
|
||||||
|
<option value="parte">Parte</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Stabilimento - solo per macchine root */}
|
||||||
|
{formData.tipo === "macchina" && !parentId && (
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-zinc-300 mb-1.5">
|
||||||
|
Stabilimento <span className="text-red-400">*</span>
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={formData.id_stabilimento}
|
||||||
|
onChange={(e) => handleFieldChange("id_stabilimento", e.target.value)}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className={`w-full px-3 py-2 text-sm rounded-lg bg-zinc-800 border text-white focus:outline-none focus:ring-2 focus:ring-blue-500/50
|
||||||
|
${errors.id_stabilimento ? "border-red-500" : "border-zinc-700"}`}
|
||||||
|
>
|
||||||
|
<option value="">Seleziona stabilimento...</option>
|
||||||
|
{stabilimenti.map((s) => (
|
||||||
|
<option key={s.id} value={s.id}>{s.descrizione}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
{errors.id_stabilimento && <p className="mt-1 text-xs text-red-400">{errors.id_stabilimento}</p>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Info ereditarietà stabilimento */}
|
||||||
|
{parentId && parentStabilimento && (
|
||||||
|
<div className="p-3 rounded-lg bg-blue-900/20 border border-blue-700/30">
|
||||||
|
<p className="text-xs text-blue-300">
|
||||||
|
Lo stabilimento verrà ereditato automaticamente dal padre.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Colore */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-zinc-300 mb-1.5">Colore testo</label>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
value={formData.colore_testo}
|
||||||
|
onChange={(e) => handleFieldChange("colore_testo", e.target.value)}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className="w-10 h-10 rounded-lg border border-zinc-700 cursor-pointer"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={formData.colore_testo}
|
||||||
|
onChange={(e) => handleFieldChange("colore_testo", e.target.value)}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className="flex-1 px-3 py-2 text-sm rounded-lg bg-zinc-800 border border-zinc-700 text-white focus:outline-none focus:ring-2 focus:ring-blue-500/50"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Error generale */}
|
||||||
|
{errors.submit && (
|
||||||
|
<div className="p-3 rounded-lg bg-red-900/20 border border-red-700/30">
|
||||||
|
<p className="text-sm text-red-400">{errors.submit}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div className="flex items-center justify-end gap-3 px-5 py-4 border-t border-zinc-800">
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className="px-4 py-2 text-sm font-medium text-zinc-300 hover:text-white rounded-lg hover:bg-zinc-800 transition-colors"
|
||||||
|
>
|
||||||
|
Annulla
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-500 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
|
||||||
|
>
|
||||||
|
{isSubmitting && (
|
||||||
|
<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" />
|
||||||
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
{isEditing ? "Aggiorna" : "Crea"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,16 +1,219 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect, useCallback } 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";
|
||||||
|
|
||||||
export default function MacchineTree() {
|
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 [copiedNodeId, setCopiedNodeId] = useState<number | null>(null);
|
||||||
|
const [isCloning, setIsCloning] = useState(false);
|
||||||
|
|
||||||
|
const [showModal, setShowModal] = useState(false);
|
||||||
|
const [editingMacchina, setEditingMacchina] = useState<MacchinaWithDetails | null>(null);
|
||||||
|
const [parentInfo, setParentInfo] = useState<{
|
||||||
|
parentId: number | null;
|
||||||
|
parentStabilimento: number | null;
|
||||||
|
}>({ parentId: null, parentStabilimento: null });
|
||||||
|
|
||||||
|
const [deleteConfirm, setDeleteConfirm] = useState<{ id: number; nome: string } | null>(null);
|
||||||
|
|
||||||
|
const fetchNodes = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/macchine");
|
||||||
|
if (!res.ok) throw new Error("Errore nel caricamento");
|
||||||
|
const data = await res.json();
|
||||||
|
setNodes(data.macchine || []);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : "Errore sconosciuto");
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
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()]);
|
||||||
|
setLoading(false);
|
||||||
|
}, [fetchNodes, fetchStabilimenti]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadInitialData();
|
||||||
|
}, [loadInitialData]);
|
||||||
|
|
||||||
|
const getParentStabilimento = (parentId: number | null): number | null => {
|
||||||
|
if (!parentId) return null;
|
||||||
|
const parent = nodes.find((n) => n.id === parentId);
|
||||||
|
return parent?.id_stabilimento || null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddMachine = () => {
|
||||||
|
setEditingMacchina(null);
|
||||||
|
setParentInfo({ parentId: null, parentStabilimento: null });
|
||||||
|
setShowModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddChild = (parentId: number) => {
|
||||||
|
setEditingMacchina(null);
|
||||||
|
const parentStabilimento = getParentStabilimento(parentId);
|
||||||
|
setParentInfo({ parentId, parentStabilimento });
|
||||||
|
setShowModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEdit = (node: MacchinaWithDetails) => {
|
||||||
|
setEditingMacchina(node);
|
||||||
|
setParentInfo({
|
||||||
|
parentId: node.id_padre,
|
||||||
|
parentStabilimento: getParentStabilimento(node.id_padre),
|
||||||
|
});
|
||||||
|
setShowModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirmModal = async (formData: {
|
||||||
|
nome: string;
|
||||||
|
tipo: string;
|
||||||
|
id_stabilimento: string;
|
||||||
|
colore_testo: string;
|
||||||
|
id_padre?: number | null;
|
||||||
|
}) => {
|
||||||
|
const payload = {
|
||||||
|
nome: formData.nome,
|
||||||
|
tipo: formData.tipo,
|
||||||
|
id_stabilimento: formData.id_stabilimento ? parseInt(formData.id_stabilimento) : null,
|
||||||
|
colore_testo: formData.colore_testo,
|
||||||
|
id_padre: formData.id_padre,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (editingMacchina) {
|
||||||
|
const res = await fetch(`/api/macchine/${editingMacchina.id}`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
throw new Error(data.error || "Errore nell'aggiornamento");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const res = await fetch("/api/macchine", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
throw new Error(data.error || "Errore nella creazione");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await fetchNodes();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = (id: number) => {
|
||||||
|
const node = nodes.find((n) => n.id === id);
|
||||||
|
if (node) {
|
||||||
|
setDeleteConfirm({ id, nome: node.nome });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmDelete = async () => {
|
||||||
|
if (!deleteConfirm) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/macchine/${deleteConfirm.id}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error("Errore nell'eliminazione");
|
||||||
|
await fetchNodes();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Errore eliminazione:", err);
|
||||||
|
} finally {
|
||||||
|
setDeleteConfirm(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClone = async (node: MacchinaWithDetails) => {
|
||||||
|
setIsCloning(true);
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/macchine", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ action: "clone", id: node.id, newParentId: node.id_padre }),
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error("Errore nella clonazione");
|
||||||
|
await fetchNodes();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Errore clonazione:", err);
|
||||||
|
} finally {
|
||||||
|
setIsCloning(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCopy = (node: MacchinaWithDetails) => {
|
||||||
|
setCopiedNodeId(node.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePaste = async (targetId: number) => {
|
||||||
|
if (!copiedNodeId) return;
|
||||||
|
|
||||||
|
setIsCloning(true);
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/macchine", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ action: "clone", id: copiedNodeId, newParentId: targetId }),
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error("Errore nell'incolla");
|
||||||
|
await fetchNodes();
|
||||||
|
setCopiedNodeId(null);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Errore incolla:", err);
|
||||||
|
} finally {
|
||||||
|
setIsCloning(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleShowArticles = (nodeId: number) => {
|
||||||
|
console.log("Show articles for node:", nodeId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDropArticle = (macchinaId: number, article: unknown) => {
|
||||||
|
console.log("Drop article on machine:", macchinaId, article);
|
||||||
|
};
|
||||||
|
|
||||||
|
const rootNodes = nodes.filter((n) => n.id_padre === null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between gap-4 mb-4">
|
<div className="flex items-center justify-between gap-4 mb-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg font-semibold text-white">Macchine</h2>
|
<h2 className="text-lg font-semibold text-white">Macchine</h2>
|
||||||
<p className="text-xs text-zinc-500">Albero macchine e parti</p>
|
<p className="text-xs text-zinc-500">
|
||||||
|
{nodes.length} elementi nell'albero
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
className="p-2 rounded-lg bg-zinc-800 text-zinc-400 hover:bg-zinc-700 hover:text-white transition-colors"
|
onClick={handleAddMachine}
|
||||||
|
disabled={isCloning}
|
||||||
|
className="p-2 rounded-lg bg-blue-600 text-white hover:bg-blue-500 transition-colors disabled:opacity-50"
|
||||||
title="Nuova macchina"
|
title="Nuova macchina"
|
||||||
>
|
>
|
||||||
<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">
|
||||||
|
|
@ -19,16 +222,133 @@ export default function MacchineTree() {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Placeholder contenuto */}
|
{/* Indicatore clonazione */}
|
||||||
|
{isCloning && (
|
||||||
|
<div className="mb-3 px-3 py-2 rounded-lg bg-blue-900/30 border border-blue-700/50 flex items-center gap-2">
|
||||||
|
<svg className="w-4 h-4 animate-spin text-blue-400" 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>
|
||||||
|
<span className="text-sm text-blue-300">Clonazione in corso...</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Indicatore copia */}
|
||||||
|
{copiedNodeId && (
|
||||||
|
<div className="mb-3 px-3 py-2 rounded-lg bg-yellow-900/30 border border-yellow-700/50 flex items-center justify-between">
|
||||||
|
<span className="text-sm text-yellow-300">
|
||||||
|
Nodo copiato. Clicca su "Incolla" per inserirlo.
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={() => setCopiedNodeId(null)}
|
||||||
|
className="text-xs text-yellow-400 hover:text-yellow-200"
|
||||||
|
>
|
||||||
|
Annulla
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Contenuto */}
|
||||||
|
<div className="flex-1 overflow-y-auto 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={loadInitialData} className="text-xs text-blue-400 hover:underline">
|
||||||
|
Riprova
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : rootNodes.length === 0 ? (
|
||||||
<div className="flex-1 flex flex-col items-center justify-center text-center p-8 border-2 border-dashed border-zinc-800 rounded-lg">
|
<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">
|
<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" />
|
<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>
|
</svg>
|
||||||
<p className="text-zinc-500 text-sm mb-2">Albero macchine</p>
|
<p className="text-zinc-500 text-sm mb-2">Nessuna macchina presente</p>
|
||||||
<p className="text-zinc-600 text-xs">
|
<p className="text-zinc-600 text-xs mb-4">
|
||||||
Trascina gli articoli qui per assegnarli alle macchine
|
Clicca sul pulsante + per aggiungere la prima macchina
|
||||||
</p>
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={handleAddMachine}
|
||||||
|
className="px-4 py-2 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-500 transition-colors"
|
||||||
|
>
|
||||||
|
Aggiungi macchina
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-1">
|
||||||
|
{rootNodes.map((node) => (
|
||||||
|
<TreeNode
|
||||||
|
key={node.id}
|
||||||
|
node={node}
|
||||||
|
allNodes={nodes}
|
||||||
|
level={0}
|
||||||
|
articlesByMacchina={articlesByMacchina}
|
||||||
|
isCloning={isCloning}
|
||||||
|
copiedNodeId={copiedNodeId}
|
||||||
|
onAdd={handleAddChild}
|
||||||
|
onEdit={handleEdit}
|
||||||
|
onDelete={handleDelete}
|
||||||
|
onClone={handleClone}
|
||||||
|
onCopy={handleCopy}
|
||||||
|
onPaste={handlePaste}
|
||||||
|
onShowArticles={handleShowArticles}
|
||||||
|
onDropArticle={handleDropArticle}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Modal */}
|
||||||
|
<MacchinaModal
|
||||||
|
isOpen={showModal}
|
||||||
|
editingMacchina={editingMacchina}
|
||||||
|
stabilimenti={stabilimenti}
|
||||||
|
parentId={parentInfo.parentId}
|
||||||
|
parentStabilimento={parentInfo.parentStabilimento}
|
||||||
|
onClose={() => {
|
||||||
|
setShowModal(false);
|
||||||
|
setEditingMacchina(null);
|
||||||
|
setParentInfo({ parentId: null, parentStabilimento: null });
|
||||||
|
}}
|
||||||
|
onConfirm={handleConfirmModal}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Delete Confirmation */}
|
||||||
|
{deleteConfirm && (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||||
|
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={() => setDeleteConfirm(null)} />
|
||||||
|
<div className="relative w-full max-w-sm bg-zinc-900 rounded-xl border border-zinc-800 shadow-2xl p-5">
|
||||||
|
<h3 className="text-lg font-semibold text-white mb-2">Conferma eliminazione</h3>
|
||||||
|
<p className="text-sm text-zinc-400 mb-4">
|
||||||
|
Sei sicuro di voler eliminare <strong className="text-white">{deleteConfirm.nome}</strong>?
|
||||||
|
<br />
|
||||||
|
<span className="text-red-400">Verranno eliminati anche tutti i sotto-elementi.</span>
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center justify-end gap-3">
|
||||||
|
<button
|
||||||
|
onClick={() => setDeleteConfirm(null)}
|
||||||
|
className="px-4 py-2 text-sm font-medium text-zinc-300 hover:text-white rounded-lg hover:bg-zinc-800 transition-colors"
|
||||||
|
>
|
||||||
|
Annulla
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={confirmDelete}
|
||||||
|
className="px-4 py-2 text-sm font-medium text-white bg-red-600 rounded-lg hover:bg-red-500 transition-colors"
|
||||||
|
>
|
||||||
|
Elimina
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
304
src/components/TreeNode.tsx
Normal file
304
src/components/TreeNode.tsx
Normal file
|
|
@ -0,0 +1,304 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, type DragEvent } from "react";
|
||||||
|
import type { MacchinaWithDetails, ArticoliPerMacchina } from "@/src/types/macchina";
|
||||||
|
|
||||||
|
function toRgb(color: string | null | undefined): { r: number; g: number; b: number } | null {
|
||||||
|
if (!color || typeof color !== "string") return null;
|
||||||
|
const c = color.trim();
|
||||||
|
|
||||||
|
if (c.startsWith("#")) {
|
||||||
|
const hex = c.slice(1);
|
||||||
|
if (hex.length === 3) {
|
||||||
|
const r = parseInt(hex[0] + hex[0], 16);
|
||||||
|
const g = parseInt(hex[1] + hex[1], 16);
|
||||||
|
const b = parseInt(hex[2] + hex[2], 16);
|
||||||
|
if ([r, g, b].some((v) => Number.isNaN(v))) return null;
|
||||||
|
return { r, g, b };
|
||||||
|
}
|
||||||
|
if (hex.length === 6) {
|
||||||
|
const r = parseInt(hex.slice(0, 2), 16);
|
||||||
|
const g = parseInt(hex.slice(2, 4), 16);
|
||||||
|
const b = parseInt(hex.slice(4, 6), 16);
|
||||||
|
if ([r, g, b].some((v) => Number.isNaN(v))) return null;
|
||||||
|
return { r, g, b };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function relativeLuminance(rgb: { r: number; g: number; b: number }): number {
|
||||||
|
const srgb = [rgb.r, rgb.g, rgb.b].map((v) => v / 255);
|
||||||
|
const lin = srgb.map((v) => (v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4)));
|
||||||
|
return 0.2126 * lin[0] + 0.7152 * lin[1] + 0.0722 * lin[2];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getReadableTextColor(inputColor: string | null | undefined): string {
|
||||||
|
const rgb = toRgb(inputColor);
|
||||||
|
if (!rgb) return "#e5e5e5";
|
||||||
|
const L = relativeLuminance(rgb);
|
||||||
|
if (L > 0.8) return "#18181b";
|
||||||
|
return inputColor || "#e5e5e5";
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TreeNodeProps {
|
||||||
|
node: MacchinaWithDetails;
|
||||||
|
allNodes: MacchinaWithDetails[];
|
||||||
|
level?: number;
|
||||||
|
articlesByMacchina?: ArticoliPerMacchina;
|
||||||
|
isCloning?: boolean;
|
||||||
|
copiedNodeId?: number | null;
|
||||||
|
onAdd: (parentId: number) => void;
|
||||||
|
onEdit: (node: MacchinaWithDetails) => void;
|
||||||
|
onDelete: (id: number) => void;
|
||||||
|
onClone: (node: MacchinaWithDetails) => void;
|
||||||
|
onCopy: (node: MacchinaWithDetails) => void;
|
||||||
|
onPaste: (targetId: number) => void;
|
||||||
|
onShowArticles?: (nodeId: number) => void;
|
||||||
|
onDropArticle?: (macchinaId: number, article: unknown) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TreeNode({
|
||||||
|
node,
|
||||||
|
allNodes,
|
||||||
|
level = 0,
|
||||||
|
articlesByMacchina = {},
|
||||||
|
isCloning = false,
|
||||||
|
copiedNodeId,
|
||||||
|
onAdd,
|
||||||
|
onEdit,
|
||||||
|
onDelete,
|
||||||
|
onClone,
|
||||||
|
onCopy,
|
||||||
|
onPaste,
|
||||||
|
onShowArticles,
|
||||||
|
onDropArticle,
|
||||||
|
}: TreeNodeProps) {
|
||||||
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
|
const [isDragOver, setIsDragOver] = useState(false);
|
||||||
|
|
||||||
|
const children = allNodes.filter((n) => n.id_padre === node.id);
|
||||||
|
const nodeArticles = articlesByMacchina[node.id] || [];
|
||||||
|
|
||||||
|
const getTotalQuantity = (currentNode: MacchinaWithDetails): number => {
|
||||||
|
const currentArticles = articlesByMacchina[currentNode.id] || [];
|
||||||
|
const currentQty = currentArticles.reduce(
|
||||||
|
(sum, art) => sum + (art.quantita || 0),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
const childNodes = allNodes.filter((n) => n.id_padre === currentNode.id);
|
||||||
|
const childrenQty = childNodes.reduce(
|
||||||
|
(sum, child) => sum + getTotalQuantity(child),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
return currentQty + childrenQty;
|
||||||
|
};
|
||||||
|
|
||||||
|
const totalQuantity = getTotalQuantity(node);
|
||||||
|
|
||||||
|
const handleDrop = (e: DragEvent<HTMLDivElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsDragOver(false);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = e.dataTransfer.getData("application/json");
|
||||||
|
if (data && onDropArticle) {
|
||||||
|
const article = JSON.parse(data);
|
||||||
|
onDropArticle(node.id, article);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore parsing errors
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragOver = (e: DragEvent<HTMLDivElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsDragOver(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragLeave = (e: DragEvent<HTMLDivElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsDragOver(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const safeTextColor = getReadableTextColor(node.colore_testo);
|
||||||
|
|
||||||
|
const getNodeTypeIcon = () => {
|
||||||
|
switch (node.tipo) {
|
||||||
|
case "macchina": return "🔧";
|
||||||
|
case "gruppo": return "📁";
|
||||||
|
case "parte": return "⚙️";
|
||||||
|
default: return "📄";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getNodeTypeBg = () => {
|
||||||
|
switch (node.tipo) {
|
||||||
|
case "macchina": return "bg-blue-900/30 border-blue-700/50";
|
||||||
|
case "gruppo": return "bg-purple-900/30 border-purple-700/50";
|
||||||
|
case "parte": return "bg-zinc-800/50 border-zinc-700/50";
|
||||||
|
default: return "bg-zinc-800 border-zinc-700";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="mb-1"
|
||||||
|
style={{ marginLeft: `${level * 16}px` }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`flex items-center gap-2 px-3 py-2 rounded-lg border transition-all group
|
||||||
|
${getNodeTypeBg()}
|
||||||
|
${isDragOver ? "ring-2 ring-blue-500 bg-blue-900/50" : ""}
|
||||||
|
hover:bg-zinc-800/70`}
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDragLeave={handleDragLeave}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
>
|
||||||
|
{children.length > 0 && (
|
||||||
|
<button
|
||||||
|
onClick={() => setIsExpanded(!isExpanded)}
|
||||||
|
className="w-5 h-5 flex items-center justify-center text-zinc-400 hover:text-white transition-colors"
|
||||||
|
title={isExpanded ? "Comprimi" : "Espandi"}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className={`w-4 h-4 transition-transform ${isExpanded ? "rotate-90" : ""}`}
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{children.length === 0 && <div className="w-5" />}
|
||||||
|
|
||||||
|
<span className="text-sm">{getNodeTypeIcon()}</span>
|
||||||
|
|
||||||
|
<span
|
||||||
|
className="flex-1 text-sm font-medium truncate"
|
||||||
|
style={{ color: safeTextColor }}
|
||||||
|
>
|
||||||
|
{node.nome}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{totalQuantity > 0 && (
|
||||||
|
<button
|
||||||
|
onClick={() => onShowArticles?.(node.id)}
|
||||||
|
className="px-2 py-0.5 text-xs rounded bg-teal-900/50 text-teal-300 border border-teal-700/50 hover:bg-teal-800/50 transition-colors"
|
||||||
|
title={`${totalQuantity} articoli assegnati`}
|
||||||
|
>
|
||||||
|
📦 {totalQuantity}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
|
<button
|
||||||
|
onClick={() => onAdd(node.id)}
|
||||||
|
disabled={isCloning}
|
||||||
|
className="p-1 rounded text-zinc-400 hover:text-green-400 hover:bg-green-900/30 transition-colors disabled:opacity-50"
|
||||||
|
title="Aggiungi sotto-elemento"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => onEdit(node)}
|
||||||
|
disabled={isCloning}
|
||||||
|
className="p-1 rounded text-zinc-400 hover:text-blue-400 hover:bg-blue-900/30 transition-colors disabled:opacity-50"
|
||||||
|
title="Modifica"
|
||||||
|
>
|
||||||
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => onCopy(node)}
|
||||||
|
disabled={isCloning}
|
||||||
|
className="p-1 rounded text-zinc-400 hover:text-yellow-400 hover:bg-yellow-900/30 transition-colors disabled:opacity-50"
|
||||||
|
title="Copia"
|
||||||
|
>
|
||||||
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => onClone(node)}
|
||||||
|
disabled={isCloning}
|
||||||
|
className="p-1 rounded text-zinc-400 hover:text-purple-400 hover:bg-purple-900/30 transition-colors disabled:opacity-50"
|
||||||
|
title="Clona con sotto-elementi"
|
||||||
|
>
|
||||||
|
{isCloning ? (
|
||||||
|
<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" />
|
||||||
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{copiedNodeId && (
|
||||||
|
<button
|
||||||
|
onClick={() => onPaste(node.id)}
|
||||||
|
disabled={isCloning}
|
||||||
|
className="p-1 rounded text-zinc-400 hover:text-orange-400 hover:bg-orange-900/30 transition-colors disabled:opacity-50"
|
||||||
|
title="Incolla"
|
||||||
|
>
|
||||||
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => onDelete(node.id)}
|
||||||
|
disabled={isCloning}
|
||||||
|
className="p-1 rounded text-zinc-400 hover:text-red-400 hover:bg-red-900/30 transition-colors disabled:opacity-50"
|
||||||
|
title="Elimina"
|
||||||
|
>
|
||||||
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isExpanded && children.length > 0 && (
|
||||||
|
<div className="mt-1">
|
||||||
|
{children.map((child) => (
|
||||||
|
<TreeNode
|
||||||
|
key={child.id}
|
||||||
|
node={child}
|
||||||
|
allNodes={allNodes}
|
||||||
|
level={level + 1}
|
||||||
|
articlesByMacchina={articlesByMacchina}
|
||||||
|
isCloning={isCloning}
|
||||||
|
copiedNodeId={copiedNodeId}
|
||||||
|
onAdd={onAdd}
|
||||||
|
onEdit={onEdit}
|
||||||
|
onDelete={onDelete}
|
||||||
|
onClone={onClone}
|
||||||
|
onCopy={onCopy}
|
||||||
|
onPaste={onPaste}
|
||||||
|
onShowArticles={onShowArticles}
|
||||||
|
onDropArticle={onDropArticle}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
260
src/lib/macchine/index.ts
Normal file
260
src/lib/macchine/index.ts
Normal file
|
|
@ -0,0 +1,260 @@
|
||||||
|
import type { RowDataPacket, ResultSetHeader } from "mysql2";
|
||||||
|
|
||||||
|
import { db } from "@/src/lib/db";
|
||||||
|
import type {
|
||||||
|
Macchina,
|
||||||
|
MacchinaWithDetails,
|
||||||
|
MacchinaCreateInput,
|
||||||
|
MacchinaUpdateInput,
|
||||||
|
TipoMacchina,
|
||||||
|
} from "@/src/types/macchina";
|
||||||
|
|
||||||
|
type MacchinaRow = Macchina & RowDataPacket;
|
||||||
|
type MacchinaWithDetailsRow = MacchinaWithDetails & RowDataPacket;
|
||||||
|
|
||||||
|
export async function listMacchine(): Promise<MacchinaWithDetails[]> {
|
||||||
|
const [rows] = await db.execute<MacchinaWithDetailsRow[]>(`
|
||||||
|
SELECT
|
||||||
|
m.id,
|
||||||
|
m.nome,
|
||||||
|
m.tipo,
|
||||||
|
m.id_padre,
|
||||||
|
m.id_stabilimento,
|
||||||
|
m.colore_testo,
|
||||||
|
s.descrizione AS stabilimento_nome,
|
||||||
|
p.nome AS padre_nome
|
||||||
|
FROM macchine_parti m
|
||||||
|
LEFT JOIN stabilimenti s ON s.id = m.id_stabilimento
|
||||||
|
LEFT JOIN macchine_parti p ON p.id = m.id_padre
|
||||||
|
ORDER BY ISNULL(m.id_padre) DESC, m.id_padre ASC, m.nome ASC
|
||||||
|
`);
|
||||||
|
|
||||||
|
return rows.map((row) => ({
|
||||||
|
id: row.id,
|
||||||
|
nome: row.nome,
|
||||||
|
tipo: row.tipo as TipoMacchina,
|
||||||
|
id_padre: row.id_padre,
|
||||||
|
id_stabilimento: row.id_stabilimento,
|
||||||
|
colore_testo: row.colore_testo || "#000000",
|
||||||
|
stabilimento_nome: row.stabilimento_nome,
|
||||||
|
padre_nome: row.padre_nome,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getMacchinaById(id: number): Promise<MacchinaWithDetails | null> {
|
||||||
|
const [rows] = await db.execute<MacchinaWithDetailsRow[]>(
|
||||||
|
`SELECT
|
||||||
|
m.id,
|
||||||
|
m.nome,
|
||||||
|
m.tipo,
|
||||||
|
m.id_padre,
|
||||||
|
m.id_stabilimento,
|
||||||
|
m.colore_testo,
|
||||||
|
s.descrizione AS stabilimento_nome,
|
||||||
|
p.nome AS padre_nome
|
||||||
|
FROM macchine_parti m
|
||||||
|
LEFT JOIN stabilimenti s ON s.id = m.id_stabilimento
|
||||||
|
LEFT JOIN macchine_parti p ON p.id = m.id_padre
|
||||||
|
WHERE m.id = :id
|
||||||
|
LIMIT 1`,
|
||||||
|
{ id }
|
||||||
|
);
|
||||||
|
|
||||||
|
const row = rows[0];
|
||||||
|
if (!row) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: row.id,
|
||||||
|
nome: row.nome,
|
||||||
|
tipo: row.tipo as TipoMacchina,
|
||||||
|
id_padre: row.id_padre,
|
||||||
|
id_stabilimento: row.id_stabilimento,
|
||||||
|
colore_testo: row.colore_testo || "#000000",
|
||||||
|
stabilimento_nome: row.stabilimento_nome,
|
||||||
|
padre_nome: row.padre_nome,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createMacchina(input: MacchinaCreateInput): Promise<Macchina> {
|
||||||
|
const { nome, tipo, id_padre, id_stabilimento, colore_testo = "#000000" } = input;
|
||||||
|
|
||||||
|
let stabilimentoId = id_stabilimento;
|
||||||
|
|
||||||
|
if (tipo === "macchina" && !id_padre && !stabilimentoId) {
|
||||||
|
throw new Error("Stabilimento obbligatorio per macchine di livello root");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (id_padre && !stabilimentoId) {
|
||||||
|
const [parentRows] = await db.execute<MacchinaRow[]>(
|
||||||
|
"SELECT id_stabilimento FROM macchine_parti WHERE id = :id_padre LIMIT 1",
|
||||||
|
{ id_padre }
|
||||||
|
);
|
||||||
|
const parent = parentRows[0];
|
||||||
|
if (parent?.id_stabilimento) {
|
||||||
|
stabilimentoId = parent.id_stabilimento;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const [result] = await db.execute<ResultSetHeader>(
|
||||||
|
`INSERT INTO macchine_parti (nome, tipo, id_padre, id_stabilimento, colore_testo)
|
||||||
|
VALUES (:nome, :tipo, :id_padre, :id_stabilimento, :colore_testo)`,
|
||||||
|
{
|
||||||
|
nome,
|
||||||
|
tipo,
|
||||||
|
id_padre: id_padre || null,
|
||||||
|
id_stabilimento: stabilimentoId || null,
|
||||||
|
colore_testo,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: result.insertId,
|
||||||
|
nome,
|
||||||
|
tipo,
|
||||||
|
id_padre: id_padre || null,
|
||||||
|
id_stabilimento: stabilimentoId || null,
|
||||||
|
colore_testo,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateMacchina(
|
||||||
|
id: number,
|
||||||
|
input: MacchinaUpdateInput
|
||||||
|
): Promise<MacchinaWithDetails | null> {
|
||||||
|
const current = await getMacchinaById(id);
|
||||||
|
if (!current) return null;
|
||||||
|
|
||||||
|
const nome = input.nome ?? current.nome;
|
||||||
|
const tipo = input.tipo ?? current.tipo;
|
||||||
|
const id_padre = input.id_padre !== undefined ? input.id_padre : current.id_padre;
|
||||||
|
const colore_testo = input.colore_testo ?? current.colore_testo;
|
||||||
|
|
||||||
|
let id_stabilimento = input.id_stabilimento !== undefined
|
||||||
|
? input.id_stabilimento
|
||||||
|
: current.id_stabilimento;
|
||||||
|
|
||||||
|
if (id_padre && !id_stabilimento) {
|
||||||
|
const [parentRows] = await db.execute<MacchinaRow[]>(
|
||||||
|
"SELECT id_stabilimento FROM macchine_parti WHERE id = :id_padre LIMIT 1",
|
||||||
|
{ id_padre }
|
||||||
|
);
|
||||||
|
const parent = parentRows[0];
|
||||||
|
if (parent?.id_stabilimento) {
|
||||||
|
id_stabilimento = parent.id_stabilimento;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.execute(
|
||||||
|
`UPDATE macchine_parti
|
||||||
|
SET nome = :nome, tipo = :tipo, id_padre = :id_padre,
|
||||||
|
id_stabilimento = :id_stabilimento, colore_testo = :colore_testo
|
||||||
|
WHERE id = :id`,
|
||||||
|
{
|
||||||
|
id,
|
||||||
|
nome,
|
||||||
|
tipo,
|
||||||
|
id_padre: id_padre || null,
|
||||||
|
id_stabilimento: id_stabilimento || null,
|
||||||
|
colore_testo,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return getMacchinaById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteMacchina(id: number): Promise<boolean> {
|
||||||
|
const [childRows] = await db.execute<MacchinaRow[]>(
|
||||||
|
"SELECT id FROM macchine_parti WHERE id_padre = :id",
|
||||||
|
{ id }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (childRows.length > 0) {
|
||||||
|
for (const child of childRows) {
|
||||||
|
await deleteMacchina(child.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const [result] = await db.execute<ResultSetHeader>(
|
||||||
|
"DELETE FROM macchine_parti WHERE id = :id",
|
||||||
|
{ id }
|
||||||
|
);
|
||||||
|
|
||||||
|
return result.affectedRows > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function cloneMacchina(
|
||||||
|
id: number,
|
||||||
|
newParentId: number | null = null
|
||||||
|
): Promise<Macchina | null> {
|
||||||
|
const original = await getMacchinaById(id);
|
||||||
|
if (!original) return null;
|
||||||
|
|
||||||
|
let copyName = `${original.nome} (copia)`;
|
||||||
|
let counter = 1;
|
||||||
|
|
||||||
|
const [existingRows] = await db.execute<MacchinaRow[]>(
|
||||||
|
"SELECT nome FROM macchine_parti WHERE nome LIKE :pattern",
|
||||||
|
{ pattern: `${original.nome} (copia%` }
|
||||||
|
);
|
||||||
|
|
||||||
|
const existingNames = new Set(existingRows.map((r) => r.nome));
|
||||||
|
while (existingNames.has(copyName)) {
|
||||||
|
copyName = `${original.nome} (copia ${counter})`;
|
||||||
|
counter++;
|
||||||
|
}
|
||||||
|
|
||||||
|
let stabilimentoId = original.id_stabilimento;
|
||||||
|
if (newParentId) {
|
||||||
|
const [parentRows] = await db.execute<MacchinaRow[]>(
|
||||||
|
"SELECT id_stabilimento FROM macchine_parti WHERE id = :id LIMIT 1",
|
||||||
|
{ id: newParentId }
|
||||||
|
);
|
||||||
|
if (parentRows[0]?.id_stabilimento) {
|
||||||
|
stabilimentoId = parentRows[0].id_stabilimento;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const newMacchina = await createMacchina({
|
||||||
|
nome: copyName,
|
||||||
|
tipo: original.tipo,
|
||||||
|
id_padre: newParentId ?? original.id_padre,
|
||||||
|
id_stabilimento: stabilimentoId,
|
||||||
|
colore_testo: original.colore_testo,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [children] = await db.execute<MacchinaRow[]>(
|
||||||
|
"SELECT id FROM macchine_parti WHERE id_padre = :id",
|
||||||
|
{ id }
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const child of children) {
|
||||||
|
await cloneMacchina(child.id, newMacchina.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return newMacchina;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getChildrenCount(id: number): Promise<number> {
|
||||||
|
const [rows] = await db.execute<(RowDataPacket & { count: number })[]>(
|
||||||
|
"SELECT COUNT(*) as count FROM macchine_parti WHERE id_padre = :id",
|
||||||
|
{ id }
|
||||||
|
);
|
||||||
|
return rows[0]?.count ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAllDescendantIds(id: number): Promise<number[]> {
|
||||||
|
const ids: number[] = [];
|
||||||
|
|
||||||
|
const [children] = await db.execute<MacchinaRow[]>(
|
||||||
|
"SELECT id FROM macchine_parti WHERE id_padre = :id",
|
||||||
|
{ id }
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const child of children) {
|
||||||
|
ids.push(child.id);
|
||||||
|
const descendantIds = await getAllDescendantIds(child.id);
|
||||||
|
ids.push(...descendantIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
47
src/types/macchina.ts
Normal file
47
src/types/macchina.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
export type TipoMacchina = "macchina" | "gruppo" | "parte";
|
||||||
|
|
||||||
|
export interface Macchina {
|
||||||
|
id: number;
|
||||||
|
nome: string;
|
||||||
|
tipo: TipoMacchina;
|
||||||
|
id_padre: number | null;
|
||||||
|
id_stabilimento: number | null;
|
||||||
|
colore_testo: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MacchinaWithDetails extends Macchina {
|
||||||
|
stabilimento_nome?: string;
|
||||||
|
padre_nome?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MacchinaCreateInput {
|
||||||
|
nome: string;
|
||||||
|
tipo: TipoMacchina;
|
||||||
|
id_padre?: number | null;
|
||||||
|
id_stabilimento?: number | null;
|
||||||
|
colore_testo?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MacchinaUpdateInput {
|
||||||
|
nome?: string;
|
||||||
|
tipo?: TipoMacchina;
|
||||||
|
id_padre?: number | null;
|
||||||
|
id_stabilimento?: number | null;
|
||||||
|
colore_testo?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ArticoloAssegnato {
|
||||||
|
id: number;
|
||||||
|
id_articolo: number;
|
||||||
|
id_macchina: number;
|
||||||
|
descrizione: string;
|
||||||
|
codice: string;
|
||||||
|
barcode?: string;
|
||||||
|
quantita: number;
|
||||||
|
qtain: number;
|
||||||
|
qtaout: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ArticoliPerMacchina {
|
||||||
|
[macchinaId: number]: ArticoloAssegnato[];
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue