import { useQuery, useMutation, useQueryClient, keepPreviousData, } from "@tanstack/react-query"; import { useAuthToken } from "~/hooks/use-root-data"; import { getApiBaseUrl } from "~/utils/api-config"; /* ------------------------------------------------------------------ * * Admin panel data hooks (superuser only). The admin API is a new, * fast-moving surface, so these call it directly with fetch (same * pattern as use-seller-hooks' useMyStores) rather than the generated * client. Responses are camelCased by the backend renderer. * ------------------------------------------------------------------ */ export interface AdminOverview { generatedAt: string; users: { total: number; active: number; sellers: number; new7d: number; new30d: number; }; sellers: { total: number; pending: number; approved: number; rejected: number; verified: number; }; products: { total: number; active: number }; orders: { total: number; pending: number; confirmed: number; shipped: number; delivered: number; cancelled: number; paidCount: number; revenueTotal: number; ordersToday: number; revenueToday: number; }; money: { walletHeld: number; sellerPayable: number; payoutsPendingCount: number; payoutsPendingAmount: number; }; ordersSeries: Array<{ date: string; orders: number; revenue: number }>; } async function adminGet(path: string, token: string): Promise { const res = await fetch(`${getApiBaseUrl()}/api/adminpanel/v1/${path}`, { headers: { Authorization: `Bearer ${token}` }, }); if (!res.ok) { throw new Error(`Admin request failed (${res.status})`); } return res.json(); } async function adminPatch( path: string, body: Record, token: string ): Promise { const res = await fetch(`${getApiBaseUrl()}/api/adminpanel/v1/${path}`, { method: "PATCH", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify(body), }); if (!res.ok) { throw new Error(`Admin update failed (${res.status})`); } return res.json(); } export function useAdminOverview() { const token = useAuthToken(); return useQuery({ queryKey: ["admin", "overview"], enabled: !!token, refetchInterval: 60_000, queryFn: () => adminGet("overview/", token as string), }); } /* ------------------------------ boards ------------------------------ */ export interface Paginated { count: number; next: string | null; previous: string | null; results: T[]; } export type AdminListParams = Record< string, string | number | boolean | undefined >; function toQuery(params: AdminListParams): string { const usp = new URLSearchParams(); Object.entries(params).forEach(([k, v]) => { if (v !== undefined && v !== "" && v !== null) usp.set(k, String(v)); }); const s = usp.toString(); return s ? `?${s}` : ""; } function useAdminList(key: string, params: AdminListParams) { const token = useAuthToken(); return useQuery({ queryKey: ["admin", key, params], enabled: !!token, placeholderData: keepPreviousData, queryFn: () => adminGet>(`${key}/${toQuery(params)}`, token as string), }); } export interface AdminOrderRow { id: string; createdAt: string; status: string; paymentStatus: string; totalPrice: number; userPhone: string | null; userName: string | null; sellerName: string | null; sellerUsername: string | null; } export interface AdminSellerRow { id: string; username: string; storeName: string; storeLogo: string | null; status: string; isVerified: boolean; platformFee: number; productCount: number; followerCount: number; reviewAverage: number; createdAt: string; userPhone: string | null; } export interface AdminUserRow { id: string; phoneNumber: string; username: string | null; email: string | null; isActive: boolean; isSeller: boolean; isStaff: boolean; isSuperuser: boolean; otpVerified: boolean; createdAt: string; } export interface AdminProductRow { id: string; title: string; basePrice: number; isActive: boolean; createdAt: string; sellerName: string | null; sellerUsername: string | null; categoryName: string | null; primaryImage: string | null; } export const useAdminOrders = (params: AdminListParams) => useAdminList("orders", params); export const useAdminSellers = (params: AdminListParams) => useAdminList("sellers", params); export const useAdminUsers = (params: AdminListParams) => useAdminList("users", params); export const useAdminProducts = (params: AdminListParams) => useAdminList("products", params); /* ----------------------------- actions ------------------------------ */ function useAdminUpdate(resource: "sellers" | "users" | "products") { const token = useAuthToken(); const qc = useQueryClient(); return useMutation({ mutationFn: ({ id, data }: { id: string; data: Record }) => adminPatch(`${resource}/${id}/`, data, token as string), onSuccess: () => { qc.invalidateQueries({ queryKey: ["admin", resource] }); qc.invalidateQueries({ queryKey: ["admin", "overview"] }); }, }); } export const useAdminSellerUpdate = () => useAdminUpdate("sellers"); export const useAdminUserUpdate = () => useAdminUpdate("users"); export const useAdminProductUpdate = () => useAdminUpdate("products"); /* ------------------------------ ledger ------------------------------ */ export interface LedgerOverview { walletHeld: number; sellerPayable: number; depositsTotal: number; purchasesTotal: number; payouts: { pending: { amount: number; count: number }; completed: { amount: number; count: number }; rejected: { amount: number; count: number }; }; reconciliation: { completedPayoutsNotDebited: number; sellerPayableRecorded: number; sellerPayableEstimatedTrue: number; }; // Slice 2c — balances derived from the append-only journal. journal: { sellerPayable: number; walletHeld: number; platformRevenue: number; sellerPayableDrift: number; walletHeldDrift: number; }; } export interface LedgerEntryRow { id: string; createdAt: string; entryType: string; account: string; delta: number; sellerName: string | null; userPhone: string | null; order: string | null; note: string | null; } export interface LedgerTransactionRow { id: string; transactionType: string; amount: number; userPhone: string | null; order: string | null; authority: string | null; createdAt: string; } export interface LedgerPayoutRow { id: string; sellerName: string | null; sellerUsername: string | null; amount: number; status: string; requestedAt: string; processedAt: string | null; iban: string | null; bankName: string | null; accountHolder: string | null; shomareMarja: string | null; shomareErja: string | null; } export interface LedgerSellerRow { sellerId: string; storeName: string | null; username: string | null; availableFunds: number; completedPayouts: number; pendingPayouts: number; } export interface LedgerSellerDetail { seller: { id: string; storeName: string; username: string; platformFee: number }; recordedBalance: number; accruedFromOrders: number; completedPayouts: number; pendingPayouts: number; derivedBalance: number; discrepancy: number; journalBalance: number; journalDiscrepancy: number; bankAccounts: Array<{ id: string; accountHolder: string; iban: string; cardNumber: string; bankName: string; isVerified: boolean; }>; payouts: LedgerPayoutRow[]; journal: LedgerEntryRow[]; } export function useLedgerOverview() { const token = useAuthToken(); return useQuery({ queryKey: ["admin", "ledger", "overview"], enabled: !!token, queryFn: () => adminGet("ledger/overview/", token as string), }); } export const useLedgerTransactions = (params: AdminListParams) => useAdminList("ledger/transactions", params); export const useLedgerPayouts = (params: AdminListParams) => useAdminList("ledger/payouts", params); export const useLedgerSellers = (params: AdminListParams) => useAdminList("ledger/sellers", params); export const useLedgerJournal = (params: AdminListParams) => useAdminList("ledger/journal", params); /* ------------------------------ audit log ------------------------------ */ export interface AuditLogRow { id: string; createdAt: string; actorPhone: string | null; action: string; targetType: string; targetId: string | null; before: Record | null; after: Record | null; note: string; } export const useAuditLog = (params: AdminListParams) => useAdminList("audit", params); /* -------------------- discounts & collections (3b) -------------------- */ export interface AdminDiscountRow { id: string; name: string; code: number; discountType: string; value: number; minPurchaseAmount: number | null; maxDiscountAmount: number | null; validFrom: string; validTo: string; isActive: boolean; eligibleUsers: string[] | null; maxCount: number | null; isValid: boolean; } export interface AdminCollectionRow { id: string; title: string; description: string | null; creatorType: string; creatorPhone: string | null; isPublic: boolean; coverImageUrl: string | null; productCount: number; createdAt: string; } /* --------------------------- moderation (3d) --------------------------- */ export interface AdminCommentRow { id: string; productId: string; productTitle: string | null; userPhone: string | null; comment: string; parentId: string | null; isReply: boolean; repliesCount: number; isHidden: boolean; isVerifiedBuyer: boolean; createdAt: string; } export interface AdminChatMessageRow { id: string; content: string; senderPhone: string | null; threadId: string | null; productId: string | null; imageUrl: string | null; isDeleted: boolean; createdAt: string; } export const useAdminComments = (params: AdminListParams) => useAdminList("moderation/comments", params); export const useAdminChatMessages = (params: AdminListParams) => useAdminList("moderation/chat", params); export function useAdminCommentModerate() { const token = useAuthToken(); const qc = useQueryClient(); return useMutation({ mutationFn: ({ id, isHidden }: { id: string; isHidden: boolean }) => adminPatch(`moderation/comments/${id}/`, { isHidden }, token as string), onSuccess: () => qc.invalidateQueries({ queryKey: ["admin", "moderation/comments"] }), }); } export function useAdminCommentDelete() { const token = useAuthToken(); const qc = useQueryClient(); return useMutation({ mutationFn: (id: string) => adminDelete(`moderation/comments/${id}/`, token as string), onSuccess: () => qc.invalidateQueries({ queryKey: ["admin", "moderation/comments"] }), }); } /* ---------------------- AI pipelines health (3c) ---------------------- */ interface FailureRow { id: string; error: string; createdAt?: string; updatedAt?: string; productId?: string; sellerId?: string; } export interface AiHealthOverview { generatedAt: string; tryon: { total: number; byStatus: Record; last24h: number; last7d: number; recentFailures: FailureRow[]; }; metadata: { total: number; byStatus: Record; last24h: number; last7d: number; recentFailures: FailureRow[]; }; instagram: { accountsTotal: number; accountsActive: number; postsTotal: number; postsPendingAi: number; postsConverted: number; importedLast7d: number; recentImports: { account: string; date: string; importedCount: number }[]; }; apgs: { total: number; success: number; failed: number; last24h: number; last7d: number; recentFailures: FailureRow[]; }; } export function useAiHealth() { const token = useAuthToken(); return useQuery({ queryKey: ["admin", "ai-health"], enabled: !!token, refetchInterval: 60_000, queryFn: () => adminGet("ai-health/overview/", token as string), }); } export const useAdminDiscounts = (params: AdminListParams) => useAdminList("discounts", params); export const useAdminCollections = (params: AdminListParams) => useAdminList("collections", params); export function useAdminDiscountCreate() { const token = useAuthToken(); const qc = useQueryClient(); return useMutation({ mutationFn: (data: Record) => adminPost("discounts/", data, token as string), onSuccess: () => qc.invalidateQueries({ queryKey: ["admin", "discounts"] }), }); } export function useAdminDiscountUpdate() { const token = useAuthToken(); const qc = useQueryClient(); return useMutation({ mutationFn: ({ id, data }: { id: string; data: Record }) => adminPatch(`discounts/${id}/`, data, token as string), onSuccess: () => qc.invalidateQueries({ queryKey: ["admin", "discounts"] }), }); } export function useAdminDiscountDelete() { const token = useAuthToken(); const qc = useQueryClient(); return useMutation({ mutationFn: (id: string) => adminDelete(`discounts/${id}/`, token as string), onSuccess: () => qc.invalidateQueries({ queryKey: ["admin", "discounts"] }), }); } export function useAdminCollectionUpdate() { const token = useAuthToken(); const qc = useQueryClient(); return useMutation({ mutationFn: ({ id, data }: { id: string; data: Record }) => adminPatch(`collections/${id}/`, data, token as string), onSuccess: () => qc.invalidateQueries({ queryKey: ["admin", "collections"] }), }); } export function useAdminCollectionDelete() { const token = useAuthToken(); const qc = useQueryClient(); return useMutation({ mutationFn: (id: string) => adminDelete(`collections/${id}/`, token as string), onSuccess: () => qc.invalidateQueries({ queryKey: ["admin", "collections"] }), }); } export function useLedgerSellerDetail(id: string | undefined) { const token = useAuthToken(); return useQuery({ queryKey: ["admin", "ledger", "seller", id], enabled: !!token && !!id, queryFn: () => adminGet(`ledger/sellers/${id}/`, token as string), }); } async function adminPost( path: string, body: Record, token: string ): Promise { const res = await fetch(`${getApiBaseUrl()}/api/adminpanel/v1/${path}`, { method: "POST", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify(body), }); if (!res.ok) { let detail = `Admin action failed (${res.status})`; try { const j = await res.json(); if (j?.detail) detail = j.detail; } catch { /* ignore */ } throw new Error(detail); } return res.json(); } async function adminDelete(path: string, token: string): Promise { const res = await fetch(`${getApiBaseUrl()}/api/adminpanel/v1/${path}`, { method: "DELETE", headers: { Authorization: `Bearer ${token}` }, }); if (!res.ok && res.status !== 204) { let detail = `Admin delete failed (${res.status})`; try { const j = await res.json(); if (j?.detail) detail = j.detail; } catch { /* ignore */ } throw new Error(detail); } } /** Approve (complete → debits balance + journals) or reject a pending payout. */ export function useLedgerPayoutAction() { const token = useAuthToken(); const qc = useQueryClient(); return useMutation({ mutationFn: ({ id, action }: { id: string; action: "complete" | "reject" }) => adminPost(`ledger/payouts/${id}/action/`, { action }, token as string), onSuccess: () => { qc.invalidateQueries({ queryKey: ["admin", "ledger"] }); qc.invalidateQueries({ queryKey: ["admin", "overview"] }); }, }); }