diff --git a/src/components/customAGGrid/cell-renderers.tsx b/src/components/customAGGrid/cell-renderers.tsx
new file mode 100644
index 000000000..c7348bece
--- /dev/null
+++ b/src/components/customAGGrid/cell-renderers.tsx
@@ -0,0 +1,264 @@
+/**
+ * Copyright (c) 2023, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+import { Box, Checkbox, Tooltip } from '@mui/material';
+import { ReactNode, useEffect, useRef, useState } from 'react';
+import { isBlankOrEmpty } from '../../utils/conversionUtils';
+import { ICellRendererParams } from 'ag-grid-community';
+import { CustomCellRendererProps } from 'ag-grid-react';
+import { mergeSx, type MuiStyles } from '../../utils/styles';
+import { useIntl } from 'react-intl';
+
+const styles = {
+ tableCell: (theme) => ({
+ fontSize: 'small',
+ cursor: 'inherit',
+ display: 'flex',
+ '&:before': {
+ content: '""',
+ position: 'absolute',
+ left: theme.spacing(0.5),
+ right: theme.spacing(0.5),
+ bottom: 0,
+ },
+ }),
+ overflow: {
+ whiteSpace: 'pre',
+ textOverflow: 'ellipsis',
+ overflow: 'hidden',
+ },
+ numericValue: {
+ marginLeft: 'inherit',
+ },
+} as const satisfies MuiStyles;
+
+const FORMULA_ERROR_KEY = 'spreadsheet/formula/error';
+
+interface BaseCellRendererProps {
+ value: string | undefined;
+ tooltip?: string;
+}
+
+export const BooleanCellRenderer = (props: any) => {
+ const isChecked = props.value;
+ return (
+
+ {props.value !== undefined && (
+
+ )}
+
+ );
+};
+
+export const BooleanNullableCellRenderer = (props: any) => {
+ return (
+
+
+
+ );
+};
+
+const formatNumericCell = (value: number, fractionDigits?: number) => {
+ if (value === null || isNaN(value)) {
+ return { value: null };
+ }
+ return { value: value.toFixed(fractionDigits ?? 2), tooltip: value?.toString() };
+};
+
+const formatCell = (props: any) => {
+ let value = props?.valueFormatted || props.value;
+ let tooltipValue = undefined;
+ // we use valueGetter only if value is not defined
+ if (!value && props.colDef.valueGetter) {
+ props.colDef.valueGetter(props);
+ }
+ if (value != null && props.colDef.context?.numeric && props.colDef.context?.fractionDigits) {
+ // only numeric rounded cells have a tooltip (their raw numeric value)
+ tooltipValue = value;
+ value = parseFloat(value).toFixed(props.colDef.context.fractionDigits);
+ }
+ if (props.colDef.context?.numeric && isNaN(value)) {
+ value = null;
+ }
+ return { value: value, tooltip: tooltipValue };
+};
+
+export interface NumericCellRendererProps extends CustomCellRendererProps {
+ fractionDigits?: number;
+}
+
+export const NumericCellRenderer = (props: NumericCellRendererProps) => {
+ const numericalValue = typeof props.value === 'number' ? props.value : Number.parseFloat(props.value);
+ const cellValue = formatNumericCell(numericalValue, props.fractionDigits);
+ return (
+
+
+ {cellValue.value}
+
+
+ );
+};
+
+const BaseCellRenderer = ({ value, tooltip }: BaseCellRendererProps) => (
+
+
+ {value}
+
+
+);
+
+export const ErrorCellRenderer = (props: CustomCellRendererProps) => {
+ const intl = useIntl();
+ const errorMessage = intl.formatMessage({ id: props.value?.error });
+ const errorValue = intl.formatMessage({ id: FORMULA_ERROR_KEY });
+ return ;
+};
+
+export const DefaultCellRenderer = (props: CustomCellRendererProps) => {
+ const cellValue = formatCell(props).value?.toString();
+ return ;
+};
+
+export const NetworkModificationNameCellRenderer = (props: CustomCellRendererProps) => {
+ return (
+
+
+ {props.value}
+
+
+ );
+};
+
+export const MessageLogCellRenderer = ({
+ param,
+ highlightColor,
+ currentHighlightColor,
+ searchTerm,
+ currentResultIndex,
+ searchResults,
+}: {
+ param: ICellRendererParams;
+ highlightColor?: string;
+ currentHighlightColor?: string;
+ searchTerm?: string;
+ currentResultIndex?: number;
+ searchResults?: number[];
+}) => {
+ const marginLeft = (param.data?.depth ?? 0) * 2; // add indentation based on depth
+ const textRef = useRef(null);
+ const [isEllipsisActive, setIsEllipsisActive] = useState(false);
+
+ const checkEllipsis = () => {
+ if (textRef.current) {
+ const zoomLevel = window.devicePixelRatio;
+ const adjustedScrollWidth = textRef.current.scrollWidth / zoomLevel;
+ const adjustedClientWidth = textRef.current.clientWidth / zoomLevel;
+ setIsEllipsisActive(adjustedScrollWidth > adjustedClientWidth);
+ }
+ };
+
+ useEffect(() => {
+ checkEllipsis();
+ const resizeObserver = new ResizeObserver(() => checkEllipsis());
+ if (textRef.current) {
+ resizeObserver.observe(textRef.current);
+ }
+
+ return () => {
+ resizeObserver.disconnect();
+ };
+ }, [param.value]);
+
+ const escapeRegExp = (string: string) => {
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+ };
+
+ const renderHighlightedText = (value: string) => {
+ if (!searchTerm || searchTerm === '') {
+ return value;
+ }
+
+ const escapedSearchTerm = escapeRegExp(searchTerm);
+ const parts = value.split(new RegExp(`(${escapedSearchTerm})`, 'gi'));
+ return (
+
+ {parts.map((part: string, index: number) =>
+ part.toLowerCase() === searchTerm.toLowerCase() ? (
+
+ {part}
+
+ ) : (
+ part
+ )
+ )}
+
+ );
+ };
+
+ return (
+
+
+
+ {renderHighlightedText(param.value)}
+
+
+
+ );
+};
+
+export const ContingencyCellRenderer = ({ value }: { value: { cellValue: ReactNode; tooltipValue: ReactNode } }) => {
+ const { cellValue, tooltipValue } = value ?? {};
+
+ if (cellValue == null || tooltipValue == null) {
+ return null;
+ }
+
+ return (
+
+ {tooltipValue}}>
+ {cellValue}
+
+
+ );
+};
diff --git a/src/components/customAGGrid/custom-aggrid-filters/custom-aggrid-autocomplete-filter.tsx b/src/components/customAGGrid/custom-aggrid-filters/custom-aggrid-autocomplete-filter.tsx
new file mode 100644
index 000000000..25f74d4b0
--- /dev/null
+++ b/src/components/customAGGrid/custom-aggrid-filters/custom-aggrid-autocomplete-filter.tsx
@@ -0,0 +1,86 @@
+/**
+ * Copyright (c) 2024, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+import React, { FunctionComponent, SyntheticEvent, useCallback, useEffect, useState } from 'react';
+import { Autocomplete, TextField } from '@mui/material';
+import { useIntl } from 'react-intl';
+import { useCustomAggridFilter } from './hooks/use-custom-aggrid-filter';
+import { isNonEmptyStringOrArray } from '../../../utils/types-utils';
+import { CustomAggridFilterParams, FILTER_TEXT_COMPARATORS } from './custom-aggrid-filter.type';
+
+export interface CustomAggridAutocompleteFilterParams extends CustomAggridFilterParams {
+ getOptionLabel?: (value: string) => string; // Used for translation of enum values in the filter
+ options?: string[];
+}
+
+export const CustomAggridAutocompleteFilter: FunctionComponent = ({
+ api,
+ colId,
+ filterParams,
+ getOptionLabel,
+ options,
+}) => {
+ const intl = useIntl();
+ const { selectedFilterData, handleChangeFilterValue } = useCustomAggridFilter(api, colId, filterParams);
+ const [computedFilterOptions, setComputedFilterOptions] = useState(options ?? []);
+
+ const getUniqueValues = useCallback(() => {
+ const uniqueValues = new Set();
+ let allNumbers = true;
+ api.forEachNode((node) => {
+ const value = api.getCellValue({
+ rowNode: node,
+ colKey: colId,
+ });
+ if (value !== undefined && value !== null && value !== '') {
+ uniqueValues.add(value);
+ if (allNumbers && isNaN(Number(value))) {
+ allNumbers = false;
+ }
+ }
+ });
+ // sort the values if they are all numbers
+ if (allNumbers) {
+ return Array.from(uniqueValues).sort((a, b) => Number(a) - Number(b));
+ }
+ return Array.from(uniqueValues);
+ }, [api, colId]);
+
+ useEffect(() => {
+ if (!options) {
+ setComputedFilterOptions(getUniqueValues());
+ }
+ }, [options, getUniqueValues]);
+
+ const handleFilterAutoCompleteChange = (_: SyntheticEvent, data: string[]) => {
+ handleChangeFilterValue({ value: data, type: FILTER_TEXT_COMPARATORS.EQUALS });
+ };
+
+ return (
+ String(getOptionLabel ? getOptionLabel(option) : option)}
+ onChange={handleFilterAutoCompleteChange}
+ size="small"
+ disableCloseOnSelect
+ renderInput={(params) => (
+
+ )}
+ fullWidth
+ />
+ );
+};
diff --git a/src/components/customAGGrid/custom-aggrid-filters/custom-aggrid-boolean-filter.tsx b/src/components/customAGGrid/custom-aggrid-filters/custom-aggrid-boolean-filter.tsx
new file mode 100644
index 000000000..20eb7832b
--- /dev/null
+++ b/src/components/customAGGrid/custom-aggrid-filters/custom-aggrid-boolean-filter.tsx
@@ -0,0 +1,79 @@
+/**
+ * Copyright (c) 2024, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+import { FunctionComponent } from 'react';
+import { IconButton, MenuItem, Select } from '@mui/material';
+import ClearIcon from '@mui/icons-material/Clear';
+import { useIntl } from 'react-intl';
+import { SelectChangeEvent } from '@mui/material/Select/SelectInput';
+import { useCustomAggridFilter } from './hooks/use-custom-aggrid-filter';
+import { isNonEmptyStringOrArray } from '../../../../utils/types-utils';
+import { mergeSx, type MuiStyles } from '../../../../utils/styles';
+import { BooleanFilterValue } from './utils/aggrid-filters-utils';
+import { CustomAggridFilterParams, FILTER_DATA_TYPES, FILTER_TEXT_COMPARATORS } from './custom-aggrid-filter.type';
+
+const styles = {
+ input: {
+ minWidth: '250px',
+ maxWidth: '40%',
+ paddingRight: '0px',
+ },
+} as const satisfies MuiStyles;
+
+export const CustomAggridBooleanFilter: FunctionComponent = ({
+ api,
+ colId,
+ filterParams,
+}) => {
+ const intl = useIntl();
+
+ const { selectedFilterData, handleChangeFilterValue } = useCustomAggridFilter(api, colId, filterParams);
+
+ const handleValueChange = (event: SelectChangeEvent) => {
+ const newValue = event.target.value;
+ handleChangeFilterValue({
+ value: newValue,
+ type: FILTER_TEXT_COMPARATORS.EQUALS,
+ dataType: FILTER_DATA_TYPES.BOOLEAN,
+ });
+ };
+
+ const handleClearFilter = () => {
+ handleChangeFilterValue({
+ value: undefined,
+ });
+ };
+
+ return (
+
+ );
+};
diff --git a/src/components/customAGGrid/custom-aggrid-filters/custom-aggrid-comparator-filter.tsx b/src/components/customAGGrid/custom-aggrid-filters/custom-aggrid-comparator-filter.tsx
new file mode 100644
index 000000000..3000c8e4c
--- /dev/null
+++ b/src/components/customAGGrid/custom-aggrid-filters/custom-aggrid-comparator-filter.tsx
@@ -0,0 +1,45 @@
+/**
+ * Copyright (c) 2024, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+import { CustomAggridComparatorSelector } from './custom-aggrid-comparator-selector';
+import { CustomAggridTextFilter } from './custom-aggrid-text-filter';
+import { Grid } from '@mui/material';
+import { useCustomAggridComparatorFilter } from './hooks/use-custom-aggrid-comparator-filter';
+
+import { CustomAggridFilterParams } from './custom-aggrid-filter.type';
+
+export const CustomAggridComparatorFilter = ({ api, colId, filterParams }: CustomAggridFilterParams) => {
+ const {
+ selectedFilterData,
+ selectedFilterComparator,
+ decimalAfterDot,
+ isNumberInput,
+ handleFilterComparatorChange,
+ handleFilterTextChange,
+ handleClearFilter,
+ } = useCustomAggridComparatorFilter(api, colId, filterParams);
+
+ const {
+ comparators = [], // used for text filter as a UI type (examples: contains, startsWith..)
+ } = filterParams;
+
+ return (
+
+
+
+
+ );
+};
diff --git a/src/components/customAGGrid/custom-aggrid-filters/custom-aggrid-comparator-selector.tsx b/src/components/customAGGrid/custom-aggrid-filters/custom-aggrid-comparator-selector.tsx
new file mode 100644
index 000000000..ffd106278
--- /dev/null
+++ b/src/components/customAGGrid/custom-aggrid-filters/custom-aggrid-comparator-selector.tsx
@@ -0,0 +1,43 @@
+/**
+ * Copyright (c) 2024, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+import React from 'react';
+import { MenuItem, Select, type SelectChangeEvent } from '@mui/material';
+import { useIntl } from 'react-intl';
+import { type MuiStyles } from '../../../utils/styles';
+
+const styles = {
+ input: {
+ minWidth: '250px',
+ maxWidth: '40%',
+ },
+} as const satisfies MuiStyles;
+
+interface CustomAggridComparatorSelectorProps {
+ value: string;
+ onChange: (event: SelectChangeEvent) => void;
+ options: string[];
+}
+
+export const CustomAggridComparatorSelector: React.FC = ({
+ value,
+ onChange,
+ options,
+}) => {
+ const intl = useIntl();
+
+ return (
+
+ );
+};
diff --git a/src/components/customAGGrid/custom-aggrid-filters/custom-aggrid-duration-filter.tsx b/src/components/customAGGrid/custom-aggrid-filters/custom-aggrid-duration-filter.tsx
new file mode 100644
index 000000000..7868d905e
--- /dev/null
+++ b/src/components/customAGGrid/custom-aggrid-filters/custom-aggrid-duration-filter.tsx
@@ -0,0 +1,195 @@
+/**
+ * Copyright (c) 2024, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+import { ChangeEvent, FunctionComponent, useCallback, useEffect, useState } from 'react';
+import { Grid, IconButton, InputAdornment, TextField, Typography } from '@mui/material';
+import { useIntl } from 'react-intl';
+import ClearIcon from '@mui/icons-material/Clear';
+import { type MuiStyles } from '../../../../utils/styles';
+import { CustomAggridComparatorSelector } from './custom-aggrid-comparator-selector';
+import { SelectChangeEvent } from '@mui/material/Select/SelectInput';
+import { useCustomAggridFilter } from './hooks/use-custom-aggrid-filter';
+import { CustomAggridFilterParams } from './custom-aggrid-filter.type';
+
+const styles = {
+ containerStyle: {
+ width: '250px',
+ },
+ iconStyle: {
+ padding: '0',
+ },
+ flexCenter: {
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ noArrows: {
+ '& input::-webkit-outer-spin-button, & input::-webkit-inner-spin-button': {
+ display: 'none',
+ },
+ '& input[type=number]': {
+ MozAppearance: 'textfield',
+ },
+ },
+} as const satisfies MuiStyles;
+
+const CustomAggridDurationFilter: FunctionComponent = ({ api, colId, filterParams }) => {
+ const intl = useIntl();
+
+ const { selectedFilterData, selectedFilterComparator, handleChangeFilterValue, handleChangeComparator } =
+ useCustomAggridFilter(api, colId, filterParams);
+
+ const {
+ comparators = [], // used for text filter as a UI type (examples: contains, startsWith..)
+ } = filterParams;
+
+ const handleFilterComparatorChange = useCallback(
+ (event: SelectChangeEvent) => {
+ const newType = event.target.value;
+ handleChangeComparator(newType);
+ },
+ [handleChangeComparator]
+ );
+
+ const handleClearFilter = useCallback(() => {
+ handleChangeFilterValue({
+ value: undefined,
+ });
+ }, [handleChangeFilterValue]);
+
+ const handleFilterDurationChange = useCallback(
+ (value?: string) => {
+ handleChangeFilterValue({
+ value,
+ });
+ },
+ [handleChangeFilterValue]
+ );
+
+ // Initialize minutes and seconds based on the initial value prop
+ const parseInitialValue = useCallback(() => {
+ if (selectedFilterData !== undefined && selectedFilterData !== '') {
+ const numericValue = Number(selectedFilterData);
+ if (!isNaN(numericValue)) {
+ return {
+ minutes: Math.floor(numericValue / 60).toString(),
+ seconds: (numericValue % 60).toString(),
+ };
+ }
+ }
+ return { minutes: '', seconds: '' };
+ }, [selectedFilterData]);
+ const { minutes: initialMinutes, seconds: initialSeconds } = parseInitialValue();
+ const [minutes, setMinutes] = useState(initialMinutes);
+ const [seconds, setSeconds] = useState(initialSeconds);
+
+ useEffect(() => {
+ if (!minutes && !seconds) {
+ setMinutes(initialMinutes);
+ setSeconds(initialSeconds);
+ }
+ }, [initialMinutes, initialSeconds, minutes, seconds]);
+
+ const handleTimeChange = useCallback(
+ (newMinutes: string, newSeconds: string) => {
+ // If both minutes and seconds are empty, clear the value
+ if (newMinutes === '' && newSeconds === '') {
+ handleFilterDurationChange('');
+ } else {
+ const totalSeconds = Number(newMinutes) * 60 + Number(newSeconds);
+ handleFilterDurationChange(totalSeconds.toString());
+ }
+ },
+ [handleFilterDurationChange]
+ );
+
+ const handleMinutesChange = useCallback(
+ (event: ChangeEvent) => {
+ const newValue = event.target.value;
+ setMinutes(newValue);
+ handleTimeChange(newValue, seconds);
+ },
+ [handleTimeChange, seconds]
+ );
+
+ const handleSecondsChange = useCallback(
+ (event: ChangeEvent) => {
+ const newValue = event.target.value;
+ if (Number(newValue) > 59) {
+ return;
+ } // Prevents seconds from being greater than 59
+ setSeconds(newValue);
+ handleTimeChange(minutes, newValue);
+ },
+ [handleTimeChange, minutes]
+ );
+
+ const clearValue = useCallback(() => {
+ handleClearFilter(); // Clears the value
+ setMinutes(''); // Reset minutes state
+ setSeconds(''); // Reset seconds state
+ }, [handleClearFilter]);
+
+ return (
+
+
+
+
+ mn,
+ inputProps: { min: 0 },
+ }}
+ sx={styles.noArrows}
+ />
+
+
+ :
+
+
+ s,
+ inputProps: { min: 0, max: 59 },
+ }}
+ sx={styles.noArrows}
+ />
+
+ {selectedFilterData !== undefined && selectedFilterData !== '' && (
+
+
+
+
+
+ )}
+
+
+ );
+};
+
+export default CustomAggridDurationFilter;
diff --git a/src/components/customAGGrid/custom-aggrid-filters/custom-aggrid-filter.tsx b/src/components/customAGGrid/custom-aggrid-filters/custom-aggrid-filter.tsx
new file mode 100644
index 000000000..b2d6b294a
--- /dev/null
+++ b/src/components/customAGGrid/custom-aggrid-filters/custom-aggrid-filter.tsx
@@ -0,0 +1,99 @@
+/**
+ * Copyright (c) 2024, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+import React, { ComponentType, MouseEvent, useMemo, useState } from 'react';
+import { Popover } from '@mui/material';
+import { type MuiStyles } from '../../../utils/styles';
+import { CustomFilterIcon } from './custom-filter-icon';
+import { useCustomAggridFilter } from './hooks/use-custom-aggrid-filter';
+import { CustomAggridAutocompleteFilterParams } from './custom-aggrid-autocomplete-filter';
+import { CustomAggridFilterParams } from './custom-aggrid-filter.type';
+
+const styles = {
+ input: {
+ minWidth: '250px',
+ maxWidth: '40%',
+ },
+ autoCompleteInput: {
+ width: '30%',
+ },
+} as const satisfies MuiStyles;
+
+interface CustomAggridFilterWrapperParams {
+ filterComponent: ComponentType;
+ filterComponentParams: F;
+ isHoveringColumnHeader: boolean;
+ forceDisplayFilterIcon: boolean;
+ handleCloseFilter: () => void;
+}
+
+export const CustomAggridFilter = ({
+ filterComponent: FilterComponent,
+ filterComponentParams,
+ isHoveringColumnHeader,
+ forceDisplayFilterIcon = false,
+ handleCloseFilter,
+}: CustomAggridFilterWrapperParams) => {
+ const [filterAnchorElement, setFilterAnchorElement] = useState(null);
+
+ const { selectedFilterData } = useCustomAggridFilter(
+ filterComponentParams.api,
+ filterComponentParams.colId,
+ filterComponentParams.filterParams
+ );
+
+ const handleShowFilter = (event: MouseEvent) => {
+ setFilterAnchorElement(event.currentTarget);
+ };
+
+ const onClose = () => {
+ handleCloseFilter();
+ setFilterAnchorElement(null);
+ };
+
+ const shouldDisplayFilterIcon = useMemo(
+ () =>
+ (!!FilterComponent && isHoveringColumnHeader) ||
+ (Array.isArray(selectedFilterData) ? selectedFilterData.length > 0 : !!selectedFilterData) ||
+ !!filterAnchorElement ||
+ forceDisplayFilterIcon,
+ [FilterComponent, filterAnchorElement, forceDisplayFilterIcon, isHoveringColumnHeader, selectedFilterData]
+ );
+
+ return (
+ <>
+ {shouldDisplayFilterIcon && (
+
+ )}
+
+
+
+ >
+ );
+};
diff --git a/src/components/customAGGrid/custom-aggrid-filters/custom-aggrid-filter.type.ts b/src/components/customAGGrid/custom-aggrid-filters/custom-aggrid-filter.type.ts
new file mode 100644
index 000000000..68060cce9
--- /dev/null
+++ b/src/components/customAGGrid/custom-aggrid-filters/custom-aggrid-filter.type.ts
@@ -0,0 +1,75 @@
+/**
+ * Copyright (c) 2025, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+import { ColDef, GridApi, IFilterOptionDef } from 'ag-grid-community';
+import { FilterParams } from '../../../types/custom-aggrid-types';
+import React, { ComponentType } from 'react';
+import { SortParams } from '../hooks/use-custom-aggrid-sort';
+import { COLUMN_TYPES, CustomCellType } from '../custom-aggrid-header.type';
+import type { UUID } from 'node:crypto';
+
+export enum FILTER_DATA_TYPES {
+ TEXT = 'text',
+ NUMBER = 'number',
+ BOOLEAN = 'boolean',
+}
+
+export enum FILTER_TEXT_COMPARATORS {
+ EQUALS = 'equals',
+ CONTAINS = 'contains',
+ STARTS_WITH = 'startsWith',
+}
+
+export enum FILTER_NUMBER_COMPARATORS {
+ EQUALS = 'equals',
+ NOT_EQUAL = 'notEqual',
+ LESS_THAN_OR_EQUAL = 'lessThanOrEqual',
+ GREATER_THAN_OR_EQUAL = 'greaterThanOrEqual',
+}
+
+// not visible in the base interface :
+export enum UNDISPLAYED_FILTER_NUMBER_COMPARATORS {
+ GREATER_THAN = 'greaterThan',
+ LESS_THAN = 'lessThan',
+}
+
+export type FilterEnumsType = Record;
+
+export interface CustomAggridFilterParams {
+ api: GridApi;
+ colId: string;
+ filterParams: FilterParams;
+}
+
+export interface ColumnContext {
+ agGridFilterParams?: {
+ filterOptions: IFilterOptionDef[];
+ };
+ tabUuid?: UUID;
+ columnType?: COLUMN_TYPES;
+ columnWidth?: number;
+ fractionDigits?: number;
+ isDefaultSort?: boolean;
+ numeric?: boolean;
+ forceDisplayFilterIcon?: boolean;
+ tabIndex?: number;
+ isCustomColumn?: boolean;
+ // Type intentionally kept generic to avoid app-specific dependency
+ Menu?: React.FC;
+ filterComponent?: ComponentType;
+ //We omit colId and api here to avoid duplicating its declaration, we reinject it later inside CustomHeaderComponent
+ filterComponentParams?: Omit;
+ sortParams?: SortParams;
+}
+
+export type CustomAggridValue = boolean | string | number | CustomCellType | unknown;
+
+export interface CustomColDef
+ extends ColDef {
+ colId: string;
+ context?: ColumnContext;
+}
diff --git a/src/components/customAGGrid/custom-aggrid-filters/custom-aggrid-text-filter.tsx b/src/components/customAGGrid/custom-aggrid-filters/custom-aggrid-text-filter.tsx
new file mode 100644
index 000000000..90b471abb
--- /dev/null
+++ b/src/components/customAGGrid/custom-aggrid-filters/custom-aggrid-text-filter.tsx
@@ -0,0 +1,82 @@
+/**
+ * Copyright (c) 2024, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+import React, { useMemo } from 'react';
+import { Grid, IconButton, InputAdornment, TextField } from '@mui/material';
+import ClearIcon from '@mui/icons-material/Clear';
+import { DisplayRounding } from '../display-rounding';
+import { useIntl } from 'react-intl';
+import { mergeSx, type MuiStyles } from '../../../../utils/styles';
+import { FILTER_DATA_TYPES } from './custom-aggrid-filter.type';
+
+const styles = {
+ input: {
+ minWidth: '250px',
+ maxWidth: '40%',
+ },
+ noArrows: {
+ '& input::-webkit-outer-spin-button, & input::-webkit-inner-spin-button': {
+ display: 'none',
+ },
+ '& input[type=number]': {
+ MozAppearance: 'textfield',
+ },
+ },
+} as const satisfies MuiStyles;
+
+interface CustomAggridTextFilterProps {
+ value: unknown;
+ onChange: (event: React.ChangeEvent) => void;
+ onClear: () => void;
+ isNumberInput: boolean;
+ decimalAfterDot: number;
+}
+
+export const CustomAggridTextFilter: React.FC = ({
+ value,
+ onChange,
+ onClear,
+ isNumberInput,
+ decimalAfterDot = 0,
+}) => {
+ const intl = useIntl();
+
+ const isRoundingDisplayed = useMemo(() => !!(isNumberInput && value), [isNumberInput, value]);
+
+ return (
+
+
+
+
+
+
+
+ ) : null,
+ }}
+ />
+
+ {isRoundingDisplayed && (
+
+
+
+ )}
+
+ );
+};
diff --git a/src/components/customAGGrid/custom-aggrid-filters/custom-filter-icon.tsx b/src/components/customAGGrid/custom-aggrid-filters/custom-filter-icon.tsx
new file mode 100644
index 000000000..21aaedbf4
--- /dev/null
+++ b/src/components/customAGGrid/custom-aggrid-filters/custom-filter-icon.tsx
@@ -0,0 +1,37 @@
+/**
+ * Copyright (c) 2024, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+import React, { MouseEventHandler } from 'react';
+import { Badge, Grid, IconButton } from '@mui/material';
+import { FilterAlt } from '@mui/icons-material';
+import { type MuiStyles } from '../../../utils/styles';
+import { isNonEmptyStringOrArray } from '../../../utils/types-utils';
+
+const styles = {
+ iconSize: { fontSize: '1rem' },
+ gridRoot: { overflow: 'visible' },
+} as const satisfies MuiStyles;
+
+interface CustomFilterIconProps {
+ handleShowFilter: MouseEventHandler | undefined;
+ selectedFilterData: unknown;
+}
+
+export const CustomFilterIcon = ({ handleShowFilter, selectedFilterData }: CustomFilterIconProps) => (
+
+
+
+
+
+
+
+
+
+);
diff --git a/src/components/customAGGrid/custom-aggrid-filters/hooks/index.ts b/src/components/customAGGrid/custom-aggrid-filters/hooks/index.ts
new file mode 100644
index 000000000..59ee19281
--- /dev/null
+++ b/src/components/customAGGrid/custom-aggrid-filters/hooks/index.ts
@@ -0,0 +1,2 @@
+export * from './use-custom-aggrid-comparator-filter';
+export * from './use-custom-aggrid-filter';
diff --git a/src/components/customAGGrid/custom-aggrid-filters/hooks/use-custom-aggrid-comparator-filter.ts b/src/components/customAGGrid/custom-aggrid-filters/hooks/use-custom-aggrid-comparator-filter.ts
new file mode 100644
index 000000000..44baf6791
--- /dev/null
+++ b/src/components/customAGGrid/custom-aggrid-filters/hooks/use-custom-aggrid-comparator-filter.ts
@@ -0,0 +1,67 @@
+/**
+ * Copyright (c) 2024, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+import { ChangeEvent, useMemo } from 'react';
+import { useSnackMessage } from '../../../../hooks/useSnackMessage';
+import { SelectChangeEvent } from '@mui/material/Select/SelectInput';
+import { countDecimalPlacesFromString } from '../../../../utils/rounding';
+import { useCustomAggridFilter } from './use-custom-aggrid-filter';
+import { GridApi } from 'ag-grid-community';
+import { computeTolerance } from '../utils/filter-tolerance-utils';
+import { FilterParams } from '../../../../types/custom-aggrid-types';
+import { FILTER_DATA_TYPES } from '../custom-aggrid-filter.type';
+
+export const useCustomAggridComparatorFilter = (api: GridApi, colId: string, filterParams: FilterParams) => {
+ const { dataType = FILTER_DATA_TYPES.TEXT } = filterParams;
+
+ const isNumberInput = dataType === FILTER_DATA_TYPES.NUMBER;
+
+ const { selectedFilterData, selectedFilterComparator, handleChangeFilterValue, handleChangeComparator } =
+ useCustomAggridFilter(api, colId, filterParams);
+
+ const { snackWarning } = useSnackMessage();
+
+ const handleFilterComparatorChange = (event: SelectChangeEvent) => {
+ const newType = event.target.value;
+ handleChangeComparator(newType);
+ };
+
+ const handleClearFilter = () => {
+ handleChangeFilterValue({
+ value: undefined,
+ });
+ };
+ const handleFilterTextChange = (event: ChangeEvent) => {
+ const value = event.target.value.toUpperCase();
+ handleChangeFilterValue({
+ value,
+ tolerance: isNumberInput ? computeTolerance(value) : undefined,
+ });
+ };
+
+ const decimalAfterDot = useMemo(() => {
+ if (isNumberInput) {
+ let decimalAfterDot: number = countDecimalPlacesFromString(String(selectedFilterData));
+ if (decimalAfterDot >= 13) {
+ snackWarning({
+ headerId: 'filter.warnRounding',
+ });
+ }
+ return decimalAfterDot;
+ }
+ return 0;
+ }, [isNumberInput, selectedFilterData, snackWarning]);
+
+ return {
+ selectedFilterData,
+ selectedFilterComparator,
+ decimalAfterDot,
+ isNumberInput,
+ handleFilterComparatorChange,
+ handleFilterTextChange,
+ handleClearFilter,
+ };
+};
diff --git a/src/components/customAGGrid/custom-aggrid-filters/hooks/use-custom-aggrid-filter.ts b/src/components/customAGGrid/custom-aggrid-filters/hooks/use-custom-aggrid-filter.ts
new file mode 100644
index 000000000..971bd6593
--- /dev/null
+++ b/src/components/customAGGrid/custom-aggrid-filters/hooks/use-custom-aggrid-filter.ts
@@ -0,0 +1,127 @@
+/**
+ * Copyright (c) 2024, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+import { useCallback, useEffect, useState } from 'react';
+import { debounce } from '@mui/material';
+import { GridApi } from 'ag-grid-community';
+import { computeTolerance } from '../utils/filter-tolerance-utils';
+import { FilterConfig, FilterData, FilterParams } from '../../../../types/custom-aggrid-types';
+import { FILTER_DATA_TYPES } from '../custom-aggrid-filter.type';
+
+const removeElementFromArrayWithFieldValue = (filtersArrayToRemoveFieldValueFrom: FilterConfig[], field: string) => {
+ return filtersArrayToRemoveFieldValueFrom.filter((f) => f.column !== field);
+};
+
+const changeValueFromArrayWithFieldValue = (
+ filtersArrayToModify: FilterConfig[],
+ field: string,
+ newData: FilterConfig
+) => {
+ const filterIndex = filtersArrayToModify.findIndex((f) => f.column === field);
+ if (filterIndex === -1) {
+ return [...filtersArrayToModify, newData];
+ } else {
+ const updatedArray = [...filtersArrayToModify];
+ updatedArray[filterIndex] = newData;
+ return updatedArray;
+ }
+};
+
+export const useCustomAggridFilter = (
+ api: GridApi,
+ colId: string,
+ { type, tab, dataType, comparators = [], debounceMs = 1000, updateFilterCallback, filters, setFilters }: FilterParams
+) => {
+ const [selectedFilterComparator, setSelectedFilterComparator] = useState('');
+ const [selectedFilterData, setSelectedFilterData] = useState();
+ const [tolerance, setTolerance] = useState();
+
+ // Store-agnostic: if consumer does not provide external filters, maintain a local list
+ const [localFilters, setLocalFilters] = useState([]);
+ const currentFilters = filters ?? localFilters;
+ const pushFilters = setFilters ?? setLocalFilters;
+
+ const updateFilter = useCallback(
+ (colId: string, data: FilterData): void => {
+ const newFilter = {
+ column: colId,
+ dataType: data.dataType,
+ tolerance: data.dataType === FILTER_DATA_TYPES.NUMBER ? computeTolerance(data.value) : undefined,
+ type: data.type,
+ value: data.value,
+ };
+
+ let updatedFilters: FilterConfig[];
+ if (!data.value) {
+ updatedFilters = removeElementFromArrayWithFieldValue(currentFilters, colId);
+ } else {
+ updatedFilters = changeValueFromArrayWithFieldValue(currentFilters, colId, newFilter);
+ }
+
+ updateFilterCallback && updateFilterCallback(api, updatedFilters);
+ pushFilters(updatedFilters);
+ },
+ [updateFilterCallback, api, pushFilters, currentFilters]
+ );
+
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ const debouncedUpdateFilter = useCallback(
+ debounce((data) => updateFilter(colId, data), debounceMs),
+ [colId, debounceMs]
+ );
+
+ const handleChangeFilterValue = useCallback(
+ (filterData: FilterData) => {
+ setSelectedFilterData(filterData.value);
+ setTolerance(filterData.tolerance);
+ debouncedUpdateFilter({
+ value: filterData.value,
+ type: filterData.type ?? selectedFilterComparator,
+ dataType,
+ tolerance: filterData.tolerance,
+ });
+ },
+ [dataType, debouncedUpdateFilter, selectedFilterComparator]
+ );
+
+ const handleChangeComparator = useCallback(
+ (newType: string) => {
+ setSelectedFilterComparator(newType);
+ if (selectedFilterData) {
+ updateFilter(colId, {
+ value: selectedFilterData,
+ type: newType,
+ dataType,
+ tolerance: tolerance,
+ });
+ }
+ },
+ [colId, dataType, selectedFilterData, tolerance, updateFilter]
+ );
+
+ useEffect(() => {
+ if (!selectedFilterComparator) {
+ setSelectedFilterComparator(comparators[0]);
+ }
+ }, [selectedFilterComparator, comparators]);
+
+ useEffect(() => {
+ const filterObject = currentFilters?.find((filter) => filter.column === colId);
+ if (filterObject) {
+ setSelectedFilterData(filterObject.value);
+ setSelectedFilterComparator(filterObject.type ?? '');
+ } else {
+ setSelectedFilterData(undefined);
+ }
+ }, [currentFilters, colId]);
+
+ return {
+ selectedFilterData,
+ selectedFilterComparator,
+ handleChangeFilterValue,
+ handleChangeComparator,
+ };
+};
diff --git a/src/components/customAGGrid/custom-aggrid-filters/index.ts b/src/components/customAGGrid/custom-aggrid-filters/index.ts
new file mode 100644
index 000000000..c44feae47
--- /dev/null
+++ b/src/components/customAGGrid/custom-aggrid-filters/index.ts
@@ -0,0 +1,10 @@
+export * from './custom-aggrid-autocomplete-filter';
+export * from './custom-aggrid-boolean-filter';
+export * from './custom-aggrid-comparator-filter';
+export { default as CustomAggridDurationFilter } from './custom-aggrid-duration-filter';
+export * from './custom-aggrid-duration-filter';
+export * from './custom-aggrid-filter';
+export * from './custom-aggrid-filter.type';
+export * from './custom-aggrid-text-filter';
+export * from './hooks';
+export * from './utils';
diff --git a/src/components/customAGGrid/custom-aggrid-filters/utils/aggrid-filters-utils.ts b/src/components/customAGGrid/custom-aggrid-filters/utils/aggrid-filters-utils.ts
new file mode 100644
index 000000000..b1cd58132
--- /dev/null
+++ b/src/components/customAGGrid/custom-aggrid-filters/utils/aggrid-filters-utils.ts
@@ -0,0 +1,111 @@
+/**
+ * Copyright (c) 2024, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+import { GridApi } from 'ag-grid-community';
+import { addToleranceToFilter } from './filter-tolerance-utils';
+import { FilterConfig } from '../../../../types/custom-aggrid-types';
+import { FILTER_DATA_TYPES, FILTER_NUMBER_COMPARATORS } from '../custom-aggrid-filter.type';
+
+export enum BooleanFilterValue {
+ TRUE = 'true',
+ FALSE = 'false',
+ UNDEFINED = 'undefinedValue',
+}
+
+interface FilterModel {
+ [colId: string]: any;
+}
+
+const generateEnumFilterModel = (filter: FilterConfig) => {
+ const filterValue = filter.value as string[];
+ return {
+ type: 'text',
+ filterType: 'customInRange',
+ filter: filterValue,
+ };
+};
+
+const formatCustomFiltersForAgGrid = (filters: FilterConfig[]): FilterModel => {
+ const agGridFilterModel: FilterModel = {};
+ const groupedFilters: { [key: string]: FilterConfig[] } = {};
+
+ // Group filters by column
+ filters.forEach((filter) => {
+ if (groupedFilters[filter.column]) {
+ groupedFilters[filter.column].push(filter);
+ } else {
+ groupedFilters[filter.column] = [filter];
+ }
+ });
+
+ // Transform groups of filters into a FilterModel
+ Object.keys(groupedFilters).forEach((column) => {
+ const filters = groupedFilters[column];
+ if (filters.length === 1) {
+ const filter = filters[0];
+ if (Array.isArray(filter.value)) {
+ agGridFilterModel[column] = generateEnumFilterModel(filter);
+ } else {
+ agGridFilterModel[column] = {
+ type: filter.type,
+ tolerance: filter.tolerance,
+ filterType: filter.dataType,
+ filter: filter.dataType === FILTER_DATA_TYPES.NUMBER ? Number(filter.value) : filter.value,
+ };
+ }
+ } else {
+ // Multiple filters on the same column
+ const conditions = filters.map((filter) => ({
+ type: filter.type,
+ tolerance: filter.tolerance,
+ filterType: filter.dataType,
+ filter: filter.dataType === FILTER_DATA_TYPES.NUMBER ? Number(filter.value) : filter.value,
+ }));
+ // Determine operator based on filter types
+ let operator = 'OR';
+ if (filters.length === 2 && filters.every((f) => f?.originalType === FILTER_NUMBER_COMPARATORS.EQUALS)) {
+ operator = 'AND'; // For EQUALS with tolerance
+ }
+
+ // Create a combined filter model with 'OR' for all conditions
+ agGridFilterModel[column] = {
+ type: filters[0].type,
+ tolerance: filters[0].tolerance,
+ operator: operator,
+ conditions,
+ };
+ }
+ });
+
+ return agGridFilterModel;
+};
+
+export const updateFilters = (api: GridApi | undefined, filters: FilterConfig[] | undefined) => {
+ // Check if filters are provided and if the AG Grid API is accessible
+ if (!filters || !api) {
+ return; // Exit if no filters are provided or if the grid API is not accessible
+ }
+
+ // Retrieve the current column definitions from AG Grid
+ const currentColumnDefs = api.getColumns();
+
+ // Filter out any filters that reference columns which are not visible or don't exist in the current column definitions
+ const validFilters = filters.filter((filter) =>
+ currentColumnDefs?.some((col) => col.getColId() === filter.column && col.isVisible())
+ );
+
+ // If we have any valid filters, apply them
+ if (validFilters.length > 0) {
+ const filterWithTolerance = addToleranceToFilter(validFilters);
+ // Format the valid filters for AG Grid and apply them using setFilterModel
+ const formattedFilters = formatCustomFiltersForAgGrid(filterWithTolerance);
+ api.setFilterModel(formattedFilters);
+ } else {
+ // Clear filters if no valid filters exist
+ api.setFilterModel(null);
+ }
+};
diff --git a/src/components/customAGGrid/custom-aggrid-filters/utils/filter-tolerance-utils.ts b/src/components/customAGGrid/custom-aggrid-filters/utils/filter-tolerance-utils.ts
new file mode 100644
index 000000000..cbdfcd86e
--- /dev/null
+++ b/src/components/customAGGrid/custom-aggrid-filters/utils/filter-tolerance-utils.ts
@@ -0,0 +1,111 @@
+/**
+ * Copyright (c) 2024, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+import { isNumber } from 'mathjs';
+import { countDecimalPlaces, countDecimalPlacesFromString } from '../../../../utils/rounding';
+import { FilterConfig } from '../../../../types/custom-aggrid-types';
+import {
+ FILTER_DATA_TYPES,
+ FILTER_NUMBER_COMPARATORS,
+ UNDISPLAYED_FILTER_NUMBER_COMPARATORS,
+} from '../custom-aggrid-filter.type';
+
+/**
+ * Compute the tolerance that should be applied when comparing filter values to database values
+ * @param value value entered in the filter
+ */
+export const computeTolerance = (value: unknown) => {
+ if (!value) {
+ return 0;
+ }
+ let decimalPrecision: number;
+ // the reference for the comparison is the number of digits after the decimal point in 'value'
+ // extra digits are ignored, but the user may add '0's after the decimal point in order to get a better precision
+ if (isNumber(value)) {
+ decimalPrecision = countDecimalPlaces(value);
+ } else {
+ decimalPrecision = countDecimalPlacesFromString(value as string);
+ }
+ // tolerance is multiplied by 0.5 to simulate the fact that the database value is rounded (in the front, from the user viewpoint)
+ // more than 13 decimal after dot will likely cause rounding errors due to double precision
+ return (1 / Math.pow(10, decimalPrecision)) * 0.5;
+};
+
+export const addToleranceToFilter = (
+ filters: FilterConfig[],
+ tolerance: number | undefined = undefined
+): FilterConfig[] => {
+ let finalTolerance: number;
+ if (tolerance !== undefined) {
+ finalTolerance = tolerance;
+ }
+ return filters
+ .map((filter): FilterConfig | FilterConfig[] => {
+ // Attempt to convert filter value to a number if it's a string, otherwise keep it as is
+ let valueAsNumber = typeof filter.value === 'string' ? parseFloat(filter.value) : filter.value;
+ // If the value is successfully converted to a number, apply tolerance adjustments
+ if (
+ typeof valueAsNumber === 'number' &&
+ !isNaN(valueAsNumber) &&
+ filter.dataType === FILTER_DATA_TYPES.NUMBER
+ ) {
+ if (tolerance === undefined) {
+ // better to use the string value (filter.value) in order not to lose the decimal precision for values like 420.0000000
+ finalTolerance = computeTolerance(filter.value);
+ }
+
+ // Depending on the filter type, adjust the filter value by adding or subtracting the tolerance
+ switch (filter.type) {
+ // Creates two conditions to test we are not in [value-tolerance..value+tolerance] (handles rounded decimal precision)
+ case FILTER_NUMBER_COMPARATORS.NOT_EQUAL:
+ return [
+ {
+ ...filter,
+ type: UNDISPLAYED_FILTER_NUMBER_COMPARATORS.GREATER_THAN,
+ originalType: filter.type,
+ value: valueAsNumber + finalTolerance,
+ },
+ {
+ ...filter,
+ type: UNDISPLAYED_FILTER_NUMBER_COMPARATORS.LESS_THAN,
+ originalType: filter.type,
+ value: valueAsNumber - finalTolerance,
+ },
+ ];
+ case FILTER_NUMBER_COMPARATORS.EQUALS:
+ return [
+ {
+ ...filter,
+ type: FILTER_NUMBER_COMPARATORS.GREATER_THAN_OR_EQUAL,
+ originalType: filter.type,
+ value: valueAsNumber - finalTolerance,
+ },
+ {
+ ...filter,
+ type: FILTER_NUMBER_COMPARATORS.LESS_THAN_OR_EQUAL,
+ originalType: filter.type,
+ value: valueAsNumber + finalTolerance,
+ },
+ ];
+ case FILTER_NUMBER_COMPARATORS.LESS_THAN_OR_EQUAL:
+ // Adjust the value upwards by the tolerance
+ return {
+ ...filter,
+ value: valueAsNumber + finalTolerance,
+ };
+ case FILTER_NUMBER_COMPARATORS.GREATER_THAN_OR_EQUAL:
+ return {
+ ...filter,
+ value: valueAsNumber - finalTolerance,
+ };
+ default:
+ return filter;
+ }
+ }
+ return filter;
+ })
+ .flat(); // Flatten the array in case any filters were expanded into multiple conditions
+};
diff --git a/src/components/customAGGrid/custom-aggrid-filters/utils/index.ts b/src/components/customAGGrid/custom-aggrid-filters/utils/index.ts
new file mode 100644
index 000000000..8e6f3d8a9
--- /dev/null
+++ b/src/components/customAGGrid/custom-aggrid-filters/utils/index.ts
@@ -0,0 +1,2 @@
+export * from './aggrid-filters-utils';
+export * from './filter-tolerance-utils';
diff --git a/src/components/customAGGrid/custom-aggrid-header.tsx b/src/components/customAGGrid/custom-aggrid-header.tsx
new file mode 100644
index 000000000..7c95994a2
--- /dev/null
+++ b/src/components/customAGGrid/custom-aggrid-header.tsx
@@ -0,0 +1,109 @@
+/**
+ * Copyright (c) 2023, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+import React, { ComponentType, useCallback, useState } from 'react';
+import { Grid } from '@mui/material';
+import { type MuiStyles } from '../../utils/styles';
+import { CustomAggridFilter } from './custom-aggrid-filters/custom-aggrid-filter';
+import { CustomAggridSort } from './custom-aggrid-sort';
+import { SortParams, useCustomAggridSort } from './hooks/use-custom-aggrid-sort';
+import { CustomMenu, CustomMenuProps } from './custom-aggrid-menu';
+import { CustomHeaderProps } from 'ag-grid-react';
+import { CustomAggridFilterParams } from './custom-aggrid-filters/custom-aggrid-filter.type';
+
+const styles = {
+ displayName: {
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+ },
+} as const satisfies MuiStyles;
+
+interface CustomHeaderComponentProps extends CustomHeaderProps {
+ displayName: string;
+ sortParams?: SortParams;
+ menu?: CustomMenuProps;
+ forceDisplayFilterIcon: boolean;
+ filterComponent: ComponentType;
+ filterComponentParams: F;
+}
+
+const CustomHeaderComponent = ({
+ column,
+ displayName,
+ sortParams,
+ menu,
+ forceDisplayFilterIcon,
+ filterComponent,
+ filterComponentParams,
+ api,
+}: CustomHeaderComponentProps) => {
+ const [isHoveringColumnHeader, setIsHoveringColumnHeader] = useState(false);
+
+ const { handleSortChange } = useCustomAggridSort(column.getId(), sortParams);
+ const isSortable = !!sortParams;
+ const handleClickHeader = () => {
+ handleSortChange && handleSortChange();
+ };
+
+ const handleCloseFilter = () => {
+ setIsHoveringColumnHeader(false);
+ };
+
+ const handleMouseEnter = useCallback(() => {
+ setIsHoveringColumnHeader(true);
+ }, []);
+
+ const handleMouseLeave = useCallback(() => {
+ setIsHoveringColumnHeader(false);
+ }, []);
+
+ return (
+
+
+ {/* We tweak flexBasis to stick the column filter and custom menu either next the column name or on the far right of the header */}
+
+
+
+ {displayName}
+ {sortParams && (
+
+
+
+ )}
+
+
+
+
+ {filterComponent && (
+
+ )}
+ {menu && }
+
+
+
+ );
+};
+
+export default CustomHeaderComponent;
diff --git a/src/components/customAGGrid/custom-aggrid-header.type.ts b/src/components/customAGGrid/custom-aggrid-header.type.ts
new file mode 100644
index 000000000..47fa6abc4
--- /dev/null
+++ b/src/components/customAGGrid/custom-aggrid-header.type.ts
@@ -0,0 +1,18 @@
+/**
+ * Copyright (c) 2023, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+export enum COLUMN_TYPES {
+ TEXT = 'TEXT',
+ ENUM = 'ENUM',
+ NUMBER = 'NUMBER',
+ BOOLEAN = 'BOOLEAN',
+}
+
+export type CustomCellType = {
+ cellValue: number;
+ tooltipValue: number;
+};
diff --git a/src/components/customAGGrid/custom-aggrid-menu.tsx b/src/components/customAGGrid/custom-aggrid-menu.tsx
new file mode 100644
index 000000000..c1256f8e8
--- /dev/null
+++ b/src/components/customAGGrid/custom-aggrid-menu.tsx
@@ -0,0 +1,45 @@
+/**
+ * Copyright (c) 2024, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+import React, { useRef, useState } from 'react';
+import { Badge, Grid, IconButton } from '@mui/material';
+import MoreVertIcon from '@mui/icons-material/MoreVert';
+import { type MuiStyles } from '../../utils/styles';
+
+const styles = {
+ iconSize: {
+ fontSize: '1rem',
+ },
+} as const satisfies MuiStyles;
+
+export interface CustomMenuProps {
+ Menu: React.FC;
+ menuParams: T;
+}
+
+export interface DialogMenuProps {
+ open: boolean;
+ anchorEl: HTMLElement | null;
+ onClose: () => void;
+}
+
+export const CustomMenu = ({ Menu, menuParams }: CustomMenuProps) => {
+ const [menuOpen, setMenuOpen] = useState(false);
+ const menuButtonRef = useRef(null);
+
+ return (
+ <>
+
+ setMenuOpen(true)}>
+
+
+
+
+
+