375 lines
13 KiB
TypeScript
375 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;
|
|
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[]>;
|
|
}
|
|
|
|
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();
|
|
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,
|
|
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 };
|