Files
acmcc/src/pages/RecordOwnerPaymentPage.tsx
T
admin e302fb91f0 Accounting platform: remove Zoho, unify reports, board access, vendor sharing
- Remove the Zoho Books integration (edge functions, sync libs, settings,
  reports/overview, banking links, fees tab, import dialog); preserve fee
  rules as a standalone FeesTab and the COA accounting_system classification.
- Financial Overview/Reports (staff + board) render the Accounting dashboard
  and reports; board reports mirror the rich Accounting Reports.
- New Reserve Fund Schedule report + an is_reserve flag on accounts.
- Unify all report exports to a branded format (logo + centered header +
  footer): shared ReportSheet (on-screen) and reportHeader (PDF). Budget vs
  Actuals and Bank Reconciliation PDFs now match the reference layout.
- Render financial reports inline (no preview pop-up).
- Budget Management mirrors Accounting Budgeting (staff-accessible) with SPA
  navigation; editable bills in the Accounting Bills page.
- Negative opening balances flow through to the GL and reports (allow negative
  input; keep non-zero on save; signed CSV import).
- Upload a per-account trial balance via CSV on Opening Balances.
- Board members: read-only RLS access to their association's accounting ledger;
  editable board-members panel on the association page; share vendor contacts
  with the board (toggle + directory section).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 18:29:31 -04:00

193 lines
8.8 KiB
TypeScript

