import { createSelector, createSlice, PayloadAction } from "@reduxjs/toolkit"; import { OrderType } from "pages/checkout/hooks/types"; import { RootState } from "redux/store"; import { CartItem, RestaurantDetails, Tax } from "utils/types/appTypes"; interface LocationData { lat: number; lng: number; address: string; } export interface RoomDetailsType { roomNo: string; floorNo: string; guestName: string; note: string; } export interface OfficeDetailsType { officeNo: string; floorNo: string; note: string; company: string; contactPerson: string; phone: string; } export interface GiftDetailsType { amount: number; receiverName: string; receiverPhone: string; message: string; senderName: string; senderPhone: string; senderEmail: string; isSecret: boolean; cardId: number; giftType: "items" | "vouchers" | "itemsAndVouchers"; } interface DiscountData { value: number; isGift: boolean; isDiscount: boolean; } interface CartState { restaurant: Partial; items: CartItem[]; tmp: any; specialRequest: string; location: LocationData | null; roomDetails: RoomDetailsType | null; officeDetails: OfficeDetailsType | null; giftDetails: GiftDetailsType | null; coupon: string; tip: string; table: string; estimateTime: Date; estimateTimeDate: Date; estimateTimeTime: string; collectionMethod: string; phone: string; paymentMethod: string; orderType: OrderType | ""; useLoyaltyPoints: boolean; loyaltyValidationError: string | null; scheduledDate: string; discount: DiscountData; plateCar: string; pickupDate: string; pickupTime: string; pickupType: string; estimateWay: string; order: any; splitBillAmount: number; customerName: string; totalServices: number; hiddenServices: number; visibleServices: number; fee: number; } // localStorage keys export const CART_STORAGE_KEYS = { ITEMS: "fascano_cart_items", SPECIAL_REQUEST: "fascano_special_request", COUPON: "fascano_coupon", TIP: "fascano_tip", TABLES: "fascano_tables", LOCATION: "fascano_location", ROOM_DETAILS: "fascano_room_details", OFFICE_DETAILS: "fascano_office_details", GIFT_DETAILS: "fascano_gift_details", ESTIMATE_TIME: "fascano_estimate_time", ESTIMATE_TIME_DATE: "fascano_estimate_time_date", ESTIMATE_TIME_TIME: "fascano_estimate_time_time", COLLECTION_METHOD: "fascano_collection_method", PHONE: "fascano_phone", PAYMENT_METHOD: "fascano_payment_method", ORDER_TYPE: "fascano_order_type", USE_LOYALTY_POINTS: "fascano_use_loyalty_points", LOYALTY_VALIDATION_ERROR: "fascano_loyalty_validation_error", RESTAURANT: "fascano_restaurant", SCHEDULED_DATE: "fascano_scheduled_date", DISCOUNT: "fascano_discount", PLATE: "fascano_plate_car", PICKUP_DATE: "fascano_pickup_date", PICKUP_TIME: "fascano_pickup_time", PICKUP_TYPE: "fascano_pickup_type", ORDER: "fascano_order", TOTAL_SERVICES: "fascano_total_services", HIDDEN_SERVICES: "fascano_hidden_services", VISIBLE_SERVICES: "fascano_visible_services", ESTIMATE_WAY: "fascano_estimate_way", CUSTOMER_NAME: "fascano_customer_name", } as const; // Utility functions for localStorage const getFromLocalStorage = (key: string, defaultValue: T): T => { if (typeof window === "undefined") return defaultValue; try { const item = localStorage.getItem(key); return item ? JSON.parse(item) : defaultValue; } catch (error) { console.warn(`Error reading ${key} from localStorage:`, error); return defaultValue; } }; // Generate a unique identifier for cart items based on product ID, variant, extras, and comment const generateUniqueId = ( item: Omit, ): string => { const variantStr = item.variant || ""; const extrasStr = item.extras ? item.extras.sort().join(",") : ""; const extrasGroupStr = item.extrasgroup ? item.extrasgroup.sort().join(",") : ""; const commentStr = item.comment || ""; return `${item.id}-${variantStr}-${extrasStr}-${extrasGroupStr}-${commentStr}`; }; const initialState: CartState = { items: getFromLocalStorage(CART_STORAGE_KEYS.ITEMS, []), tmp: null, specialRequest: getFromLocalStorage(CART_STORAGE_KEYS.SPECIAL_REQUEST, ""), location: getFromLocalStorage(CART_STORAGE_KEYS.LOCATION, null), roomDetails: getFromLocalStorage(CART_STORAGE_KEYS.ROOM_DETAILS, null), officeDetails: getFromLocalStorage(CART_STORAGE_KEYS.OFFICE_DETAILS, null), giftDetails: getFromLocalStorage(CART_STORAGE_KEYS.GIFT_DETAILS, null), coupon: getFromLocalStorage(CART_STORAGE_KEYS.COUPON, ""), tip: getFromLocalStorage(CART_STORAGE_KEYS.TIP, ""), table: getFromLocalStorage(CART_STORAGE_KEYS.TABLES, ""), estimateTime: new Date( getFromLocalStorage( CART_STORAGE_KEYS.ESTIMATE_TIME, new Date().toISOString(), ), ), estimateTimeDate: new Date( getFromLocalStorage( CART_STORAGE_KEYS.ESTIMATE_TIME_DATE, new Date().toISOString(), ), ), estimateTimeTime: getFromLocalStorage( CART_STORAGE_KEYS.ESTIMATE_TIME_TIME, "", ), collectionMethod: getFromLocalStorage( CART_STORAGE_KEYS.COLLECTION_METHOD, "", ), phone: getFromLocalStorage(CART_STORAGE_KEYS.PHONE, ""), paymentMethod: getFromLocalStorage(CART_STORAGE_KEYS.PAYMENT_METHOD, ""), orderType: getFromLocalStorage( CART_STORAGE_KEYS.ORDER_TYPE, "" as OrderType | "", ), useLoyaltyPoints: getFromLocalStorage( CART_STORAGE_KEYS.USE_LOYALTY_POINTS, false, ), loyaltyValidationError: getFromLocalStorage( CART_STORAGE_KEYS.LOYALTY_VALIDATION_ERROR, null, ), restaurant: getFromLocalStorage(CART_STORAGE_KEYS.RESTAURANT, { taxes: [] }), scheduledDate: getFromLocalStorage(CART_STORAGE_KEYS.SCHEDULED_DATE, ""), discount: getFromLocalStorage(CART_STORAGE_KEYS.DISCOUNT, { value: 0, isGift: false, isDiscount: false, }), plateCar: getFromLocalStorage(CART_STORAGE_KEYS.PLATE, ""), pickupDate: getFromLocalStorage(CART_STORAGE_KEYS.PICKUP_DATE, ""), pickupTime: getFromLocalStorage(CART_STORAGE_KEYS.PICKUP_TIME, ""), pickupType: getFromLocalStorage(CART_STORAGE_KEYS.PICKUP_TYPE, ""), estimateWay: getFromLocalStorage(CART_STORAGE_KEYS.ESTIMATE_WAY, ""), order: getFromLocalStorage(CART_STORAGE_KEYS.ORDER, null), splitBillAmount: 0, customerName: getFromLocalStorage(CART_STORAGE_KEYS.CUSTOMER_NAME, ""), totalServices: 8, hiddenServices: 0, visibleServices: 0, fee: 0, }; const orderSlice = createSlice({ name: "order", initialState, reducers: { reset() { return initialState; }, updateRestaurant(state, action: PayloadAction>) { state.restaurant = action.payload; state.fee = Number(action.payload.delivery_fees || 0); state.visibleServices = [ action.payload.dineIn, action.payload.delivery, action.payload.pickup, action.payload.gift, action.payload.toRoom, action.payload.toOffice, action.payload.is_schedule_order_enabled, action.payload.is_booking_enabled, ].filter(Boolean).length; state.hiddenServices = state.totalServices - state.visibleServices; if (typeof window !== "undefined") { localStorage.setItem( CART_STORAGE_KEYS.RESTAURANT, JSON.stringify(state.restaurant), ); localStorage.setItem( CART_STORAGE_KEYS.TOTAL_SERVICES, JSON.stringify(state.totalServices), ); localStorage.setItem( CART_STORAGE_KEYS.HIDDEN_SERVICES, JSON.stringify(state.hiddenServices), ); localStorage.setItem( CART_STORAGE_KEYS.VISIBLE_SERVICES, JSON.stringify(state.visibleServices), ); } }, addItem( state, action: PayloadAction<{ item: Omit; quantity: number; }>, ) { const { item, quantity } = action.payload; // Generate a unique ID for this item configuration const uniqueId = generateUniqueId(item); // Check if an item with the same configuration already exists const existingItem = state.items.find((i) => i.uniqueId === uniqueId); if (existingItem) { // Update quantity of existing item with same configuration state.items = state.items.map((i) => i.uniqueId === uniqueId ? { ...i, quantity: i.quantity + quantity } : i, ); } else { // Add new item with its unique identifier state.items = [...state.items, { ...item, quantity, uniqueId }]; } // Validate loyalty points if enabled if (state.useLoyaltyPoints) { const loyaltyItems = state.items.filter((item) => item.isHasLoyalty); if (loyaltyItems.length === 0) { state.loyaltyValidationError = "cart.noLoyaltyItemsInCart"; } else { state.loyaltyValidationError = null; } } // Sync to localStorage if (typeof window !== "undefined") { localStorage.setItem( CART_STORAGE_KEYS.ITEMS, JSON.stringify(state.items), ); localStorage.setItem( CART_STORAGE_KEYS.LOYALTY_VALIDATION_ERROR, JSON.stringify(state.loyaltyValidationError), ); } }, updateQuantity( state, action: PayloadAction<{ id: number | string; uniqueId: string; quantity: number; }>, ) { const { uniqueId, quantity } = action.payload; state.items = state.items.map((item) => item.uniqueId === uniqueId ? { ...item, quantity } : item, ); // Sync to localStorage if (typeof window !== "undefined") { localStorage.setItem( CART_STORAGE_KEYS.ITEMS, JSON.stringify(state.items), ); } }, removeItem(state, action: PayloadAction) { state.items = state.items.filter( (item) => item.uniqueId !== action.payload, ); // Validate loyalty points if enabled if (state.useLoyaltyPoints) { const loyaltyItems = state.items.filter((item) => item.isHasLoyalty); if (loyaltyItems.length === 0) { state.loyaltyValidationError = "cart.noLoyaltyItemsInCart"; } else { state.loyaltyValidationError = null; } } // Sync to localStorage if (typeof window !== "undefined") { localStorage.setItem( CART_STORAGE_KEYS.ITEMS, JSON.stringify(state.items), ); localStorage.setItem( CART_STORAGE_KEYS.LOYALTY_VALIDATION_ERROR, JSON.stringify(state.loyaltyValidationError), ); } }, clearCart(state) { state.items = []; state.specialRequest = ""; state.phone = ""; state.coupon = ""; state.tip = ""; state.table = ""; state.location = null; state.roomDetails = null; state.officeDetails = null; state.giftDetails = null; state.estimateTime = new Date(); state.estimateTimeDate = new Date(); state.estimateTimeTime = ""; state.collectionMethod = ""; state.paymentMethod = ""; state.loyaltyValidationError = null; // Clear all cart data from localStorage if (typeof window !== "undefined") { Object.values(CART_STORAGE_KEYS) .filter((key) => key !== CART_STORAGE_KEYS.ORDER_TYPE) .forEach((key) => { localStorage.removeItem(key); }); } }, updateSpecialRequest(state, action: PayloadAction) { state.specialRequest = action.payload; // Sync to localStorage if (typeof window !== "undefined") { localStorage.setItem( CART_STORAGE_KEYS.SPECIAL_REQUEST, JSON.stringify(state.specialRequest), ); } }, clearSpecialRequest(state) { state.specialRequest = ""; // Sync to localStorage if (typeof window !== "undefined") { localStorage.removeItem(CART_STORAGE_KEYS.SPECIAL_REQUEST); } }, updateCoupon(state, action: PayloadAction) { state.coupon = action.payload; // Sync to localStorage if (typeof window !== "undefined") { localStorage.setItem( CART_STORAGE_KEYS.COUPON, JSON.stringify(state.coupon), ); } }, updateTip(state, action: PayloadAction) { state.tip = action.payload; // Sync to localStorage if (typeof window !== "undefined") { localStorage.setItem(CART_STORAGE_KEYS.TIP, JSON.stringify(state.tip)); } }, updateTables(state, action: PayloadAction) { state.table = action.payload; // Sync to localStorage if (typeof window !== "undefined") { localStorage.setItem( CART_STORAGE_KEYS.TABLES, JSON.stringify(state.table), ); } }, removeTable(state) { state.table = ""; // Sync to localStorage if (typeof window !== "undefined") { localStorage.removeItem(CART_STORAGE_KEYS.TABLES); } }, setTmp(state, action: PayloadAction) { state.tmp = action.payload; }, updateLocation(state, action: PayloadAction) { state.location = action.payload; // Sync to localStorage if (typeof window !== "undefined") { localStorage.setItem( CART_STORAGE_KEYS.LOCATION, JSON.stringify(state.location), ); } }, clearLocation(state) { state.location = null; // Sync to localStorage if (typeof window !== "undefined") { localStorage.removeItem(CART_STORAGE_KEYS.LOCATION); } }, updateRoomDetails(state, action: PayloadAction) { state.roomDetails = action.payload; // Sync to localStorage if (typeof window !== "undefined") { localStorage.setItem( CART_STORAGE_KEYS.ROOM_DETAILS, JSON.stringify(state.roomDetails), ); } }, updateOfficeDetails( state, action: PayloadAction, ) { state.officeDetails = action.payload; // Sync to localStorage if (typeof window !== "undefined") { localStorage.setItem( CART_STORAGE_KEYS.OFFICE_DETAILS, JSON.stringify(state.officeDetails), ); } }, updateGiftDetails( state, action: PayloadAction | null>, ) { state.giftDetails = { ...state.giftDetails, ...action.payload, } as GiftDetailsType; // Sync to localStorage if (typeof window !== "undefined") { localStorage.setItem( CART_STORAGE_KEYS.GIFT_DETAILS, JSON.stringify(state.giftDetails), ); } }, updateEstimateTime( state, action: PayloadAction<{ date: Date; time: string }>, ) { state.estimateTime = action.payload.date; state.estimateTimeDate = action.payload.date; state.estimateTimeTime = action.payload.time; // Sync to localStorage if (typeof window !== "undefined") { localStorage.setItem( CART_STORAGE_KEYS.ESTIMATE_TIME, JSON.stringify(state.estimateTime.toISOString()), ); localStorage.setItem( CART_STORAGE_KEYS.ESTIMATE_TIME_DATE, JSON.stringify(state.estimateTimeDate.toISOString()), ); localStorage.setItem( CART_STORAGE_KEYS.ESTIMATE_TIME_TIME, JSON.stringify(state.estimateTimeTime), ); } }, updateCollectionMethod(state, action: PayloadAction) { state.collectionMethod = action.payload; // Sync to localStorage if (typeof window !== "undefined") { localStorage.setItem( CART_STORAGE_KEYS.COLLECTION_METHOD, JSON.stringify(state.collectionMethod), ); } }, updatePhone(state, action: PayloadAction) { state.phone = action.payload; // Sync to localStorage if (typeof window !== "undefined") { localStorage.setItem( CART_STORAGE_KEYS.PHONE, JSON.stringify(state.phone), ); } }, updatePaymentMethod(state, action: PayloadAction) { state.paymentMethod = action.payload; // Sync to localStorage if (typeof window !== "undefined") { localStorage.setItem( CART_STORAGE_KEYS.PAYMENT_METHOD, JSON.stringify(state.paymentMethod), ); } }, updatePlateCar(state, action: PayloadAction) { state.plateCar = action.payload; if (typeof window !== "undefined") { localStorage.setItem( CART_STORAGE_KEYS.PLATE, JSON.stringify(state.plateCar), ); } }, updateOrderType(state, action: PayloadAction) { state.orderType = action.payload; // Sync to localStorage if (typeof window !== "undefined") { localStorage.setItem( CART_STORAGE_KEYS.ORDER_TYPE, JSON.stringify(state.orderType), ); } }, updateUseLoyaltyPoints(state, action: PayloadAction) { state.useLoyaltyPoints = action.payload; // Validate loyalty points usage if (action.payload) { const loyaltyItems = state.items.filter((item) => item.isHasLoyalty); if (loyaltyItems.length === 0) { state.loyaltyValidationError = "cart.noLoyaltyItemsInCart"; } else { state.loyaltyValidationError = null; } } else { state.loyaltyValidationError = null; } // Sync to localStorage if (typeof window !== "undefined") { localStorage.setItem( CART_STORAGE_KEYS.USE_LOYALTY_POINTS, JSON.stringify(state.useLoyaltyPoints), ); localStorage.setItem( CART_STORAGE_KEYS.LOYALTY_VALIDATION_ERROR, JSON.stringify(state.loyaltyValidationError), ); } }, validateLoyaltyPoints(state) { if (state.useLoyaltyPoints) { const loyaltyItems = state.items.filter((item) => item.isHasLoyalty); if (loyaltyItems.length === 0) { state.loyaltyValidationError = "cart.noLoyaltyItemsInCart"; } else { state.loyaltyValidationError = null; } } else { state.loyaltyValidationError = null; } // Sync to localStorage if (typeof window !== "undefined") { localStorage.setItem( CART_STORAGE_KEYS.LOYALTY_VALIDATION_ERROR, JSON.stringify(state.loyaltyValidationError), ); } }, clearLoyaltyValidationError(state) { state.loyaltyValidationError = null; // Sync to localStorage if (typeof window !== "undefined") { localStorage.setItem( CART_STORAGE_KEYS.LOYALTY_VALIDATION_ERROR, JSON.stringify(state.loyaltyValidationError), ); } }, updateScheduledDate(state, action: PayloadAction) { state.scheduledDate = action.payload; // Sync to localStorage if (typeof window !== "undefined") { localStorage.setItem( CART_STORAGE_KEYS.SCHEDULED_DATE, JSON.stringify(state.scheduledDate), ); } }, updateDiscount(state, action: PayloadAction) { state.discount = action.payload; // Sync to localStorage if (typeof window !== "undefined") { localStorage.setItem( CART_STORAGE_KEYS.DISCOUNT, JSON.stringify(state.discount), ); } }, updatePickupDate(state, action: PayloadAction) { state.pickupDate = action.payload; if (typeof window !== "undefined") { localStorage.setItem( CART_STORAGE_KEYS.PICKUP_DATE, JSON.stringify(state.pickupDate), ); } }, updatePickupTime(state, action: PayloadAction) { state.pickupTime = action.payload; if (typeof window !== "undefined") { localStorage.setItem( CART_STORAGE_KEYS.PICKUP_TIME, JSON.stringify(state.pickupTime), ); } }, updatePickUpType(state, action: PayloadAction) { state.pickupType = action.payload; if (typeof window !== "undefined") { localStorage.setItem( CART_STORAGE_KEYS.PICKUP_TYPE, JSON.stringify(state.pickupType), ); } }, updateEstimateWay(state, action: PayloadAction) { state.estimateWay = action.payload; if (typeof window !== "undefined") { localStorage.setItem( CART_STORAGE_KEYS.ESTIMATE_WAY, JSON.stringify(state.estimateWay), ); } }, updateOrder(state, action: PayloadAction) { state.order = action.payload; if (typeof window !== "undefined") { localStorage.setItem( CART_STORAGE_KEYS.ORDER, JSON.stringify(state.order), ); } }, updateSplitBillAmount(state, action: PayloadAction) { state.splitBillAmount = action.payload; }, updateCustomerName(state, action: PayloadAction) { state.customerName = action.payload; if (typeof window !== "undefined") { localStorage.setItem( CART_STORAGE_KEYS.CUSTOMER_NAME, JSON.stringify(state.customerName), ); } }, }, }); export const { addItem, updateQuantity, removeItem, clearCart, updateSpecialRequest, clearSpecialRequest, updateCoupon, updateTip, updateTables, removeTable, setTmp, updateLocation, clearLocation, updateRoomDetails, updateOfficeDetails, updateGiftDetails, updateEstimateTime, updateCollectionMethod, updatePhone, updatePaymentMethod, updatePlateCar, updateOrderType, updateUseLoyaltyPoints, validateLoyaltyPoints, clearLoyaltyValidationError, reset, updateRestaurant, updateScheduledDate, updateDiscount, updatePickupDate, updatePickupTime, updatePickUpType, updateEstimateWay, updateOrder, updateSplitBillAmount, updateCustomerName, } = orderSlice.actions; // Tax calculation helper functions const calculateTaxAmount = ( state: RootState, amount: number, tax: Tax, ): number => { const percentage = parseFloat(tax.percentage); return (((state.order.restaurant?.vat || 0) + percentage) * amount) / 100; }; const calculateTotalTax = ( state: RootState, subtotal: number, taxes: Tax[], ): number => { return taxes .filter((tax) => tax.is_active === 1) .reduce( (total, tax) => total + calculateTaxAmount(state, subtotal, tax), 0, ); }; // Selectors export const selectCart = (state: RootState) => state.order; export const selectCartItems = (state: RootState) => state.order.items; export const selectCartTotal = (state: RootState) => state.order.items.reduce( // (total, item) => total + item.price * item.quantity, (total, item) => { const itemTotalPrice = item.price * item.quantity; return total + itemTotalPrice; }, 0, ); export const selectCartItemsQuantity = (uniqueId: string) => (state: RootState) => { const item = state.order.items.find((i) => i.uniqueId === uniqueId); return item ? item.quantity : 0; }; // Keep backward compatibility for components that still use id export const selectCartItemsQuantityById = (id: number | string) => (state: RootState) => { const item = state.order.items.find((i) => i.id === id); return item ? item.quantity : 0; }; // Loyalty selectors const selectOrderItems = (state: RootState) => state.order.items; export const selectLoyaltyItems = createSelector([selectOrderItems], (items) => items.filter((item) => item.isHasLoyalty), ); export const selectHighestPricedLoyaltyItem = (state: RootState) => { const loyaltyItems = selectLoyaltyItems(state); if (loyaltyItems.length === 0) return null; return loyaltyItems.reduce((highest, current) => current.price > highest.price ? current : highest, ); }; export const selectDiscountTotal = (state: RootState) => (state.order.discount.value / 100) * selectCartTotal(state) + (state.order.useLoyaltyPoints && state.order.restaurant?.is_loyalty_enabled === 1 ? selectHighestPricedLoyaltyItem(state)?.price || 0 : 0); export const selectLoyaltyValidation = createSelector( [(state: RootState) => state.order.useLoyaltyPoints, selectLoyaltyItems], (useLoyaltyPoints, loyaltyItems) => ({ canUseLoyaltyPoints: loyaltyItems.length > 0, hasLoyaltyItems: loyaltyItems.length > 0, loyaltyItemsCount: loyaltyItems.length, errorMessage: useLoyaltyPoints && loyaltyItems.length === 0 ? "cart.noLoyaltyItemsInCart" : null, }), ); // Tax selectors export const selectTaxes = (state: RootState) => state.order.restaurant.taxes; export const selectTaxAmount = (state: RootState) => { const subtotal = selectCartTotal(state) - selectDiscountTotal(state); const taxes = selectTaxes(state); return calculateTotalTax(state, subtotal, taxes || []); }; export const selectGrandTotal = (state: RootState) => { const totalDiscount = selectDiscountTotal(state); const taxAmount = selectTaxAmount(state); const subtotal = selectCartTotal(state); const deliveryFee = state.order.orderType === OrderType.Delivery ? Number(state.order.restaurant?.delivery_fees) || 0 : 0; return ( subtotal + taxAmount - totalDiscount + deliveryFee - state.order.splitBillAmount ); }; export default orderSlice.reducer;