Files
filezzy-staging/frontend/services/admin.service.ts
2026-02-04 17:37:28 +01:00

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 : [];
}