feat(ui): list parity — row tinting, received-orders status filter, more sortable cols, search-aware empties, skeleton suppression

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
BOHA
2026-06-09 16:55:50 +02:00
parent 9fe5113c5b
commit 61673247bf
6 changed files with 215 additions and 36 deletions

View File

@@ -1,4 +1,4 @@
import { useState } from "react";
import { useState, useEffect, useRef } from "react";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { Link as RouterLink, useNavigate } from "react-router-dom";
import Box from "@mui/material/Box";
@@ -35,10 +35,20 @@ import {
LoadingState,
type DataColumn,
} from "../ui";
import { ORDER_STATUS, statusLabel, statusColor } from "../lib/documentStatus";
import {
ORDER_STATUS,
statusLabel,
statusColor,
statusOptions,
} from "../lib/documentStatus";
const API_BASE = "/api/admin";
const STATUS_OPTIONS = statusOptions(ORDER_STATUS, {
value: "",
label: "Všechny stavy",
});
interface Order {
id: number;
order_number: string;
@@ -139,7 +149,11 @@ export default function OrdersReceived({
const { sort, order, handleSort } = useTableSort("order_number");
const [search, setSearch] = useState("");
const debouncedSearch = useDebounce(search, 300);
const [status, setStatus] = useState("");
const [page, setPage] = useState(1);
// Track first successful load so later refetches (filter/status/page change)
// keep the table visible instead of flashing the full-page skeleton.
const hasLoadedOnce = useRef(false);
const [deleteConfirm, setDeleteConfirm] = useState<{
show: boolean;
@@ -297,11 +311,18 @@ export default function OrdersReceived({
sort,
order,
page,
status: status || undefined,
month,
year,
}),
);
// Mark first load done in an effect (not during render) to avoid a
// render-phase ref mutation.
useEffect(() => {
if (!isPending) hasLoadedOnce.current = true;
}, [isPending]);
if (!hasPermission("orders.view")) return <Forbidden />;
const handleDelete = async () => {
@@ -316,7 +337,10 @@ export default function OrdersReceived({
}
};
if (isPending) {
// Only show the full-page skeleton on the very first load; on subsequent
// refetches (filter/status/page change) keep the table visible (the Card
// dims via isFetching) so it doesn't flash.
if (isPending && !hasLoadedOnce.current) {
return <LoadingState />;
}
@@ -447,6 +471,32 @@ export default function OrdersReceived({
},
];
// Per-status row tints — subtle channel-alpha washes (never a solid `.light`
// fill, which is invisible-text in dark mode). Completed = success wash,
// cancelled (stornovana) = faded/muted, matching the Offers/Invoices intensity.
const rowSx = (o: Order) => {
if (o.status === "stornovana") {
return {
opacity: 0.6,
"& td": { color: "var(--mui-palette-text-secondary)" },
};
}
if (o.status === "dokoncena") {
return {
backgroundColor: "rgba(var(--mui-palette-success-mainChannel) / 0.12)",
"&:hover": {
backgroundColor:
"rgba(var(--mui-palette-success-mainChannel) / 0.18)",
},
};
}
return {};
};
// Search/status filter is active → an empty list means "nothing matches"
// (no create hint); a genuinely empty list keeps the explanatory empty state.
const isFiltered = !!debouncedSearch || !!status;
return (
<>
<FilterBar>
@@ -461,6 +511,16 @@ export default function OrdersReceived({
fullWidth
/>
</Box>
<Box sx={{ flex: "0 1 200px" }}>
<Select
value={status}
onChange={(value) => {
setStatus(value);
setPage(1);
}}
options={STATUS_OPTIONS}
/>
</Box>
</FilterBar>
<Card sx={{ opacity: isFetching ? 0.6 : 1, transition: "opacity .2s" }}>
@@ -468,14 +528,19 @@ export default function OrdersReceived({
columns={columns}
rows={orders}
rowKey={(o) => o.id}
rowSx={rowSx}
sortBy={sort}
sortDir={order}
onSort={handleSort}
empty={
<EmptyState
title="Zatím nejsou žádné objednávky."
description="Objednávky se vytvářejí z nabídek."
/>
isFiltered ? (
<EmptyState title="Žádné objednávky neodpovídají filtru." />
) : (
<EmptyState
title="Zatím nejsou žádné objednávky."
description="Objednávky se vytvářejí z nabídek."
/>
)
}
/>
<Pagination