The text color on filled colored controls was inconsistent: contained primary resolved to white in both schemes, but error/success/warning/info used a per-scheme contrastText (#fff light, #1a1a1a dark). So in DARK mode a primary (red) button showed white text while an error (red) button — e.g. every ConfirmDialog delete confirm, the contained "Smazat", and the dashboard quick-action tiles — showed near-black text. "Black text on one red button, white on another." Unify on a single rule: every FILLED colored surface gets WHITE text/glyph in BOTH themes. The palette .main stays bright in dark mode (it drives text, outlined buttons and row tints), but a bright fill makes white illegible, so filled surfaces drop to a darker fill in dark mode (new FILLED_DARK_BG, ≈ the light-scheme shades; all ≥4.5:1 with white): - theme MuiButton: contained error/success/warning/info -> white text + darker dark-mode fill (disabled state preserved). - theme MuiChip: filled error/success/warning/info -> white label + darker dark-mode fill (covers StatusChip + status pills). - StatCard icon badges: white glyph + darker dark-mode tile. Plus two consistency cleanups: - Detail-page "Smazat" unified to variant=outlined color=error (Offer + Invoice now match Order/Project/Warehouse; the contained->confirm pattern stays: subtle trigger, strong contained confirm dialog). - "Zrušit filtry" reset buttons -> color=inherit (neutral), matching the rest of the secondary-button family (were default outlined-primary). Neutral (color=inherit) text/outlined buttons were already correct (label = ambient text color) and are untouched. tsc -b --noEmit, npm run build and vitest 152/152 all clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
199 lines
4.6 KiB
TypeScript
199 lines
4.6 KiB
TypeScript
import { useState } from "react";
|
|
import { useNavigate } from "react-router-dom";
|
|
import Box from "@mui/material/Box";
|
|
import { useAuth } from "../context/AuthContext";
|
|
import Forbidden from "../components/Forbidden";
|
|
import { usePaginatedQuery } from "../hooks/usePaginatedQuery";
|
|
|
|
import { formatDate } from "../utils/formatters";
|
|
import {
|
|
warehouseInventoryListOptions,
|
|
type WarehouseInventorySession,
|
|
} from "../lib/queries/warehouse";
|
|
import {
|
|
Button,
|
|
Card,
|
|
DataTable,
|
|
Pagination,
|
|
Select,
|
|
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",
|
|
};
|
|
|
|
const STATUS_LABEL: Record<string, string> = {
|
|
DRAFT: "Návrh",
|
|
CONFIRMED: "Potvrzeno",
|
|
};
|
|
|
|
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 WarehouseInventory() {
|
|
const { hasPermission } = useAuth();
|
|
const navigate = useNavigate();
|
|
|
|
const [page, setPage] = useState(1);
|
|
const [statusFilter, setStatusFilter] = useState<string>("");
|
|
|
|
const { items, pagination, isPending, isFetching } =
|
|
usePaginatedQuery<WarehouseInventorySession>(
|
|
warehouseInventoryListOptions({
|
|
page,
|
|
perPage: PER_PAGE,
|
|
status: statusFilter || undefined,
|
|
}),
|
|
);
|
|
|
|
if (!hasPermission("warehouse.inventory")) return <Forbidden />;
|
|
|
|
const resetFilters = () => {
|
|
setStatusFilter("");
|
|
setPage(1);
|
|
};
|
|
|
|
if (isPending) {
|
|
return <LoadingState />;
|
|
}
|
|
|
|
const total = pagination?.total ?? items.length;
|
|
const subtitle = `${total} ${
|
|
total === 1
|
|
? "inventura"
|
|
: total >= 2 && total <= 4
|
|
? "inventury"
|
|
: "inventur"
|
|
}`;
|
|
|
|
const columns: DataColumn<WarehouseInventorySession>[] = [
|
|
{
|
|
key: "session_number",
|
|
header: "Číslo inventury",
|
|
width: "30%",
|
|
mono: true,
|
|
bold: true,
|
|
render: (s) => s.session_number || "—",
|
|
},
|
|
{
|
|
key: "status",
|
|
header: "Stav",
|
|
width: "20%",
|
|
render: (s) => (
|
|
<StatusChip
|
|
label={STATUS_LABEL[s.status] ?? s.status}
|
|
color={STATUS_COLOR[s.status] ?? "default"}
|
|
/>
|
|
),
|
|
},
|
|
{
|
|
key: "created_at",
|
|
header: "Vytvořeno",
|
|
width: "30%",
|
|
mono: true,
|
|
render: (s) => formatDate(s.created_at),
|
|
},
|
|
{
|
|
key: "items",
|
|
header: "Položky",
|
|
width: "20%",
|
|
mono: true,
|
|
render: (s) => String(s.items?.length ?? 0),
|
|
},
|
|
];
|
|
|
|
return (
|
|
<PageEnter>
|
|
<PageHeader
|
|
title="Inventury"
|
|
subtitle={subtitle}
|
|
actions={
|
|
<Button
|
|
startIcon={PlusIcon}
|
|
onClick={() => navigate("/warehouse/inventory/new")}
|
|
>
|
|
Nová inventura
|
|
</Button>
|
|
}
|
|
/>
|
|
|
|
<FilterBar>
|
|
<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" },
|
|
]}
|
|
/>
|
|
</Box>
|
|
{statusFilter && (
|
|
<Button variant="outlined" color="inherit" onClick={resetFilters}>
|
|
Zrušit filtry
|
|
</Button>
|
|
)}
|
|
</FilterBar>
|
|
|
|
<Card sx={{ opacity: isFetching ? 0.6 : 1, transition: "opacity .2s" }}>
|
|
<DataTable<WarehouseInventorySession>
|
|
columns={columns}
|
|
rows={items}
|
|
rowKey={(s) => s.id}
|
|
onRowClick={(s) => navigate(`/warehouse/inventory/${s.id}`)}
|
|
empty={
|
|
statusFilter ? (
|
|
<EmptyState title="Žádné inventury pro zadaný filtr." />
|
|
) : (
|
|
<EmptyState
|
|
title="Zatím nejsou žádné inventury."
|
|
action={
|
|
<Button
|
|
startIcon={PlusIcon}
|
|
onClick={() => navigate("/warehouse/inventory/new")}
|
|
>
|
|
Vytvořit první inventuru
|
|
</Button>
|
|
}
|
|
/>
|
|
)
|
|
}
|
|
/>
|
|
<Pagination
|
|
page={page}
|
|
pageCount={pagination?.total_pages ?? 1}
|
|
onChange={setPage}
|
|
/>
|
|
</Card>
|
|
</PageEnter>
|
|
);
|
|
}
|