Files
RasadDam_Frontend/src/components/PaginationParameters/PaginationParameters.tsx
2026-01-19 13:08:58 +03:30

572 lines
18 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
};