import { useState, useMemo, useRef } from "react" import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query" import { api } from "../lib/api" import { EmptyState } from "../components/EmptyState" import { LoadingSpinner } from "../components/LoadingSpinner" import { SuggestionInput } from "../components/SuggestionInput" import type { TimeEntryInsert, SavedView } from "@emberclone/shared" function renderSimpleMarkdown(text: string | null) { if (!text) return null return text .split('\n') .map((line, i) => (
{line .replace(/\*\*(.*?)\*\*/g, '$1') .replace(/\*(.*?)\*/g, '$1')}
)) } export default function TimeEntries() { const queryClient = useQueryClient() const fileInputRef = useRef(null) const [formData, setFormData] = useState({ description: "", startTime: "", endTime: "", projectId: "", notes: "" }) const [filters, setFilters] = useState({ search: "", from: "", to: "" }) const [selectedIds, setSelectedIds] = useState([]) const [expandedRows, setExpandedRows] = useState>({}) const { data: entries, isLoading, isError } = useQuery({ queryKey: ["time-entries", filters.from, filters.to], queryFn: () => api.listTimeEntries({ from: filters.from || undefined, to: filters.to || undefined }) }) const { data: savedViews } = useQuery({ queryKey: ["saved-views", "time-entries"], queryFn: () => api.listSavedViews({ entityType: 'time-entries' }) }) const descriptionSuggestions = useMemo(() => { if (!entries) return [] return Array.from(new Set(entries.map(e => e.description).filter(Boolean))).slice(0, 50) as string[] }, [entries]) const createMutation = useMutation({ mutationFn: (data: Partial) => api.createTimeEntry(data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["time-entries"] }) setFormData({ description: "", startTime: "", endTime: "", projectId: "", notes: "" }) } }) const deleteMutation = useMutation({ mutationFn: (id: string) => api.deleteTimeEntry(id), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["time-entries"] }) } }) const bulkDeleteMutation = useMutation({ mutationFn: (ids: string[]) => api.bulkDeleteTimeEntries(ids), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["time-entries"] }) setSelectedIds([]) } }) const importMutation = useMutation({ mutationFn: (file: File) => api.importTimeEntriesCsv(file), onSuccess: (data) => { alert(`Successfully imported ${data.count} entries.`) queryClient.invalidateQueries({ queryKey: ["time-entries"] }) }, onError: (error: any) => { alert(`Import failed: ${error.message || "Unknown error"}`) } }) const saveViewMutation = useMutation({ mutationFn: (view: { name: string, filters: any }) => api.createSavedView(view), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["saved-views", "time-entries"] }) } }) const deleteViewMutation = useMutation({ mutationFn: (id: string) => api.deleteSavedView(id), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["saved-views", "time-entries"] }) } }) const filteredEntries = useMemo(() => { if (!entries) return [] return entries.filter(entry => entry.description?.toLowerCase().includes(filters.search.toLowerCase()) ) }, [entries, filters.search]) const handleSubmit = (e: React.FormEvent) => { e.preventDefault() createMutation.mutate({ description: formData.description, startTime: new Date(formData.startTime) as any, endTime: new Date(formData.endTime) as any, projectId: formData.projectId || undefined, notes: formData.notes || undefined }) } const handleExport = () => { const params = new URLSearchParams() if (filters.from) params.append("from", filters.from) if (filters.to) params.append("to", filters.to) window.location.href = `/api/time-entries/export.csv?${params.toString()}` } if (isLoading) return
if (isError) return
Error loading time entries.
return (

Time Entries

e.target.files?.[0] && importMutation.mutate(e.target.files[0])} />
setFormData(prev => ({ ...prev, description: val }))} suggestions={descriptionSuggestions} placeholder="What are you working on?" />
setFormData(prev => ({ ...prev, startTime: e.target.value }))} required />
setFormData(prev => ({ ...prev, endTime: e.target.value }))} required />