import { useEffect, useMemo, useState } from "react"; import { Alert, Modal, Pressable, ScrollView, StyleSheet, Text, View, } from "react-native"; import { FilterChip } from "@/components/FilterChip"; import { Button } from "@/components/ui/Button"; import { DateTimeField } from "@/components/ui/DateTimeField"; import { Input } from "@/components/ui/Input"; import { fonts, spacing } from "@/constants/theme"; import { useAppTheme } from "@/contexts/ThemeContext"; import { formatCurrency } from "@/lib/format"; import { parseNonNegativeNumber } from "@/lib/form-validation"; import type { ThemeColors } from "@/lib/theme-palette"; import { useThemedStyles } from "@/lib/use-themed-styles"; import { api } from "@/lib/trpc"; type TimeEntryEditSheetProps = { entryId: string | null; visible: boolean; onClose: () => void; }; export function TimeEntryEditSheet({ entryId, visible, onClose }: TimeEntryEditSheetProps) { const { colors } = useAppTheme(); const styles = useThemedStyles(createStyles); const utils = api.useUtils(); const entryQuery = api.timeEntries.getById.useQuery( { id: entryId ?? "" }, { enabled: visible && Boolean(entryId) }, ); const clientsQuery = api.clients.getAll.useQuery(undefined, { enabled: visible }); const [description, setDescription] = useState(""); const [clientId, setClientId] = useState(""); const [invoiceId, setInvoiceId] = useState(""); const [rateText, setRateText] = useState(""); const [startedAt, setStartedAt] = useState(() => new Date()); const [endedAt, setEndedAt] = useState(() => new Date()); const billableQuery = api.invoices.getBillable.useQuery( clientId ? { clientId } : undefined, { enabled: visible && Boolean(clientId) }, ); useEffect(() => { const entry = entryQuery.data; if (!entry) return; setDescription(entry.description ?? ""); setClientId(entry.clientId ?? ""); setInvoiceId(entry.invoiceId ?? ""); setRateText(entry.rate != null ? String(entry.rate) : ""); setStartedAt(new Date(entry.startedAt)); setEndedAt(entry.endedAt ? new Date(entry.endedAt) : new Date()); }, [entryQuery.data]); const hoursPreview = useMemo(() => { if (endedAt <= startedAt) return null; return Math.max(0, (endedAt.getTime() - startedAt.getTime()) / 3_600_000); }, [endedAt, startedAt]); const rate = parseNonNegativeNumber(rateText); const updateEntry = api.timeEntries.update.useMutation({ onSuccess: async () => { await Promise.all([ utils.timeEntries.getAll.invalidate(), utils.timeEntries.getById.invalidate(), utils.invoices.getAll.invalidate(), utils.dashboard.getStats.invalidate(), ]); onClose(); }, onError: (err) => Alert.alert("Could not save", err.message), }); const deleteEntry = api.timeEntries.delete.useMutation({ onSuccess: async () => { await Promise.all([ utils.timeEntries.getAll.invalidate(), utils.invoices.getAll.invalidate(), utils.dashboard.getStats.invalidate(), ]); onClose(); }, onError: (err) => Alert.alert("Could not delete", err.message), }); function handleSave() { if (!entryId) return; if (endedAt <= startedAt) { Alert.alert("Invalid times", "End time must be after start time."); return; } updateEntry.mutate({ id: entryId, description, clientId: clientId || "", invoiceId: invoiceId || "", rate: rate ?? undefined, startedAt, endedAt, hours: hoursPreview ?? undefined, }); } function confirmDelete() { if (!entryId) return; Alert.alert("Delete time entry?", "This removes the entry and any linked invoice line.", [ { text: "Cancel", style: "cancel" }, { text: "Delete", style: "destructive", onPress: () => deleteEntry.mutate({ id: entryId }), }, ]); } return ( Edit time entry Close {entryQuery.isLoading ? ( Loading… ) : ( <> Client { setClientId(""); setInvoiceId(""); }} /> {(clientsQuery.data ?? []).map((client) => ( { setClientId(client.id); setInvoiceId(""); }} /> ))} {clientId ? ( <> Invoice setInvoiceId("")} /> {(billableQuery.data ?? []).map((invoice) => ( setInvoiceId(invoice.id)} /> ))} ) : null} {hoursPreview != null ? ( {hoursPreview.toFixed(2)}h {rate != null && rate > 0 ? ` · ${formatCurrency(hoursPreview * rate)}` : ""} ) : null}