import { useState, useEffect } from "react";
import { supabase } from "@/integrations/supabase/client";
import { useToast } from "@/hooks/use-toast";
import { DollarSign, Save } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import UnitOwnerSelect from "@/components/UnitOwnerSelect";
export default function RecordOwnerPaymentPage() {
const { toast } = useToast();
const [owners, setOwners] = useState<any[]>([]);
const [bankAccounts, setBankAccounts] = useState<any[]>([]);
const [depositBatches, setDepositBatches] = useState<any[]>([]);
const [saving, setSaving] = useState(false);
const [form, setForm] = useState({
owner_id: "", amount: "", date: new Date().toISOString().split("T")[0],
payment_method: "check", reference_number: "", description: "",
bank_account_id: "", deposit_batch_id: "",
});
useEffect(() => {
Promise.all([
supabase.from("owners").select("id, first_name, last_name, balance, association_id, units(unit_number)").eq("status", "active").order("last_name"),
supabase.from("bank_accounts").select("id, account_name, association_id").eq("status", "active").order("account_name"),
supabase.from("deposit_batches").select("id, deposit_date, memo, association_id, bank_accounts(account_name)").eq("status", "open").order("deposit_date", { ascending: false }),
]).then(([oRes, baRes, dbRes]) => {
setOwners(oRes.data || []);
setBankAccounts(baRes.data || []);
setDepositBatches(dbRes.data || []);
});
}, []);
const selectedOwner = owners.find(o => o.id === form.owner_id);
const handleSubmit = async () => {
if (!form.owner_id || !form.amount) {
toast({ variant: "destructive", title: "Owner and amount are required" });
return;
}
setSaving(true);
const amount = parseFloat(form.amount);
const owner = owners.find(o => o.id === form.owner_id);
// 1. Create owner ledger entry
const { data: ledgerEntry } = await supabase.from("owner_ledger_entries").insert({
owner_id: form.owner_id,
association_id: owner?.association_id,
unit_id: owner?.unit_id || null,
date: form.date,
transaction_type: "payment",
description: form.description || `Payment - ${form.payment_method}${form.reference_number ? ` #${form.reference_number}` : ""}`,
debit: 0,
credit: amount,
reference_type: "owner_payment",
}).select("id").single();
// 2. Update owner balance
const newBalance = Number(owner?.balance || 0) - amount;
await supabase.from("owners").update({ balance: newBalance }).eq("id", form.owner_id);
// 3. If direct to bank (no deposit batch), create bank transaction
if (form.bank_account_id && !form.deposit_batch_id) {
await supabase.from("bank_transactions").insert({
bank_account_id: form.bank_account_id,
association_id: owner?.association_id,
date: form.date,
transaction_type: "owner_payment",
description: `Payment from ${owner?.first_name} ${owner?.last_name}${form.reference_number ? ` #${form.reference_number}` : ""}`,
debit: amount, credit: 0,
related_entity_type: "owner_payment",
related_entity_id: ledgerEntry?.id || null,
});
}
// 4. If deposit batch, add to batch
if (form.deposit_batch_id && ledgerEntry) {
await supabase.from("deposit_batch_items").insert({
deposit_batch_id: form.deposit_batch_id,
owner_ledger_entry_id: ledgerEntry.id,
description: `${owner?.first_name} ${owner?.last_name} - Payment`,
amount,
});
// Update batch total
const batch = depositBatches.find(b => b.id === form.deposit_batch_id);
if (batch) {
await supabase.from("deposit_batches").update({ total_amount: Number(batch.total_amount || 0) + amount }).eq("id", form.deposit_batch_id);
}
}
toast({ title: "Payment recorded successfully" });
setForm({ ...form, owner_id: "", amount: "", reference_number: "", description: "" });
setSaving(false);
// Refresh owners
const { data } = await supabase.from("owners").select("id, first_name, last_name, balance, association_id, units(unit_number)").eq("status", "active").order("last_name");
setOwners(data || []);
};
return (
<div className="space-y-6 max-w-2xl">
<div>
<h1 className="text-2xl font-bold text-foreground flex items-center gap-2"><DollarSign className="h-6 w-6 text-primary" /> Record Owner Payment</h1>
<p className="text-sm text-muted-foreground mt-1">Record a payment from an owner and update their ledger.</p>
</div>
<Card>
<CardContent className="pt-6 space-y-4">
<div>
<Label>Owner</Label>
<UnitOwnerSelect
owners={owners}
value={form.owner_id}
onValueChange={(v) => setForm({ ...form, owner_id: v })}
placeholder="Select owner"
showBalance
/>
</div>
{selectedOwner && (
<Card className="bg-muted/50">
<CardContent className="pt-4 pb-4">
<div className="flex justify-between">
<span className="text-sm text-muted-foreground">Current Balance</span>
<span className={`font-bold font-mono ${Number(selectedOwner.balance) > 0 ? "text-destructive" : "text-emerald-600"}`}>
${Math.abs(Number(selectedOwner.balance)).toFixed(2)} {Number(selectedOwner.balance) < 0 ? "CR" : Number(selectedOwner.balance) > 0 ? "DUE" : ""}
</span>
</div>
</CardContent>
</Card>
)}
<div className="grid grid-cols-2 gap-4">
<div><Label>Payment Amount</Label><Input type="number" value={form.amount} onChange={(e) => setForm({ ...form, amount: e.target.value })} placeholder="0.00" /></div>
<div><Label>Payment Date</Label><Input type="date" value={form.date} onChange={(e) => setForm({ ...form, date: e.target.value })} /></div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label>Payment Method</Label>
<Select value={form.payment_method} onValueChange={(v) => setForm({ ...form, payment_method: v })}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="check">Check</SelectItem>
<SelectItem value="ach">ACH</SelectItem>
<SelectItem value="credit_card">Credit Card</SelectItem>
<SelectItem value="cash">Cash</SelectItem>
<SelectItem value="other">Other</SelectItem>
</SelectContent>
</Select>
</div>
<div><Label>Reference / Check #</Label><Input value={form.reference_number} onChange={(e) => setForm({ ...form, reference_number: e.target.value })} /></div>
</div>
<div>
<Label>Post Directly to Bank Account (optional)</Label>
<Select value={form.bank_account_id} onValueChange={(v) => setForm({ ...form, bank_account_id: v })}>
<SelectTrigger><SelectValue placeholder="None — use deposit batch" /></SelectTrigger>
<SelectContent>
<SelectItem value="">None</SelectItem>
{bankAccounts.map(a => <SelectItem key={a.id} value={a.id}>{a.account_name}</SelectItem>)}
</SelectContent>
</Select>
</div>
{!form.bank_account_id && (
<div>
<Label>Add to Deposit Batch (optional)</Label>
<Select value={form.deposit_batch_id} onValueChange={(v) => setForm({ ...form, deposit_batch_id: v })}>
<SelectTrigger><SelectValue placeholder="None" /></SelectTrigger>
<SelectContent>
<SelectItem value="">None</SelectItem>
{depositBatches.map(b => <SelectItem key={b.id} value={b.id}>{b.deposit_date} {b.bank_accounts?.account_name} {b.memo ? `(${b.memo})` : ""}</SelectItem>)}
</SelectContent>
</Select>
</div>
)}
<div><Label>Description / Memo</Label><Textarea value={form.description} onChange={(e) => setForm({ ...form, description: e.target.value })} rows={2} /></div>
<Button onClick={handleSubmit} disabled={saving} className="w-full gap-2">
<Save className="h-4 w-4" /> {saving ? "Recording..." : "Record Payment"}
</Button>
</CardContent>
</Card>
</div>
);
}