@burdenoff/microfe-devportal 2026.914.2 → 2026.914.4
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/devportal/DevportalRoutes.d.ts.map +1 -1
- package/dist/devportal/DevportalRoutes.js +130 -110
- package/dist/devportal/DevportalRoutes.js.map +1 -1
- package/dist/devportal/components/SectionNav.d.ts +10 -1
- package/dist/devportal/components/SectionNav.d.ts.map +1 -1
- package/dist/devportal/components/SectionNav.js +60 -42
- package/dist/devportal/components/SectionNav.js.map +1 -1
- package/dist/devportal/hooks/useIsSellerSection.d.ts +13 -0
- package/dist/devportal/hooks/useIsSellerSection.d.ts.map +1 -0
- package/dist/devportal/hooks/useIsSellerSection.js +15 -0
- package/dist/devportal/hooks/useIsSellerSection.js.map +1 -0
- package/dist/devportal/pages/CreateApplicationPage.d.ts.map +1 -1
- package/dist/devportal/pages/CreateApplicationPage.js +5 -1
- package/dist/devportal/pages/CreateApplicationPage.js.map +1 -1
- package/dist/devportal/pages/CreateOrganizationPage.d.ts +3 -2
- package/dist/devportal/pages/CreateOrganizationPage.d.ts.map +1 -1
- package/dist/devportal/pages/CreateOrganizationPage.js +225 -122
- package/dist/devportal/pages/CreateOrganizationPage.js.map +1 -1
- package/dist/devportal/pages/CreatePhysicalItemPage.d.ts.map +1 -1
- package/dist/devportal/pages/CreatePhysicalItemPage.js +371 -205
- package/dist/devportal/pages/CreatePhysicalItemPage.js.map +1 -1
- package/dist/devportal/pages/DashboardPage.d.ts.map +1 -1
- package/dist/devportal/pages/DashboardPage.js +1 -1
- package/dist/devportal/pages/DashboardPage.js.map +1 -1
- package/dist/devportal/pages/EditPhysicalItemPage.d.ts.map +1 -1
- package/dist/devportal/pages/EditPhysicalItemPage.js +427 -168
- package/dist/devportal/pages/EditPhysicalItemPage.js.map +1 -1
- package/dist/devportal/pages/OrganizationDetailPage.d.ts.map +1 -1
- package/dist/devportal/pages/OrganizationDetailPage.js +361 -346
- package/dist/devportal/pages/OrganizationDetailPage.js.map +1 -1
- package/dist/devportal/pages/OrganizationMembersPage.d.ts.map +1 -1
- package/dist/devportal/pages/OrganizationMembersPage.js +291 -290
- package/dist/devportal/pages/OrganizationMembersPage.js.map +1 -1
- package/dist/devportal/pages/OrganizationsPage.d.ts.map +1 -1
- package/dist/devportal/pages/OrganizationsPage.js +102 -101
- package/dist/devportal/pages/OrganizationsPage.js.map +1 -1
- package/dist/devportal/pages/RevenueDashboardPage.d.ts.map +1 -1
- package/dist/devportal/pages/RevenueDashboardPage.js +210 -209
- package/dist/devportal/pages/RevenueDashboardPage.js.map +1 -1
- package/dist/devportal/pages/SellerDashboardPage.d.ts.map +1 -1
- package/dist/devportal/pages/SellerDashboardPage.js.map +1 -1
- package/dist/devportal/pages/SettingsPage.d.ts.map +1 -1
- package/dist/devportal/pages/SettingsPage.js +1 -1
- package/dist/devportal/pages/SettingsPage.js.map +1 -1
- package/dist/devportal/types/index.d.ts +1 -0
- package/dist/devportal/types/index.d.ts.map +1 -1
- package/dist/devportal/types/index.js.map +1 -1
- package/dist/generated/global-operations.d.ts +103 -12
- package/dist/generated/global-operations.d.ts.map +1 -1
- package/dist/generated/global-operations.js +108 -92
- package/dist/generated/global-operations.js.map +1 -1
- package/dist/generated/global-types.d.ts +2963 -242
- package/dist/generated/global-types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RevenueDashboardPage.js","names":[],"sources":["../../../src/devportal/pages/RevenueDashboardPage.tsx"],"sourcesContent":["import type { FC } from \"react\";\nimport { useMemo, useState } from \"react\";\nimport { useSearchParams } from \"react-router\";\nimport {\n Bar,\n BarChart,\n CartesianGrid,\n Line,\n LineChart,\n ResponsiveContainer,\n Tooltip,\n XAxis,\n YAxis,\n} from \"recharts\";\nimport {\n AlertCircle,\n ArrowDownToLine,\n Building2,\n Check,\n CreditCard,\n DollarSign,\n Link2,\n Loader2,\n Wallet,\n} from \"lucide-react\";\nimport { EmphasisPanel, GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport { useDevportal } from \"../providers/DevportalProvider\";\nimport { PageLayout, PageSection } from \"../components/PageLayout\";\nimport { DataTable } from \"../components/DataTable\";\nimport { EmptyState } from \"../components/EmptyState\";\nimport { StatCard, StatCardGrid } from \"../components/StatCard\";\nimport { StatusBadge } from \"../components/StatusBadge\";\nimport type { DeveloperOrganization } from \"../types\";\nimport {\n type GetMyOrganizationsQuery,\n type GetRevenueDashboardDataQuery,\n getErrorMessage,\n useGetBillingAccountsByOrgQuery,\n useGetMyOrganizationsQuery,\n useGetRevenueDashboardDataQuery,\n useLinkDevOrgBillingAccountMutation,\n} from \"../hooks\";\nimport { cn } from \"../utils/cn\";\nimport { useTr } from \"../utils/i18n\";\n\ntype TimeRange = \"30d\" | \"90d\" | \"12m\";\n\nconst chartColors = {\n primary: \"var(--color-chart-1)\",\n secondary: \"var(--color-chart-2)\",\n tertiary: \"var(--color-chart-4)\",\n};\n\ntype RevenueSale =\n GetRevenueDashboardDataQuery[\"developerOrgSales\"][\"edges\"][number][\"node\"];\ntype RevenuePayout =\n GetRevenueDashboardDataQuery[\"developerOrgPayouts\"][\"edges\"][number][\"node\"];\n\nconst daysByRange: Record<TimeRange, number> = {\n \"30d\": 30,\n \"90d\": 90,\n \"12m\": 365,\n};\n\nexport const RevenueDashboardPage: FC = () => {\n const tr = useTr();\n const { activeOrgId, tenantId } = useDevportal();\n const [timeRange, setTimeRange] = useState<TimeRange>(\"90d\");\n const [linkingBilling, setLinkingBilling] = useState(false);\n const [linkBillingError, setLinkBillingError] = useState<string | null>(null);\n\n const [linkBillingAccount] = useLinkDevOrgBillingAccountMutation();\n const [showBillingPicker, setShowBillingPicker] = useState(false);\n const [selectedBillingAccountId, setSelectedBillingAccountId] = useState<\n string | null\n >(null);\n const [searchParams] = useSearchParams();\n const now = useMemo(() => new Date(), []);\n\n const { data: billingAccountsData, loading: billingAccountsLoading } =\n useGetBillingAccountsByOrgQuery({\n fetchPolicy: \"cache-and-network\",\n context: { headers: { \"x-org-id\": tenantId ?? \"\" } },\n });\n // searchParams kept for deep-link compatibility (URL ?org= still works via OrgSwitcher auto-select)\n void searchParams;\n const startDate = useMemo(() => {\n const nextDate = new Date(now);\n nextDate.setDate(nextDate.getDate() - daysByRange[timeRange]);\n return nextDate.toISOString();\n }, [now, timeRange]);\n\n const {\n data: organizationsData,\n loading: organizationsLoading,\n error: organizationsError,\n } = useGetMyOrganizationsQuery({\n fetchPolicy: \"cache-and-network\",\n });\n const { data, loading, error } = useGetRevenueDashboardDataQuery({\n variables: {\n organizationId: activeOrgId ?? \"\",\n period: \"monthly\",\n startDate,\n endDate: now.toISOString(),\n salesLimit: 10,\n salesOffset: 0,\n payoutsLimit: 10,\n payoutsOffset: 0,\n },\n skip: !activeOrgId,\n fetchPolicy: \"cache-and-network\",\n });\n\n const organizations: DeveloperOrganization[] = useMemo(\n () =>\n organizationsData?.myDeveloperOrganizations?.map(\n (\n organization: GetMyOrganizationsQuery[\"myDeveloperOrganizations\"][number],\n ) => organization as unknown as DeveloperOrganization,\n ) ?? [],\n [organizationsData?.myDeveloperOrganizations],\n );\n\n const selectedOrganization = organizations.find(\n (organization) => organization.id === activeOrgId,\n );\n\n type BillingAccount = NonNullable<\n typeof billingAccountsData\n >[\"getBillingAccountsByOrg\"][number];\n const existingBillingAccounts: BillingAccount[] =\n billingAccountsData?.getBillingAccountsByOrg ?? [];\n\n const linkedBillingAccount =\n existingBillingAccounts.find(\n (a) => a.id === selectedOrganization?.billingAccountId,\n ) ?? null;\n\n const handleOpenBillingPicker = () => {\n // Pre-select the currently linked account; fall back to default or first\n setSelectedBillingAccountId(\n selectedOrganization?.billingAccountId ??\n existingBillingAccounts.find((a) => a.isDefault)?.id ??\n existingBillingAccounts[0]?.id ??\n null,\n );\n setShowBillingPicker(true);\n };\n\n const handleLinkExisting = async () => {\n if (!activeOrgId || !selectedBillingAccountId) return;\n setLinkingBilling(true);\n setLinkBillingError(null);\n try {\n await linkBillingAccount({\n variables: {\n organizationId: activeOrgId,\n billingAccountId: selectedBillingAccountId,\n },\n refetchQueries: [\"GetMyOrganizations\"],\n });\n setShowBillingPicker(false);\n } catch (err) {\n setLinkBillingError(\n err instanceof Error ? err.message : \"Failed to link billing account\",\n );\n } finally {\n setLinkingBilling(false);\n }\n };\n\n const summary = data?.developerOrgRevenueSummary;\n const analytics = [...(data?.developerOrgRevenueAnalytics ?? [])].sort(\n (left, right) =>\n new Date(left.date).getTime() - new Date(right.date).getTime(),\n );\n const sales: RevenueSale[] =\n data?.developerOrgSales?.edges?.map(\n (\n edge: GetRevenueDashboardDataQuery[\"developerOrgSales\"][\"edges\"][number],\n ) => edge.node,\n ) ?? [];\n const payouts: RevenuePayout[] =\n data?.developerOrgPayouts?.edges?.map(\n (\n edge: GetRevenueDashboardDataQuery[\"developerOrgPayouts\"][\"edges\"][number],\n ) => edge.node,\n ) ?? [];\n\n const chartData = analytics.map((entry) => ({\n date: new Date(entry.date).toLocaleDateString(undefined, {\n month: \"short\",\n day: \"numeric\",\n }),\n grossRevenue: entry.grossRevenue,\n netRevenue: entry.netRevenue,\n platformFees: entry.platformFees,\n refunds: entry.refunds,\n salesCount: entry.salesCount,\n }));\n\n if (organizationsLoading && !organizationsData) {\n return (\n <PageLayout\n title={tr(\"devportal.revenue.title\", \"Revenue\")}\n description={tr(\n \"devportal.revenue.description\",\n \"Track real revenue, sales, and payout activity for your developer organizations\",\n )}\n >\n <div className=\"flex items-center justify-center py-16 text-text-secondary\">\n <Loader2 className=\"h-8 w-8 animate-spin\" />\n </div>\n </PageLayout>\n );\n }\n\n if (organizationsError) {\n return (\n <PageLayout\n title={tr(\"devportal.revenue.title\", \"Revenue\")}\n description={tr(\n \"devportal.revenue.description\",\n \"Track real revenue, sales, and payout activity for your developer organizations\",\n )}\n >\n <EmptyState\n icon={<AlertCircle size={32} className=\"text-status-error-text\" />}\n title={tr(\n \"devportal.revenue.errorLoadOrgs\",\n \"Unable to load organizations\",\n )}\n description={getErrorMessage(organizationsError)}\n />\n </PageLayout>\n );\n }\n\n if (!activeOrgId) {\n return (\n <PageLayout\n title={tr(\"devportal.revenue.title\", \"Revenue\")}\n description={tr(\n \"devportal.revenue.description\",\n \"Track real revenue, sales, and payout activity for your developer organizations\",\n )}\n >\n <EmptyState\n illustration=\"empty-people\"\n icon={<Building2 size={32} className=\"text-text-secondary\" />}\n title={tr(\n \"devportal.revenue.selectOrgTitle\",\n \"Select an organization\",\n )}\n description={tr(\n \"devportal.revenue.selectOrgHint\",\n \"Use the organization switcher in the nav bar above to choose which organization's revenue to view.\",\n )}\n />\n </PageLayout>\n );\n }\n\n if (error) {\n return (\n <PageLayout\n title={tr(\"devportal.revenue.title\", \"Revenue\")}\n description={tr(\n \"devportal.revenue.description\",\n \"Track real revenue, sales, and payout activity for your developer organizations\",\n )}\n >\n <EmptyState\n icon={<AlertCircle size={32} className=\"text-status-error-text\" />}\n title={tr(\n \"devportal.revenue.errorLoadData\",\n \"Unable to load revenue data\",\n )}\n description={getErrorMessage(error)}\n />\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title={tr(\"devportal.revenue.title\", \"Revenue\")}\n description={\n selectedOrganization\n ? `${tr(\"devportal.revenue.financialPerformanceFor\", \"Financial performance for\")} ${selectedOrganization.name}`\n : tr(\n \"devportal.revenue.shortDescription\",\n \"Track revenue, sales, and payouts\",\n )\n }\n purpose={tr(\n \"devportal.revenue.purpose\",\n \"Revenue tracks the money your paid apps earn — gross sales, platform fees, and what gets paid out to your organization. Link a billing account, then use this to reconcile payouts and watch growth across time ranges.\",\n )}\n actions={\n <div className=\"flex flex-wrap gap-2\">\n <select\n value={timeRange}\n onChange={(event) => setTimeRange(event.target.value as TimeRange)}\n aria-label={tr(\n \"devportal.revenue.selectRangeAria\",\n \"Select time range\",\n )}\n className=\"rounded-md border bg-bg-surface px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary\"\n >\n <option value=\"30d\">\n {tr(\"devportal.revenue.range.30d\", \"Last 30 days\")}\n </option>\n <option value=\"90d\">\n {tr(\"devportal.revenue.range.90d\", \"Last 90 days\")}\n </option>\n <option value=\"12m\">\n {tr(\"devportal.revenue.range.12m\", \"Last 12 months\")}\n </option>\n </select>\n </div>\n }\n >\n {/* Billing account link banner for orgs not yet linked */}\n {selectedOrganization && !selectedOrganization.billingAccountId && (\n <div className=\"mb-6 rounded-card border border-border-seam bg-status-info-bg-subtle p-4 flex flex-col gap-3\">\n <div className=\"flex items-start gap-3\">\n <Link2\n size={20}\n className=\"text-accent-blue flex-shrink-0 mt-0.5\"\n />\n <div className=\"flex-1 min-w-0\">\n <p className=\"text-sm font-medium text-status-info-text\">\n {tr(\n \"devportal.revenue.billingNotLinkedTitle\",\n \"Link a billing account to receive store revenue\",\n )}\n </p>\n <p className=\"mt-1 text-sm text-status-info-text\">\n {tr(\n \"devportal.revenue.billingNotLinkedDesc\",\n \"Once linked, net earnings from app sales will appear in your billing credits — you can spend them on platform services or withdraw to your bank.\",\n )}\n </p>\n {linkBillingError && (\n <p className=\"mt-2 text-sm text-status-error-text\">\n {linkBillingError}\n </p>\n )}\n </div>\n <button\n type=\"button\"\n onClick={handleOpenBillingPicker}\n disabled={linkingBilling || billingAccountsLoading}\n className=\"inline-flex items-center gap-2 rounded-md bg-action-primary-bg px-3 py-1.5 text-xs font-medium text-action-primary-text hover:bg-action-primary-bg/90 transition-colors disabled:opacity-50 flex-shrink-0\"\n >\n {linkingBilling || billingAccountsLoading ? (\n <Loader2 size={14} className=\"animate-spin\" />\n ) : (\n <Link2 size={14} />\n )}\n {linkingBilling\n ? tr(\"devportal.revenue.linking\", \"Linking...\")\n : tr(\n \"devportal.revenue.linkBillingAccount\",\n \"Link Billing Account\",\n )}\n </button>\n </div>\n\n {/* No billing accounts message */}\n {showBillingPicker && existingBillingAccounts.length === 0 && (\n <div className=\"rounded-lg border border-border-subtle bg-bg-surface p-4 ml-8 text-sm text-text-secondary\">\n {tr(\n \"devportal.revenue.noBillingAccounts\",\n \"You don't have any billing accounts yet. Create one from your account billing settings first.\",\n )}\n </div>\n )}\n\n {/* Billing account picker — shown when user has existing accounts */}\n {showBillingPicker && existingBillingAccounts.length > 0 && (\n <div className=\"rounded-lg border border-border-subtle bg-bg-surface p-4 ml-8 space-y-3\">\n <p className=\"text-sm font-medium\">\n {tr(\n \"devportal.revenue.chooseBillingAccount\",\n \"Choose a billing account to link\",\n )}\n </p>\n <div className=\"space-y-2\">\n {existingBillingAccounts.map((account) => (\n <button\n key={account.id}\n type=\"button\"\n onClick={() =>\n setSelectedBillingAccountId(account.id ?? null)\n }\n className={cn(\n \"flex w-full items-center gap-3 rounded-md border px-3 py-2.5 text-sm text-left transition-colors\",\n selectedBillingAccountId === account.id\n ? \"border-primary bg-action-primary-bg/5 text-text-primary\"\n : \"border-border-subtle hover:bg-accent text-text-primary\",\n )}\n >\n <Check\n size={14}\n className={cn(\n \"flex-shrink-0\",\n selectedBillingAccountId === account.id\n ? \"opacity-100 text-primary\"\n : \"opacity-0\",\n )}\n />\n <div className=\"flex-1 min-w-0\">\n <p className=\"font-medium truncate\">{account.name}</p>\n {account.isDefault && (\n <p className=\"text-xs text-text-secondary\">\n {tr(\"devportal.revenue.defaultAccount\", \"Default\")}\n </p>\n )}\n </div>\n </button>\n ))}\n </div>\n <div className=\"flex items-center justify-end gap-2 pt-1\">\n <button\n type=\"button\"\n onClick={() => {\n setShowBillingPicker(false);\n setLinkBillingError(null);\n }}\n className=\"rounded-md px-3 py-1.5 text-xs font-medium text-text-secondary hover:bg-accent transition-colors\"\n >\n {tr(\"devportal.cancel\", \"Cancel\")}\n </button>\n <button\n type=\"button\"\n disabled={linkingBilling || !selectedBillingAccountId}\n onClick={() => void handleLinkExisting()}\n className=\"inline-flex items-center gap-1.5 rounded-md bg-action-primary-bg px-3 py-1.5 text-xs font-medium text-action-primary-text hover:bg-action-primary-bg/90 transition-colors disabled:opacity-50\"\n >\n {linkingBilling ? (\n <Loader2 size={13} className=\"animate-spin\" />\n ) : (\n <Link2 size={13} />\n )}\n {tr(\"devportal.revenue.confirm\", \"Confirm\")}\n </button>\n </div>\n </div>\n )}\n </div>\n )}\n\n {/* Linked billing account info — shown when billing is already linked */}\n {selectedOrganization?.billingAccountId && (\n <div className=\"mb-6 rounded-lg border border-border-subtle bg-bg-surface p-3 flex flex-col gap-3\">\n <div className=\"flex items-center gap-3\">\n <CreditCard\n size={16}\n className=\"text-text-secondary flex-shrink-0\"\n />\n <div className=\"flex-1 min-w-0\">\n <p className=\"text-sm font-medium truncate\">\n {linkedBillingAccount?.name ??\n tr(\"devportal.revenue.billingAccount\", \"Billing account\")}\n </p>\n <p className=\"text-xs text-text-secondary truncate\">\n {linkedBillingAccount\n ? `${linkedBillingAccount.currency}${linkedBillingAccount.email ? ` · ${linkedBillingAccount.email}` : \"\"}`\n : selectedOrganization.billingAccountId}\n </p>\n </div>\n {!showBillingPicker && (\n <button\n type=\"button\"\n onClick={handleOpenBillingPicker}\n disabled={billingAccountsLoading}\n className=\"text-xs text-text-secondary hover:text-text-primary transition-colors flex-shrink-0 disabled:opacity-50\"\n >\n {tr(\"devportal.revenue.changeBillingAccount\", \"Change\")}\n </button>\n )}\n </div>\n {/* Picker for changing the linked account */}\n {showBillingPicker && existingBillingAccounts.length > 0 && (\n <div className=\"rounded-lg border border-border-subtle bg-bg-surface p-4 space-y-3\">\n <p className=\"text-sm font-medium\">\n {tr(\n \"devportal.revenue.chooseBillingAccount\",\n \"Choose a billing account to link\",\n )}\n </p>\n <div className=\"space-y-2\">\n {existingBillingAccounts.map((account) => (\n <button\n key={account.id}\n type=\"button\"\n onClick={() =>\n setSelectedBillingAccountId(account.id ?? null)\n }\n className={cn(\n \"flex w-full items-center gap-3 rounded-md border px-3 py-2.5 text-sm text-left transition-colors\",\n selectedBillingAccountId === account.id\n ? \"border-primary bg-action-primary-bg/5 text-text-primary\"\n : \"border-border-subtle hover:bg-accent text-text-primary\",\n )}\n >\n <Check\n size={14}\n className={cn(\n \"flex-shrink-0\",\n selectedBillingAccountId === account.id\n ? \"opacity-100 text-primary\"\n : \"opacity-0\",\n )}\n />\n <div className=\"flex-1 min-w-0\">\n <p className=\"font-medium truncate\">{account.name}</p>\n {account.isDefault && (\n <p className=\"text-xs text-text-secondary\">\n {tr(\"devportal.revenue.defaultAccount\", \"Default\")}\n </p>\n )}\n </div>\n </button>\n ))}\n </div>\n <div className=\"flex items-center justify-end gap-2 pt-1\">\n <button\n type=\"button\"\n onClick={() => {\n setShowBillingPicker(false);\n setLinkBillingError(null);\n }}\n className=\"rounded-md px-3 py-1.5 text-xs font-medium text-text-secondary hover:bg-accent transition-colors\"\n >\n {tr(\"devportal.cancel\", \"Cancel\")}\n </button>\n <button\n type=\"button\"\n disabled={linkingBilling || !selectedBillingAccountId}\n onClick={() => void handleLinkExisting()}\n className=\"inline-flex items-center gap-1.5 rounded-md bg-action-primary-bg px-3 py-1.5 text-xs font-medium text-action-primary-text hover:bg-action-primary-bg/90 transition-colors disabled:opacity-50\"\n >\n {linkingBilling ? (\n <Loader2 size={13} className=\"animate-spin\" />\n ) : (\n <Link2 size={13} />\n )}\n {tr(\"devportal.revenue.confirm\", \"Confirm\")}\n </button>\n </div>\n {linkBillingError && (\n <p className=\"text-xs text-status-error-text\">\n {linkBillingError}\n </p>\n )}\n </div>\n )}\n </div>\n )}\n\n {loading && !summary ? (\n <div className=\"flex items-center justify-center py-16 text-text-secondary\">\n <Loader2 className=\"h-8 w-8 animate-spin\" />\n </div>\n ) : summary ? (\n <>\n <EmphasisPanel className=\"mb-6\">\n <StatCardGrid columns={4}>\n <StatCard\n title={tr(\"devportal.revenue.grossRevenue\", \"Gross Revenue\")}\n value={`$${summary.totalGrossRevenue.toLocaleString()}`}\n icon={<DollarSign size={20} />}\n />\n <StatCard\n title={tr(\"devportal.revenue.netRevenue\", \"Net Revenue\")}\n value={`$${summary.totalNetRevenue.toLocaleString()}`}\n icon={<Wallet size={20} />}\n />\n <StatCard\n title={tr(\n \"devportal.revenue.pendingBalance\",\n \"Pending Balance\",\n )}\n value={`$${summary.pendingBalance.toLocaleString()}`}\n icon={<CreditCard size={20} />}\n description={\n summary.nextPayoutDate\n ? `${tr(\"devportal.revenue.nextPayout\", \"Next payout\")} ${new Date(summary.nextPayoutDate).toLocaleDateString()}`\n : tr(\n \"devportal.revenue.noPayoutScheduled\",\n \"No payout scheduled\",\n )\n }\n />\n <StatCard\n title={tr(\"devportal.revenue.platformFees\", \"Platform Fees\")}\n value={`$${summary.totalPlatformFees.toLocaleString()}`}\n icon={<ArrowDownToLine size={20} />}\n description={`${summary.totalRefundsCount} ${tr(\"devportal.revenue.refunds\", \"refunds\")}`}\n />\n </StatCardGrid>\n </EmphasisPanel>\n\n <div className=\"grid gap-6 xl:grid-cols-2\">\n <PageSection\n title={tr(\"devportal.revenue.revenueTrend\", \"Revenue trend\")}\n >\n <GlassCard treatment=\"insight\" className=\"p-4\">\n {chartData.length > 0 ? (\n <ResponsiveContainer width=\"100%\" height={300}>\n <LineChart data={chartData}>\n <CartesianGrid\n strokeDasharray=\"3 3\"\n stroke=\"var(--color-chart-grid)\"\n />\n <XAxis\n dataKey=\"date\"\n stroke=\"var(--color-chart-axis)\"\n fontSize={12}\n />\n <YAxis stroke=\"var(--color-chart-axis)\" fontSize={12} />\n <Tooltip\n contentStyle={{\n backgroundColor: \"var(--color-card)\",\n border: \"1px solid var(--color-border-default)\",\n borderRadius: 12,\n }}\n />\n <Line\n type=\"monotone\"\n dataKey=\"grossRevenue\"\n stroke={chartColors.primary}\n strokeWidth={2}\n dot={false}\n />\n <Line\n type=\"monotone\"\n dataKey=\"netRevenue\"\n stroke={chartColors.secondary}\n strokeWidth={2}\n dot={false}\n />\n </LineChart>\n </ResponsiveContainer>\n ) : (\n <div className=\"flex h-[300px] flex-col items-center justify-center gap-2 text-text-secondary\">\n <p className=\"text-sm font-medium\">\n {tr(\"devportal.revenue.noTrendData\", \"No trend data yet\")}\n </p>\n <p className=\"text-xs text-text-tertiary\">\n {tr(\n \"devportal.revenue.noTrendDataHint\",\n \"Revenue trend appears once daily analytics have been aggregated.\",\n )}\n </p>\n </div>\n )}\n </GlassCard>\n </PageSection>\n\n <PageSection\n title={tr(\n \"devportal.revenue.topApps\",\n \"Top applications by revenue\",\n )}\n >\n {summary.topApps.length > 0 ? (\n <div className=\"space-y-2\">\n {summary.topApps.map(\n (\n application: GetRevenueDashboardDataQuery[\"developerOrgRevenueSummary\"][\"topApps\"][number],\n ) => (\n <GlassCard\n key={application.applicationId}\n treatment=\"insight\"\n className=\"border-border-seam p-4\"\n >\n <div className=\"flex items-start justify-between gap-3\">\n <div>\n <p className=\"font-medium\">\n {application.applicationName}\n </p>\n <p className=\"text-sm text-text-secondary\">\n {application.salesCount}{\" \"}\n {tr(\"devportal.revenue.sales\", \"sales\")}\n </p>\n </div>\n <div className=\"text-right\">\n <p className=\"font-semibold\">\n ${application.grossRevenue.toLocaleString()}\n </p>\n <p className=\"text-sm text-text-secondary\">\n {tr(\"devportal.revenue.net\", \"Net\")} $\n {application.netRevenue.toLocaleString()}\n </p>\n </div>\n </div>\n </GlassCard>\n ),\n )}\n </div>\n ) : (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Building2 size={24} />}\n title={tr(\n \"devportal.revenue.noLeadersTitle\",\n \"No revenue leaders yet\",\n )}\n description={tr(\n \"devportal.revenue.noLeadersDescription\",\n \"Top-performing applications will appear here once revenue is recorded.\",\n )}\n />\n )}\n </PageSection>\n </div>\n\n <PageSection\n title={tr(\"devportal.revenue.salesVolume\", \"Sales volume\")}\n >\n <GlassCard treatment=\"insight\" className=\"p-4\">\n {chartData.length > 0 ? (\n <ResponsiveContainer width=\"100%\" height={280}>\n <BarChart data={chartData}>\n <CartesianGrid\n strokeDasharray=\"3 3\"\n stroke=\"var(--color-chart-grid)\"\n />\n <XAxis\n dataKey=\"date\"\n stroke=\"var(--color-chart-axis)\"\n fontSize={12}\n />\n <YAxis stroke=\"var(--color-chart-axis)\" fontSize={12} />\n <Tooltip\n contentStyle={{\n backgroundColor: \"var(--color-card)\",\n border: \"1px solid var(--color-border-default)\",\n borderRadius: 12,\n }}\n />\n <Bar\n dataKey=\"salesCount\"\n fill={chartColors.tertiary}\n radius={[6, 6, 0, 0]}\n />\n </BarChart>\n </ResponsiveContainer>\n ) : (\n <div className=\"flex h-[280px] flex-col items-center justify-center gap-2 text-text-secondary\">\n <p className=\"text-sm font-medium\">\n {tr(\n \"devportal.revenue.noVolumeData\",\n \"No sales volume data yet\",\n )}\n </p>\n <p className=\"text-xs text-text-tertiary\">\n {tr(\n \"devportal.revenue.noVolumeDataHint\",\n \"Sales volume by period appears once analytics have been aggregated.\",\n )}\n </p>\n </div>\n )}\n </GlassCard>\n </PageSection>\n\n <div className=\"grid gap-6 xl:grid-cols-2\">\n <PageSection\n title={tr(\"devportal.revenue.recentSales\", \"Recent sales\")}\n >\n <DataTable\n data={sales}\n keyField=\"id\"\n columns={[\n {\n key: \"application\",\n header: tr(\"devportal.revenue.application\", \"Application\"),\n render: (sale) =>\n sale.application?.name ||\n sale.planName ||\n tr(\"devportal.common.unknown\", \"Unknown\"),\n },\n {\n key: \"grossAmount\",\n header: tr(\"devportal.revenue.gross\", \"Gross\"),\n render: (sale) => `$${sale.grossAmount.toLocaleString()}`,\n },\n {\n key: \"netAmount\",\n header: tr(\"devportal.revenue.net\", \"Net\"),\n render: (sale) => `$${sale.netAmount.toLocaleString()}`,\n },\n {\n key: \"status\",\n header: tr(\"devportal.revenue.status\", \"Status\"),\n render: (sale) => (\n <StatusBadge status={sale.status} type=\"app\" />\n ),\n },\n ]}\n />\n </PageSection>\n\n <PageSection\n title={tr(\"devportal.revenue.payoutHistory\", \"Payout history\")}\n >\n <DataTable\n data={payouts}\n keyField=\"id\"\n columns={[\n {\n key: \"periodLabel\",\n header: tr(\"devportal.revenue.period\", \"Period\"),\n render: (payout) => payout.periodLabel,\n },\n {\n key: \"netAmount\",\n header: tr(\"devportal.revenue.netPayout\", \"Net payout\"),\n render: (payout) => `$${payout.netAmount.toLocaleString()}`,\n },\n {\n key: \"status\",\n header: tr(\"devportal.revenue.status\", \"Status\"),\n render: (payout) => (\n <StatusBadge status={payout.status} type=\"app\" />\n ),\n },\n ]}\n />\n </PageSection>\n </div>\n </>\n ) : (\n <EmptyState\n illustration=\"empty-data\"\n icon={<DollarSign size={32} />}\n title={tr(\"devportal.revenue.noDataTitle\", \"No revenue data yet\")}\n description={tr(\n \"devportal.revenue.noDataDescription\",\n \"Revenue metrics will appear after this organization starts recording marketplace sales.\",\n )}\n />\n )}\n </PageLayout>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"RevenueDashboardPage.js","names":[],"sources":["../../../src/devportal/pages/RevenueDashboardPage.tsx"],"sourcesContent":["import type { FC } from \"react\";\nimport { useMemo, useState } from \"react\";\nimport { useSearchParams } from \"react-router\";\nimport {\n Bar,\n BarChart,\n CartesianGrid,\n Line,\n LineChart,\n ResponsiveContainer,\n Tooltip,\n XAxis,\n YAxis,\n} from \"recharts\";\nimport {\n AlertCircle,\n ArrowDownToLine,\n Building2,\n Check,\n CreditCard,\n DollarSign,\n Link2,\n Loader2,\n Wallet,\n} from \"lucide-react\";\nimport { EmphasisPanel, GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport { useDevportal } from \"../providers/DevportalProvider\";\nimport { useIsSellerSection } from \"../hooks/useIsSellerSection\";\nimport { PageLayout, PageSection } from \"../components/PageLayout\";\nimport { DataTable } from \"../components/DataTable\";\nimport { EmptyState } from \"../components/EmptyState\";\nimport { StatCard, StatCardGrid } from \"../components/StatCard\";\nimport { StatusBadge } from \"../components/StatusBadge\";\nimport type { DeveloperOrganization } from \"../types\";\nimport {\n type GetMyOrganizationsQuery,\n type GetRevenueDashboardDataQuery,\n getErrorMessage,\n useGetBillingAccountsByOrgQuery,\n useGetMyOrganizationsQuery,\n useGetRevenueDashboardDataQuery,\n useLinkDevOrgBillingAccountMutation,\n} from \"../hooks\";\nimport { cn } from \"../utils/cn\";\nimport { useTr } from \"../utils/i18n\";\n\ntype TimeRange = \"30d\" | \"90d\" | \"12m\";\n\nconst chartColors = {\n primary: \"var(--color-chart-1)\",\n secondary: \"var(--color-chart-2)\",\n tertiary: \"var(--color-chart-4)\",\n};\n\ntype RevenueSale =\n GetRevenueDashboardDataQuery[\"developerOrgSales\"][\"edges\"][number][\"node\"];\ntype RevenuePayout =\n GetRevenueDashboardDataQuery[\"developerOrgPayouts\"][\"edges\"][number][\"node\"];\n\nconst daysByRange: Record<TimeRange, number> = {\n \"30d\": 30,\n \"90d\": 90,\n \"12m\": 365,\n};\n\nexport const RevenueDashboardPage: FC = () => {\n const tr = useTr();\n const {\n activeOrgId: activeDigitalOrgId,\n activePhysicalOrgId,\n tenantId,\n } = useDevportal();\n const isSeller = useIsSellerSection();\n // Shared page for both kinds — reads whichever active-org slot matches the\n // section it's reached from (seller portal vs regular devportal). Every\n // existing `activeOrgId` reference below keeps working unchanged. See\n // DeveloperOrgKind (BOFF-7285).\n const activeOrgId = isSeller ? activePhysicalOrgId : activeDigitalOrgId;\n const [timeRange, setTimeRange] = useState<TimeRange>(\"90d\");\n const [linkingBilling, setLinkingBilling] = useState(false);\n const [linkBillingError, setLinkBillingError] = useState<string | null>(null);\n\n const [linkBillingAccount] = useLinkDevOrgBillingAccountMutation();\n const [showBillingPicker, setShowBillingPicker] = useState(false);\n const [selectedBillingAccountId, setSelectedBillingAccountId] = useState<\n string | null\n >(null);\n const [searchParams] = useSearchParams();\n const now = useMemo(() => new Date(), []);\n\n const { data: billingAccountsData, loading: billingAccountsLoading } =\n useGetBillingAccountsByOrgQuery({\n fetchPolicy: \"cache-and-network\",\n context: { headers: { \"x-org-id\": tenantId ?? \"\" } },\n });\n // searchParams kept for deep-link compatibility (URL ?org= still works via OrgSwitcher auto-select)\n void searchParams;\n const startDate = useMemo(() => {\n const nextDate = new Date(now);\n nextDate.setDate(nextDate.getDate() - daysByRange[timeRange]);\n return nextDate.toISOString();\n }, [now, timeRange]);\n\n const {\n data: organizationsData,\n loading: organizationsLoading,\n error: organizationsError,\n } = useGetMyOrganizationsQuery({\n fetchPolicy: \"cache-and-network\",\n });\n const { data, loading, error } = useGetRevenueDashboardDataQuery({\n variables: {\n organizationId: activeOrgId ?? \"\",\n period: \"monthly\",\n startDate,\n endDate: now.toISOString(),\n salesLimit: 10,\n salesOffset: 0,\n payoutsLimit: 10,\n payoutsOffset: 0,\n },\n skip: !activeOrgId,\n fetchPolicy: \"cache-and-network\",\n });\n\n const organizations: DeveloperOrganization[] = useMemo(\n () =>\n organizationsData?.myDeveloperOrganizations?.map(\n (\n organization: GetMyOrganizationsQuery[\"myDeveloperOrganizations\"][number],\n ) => organization as unknown as DeveloperOrganization,\n ) ?? [],\n [organizationsData?.myDeveloperOrganizations],\n );\n\n const selectedOrganization = organizations.find(\n (organization) => organization.id === activeOrgId,\n );\n\n type BillingAccount = NonNullable<\n typeof billingAccountsData\n >[\"getBillingAccountsByOrg\"][number];\n const existingBillingAccounts: BillingAccount[] =\n billingAccountsData?.getBillingAccountsByOrg ?? [];\n\n const linkedBillingAccount =\n existingBillingAccounts.find(\n (a) => a.id === selectedOrganization?.billingAccountId,\n ) ?? null;\n\n const handleOpenBillingPicker = () => {\n // Pre-select the currently linked account; fall back to default or first\n setSelectedBillingAccountId(\n selectedOrganization?.billingAccountId ??\n existingBillingAccounts.find((a) => a.isDefault)?.id ??\n existingBillingAccounts[0]?.id ??\n null,\n );\n setShowBillingPicker(true);\n };\n\n const handleLinkExisting = async () => {\n if (!activeOrgId || !selectedBillingAccountId) return;\n setLinkingBilling(true);\n setLinkBillingError(null);\n try {\n await linkBillingAccount({\n variables: {\n organizationId: activeOrgId,\n billingAccountId: selectedBillingAccountId,\n },\n refetchQueries: [\"GetMyOrganizations\"],\n });\n setShowBillingPicker(false);\n } catch (err) {\n setLinkBillingError(\n err instanceof Error ? err.message : \"Failed to link billing account\",\n );\n } finally {\n setLinkingBilling(false);\n }\n };\n\n const summary = data?.developerOrgRevenueSummary;\n const analytics = [...(data?.developerOrgRevenueAnalytics ?? [])].sort(\n (left, right) =>\n new Date(left.date).getTime() - new Date(right.date).getTime(),\n );\n const sales: RevenueSale[] =\n data?.developerOrgSales?.edges?.map(\n (\n edge: GetRevenueDashboardDataQuery[\"developerOrgSales\"][\"edges\"][number],\n ) => edge.node,\n ) ?? [];\n const payouts: RevenuePayout[] =\n data?.developerOrgPayouts?.edges?.map(\n (\n edge: GetRevenueDashboardDataQuery[\"developerOrgPayouts\"][\"edges\"][number],\n ) => edge.node,\n ) ?? [];\n\n const chartData = analytics.map((entry) => ({\n date: new Date(entry.date).toLocaleDateString(undefined, {\n month: \"short\",\n day: \"numeric\",\n }),\n grossRevenue: entry.grossRevenue,\n netRevenue: entry.netRevenue,\n platformFees: entry.platformFees,\n refunds: entry.refunds,\n salesCount: entry.salesCount,\n }));\n\n if (organizationsLoading && !organizationsData) {\n return (\n <PageLayout\n title={tr(\"devportal.revenue.title\", \"Revenue\")}\n description={tr(\n \"devportal.revenue.description\",\n \"Track real revenue, sales, and payout activity for your developer organizations\",\n )}\n >\n <div className=\"flex items-center justify-center py-16 text-text-secondary\">\n <Loader2 className=\"h-8 w-8 animate-spin\" />\n </div>\n </PageLayout>\n );\n }\n\n if (organizationsError) {\n return (\n <PageLayout\n title={tr(\"devportal.revenue.title\", \"Revenue\")}\n description={tr(\n \"devportal.revenue.description\",\n \"Track real revenue, sales, and payout activity for your developer organizations\",\n )}\n >\n <EmptyState\n icon={<AlertCircle size={32} className=\"text-status-error-text\" />}\n title={tr(\n \"devportal.revenue.errorLoadOrgs\",\n \"Unable to load organizations\",\n )}\n description={getErrorMessage(organizationsError)}\n />\n </PageLayout>\n );\n }\n\n if (!activeOrgId) {\n return (\n <PageLayout\n title={tr(\"devportal.revenue.title\", \"Revenue\")}\n description={tr(\n \"devportal.revenue.description\",\n \"Track real revenue, sales, and payout activity for your developer organizations\",\n )}\n >\n <EmptyState\n illustration=\"empty-people\"\n icon={<Building2 size={32} className=\"text-text-secondary\" />}\n title={tr(\n \"devportal.revenue.selectOrgTitle\",\n \"Select an organization\",\n )}\n description={tr(\n \"devportal.revenue.selectOrgHint\",\n \"Use the organization switcher in the nav bar above to choose which organization's revenue to view.\",\n )}\n />\n </PageLayout>\n );\n }\n\n if (error) {\n return (\n <PageLayout\n title={tr(\"devportal.revenue.title\", \"Revenue\")}\n description={tr(\n \"devportal.revenue.description\",\n \"Track real revenue, sales, and payout activity for your developer organizations\",\n )}\n >\n <EmptyState\n icon={<AlertCircle size={32} className=\"text-status-error-text\" />}\n title={tr(\n \"devportal.revenue.errorLoadData\",\n \"Unable to load revenue data\",\n )}\n description={getErrorMessage(error)}\n />\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title={tr(\"devportal.revenue.title\", \"Revenue\")}\n description={\n selectedOrganization\n ? `${tr(\"devportal.revenue.financialPerformanceFor\", \"Financial performance for\")} ${selectedOrganization.name}`\n : tr(\n \"devportal.revenue.shortDescription\",\n \"Track revenue, sales, and payouts\",\n )\n }\n purpose={tr(\n \"devportal.revenue.purpose\",\n \"Revenue tracks the money your paid apps earn — gross sales, platform fees, and what gets paid out to your organization. Link a billing account, then use this to reconcile payouts and watch growth across time ranges.\",\n )}\n actions={\n <div className=\"flex flex-wrap gap-2\">\n <select\n value={timeRange}\n onChange={(event) => setTimeRange(event.target.value as TimeRange)}\n aria-label={tr(\n \"devportal.revenue.selectRangeAria\",\n \"Select time range\",\n )}\n className=\"rounded-md border bg-bg-surface px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary\"\n >\n <option value=\"30d\">\n {tr(\"devportal.revenue.range.30d\", \"Last 30 days\")}\n </option>\n <option value=\"90d\">\n {tr(\"devportal.revenue.range.90d\", \"Last 90 days\")}\n </option>\n <option value=\"12m\">\n {tr(\"devportal.revenue.range.12m\", \"Last 12 months\")}\n </option>\n </select>\n </div>\n }\n >\n {/* Billing account link banner for orgs not yet linked */}\n {selectedOrganization && !selectedOrganization.billingAccountId && (\n <div className=\"mb-6 rounded-card border border-border-seam bg-status-info-bg-subtle p-4 flex flex-col gap-3\">\n <div className=\"flex items-start gap-3\">\n <Link2\n size={20}\n className=\"text-accent-blue flex-shrink-0 mt-0.5\"\n />\n <div className=\"flex-1 min-w-0\">\n <p className=\"text-sm font-medium text-status-info-text\">\n {tr(\n \"devportal.revenue.billingNotLinkedTitle\",\n \"Link a billing account to receive store revenue\",\n )}\n </p>\n <p className=\"mt-1 text-sm text-status-info-text\">\n {tr(\n \"devportal.revenue.billingNotLinkedDesc\",\n \"Once linked, net earnings from app sales will appear in your billing credits — you can spend them on platform services or withdraw to your bank.\",\n )}\n </p>\n {linkBillingError && (\n <p className=\"mt-2 text-sm text-status-error-text\">\n {linkBillingError}\n </p>\n )}\n </div>\n <button\n type=\"button\"\n onClick={handleOpenBillingPicker}\n disabled={linkingBilling || billingAccountsLoading}\n className=\"inline-flex items-center gap-2 rounded-md bg-action-primary-bg px-3 py-1.5 text-xs font-medium text-action-primary-text hover:bg-action-primary-bg/90 transition-colors disabled:opacity-50 flex-shrink-0\"\n >\n {linkingBilling || billingAccountsLoading ? (\n <Loader2 size={14} className=\"animate-spin\" />\n ) : (\n <Link2 size={14} />\n )}\n {linkingBilling\n ? tr(\"devportal.revenue.linking\", \"Linking...\")\n : tr(\n \"devportal.revenue.linkBillingAccount\",\n \"Link Billing Account\",\n )}\n </button>\n </div>\n\n {/* No billing accounts message */}\n {showBillingPicker && existingBillingAccounts.length === 0 && (\n <div className=\"rounded-lg border border-border-subtle bg-bg-surface p-4 ml-8 text-sm text-text-secondary\">\n {tr(\n \"devportal.revenue.noBillingAccounts\",\n \"You don't have any billing accounts yet. Create one from your account billing settings first.\",\n )}\n </div>\n )}\n\n {/* Billing account picker — shown when user has existing accounts */}\n {showBillingPicker && existingBillingAccounts.length > 0 && (\n <div className=\"rounded-lg border border-border-subtle bg-bg-surface p-4 ml-8 space-y-3\">\n <p className=\"text-sm font-medium\">\n {tr(\n \"devportal.revenue.chooseBillingAccount\",\n \"Choose a billing account to link\",\n )}\n </p>\n <div className=\"space-y-2\">\n {existingBillingAccounts.map((account) => (\n <button\n key={account.id}\n type=\"button\"\n onClick={() =>\n setSelectedBillingAccountId(account.id ?? null)\n }\n className={cn(\n \"flex w-full items-center gap-3 rounded-md border px-3 py-2.5 text-sm text-left transition-colors\",\n selectedBillingAccountId === account.id\n ? \"border-primary bg-action-primary-bg/5 text-text-primary\"\n : \"border-border-subtle hover:bg-accent text-text-primary\",\n )}\n >\n <Check\n size={14}\n className={cn(\n \"flex-shrink-0\",\n selectedBillingAccountId === account.id\n ? \"opacity-100 text-primary\"\n : \"opacity-0\",\n )}\n />\n <div className=\"flex-1 min-w-0\">\n <p className=\"font-medium truncate\">{account.name}</p>\n {account.isDefault && (\n <p className=\"text-xs text-text-secondary\">\n {tr(\"devportal.revenue.defaultAccount\", \"Default\")}\n </p>\n )}\n </div>\n </button>\n ))}\n </div>\n <div className=\"flex items-center justify-end gap-2 pt-1\">\n <button\n type=\"button\"\n onClick={() => {\n setShowBillingPicker(false);\n setLinkBillingError(null);\n }}\n className=\"rounded-md px-3 py-1.5 text-xs font-medium text-text-secondary hover:bg-accent transition-colors\"\n >\n {tr(\"devportal.cancel\", \"Cancel\")}\n </button>\n <button\n type=\"button\"\n disabled={linkingBilling || !selectedBillingAccountId}\n onClick={() => void handleLinkExisting()}\n className=\"inline-flex items-center gap-1.5 rounded-md bg-action-primary-bg px-3 py-1.5 text-xs font-medium text-action-primary-text hover:bg-action-primary-bg/90 transition-colors disabled:opacity-50\"\n >\n {linkingBilling ? (\n <Loader2 size={13} className=\"animate-spin\" />\n ) : (\n <Link2 size={13} />\n )}\n {tr(\"devportal.revenue.confirm\", \"Confirm\")}\n </button>\n </div>\n </div>\n )}\n </div>\n )}\n\n {/* Linked billing account info — shown when billing is already linked */}\n {selectedOrganization?.billingAccountId && (\n <div className=\"mb-6 rounded-lg border border-border-subtle bg-bg-surface p-3 flex flex-col gap-3\">\n <div className=\"flex items-center gap-3\">\n <CreditCard\n size={16}\n className=\"text-text-secondary flex-shrink-0\"\n />\n <div className=\"flex-1 min-w-0\">\n <p className=\"text-sm font-medium truncate\">\n {linkedBillingAccount?.name ??\n tr(\"devportal.revenue.billingAccount\", \"Billing account\")}\n </p>\n <p className=\"text-xs text-text-secondary truncate\">\n {linkedBillingAccount\n ? `${linkedBillingAccount.currency}${linkedBillingAccount.email ? ` · ${linkedBillingAccount.email}` : \"\"}`\n : selectedOrganization.billingAccountId}\n </p>\n </div>\n {!showBillingPicker && (\n <button\n type=\"button\"\n onClick={handleOpenBillingPicker}\n disabled={billingAccountsLoading}\n className=\"text-xs text-text-secondary hover:text-text-primary transition-colors flex-shrink-0 disabled:opacity-50\"\n >\n {tr(\"devportal.revenue.changeBillingAccount\", \"Change\")}\n </button>\n )}\n </div>\n {/* Picker for changing the linked account */}\n {showBillingPicker && existingBillingAccounts.length > 0 && (\n <div className=\"rounded-lg border border-border-subtle bg-bg-surface p-4 space-y-3\">\n <p className=\"text-sm font-medium\">\n {tr(\n \"devportal.revenue.chooseBillingAccount\",\n \"Choose a billing account to link\",\n )}\n </p>\n <div className=\"space-y-2\">\n {existingBillingAccounts.map((account) => (\n <button\n key={account.id}\n type=\"button\"\n onClick={() =>\n setSelectedBillingAccountId(account.id ?? null)\n }\n className={cn(\n \"flex w-full items-center gap-3 rounded-md border px-3 py-2.5 text-sm text-left transition-colors\",\n selectedBillingAccountId === account.id\n ? \"border-primary bg-action-primary-bg/5 text-text-primary\"\n : \"border-border-subtle hover:bg-accent text-text-primary\",\n )}\n >\n <Check\n size={14}\n className={cn(\n \"flex-shrink-0\",\n selectedBillingAccountId === account.id\n ? \"opacity-100 text-primary\"\n : \"opacity-0\",\n )}\n />\n <div className=\"flex-1 min-w-0\">\n <p className=\"font-medium truncate\">{account.name}</p>\n {account.isDefault && (\n <p className=\"text-xs text-text-secondary\">\n {tr(\"devportal.revenue.defaultAccount\", \"Default\")}\n </p>\n )}\n </div>\n </button>\n ))}\n </div>\n <div className=\"flex items-center justify-end gap-2 pt-1\">\n <button\n type=\"button\"\n onClick={() => {\n setShowBillingPicker(false);\n setLinkBillingError(null);\n }}\n className=\"rounded-md px-3 py-1.5 text-xs font-medium text-text-secondary hover:bg-accent transition-colors\"\n >\n {tr(\"devportal.cancel\", \"Cancel\")}\n </button>\n <button\n type=\"button\"\n disabled={linkingBilling || !selectedBillingAccountId}\n onClick={() => void handleLinkExisting()}\n className=\"inline-flex items-center gap-1.5 rounded-md bg-action-primary-bg px-3 py-1.5 text-xs font-medium text-action-primary-text hover:bg-action-primary-bg/90 transition-colors disabled:opacity-50\"\n >\n {linkingBilling ? (\n <Loader2 size={13} className=\"animate-spin\" />\n ) : (\n <Link2 size={13} />\n )}\n {tr(\"devportal.revenue.confirm\", \"Confirm\")}\n </button>\n </div>\n {linkBillingError && (\n <p className=\"text-xs text-status-error-text\">\n {linkBillingError}\n </p>\n )}\n </div>\n )}\n </div>\n )}\n\n {loading && !summary ? (\n <div className=\"flex items-center justify-center py-16 text-text-secondary\">\n <Loader2 className=\"h-8 w-8 animate-spin\" />\n </div>\n ) : summary ? (\n <>\n <EmphasisPanel className=\"mb-6\">\n <StatCardGrid columns={4}>\n <StatCard\n title={tr(\"devportal.revenue.grossRevenue\", \"Gross Revenue\")}\n value={`$${summary.totalGrossRevenue.toLocaleString()}`}\n icon={<DollarSign size={20} />}\n />\n <StatCard\n title={tr(\"devportal.revenue.netRevenue\", \"Net Revenue\")}\n value={`$${summary.totalNetRevenue.toLocaleString()}`}\n icon={<Wallet size={20} />}\n />\n <StatCard\n title={tr(\n \"devportal.revenue.pendingBalance\",\n \"Pending Balance\",\n )}\n value={`$${summary.pendingBalance.toLocaleString()}`}\n icon={<CreditCard size={20} />}\n description={\n summary.nextPayoutDate\n ? `${tr(\"devportal.revenue.nextPayout\", \"Next payout\")} ${new Date(summary.nextPayoutDate).toLocaleDateString()}`\n : tr(\n \"devportal.revenue.noPayoutScheduled\",\n \"No payout scheduled\",\n )\n }\n />\n <StatCard\n title={tr(\"devportal.revenue.platformFees\", \"Platform Fees\")}\n value={`$${summary.totalPlatformFees.toLocaleString()}`}\n icon={<ArrowDownToLine size={20} />}\n description={`${summary.totalRefundsCount} ${tr(\"devportal.revenue.refunds\", \"refunds\")}`}\n />\n </StatCardGrid>\n </EmphasisPanel>\n\n <div className=\"grid gap-6 xl:grid-cols-2\">\n <PageSection\n title={tr(\"devportal.revenue.revenueTrend\", \"Revenue trend\")}\n >\n <GlassCard treatment=\"insight\" className=\"p-4\">\n {chartData.length > 0 ? (\n <ResponsiveContainer width=\"100%\" height={300}>\n <LineChart data={chartData}>\n <CartesianGrid\n strokeDasharray=\"3 3\"\n stroke=\"var(--color-chart-grid)\"\n />\n <XAxis\n dataKey=\"date\"\n stroke=\"var(--color-chart-axis)\"\n fontSize={12}\n />\n <YAxis stroke=\"var(--color-chart-axis)\" fontSize={12} />\n <Tooltip\n contentStyle={{\n backgroundColor: \"var(--color-card)\",\n border: \"1px solid var(--color-border-default)\",\n borderRadius: 12,\n }}\n />\n <Line\n type=\"monotone\"\n dataKey=\"grossRevenue\"\n stroke={chartColors.primary}\n strokeWidth={2}\n dot={false}\n />\n <Line\n type=\"monotone\"\n dataKey=\"netRevenue\"\n stroke={chartColors.secondary}\n strokeWidth={2}\n dot={false}\n />\n </LineChart>\n </ResponsiveContainer>\n ) : (\n <div className=\"flex h-[300px] flex-col items-center justify-center gap-2 text-text-secondary\">\n <p className=\"text-sm font-medium\">\n {tr(\"devportal.revenue.noTrendData\", \"No trend data yet\")}\n </p>\n <p className=\"text-xs text-text-tertiary\">\n {tr(\n \"devportal.revenue.noTrendDataHint\",\n \"Revenue trend appears once daily analytics have been aggregated.\",\n )}\n </p>\n </div>\n )}\n </GlassCard>\n </PageSection>\n\n <PageSection\n title={tr(\n \"devportal.revenue.topApps\",\n \"Top applications by revenue\",\n )}\n >\n {summary.topApps.length > 0 ? (\n <div className=\"space-y-2\">\n {summary.topApps.map(\n (\n application: GetRevenueDashboardDataQuery[\"developerOrgRevenueSummary\"][\"topApps\"][number],\n ) => (\n <GlassCard\n key={application.applicationId}\n treatment=\"insight\"\n className=\"border-border-seam p-4\"\n >\n <div className=\"flex items-start justify-between gap-3\">\n <div>\n <p className=\"font-medium\">\n {application.applicationName}\n </p>\n <p className=\"text-sm text-text-secondary\">\n {application.salesCount}{\" \"}\n {tr(\"devportal.revenue.sales\", \"sales\")}\n </p>\n </div>\n <div className=\"text-right\">\n <p className=\"font-semibold\">\n ${application.grossRevenue.toLocaleString()}\n </p>\n <p className=\"text-sm text-text-secondary\">\n {tr(\"devportal.revenue.net\", \"Net\")} $\n {application.netRevenue.toLocaleString()}\n </p>\n </div>\n </div>\n </GlassCard>\n ),\n )}\n </div>\n ) : (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Building2 size={24} />}\n title={tr(\n \"devportal.revenue.noLeadersTitle\",\n \"No revenue leaders yet\",\n )}\n description={tr(\n \"devportal.revenue.noLeadersDescription\",\n \"Top-performing applications will appear here once revenue is recorded.\",\n )}\n />\n )}\n </PageSection>\n </div>\n\n <PageSection\n title={tr(\"devportal.revenue.salesVolume\", \"Sales volume\")}\n >\n <GlassCard treatment=\"insight\" className=\"p-4\">\n {chartData.length > 0 ? (\n <ResponsiveContainer width=\"100%\" height={280}>\n <BarChart data={chartData}>\n <CartesianGrid\n strokeDasharray=\"3 3\"\n stroke=\"var(--color-chart-grid)\"\n />\n <XAxis\n dataKey=\"date\"\n stroke=\"var(--color-chart-axis)\"\n fontSize={12}\n />\n <YAxis stroke=\"var(--color-chart-axis)\" fontSize={12} />\n <Tooltip\n contentStyle={{\n backgroundColor: \"var(--color-card)\",\n border: \"1px solid var(--color-border-default)\",\n borderRadius: 12,\n }}\n />\n <Bar\n dataKey=\"salesCount\"\n fill={chartColors.tertiary}\n radius={[6, 6, 0, 0]}\n />\n </BarChart>\n </ResponsiveContainer>\n ) : (\n <div className=\"flex h-[280px] flex-col items-center justify-center gap-2 text-text-secondary\">\n <p className=\"text-sm font-medium\">\n {tr(\n \"devportal.revenue.noVolumeData\",\n \"No sales volume data yet\",\n )}\n </p>\n <p className=\"text-xs text-text-tertiary\">\n {tr(\n \"devportal.revenue.noVolumeDataHint\",\n \"Sales volume by period appears once analytics have been aggregated.\",\n )}\n </p>\n </div>\n )}\n </GlassCard>\n </PageSection>\n\n <div className=\"grid gap-6 xl:grid-cols-2\">\n <PageSection\n title={tr(\"devportal.revenue.recentSales\", \"Recent sales\")}\n >\n <DataTable\n data={sales}\n keyField=\"id\"\n columns={[\n {\n key: \"application\",\n header: tr(\"devportal.revenue.application\", \"Application\"),\n render: (sale) =>\n sale.application?.name ||\n sale.planName ||\n tr(\"devportal.common.unknown\", \"Unknown\"),\n },\n {\n key: \"grossAmount\",\n header: tr(\"devportal.revenue.gross\", \"Gross\"),\n render: (sale) => `$${sale.grossAmount.toLocaleString()}`,\n },\n {\n key: \"netAmount\",\n header: tr(\"devportal.revenue.net\", \"Net\"),\n render: (sale) => `$${sale.netAmount.toLocaleString()}`,\n },\n {\n key: \"status\",\n header: tr(\"devportal.revenue.status\", \"Status\"),\n render: (sale) => (\n <StatusBadge status={sale.status} type=\"app\" />\n ),\n },\n ]}\n />\n </PageSection>\n\n <PageSection\n title={tr(\"devportal.revenue.payoutHistory\", \"Payout history\")}\n >\n <DataTable\n data={payouts}\n keyField=\"id\"\n columns={[\n {\n key: \"periodLabel\",\n header: tr(\"devportal.revenue.period\", \"Period\"),\n render: (payout) => payout.periodLabel,\n },\n {\n key: \"netAmount\",\n header: tr(\"devportal.revenue.netPayout\", \"Net payout\"),\n render: (payout) => `$${payout.netAmount.toLocaleString()}`,\n },\n {\n key: \"status\",\n header: tr(\"devportal.revenue.status\", \"Status\"),\n render: (payout) => (\n <StatusBadge status={payout.status} type=\"app\" />\n ),\n },\n ]}\n />\n </PageSection>\n </div>\n </>\n ) : (\n <EmptyState\n illustration=\"empty-data\"\n icon={<DollarSign size={32} />}\n title={tr(\"devportal.revenue.noDataTitle\", \"No revenue data yet\")}\n description={tr(\n \"devportal.revenue.noDataDescription\",\n \"Revenue metrics will appear after this organization starts recording marketplace sales.\",\n )}\n />\n )}\n </PageLayout>\n );\n};\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SellerDashboardPage.d.ts","sourceRoot":"","sources":["../../../src/devportal/pages/SellerDashboardPage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAa,MAAM,OAAO,CAAC;AA2I3C,eAAO,MAAM,mBAAmB,EAAE,
|
|
1
|
+
{"version":3,"file":"SellerDashboardPage.d.ts","sourceRoot":"","sources":["../../../src/devportal/pages/SellerDashboardPage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAa,MAAM,OAAO,CAAC;AA2I3C,eAAO,MAAM,mBAAmB,EAAE,EAiRjC,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SellerDashboardPage.js","names":[],"sources":["../../../src/devportal/pages/SellerDashboardPage.tsx"],"sourcesContent":["import type { FC, ReactNode } from \"react\";\nimport { useEffect, useState } from \"react\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\nimport {\n Package,\n Building2,\n Plus,\n ArrowRight,\n Loader2,\n Download,\n DollarSign,\n Clock,\n CheckCircle2,\n} from \"lucide-react\";\nimport { GlassCard, GradientText } from \"@burdenoff/fe-libs/ui\";\nimport { PageLayout } from \"../components/PageLayout\";\nimport { EmptyState } from \"../components/EmptyState\";\nimport { useDevportal } from \"../providers/DevportalProvider\";\nimport { PermissionGuard } from \"../components\";\n\n// ---------------------------------------------------------------------------\n// Local types\n// ---------------------------------------------------------------------------\n\ntype ProductStatus =\n \"DRAFT\" | \"PENDING_REVIEW\" | \"PUBLISHED\" | \"REJECTED\" | \"INACTIVE\";\n\ninterface PhysicalProduct {\n id: string;\n name: string;\n slug: string;\n type: string;\n status: ProductStatus;\n price: number;\n currency: string;\n stockQuantity: number;\n icon?: string | null;\n}\n\ninterface OrgSummary {\n id: string;\n name: string;\n slug: string;\n logoUrl?: string | null;\n isVerified: boolean;\n publishedApps?: number | null;\n totalInstalls?: number | null;\n totalRevenue?: number | null;\n kind: \"DIGITAL\" | \"PHYSICAL\";\n}\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst STATUS_BADGE_STYLES: Record<\n ProductStatus,\n { label: string; className: string }\n> = {\n PUBLISHED: {\n label: \"Published\",\n className: \"text-status-success-text bg-status-success-bg-subtle\",\n },\n PENDING_REVIEW: {\n label: \"Pending Review\",\n className: \"text-status-warning-text bg-status-warning-bg-subtle\",\n },\n DRAFT: {\n label: \"Draft\",\n className: \"text-text-muted bg-bg-muted\",\n },\n REJECTED: {\n label: \"Rejected\",\n className: \"text-status-error-text bg-status-error-bg/10\",\n },\n INACTIVE: {\n label: \"Inactive\",\n className: \"text-text-muted bg-bg-muted\",\n },\n};\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction formatPrice(price: number, currency: string): string {\n const curr = currency || \"USD\";\n const formatted = new Intl.NumberFormat(\"en-US\", {\n style: \"currency\",\n currency: curr,\n minimumFractionDigits: 2,\n maximumFractionDigits: 2,\n }).format(price);\n return `${formatted} ${curr}`;\n}\n\n// ---------------------------------------------------------------------------\n// GraphQL query strings\n// ---------------------------------------------------------------------------\n\nconst BOOTSTRAP_QUERY = `\n query SellerDashboardBootstrap {\n myDeveloperOrganizations {\n id\n name\n slug\n logoUrl\n isVerified\n publishedApps\n totalInstalls\n totalRevenue\n kind\n }\n myPublisher {\n id\n }\n }\n`;\n\nconst SELLER_PRODUCTS_QUERY = `\n query SellerDashboardProducts($publisherId: String!) {\n products(filter: { publisherId: $publisherId, nature: PHYSICAL }, limit: 3, offset: 0) {\n id\n name\n slug\n type\n status\n price\n currency\n stockQuantity\n icon\n }\n }\n`;\n\n// ---------------------------------------------------------------------------\n// Page component\n// ---------------------------------------------------------------------------\n\nexport const SellerDashboardPage: FC = () => {\n const { navigate, basePath, apiGatewayUrl, authToken } = useDevportal();\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n\n const gqlUrl = apiGatewayUrl ?? \"/global/graphql\";\n\n const [bootstrapLoading, setBootstrapLoading] = useState(true);\n const [productsLoading, setProductsLoading] = useState(false);\n const [orgs, setOrgs] = useState<OrgSummary[]>([]);\n const [publisherId, setPublisherId] = useState<string | null>(null);\n const [products, setProducts] = useState<PhysicalProduct[]>([]);\n\n // Fetch orgs + publisher in one round-trip\n useEffect(() => {\n let cancelled = false;\n setBootstrapLoading(true);\n\n const run = async () => {\n try {\n const res = await fetch(gqlUrl, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${authToken ?? \"\"}`,\n },\n body: JSON.stringify({ query: BOOTSTRAP_QUERY }),\n });\n if (cancelled) return;\n const json = (await res.json()) as {\n data?: {\n myDeveloperOrganizations?: OrgSummary[];\n myPublisher?: { id: string } | null;\n };\n };\n if (cancelled) return;\n // Seller portal only ever shows PHYSICAL-kind orgs — a DIGITAL org\n // (regular app-publishing) has no business here. See DeveloperOrgKind\n // (BOFF-7285).\n setOrgs(\n (json.data?.myDeveloperOrganizations ?? []).filter(\n (org) => org.kind === \"PHYSICAL\",\n ),\n );\n setPublisherId(json.data?.myPublisher?.id ?? null);\n } finally {\n if (!cancelled) setBootstrapLoading(false);\n }\n };\n\n void run();\n return () => {\n cancelled = true;\n };\n }, [gqlUrl, authToken]);\n\n // Fetch recent physical products once publisher is known\n useEffect(() => {\n if (!publisherId) return;\n let cancelled = false;\n setProductsLoading(true);\n\n const run = async () => {\n try {\n const res = await fetch(gqlUrl, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${authToken ?? \"\"}`,\n },\n body: JSON.stringify({\n query: SELLER_PRODUCTS_QUERY,\n variables: { publisherId },\n }),\n });\n if (cancelled) return;\n const json = (await res.json()) as {\n data?: {\n products?: Array<\n Omit<PhysicalProduct, \"status\"> & { status: string }\n >;\n };\n };\n if (cancelled) return;\n const DIGITAL_TYPES = new Set([\"APP\", \"NODE\", \"WORKFLOW\"]);\n setProducts(\n (json.data?.products ?? [])\n .filter((p) => !DIGITAL_TYPES.has(p.type))\n .map((p) => ({\n ...p,\n status: p.status as ProductStatus,\n })),\n );\n } finally {\n if (!cancelled) setProductsLoading(false);\n }\n };\n\n void run();\n return () => {\n cancelled = true;\n };\n }, [publisherId, gqlUrl, authToken]);\n\n const isLoading = bootstrapLoading;\n const statsLoading = bootstrapLoading || productsLoading;\n\n const totalItems = products.length;\n const pendingCount = products.filter(\n (p) => p.status === \"PENDING_REVIEW\",\n ).length;\n const publishedCount = products.filter(\n (p) => p.status === \"PUBLISHED\",\n ).length;\n\n const handleNavigate = (path: string) => {\n navigate?.(`${basePath}${path}`);\n };\n\n return (\n <PermissionGuard authOnly>\n <PageLayout\n title={\n <>\n {tr(\"devportal.seller\", \"Seller\")}{\" \"}\n <GradientText>\n {tr(\"devportal.dashboard\", \"Dashboard\")}\n </GradientText>\n </>\n }\n actions={\n // Whether or not a PHYSICAL org exists yet, this always goes to the\n // same place — CreatePhysicalItemPage auto-provisions one via\n // createSellerOrganization on first listing (see\n // useSellerOrganization, BOFF-7285). Hidden only while loading.\n !isLoading ? (\n <button\n onClick={() => handleNavigate(\"/seller/items/new\")}\n className=\"inline-flex items-center gap-2 rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90 transition-colors\"\n >\n <Plus size={16} />\n {tr(\"devportal.listAnItem\", \"List an Item\")}\n </button>\n ) : null\n }\n >\n <div className=\"flex flex-col gap-8\">\n {/* Compact inline stats */}\n <div className=\"flex items-center gap-6 border-b border-border-seam pb-6\">\n {statsLoading ? (\n <Loader2 className=\"h-4 w-4 animate-spin text-text-secondary\" />\n ) : (\n <>\n <StatPill\n icon={<Package size={14} />}\n value={totalItems}\n label={tr(\"devportal.items\", \"Items\")}\n />\n <div className=\"h-4 w-px bg-border-seam\" />\n <StatPill\n icon={<Clock size={14} />}\n value={pendingCount}\n label={tr(\"devportal.pendingReview\", \"Pending Review\")}\n />\n <div className=\"h-4 w-px bg-border-seam\" />\n <StatPill\n icon={<CheckCircle2 size={14} />}\n value={publishedCount}\n label={tr(\"devportal.published\", \"Published\")}\n />\n <div className=\"h-4 w-px bg-border-seam\" />\n <StatPill\n icon={<Building2 size={14} />}\n value={orgs.length}\n label={tr(\"devportal.organizations\", \"Organizations\")}\n />\n </>\n )}\n </div>\n\n {/* Recent Listings */}\n <DashSection\n title={tr(\"devportal.recentListings\", \"Recent Listings\")}\n action={\n products.length > 0 ? (\n <button\n onClick={() => handleNavigate(\"/seller/items\")}\n className=\"inline-flex items-center gap-1 text-sm font-medium text-primary hover:underline\"\n >\n {tr(\"devportal.viewAll\", \"View all\")}\n <ArrowRight size={14} />\n </button>\n ) : undefined\n }\n >\n {isLoading || productsLoading ? (\n <div className=\"flex items-center justify-center py-12\">\n <Loader2 className=\"h-6 w-6 animate-spin text-text-secondary\" />\n </div>\n ) : products.length > 0 ? (\n <div className=\"grid gap-4 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3\">\n {products.slice(0, 3).map((product) => (\n <ProductCard\n key={product.id}\n product={product}\n onClick={() => handleNavigate(\"/seller/items\")}\n />\n ))}\n </div>\n ) : (\n <EmptyState\n illustration=\"empty-data\"\n title={tr(\"devportal.listYourFirstItem\", \"List your first item\")}\n description={tr(\n \"devportal.addPhysicalProductsToYourStore\",\n \"Add physical products to your store and they will appear here. Your seller organization is set up automatically the first time you list an item.\",\n )}\n action={\n <button\n onClick={() => handleNavigate(\"/seller/items/new\")}\n className=\"inline-flex items-center gap-2 rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90\"\n >\n <Plus size={16} />\n {tr(\"devportal.listAnItem\", \"List an Item\")}\n </button>\n }\n />\n )}\n </DashSection>\n\n {/* Organizations */}\n {(isLoading || orgs.length > 0) && (\n <DashSection\n title={tr(\"devportal.yourOrganizations\", \"Your Organizations\")}\n action={\n orgs.length > 0 ? (\n <button\n onClick={() => handleNavigate(\"/organizations\")}\n className=\"inline-flex items-center gap-1 text-sm font-medium text-primary hover:underline\"\n >\n {tr(\"devportal.viewAll\", \"View all\")}\n <ArrowRight size={14} />\n </button>\n ) : undefined\n }\n >\n {isLoading ? (\n <div className=\"flex items-center justify-center py-12\">\n <Loader2 className=\"h-6 w-6 animate-spin text-text-secondary\" />\n </div>\n ) : (\n <div className=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {orgs.slice(0, 3).map((org) => (\n <OrgCard\n key={org.id}\n org={org}\n onClick={() => handleNavigate(`/organizations/${org.id}`)}\n />\n ))}\n </div>\n )}\n </DashSection>\n )}\n </div>\n </PageLayout>\n </PermissionGuard>\n );\n};\n\n// ---------------------------------------------------------------------------\n// Sub-components\n// ---------------------------------------------------------------------------\n\nconst StatPill: FC<{ icon: ReactNode; value: number; label: string }> = ({\n icon,\n value,\n label,\n}) => (\n <div className=\"flex items-center gap-1.5\">\n <span className=\"text-text-muted\">{icon}</span>\n <span className=\"text-sm font-semibold tabular-nums\">{value}</span>\n <span className=\"text-sm text-text-secondary\">{label}</span>\n </div>\n);\n\nconst DashSection: FC<{\n title: string;\n action?: ReactNode;\n children: ReactNode;\n}> = ({ title, action, children }) => (\n <section className=\"flex flex-col gap-4\">\n <div className=\"flex items-center justify-between\">\n <h2 className=\"text-sm font-semibold uppercase tracking-wider text-text-secondary\">\n {title}\n </h2>\n {action}\n </div>\n {children}\n </section>\n);\n\nconst ProductCard: FC<{\n product: PhysicalProduct;\n onClick: () => void;\n}> = ({ product, onClick }) => {\n const badge =\n STATUS_BADGE_STYLES[product.status] ?? STATUS_BADGE_STYLES.DRAFT;\n\n return (\n <GlassCard\n treatment=\"insight\"\n role=\"button\"\n tabIndex={0}\n className=\"cursor-pointer p-5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\"\n onClick={onClick}\n onKeyDown={(e) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n onClick();\n }\n }}\n >\n <div className=\"flex items-center gap-3 mb-4\">\n {product.icon ? (\n <img\n src={product.icon}\n alt={product.name}\n className=\"h-10 w-10 rounded-xl object-cover shrink-0\"\n />\n ) : (\n <div className=\"flex h-10 w-10 items-center justify-center rounded-xl bg-action-primary-bg/15 text-primary shrink-0\">\n <Package size={20} />\n </div>\n )}\n <div className=\"min-w-0 flex-1\">\n <p className=\"font-semibold truncate text-text-primary\">\n {product.name}\n </p>\n <p className=\"text-xs text-text-secondary mt-0.5\">\n {formatPrice(product.price, product.currency)}\n </p>\n </div>\n <span\n className={`shrink-0 rounded-full px-2.5 py-0.5 text-xs font-medium ${badge.className}`}\n >\n {badge.label}\n </span>\n </div>\n <div className=\"flex items-center gap-2 text-sm\">\n <Package size={12} className=\"text-text-muted shrink-0\" />\n <span\n className={\n product.stockQuantity === 0\n ? \"text-status-error-text\"\n : \"text-text-secondary\"\n }\n >\n {product.stockQuantity === 0\n ? \"Out of stock\"\n : `${product.stockQuantity} in stock`}\n </span>\n </div>\n </GlassCard>\n );\n};\n\nconst OrgCard: FC<{ org: OrgSummary; onClick: () => void }> = ({\n org,\n onClick,\n}) => {\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n\n const initials = org.name\n .split(\" \")\n .map((w: string) => w[0])\n .join(\"\")\n .slice(0, 2)\n .toUpperCase();\n\n return (\n <GlassCard\n treatment=\"insight\"\n role=\"button\"\n tabIndex={0}\n className=\"cursor-pointer p-5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\"\n onClick={onClick}\n onKeyDown={(e) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n onClick();\n }\n }}\n >\n <div className=\"flex items-center gap-3 mb-4\">\n {org.logoUrl ? (\n <img\n src={org.logoUrl}\n alt={org.name}\n className=\"h-10 w-10 rounded-xl object-cover\"\n />\n ) : (\n <div className=\"flex h-10 w-10 items-center justify-center rounded-xl bg-action-primary-bg/15 text-primary text-sm font-bold\">\n {initials}\n </div>\n )}\n <div className=\"min-w-0 flex-1\">\n <p className=\"font-semibold truncate text-text-primary\">{org.name}</p>\n <p className=\"text-xs text-text-secondary mt-0.5\">\n {org.publishedApps ?? 0}{\" \"}\n {tr(\"devportal.appsPublished\", \"apps published\")}\n </p>\n </div>\n <ArrowRight size={15} className=\"text-text-muted shrink-0\" />\n </div>\n\n <div className=\"grid grid-cols-2 gap-3\">\n <div className=\"flex items-center gap-2 rounded-lg bg-bg-muted px-3 py-2\">\n <Download size={12} className=\"text-text-muted shrink-0\" />\n <div>\n <p className=\"text-[11px] text-text-secondary leading-none mb-0.5\">\n {tr(\"devportal.installs\", \"Installs\")}\n </p>\n <p className=\"text-sm font-semibold tabular-nums\">\n {(org.totalInstalls ?? 0).toLocaleString()}\n </p>\n </div>\n </div>\n <div className=\"flex items-center gap-2 rounded-lg bg-bg-muted px-3 py-2\">\n <DollarSign size={12} className=\"text-text-muted shrink-0\" />\n <div>\n <p className=\"text-[11px] text-text-secondary leading-none mb-0.5\">\n {tr(\"devportal.revenue\", \"Revenue\")}\n </p>\n <p className=\"text-sm font-semibold tabular-nums\">\n ${(org.totalRevenue ?? 0).toLocaleString()}\n </p>\n </div>\n </div>\n </div>\n </GlassCard>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"SellerDashboardPage.js","names":[],"sources":["../../../src/devportal/pages/SellerDashboardPage.tsx"],"sourcesContent":["import type { FC, ReactNode } from \"react\";\nimport { useEffect, useState } from \"react\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\nimport {\n Package,\n Building2,\n Plus,\n ArrowRight,\n Loader2,\n Download,\n DollarSign,\n Clock,\n CheckCircle2,\n} from \"lucide-react\";\nimport { GlassCard, GradientText } from \"@burdenoff/fe-libs/ui\";\nimport { PageLayout } from \"../components/PageLayout\";\nimport { EmptyState } from \"../components/EmptyState\";\nimport { useDevportal } from \"../providers/DevportalProvider\";\nimport { PermissionGuard } from \"../components\";\n\n// ---------------------------------------------------------------------------\n// Local types\n// ---------------------------------------------------------------------------\n\ntype ProductStatus =\n \"DRAFT\" | \"PENDING_REVIEW\" | \"PUBLISHED\" | \"REJECTED\" | \"INACTIVE\";\n\ninterface PhysicalProduct {\n id: string;\n name: string;\n slug: string;\n type: string;\n status: ProductStatus;\n price: number;\n currency: string;\n stockQuantity: number;\n icon?: string | null;\n}\n\ninterface OrgSummary {\n id: string;\n name: string;\n slug: string;\n logoUrl?: string | null;\n isVerified: boolean;\n publishedApps?: number | null;\n totalInstalls?: number | null;\n totalRevenue?: number | null;\n kind: \"DIGITAL\" | \"PHYSICAL\";\n}\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst STATUS_BADGE_STYLES: Record<\n ProductStatus,\n { label: string; className: string }\n> = {\n PUBLISHED: {\n label: \"Published\",\n className: \"text-status-success-text bg-status-success-bg-subtle\",\n },\n PENDING_REVIEW: {\n label: \"Pending Review\",\n className: \"text-status-warning-text bg-status-warning-bg-subtle\",\n },\n DRAFT: {\n label: \"Draft\",\n className: \"text-text-muted bg-bg-muted\",\n },\n REJECTED: {\n label: \"Rejected\",\n className: \"text-status-error-text bg-status-error-bg/10\",\n },\n INACTIVE: {\n label: \"Inactive\",\n className: \"text-text-muted bg-bg-muted\",\n },\n};\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction formatPrice(price: number, currency: string): string {\n const curr = currency || \"USD\";\n const formatted = new Intl.NumberFormat(\"en-US\", {\n style: \"currency\",\n currency: curr,\n minimumFractionDigits: 2,\n maximumFractionDigits: 2,\n }).format(price);\n return `${formatted} ${curr}`;\n}\n\n// ---------------------------------------------------------------------------\n// GraphQL query strings\n// ---------------------------------------------------------------------------\n\nconst BOOTSTRAP_QUERY = `\n query SellerDashboardBootstrap {\n myDeveloperOrganizations {\n id\n name\n slug\n logoUrl\n isVerified\n publishedApps\n totalInstalls\n totalRevenue\n kind\n }\n myPublisher {\n id\n }\n }\n`;\n\nconst SELLER_PRODUCTS_QUERY = `\n query SellerDashboardProducts($publisherId: String!) {\n products(filter: { publisherId: $publisherId, nature: PHYSICAL }, limit: 3, offset: 0) {\n id\n name\n slug\n type\n status\n price\n currency\n stockQuantity\n icon\n }\n }\n`;\n\n// ---------------------------------------------------------------------------\n// Page component\n// ---------------------------------------------------------------------------\n\nexport const SellerDashboardPage: FC = () => {\n const { navigate, basePath, apiGatewayUrl, authToken } = useDevportal();\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n\n const gqlUrl = apiGatewayUrl ?? \"/global/graphql\";\n\n const [bootstrapLoading, setBootstrapLoading] = useState(true);\n const [productsLoading, setProductsLoading] = useState(false);\n const [orgs, setOrgs] = useState<OrgSummary[]>([]);\n const [publisherId, setPublisherId] = useState<string | null>(null);\n const [products, setProducts] = useState<PhysicalProduct[]>([]);\n\n // Fetch orgs + publisher in one round-trip\n useEffect(() => {\n let cancelled = false;\n setBootstrapLoading(true);\n\n const run = async () => {\n try {\n const res = await fetch(gqlUrl, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${authToken ?? \"\"}`,\n },\n body: JSON.stringify({ query: BOOTSTRAP_QUERY }),\n });\n if (cancelled) return;\n const json = (await res.json()) as {\n data?: {\n myDeveloperOrganizations?: OrgSummary[];\n myPublisher?: { id: string } | null;\n };\n };\n if (cancelled) return;\n // Seller portal only ever shows PHYSICAL-kind orgs — a DIGITAL org\n // (regular app-publishing) has no business here. See DeveloperOrgKind\n // (BOFF-7285).\n setOrgs(\n (json.data?.myDeveloperOrganizations ?? []).filter(\n (org) => org.kind === \"PHYSICAL\",\n ),\n );\n setPublisherId(json.data?.myPublisher?.id ?? null);\n } finally {\n if (!cancelled) setBootstrapLoading(false);\n }\n };\n\n void run();\n return () => {\n cancelled = true;\n };\n }, [gqlUrl, authToken]);\n\n // Fetch recent physical products once publisher is known\n useEffect(() => {\n if (!publisherId) return;\n let cancelled = false;\n setProductsLoading(true);\n\n const run = async () => {\n try {\n const res = await fetch(gqlUrl, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${authToken ?? \"\"}`,\n },\n body: JSON.stringify({\n query: SELLER_PRODUCTS_QUERY,\n variables: { publisherId },\n }),\n });\n if (cancelled) return;\n const json = (await res.json()) as {\n data?: {\n products?: Array<\n Omit<PhysicalProduct, \"status\"> & { status: string }\n >;\n };\n };\n if (cancelled) return;\n const DIGITAL_TYPES = new Set([\"APP\", \"NODE\", \"WORKFLOW\"]);\n setProducts(\n (json.data?.products ?? [])\n .filter((p) => !DIGITAL_TYPES.has(p.type))\n .map((p) => ({\n ...p,\n status: p.status as ProductStatus,\n })),\n );\n } finally {\n if (!cancelled) setProductsLoading(false);\n }\n };\n\n void run();\n return () => {\n cancelled = true;\n };\n }, [publisherId, gqlUrl, authToken]);\n\n const isLoading = bootstrapLoading;\n const statsLoading = bootstrapLoading || productsLoading;\n\n const totalItems = products.length;\n const pendingCount = products.filter(\n (p) => p.status === \"PENDING_REVIEW\",\n ).length;\n const publishedCount = products.filter(\n (p) => p.status === \"PUBLISHED\",\n ).length;\n\n const handleNavigate = (path: string) => {\n navigate?.(`${basePath}${path}`);\n };\n\n return (\n <PermissionGuard authOnly>\n <PageLayout\n title={\n <>\n {tr(\"devportal.seller\", \"Seller\")}{\" \"}\n <GradientText>\n {tr(\"devportal.dashboard\", \"Dashboard\")}\n </GradientText>\n </>\n }\n actions={\n // Whether or not a PHYSICAL org exists yet, this always goes to the\n // same place — CreatePhysicalItemPage auto-provisions one via\n // createSellerOrganization on first listing (see\n // useSellerOrganization, BOFF-7285). Hidden only while loading.\n !isLoading ? (\n <button\n onClick={() => handleNavigate(\"/seller/items/new\")}\n className=\"inline-flex items-center gap-2 rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90 transition-colors\"\n >\n <Plus size={16} />\n {tr(\"devportal.listAnItem\", \"List an Item\")}\n </button>\n ) : null\n }\n >\n <div className=\"flex flex-col gap-8\">\n {/* Compact inline stats */}\n <div className=\"flex items-center gap-6 border-b border-border-seam pb-6\">\n {statsLoading ? (\n <Loader2 className=\"h-4 w-4 animate-spin text-text-secondary\" />\n ) : (\n <>\n <StatPill\n icon={<Package size={14} />}\n value={totalItems}\n label={tr(\"devportal.items\", \"Items\")}\n />\n <div className=\"h-4 w-px bg-border-seam\" />\n <StatPill\n icon={<Clock size={14} />}\n value={pendingCount}\n label={tr(\"devportal.pendingReview\", \"Pending Review\")}\n />\n <div className=\"h-4 w-px bg-border-seam\" />\n <StatPill\n icon={<CheckCircle2 size={14} />}\n value={publishedCount}\n label={tr(\"devportal.published\", \"Published\")}\n />\n <div className=\"h-4 w-px bg-border-seam\" />\n <StatPill\n icon={<Building2 size={14} />}\n value={orgs.length}\n label={tr(\"devportal.organizations\", \"Organizations\")}\n />\n </>\n )}\n </div>\n\n {/* Recent Listings */}\n <DashSection\n title={tr(\"devportal.recentListings\", \"Recent Listings\")}\n action={\n products.length > 0 ? (\n <button\n onClick={() => handleNavigate(\"/seller/items\")}\n className=\"inline-flex items-center gap-1 text-sm font-medium text-primary hover:underline\"\n >\n {tr(\"devportal.viewAll\", \"View all\")}\n <ArrowRight size={14} />\n </button>\n ) : undefined\n }\n >\n {isLoading || productsLoading ? (\n <div className=\"flex items-center justify-center py-12\">\n <Loader2 className=\"h-6 w-6 animate-spin text-text-secondary\" />\n </div>\n ) : products.length > 0 ? (\n <div className=\"grid gap-4 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3\">\n {products.slice(0, 3).map((product) => (\n <ProductCard\n key={product.id}\n product={product}\n onClick={() => handleNavigate(\"/seller/items\")}\n />\n ))}\n </div>\n ) : (\n <EmptyState\n illustration=\"empty-data\"\n title={tr(\n \"devportal.listYourFirstItem\",\n \"List your first item\",\n )}\n description={tr(\n \"devportal.addPhysicalProductsToYourStore\",\n \"Add physical products to your store and they will appear here. Your seller organization is set up automatically the first time you list an item.\",\n )}\n action={\n <button\n onClick={() => handleNavigate(\"/seller/items/new\")}\n className=\"inline-flex items-center gap-2 rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90\"\n >\n <Plus size={16} />\n {tr(\"devportal.listAnItem\", \"List an Item\")}\n </button>\n }\n />\n )}\n </DashSection>\n\n {/* Organizations */}\n {(isLoading || orgs.length > 0) && (\n <DashSection\n title={tr(\"devportal.yourOrganizations\", \"Your Organizations\")}\n action={\n orgs.length > 0 ? (\n <button\n onClick={() => handleNavigate(\"/organizations\")}\n className=\"inline-flex items-center gap-1 text-sm font-medium text-primary hover:underline\"\n >\n {tr(\"devportal.viewAll\", \"View all\")}\n <ArrowRight size={14} />\n </button>\n ) : undefined\n }\n >\n {isLoading ? (\n <div className=\"flex items-center justify-center py-12\">\n <Loader2 className=\"h-6 w-6 animate-spin text-text-secondary\" />\n </div>\n ) : (\n <div className=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {orgs.slice(0, 3).map((org) => (\n <OrgCard\n key={org.id}\n org={org}\n onClick={() => handleNavigate(`/organizations/${org.id}`)}\n />\n ))}\n </div>\n )}\n </DashSection>\n )}\n </div>\n </PageLayout>\n </PermissionGuard>\n );\n};\n\n// ---------------------------------------------------------------------------\n// Sub-components\n// ---------------------------------------------------------------------------\n\nconst StatPill: FC<{ icon: ReactNode; value: number; label: string }> = ({\n icon,\n value,\n label,\n}) => (\n <div className=\"flex items-center gap-1.5\">\n <span className=\"text-text-muted\">{icon}</span>\n <span className=\"text-sm font-semibold tabular-nums\">{value}</span>\n <span className=\"text-sm text-text-secondary\">{label}</span>\n </div>\n);\n\nconst DashSection: FC<{\n title: string;\n action?: ReactNode;\n children: ReactNode;\n}> = ({ title, action, children }) => (\n <section className=\"flex flex-col gap-4\">\n <div className=\"flex items-center justify-between\">\n <h2 className=\"text-sm font-semibold uppercase tracking-wider text-text-secondary\">\n {title}\n </h2>\n {action}\n </div>\n {children}\n </section>\n);\n\nconst ProductCard: FC<{\n product: PhysicalProduct;\n onClick: () => void;\n}> = ({ product, onClick }) => {\n const badge =\n STATUS_BADGE_STYLES[product.status] ?? STATUS_BADGE_STYLES.DRAFT;\n\n return (\n <GlassCard\n treatment=\"insight\"\n role=\"button\"\n tabIndex={0}\n className=\"cursor-pointer p-5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\"\n onClick={onClick}\n onKeyDown={(e) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n onClick();\n }\n }}\n >\n <div className=\"flex items-center gap-3 mb-4\">\n {product.icon ? (\n <img\n src={product.icon}\n alt={product.name}\n className=\"h-10 w-10 rounded-xl object-cover shrink-0\"\n />\n ) : (\n <div className=\"flex h-10 w-10 items-center justify-center rounded-xl bg-action-primary-bg/15 text-primary shrink-0\">\n <Package size={20} />\n </div>\n )}\n <div className=\"min-w-0 flex-1\">\n <p className=\"font-semibold truncate text-text-primary\">\n {product.name}\n </p>\n <p className=\"text-xs text-text-secondary mt-0.5\">\n {formatPrice(product.price, product.currency)}\n </p>\n </div>\n <span\n className={`shrink-0 rounded-full px-2.5 py-0.5 text-xs font-medium ${badge.className}`}\n >\n {badge.label}\n </span>\n </div>\n <div className=\"flex items-center gap-2 text-sm\">\n <Package size={12} className=\"text-text-muted shrink-0\" />\n <span\n className={\n product.stockQuantity === 0\n ? \"text-status-error-text\"\n : \"text-text-secondary\"\n }\n >\n {product.stockQuantity === 0\n ? \"Out of stock\"\n : `${product.stockQuantity} in stock`}\n </span>\n </div>\n </GlassCard>\n );\n};\n\nconst OrgCard: FC<{ org: OrgSummary; onClick: () => void }> = ({\n org,\n onClick,\n}) => {\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n\n const initials = org.name\n .split(\" \")\n .map((w: string) => w[0])\n .join(\"\")\n .slice(0, 2)\n .toUpperCase();\n\n return (\n <GlassCard\n treatment=\"insight\"\n role=\"button\"\n tabIndex={0}\n className=\"cursor-pointer p-5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\"\n onClick={onClick}\n onKeyDown={(e) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n onClick();\n }\n }}\n >\n <div className=\"flex items-center gap-3 mb-4\">\n {org.logoUrl ? (\n <img\n src={org.logoUrl}\n alt={org.name}\n className=\"h-10 w-10 rounded-xl object-cover\"\n />\n ) : (\n <div className=\"flex h-10 w-10 items-center justify-center rounded-xl bg-action-primary-bg/15 text-primary text-sm font-bold\">\n {initials}\n </div>\n )}\n <div className=\"min-w-0 flex-1\">\n <p className=\"font-semibold truncate text-text-primary\">{org.name}</p>\n <p className=\"text-xs text-text-secondary mt-0.5\">\n {org.publishedApps ?? 0}{\" \"}\n {tr(\"devportal.appsPublished\", \"apps published\")}\n </p>\n </div>\n <ArrowRight size={15} className=\"text-text-muted shrink-0\" />\n </div>\n\n <div className=\"grid grid-cols-2 gap-3\">\n <div className=\"flex items-center gap-2 rounded-lg bg-bg-muted px-3 py-2\">\n <Download size={12} className=\"text-text-muted shrink-0\" />\n <div>\n <p className=\"text-[11px] text-text-secondary leading-none mb-0.5\">\n {tr(\"devportal.installs\", \"Installs\")}\n </p>\n <p className=\"text-sm font-semibold tabular-nums\">\n {(org.totalInstalls ?? 0).toLocaleString()}\n </p>\n </div>\n </div>\n <div className=\"flex items-center gap-2 rounded-lg bg-bg-muted px-3 py-2\">\n <DollarSign size={12} className=\"text-text-muted shrink-0\" />\n <div>\n <p className=\"text-[11px] text-text-secondary leading-none mb-0.5\">\n {tr(\"devportal.revenue\", \"Revenue\")}\n </p>\n <p className=\"text-sm font-semibold tabular-nums\">\n ${(org.totalRevenue ?? 0).toLocaleString()}\n </p>\n </div>\n </div>\n </div>\n </GlassCard>\n );\n};\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SettingsPage.d.ts","sourceRoot":"","sources":["../../../src/devportal/pages/SettingsPage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;AAqChC,eAAO,MAAM,YAAY,EAAE,
|
|
1
|
+
{"version":3,"file":"SettingsPage.d.ts","sourceRoot":"","sources":["../../../src/devportal/pages/SettingsPage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;AAqChC,eAAO,MAAM,YAAY,EAAE,EAiY1B,CAAC"}
|
|
@@ -41,7 +41,7 @@ var S = (e, t) => {
|
|
|
41
41
|
description: i(O)
|
|
42
42
|
})
|
|
43
43
|
});
|
|
44
|
-
let N = E?.myDeveloperProfile, P = E?.myDeveloperOrganizations ?? [], F = E?.myApiKeys?.apiKeys ?? [], I = E?.myApiKeys?.totalCount ?? F.length, L = F[0], R = P.reduce((e, t) => e + (t.totalRevenue ?? 0), 0);
|
|
44
|
+
let N = E?.myDeveloperProfile, P = (E?.myDeveloperOrganizations ?? []).filter((e) => e.kind !== "PHYSICAL"), F = E?.myApiKeys?.apiKeys ?? [], I = E?.myApiKeys?.totalCount ?? F.length, L = F[0], R = P.reduce((e, t) => e + (t.totalRevenue ?? 0), 0);
|
|
45
45
|
return /* @__PURE__ */ y(a, {
|
|
46
46
|
title: j,
|
|
47
47
|
description: M,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SettingsPage.js","names":[],"sources":["../../../src/devportal/pages/SettingsPage.tsx"],"sourcesContent":["import type { FC } from \"react\";\nimport {\n Building2,\n ExternalLink,\n KeyRound,\n Mail,\n Settings,\n Shield,\n UserCircle2,\n Webhook,\n Workflow,\n Loader2,\n AlertCircle,\n} from \"lucide-react\";\nimport { EmphasisPanel } from \"@burdenoff/fe-libs/ui\";\nimport { PageLayout, PageSection } from \"../components/PageLayout\";\nimport { EmptyState } from \"../components/EmptyState\";\nimport { VerifiedBadge } from \"../components/StatusBadge\";\nimport { useDevportal } from \"../providers/DevportalProvider\";\nimport {\n getErrorMessage,\n useGetDeveloperPortalSettingsOverviewQuery,\n} from \"../hooks\";\nimport { useTr } from \"../utils/i18n\";\n\nconst formatDate = (\n value: string | null | undefined,\n neverLabel: string,\n): string => {\n if (!value) return neverLabel;\n\n const parsed = new Date(value);\n if (Number.isNaN(parsed.getTime())) return neverLabel;\n\n return parsed.toLocaleDateString();\n};\n\nexport const SettingsPage: FC = () => {\n const { navigate, basePath } = useDevportal();\n const tr = useTr();\n const { data, loading, error } = useGetDeveloperPortalSettingsOverviewQuery({\n fetchPolicy: \"cache-and-network\",\n });\n\n const handleNavigate = (path: string) => {\n navigate?.(`${basePath}${path}`);\n };\n\n const neverLabel = tr(\"devportal.settings.never\", \"Never\");\n const title = tr(\"devportal.settings.title\", \"Settings\");\n const description = tr(\n \"devportal.settings.description\",\n \"Review your developer portal account, credentials, and support options\",\n );\n\n if (loading && !data) {\n return (\n <PageLayout title={title} description={description}>\n <div\n className=\"flex items-center justify-center py-16 text-text-secondary\"\n role=\"status\"\n aria-live=\"polite\"\n aria-label={tr(\"devportal.common.loading\", \"Loading\")}\n >\n <Loader2 className=\"h-8 w-8 animate-spin\" />\n </div>\n </PageLayout>\n );\n }\n\n if (error) {\n return (\n <PageLayout title={title} description={description}>\n <EmptyState\n icon={<AlertCircle size={32} className=\"text-status-error-text\" />}\n title={tr(\"devportal.settings.errorTitle\", \"Unable to load settings\")}\n description={getErrorMessage(error)}\n />\n </PageLayout>\n );\n }\n\n const profile = data?.myDeveloperProfile;\n const organizations = data?.myDeveloperOrganizations ?? [];\n const apiKeys = data?.myApiKeys?.apiKeys ?? [];\n const totalApiKeyCount = data?.myApiKeys?.totalCount ?? apiKeys.length;\n const latestApiKey = apiKeys[0];\n const totalRevenue = organizations.reduce(\n (sum: number, organization: (typeof organizations)[number]) =>\n sum + (organization.totalRevenue ?? 0),\n 0,\n );\n\n return (\n <PageLayout\n title={title}\n description={description}\n purpose={tr(\n \"devportal.settings.purpose\",\n \"A quick health-check of your developer account — your profile, how many API keys and organizations you have, and where to get support. Use it to spot missing setup before it blocks publishing or billing.\",\n )}\n >\n <div className=\"grid gap-6 xl:grid-cols-[minmax(0,2fr)_minmax(320px,1fr)]\">\n <div className=\"space-y-6\">\n <PageSection title={tr(\"devportal.settings.profile\", \"Profile\")}>\n <EmphasisPanel>\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between\">\n <div className=\"space-y-2\">\n <div className=\"flex items-center gap-2\">\n <UserCircle2\n className=\"h-5 w-5 text-primary\"\n aria-hidden=\"true\"\n />\n <h2 className=\"text-lg font-semibold\">\n {tr(\n \"devportal.settings.developerProfile\",\n \"Developer profile\",\n )}\n </h2>\n {profile?.isVerified && <VerifiedBadge verified />}\n </div>\n <p className=\"text-sm text-text-secondary\">\n {profile?.companyName ||\n tr(\n \"devportal.settings.noCompanyName\",\n \"No company name set yet\",\n )}\n </p>\n {profile?.bio && (\n <p className=\"max-w-2xl text-sm text-text-secondary\">\n {profile.bio}\n </p>\n )}\n <div className=\"flex flex-wrap gap-2 pt-1 text-xs text-text-secondary\">\n <span className=\"rounded-full bg-bg-sunken px-2.5 py-1\">\n {profile?.applicationCount ?? 0}{\" \"}\n {tr(\n \"devportal.settings.publishedAssetsTracked\",\n \"published assets tracked\",\n )}\n </span>\n <span className=\"rounded-full bg-bg-sunken px-2.5 py-1\">\n {profile?.totalInstalls ?? 0}{\" \"}\n {tr(\"devportal.settings.installs\", \"installs\")}\n </span>\n <span className=\"rounded-full bg-bg-sunken px-2.5 py-1\">\n {tr(\"devportal.settings.emailUpdates\", \"Email updates\")}{\" \"}\n {profile?.emailNotifications\n ? tr(\"devportal.common.enabled\", \"enabled\")\n : tr(\"devportal.common.disabled\", \"disabled\")}\n </span>\n </div>\n </div>\n <button\n type=\"button\"\n onClick={() => handleNavigate(\"/publisher/profile\")}\n className=\"inline-flex items-center gap-2 rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary\"\n >\n <Settings size={16} aria-hidden=\"true\" />\n {tr(\"devportal.settings.manageProfile\", \"Manage profile\")}\n </button>\n </div>\n </EmphasisPanel>\n </PageSection>\n\n <div className=\"grid gap-6 lg:grid-cols-2\">\n <PageSection\n title={tr(\"devportal.settings.credentials\", \"Credentials\")}\n >\n <div className=\"rounded-card border border-border-seam bg-bg-surface p-6\">\n <div className=\"grid gap-4 sm:grid-cols-2\">\n <button\n type=\"button\"\n onClick={() => handleNavigate(\"/api-keys\")}\n className=\"rounded-card border border-border-seam bg-bg-surface p-4 text-left transition-colors hover:bg-accent focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary\"\n >\n <div className=\"flex items-center gap-2 text-primary\">\n <KeyRound size={16} aria-hidden=\"true\" />\n <span className=\"font-medium\">\n {tr(\"devportal.apiKeys\", \"API Keys\")}\n </span>\n </div>\n <p className=\"mt-2 text-2xl font-semibold\">\n {totalApiKeyCount}\n </p>\n <p className=\"mt-1 text-sm text-text-secondary\">\n {latestApiKey\n ? `${tr(\"devportal.settings.lastUsed\", \"Last used\")} ${formatDate(latestApiKey.lastUsedAt, neverLabel)}`\n : tr(\n \"devportal.settings.noApiKeys\",\n \"No API keys created yet\",\n )}\n </p>\n </button>\n\n <button\n type=\"button\"\n onClick={() => handleNavigate(\"/oauth\")}\n className=\"rounded-card border border-border-seam bg-bg-surface p-4 text-left transition-colors hover:bg-accent focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary\"\n >\n <div className=\"flex items-center gap-2 text-primary\">\n <Workflow size={16} aria-hidden=\"true\" />\n <span className=\"font-medium\">\n {tr(\"devportal.settings.oauthApps\", \"OAuth Apps\")}\n </span>\n </div>\n <p className=\"mt-2 text-2xl font-semibold\">\n {tr(\"devportal.common.open\", \"Open\")}\n </p>\n <p className=\"mt-1 text-sm text-text-secondary\">\n {tr(\n \"devportal.settings.oauthDescription\",\n \"Manage OAuth clients on the dedicated credentials page.\",\n )}\n </p>\n </button>\n </div>\n </div>\n </PageSection>\n\n <PageSection\n title={tr(\"devportal.settings.organizations\", \"Organizations\")}\n >\n <div className=\"rounded-card border border-border-seam bg-bg-surface p-6\">\n <div className=\"flex items-center gap-2 text-primary\">\n <Building2 size={16} aria-hidden=\"true\" />\n <span className=\"font-medium\">\n {tr(\n \"devportal.settings.developerOrganizations\",\n \"Developer organizations\",\n )}\n </span>\n </div>\n <p className=\"mt-2 text-2xl font-semibold\">\n {organizations.length}\n </p>\n <p className=\"mt-1 text-sm text-text-secondary\">\n ${totalRevenue.toLocaleString()}{\" \"}\n {tr(\n \"devportal.settings.lifetimeRevenue\",\n \"lifetime revenue across all teams\",\n )}\n </p>\n <button\n type=\"button\"\n onClick={() => handleNavigate(\"/organizations\")}\n className=\"mt-4 inline-flex items-center gap-2 rounded-md border px-4 py-2 text-sm font-medium hover:bg-accent focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary\"\n >\n {tr(\n \"devportal.settings.viewOrganizations\",\n \"View organizations\",\n )}\n <ExternalLink size={14} aria-hidden=\"true\" />\n </button>\n </div>\n </PageSection>\n </div>\n\n <PageSection\n title={tr(\n \"devportal.settings.deliveryAutomation\",\n \"Delivery & automation\",\n )}\n >\n <div className=\"rounded-card border border-border-seam bg-bg-surface p-6\">\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between\">\n <div>\n <div className=\"flex items-center gap-2 text-primary\">\n <Webhook size={16} aria-hidden=\"true\" />\n <span className=\"font-medium\">\n {tr(\n \"devportal.settings.webhookOperations\",\n \"Webhook operations\",\n )}\n </span>\n </div>\n <p className=\"mt-2 text-sm text-text-secondary\">\n {tr(\n \"devportal.settings.webhookOperationsDescription\",\n \"Configure application webhooks from each application detail page. Delivery history and retry status are available per app.\",\n )}\n </p>\n </div>\n <button\n type=\"button\"\n onClick={() => handleNavigate(\"/webhooks\")}\n className=\"inline-flex items-center gap-2 rounded-md border px-4 py-2 text-sm font-medium hover:bg-accent focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary\"\n >\n {tr(\"devportal.settings.openWebhooks\", \"Open webhooks\")}\n <ExternalLink size={14} aria-hidden=\"true\" />\n </button>\n </div>\n </div>\n </PageSection>\n </div>\n\n <div className=\"space-y-6\">\n <PageSection\n title={tr(\n \"devportal.settings.securitySupport\",\n \"Security & support\",\n )}\n >\n <div className=\"rounded-card border border-border-seam bg-bg-surface p-6\">\n <div className=\"flex items-center gap-2 text-primary\">\n <Shield size={16} aria-hidden=\"true\" />\n <span className=\"font-medium\">\n {tr(\"devportal.settings.securityPosture\", \"Security posture\")}\n </span>\n </div>\n <ul className=\"mt-4 space-y-3 text-sm text-text-secondary\">\n <li className=\"rounded-md bg-bg-sunken px-3 py-2\">\n {tr(\n \"devportal.settings.securityNote1\",\n \"API keys and OAuth apps are managed through the public global gateway.\",\n )}\n </li>\n <li className=\"rounded-md bg-bg-sunken px-3 py-2\">\n {tr(\n \"devportal.settings.securityNote2\",\n \"Webhook testing and delivery history are available per application.\",\n )}\n </li>\n <li className=\"rounded-md bg-bg-sunken px-3 py-2\">\n {tr(\n \"devportal.settings.securityNote3\",\n \"Profile verification status is reflected directly from your developer profile.\",\n )}\n </li>\n </ul>\n </div>\n </PageSection>\n\n <PageSection\n title={tr(\n \"devportal.settings.recentCredentialActivity\",\n \"Recent credential activity\",\n )}\n >\n <div className=\"rounded-card border border-border-seam bg-bg-surface p-6\">\n {latestApiKey ? (\n <div className=\"space-y-4 text-sm\">\n <div className=\"flex items-center justify-between gap-3\">\n <div>\n <p className=\"font-medium\">{latestApiKey.name}</p>\n <p className=\"text-text-secondary\">\n {latestApiKey.prefix}\n </p>\n </div>\n <span className=\"inline-flex items-center rounded-full bg-status-success-bg-subtle px-2.5 py-1 text-xs font-medium text-status-success-text\">\n {tr(\"devportal.common.active\", \"Active\")}\n </span>\n </div>\n <dl className=\"grid gap-3 text-text-secondary\">\n <div className=\"flex items-center justify-between gap-3\">\n <dt>{tr(\"devportal.common.created\", \"Created\")}</dt>\n <dd>{formatDate(latestApiKey.createdAt, neverLabel)}</dd>\n </div>\n <div className=\"flex items-center justify-between gap-3\">\n <dt>{tr(\"devportal.common.expires\", \"Expires\")}</dt>\n <dd>{formatDate(latestApiKey.expiresAt, neverLabel)}</dd>\n </div>\n <div className=\"flex items-center justify-between gap-3\">\n <dt>{tr(\"devportal.settings.lastUsed\", \"Last used\")}</dt>\n <dd>{formatDate(latestApiKey.lastUsedAt, neverLabel)}</dd>\n </div>\n </dl>\n </div>\n ) : (\n <EmptyState\n illustration=\"empty-notifications\"\n icon={<KeyRound size={24} />}\n title={tr(\n \"devportal.settings.noCredentialActivity\",\n \"No credential activity yet\",\n )}\n description={tr(\n \"devportal.settings.noCredentialActivityDescription\",\n \"Create an API key or OAuth app to manage credentials from here.\",\n )}\n />\n )}\n </div>\n </PageSection>\n\n <PageSection\n title={tr(\"devportal.settings.accountActions\", \"Account actions\")}\n >\n <div className=\"rounded-lg border border-status-warning-border bg-status-warning-bg-subtle p-6\">\n <h2 className=\"font-semibold text-status-warning-text\">\n {tr(\n \"devportal.settings.needHelpTitle\",\n \"Need help with account changes?\",\n )}\n </h2>\n <p className=\"mt-2 text-sm text-status-warning-text\">\n {tr(\n \"devportal.settings.needHelpDescription\",\n \"Destructive account operations are handled through support so the team can verify ownership and preserve billing history, audit trails, and marketplace assets.\",\n )}\n </p>\n <a\n href=\"mailto:support@burdenoff.com?subject=Developer%20portal%20account%20support\"\n className=\"mt-4 inline-flex items-center gap-2 rounded-md border border-status-warning-border bg-bg-surface px-4 py-2 text-sm font-medium text-text-primary hover:bg-accent focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary\"\n >\n <Mail size={16} aria-hidden=\"true\" />\n {tr(\"devportal.settings.contactSupport\", \"Contact support\")}\n </a>\n </div>\n </PageSection>\n </div>\n </div>\n </PageLayout>\n );\n};\n"],"mappings":";;;;;;;;;;;;AAyBA,IAAM,KACJ,GACA,MACW;AACX,KAAI,CAAC,EAAO,QAAO;CAEnB,IAAM,IAAS,IAAI,KAAK,EAAM;AAG9B,QAFI,OAAO,MAAM,EAAO,SAAS,CAAC,GAAS,IAEpC,EAAO,oBAAoB;GAGvB,UAAyB;CACpC,IAAM,EAAE,aAAU,gBAAa,GAAc,EACvC,IAAK,GAAO,EACZ,EAAE,SAAM,YAAS,aAAU,EAA2C,EAC1E,aAAa,qBACd,CAAC,EAEI,KAAkB,MAAiB;AACvC,MAAW,GAAG,IAAW,IAAO;IAG5B,IAAa,EAAG,4BAA4B,QAAQ,EACpD,IAAQ,EAAG,4BAA4B,WAAW,EAClD,IAAc,EAClB,kCACA,yEACD;AAED,KAAI,KAAW,CAAC,EACd,QACE,kBAAC,GAAD;EAAmB;EAAoB;YACrC,kBAAC,OAAD;GACE,WAAU;GACV,MAAK;GACL,aAAU;GACV,cAAY,EAAG,4BAA4B,UAAU;aAErD,kBAAC,GAAD,EAAS,WAAU,wBAAyB,CAAA;GACxC,CAAA;EACK,CAAA;AAIjB,KAAI,EACF,QACE,kBAAC,GAAD;EAAmB;EAAoB;YACrC,kBAAC,GAAD;GACE,MAAM,kBAAC,GAAD;IAAa,MAAM;IAAI,WAAU;IAA2B,CAAA;GAClE,OAAO,EAAG,iCAAiC,0BAA0B;GACrE,aAAa,EAAgB,EAAM;GACnC,CAAA;EACS,CAAA;CAIjB,IAAM,IAAU,GAAM,oBAChB,IAAgB,GAAM,4BAA4B,EAAE,EACpD,IAAU,GAAM,WAAW,WAAW,EAAE,EACxC,IAAmB,GAAM,WAAW,cAAc,EAAQ,QAC1D,IAAe,EAAQ,IACvB,IAAe,EAAc,QAChC,GAAa,MACZ,KAAO,EAAa,gBAAgB,IACtC,EACD;AAED,QACE,kBAAC,GAAD;EACS;EACM;EACb,SAAS,EACP,8BACA,8MACD;YAED,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,GAAD;MAAa,OAAO,EAAG,8BAA8B,UAAU;gBAC7D,kBAAC,GAAD,EAAA,UACE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,OAAD;QAAK,WAAU;kBAAf;SACE,kBAAC,OAAD;UAAK,WAAU;oBAAf;WACE,kBAAC,GAAD;YACE,WAAU;YACV,eAAY;YACZ,CAAA;WACF,kBAAC,MAAD;YAAI,WAAU;sBACX,EACC,uCACA,oBACD;YACE,CAAA;WACJ,GAAS,cAAc,kBAAC,GAAD,EAAe,UAAA,IAAW,CAAA;WAC9C;;SACN,kBAAC,KAAD;UAAG,WAAU;oBACV,GAAS,eACR,EACE,oCACA,0BACD;UACD,CAAA;SACH,GAAS,OACR,kBAAC,KAAD;UAAG,WAAU;oBACV,EAAQ;UACP,CAAA;SAEN,kBAAC,OAAD;UAAK,WAAU;oBAAf;WACE,kBAAC,QAAD;YAAM,WAAU;sBAAhB;aACG,GAAS,oBAAoB;aAAG;aAChC,EACC,6CACA,2BACD;aACI;;WACP,kBAAC,QAAD;YAAM,WAAU;sBAAhB;aACG,GAAS,iBAAiB;aAAG;aAC7B,EAAG,+BAA+B,WAAW;aACzC;;WACP,kBAAC,QAAD;YAAM,WAAU;sBAAhB;aACG,EAAG,mCAAmC,gBAAgB;aAAE;aACxD,GAAS,qBACN,EAAG,4BAA4B,UAAU,GACzC,EAAG,6BAA6B,WAAW;aAC1C;;WACH;;SACF;WACN,kBAAC,UAAD;QACE,MAAK;QACL,eAAe,EAAe,qBAAqB;QACnD,WAAU;kBAHZ,CAKE,kBAAC,GAAD;SAAU,MAAM;SAAI,eAAY;SAAS,CAAA,EACxC,EAAG,oCAAoC,iBAAiB,CAClD;UACL;UACQ,CAAA;MACJ,CAAA;KAEd,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,GAAD;OACE,OAAO,EAAG,kCAAkC,cAAc;iBAE1D,kBAAC,OAAD;QAAK,WAAU;kBACb,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,UAAD;UACE,MAAK;UACL,eAAe,EAAe,YAAY;UAC1C,WAAU;oBAHZ;WAKE,kBAAC,OAAD;YAAK,WAAU;sBAAf,CACE,kBAAC,GAAD;aAAU,MAAM;aAAI,eAAY;aAAS,CAAA,EACzC,kBAAC,QAAD;aAAM,WAAU;uBACb,EAAG,qBAAqB,WAAW;aAC/B,CAAA,CACH;;WACN,kBAAC,KAAD;YAAG,WAAU;sBACV;YACC,CAAA;WACJ,kBAAC,KAAD;YAAG,WAAU;sBACV,IACG,GAAG,EAAG,+BAA+B,YAAY,CAAC,GAAG,EAAW,EAAa,YAAY,EAAW,KACpG,EACE,gCACA,0BACD;YACH,CAAA;WACG;aAET,kBAAC,UAAD;UACE,MAAK;UACL,eAAe,EAAe,SAAS;UACvC,WAAU;oBAHZ;WAKE,kBAAC,OAAD;YAAK,WAAU;sBAAf,CACE,kBAAC,GAAD;aAAU,MAAM;aAAI,eAAY;aAAS,CAAA,EACzC,kBAAC,QAAD;aAAM,WAAU;uBACb,EAAG,gCAAgC,aAAa;aAC5C,CAAA,CACH;;WACN,kBAAC,KAAD;YAAG,WAAU;sBACV,EAAG,yBAAyB,OAAO;YAClC,CAAA;WACJ,kBAAC,KAAD;YAAG,WAAU;sBACV,EACC,uCACA,0DACD;YACC,CAAA;WACG;YACL;;QACF,CAAA;OACM,CAAA,EAEd,kBAAC,GAAD;OACE,OAAO,EAAG,oCAAoC,gBAAgB;iBAE9D,kBAAC,OAAD;QAAK,WAAU;kBAAf;SACE,kBAAC,OAAD;UAAK,WAAU;oBAAf,CACE,kBAAC,GAAD;WAAW,MAAM;WAAI,eAAY;WAAS,CAAA,EAC1C,kBAAC,QAAD;WAAM,WAAU;qBACb,EACC,6CACA,0BACD;WACI,CAAA,CACH;;SACN,kBAAC,KAAD;UAAG,WAAU;oBACV,EAAc;UACb,CAAA;SACJ,kBAAC,KAAD;UAAG,WAAU;oBAAb;WAAgD;WAC5C,EAAa,gBAAgB;WAAE;WAChC,EACC,sCACA,oCACD;WACC;;SACJ,kBAAC,UAAD;UACE,MAAK;UACL,eAAe,EAAe,iBAAiB;UAC/C,WAAU;oBAHZ,CAKG,EACC,wCACA,qBACD,EACD,kBAAC,GAAD;WAAc,MAAM;WAAI,eAAY;WAAS,CAAA,CACtC;;SACL;;OACM,CAAA,CACV;;KAEN,kBAAC,GAAD;MACE,OAAO,EACL,yCACA,wBACD;gBAED,kBAAC,OAAD;OAAK,WAAU;iBACb,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,GAAD;UAAS,MAAM;UAAI,eAAY;UAAS,CAAA,EACxC,kBAAC,QAAD;UAAM,WAAU;oBACb,EACC,wCACA,qBACD;UACI,CAAA,CACH;YACN,kBAAC,KAAD;SAAG,WAAU;mBACV,EACC,mDACA,6HACD;SACC,CAAA,CACA,EAAA,CAAA,EACN,kBAAC,UAAD;SACE,MAAK;SACL,eAAe,EAAe,YAAY;SAC1C,WAAU;mBAHZ,CAKG,EAAG,mCAAmC,gBAAgB,EACvD,kBAAC,GAAD;UAAc,MAAM;UAAI,eAAY;UAAS,CAAA,CACtC;WACL;;OACF,CAAA;MACM,CAAA;KACV;OAEN,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,GAAD;MACE,OAAO,EACL,sCACA,qBACD;gBAED,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,GAAD;SAAQ,MAAM;SAAI,eAAY;SAAS,CAAA,EACvC,kBAAC,QAAD;SAAM,WAAU;mBACb,EAAG,sCAAsC,mBAAmB;SACxD,CAAA,CACH;WACN,kBAAC,MAAD;QAAI,WAAU;kBAAd;SACE,kBAAC,MAAD;UAAI,WAAU;oBACX,EACC,oCACA,yEACD;UACE,CAAA;SACL,kBAAC,MAAD;UAAI,WAAU;oBACX,EACC,oCACA,sEACD;UACE,CAAA;SACL,kBAAC,MAAD;UAAI,WAAU;oBACX,EACC,oCACA,iFACD;UACE,CAAA;SACF;UACD;;MACM,CAAA;KAEd,kBAAC,GAAD;MACE,OAAO,EACL,+CACA,6BACD;gBAED,kBAAC,OAAD;OAAK,WAAU;iBACZ,IACC,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;UAAG,WAAU;oBAAe,EAAa;UAAS,CAAA,EAClD,kBAAC,KAAD;UAAG,WAAU;oBACV,EAAa;UACZ,CAAA,CACA,EAAA,CAAA,EACN,kBAAC,QAAD;UAAM,WAAU;oBACb,EAAG,2BAA2B,SAAS;UACnC,CAAA,CACH;YACN,kBAAC,MAAD;SAAI,WAAU;mBAAd;UACE,kBAAC,OAAD;WAAK,WAAU;qBAAf,CACE,kBAAC,MAAD,EAAA,UAAK,EAAG,4BAA4B,UAAU,EAAM,CAAA,EACpD,kBAAC,MAAD,EAAA,UAAK,EAAW,EAAa,WAAW,EAAW,EAAM,CAAA,CACrD;;UACN,kBAAC,OAAD;WAAK,WAAU;qBAAf,CACE,kBAAC,MAAD,EAAA,UAAK,EAAG,4BAA4B,UAAU,EAAM,CAAA,EACpD,kBAAC,MAAD,EAAA,UAAK,EAAW,EAAa,WAAW,EAAW,EAAM,CAAA,CACrD;;UACN,kBAAC,OAAD;WAAK,WAAU;qBAAf,CACE,kBAAC,MAAD,EAAA,UAAK,EAAG,+BAA+B,YAAY,EAAM,CAAA,EACzD,kBAAC,MAAD,EAAA,UAAK,EAAW,EAAa,YAAY,EAAW,EAAM,CAAA,CACtD;;UACH;WACD;YAEN,kBAAC,GAAD;QACE,cAAa;QACb,MAAM,kBAAC,GAAD,EAAU,MAAM,IAAM,CAAA;QAC5B,OAAO,EACL,2CACA,6BACD;QACD,aAAa,EACX,sDACA,kEACD;QACD,CAAA;OAEA,CAAA;MACM,CAAA;KAEd,kBAAC,GAAD;MACE,OAAO,EAAG,qCAAqC,kBAAkB;gBAEjE,kBAAC,OAAD;OAAK,WAAU;iBAAf;QACE,kBAAC,MAAD;SAAI,WAAU;mBACX,EACC,oCACA,kCACD;SACE,CAAA;QACL,kBAAC,KAAD;SAAG,WAAU;mBACV,EACC,0CACA,kKACD;SACC,CAAA;QACJ,kBAAC,KAAD;SACE,MAAK;SACL,WAAU;mBAFZ,CAIE,kBAAC,GAAD;UAAM,MAAM;UAAI,eAAY;UAAS,CAAA,EACpC,EAAG,qCAAqC,kBAAkB,CACzD;;QACA;;MACM,CAAA;KACV;MACF;;EACK,CAAA"}
|
|
1
|
+
{"version":3,"file":"SettingsPage.js","names":[],"sources":["../../../src/devportal/pages/SettingsPage.tsx"],"sourcesContent":["import type { FC } from \"react\";\nimport {\n Building2,\n ExternalLink,\n KeyRound,\n Mail,\n Settings,\n Shield,\n UserCircle2,\n Webhook,\n Workflow,\n Loader2,\n AlertCircle,\n} from \"lucide-react\";\nimport { EmphasisPanel } from \"@burdenoff/fe-libs/ui\";\nimport { PageLayout, PageSection } from \"../components/PageLayout\";\nimport { EmptyState } from \"../components/EmptyState\";\nimport { VerifiedBadge } from \"../components/StatusBadge\";\nimport { useDevportal } from \"../providers/DevportalProvider\";\nimport {\n getErrorMessage,\n useGetDeveloperPortalSettingsOverviewQuery,\n} from \"../hooks\";\nimport { useTr } from \"../utils/i18n\";\n\nconst formatDate = (\n value: string | null | undefined,\n neverLabel: string,\n): string => {\n if (!value) return neverLabel;\n\n const parsed = new Date(value);\n if (Number.isNaN(parsed.getTime())) return neverLabel;\n\n return parsed.toLocaleDateString();\n};\n\nexport const SettingsPage: FC = () => {\n const { navigate, basePath } = useDevportal();\n const tr = useTr();\n const { data, loading, error } = useGetDeveloperPortalSettingsOverviewQuery({\n fetchPolicy: \"cache-and-network\",\n });\n\n const handleNavigate = (path: string) => {\n navigate?.(`${basePath}${path}`);\n };\n\n const neverLabel = tr(\"devportal.settings.never\", \"Never\");\n const title = tr(\"devportal.settings.title\", \"Settings\");\n const description = tr(\n \"devportal.settings.description\",\n \"Review your developer portal account, credentials, and support options\",\n );\n\n if (loading && !data) {\n return (\n <PageLayout title={title} description={description}>\n <div\n className=\"flex items-center justify-center py-16 text-text-secondary\"\n role=\"status\"\n aria-live=\"polite\"\n aria-label={tr(\"devportal.common.loading\", \"Loading\")}\n >\n <Loader2 className=\"h-8 w-8 animate-spin\" />\n </div>\n </PageLayout>\n );\n }\n\n if (error) {\n return (\n <PageLayout title={title} description={description}>\n <EmptyState\n icon={<AlertCircle size={32} className=\"text-status-error-text\" />}\n title={tr(\"devportal.settings.errorTitle\", \"Unable to load settings\")}\n description={getErrorMessage(error)}\n />\n </PageLayout>\n );\n }\n\n const profile = data?.myDeveloperProfile;\n // This overview is scoped to the regular (digital) devportal — exclude\n // PHYSICAL orgs so seller-portal orgs don't inflate the \"Developer\n // organizations\" count/revenue shown here. See DeveloperOrgKind (BOFF-7285).\n type SettingsOrganization = NonNullable<\n typeof data\n >[\"myDeveloperOrganizations\"][number];\n const organizations = (data?.myDeveloperOrganizations ?? []).filter(\n (org: SettingsOrganization) => org.kind !== \"PHYSICAL\",\n );\n const apiKeys = data?.myApiKeys?.apiKeys ?? [];\n const totalApiKeyCount = data?.myApiKeys?.totalCount ?? apiKeys.length;\n const latestApiKey = apiKeys[0];\n const totalRevenue = organizations.reduce(\n (sum: number, organization: (typeof organizations)[number]) =>\n sum + (organization.totalRevenue ?? 0),\n 0,\n );\n\n return (\n <PageLayout\n title={title}\n description={description}\n purpose={tr(\n \"devportal.settings.purpose\",\n \"A quick health-check of your developer account — your profile, how many API keys and organizations you have, and where to get support. Use it to spot missing setup before it blocks publishing or billing.\",\n )}\n >\n <div className=\"grid gap-6 xl:grid-cols-[minmax(0,2fr)_minmax(320px,1fr)]\">\n <div className=\"space-y-6\">\n <PageSection title={tr(\"devportal.settings.profile\", \"Profile\")}>\n <EmphasisPanel>\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between\">\n <div className=\"space-y-2\">\n <div className=\"flex items-center gap-2\">\n <UserCircle2\n className=\"h-5 w-5 text-primary\"\n aria-hidden=\"true\"\n />\n <h2 className=\"text-lg font-semibold\">\n {tr(\n \"devportal.settings.developerProfile\",\n \"Developer profile\",\n )}\n </h2>\n {profile?.isVerified && <VerifiedBadge verified />}\n </div>\n <p className=\"text-sm text-text-secondary\">\n {profile?.companyName ||\n tr(\n \"devportal.settings.noCompanyName\",\n \"No company name set yet\",\n )}\n </p>\n {profile?.bio && (\n <p className=\"max-w-2xl text-sm text-text-secondary\">\n {profile.bio}\n </p>\n )}\n <div className=\"flex flex-wrap gap-2 pt-1 text-xs text-text-secondary\">\n <span className=\"rounded-full bg-bg-sunken px-2.5 py-1\">\n {profile?.applicationCount ?? 0}{\" \"}\n {tr(\n \"devportal.settings.publishedAssetsTracked\",\n \"published assets tracked\",\n )}\n </span>\n <span className=\"rounded-full bg-bg-sunken px-2.5 py-1\">\n {profile?.totalInstalls ?? 0}{\" \"}\n {tr(\"devportal.settings.installs\", \"installs\")}\n </span>\n <span className=\"rounded-full bg-bg-sunken px-2.5 py-1\">\n {tr(\"devportal.settings.emailUpdates\", \"Email updates\")}{\" \"}\n {profile?.emailNotifications\n ? tr(\"devportal.common.enabled\", \"enabled\")\n : tr(\"devportal.common.disabled\", \"disabled\")}\n </span>\n </div>\n </div>\n <button\n type=\"button\"\n onClick={() => handleNavigate(\"/publisher/profile\")}\n className=\"inline-flex items-center gap-2 rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary\"\n >\n <Settings size={16} aria-hidden=\"true\" />\n {tr(\"devportal.settings.manageProfile\", \"Manage profile\")}\n </button>\n </div>\n </EmphasisPanel>\n </PageSection>\n\n <div className=\"grid gap-6 lg:grid-cols-2\">\n <PageSection\n title={tr(\"devportal.settings.credentials\", \"Credentials\")}\n >\n <div className=\"rounded-card border border-border-seam bg-bg-surface p-6\">\n <div className=\"grid gap-4 sm:grid-cols-2\">\n <button\n type=\"button\"\n onClick={() => handleNavigate(\"/api-keys\")}\n className=\"rounded-card border border-border-seam bg-bg-surface p-4 text-left transition-colors hover:bg-accent focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary\"\n >\n <div className=\"flex items-center gap-2 text-primary\">\n <KeyRound size={16} aria-hidden=\"true\" />\n <span className=\"font-medium\">\n {tr(\"devportal.apiKeys\", \"API Keys\")}\n </span>\n </div>\n <p className=\"mt-2 text-2xl font-semibold\">\n {totalApiKeyCount}\n </p>\n <p className=\"mt-1 text-sm text-text-secondary\">\n {latestApiKey\n ? `${tr(\"devportal.settings.lastUsed\", \"Last used\")} ${formatDate(latestApiKey.lastUsedAt, neverLabel)}`\n : tr(\n \"devportal.settings.noApiKeys\",\n \"No API keys created yet\",\n )}\n </p>\n </button>\n\n <button\n type=\"button\"\n onClick={() => handleNavigate(\"/oauth\")}\n className=\"rounded-card border border-border-seam bg-bg-surface p-4 text-left transition-colors hover:bg-accent focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary\"\n >\n <div className=\"flex items-center gap-2 text-primary\">\n <Workflow size={16} aria-hidden=\"true\" />\n <span className=\"font-medium\">\n {tr(\"devportal.settings.oauthApps\", \"OAuth Apps\")}\n </span>\n </div>\n <p className=\"mt-2 text-2xl font-semibold\">\n {tr(\"devportal.common.open\", \"Open\")}\n </p>\n <p className=\"mt-1 text-sm text-text-secondary\">\n {tr(\n \"devportal.settings.oauthDescription\",\n \"Manage OAuth clients on the dedicated credentials page.\",\n )}\n </p>\n </button>\n </div>\n </div>\n </PageSection>\n\n <PageSection\n title={tr(\"devportal.settings.organizations\", \"Organizations\")}\n >\n <div className=\"rounded-card border border-border-seam bg-bg-surface p-6\">\n <div className=\"flex items-center gap-2 text-primary\">\n <Building2 size={16} aria-hidden=\"true\" />\n <span className=\"font-medium\">\n {tr(\n \"devportal.settings.developerOrganizations\",\n \"Developer organizations\",\n )}\n </span>\n </div>\n <p className=\"mt-2 text-2xl font-semibold\">\n {organizations.length}\n </p>\n <p className=\"mt-1 text-sm text-text-secondary\">\n ${totalRevenue.toLocaleString()}{\" \"}\n {tr(\n \"devportal.settings.lifetimeRevenue\",\n \"lifetime revenue across all teams\",\n )}\n </p>\n <button\n type=\"button\"\n onClick={() => handleNavigate(\"/organizations\")}\n className=\"mt-4 inline-flex items-center gap-2 rounded-md border px-4 py-2 text-sm font-medium hover:bg-accent focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary\"\n >\n {tr(\n \"devportal.settings.viewOrganizations\",\n \"View organizations\",\n )}\n <ExternalLink size={14} aria-hidden=\"true\" />\n </button>\n </div>\n </PageSection>\n </div>\n\n <PageSection\n title={tr(\n \"devportal.settings.deliveryAutomation\",\n \"Delivery & automation\",\n )}\n >\n <div className=\"rounded-card border border-border-seam bg-bg-surface p-6\">\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between\">\n <div>\n <div className=\"flex items-center gap-2 text-primary\">\n <Webhook size={16} aria-hidden=\"true\" />\n <span className=\"font-medium\">\n {tr(\n \"devportal.settings.webhookOperations\",\n \"Webhook operations\",\n )}\n </span>\n </div>\n <p className=\"mt-2 text-sm text-text-secondary\">\n {tr(\n \"devportal.settings.webhookOperationsDescription\",\n \"Configure application webhooks from each application detail page. Delivery history and retry status are available per app.\",\n )}\n </p>\n </div>\n <button\n type=\"button\"\n onClick={() => handleNavigate(\"/webhooks\")}\n className=\"inline-flex items-center gap-2 rounded-md border px-4 py-2 text-sm font-medium hover:bg-accent focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary\"\n >\n {tr(\"devportal.settings.openWebhooks\", \"Open webhooks\")}\n <ExternalLink size={14} aria-hidden=\"true\" />\n </button>\n </div>\n </div>\n </PageSection>\n </div>\n\n <div className=\"space-y-6\">\n <PageSection\n title={tr(\n \"devportal.settings.securitySupport\",\n \"Security & support\",\n )}\n >\n <div className=\"rounded-card border border-border-seam bg-bg-surface p-6\">\n <div className=\"flex items-center gap-2 text-primary\">\n <Shield size={16} aria-hidden=\"true\" />\n <span className=\"font-medium\">\n {tr(\"devportal.settings.securityPosture\", \"Security posture\")}\n </span>\n </div>\n <ul className=\"mt-4 space-y-3 text-sm text-text-secondary\">\n <li className=\"rounded-md bg-bg-sunken px-3 py-2\">\n {tr(\n \"devportal.settings.securityNote1\",\n \"API keys and OAuth apps are managed through the public global gateway.\",\n )}\n </li>\n <li className=\"rounded-md bg-bg-sunken px-3 py-2\">\n {tr(\n \"devportal.settings.securityNote2\",\n \"Webhook testing and delivery history are available per application.\",\n )}\n </li>\n <li className=\"rounded-md bg-bg-sunken px-3 py-2\">\n {tr(\n \"devportal.settings.securityNote3\",\n \"Profile verification status is reflected directly from your developer profile.\",\n )}\n </li>\n </ul>\n </div>\n </PageSection>\n\n <PageSection\n title={tr(\n \"devportal.settings.recentCredentialActivity\",\n \"Recent credential activity\",\n )}\n >\n <div className=\"rounded-card border border-border-seam bg-bg-surface p-6\">\n {latestApiKey ? (\n <div className=\"space-y-4 text-sm\">\n <div className=\"flex items-center justify-between gap-3\">\n <div>\n <p className=\"font-medium\">{latestApiKey.name}</p>\n <p className=\"text-text-secondary\">\n {latestApiKey.prefix}\n </p>\n </div>\n <span className=\"inline-flex items-center rounded-full bg-status-success-bg-subtle px-2.5 py-1 text-xs font-medium text-status-success-text\">\n {tr(\"devportal.common.active\", \"Active\")}\n </span>\n </div>\n <dl className=\"grid gap-3 text-text-secondary\">\n <div className=\"flex items-center justify-between gap-3\">\n <dt>{tr(\"devportal.common.created\", \"Created\")}</dt>\n <dd>{formatDate(latestApiKey.createdAt, neverLabel)}</dd>\n </div>\n <div className=\"flex items-center justify-between gap-3\">\n <dt>{tr(\"devportal.common.expires\", \"Expires\")}</dt>\n <dd>{formatDate(latestApiKey.expiresAt, neverLabel)}</dd>\n </div>\n <div className=\"flex items-center justify-between gap-3\">\n <dt>{tr(\"devportal.settings.lastUsed\", \"Last used\")}</dt>\n <dd>{formatDate(latestApiKey.lastUsedAt, neverLabel)}</dd>\n </div>\n </dl>\n </div>\n ) : (\n <EmptyState\n illustration=\"empty-notifications\"\n icon={<KeyRound size={24} />}\n title={tr(\n \"devportal.settings.noCredentialActivity\",\n \"No credential activity yet\",\n )}\n description={tr(\n \"devportal.settings.noCredentialActivityDescription\",\n \"Create an API key or OAuth app to manage credentials from here.\",\n )}\n />\n )}\n </div>\n </PageSection>\n\n <PageSection\n title={tr(\"devportal.settings.accountActions\", \"Account actions\")}\n >\n <div className=\"rounded-lg border border-status-warning-border bg-status-warning-bg-subtle p-6\">\n <h2 className=\"font-semibold text-status-warning-text\">\n {tr(\n \"devportal.settings.needHelpTitle\",\n \"Need help with account changes?\",\n )}\n </h2>\n <p className=\"mt-2 text-sm text-status-warning-text\">\n {tr(\n \"devportal.settings.needHelpDescription\",\n \"Destructive account operations are handled through support so the team can verify ownership and preserve billing history, audit trails, and marketplace assets.\",\n )}\n </p>\n <a\n href=\"mailto:support@burdenoff.com?subject=Developer%20portal%20account%20support\"\n className=\"mt-4 inline-flex items-center gap-2 rounded-md border border-status-warning-border bg-bg-surface px-4 py-2 text-sm font-medium text-text-primary hover:bg-accent focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary\"\n >\n <Mail size={16} aria-hidden=\"true\" />\n {tr(\"devportal.settings.contactSupport\", \"Contact support\")}\n </a>\n </div>\n </PageSection>\n </div>\n </div>\n </PageLayout>\n );\n};\n"],"mappings":"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"}
|