diff --git a/src/admin/lib/queries/orders.ts b/src/admin/lib/queries/orders.ts index df2f06b..ad4f8f9 100644 --- a/src/admin/lib/queries/orders.ts +++ b/src/admin/lib/queries/orders.ts @@ -61,6 +61,7 @@ export const orderListOptions = (filters: { order?: string; page?: number; perPage?: number; + status?: string; month?: number; year?: number; }) => @@ -73,6 +74,7 @@ export const orderListOptions = (filters: { if (filters.order) params.set("order", filters.order); if (filters.page) params.set("page", String(filters.page)); if (filters.perPage) params.set("per_page", String(filters.perPage)); + if (filters.status) params.set("status", filters.status); if (filters.month) params.set("month", String(filters.month)); if (filters.year) params.set("year", String(filters.year)); const qs = params.toString(); diff --git a/src/admin/pages/Invoices.tsx b/src/admin/pages/Invoices.tsx index 8349f1f..e7b1690 100644 --- a/src/admin/pages/Invoices.tsx +++ b/src/admin/pages/Invoices.tsx @@ -233,6 +233,9 @@ export default function Invoices() { const [statsMonth, setStatsMonth] = useState(now.getMonth() + 1); const [statsYear, setStatsYear] = useState(now.getFullYear()); const blobUrlRef = useRef(null); + // Track first successful list load so later refetches (filter/status/month/ + // page change) keep the table visible instead of flashing the full skeleton. + const hasLoadedOnce = useRef(false); useEffect(() => { return () => { @@ -312,6 +315,12 @@ export default function Invoices() { }), ); + // Mark first load done in an effect (not during render) to avoid a + // render-phase ref mutation. + useEffect(() => { + if (!initialLoad) hasLoadedOnce.current = true; + }, [initialLoad]); + if (!hasPermission("invoices.view")) return ; const handleDelete = async () => { @@ -391,7 +400,10 @@ export default function Invoices() { } }; - if (initialLoad) { + // Only show the full-page skeleton on the very first load; on subsequent + // refetches (filter/status/month/page change) keep the table visible (the + // Card dims via `loading`/isFetching) so it doesn't flash. + if (initialLoad && !hasLoadedOnce.current) { return ; } @@ -832,14 +844,18 @@ export default function Invoices() { sortDir={order} onSort={handleSort} empty={ - + search || statusFilter ? ( + + ) : ( + + ) } /> { + if (!isPending) hasLoadedOnce.current = true; + }, [isPending]); + if (!hasPermission("orders.view")) return ; 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 ; } @@ -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 ( <> @@ -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={ - + isFiltered ? ( + + ) : ( + + ) } /> (""); + // Track first successful load so later refetches (filter/customer/tab/page + // change) keep the table visible instead of flashing the full skeleton. + const hasLoadedOnce = useRef(false); const { data: customers } = useQuery(offerCustomersOptions()); @@ -303,6 +306,12 @@ export default function Offers() { }), ); + // Mark first load done in an effect (not during render) to avoid a + // render-phase ref mutation. + useEffect(() => { + if (!isPending) hasLoadedOnce.current = true; + }, [isPending]); + const duplicateMutation = useApiMutation< number, { message?: string; error?: string } @@ -456,7 +465,10 @@ export default function Offers() { } }; - if (isPending) { + // Only show the full-page skeleton on the very first load; on subsequent + // refetches (filter/customer/tab/page change) keep the table visible (the + // Card dims via isFetching) so it doesn't flash. + if (isPending && !hasLoadedOnce.current) { return ; } diff --git a/src/admin/pages/OrdersReceived.tsx b/src/admin/pages/OrdersReceived.tsx index 6cdbc5d..87adee2 100644 --- a/src/admin/pages/OrdersReceived.tsx +++ b/src/admin/pages/OrdersReceived.tsx @@ -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 ; 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 ; } @@ -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 ( <> @@ -461,6 +511,16 @@ export default function OrdersReceived({ fullWidth /> + +