- hooks: notification types (CRUD), sent list, overview, sms blacklist (list + delete); NotificationType/Notification/SmsBlacklist row types. - /admin/notifications: tabbed — قالبها (template CRUD modal), ارسالها (overview KPIs + sent list), لیست سیاه (unblock). - sidebar "اعلانها" nav link. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
695 lines
19 KiB
TypeScript
695 lines
19 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;
|
|
};
|
|
// 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<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 const useLedgerJournal = (params: AdminListParams) =>
|
|
useAdminList<LedgerEntryRow>("ledger/journal", params);
|
|
|
|
/* ------------------------------ audit log ------------------------------ */
|
|
|
|
export interface AuditLogRow {
|
|
id: string;
|
|
createdAt: string;
|
|
actorPhone: string | null;
|
|
action: string;
|
|
targetType: string;
|
|
targetId: string | null;
|
|
before: Record<string, unknown> | null;
|
|
after: Record<string, unknown> | null;
|
|
note: string;
|
|
}
|
|
|
|
export const useAuditLog = (params: AdminListParams) =>
|
|
useAdminList<AuditLogRow>("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<AdminCommentRow>("moderation/comments", params);
|
|
export const useAdminChatMessages = (params: AdminListParams) =>
|
|
useAdminList<AdminChatMessageRow>("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<NotificationsOverview>("notifications/overview/", token as string),
|
|
});
|
|
}
|
|
|
|
export const useNotificationTypes = (params: AdminListParams) =>
|
|
useAdminList<NotificationTypeRow>("notifications/types", params);
|
|
export const useNotifications = (params: AdminListParams) =>
|
|
useAdminList<NotificationRow>("notifications/sent", params);
|
|
export const useSmsBlacklist = (params: AdminListParams) =>
|
|
useAdminList<SmsBlacklistRow>("notifications/blacklist", params);
|
|
|
|
export function useNotificationTypeCreate() {
|
|
const token = useAuthToken();
|
|
const qc = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: (data: Record<string, unknown>) =>
|
|
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<string, unknown> }) =>
|
|
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"] }),
|
|
});
|
|
}
|
|
|
|
/* ---------------------- 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<string, number>;
|
|
last24h: number;
|
|
last7d: number;
|
|
recentFailures: FailureRow[];
|
|
};
|
|
metadata: {
|
|
total: number;
|
|
byStatus: Record<string, number>;
|
|
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<AiHealthOverview>("ai-health/overview/", token as string),
|
|
});
|
|
}
|
|
|
|
export const useAdminDiscounts = (params: AdminListParams) =>
|
|
useAdminList<AdminDiscountRow>("discounts", params);
|
|
export const useAdminCollections = (params: AdminListParams) =>
|
|
useAdminList<AdminCollectionRow>("collections", params);
|
|
|
|
export function useAdminDiscountCreate() {
|
|
const token = useAuthToken();
|
|
const qc = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: (data: Record<string, unknown>) =>
|
|
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<string, unknown> }) =>
|
|
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<string, unknown> }) =>
|
|
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<LedgerSellerDetail>(`ledger/sellers/${id}/`, token as string),
|
|
});
|
|
}
|
|
|
|
async function adminPost<T>(
|
|
path: string,
|
|
body: Record<string, unknown>,
|
|
token: string
|
|
): Promise<T> {
|
|
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<void> {
|
|
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"] });
|
|
},
|
|
});
|
|
}
|