order: add progress timer

This commit is contained in:
2025-12-17 22:44:05 +03:00
parent 9a3608d5ab
commit 02e1e42496
6 changed files with 302 additions and 198 deletions

View File

@@ -73,6 +73,7 @@ export default function TableNumberCard() {
width: "100%", width: "100%",
height: 50, height: 50,
fontSize: 12, fontSize: 12,
borderRadius: 888,
}} }}
onChange={(value) => { onChange={(value) => {
console.log(value); console.log(value);

View File

@@ -1,4 +1,4 @@
import { MinusOutlined, PlusOutlined } from "@ant-design/icons"; import { PlusOutlined } from "@ant-design/icons";
import { Button, message } from "antd"; import { Button, message } from "antd";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
@@ -8,8 +8,7 @@ import styles from "./AddToCartButton.module.css";
import { useAppSelector, useAppDispatch } from "redux/hooks"; import { useAppSelector, useAppDispatch } from "redux/hooks";
import { Product } from "utils/types/appTypes"; import { Product } from "utils/types/appTypes";
import NextIcon from "components/Icons/NextIcon"; import NextIcon from "components/Icons/NextIcon";
import { addItem, updateQuantity, removeItem } from "features/order/orderSlice"; import { addItem } from "features/order/orderSlice";
import ProText from "components/ProText";
export function AddToCartButton({ item }: { item: Product }) { export function AddToCartButton({ item }: { item: Product }) {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -20,15 +19,15 @@ export function AddToCartButton({ item }: { item: Product }) {
const { data: restaurant } = useGetRestaurantDetailsQuery(subdomain, { const { data: restaurant } = useGetRestaurantDetailsQuery(subdomain, {
skip: !subdomain, skip: !subdomain,
}); });
const { items } = useAppSelector((state) => state.order); // const { items } = useAppSelector((state) => state.order);
// Check if product is in cart // Check if product is in cart
const cartItemsForProduct = items.filter((i) => i.id === item.id); // const cartItemsForProduct = items.filter((i) => i.id === item.id);
const totalQuantity = cartItemsForProduct.reduce( // const totalQuantity = cartItemsForProduct.reduce(
(total, item) => total + item.quantity, // (total, item) => total + item.quantity,
0 // 0,
); // );
const isInCart = totalQuantity > 0; // const isInCart = totalQuantity > 0;
// Check if item has extras, variants, or groups // Check if item has extras, variants, or groups
const hasOptions = const hasOptions =
@@ -37,12 +36,12 @@ export function AddToCartButton({ item }: { item: Product }) {
(item.theExtrasGroups && item.theExtrasGroups.length > 0); (item.theExtrasGroups && item.theExtrasGroups.length > 0);
// Find basic cart item (no variants/extras) - the one added by quick add // Find basic cart item (no variants/extras) - the one added by quick add
const basicCartItem = cartItemsForProduct.find( // const basicCartItem = cartItemsForProduct.find(
(cartItem) => // (cartItem) =>
(!cartItem.variant || cartItem.variant === "None") && // (!cartItem.variant || cartItem.variant === "None") &&
(!cartItem.extras || cartItem.extras.length === 0) && // (!cartItem.extras || cartItem.extras.length === 0) &&
(!cartItem.extrasgroupnew || cartItem.extrasgroupnew.length === 0) // (!cartItem.extrasgroupnew || cartItem.extrasgroupnew.length === 0),
); // );
const handleClick = () => { const handleClick = () => {
if (restaurant && !restaurant.isOpened) { if (restaurant && !restaurant.isOpened) {
@@ -76,86 +75,124 @@ export function AddToCartButton({ item }: { item: Product }) {
} }
}; };
const handleMinusClick = () => { // const handleMinusClick = () => {
if (restaurant && !restaurant.isOpened) { // if (restaurant && !restaurant.isOpened) {
message.warning(t("menu.restaurantIsClosed")); // message.warning(t("menu.restaurantIsClosed"));
return; // return;
} // }
if (basicCartItem && basicCartItem.uniqueId) { // if (basicCartItem && basicCartItem.uniqueId) {
if (basicCartItem.quantity > 1) { // if (basicCartItem.quantity > 1) {
// Decrease quantity // // Decrease quantity
dispatch( // dispatch(
updateQuantity({ // updateQuantity({
id: basicCartItem.id, // id: basicCartItem.id,
uniqueId: basicCartItem.uniqueId, // uniqueId: basicCartItem.uniqueId,
quantity: basicCartItem.quantity - 1, // quantity: basicCartItem.quantity - 1,
}) // }),
); // );
} else { // } else {
// Remove item if quantity is 1 // // Remove item if quantity is 1
dispatch(removeItem(basicCartItem.uniqueId)); // dispatch(removeItem(basicCartItem.uniqueId));
} // }
} else if (cartItemsForProduct.length > 0) { // } else if (cartItemsForProduct.length > 0) {
// If no basic item found but items exist, remove the first one // // If no basic item found but items exist, remove the first one
const firstItem = cartItemsForProduct[0]; // const firstItem = cartItemsForProduct[0];
if (firstItem.uniqueId) { // if (firstItem.uniqueId) {
if (firstItem.quantity > 1) { // if (firstItem.quantity > 1) {
dispatch( // dispatch(
updateQuantity({ // updateQuantity({
id: firstItem.id, // id: firstItem.id,
uniqueId: firstItem.uniqueId, // uniqueId: firstItem.uniqueId,
quantity: firstItem.quantity - 1, // quantity: firstItem.quantity - 1,
}) // }),
); // );
} else { // } else {
dispatch(removeItem(firstItem.uniqueId)); // dispatch(removeItem(firstItem.uniqueId));
} // }
} // }
} // }
}; // };
const handlePlusClick = () => { // const handlePlusClick = () => {
if (restaurant && !restaurant.isOpened) { // if (restaurant && !restaurant.isOpened) {
message.warning(t("menu.restaurantIsClosed")); // message.warning(t("menu.restaurantIsClosed"));
return; // return;
} // }
if (basicCartItem && basicCartItem.uniqueId) { // if (basicCartItem && basicCartItem.uniqueId) {
// Increase quantity of existing basic item // // Increase quantity of existing basic item
dispatch( // dispatch(
updateQuantity({ // updateQuantity({
id: basicCartItem.id, // id: basicCartItem.id,
uniqueId: basicCartItem.uniqueId, // uniqueId: basicCartItem.uniqueId,
quantity: basicCartItem.quantity + 1, // quantity: basicCartItem.quantity + 1,
}) // }),
); // );
} else if (!hasOptions) { // } else if (!hasOptions) {
// Add new basic item if no options // // Add new basic item if no options
dispatch( // dispatch(
addItem({ // addItem({
item: { // item: {
id: Number(item.id), // id: Number(item.id),
name: item.name, // name: item.name,
price: item.price, // price: item.price,
image: item.image, // image: item.image,
description: item.description, // description: item.description,
variant: "None", // variant: "None",
isHasLoyalty: item.isHasLoyalty, // isHasLoyalty: item.isHasLoyalty,
no_of_stamps_give: item.no_of_stamps_give, // no_of_stamps_give: item.no_of_stamps_give,
}, // },
quantity: 1, // quantity: 1,
}), // }),
); // );
} else { // } else {
// If has options, navigate to product page // // If has options, navigate to product page
navigate(`/${subdomain}/product/${item.id}`); // navigate(`/${subdomain}/product/${item.id}`);
} // }
}; // };
return isInCart ? ( return (
<>
<div <div
style={{
width: 48,
height: 48,
position: "absolute",
bottom: -11,
[isRTL ? "left" : "right"]: -2,
backgroundColor: "var(--background)",
borderRadius: "50%",
}}
>
<Button
shape="circle"
iconPosition="start"
icon={
hasOptions ? (
<NextIcon
className={styles.plusIcon}
iconColor="#fff"
iconSize={16}
/>
) : (
<PlusOutlined title="add" className={styles.plusIcon} />
)
}
size="small"
onClick={handleClick}
className={styles.addButton}
style={{
backgroundColor: colors.primary,
width: 36,
height: 36,
position: "absolute",
bottom: 6,
[isRTL ? "left" : "right"]: 6,
}}
/>
</div>
);
/* <div
className={styles.addButton} className={styles.addButton}
style={{ style={{
width: 107, width: 107,
@@ -238,46 +275,5 @@ export function AddToCartButton({ item }: { item: Product }) {
/> />
</div> </div>
</> </>
) : ( */
<>
<div
style={{
width: 48,
height: 48,
position: "absolute",
bottom: -11,
[isRTL ? "left" : "right"]: -2,
backgroundColor: "var(--background)",
borderRadius: "50%",
}}
>
<Button
shape="circle"
iconPosition="start"
icon={
hasOptions ? (
<NextIcon
className={styles.plusIcon}
iconColor="#fff"
iconSize={16}
/>
) : (
<PlusOutlined title="add" className={styles.plusIcon} />
)
}
size="small"
onClick={handleClick}
className={styles.addButton}
style={{
backgroundColor: colors.primary,
width: 36,
height: 36,
position: "absolute",
bottom: 6,
[isRTL ? "left" : "right"]: 6,
}}
/>
</div>
</>
);
} }

View File

@@ -1,4 +1,4 @@
import { Button, Card, Divider, Image } from "antd"; import { Button, Card, Divider, Flex, Image, Progress, Tooltip } from "antd";
import Ads2 from "components/Ads/Ads2"; import Ads2 from "components/Ads/Ads2";
import { CancelOrderBottomSheet } from "components/CustomBottomSheet/CancelOrderBottomSheet"; import { CancelOrderBottomSheet } from "components/CustomBottomSheet/CancelOrderBottomSheet";
import LocationIcon from "components/Icons/LocationIcon"; import LocationIcon from "components/Icons/LocationIcon";
@@ -11,7 +11,7 @@ import ProInputCard from "components/ProInputCard/ProInputCard";
import ProText from "components/ProText"; import ProText from "components/ProText";
import ProTitle from "components/ProTitle"; import ProTitle from "components/ProTitle";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { useEffect, useRef } from "react"; import { useEffect, useRef, useState } 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 { import {
@@ -74,6 +74,57 @@ export default function OrderPage() {
} }
}, [orderDetails?.status, restaurantSubdomain, refetchRestaurantDetails]); }, [orderDetails?.status, restaurantSubdomain, refetchRestaurantDetails]);
// Check if order is in progress (check last status alias)
const lastStatus = orderDetails?.status?.[orderDetails.status.length - 1];
const isInProgress = lastStatus?.alias === "accepted_by_restaurant";
// Calculate timer and progress
const [remainingSeconds, setRemainingSeconds] = useState<number>(0);
const [progressPercent, setProgressPercent] = useState<number>(0);
useEffect(() => {
if (
!isInProgress ||
!orderDetails?.order?.time_to_prepare ||
!orderDetails?.order?.created_at
) {
return;
}
const updateTimer = () => {
const orderCreatedAt = dayjs(orderDetails.order.created_at);
const now = dayjs();
const elapsedSeconds = now.diff(orderCreatedAt, "second");
// time_to_prepare is in minutes, convert to seconds
const totalSeconds =
(Number(orderDetails.order.time_to_prepare) || 0) * 60;
const remaining = Math.max(0, totalSeconds - elapsedSeconds);
setRemainingSeconds(remaining);
const percent =
totalSeconds > 0
? Math.min(100, Math.max(0, (elapsedSeconds / totalSeconds) * 100))
: 0;
setProgressPercent(percent);
};
updateTimer();
const interval = setInterval(updateTimer, 1000);
return () => clearInterval(interval);
}, [
isInProgress,
orderDetails?.order?.time_to_prepare,
orderDetails?.status,
]);
// Format remaining time as MM:SS
const formatTimer = (totalSeconds: number): string => {
const mins = Math.floor(totalSeconds / 60);
const secs = Math.floor(totalSeconds % 60);
return `${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`;
};
return ( return (
<> <>
<ProHeader>{t("order.title")}</ProHeader> <ProHeader>{t("order.title")}</ProHeader>
@@ -126,7 +177,22 @@ export default function OrderPage() {
</div> </div>
</div> </div>
{isInProgress ? (
<Flex gap="small" wrap justify="center">
<Tooltip title="3 done / 3 in progress / 4 to do">
<Progress
percent={progressPercent}
format={() => formatTimer(remainingSeconds)}
// success={{ percent: progressPercent }}
strokeColor="#FFB700"
size={120}
type="dashboard"
/>
</Tooltip>
</Flex>
) : (
<OrderDishIcon className={styles.orderDishIcon} /> <OrderDishIcon className={styles.orderDishIcon} />
)}
<div> <div>
<ProTitle <ProTitle

View File

@@ -163,7 +163,7 @@ export function CustomAmountChoiceBottomSheet({
style={{ style={{
display: "flex", display: "flex",
gap: 12, gap: 12,
marginTop: 20, margin: 20,
}} }}
> >
<Button <Button

View File

@@ -1,4 +1,4 @@
import { Button, Card, Image } from "antd"; import { Button, Card, Checkbox, Divider, Image } from "antd";
import { ProBottomSheet } from "components/ProBottomSheet/ProBottomSheet.tsx"; import { ProBottomSheet } from "components/ProBottomSheet/ProBottomSheet.tsx";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -7,6 +7,7 @@ import ArabicPrice from "components/ArabicPrice";
import ProText from "components/ProText"; import ProText from "components/ProText";
import { selectCart, updateSplitBillAmount } from "features/order/orderSlice"; import { selectCart, updateSplitBillAmount } from "features/order/orderSlice";
import { useAppDispatch, useAppSelector } from "redux/hooks"; import { useAppDispatch, useAppSelector } from "redux/hooks";
import styles from "./SplitBill.module.css";
interface SplitBillChoiceBottomSheetProps { interface SplitBillChoiceBottomSheetProps {
isOpen: boolean; isOpen: boolean;
@@ -72,13 +73,13 @@ export function PayForYourItemsChoiceBottomSheet({
title={t("splitBill.payForYourItems")} title={t("splitBill.payForYourItems")}
showCloseButton={true} showCloseButton={true}
initialSnap={1} initialSnap={1}
height={745} height={720}
snapPoints={[745]} snapPoints={[720]}
contentStyle={{ padding: 0 }}
> >
<div <div
style={{ style={{
padding: "0 20px", padding: 20,
margin: "20px 0",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
gap: 12, gap: 12,
@@ -101,11 +102,13 @@ export function PayForYourItemsChoiceBottomSheet({
const itemTotal = item.price * item.quantity; const itemTotal = item.price * item.quantity;
return ( return (
<>
<Card <Card
key={itemId} key={itemId}
style={{ style={{
border: "none", border: "none",
cursor: "pointer", cursor: "pointer",
padding: 0,
}} }}
onClick={() => handleItemToggle(itemId)} onClick={() => handleItemToggle(itemId)}
> >
@@ -139,14 +142,9 @@ export function PayForYourItemsChoiceBottomSheet({
<ProText style={{ fontSize: 14, fontWeight: 500 }}> <ProText style={{ fontSize: 14, fontWeight: 500 }}>
{item.name} {item.name}
</ProText> </ProText>
<ProText type="secondary" style={{ fontSize: 12 }}>
{t("cart.quantity")}: {item.quantity}
</ProText>
<ArabicPrice price={itemTotal} /> <ArabicPrice price={itemTotal} />
</div> </div>
<Button <div
type={isSelected ? "primary" : "default"}
shape="circle"
style={{ style={{
width: 32, width: 32,
height: 32, height: 32,
@@ -155,11 +153,20 @@ export function PayForYourItemsChoiceBottomSheet({
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
}} }}
onClick={(e) => e.stopPropagation()}
> >
{isSelected ? "✓" : "+"} <Checkbox
</Button> className={styles.circleCheckbox}
checked={isSelected}
onChange={() => handleItemToggle(itemId)}
/>
</div>
</div> </div>
</Card> </Card>
{item !== items[items.length - 1] && (
<Divider style={{ margin: "0" }} />
)}
</>
); );
}) })
)} )}
@@ -189,7 +196,7 @@ export function PayForYourItemsChoiceBottomSheet({
style={{ style={{
display: "flex", display: "flex",
gap: 12, gap: 12,
marginTop: 20, margin: 20,
}} }}
> >
<Button <Button

View File

@@ -19,7 +19,6 @@
height: 48px !important; height: 48px !important;
} }
.backToHomePage { .backToHomePage {
width: 100%; width: 100%;
height: 48px; height: 48px;
@@ -53,3 +52,38 @@
align-items: center; align-items: center;
font-size: 16px; font-size: 16px;
} }
/* Make AntD checkbox look like a circular check indicator (scoped via CSS modules) */
.circleCheckbox :global(.ant-checkbox-inner) {
width: 24px;
height: 24px;
border-radius: 50% !important;
border: 1.5px solid #D5D8DA;
background: transparent;
}
.circleCheckbox :global(.ant-checkbox-checked .ant-checkbox-inner) {
border-radius: 50% !important;
background: transparent;
border-color: #ffb700;
}
/* Replace AntD checkmark with a filled inner circle when checked (match SVG) */
.circleCheckbox :global(.ant-checkbox-inner::after) {
content: "";
border: 0 !important;
transform: none !important;
width: 0;
height: 0;
left: 50%;
top: 50%;
}
.circleCheckbox :global(.ant-checkbox-checked .ant-checkbox-inner::after) {
width: 18px;
height: 18px;
margin-left: -9px;
margin-top: -9px;
border-radius: 50%;
background: #ffb700;
}