Unify entities navigation, redesign time clock, and add invoice PDF preview.

Combine clients and businesses under entities, polish the web time clock,
and show live invoice PDF preview with tighter line-item editing.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-23 01:08:23 -04:00
parent 0b7ffac4e7
commit 480c50981d
19 changed files with 734 additions and 364 deletions
+1 -1
View File
@@ -50,7 +50,7 @@ export default async function BusinessDetailPage({
variant="gradient"
>
<Button asChild variant="outline" className="shadow-sm">
<Link href="/dashboard/businesses">
<Link href="/dashboard/entities?tab=businesses">
<ArrowLeft className="mr-2 h-4 w-4" />
<span>Back to Businesses</span>
</Link>
@@ -0,0 +1,19 @@
"use client";
import { api } from "~/trpc/react";
import { DataTableSkeleton } from "~/components/data/data-table";
import { BusinessesDataTable } from "./businesses-data-table";
export function BusinessesTable() {
const { data: businesses, isLoading } = api.businesses.getAll.useQuery();
if (isLoading) {
return <DataTableSkeleton columns={7} rows={5} />;
}
if (!businesses) {
return null;
}
return <BusinessesDataTable businesses={businesses} />;
}
+3 -38
View File
@@ -1,40 +1,5 @@
import { Plus } from "lucide-react";
import Link from "next/link";
import { Suspense } from "react";
import { DataTableSkeleton } from "~/components/data/data-table";
import { PageHeader } from "~/components/layout/page-header";
import { Button } from "~/components/ui/button";
import { api, HydrateClient } from "~/trpc/server";
import { BusinessesDataTable } from "./_components/businesses-data-table";
import { redirect } from "next/navigation";
// Businesses Table Component
async function BusinessesTable() {
const businesses = await api.businesses.getAll();
return <BusinessesDataTable businesses={businesses} />;
}
export default async function BusinessesPage() {
return (
<div className="page-enter space-y-8">
<PageHeader
title="Businesses"
description="Manage your businesses and their information"
variant="gradient"
>
<Button asChild variant="default" className="hover-lift shadow-md">
<Link href="/dashboard/businesses/new">
<Plus className="mr-2 h-5 w-5" />
<span>Add Business</span>
</Link>
</Button>
</PageHeader>
<HydrateClient>
<Suspense fallback={<DataTableSkeleton columns={7} rows={5} />}>
<BusinessesTable />
</Suspense>
</HydrateClient>
</div>
);
export default function BusinessesPage() {
redirect("/dashboard/entities?tab=businesses");
}
+1 -1
View File
@@ -64,7 +64,7 @@ export default async function ClientDetailPage({
variant="gradient"
>
<Button asChild variant="outline" className="shadow-sm">
<Link href="/dashboard/clients">
<Link href="/dashboard/entities?tab=clients">
<ArrowLeft className="mr-2 h-4 w-4" />
<span>Back to Clients</span>
</Link>
+3 -27
View File
@@ -1,29 +1,5 @@
import { Plus } from "lucide-react";
import Link from "next/link";
import { PageHeader } from "~/components/layout/page-header";
import { Button } from "~/components/ui/button";
import { HydrateClient } from "~/trpc/server";
import { ClientsTable } from "./_components/clients-table";
import { redirect } from "next/navigation";
export default async function ClientsPage() {
return (
<div className="page-enter space-y-6">
<PageHeader
title="Clients"
description="Manage your clients and their information."
variant="gradient"
>
<Button asChild variant="default" className="hover-lift shadow-md">
<Link href="/dashboard/clients/new">
<Plus className="mr-2 h-5 w-5" />
<span>Add Client</span>
</Link>
</Button>
</PageHeader>
<HydrateClient>
<ClientsTable />
</HydrateClient>
</div>
);
export default function ClientsPage() {
redirect("/dashboard/entities?tab=clients");
}
@@ -0,0 +1,60 @@
"use client";
import { Plus } from "lucide-react";
import Link from "next/link";
import { useRouter, useSearchParams } from "next/navigation";
import { PageHeader } from "~/components/layout/page-header";
import { Button } from "~/components/ui/button";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs";
import { ClientsTable } from "../../clients/_components/clients-table";
import { BusinessesTable } from "../../businesses/_components/businesses-table";
type EntityTab = "clients" | "businesses";
export function EntitiesView({ initialTab }: { initialTab: EntityTab }) {
const router = useRouter();
const searchParams = useSearchParams();
const tab: EntityTab =
searchParams.get("tab") === "businesses" ? "businesses" : initialTab;
function handleTabChange(value: string) {
const next = value === "businesses" ? "businesses" : "clients";
router.replace(`/dashboard/entities?tab=${next}`, { scroll: false });
}
const addHref =
tab === "clients" ? "/dashboard/clients/new" : "/dashboard/businesses/new";
const addLabel = tab === "clients" ? "Add client" : "Add business";
return (
<div className="space-y-6">
<PageHeader
title="Entities"
description="Clients you bill and businesses you send from"
variant="gradient"
>
<Button asChild variant="default" className="hover-lift shadow-md">
<Link href={addHref}>
<Plus className="mr-2 h-5 w-5" />
<span>{addLabel}</span>
</Link>
</Button>
</PageHeader>
<Tabs value={tab} onValueChange={handleTabChange}>
<TabsList className="grid w-full max-w-md grid-cols-2">
<TabsTrigger value="clients">Clients</TabsTrigger>
<TabsTrigger value="businesses">Businesses</TabsTrigger>
</TabsList>
<TabsContent value="clients" className="mt-6">
<ClientsTable />
</TabsContent>
<TabsContent value="businesses" className="mt-6">
<BusinessesTable />
</TabsContent>
</Tabs>
</div>
);
}
+26
View File
@@ -0,0 +1,26 @@
import { Suspense } from "react";
import { DataTableSkeleton } from "~/components/data/data-table";
import { api, HydrateClient } from "~/trpc/server";
import { EntitiesView } from "./_components/entities-view";
export default async function EntitiesPage({
searchParams,
}: {
searchParams: Promise<{ tab?: string }>;
}) {
const params = await searchParams;
const initialTab = params.tab === "businesses" ? "businesses" : "clients";
void api.clients.getAll.prefetch();
void api.businesses.getAll.prefetch();
return (
<div className="page-enter space-y-6">
<HydrateClient>
<Suspense fallback={<DataTableSkeleton columns={5} rows={8} />}>
<EntitiesView initialTab={initialTab} />
</Suspense>
</HydrateClient>
</div>
);
}
+1 -1
View File
@@ -17,7 +17,7 @@ export default async function TimeClockPage({
}
return (
<div className="page-enter mx-auto max-w-3xl space-y-6">
<div className="page-enter space-y-6">
<DashboardPageHeader
title="Time clock"
description="Track billable hours and save them directly to an invoice"