order: add progress timer
This commit is contained in:
@@ -73,6 +73,7 @@ export default function TableNumberCard() {
|
||||
width: "100%",
|
||||
height: 50,
|
||||
fontSize: 12,
|
||||
borderRadius: 888,
|
||||
}}
|
||||
onChange={(value) => {
|
||||
console.log(value);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { MinusOutlined, PlusOutlined } from "@ant-design/icons";
|
||||
import { PlusOutlined } from "@ant-design/icons";
|
||||
import { Button, message } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
@@ -8,8 +8,7 @@ import styles from "./AddToCartButton.module.css";
|
||||
import { useAppSelector, useAppDispatch } from "redux/hooks";
|
||||
import { Product } from "utils/types/appTypes";
|
||||
import NextIcon from "components/Icons/NextIcon";
|
||||
import { addItem, updateQuantity, removeItem } from "features/order/orderSlice";
|
||||
import ProText from "components/ProText";
|
||||
import { addItem } from "features/order/orderSlice";
|
||||
|
||||
export function AddToCartButton({ item }: { item: Product }) {
|
||||
const { t } = useTranslation();
|
||||
@@ -20,15 +19,15 @@ export function AddToCartButton({ item }: { item: Product }) {
|
||||
const { data: restaurant } = useGetRestaurantDetailsQuery(subdomain, {
|
||||
skip: !subdomain,
|
||||
});
|
||||
const { items } = useAppSelector((state) => state.order);
|
||||
// const { items } = useAppSelector((state) => state.order);
|
||||
|
||||
// Check if product is in cart
|
||||
const cartItemsForProduct = items.filter((i) => i.id === item.id);
|
||||
const totalQuantity = cartItemsForProduct.reduce(
|
||||
(total, item) => total + item.quantity,
|
||||
0
|
||||
);
|
||||
const isInCart = totalQuantity > 0;
|
||||
// const cartItemsForProduct = items.filter((i) => i.id === item.id);
|
||||
// const totalQuantity = cartItemsForProduct.reduce(
|
||||
// (total, item) => total + item.quantity,
|
||||
// 0,
|
||||
// );
|
||||
// const isInCart = totalQuantity > 0;
|
||||
|
||||
// Check if item has extras, variants, or groups
|
||||
const hasOptions =
|
||||
@@ -37,12 +36,12 @@ export function AddToCartButton({ item }: { item: Product }) {
|
||||
(item.theExtrasGroups && item.theExtrasGroups.length > 0);
|
||||
|
||||
// Find basic cart item (no variants/extras) - the one added by quick add
|
||||
const basicCartItem = cartItemsForProduct.find(
|
||||
(cartItem) =>
|
||||
(!cartItem.variant || cartItem.variant === "None") &&
|
||||
(!cartItem.extras || cartItem.extras.length === 0) &&
|
||||
(!cartItem.extrasgroupnew || cartItem.extrasgroupnew.length === 0)
|
||||
);
|
||||
// const basicCartItem = cartItemsForProduct.find(
|
||||
// (cartItem) =>
|
||||
// (!cartItem.variant || cartItem.variant === "None") &&
|
||||
// (!cartItem.extras || cartItem.extras.length === 0) &&
|
||||
// (!cartItem.extrasgroupnew || cartItem.extrasgroupnew.length === 0),
|
||||
// );
|
||||
|
||||
const handleClick = () => {
|
||||
if (restaurant && !restaurant.isOpened) {
|
||||
@@ -76,86 +75,124 @@ export function AddToCartButton({ item }: { item: Product }) {
|
||||
}
|
||||
};
|
||||
|
||||
const handleMinusClick = () => {
|
||||
if (restaurant && !restaurant.isOpened) {
|
||||
message.warning(t("menu.restaurantIsClosed"));
|
||||
return;
|
||||
}
|
||||
// const handleMinusClick = () => {
|
||||
// if (restaurant && !restaurant.isOpened) {
|
||||
// message.warning(t("menu.restaurantIsClosed"));
|
||||
// return;
|
||||
// }
|
||||
|
||||
if (basicCartItem && basicCartItem.uniqueId) {
|
||||
if (basicCartItem.quantity > 1) {
|
||||
// Decrease quantity
|
||||
dispatch(
|
||||
updateQuantity({
|
||||
id: basicCartItem.id,
|
||||
uniqueId: basicCartItem.uniqueId,
|
||||
quantity: basicCartItem.quantity - 1,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
// Remove item if quantity is 1
|
||||
dispatch(removeItem(basicCartItem.uniqueId));
|
||||
}
|
||||
} else if (cartItemsForProduct.length > 0) {
|
||||
// If no basic item found but items exist, remove the first one
|
||||
const firstItem = cartItemsForProduct[0];
|
||||
if (firstItem.uniqueId) {
|
||||
if (firstItem.quantity > 1) {
|
||||
dispatch(
|
||||
updateQuantity({
|
||||
id: firstItem.id,
|
||||
uniqueId: firstItem.uniqueId,
|
||||
quantity: firstItem.quantity - 1,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
dispatch(removeItem(firstItem.uniqueId));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
// if (basicCartItem && basicCartItem.uniqueId) {
|
||||
// if (basicCartItem.quantity > 1) {
|
||||
// // Decrease quantity
|
||||
// dispatch(
|
||||
// updateQuantity({
|
||||
// id: basicCartItem.id,
|
||||
// uniqueId: basicCartItem.uniqueId,
|
||||
// quantity: basicCartItem.quantity - 1,
|
||||
// }),
|
||||
// );
|
||||
// } else {
|
||||
// // Remove item if quantity is 1
|
||||
// dispatch(removeItem(basicCartItem.uniqueId));
|
||||
// }
|
||||
// } else if (cartItemsForProduct.length > 0) {
|
||||
// // If no basic item found but items exist, remove the first one
|
||||
// const firstItem = cartItemsForProduct[0];
|
||||
// if (firstItem.uniqueId) {
|
||||
// if (firstItem.quantity > 1) {
|
||||
// dispatch(
|
||||
// updateQuantity({
|
||||
// id: firstItem.id,
|
||||
// uniqueId: firstItem.uniqueId,
|
||||
// quantity: firstItem.quantity - 1,
|
||||
// }),
|
||||
// );
|
||||
// } else {
|
||||
// dispatch(removeItem(firstItem.uniqueId));
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// };
|
||||
|
||||
const handlePlusClick = () => {
|
||||
if (restaurant && !restaurant.isOpened) {
|
||||
message.warning(t("menu.restaurantIsClosed"));
|
||||
return;
|
||||
}
|
||||
// const handlePlusClick = () => {
|
||||
// if (restaurant && !restaurant.isOpened) {
|
||||
// message.warning(t("menu.restaurantIsClosed"));
|
||||
// return;
|
||||
// }
|
||||
|
||||
if (basicCartItem && basicCartItem.uniqueId) {
|
||||
// Increase quantity of existing basic item
|
||||
dispatch(
|
||||
updateQuantity({
|
||||
id: basicCartItem.id,
|
||||
uniqueId: basicCartItem.uniqueId,
|
||||
quantity: basicCartItem.quantity + 1,
|
||||
})
|
||||
);
|
||||
} else if (!hasOptions) {
|
||||
// Add new basic item if no options
|
||||
dispatch(
|
||||
addItem({
|
||||
item: {
|
||||
id: Number(item.id),
|
||||
name: item.name,
|
||||
price: item.price,
|
||||
image: item.image,
|
||||
description: item.description,
|
||||
variant: "None",
|
||||
isHasLoyalty: item.isHasLoyalty,
|
||||
no_of_stamps_give: item.no_of_stamps_give,
|
||||
},
|
||||
quantity: 1,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
// If has options, navigate to product page
|
||||
navigate(`/${subdomain}/product/${item.id}`);
|
||||
}
|
||||
};
|
||||
// if (basicCartItem && basicCartItem.uniqueId) {
|
||||
// // Increase quantity of existing basic item
|
||||
// dispatch(
|
||||
// updateQuantity({
|
||||
// id: basicCartItem.id,
|
||||
// uniqueId: basicCartItem.uniqueId,
|
||||
// quantity: basicCartItem.quantity + 1,
|
||||
// }),
|
||||
// );
|
||||
// } else if (!hasOptions) {
|
||||
// // Add new basic item if no options
|
||||
// dispatch(
|
||||
// addItem({
|
||||
// item: {
|
||||
// id: Number(item.id),
|
||||
// name: item.name,
|
||||
// price: item.price,
|
||||
// image: item.image,
|
||||
// description: item.description,
|
||||
// variant: "None",
|
||||
// isHasLoyalty: item.isHasLoyalty,
|
||||
// no_of_stamps_give: item.no_of_stamps_give,
|
||||
// },
|
||||
// quantity: 1,
|
||||
// }),
|
||||
// );
|
||||
// } else {
|
||||
// // If has options, navigate to product page
|
||||
// navigate(`/${subdomain}/product/${item.id}`);
|
||||
// }
|
||||
// };
|
||||
|
||||
return isInCart ? (
|
||||
<>
|
||||
return (
|
||||
<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}
|
||||
style={{
|
||||
width: 107,
|
||||
@@ -238,46 +275,5 @@ export function AddToCartButton({ item }: { item: Product }) {
|
||||
/>
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
*/
|
||||
}
|
||||
|
||||
@@ -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 { CancelOrderBottomSheet } from "components/CustomBottomSheet/CancelOrderBottomSheet";
|
||||
import LocationIcon from "components/Icons/LocationIcon";
|
||||
@@ -11,7 +11,7 @@ import ProInputCard from "components/ProInputCard/ProInputCard";
|
||||
import ProText from "components/ProText";
|
||||
import ProTitle from "components/ProTitle";
|
||||
import dayjs from "dayjs";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import {
|
||||
@@ -74,6 +74,57 @@ export default function OrderPage() {
|
||||
}
|
||||
}, [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 (
|
||||
<>
|
||||
<ProHeader>{t("order.title")}</ProHeader>
|
||||
@@ -126,7 +177,22 @@ export default function OrderPage() {
|
||||
</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} />
|
||||
)}
|
||||
|
||||
<div>
|
||||
<ProTitle
|
||||
|
||||
@@ -163,7 +163,7 @@ export function CustomAmountChoiceBottomSheet({
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: 12,
|
||||
marginTop: 20,
|
||||
margin: 20,
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
|
||||
@@ -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 { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -7,6 +7,7 @@ import ArabicPrice from "components/ArabicPrice";
|
||||
import ProText from "components/ProText";
|
||||
import { selectCart, updateSplitBillAmount } from "features/order/orderSlice";
|
||||
import { useAppDispatch, useAppSelector } from "redux/hooks";
|
||||
import styles from "./SplitBill.module.css";
|
||||
|
||||
interface SplitBillChoiceBottomSheetProps {
|
||||
isOpen: boolean;
|
||||
@@ -72,13 +73,13 @@ export function PayForYourItemsChoiceBottomSheet({
|
||||
title={t("splitBill.payForYourItems")}
|
||||
showCloseButton={true}
|
||||
initialSnap={1}
|
||||
height={745}
|
||||
snapPoints={[745]}
|
||||
height={720}
|
||||
snapPoints={[720]}
|
||||
contentStyle={{ padding: 0 }}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
padding: "0 20px",
|
||||
margin: "20px 0",
|
||||
padding: 20,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 12,
|
||||
@@ -101,11 +102,13 @@ export function PayForYourItemsChoiceBottomSheet({
|
||||
const itemTotal = item.price * item.quantity;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card
|
||||
key={itemId}
|
||||
style={{
|
||||
border: "none",
|
||||
cursor: "pointer",
|
||||
padding: 0,
|
||||
}}
|
||||
onClick={() => handleItemToggle(itemId)}
|
||||
>
|
||||
@@ -139,14 +142,9 @@ export function PayForYourItemsChoiceBottomSheet({
|
||||
<ProText style={{ fontSize: 14, fontWeight: 500 }}>
|
||||
{item.name}
|
||||
</ProText>
|
||||
<ProText type="secondary" style={{ fontSize: 12 }}>
|
||||
{t("cart.quantity")}: {item.quantity}
|
||||
</ProText>
|
||||
<ArabicPrice price={itemTotal} />
|
||||
</div>
|
||||
<Button
|
||||
type={isSelected ? "primary" : "default"}
|
||||
shape="circle"
|
||||
<div
|
||||
style={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
@@ -155,11 +153,20 @@ export function PayForYourItemsChoiceBottomSheet({
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{isSelected ? "✓" : "+"}
|
||||
</Button>
|
||||
<Checkbox
|
||||
className={styles.circleCheckbox}
|
||||
checked={isSelected}
|
||||
onChange={() => handleItemToggle(itemId)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
{item !== items[items.length - 1] && (
|
||||
<Divider style={{ margin: "0" }} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
})
|
||||
)}
|
||||
@@ -189,7 +196,7 @@ export function PayForYourItemsChoiceBottomSheet({
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: 12,
|
||||
marginTop: 20,
|
||||
margin: 20,
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
height: 48px !important;
|
||||
}
|
||||
|
||||
|
||||
.backToHomePage {
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
@@ -53,3 +52,38 @@
|
||||
align-items: center;
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user