1379 lines
44 KiB
TypeScript
1379 lines
44 KiB
TypeScript
/**
|
|
* Admin dashboard API (001-admin-dashboard)
|
|
*/
|
|
import { apiClient } from "@/lib/api";
|
|
|
|
export interface AdminMeResponse {
|
|
ok: boolean;
|
|
}
|
|
|
|
export interface AdminTool {
|
|
id: string;
|
|
slug: string;
|
|
category: string;
|
|
name: string;
|
|
description: string | null;
|
|
accessLevel: string;
|
|
countsAsOperation: boolean;
|
|
isActive: boolean;
|
|
dockerService: string | null;
|
|
processingType: string | null;
|
|
metaTitle: string | null;
|
|
metaDescription: string | null;
|
|
nameLocalized: Record<string, string> | null;
|
|
descriptionLocalized: Record<string, string> | null;
|
|
metaTitleLocalized: Record<string, string> | null;
|
|
metaDescriptionLocalized: Record<string, string> | null;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
}
|
|
|
|
export interface AdminToolsListResponse {
|
|
items: AdminTool[];
|
|
total: number;
|
|
page: number;
|
|
limit: number;
|
|
categories?: string[];
|
|
}
|
|
|
|
export interface AdminToolPatchBody {
|
|
accessLevel?: string;
|
|
isActive?: boolean;
|
|
name?: string;
|
|
description?: string | null;
|
|
metaTitle?: string | null;
|
|
metaDescription?: string | null;
|
|
nameLocalized?: Record<string, string> | null;
|
|
descriptionLocalized?: Record<string, string> | null;
|
|
metaTitleLocalized?: Record<string, string> | null;
|
|
metaDescriptionLocalized?: Record<string, string> | null;
|
|
countsAsOperation?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Check if the current user has admin access.
|
|
* GET /api/v1/admin/me returns 200 with { success: true, data: { ok: true } } if admin, 403 otherwise.
|
|
*/
|
|
export async function checkAdmin(): Promise<boolean> {
|
|
try {
|
|
const response = await apiClient.get<AdminMeResponse>("/api/v1/admin/me");
|
|
const payload = response.data?.data;
|
|
return payload?.ok === true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export interface AdminToolCategoriesResponse {
|
|
categories: string[];
|
|
}
|
|
|
|
export interface AdminToolsStats {
|
|
total: number;
|
|
active: number;
|
|
inactive: number;
|
|
byCategory: Record<string, number>;
|
|
}
|
|
|
|
export async function getAdminToolsStats(): Promise<AdminToolsStats> {
|
|
const response = await apiClient.get<AdminToolsStats>("/api/v1/admin/tools/stats");
|
|
return response.data?.data!;
|
|
}
|
|
|
|
export async function getAdminToolCategories(): Promise<string[]> {
|
|
const response = await apiClient.get<AdminToolCategoriesResponse>(
|
|
"/api/v1/admin/tools/categories"
|
|
);
|
|
const data = response.data?.data;
|
|
return data?.categories ?? [];
|
|
}
|
|
|
|
export async function listAdminTools(params?: {
|
|
page?: number;
|
|
limit?: number;
|
|
search?: string;
|
|
category?: string;
|
|
accessLevel?: string;
|
|
isActive?: boolean;
|
|
}): Promise<AdminToolsListResponse> {
|
|
const query: Record<string, string> = {};
|
|
if (params?.page != null) query.page = String(params.page);
|
|
if (params?.limit != null) query.limit = String(params.limit);
|
|
const searchTrimmed = params?.search?.trim();
|
|
if (searchTrimmed) query.search = searchTrimmed;
|
|
if (params?.category != null && params.category !== "")
|
|
query.category = params.category;
|
|
if (params?.accessLevel != null && params.accessLevel !== "")
|
|
query.accessLevel = params.accessLevel;
|
|
if (params?.isActive !== undefined)
|
|
query.isActive = String(params.isActive);
|
|
const searchParams = new URLSearchParams(query);
|
|
const q = searchParams.toString();
|
|
const url = `/api/v1/admin/tools${q ? `?${q}` : ""}`;
|
|
const response = await apiClient.get<AdminToolsListResponse>(url);
|
|
return response.data?.data!;
|
|
}
|
|
|
|
export async function getAdminTool(id: string): Promise<AdminTool> {
|
|
const response = await apiClient.get<AdminTool>(`/api/v1/admin/tools/${id}`);
|
|
return response.data?.data!;
|
|
}
|
|
|
|
export async function patchAdminTool(
|
|
id: string,
|
|
body: AdminToolPatchBody
|
|
): Promise<AdminTool> {
|
|
const response = await apiClient.patch<AdminTool>(
|
|
`/api/v1/admin/tools/${id}`,
|
|
body
|
|
);
|
|
return response.data?.data!;
|
|
}
|
|
|
|
// --- Users (US2, Step 05) ---
|
|
export interface AdminUser {
|
|
id: string;
|
|
email: string;
|
|
name: string | null;
|
|
tier: string;
|
|
accountStatus: string;
|
|
emailVerified?: boolean;
|
|
lastLoginAt: string | null;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
totalOperations?: number;
|
|
totalSpent?: number;
|
|
preferredLocale?: string | null;
|
|
}
|
|
|
|
export interface AdminUsersListResponse {
|
|
items: AdminUser[];
|
|
total: number;
|
|
page: number;
|
|
limit: number;
|
|
}
|
|
|
|
export async function listAdminUsers(params?: {
|
|
page?: number;
|
|
limit?: number;
|
|
email?: string;
|
|
tier?: string;
|
|
accountStatus?: string;
|
|
from?: string;
|
|
to?: string;
|
|
emailVerified?: "true" | "false";
|
|
activeWithinDays?: number;
|
|
}): Promise<AdminUsersListResponse> {
|
|
const searchParams = new URLSearchParams();
|
|
if (params?.page != null) searchParams.set("page", String(params.page));
|
|
if (params?.limit != null) searchParams.set("limit", String(params.limit));
|
|
if (params?.email != null && params.email !== "")
|
|
searchParams.set("email", params.email);
|
|
if (params?.tier != null) searchParams.set("tier", params.tier);
|
|
if (params?.accountStatus != null)
|
|
searchParams.set("accountStatus", params.accountStatus);
|
|
if (params?.from != null) searchParams.set("from", params.from);
|
|
if (params?.to != null) searchParams.set("to", params.to);
|
|
if (params?.emailVerified != null) searchParams.set("emailVerified", params.emailVerified);
|
|
if (params?.activeWithinDays != null)
|
|
searchParams.set("activeWithinDays", String(params.activeWithinDays));
|
|
const q = searchParams.toString();
|
|
const url = `/api/v1/admin/users${q ? `?${q}` : ""}`;
|
|
const response = await apiClient.get<AdminUsersListResponse>(url);
|
|
return response.data?.data!;
|
|
}
|
|
|
|
export async function getAdminUserHistorySubscriptions(userId: string) {
|
|
const r = await apiClient.get(`/api/v1/admin/users/${userId}/history/subscriptions`);
|
|
return r.data?.data ?? [];
|
|
}
|
|
|
|
export async function getAdminUserHistoryPayments(userId: string) {
|
|
const r = await apiClient.get(`/api/v1/admin/users/${userId}/history/payments`);
|
|
return r.data?.data ?? [];
|
|
}
|
|
|
|
export async function getAdminUserHistoryJobs(userId: string, limit = 50) {
|
|
const r = await apiClient.get(`/api/v1/admin/users/${userId}/history/jobs?limit=${limit}`);
|
|
return r.data?.data ?? [];
|
|
}
|
|
|
|
export async function getAdminUserHistoryEmails(userId: string) {
|
|
const r = await apiClient.get(`/api/v1/admin/users/${userId}/history/emails`);
|
|
return r.data?.data ?? [];
|
|
}
|
|
|
|
export interface AdminUserNote {
|
|
id: string;
|
|
userId: string;
|
|
adminId: string;
|
|
note: string;
|
|
createdAt: string;
|
|
}
|
|
|
|
export async function getAdminUserNotes(userId: string): Promise<AdminUserNote[]> {
|
|
const r = await apiClient.get<AdminUserNote[]>(`/api/v1/admin/users/${userId}/notes`);
|
|
return r.data?.data ?? [];
|
|
}
|
|
|
|
export async function addAdminUserNote(userId: string, note: string): Promise<AdminUserNote> {
|
|
const r = await apiClient.post<AdminUserNote>(`/api/v1/admin/users/${userId}/notes`, { note });
|
|
return r.data?.data!;
|
|
}
|
|
|
|
export async function deleteAdminUserNote(userId: string, noteId: string): Promise<void> {
|
|
await apiClient.delete(`/api/v1/admin/users/${userId}/notes/${noteId}`);
|
|
}
|
|
|
|
export async function getAdminUser(id: string): Promise<AdminUser> {
|
|
const response = await apiClient.get<AdminUser>(`/api/v1/admin/users/${id}`);
|
|
return response.data?.data!;
|
|
}
|
|
|
|
export async function patchAdminUser(
|
|
id: string,
|
|
body: { tier?: string; accountStatus?: string }
|
|
): Promise<AdminUser> {
|
|
const response = await apiClient.patch<AdminUser>(
|
|
`/api/v1/admin/users/${id}`,
|
|
body
|
|
);
|
|
return response.data?.data!;
|
|
}
|
|
|
|
// --- Subscriptions (US3) ---
|
|
export interface AdminSubscription {
|
|
id: string;
|
|
userId: string;
|
|
plan: string;
|
|
status: string;
|
|
provider: string;
|
|
providerSubscriptionId?: string | null;
|
|
currentPeriodStart: string | null;
|
|
currentPeriodEnd: string | null;
|
|
cancelledAt: string | null;
|
|
cancelAtPeriodEnd: boolean;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
user?: { id: string; email: string };
|
|
}
|
|
|
|
export interface AdminSubscriptionsListResponse {
|
|
items: AdminSubscription[];
|
|
total: number;
|
|
page: number;
|
|
limit: number;
|
|
}
|
|
|
|
export async function listAdminSubscriptions(params?: {
|
|
page?: number;
|
|
limit?: number;
|
|
status?: string;
|
|
userId?: string;
|
|
}): Promise<AdminSubscriptionsListResponse> {
|
|
const searchParams = new URLSearchParams();
|
|
if (params?.page != null) searchParams.set("page", String(params.page));
|
|
if (params?.limit != null) searchParams.set("limit", String(params.limit));
|
|
if (params?.status != null) searchParams.set("status", params.status);
|
|
if (params?.userId != null) searchParams.set("userId", params.userId);
|
|
const q = searchParams.toString();
|
|
const url = `/api/v1/admin/subscriptions${q ? `?${q}` : ""}`;
|
|
const response = await apiClient.get<AdminSubscriptionsListResponse>(url);
|
|
return response.data?.data!;
|
|
}
|
|
|
|
export interface AdminSubscriptionStats {
|
|
subscriptionTotal: number;
|
|
byStatus: Record<string, number>;
|
|
byPlan: Record<string, number>;
|
|
subscriptionRevenueTotal: number;
|
|
dayPassPurchaseCount: number;
|
|
dayPassRevenueTotal: number;
|
|
dayPassActiveCount: number;
|
|
}
|
|
|
|
export async function getAdminSubscriptionStats(): Promise<AdminSubscriptionStats> {
|
|
const response = await apiClient.get<AdminSubscriptionStats>("/api/v1/admin/subscriptions/stats");
|
|
return response.data?.data!;
|
|
}
|
|
|
|
export async function cancelAdminSubscription(
|
|
id: string,
|
|
effectiveFrom: "next_billing_period" | "immediately" = "next_billing_period"
|
|
): Promise<AdminSubscription> {
|
|
const response = await apiClient.post<AdminSubscription>(
|
|
`/api/v1/admin/subscriptions/${id}/cancel`,
|
|
{ effectiveFrom }
|
|
);
|
|
return response.data?.data!;
|
|
}
|
|
|
|
export async function extendAdminSubscription(
|
|
id: string,
|
|
days: number
|
|
): Promise<AdminSubscription> {
|
|
const response = await apiClient.post<AdminSubscription>(
|
|
`/api/v1/admin/subscriptions/${id}/extend`,
|
|
{ days }
|
|
);
|
|
return response.data?.data!;
|
|
}
|
|
|
|
export async function updateAdminSubscriptionPlan(
|
|
id: string,
|
|
plan: "PREMIUM_MONTHLY" | "PREMIUM_YEARLY"
|
|
): Promise<AdminSubscription> {
|
|
const response = await apiClient.patch<AdminSubscription>(
|
|
`/api/v1/admin/subscriptions/${id}`,
|
|
{ plan }
|
|
);
|
|
return response.data?.data!;
|
|
}
|
|
|
|
// --- Payments / Transactions (US3, Step 03) ---
|
|
export interface AdminPayment {
|
|
id: string;
|
|
userId: string;
|
|
amount: string;
|
|
currency: string;
|
|
status: string;
|
|
type: string;
|
|
provider: string;
|
|
providerPaymentId?: string | null;
|
|
createdAt: string;
|
|
user?: { id: string; email: string };
|
|
}
|
|
|
|
export interface AdminPaymentsListResponse {
|
|
items: AdminPayment[];
|
|
total: number;
|
|
page: number;
|
|
limit: number;
|
|
}
|
|
|
|
export async function listAdminPayments(params?: {
|
|
page?: number;
|
|
limit?: number;
|
|
status?: string;
|
|
type?: string;
|
|
userId?: string;
|
|
from?: string;
|
|
to?: string;
|
|
amountMin?: number;
|
|
amountMax?: number;
|
|
}): Promise<AdminPaymentsListResponse> {
|
|
const searchParams = new URLSearchParams();
|
|
if (params?.page != null) searchParams.set("page", String(params.page));
|
|
if (params?.limit != null) searchParams.set("limit", String(params.limit));
|
|
if (params?.status != null) searchParams.set("status", params.status);
|
|
if (params?.type != null) searchParams.set("type", params.type);
|
|
if (params?.userId != null) searchParams.set("userId", params.userId);
|
|
if (params?.from != null) searchParams.set("from", params.from);
|
|
if (params?.to != null) searchParams.set("to", params.to);
|
|
if (params?.amountMin != null) searchParams.set("amountMin", String(params.amountMin));
|
|
if (params?.amountMax != null) searchParams.set("amountMax", String(params.amountMax));
|
|
const q = searchParams.toString();
|
|
const url = `/api/v1/admin/payments${q ? `?${q}` : ""}`;
|
|
const response = await apiClient.get<AdminPaymentsListResponse>(url);
|
|
return response.data?.data!;
|
|
}
|
|
|
|
export async function getAdminPayment(id: string): Promise<AdminPayment> {
|
|
const response = await apiClient.get<AdminPayment>(`/api/v1/admin/payments/${id}`);
|
|
return response.data?.data!;
|
|
}
|
|
|
|
export async function refundAdminPayment(id: string, reason?: string): Promise<{ adjustmentId: string; status: string }> {
|
|
const response = await apiClient.post<{ adjustmentId: string; status: string }>(
|
|
`/api/v1/admin/payments/${id}/refund`,
|
|
reason ? { reason } : {}
|
|
);
|
|
return response.data?.data!;
|
|
}
|
|
|
|
export async function exportAdminTransactionsCsv(params?: {
|
|
status?: string;
|
|
type?: string;
|
|
from?: string;
|
|
to?: string;
|
|
}): Promise<void> {
|
|
const searchParams = new URLSearchParams();
|
|
if (params?.status) searchParams.set("status", params.status);
|
|
if (params?.type) searchParams.set("type", params.type);
|
|
if (params?.from) searchParams.set("from", params.from);
|
|
if (params?.to) searchParams.set("to", params.to);
|
|
const q = searchParams.toString();
|
|
const url = `/api/v1/admin/export/transactions${q ? `?${q}` : ""}`;
|
|
const response = await apiClient.get<string>(url, { responseType: "text" } as any);
|
|
const csv = typeof response.data === "string" ? response.data : (response.data as any)?.data ?? "";
|
|
const blob = new Blob([csv], { type: "text/csv;charset=utf-8" });
|
|
const blobUrl = URL.createObjectURL(blob);
|
|
const a = document.createElement("a");
|
|
a.href = blobUrl;
|
|
a.download = "transactions.csv";
|
|
a.click();
|
|
URL.revokeObjectURL(blobUrl);
|
|
}
|
|
|
|
export interface AdminPaymentStats {
|
|
totalCount: number;
|
|
completedCount: number;
|
|
totalValue: number;
|
|
byStatus: Record<string, number>;
|
|
byType: Record<string, number>;
|
|
monthly: { month: string; count: number; value: number }[];
|
|
}
|
|
|
|
export async function getAdminPaymentStats(): Promise<AdminPaymentStats> {
|
|
const response = await apiClient.get<AdminPaymentStats>("/api/v1/admin/payments/stats");
|
|
return response.data?.data!;
|
|
}
|
|
|
|
// --- Emails (US4) ---
|
|
export interface AdminEmailLog {
|
|
id: string;
|
|
recipientEmail: string;
|
|
recipientName: string | null;
|
|
emailType: string;
|
|
subject: string;
|
|
status: string;
|
|
errorMessage: string | null;
|
|
errorCode: string | null;
|
|
sentAt: string;
|
|
}
|
|
|
|
export interface AdminEmailsListResponse {
|
|
items: AdminEmailLog[];
|
|
total: number;
|
|
page: number;
|
|
limit: number;
|
|
}
|
|
|
|
export async function listAdminEmails(params?: {
|
|
page?: number;
|
|
limit?: number;
|
|
emailType?: string;
|
|
status?: string;
|
|
from?: string;
|
|
to?: string;
|
|
}): Promise<AdminEmailsListResponse> {
|
|
const searchParams = new URLSearchParams();
|
|
if (params?.page != null) searchParams.set("page", String(params.page));
|
|
if (params?.limit != null) searchParams.set("limit", String(params.limit));
|
|
if (params?.emailType != null) searchParams.set("emailType", params.emailType);
|
|
if (params?.status != null) searchParams.set("status", params.status);
|
|
if (params?.from != null) searchParams.set("from", params.from);
|
|
if (params?.to != null) searchParams.set("to", params.to);
|
|
const q = searchParams.toString();
|
|
const url = `/api/v1/admin/emails${q ? `?${q}` : ""}`;
|
|
const response = await apiClient.get<AdminEmailsListResponse>(url);
|
|
return response.data?.data!;
|
|
}
|
|
|
|
// --- Admin Emails: Send single / batch (021-email-templates-implementation) ---
|
|
export interface AdminEmailSendSingleBody {
|
|
emailType: string;
|
|
userId?: string;
|
|
email?: string;
|
|
locale?: "en" | "fr";
|
|
payload?: Record<string, unknown>;
|
|
}
|
|
|
|
export interface AdminEmailSendCustomBody {
|
|
userId?: string;
|
|
email?: string;
|
|
subject: string;
|
|
html: string;
|
|
plainText?: string;
|
|
}
|
|
|
|
export async function sendAdminEmailCustom(
|
|
body: AdminEmailSendCustomBody
|
|
): Promise<{ success: boolean; messageId?: string; sentTo?: string; error?: { message: string } }> {
|
|
try {
|
|
const response = await apiClient.post<{ success?: boolean; messageId?: string; sentTo?: string; error?: { message?: string } }>(
|
|
"/api/v1/admin/emails/send-custom",
|
|
body
|
|
);
|
|
const d = (response.data as any)?.data ?? response.data;
|
|
if (d?.success && d?.sentTo) {
|
|
return { success: true, messageId: d.messageId, sentTo: d.sentTo };
|
|
}
|
|
const err = (response.data as any)?.error ?? d?.error;
|
|
return { success: false, error: { message: err?.message ?? "Send failed" } };
|
|
} catch (err: unknown) {
|
|
const e = err as { response?: { data?: { error?: { message?: string } } }; message?: string };
|
|
const msg = e.response?.data?.error?.message ?? e.message ?? "Request failed";
|
|
return { success: false, error: { message: String(msg) } };
|
|
}
|
|
}
|
|
|
|
export interface AdminEmailSendSingleResponse {
|
|
success: boolean;
|
|
messageId?: string;
|
|
sentTo?: string;
|
|
locale?: string;
|
|
error?: { message: string; code?: string };
|
|
}
|
|
|
|
/** Find first object in tree that has sentTo (string); return that object's messageId, sentTo, locale */
|
|
function findSentToPayload(obj: unknown, depth = 0): { messageId?: string; sentTo: string; locale?: string } | null {
|
|
if (depth > 10 || obj == null || typeof obj !== "object") return null;
|
|
const o = obj as Record<string, unknown>;
|
|
const sentTo = o.sentTo as string | undefined;
|
|
if (sentTo && typeof sentTo === "string") {
|
|
return {
|
|
messageId: o.messageId as string | undefined,
|
|
sentTo,
|
|
locale: o.locale as string | undefined,
|
|
};
|
|
}
|
|
if (o.data != null) return findSentToPayload(o.data, depth + 1);
|
|
if (Array.isArray(o)) return null;
|
|
for (const v of Object.values(o)) {
|
|
const found = findSentToPayload(v, depth + 1);
|
|
if (found) return found;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export async function sendAdminEmailSingle(
|
|
body: AdminEmailSendSingleBody
|
|
): Promise<AdminEmailSendSingleResponse> {
|
|
try {
|
|
const response = await apiClient.post<AdminEmailSendSingleResponse>(
|
|
"/api/v1/admin/emails/send",
|
|
body
|
|
);
|
|
const payload = findSentToPayload(response.data);
|
|
if (payload) {
|
|
return {
|
|
success: true,
|
|
messageId: payload.messageId,
|
|
sentTo: payload.sentTo,
|
|
locale: payload.locale,
|
|
};
|
|
}
|
|
const raw = response.data as unknown as Record<string, unknown> | undefined;
|
|
const errMsg = (raw?.error as { message?: string } | undefined)?.message ?? "Unexpected response";
|
|
return { success: false, error: { message: errMsg } };
|
|
} catch (err: unknown) {
|
|
const axiosError = err as { response?: { data?: Record<string, unknown> }; message?: string };
|
|
const data = axiosError.response?.data as Record<string, unknown> | undefined;
|
|
const message = (data?.error as { message?: string } | undefined)?.message ?? (data?.message as string | undefined) ?? axiosError.message ?? "Request failed";
|
|
return { success: false, error: { message: String(message) } };
|
|
}
|
|
}
|
|
|
|
export interface AdminEmailSendBatchBody {
|
|
emailType: string;
|
|
userIds?: string[];
|
|
emails?: string[];
|
|
segment?: "all" | "all_pro" | "all_free" | "locale_en" | "locale_fr" | "locale_ar" | "active_last_7d" | "active_last_30d" | "active_last_90d" | "signup_last_7d" | "signup_last_30d";
|
|
locale?: "en" | "fr" | "ar";
|
|
limit?: number;
|
|
payload?: Record<string, unknown>;
|
|
}
|
|
|
|
export interface AdminEmailSendBatchResponse {
|
|
success: boolean;
|
|
sent: number;
|
|
failed: number;
|
|
errors: { userIdOrEmail: string; error: string }[];
|
|
}
|
|
|
|
export async function sendAdminEmailBatch(
|
|
body: AdminEmailSendBatchBody
|
|
): Promise<AdminEmailSendBatchResponse> {
|
|
try {
|
|
const response = await apiClient.post<AdminEmailSendBatchResponse>(
|
|
"/api/v1/admin/emails/send-batch",
|
|
body
|
|
);
|
|
// Backend returns 200 with { success: true, sent, failed, errors } (top-level)
|
|
const body_ = response.data as unknown as Record<string, unknown> | undefined;
|
|
const inner = (body_?.data as Record<string, unknown> | undefined) ?? body_;
|
|
const sent = inner?.sent as number | undefined;
|
|
if (body_?.success === true && typeof sent === "number") {
|
|
return {
|
|
success: true,
|
|
sent: Number(inner?.sent ?? 0),
|
|
failed: Number(inner?.failed ?? 0),
|
|
errors: (inner?.errors as { userIdOrEmail: string; error: string }[]) ?? [],
|
|
};
|
|
}
|
|
return { success: false, sent: 0, failed: 0, errors: [] };
|
|
} catch (err: unknown) {
|
|
const axiosError = err as { response?: { data?: Record<string, unknown> }; message?: string };
|
|
const data = axiosError.response?.data as Record<string, unknown> | undefined;
|
|
const message = (data?.error as { message?: string } | undefined)?.message ?? (data?.message as string | undefined) ?? axiosError.message ?? "Request failed";
|
|
return { success: false, sent: 0, failed: 0, errors: [{ userIdOrEmail: "", error: String(message) }] };
|
|
}
|
|
}
|
|
|
|
// --- Coupons / Promotions (Step 07) ---
|
|
export interface AdminCoupon {
|
|
id: string;
|
|
code: string;
|
|
discountType: string;
|
|
discountValue: string;
|
|
validFrom: string;
|
|
validUntil: string;
|
|
usageLimit: number | null;
|
|
usedCount: number;
|
|
tierRestrict: string[];
|
|
countryRestrict: string[];
|
|
perUserLimit: number | null;
|
|
isActive: boolean;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
}
|
|
|
|
export interface AdminCouponsListResponse {
|
|
items: AdminCoupon[];
|
|
total: number;
|
|
page: number;
|
|
limit: number;
|
|
}
|
|
|
|
export async function listAdminCoupons(params?: {
|
|
page?: number;
|
|
limit?: number;
|
|
code?: string;
|
|
isActive?: "true" | "false";
|
|
}): Promise<AdminCouponsListResponse> {
|
|
const searchParams = new URLSearchParams();
|
|
if (params?.page != null) searchParams.set("page", String(params.page));
|
|
if (params?.limit != null) searchParams.set("limit", String(params.limit));
|
|
if (params?.code != null) searchParams.set("code", params.code);
|
|
if (params?.isActive != null) searchParams.set("isActive", params.isActive);
|
|
const q = searchParams.toString();
|
|
const url = `/api/v1/admin/coupons${q ? `?${q}` : ""}`;
|
|
const response = await apiClient.get<AdminCouponsListResponse>(url);
|
|
return response.data?.data!;
|
|
}
|
|
|
|
export async function getAdminCoupon(id: string): Promise<AdminCoupon> {
|
|
const response = await apiClient.get<AdminCoupon>(`/api/v1/admin/coupons/${id}`);
|
|
return response.data?.data!;
|
|
}
|
|
|
|
export interface AdminCouponCreateBody {
|
|
code: string;
|
|
discountType: "PERCENT" | "FIXED";
|
|
discountValue: number;
|
|
validFrom: string;
|
|
validUntil: string;
|
|
usageLimit?: number | null;
|
|
perUserLimit?: number | null;
|
|
tierRestrict?: string[];
|
|
countryRestrict?: string[];
|
|
isActive?: boolean;
|
|
}
|
|
|
|
export async function createAdminCoupon(body: AdminCouponCreateBody): Promise<AdminCoupon> {
|
|
const response = await apiClient.post<AdminCoupon>("/api/v1/admin/coupons", body);
|
|
return response.data?.data!;
|
|
}
|
|
|
|
export interface AdminCouponUpdateBody {
|
|
code?: string;
|
|
discountType?: "PERCENT" | "FIXED";
|
|
discountValue?: number;
|
|
validFrom?: string;
|
|
validUntil?: string;
|
|
usageLimit?: number | null;
|
|
perUserLimit?: number | null;
|
|
tierRestrict?: string[];
|
|
countryRestrict?: string[];
|
|
isActive?: boolean;
|
|
}
|
|
|
|
export async function updateAdminCoupon(id: string, body: AdminCouponUpdateBody): Promise<AdminCoupon> {
|
|
const response = await apiClient.patch<AdminCoupon>(`/api/v1/admin/coupons/${id}`, body);
|
|
return response.data?.data!;
|
|
}
|
|
|
|
export async function deleteAdminCoupon(id: string): Promise<void> {
|
|
await apiClient.delete(`/api/v1/admin/coupons/${id}`);
|
|
}
|
|
|
|
// --- Tasks & Reminders (Step 08) ---
|
|
export interface AdminTaskPredefinedItem {
|
|
category: string;
|
|
title: string;
|
|
completed: boolean;
|
|
}
|
|
|
|
export interface AdminTaskPredefinedResponse {
|
|
items: AdminTaskPredefinedItem[];
|
|
date: string;
|
|
}
|
|
|
|
export async function getAdminTasksPredefined(date?: string): Promise<AdminTaskPredefinedResponse> {
|
|
const params = date ? `?date=${date}` : "";
|
|
const response = await apiClient.get<AdminTaskPredefinedResponse>(`/api/v1/admin/tasks/predefined${params}`);
|
|
return response.data?.data!;
|
|
}
|
|
|
|
export async function completeAdminTaskPredefined(
|
|
category: string,
|
|
title: string,
|
|
date?: string
|
|
): Promise<unknown> {
|
|
const response = await apiClient.post("/api/v1/admin/tasks/predefined/complete", {
|
|
category,
|
|
title,
|
|
date,
|
|
});
|
|
return response.data?.data;
|
|
}
|
|
|
|
export async function revertAdminTaskPredefined(
|
|
category: string,
|
|
title: string,
|
|
date?: string
|
|
): Promise<unknown> {
|
|
const response = await apiClient.post("/api/v1/admin/tasks/predefined/revert", {
|
|
category,
|
|
title,
|
|
date,
|
|
});
|
|
return response.data?.data;
|
|
}
|
|
|
|
export interface AdminTask {
|
|
id: string;
|
|
title: string;
|
|
description: string | null;
|
|
category: string;
|
|
dueDate: string | null;
|
|
recurring: string | null;
|
|
status: string;
|
|
completedAt: string | null;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
}
|
|
|
|
export interface AdminTasksListResponse {
|
|
items: AdminTask[];
|
|
total: number;
|
|
page: number;
|
|
limit: number;
|
|
}
|
|
|
|
export async function listAdminTasks(params?: {
|
|
page?: number;
|
|
limit?: number;
|
|
category?: string;
|
|
status?: string;
|
|
}): Promise<AdminTasksListResponse> {
|
|
const searchParams = new URLSearchParams();
|
|
if (params?.page != null) searchParams.set("page", String(params.page));
|
|
if (params?.limit != null) searchParams.set("limit", String(params.limit));
|
|
if (params?.category != null) searchParams.set("category", params.category);
|
|
if (params?.status != null) searchParams.set("status", params.status);
|
|
const q = searchParams.toString();
|
|
const url = `/api/v1/admin/tasks${q ? `?${q}` : ""}`;
|
|
const response = await apiClient.get<AdminTasksListResponse>(url);
|
|
return response.data?.data!;
|
|
}
|
|
|
|
export interface AdminTaskCreateBody {
|
|
title: string;
|
|
description?: string;
|
|
category: "daily" | "weekly" | "monthly" | "quarterly";
|
|
dueDate?: string;
|
|
recurring?: "daily" | "weekly" | "monthly";
|
|
}
|
|
|
|
export async function createAdminTask(body: AdminTaskCreateBody): Promise<AdminTask> {
|
|
const response = await apiClient.post<AdminTask>("/api/v1/admin/tasks", body);
|
|
return response.data?.data!;
|
|
}
|
|
|
|
export interface AdminTaskUpdateBody {
|
|
title?: string;
|
|
description?: string;
|
|
category?: string;
|
|
dueDate?: string | null;
|
|
recurring?: string | null;
|
|
status?: string;
|
|
}
|
|
|
|
export async function updateAdminTask(id: string, body: AdminTaskUpdateBody): Promise<AdminTask> {
|
|
const response = await apiClient.patch<AdminTask>(`/api/v1/admin/tasks/${id}`, body);
|
|
return response.data?.data!;
|
|
}
|
|
|
|
export async function deleteAdminTask(id: string): Promise<void> {
|
|
await apiClient.delete(`/api/v1/admin/tasks/${id}`);
|
|
}
|
|
|
|
// --- Reports (Step 09) ---
|
|
export type ReportType = "revenue" | "users" | "tools" | "subscriptions" | "emails";
|
|
export type ReportGranularity = "daily" | "weekly" | "monthly";
|
|
|
|
export async function getAdminReportRevenue(params?: {
|
|
from?: string;
|
|
to?: string;
|
|
granularity?: ReportGranularity;
|
|
}): Promise<{ rows: { period: string; count: number; total: number; byType: Record<string, number> }[]; from: string; to: string }> {
|
|
const q = new URLSearchParams();
|
|
if (params?.from) q.set("from", params.from);
|
|
if (params?.to) q.set("to", params.to);
|
|
if (params?.granularity) q.set("granularity", params.granularity);
|
|
const response = await apiClient.get(`/api/v1/admin/reports/revenue?${q}`);
|
|
return (response.data?.data ?? { rows: [], from: "", to: "" }) as {
|
|
rows: { period: string; count: number; total: number; byType: Record<string, number> }[];
|
|
from: string;
|
|
to: string;
|
|
};
|
|
}
|
|
|
|
export async function getAdminReportUsers(params?: {
|
|
from?: string;
|
|
to?: string;
|
|
granularity?: ReportGranularity;
|
|
}): Promise<{ rows: { period: string; count: number; byTier: Record<string, number> }[]; from: string; to: string }> {
|
|
const q = new URLSearchParams();
|
|
if (params?.from) q.set("from", params.from);
|
|
if (params?.to) q.set("to", params.to);
|
|
if (params?.granularity) q.set("granularity", params.granularity);
|
|
const response = await apiClient.get(`/api/v1/admin/reports/users?${q}`);
|
|
const d = response.data?.data;
|
|
return (d && typeof d === "object" && "rows" in d ? d : { rows: [], from: "", to: "" }) as {
|
|
rows: { period: string; count: number; byTier: Record<string, number> }[];
|
|
from: string;
|
|
to: string;
|
|
};
|
|
}
|
|
|
|
export async function getAdminReportTools(params?: {
|
|
from?: string;
|
|
to?: string;
|
|
}): Promise<{
|
|
rows: { toolId: string; slug: string | null; name: string | null; category: string | null; total: number; completed: number; failed: number }[];
|
|
from: string;
|
|
to: string;
|
|
}> {
|
|
const q = new URLSearchParams();
|
|
if (params?.from) q.set("from", params.from);
|
|
if (params?.to) q.set("to", params.to);
|
|
const response = await apiClient.get(`/api/v1/admin/reports/tools?${q}`);
|
|
const d = response.data?.data;
|
|
return (d && typeof d === "object" && "rows" in d ? d : { rows: [], from: "", to: "" }) as {
|
|
rows: { toolId: string; slug: string | null; name: string | null; category: string | null; total: number; completed: number; failed: number }[];
|
|
from: string;
|
|
to: string;
|
|
};
|
|
}
|
|
|
|
export async function getAdminReportSubscriptions(params?: {
|
|
at?: string;
|
|
}): Promise<{ total: number; byStatus: Record<string, number>; byPlan: Record<string, number>; at: string }> {
|
|
const q = params?.at ? `?at=${params.at}` : "";
|
|
const response = await apiClient.get(`/api/v1/admin/reports/subscriptions${q}`);
|
|
const d = response.data?.data;
|
|
return (d && typeof d === "object" ? d : { total: 0, byStatus: {}, byPlan: {}, at: "" }) as {
|
|
total: number;
|
|
byStatus: Record<string, number>;
|
|
byPlan: Record<string, number>;
|
|
at: string;
|
|
};
|
|
}
|
|
|
|
export async function getAdminReportEmails(params?: {
|
|
from?: string;
|
|
to?: string;
|
|
}): Promise<{ total: number; byType: Record<string, number>; byStatus: Record<string, number>; from: string; to: string }> {
|
|
const q = new URLSearchParams();
|
|
if (params?.from) q.set("from", params.from);
|
|
if (params?.to) q.set("to", params.to);
|
|
const response = await apiClient.get(`/api/v1/admin/reports/emails?${q}`);
|
|
const d = response.data?.data;
|
|
return (d && typeof d === "object" ? d : { total: 0, byType: {}, byStatus: {}, from: "", to: "" }) as {
|
|
total: number;
|
|
byType: Record<string, number>;
|
|
byStatus: Record<string, number>;
|
|
from: string;
|
|
to: string;
|
|
};
|
|
}
|
|
|
|
function buildReportExportParams(params: {
|
|
type: ReportType;
|
|
from?: string;
|
|
to?: string;
|
|
granularity?: ReportGranularity;
|
|
format?: "csv" | "xlsx";
|
|
}) {
|
|
const q = new URLSearchParams({ type: params.type });
|
|
if (params.from) q.set("from", params.from);
|
|
if (params.to) q.set("to", params.to);
|
|
if (params.granularity) q.set("granularity", params.granularity);
|
|
if (params.format) q.set("format", params.format);
|
|
return q;
|
|
}
|
|
|
|
export async function exportAdminReportCsv(params: {
|
|
type: ReportType;
|
|
from?: string;
|
|
to?: string;
|
|
granularity?: ReportGranularity;
|
|
}): Promise<void> {
|
|
const q = buildReportExportParams({ ...params, format: "csv" });
|
|
const response = await apiClient.get<string>(`/api/v1/admin/reports/export?${q}`, { responseType: "text" } as any);
|
|
const csv = typeof response.data === "string" ? response.data : (response.data as any)?.data ?? "";
|
|
const blob = new Blob([typeof csv === "string" ? csv : ""], { type: "text/csv;charset=utf-8" });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement("a");
|
|
a.href = url;
|
|
a.download = `report-${params.type}.csv`;
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
|
|
export async function exportAdminReportExcel(params: {
|
|
type: ReportType;
|
|
from?: string;
|
|
to?: string;
|
|
granularity?: ReportGranularity;
|
|
}): Promise<void> {
|
|
const q = buildReportExportParams({ ...params, format: "xlsx" });
|
|
const response = await apiClient.get<ArrayBuffer>(`/api/v1/admin/reports/export?${q}`, {
|
|
responseType: "arraybuffer",
|
|
} as any);
|
|
const buffer = response.data as unknown;
|
|
if (!buffer) throw new Error("No data");
|
|
const blob = new Blob([buffer as BlobPart], {
|
|
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
});
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement("a");
|
|
a.href = url;
|
|
a.download = `report-${params.type}.xlsx`;
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
|
|
// --- SEO (Step 10) ---
|
|
export interface AdminSeoSitemapPreview {
|
|
urls: string[];
|
|
sitemapUrl: string;
|
|
count: number;
|
|
}
|
|
|
|
export async function getAdminSeoSitemapPreview(): Promise<AdminSeoSitemapPreview> {
|
|
const response = await apiClient.get<AdminSeoSitemapPreview>("/api/v1/admin/seo/sitemap-preview");
|
|
return response.data?.data!;
|
|
}
|
|
|
|
export interface AdminSeoSubmission {
|
|
id: string;
|
|
url: string;
|
|
platform: string;
|
|
status: string;
|
|
submittedAt: string;
|
|
response: Record<string, unknown> | null;
|
|
}
|
|
|
|
export async function submitAdminSeoSitemap(platform: "google" | "bing"): Promise<{
|
|
submission: AdminSeoSubmission;
|
|
pingUrl: string;
|
|
status: string;
|
|
}> {
|
|
const response = await apiClient.post<{
|
|
submission: AdminSeoSubmission;
|
|
pingUrl: string;
|
|
status: string;
|
|
}>("/api/v1/admin/seo/submit-sitemap", { platform });
|
|
return response.data?.data!;
|
|
}
|
|
|
|
export interface AdminSeoSubmissionsListResponse {
|
|
items: AdminSeoSubmission[];
|
|
total: number;
|
|
page: number;
|
|
limit: number;
|
|
}
|
|
|
|
export async function listAdminSeoSubmissions(params?: {
|
|
page?: number;
|
|
limit?: number;
|
|
platform?: "google" | "bing";
|
|
}): Promise<AdminSeoSubmissionsListResponse> {
|
|
const q = new URLSearchParams();
|
|
if (params?.page != null) q.set("page", String(params.page));
|
|
if (params?.limit != null) q.set("limit", String(params.limit));
|
|
if (params?.platform) q.set("platform", params.platform);
|
|
const response = await apiClient.get<AdminSeoSubmissionsListResponse>(
|
|
`/api/v1/admin/seo/submissions?${q}`
|
|
);
|
|
return response.data?.data!;
|
|
}
|
|
|
|
export interface AdminSeoMetaTool {
|
|
id: string;
|
|
slug: string;
|
|
name: string;
|
|
category: string;
|
|
metaTitle: string | null;
|
|
metaDescription: string | null;
|
|
}
|
|
|
|
export async function getAdminSeoMetaOverview(): Promise<{ tools: AdminSeoMetaTool[] }> {
|
|
const response = await apiClient.get<{ tools: AdminSeoMetaTool[] }>("/api/v1/admin/seo/meta-overview");
|
|
return response.data?.data!;
|
|
}
|
|
|
|
// --- Audit log (002-admin-dashboard-polish, Step 02) ---
|
|
export interface AdminAuditLogEntry {
|
|
id: string;
|
|
adminUserId: string;
|
|
adminUserEmail: string | null;
|
|
action: string;
|
|
entityType: string;
|
|
entityId: string;
|
|
changes: Record<string, unknown> | null;
|
|
ipAddress: string | null;
|
|
createdAt: string;
|
|
}
|
|
|
|
export interface AdminAuditLogListResponse {
|
|
items: AdminAuditLogEntry[];
|
|
total: number;
|
|
page: number;
|
|
limit: number;
|
|
}
|
|
|
|
export async function listAdminAuditLog(params?: {
|
|
page?: number;
|
|
limit?: number;
|
|
entityType?: string;
|
|
action?: string;
|
|
from?: string;
|
|
to?: string;
|
|
}): Promise<AdminAuditLogListResponse> {
|
|
const query: Record<string, string> = {};
|
|
if (params?.page != null) query.page = String(params.page);
|
|
if (params?.limit != null) query.limit = String(params.limit);
|
|
if (params?.entityType != null && params.entityType !== "")
|
|
query.entityType = params.entityType;
|
|
if (params?.action != null && params.action !== "") query.action = params.action;
|
|
if (params?.from != null) query.from = params.from;
|
|
if (params?.to != null) query.to = params.to;
|
|
const searchParams = new URLSearchParams(query);
|
|
const q = searchParams.toString();
|
|
const url = `/api/v1/admin/audit-log${q ? `?${q}` : ""}`;
|
|
const response = await apiClient.get<AdminAuditLogListResponse>(url);
|
|
return response.data?.data!;
|
|
}
|
|
|
|
// --- Admin Health (002-admin-dashboard-polish) ---
|
|
export interface AdminHealthCheck {
|
|
status: string;
|
|
responseTime?: string;
|
|
message?: string;
|
|
}
|
|
|
|
export interface AdminHealthCheckWithDetails extends AdminHealthCheck {
|
|
details?: Record<string, unknown>;
|
|
}
|
|
|
|
export interface AdminHealthResponse {
|
|
status: 'ok' | 'degraded';
|
|
checks: {
|
|
database: AdminHealthCheck;
|
|
redis: AdminHealthCheck;
|
|
minio: AdminHealthCheck;
|
|
email: AdminHealthCheck;
|
|
queue?: AdminHealthCheckWithDetails;
|
|
paddle?: AdminHealthCheck;
|
|
};
|
|
responseTime: string;
|
|
timestamp: string;
|
|
uptime: number;
|
|
}
|
|
|
|
export async function getAdminHealth(): Promise<AdminHealthResponse> {
|
|
const response = await apiClient.get<AdminHealthResponse>("/api/v1/admin/health");
|
|
return response.data?.data!;
|
|
}
|
|
|
|
// --- Admin Jobs (002-admin-dashboard-polish) ---
|
|
export interface AdminJob {
|
|
id: string;
|
|
status: string;
|
|
errorMessage: string | null;
|
|
createdAt: string;
|
|
completedAt: string | null;
|
|
processingTimeMs: number | null;
|
|
toolId: string;
|
|
userId: string | null;
|
|
tool: { slug: string; name: string; category?: string };
|
|
user: { email: string; tier?: string } | null;
|
|
}
|
|
|
|
export interface AdminJobsListResponse {
|
|
items: AdminJob[];
|
|
total: number;
|
|
page: number;
|
|
limit: number;
|
|
}
|
|
|
|
export interface AdminJobsStats {
|
|
total: number;
|
|
completed: number;
|
|
failed: number;
|
|
queued: number;
|
|
processing: number;
|
|
cancelled: number;
|
|
notCompleted: number;
|
|
}
|
|
|
|
export async function getAdminJobsStats(): Promise<AdminJobsStats> {
|
|
const response = await apiClient.get<AdminJobsStats>("/api/v1/admin/jobs/stats");
|
|
return response.data?.data!;
|
|
}
|
|
|
|
export async function listAdminJobs(params?: {
|
|
page?: number;
|
|
limit?: number;
|
|
status?: string;
|
|
}): Promise<AdminJobsListResponse> {
|
|
const query: Record<string, string> = {};
|
|
if (params?.page != null) query.page = String(params.page);
|
|
if (params?.limit != null) query.limit = String(params.limit);
|
|
if (params?.status != null && params.status !== "") query.status = params.status;
|
|
const searchParams = new URLSearchParams(query);
|
|
const q = searchParams.toString();
|
|
const url = `/api/v1/admin/jobs${q ? `?${q}` : ""}`;
|
|
const response = await apiClient.get<AdminJobsListResponse>(url);
|
|
return response.data?.data!;
|
|
}
|
|
|
|
// --- Admin Export CSV (002-admin-dashboard-polish) ---
|
|
function downloadCsv(csv: string, filename: string) {
|
|
const blob = new Blob([csv], { type: "text/csv;charset=utf-8" });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement("a");
|
|
a.href = url;
|
|
a.download = filename;
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
|
|
export async function exportAdminUsersCsv(): Promise<void> {
|
|
const response = await apiClient.get<string>("/api/v1/admin/export/users?format=csv", {
|
|
responseType: "text",
|
|
} as any);
|
|
const csv = typeof response.data === "string" ? response.data : (response.data as any)?.data ?? "";
|
|
downloadCsv(csv, "users.csv");
|
|
}
|
|
|
|
export async function exportAdminUsersExcel(): Promise<void> {
|
|
const response = await apiClient.get<ArrayBuffer>("/api/v1/admin/export/users?format=xlsx", {
|
|
responseType: "arraybuffer",
|
|
} as any);
|
|
const buffer = response.data as unknown;
|
|
if (!buffer) throw new Error("No data");
|
|
const blob = new Blob([buffer as BlobPart], {
|
|
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
});
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement("a");
|
|
a.href = url;
|
|
a.download = "users.xlsx";
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
|
|
export async function exportAdminToolsCsv(): Promise<void> {
|
|
const response = await apiClient.get<string>("/api/v1/admin/export/tools?format=csv", {
|
|
responseType: "text",
|
|
} as any);
|
|
const csv = typeof response.data === "string" ? response.data : (response.data as any)?.data ?? "";
|
|
downloadCsv(csv, "tools.csv");
|
|
}
|
|
|
|
export async function exportAdminToolsExcel(): Promise<void> {
|
|
const response = await apiClient.get<ArrayBuffer>("/api/v1/admin/export/tools?format=xlsx", {
|
|
responseType: "arraybuffer",
|
|
} as any);
|
|
const buffer = response.data as unknown;
|
|
if (!buffer) throw new Error("No data");
|
|
const blob = new Blob([buffer as BlobPart], {
|
|
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
});
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement("a");
|
|
a.href = url;
|
|
a.download = "tools.xlsx";
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
|
|
// --- Analytics (US5) ---
|
|
export interface AdminAnalyticsTopCustomer {
|
|
userId: string | null;
|
|
jobCount: number;
|
|
email: string | null;
|
|
tier: string | null;
|
|
}
|
|
|
|
export interface AdminAnalyticsRecentSignup {
|
|
id: string;
|
|
email: string;
|
|
createdAt: string;
|
|
tier: string;
|
|
}
|
|
|
|
export interface AdminAnalytics {
|
|
totalUsers: number;
|
|
activeSubscriptions: number;
|
|
totalJobsLast24h: number | null;
|
|
jobsTotal?: number;
|
|
jobsCompleted?: number;
|
|
jobsFailed?: number;
|
|
usersByTier?: Record<string, number>;
|
|
emailsTotal?: number;
|
|
emailsLast24h?: number;
|
|
emailsByType?: Record<string, number>;
|
|
emailsByStatus?: Record<string, number>;
|
|
topCustomers?: AdminAnalyticsTopCustomer[];
|
|
/** Total revenue from subscription payments (initial + renewal + upgrade), completed only */
|
|
subscriptionRevenueTotal?: number;
|
|
/** Number of completed day pass purchases */
|
|
dayPassPurchaseCount?: number;
|
|
/** Total revenue from day pass purchases */
|
|
dayPassRevenueTotal?: number;
|
|
/** Users currently with an active day pass (dayPassExpiresAt > now) */
|
|
dayPassActiveCount?: number;
|
|
/** Step 17: Last 10 users by signup date */
|
|
recentSignups?: AdminAnalyticsRecentSignup[];
|
|
}
|
|
|
|
export async function getAdminAnalytics(): Promise<AdminAnalytics> {
|
|
const response = await apiClient.get<AdminAnalytics>("/api/v1/admin/analytics");
|
|
return response.data?.data!;
|
|
}
|
|
|
|
// --- Tools usage (top tools by completed job count) Step 11 ---
|
|
export type ToolsUsagePeriod = "today" | "week" | "month" | "all";
|
|
|
|
export interface AdminToolUsageItem {
|
|
toolId: string;
|
|
count: number;
|
|
slug: string | null;
|
|
name: string | null;
|
|
category: string | null;
|
|
/** Step 11: total jobs (completed + failed + etc) */
|
|
total?: number;
|
|
failed?: number;
|
|
successRate?: number | null;
|
|
errorRate?: number | null;
|
|
avgProcessingMs?: number | null;
|
|
}
|
|
|
|
export interface AdminToolsUsageSummary {
|
|
totalOps: number;
|
|
avgProcessingMs: number | null;
|
|
mostUsedTool: { toolId: string; name: string | null; count: number } | null;
|
|
leastUsedTool: { toolId: string; name: string | null; count: number } | null;
|
|
queueLength: { pdf: number; image: number; text: number; total: number };
|
|
}
|
|
|
|
export interface AdminToolsUsageResponse {
|
|
items: AdminToolUsageItem[];
|
|
summary?: AdminToolsUsageSummary;
|
|
period?: string;
|
|
dateFrom?: string | null;
|
|
}
|
|
|
|
/** When limit is 0, returns all tools. period: today|week|month|all. */
|
|
export async function getAdminToolsUsage(
|
|
limit = 0,
|
|
period: ToolsUsagePeriod = "all"
|
|
): Promise<AdminToolUsageItem[]> {
|
|
const q = new URLSearchParams();
|
|
if (limit > 0) q.set("limit", String(limit));
|
|
if (period && period !== "all") q.set("period", period);
|
|
const response = await apiClient.get<{ data: AdminToolsUsageResponse }>(
|
|
`/api/v1/admin/tools/usage?${q}`
|
|
);
|
|
const res = response.data as { data?: { items?: AdminToolUsageItem[] } } | undefined;
|
|
return res?.data?.items ?? [];
|
|
}
|
|
|
|
/** Full tools usage with summary (Step 11). */
|
|
export async function getAdminToolsUsageFull(params?: {
|
|
limit?: number;
|
|
period?: ToolsUsagePeriod;
|
|
}): Promise<AdminToolsUsageResponse> {
|
|
const q = new URLSearchParams();
|
|
if (params?.limit != null && params.limit > 0) q.set("limit", String(params.limit));
|
|
if (params?.period && params.period !== "all") q.set("period", params.period);
|
|
const response = await apiClient.get<{ data: AdminToolsUsageResponse }>(
|
|
`/api/v1/admin/tools/usage?${q}`
|
|
);
|
|
const res = response.data as unknown as { data?: AdminToolsUsageResponse } | undefined;
|
|
return res?.data ?? { items: [] };
|
|
}
|
|
|
|
// --- Runtime Config (022-runtime-config) ---
|
|
export interface AdminConfigEntry {
|
|
key: string;
|
|
value: unknown;
|
|
valueType: string;
|
|
category: string;
|
|
description: string | null;
|
|
isSensitive: boolean;
|
|
isPublic: boolean;
|
|
updatedAt: string;
|
|
}
|
|
|
|
export async function getAdminConfig(): Promise<AdminConfigEntry[]> {
|
|
const response = await apiClient.get<AdminConfigEntry[]>("/api/v1/admin/config");
|
|
return Array.isArray(response.data) ? response.data : [];
|
|
}
|
|
|
|
export async function patchAdminConfig(
|
|
body:
|
|
| { key: string; value: unknown }
|
|
| { updates: Record<string, unknown>; reason?: string }
|
|
): Promise<{ success: boolean; updated: string[] }> {
|
|
const response = await apiClient.patch<{ success: boolean; updated: string[] }>(
|
|
"/api/v1/admin/config",
|
|
body
|
|
);
|
|
const d = (response.data as unknown) as { success?: boolean; updated?: string[] } | undefined;
|
|
return { success: d?.success ?? false, updated: d?.updated ?? [] };
|
|
}
|
|
|
|
export interface AdminConfigAuditEntry {
|
|
id: string;
|
|
configKey: string;
|
|
oldValue: unknown;
|
|
newValue: unknown;
|
|
changedBy: string | null;
|
|
changeReason: string | null;
|
|
ipAddress: string | null;
|
|
createdAt: string;
|
|
}
|
|
|
|
export async function getAdminConfigAudit(params?: {
|
|
key?: string;
|
|
limit?: number;
|
|
offset?: number;
|
|
}): Promise<AdminConfigAuditEntry[]> {
|
|
const search = new URLSearchParams();
|
|
if (params?.key) search.set("key", params.key);
|
|
if (params?.limit != null) search.set("limit", String(params.limit));
|
|
if (params?.offset != null) search.set("offset", String(params.offset));
|
|
const qs = search.toString();
|
|
const url = qs ? `/api/v1/admin/config/audit?${qs}` : "/api/v1/admin/config/audit";
|
|
const response = await apiClient.get<AdminConfigAuditEntry[]>(url);
|
|
return Array.isArray(response.data) ? response.data : [];
|
|
}
|