diff --git a/src/admin/pages/Invoices.tsx b/src/admin/pages/Invoices.tsx index 239ffc5..128cb79 100644 --- a/src/admin/pages/Invoices.tsx +++ b/src/admin/pages/Invoices.tsx @@ -20,6 +20,7 @@ import { import { normalizeDateStr } from "../utils/attendanceHelpers"; import { usePaginatedQuery } from "../hooks/usePaginatedQuery"; import useTableSort from "../hooks/useTableSort"; +import useDebounce from "../hooks/useDebounce"; import { invoiceListOptions, invoiceStatsOptions, @@ -214,6 +215,7 @@ export default function Invoices() { const [receivedUploadOpen, setReceivedUploadOpen] = useState(false); const { sort, order, handleSort } = useTableSort("invoice_number"); const [search, setSearch] = useState(""); + const debouncedSearch = useDebounce(search, 300); const [page, setPage] = useState(1); const [statusFilter, setStatusFilter] = useState(""); @@ -269,6 +271,11 @@ export default function Invoices() { invoice: Invoice | null; }>({ show: false, invoice: null }); const [deleting, setDeleting] = useState(false); + const [paidConfirm, setPaidConfirm] = useState<{ + show: boolean; + invoice: Invoice | null; + }>({ show: false, invoice: null }); + const [markingPaid, setMarkingPaid] = useState(false); const [pdfLoading, setPdfLoading] = useState(null); const queryClient = useQueryClient(); @@ -279,7 +286,7 @@ export default function Invoices() { isFetching: loading, } = usePaginatedQuery( invoiceListOptions({ - search, + search: debouncedSearch, sort, order, page, @@ -294,7 +301,7 @@ export default function Invoices() { // matches what's shown. Separate from the KPI `statsQuery` above. const listTotalsQuery = useQuery( invoiceTotalsOptions({ - search, + search: debouncedSearch, status: statusFilter || undefined, month: statsMonth, year: statsYear, @@ -336,16 +343,21 @@ export default function Invoices() { } }; - const toggleStatus = async (inv: Invoice) => { - if (inv.status === "paid") return; + const handleMarkPaid = async () => { + if (!paidConfirm.invoice) return; + setMarkingPaid(true); try { - const res = await apiFetch(`${API_BASE}/invoices/${inv.id}`, { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ status: "paid" }), - }); + const res = await apiFetch( + `${API_BASE}/invoices/${paidConfirm.invoice.id}`, + { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ status: "paid" }), + }, + ); const data = await res.json(); if (data.success) { + setPaidConfirm({ show: false, invoice: null }); alert.success("Faktura označena jako zaplacená"); queryClient.invalidateQueries({ queryKey: ["invoices"] }); queryClient.invalidateQueries({ queryKey: ["orders"] }); @@ -355,6 +367,8 @@ export default function Invoices() { } } catch { alert.error("Chyba připojení"); + } finally { + setMarkingPaid(false); } }; @@ -557,7 +571,8 @@ export default function Invoices() { toggleStatus(inv)} + title="Označit jako zaplacenou" + onClick={() => setPaidConfirm({ show: true, invoice: inv })} /> ), }, @@ -613,7 +628,8 @@ export default function Invoices() { > {inv.status === "paid" ? ViewIcon : EditIcon} - {hasPermission("invoices.view") && ( + {/* Drafts have no number yet — /file 404s for them. */} + {hasPermission("invoices.view") && inv.invoice_number && ( handlePdf(inv)} @@ -760,7 +776,7 @@ export default function Invoices() { sortDir={order} onSort={handleSort} empty={ - search || statusFilter ? ( + debouncedSearch || statusFilter ? ( ) : ( setDeleteConfirm({ show: false, invoice: null })} onConfirm={handleDelete} title="Smazat fakturu" - message={`Opravdu chcete smazat fakturu "${deleteConfirm.invoice?.invoice_number}"? Tato akce je nevratná.`} + message={`Opravdu chcete smazat fakturu "${documentNumberLabel(deleteConfirm.invoice?.invoice_number)}"? Tato akce je nevratná.`} confirmText="Smazat" cancelText="Zrušit" confirmVariant="danger" loading={deleting} /> + + setPaidConfirm({ show: false, invoice: null })} + onConfirm={handleMarkPaid} + title="Označit fakturu jako zaplacenou" + message={`Označit fakturu "${documentNumberLabel(paidConfirm.invoice?.invoice_number)}" jako zaplacenou? Tato akce je nevratná.`} + confirmText="Označit jako zaplacenou" + cancelText="Zrušit" + loading={markingPaid} + /> diff --git a/src/admin/pages/IssuedOrders.tsx b/src/admin/pages/IssuedOrders.tsx index 61a4fed..5237f3f 100644 --- a/src/admin/pages/IssuedOrders.tsx +++ b/src/admin/pages/IssuedOrders.tsx @@ -436,7 +436,7 @@ export default function IssuedOrders({ month, year }: IssuedOrdersProps) { action={ hasPermission("orders.create") ? ( ) : undefined } diff --git a/src/admin/pages/Orders.tsx b/src/admin/pages/Orders.tsx index be1d4e4..e01dfb2 100644 --- a/src/admin/pages/Orders.tsx +++ b/src/admin/pages/Orders.tsx @@ -122,11 +122,11 @@ export default function Orders() { startIcon={PlusIcon} onClick={() => navigate("/orders/issued/new")} > - Vytvořit objednávku + Nová vydaná objednávka ) : ( ) ) : undefined diff --git a/src/admin/pages/ReceivedInvoices.tsx b/src/admin/pages/ReceivedInvoices.tsx index 0a0de93..8b11a94 100644 --- a/src/admin/pages/ReceivedInvoices.tsx +++ b/src/admin/pages/ReceivedInvoices.tsx @@ -16,6 +16,7 @@ import { } from "../utils/formatters"; import { normalizeDateStr } from "../utils/attendanceHelpers"; import useTableSort from "../hooks/useTableSort"; +import useDebounce from "../hooks/useDebounce"; import { usePaginatedQuery } from "../hooks/usePaginatedQuery"; import { companySettingsOptions, @@ -248,6 +249,7 @@ export default function ReceivedInvoices({ const queryClient = useQueryClient(); const { sort, order, handleSort } = useTableSort("created_at"); const [search, setSearch] = useState(""); + const debouncedSearch = useDebounce(search, 300); const [editOpen, setEditOpen] = useState(false); const [editInvoice, setEditInvoice] = useState(null); @@ -257,6 +259,10 @@ export default function ReceivedInvoices({ show: boolean; invoice: ReceivedInvoice | null; }>({ show: false, invoice: null }); + const [paidConfirm, setPaidConfirm] = useState<{ + show: boolean; + invoice: ReceivedInvoice | null; + }>({ show: false, invoice: null }); const hasLoadedOnce = useRef(false); const blobTimeoutsRef = useRef[]>([]); @@ -286,7 +292,7 @@ export default function ReceivedInvoices({ receivedInvoiceListOptions({ month: statsMonth, year: statsYear, - search, + search: debouncedSearch, sort, order, page, @@ -306,7 +312,7 @@ export default function ReceivedInvoices({ receivedInvoiceTotalsOptions({ month: statsMonth, year: statsYear, - search, + search: debouncedSearch, }), ); @@ -580,10 +586,14 @@ export default function ReceivedInvoices({ } }; - const toggleStatus = async (inv: ReceivedInvoice) => { - if (inv.status === "paid") return; + const handleMarkPaid = async () => { + if (!paidConfirm.invoice) return; try { - await toggleStatusMutation.mutateAsync({ id: inv.id, status: "paid" }); + await toggleStatusMutation.mutateAsync({ + id: paidConfirm.invoice.id, + status: "paid", + }); + setPaidConfirm({ show: false, invoice: null }); } catch (e) { alert.error(e instanceof Error ? e.message : "Nepodařilo se změnit stav"); } @@ -675,7 +685,8 @@ export default function ReceivedInvoices({ toggleStatus(inv)} + title="Označit jako uhrazenou" + onClick={() => setPaidConfirm({ show: true, invoice: inv })} /> ), }, @@ -855,7 +866,7 @@ export default function ReceivedInvoices({ sortDir={order} onSort={handleSort} empty={ - search ? ( + debouncedSearch ? ( ) : ( + + setPaidConfirm({ show: false, invoice: null })} + onConfirm={handleMarkPaid} + title="Označit fakturu jako uhrazenou" + message={`Označit fakturu "${paidConfirm.invoice?.invoice_number || paidConfirm.invoice?.supplier_name || ""}" jako uhrazenou?`} + confirmText="Označit jako uhrazenou" + cancelText="Zrušit" + loading={toggleStatusMutation.isPending} + /> ); }