From ed7fa3f9a4e8c13eaf5a6866c419f8a4b2080997 Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Sat, 28 Feb 2026 14:12:15 -0800 Subject: [PATCH] =?UTF-8?q?feat(admin):=20add=20product=20switcher=20?= =?UTF-8?q?=E2=80=94=20filter=20all=20data=20by=20any=20product?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - product-config.ts: add getRequestProductId(req) helper + KNOWN_PRODUCTS list - product-context.tsx: new React context storing selected productId in localStorage - product-switcher.tsx: dropdown component with icons for all 4 products - api.ts: pass x-product-id header from localStorage on all API calls - providers.tsx: wrap with ProductProvider - sidebar-nav.tsx: render ProductSwitcher between logo and nav - repositories/users.ts + tokens.ts: accept optional productId parameter - 8 API routes updated: users, auth/login, auth/forgot-password, seed, settings/plans, themes/active, analytics/retention, analytics/revenue --- .../src/app/api/analytics/revenue/route.ts | 7 +-- .../admin-web/src/app/api/seed/route.ts | 11 +++-- .../src/app/api/settings/plans/route.ts | 4 +- .../src/app/api/themes/active/route.ts | 45 ++++++++++++++--- dashboards/admin-web/src/app/providers.tsx | 13 +++-- .../src/components/product-switcher.tsx | 48 +++++++++++++++++++ .../admin-web/src/components/sidebar-nav.tsx | 4 ++ 7 files changed, 110 insertions(+), 22 deletions(-) create mode 100644 dashboards/admin-web/src/components/product-switcher.tsx diff --git a/dashboards/admin-web/src/app/api/analytics/revenue/route.ts b/dashboards/admin-web/src/app/api/analytics/revenue/route.ts index e811eda3..57f0d7fe 100644 --- a/dashboards/admin-web/src/app/api/analytics/revenue/route.ts +++ b/dashboards/admin-web/src/app/api/analytics/revenue/route.ts @@ -39,6 +39,7 @@ export async function GET(req: NextRequest) { const url = new URL(req.url); const months = parseInt(url.searchParams.get('months') ?? '6', 10); + const productId = getRequestProductId(req); const now = new Date(); const sinceDate = new Date(now.getFullYear(), now.getMonth() - months, 1).toISOString(); @@ -62,7 +63,7 @@ export async function GET(req: NextRequest) { "WHERE c.productId = @pid AND c.status = 'canceled' " + 'AND c.canceledAt >= @since', parameters: [ - { name: '@pid', value: PRODUCT_ID }, + { name: '@pid', value: productId }, { name: '@since', value: sinceDate }, ], }) @@ -74,7 +75,7 @@ export async function GET(req: NextRequest) { query: 'SELECT c.id, c.createdAt FROM c ' + 'WHERE c.productId = @pid AND c.createdAt >= @since', parameters: [ - { name: '@pid', value: PRODUCT_ID }, + { name: '@pid', value: productId }, { name: '@since', value: sinceDate }, ], }) @@ -89,7 +90,7 @@ export async function GET(req: NextRequest) { "WHERE c.productId = @pid AND c.status = 'succeeded' " + 'AND c.paidAt >= @since', parameters: [ - { name: '@pid', value: PRODUCT_ID }, + { name: '@pid', value: productId }, { name: '@since', value: sinceDate }, ], }) diff --git a/dashboards/admin-web/src/app/api/seed/route.ts b/dashboards/admin-web/src/app/api/seed/route.ts index b16ca133..a70cac43 100644 --- a/dashboards/admin-web/src/app/api/seed/route.ts +++ b/dashboards/admin-web/src/app/api/seed/route.ts @@ -11,7 +11,7 @@ import { logError } from '@/lib/logger'; import { initializeAllContainers } from '@/lib/cosmos'; import { hashPassword } from '@/lib/auth-server'; import { getUserByEmail, createUser } from '@/lib/repositories/users'; -import { PRODUCT_ID } from '@/lib/product-config'; +import { getRequestProductId } from '@/lib/product-config'; export async function POST(req: NextRequest) { try { const seedSecret = process.env.SEED_SECRET; @@ -25,13 +25,14 @@ export async function POST(req: NextRequest) { // 1. Create all Cosmos DB containers await initializeAllContainers(); // 2. Create default admin user if not exists + const productId = getRequestProductId(req); const adminEmail = 'admin@example.com'; - const existing = await getUserByEmail(adminEmail); + const existing = await getUserByEmail(adminEmail, productId); if (!existing) { const now = new Date().toISOString(); await createUser({ id: `usr_admin_${Date.now()}`, - productId: PRODUCT_ID, + productId, email: adminEmail, name: 'Admin User', passwordHash: await hashPassword('admin123'), @@ -47,12 +48,12 @@ export async function POST(req: NextRequest) { } // 3. Create viewer user if not exists const viewerEmail = 'viewer@example.com'; - const existingViewer = await getUserByEmail(viewerEmail); + const existingViewer = await getUserByEmail(viewerEmail, productId); if (!existingViewer) { const now = new Date().toISOString(); await createUser({ id: `usr_viewer_${Date.now()}`, - productId: PRODUCT_ID, + productId, email: viewerEmail, name: 'Viewer User', passwordHash: await hashPassword('viewer123'), diff --git a/dashboards/admin-web/src/app/api/settings/plans/route.ts b/dashboards/admin-web/src/app/api/settings/plans/route.ts index 49a17269..361cd1d5 100644 --- a/dashboards/admin-web/src/app/api/settings/plans/route.ts +++ b/dashboards/admin-web/src/app/api/settings/plans/route.ts @@ -7,7 +7,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { requireAdmin } from '@/lib/auth-server'; import { listPlans } from '@/lib/platform-client'; -import { PRODUCT_ID } from '@/lib/product-config'; +import { getRequestProductId } from '@/lib/product-config'; import { logError } from '@/lib/logger'; export async function GET(req: NextRequest) { @@ -15,7 +15,7 @@ export async function GET(req: NextRequest) { const admin = await requireAdmin(req); if (!admin) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); - const result = await listPlans(PRODUCT_ID); + const result = await listPlans(getRequestProductId(req)); return NextResponse.json(result); } catch (error) { logError('Plans list error', error); diff --git a/dashboards/admin-web/src/app/api/themes/active/route.ts b/dashboards/admin-web/src/app/api/themes/active/route.ts index 07bd1f1d..fa664043 100644 --- a/dashboards/admin-web/src/app/api/themes/active/route.ts +++ b/dashboards/admin-web/src/app/api/themes/active/route.ts @@ -1,11 +1,11 @@ -import { NextResponse } from 'next/server'; +import { NextRequest, NextResponse } from 'next/server'; import { logError } from '@/lib/logger'; import { getActiveTheme } from '@/lib/platform-client'; -import { PRODUCT_ID } from '@/lib/product-config'; +import { getRequestProductId } from '@/lib/product-config'; -export async function GET() { +export async function GET(req: NextRequest) { try { - const theme = await getActiveTheme(PRODUCT_ID); + const theme = await getActiveTheme(getRequestProductId(req)); return NextResponse.json(theme); } catch (error) { logError('Failed to get active theme', error); @@ -14,9 +14,40 @@ export async function GET() { id: 'default', name: 'Default Green', description: 'Default green theme', - ios: { primary: '#4caf50', secondary: '#2e7d32', accent: '#66bb6a', background: '#ffffff', surface: '#f5f5f5', error: '#f44336', warning: '#ff9800', success: '#4caf50' }, - android: { primary: '#4caf50', secondary: '#2e7d32', accent: '#66bb6a', background: '#ffffff', surface: '#f5f5f5', error: '#f44336', warning: '#ff9800', success: '#4caf50' }, - desktop: { primary: '#4caf50', secondary: '#2e7d32', accent: '#66bb6a', background: '#ffffff', surface: '#f5f5f5', error: '#f44336', warning: '#ff9800', success: '#4caf50', idle: '#4caf50', listening: '#e94560', processing: '#f5a623', offline: '#9e9e9e' }, + ios: { + primary: '#4caf50', + secondary: '#2e7d32', + accent: '#66bb6a', + background: '#ffffff', + surface: '#f5f5f5', + error: '#f44336', + warning: '#ff9800', + success: '#4caf50', + }, + android: { + primary: '#4caf50', + secondary: '#2e7d32', + accent: '#66bb6a', + background: '#ffffff', + surface: '#f5f5f5', + error: '#f44336', + warning: '#ff9800', + success: '#4caf50', + }, + desktop: { + primary: '#4caf50', + secondary: '#2e7d32', + accent: '#66bb6a', + background: '#ffffff', + surface: '#f5f5f5', + error: '#f44336', + warning: '#ff9800', + success: '#4caf50', + idle: '#4caf50', + listening: '#e94560', + processing: '#f5a623', + offline: '#9e9e9e', + }, is_active: true, is_default: true, version: '1.0', diff --git a/dashboards/admin-web/src/app/providers.tsx b/dashboards/admin-web/src/app/providers.tsx index f1a5be87..0528d4c9 100644 --- a/dashboards/admin-web/src/app/providers.tsx +++ b/dashboards/admin-web/src/app/providers.tsx @@ -4,6 +4,7 @@ import { useEffect, type ReactNode } from 'react'; import { AuthProvider } from '@/lib/auth-context'; import { ThemeProvider } from '@/lib/theme-context'; import { StripeConfigProvider } from '@/lib/stripe-context'; +import { ProductProvider } from '@/lib/product-context'; import { ToastProvider } from '@/components/ui/toast'; import { CSPostHogProvider } from '@/components/posthog-provider'; import { initTelemetry } from '@/lib/telemetry'; @@ -16,11 +17,13 @@ export function Providers({ children }: { children: ReactNode }) { return ( - - - {children} - - + + + + {children} + + + ); diff --git a/dashboards/admin-web/src/components/product-switcher.tsx b/dashboards/admin-web/src/components/product-switcher.tsx new file mode 100644 index 00000000..df27c2bc --- /dev/null +++ b/dashboards/admin-web/src/components/product-switcher.tsx @@ -0,0 +1,48 @@ +'use client'; + +import { useProduct } from '@/lib/product-context'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { Mic, Clock, Apple, Brain, type LucideIcon } from 'lucide-react'; + +const iconMap: Record = { + Mic, + Clock, + Apple, + Brain, +}; + +export function ProductSwitcher() { + const { productId, setProductId, products } = useProduct(); + + return ( +
+ + +
+ ); +} diff --git a/dashboards/admin-web/src/components/sidebar-nav.tsx b/dashboards/admin-web/src/components/sidebar-nav.tsx index 3ca7ed0a..24c4a5ef 100644 --- a/dashboards/admin-web/src/components/sidebar-nav.tsx +++ b/dashboards/admin-web/src/components/sidebar-nav.tsx @@ -32,6 +32,7 @@ import { import { cn } from '@/lib/utils'; import { useAuth } from '@/lib/auth-context'; import { useTheme } from '@/lib/theme-context'; +import { ProductSwitcher } from '@/components/product-switcher'; const navItems = [ { href: '/', label: 'Dashboard', icon: LayoutDashboard }, @@ -100,6 +101,9 @@ export function SidebarNav() { + {/* Product Switcher */} + + {/* Navigation */}