Files
app/src/admin/pages/WarehouseIssues.tsx
BOHA 4fb52e9626 feat(mui): migrate Warehouse Issues (Výdeje) onto MUI kit
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 23:35:22 +02:00

298 lines
7.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { useQuery } from "@tanstack/react-query";
import { useAuth } from "../context/AuthContext";
import Forbidden from "../components/Forbidden";
import { motion } from "framer-motion";
import Box from "@mui/material/Box";
import useDebounce from "../hooks/useDebounce";
import { usePaginatedQuery } from "../hooks/usePaginatedQuery";
import { formatDate } from "../utils/formatters";
import {
warehouseIssueListOptions,
type WarehouseIssue,
} from "../lib/queries/warehouse";
import { projectListOptions, type Project } from "../lib/queries/projects";
import {
Button,
Card,
DataTable,
Pagination,
TextField,
Select,
DateField,
StatusChip,
PageHeader,
FilterBar,
EmptyState,
LoadingState,
type DataColumn,
} from "../ui";
const PER_PAGE = 20;
const STATUS_COLOR: Record<
string,
"warning" | "success" | "error" | "info" | "default"
> = {
DRAFT: "warning",
CONFIRMED: "success",
CANCELLED: "error",
};
const STATUS_LABEL: Record<string, string> = {
DRAFT: "Návrh",
CONFIRMED: "Potvrzeno",
CANCELLED: "Zrušeno",
};
const PlusIcon = (
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<line x1="12" y1="5" x2="12" y2="19" />
<line x1="5" y1="12" x2="19" y2="12" />
</svg>
);
export default function WarehouseIssues() {
const { hasPermission } = useAuth();
const navigate = useNavigate();
const [page, setPage] = useState(1);
const [search, setSearch] = useState("");
const [statusFilter, setStatusFilter] = useState<string>("");
const [projectId, setProjectId] = useState<number | "">("");
const [dateFrom, setDateFrom] = useState("");
const [dateTo, setDateTo] = useState("");
const debouncedSearch = useDebounce(search, 300);
const { data: projectsData } = useQuery(projectListOptions({ perPage: 100 }));
const projects = projectsData?.data ?? [];
const { items, pagination, isPending, isFetching } =
usePaginatedQuery<WarehouseIssue>(
warehouseIssueListOptions({
search: debouncedSearch || undefined,
page,
perPage: PER_PAGE,
status: statusFilter || undefined,
project_id: projectId ? Number(projectId) : undefined,
date_from: dateFrom || undefined,
date_to: dateTo || undefined,
}),
);
if (!hasPermission("warehouse.view")) return <Forbidden />;
const canOperate = hasPermission("warehouse.operate");
const resetFilters = () => {
setSearch("");
setStatusFilter("");
setProjectId("");
setDateFrom("");
setDateTo("");
setPage(1);
};
const hasActiveFilters = !!(statusFilter || projectId || dateFrom || dateTo);
if (isPending) {
return <LoadingState />;
}
const total = pagination?.total ?? items.length;
const subtitle = `${total} ${
total === 1 ? "doklad" : total >= 2 && total <= 4 ? "doklady" : "dokladů"
}`;
const columns: DataColumn<WarehouseIssue>[] = [
{
key: "issue_number",
header: "Číslo dokladu",
width: "15%",
mono: true,
bold: true,
render: (r) => r.issue_number || "—",
},
{
key: "project",
header: "Projekt",
width: "25%",
render: (r) =>
r.project ? `${r.project.project_number} ${r.project.name}` : "—",
},
{
key: "status",
header: "Stav",
width: "13%",
render: (r) => (
<StatusChip
label={STATUS_LABEL[r.status] ?? r.status}
color={STATUS_COLOR[r.status] ?? "default"}
/>
),
},
{
key: "issued_by_user",
header: "Vydal",
width: "20%",
render: (r) =>
r.issued_by_user
? `${r.issued_by_user.first_name} ${r.issued_by_user.last_name}`
: "—",
},
{
key: "created_at",
header: "Vytvořeno",
width: "14%",
mono: true,
render: (r) => formatDate(r.created_at),
},
{
key: "items_count",
header: "Položek",
width: "13%",
mono: true,
render: (r) => String(r._count?.items ?? 0),
},
];
return (
<Box>
<motion.div
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.25 }}
>
<PageHeader
title="Výdejky"
subtitle={subtitle}
actions={
canOperate ? (
<Button
startIcon={PlusIcon}
onClick={() => navigate("/warehouse/issues/new")}
>
Nová výdejka
</Button>
) : undefined
}
/>
</motion.div>
<FilterBar>
<Box sx={{ flex: "1 1 220px" }}>
<TextField
value={search}
onChange={(e) => {
setSearch(e.target.value);
setPage(1);
}}
placeholder="Hledat podle čísla dokladu..."
fullWidth
/>
</Box>
<Box sx={{ flex: "0 0 160px" }}>
<Select
value={statusFilter}
onChange={(val) => {
setStatusFilter(val);
setPage(1);
}}
options={[
{ value: "", label: "Všechny stavy" },
{ value: "DRAFT", label: "Návrh" },
{ value: "CONFIRMED", label: "Potvrzeno" },
{ value: "CANCELLED", label: "Zrušeno" },
]}
/>
</Box>
{projects.length > 0 && (
<Box sx={{ flex: "0 0 200px" }}>
<Select
value={projectId === "" ? "" : String(projectId)}
onChange={(val) => {
setProjectId(val === "" ? "" : Number(val));
setPage(1);
}}
options={[
{ value: "", label: "Všechny projekty" },
...projects.map((p: Project) => ({
value: String(p.id),
label: `${p.project_number} ${p.name}`,
})),
]}
/>
</Box>
)}
<Box sx={{ flex: "0 0 150px" }}>
<DateField
value={dateFrom}
onChange={(val) => {
setDateFrom(val);
setPage(1);
}}
label="Od"
/>
</Box>
<Box sx={{ flex: "0 0 150px" }}>
<DateField
value={dateTo}
onChange={(val) => {
setDateTo(val);
setPage(1);
}}
label="Do"
/>
</Box>
{hasActiveFilters && (
<Button variant="outlined" onClick={resetFilters}>
Zrušit filtry
</Button>
)}
</FilterBar>
<Card sx={{ opacity: isFetching ? 0.6 : 1, transition: "opacity .2s" }}>
<DataTable<WarehouseIssue>
columns={columns}
rows={items}
rowKey={(r) => r.id}
onRowClick={(r) => navigate(`/warehouse/issues/${r.id}`)}
empty={
search || hasActiveFilters ? (
<EmptyState title="Žádné doklady pro zadaný filtr." />
) : (
<EmptyState
title="Zatím nejsou žádné výdejky."
action={
canOperate ? (
<Button
startIcon={PlusIcon}
onClick={() => navigate("/warehouse/issues/new")}
>
Vytvořit první výdejku
</Button>
) : undefined
}
/>
)
}
/>
<Pagination
page={page}
pageCount={pagination?.total_pages ?? 1}
onChange={setPage}
/>
</Card>
</Box>
);
}