Merge pull request 'feature/WNPT-737' (#5) from feature/WNPT-737 into develop
Reviewed-on: #5
This commit is contained in:
commit
228d9cd546
22 changed files with 779 additions and 43 deletions
10
.idea/.gitignore
generated
vendored
Normal file
10
.idea/.gitignore
generated
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
|
# Ignored default folder with query files
|
||||||
|
/queries/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
6
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
6
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
</profile>
|
||||||
|
</component>
|
||||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/winofy-front.iml" filepath="$PROJECT_DIR$/.idea/winofy-front.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
8
.idea/winofy-front.iml
generated
Normal file
8
.idea/winofy-front.iml
generated
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="WEB_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
3
public/server-error-icon.svg
Normal file
3
public/server-error-icon.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="56" height="56" viewBox="0 0 56 56" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M14.0027 38.0953C11.5441 36.9277 9.41619 35.164 7.81244 32.9647C6.2087 30.7653 5.17996 28.2 4.81972 25.5018C4.45948 22.8037 4.77916 20.0582 5.74972 17.5151C6.72028 14.9719 8.31093 12.7117 10.3771 10.9398C12.4433 9.16786 14.9194 7.94048 17.5806 7.36917C20.2417 6.79787 23.0034 6.90076 25.6146 7.66849C28.2259 8.43623 30.6039 9.84446 32.5325 11.7652C34.4612 13.6859 35.8793 16.0583 36.658 18.6666H40.8344C43.521 18.6619 46.1072 19.6875 48.0607 21.5321C50.0142 23.3768 51.1863 25.9003 51.3358 28.5831C51.4852 31.2659 50.6007 33.904 48.8642 35.9543C47.1278 38.0047 44.6715 39.3112 42.0009 39.6051M30.3342 28L23.3347 39.6675H32.6674L25.6679 51.335" stroke="#EF4444" stroke-width="2.5" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 817 B |
43
src/app/(customer)/(shop)/orders/(routes)/(landing)/page.tsx
Normal file
43
src/app/(customer)/(shop)/orders/(routes)/(landing)/page.tsx
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useOrders } from "@/lib/providers/orders-provider";
|
||||||
|
import OrderItem from "@/components/orders/order-item";
|
||||||
|
import OrderEmpty from "@/components/orders/order-empty";
|
||||||
|
import OrderServerError from "@/components/orders/order-server-error";
|
||||||
|
import OrdersSkeleton from "@/components/orders/orders-skeleton";
|
||||||
|
|
||||||
|
export default function OrdersPage() {
|
||||||
|
const { data, error, isLoading } = useOrders();
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<section className="p-4">
|
||||||
|
<OrdersSkeleton />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<section className="p-4">
|
||||||
|
<OrderServerError />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data || data.length === 0) {
|
||||||
|
return (
|
||||||
|
<section className="p-4">
|
||||||
|
<OrderEmpty />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="p-4 space-y-3">
|
||||||
|
{data.map((group) => (
|
||||||
|
<OrderItem key={group.uuid} orderGroup={group} />
|
||||||
|
))}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
29
src/app/(customer)/(shop)/orders/(routes)/[orderId]/page.tsx
Normal file
29
src/app/(customer)/(shop)/orders/(routes)/[orderId]/page.tsx
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { requireSession } from "@/lib/auth/session";
|
||||||
|
import { getOrderGroup } from "@/lib/api/orders";
|
||||||
|
import { OrderDetailProvider } from "@/lib/providers/order-detail-provider";
|
||||||
|
import OrderDetailContent from "@/components/orders/order-detail-content";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
params: Promise<{ orderId: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function OrderDetailPage({ params }: Props) {
|
||||||
|
await requireSession("/login");
|
||||||
|
|
||||||
|
const { orderId } = await params;
|
||||||
|
|
||||||
|
let orderGroup = null;
|
||||||
|
let error: string | null = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
orderGroup = await getOrderGroup(orderId);
|
||||||
|
} catch (e: any) {
|
||||||
|
error = e?.message ?? "خطا در دریافت اطلاعات سفارش";
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OrderDetailProvider order={orderGroup} error={error}>
|
||||||
|
<OrderDetailContent />
|
||||||
|
</OrderDetailProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
30
src/app/(customer)/(shop)/orders/layout.tsx
Normal file
30
src/app/(customer)/(shop)/orders/layout.tsx
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
import OrderHeader from '@/components/orders/order-header';
|
||||||
|
import { requireSession } from "@/lib/auth/session";
|
||||||
|
import { getOrderGroups } from "@/lib/api/orders";
|
||||||
|
import { OrdersProvider } from "@/lib/providers/orders-provider";
|
||||||
|
|
||||||
|
interface OrderLayoutInterface {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function OrderLayout({ children }: OrderLayoutInterface) {
|
||||||
|
await requireSession("/login");
|
||||||
|
|
||||||
|
let orderGroups: Awaited<ReturnType<typeof getOrderGroups>> | null = null;
|
||||||
|
let error: string | null = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
orderGroups = await getOrderGroups();
|
||||||
|
} catch (e: any) {
|
||||||
|
error = e?.message ?? "خطا در دریافت سفارشها";
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="max-w-5xl w-full mx-auto p-0">
|
||||||
|
<OrdersProvider orderGroups={orderGroups} error={error}>
|
||||||
|
<OrderHeader />
|
||||||
|
{children}
|
||||||
|
</OrdersProvider>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
import Link from "next/link";
|
|
||||||
import { requireSession } from "@/lib/auth/session";
|
|
||||||
import { getOrderGroups } from "@/lib/api/orders";
|
|
||||||
import { PAYMENT_STATUS_LABELS_FA } from "@/lib/order-status";
|
|
||||||
|
|
||||||
export default async function OrdersPage() {
|
|
||||||
await requireSession("/orders");
|
|
||||||
const orderGroups = await getOrderGroups();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<main className="mx-auto flex w-full max-w-2xl flex-1 flex-col gap-4 px-4 py-6">
|
|
||||||
<h1 className="text-lg font-bold">سفارشهای من</h1>
|
|
||||||
|
|
||||||
{orderGroups.length === 0 && <p className="text-sm text-muted-foreground">هنوز سفارشی ثبت نکردهاید.</p>}
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-3">
|
|
||||||
{orderGroups.map((group) => (
|
|
||||||
<Link
|
|
||||||
key={group.uuid}
|
|
||||||
href={`/order-groups/${group.uuid}`}
|
|
||||||
className="flex flex-col gap-2 rounded-2xl border border-border p-4"
|
|
||||||
>
|
|
||||||
<div className="flex justify-between text-sm text-muted-foreground">
|
|
||||||
<span>{new Date(group.created_at).toLocaleDateString("fa-IR")}</span>
|
|
||||||
<span>{PAYMENT_STATUS_LABELS_FA[group.payment_status]}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-wrap gap-1 text-sm">
|
|
||||||
{group.orders.map((order) => (
|
|
||||||
<span key={order.uuid} className="rounded-full bg-muted px-2 py-0.5">
|
|
||||||
{order.store.name}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between font-semibold">
|
|
||||||
<span>مجموع</span>
|
|
||||||
<span>{group.total_amount.toLocaleString("fa-IR")} تومان</span>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
63
src/components/orders/order-detail-content.tsx
Normal file
63
src/components/orders/order-detail-content.tsx
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useOrderDetail } from "@/lib/providers/order-detail-provider";
|
||||||
|
import OrderStepperStatus from "./order-stepper-status";
|
||||||
|
import OrderTimeAddressInfoAlert from "./order-time-address-info-alert";
|
||||||
|
import OrderItems from "./order-items";
|
||||||
|
import OrderPaymentSticky from "./order-payment-sticky";
|
||||||
|
|
||||||
|
const STATUS_LABELS: Record<string, string> = {
|
||||||
|
placed: "ثبت شد",
|
||||||
|
preparing: "در حال آمادهسازی",
|
||||||
|
ready_to_ship: "آماده ارسال",
|
||||||
|
handed_to_courier: "ارسال شد",
|
||||||
|
delivered: "تحویل داده شد",
|
||||||
|
cancelled: "لغو شد",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function OrderDetailContent() {
|
||||||
|
const { data: orderGroup, error, isLoading } = useOrderDetail();
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <div className="p-8 text-center text-gray-500">در حال بارگذاری...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error || !orderGroup) {
|
||||||
|
return (
|
||||||
|
<div className="p-8 text-center text-destructive">
|
||||||
|
{error ?? "سفارش یافت نشد"}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// وضعیت اصلی رو از اولین سفارش میگیریم (یا میتونی منطق پیچیدهتری بذاری)
|
||||||
|
const mainStatus = orderGroup.orders[0]?.status ?? "placed";
|
||||||
|
|
||||||
|
const statusToStep: Record<string, number> = {
|
||||||
|
placed: 0,
|
||||||
|
preparing: 2,
|
||||||
|
ready_to_ship: 3,
|
||||||
|
handed_to_courier: 3,
|
||||||
|
delivered: 4,
|
||||||
|
cancelled: -1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const currentStep = statusToStep[mainStatus] ?? 0;
|
||||||
|
const statusLabel = STATUS_LABELS[mainStatus] ?? mainStatus;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="relative pb-32">
|
||||||
|
<h2 className="w-full p-3 px-4 bg-primary-action text-primary-foreground text-md font-medium">
|
||||||
|
{statusLabel}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{mainStatus !== "cancelled" && (
|
||||||
|
<OrderStepperStatus currentStep={currentStep} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<OrderTimeAddressInfoAlert order={orderGroup} />
|
||||||
|
<OrderItems order={orderGroup} />
|
||||||
|
<OrderPaymentSticky order={orderGroup} />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
19
src/components/orders/order-empty.tsx
Normal file
19
src/components/orders/order-empty.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { ShoppingBagIcon } from 'lucide-react'
|
||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
const OrderEmpty = () => {
|
||||||
|
return (
|
||||||
|
<div className='h-96 flex items-center justify-center p-5'>
|
||||||
|
<div className='max-w-xl w-full p-5 flex items-center justify-center flex-col gap-3'>
|
||||||
|
<div className='size-30 mb-2 bg-gray-100 flex items-center justify-center rounded-full'>
|
||||||
|
<ShoppingBagIcon className='size-12 text-gray-600' />
|
||||||
|
</div>
|
||||||
|
<h1 className='text-lg font-bold'>هنوز سفارشی ثبت نکردهاید</h1>
|
||||||
|
<p className='text-sm text-center text-gray-600'>لیست سفارشهای شما در حال حاضر خالی است. برای خرید از فروشگاههای اطراف روی دکمه زیر کلیک کنید.</p>
|
||||||
|
<Link href='/' className='h-12 mt-4 rounded-full flex items-center justify-center w-full bg-primary-action text-white cursor-pointer'>شروع خرید</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OrderEmpty
|
||||||
44
src/components/orders/order-header.tsx
Normal file
44
src/components/orders/order-header.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { ArrowRight, ShoppingBagIcon } from 'lucide-react';
|
||||||
|
import { usePathname, useRouter } from 'next/navigation';
|
||||||
|
|
||||||
|
const OrderHeader = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const pathname = usePathname();
|
||||||
|
const isExactOrders = pathname === '/orders';
|
||||||
|
|
||||||
|
const onBackHandler = () => {
|
||||||
|
router.back();
|
||||||
|
};
|
||||||
|
|
||||||
|
const goToCartPage = () => {
|
||||||
|
router.push('/cart');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header className="flex items-center justify-between w-full mb-3 px-4 pt-3">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<ArrowRight className="size-5 cursor-pointer" onClick={onBackHandler} />
|
||||||
|
<h2 className="text-lg font-bold">سفارشها</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isExactOrders && (
|
||||||
|
<div
|
||||||
|
className="relative cursor-pointer inline-block"
|
||||||
|
onClick={goToCartPage}
|
||||||
|
>
|
||||||
|
<div className="size-10 rounded-full flex items-center justify-center">
|
||||||
|
<ShoppingBagIcon className="size-6" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span className="absolute -top-1 -right-1 flex items-center justify-center min-w-4 h-4 px-1 bg-primary-action text-primary-foreground text-[10px] font-bold rounded-full">
|
||||||
|
90
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OrderHeader;
|
||||||
127
src/components/orders/order-item.tsx
Normal file
127
src/components/orders/order-item.tsx
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import type { OrderGroup } from "@/types/api";
|
||||||
|
import { PAYMENT_STATUS_LABELS_FA } from "@/lib/order-status"; // اگه داری
|
||||||
|
|
||||||
|
interface OrderItemProps {
|
||||||
|
orderGroup: OrderGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
const OrderItem = ({ orderGroup }: OrderItemProps) => {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const goToOrderDetail = () => {
|
||||||
|
router.push(`/orders/${orderGroup.uuid}`); // یا مسیر واقعی جزئیات
|
||||||
|
};
|
||||||
|
|
||||||
|
// وضعیت کلی (از اولین سفارش یا payment_status)
|
||||||
|
const mainOrder = orderGroup.orders[0];
|
||||||
|
const status = mainOrder?.status ?? "placed";
|
||||||
|
|
||||||
|
// مپ وضعیت به فارسی و کلاس
|
||||||
|
const statusMap: Record<string, { label: string; className: string }> = {
|
||||||
|
placed: { label: "ثبت شده", className: "bg-[#FFF4E5] text-[#E67E22]" },
|
||||||
|
preparing: { label: "در حال آمادهسازی", className: "bg-[#E5F3FF] text-[#007BFF]" },
|
||||||
|
ready_to_ship: { label: "آماده ارسال", className: "bg-[#EBE5FF] text-[#734DBF]" },
|
||||||
|
handed_to_courier: { label: "ارسال شد", className: "bg-teal-bg text-primary-action" },
|
||||||
|
delivered: { label: "تحویل داده شد", className: "bg-emerald-bg text-success" },
|
||||||
|
cancelled: { label: "لغو شد", className: "bg-[#FCE8E8] text-error" },
|
||||||
|
};
|
||||||
|
|
||||||
|
const currentStatus = statusMap[status] ?? { label: status, className: "bg-gray-100 text-gray-700" };
|
||||||
|
|
||||||
|
// نام فروشگاهها
|
||||||
|
const storeNames = orderGroup.orders.map(o => o.store.name).join(" + ");
|
||||||
|
|
||||||
|
// تعداد کل کالاها
|
||||||
|
const totalItems = orderGroup.orders.reduce((sum, order) => {
|
||||||
|
return sum + order.items.reduce((s, item) => s + item.quantity, 0);
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
// تصاویر محصولات (حداکثر ۵ تا)
|
||||||
|
const images = orderGroup.orders
|
||||||
|
.flatMap(order => order.items)
|
||||||
|
.slice(0, 5)
|
||||||
|
.map((item, index) => ({
|
||||||
|
id: index,
|
||||||
|
src: "", // اگر تصویر محصول داری اینجا بذار، فعلاً خالی
|
||||||
|
alt: item.product_name_snapshot,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// تاریخ و ساعت
|
||||||
|
const createdDate = new Date(orderGroup.created_at);
|
||||||
|
const timeText = createdDate.toLocaleDateString("fa-IR", {
|
||||||
|
weekday: "long",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full rounded-2xl min-h-32 border border-gray-300 bg-background p-4 mb-2">
|
||||||
|
{/* هدر */}
|
||||||
|
<div className="flex items-center justify-between w-full gap-3">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="size-12 rounded-full bg-gradient-to-br from-primary-action to-primary flex items-center justify-center text-primary-foreground font-bold text-lg overflow-hidden">
|
||||||
|
{mainOrder?.store.logo_url ? (
|
||||||
|
<img src={mainOrder.store.logo_url} alt={mainOrder.store.name} className="w-full h-full object-cover" />
|
||||||
|
) : (
|
||||||
|
mainOrder?.store.name?.charAt(0) ?? "س"
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-start">
|
||||||
|
<h2 className="text-md font-bold text-gray-800 line-clamp-1">{storeNames}</h2>
|
||||||
|
<p className="text-sm text-gray-600 line-clamp-1">
|
||||||
|
{orderGroup.orders.length > 1
|
||||||
|
? `${orderGroup.orders.length} فروشگاه`
|
||||||
|
: mainOrder?.items[0]?.product_name_snapshot ?? "سفارش"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span className={`flex items-center justify-center px-3 py-1 rounded-full text-xs font-bold ${currentStatus.className}`}>
|
||||||
|
{currentStatus.label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full h-px bg-gray-200 my-4" />
|
||||||
|
|
||||||
|
{/* پایین کارت */}
|
||||||
|
<div className="grid grid-cols-3 gap-3 w-full">
|
||||||
|
<div className="col-span-2 flex items-center gap-1 overflow-x-auto pb-2">
|
||||||
|
{images.length > 0 ? (
|
||||||
|
images.map((img) => (
|
||||||
|
<div key={img.id} className="size-10 flex-shrink-0 rounded-lg bg-gray-200 overflow-hidden">
|
||||||
|
{img.src ? (
|
||||||
|
<img src={img.src} alt={img.alt} className="w-full h-full object-cover" />
|
||||||
|
) : (
|
||||||
|
<div className="w-full h-full flex items-center justify-center text-xs text-gray-400">
|
||||||
|
{img.alt.charAt(0)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="text-sm text-gray-500">بدون تصویر</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-span-1 flex flex-col items-end gap-1">
|
||||||
|
<div className="text-xs text-gray-600">{timeText}</div>
|
||||||
|
<div className="text-primary-action text-sm font-bold text-left">
|
||||||
|
{totalItems} کالا • {orderGroup.total_amount.toLocaleString("fa-IR")} تومان
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="w-full mt-6 rounded-xl h-12 bg-primary-action text-primary-foreground cursor-pointer hover:bg-primary-action/90 transition-colors font-medium"
|
||||||
|
onClick={goToOrderDetail}
|
||||||
|
>
|
||||||
|
مشاهده وضعیت
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OrderItem;
|
||||||
45
src/components/orders/order-items.tsx
Normal file
45
src/components/orders/order-items.tsx
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import type { OrderGroup } from "@/types/api";
|
||||||
|
import {formatTomanPrice} from "@/lib/utils/priceUtils";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
order: OrderGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
const OrderItems = ({ order }: Props) => {
|
||||||
|
// همه آیتمهای همه فروشگاهها رو جمع میکنیم
|
||||||
|
const allItems = order.orders.flatMap((o) =>
|
||||||
|
o.items.map((item) => ({
|
||||||
|
...item,
|
||||||
|
storeName: o.store.name,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-5 grid grid-cols-1 gap-3 mx-8">
|
||||||
|
<h1 className="text-lg font-bold mb-3">اقلام سفارش</h1>
|
||||||
|
|
||||||
|
{allItems.map((item) => (
|
||||||
|
<div key={item.uuid} className="flex items-center justify-between w-full">
|
||||||
|
<div className="flex flex-col gap-0.5">
|
||||||
|
<div className="flex gap-3 items-center">
|
||||||
|
<span className="font-semibold text-sm">{item.product_name_snapshot}</span>
|
||||||
|
<span className="text-xs font-medium text-gray-700">
|
||||||
|
{item.quantity.toLocaleString("fa-IR")} عدد
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{order.orders.length > 1 && (
|
||||||
|
<span className="text-xs text-gray-500">{item.storeName}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span className="text-sm font-bold">
|
||||||
|
{formatTomanPrice(item.line_total)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OrderItems;
|
||||||
48
src/components/orders/order-payment-sticky.tsx
Normal file
48
src/components/orders/order-payment-sticky.tsx
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import type { OrderGroup } from "@/types/api";
|
||||||
|
import {formatTomanPrice} from "@/lib/utils/priceUtils";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
order: OrderGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
const OrderPaymentSticky = ({ order }: Props) => {
|
||||||
|
const paymentMethodLabel: Record<string, string> = {
|
||||||
|
wallet: "پرداخت از کیف پول",
|
||||||
|
online: "پرداخت آنلاین",
|
||||||
|
cash_on_delivery: "پرداخت در محل",
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed bottom-0 left-0 right-0 z-50 bg-background border-t border-gray-100 w-full">
|
||||||
|
<div className="px-5 pt-5 pb-3 max-w-sm mx-auto w-full">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<div className="flex flex-col items-start">
|
||||||
|
<span className="text-lg font-medium text-foreground">مبلغ پرداختی</span>
|
||||||
|
<span className="text-xs font-normal text-gray-700">
|
||||||
|
{paymentMethodLabel[order.payment_method] ?? order.payment_method}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-lg font-medium text-foreground">
|
||||||
|
{formatTomanPrice(order.total_amount)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* فعلاً فقط نمایش، بعداً میتونی دکمه لغو اضافه کنی */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="w-full h-12 rounded-xl border border-error bg-background text-error text-[15px] font-medium active:scale-[0.98] transition-transform cursor-pointer"
|
||||||
|
>
|
||||||
|
لغو سفارش
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-center pb-2">
|
||||||
|
<div className="w-[134px] h-[5px] rounded-full bg-gray-300" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OrderPaymentSticky;
|
||||||
24
src/components/orders/order-server-error.tsx
Normal file
24
src/components/orders/order-server-error.tsx
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { ServerCrashIcon } from 'lucide-react'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
const OrderServerError = () => {
|
||||||
|
return (
|
||||||
|
<div className="h-96 flex items-center justify-center p-5">
|
||||||
|
<div className="max-w-xl w-full p-5 flex items-center justify-center flex-col gap-3">
|
||||||
|
<div className="size-30 mb-2 bg-[#FEF2F2] flex items-center justify-center rounded-full">
|
||||||
|
{/* <ServerCrashIcon className="size-12 text-error" /> */}
|
||||||
|
<img src="/server-error-icon.svg" alt="وینسو" className="size-16" />
|
||||||
|
</div>
|
||||||
|
<h1 className="text-lg font-bold">خطا در برقراری ارتباط</h1>
|
||||||
|
<p className="text-sm text-center text-gray-600">
|
||||||
|
متأسفانه در دریافت اطلاعات سفارشها مشکلی پیش آمده است. لطفاً اتصال اینترنت خود را بررسی کرده و مجدداً تلاش کنید.
|
||||||
|
</p>
|
||||||
|
<button className="h-12 mt-4 rounded-full w-full border border-primary-action bg-background text-primary-action font-bold cursor-pointer">
|
||||||
|
تلاش مجدد
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OrderServerError
|
||||||
100
src/components/orders/order-stepper-status.tsx
Normal file
100
src/components/orders/order-stepper-status.tsx
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { Check } from 'lucide-react'
|
||||||
|
|
||||||
|
type StepStatus = 'completed' | 'current' | 'upcoming'
|
||||||
|
|
||||||
|
interface Step {
|
||||||
|
label: string
|
||||||
|
status: StepStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OrderStepperStatusProps {
|
||||||
|
/** ایندکس مرحله فعلی (۰-based). مراحل قبل completed، خودش current، بعد upcoming */
|
||||||
|
currentStep?: number
|
||||||
|
steps?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_STEPS = [
|
||||||
|
'ثبت شد',
|
||||||
|
'پذیرفته شد',
|
||||||
|
'آمادهسازی',
|
||||||
|
'ارسال شد',
|
||||||
|
'تحویل',
|
||||||
|
]
|
||||||
|
|
||||||
|
const OrderStepperStatus = ({
|
||||||
|
currentStep = 2,
|
||||||
|
steps = DEFAULT_STEPS,
|
||||||
|
}: OrderStepperStatusProps) => {
|
||||||
|
const computedSteps: Step[] = steps.map((label, index) => {
|
||||||
|
if (index < currentStep) return { label, status: 'completed' }
|
||||||
|
if (index === currentStep) return { label, status: 'current' }
|
||||||
|
return { label, status: 'upcoming' }
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full px-8 py-6" dir="rtl">
|
||||||
|
<div className="flex items-center w-full">
|
||||||
|
{computedSteps.map((step, index) => {
|
||||||
|
const isLast = index === computedSteps.length - 1
|
||||||
|
const isCompleted = step.status === 'completed'
|
||||||
|
const isCurrent = step.status === 'current'
|
||||||
|
|
||||||
|
const circleBg = step.status === 'upcoming' ? 'bg-gray-300' : 'bg-primary-action'
|
||||||
|
const nextLineBg =
|
||||||
|
index < currentStep ? 'bg-primary-action' : 'bg-gray-300'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment key={index}>
|
||||||
|
<div
|
||||||
|
className={`
|
||||||
|
size-6 shrink-0 rounded-full flex items-center justify-center
|
||||||
|
${circleBg}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{isCompleted && (
|
||||||
|
<Check className="size-3.5 text-primary-foreground stroke-[3]" />
|
||||||
|
)}
|
||||||
|
{isCurrent && (
|
||||||
|
<div className="size-2 rounded-full bg-background" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!isLast && (
|
||||||
|
<div className={`flex-1 h-0.5 ${nextLineBg}`} />
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex w-full mt-3">
|
||||||
|
{computedSteps.map((step, index) => {
|
||||||
|
const isLast = index === computedSteps.length - 1
|
||||||
|
const textColor =
|
||||||
|
step.status === 'upcoming'
|
||||||
|
? 'text-gray-500'
|
||||||
|
: 'text-primary-action'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment key={index}>
|
||||||
|
<div className="w-6 shrink-0 flex justify-center">
|
||||||
|
<span
|
||||||
|
className={`
|
||||||
|
text-[11px] font-medium text-center leading-tight
|
||||||
|
w-16 -mx-5 ${textColor}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{step.label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{!isLast && <div className="flex-1" />}
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OrderStepperStatus
|
||||||
35
src/components/orders/order-time-address-info-alert.tsx
Normal file
35
src/components/orders/order-time-address-info-alert.tsx
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { ClockIcon, MapPinIcon } from "lucide-react";
|
||||||
|
import type { OrderGroup } from "@/types/api";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
order: OrderGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
const OrderTimeAddressInfoAlert = ({ order }: Props) => {
|
||||||
|
// زمان تخمینی (فعلاً ساده)
|
||||||
|
const estimatedTime =
|
||||||
|
order.delivery_type === "express"
|
||||||
|
? "۳۰–۴۵ دقیقه دیگر"
|
||||||
|
: order.scheduled_at
|
||||||
|
? new Date(order.scheduled_at).toLocaleString("fa-IR")
|
||||||
|
: "زمان مشخص نشده";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-4 items-start p-4 bg-teal-bg rounded-2xl my-4 mx-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<ClockIcon className="size-5 text-primary-action" />
|
||||||
|
<p className="text-md font-bold text-primary-action">
|
||||||
|
زمان تخمینی تحویل: {estimatedTime}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<MapPinIcon className="size-5 text-primary-action" />
|
||||||
|
<p className="text-sm text-gray-700">{order.full_address}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OrderTimeAddressInfoAlert;
|
||||||
47
src/components/orders/orders-skeleton.tsx
Normal file
47
src/components/orders/orders-skeleton.tsx
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const OrdersSkeleton = () => {
|
||||||
|
return (
|
||||||
|
<div className="w-full space-y-2">
|
||||||
|
{Array.from({ length: 10 }).map((_, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="w-full rounded-2xl min-h-32 border border-gray-300 bg-background p-4 animate-pulse"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between w-full gap-3">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="size-12 rounded-full bg-gray-200 flex-shrink-0" />
|
||||||
|
<div className="flex flex-col items-start gap-2">
|
||||||
|
<div className="w-32 h-4 bg-gray-200 rounded" />
|
||||||
|
<div className="w-24 h-3 bg-gray-200 rounded" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-24 h-6 bg-gray-200 rounded-full" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full h-px bg-gray-200 my-4" />
|
||||||
|
|
||||||
|
<div className="grid grid-cols-3 gap-3 w-full">
|
||||||
|
<div className="col-span-2 flex items-center gap-1 overflow-x-auto pb-2">
|
||||||
|
{Array.from({ length: 3 }).map((_, imgIndex) => (
|
||||||
|
<div
|
||||||
|
key={imgIndex}
|
||||||
|
className="size-10 flex-shrink-0 rounded-lg bg-gray-200"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-span-1 flex flex-col items-end gap-2">
|
||||||
|
<div className="w-20 h-3 bg-gray-200 rounded" />
|
||||||
|
<div className="w-28 h-4 bg-gray-200 rounded" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full mt-8 rounded-xl h-12 bg-gray-200" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OrdersSkeleton;
|
||||||
42
src/lib/providers/order-detail-provider.tsx
Normal file
42
src/lib/providers/order-detail-provider.tsx
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import {createContext, useContext, type ReactNode} from "react";
|
||||||
|
import type {Order, OrderGroup} from "@/types/api";
|
||||||
|
|
||||||
|
type OrderDetailContextValue = {
|
||||||
|
data: OrderGroup | null;
|
||||||
|
error: string | null;
|
||||||
|
isLoading: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const OrderDetailContext = createContext<OrderDetailContextValue | undefined>(undefined);
|
||||||
|
|
||||||
|
export function OrderDetailProvider({
|
||||||
|
children,
|
||||||
|
order,
|
||||||
|
error = null,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
order: OrderGroup | null;
|
||||||
|
error?: string | null;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<OrderDetailContext.Provider
|
||||||
|
value={{
|
||||||
|
data: order,
|
||||||
|
error,
|
||||||
|
isLoading: false,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</OrderDetailContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useOrderDetail() {
|
||||||
|
const ctx = useContext(OrderDetailContext);
|
||||||
|
if (ctx === undefined) {
|
||||||
|
throw new Error("useOrderDetail must be used within OrderDetailProvider");
|
||||||
|
}
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
42
src/lib/providers/orders-provider.tsx
Normal file
42
src/lib/providers/orders-provider.tsx
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { createContext, useContext, type ReactNode } from "react";
|
||||||
|
import type { OrderGroup } from "@/types/api";
|
||||||
|
|
||||||
|
type OrdersContextValue = {
|
||||||
|
data: OrderGroup[] | null;
|
||||||
|
error: string | null;
|
||||||
|
isLoading: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const OrdersContext = createContext<OrdersContextValue | undefined>(undefined);
|
||||||
|
|
||||||
|
export function OrdersProvider({
|
||||||
|
children,
|
||||||
|
orderGroups,
|
||||||
|
error = null,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
orderGroups: OrderGroup[] | null;
|
||||||
|
error?: string | null;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<OrdersContext.Provider
|
||||||
|
value={{
|
||||||
|
data: orderGroups,
|
||||||
|
error,
|
||||||
|
isLoading: false,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</OrdersContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useOrders() {
|
||||||
|
const ctx = useContext(OrdersContext);
|
||||||
|
if (ctx === undefined) {
|
||||||
|
throw new Error("useOrders must be used within OrdersProvider");
|
||||||
|
}
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue