Vitron-Front/app/routes/store.$storeId.orders._index.tsx
2026-04-29 01:44:16 +03:30

318 lines
11 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 { 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>
);
}