freshyo/apps/user-ui/hooks/cart-query-hooks.tsx
2026-09-06 18:47:21 +05:30

367 lines
13 KiB
TypeScript

import { useCentralProductStore } from '@/src/store/centralProductStore';
import { useCentralSlotStore } from '@/src/store/centralSlotStore';
import { Alert } from 'react-native';
import { useQuery, useMutation, useQueryClient, UseQueryResult, UseMutationResult } from '@tanstack/react-query';
import type { CartData as SharedCartData, ProductSummaryCore } from '@packages/shared';
import { StorageServiceCasual } from 'common-ui/src/services/StorageServiceCasual';
// Cart type definition
export type CartType = "regular" | "flash";
const getCartStorageKey = (cartType: CartType = "regular"): string => {
return cartType === "flash" ? "flash_cart_items" : "cart_items";
};
type LocalCartItem = Omit<CartItem, 'subtotal'>
// Cart product summary — anchored to the shared ProductSummaryCore; the cart
// view keeps most display fields optional and adds the flash flags.
type ProductSummary = Omit<
ProductSummaryCore,
'name' | 'images' | 'unitNotation' | 'marketPrice'
> & {
isFlashAvailable: boolean
name?: string
flashPrice?: number | null
images?: string[] | null
productQuantity?: number
unitNotation?: string
marketPrice?: string | null
}
export interface CartItem {
id: number;
skuId: number;
quantity: number;
addedAt: string;
subtotal: number;
slotId: number;
}
type CartData = SharedCartData<CartItem>
interface UseGetCartOptions {
refetchOnWindowFocus?: boolean;
enabled?: boolean;
}
interface UseGetCartReturn {
data: CartData | undefined;
isLoading: boolean;
error: Error | null;
refetch: () => Promise<UseQueryResult<CartData, Error>>;
cartItems: CartItem[];
totalItems: number;
totalPrice: number;
isEmpty: boolean;
hasItems: boolean;
}
interface AddToCartVariables {
skuId: number;
quantity: number;
slotId: number;
}
interface UpdateCartVariables {
itemId: number;
quantity: number;
}
interface RemoveCartVariables {
itemId: number;
}
interface MutationOptions<TData, TVariables> {
onSuccess?: (data: TData, variables: TVariables) => void;
onError?: (error: Error) => void;
showSuccessAlert?: boolean;
showErrorAlert?: boolean;
refetchCart?: boolean;
}
interface UseAddToCartReturn {
mutate: UseMutationResult<LocalCartItem[], Error, AddToCartVariables>['mutate'];
mutateAsync: UseMutationResult<LocalCartItem[], Error, AddToCartVariables>['mutateAsync'];
isLoading: boolean;
error: Error | null;
data: LocalCartItem[] | undefined;
addToCart: (skuId: number, quantity?: number, slotId?: number, onSettled?: (data: LocalCartItem[] | undefined, error: Error | null) => void) => void;
addToCartAsync: (skuId: number, quantity?: number, slotId?: number) => Promise<LocalCartItem[]>;
}
interface UseUpdateCartItemReturn {
mutate: UseMutationResult<LocalCartItem[], Error, UpdateCartVariables>['mutate'];
mutateAsync: UseMutationResult<LocalCartItem[], Error, UpdateCartVariables>['mutateAsync'];
isLoading: boolean;
error: Error | null;
data: LocalCartItem[] | undefined;
updateCartItem: (itemId: number, quantity: number) => void;
updateCartItemAsync: (itemId: number, quantity: number) => Promise<LocalCartItem[]>;
}
interface UseRemoveFromCartReturn {
mutate: UseMutationResult<LocalCartItem[], Error, RemoveCartVariables>['mutate'];
mutateAsync: UseMutationResult<LocalCartItem[], Error, RemoveCartVariables>['mutateAsync'];
isLoading: boolean;
error: Error | null;
data: LocalCartItem[] | undefined;
removeFromCart: (itemId: number) => void;
removeFromCartAsync: (itemId: number) => Promise<LocalCartItem[]>;
}
const getLocalCart = async (cartType: CartType = "regular"): Promise<LocalCartItem[]> => {
const key = getCartStorageKey(cartType);
const data = await StorageServiceCasual.getItem(key);
return data ? JSON.parse(data) : [];
};
const saveLocalCart = async (items: LocalCartItem[], cartType: CartType = "regular"): Promise<void> => {
const key = getCartStorageKey(cartType);
await StorageServiceCasual.setItem(key, JSON.stringify(items));
await getLocalCart(cartType);
};
const getNextCartItemId = (items: LocalCartItem[]): number => {
const maxId = items.length > 0 ? Math.max(...items.map(item => item.id)) : 0;
return maxId + 1;
};
const addToLocalCart = async (skuId: number, quantity: number, slotId: number | undefined, cartType: CartType = "regular"): Promise<LocalCartItem[]> => {
const items = await getLocalCart(cartType);
const existingIndex = items.findIndex(item => item.skuId === skuId);
if (existingIndex >= 0) {
items[existingIndex].quantity += quantity;
if (slotId !== undefined) {
items[existingIndex].slotId = slotId;
}
} else {
const newId = getNextCartItemId(items);
const cartItem: LocalCartItem = {
id: newId,
skuId,
quantity,
slotId: slotId ?? 0,
addedAt: new Date().toISOString(),
};
items.push(cartItem);
}
await saveLocalCart(items, cartType);
return items;
};
const updateLocalCartItem = async (itemId: number, quantity: number, cartType: CartType = "regular"): Promise<LocalCartItem[]> => {
const items = await getLocalCart(cartType);
const item = items.find(i => i.id === itemId);
if (item) {
item.quantity = quantity;
await saveLocalCart(items, cartType);
}
return items;
};
const removeFromLocalCart = async (itemId: number, cartType: CartType = "regular"): Promise<LocalCartItem[]> => {
const items = await getLocalCart(cartType);
const filtered = items.filter(i => i.id !== itemId);
await saveLocalCart(filtered, cartType);
return filtered;
};
const clearLocalCart = async (cartType: CartType = "regular"): Promise<void> => {
const key = getCartStorageKey(cartType);
await StorageServiceCasual.setItem(key, JSON.stringify([]));
};
export function useGetCart(options: UseGetCartOptions = {}, cartType: CartType = "regular"): UseGetCartReturn {
const productsById = useCentralProductStore((state) => state.productsById);
const productSlotsMap = useCentralSlotStore((state) => state.productSlotsMap);
const query: UseQueryResult<CartData, Error> = useQuery({
queryKey: [`local-cart-${cartType}`],
queryFn: async (): Promise<CartData> => {
const cartItems = await getLocalCart(cartType);
const productMap: Record<number, Omit<ProductSummary, 'isOutOfStock' | 'isFlashAvailable'>> = Object.fromEntries(
Object.values(productsById).map((p) => [
p.id,
{
id: p.id,
price: String(p.price),
incrementStep: p.incrementStep,
marketPrice: p.marketPrice === null || p.marketPrice === undefined ? null : String(p.marketPrice),
name: p.name,
flashPrice: p.flashPrice,
images: p.images,
productQuantity: p.productQuantity,
unitNotation: p.unitNotation,
},
])
);
const items: CartItem[] = cartItems
.map((cartItem): CartItem | null => {
const productBasic = productMap[cartItem.skuId];
const productAvailability = productSlotsMap[cartItem.skuId];
if (!productBasic || !productAvailability) return null;
return {
id: cartItem.id,
skuId: cartItem.skuId,
quantity: cartItem.quantity,
addedAt: cartItem.addedAt,
subtotal: Number(productBasic.price) * cartItem.quantity,
slotId: cartItem.slotId,
};
})
.filter((item): item is CartItem => item !== null);
const totalAmount = items.reduce((sum, item) => sum + item.subtotal, 0);
return {
items,
totalItems: items.length,
totalAmount,
};
},
refetchOnWindowFocus: options?.refetchOnWindowFocus ?? true,
enabled: (options?.enabled ?? true) && Object.keys(productsById).length > 0,
});
return {
data: query.data,
isLoading: query.isLoading,
error: query.error,
refetch: query.refetch,
cartItems: query.data?.items ?? [],
totalItems: query.data?.totalItems ?? 0,
totalPrice: query.data?.totalAmount ?? 0,
isEmpty: !(query.data?.items?.length ?? 0),
hasItems: Boolean(query.data?.items?.length),
};
}
export function useAddToCart(options: MutationOptions<LocalCartItem[], AddToCartVariables> = {}, cartType: CartType = "regular"): UseAddToCartReturn {
const queryClient = useQueryClient();
const mutation: UseMutationResult<LocalCartItem[], Error, AddToCartVariables> = useMutation({
mutationFn: async ({ skuId, quantity, slotId }: AddToCartVariables): Promise<LocalCartItem[]> => {
return await addToLocalCart(skuId, quantity, slotId, cartType);
},
onSuccess: (data: LocalCartItem[], variables: AddToCartVariables) => {
queryClient.invalidateQueries({ queryKey: [`local-cart-${cartType}`] });
if (options?.showSuccessAlert !== false) {
Alert.alert("Success", "Item added to cart!");
}
options?.onSuccess?.(data, variables);
},
onError: (error: Error) => {
if (options?.showErrorAlert !== false) {
Alert.alert("Error", error.message || "Failed to add item to cart");
}
options?.onError?.(error);
},
});
const addToCart = (skuId: number, quantity = 1, slotId?: number, onSettled?: (data: LocalCartItem[] | undefined, error: Error | null) => void): void => {
if (slotId == null) {
throw new Error('slotId is required for adding to cart');
}
mutation.mutate({ skuId, quantity, slotId }, {
onSettled: (data: LocalCartItem[] | undefined, error: Error | null) => {
onSettled?.(data, error);
}
});
};
return {
mutate: mutation.mutate,
mutateAsync: mutation.mutateAsync,
isLoading: mutation.isPending,
error: mutation.error,
data: mutation.data,
addToCart,
addToCartAsync: (skuId: number, quantity = 1, slotId?: number): Promise<LocalCartItem[]> => {
if (slotId == null) {
throw new Error('slotId is required for adding to cart');
}
return mutation.mutateAsync({ skuId, quantity, slotId });
},
};
}
export function useUpdateCartItem(options: MutationOptions<LocalCartItem[], UpdateCartVariables> = {}, cartType: CartType = "regular"): UseUpdateCartItemReturn {
const queryClient = useQueryClient();
const mutation: UseMutationResult<LocalCartItem[], Error, UpdateCartVariables> = useMutation({
mutationFn: async ({ itemId, quantity }: UpdateCartVariables): Promise<LocalCartItem[]> => {
return await updateLocalCartItem(itemId, quantity, cartType);
},
onSuccess: (data: LocalCartItem[], variables: UpdateCartVariables) => {
queryClient.invalidateQueries({ queryKey: [`local-cart-${cartType}`] });
if (options?.showSuccessAlert !== false) {
Alert.alert("Success", "Cart item updated!");
}
options?.onSuccess?.(data, variables);
},
onError: (error: Error) => {
if (options?.showErrorAlert !== false) {
Alert.alert("Error", error.message || "Failed to update cart item");
}
options?.onError?.(error);
},
});
return {
mutate: mutation.mutate,
mutateAsync: mutation.mutateAsync,
isLoading: mutation.isPending,
error: mutation.error,
data: mutation.data,
updateCartItem: (itemId: number, quantity: number): void =>
mutation.mutate({ itemId, quantity }),
updateCartItemAsync: (itemId: number, quantity: number): Promise<LocalCartItem[]> =>
mutation.mutateAsync({ itemId, quantity }),
};
}
export function useRemoveFromCart(options: MutationOptions<LocalCartItem[], RemoveCartVariables> = {}, cartType: CartType = "regular"): UseRemoveFromCartReturn {
const queryClient = useQueryClient();
const mutation: UseMutationResult<LocalCartItem[], Error, RemoveCartVariables> = useMutation({
mutationFn: async ({ itemId }: RemoveCartVariables): Promise<LocalCartItem[]> => {
return await removeFromLocalCart(itemId, cartType);
},
onSuccess: (data: LocalCartItem[], variables: RemoveCartVariables) => {
queryClient.invalidateQueries({ queryKey: [`local-cart-${cartType}`] });
if (options?.showSuccessAlert !== false) {
Alert.alert("Success", "Item removed from cart!");
}
options?.onSuccess?.(data, variables);
},
onError: (error: Error) => {
if (options?.showErrorAlert !== false) {
Alert.alert("Error", error.message || "Failed to remove item from cart");
}
options?.onError?.(error);
},
});
return {
mutate: mutation.mutate,
mutateAsync: mutation.mutateAsync,
isLoading: mutation.isPending,
error: mutation.error,
data: mutation.data,
removeFromCart: (itemId: number): void =>
mutation.mutate({ itemId }),
removeFromCartAsync: (itemId: number): Promise<LocalCartItem[]> =>
mutation.mutateAsync({ itemId }),
};
}
// Export clear cart function for direct use
export { clearLocalCart };