Changes
Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
co-authored by
renee-png
parent
0b76a68698
commit
0881a0efe4
@@ -0,0 +1,96 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
fetchClients,
|
||||
fetchHomeowners,
|
||||
type ClientLite,
|
||||
type HomeownerLite,
|
||||
} from "@/lib/forms-shared";
|
||||
|
||||
export function ClientHomeownerPicker({
|
||||
clientId,
|
||||
homeownerId,
|
||||
onClientChange,
|
||||
onHomeownerChange,
|
||||
showHomeowner = true,
|
||||
}: {
|
||||
clientId: string;
|
||||
homeownerId: string;
|
||||
onClientChange: (id: string, client: ClientLite | null) => void;
|
||||
onHomeownerChange: (id: string, homeowner: HomeownerLite | null) => void;
|
||||
showHomeowner?: boolean;
|
||||
}) {
|
||||
const [clients, setClients] = useState<ClientLite[]>([]);
|
||||
const [homeowners, setHomeowners] = useState<HomeownerLite[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchClients().then(setClients);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!clientId) {
|
||||
setHomeowners([]);
|
||||
return;
|
||||
}
|
||||
fetchHomeowners(clientId).then(setHomeowners);
|
||||
}, [clientId]);
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>Client / Association</Label>
|
||||
<Select
|
||||
value={clientId}
|
||||
onValueChange={(id) => {
|
||||
const c = clients.find((x) => x.id === id) ?? null;
|
||||
onClientChange(id, c);
|
||||
onHomeownerChange("", null);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a client" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{clients.map((c) => (
|
||||
<SelectItem key={c.id} value={c.id}>
|
||||
{c.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{showHomeowner && (
|
||||
<div>
|
||||
<Label>Homeowner (optional)</Label>
|
||||
<Select
|
||||
value={homeownerId}
|
||||
onValueChange={(id) => {
|
||||
const h = homeowners.find((x) => x.id === id) ?? null;
|
||||
onHomeownerChange(id, h);
|
||||
}}
|
||||
disabled={!clientId}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={clientId ? "Select homeowner" : "Pick client first"} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{homeowners.map((h) => (
|
||||
<SelectItem key={h.id} value={h.id}>
|
||||
{h.last_name}, {h.first_name}
|
||||
{h.unit_number ? ` — Unit ${h.unit_number}` : ""}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
import { supabase } from "@/integrations/supabase/client";
|
||||
import { format } from "date-fns";
|
||||
|
||||
export interface ClientLite {
|
||||
id: string;
|
||||
name: string;
|
||||
address_line1: string | null;
|
||||
address_line2: string | null;
|
||||
city: string | null;
|
||||
state: string | null;
|
||||
postal_code: string | null;
|
||||
primary_contact_email: string | null;
|
||||
primary_contact_phone: string | null;
|
||||
annual_interest_rate: number | null;
|
||||
}
|
||||
|
||||
export interface HomeownerLite {
|
||||
id: string;
|
||||
client_id: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
unit_number: string | null;
|
||||
address: string | null;
|
||||
email: string | null;
|
||||
phone: string | null;
|
||||
opening_balance: number;
|
||||
}
|
||||
|
||||
export async function fetchClients(): Promise<ClientLite[]> {
|
||||
const { data } = await supabase
|
||||
.from("clients")
|
||||
.select(
|
||||
"id,name,address_line1,address_line2,city,state,postal_code,primary_contact_email,primary_contact_phone,annual_interest_rate",
|
||||
)
|
||||
.order("name");
|
||||
return (data ?? []) as ClientLite[];
|
||||
}
|
||||
|
||||
export async function fetchHomeowners(clientId: string): Promise<HomeownerLite[]> {
|
||||
const { data } = await supabase
|
||||
.from("homeowners")
|
||||
.select("id,client_id,first_name,last_name,unit_number,address,email,phone,opening_balance")
|
||||
.eq("client_id", clientId)
|
||||
.order("last_name");
|
||||
return (data ?? []) as HomeownerLite[];
|
||||
}
|
||||
|
||||
export function ownerFullName(h: HomeownerLite | null | undefined): string {
|
||||
if (!h) return "";
|
||||
return `${h.first_name} ${h.last_name}`.trim();
|
||||
}
|
||||
|
||||
export function clientAddressLines(c: ClientLite | null | undefined): string[] {
|
||||
if (!c) return [];
|
||||
const lines: string[] = [];
|
||||
if (c.address_line1) lines.push(c.address_line1);
|
||||
if (c.address_line2) lines.push(c.address_line2);
|
||||
const cityLine = [c.city, c.state, c.postal_code].filter(Boolean).join(", ");
|
||||
if (cityLine) lines.push(cityLine);
|
||||
return lines;
|
||||
}
|
||||
|
||||
export function ownerMailingLines(h: HomeownerLite | null | undefined): string[] {
|
||||
if (!h) return [];
|
||||
const lines: string[] = [ownerFullName(h)];
|
||||
if (h.address) lines.push(h.address);
|
||||
return lines;
|
||||
}
|
||||
|
||||
export const SYSTEM_VARIABLES = [
|
||||
{ key: "{{clientName}}", description: "Client / Association name" },
|
||||
{ key: "{{ownerName}}", description: "Homeowner full name" },
|
||||
{ key: "{{propertyAddress}}", description: "Homeowner property address" },
|
||||
{ key: "{{unitNumber}}", description: "Homeowner unit number" },
|
||||
{ key: "{{accountNumber}}", description: "Account / unit identifier" },
|
||||
{ key: "{{balance}}", description: "Current balance" },
|
||||
{ key: "{{currentDate}}", description: "Today's date" },
|
||||
{ key: "{{firmName}}", description: "Your firm name" },
|
||||
] as const;
|
||||
|
||||
export function applyVariables(
|
||||
body: string,
|
||||
ctx: { client?: ClientLite | null; homeowner?: HomeownerLite | null; firmName?: string },
|
||||
): string {
|
||||
const today = format(new Date(), "MMMM d, yyyy");
|
||||
const replacements: Record<string, string> = {
|
||||
"{{clientName}}": ctx.client?.name ?? "",
|
||||
"{{ownerName}}": ownerFullName(ctx.homeowner),
|
||||
"{{propertyAddress}}": ctx.homeowner?.address ?? "",
|
||||
"{{unitNumber}}": ctx.homeowner?.unit_number ?? "",
|
||||
"{{accountNumber}}": ctx.homeowner?.unit_number ?? "",
|
||||
"{{balance}}": (ctx.homeowner?.opening_balance ?? 0).toFixed(2),
|
||||
"{{currentDate}}": today,
|
||||
"{{firmName}}": ctx.firmName ?? "",
|
||||
};
|
||||
let out = body;
|
||||
for (const [k, v] of Object.entries(replacements)) {
|
||||
out = out.split(k).join(v);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function fmtCurrency(n: number | string | null | undefined): string {
|
||||
const v = typeof n === "string" ? parseFloat(n) : (n ?? 0);
|
||||
return (Number.isFinite(v) ? v : 0).toLocaleString("en-US", {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
});
|
||||
}
|
||||
|
||||
export function fmtDateLong(d: Date | string | null | undefined): string {
|
||||
if (!d) return "";
|
||||
const date = typeof d === "string" ? new Date(d.includes("T") ? d : d + "T12:00:00") : d;
|
||||
return format(date, "MMMM d, yyyy");
|
||||
}
|
||||
|
||||
export interface FirmInfo {
|
||||
company_name: string | null;
|
||||
address_line1: string | null;
|
||||
address_line2: string | null;
|
||||
city: string | null;
|
||||
state: string | null;
|
||||
postal_code: string | null;
|
||||
contact_email: string | null;
|
||||
contact_phone: string | null;
|
||||
website: string | null;
|
||||
}
|
||||
|
||||
export async function fetchFirm(): Promise<FirmInfo | null> {
|
||||
const { data } = await supabase.from("firm_settings").select("*").maybeSingle();
|
||||
return (data as FirmInfo) ?? null;
|
||||
}
|
||||
|
||||
export async function savePdfToDocuments(opts: {
|
||||
blob: Blob;
|
||||
caseId: string;
|
||||
name: string;
|
||||
folder?: string;
|
||||
}) {
|
||||
const { blob, caseId, name, folder = "Forms & Letters" } = opts;
|
||||
const path = `${caseId}/${Date.now()}-${name}`;
|
||||
const { error: upErr } = await supabase.storage
|
||||
.from("case-documents")
|
||||
.upload(path, blob, { contentType: "application/pdf", upsert: false });
|
||||
if (upErr) throw upErr;
|
||||
const { data: u } = await supabase.auth.getUser();
|
||||
const { error: insErr } = await supabase.from("documents").insert({
|
||||
case_id: caseId,
|
||||
folder,
|
||||
name,
|
||||
storage_path: path,
|
||||
mime_type: "application/pdf",
|
||||
size_bytes: blob.size,
|
||||
uploaded_by: u.user?.id ?? null,
|
||||
});
|
||||
if (insErr) throw insErr;
|
||||
}
|
||||
Reference in New Issue
Block a user