"use client"; import { useMemo, useState } from "react"; import { api } from "~/trpc/react"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, } from "~/components/ui/dialog"; import { Button } from "~/components/ui/button"; import { Input } from "~/components/ui/input"; import { Label } from "~/components/ui/label"; import { NumberInput } from "~/components/ui/number-input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "~/components/ui/select"; import { toast } from "sonner"; import { invoiceLabel } from "~/lib/time-entry-display"; import type { RouterOutputs } from "~/trpc/react"; import { toLocalDateTimeInputValue } from "@beenvoice/domain/time-zone"; type TimeEntry = RouterOutputs["timeEntries"]["getById"]; export type TimeEntryEditDialogProps = { entryId: string | null; open: boolean; onOpenChange: (open: boolean) => void; }; type TimeEntryEditFormProps = { entry: TimeEntry; entryId: string; clients: RouterOutputs["clients"]["getAll"]; onClose: () => void; }; function TimeEntryEditForm({ entry, entryId, clients, onClose, }: TimeEntryEditFormProps) { const utils = api.useUtils(); const [description, setDescription] = useState(entry.description ?? ""); const [clientId, setClientId] = useState(entry.clientId ?? ""); const [invoiceId, setInvoiceId] = useState(entry.invoiceId ?? ""); const [rate, setRate] = useState(entry.rate ?? 0); const [startedAt, setStartedAt] = useState(() => toLocalDateTimeInputValue(new Date(entry.startedAt)), ); const [endedAt, setEndedAt] = useState(() => entry.endedAt ? toLocalDateTimeInputValue(new Date(entry.endedAt)) : "", ); const { data: billableInvoices } = api.invoices.getBillable.useQuery( clientId ? { clientId } : undefined, { enabled: Boolean(clientId) }, ); const hoursPreview = useMemo(() => { if (!startedAt || !endedAt) return null; const start = new Date(startedAt); const end = new Date(endedAt); if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime())) return null; return Math.max(0, (end.getTime() - start.getTime()) / 3_600_000); }, [endedAt, startedAt]); const updateEntry = api.timeEntries.update.useMutation({ onSuccess: async () => { toast.success("Time entry updated"); await Promise.all([ utils.timeEntries.getAll.invalidate(), utils.timeEntries.getById.invalidate(), utils.invoices.getAll.invalidate(), utils.dashboard.getStats.invalidate(), ]); onClose(); }, onError: (e) => toast.error(e.message), }); const deleteEntry = api.timeEntries.delete.useMutation({ onSuccess: async () => { toast.success("Time entry deleted"); await Promise.all([ utils.timeEntries.getAll.invalidate(), utils.invoices.getAll.invalidate(), utils.dashboard.getStats.invalidate(), ]); onClose(); }, onError: (e) => toast.error(e.message), }); function handleSave() { const start = new Date(startedAt); const end = endedAt ? new Date(endedAt) : undefined; if (Number.isNaN(start.getTime()) || (end && Number.isNaN(end.getTime()))) { toast.error("Invalid start or end time"); return; } if (end && end <= start) { toast.error("End time must be after start time"); return; } updateEntry.mutate({ id: entryId, description, clientId: clientId || "", invoiceId: invoiceId || "", rate, startedAt: start, endedAt: end, hours: hoursPreview ?? undefined, }); } return ( <>
setDescription(e.target.value)} />
setStartedAt(e.target.value)} />
setEndedAt(e.target.value)} />
{hoursPreview != null ? (

Duration: {hoursPreview.toFixed(2)}h {rate > 0 ? ` · $${(hoursPreview * rate).toFixed(2)}` : ""}

) : null}
); } export function TimeEntryEditDialog({ entryId, open, onOpenChange, }: TimeEntryEditDialogProps) { const entryQuery = api.timeEntries.getById.useQuery( { id: entryId ?? "" }, { enabled: Boolean(entryId) && open }, ); const { data: clients = [] } = api.clients.getAll.useQuery(undefined, { enabled: open, }); return ( Edit time entry {entryQuery.isLoading ? (

Loading…

) : entryQuery.data && entryId ? ( onOpenChange(false)} /> ) : (

Time entry not found.

)}
); }