feat(frontend): Lagerverwaltung-Ansicht (Modul 12)
/storage mit Lagerbehälter- und Lagerartikelverwaltung inkl. Bestandsbewegungen (Zugang/Abgang/Umlagerung/Korrektur).
This commit is contained in:
1 parent
15b6ea4edb
commit
7ec20199d3
8 files changed
+559
-1
No files matched your search
@@ -43,6 +43,9 @@ export default function AppLayout({ children }: { children: ReactNode }) {
|
|||||||
>
|
>
|
||||||
Honigchargen
|
Honigchargen
|
||||||
</Link>
|
</Link>
|
||||||
|
<Link className="text-sm text-muted-foreground hover:text-foreground" href="/storage">
|
||||||
|
Lager
|
||||||
|
</Link>
|
||||||
{user.role === "ADMIN" ? (
|
{user.role === "ADMIN" ? (
|
||||||
<Link
|
<Link
|
||||||
className="text-sm text-muted-foreground hover:text-foreground"
|
className="text-sm text-muted-foreground hover:text-foreground"
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { FormEvent, useState } from "react";
|
||||||
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { createStockItem } from "@/lib/storage/api";
|
||||||
|
import { StockCategory } from "@/lib/storage/types";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
|
||||||
|
const CATEGORY_OPTIONS: StockCategory[] = ["HONEY", "CONSUMABLE", "EQUIPMENT_MATERIAL"];
|
||||||
|
|
||||||
|
export function CreateStockItemDialog() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [name, setName] = useState("");
|
||||||
|
const [category, setCategory] = useState<StockCategory>("CONSUMABLE");
|
||||||
|
const [unit, setUnit] = useState("");
|
||||||
|
const [quantity, setQuantity] = useState("");
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: createStockItem,
|
||||||
|
onSuccess: () => {
|
||||||
|
void queryClient.invalidateQueries({ queryKey: ["stock-items"] });
|
||||||
|
setOpen(false);
|
||||||
|
setName("");
|
||||||
|
setUnit("");
|
||||||
|
setQuantity("");
|
||||||
|
setError(null);
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
setError("Lagerartikel konnte nicht angelegt werden.");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleSubmit(event: FormEvent<HTMLFormElement>) {
|
||||||
|
event.preventDefault();
|
||||||
|
setError(null);
|
||||||
|
mutation.mutate({
|
||||||
|
name,
|
||||||
|
category,
|
||||||
|
unit,
|
||||||
|
quantity: quantity ? Number(quantity) : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
|
<DialogTrigger render={<Button size="sm">+ Neuer Artikel</Button>} />
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Neuer Lagerartikel</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<form className="flex flex-col gap-4" onSubmit={handleSubmit}>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label htmlFor="item-name">Name</Label>
|
||||||
|
<Input
|
||||||
|
id="item-name"
|
||||||
|
required
|
||||||
|
value={name}
|
||||||
|
onChange={(event) => setName(event.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label htmlFor="item-category">Kategorie</Label>
|
||||||
|
<Select
|
||||||
|
value={category}
|
||||||
|
onValueChange={(value) => setCategory((value ?? "CONSUMABLE") as StockCategory)}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="item-category">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{CATEGORY_OPTIONS.map((option) => (
|
||||||
|
<SelectItem key={option} value={option}>
|
||||||
|
{option}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label htmlFor="item-unit">Einheit</Label>
|
||||||
|
<Input
|
||||||
|
id="item-unit"
|
||||||
|
required
|
||||||
|
value={unit}
|
||||||
|
onChange={(event) => setUnit(event.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label htmlFor="item-quantity">Anfangsbestand</Label>
|
||||||
|
<Input
|
||||||
|
id="item-quantity"
|
||||||
|
type="number"
|
||||||
|
inputMode="decimal"
|
||||||
|
value={quantity}
|
||||||
|
onChange={(event) => setQuantity(event.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{error ? (
|
||||||
|
<p role="alert" className="text-sm text-red-600">
|
||||||
|
{error}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
<DialogFooter>
|
||||||
|
<Button type="submit" disabled={mutation.isPending}>
|
||||||
|
{mutation.isPending ? "Wird angelegt…" : "Anlegen"}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { FormEvent, useState } from "react";
|
||||||
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { createStorageContainer } from "@/lib/storage/api";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
|
||||||
|
export function CreateStorageContainerDialog() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [code, setCode] = useState("");
|
||||||
|
const [containerType, setContainerType] = useState("");
|
||||||
|
const [capacity, setCapacity] = useState("");
|
||||||
|
const [location, setLocation] = useState("");
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: createStorageContainer,
|
||||||
|
onSuccess: () => {
|
||||||
|
void queryClient.invalidateQueries({ queryKey: ["storage-containers"] });
|
||||||
|
setOpen(false);
|
||||||
|
setCode("");
|
||||||
|
setContainerType("");
|
||||||
|
setCapacity("");
|
||||||
|
setLocation("");
|
||||||
|
setError(null);
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
setError("Lagerbehälter konnte nicht angelegt werden.");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleSubmit(event: FormEvent<HTMLFormElement>) {
|
||||||
|
event.preventDefault();
|
||||||
|
setError(null);
|
||||||
|
mutation.mutate({
|
||||||
|
code,
|
||||||
|
containerType: containerType || undefined,
|
||||||
|
capacity: capacity ? Number(capacity) : undefined,
|
||||||
|
location: location || undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
|
<DialogTrigger render={<Button size="sm">+ Neuer Behälter</Button>} />
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Neuer Lagerbehälter</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<form className="flex flex-col gap-4" onSubmit={handleSubmit}>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label htmlFor="container-code">Code</Label>
|
||||||
|
<Input
|
||||||
|
id="container-code"
|
||||||
|
required
|
||||||
|
value={code}
|
||||||
|
onChange={(event) => setCode(event.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label htmlFor="container-type">Typ</Label>
|
||||||
|
<Input
|
||||||
|
id="container-type"
|
||||||
|
value={containerType}
|
||||||
|
onChange={(event) => setContainerType(event.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label htmlFor="container-capacity">Kapazität (kg)</Label>
|
||||||
|
<Input
|
||||||
|
id="container-capacity"
|
||||||
|
type="number"
|
||||||
|
inputMode="decimal"
|
||||||
|
value={capacity}
|
||||||
|
onChange={(event) => setCapacity(event.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label htmlFor="container-location">Standort</Label>
|
||||||
|
<Input
|
||||||
|
id="container-location"
|
||||||
|
value={location}
|
||||||
|
onChange={(event) => setLocation(event.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{error ? (
|
||||||
|
<p role="alert" className="text-sm text-red-600">
|
||||||
|
{error}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
<DialogFooter>
|
||||||
|
<Button type="submit" disabled={mutation.isPending}>
|
||||||
|
{mutation.isPending ? "Wird angelegt…" : "Anlegen"}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { fetchStockItems, fetchStorageContainers } from "@/lib/storage/api";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table";
|
||||||
|
import { CreateStorageContainerDialog } from "./create-storage-container-dialog";
|
||||||
|
import { CreateStockItemDialog } from "./create-stock-item-dialog";
|
||||||
|
import { RecordMovementDialog } from "./record-movement-dialog";
|
||||||
|
|
||||||
|
export default function StoragePage() {
|
||||||
|
const { data: containers, isLoading: containersLoading } = useQuery({
|
||||||
|
queryKey: ["storage-containers"],
|
||||||
|
queryFn: fetchStorageContainers,
|
||||||
|
});
|
||||||
|
const { data: stockItems, isLoading: itemsLoading } = useQuery({
|
||||||
|
queryKey: ["stock-items"],
|
||||||
|
queryFn: fetchStockItems,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="flex flex-col gap-8 p-4 sm:p-6">
|
||||||
|
<section className="flex flex-col gap-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h1 className="text-xl font-semibold">Lagerbehälter</h1>
|
||||||
|
<CreateStorageContainerDialog />
|
||||||
|
</div>
|
||||||
|
{containersLoading ? <p className="text-sm text-muted-foreground">Lädt…</p> : null}
|
||||||
|
{containers ? (
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Code</TableHead>
|
||||||
|
<TableHead>Typ</TableHead>
|
||||||
|
<TableHead>Standort</TableHead>
|
||||||
|
<TableHead>Status</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{containers.map((container) => (
|
||||||
|
<TableRow key={container.id}>
|
||||||
|
<TableCell>{container.code}</TableCell>
|
||||||
|
<TableCell>{container.containerType ?? "—"}</TableCell>
|
||||||
|
<TableCell>{container.location ?? "—"}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge variant={container.status === "FILLED" ? "default" : "secondary"}>
|
||||||
|
{container.status}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
) : null}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="flex flex-col gap-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h2 className="text-xl font-semibold">Lagerartikel</h2>
|
||||||
|
<CreateStockItemDialog />
|
||||||
|
</div>
|
||||||
|
{itemsLoading ? <p className="text-sm text-muted-foreground">Lädt…</p> : null}
|
||||||
|
{stockItems ? (
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Name</TableHead>
|
||||||
|
<TableHead>Kategorie</TableHead>
|
||||||
|
<TableHead>Bestand</TableHead>
|
||||||
|
<TableHead>Aktion</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{stockItems.map((item) => (
|
||||||
|
<TableRow key={item.id}>
|
||||||
|
<TableCell>{item.name}</TableCell>
|
||||||
|
<TableCell>{item.category}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{item.quantity} {item.unit}
|
||||||
|
{item.minThreshold !== null && item.quantity < item.minThreshold ? (
|
||||||
|
<Badge variant="destructive" className="ml-2">
|
||||||
|
Niedrig
|
||||||
|
</Badge>
|
||||||
|
) : null}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<RecordMovementDialog stockItemId={item.id} />
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
) : null}
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { FormEvent, useState } from "react";
|
||||||
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { addStockMovement } from "@/lib/storage/api";
|
||||||
|
import { StockMovementType } from "@/lib/storage/types";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
|
||||||
|
const TYPE_OPTIONS: StockMovementType[] = ["IN", "OUT", "TRANSFER", "ADJUSTMENT"];
|
||||||
|
|
||||||
|
export function RecordMovementDialog({ stockItemId }: { stockItemId: string }) {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [type, setType] = useState<StockMovementType>("IN");
|
||||||
|
const [quantity, setQuantity] = useState("");
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: (input: { type: StockMovementType; quantity: number }) =>
|
||||||
|
addStockMovement(stockItemId, input),
|
||||||
|
onSuccess: () => {
|
||||||
|
void queryClient.invalidateQueries({ queryKey: ["stock-items"] });
|
||||||
|
setOpen(false);
|
||||||
|
setQuantity("");
|
||||||
|
setError(null);
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
setError("Bewegung konnte nicht gespeichert werden.");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleSubmit(event: FormEvent<HTMLFormElement>) {
|
||||||
|
event.preventDefault();
|
||||||
|
setError(null);
|
||||||
|
if (!quantity) {
|
||||||
|
setError("Bitte eine Menge angeben.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mutation.mutate({ type, quantity: Number(quantity) });
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
|
<DialogTrigger render={<Button size="sm" variant="outline">Bewegung buchen</Button>} />
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Bestandsbewegung</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<form className="flex flex-col gap-4" onSubmit={handleSubmit}>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label htmlFor="movement-type">Art</Label>
|
||||||
|
<Select
|
||||||
|
value={type}
|
||||||
|
onValueChange={(value) => setType((value ?? "IN") as StockMovementType)}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="movement-type">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{TYPE_OPTIONS.map((option) => (
|
||||||
|
<SelectItem key={option} value={option}>
|
||||||
|
{option}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label htmlFor="movement-quantity">Menge</Label>
|
||||||
|
<Input
|
||||||
|
id="movement-quantity"
|
||||||
|
type="number"
|
||||||
|
inputMode="decimal"
|
||||||
|
required
|
||||||
|
value={quantity}
|
||||||
|
onChange={(event) => setQuantity(event.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{error ? (
|
||||||
|
<p role="alert" className="text-sm text-red-600">
|
||||||
|
{error}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
<DialogFooter>
|
||||||
|
<Button type="submit" disabled={mutation.isPending}>
|
||||||
|
{mutation.isPending ? "Wird gespeichert…" : "Buchen"}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { apiFetch } from "@/lib/auth/api-client";
|
||||||
|
import {
|
||||||
|
CreateStockItemInput,
|
||||||
|
CreateStockMovementInput,
|
||||||
|
CreateStorageContainerInput,
|
||||||
|
StockItem,
|
||||||
|
StorageContainer,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
|
export function fetchStorageContainers(): Promise<StorageContainer[]> {
|
||||||
|
return apiFetch<StorageContainer[]>("/storage-containers");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createStorageContainer(
|
||||||
|
input: CreateStorageContainerInput,
|
||||||
|
): Promise<StorageContainer> {
|
||||||
|
return apiFetch<StorageContainer>("/storage-containers", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(input),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchStockItems(): Promise<StockItem[]> {
|
||||||
|
return apiFetch<StockItem[]>("/stock-items");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createStockItem(input: CreateStockItemInput): Promise<StockItem> {
|
||||||
|
return apiFetch<StockItem>("/stock-items", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(input),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addStockMovement(
|
||||||
|
id: string,
|
||||||
|
input: CreateStockMovementInput,
|
||||||
|
): Promise<StockItem> {
|
||||||
|
return apiFetch<StockItem>(`/stock-items/${id}/movements`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(input),
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
export type StockCategory = "HONEY" | "CONSUMABLE" | "EQUIPMENT_MATERIAL";
|
||||||
|
export type StockMovementType = "IN" | "OUT" | "TRANSFER" | "ADJUSTMENT";
|
||||||
|
export type StorageContainerStatus = "EMPTY" | "FILLED" | "CLEANING" | "OUT_OF_SERVICE";
|
||||||
|
|
||||||
|
export interface StorageContainer {
|
||||||
|
id: string;
|
||||||
|
code: string;
|
||||||
|
containerType: string | null;
|
||||||
|
capacity: number | null;
|
||||||
|
currentAmount: number | null;
|
||||||
|
location: string | null;
|
||||||
|
status: StorageContainerStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StockMovement {
|
||||||
|
id: string;
|
||||||
|
stockItemId: string;
|
||||||
|
type: StockMovementType;
|
||||||
|
quantity: number;
|
||||||
|
date: string;
|
||||||
|
relatedType: string | null;
|
||||||
|
relatedId: string | null;
|
||||||
|
notes: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StockItem {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
category: StockCategory;
|
||||||
|
unit: string;
|
||||||
|
quantity: number;
|
||||||
|
minThreshold: number | null;
|
||||||
|
notes: string | null;
|
||||||
|
movements?: StockMovement[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateStorageContainerInput {
|
||||||
|
code: string;
|
||||||
|
containerType?: string;
|
||||||
|
capacity?: number;
|
||||||
|
location?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateStockItemInput {
|
||||||
|
name: string;
|
||||||
|
category: StockCategory;
|
||||||
|
unit: string;
|
||||||
|
quantity?: number;
|
||||||
|
minThreshold?: number;
|
||||||
|
notes?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateStockMovementInput {
|
||||||
|
type: StockMovementType;
|
||||||
|
quantity: number;
|
||||||
|
notes?: string;
|
||||||
|
}
|
||||||
@@ -107,7 +107,7 @@ Abgeleitet aus [`docs/lastenheft.md`](docs/lastenheft.md) Abschnitt 26. Reihenfo
|
|||||||
- Endpunkte: `GET/POST /api/v1/storage-containers`, `GET/POST /api/v1/stock-items`, `POST /api/v1/stock-items/:id/movements`
|
- Endpunkte: `GET/POST /api/v1/storage-containers`, `GET/POST /api/v1/stock-items`, `POST /api/v1/stock-items/:id/movements`
|
||||||
- Frontend: `/storage`
|
- Frontend: `/storage`
|
||||||
- [x] Backend-Modul fertig, getestet, grün, committet
|
- [x] Backend-Modul fertig, getestet, grün, committet
|
||||||
- [ ] Frontend-Views fertig, getestet, grün, committet
|
- [x] Frontend-Views fertig, getestet, grün, committet
|
||||||
|
|
||||||
### 13. Dashboard
|
### 13. Dashboard
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user