Files
web-menu-react-version-/src/features/order/orderSlice.ts
2025-12-26 00:00:29 +03:00

836 lines
24 KiB
TypeScript

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 {
receiverName: string;
receiverPhone: string;
message: string;
senderName: string;
senderPhone: string;
senderEmail: string;
isSecret: boolean;
}
interface DiscountData {
value: number;
isGift: boolean;
isDiscount: boolean;
}
interface CartState {
restaurant: Partial<RestaurantDetails>;
items: CartItem[];
tmp: any;
specialRequest: string;
location: LocationData | null;
roomDetails: RoomDetailsType | null;
officeDetails: OfficeDetailsType | null;
giftDetails: GiftDetailsType | null;
coupon: string;
tip: string;
tables: 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;
order: any;
splitBillAmount: number;
customerName: string;
totalServices: number;
hiddenServices: number;
visibleServices: 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",
} as const;
// Utility functions for localStorage
const getFromLocalStorage = <T>(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<CartItem, "quantity" | "uniqueId">,
): 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, ""),
tables: 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, ""),
order: getFromLocalStorage(CART_STORAGE_KEYS.ORDER, null),
splitBillAmount: 0,
customerName: "",
totalServices: 8,
hiddenServices: 0,
visibleServices: 0,
};
const orderSlice = createSlice({
name: "order",
initialState,
reducers: {
reset() {
return initialState;
},
updateRestaurant(state, action: PayloadAction<Partial<RestaurantDetails>>) {
state.restaurant = action.payload;
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<CartItem, "quantity">;
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<string>) {
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.tables = [];
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<string>) {
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<string>) {
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<string>) {
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<string[]>) {
state.tables = action.payload;
// Sync to localStorage
if (typeof window !== "undefined") {
localStorage.setItem(
CART_STORAGE_KEYS.TABLES,
JSON.stringify(state.tables),
);
}
},
removeTable(state) {
state.tables = [];
// Sync to localStorage
if (typeof window !== "undefined") {
localStorage.removeItem(CART_STORAGE_KEYS.TABLES);
}
},
setTmp(state, action: PayloadAction<unknown>) {
state.tmp = action.payload;
},
updateLocation(state, action: PayloadAction<LocationData | null>) {
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<RoomDetailsType | null>) {
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<OfficeDetailsType | null>,
) {
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<GiftDetailsType | null>) {
state.giftDetails = action.payload;
// 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<string>) {
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<string>) {
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<string>) {
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<string>) {
state.plateCar = action.payload;
if (typeof window !== "undefined") {
localStorage.setItem(
CART_STORAGE_KEYS.PLATE,
JSON.stringify(state.plateCar),
);
}
},
updateOrderType(state, action: PayloadAction<OrderType>) {
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<boolean>) {
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<string>) {
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<DiscountData>) {
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<string>) {
state.pickupDate = action.payload;
if (typeof window !== "undefined") {
localStorage.setItem(
CART_STORAGE_KEYS.PICKUP_DATE,
JSON.stringify(state.pickupDate),
);
}
},
updatePickupTime(state, action: PayloadAction<string>) {
state.pickupTime = action.payload;
if (typeof window !== "undefined") {
localStorage.setItem(
CART_STORAGE_KEYS.PICKUP_TIME,
JSON.stringify(state.pickupTime),
);
}
},
updatePickUpType(state, action: PayloadAction<string>) {
state.pickupType = action.payload;
if (typeof window !== "undefined") {
localStorage.setItem(
CART_STORAGE_KEYS.PICKUP_TYPE,
JSON.stringify(state.pickupType),
);
}
},
updateOrder(state, action: PayloadAction<any>) {
state.order = action.payload;
if (typeof window !== "undefined") {
localStorage.setItem(
CART_STORAGE_KEYS.ORDER,
JSON.stringify(state.order),
);
}
},
updateSplitBillAmount(state, action: PayloadAction<number>) {
state.splitBillAmount = action.payload;
},
updateCustomerName(state, action: PayloadAction<string>) {
state.customerName = action.payload;
},
},
});
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,
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;