All files / src/pages/admin dashboard.tsx

100% Statements 7/7
96.15% Branches 25/26
100% Functions 3/3
100% Lines 7/7

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144                                                        10x 10x 10x   10x   10x                                                                                                                   3x                       16x                                                                              
import { Link } from "@tanstack/react-router";
import { Store } from "lucide-react";
 
import { AdminMetric, AdminMetrics } from "../../components/admin/AdminMetric";
import { AdminBand, AdminPageHead } from "../../components/admin/AdminPageHead";
import { Card } from "../../components/ui/card";
import { EmptyState } from "../../components/ui/empty-state";
import { Skeleton } from "../../components/ui/skeleton";
import { StatusBadge } from "../../components/ui/status-badge";
import { useAdminDashboard } from "../../hooks/queries/useAdminQueries";
 
/**
 * `/admin` — the operator's first screen, in the Job Book idiom.
 *
 * WHAT CHANGED
 * Four stat *cards* became four `.metric` figures: a label, a 2px rule and a
 * tabular number. Boxes are the one thing the Job Book forbids, and a figure
 * buried in the third element of a shadowed card is not a figure leading.
 *
 * The pending-publish tile used to turn warning-coloured — border, ring, icon
 * and number — when it was non-zero. That is colour travelling alone, and the
 * number it coloured is not a state. It now prints a `Waiting` code beside its
 * label, which says the same thing in a word.
 *
 * Recent pros became ruled rows that tint on hover, with the pro's status as a
 * printed code on the right, rather than a card with a 1.125rem title.
 */
export function AdminDashboardPage() {
	const { data, isLoading } = useAdminDashboard();
	const stats = data?.stats ?? null;
	const recentPros = data?.recentPros ?? [];
 
	const pendingPros = stats?.pendingPros ?? 0;
 
	return (
		<div className="space-y-8">
			<AdminPageHead
				title="Admin Dashboard"
				description="Overview of all pros and projects."
			/>
 
			{/* Figures lead. `.metrics` — a label, a rule and a number each. */}
			<AdminMetrics>
				<AdminMetric
					label="Total Pros"
					value={stats?.totalPros ?? "-"}
					note={`${stats?.publishedPros ?? 0} published`}
					href="/admin/pros"
					isLoading={isLoading}
				/>
				<AdminMetric
					label="Pros pending publish"
					value={pendingPros}
					note={
						pendingPros === 0
							? "No pros waiting for approval"
							: "Set status to Published to approve"
					}
					// State is a printed code, never a tinted number.
					code={
						pendingPros > 0 ? (
							<StatusBadge status="Waiting" variant="warning" />
						) : undefined
					}
					href="/admin/pros?status=draft"
					isLoading={isLoading}
				/>
				<AdminMetric
					label="Total Projects"
					value={stats?.totalProjects ?? "-"}
					note={`${stats?.publishedProjects ?? 0} published`}
					href="/admin/projects"
					isLoading={isLoading}
				/>
				<AdminMetric
					label="Conversion Rate"
					value="-"
					note="Coming soon"
					isLoading={isLoading}
				/>
			</AdminMetrics>
 
			<section className="space-y-2">
				<AdminBand
					title="Recent Pros"
					count={isLoading ? undefined : recentPros.length}
					quiet
				/>
 
				{isLoading ? (
					<Card className="divide-y divide-border-default overflow-hidden">
						{[1, 2, 3].map((i) => (
							<div key={i} className="space-y-2 px-3.5 py-2.5">
								<Skeleton className="h-4 w-32" />
								<Skeleton className="h-3 w-24" />
							</div>
						))}
					</Card>
				) : recentPros.length === 0 ? (
					<EmptyState icon={Store} title="No pros yet" className="py-8" />
				) : (
					<Card className="overflow-hidden">
						<ul>
							{recentPros.map((pro) => (
								<li
									key={pro.id}
									className="[&+li]:border-t [&+li]:border-border-default"
								>
									{/* The whole row is the target: one tab stop, one hit
									    area. Rows tint on hover; they never lift. */}
									<Link
										to={`/admin/pros/${pro.id}`}
										className="flex items-center gap-3 px-3.5 py-2.5 transition-colors hover:bg-foreground-default/7 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500"
									>
										<span className="min-w-0 grow">
											<span className="block truncate text-sm font-medium text-foreground-default">
												{pro.businessName}
											</span>
											<span className="block truncate text-xs text-foreground-muted">
												{pro.primaryLocality
													? `${pro.primaryLocality.name}, ${pro.primaryLocality.city}`
													: "No location set"}
											</span>
										</span>
										<StatusBadge status={pro.status} />
									</Link>
								</li>
							))}
						</ul>
						<div className="border-t border-border-strong px-3.5 py-2">
							<Link
								to="/admin/pros"
								className="text-xs font-semibold uppercase tracking-wider text-primary-600 hover:text-primary-700"
							>
								View all pros →
							</Link>
						</div>
					</Card>
				)}
			</section>
		</div>
	);
}