Files
app/src/admin/pages/WarehouseIssueDetail.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

401 lines
11 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 { useParams, useNavigate, Link as RouterLink } from "react-router-dom";
import { useQuery } from "@tanstack/react-query";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import { useAlert } from "../context/AlertContext";
import { useAuth } from "../context/AuthContext";
import Forbidden from "../components/Forbidden";
import { formatCurrency, formatDate } from "../utils/formatters";
import {
warehouseIssueDetailOptions,
type WarehouseIssue,
type WarehouseIssueItem,
} from "../lib/queries/warehouse";
import { useApiMutation } from "../lib/queries/mutations";
import {
Button,
Card,
DataTable,
StatusChip,
EmptyState,
LoadingState,
PageHeader,
ConfirmDialog,
PageEnter,
type DataColumn,
} from "../ui";
const STATUS_COLOR: Record<
string,
"default" | "success" | "error" | "warning" | "info"
> = {
DRAFT: "warning",
CONFIRMED: "success",
CANCELLED: "error",
};
const STATUS_LABEL: Record<string, string> = {
DRAFT: "Návrh",
CONFIRMED: "Potvrzeno",
CANCELLED: "Zrušeno",
};
const BackIcon = (
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path d="M19 12H5M12 19l-7-7 7-7" />
</svg>
);
export default function WarehouseIssueDetail() {
const { id } = useParams();
const alert = useAlert();
const { hasPermission } = useAuth();
const navigate = useNavigate();
const [cancelConfirm, setCancelConfirm] = useState(false);
const {
data: issue,
isPending,
error,
} = useQuery(warehouseIssueDetailOptions(id));
if (!hasPermission("warehouse.view")) return <Forbidden />;
const canOperate = hasPermission("warehouse.operate");
const confirmMutation = useApiMutation<void, void>({
url: () =>
issue
? `/api/admin/warehouse/issues/${issue.id}/confirm`
: "/api/admin/warehouse/issues/0/confirm",
method: () => "POST",
invalidate: ["warehouse"],
onSuccess: () => {
alert.success("Výdejka byla potvrzena");
},
});
const cancelMutation = useApiMutation<void, void>({
url: () =>
issue
? `/api/admin/warehouse/issues/${issue.id}/cancel`
: "/api/admin/warehouse/issues/0/cancel",
method: () => "POST",
invalidate: ["warehouse"],
onSuccess: () => {
setCancelConfirm(false);
alert.success("Výdejka byla zrušena");
},
});
const handleConfirm = async () => {
if (!issue) return;
try {
await confirmMutation.mutateAsync(undefined);
} catch (e) {
alert.error(e instanceof Error ? e.message : "Chyba připojení");
}
};
const handleCancel = async () => {
if (!issue) return;
try {
await cancelMutation.mutateAsync(undefined);
} catch (e) {
alert.error(e instanceof Error ? e.message : "Chyba připojení");
}
};
const confirming = confirmMutation.isPending;
const cancelling = cancelMutation.isPending;
if (isPending) {
return <LoadingState />;
}
if (error || !issue) {
return (
<Box>
<PageHeader
title="Výdejka"
actions={
<Button
component={RouterLink}
to="/warehouse/issues"
variant="outlined"
color="inherit"
startIcon={BackIcon}
>
Zpět
</Button>
}
/>
<Card>
<EmptyState title="Výdejka nebyla nalezena." />
</Card>
</Box>
);
}
const iss = issue as WarehouseIssue;
const items = iss.items ?? [];
const itemColumns: DataColumn<WarehouseIssueItem>[] = [
{
key: "item",
header: "Položka",
width: "20%",
bold: true,
render: (row) => row.item?.name || `ID: ${row.item_id}`,
},
{
key: "batch",
header: "Šarže",
width: "18%",
mono: true,
render: (row) =>
row.batch
? `${formatDate(row.batch.received_at)} ${formatCurrency(Number(row.batch.unit_price), "CZK")}`
: "—",
},
{
key: "quantity",
header: "Množství",
width: "10%",
mono: true,
render: (row) => String(Number(row.quantity)),
},
{
key: "unit_price",
header: "Cena/ks",
width: "13%",
mono: true,
render: (row) =>
row.batch ? formatCurrency(Number(row.batch.unit_price), "CZK") : "—",
},
{
key: "total",
header: "Celkem",
width: "13%",
mono: true,
bold: true,
render: (row) =>
row.batch
? formatCurrency(
Number(row.quantity) * Number(row.batch.unit_price),
"CZK",
)
: "—",
},
{
key: "location",
header: "Lokace",
width: "10%",
mono: true,
render: (row) => row.location?.code || "—",
},
{
key: "reservation",
header: "Rezervace",
width: "10%",
mono: true,
render: (row) =>
row.reservation
? `ID: ${row.reservation.id} (${Number(row.reservation.remaining_qty)} ks)`
: "—",
},
{
key: "notes",
header: "Poznámka",
width: "6%",
render: (row) => row.notes || "—",
},
];
return (
<PageEnter>
{/* Header */}
<PageHeader
title={iss.issue_number || "Nová výdejka"}
subtitle={
iss.project
? `${iss.project.project_number} ${iss.project.name}`
: undefined
}
actions={
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
<Button
component={RouterLink}
to="/warehouse/issues"
variant="outlined"
color="inherit"
startIcon={BackIcon}
>
Zpět
</Button>
<StatusChip
label={STATUS_LABEL[iss.status] ?? iss.status}
color={STATUS_COLOR[iss.status] ?? "default"}
/>
{canOperate && iss.status === "DRAFT" && (
<>
<Button
variant="outlined"
color="inherit"
onClick={() => navigate(`/warehouse/issues/${iss.id}/edit`)}
>
Upravit
</Button>
<Button onClick={handleConfirm} disabled={confirming}>
{confirming ? "Potvrzování..." : "Potvrdit"}
</Button>
<Button
variant="outlined"
color="error"
onClick={() => setCancelConfirm(true)}
>
Zrušit
</Button>
</>
)}
{canOperate && iss.status === "CONFIRMED" && (
<Button
variant="outlined"
color="error"
onClick={() => setCancelConfirm(true)}
>
Zrušit
</Button>
)}
</Box>
}
/>
{/* Basic info */}
<Card sx={{ mb: 3 }}>
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 600 }}>
Základní údaje
</Typography>
<Box
sx={{
display: "grid",
gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr 1fr" },
gap: 2,
}}
>
<Box>
<Typography variant="caption" color="text.secondary">
Číslo dokladu
</Typography>
<Typography
variant="body2"
sx={{
fontFamily: "'DM Mono', Menlo, monospace",
fontWeight: 500,
}}
>
{iss.issue_number || "—"}
</Typography>
</Box>
<Box>
<Typography variant="caption" color="text.secondary">
Projekt
</Typography>
{iss.project ? (
<Typography
variant="body2"
component="a"
href={`/projects/${iss.project.id}`}
sx={{
fontFamily: "'DM Mono', Menlo, monospace",
fontWeight: 500,
color: "primary.main",
textDecoration: "none",
"&:hover": { textDecoration: "underline" },
display: "block",
}}
>
{iss.project.project_number} {iss.project.name}
</Typography>
) : (
<Typography variant="body2"></Typography>
)}
</Box>
<Box>
<Typography variant="caption" color="text.secondary">
Vydal
</Typography>
<Typography
variant="body2"
sx={{
fontFamily: "'DM Mono', Menlo, monospace",
fontWeight: 500,
}}
>
{iss.issued_by_user
? `${iss.issued_by_user.first_name} ${iss.issued_by_user.last_name}`
: "—"}
</Typography>
</Box>
<Box>
<Typography variant="caption" color="text.secondary">
Vytvořeno
</Typography>
<Typography
variant="body2"
sx={{ fontFamily: "'DM Mono', Menlo, monospace" }}
>
{formatDate(iss.created_at)}
</Typography>
</Box>
{iss.notes && (
<Box sx={{ gridColumn: { sm: "1 / -1" } }}>
<Typography variant="caption" color="text.secondary">
Poznámky
</Typography>
<Typography variant="body2" sx={{ whiteSpace: "pre-wrap" }}>
{iss.notes}
</Typography>
</Box>
)}
</Box>
</Card>
{/* Lines table */}
<Card sx={{ mb: 3 }}>
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 600 }}>
Položky
</Typography>
<DataTable<WarehouseIssueItem>
columns={itemColumns}
rows={items}
rowKey={(row) => row.id}
empty={<EmptyState title="Žádné řádky" />}
/>
</Card>
{/* Cancel confirmation dialog */}
<ConfirmDialog
isOpen={cancelConfirm}
onClose={() => setCancelConfirm(false)}
onConfirm={handleCancel}
title="Zrušit výdejku"
message={`Opravdu chcete zrušit výdejku „${iss.issue_number || iss.id}"?`}
confirmText="Zrušit výdejku"
confirmVariant="danger"
loading={cancelling}
/>
</PageEnter>
);
}