feat(03-03): add Dashboard page with filterable documents table
- Async server component queries all documents with LEFT JOIN to clients - Filter state lives in URL (?status=Draft|Sent|Viewed|Signed) via DashboardFilters client component - Rows filtered in JavaScript after fetch (tiny dataset in Phase 3) - DashboardFilters extracted to _components/DashboardFilters.tsx (use client + useRouter) - Displays agent first name extracted from session email
This commit is contained in:
@@ -0,0 +1,55 @@
|
|||||||
|
import { auth } from "@/lib/auth";
|
||||||
|
import { db } from "@/lib/db";
|
||||||
|
import { documents, clients } from "@/lib/db/schema";
|
||||||
|
import { eq, desc } from "drizzle-orm";
|
||||||
|
import { DocumentsTable } from "../../_components/DocumentsTable";
|
||||||
|
import { DashboardFilters } from "../../_components/DashboardFilters";
|
||||||
|
|
||||||
|
const VALID_STATUSES = ["Draft", "Sent", "Viewed", "Signed"] as const;
|
||||||
|
type ValidStatus = (typeof VALID_STATUSES)[number];
|
||||||
|
|
||||||
|
export default async function DashboardPage({
|
||||||
|
searchParams,
|
||||||
|
}: {
|
||||||
|
searchParams: Promise<{ status?: string }>;
|
||||||
|
}) {
|
||||||
|
const session = await auth();
|
||||||
|
const { status } = await searchParams;
|
||||||
|
|
||||||
|
const allRows = await db
|
||||||
|
.select({
|
||||||
|
id: documents.id,
|
||||||
|
name: documents.name,
|
||||||
|
status: documents.status,
|
||||||
|
sentAt: documents.sentAt,
|
||||||
|
clientName: clients.name,
|
||||||
|
clientId: documents.clientId,
|
||||||
|
})
|
||||||
|
.from(documents)
|
||||||
|
.leftJoin(clients, eq(documents.clientId, clients.id))
|
||||||
|
.orderBy(desc(documents.createdAt));
|
||||||
|
|
||||||
|
const filteredRows =
|
||||||
|
status && (VALID_STATUSES as readonly string[]).includes(status)
|
||||||
|
? allRows.filter((r) => r.status === (status as ValidStatus))
|
||||||
|
: allRows;
|
||||||
|
|
||||||
|
const firstName = session?.user?.email?.split("@")[0] ?? "Agent";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<h1 className="text-[var(--navy)] text-2xl font-semibold">
|
||||||
|
Welcome back, {firstName}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-4 mb-6">
|
||||||
|
<span className="text-sm text-gray-600">Filter by status:</span>
|
||||||
|
<DashboardFilters currentStatus={status} />
|
||||||
|
</div>
|
||||||
|
<div className="bg-white rounded-xl shadow-sm overflow-hidden">
|
||||||
|
<DocumentsTable rows={filteredRows} showClientColumn={true} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
|
export function DashboardFilters({ currentStatus }: { currentStatus?: string }) {
|
||||||
|
const router = useRouter();
|
||||||
|
return (
|
||||||
|
<select
|
||||||
|
value={currentStatus ?? ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
router.push(
|
||||||
|
e.target.value
|
||||||
|
? `?status=${e.target.value}`
|
||||||
|
: "/portal/dashboard"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className="border border-gray-200 rounded-lg px-3 py-1.5 text-sm"
|
||||||
|
>
|
||||||
|
<option value="">All statuses</option>
|
||||||
|
<option value="Draft">Draft</option>
|
||||||
|
<option value="Sent">Sent</option>
|
||||||
|
<option value="Viewed">Viewed</option>
|
||||||
|
<option value="Signed">Signed</option>
|
||||||
|
</select>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user