diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index 94db69b..2c230ca 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -1046,3 +1046,12 @@ const rootRouteChildren: RootRouteChildren = { export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/react-start' +declare module '@tanstack/react-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/src/routes/invoices.index.tsx b/src/routes/invoices.index.tsx index 03238a7..de93113 100644 --- a/src/routes/invoices.index.tsx +++ b/src/routes/invoices.index.tsx @@ -27,6 +27,26 @@ export const Route = createFileRoute("/invoices/")({ type UnbilledStatus = "unbilled" | "marked" | "all"; const PAGE_SIZE = 100; +const UNBILLED_PAGE_SIZE = 1000; + +async function fetchAllPages( + loader: (from: number, to: number) => Promise<{ data: T[] | null; error: { message: string } | null }>, +) { + const rows: T[] = []; + let from = 0; + + while (true) { + const to = from + UNBILLED_PAGE_SIZE - 1; + const { data, error } = await loader(from, to); + if (error) throw new Error(error.message); + const batch = data ?? []; + rows.push(...batch); + if (batch.length < UNBILLED_PAGE_SIZE) break; + from += UNBILLED_PAGE_SIZE; + } + + return rows; +} function InvoicesIndex() { const navigate = useNavigate(); @@ -94,23 +114,37 @@ function InvoicesIndex() { }, [q]); const loadUnbilled = async () => { - const [{ data: t }, { data: e }] = await Promise.all([ - supabase - .from("time_entries") - .select("id, work_date, description, hours, hourly_rate, billable, user_id, invoice_id, case_id, case:cases(id, case_number, title, client_id, client:clients(id, name)), profile:profiles!time_entries_user_id_fkey(id, full_name, email)") - .eq("billable", true) - .order("work_date", { ascending: false }) - .limit(2000), - supabase - .from("expenses") - .select("id, expense_date, description, amount, billable, user_id, invoice_id, case_id, case:cases(id, case_number, title, client_id, client:clients(id, name)), profile:profiles!expenses_user_id_fkey(id, full_name, email)") - .eq("billable", true) - .order("expense_date", { ascending: false }) - .limit(2000), - ]); - setTime(t ?? []); - setExpenses(e ?? []); - setTabsLoaded(true); + try { + setTabsLoaded(false); + const [t, e] = await Promise.all([ + fetchAllPages(async (from, to) => { + const result = await supabase + .from("time_entries") + .select("id, work_date, description, hours, hourly_rate, billable, user_id, invoice_id, case_id, case:cases(id, case_number, title, client_id, client:clients(id, name)), profile:profiles!time_entries_user_id_fkey(id, full_name, email)") + .eq("billable", true) + .order("work_date", { ascending: false }) + .range(from, to); + return { data: result.data, error: result.error ? { message: result.error.message } : null }; + }), + fetchAllPages(async (from, to) => { + const result = await supabase + .from("expenses") + .select("id, expense_date, description, amount, billable, user_id, invoice_id, case_id, case:cases(id, case_number, title, client_id, client:clients(id, name)), profile:profiles!expenses_user_id_fkey(id, full_name, email)") + .eq("billable", true) + .order("expense_date", { ascending: false }) + .range(from, to); + return { data: result.data, error: result.error ? { message: result.error.message } : null }; + }), + ]); + setTime(t); + setExpenses(e); + } catch (error: any) { + toast.error(error?.message ?? "Could not load unbilled items"); + setTime([]); + setExpenses([]); + } finally { + setTabsLoaded(true); + } }; useEffect(() => { loadUnbilled(); }, []);