order list
This commit is contained in:
@@ -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="سفارشی یافت نشد"
|
||||
|
||||
+57
-157
@@ -1,184 +1,84 @@
|
||||
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">
|
||||
<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 className="flex items-center gap-3">
|
||||
<RefreshButton onClick={() => refetch()} isLoading={isFetching} />
|
||||
<span className="text-sm text-[#8C90A3]">سفارش #{order?.orderNumber}</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* اطلاعات سفارش */}
|
||||
<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-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') : '—'}
|
||||
<div>
|
||||
<h1 className="text-lg font-light text-gray-900">جزئیات سفارش</h1>
|
||||
<p className="mt-1 text-sm text-gray-500">
|
||||
مشاهده اطلاعات سفارش، آیتم پیشفاکتور و گفتگو
|
||||
</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 className="flex flex-wrap items-center gap-2">
|
||||
<RefreshButton onClick={() => refetch()} isLoading={isFetching} />
|
||||
<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>
|
||||
<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} />
|
||||
</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>
|
||||
<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>
|
||||
</Button>
|
||||
</Link>
|
||||
</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}
|
||||
<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>
|
||||
|
||||
<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
|
||||
@@ -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 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>
|
||||
|
||||
{/* 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 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>
|
||||
|
||||
{/* 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}
|
||||
{designerName && designerName !== '—' && (
|
||||
<p className="mt-3 text-sm text-gray-600">
|
||||
<span className="text-gray-500">طراح:</span> {designerName}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='font-bold mb-5 mt-7 '>
|
||||
ویژگی ها
|
||||
{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?.map((field) => {
|
||||
const value = item.attributes?.find((o) => String(o.attributeId) === String(field.id))
|
||||
|
||||
)}
|
||||
|
||||
{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 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>
|
||||
<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;
|
||||
@@ -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}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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)
|
||||
@@ -251,6 +255,7 @@ const TicketSection: FC = () => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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"] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user