cart & checkout: apply validation based on required inputs in each service & add phone input in checkout page

This commit is contained in:
2025-10-14 23:25:08 +03:00
parent af27d1e509
commit b88cc28c89
13 changed files with 186 additions and 113 deletions

View File

@@ -221,7 +221,8 @@
"inYourCart": "في سلة المشتريات", "inYourCart": "في سلة المشتريات",
"updatedSuccessfully": "تم التحديث بنجاح", "updatedSuccessfully": "تم التحديث بنجاح",
"editNote": "تعديل الملاحظة", "editNote": "تعديل الملاحظة",
"selectTimeEstimate": "أدخل وقت التقديم" "selectTimeEstimate": "أدخل وقت التقديم",
"pleaseAddItemsToCart": "يرجى إضافة عناصر إلى السلة"
}, },
"checkout": { "checkout": {
"title": "الدفع", "title": "الدفع",
@@ -238,7 +239,8 @@
"totalAmount": "المبلغ الكلي", "totalAmount": "المبلغ الكلي",
"items": "العناصر", "items": "العناصر",
"expiresIn": "تنتهي في", "expiresIn": "تنتهي في",
"expiresInDescription": "تنتهي في:12/26" "expiresInDescription": "تنتهي في:12/26",
"phoneNumber": "رقم الهاتف"
}, },
"address": { "address": {
"title": "العنوان", "title": "العنوان",

View File

@@ -231,7 +231,8 @@
"plateNumber": "Plate Number", "plateNumber": "Plate Number",
"plateNumberPlaceholder": "Enter plate number", "plateNumberPlaceholder": "Enter plate number",
"editNote": "Edit Note", "editNote": "Edit Note",
"selectTimeEstimate": "Select Estimate Time" "selectTimeEstimate": "Select Estimate Time",
"pleaseAddItemsToCart": "Please add items to your cart"
}, },
"checkout": { "checkout": {
"title": "Checkout", "title": "Checkout",
@@ -248,7 +249,8 @@
"totalAmount": "Total Amount", "totalAmount": "Total Amount",
"items": "Items", "items": "Items",
"expiresIn": "Expires in", "expiresIn": "Expires in",
"expiresInDescription": "Expires in:12/26" "expiresInDescription": "Expires in:12/26",
"phoneNumber": "Phone Number"
}, },
"address": { "address": {
"title": "Address", "title": "Address",

View File

@@ -1,4 +1,4 @@
import { Radio, Space } from "antd"; import { Form, Radio, Space } from "antd";
import { Group } from "antd/es/radio"; import { Group } from "antd/es/radio";
import ArabicPrice from "components/ArabicPrice"; import ArabicPrice from "components/ArabicPrice";
import DifferentCardIcon from "components/Icons/paymentMethods/DifferentCardIcon"; import DifferentCardIcon from "components/Icons/paymentMethods/DifferentCardIcon";
@@ -50,67 +50,69 @@ const PaymentMethods = ({ onPaymentSelect, ...props }: PaymentMethodsProps) => {
return ( return (
<ProInputCard title={t("checkout.selectedPaymentMethod")}> <ProInputCard title={t("checkout.selectedPaymentMethod")}>
<Group <Form.Item name="paymentMethod" required rules={[{ required: true }]}>
className={styles.paymentMethods} <Group
style={{ className={styles.paymentMethods}
width: "100%", style={{
}} width: "100%",
{...props} }}
size="large" {...props}
> size="large"
<Space direction="vertical" style={{ width: "100%" }}> >
{options.map((option) => ( <Space direction="vertical" style={{ width: "100%" }}>
<div key={option.value}> {options.map((option) => (
<Radio <div key={option.value}>
key={option.value} <Radio
value={option.value} key={option.value}
onClick={onPaymentSelect} value={option.value}
style={{ onClick={onPaymentSelect}
height: 50,
borderRadius: 888,
border: "1px solid #DDD",
padding: 16,
}}
>
<div
style={{ style={{
display: "flex", height: 50,
flexDirection: "row", borderRadius: 888,
justifyContent: "space-between", border: "1px solid #DDD",
width: "100%", padding: 16,
position: "relative",
top: -4,
}} }}
> >
<ProText <div
style={{ style={{
fontSize: "0.85rem", display: "flex",
color: ProGray1, flexDirection: "row",
justifyContent: "space-between",
width: "100%",
position: "relative",
top: -4,
}} }}
> >
{option.label} <ProText
</ProText>
{!option.icon ? (
<ArabicPrice
price={option.price || 0}
style={{ style={{
fontSize: "0.75rem", fontSize: "0.85rem",
color: ProGray1, color: ProGray1,
fontWeight: 400,
...option?.style,
position: "relative",
top: 4,
}} }}
/> >
) : ( {option.label}
<>{option.icon}</> </ProText>
)} {!option.icon ? (
</div> <ArabicPrice
</Radio> price={option.price || 0}
</div> style={{
))} fontSize: "0.75rem",
</Space> color: ProGray1,
</Group> fontWeight: 400,
...option?.style,
position: "relative",
top: 4,
}}
/>
) : (
<>{option.icon}</>
)}
</div>
</Radio>
</div>
))}
</Space>
</Group>
</Form.Item>
</ProInputCard> </ProInputCard>
); );
}; };

View File

@@ -48,6 +48,7 @@ interface CartState {
estimateTimeDate: Date; estimateTimeDate: Date;
estimateTimeTime: string; estimateTimeTime: string;
collectionMethod: string; collectionMethod: string;
phone: string;
} }
// localStorage keys // localStorage keys
@@ -65,6 +66,7 @@ const CART_STORAGE_KEYS = {
ESTIMATE_TIME_DATE: 'fascano_estimate_time_date', ESTIMATE_TIME_DATE: 'fascano_estimate_time_date',
ESTIMATE_TIME_TIME: 'fascano_estimate_time_time', ESTIMATE_TIME_TIME: 'fascano_estimate_time_time',
COLLECTION_METHOD: 'fascano_collection_method', COLLECTION_METHOD: 'fascano_collection_method',
PHONE: 'fascano_phone',
} as const; } as const;
// Utility functions for localStorage // Utility functions for localStorage
@@ -95,6 +97,7 @@ const initialState: CartState = {
estimateTimeDate: new Date(getFromLocalStorage(CART_STORAGE_KEYS.ESTIMATE_TIME_DATE, new Date().toISOString())), estimateTimeDate: new Date(getFromLocalStorage(CART_STORAGE_KEYS.ESTIMATE_TIME_DATE, new Date().toISOString())),
estimateTimeTime: getFromLocalStorage(CART_STORAGE_KEYS.ESTIMATE_TIME_TIME, ""), estimateTimeTime: getFromLocalStorage(CART_STORAGE_KEYS.ESTIMATE_TIME_TIME, ""),
collectionMethod: getFromLocalStorage(CART_STORAGE_KEYS.COLLECTION_METHOD, ""), collectionMethod: getFromLocalStorage(CART_STORAGE_KEYS.COLLECTION_METHOD, ""),
phone: getFromLocalStorage(CART_STORAGE_KEYS.PHONE, ""),
}; };
const orderSlice = createSlice({ const orderSlice = createSlice({
@@ -273,6 +276,14 @@ const orderSlice = createSlice({
localStorage.setItem(CART_STORAGE_KEYS.COLLECTION_METHOD, JSON.stringify(state.collectionMethod)); 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));
}
},
}, },
}); });
@@ -295,6 +306,7 @@ export const {
updateGiftDetails, updateGiftDetails,
updateEstimateTime, updateEstimateTime,
updateCollectionMethod, updateCollectionMethod,
updatePhone,
reset, reset,
} = orderSlice.actions; } = orderSlice.actions;

