@burdenoff/microfe-subscriberbot 2026.628.1 → 2026.629.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/subscriberbot/SubscriberBotRoot.js.map +1 -1
- package/dist/subscriberbot/components/StateViews.js.map +1 -1
- package/dist/subscriberbot/pages/DashboardPage.js.map +1 -1
- package/dist/subscriberbot/pages/InboxPage.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/AgentsListPage.js.map +1 -1
- package/dist/subscriberbot/pages/providers/ProviderDetailPage.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/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SubscriberBotRoot.js","names":[],"sources":["../../src/subscriberbot/SubscriberBotRoot.tsx"],"sourcesContent":["/**\n * SubscriberBot Root Component\n * @module subscriberbot\n */\n\nimport type { FC } from 'react';\nimport { useCallback, useEffect } from 'react';\nimport { QueryClient, QueryClientProvider } from '@tanstack/react-query';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport {
|
|
1
|
+
{"version":3,"file":"SubscriberBotRoot.js","names":[],"sources":["../../src/subscriberbot/SubscriberBotRoot.tsx"],"sourcesContent":["/**\n * SubscriberBot Root Component\n * @module subscriberbot\n */\n\nimport type { FC } from 'react';\nimport { useCallback, useEffect } from 'react';\nimport { QueryClient, QueryClientProvider } from '@tanstack/react-query';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { LayoutDashboard, Repeat2, Building2, Inbox, Receipt, Bot, Workflow } from 'lucide-react';\nimport { SubscriberBotProvider } from './context/SubscriberBotContext';\nimport { FeatureFlagProvider } from '@burdenoff/fe-libs/shared/feature-flags';\nimport { SubscriberBotRoutes } from './SubscriberBotRoutes';\nimport type { SubscriberBotRootProps, NavItem } from './types';\n\n// Create a query client for this microfrontend\nconst queryClient = new QueryClient({\n defaultOptions: {\n queries: {\n staleTime: 5 * 60 * 1000,\n retry: 1,\n },\n },\n});\n\nexport const SubscriberBotRoot: FC<SubscriberBotRootProps> = (props) => {\n const { registerNavItems, registerFooterItems, basePath = '/' } = props;\n const { t: translate } = useI18n();\n const t = useCallback(\n (key: string, fallback: string): string => {\n const value = translate(key);\n return value === key ? fallback : value;\n },\n [translate]\n );\n\n // Helper to join paths correctly\n const joinPath = (base: string, path: string): string => {\n const cleanBase = base.endsWith('/') ? base.slice(0, -1) : base;\n const cleanPath = path.startsWith('/') ? path : `/${path}`;\n return cleanBase + cleanPath;\n };\n\n // Register navigation items with shell on mount\n useEffect(() => {\n if (!registerNavItems) return;\n\n const navItems: NavItem[] = [\n {\n id: 'subscriberbot-overview',\n label: t('subscriberbot.nav.overview', 'Overview'),\n path: joinPath(basePath, ''),\n icon: <LayoutDashboard size={20} />,\n order: 1,\n group: 'subscriberbot',\n },\n {\n id: 'subscriberbot-relationships',\n label: t('subscriberbot.nav.relationships', 'Relationships'),\n path: joinPath(basePath, 'relationships'),\n icon: <Repeat2 size={20} />,\n order: 2,\n group: 'subscriberbot',\n },\n {\n id: 'subscriberbot-providers',\n label: t('subscriberbot.nav.providers', 'Providers'),\n path: joinPath(basePath, 'providers'),\n icon: <Building2 size={20} />,\n order: 3,\n group: 'subscriberbot',\n },\n {\n id: 'subscriberbot-inbox',\n label: t('subscriberbot.nav.inbox', 'Universal Inbox'),\n path: joinPath(basePath, 'inbox'),\n icon: <Inbox size={20} />,\n order: 4,\n group: 'subscriberbot',\n },\n {\n id: 'subscriberbot-invoices',\n label: t('subscriberbot.nav.invoices', 'Invoices'),\n path: joinPath(basePath, 'invoices'),\n icon: <Receipt size={20} />,\n order: 5,\n group: 'subscriberbot',\n },\n {\n id: 'subscriberbot-agents',\n label: t('subscriberbot.nav.agents', 'AI Agents'),\n path: joinPath(basePath, 'agents'),\n icon: <Bot size={20} />,\n order: 6,\n group: 'subscriberbot',\n },\n {\n id: 'subscriberbot-workflows',\n label: t('subscriberbot.nav.workflows', 'Workflows'),\n path: joinPath(basePath, 'workflows'),\n icon: <Workflow size={20} />,\n order: 7,\n group: 'subscriberbot',\n },\n ];\n\n const cleanup = registerNavItems(navItems);\n return cleanup;\n }, [registerNavItems, basePath, t]);\n\n // Register footer items\n useEffect(() => {\n if (!registerFooterItems) return;\n\n const footerItems = [\n {\n id: 'subscriberbot-status',\n label: t('subscriberbot.footer.ready', 'SubscriberBot Ready'),\n type: 'status' as const,\n order: 10,\n section: 'left' as const,\n },\n ];\n\n const cleanup = registerFooterItems(footerItems);\n return cleanup;\n }, [registerFooterItems, t]);\n\n return (\n <QueryClientProvider client={queryClient}>\n <FeatureFlagProvider workspaceId={null} gateway=\"global\" defaultEnabled={true}>\n <SubscriberBotProvider {...props}>\n <SubscriberBotRoutes />\n </SubscriberBotProvider>\n </FeatureFlagProvider>\n </QueryClientProvider>\n );\n};\n"],"mappings":";;;;;;;;;AAgBA,IAAM,IAAc,IAAI,EAAY,EAClC,gBAAgB,EACd,SAAS;CACP,WAAW,MAAS;CACpB,OAAO;CACR,EACF,EACF,CAAC,EAEW,KAAiD,MAAU;CACtE,IAAM,EAAE,qBAAkB,wBAAqB,cAAW,QAAQ,GAC5D,EAAE,GAAG,MAAc,GAAS,EAC5B,IAAI,GACP,GAAa,MAA6B;EACzC,IAAM,IAAQ,EAAU,EAAI;AAC5B,SAAO,MAAU,IAAM,IAAW;IAEpC,CAAC,EAAU,CACZ,EAGK,KAAY,GAAc,OACZ,EAAK,SAAS,IAAI,GAAG,EAAK,MAAM,GAAG,GAAG,GAAG,MACzC,EAAK,WAAW,IAAI,GAAG,IAAO,IAAI;AAyFtD,QApFA,QAAgB;AACT,QA8DL,QADgB,EA3DY;GAC1B;IACE,IAAI;IACJ,OAAO,EAAE,8BAA8B,WAAW;IAClD,MAAM,EAAS,GAAU,GAAG;IAC5B,MAAM,kBAAC,GAAD,EAAiB,MAAM,IAAM,CAAA;IACnC,OAAO;IACP,OAAO;IACR;GACD;IACE,IAAI;IACJ,OAAO,EAAE,mCAAmC,gBAAgB;IAC5D,MAAM,EAAS,GAAU,gBAAgB;IACzC,MAAM,kBAAC,GAAD,EAAS,MAAM,IAAM,CAAA;IAC3B,OAAO;IACP,OAAO;IACR;GACD;IACE,IAAI;IACJ,OAAO,EAAE,+BAA+B,YAAY;IACpD,MAAM,EAAS,GAAU,YAAY;IACrC,MAAM,kBAAC,GAAD,EAAW,MAAM,IAAM,CAAA;IAC7B,OAAO;IACP,OAAO;IACR;GACD;IACE,IAAI;IACJ,OAAO,EAAE,2BAA2B,kBAAkB;IACtD,MAAM,EAAS,GAAU,QAAQ;IACjC,MAAM,kBAAC,GAAD,EAAO,MAAM,IAAM,CAAA;IACzB,OAAO;IACP,OAAO;IACR;GACD;IACE,IAAI;IACJ,OAAO,EAAE,8BAA8B,WAAW;IAClD,MAAM,EAAS,GAAU,WAAW;IACpC,MAAM,kBAAC,GAAD,EAAS,MAAM,IAAM,CAAA;IAC3B,OAAO;IACP,OAAO;IACR;GACD;IACE,IAAI;IACJ,OAAO,EAAE,4BAA4B,YAAY;IACjD,MAAM,EAAS,GAAU,SAAS;IAClC,MAAM,kBAAC,GAAD,EAAK,MAAM,IAAM,CAAA;IACvB,OAAO;IACP,OAAO;IACR;GACD;IACE,IAAI;IACJ,OAAO,EAAE,+BAA+B,YAAY;IACpD,MAAM,EAAS,GAAU,YAAY;IACrC,MAAM,kBAAC,GAAD,EAAU,MAAM,IAAM,CAAA;IAC5B,OAAO;IACP,OAAO;IACR;GACF,CAEyC;IAEzC;EAAC;EAAkB;EAAU;EAAE,CAAC,EAGnC,QAAgB;AACT,QAaL,QADgB,EAVI,CAClB;GACE,IAAI;GACJ,OAAO,EAAE,8BAA8B,sBAAsB;GAC7D,MAAM;GACN,OAAO;GACP,SAAS;GACV,CACF,CAE+C;IAE/C,CAAC,GAAqB,EAAE,CAAC,EAG1B,kBAAC,GAAD;EAAqB,QAAQ;YAC3B,kBAAC,GAAD;GAAqB,aAAa;GAAM,SAAQ;GAAS,gBAAgB;aACvE,kBAAC,GAAD;IAAuB,GAAI;cACzB,kBAAC,GAAD,EAAuB,CAAA;IACD,CAAA;GACJ,CAAA;EACF,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StateViews.js","names":[],"sources":["../../../src/subscriberbot/components/StateViews.tsx"],"sourcesContent":["/**\n * Shared loading / error state views\n * @module subscriberbot/components\n *\n * Small reusable building blocks so every page renders consistent\n * loading / error / empty states without re-implementing them.\n */\n\nimport type { FC, ReactNode } from 'react';\nimport { AlertCircle } from 'lucide-react';\nimport { Spinner } from '@burdenoff/fe-libs/ui';\nimport { cn } from '../utils/cn';\n\ninterface LoadingViewProps {\n label?: string;\n className?: string;\n}\n\nexport const LoadingView: FC<LoadingViewProps> = ({ label = 'Loading…', className }) => (\n <div className={cn('flex min-h-[40vh] flex-col items-center justify-center gap-3', className)}>\n <Spinner size=\"lg\" />\n <p className=\"text-sm text-text-secondary\">{label}</p>\n </div>\n);\n\ninterface ErrorViewProps {\n title?: string;\n message?: string;\n onRetry?: () => void;\n className?: string;\n}\n\nexport const ErrorView: FC<ErrorViewProps> = ({\n title = 'Something went wrong',\n message,\n onRetry,\n className,\n}) => (\n <div\n className={cn('flex min-h-[40vh] flex-col items-center justify-center gap-4 text-center'
|
|
1
|
+
{"version":3,"file":"StateViews.js","names":[],"sources":["../../../src/subscriberbot/components/StateViews.tsx"],"sourcesContent":["/**\n * Shared loading / error state views\n * @module subscriberbot/components\n *\n * Small reusable building blocks so every page renders consistent\n * loading / error / empty states without re-implementing them.\n */\n\nimport type { FC, ReactNode } from 'react';\nimport { AlertCircle } from 'lucide-react';\nimport { Spinner } from '@burdenoff/fe-libs/ui';\nimport { cn } from '../utils/cn';\n\ninterface LoadingViewProps {\n label?: string;\n className?: string;\n}\n\nexport const LoadingView: FC<LoadingViewProps> = ({ label = 'Loading…', className }) => (\n <div className={cn('flex min-h-[40vh] flex-col items-center justify-center gap-3', className)}>\n <Spinner size=\"lg\" />\n <p className=\"text-sm text-text-secondary\">{label}</p>\n </div>\n);\n\ninterface ErrorViewProps {\n title?: string;\n message?: string;\n onRetry?: () => void;\n className?: string;\n}\n\nexport const ErrorView: FC<ErrorViewProps> = ({\n title = 'Something went wrong',\n message,\n onRetry,\n className,\n}) => (\n <div\n className={cn(\n 'flex min-h-[40vh] flex-col items-center justify-center gap-4 text-center',\n className\n )}\n >\n <AlertCircle className=\"h-10 w-10 text-status-error-text\" />\n <div>\n <h2 className=\"text-lg font-semibold text-text-primary\">{title}</h2>\n {message && <p className=\"mt-1 max-w-md text-sm text-text-secondary\">{message}</p>}\n </div>\n {onRetry && (\n <button\n type=\"button\"\n onClick={onRetry}\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 Retry\n </button>\n )}\n </div>\n);\n\ninterface SectionProps {\n title: string;\n description?: string;\n action?: ReactNode;\n children: ReactNode;\n className?: string;\n}\n\nexport const Section: FC<SectionProps> = ({ title, description, action, children, className }) => (\n <section className={cn('mt-8 first:mt-0', className)}>\n <div className=\"mb-3 flex items-end justify-between gap-3\">\n <div>\n <h2 className=\"text-base font-semibold text-text-primary\">{title}</h2>\n {description && <p className=\"mt-0.5 text-sm text-text-secondary\">{description}</p>}\n </div>\n {action && <div className=\"shrink-0\">{action}</div>}\n </div>\n {children}\n </section>\n);\n"],"mappings":";;;;;AAkBA,IAAa,KAAqC,EAAE,WAAQ,YAAY,mBACtE,kBAAC,OAAD;CAAK,WAAW,EAAG,gEAAgE,EAAU;WAA7F,CACE,kBAAC,GAAD,EAAS,MAAK,MAAO,CAAA,EACrB,kBAAC,KAAD;EAAG,WAAU;YAA+B;EAAU,CAAA,CAClD;IAUK,KAAiC,EAC5C,WAAQ,wBACR,YACA,YACA,mBAEA,kBAAC,OAAD;CACE,WAAW,EACT,4EACA,EACD;WAJH;EAME,kBAAC,GAAD,EAAa,WAAU,oCAAqC,CAAA;EAC5D,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,MAAD;GAAI,WAAU;aAA2C;GAAW,CAAA,EACnE,KAAW,kBAAC,KAAD;GAAG,WAAU;aAA6C;GAAY,CAAA,CAC9E,EAAA,CAAA;EACL,KACC,kBAAC,UAAD;GACE,MAAK;GACL,SAAS;GACT,WAAU;aACX;GAEQ,CAAA;EAEP;IAWK,KAA6B,EAAE,UAAO,gBAAa,WAAQ,aAAU,mBAChF,kBAAC,WAAD;CAAS,WAAW,EAAG,mBAAmB,EAAU;WAApD,CACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,MAAD;GAAI,WAAU;aAA6C;GAAW,CAAA,EACrE,KAAe,kBAAC,KAAD;GAAG,WAAU;aAAsC;GAAgB,CAAA,CAC/E,EAAA,CAAA,EACL,KAAU,kBAAC,OAAD;GAAK,WAAU;aAAY;GAAa,CAAA,CAC/C;KACL,EACO"}
|
|
@@ -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 single view of every recurring digital\n * relationship — total recurring spend, lifecycle breakdown, upcoming\n * renewals, the action-required inbox, and outstanding invoices. Every number\n * is computed from real data returned by `wspace-subscriberbot-svc`.\n */\n\nimport { useMemo } from 'react';\nimport {\n Repeat2,\n Wallet,\n CalendarClock,\n Inbox,\n AlertTriangle,\n ArrowRight,\n Sparkles,\n Bot,\n} from 'lucide-react';\nimport { PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../components/PageLayout';\nimport { MetricsCard } from '../components/MetricsCard';\nimport { StatusBadge } from '../components/StatusBadge';\nimport { EmptyState } from '../components/EmptyState';\nimport { LoadingView, ErrorView, Section } from '../components/StateViews';\nimport { useSubscriberBot } from '../context/SubscriberBotContext';\nimport {\n useRelationships,\n useInvoices,\n useInboxMessages,\n useRenewalEvents,\n} from '../hooks/useSubscriberBotApi';\nimport { formatCurrency, formatRelativeTime, formatEnumLabel } from '../utils/formatters';\nimport type { SubBillingInterval } 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\nexport function DashboardPage() {\n const { navigateTo, currentUser } = useSubscriberBot();\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(\n () => relationshipsQ.data?.items ?? [],\n [relationshipsQ.data]\n );\n const invoices = useMemo(() => invoicesQ.data?.items ?? [], [invoicesQ.data]);\n const messages = useMemo(() => inboxQ.data?.items ?? [], [inboxQ.data]);\n const renewals = renewalsQ.data ?? [];\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 trials = relationships.filter((r) => r.status === 'TRIAL').length;\n const pastDue = relationships.filter((r) => r.status === 'PAST_DUE').length;\n const primaryCurrency = active[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\n return {\n activeCount: active.length,\n monthlySpend,\n primaryCurrency,\n trials,\n pastDue,\n unreadActionRequired,\n openInvoiceCount: openInvoices.length,\n openInvoiceTotal,\n };\n }, [relationships, messages, invoices]);\n\n const statusBreakdown = useMemo(() => {\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()).sort((a, b) => b[1] - a[1]);\n }, [relationships]);\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 title=\"Overview\" description=\"Your recurring digital relationships at a glance\">\n <LoadingView label=\"Loading your subscription portfolio…\" />\n </PageLayout>\n );\n }\n\n if (isError) {\n return (\n <PageLayout title=\"Overview\" description=\"Your recurring digital relationships at a glance\">\n <ErrorView\n message=\"We couldn't reach the Subscriber Bot service. Please try again.\"\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 return (\n <PageLayout\n title={greetingName ? `Welcome back, ${greetingName}` : 'Overview'}\n description=\"Everything you pay for on repeat — in one intelligent console.\"\n >\n <PagePurpose className=\"mb-6\">\n Subscriber Bot is your system of record for every recurring digital relationship. See total\n recurring spend, what renews next, what needs action, and what to optimize — all from real\n data across your providers.\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=\"Recurring spend / month\"\n />\n <MetricsCard\n icon={<Repeat2 />}\n value={stats.activeCount}\n label=\"Active relationships\"\n />\n <MetricsCard\n icon={<CalendarClock />}\n value={renewals.length}\n label=\"Upcoming renewals\"\n />\n <MetricsCard\n icon={<AlertTriangle />}\n value={stats.unreadActionRequired}\n label=\"Need your action\"\n />\n </div>\n\n {/* Quick actions */}\n <div className=\"mt-6 grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4\">\n {[\n {\n label: 'Add a relationship',\n description: 'Track a new subscription',\n icon: <Repeat2 className=\"h-5 w-5\" />,\n path: '/relationships/new',\n },\n {\n label: 'Discover providers',\n description: 'Browse the registry',\n icon: <Sparkles className=\"h-5 w-5\" />,\n path: '/providers',\n },\n {\n label: 'Open inbox',\n description: 'Merged communications',\n icon: <Inbox className=\"h-5 w-5\" />,\n path: '/inbox',\n },\n {\n label: 'Configure agents',\n description: 'Automate your portfolio',\n icon: <Bot className=\"h-5 w-5\" />,\n path: '/agents',\n },\n ].map((qa) => (\n <button\n key={qa.path}\n type=\"button\"\n onClick={() => navigateTo(qa.path)}\n className=\"flex items-center gap-3 rounded-lg border border-border-subtle bg-bg-surface p-4 text-left transition-shadow hover:shadow-md\"\n >\n <span className=\"flex size-10 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\">{qa.label}</span>\n <span className=\"block text-xs text-text-secondary\">{qa.description}</span>\n </span>\n <ArrowRight className=\"h-4 w-4 shrink-0 text-text-secondary\" />\n </button>\n ))}\n </div>\n\n <div className=\"grid grid-cols-1 gap-8 lg:grid-cols-2\">\n {/* Upcoming renewals */}\n <Section\n title=\"Upcoming renewals\"\n description=\"What's charging next\"\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('/relationships')}\n className=\"text-sm font-medium text-text-link hover:underline\"\n >\n View all\n </button>\n }\n >\n {renewals.length === 0 ? (\n <EmptyState\n illustration=\"empty-calendar\"\n title=\"No upcoming renewals\"\n description=\"Renewals you track will appear here ahead of time.\"\n />\n ) : (\n <div className=\"divide-y divide-border-subtle rounded-lg border border-border-subtle bg-bg-surface\">\n {renewals.slice(0, 6).map((event) => (\n <button\n key={event.id}\n type=\"button\"\n onClick={() => navigateTo(`/relationships/${event.relationshipId}`)}\n className=\"flex w-full items-center justify-between gap-3 p-3 text-left hover:bg-bg-sunken\"\n >\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {formatEnumLabel(event.eventType)}\n </p>\n <p className=\"text-xs text-text-secondary\">\n {formatRelativeTime(event.occursAt)}\n {event.detail ? ` · ${event.detail}` : ''}\n </p>\n </div>\n {event.amount != null && (\n <span className=\"shrink-0 text-sm font-semibold tabular-nums text-text-primary\">\n {formatCurrency(event.amount, event.currency ?? stats.primaryCurrency)}\n </span>\n )}\n </button>\n ))}\n </div>\n )}\n </Section>\n\n {/* Action-required inbox */}\n <Section\n title=\"Needs your attention\"\n description=\"Required actions from the Universal Inbox\"\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('/inbox')}\n className=\"text-sm font-medium text-text-link hover:underline\"\n >\n Open inbox\n </button>\n }\n >\n {messages.filter((m) => m.actionRequired).length === 0 ? (\n <EmptyState\n illustration=\"empty-inbox\"\n title=\"You're all caught up\"\n description=\"Required actions across all your channels will surface here.\"\n />\n ) : (\n <div className=\"divide-y divide-border-subtle rounded-lg border border-border-subtle bg-bg-surface\">\n {messages\n .filter((m) => m.actionRequired)\n .slice(0, 6)\n .map((message) => (\n <button\n key={message.id}\n type=\"button\"\n onClick={() => navigateTo('/inbox')}\n className=\"flex w-full items-start justify-between gap-3 p-3 text-left hover:bg-bg-sunken\"\n >\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {message.subject || '(no subject)'}\n </p>\n {message.preview && (\n <p className=\"truncate text-xs text-text-secondary\">{message.preview}</p>\n )}\n </div>\n <StatusBadge status={message.category} className=\"shrink-0\" />\n </button>\n ))}\n </div>\n )}\n </Section>\n </div>\n\n {/* Lifecycle breakdown */}\n <Section title=\"Portfolio by lifecycle\" description=\"Where your relationships stand\">\n {statusBreakdown.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n title=\"No relationships yet\"\n description=\"Add your first subscription to see your portfolio breakdown.\"\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 Add relationship\n </button>\n }\n />\n ) : (\n <div className=\"flex flex-wrap gap-3\">\n {statusBreakdown.map(([status, count]) => (\n <div\n key={status}\n className=\"flex items-center gap-2 rounded-lg border border-border-subtle bg-bg-surface px-4 py-3\"\n >\n <StatusBadge status={status} />\n <span className=\"text-lg font-semibold tabular-nums text-text-primary\">{count}</span>\n </div>\n ))}\n {stats.openInvoiceCount > 0 && (\n <div className=\"flex items-center gap-2 rounded-lg border border-status-warning-text/30 bg-status-warning-bg-subtle px-4 py-3\">\n <span className=\"text-sm font-medium text-status-warning-text\">\n {stats.openInvoiceCount} open invoice{stats.openInvoiceCount !== 1 ? 's' : ''} ·{' '}\n {formatCurrency(stats.openInvoiceTotal, stats.primaryCurrency)}\n </span>\n </div>\n )}\n </div>\n )}\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 single view of every recurring digital\n * relationship — total recurring spend, lifecycle breakdown, upcoming\n * renewals, the action-required inbox, and outstanding invoices. Every number\n * is computed from real data returned by `wspace-subscriberbot-svc`.\n */\n\nimport { useMemo } from 'react';\nimport {\n Repeat2,\n Wallet,\n CalendarClock,\n Inbox,\n AlertTriangle,\n ArrowRight,\n Sparkles,\n Bot,\n} from 'lucide-react';\nimport { PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../components/PageLayout';\nimport { MetricsCard } from '../components/MetricsCard';\nimport { StatusBadge } from '../components/StatusBadge';\nimport { EmptyState } from '../components/EmptyState';\nimport { LoadingView, ErrorView, Section } from '../components/StateViews';\nimport { useSubscriberBot } from '../context/SubscriberBotContext';\nimport {\n useRelationships,\n useInvoices,\n useInboxMessages,\n useRenewalEvents,\n} from '../hooks/useSubscriberBotApi';\nimport { formatCurrency, formatRelativeTime, formatEnumLabel } from '../utils/formatters';\nimport type { SubBillingInterval } 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\nexport function DashboardPage() {\n const { navigateTo, currentUser } = useSubscriberBot();\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 = renewalsQ.data ?? [];\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 trials = relationships.filter((r) => r.status === 'TRIAL').length;\n const pastDue = relationships.filter((r) => r.status === 'PAST_DUE').length;\n const primaryCurrency = active[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\n return {\n activeCount: active.length,\n monthlySpend,\n primaryCurrency,\n trials,\n pastDue,\n unreadActionRequired,\n openInvoiceCount: openInvoices.length,\n openInvoiceTotal,\n };\n }, [relationships, messages, invoices]);\n\n const statusBreakdown = useMemo(() => {\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()).sort((a, b) => b[1] - a[1]);\n }, [relationships]);\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 title=\"Overview\" description=\"Your recurring digital relationships at a glance\">\n <LoadingView label=\"Loading your subscription portfolio…\" />\n </PageLayout>\n );\n }\n\n if (isError) {\n return (\n <PageLayout title=\"Overview\" description=\"Your recurring digital relationships at a glance\">\n <ErrorView\n message=\"We couldn't reach the Subscriber Bot service. Please try again.\"\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 return (\n <PageLayout\n title={greetingName ? `Welcome back, ${greetingName}` : 'Overview'}\n description=\"Everything you pay for on repeat — in one intelligent console.\"\n >\n <PagePurpose className=\"mb-6\">\n Subscriber Bot is your system of record for every recurring digital relationship. See total\n recurring spend, what renews next, what needs action, and what to optimize — all from real\n data across your providers.\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=\"Recurring spend / month\"\n />\n <MetricsCard icon={<Repeat2 />} value={stats.activeCount} label=\"Active relationships\" />\n <MetricsCard icon={<CalendarClock />} value={renewals.length} label=\"Upcoming renewals\" />\n <MetricsCard\n icon={<AlertTriangle />}\n value={stats.unreadActionRequired}\n label=\"Need your action\"\n />\n </div>\n\n {/* Quick actions */}\n <div className=\"mt-6 grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4\">\n {[\n {\n label: 'Add a relationship',\n description: 'Track a new subscription',\n icon: <Repeat2 className=\"h-5 w-5\" />,\n path: '/relationships/new',\n },\n {\n label: 'Discover providers',\n description: 'Browse the registry',\n icon: <Sparkles className=\"h-5 w-5\" />,\n path: '/providers',\n },\n {\n label: 'Open inbox',\n description: 'Merged communications',\n icon: <Inbox className=\"h-5 w-5\" />,\n path: '/inbox',\n },\n {\n label: 'Configure agents',\n description: 'Automate your portfolio',\n icon: <Bot className=\"h-5 w-5\" />,\n path: '/agents',\n },\n ].map((qa) => (\n <button\n key={qa.path}\n type=\"button\"\n onClick={() => navigateTo(qa.path)}\n className=\"flex items-center gap-3 rounded-lg border border-border-subtle bg-bg-surface p-4 text-left transition-shadow hover:shadow-md\"\n >\n <span className=\"flex size-10 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\">{qa.label}</span>\n <span className=\"block text-xs text-text-secondary\">{qa.description}</span>\n </span>\n <ArrowRight className=\"h-4 w-4 shrink-0 text-text-secondary\" />\n </button>\n ))}\n </div>\n\n <div className=\"grid grid-cols-1 gap-8 lg:grid-cols-2\">\n {/* Upcoming renewals */}\n <Section\n title=\"Upcoming renewals\"\n description=\"What's charging next\"\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('/relationships')}\n className=\"text-sm font-medium text-text-link hover:underline\"\n >\n View all\n </button>\n }\n >\n {renewals.length === 0 ? (\n <EmptyState\n illustration=\"empty-calendar\"\n title=\"No upcoming renewals\"\n description=\"Renewals you track will appear here ahead of time.\"\n />\n ) : (\n <div className=\"divide-y divide-border-subtle rounded-lg border border-border-subtle bg-bg-surface\">\n {renewals.slice(0, 6).map((event) => (\n <button\n key={event.id}\n type=\"button\"\n onClick={() => navigateTo(`/relationships/${event.relationshipId}`)}\n className=\"flex w-full items-center justify-between gap-3 p-3 text-left hover:bg-bg-sunken\"\n >\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {formatEnumLabel(event.eventType)}\n </p>\n <p className=\"text-xs text-text-secondary\">\n {formatRelativeTime(event.occursAt)}\n {event.detail ? ` · ${event.detail}` : ''}\n </p>\n </div>\n {event.amount != null && (\n <span className=\"shrink-0 text-sm font-semibold tabular-nums text-text-primary\">\n {formatCurrency(event.amount, event.currency ?? stats.primaryCurrency)}\n </span>\n )}\n </button>\n ))}\n </div>\n )}\n </Section>\n\n {/* Action-required inbox */}\n <Section\n title=\"Needs your attention\"\n description=\"Required actions from the Universal Inbox\"\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('/inbox')}\n className=\"text-sm font-medium text-text-link hover:underline\"\n >\n Open inbox\n </button>\n }\n >\n {messages.filter((m) => m.actionRequired).length === 0 ? (\n <EmptyState\n illustration=\"empty-inbox\"\n title=\"You're all caught up\"\n description=\"Required actions across all your channels will surface here.\"\n />\n ) : (\n <div className=\"divide-y divide-border-subtle rounded-lg border border-border-subtle bg-bg-surface\">\n {messages\n .filter((m) => m.actionRequired)\n .slice(0, 6)\n .map((message) => (\n <button\n key={message.id}\n type=\"button\"\n onClick={() => navigateTo('/inbox')}\n className=\"flex w-full items-start justify-between gap-3 p-3 text-left hover:bg-bg-sunken\"\n >\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {message.subject || '(no subject)'}\n </p>\n {message.preview && (\n <p className=\"truncate text-xs text-text-secondary\">{message.preview}</p>\n )}\n </div>\n <StatusBadge status={message.category} className=\"shrink-0\" />\n </button>\n ))}\n </div>\n )}\n </Section>\n </div>\n\n {/* Lifecycle breakdown */}\n <Section title=\"Portfolio by lifecycle\" description=\"Where your relationships stand\">\n {statusBreakdown.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n title=\"No relationships yet\"\n description=\"Add your first subscription to see your portfolio breakdown.\"\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 Add relationship\n </button>\n }\n />\n ) : (\n <div className=\"flex flex-wrap gap-3\">\n {statusBreakdown.map(([status, count]) => (\n <div\n key={status}\n className=\"flex items-center gap-2 rounded-lg border border-border-subtle bg-bg-surface px-4 py-3\"\n >\n <StatusBadge status={status} />\n <span className=\"text-lg font-semibold tabular-nums text-text-primary\">\n {count}\n </span>\n </div>\n ))}\n {stats.openInvoiceCount > 0 && (\n <div className=\"flex items-center gap-2 rounded-lg border border-status-warning-text/30 bg-status-warning-bg-subtle px-4 py-3\">\n <span className=\"text-sm font-medium text-status-warning-text\">\n {stats.openInvoiceCount} open invoice{stats.openInvoiceCount !== 1 ? 's' : ''} ·{' '}\n {formatCurrency(stats.openInvoiceTotal, stats.primaryCurrency)}\n </span>\n </div>\n )}\n </div>\n )}\n </Section>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -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 * Filter by category / unread, mark messages read. Reads\n * `subListInboxMessages` (a SubInboxMessageConnection); writes\n * `subMarkMessageRead`.\n */\n\nimport { useState } from 'react';\nimport { Mail, MailOpen, AlertTriangle, Inbox as InboxIcon } from 'lucide-react';\nimport { PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../components/PageLayout';\nimport { StatusBadge } from '../components/StatusBadge';\nimport { EmptyState } from '../components/EmptyState';\nimport { LoadingView, ErrorView } from '../components/StateViews';\nimport { useSubscriberBot } from '../context/SubscriberBotContext';\nimport { useInboxMessages, useMarkMessageRead } from '../hooks/useSubscriberBotApi';\nimport { cn } from '../utils/cn';\nimport { formatRelativeTime } from '../utils/formatters';\nimport type { SubMessageCategory } from '../types';\n\nconst PAGE_SIZE = 25;\n\nconst CATEGORY_OPTIONS: Array<{ value: '' | SubMessageCategory; label: string }> = [\n { value: '', label: 'All categories' },\n { value: 'BILLING', label: 'Billing' },\n { value: 'RENEWAL', label: 'Renewals' },\n { value: 'REQUIRED_ACTION', label: 'Required actions' },\n { value: 'SECURITY_ALERT', label: 'Security alerts' },\n { value: 'PRODUCT_UPDATE', label: 'Product updates' },\n { value: 'MARKETING', label: 'Marketing' },\n { value: 'OTHER', label: 'Other' },\n];\n\nexport function InboxPage() {\n const { navigateTo } = useSubscriberBot();\n const [page, setPage] = useState(0);\n const [category, setCategory] = useState<'' | SubMessageCategory>('');\n const [unreadOnly, setUnreadOnly] = useState(false);\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 markRead = useMarkMessageRead();\n\n const items = data?.items ?? [];\n const hasMore = data?.pageInfo.hasMore ?? false;\n\n return (\n <PageLayout\n title=\"Universal Inbox\"\n description=\"Every subscription communication, merged and categorized\"\n >\n <PagePurpose className=\"mb-4\">\n Subscription communication is noisy and scattered. The Universal Inbox merges every channel\n a provider uses and uses AI to categorize each message so you never miss a renewal,\n required action or security notice.\n </PagePurpose>\n\n <div className=\"flex flex-wrap items-center gap-3\">\n <select\n value={category}\n onChange={(e) => {\n setCategory(e.target.value as '' | SubMessageCategory);\n setPage(0);\n }}\n className=\"rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-xs text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n {CATEGORY_OPTIONS.map((o) => (\n <option key={o.value} value={o.value}>\n {o.label}\n </option>\n ))}\n </select>\n <label className=\"flex items-center gap-2 text-sm text-text-primary\">\n <input\n type=\"checkbox\"\n checked={unreadOnly}\n onChange={(e) => {\n setUnreadOnly(e.target.checked);\n setPage(0);\n }}\n className=\"size-4 rounded border-border-subtle\"\n />\n Unread only\n </label>\n </div>\n\n {isLoading ? (\n <LoadingView label=\"Loading inbox…\" />\n ) : isError ? (\n <ErrorView message=\"Could not load the inbox.\" onRetry={() => void refetch()} />\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=\"Inbox is empty\"\n description={\n unreadOnly || category\n ? 'No messages match these filters.'\n : 'Merged subscription communications will appear here once channels start ingesting.'\n }\n />\n </div>\n ) : (\n <>\n <div className=\"mt-6 divide-y divide-border-subtle rounded-lg border border-border-subtle bg-bg-surface\">\n {items.map((m) => (\n <div\n key={m.id}\n className={cn(\n 'flex items-start gap-3 p-4',\n !m.isRead && 'bg-accent-wash'\n )}\n >\n <span className=\"mt-0.5 shrink-0 text-text-secondary\">\n {m.isRead ? (\n <MailOpen className=\"h-4 w-4\" />\n ) : (\n <Mail className=\"h-4 w-4 text-text-link\" />\n )}\n </span>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-center gap-2\">\n <p\n className={cn(\n 'truncate text-sm',\n m.isRead ? 'font-normal text-text-primary' : 'font-semibold text-text-primary'\n )}\n >\n {m.subject || '(no subject)'}\n </p>\n {m.actionRequired && (\n <span\n title=\"Action required\"\n className=\"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 >\n <AlertTriangle className=\"h-3 w-3\" /> Action\n </span>\n )}\n </div>\n {m.preview && (\n <p className=\"mt-0.5 line-clamp-2 text-xs text-text-secondary\">{m.preview}</p>\n )}\n <div className=\"mt-1.5 flex flex-wrap items-center gap-2\">\n <StatusBadge status={m.category} />\n <span className=\"text-xs text-text-secondary\">\n {formatRelativeTime(m.receivedAt)}\n </span>\n {m.relationshipId && (\n <button\n type=\"button\"\n onClick={() => navigateTo(`/relationships/${m.relationshipId}`)}\n className=\"text-xs font-medium text-text-link hover:underline\"\n >\n View relationship\n </button>\n )}\n </div>\n </div>\n {!m.isRead && (\n <button\n type=\"button\"\n disabled={markRead.isPending}\n onClick={() => markRead.mutate(m.id)}\n className=\"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 Mark read\n </button>\n )}\n </div>\n ))}\n </div>\n\n <div className=\"mt-4 flex items-center justify-between\">\n <p className=\"text-xs text-text-secondary\">\n Page {page + 1}\n {isFetching ? ' · refreshing…' : ''}\n </p>\n <div className=\"flex gap-2\">\n <button\n type=\"button\"\n disabled={page === 0}\n onClick={() => setPage((p) => Math.max(0, p - 1))}\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 Previous\n </button>\n <button\n type=\"button\"\n disabled={!hasMore}\n onClick={() => setPage((p) => p + 1)}\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 Next\n </button>\n </div>\n </div>\n </>\n )}\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 * Filter by category / unread, mark messages read. Reads\n * `subListInboxMessages` (a SubInboxMessageConnection); writes\n * `subMarkMessageRead`.\n */\n\nimport { useState } from 'react';\nimport { Mail, MailOpen, AlertTriangle, Inbox as InboxIcon } from 'lucide-react';\nimport { PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../components/PageLayout';\nimport { StatusBadge } from '../components/StatusBadge';\nimport { EmptyState } from '../components/EmptyState';\nimport { LoadingView, ErrorView } from '../components/StateViews';\nimport { useSubscriberBot } from '../context/SubscriberBotContext';\nimport { useInboxMessages, useMarkMessageRead } from '../hooks/useSubscriberBotApi';\nimport { cn } from '../utils/cn';\nimport { formatRelativeTime } from '../utils/formatters';\nimport type { SubMessageCategory } from '../types';\n\nconst PAGE_SIZE = 25;\n\nconst CATEGORY_OPTIONS: Array<{ value: '' | SubMessageCategory; label: string }> = [\n { value: '', label: 'All categories' },\n { value: 'BILLING', label: 'Billing' },\n { value: 'RENEWAL', label: 'Renewals' },\n { value: 'REQUIRED_ACTION', label: 'Required actions' },\n { value: 'SECURITY_ALERT', label: 'Security alerts' },\n { value: 'PRODUCT_UPDATE', label: 'Product updates' },\n { value: 'MARKETING', label: 'Marketing' },\n { value: 'OTHER', label: 'Other' },\n];\n\nexport function InboxPage() {\n const { navigateTo } = useSubscriberBot();\n const [page, setPage] = useState(0);\n const [category, setCategory] = useState<'' | SubMessageCategory>('');\n const [unreadOnly, setUnreadOnly] = useState(false);\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 markRead = useMarkMessageRead();\n\n const items = data?.items ?? [];\n const hasMore = data?.pageInfo.hasMore ?? false;\n\n return (\n <PageLayout\n title=\"Universal Inbox\"\n description=\"Every subscription communication, merged and categorized\"\n >\n <PagePurpose className=\"mb-4\">\n Subscription communication is noisy and scattered. The Universal Inbox merges every channel\n a provider uses and uses AI to categorize each message so you never miss a renewal, required\n action or security notice.\n </PagePurpose>\n\n <div className=\"flex flex-wrap items-center gap-3\">\n <select\n value={category}\n onChange={(e) => {\n setCategory(e.target.value as '' | SubMessageCategory);\n setPage(0);\n }}\n className=\"rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-xs text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n {CATEGORY_OPTIONS.map((o) => (\n <option key={o.value} value={o.value}>\n {o.label}\n </option>\n ))}\n </select>\n <label className=\"flex items-center gap-2 text-sm text-text-primary\">\n <input\n type=\"checkbox\"\n checked={unreadOnly}\n onChange={(e) => {\n setUnreadOnly(e.target.checked);\n setPage(0);\n }}\n className=\"size-4 rounded border-border-subtle\"\n />\n Unread only\n </label>\n </div>\n\n {isLoading ? (\n <LoadingView label=\"Loading inbox…\" />\n ) : isError ? (\n <ErrorView message=\"Could not load the inbox.\" onRetry={() => void refetch()} />\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=\"Inbox is empty\"\n description={\n unreadOnly || category\n ? 'No messages match these filters.'\n : 'Merged subscription communications will appear here once channels start ingesting.'\n }\n />\n </div>\n ) : (\n <>\n <div className=\"mt-6 divide-y divide-border-subtle rounded-lg border border-border-subtle bg-bg-surface\">\n {items.map((m) => (\n <div\n key={m.id}\n className={cn('flex items-start gap-3 p-4', !m.isRead && 'bg-accent-wash')}\n >\n <span className=\"mt-0.5 shrink-0 text-text-secondary\">\n {m.isRead ? (\n <MailOpen className=\"h-4 w-4\" />\n ) : (\n <Mail className=\"h-4 w-4 text-text-link\" />\n )}\n </span>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-center gap-2\">\n <p\n className={cn(\n 'truncate text-sm',\n m.isRead\n ? 'font-normal text-text-primary'\n : 'font-semibold text-text-primary'\n )}\n >\n {m.subject || '(no subject)'}\n </p>\n {m.actionRequired && (\n <span\n title=\"Action required\"\n className=\"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 >\n <AlertTriangle className=\"h-3 w-3\" /> Action\n </span>\n )}\n </div>\n {m.preview && (\n <p className=\"mt-0.5 line-clamp-2 text-xs text-text-secondary\">{m.preview}</p>\n )}\n <div className=\"mt-1.5 flex flex-wrap items-center gap-2\">\n <StatusBadge status={m.category} />\n <span className=\"text-xs text-text-secondary\">\n {formatRelativeTime(m.receivedAt)}\n </span>\n {m.relationshipId && (\n <button\n type=\"button\"\n onClick={() => navigateTo(`/relationships/${m.relationshipId}`)}\n className=\"text-xs font-medium text-text-link hover:underline\"\n >\n View relationship\n </button>\n )}\n </div>\n </div>\n {!m.isRead && (\n <button\n type=\"button\"\n disabled={markRead.isPending}\n onClick={() => markRead.mutate(m.id)}\n className=\"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 Mark read\n </button>\n )}\n </div>\n ))}\n </div>\n\n <div className=\"mt-4 flex items-center justify-between\">\n <p className=\"text-xs text-text-secondary\">\n Page {page + 1}\n {isFetching ? ' · refreshing…' : ''}\n </p>\n <div className=\"flex gap-2\">\n <button\n type=\"button\"\n disabled={page === 0}\n onClick={() => setPage((p) => Math.max(0, p - 1))}\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 Previous\n </button>\n <button\n type=\"button\"\n disabled={!hasMore}\n onClick={() => setPage((p) => p + 1)}\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 Next\n </button>\n </div>\n </div>\n </>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WorkflowsPage.js","names":[],"sources":["../../../src/subscriberbot/pages/WorkflowsPage.tsx"],"sourcesContent":["/**\n * Workflows (Automation)\n * @module subscriberbot/pages\n *\n * The Workflow Engine — event-driven automation (\"when invoice arrives →\n * classify → update relationship → notify finance\"). List + create + activate.\n * Reads `subListWorkflows`; writes `subCreateWorkflow` / `subActivateWorkflow`.\n */\n\nimport { useState } from 'react';\nimport { Plus, Workflow as WorkflowIcon, Play, X } from 'lucide-react';\nimport { PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../components/PageLayout';\nimport { StatusBadge } from '../components/StatusBadge';\nimport { EmptyState } from '../components/EmptyState';\nimport { LoadingView, ErrorView } from '../components/StateViews';\nimport { useSubscriberBot } from '../context/SubscriberBotContext';\nimport {\n useWorkflows,\n useCreateWorkflow,\n useActivateWorkflow,\n} from '../hooks/useSubscriberBotApi';\nimport { formatRelativeTime } from '../utils/formatters';\nimport type { SubCreateWorkflowInput, SubWorkflowStatus } from '../types';\n\nconst STATUS_OPTIONS: Array<{ value: '' | SubWorkflowStatus; label: string }> = [\n { value: '', label: 'All statuses' },\n { value: 'ACTIVE', label: 'Active' },\n { value: 'DRAFT', label: 'Draft' },\n { value: 'PAUSED', label: 'Paused' },\n { value: 'ARCHIVED', label: 'Archived' },\n];\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\nexport function WorkflowsPage() {\n const { navigate } = useSubscriberBot();\n const [statusFilter, setStatusFilter] = useState<'' | SubWorkflowStatus>('');\n const [showCreate, setShowCreate] = useState(false);\n\n const { data, isLoading, isError, refetch } = useWorkflows(statusFilter || undefined);\n const create = useCreateWorkflow();\n const activate = useActivateWorkflow();\n\n const [name, setName] = useState('');\n const [triggerEvent, setTriggerEvent] = useState('');\n const [description, setDescription] = useState('');\n const [formError, setFormError] = useState<string | null>(null);\n\n const workflows = data ?? [];\n // `navigate` is only consumed via the back-compat context; reference it so\n // its presence keeps integrated routing wired (no-op otherwise).\n void navigate;\n\n const resetForm = () => {\n setName('');\n setTriggerEvent('');\n setDescription('');\n setFormError(null);\n };\n\n const handleCreate = (e: React.FormEvent) => {\n e.preventDefault();\n setFormError(null);\n if (!name.trim() || !triggerEvent.trim()) {\n setFormError('Name and trigger event are required.');\n return;\n }\n const input: SubCreateWorkflowInput = {\n name: name.trim(),\n triggerEvent: triggerEvent.trim(),\n ...(description.trim() ? { description: description.trim() } : {}),\n };\n create.mutate(input, {\n onSuccess: () => {\n resetForm();\n setShowCreate(false);\n },\n onError: (err) =>\n setFormError(err instanceof Error ? err.message : 'Could not create workflow.'),\n });\n };\n\n return (\n <PageLayout\n title=\"Workflows\"\n description=\"Event-driven automation across your portfolio\"\n actions={\n <button\n type=\"button\"\n onClick={() => setShowCreate(true)}\n className=\"inline-flex items-center gap-2 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:opacity-90\"\n >\n <Plus className=\"h-4 w-4\" /> New workflow\n </button>\n }\n >\n <PagePurpose className=\"mb-4\">\n Workflows react to domain events — a new invoice, a detected renewal, a cancelled\n relationship — and run multi-step automations. Draft a workflow, then activate it when\n you're ready.\n </PagePurpose>\n\n <div className=\"flex flex-wrap items-center gap-3\">\n <select\n value={statusFilter}\n onChange={(e) => setStatusFilter(e.target.value as '' | SubWorkflowStatus)}\n className=\"rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-xs text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n {STATUS_OPTIONS.map((o) => (\n <option key={o.value} value={o.value}>\n {o.label}\n </option>\n ))}\n </select>\n </div>\n\n {isLoading ? (\n <LoadingView label=\"Loading workflows…\" />\n ) : isError ? (\n <ErrorView message=\"Could not load workflows.\" onRetry={() => void refetch()} />\n ) : workflows.length === 0 ? (\n <div className=\"mt-6\">\n <EmptyState\n illustration=\"empty-data\"\n icon={<WorkflowIcon className=\"h-12 w-12\" />}\n title=\"No workflows yet\"\n description=\"Create your first event-driven workflow to automate your recurring portfolio.\"\n action={\n <button\n type=\"button\"\n onClick={() => setShowCreate(true)}\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 New workflow\n </button>\n }\n />\n </div>\n ) : (\n <div className=\"mt-6 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {workflows.map((wf) => (\n <div key={wf.id} className=\"flex flex-col rounded-lg border border-border-subtle bg-bg-surface p-4\">\n <div className=\"flex items-start justify-between gap-2\">\n <span className=\"flex size-9 items-center justify-center rounded-lg bg-[var(--color-accent-soft)] text-text-primary\">\n <WorkflowIcon className=\"h-4 w-4\" />\n </span>\n <StatusBadge status={wf.status} />\n </div>\n <p className=\"mt-3 truncate text-sm font-semibold text-text-primary\">{wf.name}</p>\n <p className=\"truncate text-xs text-text-secondary\">on {wf.triggerEvent}</p>\n {wf.description && (\n <p className=\"mt-2 line-clamp-2 text-xs text-text-secondary\">{wf.description}</p>\n )}\n <p className=\"mt-2 text-xs text-text-secondary\">\n Ran {wf.runCount}×\n {wf.lastRunAt ? ` · last ${formatRelativeTime(wf.lastRunAt)}` : ''}\n </p>\n {wf.status !== 'ACTIVE' && (\n <button\n type=\"button\"\n disabled={activate.isPending}\n onClick={() => activate.mutate(wf.id)}\n className=\"mt-3 inline-flex items-center justify-center gap-1.5 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 <Play className=\"h-3.5 w-3.5\" /> Activate\n </button>\n )}\n </div>\n ))}\n </div>\n )}\n\n {/* Create modal */}\n {showCreate && (\n <div className=\"fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4\">\n <div className=\"w-full max-w-lg rounded-lg border border-border-subtle bg-bg-surface p-5\">\n <div className=\"mb-4 flex items-center justify-between\">\n <h3 className=\"text-lg font-semibold text-text-primary\">New workflow</h3>\n <button\n type=\"button\"\n onClick={() => {\n setShowCreate(false);\n resetForm();\n }}\n className=\"rounded-md p-1 text-text-secondary hover:text-text-primary\"\n >\n <X className=\"h-5 w-5\" />\n </button>\n </div>\n <form onSubmit={handleCreate} 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=\"wf-name\" className={labelCls}>\n Name <span className=\"text-status-error-text\">*</span>\n </label>\n <input\n id=\"wf-name\"\n type=\"text\"\n value={name}\n onChange={(e) => setName(e.target.value)}\n placeholder=\"e.g. Invoice → notify finance\"\n className={inputCls}\n required\n />\n </div>\n <div>\n <label htmlFor=\"wf-trigger\" className={labelCls}>\n Trigger event <span className=\"text-status-error-text\">*</span>\n </label>\n <input\n id=\"wf-trigger\"\n type=\"text\"\n value={triggerEvent}\n onChange={(e) => setTriggerEvent(e.target.value)}\n placeholder=\"e.g. InvoiceReceived, RenewalDetected\"\n className={inputCls}\n required\n />\n </div>\n <div>\n <label htmlFor=\"wf-desc\" className={labelCls}>\n Description\n </label>\n <textarea\n id=\"wf-desc\"\n value={description}\n onChange={(e) => setDescription(e.target.value)}\n rows={3}\n placeholder=\"What should this workflow do when the event fires?\"\n className={inputCls}\n />\n </div>\n <div className=\"flex justify-end gap-3\">\n <button\n type=\"button\"\n onClick={() => {\n setShowCreate(false);\n resetForm();\n }}\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 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 ? 'Creating…' : 'Create workflow'}\n </button>\n </div>\n </form>\n </div>\n </div>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"WorkflowsPage.js","names":[],"sources":["../../../src/subscriberbot/pages/WorkflowsPage.tsx"],"sourcesContent":["/**\n * Workflows (Automation)\n * @module subscriberbot/pages\n *\n * The Workflow Engine — event-driven automation (\"when invoice arrives →\n * classify → update relationship → notify finance\"). List + create + activate.\n * Reads `subListWorkflows`; writes `subCreateWorkflow` / `subActivateWorkflow`.\n */\n\nimport { useState } from 'react';\nimport { Plus, Workflow as WorkflowIcon, Play, X } from 'lucide-react';\nimport { PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../components/PageLayout';\nimport { StatusBadge } from '../components/StatusBadge';\nimport { EmptyState } from '../components/EmptyState';\nimport { LoadingView, ErrorView } from '../components/StateViews';\nimport { useSubscriberBot } from '../context/SubscriberBotContext';\nimport { useWorkflows, useCreateWorkflow, useActivateWorkflow } from '../hooks/useSubscriberBotApi';\nimport { formatRelativeTime } from '../utils/formatters';\nimport type { SubCreateWorkflowInput, SubWorkflowStatus } from '../types';\n\nconst STATUS_OPTIONS: Array<{ value: '' | SubWorkflowStatus; label: string }> = [\n { value: '', label: 'All statuses' },\n { value: 'ACTIVE', label: 'Active' },\n { value: 'DRAFT', label: 'Draft' },\n { value: 'PAUSED', label: 'Paused' },\n { value: 'ARCHIVED', label: 'Archived' },\n];\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\nexport function WorkflowsPage() {\n const { navigate } = useSubscriberBot();\n const [statusFilter, setStatusFilter] = useState<'' | SubWorkflowStatus>('');\n const [showCreate, setShowCreate] = useState(false);\n\n const { data, isLoading, isError, refetch } = useWorkflows(statusFilter || undefined);\n const create = useCreateWorkflow();\n const activate = useActivateWorkflow();\n\n const [name, setName] = useState('');\n const [triggerEvent, setTriggerEvent] = useState('');\n const [description, setDescription] = useState('');\n const [formError, setFormError] = useState<string | null>(null);\n\n const workflows = data ?? [];\n // `navigate` is only consumed via the back-compat context; reference it so\n // its presence keeps integrated routing wired (no-op otherwise).\n void navigate;\n\n const resetForm = () => {\n setName('');\n setTriggerEvent('');\n setDescription('');\n setFormError(null);\n };\n\n const handleCreate = (e: React.FormEvent) => {\n e.preventDefault();\n setFormError(null);\n if (!name.trim() || !triggerEvent.trim()) {\n setFormError('Name and trigger event are required.');\n return;\n }\n const input: SubCreateWorkflowInput = {\n name: name.trim(),\n triggerEvent: triggerEvent.trim(),\n ...(description.trim() ? { description: description.trim() } : {}),\n };\n create.mutate(input, {\n onSuccess: () => {\n resetForm();\n setShowCreate(false);\n },\n onError: (err) =>\n setFormError(err instanceof Error ? err.message : 'Could not create workflow.'),\n });\n };\n\n return (\n <PageLayout\n title=\"Workflows\"\n description=\"Event-driven automation across your portfolio\"\n actions={\n <button\n type=\"button\"\n onClick={() => setShowCreate(true)}\n className=\"inline-flex items-center gap-2 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:opacity-90\"\n >\n <Plus className=\"h-4 w-4\" /> New workflow\n </button>\n }\n >\n <PagePurpose className=\"mb-4\">\n Workflows react to domain events — a new invoice, a detected renewal, a cancelled\n relationship — and run multi-step automations. Draft a workflow, then activate it when\n you're ready.\n </PagePurpose>\n\n <div className=\"flex flex-wrap items-center gap-3\">\n <select\n value={statusFilter}\n onChange={(e) => setStatusFilter(e.target.value as '' | SubWorkflowStatus)}\n className=\"rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-xs text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n {STATUS_OPTIONS.map((o) => (\n <option key={o.value} value={o.value}>\n {o.label}\n </option>\n ))}\n </select>\n </div>\n\n {isLoading ? (\n <LoadingView label=\"Loading workflows…\" />\n ) : isError ? (\n <ErrorView message=\"Could not load workflows.\" onRetry={() => void refetch()} />\n ) : workflows.length === 0 ? (\n <div className=\"mt-6\">\n <EmptyState\n illustration=\"empty-data\"\n icon={<WorkflowIcon className=\"h-12 w-12\" />}\n title=\"No workflows yet\"\n description=\"Create your first event-driven workflow to automate your recurring portfolio.\"\n action={\n <button\n type=\"button\"\n onClick={() => setShowCreate(true)}\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 New workflow\n </button>\n }\n />\n </div>\n ) : (\n <div className=\"mt-6 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {workflows.map((wf) => (\n <div\n key={wf.id}\n className=\"flex flex-col rounded-lg border border-border-subtle bg-bg-surface p-4\"\n >\n <div className=\"flex items-start justify-between gap-2\">\n <span className=\"flex size-9 items-center justify-center rounded-lg bg-[var(--color-accent-soft)] text-text-primary\">\n <WorkflowIcon className=\"h-4 w-4\" />\n </span>\n <StatusBadge status={wf.status} />\n </div>\n <p className=\"mt-3 truncate text-sm font-semibold text-text-primary\">{wf.name}</p>\n <p className=\"truncate text-xs text-text-secondary\">on {wf.triggerEvent}</p>\n {wf.description && (\n <p className=\"mt-2 line-clamp-2 text-xs text-text-secondary\">{wf.description}</p>\n )}\n <p className=\"mt-2 text-xs text-text-secondary\">\n Ran {wf.runCount}×\n {wf.lastRunAt ? ` · last ${formatRelativeTime(wf.lastRunAt)}` : ''}\n </p>\n {wf.status !== 'ACTIVE' && (\n <button\n type=\"button\"\n disabled={activate.isPending}\n onClick={() => activate.mutate(wf.id)}\n className=\"mt-3 inline-flex items-center justify-center gap-1.5 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 <Play className=\"h-3.5 w-3.5\" /> Activate\n </button>\n )}\n </div>\n ))}\n </div>\n )}\n\n {/* Create modal */}\n {showCreate && (\n <div className=\"fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4\">\n <div className=\"w-full max-w-lg rounded-lg border border-border-subtle bg-bg-surface p-5\">\n <div className=\"mb-4 flex items-center justify-between\">\n <h3 className=\"text-lg font-semibold text-text-primary\">New workflow</h3>\n <button\n type=\"button\"\n onClick={() => {\n setShowCreate(false);\n resetForm();\n }}\n className=\"rounded-md p-1 text-text-secondary hover:text-text-primary\"\n >\n <X className=\"h-5 w-5\" />\n </button>\n </div>\n <form onSubmit={handleCreate} 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=\"wf-name\" className={labelCls}>\n Name <span className=\"text-status-error-text\">*</span>\n </label>\n <input\n id=\"wf-name\"\n type=\"text\"\n value={name}\n onChange={(e) => setName(e.target.value)}\n placeholder=\"e.g. Invoice → notify finance\"\n className={inputCls}\n required\n />\n </div>\n <div>\n <label htmlFor=\"wf-trigger\" className={labelCls}>\n Trigger event <span className=\"text-status-error-text\">*</span>\n </label>\n <input\n id=\"wf-trigger\"\n type=\"text\"\n value={triggerEvent}\n onChange={(e) => setTriggerEvent(e.target.value)}\n placeholder=\"e.g. InvoiceReceived, RenewalDetected\"\n className={inputCls}\n required\n />\n </div>\n <div>\n <label htmlFor=\"wf-desc\" className={labelCls}>\n Description\n </label>\n <textarea\n id=\"wf-desc\"\n value={description}\n onChange={(e) => setDescription(e.target.value)}\n rows={3}\n placeholder=\"What should this workflow do when the event fires?\"\n className={inputCls}\n />\n </div>\n <div className=\"flex justify-end gap-3\">\n <button\n type=\"button\"\n onClick={() => {\n setShowCreate(false);\n resetForm();\n }}\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 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 ? 'Creating…' : 'Create workflow'}\n </button>\n </div>\n </form>\n </div>\n </div>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AgentDetailPage.js","names":[],"sources":["../../../../src/subscriberbot/pages/agents/AgentDetailPage.tsx"],"sourcesContent":["/**\n * Agent detail\n * @module subscriberbot/pages/agents\n *\n * A single AI agent: its kind, posture, last run, and the autonomous policies\n * that bound it. Reads `subAgent(id)`; changes posture via `subSetAgentStatus`.\n */\n\nimport { useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport { ArrowLeft, Bot, ShieldCheck } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingView, ErrorView, Section } from '../../components/StateViews';\nimport { useSubscriberBot } from '../../context/SubscriberBotContext';\nimport { useAgent, useSetAgentStatus } from '../../hooks/useSubscriberBotApi';\nimport { formatEnumLabel, formatRelativeTime } from '../../utils/formatters';\nimport type { SubAgentStatus } from '../../types';\n\nconst STATUS_OPTIONS: SubAgentStatus[] = ['DISABLED', 'SUGGEST_ONLY', 'AUTONOMOUS'];\n\nexport function AgentDetailPage() {\n const { id } = useParams<{ id: string }>();\n const { navigateTo } = useSubscriberBot();\n const { data: agent, isLoading, isError, refetch } = useAgent(id);\n const setAgentStatus = useSetAgentStatus();\n const [actionError, setActionError] = useState<string | null>(null);\n\n if (isLoading) {\n return (\n <PageLayout title=\"Agent\">\n <LoadingView label=\"Loading agent…\" />\n </PageLayout>\n );\n }\n\n if (isError) {\n return (\n <PageLayout title=\"Agent\">\n <ErrorView message=\"Could not load this agent.\" onRetry={() => void refetch()} />\n </PageLayout>\n );\n }\n\n if (!agent) {\n return (\n <PageLayout title=\"Agent\">\n <EmptyState\n illustration=\"empty-data\"\n title=\"Agent not found\"\n description=\"It may have been removed, or you may not have access to it.\"\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('/agents')}\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 Back to agents\n </button>\n }\n />\n </PageLayout>\n );\n }\n\n const a = agent;\n\n return (\n <PageLayout\n title={a.name}\n description={`${formatEnumLabel(a.kind)} agent`}\n actions={\n <button\n type=\"button\"\n onClick={() => navigateTo('/agents')}\n className=\"inline-flex items-center gap-2 rounded-lg border border-border-subtle px-3 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken\"\n >\n <ArrowLeft className=\"h-4 w-4\" /> Back\n </button>\n }\n >\n <div className=\"flex flex-wrap items-center gap-3\">\n <span className=\"flex size-10 items-center justify-center rounded-lg bg-[var(--color-accent-soft)] text-text-primary\">\n <Bot className=\"h-5 w-5\" />\n </span>\n <StatusBadge status={a.status} />\n <span className=\"text-sm text-text-secondary\">\n {a.lastRunAt ? `Last run ${formatRelativeTime(a.lastRunAt)}` : 'Never run'}\n </span>\n </div>\n\n {a.description && (\n <p className=\"mt-4 max-w-3xl text-sm text-text-secondary\">{a.description}</p>\n )}\n\n {actionError && (\n <div className=\"mt-4 rounded-lg border border-status-error-text/30 bg-status-error-bg-subtle px-4 py-2 text-sm text-status-error-text\">\n {actionError}\n </div>\n )}\n\n {/* Posture control */}\n <div className=\"mt-6 max-w-sm\">\n <label
|
|
1
|
+
{"version":3,"file":"AgentDetailPage.js","names":[],"sources":["../../../../src/subscriberbot/pages/agents/AgentDetailPage.tsx"],"sourcesContent":["/**\n * Agent detail\n * @module subscriberbot/pages/agents\n *\n * A single AI agent: its kind, posture, last run, and the autonomous policies\n * that bound it. Reads `subAgent(id)`; changes posture via `subSetAgentStatus`.\n */\n\nimport { useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport { ArrowLeft, Bot, ShieldCheck } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingView, ErrorView, Section } from '../../components/StateViews';\nimport { useSubscriberBot } from '../../context/SubscriberBotContext';\nimport { useAgent, useSetAgentStatus } from '../../hooks/useSubscriberBotApi';\nimport { formatEnumLabel, formatRelativeTime } from '../../utils/formatters';\nimport type { SubAgentStatus } from '../../types';\n\nconst STATUS_OPTIONS: SubAgentStatus[] = ['DISABLED', 'SUGGEST_ONLY', 'AUTONOMOUS'];\n\nexport function AgentDetailPage() {\n const { id } = useParams<{ id: string }>();\n const { navigateTo } = useSubscriberBot();\n const { data: agent, isLoading, isError, refetch } = useAgent(id);\n const setAgentStatus = useSetAgentStatus();\n const [actionError, setActionError] = useState<string | null>(null);\n\n if (isLoading) {\n return (\n <PageLayout title=\"Agent\">\n <LoadingView label=\"Loading agent…\" />\n </PageLayout>\n );\n }\n\n if (isError) {\n return (\n <PageLayout title=\"Agent\">\n <ErrorView message=\"Could not load this agent.\" onRetry={() => void refetch()} />\n </PageLayout>\n );\n }\n\n if (!agent) {\n return (\n <PageLayout title=\"Agent\">\n <EmptyState\n illustration=\"empty-data\"\n title=\"Agent not found\"\n description=\"It may have been removed, or you may not have access to it.\"\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('/agents')}\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 Back to agents\n </button>\n }\n />\n </PageLayout>\n );\n }\n\n const a = agent;\n\n return (\n <PageLayout\n title={a.name}\n description={`${formatEnumLabel(a.kind)} agent`}\n actions={\n <button\n type=\"button\"\n onClick={() => navigateTo('/agents')}\n className=\"inline-flex items-center gap-2 rounded-lg border border-border-subtle px-3 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken\"\n >\n <ArrowLeft className=\"h-4 w-4\" /> Back\n </button>\n }\n >\n <div className=\"flex flex-wrap items-center gap-3\">\n <span className=\"flex size-10 items-center justify-center rounded-lg bg-[var(--color-accent-soft)] text-text-primary\">\n <Bot className=\"h-5 w-5\" />\n </span>\n <StatusBadge status={a.status} />\n <span className=\"text-sm text-text-secondary\">\n {a.lastRunAt ? `Last run ${formatRelativeTime(a.lastRunAt)}` : 'Never run'}\n </span>\n </div>\n\n {a.description && (\n <p className=\"mt-4 max-w-3xl text-sm text-text-secondary\">{a.description}</p>\n )}\n\n {actionError && (\n <div className=\"mt-4 rounded-lg border border-status-error-text/30 bg-status-error-bg-subtle px-4 py-2 text-sm text-status-error-text\">\n {actionError}\n </div>\n )}\n\n {/* Posture control */}\n <div className=\"mt-6 max-w-sm\">\n <label\n htmlFor=\"agent-posture\"\n className=\"mb-1 block text-xs font-medium text-text-secondary\"\n >\n Operating posture\n </label>\n <select\n id=\"agent-posture\"\n value={a.status}\n disabled={setAgentStatus.isPending}\n onChange={(e) => {\n setActionError(null);\n setAgentStatus.mutate(\n { id: a.id, status: e.target.value as SubAgentStatus },\n {\n onError: (err) =>\n setActionError(err instanceof Error ? err.message : 'Could not change posture.'),\n }\n );\n }}\n className=\"w-full rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)] disabled:opacity-50\"\n >\n {STATUS_OPTIONS.map((s) => (\n <option key={s} value={s}>\n {formatEnumLabel(s)}\n </option>\n ))}\n </select>\n <p className=\"mt-1.5 text-xs text-text-secondary\">\n Disabled never acts; Suggest-only proposes; Autonomous acts within its policies.\n </p>\n </div>\n\n {/* Policies */}\n <Section title=\"Policies\" description=\"The autonomous rules that bound this agent\">\n {!a.policies || a.policies.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n title=\"No policies\"\n description=\"Policies define what this agent may do autonomously, and what it must alert on.\"\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-3 sm:grid-cols-2\">\n {a.policies.map((policy) => (\n <GlassCard key={policy.id} treatment=\"metric\" className=\"p-4\">\n <div className=\"flex items-start justify-between gap-2\">\n <p className=\"text-sm font-medium text-text-primary\">{policy.name}</p>\n <StatusBadge status={policy.isEnabled ? 'ACTIVE' : 'DISABLED'} />\n </div>\n <p className=\"mt-1 flex items-center gap-1.5 text-xs text-text-secondary\">\n <ShieldCheck className=\"h-3.5 w-3.5\" /> Action: {formatEnumLabel(policy.action)}\n </p>\n {policy.description && (\n <p className=\"mt-2 text-xs text-text-secondary\">{policy.description}</p>\n )}\n <p className=\"mt-2 text-xs text-text-secondary\">\n Triggered {policy.triggerCount}×\n {policy.lastTriggeredAt\n ? ` · last ${formatRelativeTime(policy.lastTriggeredAt)}`\n : ''}\n </p>\n </GlassCard>\n ))}\n </div>\n )}\n </Section>\n </PageLayout>\n );\n}\n"],"mappings":";;;;;;;;;;;;;AAqBA,IAAM,IAAmC;CAAC;CAAY;CAAgB;CAAa;AAEnF,SAAgB,IAAkB;CAChC,IAAM,EAAE,UAAO,GAA2B,EACpC,EAAE,kBAAe,GAAkB,EACnC,EAAE,MAAM,GAAO,cAAW,YAAS,eAAY,EAAS,EAAG,EAC3D,IAAiB,GAAmB,EACpC,CAAC,GAAa,KAAkB,EAAwB,KAAK;AAEnE,KAAI,EACF,QACE,kBAAC,GAAD;EAAY,OAAM;YAChB,kBAAC,GAAD,EAAa,OAAM,kBAAmB,CAAA;EAC3B,CAAA;AAIjB,KAAI,EACF,QACE,kBAAC,GAAD;EAAY,OAAM;YAChB,kBAAC,GAAD;GAAW,SAAQ;GAA6B,eAAe,KAAK,GAAS;GAAI,CAAA;EACtE,CAAA;AAIjB,KAAI,CAAC,EACH,QACE,kBAAC,GAAD;EAAY,OAAM;YAChB,kBAAC,GAAD;GACE,cAAa;GACb,OAAM;GACN,aAAY;GACZ,QACE,kBAAC,UAAD;IACE,MAAK;IACL,eAAe,EAAW,UAAU;IACpC,WAAU;cACX;IAEQ,CAAA;GAEX,CAAA;EACS,CAAA;CAIjB,IAAM,IAAI;AAEV,QACE,kBAAC,GAAD;EACE,OAAO,EAAE;EACT,aAAa,GAAG,EAAgB,EAAE,KAAK,CAAC;EACxC,SACE,kBAAC,UAAD;GACE,MAAK;GACL,eAAe,EAAW,UAAU;GACpC,WAAU;aAHZ,CAKE,kBAAC,GAAD,EAAW,WAAU,WAAY,CAAA,EAAA,QAC1B;;YAVb;GAaE,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,QAAD;MAAM,WAAU;gBACd,kBAAC,GAAD,EAAK,WAAU,WAAY,CAAA;MACtB,CAAA;KACP,kBAAC,GAAD,EAAa,QAAQ,EAAE,QAAU,CAAA;KACjC,kBAAC,QAAD;MAAM,WAAU;gBACb,EAAE,YAAY,YAAY,EAAmB,EAAE,UAAU,KAAK;MAC1D,CAAA;KACH;;GAEL,EAAE,eACD,kBAAC,KAAD;IAAG,WAAU;cAA8C,EAAE;IAAgB,CAAA;GAG9E,KACC,kBAAC,OAAD;IAAK,WAAU;cACZ;IACG,CAAA;GAIR,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,SAAD;MACE,SAAQ;MACR,WAAU;gBACX;MAEO,CAAA;KACR,kBAAC,UAAD;MACE,IAAG;MACH,OAAO,EAAE;MACT,UAAU,EAAe;MACzB,WAAW,MAAM;AAEf,OADA,EAAe,KAAK,EACpB,EAAe,OACb;QAAE,IAAI,EAAE;QAAI,QAAQ,EAAE,OAAO;QAAyB,EACtD,EACE,UAAU,MACR,EAAe,aAAe,QAAQ,EAAI,UAAU,4BAA4B,EACnF,CACF;;MAEH,WAAU;gBAET,EAAe,KAAK,MACnB,kBAAC,UAAD;OAAgB,OAAO;iBACpB,EAAgB,EAAE;OACZ,EAFI,EAEJ,CACT;MACK,CAAA;KACT,kBAAC,KAAD;MAAG,WAAU;gBAAqC;MAE9C,CAAA;KACA;;GAGN,kBAAC,GAAD;IAAS,OAAM;IAAW,aAAY;cACnC,CAAC,EAAE,YAAY,EAAE,SAAS,WAAW,IACpC,kBAAC,GAAD;KACE,cAAa;KACb,OAAM;KACN,aAAY;KACZ,CAAA,GAEF,kBAAC,OAAD;KAAK,WAAU;eACZ,EAAE,SAAS,KAAK,MACf,kBAAC,GAAD;MAA2B,WAAU;MAAS,WAAU;gBAAxD;OACE,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,KAAD;SAAG,WAAU;mBAAyC,EAAO;SAAS,CAAA,EACtE,kBAAC,GAAD,EAAa,QAAQ,EAAO,YAAY,WAAW,YAAc,CAAA,CAC7D;;OACN,kBAAC,KAAD;QAAG,WAAU;kBAAb;SACE,kBAAC,GAAD,EAAa,WAAU,eAAgB,CAAA;;SAAU,EAAgB,EAAO,OAAO;SAC7E;;OACH,EAAO,eACN,kBAAC,KAAD;QAAG,WAAU;kBAAoC,EAAO;QAAgB,CAAA;OAE1E,kBAAC,KAAD;QAAG,WAAU;kBAAb;SAAgD;SACnC,EAAO;SAAa;SAC9B,EAAO,kBACJ,WAAW,EAAmB,EAAO,gBAAgB,KACrD;SACF;;OACM;QAjBI,EAAO,GAiBX,CACZ;KACE,CAAA;IAEA,CAAA;GACC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AgentsListPage.js","names":[],"sources":["../../../../src/subscriberbot/pages/agents/AgentsListPage.tsx"],"sourcesContent":["/**\n * AI Agents list\n * @module subscriberbot/pages/agents\n *\n * The AI Agents Framework — specialized agents (Discovery, Renewal, Cost,\n * Newsletter, Vendor, Compliance, Negotiation, Migration, Concierge) that each\n * own a slice of the recurring-relationship problem. Filter by kind / posture.\n * Reads `subListAgents`; sets posture inline via `subSetAgentStatus`.\n */\n\nimport { useState } from 'react';\nimport { Plus, Bot } from 'lucide-react';\nimport { PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { useSubscriberBot } from '../../context/SubscriberBotContext';\nimport { useAgents, useSetAgentStatus } from '../../hooks/useSubscriberBotApi';\nimport { formatEnumLabel, formatRelativeTime } from '../../utils/formatters';\nimport type { SubAgentKind, SubAgentStatus } from '../../types';\n\nconst KIND_OPTIONS: Array<{ value: '' | SubAgentKind; label: string }> = [\n { value: '', label: 'All kinds' },\n { value: 'DISCOVERY', label: 'Discovery' },\n { value: 'RENEWAL', label: 'Renewal' },\n { value: 'COST', label: 'Cost' },\n { value: 'NEWSLETTER', label: 'Newsletter' },\n { value: 'VENDOR', label: 'Vendor' },\n { value: 'COMPLIANCE', label: 'Compliance' },\n { value: 'NEGOTIATION', label: 'Negotiation' },\n { value: 'MIGRATION', label: 'Migration' },\n { value: 'CONCIERGE', label: 'Concierge' },\n];\n\nconst STATUS_OPTIONS: SubAgentStatus[] = ['DISABLED', 'SUGGEST_ONLY', 'AUTONOMOUS'];\n\nexport function AgentsListPage() {\n const { navigateTo } = useSubscriberBot();\n const [kind, setKind] = useState<'' | SubAgentKind>('');\n const [status, setStatus] = useState<'' | SubAgentStatus>('');\n\n const { data, isLoading, isError, refetch } = useAgents({\n kind: kind || undefined,\n status: status || undefined,\n });\n const setAgentStatus = useSetAgentStatus();\n\n const agents = data ?? [];\n\n return (\n <PageLayout\n title=\"AI Agents\"\n description=\"Specialized agents that automate your recurring portfolio\"\n actions={\n <button\n type=\"button\"\n onClick={() => navigateTo('/agents/new')}\n className=\"inline-flex items-center gap-2 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:opacity-90\"\n >\n <Plus className=\"h-4 w-4\" /> New agent\n </button>\n }\n >\n <PagePurpose className=\"mb-4\">\n Agents propose; your policies decide what they may do autonomously. Set each agent's posture\n — Disabled, Suggest-only, or Autonomous — and open one to review its policies.\n </PagePurpose>\n\n <div className=\"flex flex-wrap items-center gap-3\">\n <select\n value={kind}\n onChange={(e) => setKind(e.target.value as '' | SubAgentKind)}\n className=\"rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-xs text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n {KIND_OPTIONS.map((o) => (\n <option key={o.value} value={o.value}>\n {o.label}\n </option>\n ))}\n </select>\n <select\n value={status}\n onChange={(e) => setStatus(e.target.value as '' | SubAgentStatus)}\n className=\"rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-xs text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n <option value=\"\">Any posture</option>\n {STATUS_OPTIONS.map((s) => (\n <option key={s} value={s}>\n {formatEnumLabel(s)}\n </option>\n ))}\n </select>\n </div>\n\n {isLoading ? (\n <LoadingView label=\"Loading agents…\" />\n ) : isError ? (\n <ErrorView message=\"Could not load agents.\" onRetry={() => void refetch()} />\n ) : agents.length === 0 ? (\n <div className=\"mt-6\">\n <EmptyState\n illustration=\"empty-data\"\n icon={<Bot className=\"h-12 w-12\" />}\n title=\"No agents yet\"\n description=\"Create your first AI agent to start automating discovery, renewals, or cost optimization.\"\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('/agents/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 New agent\n </button>\n }\n />\n </div>\n ) : (\n <div className=\"mt-6 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {agents.map((a) => (\n <div key={a.id} className=\"flex flex-col rounded-lg border border-border-subtle bg-bg-surface p-4\">\n <button\n type=\"button\"\n onClick={() => navigateTo(`/agents/${a.id}`)}\n className=\"text-left\"\n >\n <div className=\"flex items-start justify-between gap-2\">\n <span className=\"flex size-9 items-center justify-center rounded-lg bg-[var(--color-accent-soft)] text-text-primary\">\n <Bot className=\"h-4 w-4\" />\n </span>\n <StatusBadge status={a.status} />\n </div>\n <p className=\"mt-3 truncate text-sm font-semibold text-text-primary\">{a.name}</p>\n <p className=\"text-xs text-text-secondary\">{formatEnumLabel(a.kind)} agent</p>\n {a.description && (\n <p className=\"mt-2 line-clamp-2 text-xs text-text-secondary\">{a.description}</p>\n )}\n <p className=\"mt-2 text-xs text-text-secondary\">\n {a.lastRunAt ? `Last run ${formatRelativeTime(a.lastRunAt)}` : 'Never run'}\n </p>\n </button>\n <div className=\"mt-3 border-t border-border-subtle pt-3\">\n <label className=\"mb-1 block text-[10px] font-medium uppercase tracking-wide text-text-secondary\">\n Posture\n </label>\n <select\n value={a.status}\n disabled={setAgentStatus.isPending}\n onChange={(e) =>\n setAgentStatus.mutate({ id: a.id, status: e.target.value as SubAgentStatus })\n }\n className=\"w-full rounded-lg border border-border-subtle bg-bg-surface px-2 py-1.5 text-xs text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)] disabled:opacity-50\"\n >\n {STATUS_OPTIONS.map((s) => (\n <option key={s} value={s}>\n {formatEnumLabel(s)}\n </option>\n ))}\n </select>\n </div>\n </div>\n ))}\n </div>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AgentsListPage.js","names":[],"sources":["../../../../src/subscriberbot/pages/agents/AgentsListPage.tsx"],"sourcesContent":["/**\n * AI Agents list\n * @module subscriberbot/pages/agents\n *\n * The AI Agents Framework — specialized agents (Discovery, Renewal, Cost,\n * Newsletter, Vendor, Compliance, Negotiation, Migration, Concierge) that each\n * own a slice of the recurring-relationship problem. Filter by kind / posture.\n * Reads `subListAgents`; sets posture inline via `subSetAgentStatus`.\n */\n\nimport { useState } from 'react';\nimport { Plus, Bot } from 'lucide-react';\nimport { PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { useSubscriberBot } from '../../context/SubscriberBotContext';\nimport { useAgents, useSetAgentStatus } from '../../hooks/useSubscriberBotApi';\nimport { formatEnumLabel, formatRelativeTime } from '../../utils/formatters';\nimport type { SubAgentKind, SubAgentStatus } from '../../types';\n\nconst KIND_OPTIONS: Array<{ value: '' | SubAgentKind; label: string }> = [\n { value: '', label: 'All kinds' },\n { value: 'DISCOVERY', label: 'Discovery' },\n { value: 'RENEWAL', label: 'Renewal' },\n { value: 'COST', label: 'Cost' },\n { value: 'NEWSLETTER', label: 'Newsletter' },\n { value: 'VENDOR', label: 'Vendor' },\n { value: 'COMPLIANCE', label: 'Compliance' },\n { value: 'NEGOTIATION', label: 'Negotiation' },\n { value: 'MIGRATION', label: 'Migration' },\n { value: 'CONCIERGE', label: 'Concierge' },\n];\n\nconst STATUS_OPTIONS: SubAgentStatus[] = ['DISABLED', 'SUGGEST_ONLY', 'AUTONOMOUS'];\n\nexport function AgentsListPage() {\n const { navigateTo } = useSubscriberBot();\n const [kind, setKind] = useState<'' | SubAgentKind>('');\n const [status, setStatus] = useState<'' | SubAgentStatus>('');\n\n const { data, isLoading, isError, refetch } = useAgents({\n kind: kind || undefined,\n status: status || undefined,\n });\n const setAgentStatus = useSetAgentStatus();\n\n const agents = data ?? [];\n\n return (\n <PageLayout\n title=\"AI Agents\"\n description=\"Specialized agents that automate your recurring portfolio\"\n actions={\n <button\n type=\"button\"\n onClick={() => navigateTo('/agents/new')}\n className=\"inline-flex items-center gap-2 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:opacity-90\"\n >\n <Plus className=\"h-4 w-4\" /> New agent\n </button>\n }\n >\n <PagePurpose className=\"mb-4\">\n Agents propose; your policies decide what they may do autonomously. Set each agent's posture\n — Disabled, Suggest-only, or Autonomous — and open one to review its policies.\n </PagePurpose>\n\n <div className=\"flex flex-wrap items-center gap-3\">\n <select\n value={kind}\n onChange={(e) => setKind(e.target.value as '' | SubAgentKind)}\n className=\"rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-xs text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n {KIND_OPTIONS.map((o) => (\n <option key={o.value} value={o.value}>\n {o.label}\n </option>\n ))}\n </select>\n <select\n value={status}\n onChange={(e) => setStatus(e.target.value as '' | SubAgentStatus)}\n className=\"rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-xs text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n <option value=\"\">Any posture</option>\n {STATUS_OPTIONS.map((s) => (\n <option key={s} value={s}>\n {formatEnumLabel(s)}\n </option>\n ))}\n </select>\n </div>\n\n {isLoading ? (\n <LoadingView label=\"Loading agents…\" />\n ) : isError ? (\n <ErrorView message=\"Could not load agents.\" onRetry={() => void refetch()} />\n ) : agents.length === 0 ? (\n <div className=\"mt-6\">\n <EmptyState\n illustration=\"empty-data\"\n icon={<Bot className=\"h-12 w-12\" />}\n title=\"No agents yet\"\n description=\"Create your first AI agent to start automating discovery, renewals, or cost optimization.\"\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('/agents/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 New agent\n </button>\n }\n />\n </div>\n ) : (\n <div className=\"mt-6 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {agents.map((a) => (\n <div\n key={a.id}\n className=\"flex flex-col rounded-lg border border-border-subtle bg-bg-surface p-4\"\n >\n <button\n type=\"button\"\n onClick={() => navigateTo(`/agents/${a.id}`)}\n className=\"text-left\"\n >\n <div className=\"flex items-start justify-between gap-2\">\n <span className=\"flex size-9 items-center justify-center rounded-lg bg-[var(--color-accent-soft)] text-text-primary\">\n <Bot className=\"h-4 w-4\" />\n </span>\n <StatusBadge status={a.status} />\n </div>\n <p className=\"mt-3 truncate text-sm font-semibold text-text-primary\">{a.name}</p>\n <p className=\"text-xs text-text-secondary\">{formatEnumLabel(a.kind)} agent</p>\n {a.description && (\n <p className=\"mt-2 line-clamp-2 text-xs text-text-secondary\">{a.description}</p>\n )}\n <p className=\"mt-2 text-xs text-text-secondary\">\n {a.lastRunAt ? `Last run ${formatRelativeTime(a.lastRunAt)}` : 'Never run'}\n </p>\n </button>\n <div className=\"mt-3 border-t border-border-subtle pt-3\">\n <label className=\"mb-1 block text-[10px] font-medium uppercase tracking-wide text-text-secondary\">\n Posture\n </label>\n <select\n value={a.status}\n disabled={setAgentStatus.isPending}\n onChange={(e) =>\n setAgentStatus.mutate({ id: a.id, status: e.target.value as SubAgentStatus })\n }\n className=\"w-full rounded-lg border border-border-subtle bg-bg-surface px-2 py-1.5 text-xs text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)] disabled:opacity-50\"\n >\n {STATUS_OPTIONS.map((s) => (\n <option key={s} value={s}>\n {formatEnumLabel(s)}\n </option>\n ))}\n </select>\n </div>\n </div>\n ))}\n </div>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ProviderDetailPage.js","names":[],"sources":["../../../../src/subscriberbot/pages/providers/ProviderDetailPage.tsx"],"sourcesContent":["/**\n * Provider detail\n * @module subscriberbot/pages/providers\n *\n * A provider profile from the registry: category, status, region, scores,\n * tags and its published plans. Reads `subProvider(id)`.\n */\n\nimport { ArrowLeft, Building2, Globe, ShieldCheck, TrendingDown, Tag } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingView, ErrorView, Section } from '../../components/StateViews';\nimport { useParams } from 'react-router-dom';\nimport { useSubscriberBot } from '../../context/SubscriberBotContext';\nimport { useProvider } from '../../hooks/useSubscriberBotApi';\nimport {\n formatCurrency,\n formatEnumLabel,\n formatIntervalSuffix,\n} from '../../utils/formatters';\n\nexport function ProviderDetailPage() {\n const { id } = useParams<{ id: string }>();\n const { navigateTo } = useSubscriberBot();\n const { data: provider, isLoading, isError, refetch } = useProvider(id);\n\n if (isLoading) {\n return (\n <PageLayout title=\"Provider\">\n <LoadingView label=\"Loading provider…\" />\n </PageLayout>\n );\n }\n\n if (isError) {\n return (\n <PageLayout title=\"Provider\">\n <ErrorView message=\"Could not load this provider.\" onRetry={() => void refetch()} />\n </PageLayout>\n );\n }\n\n if (!provider) {\n return (\n <PageLayout title=\"Provider\">\n <EmptyState\n illustration=\"empty-data\"\n title=\"Provider not found\"\n description=\"It may have been removed, or you may not have access to it.\"\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('/providers')}\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 Back to providers\n </button>\n }\n />\n </PageLayout>\n );\n }\n\n const p = provider;\n\n return (\n <PageLayout\n title={p.name}\n description={formatEnumLabel(p.category)}\n actions={\n <button\n type=\"button\"\n onClick={() => navigateTo('/providers')}\n className=\"inline-flex items-center gap-2 rounded-lg border border-border-subtle px-3 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken\"\n >\n <ArrowLeft className=\"h-4 w-4\" /> Back\n </button>\n }\n >\n <div className=\"flex flex-wrap items-center gap-3\">\n <span className=\"flex size-10 items-center justify-center rounded-lg bg-[var(--color-accent-soft)] text-text-primary\">\n <Building2 className=\"h-5 w-5\" />\n </span>\n <StatusBadge status={p.status} />\n {p.region && <span className=\"text-sm text-text-secondary\">{p.region}</span>}\n {p.websiteUrl && (\n <a\n href={p.websiteUrl}\n target=\"_blank\"\n rel=\"noreferrer\"\n className=\"inline-flex items-center gap-1 text-sm text-text-link hover:underline\"\n >\n <Globe className=\"h-3.5 w-3.5\" /> Website\n </a>\n )}\n </div>\n\n {p.description && (\n <p className=\"mt-4 max-w-3xl text-sm text-text-secondary\">{p.description}</p>\n )}\n\n <div className=\"mt-6 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <GlassCard treatment=\"metric\" className=\"p-4\">\n <div className=\"flex items-center gap-2 text-xs font-medium text-text-secondary\">\n <ShieldCheck className=\"h-3.5 w-3.5\" /> Vendor health\n </div>\n <p className=\"mt-1.5 text-lg font-semibold tabular-nums text-text-primary\">\n {p.vendorHealthScore != null ? `${p.vendorHealthScore}/100` : '—'}\n </p>\n </GlassCard>\n <GlassCard treatment=\"metric\" className=\"p-4\">\n <div className=\"flex items-center gap-2 text-xs font-medium text-text-secondary\">\n <TrendingDown className=\"h-3.5 w-3.5\" /> Churn risk\n </div>\n <p className=\"mt-1.5 text-lg font-semibold tabular-nums text-text-primary\">\n {p.churnRiskScore != null ? `${p.churnRiskScore}/100` : '—'}\n </p>\n </GlassCard>\n <GlassCard treatment=\"metric\" className=\"p-4\">\n <div className=\"flex items-center gap-2 text-xs font-medium text-text-secondary\">\n Industry\n </div>\n <p className=\"mt-1.5 text-sm text-text-primary\">{p.industry ?? '—'}</p>\n </GlassCard>\n <GlassCard treatment=\"metric\" className=\"p-4\">\n <div className=\"flex items-center gap-2 text-xs font-medium text-text-secondary\">\n Language\n </div>\n <p className=\"mt-1.5 text-sm text-text-primary\">{p.language ?? '—'}</p>\n </GlassCard>\n </div>\n\n {p.tags.length > 0 && (\n <div className=\"mt-5 flex flex-wrap items-center gap-2\">\n <Tag className=\"h-3.5 w-3.5 text-text-secondary\" />\n {p.tags.map((tag) => (\n <span\n key={tag}\n className=\"rounded-full bg-bg-sunken px-2.5 py-0.5 text-xs text-text-secondary\"\n >\n {tag}\n </span>\n ))}\n </div>\n )}\n\n <Section title=\"Plans\" description=\"Pricing this provider publishes\">\n {!p.plans || p.plans.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n title=\"No published plans\"\n description=\"When this provider publishes plans, they'll appear here.\"\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {p.plans.map((plan) => (\n <div key={plan.id} className=\"rounded-lg border border-border-subtle bg-bg-surface p-4\">\n <div className=\"flex items-start justify-between gap-2\">\n <p className=\"text-sm font-semibold text-text-primary\">{plan.name}</p>\n <StatusBadge status={plan.isActive ? 'ACTIVE' : 'DISABLED'} />\n </div>\n <p className=\"mt-2 text-lg font-bold tabular-nums text-text-primary\">\n {formatCurrency(plan.amount, plan.currency)}\n <span className=\"text-xs font-normal text-text-secondary\">\n {formatIntervalSuffix(plan.interval)}\n </span>\n </p>\n {plan.description && (\n <p className=\"mt-1 text-xs text-text-secondary\">{plan.description}</p>\n )}\n {plan.features.length > 0 && (\n <ul className=\"mt-3 space-y-1\">\n {plan.features.slice(0, 5).map((f) => (\n <li key={f} className=\"text-xs text-text-secondary\">\n • {f}\n </li>\n ))}\n </ul>\n )}\n </div>\n ))}\n </div>\n )}\n </Section>\n </PageLayout>\n );\n}\n"],"mappings":";;;;;;;;;;;;AAuBA,SAAgB,IAAqB;CACnC,IAAM,EAAE,UAAO,GAA2B,EACpC,EAAE,kBAAe,GAAkB,EACnC,EAAE,MAAM,GAAU,cAAW,YAAS,eAAY,EAAY,EAAG;AAEvE,KAAI,EACF,QACE,kBAAC,GAAD;EAAY,OAAM;YAChB,kBAAC,GAAD,EAAa,OAAM,qBAAsB,CAAA;EAC9B,CAAA;AAIjB,KAAI,EACF,QACE,kBAAC,GAAD;EAAY,OAAM;YAChB,kBAAC,GAAD;GAAW,SAAQ;GAAgC,eAAe,KAAK,GAAS;GAAI,CAAA;EACzE,CAAA;AAIjB,KAAI,CAAC,EACH,QACE,kBAAC,GAAD;EAAY,OAAM;YAChB,kBAAC,GAAD;GACE,cAAa;GACb,OAAM;GACN,aAAY;GACZ,QACE,kBAAC,UAAD;IACE,MAAK;IACL,eAAe,EAAW,aAAa;IACvC,WAAU;cACX;IAEQ,CAAA;GAEX,CAAA;EACS,CAAA;CAIjB,IAAM,IAAI;AAEV,QACE,kBAAC,GAAD;EACE,OAAO,EAAE;EACT,aAAa,EAAgB,EAAE,SAAS;EACxC,SACE,kBAAC,UAAD;GACE,MAAK;GACL,eAAe,EAAW,aAAa;GACvC,WAAU;aAHZ,CAKE,kBAAC,GAAD,EAAW,WAAU,WAAY,CAAA,EAAA,QAC1B;;YAVb;GAaE,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,QAAD;MAAM,WAAU;gBACd,kBAAC,GAAD,EAAW,WAAU,WAAY,CAAA;MAC5B,CAAA;KACP,kBAAC,GAAD,EAAa,QAAQ,EAAE,QAAU,CAAA;KAChC,EAAE,UAAU,kBAAC,QAAD;MAAM,WAAU;gBAA+B,EAAE;MAAc,CAAA;KAC3E,EAAE,cACD,kBAAC,KAAD;MACE,MAAM,EAAE;MACR,QAAO;MACP,KAAI;MACJ,WAAU;gBAJZ,CAME,kBAAC,GAAD,EAAO,WAAU,eAAgB,CAAA,EAAA,WAC/B;;KAEF;;GAEL,EAAE,eACD,kBAAC,KAAD;IAAG,WAAU;cAA8C,EAAE;IAAgB,CAAA;GAG/E,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,GAAD;MAAW,WAAU;MAAS,WAAU;gBAAxC,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,GAAD,EAAa,WAAU,eAAgB,CAAA,EAAA,iBACnC;UACN,kBAAC,KAAD;OAAG,WAAU;iBACV,EAAE,qBAAqB,OAAsC,MAA/B,GAAG,EAAE,kBAAkB;OACpD,CAAA,CACM;;KACZ,kBAAC,GAAD;MAAW,WAAU;MAAS,WAAU;gBAAxC,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,GAAD,EAAc,WAAU,eAAgB,CAAA,EAAA,cACpC;UACN,kBAAC,KAAD;OAAG,WAAU;iBACV,EAAE,kBAAkB,OAAmC,MAA5B,GAAG,EAAE,eAAe;OAC9C,CAAA,CACM;;KACZ,kBAAC,GAAD;MAAW,WAAU;MAAS,WAAU;gBAAxC,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAkE;OAE3E,CAAA,EACN,kBAAC,KAAD;OAAG,WAAU;iBAAoC,EAAE,YAAY;OAAQ,CAAA,CAC7D;;KACZ,kBAAC,GAAD;MAAW,WAAU;MAAS,WAAU;gBAAxC,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAkE;OAE3E,CAAA,EACN,kBAAC,KAAD;OAAG,WAAU;iBAAoC,EAAE,YAAY;OAAQ,CAAA,CAC7D;;KACR;;GAEL,EAAE,KAAK,SAAS,KACf,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,GAAD,EAAK,WAAU,mCAAoC,CAAA,EAClD,EAAE,KAAK,KAAK,MACX,kBAAC,QAAD;KAEE,WAAU;eAET;KACI,EAJA,EAIA,CACP,CACE;;GAGR,kBAAC,GAAD;IAAS,OAAM;IAAQ,aAAY;cAChC,CAAC,EAAE,SAAS,EAAE,MAAM,WAAW,IAC9B,kBAAC,GAAD;KACE,cAAa;KACb,OAAM;KACN,aAAY;KACZ,CAAA,GAEF,kBAAC,OAAD;KAAK,WAAU;eACZ,EAAE,MAAM,KAAK,MACZ,kBAAC,OAAD;MAAmB,WAAU;gBAA7B;OACE,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,KAAD;SAAG,WAAU;mBAA2C,EAAK;SAAS,CAAA,EACtE,kBAAC,GAAD,EAAa,QAAQ,EAAK,WAAW,WAAW,YAAc,CAAA,CAC1D;;OACN,kBAAC,KAAD;QAAG,WAAU;kBAAb,CACG,EAAe,EAAK,QAAQ,EAAK,SAAS,EAC3C,kBAAC,QAAD;SAAM,WAAU;mBACb,EAAqB,EAAK,SAAS;SAC/B,CAAA,CACL;;OACH,EAAK,eACJ,kBAAC,KAAD;QAAG,WAAU;kBAAoC,EAAK;QAAgB,CAAA;OAEvE,EAAK,SAAS,SAAS,KACtB,kBAAC,MAAD;QAAI,WAAU;kBACX,EAAK,SAAS,MAAM,GAAG,EAAE,CAAC,KAAK,MAC9B,kBAAC,MAAD;SAAY,WAAU;mBAAtB,CAAoD,MAC/C,EACA;WAFI,EAEJ,CACL;QACC,CAAA;OAEH;QAvBI,EAAK,GAuBT,CACN;KACE,CAAA;IAEA,CAAA;GACC"}
|
|
1
|
+
{"version":3,"file":"ProviderDetailPage.js","names":[],"sources":["../../../../src/subscriberbot/pages/providers/ProviderDetailPage.tsx"],"sourcesContent":["/**\n * Provider detail\n * @module subscriberbot/pages/providers\n *\n * A provider profile from the registry: category, status, region, scores,\n * tags and its published plans. Reads `subProvider(id)`.\n */\n\nimport { ArrowLeft, Building2, Globe, ShieldCheck, TrendingDown, Tag } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingView, ErrorView, Section } from '../../components/StateViews';\nimport { useParams } from 'react-router-dom';\nimport { useSubscriberBot } from '../../context/SubscriberBotContext';\nimport { useProvider } from '../../hooks/useSubscriberBotApi';\nimport { formatCurrency, formatEnumLabel, formatIntervalSuffix } from '../../utils/formatters';\n\nexport function ProviderDetailPage() {\n const { id } = useParams<{ id: string }>();\n const { navigateTo } = useSubscriberBot();\n const { data: provider, isLoading, isError, refetch } = useProvider(id);\n\n if (isLoading) {\n return (\n <PageLayout title=\"Provider\">\n <LoadingView label=\"Loading provider…\" />\n </PageLayout>\n );\n }\n\n if (isError) {\n return (\n <PageLayout title=\"Provider\">\n <ErrorView message=\"Could not load this provider.\" onRetry={() => void refetch()} />\n </PageLayout>\n );\n }\n\n if (!provider) {\n return (\n <PageLayout title=\"Provider\">\n <EmptyState\n illustration=\"empty-data\"\n title=\"Provider not found\"\n description=\"It may have been removed, or you may not have access to it.\"\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('/providers')}\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 Back to providers\n </button>\n }\n />\n </PageLayout>\n );\n }\n\n const p = provider;\n\n return (\n <PageLayout\n title={p.name}\n description={formatEnumLabel(p.category)}\n actions={\n <button\n type=\"button\"\n onClick={() => navigateTo('/providers')}\n className=\"inline-flex items-center gap-2 rounded-lg border border-border-subtle px-3 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken\"\n >\n <ArrowLeft className=\"h-4 w-4\" /> Back\n </button>\n }\n >\n <div className=\"flex flex-wrap items-center gap-3\">\n <span className=\"flex size-10 items-center justify-center rounded-lg bg-[var(--color-accent-soft)] text-text-primary\">\n <Building2 className=\"h-5 w-5\" />\n </span>\n <StatusBadge status={p.status} />\n {p.region && <span className=\"text-sm text-text-secondary\">{p.region}</span>}\n {p.websiteUrl && (\n <a\n href={p.websiteUrl}\n target=\"_blank\"\n rel=\"noreferrer\"\n className=\"inline-flex items-center gap-1 text-sm text-text-link hover:underline\"\n >\n <Globe className=\"h-3.5 w-3.5\" /> Website\n </a>\n )}\n </div>\n\n {p.description && (\n <p className=\"mt-4 max-w-3xl text-sm text-text-secondary\">{p.description}</p>\n )}\n\n <div className=\"mt-6 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <GlassCard treatment=\"metric\" className=\"p-4\">\n <div className=\"flex items-center gap-2 text-xs font-medium text-text-secondary\">\n <ShieldCheck className=\"h-3.5 w-3.5\" /> Vendor health\n </div>\n <p className=\"mt-1.5 text-lg font-semibold tabular-nums text-text-primary\">\n {p.vendorHealthScore != null ? `${p.vendorHealthScore}/100` : '—'}\n </p>\n </GlassCard>\n <GlassCard treatment=\"metric\" className=\"p-4\">\n <div className=\"flex items-center gap-2 text-xs font-medium text-text-secondary\">\n <TrendingDown className=\"h-3.5 w-3.5\" /> Churn risk\n </div>\n <p className=\"mt-1.5 text-lg font-semibold tabular-nums text-text-primary\">\n {p.churnRiskScore != null ? `${p.churnRiskScore}/100` : '—'}\n </p>\n </GlassCard>\n <GlassCard treatment=\"metric\" className=\"p-4\">\n <div className=\"flex items-center gap-2 text-xs font-medium text-text-secondary\">\n Industry\n </div>\n <p className=\"mt-1.5 text-sm text-text-primary\">{p.industry ?? '—'}</p>\n </GlassCard>\n <GlassCard treatment=\"metric\" className=\"p-4\">\n <div className=\"flex items-center gap-2 text-xs font-medium text-text-secondary\">\n Language\n </div>\n <p className=\"mt-1.5 text-sm text-text-primary\">{p.language ?? '—'}</p>\n </GlassCard>\n </div>\n\n {p.tags.length > 0 && (\n <div className=\"mt-5 flex flex-wrap items-center gap-2\">\n <Tag className=\"h-3.5 w-3.5 text-text-secondary\" />\n {p.tags.map((tag) => (\n <span\n key={tag}\n className=\"rounded-full bg-bg-sunken px-2.5 py-0.5 text-xs text-text-secondary\"\n >\n {tag}\n </span>\n ))}\n </div>\n )}\n\n <Section title=\"Plans\" description=\"Pricing this provider publishes\">\n {!p.plans || p.plans.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n title=\"No published plans\"\n description=\"When this provider publishes plans, they'll appear here.\"\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {p.plans.map((plan) => (\n <div\n key={plan.id}\n className=\"rounded-lg border border-border-subtle bg-bg-surface p-4\"\n >\n <div className=\"flex items-start justify-between gap-2\">\n <p className=\"text-sm font-semibold text-text-primary\">{plan.name}</p>\n <StatusBadge status={plan.isActive ? 'ACTIVE' : 'DISABLED'} />\n </div>\n <p className=\"mt-2 text-lg font-bold tabular-nums text-text-primary\">\n {formatCurrency(plan.amount, plan.currency)}\n <span className=\"text-xs font-normal text-text-secondary\">\n {formatIntervalSuffix(plan.interval)}\n </span>\n </p>\n {plan.description && (\n <p className=\"mt-1 text-xs text-text-secondary\">{plan.description}</p>\n )}\n {plan.features.length > 0 && (\n <ul className=\"mt-3 space-y-1\">\n {plan.features.slice(0, 5).map((f) => (\n <li key={f} className=\"text-xs text-text-secondary\">\n • {f}\n </li>\n ))}\n </ul>\n )}\n </div>\n ))}\n </div>\n )}\n </Section>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RelationshipDetailPage.js","names":[],"sources":["../../../../src/subscriberbot/pages/relationships/RelationshipDetailPage.tsx"],"sourcesContent":["/**\n * Relationship detail\n * @module subscriberbot/pages/relationships\n *\n * Full per-relationship view: plan, price, billing period, next renewal,\n * health score, entitlements, attached assets and renewal events — plus\n * lifecycle transitions (validated server-side) and cancel.\n * Reads `subRelationship(id)`; writes `subTransitionRelationship` /\n * `subCancelRelationship`.\n */\n\nimport { useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport {\n ArrowLeft,\n Building2,\n CreditCard,\n ShieldCheck,\n CalendarClock,\n Paperclip,\n Activity,\n Tag,\n} from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingView, ErrorView, Section } from '../../components/StateViews';\nimport { useSubscriberBot } from '../../context/SubscriberBotContext';\nimport {\n useRelationship,\n useTransitionRelationship,\n useCancelRelationship,\n} from '../../hooks/useSubscriberBotApi';\nimport {\n formatCurrency,\n formatDate,\n formatEnumLabel,\n formatIntervalSuffix,\n} from '../../utils/formatters';\nimport type { SubRelationshipStatus } from '../../types';\n\n// Lifecycle actions surfaced as buttons; the server validates the transition.\nconst TRANSITIONS: Array<{ status: SubRelationshipStatus; label: string }> = [\n { status: 'ACTIVE', label: 'Activate' },\n { status: 'PAUSED', label: 'Pause' },\n { status: 'EXPIRED', label: 'Mark expired' },\n];\n\nexport function RelationshipDetailPage() {\n const { id } = useParams<{ id: string }>();\n const { navigateTo } = useSubscriberBot();\n const { data: relationship, isLoading, isError, refetch } = useRelationship(id);\n const transition = useTransitionRelationship();\n const cancel = useCancelRelationship();\n const [actionError, setActionError] = useState<string | null>(null);\n\n if (isLoading) {\n return (\n <PageLayout title=\"Relationship\">\n <LoadingView label=\"Loading relationship…\" />\n </PageLayout>\n );\n }\n\n if (isError) {\n return (\n <PageLayout title=\"Relationship\">\n <ErrorView message=\"Could not load this relationship.\" onRetry={() => void refetch()} />\n </PageLayout>\n );\n }\n\n if (!relationship) {\n return (\n <PageLayout title=\"Relationship\">\n <EmptyState\n illustration=\"empty-data\"\n title=\"Relationship not found\"\n description=\"It may have been removed, or you may not have access to it.\"\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('/relationships')}\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 Back to relationships\n </button>\n }\n />\n </PageLayout>\n );\n }\n\n const r = relationship;\n const isBusy = transition.isPending || cancel.isPending;\n\n const runTransition = (status: SubRelationshipStatus) => {\n setActionError(null);\n transition.mutate(\n { id: r.id, status },\n { onError: (e) => setActionError(e instanceof Error ? e.message : 'Transition failed') }\n );\n };\n\n const runCancel = () => {\n setActionError(null);\n cancel.mutate(r.id, {\n onError: (e) => setActionError(e instanceof Error ? e.message : 'Cancel failed'),\n });\n };\n\n return (\n <PageLayout\n title={r.label}\n description={r.provider?.name ?? formatEnumLabel(r.kind)}\n actions={\n <button\n type=\"button\"\n onClick={() => navigateTo('/relationships')}\n className=\"inline-flex items-center gap-2 rounded-lg border border-border-subtle px-3 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken\"\n >\n <ArrowLeft className=\"h-4 w-4\" /> Back\n </button>\n }\n >\n {/* Header summary */}\n <div className=\"flex flex-wrap items-center gap-3\">\n <StatusBadge status={r.status} />\n <span className=\"text-sm text-text-secondary\">{formatEnumLabel(r.kind)}</span>\n <span className=\"text-sm text-text-secondary\">·</span>\n <span className=\"text-sm font-semibold text-text-primary\">\n {formatCurrency(r.amount, r.currency)}\n <span className=\"text-xs font-normal text-text-secondary\">\n {formatIntervalSuffix(r.interval)}\n </span>\n </span>\n {r.autoRenew && <span className=\"text-xs text-text-secondary\">Auto-renews</span>}\n </div>\n\n {actionError && (\n <div className=\"mt-4 rounded-lg border border-status-error-text/30 bg-status-error-bg-subtle px-4 py-2 text-sm text-status-error-text\">\n {actionError}\n </div>\n )}\n\n {/* Lifecycle actions */}\n <div className=\"mt-5 flex flex-wrap gap-2\">\n {TRANSITIONS.filter((t) => t.status !== r.status).map((t) => (\n <button\n key={t.status}\n type=\"button\"\n disabled={isBusy}\n onClick={() => runTransition(t.status)}\n className=\"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 {t.label}\n </button>\n ))}\n {r.status !== 'CANCELLED' && (\n <button\n type=\"button\"\n disabled={isBusy}\n onClick={runCancel}\n className=\"rounded-lg border border-status-error-text/30 bg-status-error-bg-subtle px-3 py-1.5 text-xs font-medium text-status-error-text hover:opacity-90 disabled:opacity-50\"\n >\n Cancel relationship\n </button>\n )}\n </div>\n\n {/* Key facts grid */}\n <div className=\"mt-6 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n <GlassCard treatment=\"metric\" className=\"p-4\">\n <div className=\"flex items-center gap-2 text-xs font-medium text-text-secondary\">\n <Building2 className=\"h-3.5 w-3.5\" /> Provider\n </div>\n <p className=\"mt-1.5 text-sm text-text-primary\">{r.provider?.name ?? '—'}</p>\n </GlassCard>\n <GlassCard treatment=\"metric\" className=\"p-4\">\n <div className=\"flex items-center gap-2 text-xs font-medium text-text-secondary\">\n <CreditCard className=\"h-3.5 w-3.5\" /> Plan\n </div>\n <p className=\"mt-1.5 text-sm text-text-primary\">{r.plan?.name ?? '—'}</p>\n </GlassCard>\n <GlassCard treatment=\"metric\" className=\"p-4\">\n <div className=\"flex items-center gap-2 text-xs font-medium text-text-secondary\">\n <CalendarClock className=\"h-3.5 w-3.5\" /> Next renewal\n </div>\n <p className=\"mt-1.5 text-sm text-text-primary\">\n {r.renewsAt ? formatDate(r.renewsAt) : '—'}\n </p>\n </GlassCard>\n <GlassCard treatment=\"metric\" className=\"p-4\">\n <div className=\"flex items-center gap-2 text-xs font-medium text-text-secondary\">\n <Activity className=\"h-3.5 w-3.5\" /> Health score\n </div>\n <p className=\"mt-1.5 text-sm text-text-primary\">\n {r.healthScore != null ? `${r.healthScore}/100` : '—'}\n </p>\n </GlassCard>\n <GlassCard treatment=\"metric\" className=\"p-4\">\n <div className=\"flex items-center gap-2 text-xs font-medium text-text-secondary\">\n <ShieldCheck className=\"h-3.5 w-3.5\" /> Discovery\n </div>\n <p className=\"mt-1.5 text-sm text-text-primary\">\n {formatEnumLabel(r.discoverySource)}\n </p>\n </GlassCard>\n <GlassCard treatment=\"metric\" className=\"p-4\">\n <div className=\"flex items-center gap-2 text-xs font-medium text-text-secondary\">\n <CalendarClock className=\"h-3.5 w-3.5\" /> Started\n </div>\n <p className=\"mt-1.5 text-sm text-text-primary\">\n {r.startedAt ? formatDate(r.startedAt) : '—'}\n </p>\n </GlassCard>\n </div>\n\n {r.tags.length > 0 && (\n <div className=\"mt-5 flex flex-wrap items-center gap-2\">\n <Tag className=\"h-3.5 w-3.5 text-text-secondary\" />\n {r.tags.map((tag) => (\n <span\n key={tag}\n className=\"rounded-full bg-bg-sunken px-2.5 py-0.5 text-xs text-text-secondary\"\n >\n {tag}\n </span>\n ))}\n </div>\n )}\n\n {r.notes && (\n <div className=\"mt-5 rounded-lg border border-border-subtle bg-bg-surface p-4\">\n <p className=\"text-xs font-medium text-text-secondary\">Notes</p>\n <p className=\"mt-1 whitespace-pre-wrap text-sm text-text-primary\">{r.notes}</p>\n </div>\n )}\n\n {/* Entitlements */}\n <Section title=\"Entitlements\" description=\"What this relationship grants you\">\n {!r.entitlements || r.entitlements.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n title=\"No entitlements\"\n description=\"Entitlements (seats, quotas, feature access) will appear here.\"\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3\">\n {r.entitlements.map((e) => (\n <div key={e.id} className=\"rounded-lg border border-border-subtle bg-bg-surface p-4\">\n <div className=\"flex items-start justify-between gap-2\">\n <p className=\"text-sm font-medium text-text-primary\">{e.name}</p>\n <StatusBadge status={e.isActive ? 'ACTIVE' : 'DISABLED'} />\n </div>\n <p className=\"mt-1 text-xs text-text-secondary\">{formatEnumLabel(e.type)}</p>\n {e.quota != null && (\n <p className=\"mt-2 text-xs text-text-secondary\">\n Used {e.used} of {e.quota}\n {e.unit ? ` ${e.unit}` : ''}\n </p>\n )}\n </div>\n ))}\n </div>\n )}\n </Section>\n\n {/* Assets */}\n <Section title=\"Assets\" description=\"Invoices, contracts, licenses and credentials\">\n {!r.assets || r.assets.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n title=\"No assets attached\"\n description=\"Attach invoices, contracts, licenses or receipts to this relationship.\"\n />\n ) : (\n <div className=\"divide-y divide-border-subtle rounded-lg border border-border-subtle bg-bg-surface\">\n {r.assets.map((a) => (\n <div key={a.id} className=\"flex items-center justify-between gap-3 p-3\">\n <div className=\"flex min-w-0 items-center gap-2\">\n <Paperclip className=\"h-4 w-4 shrink-0 text-text-secondary\" />\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-medium text-text-primary\">{a.name}</p>\n <p className=\"text-xs text-text-secondary\">{formatEnumLabel(a.type)}</p>\n </div>\n </div>\n {a.expiresAt && (\n <span className=\"shrink-0 text-xs text-text-secondary\">\n Expires {formatDate(a.expiresAt)}\n </span>\n )}\n </div>\n ))}\n </div>\n )}\n </Section>\n\n {/* Renewal events */}\n <Section title=\"Renewal timeline\" description=\"Detected renewals and price changes\">\n {!r.renewalEvents || r.renewalEvents.length === 0 ? (\n <EmptyState\n illustration=\"empty-calendar\"\n title=\"No renewal events\"\n description=\"Renewals and price changes detected for this relationship will appear here.\"\n />\n ) : (\n <div className=\"divide-y divide-border-subtle rounded-lg border border-border-subtle bg-bg-surface\">\n {r.renewalEvents.map((event) => (\n <div key={event.id} className=\"flex items-center justify-between gap-3 p-3\">\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {formatEnumLabel(event.eventType)}\n </p>\n <p className=\"text-xs text-text-secondary\">\n {formatDate(event.occursAt)}\n {event.detail ? ` · ${event.detail}` : ''}\n </p>\n </div>\n {event.amount != null && (\n <span className=\"shrink-0 text-sm font-semibold tabular-nums text-text-primary\">\n {formatCurrency(event.amount, event.currency ?? r.currency)}\n </span>\n )}\n </div>\n ))}\n </div>\n )}\n </Section>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"RelationshipDetailPage.js","names":[],"sources":["../../../../src/subscriberbot/pages/relationships/RelationshipDetailPage.tsx"],"sourcesContent":["/**\n * Relationship detail\n * @module subscriberbot/pages/relationships\n *\n * Full per-relationship view: plan, price, billing period, next renewal,\n * health score, entitlements, attached assets and renewal events — plus\n * lifecycle transitions (validated server-side) and cancel.\n * Reads `subRelationship(id)`; writes `subTransitionRelationship` /\n * `subCancelRelationship`.\n */\n\nimport { useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport {\n ArrowLeft,\n Building2,\n CreditCard,\n ShieldCheck,\n CalendarClock,\n Paperclip,\n Activity,\n Tag,\n} from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingView, ErrorView, Section } from '../../components/StateViews';\nimport { useSubscriberBot } from '../../context/SubscriberBotContext';\nimport {\n useRelationship,\n useTransitionRelationship,\n useCancelRelationship,\n} from '../../hooks/useSubscriberBotApi';\nimport {\n formatCurrency,\n formatDate,\n formatEnumLabel,\n formatIntervalSuffix,\n} from '../../utils/formatters';\nimport type { SubRelationshipStatus } from '../../types';\n\n// Lifecycle actions surfaced as buttons; the server validates the transition.\nconst TRANSITIONS: Array<{ status: SubRelationshipStatus; label: string }> = [\n { status: 'ACTIVE', label: 'Activate' },\n { status: 'PAUSED', label: 'Pause' },\n { status: 'EXPIRED', label: 'Mark expired' },\n];\n\nexport function RelationshipDetailPage() {\n const { id } = useParams<{ id: string }>();\n const { navigateTo } = useSubscriberBot();\n const { data: relationship, isLoading, isError, refetch } = useRelationship(id);\n const transition = useTransitionRelationship();\n const cancel = useCancelRelationship();\n const [actionError, setActionError] = useState<string | null>(null);\n\n if (isLoading) {\n return (\n <PageLayout title=\"Relationship\">\n <LoadingView label=\"Loading relationship…\" />\n </PageLayout>\n );\n }\n\n if (isError) {\n return (\n <PageLayout title=\"Relationship\">\n <ErrorView message=\"Could not load this relationship.\" onRetry={() => void refetch()} />\n </PageLayout>\n );\n }\n\n if (!relationship) {\n return (\n <PageLayout title=\"Relationship\">\n <EmptyState\n illustration=\"empty-data\"\n title=\"Relationship not found\"\n description=\"It may have been removed, or you may not have access to it.\"\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('/relationships')}\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 Back to relationships\n </button>\n }\n />\n </PageLayout>\n );\n }\n\n const r = relationship;\n const isBusy = transition.isPending || cancel.isPending;\n\n const runTransition = (status: SubRelationshipStatus) => {\n setActionError(null);\n transition.mutate(\n { id: r.id, status },\n { onError: (e) => setActionError(e instanceof Error ? e.message : 'Transition failed') }\n );\n };\n\n const runCancel = () => {\n setActionError(null);\n cancel.mutate(r.id, {\n onError: (e) => setActionError(e instanceof Error ? e.message : 'Cancel failed'),\n });\n };\n\n return (\n <PageLayout\n title={r.label}\n description={r.provider?.name ?? formatEnumLabel(r.kind)}\n actions={\n <button\n type=\"button\"\n onClick={() => navigateTo('/relationships')}\n className=\"inline-flex items-center gap-2 rounded-lg border border-border-subtle px-3 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken\"\n >\n <ArrowLeft className=\"h-4 w-4\" /> Back\n </button>\n }\n >\n {/* Header summary */}\n <div className=\"flex flex-wrap items-center gap-3\">\n <StatusBadge status={r.status} />\n <span className=\"text-sm text-text-secondary\">{formatEnumLabel(r.kind)}</span>\n <span className=\"text-sm text-text-secondary\">·</span>\n <span className=\"text-sm font-semibold text-text-primary\">\n {formatCurrency(r.amount, r.currency)}\n <span className=\"text-xs font-normal text-text-secondary\">\n {formatIntervalSuffix(r.interval)}\n </span>\n </span>\n {r.autoRenew && <span className=\"text-xs text-text-secondary\">Auto-renews</span>}\n </div>\n\n {actionError && (\n <div className=\"mt-4 rounded-lg border border-status-error-text/30 bg-status-error-bg-subtle px-4 py-2 text-sm text-status-error-text\">\n {actionError}\n </div>\n )}\n\n {/* Lifecycle actions */}\n <div className=\"mt-5 flex flex-wrap gap-2\">\n {TRANSITIONS.filter((t) => t.status !== r.status).map((t) => (\n <button\n key={t.status}\n type=\"button\"\n disabled={isBusy}\n onClick={() => runTransition(t.status)}\n className=\"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 {t.label}\n </button>\n ))}\n {r.status !== 'CANCELLED' && (\n <button\n type=\"button\"\n disabled={isBusy}\n onClick={runCancel}\n className=\"rounded-lg border border-status-error-text/30 bg-status-error-bg-subtle px-3 py-1.5 text-xs font-medium text-status-error-text hover:opacity-90 disabled:opacity-50\"\n >\n Cancel relationship\n </button>\n )}\n </div>\n\n {/* Key facts grid */}\n <div className=\"mt-6 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n <GlassCard treatment=\"metric\" className=\"p-4\">\n <div className=\"flex items-center gap-2 text-xs font-medium text-text-secondary\">\n <Building2 className=\"h-3.5 w-3.5\" /> Provider\n </div>\n <p className=\"mt-1.5 text-sm text-text-primary\">{r.provider?.name ?? '—'}</p>\n </GlassCard>\n <GlassCard treatment=\"metric\" className=\"p-4\">\n <div className=\"flex items-center gap-2 text-xs font-medium text-text-secondary\">\n <CreditCard className=\"h-3.5 w-3.5\" /> Plan\n </div>\n <p className=\"mt-1.5 text-sm text-text-primary\">{r.plan?.name ?? '—'}</p>\n </GlassCard>\n <GlassCard treatment=\"metric\" className=\"p-4\">\n <div className=\"flex items-center gap-2 text-xs font-medium text-text-secondary\">\n <CalendarClock className=\"h-3.5 w-3.5\" /> Next renewal\n </div>\n <p className=\"mt-1.5 text-sm text-text-primary\">\n {r.renewsAt ? formatDate(r.renewsAt) : '—'}\n </p>\n </GlassCard>\n <GlassCard treatment=\"metric\" className=\"p-4\">\n <div className=\"flex items-center gap-2 text-xs font-medium text-text-secondary\">\n <Activity className=\"h-3.5 w-3.5\" /> Health score\n </div>\n <p className=\"mt-1.5 text-sm text-text-primary\">\n {r.healthScore != null ? `${r.healthScore}/100` : '—'}\n </p>\n </GlassCard>\n <GlassCard treatment=\"metric\" className=\"p-4\">\n <div className=\"flex items-center gap-2 text-xs font-medium text-text-secondary\">\n <ShieldCheck className=\"h-3.5 w-3.5\" /> Discovery\n </div>\n <p className=\"mt-1.5 text-sm text-text-primary\">{formatEnumLabel(r.discoverySource)}</p>\n </GlassCard>\n <GlassCard treatment=\"metric\" className=\"p-4\">\n <div className=\"flex items-center gap-2 text-xs font-medium text-text-secondary\">\n <CalendarClock className=\"h-3.5 w-3.5\" /> Started\n </div>\n <p className=\"mt-1.5 text-sm text-text-primary\">\n {r.startedAt ? formatDate(r.startedAt) : '—'}\n </p>\n </GlassCard>\n </div>\n\n {r.tags.length > 0 && (\n <div className=\"mt-5 flex flex-wrap items-center gap-2\">\n <Tag className=\"h-3.5 w-3.5 text-text-secondary\" />\n {r.tags.map((tag) => (\n <span\n key={tag}\n className=\"rounded-full bg-bg-sunken px-2.5 py-0.5 text-xs text-text-secondary\"\n >\n {tag}\n </span>\n ))}\n </div>\n )}\n\n {r.notes && (\n <div className=\"mt-5 rounded-lg border border-border-subtle bg-bg-surface p-4\">\n <p className=\"text-xs font-medium text-text-secondary\">Notes</p>\n <p className=\"mt-1 whitespace-pre-wrap text-sm text-text-primary\">{r.notes}</p>\n </div>\n )}\n\n {/* Entitlements */}\n <Section title=\"Entitlements\" description=\"What this relationship grants you\">\n {!r.entitlements || r.entitlements.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n title=\"No entitlements\"\n description=\"Entitlements (seats, quotas, feature access) will appear here.\"\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3\">\n {r.entitlements.map((e) => (\n <div key={e.id} className=\"rounded-lg border border-border-subtle bg-bg-surface p-4\">\n <div className=\"flex items-start justify-between gap-2\">\n <p className=\"text-sm font-medium text-text-primary\">{e.name}</p>\n <StatusBadge status={e.isActive ? 'ACTIVE' : 'DISABLED'} />\n </div>\n <p className=\"mt-1 text-xs text-text-secondary\">{formatEnumLabel(e.type)}</p>\n {e.quota != null && (\n <p className=\"mt-2 text-xs text-text-secondary\">\n Used {e.used} of {e.quota}\n {e.unit ? ` ${e.unit}` : ''}\n </p>\n )}\n </div>\n ))}\n </div>\n )}\n </Section>\n\n {/* Assets */}\n <Section title=\"Assets\" description=\"Invoices, contracts, licenses and credentials\">\n {!r.assets || r.assets.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n title=\"No assets attached\"\n description=\"Attach invoices, contracts, licenses or receipts to this relationship.\"\n />\n ) : (\n <div className=\"divide-y divide-border-subtle rounded-lg border border-border-subtle bg-bg-surface\">\n {r.assets.map((a) => (\n <div key={a.id} className=\"flex items-center justify-between gap-3 p-3\">\n <div className=\"flex min-w-0 items-center gap-2\">\n <Paperclip className=\"h-4 w-4 shrink-0 text-text-secondary\" />\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-medium text-text-primary\">{a.name}</p>\n <p className=\"text-xs text-text-secondary\">{formatEnumLabel(a.type)}</p>\n </div>\n </div>\n {a.expiresAt && (\n <span className=\"shrink-0 text-xs text-text-secondary\">\n Expires {formatDate(a.expiresAt)}\n </span>\n )}\n </div>\n ))}\n </div>\n )}\n </Section>\n\n {/* Renewal events */}\n <Section title=\"Renewal timeline\" description=\"Detected renewals and price changes\">\n {!r.renewalEvents || r.renewalEvents.length === 0 ? (\n <EmptyState\n illustration=\"empty-calendar\"\n title=\"No renewal events\"\n description=\"Renewals and price changes detected for this relationship will appear here.\"\n />\n ) : (\n <div className=\"divide-y divide-border-subtle rounded-lg border border-border-subtle bg-bg-surface\">\n {r.renewalEvents.map((event) => (\n <div key={event.id} className=\"flex items-center justify-between gap-3 p-3\">\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {formatEnumLabel(event.eventType)}\n </p>\n <p className=\"text-xs text-text-secondary\">\n {formatDate(event.occursAt)}\n {event.detail ? ` · ${event.detail}` : ''}\n </p>\n </div>\n {event.amount != null && (\n <span className=\"shrink-0 text-sm font-semibold tabular-nums text-text-primary\">\n {formatCurrency(event.amount, event.currency ?? r.currency)}\n </span>\n )}\n </div>\n ))}\n </div>\n )}\n </Section>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RelationshipNewPage.js","names":[],"sources":["../../../../src/subscriberbot/pages/relationships/RelationshipNewPage.tsx"],"sourcesContent":["/**\n * Create relationship\n * @module subscriberbot/pages/relationships\n *\n * Manual-add form for a recurring relationship AI can't auto-detect.\n * Writes `subCreateRelationship`. Provider + plan are optional pickers\n * sourced from the real registry (`subListProviders` / `subListPlans`).\n */\n\nimport { useState } from 'react';\nimport { ArrowLeft } from 'lucide-react';\nimport { PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { ErrorView } from '../../components/StateViews';\nimport { useSubscriberBot } from '../../context/SubscriberBotContext';\nimport {\n useCreateRelationship,\n useProviders,\n usePlans,\n} from '../../hooks/useSubscriberBotApi';\nimport type {\n SubBillingInterval,\n SubCreateRelationshipInput,\n SubRelationshipKind,\n} from '../../types';\n\nconst KINDS: SubRelationshipKind[] = [\n 'SUBSCRIPTION',\n 'MEMBERSHIP',\n 'LICENSE',\n 'CONTRACT',\n 'NEWSLETTER',\n 'RECURRING_PAYMENT',\n 'ENROLLMENT',\n 'TRIAL',\n];\n\nconst INTERVALS: SubBillingInterval[] = [\n 'MONTHLY',\n 'YEARLY',\n 'QUARTERLY',\n 'WEEKLY',\n 'DAILY',\n 'USAGE_BASED',\n 'LIFETIME',\n 'FREE',\n];\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\nexport function RelationshipNewPage() {\n const { navigateTo } = useSubscriberBot();\n const create = useCreateRelationship();\n const providersQ = useProviders({ pagination: { limit: 100, offset: 0 } });\n\n const [label, setLabel] = useState('');\n const [kind, setKind] = useState<SubRelationshipKind>('SUBSCRIPTION');\n const [providerId, setProviderId] = useState('');\n const [planId, setPlanId] = useState('');\n const [amount, setAmount] = useState('');\n const [currency, setCurrency] = useState('USD');\n const [interval, setInterval] = useState<SubBillingInterval>('MONTHLY');\n const [renewsAt, setRenewsAt] = useState('');\n const [autoRenew, setAutoRenew] = useState(true);\n const [notes, setNotes] = useState('');\n const [formError, setFormError] = useState<string | null>(null);\n\n const plansQ = usePlans(providerId || undefined);\n const providers = providersQ.data?.items ?? [];\n const plans = plansQ.data ?? [];\n\n const handleSubmit = (e: React.FormEvent) => {\n e.preventDefault();\n setFormError(null);\n if (!label.trim()) {\n setFormError('A label is required.');\n return;\n }\n const input: SubCreateRelationshipInput = {\n label: label.trim(),\n kind,\n interval,\n autoRenew,\n currency: currency.trim() || 'USD',\n ...(providerId ? { providerId } : {}),\n ...(planId ? { planId } : {}),\n ...(amount ? { amount: Number(amount) } : {}),\n ...(renewsAt ? { renewsAt: new Date(renewsAt).toISOString() } : {}),\n ...(notes.trim() ? { notes: notes.trim() } : {}),\n };\n create.mutate(input, {\n onSuccess: (rel) => navigateTo(`/relationships/${rel.id}`),\n onError: (err) =>\n setFormError(err instanceof Error ? err.message : 'Could not create relationship.'),\n });\n };\n\n if (providersQ.isError) {\n return (\n <PageLayout title=\"Add relationship\">\n <ErrorView\n message=\"Could not load the provider registry.\"\n onRetry={() => void providersQ.refetch()}\n />\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title=\"Add relationship\"\n description=\"Track a new recurring digital relationship\"\n actions={\n <button\n type=\"button\"\n onClick={() => navigateTo('/relationships')}\n className=\"inline-flex items-center gap-2 rounded-lg border border-border-subtle px-3 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken\"\n >\n <ArrowLeft className=\"h-4 w-4\" /> Back\n </button>\n }\n >\n <PagePurpose className=\"mb-6\">\n Manually add a subscription, membership, license or contract. Link it to a provider and plan\n from the registry, set the price and renewal date, and Subscriber Bot will track its\n lifecycle from here.\n </PagePurpose>\n\n <form onSubmit={handleSubmit} className=\"max-w-2xl space-y-5\">\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\n <div>\n <label htmlFor=\"rel-label\" className={labelCls}>\n Label <span className=\"text-status-error-text\">*</span>\n </label>\n <input\n id=\"rel-label\"\n type=\"text\"\n value={label}\n onChange={(e) => setLabel(e.target.value)}\n placeholder=\"e.g. Netflix Premium, AWS Production\"\n className={inputCls}\n required\n />\n </div>\n\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <div>\n <label htmlFor=\"rel-kind\" className={labelCls}>\n Kind\n </label>\n <select\n id=\"rel-kind\"\n value={kind}\n onChange={(e) => setKind(e.target.value as SubRelationshipKind)}\n className={inputCls}\n >\n {KINDS.map((k) => (\n <option key={k} value={k}>\n {k.replace(/_/g, ' ')}\n </option>\n ))}\n </select>\n </div>\n <div>\n <label htmlFor=\"rel-provider\" className={labelCls}>\n Provider\n </label>\n <select\n id=\"rel-provider\"\n value={providerId}\n onChange={(e) => {\n setProviderId(e.target.value);\n setPlanId('');\n }}\n className={inputCls}\n >\n <option value=\"\">— None —</option>\n {providers.map((p) => (\n <option key={p.id} value={p.id}>\n {p.name}\n </option>\n ))}\n </select>\n </div>\n </div>\n\n {providerId && plans.length > 0 && (\n <div>\n <label htmlFor=\"rel-plan\" className={labelCls}>\n Plan\n </label>\n <select\n id=\"rel-plan\"\n value={planId}\n onChange={(e) => setPlanId(e.target.value)}\n className={inputCls}\n >\n <option value=\"\">— None —</option>\n {plans.map((pl) => (\n <option key={pl.id} value={pl.id}>\n {pl.name}\n </option>\n ))}\n </select>\n </div>\n )}\n\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-3\">\n <div>\n <label htmlFor=\"rel-amount\" className={labelCls}>\n Amount\n </label>\n <input\n id=\"rel-amount\"\n type=\"number\"\n min=\"0\"\n step=\"0.01\"\n value={amount}\n onChange={(e) => setAmount(e.target.value)}\n placeholder=\"0.00\"\n className={inputCls}\n />\n </div>\n <div>\n <label htmlFor=\"rel-currency\" className={labelCls}>\n Currency\n </label>\n <input\n id=\"rel-currency\"\n type=\"text\"\n value={currency}\n onChange={(e) => setCurrency(e.target.value.toUpperCase())}\n placeholder=\"USD\"\n maxLength={3}\n className={inputCls}\n />\n </div>\n <div>\n <label htmlFor=\"rel-interval\" className={labelCls}>\n Billing interval\n </label>\n <select\n id=\"rel-interval\"\n value={interval}\n onChange={(e) => setInterval(e.target.value as SubBillingInterval)}\n className={inputCls}\n >\n {INTERVALS.map((i) => (\n <option key={i} value={i}>\n {i.replace(/_/g, ' ')}\n </option>\n ))}\n </select>\n </div>\n </div>\n\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <div>\n <label htmlFor=\"rel-renews\" className={labelCls}>\n Renews on\n </label>\n <input\n id=\"rel-renews\"\n type=\"date\"\n value={renewsAt}\n onChange={(e) => setRenewsAt(e.target.value)}\n className={inputCls}\n />\n </div>\n <div className=\"flex items-end\">\n <label className=\"flex items-center gap-2 text-sm text-text-primary\">\n <input\n type=\"checkbox\"\n checked={autoRenew}\n onChange={(e) => setAutoRenew(e.target.checked)}\n className=\"size-4 rounded border-border-subtle\"\n />\n Auto-renews\n </label>\n </div>\n </div>\n\n <div>\n <label htmlFor=\"rel-notes\" className={labelCls}>\n Notes\n </label>\n <textarea\n id=\"rel-notes\"\n value={notes}\n onChange={(e) => setNotes(e.target.value)}\n rows={3}\n placeholder=\"Anything worth remembering about this relationship…\"\n className={inputCls}\n />\n </div>\n\n <div className=\"flex gap-3\">\n <button\n type=\"submit\"\n disabled={create.isPending}\n className=\"rounded-lg bg-action-primary-bg px-5 py-2.5 text-sm font-medium text-action-primary-text hover:opacity-90 disabled:opacity-50\"\n >\n {create.isPending ? 'Creating…' : 'Create relationship'}\n </button>\n <button\n type=\"button\"\n onClick={() => navigateTo('/relationships')}\n className=\"rounded-lg border border-border-subtle px-5 py-2.5 text-sm font-medium text-text-primary hover:bg-bg-sunken\"\n >\n Cancel\n </button>\n </div>\n </form>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"RelationshipNewPage.js","names":[],"sources":["../../../../src/subscriberbot/pages/relationships/RelationshipNewPage.tsx"],"sourcesContent":["/**\n * Create relationship\n * @module subscriberbot/pages/relationships\n *\n * Manual-add form for a recurring relationship AI can't auto-detect.\n * Writes `subCreateRelationship`. Provider + plan are optional pickers\n * sourced from the real registry (`subListProviders` / `subListPlans`).\n */\n\nimport { useState } from 'react';\nimport { ArrowLeft } from 'lucide-react';\nimport { PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { ErrorView } from '../../components/StateViews';\nimport { useSubscriberBot } from '../../context/SubscriberBotContext';\nimport { useCreateRelationship, useProviders, usePlans } from '../../hooks/useSubscriberBotApi';\nimport type {\n SubBillingInterval,\n SubCreateRelationshipInput,\n SubRelationshipKind,\n} from '../../types';\n\nconst KINDS: SubRelationshipKind[] = [\n 'SUBSCRIPTION',\n 'MEMBERSHIP',\n 'LICENSE',\n 'CONTRACT',\n 'NEWSLETTER',\n 'RECURRING_PAYMENT',\n 'ENROLLMENT',\n 'TRIAL',\n];\n\nconst INTERVALS: SubBillingInterval[] = [\n 'MONTHLY',\n 'YEARLY',\n 'QUARTERLY',\n 'WEEKLY',\n 'DAILY',\n 'USAGE_BASED',\n 'LIFETIME',\n 'FREE',\n];\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\nexport function RelationshipNewPage() {\n const { navigateTo } = useSubscriberBot();\n const create = useCreateRelationship();\n const providersQ = useProviders({ pagination: { limit: 100, offset: 0 } });\n\n const [label, setLabel] = useState('');\n const [kind, setKind] = useState<SubRelationshipKind>('SUBSCRIPTION');\n const [providerId, setProviderId] = useState('');\n const [planId, setPlanId] = useState('');\n const [amount, setAmount] = useState('');\n const [currency, setCurrency] = useState('USD');\n const [interval, setInterval] = useState<SubBillingInterval>('MONTHLY');\n const [renewsAt, setRenewsAt] = useState('');\n const [autoRenew, setAutoRenew] = useState(true);\n const [notes, setNotes] = useState('');\n const [formError, setFormError] = useState<string | null>(null);\n\n const plansQ = usePlans(providerId || undefined);\n const providers = providersQ.data?.items ?? [];\n const plans = plansQ.data ?? [];\n\n const handleSubmit = (e: React.FormEvent) => {\n e.preventDefault();\n setFormError(null);\n if (!label.trim()) {\n setFormError('A label is required.');\n return;\n }\n const input: SubCreateRelationshipInput = {\n label: label.trim(),\n kind,\n interval,\n autoRenew,\n currency: currency.trim() || 'USD',\n ...(providerId ? { providerId } : {}),\n ...(planId ? { planId } : {}),\n ...(amount ? { amount: Number(amount) } : {}),\n ...(renewsAt ? { renewsAt: new Date(renewsAt).toISOString() } : {}),\n ...(notes.trim() ? { notes: notes.trim() } : {}),\n };\n create.mutate(input, {\n onSuccess: (rel) => navigateTo(`/relationships/${rel.id}`),\n onError: (err) =>\n setFormError(err instanceof Error ? err.message : 'Could not create relationship.'),\n });\n };\n\n if (providersQ.isError) {\n return (\n <PageLayout title=\"Add relationship\">\n <ErrorView\n message=\"Could not load the provider registry.\"\n onRetry={() => void providersQ.refetch()}\n />\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title=\"Add relationship\"\n description=\"Track a new recurring digital relationship\"\n actions={\n <button\n type=\"button\"\n onClick={() => navigateTo('/relationships')}\n className=\"inline-flex items-center gap-2 rounded-lg border border-border-subtle px-3 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken\"\n >\n <ArrowLeft className=\"h-4 w-4\" /> Back\n </button>\n }\n >\n <PagePurpose className=\"mb-6\">\n Manually add a subscription, membership, license or contract. Link it to a provider and plan\n from the registry, set the price and renewal date, and Subscriber Bot will track its\n lifecycle from here.\n </PagePurpose>\n\n <form onSubmit={handleSubmit} className=\"max-w-2xl space-y-5\">\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\n <div>\n <label htmlFor=\"rel-label\" className={labelCls}>\n Label <span className=\"text-status-error-text\">*</span>\n </label>\n <input\n id=\"rel-label\"\n type=\"text\"\n value={label}\n onChange={(e) => setLabel(e.target.value)}\n placeholder=\"e.g. Netflix Premium, AWS Production\"\n className={inputCls}\n required\n />\n </div>\n\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <div>\n <label htmlFor=\"rel-kind\" className={labelCls}>\n Kind\n </label>\n <select\n id=\"rel-kind\"\n value={kind}\n onChange={(e) => setKind(e.target.value as SubRelationshipKind)}\n className={inputCls}\n >\n {KINDS.map((k) => (\n <option key={k} value={k}>\n {k.replace(/_/g, ' ')}\n </option>\n ))}\n </select>\n </div>\n <div>\n <label htmlFor=\"rel-provider\" className={labelCls}>\n Provider\n </label>\n <select\n id=\"rel-provider\"\n value={providerId}\n onChange={(e) => {\n setProviderId(e.target.value);\n setPlanId('');\n }}\n className={inputCls}\n >\n <option value=\"\">— None —</option>\n {providers.map((p) => (\n <option key={p.id} value={p.id}>\n {p.name}\n </option>\n ))}\n </select>\n </div>\n </div>\n\n {providerId && plans.length > 0 && (\n <div>\n <label htmlFor=\"rel-plan\" className={labelCls}>\n Plan\n </label>\n <select\n id=\"rel-plan\"\n value={planId}\n onChange={(e) => setPlanId(e.target.value)}\n className={inputCls}\n >\n <option value=\"\">— None —</option>\n {plans.map((pl) => (\n <option key={pl.id} value={pl.id}>\n {pl.name}\n </option>\n ))}\n </select>\n </div>\n )}\n\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-3\">\n <div>\n <label htmlFor=\"rel-amount\" className={labelCls}>\n Amount\n </label>\n <input\n id=\"rel-amount\"\n type=\"number\"\n min=\"0\"\n step=\"0.01\"\n value={amount}\n onChange={(e) => setAmount(e.target.value)}\n placeholder=\"0.00\"\n className={inputCls}\n />\n </div>\n <div>\n <label htmlFor=\"rel-currency\" className={labelCls}>\n Currency\n </label>\n <input\n id=\"rel-currency\"\n type=\"text\"\n value={currency}\n onChange={(e) => setCurrency(e.target.value.toUpperCase())}\n placeholder=\"USD\"\n maxLength={3}\n className={inputCls}\n />\n </div>\n <div>\n <label htmlFor=\"rel-interval\" className={labelCls}>\n Billing interval\n </label>\n <select\n id=\"rel-interval\"\n value={interval}\n onChange={(e) => setInterval(e.target.value as SubBillingInterval)}\n className={inputCls}\n >\n {INTERVALS.map((i) => (\n <option key={i} value={i}>\n {i.replace(/_/g, ' ')}\n </option>\n ))}\n </select>\n </div>\n </div>\n\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <div>\n <label htmlFor=\"rel-renews\" className={labelCls}>\n Renews on\n </label>\n <input\n id=\"rel-renews\"\n type=\"date\"\n value={renewsAt}\n onChange={(e) => setRenewsAt(e.target.value)}\n className={inputCls}\n />\n </div>\n <div className=\"flex items-end\">\n <label className=\"flex items-center gap-2 text-sm text-text-primary\">\n <input\n type=\"checkbox\"\n checked={autoRenew}\n onChange={(e) => setAutoRenew(e.target.checked)}\n className=\"size-4 rounded border-border-subtle\"\n />\n Auto-renews\n </label>\n </div>\n </div>\n\n <div>\n <label htmlFor=\"rel-notes\" className={labelCls}>\n Notes\n </label>\n <textarea\n id=\"rel-notes\"\n value={notes}\n onChange={(e) => setNotes(e.target.value)}\n rows={3}\n placeholder=\"Anything worth remembering about this relationship…\"\n className={inputCls}\n />\n </div>\n\n <div className=\"flex gap-3\">\n <button\n type=\"submit\"\n disabled={create.isPending}\n className=\"rounded-lg bg-action-primary-bg px-5 py-2.5 text-sm font-medium text-action-primary-text hover:opacity-90 disabled:opacity-50\"\n >\n {create.isPending ? 'Creating…' : 'Create relationship'}\n </button>\n <button\n type=\"button\"\n onClick={() => navigateTo('/relationships')}\n className=\"rounded-lg border border-border-subtle px-5 py-2.5 text-sm font-medium text-text-primary hover:bg-bg-sunken\"\n >\n Cancel\n </button>\n </div>\n </form>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RelationshipsListPage.js","names":[],"sources":["../../../../src/subscriberbot/pages/relationships/RelationshipsListPage.tsx"],"sourcesContent":["/**\n * Relationships list\n * @module subscriberbot/pages/relationships\n *\n * The core CRUDL list — every recurring relationship (subscription /\n * membership / license / contract) in one paginated, filterable view.\n * Data comes from `subListRelationships` (a SubRelationshipConnection).\n */\n\nimport { useMemo, useState } from 'react';\nimport { Plus, Search, Repeat2 } from 'lucide-react';\nimport { EmphasisPanel, PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { useSubscriberBot } from '../../context/SubscriberBotContext';\nimport { useRelationships } from '../../hooks/useSubscriberBotApi';\nimport { cn } from '../../utils/cn';\nimport { formatCurrency, formatDate, formatEnumLabel, formatIntervalSuffix } from '../../utils/formatters';\nimport type { SubRelationshipKind, SubRelationshipStatus } from '../../types';\n\nconst PAGE_SIZE = 20;\n\nconst STATUS_OPTIONS: Array<{ value: '' | SubRelationshipStatus; label: string }> = [\n { value: '', label: 'All statuses' },\n { value: 'ACTIVE', label: 'Active' },\n { value: 'TRIAL', label: 'Trial' },\n { value: 'PAUSED', label: 'Paused' },\n { value: 'PAST_DUE', label: 'Past due' },\n { value: 'LAPSED', label: 'Lapsed' },\n { value: 'EXPIRED', label: 'Expired' },\n { value: 'CANCELLED', label: 'Cancelled' },\n];\n\nconst KIND_OPTIONS: Array<{ value: '' | SubRelationshipKind; label: string }> = [\n { value: '', label: 'All kinds' },\n { value: 'SUBSCRIPTION', label: 'Subscription' },\n { value: 'MEMBERSHIP', label: 'Membership' },\n { value: 'LICENSE', label: 'License' },\n { value: 'CONTRACT', label: 'Contract' },\n { value: 'NEWSLETTER', label: 'Newsletter' },\n { value: 'RECURRING_PAYMENT', label: 'Recurring payment' },\n { value: 'ENROLLMENT', label: 'Enrollment' },\n { value: 'TRIAL', label: 'Trial' },\n];\n\nexport function RelationshipsListPage() {\n const { navigateTo } = useSubscriberBot();\n const [page, setPage] = useState(0);\n const [statusFilter, setStatusFilter] = useState<'' | SubRelationshipStatus>('');\n const [kindFilter, setKindFilter] = useState<'' | SubRelationshipKind>('');\n const [search, setSearch] = useState('');\n\n const { data, isLoading, isError, refetch, isFetching } = useRelationships({\n status: statusFilter || undefined,\n kind: kindFilter || undefined,\n pagination: { limit: PAGE_SIZE, offset: page * PAGE_SIZE },\n });\n\n const items = useMemo(() => data?.items ?? [], [data]);\n const hasMore = data?.pageInfo.hasMore ?? false;\n\n // Client-side label search over the current page (server has no search arg).\n const visible = useMemo(() => {\n if (!search.trim()) return items;\n const q = search.toLowerCase();\n return items.filter(\n (r) =>\n r.label.toLowerCase().includes(q) ||\n (r.provider?.name?.toLowerCase().includes(q) ?? false)\n );\n }, [items, search]);\n\n return (\n <PageLayout\n title=\"Relationships\"\n description=\"Every subscription, membership, license and contract you track\"\n actions={\n <button\n type=\"button\"\n onClick={() => navigateTo('/relationships/new')}\n className=\"inline-flex items-center gap-2 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:opacity-90\"\n >\n <Plus className=\"h-4 w-4\" /> Add relationship\n </button>\n }\n >\n <PagePurpose className=\"mb-4\">\n This is the fundamental object of Subscriber Bot. Filter by lifecycle and kind, open any row\n to manage its plan, payment, entitlements, assets and renewals, or advance its lifecycle.\n </PagePurpose>\n\n <EmphasisPanel>\n <div className=\"relative\">\n <Search className=\"absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary\" />\n <input\n type=\"text\"\n placeholder=\"Search relationships on this page…\"\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n className=\"w-full rounded-lg border border-border-subtle bg-bg-surface py-2.5 pl-10 pr-3 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n />\n </div>\n </EmphasisPanel>\n\n <div className=\"mt-4 flex flex-wrap items-center gap-3\">\n <select\n value={statusFilter}\n onChange={(e) => {\n setStatusFilter(e.target.value as '' | SubRelationshipStatus);\n setPage(0);\n }}\n className=\"rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-xs text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n {STATUS_OPTIONS.map((o) => (\n <option key={o.value} value={o.value}>\n {o.label}\n </option>\n ))}\n </select>\n <select\n value={kindFilter}\n onChange={(e) => {\n setKindFilter(e.target.value as '' | SubRelationshipKind);\n setPage(0);\n }}\n className=\"rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-xs text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n {KIND_OPTIONS.map((o) => (\n <option key={o.value} value={o.value}>\n {o.label}\n </option>\n ))}\n </select>\n </div>\n\n {isLoading ? (\n <LoadingView label=\"Loading relationships…\" />\n ) : isError ? (\n <ErrorView message=\"Could not load relationships.\" onRetry={() => void refetch()} />\n ) : visible.length === 0 ? (\n <div className=\"mt-6\">\n <EmptyState\n illustration=\"empty-data\"\n icon={<Repeat2 className=\"h-12 w-12\" />}\n title=\"No relationships found\"\n description={\n search\n ? `No matches for \"${search}\" on this page.`\n : 'Add your first recurring relationship to start tracking it.'\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 Add relationship\n </button>\n }\n />\n </div>\n ) : (\n <>\n <div className=\"mt-6 overflow-hidden rounded-lg border border-border-subtle bg-bg-surface\">\n <div className=\"hidden grid-cols-[2fr_1fr_1fr_1fr_1fr] gap-4 border-b border-border-subtle bg-bg-sunken px-4 py-2.5 text-xs font-medium text-text-secondary sm:grid\">\n <span>Relationship</span>\n <span>Kind</span>\n <span>Status</span>\n <span className=\"text-right\">Amount</span>\n <span className=\"text-right\">Renews</span>\n </div>\n <div className=\"divide-y divide-border-subtle\">\n {visible.map((r) => (\n <button\n key={r.id}\n type=\"button\"\n onClick={() => navigateTo(`/relationships/${r.id}`)}\n className=\"grid w-full grid-cols-1 gap-2 px-4 py-3 text-left hover:bg-bg-sunken sm:grid-cols-[2fr_1fr_1fr_1fr_1fr] sm:items-center sm:gap-4\"\n >\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-medium text-text-primary\">{r.label}</p>\n {r.provider?.name && (\n <p className=\"truncate text-xs text-text-secondary\">{r.provider.name}</p>\n )}\n </div>\n <span className=\"text-xs text-text-secondary\">{formatEnumLabel(r.kind)}</span>\n <span>\n <StatusBadge status={r.status} />\n </span>\n <span className=\"text-sm font-semibold tabular-nums text-text-primary sm:text-right\">\n {formatCurrency(r.amount, r.currency)}\n <span className=\"text-xs font-normal text-text-secondary\">\n {formatIntervalSuffix(r.interval)}\n </span>\n </span>\n <span className=\"text-xs text-text-secondary sm:text-right\">\n {r.renewsAt ? formatDate(r.renewsAt) : '—'}\n </span>\n </button>\n ))}\n </div>\n </div>\n\n <div className=\"mt-4 flex items-center justify-between\">\n <p className=\"text-xs text-text-secondary\">\n Page {page + 1}\n {isFetching ? ' · refreshing…' : ''}\n </p>\n <div className=\"flex gap-2\">\n <button\n type=\"button\"\n disabled={page === 0}\n onClick={() => setPage((p) => Math.max(0, p - 1))}\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 Previous\n </button>\n <button\n type=\"button\"\n disabled={!hasMore}\n onClick={() => setPage((p) => p + 1)}\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 Next\n </button>\n </div>\n </div>\n </>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"RelationshipsListPage.js","names":[],"sources":["../../../../src/subscriberbot/pages/relationships/RelationshipsListPage.tsx"],"sourcesContent":["/**\n * Relationships list\n * @module subscriberbot/pages/relationships\n *\n * The core CRUDL list — every recurring relationship (subscription /\n * membership / license / contract) in one paginated, filterable view.\n * Data comes from `subListRelationships` (a SubRelationshipConnection).\n */\n\nimport { useMemo, useState } from 'react';\nimport { Plus, Search, Repeat2 } from 'lucide-react';\nimport { EmphasisPanel, PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { useSubscriberBot } from '../../context/SubscriberBotContext';\nimport { useRelationships } from '../../hooks/useSubscriberBotApi';\nimport { cn } from '../../utils/cn';\nimport {\n formatCurrency,\n formatDate,\n formatEnumLabel,\n formatIntervalSuffix,\n} from '../../utils/formatters';\nimport type { SubRelationshipKind, SubRelationshipStatus } from '../../types';\n\nconst PAGE_SIZE = 20;\n\nconst STATUS_OPTIONS: Array<{ value: '' | SubRelationshipStatus; label: string }> = [\n { value: '', label: 'All statuses' },\n { value: 'ACTIVE', label: 'Active' },\n { value: 'TRIAL', label: 'Trial' },\n { value: 'PAUSED', label: 'Paused' },\n { value: 'PAST_DUE', label: 'Past due' },\n { value: 'LAPSED', label: 'Lapsed' },\n { value: 'EXPIRED', label: 'Expired' },\n { value: 'CANCELLED', label: 'Cancelled' },\n];\n\nconst KIND_OPTIONS: Array<{ value: '' | SubRelationshipKind; label: string }> = [\n { value: '', label: 'All kinds' },\n { value: 'SUBSCRIPTION', label: 'Subscription' },\n { value: 'MEMBERSHIP', label: 'Membership' },\n { value: 'LICENSE', label: 'License' },\n { value: 'CONTRACT', label: 'Contract' },\n { value: 'NEWSLETTER', label: 'Newsletter' },\n { value: 'RECURRING_PAYMENT', label: 'Recurring payment' },\n { value: 'ENROLLMENT', label: 'Enrollment' },\n { value: 'TRIAL', label: 'Trial' },\n];\n\nexport function RelationshipsListPage() {\n const { navigateTo } = useSubscriberBot();\n const [page, setPage] = useState(0);\n const [statusFilter, setStatusFilter] = useState<'' | SubRelationshipStatus>('');\n const [kindFilter, setKindFilter] = useState<'' | SubRelationshipKind>('');\n const [search, setSearch] = useState('');\n\n const { data, isLoading, isError, refetch, isFetching } = useRelationships({\n status: statusFilter || undefined,\n kind: kindFilter || undefined,\n pagination: { limit: PAGE_SIZE, offset: page * PAGE_SIZE },\n });\n\n const items = useMemo(() => data?.items ?? [], [data]);\n const hasMore = data?.pageInfo.hasMore ?? false;\n\n // Client-side label search over the current page (server has no search arg).\n const visible = useMemo(() => {\n if (!search.trim()) return items;\n const q = search.toLowerCase();\n return items.filter(\n (r) =>\n r.label.toLowerCase().includes(q) || (r.provider?.name?.toLowerCase().includes(q) ?? false)\n );\n }, [items, search]);\n\n return (\n <PageLayout\n title=\"Relationships\"\n description=\"Every subscription, membership, license and contract you track\"\n actions={\n <button\n type=\"button\"\n onClick={() => navigateTo('/relationships/new')}\n className=\"inline-flex items-center gap-2 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:opacity-90\"\n >\n <Plus className=\"h-4 w-4\" /> Add relationship\n </button>\n }\n >\n <PagePurpose className=\"mb-4\">\n This is the fundamental object of Subscriber Bot. Filter by lifecycle and kind, open any row\n to manage its plan, payment, entitlements, assets and renewals, or advance its lifecycle.\n </PagePurpose>\n\n <EmphasisPanel>\n <div className=\"relative\">\n <Search className=\"absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary\" />\n <input\n type=\"text\"\n placeholder=\"Search relationships on this page…\"\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n className=\"w-full rounded-lg border border-border-subtle bg-bg-surface py-2.5 pl-10 pr-3 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n />\n </div>\n </EmphasisPanel>\n\n <div className=\"mt-4 flex flex-wrap items-center gap-3\">\n <select\n value={statusFilter}\n onChange={(e) => {\n setStatusFilter(e.target.value as '' | SubRelationshipStatus);\n setPage(0);\n }}\n className=\"rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-xs text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n {STATUS_OPTIONS.map((o) => (\n <option key={o.value} value={o.value}>\n {o.label}\n </option>\n ))}\n </select>\n <select\n value={kindFilter}\n onChange={(e) => {\n setKindFilter(e.target.value as '' | SubRelationshipKind);\n setPage(0);\n }}\n className=\"rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-xs text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n {KIND_OPTIONS.map((o) => (\n <option key={o.value} value={o.value}>\n {o.label}\n </option>\n ))}\n </select>\n </div>\n\n {isLoading ? (\n <LoadingView label=\"Loading relationships…\" />\n ) : isError ? (\n <ErrorView message=\"Could not load relationships.\" onRetry={() => void refetch()} />\n ) : visible.length === 0 ? (\n <div className=\"mt-6\">\n <EmptyState\n illustration=\"empty-data\"\n icon={<Repeat2 className=\"h-12 w-12\" />}\n title=\"No relationships found\"\n description={\n search\n ? `No matches for \"${search}\" on this page.`\n : 'Add your first recurring relationship to start tracking it.'\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 Add relationship\n </button>\n }\n />\n </div>\n ) : (\n <>\n <div className=\"mt-6 overflow-hidden rounded-lg border border-border-subtle bg-bg-surface\">\n <div className=\"hidden grid-cols-[2fr_1fr_1fr_1fr_1fr] gap-4 border-b border-border-subtle bg-bg-sunken px-4 py-2.5 text-xs font-medium text-text-secondary sm:grid\">\n <span>Relationship</span>\n <span>Kind</span>\n <span>Status</span>\n <span className=\"text-right\">Amount</span>\n <span className=\"text-right\">Renews</span>\n </div>\n <div className=\"divide-y divide-border-subtle\">\n {visible.map((r) => (\n <button\n key={r.id}\n type=\"button\"\n onClick={() => navigateTo(`/relationships/${r.id}`)}\n className=\"grid w-full grid-cols-1 gap-2 px-4 py-3 text-left hover:bg-bg-sunken sm:grid-cols-[2fr_1fr_1fr_1fr_1fr] sm:items-center sm:gap-4\"\n >\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-medium text-text-primary\">{r.label}</p>\n {r.provider?.name && (\n <p className=\"truncate text-xs text-text-secondary\">{r.provider.name}</p>\n )}\n </div>\n <span className=\"text-xs text-text-secondary\">{formatEnumLabel(r.kind)}</span>\n <span>\n <StatusBadge status={r.status} />\n </span>\n <span className=\"text-sm font-semibold tabular-nums text-text-primary sm:text-right\">\n {formatCurrency(r.amount, r.currency)}\n <span className=\"text-xs font-normal text-text-secondary\">\n {formatIntervalSuffix(r.interval)}\n </span>\n </span>\n <span className=\"text-xs text-text-secondary sm:text-right\">\n {r.renewsAt ? formatDate(r.renewsAt) : '—'}\n </span>\n </button>\n ))}\n </div>\n </div>\n\n <div className=\"mt-4 flex items-center justify-between\">\n <p className=\"text-xs text-text-secondary\">\n Page {page + 1}\n {isFetching ? ' · refreshing…' : ''}\n </p>\n <div className=\"flex gap-2\">\n <button\n type=\"button\"\n disabled={page === 0}\n onClick={() => setPage((p) => Math.max(0, p - 1))}\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 Previous\n </button>\n <button\n type=\"button\"\n disabled={!hasMore}\n onClick={() => setPage((p) => p + 1)}\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 Next\n </button>\n </div>\n </div>\n </>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|