order status update modal
deploy to danak / build_and_deploy (push) Has been cancelled

This commit is contained in:
2026-07-08 20:02:00 +03:30
parent 9684b78b9f
commit 2afbcb33f2
6 changed files with 197 additions and 61 deletions
+27 -2
View File
@@ -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>
) )
} }
+13 -45
View File
@@ -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;
+28 -2
View File
@@ -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
-1
View File
@@ -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",