feat(admin): add product switcher — filter all data by any product
- 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
This commit is contained in:
parent
ba2641c552
commit
ed7fa3f9a4
@ -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 },
|
||||
],
|
||||
})
|
||||
|
||||
@ -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'),
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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 (
|
||||
<CSPostHogProvider>
|
||||
<ThemeProvider>
|
||||
<AuthProvider>
|
||||
<StripeConfigProvider>
|
||||
<ToastProvider>{children}</ToastProvider>
|
||||
</StripeConfigProvider>
|
||||
</AuthProvider>
|
||||
<ProductProvider>
|
||||
<AuthProvider>
|
||||
<StripeConfigProvider>
|
||||
<ToastProvider>{children}</ToastProvider>
|
||||
</StripeConfigProvider>
|
||||
</AuthProvider>
|
||||
</ProductProvider>
|
||||
</ThemeProvider>
|
||||
</CSPostHogProvider>
|
||||
);
|
||||
|
||||
48
dashboards/admin-web/src/components/product-switcher.tsx
Normal file
48
dashboards/admin-web/src/components/product-switcher.tsx
Normal file
@ -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<string, LucideIcon> = {
|
||||
Mic,
|
||||
Clock,
|
||||
Apple,
|
||||
Brain,
|
||||
};
|
||||
|
||||
export function ProductSwitcher() {
|
||||
const { productId, setProductId, products } = useProduct();
|
||||
|
||||
return (
|
||||
<div className="px-3 py-2">
|
||||
<label className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground px-3 mb-1 block">
|
||||
Product
|
||||
</label>
|
||||
<Select value={productId} onValueChange={setProductId}>
|
||||
<SelectTrigger className="w-full h-9 text-sm">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{products.map(p => {
|
||||
const Icon = iconMap[p.icon];
|
||||
return (
|
||||
<SelectItem key={p.id} value={p.id}>
|
||||
<span className="flex items-center gap-2">
|
||||
{Icon && <Icon className="h-3.5 w-3.5" />}
|
||||
{p.name}
|
||||
</span>
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -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() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Product Switcher */}
|
||||
<ProductSwitcher />
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className="flex-1 space-y-1 px-3 py-4">
|
||||
{navItems.map(item => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user