This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { type FC, useEffect, useRef, useState } from "react";
|
||||
import { CloseCircle } from "iconsax-react";
|
||||
import { clx } from "@/helpers/utils";
|
||||
import { useDesignerSearch } from "./hooks/useDesignerSearch";
|
||||
import type { AdminItemType } from "../admin/types/Types";
|
||||
@@ -51,22 +52,51 @@ const DesignerSearch: FC<DesignerSearchProps> = ({
|
||||
const displayLabel = (designer: AdminItemType) =>
|
||||
`${designer.firstName} ${designer.lastName}`;
|
||||
|
||||
useEffect(() => {
|
||||
if (!value) {
|
||||
setSearch("");
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
const handleClear = () => {
|
||||
setSearch("");
|
||||
setIsOpen(false);
|
||||
onChange?.("");
|
||||
};
|
||||
|
||||
const hasValue = Boolean(value || search);
|
||||
|
||||
return (
|
||||
<div ref={wrapperRef} className={clx("w-full relative", className)}>
|
||||
{label && (
|
||||
<label className="text-sm text-primary-content block mb-1">{label}</label>
|
||||
)}
|
||||
<input
|
||||
type="text"
|
||||
value={search}
|
||||
onChange={(e) => {
|
||||
setSearch(e.target.value);
|
||||
setIsOpen(true);
|
||||
}}
|
||||
onFocus={() => setIsOpen(true)}
|
||||
placeholder={placeholder}
|
||||
className="w-full bg-white h-10 text-black block px-4 text-xs rounded-xl border border-border"
|
||||
/>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
value={search}
|
||||
onChange={(e) => {
|
||||
setSearch(e.target.value);
|
||||
setIsOpen(true);
|
||||
}}
|
||||
onFocus={() => setIsOpen(true)}
|
||||
placeholder={placeholder}
|
||||
className={clx(
|
||||
"w-full bg-white h-10 text-black block px-4 text-xs rounded-xl border border-border",
|
||||
hasValue && "pl-10",
|
||||
)}
|
||||
/>
|
||||
{hasValue && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClear}
|
||||
className="absolute top-0 bottom-0 my-auto left-3 z-10 flex items-center"
|
||||
aria-label="پاک کردن"
|
||||
>
|
||||
<CloseCircle size={16} color="#8C90A3" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{isOpen && debouncedSearch.length > 0 && (
|
||||
<div className="absolute z-10 top-full left-0 right-0 mt-1 bg-white border border-border rounded-xl shadow-lg max-h-60 overflow-auto">
|
||||
{isFetching ? (
|
||||
|
||||
+163
-250
@@ -1,257 +1,170 @@
|
||||
import Filters, { type FilterValues } from '@/components/Filters'
|
||||
import { type FC, useEffect, useMemo, useState } from 'react'
|
||||
import Table from '@/components/Table'
|
||||
import Tabs from '@/components/Tabs'
|
||||
import { AddSquare, Edit2, Printer } from 'iconsax-react'
|
||||
import { useGetOrders } from './hooks/useOrderData'
|
||||
import moment from 'moment-jalaali'
|
||||
import { Link, useSearchParams } from 'react-router-dom'
|
||||
import { Paths } from '@/config/Paths'
|
||||
import type { OrderListItemType } from './types/Types'
|
||||
import Button from '@/components/Button'
|
||||
import RefreshButton from '@/components/RefreshButton'
|
||||
import UserSearch from '../user/UserSearch'
|
||||
import DesignerSearch from '../designer/DesignerSearch'
|
||||
import CategoriesSelect from '../product/components/CategoriesSelect'
|
||||
import { getOrderStatusBadgeClass, getOrderStatusLabel, orderListTabStatuses, orderListTabs } from './constants/orderStatus'
|
||||
import { TabOrderListEnum } from './enum/OrderEnum'
|
||||
import { type FC, useMemo, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { AddSquare, ArrowDown2, Filter } from "iconsax-react";
|
||||
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 { 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 {
|
||||
ORDER_LIST_FILTER_FIELDS,
|
||||
useOrderListParams,
|
||||
} from "./hooks/useOrderListParams";
|
||||
import type { OrderListItemType } from "./types/Types";
|
||||
import { TabOrderListEnum } from "./enum/OrderEnum";
|
||||
|
||||
const countActiveFilters = (params: {
|
||||
search?: string;
|
||||
from?: string;
|
||||
to?: string;
|
||||
userId?: string;
|
||||
designerId?: string;
|
||||
categoryId?: string;
|
||||
}) =>
|
||||
[params.search, params.from, params.to, params.userId, params.designerId, params.categoryId]
|
||||
.filter(Boolean).length;
|
||||
|
||||
const OrdersList: FC = () => {
|
||||
const [searchParams, setSearchParams] = useSearchParams()
|
||||
const [isFiltersExpanded, setIsFiltersExpanded] = useState(false);
|
||||
|
||||
const [activeTab, setActiveTab] = useState<TabOrderListEnum>(
|
||||
() => (searchParams.get('tab') as TabOrderListEnum) || TabOrderListEnum.IN_PROGRESS,
|
||||
)
|
||||
const [designId, setDesignId] = useState<string | undefined>(
|
||||
() => searchParams.get('designerId') ?? searchParams.get('designId') ?? undefined,
|
||||
)
|
||||
const [categoryId, setCategoryId] = useState<string | undefined>(
|
||||
() => searchParams.get('categoryId') ?? undefined,
|
||||
)
|
||||
const [userId, setUserId] = useState<string | undefined>(
|
||||
() => searchParams.get('userId') ?? undefined,
|
||||
)
|
||||
const [filters, setFilters] = useState<FilterValues>({})
|
||||
const {
|
||||
params,
|
||||
queryParams,
|
||||
filterInitialValues,
|
||||
setTab,
|
||||
setFilters,
|
||||
updateParams,
|
||||
} = useOrderListParams();
|
||||
|
||||
useEffect(() => {
|
||||
const nextCategoryId = searchParams.get('categoryId') ?? undefined
|
||||
const nextUserId = searchParams.get('userId') ?? undefined
|
||||
const nextDesignId =
|
||||
searchParams.get('designerId') ?? searchParams.get('designId') ?? undefined
|
||||
const nextTab = (searchParams.get('tab') as TabOrderListEnum) || TabOrderListEnum.IN_PROGRESS
|
||||
const { data, refetch, isFetching } = useGetOrders(queryParams);
|
||||
const activeFilterCount = useMemo(() => countActiveFilters(params), [params]);
|
||||
const hasActiveFilters = activeFilterCount > 0;
|
||||
|
||||
setCategoryId(nextCategoryId)
|
||||
setUserId(nextUserId)
|
||||
setDesignId(nextDesignId)
|
||||
setActiveTab(nextTab)
|
||||
}, [searchParams])
|
||||
|
||||
const updateSearchParam = (key: string, value?: string) => {
|
||||
setSearchParams((current) => {
|
||||
const next = new URLSearchParams(current)
|
||||
if (value) {
|
||||
next.set(key, value)
|
||||
} else {
|
||||
next.delete(key)
|
||||
}
|
||||
return next
|
||||
}, { replace: true })
|
||||
}
|
||||
|
||||
const statuses = useMemo(
|
||||
() => orderListTabStatuses[activeTab],
|
||||
[activeTab],
|
||||
)
|
||||
|
||||
const { data, refetch, isFetching } = useGetOrders({ designId, categoryId, userId, statuses, ...filters })
|
||||
|
||||
const handleTabChange = (tab: string) => {
|
||||
const nextTab = tab as TabOrderListEnum
|
||||
setActiveTab(nextTab)
|
||||
updateSearchParam('tab', nextTab)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='mt-5'>
|
||||
<div className='flex justify-between items-center'>
|
||||
<h1 className='text-lg font-light'>سفارش ها</h1>
|
||||
<div className='flex items-center gap-3'>
|
||||
<RefreshButton onClick={() => refetch()} isLoading={isFetching} />
|
||||
<Link to={Paths.convertToOrder}>
|
||||
<Button className='w-fit px-6'>
|
||||
<div className='flex gap-2 items-center'>
|
||||
<AddSquare size={18} color='black' />
|
||||
سفارش جدید
|
||||
</div>
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='mt-8 flex flex-wrap items-end gap-4'>
|
||||
<Filters
|
||||
fields={[
|
||||
{
|
||||
name: 'search',
|
||||
type: 'input',
|
||||
placeholder: 'جستجو',
|
||||
},
|
||||
{
|
||||
name: 'from',
|
||||
type: 'date',
|
||||
placeholder: 'از تاریخ',
|
||||
},
|
||||
{
|
||||
name: 'to',
|
||||
type: 'date',
|
||||
placeholder: 'تا تریخ',
|
||||
},
|
||||
]}
|
||||
onChange={setFilters} />
|
||||
</div>
|
||||
|
||||
<div className='flex mt-6 gap-4'>
|
||||
<UserSearch
|
||||
placeholder='جستجوی کاربر'
|
||||
className='w-[220px]'
|
||||
value={userId ?? ''}
|
||||
onChange={(id) => {
|
||||
const nextUserId = id || undefined
|
||||
setUserId(nextUserId)
|
||||
updateSearchParam('userId', nextUserId)
|
||||
}}
|
||||
/>
|
||||
<DesignerSearch
|
||||
className='w-[220px]'
|
||||
placeholder='جستجوی طراح'
|
||||
value={designId ?? ''}
|
||||
onChange={(id) => {
|
||||
const nextDesignId = id || undefined
|
||||
setDesignId(nextDesignId)
|
||||
updateSearchParam('designerId', nextDesignId)
|
||||
}}
|
||||
/>
|
||||
<div className='w-[220px]'>
|
||||
<CategoriesSelect
|
||||
isDisableShowLable
|
||||
placeholder='جستجوی نوع'
|
||||
value={categoryId ?? ''}
|
||||
onChange={(e) => {
|
||||
const nextCategoryId = e.target.value || undefined
|
||||
setCategoryId(nextCategoryId)
|
||||
updateSearchParam('categoryId', nextCategoryId)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='mt-8'>
|
||||
<Tabs
|
||||
activeTab={activeTab}
|
||||
items={[...orderListTabs]}
|
||||
onTabChange={handleTabChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='mt-8'>
|
||||
<Table<OrderListItemType>
|
||||
columns={[
|
||||
{
|
||||
key: 'orderNumber',
|
||||
title: 'شماره',
|
||||
},
|
||||
{
|
||||
key: 'title',
|
||||
title: 'عنوان',
|
||||
render: (item) => (
|
||||
<Link
|
||||
to={Paths.order.details + item.id}
|
||||
className="text-[#0037FF] hover:underline"
|
||||
>
|
||||
{item.title}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'customer',
|
||||
title: 'مشتری',
|
||||
render: (item) => {
|
||||
return (
|
||||
<div>
|
||||
{item.user?.firstName ? item.user?.firstName + ' ' + item.user?.lastName : item.user?.phone}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
{
|
||||
key: 'product',
|
||||
title: 'نام محصول',
|
||||
render: (item) => (
|
||||
<div>{item.product?.title ?? '—'}</div>
|
||||
),
|
||||
},
|
||||
|
||||
|
||||
{
|
||||
key: 'designer',
|
||||
title: 'مجری / طراح',
|
||||
render: (item) => {
|
||||
const designer = item.designer
|
||||
if (!designer || typeof designer === 'string') {
|
||||
return <div>{typeof designer === 'string' ? designer : '—'}</div>
|
||||
}
|
||||
const name = `${designer.firstName ?? ''} ${designer.lastName ?? ''}`.trim()
|
||||
return <div>{name || designer.phone || '—'}</div>
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
title: 'وضعیت',
|
||||
render: (item) => (
|
||||
<span
|
||||
className={`inline-flex h-6 items-center rounded-full px-2.5 text-xs ${getOrderStatusBadgeClass(item.status)}`}
|
||||
>
|
||||
{getOrderStatusLabel(item.status)}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'createdAt',
|
||||
title: 'تاریخ ایجاد ',
|
||||
render: (item) => {
|
||||
return (
|
||||
<div>{moment(item.createdAt).format('jYYYY-jMM-jDD')}</div>
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
title: '',
|
||||
render: (item) => {
|
||||
return (
|
||||
<div className='flex gap-2 items-center'>
|
||||
<Link title='فرم چاپ' to={Paths.orderPrint + item.id}>
|
||||
<Printer
|
||||
size={20}
|
||||
color='black'
|
||||
/>
|
||||
</Link>
|
||||
<Link to={Paths.order.edit + item.id}>
|
||||
<Edit2 size={20} color='#0037FF' />
|
||||
</Link>
|
||||
{/* <Receipt21 size={20} color='#FF8000' /> */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
},
|
||||
]}
|
||||
data={data?.data}
|
||||
/>
|
||||
</div>
|
||||
return (
|
||||
<div className="mt-5 space-y-6">
|
||||
<div className="flex flex-wrap items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-lg font-light">سفارشها</h1>
|
||||
<p className="mt-1 text-sm text-gray-500">
|
||||
مدیریت و پیگیری سفارشهای ثبتشده
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default OrdersList
|
||||
<div className="flex items-center gap-3">
|
||||
<RefreshButton onClick={() => refetch()} isLoading={isFetching} />
|
||||
<Link to={Paths.convertToOrder}>
|
||||
<Button className="w-fit px-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<AddSquare size={18} color="black" />
|
||||
سفارش جدید
|
||||
</div>
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs
|
||||
activeTab={params.tab}
|
||||
items={[...orderListTabs]}
|
||||
onTabChange={(tab) => setTab(tab as TabOrderListEnum)}
|
||||
/>
|
||||
|
||||
<div className="overflow-hidden rounded-3xl bg-white">
|
||||
<div className="flex items-center justify-between gap-3 p-5 md:px-6">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsFiltersExpanded((prev) => !prev)}
|
||||
className="flex flex-1 items-center gap-2 text-right"
|
||||
>
|
||||
<Filter size={18} color="#8C90A3" />
|
||||
<span className="text-sm font-medium text-gray-700">فیلترها</span>
|
||||
{hasActiveFilters && (
|
||||
<span className="inline-flex h-5 min-w-5 items-center justify-center rounded-full bg-primary px-1.5 text-xs font-medium text-black">
|
||||
{activeFilterCount.toLocaleString("fa-IR")}
|
||||
</span>
|
||||
)}
|
||||
<ArrowDown2
|
||||
size={18}
|
||||
color="#8C90A3"
|
||||
className={clx(
|
||||
"mr-auto transition-transform duration-200",
|
||||
isFiltersExpanded && "rotate-180",
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{hasActiveFilters && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
updateParams({
|
||||
search: undefined,
|
||||
from: undefined,
|
||||
to: undefined,
|
||||
userId: undefined,
|
||||
designerId: undefined,
|
||||
categoryId: undefined,
|
||||
})
|
||||
}
|
||||
className="shrink-0 text-xs text-[#0037FF] hover:underline"
|
||||
>
|
||||
پاک کردن فیلترها
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isFiltersExpanded && (
|
||||
<div className="space-y-5 border-t border-gray-100 px-5 pb-5 pt-4 md:px-6 md:pb-6">
|
||||
<Filters
|
||||
fields={ORDER_LIST_FILTER_FIELDS}
|
||||
initialValues={filterInitialValues}
|
||||
onChange={setFilters}
|
||||
className="w-full"
|
||||
fieldClassName="grid w-full grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-3"
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-3">
|
||||
<UserSearch
|
||||
placeholder="جستجوی کاربر"
|
||||
className="w-full"
|
||||
value={params.userId ?? ""}
|
||||
onChange={(id) => updateParams({ userId: id || undefined })}
|
||||
/>
|
||||
<DesignerSearch
|
||||
className="w-full"
|
||||
placeholder="جستجوی طراح"
|
||||
value={params.designerId ?? ""}
|
||||
onChange={(id) => updateParams({ designerId: id || undefined })}
|
||||
/>
|
||||
<CategoriesSelect
|
||||
isDisableShowLable
|
||||
placeholder="جستجوی نوع"
|
||||
value={params.categoryId ?? ""}
|
||||
onChange={(e) =>
|
||||
updateParams({ categoryId: e.target.value || undefined })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Table<OrderListItemType>
|
||||
columns={orderListColumns}
|
||||
data={data?.data}
|
||||
isLoading={isFetching}
|
||||
noDataMessage="سفارشی یافت نشد"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OrdersList;
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
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 type { OrderListItemType } from "../types/Types";
|
||||
|
||||
const getCustomerName = (item: OrderListItemType) => {
|
||||
const user = item.user;
|
||||
if (!user) return "—";
|
||||
|
||||
const fullName = [user.firstName, user.lastName].filter(Boolean).join(" ");
|
||||
return fullName || user.phone || "—";
|
||||
};
|
||||
|
||||
const getDesignerName = (item: OrderListItemType) => {
|
||||
const designer = item.designer;
|
||||
if (!designer || typeof designer === "string") {
|
||||
return typeof designer === "string" ? designer : "—";
|
||||
}
|
||||
|
||||
const name = [designer.firstName, designer.lastName].filter(Boolean).join(" ");
|
||||
return name || designer.phone || "—";
|
||||
};
|
||||
|
||||
export const orderListColumns: ColumnType<OrderListItemType>[] = [
|
||||
{
|
||||
key: "orderNumber",
|
||||
title: "شماره",
|
||||
},
|
||||
{
|
||||
key: "title",
|
||||
title: "عنوان",
|
||||
render: (item) => (
|
||||
<Link
|
||||
to={Paths.order.details + item.id}
|
||||
className="font-medium text-[#0037FF] hover:underline"
|
||||
>
|
||||
{item.title}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "customer",
|
||||
title: "مشتری",
|
||||
render: (item) => <span>{getCustomerName(item)}</span>,
|
||||
},
|
||||
{
|
||||
key: "product",
|
||||
title: "نام محصول",
|
||||
render: (item) => <span>{item.product?.title ?? "—"}</span>,
|
||||
},
|
||||
{
|
||||
key: "designer",
|
||||
title: "مجری / طراح",
|
||||
render: (item) => <span>{getDesignerName(item)}</span>,
|
||||
},
|
||||
{
|
||||
key: "status",
|
||||
title: "وضعیت",
|
||||
render: (item) => (
|
||||
<span
|
||||
className={`inline-flex h-6 items-center rounded-full px-2.5 text-xs ${getOrderStatusBadgeClass(item.status)}`}
|
||||
>
|
||||
{getOrderStatusLabel(item.status)}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "createdAt",
|
||||
title: "تاریخ ایجاد",
|
||||
render: (item) => (
|
||||
<span className="dltr text-right">
|
||||
{moment(item.createdAt).format("jYYYY-jMM-jDD")}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "actions",
|
||||
title: "",
|
||||
render: (item) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<Link title="فرم چاپ" to={Paths.orderPrint + item.id}>
|
||||
<Printer size={20} color="black" />
|
||||
</Link>
|
||||
<Link title="ویرایش" to={Paths.order.edit + item.id}>
|
||||
<Edit2 size={20} color="#0037FF" />
|
||||
</Link>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
@@ -1,4 +1,4 @@
|
||||
export const enum TabOrderListEnum {
|
||||
export enum TabOrderListEnum {
|
||||
IN_PROGRESS = "in_progress",
|
||||
FINISHED = "finished",
|
||||
CANCELED = "canceled",
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import type { FilterValues } from "@/components/Filters";
|
||||
import { orderListTabStatuses } from "../constants/orderStatus";
|
||||
import { TabOrderListEnum } from "../enum/OrderEnum";
|
||||
import type { GetOrdersParams } from "../service/OrderService";
|
||||
|
||||
const DEFAULT_TAB = TabOrderListEnum.IN_PROGRESS;
|
||||
|
||||
const isValidTab = (value: string | null): value is TabOrderListEnum =>
|
||||
Object.values(TabOrderListEnum).includes(value as TabOrderListEnum);
|
||||
|
||||
export type OrderListQueryParams = {
|
||||
tab: TabOrderListEnum;
|
||||
search?: string;
|
||||
from?: string;
|
||||
to?: string;
|
||||
userId?: string;
|
||||
designerId?: string;
|
||||
categoryId?: string;
|
||||
};
|
||||
|
||||
export const ORDER_LIST_FILTER_FIELDS = [
|
||||
{
|
||||
name: "search",
|
||||
type: "input" as const,
|
||||
placeholder: "جستجو (شماره سفارش / مشتری)",
|
||||
},
|
||||
{
|
||||
name: "from",
|
||||
type: "date" as const,
|
||||
placeholder: "از تاریخ",
|
||||
},
|
||||
{
|
||||
name: "to",
|
||||
type: "date" as const,
|
||||
placeholder: "تا تاریخ",
|
||||
},
|
||||
];
|
||||
|
||||
const readQueryParams = (searchParams: URLSearchParams): OrderListQueryParams => {
|
||||
const tabParam = searchParams.get("tab");
|
||||
const tab = isValidTab(tabParam) ? tabParam : DEFAULT_TAB;
|
||||
|
||||
const read = (key: string) => searchParams.get(key) ?? undefined;
|
||||
|
||||
return {
|
||||
tab,
|
||||
search: read("search"),
|
||||
from: read("from"),
|
||||
to: read("to"),
|
||||
userId: read("userId"),
|
||||
designerId: read("designerId") ?? read("designId"),
|
||||
categoryId: read("categoryId"),
|
||||
};
|
||||
};
|
||||
|
||||
export const useOrderListParams = () => {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const params = useMemo(() => readQueryParams(searchParams), [searchParams]);
|
||||
|
||||
const updateParams = useCallback(
|
||||
(updates: Partial<Record<keyof OrderListQueryParams, string | undefined>>) => {
|
||||
setSearchParams(
|
||||
(current) => {
|
||||
const next = new URLSearchParams(current);
|
||||
|
||||
for (const [key, value] of Object.entries(updates)) {
|
||||
if (value) {
|
||||
next.set(key, value);
|
||||
} else {
|
||||
next.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
if (updates.designerId === undefined && "designerId" in updates) {
|
||||
next.delete("designId");
|
||||
}
|
||||
|
||||
return next;
|
||||
},
|
||||
{ replace: true },
|
||||
);
|
||||
},
|
||||
[setSearchParams],
|
||||
);
|
||||
|
||||
const setTab = useCallback(
|
||||
(tab: TabOrderListEnum) => updateParams({ tab }),
|
||||
[updateParams],
|
||||
);
|
||||
|
||||
const setFilters = useCallback(
|
||||
(filters: FilterValues) => {
|
||||
updateParams({
|
||||
search: filters.search?.trim() || undefined,
|
||||
from: filters.from || undefined,
|
||||
to: filters.to || undefined,
|
||||
});
|
||||
},
|
||||
[updateParams],
|
||||
);
|
||||
|
||||
const filterInitialValues = useMemo<FilterValues>(
|
||||
() => ({
|
||||
search: params.search ?? null,
|
||||
from: params.from ?? null,
|
||||
to: params.to ?? null,
|
||||
}),
|
||||
[params.search, params.from, params.to],
|
||||
);
|
||||
|
||||
const queryParams = useMemo<GetOrdersParams>(
|
||||
() => ({
|
||||
designId: params.designerId,
|
||||
categoryId: params.categoryId,
|
||||
userId: params.userId,
|
||||
search: params.search ?? null,
|
||||
from: params.from ?? null,
|
||||
to: params.to ?? null,
|
||||
statuses: orderListTabStatuses[params.tab],
|
||||
}),
|
||||
[params],
|
||||
);
|
||||
|
||||
return {
|
||||
params,
|
||||
queryParams,
|
||||
filterInitialValues,
|
||||
setTab,
|
||||
setFilters,
|
||||
updateParams,
|
||||
};
|
||||
};
|
||||
@@ -6,13 +6,14 @@ type Props = {
|
||||
error_text?: string,
|
||||
isDisableShowLable?: boolean,
|
||||
placeholder?: string,
|
||||
|
||||
clearable?: boolean,
|
||||
} & SelectHTMLAttributes<HTMLSelectElement>
|
||||
|
||||
const CategoriesSelect: FC<Props> = ({
|
||||
isDisableShowLable,
|
||||
placeholder,
|
||||
error_text,
|
||||
clearable = true,
|
||||
...selectProps
|
||||
}) => {
|
||||
|
||||
@@ -23,6 +24,7 @@ const CategoriesSelect: FC<Props> = ({
|
||||
label={isDisableShowLable ? undefined : 'دسته'}
|
||||
placeholder={placeholder || 'انتخاب'}
|
||||
error_text={error_text}
|
||||
clearable={clearable}
|
||||
items={categories?.data?.map((item) => {
|
||||
return {
|
||||
label: item.title,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { type FC, useEffect, useRef, useState } from "react";
|
||||
import { CloseCircle } from "iconsax-react";
|
||||
import { clx } from "@/helpers/utils";
|
||||
import { useUserSearch } from "./hooks/useUserSearch";
|
||||
import type { UserType } from "./types/Types";
|
||||
@@ -51,22 +52,51 @@ const UserSearch: FC<UserSearchProps> = ({
|
||||
const displayLabel = (user: UserType) =>
|
||||
user.firstName && user.lastName ? `${user.firstName} ${user.lastName}` : user.phone;
|
||||
|
||||
useEffect(() => {
|
||||
if (!value) {
|
||||
setSearch("");
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
const handleClear = () => {
|
||||
setSearch("");
|
||||
setIsOpen(false);
|
||||
onChange?.("");
|
||||
};
|
||||
|
||||
const hasValue = Boolean(value || search);
|
||||
|
||||
return (
|
||||
<div ref={wrapperRef} className={clx("w-full relative", className)}>
|
||||
{label && (
|
||||
<label className="text-sm text-primary-content block mb-1">{label}</label>
|
||||
)}
|
||||
<input
|
||||
type="text"
|
||||
value={search}
|
||||
onChange={(e) => {
|
||||
setSearch(e.target.value);
|
||||
setIsOpen(true);
|
||||
}}
|
||||
onFocus={() => setIsOpen(true)}
|
||||
placeholder={placeholder}
|
||||
className="w-full bg-white h-10 text-black block px-4 text-xs rounded-xl border border-border"
|
||||
/>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
value={search}
|
||||
onChange={(e) => {
|
||||
setSearch(e.target.value);
|
||||
setIsOpen(true);
|
||||
}}
|
||||
onFocus={() => setIsOpen(true)}
|
||||
placeholder={placeholder}
|
||||
className={clx(
|
||||
"w-full bg-white h-10 text-black block px-4 text-xs rounded-xl border border-border",
|
||||
hasValue && "pl-10",
|
||||
)}
|
||||
/>
|
||||
{hasValue && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClear}
|
||||
className="absolute top-0 bottom-0 my-auto left-3 z-10 flex items-center"
|
||||
aria-label="پاک کردن"
|
||||
>
|
||||
<CloseCircle size={16} color="#8C90A3" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{isOpen && debouncedSearch.length > 0 && (
|
||||
<div className="absolute z-10 top-full left-0 right-0 mt-1 bg-white border border-border rounded-xl shadow-lg max-h-60 overflow-auto">
|
||||
{isFetching ? (
|
||||
|
||||
Reference in New Issue
Block a user