Added fee item picker to timer

X-Lovable-Edit-ID: edt-ebd0a9ba-15cd-4164-a4fc-ea959ea9e815
Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-04-19 15:45:09 +00:00
co-authored by renee-png
+66 -3
View File
@@ -34,6 +34,13 @@ interface CaseOption {
default_hourly_rate: number | null;
}
interface FeeItem {
id: string;
name: string;
description: string | null;
amount: number;
}
export function HeaderTimer() {
const { user } = useAuth();
const { timer, elapsedMs, isRunning, hasStarted, setClient, setCase, setDescription, start, pause, resume, reset, consume } = useTimer();
@@ -44,22 +51,33 @@ export function HeaderTimer() {
const [profileRate, setProfileRate] = useState<number | null>(null);
const [saving, setSaving] = useState(false);
const [billable, setBillable] = useState(true);
const [feeItems, setFeeItems] = useState<FeeItem[]>([]);
const [feeItemId, setFeeItemId] = useState<string>("");
// Load clients + cases the user can access (RLS handles filtering)
useEffect(() => {
if (!user?.id) return;
(async () => {
const [{ data: cs }, { data: ks }, { data: prof }] = await Promise.all([
const [{ data: cs }, { data: ks }, { data: prof }, { data: fees }] = await Promise.all([
supabase.from("clients").select("id, name").order("name"),
supabase
.from("cases")
.select("id, case_number, title, client_id, default_hourly_rate")
.order("case_number", { ascending: false }),
supabase.from("profiles").select("hourly_rate").eq("id", user.id).maybeSingle(),
supabase
.from("fee_schedule_items")
.select("id, name, description, amount")
.eq("active", true)
.eq("billable", true)
.eq("category", "time")
.order("sort_order")
.order("name"),
]);
setClients(cs ?? []);
setCases(ks ?? []);
setProfileRate((prof as any)?.hourly_rate ?? null);
setFeeItems((fees ?? []) as FeeItem[]);
})();
}, [user?.id]);
@@ -73,7 +91,21 @@ export function HeaderTimer() {
[cases, timer.caseId],
);
const effectiveRate = activeCase?.default_hourly_rate ?? profileRate ?? 0;
const selectedFee = useMemo(
() => feeItems.find((f) => f.id === feeItemId) ?? null,
[feeItems, feeItemId],
);
const effectiveRate =
selectedFee?.amount ?? activeCase?.default_hourly_rate ?? profileRate ?? 0;
const handleSelectFee = (id: string) => {
setFeeItemId(id);
const fee = feeItems.find((f) => f.id === id);
if (fee && !timer.description.trim()) {
setDescription(fee.description?.trim() || fee.name);
}
};
const handleSave = async () => {
if (!user?.id) return toast.error("Not signed in");
@@ -100,6 +132,7 @@ export function HeaderTimer() {
description: `${formatCurrency(hours * effectiveRate)} at ${formatCurrency(effectiveRate)}/hr`,
});
setBillable(true);
setFeeItemId("");
setOpen(false);
};
@@ -107,6 +140,7 @@ export function HeaderTimer() {
if (hasStarted && !confirm("Discard the running timer?")) return;
reset();
setBillable(true);
setFeeItemId("");
};
const dotClass = isRunning
@@ -198,6 +232,29 @@ export function HeaderTimer() {
</Select>
</div>
{feeItems.length > 0 && (
<div className="space-y-1.5">
<Label className="text-xs">Fee item (optional)</Label>
<Select
value={feeItemId || "__none"}
onValueChange={(v) => handleSelectFee(v === "__none" ? "" : v)}
>
<SelectTrigger>
<SelectValue placeholder="No fee item" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__none">No fee item</SelectItem>
{feeItems.map((f) => (
<SelectItem key={f.id} value={f.id}>
<span className="mr-2">{f.name}</span>
<span className="text-muted-foreground">{formatCurrency(f.amount)}/hr</span>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
<div className="space-y-1.5">
<Label className="text-xs">Description</Label>
<Textarea
@@ -216,7 +273,13 @@ export function HeaderTimer() {
{effectiveRate ? `${formatCurrency(effectiveRate)}/hr` : "—"}
</div>
<div className="text-[10px] text-muted-foreground">
{activeCase?.default_hourly_rate ? "From case" : profileRate ? "Your default" : "Set rate in profile"}
{selectedFee
? `Fee: ${selectedFee.name}`
: activeCase?.default_hourly_rate
? "From case"
: profileRate
? "Your default"
: "Set rate in profile"}
</div>
</div>
<label className="flex items-end gap-2 text-sm pb-1">