Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-04-29 20:29:51 +00:00
co-authored by renee-png
parent 14e43c576d
commit 5b31a252e4
+124
View File
@@ -62,6 +62,7 @@ export function CaseTimeTab({ caseRecord, onInvoice }: CaseTimeTabProps) {
const [selectedFeeId, setSelectedFeeId] = useState<string>("");
const [showNewFee, setShowNewFee] = useState(false);
const [showImport, setShowImport] = useState(false);
const [editEntry, setEditEntry] = useState<any | null>(null);
useEffect(() => {
(async () => {
@@ -355,6 +356,7 @@ export function CaseTimeTab({ caseRecord, onInvoice }: CaseTimeTabProps) {
</Button>
)}
{!e.invoice_id && <Button variant="ghost" size="icon" onClick={() => del(e.id)}><Trash2 className="h-4 w-4 text-destructive" /></Button>}
{!e.invoice_id && <Button variant="ghost" size="icon" onClick={() => setEditEntry(e)} title="Edit"><Pencil className="h-4 w-4" /></Button>}
</div>
</td>
</tr>
@@ -394,6 +396,14 @@ export function CaseTimeTab({ caseRecord, onInvoice }: CaseTimeTabProps) {
clientId={caseRecord.client_id ?? null}
onImported={load}
/>
<EditTimeEntryDialog
entry={editEntry}
staff={staff}
currentUserId={user?.id ?? ""}
onClose={() => setEditEntry(null)}
onSaved={() => { setEditEntry(null); load(); }}
/>
</div>
);
}
@@ -406,3 +416,117 @@ function Stat({ label, value }: { label: string; value: string }) {
</div>
);
}
function EditTimeEntryDialog({
entry,
staff,
currentUserId,
onClose,
onSaved,
}: {
entry: any | null;
staff: Array<{ id: string; full_name: string | null; email: string | null; hourly_rate: number | null }>;
currentUserId: string;
onClose: () => void;
onSaved: () => void;
}) {
const [form, setForm] = useState({
work_date: "",
user_id: "",
hours: "",
hourly_rate: "",
description: "",
billable: true,
});
const [saving, setSaving] = useState(false);
useEffect(() => {
if (entry) {
setForm({
work_date: entry.work_date ?? "",
user_id: entry.user_id ?? currentUserId,
hours: String(entry.hours ?? ""),
hourly_rate: String(entry.hourly_rate ?? ""),
description: entry.description ?? "",
billable: !!entry.billable,
});
}
}, [entry, currentUserId]);
const save = async () => {
if (!entry) return;
const hours = roundToTenth(parseFloat(form.hours || "0") || 0);
const rate = parseFloat(form.hourly_rate || "0") || 0;
if (hours <= 0) { toast.error("Hours must be greater than 0"); return; }
if (!form.description.trim()) { toast.error("Description required"); return; }
setSaving(true);
const { error } = await supabase
.from("time_entries")
.update({
work_date: form.work_date,
user_id: form.user_id,
hours,
hourly_rate: rate,
description: form.description.trim(),
billable: form.billable,
})
.eq("id", entry.id);
setSaving(false);
if (error) { toast.error(error.message); return; }
toast.success("Time entry updated");
onSaved();
};
return (
<Dialog open={!!entry} onOpenChange={(o) => { if (!o) onClose(); }}>
<DialogContent>
<DialogHeader><DialogTitle>Edit time entry</DialogTitle></DialogHeader>
<div className="space-y-3">
<div className="grid grid-cols-2 gap-3">
<div>
<Label>Date</Label>
<Input type="date" value={form.work_date} onChange={(e) => setForm({ ...form, work_date: e.target.value })} />
</div>
<div>
<Label>Entered by</Label>
<Select value={form.user_id} onValueChange={(v) => setForm({ ...form, user_id: v })}>
<SelectTrigger><SelectValue placeholder="Select staff…" /></SelectTrigger>
<SelectContent>
{staff.map((s) => (
<SelectItem key={s.id} value={s.id}>
{(s.full_name || s.email || "Unknown") + (s.id === currentUserId ? " (me)" : "")}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<Label>Hours (decimal)</Label>
<Input type="number" step="0.1" min="0" value={form.hours} onChange={(e) => setForm({ ...form, hours: e.target.value })} />
</div>
<div>
<Label>Hourly rate ($)</Label>
<Input type="number" step="0.01" min="0" value={form.hourly_rate} onChange={(e) => setForm({ ...form, hourly_rate: e.target.value })} />
</div>
</div>
<div>
<Label>Description</Label>
<Textarea rows={3} value={form.description} onChange={(e) => setForm({ ...form, description: e.target.value })} maxLength={1000} />
</div>
<label className="flex items-center gap-2 text-sm">
<Checkbox checked={form.billable} onCheckedChange={(c) => setForm({ ...form, billable: !!c })} />
Billable
</label>
</div>
<DialogFooter>
<Button variant="outline" onClick={onClose}>Cancel</Button>
<Button onClick={save} disabled={saving}>
{saving && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}Save
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}