Files
app/src/admin/lib/queries/invoices.ts
BOHA 97101c4db7 feat(offers,invoices): per-currency 'Celkem' totals beneath the lists (consistent with orders)
Mirrors the orders totals exactly: /stats (offers) + /list-totals (issued +
received invoices) endpoints sum each doc's total per currency across the active
filter (reusing extracted where-builders so they stay in sync with the lists),
rendered as the identical 'Celkem: …' block via the shared formatMultiCurrency.
Existing invoice/received KPI stats untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 22:12:24 +02:00

259 lines
7.2 KiB
TypeScript

import { queryOptions } from "@tanstack/react-query";
import { jsonQuery, paginatedJsonQuery } from "../apiAdapter";
export interface CurrencyAmount {
amount: number;
currency: string;
}
export interface Invoice {
id: number;
invoice_number: string;
customer_name: string | null;
status: string;
issue_date: string;
due_date: string;
total: number;
currency: string;
}
export interface InvoiceStats {
paid_month: CurrencyAmount[];
paid_month_czk: number;
paid_month_count: number;
awaiting: CurrencyAmount[];
awaiting_czk: number;
awaiting_count: number;
overdue: CurrencyAmount[];
overdue_czk: number;
overdue_count: number;
vat_month: CurrencyAmount[];
vat_month_czk: number;
}
export interface InvoiceItem {
id?: number;
description: string;
item_description?: string;
quantity: number;
unit: string;
unit_price: number;
vat_rate?: number;
is_included_in_total: boolean;
position?: number;
}
export interface InvoiceDetail {
id: number;
invoice_number: string;
customer_id: number | null;
customer_name: string;
customer?: {
company_id?: string;
vat_id?: string;
};
status: string;
issue_date: string;
due_date: string;
taxable_date?: string;
tax_date?: string;
currency: string;
language: string;
vat_rate: number;
apply_vat: boolean;
exchange_rate: string;
notes?: string;
payment_method?: string;
variable_symbol?: string;
constant_symbol?: string;
issued_by?: string | null;
paid_date?: string;
billing_text?: string;
bank_name?: string;
bank_swift?: string;
bank_iban?: string;
bank_account?: string;
bank_account_id?: number | null;
items?: InvoiceItem[];
subtotal: number;
vat_amount: number;
total: number;
order?: {
id: number;
order_number: string;
status?: string;
} | null;
order_id?: number;
order_number?: string;
has_pdf?: boolean;
valid_transitions?: string[];
}
export interface ReceivedInvoice {
id: number;
supplier_name: string;
invoice_number: string;
amount: number;
currency: string;
vat_rate: number;
issue_date: string;
due_date: string;
notes: string;
status: string;
file_name?: string;
created_at: string;
}
export interface ReceivedStats {
total_month: CurrencyAmount[];
total_month_czk: number | null;
vat_month: CurrencyAmount[];
vat_month_czk: number | null;
unpaid: CurrencyAmount[];
unpaid_czk: number | null;
unpaid_count: number;
month_count: number;
}
export const invoiceListOptions = (filters: {
search?: string;
sort?: string;
order?: string;
page?: number;
perPage?: number;
month?: number;
year?: number;
status?: string;
}) =>
queryOptions({
queryKey: ["invoices", "list", filters],
queryFn: () => {
const params = new URLSearchParams();
if (filters.search) params.set("search", filters.search);
if (filters.sort) params.set("sort", filters.sort);
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.month) params.set("month", String(filters.month));
if (filters.year) params.set("year", String(filters.year));
if (filters.status) params.set("status", filters.status);
const qs = params.toString();
return paginatedJsonQuery<Invoice>(
`/api/admin/invoices${qs ? `?${qs}` : ""}`,
);
},
});
export const receivedInvoiceListOptions = (filters: {
month?: number;
year?: number;
search?: string;
sort?: string;
order?: string;
page?: number;
perPage?: number;
}) =>
queryOptions({
queryKey: [
"invoices",
"received",
{
month: filters.month,
year: filters.year,
search: filters.search,
sort: filters.sort,
order: filters.order,
page: filters.page,
perPage: filters.perPage,
},
],
queryFn: () => {
const params = new URLSearchParams();
if (filters.month) params.set("month", String(filters.month));
if (filters.year) params.set("year", String(filters.year));
if (filters.search) params.set("search", filters.search);
if (filters.sort) params.set("sort", filters.sort);
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));
const qs = params.toString();
return paginatedJsonQuery<ReceivedInvoice>(
`/api/admin/received-invoices${qs ? `?${qs}` : ""}`,
);
},
});
export const invoiceStatsOptions = (month: number, year: number) =>
queryOptions({
queryKey: ["invoices", "stats", month, year],
queryFn: () =>
jsonQuery<InvoiceStats>(
`/api/admin/invoices/stats?month=${month}&year=${year}`,
),
});
// Per-currency total over the WHOLE filtered set of the ISSUED-invoice list.
// Distinct from `invoiceStatsOptions` (the KPI cards) — hits /list-totals and
// passes the same filters the issued list uses (search/status/month/year).
export const invoiceTotalsOptions = (filters: {
search?: string;
status?: string;
month?: number;
year?: number;
}) =>
queryOptions({
queryKey: ["invoices", "list-totals", filters],
queryFn: async () => {
const params = new URLSearchParams();
if (filters.search) params.set("search", filters.search);
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();
const data = await jsonQuery<{ totals?: CurrencyAmount[] }>(
`/api/admin/invoices/list-totals${qs ? `?${qs}` : ""}`,
);
return data.totals ?? [];
},
});
export const receivedInvoiceStatsOptions = (month: number, year: number) =>
queryOptions({
queryKey: ["invoices", "received", "stats", month, year],
queryFn: () =>
jsonQuery<ReceivedStats>(
`/api/admin/received-invoices/stats?month=${month}&year=${year}`,
),
});
// Per-currency total over the WHOLE filtered set of the received-invoice list.
// Distinct from `receivedInvoiceStatsOptions` (the KPI cards) — hits
// /list-totals and passes the same filters the received list uses
// (month/year/search). `amount` is GROSS (VAT-inclusive).
export const receivedInvoiceTotalsOptions = (filters: {
month?: number;
year?: number;
search?: string;
}) =>
queryOptions({
queryKey: ["invoices", "received", "list-totals", filters],
queryFn: async () => {
const params = new URLSearchParams();
if (filters.month) params.set("month", String(filters.month));
if (filters.year) params.set("year", String(filters.year));
if (filters.search) params.set("search", filters.search);
const qs = params.toString();
const data = await jsonQuery<{ totals?: CurrencyAmount[] }>(
`/api/admin/received-invoices/list-totals${qs ? `?${qs}` : ""}`,
);
return data.totals ?? [];
},
});
export const invoiceDetailOptions = (id: string | undefined) =>
queryOptions({
queryKey: ["invoices", id],
queryFn: () => jsonQuery<InvoiceDetail>(`/api/admin/invoices/${id}`),
enabled: !!id,
});