This commit is contained in:
@@ -3,7 +3,7 @@ import DatePicker from 'react-multi-date-picker';
|
|||||||
import persian from 'react-date-object/calendars/persian';
|
import persian from 'react-date-object/calendars/persian';
|
||||||
import persian_fa from 'react-date-object/locales/persian_fa';
|
import persian_fa from 'react-date-object/locales/persian_fa';
|
||||||
import DateObject from 'react-date-object';
|
import DateObject from 'react-date-object';
|
||||||
import { Calendar } from 'iconsax-react';
|
import { Calendar, CloseCircle } from 'iconsax-react';
|
||||||
import { clx } from '@/helpers/utils';
|
import { clx } from '@/helpers/utils';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -16,8 +16,36 @@ type Props = {
|
|||||||
className?: string;
|
className?: string;
|
||||||
label?: string,
|
label?: string,
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
|
/** When false, opening the calendar does not auto-select today's date. */
|
||||||
|
onOpenPickNewDate?: boolean;
|
||||||
|
clearable?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const parseDefaultValue = (defaultValue: string): DateObject | null => {
|
||||||
|
const gregorian = new DateObject({
|
||||||
|
date: defaultValue,
|
||||||
|
format: 'YYYY-MM-DD',
|
||||||
|
calendar: persian,
|
||||||
|
locale: persian_fa,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (gregorian.isValid) {
|
||||||
|
return gregorian;
|
||||||
|
}
|
||||||
|
|
||||||
|
const jalali = new DateObject({
|
||||||
|
date: defaultValue,
|
||||||
|
format: 'jYYYY/jMM/jDD',
|
||||||
|
calendar: persian,
|
||||||
|
locale: persian_fa,
|
||||||
|
});
|
||||||
|
|
||||||
|
return jalali.isValid ? jalali : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatJalaliDate = (date: DateObject) =>
|
||||||
|
`${date.year}/${date.month.number}/${date.day}`;
|
||||||
|
|
||||||
const DatePickerComponent: FC<Props> = (props) => {
|
const DatePickerComponent: FC<Props> = (props) => {
|
||||||
const [value, setValue] = useState<DateObject | null>(null);
|
const [value, setValue] = useState<DateObject | null>(null);
|
||||||
|
|
||||||
@@ -28,22 +56,36 @@ const DatePickerComponent: FC<Props> = (props) => {
|
|||||||
}, [props.reset]);
|
}, [props.reset]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (value) {
|
if (props.reset) return;
|
||||||
const formattedDate = `${value.year}/${value.month.number}/${value.day}`;
|
|
||||||
props.onChange(formattedDate);
|
|
||||||
}
|
|
||||||
}, [value]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
if (props.defaultValue) {
|
||||||
if (props.defaultValue && !value) {
|
setValue(parseDefaultValue(props.defaultValue));
|
||||||
const defaultDate = new DateObject({
|
} else {
|
||||||
date: props.defaultValue,
|
setValue(null);
|
||||||
calendar: persian,
|
|
||||||
locale: persian_fa,
|
|
||||||
});
|
|
||||||
setValue(defaultDate);
|
|
||||||
}
|
}
|
||||||
}, [props.defaultValue, value]);
|
}, [props.defaultValue, props.reset]);
|
||||||
|
|
||||||
|
const handleChange = (
|
||||||
|
date: DateObject | DateObject[] | null,
|
||||||
|
{ isTyping }: { isTyping: boolean },
|
||||||
|
) => {
|
||||||
|
if (isTyping) return;
|
||||||
|
|
||||||
|
if (!date || Array.isArray(date)) {
|
||||||
|
setValue(null);
|
||||||
|
props.onChange('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selected = date as DateObject;
|
||||||
|
setValue(selected);
|
||||||
|
props.onChange(formatJalaliDate(selected));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClear = () => {
|
||||||
|
setValue(null);
|
||||||
|
props.onChange('');
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
@@ -54,15 +96,20 @@ const DatePickerComponent: FC<Props> = (props) => {
|
|||||||
<div className={clx(
|
<div className={clx(
|
||||||
'relative',
|
'relative',
|
||||||
props.readOnly && 'readOny',
|
props.readOnly && 'readOny',
|
||||||
props.label && 'mt-1'
|
props.label && 'mt-1',
|
||||||
|
props.clearable && Boolean(value) && 'date-picker--clearable',
|
||||||
)}>
|
)}>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
placeholder={props.placeholder}
|
placeholder={props.placeholder}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(date) => setValue(date as DateObject)}
|
onChange={handleChange}
|
||||||
|
onOpenPickNewDate={props.onOpenPickNewDate ?? false}
|
||||||
|
editable={false}
|
||||||
calendar={persian}
|
calendar={persian}
|
||||||
locale={persian_fa}
|
locale={persian_fa}
|
||||||
calendarPosition="bottom-right"
|
calendarPosition="bottom-right"
|
||||||
|
portal
|
||||||
|
zIndex={50}
|
||||||
className={props.className}
|
className={props.className}
|
||||||
readOnly={props.readOnly}
|
readOnly={props.readOnly}
|
||||||
/>
|
/>
|
||||||
@@ -77,9 +124,19 @@ const DatePickerComponent: FC<Props> = (props) => {
|
|||||||
color='#8c90a3'
|
color='#8c90a3'
|
||||||
className='absolute top-0 bottom-0 my-auto left-2 pointer-events-none'
|
className='absolute top-0 bottom-0 my-auto left-2 pointer-events-none'
|
||||||
/>
|
/>
|
||||||
|
{props.clearable && value && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleClear}
|
||||||
|
className="absolute top-0 bottom-0 my-auto left-9 z-10 flex items-center"
|
||||||
|
aria-label="پاک کردن"
|
||||||
|
>
|
||||||
|
<CloseCircle size={16} color="#8C90A3" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DatePickerComponent;
|
export default DatePickerComponent;
|
||||||
|
|||||||
@@ -122,6 +122,14 @@ const Filters: FC<FiltersProps> = ({
|
|||||||
const handleInputChange = (name: string, event: ChangeEvent<HTMLInputElement>) => {
|
const handleInputChange = (name: string, event: ChangeEvent<HTMLInputElement>) => {
|
||||||
const value = event.target.value;
|
const value = event.target.value;
|
||||||
setInputValues(prev => ({ ...prev, [name]: value }));
|
setInputValues(prev => ({ ...prev, [name]: value }));
|
||||||
|
if (!value) {
|
||||||
|
handleChange(name, null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasFieldValue = (field: FieldType) => {
|
||||||
|
const currentValue = field.type === 'input' ? inputValues[field.name] : filters[field.name];
|
||||||
|
return Boolean(currentValue);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderField = (field: FieldType) => {
|
const renderField = (field: FieldType) => {
|
||||||
@@ -133,7 +141,18 @@ const Filters: FC<FiltersProps> = ({
|
|||||||
<DatePicker
|
<DatePicker
|
||||||
key={field.name}
|
key={field.name}
|
||||||
placeholder={field.placeholder}
|
placeholder={field.placeholder}
|
||||||
onChange={(value) => handleChange(field.name, moment(value, 'jYYYY/jMM/jDD').format('YYYY-MM-DD'))}
|
clearable={hasFieldValue(field)}
|
||||||
|
onChange={(value) => {
|
||||||
|
if (!value?.trim()) {
|
||||||
|
handleChange(field.name, null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = moment(value, 'jYYYY/jMM/jDD', true);
|
||||||
|
if (!parsed.isValid()) return;
|
||||||
|
|
||||||
|
handleChange(field.name, parsed.format('YYYY-MM-DD'));
|
||||||
|
}}
|
||||||
defaultValue={currentValue || field.defaultValue || ''}
|
defaultValue={currentValue || field.defaultValue || ''}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -143,8 +162,9 @@ const Filters: FC<FiltersProps> = ({
|
|||||||
<Select
|
<Select
|
||||||
key={field.name}
|
key={field.name}
|
||||||
placeholder={field.placeholder}
|
placeholder={field.placeholder}
|
||||||
onChange={(e) => handleChange(field.name, e.target.value)}
|
onChange={(e) => handleChange(field.name, e.target.value || null)}
|
||||||
defaultValue={currentValue || field.defaultValue || ''}
|
value={currentValue || ''}
|
||||||
|
clearable={hasFieldValue(field)}
|
||||||
items={field.options}
|
items={field.options}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -157,6 +177,7 @@ const Filters: FC<FiltersProps> = ({
|
|||||||
variant="search"
|
variant="search"
|
||||||
className="w-full md:min-w-[230px] md:max-w-[230px]"
|
className="w-full md:min-w-[230px] md:max-w-[230px]"
|
||||||
value={currentValue || field.defaultValue || ''}
|
value={currentValue || field.defaultValue || ''}
|
||||||
|
clearable={hasFieldValue(field)}
|
||||||
onChange={(e) => handleInputChange(field.name, e)}
|
onChange={(e) => handleInputChange(field.name, e)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { type FC, type InputHTMLAttributes, useEffect, useState } from 'react'
|
import { type FC, type InputHTMLAttributes, useEffect, useState } from 'react'
|
||||||
import { clx } from '../helpers/utils';
|
import { clx } from '../helpers/utils';
|
||||||
import { Eye, EyeSlash, SearchNormal } from 'iconsax-react';
|
import { CloseCircle, Eye, EyeSlash, SearchNormal } from 'iconsax-react';
|
||||||
import Error from './Error';
|
import Error from './Error';
|
||||||
|
|
||||||
type Variant = "floating_outlined" | "primary" | "search";
|
type Variant = "floating_outlined" | "primary" | "search";
|
||||||
@@ -15,6 +15,7 @@ type Props = {
|
|||||||
seprator?: boolean;
|
seprator?: boolean;
|
||||||
isNotRequired?: boolean;
|
isNotRequired?: boolean;
|
||||||
onChangeSearchFinal?: (value: string) => void;
|
onChangeSearchFinal?: (value: string) => void;
|
||||||
|
clearable?: boolean;
|
||||||
} & InputHTMLAttributes<HTMLInputElement>
|
} & InputHTMLAttributes<HTMLInputElement>
|
||||||
|
|
||||||
const formatNumber = (value: string | number): string => {
|
const formatNumber = (value: string | number): string => {
|
||||||
@@ -32,10 +33,13 @@ const Input: FC<Props> = (props: Props) => {
|
|||||||
const [showPassword, setShowPassword] = useState<boolean>(false)
|
const [showPassword, setShowPassword] = useState<boolean>(false)
|
||||||
const [search, setSearch] = useState<string>('')
|
const [search, setSearch] = useState<string>('')
|
||||||
|
|
||||||
|
const hasClearValue = Boolean(props.clearable && props.value);
|
||||||
|
|
||||||
const inputClass = clx(
|
const inputClass = clx(
|
||||||
'w-full bg-white h-10 text-black block px-4 text-xs rounded-xl border border-border',
|
'w-full bg-white h-10 text-black block px-4 text-xs rounded-xl border border-border',
|
||||||
props.readOnly && 'bg-gray-100 border-0 text-description',
|
props.readOnly && 'bg-gray-100 border-0 text-description',
|
||||||
props.variant === 'search' && 'border-0 ps-10 mt-0',
|
props.variant === 'search' && 'border-0 ps-10 mt-0',
|
||||||
|
hasClearValue && 'pl-10',
|
||||||
props.className
|
props.className
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -103,6 +107,23 @@ const Input: FC<Props> = (props: Props) => {
|
|||||||
<SearchNormal size={20} color='#8C90A3' className='absolute top-0 w-5 bottom-0 my-auto right-3' />
|
<SearchNormal size={20} color='#8C90A3' className='absolute top-0 w-5 bottom-0 my-auto right-3' />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
hasClearValue &&
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setSearch('');
|
||||||
|
props.onChange?.({
|
||||||
|
target: { value: '' },
|
||||||
|
} as React.ChangeEvent<HTMLInputElement>);
|
||||||
|
}}
|
||||||
|
className="absolute top-0 bottom-0 my-auto left-3 z-10 flex items-center"
|
||||||
|
aria-label="پاک کردن"
|
||||||
|
>
|
||||||
|
<CloseCircle size={16} color="#8C90A3" />
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
props.error_text &&
|
props.error_text &&
|
||||||
<Error
|
<Error
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { FC, SelectHTMLAttributes } from 'react'
|
import type { ChangeEvent, FC, SelectHTMLAttributes } from 'react'
|
||||||
import { clx } from '../helpers/utils'
|
import { clx } from '../helpers/utils'
|
||||||
import { ArrowDown2 } from 'iconsax-react'
|
import { ArrowDown2, CloseCircle } from 'iconsax-react'
|
||||||
|
|
||||||
export type ItemsSelectType = {
|
export type ItemsSelectType = {
|
||||||
value: string | number,
|
value: string | number,
|
||||||
@@ -13,6 +13,7 @@ type Props = {
|
|||||||
placeholder?: string,
|
placeholder?: string,
|
||||||
label?: string,
|
label?: string,
|
||||||
readOnly?: boolean,
|
readOnly?: boolean,
|
||||||
|
clearable?: boolean,
|
||||||
} & SelectHTMLAttributes<HTMLSelectElement>
|
} & SelectHTMLAttributes<HTMLSelectElement>
|
||||||
|
|
||||||
const Select: FC<Props> = ({
|
const Select: FC<Props> = ({
|
||||||
@@ -22,9 +23,18 @@ const Select: FC<Props> = ({
|
|||||||
placeholder,
|
placeholder,
|
||||||
label,
|
label,
|
||||||
readOnly,
|
readOnly,
|
||||||
|
clearable,
|
||||||
...selectProps
|
...selectProps
|
||||||
}) => {
|
}) => {
|
||||||
const isControlled = selectProps.value !== undefined
|
const isControlled = selectProps.value !== undefined
|
||||||
|
const hasClearValue = Boolean(clearable && selectProps.value)
|
||||||
|
|
||||||
|
const handleClear = () => {
|
||||||
|
selectProps.onChange?.({
|
||||||
|
target: { value: '' },
|
||||||
|
currentTarget: { value: '' },
|
||||||
|
} as ChangeEvent<HTMLSelectElement>);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='w-full'>
|
<div className='w-full'>
|
||||||
@@ -41,6 +51,7 @@ const Select: FC<Props> = ({
|
|||||||
className={clx(
|
className={clx(
|
||||||
'w-full bg-white border border-border input-surface relative block appearance-none px-2.5 h-10 text-sm rounded-[10px] transition-colors',
|
'w-full bg-white border border-border input-surface relative block appearance-none px-2.5 h-10 text-sm rounded-[10px] transition-colors',
|
||||||
readOnly && 'bg-muted border-0 text-muted-foreground',
|
readOnly && 'bg-muted border-0 text-muted-foreground',
|
||||||
|
hasClearValue && 'pl-10',
|
||||||
className,
|
className,
|
||||||
label && 'mt-1'
|
label && 'mt-1'
|
||||||
)}>
|
)}>
|
||||||
@@ -61,6 +72,16 @@ const Select: FC<Props> = ({
|
|||||||
</select>
|
</select>
|
||||||
<ArrowDown2 size={16} color='#8c90a3' className='absolute z-0 top-3 left-2 pointer-events-none'
|
<ArrowDown2 size={16} color='#8c90a3' className='absolute z-0 top-3 left-2 pointer-events-none'
|
||||||
/>
|
/>
|
||||||
|
{hasClearValue && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleClear}
|
||||||
|
className="absolute top-0 bottom-0 my-auto left-8 z-10 flex items-center"
|
||||||
|
aria-label="پاک کردن"
|
||||||
|
>
|
||||||
|
<CloseCircle size={16} color="#8C90A3" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{
|
{
|
||||||
error_text && error_text !== '' ?
|
error_text && error_text !== '' ?
|
||||||
|
|||||||
@@ -61,10 +61,15 @@ tbody tr:nth-child(odd) {
|
|||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
padding-right: 16px !important;
|
padding-right: 16px !important;
|
||||||
|
padding-left: 16px !important;
|
||||||
transition:
|
transition:
|
||||||
background-color 0.3s ease,
|
background-color 0.3s ease,
|
||||||
border-color 0.3s ease;
|
border-color 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.date-picker--clearable .rmdp-input {
|
||||||
|
padding-left: 52px !important;
|
||||||
|
}
|
||||||
.readOny .rmdp-input {
|
.readOny .rmdp-input {
|
||||||
background-color: #f5f5f5 !important;
|
background-color: #f5f5f5 !important;
|
||||||
border: none !important;
|
border: none !important;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { type FC, useEffect, useRef, useState } from "react";
|
import { type FC, useEffect, useRef, useState } from "react";
|
||||||
|
import { CloseCircle } from "iconsax-react";
|
||||||
import { clx } from "@/helpers/utils";
|
import { clx } from "@/helpers/utils";
|
||||||
import { useDesignerSearch } from "./hooks/useDesignerSearch";
|
import { useDesignerSearch } from "./hooks/useDesignerSearch";
|
||||||
import type { AdminItemType } from "../admin/types/Types";
|
import type { AdminItemType } from "../admin/types/Types";
|
||||||
@@ -51,22 +52,51 @@ const DesignerSearch: FC<DesignerSearchProps> = ({
|
|||||||
const displayLabel = (designer: AdminItemType) =>
|
const displayLabel = (designer: AdminItemType) =>
|
||||||
`${designer.firstName} ${designer.lastName}`;
|
`${designer.firstName} ${designer.lastName}`;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!value) {
|
||||||
|
setSearch("");
|
||||||
|
}
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
const handleClear = () => {
|
||||||
|
setSearch("");
|
||||||
|
setIsOpen(false);
|
||||||
|
onChange?.("");
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasValue = Boolean(value || search);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={wrapperRef} className={clx("w-full relative", className)}>
|
<div ref={wrapperRef} className={clx("w-full relative", className)}>
|
||||||
{label && (
|
{label && (
|
||||||
<label className="text-sm text-primary-content block mb-1">{label}</label>
|
<label className="text-sm text-primary-content block mb-1">{label}</label>
|
||||||
)}
|
)}
|
||||||
<input
|
<div className="relative">
|
||||||
type="text"
|
<input
|
||||||
value={search}
|
type="text"
|
||||||
onChange={(e) => {
|
value={search}
|
||||||
setSearch(e.target.value);
|
onChange={(e) => {
|
||||||
setIsOpen(true);
|
setSearch(e.target.value);
|
||||||
}}
|
setIsOpen(true);
|
||||||
onFocus={() => setIsOpen(true)}
|
}}
|
||||||
placeholder={placeholder}
|
onFocus={() => setIsOpen(true)}
|
||||||
className="w-full bg-white h-10 text-black block px-4 text-xs rounded-xl border border-border"
|
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 && (
|
{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">
|
<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 ? (
|
{isFetching ? (
|
||||||
|
|||||||
+163
-250
@@ -1,257 +1,170 @@
|
|||||||
import Filters, { type FilterValues } from '@/components/Filters'
|
import { type FC, useMemo, useState } from "react";
|
||||||
import { type FC, useEffect, useMemo, useState } from 'react'
|
import { Link } from "react-router-dom";
|
||||||
import Table from '@/components/Table'
|
import { AddSquare, ArrowDown2, Filter } from "iconsax-react";
|
||||||
import Tabs from '@/components/Tabs'
|
import Filters from "@/components/Filters";
|
||||||
import { AddSquare, Edit2, Printer } from 'iconsax-react'
|
import Table from "@/components/Table";
|
||||||
import { useGetOrders } from './hooks/useOrderData'
|
import Tabs from "@/components/Tabs";
|
||||||
import moment from 'moment-jalaali'
|
import Button from "@/components/Button";
|
||||||
import { Link, useSearchParams } from 'react-router-dom'
|
import RefreshButton from "@/components/RefreshButton";
|
||||||
import { Paths } from '@/config/Paths'
|
import { Paths } from "@/config/Paths";
|
||||||
import type { OrderListItemType } from './types/Types'
|
import { clx } from "@/helpers/utils";
|
||||||
import Button from '@/components/Button'
|
import UserSearch from "../user/UserSearch";
|
||||||
import RefreshButton from '@/components/RefreshButton'
|
import DesignerSearch from "../designer/DesignerSearch";
|
||||||
import UserSearch from '../user/UserSearch'
|
import CategoriesSelect from "../product/components/CategoriesSelect";
|
||||||
import DesignerSearch from '../designer/DesignerSearch'
|
import { orderListTabs } from "./constants/orderStatus";
|
||||||
import CategoriesSelect from '../product/components/CategoriesSelect'
|
import { orderListColumns } from "./components/OrderListColumns";
|
||||||
import { getOrderStatusBadgeClass, getOrderStatusLabel, orderListTabStatuses, orderListTabs } from './constants/orderStatus'
|
import { useGetOrders } from "./hooks/useOrderData";
|
||||||
import { TabOrderListEnum } from './enum/OrderEnum'
|
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 OrdersList: FC = () => {
|
||||||
const [searchParams, setSearchParams] = useSearchParams()
|
const [isFiltersExpanded, setIsFiltersExpanded] = useState(false);
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useState<TabOrderListEnum>(
|
const {
|
||||||
() => (searchParams.get('tab') as TabOrderListEnum) || TabOrderListEnum.IN_PROGRESS,
|
params,
|
||||||
)
|
queryParams,
|
||||||
const [designId, setDesignId] = useState<string | undefined>(
|
filterInitialValues,
|
||||||
() => searchParams.get('designerId') ?? searchParams.get('designId') ?? undefined,
|
setTab,
|
||||||
)
|
setFilters,
|
||||||
const [categoryId, setCategoryId] = useState<string | undefined>(
|
updateParams,
|
||||||
() => searchParams.get('categoryId') ?? undefined,
|
} = useOrderListParams();
|
||||||
)
|
|
||||||
const [userId, setUserId] = useState<string | undefined>(
|
|
||||||
() => searchParams.get('userId') ?? undefined,
|
|
||||||
)
|
|
||||||
const [filters, setFilters] = useState<FilterValues>({})
|
|
||||||
|
|
||||||
useEffect(() => {
|
const { data, refetch, isFetching } = useGetOrders(queryParams);
|
||||||
const nextCategoryId = searchParams.get('categoryId') ?? undefined
|
const activeFilterCount = useMemo(() => countActiveFilters(params), [params]);
|
||||||
const nextUserId = searchParams.get('userId') ?? undefined
|
const hasActiveFilters = activeFilterCount > 0;
|
||||||
const nextDesignId =
|
|
||||||
searchParams.get('designerId') ?? searchParams.get('designId') ?? undefined
|
|
||||||
const nextTab = (searchParams.get('tab') as TabOrderListEnum) || TabOrderListEnum.IN_PROGRESS
|
|
||||||
|
|
||||||
setCategoryId(nextCategoryId)
|
return (
|
||||||
setUserId(nextUserId)
|
<div className="mt-5 space-y-6">
|
||||||
setDesignId(nextDesignId)
|
<div className="flex flex-wrap items-center justify-between gap-4">
|
||||||
setActiveTab(nextTab)
|
<div>
|
||||||
}, [searchParams])
|
<h1 className="text-lg font-light">سفارشها</h1>
|
||||||
|
<p className="mt-1 text-sm text-gray-500">
|
||||||
const updateSearchParam = (key: string, value?: string) => {
|
مدیریت و پیگیری سفارشهای ثبتشده
|
||||||
setSearchParams((current) => {
|
</p>
|
||||||
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>
|
|
||||||
</div>
|
</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",
|
IN_PROGRESS = "in_progress",
|
||||||
FINISHED = "finished",
|
FINISHED = "finished",
|
||||||
CANCELED = "canceled",
|
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,
|
error_text?: string,
|
||||||
isDisableShowLable?: boolean,
|
isDisableShowLable?: boolean,
|
||||||
placeholder?: string,
|
placeholder?: string,
|
||||||
|
clearable?: boolean,
|
||||||
} & SelectHTMLAttributes<HTMLSelectElement>
|
} & SelectHTMLAttributes<HTMLSelectElement>
|
||||||
|
|
||||||
const CategoriesSelect: FC<Props> = ({
|
const CategoriesSelect: FC<Props> = ({
|
||||||
isDisableShowLable,
|
isDisableShowLable,
|
||||||
placeholder,
|
placeholder,
|
||||||
error_text,
|
error_text,
|
||||||
|
clearable = true,
|
||||||
...selectProps
|
...selectProps
|
||||||
}) => {
|
}) => {
|
||||||
|
|
||||||
@@ -23,6 +24,7 @@ const CategoriesSelect: FC<Props> = ({
|
|||||||
label={isDisableShowLable ? undefined : 'دسته'}
|
label={isDisableShowLable ? undefined : 'دسته'}
|
||||||
placeholder={placeholder || 'انتخاب'}
|
placeholder={placeholder || 'انتخاب'}
|
||||||
error_text={error_text}
|
error_text={error_text}
|
||||||
|
clearable={clearable}
|
||||||
items={categories?.data?.map((item) => {
|
items={categories?.data?.map((item) => {
|
||||||
return {
|
return {
|
||||||
label: item.title,
|
label: item.title,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { type FC, useEffect, useRef, useState } from "react";
|
import { type FC, useEffect, useRef, useState } from "react";
|
||||||
|
import { CloseCircle } from "iconsax-react";
|
||||||
import { clx } from "@/helpers/utils";
|
import { clx } from "@/helpers/utils";
|
||||||
import { useUserSearch } from "./hooks/useUserSearch";
|
import { useUserSearch } from "./hooks/useUserSearch";
|
||||||
import type { UserType } from "./types/Types";
|
import type { UserType } from "./types/Types";
|
||||||
@@ -51,22 +52,51 @@ const UserSearch: FC<UserSearchProps> = ({
|
|||||||
const displayLabel = (user: UserType) =>
|
const displayLabel = (user: UserType) =>
|
||||||
user.firstName && user.lastName ? `${user.firstName} ${user.lastName}` : user.phone;
|
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 (
|
return (
|
||||||
<div ref={wrapperRef} className={clx("w-full relative", className)}>
|
<div ref={wrapperRef} className={clx("w-full relative", className)}>
|
||||||
{label && (
|
{label && (
|
||||||
<label className="text-sm text-primary-content block mb-1">{label}</label>
|
<label className="text-sm text-primary-content block mb-1">{label}</label>
|
||||||
)}
|
)}
|
||||||
<input
|
<div className="relative">
|
||||||
type="text"
|
<input
|
||||||
value={search}
|
type="text"
|
||||||
onChange={(e) => {
|
value={search}
|
||||||
setSearch(e.target.value);
|
onChange={(e) => {
|
||||||
setIsOpen(true);
|
setSearch(e.target.value);
|
||||||
}}
|
setIsOpen(true);
|
||||||
onFocus={() => setIsOpen(true)}
|
}}
|
||||||
placeholder={placeholder}
|
onFocus={() => setIsOpen(true)}
|
||||||
className="w-full bg-white h-10 text-black block px-4 text-xs rounded-xl border border-border"
|
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 && (
|
{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">
|
<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 ? (
|
{isFetching ? (
|
||||||
|
|||||||
Reference in New Issue
Block a user