All files / src/pages dashboard.tsx

100% Statements 16/16
100% Branches 35/35
100% Functions 3/3
100% Lines 16/16

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 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215                                                                                                                    37x   37x   37x 37x       37x   37x 37x 37x   37x 37x               37x 37x               37x             37x   37x   37x                                                                                                                                                                                                                                
import { useMemo } from "react";
import { Link } from "@tanstack/react-router";
import {
	Figure,
	WorkBand,
	WorkQueue,
	WorkQueueSkeleton,
} from "../components/today/WorkQueue";
import { FirstRunCard } from "../components/today/FirstRun";
import { StudioCard } from "../components/today/StudioCard";
import {
	buildWaitingOnSomeoneElse,
	buildWaitingOnYou,
} from "../components/today/work-queue";
import { PushPermissionBanner } from "../components/pwa/PushPermissionBanner";
import { IosBanner } from "../components/pwa/IosBanner";
import { Separator } from "../components/ui/separator";
import { usePro } from "../lib/pro-context";
import { useProjects } from "../hooks/queries/useProjectQueries";
import { useCrmAnalytics, useKanbanData } from "../hooks/queries/useCrmQueries";
import { useNotifications } from "../hooks/queries/useNotificationQueries";
import { useSocialDrafts } from "../hooks/queries/useSocialDraftQueries";
import { useWebsiteSettings } from "../hooks/queries/useWebsiteQueries";
import { usePublishReadiness } from "../hooks/queries/usePublishReadiness";
import { abbreviatePaise } from "../lib/currency-utils";
 
/**
 * `/` — Today. Plan §1.3. Specification: `.context/mockups/today.html` and
 * `today-first-run.html`.
 *
 * WHAT THIS PAGE USED TO BE
 * Four stat cards, a Recent Leads list, a Quick Actions grid of four tiles,
 * a 0% profile checklist of six chores, and — for every pro who had not been
 * published yet, which is every new pro — an amber banner reading "Your
 * profile is pending review … Contact admin for more details."
 *
 * Every one of those is gone, and the banner is the one worth naming. A pro
 * can now publish themselves: clearing an objective bar flips the studio to
 * `published` automatically, server-side, with no human in the loop. "Pending
 * review" described a review that does not exist and told the pro to contact
 * somebody who has nothing to do. It is replaced by the first-run card (what
 * to do) and the YOUR STUDIO panel (where you stand, in the server's words).
 *
 * WHAT IT IS NOW: A WORK QUEUE ORDERED BY WHO IS WAITING
 *   ▌ WAITING ON YOU             work nobody else can move
 *   ▌ WAITING ON SOMEONE ELSE    work in flight elsewhere
 *   ▌ THIS WEEK                  the four-figure strip, beside YOUR STUDIO
 *
 * Built entirely from queries the portal already makes. The derivations, and
 * the two rows the mockup draws that no existing query can answer, are
 * documented in `components/today/work-queue.ts` — read that file before
 * adding a band.
 *
 * Quick Actions is gone rather than reskinned. Three of its four tiles were
 * links to rail destinations and the fourth was the theme switcher, which
 * lives in the top bar. A grid of links to the navigation is not work.
 */
