@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,1037 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { cn } from "../../utils"
5
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../../components/card"
6
+ import { Button } from "../../components/button"
7
+ import { Input } from "../../components/input"
8
+ import { Label } from "../../components/label"
9
+ import { Badge } from "../../components/badge"
10
+ import { Avatar, AvatarFallback, AvatarImage } from "../../components/avatar"
11
+ import { Checkbox } from "../../components/checkbox"
12
+ import {
13
+ Select,
14
+ SelectContent,
15
+ SelectItem,
16
+ SelectTrigger,
17
+ SelectValue,
18
+ } from "../../components/select"
19
+ import {
20
+ Dialog,
21
+ DialogContent,
22
+ DialogDescription,
23
+ DialogFooter,
24
+ DialogHeader,
25
+ DialogTitle,
26
+ } from "../../components/dialog"
27
+ import {
28
+ AlertDialog,
29
+ AlertDialogAction,
30
+ AlertDialogCancel,
31
+ AlertDialogContent,
32
+ AlertDialogDescription,
33
+ AlertDialogFooter,
34
+ AlertDialogHeader,
35
+ AlertDialogTitle,
36
+ } from "../../components/alert-dialog"
37
+ import {
38
+ DropdownMenu,
39
+ DropdownMenuContent,
40
+ DropdownMenuItem,
41
+ DropdownMenuLabel,
42
+ DropdownMenuSeparator,
43
+ DropdownMenuTrigger,
44
+ } from "../../components/dropdown-menu"
45
+ import {
46
+ Table,
47
+ TableBody,
48
+ TableCell,
49
+ TableHead,
50
+ TableHeader,
51
+ TableRow,
52
+ } from "../../components/table"
53
+ import {
54
+ Users,
55
+ UserPlus,
56
+ Search,
57
+ MoreHorizontal,
58
+ Edit,
59
+ Trash2,
60
+ Shield,
61
+ ShieldCheck,
62
+ ShieldAlert,
63
+ Mail,
64
+ Ban,
65
+ CheckCircle,
66
+ XCircle,
67
+ Key,
68
+ Loader2,
69
+ Filter,
70
+ Download,
71
+ RefreshCw,
72
+ } from "lucide-react"
73
+
74
+ // ============================================
75
+ // TYPES
76
+ // ============================================
77
+
78
+ export type UserRole = "admin" | "moderator" | "user" | "guest"
79
+ export type UserStatus = "active" | "inactive" | "pending" | "banned"
80
+
81
+ export interface UserPermission {
82
+ id: string
83
+ name: string
84
+ description?: string
85
+ category?: string
86
+ }
87
+
88
+ export interface UserData {
89
+ id: string
90
+ email: string
91
+ displayName: string
92
+ firstName?: string
93
+ lastName?: string
94
+ avatarUrl?: string
95
+ role: UserRole
96
+ status: UserStatus
97
+ permissions?: string[]
98
+ createdAt: Date | string
99
+ lastLogin?: Date | string
100
+ metadata?: Record<string, unknown>
101
+ }
102
+
103
+ export interface UserFormData {
104
+ email: string
105
+ displayName: string
106
+ firstName?: string
107
+ lastName?: string
108
+ role: UserRole
109
+ status: UserStatus
110
+ permissions?: string[]
111
+ }
112
+
113
+ export interface WakaUserManagementProps {
114
+ /** Liste des utilisateurs */
115
+ users: UserData[]
116
+ /** Rôles disponibles */
117
+ roles?: { value: UserRole; label: string; color?: string }[]
118
+ /** Permissions disponibles */
119
+ permissions?: UserPermission[]
120
+ /** Utilisateurs sélectionnés */
121
+ selectedUsers?: string[]
122
+ /** Callback sélection */
123
+ onSelectionChange?: (userIds: string[]) => void
124
+ /** Callback création */
125
+ onCreate?: (user: UserFormData) => void | Promise<void>
126
+ /** Callback modification */
127
+ onEdit?: (userId: string, data: Partial<UserFormData>) => void | Promise<void>
128
+ /** Callback suppression */
129
+ onDelete?: (userId: string) => void | Promise<void>
130
+ /** Callback suppression multiple */
131
+ onBulkDelete?: (userIds: string[]) => void | Promise<void>
132
+ /** Callback changement de rôle */
133
+ onRoleChange?: (userId: string, role: UserRole) => void | Promise<void>
134
+ /** Callback changement de statut */
135
+ onStatusChange?: (userId: string, status: UserStatus) => void | Promise<void>
136
+ /** Callback réinitialisation mot de passe */
137
+ onResetPassword?: (userId: string) => void | Promise<void>
138
+ /** Callback export */
139
+ onExport?: () => void
140
+ /** Callback rafraîchissement */
141
+ onRefresh?: () => void
142
+ /** En cours de chargement */
143
+ loading?: boolean
144
+ /** En cours d'action */
145
+ actionLoading?: boolean
146
+ /** Recherche */
147
+ searchQuery?: string
148
+ /** Callback recherche */
149
+ onSearchChange?: (query: string) => void
150
+ /** Filtre par rôle */
151
+ roleFilter?: UserRole | "all"
152
+ /** Callback filtre rôle */
153
+ onRoleFilterChange?: (role: UserRole | "all") => void
154
+ /** Filtre par statut */
155
+ statusFilter?: UserStatus | "all"
156
+ /** Callback filtre statut */
157
+ onStatusFilterChange?: (status: UserStatus | "all") => void
158
+ /** Afficher les actions en masse */
159
+ showBulkActions?: boolean
160
+ /** Afficher le bouton création */
161
+ showCreateButton?: boolean
162
+ /** Titre */
163
+ title?: string
164
+ /** Description */
165
+ description?: string
166
+ /** Classes CSS additionnelles */
167
+ className?: string
168
+ }
169
+
170
+ // ============================================
171
+ // CONSTANTS
172
+ // ============================================
173
+
174
+ const defaultRoles: { value: UserRole; label: string; color: string }[] = [
175
+ { value: "admin", label: "Administrateur", color: "destructive" },
176
+ { value: "moderator", label: "Modérateur", color: "warning" },
177
+ { value: "user", label: "Utilisateur", color: "default" },
178
+ { value: "guest", label: "Invité", color: "secondary" },
179
+ ]
180
+
181
+ const statusConfig: Record<UserStatus, { label: string; color: string; icon: React.ElementType }> = {
182
+ active: { label: "Actif", color: "bg-green-500", icon: CheckCircle },
183
+ inactive: { label: "Inactif", color: "bg-gray-500", icon: XCircle },
184
+ pending: { label: "En attente", color: "bg-yellow-500", icon: Mail },
185
+ banned: { label: "Banni", color: "bg-red-500", icon: Ban },
186
+ }
187
+
188
+ // ============================================
189
+ // SUB-COMPONENTS
190
+ // ============================================
191
+
192
+ interface UserAvatarProps {
193
+ user: UserData
194
+ size?: "sm" | "md"
195
+ }
196
+
197
+ function UserAvatar({ user, size = "sm" }: UserAvatarProps) {
198
+ const sizeClasses = {
199
+ sm: "h-8 w-8",
200
+ md: "h-10 w-10",
201
+ }
202
+
203
+ const initials = user.displayName
204
+ .split(" ")
205
+ .map((n) => n[0])
206
+ .join("")
207
+ .toUpperCase()
208
+ .slice(0, 2)
209
+
210
+ return (
211
+ <Avatar className={sizeClasses[size]}>
212
+ <AvatarImage src={user.avatarUrl} alt={user.displayName} />
213
+ <AvatarFallback className="text-xs">{initials}</AvatarFallback>
214
+ </Avatar>
215
+ )
216
+ }
217
+
218
+ interface RoleBadgeProps {
219
+ role: UserRole
220
+ roles?: { value: UserRole; label: string; color?: string }[]
221
+ }
222
+
223
+ function RoleBadge({ role, roles = defaultRoles }: RoleBadgeProps) {
224
+ const roleConfig = roles.find((r) => r.value === role)
225
+ const Icon = role === "admin" ? ShieldAlert : role === "moderator" ? ShieldCheck : Shield
226
+
227
+ return (
228
+ <Badge variant={roleConfig?.color as "default" | "secondary" | "destructive" | "outline"}>
229
+ <Icon className="h-3 w-3 mr-1" />
230
+ {roleConfig?.label || role}
231
+ </Badge>
232
+ )
233
+ }
234
+
235
+ interface StatusIndicatorProps {
236
+ status: UserStatus
237
+ }
238
+
239
+ function StatusIndicator({ status }: StatusIndicatorProps) {
240
+ const config = statusConfig[status]
241
+
242
+ return (
243
+ <div className="flex items-center gap-2">
244
+ <div className={cn("h-2 w-2 rounded-full", config.color)} />
245
+ <span className="text-sm">{config.label}</span>
246
+ </div>
247
+ )
248
+ }
249
+
250
+ interface UserFormDialogProps {
251
+ open: boolean
252
+ onOpenChange: (open: boolean) => void
253
+ user?: UserData
254
+ roles: { value: UserRole; label: string; color?: string }[]
255
+ permissions?: UserPermission[]
256
+ onSubmit: (data: UserFormData) => void | Promise<void>
257
+ loading?: boolean
258
+ }
259
+
260
+ function UserFormDialog({
261
+ open,
262
+ onOpenChange,
263
+ user,
264
+ roles,
265
+ permissions,
266
+ onSubmit,
267
+ loading,
268
+ }: UserFormDialogProps) {
269
+ const [formData, setFormData] = React.useState<UserFormData>({
270
+ email: "",
271
+ displayName: "",
272
+ firstName: "",
273
+ lastName: "",
274
+ role: "user",
275
+ status: "pending",
276
+ permissions: [],
277
+ })
278
+
279
+ React.useEffect(() => {
280
+ if (user) {
281
+ setFormData({
282
+ email: user.email,
283
+ displayName: user.displayName,
284
+ firstName: user.firstName || "",
285
+ lastName: user.lastName || "",
286
+ role: user.role,
287
+ status: user.status,
288
+ permissions: user.permissions || [],
289
+ })
290
+ } else {
291
+ setFormData({
292
+ email: "",
293
+ displayName: "",
294
+ firstName: "",
295
+ lastName: "",
296
+ role: "user",
297
+ status: "pending",
298
+ permissions: [],
299
+ })
300
+ }
301
+ }, [user, open])
302
+
303
+ const handleSubmit = async (e: React.FormEvent) => {
304
+ e.preventDefault()
305
+ await onSubmit(formData)
306
+ onOpenChange(false)
307
+ }
308
+
309
+ const handlePermissionToggle = (permissionId: string) => {
310
+ setFormData((prev) => ({
311
+ ...prev,
312
+ permissions: prev.permissions?.includes(permissionId)
313
+ ? prev.permissions.filter((p) => p !== permissionId)
314
+ : [...(prev.permissions || []), permissionId],
315
+ }))
316
+ }
317
+
318
+ return (
319
+ <Dialog open={open} onOpenChange={onOpenChange}>
320
+ <DialogContent className="sm:max-w-[500px]">
321
+ <form onSubmit={handleSubmit}>
322
+ <DialogHeader>
323
+ <DialogTitle>
324
+ {user ? "Modifier l'utilisateur" : "Créer un utilisateur"}
325
+ </DialogTitle>
326
+ <DialogDescription>
327
+ {user
328
+ ? "Modifiez les informations de l'utilisateur."
329
+ : "Remplissez les informations pour créer un nouvel utilisateur."}
330
+ </DialogDescription>
331
+ </DialogHeader>
332
+
333
+ <div className="space-y-4 py-4">
334
+ <div className="grid grid-cols-2 gap-4">
335
+ <div className="space-y-2">
336
+ <Label htmlFor="firstName">Prénom</Label>
337
+ <Input
338
+ id="firstName"
339
+ value={formData.firstName}
340
+ onChange={(e) =>
341
+ setFormData((prev) => ({ ...prev, firstName: e.target.value }))
342
+ }
343
+ placeholder="Jean"
344
+ />
345
+ </div>
346
+ <div className="space-y-2">
347
+ <Label htmlFor="lastName">Nom</Label>
348
+ <Input
349
+ id="lastName"
350
+ value={formData.lastName}
351
+ onChange={(e) =>
352
+ setFormData((prev) => ({ ...prev, lastName: e.target.value }))
353
+ }
354
+ placeholder="Dupont"
355
+ />
356
+ </div>
357
+ </div>
358
+
359
+ <div className="space-y-2">
360
+ <Label htmlFor="displayName">Nom d'affichage *</Label>
361
+ <Input
362
+ id="displayName"
363
+ value={formData.displayName}
364
+ onChange={(e) =>
365
+ setFormData((prev) => ({ ...prev, displayName: e.target.value }))
366
+ }
367
+ placeholder="Jean Dupont"
368
+ required
369
+ />
370
+ </div>
371
+
372
+ <div className="space-y-2">
373
+ <Label htmlFor="email">Email *</Label>
374
+ <Input
375
+ id="email"
376
+ type="email"
377
+ value={formData.email}
378
+ onChange={(e) =>
379
+ setFormData((prev) => ({ ...prev, email: e.target.value }))
380
+ }
381
+ placeholder="jean@exemple.com"
382
+ required
383
+ />
384
+ </div>
385
+
386
+ <div className="grid grid-cols-2 gap-4">
387
+ <div className="space-y-2">
388
+ <Label htmlFor="role">Rôle</Label>
389
+ <Select
390
+ value={formData.role}
391
+ onValueChange={(value) =>
392
+ setFormData((prev) => ({ ...prev, role: value as UserRole }))
393
+ }
394
+ >
395
+ <SelectTrigger>
396
+ <SelectValue />
397
+ </SelectTrigger>
398
+ <SelectContent>
399
+ {roles.map((role) => (
400
+ <SelectItem key={role.value} value={role.value}>
401
+ {role.label}
402
+ </SelectItem>
403
+ ))}
404
+ </SelectContent>
405
+ </Select>
406
+ </div>
407
+ <div className="space-y-2">
408
+ <Label htmlFor="status">Statut</Label>
409
+ <Select
410
+ value={formData.status}
411
+ onValueChange={(value) =>
412
+ setFormData((prev) => ({ ...prev, status: value as UserStatus }))
413
+ }
414
+ >
415
+ <SelectTrigger>
416
+ <SelectValue />
417
+ </SelectTrigger>
418
+ <SelectContent>
419
+ {Object.entries(statusConfig).map(([value, config]) => (
420
+ <SelectItem key={value} value={value}>
421
+ {config.label}
422
+ </SelectItem>
423
+ ))}
424
+ </SelectContent>
425
+ </Select>
426
+ </div>
427
+ </div>
428
+
429
+ {permissions && permissions.length > 0 && (
430
+ <div className="space-y-2">
431
+ <Label>Permissions</Label>
432
+ <div className="border rounded-md p-3 space-y-2 max-h-[150px] overflow-y-auto">
433
+ {permissions.map((permission) => (
434
+ <div key={permission.id} className="flex items-center space-x-2">
435
+ <Checkbox
436
+ id={permission.id}
437
+ checked={formData.permissions?.includes(permission.id)}
438
+ onCheckedChange={() => handlePermissionToggle(permission.id)}
439
+ />
440
+ <label
441
+ htmlFor={permission.id}
442
+ className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
443
+ >
444
+ {permission.name}
445
+ </label>
446
+ </div>
447
+ ))}
448
+ </div>
449
+ </div>
450
+ )}
451
+ </div>
452
+
453
+ <DialogFooter>
454
+ <Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
455
+ Annuler
456
+ </Button>
457
+ <Button type="submit" disabled={loading}>
458
+ {loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
459
+ {user ? "Enregistrer" : "Créer"}
460
+ </Button>
461
+ </DialogFooter>
462
+ </form>
463
+ </DialogContent>
464
+ </Dialog>
465
+ )
466
+ }
467
+
468
+ interface UserActionsMenuProps {
469
+ user: UserData
470
+ onEdit?: () => void
471
+ onDelete?: () => void
472
+ onResetPassword?: () => void
473
+ onStatusChange?: (status: UserStatus) => void
474
+ onRoleChange?: (role: UserRole) => void
475
+ roles: { value: UserRole; label: string }[]
476
+ }
477
+
478
+ function UserActionsMenu({
479
+ user,
480
+ onEdit,
481
+ onDelete,
482
+ onResetPassword,
483
+ onStatusChange,
484
+ onRoleChange,
485
+ roles,
486
+ }: UserActionsMenuProps) {
487
+ return (
488
+ <DropdownMenu>
489
+ <DropdownMenuTrigger asChild>
490
+ <Button variant="ghost" size="icon" className="h-8 w-8">
491
+ <MoreHorizontal className="h-4 w-4" />
492
+ </Button>
493
+ </DropdownMenuTrigger>
494
+ <DropdownMenuContent align="end">
495
+ {onEdit && (
496
+ <DropdownMenuItem onClick={onEdit}>
497
+ <Edit className="mr-2 h-4 w-4" />
498
+ Modifier
499
+ </DropdownMenuItem>
500
+ )}
501
+ {onResetPassword && (
502
+ <DropdownMenuItem onClick={onResetPassword}>
503
+ <Key className="mr-2 h-4 w-4" />
504
+ Réinitialiser le mot de passe
505
+ </DropdownMenuItem>
506
+ )}
507
+
508
+ {onRoleChange && (
509
+ <>
510
+ <DropdownMenuSeparator />
511
+ <DropdownMenuLabel>Changer le rôle</DropdownMenuLabel>
512
+ {roles.map((role) => (
513
+ <DropdownMenuItem
514
+ key={role.value}
515
+ onClick={() => onRoleChange(role.value)}
516
+ disabled={user.role === role.value}
517
+ >
518
+ {role.label}
519
+ </DropdownMenuItem>
520
+ ))}
521
+ </>
522
+ )}
523
+
524
+ {onStatusChange && (
525
+ <>
526
+ <DropdownMenuSeparator />
527
+ <DropdownMenuLabel>Changer le statut</DropdownMenuLabel>
528
+ {Object.entries(statusConfig).map(([value, config]) => (
529
+ <DropdownMenuItem
530
+ key={value}
531
+ onClick={() => onStatusChange(value as UserStatus)}
532
+ disabled={user.status === value}
533
+ >
534
+ {config.label}
535
+ </DropdownMenuItem>
536
+ ))}
537
+ </>
538
+ )}
539
+
540
+ {onDelete && (
541
+ <>
542
+ <DropdownMenuSeparator />
543
+ <DropdownMenuItem
544
+ onClick={onDelete}
545
+ className="text-destructive focus:text-destructive"
546
+ >
547
+ <Trash2 className="mr-2 h-4 w-4" />
548
+ Supprimer
549
+ </DropdownMenuItem>
550
+ </>
551
+ )}
552
+ </DropdownMenuContent>
553
+ </DropdownMenu>
554
+ )
555
+ }
556
+
557
+ // ============================================
558
+ // MAIN COMPONENT
559
+ // ============================================
560
+
561
+ export function WakaUserManagement({
562
+ users,
563
+ roles = defaultRoles,
564
+ permissions,
565
+ selectedUsers = [],
566
+ onSelectionChange,
567
+ onCreate,
568
+ onEdit,
569
+ onDelete,
570
+ onBulkDelete,
571
+ onRoleChange,
572
+ onStatusChange,
573
+ onResetPassword,
574
+ onExport,
575
+ onRefresh,
576
+ loading = false,
577
+ actionLoading = false,
578
+ searchQuery = "",
579
+ onSearchChange,
580
+ roleFilter = "all",
581
+ onRoleFilterChange,
582
+ statusFilter = "all",
583
+ onStatusFilterChange,
584
+ showBulkActions = true,
585
+ showCreateButton = true,
586
+ title = "Gestion des utilisateurs",
587
+ description = "Gérez les utilisateurs, leurs rôles et permissions.",
588
+ className,
589
+ }: WakaUserManagementProps) {
590
+ const [createDialogOpen, setCreateDialogOpen] = React.useState(false)
591
+ const [editingUser, setEditingUser] = React.useState<UserData | null>(null)
592
+ const [deletingUser, setDeletingUser] = React.useState<UserData | null>(null)
593
+ const [bulkDeleteOpen, setBulkDeleteOpen] = React.useState(false)
594
+
595
+ const filteredUsers = React.useMemo(() => {
596
+ return users.filter((user) => {
597
+ // Search filter
598
+ if (searchQuery) {
599
+ const query = searchQuery.toLowerCase()
600
+ const matchesSearch =
601
+ user.displayName.toLowerCase().includes(query) ||
602
+ user.email.toLowerCase().includes(query)
603
+ if (!matchesSearch) return false
604
+ }
605
+
606
+ // Role filter
607
+ if (roleFilter !== "all" && user.role !== roleFilter) {
608
+ return false
609
+ }
610
+
611
+ // Status filter
612
+ if (statusFilter !== "all" && user.status !== statusFilter) {
613
+ return false
614
+ }
615
+
616
+ return true
617
+ })
618
+ }, [users, searchQuery, roleFilter, statusFilter])
619
+
620
+ const handleSelectAll = (checked: boolean) => {
621
+ if (checked) {
622
+ onSelectionChange?.(filteredUsers.map((u) => u.id))
623
+ } else {
624
+ onSelectionChange?.([])
625
+ }
626
+ }
627
+
628
+ const handleSelectUser = (userId: string, checked: boolean) => {
629
+ if (checked) {
630
+ onSelectionChange?.([...selectedUsers, userId])
631
+ } else {
632
+ onSelectionChange?.(selectedUsers.filter((id) => id !== userId))
633
+ }
634
+ }
635
+
636
+ const handleCreate = async (data: UserFormData) => {
637
+ await onCreate?.(data)
638
+ }
639
+
640
+ const handleEdit = async (data: UserFormData) => {
641
+ if (editingUser) {
642
+ await onEdit?.(editingUser.id, data)
643
+ setEditingUser(null)
644
+ }
645
+ }
646
+
647
+ const handleDelete = async () => {
648
+ if (deletingUser) {
649
+ await onDelete?.(deletingUser.id)
650
+ setDeletingUser(null)
651
+ }
652
+ }
653
+
654
+ const handleBulkDelete = async () => {
655
+ await onBulkDelete?.(selectedUsers)
656
+ onSelectionChange?.([])
657
+ setBulkDeleteOpen(false)
658
+ }
659
+
660
+ const formatDate = (date: Date | string): string => {
661
+ const d = typeof date === "string" ? new Date(date) : date
662
+ return d.toLocaleDateString("fr-FR", {
663
+ day: "numeric",
664
+ month: "short",
665
+ year: "numeric",
666
+ })
667
+ }
668
+
669
+ return (
670
+ <div className={cn("space-y-6", className)}>
671
+ {/* Header */}
672
+ <div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
673
+ <div>
674
+ <h1 className="text-2xl font-bold tracking-tight flex items-center gap-2">
675
+ <Users className="h-6 w-6" />
676
+ {title}
677
+ </h1>
678
+ {description && (
679
+ <p className="text-muted-foreground mt-1">{description}</p>
680
+ )}
681
+ </div>
682
+ <div className="flex items-center gap-2">
683
+ {onRefresh && (
684
+ <Button variant="outline" size="sm" onClick={onRefresh} disabled={loading}>
685
+ <RefreshCw className={cn("h-4 w-4", loading && "animate-spin")} />
686
+ </Button>
687
+ )}
688
+ {onExport && (
689
+ <Button variant="outline" size="sm" onClick={onExport}>
690
+ <Download className="mr-2 h-4 w-4" />
691
+ Exporter
692
+ </Button>
693
+ )}
694
+ {showCreateButton && onCreate && (
695
+ <Button size="sm" onClick={() => setCreateDialogOpen(true)}>
696
+ <UserPlus className="mr-2 h-4 w-4" />
697
+ Ajouter
698
+ </Button>
699
+ )}
700
+ </div>
701
+ </div>
702
+
703
+ {/* Filters */}
704
+ <Card>
705
+ <CardContent className="pt-6">
706
+ <div className="flex flex-col gap-4 md:flex-row md:items-center">
707
+ {/* Search */}
708
+ {onSearchChange && (
709
+ <div className="relative flex-1">
710
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
711
+ <Input
712
+ placeholder="Rechercher un utilisateur..."
713
+ value={searchQuery}
714
+ onChange={(e) => onSearchChange(e.target.value)}
715
+ className="pl-9"
716
+ />
717
+ </div>
718
+ )}
719
+
720
+ {/* Role filter */}
721
+ {onRoleFilterChange && (
722
+ <Select
723
+ value={roleFilter}
724
+ onValueChange={(value) => onRoleFilterChange(value as UserRole | "all")}
725
+ >
726
+ <SelectTrigger className="w-[180px]">
727
+ <Filter className="mr-2 h-4 w-4" />
728
+ <SelectValue placeholder="Rôle" />
729
+ </SelectTrigger>
730
+ <SelectContent>
731
+ <SelectItem value="all">Tous les rôles</SelectItem>
732
+ {roles.map((role) => (
733
+ <SelectItem key={role.value} value={role.value}>
734
+ {role.label}
735
+ </SelectItem>
736
+ ))}
737
+ </SelectContent>
738
+ </Select>
739
+ )}
740
+
741
+ {/* Status filter */}
742
+ {onStatusFilterChange && (
743
+ <Select
744
+ value={statusFilter}
745
+ onValueChange={(value) => onStatusFilterChange(value as UserStatus | "all")}
746
+ >
747
+ <SelectTrigger className="w-[180px]">
748
+ <Filter className="mr-2 h-4 w-4" />
749
+ <SelectValue placeholder="Statut" />
750
+ </SelectTrigger>
751
+ <SelectContent>
752
+ <SelectItem value="all">Tous les statuts</SelectItem>
753
+ {Object.entries(statusConfig).map(([value, config]) => (
754
+ <SelectItem key={value} value={value}>
755
+ {config.label}
756
+ </SelectItem>
757
+ ))}
758
+ </SelectContent>
759
+ </Select>
760
+ )}
761
+ </div>
762
+
763
+ {/* Bulk actions */}
764
+ {showBulkActions && selectedUsers.length > 0 && (
765
+ <div className="flex items-center gap-4 mt-4 p-3 bg-muted rounded-lg">
766
+ <span className="text-sm font-medium">
767
+ {selectedUsers.length} utilisateur(s) sélectionné(s)
768
+ </span>
769
+ {onBulkDelete && (
770
+ <Button
771
+ variant="destructive"
772
+ size="sm"
773
+ onClick={() => setBulkDeleteOpen(true)}
774
+ >
775
+ <Trash2 className="mr-2 h-4 w-4" />
776
+ Supprimer
777
+ </Button>
778
+ )}
779
+ <Button
780
+ variant="outline"
781
+ size="sm"
782
+ onClick={() => onSelectionChange?.([])}
783
+ >
784
+ Désélectionner
785
+ </Button>
786
+ </div>
787
+ )}
788
+ </CardContent>
789
+ </Card>
790
+
791
+ {/* Users table */}
792
+ <Card>
793
+ <CardContent className="p-0">
794
+ <Table>
795
+ <TableHeader>
796
+ <TableRow>
797
+ {onSelectionChange && (
798
+ <TableHead className="w-[50px]">
799
+ <Checkbox
800
+ checked={
801
+ filteredUsers.length > 0 &&
802
+ selectedUsers.length === filteredUsers.length
803
+ }
804
+ onCheckedChange={handleSelectAll}
805
+ />
806
+ </TableHead>
807
+ )}
808
+ <TableHead>Utilisateur</TableHead>
809
+ <TableHead>Rôle</TableHead>
810
+ <TableHead>Statut</TableHead>
811
+ <TableHead>Inscrit le</TableHead>
812
+ <TableHead>Dernière connexion</TableHead>
813
+ <TableHead className="w-[50px]"></TableHead>
814
+ </TableRow>
815
+ </TableHeader>
816
+ <TableBody>
817
+ {loading ? (
818
+ <TableRow>
819
+ <TableCell
820
+ colSpan={onSelectionChange ? 7 : 6}
821
+ className="text-center py-8"
822
+ >
823
+ <Loader2 className="h-6 w-6 animate-spin mx-auto" />
824
+ </TableCell>
825
+ </TableRow>
826
+ ) : filteredUsers.length === 0 ? (
827
+ <TableRow>
828
+ <TableCell
829
+ colSpan={onSelectionChange ? 7 : 6}
830
+ className="text-center py-8 text-muted-foreground"
831
+ >
832
+ Aucun utilisateur trouvé
833
+ </TableCell>
834
+ </TableRow>
835
+ ) : (
836
+ filteredUsers.map((user) => (
837
+ <TableRow key={user.id}>
838
+ {onSelectionChange && (
839
+ <TableCell>
840
+ <Checkbox
841
+ checked={selectedUsers.includes(user.id)}
842
+ onCheckedChange={(checked) =>
843
+ handleSelectUser(user.id, checked as boolean)
844
+ }
845
+ />
846
+ </TableCell>
847
+ )}
848
+ <TableCell>
849
+ <div className="flex items-center gap-3">
850
+ <UserAvatar user={user} />
851
+ <div>
852
+ <div className="font-medium">{user.displayName}</div>
853
+ <div className="text-sm text-muted-foreground">
854
+ {user.email}
855
+ </div>
856
+ </div>
857
+ </div>
858
+ </TableCell>
859
+ <TableCell>
860
+ <RoleBadge role={user.role} roles={roles} />
861
+ </TableCell>
862
+ <TableCell>
863
+ <StatusIndicator status={user.status} />
864
+ </TableCell>
865
+ <TableCell className="text-muted-foreground">
866
+ {formatDate(user.createdAt)}
867
+ </TableCell>
868
+ <TableCell className="text-muted-foreground">
869
+ {user.lastLogin ? formatDate(user.lastLogin) : "-"}
870
+ </TableCell>
871
+ <TableCell>
872
+ <UserActionsMenu
873
+ user={user}
874
+ roles={roles}
875
+ onEdit={onEdit ? () => setEditingUser(user) : undefined}
876
+ onDelete={onDelete ? () => setDeletingUser(user) : undefined}
877
+ onResetPassword={
878
+ onResetPassword ? () => onResetPassword(user.id) : undefined
879
+ }
880
+ onRoleChange={
881
+ onRoleChange
882
+ ? (role) => onRoleChange(user.id, role)
883
+ : undefined
884
+ }
885
+ onStatusChange={
886
+ onStatusChange
887
+ ? (status) => onStatusChange(user.id, status)
888
+ : undefined
889
+ }
890
+ />
891
+ </TableCell>
892
+ </TableRow>
893
+ ))
894
+ )}
895
+ </TableBody>
896
+ </Table>
897
+ </CardContent>
898
+ </Card>
899
+
900
+ {/* Create dialog */}
901
+ <UserFormDialog
902
+ open={createDialogOpen}
903
+ onOpenChange={setCreateDialogOpen}
904
+ roles={roles}
905
+ permissions={permissions}
906
+ onSubmit={handleCreate}
907
+ loading={actionLoading}
908
+ />
909
+
910
+ {/* Edit dialog */}
911
+ <UserFormDialog
912
+ open={!!editingUser}
913
+ onOpenChange={(open) => !open && setEditingUser(null)}
914
+ user={editingUser || undefined}
915
+ roles={roles}
916
+ permissions={permissions}
917
+ onSubmit={handleEdit}
918
+ loading={actionLoading}
919
+ />
920
+
921
+ {/* Delete confirmation */}
922
+ <AlertDialog open={!!deletingUser} onOpenChange={(open) => !open && setDeletingUser(null)}>
923
+ <AlertDialogContent>
924
+ <AlertDialogHeader>
925
+ <AlertDialogTitle>Supprimer l'utilisateur ?</AlertDialogTitle>
926
+ <AlertDialogDescription>
927
+ Êtes-vous sûr de vouloir supprimer {deletingUser?.displayName} ?
928
+ Cette action est irréversible.
929
+ </AlertDialogDescription>
930
+ </AlertDialogHeader>
931
+ <AlertDialogFooter>
932
+ <AlertDialogCancel>Annuler</AlertDialogCancel>
933
+ <AlertDialogAction
934
+ onClick={handleDelete}
935
+ className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
936
+ >
937
+ Supprimer
938
+ </AlertDialogAction>
939
+ </AlertDialogFooter>
940
+ </AlertDialogContent>
941
+ </AlertDialog>
942
+
943
+ {/* Bulk delete confirmation */}
944
+ <AlertDialog open={bulkDeleteOpen} onOpenChange={setBulkDeleteOpen}>
945
+ <AlertDialogContent>
946
+ <AlertDialogHeader>
947
+ <AlertDialogTitle>Supprimer les utilisateurs ?</AlertDialogTitle>
948
+ <AlertDialogDescription>
949
+ Êtes-vous sûr de vouloir supprimer {selectedUsers.length} utilisateur(s) ?
950
+ Cette action est irréversible.
951
+ </AlertDialogDescription>
952
+ </AlertDialogHeader>
953
+ <AlertDialogFooter>
954
+ <AlertDialogCancel>Annuler</AlertDialogCancel>
955
+ <AlertDialogAction
956
+ onClick={handleBulkDelete}
957
+ className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
958
+ >
959
+ Supprimer
960
+ </AlertDialogAction>
961
+ </AlertDialogFooter>
962
+ </AlertDialogContent>
963
+ </AlertDialog>
964
+ </div>
965
+ )
966
+ }
967
+
968
+ // ============================================
969
+ // PRESETS
970
+ // ============================================
971
+
972
+ export const defaultUsers: UserData[] = [
973
+ {
974
+ id: "1",
975
+ email: "admin@exemple.com",
976
+ displayName: "Admin User",
977
+ firstName: "Admin",
978
+ lastName: "User",
979
+ role: "admin",
980
+ status: "active",
981
+ createdAt: new Date("2023-01-01"),
982
+ lastLogin: new Date("2024-01-15"),
983
+ },
984
+ {
985
+ id: "2",
986
+ email: "moderator@exemple.com",
987
+ displayName: "Marie Modérateur",
988
+ firstName: "Marie",
989
+ lastName: "Modérateur",
990
+ role: "moderator",
991
+ status: "active",
992
+ createdAt: new Date("2023-03-15"),
993
+ lastLogin: new Date("2024-01-14"),
994
+ },
995
+ {
996
+ id: "3",
997
+ email: "user@exemple.com",
998
+ displayName: "Jean Utilisateur",
999
+ firstName: "Jean",
1000
+ lastName: "Utilisateur",
1001
+ role: "user",
1002
+ status: "active",
1003
+ createdAt: new Date("2023-06-01"),
1004
+ lastLogin: new Date("2024-01-10"),
1005
+ },
1006
+ {
1007
+ id: "4",
1008
+ email: "pending@exemple.com",
1009
+ displayName: "Paul Pending",
1010
+ firstName: "Paul",
1011
+ lastName: "Pending",
1012
+ role: "user",
1013
+ status: "pending",
1014
+ createdAt: new Date("2024-01-10"),
1015
+ },
1016
+ {
1017
+ id: "5",
1018
+ email: "banned@exemple.com",
1019
+ displayName: "Bob Banni",
1020
+ firstName: "Bob",
1021
+ lastName: "Banni",
1022
+ role: "guest",
1023
+ status: "banned",
1024
+ createdAt: new Date("2023-09-01"),
1025
+ lastLogin: new Date("2023-12-01"),
1026
+ },
1027
+ ]
1028
+
1029
+ export const defaultPermissions: UserPermission[] = [
1030
+ { id: "read", name: "Lecture", description: "Accès en lecture", category: "Base" },
1031
+ { id: "write", name: "Écriture", description: "Accès en écriture", category: "Base" },
1032
+ { id: "delete", name: "Suppression", description: "Accès suppression", category: "Base" },
1033
+ { id: "admin", name: "Administration", description: "Accès admin", category: "Admin" },
1034
+ { id: "users", name: "Gestion utilisateurs", description: "Gérer les utilisateurs", category: "Admin" },
1035
+ ]
1036
+
1037
+ export default WakaUserManagement