Vitron-Front/app/routes/store.$storeId.orders._index.tsx
Arda Samadi 257871d5f1 feat(seller-dashboard): desktop-friendly layouts for list and hub pages
Redesign mobile-style seller dashboard pages for desktop (lg:) while
leaving the mobile view unchanged:

- products: table-style header row (product/price/variant/actions) with a
  bordered card wrapper and an "add product" button; accordion rows align
  to the columns, inline price hidden on desktop.
- orders: pill-style status tabs + search on one row, orders shown as a
  two-column card grid instead of stretched full-width rows.
- financial-dashboard: three-column card grid with icon chips.
- setting: status/theme as side-by-side cards, menu items as a
  three-column card grid (logout styled in red).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 13:00:18 +03:30

326 lines
12 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 lg:border lg:border-WHITE3 lg:rounded-2xl lg:p-5 lg:hover:shadow-md lg:hover:bg-WHITE`}
>
<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 lg:bg-transparent">
{/* Header */}
<div className="lg:hidden">
<HeaderWithSupport title="سفارش‌های دریافتی" onBack={handleBack} />
</div>
<h1 className="hidden lg:block text-[24px] font-extrabold mt-2">
سفارشهای دریافتی
</h1>
<div className="py-4">
{/* Tabs + Search row (desktop puts them side by side) */}
<div className="lg:flex lg:items-center lg:justify-between lg:gap-4 lg:mb-4">
{/* Tabs */}
<div className="bg-WHITE lg:bg-transparent px-4 lg:px-0 mb-4 lg:mb-0 overflow-hidden">
<div className="overflow-x-auto grid grid-cols-4 lg:flex lg:gap-2 lg:w-auto">
{orderTabs.map((tab) => (
<button
key={tab.key}
onClick={() => setActiveTab(tab.key)}
className={`flex-shrink-0 flex flex-col lg:flex-row items-center justify-center gap-1 lg:gap-2 py-1 lg:py-2 lg:px-4 text-R12 lg:text-[13px] font-medium border-b-2 lg:border-b-0 lg:rounded-xl transition-colors lg:[&_svg]:size-4 ${
activeTab === tab.key
? "lg:bg-BLACK lg:text-white"
: "border-transparent text-GRAY hover:text-BLACK2 hover:bg-WHITE3 lg:border lg:border-WHITE3"
}`}
>
{tab.icon}
{tab.label}
</button>
))}
</div>
</div>
{/* Search Bar */}
<div className="mb-4 lg:mb-0 px-4 lg:px-0 lg:w-72 lg:shrink-0">
<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>
</div>
{/* Orders List */}
{filteredOrders.length > 0 ? (
<div className="space-y-4 lg:space-y-0 lg:grid lg:grid-cols-2 lg:gap-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>
);
}