@goplusvn/core 0.1.0 → 0.1.1

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 (369) hide show
  1. package/package.json +2 -1
  2. package/src/assets/erp_wallpaper.png +0 -0
  3. package/src/assets/goeat_logo.png +0 -0
  4. package/src/audit/audit-manager.ts +139 -0
  5. package/src/audit/index.ts +11 -0
  6. package/src/audit/memory-audit-logger.ts +86 -0
  7. package/src/audit/types.ts +50 -0
  8. package/src/auth/auth-service.ts +97 -0
  9. package/src/auth/index.ts +266 -0
  10. package/src/code-generation/index.ts +69 -0
  11. package/src/configs/auth-routes.ts +17 -0
  12. package/src/configs/crud.ts +136 -0
  13. package/src/configs/data/navigations.ts +781 -0
  14. package/src/configs/data/oauth-links.ts +10 -0
  15. package/src/configs/entities/material-categories.config.ts +125 -0
  16. package/src/configs/i18n.ts +12 -0
  17. package/src/configs/index.ts +26 -0
  18. package/src/configs/status.ts +25 -0
  19. package/src/configs/themes.ts +100 -0
  20. package/src/crud/components/crud-bulk-actions.tsx +91 -0
  21. package/src/crud/components/crud-card-view.tsx +241 -0
  22. package/src/crud/components/crud-context.tsx +122 -0
  23. package/src/crud/components/crud-delete-dialog.tsx +145 -0
  24. package/src/crud/components/crud-dialog.tsx +406 -0
  25. package/src/crud/components/crud-empty-state.tsx +104 -0
  26. package/src/crud/components/crud-export-button.tsx +170 -0
  27. package/src/crud/components/crud-field-renderer.tsx +653 -0
  28. package/src/crud/components/crud-filter-chips.tsx +102 -0
  29. package/src/crud/components/crud-filters/checkbox-filter.tsx +97 -0
  30. package/src/crud/components/crud-filters/datetime-filter.tsx +83 -0
  31. package/src/crud/components/crud-filters/filter-builder.tsx +66 -0
  32. package/src/crud/components/crud-filters/index.tsx +76 -0
  33. package/src/crud/components/crud-filters/radio-filter.tsx +86 -0
  34. package/src/crud/components/crud-filters/select-filter.tsx +141 -0
  35. package/src/crud/components/crud-filters/text-filter.tsx +86 -0
  36. package/src/crud/components/crud-form.tsx +642 -0
  37. package/src/crud/components/crud-import-dialog.tsx +440 -0
  38. package/src/crud/components/crud-infinite-scroll.tsx +116 -0
  39. package/src/crud/components/crud-page.tsx +1017 -0
  40. package/src/crud/components/crud-provider.tsx +277 -0
  41. package/src/crud/components/crud-row-actions.tsx +189 -0
  42. package/src/crud/components/crud-search.tsx +82 -0
  43. package/src/crud/components/crud-sheet.tsx +336 -0
  44. package/src/crud/components/crud-table-skeleton.tsx +26 -0
  45. package/src/crud/components/crud-table-toolbar.tsx +91 -0
  46. package/src/crud/components/crud-table.tsx +352 -0
  47. package/src/crud/components/crud-virtual-table.tsx +55 -0
  48. package/src/crud/components/index.tsx +20 -0
  49. package/src/crud/crud-filters/checkbox-filter.tsx +87 -0
  50. package/src/crud/crud-filters/datetime-filter.tsx +82 -0
  51. package/src/crud/crud-filters/filter-builder.tsx +64 -0
  52. package/src/crud/crud-filters/index.tsx +78 -0
  53. package/src/crud/crud-filters/radio-filter.tsx +79 -0
  54. package/src/crud/crud-filters/select-filter.tsx +148 -0
  55. package/src/crud/crud-filters/text-filter.tsx +81 -0
  56. package/src/crud/index.ts +43 -0
  57. package/src/crud/lib/crud-service.test.ts +334 -0
  58. package/src/crud/lib/crud-service.ts +358 -0
  59. package/src/crud/lib/crud-utils.test.ts +354 -0
  60. package/src/crud/lib/crud-utils.ts +299 -0
  61. package/src/crud/lib/crud-validator.ts +247 -0
  62. package/src/crud/lib/data-loader.ts +234 -0
  63. package/src/crud/lib/field-calculator.ts +241 -0
  64. package/src/crud/lib/field-formatter.ts +240 -0
  65. package/src/crud/lib/import-export-service.test.ts +290 -0
  66. package/src/crud/lib/import-export-service.ts +352 -0
  67. package/src/crud/lib/import-server-utils.ts +109 -0
  68. package/src/crud/lib/lazy-loader.ts +241 -0
  69. package/src/crud/lib/parse-filters.ts +85 -0
  70. package/src/crud/lib/permissions.ts +52 -0
  71. package/src/crud/lib/serialize-config.ts +60 -0
  72. package/src/crud/lib/stream-loader.ts +145 -0
  73. package/src/crud/lib/translate-config.ts +335 -0
  74. package/src/crud/lib/types.ts +11 -0
  75. package/src/crud/pages/entity-crud-page.tsx +144 -0
  76. package/src/crud/server.ts +8 -0
  77. package/src/home/constants.tsx +142 -0
  78. package/src/home/feature-showcase.tsx +171 -0
  79. package/src/home/home-page.tsx +191 -0
  80. package/src/home/hooks/index.ts +1 -0
  81. package/src/home/hooks/useWidgetPreferences.ts +167 -0
  82. package/src/home/index.ts +33 -0
  83. package/src/home/quick-access-dialog.tsx +271 -0
  84. package/src/home/quick-access-menu.tsx +267 -0
  85. package/src/home/types.ts +140 -0
  86. package/src/home/welcome-card.tsx +92 -0
  87. package/src/home/widget-container.tsx +258 -0
  88. package/src/home/widgets/base-widget.tsx +200 -0
  89. package/src/home/widgets/customers-widget.tsx +74 -0
  90. package/src/home/widgets/index.ts +6 -0
  91. package/src/home/widgets/orders-widget.tsx +87 -0
  92. package/src/home/widgets/revenue-widget.tsx +71 -0
  93. package/src/home/widgets/stock-widget.tsx +109 -0
  94. package/src/hooks/index.tsx +598 -0
  95. package/src/hooks/use-tenant.test.tsx +30 -0
  96. package/src/hooks/use-tenant.ts +5 -0
  97. package/src/index.ts +17 -0
  98. package/src/infrastructure/__tests__/architecture-verification.spec.ts +103 -0
  99. package/src/infrastructure/api-service.ts +317 -0
  100. package/src/infrastructure/cache/cache-manager.ts +107 -0
  101. package/src/infrastructure/cache/cache.ts +120 -0
  102. package/src/infrastructure/cache/index.ts +8 -0
  103. package/src/infrastructure/cache/types.ts +48 -0
  104. package/src/infrastructure/cron/cron-manager.ts +239 -0
  105. package/src/infrastructure/cron/index.ts +6 -0
  106. package/src/infrastructure/cron/types.ts +41 -0
  107. package/src/infrastructure/event-bus/event-bus.ts +145 -0
  108. package/src/infrastructure/event-bus/index.ts +2 -0
  109. package/src/infrastructure/event-bus/types.ts +22 -0
  110. package/src/infrastructure/index.ts +32 -0
  111. package/src/infrastructure/lock/decorators.ts +67 -0
  112. package/src/infrastructure/lock/index.ts +2 -0
  113. package/src/infrastructure/lock/lock-manager.ts +33 -0
  114. package/src/infrastructure/logger/index.ts +2 -0
  115. package/src/infrastructure/logger/logger.ts +96 -0
  116. package/src/infrastructure/logger/types.ts +25 -0
  117. package/src/layout/index.tsx +185 -0
  118. package/src/navigation/index.ts +91 -0
  119. package/src/notification/index.ts +14 -0
  120. package/src/notification/notification-service.ts +120 -0
  121. package/src/notification/storage/in-memory.ts +56 -0
  122. package/src/notification/storage/index.ts +1 -0
  123. package/src/notification/types.ts +51 -0
  124. package/src/organization/branch-service.ts +299 -0
  125. package/src/organization/branches.config.ts +154 -0
  126. package/src/organization/index.ts +5 -0
  127. package/src/plugin/apps-registry.ts +97 -0
  128. package/src/plugin/index.ts +5 -0
  129. package/src/plugin/types.ts +41 -0
  130. package/src/providers/index.tsx +109 -0
  131. package/src/providers/tenant-provider.tsx +45 -0
  132. package/src/rbac/components/roles/role-card.tsx +158 -0
  133. package/src/rbac/components/roles/role-stats-cards.tsx +29 -0
  134. package/src/rbac/components/roles/role-toolbar.tsx +123 -0
  135. package/src/rbac/hooks/use-role-operations.ts +159 -0
  136. package/src/rbac/hooks/use-roles-data.ts +59 -0
  137. package/src/rbac/index.ts +297 -0
  138. package/src/rbac/lib/permission-helpers.ts +63 -0
  139. package/src/rbac/pages/action-list-page.tsx +25 -0
  140. package/src/rbac/pages/resource-list-page.tsx +25 -0
  141. package/src/rbac/pages/role-list-page.tsx +378 -0
  142. package/src/rbac/permission-service.ts +140 -0
  143. package/src/rbac/permissions.ts +135 -0
  144. package/src/rbac/resource-service.ts +115 -0
  145. package/src/rbac/resource-validator.ts +119 -0
  146. package/src/rbac/role-service.ts +165 -0
  147. package/src/rbac/server.ts +16 -0
  148. package/src/rbac/types.ts +38 -0
  149. package/src/schemas/action.schema.ts +66 -0
  150. package/src/schemas/branch.schema.ts +52 -0
  151. package/src/schemas/coming-soon-schema.ts +9 -0
  152. package/src/schemas/company.schema.ts +44 -0
  153. package/src/schemas/forgot-passward-schema.ts +9 -0
  154. package/src/schemas/index.ts +30 -0
  155. package/src/schemas/material-category.schema.ts +43 -0
  156. package/src/schemas/material-pricing.schema.ts +74 -0
  157. package/src/schemas/material.schema.ts +76 -0
  158. package/src/schemas/materials.ts +52 -0
  159. package/src/schemas/new-passward-schema.ts +15 -0
  160. package/src/schemas/partner-company.schema.ts +149 -0
  161. package/src/schemas/register-schema.ts +36 -0
  162. package/src/schemas/resource.schema.ts +133 -0
  163. package/src/schemas/role.schema.ts +11 -0
  164. package/src/schemas/sign-in-schema.ts +24 -0
  165. package/src/schemas/supplier-pricing.schema.ts +15 -0
  166. package/src/schemas/supplier.schema.ts +120 -0
  167. package/src/schemas/system-category-group.schema.ts +67 -0
  168. package/src/schemas/system-category.schema.ts +77 -0
  169. package/src/schemas/system-config.schema.ts +118 -0
  170. package/src/schemas/uom.schema.ts +75 -0
  171. package/src/schemas/user-supplier.schema.ts +179 -0
  172. package/src/schemas/user.schema.ts +18 -0
  173. package/src/schemas/verify-email-schema.ts +9 -0
  174. package/src/schemas/warehouse.schema.ts +49 -0
  175. package/src/system/components/categories/category-list.tsx +529 -0
  176. package/src/system/components/categories/category-manager.tsx +89 -0
  177. package/src/system/components/categories/group-sidebar.tsx +308 -0
  178. package/src/system/components/settings/setting-dialogs.tsx +197 -0
  179. package/src/system/components/settings/setting-field.tsx +291 -0
  180. package/src/system/components/settings/setting-form-dialog.tsx +308 -0
  181. package/src/system/components/settings/settings-groups.ts +80 -0
  182. package/src/system/components/settings/settings-search.tsx +71 -0
  183. package/src/system/components/settings/settings-section.tsx +74 -0
  184. package/src/system/components/settings/settings-sidebar.tsx +81 -0
  185. package/src/system/constants.ts +3 -0
  186. package/src/system/index.ts +150 -0
  187. package/src/system/job-manager.ts +176 -0
  188. package/src/system/pages/components/categories/category-list.tsx +537 -0
  189. package/src/system/pages/components/categories/category-manager.tsx +90 -0
  190. package/src/system/pages/components/categories/group-sidebar.tsx +311 -0
  191. package/src/system/pages/components/settings/sales-rules-settings.tsx +222 -0
  192. package/src/system/pages/components/settings/setting-dialogs.tsx +197 -0
  193. package/src/system/pages/components/settings/setting-field.tsx +292 -0
  194. package/src/system/pages/components/settings/setting-form-dialog.tsx +308 -0
  195. package/src/system/pages/components/settings/settings-groups.ts +87 -0
  196. package/src/system/pages/components/settings/settings-page.tsx +372 -0
  197. package/src/system/pages/components/settings/settings-search.tsx +71 -0
  198. package/src/system/pages/components/settings/settings-section.tsx +74 -0
  199. package/src/system/pages/components/settings/settings-sidebar.tsx +81 -0
  200. package/src/system/pages/components/settings/system-settings.tsx +244 -0
  201. package/src/system/pages/system-category-page.tsx +15 -0
  202. package/src/system/pages/system-settings-page.tsx +380 -0
  203. package/src/system/schemas/system-category-group.schema.ts +46 -0
  204. package/src/system/schemas/system-category.schema.ts +56 -0
  205. package/src/system/services/settings-service.ts +127 -0
  206. package/src/system/services/system-category-service.ts +63 -0
  207. package/src/system/types.ts +45 -0
  208. package/src/types/index.ts +703 -0
  209. package/src/ui/auth/auth-layout.tsx +135 -0
  210. package/src/ui/auth/forgot-password-form.tsx +98 -0
  211. package/src/ui/auth/index.tsx +7 -0
  212. package/src/ui/auth/new-password-form.tsx +107 -0
  213. package/src/ui/auth/oauth-links.tsx +30 -0
  214. package/src/ui/auth/register-form.tsx +202 -0
  215. package/src/ui/auth/sign-in-form.tsx +238 -0
  216. package/src/ui/auth/verify-email-form.tsx +104 -0
  217. package/src/ui/crud/index.tsx +10 -0
  218. package/src/ui/data-display/accordion.tsx +65 -0
  219. package/src/ui/data-display/aspect-ratio.tsx +11 -0
  220. package/src/ui/data-display/avatar.tsx +163 -0
  221. package/src/ui/data-display/bento-grid.tsx +77 -0
  222. package/src/ui/data-display/carousel.tsx +249 -0
  223. package/src/ui/data-display/chart.tsx +363 -0
  224. package/src/ui/data-display/code-block-highlight.tsx +54 -0
  225. package/src/ui/data-display/collapsible.tsx +42 -0
  226. package/src/ui/data-display/compact-stat-bar.tsx +149 -0
  227. package/src/ui/data-display/data-table/data-table-context.tsx +255 -0
  228. package/src/ui/data-display/data-table/data-table-empty-state.tsx +133 -0
  229. package/src/ui/data-display/data-table/data-table-skeleton.tsx +145 -0
  230. package/src/ui/data-display/data-table/data-table-toolbar.tsx +353 -0
  231. package/src/ui/data-display/data-table/data-table.tsx +597 -0
  232. package/src/ui/data-display/data-table/index.ts +44 -0
  233. package/src/ui/data-display/data-table-column-header.tsx +75 -0
  234. package/src/ui/data-display/data-table-pagination.tsx +130 -0
  235. package/src/ui/data-display/data-table-view-options.tsx +59 -0
  236. package/src/ui/data-display/formatted-number-input.tsx +210 -0
  237. package/src/ui/data-display/highlight.tsx +20 -0
  238. package/src/ui/data-display/hover-card.tsx +48 -0
  239. package/src/ui/data-display/index.tsx +50 -0
  240. package/src/ui/data-display/iphone-15-pro.tsx +114 -0
  241. package/src/ui/data-display/kanban/index.ts +4 -0
  242. package/src/ui/data-display/kanban/kanban-board.tsx +192 -0
  243. package/src/ui/data-display/kanban/kanban-column.tsx +74 -0
  244. package/src/ui/data-display/kanban/kanban-item.tsx +50 -0
  245. package/src/ui/data-display/kanban/kanban-types.ts +21 -0
  246. package/src/ui/data-display/kpi-card.tsx +68 -0
  247. package/src/ui/data-display/media-grid.tsx +110 -0
  248. package/src/ui/data-display/safari.tsx +175 -0
  249. package/src/ui/data-display/show-more-text.tsx +55 -0
  250. package/src/ui/data-display/tabs.tsx +68 -0
  251. package/src/ui/data-display/timeline.tsx +256 -0
  252. package/src/ui/feedback/alert.tsx +60 -0
  253. package/src/ui/feedback/context-menu.tsx +245 -0
  254. package/src/ui/feedback/drawer.tsx +132 -0
  255. package/src/ui/feedback/error-dialog.tsx +273 -0
  256. package/src/ui/feedback/index.tsx +183 -0
  257. package/src/ui/feedback/progress.tsx +32 -0
  258. package/src/ui/feedback/sheet.tsx +148 -0
  259. package/src/ui/feedback/sonner.tsx +36 -0
  260. package/src/ui/forms/command.tsx +157 -0
  261. package/src/ui/forms/date-picker.tsx +73 -0
  262. package/src/ui/forms/date-range-picker.tsx +76 -0
  263. package/src/ui/forms/date-time-picker.tsx +109 -0
  264. package/src/ui/forms/editor/editor-menu-bar.tsx +394 -0
  265. package/src/ui/forms/editor/index.tsx +130 -0
  266. package/src/ui/forms/editor/multi-select-example.tsx +1234 -0
  267. package/src/ui/forms/emoji-picker.tsx +109 -0
  268. package/src/ui/forms/file-dropzone.tsx +169 -0
  269. package/src/ui/forms/file-thumbnail.tsx +29 -0
  270. package/src/ui/forms/index.tsx +201 -0
  271. package/src/ui/forms/input-file.tsx +99 -0
  272. package/src/ui/forms/input-group.tsx +46 -0
  273. package/src/ui/forms/input-otp.tsx +81 -0
  274. package/src/ui/forms/input-phone.tsx +172 -0
  275. package/src/ui/forms/input-spin.tsx +116 -0
  276. package/src/ui/forms/input-tags.tsx +219 -0
  277. package/src/ui/forms/input-time.tsx +42 -0
  278. package/src/ui/forms/multi-select.tsx +629 -0
  279. package/src/ui/forms/multiple-date-picker.tsx +74 -0
  280. package/src/ui/forms/radio-group.tsx +42 -0
  281. package/src/ui/forms/rating.tsx +158 -0
  282. package/src/ui/forms/time-picker.tsx +57 -0
  283. package/src/ui/index.tsx +17 -0
  284. package/src/ui/layout/animated-list.tsx +77 -0
  285. package/src/ui/layout/animated-sidebar.tsx +294 -0
  286. package/src/ui/layout/command-menu.tsx +355 -0
  287. package/src/ui/layout/customizer.tsx +324 -0
  288. package/src/ui/layout/footer.tsx +43 -0
  289. package/src/ui/layout/full-screen-toggle.tsx +52 -0
  290. package/src/ui/layout/header-breadcrumb.tsx +77 -0
  291. package/src/ui/layout/horizontal-layout-header.tsx +83 -0
  292. package/src/ui/layout/horizontal-layout.tsx +50 -0
  293. package/src/ui/layout/index.tsx +25 -0
  294. package/src/ui/layout/language-dropdown.tsx +103 -0
  295. package/src/ui/layout/logo.tsx +63 -0
  296. package/src/ui/layout/main-layout.tsx +57 -0
  297. package/src/ui/layout/mode-dropdown.tsx +58 -0
  298. package/src/ui/layout/notification-dropdown.tsx +127 -0
  299. package/src/ui/layout/page-tabs.tsx +306 -0
  300. package/src/ui/layout/route-cache.tsx +214 -0
  301. package/src/ui/layout/sidebar-group-icon-menu.tsx +195 -0
  302. package/src/ui/layout/sidebar.tsx +279 -0
  303. package/src/ui/layout/tab-content-cache.tsx +201 -0
  304. package/src/ui/layout/tab-navigation-provider.tsx +536 -0
  305. package/src/ui/layout/toggle-mobile-sidebar.tsx +33 -0
  306. package/src/ui/layout/top-bar-header-menubar.tsx +412 -0
  307. package/src/ui/layout/user-dropdown.tsx +188 -0
  308. package/src/ui/layout/vertical-layout-header.tsx +65 -0
  309. package/src/ui/layout/vertical-layout.tsx +47 -0
  310. package/src/ui/management/audit-log-page.tsx +209 -0
  311. package/src/ui/management/cache-management.tsx +349 -0
  312. package/src/ui/management/index.ts +3 -0
  313. package/src/ui/management/job-management.tsx +308 -0
  314. package/src/ui/pages/not-found.tsx +30 -0
  315. package/src/ui/primitives/badge.tsx +66 -0
  316. package/src/ui/primitives/breadcrumb.tsx +103 -0
  317. package/src/ui/primitives/button.tsx +129 -0
  318. package/src/ui/primitives/calendar.tsx +74 -0
  319. package/src/ui/primitives/card.tsx +86 -0
  320. package/src/ui/primitives/checkbox.tsx +31 -0
  321. package/src/ui/primitives/client.ts +30 -0
  322. package/src/ui/primitives/combobox.tsx +290 -0
  323. package/src/ui/primitives/dialog.tsx +121 -0
  324. package/src/ui/primitives/dropdown-menu.tsx +239 -0
  325. package/src/ui/primitives/dynamic-icon.tsx +24 -0
  326. package/src/ui/primitives/index.tsx +134 -0
  327. package/src/ui/primitives/input-number.tsx +131 -0
  328. package/src/ui/primitives/input.tsx +22 -0
  329. package/src/ui/primitives/keyboard.tsx +23 -0
  330. package/src/ui/primitives/label.tsx +24 -0
  331. package/src/ui/primitives/menubar.tsx +262 -0
  332. package/src/ui/primitives/navigation-menu.tsx +157 -0
  333. package/src/ui/primitives/pagination.tsx +118 -0
  334. package/src/ui/primitives/popover.tsx +56 -0
  335. package/src/ui/primitives/prefetch-link.tsx +60 -0
  336. package/src/ui/primitives/resizable.tsx +59 -0
  337. package/src/ui/primitives/scroll-area.tsx +63 -0
  338. package/src/ui/primitives/select.tsx +172 -0
  339. package/src/ui/primitives/separator.tsx +51 -0
  340. package/src/ui/primitives/sidebar.tsx +844 -0
  341. package/src/ui/primitives/slider.tsx +27 -0
  342. package/src/ui/primitives/status-badge.tsx +47 -0
  343. package/src/ui/primitives/sticky-layout.tsx +50 -0
  344. package/src/ui/primitives/switch.tsx +29 -0
  345. package/src/ui/primitives/table.tsx +116 -0
  346. package/src/ui/primitives/tabs.tsx +55 -0
  347. package/src/ui/primitives/toggle-group.tsx +70 -0
  348. package/src/ui/primitives/toggle.tsx +47 -0
  349. package/src/ui/primitives/tooltip.tsx +59 -0
  350. package/src/user/components/dangerous-zone.tsx +34 -0
  351. package/src/user/components/delete-account-form.tsx +40 -0
  352. package/src/user/components/index.ts +4 -0
  353. package/src/user/components/profile-info-form.tsx +390 -0
  354. package/src/user/components/profile-info.tsx +32 -0
  355. package/src/user/components/unified-profile-dialog.tsx +1019 -0
  356. package/src/user/components/user-stats.tsx +27 -0
  357. package/src/user/components/user-toolbar.tsx +137 -0
  358. package/src/user/components/users-card-view.tsx +253 -0
  359. package/src/user/index.ts +11 -0
  360. package/src/user/pages/user-list-page.tsx +234 -0
  361. package/src/user/pages/users-client-page.tsx +385 -0
  362. package/src/user/profile-page.tsx +19 -0
  363. package/src/user/schemas.ts +68 -0
  364. package/src/user/types.ts +34 -0
  365. package/src/user/user-service.ts +538 -0
  366. package/src/utils/index.ts +906 -0
  367. package/src/workflow/activity-timeline.tsx +412 -0
  368. package/src/workflow/approval-workflow.tsx +31 -0
  369. package/src/workflow/index.ts +2 -0