export function DashboardPage() {
	const { proId, pro } = usePro();
 
	const { data: projects = [], isLoading: projectsLoading } =
		useProjects(proId);
	const { data: kanbanData, isLoading: kanbanLoading } = useKanbanData(proId);
	const { data: crmDashboard, isLoading: crmLoading } = useCrmAnalytics(
		proId,
		"this_week",
	);
	const { data: notificationsData, isLoading: notificationsLoading } =
		useNotifications({ category: "enquiry", limit: 20 });
	const { data: draftsData } = useSocialDrafts(proId);
	const { data: websiteSettings } = useWebsiteSettings(proId);
	const { data: readiness } = usePublishReadiness(proId);
 
	const waitingOnYou = useMemo(
		() =>
			buildWaitingOnYou({
				leads: kanbanData?.leads ?? [],
				notifications: notificationsData?.notifications ?? [],
			}),
		[kanbanData, notificationsData],
	);
 
	const waitingOnOthers = useMemo(
		() =>
			buildWaitingOnSomeoneElse({
				drafts: draftsData?.drafts ?? [],
				latestBuild: websiteSettings?.latestBuild ?? null,
			}),
		[draftsData, websiteSettings],
	);
 
	const queueLoading = kanbanLoading || notificationsLoading;
 
	/**
	 * First run is the state that earns a different screen: no projects at
	 * all. A pro with one project and no leads is not a first run — they have
	 * work in flight and the bands are the honest answer.
	 */
	const isFirstRun = !projectsLoading && projects.length === 0;
 
	const pipeline = crmDashboard?.pipeline;
 
	return (
		<div className="space-y-8">
			{/*
			 * A page title caps at `.title` (~1.375rem); `text-xl` is the nearest
			 * step on the token scale under that cap. Display LG/XL stay out of
			 * the shell — the only Display on this page is the first-run
			 * headline's Display MD.
			 */}
			<div>
				<div className="flex flex-wrap items-baseline justify-between gap-3 pb-3">
					<h1 className="text-xl font-bold text-foreground-default">Today</h1>
					{/* `en-IN` from one place: the browser's Intl, with the one locale. */}
					<span className="text-xs font-semibold uppercase tracking-wider tabular-nums text-foreground-muted">
						{new Date().toLocaleDateString("en-IN", {
							weekday: "short",
							day: "numeric",
							month: "short",
						})}
					</span>
				</div>
				{/* The page rule, with its one 3px index-red inset bar. */}
				<Separator strong marked />
			</div>
 
			{/* Notification permission prompts — Today only, as before. */}
			<PushPermissionBanner />
			<IosBanner />
 
			{/*
			 * FIRST RUN IS THE WHOLE SCREEN, NOT A CARD ON TOP OF ONE.
			 *
			 * The figure strip and the studio panel are deliberately INSIDE the
			 * `else` branch. A brand-new designer has nothing to count, so
			 * printing `0 · 0 · 0 · ₹0` under a band would reinstate the four
			 * zeros this change exists to delete — and the studio panel would
			 * restate, in a second place, what the phase rail already says. One
			 * band, one action, and nothing else on the page.
			 */}
			{isFirstRun ? (
				<FirstRunCard readiness={readiness} />
			) : (
				<>
					{queueLoading ? (
						<WorkQueueSkeleton title="Waiting on you" />
					) : (
						<WorkQueue
							title="Waiting on you"
							items={waitingOnYou}
							markFirst
							emptyLabel="Nothing is waiting on you. Overdue follow-ups and new enquiries land here."
						/>
					)}
 
					<WorkQueue
						title="Waiting on someone else"
						items={waitingOnOthers}
						emptyLabel="Nothing in flight. Reels and website builds show their progress here."
					/>
 
					{/* ▌ THIS WEEK beside YOUR STUDIO. */}
					<div className="grid grid-cols-1 items-start gap-6 lg:grid-cols-3">
						<section className="lg:col-span-2">
							<WorkBand title="This week" quiet />
							<div className="mt-3 grid grid-cols-2 gap-x-4 sm:grid-cols-4">
								<Figure
									label="Added"
									value={crmLoading ? "—" : (pipeline?.newLeadsInPeriod ?? 0)}
								/>
								<Figure
									label="Won"
									value={crmLoading ? "—" : (pipeline?.wonInPeriod ?? 0)}
								/>
								<Figure
									label="Lost"
									value={crmLoading ? "—" : (pipeline?.lostInPeriod ?? 0)}
								/>
								{/*
								 * ADDED / WON / LOST are flows through the week; PIPELINE is
								 * the stock standing in it right now, which is what the
								 * mockup prints and what a pro means by the word. Paise
								 * integers in, `lib/currency-utils` out, ₹ glyphed — the
								 * character, never "Rs." and never an image.
								 */}
								<Figure
									label="Pipeline"
									value={
										crmLoading || !pipeline
											? "—"
											: `₹ ${abbreviatePaise(pipeline.totalPipelineValue)}`
									}
								/>
							</div>
							<p className="mt-2 text-xs text-foreground-muted">
								<Link
									to="/insights"
									className="font-semibold text-primary-600 underline decoration-2 underline-offset-4 hover:text-primary-700"
								>
									Insights <span aria-hidden="true">&#9656;</span>
								</Link>{" "}
								has the rest &mdash; pipeline, sources and marketplace traffic.
							</p>
						</section>
 
						{pro && (
							<StudioCard pro={pro} projects={projects} readiness={readiness} />
						)}
					</div>
				</>
			)}
		</div>
	);
}