@wakastellar/ui 2.0.0 → 2.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.
Files changed (290) hide show
  1. package/dist/cli/commands/add.d.ts +7 -0
  2. package/dist/cli/commands/init.d.ts +6 -0
  3. package/dist/cli/commands/list.d.ts +5 -0
  4. package/dist/cli/commands/search.d.ts +1 -0
  5. package/dist/cli/index.cjs +4844 -0
  6. package/dist/cli/index.d.ts +1 -0
  7. package/dist/cli/utils/config.d.ts +29 -0
  8. package/dist/cli/utils/logger.d.ts +20 -0
  9. package/dist/cli/utils/registry.d.ts +23 -0
  10. package/package.json +14 -3
  11. package/src/blocks/activity-timeline/index.tsx +586 -0
  12. package/src/blocks/calendar-view/index.tsx +756 -0
  13. package/src/blocks/chat/index.tsx +1018 -0
  14. package/src/blocks/chat/widget.tsx +504 -0
  15. package/src/blocks/dashboard/index.tsx +522 -0
  16. package/src/blocks/empty-states/index.tsx +452 -0
  17. package/src/blocks/error-pages/index.tsx +426 -0
  18. package/src/blocks/faq/index.tsx +479 -0
  19. package/src/blocks/file-manager/index.tsx +890 -0
  20. package/src/blocks/footer/index.tsx +133 -0
  21. package/src/blocks/header/index.tsx +357 -0
  22. package/src/blocks/headtab/index.tsx +139 -0
  23. package/src/blocks/i18n-editor/index.tsx +1016 -0
  24. package/src/blocks/index.ts +80 -0
  25. package/src/blocks/kanban-board/index.tsx +779 -0
  26. package/src/blocks/landing/index.tsx +677 -0
  27. package/src/blocks/language-selector/index.tsx +88 -0
  28. package/src/blocks/layout/index.tsx +159 -0
  29. package/src/blocks/login/index.tsx +339 -0
  30. package/src/blocks/login/types.ts +131 -0
  31. package/src/blocks/pricing/index.tsx +564 -0
  32. package/src/blocks/profile/index.tsx +746 -0
  33. package/src/blocks/settings/index.tsx +558 -0
  34. package/src/blocks/sidebar/index.tsx +713 -0
  35. package/src/blocks/theme-creator-block/index.tsx +835 -0
  36. package/src/blocks/user-management/index.tsx +1037 -0
  37. package/src/blocks/wizard/index.tsx +719 -0
  38. package/src/components/DataTable/DataTable.tsx +406 -0
  39. package/src/components/DataTable/DataTableAdvanced.tsx +720 -0
  40. package/src/components/DataTable/DataTableBody.tsx +216 -0
  41. package/src/components/DataTable/DataTableCell.tsx +172 -0
  42. package/src/components/DataTable/DataTableColumnResizer.tsx +62 -0
  43. package/src/components/DataTable/DataTableConflictResolver.tsx +478 -0
  44. package/src/components/DataTable/DataTableContextMenu.tsx +219 -0
  45. package/src/components/DataTable/DataTableEditCell.tsx +279 -0
  46. package/src/components/DataTable/DataTableFilterBuilder.tsx +519 -0
  47. package/src/components/DataTable/DataTableFilters.tsx +535 -0
  48. package/src/components/DataTable/DataTableGrouping.tsx +147 -0
  49. package/src/components/DataTable/DataTableHeader.tsx +172 -0
  50. package/src/components/DataTable/DataTablePagination.tsx +125 -0
  51. package/src/components/DataTable/DataTableSelection.tsx +269 -0
  52. package/src/components/DataTable/DataTableSyncStatus.tsx +281 -0
  53. package/src/components/DataTable/DataTableToolbar.tsx +262 -0
  54. package/src/components/DataTable/README.md +446 -0
  55. package/src/components/DataTable/__tests__/DataTableAdvanced.test.tsx +426 -0
  56. package/src/components/DataTable/__tests__/DataTableEdit.test.tsx +329 -0
  57. package/src/components/DataTable/__tests__/useDataTableAdvanced.test.ts +455 -0
  58. package/src/components/DataTable/examples/EditExample.tsx +166 -0
  59. package/src/components/DataTable/formatters/index.ts +335 -0
  60. package/src/components/DataTable/hooks/__tests__/useDataTableEdit.test.ts +239 -0
  61. package/src/components/DataTable/hooks/useDataTable.ts +145 -0
  62. package/src/components/DataTable/hooks/useDataTableAdvanced.ts +342 -0
  63. package/src/components/DataTable/hooks/useDataTableAdvancedFilters.ts +637 -0
  64. package/src/components/DataTable/hooks/useDataTableColumnTemplates.ts +186 -0
  65. package/src/components/DataTable/hooks/useDataTableEdit.ts +167 -0
  66. package/src/components/DataTable/hooks/useDataTableExport.ts +227 -0
  67. package/src/components/DataTable/hooks/useDataTableImport.ts +216 -0
  68. package/src/components/DataTable/hooks/useDataTableOffline.ts +481 -0
  69. package/src/components/DataTable/hooks/useDataTableTheme.ts +213 -0
  70. package/src/components/DataTable/hooks/useDataTableVirtualization.ts +99 -0
  71. package/src/components/DataTable/hooks/useTableLayout.ts +85 -0
  72. package/src/components/DataTable/index.ts +81 -0
  73. package/src/components/DataTable/services/IndexedDBService.ts +504 -0
  74. package/src/components/DataTable/templates/index.tsx +803 -0
  75. package/src/components/DataTable/types.ts +504 -0
  76. package/src/components/DataTable/utils.ts +164 -0
  77. package/src/components/DataTable/workers/exportWorker.ts +213 -0
  78. package/src/components/accordion/index.tsx +61 -0
  79. package/src/components/alert/index.tsx +61 -0
  80. package/src/components/alert-dialog/index.tsx +146 -0
  81. package/src/components/aspect-ratio/index.tsx +12 -0
  82. package/src/components/avatar/index.tsx +54 -0
  83. package/src/components/badge/Badge.stories.tsx +64 -0
  84. package/src/components/badge/index.tsx +38 -0
  85. package/src/components/button/Button.stories.tsx +173 -0
  86. package/src/components/button/index.tsx +56 -0
  87. package/src/components/calendar/index.tsx +73 -0
  88. package/src/components/card/index.tsx +78 -0
  89. package/src/components/checkbox/index.tsx +34 -0
  90. package/src/components/code/index.tsx +229 -0
  91. package/src/components/collapsible/index.tsx +16 -0
  92. package/src/components/command/index.tsx +162 -0
  93. package/src/components/context-menu/index.tsx +204 -0
  94. package/src/components/dialog/index.tsx +126 -0
  95. package/src/components/dropdown-menu/index.tsx +204 -0
  96. package/src/components/error-boundary/ErrorBoundary.tsx +281 -0
  97. package/src/components/error-boundary/index.ts +7 -0
  98. package/src/components/form/index.tsx +183 -0
  99. package/src/components/hover-card/index.tsx +33 -0
  100. package/src/components/index.ts +368 -0
  101. package/src/components/input/Input.stories.tsx +100 -0
  102. package/src/components/input/index.tsx +27 -0
  103. package/src/components/input-otp/index.tsx +277 -0
  104. package/src/components/label/index.tsx +30 -0
  105. package/src/components/language-selector/index.tsx +341 -0
  106. package/src/components/menubar/index.tsx +240 -0
  107. package/src/components/navigation-menu/index.tsx +134 -0
  108. package/src/components/popover/index.tsx +35 -0
  109. package/src/components/progress/index.tsx +32 -0
  110. package/src/components/radio-group/index.tsx +48 -0
  111. package/src/components/scroll-area/index.tsx +52 -0
  112. package/src/components/select/index.tsx +164 -0
  113. package/src/components/separator/index.tsx +35 -0
  114. package/src/components/sheet/index.tsx +147 -0
  115. package/src/components/skeleton/index.tsx +22 -0
  116. package/src/components/slider/index.tsx +32 -0
  117. package/src/components/switch/index.tsx +33 -0
  118. package/src/components/table/index.tsx +117 -0
  119. package/src/components/tabs/index.tsx +59 -0
  120. package/src/components/textarea/index.tsx +30 -0
  121. package/src/components/theme-selector/index.tsx +327 -0
  122. package/src/components/toast/index.tsx +133 -0
  123. package/src/components/toaster/index.tsx +34 -0
  124. package/src/components/toggle/index.tsx +49 -0
  125. package/src/components/tooltip/index.tsx +34 -0
  126. package/src/components/typography/index.tsx +276 -0
  127. package/src/components/waka-3d-pie-chart/index.tsx +486 -0
  128. package/src/components/waka-achievement-unlock/index.tsx +716 -0
  129. package/src/components/waka-activity-feed/index.tsx +686 -0
  130. package/src/components/waka-address-autocomplete/index.tsx +1202 -0
  131. package/src/components/waka-admincrumb/index.tsx +349 -0
  132. package/src/components/waka-alert-stack/index.tsx +827 -0
  133. package/src/components/waka-allocation-matrix/index.tsx +1278 -0
  134. package/src/components/waka-approval-chain/index.tsx +766 -0
  135. package/src/components/waka-audit-log/index.tsx +1475 -0
  136. package/src/components/waka-autocomplete/index.tsx +358 -0
  137. package/src/components/waka-badge-showcase/index.tsx +704 -0
  138. package/src/components/waka-barcode/index.tsx +260 -0
  139. package/src/components/waka-biometric-prompt/index.tsx +765 -0
  140. package/src/components/waka-bottom-sheet/index.tsx +495 -0
  141. package/src/components/waka-breadcrumb/index.tsx +376 -0
  142. package/src/components/waka-breadcrumb-path/index.tsx +513 -0
  143. package/src/components/waka-budget-burn/index.tsx +1234 -0
  144. package/src/components/waka-capacity-planner/index.tsx +1107 -0
  145. package/src/components/waka-carousel/index.tsx +893 -0
  146. package/src/components/waka-cart-summary/index.tsx +1055 -0
  147. package/src/components/waka-challenge-timer/index.tsx +1044 -0
  148. package/src/components/waka-charts/WakaAreaChart.tsx +251 -0
  149. package/src/components/waka-charts/WakaBarChart.tsx +222 -0
  150. package/src/components/waka-charts/WakaChart.tsx +124 -0
  151. package/src/components/waka-charts/WakaLineChart.tsx +219 -0
  152. package/src/components/waka-charts/WakaMiniChart.tsx +133 -0
  153. package/src/components/waka-charts/WakaPieChart.tsx +214 -0
  154. package/src/components/waka-charts/WakaSparkline.tsx +229 -0
  155. package/src/components/waka-charts/dataTableHelpers.ts +109 -0
  156. package/src/components/waka-charts/hooks/useChartTheme.ts +123 -0
  157. package/src/components/waka-charts/hooks/useRechartsLoader.ts +234 -0
  158. package/src/components/waka-charts/index.ts +90 -0
  159. package/src/components/waka-charts/types.ts +330 -0
  160. package/src/components/waka-chat-bubble/index.tsx +1060 -0
  161. package/src/components/waka-checklist/index.tsx +1067 -0
  162. package/src/components/waka-checkout-stepper/index.tsx +976 -0
  163. package/src/components/waka-cohort-table/index.tsx +1011 -0
  164. package/src/components/waka-color-picker/index.tsx +447 -0
  165. package/src/components/waka-combo-counter/index.tsx +864 -0
  166. package/src/components/waka-combobox/index.tsx +497 -0
  167. package/src/components/waka-command-bar/index.tsx +403 -0
  168. package/src/components/waka-compare-period/index.tsx +1230 -0
  169. package/src/components/waka-connection-matrix/index.tsx +1053 -0
  170. package/src/components/waka-contribution-graph/index.tsx +552 -0
  171. package/src/components/waka-cost-breakdown/index.tsx +1065 -0
  172. package/src/components/waka-coupon-input/index.tsx +592 -0
  173. package/src/components/waka-credit-card-input/index.tsx +982 -0
  174. package/src/components/waka-daily-reward/index.tsx +762 -0
  175. package/src/components/waka-date-range-picker/index.tsx +378 -0
  176. package/src/components/waka-datetime-picker/index.tsx +793 -0
  177. package/src/components/waka-datetime-picker.form-integration/index.tsx +402 -0
  178. package/src/components/waka-deployment-lane/index.tsx +673 -0
  179. package/src/components/waka-device-trust/index.tsx +1259 -0
  180. package/src/components/waka-dock/index.tsx +285 -0
  181. package/src/components/waka-drawer/index.tsx +319 -0
  182. package/src/components/waka-empty-state/index.tsx +545 -0
  183. package/src/components/waka-error-shake/index.tsx +398 -0
  184. package/src/components/waka-feature-announcement/index.tsx +991 -0
  185. package/src/components/waka-file-upload/index.tsx +437 -0
  186. package/src/components/waka-floating-nav/index.tsx +413 -0
  187. package/src/components/waka-flow-diagram/index.tsx +508 -0
  188. package/src/components/waka-funnel-chart/index.tsx +823 -0
  189. package/src/components/waka-glow-card/index.tsx +246 -0
  190. package/src/components/waka-goal-progress/index.tsx +1025 -0
  191. package/src/components/waka-haptic-button/index.tsx +388 -0
  192. package/src/components/waka-health-pulse/index.tsx +451 -0
  193. package/src/components/waka-heatmap/index.tsx +1026 -0
  194. package/src/components/waka-hotspot/index.tsx +682 -0
  195. package/src/components/waka-image/index.tsx +373 -0
  196. package/src/components/waka-incident-timeline/index.tsx +686 -0
  197. package/src/components/waka-invoice-preview/index.tsx +829 -0
  198. package/src/components/waka-kanban/index.tsx +646 -0
  199. package/src/components/waka-kpi-dashboard/index.tsx +755 -0
  200. package/src/components/waka-leaderboard/index.tsx +746 -0
  201. package/src/components/waka-level-progress/index.tsx +665 -0
  202. package/src/components/waka-liquid-button/index.tsx +520 -0
  203. package/src/components/waka-loading-orbit/index.tsx +478 -0
  204. package/src/components/waka-loot-box/index.tsx +1091 -0
  205. package/src/components/waka-magic-link/index.tsx +321 -0
  206. package/src/components/waka-magnetic-button/index.tsx +567 -0
  207. package/src/components/waka-mention-input/index.tsx +953 -0
  208. package/src/components/waka-metric-sparkline/index.tsx +627 -0
  209. package/src/components/waka-milestone-road/index.tsx +1064 -0
  210. package/src/components/waka-modal/index.tsx +374 -0
  211. package/src/components/waka-morph-button/index.tsx +495 -0
  212. package/src/components/waka-network-topology/index.tsx +801 -0
  213. package/src/components/waka-notifications/index.tsx +414 -0
  214. package/src/components/waka-number-input/index.tsx +373 -0
  215. package/src/components/waka-orbital-menu/index.tsx +445 -0
  216. package/src/components/waka-order-tracker/index.tsx +1041 -0
  217. package/src/components/waka-pagination/index.tsx +393 -0
  218. package/src/components/waka-password-strength/index.tsx +824 -0
  219. package/src/components/waka-payment-method-picker/index.tsx +715 -0
  220. package/src/components/waka-permission-matrix/index.tsx +1302 -0
  221. package/src/components/waka-phone-input/index.tsx +801 -0
  222. package/src/components/waka-pipeline-view/index.tsx +604 -0
  223. package/src/components/waka-player-card/index.tsx +691 -0
  224. package/src/components/waka-points-popup/index.tsx +366 -0
  225. package/src/components/waka-power-up/index.tsx +1155 -0
  226. package/src/components/waka-presence-indicator/index.tsx +1181 -0
  227. package/src/components/waka-pricing-table/index.tsx +755 -0
  228. package/src/components/waka-product-card/index.tsx +786 -0
  229. package/src/components/waka-progress-onboarding/index.tsx +878 -0
  230. package/src/components/waka-pull-to-refresh/index.tsx +451 -0
  231. package/src/components/waka-qrcode/index.tsx +232 -0
  232. package/src/components/waka-quest-card/index.tsx +1275 -0
  233. package/src/components/waka-quota-bar/index.tsx +693 -0
  234. package/src/components/waka-radar-score/index.tsx +512 -0
  235. package/src/components/waka-rank-badge/index.tsx +813 -0
  236. package/src/components/waka-rating-input/index.tsx +560 -0
  237. package/src/components/waka-reaction-picker/index.tsx +1062 -0
  238. package/src/components/waka-region-map/index.tsx +730 -0
  239. package/src/components/waka-resource-gauge/index.tsx +654 -0
  240. package/src/components/waka-resource-pool/index.tsx +1035 -0
  241. package/src/components/waka-rich-text-editor/index.tsx +594 -0
  242. package/src/components/waka-rollback-slider/index.tsx +891 -0
  243. package/src/components/waka-sankey-diagram/index.tsx +1032 -0
  244. package/src/components/waka-schedule-picker/index.tsx +1060 -0
  245. package/src/components/waka-scratch-card/index.tsx +914 -0
  246. package/src/components/waka-season-pass/index.tsx +886 -0
  247. package/src/components/waka-security-score/index.tsx +1126 -0
  248. package/src/components/waka-segmented-control/index.tsx +238 -0
  249. package/src/components/waka-server-rack/index.tsx +764 -0
  250. package/src/components/waka-session-manager/index.tsx +815 -0
  251. package/src/components/waka-signature-pad/index.tsx +744 -0
  252. package/src/components/waka-skeleton-wave/index.tsx +454 -0
  253. package/src/components/waka-skill-tree/index.tsx +1031 -0
  254. package/src/components/waka-sla-tracker/index.tsx +798 -0
  255. package/src/components/waka-slider-range/index.tsx +765 -0
  256. package/src/components/waka-spin-wheel/index.tsx +671 -0
  257. package/src/components/waka-spinner/index.tsx +284 -0
  258. package/src/components/waka-spotlight/index.tsx +410 -0
  259. package/src/components/waka-stat/index.tsx +428 -0
  260. package/src/components/waka-stats-hexagon/index.tsx +824 -0
  261. package/src/components/waka-status-matrix/index.tsx +565 -0
  262. package/src/components/waka-stepper/index.tsx +489 -0
  263. package/src/components/waka-streak-counter/index.tsx +334 -0
  264. package/src/components/waka-success-explosion/index.tsx +453 -0
  265. package/src/components/waka-swipe-card/index.tsx +574 -0
  266. package/src/components/waka-tabs-morph/index.tsx +509 -0
  267. package/src/components/waka-tag-input/index.tsx +877 -0
  268. package/src/components/waka-team-banner/index.tsx +1183 -0
  269. package/src/components/waka-terminal-output/index.tsx +836 -0
  270. package/src/components/waka-theme-creator/index.tsx +762 -0
  271. package/src/components/waka-theme-manager/index.tsx +654 -0
  272. package/src/components/waka-thread-view/index.tsx +874 -0
  273. package/src/components/waka-tilt-card/index.tsx +250 -0
  274. package/src/components/waka-time-picker/index.tsx +479 -0
  275. package/src/components/waka-timeline/index.tsx +385 -0
  276. package/src/components/waka-tooltip-tour/index.tsx +855 -0
  277. package/src/components/waka-tour-guide/index.tsx +920 -0
  278. package/src/components/waka-tournament-bracket/index.tsx +1276 -0
  279. package/src/components/waka-tree/index.tsx +557 -0
  280. package/src/components/waka-treemap-chart/index.tsx +1031 -0
  281. package/src/components/waka-two-factor-setup/index.tsx +995 -0
  282. package/src/components/waka-typewriter/index.tsx +566 -0
  283. package/src/components/waka-typing-indicator/index.tsx +649 -0
  284. package/src/components/waka-versus-card/index.tsx +1026 -0
  285. package/src/components/waka-video/index.tsx +557 -0
  286. package/src/components/waka-video-call/index.tsx +1087 -0
  287. package/src/components/waka-virtual-list/index.tsx +327 -0
  288. package/src/components/waka-voice-message/index.tsx +1019 -0
  289. package/src/components/waka-welcome-modal/index.tsx +790 -0
  290. package/src/components/waka-xp-bar/index.tsx +799 -0
