Files
negareh-admin/src/pages/order/components/OrderDetailSidebar.tsx
T
morteza 7271ea8f21
deploy to danak / build_and_deploy (push) Has been cancelled
presigned url
2026-07-10 20:08:31 +03:30

242 lines
10 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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