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 { Link } from "react-router-dom";
|
||||||
import { AddSquare, ArrowDown2, Filter } from "iconsax-react";
|
import { AddSquare, ArrowDown2, Filter } from "iconsax-react";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
import Filters from "@/components/Filters";
|
import Filters from "@/components/Filters";
|
||||||
import Table from "@/components/Table";
|
import Table from "@/components/Table";
|
||||||
import Tabs from "@/components/Tabs";
|
import Tabs from "@/components/Tabs";
|
||||||
import Button from "@/components/Button";
|
import Button from "@/components/Button";
|
||||||
import RefreshButton from "@/components/RefreshButton";
|
import RefreshButton from "@/components/RefreshButton";
|
||||||
import { Paths } from "@/config/Paths";
|
import { Paths } from "@/config/Paths";
|
||||||
|
import { extractErrorMessage } from "@/config/func";
|
||||||
import { clx } from "@/helpers/utils";
|
import { clx } from "@/helpers/utils";
|
||||||
import UserSearch from "../user/UserSearch";
|
import UserSearch from "../user/UserSearch";
|
||||||
import DesignerSearch from "../designer/DesignerSearch";
|
import DesignerSearch from "../designer/DesignerSearch";
|
||||||
import CategoriesSelect from "../product/components/CategoriesSelect";
|
import CategoriesSelect from "../product/components/CategoriesSelect";
|
||||||
import { orderListTabs } from "./constants/orderStatus";
|
import { orderListTabs } from "./constants/orderStatus";
|
||||||
import { orderListColumns } from "./components/OrderListColumns";
|
import { getOrderListColumns } from "./components/OrderListColumns";
|
||||||
import { useGetOrders } from "./hooks/useOrderData";
|
import { useDeleteOrder, useGetOrders } from "./hooks/useOrderData";
|
||||||
import {
|
import {
|
||||||
ORDER_LIST_FILTER_FIELDS,
|
ORDER_LIST_FILTER_FIELDS,
|
||||||
useOrderListParams,
|
useOrderListParams,
|
||||||
@@ -45,9 +47,30 @@ const OrdersList: FC = () => {
|
|||||||
} = useOrderListParams();
|
} = useOrderListParams();
|
||||||
|
|
||||||
const { data, refetch, isFetching } = useGetOrders(queryParams);
|
const { data, refetch, isFetching } = useGetOrders(queryParams);
|
||||||
|
const { mutate: deleteOrder, isPending: isDeleting } = useDeleteOrder();
|
||||||
const activeFilterCount = useMemo(() => countActiveFilters(params), [params]);
|
const activeFilterCount = useMemo(() => countActiveFilters(params), [params]);
|
||||||
const hasActiveFilters = activeFilterCount > 0;
|
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 (
|
return (
|
||||||
<div className="mt-5 space-y-6">
|
<div className="mt-5 space-y-6">
|
||||||
<div className="flex flex-wrap items-center justify-between gap-4">
|
<div className="flex flex-wrap items-center justify-between gap-4">
|
||||||
@@ -158,7 +181,7 @@ const OrdersList: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Table<OrderListItemType>
|
<Table<OrderListItemType>
|
||||||
columns={orderListColumns}
|
columns={columns}
|
||||||
data={data?.data}
|
data={data?.data}
|
||||||
isLoading={isFetching}
|
isLoading={isFetching}
|
||||||
noDataMessage="سفارشی یافت نشد"
|
noDataMessage="سفارشی یافت نشد"
|
||||||
|
|||||||
+65
-165
@@ -1,185 +1,85 @@
|
|||||||
import { type FC } from 'react'
|
import { type FC } from 'react'
|
||||||
import { useParams } from 'react-router-dom'
|
import { Link, useParams } from 'react-router-dom'
|
||||||
import moment from 'moment-jalaali'
|
import { Edit2, Printer } from 'iconsax-react'
|
||||||
import { User } from 'iconsax-react'
|
|
||||||
import BackButton from '@/components/BackButton'
|
import BackButton from '@/components/BackButton'
|
||||||
import RefreshButton from '@/components/RefreshButton'
|
import RefreshButton from '@/components/RefreshButton'
|
||||||
|
import Button from '@/components/Button'
|
||||||
import { Paths } from '@/config/Paths'
|
import { Paths } from '@/config/Paths'
|
||||||
import { useGetOrderDetails } from './hooks/useOrderData'
|
import { useGetOrderDetails } from './hooks/useOrderData'
|
||||||
import TicketSection from './components/TicketSection'
|
import TicketSection from './components/TicketSection'
|
||||||
import OrderItem from './components/OrderItem'
|
import OrderDetailSidebar from './components/OrderDetailSidebar'
|
||||||
import type { OrderDetailDataType, OrderItemType } from './types/Types'
|
import InvoiceItemDetailSection from './components/InvoiceItemDetailSection'
|
||||||
|
import type { OrderDetailDataType } 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>
|
|
||||||
)
|
|
||||||
|
|
||||||
const OrderDetail: FC = () => {
|
const OrderDetail: FC = () => {
|
||||||
const { id } = useParams()
|
const { id } = useParams()
|
||||||
const { data, refetch, isFetching } = useGetOrderDetails(id!)
|
const { data, refetch, isFetching, isPending } = useGetOrderDetails(id!)
|
||||||
const order = data?.data as (OrderDetailDataType & { items?: OrderItemType[] }) | undefined
|
const order = data?.data as OrderDetailDataType | undefined
|
||||||
const creator = order?.creator
|
|
||||||
const creatorName = creator ? `${creator.firstName} ${creator.lastName}`.trim() : ''
|
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 (
|
return (
|
||||||
<div className="w-full min-h-screen bg-[#eceef6] p-6">
|
<div className="mt-5">
|
||||||
{/* Header */}
|
<div className="flex flex-wrap items-start justify-between gap-4">
|
||||||
<header className="flex items-center justify-between mb-6">
|
<div className="space-y-3">
|
||||||
<BackButton to={Paths.order.list} />
|
<BackButton to={Paths.order.list} />
|
||||||
<div className="flex items-center gap-3">
|
<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} />
|
<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>
|
</div>
|
||||||
</header>
|
</div>
|
||||||
|
|
||||||
{/* اطلاعات سفارش */}
|
<div className="mt-6 flex flex-col-reverse gap-6 xl:flex-row xl:items-start">
|
||||||
<section className={`${CARD_CLASS} mb-6`}>
|
<div className="min-w-0 flex-1 space-y-6">
|
||||||
<div className={SECTION_HEADER_CLASS}>
|
<InvoiceItemDetailSection invoiceItemId={order?.invoiceItem} />
|
||||||
<h2 className={SECTION_TITLE_CLASS}>اطلاعات سفارش</h2>
|
|
||||||
|
<TicketSection />
|
||||||
</div>
|
</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>
|
|
||||||
|
|
||||||
{/* اطلاعات مشتری */}
|
<aside className="w-full shrink-0 xl:sticky xl:top-4 xl:w-[320px]">
|
||||||
{order?.user && (
|
<OrderDetailSidebar order={order} />
|
||||||
<section className={`${CARD_CLASS} mb-6`}>
|
</aside>
|
||||||
<div className={SECTION_HEADER_CLASS}>
|
</div>
|
||||||
<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 />
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default OrderDetail
|
export default OrderDetail
|
||||||
|
|||||||
@@ -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,86 +4,134 @@ import { useGetEntityField } from '@/pages/formBuilder/hooks/useFormBuilderData'
|
|||||||
import Button from '@/components/Button'
|
import Button from '@/components/Button'
|
||||||
import { Link, useParams } from 'react-router-dom'
|
import { Link, useParams } from 'react-router-dom'
|
||||||
import { Paths } from '@/config/Paths'
|
import { Paths } from '@/config/Paths'
|
||||||
|
import { clx } from '@/helpers/utils'
|
||||||
|
import { DocumentText, Printer } from 'iconsax-react'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
item: OrderItemType,
|
item: OrderItemType
|
||||||
estimatedDays?: number,
|
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 { id } = useParams()
|
||||||
const { data: fields } = useGetEntityField(String(item.product?.id ?? ''))
|
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 (
|
return (
|
||||||
<div key={item.product.id} className='border border-border rounded-3xl p-6'>
|
<article className="overflow-hidden rounded-2xl border border-gray-100 bg-gray-50/40">
|
||||||
<div className='flex items-center justify-between mb-4'>
|
<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>#{item.id}</div>
|
<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}`}>
|
<Link to={Paths.print.form + item.id + `/${id}`}>
|
||||||
<Button
|
<Button
|
||||||
label={item.printAttributes?.length ? 'ویرایش فرم پرینت' : 'فرم پرینت'}
|
className={clx(
|
||||||
className='w-fit px-6 bg-blue-400 text-white'
|
'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>
|
</Link>
|
||||||
</div>
|
</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="p-4 md:p-5">
|
||||||
<div className="flex-1 flex gap-20 pr-10">
|
<div className="flex flex-col gap-5 md:flex-row">
|
||||||
<div className='flex items-center gap-2'>
|
<div className="size-24 shrink-0 overflow-hidden rounded-xl border border-gray-200 bg-white">
|
||||||
<div className="text-xs text-desc">عنوان:</div>
|
{imageUrl ? (
|
||||||
<div className="text-sm font-medium text-black">{item.product.title}</div>
|
<img
|
||||||
</div>
|
src={imageUrl}
|
||||||
<div className='flex items-center gap-2'>
|
alt={item.product?.title}
|
||||||
<div className="text-xs text-desc">نام طرح:</div>
|
className="size-full object-cover"
|
||||||
<div className="text-sm font-medium text-black">{'orderData.designer'}</div>
|
/>
|
||||||
</div>
|
) : (
|
||||||
<div className='flex items-center gap-2'>
|
<div className="flex size-full items-center justify-center text-gray-300">
|
||||||
<div className="text-xs text-desc">تعداد:</div>
|
<DocumentText size={32} />
|
||||||
<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>
|
</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">
|
||||||
</div>
|
<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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default OrderItem
|
export default OrderItem
|
||||||
|
|||||||
@@ -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 moment from "moment-jalaali";
|
||||||
import { Edit2, Printer } from "iconsax-react";
|
|
||||||
import type { ColumnType } from "@/components/types/TableTypes";
|
import type { ColumnType } from "@/components/types/TableTypes";
|
||||||
import { Paths } from "@/config/Paths";
|
|
||||||
import {
|
import {
|
||||||
getOrderStatusBadgeClass,
|
getOrderStatusBadgeClass,
|
||||||
getOrderStatusLabel,
|
getOrderStatusLabel,
|
||||||
} from "../constants/orderStatus";
|
} from "../constants/orderStatus";
|
||||||
|
import OrderListActions from "./OrderListActions";
|
||||||
import type { OrderListItemType } from "../types/Types";
|
import type { OrderListItemType } from "../types/Types";
|
||||||
|
|
||||||
const getCustomerName = (item: OrderListItemType) => {
|
const getCustomerName = (item: OrderListItemType) => {
|
||||||
@@ -27,7 +25,15 @@ const getDesignerName = (item: OrderListItemType) => {
|
|||||||
return name || designer.phone || "—";
|
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",
|
key: "orderNumber",
|
||||||
title: "شماره",
|
title: "شماره",
|
||||||
@@ -35,14 +41,7 @@ export const orderListColumns: ColumnType<OrderListItemType>[] = [
|
|||||||
{
|
{
|
||||||
key: "title",
|
key: "title",
|
||||||
title: "عنوان",
|
title: "عنوان",
|
||||||
render: (item) => (
|
render: (item) => <span>{item.title}</span>,
|
||||||
<Link
|
|
||||||
to={Paths.order.details + item.id}
|
|
||||||
className="font-medium text-[#0037FF] hover:underline"
|
|
||||||
>
|
|
||||||
{item.title}
|
|
||||||
</Link>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "customer",
|
key: "customer",
|
||||||
@@ -83,14 +82,11 @@ export const orderListColumns: ColumnType<OrderListItemType>[] = [
|
|||||||
key: "actions",
|
key: "actions",
|
||||||
title: "",
|
title: "",
|
||||||
render: (item) => (
|
render: (item) => (
|
||||||
<div className="flex items-center gap-2">
|
<OrderListActions
|
||||||
<Link title="فرم چاپ" to={Paths.orderPrint + item.id}>
|
item={item}
|
||||||
<Printer size={20} color="black" />
|
onDelete={onDelete}
|
||||||
</Link>
|
isDeleting={isDeleting}
|
||||||
<Link title="ویرایش" to={Paths.order.edit + item.id}>
|
/>
|
||||||
<Edit2 size={20} color="#0037FF" />
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -88,11 +88,15 @@ const TicketSection: FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white rounded-2xl p-6">
|
<div className="overflow-hidden rounded-3xl bg-white shadow-sm">
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between border-b border-gray-100 px-5 py-4 md:px-6">
|
||||||
<h2 className="text-lg font-light">گفتگو</h2>
|
<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} />
|
<RefreshButton onClick={() => refetch()} isLoading={isFetching} />
|
||||||
</div>
|
</div>
|
||||||
|
<div className="p-5 md:p-6">
|
||||||
{
|
{
|
||||||
data?.data?.map((item) => {
|
data?.data?.map((item) => {
|
||||||
if (item.user)
|
if (item.user)
|
||||||
@@ -250,6 +254,7 @@ const TicketSection: FC = () => {
|
|||||||
isLoading={singleUpload.isPending || multiUpload.isPending || addTicket.isPending}
|
isLoading={singleUpload.isPending || multiUpload.isPending || addTicket.isPending}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</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 * as api from "../service/OrderService";
|
||||||
import type {
|
import type {
|
||||||
AddTicketType,
|
AddTicketType,
|
||||||
@@ -121,3 +121,14 @@ export const useGetOrderPrint = (orderId: string) => {
|
|||||||
enabled: !!orderId,
|
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;
|
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 OrderResponseType = BaseResponse<OrderType[]>;
|
||||||
|
|
||||||
|
export type OrderListInvoiceItemRef = {
|
||||||
|
id: string;
|
||||||
|
invoice?: { id: string } | string | null;
|
||||||
|
};
|
||||||
|
|
||||||
export interface OrderListItemType extends RowDataType {
|
export interface OrderListItemType extends RowDataType {
|
||||||
id: string;
|
id: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
deletedAt: string | null;
|
deletedAt: string | null;
|
||||||
invoiceItem: string;
|
invoiceItem: string | OrderListInvoiceItemRef | null;
|
||||||
product: Product | null;
|
product: Product | null;
|
||||||
print: string | null;
|
print: string | null;
|
||||||
title: string;
|
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