learning_ai_common_plat/dashboards/admin-web/src/lib/api.ts
saravanakumardb1 ee3b23711f feat(admin): Phase 2.1 - Broadcast and Survey list pages
- broadcasts/page.tsx: List, filter, delete, send, pause, clone actions
- surveys/page.tsx: List, filter, delete, pause, duplicate, analytics, CSV export
- api.ts: Add all broadcast and survey API functions with types
- sidebar-nav.tsx: Add navigation items with Megaphone and ClipboardList icons
2026-03-03 07:21:37 -08:00

785 lines
22 KiB
TypeScript

/**
* 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<string, string> = {};
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<T>(
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<LoginResponse>('/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<string, number> }>(
`/users?limit=${limit}&offset=${offset}`
);
}
export async function apiCreateUser(body: {
email: string;
name: string;
password: string;
role?: string;
plan?: string;
}) {
return apiFetch<ApiUser>('/users', {
method: 'POST',
body: JSON.stringify(body),
});
}
export async function apiUpdateUser(id: string, body: Record<string, unknown>) {
return apiFetch<ApiUser>(`/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<ApiToken & { rawToken: string }>('/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<RevenueAnalytics>(`/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<string, number> };
tokens: { active: number };
usage: { totalWords: number; totalDictations: number; totalCost: number };
audit: { total: number; failedLogins: number };
}
export async function apiGetDashboardStats() {
return apiFetch<DashboardStats>('/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<ApiInvitation>('/invitations', {
method: 'POST',
body: JSON.stringify(body),
});
}
export async function apiUpdateInvitation(id: string, body: Record<string, unknown>) {
return apiFetch<ApiInvitation>(`/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<BulkInviteResult>('/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<string, string>;
}
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<ApiPromo>('/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<ApiPromo>(`/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<ReferralStats>('/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<UserViewResponse>(`/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<ApiBroadcast>(`/admin/broadcasts/${id}`);
}
export async function apiCreateBroadcast(body: Omit<ApiBroadcast, 'id' | 'metrics' | 'createdAt' | 'updatedAt' | 'createdBy'>) {
return apiFetch<ApiBroadcast>('/admin/broadcasts', {
method: 'POST',
body: JSON.stringify(body),
});
}
export async function apiUpdateBroadcast(id: string, body: Partial<ApiBroadcast>) {
return apiFetch<ApiBroadcast>(`/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<ApiBroadcast>(`/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<string, number>;
platforms: Record<string, number>;
countries: Record<string, number>;
};
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<string, unknown>;
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<string, number>;
average?: number;
min?: number;
max?: number;
distribution?: Record<number, number>;
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<ApiSurvey>(`/admin/surveys/${id}`);
}
export async function apiCreateSurvey(body: Omit<ApiSurvey, 'id' | 'metrics' | 'createdAt' | 'updatedAt' | 'createdBy'>) {
return apiFetch<ApiSurvey>('/admin/surveys', {
method: 'POST',
body: JSON.stringify(body),
});
}
export async function apiUpdateSurvey(id: string, body: Partial<ApiSurvey>) {
return apiFetch<ApiSurvey>(`/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<ApiSurvey>(`/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<ApiSurveyAnalytics>(`/admin/surveys/${id}/analytics`);
}
export async function apiExportSurveyCSV(id: string): Promise<string> {
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();
}