@tsiky/components-r19 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.ts +35 -33
- package/package.json +1 -1
- package/src/components/Charts/area-chart-admission/AreaChartAdmission.tsx +123 -89
- package/src/components/Charts/bar-chart/BarChart.tsx +167 -132
- package/src/components/Charts/mixed-chart/MixedChart.tsx +65 -9
- package/src/components/Charts/sankey-chart/SankeyChart.tsx +183 -155
- package/src/components/Confirmationpopup/ConfirmationPopup.module.css +88 -0
- package/src/components/Confirmationpopup/ConfirmationPopup.stories.tsx +94 -0
- package/src/components/Confirmationpopup/ConfirmationPopup.tsx +47 -0
- package/src/components/Confirmationpopup/index.ts +6 -0
- package/src/components/Confirmationpopup/useConfirmationPopup.ts +48 -0
- package/src/components/DayStatCard/DayStatCard.tsx +96 -69
- package/src/components/DynamicTable/AdvancedFilters.tsx +196 -196
- package/src/components/DynamicTable/ColumnSorter.tsx +185 -185
- package/src/components/DynamicTable/Pagination.tsx +115 -115
- package/src/components/DynamicTable/TableauDynamique.module.css +1287 -1287
- package/src/components/DynamicTable/filters/SelectFilter.tsx +69 -69
- package/src/components/EntryControl/EntryControl.tsx +117 -117
- package/src/components/Grid/Grid.tsx +5 -0
- package/src/components/Header/Header.tsx +4 -2
- package/src/components/Header/header.css +61 -31
- package/src/components/MetricsPanel/MetricsPanel.module.css +688 -636
- package/src/components/MetricsPanel/MetricsPanel.tsx +220 -282
- package/src/components/MetricsPanel/renderers/CompactRenderer.tsx +148 -125
- package/src/components/NavBar/NavBar.tsx +1 -1
- package/src/components/SelectFilter/SelectFilter.module.css +249 -0
- package/src/components/SelectFilter/SelectFilter.stories.tsx +321 -0
- package/src/components/SelectFilter/SelectFilter.tsx +219 -0
- package/src/components/SelectFilter/index.ts +2 -0
- package/src/components/SelectFilter/types.ts +19 -0
- package/src/components/TranslationKey/TranslationKey.tsx +265 -245
- package/src/components/TrendList/TrendList.tsx +72 -45
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import styles from './ConfirmationPopup.module.css';
|
|
3
|
+
|
|
4
|
+
interface ConfirmationPopupProps {
|
|
5
|
+
isOpen: boolean;
|
|
6
|
+
title: string;
|
|
7
|
+
message: string;
|
|
8
|
+
onConfirm: () => void;
|
|
9
|
+
onCancel: () => void;
|
|
10
|
+
confirmText?: string;
|
|
11
|
+
cancelText?: string;
|
|
12
|
+
type?: 'warning' | 'info' | 'danger';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const ConfirmationPopup: React.FC<ConfirmationPopupProps> = ({
|
|
16
|
+
isOpen,
|
|
17
|
+
title,
|
|
18
|
+
message,
|
|
19
|
+
onConfirm,
|
|
20
|
+
onCancel,
|
|
21
|
+
confirmText = 'Confirmer',
|
|
22
|
+
cancelText = 'Annuler',
|
|
23
|
+
type = 'warning',
|
|
24
|
+
}) => {
|
|
25
|
+
if (!isOpen) return null;
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div className={styles.overlay}>
|
|
29
|
+
<div className={`${styles.popup} ${styles[type]}`}>
|
|
30
|
+
<div className={styles.header}>
|
|
31
|
+
<h3>{title}</h3>
|
|
32
|
+
</div>
|
|
33
|
+
<div className={styles.body}>
|
|
34
|
+
<p>{message}</p>
|
|
35
|
+
</div>
|
|
36
|
+
<div className={styles.footer}>
|
|
37
|
+
<button className={styles.cancelButton} onClick={onCancel}>
|
|
38
|
+
{cancelText}
|
|
39
|
+
</button>
|
|
40
|
+
<button className={styles.confirmButton} onClick={onConfirm}>
|
|
41
|
+
{confirmText}
|
|
42
|
+
</button>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { useState, useCallback } from 'react';
|
|
2
|
+
|
|
3
|
+
interface ConfirmationState {
|
|
4
|
+
isOpen: boolean;
|
|
5
|
+
title: string;
|
|
6
|
+
message: string;
|
|
7
|
+
onConfirm: (() => void) | null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const useConfirmationPopup = () => {
|
|
11
|
+
const [confirmationState, setConfirmationState] = useState<ConfirmationState>({
|
|
12
|
+
isOpen: false,
|
|
13
|
+
title: '',
|
|
14
|
+
message: '',
|
|
15
|
+
onConfirm: null,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const showConfirmation = useCallback((title: string, message: string, onConfirm: () => void) => {
|
|
19
|
+
setConfirmationState({
|
|
20
|
+
isOpen: true,
|
|
21
|
+
title,
|
|
22
|
+
message,
|
|
23
|
+
onConfirm,
|
|
24
|
+
});
|
|
25
|
+
}, []);
|
|
26
|
+
|
|
27
|
+
const hideConfirmation = useCallback(() => {
|
|
28
|
+
setConfirmationState((prev) => ({
|
|
29
|
+
...prev,
|
|
30
|
+
isOpen: false,
|
|
31
|
+
onConfirm: null,
|
|
32
|
+
}));
|
|
33
|
+
}, []);
|
|
34
|
+
|
|
35
|
+
const handleConfirm = useCallback(() => {
|
|
36
|
+
confirmationState.onConfirm?.();
|
|
37
|
+
hideConfirmation();
|
|
38
|
+
}, [confirmationState.onConfirm, hideConfirmation]);
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
isOpen: confirmationState.isOpen,
|
|
42
|
+
title: confirmationState.title,
|
|
43
|
+
message: confirmationState.message,
|
|
44
|
+
showConfirmation,
|
|
45
|
+
hideConfirmation,
|
|
46
|
+
handleConfirm,
|
|
47
|
+
};
|
|
48
|
+
};
|
|
@@ -1,69 +1,96 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import type { CSSProperties } from 'react';
|
|
3
|
-
import './DayStatCard.css';
|
|
4
|
-
import { CircularProgress } from '../CircularProgress/CircularProgress';
|
|
5
|
-
import type { CircularProgressProps } from '../CircularProgress/CircularProgress';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
<
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { CSSProperties } from 'react';
|
|
3
|
+
import './DayStatCard.css';
|
|
4
|
+
import { CircularProgress } from '../CircularProgress/CircularProgress';
|
|
5
|
+
import type { CircularProgressProps } from '../CircularProgress/CircularProgress';
|
|
6
|
+
import { Move } from 'lucide-react';
|
|
7
|
+
|
|
8
|
+
export type DayStatCardProps = {
|
|
9
|
+
background?: string;
|
|
10
|
+
headerTop: string;
|
|
11
|
+
headerBottom?: string;
|
|
12
|
+
headerTopStyle?: CSSProperties;
|
|
13
|
+
headerBottomStyle?: CSSProperties;
|
|
14
|
+
progress1: CircularProgressProps;
|
|
15
|
+
progress2: CircularProgressProps;
|
|
16
|
+
progress3: CircularProgressProps;
|
|
17
|
+
style?: CSSProperties;
|
|
18
|
+
size?: number;
|
|
19
|
+
className?: string;
|
|
20
|
+
draggable?: boolean; // nouvelle prop
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const DayStatCard: React.FC<DayStatCardProps> = ({
|
|
24
|
+
size = 200,
|
|
25
|
+
background = '#fff',
|
|
26
|
+
headerTop,
|
|
27
|
+
headerBottom,
|
|
28
|
+
headerTopStyle,
|
|
29
|
+
headerBottomStyle,
|
|
30
|
+
progress1,
|
|
31
|
+
progress2,
|
|
32
|
+
progress3,
|
|
33
|
+
style,
|
|
34
|
+
className = '',
|
|
35
|
+
draggable = false,
|
|
36
|
+
}) => {
|
|
37
|
+
const progress1WithDefaults = { size: size * 0.5, ...progress1 };
|
|
38
|
+
const progress2WithDefaults = { size: size * 0.4, ...progress2 };
|
|
39
|
+
const progress3WithDefaults = { size: size * 0.4, ...progress3 };
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div
|
|
43
|
+
style={{ width: size, background, position: 'relative', ...style }}
|
|
44
|
+
className={`stat-card ${className}`}
|
|
45
|
+
>
|
|
46
|
+
{/* Header toujours visible */}
|
|
47
|
+
<div className='stat-card-header'>
|
|
48
|
+
<div className='stat-card-header-top' style={headerTopStyle}>
|
|
49
|
+
{headerTop}
|
|
50
|
+
</div>
|
|
51
|
+
{headerBottom && (
|
|
52
|
+
<div className='stat-card-header-bottom' style={headerBottomStyle}>
|
|
53
|
+
{headerBottom}
|
|
54
|
+
</div>
|
|
55
|
+
)}
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
{/* Body */}
|
|
59
|
+
<div className='stat-card-body' style={{ opacity: draggable ? 0 : 1 }}>
|
|
60
|
+
{/* Première ligne - Progress Centré */}
|
|
61
|
+
<div className='progress-row-single'>
|
|
62
|
+
<CircularProgress {...progress1WithDefaults} />
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
{/* Deuxième ligne - Deux Progress côte à côte */}
|
|
66
|
+
<div className='progress-row-double'>
|
|
67
|
+
<div className='progress-item'>
|
|
68
|
+
<CircularProgress {...progress2WithDefaults} />
|
|
69
|
+
</div>
|
|
70
|
+
<div className='progress-item'>
|
|
71
|
+
<CircularProgress {...progress3WithDefaults} />
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
{/* Overlay Move */}
|
|
77
|
+
{draggable && (
|
|
78
|
+
<div
|
|
79
|
+
style={{
|
|
80
|
+
position: 'absolute',
|
|
81
|
+
top: 0,
|
|
82
|
+
left: 0,
|
|
83
|
+
width: '100%',
|
|
84
|
+
height: '100%',
|
|
85
|
+
display: 'flex',
|
|
86
|
+
alignItems: 'center',
|
|
87
|
+
justifyContent: 'center',
|
|
88
|
+
pointerEvents: 'none',
|
|
89
|
+
}}
|
|
90
|
+
>
|
|
91
|
+
<Move size={32} strokeWidth={1.5} style={{ opacity: 0.8 }} />
|
|
92
|
+
</div>
|
|
93
|
+
)}
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
};
|
|
@@ -1,196 +1,196 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import React, { useEffect, useRef, useState } from 'react';
|
|
4
|
-
import { X, Filter } from 'lucide-react';
|
|
5
|
-
import type { FilterConfig, AppliedFilter } from './tools/filterTypes';
|
|
6
|
-
import { useFilters } from './hooks/useFilters';
|
|
7
|
-
import FilterRenderer from './filters/FilterRenderer';
|
|
8
|
-
import styles from './TableauDynamique.module.css';
|
|
9
|
-
|
|
10
|
-
interface AdvancedFiltersProps {
|
|
11
|
-
filters: FilterConfig[];
|
|
12
|
-
externalFilters?: AppliedFilter[];
|
|
13
|
-
onFiltersChange?: (filters: AppliedFilter[]) => void;
|
|
14
|
-
onApply?: (filters: AppliedFilter[]) => void;
|
|
15
|
-
isControlled?: boolean;
|
|
16
|
-
debounceTimeout?: number;
|
|
17
|
-
resultsCount?: number;
|
|
18
|
-
expanded?: boolean;
|
|
19
|
-
onToggle?: (expanded: boolean) => void;
|
|
20
|
-
panelPlacement?: 'overlay' | 'top';
|
|
21
|
-
onPanelHeightChange?: (height: number) => void;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const AdvancedFilters: React.FC<AdvancedFiltersProps> = ({
|
|
25
|
-
filters,
|
|
26
|
-
externalFilters = [],
|
|
27
|
-
onFiltersChange,
|
|
28
|
-
onApply,
|
|
29
|
-
isControlled = false,
|
|
30
|
-
debounceTimeout = 300,
|
|
31
|
-
resultsCount = 0,
|
|
32
|
-
expanded,
|
|
33
|
-
onToggle,
|
|
34
|
-
panelPlacement = 'top',
|
|
35
|
-
onPanelHeightChange,
|
|
36
|
-
}) => {
|
|
37
|
-
const [localExpanded, setLocalExpanded] = useState(false);
|
|
38
|
-
const isExpanded = typeof expanded === 'boolean' ? expanded : localExpanded;
|
|
39
|
-
|
|
40
|
-
const { appliedFilters, tempFilters, updateFilter, applyFilters, clearAllFilters } = useFilters({
|
|
41
|
-
filters,
|
|
42
|
-
externalFilters,
|
|
43
|
-
onFiltersChange,
|
|
44
|
-
onApply,
|
|
45
|
-
debounceTimeout,
|
|
46
|
-
isControlled,
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
const panelRef = useRef<HTMLDivElement | null>(null);
|
|
50
|
-
const wrapperRef = useRef<HTMLDivElement | null>(null);
|
|
51
|
-
|
|
52
|
-
useEffect(() => {
|
|
53
|
-
if (isExpanded && panelRef.current) {
|
|
54
|
-
const h = panelRef.current.getBoundingClientRect().height;
|
|
55
|
-
onPanelHeightChange?.(h);
|
|
56
|
-
} else {
|
|
57
|
-
onPanelHeightChange?.(0);
|
|
58
|
-
}
|
|
59
|
-
}, [isExpanded, filters, tempFilters, onPanelHeightChange]);
|
|
60
|
-
|
|
61
|
-
const toggle = () => {
|
|
62
|
-
if (typeof expanded === 'boolean') {
|
|
63
|
-
onToggle?.(!expanded);
|
|
64
|
-
} else {
|
|
65
|
-
setLocalExpanded((s) => {
|
|
66
|
-
const next = !s;
|
|
67
|
-
onToggle?.(next);
|
|
68
|
-
return next;
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
const handleApply = async () => {
|
|
74
|
-
await applyFilters();
|
|
75
|
-
onToggle?.(false);
|
|
76
|
-
if (typeof expanded !== 'boolean') setLocalExpanded(false);
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
useEffect(() => {
|
|
80
|
-
const onPointerDown = (e: PointerEvent) => {
|
|
81
|
-
if (!isExpanded) return;
|
|
82
|
-
if (!wrapperRef.current) return;
|
|
83
|
-
if (!wrapperRef.current.contains(e.target as Node)) {
|
|
84
|
-
onToggle?.(false);
|
|
85
|
-
if (typeof expanded !== 'boolean') setLocalExpanded(false);
|
|
86
|
-
}
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
const onKey = (e: KeyboardEvent) => {
|
|
90
|
-
if (e.key === 'Escape' && isExpanded) {
|
|
91
|
-
onToggle?.(false);
|
|
92
|
-
if (typeof expanded !== 'boolean') setLocalExpanded(false);
|
|
93
|
-
}
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
if (isExpanded) {
|
|
97
|
-
document.addEventListener('pointerdown', onPointerDown);
|
|
98
|
-
document.addEventListener('keydown', onKey);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return () => {
|
|
102
|
-
document.removeEventListener('pointerdown', onPointerDown);
|
|
103
|
-
document.removeEventListener('keydown', onKey);
|
|
104
|
-
};
|
|
105
|
-
}, [isExpanded, expanded, onToggle]);
|
|
106
|
-
|
|
107
|
-
return (
|
|
108
|
-
<div className={styles.advancedFiltersContainer} ref={wrapperRef}>
|
|
109
|
-
<div className={styles.filtersHeader}>
|
|
110
|
-
<button
|
|
111
|
-
className={styles.toggleFiltersButton}
|
|
112
|
-
onClick={toggle}
|
|
113
|
-
aria-expanded={isExpanded}
|
|
114
|
-
aria-controls='advanced-filters-panel'
|
|
115
|
-
>
|
|
116
|
-
<Filter size={14} />
|
|
117
|
-
<span className={styles.buttonTextCompact}>Advanced-Filters</span>
|
|
118
|
-
</button>
|
|
119
|
-
|
|
120
|
-
{appliedFilters.length > 0 && (
|
|
121
|
-
<button className={styles.clearFiltersButton} onClick={clearAllFilters}>
|
|
122
|
-
<X size={14} /> <span className={styles.buttonTextCompact}>Effacer</span>
|
|
123
|
-
</button>
|
|
124
|
-
)}
|
|
125
|
-
</div>
|
|
126
|
-
|
|
127
|
-
{isExpanded && (
|
|
128
|
-
<div
|
|
129
|
-
id='advanced-filters-panel'
|
|
130
|
-
ref={panelRef}
|
|
131
|
-
className={`${styles.filtersPanel} ${
|
|
132
|
-
panelPlacement === 'top'
|
|
133
|
-
? `${styles.filtersPanelInline} ${styles.filtersPanelHorizontal}`
|
|
134
|
-
: ''
|
|
135
|
-
}`}
|
|
136
|
-
role='region'
|
|
137
|
-
aria-label='Panneau de filtres avancés'
|
|
138
|
-
>
|
|
139
|
-
<div className={styles.filtersRow}>
|
|
140
|
-
{filters.map((filter) => (
|
|
141
|
-
<div key={filter.id} className={`${styles.filterItem} ${styles.filterItemInline}`}>
|
|
142
|
-
<label className={styles.filterLabel}>
|
|
143
|
-
{filter.label}
|
|
144
|
-
{filter.required && <span className={styles.required}>*</span>}
|
|
145
|
-
</label>
|
|
146
|
-
<FilterRenderer
|
|
147
|
-
config={filter}
|
|
148
|
-
value={tempFilters[filter.id]}
|
|
149
|
-
onChange={(value) => updateFilter(filter.id, value)}
|
|
150
|
-
disabled={filter.disabled}
|
|
151
|
-
/>
|
|
152
|
-
</div>
|
|
153
|
-
))}
|
|
154
|
-
</div>
|
|
155
|
-
|
|
156
|
-
<div className={styles.filtersActionsHorizontal}>
|
|
157
|
-
<div className={styles.activeFiltersWrap}>
|
|
158
|
-
{appliedFilters.length > 0 && (
|
|
159
|
-
<div className={styles.activeFilters}>
|
|
160
|
-
<span className={styles.activeLabelCompact}>Actifs :</span>
|
|
161
|
-
{appliedFilters.map((f) => (
|
|
162
|
-
<div key={f.id} className={styles.activeFilterTag}>
|
|
163
|
-
{f.label ?? f.id}
|
|
164
|
-
<button
|
|
165
|
-
onClick={() => updateFilter(f.id, undefined)}
|
|
166
|
-
aria-label={`Supprimer ${f.id}`}
|
|
167
|
-
>
|
|
168
|
-
×
|
|
169
|
-
</button>
|
|
170
|
-
</div>
|
|
171
|
-
))}
|
|
172
|
-
</div>
|
|
173
|
-
)}
|
|
174
|
-
</div>
|
|
175
|
-
|
|
176
|
-
<div className={styles.actionsGroup}>
|
|
177
|
-
<button
|
|
178
|
-
className={styles.cancelButton}
|
|
179
|
-
onClick={() => {
|
|
180
|
-
onToggle?.(false);
|
|
181
|
-
}}
|
|
182
|
-
>
|
|
183
|
-
Cancel
|
|
184
|
-
</button>
|
|
185
|
-
<button className={styles.applyButton} onClick={handleApply}>
|
|
186
|
-
Apply
|
|
187
|
-
</button>
|
|
188
|
-
</div>
|
|
189
|
-
</div>
|
|
190
|
-
</div>
|
|
191
|
-
)}
|
|
192
|
-
</div>
|
|
193
|
-
);
|
|
194
|
-
};
|
|
195
|
-
|
|
196
|
-
export default AdvancedFilters;
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
4
|
+
import { X, Filter } from 'lucide-react';
|
|
5
|
+
import type { FilterConfig, AppliedFilter } from './tools/filterTypes';
|
|
6
|
+
import { useFilters } from './hooks/useFilters';
|
|
7
|
+
import FilterRenderer from './filters/FilterRenderer';
|
|
8
|
+
import styles from './TableauDynamique.module.css';
|
|
9
|
+
|
|
10
|
+
interface AdvancedFiltersProps {
|
|
11
|
+
filters: FilterConfig[];
|
|
12
|
+
externalFilters?: AppliedFilter[];
|
|
13
|
+
onFiltersChange?: (filters: AppliedFilter[]) => void;
|
|
14
|
+
onApply?: (filters: AppliedFilter[]) => void;
|
|
15
|
+
isControlled?: boolean;
|
|
16
|
+
debounceTimeout?: number;
|
|
17
|
+
resultsCount?: number;
|
|
18
|
+
expanded?: boolean;
|
|
19
|
+
onToggle?: (expanded: boolean) => void;
|
|
20
|
+
panelPlacement?: 'overlay' | 'top';
|
|
21
|
+
onPanelHeightChange?: (height: number) => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const AdvancedFilters: React.FC<AdvancedFiltersProps> = ({
|
|
25
|
+
filters,
|
|
26
|
+
externalFilters = [],
|
|
27
|
+
onFiltersChange,
|
|
28
|
+
onApply,
|
|
29
|
+
isControlled = false,
|
|
30
|
+
debounceTimeout = 300,
|
|
31
|
+
resultsCount = 0,
|
|
32
|
+
expanded,
|
|
33
|
+
onToggle,
|
|
34
|
+
panelPlacement = 'top',
|
|
35
|
+
onPanelHeightChange,
|
|
36
|
+
}) => {
|
|
37
|
+
const [localExpanded, setLocalExpanded] = useState(false);
|
|
38
|
+
const isExpanded = typeof expanded === 'boolean' ? expanded : localExpanded;
|
|
39
|
+
|
|
40
|
+
const { appliedFilters, tempFilters, updateFilter, applyFilters, clearAllFilters } = useFilters({
|
|
41
|
+
filters,
|
|
42
|
+
externalFilters,
|
|
43
|
+
onFiltersChange,
|
|
44
|
+
onApply,
|
|
45
|
+
debounceTimeout,
|
|
46
|
+
isControlled,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const panelRef = useRef<HTMLDivElement | null>(null);
|
|
50
|
+
const wrapperRef = useRef<HTMLDivElement | null>(null);
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (isExpanded && panelRef.current) {
|
|
54
|
+
const h = panelRef.current.getBoundingClientRect().height;
|
|
55
|
+
onPanelHeightChange?.(h);
|
|
56
|
+
} else {
|
|
57
|
+
onPanelHeightChange?.(0);
|
|
58
|
+
}
|
|
59
|
+
}, [isExpanded, filters, tempFilters, onPanelHeightChange]);
|
|
60
|
+
|
|
61
|
+
const toggle = () => {
|
|
62
|
+
if (typeof expanded === 'boolean') {
|
|
63
|
+
onToggle?.(!expanded);
|
|
64
|
+
} else {
|
|
65
|
+
setLocalExpanded((s) => {
|
|
66
|
+
const next = !s;
|
|
67
|
+
onToggle?.(next);
|
|
68
|
+
return next;
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const handleApply = async () => {
|
|
74
|
+
await applyFilters();
|
|
75
|
+
onToggle?.(false);
|
|
76
|
+
if (typeof expanded !== 'boolean') setLocalExpanded(false);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
const onPointerDown = (e: PointerEvent) => {
|
|
81
|
+
if (!isExpanded) return;
|
|
82
|
+
if (!wrapperRef.current) return;
|
|
83
|
+
if (!wrapperRef.current.contains(e.target as Node)) {
|
|
84
|
+
onToggle?.(false);
|
|
85
|
+
if (typeof expanded !== 'boolean') setLocalExpanded(false);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const onKey = (e: KeyboardEvent) => {
|
|
90
|
+
if (e.key === 'Escape' && isExpanded) {
|
|
91
|
+
onToggle?.(false);
|
|
92
|
+
if (typeof expanded !== 'boolean') setLocalExpanded(false);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
if (isExpanded) {
|
|
97
|
+
document.addEventListener('pointerdown', onPointerDown);
|
|
98
|
+
document.addEventListener('keydown', onKey);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return () => {
|
|
102
|
+
document.removeEventListener('pointerdown', onPointerDown);
|
|
103
|
+
document.removeEventListener('keydown', onKey);
|
|
104
|
+
};
|
|
105
|
+
}, [isExpanded, expanded, onToggle]);
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<div className={styles.advancedFiltersContainer} ref={wrapperRef}>
|
|
109
|
+
<div className={styles.filtersHeader}>
|
|
110
|
+
<button
|
|
111
|
+
className={styles.toggleFiltersButton}
|
|
112
|
+
onClick={toggle}
|
|
113
|
+
aria-expanded={isExpanded}
|
|
114
|
+
aria-controls='advanced-filters-panel'
|
|
115
|
+
>
|
|
116
|
+
<Filter size={14} />
|
|
117
|
+
<span className={styles.buttonTextCompact}>Advanced-Filters</span>
|
|
118
|
+
</button>
|
|
119
|
+
|
|
120
|
+
{appliedFilters.length > 0 && (
|
|
121
|
+
<button className={styles.clearFiltersButton} onClick={clearAllFilters}>
|
|
122
|
+
<X size={14} /> <span className={styles.buttonTextCompact}>Effacer</span>
|
|
123
|
+
</button>
|
|
124
|
+
)}
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
{isExpanded && (
|
|
128
|
+
<div
|
|
129
|
+
id='advanced-filters-panel'
|
|
130
|
+
ref={panelRef}
|
|
131
|
+
className={`${styles.filtersPanel} ${
|
|
132
|
+
panelPlacement === 'top'
|
|
133
|
+
? `${styles.filtersPanelInline} ${styles.filtersPanelHorizontal}`
|
|
134
|
+
: ''
|
|
135
|
+
}`}
|
|
136
|
+
role='region'
|
|
137
|
+
aria-label='Panneau de filtres avancés'
|
|
138
|
+
>
|
|
139
|
+
<div className={styles.filtersRow}>
|
|
140
|
+
{filters.map((filter) => (
|
|
141
|
+
<div key={filter.id} className={`${styles.filterItem} ${styles.filterItemInline}`}>
|
|
142
|
+
<label className={styles.filterLabel}>
|
|
143
|
+
{filter.label}
|
|
144
|
+
{filter.required && <span className={styles.required}>*</span>}
|
|
145
|
+
</label>
|
|
146
|
+
<FilterRenderer
|
|
147
|
+
config={filter}
|
|
148
|
+
value={tempFilters[filter.id]}
|
|
149
|
+
onChange={(value) => updateFilter(filter.id, value)}
|
|
150
|
+
disabled={filter.disabled}
|
|
151
|
+
/>
|
|
152
|
+
</div>
|
|
153
|
+
))}
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
<div className={styles.filtersActionsHorizontal}>
|
|
157
|
+
<div className={styles.activeFiltersWrap}>
|
|
158
|
+
{appliedFilters.length > 0 && (
|
|
159
|
+
<div className={styles.activeFilters}>
|
|
160
|
+
<span className={styles.activeLabelCompact}>Actifs :</span>
|
|
161
|
+
{appliedFilters.map((f) => (
|
|
162
|
+
<div key={f.id} className={styles.activeFilterTag}>
|
|
163
|
+
{f.label ?? f.id}
|
|
164
|
+
<button
|
|
165
|
+
onClick={() => updateFilter(f.id, undefined)}
|
|
166
|
+
aria-label={`Supprimer ${f.id}`}
|
|
167
|
+
>
|
|
168
|
+
×
|
|
169
|
+
</button>
|
|
170
|
+
</div>
|
|
171
|
+
))}
|
|
172
|
+
</div>
|
|
173
|
+
)}
|
|
174
|
+
</div>
|
|
175
|
+
|
|
176
|
+
<div className={styles.actionsGroup}>
|
|
177
|
+
<button
|
|
178
|
+
className={styles.cancelButton}
|
|
179
|
+
onClick={() => {
|
|
180
|
+
onToggle?.(false);
|
|
181
|
+
}}
|
|
182
|
+
>
|
|
183
|
+
Cancel
|
|
184
|
+
</button>
|
|
185
|
+
<button className={styles.applyButton} onClick={handleApply}>
|
|
186
|
+
Apply
|
|
187
|
+
</button>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
)}
|
|
192
|
+
</div>
|
|
193
|
+
);
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
export default AdvancedFilters;
|