diff --git a/src/admin/pages/Orders.tsx b/src/admin/pages/Orders.tsx index 3ee4427..e29b6a1 100644 --- a/src/admin/pages/Orders.tsx +++ b/src/admin/pages/Orders.tsx @@ -1,18 +1,16 @@ import { useState } from "react"; import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { Link as RouterLink, useNavigate } from "react-router-dom"; +import { motion } from "framer-motion"; +import Box from "@mui/material/Box"; +import IconButton from "@mui/material/IconButton"; import { useAlert } from "../context/AlertContext"; import { useAuth } from "../context/AuthContext"; -import { Link, useNavigate } from "react-router-dom"; -import apiFetch from "../utils/api"; import Forbidden from "../components/Forbidden"; -import { motion } from "framer-motion"; -import ConfirmModal from "../components/ConfirmModal"; -import FormModal from "../components/FormModal"; -import FormField from "../components/FormField"; - +import apiFetch from "../utils/api"; import { formatCurrency, formatDate, czechPlural } from "../utils/formatters"; -import SortIcon from "../components/SortIcon"; import useTableSort from "../hooks/useTableSort"; +import useDebounce from "../hooks/useDebounce"; import { usePaginatedQuery } from "../hooks/usePaginatedQuery"; import { orderListOptions, @@ -20,7 +18,25 @@ import { } from "../lib/queries/orders"; import { offerCustomersOptions } from "../lib/queries/offers"; import { useApiMutation } from "../lib/queries/mutations"; -import Pagination from "../components/Pagination"; +import { + Button, + Card, + DataTable, + Pagination, + Modal, + ConfirmDialog, + Field, + TextField, + Select, + StatusChip, + CheckboxField, + FileUpload, + PageHeader, + FilterBar, + EmptyState, + LoadingState, + type DataColumn, +} from "../ui"; const API_BASE = "/api/admin"; @@ -31,11 +47,14 @@ const STATUS_LABELS: Record = { stornovana: "Stornována", }; -const STATUS_CLASSES: Record = { - prijata: "admin-badge-order-prijata", - v_realizaci: "admin-badge-order-realizace", - dokoncena: "admin-badge-order-dokoncena", - stornovana: "admin-badge-order-stornovana", +const STATUS_COLORS: Record< + string, + "default" | "success" | "error" | "warning" | "info" +> = { + prijata: "info", + v_realizaci: "warning", + dokoncena: "success", + stornovana: "error", }; interface Order { @@ -51,12 +70,94 @@ interface Order { invoice_id?: number; } +const PlusIcon = ( + + + + +); +const ViewIcon = ( + + + + +); +const InvoiceViewIcon = ( + + + + + F + + +); +const InvoiceCreateIcon = ( + + + + + + +); +const DeleteIcon = ( + + + + +); + export default function Orders() { const alert = useAlert(); const { hasPermission } = useAuth(); - const { sort, order, handleSort, activeSort } = useTableSort("order_number"); + const { sort, order, handleSort } = useTableSort("order_number"); const [search, setSearch] = useState(""); + const debouncedSearch = useDebounce(search, 300); const [page, setPage] = useState(1); const [deleteConfirm, setDeleteConfirm] = useState<{ @@ -204,7 +305,9 @@ export default function Orders() { pagination, isPending, isFetching, - } = usePaginatedQuery(orderListOptions({ search, sort, order, page })); + } = usePaginatedQuery( + orderListOptions({ search: debouncedSearch, sort, order, page }), + ); if (!hasPermission("orders.view")) return ; @@ -221,281 +324,207 @@ export default function Orders() { }; if (isPending) { - return ( -
-
-
- ); + return ; } + const total = pagination?.total ?? orders.length; + const subtitle = `${total} ${czechPlural( + total, + "objednávka", + "objednávky", + "objednávek", + )}`; + + const columns: DataColumn[] = [ + { + key: "order_number", + header: "Číslo", + width: "14%", + sortKey: "order_number", + mono: true, + render: (o) => ( + + {o.order_number} + + ), + }, + { + key: "quotation", + header: "Nabídka", + width: "14%", + render: (o) => ( + + {o.quotation_number} + + ), + }, + { + key: "customer", + header: "Zákazník", + width: "20%", + render: (o) => o.customer_name || "—", + }, + { + key: "status", + header: "Stav", + width: "13%", + sortKey: "status", + render: (o) => ( + + ), + }, + { + key: "created_at", + header: "Datum", + width: "12%", + sortKey: "created_at", + mono: true, + render: (o) => formatDate(o.created_at), + }, + { + key: "total", + header: "Celkem", + width: "13%", + align: "right", + mono: true, + bold: true, + render: (o) => formatCurrency(o.total, o.currency), + }, + { + key: "actions", + header: "Akce", + width: "14%", + align: "right", + render: (o) => ( + + navigate(`/orders/${o.id}`)} + aria-label="Detail" + title="Detail" + > + {ViewIcon} + + {o.invoice_id ? ( + navigate(`/invoices/${o.invoice_id}`)} + aria-label="Zobrazit fakturu" + title="Zobrazit fakturu" + > + {InvoiceViewIcon} + + ) : ( + hasPermission("invoices.create") && ( + navigate(`/invoices/new?fromOrder=${o.id}`)} + aria-label="Vytvořit fakturu" + title="Vytvořit fakturu" + > + {InvoiceCreateIcon} + + ) + )} + {hasPermission("orders.delete") && ( + setDeleteConfirm({ show: true, order: o })} + aria-label="Smazat" + title="Smazat" + > + {DeleteIcon} + + )} + + ), + }, + ]; + return ( -
+ -
-

Objednávky

-

- {pagination?.total ?? orders.length}{" "} - {czechPlural( - pagination?.total ?? orders.length, - "objednávka", - "objednávky", - "objednávek", - )} -

-
- {hasPermission("orders.create") && ( -
- -
- )} + + Vytvořit objednávku + + ) : undefined + } + />
+ + + { + setSearch(e.target.value); + setPage(1); + }} + placeholder="Hledat podle čísla, nabídky, projektu nebo zákazníka..." + fullWidth + /> + + + -
-
- { - setSearch(e.target.value); - setPage(1); - }} - className="admin-form-input" - placeholder="Hledat podle čísla, nabídky, projektu nebo zákazníka..." - /> -
- - {orders.length === 0 ? ( -
-
- - - - - -
-

Zatím nejsou žádné objednávky.

-

- Objednávky se vytvářejí z nabídek. -

-
- ) : ( -
- - - - - - - - - - - - - - {(orders as Order[]).map((o) => ( - - - - - - - - - - ))} - -
handleSort("order_number")} - > - Číslo{" "} - - NabídkaZákazník handleSort("status")} - > - Stav{" "} - - handleSort("created_at")} - > - Datum{" "} - - CelkemAkce
- - {o.order_number} - - - - {o.quotation_number} - - {o.customer_name || "—"} - - {STATUS_LABELS[o.status] || o.status} - - {formatDate(o.created_at)} - {formatCurrency(o.total, o.currency)} - -
- - - - - - - {o.invoice_id ? ( - - - - - - F - - - - ) : ( - hasPermission("invoices.create") && ( - - - - - - - - - ) - )} - {hasPermission("orders.delete") && ( - - )} -
-
-
- )} - -
+ + + columns={columns} + rows={orders} + rowKey={(o) => o.id} + sortBy={sort} + sortDir={order} + onSort={handleSort} + empty={ + + } + /> + +
- { setDeleteConfirm({ show: false, order: null }); @@ -504,58 +533,55 @@ export default function Orders() { onConfirm={handleDelete} title="Smazat objednávku" message={ - <> - Opravdu chcete smazat objednávku " - {deleteConfirm.order?.order_number}"? Bude smazán i přidružený - projekt. Tato akce je nevratná. - - + deleteConfirm.order + ? `Opravdu chcete smazat objednávku „${deleteConfirm.order.order_number}"? Bude smazán i přidružený projekt. Tato akce je nevratná.` + : "" } confirmText="Smazat" - cancelText="Zrušit" - type="danger" + confirmVariant="danger" loading={deleteMutation.isPending} - /> + > + + - setShowCreate(false)} onSubmit={handleCreate} title="Vytvořit objednávku bez nabídky" - submitLabel="Vytvořit" + subtitle={`Číslo objednávky: ${ + nextNumberQuery.data?.number ?? + nextNumberQuery.data?.next_number ?? + "…" + } (přiděleno automaticky)`} + submitText="Vytvořit" loading={creating} > -
-
- - - setCreateForm({ ...createForm, customer_id: e.target.value }) + onChange={(value) => + setCreateForm({ ...createForm, customer_id: value }) } - className="admin-form-input" - > - - {(customersQuery.data ?? []).map((c) => ( - - ))} - - - - ({ + value: String(c.id), + label: c.name, + })), + ]} + /> + + + + + setCreateForm({ @@ -563,193 +589,128 @@ export default function Orders() { customer_order_number: e.target.value, }) } - className="admin-form-input" /> - -
+ + + -
- - - setCreateForm({ ...createForm, currency: e.target.value }) + onChange={(value) => + setCreateForm({ ...createForm, currency: value }) } - className="admin-form-input" - > - - - - - - - - + + + + + setCreateForm({ ...createForm, vat_rate: e.target.value }) } - className="admin-form-input" - min="0" + slotProps={{ htmlInput: { min: 0 } }} /> - -
+ + + -
- - + + + setCreateForm({ ...createForm, price: e.target.value }) } - className="admin-form-input" - min="0" - step="0.01" placeholder="0" + slotProps={{ htmlInput: { min: 0, step: 0.01 } }} /> - - - + + + + setCreateForm({ ...createForm, quantity: e.target.value }) } - className="admin-form-input" - min="0" - step="1" + slotProps={{ htmlInput: { min: 0, step: 1 } }} /> - -
+ + + - - - setCreateForm({ ...createForm, scope_title: e.target.value }) - } - className="admin-form-input" - /> - + + + setCreateForm({ ...createForm, scope_title: e.target.value }) + } + /> + - -