feat(inventory): add spare parts management
This commit is contained in:
parent
abc6e308dd
commit
fb5c2cc26a
43 changed files with 2950 additions and 2 deletions
107
frontend/athena/components/inventory/StockActionDialog.tsx
Normal file
107
frontend/athena/components/inventory/StockActionDialog.tsx
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import type { InventoryItem, InventoryStockPayload } from "@/types/inventory";
|
||||
|
||||
export type StockAction = "adjust" | "reserve" | "release" | "consume";
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
item: InventoryItem | null;
|
||||
action: StockAction;
|
||||
pending: boolean;
|
||||
serverError: string;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onSubmit: (payload: InventoryStockPayload) => void;
|
||||
};
|
||||
|
||||
const labels: Record<StockAction, { title: string; description: string; reason: string }> = {
|
||||
adjust: {
|
||||
title: "Bestand anpassen",
|
||||
description: "Setzt den physischen Bestand auf den angegebenen Wert.",
|
||||
reason: "Inventurkorrektur",
|
||||
},
|
||||
reserve: {
|
||||
title: "Bestand reservieren",
|
||||
description: "Reserviert verfügbaren Bestand für einen späteren Vorgang.",
|
||||
reason: "Reservierung",
|
||||
},
|
||||
release: {
|
||||
title: "Reservierung aufheben",
|
||||
description: "Gibt reservierten Bestand wieder frei.",
|
||||
reason: "Reservierung aufgehoben",
|
||||
},
|
||||
consume: {
|
||||
title: "Verbrauch buchen",
|
||||
description: "Bucht Bestand als verbraucht aus.",
|
||||
reason: "Verbrauch",
|
||||
},
|
||||
};
|
||||
|
||||
export default function StockActionDialog({ open, item, action, pending, serverError, onOpenChange, onSubmit }: Props) {
|
||||
const [quantity, setQuantity] = useState(action === "adjust" ? item?.quantity_on_hand ?? 0 : 1);
|
||||
const [reason, setReason] = useState(labels[action].reason);
|
||||
const [note, setNote] = useState("");
|
||||
const [clientError, setClientError] = useState("");
|
||||
|
||||
function submit() {
|
||||
if (quantity <= 0 && action !== "adjust") {
|
||||
setClientError("Bitte eine Menge größer 0 angeben.");
|
||||
return;
|
||||
}
|
||||
if (quantity < 0) {
|
||||
setClientError("Bitte keine negative Menge eingeben.");
|
||||
return;
|
||||
}
|
||||
if (!reason.trim()) {
|
||||
setClientError("Bitte einen Grund angeben.");
|
||||
return;
|
||||
}
|
||||
setClientError("");
|
||||
onSubmit({
|
||||
quantity,
|
||||
reason: reason.trim(),
|
||||
note: note.trim() || null,
|
||||
reference_type: null,
|
||||
reference_id: null,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{labels[action].title}</DialogTitle>
|
||||
<DialogDescription>{item ? `${item.sku} · ${item.name}` : labels[action].description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="grid gap-3">
|
||||
<p className="rounded-lg border bg-slate-50 p-3 text-sm text-slate-600">
|
||||
Bestand {item?.quantity_on_hand ?? 0} · Reserviert {item?.quantity_reserved ?? 0} · Verfügbar {item?.quantity_available ?? 0}
|
||||
</p>
|
||||
<label className="grid gap-1 text-sm">Menge<Input type="number" min={action === "adjust" ? 0 : 1} value={quantity} onChange={(event) => setQuantity(Number(event.target.value))} /></label>
|
||||
<label className="grid gap-1 text-sm">Grund<Input value={reason} onChange={(event) => setReason(event.target.value)} /></label>
|
||||
<label className="grid gap-1 text-sm">Notiz<textarea className="min-h-20 rounded-lg border p-2 text-sm" value={note} onChange={(event) => setNote(event.target.value)} /></label>
|
||||
</div>
|
||||
|
||||
{(clientError || serverError) && <p className="text-sm text-red-600">{clientError || serverError}</p>}
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" onClick={() => onOpenChange(false)} disabled={pending}>Abbrechen</Button>
|
||||
<Button type="button" onClick={submit} disabled={pending}>{pending ? "Speichert..." : "Buchen"}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue