Compare commits

..

6 Commits

28 changed files with 187 additions and 341 deletions

BIN
public/thawani.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -178,7 +178,9 @@
"wednesday": "الأربعاء", "wednesday": "الأربعاء",
"thursday": "الخميس", "thursday": "الخميس",
"friday": "الجمعة", "friday": "الجمعة",
"saturday": "السبت" "saturday": "السبت",
"selectYourTable": "اختر طاولتك",
"calledWaiterSuccess": "تم اتصال الرادير بنجاح"
}, },
"cart": { "cart": {
"addSpecialRequestOptional": "إضافة طلب خاص (اختياري)", "addSpecialRequestOptional": "إضافة طلب خاص (اختياري)",
@@ -287,8 +289,8 @@
"cannotSelectPastDate": "لا يمكنك اختيار تاريخ سابق. يرجى اختيار اليوم أو تاريخ مستقبلي.", "cannotSelectPastDate": "لا يمكنك اختيار تاريخ سابق. يرجى اختيار اليوم أو تاريخ مستقبلي.",
"checkRequiredFields": "يرجى التحقق من الحقول المطلوبة", "checkRequiredFields": "يرجى التحقق من الحقول المطلوبة",
"loyalty": "ولاء", "loyalty": "ولاء",
"vatTax": "ضريبة (القيمة المضافة)", "vat": "ضريبة القيمة المضافة ({{value}}%))",
"otherTaxes": "ضريبة (أخرى)" "otherTaxes": "{{name}} ({{value}}%)"
}, },
"checkout": { "checkout": {
"addCarDetails": "إضافة تفاصيل السيارة", "addCarDetails": "إضافة تفاصيل السيارة",
@@ -333,7 +335,9 @@
"change": "تغيير", "change": "تغيير",
"pickup": "استلام", "pickup": "استلام",
"setPickupTime": "تحديد وقت الاستلام", "setPickupTime": "تحديد وقت الاستلام",
"carPlateNumber": "رقم لوحة السيارة" "carPlateNumber": "رقم لوحة السيارة",
"noItems": "لا يوجد عناصر في السلة",
"thawani": "ثواني"
}, },
"address": { "address": {
"title": "العنوان", "title": "العنوان",

View File

@@ -190,7 +190,9 @@
"wednesday": "Wednesday", "wednesday": "Wednesday",
"thursday": "Thursday", "thursday": "Thursday",
"friday": "Friday", "friday": "Friday",
"saturday": "Saturday" "saturday": "Saturday",
"selectYourTable": "Select your table",
"calledWaiterSuccess": "Waiter has been called successfully"
}, },
"cart": { "cart": {
"remainingToPay": "Remaining to Pay", "remainingToPay": "Remaining to Pay",
@@ -304,8 +306,8 @@
"checkRequiredFields": "Please check required fields", "checkRequiredFields": "Please check required fields",
"applyYourAvailableRewardsToGetDiscountsOnItemsInYourCart": "Apply your available rewards to get discounts on items in your cart", "applyYourAvailableRewardsToGetDiscountsOnItemsInYourCart": "Apply your available rewards to get discounts on items in your cart",
"loyalty": "Loyalty", "loyalty": "Loyalty",
"vatTax": "Tax (Vat)", "vat": "Vat ({{value}}%)",
"otherTaxes": "Tax (Other)" "otherTaxes": "{{name}} ({{value}}%)"
}, },
"checkout": { "checkout": {
"addCarDetails": "Add Car Details", "addCarDetails": "Add Car Details",
@@ -352,7 +354,9 @@
"change": "Change", "change": "Change",
"pickup": "Pickup", "pickup": "Pickup",
"setPickupTime": "Set Pickup Time", "setPickupTime": "Set Pickup Time",
"carPlateNumber": "Car Plate Number" "carPlateNumber": "Car Plate Number",
"noItems": "No items in cart",
"thawani": "Thawani"
}, },
"address": { "address": {
"title": "Address", "title": "Address",

View File

@@ -27,7 +27,7 @@ const ArabicPrice: React.FC<ArabicPriceProps> = ({
// Format the price to ensure it has 2 decimal places // Format the price to ensure it has 2 decimal places
const formattedPrice = const formattedPrice =
typeof price === "number" ? formatPriceUi(price, 3) : price; typeof price === "number" ? formatPriceUi(price, restaurant?.currency_decimals ?? 3) : price;
const { textDecoration, ...restStyle } = style; const { textDecoration, ...restStyle } = style;
const decorationStyle = textDecoration const decorationStyle = textDecoration
? ({ textDecoration } as React.CSSProperties) ? ({ textDecoration } as React.CSSProperties)

View File

@@ -18,9 +18,9 @@ const RefershIcon = ({ className, onClick, dimension }: RefershIconType) => {
<path <path
d="M1.5 7.5C1.5 7.5 1.59099 6.86307 4.22703 4.22703C6.86307 1.59099 11.1369 1.59099 13.773 4.22703C14.7069 5.16099 15.31 6.30054 15.5821 7.5M1.5 7.5V3M1.5 7.5H6M16.5 10.5C16.5 10.5 16.409 11.1369 13.773 13.773C11.1369 16.409 6.86307 16.409 4.22703 13.773C3.29307 12.839 2.69002 11.6995 2.41787 10.5M16.5 10.5V15M16.5 10.5H12" d="M1.5 7.5C1.5 7.5 1.59099 6.86307 4.22703 4.22703C6.86307 1.59099 11.1369 1.59099 13.773 4.22703C14.7069 5.16099 15.31 6.30054 15.5821 7.5M1.5 7.5V3M1.5 7.5H6M16.5 10.5C16.5 10.5 16.409 11.1369 13.773 13.773C11.1369 16.409 6.86307 16.409 4.22703 13.773C3.29307 12.839 2.69002 11.6995 2.41787 10.5M16.5 10.5V15M16.5 10.5H12"
stroke="#5F6C7B" stroke="#5F6C7B"
stroke-width="1.5" strokeWidth="1.5"
stroke-linecap="round" strokeLinecap="round"
stroke-linejoin="round" strokeLinejoin="round"
/> />
</svg> </svg>
); );

View File

@@ -1,8 +1,6 @@
import { Card, Col, Image, Row } from "antd"; import { Card, Col, Image, Row } from "antd";
import PresentIcon from "components/Icons/cart/PresentIcon";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Link, useParams } from "react-router-dom"; import { Link, useParams } from "react-router-dom";
import { useGetRestaurantDetailsQuery } from "redux/api/others";
import { ACCESS_TOKEN } from "utils/constants"; import { ACCESS_TOKEN } from "utils/constants";
import { colors } from "ThemeConstants.ts"; import { colors } from "ThemeConstants.ts";
import ProText from "../ProText"; import ProText from "../ProText";
@@ -12,7 +10,7 @@ import { useAppSelector } from "redux/hooks";
const LoyaltyCard = () => { const LoyaltyCard = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { subdomain } = useParams(); const { subdomain } = useParams();
const { data: restaurant } = useGetRestaurantDetailsQuery(subdomain); const { restaurant } = useAppSelector((state) => state.order);
const { isRTL } = useAppSelector((state) => state.locale); const { isRTL } = useAppSelector((state) => state.locale);
const token = localStorage.getItem(ACCESS_TOKEN); const token = localStorage.getItem(ACCESS_TOKEN);
const loyaltyStamps = restaurant?.loyalty_stamps ?? 0; const loyaltyStamps = restaurant?.loyalty_stamps ?? 0;

View File

@@ -1,4 +1,4 @@
import { Card, Checkbox, Divider, Space, Tag } from "antd"; import { Card, Divider, Space, Tag } from "antd";
import ArabicPrice from "components/ArabicPrice"; import ArabicPrice from "components/ArabicPrice";
import { import {
selectCart, selectCart,
@@ -6,7 +6,7 @@ import {
selectDiscountTotal, selectDiscountTotal,
selectGrandTotal, selectGrandTotal,
selectHighestPricedLoyaltyItem, selectHighestPricedLoyaltyItem,
selectTaxAmount, totalTaxes,
} from "features/order/orderSlice"; } from "features/order/orderSlice";
import { OrderType } from "pages/checkout/hooks/types"; import { OrderType } from "pages/checkout/hooks/types";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -16,7 +16,7 @@ import { useAppSelector } from "redux/hooks";
import ProText from "../ProText"; import ProText from "../ProText";
import ProTitle from "../ProTitle"; import ProTitle from "../ProTitle";
import styles from "./OrderSummary.module.css"; import styles from "./OrderSummary.module.css";
import { CSSProperties } from "react"; import { CSSProperties, useMemo } from "react";
export default function OrderSummary() { export default function OrderSummary() {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -26,13 +26,13 @@ export default function OrderSummary() {
const { orderType } = useAppSelector(selectCart); const { orderType } = useAppSelector(selectCart);
const subtotal = useAppSelector(selectCartTotal); const subtotal = useAppSelector(selectCartTotal);
const highestLoyaltyItem = useAppSelector(selectHighestPricedLoyaltyItem); const highestLoyaltyItem = useAppSelector(selectHighestPricedLoyaltyItem);
const taxAmount = useAppSelector(selectTaxAmount); const totalTaxesAmount = useAppSelector(totalTaxes);
const grandTotal = useAppSelector(selectGrandTotal); const grandTotal = useAppSelector(selectGrandTotal);
const discountAmount = useAppSelector(selectDiscountTotal); const discountAmount = useAppSelector(selectDiscountTotal);
const isHasLoyaltyGift = const isHasLoyaltyGift =
(restaurant?.loyalty_stamps ?? 0) - (restaurant?.loyalty_stamps ?? 0) -
(restaurant?.customer_loyalty_points ?? 0) <= (restaurant?.customer_loyalty_points ?? 0) <=
0; 0;
const titlesStyle: CSSProperties = { const titlesStyle: CSSProperties = {
@@ -44,7 +44,27 @@ export default function OrderSummary() {
textAlign: "center", textAlign: "center",
}; };
const vat = ((restaurant?.vat ?? 0) / 100) * subtotal || 0; const vat = ((restaurant?.vat ?? 0) / 100) * (subtotal - discountAmount) || 0;
// Calculate individual taxes
const taxesList = useMemo(() => {
if (!restaurant?.taxes) return [];
const subtotalAfterDiscount = subtotal - discountAmount;
return restaurant.taxes
.filter((tax) => tax.is_active === 1)
.map((tax) => {
const amount = ((Number(tax.percentage) || 0) / 100) * subtotalAfterDiscount;
return {
id: tax.id,
name: tax.name,
name_local: tax.name_local,
percentage: tax.percentage,
amount,
};
});
}, [restaurant?.taxes, subtotal, discountAmount]);
return ( return (
<> <>
@@ -93,9 +113,9 @@ export default function OrderSummary() {
}} }}
> >
{isHasLoyaltyGift && {isHasLoyaltyGift &&
useLoyaltyPoints && useLoyaltyPoints &&
highestLoyaltyItem && highestLoyaltyItem &&
restaurant?.is_loyalty_enabled === 1 ? ( restaurant?.is_loyalty_enabled === 1 ? (
<Tag <Tag
color="green" color="green"
style={{ style={{
@@ -117,7 +137,7 @@ export default function OrderSummary() {
</Tag> </Tag>
) : null} ) : null}
<ArabicPrice <ArabicPrice
price={discountAmount} price={discountAmount + (useLoyaltyPoints && restaurant?.is_loyalty_enabled === 1 ? ((highestLoyaltyItem?.price || 0) * (totalTaxesAmount / 100)) : 0)}
textStyle={{ ...titlesStyle, color: "#434E5C" }} textStyle={{ ...titlesStyle, color: "#434E5C" }}
/> />
</div> </div>
@@ -156,10 +176,10 @@ export default function OrderSummary() {
/> />
</div> </div>
)} )}
{orderType !== OrderType.Redeem && ( {orderType !== OrderType.Redeem && restaurant?.vat && (
<div className={styles.summaryRow}> <div className={styles.summaryRow}>
<ProText type="secondary" style={titlesStyle}> <ProText type="secondary" style={titlesStyle}>
{t("cart.vatTax")} {t("cart.vat", { value: restaurant.vat })}
</ProText> </ProText>
<ArabicPrice <ArabicPrice
price={vat} price={vat}
@@ -167,17 +187,21 @@ export default function OrderSummary() {
/> />
</div> </div>
)} )}
{orderType !== OrderType.Redeem && ( {orderType !== OrderType.Redeem &&
<div className={styles.summaryRow}> taxesList.map((tax) => (
<ProText type="secondary" style={titlesStyle}> <div key={tax.id} className={styles.summaryRow}>
{t("cart.otherTaxes")} <ProText type="secondary" style={titlesStyle}>
</ProText> {t("cart.otherTaxes", {
<ArabicPrice name: tax.name || tax.name_local,
price={taxAmount - vat || 0} value: tax.percentage,
textStyle={{ ...titlesStyle, color: "#434E5C" }} })}
/> </ProText>
</div> <ArabicPrice
)} price={tax.amount}
textStyle={{ ...titlesStyle, color: "#434E5C" }}
/>
</div>
))}
{orderType !== OrderType.Redeem && splitBillAmount > 0 && ( {orderType !== OrderType.Redeem && splitBillAmount > 0 && (
<div className={styles.summaryRow}> <div className={styles.summaryRow}>
<ProText type="secondary" style={titlesStyle}> <ProText type="secondary" style={titlesStyle}>

View File

@@ -1,7 +1,7 @@
import { Form, Radio, Space } from "antd"; import { Form, Image, 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";
import ProText from "components/ProText"; import ProText from "components/ProText";
import { import {
selectCart, selectCart,
@@ -14,7 +14,6 @@ import { colors, ProGray1 } from "../../ThemeConstants";
import ProInputCard from "../ProInputCard/ProInputCard"; import ProInputCard from "../ProInputCard/ProInputCard";
import styles from "./PaymentMethods.module.css"; import styles from "./PaymentMethods.module.css";
import { OrderType } from "pages/checkout/hooks/types.ts"; import { OrderType } from "pages/checkout/hooks/types.ts";
import RCardIcon from "components/Icons/RCardIcon";
import { formatPriceUi } from "utils/helpers"; import { formatPriceUi } from "utils/helpers";
const PaymentMethods = () => { const PaymentMethods = () => {
@@ -22,7 +21,8 @@ const PaymentMethods = () => {
const { paymentMethod, orderType } = useAppSelector(selectCart); const { paymentMethod, orderType } = useAppSelector(selectCart);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const grandTotal = useAppSelector(selectGrandTotal); const grandTotal = useAppSelector(selectGrandTotal);
const { isRTL } = useAppSelector((state) => state.locale); const { restaurant } = useAppSelector((state) => state.order);
// const { isRTL } = useAppSelector((state) => state.locale);
const options: { const options: {
label: React.ReactNode; label: React.ReactNode;
@@ -40,16 +40,18 @@ const PaymentMethods = () => {
<ProText <ProText
style={{ style={{
color: "#E8B400", color: "#E8B400",
[isRTL ? "marginLeft" : "marginRight"]: 4, // [isRTL ? "marginLeft" : "marginRight"]: 4,
}} }}
> >
$ {/* $ */}
</ProText>
<ProText style={{ color: "#5F6C7B" }}>
{t("checkout.cash")}
</ProText> </ProText>
<ProText style={{color: '#5F6C7B'}}>{t("checkout.cash")}</ProText>
</> </>
), ),
value: "cash", value: "cash",
price: formatPriceUi(grandTotal, 3), price: formatPriceUi(grandTotal, restaurant.currency_decimals ?? 3),
style: { style: {
color: colors.primary, color: colors.primary,
}, },
@@ -65,14 +67,26 @@ const PaymentMethods = () => {
{ {
label: ( label: (
<> <>
<RCardIcon className={styles.eCardIcon} /> {/* <RCardIcon className={styles.eCardIcon} /> */}
<ProText style={{color: '#5F6C7B'}}>{t("checkout.differentCard")}</ProText> <ProText style={{ color: "#5F6C7B" }}>
{t("checkout.thawani")}
</ProText>
</> </>
), ),
value: "differentCard", value: "thawani",
icon: ( icon: (
<div className={styles.differentCardIcon}> <div className={styles.differentCardIcon}>
<DifferentCardIcon /> <Image
preview={false}
src={"thawani.png"}
alt="thawani"
width={24}
height={24}
style={{
position: "relative",
bottom: 3,
}}
/>
</div> </div>
), ),
hideCurrency: true, hideCurrency: true,

View File

@@ -1,47 +0,0 @@
import { Loader } from "components/Loader/Loader";
import { Navigate, useParams } from "react-router-dom";
import { useAppSelector } from "redux/hooks";
export const PrivateRoute = ({
children,
}: // permission,
{
children: JSX.Element;
permission?: string;
}) => {
const { token, loading } = useAppSelector((state) => state.auth);
const { subdomain } = useParams();
// const { data: user, isLoading: loadingUser } = useGetSignedUserInfoQuery();
// useEffect(() => {
// if (user) dispatch(setUser(user));
// }, [dispatch, user]);
if (loading) {
return <Loader />;
}
// let newPermissions: any[] = [];
// // aggregate the rules from multi
// user?.roles?.forEach(
// (r) => (newPermissions = [...newPermissions, ...r.permissions])
// );
// const userHasRequiredPermission = permission
// ? !!newPermissions.find((p) => p.name === permission)
// : true;
console.log(token);
if (!token) {
return <Navigate to={`/${subdomain}/login`} />;
}
// if (token && !userHasRequiredPermission) {
// return <AccessDenied />; // build your won access denied page (sth like 404)
// }
return children;
};

View File

@@ -380,7 +380,11 @@ const orderSlice = createSlice({
// Clear all cart data from localStorage // Clear all cart data from localStorage
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
Object.values(CART_STORAGE_KEYS) Object.values(CART_STORAGE_KEYS)
.filter((key) => key !== CART_STORAGE_KEYS.ORDER_TYPE) .filter(
(key) =>
key !== CART_STORAGE_KEYS.ORDER_TYPE &&
key !== CART_STORAGE_KEYS.PAYMENT_METHOD,
)
.forEach((key) => { .forEach((key) => {
localStorage.removeItem(key); localStorage.removeItem(key);
}); });
@@ -703,7 +707,7 @@ const orderSlice = createSlice({
}, },
updateOrder(state, action: PayloadAction<any>) { updateOrder(state, action: PayloadAction<any>) {
state.order = { ...(state.order || {}), ...action.payload }; state.order = { ...(state.order || {}), ...action.payload };
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
localStorage.setItem( localStorage.setItem(
CART_STORAGE_KEYS.ORDER, CART_STORAGE_KEYS.ORDER,
@@ -767,23 +771,21 @@ export const {
// Tax calculation helper functions // Tax calculation helper functions
const calculateTaxAmount = ( const calculateTaxAmount = (
state: RootState,
amount: number, amount: number,
tax: Tax, tax: Tax,
): number => { ): number => {
const percentage = parseFloat(tax.percentage); const percentage = parseFloat(tax.percentage);
return (((state.order.restaurant?.vat || 0) + percentage) * amount) / 100; return (percentage * amount) / 100;
}; };
const calculateTotalTax = ( const calculateTotalTax = (
state: RootState,
subtotal: number, subtotal: number,
taxes: Tax[], taxes: Tax[],
): number => { ): number => {
return taxes return taxes
.filter((tax) => tax.is_active === 1) .filter((tax) => tax.is_active === 1)
.reduce( .reduce(
(total, tax) => total + calculateTaxAmount(state, subtotal, tax), (total, tax) => total + calculateTaxAmount(subtotal, tax),
0, 0,
); );
}; };
@@ -822,21 +824,31 @@ export const selectLoyaltyItems = createSelector([selectOrderItems], (items) =>
items.filter((item) => item.isHasLoyalty), items.filter((item) => item.isHasLoyalty),
); );
// Tax selectors
export const selectTaxes = (state: RootState) => state.order.restaurant.taxes;
export const totalTaxes = (state: RootState) => {
const taxes = selectTaxes(state);
const totalTaxes = taxes?.reduce((total, tax) => total + parseFloat(tax.percentage), 0) || 0;
const vat = (state.order.restaurant?.vat || 0);
return totalTaxes + vat;
};
export const selectHighestPricedLoyaltyItem = (state: RootState) => { export const selectHighestPricedLoyaltyItem = (state: RootState) => {
const loyaltyItems = selectLoyaltyItems(state); const loyaltyItems = selectLoyaltyItems(state);
if (loyaltyItems.length === 0) return null; if (loyaltyItems.length === 0) return null;
const highestItem = loyaltyItems.reduce((highest, current) =>
return loyaltyItems.reduce((highest, current) =>
current.price > highest.price ? current : highest, current.price > highest.price ? current : highest,
); )
return highestItem;
}; };
export const selectDiscountTotal = (state: RootState) => export const selectDiscountTotal = (state: RootState) => {
(state.order.discount.value / 100) * selectCartTotal(state) + return (state.order.discount.value / 100) * selectCartTotal(state) +
(state.order.useLoyaltyPoints && (state.order.useLoyaltyPoints &&
state.order.restaurant?.is_loyalty_enabled === 1 state.order.restaurant?.is_loyalty_enabled === 1
? selectHighestPricedLoyaltyItem(state)?.price || 0 ? ((selectHighestPricedLoyaltyItem(state)?.price || 0))
: 0); : 0);
}
export const selectLoyaltyValidation = createSelector( export const selectLoyaltyValidation = createSelector(
[(state: RootState) => state.order.useLoyaltyPoints, selectLoyaltyItems], [(state: RootState) => state.order.useLoyaltyPoints, selectLoyaltyItems],
@@ -851,18 +863,18 @@ export const selectLoyaltyValidation = createSelector(
}), }),
); );
// Tax selectors
export const selectTaxes = (state: RootState) => state.order.restaurant.taxes;
export const selectTaxAmount = (state: RootState) => { export const selectTaxAmount = (state: RootState) => {
const subtotal = selectCartTotal(state) - selectDiscountTotal(state); const subtotal = selectCartTotal(state) - selectDiscountTotal(state);
const taxes = selectTaxes(state); const taxes = selectTaxes(state);
return calculateTotalTax(state, subtotal, taxes || []); return calculateTotalTax(subtotal, taxes || []);
}; };
export const selectGrandTotal = (state: RootState) => { export const selectGrandTotal = (state: RootState) => {
const totalDiscount = selectDiscountTotal(state); const totalDiscount = selectDiscountTotal(state);
const taxAmount = selectTaxAmount(state); const taxAmount = selectTaxAmount(state);
const vatAmount = ((state.order.restaurant?.vat || 0) / 100) * (selectCartTotal(state) - selectDiscountTotal(state));
const subtotal = selectCartTotal(state); const subtotal = selectCartTotal(state);
const deliveryFee = const deliveryFee =
state.order.orderType === OrderType.Delivery state.order.orderType === OrderType.Delivery
@@ -870,12 +882,13 @@ export const selectGrandTotal = (state: RootState) => {
: 0; : 0;
return ( return (
subtotal + subtotal -
taxAmount -
totalDiscount + totalDiscount +
deliveryFee - taxAmount +
state.order.splitBillAmount + vatAmount -
Number(state.order.tip) deliveryFee
// state.order.splitBillAmount +
// Number(state.order.tip)
); );
}; };

View File

@@ -1,39 +0,0 @@
import { useEffect, useState } from "react";
export function useDetectBarcode() {
const [barcode, setBarcode] = useState<string>("");
const [lastBarcode, setLastBarcode] = useState<string>("");
useEffect(() => {
let interval: any;
const handleKeydown = (evt: any) => {
if (interval) clearInterval(interval);
if (evt.code === "Enter") {
if (barcode) handleBarcode(barcode);
setBarcode("");
return;
}
if (evt.key !== "Shift") setBarcode((prev) => prev + evt.key);
interval = setInterval(() => setBarcode(""), 20);
};
// Adding the event listener when the component mounts
document.addEventListener("keydown", handleKeydown);
// Cleanup the event listener when the component unmounts
return () => {
document.removeEventListener("keydown", handleKeydown);
if (interval) clearInterval(interval);
};
}, [barcode]);
const handleBarcode = (scannedBarcode: string) => {
setLastBarcode(scannedBarcode);
};
return lastBarcode;
}

View File

@@ -1,52 +0,0 @@
import { useRef, useState, useCallback } from "react";
type Props = { isEnabled: boolean; swipeAction: () => void };
export default function useSwipeUp({ isEnabled, swipeAction }: Props) {
// Swipe detection
const startYRef = useRef(0);
const [isSwiping, setIsSwiping] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
// Touch event handlers for swipe detection
const handleTouchStart = useCallback(
(e: React.TouchEvent) => {
if (!isEnabled) return;
startYRef.current = e.touches[0].clientY;
setIsSwiping(true);
},
[isEnabled],
);
/*
const handleTouchMove = useCallback(
(e: React.TouchEvent) => {
if (!isSwiping) return;
},
[isSwiping],
);
*/
const handleTouchEnd = useCallback(
(e: React.TouchEvent) => {
if (!isSwiping || !isEnabled) return;
const endY = e.changedTouches[0].clientY;
const deltaY = startYRef.current - endY;
const threshold = 70; // Threshold to detect a swipe up
if (deltaY > threshold) {
// Swipe up detected
swipeAction();
}
setIsSwiping(false);
},
[isSwiping, isEnabled],
);
return {
containerRef,
handleTouchStart,
handleTouchEnd,
};
}

View File

@@ -1,7 +0,0 @@
import { useAppSelector } from "redux/hooks";
export function useTranslations() {
const { isRTL } = useAppSelector((state) => state.locale);
const nameProp = isRTL ? "arabic_name" : "name";
return [nameProp] as const ;
}

View File

@@ -1,13 +0,0 @@
import { Layout } from 'antd';
const { Footer } = Layout;
type FooterNavProps = React.HTMLAttributes<HTMLDivElement>;
const FooterNav = ({ ...others }: FooterNavProps) => {
return (
<Footer {...others}>AntD Dashboard © 2023 Created by Design Sparx</Footer>
);
};
export default FooterNav;

View File

@@ -1,70 +0,0 @@
import { BugFilled } from "@ant-design/icons";
import { MenuProps } from "antd";
import { PATHS } from "utils/constants";
// import WarehouseIcon from "components/Icons/WarehouseIcon";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
export default function useSidebarItems() {
type MenuItem = Required<MenuProps>["items"][number] & {
permission: string;
children?: MenuItem[];
};
const { t } = useTranslation();
// const [isAuth] = useAuth();
const getItem = (
label: React.ReactNode,
key: React.Key,
permission?: string,
icon?: React.ReactNode,
children?: MenuItem[],
type?: "group"
): MenuItem => {
return {
key,
icon,
children,
label,
type,
permission,
} as MenuItem;
};
// Recursive function to filter items based on permissions
const getGrantedItems = (items: any[]): MenuItem[] => {
return items
.filter(() => true)// Filter out items without permission
.map((item: any) => {
if (item.children) {
// Recursively filter children
return {
...item,
children: getGrantedItems(item.children),
};
}
return item;
});
};
const items: MenuProps["items"] = [
getItem(
t("menu"),
PATHS.menu,
undefined,
<div style={{ marginTop: 10 }}>
<Link style={{}} to={PATHS.menu}>
<BugFilled className="icon-container pos-icon" />
</Link>
</div>
),
];
// if we have a menu with empty children after applying "getGrantedItems"
// we going to remove it
const grantedItems = getGrantedItems(items).filter(
(i) => (i.children && i.children.length !== 0) || !i.children
);
return grantedItems;
}

View File

@@ -33,7 +33,6 @@ export default function CartMobileTabletLayout({
const { items, orderType } = useAppSelector(selectCart); const { items, orderType } = useAppSelector(selectCart);
const { isRTL } = useAppSelector((state) => state.locale); const { isRTL } = useAppSelector((state) => state.locale);
const { subdomain } = useParams(); const { subdomain } = useParams();
const { pickup_type } = useAppSelector((state) => state.order.restaurant);
const { isMobile, isTablet } = useBreakPoint(); const { isMobile, isTablet } = useBreakPoint();
const getResponsiveClass = () => (isTablet ? "tablet" : "mobile"); const getResponsiveClass = () => (isTablet ? "tablet" : "mobile");
const navigate = useNavigate(); const navigate = useNavigate();

View File

@@ -1,4 +1,4 @@
import { Button, FormInstance, Layout } from "antd"; import { Button, FormInstance, Layout, message } from "antd";
import { selectCart, updateSplitBillAmount } from "features/order/orderSlice"; import { selectCart, updateSplitBillAmount } from "features/order/orderSlice";
import { OrderType } from "pages/checkout/hooks/types.ts"; import { OrderType } from "pages/checkout/hooks/types.ts";
import { useCallback, useMemo, useState } from "react"; import { useCallback, useMemo, useState } from "react";
@@ -18,7 +18,7 @@ type SplitWay = "customAmount" | "equality" | "payForItems" | null;
export default function CheckoutButton({ form }: { form: FormInstance }) { export default function CheckoutButton({ form }: { form: FormInstance }) {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
const { orderType, giftDetails } = useAppSelector(selectCart); const { orderType, giftDetails, items } = useAppSelector(selectCart);
const { handleCreateOrder } = useOrder(); const { handleCreateOrder } = useOrder();
const { handleCreateGiftAmount } = useGidtAmount(); const { handleCreateGiftAmount } = useGidtAmount();
const [selectedSplitWay, setSelectedSplitWay] = useState<SplitWay>(null); const [selectedSplitWay, setSelectedSplitWay] = useState<SplitWay>(null);
@@ -56,7 +56,11 @@ export default function CheckoutButton({ form }: { form: FormInstance }) {
) { ) {
handleCreateGiftAmount(); handleCreateGiftAmount();
} else { } else {
handleCreateOrder(); if (items.length > 0) {
handleCreateOrder();
} else {
message.error(t("checkout.noItems"));
}
} }
} catch (error) { } catch (error) {
console.log(error); console.log(error);

View File

@@ -75,8 +75,6 @@ export default function useOrder() {
duration: 0, duration: 0,
}); });
console.log(order?.roomNumber);
createOrder({ createOrder({
phone: mobilenumber || phone || giftDetails?.senderPhone, phone: mobilenumber || phone || giftDetails?.senderPhone,
comment: specialRequest, comment: specialRequest,
@@ -152,8 +150,9 @@ export default function useOrder() {
); );
else { else {
const redirectMessageKey = "order-redirect-loader"; const redirectMessageKey = "order-redirect-loader";
if ( if (
orderType === OrderType.Gift && localStorage.getItem("fascano_payment_method")?.toString() === '"thawani"' &&
mutationResult.data?.result?.orderID mutationResult.data?.result?.orderID
) { ) {
message.loading({ message.loading({

View File

@@ -13,13 +13,14 @@ import "react-phone-input-2/lib/style.css";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import { useSendOtpMutation } from "redux/api/auth"; import { useSendOtpMutation } from "redux/api/auth";
import { useGetRestaurantDetailsQuery } from "redux/api/others"; import { useGetRestaurantDetailsQuery } from "redux/api/others";
import { useAppSelector } from "redux/hooks"; import { useAppDispatch, useAppSelector } from "redux/hooks";
import { colors, DisabledColor, ProGray1 } from "ThemeConstants"; import { colors, DisabledColor, ProGray1 } from "ThemeConstants";
import { default_image } from "utils/constants"; import { default_image } from "utils/constants";
import styles from "./login.module.css"; import styles from "./login.module.css";
import { Layout } from "antd"; import { Layout } from "antd";
import ProHeader from "components/ProHeader/ProHeader"; import ProHeader from "components/ProHeader/ProHeader";
import useBreakPoint from "hooks/useBreakPoint"; import useBreakPoint from "hooks/useBreakPoint";
import { updateCustomerName } from "features/order/orderSlice";
export default function LoginPage() { export default function LoginPage() {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -32,7 +33,7 @@ export default function LoginPage() {
skip: !subdomain, skip: !subdomain,
}); });
const { isTablet } = useBreakPoint(); const { isTablet } = useBreakPoint();
const dispatch = useAppDispatch();
// const [phone, setPhone] = useState<string>(""); // const [phone, setPhone] = useState<string>("");
const [selectedDate, setSelectedDate] = useState<string>(""); const [selectedDate, setSelectedDate] = useState<string>("");
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
@@ -113,6 +114,10 @@ export default function LoginPage() {
height: 50, height: 50,
fontSize: 14, fontSize: 14,
}} }}
onBlur={(e) => {
dispatch(updateCustomerName(e.target.value));
}}
disabled={isLoading}
/> />
</Form.Item> </Form.Item>

View File

@@ -1,5 +1,5 @@
import { StarFilled } from "@ant-design/icons"; import { StarFilled } from "@ant-design/icons";
import { Button, Image, Select, Space } from "antd"; import { Button, Image, message, Select, Space } from "antd";
import { FloatingButton } from "components/FloatingButton/FloatingButton"; import { FloatingButton } from "components/FloatingButton/FloatingButton";
import LogoContainerIcon from "components/Icons/LogoContainerIcon"; import LogoContainerIcon from "components/Icons/LogoContainerIcon";
import ImageWithFallback from "components/ImageWithFallback"; import ImageWithFallback from "components/ImageWithFallback";
@@ -12,6 +12,7 @@ import { OrderType } from "pages/checkout/hooks/types.ts";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { import {
useCallWaiterMutation,
useGetMenuQuery, useGetMenuQuery,
useGetRestaurantDetailsQuery, useGetRestaurantDetailsQuery,
} from "redux/api/others"; } from "redux/api/others";
@@ -37,7 +38,7 @@ import { OrderTypesBottomSheet } from "components/CustomBottomSheet/OrderTypesBo
function MenuPage() { function MenuPage() {
const { subdomain } = useParams(); const { subdomain } = useParams();
const { isRTL } = useAppSelector((state) => state.locale); const { isRTL } = useAppSelector((state) => state.locale);
const { orderType } = useAppSelector((state) => state.order); const { orderType, table } = useAppSelector((state) => state.order);
const { token } = useAppSelector((state) => state.auth); const { token } = useAppSelector((state) => state.auth);
const { t } = useTranslation(); const { t } = useTranslation();
const { data: restaurant, isLoading: isLoadingRestaurant } = const { data: restaurant, isLoading: isLoadingRestaurant } =
@@ -56,6 +57,7 @@ function MenuPage() {
const [isOpeningTimesOpen, setIsOpeningTimesOpen] = useState(false); const [isOpeningTimesOpen, setIsOpeningTimesOpen] = useState(false);
const [isOrderTypesOpen, setIsOrderTypesOpen] = useState(false); const [isOrderTypesOpen, setIsOrderTypesOpen] = useState(false);
const orderTypeOptions = enumToSelectOptions(OrderType, t, "orderTypes"); const orderTypeOptions = enumToSelectOptions(OrderType, t, "orderTypes");
const [callWaiter] = useCallWaiterMutation()
// Automatically load restaurant taxes when restaurant data is available // Automatically load restaurant taxes when restaurant data is available
useRestaurant(restaurant); useRestaurant(restaurant);
@@ -203,6 +205,19 @@ function MenuPage() {
backgroundColor: "#EBEBEC", backgroundColor: "#EBEBEC",
border: "none", border: "none",
}} }}
onClick={() => {
if (table) callWaiter({
table_id: table,
call_reason: "call_waiter",
}).unwrap().then(() => {
message.success(t("menu.calledWaiterSuccess"));
}).catch((err) => {
message.error(err.data.message);
})
else
message.info(t("menu.selectYourTable"));
}
}
> >
<ProText <ProText
style={{ style={{

View File

@@ -37,13 +37,14 @@ import NewRateIcon from "components/Icons/order/NewRateIcon";
import NoteIcon from "components/Icons/NoteIcon"; import NoteIcon from "components/Icons/NoteIcon";
import SuccessIcon from "components/Icons/SuccessIcon"; import SuccessIcon from "components/Icons/SuccessIcon";
import { SplitBillParticipantsBottomSheet } from "./components/SplitBillParticipantsBottomSheet"; import { SplitBillParticipantsBottomSheet } from "./components/SplitBillParticipantsBottomSheet";
import { OrderType } from "pages/checkout/hooks/types";
export default function OrderPage() { export default function OrderPage() {
const { t } = useTranslation(); const { t } = useTranslation();
const { orderId } = useParams(); const { orderId } = useParams();
const navigate = useNavigate(); const navigate = useNavigate();
const { isRTL } = useAppSelector((state) => state.locale); const { isRTL } = useAppSelector((state) => state.locale);
const { restaurant } = useAppSelector((state) => state.order); const { restaurant, orderType } = useAppSelector((state) => state.order);
const hasRefetchedRef = useRef(false); const hasRefetchedRef = useRef(false);
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [isRateOrderOpen, setIsRateOrderOpen] = useState(false); const [isRateOrderOpen, setIsRateOrderOpen] = useState(false);
@@ -429,7 +430,7 @@ export default function OrderPage() {
<Stepper statuses={orderDetails?.status} /> <Stepper statuses={orderDetails?.status} />
</div> </div>
{!hasClosedStatus && ( {!hasClosedStatus && orderType === OrderType.DineIn && (
<div className={styles.orderNotes}> <div className={styles.orderNotes}>
<NoteIcon className={styles.noteIcon} /> <NoteIcon className={styles.noteIcon} />
<div <div
@@ -464,7 +465,7 @@ export default function OrderPage() {
</div> </div>
</div> </div>
)} )}
{hasClosedStatus && ( {hasClosedStatus && orderType === OrderType.DineIn && (
<div className={styles.orderNotesClosed}> <div className={styles.orderNotesClosed}>
<SuccessIcon className={styles.noteIcon} /> <SuccessIcon className={styles.noteIcon} />
<ProText <ProText

View File

@@ -6,6 +6,7 @@ import { useTranslation } from "react-i18next";
import { Extra as ExtraType } from "utils/types/appTypes"; import { Extra as ExtraType } from "utils/types/appTypes";
import styles from "../product.module.css"; import styles from "../product.module.css";
import { formatPriceUi } from "utils/helpers"; import { formatPriceUi } from "utils/helpers";
import { useAppSelector } from "redux/hooks";
export default function Extra({ export default function Extra({
extrasList, extrasList,
@@ -17,7 +18,7 @@ export default function Extra({
setSelectedExtras: Dispatch<SetStateAction<ExtraType[]>>; setSelectedExtras: Dispatch<SetStateAction<ExtraType[]>>;
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const { restaurant } = useAppSelector((state) => state.order);
return ( return (
<> <>
{extrasList.length > 0 && ( {extrasList.length > 0 && (
@@ -46,7 +47,7 @@ export default function Extra({
return { return {
value: value.id.toString(), value: value.id.toString(),
label: value.name, label: value.name,
price: `+${formatPriceUi(value.price, 3)}`, price: `+${formatPriceUi(value.price, restaurant.currency_decimals ?? 3)}`,
}; };
})} })}
value={selectedExtras.map((ex) => ex.id.toString())} value={selectedExtras.map((ex) => ex.id.toString())}

View File

@@ -21,7 +21,7 @@ export default function Variants({
const { isRTL } = useAppSelector((state) => state.locale); const { isRTL } = useAppSelector((state) => state.locale);
const { t } = useTranslation(); const { t } = useTranslation();
const { isDesktop } = useBreakPoint(); const { isDesktop } = useBreakPoint();
const { restaurant } = useAppSelector((state) => state.order);
// Determine variant levels based on options array length // Determine variant levels based on options array length
const variantLevels = useMemo(() => { const variantLevels = useMemo(() => {
if (!variantsList || variantsList.length === 0) return []; if (!variantsList || variantsList.length === 0) return [];
@@ -170,7 +170,7 @@ export default function Variants({
value: value, value: value,
label: value, label: value,
price: variant price: variant
? `+${formatPriceUi(variant.price, 3)}` ? `+${formatPriceUi(variant.price, restaurant.currency_decimals ?? 3)}`
: "", : "",
}; };
})} })}

View File

@@ -10,16 +10,13 @@ import styles from "./restaurant.module.css";
import RestaurantServices from "./RestaurantServices"; import RestaurantServices from "./RestaurantServices";
// Import the Client Component for localStorage handling // Import the Client Component for localStorage handling
import Ads1 from "components/Ads/Ads1";
import { OrderDetailsBottomSheet } from "components/CustomBottomSheet/orderDetailsSheet/OrderDetailsBottomSheet.tsx"; import { OrderDetailsBottomSheet } from "components/CustomBottomSheet/orderDetailsSheet/OrderDetailsBottomSheet.tsx";
import { Loader } from "components/Loader/Loader"; import { Loader } from "components/Loader/Loader";
import { import {
CART_STORAGE_KEYS, CART_STORAGE_KEYS,
updateOrderType, updateOrderType,
} from "features/order/orderSlice.ts"; } from "features/order/orderSlice.ts";
import useBreakPoint from "hooks/useBreakPoint";
import { useRestaurant } from "hooks/useRestaurant"; import { useRestaurant } from "hooks/useRestaurant";
import useSwipeUp from "hooks/useSwipeUp";
import { OrderType } from "pages/checkout/hooks/types.ts"; import { OrderType } from "pages/checkout/hooks/types.ts";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";

View File

@@ -13,6 +13,7 @@ import {
REDEEM_DETAILS_URL, REDEEM_DETAILS_URL,
LOYALTY_HISTORY_URL, LOYALTY_HISTORY_URL,
CREATE_GIFT_AMOUNT_URL, CREATE_GIFT_AMOUNT_URL,
CALL_WAITER_URL,
OPENING_TIMES_URL, OPENING_TIMES_URL,
} from "utils/constants"; } from "utils/constants";
@@ -221,6 +222,13 @@ export const branchApi = baseApi.injectEndpoints({
return response.result; return response.result;
}, },
}), }),
callWaiter: builder.mutation({
query: (body: any) => ({
url: CALL_WAITER_URL,
method: "POST",
body,
}),
}),
}), }),
}); });
export const { export const {
@@ -239,4 +247,5 @@ export const {
useGetLoyaltyHistoryQuery, useGetLoyaltyHistoryQuery,
useCreateGiftAmountMutation, useCreateGiftAmountMutation,
useGetOpeningTimesQuery, useGetOpeningTimesQuery,
useCallWaiterMutation
} = branchApi; } = branchApi;

View File

@@ -1,6 +1,5 @@
import { Grid } from "antd"; import { Grid } from "antd";
import { Loader } from "components/Loader/Loader"; import { Loader } from "components/Loader/Loader";
import { PrivateRoute } from "components/privateRoute/PrivateRoute";
import { PublicRoute } from "components/publicRoute/PublicRoute"; import { PublicRoute } from "components/publicRoute/PublicRoute";
import { AppLayout } from "layouts"; import { AppLayout } from "layouts";
import HeaderMenuDrawer from "layouts/app/HeaderMenuDrawer"; import HeaderMenuDrawer from "layouts/app/HeaderMenuDrawer";
@@ -9,7 +8,7 @@ import CardDetailsPage from "pages/CardDetails/CardDetails";
import CartPage from "pages/cart/page"; import CartPage from "pages/cart/page";
import CheckoutPage from "pages/checkout/page"; import CheckoutPage from "pages/checkout/page";
import EGiftCardsPage from "pages/EGiftCards/EGiftCards"; import EGiftCardsPage from "pages/EGiftCards/EGiftCards";
import { Error400Page, ErrorPage } from "pages/errors"; import { ErrorPage } from "pages/errors";
import LoginPage from "pages/login/page"; import LoginPage from "pages/login/page";
import MenuPage from "pages/menu/page"; import MenuPage from "pages/menu/page";
import OrderDetails from "pages/order/components/OrderDetails"; import OrderDetails from "pages/order/components/OrderDetails";
@@ -123,9 +122,9 @@ export const router = createHashRouter([
element: ( element: (
<PageWrapper <PageWrapper
children={ children={
<PrivateRoute> <PublicRoute>
<OrdersPage /> <OrdersPage />
</PrivateRoute> </PublicRoute>
} }
/> />
), ),
@@ -195,20 +194,6 @@ export const router = createHashRouter([
}, },
], ],
}, },
{
path: "errors",
errorElement: <ErrorPage />,
children: [
{
path: "400",
element: (
<PrivateRoute>
<Error400Page />
</PrivateRoute>
),
},
],
},
]); ]);
export default router; export default router;

View File

@@ -112,4 +112,5 @@ export const EGIFT_CARDS_URL = `${BASE_URL}gift/cards`;
export const REDEEM_DETAILS_URL = `${BASE_URL}gift/getGiftOrderByVoucherCode`; export const REDEEM_DETAILS_URL = `${BASE_URL}gift/getGiftOrderByVoucherCode`;
export const LOYALTY_HISTORY_URL = `${BASE_URL}loyaltyHistory`; export const LOYALTY_HISTORY_URL = `${BASE_URL}loyaltyHistory`;
export const CREATE_GIFT_AMOUNT_URL = `${BASE_URL}gift/addGiftAmount`; export const CREATE_GIFT_AMOUNT_URL = `${BASE_URL}gift/addGiftAmount`;
export const OPENING_TIMES_URL = `${BASE_URL}restaurant/getWorkingHours`; export const OPENING_TIMES_URL = `${BASE_URL}restaurant/getWorkingHours`;
export const CALL_WAITER_URL = `${BASE_URL}call_waiter`;

View File

@@ -534,6 +534,7 @@ export interface RestaurantDetails {
closingTime: string; closingTime: string;
isOpened: boolean; isOpened: boolean;
isFav: boolean; isFav: boolean;
currency_decimals: number;
} }
export interface Banner { export interface Banner {