242 lines
10 KiB
TypeScript
242 lines
10 KiB
TypeScript
import { type FC, useState } from 'react'
|
||
import { Link } from 'react-router-dom'
|
||
import { Call, Edit2, Location, Paperclip2, Profile2User, Receipt21, User } from 'iconsax-react'
|
||
import { clx } from '@/helpers/utils'
|
||
import { getFileNameAndExtensionFromUrl } from '@/config/func'
|
||
import { getPresignedUrl } from '@/pages/uploader/service/UploaderService'
|
||
import { Paths } from '@/config/Paths'
|
||
import {
|
||
getAttachmentUrl,
|
||
formatJalaliDateTime,
|
||
getCreatorDisplayName,
|
||
getUserDisplayName,
|
||
} from '../utils/orderDetailUtils'
|
||
import {
|
||
getOrderStatusBadgeClass,
|
||
getOrderStatusLabel,
|
||
} from '../constants/orderStatus'
|
||
import { OrderStatusEnum } from '../enum/OrderEnum'
|
||
import type { OrderDetailDataType } from '../types/Types'
|
||
import OrderStatusModal from './OrderStatusModal'
|
||
|
||
type Props = {
|
||
order?: OrderDetailDataType
|
||
onUpdateStatus?: (status: OrderStatusEnum) => void
|
||
isUpdatingStatus?: boolean
|
||
}
|
||
|
||
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,
|
||
onUpdateStatus,
|
||
isUpdatingStatus,
|
||
}) => {
|
||
const attachments = (order?.attachments ?? [])
|
||
.map(getAttachmentUrl)
|
||
.filter((url): url is string => Boolean(url))
|
||
const [isStatusModalOpen, setIsStatusModalOpen] = useState(false)
|
||
const [selectedStatus, setSelectedStatus] = useState<OrderStatusEnum>(
|
||
(order?.status as OrderStatusEnum) ?? OrderStatusEnum.CREATED,
|
||
)
|
||
|
||
const handleOpenStatusModal = () => {
|
||
if (!order?.status) return
|
||
setSelectedStatus(order.status as OrderStatusEnum)
|
||
setIsStatusModalOpen(true)
|
||
}
|
||
|
||
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 && (
|
||
<>
|
||
<button
|
||
type="button"
|
||
title="تغییر وضعیت سفارش"
|
||
onClick={handleOpenStatusModal}
|
||
className="inline-flex size-8 items-center justify-center rounded-lg border border-blue-200 text-[#0037FF] hover:bg-blue-50 transition-colors"
|
||
>
|
||
<Receipt21 size={16} color="#0037FF" />
|
||
</button>
|
||
<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((key, idx) => {
|
||
const { fileName } = getFileNameAndExtensionFromUrl(key)
|
||
return (
|
||
<button
|
||
key={key}
|
||
type="button"
|
||
onClick={async () => {
|
||
const resolvedUrl = await getPresignedUrl(key)
|
||
window.open(resolvedUrl, '_blank', '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 text-right w-full"
|
||
>
|
||
<Paperclip2 size={18} className="shrink-0" />
|
||
<span className="truncate">{fileName || `پیوست ${idx + 1}`}</span>
|
||
</button>
|
||
)
|
||
})}
|
||
</div>
|
||
</section>
|
||
)}
|
||
|
||
<OrderStatusModal
|
||
open={isStatusModalOpen}
|
||
close={() => setIsStatusModalOpen(false)}
|
||
initialStatus={selectedStatus}
|
||
inputName={`order-detail-status-${order?.id ?? 'unknown'}`}
|
||
isLoading={isUpdatingStatus}
|
||
onSubmit={(status) => {
|
||
setSelectedStatus(status)
|
||
onUpdateStatus?.(status)
|
||
}}
|
||
/>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default OrderDetailSidebar
|