@@ -0,0 +1,1019 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect, useMemo } from "react";
4
+ import useSWR from "swr";
5
+ import {
6
+ Dialog,
7
+ DialogContent,
8
+ DialogHeader,
9
+ DialogTitle,
10
+ Button,
11
+ Input,
12
+ Label,
13
+ Checkbox,
14
+ Badge,
15
+ Avatar,
16
+ AvatarFallback,
17
+ AvatarImage,
18
+ Select,
19
+ SelectContent,
20
+ SelectItem,
21
+ SelectTrigger,
22
+ SelectValue,
23
+ Switch,
24
+ } from "../../ui";
25
+ import { toast } from "sonner";
26
+ import { cn } from "../../utils";
27
+ import {
28
+ X,
29
+ Search,
30
+ Eye,
31
+ EyeOff,
32
+ Loader2,
33
+ Ban,
34
+ UserCog,
35
+ Store,
36
+ CheckCircle2,
37
+ User,
38
+ Briefcase,
39
+ ShieldCheck,
40
+ KeyRound,
41
+ } from "lucide-react";
42
+
43
+ interface UnifiedProfileDialogProps {
44
+ open: boolean;
45
+ onOpenChange: (open: boolean) => void;
46
+ data?: any;
47
+ mode?: "create" | "edit" | "view";
48
+ viewMode: "admin" | "crm" | "srm";
49
+ roles?: any[];
50
+ onSaveProfile?: (data: any, id?: string) => Promise<void>;
51
+ onSaveRoles?: (userId: string, roleCodes: string[]) => Promise<void>;
52
+ onSavePassword?: (userId: string, password: string) => Promise<void>;
53
+ }
54
+
55
+ const fetcher = async (url: string) => {
56
+ const res = await fetch(url);
57
+ if (!res.ok) throw new Error("Failed to fetch");
58
+ const data = await res.json();
59
+ return Array.isArray(data) ? data : data.items || data.data || [];
60
+ };
61
+
62
+ export function UnifiedProfileDialog({
63
+ open,
64
+ onOpenChange,
65
+ data,
66
+ mode = "edit",
67
+ viewMode = "admin",
68
+ roles = [],
69
+ onSaveProfile,
70
+ onSaveRoles,
71
+ onSavePassword,
72
+ }: UnifiedProfileDialogProps) {
73
+ // -- Data Fetching --
74
+ const { data: departments } = useSWR<{ id: string; name: string }[]>(
75
+ "/api/departments?status=active",
76
+ fetcher,
77
+ );
78
+ const { data: jobTitles } = useSWR<{ id: string; name: string }[]>(
79
+ "/api/job-titles?status=active",
80
+ fetcher,
81
+ );
82
+ const { data: suppliers } = useSWR<{ id: string; name: string }[]>(
83
+ "/api/suppliers?pageSize=100",
84
+ fetcher,
85
+ );
86
+ const { data: customerGroups } = useSWR<{ id: string; name: string }[]>(
87
+ "/api/crud/customer-groups?status=active",
88
+ fetcher,
89
+ );
90
+ const { data: branches } = useSWR<{ id: string; name: string }[]>(
91
+ "/api/branches",
92
+ fetcher,
93
+ );
94
+
95
+ // -- State --
96
+ const [activeSection, setActiveSection] = useState("general");
97
+ const [isSubmitting, setIsSubmitting] = useState(false);
98
+ const [error, setError] = useState("");
99
+
100
+ // Form State
101
+ const [userType, setUserType] = useState<string>("employee");
102
+ const [profileData, setProfileData] = useState<any>({});
103
+ const [selectedRoles, setSelectedRoles] = useState<string[]>([]);
104
+ const [selectedBranches, setSelectedBranches] = useState<string[]>([]);
105
+ const [passwordData, setPasswordData] = useState({
106
+ password: "",
107
+ confirm: "",
108
+ });
109
+ const [showPassword, setShowPassword] = useState(false);
110
+ const [roleSearchQuery, setRoleSearchQuery] = useState("");
111
+ const [enableAccount, setEnableAccount] = useState(false);
112
+
113
+ const displayData = data || {};
114
+
115
+ // Derived state
116
+ const isCustomer = viewMode === "crm" || userType === "customer";
117
+
118
+ // -- Navigation Items (Conditional) --
119
+ const navItems = useMemo(() => {
120
+ // For Customers: Only ONE section with all info merged
121
+ if (isCustomer) {
122
+ return [{ id: "general", label: "Thông tin khách hàng", icon: User }];
123
+ }
124
+
125
+ // For Employees/Suppliers: Multiple sections
126
+ const items: { id: string; label: string; icon: any }[] = [
127
+ { id: "general", label: "Thông tin chung", icon: User },
128
+ ];
129
+
130
+ if (userType === "employee") {
131
+ items.push({ id: "work", label: "Công việc", icon: Briefcase });
132
+ }
133
+
134
+ if (userType === "supplier") {
135
+ items.push({ id: "supplier_link", label: "Nhà cung cấp", icon: Store });
136
+ }
137
+
138
+ // Roles & Security for users with accounts
139
+ items.push({ id: "roles", label: "Phân quyền", icon: ShieldCheck });
140
+ items.push({ id: "security", label: "Bảo mật", icon: KeyRound });
141
+
142
+ return items;
143
+ }, [userType, isCustomer]);
144
+
145
+ // -- Initialization --
146
+ useEffect(() => {
147
+ if (open) {
148
+ setError("");
149
+ setIsSubmitting(false);
150
+ setActiveSection("general");
151
+
152
+ if (mode === "create") {
153
+ const initialType =
154
+ viewMode === "crm"
155
+ ? "customer"
156
+ : viewMode === "srm"
157
+ ? "supplier"
158
+ : "employee";
159
+ setUserType(initialType);
160
+ setProfileData({
161
+ status: "active",
162
+ name: "",
163
+ email: "",
164
+ phone: "",
165
+ address: "",
166
+ departmentId: "",
167
+ jobTitleId: "",
168
+ supplierId: "",
169
+ groupId: "",
170
+ idNumber: "",
171
+ gender: "",
172
+ birthday: "",
173
+ });
174
+ setSelectedRoles([]);
175
+ setSelectedBranches([]);
176
+ setPasswordData({ password: "", confirm: "" });
177
+ setEnableAccount(false);
178
+ } else {
179
+ const d = displayData;
180
+ let type = "employee";
181
+ if (viewMode === "crm" || d.userType === "customer" || d.customerId)
182
+ type = "customer";
183
+ else if (d.userType === "supplier" || d.supplierId) type = "supplier";
184
+ else if (d.userType) type = d.userType;
185
+
186
+ setUserType(type);
187
+ setProfileData({
188
+ status: d.status || "active",
189
+ name: d.name || "",
190
+ email: d.email || "",
191
+ phone: d.phone || d.profiles?.phone || "",
192
+ address: d.address || d.profiles?.address || "",
193
+ departmentId: d.departmentId || d.profiles?.departmentId || "",
194
+ jobTitleId: d.jobTitleId || d.profiles?.jobTitleId || "",
195
+ supplierId: d.supplierId || d.profiles?.supplierId || "",
196
+ groupId: d.groupId || "",
197
+ idNumber: d.idNumber || "",
198
+ gender: d.gender || "",
199
+ birthday: d.birthday
200
+ ? new Date(d.birthday).toISOString().split("T")[0]
201
+ : "",
202
+ });
203
+ const roles = d.roleCodes || [];
204
+ setSelectedRoles(roles);
205
+ setSelectedBranches(d.branchIds || []);
206
+ setPasswordData({ password: "", confirm: "" });
207
+ // Enable account if user has roles or if it's not a customer (employees always have accounts?)
208
+ // For now, if roles exist, we assume account is enabled.
209
+ setEnableAccount(roles.length > 0 || type !== "customer");
210
+ }
211
+ }
212
+ }, [open, data, mode, viewMode]);
213
+
214
+ useEffect(() => {
215
+ if (open && branches?.length === 1) {
216
+ if (mode === "create" || (data?.branchIds?.length || 0) === 0) {
217
+ if (selectedBranches.length === 0) {
218
+ setSelectedBranches([branches[0].id]);
219
+ }
220
+ }
221
+ }
222
+ }, [open, branches, mode, data]);
223
+
224
+ // -- Helpers --
225
+ const getInitials = (name: string | null) => {
226
+ if (!name) return "U";
227
+ return name
228
+ .split(" ")
229
+ .map((n) => n[0])
230
+ .join("")
231
+ .toUpperCase()
232
+ .slice(0, 2);
233
+ };
234
+
235
+ const filteredRoles = useMemo(() => {
236
+ if (!roleSearchQuery.trim()) return roles;
237
+ const query = roleSearchQuery.toLowerCase();
238
+ return roles.filter(
239
+ (r) =>
240
+ r.name.toLowerCase().includes(query) ||
241
+ r.code.toLowerCase().includes(query),
242
+ );
243
+ }, [roles, roleSearchQuery]);
244
+
245
+ // -- Save Handler --
246
+ const handleSave = async () => {
247
+ try {
248
+ setIsSubmitting(true);
249
+ setError("");
250
+
251
+ // Prepare payload
252
+ const payload: any = {
253
+ ...profileData,
254
+ userType, // Add userType to payload
255
+ roleCodes: selectedRoles,
256
+ branchIds: selectedBranches,
257
+ };
258
+
259
+ // Handle Password for Customer
260
+ if (isCustomer) {
261
+ if (enableAccount) {
262
+ if (passwordData.password) {
263
+ if (passwordData.password !== passwordData.confirm) {
264
+ setError("Mật khẩu xác nhận không khớp");
265
+ setIsSubmitting(false);
266
+ return;
267
+ }
268
+ if (passwordData.password.length < 6) {
269
+ setError("Mật khẩu phải có ít nhất 6 ký tự");
270
+ setIsSubmitting(false);
271
+ return;
272
+ }
273
+ payload.password = passwordData.password;
274
+ } else if (mode === "create") {
275
+ // If creating a customer account, password is required
276
+ setError("Mật khẩu là bắt buộc cho tài khoản khách hàng mới");
277
+ setIsSubmitting(false);
278
+ return;
279
+ }
280
+ } else {
281
+ // If account not enabled, default or clear
282
+ if (mode === "create") {
283
+ payload.password = "Goeat@123456";
284
+ } else {
285
+ delete payload.password;
286
+ payload.roleCodes = [];
287
+ }
288
+ }
289
+ } else {
290
+ if (passwordData.password) {
291
+ if (passwordData.password !== passwordData.confirm) {
292
+ setError("Mật khẩu xác nhận không khớp");
293
+ setIsSubmitting(false);
294
+ return;
295
+ }
296
+ if (passwordData.password.length < 6) {
297
+ setError("Mật khẩu phải có ít nhất 6 ký tự");
298
+ setIsSubmitting(false);
299
+ return;
300
+ }
301
+ payload.password = passwordData.password;
302
+ } else if (mode === "create") {
303
+ setError("Mật khẩu là bắt buộc cho tài khoản mới");
304
+ setIsSubmitting(false);
305
+ return;
306
+ }
307
+ }
308
+
309
+ if (!payload.name) {
310
+ setError("Tên là bắt buộc");
311
+ setIsSubmitting(false);
312
+ return;
313
+ }
314
+
315
+ // Call API
316
+ if (mode === "create") {
317
+ if (onSaveProfile) await onSaveProfile(payload);
318
+ toast.success("Đã tạo hồ sơ mới thành công");
319
+ } else {
320
+ // Save all data at once (profile + roles + branches)
321
+ if (onSaveProfile) {
322
+ await onSaveProfile(payload, data.id);
323
+ }
324
+
325
+ // Also save password if provided
326
+ if (payload.password && onSavePassword) {
327
+ await onSavePassword(data.id, payload.password);
328
+ }
329
+
330
+ toast.success("Đã lưu thông tin");
331
+ }
332
+
333
+ onOpenChange(false);
334
+ } catch (e: any) {
335
+ console.error(e);
336
+ setError(e.message || "Đã xảy ra lỗi");
337
+ toast.error("Đã xảy ra lỗi khi lưu thông tin");
338
+ } finally {
339
+ setIsSubmitting(false);
340
+ }
341
+ };
342
+
343
+ // -- Render Helpers --
344
+ const isActive = profileData.status === "active";
345
+
346
+ return (
347
+ <Dialog open={open} onOpenChange={onOpenChange}>
348
+ <DialogContent className="w-[100dvw] h-[100dvh] max-w-none max-h-none sm:w-auto sm:max-w-5xl sm:h-[85vh] flex flex-col p-0 gap-0 overflow-hidden border border-[#dee3e9] dark:border-[#1c1e21] shadow-[0_12px_32px_rgba(0,0,0,0.1)] bg-[#ffffff] dark:bg-[#0a1317] rounded-none sm:rounded-[12px] [&>button.absolute]:hidden">
349
+ <DialogHeader className="sr-only">
350
+ <DialogTitle>
351
+ {mode === "create" ? "Tạo mới" : "Chỉnh sửa"}
352
+ </DialogTitle>
353
+ </DialogHeader>
354
+
355
+ <div className="flex flex-col w-full h-full overflow-hidden">
356
+ {/* TOP HEADER: Profile Info + Actions */}
357
+ <div className="shrink-0 flex flex-col border-b border-[#e2e8f0] dark:border-[#334155] bg-[#f8fafc] dark:bg-[#0f172a] px-6 pt-5 pb-0">
358
+ <div className="flex flex-col sm:flex-row sm:items-start justify-between gap-4 pb-5">
359
+ {/* Profile Header */}
360
+ <div className="flex items-center gap-4">
361
+ <div className="relative">
362
+ <Avatar className="h-12 w-12 sm:h-14 sm:w-14 rounded-full border border-[#e2e8f0] dark:border-[#334155]">
363
+ <AvatarImage
364
+ src={displayData.avatar}
365
+ className="object-cover rounded-full"
366
+ />
367
+ <AvatarFallback className="text-[16px] sm:text-[18px] font-semibold bg-[#1641CE] text-white dark:bg-[#1E40AF] dark:text-white rounded-full">
368
+ {getInitials(profileData.name)}
369
+ </AvatarFallback>
370
+ </Avatar>
371
+ <div
372
+ className={cn(
373
+ "absolute -bottom-0.5 -right-0.5 w-3.5 h-3.5 sm:w-4 sm:h-4 border-[2px] border-[#f8fafc] dark:border-[#0f172a] rounded-full z-10",
374
+ isActive ? "bg-[#10b981]" : "bg-[#cbd5e1]"
375
+ )}
376
+ title={isActive ? "Hoạt động" : "Đã khóa"}
377
+ />
378
+ </div>
379
+ <div>
380
+ <h2 className="text-[18px] sm:text-[20px] font-semibold leading-tight text-[#0f172a] dark:text-white">
381
+ {profileData.name || "Chưa đặt tên"}
382
+ </h2>
383
+ <p className="mt-1 text-[13px] text-[#64748b] dark:text-[#94a3b8]">
384
+ {profileData.email || profileData.phone || "Chưa có thông tin liên hệ"}
385
+ </p>
386
+ </div>
387
+ </div>
388
+
389
+ {/* Actions & User Type */}
390
+ <div className="flex items-center gap-2 sm:gap-3 shrink-0">
391
+ {viewMode === "admin" ? (
392
+ <Select
393
+ value={userType}
394
+ onValueChange={(val) => setUserType(val)}
395
+ disabled={mode === "view"}
396
+ >
397
+ <SelectTrigger className="w-[140px] bg-white dark:bg-[#1e293b] border-[#e2e8f0] dark:border-[#334155] focus:ring-[#1641CE] focus:border-[#1641CE] h-9 rounded-md">
398
+ <SelectValue placeholder="Chọn loại" />
399
+ </SelectTrigger>
400
+ <SelectContent>
401
+ <SelectItem value="employee">
402
+ <span className="flex items-center gap-2">
403
+ <UserCog className="h-4 w-4 text-[#1641CE]" />
404
+ Nhân viên
405
+ </span>
406
+ </SelectItem>
407
+ <SelectItem value="customer">
408
+ <span className="flex items-center gap-2">
409
+ <User className="h-4 w-4 text-[#059669]" />
410
+ Khách hàng
411
+ </span>
412
+ </SelectItem>
413
+ <SelectItem value="supplier">
414
+ <span className="flex items-center gap-2">
415
+ <Store className="h-4 w-4 text-[#EA580C]" />
416
+ Nhà cung cấp
417
+ </span>
418
+ </SelectItem>
419
+ </SelectContent>
420
+ </Select>
421
+ ) : (
422
+ <div className="flex items-center gap-1.5 shrink-0">
423
+ {userType === "employee" && (
424
+ <span className="text-[12px] px-2.5 py-1 font-semibold rounded-md bg-[#1641CE]/10 text-[#1641CE] border border-[#1641CE]/20 inline-flex items-center">
425
+ <UserCog className="h-3.5 w-3.5 mr-1.5" /> Nhân viên
426
+ </span>
427
+ )}
428
+ {userType === "customer" && (
429
+ <span className="text-[12px] px-2.5 py-1 font-semibold rounded-md bg-[#059669] text-white inline-flex items-center">
430
+ <User className="h-3.5 w-3.5 mr-1.5" /> Khách hàng
431
+ </span>
432
+ )}
433
+ {userType === "supplier" && (
434
+ <span className="text-[12px] px-2.5 py-1 font-semibold rounded-md bg-[#EA580C] text-white inline-flex items-center">
435
+ <Store className="h-3.5 w-3.5 mr-1.5" /> Nhà cung cấp
436
+ </span>
437
+ )}
438
+ </div>
439
+ )}
440
+
441
+ <Button
442
+ variant="outline"
443
+ onClick={() => onOpenChange(false)}
444
+ className="border border-[#e2e8f0] bg-white dark:border-[#334155] text-[#475569] dark:text-white dark:bg-[#1e293b] hover:bg-[#f1f5f9] gap-2 h-9 rounded-md px-4 text-[13px] font-medium"
445
+ >
446
+ <span className="hidden sm:inline">Đóng</span>
447
+ <X className="h-4 w-4 sm:hidden" />
448
+ </Button>
449
+ <Button
450
+ onClick={handleSave}
451
+ disabled={isSubmitting}
452
+ className="bg-primary hover:bg-primary/90 text-primary-foreground min-w-[80px] h-9 px-5 gap-2 font-medium text-[13px] rounded-md"
453
+ >
454
+ {isSubmitting ? (
455
+ <Loader2 className="h-4 w-4 animate-spin" />
456
+ ) : (
457
+ <CheckCircle2 className="h-4 w-4" />
458
+ )}
459
+ <span>{mode === "create" ? "Tạo" : "Lưu"}</span>
460
+ </Button>
461
+ </div>
462
+ </div>
463
+
464
+ {/* Corporate Navy Underline Tabs */}
465
+ <nav className="flex gap-6 overflow-x-auto hide-scrollbar">
466
+ {navItems.map((item) => (
467
+ <button
468
+ key={item.id}
469
+ onClick={() => setActiveSection(item.id)}
470
+ className={cn(
471
+ "relative pb-3 text-[14px] font-medium transition-all whitespace-nowrap flex items-center gap-2",
472
+ activeSection === item.id
473
+ ? "text-primary dark:text-primary"
474
+ : "text-[#64748b] hover:text-[#0f172a] dark:text-[#94a3b8] dark:hover:text-white"
475
+ )}
476
+ >
477
+ <item.icon
478
+ className={cn(
479
+ "h-[16px] w-[16px]",
480
+ activeSection === item.id
481
+ ? "text-primary dark:text-primary"
482
+ : "text-[#94a3b8]"
483
+ )}
484
+ strokeWidth={2}
485
+ />
486
+ {item.label}
487
+ {activeSection === item.id && (
488
+ <div className="absolute bottom-0 left-0 right-0 h-[2px] bg-primary dark:bg-primary rounded-t-sm" />
489
+ )}
490
+ </button>
491
+ ))}
492
+ </nav>
493
+ </div>
494
+
495
+ {/* MAIN CONTENT AREA */}
496
+ <div className="flex-1 bg-white dark:bg-[#0f172a] overflow-y-auto overscroll-contain touch-pan-y" style={{ WebkitOverflowScrolling: 'touch' }}>
497
+ <div className="p-4 sm:p-5 max-w-4xl mx-auto space-y-4 pb-20">
498
+ {/* General Section */}
499
+ {activeSection === "general" && (
500
+ <div className="space-y-4 animate-in fade-in zoom-in-95 duration-200">
501
+
502
+ <div className="grid md:grid-cols-2 gap-x-6 gap-y-4">
503
+ <div className="space-y-1.5">
504
+ <Label className="text-[13px] font-medium text-[#334155] dark:text-[#cbd5e1]">
505
+ Họ và tên <span className="text-[#e41e3f]">*</span>
506
+ </Label>
507
+ <Input
508
+ value={profileData.name}
509
+ onChange={(e) =>
510
+ setProfileData({
511
+ ...profileData,
512
+ name: e.target.value,
513
+ })
514
+ }
515
+ placeholder="Nguyễn Văn A"
516
+ className="bg-white dark:bg-[#1e293b] border-[#e2e8f0] dark:border-[#334155] focus:ring-primary focus:border-primary rounded-md h-9 text-[13px]"
517
+ />
518
+ </div>
519
+ <div className="space-y-1.5">
520
+ <Label className="text-[13px] font-medium text-[#334155] dark:text-[#cbd5e1]">Email</Label>
521
+ <Input
522
+ value={profileData.email}
523
+ onChange={(e) =>
524
+ setProfileData({
525
+ ...profileData,
526
+ email: e.target.value,
527
+ })
528
+ }
529
+ placeholder="email@example.com"
530
+ className="bg-white dark:bg-[#1e293b] border-[#e2e8f0] dark:border-[#334155] focus:ring-primary focus:border-primary rounded-md h-9 text-[13px]"
531
+ />
532
+ </div>
533
+ <div className="space-y-1.5">
534
+ <Label className="text-[13px] font-medium text-[#334155] dark:text-[#cbd5e1]">Số điện thoại</Label>
535
+ <Input
536
+ value={profileData.phone}
537
+ onChange={(e) =>
538
+ setProfileData({
539
+ ...profileData,
540
+ phone: e.target.value,
541
+ })
542
+ }
543
+ placeholder="0912..."
544
+ className="bg-white dark:bg-[#1e293b] border-[#e2e8f0] dark:border-[#334155] focus:ring-primary focus:border-primary rounded-md h-9 text-[13px]"
545
+ />
546
+ </div>
547
+ <div className="space-y-1.5">
548
+ <Label className="text-[13px] font-medium text-[#334155] dark:text-[#cbd5e1]">Địa chỉ</Label>
549
+ <Input
550
+ value={profileData.address}
551
+ onChange={(e) =>
552
+ setProfileData({
553
+ ...profileData,
554
+ address: e.target.value,
555
+ })
556
+ }
557
+ placeholder="Số nhà, đường..."
558
+ className="bg-white dark:bg-[#1e293b] border-[#e2e8f0] dark:border-[#334155] focus:ring-primary focus:border-primary rounded-md h-9 text-[13px]"
559
+ />
560
+ </div>
561
+ {/* Status Toggle */}
562
+ {viewMode === "admin" && (
563
+ <div className="space-y-1.5">
564
+ <Label className="text-[13px] font-medium text-[#334155] dark:text-[#cbd5e1]">Trạng thái</Label>
565
+ <div className="flex items-center gap-3 h-9 px-3 rounded-[6px] border border-[#ced0d4] dark:border-[#333840] bg-[#ffffff] dark:bg-[#0a1317]">
566
+ <Switch
567
+ id="user-active-switch"
568
+ checked={isActive}
569
+ onCheckedChange={(checked) =>
570
+ setProfileData({
571
+ ...profileData,
572
+ status: checked ? "active" : "inactive",
573
+ })
574
+ }
575
+ />
576
+ <span
577
+ className={cn(
578
+ "text-[13px] font-bold tracking-[-0.13px]",
579
+ isActive ? "text-[#31a24c]" : "text-[#444950]",
580
+ )}
581
+ >
582
+ {isActive ? "Hoạt động" : "Đã khóa"}
583
+ </span>
584
+ </div>
585
+ </div>
586
+ )}
587
+ {/* Additional Info for All */}
588
+ <div className="space-y-1.5">
589
+ <Label className="text-[13px] font-medium text-[#334155] dark:text-[#cbd5e1]">Ngày sinh</Label>
590
+ <Input
591
+ type="date"
592
+ value={profileData.birthday}
593
+ onChange={(e) =>
594
+ setProfileData({
595
+ ...profileData,
596
+ birthday: e.target.value,
597
+ })
598
+ }
599
+ className="bg-white dark:bg-[#1e293b] border-[#e2e8f0] dark:border-[#334155] focus:ring-primary focus:border-primary rounded-md h-9 text-[13px]"
600
+ />
601
+ </div>
602
+ <div className="space-y-1.5">
603
+ <Label className="text-[13px] font-medium text-[#334155] dark:text-[#cbd5e1]">Giới tính</Label>
604
+ <Select
605
+ value={profileData.gender}
606
+ onValueChange={(val) =>
607
+ setProfileData({ ...profileData, gender: val })
608
+ }
609
+ >
610
+ <SelectTrigger className="bg-white dark:bg-[#1e293b] border-[#e2e8f0] dark:border-[#334155] focus:ring-primary focus:border-primary rounded-md h-9 text-[13px]">
611
+ <SelectValue placeholder="Chọn giới tính" />
612
+ </SelectTrigger>
613
+ <SelectContent>
614
+ <SelectItem value="male">Nam</SelectItem>
615
+ <SelectItem value="female">Nữ</SelectItem>
616
+ <SelectItem value="other">Khác</SelectItem>
617
+ </SelectContent>
618
+ </Select>
619
+ </div>
620
+ <div className="space-y-1.5">
621
+ <Label className="text-[13px] font-medium text-[#334155] dark:text-[#cbd5e1]">CCCD/CMND</Label>
622
+ <Input
623
+ value={profileData.idNumber}
624
+ onChange={(e) =>
625
+ setProfileData({
626
+ ...profileData,
627
+ idNumber: e.target.value,
628
+ })
629
+ }
630
+ className="bg-white dark:bg-[#1e293b] border-[#e2e8f0] dark:border-[#334155] focus:ring-primary focus:border-primary rounded-md h-9 text-[13px]"
631
+ />
632
+ </div>
633
+
634
+ {/* Customer Specific */}
635
+ {isCustomer && (
636
+ <div className="space-y-1.5">
637
+ <Label className="text-[13px] font-medium text-[#334155] dark:text-[#cbd5e1]">
638
+ Nhóm khách hàng
639
+ </Label>
640
+ <Select
641
+ value={profileData.groupId}
642
+ onValueChange={(val) =>
643
+ setProfileData({ ...profileData, groupId: val })
644
+ }
645
+ >
646
+ <SelectTrigger className="bg-white dark:bg-[#1e293b] border-[#e2e8f0] dark:border-[#334155] focus:ring-primary focus:border-primary rounded-md h-9 text-[13px]">
647
+ <SelectValue placeholder="Chọn nhóm" />
648
+ </SelectTrigger>
649
+ <SelectContent>
650
+ {customerGroups?.map((g) => (
651
+ <SelectItem key={g.id} value={g.id}>
652
+ {g.name}
653
+ </SelectItem>
654
+ ))}
655
+ </SelectContent>
656
+ </Select>
657
+ </div>
658
+ )}
659
+ </div>
660
+
661
+ {isCustomer && (
662
+ <div className="pt-4">
663
+ <div className="flex items-center justify-between mb-4 border-b border-[#dee3e9] pb-2">
664
+ <h3 className="text-[18px] font-bold tracking-[-0.14px] text-[#0a1317] dark:text-white">
665
+ Tài khoản đăng nhập
666
+ </h3>
667
+ <Switch
668
+ checked={enableAccount}
669
+ onCheckedChange={setEnableAccount}
670
+ />
671
+ </div>
672
+
673
+ {enableAccount && (
674
+ <div className="space-y-4 animate-in fade-in slide-in-from-top-2">
675
+ <div className="grid md:grid-cols-2 gap-x-6 gap-y-4">
676
+ <div className="space-y-1.5 col-span-2 md:col-span-1">
677
+ <Label className="text-[13px] font-medium text-[#334155] dark:text-[#cbd5e1]">
678
+ Mật khẩu
679
+ </Label>
680
+ <div className="relative">
681
+ <Input
682
+ type={showPassword ? "text" : "password"}
683
+ value={passwordData.password}
684
+ onChange={(e) =>
685
+ setPasswordData((prev) => ({
686
+ ...prev,
687
+ password: e.target.value,
688
+ }))
689
+ }
690
+ placeholder="Nhập mật khẩu"
691
+ className="pr-10 bg-[#ffffff] dark:bg-[#0a1317] border-[#ced0d4] dark:border-[#333840] focus:ring-[#0064e0] focus:border-[#0064e0] rounded-[6px] h-9 text-[14px]"
692
+ />
693
+ <Button
694
+ type="button"
695
+ variant="ghost"
696
+ size="icon"
697
+ className="absolute right-0 top-0 h-full w-9 text-[#444950] hover:text-[#0a1317]"
698
+ onClick={() =>
699
+ setShowPassword(!showPassword)
700
+ }
701
+ >
702
+ {showPassword ? (
703
+ <EyeOff className="h-4 w-4" />
704
+ ) : (
705
+ <Eye className="h-4 w-4" />
706
+ )}
707
+ </Button>
708
+ </div>
709
+ <p className="text-[12px] text-[#444950] mt-1.5 flex items-center gap-1.5">
710
+ <span className="inline-block w-1.5 h-1.5 rounded-full bg-[#0064e0]" />
711
+ Mặc định:{" "}
712
+ <span className="font-mono font-bold text-[#1c1e21] dark:text-white">
713
+ Goeat@123456
714
+ </span>
715
+ </p>
716
+ </div>
717
+ </div>
718
+
719
+ <div className="space-y-2 pt-2">
720
+ <Label className="text-[13px] font-medium text-[#334155] dark:text-[#cbd5e1]">
721
+ Phân quyền
722
+ </Label>
723
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
724
+ {filteredRoles.map((role) => {
725
+ const isSelected = selectedRoles.includes(
726
+ role.code,
727
+ );
728
+ return (
729
+ <div
730
+ key={role.code}
731
+ className={cn(
732
+ "flex items-start gap-3 p-3 rounded-md border cursor-pointer hover:bg-slate-50 transition-colors",
733
+ isSelected
734
+ ? "border-blue-500 bg-blue-50/50"
735
+ : "bg-background border-slate-200 dark:border-slate-800",
736
+ )}
737
+ onClick={() => {
738
+ setSelectedRoles((prev) =>
739
+ prev.includes(role.code)
740
+ ? prev.filter(
741
+ (c) => c !== role.code,
742
+ )
743
+ : [...prev, role.code],
744
+ );
745
+ }}
746
+ >
747
+ <Checkbox
748
+ checked={isSelected}
749
+ className="mt-1 data-[state=checked]:bg-blue-600 data-[state=checked]:border-blue-600"
750
+ />
751
+ <div className="space-y-0.5">
752
+ <p className="text-sm font-medium text-slate-900 dark:text-slate-100">
753
+ {role.name}
754
+ </p>
755
+ <p className="text-xs text-muted-foreground line-clamp-1">
756
+ {role.description || role.code}
757
+ </p>
758
+ </div>
759
+ </div>
760
+ );
761
+ })}
762
+ </div>
763
+ </div>
764
+ </div>
765
+ )}
766
+ </div>
767
+ )}
768
+ </div>
769
+ )}
770
+
771
+ {/* Work Section */}
772
+ {activeSection === "work" && (
773
+ <div className="space-y-4 animate-in fade-in zoom-in-95 duration-200">
774
+ <div className="grid md:grid-cols-2 gap-x-8 gap-y-6">
775
+ <div className="space-y-2">
776
+ <Label className="text-[13px] font-medium text-[#41454d] dark:text-[#9297a0]">Phòng ban</Label>
777
+ <Select
778
+ value={profileData.departmentId}
779
+ onValueChange={(val) =>
780
+ setProfileData({
781
+ ...profileData,
782
+ departmentId: val,
783
+ })
784
+ }
785
+ >
786
+ <SelectTrigger className="bg-[#ffffff] dark:bg-[#181d26] border-[#dddddd] dark:border-[#333840] focus:ring-[#458fff] focus:border-[#458fff] shadow-sm rounded-[6px]">
787
+ <SelectValue placeholder="Chọn phòng ban" />
788
+ </SelectTrigger>
789
+ <SelectContent>
790
+ {departments?.map((d) => (
791
+ <SelectItem key={d.id} value={d.id}>
792
+ {d.name}
793
+ </SelectItem>
794
+ ))}
795
+ </SelectContent>
796
+ </Select>
797
+ </div>
798
+ <div className="space-y-2">
799
+ <Label className="text-[13px] font-medium text-[#41454d] dark:text-[#9297a0]">Chức danh</Label>
800
+ <Select
801
+ value={profileData.jobTitleId}
802
+ onValueChange={(val) =>
803
+ setProfileData({ ...profileData, jobTitleId: val })
804
+ }
805
+ >
806
+ <SelectTrigger className="bg-[#ffffff] dark:bg-[#181d26] border-[#dddddd] dark:border-[#333840] focus:ring-[#458fff] focus:border-[#458fff] shadow-sm rounded-[6px]">
807
+ <SelectValue placeholder="Chọn chức danh" />
808
+ </SelectTrigger>
809
+ <SelectContent>
810
+ {jobTitles?.map((j) => (
811
+ <SelectItem key={j.id} value={j.id}>
812
+ {j.name}
813
+ </SelectItem>
814
+ ))}
815
+ </SelectContent>
816
+ </Select>
817
+ </div>
818
+ <div className="col-span-2 space-y-3 pt-2">
819
+ <Label className="text-[13px] font-medium text-[#41454d] dark:text-[#9297a0]">Chi nhánh làm việc</Label>
820
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
821
+ {branches?.map((branch) => {
822
+ const isSelected = selectedBranches.includes(branch.id);
823
+ return (
824
+ <div
825
+ key={branch.id}
826
+ className={cn(
827
+ "flex items-center gap-3 p-3 rounded-md border cursor-pointer hover:bg-slate-50 transition-colors",
828
+ isSelected
829
+ ? "border-blue-500 bg-blue-50/50"
830
+ : "bg-background border-slate-200 dark:border-slate-800",
831
+ )}
832
+ onClick={() => {
833
+ setSelectedBranches((prev) =>
834
+ prev.includes(branch.id)
835
+ ? prev.filter((id) => id !== branch.id)
836
+ : [...prev, branch.id],
837
+ );
838
+ }}
839
+ >
840
+ <Checkbox
841
+ checked={isSelected}
842
+ className="data-[state=checked]:bg-blue-600 data-[state=checked]:border-blue-600"
843
+ />
844
+ <span className="text-sm font-medium text-slate-900 dark:text-slate-100">
845
+ {branch.name}
846
+ </span>
847
+ </div>
848
+ );
849
+ })}
850
+ </div>
851
+ </div>
852
+ </div>
853
+ </div>
854
+ )}
855
+
856
+ {activeSection === "supplier_link" && (
857
+ <div className="space-y-4 animate-in fade-in zoom-in-95 duration-200">
858
+ <div className="max-w-2xl space-y-6">
859
+ <div className="space-y-2">
860
+ <Label className="text-[13px] font-medium text-[#41454d] dark:text-[#9297a0]">
861
+ Chọn nhà cung cấp
862
+ </Label>
863
+ <Select
864
+ value={profileData.supplierId}
865
+ onValueChange={(val) =>
866
+ setProfileData({ ...profileData, supplierId: val })
867
+ }
868
+ >
869
+ <SelectTrigger className="bg-[#ffffff] dark:bg-[#181d26] border-[#dddddd] dark:border-[#333840] focus:ring-[#458fff] focus:border-[#458fff] shadow-sm rounded-[6px]">
870
+ <SelectValue placeholder="Chọn nhà cung cấp" />
871
+ </SelectTrigger>
872
+ <SelectContent>
873
+ {suppliers?.map((s) => (
874
+ <SelectItem key={s.id} value={s.id}>
875
+ {s.name}
876
+ </SelectItem>
877
+ ))}
878
+ </SelectContent>
879
+ </Select>
880
+ </div>
881
+ <div className="rounded-md bg-blue-50 p-4 text-sm text-blue-700 flex items-start gap-3">
882
+ <Store className="h-5 w-5 mt-0.5 shrink-0" />
883
+ <p>
884
+ Tài khoản này sẽ được liên kết với nhà cung cấp đã
885
+ chọn. Người dùng sẽ có quyền truy cập vào dữ liệu và
886
+ chức năng dành riêng cho nhà cung cấp này.
887
+ </p>
888
+ </div>
889
+ </div>
890
+ </div>
891
+ )}
892
+
893
+ {activeSection === "roles" && (
894
+ <div className="animate-in fade-in zoom-in-95 duration-200">
895
+ <div className="sticky -top-4 sm:-top-5 pt-2 sm:pt-2 pb-4 -mx-4 sm:-mx-5 px-4 sm:px-5 bg-white/95 dark:bg-[#0f172a]/95 backdrop-blur-sm z-10 border-b border-[#e2e8f0] dark:border-[#334155] mb-4 shadow-sm">
896
+ <div className="relative max-w-md">
897
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-[#64748b]" />
898
+ <Input
899
+ placeholder="Tìm kiếm vai trò..."
900
+ value={roleSearchQuery}
901
+ onChange={(e) => setRoleSearchQuery(e.target.value)}
902
+ className="pl-9 bg-white dark:bg-[#1e293b] border-[#e2e8f0] dark:border-[#334155] focus:ring-primary focus:border-primary shadow-sm rounded-md h-9 text-[13px]"
903
+ />
904
+ </div>
905
+ </div>
906
+
907
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
908
+ {filteredRoles.map((role) => {
909
+ const isSelected = selectedRoles.includes(role.code);
910
+ return (
911
+ <div
912
+ key={role.code}
913
+ className={cn(
914
+ "flex items-start gap-3 p-3 rounded-md border cursor-pointer hover:bg-slate-50 transition-colors",
915
+ isSelected
916
+ ? "border-blue-500 bg-blue-50/50"
917
+ : "bg-background border-slate-200 dark:border-slate-800",
918
+ )}
919
+ onClick={() => {
920
+ setSelectedRoles((prev) =>
921
+ prev.includes(role.code)
922
+ ? prev.filter((c) => c !== role.code)
923
+ : [...prev, role.code],
924
+ );
925
+ }}
926
+ >
927
+ <Checkbox
928
+ checked={isSelected}
929
+ className="mt-1 data-[state=checked]:bg-blue-600 data-[state=checked]:border-blue-600"
930
+ />
931
+ <div className="space-y-0.5">
932
+ <p className="text-sm font-medium text-slate-900 dark:text-slate-100">
933
+ {role.name}
934
+ </p>
935
+ <p className="text-xs text-muted-foreground line-clamp-1">
936
+ {role.description || role.code}
937
+ </p>
938
+ </div>
939
+ </div>
940
+ );
941
+ })}
942
+ </div>
943
+ </div>
944
+ )}
945
+
946
+ {activeSection === "security" && (
947
+ <div className="space-y-4 animate-in fade-in zoom-in-95 duration-200">
948
+ <div className="max-w-xl space-y-6">
949
+ <div className="flex items-center gap-3 rounded-md bg-amber-50 p-4 text-sm text-amber-900 border border-amber-100">
950
+ <ShieldCheck className="h-5 w-5 shrink-0 text-amber-600" />
951
+ <span>
952
+ Mật khẩu mạnh giúp bảo vệ tài khoản khỏi truy cập trái
953
+ phép. Sử dụng ít nhất 6 ký tự bao gồm chữ và số.
954
+ </span>
955
+ </div>
956
+ <div className="grid md:grid-cols-2 gap-x-8 gap-y-6">
957
+ <div className="space-y-2">
958
+ <Label className="text-[13px] font-medium text-[#41454d] dark:text-[#9297a0]">Mật khẩu mới</Label>
959
+ <div className="relative">
960
+ <Input
961
+ type={showPassword ? "text" : "password"}
962
+ value={passwordData.password}
963
+ onChange={(e) =>
964
+ setPasswordData((prev) => ({
965
+ ...prev,
966
+ password: e.target.value,
967
+ }))
968
+ }
969
+ className="pr-10 bg-[#ffffff] dark:bg-[#181d26] border-[#dddddd] dark:border-[#333840] focus:ring-[#458fff] focus:border-[#458fff] shadow-sm rounded-[6px]"
970
+ />
971
+ <Button
972
+ type="button"
973
+ variant="ghost"
974
+ size="icon"
975
+ className="absolute right-0 top-0 h-full w-9 text-muted-foreground hover:text-foreground"
976
+ onClick={() => setShowPassword(!showPassword)}
977
+ >
978
+ {showPassword ? (
979
+ <EyeOff className="h-4 w-4" />
980
+ ) : (
981
+ <Eye className="h-4 w-4" />
982
+ )}
983
+ </Button>
984
+ </div>
985
+ </div>
986
+ <div className="space-y-2">
987
+ <Label className="text-[13px] font-medium text-[#41454d] dark:text-[#9297a0]">
988
+ Xác nhận mật khẩu
989
+ </Label>
990
+ <Input
991
+ type="password"
992
+ value={passwordData.confirm}
993
+ onChange={(e) =>
994
+ setPasswordData((prev) => ({
995
+ ...prev,
996
+ confirm: e.target.value,
997
+ }))
998
+ }
999
+ className="bg-[#ffffff] dark:bg-[#181d26] border-[#dddddd] dark:border-[#333840] focus:ring-[#458fff] focus:border-[#458fff] shadow-sm rounded-[6px]"
1000
+ />
1001
+ </div>
1002
+ </div>
1003
+ </div>
1004
+ </div>
1005
+ )}
1006
+
1007
+ {error && (
1008
+ <div className="p-4 text-sm text-destructive bg-destructive/5 border border-destructive/20 rounded-md flex items-center gap-2 animate-in fade-in slide-in-from-bottom-2">
1009
+ <Ban className="h-4 w-4" />
1010
+ {error}
1011
+ </div>
1012
+ )}
1013
+ </div>
1014
+ </div>
1015
+ </div>
1016
+ </DialogContent>
1017
+ </Dialog>
1018
+ );
1019
+ }