318 lines
11 KiB
TypeScript
318 lines
11 KiB
TypeScript
import { LoaderFunctionArgs, type MetaFunction } from "@remix-run/node";
|
||
import ProfilePagesHeader from "../components/ProfilePagesHeader";
|
||
import { useGetSellerOrders } from "../requestHandler/use-order-hooks";
|
||
import {
|
||
formatDate,
|
||
formatPrice,
|
||
formatTime,
|
||
safeGoBack,
|
||
} from "../utils/helpers";
|
||
import {
|
||
SellerOrderList,
|
||
SellerOrderListStatusEnum,
|
||
} from "../../src/api/types";
|
||
import { useState } from "react";
|
||
import {
|
||
AlarmClockCheck,
|
||
CircleCheckBig,
|
||
CircleX,
|
||
DiamondPlus,
|
||
LayoutGrid,
|
||
PackageCheck,
|
||
Search,
|
||
} from "lucide-react";
|
||
import HeaderWithSupport from "../components/HeaderWithSupport";
|
||
import { useLoaderData, useNavigate } from "@remix-run/react";
|
||
import { useStoreOwnership } from "../hooks/useStoreOwnership";
|
||
|
||
export const meta: MetaFunction = () => {
|
||
return [
|
||
{ title: "سفارشهای دریافتی - ویترون" },
|
||
{
|
||
name: "description",
|
||
content:
|
||
"مدیریت و مشاهده تمام سفارشهای دریافتی در فروشگاه شما. فیلتر بر اساس وضعیت و جستجو در سفارشها.",
|
||
},
|
||
{
|
||
name: "keywords",
|
||
content: "سفارشها، مدیریت سفارش، فروشگاه، مشتری، ویترون",
|
||
},
|
||
{ name: "robots", content: "noindex, follow" },
|
||
{ property: "og:title", content: "سفارشهای دریافتی - ویترون" },
|
||
{
|
||
property: "og:description",
|
||
content:
|
||
"مدیریت و مشاهده تمام سفارشهای دریافتی در فروشگاه شما. فیلتر بر اساس وضعیت و جستجو در سفارشها.",
|
||
},
|
||
{ property: "og:type", content: "website" },
|
||
{ name: "twitter:card", content: "summary" },
|
||
{ name: "twitter:title", content: "سفارشهای دریافتی - ویترون" },
|
||
{
|
||
name: "twitter:description",
|
||
content:
|
||
"مدیریت و مشاهده تمام سفارشهای دریافتی در فروشگاه شما. فیلتر بر اساس وضعیت و جستجو در سفارشها.",
|
||
},
|
||
];
|
||
};
|
||
|
||
export async function loader({ params }: LoaderFunctionArgs) {
|
||
const storeId = params.storeId;
|
||
if (!storeId) {
|
||
throw new Response("Store ID is required", { status: 400 });
|
||
}
|
||
return { storeId };
|
||
}
|
||
|
||
// Status translation map
|
||
const statusMap: Record<SellerOrderListStatusEnum, string> = {
|
||
shipped: "ارسال شده",
|
||
delivered: "تحویل داده شده",
|
||
confirmed: "تایید شده",
|
||
pending: "جدید",
|
||
cancelled: "لغو شده",
|
||
};
|
||
const statusIconMap: Record<SellerOrderListStatusEnum, React.ReactNode> = {
|
||
pending: <DiamondPlus size={16} className={"text-blue-600"} />,
|
||
confirmed: <AlarmClockCheck size={16} className={"text-blue-600"} />,
|
||
shipped: <CircleCheckBig size={16} className={"text-blue-600"} />,
|
||
delivered: <PackageCheck size={16} className={"text-green-600"} />,
|
||
cancelled: <CircleX size={16} className={"text-red-600"} />,
|
||
};
|
||
|
||
// Tab configuration
|
||
const orderTabs = [
|
||
{
|
||
key: "all",
|
||
label: "همه",
|
||
statuses: ["pending", "confirmed", "shipped", "delivered", "cancelled"],
|
||
icon: <LayoutGrid />,
|
||
},
|
||
{
|
||
key: "pending",
|
||
label: "جدید",
|
||
statuses: ["pending"],
|
||
icon: <DiamondPlus />,
|
||
},
|
||
{
|
||
key: "confirmed",
|
||
label: "آماده ارسال",
|
||
statuses: ["confirmed"],
|
||
icon: <AlarmClockCheck />,
|
||
},
|
||
{
|
||
key: "shipped-delivered",
|
||
label: "ارسال شده",
|
||
statuses: ["shipped", "delivered"],
|
||
icon: <CircleCheckBig />,
|
||
},
|
||
];
|
||
|
||
function OrderItem({
|
||
order,
|
||
isLastOrder,
|
||
storeId,
|
||
}: {
|
||
order: SellerOrderList;
|
||
isLastOrder: boolean;
|
||
storeId: string;
|
||
}) {
|
||
const navigate = useNavigate();
|
||
return (
|
||
<div
|
||
onClick={() => {
|
||
navigate(`/store/${storeId}/orders/${order.id}`);
|
||
}}
|
||
role="button"
|
||
tabIndex={0}
|
||
onKeyDown={(e) => {
|
||
if (e.key === "Enter") {
|
||
navigate(`/store/${storeId}/orders/${order.id}`);
|
||
}
|
||
}}
|
||
className={`border-t ${isLastOrder && "border-b"} p-2 bg-WHITE hover:bg-WHITE3 cursor-pointer transition-colors duration-300`}
|
||
>
|
||
<div className="flex flex-col space-y-3">
|
||
{/* Header */}
|
||
<div className="flex justify-between items-start w-full">
|
||
<div className="flex gap-1 items-center text-R12 shrink-0">
|
||
{statusIconMap[order.status || "pending"]}
|
||
{statusMap[order.status || "pending"]}
|
||
</div>
|
||
<div className="flex flex-col w-full gap-1 text-left">
|
||
<p className="text-R12 font-medium text-GRAY">
|
||
شماره سفارش: {order.invoiceNumber}
|
||
</p>
|
||
<p className="text-R12 font-medium text-GRAY">
|
||
{order.createdAt &&
|
||
`${formatDate(order.createdAt)} - ${formatTime(order.createdAt)}`}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<p className="text-R12 line-clamp-1">
|
||
{order.productNames?.join(", ")}
|
||
</p>
|
||
<div className="flex justify-between items-start">
|
||
<p className="text-R12 text-GRAY">
|
||
مبلغ فاکتور: {formatPrice(order.totalPrice)} تومان
|
||
</p>
|
||
</div>
|
||
<div className="flex justify-between items-start">
|
||
<p className="text-R12 text-GRAY">
|
||
آدرس: {order.shippingInfo?.address?.mainAddress || "آدرس ثبت نشده"}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export default function Orders() {
|
||
const { data: orders, isLoading, error } = useGetSellerOrders();
|
||
const [activeTab, setActiveTab] = useState("all");
|
||
const [searchQuery, setSearchQuery] = useState("");
|
||
const { storeId } = useLoaderData<typeof loader>();
|
||
const navigate = useNavigate();
|
||
// Check store ownership - this will redirect if user doesn't own the store
|
||
const { isLoading: isOwnershipLoading } = useStoreOwnership(storeId);
|
||
|
||
// Show loading while checking ownership
|
||
if (isOwnershipLoading) {
|
||
return (
|
||
<div className="flex items-center justify-center min-h-[500px] bg-WHITE">
|
||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
||
</div>
|
||
);
|
||
}
|
||
// Filter orders based on active tab
|
||
const filteredOrders =
|
||
orders?.filter((order) => {
|
||
const activeTabConfig = orderTabs.find((tab) => tab.key === activeTab);
|
||
const matchesTab =
|
||
activeTab === "all" ||
|
||
(activeTabConfig &&
|
||
order.status &&
|
||
activeTabConfig.statuses.includes(order.status));
|
||
const matchesSearch =
|
||
searchQuery === "" ||
|
||
order.invoiceNumber?.toString().includes(searchQuery) ||
|
||
order.buyerName?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||
order?.productNames
|
||
?.join(" ")
|
||
?.toLowerCase()
|
||
.includes(searchQuery.toLowerCase());
|
||
|
||
return matchesTab && matchesSearch;
|
||
}) || [];
|
||
|
||
if (isLoading) {
|
||
return (
|
||
<div className="min-h-[500px] bg-WHITE">
|
||
<ProfilePagesHeader title="سفارشهای فروشگاه" />
|
||
<div className="flex justify-center items-center p-8">
|
||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (error) {
|
||
return (
|
||
<div className="min-h-[500px] bg-WHITE">
|
||
<ProfilePagesHeader title="سفارشهای فروشگاه" />
|
||
<div className="p-4">
|
||
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
|
||
<p className="text-red-700 text-center">
|
||
خطا در بارگذاری سفارشات. لطفا دوباره تلاش کنید.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
const handleBack = () => {
|
||
safeGoBack(navigate);
|
||
};
|
||
|
||
return (
|
||
<div className="bg-WHITE">
|
||
{/* Header */}
|
||
<HeaderWithSupport title="سفارشهای دریافتی" onBack={handleBack} />
|
||
|
||
<div className="py-4">
|
||
{/* Tabs */}
|
||
<div className="bg-WHITE px-4 mb-4 overflow-hidden">
|
||
<div className="overflow-x-auto grid grid-cols-4">
|
||
{orderTabs.map((tab) => (
|
||
<button
|
||
key={tab.key}
|
||
onClick={() => setActiveTab(tab.key)}
|
||
className={`flex-shrink-0 flex flex-col items-center justify-center gap-1 py-1 text-R12 font-medium border-b-2 transition-colors ${
|
||
activeTab === tab.key
|
||
? ""
|
||
: "border-transparent text-GRAY hover:text-BLACK2 hover:bg-WHITE3"
|
||
}`}
|
||
>
|
||
{tab.icon}
|
||
{tab.label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Search Bar */}
|
||
<div className="mb-4 px-4">
|
||
<div className="relative">
|
||
<input
|
||
type="text"
|
||
placeholder="جستجو..."
|
||
value={searchQuery}
|
||
onChange={(e) => setSearchQuery(e.target.value)}
|
||
className="w-full h-10 px-3 py-2 pr-10 bg-WHITE2 border border-WHITE2 rounded-lg focus:outline-none focus:ring-2 focus:ring-gray-200 focus:border-transparent"
|
||
/>
|
||
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
|
||
<Search size={24} className="text-BLACK2" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Orders List */}
|
||
{filteredOrders.length > 0 ? (
|
||
<div className="space-y-4">
|
||
{filteredOrders.map((order, index) => (
|
||
<OrderItem
|
||
key={order.id}
|
||
order={order}
|
||
isLastOrder={index === filteredOrders.length - 1}
|
||
storeId={storeId}
|
||
/>
|
||
))}
|
||
</div>
|
||
) : (
|
||
<div className="text-center py-12 px-4">
|
||
<div className="bg-WHITE rounded-lg p-8 mx-auto max-w-sm">
|
||
<div className="text-GRAY text-6xl mb-4">📦</div>
|
||
<h3 className="text-lg font-medium text-BLACK mb-2">
|
||
{searchQuery ? "سفارشی یافت نشد" : "هیچ سفارشی موجود نیست"}
|
||
</h3>
|
||
<p className="text-GRAY text-sm">
|
||
{searchQuery
|
||
? "سفارشی با این مشخصات پیدا نشد. جستجوی دیگری امتحان کنید."
|
||
: "هنوز هیچ سفارشی در این بخش ثبت نشده است."}
|
||
</p>
|
||
{searchQuery && (
|
||
<button
|
||
onClick={() => setSearchQuery("")}
|
||
className="mt-3 text-blue-600 hover:text-blue-800 text-sm font-medium"
|
||
>
|
||
پاک کردن جستجو
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|