Vitron-Front/app/requestHandler/use-seller-hooks.ts
Arda Samadi a642fc0fae feat(seller-team): multi-user dashboard — store switcher, role gating, team UI
- useMyStores + team hooks (invite by phone / list / remove) in use-seller-hooks.
- useStoreOwnership rewritten to allow staff (not just the owner) via my-stores;
  exposes `role` so callers can gate owner-only UI, and no longer redirects
  members away from a store they help manage.
- Store settings: owner-only items (financial, shipping, edit store, instagram
  sync, team) hidden from staff; new "مدیریت اعضای تیم" entry (owner-only).
- New /store/:storeId/team page: invite by phone + members list with pending/
  active status + remove (owner-only, redirects staff). MyLayout keeps store mode
  on the team route.
- StoreForm edit is owner-only (redirects staff). Profile "enter my store" now
  routes owners to their store and staff to the store they manage.
- Chat already split earlier: seller inbox vs personal buyer inbox.

Depends on the seller-team backend; ships on this branch, not main.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 16:43:53 +03:30

399 lines
12 KiB
TypeScript

import {
useQuery,
useMutation,
useQueryClient,
useInfiniteQuery,
} from "@tanstack/react-query";
import { useAuthToken } from "../hooks/use-root-data";
import {
SellerApplication,
InstagramVerification,
} from "../../src/api/types/models";
import { toast } from "~/hooks/use-toast";
import { useNavigate } from "react-router-dom";
import {
createFollowsManagementApi,
createSellerManagementApi,
} from "~/utils/api-client-factory";
import { getApiBaseUrl } from "../utils/api-config";
// ── Multi-user team ─────────────────────────────────────────────────────────
export interface MyStore {
username: string;
storeName: string;
storeLogo: string | null;
role: "owner" | "staff" | null;
}
export interface StoreMember {
id: string;
userId: string | null;
name: string | null;
phone: string;
role: string;
status: "active" | "pending";
createdAt: string;
}
/** Stores the current user can manage (owned + memberships), with their role. */
export function useMyStores() {
const token = useAuthToken();
return useQuery({
queryKey: ["myStores"],
queryFn: async (): Promise<MyStore[]> => {
if (!token) return [];
const res = await fetch(`${getApiBaseUrl()}/api/seller/my-stores/`, {
headers: { Authorization: `Bearer ${token}` },
});
if (!res.ok) return [];
return res.json();
},
enabled: !!token,
staleTime: 60 * 1000,
});
}
/** A store's team members (owner only). */
export function useStoreMembers(username?: string) {
const token = useAuthToken();
return useQuery({
queryKey: ["storeMembers", username],
queryFn: async (): Promise<StoreMember[]> => {
const res = await fetch(
`${getApiBaseUrl()}/api/seller/members/${username}/`,
{ headers: { Authorization: `Bearer ${token}` } }
);
if (!res.ok) throw new Error("failed to load members");
return res.json();
},
enabled: !!token && !!username,
});
}
/** Invite a team member by phone number (owner only). */
export function useInviteMember(username?: string) {
const token = useAuthToken();
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (phone: string) => {
const res = await fetch(
`${getApiBaseUrl()}/api/seller/members/${username}/`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ phone }),
}
);
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.error || "خطا در دعوت عضو جدید");
}
return res.json();
},
onSuccess: () =>
queryClient.invalidateQueries({ queryKey: ["storeMembers", username] }),
});
}
/** Remove a team member (owner only). */
export function useRemoveMember(username?: string) {
const token = useAuthToken();
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (memberId: string) => {
const res = await fetch(
`${getApiBaseUrl()}/api/seller/members/${username}/${memberId}/`,
{ method: "DELETE", headers: { Authorization: `Bearer ${token}` } }
);
if (!res.ok) throw new Error("خطا در حذف عضو");
return res.json();
},
onSuccess: () =>
queryClient.invalidateQueries({ queryKey: ["storeMembers", username] }),
});
}
/**
* Custom hook to fetch seller data by sellerId using React Query
*/
export const useSellerData = (sellerId: string | undefined) => {
const token = useAuthToken();
return useQuery({
queryKey: ["seller", sellerId],
queryFn: async () => {
if (!sellerId) throw new Error("Seller ID is required");
const sellerApi = createSellerManagementApi(token);
return await sellerApi.sellerRead({ username: sellerId });
},
enabled: !!sellerId, // Only run the query if sellerId exists
});
};
/**
* Custom hook to check if the current user is following a seller
*/
export const useSellerFollowStatus = (sellerId: string | undefined) => {
const token = useAuthToken();
return useQuery({
queryKey: ["sellerFollowStatus", sellerId],
queryFn: async () => {
if (!sellerId) throw new Error("Seller ID is required");
if (!token) throw new Error("Authentication required");
try {
const followsApi = createFollowsManagementApi(token);
const response = await followsApi.apiEngagementV1FollowsCheckRead({
sellerId,
});
return response?.following;
} catch (error) {
return false; // If error is thrown, user is not following the seller
}
},
enabled: !!sellerId && !!token, // Only run the query if sellerId and token exist
});
};
/**
* Custom hook to follow a seller
*/
export const useSellerFollow = () => {
const token = useAuthToken();
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ sellerId }: { sellerId: string }) => {
if (!token) throw new Error("Authentication required");
const followsApi = createFollowsManagementApi(token);
await followsApi.apiEngagementV1FollowsCreate({
data: { followingId: sellerId },
});
return { success: true };
},
onSuccess: (_, variables) => {
// Invalidate and refetch follow status after mutation
queryClient.invalidateQueries({
queryKey: ["sellerFollowStatus", variables.sellerId],
});
queryClient.invalidateQueries({
queryKey: ["getFollowedSellersService"],
});
},
});
};
/**
* Custom hook to unfollow a seller
*/
export const useSellerUnfollow = () => {
const token = useAuthToken();
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ sellerId }: { sellerId: string }) => {
if (!token) throw new Error("Authentication required");
const followsApi = createFollowsManagementApi(token);
await followsApi.apiEngagementV1FollowsDelete({ sellerId });
return { success: true };
},
onSuccess: (_, variables) => {
// Invalidate and refetch follow status after mutation
queryClient.invalidateQueries({
queryKey: ["sellerFollowStatus", variables.sellerId],
});
queryClient.invalidateQueries({
queryKey: ["getFollowedSellersService"],
});
},
});
};
export const useGetSellerProducts = (
sellerId: string,
options?: {
q?: string;
priceMin?: number;
priceMax?: number;
ordering?: string;
}
) => {
const token = useAuthToken();
return useInfiniteQuery({
queryKey: ["sellerProducts", sellerId, options],
queryFn: async ({ pageParam = 1 }) => {
const sellerApi = createSellerManagementApi(token);
return await sellerApi.sellerProductList({
username: sellerId,
page: pageParam,
q: options?.q,
priceMin: options?.priceMin,
priceMax: options?.priceMax,
ordering: options?.ordering,
});
},
getNextPageParam: (lastPage) => {
if (!lastPage.next) return undefined;
// Extract page number from the next URL
const url = new URL(lastPage.next);
const page = url.searchParams.get("page");
return page ? parseInt(page, 10) : undefined;
},
initialPageParam: 1,
enabled: !!sellerId,
// Public storefront: prices/discounts must reflect the seller's latest
// edits. Always treat as stale and revalidate on mount/refocus so the
// store list can't keep showing a pre-edit price (the detail page is SSR
// and already fresh, which caused the list-vs-detail mismatch).
staleTime: 0,
refetchOnMount: true,
refetchOnWindowFocus: true,
});
};
/**
* Custom hook to fetch seller categories
*/
export const useSellerCategories = () => {
return useQuery({
queryKey: ["sellerCategories"],
queryFn: async () => {
const sellerApi = createSellerManagementApi();
return await sellerApi.apiSellerV1CategoriesList();
},
});
};
/**
* Custom hook to fetch state and city data
*/
export const useStateCityData = () => {
return useQuery({
queryKey: ["stateCityData"],
queryFn: async () => {
const sellerApi = createSellerManagementApi();
return await sellerApi.apiSellerV1StateCityList();
},
});
};
/**
* Custom hook to create seller application
*/
export const useCreateSellerApplication = () => {
const token = useAuthToken();
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (data: SellerApplication) => {
if (!token) throw new Error("Authentication required");
const sellerApi = createSellerManagementApi(token);
return await sellerApi.apiSellerV1ApplyCreate({ data });
},
onSuccess: () => {
// Invalidate profile query to refresh sellerUsername
queryClient.invalidateQueries({ queryKey: ["getProfile"] });
},
});
};
/**
* Custom hook to apply Instagram verification
*/
export const useInstagramVerification = () => {
const token = useAuthToken();
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (
data: InstagramVerification
): Promise<InstagramVerification> => {
if (!token) throw new Error("Authentication required");
const sellerApi = createSellerManagementApi(token);
return await sellerApi.apiSellerV1InstapplyCreate({ data });
},
onSuccess: (data: InstagramVerification) => {
if (data?.isVerified) {
toast({
title: "احراز اکانت اینستاگرام با موفقیت انجام شد",
description: "اکانت اینستاگرام شما با موفقیت احراز شد",
});
// Invalidate seller data after successful verification
queryClient.invalidateQueries({
queryKey: ["seller"],
});
} else {
toast({
title: "احراز اکانت اینستاگرام با موفقیت انجام نشد",
description: "لطفا مجددا تلاش کنید",
variant: "destructive",
});
}
},
onError: () => {
toast({
title: "احراز اکانت اینستاگرام با موفقیت انجام نشد",
description: "لطفا مجددا تلاش کنید",
variant: "destructive",
});
},
});
};
/**
* Custom hook to update seller information
*/
export const useSellerUpdate = () => {
const token = useAuthToken();
const queryClient = useQueryClient();
const navigate = useNavigate();
return useMutation({
mutationFn: async ({
username,
data,
}: {
username: string;
data: {
storeName: string;
storeDescription?: string | null;
storeLogo?: string | null;
instagramId?: string | null;
bannerType?: string | null;
bannerColor?: string | null;
bannerImage?: string | null;
};
}) => {
if (!token) throw new Error("Authentication required");
const sellerApi = createSellerManagementApi(token);
return await sellerApi.sellerPartialUpdate({ username, data });
},
onSuccess: (_, variables) => {
toast({
title: "اطلاعات فروشگاه با موفقیت به‌روزرسانی شد",
description: "تغییرات شما ذخیره شد",
});
// Invalidate seller data after successful update
queryClient.invalidateQueries({
queryKey: ["seller", variables.username],
});
navigate(`/store/${variables.username}`);
},
onError: () => {
toast({
title: "خطا در به‌روزرسانی اطلاعات",
description: "لطفا مجددا تلاش کنید",
variant: "destructive",
});
},
});
};