Added case rate edit & wrap

X-Lovable-Edit-ID: edt-15e65299-9d83-43b6-af10-cd19014af5ad
Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-04-19 20:08:03 +00:00
co-authored by renee-png
2 changed files with 84 additions and 4 deletions
+81 -1
View File
@@ -89,6 +89,23 @@ function CaseDetail() {
else { toast.success("Assignee updated"); load(); }
};
const updateRate = async (rateInput: string) => {
const trimmed = rateInput.trim();
const next = trimmed === "" ? null : Number(trimmed);
if (next !== null && (!Number.isFinite(next) || next < 0)) {
toast.error("Enter a valid rate (0 or higher)");
return false;
}
const { error } = await supabase
.from("cases")
.update({ default_hourly_rate: next })
.eq("id", caseId);
if (error) { toast.error(error.message); return false; }
toast.success(next === null ? "Case rate cleared" : `Case rate set to $${next.toFixed(2)}/hr`);
load();
return true;
};
const filteredClients = useMemo(() => {
const q = clientSearch.trim().toLowerCase();
if (!q) return clients.slice(0, 50);
@@ -275,7 +292,11 @@ function CaseDetail() {
/>
<div className="mt-8 grid grid-cols-2 sm:grid-cols-4 gap-3 text-center text-xs">
<SmallStat label="Default rate" value={data.default_hourly_rate ? formatCurrency(data.default_hourly_rate) + "/hr" : "—"} />
{canManage ? (
<EditableRateStat value={data.default_hourly_rate} onSave={updateRate} />
) : (
<SmallStat label="Case rate" value={data.default_hourly_rate ? formatCurrency(data.default_hourly_rate) + "/hr" : "—"} />
)}
<SmallStat label="Attorney" value={data.assignee?.full_name || data.assignee?.email || "—"} />
<SmallStat label="Closed" value={data.closed_at ? formatDate(data.closed_at) : "—"} />
<SmallStat label="Updated" value={formatDate(data.updated_at)} />
@@ -331,3 +352,62 @@ function SmallStat({ label, value }: { label: string; value: string }) {
</div>
);
}
function EditableRateStat({
value,
onSave,
}: {
value: number | null;
onSave: (raw: string) => Promise<boolean>;
}) {
const [editing, setEditing] = useState(false);
const [raw, setRaw] = useState(value != null ? String(value) : "");
const [saving, setSaving] = useState(false);
useEffect(() => {
setRaw(value != null ? String(value) : "");
}, [value]);
const commit = async () => {
setSaving(true);
const ok = await onSave(raw);
setSaving(false);
if (ok) setEditing(false);
};
return (
<div className="border rounded-md py-2 px-3 text-left">
<div className="text-[10px] uppercase tracking-wider text-muted-foreground text-center">Case rate</div>
{editing ? (
<div className="flex items-center gap-1 mt-1">
<span className="text-sm text-muted-foreground">$</span>
<Input
type="number"
step="0.01"
min="0"
value={raw}
placeholder="0.00"
onChange={(e) => setRaw(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") commit();
if (e.key === "Escape") { setRaw(value != null ? String(value) : ""); setEditing(false); }
}}
autoFocus
className="h-7 px-2 text-sm"
/>
<span className="text-xs text-muted-foreground">/hr</span>
<Button size="sm" variant="ghost" className="h-7 px-2 text-xs" disabled={saving} onClick={commit}>Save</Button>
</div>
) : (
<button
type="button"
onClick={() => setEditing(true)}
className="text-sm font-medium mt-0.5 truncate hover:text-primary w-full text-center"
title="Click to edit case rate"
>
{value ? formatCurrency(value) + "/hr" : "Set rate…"}
</button>
)}
</div>
);
}
+3 -3
View File
@@ -425,13 +425,13 @@ function InvoiceDetail() {
{sub.items.map((it) => (
<tr key={it.id} className={`border-t ${!sub.billable ? "text-muted-foreground" : ""}`}>
<td className="px-3 py-2 text-xs">{it.work_date ? formatDate(it.work_date) : "—"}</td>
<td className="px-3 py-2">
<td className="px-3 py-2 align-top">
{canEdit && isDraft ? (
<Input className="h-8" defaultValue={it.description}
onBlur={(e) => e.target.value !== it.description && updateItem(it.id, { description: e.target.value })} />
) : (
<div>
<div className={!sub.billable ? "italic" : ""}>{it.description}</div>
<div className="min-w-0">
<div className={`whitespace-pre-wrap break-words ${!sub.billable ? "italic" : ""}`}>{it.description}</div>
{it.user?.full_name && <div className="text-[11px] text-muted-foreground italic">{it.user.full_name}</div>}
</div>
)}