Archived
Link clocked time to invoice items with bidirectional sync, add entry editing on web, broaden session cookie detection for Expo clients, and handle API rate limits without signing users out. Co-authored-by: Cursor <cursoragent@cursor.com>
95 lines
2.8 KiB
TypeScript
95 lines
2.8 KiB
TypeScript
"use client";
|
|
|
|
import Link from "next/link";
|
|
import { useMemo, useState } from "react";
|
|
import { api } from "~/trpc/react";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
|
|
import { Button } from "~/components/ui/button";
|
|
import { EmptyState } from "~/components/layout/page-layout";
|
|
import { Clock, Play } from "lucide-react";
|
|
import { groupEntriesByDate } from "~/lib/time-entry-display";
|
|
import { TimeEntryRow } from "~/components/time-clock/time-entry-list";
|
|
import { TimeEntryEditDialog } from "~/components/time-clock/time-entry-edit-dialog";
|
|
import type { TimeEntryListItem } from "~/lib/time-entry-display";
|
|
|
|
export function TimeEntriesHistory() {
|
|
const { data: entries, isLoading } = api.timeEntries.getAll.useQuery();
|
|
const [editEntryId, setEditEntryId] = useState<string | null>(null);
|
|
|
|
const completedEntries = useMemo(
|
|
() => (entries ?? []).filter((e) => e.endedAt),
|
|
[entries],
|
|
);
|
|
|
|
const grouped = useMemo(
|
|
() => groupEntriesByDate(completedEntries),
|
|
[completedEntries],
|
|
);
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<Card>
|
|
<CardContent className="text-muted-foreground p-6 text-sm">
|
|
Loading entries…
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
if (completedEntries.length === 0) {
|
|
return (
|
|
<Card>
|
|
<CardContent>
|
|
<EmptyState
|
|
icon={<Clock className="h-6 w-6" />}
|
|
title="No time entries yet"
|
|
description="Start the timer to track billable hours. Completed entries will show up here."
|
|
action={
|
|
<Button asChild>
|
|
<Link href="/dashboard/time-clock">
|
|
<Play className="mr-2 h-4 w-4" />
|
|
Start timer
|
|
</Link>
|
|
</Button>
|
|
}
|
|
className="py-16"
|
|
/>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<div className="space-y-6">
|
|
{grouped.map((group) => (
|
|
<Card key={group.dateKey}>
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="text-muted-foreground text-sm font-medium">
|
|
{group.label}
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{group.entries.map((entry, index) => (
|
|
<TimeEntryRow
|
|
key={entry.id}
|
|
entry={entry}
|
|
isLast={index === group.entries.length - 1}
|
|
onEdit={(item: TimeEntryListItem) => setEditEntryId(item.id)}
|
|
/>
|
|
))}
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
<TimeEntryEditDialog
|
|
entryId={editEntryId}
|
|
open={editEntryId != null}
|
|
onOpenChange={(open) => {
|
|
if (!open) setEditEntryId(null);
|
|
}}
|
|
/>
|
|
</>
|
|
);
|
|
}
|