From ca6085a2b2dec1de37c09819e2b9f720b1174961 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sun, 19 Apr 2026 16:24:49 +0000 Subject: [PATCH 1/7] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/components/cases/time-tab.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/cases/time-tab.tsx b/src/components/cases/time-tab.tsx index 5232bf0..ae88e15 100644 --- a/src/components/cases/time-tab.tsx +++ b/src/components/cases/time-tab.tsx @@ -186,9 +186,14 @@ export function CaseTimeTab({ caseRecord, onInvoice }: CaseTimeTabProps) { setSelectedFeeId(v); const fee = fees.find((f) => f.id === v); if (fee) { + // If the fee item has no rate set (0), fall back to the + // case's default rate or the user's profile hourly rate. + const fallback = + caseRecord.default_hourly_rate ?? profileRate ?? 0; + const rate = Number(fee.amount) > 0 ? fee.amount : fallback; setForm((f) => ({ ...f, - hourly_rate: String(fee.amount), + hourly_rate: rate ? String(rate) : f.hourly_rate, billable: fee.billable, description: fee.description || fee.name, })); From e9112a710941c2148c8a8fc357974e44c3d6a1f9 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sun, 19 Apr 2026 16:25:02 +0000 Subject: [PATCH 2/7] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/components/cases/time-tab.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/cases/time-tab.tsx b/src/components/cases/time-tab.tsx index ae88e15..5623590 100644 --- a/src/components/cases/time-tab.tsx +++ b/src/components/cases/time-tab.tsx @@ -206,7 +206,10 @@ export function CaseTimeTab({ caseRecord, onInvoice }: CaseTimeTabProps) { {fees.map((f) => ( - {f.name} — {formatCurrency(Number(f.amount))}/hr + {f.name} + {Number(f.amount) > 0 + ? ` — ${formatCurrency(Number(f.amount))}/hr` + : " — uses your default rate"} ))} From 1c377ed6344cf7dd0e62cea55ec0257493dff93f Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sun, 19 Apr 2026 16:25:24 +0000 Subject: [PATCH 3/7] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/components/timer/header-timer.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/timer/header-timer.tsx b/src/components/timer/header-timer.tsx index 9ef5718..98031ba 100644 --- a/src/components/timer/header-timer.tsx +++ b/src/components/timer/header-timer.tsx @@ -101,8 +101,11 @@ export function HeaderTimer() { const isFlatFee = selectedFee?.pricing_type === "flat"; + // If a fee item has no rate (0), fall back to the case rate then the user's + // profile rate. Flat-fee items always use their own amount. + const feeRate = selectedFee && Number(selectedFee.amount) > 0 ? selectedFee.amount : null; const effectiveRate = - selectedFee?.amount ?? activeCase?.default_hourly_rate ?? profileRate ?? 0; + feeRate ?? activeCase?.default_hourly_rate ?? profileRate ?? 0; const handleSelectFee = (id: string) => { setFeeItemId(id); From d1a9c7d9786b57bae57189bba69a66e5c3028b03 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sun, 19 Apr 2026 16:25:42 +0000 Subject: [PATCH 4/7] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/components/quick-add/quick-add.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/components/quick-add/quick-add.tsx b/src/components/quick-add/quick-add.tsx index d4bb8ce..d65b38e 100644 --- a/src/components/quick-add/quick-add.tsx +++ b/src/components/quick-add/quick-add.tsx @@ -220,9 +220,11 @@ export function QuickAddTime() { setFeeId(v); const fee = fees.find((f) => f.id === v); if (fee) { + const fallback = activeCase?.default_hourly_rate ?? profileRate ?? 0; + const rate = Number(fee.amount) > 0 ? fee.amount : fallback; setForm((f) => ({ ...f, - rate: String(fee.amount), + rate: rate ? String(rate) : f.rate, billable: fee.billable, description: f.description || fee.name, })); @@ -233,7 +235,10 @@ export function QuickAddTime() { {fees.map((f) => ( - {f.name} — ${Number(f.amount).toFixed(2)}/hr + {f.name} + {Number(f.amount) > 0 + ? ` — $${Number(f.amount).toFixed(2)}/hr` + : " — uses your default rate"} ))} From 578697640547efe7047f63727460cf75d566a25d Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sun, 19 Apr 2026 16:26:06 +0000 Subject: [PATCH 5/7] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/routes/settings.fees.tsx | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/routes/settings.fees.tsx b/src/routes/settings.fees.tsx index b31aee8..5900f38 100644 --- a/src/routes/settings.fees.tsx +++ b/src/routes/settings.fees.tsx @@ -58,6 +58,7 @@ function FeeSchedulePage() { const [loading, setLoading] = useState(true); const [editing, setEditing] = useState | null>(null); const [importing, setImporting] = useState<"time" | "expense" | null>(null); + const [bulk, setBulk] = useState<{ category: "time" | "expense"; ids: string[] } | null>(null); const load = async () => { setLoading(true); @@ -97,7 +98,9 @@ function FeeSchedulePage() {

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. + expense. The billable flag becomes the default for new entries. If a + time item's rate is left at $0, the user's profile hourly rate is used + when logging time.

+
+ {selected.size > 0 && ( + <> + + {selected.size} selected + + + + )} + +
@@ -200,6 +248,13 @@ function FeeSection({ + + + Name Description Amount @@ -210,7 +265,14 @@ function FeeSection({ {items.map((i) => ( - + + + toggle(i.id)} + aria-label={`Select ${i.name}`} + /> + {i.name} {i.description || "—"} @@ -272,6 +334,175 @@ function FeeSection({ ); } +function FeeBulkEditDialog({ + bulk, + onClose, + onSaved, +}: { + bulk: { category: "time" | "expense"; ids: string[] } | null; + onClose: () => void; + onSaved: () => void; +}) { + const [billableMode, setBillableMode] = useState<"keep" | "true" | "false">("keep"); + const [activeMode, setActiveMode] = useState<"keep" | "true" | "false">("keep"); + const [pricingMode, setPricingMode] = useState<"keep" | "hourly" | "flat">("keep"); + const [amountMode, setAmountMode] = useState<"keep" | "set" | "clear">("keep"); + const [amount, setAmount] = useState(""); + const [saving, setSaving] = useState(false); + + useEffect(() => { + if (bulk) { + setBillableMode("keep"); + setActiveMode("keep"); + setPricingMode("keep"); + setAmountMode("keep"); + setAmount(""); + } + }, [bulk]); + + if (!bulk) return null; + const isTime = bulk.category === "time"; + const count = bulk.ids.length; + + const submit = async () => { + const payload: Record = {}; + if (billableMode !== "keep") payload.billable = billableMode === "true"; + if (activeMode !== "keep") payload.active = activeMode === "true"; + if (isTime && pricingMode !== "keep") payload.pricing_type = pricingMode; + if (amountMode === "clear") payload.amount = 0; + if (amountMode === "set") { + const n = parseFloat(amount); + if (Number.isNaN(n) || n < 0) { + toast.error("Enter a valid amount"); + return; + } + payload.amount = n; + } + if (Object.keys(payload).length === 0) { + toast.error("Pick at least one field to change"); + return; + } + setSaving(true); + const { error } = await supabase + .from("fee_schedule_items") + .update(payload) + .in("id", bulk.ids); + setSaving(false); + if (error) { + toast.error("Bulk update failed", { description: error.message }); + return; + } + toast.success(`Updated ${count} item${count === 1 ? "" : "s"}`); + onSaved(); + }; + + return ( + !v && onClose()}> + + + + Bulk edit {count} {isTime ? "time" : "expense"} item{count === 1 ? "" : "s"} + + + Only fields set to a new value are applied — others are left + unchanged. {isTime && "Setting an amount of $0 makes time entries fall back to the user's profile hourly rate."} + + + +
+
+
+ + +
+
+ + +
+
+ + {isTime && ( +
+ + +
+ )} + +
+ +
+ + setAmount(e.target.value)} + className="col-span-2" + /> +
+
+
+ + + + + +
+
+ ); +} + function FeeEditDialog({ item, onClose, From 851c1a489f2199d05ed828d217c27c32aaf6cb8e Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sun, 19 Apr 2026 16:26:59 +0000 Subject: [PATCH 7/7] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/routes/settings.fees.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/settings.fees.tsx b/src/routes/settings.fees.tsx index d37d454..a930c09 100644 --- a/src/routes/settings.fees.tsx +++ b/src/routes/settings.fees.tsx @@ -385,7 +385,7 @@ function FeeBulkEditDialog({ setSaving(true); const { error } = await supabase .from("fee_schedule_items") - .update(payload) + .update(payload as never) .in("id", bulk.ids); setSaving(false); if (error) {