diff --git a/src/assets/locals/ar.json b/src/assets/locals/ar.json index 53d06f7..2683add 100644 --- a/src/assets/locals/ar.json +++ b/src/assets/locals/ar.json @@ -412,6 +412,13 @@ "payAsCustomAmount": "دفع كمبلغ مخصص", "payAsSplitAmount": "دفع كمبلغ مقسم", "divideTheBillEqually": "تقسيم الفاتورة الى حسب العدد", - "payForYourItems": "دفع للطلبات الخاصة بي" + "payForYourItems": "دفع للطلبات الخاصة بي", + "enterCustomAmount": "أدخل المبلغ المخصص", + "amountPlaceholder": "0.00", + "divisionPreview": "معاينة التقسيم", + "yourAmount": "مبلغك", + "selectedTotal": "المجموع المحدد", + "splitBillAmount": "مبلغ تقسيم الفاتورة", + "removeSplitWay": "إزالة طريقة التقسيم" } } diff --git a/src/assets/locals/en.json b/src/assets/locals/en.json index 10dd2bb..ddd8c9e 100644 --- a/src/assets/locals/en.json +++ b/src/assets/locals/en.json @@ -424,6 +424,13 @@ "payAsCustomAmount": "Pay as Custom Amount", "payAsSplitAmount": "Pay as Split Amount", "divideTheBillEqually": "Divide the Bill Equally", - "payForYourItems": "Pay for Your Items" + "payForYourItems": "Pay for Your Items", + "enterCustomAmount": "Enter Custom Amount", + "amountPlaceholder": "0.00", + "divisionPreview": "Division Preview", + "yourAmount": "Your Amount", + "selectedTotal": "Selected Total", + "splitBillAmount": "Split Bill Amount", + "removeSplitWay": "Remove Split Way" } } diff --git a/src/components/OrderSummary/OrderSummary.tsx b/src/components/OrderSummary/OrderSummary.tsx index 3a319c6..c123338 100644 --- a/src/components/OrderSummary/OrderSummary.tsx +++ b/src/components/OrderSummary/OrderSummary.tsx @@ -21,7 +21,7 @@ import styles from "./OrderSummary.module.css"; export default function OrderSummary() { const { t } = useTranslation(); - const { useLoyaltyPoints } = useAppSelector(selectCart); + const { useLoyaltyPoints, splitBillAmount } = useAppSelector(selectCart); const { subdomain } = useParams(); const { data: restaurant } = useGetRestaurantDetailsQuery(subdomain); const { orderType } = useAppSelector(selectCart); @@ -62,6 +62,14 @@ export default function OrderSummary() { {t("cart.tax")} + {splitBillAmount > 0 && ( +
+ + {t("splitBill.splitBillAmount")} + + +
+ )}
diff --git a/src/features/order/orderSlice.ts b/src/features/order/orderSlice.ts index 020f327..3b7f52c 100644 --- a/src/features/order/orderSlice.ts +++ b/src/features/order/orderSlice.ts @@ -69,6 +69,7 @@ interface CartState { pickupTime: string; pickupType: string; order: any; + splitBillAmount: number; } // localStorage keys @@ -185,6 +186,7 @@ const initialState: CartState = { pickupTime: getFromLocalStorage(CART_STORAGE_KEYS.PICKUP_TIME, ""), pickupType: getFromLocalStorage(CART_STORAGE_KEYS.PICKUP_TYPE, ""), order: getFromLocalStorage(CART_STORAGE_KEYS.ORDER, null), + splitBillAmount: 0, }; const orderSlice = createSlice({ @@ -632,6 +634,9 @@ const orderSlice = createSlice({ localStorage.setItem(CART_STORAGE_KEYS.ORDER, JSON.stringify(state.order)); } }, + updateSplitBillAmount(state, action: PayloadAction) { + state.splitBillAmount = action.payload; + }, }, }); @@ -669,6 +674,7 @@ export const { updatePickupTime, updatePickUpType, updateOrder, + updateSplitBillAmount, } = orderSlice.actions; // Tax calculation helper functions @@ -775,7 +781,7 @@ export const selectGrandTotal = (state: RootState) => { ? Number(state.order.restaurant?.delivery_fees) || 0 : 0; - return subtotal + taxAmount - totalDiscount + deliveryFee; + return subtotal + taxAmount - totalDiscount + deliveryFee - state.order.splitBillAmount; }; export default orderSlice.reducer; diff --git a/src/pages/pay/components/PayButton.tsx b/src/pages/pay/components/PayButton.tsx index 73385e5..c8e5c3e 100644 --- a/src/pages/pay/components/PayButton.tsx +++ b/src/pages/pay/components/PayButton.tsx @@ -1,25 +1,59 @@ import { Button, FormInstance, Layout } from "antd"; -import { selectCart } from "features/order/orderSlice"; +import { selectCart, updateSplitBillAmount } from "features/order/orderSlice"; import { OrderType } from "pages/checkout/hooks/types.ts"; import useOrder from "pages/checkout/hooks/useOrder"; import { useCallback, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; -import { useAppSelector } from "redux/hooks"; +import { useAppDispatch, useAppSelector } from "redux/hooks"; import styles from "../../address/address.module.css"; +import { CustomAmountChoiceBottomSheet } from "./splitBill/CustomAmountChoiceBottomSheet"; +import { EqualltyChoiceBottomSheet } from "./splitBill/EqualltyChoiceBottomSheet"; +import { PayForYourItemsChoiceBottomSheet } from "./splitBill/PayForYourItemsChoiceBottomSheet"; import { SplitBillChoiceBottomSheet } from "./splitBill/SplitBillChoiceBottomSheet"; +type SplitWay = "customAmount" | "equality" | "payForItems" | null; + export default function PayButton({ form }: { form: FormInstance }) { const { t } = useTranslation(); + const dispatch = useAppDispatch(); const { orderType } = useAppSelector(selectCart); const { handleCreateOrder } = useOrder(); + const [selectedSplitWay, setSelectedSplitWay] = useState(null); const [ isSplitBillChoiceBottomSheetOpen, setIsSplitBillChoiceBottomSheetOpen, ] = useState(false); + const [isCustomAmountOpen, setIsCustomAmountOpen] = useState(false); + const [isEqualityOpen, setIsEqualityOpen] = useState(false); + const [isPayForItemsOpen, setIsPayForItemsOpen] = useState(false); const handleSplitBillClick = useCallback(() => { - setIsSplitBillChoiceBottomSheetOpen(true); - }, []); + if (selectedSplitWay === "customAmount") { + setIsCustomAmountOpen(true); + } else if (selectedSplitWay === "equality") { + setIsEqualityOpen(true); + } else if (selectedSplitWay === "payForItems") { + setIsPayForItemsOpen(true); + } else { + setIsSplitBillChoiceBottomSheetOpen(true); + } + }, [selectedSplitWay]); + + const handleRemoveSplitWay = useCallback(() => { + setSelectedSplitWay(null); + dispatch(updateSplitBillAmount(0)); + }, [dispatch]); + + const getSplitButtonTitle = useMemo(() => { + if (selectedSplitWay === "customAmount") { + return t("splitBill.payAsCustomAmount"); + } else if (selectedSplitWay === "equality") { + return t("splitBill.divideTheBillEqually"); + } else if (selectedSplitWay === "payForItems") { + return t("splitBill.payForYourItems"); + } + return t("checkout.splitBill"); + }, [selectedSplitWay, t]); const handlePlaceOrderClick = useCallback(async () => { try { @@ -43,7 +77,7 @@ export default function PayButton({ form }: { form: FormInstance }) { className={styles.splitBillButton} onClick={handleSplitBillClick} > - {t("checkout.splitBill")} + {getSplitButtonTitle} )} @@ -60,6 +94,25 @@ export default function PayButton({ form }: { form: FormInstance }) { setIsSplitBillChoiceBottomSheetOpen(false)} + onSelectSplitWay={setSelectedSplitWay} + /> + + setIsCustomAmountOpen(false)} + onRemoveSplitWay={handleRemoveSplitWay} + /> + + setIsEqualityOpen(false)} + onRemoveSplitWay={handleRemoveSplitWay} + /> + + setIsPayForItemsOpen(false)} + onRemoveSplitWay={handleRemoveSplitWay} /> ); diff --git a/src/pages/pay/components/splitBill/CustomAmountChoiceBottomSheet.tsx b/src/pages/pay/components/splitBill/CustomAmountChoiceBottomSheet.tsx new file mode 100644 index 0000000..abbe736 --- /dev/null +++ b/src/pages/pay/components/splitBill/CustomAmountChoiceBottomSheet.tsx @@ -0,0 +1,112 @@ +import { Button, Form, InputNumber } from "antd"; +import { ProBottomSheet } from "components/ProBottomSheet/ProBottomSheet.tsx"; +import { useState, useEffect } from "react"; +import { useTranslation } from "react-i18next"; + +import { + selectCart, + selectGrandTotal, + updateSplitBillAmount, +} from "features/order/orderSlice"; +import { useAppDispatch, useAppSelector } from "redux/hooks"; +import ProInputCard from "components/ProInputCard/ProInputCard"; + +interface SplitBillChoiceBottomSheetProps { + isOpen: boolean; + onClose: () => void; + onSave?: (value: string) => void; + onRemoveSplitWay?: () => void; +} + +export function CustomAmountChoiceBottomSheet({ + isOpen, + onClose, + onRemoveSplitWay, +}: SplitBillChoiceBottomSheetProps) { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const { splitBillAmount } = useAppSelector(selectCart); + const grandTotal = useAppSelector(selectGrandTotal); + const [amount, setAmount] = useState( + splitBillAmount > 0 ? splitBillAmount.toString() : "", + ); + + useEffect(() => { + if (isOpen && splitBillAmount > 0) { + setAmount(splitBillAmount.toString()); + } + }, [isOpen, splitBillAmount]); + + const handleSave = () => { + const numAmount = parseFloat(amount) || 0; + dispatch(updateSplitBillAmount(numAmount)); + onClose(); + }; + + const handleRemoveSplitWay = () => { + setAmount(""); + dispatch(updateSplitBillAmount(0)); + onRemoveSplitWay?.(); + onClose(); + }; + + return ( + +
+
+ + + setAmount(value?.toString() || "")} + placeholder={t("splitBill.amount")} + max={grandTotal.toString()} + min={"0"} + style={{ width: "100%", fontSize:"1rem" }} + /> + + +
+ +
+ + +
+
+
+ ); +} diff --git a/src/pages/pay/components/splitBill/EqualltyChoiceBottomSheet.tsx b/src/pages/pay/components/splitBill/EqualltyChoiceBottomSheet.tsx new file mode 100644 index 0000000..e196c01 --- /dev/null +++ b/src/pages/pay/components/splitBill/EqualltyChoiceBottomSheet.tsx @@ -0,0 +1,310 @@ +import { Button } from "antd"; +import { ProBottomSheet } from "components/ProBottomSheet/ProBottomSheet.tsx"; +import { useEffect, useMemo } from "react"; +import { useTranslation } from "react-i18next"; + +import PeopleIcon from "components/Icons/PeopleIcon"; +import ProInputCard from "components/ProInputCard/ProInputCard"; +import ProText from "components/ProText"; +import ProTitle from "components/ProTitle"; +import { + selectCart, + selectGrandTotal, + updateSplitBillAmount, +} from "features/order/orderSlice"; +import { useAppDispatch, useAppSelector } from "redux/hooks"; +import { ProGray1 } from "ThemeConstants"; +import PayForActions from "../../../split-bill/components/PayForActions"; +import TotalPeopleActions from "../../../split-bill/components/TotalPeopleActions"; + +interface SplitBillChoiceBottomSheetProps { + isOpen: boolean; + onClose: () => void; + onSave?: (value: string) => void; + onRemoveSplitWay?: () => void; +} + +interface SplitBillTmp { + totalPeople?: number; + payFor?: number; +} + +export function EqualltyChoiceBottomSheet({ + isOpen, + onClose, + onRemoveSplitWay, +}: SplitBillChoiceBottomSheetProps) { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const { tmp } = useAppSelector(selectCart); + const grandTotal = useAppSelector(selectGrandTotal); + + const splitBillTmp = tmp as SplitBillTmp; + const totalPeople = splitBillTmp?.totalPeople || 1; + const payFor = splitBillTmp?.payFor || 1; + + // Calculate split amount + const splitAmount = useMemo(() => { + if (totalPeople > 0) { + return (grandTotal / totalPeople) * payFor; + } + return 0; + }, [grandTotal, totalPeople, payFor]); + + // Update splitBillAmount when values change + useEffect(() => { + if (isOpen && splitAmount > 0) { + dispatch(updateSplitBillAmount(splitAmount)); + } + }, [isOpen, splitAmount, dispatch]); + + const handleSave = () => { + dispatch(updateSplitBillAmount(splitAmount)); + onClose(); + }; + + const handleRemoveSplitWay = () => { + dispatch(updateSplitBillAmount(0)); + onRemoveSplitWay?.(); + onClose(); + }; + + return ( + +
+ +
+
+
+ + + {t("checkout.totalPeople")} + +
+ + +
+ +
+
+ + + {t("checkout.payFor")} + +
+ + +
+
+
+ + {/* Spinner Visualization - Blank Spin Wheel */} + {totalPeople > 0 && ( +
+ + {t("splitBill.divisionPreview")} + +
+ + {Array.from({ length: totalPeople }).map((_, index) => { + const anglePerSlice = 360 / totalPeople; + const startAngle = index * anglePerSlice; + const endAngle = (index + 1) * anglePerSlice; + const isSelected = index < payFor; + + // Convert angles to radians + const startAngleRad = (startAngle * Math.PI) / 180; + const endAngleRad = (endAngle * Math.PI) / 180; + + // Calculate path for pie slice + const radius = 90; + const centerX = 100; + const centerY = 100; + + const x1 = centerX + radius * Math.cos(startAngleRad); + const y1 = centerY + radius * Math.sin(startAngleRad); + const x2 = centerX + radius * Math.cos(endAngleRad); + const y2 = centerY + radius * Math.sin(endAngleRad); + + const largeArcFlag = anglePerSlice > 180 ? 1 : 0; + + const pathData = [ + `M ${centerX} ${centerY}`, + `L ${x1} ${y1}`, + `A ${radius} ${radius} 0 ${largeArcFlag} 1 ${x2} ${y2}`, + "Z", + ].join(" "); + + return ( + + ); + })} + + {/* Center circle to make it look like a blank wheel */} +
+ {payFor} +
+
+ + {t("splitBill.yourAmount")}: {splitAmount.toFixed(2)} + +
+ )} + +
+ + +
+
+
+ ); +} diff --git a/src/pages/pay/components/splitBill/PayForYourItemsChoiceBottomSheet.tsx b/src/pages/pay/components/splitBill/PayForYourItemsChoiceBottomSheet.tsx new file mode 100644 index 0000000..0af7772 --- /dev/null +++ b/src/pages/pay/components/splitBill/PayForYourItemsChoiceBottomSheet.tsx @@ -0,0 +1,211 @@ +import { Button, Card, Image } from "antd"; +import { ProBottomSheet } from "components/ProBottomSheet/ProBottomSheet.tsx"; +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; + +import ArabicPrice from "components/ArabicPrice"; +import ProText from "components/ProText"; +import { selectCart, updateSplitBillAmount } from "features/order/orderSlice"; +import { useAppDispatch, useAppSelector } from "redux/hooks"; + +interface SplitBillChoiceBottomSheetProps { + isOpen: boolean; + onClose: () => void; + onSave?: (value: string) => void; + onRemoveSplitWay?: () => void; +} + +export function PayForYourItemsChoiceBottomSheet({ + isOpen, + onClose, + onRemoveSplitWay, +}: SplitBillChoiceBottomSheetProps) { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const { items } = useAppSelector(selectCart); + const [selectedItems, setSelectedItems] = useState>(new Set()); + + // Calculate total for selected items + const selectedTotal = items + .filter((item) => selectedItems.has(item.uniqueId || item.id.toString())) + .reduce((sum, item) => sum + item.price * item.quantity, 0); + + useEffect(() => { + if (isOpen) { + setSelectedItems(new Set()); + dispatch(updateSplitBillAmount(0)); + } + }, [isOpen, dispatch]); + + const handleItemToggle = (uniqueId: string) => { + const newSelected = new Set(selectedItems); + if (newSelected.has(uniqueId)) { + newSelected.delete(uniqueId); + } else { + newSelected.add(uniqueId); + } + setSelectedItems(newSelected); + + // Calculate and update split bill amount + const newTotal = items + .filter((item) => newSelected.has(item.uniqueId || item.id.toString())) + .reduce((sum, item) => sum + item.price * item.quantity, 0); + dispatch(updateSplitBillAmount(newTotal)); + }; + + const handleSave = () => { + dispatch(updateSplitBillAmount(selectedTotal)); + onClose(); + }; + + const handleRemoveSplitWay = () => { + setSelectedItems(new Set()); + dispatch(updateSplitBillAmount(0)); + onRemoveSplitWay?.(); + onClose(); + }; + + return ( + +
+ {items.length === 0 ? ( + + {t("cart.emptyCart")} + + ) : ( + items.map((item) => { + const itemId = item.uniqueId || item.id.toString(); + const isSelected = selectedItems.has(itemId); + const itemTotal = item.price * item.quantity; + + return ( + handleItemToggle(itemId)} + > +
+ {item.name} +
+ + {item.name} + + + {t("cart.quantity")}: {item.quantity} + + +
+ +
+
+ ); + }) + )} + + {selectedTotal > 0 && ( +
+
+ + {t("splitBill.selectedTotal")}: + + +
+
+ )} + +
+ + +
+
+
+ ); +} diff --git a/src/pages/pay/components/splitBill/SplitBillChoiceBottomSheet.tsx b/src/pages/pay/components/splitBill/SplitBillChoiceBottomSheet.tsx index 347652c..fafb501 100644 --- a/src/pages/pay/components/splitBill/SplitBillChoiceBottomSheet.tsx +++ b/src/pages/pay/components/splitBill/SplitBillChoiceBottomSheet.tsx @@ -1,46 +1,60 @@ import { Card } from "antd"; import { ProBottomSheet } from "components/ProBottomSheet/ProBottomSheet.tsx"; +import { useState } from "react"; import { useTranslation } from "react-i18next"; import BackIcon from "components/Icons/BackIcon"; import NextIcon from "components/Icons/NextIcon"; import ProTitle from "components/ProTitle"; -import { useNavigate, useParams } from "react-router-dom"; -import { useGetRestaurantDetailsQuery } from "redux/api/others"; import { useAppSelector } from "redux/hooks"; +import { CustomAmountChoiceBottomSheet } from "./CustomAmountChoiceBottomSheet"; +import { EqualltyChoiceBottomSheet } from "./EqualltyChoiceBottomSheet"; +import { PayForYourItemsChoiceBottomSheet } from "./PayForYourItemsChoiceBottomSheet"; import styles from "./SplitBill.module.css"; interface SplitBillChoiceBottomSheetProps { isOpen: boolean; onClose: () => void; onSave?: (value: string) => void; + onSelectSplitWay?: (way: "customAmount" | "equality" | "payForItems") => void; } export function SplitBillChoiceBottomSheet({ isOpen, onClose, - // onSave, + onSelectSplitWay, }: SplitBillChoiceBottomSheetProps) { const { t } = useTranslation(); - // const [value, setValue] = useState(initialValue); const { isRTL } = useAppSelector((state) => state.locale); - const { subdomain } = useParams(); - const navigate = useNavigate(); - const { data: restaurant } = useGetRestaurantDetailsQuery(subdomain, { - skip: !subdomain, - }); - // const handleSave = () => { - // onSave(value); - // onClose(); - // }; + const [isCustomAmountOpen, setIsCustomAmountOpen] = useState(false); + const [isEqualityOpen, setIsEqualityOpen] = useState(false); + const [isPayForItemsOpen, setIsPayForItemsOpen] = useState(false); const handleCancel = () => { - // setValue(initialValue); onClose(); }; + const handleCustomAmountClick = () => { + onSelectSplitWay?.("customAmount"); + onClose(); + setIsCustomAmountOpen(true); + }; + + const handleEqualityClick = () => { + onSelectSplitWay?.("equality"); + onClose(); + setIsEqualityOpen(true); + }; + + const handlePayForItemsClick = () => { + onSelectSplitWay?.("payForItems"); + onClose(); + setIsPayForItemsOpen(true); + }; + return ( + <> navigate(`/${restaurant?.subdomain}`)} + onClick={handleCustomAmountClick} >
navigate(`/${restaurant?.subdomain}`)} + onClick={handleEqualityClick} >
navigate(`/${restaurant?.subdomain}`)} + onClick={handlePayForItemsClick} >
+ + setIsCustomAmountOpen(false)} + /> + + setIsEqualityOpen(false)} + /> + + setIsPayForItemsOpen(false)} + /> + ); }