- 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
785 lines
22 KiB
TypeScript
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();
|
|
}
|