"use server"; import { createAdminClient } from "@/lib/supabase/server"; import { auth } from "@clerk/nextjs/server"; async function getOrganisationId(): Promise { const { userId } = await auth(); if (!userId) throw new Error("Unauthenticated"); const supabase = await createAdminClient(); const { data: member, error } = await supabase .from("organisation_members") .select("organisation_id") .eq("user_id", userId) .single(); if (error || !member) throw new Error("No organisation found"); return member.organisation_id; } export interface ProfitAndLossData { periodStart: string; periodEnd: string; revenue: { total: number; byMonth: { month: string; amount: number }[]; }; costOfSales: { total: number; items: { name: string; amount: number }[]; }; grossProfit: number; operatingExpenses: { total: number; byCategory: { category: string; amount: number }[]; byMonth: { month: string; amount: number }[]; }; netProfit: number; grossMargin: number; netMargin: number; } export async function fetchProfitAndLoss( periodStart: string, periodEnd: string ): Promise { const orgId = await getOrganisationId(); const supabase = await createAdminClient(); const monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; // ── Revenue (paid invoices) ───────────────────────────────── const { data: invoices, error: invError } = await supabase .from("invoices") .select("total_cents, amount, status, issue_date") .eq("organisation_id", orgId) .eq("status", "paid") .gte("issue_date", periodStart) .lte("issue_date", periodEnd); if (invError) console.error("P&L invoices error:", invError); let totalRevenue = 0; const revenueByMonth: Record = {}; (invoices || []).forEach((inv) => { const amt = inv.total_cents > 0 ? inv.total_cents : Math.round((inv.amount || 0) * 100); totalRevenue += amt; const d = new Date(inv.issue_date + "T00:00:00"); const key = `${monthNames[d.getMonth()]} ${d.getFullYear()}`; revenueByMonth[key] = (revenueByMonth[key] || 0) + amt; }); // ── Operating expenses ────────────────────────────────────── const { data: expenses, error: expError } = await supabase .from("expenses") .select("amount, expense_date, category:expense_categories(name), status") .eq("organisation_id", orgId) .gte("expense_date", periodStart) .lte("expense_date", periodEnd); if (expError) console.error("P&L expenses error:", expError); let totalExpenses = 0; const expensesByCategory: Record = {}; const expensesByMonth: Record = {}; (expenses || []) .filter((e: any) => e.status !== "draft" && e.status !== "rejected") .forEach((e: any) => { const amt = Number(e.amount); totalExpenses += amt; const catName = (e.category as any)?.name || "Uncategorised"; expensesByCategory[catName] = (expensesByCategory[catName] || 0) + amt; const d = new Date(e.expense_date + "T00:00:00"); const key = `${monthNames[d.getMonth()]} ${d.getFullYear()}`; expensesByMonth[key] = (expensesByMonth[key] || 0) + amt; }); // Build monthly timeline const allMonths = new Set(); Object.keys(revenueByMonth).forEach((m) => allMonths.add(m)); Object.keys(expensesByMonth).forEach((m) => allMonths.add(m)); const sortedMonths = Array.from(allMonths).sort((a, b) => { const [aM, aY] = a.split(" "); const [bM, bY] = b.split(" "); const aIdx = monthNames.indexOf(aM); const bIdx = monthNames.indexOf(bM); return aY !== bY ? Number(aY) - Number(bY) : aIdx - bIdx; }); const revenueByMonthArr = sortedMonths.map((m) => ({ month: m, amount: revenueByMonth[m] || 0 })); const expensesByMonthArr = sortedMonths.map((m) => ({ month: m, amount: expensesByMonth[m] || 0 })); const costOfSalesTotal = 0; // No COGS tracking yet — Phase 3 const grossProfit = totalRevenue; // Revenue minus zero COGS const netProfit = grossProfit - totalExpenses; const grossMargin = totalRevenue > 0 ? (grossProfit / totalRevenue) * 100 : 0; const netMargin = totalRevenue > 0 ? (netProfit / totalRevenue) * 100 : 0; return { periodStart, periodEnd, revenue: { total: Math.round(totalRevenue) / 100, byMonth: revenueByMonthArr.map((m) => ({ month: m.month, amount: Math.round(m.amount) / 100 })), }, costOfSales: { total: 0, items: [] }, grossProfit: Math.round(grossProfit) / 100, operatingExpenses: { total: Math.round(totalExpenses * 100) / 100, byCategory: Object.entries(expensesByCategory) .map(([category, amount]) => ({ category, amount: Math.round(amount * 100) / 100 })) .sort((a, b) => b.amount - a.amount), byMonth: expensesByMonthArr.map((m) => ({ month: m.month, amount: Math.round(m.amount * 100) / 100 })), }, netProfit: Math.round(netProfit) / 100, grossMargin, netMargin, }; } // ── Balance Sheet ──────────────────────────────────────────── export interface BalanceSheetData { asOfDate: string; assets: { name: string; amount: number }[]; totalAssets: number; liabilities: { name: string; amount: number }[]; totalLiabilities: number; equity: { name: string; amount: number }[]; netAssets: number; } export async function fetchBalanceSheet(asOfDate: string): Promise { const orgId = await getOrganisationId(); const supabase = await createAdminClient(); // Assets: outstanding invoices (sent but not paid) = Accounts Receivable const { data: unpaidInvoices, error: arError } = await supabase .from("invoices") .select("total_cents, amount, status") .eq("organisation_id", orgId) .in("status", ["sent"]); if (arError) console.error("Balance sheet AR error:", arError); const accountsReceivable = (unpaidInvoices || []).reduce( (s, i) => s + (i.total_cents > 0 ? i.total_cents : Math.round((i.amount || 0) * 100)), 0 ) / 100; // Liabilities: submitted/approved expenses not yet reimbursed const { data: unpaidExpenses, error: apError } = await supabase .from("expenses") .select("amount, status") .eq("organisation_id", orgId) .in("status", ["submitted", "approved"]); if (apError) console.error("Balance sheet AP error:", apError); const accountsPayable = (unpaidExpenses || []).reduce( (s, e) => s + Number(e.amount), 0 ); // Payroll liabilities: draft/confirmed payroll runs const { data: pendingPayroll, error: ppError } = await supabase .from("payroll_runs") .select("total_net, total_deductions, status") .eq("organisation_id", orgId) .in("status", ["draft", "confirmed"]); if (ppError) console.error("Balance sheet payroll error:", ppError); const payrollLiability = (pendingPayroll || []).reduce( (s, p) => s + Number(p.total_net) + Number(p.total_deductions), 0 ); // Retained earnings: total revenue minus total expenses (simplified — no GL) const { data: allInvoices, error: revError } = await supabase .from("invoices") .select("total_cents, amount, status") .eq("organisation_id", orgId); if (revError) console.error("Balance sheet revenue error:", revError); const totalRevenue = (allInvoices || []) .filter((i) => i.status === "paid") .reduce((s, i) => s + (i.total_cents > 0 ? i.total_cents : Math.round((i.amount || 0) * 100)), 0) / 100; const { data: allExpenses, error: allExpError } = await supabase .from("expenses") .select("amount, status") .eq("organisation_id", orgId); if (allExpError) console.error("Balance sheet expenses error:", allExpError); const totalExpenses = (allExpenses || []) .filter((e) => e.status !== "draft" && e.status !== "rejected") .reduce((s, e) => s + Number(e.amount), 0); const retainedEarnings = totalRevenue - totalExpenses; const totalLiabilities = accountsPayable + payrollLiability; const totalAssets = accountsReceivable; const netAssets = totalAssets - totalLiabilities; const asOfFormatted = new Date(asOfDate + "T00:00:00").toLocaleDateString("en-AU", { day: "numeric", month: "long", year: "numeric", }); return { asOfDate: asOfFormatted, assets: [ { name: "Accounts Receivable", amount: Math.round(accountsReceivable * 100) / 100 }, ].filter((a) => a.amount > 0), totalAssets: Math.round(totalAssets * 100) / 100, liabilities: [ { name: "Accounts Payable", amount: Math.round(accountsPayable * 100) / 100 }, { name: "Payroll Payable", amount: Math.round(payrollLiability * 100) / 100 }, ].filter((l) => l.amount > 0), totalLiabilities: Math.round(totalLiabilities * 100) / 100, equity: [ { name: "Retained Earnings", amount: Math.round(retainedEarnings * 100) / 100 }, ], netAssets: Math.round(netAssets * 100) / 100, }; }