diff --git a/src/pages/order/List.tsx b/src/pages/order/List.tsx index 782f017..96c34c5 100644 --- a/src/pages/order/List.tsx +++ b/src/pages/order/List.tsx @@ -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 { AddSquare, ArrowDown2, Filter } from "iconsax-react"; +import { toast } from "react-toastify"; import Filters from "@/components/Filters"; import Table from "@/components/Table"; import Tabs from "@/components/Tabs"; import Button from "@/components/Button"; import RefreshButton from "@/components/RefreshButton"; import { Paths } from "@/config/Paths"; +import { extractErrorMessage } from "@/config/func"; import { clx } from "@/helpers/utils"; import UserSearch from "../user/UserSearch"; import DesignerSearch from "../designer/DesignerSearch"; import CategoriesSelect from "../product/components/CategoriesSelect"; import { orderListTabs } from "./constants/orderStatus"; -import { orderListColumns } from "./components/OrderListColumns"; -import { useGetOrders } from "./hooks/useOrderData"; +import { getOrderListColumns } from "./components/OrderListColumns"; +import { useDeleteOrder, useGetOrders } from "./hooks/useOrderData"; import { ORDER_LIST_FILTER_FIELDS, useOrderListParams, @@ -45,9 +47,30 @@ const OrdersList: FC = () => { } = useOrderListParams(); const { data, refetch, isFetching } = useGetOrders(queryParams); + const { mutate: deleteOrder, isPending: isDeleting } = useDeleteOrder(); const activeFilterCount = useMemo(() => countActiveFilters(params), [params]); 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 (
@@ -158,7 +181,7 @@ const OrdersList: FC = () => {
- columns={orderListColumns} + columns={columns} data={data?.data} isLoading={isFetching} noDataMessage="سفارشی یافت نشد" diff --git a/src/pages/order/OrderDetail.tsx b/src/pages/order/OrderDetail.tsx index 9643f10..8c03892 100644 --- a/src/pages/order/OrderDetail.tsx +++ b/src/pages/order/OrderDetail.tsx @@ -1,185 +1,85 @@ import { type FC } from 'react' -import { useParams } from 'react-router-dom' -import moment from 'moment-jalaali' -import { User } from 'iconsax-react' +import { Link, useParams } from 'react-router-dom' +import { Edit2, Printer } from 'iconsax-react' import BackButton from '@/components/BackButton' import RefreshButton from '@/components/RefreshButton' +import Button from '@/components/Button' import { Paths } from '@/config/Paths' import { useGetOrderDetails } from './hooks/useOrderData' import TicketSection from './components/TicketSection' -import OrderItem from './components/OrderItem' -import type { OrderDetailDataType, OrderItemType } 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 }) => ( -
- {label} - {value ?? '—'} -
-) +import OrderDetailSidebar from './components/OrderDetailSidebar' +import InvoiceItemDetailSection from './components/InvoiceItemDetailSection' +import type { OrderDetailDataType } from './types/Types' const OrderDetail: FC = () => { const { id } = useParams() - const { data, refetch, isFetching } = useGetOrderDetails(id!) - const order = data?.data as (OrderDetailDataType & { items?: OrderItemType[] }) | undefined - const creator = order?.creator - const creatorName = creator ? `${creator.firstName} ${creator.lastName}`.trim() : '' + const { data, refetch, isFetching, isPending } = useGetOrderDetails(id!) + const order = data?.data as OrderDetailDataType | undefined + + if (isPending) { + return ( +
+
+
+
+
+
+
+
+
+
+
+
+ ) + } return ( -
- {/* Header */} -
- -
+
+
+
+ +
+

جزئیات سفارش

+

+ مشاهده اطلاعات سفارش، آیتم پیش‌فاکتور و گفتگو +

+
+
+ +
refetch()} isLoading={isFetching} /> - سفارش #{order?.orderNumber} + + + + + +
-
+
- {/* اطلاعات سفارش */} -
-
-

اطلاعات سفارش

+
+
+ + +
-
- {/* شماره و وضعیت — برجسته */} -
-
- شماره سفارش - #{order?.orderNumber ?? '—'} -
- {order?.status && ( - - {order.status} - - )} -
- {/* جزئیات در گرید منظم */} -
-
-
-

تاریخ ایجاد

-

- {order?.createdAt ? moment(order.createdAt).format('jYYYY/jMM/jDD — HH:mm') : '—'} -

-
-
-

تخمین زمان (روز)

-

{order?.estimatedDays ?? '—'}

-
-
-

عنوان

-

{order?.title ?? '—'}

-
-
-
-
-

نوع / دسته‌بندی

-

{order?.type?.title ?? '—'}

-
-
-

محصول

-

{order?.product?.title ?? '—'}

-
-
-

شناسه آیتم فاکتور

-

{order?.invoiceItem ?? '—'}

-
-
-
-
-
- {/* اطلاعات مشتری */} - {order?.user && ( -
-
-

اطلاعات مشتری

-
-
-
-
- - -
-
- -
-
-
-
- )} - - {/* ایجادکننده سفارش — کارت کامل */} - {creator && ( -
-
-

ایجادکننده سفارش

-
-
-
- {/* آواتار و نام */} -
-
- -
-
-

{creatorName || '—'}

-

ادمین ثبت‌کننده

-
-
- -
-
-
- )} - - {/* پیوست‌ها */} - {order?.attachments?.length ? ( -
-
-

پیوست‌ها

-
-
- {order.attachments.map((att, idx) => { - const url = typeof att === 'string' ? att : (att as { url?: string })?.url - if (!url) return null - return ( - - پیوست {idx + 1} - - ) - })} -
-
- ) : null} - - {/* اقلام سفارش */} - {order?.items?.length ? ( -
-
-

اقلام سفارش

-
-
- {order.items.map((item) => ( - - ))} -
-
- ) : null} - - + +
) } -export default OrderDetail \ No newline at end of file +export default OrderDetail diff --git a/src/pages/order/components/InvoiceItemDetailSection.tsx b/src/pages/order/components/InvoiceItemDetailSection.tsx new file mode 100644 index 0000000..2a84767 --- /dev/null +++ b/src/pages/order/components/InvoiceItemDetailSection.tsx @@ -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 = ({ label, children, className }) => ( +
+

{label}

+
{children}
+
+) + +const InvoiceItemDetailSection: FC = ({ 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 ( +
+

آیتم فاکتوری به این سفارش متصل نیست.

+
+ ) + } + + if (isLoading) { + return ( +
+
+
+
+
+
+
+ {Array.from({ length: 6 }).map((_, i) => ( +
+ ))} +
+
+
+ ) + } + + if (isError || !invoiceItem) { + return ( +
+

آیتم پیش‌فاکتور یافت نشد.

+
+ ) + } + + return ( +
+
+

جزئیات آیتم پیش‌فاکتور

+

+ اطلاعات مالی و محصول مرتبط با این سفارش +

+
+ +
+
+
+ {imageUrl ? ( + {invoiceItem.product?.title} + ) : ( +
+ +
+ )} +
+ +
+ + + #{invoiceItem.invoice.invoiceNumber} + + + + {getUserDisplayName(invoiceItem.invoice.user)} + + + {invoiceItem.product?.title ?? '—'} + + + {invoiceItem.quantity?.toLocaleString('fa-IR') ?? '—'} + + + {invoiceItem.unitPrice != null + ? formatAmount(invoiceItem.unitPrice) + : '—'} + + + {discounts?.value ?? '—'} + + + {discounts?.percent ?? '—'} + + + + {formatAmount(invoiceItem.total ?? 0)} + + + + {invoiceItem.confirmedAt + ? moment(invoiceItem.confirmedAt).format('jYYYY/jMM/jDD HH:mm') + : '—'} + +
+
+ + {invoiceItem.description && ( +
+

توضیحات

+

+ {invoiceItem.description} +

+
+ )} +
+
+ ) +} + +export default InvoiceItemDetailSection diff --git a/src/pages/order/components/OrderDetailSidebar.tsx b/src/pages/order/components/OrderDetailSidebar.tsx new file mode 100644 index 0000000..a508e7a --- /dev/null +++ b/src/pages/order/components/OrderDetailSidebar.tsx @@ -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 = ({ label, value, mono }) => ( +
+
{label}
+
+ {value ?? '—'} +
+
+) + +const PersonCard: FC<{ + title: string + name: string + subtitle?: string + phone?: string + icon?: 'user' | 'designer' +}> = ({ title, name, subtitle, phone, icon = 'user' }) => ( +
+

{title}

+
+
+ {icon === 'designer' ? ( + + ) : ( + + )} +
+
+

{name}

+ {subtitle &&

{subtitle}

} + {phone && ( +

+ + {phone} +

+ )} +
+
+
+) + +const OrderDetailSidebar: FC = ({ order }) => { + const attachments = (order?.attachments ?? []) + .map(getAttachmentUrl) + .filter((url): url is string => Boolean(url)) + + return ( +
+
+
+
+

شماره سفارش

+

+ #{order?.orderNumber ?? '—'} +

+
+
+ {order?.id && ( + + + + )} + {order?.status && ( + + {getOrderStatusLabel(order.status)} + + )} +
+
+ +
+ + + + + +
+
+ + {order?.user && ( +
+

مشتری

+ + {order.user.addresse && ( +
+ +

{order.user.addresse}

+
+ )} +
+ )} + + {(order?.creator || order?.designer) && ( +
+

تیم

+ {order.creator && ( + + )} + {order.designer && ( + + )} +
+ )} + + {attachments.length > 0 && ( +
+

پیوست‌ها

+
+ {attachments.map((url, idx) => { + const { fileName } = getFileNameAndExtensionFromUrl(url) + return ( + + + {fileName || `پیوست ${idx + 1}`} + + ) + })} +
+
+ )} +
+ ) +} + +export default OrderDetailSidebar diff --git a/src/pages/order/components/OrderItem.tsx b/src/pages/order/components/OrderItem.tsx index bf6891a..c04593c 100644 --- a/src/pages/order/components/OrderItem.tsx +++ b/src/pages/order/components/OrderItem.tsx @@ -4,86 +4,134 @@ import { useGetEntityField } from '@/pages/formBuilder/hooks/useFormBuilderData' import Button from '@/components/Button' import { Link, useParams } from 'react-router-dom' import { Paths } from '@/config/Paths' +import { clx } from '@/helpers/utils' +import { DocumentText, Printer } from 'iconsax-react' type Props = { - item: OrderItemType, - estimatedDays?: number, + item: OrderItemType + estimatedDays?: number + designerName?: string + index?: number } -const OrderItem: FC = ({ item, estimatedDays }) => { +const formatPrice = (value?: number | null) => + value != null ? value.toLocaleString('fa-IR') + ' ریال' : '—' +const OrderItem: FC = ({ item, estimatedDays, designerName, index }) => { const { id } = useParams() 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 ( -
-
-
#{item.id}
+
+
+
+ + {index ?? item.id} + +
+

+ {item.product?.title ?? '—'} +

+

شناسه: {item.id}

+
+
-
- {/* Product Image */} -
- -
- {/* Order Details */} -
-
-
عنوان:
-
{item.product.title}
-
-
-
نام طرح:
-
{'orderData.designer'}
-
-
-
تعداد:
-
{item.quantity}
-
-
-
تخمین زمان:
-
{estimatedDays}
-
-
-
- - {/* Order Description */} -
-
شرح سفارش:
-

- {item.description} -

-
- -
- ویژگی ها -
- { - fields?.data?.map((field) => { - const value = item.attributes?.find((o) => String(o.attributeId) === String(field.id)) - - - return ( -
-
- {field.name} : -
-
- {value?.value || '-'} +
+
+
+ {imageUrl ? ( + {item.product?.title} + ) : ( +
+
+ )} +
+ +
+
+ {stats.map(({ label, value }) => ( +
+

{label}

+

{value}

+
+ ))}
- ) - }) - } -
+ + {designerName && designerName !== '—' && ( +

+ طراح: {designerName} +

+ )} +
+
+ + {item.description && ( +
+

شرح سفارش

+

{item.description}

+
+ )} + + {fields?.data && fields.data.length > 0 && ( +
+

ویژگی‌ها

+
+ {fields.data.map((field) => { + const value = item.attributes?.find( + (attr) => String(attr.attributeId) === String(field.id), + ) + return ( +
+ {field.name} + + {value?.value || '—'} + +
+ ) + })} +
+
+ )} +
+
) } -export default OrderItem \ No newline at end of file +export default OrderItem diff --git a/src/pages/order/components/OrderListActions.tsx b/src/pages/order/components/OrderListActions.tsx new file mode 100644 index 0000000..bc8c69d --- /dev/null +++ b/src/pages/order/components/OrderListActions.tsx @@ -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 = ({ item, onDelete, isDeleting }) => { + const invoiceRef = getOrderInvoiceItemRef(item); + + return ( +
+ + + + + + + {invoiceRef?.invoiceId && ( + + + + )} + + + + + + + + + + onDelete(item.id)} + isloading={isDeleting} + colorIcon="red" + /> +
+ ); +}; + +export default OrderListActions; diff --git a/src/pages/order/components/OrderListColumns.tsx b/src/pages/order/components/OrderListColumns.tsx index c47f11a..849aa3d 100644 --- a/src/pages/order/components/OrderListColumns.tsx +++ b/src/pages/order/components/OrderListColumns.tsx @@ -1,12 +1,10 @@ -import { Link } from "react-router-dom"; import moment from "moment-jalaali"; -import { Edit2, Printer } from "iconsax-react"; import type { ColumnType } from "@/components/types/TableTypes"; -import { Paths } from "@/config/Paths"; import { getOrderStatusBadgeClass, getOrderStatusLabel, } from "../constants/orderStatus"; +import OrderListActions from "./OrderListActions"; import type { OrderListItemType } from "../types/Types"; const getCustomerName = (item: OrderListItemType) => { @@ -27,7 +25,15 @@ const getDesignerName = (item: OrderListItemType) => { return name || designer.phone || "—"; }; -export const orderListColumns: ColumnType[] = [ +type OrderListColumnsOptions = { + onDelete: (id: string) => void; + isDeleting?: boolean; +}; + +export const getOrderListColumns = ({ + onDelete, + isDeleting, +}: OrderListColumnsOptions): ColumnType[] => [ { key: "orderNumber", title: "شماره", @@ -35,14 +41,7 @@ export const orderListColumns: ColumnType[] = [ { key: "title", title: "عنوان", - render: (item) => ( - - {item.title} - - ), + render: (item) => {item.title}, }, { key: "customer", @@ -83,14 +82,11 @@ export const orderListColumns: ColumnType[] = [ key: "actions", title: "", render: (item) => ( -
- - - - - - -
+ ), }, ]; diff --git a/src/pages/order/components/TicketSection.tsx b/src/pages/order/components/TicketSection.tsx index 62a4186..6df98be 100644 --- a/src/pages/order/components/TicketSection.tsx +++ b/src/pages/order/components/TicketSection.tsx @@ -88,11 +88,15 @@ const TicketSection: FC = () => { } return ( -
-
-

گفتگو

+
+
+
+

گفتگو

+

پیام‌ها و فایل‌های مرتبط با سفارش

+
refetch()} isLoading={isFetching} />
+
{ data?.data?.map((item) => { if (item.user) @@ -250,6 +254,7 @@ const TicketSection: FC = () => { isLoading={singleUpload.isPending || multiUpload.isPending || addTicket.isPending} />
+
) } diff --git a/src/pages/order/hooks/useOrderData.ts b/src/pages/order/hooks/useOrderData.ts index 4608f29..53848fc 100644 --- a/src/pages/order/hooks/useOrderData.ts +++ b/src/pages/order/hooks/useOrderData.ts @@ -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 type { AddTicketType, @@ -121,3 +121,14 @@ export const useGetOrderPrint = (orderId: string) => { enabled: !!orderId, }); }; + +export const useDeleteOrder = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: api.deleteOrder, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["orders"] }); + }, + }); +}; diff --git a/src/pages/order/service/OrderService.ts b/src/pages/order/service/OrderService.ts index 6ec9c3e..42d716e 100644 --- a/src/pages/order/service/OrderService.ts +++ b/src/pages/order/service/OrderService.ts @@ -143,3 +143,8 @@ export const getOrderPrint = async ( ); return data; }; + +export const deleteOrder = async (orderId: string) => { + const { data } = await axios.delete(`/admin/orders/${orderId}`); + return data; +}; diff --git a/src/pages/order/types/Types.ts b/src/pages/order/types/Types.ts index 1064d42..80e09aa 100644 --- a/src/pages/order/types/Types.ts +++ b/src/pages/order/types/Types.ts @@ -73,11 +73,16 @@ export interface OrderItemAttribute { export type OrderResponseType = BaseResponse; +export type OrderListInvoiceItemRef = { + id: string; + invoice?: { id: string } | string | null; +}; + export interface OrderListItemType extends RowDataType { id: string; createdAt: string; deletedAt: string | null; - invoiceItem: string; + invoiceItem: string | OrderListInvoiceItemRef | null; product: Product | null; print: string | null; title: string; diff --git a/src/pages/order/utils/orderDetailUtils.ts b/src/pages/order/utils/orderDetailUtils.ts new file mode 100644 index 0000000..4841dbd --- /dev/null +++ b/src/pages/order/utils/orderDetailUtils.ts @@ -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) diff --git a/src/pages/order/utils/orderListUtils.ts b/src/pages/order/utils/orderListUtils.ts new file mode 100644 index 0000000..0283b94 --- /dev/null +++ b/src/pages/order/utils/orderListUtils.ts @@ -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, + }; +};