View File

@@ -1,5 +1,5 @@
import { PlusOutlined } from "@ant-design/icons"; import { PlusOutlined } from "@ant-design/icons";
import { Card, Divider, Space } from "antd"; import { Card, Divider, Form, Space } from "antd";
import ArabicPrice from "components/ArabicPrice"; import ArabicPrice from "components/ArabicPrice";
import CartActionsButtons from "components/CartActionsButtons/CartActionsButtons.tsx"; import CartActionsButtons from "components/CartActionsButtons/CartActionsButtons.tsx";
import ImageWithFallback from "components/ImageWithFallback"; import ImageWithFallback from "components/ImageWithFallback";
@@ -35,7 +35,9 @@ interface CartMobileTabletLayoutProps {
form: FormInstance; form: FormInstance;
} }
export default function CartMobileTabletLayout({ form }: CartMobileTabletLayoutProps) { export default function CartMobileTabletLayout({
form,
}: CartMobileTabletLayoutProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const { items, collectionMethod } = useAppSelector(selectCart); const { items, collectionMethod } = useAppSelector(selectCart);
const { id } = useParams(); const { id } = useParams();
@@ -235,24 +237,30 @@ export default function CartMobileTabletLayout({ form }: CartMobileTabletLayoutP
{/* Collection Method */} {/* Collection Method */}
{orderType === "pickup" && ( {orderType === "pickup" && (
<ProInputCard title={t("cart.collectionMethod")}> <ProInputCard title={t("cart.collectionMethod")}>
<ProRatioGroups <Form.Item
options={[ name="collectionMethod"
{ label: t("cart.Cash"), value: "cod", price: "" }, required
{ rules={[{ required: true }]}
label: t("cart.e-payment"), >
value: "paymentgateway", <ProRatioGroups
price: "", options={[
}, { label: t("cart.Cash"), value: "cod", price: "" },
]} {
value={collectionMethod} label: t("cart.e-payment"),
onRatioClick={(value) => { value: "paymentgateway",
if (value === "cod") { price: "",
updateCollectionMethod(value); },
} else { ]}
updateCollectionMethod(value); value={collectionMethod}
} onRatioClick={(value) => {
}} if (value === "cod") {
/> updateCollectionMethod(value);
} else {
updateCollectionMethod(value);
}
}}
/>
</Form.Item>
</ProInputCard> </ProInputCard>
)} )}

View File

@@ -1,5 +1,5 @@
import { colors } from "ThemeConstants.ts"; import { colors } from "ThemeConstants.ts";
import { Button, FormInstance } from "antd"; import { Button, FormInstance, message } from "antd";
import { selectCart } from "features/order/orderSlice.ts"; import { selectCart } from "features/order/orderSlice.ts";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Link, useNavigate, useParams } from "react-router-dom"; import { Link, useNavigate, useParams } from "react-router-dom";
@@ -17,15 +17,15 @@ export default function CartFooter({ form }: CartFooterProps) {
const orderType = localStorage.getItem("orderType"); const orderType = localStorage.getItem("orderType");
const navigate = useNavigate(); const navigate = useNavigate();
// Check if checkout should be disabled
const isCheckoutDisabled = items.length === 0;
const handleCheckoutClick = async () => { const handleCheckoutClick = async () => {
try { if (items.length === 0) message.warning(t("cart.pleaseAddItemsToCart"));
await form.validateFields(); else {
navigate(`/${id}/checkout`); try {
} catch (error) { await form.validateFields();
console.log("Form validation failed:", error); navigate(`/${id}/checkout`);
} catch (error) {
console.log("Form validation failed:", error);
}
} }
}; };
@@ -60,7 +60,6 @@ export default function CartFooter({ form }: CartFooterProps) {
color: "white", color: "white",
width: "100%", width: "100%",
}} }}
disabled={isCheckoutDisabled}
onClick={handleCheckoutClick} onClick={handleCheckoutClick}
> >
{t("cart.checkout")} {t("cart.checkout")}

