apply coupon discount

This commit is contained in:
2025-11-09 21:58:56 +03:00
parent b327d16260
commit 4389d267d4
8 changed files with 160 additions and 103 deletions

View File

@@ -3,6 +3,7 @@ import ArabicPrice from "components/ArabicPrice";
import {
selectCart,
selectCartTotal,
selectDiscountTotal,
selectGrandTotal,
selectHighestPricedLoyaltyItem,
selectLoyaltyValidation,
@@ -30,6 +31,7 @@ export default function OrderSummary() {
const highestLoyaltyItem = useAppSelector(selectHighestPricedLoyaltyItem);
const taxAmount = useAppSelector(selectTaxAmount);
const grandTotal = useAppSelector(selectGrandTotal);
const discountAmount = useAppSelector(selectDiscountTotal);
const isHasLoyaltyGift =
(restaurant?.loyalty_stamps ?? 0) -
@@ -54,13 +56,7 @@ export default function OrderSummary() {
)}
<div className={styles.summaryRow}>
<ProText type="secondary">{t("cart.discount")}</ProText>
<ArabicPrice
price={
useLoyaltyPoints && restaurant?.is_loyalty_enabled === 1
? highestLoyaltyItem?.price || 0
: 0
}
/>
<ArabicPrice price={discountAmount} />
</div>
<div className={styles.summaryRow}>
<ProText type="secondary">{t("cart.tax")}</ProText>

View File

@@ -35,6 +35,12 @@ export interface GiftDetailsType {
isSecret: boolean;
}
interface DiscountData {
value: number;
isGift: boolean;
isDiscount: boolean;
}
interface CartState {
restaurant: Partial<RestaurantDetails>;
items: CartItem[];
@@ -57,6 +63,7 @@ interface CartState {
useLoyaltyPoints: boolean;
loyaltyValidationError: string | null;
scheduledDate: string;
discount: DiscountData;
}
// localStorage keys
@@ -81,6 +88,7 @@ export const CART_STORAGE_KEYS = {
LOYALTY_VALIDATION_ERROR: "fascano_loyalty_validation_error",
RESTAURANT: "fascano_restaurant",
SCHEDULED_DATE: "fascano_scheduled_date",
DISCOUNT: "fascano_discount",
} as const;
// Utility functions for localStorage
@@ -97,11 +105,15 @@ const getFromLocalStorage = <T>(key: string, defaultValue: T): T => {
};
// 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 || '';
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}`;
};
@@ -153,6 +165,11 @@ const initialState: CartState = {
),
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,
}),
};
const orderSlice = createSlice({
@@ -189,7 +206,9 @@ const orderSlice = createSlice({
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,
i.uniqueId === uniqueId
? { ...i, quantity: i.quantity + quantity }
: i,
);
} else {
// Add new item with its unique identifier
@@ -220,7 +239,11 @@ const orderSlice = createSlice({
},
updateQuantity(
state,
action: PayloadAction<{ id: number | string; uniqueId: string; quantity: number }>,
action: PayloadAction<{
id: number | string;
uniqueId: string;
quantity: number;
}>,
) {
const { uniqueId, quantity } = action.payload;
state.items = state.items.map((item) =>
@@ -236,7 +259,9 @@ const orderSlice = createSlice({
}
},
removeItem(state, action: PayloadAction<string>) {
state.items = state.items.filter((item) => item.uniqueId !== action.payload);
state.items = state.items.filter(
(item) => item.uniqueId !== action.payload,
);
// Validate loyalty points if enabled
if (state.useLoyaltyPoints) {
@@ -538,6 +563,17 @@ const orderSlice = createSlice({
);
}
},
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),
);
}
},
},
});
@@ -569,6 +605,7 @@ export const {
reset,
updateRestaurant,
updateScheduledDate,
updateDiscount,
} = orderSlice.actions;
// Tax calculation helper functions
@@ -586,11 +623,13 @@ const calculateTotalTax = (subtotal: number, taxes: Tax[]): number => {
// 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,
0,
);
export const selectCartItemsQuantity =
(uniqueId: string) => (state: RootState) => {
const item = state.order.items.find((i) => i.uniqueId === uniqueId);
@@ -617,6 +656,13 @@ export const selectHighestPricedLoyaltyItem = (state: RootState) => {
);
};
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 = (state: RootState) => {
const useLoyaltyPoints = state.order.useLoyaltyPoints;
const loyaltyItems = selectLoyaltyItems(state);
@@ -636,13 +682,13 @@ export const selectLoyaltyValidation = (state: RootState) => {
export const selectTaxes = (state: RootState) => state.order.restaurant.taxes;
export const selectTaxAmount = (state: RootState) => {
const subtotal = selectCartTotal(state);
const subtotal = selectCartTotal(state) - selectDiscountTotal(state);
const taxes = selectTaxes(state);
return calculateTotalTax(subtotal, taxes || []);
};
export const selectGrandTotal = (state: RootState) => {
const loyaltyDiscount = selectHighestPricedLoyaltyItem(state)?.price || 0;
const totalDiscount = selectDiscountTotal(state);
const taxAmount = selectTaxAmount(state);
const subtotal = selectCartTotal(state);
const deliveryFee =
@@ -650,11 +696,7 @@ export const selectGrandTotal = (state: RootState) => {
? Number(state.order.restaurant?.delivery_fees) || 0
: 0;
return (
subtotal +
taxAmount -
(state.order.useLoyaltyPoints && state.order.restaurant?.is_loyalty_enabled === 1 ? loyaltyDiscount : 0) +
deliveryFee
);};
return subtotal + taxAmount - totalDiscount + deliveryFee;
};
export default orderSlice.reducer;

View File

@@ -1,61 +1,76 @@
import { Button, Form, Input, message } from "antd";
import { CouponBottomSheet } from "components/CustomBottomSheet/CouponBottomSheet.tsx";
import { CouponDialog } from "components/CustomBottomSheet/CouponDialog.tsx";
import CouponHeartIcon from "components/Icons/cart/CouponHeart.tsx";
import DonateIcon from "components/Icons/cart/DonateIcon.tsx";
import ProInputCard from "components/ProInputCard/ProInputCard.tsx";
import ProText from "components/ProText.tsx";
import { selectCart, updateCoupon } from "features/order/orderSlice.ts";
import useBreakPoint from "hooks/useBreakPoint.ts";
import {
selectCart,
updateCoupon,
updateDiscount,
} from "features/order/orderSlice.ts";
import styles from "pages/cart/cart.module.css";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useGetDiscountMutation } from "redux/api/others";
import { useAppDispatch, useAppSelector } from "redux/hooks.ts";
import { colors } from "ThemeConstants.ts";
export default function CouponCard() {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const { restaurant } = useAppSelector((state) => state.order);
const { coupon } = useAppSelector(selectCart);
const { isDesktop } = useBreakPoint();
// const { isDesktop } = useBreakPoint();
const [getDiscount] = useGetDiscountMutation();
const [isCouponOpen, setIsCouponOpen] = useState(false);
// const [isCouponOpen, setIsCouponOpen] = useState(false);
const handleCouponSave = (value: string) => {
dispatch(updateCoupon(value));
message.success(t("cart.coupon") + " " + t("updatedSuccessfully"));
getDiscount({
discountCode: value,
restaurantID: restaurant.restautantId || "",
})
.unwrap()
.then((response) => {
dispatch(
updateDiscount({
value: response.value,
isGift: response.isGift,
isDiscount: response.isDiscount,
}),
);
})
.catch((error) => {
message.error(error.data.message || t("cart.couponInvalid"));
});
};
const handleCouponClose = () => {
setIsCouponOpen(false);
};
// const handleCouponClose = () => {
// setIsCouponOpen(false);
// };
return (
<>
<ProInputCard
title={t("cart.couponCode")}
titleRight={
<div
style={{
display: "flex",
flexDirection: "row",
alignItems: "center",
gap: 10,
}}
onClick={() => setIsCouponOpen(true)}
>
<ProText
style={{
color: colors.primary,
fontSize: 14,
cursor: "pointer",
}}
>
{t("cart.viewOffers")}
</ProText>
<DonateIcon />
</div>
}
// titleRight={
// <div
// style={{
// display: "flex",
// flexDirection: "row",
// alignItems: "center",
// gap: 10,
// }}
// onClick={() => setIsCouponOpen(true)}
// >
// <ProText
// style={{
// color: colors.primary,
// fontSize: 14,
// cursor: "pointer",
// }}
// >
// {t("cart.viewOffers")}
// </ProText>
// <DonateIcon />
// </div>
// }
dividerStyle={{ margin: "5px 0 0 0" }}
>
<Form.Item
@@ -68,6 +83,9 @@ export default function CouponCard() {
size="large"
autoFocus={false}
style={{ padding: "7px 11px", height: 50 }}
onChange={(e) => {
dispatch(updateCoupon(e.target.value));
}}
suffix={
<Button
style={{
@@ -77,6 +95,7 @@ export default function CouponCard() {
backgroundColor: "black",
color: "white",
}}
onClick={() => handleCouponSave(coupon)}
>
{t("cart.apply")}
<CouponHeartIcon className={styles.couponApplyIcon} />
@@ -85,7 +104,7 @@ export default function CouponCard() {
/>
</Form.Item>
</ProInputCard>
{isDesktop ? (
{/* {isDesktop ? (
<CouponDialog
isOpen={isCouponOpen}
onClose={handleCouponClose}
@@ -99,7 +118,7 @@ export default function CouponCard() {
initialValue={coupon}
onSave={handleCouponSave}
/>
)}
)} */}
</>
);
}

View File

@@ -10,11 +10,11 @@ import { useAppSelector } from "redux/hooks";
export default function CartPage() {
const { isDesktop } = useBreakPoint();
const [form] = Form.useForm();
const { specialRequest } = useAppSelector(selectCart);
const { specialRequest, coupon } = useAppSelector(selectCart);
useEffect(() => {
form.setFieldsValue({ specialRequest });
}, [form, specialRequest]);
form.setFieldsValue({ specialRequest, coupon });
}, [form, specialRequest, coupon]);
// Prevent keyboard from appearing automatically on mobile
useEffect(() => {

View File

@@ -35,6 +35,7 @@ export default function useOrder() {
orderType,
giftDetails,
location,
discount,
} = useAppSelector(selectCart);
const highestLoyaltyItem = useAppSelector(selectHighestPricedLoyaltyItem);
const { useLoyaltyPoints } = useAppSelector(selectCart);
@@ -57,8 +58,6 @@ export default function useOrder() {
const handleCreateOrder = useCallback(() => {
createOrder({
phone: mobilenumber || phone || giftDetails?.senderPhone,
couponID: coupon,
discountAmount: 0,
comment: specialRequest,
delivery_method: getDeliveryMethod(),
timeslot: "",
@@ -75,7 +74,9 @@ export default function useOrder() {
})),
office_no: officeDetails?.officeNo || "",
vatvalue: 0,
discountGiftCode: "",
...(discount.isDiscount ? { couponID: coupon } : {}),
...(discount.isGift ? { discountGiftCode: coupon } : {}),
discountAmount: discount.value || 0,
paymentType: "cod",
uuid: user_uuid,
pickup_comments: "",
@@ -123,37 +124,6 @@ export default function useOrder() {
.catch((error: any) => {
console.error("Create Order failed:", error);
});
}, [
createOrder,
mobilenumber,
phone,
giftDetails?.senderPhone,
giftDetails?.receiverName,
giftDetails?.receiverPhone,
giftDetails?.message,
giftDetails?.isSecret,
giftDetails?.senderEmail,
giftDetails?.senderName,
coupon,
specialRequest,
tables,
orderType,
restaurantID,
items,
officeDetails?.officeNo,
user_uuid,
estimateTime,
orderPrice,
useLoyaltyPoints,
highestLoyaltyItem,
tip,
location?.lat,
location?.lng,
location?.address,
t,
navigate,
subdomain,
dispatch,
]);
}, [createOrder, mobilenumber, phone, giftDetails?.senderPhone, giftDetails?.receiverName, giftDetails?.receiverPhone, giftDetails?.message, giftDetails?.isSecret, giftDetails?.senderEmail, giftDetails?.senderName, specialRequest, getDeliveryMethod, tables, orderType, restaurantID, items, officeDetails?.officeNo, discount.isDiscount, discount.isGift, discount.value, coupon, user_uuid, estimateTime, orderPrice, useLoyaltyPoints, highestLoyaltyItem, tip, location?.lat, location?.lng, location?.address, t, navigate, subdomain, dispatch]);
return { handleCreateOrder };
}

View File

@@ -1,6 +1,7 @@
import {
CANCEL_ORDER_URL,
CREATE_ORDER_URL,
DISCOUNT_URL,
ORDER_DETAILS_URL,
ORDERS_URL,
PRODUCTS_AND_CATEGORIES_URL,
@@ -10,7 +11,7 @@ import {
import { OrderDetails } from "pages/checkout/hooks/types";
import menuParser from "pages/menu/helper";
import { RestaurantDetails } from "utils/types/appTypes";
import { DiscountResultType, RestaurantDetails } from "utils/types/appTypes";
import { baseApi } from "./apiSlice";
export const branchApi = baseApi.injectEndpoints({
@@ -101,6 +102,24 @@ export const branchApi = baseApi.injectEndpoints({
return response.result;
},
}),
getDiscount: builder.mutation<
DiscountResultType,
{ discountCode: string; restaurantID: string }
>({
query: ({
discountCode,
restaurantID,
}: {
discountCode: string;
restaurantID: string;
}) => ({
url: `${DISCOUNT_URL}/${discountCode}/${restaurantID}`,
method: "GET",
}),
transformResponse: (response: any) => {
return response.result;
},
}),
}),
});
export const {
@@ -111,4 +130,5 @@ export const {
useGetTablesQuery,
useGetOrderDetailsQuery,
useCancelOrderMutation,
useGetDiscountMutation
} = branchApi;

View File

@@ -103,3 +103,4 @@ export const LOGIN_URL = `${API_BASE_URL}login`;
export const SEND_OTP_URL = `${API_BASE_URL}sendOtp`;
export const CONFIRM_OTP_URL = `${API_BASE_URL}confirmOtp`;
export const PAYMENT_CONFIRMATION_URL = `https://menu.fascano.com/payment/confirmation`;
export const DISCOUNT_URL = `${BASE_URL}getDiscount`;

View File

@@ -342,13 +342,22 @@ export interface User {
export type Locale = "en" | "ar";
export type Theme = "light" | "dark";
export interface ApiResponse {
export interface ApiResponse<T> {
success: boolean;
result: any;
result: T;
message: string;
error: any;
}
export interface DiscountResultType {
value: number
is_on_category: boolean
id: number
isDiscount: boolean
isGift: boolean
categories: any[]
}
export interface Restaurant {
id: string;
name: string;