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

View File

@@ -35,6 +35,12 @@ export interface GiftDetailsType {
isSecret: boolean; isSecret: boolean;
} }
interface DiscountData {
value: number;
isGift: boolean;
isDiscount: boolean;
}
interface CartState { interface CartState {
restaurant: Partial<RestaurantDetails>; restaurant: Partial<RestaurantDetails>;
items: CartItem[]; items: CartItem[];
@@ -57,6 +63,7 @@ interface CartState {
useLoyaltyPoints: boolean; useLoyaltyPoints: boolean;
loyaltyValidationError: string | null; loyaltyValidationError: string | null;
scheduledDate: string; scheduledDate: string;
discount: DiscountData;
} }
// localStorage keys // localStorage keys
@@ -81,6 +88,7 @@ export const CART_STORAGE_KEYS = {
LOYALTY_VALIDATION_ERROR: "fascano_loyalty_validation_error", LOYALTY_VALIDATION_ERROR: "fascano_loyalty_validation_error",
RESTAURANT: "fascano_restaurant", RESTAURANT: "fascano_restaurant",
SCHEDULED_DATE: "fascano_scheduled_date", SCHEDULED_DATE: "fascano_scheduled_date",
DISCOUNT: "fascano_discount",
} as const; } as const;
// Utility functions for localStorage // 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 // Generate a unique identifier for cart items based on product ID, variant, extras, and comment
const generateUniqueId = (item: Omit<CartItem, "quantity" | "uniqueId">): string => { const generateUniqueId = (
const variantStr = item.variant || ''; item: Omit<CartItem, "quantity" | "uniqueId">,
const extrasStr = item.extras ? item.extras.sort().join(',') : ''; ): string => {
const extrasGroupStr = item.extrasgroup ? item.extrasgroup.sort().join(',') : ''; const variantStr = item.variant || "";
const commentStr = item.comment || ''; 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}`; return `${item.id}-${variantStr}-${extrasStr}-${extrasGroupStr}-${commentStr}`;
}; };
@@ -153,6 +165,11 @@ const initialState: CartState = {
), ),
restaurant: getFromLocalStorage(CART_STORAGE_KEYS.RESTAURANT, { taxes: [] }), restaurant: getFromLocalStorage(CART_STORAGE_KEYS.RESTAURANT, { taxes: [] }),
scheduledDate: getFromLocalStorage(CART_STORAGE_KEYS.SCHEDULED_DATE, ""), scheduledDate: getFromLocalStorage(CART_STORAGE_KEYS.SCHEDULED_DATE, ""),
discount: getFromLocalStorage(CART_STORAGE_KEYS.DISCOUNT, {
value: 0,
isGift: false,
isDiscount: false,
}),
}; };
const orderSlice = createSlice({ const orderSlice = createSlice({
@@ -189,7 +206,9 @@ const orderSlice = createSlice({
if (existingItem) { if (existingItem) {
// Update quantity of existing item with same configuration // Update quantity of existing item with same configuration
state.items = state.items.map((i) => 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 { } else {
// Add new item with its unique identifier // Add new item with its unique identifier
@@ -220,7 +239,11 @@ const orderSlice = createSlice({
}, },
updateQuantity( updateQuantity(
state, state,
action: PayloadAction<{ id: number | string; uniqueId: string; quantity: number }>, action: PayloadAction<{
id: number | string;
uniqueId: string;
quantity: number;
}>,
) { ) {
const { uniqueId, quantity } = action.payload; const { uniqueId, quantity } = action.payload;
state.items = state.items.map((item) => state.items = state.items.map((item) =>
@@ -236,7 +259,9 @@ const orderSlice = createSlice({
} }
}, },
removeItem(state, action: PayloadAction<string>) { 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 // Validate loyalty points if enabled
if (state.useLoyaltyPoints) { 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, reset,
updateRestaurant, updateRestaurant,
updateScheduledDate, updateScheduledDate,
updateDiscount,
} = orderSlice.actions; } = orderSlice.actions;
// Tax calculation helper functions // Tax calculation helper functions
@@ -586,11 +623,13 @@ const calculateTotalTax = (subtotal: number, taxes: Tax[]): number => {
// Selectors // Selectors
export const selectCart = (state: RootState) => state.order; export const selectCart = (state: RootState) => state.order;
export const selectCartItems = (state: RootState) => state.order.items; export const selectCartItems = (state: RootState) => state.order.items;
export const selectCartTotal = (state: RootState) => export const selectCartTotal = (state: RootState) =>
state.order.items.reduce( state.order.items.reduce(
(total, item) => total + item.price * item.quantity, (total, item) => total + item.price * item.quantity,
0, 0,
); );
export const selectCartItemsQuantity = export const selectCartItemsQuantity =
(uniqueId: string) => (state: RootState) => { (uniqueId: string) => (state: RootState) => {
const item = state.order.items.find((i) => i.uniqueId === uniqueId); 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) => { export const selectLoyaltyValidation = (state: RootState) => {
const useLoyaltyPoints = state.order.useLoyaltyPoints; const useLoyaltyPoints = state.order.useLoyaltyPoints;
const loyaltyItems = selectLoyaltyItems(state); const loyaltyItems = selectLoyaltyItems(state);
@@ -636,13 +682,13 @@ export const selectLoyaltyValidation = (state: RootState) => {
export const selectTaxes = (state: RootState) => state.order.restaurant.taxes; export const selectTaxes = (state: RootState) => state.order.restaurant.taxes;
export const selectTaxAmount = (state: RootState) => { export const selectTaxAmount = (state: RootState) => {
const subtotal = selectCartTotal(state); const subtotal = selectCartTotal(state) - selectDiscountTotal(state);
const taxes = selectTaxes(state); const taxes = selectTaxes(state);
return calculateTotalTax(subtotal, taxes || []); return calculateTotalTax(subtotal, taxes || []);
}; };
export const selectGrandTotal = (state: RootState) => { export const selectGrandTotal = (state: RootState) => {
const loyaltyDiscount = selectHighestPricedLoyaltyItem(state)?.price || 0; const totalDiscount = selectDiscountTotal(state);
const taxAmount = selectTaxAmount(state); const taxAmount = selectTaxAmount(state);
const subtotal = selectCartTotal(state); const subtotal = selectCartTotal(state);
const deliveryFee = const deliveryFee =
@@ -650,11 +696,7 @@ export const selectGrandTotal = (state: RootState) => {
? Number(state.order.restaurant?.delivery_fees) || 0 ? Number(state.order.restaurant?.delivery_fees) || 0
: 0; : 0;
return ( return subtotal + taxAmount - totalDiscount + deliveryFee;
subtotal + };
taxAmount -
(state.order.useLoyaltyPoints && state.order.restaurant?.is_loyalty_enabled === 1 ? loyaltyDiscount : 0) +
deliveryFee
);};
export default orderSlice.reducer; export default orderSlice.reducer;

View File

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

View File

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

View File

@@ -35,6 +35,7 @@ export default function useOrder() {
orderType, orderType,
giftDetails, giftDetails,
location, location,
discount,
} = useAppSelector(selectCart); } = useAppSelector(selectCart);
const highestLoyaltyItem = useAppSelector(selectHighestPricedLoyaltyItem); const highestLoyaltyItem = useAppSelector(selectHighestPricedLoyaltyItem);
const { useLoyaltyPoints } = useAppSelector(selectCart); const { useLoyaltyPoints } = useAppSelector(selectCart);
@@ -57,8 +58,6 @@ export default function useOrder() {
const handleCreateOrder = useCallback(() => { const handleCreateOrder = useCallback(() => {
createOrder({ createOrder({
phone: mobilenumber || phone || giftDetails?.senderPhone, phone: mobilenumber || phone || giftDetails?.senderPhone,
couponID: coupon,
discountAmount: 0,
comment: specialRequest, comment: specialRequest,
delivery_method: getDeliveryMethod(), delivery_method: getDeliveryMethod(),
timeslot: "", timeslot: "",
@@ -75,7 +74,9 @@ export default function useOrder() {
})), })),
office_no: officeDetails?.officeNo || "", office_no: officeDetails?.officeNo || "",
vatvalue: 0, vatvalue: 0,
discountGiftCode: "", ...(discount.isDiscount ? { couponID: coupon } : {}),
...(discount.isGift ? { discountGiftCode: coupon } : {}),
discountAmount: discount.value || 0,
paymentType: "cod", paymentType: "cod",
uuid: user_uuid, uuid: user_uuid,
pickup_comments: "", pickup_comments: "",
@@ -123,37 +124,6 @@ export default function useOrder() {
.catch((error: any) => { .catch((error: any) => {
console.error("Create Order failed:", error); console.error("Create Order failed:", error);
}); });
}, [ }, [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]);
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,
]);
return { handleCreateOrder }; return { handleCreateOrder };
} }

View File

@@ -1,6 +1,7 @@
import { import {
CANCEL_ORDER_URL, CANCEL_ORDER_URL,
CREATE_ORDER_URL, CREATE_ORDER_URL,
DISCOUNT_URL,
ORDER_DETAILS_URL, ORDER_DETAILS_URL,
ORDERS_URL, ORDERS_URL,
PRODUCTS_AND_CATEGORIES_URL, PRODUCTS_AND_CATEGORIES_URL,
@@ -10,7 +11,7 @@ import {
import { OrderDetails } from "pages/checkout/hooks/types"; import { OrderDetails } from "pages/checkout/hooks/types";
import menuParser from "pages/menu/helper"; import menuParser from "pages/menu/helper";
import { RestaurantDetails } from "utils/types/appTypes"; import { DiscountResultType, RestaurantDetails } from "utils/types/appTypes";
import { baseApi } from "./apiSlice"; import { baseApi } from "./apiSlice";
export const branchApi = baseApi.injectEndpoints({ export const branchApi = baseApi.injectEndpoints({
@@ -101,6 +102,24 @@ export const branchApi = baseApi.injectEndpoints({
return response.result; 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 { export const {
@@ -111,4 +130,5 @@ export const {
useGetTablesQuery, useGetTablesQuery,
useGetOrderDetailsQuery, useGetOrderDetailsQuery,
useCancelOrderMutation, useCancelOrderMutation,
useGetDiscountMutation
} = branchApi; } = 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 SEND_OTP_URL = `${API_BASE_URL}sendOtp`;
export const CONFIRM_OTP_URL = `${API_BASE_URL}confirmOtp`; export const CONFIRM_OTP_URL = `${API_BASE_URL}confirmOtp`;
export const PAYMENT_CONFIRMATION_URL = `https://menu.fascano.com/payment/confirmation`; 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 Locale = "en" | "ar";
export type Theme = "light" | "dark"; export type Theme = "light" | "dark";
export interface ApiResponse { export interface ApiResponse<T> {
success: boolean; success: boolean;
result: any; result: T;
message: string; message: string;
error: any; error: any;
} }
export interface DiscountResultType {
value: number
is_on_category: boolean
id: number
isDiscount: boolean
isGift: boolean
categories: any[]
}
export interface Restaurant { export interface Restaurant {
id: string; id: string;
name: string; name: string;