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 { useNavigate, Link as RouterLink } from "react-router-dom";
import Box from "@mui/material/Box";
import IconButton from "@mui/material/IconButton";
@@ -101,6 +101,9 @@ export default function IssuedOrders({ month, year }: IssuedOrdersProps) {
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;
@@ -138,6 +141,12 @@ export default function IssuedOrders({ month, year }: IssuedOrdersProps) {
}),
);
// 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 () => {
@@ -173,7 +182,10 @@ export default function IssuedOrders({ month, year }: IssuedOrdersProps) {
}
};
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 />;
}
@@ -274,6 +286,32 @@ export default function IssuedOrders({ month, year }: IssuedOrdersProps) {
},
];
// Per-status row tints — subtle channel-alpha washes (never a solid `.light`
// fill, which is invisible-text in dark mode). Completed = success wash,
// cancelled = faded/muted, matching the Offers/Invoices intensity.
const rowSx = (o: IssuedOrder) => {
if (o.status === "cancelled") {
return {
opacity: 0.6,
"& td": { color: "var(--mui-palette-text-secondary)" },
};
}
if (o.status === "completed") {
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 CTA); a genuinely empty list keeps the create-CTA empty state.
const isFiltered = !!debouncedSearch || !!status;
return (
<>
<FilterBar>
@@ -305,18 +343,23 @@ export default function IssuedOrders({ month, year }: IssuedOrdersProps) {
columns={columns}
rows={orders}
rowKey={(o) => o.id}
rowSx={rowSx}
sortBy={sort}
sortDir={order}
onSort={handleSort}
empty={
<EmptyState
title="Zatím žádné vydané objednávky."
description={
hasPermission("orders.create")
? "Vytvořte první tlačítkem „Vytvořit objednávku vydanou“."
: undefined
}
/>
isFiltered ? (
<EmptyState title="Žádné objednávky neodpovídají filtru." />
) : (
<EmptyState
title="Zatím žádné vydané objednávky."
description={
hasPermission("orders.create")
? "Vytvořte první tlačítkem „Vytvořit objednávku vydanou“."
: undefined
}
/>
)
}
/>
<Pagination