first commit
This commit is contained in:
571
src/components/PaginationParameters/PaginationParameters.tsx
Normal file
571
src/components/PaginationParameters/PaginationParameters.tsx
Normal file
@@ -0,0 +1,571 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user