diff --git a/src/components/timer/header-timer.tsx b/src/components/timer/header-timer.tsx index 558837d..35c80d5 100644 --- a/src/components/timer/header-timer.tsx +++ b/src/components/timer/header-timer.tsx @@ -39,6 +39,7 @@ interface FeeItem { name: string; description: string | null; amount: number; + pricing_type: "hourly" | "flat"; } export function HeaderTimer() { @@ -67,7 +68,7 @@ export function HeaderTimer() { supabase.from("profiles").select("hourly_rate").eq("id", user.id).maybeSingle(), supabase .from("fee_schedule_items") - .select("id, name, description, amount") + .select("id, name, description, amount, pricing_type") .eq("active", true) .eq("billable", true) .eq("category", "time") @@ -96,6 +97,8 @@ export function HeaderTimer() { [feeItems, feeItemId], ); + const isFlatFee = selectedFee?.pricing_type === "flat"; + const effectiveRate = selectedFee?.amount ?? activeCase?.default_hourly_rate ?? profileRate ?? 0; @@ -111,15 +114,18 @@ export function HeaderTimer() { if (!user?.id) return toast.error("Not signed in"); if (!timer.caseId) return toast.error("Select a case before saving"); if (!timer.description.trim()) return toast.error("Description required"); - if (elapsedMs < 1000) return toast.error("Timer hasn't recorded any time yet"); + if (elapsedMs < 1000 && !isFlatFee) return toast.error("Timer hasn't recorded any time yet"); setSaving(true); - const { hours } = consume(); + const { hours: tracked } = consume(); + // For a flat-fee item, store 1 hour at the flat amount so the total = flat amount. + const hours = isFlatFee ? 1 : tracked; + const rate = isFlatFee ? selectedFee!.amount : effectiveRate; const { error } = await supabase.from("time_entries").insert({ case_id: timer.caseId, user_id: user.id, work_date: new Date().toISOString().slice(0, 10), hours, - hourly_rate: effectiveRate, + hourly_rate: rate, description: timer.description.trim(), billable, }); @@ -128,9 +134,14 @@ export function HeaderTimer() { toast.error(error.message); return; } - toast.success(`Saved ${hours.toFixed(1)} hr`, { - description: `${formatCurrency(hours * effectiveRate)} at ${formatCurrency(effectiveRate)}/hr`, - }); + toast.success( + isFlatFee ? `Saved flat fee` : `Saved ${hours.toFixed(1)} hr`, + { + description: isFlatFee + ? formatCurrency(rate) + : `${formatCurrency(hours * rate)} at ${formatCurrency(rate)}/hr`, + }, + ); setBillable(true); setFeeItemId(""); setOpen(false); @@ -247,7 +258,10 @@ export function HeaderTimer() { {feeItems.map((f) => ( {f.name} - {formatCurrency(f.amount)}/hr + + {formatCurrency(f.amount)} + {f.pricing_type === "flat" ? " flat" : "/hr"} + ))} @@ -268,9 +282,15 @@ export function HeaderTimer() {
-
Rate
+
+ {isFlatFee ? "Flat fee" : "Rate"} +
- {effectiveRate ? `${formatCurrency(effectiveRate)}/hr` : "—"} + {effectiveRate + ? isFlatFee + ? formatCurrency(effectiveRate) + : `${formatCurrency(effectiveRate)}/hr` + : "—"}
{selectedFee @@ -289,17 +309,24 @@ export function HeaderTimer() {

- Time saves rounded up to the nearest 1/10 hour (6 minutes). + {isFlatFee + ? "Flat fee items save at the fixed amount regardless of elapsed time." + : "Time saves rounded up to the nearest 1/10 hour (6 minutes)."}

diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts index 7246c52..321fb7b 100644 --- a/src/integrations/supabase/types.ts +++ b/src/integrations/supabase/types.ts @@ -1490,6 +1490,7 @@ export type Database = { description: string | null id: string name: string + pricing_type: string sort_order: number updated_at: string } @@ -1503,6 +1504,7 @@ export type Database = { description?: string | null id?: string name: string + pricing_type?: string sort_order?: number updated_at?: string } @@ -1516,6 +1518,7 @@ export type Database = { description?: string | null id?: string name?: string + pricing_type?: string sort_order?: number updated_at?: string } diff --git a/src/routes/settings.fees.tsx b/src/routes/settings.fees.tsx index abb5560..b31aee8 100644 --- a/src/routes/settings.fees.tsx +++ b/src/routes/settings.fees.tsx @@ -45,6 +45,7 @@ interface FeeItem { name: string; description: string | null; category: "time" | "expense"; + pricing_type: "hourly" | "flat"; amount: number; billable: boolean; active: boolean; @@ -98,7 +99,7 @@ function FeeSchedulePage() { logging time; expense fees auto-fill the amount when adding an expense. The billable flag becomes the default for new entries.

-
@@ -202,11 +203,13 @@ function FeeSection({ {formatCurrency(Number(i.amount))} - {unit && ( - - {unit} - - )} + {(() => { + const isHourly = i.category === "time" && i.pricing_type !== "flat"; + const u = isHourly ? "/ hr" : i.category === "time" ? " flat" : unit; + return u ? ( + {u} + ) : null; + })()} {i.billable ? ( @@ -279,6 +282,8 @@ function FeeEditDialog({ name: form.name.trim(), description: form.description?.trim() || null, category: form.category, + pricing_type: + form.category === "expense" ? "flat" : form.pricing_type ?? "hourly", amount: Number(form.amount) || 0, billable: form.billable ?? true, active: form.active ?? true, @@ -316,27 +321,56 @@ function FeeEditDialog({
-
+
+
+
+ +