redeem: integration

This commit is contained in:
2026-01-11 15:25:45 +03:00
parent 6271c14eff
commit b0288ebcf6
9 changed files with 278 additions and 170 deletions

View File

@@ -563,6 +563,8 @@
"voucherBalance": "رصيد القسيمة", "voucherBalance": "رصيد القسيمة",
"getDirections": "الحصول على الاتجاهات", "getDirections": "الحصول على الاتجاهات",
"giftedItems": "العناصر المهدية", "giftedItems": "العناصر المهدية",
"viewAll": "عرض الكل" "viewAll": "عرض الكل",
"voucherCodeCopied": "تم نسخ رمز القسيمة",
"copyFailed": "فشل نسخ رمز القسيمة"
} }
} }

View File

@@ -581,6 +581,8 @@
"voucherBalance": "Voucher Balance", "voucherBalance": "Voucher Balance",
"getDirections": "Get Directions", "getDirections": "Get Directions",
"giftedItems": "Gifted Items", "giftedItems": "Gifted Items",
"viewAll": "View All" "viewAll": "View All",
"voucherCodeCopied": "Voucher code copied!",
"copyFailed": "Failed to copy voucher code"
} }
} }

View File

@@ -4,7 +4,10 @@ import ProText from "components/ProText";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import styles from "./GiftItemsCard.module.css"; import styles from "./GiftItemsCard.module.css";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { useGetOrderDetailsQuery } from "redux/api/others"; import {
useGetOrderDetailsQuery,
useGetRedeemDetailsQuery,
} from "redux/api/others";
import ImageWithFallback from "components/ImageWithFallback"; import ImageWithFallback from "components/ImageWithFallback";
import { useAppSelector } from "redux/hooks"; import { useAppSelector } from "redux/hooks";
import useBreakPoint from "hooks/useBreakPoint"; import useBreakPoint from "hooks/useBreakPoint";
@@ -16,15 +19,13 @@ export function GiftItemsCard({ isCart = false }: { isCart?: boolean }) {
const { t } = useTranslation(); const { t } = useTranslation();
const { voucherId } = useParams(); const { voucherId } = useParams();
const { data: orderDetails, isLoading } = useGetOrderDetailsQuery( const { data: redeemDetails, isLoading } = useGetRedeemDetailsQuery(
voucherId || "",
{ {
orderID: voucherId || "5711385", skip: !voucherId,
restaurantID: localStorage.getItem("restaurantID") || "",
}, },
// {
// skip: !voucherId,
// },
); );
const { isRTL } = useAppSelector((state) => state.locale); const { isRTL } = useAppSelector((state) => state.locale);
const { isMobile, isTablet } = useBreakPoint(); const { isMobile, isTablet } = useBreakPoint();
const getMenuItemImageStyle = () => { const getMenuItemImageStyle = () => {
@@ -176,7 +177,7 @@ export function GiftItemsCard({ isCart = false }: { isCart?: boolean }) {
))} ))}
</> </>
) : ( ) : (
orderDetails?.orderItems?.map((item: any, index: number) => ( redeemDetails?.gift?.items?.map((item: any, index: number) => (
<div key={index} style={{ position: "relative" }}> <div key={index} style={{ position: "relative" }}>
<Space <Space
size="middle" size="middle"
@@ -284,7 +285,7 @@ export function GiftItemsCard({ isCart = false }: { isCart?: boolean }) {
</div> </div>
</Space> </Space>
{index !== orderDetails?.orderItems?.length - 1 && ( {index !== redeemDetails?.gift?.items?.length - 1 && (
<Divider style={{ margin: "16px 0" }} /> <Divider style={{ margin: "16px 0" }} />
)} )}
</div> </div>

View File

@@ -1,16 +1,32 @@
import { Divider, Tag } from "antd"; import { Divider, Tag } from "antd";
import ProInputCard from "components/ProInputCard/ProInputCard"; import ProInputCard from "components/ProInputCard/ProInputCard";
import ProText from "components/ProText"; import ProText from "components/ProText";
import { selectCart } from "features/order/orderSlice";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useAppSelector } from "redux/hooks";
import styles from "./LocationCard.module.css"; import styles from "./LocationCard.module.css";
import { GoogleMap } from "components/CustomBottomSheet/GoogleMap"; import { GoogleMap } from "components/CustomBottomSheet/GoogleMap";
import DirectionsIcon from "components/Icons/DirectionsIcon"; import DirectionsIcon from "components/Icons/DirectionsIcon";
import { useGetRedeemDetailsQuery } from "redux/api/others";
import { useParams } from "react-router-dom";
export function LocationCard() { export function LocationCard() {
const { t } = useTranslation(); const { t } = useTranslation();
const { restaurant } = useAppSelector(selectCart); const { voucherId } = useParams();
const { data: redeemDetails } = useGetRedeemDetailsQuery(voucherId || "", {
skip: !voucherId,
});
const handleGetDirections = () => {
const lat = redeemDetails?.gift?.lat;
const lng = redeemDetails?.gift?.lng;
if (lat && lng) {
// Google Maps URL that works on both mobile and desktop
// On mobile devices, this will open the Google Maps app if installed
const googleMapsUrl = `https://www.google.com/maps/dir/?api=1&destination=${lat},${lng}`;
// Use window.location.href for better mobile compatibility (works in webviews)
window.location.href = googleMapsUrl;
}
};
return ( return (
<> <>
@@ -19,8 +35,8 @@ export function LocationCard() {
<GoogleMap <GoogleMap
readOnly={true} readOnly={true}
initialLocation={{ initialLocation={{
lat: parseFloat(restaurant.lat || "0"), lat: parseFloat(redeemDetails?.gift?.lat || "0"),
lng: parseFloat(restaurant.lng || "0"), lng: parseFloat(redeemDetails?.gift?.lng || "0"),
address: "", address: "",
}} }}
height="160px" height="160px"
@@ -48,7 +64,7 @@ export function LocationCard() {
color: "#333333", color: "#333333",
}} }}
> >
{restaurant.restautantName} {redeemDetails?.gift?.restaurant}
</ProText> </ProText>
<ProText <ProText
@@ -61,16 +77,18 @@ export function LocationCard() {
color: "#777580", color: "#777580",
}} }}
> >
{restaurant.address} {redeemDetails?.gift?.restaurant}
</ProText> </ProText>
</div> </div>
<Tag <Tag
onClick={handleGetDirections}
style={{ style={{
backgroundColor: "#FFF9E6", backgroundColor: "#FFF9E6",
color: "#E8B400", color: "#E8B400",
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
gap: 4, gap: 4,
cursor: "pointer",
}} }}
> >
<DirectionsIcon /> <DirectionsIcon />

View File

@@ -1,48 +1,46 @@
import { Divider, Switch, Tag } from "antd"; import { Divider, Switch, Tag } from "antd";
import ProInputCard from "components/ProInputCard/ProInputCard"; import ProInputCard from "components/ProInputCard/ProInputCard";
import ProText from "components/ProText"; import ProText from "components/ProText";
import { selectCart } from "features/order/orderSlice";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useAppSelector } from "redux/hooks";
import styles from "./VoucherBalanceCard.module.css"; import styles from "./VoucherBalanceCard.module.css";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import CardAmountIcon from "components/Icons/CardAmountIcon"; import CardAmountIcon from "components/Icons/CardAmountIcon";
import ArabicPrice from "components/ArabicPrice"; import ArabicPrice from "components/ArabicPrice";
import { useGetRedeemDetailsQuery } from "redux/api/others";
export function VoucherBalanceCard() { export function VoucherBalanceCard() {
const { t } = useTranslation(); const { t } = useTranslation();
const { giftDetails } = useAppSelector(selectCart);
const navigate = useNavigate(); const navigate = useNavigate();
const { subdomain } = useParams(); const { voucherId } = useParams();
const { data: redeemDetails } = useGetRedeemDetailsQuery(voucherId || "", {
skip: !voucherId,
});
return ( return (
<> <>
<ProInputCard <ProInputCard
title={t("redeem.voucherBalance")} title={t("redeem.voucherBalance")}
titleRight={ titleRight={
<> <Tag
<Tag style={{
style={{ height: 23,
height: 23, textAlign: "center",
textAlign: "center", opacity: 1,
opacity: 1, paddingRight: 10,
paddingRight: 10, paddingLeft: 10,
paddingLeft: 10, borderRadius: 100,
borderRadius: 100, fontWeight: 500,
fontWeight: 500, fontStyle: "Medium",
fontStyle: "Medium", fontSize: 12,
fontSize: 12, lineHeight: "140%",
lineHeight: "140%", letterSpacing: "0%",
letterSpacing: "0%", cursor: "pointer",
cursor: "pointer", backgroundColor: "#FFF9E6",
backgroundColor: "#FFF9E6", color: "#B58D00",
color: "#B58D00", }}
}} >
> {t("redeem.pending")}
{t("redeem.pending")} </Tag>
</Tag>
</>
} }
> >
<div className={styles.orderNotes}> <div className={styles.orderNotes}>
@@ -87,7 +85,7 @@ export function VoucherBalanceCard() {
color: "#333333", color: "#333333",
}} }}
> >
<ArabicPrice price={giftDetails?.amount || 0} /> <ArabicPrice price={redeemDetails?.gift?.amount || 0} />
</ProText> </ProText>
</div> </div>
<Switch /> <Switch />
@@ -100,9 +98,6 @@ export function VoucherBalanceCard() {
flexDirection: "row", flexDirection: "row",
justifyContent: "space-between", justifyContent: "space-between",
}} }}
onClick={() => {
navigate(`/${subdomain}/cart`);
}}
> >
<ProText <ProText
style={{ style={{

View File

@@ -1,73 +1,51 @@
import { Button, Card, Image, Layout, Skeleton } from "antd"; import { Button, Card, Image, Layout, QRCode, Skeleton, message } from "antd";
import ProHeader from "components/ProHeader/ProHeader"; import ProHeader from "components/ProHeader/ProHeader";
import ProText from "components/ProText"; import ProText from "components/ProText";
import { useEffect, useRef } 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 { useGetRedeemDetailsQuery } from "redux/api/others";
useGetOrderDetailsQuery,
useGetRestaurantDetailsQuery,
} from "redux/api/others";
import styles from "./redeem.module.css"; import styles from "./redeem.module.css";
import QRIcon from "components/Icons/QRIcon";
import CopyIcon from "components/Icons/CopyIcon"; import CopyIcon from "components/Icons/CopyIcon";
import { useAppSelector } from "redux/hooks";
import { LocationCard } from "./components/LocationCard.tsx"; import { LocationCard } from "./components/LocationCard.tsx";
import { GiftItemsCard } from "./components/GiftItemsCard.tsx"; import { GiftItemsCard } from "./components/GiftItemsCard.tsx";
import { VoucherBalanceCard } from "./components/VoucherBalanceCard.tsx"; import { VoucherBalanceCard } from "./components/VoucherBalanceCard.tsx";
import { OrderType } from "pages/checkout/hooks/types.ts"; import { OrderType } from "pages/checkout/hooks/types.ts";
import { Loader } from "components/Loader/Loader.tsx";
export default function RedeemPage() { export default function RedeemPage() {
const { t } = useTranslation(); const { t } = useTranslation();
const { voucherId } = useParams(); const { voucherId } = useParams();
const { restaurant } = useAppSelector((state) => state.order);
const hasRefetchedRef = useRef(false);
const navigate = useNavigate(); const navigate = useNavigate();
const { subdomain } = useParams(); const { subdomain } = useParams();
const { data: orderDetails, isLoading } = useGetOrderDetailsQuery( const { data: redeemDetails, isLoading } = useGetRedeemDetailsQuery(
{ voucherId || "",
orderID: voucherId || "",
restaurantID: localStorage.getItem("restaurantID") || "",
},
{ {
skip: !voucherId, skip: !voucherId,
}, },
); );
// 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;
}, [voucherId]);
// Refetch restaurant details when order status has alias "closed"
useEffect(() => {
if (orderDetails?.status && !hasRefetchedRef.current) {
const hasClosedStatus = orderDetails.status.some(
(status) => status?.alias === "closed",
);
if (hasClosedStatus && restaurantSubdomain) {
refetchRestaurantDetails();
hasRefetchedRef.current = true;
}
}
}, [orderDetails?.status, restaurantSubdomain, refetchRestaurantDetails]);
const handleCheckout = () => { const handleCheckout = () => {
navigate(`/${subdomain}/menu?orderType=${OrderType.Redeem}`); navigate(`/${subdomain}/menu?orderType=${OrderType.Redeem}`);
}; };
const handleCopyVoucherCode = async () => {
const voucherCode = redeemDetails?.gift?.voucher_code;
if (voucherCode) {
try {
await navigator.clipboard.writeText(voucherCode);
message.success(
t("redeem.voucherCodeCopied") || "Voucher code copied!",
);
} catch (error) {
message.error(t("redeem.copyFailed") || "Failed to copy voucher code");
}
}
};
if (isLoading) return <Loader />;
return ( return (
<> <>
<Layout> <Layout>
@@ -96,7 +74,7 @@ export default function RedeemPage() {
</ProText> </ProText>
</div> </div>
{isLoading || !orderDetails?.restaurant_iimage ? ( {isLoading || !redeemDetails?.gift?.restaurant_iimage ? (
<div className={styles.carouselContainer}> <div className={styles.carouselContainer}>
<div className={styles.cardWrapper}> <div className={styles.cardWrapper}>
<Skeleton.Image <Skeleton.Image
@@ -113,7 +91,7 @@ export default function RedeemPage() {
<div className={styles.carouselContainer}> <div className={styles.carouselContainer}>
<div className={styles.cardWrapper}> <div className={styles.cardWrapper}>
<Image <Image
src={orderDetails?.restaurant_iimage} src={redeemDetails?.gift?.card_url}
width={205} width={205}
height={134} height={134}
className={styles.cardImage} className={styles.cardImage}
@@ -161,56 +139,28 @@ export default function RedeemPage() {
</ProText> </ProText>
</div> </div>
<Card <div>
style={{ <Card
borderRadius: 0,
borderTopRightRadius: 16,
borderTopLeftRadius: 16,
border: "1px solid transparent",
borderImageSource:
"radial-gradient(38.92% 103.83% at 49.85% -3.83%, #FFB700 0%, rgba(255, 233, 179, 0) 100%)",
borderImageSlice: 1,
}}
styles={{
body: {
display: "flex",
flexDirection: "column",
gap: 24,
alignItems: "center",
justifyContent: "center",
padding: 32,
},
}}
>
<ProText
style={{ style={{
fontWeight: 400, borderRadius: 0,
fontStyle: "Regular", borderTopRightRadius: 16,
fontSize: 14, borderTopLeftRadius: 16,
lineHeight: "140%", border: "1px solid transparent",
letterSpacing: "0%", borderImageSource:
color: "#95949C", "radial-gradient(38.92% 103.83% at 49.85% -3.83%, #FFB700 0%, rgba(255, 233, 179, 0) 100%)",
alignItems: "center", borderImageSlice: 1,
}}
styles={{
body: {
display: "flex",
flexDirection: "column",
gap: 24,
alignItems: "center",
justifyContent: "center",
padding: 32,
},
}} }}
> >
{t("redeem.showThisCodeAtTheRestaurant")}
</ProText>
<QRIcon />
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
<Button
style={{
height: 40,
borderRadius: 888,
gap: 16,
opacity: 1,
borderWidth: 1,
backgroundColor: "var(--background)",
}}
icon={<CopyIcon className={styles.copyIcon} />}
iconPlacement="end"
>
GFT - 92KD - 7X84
</Button>
<ProText <ProText
style={{ style={{
fontWeight: 400, fontWeight: 400,
@@ -222,40 +172,73 @@ export default function RedeemPage() {
alignItems: "center", alignItems: "center",
}} }}
> >
{t("redeem.useThisCodeIfScanningNotPossible")} {t("redeem.showThisCodeAtTheRestaurant")}
</ProText> </ProText>
</div> <QRCode value={redeemDetails?.gift?.gr_url || "-"} />
</Card> <div
style={{ display: "flex", flexDirection: "column", gap: 12 }}
>
<Button
style={{
height: 40,
borderRadius: 888,
gap: 16,
opacity: 1,
borderWidth: 1,
backgroundColor: "var(--background)",
}}
icon={<CopyIcon className={styles.copyIcon} />}
iconPlacement="end"
onClick={handleCopyVoucherCode}
>
{redeemDetails?.gift?.voucher_code}
</Button>
<ProText
style={{
fontWeight: 400,
fontStyle: "Regular",
fontSize: 14,
lineHeight: "140%",
letterSpacing: "0%",
color: "#95949C",
alignItems: "center",
}}
>
{t("redeem.useThisCodeIfScanningNotPossible")}
</ProText>
</div>
</Card>
<div <div
style={{
width: "100%",
height: 44,
opacity: 1,
borderBottomRightRadius: 16,
borderBottomLeftRadius: 16,
paddingTop: 12,
paddingBottom: 12,
gap: 10,
borderTopWidth: 1,
background: "#FFF9E6",
borderTop: "1px solid #FFEDB0",
textAlign: "center",
}}
>
<ProText
style={{ style={{
fontSize: 14, width: "100%",
fontWeight: 500, height: 44,
fontStyle: "Medium", opacity: 1,
lineHeight: "140%", borderBottomRightRadius: 16,
letterSpacing: "0%", borderBottomLeftRadius: 16,
paddingTop: 12,
paddingBottom: 12,
gap: 10,
borderTopWidth: 1,
background: "#FFF9E6",
borderTop: "1px solid #FFEDB0",
textAlign: "center", textAlign: "center",
color: "#E8B400",
}} }}
> >
Active - Expires in 12 days <ProText
</ProText> style={{
fontSize: 14,
fontWeight: 500,
fontStyle: "Medium",
lineHeight: "140%",
letterSpacing: "0%",
textAlign: "center",
color: "#E8B400",
}}
>
Active - Expires in 12 days
</ProText>
</div>
</div> </div>
<div <div

94
src/pages/redeem/types.ts Normal file
View File

@@ -0,0 +1,94 @@
export interface RedeemResponse {
gift: Gift
working_hours: WorkingHours
notes: Notes
is_restaurant_closed: boolean
}
export interface Gift {
id: number
voucher_amount: string
amount: string
restorant_id: number
voucher_code: string
used_amount: string
show_sender_info: number
remaining_amount: string
is_used: number
sender_phone: string
gift_type: string
sender_name: string
recipient_phone: string
recipient_name: string
message: any
created_at: string
card_url: string
gr_url: string
restaurant: string
global_currency: string
lat: string
lng: string
local_currency: string
restaurant_iimage: string
order_id: number
items: Item[]
itemsImagePrefix: string
itemsImagePrefixOld: string
}
export interface Item {
id: number
is_loyalty_used: number
no_of_stamps_give: number
isHasLoyalty: boolean
is_vat_disabled: number
name: string
price: number
qty: number
variant_price: string
image: string
imageName: string
variantName: string
variantLocalName: string
extras: any[]
descriptionEN: string
descriptionAR: string
itemline: string
itemlineAR: string
itemlineAREN: string
extrasgroups: any[]
extragroupnew: any[]
itemComment: string
variant: any
itemExtras: any[]
AvaiilableVariantExtras: any[]
isPrinted: number
category_id: number
pos_order_id: string
updated_at: string
created_at: string
old_qty: number
new_qty: number
last_printed_qty: number
deleted_qty: number
discount_value: any
discount_type_id: any
original_price: number
pricing_method: string
is_already_paid: number
hash_item: string
}
export interface WorkingHours {
opening_time: string
closing_time: string
opening_time_2: any
closing_time_2: any
is_open: boolean
}
export interface Notes {
en: string
ar: string
}

View File

@@ -10,6 +10,7 @@ import {
TABLES_URL, TABLES_URL,
USER_DETAILS_URL, USER_DETAILS_URL,
EGIFT_CARDS_URL, EGIFT_CARDS_URL,
REDEEM_DETAILS_URL,
} from "utils/constants"; } from "utils/constants";
import { OrderDetails } from "pages/checkout/hooks/types"; import { OrderDetails } from "pages/checkout/hooks/types";
@@ -21,6 +22,7 @@ import {
} from "utils/types/appTypes"; } from "utils/types/appTypes";
import { baseApi } from "./apiSlice"; import { baseApi } from "./apiSlice";
import { EGiftCard } from "pages/EGiftCards/type"; import { EGiftCard } from "pages/EGiftCards/type";
import { RedeemResponse } from "pages/redeem/types";
export const branchApi = baseApi.injectEndpoints({ export const branchApi = baseApi.injectEndpoints({
endpoints: (builder) => ({ endpoints: (builder) => ({
@@ -171,6 +173,15 @@ export const branchApi = baseApi.injectEndpoints({
return response.result; return response.result;
}, },
}), }),
getRedeemDetails: builder.query<RedeemResponse, string>({
query: (voucherId: string) => ({
url: `${REDEEM_DETAILS_URL}/${voucherId}`,
method: "GET",
}),
transformResponse: (response: any) => {
return response.result;
},
}),
}), }),
}); });
export const { export const {
@@ -185,4 +196,5 @@ export const {
useRateOrderMutation, useRateOrderMutation,
useGetUserDetailsQuery, useGetUserDetailsQuery,
useGetEGiftCardsQuery, useGetEGiftCardsQuery,
useGetRedeemDetailsQuery,
} = branchApi; } = branchApi;

View File

@@ -109,3 +109,4 @@ export const CONFIRM_OTP_URL = `${API_BASE_URL}confirmOtp`;
export const PAYMENT_CONFIRMATION_URL = `https://menu.fascano.com/payment/confirmation`; export const PAYMENT_CONFIRMATION_URL = `https://menu.fascano.com/payment/confirmation`;
export const DISCOUNT_URL = `${BASE_URL}getDiscount`; export const DISCOUNT_URL = `${BASE_URL}getDiscount`;
export const EGIFT_CARDS_URL = `${BASE_URL}gift/cards`; export const EGIFT_CARDS_URL = `${BASE_URL}gift/cards`;
export const REDEEM_DETAILS_URL = `${BASE_URL}gift/getGiftOrderByVoucherCode`;