order list

This commit is contained in:
2026-07-07 21:08:19 +03:30
parent 0889a61514
commit 311f7f55e5
13 changed files with 698 additions and 257 deletions
+27 -4
View File
@@ -1,19 +1,21 @@
import { type FC, useMemo, useState } from "react";
import { type FC, useCallback, useMemo, useState } from "react";
import { Link } from "react-router-dom";
import { AddSquare, ArrowDown2, Filter } from "iconsax-react";
import { toast } from "react-toastify";
import Filters from "@/components/Filters";
import Table from "@/components/Table";
import Tabs from "@/components/Tabs";
import Button from "@/components/Button";
import RefreshButton from "@/components/RefreshButton";
import { Paths } from "@/config/Paths";
import { extractErrorMessage } from "@/config/func";
import { clx } from "@/helpers/utils";
import UserSearch from "../user/UserSearch";
import DesignerSearch from "../designer/DesignerSearch";
import CategoriesSelect from "../product/components/CategoriesSelect";
import { orderListTabs } from "./constants/orderStatus";
import { orderListColumns } from "./components/OrderListColumns";
import { useGetOrders } from "./hooks/useOrderData";
import { getOrderListColumns } from "./components/OrderListColumns";
import { useDeleteOrder, useGetOrders } from "./hooks/useOrderData";
import {
ORDER_LIST_FILTER_FIELDS,
useOrderListParams,
@@ -45,9 +47,30 @@ const OrdersList: FC = () => {
} = useOrderListParams();
const { data, refetch, isFetching } = useGetOrders(queryParams);
const { mutate: deleteOrder, isPending: isDeleting } = useDeleteOrder();
const activeFilterCount = useMemo(() => countActiveFilters(params), [params]);
const hasActiveFilters = activeFilterCount > 0;
const handleDelete = useCallback(
(id: string) => {
deleteOrder(id, {
onSuccess: () => {
refetch();
toast.success("سفارش با موفقیت حذف شد");
},
onError: (error) => {
toast.error(extractErrorMessage(error));
},
});
},
[deleteOrder, refetch],
);
const columns = useMemo(
() => getOrderListColumns({ onDelete: handleDelete, isDeleting }),
[handleDelete, isDeleting],
);
return (
<div className="mt-5 space-y-6">
<div className="flex flex-wrap items-center justify-between gap-4">
@@ -158,7 +181,7 @@ const OrdersList: FC = () => {
</div>
<Table<OrderListItemType>
columns={orderListColumns}
columns={columns}
data={data?.data}
isLoading={isFetching}
noDataMessage="سفارشی یافت نشد"
+64 -164
View File
@@ -1,183 +1,83 @@
import { type FC } from 'react'
import { useParams } from 'react-router-dom'
import moment from 'moment-jalaali'
import { User } from 'iconsax-react'
import { Link, useParams } from 'react-router-dom'
import { Edit2, Printer } from 'iconsax-react'
import BackButton from '@/components/BackButton'
import RefreshButton from '@/components/RefreshButton'
import Button from '@/components/Button'
import { Paths } from '@/config/Paths'
import { useGetOrderDetails } from './hooks/useOrderData'
import TicketSection from './components/TicketSection'
import OrderItem from './components/OrderItem'
import type { OrderDetailDataType, OrderItemType } from './types/Types'
const CARD_CLASS = 'bg-white rounded-2xl border border-border border-opacity-40 shadow-sm overflow-hidden'
const SECTION_TITLE_CLASS = 'text-lg font-light text-black mb-0'
const SECTION_HEADER_CLASS = 'border-b border-border border-opacity-40 px-6 py-5'
const InfoRow: FC<{ label: string; value?: string | number | null }> = ({ label, value }) => (
<div className="flex items-center gap-4 py-3 border-b border-border border-opacity-30 last:border-b-0">
<span className="text-[13px] text-[#8C90A3] min-w-[140px] flex-shrink-0">{label}</span>
<span className="text-sm text-black font-normal">{value ?? '—'}</span>
</div>
)
import OrderDetailSidebar from './components/OrderDetailSidebar'
import InvoiceItemDetailSection from './components/InvoiceItemDetailSection'
import type { OrderDetailDataType } from './types/Types'
const OrderDetail: FC = () => {
const { id } = useParams()
const { data, refetch, isFetching } = useGetOrderDetails(id!)
const order = data?.data as (OrderDetailDataType & { items?: OrderItemType[] }) | undefined
const creator = order?.creator
const creatorName = creator ? `${creator.firstName} ${creator.lastName}`.trim() : ''
const { data, refetch, isFetching, isPending } = useGetOrderDetails(id!)
const order = data?.data as OrderDetailDataType | undefined
if (isPending) {
return (
<div className="mt-5 space-y-6 animate-pulse">
<div className="h-8 w-48 rounded-lg bg-gray-200" />
<div className="flex flex-col-reverse gap-6 xl:flex-row">
<div className="flex-1 space-y-4">
<div className="h-64 rounded-3xl bg-gray-200" />
<div className="h-96 rounded-3xl bg-gray-200" />
</div>
<div className="xl:w-[320px] w-full space-y-4">
<div className="h-80 rounded-3xl bg-gray-200" />
</div>
</div>
</div>
)
}
return (
<div className="w-full min-h-screen bg-[#eceef6] p-6">
{/* Header */}
<header className="flex items-center justify-between mb-6">
<BackButton to={Paths.order.list} />
<div className="flex items-center gap-3">
<div className="mt-5">
<div className="flex flex-wrap items-start justify-between gap-4">
<div className="space-y-3">
<BackButton to={Paths.order.list} />
<div>
<h1 className="text-lg font-light text-gray-900">جزئیات سفارش</h1>
<p className="mt-1 text-sm text-gray-500">
مشاهده اطلاعات سفارش، آیتم پیشفاکتور و گفتگو
</p>
</div>
</div>
<div className="flex flex-wrap items-center gap-2">
<RefreshButton onClick={() => refetch()} isLoading={isFetching} />
<span className="text-sm text-[#8C90A3]">سفارش #{order?.orderNumber}</span>
<Link to={Paths.orderPrint + id}>
<Button className="w-fit px-4 bg-white border border-gray-200 text-gray-800 hover:bg-gray-50">
<div className="flex items-center gap-2">
<Printer size={18} color="currentColor" />
فرم چاپ
</div>
</Button>
</Link>
<Link to={Paths.order.edit + id}>
<Button className="w-fit px-4">
<div className="flex items-center gap-2">
<Edit2 size={18} color="black" />
ویرایش سفارش
</div>
</Button>
</Link>
</div>
</header>
</div>
{/* اطلاعات سفارش */}
<section className={`${CARD_CLASS} mb-6`}>
<div className={SECTION_HEADER_CLASS}>
<h2 className={SECTION_TITLE_CLASS}>اطلاعات سفارش</h2>
<div className="mt-6 flex flex-col-reverse gap-6 xl:flex-row xl:items-start">
<div className="min-w-0 flex-1 space-y-6">
<InvoiceItemDetailSection invoiceItemId={order?.invoiceItem} />
<TicketSection />
</div>
<div className="px-6 py-5">
{/* شماره و وضعیت — برجسته */}
<div className="flex flex-wrap items-center gap-4 mb-5 pb-5 border-b border-border border-opacity-40">
<div className="flex items-center gap-3">
<span className="text-[13px] text-[#8C90A3]">شماره سفارش</span>
<span className="text-xl font-medium text-black tabular-nums">#{order?.orderNumber ?? '—'}</span>
</div>
{order?.status && (
<span className="h-6 inline-flex items-center bg-[#FFEDCA] text-[#FF7B00] rounded-full px-3 text-xs font-medium">
{order.status}
</span>
)}
</div>
{/* جزئیات در گرید منظم */}
<div className="border border-border border-opacity-30 rounded-xl bg-[#fafbfc] overflow-hidden divide-y divide-border divide-opacity-30">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 divide-x divide-border divide-opacity-30">
<div className="px-4 py-3">
<p className="text-[13px] text-[#8C90A3] mb-1">تاریخ ایجاد</p>
<p className="text-sm text-black font-normal">
{order?.createdAt ? moment(order.createdAt).format('jYYYY/jMM/jDD — HH:mm') : '—'}
</p>
</div>
<div className="px-4 py-3">
<p className="text-[13px] text-[#8C90A3] mb-1">تخمین زمان (روز)</p>
<p className="text-sm text-black font-normal">{order?.estimatedDays ?? '—'}</p>
</div>
<div className="px-4 py-3">
<p className="text-[13px] text-[#8C90A3] mb-1">عنوان</p>
<p className="text-sm text-black font-normal line-clamp-2" title={order?.title ?? undefined}>{order?.title ?? '—'}</p>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 divide-x divide-border divide-opacity-30">
<div className="px-4 py-3">
<p className="text-[13px] text-[#8C90A3] mb-1">نوع / دستهبندی</p>
<p className="text-sm text-black font-normal">{order?.type?.title ?? '—'}</p>
</div>
<div className="px-4 py-3">
<p className="text-[13px] text-[#8C90A3] mb-1">محصول</p>
<p className="text-sm text-black font-normal line-clamp-2" title={order?.product?.title ?? undefined}>{order?.product?.title ?? '—'}</p>
</div>
<div className="px-4 py-3">
<p className="text-[13px] text-[#8C90A3] mb-1">شناسه آیتم فاکتور</p>
<p className="text-sm text-black font-normal font-mono text-[13px] break-all">{order?.invoiceItem ?? '—'}</p>
</div>
</div>
</div>
</div>
</section>
{/* اطلاعات مشتری */}
{order?.user && (
<section className={`${CARD_CLASS} mb-6`}>
<div className={SECTION_HEADER_CLASS}>
<h2 className={SECTION_TITLE_CLASS}>اطلاعات مشتری</h2>
</div>
<div className="px-6 py-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-12">
<div>
<InfoRow label="نام و نام‌خانوادگی" value={order.user.firstName ? `${order.user.firstName} ${order.user.lastName ?? ''}`.trim() : undefined} />
<InfoRow label="موبایل" value={order.user.phone} />
</div>
<div>
<InfoRow label="آدرس" value={order.user.addresse} />
</div>
</div>
</div>
</section>
)}
{/* ایجادکننده سفارش — کارت کامل */}
{creator && (
<section className={`${CARD_CLASS} mb-6`}>
<div className={SECTION_HEADER_CLASS}>
<h2 className={SECTION_TITLE_CLASS}>ایجادکننده سفارش</h2>
</div>
<div className="px-6 py-5">
<div className="flex flex-col sm:flex-row gap-6">
{/* آواتار و نام */}
<div className="flex items-center gap-4 flex-shrink-0">
<div className="w-14 h-14 rounded-full bg-[#eceef6] border border-border border-opacity-40 flex items-center justify-center">
<User size={28} className="text-[#8C90A3]" />
</div>
<div>
<p className="text-base font-medium text-black">{creatorName || '—'}</p>
<p className="text-[13px] text-[#8C90A3] mt-0.5">ادمین ثبتکننده</p>
</div>
</div>
</div>
</div>
</section>
)}
{/* پیوست‌ها */}
{order?.attachments?.length ? (
<section className={`${CARD_CLASS} mb-6`}>
<div className={SECTION_HEADER_CLASS}>
<h2 className={SECTION_TITLE_CLASS}>پیوستها</h2>
</div>
<div className="px-6 py-4 flex flex-wrap gap-3">
{order.attachments.map((att, idx) => {
const url = typeof att === 'string' ? att : (att as { url?: string })?.url
if (!url) return null
return (
<a
key={idx}
href={url}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-[#0037FF] hover:underline py-1 px-2 rounded hover:bg-[#eceef6]"
>
پیوست {idx + 1}
</a>
)
})}
</div>
</section>
) : null}
{/* اقلام سفارش */}
{order?.items?.length ? (
<section className={`${CARD_CLASS} mb-6`}>
<div className={SECTION_HEADER_CLASS}>
<h2 className={SECTION_TITLE_CLASS}>اقلام سفارش</h2>
</div>
<div className="p-6 flex flex-col gap-10">
{order.items.map((item) => (
<OrderItem key={item.id} estimatedDays={order.estimatedDays} item={item as unknown as OrderItemType} />
))}
</div>
</section>
) : null}
<TicketSection />
<aside className="w-full shrink-0 xl:sticky xl:top-4 xl:w-[320px]">
<OrderDetailSidebar order={order} />
</aside>
</div>
</div>
)
}
@@ -0,0 +1,150 @@
import { type FC, type ReactNode } from 'react'
import { Link } from 'react-router-dom'
import moment from 'moment-jalaali'
import { DocumentText } from 'iconsax-react'
import { Paths } from '@/config/Paths'
import { useGetInvoiceItem } from '@/pages/invoice/hooks/useInvoiceData'
import { formatItemDiscountDisplay } from '@/pages/invoice/utils/invoiceItem'
import { getUserDisplayName } from '../utils/orderDetailUtils'
type Props = {
invoiceItemId?: string | null
}
const formatAmount = (amount: number) =>
Number(amount).toLocaleString('fa-IR') + ' ریال'
type FieldProps = {
label: string
children: ReactNode
className?: string
}
const Field: FC<FieldProps> = ({ label, children, className }) => (
<div className={className}>
<p className="mb-1.5 text-xs text-gray-500">{label}</p>
<div className="text-sm text-gray-900">{children}</div>
</div>
)
const InvoiceItemDetailSection: FC<Props> = ({ invoiceItemId }) => {
const { data, isLoading, isError } = useGetInvoiceItem(invoiceItemId ?? null)
const invoiceItem = data?.data
const discounts = invoiceItem ? formatItemDiscountDisplay(invoiceItem) : null
const imageUrl = invoiceItem?.product?.images?.[0]
if (!invoiceItemId) {
return (
<section id="invoice-item" className="rounded-3xl bg-white p-8 text-center shadow-sm">
<p className="text-sm text-gray-500">آیتم فاکتوری به این سفارش متصل نیست.</p>
</section>
)
}
if (isLoading) {
return (
<section id="invoice-item" className="overflow-hidden rounded-3xl bg-white shadow-sm animate-pulse">
<div className="border-b border-gray-100 px-5 py-4 md:px-6">
<div className="h-5 w-40 rounded bg-gray-200" />
</div>
<div className="p-5 md:p-6 space-y-4">
<div className="h-24 rounded-2xl bg-gray-100" />
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3">
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="h-14 rounded-xl bg-gray-100" />
))}
</div>
</div>
</section>
)
}
if (isError || !invoiceItem) {
return (
<section id="invoice-item" className="rounded-3xl bg-white p-8 text-center shadow-sm">
<p className="text-sm text-red-500">آیتم پیشفاکتور یافت نشد.</p>
</section>
)
}
return (
<section id="invoice-item" className="overflow-hidden rounded-3xl bg-white shadow-sm">
<div className="border-b border-gray-100 px-5 py-4 md:px-6">
<h2 className="text-base font-medium text-gray-900">جزئیات آیتم پیشفاکتور</h2>
<p className="mt-0.5 text-xs text-gray-500">
اطلاعات مالی و محصول مرتبط با این سفارش
</p>
</div>
<div className="p-5 md:p-6">
<div className="flex flex-col gap-5 md:flex-row">
<div className="size-24 shrink-0 overflow-hidden rounded-xl border border-gray-200 bg-gray-50">
{imageUrl ? (
<img
src={imageUrl}
alt={invoiceItem.product?.title}
className="size-full object-cover"
/>
) : (
<div className="flex size-full items-center justify-center text-gray-300">
<DocumentText size={32} />
</div>
)}
</div>
<div className="min-w-0 flex-1 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
<Field label="پیش‌فاکتور">
<Link
to={Paths.perfomaInvoice.detail + invoiceItem.invoice.id}
className="font-medium text-[#0037FF] hover:underline"
>
#{invoiceItem.invoice.invoiceNumber}
</Link>
</Field>
<Field label="مشتری">
{getUserDisplayName(invoiceItem.invoice.user)}
</Field>
<Field label="محصول">
{invoiceItem.product?.title ?? '—'}
</Field>
<Field label="تعداد">
{invoiceItem.quantity?.toLocaleString('fa-IR') ?? '—'}
</Field>
<Field label="مبلغ واحد">
{invoiceItem.unitPrice != null
? formatAmount(invoiceItem.unitPrice)
: '—'}
</Field>
<Field label="تخفیف (مبلغ)">
{discounts?.value ?? '—'}
</Field>
<Field label="تخفیف (درصد)">
{discounts?.percent ?? '—'}
</Field>
<Field label="مبلغ کل">
<span className="font-semibold">
{formatAmount(invoiceItem.total ?? 0)}
</span>
</Field>
<Field label="تاریخ تایید">
{invoiceItem.confirmedAt
? moment(invoiceItem.confirmedAt).format('jYYYY/jMM/jDD HH:mm')
: '—'}
</Field>
</div>
</div>
{invoiceItem.description && (
<div className="mt-5 rounded-xl border border-dashed border-gray-200 bg-gray-50/60 px-4 py-3">
<p className="text-xs font-medium text-gray-500 mb-1.5">توضیحات</p>
<p className="whitespace-pre-wrap text-sm leading-7 text-gray-800">
{invoiceItem.description}
</p>
</div>
)}
</div>
</section>
)
}
export default InvoiceItemDetailSection
@@ -0,0 +1,198 @@
import { type FC } from 'react'
import { Link } from 'react-router-dom'
import { Call, Edit2, Location, Paperclip2, Profile2User, User } from 'iconsax-react'
import { clx } from '@/helpers/utils'
import { getFileNameAndExtensionFromUrl } from '@/config/func'
import { Paths } from '@/config/Paths'
import {
getAttachmentUrl,
formatJalaliDateTime,
getCreatorDisplayName,
getUserDisplayName,
} from '../utils/orderDetailUtils'
import {
getOrderStatusBadgeClass,
getOrderStatusLabel,
} from '../constants/orderStatus'
import type { OrderDetailDataType } from '../types/Types'
type Props = {
order?: OrderDetailDataType
}
type InfoRowProps = {
label: string
value?: string | number | null
mono?: boolean
}
const InfoRow: FC<InfoRowProps> = ({ label, value, mono }) => (
<div className="flex items-start justify-between gap-3 py-2.5 border-b border-gray-100 last:border-b-0">
<dt className="text-xs text-gray-500 shrink-0">{label}</dt>
<dd
className={clx(
'text-sm text-gray-900 text-left min-w-0 break-words',
mono && 'font-mono text-xs',
)}
title={value != null ? String(value) : undefined}
>
{value ?? '—'}
</dd>
</div>
)
const PersonCard: FC<{
title: string
name: string
subtitle?: string
phone?: string
icon?: 'user' | 'designer'
}> = ({ title, name, subtitle, phone, icon = 'user' }) => (
<div className="rounded-2xl border border-gray-100 bg-gray-50/80 p-4">
<p className="text-xs text-gray-500 mb-3">{title}</p>
<div className="flex items-center gap-3">
<div className="size-11 shrink-0 rounded-full bg-white border border-gray-200 flex items-center justify-center">
{icon === 'designer' ? (
<Profile2User size={22} className="text-gray-400" />
) : (
<User size={22} className="text-gray-400" />
)}
</div>
<div className="min-w-0">
<p className="text-sm font-medium text-gray-900 truncate">{name}</p>
{subtitle && <p className="text-xs text-gray-500 mt-0.5">{subtitle}</p>}
{phone && (
<p className="text-xs text-gray-600 mt-1 flex items-center gap-1 dltr">
<Call size={14} className="text-gray-400 shrink-0" />
{phone}
</p>
)}
</div>
</div>
</div>
)
const OrderDetailSidebar: FC<Props> = ({ order }) => {
const attachments = (order?.attachments ?? [])
.map(getAttachmentUrl)
.filter((url): url is string => Boolean(url))
return (
<div className="space-y-4">
<section className="bg-white rounded-3xl p-5 shadow-sm">
<div className="flex items-start justify-between gap-3">
<div>
<p className="text-xs text-gray-500">شماره سفارش</p>
<p className="mt-1 text-2xl font-medium text-gray-900 tabular-nums">
#{order?.orderNumber ?? '—'}
</p>
</div>
<div className="flex items-center gap-2 shrink-0">
{order?.id && (
<Link
to={Paths.order.edit + order.id}
title="ویرایش سفارش"
className="inline-flex size-8 items-center justify-center rounded-lg border border-gray-200 text-[#0037FF] hover:bg-blue-50 transition-colors"
>
<Edit2 size={18} color="#0037FF" />
</Link>
)}
{order?.status && (
<span
className={clx(
'inline-flex h-7 items-center rounded-full px-3 text-xs font-medium',
getOrderStatusBadgeClass(order.status),
)}
>
{getOrderStatusLabel(order.status)}
</span>
)}
</div>
</div>
<dl className="mt-5">
<InfoRow label="عنوان" value={order?.title} />
<InfoRow
label="تاریخ ایجاد"
value={formatJalaliDateTime(order?.createdAt)}
/>
<InfoRow
label="تخمین زمان"
value={
order?.estimatedDays != null
? `${order.estimatedDays} روز`
: undefined
}
/>
<InfoRow label="نوع / دسته‌بندی" value={order?.type?.title} />
<InfoRow label="محصول" value={order?.product?.title} />
</dl>
</section>
{order?.user && (
<section className="bg-white rounded-3xl p-5 shadow-sm space-y-4">
<h3 className="text-sm font-medium text-gray-900">مشتری</h3>
<PersonCard
title="اطلاعات تماس"
name={getUserDisplayName(order.user)}
phone={order.user.phone}
/>
{order.user.addresse && (
<div className="flex items-start gap-2 text-sm text-gray-700 rounded-2xl border border-gray-100 bg-gray-50/80 p-4">
<Location size={18} className="text-gray-400 shrink-0 mt-0.5" />
<p className="leading-6">{order.user.addresse}</p>
</div>
)}
</section>
)}
{(order?.creator || order?.designer) && (
<section className="bg-white rounded-3xl p-5 shadow-sm space-y-3">
<h3 className="text-sm font-medium text-gray-900">تیم</h3>
{order.creator && (
<PersonCard
title="ایجادکننده"
name={getCreatorDisplayName(order.creator)}
subtitle="ادمین ثبت‌کننده"
phone={order.creator.phone}
/>
)}
{order.designer && (
<PersonCard
title="طراح / مجری"
name={getCreatorDisplayName(order.designer)}
subtitle="مسئول طراحی"
phone={order.designer.phone}
icon="designer"
/>
)}
</section>
)}
{attachments.length > 0 && (
<section className="bg-white rounded-3xl p-5 shadow-sm">
<h3 className="text-sm font-medium text-gray-900 mb-4">پیوستها</h3>
<div className="flex flex-col gap-2">
{attachments.map((url, idx) => {
const { fileName } = getFileNameAndExtensionFromUrl(url)
return (
<a
key={url}
href={url}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 rounded-xl border border-gray-100 bg-gray-50/80 px-3 py-2.5 text-sm text-[#0037FF] hover:bg-blue-50 transition-colors"
>
<Paperclip2 size={18} className="shrink-0" />
<span className="truncate">{fileName || `پیوست ${idx + 1}`}</span>
</a>
)
})}
</div>
</section>
)}
</div>
)
}
export default OrderDetailSidebar
+110 -62
View File
@@ -4,85 +4,133 @@ import { useGetEntityField } from '@/pages/formBuilder/hooks/useFormBuilderData'
import Button from '@/components/Button'
import { Link, useParams } from 'react-router-dom'
import { Paths } from '@/config/Paths'
import { clx } from '@/helpers/utils'
import { DocumentText, Printer } from 'iconsax-react'
type Props = {
item: OrderItemType,
estimatedDays?: number,
item: OrderItemType
estimatedDays?: number
designerName?: string
index?: number
}
const OrderItem: FC<Props> = ({ item, estimatedDays }) => {
const formatPrice = (value?: number | null) =>
value != null ? value.toLocaleString('fa-IR') + ' ریال' : '—'
const OrderItem: FC<Props> = ({ item, estimatedDays, designerName, index }) => {
const { id } = useParams()
const { data: fields } = useGetEntityField(String(item.product?.id ?? ''))
const hasPrintForm = Boolean(item.printAttributes?.length)
const imageUrl = item.product?.images?.[0]
const stats = [
{ label: 'تعداد', value: item.quantity?.toLocaleString('fa-IR') },
{ label: 'تخمین زمان', value: estimatedDays != null ? `${estimatedDays} روز` : '—' },
{ label: 'مبلغ واحد', value: formatPrice(item.unitPrice) },
{ label: 'تخفیف', value: formatPrice(item.discount) },
{ label: 'جمع', value: formatPrice(item.total) },
]
return (
<div key={item.product.id} className='border border-border rounded-3xl p-6'>
<div className='flex items-center justify-between mb-4'>
<div>#{item.id}</div>
<article className="overflow-hidden rounded-2xl border border-gray-100 bg-gray-50/40">
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-gray-100 bg-white px-4 py-3 md:px-5">
<div className="flex items-center gap-3 min-w-0">
<span className="inline-flex size-8 shrink-0 items-center justify-center rounded-full bg-primary/20 text-xs font-medium text-gray-800">
{index ?? item.id}
</span>
<div className="min-w-0">
<p className="text-sm font-medium text-gray-900 truncate">
{item.product?.title ?? '—'}
</p>
<p className="text-xs text-gray-500 mt-0.5">شناسه: {item.id}</p>
</div>
</div>
<Link to={Paths.print.form + item.id + `/${id}`}>
<Button
label={item.printAttributes?.length ? 'ویرایش فرم پرینت' : 'فرم پرینت'}
className='w-fit px-6 bg-blue-400 text-white'
/>
className={clx(
'w-fit px-4 text-sm',
hasPrintForm
? 'bg-blue-50 border border-blue-200 text-blue-700'
: 'bg-white border border-gray-200 text-gray-800',
)}
>
<div className="flex items-center gap-2">
<Printer size={16} color="currentColor" />
{hasPrintForm ? 'ویرایش فرم پرینت' : 'فرم پرینت'}
</div>
</Button>
</Link>
</div>
<div className="flex items-center gap-6">
{/* Product Image */}
<div className='size-[86px] rounded-lg overflow-hidden'>
<img src={item.product?.images?.[0]} className='size-full' />
</div>
{/* Order Details */}
<div className="flex-1 flex gap-20 pr-10">
<div className='flex items-center gap-2'>
<div className="text-xs text-desc">عنوان:</div>
<div className="text-sm font-medium text-black">{item.product.title}</div>
</div>
<div className='flex items-center gap-2'>
<div className="text-xs text-desc">نام طرح:</div>
<div className="text-sm font-medium text-black">{'orderData.designer'}</div>
</div>
<div className='flex items-center gap-2'>
<div className="text-xs text-desc">تعداد:</div>
<div className="text-sm font-medium text-black">{item.quantity}</div>
</div>
<div className='flex items-center gap-2'>
<div className="text-xs text-desc">تخمین زمان:</div>
<div className="text-sm font-medium text-black">{estimatedDays}</div>
</div>
</div>
</div>
{/* Order Description */}
<div className="mt-6 pt-6 border-t border-dashed border-desc">
<div className="text-sm font-medium mb-2 text-desc">شرح سفارش:</div>
<p className="text-sm font-light text-black leading-6">
{item.description}
</p>
</div>
<div className='font-bold mb-5 mt-7 '>
ویژگی ها
</div>
{
fields?.data?.map((field) => {
const value = item.attributes?.find((o) => String(o.attributeId) === String(field.id))
return (
<div className='flex gap-3 items-center mt-3' key={field.id}>
<div className='text-sm text-gray-500'>
{field.name} :
</div>
<div className='text-sm -mt-px'>
{value?.value || '-'}
<div className="p-4 md:p-5">
<div className="flex flex-col gap-5 md:flex-row">
<div className="size-24 shrink-0 overflow-hidden rounded-xl border border-gray-200 bg-white">
{imageUrl ? (
<img
src={imageUrl}
alt={item.product?.title}
className="size-full object-cover"
/>
) : (
<div className="flex size-full items-center justify-center text-gray-300">
<DocumentText size={32} />
</div>
)}
</div>
<div className="min-w-0 flex-1">
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-5">
{stats.map(({ label, value }) => (
<div
key={label}
className="rounded-xl border border-gray-100 bg-white px-3 py-2.5"
>
<p className="text-[11px] text-gray-500">{label}</p>
<p className="mt-1 text-sm font-medium text-gray-900">{value}</p>
</div>
))}
</div>
)
})
}
</div>
{designerName && designerName !== '—' && (
<p className="mt-3 text-sm text-gray-600">
<span className="text-gray-500">طراح:</span> {designerName}
</p>
)}
</div>
</div>
{item.description && (
<div className="mt-5 rounded-xl border border-dashed border-gray-200 bg-white px-4 py-3">
<p className="text-xs font-medium text-gray-500 mb-1.5">شرح سفارش</p>
<p className="text-sm leading-7 text-gray-800">{item.description}</p>
</div>
)}
{fields?.data && fields.data.length > 0 && (
<div className="mt-5">
<p className="text-xs font-medium text-gray-500 mb-3">ویژگیها</p>
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
{fields.data.map((field) => {
const value = item.attributes?.find(
(attr) => String(attr.attributeId) === String(field.id),
)
return (
<div
key={field.id}
className="flex items-center justify-between gap-3 rounded-xl border border-gray-100 bg-white px-3 py-2.5"
>
<span className="text-xs text-gray-500">{field.name}</span>
<span className="text-sm text-gray-900">
{value?.value || '—'}
</span>
</div>
)
})}
</div>
</div>
)}
</div>
</article>
)
}
@@ -0,0 +1,52 @@
import { type FC } from "react";
import { Link } from "react-router-dom";
import { DocumentText, Edit2, Eye, Printer, Receipt21 } from "iconsax-react";
import TrashWithConfrim from "@/components/TrashWithConfrim";
import { Paths } from "@/config/Paths";
import { getOrderInvoiceItemRef } from "../utils/orderListUtils";
import type { OrderListItemType } from "../types/Types";
type Props = {
item: OrderListItemType;
onDelete: (id: string) => void;
isDeleting?: boolean;
};
const OrderListActions: FC<Props> = ({ item, onDelete, isDeleting }) => {
const invoiceRef = getOrderInvoiceItemRef(item);
return (
<div className="flex items-center gap-2">
<Link title="جزئیات سفارش" to={Paths.order.details + item.id}>
<Eye size={20} color="#8C90A3" />
</Link>
{invoiceRef?.invoiceId && (
<Link
title="جزئیات پیش‌فاکتور"
to={Paths.perfomaInvoice.detail + invoiceRef.invoiceId}
>
<DocumentText size={20} color="#8C90A3" />
</Link>
)}
<Link title="فرم چاپ" to={Paths.orderPrint + item.id}>
<Printer size={20} color="black" />
</Link>
<Link title="ویرایش" to={Paths.order.edit + item.id}>
<Edit2 size={20} color="#0037FF" />
</Link>
<TrashWithConfrim
onDelete={() => onDelete(item.id)}
isloading={isDeleting}
colorIcon="red"
/>
</div>
);
};
export default OrderListActions;
+16 -20
View File
@@ -1,12 +1,10 @@
import { Link } from "react-router-dom";
import moment from "moment-jalaali";
import { Edit2, Printer } from "iconsax-react";
import type { ColumnType } from "@/components/types/TableTypes";
import { Paths } from "@/config/Paths";
import {
getOrderStatusBadgeClass,
getOrderStatusLabel,
} from "../constants/orderStatus";
import OrderListActions from "./OrderListActions";
import type { OrderListItemType } from "../types/Types";
const getCustomerName = (item: OrderListItemType) => {
@@ -27,7 +25,15 @@ const getDesignerName = (item: OrderListItemType) => {
return name || designer.phone || "—";
};
export const orderListColumns: ColumnType<OrderListItemType>[] = [
type OrderListColumnsOptions = {
onDelete: (id: string) => void;
isDeleting?: boolean;
};
export const getOrderListColumns = ({
onDelete,
isDeleting,
}: OrderListColumnsOptions): ColumnType<OrderListItemType>[] => [
{
key: "orderNumber",
title: "شماره",
@@ -35,14 +41,7 @@ export const orderListColumns: ColumnType<OrderListItemType>[] = [
{
key: "title",
title: "عنوان",
render: (item) => (
<Link
to={Paths.order.details + item.id}
className="font-medium text-[#0037FF] hover:underline"
>
{item.title}
</Link>
),
render: (item) => <span>{item.title}</span>,
},
{
key: "customer",
@@ -83,14 +82,11 @@ export const orderListColumns: ColumnType<OrderListItemType>[] = [
key: "actions",
title: "",
render: (item) => (
<div className="flex items-center gap-2">
<Link title="فرم چاپ" to={Paths.orderPrint + item.id}>
<Printer size={20} color="black" />
</Link>
<Link title="ویرایش" to={Paths.order.edit + item.id}>
<Edit2 size={20} color="#0037FF" />
</Link>
</div>
<OrderListActions
item={item}
onDelete={onDelete}
isDeleting={isDeleting}
/>
),
},
];
+8 -3
View File
@@ -88,11 +88,15 @@ const TicketSection: FC = () => {
}
return (
<div className="bg-white rounded-2xl p-6">
<div className="flex items-center justify-between mb-2">
<h2 className="text-lg font-light">گفتگو</h2>
<div className="overflow-hidden rounded-3xl bg-white shadow-sm">
<div className="flex items-center justify-between border-b border-gray-100 px-5 py-4 md:px-6">
<div>
<h2 className="text-base font-medium text-gray-900">گفتگو</h2>
<p className="mt-0.5 text-xs text-gray-500">پیامها و فایلهای مرتبط با سفارش</p>
</div>
<RefreshButton onClick={() => refetch()} isLoading={isFetching} />
</div>
<div className="p-5 md:p-6">
{
data?.data?.map((item) => {
if (item.user)
@@ -250,6 +254,7 @@ const TicketSection: FC = () => {
isLoading={singleUpload.isPending || multiUpload.isPending || addTicket.isPending}
/>
</div>
</div>
</div>
)
}
+12 -1
View File
@@ -1,4 +1,4 @@
import { useMutation, useQuery } from "@tanstack/react-query";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import * as api from "../service/OrderService";
import type {
AddTicketType,
@@ -121,3 +121,14 @@ export const useGetOrderPrint = (orderId: string) => {
enabled: !!orderId,
});
};
export const useDeleteOrder = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: api.deleteOrder,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["orders"] });
},
});
};
+5
View File
@@ -143,3 +143,8 @@ export const getOrderPrint = async (
);
return data;
};
export const deleteOrder = async (orderId: string) => {
const { data } = await axios.delete(`/admin/orders/${orderId}`);
return data;
};
+6 -1
View File
@@ -73,11 +73,16 @@ export interface OrderItemAttribute {
export type OrderResponseType = BaseResponse<OrderType[]>;
export type OrderListInvoiceItemRef = {
id: string;
invoice?: { id: string } | string | null;
};
export interface OrderListItemType extends RowDataType {
id: string;
createdAt: string;
deletedAt: string | null;
invoiceItem: string;
invoiceItem: string | OrderListInvoiceItemRef | null;
product: Product | null;
print: string | null;
title: string;
+29
View File
@@ -0,0 +1,29 @@
import moment from 'moment-jalaali'
import type { OrderDetailCreatorType, UserType } from '../types/Types'
export const formatPersonName = (
person?: { firstName?: string | null; lastName?: string | null; phone?: string } | null,
) => {
if (!person) return '—'
const name = [person.firstName, person.lastName].filter(Boolean).join(' ').trim()
return name || person.phone || '—'
}
export const formatJalaliDateTime = (date?: string | null) =>
date ? moment(date).format('jYYYY/jMM/jDD — HH:mm') : '—'
export const formatJalaliDate = (date?: string | null) =>
date ? moment(date).format('jYYYY/jMM/jDD') : '—'
export const getAttachmentUrl = (attachment: unknown): string | null => {
if (typeof attachment === 'string') return attachment
if (attachment && typeof attachment === 'object' && 'url' in attachment) {
return (attachment as { url?: string }).url ?? null
}
return null
}
export const getUserDisplayName = (user?: UserType | null) => formatPersonName(user)
export const getCreatorDisplayName = (creator?: OrderDetailCreatorType | null) =>
formatPersonName(creator)
+19
View File
@@ -0,0 +1,19 @@
import type { OrderListItemType } from "../types/Types";
export const getOrderInvoiceItemRef = (item: OrderListItemType) => {
const invoiceItem = item.invoiceItem;
if (!invoiceItem) return null;
if (typeof invoiceItem === "string") {
return { invoiceItemId: invoiceItem, invoiceId: null as string | null };
}
const invoice = invoiceItem.invoice;
const invoiceId =
typeof invoice === "string" ? invoice : invoice?.id ?? null;
return {
invoiceItemId: invoiceItem.id,
invoiceId,
};
};