/** * IntelligencePage — Hot Demand + Stock Gaps + Product Ideas (with podium * medals) + Intent Themes. Same shape as the Shopify embed Intelligence page. * * @layer Presentation */ import { useEffect, useMemo, useState } from 'react'; import { useSearch, useNavigate } from '@tanstack/react-router'; import { PageLayout } from '../../components/shared'; import { useTranslation } from '@/i18n/TranslationProvider'; import { useTenants } from '@/features/tenants/hooks/useTenants'; import { useProductDemand, useQueryPhrase, useIntentTheme } from './hooks/useIntelligence'; import { resolveRange, resolvePeriod, generatePeriods, type TimeRange } from './components/time-range'; import { TimeRangePicker } from './components/TimeRangePicker'; import { PeriodSlider } from './components/PeriodSlider'; import { HotDemandBarChart } from './components/HotDemandBarChart'; import { Medal } from './components/Medal'; interface IntelSearch { range?: TimeRange; period?: string; tenant?: string } const formatPercent = (n: number) => `${(n * 100).toFixed(1)}%`; export function IntelligencePage() { const { t, locale } = useTranslation(); const lang: 'en' | 'de' = (locale as string)?.startsWith('de') ? 'de' : 'en'; const navigate = useNavigate(); const search = useSearch({ strict: false }) as IntelSearch; const { data: tenantsRes } = useTenants(); const tenants = useMemo(() => (tenantsRes as any)?.tenants ?? (tenantsRes as any)?.items ?? (Array.isArray(tenantsRes) ? tenantsRes : []), [tenantsRes]); const fallbackTenantId: string | null = tenants?.[0]?.id ?? null; const tenantId = search.tenant && tenants.find((t: any) => t.id === search.tenant) ? search.tenant : fallbackTenantId; // Stable ISO strings — same loop-prevention pattern as DashboardPage. const rangeParam = (search.range ?? 'daily') as TimeRange; const periodParam = search.period ?? null; const [periods, setPeriods] = useState(() => generatePeriods(rangeParam)); const [stable, setStable] = useState(() => { const ap = resolvePeriod(rangeParam, periodParam); return { range: rangeParam, activeKey: ap.key, longLabel: ap.longLabel, startISO: ap.start.toISOString(), endISO: ap.end.toISOString() }; }); useEffect(() => { setPeriods(generatePeriods(rangeParam)); }, [rangeParam]); useEffect(() => { const ap = resolvePeriod(rangeParam, periodParam); setStable({ range: rangeParam, activeKey: ap.key, longLabel: ap.longLabel, startISO: ap.start.toISOString(), endISO: ap.end.toISOString() }); }, [rangeParam, periodParam]); const { startISO, endISO, activeKey } = stable; const resolved = { range: rangeParam }; const setRange = (next: TimeRange) => navigate({ search: (s: any) => ({ ...s, range: next, period: undefined }) }); const setPeriod = (key: string) => navigate({ search: (s: any) => ({ ...s, period: key }) }); const setTenant = (id: string) => navigate({ search: (s: any) => ({ ...s, tenant: id }) }); const { data: demand } = useProductDemand(tenantId, 'demand', { startISO, endISO, limit: 10 }); const { data: stockGaps } = useProductDemand(tenantId, 'oos', { startISO, endISO, limit: 10 }); const { data: phrases } = useQueryPhrase(tenantId, { startISO, endISO, limit: 10, gapOnly: true }); const { data: themes } = useIntentTheme(tenantId, { startISO, endISO }); if (!tenantId) { return
No tenants yet.
; } return ( {tenants.length > 1 && (
)}

Hot Demand — products customers ask about most

{!demand || demand.length === 0 ? (

No product-matched commands in this window.

) : ( <> {demand.map((p) => ( ))}
ProductAsks
{p.productName ?? 'Unknown product'} {p.demandHits.toLocaleString()}
)}

Stock Gaps — products customers wanted while out of stock

Each ask while out of stock is a missed sale.

{!stockGaps || stockGaps.length === 0 ? (
Nothing was asked for while out of stock — top products were available.
) : ( <> {stockGaps.map((p) => ( ))}
ProductOOS asksTotal asks
{p.productName ?? 'Unknown product'} {p.oosHits.toLocaleString()} {p.demandHits.toLocaleString()}
)}

Product Ideas — what customers ask for that you don't sell yet

Phrases that found no match in your catalog. Treat as a backlog.

{!phrases || phrases.length === 0 ? (
No ideas yet — every query matched something in your catalog.
) : ( {phrases.map((q, idx) => { const place = (idx < 3 ? (idx + 1) : null) as 1 | 2 | 3 | null; return ( ); })}
PhraseAsks
{place && } {q.sampleText ?? '(unknown)'} {q.gapCount.toLocaleString()}
)}

Intent Themes — what kinds of help customers seek

{!themes || themes.length === 0 ? (

No intent themes yet.

) : ( {themes.map((it) => ( ))}
ThemeAsksShare
{it.themeLabel} {it.hitCount.toLocaleString()} {formatPercent(it.percentage)}
)}
); }