mirror of
https://github.com/renee-png/acmcc.git
synced 2026-06-21 09:50:01 +00:00
e302fb91f0
- 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>
193 lines
8.8 KiB
TypeScript
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>
|
|
);
|
|
}
|