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 | 1x 27x 27x 27x 27x 27x 1x 1x 1x 27x 27x 1x | import { useState } from "react";
import { BarChart3, Download } from "lucide-react";
import { MarketplaceTraffic } from "../components/analytics";
import { WorkBand } from "../components/today/WorkQueue";
import { Button } from "../components/ui/button";
import { Select } from "../components/ui/select";
import { Separator } from "../components/ui/separator";
import { useCrmAnalytics } from "../hooks/queries/useCrmQueries";
import { useProStats } from "../hooks/queries/useProQueries";
import { downloadCsv } from "../lib/csv";
import { usePro } from "../lib/pro-context";
import {
FinancialSummary,
FollowupHealth,
KpiCards,
LossReasonBreakdown,
PipelineOverview,
SourcePerformance,
} from "./insights/analytics-panels";
/**
* `/insights` — ONE page. Plan §0.3's deferred merge, done.
*
* WHAT WAS MERGED
* `/insights` (marketplace traffic, `proApi.getProStats`) and
* `/insights/pipeline` (sales pipeline, `useCrmAnalytics`, period-scoped) were
* two pages, two code paths and two panels both labelled "Sources". The merge
* was deferred once, with a written reason: the traffic half has no period
* dimension, so what does the period picker apply to?
*
* THE ANSWER THAT UNBLOCKED IT: the picker belongs to the PIPELINE section,
* not to the page. Marketplace traffic keeps its fixed 7-day and all-time
* windows because that is the only shape its query has; the pipeline section
* keeps the five-period picker because that is the shape its query has. Two
* sections on one page, each honest about its own window, is a better answer
* than one window that lies about half the figures.
*
* NOTHING WAS DROPPED. Every metric both pages showed is below:
*
* From `/insights` (marketplace traffic — 9 figures, 4 panels)
* Profile views (7d) · Project clicks (7d) · WhatsApp clicks (all time) ·
* Leads (all time) · the views/clicks/sessions daily trend ·
* Top projects · Traffic sources · Devices · Top cities · CSV export
*
* From `/insights/pipeline` (sales pipeline — 6 panels)
* KPI cards (active leads, pipeline value, won, conversion) ·
* Pipeline overview (per-stage counts and values, new/won/lost) ·
* Source performance (leads, won, conv %, value per lead source) ·
* Financial summary (won deals, avg deal size, quote→order, active
* pipeline) · Loss reasons · Follow-up health (stale, overdue, stuck,
* no-notes) · the period picker
*
* The two "Sources" panels both survive and are now distinguishable, because
* they sit under bands that say which kind of source they count: TRAFFIC
* SOURCES is the referrer of a marketplace visit, LEAD SOURCES is Walk-in /
* Referral / Instagram. That ambiguity was the strongest argument for merging
* and the merge is what fixes it.
*
* JOB BOOK IDIOM
* Ruled bands instead of card titles, the page title capped at `.title`, the
* trend table on the `Table` family (it was a hand-rolled HTML table, which
* Phase 6's grep test will forbid outside `components/ui/`), and the period
* picker on the `Select` primitive (it was a raw native select).
*
* ONE MARKETPLACE SECTION, NOT TWO
* The marketplace half is `<MarketplaceTraffic context="pro">` — the same
* component `pages/admin/pro-analytics.tsx` renders with `context="admin"`.
* The two files used to carry the same six panels off the same `ProStats`
* shape, and had already drifted (one dropped `unknown` cities, the other did
* not; one formatted `en-IN`, the other did not). This page keeps `useProStats`
* only for the CSV export, which needs the raw daily rows; React Query
* de-duplicates the two calls behind one query key, so it is one fetch.
*
* `/insights/pipeline` still resolves — it redirects here, so bookmarks, the
* `/crm/analytics` legacy redirect that targets it, and the link on the leads
* board all keep working. Collapsing it into `LEGACY_REDIRECTS` is a change to
* `src/routes.tsx`, which this group does not own; it is reported.
*/
const PERIOD_OPTIONS = [
{ value: "this_week", label: "This week" },
{ value: "this_month", label: "This month" },
{ value: "last_month", label: "Last month" },
{ value: "this_quarter", label: "This quarter" },
{ value: "all_time", label: "All time" },
] as const;
export function InsightsPage() {
const { proId } = usePro();
const [period, setPeriod] = useState<string>("this_month");
// React Query, not useState+useEffect: the old page refetched on every
// navigation because it bypassed the cache. `useProStats` already existed.
// Only the CSV export reads `stats` here now — `MarketplaceTraffic` runs
// the same query for the panels and React Query serves both from one fetch.
const { data: stats } = useProStats(proId);
const { data: crm, isLoading: crmLoading } = useCrmAnalytics(proId, period);
const handleExportCsv = () => {
Iif (!stats?.daily || stats.daily.length === 0) return;
downloadCsv(
stats.daily.map((day) => ({
Date: day.date,
"Page Views": day.pageViews,
"Project Clicks": day.projectClicks,
"Image Clicks": day.imageClicks,
"WhatsApp Clicks": day.whatsappClicks,
"Call Clicks": day.callClicks,
"Contact Clicks": day.inquiryClicks,
"Unique Sessions": day.uniqueSessions,
})),
"analytics-report",
);
};
const dailyRows = stats?.daily ?? [];
return (
<div className="space-y-8">
<div>
<div className="flex flex-wrap items-baseline justify-between gap-3 pb-3">
<h1 className="text-xl font-bold text-foreground-default">
Insights
</h1>
</div>
<Separator strong marked />
</div>
<MarketplaceTraffic
context="pro"
proId={proId}
bandAction={
dailyRows.length > 0 ? (
<Button variant="outline" size="sm" onClick={handleExportCsv}>
<Download aria-hidden="true" />
Export CSV
</Button>
) : undefined
}
errorMessage="Marketplace figures could not be loaded. The pipeline section below is unaffected."
/>
{/* ───────── Pipeline ───────── */}
<section className="space-y-3">
<div className="flex flex-wrap items-end justify-between gap-3">
<WorkBand title="Pipeline" quiet className="grow" />
<Select
tier="dense"
aria-label="Pipeline period"
value={period}
onChange={(event) => setPeriod(event.target.value)}
options={PERIOD_OPTIONS}
className="w-40"
/>
</div>
{crmLoading ? (
<div className="h-48 animate-pulse rounded-lg bg-background-subtle" />
) : !crm ? (
<div className="rounded-lg border border-border-default bg-background-elevated py-16 text-center">
<BarChart3
aria-hidden="true"
className="mx-auto mb-3 size-10 text-foreground-subtle"
/>
<p className="text-sm text-foreground-muted">
No pipeline data yet. Add leads and they will show up here.
</p>
</div>
) : (
<div className="space-y-6">
<KpiCards data={crm} />
<PipelineOverview data={crm} />
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
<SourcePerformance data={crm} />
<FinancialSummary data={crm} />
</div>
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
<LossReasonBreakdown data={crm} />
<FollowupHealth data={crm} />
</div>
</div>
)}
</section>
</div>
);
}
|