Vitron-Front/app/routes/profile.orders.tsx
2026-04-29 01:44:16 +03:30

539 lines
21 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useEffect, useState } from "react";
import { MetaFunction } from "@remix-run/node";
import ProfilePagesHeader from "../components/ProfilePagesHeader";
import placeholderImage from "~/assets/icons/product.png";
import placeholderImageDark from "~/assets/icons/product-dark.png";
import {
useGetUserOrders,
useGetOrderDetail,
} from "../requestHandler/use-order-hooks";
import { useServiceGetCouriers } from "../requestHandler/use-courier-hooks";
import {
Archive,
ArrowRight,
ArrowUpLeft,
Component,
SquareCheckBig,
SquareX,
X,
} from "lucide-react";
import { formatDate, formatPrice } from "~/utils/helpers";
import { Dialog, DialogContent, DialogOverlay } from "../components/ui/dialog";
import { Button } from "~/components/ui/button";
import { useNavigate } from "@remix-run/react";
import { formatDateRange } from "~/components/cart/ShippingInformation";
import { OrderItem, OrderItemSummary, OrderList } from "src/api/types";
import UiProvider from "~/components/UiProvider";
import { isVideo } from "~/components/MondrianProductList";
import SellerLogo from "~/components/SellerLogo";
import { useRootData } from "~/hooks/use-root-data";
// Product Modal Component
const ProductModal = ({
product,
isOpen,
onClose,
}: {
product: OrderItemSummary | null;
isOpen: boolean;
onClose: () => void;
}) => {
const navigate = useNavigate();
if (!product) return null;
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogOverlay />
<DialogContent
closeButton={true}
className="p-4 bg-WHITE overflow-y-auto max-h-[90vh] rounded-[12px] w-[calc(100%-40px)]"
>
<div className="flex w-full items-center flex-col gap-4">
<X
size={16}
onClick={() => {
onClose();
}}
/>
<img
src={product.productImages?.[0] || ""}
alt={product.productTitle}
className="rounded-lg w-full aspect-square"
/>
<p className="text-R14">{product.productTitle}</p>
<Button
onClick={() => {
navigate(`/product/${product.productId}`);
}}
size="lg"
variant="dark"
className="w-full py-1"
>
مشاهده این پست
</Button>
</div>
</DialogContent>
</Dialog>
);
};
export default function Orders() {
const { data, isLoading, isError, error, refetch } = useGetUserOrders();
const [currentData, setCurrentData] = useState<OrderList[]>([]);
const [activeTab, setActiveTab] = useState(0);
const [activeFactorId, setActiveFactorId] = useState("");
const { theme } = useRootData();
const [showModal, setShowModal] = useState(false);
const [selectedProduct, setSelectedProduct] =
useState<OrderItemSummary | null>(null);
// Update currentData when data or activeTab changes
useEffect(() => {
if (data) {
let filteredData: OrderList[] = [];
if (activeTab === 0) {
filteredData = data;
} else if (activeTab === 1) {
filteredData = data.filter(
(order: OrderList) =>
order.status === "confirmed" || order.status === "pending"
);
} else if (activeTab === 2) {
filteredData = data.filter(
(order: OrderList) =>
order.status === "delivered" || order.status === "shipped"
);
} else if (activeTab === 3) {
filteredData = data.filter(
(order: OrderList) => order.status === "cancelled"
);
}
setCurrentData(filteredData);
}
if (isError) {
console.error("Error fetching orders:", error);
setCurrentData([]);
}
}, [data, isError, error, activeTab]);
const orderTabs = [
{ title: "همه", logo: <Component size={16} /> },
{ title: "جاری", logo: <Archive size={16} /> },
{ title: "ارسال شده", logo: <SquareCheckBig size={16} /> },
{ title: "لغو شده", logo: <SquareX size={16} /> },
];
const handleProductClick = (item: OrderItemSummary) => {
setSelectedProduct(item);
setShowModal(true);
};
const closeModal = () => {
setShowModal(false);
};
return (
<>
{activeFactorId ? (
<FactorDetail
factorId={activeFactorId}
setActiveFactorId={setActiveFactorId}
/>
) : (
<div className="flex flex-col w-full items-center">
<ProfilePagesHeader title="سفارش‌های من" />
<div
className={`pt-4 px-6 pb-[2px] flex w-full justify-between border-b`}
>
{orderTabs.map(
(
tab: { title: string; logo: React.ReactNode },
index: number
) => (
<div
key={index}
role="button"
tabIndex={index}
onKeyDown={() => {}}
onClick={() => {
setActiveTab(index);
}}
className={`flex flex-col items-center gap-1 ${activeTab === index ? "text-BLACK border-b border-BLACK" : "text-GRAY"}`}
>
{tab.logo}
<p className="text-R12">{tab.title}</p>
</div>
)
)}
</div>
{/* Add min-height to this container to prevent layout shift */}
<div className="w-full min-h-[80vh]">
<UiProvider
isLoading={isLoading}
isEmpty={
currentData &&
currentData.length === 0 &&
!isLoading &&
!isError
}
isError={isError}
onRetry={refetch}
type="order"
>
<>
{currentData &&
currentData.length > 0 &&
currentData.map((order: OrderList, index: number) => {
return (
<div
key={index}
className="w-full flex flex-col p-4 border-b gap-4"
>
<div className="flex justify-between w-full">
<div className="flex gap-1">
{getOrderStatusIcon(order.status || "")}
<p className="text-R12">
{getOrderStatus(order.status || "")}
</p>
</div>
<div className="flex gap-2 text-GRAY">
<p className="text-R12">شماره سفارش: </p>
<p className="text-R12">{order.invoiceNumber}</p>
</div>
</div>
<div className="flex justify-between w-full">
<p className="text-R12">تاریخ ثبت سفارش: </p>
<p className="text-R12">
{formatDate(order.createdAt || "")}
</p>
</div>
<div className="flex justify-between w-full">
<p className="text-R12">مبلغ پرداخت شده: </p>
<p className="text-R12">
{formatPrice(order.totalPrice)} تومان
</p>
</div>
<div className="flex gap-2 w-full flex-col">
<p className="text-R12">اقلام سبد خرید:</p>
<div className="flex w-full gap-2 overflow-hidden">
{order.items?.map(
(item: OrderItemSummary, index: number) => {
return (
<div
className="relative rounded-lg overflow-hidden cursor-pointer"
key={index}
onClick={() => handleProductClick(item)}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === "Enter")
handleProductClick(item);
}}
>
{isVideo(item.productImages?.[0] || "") ? (
<video
src={item.productImages?.[0] || ""}
className="w-14 h-14 object-cover"
autoPlay
muted
loop
playsInline
/>
) : (
<img
src={item.productImages?.[0] || ""}
alt={item.productTitle}
className="w-14 h-14 object-cover"
onError={(e) => {
e.currentTarget.src =
theme === "dark"
? placeholderImageDark
: placeholderImage;
e.currentTarget.style.objectFit =
"contain";
e.currentTarget.style.backgroundColor =
theme === "dark"
? "#121212"
: "#f2f0f0";
}}
/>
)}
<div className="absolute bottom-1 left-1 w-4 h-4 rounded-full bg-BLACK/80 flex items-center justify-center">
<p className="text-R8 text-WHITE">x1</p>
</div>
</div>
);
}
)}
</div>
</div>
<div
role="button"
tabIndex={index}
onKeyDown={() => {}}
onClick={() => setActiveFactorId(order.id || "")}
className="flex text-BLUE gap-2 cursor-pointer justify-center w-full"
>
<ArrowUpLeft size={24} />
<p className="text-R12">مشاهده فاکتور</p>
</div>
</div>
);
})}
</>
</UiProvider>
</div>
{/* Render product modal component */}
<ProductModal
product={selectedProduct}
isOpen={showModal}
onClose={closeModal}
/>
</div>
)}
</>
);
}
const FactorDetail = ({
factorId,
setActiveFactorId,
}: {
factorId: string;
setActiveFactorId: (id: string) => void;
}) => {
const { data: factorDetail, isLoading: isLoadingDetail } =
useGetOrderDetail(factorId);
const sellerId = factorDetail?.seller || "";
const { data: sellerCouriers } = useServiceGetCouriers(sellerId);
const courierName =
factorDetail?.shippingInfo?.sellerCourierDetail?.courierName || "-";
return (
<div className="flex flex-col w-full pb-4">
<div className="flex flex-row h-[65px] w-full relative items-center justify-center border-b border-inner-border">
<ArrowRight
onClick={() => setActiveFactorId("")}
className="absolute top-5 right-5"
/>
<p className={"text-B16 font-bold"}>جزییات فاکتور</p>
</div>
{isLoadingDetail ? (
<p className="text-GRAY">در حال بارگذاری فاکتور...</p>
) : (
<div className="w-full">
<div className="w-full px-4">
<div className="flex flex-col gap-4 py-4 w-full border-b border-TRANSPARENT_BLACK_10 pb-4">
<div className="w-full flex gap-1 justify-between">
<p className="text-R12">وضعیت سفارش</p>
<p className="text-R12">
{getOrderStatus(factorDetail?.status || "")}
</p>
</div>
<div className="w-full flex gap-1 justify-between">
<p className="text-R12">شماره سفارش</p>
<p className="text-R12">
...{factorDetail?.invoice?.id?.slice(0, 10)}
</p>
</div>
{factorDetail?.shippingInfo?.trackingCode && (
<div className="w-full flex gap-1 justify-between">
<p className="text-R12">کد پیگیری پستی</p>
<p className="text-R12">
{factorDetail?.shippingInfo?.trackingCode || "-"}
</p>
</div>
)}
<div className="w-full flex gap-1 justify-between">
<p className="text-R12">تاریخ ثبت سفارش</p>
<p className="text-R12">
{formatDate(factorDetail?.createdAt || "")}
</p>
</div>
<div className="w-full flex gap-1 justify-between">
<p className="text-R12">روش ارسال</p>
<p className="text-R12">{courierName}</p>
</div>
</div>
<div className="flex flex-col gap-4 w-full border-b py-4">
{factorDetail?.items?.map((item: OrderItem, index: number) => (
<div key={index} className="w-full flex flex-col gap-1">
<div className="w-full justify-between flex gap-1">
<p className="text-R12">
{item.productTitle || item.variant?.variantName || ""}
</p>
<p className="text-R12 tracking-wide">
{formatPrice(item.variant?.price || item.unitPrice || "")}
x{item.quantity} تومان
</p>
</div>
<div className="flex items-center gap-4">
{item?.variant?.color?.value &&
item?.variant?.color?.value !== "UNSELECTED" && (
<div className="flex items-center gap-1">
<span className="text-R12">رنگ:</span>
<span
className="inline-block w-3 h-3 rounded-full border border-WHITE shadow"
style={{
backgroundColor:
item?.variant?.color?.info?.detail || "#000",
}}
aria-label="color"
/>
{item?.variant?.color?.info?.persian && (
<span className="text-R12">
{item?.variant?.color?.info?.persian}
</span>
)}
</div>
)}
{item?.variant?.size?.value &&
item?.variant?.size?.value !== "UNSELECTED" && (
<div className="flex items-center gap-1">
<span className="text-R12">سایز:</span>
<span className="text-R12">
{item?.variant?.size?.info?.persian ||
item?.variant?.size?.value}
</span>
</div>
)}
</div>
</div>
))}
<div className="w-full justify-between flex gap-1">
<p className="text-R12">مقدار تخفیف</p>
<p className="text-R12 tracking-wide">
{formatPrice(factorDetail?.invoice?.discountValue || "0")}
تومان
</p>
</div>
<div className="w-full justify-between flex gap-1">
<p className="text-R12">هزینه ارسال</p>
<p className="text-R12 tracking-wide">
{factorDetail?.shippingInfo?.sellerCourierDetail?.courierIsFreightCollect
? "پس پرداخت"
: `${formatPrice(
factorDetail?.shippingInfo?.deliveryPrice || ""
)} تومان`}
</p>
</div>
<div className="w-full justify-between flex gap-1">
<p className="text-B16 font-bold">مبلغ کل فاکتور</p>
<p className="text-B16 font-bold tracking-wide">
{formatPrice(factorDetail?.totalPrice || "")}
تومان
</p>
</div>
</div>
<div className="flex flex-col gap-4 py-4 w-full border-b border-TRANSPARENT_BLACK_10 pb-4">
<div className="w-full flex gap-1 justify-between">
<p className="text-R12">تحویل گیرنده</p>
<p className="text-R12">
{factorDetail?.shippingInfo?.receiverName || "حضوری"}
</p>
</div>
<div className="w-full flex gap-1 justify-between">
<p className="text-R12">شماره موبایل</p>
<p className="text-R12">
{factorDetail?.shippingInfo?.receiverPhone || "-"}
</p>
</div>
<div className="w-full flex gap-1 justify-between">
<p className="text-R12">زمان تحویل</p>
<p className="text-R12">
{formatDateRange(
factorDetail?.shippingInfo?.estimatedDeliveryDays,
factorDetail?.shippingInfo?.createdAt
)}
</p>
</div>
{factorDetail?.shippingInfo?.mainAddress && (
<div className="w-full flex gap-1">
<p className="text-R12">
آدرس:{factorDetail?.shippingInfo?.mainAddress}
</p>
</div>
)}
</div>
</div>
<div className="w-full flex flex-col gap-4 px-4 pt-10 items-center justify-center">
<a
href={`/seller/${factorDetail?.seller}`}
className="gap-2 flex items-center"
>
<SellerLogo src={factorDetail?.storeLogo} alt="sellerLogo" />
<p className="text-B12 font-bold">
{factorDetail?.storeName || ""}
</p>
</a>
</div>
</div>
)}
</div>
);
};
const getOrderStatus = (status: string) => {
switch (status) {
case "confirmed":
return "جاری";
case "delivered":
return "تحویل شده";
case "cancelled":
return "لغو شده";
case "pending":
return "پرداخت شده";
case "shipped":
return "ارسال شده";
default:
return "نامشخص";
}
};
const getOrderStatusIcon = (status: string) => {
switch (status) {
case "confirmed":
return <Archive size={18} className="text-GREEN" />;
case "delivered":
return <SquareCheckBig size={18} className="text-GREEN" />;
case "cancelled":
return <SquareX size={18} className="text-RED" />;
case "pending":
return <Archive size={18} className="text-YELLOW" />;
case "shipped":
return <Archive size={18} className="text-YELLOW" />;
default:
return <Archive size={18} className="text-GRAY" />;
}
};
export const meta: MetaFunction = () => {
const title = "سفارش‌های من | ویترون";
const description = "مشاهده و پیگیری سفارش‌های خود در ویترون";
const imageUrl =
"https://vitrownstatics.s3.ir-thr-at1.arvanstorage.ir/SVG-06.svg?versionId=";
return [
{ title },
{ name: "description", content: description },
{
name: "keywords",
content: "سفارش‌ها، پیگیری سفارش، تاریخچه خرید، ویترون",
},
{ name: "robots", content: "noindex, follow" },
// Open Graph
{ property: "og:type", content: "website" },
{ property: "og:title", content: title },
{ property: "og:description", content: description },
{ property: "og:image", content: imageUrl },
{ property: "og:site_name", content: "ویترون" },
{ property: "og:locale", content: "fa_IR" },
// Twitter Card
{ name: "twitter:card", content: "summary" },
{ name: "twitter:title", content: title },
{ name: "twitter:description", content: description },
];
};