import { queryOptions } from "@tanstack/react-query"; import { jsonQuery, paginatedJsonQuery } from "../apiAdapter"; export interface CurrencyAmount { amount: number; currency: string; } export interface ItemTemplate { id: number; name: string; description: string; default_price: number; category: string; } export interface ScopeSection { _key: string; title: string; title_cz: string; content: string; } export interface ScopeTemplate { id: number; name: string; title?: string; description?: string; scope_template_sections?: ScopeSection[]; } export interface CustomField { name: string; value: string; showLabel: boolean; _key?: string; } export interface Customer { id: number; name: string; street?: string; city?: string; postal_code?: string; country?: string; company_id?: string; vat_id?: string; quotation_count: number; custom_fields?: CustomField[]; customer_field_order?: string[]; } export const offerCustomersOptions = () => queryOptions({ // Under the broad ["offers"] domain so offer-domain invalidations // (customer CRUD, offer mutations) refresh the picker automatically. queryKey: ["offers", "customers"], queryFn: () => jsonQuery("/api/admin/customers"), staleTime: 2 * 60_000, }); export const itemTemplatesOptions = () => queryOptions({ queryKey: ["offer-templates", "items"], queryFn: () => jsonQuery("/api/admin/offers-templates?action=items"), }); export const scopeTemplatesOptions = () => queryOptions({ queryKey: ["offer-templates", "scopes"], queryFn: () => jsonQuery("/api/admin/offers-templates"), }); /** Offer list row (the shape returned by GET /api/admin/offers). */ export interface Quotation { id: number; quotation_number: string; project_code: string; customer_name: string; created_at: string; valid_until: string; currency: string; total: number; status: string; order_id?: number; order_status?: string; } export const offerListOptions = (filters: { search?: string; sort?: string; order?: string; page?: number; perPage?: number; status?: string; customer_id?: number; }) => queryOptions({ queryKey: ["offers", "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.status) params.set("status", filters.status); if (filters.customer_id) params.set("customer_id", String(filters.customer_id)); const qs = params.toString(); return paginatedJsonQuery( `/api/admin/offers${qs ? `?${qs}` : ""}`, ); }, }); export const offerStatsOptions = (filters: { search?: string; status?: string; customer_id?: number; }) => queryOptions({ queryKey: ["offers", "stats", 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.customer_id) params.set("customer_id", String(filters.customer_id)); const qs = params.toString(); const data = await jsonQuery<{ totals?: CurrencyAmount[] }>( `/api/admin/offers/stats${qs ? `?${qs}` : ""}`, ); return data.totals ?? []; }, }); export interface OfferItemData { id?: number; description: string; item_description: string; quantity: number; unit: string; unit_price: number; discount?: number; is_included_in_total: boolean; position?: number; } export interface OfferSectionData { title: string; title_cz: string; content: string; position?: number; } export interface OfferLockInfo { user_id: number; username: string; full_name: string; } export interface OfferOrderInfo { id: number; order_number: string; status?: string; } export interface OfferDetailData { id: number; quotation_number: string; project_code: string; customer_id: number | null; customer_name: string; created_at: string; valid_until: string; currency: string; language: string; items?: OfferItemData[]; sections?: OfferSectionData[]; status: string; order: OfferOrderInfo | null; locked_by: OfferLockInfo | null; /** Legal next statuses, computed server-side (same contract as issued orders). */ valid_transitions: string[]; selected_custom_fields: number[]; } export const offerDetailOptions = (id: string | undefined) => queryOptions({ queryKey: ["offers", id], queryFn: () => jsonQuery(`/api/admin/offers/${id}`), enabled: !!id, // 404s (deleted/missing offers) are not transient. Retrying just spams // GETs and fires the detail-page redirect toast repeatedly. retry: false, // OfferDetail toasts its own Czech error + navigates away (and suppresses // it during delete) — the global QueryCache toast would double up. meta: { suppressGlobalErrorToast: true }, }); export const offerNextNumberOptions = () => queryOptions({ queryKey: ["offers", "next-number"], queryFn: () => jsonQuery<{ next_number?: string; number?: string }>( "/api/admin/offers/next-number", ), });