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 => { if (!token) return []; const res = await fetch(`${getApiBaseUrl()}/api/seller/v1/my-stores/`, { headers: { Authorization: `Bearer ${token}` }, }); // Throw on failure so a transient error stays an ERROR state (data kept, // retried) instead of resolving to [] — otherwise a blip would look like // "no stores" and bounce an owner off their own dashboard. if (!res.ok) throw new Error(`my-stores failed: ${res.status}`); return res.json(); }, enabled: !!token, retry: 1, 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 => { const res = await fetch( `${getApiBaseUrl()}/api/seller/v1/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/v1/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/v1/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 => { 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", }); }, }); };