Files
RasadDam_Frontend/src/components/PaginationParameters/PaginationParameters.tsx

572 lines
18 KiB
TypeScript
Raw Normal View History

2026-01-19 13:08:58 +03:30
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>
);
};