implement scheduled order type

This commit is contained in:
2025-10-29 20:41:26 +03:00
parent 144cfa3f3a
commit ab5d0da47e
18 changed files with 219 additions and 82 deletions

View File

@@ -73,7 +73,8 @@
"delivery": "التوصيل",
"room": "الغرفة",
"office": "المكتب",
"booking": "الحجز"
"booking": "الحجز",
"scheduledOrder": "الطلب المجدول"
},
"home": {
"title": "العنوان",
@@ -84,7 +85,8 @@
"gift": "أرسل وجبة كهدية لشخص مميز.",
"room": "خدمة الغرف لراحتك.",
"office": "توصيل إلى مكتبك.",
"booking": "احجز طاولة مسبقاً."
"booking": "احجز طاولة مسبقاً.",
"scheduledOrder": "طلب مجدول"
},
"promotion": {
"title": "الترويجات",
@@ -235,7 +237,8 @@
"noLoyaltyItemsInCart": "لا توجد عناصر ولاء في سلة المشتريات",
"pleaseAddLoyaltyItems": "يرجى إضافة عناصر ولاء إلى سلة المشتريات لاستخدام نقاط الولاء",
"loyaltyDiscountApplied": "تم تطبيق خصم الولاء: {{itemName}} (خصم {{amount}})",
"deliveryFee": "رسوم التوصيل"
"deliveryFee": "رسوم التوصيل",
"scheduledDate": "تاريخ الطلب المجدول"
},
"checkout": {
"title": "الدفع",

View File

@@ -73,7 +73,8 @@
"delivery": "Delivery",
"noMenuItemsAvailable": "No menu items available",
"restaurantCover": "Restaurant Cover",
"restaurantLogo": "Restaurant Logo"
"restaurantLogo": "Restaurant Logo",
"scheduledOrder": "Scheduled Order"
},
"home": {
"title": "title",
@@ -100,7 +101,8 @@
"room": "Room service for your comfort.",
"office": "Delivery to your office.",
"booking": "Book a table in advance.",
"delivery": "Delivery"
"delivery": "Delivery",
"scheduledOrder": "Scheduled Order"
},
"promotion": {
"title": "Promotions",
@@ -245,7 +247,8 @@
"noLoyaltyItemsInCart": "No loyalty items found in your cart",
"pleaseAddLoyaltyItems": "Please add loyalty items to your cart to use loyalty points",
"loyaltyDiscountApplied": "Loyalty discount applied: {{itemName}} ({{amount}} off)",
"deliveryFee": "Delivery Fee"
"deliveryFee": "Delivery Fee",
"scheduledDate": "Scheduled Date"
},
"checkout": {
"title": "Checkout",
@@ -370,6 +373,7 @@
"delivery": "Delivery",
"office": "To Office",
"scheduled_order": "Scheduled",
"booking": "Booking"
"booking": "Booking",
"scheduledOrder": "Scheduled Order"
}
}

View File

@@ -8,6 +8,7 @@ interface ProInputCardProps {
title?: string | ReactNode;
titleRight?: ReactNode;
className?: string;
dividerStyle?: React.CSSProperties;
}
const ProInputCard: FunctionComponent<ProInputCardProps> = ({
@@ -15,6 +16,7 @@ const ProInputCard: FunctionComponent<ProInputCardProps> = ({
title,
titleRight,
className,
dividerStyle,
}) => {
return (
<Card className={`${styles.ProInputCard} ${className}`}>
@@ -31,7 +33,7 @@ const ProInputCard: FunctionComponent<ProInputCardProps> = ({
{title && typeof title !== "string" && title}
<div style={{ position: "relative", top: 0 }}>{titleRight}</div>
</div>
<Divider style={{ margin: "5px 0 15px 0" }} />
<Divider style={{ margin: "5px 0 15px 0", ...dividerStyle }} />
{children}
</Card>
);

View File

@@ -56,6 +56,7 @@ interface CartState {
orderType: OrderType | "";
useLoyaltyPoints: boolean;
loyaltyValidationError: string | null;
scheduledDate: string;
}
// localStorage keys
@@ -79,6 +80,7 @@ export const CART_STORAGE_KEYS = {
USE_LOYALTY_POINTS: "fascano_use_loyalty_points",
LOYALTY_VALIDATION_ERROR: "fascano_loyalty_validation_error",
RESTAURANT: "fascano_restaurant",
SCHEDULED_DATE: "fascano_scheduled_date",
} as const;
// Utility functions for localStorage
@@ -127,7 +129,10 @@ const initialState: CartState = {
),
phone: getFromLocalStorage(CART_STORAGE_KEYS.PHONE, ""),
paymentMethod: getFromLocalStorage(CART_STORAGE_KEYS.PAYMENT_METHOD, ""),
orderType: getFromLocalStorage(CART_STORAGE_KEYS.ORDER_TYPE, "" as OrderType | ""),
orderType: getFromLocalStorage(
CART_STORAGE_KEYS.ORDER_TYPE,
"" as OrderType | "",
),
useLoyaltyPoints: getFromLocalStorage(
CART_STORAGE_KEYS.USE_LOYALTY_POINTS,
false,
@@ -137,6 +142,7 @@ const initialState: CartState = {
null,
),
restaurant: getFromLocalStorage(CART_STORAGE_KEYS.RESTAURANT, { taxes: [] }),
scheduledDate: getFromLocalStorage(CART_STORAGE_KEYS.SCHEDULED_DATE, ""),
};
const orderSlice = createSlice({
@@ -504,6 +510,17 @@ const orderSlice = createSlice({
);
}
},
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),
);
}
},
},
});
@@ -534,6 +551,7 @@ export const {
clearLoyaltyValidationError,
reset,
updateRestaurant,
updateScheduledDate,
} = orderSlice.actions;
// Tax calculation helper functions

