Rewrote collections ledger UI
X-Lovable-Edit-ID: edt-e110d3f5-d709-4f4c-bf2d-a1694377097a Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -205,6 +205,10 @@ export type Database = {
|
||||
}
|
||||
collection_ledger_entries: {
|
||||
Row: {
|
||||
account: string | null
|
||||
admin: number
|
||||
assess: number
|
||||
bank: number
|
||||
collection_id: string
|
||||
created_at: string
|
||||
created_by: string | null
|
||||
@@ -213,10 +217,19 @@ export type Database = {
|
||||
description: string | null
|
||||
entry_date: string
|
||||
id: string
|
||||
interest: number
|
||||
late: number
|
||||
legal: number
|
||||
payment: number
|
||||
transaction_type: string
|
||||
updated_at: string
|
||||
viol: number
|
||||
}
|
||||
Insert: {
|
||||
account?: string | null
|
||||
admin?: number
|
||||
assess?: number
|
||||
bank?: number
|
||||
collection_id: string
|
||||
created_at?: string
|
||||
created_by?: string | null
|
||||
@@ -225,10 +238,19 @@ export type Database = {
|
||||
description?: string | null
|
||||
entry_date?: string
|
||||
id?: string
|
||||
interest?: number
|
||||
late?: number
|
||||
legal?: number
|
||||
payment?: number
|
||||
transaction_type: string
|
||||
updated_at?: string
|
||||
viol?: number
|
||||
}
|
||||
Update: {
|
||||
account?: string | null
|
||||
admin?: number
|
||||
assess?: number
|
||||
bank?: number
|
||||
collection_id?: string
|
||||
created_at?: string
|
||||
created_by?: string | null
|
||||
@@ -237,8 +259,13 @@ export type Database = {
|
||||
description?: string | null
|
||||
entry_date?: string
|
||||
id?: string
|
||||
interest?: number
|
||||
late?: number
|
||||
legal?: number
|
||||
payment?: number
|
||||
transaction_type?: string
|
||||
updated_at?: string
|
||||
viol?: number
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
@@ -250,6 +277,48 @@ export type Database = {
|
||||
},
|
||||
]
|
||||
}
|
||||
collection_payment_allocations: {
|
||||
Row: {
|
||||
amount: number
|
||||
bucket: string
|
||||
collection_id: string
|
||||
created_at: string
|
||||
entry_id: string
|
||||
id: string
|
||||
}
|
||||
Insert: {
|
||||
amount?: number
|
||||
bucket: string
|
||||
collection_id: string
|
||||
created_at?: string
|
||||
entry_id: string
|
||||
id?: string
|
||||
}
|
||||
Update: {
|
||||
amount?: number
|
||||
bucket?: string
|
||||
collection_id?: string
|
||||
created_at?: string
|
||||
entry_id?: string
|
||||
id?: string
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "collection_payment_allocations_collection_id_fkey"
|
||||
columns: ["collection_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "collections"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
{
|
||||
foreignKeyName: "collection_payment_allocations_entry_id_fkey"
|
||||
columns: ["entry_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "collection_ledger_entries"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
]
|
||||
}
|
||||
collection_tasks: {
|
||||
Row: {
|
||||
assignee_id: string | null
|
||||
@@ -391,6 +460,8 @@ export type Database = {
|
||||
current_stage: string | null
|
||||
homeowner_id: string
|
||||
id: string
|
||||
interest_rate_override: number | null
|
||||
name: string | null
|
||||
notes: string | null
|
||||
opened_at: string
|
||||
status: string
|
||||
@@ -404,6 +475,8 @@ export type Database = {
|
||||
current_stage?: string | null
|
||||
homeowner_id: string
|
||||
id?: string
|
||||
interest_rate_override?: number | null
|
||||
name?: string | null
|
||||
notes?: string | null
|
||||
opened_at?: string
|
||||
status?: string
|
||||
@@ -417,6 +490,8 @@ export type Database = {
|
||||
current_stage?: string | null
|
||||
homeowner_id?: string
|
||||
id?: string
|
||||
interest_rate_override?: number | null
|
||||
name?: string | null
|
||||
notes?: string | null
|
||||
opened_at?: string
|
||||
status?: string
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
// Priority order for payment allocation: Bank → Interest → Late → Legal → Admin → Violations → Assessments
|
||||
export const BUCKETS = ["bank", "interest", "late", "legal", "admin", "viol", "assess"] as const;
|
||||
export type Bucket = (typeof BUCKETS)[number];
|
||||
|
||||
export const BUCKET_LABEL: Record<Bucket, string> = {
|
||||
bank: "Bank Fees",
|
||||
interest: "Interest",
|
||||
late: "Late Fees",
|
||||
legal: "Legal Fees",
|
||||
admin: "Admin Fees",
|
||||
viol: "Violations",
|
||||
assess: "Assessments",
|
||||
};
|
||||
|
||||
export const BUCKET_SHORT: Record<Bucket, string> = {
|
||||
bank: "Bank",
|
||||
interest: "Int",
|
||||
late: "Late",
|
||||
legal: "Legal",
|
||||
admin: "Admin",
|
||||
viol: "Viol",
|
||||
assess: "Assess",
|
||||
};
|
||||
|
||||
export interface LedgerEntry {
|
||||
id: string;
|
||||
collection_id: string;
|
||||
entry_date: string;
|
||||
description: string | null;
|
||||
account: string | null;
|
||||
assess: number;
|
||||
late: number;
|
||||
admin: number;
|
||||
legal: number;
|
||||
viol: number;
|
||||
interest: number;
|
||||
bank: number;
|
||||
payment: number;
|
||||
debit?: number;
|
||||
credit?: number;
|
||||
transaction_type?: string;
|
||||
}
|
||||
|
||||
export const num = (v: any) => Number(v) || 0;
|
||||
|
||||
/** Sum of all charge columns for one row */
|
||||
export const rowCharges = (e: Pick<LedgerEntry, Bucket>) =>
|
||||
BUCKETS.reduce((s, b) => s + num((e as any)[b]), 0);
|
||||
|
||||
/** Compute live remaining balance per bucket given chronological entries (FIFO payment allocation by priority). */
|
||||
export function computeBucketBalances(
|
||||
entries: LedgerEntry[],
|
||||
opening = 0,
|
||||
): Record<Bucket, number> & { total: number } {
|
||||
const bal: Record<Bucket, number> = {
|
||||
bank: 0,
|
||||
interest: 0,
|
||||
late: 0,
|
||||
legal: 0,
|
||||
admin: 0,
|
||||
viol: 0,
|
||||
assess: 0,
|
||||
};
|
||||
// Treat opening balance as outstanding assessments
|
||||
bal.assess += opening;
|
||||
|
||||
for (const e of entries) {
|
||||
BUCKETS.forEach((b) => {
|
||||
bal[b] += num((e as any)[b]);
|
||||
});
|
||||
let pay = num(e.payment);
|
||||
if (pay > 0) {
|
||||
for (const b of BUCKETS) {
|
||||
if (pay <= 0) break;
|
||||
if (bal[b] <= 0) continue;
|
||||
const take = Math.min(bal[b], pay);
|
||||
bal[b] -= take;
|
||||
pay -= take;
|
||||
}
|
||||
// any leftover credit goes against assessments (overpayment)
|
||||
if (pay > 0) bal.assess -= pay;
|
||||
}
|
||||
}
|
||||
const total = BUCKETS.reduce((s, b) => s + bal[b], 0);
|
||||
return { ...bal, total };
|
||||
}
|
||||
|
||||
/** Running balance per row */
|
||||
export function withRunningBalance(entries: LedgerEntry[], opening = 0) {
|
||||
let bal = opening;
|
||||
return entries.map((e) => {
|
||||
bal += rowCharges(e) - num(e.payment);
|
||||
return { ...e, runningBalance: bal };
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
-- Add category columns to ledger entries
|
||||
ALTER TABLE public.collection_ledger_entries
|
||||
ADD COLUMN IF NOT EXISTS assess numeric NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS late numeric NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS admin numeric NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS legal numeric NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS viol numeric NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS interest numeric NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS bank numeric NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS payment numeric NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS account text;
|
||||
|
||||
-- Backfill: split existing debit/credit into the appropriate column based on transaction_type
|
||||
UPDATE public.collection_ledger_entries
|
||||
SET
|
||||
assess = CASE WHEN transaction_type = 'assessment' THEN COALESCE(debit,0) ELSE 0 END,
|
||||
late = CASE WHEN transaction_type = 'late_fee' THEN COALESCE(debit,0) ELSE 0 END,
|
||||
admin = CASE WHEN transaction_type = 'admin_fee' THEN COALESCE(debit,0) ELSE 0 END,
|
||||
legal = CASE WHEN transaction_type = 'legal_fee' THEN COALESCE(debit,0) ELSE 0 END,
|
||||
viol = CASE WHEN transaction_type = 'violation' THEN COALESCE(debit,0) ELSE 0 END,
|
||||
interest = CASE WHEN transaction_type = 'interest' THEN COALESCE(debit,0) ELSE 0 END,
|
||||
bank = CASE WHEN transaction_type IN ('bank_fee') THEN COALESCE(debit,0) ELSE 0 END,
|
||||
payment = CASE WHEN transaction_type IN ('payment','adjustment') THEN COALESCE(credit,0) ELSE 0 END,
|
||||
account = transaction_type
|
||||
WHERE assess = 0 AND late = 0 AND admin = 0 AND legal = 0 AND viol = 0 AND interest = 0 AND bank = 0 AND payment = 0;
|
||||
|
||||
-- Add ledger metadata to collections
|
||||
ALTER TABLE public.collections
|
||||
ADD COLUMN IF NOT EXISTS name text,
|
||||
ADD COLUMN IF NOT EXISTS interest_rate_override numeric;
|
||||
|
||||
-- Payment allocations: how a payment row is applied across priority buckets
|
||||
CREATE TABLE IF NOT EXISTS public.collection_payment_allocations (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
entry_id uuid NOT NULL REFERENCES public.collection_ledger_entries(id) ON DELETE CASCADE,
|
||||
collection_id uuid NOT NULL REFERENCES public.collections(id) ON DELETE CASCADE,
|
||||
bucket text NOT NULL, -- 'bank' | 'interest' | 'late' | 'legal' | 'admin' | 'viol' | 'assess'
|
||||
amount numeric NOT NULL DEFAULT 0,
|
||||
created_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
ALTER TABLE public.collection_payment_allocations ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY "cpa_select_case" ON public.collection_payment_allocations
|
||||
FOR SELECT TO authenticated USING (
|
||||
EXISTS (SELECT 1 FROM public.collections c WHERE c.id = collection_id AND can_access_case(c.case_id, auth.uid()))
|
||||
);
|
||||
CREATE POLICY "cpa_insert_case" ON public.collection_payment_allocations
|
||||
FOR INSERT TO authenticated WITH CHECK (
|
||||
EXISTS (SELECT 1 FROM public.collections c WHERE c.id = collection_id AND can_access_case(c.case_id, auth.uid()))
|
||||
);
|
||||
CREATE POLICY "cpa_delete_case" ON public.collection_payment_allocations
|
||||
FOR DELETE TO authenticated USING (
|
||||
EXISTS (SELECT 1 FROM public.collections c WHERE c.id = collection_id AND can_access_case(c.case_id, auth.uid()))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_cpa_entry ON public.collection_payment_allocations(entry_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_cpa_collection ON public.collection_payment_allocations(collection_id);
|
||||
Reference in New Issue
Block a user