freshyo/apps/user-ui/hooks/cart-query-hooks.tsx
2026-03-11 16:31:23 +05:30

396 lines
13 KiB
TypeScript

import { useAllProducts } from '@/src/hooks/prominent-api-hooks';
import { useCentralSlotStore } from '@/src/store/centralSlotStore';
import { Alert } from 'react-native';
import { useQuery, useMutation, useQueryClient, UseQueryResult, UseMutationResult } from '@tanstack/react-query';
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";
};
interface LocalCartItem {
id: number;
productId: number;
quantity: number;
slotId: number;
addedAt: string;
}
interface ProductSummary {
id: number;
price: string;
incrementStep: number;
isOutOfStock: boolean;
isFlashAvailable: boolean;
name?: string;
flashPrice?: string | null;
images?: string[];
productQuantity?: number;
unitNotation?: string;
marketPrice?: string | null;
}
export interface CartItem {
id: number;
productId: number;
quantity: number;
addedAt: string;
product: ProductSummary;
subtotal: number;
slotId: number;
}
interface CartData {
items: CartItem[];
totalItems: number;
totalAmount: number;
}
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 {
productId: 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: (productId: number, quantity?: number, slotId?: number, onSettled?: (data: LocalCartItem[] | undefined, error: Error | null) => void) => void;
addToCartAsync: (productId: 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[]>;
}
interface AllProductsResponse {
products: Array<{
id: number;
price: number;
incrementStep: number;
marketPrice?: number | null;
name?: string;
flashPrice?: string | null;
images?: string[];
productQuantity?: number;
unitNotation?: string;
}>;
}
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 (productId: number, quantity: number, slotId: number | undefined, cartType: CartType = "regular"): Promise<LocalCartItem[]> => {
const items = await getLocalCart(cartType);
const existingIndex = items.findIndex(item => item.productId === productId);
if (existingIndex >= 0) {
items[existingIndex].quantity += quantity;
if (slotId !== undefined) {
items[existingIndex].slotId = slotId;
}
} else {
const newId = getNextCartItemId(items);
const cartItem: LocalCartItem = {
id: newId,
productId,
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 { data: products } = useAllProducts() as { data: AllProductsResponse | undefined };
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(
products?.products?.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.productId];
const productAvailability = productSlotsMap[cartItem.productId];
if (!productBasic || !productAvailability) return null;
return {
id: cartItem.id,
productId: cartItem.productId,
quantity: cartItem.quantity,
addedAt: cartItem.addedAt,
product: {
...productBasic,
isOutOfStock: productAvailability.isOutOfStock,
isFlashAvailable: productAvailability.isFlashAvailable,
},
incrementStep: productBasic.incrementStep,
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) && !!products,
});
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 ({ productId, quantity, slotId }: AddToCartVariables): Promise<LocalCartItem[]> => {
return await addToLocalCart(productId, 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 = (productId: 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({ productId, 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: (productId: number, quantity = 1, slotId?: number): Promise<LocalCartItem[]> => {
if (slotId == null) {
throw new Error('slotId is required for adding to cart');
}
return mutation.mutateAsync({ productId, 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 };