/** * API client for the admin dashboard. * Fetches from Next.js API routes which connect directly to Cosmos DB. * Falls back gracefully when API is unavailable (e.g., no Cosmos configured). */ const API_BASE = '/api'; function getBaseUrl(): string { if (typeof window === 'undefined') return ''; return window.location.origin; } function getAuthHeaders(): HeadersInit { if (typeof window === 'undefined') return {}; const headers: Record = {}; const token = localStorage.getItem('admin_access_token'); if (token) headers['Authorization'] = `Bearer ${token}`; const productId = localStorage.getItem('admin_selected_product'); if (productId) headers['x-product-id'] = productId; return headers; } export async function apiFetch( path: string, options: RequestInit = {} ): Promise<{ data: T | null; error: string | null }> { try { const res = await fetch(`${API_BASE}${path}`, { ...options, headers: { 'Content-Type': 'application/json', ...getAuthHeaders(), ...options.headers, }, }); if (!res.ok) { const body = await res.json().catch(() => ({ error: res.statusText })); return { data: null, error: body.error || `HTTP ${res.status}` }; } const data = await res.json(); return { data, error: null }; } catch { return { data: null, error: 'API unavailable' }; } } // ── Auth ──────────────────────────────────────────────────────────── export interface LoginResponse { accessToken: string; refreshToken: string; user: { id: string; email: string; name: string; role: string; plan: string; }; } export async function apiLogin(email: string, password: string) { return apiFetch('/auth/login', { method: 'POST', body: JSON.stringify({ email, password }), }); } export async function apiGetMe() { return apiFetch<{ id: string; email: string; name: string; role: string; plan: string; status: string; }>('/auth/me'); } // ── Users ─────────────────────────────────────────────────────────── export interface ApiUser { id: string; email: string; name: string; role: string; plan: string; status: string; createdAt: string; lastActive: string; totalTokensUsed: number; totalRequests: number; monthlySpend: number; } export async function apiListUsers(limit = 100, offset = 0) { return apiFetch<{ users: ApiUser[]; total: number; byPlan: Record }>( `/users?limit=${limit}&offset=${offset}` ); } export async function apiCreateUser(body: { email: string; name: string; password: string; role?: string; plan?: string; }) { return apiFetch('/users', { method: 'POST', body: JSON.stringify(body), }); } export async function apiUpdateUser(id: string, body: Record) { return apiFetch(`/users/${id}`, { method: 'PUT', body: JSON.stringify(body), }); } export async function apiDeleteUser(id: string) { return apiFetch<{ success: boolean }>(`/users/${id}`, { method: 'DELETE' }); } // ── Tokens ────────────────────────────────────────────────────────── export interface ApiToken { id: string; userId: string; userName: string; name: string; prefix: string; status: string; scopes: string[]; createdAt: string; expiresAt: string; lastUsed: string | null; } export async function apiListTokens() { return apiFetch<{ tokens: ApiToken[] }>('/tokens'); } export async function apiCreateToken(body: { name: string; scopes?: string[]; expiresInDays?: number; }) { return apiFetch('/tokens', { method: 'POST', body: JSON.stringify(body), }); } export async function apiRevokeToken(id: string) { return apiFetch<{ success: boolean }>(`/tokens/${id}`, { method: 'PATCH', body: JSON.stringify({ action: 'revoke' }), }); } export async function apiDeleteToken(id: string) { return apiFetch<{ success: boolean }>(`/tokens/${id}`, { method: 'DELETE' }); } // ── Subscriptions / Plans ─────────────────────────────────────────── export interface PlanDoc { id: string; productId: string; name: string; displayName: string; price: number; tokens: number; words: number; dictations: number; features: string[]; stripePriceId?: string; active: boolean; createdAt: string; updatedAt: string; } export async function apiListPlans() { return apiFetch<{ plans: PlanDoc[] }>('/settings/plans'); } export interface PlanUpdate { price?: number; tokensPerMonth?: number; requestsPerDay?: number; } export async function apiUpdatePlan(planId: string, body: PlanUpdate) { return apiFetch<{ success: boolean; plan: string }>(`/settings/plans/${planId}`, { method: 'PUT', body: JSON.stringify(body), }); } export interface NewPlan { name: string; price: number; tokensPerMonth: number; requestsPerDay: number; yearlyDiscount?: boolean; } export async function apiCreatePlan(body: NewPlan) { return apiFetch<{ success: boolean; plan: string }>('/settings/plans', { method: 'POST', body: JSON.stringify(body), }); } // ── Revenue Analytics ─────────────────────────────────────────────── export interface RevenueAnalytics { mrr: number; arr: number; mrrChange: number; totalRevenue: number; revenueByMonth: Array<{ month: string; revenue: number; subscriptions: number }>; churnRate: number; churnCount: number; ltv: number; arpu: number; newSubscriptions: number; canceledSubscriptions: number; } export async function apiGetRevenueAnalytics(months = 6) { return apiFetch(`/analytics/revenue?months=${months}`); } // ── Usage ─────────────────────────────────────────────────────────── export interface ApiUsageRecord { id: string; userId: string; date: string; dictations: number; words: number; durationMs: number; tokensUsed: number; costUsd: number; model?: string; source?: string; } export interface ApiModelBreakdown { model: string; tokens: number; requests: number; cost: number; } export interface ApiSourceBreakdown { source: string; tokens: number; requests: number; cost: number; } export interface ApiProductBreakdown { productId: string; tokens: number; requests: number; cost: number; } export async function apiGetUsage(days = 30, userId?: string, productId?: string) { const params = new URLSearchParams({ days: String(days) }); if (userId) params.set('userId', userId); if (productId) params.set('productId', productId); return apiFetch<{ records?: ApiUsageRecord[]; totalWords?: number; totalDictations?: number; totalTokens?: number; totalCost?: number; }>(`/usage?${params}`); } export async function apiGetUsageSummary(days = 30, userId?: string, productId?: string) { const params = new URLSearchParams({ days: String(days), summary: 'true' }); if (userId) params.set('userId', userId); if (productId) params.set('productId', productId); return apiFetch<{ totalWords: number; totalDictations: number; totalTokens: number; totalCost: number; records: ApiUsageRecord[]; modelBreakdown: ApiModelBreakdown[]; sourceBreakdown?: ApiSourceBreakdown[]; productBreakdown?: ApiProductBreakdown[]; }>(`/usage?${params}`); } // ── Audit ─────────────────────────────────────────────────────────── export interface ApiAuditEntry { id: string; category: string; action: string; actor: string; target: string; ip: string; timestamp: string; } export async function apiGetAudit(category?: string, limit = 100) { const params = new URLSearchParams({ limit: String(limit) }); if (category) params.set('category', category); return apiFetch<{ entries: ApiAuditEntry[]; total: number }>(`/audit?${params}`); } export async function apiGetAuditSummary() { return apiFetch<{ total: number; failedLogins: number }>('/audit?summary=true'); } // ── Dashboard Stats ───────────────────────────────────────────────── export interface DashboardStats { users: { total: number; byPlan: Record }; tokens: { active: number }; usage: { totalWords: number; totalDictations: number; totalCost: number }; audit: { total: number; failedLogins: number }; } export async function apiGetDashboardStats() { return apiFetch('/dashboard/stats'); } // ── Invitations ──────────────────────────────────────────────────── export interface ApiInvitation { id: string; code: string; description: string; createdBy: string; grantPlan: 'pro' | 'enterprise'; grantTrialDays: number; bonusTokens: number; maxUses: number; currentUses: number; redeemedBy: string[]; status: 'active' | 'expired' | 'disabled'; expiresAt: string | null; createdAt: string; updatedAt: string; } export async function apiListInvitations(limit = 100, offset = 0) { return apiFetch<{ codes: ApiInvitation[]; total: number }>( `/invitations?limit=${limit}&offset=${offset}` ); } export async function apiCreateInvitation(body: { code?: string; description?: string; grantPlan?: string; grantTrialDays?: number; bonusTokens?: number; maxUses?: number; expiresAt?: string | null; }) { return apiFetch('/invitations', { method: 'POST', body: JSON.stringify(body), }); } export async function apiUpdateInvitation(id: string, body: Record) { return apiFetch(`/invitations/${id}`, { method: 'PATCH', body: JSON.stringify(body), }); } export async function apiDeleteInvitation(id: string) { return apiFetch<{ success: boolean }>(`/invitations/${id}`, { method: 'DELETE' }); } export interface BulkInviteResult { total: number; created: number; failed: number; invitations: ApiInvitation[]; errors: { index: number; error: string }[]; } export async function apiBulkCreateInvitations( invitations: Array<{ code: string; description?: string; createdBy: string; grantPlan: string; grantTrialDays?: number; bonusTokens?: number; maxUses?: number; expiresAt?: string | null; }> ) { return apiFetch('/invitations/bulk', { method: 'POST', body: JSON.stringify(invitations), }); } // ── Promos (Stripe) ──────────────────────────────────────────────── export interface ApiPromo { id: string; code: string; active: boolean; couponId: string; percentOff: number | null; amountOff: number | null; currency: string | null; duration: string | null; timesRedeemed: number; maxRedemptions: number | null; expiresAt: string | null; created: string; metadata: Record; } export async function apiListPromos(active?: boolean) { const params = active !== undefined ? `?active=${active}` : ''; return apiFetch<{ promos: ApiPromo[] }>(`/promos${params}`); } export async function apiCreatePromo(body: { code: string; percentOff?: number; amountOff?: number; currency?: string; duration?: string; durationInMonths?: number; maxRedemptions?: number; expiresAt?: string; }) { return apiFetch('/promos', { method: 'POST', body: JSON.stringify(body), }); } export async function apiDeletePromo(id: string) { return apiFetch<{ success: boolean }>(`/promos/${id}`, { method: 'DELETE' }); } export async function apiUpdatePromo(id: string, body: { active?: boolean }) { return apiFetch(`/promos/${id}`, { method: 'PATCH', body: JSON.stringify(body), }); } // ── Referrals ────────────────────────────────────────────────────── export interface ApiReferral { id: string; referrerId: string; referrerEmail: string; referredUserId: string | null; referredEmail: string; status: 'pending' | 'signed_up' | 'subscribed' | 'rewarded'; referrerRewardTokens: number; referredRewardTokens: number; referrerRewarded: boolean; referredRewarded: boolean; createdAt: string; completedAt: string | null; } export interface ReferralStats { total: number; completed: number; rewarded: number; } export async function apiListReferrals(limit = 100, offset = 0) { return apiFetch<{ referrals: ApiReferral[]; stats: ReferralStats }>( `/referrals?limit=${limit}&offset=${offset}` ); } export async function apiGetReferralStats() { return apiFetch('/referrals?mode=summary'); } // ── Cohort Retention Analysis ──────────────────────────────────────── export interface RetentionCohort { cohortWeek: string; cohortStart: string; signups: number; retained7d: number; retained14d: number; retained30d: number; rate7d: number; rate14d: number; rate30d: number; } export async function apiGetRetention(weeks = 8) { return apiFetch<{ cohorts: RetentionCohort[]; totalUsers: number; weeksAnalyzed: number; }>(`/analytics/retention?weeks=${weeks}`); } // ── User Impersonation (read-only view) ───────────────────────────── export interface UserViewResponse { profile: ApiUser & { role: string; productId: string }; usage: { records: ApiUsageRecord[]; totalWords?: number; totalDictations?: number; totalTokens?: number; totalCost?: number; }; viewedBy: { id: string; name: string; role: string }; viewedAt: string; } export async function apiGetUserView(userId: string) { return apiFetch(`/users/${userId}/view`); } // ── Seed ──────────────────────────────────────────────────────────── export async function apiSeed(secret: string) { return apiFetch<{ success: boolean; message: string }>(`/seed?secret=${secret}`, { method: 'POST', }); } // ── Broadcasts ───────────────────────────────────────────────────── export interface ApiBroadcast { id: string; title: string; body: string; bodyMarkdown?: string; ctaText?: string; ctaUrl?: string; imageUrl?: string; target: { userSegments?: string[]; platforms?: string[]; appVersionMin?: string; appVersionMax?: string; countryCodes?: string[]; regionCodes?: string[]; osVersionMin?: string; osVersionMax?: string; percentageRollout?: number; specificUserIds?: string[]; }; channels: ('push' | 'in_app' | 'email')[]; status: 'draft' | 'scheduled' | 'sending' | 'sent' | 'paused'; scheduledAt?: string; sentAt?: string; variant?: 'control' | 'treatment'; experimentId?: string; parentBroadcastId?: string; metrics: { targetedCount: number; sentCount: number; deliveredCount: number; openedCount: number; clickedCount: number; dismissedCount: number; convertedCount: number; }; createdAt: string; updatedAt: string; createdBy: string; } export async function apiListBroadcasts(status?: ApiBroadcast['status']) { const params = status ? `?status=${status}` : ''; return apiFetch<{ broadcasts: ApiBroadcast[]; total: number }>(`/admin/broadcasts${params}`); } export async function apiGetBroadcast(id: string) { return apiFetch(`/admin/broadcasts/${id}`); } export async function apiCreateBroadcast( body: Omit ) { return apiFetch('/admin/broadcasts', { method: 'POST', body: JSON.stringify(body), }); } export async function apiUpdateBroadcast(id: string, body: Partial) { return apiFetch(`/admin/broadcasts/${id}`, { method: 'PUT', body: JSON.stringify(body), }); } export async function apiDeleteBroadcast(id: string) { return apiFetch<{ success: boolean }>(`/admin/broadcasts/${id}`, { method: 'DELETE' }); } export async function apiCloneBroadcast(id: string, variant?: 'control' | 'treatment') { return apiFetch(`/admin/broadcasts/${id}/clone`, { method: 'POST', body: JSON.stringify({ variant }), }); } export async function apiSendBroadcast(id: string) { return apiFetch<{ success: boolean }>(`/admin/broadcasts/${id}/send`, { method: 'POST' }); } export async function apiPauseBroadcast(id: string) { return apiFetch<{ success: boolean }>(`/admin/broadcasts/${id}/pause`, { method: 'POST' }); } export async function apiGetBroadcastMetrics(id: string) { return apiFetch<{ broadcastId: string; metrics: ApiBroadcast['metrics']; status: ApiBroadcast['status']; }>(`/admin/broadcasts/${id}/metrics`); } export async function apiEstimateBroadcastReach(target: ApiBroadcast['target']) { return apiFetch<{ estimatedCount: number; targetBreakdown: { userSegments: Record; platforms: Record; countries: Record; }; sampleUserIds: string[]; }>('/admin/broadcasts/estimate-reach', { method: 'POST', body: JSON.stringify(target), }); } // ── Surveys ──────────────────────────────────────────────────────── export interface ApiSurvey { id: string; title: string; description?: string; questions: { id: string; type: | 'single_choice' | 'multiple_choice' | 'rating' | 'nps' | 'text_short' | 'text_long' | 'dropdown' | 'scale' | 'ranking'; text: string; description?: string; required: boolean; options?: { id: string; text: string; emoji?: string }[]; showIf?: unknown; minLength?: number; maxLength?: number; minValue?: number; maxValue?: number; }[]; target: ApiBroadcast['target']; status: 'draft' | 'active' | 'paused' | 'closed'; startsAt?: string; endsAt?: string; displayTrigger: | { type: 'immediate' } | { type: 'delay_seconds'; seconds: number } | { type: 'event'; eventName: string } | { type: 'page_view'; pagePattern: string }; incentive?: { type: 'pro_days' | 'credits'; amount: number }; metrics: { impressions: number; starts: number; completions: number; avgTimeSeconds: number; incentiveClaims: number; }; createdAt: string; updatedAt: string; createdBy: string; } export interface ApiSurveyResponse { id: string; surveyId: string; userId: string; answers: Record; currentQuestionIndex: number; startedAt: string; completedAt?: string; isComplete: boolean; incentiveClaimed: boolean; createdAt: string; } export interface ApiSurveyAnalytics { totalStarts: number; totalCompletions: number; completionRate: number; avgTimeSeconds: number; questionAnalytics: { questionId: string; questionType: string; totalResponses: number; optionCounts?: Record; average?: number; min?: number; max?: number; distribution?: Record; sampleResponses?: string[]; }[]; computedMetrics: { completionRate: number; }; } export async function apiListSurveys(status?: ApiSurvey['status']) { const params = status ? `?status=${status}` : ''; return apiFetch<{ surveys: ApiSurvey[]; total: number }>(`/admin/surveys${params}`); } export async function apiGetSurvey(id: string) { return apiFetch(`/admin/surveys/${id}`); } export async function apiCreateSurvey( body: Omit ) { return apiFetch('/admin/surveys', { method: 'POST', body: JSON.stringify(body), }); } export async function apiUpdateSurvey(id: string, body: Partial) { return apiFetch(`/admin/surveys/${id}`, { method: 'PUT', body: JSON.stringify(body), }); } export async function apiDeleteSurvey(id: string) { return apiFetch<{ success: boolean }>(`/admin/surveys/${id}`, { method: 'DELETE' }); } export async function apiDuplicateSurvey(id: string) { return apiFetch(`/admin/surveys/${id}/duplicate`, { method: 'POST' }); } export async function apiPauseSurvey(id: string) { return apiFetch<{ success: boolean }>(`/admin/surveys/${id}/pause`, { method: 'POST' }); } export async function apiGetSurveyResponses( id: string, options?: { isComplete?: boolean; limit?: number; offset?: number } ) { const params = new URLSearchParams(); if (options?.isComplete !== undefined) params.set('isComplete', String(options.isComplete)); if (options?.limit) params.set('limit', String(options.limit)); if (options?.offset) params.set('offset', String(options.offset)); return apiFetch<{ responses: ApiSurveyResponse[]; total: number }>( `/admin/surveys/${id}/responses?${params}` ); } export async function apiGetSurveyRespondents(id: string) { return apiFetch<{ userIds: string[]; count: number }>(`/admin/surveys/${id}/respondents`); } export async function apiGetSurveyAnalytics(id: string) { return apiFetch(`/admin/surveys/${id}/analytics`); } export async function apiExportSurveyCSV(id: string): Promise { const res = await fetch(`${getBaseUrl()}/admin/surveys/${id}/export.csv`, { headers: getAuthHeaders(), }); if (!res.ok) throw new Error(`Export failed: ${res.status}`); return res.text(); }