Files
app/src/admin/pages/WarehouseIssues.tsx
BOHA 39fe84ce99 feat(mui): consistent staggered page entrance across all 43 route pages
Adopt the PageEnter wrapper as every page's outermost render element and remove the ad-hoc per-page entrance motion.div wrappers. Every page now enters the same way — all top-level sections rise+fade in, staggered — so nothing appears instantly and the motion is identical app-wide. Presentation-only: no data/logic/hooks/invalidate/permissions touched. Embedded sub-tabs (CompanySettings, ReceivedInvoices), Login (auth shell) and the dev UiKit are intentionally excluded. Gates: tsc -b --noEmit=0, build=0, vitest 152/152.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 10:13:06 +02:00

292 lines
7.5 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 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,
PageEnter,
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 (
<PageEnter>
<PageHeader
title="Výdejky"
subtitle={subtitle}
actions={
canOperate ? (
<Button
startIcon={PlusIcon}
onClick={() => navigate("/warehouse/issues/new")}
>
Nová výdejka
</Button>
) : undefined
}
/>
<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>
</PageEnter>
);
}