@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,271 @@
1
+ "use client";
2
+ import { useEffect } from "react";
3
+ import { useForm } from "react-hook-form";
4
+ import { zodResolver } from "@hookform/resolvers/zod";
5
+ import * as z from "zod";
6
+
7
+ import * as DialogPrimitive from "@radix-ui/react-dialog";
8
+ import { X } from "lucide-react";
9
+ import { cn } from "../utils";
10
+ import type { QuickAccessItem } from "./types";
11
+ import { iconMap } from "./constants";
12
+
13
+ const AVAILABLE_ICONS = Object.keys(iconMap);
14
+ const AVAILABLE_COLORS = [
15
+ "blue",
16
+ "green",
17
+ "purple",
18
+ "orange",
19
+ "pink",
20
+ "indigo",
21
+ "teal",
22
+ "red",
23
+ ];
24
+
25
+ const formSchema = z.object({
26
+ label: z.string().min(1, "Vui lòng nhập tên lối tắt"),
27
+ href: z.string().min(1, "Vui lòng nhập đường dẫn"),
28
+ icon: z.string(),
29
+ color: z.string(),
30
+ });
31
+
32
+ type FormValues = z.infer<typeof formSchema>;
33
+
34
+ interface QuickAccessDialogProps {
35
+ open: boolean;
36
+ onOpenChange: (open: boolean) => void;
37
+ onSubmit: (data: Omit<QuickAccessItem, "id">) => void;
38
+ initialData?: QuickAccessItem;
39
+ availableFeatures?: Array<{ label: string; href: string; icon?: string }>;
40
+ }
41
+
42
+ export function QuickAccessDialog({
43
+ open,
44
+ onOpenChange,
45
+ onSubmit,
46
+ initialData,
47
+ availableFeatures,
48
+ }: QuickAccessDialogProps) {
49
+ const form = useForm<FormValues>({
50
+ resolver: zodResolver(formSchema),
51
+ defaultValues: {
52
+ label: "",
53
+ href: "",
54
+ icon: "document-text",
55
+ color: "blue",
56
+ },
57
+ });
58
+
59
+ // Reset form when dialog opens/closes or initialData changes
60
+ useEffect(() => {
61
+ if (open) {
62
+ form.reset(
63
+ initialData
64
+ ? {
65
+ label: initialData.label,
66
+ href: initialData.href,
67
+ icon: initialData.icon,
68
+ color: initialData.color,
69
+ }
70
+ : {
71
+ label: "",
72
+ href: "",
73
+ icon: "document-text",
74
+ color: "blue",
75
+ },
76
+ );
77
+ }
78
+ }, [open, initialData, form]);
79
+
80
+ const handleSubmit = (values: FormValues) => {
81
+ onSubmit(values);
82
+ onOpenChange(false);
83
+ };
84
+
85
+ const handleFeatureChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
86
+ const selectedHref = e.target.value;
87
+ const feature = availableFeatures?.find((f) => f.href === selectedHref);
88
+
89
+ if (feature) {
90
+ form.setValue("href", feature.href);
91
+ form.setValue("label", feature.label);
92
+ if (feature.icon && iconMap[feature.icon]) {
93
+ form.setValue("icon", feature.icon);
94
+ } else if (feature.icon && iconMap[feature.icon.replace(/-/g, "")]) {
95
+ // Attempt fuzzy match or mapping if needed, theoretically icon names should match
96
+ }
97
+ }
98
+ };
99
+
100
+ return (
101
+ <DialogPrimitive.Root open={open} onOpenChange={onOpenChange}>
102
+ <DialogPrimitive.Portal>
103
+ <DialogPrimitive.Overlay className="fixed inset-0 z-50 bg-black/50 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0" />
104
+ <DialogPrimitive.Content className="fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg">
105
+ <div className="flex flex-col space-y-1.5 text-center sm:text-left">
106
+ <DialogPrimitive.Title className="text-lg font-semibold leading-none tracking-tight">
107
+ {initialData ? "Sửa lối tắt" : "Thêm lối tắt mới"}
108
+ </DialogPrimitive.Title>
109
+ <DialogPrimitive.Description className="text-sm text-muted-foreground">
110
+ Tạo lối tắt để truy cập nhanh các tính năng thường dùng
111
+ </DialogPrimitive.Description>
112
+ </div>
113
+
114
+ <form
115
+ onSubmit={form.handleSubmit(handleSubmit)}
116
+ className="space-y-4"
117
+ >
118
+ <div className="grid gap-4 py-4">
119
+ {/* Feature Selection if available */}
120
+ {availableFeatures && availableFeatures.length > 0 && (
121
+ <div className="grid grid-cols-4 items-center gap-4">
122
+ <label
123
+ htmlFor="feature-select"
124
+ className="text-right text-sm font-medium"
125
+ >
126
+ Chức năng
127
+ </label>
128
+ <div className="col-span-3">
129
+ <select
130
+ id="feature-select"
131
+ className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
132
+ onChange={handleFeatureChange}
133
+ defaultValue={initialData?.href || ""}
134
+ >
135
+ <option value="">Chọn chức năng...</option>
136
+ {availableFeatures.map((f) => (
137
+ <option key={f.href} value={f.href}>
138
+ {f.label}
139
+ </option>
140
+ ))}
141
+ </select>
142
+ </div>
143
+ </div>
144
+ )}
145
+
146
+ <div className="grid grid-cols-4 items-center gap-4">
147
+ <label
148
+ htmlFor="label"
149
+ className="text-right text-sm font-medium"
150
+ >
151
+ Tên lối tắt
152
+ </label>
153
+ <div className="col-span-3">
154
+ <input
155
+ id="label"
156
+ className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
157
+ {...form.register("label")}
158
+ />
159
+ {form.formState.errors.label && (
160
+ <p className="text-xs text-red-500 mt-1">
161
+ {form.formState.errors.label.message}
162
+ </p>
163
+ )}
164
+ </div>
165
+ </div>
166
+
167
+ {/* Show HREF input only if NO features available (fallback) or if we want to show it as readonly?
168
+ The requirement says "only allow ... in the list".
169
+ So if list is present, HIDE the href input or make it readOnly hidden.
170
+ Let's hide it but keep it registered in form so validation passes.
171
+ */}
172
+ <div
173
+ className={cn(
174
+ "grid grid-cols-4 items-center gap-4",
175
+ availableFeatures?.length ? "hidden" : "",
176
+ )}
177
+ >
178
+ <label
179
+ htmlFor="href"
180
+ className="text-right text-sm font-medium"
181
+ >
182
+ Đường dẫn
183
+ </label>
184
+ <div className="col-span-3">
185
+ <input
186
+ id="href"
187
+ className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
188
+ {...form.register("href")}
189
+ readOnly={!!availableFeatures?.length}
190
+ />
191
+ {form.formState.errors.href && (
192
+ <p className="text-xs text-red-500 mt-1">
193
+ {form.formState.errors.href.message}
194
+ </p>
195
+ )}
196
+ </div>
197
+ </div>
198
+
199
+ <div className="grid grid-cols-4 items-start gap-4">
200
+ <label className="text-right text-sm font-medium pt-2">
201
+ Icon
202
+ </label>
203
+ <div className="col-span-3 flex flex-wrap gap-2">
204
+ {AVAILABLE_ICONS.map((icon) => (
205
+ <button
206
+ key={icon}
207
+ type="button"
208
+ onClick={() => form.setValue("icon", icon)}
209
+ className={cn(
210
+ "flex h-8 w-8 items-center justify-center rounded-md border transition-colors",
211
+ form.watch("icon") === icon
212
+ ? "border-primary bg-primary/10 text-primary"
213
+ : "border-input hover:bg-muted",
214
+ )}
215
+ >
216
+ {iconMap[icon]}
217
+ </button>
218
+ ))}
219
+ </div>
220
+ </div>
221
+
222
+ <div className="grid grid-cols-4 items-start gap-4">
223
+ <label className="text-right text-sm font-medium pt-2">
224
+ Màu sắc
225
+ </label>
226
+ <div className="col-span-3 flex flex-wrap gap-2">
227
+ {AVAILABLE_COLORS.map((color) => (
228
+ <button
229
+ key={color}
230
+ type="button"
231
+ onClick={() => form.setValue("color", color)}
232
+ className={cn(
233
+ "h-8 w-8 rounded-full border-2 transition-all",
234
+ `bg-${color}-500`,
235
+ form.watch("color") === color
236
+ ? "border-primary scale-110 shadow-sm"
237
+ : "border-transparent hover:scale-105",
238
+ )}
239
+ aria-label={color}
240
+ />
241
+ ))}
242
+ </div>
243
+ </div>
244
+ </div>
245
+
246
+ <div className="flex justify-end gap-2">
247
+ <DialogPrimitive.Close asChild>
248
+ <button
249
+ type="button"
250
+ className="inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground h-9 px-4 py-2"
251
+ >
252
+ Hủy
253
+ </button>
254
+ </DialogPrimitive.Close>
255
+ <button
256
+ type="submit"
257
+ className="inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground shadow hover:bg-primary/90 h-9 px-4 py-2"
258
+ >
259
+ {initialData ? "Lưu thay đổi" : "Thêm mới"}
260
+ </button>
261
+ </div>
262
+ </form>
263
+ <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
264
+ <X className="h-4 w-4" />
265
+ <span className="sr-only">Close</span>
266
+ </DialogPrimitive.Close>
267
+ </DialogPrimitive.Content>
268
+ </DialogPrimitive.Portal>
269
+ </DialogPrimitive.Root>
270
+ );
271
+ }
@@ -0,0 +1,267 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { motion, AnimatePresence } from "framer-motion";
5
+ import { Plus, X, Pencil, GripVertical } from "lucide-react";
6
+ import { cn } from "../utils";
7
+ import type { QuickAccessItem } from "./types";
8
+ import { QuickAccessDialog } from "./quick-access-dialog";
9
+
10
+ interface QuickAccessMenuProps {
11
+ items: QuickAccessItem[];
12
+ onItemClick?: (item: QuickAccessItem) => void;
13
+ onItemsChange?: (items: QuickAccessItem[]) => void;
14
+ className?: string;
15
+ editable?: boolean;
16
+ availableFeatures?: Array<{ label: string; href: string; icon?: string }>;
17
+ }
18
+
19
+ const containerVariants = {
20
+ hidden: { opacity: 0 },
21
+ visible: {
22
+ opacity: 1,
23
+ transition: {
24
+ staggerChildren: 0.05,
25
+ delayChildren: 0.1,
26
+ },
27
+ },
28
+ };
29
+
30
+ const itemVariants = {
31
+ hidden: { opacity: 0, scale: 0.9 },
32
+ visible: {
33
+ opacity: 1,
34
+ scale: 1,
35
+ transition: {
36
+ duration: 0.2,
37
+ ease: "easeOut",
38
+ },
39
+ },
40
+ };
41
+
42
+ import { iconMap, colorClasses } from "./constants";
43
+
44
+ export function QuickAccessMenu({
45
+ items,
46
+ onItemClick,
47
+ onItemsChange,
48
+ className,
49
+ editable = false,
50
+ availableFeatures,
51
+ }: QuickAccessMenuProps) {
52
+ const [isEditing, setIsEditing] = useState(false);
53
+ const [dialogOpen, setDialogOpen] = useState(false);
54
+ const [editingItem, setEditingItem] = useState<QuickAccessItem | undefined>(
55
+ undefined,
56
+ );
57
+
58
+ const handleAddStart = () => {
59
+ setEditingItem(undefined);
60
+ setDialogOpen(true);
61
+ };
62
+
63
+ const handleEditStart = (item: QuickAccessItem, e: React.MouseEvent) => {
64
+ e.stopPropagation();
65
+ setEditingItem(item);
66
+ setDialogOpen(true);
67
+ };
68
+
69
+ const handleDelete = (itemId: string, e: React.MouseEvent) => {
70
+ e.stopPropagation();
71
+ if (confirm("Bạn có chắc muốn xóa lối tắt này?")) {
72
+ onItemsChange?.(items.filter((item) => item.id !== itemId));
73
+ }
74
+ };
75
+
76
+ const handleSave = (data: Omit<QuickAccessItem, "id">) => {
77
+ // Check for duplicates
78
+ const isDuplicate = items.some(
79
+ (item) => item.href === data.href && item.id !== editingItem?.id,
80
+ );
81
+
82
+ if (isDuplicate) {
83
+ alert("Lối tắt này đã tồn tại!");
84
+ return;
85
+ }
86
+
87
+ if (editingItem) {
88
+ // Edit existing
89
+ onItemsChange?.(
90
+ items.map((item) =>
91
+ item.id === editingItem.id ? { ...item, ...data } : item,
92
+ ),
93
+ );
94
+ } else {
95
+ // Add new
96
+ const newItem: QuickAccessItem = {
97
+ ...data,
98
+ id: `custom-${Date.now()}`,
99
+ };
100
+ onItemsChange?.([...items, newItem]);
101
+ }
102
+ };
103
+
104
+ if (items.length === 0 && !editable) {
105
+ return null;
106
+ }
107
+
108
+ return (
109
+ <div className={cn("space-y-4", className)}>
110
+ <div className="flex items-center justify-between">
111
+ <h3 className="text-slate-900 dark:text-white text-lg font-bold">
112
+ Quick Shortcuts
113
+ </h3>
114
+ </div>
115
+
116
+ <motion.div
117
+ variants={containerVariants}
118
+ initial="hidden"
119
+ animate="visible"
120
+ // @ts-ignore className is valid for motion.div
121
+ className="grid grid-cols-2 gap-4 sm:grid-cols-4"
122
+ >
123
+ <AnimatePresence mode="popLayout">
124
+ {items.map((item) => (
125
+ <motion.button
126
+ key={item.id}
127
+ variants={itemVariants}
128
+ whileHover={{ scale: 1.02 }}
129
+ whileTap={{ scale: 0.98 }}
130
+ // @ts-ignore onClick and className are valid for motion.button
131
+ onClick={() => {
132
+ if (!isEditing) {
133
+ onItemClick?.(item);
134
+ }
135
+ }}
136
+ className={cn(
137
+ "group relative flex flex-col items-center justify-center gap-3 bg-white dark:bg-slate-800 p-5 rounded-xl border border-slate-200 dark:border-slate-700 shadow-sm hover:shadow-md hover:border-primary/50 transition-all text-left",
138
+ isEditing && "cursor-default opacity-90 hover:opacity-100",
139
+ )}
140
+ >
141
+ {isEditing && (
142
+ <>
143
+ <div
144
+ onClick={(e) => handleDelete(item.id, e)}
145
+ className="absolute -right-2 -top-2 z-10 flex h-6 w-6 cursor-pointer items-center justify-center rounded-full bg-destructive text-white shadow-sm ring-2 ring-white hover:bg-destructive/90"
146
+ >
147
+ <X className="h-3 w-3" />
148
+ </div>
149
+ <div
150
+ onClick={(e) => handleEditStart(item, e)}
151
+ className="absolute -left-2 -top-2 z-10 flex h-6 w-6 cursor-pointer items-center justify-center rounded-full bg-background text-foreground shadow-sm ring-2 ring-white hover:bg-muted"
152
+ >
153
+ <Pencil className="h-3 w-3" />
154
+ </div>
155
+ </>
156
+ )}
157
+
158
+ {editable && (
159
+ <span
160
+ onClick={() => setIsEditing(!isEditing)}
161
+ className="absolute top-2 right-2 text-slate-300 dark:text-slate-600 cursor-move opacity-100 hover:text-primary transition-colors"
162
+ >
163
+ <GripVertical size={18} />
164
+ </span>
165
+ )}
166
+
167
+ <div
168
+ className={cn(
169
+ "size-12 rounded-full flex items-center justify-center group-hover:scale-110 transition-transform",
170
+ colorClasses[item.color || "blue"] || colorClasses.blue,
171
+ )}
172
+ >
173
+ {iconMap[item.icon] || iconMap["document-text"]}
174
+ </div>
175
+ <span className="text-slate-700 dark:text-slate-200 font-semibold text-sm">
176
+ {item.label}
177
+ </span>
178
+ </motion.button>
179
+ ))}
180
+
181
+ {editable && (
182
+ <motion.button
183
+ variants={itemVariants}
184
+ // @ts-ignore
185
+ onClick={handleAddStart}
186
+ whileHover={{ scale: 1.02 }}
187
+ whileTap={{ scale: 0.98 }}
188
+ // @ts-ignore
189
+ className="flex flex-col items-center justify-center gap-2 bg-slate-50 dark:bg-slate-800/50 p-5 rounded-xl border-2 border-dashed border-slate-300 dark:border-slate-600 hover:border-primary hover:bg-slate-100 dark:hover:bg-slate-800 transition-all text-slate-500 hover:text-primary cursor-pointer group h-full min-h-[140px]"
190
+ >
191
+ <Plus className="h-8 w-8 group-hover:scale-110 transition-transform" />
192
+ <span className="text-sm font-medium">Add Shortcut</span>
193
+ </motion.button>
194
+ )}
195
+ </AnimatePresence>
196
+ </motion.div>
197
+
198
+ <QuickAccessDialog
199
+ open={dialogOpen}
200
+ onOpenChange={setDialogOpen}
201
+ onSubmit={handleSave}
202
+ initialData={editingItem}
203
+ availableFeatures={availableFeatures}
204
+ />
205
+ </div>
206
+ );
207
+ }
208
+
209
+ // Default quick access items
210
+ export const DEFAULT_QUICK_ACCESS: QuickAccessItem[] = [
211
+ {
212
+ id: "pos",
213
+ label: "Bán hàng",
214
+ href: "/sales/pos",
215
+ icon: "shopping-cart",
216
+ color: "green",
217
+ },
218
+ {
219
+ id: "orders",
220
+ label: "Đơn hàng",
221
+ href: "/sales-orders",
222
+ icon: "clipboard-list",
223
+ color: "blue",
224
+ },
225
+ {
226
+ id: "customers",
227
+ label: "Khách hàng",
228
+ href: "/customers",
229
+ icon: "users",
230
+ color: "purple",
231
+ },
232
+ {
233
+ id: "inventory",
234
+ label: "Tồn kho",
235
+ href: "/inventory",
236
+ icon: "package",
237
+ color: "orange",
238
+ },
239
+ {
240
+ id: "reports",
241
+ label: "Báo cáo",
242
+ href: "/reports",
243
+ icon: "chart-bar",
244
+ color: "teal",
245
+ },
246
+ {
247
+ id: "finance",
248
+ label: "Thu chi",
249
+ href: "/finance",
250
+ icon: "currency-dollar",
251
+ color: "pink",
252
+ },
253
+ {
254
+ id: "products",
255
+ label: "Sản phẩm",
256
+ href: "/crud/products",
257
+ icon: "package",
258
+ color: "indigo",
259
+ },
260
+ {
261
+ id: "settings",
262
+ label: "Cài đặt",
263
+ href: "/admin",
264
+ icon: "cog",
265
+ color: "red",
266
+ },
267
+ ];
@@ -0,0 +1,140 @@
1
+ // Widget System Types for Home Page
2
+
3
+ export type WidgetType = "revenue" | "orders" | "customers" | "stock";
4
+
5
+ export type WidgetSize = "small" | "medium" | "large";
6
+
7
+ export interface WidgetConfig {
8
+ id: string;
9
+ type: WidgetType;
10
+ position: number;
11
+ visible: boolean;
12
+ size: WidgetSize;
13
+ settings?: Record<string, unknown>;
14
+ }
15
+
16
+ export interface WidgetProps {
17
+ config: WidgetConfig;
18
+ onRemove?: () => void;
19
+ onSettings?: () => void;
20
+ className?: string;
21
+ }
22
+
23
+ export interface WidgetData {
24
+ value: number | string;
25
+ label: string;
26
+ change?: {
27
+ value: number;
28
+ type: "increase" | "decrease";
29
+ period: string;
30
+ };
31
+ loading?: boolean;
32
+ error?: string;
33
+ }
34
+
35
+ export interface QuickAccessItem {
36
+ id: string;
37
+ label: string;
38
+ href: string;
39
+ icon: string;
40
+ color?: string;
41
+ }
42
+
43
+ export interface UserHomePreferences {
44
+ widgets: WidgetConfig[];
45
+ quickAccess: QuickAccessItem[]; // List of quick access items
46
+ }
47
+
48
+ export const DEFAULT_WIDGETS: WidgetConfig[] = [
49
+ {
50
+ id: "revenue",
51
+ type: "revenue",
52
+ position: 0,
53
+ visible: true,
54
+ size: "medium",
55
+ },
56
+ { id: "orders", type: "orders", position: 1, visible: true, size: "medium" },
57
+ {
58
+ id: "customers",
59
+ type: "customers",
60
+ position: 2,
61
+ visible: true,
62
+ size: "medium",
63
+ },
64
+ { id: "stock", type: "stock", position: 3, visible: true, size: "medium" },
65
+ ];
66
+
67
+ export const WIDGET_LABELS: Record<WidgetType, string> = {
68
+ revenue: "Doanh thu",
69
+ orders: "Đơn hàng",
70
+ customers: "Khách hàng",
71
+ stock: "Tồn kho",
72
+ };
73
+
74
+ export interface ERPFeature {
75
+ id: string;
76
+ icon: string;
77
+ title: string;
78
+ description: string;
79
+ subFeatures: string[];
80
+ color: string;
81
+ }
82
+
83
+ export const ERP_FEATURES: ERPFeature[] = [
84
+ {
85
+ id: "inventory",
86
+ icon: "📦",
87
+ title: "Quản lý Kho",
88
+ description: "Quản lý toàn diện hàng hóa và tồn kho",
89
+ subFeatures: ["Nhập/Xuất kho", "Kiểm kê", "Điều chuyển", "Cảnh báo tồn"],
90
+ color: "from-blue-500 to-cyan-500",
91
+ },
92
+ {
93
+ id: "sales",
94
+ icon: "💰",
95
+ title: "Quản lý Bán hàng",
96
+ description: "Xử lý đơn hàng và bán hàng hiệu quả",
97
+ subFeatures: ["POS", "Đơn hàng", "Hợp đồng", "Báo giá"],
98
+ color: "from-green-500 to-emerald-500",
99
+ },
100
+ {
101
+ id: "purchasing",
102
+ icon: "🛒",
103
+ title: "Quản lý Mua hàng",
104
+ description: "Quản lý nhà cung cấp và đặt hàng",
105
+ subFeatures: ["Đặt hàng NCC", "Nhập hàng", "Công nợ phải trả"],
106
+ color: "from-orange-500 to-amber-500",
107
+ },
108
+ {
109
+ id: "finance",
110
+ icon: "💵",
111
+ title: "Tài chính",
112
+ description: "Quản lý dòng tiền và công nợ",
113
+ subFeatures: ["Thu/Chi", "Sổ quỹ", "Công nợ", "Báo cáo tài chính"],
114
+ color: "from-purple-500 to-violet-500",
115
+ },
116
+ {
117
+ id: "crm",
118
+ icon: "👥",
119
+ title: "CRM",
120
+ description: "Chăm sóc và quản lý khách hàng",
121
+ subFeatures: ["Khách hàng", "Lịch sử giao dịch", "Phân loại"],
122
+ color: "from-pink-500 to-rose-500",
123
+ },
124
+ {
125
+ id: "hr",
126
+ icon: "👨‍💼",
127
+ title: "Nhân sự",
128
+ description: "Quản lý người dùng và phân quyền",
129
+ subFeatures: ["Users", "Phân quyền", "Roles"],
130
+ color: "from-indigo-500 to-blue-500",
131
+ },
132
+ {
133
+ id: "reports",
134
+ icon: "📊",
135
+ title: "Báo cáo",
136
+ description: "Thống kê và phân tích dữ liệu",
137
+ subFeatures: ["Dashboard", "Thống kê", "Export"],
138
+ color: "from-teal-500 to-green-500",
139
+ },
140
+ ];