View File

@@ -1,3 +1,4 @@
import { Form } from "antd";
import ProInputCard from "components/ProInputCard/ProInputCard.tsx"; import ProInputCard from "components/ProInputCard/ProInputCard.tsx";
import ProRatioGroups from "components/ProRatioGroups/ProRatioGroups.tsx"; import ProRatioGroups from "components/ProRatioGroups/ProRatioGroups.tsx";
import { selectCart, updateEstimateTime } from "features/order/orderSlice.ts"; import { selectCart, updateEstimateTime } from "features/order/orderSlice.ts";
@@ -27,22 +28,24 @@ export default function TimeEstimateCard() {
return ( return (
<> <>
<ProInputCard title={t("cart.estimateTime")}> <ProInputCard title={t("cart.estimateTime")}>
<ProRatioGroups <Form.Item name="estimateWay" required rules={[{ required: true }]}>
options={[ <ProRatioGroups
{ label: t("cart.now"), value: "now", price: "" }, options={[
{ label: t("cart.later"), value: "later", price: "" }, { label: t("cart.now"), value: "now", price: "" },
]} { label: t("cart.later"), value: "later", price: "" },
value={estimateWay} ]}
onRatioClick={(value) => { value={estimateWay}
if (value === "now") { onRatioClick={(value) => {
setEstimateWay(value); if (value === "now") {
handleEstimateTimeSave(new Date(), "now"); setEstimateWay(value);
} else { handleEstimateTimeSave(new Date(), "now");
setEstimateWay(value); } else {
setIsEstimateTimeOpen(true); setEstimateWay(value);
} setIsEstimateTimeOpen(true);
}} }
/> }}
/>
</Form.Item>
</ProInputCard> </ProInputCard>
{isDesktop ? ( {isDesktop ? (
<Dialog <Dialog

View File

@@ -1,3 +1,7 @@
.youMightAlsoLikeTitle {
margin: 0 16px 16px 16px;
}
.youMightAlsoLikeContainer { .youMightAlsoLikeContainer {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@@ -66,6 +70,9 @@
.youMightAlsoLikeContainer { .youMightAlsoLikeContainer {
height: 200px !important; height: 200px !important;
} }
.youMightAlsoLikeTitle {
margin: 16px;
}
} }
/* Hover effects for devices that support hover */ /* Hover effects for devices that support hover */

View File

@@ -117,7 +117,7 @@ export default function YouMightAlsoLike() {
return ( return (
<> <>
<div style={{ margin: 16 }}> <div className={styles.youMightAlsoLikeTitle} >
<ProText <ProText
strong strong
style={{ style={{

View File

@@ -1,11 +1,11 @@
import { Button } from "antd"; import { Button, FormInstance } from "antd";
import { useCallback, useMemo } from "react"; import { useCallback, useMemo } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import styles from "../../address/address.module.css"; import styles from "../../address/address.module.css";
import useOrder from "../hooks/useOrder"; import useOrder from "../hooks/useOrder";
export default function CheckoutButton() { export default function CheckoutButton({ form }: { form: FormInstance }) {
const { t } = useTranslation(); const { t } = useTranslation();
const orderType = useMemo(() => localStorage.getItem("orderType"), []); const orderType = useMemo(() => localStorage.getItem("orderType"), []);
const navigate = useNavigate(); const navigate = useNavigate();
@@ -16,13 +16,18 @@ export default function CheckoutButton() {
navigate(`/${id}/split-bill`); navigate(`/${id}/split-bill`);
}, [navigate, id]); }, [navigate, id]);
const handlePlaceOrderClick = useCallback(() => { const handlePlaceOrderClick = useCallback(async () => {
handleCreateOrder(); try {
}, [handleCreateOrder]); await form.validateFields();
handleCreateOrder();
} catch (error) {
console.log(error);
}
}, [handleCreateOrder, form]);
const shouldShowSplitBill = useMemo( const shouldShowSplitBill = useMemo(
() => orderType === "dine-in", () => orderType === "dine-in",
[orderType] [orderType],
); );
return ( return (

View File

@@ -0,0 +1,27 @@
import { Form, Input } from "antd";
import ProInputCard from "components/ProInputCard/ProInputCard.tsx";
import { selectCart, updatePhone } from "features/order/orderSlice";
import { useTranslation } from "react-i18next";
import { useAppDispatch, useAppSelector } from "redux/hooks";
export default function PhoneCard() {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const { phone } = useAppSelector(selectCart);
return (
<>
<ProInputCard title={t("checkout.phoneNumber")}>
<Form.Item name="phone" required rules={[{ required: true }]}>
<Input
placeholder={t("checkout.phoneNumber")}
size="large"
autoFocus={false}
style={{ padding: "7px 11px", height: 50, borderRadius: 888 }}
value={phone}
onChange={(e) => dispatch(updatePhone(e.target.value))}
/>
</Form.Item>
</ProInputCard>
</>
);
}

View File

@@ -13,14 +13,14 @@ export default function useOrder() {
const { mobilenumber, user_uuid } = JSON.parse( const { mobilenumber, user_uuid } = JSON.parse(
localStorage.getItem("customer") || "{}" localStorage.getItem("customer") || "{}"
) as Customer; ) as Customer;
const { items, coupon, tip, tables, specialRequest } = const { items, coupon, tip, tables, specialRequest, phone } =
useAppSelector(selectCart); useAppSelector(selectCart);
const [createOrder] = useCreateOrderMutation(); const [createOrder] = useCreateOrderMutation();
const handleCreateOrder = useCallback(() => { const handleCreateOrder = useCallback(() => {
createOrder({ createOrder({
phone: mobilenumber, phone: mobilenumber || phone,
couponID: coupon, couponID: coupon,
discountAmount: 0, discountAmount: 0,
comment: specialRequest, comment: specialRequest,

View File

@@ -1,3 +1,4 @@
import { Form } from "antd";
import OrderSummary from "components/OrderSummary/OrderSummary"; import OrderSummary from "components/OrderSummary/OrderSummary";
import PaymentMethods from "components/PaymentMethods/PaymentMethods"; import PaymentMethods from "components/PaymentMethods/PaymentMethods";
import ProHeader from "components/ProHeader/ProHeader"; import ProHeader from "components/ProHeader/ProHeader";
@@ -9,12 +10,15 @@ import CheckoutButton from "./components/CheckoutButton";
import { GiftDetails } from "./components/GiftDetails"; import { GiftDetails } from "./components/GiftDetails";
import { OfficeDetails } from "./components/OfficeDetails"; import { OfficeDetails } from "./components/OfficeDetails";
import { RoomDetails } from "./components/RoomDetails"; import { RoomDetails } from "./components/RoomDetails";
import PhoneCard from "./components/phoneCard";
export default function CheckoutPage() { export default function CheckoutPage() {
const { t } = useTranslation(); const { t } = useTranslation();
const [form] = Form.useForm();
return ( return (
<> <>
<Form form={form}>
<ProHeader>{t("checkout.title")}</ProHeader> <ProHeader>{t("checkout.title")}</ProHeader>
<div className={styles.checkoutContainer}> <div className={styles.checkoutContainer}>
<AddressSummary /> <AddressSummary />
@@ -22,11 +26,13 @@ export default function CheckoutPage() {
<OfficeDetails /> <OfficeDetails />
<GiftDetails /> <GiftDetails />
<BriefMenu /> <BriefMenu />
<PhoneCard />
<PaymentMethods /> <PaymentMethods />
<OrderSummary /> <OrderSummary />
</div> </div>
<CheckoutButton /> <CheckoutButton form={form} />
</Form>
</> </>
); );
} }