@burdenoff/microfe-subscriberbot 2026.1014.1 → 2026.1023.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/node_modules/clsx/dist/clsx.js.map +1 -1
- package/dist/node_modules/tailwind-merge/dist/bundle-mjs.js +97 -18
- package/dist/node_modules/tailwind-merge/dist/bundle-mjs.js.map +1 -1
- package/dist/subscriberbot/SubscriberBotRoot.js.map +1 -1
- package/dist/subscriberbot/SubscriberBotRoutes.js.map +1 -1
- package/dist/subscriberbot/admin/AdminDashboardPage.js.map +1 -1
- package/dist/subscriberbot/admin/AdminRoot.js.map +1 -1
- package/dist/subscriberbot/components/EmptyState.js.map +1 -1
- package/dist/subscriberbot/components/MetricsCard.js.map +1 -1
- package/dist/subscriberbot/components/Modal.js.map +1 -1
- package/dist/subscriberbot/components/PageLayout.js.map +1 -1
- package/dist/subscriberbot/components/StateViews.js.map +1 -1
- package/dist/subscriberbot/components/StatusBadge.js.map +1 -1
- package/dist/subscriberbot/components/viz.js.map +1 -1
- package/dist/subscriberbot/context/SubscriberBotContext.js.map +1 -1
- package/dist/subscriberbot/hooks/useSubscriberBotApi.d.ts +19 -19
- package/dist/subscriberbot/hooks/useSubscriberBotApi.d.ts.map +1 -1
- package/dist/subscriberbot/hooks/useSubscriberBotApi.js.map +1 -1
- package/dist/subscriberbot/hooks/useTr.js.map +1 -1
- package/dist/subscriberbot/pages/AssetVaultPage.js.map +1 -1
- package/dist/subscriberbot/pages/DashboardPage.d.ts.map +1 -1
- package/dist/subscriberbot/pages/DashboardPage.js +4 -4
- package/dist/subscriberbot/pages/DashboardPage.js.map +1 -1
- package/dist/subscriberbot/pages/InboxPage.js.map +1 -1
- package/dist/subscriberbot/pages/InsightsPage.js.map +1 -1
- package/dist/subscriberbot/pages/InvoicesPage.js.map +1 -1
- package/dist/subscriberbot/pages/WorkflowsPage.js.map +1 -1
- package/dist/subscriberbot/pages/agents/AgentDetailPage.js.map +1 -1
- package/dist/subscriberbot/pages/agents/AgentNewPage.js.map +1 -1
- package/dist/subscriberbot/pages/agents/AgentsListPage.js.map +1 -1
- package/dist/subscriberbot/pages/providers/ProviderDetailPage.js.map +1 -1
- package/dist/subscriberbot/pages/providers/ProviderNewPage.js.map +1 -1
- package/dist/subscriberbot/pages/providers/ProvidersListPage.js.map +1 -1
- package/dist/subscriberbot/pages/relationships/RelationshipDetailPage.js.map +1 -1
- package/dist/subscriberbot/pages/relationships/RelationshipNewPage.js.map +1 -1
- package/dist/subscriberbot/pages/relationships/RelationshipsListPage.js.map +1 -1
- package/dist/subscriberbot/services/createSandboxAssistantTransport.js.map +1 -1
- package/dist/subscriberbot/store/subscriberbotStore.js.map +1 -1
- package/dist/subscriberbot/utils/cn.js.map +1 -1
- package/dist/subscriberbot/utils/formatters.js.map +1 -1
- package/dist/subscriberbot/utils/navigation.js.map +1 -1
- package/dist/subscriberbot/utils/pageContext.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DashboardPage.js","names":[],"sources":["../../../src/subscriberbot/pages/DashboardPage.tsx"],"sourcesContent":["/**\n * Dashboard / Overview\n * @module subscriberbot/pages\n *\n * The Subscriber Bot landing page: a premium fintech console over every\n * recurring digital relationship — total recurring spend, portfolio-by-status,\n * upcoming renewals, the action-required inbox, and outstanding invoices. Every\n * number and chart is COMPUTED from real data returned by\n * `wspace-subscriberbot-svc` — nothing is mocked.\n */\n\nimport { useMemo } from 'react';\nimport {\n Repeat2,\n Wallet,\n CalendarClock,\n Inbox,\n AlertTriangle,\n ArrowRight,\n ArrowUpRight,\n Sparkles,\n Bot,\n ShieldCheck,\n} from 'lucide-react';\nimport { PagePurpose, GlassCard } from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n MetricsCard,\n StatusBadge,\n EmptyState,\n LoadingView,\n ErrorView,\n Section,\n Donut,\n Legend,\n Timeline,\n type Segment,\n type TimelineItem,\n} from '../components';\nimport { useSubscriberBot } from '../context/SubscriberBotContext';\nimport {\n useRelationships,\n useInvoices,\n useInboxMessages,\n useRenewalEvents,\n} from '../hooks/useSubscriberBotApi';\nimport { useTr } from '../hooks/useTr';\nimport { formatCurrency, formatRelativeTime, formatEnumLabel } from '../utils/formatters';\nimport type { SubBillingInterval, SubRelationship, SubRenewalEvent } from '../types';\n\n// Normalize any billing interval to an approximate monthly multiplier so we can\n// express total recurring spend as one comparable \"per month\" number.\nconst MONTHLY_FACTOR: Record<SubBillingInterval, number> = {\n DAILY: 30,\n WEEKLY: 4.345,\n MONTHLY: 1,\n QUARTERLY: 1 / 3,\n YEARLY: 1 / 12,\n LIFETIME: 0,\n FREE: 0,\n USAGE_BASED: 1,\n};\n\n// A single normalized \"what charges next\" row, sourced from real renewal events\n// when present, otherwise derived from each relationship's own `renewsAt`.\ninterface UpcomingRenewal {\n id: string;\n relationshipId: string;\n title: string;\n when: string;\n amount: number | null;\n currency: string | null;\n soon: boolean;\n}\n\nconst MS_PER_DAY = 1000 * 60 * 60 * 24;\n\nexport function DashboardPage() {\n const { navigateTo, currentUser } = useSubscriberBot();\n const tr = useTr();\n\n const relationshipsQ = useRelationships({ pagination: { limit: 100, offset: 0 } });\n const invoicesQ = useInvoices({ pagination: { limit: 50, offset: 0 } });\n const inboxQ = useInboxMessages({ pagination: { limit: 50, offset: 0 } });\n const renewalsQ = useRenewalEvents({ upcomingOnly: true });\n\n const relationships = useMemo(() => relationshipsQ.data?.items ?? [], [relationshipsQ.data]);\n const invoices = useMemo(() => invoicesQ.data?.items ?? [], [invoicesQ.data]);\n const messages = useMemo(() => inboxQ.data?.items ?? [], [inboxQ.data]);\n const renewals = useMemo(() => renewalsQ.data ?? [], [renewalsQ.data]);\n\n // Fast label lookup so renewal rows can name the relationship they belong to.\n const relationshipById = useMemo(() => {\n const map = new Map<string, SubRelationship>();\n relationships.forEach((r) => map.set(r.id, r));\n return map;\n }, [relationships]);\n\n const stats = useMemo(() => {\n const active = relationships.filter((r) => r.status === 'ACTIVE' || r.status === 'TRIAL');\n const monthlySpend = active.reduce(\n (sum, r) => sum + r.amount * (MONTHLY_FACTOR[r.interval] ?? 1),\n 0\n );\n const primaryCurrency = active[0]?.currency ?? relationships[0]?.currency ?? 'USD';\n const unreadActionRequired = messages.filter((m) => m.actionRequired && !m.isRead).length;\n const openInvoices = invoices.filter(\n (i) => i.status === 'ISSUED' || i.status === 'OVERDUE' || i.status === 'FAILED'\n );\n const openInvoiceTotal = openInvoices.reduce((sum, i) => sum + i.total, 0);\n const atRisk = relationships.filter(\n (r) => r.status === 'PAST_DUE' || r.status === 'LAPSED'\n ).length;\n\n return {\n activeCount: active.length,\n totalCount: relationships.length,\n monthlySpend,\n primaryCurrency,\n unreadActionRequired,\n openInvoiceCount: openInvoices.length,\n openInvoiceTotal,\n atRisk,\n };\n }, [relationships, messages, invoices]);\n\n // Portfolio-by-status → donut segments (grouped from real relationships).\n const statusSegments = useMemo<Segment[]>(() => {\n const counts = new Map<string, number>();\n relationships.forEach((r) => counts.set(r.status, (counts.get(r.status) ?? 0) + 1));\n return Array.from(counts.entries())\n .sort((a, b) => b[1] - a[1])\n .map(([status, value]) => ({ label: formatEnumLabel(status), value }));\n }, [relationships]);\n\n // Normalize upcoming renewals: prefer real renewal events, fall back to\n // relationship `renewsAt` when the event stream is empty. Sorted soonest-first.\n const upcoming = useMemo<UpcomingRenewal[]>(() => {\n const now = Date.now();\n const fromEvents: UpcomingRenewal[] = renewals.map((e: SubRenewalEvent) => {\n const rel = relationshipById.get(e.relationshipId);\n const t = new Date(e.occursAt).getTime();\n return {\n id: e.id,\n relationshipId: e.relationshipId,\n title: rel?.label ?? formatEnumLabel(e.eventType),\n when: e.occursAt,\n amount: e.amount ?? rel?.amount ?? null,\n currency: e.currency ?? rel?.currency ?? null,\n soon: !Number.isNaN(t) && t - now <= 7 * MS_PER_DAY,\n };\n });\n\n if (fromEvents.length > 0) {\n return fromEvents\n .filter((u) => !Number.isNaN(new Date(u.when).getTime()))\n .sort((a, b) => new Date(a.when).getTime() - new Date(b.when).getTime());\n }\n\n // Derive from relationships that carry a future renewal date.\n return relationships\n .filter((r) => {\n if (!r.renewsAt) return false;\n const t = new Date(r.renewsAt).getTime();\n return !Number.isNaN(t) && t >= now;\n })\n .map((r) => {\n const t = new Date(r.renewsAt as string).getTime();\n return {\n id: r.id,\n relationshipId: r.id,\n title: r.label,\n when: r.renewsAt as string,\n amount: r.amount,\n currency: r.currency,\n soon: t - now <= 7 * MS_PER_DAY,\n };\n })\n .sort((a, b) => new Date(a.when).getTime() - new Date(b.when).getTime());\n }, [renewals, relationships, relationshipById]);\n\n const timelineItems = useMemo<TimelineItem[]>(\n () =>\n upcoming.slice(0, 6).map((u) => ({\n title: u.title,\n sub: formatRelativeTime(u.when),\n tone: u.soon ? 'warning' : 'default',\n meta:\n u.amount != null\n ? formatCurrency(u.amount, u.currency ?? stats.primaryCurrency, {\n maximumFractionDigits: 0,\n })\n : undefined,\n })),\n [upcoming, stats.primaryCurrency]\n );\n\n const actionMessages = useMemo(() => messages.filter((m) => m.actionRequired), [messages]);\n\n const isLoading =\n relationshipsQ.isLoading || invoicesQ.isLoading || inboxQ.isLoading || renewalsQ.isLoading;\n const isError =\n relationshipsQ.isError && invoicesQ.isError && inboxQ.isError && renewalsQ.isError;\n\n if (isLoading) {\n return (\n <PageLayout\n title={tr('dashboard.title', 'Overview')}\n description={tr(\n 'dashboard.description',\n 'Your recurring digital relationships at a glance'\n )}\n >\n <LoadingView label={tr('dashboard.loading', 'Loading your subscription portfolio…')} />\n </PageLayout>\n );\n }\n\n if (isError) {\n return (\n <PageLayout\n title={tr('dashboard.title', 'Overview')}\n description={tr(\n 'dashboard.description',\n 'Your recurring digital relationships at a glance'\n )}\n >\n <ErrorView\n message={tr(\n 'dashboard.error',\n \"We couldn't reach the Subscriber Bot service. Please try again.\"\n )}\n onRetry={() => {\n void relationshipsQ.refetch();\n void invoicesQ.refetch();\n void inboxQ.refetch();\n void renewalsQ.refetch();\n }}\n />\n </PageLayout>\n );\n }\n\n const greetingName = currentUser?.firstName || currentUser?.name?.split(' ')[0];\n\n const quickActions = [\n {\n labelKey: 'dashboard.quickActions.addRelationship.label',\n descriptionKey: 'dashboard.quickActions.addRelationship.description',\n fallbackLabel: 'Add a relationship',\n fallbackDescription: 'Track a new subscription',\n icon: <Repeat2 className=\"h-5 w-5\" />,\n path: '/relationships/new',\n },\n {\n labelKey: 'dashboard.quickActions.discoverProviders.label',\n descriptionKey: 'dashboard.quickActions.discoverProviders.description',\n fallbackLabel: 'Discover providers',\n fallbackDescription: 'Browse the registry',\n icon: <Sparkles className=\"h-5 w-5\" />,\n path: '/providers',\n },\n {\n labelKey: 'dashboard.quickActions.openInbox.label',\n descriptionKey: 'dashboard.quickActions.openInbox.description',\n fallbackLabel: 'Open inbox',\n fallbackDescription: 'Merged communications',\n icon: <Inbox className=\"h-5 w-5\" />,\n path: '/inbox',\n },\n {\n labelKey: 'dashboard.quickActions.configureAgents.label',\n descriptionKey: 'dashboard.quickActions.configureAgents.description',\n fallbackLabel: 'Configure agents',\n fallbackDescription: 'Automate your portfolio',\n icon: <Bot className=\"h-5 w-5\" />,\n path: '/agents',\n },\n ];\n\n return (\n <PageLayout\n title={\n greetingName\n ? tr('dashboard.welcomeBack', 'Welcome back, {{name}}', { name: greetingName })\n : tr('dashboard.title', 'Overview')\n }\n description={tr(\n 'dashboard.description',\n 'Everything you pay for on repeat — in one intelligent console.'\n )}\n >\n <PagePurpose className=\"mb-6\">\n {tr(\n 'dashboard.purpose',\n 'Subscriber Bot is your system of record for every recurring digital relationship. See total recurring spend, what renews next, what needs action, and what to optimize — all from real data across your providers.'\n )}\n </PagePurpose>\n\n {/* KPI tiles */}\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <MetricsCard\n featured\n icon={<Wallet />}\n value={formatCurrency(stats.monthlySpend, stats.primaryCurrency, {\n maximumFractionDigits: 0,\n })}\n label={tr('dashboard.metrics.recurringSpend', 'Recurring spend / month')}\n />\n <MetricsCard\n icon={<Repeat2 />}\n value={stats.activeCount}\n label={tr('dashboard.metrics.activeRelationships', 'Active relationships')}\n />\n <MetricsCard\n icon={<CalendarClock />}\n value={upcoming.length}\n label={tr('dashboard.metrics.upcomingRenewals', 'Upcoming renewals')}\n />\n <MetricsCard\n icon={<AlertTriangle />}\n value={stats.unreadActionRequired}\n label={tr('dashboard.metrics.needsAction', 'Need your action')}\n trend={stats.unreadActionRequired > 0 ? 'down' : undefined}\n />\n </div>\n\n {/* Main console: renewals (left) + portfolio donut & needs-attention (right) */}\n <div className=\"mt-8 grid grid-cols-1 gap-6 lg:grid-cols-3\">\n {/* Upcoming renewals timeline — spans two columns on desktop */}\n <div className=\"lg:col-span-2\">\n <Section\n title={tr('dashboard.renewals.title', 'Upcoming renewals')}\n description={tr(\n 'dashboard.renewals.description',\n \"What's charging next across your portfolio\"\n )}\n className=\"mt-0\"\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('/relationships')}\n className=\"inline-flex items-center gap-1 text-sm font-medium text-text-link hover:underline\"\n >\n {tr('common.viewAll', 'View all')} <ArrowUpRight className=\"h-3.5 w-3.5\" />\n </button>\n }\n >\n <GlassCard treatment=\"plain\" className=\"p-5\">\n {timelineItems.length === 0 ? (\n <EmptyState\n illustration=\"empty-calendar\"\n title={tr('dashboard.renewals.emptyTitle', 'No upcoming renewals')}\n description={tr(\n 'dashboard.renewals.emptyDescription',\n 'Renewals you track will appear here ahead of time.'\n )}\n />\n ) : (\n <Timeline items={timelineItems} />\n )}\n </GlassCard>\n </Section>\n </div>\n\n {/* Right rail: portfolio-by-status donut + needs-attention panel */}\n <div className=\"flex flex-col gap-6\">\n <Section\n title={tr('dashboard.portfolio.title', 'Portfolio by status')}\n description={tr('dashboard.portfolio.description', 'Where your relationships stand')}\n className=\"mt-0\"\n >\n <GlassCard treatment=\"glass\" className=\"p-5\">\n {statusSegments.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n title={tr('dashboard.portfolio.emptyTitle', 'No relationships yet')}\n description={tr(\n 'dashboard.portfolio.emptyDescription',\n 'Add your first subscription to see the breakdown.'\n )}\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('/relationships/new')}\n className=\"rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:opacity-90\"\n >\n {tr('dashboard.portfolio.addRelationship', 'Add relationship')}\n </button>\n }\n />\n ) : (\n <div className=\"flex flex-col items-center gap-5 sm:flex-row sm:items-center lg:flex-col xl:flex-row\">\n <Donut\n data={statusSegments}\n size={148}\n thickness={20}\n centerLabel={String(stats.totalCount)}\n centerSub={tr(\n stats.totalCount === 1\n ? 'dashboard.portfolio.relationshipSingular'\n : 'dashboard.portfolio.relationshipPlural',\n stats.totalCount === 1 ? 'relationship' : 'relationships'\n )}\n className=\"shrink-0\"\n />\n <Legend data={statusSegments} className=\"w-full min-w-0 flex-1\" />\n </div>\n )}\n </GlassCard>\n </Section>\n\n <Section\n title={tr('dashboard.needsAttention.title', 'Needs attention')}\n description={tr(\n 'dashboard.needsAttention.description',\n 'Required actions from the Universal Inbox'\n )}\n className=\"mt-0\"\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('/inbox')}\n className=\"text-sm font-medium text-text-link hover:underline\"\n >\n {tr('dashboard.needsAttention.openInbox', 'Open inbox')}\n </button>\n }\n >\n <GlassCard treatment=\"plain\" className=\"p-3\">\n {actionMessages.length === 0 ? (\n <div className=\"flex items-center gap-3 px-2 py-4\">\n <span className=\"flex size-10 shrink-0 items-center justify-center rounded-lg bg-status-success-bg-subtle text-status-success-text\">\n <ShieldCheck className=\"h-5 w-5\" />\n </span>\n <div className=\"min-w-0\">\n <p className=\"text-sm font-medium text-text-primary\">\n {tr('dashboard.needsAttention.allCaughtUp', \"You're all caught up\")}\n </p>\n <p className=\"text-xs text-text-secondary\">\n {tr(\n 'dashboard.needsAttention.allCaughtUpDescription',\n 'Required actions across your channels will surface here.'\n )}\n </p>\n </div>\n </div>\n ) : (\n <ul className=\"space-y-1\">\n {actionMessages.slice(0, 5).map((message) => (\n <li key={message.id}>\n <button\n type=\"button\"\n onClick={() => navigateTo('/inbox')}\n className=\"flex w-full items-center gap-3 rounded-lg border-l-2 border-status-warning-text/60 bg-status-warning-bg-subtle/50 p-3 text-left transition-colors hover:bg-status-warning-bg-subtle\"\n >\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {message.subject ||\n tr('dashboard.needsAttention.noSubject', '(no subject)')}\n </p>\n {message.preview && (\n <p className=\"truncate text-xs text-text-secondary\">\n {message.preview}\n </p>\n )}\n </div>\n <StatusBadge status={message.category} className=\"shrink-0\" />\n </button>\n </li>\n ))}\n </ul>\n )}\n </GlassCard>\n </Section>\n </div>\n </div>\n\n {/* Open-invoice call-out (only when there is real outstanding balance) */}\n {stats.openInvoiceCount > 0 && (\n <button\n type=\"button\"\n onClick={() => navigateTo('/invoices')}\n className=\"mt-6 flex w-full items-center justify-between gap-3 rounded-xl border border-status-warning-text/30 bg-status-warning-bg-subtle px-5 py-4 text-left transition-shadow hover:shadow-md\"\n >\n <div className=\"flex items-center gap-3\">\n <AlertTriangle className=\"h-5 w-5 shrink-0 text-status-warning-text\" />\n <span className=\"text-sm font-medium text-status-warning-text\">\n {tr(\n 'dashboard.openInvoices.label',\n '{{count}} open invoice · {{amount}} outstanding',\n {\n count: stats.openInvoiceCount,\n amount: formatCurrency(stats.openInvoiceTotal, stats.primaryCurrency),\n }\n )}\n </span>\n </div>\n <ArrowRight className=\"h-4 w-4 shrink-0 text-status-warning-text\" />\n </button>\n )}\n\n {/* Quick actions */}\n <Section\n title={tr('dashboard.quickActions.title', 'Quick actions')}\n description={tr('dashboard.quickActions.description', 'Jump straight into your workflow')}\n >\n <div className=\"grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4\">\n {quickActions.map((qa) => (\n <button\n key={qa.path}\n type=\"button\"\n onClick={() => navigateTo(qa.path)}\n className=\"group flex items-center gap-3 rounded-xl border border-border-subtle bg-bg-surface p-4 text-left transition-all hover:-translate-y-0.5 hover:border-border-seam hover:shadow-md\"\n >\n <span className=\"flex size-10 shrink-0 items-center justify-center rounded-lg bg-[var(--color-accent-soft)] text-text-primary\">\n {qa.icon}\n </span>\n <span className=\"min-w-0 flex-1\">\n <span className=\"block text-sm font-medium text-text-primary\">\n {tr(qa.labelKey, qa.fallbackLabel)}\n </span>\n <span className=\"block text-xs text-text-secondary\">\n {tr(qa.descriptionKey, qa.fallbackDescription)}\n </span>\n </span>\n <ArrowRight className=\"h-4 w-4 shrink-0 text-text-secondary transition-transform group-hover:translate-x-0.5\" />\n </button>\n ))}\n </div>\n </Section>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"DashboardPage.js","names":[],"sources":["../../../src/subscriberbot/pages/DashboardPage.tsx"],"sourcesContent":["/**\n * Dashboard / Overview\n * @module subscriberbot/pages\n *\n * The Subscriber Bot landing page: a premium fintech console over every\n * recurring digital relationship — total recurring spend, portfolio-by-status,\n * upcoming renewals, the action-required inbox, and outstanding invoices. Every\n * number and chart is COMPUTED from real data returned by\n * `wspace-subscriberbot-svc` — nothing is mocked.\n */\n\nimport { useMemo } from 'react';\nimport {\n Repeat2,\n Wallet,\n CalendarClock,\n Inbox,\n AlertTriangle,\n ArrowRight,\n ArrowUpRight,\n Sparkles,\n Bot,\n ShieldCheck,\n} from 'lucide-react';\nimport { PagePurpose, GlassCard } from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n MetricsCard,\n StatusBadge,\n EmptyState,\n LoadingView,\n ErrorView,\n Section,\n Donut,\n Legend,\n Timeline,\n type Segment,\n type TimelineItem,\n} from '../components';\nimport { useSubscriberBot } from '../context/SubscriberBotContext';\nimport {\n useRelationships,\n useInvoices,\n useInboxMessages,\n useRenewalEvents,\n} from '../hooks/useSubscriberBotApi';\nimport { useTr } from '../hooks/useTr';\nimport { formatCurrency, formatRelativeTime, formatEnumLabel } from '../utils/formatters';\nimport type { SubBillingInterval, SubRelationship, SubRenewalEvent } from '../types';\n\n// Normalize any billing interval to an approximate monthly multiplier so we can\n// express total recurring spend as one comparable \"per month\" number.\nconst MONTHLY_FACTOR: Record<SubBillingInterval, number> = {\n DAILY: 30,\n WEEKLY: 4.345,\n MONTHLY: 1,\n QUARTERLY: 1 / 3,\n YEARLY: 1 / 12,\n LIFETIME: 0,\n FREE: 0,\n USAGE_BASED: 1,\n};\n\n// A single normalized \"what charges next\" row, sourced from real renewal events\n// when present, otherwise derived from each relationship's own `renewsAt`.\ninterface UpcomingRenewal {\n id: string;\n relationshipId: string;\n title: string;\n when: string;\n amount: number | null;\n currency: string | null;\n soon: boolean;\n}\n\nconst MS_PER_DAY = 1000 * 60 * 60 * 24;\n\nexport function DashboardPage() {\n const { navigateTo, currentUser } = useSubscriberBot();\n const tr = useTr();\n\n const relationshipsQ = useRelationships({ pagination: { limit: 100, offset: 0 } });\n const invoicesQ = useInvoices({ pagination: { limit: 50, offset: 0 } });\n const inboxQ = useInboxMessages({ pagination: { limit: 50, offset: 0 } });\n const renewalsQ = useRenewalEvents({ upcomingOnly: true });\n\n const relationships = useMemo(() => relationshipsQ.data?.items ?? [], [relationshipsQ.data]);\n const invoices = useMemo(() => invoicesQ.data?.items ?? [], [invoicesQ.data]);\n const messages = useMemo(() => inboxQ.data?.items ?? [], [inboxQ.data]);\n const renewals = useMemo(() => renewalsQ.data ?? [], [renewalsQ.data]);\n\n // Fast label lookup so renewal rows can name the relationship they belong to.\n const relationshipById = useMemo(() => {\n const map = new Map<string, SubRelationship>();\n relationships.forEach((r) => map.set(r.id, r));\n return map;\n }, [relationships]);\n\n const stats = useMemo(() => {\n const active = relationships.filter((r) => r.status === 'ACTIVE' || r.status === 'TRIAL');\n const monthlySpend = active.reduce(\n (sum, r) => sum + r.amount * (MONTHLY_FACTOR[r.interval] ?? 1),\n 0\n );\n const primaryCurrency = active[0]?.currency ?? relationships[0]?.currency ?? 'USD';\n // BOFF-8660 — KPI must match the Universal Inbox \"need action\" counter\n // and the on-page Needs-attention list, both of which count by\n // `actionRequired` alone (read state is irrelevant). The previous\n // `&& !m.isRead` clause made the tile show 0 the moment the user read\n // the only action-required message, contradicting the surfaces below.\n const actionRequired = messages.filter((m) => m.actionRequired).length;\n const openInvoices = invoices.filter(\n (i) => i.status === 'ISSUED' || i.status === 'OVERDUE' || i.status === 'FAILED'\n );\n const openInvoiceTotal = openInvoices.reduce((sum, i) => sum + i.total, 0);\n const atRisk = relationships.filter(\n (r) => r.status === 'PAST_DUE' || r.status === 'LAPSED'\n ).length;\n\n return {\n activeCount: active.length,\n totalCount: relationships.length,\n monthlySpend,\n primaryCurrency,\n actionRequired,\n openInvoiceCount: openInvoices.length,\n openInvoiceTotal,\n atRisk,\n };\n }, [relationships, messages, invoices]);\n\n // Portfolio-by-status → donut segments (grouped from real relationships).\n const statusSegments = useMemo<Segment[]>(() => {\n const counts = new Map<string, number>();\n relationships.forEach((r) => counts.set(r.status, (counts.get(r.status) ?? 0) + 1));\n return Array.from(counts.entries())\n .sort((a, b) => b[1] - a[1])\n .map(([status, value]) => ({ label: formatEnumLabel(status), value }));\n }, [relationships]);\n\n // Normalize upcoming renewals: prefer real renewal events, fall back to\n // relationship `renewsAt` when the event stream is empty. Sorted soonest-first.\n const upcoming = useMemo<UpcomingRenewal[]>(() => {\n const now = Date.now();\n const fromEvents: UpcomingRenewal[] = renewals.map((e: SubRenewalEvent) => {\n const rel = relationshipById.get(e.relationshipId);\n const t = new Date(e.occursAt).getTime();\n return {\n id: e.id,\n relationshipId: e.relationshipId,\n title: rel?.label ?? formatEnumLabel(e.eventType),\n when: e.occursAt,\n amount: e.amount ?? rel?.amount ?? null,\n currency: e.currency ?? rel?.currency ?? null,\n soon: !Number.isNaN(t) && t - now <= 7 * MS_PER_DAY,\n };\n });\n\n if (fromEvents.length > 0) {\n return fromEvents\n .filter((u) => !Number.isNaN(new Date(u.when).getTime()))\n .sort((a, b) => new Date(a.when).getTime() - new Date(b.when).getTime());\n }\n\n // Derive from relationships that carry a future renewal date.\n return relationships\n .filter((r) => {\n if (!r.renewsAt) return false;\n const t = new Date(r.renewsAt).getTime();\n return !Number.isNaN(t) && t >= now;\n })\n .map((r) => {\n const t = new Date(r.renewsAt as string).getTime();\n return {\n id: r.id,\n relationshipId: r.id,\n title: r.label,\n when: r.renewsAt as string,\n amount: r.amount,\n currency: r.currency,\n soon: t - now <= 7 * MS_PER_DAY,\n };\n })\n .sort((a, b) => new Date(a.when).getTime() - new Date(b.when).getTime());\n }, [renewals, relationships, relationshipById]);\n\n const timelineItems = useMemo<TimelineItem[]>(\n () =>\n upcoming.slice(0, 6).map((u) => ({\n title: u.title,\n sub: formatRelativeTime(u.when),\n tone: u.soon ? 'warning' : 'default',\n meta:\n u.amount != null\n ? formatCurrency(u.amount, u.currency ?? stats.primaryCurrency, {\n maximumFractionDigits: 0,\n })\n : undefined,\n })),\n [upcoming, stats.primaryCurrency]\n );\n\n const actionMessages = useMemo(() => messages.filter((m) => m.actionRequired), [messages]);\n\n const isLoading =\n relationshipsQ.isLoading || invoicesQ.isLoading || inboxQ.isLoading || renewalsQ.isLoading;\n const isError =\n relationshipsQ.isError && invoicesQ.isError && inboxQ.isError && renewalsQ.isError;\n\n if (isLoading) {\n return (\n <PageLayout\n title={tr('dashboard.title', 'Overview')}\n description={tr(\n 'dashboard.description',\n 'Your recurring digital relationships at a glance'\n )}\n >\n <LoadingView label={tr('dashboard.loading', 'Loading your subscription portfolio…')} />\n </PageLayout>\n );\n }\n\n if (isError) {\n return (\n <PageLayout\n title={tr('dashboard.title', 'Overview')}\n description={tr(\n 'dashboard.description',\n 'Your recurring digital relationships at a glance'\n )}\n >\n <ErrorView\n message={tr(\n 'dashboard.error',\n \"We couldn't reach the Subscriber Bot service. Please try again.\"\n )}\n onRetry={() => {\n void relationshipsQ.refetch();\n void invoicesQ.refetch();\n void inboxQ.refetch();\n void renewalsQ.refetch();\n }}\n />\n </PageLayout>\n );\n }\n\n const greetingName = currentUser?.firstName || currentUser?.name?.split(' ')[0];\n\n const quickActions = [\n {\n labelKey: 'dashboard.quickActions.addRelationship.label',\n descriptionKey: 'dashboard.quickActions.addRelationship.description',\n fallbackLabel: 'Add a relationship',\n fallbackDescription: 'Track a new subscription',\n icon: <Repeat2 className=\"h-5 w-5\" />,\n path: '/relationships/new',\n },\n {\n labelKey: 'dashboard.quickActions.discoverProviders.label',\n descriptionKey: 'dashboard.quickActions.discoverProviders.description',\n fallbackLabel: 'Discover providers',\n fallbackDescription: 'Browse the registry',\n icon: <Sparkles className=\"h-5 w-5\" />,\n path: '/providers',\n },\n {\n labelKey: 'dashboard.quickActions.openInbox.label',\n descriptionKey: 'dashboard.quickActions.openInbox.description',\n fallbackLabel: 'Open inbox',\n fallbackDescription: 'Merged communications',\n icon: <Inbox className=\"h-5 w-5\" />,\n path: '/inbox',\n },\n {\n labelKey: 'dashboard.quickActions.configureAgents.label',\n descriptionKey: 'dashboard.quickActions.configureAgents.description',\n fallbackLabel: 'Configure agents',\n fallbackDescription: 'Automate your portfolio',\n icon: <Bot className=\"h-5 w-5\" />,\n path: '/agents',\n },\n ];\n\n return (\n <PageLayout\n title={\n greetingName\n ? tr('dashboard.welcomeBack', 'Welcome back, {{name}}', { name: greetingName })\n : tr('dashboard.title', 'Overview')\n }\n description={tr(\n 'dashboard.description',\n 'Everything you pay for on repeat — in one intelligent console.'\n )}\n >\n <PagePurpose className=\"mb-6\">\n {tr(\n 'dashboard.purpose',\n 'Subscriber Bot is your system of record for every recurring digital relationship. See total recurring spend, what renews next, what needs action, and what to optimize — all from real data across your providers.'\n )}\n </PagePurpose>\n\n {/* KPI tiles */}\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <MetricsCard\n featured\n icon={<Wallet />}\n value={formatCurrency(stats.monthlySpend, stats.primaryCurrency, {\n maximumFractionDigits: 0,\n })}\n label={tr('dashboard.metrics.recurringSpend', 'Recurring spend / month')}\n />\n <MetricsCard\n icon={<Repeat2 />}\n value={stats.activeCount}\n label={tr('dashboard.metrics.activeRelationships', 'Active relationships')}\n />\n <MetricsCard\n icon={<CalendarClock />}\n value={upcoming.length}\n label={tr('dashboard.metrics.upcomingRenewals', 'Upcoming renewals')}\n />\n <MetricsCard\n icon={<AlertTriangle />}\n value={stats.actionRequired}\n label={tr('dashboard.metrics.needsAction', 'Need your action')}\n trend={stats.actionRequired > 0 ? 'down' : undefined}\n />\n </div>\n\n {/* Main console: renewals (left) + portfolio donut & needs-attention (right) */}\n <div className=\"mt-8 grid grid-cols-1 gap-6 lg:grid-cols-3\">\n {/* Upcoming renewals timeline — spans two columns on desktop */}\n <div className=\"lg:col-span-2\">\n <Section\n title={tr('dashboard.renewals.title', 'Upcoming renewals')}\n description={tr(\n 'dashboard.renewals.description',\n \"What's charging next across your portfolio\"\n )}\n className=\"mt-0\"\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('/relationships')}\n className=\"inline-flex items-center gap-1 text-sm font-medium text-text-link hover:underline\"\n >\n {tr('common.viewAll', 'View all')} <ArrowUpRight className=\"h-3.5 w-3.5\" />\n </button>\n }\n >\n <GlassCard treatment=\"plain\" className=\"p-5\">\n {timelineItems.length === 0 ? (\n <EmptyState\n illustration=\"empty-calendar\"\n title={tr('dashboard.renewals.emptyTitle', 'No upcoming renewals')}\n description={tr(\n 'dashboard.renewals.emptyDescription',\n 'Renewals you track will appear here ahead of time.'\n )}\n />\n ) : (\n <Timeline items={timelineItems} />\n )}\n </GlassCard>\n </Section>\n </div>\n\n {/* Right rail: portfolio-by-status donut + needs-attention panel */}\n <div className=\"flex flex-col gap-6\">\n <Section\n title={tr('dashboard.portfolio.title', 'Portfolio by status')}\n description={tr('dashboard.portfolio.description', 'Where your relationships stand')}\n className=\"mt-0\"\n >\n <GlassCard treatment=\"glass\" className=\"p-5\">\n {statusSegments.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n title={tr('dashboard.portfolio.emptyTitle', 'No relationships yet')}\n description={tr(\n 'dashboard.portfolio.emptyDescription',\n 'Add your first subscription to see the breakdown.'\n )}\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('/relationships/new')}\n className=\"rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:opacity-90\"\n >\n {tr('dashboard.portfolio.addRelationship', 'Add relationship')}\n </button>\n }\n />\n ) : (\n <div className=\"flex flex-col items-center gap-5 sm:flex-row sm:items-center lg:flex-col xl:flex-row\">\n <Donut\n data={statusSegments}\n size={148}\n thickness={20}\n centerLabel={String(stats.totalCount)}\n centerSub={tr(\n stats.totalCount === 1\n ? 'dashboard.portfolio.relationshipSingular'\n : 'dashboard.portfolio.relationshipPlural',\n stats.totalCount === 1 ? 'relationship' : 'relationships'\n )}\n className=\"shrink-0\"\n />\n <Legend data={statusSegments} className=\"w-full min-w-0 flex-1\" />\n </div>\n )}\n </GlassCard>\n </Section>\n\n <Section\n title={tr('dashboard.needsAttention.title', 'Needs attention')}\n description={tr(\n 'dashboard.needsAttention.description',\n 'Required actions from the Universal Inbox'\n )}\n className=\"mt-0\"\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('/inbox')}\n className=\"text-sm font-medium text-text-link hover:underline\"\n >\n {tr('dashboard.needsAttention.openInbox', 'Open inbox')}\n </button>\n }\n >\n <GlassCard treatment=\"plain\" className=\"p-3\">\n {actionMessages.length === 0 ? (\n <div className=\"flex items-center gap-3 px-2 py-4\">\n <span className=\"flex size-10 shrink-0 items-center justify-center rounded-lg bg-status-success-bg-subtle text-status-success-text\">\n <ShieldCheck className=\"h-5 w-5\" />\n </span>\n <div className=\"min-w-0\">\n <p className=\"text-sm font-medium text-text-primary\">\n {tr('dashboard.needsAttention.allCaughtUp', \"You're all caught up\")}\n </p>\n <p className=\"text-xs text-text-secondary\">\n {tr(\n 'dashboard.needsAttention.allCaughtUpDescription',\n 'Required actions across your channels will surface here.'\n )}\n </p>\n </div>\n </div>\n ) : (\n <ul className=\"space-y-1\">\n {actionMessages.slice(0, 5).map((message) => (\n <li key={message.id}>\n <button\n type=\"button\"\n onClick={() => navigateTo('/inbox')}\n className=\"flex w-full items-center gap-3 rounded-lg border-l-2 border-status-warning-text/60 bg-status-warning-bg-subtle/50 p-3 text-left transition-colors hover:bg-status-warning-bg-subtle\"\n >\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {message.subject ||\n tr('dashboard.needsAttention.noSubject', '(no subject)')}\n </p>\n {message.preview && (\n <p className=\"truncate text-xs text-text-secondary\">\n {message.preview}\n </p>\n )}\n </div>\n <StatusBadge status={message.category} className=\"shrink-0\" />\n </button>\n </li>\n ))}\n </ul>\n )}\n </GlassCard>\n </Section>\n </div>\n </div>\n\n {/* Open-invoice call-out (only when there is real outstanding balance) */}\n {stats.openInvoiceCount > 0 && (\n <button\n type=\"button\"\n onClick={() => navigateTo('/invoices')}\n className=\"mt-6 flex w-full items-center justify-between gap-3 rounded-xl border border-status-warning-text/30 bg-status-warning-bg-subtle px-5 py-4 text-left transition-shadow hover:shadow-md\"\n >\n <div className=\"flex items-center gap-3\">\n <AlertTriangle className=\"h-5 w-5 shrink-0 text-status-warning-text\" />\n <span className=\"text-sm font-medium text-status-warning-text\">\n {tr(\n 'dashboard.openInvoices.label',\n '{{count}} open invoice · {{amount}} outstanding',\n {\n count: stats.openInvoiceCount,\n amount: formatCurrency(stats.openInvoiceTotal, stats.primaryCurrency),\n }\n )}\n </span>\n </div>\n <ArrowRight className=\"h-4 w-4 shrink-0 text-status-warning-text\" />\n </button>\n )}\n\n {/* Quick actions */}\n <Section\n title={tr('dashboard.quickActions.title', 'Quick actions')}\n description={tr('dashboard.quickActions.description', 'Jump straight into your workflow')}\n >\n <div className=\"grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4\">\n {quickActions.map((qa) => (\n <button\n key={qa.path}\n type=\"button\"\n onClick={() => navigateTo(qa.path)}\n className=\"group flex items-center gap-3 rounded-xl border border-border-subtle bg-bg-surface p-4 text-left transition-all hover:-translate-y-0.5 hover:border-border-seam hover:shadow-md\"\n >\n <span className=\"flex size-10 shrink-0 items-center justify-center rounded-lg bg-[var(--color-accent-soft)] text-text-primary\">\n {qa.icon}\n </span>\n <span className=\"min-w-0 flex-1\">\n <span className=\"block text-sm font-medium text-text-primary\">\n {tr(qa.labelKey, qa.fallbackLabel)}\n </span>\n <span className=\"block text-xs text-text-secondary\">\n {tr(qa.descriptionKey, qa.fallbackDescription)}\n </span>\n </span>\n <ArrowRight className=\"h-4 w-4 shrink-0 text-text-secondary transition-transform group-hover:translate-x-0.5\" />\n </button>\n ))}\n </div>\n </Section>\n </PageLayout>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAoDA,IAAM,IAAqD;CACzD,OAAO;CACP,QAAQ;CACR,SAAS;CACT,WAAW,IAAI;CACf,QAAQ,IAAI;CACZ,UAAU;CACV,MAAM;CACN,aAAa;AACf,GAcM,IAAa,MAAO,KAAK,KAAK;AAEpC,SAAgB,IAAgB;CAC9B,IAAM,EAAE,eAAY,mBAAgB,EAAiB,GAC/C,IAAK,EAAM,GAEX,IAAiB,EAAiB,EAAE,YAAY;EAAE,OAAO;EAAK,QAAQ;CAAE,EAAE,CAAC,GAC3E,IAAY,EAAY,EAAE,YAAY;EAAE,OAAO;EAAI,QAAQ;CAAE,EAAE,CAAC,GAChE,IAAS,EAAiB,EAAE,YAAY;EAAE,OAAO;EAAI,QAAQ;CAAE,EAAE,CAAC,GAClE,IAAY,EAAiB,EAAE,cAAc,GAAK,CAAC,GAEnD,IAAgB,QAAc,EAAe,MAAM,SAAS,CAAC,GAAG,CAAC,EAAe,IAAI,CAAC,GACrF,IAAW,QAAc,EAAU,MAAM,SAAS,CAAC,GAAG,CAAC,EAAU,IAAI,CAAC,GACtE,IAAW,QAAc,EAAO,MAAM,SAAS,CAAC,GAAG,CAAC,EAAO,IAAI,CAAC,GAChE,IAAW,QAAc,EAAU,QAAQ,CAAC,GAAG,CAAC,EAAU,IAAI,CAAC,GAG/D,IAAmB,QAAc;EACrC,IAAM,oBAAM,IAAI,IAA6B;EAE7C,OADA,EAAc,SAAS,MAAM,EAAI,IAAI,EAAE,IAAI,CAAC,CAAC,GACtC;CACT,GAAG,CAAC,CAAa,CAAC,GAEZ,IAAQ,QAAc;EAC1B,IAAM,IAAS,EAAc,QAAQ,MAAM,EAAE,WAAW,YAAY,EAAE,WAAW,OAAO,GAClF,IAAe,EAAO,QACzB,GAAK,MAAM,IAAM,EAAE,UAAU,EAAe,EAAE,aAAa,IAC5D,CACF,GACM,IAAkB,EAAO,IAAI,YAAY,EAAc,IAAI,YAAY,OAMvE,IAAiB,EAAS,QAAQ,MAAM,EAAE,cAAc,EAAE,QAC1D,IAAe,EAAS,QAC3B,MAAM,EAAE,WAAW,YAAY,EAAE,WAAW,aAAa,EAAE,WAAW,QACzE,GACM,IAAmB,EAAa,QAAQ,GAAK,MAAM,IAAM,EAAE,OAAO,CAAC,GACnE,IAAS,EAAc,QAC1B,MAAM,EAAE,WAAW,cAAc,EAAE,WAAW,QACjD,EAAE;EAEF,OAAO;GACL,aAAa,EAAO;GACpB,YAAY,EAAc;GAC1B;GACA;GACA;GACA,kBAAkB,EAAa;GAC/B;GACA;EACF;CACF,GAAG;EAAC;EAAe;EAAU;CAAQ,CAAC,GAGhC,IAAiB,QAAyB;EAC9C,IAAM,oBAAS,IAAI,IAAoB;EAEvC,OADA,EAAc,SAAS,MAAM,EAAO,IAAI,EAAE,SAAS,EAAO,IAAI,EAAE,MAAM,KAAK,KAAK,CAAC,CAAC,GAC3E,MAAM,KAAK,EAAO,QAAQ,CAAC,EAC/B,MAAM,GAAG,MAAM,EAAE,KAAK,EAAE,EAAE,EAC1B,KAAK,CAAC,GAAQ,QAAY;GAAE,OAAO,EAAgB,CAAM;GAAG;EAAM,EAAE;CACzE,GAAG,CAAC,CAAa,CAAC,GAIZ,IAAW,QAAiC;EAChD,IAAM,IAAM,KAAK,IAAI,GACf,IAAgC,EAAS,KAAK,MAAuB;GACzE,IAAM,IAAM,EAAiB,IAAI,EAAE,cAAc,GAC3C,IAAI,IAAI,KAAK,EAAE,QAAQ,EAAE,QAAQ;GACvC,OAAO;IACL,IAAI,EAAE;IACN,gBAAgB,EAAE;IAClB,OAAO,GAAK,SAAS,EAAgB,EAAE,SAAS;IAChD,MAAM,EAAE;IACR,QAAQ,EAAE,UAAU,GAAK,UAAU;IACnC,UAAU,EAAE,YAAY,GAAK,YAAY;IACzC,MAAM,CAAC,OAAO,MAAM,CAAC,KAAK,IAAI,KAAO,IAAI;GAC3C;EACF,CAAC;EASD,OAPI,EAAW,SAAS,IACf,EACJ,QAAQ,MAAM,CAAC,OAAO,MAAM,IAAI,KAAK,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC,EACvD,MAAM,GAAG,MAAM,IAAI,KAAK,EAAE,IAAI,EAAE,QAAQ,IAAI,IAAI,KAAK,EAAE,IAAI,EAAE,QAAQ,CAAC,IAIpE,EACJ,QAAQ,MAAM;GACb,IAAI,CAAC,EAAE,UAAU,OAAO;GACxB,IAAM,IAAI,IAAI,KAAK,EAAE,QAAQ,EAAE,QAAQ;GACvC,OAAO,CAAC,OAAO,MAAM,CAAC,KAAK,KAAK;EAClC,CAAC,EACA,KAAK,MAAM;GACV,IAAM,IAAI,IAAI,KAAK,EAAE,QAAkB,EAAE,QAAQ;GACjD,OAAO;IACL,IAAI,EAAE;IACN,gBAAgB,EAAE;IAClB,OAAO,EAAE;IACT,MAAM,EAAE;IACR,QAAQ,EAAE;IACV,UAAU,EAAE;IACZ,MAAM,IAAI,KAAO,IAAI;GACvB;EACF,CAAC,EACA,MAAM,GAAG,MAAM,IAAI,KAAK,EAAE,IAAI,EAAE,QAAQ,IAAI,IAAI,KAAK,EAAE,IAAI,EAAE,QAAQ,CAAC;CAC3E,GAAG;EAAC;EAAU;EAAe;CAAgB,CAAC,GAExC,IAAgB,QAElB,EAAS,MAAM,GAAG,CAAC,EAAE,KAAK,OAAO;EAC/B,OAAO,EAAE;EACT,KAAK,EAAmB,EAAE,IAAI;EAC9B,MAAM,EAAE,OAAO,YAAY;EAC3B,MACE,EAAE,UAAU,OAIR,KAAA,IAHA,EAAe,EAAE,QAAQ,EAAE,YAAY,EAAM,iBAAiB,EAC5D,uBAAuB,EACzB,CAAC;CAET,EAAE,GACJ,CAAC,GAAU,EAAM,eAAe,CAClC,GAEM,IAAiB,QAAc,EAAS,QAAQ,MAAM,EAAE,cAAc,GAAG,CAAC,CAAQ,CAAC,GAEnF,IACJ,EAAe,aAAa,EAAU,aAAa,EAAO,aAAa,EAAU,WAC7E,KACJ,EAAe,WAAW,EAAU,WAAW,EAAO,WAAW,EAAU;CAE7E,IAAI,GACF,OACE,kBAAC,GAAD;EACE,OAAO,EAAG,mBAAmB,UAAU;EACvC,aAAa,EACX,yBACA,kDACF;YAEA,kBAAC,GAAD,EAAa,OAAO,EAAG,qBAAqB,sCAAsC,EAAI,CAAA;CAC5E,CAAA;CAIhB,IAAI,IACF,OACE,kBAAC,GAAD;EACE,OAAO,EAAG,mBAAmB,UAAU;EACvC,aAAa,EACX,yBACA,kDACF;YAEA,kBAAC,GAAD;GACE,SAAS,EACP,mBACA,iEACF;GACA,eAAe;IAIb,AAHA,EAAoB,QAAQ,GAC5B,EAAe,QAAQ,GACvB,EAAY,QAAQ,GACpB,EAAe,QAAQ;GACzB;EACD,CAAA;CACS,CAAA;CAIhB,IAAM,IAAe,GAAa,aAAa,GAAa,MAAM,MAAM,GAAG,EAAE,IAEvE,KAAe;EACnB;GACE,UAAU;GACV,gBAAgB;GAChB,eAAe;GACf,qBAAqB;GACrB,MAAM,kBAAC,GAAD,EAAS,WAAU,UAAW,CAAA;GACpC,MAAM;EACR;EACA;GACE,UAAU;GACV,gBAAgB;GAChB,eAAe;GACf,qBAAqB;GACrB,MAAM,kBAAC,GAAD,EAAU,WAAU,UAAW,CAAA;GACrC,MAAM;EACR;EACA;GACE,UAAU;GACV,gBAAgB;GAChB,eAAe;GACf,qBAAqB;GACrB,MAAM,kBAAC,GAAD,EAAO,WAAU,UAAW,CAAA;GAClC,MAAM;EACR;EACA;GACE,UAAU;GACV,gBAAgB;GAChB,eAAe;GACf,qBAAqB;GACrB,MAAM,kBAAC,GAAD,EAAK,WAAU,UAAW,CAAA;GAChC,MAAM;EACR;CACF;CAEA,OACE,kBAAC,GAAD;EACE,OACE,IACI,EAAG,yBAAyB,0BAA0B,EAAE,MAAM,EAAa,CAAC,IAC5E,EAAG,mBAAmB,UAAU;EAEtC,aAAa,EACX,yBACA,gEACF;YATF;GAWE,kBAAC,GAAD;IAAa,WAAU;cACpB,EACC,qBACA,oNACF;GACW,CAAA;GAGb,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,GAAD;MACE,UAAA;MACA,MAAM,kBAAC,GAAD,CAAS,CAAA;MACf,OAAO,EAAe,EAAM,cAAc,EAAM,iBAAiB,EAC/D,uBAAuB,EACzB,CAAC;MACD,OAAO,EAAG,oCAAoC,yBAAyB;KACxE,CAAA;KACD,kBAAC,GAAD;MACE,MAAM,kBAAC,GAAD,CAAU,CAAA;MAChB,OAAO,EAAM;MACb,OAAO,EAAG,yCAAyC,sBAAsB;KAC1E,CAAA;KACD,kBAAC,GAAD;MACE,MAAM,kBAAC,GAAD,CAAgB,CAAA;MACtB,OAAO,EAAS;MAChB,OAAO,EAAG,sCAAsC,mBAAmB;KACpE,CAAA;KACD,kBAAC,GAAD;MACE,MAAM,kBAAC,GAAD,CAAgB,CAAA;MACtB,OAAO,EAAM;MACb,OAAO,EAAG,iCAAiC,kBAAkB;MAC7D,OAAO,EAAM,iBAAiB,IAAI,SAAS,KAAA;KAC5C,CAAA;IACE;;GAGL,kBAAC,OAAD;IAAK,WAAU;cAAf,CAEE,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,GAAD;MACE,OAAO,EAAG,4BAA4B,mBAAmB;MACzD,aAAa,EACX,kCACA,4CACF;MACA,WAAU;MACV,QACE,kBAAC,UAAD;OACE,MAAK;OACL,eAAe,EAAW,gBAAgB;OAC1C,WAAU;iBAHZ;QAKG,EAAG,kBAAkB,UAAU;QAAE;QAAC,kBAAC,GAAD,EAAc,WAAU,cAAe,CAAA;OACpE;;gBAGV,kBAAC,GAAD;OAAW,WAAU;OAAQ,WAAU;iBACpC,EAAc,WAAW,IACxB,kBAAC,GAAD;QACE,cAAa;QACb,OAAO,EAAG,iCAAiC,sBAAsB;QACjE,aAAa,EACX,uCACA,oDACF;OACD,CAAA,IAED,kBAAC,GAAD,EAAU,OAAO,EAAgB,CAAA;MAE1B,CAAA;KACJ,CAAA;IACN,CAAA,GAGL,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,GAAD;MACE,OAAO,EAAG,6BAA6B,qBAAqB;MAC5D,aAAa,EAAG,mCAAmC,gCAAgC;MACnF,WAAU;gBAEV,kBAAC,GAAD;OAAW,WAAU;OAAQ,WAAU;iBACpC,EAAe,WAAW,IACzB,kBAAC,GAAD;QACE,cAAa;QACb,OAAO,EAAG,kCAAkC,sBAAsB;QAClE,aAAa,EACX,wCACA,mDACF;QACA,QACE,kBAAC,UAAD;SACE,MAAK;SACL,eAAe,EAAW,oBAAoB;SAC9C,WAAU;mBAET,EAAG,uCAAuC,kBAAkB;QACvD,CAAA;OAEX,CAAA,IAED,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,GAAD;SACE,MAAM;SACN,MAAM;SACN,WAAW;SACX,aAAa,OAAO,EAAM,UAAU;SACpC,WAAW,EACT,EAAM,eAAe,IACjB,6CACA,0CACJ,EAAM,eAAe,IAAI,iBAAiB,eAC5C;SACA,WAAU;QACX,CAAA,GACD,kBAAC,GAAD;SAAQ,MAAM;SAAgB,WAAU;QAAyB,CAAA,CAC9D;;MAEE,CAAA;KACJ,CAAA,GAET,kBAAC,GAAD;MACE,OAAO,EAAG,kCAAkC,iBAAiB;MAC7D,aAAa,EACX,wCACA,2CACF;MACA,WAAU;MACV,QACE,kBAAC,UAAD;OACE,MAAK;OACL,eAAe,EAAW,QAAQ;OAClC,WAAU;iBAET,EAAG,sCAAsC,YAAY;MAChD,CAAA;gBAGV,kBAAC,GAAD;OAAW,WAAU;OAAQ,WAAU;iBACpC,EAAe,WAAW,IACzB,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,QAAD;SAAM,WAAU;mBACd,kBAAC,IAAD,EAAa,WAAU,UAAW,CAAA;QAC9B,CAAA,GACN,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,KAAD;UAAG,WAAU;oBACV,EAAG,wCAAwC,sBAAsB;SACjE,CAAA,GACH,kBAAC,KAAD;UAAG,WAAU;oBACV,EACC,mDACA,0DACF;SACC,CAAA,CACA;UACF;YAEL,kBAAC,MAAD;QAAI,WAAU;kBACX,EAAe,MAAM,GAAG,CAAC,EAAE,KAAK,MAC/B,kBAAC,MAAD,EAAA,UACE,kBAAC,UAAD;SACE,MAAK;SACL,eAAe,EAAW,QAAQ;SAClC,WAAU;mBAHZ,CAKE,kBAAC,OAAD;UAAK,WAAU;oBAAf,CACE,kBAAC,KAAD;WAAG,WAAU;qBACV,EAAQ,WACP,EAAG,sCAAsC,cAAc;UACxD,CAAA,GACF,EAAQ,WACP,kBAAC,KAAD;WAAG,WAAU;qBACV,EAAQ;UACR,CAAA,CAEF;aACL,kBAAC,GAAD;UAAa,QAAQ,EAAQ;UAAU,WAAU;SAAY,CAAA,CACvD;WACN,GAnBK,EAAQ,EAmBb,CACL;OACC,CAAA;MAEG,CAAA;KACJ,CAAA,CACN;MACF;;GAGJ,EAAM,mBAAmB,KACxB,kBAAC,UAAD;IACE,MAAK;IACL,eAAe,EAAW,WAAW;IACrC,WAAU;cAHZ,CAKE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,GAAD,EAAe,WAAU,4CAA6C,CAAA,GACtE,kBAAC,QAAD;MAAM,WAAU;gBACb,EACC,gCACA,mDACA;OACE,OAAO,EAAM;OACb,QAAQ,EAAe,EAAM,kBAAkB,EAAM,eAAe;MACtE,CACF;KACI,CAAA,CACH;QACL,kBAAC,GAAD,EAAY,WAAU,4CAA6C,CAAA,CAC7D;;GAIV,kBAAC,GAAD;IACE,OAAO,EAAG,gCAAgC,eAAe;IACzD,aAAa,EAAG,sCAAsC,kCAAkC;cAExF,kBAAC,OAAD;KAAK,WAAU;eACZ,GAAa,KAAK,MACjB,kBAAC,UAAD;MAEE,MAAK;MACL,eAAe,EAAW,EAAG,IAAI;MACjC,WAAU;gBAJZ;OAME,kBAAC,QAAD;QAAM,WAAU;kBACb,EAAG;OACA,CAAA;OACN,kBAAC,QAAD;QAAM,WAAU;kBAAhB,CACE,kBAAC,QAAD;SAAM,WAAU;mBACb,EAAG,EAAG,UAAU,EAAG,aAAa;QAC7B,CAAA,GACN,kBAAC,QAAD;SAAM,WAAU;mBACb,EAAG,EAAG,gBAAgB,EAAG,mBAAmB;QACzC,CAAA,CACF;;OACN,kBAAC,GAAD,EAAY,WAAU,wFAAyF,CAAA;MACzG;QAjBD,EAAG,IAiBF,CACT;IACE,CAAA;GACE,CAAA;EACC;;AAEhB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"InboxPage.js","names":[],"sources":["../../../src/subscriberbot/pages/InboxPage.tsx"],"sourcesContent":["/**\n * Universal Inbox\n * @module subscriberbot/pages\n *\n * Every merged subscription communication in one timeline, AI-categorized,\n * plus management of the ingest channels that feed it. Filter by category /\n * unread, mark messages read, add & verify channels, and log a test message.\n * Reads `subListInboxMessages` / `subListChannels`; writes `subMarkMessageRead`,\n * `subCreateChannel`, `subVerifyChannel`, `subIngestMessage`.\n */\n\nimport { useState } from 'react';\nimport {\n Mail,\n MailOpen,\n AlertTriangle,\n Inbox as InboxIcon,\n Plus,\n Radio,\n BadgeCheck,\n Send,\n MessageSquare,\n MessagesSquare,\n Bell,\n Webhook,\n Phone,\n Hash,\n ShieldAlert,\n Sparkles,\n ChevronRight,\n MousePointerClick,\n type LucideIcon,\n} from 'lucide-react';\nimport { PagePurpose, GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../components/PageLayout';\nimport { StatusBadge } from '../components/StatusBadge';\nimport { EmptyState } from '../components/EmptyState';\nimport { Modal } from '../components/Modal';\nimport { LoadingView, ErrorView, Section } from '../components/StateViews';\nimport { MiniProgress } from '../components/viz';\nimport { useSubscriberBot } from '../context/SubscriberBotContext';\nimport {\n useInboxMessages,\n useMarkMessageRead,\n useChannels,\n useCreateChannel,\n useVerifyChannel,\n useIngestMessage,\n} from '../hooks/useSubscriberBotApi';\nimport { useTr } from '../hooks/useTr';\nimport { cn } from '../utils/cn';\nimport { formatEnumLabel, formatRelativeTime, formatDateTime } from '../utils/formatters';\nimport type {\n SubChannelType,\n SubCommunicationChannel,\n SubInboxMessage,\n SubMessageCategory,\n} from '../types';\n\nconst PAGE_SIZE = 25;\n\nconst inputCls =\n 'w-full rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]';\nconst labelCls = 'mb-1 block text-xs font-medium text-text-secondary';\n\nconst MESSAGE_CATEGORIES: SubMessageCategory[] = [\n 'BILLING',\n 'RENEWAL',\n 'REQUIRED_ACTION',\n 'SECURITY_ALERT',\n 'PRODUCT_UPDATE',\n 'MARKETING',\n 'OTHER',\n];\n\nconst CHANNEL_TYPES: SubChannelType[] = [\n 'EMAIL',\n 'SMS',\n 'SLACK',\n 'DISCORD',\n 'WHATSAPP',\n 'PUSH',\n 'WEBHOOK',\n];\n\nconst CATEGORY_KEY: Record<SubMessageCategory, string> = {\n BILLING: 'inbox.categories.billing',\n RENEWAL: 'inbox.categories.renewal',\n REQUIRED_ACTION: 'inbox.categories.requiredAction',\n SECURITY_ALERT: 'inbox.categories.securityAlert',\n PRODUCT_UPDATE: 'inbox.categories.productUpdate',\n MARKETING: 'inbox.categories.marketing',\n OTHER: 'inbox.categories.other',\n};\n\n// Category filter tabs (chips) — the \"All\" pseudo-value plus the real enum.\n// Labels are resolved via useTr inside the page component.\nconst CATEGORY_TAB_VALUES: Array<{\n value: '' | SubMessageCategory;\n key: string;\n fallback: string;\n}> = [\n { value: '', key: 'inbox.categories.all', fallback: 'All' },\n { value: 'BILLING', key: CATEGORY_KEY.BILLING, fallback: 'Billing' },\n { value: 'RENEWAL', key: CATEGORY_KEY.RENEWAL, fallback: 'Renewals' },\n { value: 'REQUIRED_ACTION', key: CATEGORY_KEY.REQUIRED_ACTION, fallback: 'Required actions' },\n { value: 'SECURITY_ALERT', key: CATEGORY_KEY.SECURITY_ALERT, fallback: 'Security' },\n { value: 'PRODUCT_UPDATE', key: CATEGORY_KEY.PRODUCT_UPDATE, fallback: 'Product updates' },\n { value: 'MARKETING', key: CATEGORY_KEY.MARKETING, fallback: 'Marketing' },\n { value: 'OTHER', key: CATEGORY_KEY.OTHER, fallback: 'Other' },\n];\n\n// Per-category tag tone — semantic tokens only.\nconst CATEGORY_TONE: Record<SubMessageCategory, string> = {\n BILLING: 'bg-[var(--color-accent-soft)] text-text-primary',\n RENEWAL: 'bg-status-success-bg-subtle text-status-success-text',\n REQUIRED_ACTION: 'bg-status-warning-bg-subtle text-status-warning-text',\n SECURITY_ALERT: 'bg-status-error-bg-subtle text-status-error-text',\n PRODUCT_UPDATE: 'bg-[var(--color-accent-soft)] text-text-primary',\n MARKETING: 'bg-bg-sunken text-text-secondary',\n OTHER: 'bg-bg-sunken text-text-secondary',\n};\n\nconst CHANNEL_ICON: Record<SubChannelType, LucideIcon> = {\n EMAIL: Mail,\n SMS: MessageSquare,\n SLACK: Hash,\n DISCORD: MessagesSquare,\n WHATSAPP: Phone,\n PUSH: Bell,\n WEBHOOK: Webhook,\n};\n\nfunction CategoryTag({ category }: { category: SubMessageCategory }) {\n return (\n <span\n className={cn(\n 'inline-flex items-center rounded-full px-2 py-0.5 text-[11px] font-medium',\n CATEGORY_TONE[category]\n )}\n >\n {formatEnumLabel(category)}\n </span>\n );\n}\n\nfunction ActionFlag({ className }: { className?: string }) {\n const tr = useTr();\n return (\n <span\n title={tr('inbox.message.actionRequiredTitle', 'Action required')}\n className={cn(\n 'inline-flex items-center gap-1 rounded-full bg-status-warning-bg-subtle px-2 py-0.5 text-[10px] font-medium text-status-warning-text',\n className\n )}\n >\n <AlertTriangle className=\"h-3 w-3\" /> {tr('inbox.message.actionFlag', 'Action')}\n </span>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Add channel modal → subCreateChannel\n// ---------------------------------------------------------------------------\n\nfunction AddChannelModal({ onClose }: { onClose: () => void }) {\n const tr = useTr();\n const create = useCreateChannel();\n const [type, setType] = useState<SubChannelType>('EMAIL');\n const [address, setAddress] = useState('');\n const [label, setLabel] = useState('');\n const [ingestEnabled, setIngestEnabled] = useState(true);\n const [formError, setFormError] = useState<string | null>(null);\n\n const handleSubmit = (e: React.FormEvent) => {\n e.preventDefault();\n setFormError(null);\n if (!address.trim()) {\n setFormError(tr('inbox.addChannelModal.addressRequired', 'An address is required.'));\n return;\n }\n create.mutate(\n {\n type,\n address: address.trim(),\n ingestEnabled,\n ...(label.trim() ? { label: label.trim() } : {}),\n },\n {\n onSuccess: () => onClose(),\n onError: (err) =>\n setFormError(\n err instanceof Error\n ? err.message\n : tr('inbox.addChannelModal.error', 'Could not add channel.')\n ),\n }\n );\n };\n\n return (\n <Modal\n title={tr('inbox.addChannelModal.title', 'Add channel')}\n onClose={onClose}\n maxWidthClassName=\"max-w-xl\"\n >\n <form onSubmit={handleSubmit} className=\"space-y-4\">\n {formError && (\n <div className=\"rounded-lg border border-status-error-text/30 bg-status-error-bg-subtle px-4 py-2 text-sm text-status-error-text\">\n {formError}\n </div>\n )}\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <div>\n <label htmlFor=\"ch-type\" className={labelCls}>\n {tr('inbox.addChannelModal.typeLabel', 'Type')}\n </label>\n <select\n id=\"ch-type\"\n value={type}\n onChange={(e) => setType(e.target.value as SubChannelType)}\n className={inputCls}\n >\n {CHANNEL_TYPES.map((t) => (\n <option key={t} value={t}>\n {formatEnumLabel(t)}\n </option>\n ))}\n </select>\n </div>\n <div>\n <label htmlFor=\"ch-label\" className={labelCls}>\n {tr('inbox.addChannelModal.labelLabel', 'Label')}\n </label>\n <input\n id=\"ch-label\"\n type=\"text\"\n value={label}\n onChange={(e) => setLabel(e.target.value)}\n placeholder={tr('inbox.addChannelModal.labelPlaceholder', 'e.g. Billing inbox')}\n className={inputCls}\n />\n </div>\n </div>\n <div>\n <label htmlFor=\"ch-address\" className={labelCls}>\n {tr('inbox.addChannelModal.addressLabel', 'Address')}{' '}\n <span className=\"text-status-error-text\">*</span>\n </label>\n <input\n id=\"ch-address\"\n type=\"text\"\n value={address}\n onChange={(e) => setAddress(e.target.value)}\n placeholder={tr(\n 'inbox.addChannelModal.addressPlaceholder',\n 'e.g. billing@acme.com, +1…, a webhook URL'\n )}\n className={inputCls}\n required\n />\n </div>\n <label className=\"flex items-center gap-2 text-sm text-text-primary\">\n <input\n type=\"checkbox\"\n checked={ingestEnabled}\n onChange={(e) => setIngestEnabled(e.target.checked)}\n className=\"size-4 rounded border-border-subtle\"\n />\n {tr('inbox.addChannelModal.ingestLabel', 'Ingest messages from this channel')}\n </label>\n <div className=\"flex justify-end gap-3\">\n <button\n type=\"button\"\n onClick={onClose}\n className=\"rounded-lg border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken\"\n >\n {tr('inbox.addChannelModal.cancel', 'Cancel')}\n </button>\n <button\n type=\"submit\"\n disabled={create.isPending}\n className=\"rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:opacity-90 disabled:opacity-50\"\n >\n {create.isPending\n ? tr('inbox.addChannelModal.adding', 'Adding…')\n : tr('inbox.addChannelModal.submit', 'Add channel')}\n </button>\n </div>\n </form>\n </Modal>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Log test message modal → subIngestMessage\n// ---------------------------------------------------------------------------\n\nfunction LogMessageModal({\n channel,\n onClose,\n}: {\n channel: SubCommunicationChannel;\n onClose: () => void;\n}) {\n const tr = useTr();\n const ingest = useIngestMessage();\n const [subject, setSubject] = useState('');\n const [preview, setPreview] = useState('');\n const [category, setCategory] = useState<SubMessageCategory>('OTHER');\n const [actionRequired, setActionRequired] = useState(false);\n const [formError, setFormError] = useState<string | null>(null);\n\n const handleSubmit = (e: React.FormEvent) => {\n e.preventDefault();\n setFormError(null);\n ingest.mutate(\n {\n channelId: channel.id,\n receivedAt: new Date().toISOString(),\n category,\n actionRequired,\n ...(subject.trim() ? { subject: subject.trim() } : {}),\n ...(preview.trim() ? { preview: preview.trim() } : {}),\n },\n {\n onSuccess: () => onClose(),\n onError: (err) =>\n setFormError(\n err instanceof Error\n ? err.message\n : tr('inbox.logMessageModal.error', 'Could not log the message.')\n ),\n }\n );\n };\n\n return (\n <Modal\n title={tr('inbox.logMessageModal.title', 'Log test message')}\n description={tr(\n 'inbox.logMessageModal.description',\n 'Simulate an ingested message from {channel}.',\n { channel: channel.label || channel.address }\n )}\n onClose={onClose}\n >\n <form onSubmit={handleSubmit} className=\"space-y-4\">\n {formError && (\n <div className=\"rounded-lg border border-status-error-text/30 bg-status-error-bg-subtle px-4 py-2 text-sm text-status-error-text\">\n {formError}\n </div>\n )}\n <div>\n <label htmlFor=\"msg-subject\" className={labelCls}>\n {tr('inbox.logMessageModal.subjectLabel', 'Subject')}\n </label>\n <input\n id=\"msg-subject\"\n type=\"text\"\n value={subject}\n onChange={(e) => setSubject(e.target.value)}\n placeholder={tr(\n 'inbox.logMessageModal.subjectPlaceholder',\n 'e.g. Your plan renews in 3 days'\n )}\n className={inputCls}\n />\n </div>\n <div>\n <label htmlFor=\"msg-preview\" className={labelCls}>\n {tr('inbox.logMessageModal.previewLabel', 'Preview')}\n </label>\n <textarea\n id=\"msg-preview\"\n value={preview}\n onChange={(e) => setPreview(e.target.value)}\n rows={2}\n className={inputCls}\n />\n </div>\n <div>\n <label htmlFor=\"msg-category\" className={labelCls}>\n {tr('inbox.logMessageModal.categoryLabel', 'Category')}\n </label>\n <select\n id=\"msg-category\"\n value={category}\n onChange={(e) => setCategory(e.target.value as SubMessageCategory)}\n className={inputCls}\n >\n {MESSAGE_CATEGORIES.map((c) => (\n <option key={c} value={c}>\n {tr(CATEGORY_KEY[c], formatEnumLabel(c))}\n </option>\n ))}\n </select>\n </div>\n <label className=\"flex items-center gap-2 text-sm text-text-primary\">\n <input\n type=\"checkbox\"\n checked={actionRequired}\n onChange={(e) => setActionRequired(e.target.checked)}\n className=\"size-4 rounded border-border-subtle\"\n />\n {tr('inbox.logMessageModal.actionRequired', 'Requires action')}\n </label>\n <div className=\"flex justify-end gap-3\">\n <button\n type=\"button\"\n onClick={onClose}\n className=\"rounded-lg border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken\"\n >\n {tr('inbox.logMessageModal.cancel', 'Cancel')}\n </button>\n <button\n type=\"submit\"\n disabled={ingest.isPending}\n className=\"rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:opacity-90 disabled:opacity-50\"\n >\n {ingest.isPending\n ? tr('inbox.logMessageModal.logging', 'Logging…')\n : tr('inbox.logMessageModal.submit', 'Log message')}\n </button>\n </div>\n </form>\n </Modal>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Channel card → one per ingest source\n// ---------------------------------------------------------------------------\n\nfunction ChannelCard({\n channel,\n onVerify,\n verifying,\n onLog,\n}: {\n channel: SubCommunicationChannel;\n onVerify: (id: string) => void;\n verifying: boolean;\n onLog: (ch: SubCommunicationChannel) => void;\n}) {\n const tr = useTr();\n const Icon = CHANNEL_ICON[channel.type] ?? Radio;\n const verified = channel.status === 'VERIFIED';\n return (\n <GlassCard treatment=\"metric\" className=\"flex flex-col p-4\">\n <div className=\"flex items-start justify-between gap-2\">\n <div className=\"flex min-w-0 items-center gap-2.5\">\n <span className=\"flex size-9 shrink-0 items-center justify-center rounded-lg bg-[var(--color-accent-soft)] text-text-primary\">\n <Icon className=\"h-4 w-4\" />\n </span>\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-semibold text-text-primary\">\n {formatEnumLabel(channel.type)}\n </p>\n {channel.label && (\n <p className=\"truncate text-xs text-text-secondary\">{channel.label}</p>\n )}\n </div>\n </div>\n <StatusBadge status={channel.status} />\n </div>\n\n <div className=\"mt-3 border-t border-border-subtle pt-3\">\n <p className=\"text-[11px] uppercase tracking-wide text-text-secondary\">\n {channel.ingestEnabled\n ? tr('inbox.channels.ingestingFrom', 'Ingesting from')\n : tr('inbox.channels.ingestPaused', 'Ingest paused')}\n </p>\n <p className=\"mt-0.5 truncate text-sm text-text-primary\" title={channel.address}>\n {channel.address}\n </p>\n </div>\n\n <div className=\"mt-auto flex flex-wrap items-center gap-2 pt-3\">\n {channel.status === 'PENDING' && (\n <button\n type=\"button\"\n disabled={verifying}\n onClick={() => onVerify(channel.id)}\n className=\"inline-flex items-center gap-1 rounded-lg bg-action-primary-bg px-3 py-1.5 text-xs font-medium text-action-primary-text hover:opacity-90 disabled:opacity-50\"\n >\n <BadgeCheck className=\"h-3.5 w-3.5\" /> {tr('inbox.channels.verify', 'Verify')}\n </button>\n )}\n {verified && channel.ingestEnabled && (\n <button\n type=\"button\"\n onClick={() => onLog(channel)}\n className=\"inline-flex items-center gap-1 rounded-lg border border-border-subtle px-3 py-1.5 text-xs font-medium text-text-primary hover:bg-bg-sunken\"\n >\n <Send className=\"h-3.5 w-3.5\" />{' '}\n {tr('inbox.channels.logTestMessage', 'Log test message')}\n </button>\n )}\n {channel.status === 'DISABLED' && (\n <span className=\"text-xs text-text-secondary\">\n {tr('inbox.channels.disabled', 'Channel disabled')}\n </span>\n )}\n </div>\n </GlassCard>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Channels management panel\n// ---------------------------------------------------------------------------\n\nfunction ChannelsSection() {\n const tr = useTr();\n const { data, isLoading, isError, refetch } = useChannels();\n const verify = useVerifyChannel();\n const [showAdd, setShowAdd] = useState(false);\n const [logChannel, setLogChannel] = useState<SubCommunicationChannel | null>(null);\n\n const channels = data ?? [];\n const verifiedCount = channels.filter((c) => c.status === 'VERIFIED').length;\n const pendingCount = channels.filter((c) => c.status === 'PENDING').length;\n\n return (\n <Section\n title={tr('inbox.channels.title', 'Ingest channels')}\n description={tr(\n 'inbox.channels.description',\n 'The sources the Universal Inbox pulls subscription communication from'\n )}\n action={\n <button\n type=\"button\"\n onClick={() => setShowAdd(true)}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-3 py-1.5 text-xs font-medium text-action-primary-text hover:opacity-90\"\n >\n <Plus className=\"h-3.5 w-3.5\" /> {tr('inbox.channels.add', 'Add channel')}\n </button>\n }\n >\n {isLoading ? (\n <LoadingView label={tr('inbox.channels.loading', 'Loading channels…')} />\n ) : isError ? (\n <ErrorView\n message={tr('inbox.channels.error', 'Could not load channels.')}\n onRetry={() => void refetch()}\n />\n ) : channels.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Radio className=\"h-12 w-12\" />}\n title={tr('inbox.channels.emptyTitle', 'No channels yet')}\n description={tr(\n 'inbox.channels.emptyDescription',\n 'Add an email, SMS, Slack or webhook channel to start ingesting subscription communication.'\n )}\n />\n ) : (\n <>\n {(verifiedCount > 0 || pendingCount > 0) && (\n <div className=\"mb-3 flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-text-secondary\">\n <span className=\"inline-flex items-center gap-1.5\">\n <span className=\"size-1.5 rounded-full bg-status-success-text\" aria-hidden />\n {tr('inbox.channels.verified', '{{count}} verified', { count: verifiedCount })}\n </span>\n {pendingCount > 0 && (\n <span className=\"inline-flex items-center gap-1.5\">\n <span className=\"size-1.5 rounded-full bg-status-warning-text\" aria-hidden />\n {tr('inbox.channels.pending', '{{count}} pending verification', {\n count: pendingCount,\n })}\n </span>\n )}\n </div>\n )}\n <div className=\"grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3\">\n {channels.map((ch) => (\n <ChannelCard\n key={ch.id}\n channel={ch}\n verifying={verify.isPending}\n onVerify={(id) => verify.mutate(id)}\n onLog={(c) => setLogChannel(c)}\n />\n ))}\n </div>\n </>\n )}\n\n {showAdd && <AddChannelModal onClose={() => setShowAdd(false)} />}\n {logChannel && <LogMessageModal channel={logChannel} onClose={() => setLogChannel(null)} />}\n </Section>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Message row (list)\n// ---------------------------------------------------------------------------\n\nfunction MessageRow({\n message,\n active,\n onSelect,\n onMarkRead,\n markPending,\n onViewRelationship,\n}: {\n message: SubInboxMessage;\n active: boolean;\n onSelect: () => void;\n onMarkRead: () => void;\n markPending: boolean;\n onViewRelationship: (() => void) | null;\n}) {\n const tr = useTr();\n const unread = !message.isRead;\n return (\n <div\n role=\"button\"\n tabIndex={0}\n aria-pressed={active}\n onClick={onSelect}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n onSelect();\n }\n }}\n className={cn(\n 'relative flex cursor-pointer items-start gap-3 p-4 transition-colors',\n 'focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)] focus-visible:ring-inset',\n active ? 'bg-bg-sunken' : unread ? 'bg-accent-wash' : 'hover:bg-bg-sunken/60'\n )}\n >\n {/* unread accent rail */}\n {unread && <span aria-hidden className=\"absolute inset-y-0 left-0 w-0.5 bg-text-link\" />}\n <span className=\"mt-0.5 shrink-0 text-text-secondary\">\n {unread ? <Mail className=\"h-4 w-4 text-text-link\" /> : <MailOpen className=\"h-4 w-4\" />}\n </span>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-center gap-2\">\n {unread && (\n <span\n aria-label={tr('inbox.message.unreadAria', 'Unread')}\n className=\"size-1.5 shrink-0 rounded-full bg-text-link\"\n />\n )}\n <p\n className={cn(\n 'min-w-0 flex-1 truncate text-sm',\n unread ? 'font-semibold text-text-primary' : 'font-normal text-text-primary'\n )}\n >\n {message.subject || tr('inbox.message.noSubject', '(no subject)')}\n </p>\n <span className=\"shrink-0 text-xs tabular-nums text-text-secondary\">\n {formatRelativeTime(message.receivedAt)}\n </span>\n </div>\n {message.preview && (\n <p className=\"mt-0.5 line-clamp-2 text-xs text-text-secondary\">{message.preview}</p>\n )}\n <div className=\"mt-1.5 flex flex-wrap items-center gap-2\">\n <CategoryTag category={message.category} />\n {message.actionRequired && <ActionFlag />}\n {onViewRelationship && (\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation();\n onViewRelationship();\n }}\n className=\"text-xs font-medium text-text-link hover:underline\"\n >\n {tr('inbox.message.viewRelationship', 'View relationship')}\n </button>\n )}\n {unread && (\n <button\n type=\"button\"\n disabled={markPending}\n onClick={(e) => {\n e.stopPropagation();\n onMarkRead();\n }}\n className=\"ml-auto shrink-0 rounded-lg border border-border-subtle px-2.5 py-1 text-xs font-medium text-text-primary hover:bg-bg-sunken disabled:opacity-50\"\n >\n {tr('inbox.message.markRead', 'Mark read')}\n </button>\n )}\n </div>\n </div>\n </div>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Reading pane (wide screens)\n// ---------------------------------------------------------------------------\n\nfunction ReadingPane({\n message,\n channel,\n onMarkRead,\n markPending,\n onViewRelationship,\n}: {\n message: SubInboxMessage | null;\n channel: SubCommunicationChannel | undefined;\n onMarkRead: () => void;\n markPending: boolean;\n onViewRelationship: (() => void) | null;\n}) {\n const tr = useTr();\n if (!message) {\n return (\n <GlassCard\n treatment=\"metric\"\n className=\"flex min-h-[20rem] flex-col items-center justify-center p-8 text-center\"\n >\n <MousePointerClick className=\"h-10 w-10 text-text-secondary\" />\n <p className=\"mt-3 text-sm font-medium text-text-primary\">\n {tr('inbox.readingPane.selectTitle', 'Select a message')}\n </p>\n <p className=\"mt-1 max-w-[16rem] text-xs text-text-secondary\">\n {tr(\n 'inbox.readingPane.selectDescription',\n 'Pick any message from the list to read the full details here.'\n )}\n </p>\n </GlassCard>\n );\n }\n\n const Icon = channel ? (CHANNEL_ICON[channel.type] ?? Radio) : Radio;\n const confidencePct =\n message.categoryConfidence != null\n ? Math.round(Math.max(0, Math.min(1, message.categoryConfidence)) * 100)\n : null;\n\n return (\n <GlassCard treatment=\"metric\" className=\"flex flex-col p-5\">\n <div className=\"flex items-start justify-between gap-3\">\n <h3 className=\"text-base font-semibold text-text-primary\">\n {message.subject || tr('inbox.message.noSubject', '(no subject)')}\n </h3>\n {!message.isRead && (\n <button\n type=\"button\"\n disabled={markPending}\n onClick={onMarkRead}\n className=\"shrink-0 rounded-lg border border-border-subtle px-3 py-1.5 text-xs font-medium text-text-primary hover:bg-bg-sunken disabled:opacity-50\"\n >\n {tr('inbox.message.markRead', 'Mark read')}\n </button>\n )}\n </div>\n\n {channel && (\n <div className=\"mt-2 flex items-center gap-2 text-sm\">\n <Icon className=\"h-4 w-4 shrink-0 text-text-secondary\" />\n <span className=\"truncate text-text-secondary\" title={channel.address}>\n {channel.address}\n </span>\n </div>\n )}\n\n <div className=\"mt-3 flex flex-wrap items-center gap-2\">\n <CategoryTag category={message.category} />\n {message.actionRequired && <ActionFlag />}\n <span className=\"text-xs text-text-secondary\">{formatDateTime(message.receivedAt)}</span>\n </div>\n\n {confidencePct != null && (\n <div className=\"mt-4 rounded-lg border border-border-subtle bg-bg-surface p-3\">\n <div className=\"flex items-center justify-between text-xs\">\n <span className=\"inline-flex items-center gap-1.5 font-medium text-text-primary\">\n <Sparkles className=\"h-3.5 w-3.5 text-text-link\" />{' '}\n {tr('inbox.readingPane.aiCategorization', 'AI categorization')}\n </span>\n <span className=\"tabular-nums text-text-secondary\">\n {tr('inbox.readingPane.confidence', '{{pct}}% confidence', { pct: confidencePct })}\n </span>\n </div>\n <MiniProgress\n value={confidencePct}\n max={100}\n tone={confidencePct >= 66 ? 'positive' : confidencePct >= 33 ? 'accent' : 'warning'}\n className=\"mt-2\"\n />\n </div>\n )}\n\n <div className=\"mt-4 border-t border-border-subtle pt-4\">\n {message.preview ? (\n <p className=\"whitespace-pre-wrap text-sm leading-relaxed text-text-primary\">\n {message.preview}\n </p>\n ) : (\n <p className=\"text-sm italic text-text-secondary\">\n {tr('inbox.readingPane.noPreview', 'No preview available for this message.')}\n </p>\n )}\n </div>\n\n {message.actionRequired && (\n <div className=\"mt-4 flex items-start gap-2 rounded-lg border border-status-warning-text/30 bg-status-warning-bg-subtle p-3\">\n <ShieldAlert className=\"mt-0.5 h-4 w-4 shrink-0 text-status-warning-text\" />\n <p className=\"text-xs text-status-warning-text\">\n {tr(\n 'inbox.readingPane.attention',\n 'This message needs your attention — review it before its provider deadline.'\n )}\n </p>\n </div>\n )}\n\n {onViewRelationship && (\n <button\n type=\"button\"\n onClick={onViewRelationship}\n className=\"mt-4 inline-flex items-center gap-1 self-start text-sm font-medium text-text-link hover:underline\"\n >\n {tr('inbox.message.viewRelationship', 'View relationship')}{' '}\n <ChevronRight className=\"h-4 w-4\" />\n </button>\n )}\n </GlassCard>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Filter chip\n// ---------------------------------------------------------------------------\n\nfunction Chip({\n active,\n onClick,\n children,\n tone = 'default',\n}: {\n active: boolean;\n onClick: () => void;\n children: React.ReactNode;\n tone?: 'default' | 'warning';\n}) {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n aria-pressed={active}\n className={cn(\n 'inline-flex items-center gap-1.5 rounded-full border px-3 py-1.5 text-xs font-medium transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]',\n active\n ? tone === 'warning'\n ? 'border-transparent bg-status-warning-bg-subtle text-status-warning-text'\n : 'border-transparent bg-action-primary-bg text-action-primary-text'\n : 'border-border-subtle text-text-secondary hover:bg-bg-sunken hover:text-text-primary'\n )}\n >\n {children}\n </button>\n );\n}\n\nexport function InboxPage() {\n const tr = useTr();\n const { navigateTo } = useSubscriberBot();\n const [page, setPage] = useState(0);\n const [category, setCategory] = useState<'' | SubMessageCategory>('');\n const [unreadOnly, setUnreadOnly] = useState(false);\n const [actionOnly, setActionOnly] = useState(false);\n const [selectedId, setSelectedId] = useState<string | null>(null);\n\n const { data, isLoading, isError, refetch, isFetching } = useInboxMessages({\n category: category || undefined,\n unreadOnly: unreadOnly || undefined,\n pagination: { limit: PAGE_SIZE, offset: page * PAGE_SIZE },\n });\n const { data: channelsData } = useChannels();\n const markRead = useMarkMessageRead();\n\n const items = data?.items ?? [];\n const hasMore = data?.pageInfo.hasMore ?? false;\n\n // \"Action required\" is a client-side lens over the current page.\n const displayed = actionOnly ? items.filter((m) => m.actionRequired) : items;\n\n // Real, computed page stats.\n const unreadCount = items.filter((m) => !m.isRead).length;\n const actionCount = items.filter((m) => m.actionRequired).length;\n\n const channelMap = new Map((channelsData ?? []).map((c) => [c.id, c]));\n const selected = displayed.find((m) => m.id === selectedId) ?? null;\n const selectedChannel = selected ? channelMap.get(selected.channelId) : undefined;\n\n const resetToFirst = () => {\n setPage(0);\n setSelectedId(null);\n };\n\n return (\n <PageLayout\n title={tr('inbox.title', 'Universal Inbox')}\n description={tr(\n 'inbox.description',\n 'Every subscription communication, merged and categorized'\n )}\n >\n <PagePurpose className=\"mb-4\">\n {tr(\n 'inbox.purpose',\n 'Subscription communication is noisy and scattered. The Universal Inbox merges every channel a provider uses and uses AI to categorize each message so you never miss a renewal, required action or security notice.'\n )}\n </PagePurpose>\n\n {/* Category tabs */}\n <div className=\"-mx-1 flex gap-2 overflow-x-auto px-1 pb-1\">\n {CATEGORY_TAB_VALUES.map((t) => (\n <span key={t.value} className=\"shrink-0\">\n <Chip\n active={category === t.value}\n onClick={() => {\n setCategory(t.value);\n resetToFirst();\n }}\n >\n {tr(t.key, t.fallback)}\n </Chip>\n </span>\n ))}\n </div>\n\n {/* Lenses + stats */}\n <div className=\"mt-3 flex flex-wrap items-center gap-2\">\n <Chip\n active={actionOnly}\n tone=\"warning\"\n onClick={() => {\n setActionOnly((v) => !v);\n setSelectedId(null);\n }}\n >\n <AlertTriangle className=\"h-3.5 w-3.5\" />{' '}\n {tr('inbox.filters.actionRequired', 'Action required')}\n </Chip>\n <Chip\n active={unreadOnly}\n onClick={() => {\n setUnreadOnly((v) => !v);\n resetToFirst();\n }}\n >\n <Mail className=\"h-3.5 w-3.5\" /> {tr('inbox.filters.unreadOnly', 'Unread only')}\n </Chip>\n {!isLoading && !isError && items.length > 0 && (\n <p className=\"ml-auto text-xs text-text-secondary\">\n {tr('inbox.filters.unreadCount', '{{count}} unread', { count: unreadCount })}\n {' · '}\n {tr('inbox.filters.actionCount', '{{count}} need action', { count: actionCount })}\n {isFetching ? ` · ${tr('inbox.filters.refreshing', 'refreshing…')}` : ''}\n </p>\n )}\n </div>\n\n {isLoading ? (\n <LoadingView label={tr('inbox.loading', 'Loading inbox…')} />\n ) : isError ? (\n <ErrorView\n message={tr('inbox.error', 'Could not load the inbox.')}\n onRetry={() => void refetch()}\n />\n ) : items.length === 0 ? (\n <div className=\"mt-6\">\n <EmptyState\n illustration=\"empty-inbox\"\n icon={<InboxIcon className=\"h-12 w-12\" />}\n title={tr('inbox.emptyTitle', 'Inbox is empty')}\n description={\n unreadOnly || category\n ? tr('inbox.emptyDescriptionFiltered', 'No messages match these filters.')\n : tr(\n 'inbox.emptyDescription',\n 'Merged subscription communications will appear here once channels start ingesting.'\n )\n }\n />\n </div>\n ) : (\n <div className=\"mt-5 grid grid-cols-1 gap-5 lg:grid-cols-[minmax(0,1fr)_380px]\">\n {/* List */}\n <div className=\"min-w-0\">\n {displayed.length === 0 ? (\n <div className=\"rounded-xl border border-border-subtle bg-bg-surface p-8 text-center\">\n <AlertTriangle className=\"mx-auto h-8 w-8 text-text-secondary\" />\n <p className=\"mt-2 text-sm font-medium text-text-primary\">\n {tr('inbox.noActionTitle', 'No action-required messages on this page')}\n </p>\n <p className=\"mt-1 text-xs text-text-secondary\">\n {tr(\n 'inbox.noActionDescription',\n 'Turn off the “Action required” lens or move to another page.'\n )}\n </p>\n </div>\n ) : (\n <div className=\"divide-y divide-border-subtle overflow-hidden rounded-xl border border-border-subtle bg-bg-surface\">\n {displayed.map((m) => (\n <MessageRow\n key={m.id}\n message={m}\n active={m.id === selectedId}\n onSelect={() => setSelectedId(m.id)}\n onMarkRead={() => markRead.mutate(m.id)}\n markPending={markRead.isPending}\n onViewRelationship={\n m.relationshipId\n ? () => navigateTo(`/relationships/${m.relationshipId}`)\n : null\n }\n />\n ))}\n </div>\n )}\n\n <div className=\"mt-4 flex items-center justify-between\">\n <p className=\"text-xs text-text-secondary\">\n {tr('inbox.pagination.page', 'Page {{page}}', { page: page + 1 })}\n {isFetching ? ` · ${tr('inbox.filters.refreshing', 'refreshing…')}` : ''}\n </p>\n <div className=\"flex gap-2\">\n <button\n type=\"button\"\n disabled={page === 0}\n onClick={() => {\n setPage((p) => Math.max(0, p - 1));\n setSelectedId(null);\n }}\n className={cn(\n 'rounded-lg border border-border-subtle px-3 py-1.5 text-xs font-medium',\n page === 0\n ? 'cursor-not-allowed text-text-secondary opacity-50'\n : 'text-text-primary hover:bg-bg-sunken'\n )}\n >\n {tr('inbox.pagination.previous', 'Previous')}\n </button>\n <button\n type=\"button\"\n disabled={!hasMore}\n onClick={() => {\n setPage((p) => p + 1);\n setSelectedId(null);\n }}\n className={cn(\n 'rounded-lg border border-border-subtle px-3 py-1.5 text-xs font-medium',\n !hasMore\n ? 'cursor-not-allowed text-text-secondary opacity-50'\n : 'text-text-primary hover:bg-bg-sunken'\n )}\n >\n {tr('inbox.pagination.next', 'Next')}\n </button>\n </div>\n </div>\n </div>\n\n {/* Reading pane — wide screens only */}\n <aside className=\"hidden lg:block\">\n <div className=\"lg:sticky lg:top-4\">\n <ReadingPane\n message={selected}\n channel={selectedChannel}\n onMarkRead={() => selected && markRead.mutate(selected.id)}\n markPending={markRead.isPending}\n onViewRelationship={\n selected?.relationshipId\n ? () => navigateTo(`/relationships/${selected.relationshipId}`)\n : null\n }\n />\n </div>\n </aside>\n </div>\n )}\n\n <ChannelsSection />\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"InboxPage.js","names":[],"sources":["../../../src/subscriberbot/pages/InboxPage.tsx"],"sourcesContent":["/**\n * Universal Inbox\n * @module subscriberbot/pages\n *\n * Every merged subscription communication in one timeline, AI-categorized,\n * plus management of the ingest channels that feed it. Filter by category /\n * unread, mark messages read, add & verify channels, and log a test message.\n * Reads `subListInboxMessages` / `subListChannels`; writes `subMarkMessageRead`,\n * `subCreateChannel`, `subVerifyChannel`, `subIngestMessage`.\n */\n\nimport { useState } from 'react';\nimport {\n Mail,\n MailOpen,\n AlertTriangle,\n Inbox as InboxIcon,\n Plus,\n Radio,\n BadgeCheck,\n Send,\n MessageSquare,\n MessagesSquare,\n Bell,\n Webhook,\n Phone,\n Hash,\n ShieldAlert,\n Sparkles,\n ChevronRight,\n MousePointerClick,\n type LucideIcon,\n} from 'lucide-react';\nimport { PagePurpose, GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../components/PageLayout';\nimport { StatusBadge } from '../components/StatusBadge';\nimport { EmptyState } from '../components/EmptyState';\nimport { Modal } from '../components/Modal';\nimport { LoadingView, ErrorView, Section } from '../components/StateViews';\nimport { MiniProgress } from '../components/viz';\nimport { useSubscriberBot } from '../context/SubscriberBotContext';\nimport {\n useInboxMessages,\n useMarkMessageRead,\n useChannels,\n useCreateChannel,\n useVerifyChannel,\n useIngestMessage,\n} from '../hooks/useSubscriberBotApi';\nimport { useTr } from '../hooks/useTr';\nimport { cn } from '../utils/cn';\nimport { formatEnumLabel, formatRelativeTime, formatDateTime } from '../utils/formatters';\nimport type {\n SubChannelType,\n SubCommunicationChannel,\n SubInboxMessage,\n SubMessageCategory,\n} from '../types';\n\nconst PAGE_SIZE = 25;\n\nconst inputCls =\n 'w-full rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]';\nconst labelCls = 'mb-1 block text-xs font-medium text-text-secondary';\n\nconst MESSAGE_CATEGORIES: SubMessageCategory[] = [\n 'BILLING',\n 'RENEWAL',\n 'REQUIRED_ACTION',\n 'SECURITY_ALERT',\n 'PRODUCT_UPDATE',\n 'MARKETING',\n 'OTHER',\n];\n\nconst CHANNEL_TYPES: SubChannelType[] = [\n 'EMAIL',\n 'SMS',\n 'SLACK',\n 'DISCORD',\n 'WHATSAPP',\n 'PUSH',\n 'WEBHOOK',\n];\n\nconst CATEGORY_KEY: Record<SubMessageCategory, string> = {\n BILLING: 'inbox.categories.billing',\n RENEWAL: 'inbox.categories.renewal',\n REQUIRED_ACTION: 'inbox.categories.requiredAction',\n SECURITY_ALERT: 'inbox.categories.securityAlert',\n PRODUCT_UPDATE: 'inbox.categories.productUpdate',\n MARKETING: 'inbox.categories.marketing',\n OTHER: 'inbox.categories.other',\n};\n\n// Category filter tabs (chips) — the \"All\" pseudo-value plus the real enum.\n// Labels are resolved via useTr inside the page component.\nconst CATEGORY_TAB_VALUES: Array<{\n value: '' | SubMessageCategory;\n key: string;\n fallback: string;\n}> = [\n { value: '', key: 'inbox.categories.all', fallback: 'All' },\n { value: 'BILLING', key: CATEGORY_KEY.BILLING, fallback: 'Billing' },\n { value: 'RENEWAL', key: CATEGORY_KEY.RENEWAL, fallback: 'Renewals' },\n { value: 'REQUIRED_ACTION', key: CATEGORY_KEY.REQUIRED_ACTION, fallback: 'Required actions' },\n { value: 'SECURITY_ALERT', key: CATEGORY_KEY.SECURITY_ALERT, fallback: 'Security' },\n { value: 'PRODUCT_UPDATE', key: CATEGORY_KEY.PRODUCT_UPDATE, fallback: 'Product updates' },\n { value: 'MARKETING', key: CATEGORY_KEY.MARKETING, fallback: 'Marketing' },\n { value: 'OTHER', key: CATEGORY_KEY.OTHER, fallback: 'Other' },\n];\n\n// Per-category tag tone — semantic tokens only.\nconst CATEGORY_TONE: Record<SubMessageCategory, string> = {\n BILLING: 'bg-[var(--color-accent-soft)] text-text-primary',\n RENEWAL: 'bg-status-success-bg-subtle text-status-success-text',\n REQUIRED_ACTION: 'bg-status-warning-bg-subtle text-status-warning-text',\n SECURITY_ALERT: 'bg-status-error-bg-subtle text-status-error-text',\n PRODUCT_UPDATE: 'bg-[var(--color-accent-soft)] text-text-primary',\n MARKETING: 'bg-bg-sunken text-text-secondary',\n OTHER: 'bg-bg-sunken text-text-secondary',\n};\n\nconst CHANNEL_ICON: Record<SubChannelType, LucideIcon> = {\n EMAIL: Mail,\n SMS: MessageSquare,\n SLACK: Hash,\n DISCORD: MessagesSquare,\n WHATSAPP: Phone,\n PUSH: Bell,\n WEBHOOK: Webhook,\n};\n\nfunction CategoryTag({ category }: { category: SubMessageCategory }) {\n return (\n <span\n className={cn(\n 'inline-flex items-center rounded-full px-2 py-0.5 text-[11px] font-medium',\n CATEGORY_TONE[category]\n )}\n >\n {formatEnumLabel(category)}\n </span>\n );\n}\n\nfunction ActionFlag({ className }: { className?: string }) {\n const tr = useTr();\n return (\n <span\n title={tr('inbox.message.actionRequiredTitle', 'Action required')}\n className={cn(\n 'inline-flex items-center gap-1 rounded-full bg-status-warning-bg-subtle px-2 py-0.5 text-[10px] font-medium text-status-warning-text',\n className\n )}\n >\n <AlertTriangle className=\"h-3 w-3\" /> {tr('inbox.message.actionFlag', 'Action')}\n </span>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Add channel modal → subCreateChannel\n// ---------------------------------------------------------------------------\n\nfunction AddChannelModal({ onClose }: { onClose: () => void }) {\n const tr = useTr();\n const create = useCreateChannel();\n const [type, setType] = useState<SubChannelType>('EMAIL');\n const [address, setAddress] = useState('');\n const [label, setLabel] = useState('');\n const [ingestEnabled, setIngestEnabled] = useState(true);\n const [formError, setFormError] = useState<string | null>(null);\n\n const handleSubmit = (e: React.FormEvent) => {\n e.preventDefault();\n setFormError(null);\n if (!address.trim()) {\n setFormError(tr('inbox.addChannelModal.addressRequired', 'An address is required.'));\n return;\n }\n create.mutate(\n {\n type,\n address: address.trim(),\n ingestEnabled,\n ...(label.trim() ? { label: label.trim() } : {}),\n },\n {\n onSuccess: () => onClose(),\n onError: (err) =>\n setFormError(\n err instanceof Error\n ? err.message\n : tr('inbox.addChannelModal.error', 'Could not add channel.')\n ),\n }\n );\n };\n\n return (\n <Modal\n title={tr('inbox.addChannelModal.title', 'Add channel')}\n onClose={onClose}\n maxWidthClassName=\"max-w-xl\"\n >\n <form onSubmit={handleSubmit} className=\"space-y-4\">\n {formError && (\n <div className=\"rounded-lg border border-status-error-text/30 bg-status-error-bg-subtle px-4 py-2 text-sm text-status-error-text\">\n {formError}\n </div>\n )}\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <div>\n <label htmlFor=\"ch-type\" className={labelCls}>\n {tr('inbox.addChannelModal.typeLabel', 'Type')}\n </label>\n <select\n id=\"ch-type\"\n value={type}\n onChange={(e) => setType(e.target.value as SubChannelType)}\n className={inputCls}\n >\n {CHANNEL_TYPES.map((t) => (\n <option key={t} value={t}>\n {formatEnumLabel(t)}\n </option>\n ))}\n </select>\n </div>\n <div>\n <label htmlFor=\"ch-label\" className={labelCls}>\n {tr('inbox.addChannelModal.labelLabel', 'Label')}\n </label>\n <input\n id=\"ch-label\"\n type=\"text\"\n value={label}\n onChange={(e) => setLabel(e.target.value)}\n placeholder={tr('inbox.addChannelModal.labelPlaceholder', 'e.g. Billing inbox')}\n className={inputCls}\n />\n </div>\n </div>\n <div>\n <label htmlFor=\"ch-address\" className={labelCls}>\n {tr('inbox.addChannelModal.addressLabel', 'Address')}{' '}\n <span className=\"text-status-error-text\">*</span>\n </label>\n <input\n id=\"ch-address\"\n type=\"text\"\n value={address}\n onChange={(e) => setAddress(e.target.value)}\n placeholder={tr(\n 'inbox.addChannelModal.addressPlaceholder',\n 'e.g. billing@acme.com, +1…, a webhook URL'\n )}\n className={inputCls}\n required\n />\n </div>\n <label className=\"flex items-center gap-2 text-sm text-text-primary\">\n <input\n type=\"checkbox\"\n checked={ingestEnabled}\n onChange={(e) => setIngestEnabled(e.target.checked)}\n className=\"size-4 rounded border-border-subtle\"\n />\n {tr('inbox.addChannelModal.ingestLabel', 'Ingest messages from this channel')}\n </label>\n <div className=\"flex justify-end gap-3\">\n <button\n type=\"button\"\n onClick={onClose}\n className=\"rounded-lg border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken\"\n >\n {tr('inbox.addChannelModal.cancel', 'Cancel')}\n </button>\n <button\n type=\"submit\"\n disabled={create.isPending}\n className=\"rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:opacity-90 disabled:opacity-50\"\n >\n {create.isPending\n ? tr('inbox.addChannelModal.adding', 'Adding…')\n : tr('inbox.addChannelModal.submit', 'Add channel')}\n </button>\n </div>\n </form>\n </Modal>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Log test message modal → subIngestMessage\n// ---------------------------------------------------------------------------\n\nfunction LogMessageModal({\n channel,\n onClose,\n}: {\n channel: SubCommunicationChannel;\n onClose: () => void;\n}) {\n const tr = useTr();\n const ingest = useIngestMessage();\n const [subject, setSubject] = useState('');\n const [preview, setPreview] = useState('');\n const [category, setCategory] = useState<SubMessageCategory>('OTHER');\n const [actionRequired, setActionRequired] = useState(false);\n const [formError, setFormError] = useState<string | null>(null);\n\n const handleSubmit = (e: React.FormEvent) => {\n e.preventDefault();\n setFormError(null);\n ingest.mutate(\n {\n channelId: channel.id,\n receivedAt: new Date().toISOString(),\n category,\n actionRequired,\n ...(subject.trim() ? { subject: subject.trim() } : {}),\n ...(preview.trim() ? { preview: preview.trim() } : {}),\n },\n {\n onSuccess: () => onClose(),\n onError: (err) =>\n setFormError(\n err instanceof Error\n ? err.message\n : tr('inbox.logMessageModal.error', 'Could not log the message.')\n ),\n }\n );\n };\n\n return (\n <Modal\n title={tr('inbox.logMessageModal.title', 'Log test message')}\n description={tr(\n 'inbox.logMessageModal.description',\n 'Simulate an ingested message from {channel}.',\n { channel: channel.label || channel.address }\n )}\n onClose={onClose}\n >\n <form onSubmit={handleSubmit} className=\"space-y-4\">\n {formError && (\n <div className=\"rounded-lg border border-status-error-text/30 bg-status-error-bg-subtle px-4 py-2 text-sm text-status-error-text\">\n {formError}\n </div>\n )}\n <div>\n <label htmlFor=\"msg-subject\" className={labelCls}>\n {tr('inbox.logMessageModal.subjectLabel', 'Subject')}\n </label>\n <input\n id=\"msg-subject\"\n type=\"text\"\n value={subject}\n onChange={(e) => setSubject(e.target.value)}\n placeholder={tr(\n 'inbox.logMessageModal.subjectPlaceholder',\n 'e.g. Your plan renews in 3 days'\n )}\n className={inputCls}\n />\n </div>\n <div>\n <label htmlFor=\"msg-preview\" className={labelCls}>\n {tr('inbox.logMessageModal.previewLabel', 'Preview')}\n </label>\n <textarea\n id=\"msg-preview\"\n value={preview}\n onChange={(e) => setPreview(e.target.value)}\n rows={2}\n className={inputCls}\n />\n </div>\n <div>\n <label htmlFor=\"msg-category\" className={labelCls}>\n {tr('inbox.logMessageModal.categoryLabel', 'Category')}\n </label>\n <select\n id=\"msg-category\"\n value={category}\n onChange={(e) => setCategory(e.target.value as SubMessageCategory)}\n className={inputCls}\n >\n {MESSAGE_CATEGORIES.map((c) => (\n <option key={c} value={c}>\n {tr(CATEGORY_KEY[c], formatEnumLabel(c))}\n </option>\n ))}\n </select>\n </div>\n <label className=\"flex items-center gap-2 text-sm text-text-primary\">\n <input\n type=\"checkbox\"\n checked={actionRequired}\n onChange={(e) => setActionRequired(e.target.checked)}\n className=\"size-4 rounded border-border-subtle\"\n />\n {tr('inbox.logMessageModal.actionRequired', 'Requires action')}\n </label>\n <div className=\"flex justify-end gap-3\">\n <button\n type=\"button\"\n onClick={onClose}\n className=\"rounded-lg border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken\"\n >\n {tr('inbox.logMessageModal.cancel', 'Cancel')}\n </button>\n <button\n type=\"submit\"\n disabled={ingest.isPending}\n className=\"rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:opacity-90 disabled:opacity-50\"\n >\n {ingest.isPending\n ? tr('inbox.logMessageModal.logging', 'Logging…')\n : tr('inbox.logMessageModal.submit', 'Log message')}\n </button>\n </div>\n </form>\n </Modal>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Channel card → one per ingest source\n// ---------------------------------------------------------------------------\n\nfunction ChannelCard({\n channel,\n onVerify,\n verifying,\n onLog,\n}: {\n channel: SubCommunicationChannel;\n onVerify: (id: string) => void;\n verifying: boolean;\n onLog: (ch: SubCommunicationChannel) => void;\n}) {\n const tr = useTr();\n const Icon = CHANNEL_ICON[channel.type] ?? Radio;\n const verified = channel.status === 'VERIFIED';\n return (\n <GlassCard treatment=\"metric\" className=\"flex flex-col p-4\">\n <div className=\"flex items-start justify-between gap-2\">\n <div className=\"flex min-w-0 items-center gap-2.5\">\n <span className=\"flex size-9 shrink-0 items-center justify-center rounded-lg bg-[var(--color-accent-soft)] text-text-primary\">\n <Icon className=\"h-4 w-4\" />\n </span>\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-semibold text-text-primary\">\n {formatEnumLabel(channel.type)}\n </p>\n {channel.label && (\n <p className=\"truncate text-xs text-text-secondary\">{channel.label}</p>\n )}\n </div>\n </div>\n <StatusBadge status={channel.status} />\n </div>\n\n <div className=\"mt-3 border-t border-border-subtle pt-3\">\n <p className=\"text-[11px] uppercase tracking-wide text-text-secondary\">\n {channel.ingestEnabled\n ? tr('inbox.channels.ingestingFrom', 'Ingesting from')\n : tr('inbox.channels.ingestPaused', 'Ingest paused')}\n </p>\n <p className=\"mt-0.5 truncate text-sm text-text-primary\" title={channel.address}>\n {channel.address}\n </p>\n </div>\n\n <div className=\"mt-auto flex flex-wrap items-center gap-2 pt-3\">\n {channel.status === 'PENDING' && (\n <button\n type=\"button\"\n disabled={verifying}\n onClick={() => onVerify(channel.id)}\n className=\"inline-flex items-center gap-1 rounded-lg bg-action-primary-bg px-3 py-1.5 text-xs font-medium text-action-primary-text hover:opacity-90 disabled:opacity-50\"\n >\n <BadgeCheck className=\"h-3.5 w-3.5\" /> {tr('inbox.channels.verify', 'Verify')}\n </button>\n )}\n {verified && channel.ingestEnabled && (\n <button\n type=\"button\"\n onClick={() => onLog(channel)}\n className=\"inline-flex items-center gap-1 rounded-lg border border-border-subtle px-3 py-1.5 text-xs font-medium text-text-primary hover:bg-bg-sunken\"\n >\n <Send className=\"h-3.5 w-3.5\" />{' '}\n {tr('inbox.channels.logTestMessage', 'Log test message')}\n </button>\n )}\n {channel.status === 'DISABLED' && (\n <span className=\"text-xs text-text-secondary\">\n {tr('inbox.channels.disabled', 'Channel disabled')}\n </span>\n )}\n </div>\n </GlassCard>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Channels management panel\n// ---------------------------------------------------------------------------\n\nfunction ChannelsSection() {\n const tr = useTr();\n const { data, isLoading, isError, refetch } = useChannels();\n const verify = useVerifyChannel();\n const [showAdd, setShowAdd] = useState(false);\n const [logChannel, setLogChannel] = useState<SubCommunicationChannel | null>(null);\n\n const channels = data ?? [];\n const verifiedCount = channels.filter((c) => c.status === 'VERIFIED').length;\n const pendingCount = channels.filter((c) => c.status === 'PENDING').length;\n\n return (\n <Section\n title={tr('inbox.channels.title', 'Ingest channels')}\n description={tr(\n 'inbox.channels.description',\n 'The sources the Universal Inbox pulls subscription communication from'\n )}\n action={\n <button\n type=\"button\"\n onClick={() => setShowAdd(true)}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-3 py-1.5 text-xs font-medium text-action-primary-text hover:opacity-90\"\n >\n <Plus className=\"h-3.5 w-3.5\" /> {tr('inbox.channels.add', 'Add channel')}\n </button>\n }\n >\n {isLoading ? (\n <LoadingView label={tr('inbox.channels.loading', 'Loading channels…')} />\n ) : isError ? (\n <ErrorView\n message={tr('inbox.channels.error', 'Could not load channels.')}\n onRetry={() => void refetch()}\n />\n ) : channels.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Radio className=\"h-12 w-12\" />}\n title={tr('inbox.channels.emptyTitle', 'No channels yet')}\n description={tr(\n 'inbox.channels.emptyDescription',\n 'Add an email, SMS, Slack or webhook channel to start ingesting subscription communication.'\n )}\n />\n ) : (\n <>\n {(verifiedCount > 0 || pendingCount > 0) && (\n <div className=\"mb-3 flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-text-secondary\">\n <span className=\"inline-flex items-center gap-1.5\">\n <span className=\"size-1.5 rounded-full bg-status-success-text\" aria-hidden />\n {tr('inbox.channels.verified', '{{count}} verified', { count: verifiedCount })}\n </span>\n {pendingCount > 0 && (\n <span className=\"inline-flex items-center gap-1.5\">\n <span className=\"size-1.5 rounded-full bg-status-warning-text\" aria-hidden />\n {tr('inbox.channels.pending', '{{count}} pending verification', {\n count: pendingCount,\n })}\n </span>\n )}\n </div>\n )}\n <div className=\"grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3\">\n {channels.map((ch) => (\n <ChannelCard\n key={ch.id}\n channel={ch}\n verifying={verify.isPending}\n onVerify={(id) => verify.mutate(id)}\n onLog={(c) => setLogChannel(c)}\n />\n ))}\n </div>\n </>\n )}\n\n {showAdd && <AddChannelModal onClose={() => setShowAdd(false)} />}\n {logChannel && <LogMessageModal channel={logChannel} onClose={() => setLogChannel(null)} />}\n </Section>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Message row (list)\n// ---------------------------------------------------------------------------\n\nfunction MessageRow({\n message,\n active,\n onSelect,\n onMarkRead,\n markPending,\n onViewRelationship,\n}: {\n message: SubInboxMessage;\n active: boolean;\n onSelect: () => void;\n onMarkRead: () => void;\n markPending: boolean;\n onViewRelationship: (() => void) | null;\n}) {\n const tr = useTr();\n const unread = !message.isRead;\n return (\n <div\n role=\"button\"\n tabIndex={0}\n aria-pressed={active}\n onClick={onSelect}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n onSelect();\n }\n }}\n className={cn(\n 'relative flex cursor-pointer items-start gap-3 p-4 transition-colors',\n 'focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)] focus-visible:ring-inset',\n active ? 'bg-bg-sunken' : unread ? 'bg-accent-wash' : 'hover:bg-bg-sunken/60'\n )}\n >\n {/* unread accent rail */}\n {unread && <span aria-hidden className=\"absolute inset-y-0 left-0 w-0.5 bg-text-link\" />}\n <span className=\"mt-0.5 shrink-0 text-text-secondary\">\n {unread ? <Mail className=\"h-4 w-4 text-text-link\" /> : <MailOpen className=\"h-4 w-4\" />}\n </span>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-center gap-2\">\n {unread && (\n <span\n aria-label={tr('inbox.message.unreadAria', 'Unread')}\n className=\"size-1.5 shrink-0 rounded-full bg-text-link\"\n />\n )}\n <p\n className={cn(\n 'min-w-0 flex-1 truncate text-sm',\n unread ? 'font-semibold text-text-primary' : 'font-normal text-text-primary'\n )}\n >\n {message.subject || tr('inbox.message.noSubject', '(no subject)')}\n </p>\n <span className=\"shrink-0 text-xs tabular-nums text-text-secondary\">\n {formatRelativeTime(message.receivedAt)}\n </span>\n </div>\n {message.preview && (\n <p className=\"mt-0.5 line-clamp-2 text-xs text-text-secondary\">{message.preview}</p>\n )}\n <div className=\"mt-1.5 flex flex-wrap items-center gap-2\">\n <CategoryTag category={message.category} />\n {message.actionRequired && <ActionFlag />}\n {onViewRelationship && (\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation();\n onViewRelationship();\n }}\n className=\"text-xs font-medium text-text-link hover:underline\"\n >\n {tr('inbox.message.viewRelationship', 'View relationship')}\n </button>\n )}\n {unread && (\n <button\n type=\"button\"\n disabled={markPending}\n onClick={(e) => {\n e.stopPropagation();\n onMarkRead();\n }}\n className=\"ml-auto shrink-0 rounded-lg border border-border-subtle px-2.5 py-1 text-xs font-medium text-text-primary hover:bg-bg-sunken disabled:opacity-50\"\n >\n {tr('inbox.message.markRead', 'Mark read')}\n </button>\n )}\n </div>\n </div>\n </div>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Reading pane (wide screens)\n// ---------------------------------------------------------------------------\n\nfunction ReadingPane({\n message,\n channel,\n onMarkRead,\n markPending,\n onViewRelationship,\n}: {\n message: SubInboxMessage | null;\n channel: SubCommunicationChannel | undefined;\n onMarkRead: () => void;\n markPending: boolean;\n onViewRelationship: (() => void) | null;\n}) {\n const tr = useTr();\n if (!message) {\n return (\n <GlassCard\n treatment=\"metric\"\n className=\"flex min-h-[20rem] flex-col items-center justify-center p-8 text-center\"\n >\n <MousePointerClick className=\"h-10 w-10 text-text-secondary\" />\n <p className=\"mt-3 text-sm font-medium text-text-primary\">\n {tr('inbox.readingPane.selectTitle', 'Select a message')}\n </p>\n <p className=\"mt-1 max-w-[16rem] text-xs text-text-secondary\">\n {tr(\n 'inbox.readingPane.selectDescription',\n 'Pick any message from the list to read the full details here.'\n )}\n </p>\n </GlassCard>\n );\n }\n\n const Icon = channel ? (CHANNEL_ICON[channel.type] ?? Radio) : Radio;\n const confidencePct =\n message.categoryConfidence != null\n ? Math.round(Math.max(0, Math.min(1, message.categoryConfidence)) * 100)\n : null;\n\n return (\n <GlassCard treatment=\"metric\" className=\"flex flex-col p-5\">\n <div className=\"flex items-start justify-between gap-3\">\n <h3 className=\"text-base font-semibold text-text-primary\">\n {message.subject || tr('inbox.message.noSubject', '(no subject)')}\n </h3>\n {!message.isRead && (\n <button\n type=\"button\"\n disabled={markPending}\n onClick={onMarkRead}\n className=\"shrink-0 rounded-lg border border-border-subtle px-3 py-1.5 text-xs font-medium text-text-primary hover:bg-bg-sunken disabled:opacity-50\"\n >\n {tr('inbox.message.markRead', 'Mark read')}\n </button>\n )}\n </div>\n\n {channel && (\n <div className=\"mt-2 flex items-center gap-2 text-sm\">\n <Icon className=\"h-4 w-4 shrink-0 text-text-secondary\" />\n <span className=\"truncate text-text-secondary\" title={channel.address}>\n {channel.address}\n </span>\n </div>\n )}\n\n <div className=\"mt-3 flex flex-wrap items-center gap-2\">\n <CategoryTag category={message.category} />\n {message.actionRequired && <ActionFlag />}\n <span className=\"text-xs text-text-secondary\">{formatDateTime(message.receivedAt)}</span>\n </div>\n\n {confidencePct != null && (\n <div className=\"mt-4 rounded-lg border border-border-subtle bg-bg-surface p-3\">\n <div className=\"flex items-center justify-between text-xs\">\n <span className=\"inline-flex items-center gap-1.5 font-medium text-text-primary\">\n <Sparkles className=\"h-3.5 w-3.5 text-text-link\" />{' '}\n {tr('inbox.readingPane.aiCategorization', 'AI categorization')}\n </span>\n <span className=\"tabular-nums text-text-secondary\">\n {tr('inbox.readingPane.confidence', '{{pct}}% confidence', { pct: confidencePct })}\n </span>\n </div>\n <MiniProgress\n value={confidencePct}\n max={100}\n tone={confidencePct >= 66 ? 'positive' : confidencePct >= 33 ? 'accent' : 'warning'}\n className=\"mt-2\"\n />\n </div>\n )}\n\n <div className=\"mt-4 border-t border-border-subtle pt-4\">\n {message.preview ? (\n <p className=\"whitespace-pre-wrap text-sm leading-relaxed text-text-primary\">\n {message.preview}\n </p>\n ) : (\n <p className=\"text-sm italic text-text-secondary\">\n {tr('inbox.readingPane.noPreview', 'No preview available for this message.')}\n </p>\n )}\n </div>\n\n {message.actionRequired && (\n <div className=\"mt-4 flex items-start gap-2 rounded-lg border border-status-warning-text/30 bg-status-warning-bg-subtle p-3\">\n <ShieldAlert className=\"mt-0.5 h-4 w-4 shrink-0 text-status-warning-text\" />\n <p className=\"text-xs text-status-warning-text\">\n {tr(\n 'inbox.readingPane.attention',\n 'This message needs your attention — review it before its provider deadline.'\n )}\n </p>\n </div>\n )}\n\n {onViewRelationship && (\n <button\n type=\"button\"\n onClick={onViewRelationship}\n className=\"mt-4 inline-flex items-center gap-1 self-start text-sm font-medium text-text-link hover:underline\"\n >\n {tr('inbox.message.viewRelationship', 'View relationship')}{' '}\n <ChevronRight className=\"h-4 w-4\" />\n </button>\n )}\n </GlassCard>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Filter chip\n// ---------------------------------------------------------------------------\n\nfunction Chip({\n active,\n onClick,\n children,\n tone = 'default',\n}: {\n active: boolean;\n onClick: () => void;\n children: React.ReactNode;\n tone?: 'default' | 'warning';\n}) {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n aria-pressed={active}\n className={cn(\n 'inline-flex items-center gap-1.5 rounded-full border px-3 py-1.5 text-xs font-medium transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]',\n active\n ? tone === 'warning'\n ? 'border-transparent bg-status-warning-bg-subtle text-status-warning-text'\n : 'border-transparent bg-action-primary-bg text-action-primary-text'\n : 'border-border-subtle text-text-secondary hover:bg-bg-sunken hover:text-text-primary'\n )}\n >\n {children}\n </button>\n );\n}\n\nexport function InboxPage() {\n const tr = useTr();\n const { navigateTo } = useSubscriberBot();\n const [page, setPage] = useState(0);\n const [category, setCategory] = useState<'' | SubMessageCategory>('');\n const [unreadOnly, setUnreadOnly] = useState(false);\n const [actionOnly, setActionOnly] = useState(false);\n const [selectedId, setSelectedId] = useState<string | null>(null);\n\n const { data, isLoading, isError, refetch, isFetching } = useInboxMessages({\n category: category || undefined,\n unreadOnly: unreadOnly || undefined,\n pagination: { limit: PAGE_SIZE, offset: page * PAGE_SIZE },\n });\n const { data: channelsData } = useChannels();\n const markRead = useMarkMessageRead();\n\n const items = data?.items ?? [];\n const hasMore = data?.pageInfo.hasMore ?? false;\n\n // \"Action required\" is a client-side lens over the current page.\n const displayed = actionOnly ? items.filter((m) => m.actionRequired) : items;\n\n // Real, computed page stats.\n const unreadCount = items.filter((m) => !m.isRead).length;\n const actionCount = items.filter((m) => m.actionRequired).length;\n\n const channelMap = new Map((channelsData ?? []).map((c) => [c.id, c]));\n const selected = displayed.find((m) => m.id === selectedId) ?? null;\n const selectedChannel = selected ? channelMap.get(selected.channelId) : undefined;\n\n const resetToFirst = () => {\n setPage(0);\n setSelectedId(null);\n };\n\n return (\n <PageLayout\n title={tr('inbox.title', 'Universal Inbox')}\n description={tr(\n 'inbox.description',\n 'Every subscription communication, merged and categorized'\n )}\n >\n <PagePurpose className=\"mb-4\">\n {tr(\n 'inbox.purpose',\n 'Subscription communication is noisy and scattered. The Universal Inbox merges every channel a provider uses and uses AI to categorize each message so you never miss a renewal, required action or security notice.'\n )}\n </PagePurpose>\n\n {/* Category tabs */}\n <div className=\"-mx-1 flex gap-2 overflow-x-auto px-1 pb-1\">\n {CATEGORY_TAB_VALUES.map((t) => (\n <span key={t.value} className=\"shrink-0\">\n <Chip\n active={category === t.value}\n onClick={() => {\n setCategory(t.value);\n resetToFirst();\n }}\n >\n {tr(t.key, t.fallback)}\n </Chip>\n </span>\n ))}\n </div>\n\n {/* Lenses + stats */}\n <div className=\"mt-3 flex flex-wrap items-center gap-2\">\n <Chip\n active={actionOnly}\n tone=\"warning\"\n onClick={() => {\n setActionOnly((v) => !v);\n setSelectedId(null);\n }}\n >\n <AlertTriangle className=\"h-3.5 w-3.5\" />{' '}\n {tr('inbox.filters.actionRequired', 'Action required')}\n </Chip>\n <Chip\n active={unreadOnly}\n onClick={() => {\n setUnreadOnly((v) => !v);\n resetToFirst();\n }}\n >\n <Mail className=\"h-3.5 w-3.5\" /> {tr('inbox.filters.unreadOnly', 'Unread only')}\n </Chip>\n {!isLoading && !isError && items.length > 0 && (\n <p className=\"ml-auto text-xs text-text-secondary\">\n {tr('inbox.filters.unreadCount', '{{count}} unread', { count: unreadCount })}\n {' · '}\n {tr('inbox.filters.actionCount', '{{count}} need action', { count: actionCount })}\n {isFetching ? ` · ${tr('inbox.filters.refreshing', 'refreshing…')}` : ''}\n </p>\n )}\n </div>\n\n {isLoading ? (\n <LoadingView label={tr('inbox.loading', 'Loading inbox…')} />\n ) : isError ? (\n <ErrorView\n message={tr('inbox.error', 'Could not load the inbox.')}\n onRetry={() => void refetch()}\n />\n ) : items.length === 0 ? (\n <div className=\"mt-6\">\n <EmptyState\n illustration=\"empty-inbox\"\n icon={<InboxIcon className=\"h-12 w-12\" />}\n title={tr('inbox.emptyTitle', 'Inbox is empty')}\n description={\n unreadOnly || category\n ? tr('inbox.emptyDescriptionFiltered', 'No messages match these filters.')\n : tr(\n 'inbox.emptyDescription',\n 'Merged subscription communications will appear here once channels start ingesting.'\n )\n }\n />\n </div>\n ) : (\n <div className=\"mt-5 grid grid-cols-1 gap-5 lg:grid-cols-[minmax(0,1fr)_380px]\">\n {/* List */}\n <div className=\"min-w-0\">\n {displayed.length === 0 ? (\n <div className=\"rounded-xl border border-border-subtle bg-bg-surface p-8 text-center\">\n <AlertTriangle className=\"mx-auto h-8 w-8 text-text-secondary\" />\n <p className=\"mt-2 text-sm font-medium text-text-primary\">\n {tr('inbox.noActionTitle', 'No action-required messages on this page')}\n </p>\n <p className=\"mt-1 text-xs text-text-secondary\">\n {tr(\n 'inbox.noActionDescription',\n 'Turn off the “Action required” lens or move to another page.'\n )}\n </p>\n </div>\n ) : (\n <div className=\"divide-y divide-border-subtle overflow-hidden rounded-xl border border-border-subtle bg-bg-surface\">\n {displayed.map((m) => (\n <MessageRow\n key={m.id}\n message={m}\n active={m.id === selectedId}\n onSelect={() => setSelectedId(m.id)}\n onMarkRead={() => markRead.mutate(m.id)}\n markPending={markRead.isPending}\n onViewRelationship={\n m.relationshipId\n ? () => navigateTo(`/relationships/${m.relationshipId}`)\n : null\n }\n />\n ))}\n </div>\n )}\n\n <div className=\"mt-4 flex items-center justify-between\">\n <p className=\"text-xs text-text-secondary\">\n {tr('inbox.pagination.page', 'Page {{page}}', { page: page + 1 })}\n {isFetching ? ` · ${tr('inbox.filters.refreshing', 'refreshing…')}` : ''}\n </p>\n <div className=\"flex gap-2\">\n <button\n type=\"button\"\n disabled={page === 0}\n onClick={() => {\n setPage((p) => Math.max(0, p - 1));\n setSelectedId(null);\n }}\n className={cn(\n 'rounded-lg border border-border-subtle px-3 py-1.5 text-xs font-medium',\n page === 0\n ? 'cursor-not-allowed text-text-secondary opacity-50'\n : 'text-text-primary hover:bg-bg-sunken'\n )}\n >\n {tr('inbox.pagination.previous', 'Previous')}\n </button>\n <button\n type=\"button\"\n disabled={!hasMore}\n onClick={() => {\n setPage((p) => p + 1);\n setSelectedId(null);\n }}\n className={cn(\n 'rounded-lg border border-border-subtle px-3 py-1.5 text-xs font-medium',\n !hasMore\n ? 'cursor-not-allowed text-text-secondary opacity-50'\n : 'text-text-primary hover:bg-bg-sunken'\n )}\n >\n {tr('inbox.pagination.next', 'Next')}\n </button>\n </div>\n </div>\n </div>\n\n {/* Reading pane — wide screens only */}\n <aside className=\"hidden lg:block\">\n <div className=\"lg:sticky lg:top-4\">\n <ReadingPane\n message={selected}\n channel={selectedChannel}\n onMarkRead={() => selected && markRead.mutate(selected.id)}\n markPending={markRead.isPending}\n onViewRelationship={\n selected?.relationshipId\n ? () => navigateTo(`/relationships/${selected.relationshipId}`)\n : null\n }\n />\n </div>\n </aside>\n </div>\n )}\n\n <ChannelsSection />\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"InsightsPage.js","names":[],"sources":["../../../src/subscriberbot/pages/InsightsPage.tsx"],"sourcesContent":["/**\n * Insights — spend analytics + renewal forecast, aggregated entirely from the\n * real subscription portfolio (`useRelationships`). Nothing is mocked: every\n * figure is computed from live relationship data (amount normalized to a monthly\n * equivalent by billing interval). Covers the concept mocks for cost insight,\n * renewal cashflow forecast, and spend management.\n * @module subscriberbot/pages\n */\nimport { useMemo, type FC } from 'react';\nimport { TrendingUp, Wallet, CalendarClock, HeartPulse, Layers } from 'lucide-react';\nimport { GlassCard, PagePurpose } from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n MetricsCard,\n EmptyState,\n LoadingView,\n ErrorView,\n Section,\n Donut,\n Legend,\n BarList,\n MiniProgress,\n} from '../components';\nimport { useRelationships } from '../hooks/useSubscriberBotApi';\nimport { useTr } from '../hooks/useTr';\nimport { formatCurrency, formatEnumLabel } from '../utils/formatters';\nimport type { SubBillingInterval, SubRelationship } from '../types';\nimport { cn } from '../utils/cn';\n\nconst MONTHLY_FACTOR: Record<SubBillingInterval, number> = {\n DAILY: 30,\n WEEKLY: 4.345,\n MONTHLY: 1,\n QUARTERLY: 1 / 3,\n YEARLY: 1 / 12,\n LIFETIME: 0,\n FREE: 0,\n USAGE_BASED: 1,\n};\n\nconst monthly = (r: SubRelationship): number => r.amount * (MONTHLY_FACTOR[r.interval] ?? 1);\nconst isActive = (r: SubRelationship): boolean => r.status === 'ACTIVE';\n\nexport const InsightsPage: FC = () => {\n const query = useRelationships({ pagination: { limit: 100 } });\n const tr = useTr();\n const rels = useMemo(() => query.data?.items ?? [], [query.data]);\n\n const stats = useMemo(() => {\n const active = rels.filter(isActive);\n const currency = active[0]?.currency ?? rels[0]?.currency ?? 'USD';\n const monthlySpend = active.reduce((s, r) => s + monthly(r), 0);\n const scored = rels.filter((r) => r.healthScore != null);\n const avgHealth = scored.length\n ? scored.reduce((s, r) => s + (r.healthScore ?? 0), 0) / scored.length\n : null;\n\n // by status (donut)\n const byStatus = new Map<string, number>();\n for (const r of rels) byStatus.set(r.status, (byStatus.get(r.status) ?? 0) + 1);\n\n // spend by kind (bar)\n const byKind = new Map<string, number>();\n for (const r of active) byKind.set(r.kind, (byKind.get(r.kind) ?? 0) + monthly(r));\n\n // top monthly costs\n const topCosts = [...active].sort((a, b) => monthly(b) - monthly(a)).slice(0, 6);\n\n // renewal forecast — next 6 calendar months by renewsAt\n const buckets: { label: string; value: number }[] = [];\n const now = new Date();\n for (let i = 0; i < 6; i += 1) {\n const d = new Date(now.getFullYear(), now.getMonth() + i, 1);\n const key = `${d.getFullYear()}-${d.getMonth()}`;\n const label = d.toLocaleString(undefined, { month: 'short' });\n const value = rels.reduce((s, r) => {\n if (!r.renewsAt) return s;\n const rd = new Date(r.renewsAt);\n if (Number.isNaN(rd.getTime())) return s;\n return `${rd.getFullYear()}-${rd.getMonth()}` === key ? s + r.amount : s;\n }, 0);\n buckets.push({ label, value });\n }\n\n // low-health watchlist\n const watchlist = rels\n .filter((r) => r.healthScore != null && (r.healthScore as number) < 40)\n .sort((a, b) => (a.healthScore ?? 0) - (b.healthScore ?? 0))\n .slice(0, 6);\n\n const renewals30 = rels.filter((r) => {\n if (!r.renewsAt) return false;\n const t = new Date(r.renewsAt).getTime();\n if (Number.isNaN(t)) return false;\n const days = (t - Date.now()) / 86_400_000;\n return days >= 0 && days <= 30;\n }).length;\n\n return {\n currency,\n monthlySpend,\n activeCount: active.length,\n avgHealth,\n renewals30,\n statusData: [...byStatus.entries()].map(([label, value]) => ({\n label: formatEnumLabel(label),\n value,\n })),\n kindData: [...byKind.entries()]\n .map(([label, value]) => ({ label: formatEnumLabel(label), value }))\n .sort((a, b) => b.value - a.value),\n topCosts,\n forecast: buckets,\n watchlist,\n total: rels.length,\n };\n }, [rels]);\n\n if (query.isLoading)\n return <LoadingView label={tr('insights.loading', 'Crunching your spend insights…')} />;\n if (query.isError)\n return (\n <ErrorView\n message={tr('insights.error', 'Could not load insights.')}\n onRetry={() => void query.refetch()}\n />\n );\n\n const fmt = (n: number) => formatCurrency(n, stats.currency);\n\n return (\n <PageLayout\n title={tr('insights.title', 'Insights')}\n description={tr(\n 'insights.description',\n 'Where your recurring money goes, what renews next, and what to review — computed live from your portfolio.'\n )}\n >\n <PagePurpose className=\"mb-2\">\n {tr(\n 'insights.purpose',\n 'Understand and optimize your recurring spend. Every figure here is aggregated from your real subscriptions — normalized to a monthly equivalent so plans on different billing cycles are comparable.'\n )}\n </PagePurpose>\n\n {stats.total === 0 ? (\n <EmptyState\n icon={<TrendingUp className=\"h-8 w-8\" />}\n title={tr('insights.emptyTitle', 'No data to analyze yet')}\n description={tr(\n 'insights.emptyDescription',\n 'Add a few subscriptions and your spend breakdown, renewal forecast and health watchlist will appear here.'\n )}\n />\n ) : (\n <>\n <div className=\"grid grid-cols-2 gap-4 lg:grid-cols-4\">\n <MetricsCard\n icon={<Wallet className=\"h-5 w-5\" />}\n value={fmt(stats.monthlySpend)}\n label={tr('insights.metrics.recurringSpend', 'Recurring spend / month')}\n featured\n />\n <MetricsCard\n icon={<Layers className=\"h-5 w-5\" />}\n value={stats.activeCount}\n label={tr('insights.metrics.activeSubscriptions', 'Active subscriptions')}\n />\n <MetricsCard\n icon={<CalendarClock className=\"h-5 w-5\" />}\n value={stats.renewals30}\n label={tr('insights.metrics.renewing30Days', 'Renewing in 30 days')}\n />\n <MetricsCard\n icon={<HeartPulse className=\"h-5 w-5\" />}\n value={\n stats.avgHealth != null\n ? Math.round(stats.avgHealth)\n : tr('insights.metrics.noScore', '—')\n }\n label={tr('insights.metrics.avgHealthScore', 'Avg. health score')}\n />\n </div>\n\n <div className=\"grid grid-cols-1 gap-4 lg:grid-cols-3\">\n <GlassCard treatment=\"plain\" className=\"p-6\">\n <h3 className=\"mb-4 text-sm font-semibold text-text-primary\">\n {tr('insights.portfolioByStatus', 'Portfolio by status')}\n </h3>\n <div className=\"flex flex-col items-center gap-6 sm:flex-row\">\n <Donut\n data={stats.statusData}\n centerLabel={String(stats.total)}\n centerSub={tr('insights.subscriptions', 'subscriptions')}\n />\n <Legend className=\"w-full sm:flex-1\" data={stats.statusData} />\n </div>\n </GlassCard>\n\n <GlassCard treatment=\"plain\" className=\"p-6\">\n <h3 className=\"mb-4 text-sm font-semibold text-text-primary\">\n {tr('insights.spendByType', 'Monthly spend by type')}\n </h3>\n {stats.kindData.length ? (\n <BarList data={stats.kindData} valueFormat={fmt} />\n ) : (\n <p className=\"text-sm text-text-secondary\">\n {tr('insights.noActiveSpend', 'No active spend.')}\n </p>\n )}\n </GlassCard>\n\n <GlassCard treatment=\"plain\" className=\"p-6\">\n <h3 className=\"mb-4 text-sm font-semibold text-text-primary\">\n {tr('insights.renewalForecast', 'Renewal forecast (6 mo)')}\n </h3>\n {stats.forecast.some((f) => f.value > 0) ? (\n <BarList data={stats.forecast} valueFormat={fmt} />\n ) : (\n <p className=\"text-sm text-text-secondary\">\n {tr('insights.noRenewals', 'No dated renewals in the next 6 months.')}\n </p>\n )}\n </GlassCard>\n </div>\n\n <div className=\"grid grid-cols-1 gap-4 lg:grid-cols-2\">\n <Section title={tr('insights.topCosts', 'Top monthly costs')}>\n <GlassCard treatment=\"plain\" className=\"divide-y divide-border-subtle\">\n {stats.topCosts.map((r) => (\n <div key={r.id} className=\"flex items-center justify-between p-4\">\n <span className=\"truncate text-sm text-text-primary\">{r.label}</span>\n <span className=\"ml-3 shrink-0 text-sm font-semibold tabular-nums text-text-primary\">\n {fmt(monthly(r))}\n <span className=\"text-xs font-normal text-text-secondary\">\n {tr('insights.perMonth', '/mo')}\n </span>\n </span>\n </div>\n ))}\n </GlassCard>\n </Section>\n\n <Section title={tr('insights.healthWatchlist', 'Health watchlist')}>\n {stats.watchlist.length ? (\n <GlassCard treatment=\"plain\" className=\"divide-y divide-border-subtle\">\n {stats.watchlist.map((r) => (\n <div key={r.id} className=\"flex items-center gap-3 p-4\">\n <span className=\"w-32 shrink-0 truncate text-sm text-text-primary\">\n {r.label}\n </span>\n <MiniProgress className=\"flex-1\" value={r.healthScore ?? 0} tone=\"negative\" />\n <span\n className={cn(\n 'w-8 shrink-0 text-right text-sm font-medium tabular-nums text-status-warning-text'\n )}\n >\n {r.healthScore}\n </span>\n </div>\n ))}\n </GlassCard>\n ) : (\n <GlassCard treatment=\"plain\" className=\"p-6 text-sm text-text-secondary\">\n {tr(\n 'insights.allHealthy',\n 'Nothing needs review — all scored subscriptions look healthy.'\n )}\n </GlassCard>\n )}\n </Section>\n </div>\n </>\n )}\n </PageLayout>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"InsightsPage.js","names":[],"sources":["../../../src/subscriberbot/pages/InsightsPage.tsx"],"sourcesContent":["/**\n * Insights — spend analytics + renewal forecast, aggregated entirely from the\n * real subscription portfolio (`useRelationships`). Nothing is mocked: every\n * figure is computed from live relationship data (amount normalized to a monthly\n * equivalent by billing interval). Covers the concept mocks for cost insight,\n * renewal cashflow forecast, and spend management.\n * @module subscriberbot/pages\n */\nimport { useMemo, type FC } from 'react';\nimport { TrendingUp, Wallet, CalendarClock, HeartPulse, Layers } from 'lucide-react';\nimport { GlassCard, PagePurpose } from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n MetricsCard,\n EmptyState,\n LoadingView,\n ErrorView,\n Section,\n Donut,\n Legend,\n BarList,\n MiniProgress,\n} from '../components';\nimport { useRelationships } from '../hooks/useSubscriberBotApi';\nimport { useTr } from '../hooks/useTr';\nimport { formatCurrency, formatEnumLabel } from '../utils/formatters';\nimport type { SubBillingInterval, SubRelationship } from '../types';\nimport { cn } from '../utils/cn';\n\nconst MONTHLY_FACTOR: Record<SubBillingInterval, number> = {\n DAILY: 30,\n WEEKLY: 4.345,\n MONTHLY: 1,\n QUARTERLY: 1 / 3,\n YEARLY: 1 / 12,\n LIFETIME: 0,\n FREE: 0,\n USAGE_BASED: 1,\n};\n\nconst monthly = (r: SubRelationship): number => r.amount * (MONTHLY_FACTOR[r.interval] ?? 1);\nconst isActive = (r: SubRelationship): boolean => r.status === 'ACTIVE';\n\nexport const InsightsPage: FC = () => {\n const query = useRelationships({ pagination: { limit: 100 } });\n const tr = useTr();\n const rels = useMemo(() => query.data?.items ?? [], [query.data]);\n\n const stats = useMemo(() => {\n const active = rels.filter(isActive);\n const currency = active[0]?.currency ?? rels[0]?.currency ?? 'USD';\n const monthlySpend = active.reduce((s, r) => s + monthly(r), 0);\n const scored = rels.filter((r) => r.healthScore != null);\n const avgHealth = scored.length\n ? scored.reduce((s, r) => s + (r.healthScore ?? 0), 0) / scored.length\n : null;\n\n // by status (donut)\n const byStatus = new Map<string, number>();\n for (const r of rels) byStatus.set(r.status, (byStatus.get(r.status) ?? 0) + 1);\n\n // spend by kind (bar)\n const byKind = new Map<string, number>();\n for (const r of active) byKind.set(r.kind, (byKind.get(r.kind) ?? 0) + monthly(r));\n\n // top monthly costs\n const topCosts = [...active].sort((a, b) => monthly(b) - monthly(a)).slice(0, 6);\n\n // renewal forecast — next 6 calendar months by renewsAt\n const buckets: { label: string; value: number }[] = [];\n const now = new Date();\n for (let i = 0; i < 6; i += 1) {\n const d = new Date(now.getFullYear(), now.getMonth() + i, 1);\n const key = `${d.getFullYear()}-${d.getMonth()}`;\n const label = d.toLocaleString(undefined, { month: 'short' });\n const value = rels.reduce((s, r) => {\n if (!r.renewsAt) return s;\n const rd = new Date(r.renewsAt);\n if (Number.isNaN(rd.getTime())) return s;\n return `${rd.getFullYear()}-${rd.getMonth()}` === key ? s + r.amount : s;\n }, 0);\n buckets.push({ label, value });\n }\n\n // low-health watchlist\n const watchlist = rels\n .filter((r) => r.healthScore != null && (r.healthScore as number) < 40)\n .sort((a, b) => (a.healthScore ?? 0) - (b.healthScore ?? 0))\n .slice(0, 6);\n\n const renewals30 = rels.filter((r) => {\n if (!r.renewsAt) return false;\n const t = new Date(r.renewsAt).getTime();\n if (Number.isNaN(t)) return false;\n const days = (t - Date.now()) / 86_400_000;\n return days >= 0 && days <= 30;\n }).length;\n\n return {\n currency,\n monthlySpend,\n activeCount: active.length,\n avgHealth,\n renewals30,\n statusData: [...byStatus.entries()].map(([label, value]) => ({\n label: formatEnumLabel(label),\n value,\n })),\n kindData: [...byKind.entries()]\n .map(([label, value]) => ({ label: formatEnumLabel(label), value }))\n .sort((a, b) => b.value - a.value),\n topCosts,\n forecast: buckets,\n watchlist,\n total: rels.length,\n };\n }, [rels]);\n\n if (query.isLoading)\n return <LoadingView label={tr('insights.loading', 'Crunching your spend insights…')} />;\n if (query.isError)\n return (\n <ErrorView\n message={tr('insights.error', 'Could not load insights.')}\n onRetry={() => void query.refetch()}\n />\n );\n\n const fmt = (n: number) => formatCurrency(n, stats.currency);\n\n return (\n <PageLayout\n title={tr('insights.title', 'Insights')}\n description={tr(\n 'insights.description',\n 'Where your recurring money goes, what renews next, and what to review — computed live from your portfolio.'\n )}\n >\n <PagePurpose className=\"mb-2\">\n {tr(\n 'insights.purpose',\n 'Understand and optimize your recurring spend. Every figure here is aggregated from your real subscriptions — normalized to a monthly equivalent so plans on different billing cycles are comparable.'\n )}\n </PagePurpose>\n\n {stats.total === 0 ? (\n <EmptyState\n icon={<TrendingUp className=\"h-8 w-8\" />}\n title={tr('insights.emptyTitle', 'No data to analyze yet')}\n description={tr(\n 'insights.emptyDescription',\n 'Add a few subscriptions and your spend breakdown, renewal forecast and health watchlist will appear here.'\n )}\n />\n ) : (\n <>\n <div className=\"grid grid-cols-2 gap-4 lg:grid-cols-4\">\n <MetricsCard\n icon={<Wallet className=\"h-5 w-5\" />}\n value={fmt(stats.monthlySpend)}\n label={tr('insights.metrics.recurringSpend', 'Recurring spend / month')}\n featured\n />\n <MetricsCard\n icon={<Layers className=\"h-5 w-5\" />}\n value={stats.activeCount}\n label={tr('insights.metrics.activeSubscriptions', 'Active subscriptions')}\n />\n <MetricsCard\n icon={<CalendarClock className=\"h-5 w-5\" />}\n value={stats.renewals30}\n label={tr('insights.metrics.renewing30Days', 'Renewing in 30 days')}\n />\n <MetricsCard\n icon={<HeartPulse className=\"h-5 w-5\" />}\n value={\n stats.avgHealth != null\n ? Math.round(stats.avgHealth)\n : tr('insights.metrics.noScore', '—')\n }\n label={tr('insights.metrics.avgHealthScore', 'Avg. health score')}\n />\n </div>\n\n <div className=\"grid grid-cols-1 gap-4 lg:grid-cols-3\">\n <GlassCard treatment=\"plain\" className=\"p-6\">\n <h3 className=\"mb-4 text-sm font-semibold text-text-primary\">\n {tr('insights.portfolioByStatus', 'Portfolio by status')}\n </h3>\n <div className=\"flex flex-col items-center gap-6 sm:flex-row\">\n <Donut\n data={stats.statusData}\n centerLabel={String(stats.total)}\n centerSub={tr('insights.subscriptions', 'subscriptions')}\n />\n <Legend className=\"w-full sm:flex-1\" data={stats.statusData} />\n </div>\n </GlassCard>\n\n <GlassCard treatment=\"plain\" className=\"p-6\">\n <h3 className=\"mb-4 text-sm font-semibold text-text-primary\">\n {tr('insights.spendByType', 'Monthly spend by type')}\n </h3>\n {stats.kindData.length ? (\n <BarList data={stats.kindData} valueFormat={fmt} />\n ) : (\n <p className=\"text-sm text-text-secondary\">\n {tr('insights.noActiveSpend', 'No active spend.')}\n </p>\n )}\n </GlassCard>\n\n <GlassCard treatment=\"plain\" className=\"p-6\">\n <h3 className=\"mb-4 text-sm font-semibold text-text-primary\">\n {tr('insights.renewalForecast', 'Renewal forecast (6 mo)')}\n </h3>\n {stats.forecast.some((f) => f.value > 0) ? (\n <BarList data={stats.forecast} valueFormat={fmt} />\n ) : (\n <p className=\"text-sm text-text-secondary\">\n {tr('insights.noRenewals', 'No dated renewals in the next 6 months.')}\n </p>\n )}\n </GlassCard>\n </div>\n\n <div className=\"grid grid-cols-1 gap-4 lg:grid-cols-2\">\n <Section title={tr('insights.topCosts', 'Top monthly costs')}>\n <GlassCard treatment=\"plain\" className=\"divide-y divide-border-subtle\">\n {stats.topCosts.map((r) => (\n <div key={r.id} className=\"flex items-center justify-between p-4\">\n <span className=\"truncate text-sm text-text-primary\">{r.label}</span>\n <span className=\"ml-3 shrink-0 text-sm font-semibold tabular-nums text-text-primary\">\n {fmt(monthly(r))}\n <span className=\"text-xs font-normal text-text-secondary\">\n {tr('insights.perMonth', '/mo')}\n </span>\n </span>\n </div>\n ))}\n </GlassCard>\n </Section>\n\n <Section title={tr('insights.healthWatchlist', 'Health watchlist')}>\n {stats.watchlist.length ? (\n <GlassCard treatment=\"plain\" className=\"divide-y divide-border-subtle\">\n {stats.watchlist.map((r) => (\n <div key={r.id} className=\"flex items-center gap-3 p-4\">\n <span className=\"w-32 shrink-0 truncate text-sm text-text-primary\">\n {r.label}\n </span>\n <MiniProgress className=\"flex-1\" value={r.healthScore ?? 0} tone=\"negative\" />\n <span\n className={cn(\n 'w-8 shrink-0 text-right text-sm font-medium tabular-nums text-status-warning-text'\n )}\n >\n {r.healthScore}\n </span>\n </div>\n ))}\n </GlassCard>\n ) : (\n <GlassCard treatment=\"plain\" className=\"p-6 text-sm text-text-secondary\">\n {tr(\n 'insights.allHealthy',\n 'Nothing needs review — all scored subscriptions look healthy.'\n )}\n </GlassCard>\n )}\n </Section>\n </div>\n </>\n )}\n </PageLayout>\n );\n};\n"],"mappings":"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"}
|