572 lines
18 KiB
TypeScript
572 lines
18 KiB
TypeScript
|
|
import { useState, useEffect } from "react";
|
|||
|
|
import { motion, AnimatePresence } from "framer-motion";
|
|||
|
|
import DatePicker from "../date-picker/DatePicker";
|
|||
|
|
import { Grid } from "../Grid/Grid";
|
|||
|
|
import Textfield from "../Textfeild/Textfeild";
|
|||
|
|
import Typography from "../Typography/Typography";
|
|||
|
|
import {
|
|||
|
|
CircleStackIcon,
|
|||
|
|
DocumentArrowDownIcon,
|
|||
|
|
} from "@heroicons/react/24/outline";
|
|||
|
|
import { checkIsMobile } from "../../utils/checkIsMobile";
|
|||
|
|
import AutoComplete from "../AutoComplete/AutoComplete";
|
|||
|
|
import { Tooltip } from "../Tooltip/Tooltip";
|
|||
|
|
import api from "../../utils/axios";
|
|||
|
|
import { useBackdropStore } from "../../context/zustand-store/appStore";
|
|||
|
|
import { useToast } from "../../hooks/useToast";
|
|||
|
|
import { useApiRequest } from "../../utils/useApiRequest";
|
|||
|
|
import { getNestedValue } from "../../utils/getNestedValue";
|
|||
|
|
import { getFaPermissions } from "../../utils/getFaPermissions";
|
|||
|
|
|
|||
|
|
interface FilterItem {
|
|||
|
|
key: number | string;
|
|||
|
|
value: string;
|
|||
|
|
disabled?: boolean;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
interface FilterConfig {
|
|||
|
|
key?: string;
|
|||
|
|
data?: FilterItem[];
|
|||
|
|
api?: string;
|
|||
|
|
selectedKeys: (number | string)[];
|
|||
|
|
onChange: (keys: (number | string)[]) => void;
|
|||
|
|
title: string;
|
|||
|
|
size?: "small" | "medium" | "large";
|
|||
|
|
inPage?: boolean;
|
|||
|
|
selectField?: boolean;
|
|||
|
|
keyField?: string;
|
|||
|
|
valueField?: string | string[];
|
|||
|
|
valueField2?: string | string[];
|
|||
|
|
valueField3?: string | string[];
|
|||
|
|
filterAddress?: string[];
|
|||
|
|
filterValue?: string[];
|
|||
|
|
valueTemplate?: string;
|
|||
|
|
valueTemplateProps?: Array<{ [key: string]: "string" | "number" }>;
|
|||
|
|
groupBy?: string | string[];
|
|||
|
|
groupFunction?: (item: any) => string;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type ExcelProps = {
|
|||
|
|
title?: string;
|
|||
|
|
link?: string;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
interface PaginationParametersProps {
|
|||
|
|
noCustomDate?: boolean;
|
|||
|
|
defaultActive?: boolean;
|
|||
|
|
justOne?: boolean;
|
|||
|
|
noSearch?: boolean;
|
|||
|
|
startLabel?: string;
|
|||
|
|
endLabel?: string;
|
|||
|
|
title?: string;
|
|||
|
|
onChange?: (data: {
|
|||
|
|
date1?: string | null;
|
|||
|
|
date2?: string | null;
|
|||
|
|
search?: string | null;
|
|||
|
|
[key: string]: any;
|
|||
|
|
}) => void;
|
|||
|
|
getData?: () => void;
|
|||
|
|
children?: React.ReactNode;
|
|||
|
|
filters?: FilterConfig[];
|
|||
|
|
excelInfo?: ExcelProps;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const ApiBasedFilter: React.FC<{
|
|||
|
|
filter: FilterConfig;
|
|||
|
|
}> = ({ filter }) => {
|
|||
|
|
const [filterData, setFilterData] = useState<FilterItem[]>([]);
|
|||
|
|
|
|||
|
|
if (!filter.api) {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const formatValue = (value: any, fieldKey: string) => {
|
|||
|
|
if (!filter.valueTemplate || !filter.valueTemplateProps) return value;
|
|||
|
|
|
|||
|
|
const templateProp = filter.valueTemplateProps.find(
|
|||
|
|
(prop) => prop[fieldKey]
|
|||
|
|
);
|
|||
|
|
if (!templateProp) return value;
|
|||
|
|
|
|||
|
|
const fieldType = templateProp[fieldKey];
|
|||
|
|
|
|||
|
|
if (fieldType === "number") {
|
|||
|
|
const numValue = typeof value === "number" ? value : Number(value);
|
|||
|
|
return !isNaN(numValue) ? numValue.toLocaleString() : String(value);
|
|||
|
|
} else if (fieldType === "string") {
|
|||
|
|
let result = String(value);
|
|||
|
|
if (typeof value === "string" && value.includes(",")) {
|
|||
|
|
result = value.replace(/,/g, "");
|
|||
|
|
}
|
|||
|
|
return result;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return value;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const { data: apiData } = useApiRequest({
|
|||
|
|
api: filter.api,
|
|||
|
|
params: { page: 1, page_size: 1000 },
|
|||
|
|
queryKey: [filter.api, filter.key],
|
|||
|
|
disableBackdrop: true,
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
if (apiData?.results && filter.api) {
|
|||
|
|
let data;
|
|||
|
|
|
|||
|
|
if (filter.filterAddress && filter.filterValue) {
|
|||
|
|
data = apiData.results?.filter(
|
|||
|
|
(item: any) =>
|
|||
|
|
!filter.filterValue!.includes(
|
|||
|
|
getNestedValue(item, filter.filterAddress!)
|
|||
|
|
)
|
|||
|
|
);
|
|||
|
|
} else {
|
|||
|
|
data = apiData.results;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const keyField = filter.keyField || "id";
|
|||
|
|
const valueField = filter.valueField || "name";
|
|||
|
|
|
|||
|
|
const formattedData = data?.map((option: any) => ({
|
|||
|
|
key: option[keyField],
|
|||
|
|
value: filter.valueTemplate
|
|||
|
|
? filter.valueTemplate
|
|||
|
|
.replace(
|
|||
|
|
/v1/g,
|
|||
|
|
formatValue(
|
|||
|
|
valueField === "page"
|
|||
|
|
? getFaPermissions(option[valueField])
|
|||
|
|
: typeof valueField === "string"
|
|||
|
|
? option[valueField]
|
|||
|
|
: getNestedValue(option, valueField),
|
|||
|
|
"v1"
|
|||
|
|
)
|
|||
|
|
)
|
|||
|
|
.replace(
|
|||
|
|
/v2/g,
|
|||
|
|
formatValue(
|
|||
|
|
filter.valueField2
|
|||
|
|
? typeof filter.valueField2 === "string"
|
|||
|
|
? option[filter.valueField2]
|
|||
|
|
: getNestedValue(option, filter.valueField2)
|
|||
|
|
: "",
|
|||
|
|
"v2"
|
|||
|
|
)
|
|||
|
|
)
|
|||
|
|
.replace(
|
|||
|
|
/v3/g,
|
|||
|
|
formatValue(
|
|||
|
|
filter.valueField3
|
|||
|
|
? typeof filter.valueField3 === "string"
|
|||
|
|
? option[filter.valueField3]
|
|||
|
|
: getNestedValue(option, filter.valueField3)
|
|||
|
|
: "",
|
|||
|
|
"v3"
|
|||
|
|
)
|
|||
|
|
)
|
|||
|
|
: `${
|
|||
|
|
valueField === "page"
|
|||
|
|
? getFaPermissions(option[valueField])
|
|||
|
|
: typeof valueField === "string"
|
|||
|
|
? option[valueField]
|
|||
|
|
: getNestedValue(option, valueField)
|
|||
|
|
} ${
|
|||
|
|
filter.valueField2
|
|||
|
|
? " - " +
|
|||
|
|
(typeof filter.valueField2 === "string"
|
|||
|
|
? option[filter.valueField2]
|
|||
|
|
: getNestedValue(option, filter.valueField2))
|
|||
|
|
: ""
|
|||
|
|
} ${
|
|||
|
|
filter.valueField3
|
|||
|
|
? "(" +
|
|||
|
|
(typeof filter.valueField3 === "string"
|
|||
|
|
? option[filter.valueField3]
|
|||
|
|
: getNestedValue(option, filter.valueField3)) +
|
|||
|
|
")"
|
|||
|
|
: ""
|
|||
|
|
}`,
|
|||
|
|
}));
|
|||
|
|
|
|||
|
|
setFilterData(formattedData || []);
|
|||
|
|
}
|
|||
|
|
}, [apiData, filter]);
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<AutoComplete
|
|||
|
|
key={filter.key}
|
|||
|
|
inPage={filter.inPage ?? true}
|
|||
|
|
size={filter.size ?? "small"}
|
|||
|
|
data={filterData}
|
|||
|
|
selectedKeys={filter.selectedKeys}
|
|||
|
|
onChange={filter.onChange}
|
|||
|
|
title={filter.title}
|
|||
|
|
selectField={filter.selectField}
|
|||
|
|
/>
|
|||
|
|
);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
export const PaginationParameters: React.FC<PaginationParametersProps> = ({
|
|||
|
|
noCustomDate = false,
|
|||
|
|
defaultActive = false,
|
|||
|
|
justOne = false,
|
|||
|
|
noSearch = false,
|
|||
|
|
startLabel = "",
|
|||
|
|
endLabel = "تا",
|
|||
|
|
title,
|
|||
|
|
onChange,
|
|||
|
|
getData,
|
|||
|
|
children,
|
|||
|
|
filters = [],
|
|||
|
|
excelInfo,
|
|||
|
|
}) => {
|
|||
|
|
const [selectedDate1, setSelectedDate1] = useState<string>();
|
|||
|
|
const [selectedDate2, setSelectedDate2] = useState<string>();
|
|||
|
|
const [keyword, setKeyword] = useState<string>("");
|
|||
|
|
const [enableDates, setEnableDates] = useState<boolean>(defaultActive);
|
|||
|
|
const { openBackdrop, closeBackdrop } = useBackdropStore();
|
|||
|
|
const showToast = useToast();
|
|||
|
|
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
|||
|
|
e.preventDefault();
|
|||
|
|
const data = {
|
|||
|
|
start: enableDates ? selectedDate1 : null,
|
|||
|
|
end: enableDates ? selectedDate2 : null,
|
|||
|
|
search: noSearch ? null : keyword,
|
|||
|
|
};
|
|||
|
|
await onChange?.(data);
|
|||
|
|
getData?.();
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleExcelDownload = () => {
|
|||
|
|
openBackdrop();
|
|||
|
|
if (!excelInfo?.link) return;
|
|||
|
|
|
|||
|
|
const urlParts = excelInfo.link.split("?");
|
|||
|
|
const baseUrl = urlParts[0];
|
|||
|
|
const existingParams: Record<string, string> = {};
|
|||
|
|
|
|||
|
|
if (urlParts[1]) {
|
|||
|
|
const queryString = urlParts[1];
|
|||
|
|
const params = new URLSearchParams(queryString);
|
|||
|
|
params.forEach((value, key) => {
|
|||
|
|
if (value) {
|
|||
|
|
existingParams[key] = value;
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const mergedParams: Record<string, string | undefined> = {
|
|||
|
|
...existingParams,
|
|||
|
|
...(keyword ? { search: keyword } : {}),
|
|||
|
|
...(enableDates && selectedDate1 && selectedDate2
|
|||
|
|
? { start: selectedDate1, end: selectedDate2 }
|
|||
|
|
: enableDates && selectedDate1
|
|||
|
|
? { start: selectedDate1 }
|
|||
|
|
: enableDates && selectedDate2
|
|||
|
|
? { end: selectedDate2 }
|
|||
|
|
: {}),
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const finalParams: Record<string, string> = {};
|
|||
|
|
Object.keys(mergedParams).forEach((key) => {
|
|||
|
|
const value = mergedParams[key];
|
|||
|
|
if (value !== undefined && value !== null && value !== "") {
|
|||
|
|
finalParams[key] = value;
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
api
|
|||
|
|
.get(baseUrl, {
|
|||
|
|
params: finalParams,
|
|||
|
|
responseType: "blob",
|
|||
|
|
})
|
|||
|
|
.then((response) => {
|
|||
|
|
const url = window.URL.createObjectURL(new Blob([response.data]));
|
|||
|
|
|
|||
|
|
const link = document.createElement("a");
|
|||
|
|
link.href = url;
|
|||
|
|
link.setAttribute(
|
|||
|
|
"download",
|
|||
|
|
`${excelInfo?.title || title || "خروجی"}.xlsx`
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
document.body.appendChild(link);
|
|||
|
|
link.click();
|
|||
|
|
document.body.removeChild(link);
|
|||
|
|
|
|||
|
|
window.URL.revokeObjectURL(url);
|
|||
|
|
})
|
|||
|
|
.catch((error) => {
|
|||
|
|
console.error("Error downloading file:", error);
|
|||
|
|
showToast("در حال حاضر امکان دانلود خروجی اکسل وجود ندارد", "error");
|
|||
|
|
})
|
|||
|
|
.finally(() => {
|
|||
|
|
closeBackdrop();
|
|||
|
|
});
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const renderDate1 = (
|
|||
|
|
<DatePicker
|
|||
|
|
size="small"
|
|||
|
|
disabled={!noCustomDate && !enableDates}
|
|||
|
|
value={selectedDate1}
|
|||
|
|
onChange={(r) => setSelectedDate1(r)}
|
|||
|
|
label={startLabel}
|
|||
|
|
/>
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
const renderDate2 = !justOne ? (
|
|||
|
|
<DatePicker
|
|||
|
|
size="small"
|
|||
|
|
disabled={!noCustomDate && !enableDates}
|
|||
|
|
value={selectedDate2}
|
|||
|
|
onChange={(r) => setSelectedDate2(r)}
|
|||
|
|
label={endLabel}
|
|||
|
|
/>
|
|||
|
|
) : (
|
|||
|
|
""
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
const isMobile = checkIsMobile();
|
|||
|
|
|
|||
|
|
if (isMobile) {
|
|||
|
|
return (
|
|||
|
|
<form onSubmit={handleSubmit}>
|
|||
|
|
<div className="flex flex-col bg-gray-100 dark:bg-dark-800 rounded-4xl m-4">
|
|||
|
|
<div className="flex-1 px-4 pt-6 pb-2 space-y-4">
|
|||
|
|
{(title || excelInfo) && (
|
|||
|
|
<div className="flex items-center gap-2">
|
|||
|
|
<CircleStackIcon className="h-6 w-6 text-primary-600 dark:text-white" />
|
|||
|
|
<Typography className="text-lg font-semibold text-gray-800 dark:text-white">
|
|||
|
|
{title}
|
|||
|
|
</Typography>
|
|||
|
|
{excelInfo && (
|
|||
|
|
<div
|
|||
|
|
className="cursor-pointer rounded-sm px-1 border-red-400 shadow-b-xl shadow-sky-50"
|
|||
|
|
onClick={handleExcelDownload}
|
|||
|
|
>
|
|||
|
|
<Tooltip
|
|||
|
|
position="top"
|
|||
|
|
title={
|
|||
|
|
checkIsMobile() ? "" : excelInfo?.title || "خروجی اکسل"
|
|||
|
|
}
|
|||
|
|
>
|
|||
|
|
<DocumentArrowDownIcon className="text-primary-600 w-5 animate-pulse" />
|
|||
|
|
</Tooltip>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{!noCustomDate && (
|
|||
|
|
<div className="bg-white dark:bg-dark-600 shadow rounded-2xl p-4 flex items-center justify-between">
|
|||
|
|
<span className="text-sm text-gray-700 dark:text-gray-200">
|
|||
|
|
فعالسازی بازه تاریخی
|
|||
|
|
</span>
|
|||
|
|
<button
|
|||
|
|
type="button"
|
|||
|
|
onClick={() => setEnableDates((prev) => !prev)}
|
|||
|
|
className={`relative w-12 h-7 rounded-full transition-colors duration-300 ${
|
|||
|
|
enableDates ? "bg-green-500" : "bg-gray-300"
|
|||
|
|
}`}
|
|||
|
|
>
|
|||
|
|
<motion.div
|
|||
|
|
layout
|
|||
|
|
transition={{ type: "spring", stiffness: 500, damping: 30 }}
|
|||
|
|
className="absolute top-0.5 left-0.5 w-6 h-6 bg-white rounded-full shadow"
|
|||
|
|
animate={{ x: enableDates ? 20 : 0 }}
|
|||
|
|
/>
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
<div className="bg-white dark:bg-dark-600 shadow rounded-2xl p-4 space-y-2">
|
|||
|
|
{renderDate1}
|
|||
|
|
{renderDate2}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{filters.length > 0 && (
|
|||
|
|
<div className="bg-white dark:bg-dark-600 shadow rounded-2xl p-4 space-y-2">
|
|||
|
|
{filters.map((filter) =>
|
|||
|
|
filter.api ? (
|
|||
|
|
<ApiBasedFilter key={filter.key} filter={filter} />
|
|||
|
|
) : (
|
|||
|
|
<AutoComplete
|
|||
|
|
key={filter.key}
|
|||
|
|
inPage={filter.inPage ?? true}
|
|||
|
|
size={filter.size ?? "small"}
|
|||
|
|
data={filter.data || []}
|
|||
|
|
selectedKeys={filter.selectedKeys}
|
|||
|
|
onChange={filter.onChange}
|
|||
|
|
title={filter.title}
|
|||
|
|
selectField={filter.selectField}
|
|||
|
|
/>
|
|||
|
|
)
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
<div className="bg-white dark:bg-dark-600 shadow rounded-2xl p-4">
|
|||
|
|
{!noSearch && (
|
|||
|
|
<Textfield
|
|||
|
|
inputSize="small"
|
|||
|
|
fullWidth
|
|||
|
|
placeholder="عبارت جستجو را وارد کنید"
|
|||
|
|
value={keyword}
|
|||
|
|
onChange={(e) => setKeyword(e.target.value)}
|
|||
|
|
/>
|
|||
|
|
)}
|
|||
|
|
<motion.button
|
|||
|
|
whileHover={{ scale: 1.03 }}
|
|||
|
|
whileTap={{ scale: 0.97 }}
|
|||
|
|
type="submit"
|
|||
|
|
className="w-full mt-2 bg-primary-600 hover:bg-primary-700 text-white text-base font-semibold py-3 rounded-2xl shadow-lg transition-all"
|
|||
|
|
>
|
|||
|
|
جستجو
|
|||
|
|
</motion.button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</form>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<form onSubmit={handleSubmit}>
|
|||
|
|
<Grid className="flex flex-wrap gap-2 justify-center items-center p-4 w-full bg-gradient-to-r from-transparent to-transparent dark:via-gray-900 via-gray-100">
|
|||
|
|
{title && (
|
|||
|
|
<Grid className="bg-primary-100 items-center dark:bg-dark-700 ml-2 p-1 px-4 rounded-xl flex justify-center gap-2 w-full md:w-auto">
|
|||
|
|
<Grid>
|
|||
|
|
<CircleStackIcon className="h-6 w-6 text-primary-800 dark:text-white" />
|
|||
|
|
</Grid>
|
|||
|
|
<Grid>
|
|||
|
|
<Typography
|
|||
|
|
variant="body2"
|
|||
|
|
color="text-gray-600 dark:text-gray-100"
|
|||
|
|
>
|
|||
|
|
{title}
|
|||
|
|
</Typography>
|
|||
|
|
</Grid>
|
|||
|
|
</Grid>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
<div className="flex items-center gap-2">
|
|||
|
|
{filters.map((filter) => (
|
|||
|
|
<Grid
|
|||
|
|
container
|
|||
|
|
className="items-center gap-2 max-w-40"
|
|||
|
|
key={filter.key}
|
|||
|
|
>
|
|||
|
|
{filter.api ? (
|
|||
|
|
<ApiBasedFilter filter={filter} />
|
|||
|
|
) : (
|
|||
|
|
<AutoComplete
|
|||
|
|
inPage={filter.inPage ?? true}
|
|||
|
|
size={filter.size ?? "small"}
|
|||
|
|
data={filter.data || []}
|
|||
|
|
selectedKeys={filter.selectedKeys}
|
|||
|
|
onChange={filter.onChange}
|
|||
|
|
title={filter.title}
|
|||
|
|
selectField={filter.selectField}
|
|||
|
|
/>
|
|||
|
|
)}
|
|||
|
|
</Grid>
|
|||
|
|
))}
|
|||
|
|
{children}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{!noCustomDate && (
|
|||
|
|
<div className="flex items-center gap-2">
|
|||
|
|
<button
|
|||
|
|
type="button"
|
|||
|
|
onClick={() => setEnableDates((prev) => !prev)}
|
|||
|
|
className={`relative w-10 h-5 rounded-full transition-colors duration-300 cursor-pointer ${
|
|||
|
|
enableDates ? "bg-green-500" : "bg-gray-300"
|
|||
|
|
}`}
|
|||
|
|
>
|
|||
|
|
<motion.div
|
|||
|
|
layout
|
|||
|
|
transition={{ type: "spring", stiffness: 500, damping: 30 }}
|
|||
|
|
className="absolute top-[2px] left-[2px] w-4 h-4 bg-white rounded-full shadow-md"
|
|||
|
|
animate={{ x: enableDates ? 20 : 0 }}
|
|||
|
|
/>
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
<AnimatePresence>
|
|||
|
|
<>
|
|||
|
|
<motion.div
|
|||
|
|
key="date1"
|
|||
|
|
initial={{ opacity: 0, y: 10 }}
|
|||
|
|
animate={{ opacity: 1, y: 0 }}
|
|||
|
|
exit={{ opacity: 0, y: 10 }}
|
|||
|
|
transition={{ duration: 0.3 }}
|
|||
|
|
className="w-full sm:w-auto flex-grow sm:flex-grow-0 min-w-[150px]"
|
|||
|
|
>
|
|||
|
|
{renderDate1}
|
|||
|
|
</motion.div>
|
|||
|
|
|
|||
|
|
<motion.div
|
|||
|
|
key="date2"
|
|||
|
|
initial={{ opacity: 0, y: 10 }}
|
|||
|
|
animate={{ opacity: 1, y: 0 }}
|
|||
|
|
exit={{ opacity: 0, y: 10 }}
|
|||
|
|
transition={{ duration: 0.3 }}
|
|||
|
|
className="w-full sm:w-auto flex-grow sm:flex-grow-0"
|
|||
|
|
>
|
|||
|
|
{renderDate2}
|
|||
|
|
</motion.div>
|
|||
|
|
</>
|
|||
|
|
</AnimatePresence>
|
|||
|
|
|
|||
|
|
<motion.div
|
|||
|
|
initial={{ opacity: 0 }}
|
|||
|
|
animate={{ opacity: 1 }}
|
|||
|
|
transition={{ duration: 1 }}
|
|||
|
|
className="w-full sm:w-auto flex-grow sm:flex-grow-0"
|
|||
|
|
>
|
|||
|
|
{!noSearch && (
|
|||
|
|
<Textfield
|
|||
|
|
inputSize="small"
|
|||
|
|
fullWidth
|
|||
|
|
placeholder="جستجو"
|
|||
|
|
value={keyword}
|
|||
|
|
onChange={(e) => setKeyword(e.target.value)}
|
|||
|
|
/>
|
|||
|
|
)}
|
|||
|
|
</motion.div>
|
|||
|
|
|
|||
|
|
<motion.button
|
|||
|
|
whileHover={{ scale: 1.05 }}
|
|||
|
|
whileTap={{ scale: 0.95 }}
|
|||
|
|
type="submit"
|
|||
|
|
className="
|
|||
|
|
bg-primary-600 text-white rounded-xl px-6 py-1.5 text-sm font-medium shadow cursor-pointer
|
|||
|
|
hover:bg-primary-700 transition-colors
|
|||
|
|
w-full sm:w-auto min-w-[100px]
|
|||
|
|
"
|
|||
|
|
>
|
|||
|
|
جستجو
|
|||
|
|
</motion.button>
|
|||
|
|
|
|||
|
|
{excelInfo && (
|
|||
|
|
<div
|
|||
|
|
className="cursor-pointer rounded-sm border-red-400 flex items-center justify-center"
|
|||
|
|
onClick={handleExcelDownload}
|
|||
|
|
>
|
|||
|
|
<Tooltip
|
|||
|
|
position="top"
|
|||
|
|
title={checkIsMobile() ? "" : excelInfo?.title || "خروجی اکسل"}
|
|||
|
|
>
|
|||
|
|
<DocumentArrowDownIcon className="text-primary-600 w-8 animate-pulse" />
|
|||
|
|
</Tooltip>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</Grid>
|
|||
|
|
</form>
|
|||
|
|
);
|
|||
|
|
};
|