153 lines
6.3 KiB
TypeScript
153 lines
6.3 KiB
TypeScript
"use client";
|
|
|
|
import { use, useCallback, useEffect, useState } from "react";
|
|
import Link from "next/link";
|
|
|
|
import { formatDate, formatMoney, getErrorMessage } from "@/components/inventory/inventory-utils";
|
|
import { Button } from "@/components/ui/button";
|
|
import { api } from "@/lib/api";
|
|
import type { InventoryItem, InventoryStockMovement } from "@/types/inventory";
|
|
|
|
type PageProps = {
|
|
params: Promise<{
|
|
id: string;
|
|
}>;
|
|
};
|
|
|
|
const movementLabels: Record<string, string> = {
|
|
initial: "Anfangsbestand",
|
|
purchase: "Einkauf",
|
|
adjustment: "Anpassung",
|
|
reservation: "Reservierung",
|
|
release: "Freigabe",
|
|
consumption: "Verbrauch",
|
|
return: "Rückgabe",
|
|
correction: "Korrektur",
|
|
};
|
|
|
|
export default function InventoryItemDetailPage({ params }: PageProps) {
|
|
const { id } = use(params);
|
|
const [item, setItem] = useState<InventoryItem | null>(null);
|
|
const [movements, setMovements] = useState<InventoryStockMovement[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState("");
|
|
|
|
const loadItem = useCallback(async () => {
|
|
setLoading(true);
|
|
setError("");
|
|
try {
|
|
const [itemResponse, movementResponse] = await Promise.all([
|
|
api.get<InventoryItem>(`/inventory/items/${id}`),
|
|
api.get<InventoryStockMovement[]>(`/inventory/items/${id}/movements`),
|
|
]);
|
|
setItem(itemResponse.data);
|
|
setMovements(movementResponse.data);
|
|
} catch (err) {
|
|
setError(getErrorMessage(err));
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [id]);
|
|
|
|
useEffect(() => {
|
|
queueMicrotask(() => {
|
|
void loadItem();
|
|
});
|
|
}, [loadItem]);
|
|
|
|
if (loading) {
|
|
return <div className="rounded-lg border bg-white p-8 text-slate-500">Artikel wird geladen...</div>;
|
|
}
|
|
|
|
if (error || !item) {
|
|
return <div className="rounded-lg border bg-white p-8 text-red-600">{error || "Artikel nicht gefunden"}</div>;
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
|
<div>
|
|
<p className="text-sm font-medium text-slate-500">{item.sku}</p>
|
|
<h1 className="text-3xl font-bold text-slate-950">{item.name}</h1>
|
|
<p className="mt-1 text-sm text-slate-500">{item.category?.name ?? "Keine Kategorie"} · {item.is_active ? "Aktiv" : "Inaktiv"}</p>
|
|
</div>
|
|
<Link href="/inventory"><Button type="button" variant="outline">Zurück zum Lager</Button></Link>
|
|
</div>
|
|
|
|
<div className="grid gap-4 xl:grid-cols-3">
|
|
<section className="rounded-lg border bg-white p-5 xl:col-span-2">
|
|
<h2 className="font-semibold text-slate-950">Stammdaten</h2>
|
|
<dl className="mt-4 grid gap-4 md:grid-cols-2">
|
|
<Info label="Beschreibung" value={item.description ?? "Keine Angabe"} />
|
|
<Info label="Hersteller" value={item.manufacturer ?? "Keine Angabe"} />
|
|
<Info label="Hersteller-Teilenummer" value={item.manufacturer_part_number ?? "Keine Angabe"} />
|
|
<Info label="Lieferanten-Teilenummer" value={item.supplier_part_number ?? "Keine Angabe"} />
|
|
<Info label="Lieferant" value={item.supplier?.name ?? "Kein Lieferant"} />
|
|
<Info label="Lagerort" value={item.location?.name ?? "Kein Lagerort"} />
|
|
<Info label="Kompatible Geräte" value={item.compatible_devices ?? "Keine Angabe"} />
|
|
<Info label="Notizen" value={item.notes ?? "Keine Notizen"} />
|
|
</dl>
|
|
</section>
|
|
|
|
<section className="rounded-lg border bg-white p-5">
|
|
<h2 className="font-semibold text-slate-950">Bestand</h2>
|
|
<dl className="mt-4 grid gap-3">
|
|
<Metric label="Bestand" value={`${item.quantity_on_hand} ${item.unit}`} />
|
|
<Metric label="Reserviert" value={`${item.quantity_reserved} ${item.unit}`} />
|
|
<Metric label="Verfügbar" value={`${item.quantity_available} ${item.unit}`} />
|
|
<Metric label="Mindestbestand" value={`${item.reorder_level} ${item.unit}`} />
|
|
</dl>
|
|
</section>
|
|
</div>
|
|
|
|
<div className="grid gap-4 xl:grid-cols-3">
|
|
<section className="rounded-lg border bg-white p-5">
|
|
<h2 className="font-semibold text-slate-950">Preise</h2>
|
|
<dl className="mt-4 grid gap-3">
|
|
<Metric label="Einkaufspreis" value={formatMoney(item.purchase_price_cents, item.currency)} />
|
|
<Metric label="Verkaufspreis" value={formatMoney(item.selling_price_cents, item.currency)} />
|
|
<Metric label="MwSt." value={`${item.tax_rate_percent} %`} />
|
|
</dl>
|
|
</section>
|
|
|
|
<section className="rounded-lg border bg-white p-5 xl:col-span-2">
|
|
<h2 className="font-semibold text-slate-950">Bewegungen</h2>
|
|
{movements.length === 0 ? (
|
|
<p className="mt-4 text-sm text-slate-500">Noch keine Bewegungen vorhanden.</p>
|
|
) : (
|
|
<div className="mt-4 divide-y">
|
|
{movements.map((movement) => (
|
|
<div key={movement.id} className="py-3">
|
|
<div className="flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between">
|
|
<p className="font-medium text-slate-950">{movementLabels[movement.movement_type] ?? movement.movement_type} · {movement.quantity}</p>
|
|
<time className="text-xs text-slate-500" dateTime={movement.created_at}>{formatDate(movement.created_at)}</time>
|
|
</div>
|
|
<p className="mt-1 text-sm text-slate-500">{movement.reason}{movement.note ? ` · ${movement.note}` : ""}</p>
|
|
{movement.actor_username && <p className="mt-1 text-xs text-slate-400">Gebucht von {movement.actor_username}</p>}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</section>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function Info({ label, value }: { label: string; value: string }) {
|
|
return (
|
|
<div>
|
|
<dt className="text-xs font-semibold uppercase text-slate-500">{label}</dt>
|
|
<dd className="mt-1 whitespace-pre-wrap text-sm text-slate-950">{value}</dd>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function Metric({ label, value }: { label: string; value: string }) {
|
|
return (
|
|
<div className="rounded-lg border bg-slate-50 p-3">
|
|
<dt className="text-xs font-semibold uppercase text-slate-500">{label}</dt>
|
|
<dd className="mt-1 text-lg font-semibold text-slate-950">{value}</dd>
|
|
</div>
|
|
);
|
|
}
|