733 lines
23 KiB
TypeScript
733 lines
23 KiB
TypeScript
import {
|
|
Button,
|
|
Card,
|
|
Divider,
|
|
Flex,
|
|
Image,
|
|
Layout,
|
|
Progress,
|
|
Tooltip,
|
|
} from "antd";
|
|
import { CancelOrderBottomSheet } from "components/CustomBottomSheet/CancelOrderBottomSheet";
|
|
import LocationIcon from "components/Icons/LocationIcon";
|
|
import InvoiceIcon from "components/Icons/order/InvoiceIcon";
|
|
import TimeIcon from "components/Icons/order/TimeIcon";
|
|
import OrderDishIcon from "components/Icons/OrderDishIcon";
|
|
import PaymentDetails from "components/PaymentDetails/PaymentDetails";
|
|
import ProHeader from "components/ProHeader/ProHeader";
|
|
import ProText from "components/ProText";
|
|
import ProTitle from "components/ProTitle";
|
|
import dayjs from "dayjs";
|
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { useNavigate, useParams } from "react-router-dom";
|
|
import {
|
|
useGetOrderDetailsQuery,
|
|
useGetRestaurantDetailsQuery,
|
|
} from "redux/api/others";
|
|
import { useAppSelector } from "redux/hooks";
|
|
import Stepper from "./components/Stepper";
|
|
import styles from "./order.module.css";
|
|
import BackIcon from "components/Icons/BackIcon";
|
|
import NextIcon from "components/Icons/NextIcon";
|
|
import { RateBottomSheet } from "components/CustomBottomSheet/RateBottomSheet";
|
|
import BriefMenuCard from "pages/checkout/components/BriefMenuCard";
|
|
import { QRBottomSheet } from "pages/pay/components/splitBill/QRBottomSheet";
|
|
import NewRateIcon from "components/Icons/order/NewRateIcon";
|
|
import NoteIcon from "components/Icons/NoteIcon";
|
|
import SuccessIcon from "components/Icons/SuccessIcon";
|
|
import { SplitBillParticipantsBottomSheet } from "./components/SplitBillParticipantsBottomSheet";
|
|
import { OrderType } from "pages/checkout/hooks/types";
|
|
|
|
export default function OrderPage() {
|
|
const { t } = useTranslation();
|
|
const { orderId } = useParams();
|
|
const navigate = useNavigate();
|
|
const { isRTL } = useAppSelector((state) => state.locale);
|
|
const { restaurant, orderType } = useAppSelector((state) => state.order);
|
|
const hasRefetchedRef = useRef(false);
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
const [isRateOrderOpen, setIsRateOrderOpen] = useState(false);
|
|
|
|
// Track order details in state to trigger re-evaluation of polling
|
|
const [lastOrderDetails, setLastOrderDetails] = useState<any>(null);
|
|
|
|
// Compute polling interval based on order status
|
|
const pollingInterval = useMemo(() => {
|
|
const orderDetailsToCheck = lastOrderDetails;
|
|
if (orderDetailsToCheck?.status) {
|
|
const hasClosedOrCanceled = orderDetailsToCheck.status.some(
|
|
(status: any) =>
|
|
status?.alias === "closed" ||
|
|
status?.alias === "canceled_by_customer",
|
|
);
|
|
if (hasClosedOrCanceled) {
|
|
return 0; // Stop polling
|
|
}
|
|
}
|
|
return 10000; // Continue polling
|
|
}, [lastOrderDetails]);
|
|
|
|
const { data: orderDetails } = useGetOrderDetailsQuery(
|
|
{
|
|
orderID: orderId || "",
|
|
restaurantID: localStorage.getItem("restaurantID") || "",
|
|
},
|
|
{
|
|
skip: !orderId,
|
|
// return it t0 60000 after finish testing
|
|
// Stop polling if order is closed or canceled
|
|
pollingInterval,
|
|
refetchOnMountOrArgChange: true,
|
|
},
|
|
);
|
|
|
|
// Update state when orderDetails changes to trigger polling re-evaluation
|
|
useEffect(() => {
|
|
if (orderDetails) {
|
|
setLastOrderDetails(orderDetails);
|
|
}
|
|
}, [orderDetails]);
|
|
|
|
const hasClosedStatus = orderDetails?.status?.some(
|
|
(status) => status?.alias === "closed",
|
|
);
|
|
|
|
const hasCanceledByCustomerStatus = orderDetails?.status?.some(
|
|
(status) => status?.alias === "canceled_by_customer",
|
|
);
|
|
|
|
const [
|
|
isSplitBillParticipantsBottomSheetOpen,
|
|
setIsSplitBillParticipantsBottomSheetOpen,
|
|
] = useState(false);
|
|
|
|
// Get restaurant subdomain for refetching
|
|
const restaurantSubdomain = restaurant?.subdomain;
|
|
const { refetch: refetchRestaurantDetails } = useGetRestaurantDetailsQuery(
|
|
restaurantSubdomain || "",
|
|
{
|
|
skip: !restaurantSubdomain,
|
|
},
|
|
);
|
|
|
|
// Reset refetch flag when orderId changes
|
|
useEffect(() => {
|
|
hasRefetchedRef.current = false;
|
|
}, [orderId]);
|
|
|
|
// Refetch restaurant details when order status has alias "closed"
|
|
useEffect(() => {
|
|
if (orderDetails?.status && !hasRefetchedRef.current) {
|
|
if (
|
|
(hasClosedStatus || hasCanceledByCustomerStatus) &&
|
|
restaurantSubdomain
|
|
) {
|
|
refetchRestaurantDetails();
|
|
hasRefetchedRef.current = true;
|
|
}
|
|
}
|
|
}, [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);
|
|
// Calculate progress as remaining time percentage (starts at 100%, decreases to 0%)
|
|
const percent =
|
|
totalSeconds > 0
|
|
? Math.min(100, Math.max(0, (remaining / totalSeconds) * 100))
|
|
: 100;
|
|
setProgressPercent(percent);
|
|
};
|
|
|
|
updateTimer();
|
|
const interval = setInterval(updateTimer, 1000);
|
|
|
|
return () => clearInterval(interval);
|
|
}, [
|
|
isInProgress,
|
|
orderDetails?.order?.time_to_prepare,
|
|
orderDetails?.status,
|
|
]);
|
|
|
|
// Format remaining time with Min and Sec labels
|
|
const formatTimer = (totalSeconds: number) => {
|
|
const mins = Math.floor(totalSeconds / 60);
|
|
const secs = Math.floor(totalSeconds % 60);
|
|
return (
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
gap: 8,
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
alignItems: "center",
|
|
gap: 2,
|
|
}}
|
|
>
|
|
<ProText
|
|
style={{
|
|
fontWeight: 600,
|
|
fontStyle: "SemiBold",
|
|
fontSize: 28,
|
|
lineHeight: "100%",
|
|
letterSpacing: "0%",
|
|
color: "#CC9300",
|
|
}}
|
|
>
|
|
{mins.toString().padStart(2, "0")}
|
|
</ProText>
|
|
<ProText
|
|
style={{
|
|
fontWeight: 400,
|
|
fontStyle: "Regular",
|
|
fontSize: 12,
|
|
lineHeight: "140%",
|
|
letterSpacing: "0%",
|
|
color: "#CC9300",
|
|
}}
|
|
>
|
|
{t("order.min")}
|
|
</ProText>
|
|
</div>
|
|
<ProText
|
|
style={{
|
|
fontWeight: 600,
|
|
fontStyle: "SemiBold",
|
|
fontSize: 28,
|
|
lineHeight: "100%",
|
|
letterSpacing: "0%",
|
|
color: "#CC9300",
|
|
}}
|
|
>
|
|
:
|
|
</ProText>
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
alignItems: "center",
|
|
gap: 2,
|
|
}}
|
|
>
|
|
<ProText
|
|
style={{
|
|
fontWeight: 600,
|
|
fontStyle: "SemiBold",
|
|
fontSize: 28,
|
|
lineHeight: "100%",
|
|
letterSpacing: "0%",
|
|
color: "#CC9300",
|
|
}}
|
|
>
|
|
{secs.toString().padStart(2, "0")}
|
|
</ProText>
|
|
<ProText
|
|
style={{
|
|
fontWeight: 400,
|
|
fontStyle: "Regular",
|
|
fontSize: 12,
|
|
lineHeight: "140%",
|
|
letterSpacing: "0%",
|
|
color: "#CC9300",
|
|
}}
|
|
>
|
|
{t("order.sec")}
|
|
</ProText>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<Layout>
|
|
<ProHeader customRoute={`/${restaurant?.subdomain}`}>
|
|
{t("order.title")}
|
|
</ProHeader>
|
|
<Layout.Content className={styles.orderContent}>
|
|
<Card className={styles.orderCard}>
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
flexDirection: "row",
|
|
gap: "1rem",
|
|
backgroundColor: "#f5f5f6",
|
|
borderRadius: "12px",
|
|
padding: 16,
|
|
}}
|
|
>
|
|
<Button type="text" shape="circle">
|
|
<Image
|
|
src={orderDetails?.restaurant_iimage}
|
|
className={styles.profileImage}
|
|
width={40}
|
|
height={40}
|
|
preview={false}
|
|
/>
|
|
</Button>
|
|
<div>
|
|
<ProText
|
|
style={{
|
|
fontFamily: "Bold/Caption Bold",
|
|
fontSize: "12px",
|
|
lineHeight: "140%",
|
|
letterSpacing: "0%",
|
|
fontWeight: "bold",
|
|
color: "#434E5C",
|
|
}}
|
|
>
|
|
{t("order.yourOrderFromFascanoRestaurant")}
|
|
</ProText>
|
|
<br />
|
|
<ProText type="secondary">
|
|
<LocationIcon className={styles.locationIcon} />{" "}
|
|
{isRTL
|
|
? orderDetails?.restaurantAR
|
|
: orderDetails?.restaurant}
|
|
</ProText>
|
|
</div>
|
|
</div>
|
|
|
|
{isInProgress ? (
|
|
<Flex gap="small" wrap justify="center" style={{ marginTop: 16 }}>
|
|
<Tooltip title="3 done / 3 in progress / 4 to do">
|
|
<div
|
|
style={{
|
|
transform: "scaleX(-1)",
|
|
}}
|
|
>
|
|
<Progress
|
|
percent={progressPercent}
|
|
format={() => (
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
gap: 4,
|
|
transform: "scaleX(-1)",
|
|
}}
|
|
>
|
|
<ProText
|
|
style={{
|
|
fontWeight: 400,
|
|
fontStyle: "Regular",
|
|
fontSize: 18,
|
|
lineHeight: "140%",
|
|
letterSpacing: "0%",
|
|
}}
|
|
>
|
|
{t("order.remainingTime")}
|
|
</ProText>
|
|
{formatTimer(remainingSeconds)}
|
|
</div>
|
|
)}
|
|
strokeColor="#FFB700"
|
|
size={200}
|
|
type="dashboard"
|
|
/>
|
|
</div>
|
|
</Tooltip>
|
|
</Flex>
|
|
) : (
|
|
<div style={{ margin: "56px 0" }}>
|
|
<OrderDishIcon className={styles.orderDishIcon} />
|
|
</div>
|
|
)}
|
|
|
|
<div>
|
|
<ProTitle
|
|
level={5}
|
|
style={{
|
|
fontWeight: 500,
|
|
fontStyle: "Medium",
|
|
fontSize: 18,
|
|
lineHeight: "140%",
|
|
letterSpacing: "0%",
|
|
marginBottom: "0.75rem",
|
|
color: "#333333",
|
|
}}
|
|
>
|
|
{t("order.inProgressOrder")} (1)
|
|
</ProTitle>
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
flexDirection: "row",
|
|
gap: 8,
|
|
}}
|
|
>
|
|
<InvoiceIcon className={styles.invoiceIcon} />
|
|
<ProText
|
|
type="secondary"
|
|
style={{
|
|
fontWeight: 400,
|
|
fontStyle: "Regular",
|
|
fontSize: 14,
|
|
lineHeight: "140%",
|
|
letterSpacing: "0%",
|
|
textAlign: "center",
|
|
color: "#333333",
|
|
}}
|
|
>
|
|
#{orderDetails?.order.id}
|
|
</ProText>
|
|
<TimeIcon className={styles.timeIcon} />
|
|
<ProText
|
|
type="secondary"
|
|
style={{
|
|
fontWeight: 400,
|
|
fontStyle: "Regular",
|
|
fontSize: 14,
|
|
lineHeight: "140%",
|
|
letterSpacing: "0%",
|
|
textAlign: "center",
|
|
color: "#333333",
|
|
}}
|
|
>
|
|
ordered :- Today -{" "}
|
|
{dayjs(orderDetails?.status[0]?.pivot?.created_at).format(
|
|
"h:mm A",
|
|
)}
|
|
</ProText>
|
|
</div>
|
|
|
|
<Divider style={{ margin: "12px 0" }} />
|
|
|
|
<Stepper statuses={orderDetails?.status} />
|
|
</div>
|
|
|
|
{!hasClosedStatus && orderType === OrderType.DineIn && (
|
|
<div className={styles.orderNotes}>
|
|
<NoteIcon className={styles.noteIcon} />
|
|
<div
|
|
style={{ display: "flex", flexDirection: "column", gap: 4 }}
|
|
>
|
|
<ProText
|
|
style={{
|
|
fontWeight: 400,
|
|
fontStyle: "Regular",
|
|
fontSize: 12,
|
|
lineHeight: "140%",
|
|
letterSpacing: "0%",
|
|
color: "#777580",
|
|
}}
|
|
>
|
|
{t(
|
|
"order.aStaffMemberWillCollectTheCashFromYouAtYourTable",
|
|
)}
|
|
</ProText>
|
|
<ProText
|
|
style={{
|
|
fontWeight: 600,
|
|
fontStyle: "SemiBold",
|
|
fontSize: 12,
|
|
lineHeight: "140%",
|
|
letterSpacing: "0%",
|
|
color: "#3D3B4A",
|
|
}}
|
|
>
|
|
{t("order.callWaiter")}
|
|
</ProText>
|
|
</div>
|
|
</div>
|
|
)}
|
|
{hasClosedStatus && orderType === OrderType.DineIn && (
|
|
<div className={styles.orderNotesClosed}>
|
|
<SuccessIcon className={styles.noteIcon} />
|
|
<ProText
|
|
style={{
|
|
fontSize: "12px",
|
|
lineHeight: "140%",
|
|
letterSpacing: "0%",
|
|
color: "#3D3B4A",
|
|
}}
|
|
>
|
|
{t("order.cashPaymentConfirmed")}
|
|
</ProText>
|
|
</div>
|
|
)}
|
|
</Card>
|
|
|
|
{/* <Ads2 /> */}
|
|
|
|
{/* <ProInputCard
|
|
title={
|
|
<div style={{ marginBottom: 7 }}>
|
|
<ProText style={{ fontSize: "1rem" }}>
|
|
{t("order.yourOrderFrom")}
|
|
</ProText>
|
|
<br />
|
|
<ProText type="secondary">
|
|
<LocationIcon className={styles.locationIcon} />{" "}
|
|
{t("order.muscat")}
|
|
</ProText>
|
|
</div>
|
|
}
|
|
>
|
|
<div
|
|
style={{ display: "flex", flexDirection: "column", gap: "1rem" }}
|
|
>
|
|
{orderDetails?.orderItems.map((item, index) => (
|
|
<div key={item.id}>
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
flexDirection: "row",
|
|
justifyContent: "flex-start",
|
|
gap: "1rem",
|
|
}}
|
|
>
|
|
<Button
|
|
type="text"
|
|
shape="circle"
|
|
style={{
|
|
backgroundColor: "rgba(255, 183, 0, 0.08)",
|
|
}}
|
|
>
|
|
{index + 1}X
|
|
</Button>
|
|
<div>
|
|
<ProText
|
|
style={{ fontSize: "1rem", position: "relative", top: 8 }}
|
|
>
|
|
{item.name}
|
|
</ProText>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</ProInputCard> */}
|
|
|
|
<BriefMenuCard />
|
|
|
|
<PaymentDetails order={orderDetails?.order} />
|
|
|
|
{/* inviteToBill */}
|
|
{/* {!hasClosedStatus && (
|
|
<ProInputCard
|
|
title={
|
|
<>
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
marginBottom: 9,
|
|
gap: 2,
|
|
}}
|
|
>
|
|
<ProText
|
|
style={{
|
|
fontSize: "18px",
|
|
fontWeight: 500,
|
|
fontStyle: "Medium",
|
|
lineHeight: "140%",
|
|
letterSpacing: "0%",
|
|
color: "#333333",
|
|
}}
|
|
>
|
|
{t("order.whosPaidTheirShareSplitBill")}
|
|
</ProText>
|
|
<ProText
|
|
style={{
|
|
fontWeight: 400,
|
|
fontStyle: "Regular",
|
|
fontSize: 12,
|
|
lineHeight: "140%",
|
|
letterSpacing: "0%",
|
|
color: "#777580",
|
|
}}
|
|
>
|
|
{t("order.seeWhoPaidAndHowMuch")}
|
|
</ProText>
|
|
</div>
|
|
</>
|
|
}
|
|
>
|
|
<div
|
|
className={styles.inviteToBill}
|
|
onClick={() => setIsSplitBillParticipantsBottomSheetOpen(true)}
|
|
>
|
|
<Button
|
|
shape="circle"
|
|
iconPlacement="start"
|
|
size="small"
|
|
className={styles.addButton}
|
|
style={{
|
|
background: "#F9F9FA",
|
|
width: 28,
|
|
height: 28,
|
|
border: "none",
|
|
color: "#FFB700",
|
|
}}
|
|
>
|
|
<ProText style={{ color: "#1F1C2E" }}> 1 </ProText>
|
|
</Button>
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
gap: 4,
|
|
width: "100%",
|
|
}}
|
|
>
|
|
<ProText
|
|
style={{
|
|
fontWeight: 500,
|
|
fontStyle: "Medium",
|
|
fontSize: 14,
|
|
lineHeight: "140%",
|
|
letterSpacing: "0%",
|
|
color: "#777580",
|
|
}}
|
|
>
|
|
{t("order.personHasPaid")}
|
|
</ProText>
|
|
</div>
|
|
{isRTL ? (
|
|
<BackIcon iconSize={24} />
|
|
) : (
|
|
<NextIcon iconSize={24} />
|
|
)}
|
|
</div>
|
|
<Button
|
|
type="text"
|
|
style={{
|
|
height: 48,
|
|
width: "100%",
|
|
display: "flex",
|
|
flexDirection: "row",
|
|
justifyContent: "center",
|
|
alignItems: "center",
|
|
gap: 8,
|
|
border: "1px solid #C0BFC4",
|
|
borderRadius: 888,
|
|
marginTop: 16,
|
|
}}
|
|
onClick={() => setIsOpen(true)}
|
|
>
|
|
<ProText
|
|
style={{
|
|
fontWeight: 400,
|
|
fontStyle: "Regular",
|
|
fontSize: 16,
|
|
lineHeight: "140%",
|
|
letterSpacing: "0%",
|
|
color: "#333333",
|
|
}}
|
|
>
|
|
{t("order.inviteOthersToBill")}
|
|
</ProText>
|
|
</Button>
|
|
</ProInputCard>
|
|
)} */}
|
|
|
|
<QRBottomSheet isOpen={isOpen} onClose={() => setIsOpen(false)} />
|
|
|
|
{hasClosedStatus && (
|
|
<Card
|
|
className={styles.backToHomePage}
|
|
onClick={() => setIsRateOrderOpen(true)}
|
|
>
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
flexDirection: "row",
|
|
justifyContent: "space-between",
|
|
}}
|
|
>
|
|
<div>
|
|
<NewRateIcon className={styles.newRateIcon} />
|
|
<ProText
|
|
style={{
|
|
fontWeight: 500,
|
|
fontStyle: "Medium",
|
|
fontSize: 14,
|
|
lineHeight: "140%",
|
|
letterSpacing: "0%",
|
|
color: "#595764",
|
|
position: "relative",
|
|
margin: "0 8px",
|
|
top: -12,
|
|
}}
|
|
>
|
|
{t("order.rateOrder")}
|
|
</ProText>
|
|
</div>
|
|
|
|
{isRTL ? (
|
|
<BackIcon className={styles.serviceIcon} />
|
|
) : (
|
|
<NextIcon className={styles.serviceIcon} />
|
|
)}
|
|
</div>
|
|
</Card>
|
|
)}
|
|
|
|
<RateBottomSheet
|
|
isOpen={isRateOrderOpen}
|
|
onClose={() => setIsRateOrderOpen(false)}
|
|
/>
|
|
|
|
{!hasClosedStatus && !hasCanceledByCustomerStatus && (
|
|
<CancelOrderBottomSheet />
|
|
)}
|
|
</Layout.Content>
|
|
|
|
{(hasClosedStatus || hasCanceledByCustomerStatus) && (
|
|
<Layout.Footer className={styles.checkoutButtonContainer}>
|
|
<Button
|
|
type="primary"
|
|
shape="round"
|
|
className={styles.checkoutButton}
|
|
onClick={() => {
|
|
navigate(`/${restaurant?.subdomain}/menu`);
|
|
}}
|
|
>
|
|
{t("order.newOrder")}
|
|
</Button>
|
|
</Layout.Footer>
|
|
)}
|
|
</Layout>
|
|
<SplitBillParticipantsBottomSheet
|
|
isOpen={isSplitBillParticipantsBottomSheetOpen}
|
|
onClose={() => setIsSplitBillParticipantsBottomSheetOpen(false)}
|
|
/>
|
|
</>
|
|
);
|
|
}
|