mirror of
https://github.com/renee-png/acmcc.git
synced 2026-06-21 09:50:01 +00:00
Accounting: fix empty JE/GL pages + nightly Buildium GL pull sync
- Add missing indexes on journal_entry_lines (journal_entry_id, account_id) and journal_entries (company_id, date): Bridgewater's ledger query took 7.5s, blew the 8s statement timeout, and rendered the JE/GL pages empty - Paginate JE/GL page fetches past the 1000-row PostgREST cap (shared fetchJournalEntries helper) and surface query errors instead of swallowing them into an empty list - New buildium-gl-sync edge function (scheduled nightly via pg_cron): incrementally pulls new Buildium GL activity into accounting journal entries via GET /v1/generalledger, reconstructing double-entry JEs by grouping per-account entries by transaction id; watermark + 14-day overlap window, dedupe on (company_id, external_source, external_id), account mapping by external_id/code/name with auto-create for new Buildium accounts Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useMemo, useState } from "react";
|
||||
import { accounting } from "@/lib/accountingClient";
|
||||
import { useCompanyId } from "./lib/useCompanyId";
|
||||
import { fetchAllJournalEntries } from "./lib/fetchJournalEntries";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
@@ -44,18 +45,12 @@ export default function AccountingGeneralLedgerPage() {
|
||||
const [activityOnly, setActivityOnly] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
|
||||
const { data: entries = [], isFetching } = useQuery({
|
||||
const { data: entries = [], isFetching, error: entriesError } = useQuery({
|
||||
queryKey: ["gl-journal-entries", cid],
|
||||
enabled: !!cid,
|
||||
queryFn: async () =>
|
||||
(
|
||||
await accounting
|
||||
.from("journal_entries")
|
||||
.select("id, date, description, reference, journal_entry_lines(debit, credit, account_id, accounts(code, name, type))")
|
||||
.eq("company_id", cid)
|
||||
.order("date", { ascending: true })
|
||||
.order("created_at", { ascending: true })
|
||||
).data ?? [],
|
||||
queryFn: () =>
|
||||
// Running balances below assume date-ascending order.
|
||||
fetchAllJournalEntries(cid, "id, date, description, reference, journal_entry_lines(debit, credit, account_id, accounts(code, name, type))", { ascending: true }),
|
||||
});
|
||||
|
||||
const { data: accounts = [] } = useQuery({
|
||||
@@ -169,6 +164,13 @@ export default function AccountingGeneralLedgerPage() {
|
||||
if (!associationId) return <p className="text-sm text-muted-foreground">Select an association.</p>;
|
||||
if (companyLoading) return <div className="flex justify-center py-12"><Loader2 className="h-6 w-6 animate-spin text-muted-foreground" /></div>;
|
||||
if (companyError || !companyId) return <p className="text-sm text-muted-foreground text-center py-12">{companyError || "Accounting setup is not ready."}</p>;
|
||||
if (entriesError) {
|
||||
return (
|
||||
<p className="text-sm text-destructive text-center py-12">
|
||||
Failed to load the general ledger: {(entriesError as any)?.message || String(entriesError)}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useMemo, useState } from "react";
|
||||
import { accounting } from "@/lib/accountingClient";
|
||||
import { useCompanyId } from "./lib/useCompanyId";
|
||||
import { fetchAllJournalEntries } from "./lib/fetchJournalEntries";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
@@ -39,18 +40,10 @@ export default function AccountingJournalEntriesPage() {
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [editingId, setEditingId] = useState<string | null>(null);
|
||||
|
||||
const { data: entries = [] } = useQuery({
|
||||
const { data: entries = [], error: entriesError } = useQuery({
|
||||
queryKey: ["journal-entries", cid],
|
||||
enabled: !!cid,
|
||||
queryFn: async () =>
|
||||
(
|
||||
await accounting
|
||||
.from("journal_entries")
|
||||
.select("*, journal_entry_lines(*, accounts(name, code))")
|
||||
.eq("company_id", cid)
|
||||
.order("date", { ascending: false })
|
||||
.order("created_at", { ascending: false })
|
||||
).data ?? [],
|
||||
queryFn: () => fetchAllJournalEntries(cid, "*, journal_entry_lines(*, accounts(name, code))"),
|
||||
});
|
||||
|
||||
const { data: accounts = [] } = useQuery({
|
||||
@@ -186,6 +179,13 @@ export default function AccountingJournalEntriesPage() {
|
||||
if (!associationId) return <p className="text-sm text-muted-foreground">Select an association.</p>;
|
||||
if (companyLoading) return <div className="flex justify-center py-12"><Loader2 className="h-6 w-6 animate-spin text-muted-foreground" /></div>;
|
||||
if (companyError || !companyId) return <p className="text-sm text-muted-foreground text-center py-12">{companyError || "Accounting setup is not ready."}</p>;
|
||||
if (entriesError) {
|
||||
return (
|
||||
<p className="flex items-center justify-center gap-2 text-sm text-destructive py-12">
|
||||
<AlertCircle className="h-4 w-4" /> Failed to load journal entries: {(entriesError as any)?.message || String(entriesError)}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { accounting } from "@/lib/accountingClient";
|
||||
|
||||
// PostgREST caps each response at 1000 rows, so companies with more journal
|
||||
// entries than that (e.g. Buildium-imported books) were silently truncated —
|
||||
// and a query error (statement timeout) used to be swallowed into an empty
|
||||
// list. Page through all entries on a stable key and throw on error so
|
||||
// react-query surfaces failures instead of rendering an empty ledger.
|
||||
const PAGE = 1000;
|
||||
|
||||
export async function fetchAllJournalEntries(
|
||||
cid: string,
|
||||
select: string,
|
||||
opts: { ascending?: boolean } = {},
|
||||
): Promise<any[]> {
|
||||
const ascending = opts.ascending ?? false;
|
||||
const out: any[] = [];
|
||||
for (let offset = 0; ; offset += PAGE) {
|
||||
const { data, error } = await accounting
|
||||
.from("journal_entries")
|
||||
.select(select)
|
||||
.eq("company_id", cid)
|
||||
.order("date", { ascending })
|
||||
.order("created_at", { ascending })
|
||||
.order("id", { ascending: true })
|
||||
.range(offset, offset + PAGE - 1);
|
||||
if (error) throw error;
|
||||
const rows = (data ?? []) as any[];
|
||||
out.push(...rows);
|
||||
if (rows.length < PAGE) break;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
Reference in New Issue
Block a user