@@ -0,0 +1,637 @@
1
+ import { useState, useMemo, useCallback, useEffect } from "react"
2
+ import type {
3
+ FilterOperator,
4
+ FilterColumnType,
5
+ FilterRule,
6
+ FilterGroup,
7
+ FilterPreset,
8
+ FilterableColumn,
9
+ AdvancedFiltersConfig,
10
+ AdvancedFiltersState,
11
+ AdvancedFiltersActions,
12
+ AdvancedFiltersResult,
13
+ } from "../types"
14
+
15
+ /**
16
+ * Génère un ID unique
17
+ */
18
+ function generateId(): string {
19
+ return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`
20
+ }
21
+
22
+ /**
23
+ * Opérateurs par type de colonne
24
+ */
25
+ const OPERATORS_BY_TYPE: Record<FilterColumnType, FilterOperator[]> = {
26
+ string: [
27
+ "equals",
28
+ "notEquals",
29
+ "contains",
30
+ "notContains",
31
+ "startsWith",
32
+ "endsWith",
33
+ "isEmpty",
34
+ "isNotEmpty",
35
+ "isNull",
36
+ "isNotNull",
37
+ ],
38
+ number: [
39
+ "equals",
40
+ "notEquals",
41
+ "greaterThan",
42
+ "greaterThanOrEqual",
43
+ "lessThan",
44
+ "lessThanOrEqual",
45
+ "between",
46
+ "isNull",
47
+ "isNotNull",
48
+ ],
49
+ date: [
50
+ "equals",
51
+ "notEquals",
52
+ "greaterThan",
53
+ "greaterThanOrEqual",
54
+ "lessThan",
55
+ "lessThanOrEqual",
56
+ "between",
57
+ "isNull",
58
+ "isNotNull",
59
+ ],
60
+ datetime: [
61
+ "equals",
62
+ "notEquals",
63
+ "greaterThan",
64
+ "greaterThanOrEqual",
65
+ "lessThan",
66
+ "lessThanOrEqual",
67
+ "between",
68
+ "isNull",
69
+ "isNotNull",
70
+ ],
71
+ boolean: ["equals", "notEquals", "isNull", "isNotNull"],
72
+ enum: ["equals", "notEquals", "in", "notIn", "isNull", "isNotNull"],
73
+ array: ["contains", "notContains", "isEmpty", "isNotEmpty", "isNull", "isNotNull"],
74
+ }
75
+
76
+ /**
77
+ * Labels des opérateurs pour l'affichage
78
+ */
79
+ export const OPERATOR_LABELS: Record<FilterOperator, string> = {
80
+ equals: "est égal à",
81
+ notEquals: "est différent de",
82
+ contains: "contient",
83
+ notContains: "ne contient pas",
84
+ startsWith: "commence par",
85
+ endsWith: "finit par",
86
+ greaterThan: "supérieur à",
87
+ greaterThanOrEqual: "supérieur ou égal à",
88
+ lessThan: "inférieur à",
89
+ lessThanOrEqual: "inférieur ou égal à",
90
+ between: "entre",
91
+ in: "est dans",
92
+ notIn: "n'est pas dans",
93
+ isEmpty: "est vide",
94
+ isNotEmpty: "n'est pas vide",
95
+ isNull: "est null",
96
+ isNotNull: "n'est pas null",
97
+ }
98
+
99
+ /**
100
+ * Évalue une règle de filtre sur une ligne de données
101
+ */
102
+ function evaluateRule<TData>(
103
+ row: TData,
104
+ rule: FilterRule<TData>,
105
+ columns: FilterableColumn<TData>[]
106
+ ): boolean {
107
+ const column = columns.find((c) => c.key === rule.column)
108
+
109
+ // Si la colonne a une fonction de filtre personnalisée
110
+ if (column?.filterFn) {
111
+ return column.filterFn(row, rule)
112
+ }
113
+
114
+ const value = (row as Record<string, unknown>)[rule.column as string]
115
+ const filterValue = rule.value
116
+ const filterValueTo = rule.valueTo
117
+
118
+ // Opérateurs de nullité
119
+ if (rule.operator === "isNull") return value === null || value === undefined
120
+ if (rule.operator === "isNotNull") return value !== null && value !== undefined
121
+ if (rule.operator === "isEmpty") {
122
+ if (Array.isArray(value)) return value.length === 0
123
+ if (typeof value === "string") return value.trim() === ""
124
+ return value === null || value === undefined
125
+ }
126
+ if (rule.operator === "isNotEmpty") {
127
+ if (Array.isArray(value)) return value.length > 0
128
+ if (typeof value === "string") return value.trim() !== ""
129
+ return value !== null && value !== undefined
130
+ }
131
+
132
+ // Si la valeur est null/undefined, les autres opérateurs retournent false
133
+ if (value === null || value === undefined) return false
134
+
135
+ // Comparaisons de chaînes
136
+ if (typeof value === "string" || column?.type === "string") {
137
+ const strValue = String(value).toLowerCase()
138
+ const strFilter = String(filterValue ?? "").toLowerCase()
139
+
140
+ switch (rule.operator) {
141
+ case "equals":
142
+ return strValue === strFilter
143
+ case "notEquals":
144
+ return strValue !== strFilter
145
+ case "contains":
146
+ return strValue.includes(strFilter)
147
+ case "notContains":
148
+ return !strValue.includes(strFilter)
149
+ case "startsWith":
150
+ return strValue.startsWith(strFilter)
151
+ case "endsWith":
152
+ return strValue.endsWith(strFilter)
153
+ default:
154
+ return true
155
+ }
156
+ }
157
+
158
+ // Comparaisons numériques
159
+ if (typeof value === "number" || column?.type === "number") {
160
+ const numValue = Number(value)
161
+ const numFilter = Number(filterValue)
162
+ const numFilterTo = filterValueTo !== undefined ? Number(filterValueTo) : undefined
163
+
164
+ switch (rule.operator) {
165
+ case "equals":
166
+ return numValue === numFilter
167
+ case "notEquals":
168
+ return numValue !== numFilter
169
+ case "greaterThan":
170
+ return numValue > numFilter
171
+ case "greaterThanOrEqual":
172
+ return numValue >= numFilter
173
+ case "lessThan":
174
+ return numValue < numFilter
175
+ case "lessThanOrEqual":
176
+ return numValue <= numFilter
177
+ case "between":
178
+ return numFilterTo !== undefined && numValue >= numFilter && numValue <= numFilterTo
179
+ default:
180
+ return true
181
+ }
182
+ }
183
+
184
+ // Comparaisons de dates
185
+ if (value instanceof Date || column?.type === "date" || column?.type === "datetime") {
186
+ const dateValue = value instanceof Date ? value : new Date(value as string)
187
+ const dateFilter = filterValue instanceof Date ? filterValue : new Date(filterValue as string)
188
+ const dateFilterTo =
189
+ filterValueTo instanceof Date ? filterValueTo : filterValueTo ? new Date(filterValueTo as string) : undefined
190
+
191
+ switch (rule.operator) {
192
+ case "equals":
193
+ return dateValue.getTime() === dateFilter.getTime()
194
+ case "notEquals":
195
+ return dateValue.getTime() !== dateFilter.getTime()
196
+ case "greaterThan":
197
+ return dateValue.getTime() > dateFilter.getTime()
198
+ case "greaterThanOrEqual":
199
+ return dateValue.getTime() >= dateFilter.getTime()
200
+ case "lessThan":
201
+ return dateValue.getTime() < dateFilter.getTime()
202
+ case "lessThanOrEqual":
203
+ return dateValue.getTime() <= dateFilter.getTime()
204
+ case "between":
205
+ return dateFilterTo !== undefined && dateValue >= dateFilter && dateValue <= dateFilterTo
206
+ default:
207
+ return true
208
+ }
209
+ }
210
+
211
+ // Comparaisons booléennes
212
+ if (typeof value === "boolean" || column?.type === "boolean") {
213
+ const boolValue = Boolean(value)
214
+ const boolFilter = filterValue === true || filterValue === "true"
215
+
216
+ switch (rule.operator) {
217
+ case "equals":
218
+ return boolValue === boolFilter
219
+ case "notEquals":
220
+ return boolValue !== boolFilter
221
+ default:
222
+ return true
223
+ }
224
+ }
225
+
226
+ // Comparaisons de tableaux et enum (in/notIn)
227
+ if (rule.operator === "in") {
228
+ if (Array.isArray(filterValue)) {
229
+ return filterValue.includes(value)
230
+ }
231
+ return value === filterValue
232
+ }
233
+ if (rule.operator === "notIn") {
234
+ if (Array.isArray(filterValue)) {
235
+ return !filterValue.includes(value)
236
+ }
237
+ return value !== filterValue
238
+ }
239
+
240
+ // Contient pour les tableaux
241
+ if (Array.isArray(value)) {
242
+ switch (rule.operator) {
243
+ case "contains":
244
+ return value.includes(filterValue)
245
+ case "notContains":
246
+ return !value.includes(filterValue)
247
+ default:
248
+ return true
249
+ }
250
+ }
251
+
252
+ return true
253
+ }
254
+
255
+ /**
256
+ * Évalue un groupe de filtres (récursif)
257
+ */
258
+ function evaluateGroup<TData>(
259
+ row: TData,
260
+ group: FilterGroup<TData>,
261
+ columns: FilterableColumn<TData>[]
262
+ ): boolean {
263
+ const enabledRules = group.rules.filter((r) => r.enabled !== false)
264
+ const results: boolean[] = []
265
+
266
+ // Évaluer les règles
267
+ for (const rule of enabledRules) {
268
+ results.push(evaluateRule(row, rule, columns))
269
+ }
270
+
271
+ // Évaluer les sous-groupes
272
+ for (const subGroup of group.groups) {
273
+ results.push(evaluateGroup(row, subGroup, columns))
274
+ }
275
+
276
+ // Si pas de résultats, considérer comme match
277
+ if (results.length === 0) return true
278
+
279
+ // Appliquer le connecteur
280
+ if (group.connector === "AND") {
281
+ return results.every(Boolean)
282
+ } else {
283
+ return results.some(Boolean)
284
+ }
285
+ }
286
+
287
+ /**
288
+ * Trouve une règle par ID dans un groupe (récursif)
289
+ */
290
+ function findRuleById<TData>(
291
+ group: FilterGroup<TData>,
292
+ ruleId: string
293
+ ): { rule: FilterRule<TData>; parent: FilterGroup<TData> } | null {
294
+ for (const rule of group.rules) {
295
+ if (rule.id === ruleId) {
296
+ return { rule, parent: group }
297
+ }
298
+ }
299
+ for (const subGroup of group.groups) {
300
+ const found = findRuleById(subGroup, ruleId)
301
+ if (found) return found
302
+ }
303
+ return null
304
+ }
305
+
306
+ /**
307
+ * Trouve un groupe par ID (récursif)
308
+ */
309
+ function findGroupById<TData>(
310
+ group: FilterGroup<TData>,
311
+ groupId: string,
312
+ parent: FilterGroup<TData> | null = null
313
+ ): { group: FilterGroup<TData>; parent: FilterGroup<TData> | null } | null {
314
+ if (group.id === groupId) {
315
+ return { group, parent }
316
+ }
317
+ for (const subGroup of group.groups) {
318
+ const found = findGroupById(subGroup, groupId, group)
319
+ if (found) return found
320
+ }
321
+ return null
322
+ }
323
+
324
+ /**
325
+ * Clone profondément un filtre
326
+ */
327
+ function deepCloneFilter<TData>(filter: FilterGroup<TData>): FilterGroup<TData> {
328
+ return JSON.parse(JSON.stringify(filter))
329
+ }
330
+
331
+ /**
332
+ * Hook pour gérer les filtres avancés du DataTable
333
+ */
334
+ export function useDataTableAdvancedFilters<TData extends Record<string, unknown>>({
335
+ data,
336
+ config,
337
+ }: {
338
+ data: TData[]
339
+ config: AdvancedFiltersConfig<TData>
340
+ }): AdvancedFiltersResult<TData> {
341
+ // État initial
342
+ const createEmptyFilter = useCallback((): FilterGroup<TData> => ({
343
+ id: generateId(),
344
+ connector: "AND",
345
+ rules: [],
346
+ groups: [],
347
+ }), [])
348
+
349
+ const [state, setState] = useState<AdvancedFiltersState<TData>>(() => ({
350
+ currentFilter: createEmptyFilter(),
351
+ presets: config.presets || [],
352
+ activePresetId: null,
353
+ isEditing: false,
354
+ isDirty: false,
355
+ }))
356
+
357
+ // Charger les presets depuis localStorage
358
+ useEffect(() => {
359
+ if (!config.persistPresets || typeof window === "undefined") return
360
+
361
+ const storageKey = config.storageKey || "datatable-filter-presets"
362
+ try {
363
+ const stored = localStorage.getItem(storageKey)
364
+ if (stored) {
365
+ const presets = JSON.parse(stored) as FilterPreset<TData>[]
366
+ setState((s) => ({
367
+ ...s,
368
+ presets: [...(config.presets || []), ...presets.filter(
369
+ (p) => !config.presets?.some((cp) => cp.id === p.id)
370
+ )],
371
+ }))
372
+ }
373
+ } catch {
374
+ // Ignore errors
375
+ }
376
+ }, [config.persistPresets, config.storageKey, config.presets])
377
+
378
+ // Sauvegarder les presets dans localStorage
379
+ const savePresetsToStorage = useCallback(
380
+ (presets: FilterPreset<TData>[]) => {
381
+ if (!config.persistPresets || typeof window === "undefined") return
382
+ const storageKey = config.storageKey || "datatable-filter-presets"
383
+ try {
384
+ // Ne sauvegarder que les presets créés par l'utilisateur
385
+ const userPresets = presets.filter(
386
+ (p) => !config.presets?.some((cp) => cp.id === p.id)
387
+ )
388
+ localStorage.setItem(storageKey, JSON.stringify(userPresets))
389
+ } catch {
390
+ // Ignore errors
391
+ }
392
+ },
393
+ [config.persistPresets, config.storageKey, config.presets]
394
+ )
395
+
396
+ // Opérateurs disponibles pour une colonne
397
+ const getOperatorsForColumn = useCallback(
398
+ (columnKey: keyof TData | string): FilterOperator[] => {
399
+ const column = config.columns.find((c) => c.key === columnKey)
400
+ if (!column) return OPERATORS_BY_TYPE.string
401
+ return column.operators || OPERATORS_BY_TYPE[column.type] || OPERATORS_BY_TYPE.string
402
+ },
403
+ [config.columns]
404
+ )
405
+
406
+ // Validation d'un filtre
407
+ const validateFilter = useCallback(
408
+ (filter: FilterGroup<TData>): { valid: boolean; errors: string[] } => {
409
+ const errors: string[] = []
410
+
411
+ const validateGroup = (group: FilterGroup<TData>, depth = 0) => {
412
+ // Vérifier la profondeur max
413
+ if (config.maxNestingDepth !== undefined && depth > config.maxNestingDepth) {
414
+ errors.push(`Profondeur maximale d'imbrication dépassée (${config.maxNestingDepth})`)
415
+ }
416
+
417
+ // Vérifier le nombre de règles
418
+ if (config.maxRulesPerGroup !== undefined && group.rules.length > config.maxRulesPerGroup) {
419
+ errors.push(`Nombre maximum de règles par groupe dépassé (${config.maxRulesPerGroup})`)
420
+ }
421
+
422
+ // Valider les règles
423
+ for (const rule of group.rules) {
424
+ const column = config.columns.find((c) => c.key === rule.column)
425
+ if (!column) {
426
+ errors.push(`Colonne inconnue: ${String(rule.column)}`)
427
+ }
428
+
429
+ const allowedOperators = getOperatorsForColumn(rule.column)
430
+ if (!allowedOperators.includes(rule.operator)) {
431
+ errors.push(`Opérateur invalide pour ${String(rule.column)}: ${rule.operator}`)
432
+ }
433
+ }
434
+
435
+ // Récursif pour les sous-groupes
436
+ for (const subGroup of group.groups) {
437
+ validateGroup(subGroup, depth + 1)
438
+ }
439
+ }
440
+
441
+ validateGroup(filter)
442
+ return { valid: errors.length === 0, errors }
443
+ },
444
+ [config.columns, config.maxNestingDepth, config.maxRulesPerGroup, getOperatorsForColumn]
445
+ )
446
+
447
+ // Actions
448
+ const actions: AdvancedFiltersActions<TData> = useMemo(() => ({
449
+ addRule: (groupId, rule) => {
450
+ setState((s) => {
451
+ const newFilter = deepCloneFilter(s.currentFilter)
452
+ const found = findGroupById(newFilter, groupId)
453
+ if (found) {
454
+ found.group.rules.push({
455
+ ...rule,
456
+ id: generateId(),
457
+ enabled: true,
458
+ } as FilterRule<TData>)
459
+ }
460
+ return { ...s, currentFilter: newFilter, isDirty: true }
461
+ })
462
+ },
463
+
464
+ updateRule: (ruleId, updates) => {
465
+ setState((s) => {
466
+ const newFilter = deepCloneFilter(s.currentFilter)
467
+ const found = findRuleById(newFilter, ruleId)
468
+ if (found) {
469
+ Object.assign(found.rule, updates)
470
+ }
471
+ return { ...s, currentFilter: newFilter, isDirty: true }
472
+ })
473
+ },
474
+
475
+ removeRule: (ruleId) => {
476
+ setState((s) => {
477
+ const newFilter = deepCloneFilter(s.currentFilter)
478
+ const found = findRuleById(newFilter, ruleId)
479
+ if (found) {
480
+ found.parent.rules = found.parent.rules.filter((r) => r.id !== ruleId)
481
+ }
482
+ return { ...s, currentFilter: newFilter, isDirty: true }
483
+ })
484
+ },
485
+
486
+ addGroup: (parentGroupId, connector = "AND") => {
487
+ setState((s) => {
488
+ const newFilter = deepCloneFilter(s.currentFilter)
489
+ const found = findGroupById(newFilter, parentGroupId)
490
+ if (found) {
491
+ found.group.groups.push({
492
+ id: generateId(),
493
+ connector,
494
+ rules: [],
495
+ groups: [],
496
+ })
497
+ }
498
+ return { ...s, currentFilter: newFilter, isDirty: true }
499
+ })
500
+ },
501
+
502
+ removeGroup: (groupId) => {
503
+ setState((s) => {
504
+ const newFilter = deepCloneFilter(s.currentFilter)
505
+ const found = findGroupById(newFilter, groupId)
506
+ if (found && found.parent) {
507
+ found.parent.groups = found.parent.groups.filter((g) => g.id !== groupId)
508
+ }
509
+ return { ...s, currentFilter: newFilter, isDirty: true }
510
+ })
511
+ },
512
+
513
+ setGroupConnector: (groupId, connector) => {
514
+ setState((s) => {
515
+ const newFilter = deepCloneFilter(s.currentFilter)
516
+ const found = findGroupById(newFilter, groupId)
517
+ if (found) {
518
+ found.group.connector = connector
519
+ }
520
+ return { ...s, currentFilter: newFilter, isDirty: true }
521
+ })
522
+ },
523
+
524
+ resetFilters: () => {
525
+ setState((s) => ({
526
+ ...s,
527
+ currentFilter: createEmptyFilter(),
528
+ activePresetId: null,
529
+ isDirty: false,
530
+ }))
531
+ },
532
+
533
+ applyFilters: () => {
534
+ setState((s) => {
535
+ config.onFilterChange?.(s.currentFilter)
536
+ return { ...s, isEditing: false }
537
+ })
538
+ },
539
+
540
+ saveAsPreset: (name, description) => {
541
+ setState((s) => {
542
+ const preset: FilterPreset<TData> = {
543
+ id: generateId(),
544
+ name,
545
+ description,
546
+ filter: deepCloneFilter(s.currentFilter),
547
+ createdAt: new Date(),
548
+ }
549
+ const newPresets = [...s.presets, preset]
550
+ savePresetsToStorage(newPresets)
551
+ config.onPresetSave?.(preset)
552
+ return {
553
+ ...s,
554
+ presets: newPresets,
555
+ activePresetId: preset.id,
556
+ isDirty: false,
557
+ }
558
+ })
559
+ },
560
+
561
+ applyPreset: (presetId) => {
562
+ setState((s) => {
563
+ const preset = s.presets.find((p) => p.id === presetId)
564
+ if (!preset) return s
565
+ const newFilter = deepCloneFilter(preset.filter)
566
+ config.onFilterChange?.(newFilter)
567
+ return {
568
+ ...s,
569
+ currentFilter: newFilter,
570
+ activePresetId: presetId,
571
+ isDirty: false,
572
+ }
573
+ })
574
+ },
575
+
576
+ deletePreset: (presetId) => {
577
+ setState((s) => {
578
+ const newPresets = s.presets.filter((p) => p.id !== presetId)
579
+ savePresetsToStorage(newPresets)
580
+ config.onPresetDelete?.(presetId)
581
+ return {
582
+ ...s,
583
+ presets: newPresets,
584
+ activePresetId: s.activePresetId === presetId ? null : s.activePresetId,
585
+ }
586
+ })
587
+ },
588
+
589
+ importFilters: (json) => {
590
+ try {
591
+ const filter = JSON.parse(json) as FilterGroup<TData>
592
+ const validation = validateFilter(filter)
593
+ if (!validation.valid) {
594
+ console.error("Invalid filter:", validation.errors)
595
+ return
596
+ }
597
+ setState((s) => ({
598
+ ...s,
599
+ currentFilter: filter,
600
+ activePresetId: null,
601
+ isDirty: true,
602
+ }))
603
+ } catch (e) {
604
+ console.error("Failed to import filters:", e)
605
+ }
606
+ },
607
+
608
+ exportFilters: () => {
609
+ return JSON.stringify(state.currentFilter, null, 2)
610
+ },
611
+ }), [config, createEmptyFilter, savePresetsToStorage, validateFilter, state.currentFilter])
612
+
613
+ // Données filtrées
614
+ const filteredData = useMemo(() => {
615
+ if (!config.enabled) return data
616
+
617
+ // Si le filtre est vide, retourner toutes les données
618
+ if (state.currentFilter.rules.length === 0 && state.currentFilter.groups.length === 0) {
619
+ return data
620
+ }
621
+
622
+ return data.filter((row) => evaluateGroup(row, state.currentFilter, config.columns))
623
+ }, [data, config.enabled, config.columns, state.currentFilter])
624
+
625
+ return {
626
+ state,
627
+ actions,
628
+ filteredData,
629
+ filteredCount: filteredData.length,
630
+ getOperatorsForColumn,
631
+ createEmptyFilter,
632
+ validateFilter,
633
+ }
634
+ }
635
+
636
+ export { OPERATORS_BY_TYPE }
637
+ export default useDataTableAdvancedFilters