import { useQuery, useMutation, useQueryClient, UseQueryOptions, } from "@tanstack/react-query"; import { apiClient } from "@/lib/api"; import type { CartItem } from "@/lib/types/api"; import { useEffect } from "react"; interface CartResponse { message: string; data: CartItem[]; errorDetails?: string; } // DEBUG: Enable detailed logging const DEBUG = true; const log = (...args: any[]) => { if (DEBUG) console.log('[useCart]', ...args); }; // CRITICAL: Single source of truth for pending updates const pendingUpdates = new Map(); // productId -> quantity let updateLock = false; class CartEventEmitter { private listeners: Set<() => void> = new Set(); subscribe(callback: () => void) { log('๐Ÿ”” New subscriber added. Total:', this.listeners.size + 1); this.listeners.add(callback); return () => { log('๐Ÿ”• Subscriber removed. Total:', this.listeners.size - 1); this.listeners.delete(callback); }; } emit() { log('๐Ÿ“ข Emitting cart event to', this.listeners.size, 'listeners'); this.listeners.forEach((cb) => cb()); } } export const cartEvents = new CartEventEmitter(); function transformCartResponse(response: any): CartResponse { if ( typeof response === "string" && (response.trim().startsWith(">) { const queryClient = useQueryClient(); log('๐ŸŽฃ useCart hook called with options:', options); const query = useQuery({ queryKey: ["cart"], queryFn: async () => { log('๐ŸŒ Fetching cart from API...'); const response = await apiClient.get("/carts"); const transformed = transformCartResponse(response.data); log('โœ… Cart fetched:', { itemCount: transformed.data.length, items: transformed.data.map(item => ({ productId: item.product?.id, quantity: item.product_quantity })) }); return transformed; }, // CRITICAL FIX: Merge options AFTER defaults refetchOnMount: false, refetchOnWindowFocus: false, refetchOnReconnect: true, staleTime: Infinity, gcTime: 1000 * 60 * 5, retry: 2, retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 10000), // User options OVERRIDE defaults ...options, }); log('๐Ÿ”ง Query config after merge:', { refetchOnMount: query.refetch !== undefined, staleTime: query.isStale, dataUpdatedAt: query.dataUpdatedAt }); useEffect(() => { log('๐Ÿ”— Setting up cart events listener in useCart'); const unsubscribe = cartEvents.subscribe(() => { log('๐Ÿ“ฅ Cart event received in useCart, invalidating query'); queryClient.invalidateQueries({ queryKey: ["cart"], refetchType: "none", }); }); return () => { log('๐Ÿ”Œ Cleaning up cart events listener in useCart'); unsubscribe(); }; }, [queryClient]); return query; } export function useAddToCart() { const queryClient = useQueryClient(); return useMutation({ mutationFn: async ({ productId, quantity = 1, }: { productId: number; quantity?: number; }) => { log('โž• AddToCart mutation:', { productId, quantity }); const params = new URLSearchParams({ product_id: String(productId), product_quantity: String(quantity), }); const response = await apiClient.post("/carts", params.toString(), { headers: { "Content-Type": "application/x-www-form-urlencoded", }, }); if (typeof response.data === "object" && response.data.data) { return response.data; } if (typeof response.data === "string") { try { return JSON.parse(response.data); } catch { return { message: "success", data: "Added to cart" }; } } return { message: "success", data: "Added to cart" }; }, onMutate: async ({ productId, quantity }) => { log('๐Ÿ”’ AddToCart onMutate - Waiting for lock...'); while (updateLock) { await new Promise(resolve => setTimeout(resolve, 50)); } updateLock = true; log('๐Ÿ”“ Lock acquired'); await queryClient.cancelQueries({ queryKey: ["cart"] }); const previousCart = queryClient.getQueryData(["cart"]); log('๐Ÿ“ธ Previous cart state:', previousCart?.data.length, 'items'); queryClient.setQueryData(["cart"], (old) => { if (!old) return old; let updated = { ...old, data: [...old.data] }; pendingUpdates.forEach((pendingQty, pendingId) => { const idx = updated.data.findIndex( (item: any) => item.product?.id === pendingId ); if (idx !== -1) { updated.data[idx] = { ...updated.data[idx], product_quantity: pendingQty, }; } }); const existingItem = updated.data.find( (item: any) => item.product?.id === productId ); if (existingItem) { updated.data = updated.data.map((item: any) => item.product?.id === productId ? { ...item, product_quantity: item.product_quantity + quantity, } : item ); } else { updated.data = [ ...updated.data, { product: { id: productId }, product_quantity: quantity, } as any, ]; } const finalItem = updated.data.find( (item: any) => item.product?.id === productId ); if (finalItem) { pendingUpdates.set(productId, finalItem.product_quantity); log('๐Ÿ’พ Pending update saved:', productId, 'โ†’', finalItem.product_quantity); } log('๐Ÿ”„ Cart updated optimistically:', updated.data.length, 'items'); return updated; }); cartEvents.emit(); updateLock = false; return { previousCart }; }, onError: (error, variables, context) => { log('โŒ AddToCart error:', error); if (context?.previousCart) { queryClient.setQueryData(["cart"], context.previousCart); pendingUpdates.delete(variables.productId); cartEvents.emit(); } }, onSuccess: (data, variables) => { log('โœ… AddToCart success'); pendingUpdates.delete(variables.productId); queryClient.invalidateQueries({ queryKey: ["cart"], refetchType: "active", }); }, }); } export function useRemoveFromCart() { const queryClient = useQueryClient(); return useMutation({ mutationFn: async (productId: number) => { log('๐Ÿ—‘๏ธ RemoveFromCart mutation:', productId); const params = new URLSearchParams({ product_id: String(productId) }); const response = await apiClient.patch("/carts", params.toString(), { headers: { "Content-Type": "application/x-www-form-urlencoded", }, }); if (typeof response.data === "object" && response.data.data) { return response.data.data; } if (typeof response.data === "string") { try { const parsed = JSON.parse(response.data); return parsed.data || []; } catch { return []; } } return []; }, onMutate: async (productId) => { while (updateLock) { await new Promise(resolve => setTimeout(resolve, 50)); } updateLock = true; await queryClient.cancelQueries({ queryKey: ["cart"] }); const previousCart = queryClient.getQueryData(["cart"]); queryClient.setQueryData(["cart"], (old) => { if (!old) return old; let updated = { ...old, data: [...old.data] }; pendingUpdates.forEach((pendingQty, pendingId) => { if (pendingId !== productId) { const idx = updated.data.findIndex( (item: any) => item.product?.id === pendingId ); if (idx !== -1) { updated.data[idx] = { ...updated.data[idx], product_quantity: pendingQty, }; } } }); updated.data = updated.data.filter( (item: any) => item.product?.id !== productId ); pendingUpdates.delete(productId); log('๐Ÿ—‘๏ธ Item removed optimistically:', productId); return updated; }); cartEvents.emit(); updateLock = false; return { previousCart }; }, onError: (error, variables, context) => { log('โŒ RemoveFromCart error:', error); if (context?.previousCart) { queryClient.setQueryData(["cart"], context.previousCart); cartEvents.emit(); } }, onSuccess: () => { log('โœ… RemoveFromCart success'); queryClient.invalidateQueries({ queryKey: ["cart"], refetchType: "active", }); }, }); } export function useCleanCart() { const queryClient = useQueryClient(); return useMutation({ mutationFn: async () => { log('๐Ÿงน CleanCart mutation'); const response = await apiClient.delete("/carts", { headers: { "Content-Type": "application/x-www-form-urlencoded", }, }); if (typeof response.data === "object" && response.data.data) { return response.data.data; } if (typeof response.data === "string") { try { const parsed = JSON.parse(response.data); return parsed.data || []; } catch { return []; } } return []; }, onMutate: async () => { while (updateLock) { await new Promise(resolve => setTimeout(resolve, 50)); } updateLock = true; await queryClient.cancelQueries({ queryKey: ["cart"] }); const previousCart = queryClient.getQueryData(["cart"]); queryClient.setQueryData(["cart"], (old) => { if (!old) return old; pendingUpdates.clear(); return { ...old, data: [] }; }); cartEvents.emit(); updateLock = false; return { previousCart }; }, onError: (error, variables, context) => { if (context?.previousCart) { queryClient.setQueryData(["cart"], context.previousCart); cartEvents.emit(); } }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["cart"] }); }, }); } export function useUpdateCartItemQuantity() { const queryClient = useQueryClient(); return useMutation({ mutationFn: async ({ productId, quantity, }: { productId: number; quantity: number; }) => { log('๐Ÿ”„ UpdateQuantity mutation:', { productId, quantity }); const params = new URLSearchParams({ product_id: String(productId), product_quantity: String(quantity), }); const response = await apiClient.post("/carts", params.toString(), { headers: { "Content-Type": "application/x-www-form-urlencoded", }, timeout: 15000, }); if (typeof response.data === "object" && response.data.data) { return response.data; } if (typeof response.data === "string") { try { return JSON.parse(response.data); } catch { return { message: "success", data: "Updated cart" }; } } return { message: "success", data: "Updated cart" }; }, onMutate: async ({ productId, quantity }) => { log('๐Ÿ”’ UpdateQuantity onMutate - Waiting for lock...'); while (updateLock) { await new Promise(resolve => setTimeout(resolve, 50)); } updateLock = true; log('๐Ÿ”“ Lock acquired'); await queryClient.cancelQueries({ queryKey: ["cart"] }); const previousCart = queryClient.getQueryData(["cart"]); log('๐Ÿ“ธ Previous cart state:', previousCart?.data.length, 'items'); queryClient.setQueryData(["cart"], (old) => { if (!old) return old; let updated = { ...old, data: [...old.data] }; pendingUpdates.forEach((pendingQty, pendingId) => { const idx = updated.data.findIndex( (item: any) => item.product?.id === pendingId ); if (idx !== -1) { updated.data[idx] = { ...updated.data[idx], product_quantity: pendingQty, }; } }); updated.data = updated.data.map((item: any) => item.product?.id === productId ? { ...item, product_quantity: quantity } : item ); pendingUpdates.set(productId, quantity); log('๐Ÿ’พ Pending update saved:', productId, 'โ†’', quantity); log('๐Ÿ”„ Cart updated optimistically:', updated.data.length, 'items'); return updated; }); cartEvents.emit(); updateLock = false; return { previousCart }; }, onError: (error, variables, context) => { log('โŒ UpdateQuantity error:', error); if (context?.previousCart) { queryClient.setQueryData(["cart"], context.previousCart); pendingUpdates.delete(variables.productId); cartEvents.emit(); } throw error; }, onSuccess: (data, variables) => { log('โœ… UpdateQuantity success'); pendingUpdates.delete(variables.productId); queryClient.invalidateQueries({ queryKey: ["cart"], refetchType: "none", }); }, }); } export function useCreateOrder() { const queryClient = useQueryClient(); return useMutation({ mutationFn: async (payload: { customer_name?: string; customer_phone: string; customer_address: string; shipping_method: string; payment_type_id: number; delivery_time?: string; delivery_at?: string; region: string; note?: string; }) => { const response = await apiClient.post("/orders", payload); return response.data; }, onSuccess: () => { pendingUpdates.clear(); queryClient.setQueryData(["cart"], (old) => { if (!old) return old; return { ...old, data: [] }; }); cartEvents.emit(); queryClient.invalidateQueries({ queryKey: ["orders"] }); }, onError: (error: any) => { console.error( "Create order error:", error.response?.data?.message || error.message ); }, }); } export function useCartCount() { const { data } = useCart(); return ( data?.data?.reduce( (sum: number, item: any) => sum + (item.product_quantity || 0), 0 ) || 0 ); }