import { createFileRoute } from "@tanstack/react-router"; import { useEffect, useState } from "react"; import { supabase } from "@/integrations/supabase/client"; import { useAuth } from "@/lib/auth"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Checkbox } from "@/components/ui/checkbox"; import { Badge } from "@/components/ui/badge"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Clock, Loader2, Pencil, Plus, Receipt, Trash2, ClipboardPaste } from "lucide-react"; import { toast } from "sonner"; import { formatCurrency } from "@/lib/format"; export const Route = createFileRoute("/settings/fees")({ component: FeeSchedulePage, }); interface FeeItem { id: string; name: string; description: string | null; category: "time" | "expense"; amount: number; billable: boolean; active: boolean; sort_order: number; } function FeeSchedulePage() { const { user } = useAuth(); const [items, setItems] = useState([]); const [loading, setLoading] = useState(true); const [editing, setEditing] = useState | null>(null); const [importing, setImporting] = useState<"time" | "expense" | null>(null); const load = async () => { setLoading(true); const { data } = await supabase .from("fee_schedule_items") .select("*") .order("category") .order("sort_order") .order("name"); setItems((data ?? []) as FeeItem[]); setLoading(false); }; useEffect(() => { load(); }, []); const remove = async (id: string) => { if (!confirm("Delete this fee item?")) return; const { error } = await supabase .from("fee_schedule_items") .delete() .eq("id", id); if (error) toast.error("Could not delete", { description: error.message }); else { toast.success("Fee item deleted"); load(); } }; const timeItems = items.filter((i) => i.category === "time"); const expenseItems = items.filter((i) => i.category === "expense"); return (

Define reusable billable items. Time fees set the hourly rate when logging time; expense fees auto-fill the amount when adding an expense. The billable flag becomes the default for new entries.

} unit="/ hr" items={timeItems} loading={loading} onEdit={setEditing} onDelete={remove} onImport={() => setImporting("time")} /> } unit="" items={expenseItems} loading={loading} onEdit={setEditing} onDelete={remove} onImport={() => setImporting("expense")} /> setEditing(null)} onSaved={load} /> setImporting(null)} onSaved={load} />
); } function FeeSection({ title, icon, unit, items, loading, onEdit, onDelete, onImport, }: { title: string; icon: React.ReactNode; unit: string; items: FeeItem[]; loading: boolean; onEdit: (i: FeeItem) => void; onDelete: (id: string) => void; onImport: () => void; }) { return (

{icon} {title}

{loading ? (
Loading…
) : items.length === 0 ? (
No {title.toLowerCase()} configured yet.
) : ( Name Description Amount Billable Active {items.map((i) => ( {i.name} {i.description || "—"} {formatCurrency(Number(i.amount))} {unit && ( {unit} )} {i.billable ? ( Billable ) : ( No )} {i.active ? ( Active ) : ( Inactive )} ))}
)}
); } function FeeEditDialog({ item, onClose, onSaved, }: { item: Partial | null; onClose: () => void; onSaved: () => void; }) { const { user } = useAuth(); const [form, setForm] = useState>({}); const [saving, setSaving] = useState(false); useEffect(() => { if (item) setForm(item); }, [item]); const submit = async () => { if (!form.name?.trim()) return toast.error("Name required"); if (!form.category) return toast.error("Category required"); const payload: any = { name: form.name.trim(), description: form.description?.trim() || null, category: form.category, amount: Number(form.amount) || 0, billable: form.billable ?? true, active: form.active ?? true, sort_order: form.sort_order ?? 0, }; setSaving(true); const { error } = form.id ? await supabase .from("fee_schedule_items") .update(payload) .eq("id", form.id) : await supabase .from("fee_schedule_items") .insert({ ...payload, created_by: user?.id }); setSaving(false); if (error) { toast.error("Could not save", { description: error.message }); return; } toast.success(form.id ? "Fee updated" : "Fee added"); onClose(); onSaved(); }; return ( !v && onClose()}> {form.id ? "Edit fee item" : "Add fee item"} Time fees feed the hourly rate dropdown; expense fees feed the expense amount picker.
setForm({ ...form, amount: parseFloat(e.target.value) || 0 }) } />
setForm({ ...form, name: e.target.value })} placeholder={ form.category === "expense" ? "e.g. Filing fee" : "e.g. Senior partner rate" } />