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:
@@ -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} />;
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user