"use client" import { useEffect, useState, useTransition } from "react" import { toast } from "sonner" import { RefreshCw, Link2, CheckCircle2, AlertTriangle, BookOpen } from "lucide-react" import { formatDate } from "@/lib/utils" import { syncAccountingNow, disconnectAccounting } from "@/app/actions/accounting" type Conn = { provider: string; orgName: string | null; status: string; lastSyncAt: string | null; lastError: string | null } type Prov = { id: string; label: string; configured: boolean } const ERR_MSG: Record = { connect_failed: "Connection failed — please try again.", invalid_state: "The callback was invalid. Please retry the connection.", owner_only: "Only the account owner can manage integrations.", not_configured: "That provider isn't configured on the server yet.", unknown_provider: "Unknown provider.", } export function AccountingIntegrations({ providers, connections, isOwner, flash, }: { providers: Prov[] connections: Conn[] isOwner: boolean flash: { connected?: string; error?: string } }) { const connByProvider: Record = Object.fromEntries(connections.map((c) => [c.provider, c])) const [pending, start] = useTransition() const [busy, setBusy] = useState(null) useEffect(() => { if (flash.connected) toast.success(`Connected to ${flash.connected === "quickbooks" ? "QuickBooks" : "Xero"}`) if (flash.error) toast.error(ERR_MSG[flash.error] ?? "Something went wrong") // eslint-disable-next-line react-hooks/exhaustive-deps }, []) function sync(id: string) { setBusy(id) start(async () => { try { const r = await syncAccountingNow(id) toast.success(`Synced ${r.income} income + ${r.expense} expense entries`) } catch (e) { toast.error((e as Error).message || "Sync failed") } finally { setBusy(null) } }) } function remove(id: string) { if (!confirm("Disconnect this integration? Future income/expenses won't sync until you reconnect.")) return setBusy(id) start(async () => { try { await disconnectAccounting(id) toast.success("Disconnected") } catch { toast.error("Couldn't disconnect") } finally { setBusy(null) } }) } if (!isOwner) { return (
Only the account owner can manage accounting integrations.
) } return (
{providers.map((p) => { const conn = connByProvider[p.id] const isBusy = pending && busy === p.id return (

{p.label}

{conn ? (

{conn.orgName ?? "Connected"} · Last sync {conn.lastSyncAt ? formatDate(conn.lastSyncAt) : "never"}

) : (

Push rent income & expenses to {p.label}

)}
{conn ? ( {conn.status === "error" ? : } {conn.status === "error" ? "Error" : "Connected"} ) : p.configured ? null : ( Not configured )}
{conn?.status === "error" && conn.lastError && (

{conn.lastError}

)}
{conn ? ( <> ) : p.configured ? ( Connect {p.label} ) : (

Set this provider's OAuth credentials on the server to enable connecting.

)}
) })}

One-way sync — paid rent is pushed as income and expenses as bills/purchases. We never read or modify existing data in your books.

) }