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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user