This commit is contained in:
@@ -1,21 +1,42 @@
|
|||||||
import { type FC } from 'react'
|
import { type FC } from 'react'
|
||||||
import { Link, useParams } from 'react-router-dom'
|
import { Link, useParams } from 'react-router-dom'
|
||||||
import { Edit2, Printer } from 'iconsax-react'
|
import { Edit2, Printer } from 'iconsax-react'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
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 Button from '@/components/Button'
|
||||||
import { Paths } from '@/config/Paths'
|
import { Paths } from '@/config/Paths'
|
||||||
import { useGetOrderDetails } from './hooks/useOrderData'
|
import { extractErrorMessage } from '@/config/func'
|
||||||
|
import { useGetOrderDetails, useUpdateOrderStatus } from './hooks/useOrderData'
|
||||||
import TicketSection from './components/TicketSection'
|
import TicketSection from './components/TicketSection'
|
||||||
import OrderDetailSidebar from './components/OrderDetailSidebar'
|
import OrderDetailSidebar from './components/OrderDetailSidebar'
|
||||||
import InvoiceItemDetailSection from './components/InvoiceItemDetailSection'
|
import InvoiceItemDetailSection from './components/InvoiceItemDetailSection'
|
||||||
import type { OrderDetailDataType } from './types/Types'
|
import type { OrderDetailDataType } from './types/Types'
|
||||||
|
import { OrderStatusEnum } from './enum/OrderEnum'
|
||||||
|
|
||||||
const OrderDetail: FC = () => {
|
const OrderDetail: FC = () => {
|
||||||
const { id } = useParams()
|
const { id } = useParams()
|
||||||
const { data, refetch, isFetching, isPending } = useGetOrderDetails(id!)
|
const { data, refetch, isFetching, isPending } = useGetOrderDetails(id!)
|
||||||
|
const { mutate: updateOrderStatus, isPending: isUpdatingStatus } =
|
||||||
|
useUpdateOrderStatus()
|
||||||
const order = data?.data as OrderDetailDataType | undefined
|
const order = data?.data as OrderDetailDataType | undefined
|
||||||
|
|
||||||
|
const handleUpdateStatus = (status: OrderStatusEnum) => {
|
||||||
|
if (!id) return
|
||||||
|
updateOrderStatus(
|
||||||
|
{ orderId: id, status },
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
refetch()
|
||||||
|
toast.success('وضعیت سفارش با موفقیت بهروزرسانی شد')
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(extractErrorMessage(error))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (isPending) {
|
if (isPending) {
|
||||||
return (
|
return (
|
||||||
<div className="mt-5 space-y-6 animate-pulse">
|
<div className="mt-5 space-y-6 animate-pulse">
|
||||||
@@ -75,7 +96,11 @@ const OrderDetail: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<aside className="w-full shrink-0 xl:sticky xl:top-4 xl:w-[320px]">
|
<aside className="w-full shrink-0 xl:sticky xl:top-4 xl:w-[320px]">
|
||||||
<OrderDetailSidebar order={order} />
|
<OrderDetailSidebar
|
||||||
|
order={order}
|
||||||
|
onUpdateStatus={handleUpdateStatus}
|
||||||
|
isUpdatingStatus={isUpdatingStatus}
|
||||||
|
/>
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { type FC } from 'react'
|
import { type FC, useState } from 'react'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { Call, Edit2, Location, Paperclip2, Profile2User, User } from 'iconsax-react'
|
import { Call, Edit2, Location, Paperclip2, Profile2User, Receipt21, User } from 'iconsax-react'
|
||||||
import { clx } from '@/helpers/utils'
|
import { clx } from '@/helpers/utils'
|
||||||
import { getFileNameAndExtensionFromUrl } from '@/config/func'
|
import { getFileNameAndExtensionFromUrl } from '@/config/func'
|
||||||
import { Paths } from '@/config/Paths'
|
import { Paths } from '@/config/Paths'
|
||||||
@@ -14,10 +14,14 @@ import {
|
|||||||
getOrderStatusBadgeClass,
|
getOrderStatusBadgeClass,
|
||||||
getOrderStatusLabel,
|
getOrderStatusLabel,
|
||||||
} from '../constants/orderStatus'
|
} from '../constants/orderStatus'
|
||||||
|
import { OrderStatusEnum } from '../enum/OrderEnum'
|
||||||
import type { OrderDetailDataType } from '../types/Types'
|
import type { OrderDetailDataType } from '../types/Types'
|
||||||
|
import OrderStatusModal from './OrderStatusModal'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
order?: OrderDetailDataType
|
order?: OrderDetailDataType
|
||||||
|
onUpdateStatus?: (status: OrderStatusEnum) => void
|
||||||
|
isUpdatingStatus?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type InfoRowProps = {
|
type InfoRowProps = {
|
||||||
@@ -72,10 +76,24 @@ const PersonCard: FC<{
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
const OrderDetailSidebar: FC<Props> = ({ order }) => {
|
const OrderDetailSidebar: FC<Props> = ({
|
||||||
|
order,
|
||||||
|
onUpdateStatus,
|
||||||
|
isUpdatingStatus,
|
||||||
|
}) => {
|
||||||
const attachments = (order?.attachments ?? [])
|
const attachments = (order?.attachments ?? [])
|
||||||
.map(getAttachmentUrl)
|
.map(getAttachmentUrl)
|
||||||
.filter((url): url is string => Boolean(url))
|
.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 (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@@ -98,14 +116,24 @@ const OrderDetailSidebar: FC<Props> = ({ order }) => {
|
|||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
{order?.status && (
|
{order?.status && (
|
||||||
<span
|
<>
|
||||||
className={clx(
|
<button
|
||||||
'inline-flex h-7 items-center rounded-full px-3 text-xs font-medium',
|
type="button"
|
||||||
getOrderStatusBadgeClass(order.status),
|
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"
|
||||||
{getOrderStatusLabel(order.status)}
|
>
|
||||||
</span>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
@@ -191,6 +219,18 @@ const OrderDetailSidebar: FC<Props> = ({ order }) => {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
import { type FC, useMemo, useState } from "react";
|
import { type FC, useState } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { DocumentText, Edit2, Eye, Printer, Receipt21 } from "iconsax-react";
|
import { DocumentText, Edit2, Eye, Printer, Receipt21 } from "iconsax-react";
|
||||||
import TrashWithConfrim from "@/components/TrashWithConfrim";
|
import TrashWithConfrim from "@/components/TrashWithConfrim";
|
||||||
import DefaulModal from "@/components/DefaulModal";
|
|
||||||
import Select from "@/components/Select";
|
|
||||||
import Button from "@/components/Button";
|
|
||||||
import { Paths } from "@/config/Paths";
|
import { Paths } from "@/config/Paths";
|
||||||
import { getOrderInvoiceItemRef } from "../utils/orderListUtils";
|
import { getOrderInvoiceItemRef } from "../utils/orderListUtils";
|
||||||
import type { OrderListItemType } from "../types/Types";
|
import type { OrderListItemType } from "../types/Types";
|
||||||
import { OrderStatusEnum } from "../enum/OrderEnum";
|
import { OrderStatusEnum } from "../enum/OrderEnum";
|
||||||
import { getOrderStatusLabel } from "../constants/orderStatus";
|
import OrderStatusModal from "./OrderStatusModal";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
item: OrderListItemType;
|
item: OrderListItemType;
|
||||||
@@ -31,25 +28,12 @@ const OrderListActions: FC<Props> = ({
|
|||||||
item.status as OrderStatusEnum,
|
item.status as OrderStatusEnum,
|
||||||
);
|
);
|
||||||
const invoiceRef = getOrderInvoiceItemRef(item);
|
const invoiceRef = getOrderInvoiceItemRef(item);
|
||||||
const statusOptions = useMemo(
|
|
||||||
() =>
|
|
||||||
Object.values(OrderStatusEnum).map((status) => ({
|
|
||||||
value: status,
|
|
||||||
label: getOrderStatusLabel(status),
|
|
||||||
})),
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleOpenStatusModal = () => {
|
const handleOpenStatusModal = () => {
|
||||||
setSelectedStatus(item.status as OrderStatusEnum);
|
setSelectedStatus(item.status as OrderStatusEnum);
|
||||||
setIsStatusModalOpen(true);
|
setIsStatusModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmitStatus = () => {
|
|
||||||
onUpdateStatus(item.id, selectedStatus);
|
|
||||||
setIsStatusModalOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
|
||||||
@@ -79,9 +63,9 @@ const OrderListActions: FC<Props> = ({
|
|||||||
type="button"
|
type="button"
|
||||||
title="تغییر وضعیت سفارش"
|
title="تغییر وضعیت سفارش"
|
||||||
onClick={handleOpenStatusModal}
|
onClick={handleOpenStatusModal}
|
||||||
className="inline-flex"
|
className="inline-flex items-center gap-1 rounded-full px-1 py-1 text-xs font-medium text-[#0037FF] "
|
||||||
>
|
>
|
||||||
<Receipt21 size={20} color="#8C90A3" />
|
<Receipt21 size={16} color="#0037FF" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<TrashWithConfrim
|
<TrashWithConfrim
|
||||||
@@ -90,33 +74,17 @@ const OrderListActions: FC<Props> = ({
|
|||||||
colorIcon="red"
|
colorIcon="red"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DefaulModal
|
<OrderStatusModal
|
||||||
open={isStatusModalOpen}
|
open={isStatusModalOpen}
|
||||||
close={() => setIsStatusModalOpen(false)}
|
close={() => setIsStatusModalOpen(false)}
|
||||||
title_header="تغییر وضعیت سفارش"
|
initialStatus={selectedStatus}
|
||||||
isHeader
|
inputName={`order-status-${item.id}`}
|
||||||
>
|
isLoading={isUpdatingStatus}
|
||||||
<div className="mt-6 space-y-6">
|
onSubmit={(status) => {
|
||||||
<Select
|
setSelectedStatus(status);
|
||||||
label="وضعیت سفارش"
|
onUpdateStatus(item.id, status);
|
||||||
value={selectedStatus}
|
}}
|
||||||
onChange={(e) => setSelectedStatus(e.target.value as OrderStatusEnum)}
|
/>
|
||||||
items={statusOptions}
|
|
||||||
/>
|
|
||||||
<div className="flex justify-center gap-3">
|
|
||||||
<Button
|
|
||||||
label="ثبت"
|
|
||||||
onClick={handleSubmitStatus}
|
|
||||||
isLoading={isUpdatingStatus}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
label="لغو"
|
|
||||||
className="border border-primary bg-transparent text-black"
|
|
||||||
onClick={() => setIsStatusModalOpen(false)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</DefaulModal>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import { type FC, useEffect, useMemo, useState } from "react";
|
||||||
|
import DefaulModal from "@/components/DefaulModal";
|
||||||
|
import Button from "@/components/Button";
|
||||||
|
import { getOrderStatusLabel, orderStatusCategories } from "../constants/orderStatus";
|
||||||
|
import { OrderStatusEnum } from "../enum/OrderEnum";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
open: boolean;
|
||||||
|
close: () => void;
|
||||||
|
initialStatus: OrderStatusEnum;
|
||||||
|
inputName: string;
|
||||||
|
isLoading?: boolean;
|
||||||
|
onSubmit: (status: OrderStatusEnum) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const OrderStatusModal: FC<Props> = ({
|
||||||
|
open,
|
||||||
|
close,
|
||||||
|
initialStatus,
|
||||||
|
inputName,
|
||||||
|
isLoading,
|
||||||
|
onSubmit,
|
||||||
|
}) => {
|
||||||
|
const [selectedStatus, setSelectedStatus] = useState<OrderStatusEnum>(initialStatus);
|
||||||
|
const categorizedStatuses = useMemo(() => orderStatusCategories, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
setSelectedStatus(initialStatus);
|
||||||
|
}
|
||||||
|
}, [initialStatus, open]);
|
||||||
|
|
||||||
|
const handleSubmitStatus = () => {
|
||||||
|
onSubmit(selectedStatus);
|
||||||
|
close();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DefaulModal open={open} close={close} title_header="تغییر وضعیت سفارش" isHeader>
|
||||||
|
<div className="mt-6 space-y-6">
|
||||||
|
<div className="space-y-4">
|
||||||
|
{categorizedStatuses.map((category) => (
|
||||||
|
<div key={category.key} className="rounded-xl border border-border p-3">
|
||||||
|
<div className="mb-2 text-sm font-medium text-gray-700">{category.label}</div>
|
||||||
|
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
||||||
|
{category.statuses.map((status) => (
|
||||||
|
<label
|
||||||
|
key={status}
|
||||||
|
className="flex cursor-pointer items-center gap-2 text-sm"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name={inputName}
|
||||||
|
value={status}
|
||||||
|
checked={selectedStatus === status}
|
||||||
|
onChange={() => setSelectedStatus(status)}
|
||||||
|
/>
|
||||||
|
<span>{getOrderStatusLabel(status)}</span>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-center gap-3">
|
||||||
|
<Button label="ثبت" onClick={handleSubmitStatus} isLoading={isLoading} />
|
||||||
|
<Button
|
||||||
|
label="لغو"
|
||||||
|
className="border border-primary bg-transparent text-black"
|
||||||
|
onClick={close}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DefaulModal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OrderStatusModal;
|
||||||
@@ -29,7 +29,6 @@ export const orderListTabs = [
|
|||||||
export const orderStatusLabels: Record<OrderStatusEnum, string> = {
|
export const orderStatusLabels: Record<OrderStatusEnum, string> = {
|
||||||
[OrderStatusEnum.CREATED]: 'ایجاد شده',
|
[OrderStatusEnum.CREATED]: 'ایجاد شده',
|
||||||
[OrderStatusEnum.INVOICED]: 'فاکتور شده',
|
[OrderStatusEnum.INVOICED]: 'فاکتور شده',
|
||||||
[OrderStatusEnum.WAITING_to_CONFIRM_INVOICE]: 'در انتظار تایید فاکتور',
|
|
||||||
[OrderStatusEnum.DESIGN_INITIATED]: 'شروع طراحی',
|
[OrderStatusEnum.DESIGN_INITIATED]: 'شروع طراحی',
|
||||||
[OrderStatusEnum.DESIGN_FINISHED]: 'طراحی تمام شده',
|
[OrderStatusEnum.DESIGN_FINISHED]: 'طراحی تمام شده',
|
||||||
[OrderStatusEnum.DESIGN_REVISION]: 'بازنگری طراحی',
|
[OrderStatusEnum.DESIGN_REVISION]: 'بازنگری طراحی',
|
||||||
@@ -47,7 +46,6 @@ export const orderStatusLabels: Record<OrderStatusEnum, string> = {
|
|||||||
export const orderStatusBadgeClass: Record<OrderStatusEnum, string> = {
|
export const orderStatusBadgeClass: Record<OrderStatusEnum, string> = {
|
||||||
[OrderStatusEnum.CREATED]: 'bg-blue-100 text-blue-800',
|
[OrderStatusEnum.CREATED]: 'bg-blue-100 text-blue-800',
|
||||||
[OrderStatusEnum.INVOICED]: 'bg-green-100 text-green-800',
|
[OrderStatusEnum.INVOICED]: 'bg-green-100 text-green-800',
|
||||||
[OrderStatusEnum.WAITING_to_CONFIRM_INVOICE]: 'bg-amber-100 text-amber-800',
|
|
||||||
[OrderStatusEnum.DESIGN_INITIATED]: 'bg-blue-100 text-blue-800',
|
[OrderStatusEnum.DESIGN_INITIATED]: 'bg-blue-100 text-blue-800',
|
||||||
[OrderStatusEnum.DESIGN_FINISHED]: 'bg-green-100 text-green-800',
|
[OrderStatusEnum.DESIGN_FINISHED]: 'bg-green-100 text-green-800',
|
||||||
[OrderStatusEnum.DESIGN_REVISION]: 'bg-amber-100 text-amber-800',
|
[OrderStatusEnum.DESIGN_REVISION]: 'bg-amber-100 text-amber-800',
|
||||||
@@ -62,6 +60,34 @@ export const orderStatusBadgeClass: Record<OrderStatusEnum, string> = {
|
|||||||
[OrderStatusEnum.CANCELED]: 'bg-red-100 text-red-800',
|
[OrderStatusEnum.CANCELED]: 'bg-red-100 text-red-800',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const orderStatusCategories: {
|
||||||
|
key: TabOrderListEnum
|
||||||
|
label: string
|
||||||
|
statuses: OrderStatusEnum[]
|
||||||
|
}[] = [
|
||||||
|
{
|
||||||
|
key: TabOrderListEnum.IN_PROGRESS,
|
||||||
|
label: 'جاری',
|
||||||
|
statuses: orderListTabStatuses[TabOrderListEnum.IN_PROGRESS],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
key: TabOrderListEnum.FINISHED,
|
||||||
|
label: 'پایان یافته',
|
||||||
|
statuses: orderListTabStatuses[TabOrderListEnum.FINISHED],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: TabOrderListEnum.CANCELED,
|
||||||
|
label: 'لغو شده',
|
||||||
|
statuses: orderListTabStatuses[TabOrderListEnum.CANCELED],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: TabOrderListEnum.INVOICED,
|
||||||
|
label: 'فاکتور شده',
|
||||||
|
statuses: orderListTabStatuses[TabOrderListEnum.INVOICED],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
export const getOrderStatusLabel = (status: string) =>
|
export const getOrderStatusLabel = (status: string) =>
|
||||||
orderStatusLabels[status as OrderStatusEnum] ?? status
|
orderStatusLabels[status as OrderStatusEnum] ?? status
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ export enum OrderStatusEnum {
|
|||||||
CREATED = "created",
|
CREATED = "created",
|
||||||
|
|
||||||
INVOICED = "invoiced",
|
INVOICED = "invoiced",
|
||||||
WAITING_to_CONFIRM_INVOICE = "waiting_to_confirm_invoice",
|
|
||||||
|
|
||||||
DESIGN_INITIATED = "design_initiated",
|
DESIGN_INITIATED = "design_initiated",
|
||||||
DESIGN_FINISHED = "design_finished",
|
DESIGN_FINISHED = "design_finished",
|
||||||
|
|||||||
Reference in New Issue
Block a user