View File

@@ -384,17 +384,22 @@ label {
transition: background-color 5000s ease-in-out 0s !important;
}
/* Style for the select component and its dropdown */
:where(.ant-select .ant-select-selection-item) {
/* Styles scoped to orderTypeSelectContainer dropdown */
.order-type-select-dropdown :where(.ant-select .ant-select-selection-item) {
font-size: 12px !important;
font-weight: 700 !important;
text-align: center;
}
.menu-select-container :where(.ant-select .ant-select-arrow) {
.order-type-select-dropdown :where(.ant-select .ant-select-arrow) {
font-weight: 600 !important;
color: black;
}
.menu-select-container :where(.ant-select-dropdown .ant-select-item) {
.order-type-select-dropdown :where(.ant-select-dropdown .ant-select-item) {
font-size: 12px !important;
font-weight: 600 !important;
}
.order-type-select-dropdown :where(.ant-select-item-option) {
min-height: 30px !important;
padding: 5px 19px !important;
}

View File

@@ -1,4 +1,4 @@
import { Input } from "antd";
import { Form, Input } from "antd";
import ProInputCard from "components/ProInputCard/ProInputCard.tsx";
import { useTranslation } from "react-i18next";
@@ -6,13 +6,18 @@ export default function CarPlateCard() {
const { t } = useTranslation();
return (
<>
<ProInputCard title={t("cart.plateNumber")}>
<ProInputCard
title={t("cart.plateNumber")}
dividerStyle={{ margin: "5px 0 0 0" }}
>
<Form.Item label={t("cart.plateNumber")} name="plateNumber" style={{position:"relative", top: -5}}>
<Input
placeholder={t("plateNumber")}
placeholder={t("cart.plateNumber")}
size="large"
autoFocus={false}
style={{ padding: "7px 11px", height: 50, borderRadius: 888 }}
/>
</Form.Item>
</ProInputCard>
</>
);

View File

@@ -25,12 +25,13 @@ import useBreakPoint from "hooks/useBreakPoint.ts";
import CarPlateCard from "pages/cart/components/CarPlateCard.tsx";
import CartFooter from "pages/cart/components/cartFooter/CartFooter.tsx";
import CouponCard from "pages/cart/components/CouponCard.tsx";
import DateCard from "pages/cart/components/DateCard.tsx";
import RewardWaiterCard from "pages/cart/components/RewardWaiterCard.tsx";
import SpecialRequestCard from "pages/cart/components/specialRequest/SpecialRequestCard.tsx";
import TableNumberCard from "pages/cart/components/TableNumberCard.tsx";
import TimeEstimateCard from "pages/cart/components/timeEstimate/TimeEstimateCard.tsx";
import { useTranslation } from "react-i18next";
import { OrderType } from "pages/checkout/hooks/types";
import { useTranslation } from "react-i18next";
interface CartMobileTabletLayoutProps {
form: FormInstance;
@@ -58,6 +59,7 @@ export default function CartMobileTabletLayout({
height: 120,
};
};
return (
<>
<ProHeader>{t("cart.title")}</ProHeader>
@@ -225,11 +227,15 @@ export default function CartMobileTabletLayout({
<CouponCard />
{/* Car Plate*/}
{orderType === OrderType.Pickup && <CarPlateCard />}
{(orderType === OrderType.Pickup ||
orderType === OrderType.ScheduledOrder) && <CarPlateCard />}
{/* Estimate Time */}
{(orderType === OrderType.Delivery ||
orderType === OrderType.Pickup) && <TimeEstimateCard />}
orderType === OrderType.Pickup ||
orderType === OrderType.ScheduledOrder) && <TimeEstimateCard />}
{orderType === OrderType.ScheduledOrder && <DateCard form={form} />}
{/* Collection Method */}
{orderType === OrderType.Pickup && (

View File

@@ -1,20 +1,19 @@
import { Button, Form, Input, message } from "antd";
import { CouponBottomSheet } from "components/CustomBottomSheet/CouponBottomSheet.tsx";
import { useAppSelector, useAppDispatch } from "redux/hooks.ts";
import { selectCart, updateCoupon } from "features/order/orderSlice.ts";
import { useState } from "react";
import { message, Input, Button } from "antd";
import { useTranslation } from "react-i18next";
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 { colors } from "ThemeConstants.ts";
import DonateIcon from "components/Icons/cart/DonateIcon.tsx";
import CouponHeartIcon from "components/Icons/cart/CouponHeart.tsx";
import styles from "pages/cart/cart.module.css";
import { CouponDialog } from "components/CustomBottomSheet/CouponDialog.tsx";
import { selectCart, updateCoupon } from "features/order/orderSlice.ts";
import useBreakPoint from "hooks/useBreakPoint.ts";
import styles from "pages/cart/cart.module.css";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useAppDispatch, useAppSelector } from "redux/hooks.ts";
import { colors } from "ThemeConstants.ts";
type Props = {};
export default function CouponCard({}: Props) {
export default function CouponCard() {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const { coupon } = useAppSelector(selectCart);
@@ -57,6 +56,12 @@ export default function CouponCard({}: Props) {
<DonateIcon />
</div>
}
dividerStyle={{ margin: "5px 0 0 0" }}
>
<Form.Item
label={t("cart.couponCode")}
name="coupon"
style={{ position: "relative", top: -5 }}
>
<Input
placeholder={t("cart.couponCode")}
@@ -78,6 +83,7 @@ export default function CouponCard({}: Props) {
</Button>
}
/>
</Form.Item>
</ProInputCard>
{isDesktop ? (
<CouponDialog

View File

@@ -0,0 +1,56 @@
import { Form, FormInstance, Input } from "antd";
import DatePickerBottomSheet from "components/CustomBottomSheet/DatePickerBottomSheet";
import ProInputCard from "components/ProInputCard/ProInputCard.tsx";
import { selectCart, updateScheduledDate } from "features/order/orderSlice";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useAppDispatch, useAppSelector } from "redux/hooks";
export default function DateCard({ form }: { form: FormInstance }) {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const { scheduledDate } = useAppSelector(selectCart);
const [isOpen, setIsOpen] = useState(false);
return (
<>
<ProInputCard
title={t("cart.scheduledDate")}
dividerStyle={{ margin: "5px 0 0 0" }}
>
<Form.Item
label={t("cart.scheduledDate")}
name="date"
required
rules={[{ required: true }]}
style={{position:"relative", top: -5}}
>
<Input
placeholder={t("cart.scheduledDate")}
size="large"
onClick={() => setIsOpen(true)}
readOnly
value={scheduledDate}
style={{
cursor: "pointer",
height: 50,
fontSize: 14,
borderRadius: 888,
}}
/>
</Form.Item>
</ProInputCard>
<DatePickerBottomSheet
isOpen={isOpen}
onClose={() => setIsOpen(false)}
onDateSelect={(date) => {
const formattedDate = `${date.month}/${date.day}/${date.year}`;
dispatch(updateScheduledDate(formattedDate));
form.setFieldValue("date", formattedDate);
}}
initialDate={new Date(1990, 0, 1)}
/>
</>
);
}

View File

@@ -26,12 +26,15 @@ export default function TableNumberCard() {
<ProInputCard
title={t("cart.tableNumber")}
className={styles.tableNumberCard}
dividerStyle={{ margin: "5px 0 0 0" }}
>
<Form.Item
label={t("cart.tableNumber")}
name="tables"
required
rules={[{ required: true, message: t("cart.pleaseSelectTable") }]}
initialValue={tables}
style={{ position: "relative", top: -5 }}
>
<Select
value={tables}

View File

@@ -1,5 +1,5 @@
import { RightOutlined } from "@ant-design/icons";
import { Input } from "antd";
import { Form, Input } from "antd";
import ProInputCard from "components/ProInputCard/ProInputCard.tsx";
import { selectCart, updateSpecialRequest } from "features/order/orderSlice.ts";
import useBreakPoint from "hooks/useBreakPoint.ts";
@@ -27,7 +27,15 @@ export default function SpecialRequestCard() {
};
return (
<>
<ProInputCard title={t("cart.specialRequest")}>
<ProInputCard
title={t("cart.specialRequest")}
dividerStyle={{ margin: "5px 0 0 0" }}
>
<Form.Item
label={t("cart.specialRequest")}
name="specialRequest"
style={{ position: "relative", top: -5 }}
>
<Input
value={specialRequest}
placeholder={t("cart.specialRequest")}
@@ -43,6 +51,7 @@ export default function SpecialRequestCard() {
</div>
}
/>
</Form.Item>
</ProInputCard>
{isDesktop ? (
<Dialog

View File

@@ -28,7 +28,7 @@ export default function CartPage() {
// Enhanced desktop layout
if (isDesktop) {
return (
<Form form={form}>
<Form form={form} layout="vertical">
<CartDesktopLayout form={form} />
</Form>
);
@@ -36,7 +36,7 @@ export default function CartPage() {
// Mobile/Tablet Layout (existing code)
return (
<Form form={form}>
<Form form={form} layout="vertical">
<CartMobileTabletLayout form={form} />
</Form>
);

View File

@@ -119,7 +119,6 @@
transform-origin: top center;
gap: 0.5rem;
padding: 0.5rem 1rem 1rem 1rem;
margin-bottom: 0.5rem;
user-select: none;
}

View File

@@ -166,7 +166,6 @@ export function CategoriesList({ categories }: CategoriesListProps) {
className={`${styles.categoriesContainer} ${
isCategoriesSticky ? styles.categoriesSticky : ""
}`}
style={!isCategoriesSticky ? { paddingTop: "1rem" } : {}}
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}

View File

@@ -101,7 +101,7 @@ function MenuPage() {
<BackButton />
</div>
<div
className={`${styles.headerFloatingBtn} ${styles.orderTypeSelectContainer} menu-select-container`}
className={`${styles.headerFloatingBtn} ${styles.orderTypeSelectContainer}`}
>
<Select
value={orderType}
@@ -112,6 +112,7 @@ function MenuPage() {
variant="borderless"
size="small"
className={styles.orderTypeSelect}
classNames={{ popup: { root: "order-type-select-dropdown" } }}
listHeight={150}
/>
</div>
@@ -138,7 +139,7 @@ function MenuPage() {
</div>
<div className={`${styles.pageContainer}`}>
<Space direction="vertical" style={{ width: "100%", gap: 16 }}>
<Space direction="vertical" style={{ width: "100%" }}>
<div>
{restaurant?.loyalty_stamps &&
restaurant?.is_loyalty_enabled && <LoyaltyCard />}

View File

@@ -1,3 +1,4 @@
import { ScheduleFilled } from "@ant-design/icons";
import { Card } from "antd";
import BackIcon from "components/Icons/BackIcon";
import BookingIcon from "components/Icons/BookingIcon";
@@ -10,11 +11,11 @@ import ToOfficeIcon from "components/Icons/ToOfficeIcon";
import ToRoomIcon from "components/Icons/ToRoomIcon";
import ProTitle from "components/ProTitle";
import { updateOrderType } from "features/order/orderSlice";
import { OrderType } from "pages/checkout/hooks/types.ts";
import { useTranslation } from "react-i18next";
import { Link, useParams } from "react-router-dom";
import { useAppDispatch, useAppSelector } from "redux/hooks";
import styles from "./restaurant.module.css";
import { OrderType } from "pages/checkout/hooks/types.ts";
interface RestaurantServicesProps {
dineIn?: boolean;
@@ -145,6 +146,21 @@ export default function RestaurantServices({
},
]) ||
[]),
...((true && [
{
id: OrderType.ScheduledOrder,
title: t("common.scheduledOrder"),
description: t("home.services.scheduledOrder"),
icon: (
<ScheduleFilled
className={styles.serviceIcon + " " + styles.scheduledOrderIcon}
/>
),
color: "bg-indigo-50 text-indigo-600",
href: `/${id}/menu?orderType=${OrderType.ScheduledOrder}`,
},
]) ||
[]),
];
// Determine grid class based on number of services

View File

@@ -1003,3 +1003,8 @@
.deliveryIcon {
margin-top: -1px;
}
.scheduledOrderIcon {
margin-top: -2px;
font-size: 22px;
}