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"] }), }); } /* -------------------------- notifications (3e) -------------------------- */ export interface NotificationTypeRow { id: number; code: string; title: string; defaultSms: boolean; defaultInApp: boolean; template: string; variables: string[]; } export interface NotificationRow { id: number; typeCode: string | null; typeTitle: string | null; receiverPhone: string | null; title: string; body: string; read: boolean; sentSms: boolean; sentInApp: boolean; smsLineNumber: number | null; createdAt: string; } export interface SmsBlacklistRow { id: number; mobile: string; lineNumber: number; reason: string | null; createdAt: string; } export interface NotificationsOverview { total: number; smsSent: number; inAppSent: number; unread: number; types: number; pushSubscriptions: number; blacklistedLines: number; byType: { code: string; title: string; count: number }[]; } export function useNotificationsOverview() { const token = useAuthToken(); return useQuery({ queryKey: ["admin", "notifications", "overview"], enabled: !!token, queryFn: () => adminGet("notifications/overview/", token as string), }); } export const useNotificationTypes = (params: AdminListParams) => useAdminList("notifications/types", params); export const useNotifications = (params: AdminListParams) => useAdminList("notifications/sent", params); export const useSmsBlacklist = (params: AdminListParams) => useAdminList("notifications/blacklist", params); export function useNotificationTypeCreate() { const token = useAuthToken(); const qc = useQueryClient(); return useMutation({ mutationFn: (data: Record) => adminPost("notifications/types/", data, token as string), onSuccess: () => qc.invalidateQueries({ queryKey: ["admin", "notifications/types"] }), }); } export function useNotificationTypeUpdate() { const token = useAuthToken(); const qc = useQueryClient(); return useMutation({ mutationFn: ({ id, data }: { id: number; data: Record }) => adminPatch(`notifications/types/${id}/`, data, token as string), onSuccess: () => qc.invalidateQueries({ queryKey: ["admin", "notifications/types"] }), }); } export function useNotificationTypeDelete() { const token = useAuthToken(); const qc = useQueryClient(); return useMutation({ mutationFn: (id: number) => adminDelete(`notifications/types/${id}/`, token as string), onSuccess: () => qc.invalidateQueries({ queryKey: ["admin", "notifications/types"] }), }); } export function useSmsBlacklistDelete() { const token = useAuthToken(); const qc = useQueryClient(); return useMutation({ mutationFn: (id: number) => adminDelete(`notifications/blacklist/${id}/`, token as string), onSuccess: () => qc.invalidateQueries({ queryKey: ["admin", "notifications/blacklist"] }), }); } /* --------------------- homepage / merchandising (3f) --------------------- */ export interface HomeBannerRow { id: number; title: string; linkType: string; linkId: number; isActive: boolean; isVisible: boolean; imageUrl: string | null; startDate: string; endDate: string; createdAt: string; } export interface HomeCategoryRow { id: string; categoryId: string; categoryName: string | null; imageUrl: string | null; isActive: boolean; } export interface HomeSellerTileRow { id: string; sellerId: string; sellerName: string | null; imageUrl: string | null; isActive: boolean; order: number; createdAt: string; } export const useHomeBanners = (params: AdminListParams) => useAdminList("homepage/banners", params); export const useHomeCategories = (params: AdminListParams) => useAdminList("homepage/categories", params); export const useHomeSellerTiles = (params: AdminListParams) => useAdminList("homepage/tiles", params); /** PATCH/DELETE a homepage entry. `kind` maps to the URL segment. */ function useHomeMutations(kind: "banners" | "categories" | "tiles") { const token = useAuthToken(); const qc = useQueryClient(); const invalidate = () => qc.invalidateQueries({ queryKey: ["admin", `homepage/${kind}`] }); const update = useMutation({ mutationFn: ({ id, data }: { id: string | number; data: Record }) => adminPatch(`homepage/${kind}/${id}/`, data, token as string), onSuccess: invalidate, }); const remove = useMutation({ mutationFn: (id: string | number) => adminDelete(`homepage/${kind}/${id}/`, token as string), onSuccess: invalidate, }); return { update, remove }; } export const useHomeBannerMutations = () => useHomeMutations("banners"); export const useHomeCategoryMutations = () => useHomeMutations("categories"); export const useHomeSellerTileMutations = () => useHomeMutations("tiles"); /* ------------------------ catalog attributes (3g) ------------------------ */ export interface CategoryLite { id: string; name: string; } export interface AttributeRow { id: string; name: string; categories: string[]; categoryNames: string[]; valueCount: number; } export interface AttributeValueRow { id: string; attribute: string; attributeName: string; value: string; info: unknown; } export function useCatalogCategories() { const token = useAuthToken(); return useQuery({ queryKey: ["admin", "catalog", "categories"], enabled: !!token, queryFn: () => adminGet("catalog/categories/", token as string), }); } export const useAttributes = (params: AdminListParams) => useAdminList("catalog/attributes", params); export const useAttributeValues = (params: AdminListParams) => useAdminList("catalog/attribute-values", params); function useCatalogMutations(kind: "attributes" | "attribute-values", invalidateKey: string) { const token = useAuthToken(); const qc = useQueryClient(); const invalidate = () => qc.invalidateQueries({ queryKey: ["admin", invalidateKey] }); const create = useMutation({ mutationFn: (data: Record) => adminPost(`catalog/${kind}/`, data, token as string), onSuccess: invalidate, }); const update = useMutation({ mutationFn: ({ id, data }: { id: string; data: Record }) => adminPatch(`catalog/${kind}/${id}/`, data, token as string), onSuccess: invalidate, }); const remove = useMutation({ mutationFn: (id: string) => adminDelete(`catalog/${kind}/${id}/`, token as string), onSuccess: invalidate, }); return { create, update, remove }; } export const useAttributeMutations = () => useCatalogMutations("attributes", "catalog/attributes"); export const useAttributeValueMutations = () => useCatalogMutations("attribute-values", "catalog/attribute-values"); /* ---------------------- 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"] }); }, }); }