Vitron-Front/app/requestHandler/use-admin-hooks.ts
Arda Samadi cedeef4746 feat(admin): ledger UI — overview/reconciliation, payouts, transactions, per-seller (slice 2a)
Ledger section with sub-tabs: overview (money held/owed, payouts by status, and
a reconciliation warning card), payouts + transactions lists, per-seller balances
list, and a per-seller detail showing recorded vs derived balance + discrepancy,
bank accounts and payout history. Enabled the Ledger nav item.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 12:28:11 +03:30

305 lines
8.1 KiB
TypeScript

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<T>(path: string, token: string): Promise<T> {
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<T>(
path: string,
body: Record<string, unknown>,
token: string
): Promise<T> {
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<AdminOverview>("overview/", token as string),
});
}
/* ------------------------------ boards ------------------------------ */
export interface Paginated<T> {
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<T>(key: string, params: AdminListParams) {
const token = useAuthToken();
return useQuery({
queryKey: ["admin", key, params],
enabled: !!token,
placeholderData: keepPreviousData,
queryFn: () =>
adminGet<Paginated<T>>(`${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<AdminOrderRow>("orders", params);
export const useAdminSellers = (params: AdminListParams) =>
useAdminList<AdminSellerRow>("sellers", params);
export const useAdminUsers = (params: AdminListParams) =>
useAdminList<AdminUserRow>("users", params);
export const useAdminProducts = (params: AdminListParams) =>
useAdminList<AdminProductRow>("products", params);
/* ----------------------------- actions ------------------------------ */
function useAdminUpdate(resource: "sellers" | "users" | "products") {
const token = useAuthToken();
const qc = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: string; data: Record<string, unknown> }) =>
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;
};
}
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;
bankAccounts: Array<{
id: string;
accountHolder: string;
iban: string;
cardNumber: string;
bankName: string;
isVerified: boolean;
}>;
payouts: LedgerPayoutRow[];
}
export function useLedgerOverview() {
const token = useAuthToken();
return useQuery({
queryKey: ["admin", "ledger", "overview"],
enabled: !!token,
queryFn: () => adminGet<LedgerOverview>("ledger/overview/", token as string),
});
}
export const useLedgerTransactions = (params: AdminListParams) =>
useAdminList<LedgerTransactionRow>("ledger/transactions", params);
export const useLedgerPayouts = (params: AdminListParams) =>
useAdminList<LedgerPayoutRow>("ledger/payouts", params);
export const useLedgerSellers = (params: AdminListParams) =>
useAdminList<LedgerSellerRow>("ledger/sellers", params);
export function useLedgerSellerDetail(id: string | undefined) {
const token = useAuthToken();
return useQuery({
queryKey: ["admin", "ledger", "seller", id],
enabled: !!token && !!id,
queryFn: () =>
adminGet<LedgerSellerDetail>(`ledger/sellers/${id}/`, token as string),
});
}