mirror of
https://github.com/renee-png/acmcc.git
synced 2026-06-21 09:50:01 +00:00
183fe0a93c
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
505 lines
24 KiB
TypeScript
505 lines
24 KiB
TypeScript
import { useState, useEffect, useCallback } from "react";
|
|
import { supabase } from "@/integrations/supabase/client";
|
|
import { useToast } from "@/hooks/use-toast";
|
|
import { DollarSign, Plus, MoreHorizontal, Edit, Trash2, Check, ChevronDown, ChevronRight, CalendarCheck } from "lucide-react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Card, CardContent } from "@/components/ui/card";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from "@/components/ui/dialog";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
|
import { Checkbox } from "@/components/ui/checkbox";
|
|
import { useAuth } from "@/contexts/AuthContext";
|
|
import { format } from "date-fns";
|
|
|
|
export default function PaymentPlansPage() {
|
|
const { toast } = useToast();
|
|
const { user } = useAuth();
|
|
const [plans, setPlans] = useState<any[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [dialogOpen, setDialogOpen] = useState(false);
|
|
const [editing, setEditing] = useState<any>(null);
|
|
const [associations, setAssociations] = useState<any[]>([]);
|
|
const [owners, setOwners] = useState<any[]>([]);
|
|
const [units, setUnits] = useState<any[]>([]);
|
|
const [expandedPlan, setExpandedPlan] = useState<string | null>(null);
|
|
const [installments, setInstallments] = useState<Record<string, any[]>>({});
|
|
|
|
const [form, setForm] = useState({
|
|
association_id: "",
|
|
owner_id: "",
|
|
unit_id: "",
|
|
total_amount: "",
|
|
monthly_payment: "",
|
|
start_date: "",
|
|
end_date: "",
|
|
status: "active",
|
|
notes: "",
|
|
});
|
|
|
|
const fetchData = useCallback(async () => {
|
|
setLoading(true);
|
|
const { data } = await supabase
|
|
.from("payment_plans")
|
|
.select("*, associations(name), owners(first_name, last_name), units:unit_id(unit_number, address)")
|
|
.order("created_at", { ascending: false });
|
|
setPlans(data || []);
|
|
setLoading(false);
|
|
}, []);
|
|
|
|
const fetchAssociations = useCallback(async () => {
|
|
const { data } = await supabase.from("associations").select("id, name").eq("status", "active").order("name");
|
|
setAssociations(data || []);
|
|
}, []);
|
|
|
|
useEffect(() => { fetchData(); fetchAssociations(); }, [fetchData, fetchAssociations]);
|
|
|
|
// Load owners and units when association changes
|
|
const loadAssociationData = useCallback(async (assocId: string) => {
|
|
if (!assocId) { setOwners([]); setUnits([]); return; }
|
|
const [ownersRes, unitsRes] = await Promise.all([
|
|
supabase.from("owners").select("id, first_name, last_name, account_number").eq("association_id", assocId).eq("status", "active").order("last_name"),
|
|
supabase.from("units").select("id, unit_number, address").eq("association_id", assocId).order("unit_number"),
|
|
]);
|
|
setOwners(ownersRes.data || []);
|
|
setUnits(unitsRes.data || []);
|
|
}, []);
|
|
|
|
const openNew = () => {
|
|
setEditing(null);
|
|
setForm({ association_id: "", owner_id: "", unit_id: "", total_amount: "", monthly_payment: "", start_date: "", end_date: "", status: "active", notes: "" });
|
|
setOwners([]);
|
|
setUnits([]);
|
|
setDialogOpen(true);
|
|
};
|
|
|
|
const openEdit = (p: any) => {
|
|
setEditing(p);
|
|
setForm({
|
|
association_id: p.association_id || "",
|
|
owner_id: p.owner_id || "",
|
|
unit_id: p.unit_id || "",
|
|
total_amount: p.total_amount?.toString() || "",
|
|
monthly_payment: p.monthly_payment?.toString() || "",
|
|
start_date: p.start_date || "",
|
|
end_date: p.end_date || "",
|
|
status: p.status,
|
|
notes: p.notes || "",
|
|
});
|
|
if (p.association_id) loadAssociationData(p.association_id);
|
|
setDialogOpen(true);
|
|
};
|
|
|
|
const handleSave = async () => {
|
|
if (!form.association_id) {
|
|
toast({ variant: "destructive", title: "Error", description: "Please select an association." });
|
|
return;
|
|
}
|
|
const payload: any = {
|
|
association_id: form.association_id,
|
|
owner_id: form.owner_id || null,
|
|
unit_id: form.unit_id || null,
|
|
total_amount: parseFloat(form.total_amount) || 0,
|
|
monthly_payment: parseFloat(form.monthly_payment) || 0,
|
|
start_date: form.start_date || null,
|
|
end_date: form.end_date || null,
|
|
status: form.status,
|
|
notes: form.notes || null,
|
|
};
|
|
|
|
if (editing) {
|
|
await supabase.from("payment_plans").update(payload).eq("id", editing.id);
|
|
toast({ title: "Updated" });
|
|
} else {
|
|
payload.created_by = user?.id;
|
|
const { data, error } = await supabase.from("payment_plans").insert(payload).select().single();
|
|
if (error) { toast({ variant: "destructive", title: "Error", description: error.message }); return; }
|
|
// Auto-generate installments if we have monthly payment and start/end dates
|
|
if (data && form.start_date && form.end_date && parseFloat(form.monthly_payment) > 0) {
|
|
await generateInstallments(data.id, form.start_date, form.end_date, parseFloat(form.monthly_payment));
|
|
}
|
|
toast({ title: "Plan created" });
|
|
}
|
|
setDialogOpen(false);
|
|
fetchData();
|
|
};
|
|
|
|
const generateInstallments = async (planId: string, startDate: string, endDate: string, monthlyAmount: number) => {
|
|
const installmentRows: any[] = [];
|
|
let current = new Date(startDate + "T00:00:00");
|
|
const end = new Date(endDate + "T00:00:00");
|
|
let num = 1;
|
|
while (current <= end) {
|
|
installmentRows.push({
|
|
payment_plan_id: planId,
|
|
installment_number: num,
|
|
due_date: current.toISOString().split("T")[0],
|
|
amount_due: monthlyAmount,
|
|
status: "pending",
|
|
});
|
|
num++;
|
|
current.setMonth(current.getMonth() + 1);
|
|
}
|
|
if (installmentRows.length > 0) {
|
|
await supabase.from("payment_plan_installments").insert(installmentRows);
|
|
}
|
|
};
|
|
|
|
const handleDelete = async (id: string) => {
|
|
await supabase.from("payment_plans").delete().eq("id", id);
|
|
toast({ title: "Deleted" });
|
|
fetchData();
|
|
};
|
|
|
|
// Fetch installments for a plan
|
|
const fetchInstallments = async (planId: string) => {
|
|
const { data } = await supabase
|
|
.from("payment_plan_installments")
|
|
.select("*")
|
|
.eq("payment_plan_id", planId)
|
|
.order("installment_number");
|
|
setInstallments((prev) => ({ ...prev, [planId]: data || [] }));
|
|
};
|
|
|
|
const toggleExpand = (planId: string) => {
|
|
if (expandedPlan === planId) {
|
|
setExpandedPlan(null);
|
|
} else {
|
|
setExpandedPlan(planId);
|
|
if (!installments[planId]) fetchInstallments(planId);
|
|
}
|
|
};
|
|
|
|
// Mark installment as paid
|
|
const markInstallmentPaid = async (inst: any) => {
|
|
const newStatus = inst.status === "paid" ? "pending" : "paid";
|
|
const updates: any = {
|
|
status: newStatus,
|
|
paid_date: newStatus === "paid" ? new Date().toISOString().split("T")[0] : null,
|
|
amount_paid: newStatus === "paid" ? inst.amount_due : 0,
|
|
};
|
|
await supabase.from("payment_plan_installments").update(updates).eq("id", inst.id);
|
|
fetchInstallments(inst.payment_plan_id);
|
|
toast({ title: newStatus === "paid" ? "Marked as Paid" : "Marked as Pending" });
|
|
};
|
|
|
|
// Add a single installment
|
|
const [addInstDialog, setAddInstDialog] = useState<string | null>(null);
|
|
const [instForm, setInstForm] = useState({ due_date: "", amount_due: "" });
|
|
|
|
const handleAddInstallment = async () => {
|
|
if (!addInstDialog) return;
|
|
const existing = installments[addInstDialog] || [];
|
|
const nextNum = existing.length > 0 ? Math.max(...existing.map((i: any) => i.installment_number)) + 1 : 1;
|
|
await supabase.from("payment_plan_installments").insert({
|
|
payment_plan_id: addInstDialog,
|
|
installment_number: nextNum,
|
|
due_date: instForm.due_date,
|
|
amount_due: parseFloat(instForm.amount_due) || 0,
|
|
status: "pending",
|
|
});
|
|
fetchInstallments(addInstDialog);
|
|
setAddInstDialog(null);
|
|
setInstForm({ due_date: "", amount_due: "" });
|
|
toast({ title: "Installment added" });
|
|
};
|
|
|
|
const statusColors: Record<string, string> = {
|
|
active: "bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300",
|
|
completed: "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300",
|
|
defaulted: "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300",
|
|
};
|
|
|
|
const fmt = (n: number) => `$${Number(n).toLocaleString(undefined, { minimumFractionDigits: 2 })}`;
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-foreground flex items-center gap-2">
|
|
<DollarSign className="h-6 w-6 text-primary" /> Payment Plans
|
|
</h1>
|
|
<p className="text-sm text-muted-foreground mt-1">Set up and manage installment payment plans.</p>
|
|
</div>
|
|
<Button className="gap-2" onClick={openNew}><Plus className="h-4 w-4" /> New Plan</Button>
|
|
</div>
|
|
|
|
{loading ? (
|
|
<div className="flex justify-center py-12"><div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" /></div>
|
|
) : plans.length === 0 ? (
|
|
<Card><CardContent className="py-12 text-center text-muted-foreground">No payment plans found.</CardContent></Card>
|
|
) : (
|
|
<Card>
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead className="w-8" />
|
|
<TableHead>Owner</TableHead>
|
|
<TableHead>Property</TableHead>
|
|
<TableHead>Association</TableHead>
|
|
<TableHead>Total</TableHead>
|
|
<TableHead>Monthly</TableHead>
|
|
<TableHead>Progress</TableHead>
|
|
<TableHead>Status</TableHead>
|
|
<TableHead className="w-12" />
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{plans.map((p) => {
|
|
const isExpanded = expandedPlan === p.id;
|
|
const planInstallments = installments[p.id] || [];
|
|
const paidCount = planInstallments.filter((i: any) => i.status === "paid").length;
|
|
const totalCount = planInstallments.length;
|
|
const paidAmount = planInstallments.reduce((sum: number, i: any) => sum + Number(i.amount_paid || 0), 0);
|
|
|
|
return (
|
|
<>
|
|
<TableRow key={p.id} className="cursor-pointer hover:bg-muted/50" onClick={() => toggleExpand(p.id)}>
|
|
<TableCell>
|
|
{isExpanded ? <ChevronDown className="h-4 w-4 text-muted-foreground" /> : <ChevronRight className="h-4 w-4 text-muted-foreground" />}
|
|
</TableCell>
|
|
<TableCell className="font-medium">
|
|
{p.owners ? `${p.owners.first_name} ${p.owners.last_name}` : <span className="text-muted-foreground">—</span>}
|
|
</TableCell>
|
|
<TableCell>
|
|
{p.units ? (p.units.unit_number || p.units.address) : <span className="text-muted-foreground">—</span>}
|
|
</TableCell>
|
|
<TableCell className="text-sm">{p.associations?.name || "—"}</TableCell>
|
|
<TableCell>{fmt(p.total_amount)}</TableCell>
|
|
<TableCell>{fmt(p.monthly_payment)}</TableCell>
|
|
<TableCell>
|
|
{totalCount > 0 ? (
|
|
<div className="flex items-center gap-2">
|
|
<div className="flex-1 h-2 bg-muted rounded-full overflow-hidden max-w-[80px]">
|
|
<div className="h-full bg-primary rounded-full transition-all" style={{ width: `${(paidCount / totalCount) * 100}%` }} />
|
|
</div>
|
|
<span className="text-xs text-muted-foreground">{paidCount}/{totalCount}</span>
|
|
</div>
|
|
) : (
|
|
<span className="text-xs text-muted-foreground">No installments</span>
|
|
)}
|
|
</TableCell>
|
|
<TableCell><Badge className={statusColors[p.status] || "bg-muted text-muted-foreground"}>{p.status}</Badge></TableCell>
|
|
<TableCell onClick={(e) => e.stopPropagation()}>
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild><Button variant="ghost" size="icon" className="h-8 w-8"><MoreHorizontal className="h-4 w-4" /></Button></DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end">
|
|
<DropdownMenuItem onClick={() => openEdit(p)}><Edit className="h-4 w-4 mr-2" /> Edit</DropdownMenuItem>
|
|
<DropdownMenuItem className="text-destructive" onClick={() => handleDelete(p.id)}><Trash2 className="h-4 w-4 mr-2" /> Delete</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</TableCell>
|
|
</TableRow>
|
|
|
|
{/* Expanded installments */}
|
|
{isExpanded && (
|
|
<TableRow key={`${p.id}-inst`}>
|
|
<TableCell colSpan={9} className="bg-muted/20 p-0">
|
|
<div className="px-6 py-4 space-y-3">
|
|
<div className="flex items-center justify-between">
|
|
<h3 className="text-sm font-semibold text-foreground flex items-center gap-2">
|
|
<CalendarCheck className="h-4 w-4" /> Payment Schedule
|
|
{totalCount > 0 && <span className="text-muted-foreground font-normal">— {fmt(paidAmount)} paid of {fmt(p.total_amount)}</span>}
|
|
</h3>
|
|
<Button size="sm" variant="outline" className="gap-1.5 text-xs" onClick={() => { setAddInstDialog(p.id); setInstForm({ due_date: "", amount_due: p.monthly_payment?.toString() || "" }); }}>
|
|
<Plus className="h-3 w-3" /> Add Installment
|
|
</Button>
|
|
</div>
|
|
|
|
{planInstallments.length === 0 ? (
|
|
<p className="text-sm text-muted-foreground py-2">No installments yet. Add installments or they'll be auto-generated when creating a new plan with start/end dates.</p>
|
|
) : (
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead className="w-8">#</TableHead>
|
|
<TableHead>Due Date</TableHead>
|
|
<TableHead>Amount Due</TableHead>
|
|
<TableHead>Amount Paid</TableHead>
|
|
<TableHead>Paid Date</TableHead>
|
|
<TableHead>Status</TableHead>
|
|
<TableHead className="w-24">Action</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{planInstallments.map((inst: any) => (
|
|
<TableRow key={inst.id} className={inst.status === "paid" ? "bg-emerald-50/50 dark:bg-emerald-900/10" : ""}>
|
|
<TableCell className="text-xs text-muted-foreground">{inst.installment_number}</TableCell>
|
|
<TableCell className="text-sm">{inst.due_date ? format(new Date(inst.due_date + "T00:00:00"), "MMM d, yyyy") : "—"}</TableCell>
|
|
<TableCell className="text-sm">{fmt(inst.amount_due)}</TableCell>
|
|
<TableCell className="text-sm">{inst.amount_paid > 0 ? fmt(inst.amount_paid) : "—"}</TableCell>
|
|
<TableCell className="text-sm">{inst.paid_date ? format(new Date(inst.paid_date + "T00:00:00"), "MMM d, yyyy") : "—"}</TableCell>
|
|
<TableCell>
|
|
<Badge className={inst.status === "paid" ? "bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300" : "bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300"}>
|
|
{inst.status}
|
|
</Badge>
|
|
</TableCell>
|
|
<TableCell>
|
|
<Button
|
|
size="sm"
|
|
variant={inst.status === "paid" ? "outline" : "default"}
|
|
className="gap-1 text-xs h-7"
|
|
onClick={() => markInstallmentPaid(inst)}
|
|
>
|
|
<Check className="h-3 w-3" />
|
|
{inst.status === "paid" ? "Undo" : "Paid"}
|
|
</Button>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
)}
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
)}
|
|
</>
|
|
);
|
|
})}
|
|
</TableBody>
|
|
</Table>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Create / Edit Plan Dialog */}
|
|
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
|
<DialogContent className="max-w-lg">
|
|
<DialogHeader>
|
|
<DialogTitle>{editing ? "Edit" : "New"} Payment Plan</DialogTitle>
|
|
<DialogDescription>Configure the payment plan details, owner, and property.</DialogDescription>
|
|
</DialogHeader>
|
|
<div className="space-y-4">
|
|
{/* Association */}
|
|
<div>
|
|
<Label className="text-xs">Association</Label>
|
|
<Select
|
|
value={form.association_id}
|
|
onValueChange={(v) => {
|
|
setForm({ ...form, association_id: v, owner_id: "", unit_id: "" });
|
|
loadAssociationData(v);
|
|
}}
|
|
>
|
|
<SelectTrigger className="h-9 text-sm"><SelectValue placeholder="Select association" /></SelectTrigger>
|
|
<SelectContent>
|
|
{associations.map((a) => <SelectItem key={a.id} value={a.id}>{a.name}</SelectItem>)}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* Owner & Unit */}
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<Label className="text-xs">Owner</Label>
|
|
<Select value={form.owner_id} onValueChange={(v) => setForm({ ...form, owner_id: v })}>
|
|
<SelectTrigger className="h-9 text-sm"><SelectValue placeholder="Select owner" /></SelectTrigger>
|
|
<SelectContent>
|
|
{owners.map((o) => (
|
|
<SelectItem key={o.id} value={o.id}>
|
|
{o.last_name}, {o.first_name} {o.account_number ? `(${o.account_number})` : ""}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div>
|
|
<Label className="text-xs">Property / Unit</Label>
|
|
<Select value={form.unit_id} onValueChange={(v) => setForm({ ...form, unit_id: v })}>
|
|
<SelectTrigger className="h-9 text-sm"><SelectValue placeholder="Select unit" /></SelectTrigger>
|
|
<SelectContent>
|
|
{units.map((u) => (
|
|
<SelectItem key={u.id} value={u.id}>
|
|
{u.unit_number || u.address}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Amounts */}
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<Label className="text-xs">Total Amount</Label>
|
|
<Input type="number" value={form.total_amount} onChange={(e) => setForm({ ...form, total_amount: e.target.value })} className="h-9 text-sm" />
|
|
</div>
|
|
<div>
|
|
<Label className="text-xs">Monthly Payment</Label>
|
|
<Input type="number" value={form.monthly_payment} onChange={(e) => setForm({ ...form, monthly_payment: e.target.value })} className="h-9 text-sm" />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Dates */}
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<Label className="text-xs">Start Date</Label>
|
|
<Input type="date" value={form.start_date} onChange={(e) => setForm({ ...form, start_date: e.target.value })} className="h-9 text-sm" />
|
|
</div>
|
|
<div>
|
|
<Label className="text-xs">End Date</Label>
|
|
<Input type="date" value={form.end_date} onChange={(e) => setForm({ ...form, end_date: e.target.value })} className="h-9 text-sm" />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Status */}
|
|
<div>
|
|
<Label className="text-xs">Status</Label>
|
|
<Select value={form.status} onValueChange={(v) => setForm({ ...form, status: v })}>
|
|
<SelectTrigger className="h-9 text-sm"><SelectValue /></SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="active">Active</SelectItem>
|
|
<SelectItem value="completed">Completed</SelectItem>
|
|
<SelectItem value="defaulted">Defaulted</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* Notes */}
|
|
<div>
|
|
<Label className="text-xs">Notes</Label>
|
|
<Textarea value={form.notes} onChange={(e) => setForm({ ...form, notes: e.target.value })} rows={2} className="text-sm" />
|
|
</div>
|
|
|
|
{!editing && form.start_date && form.end_date && parseFloat(form.monthly_payment) > 0 && (
|
|
<p className="text-xs text-muted-foreground bg-muted/50 rounded p-2">
|
|
💡 Installments will be auto-generated monthly from {form.start_date} to {form.end_date}.
|
|
</p>
|
|
)}
|
|
</div>
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={() => setDialogOpen(false)}>Cancel</Button>
|
|
<Button onClick={handleSave}>{editing ? "Update" : "Create"}</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
{/* Add Installment Dialog */}
|
|
<Dialog open={!!addInstDialog} onOpenChange={() => setAddInstDialog(null)}>
|
|
<DialogContent className="max-w-sm">
|
|
<DialogHeader>
|
|
<DialogTitle>Add Installment</DialogTitle>
|
|
</DialogHeader>
|
|
<div className="space-y-4">
|
|
<div>
|
|
<Label className="text-xs">Due Date</Label>
|
|
<Input type="date" value={instForm.due_date} onChange={(e) => setInstForm({ ...instForm, due_date: e.target.value })} className="h-9 text-sm" />
|
|
</div>
|
|
<div>
|
|
<Label className="text-xs">Amount Due</Label>
|
|
<Input type="number" value={instForm.amount_due} onChange={(e) => setInstForm({ ...instForm, amount_due: e.target.value })} className="h-9 text-sm" />
|
|
</div>
|
|
</div>
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={() => setAddInstDialog(null)}>Cancel</Button>
|
|
<Button onClick={handleAddInstallment} disabled={!instForm.due_date}>Add</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
);
|
|
}
|