@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,122 @@
1
+ "use client";
2
+
3
+ import { createContext, useContext } from "react";
4
+ import type {
5
+ ActiveFilter,
6
+ CrudPermissions,
7
+ CrudQueryParams,
8
+ EntityConfig,
9
+ SortingState,
10
+ } from "../../types";
11
+
12
+ // Translations interface for CRUD components
13
+ export interface CrudTranslations {
14
+ edit?: string;
15
+ delete?: string;
16
+ save?: string;
17
+ cancel?: string;
18
+ create?: string;
19
+ update?: string;
20
+ creating?: string;
21
+ updating?: string;
22
+ deleting?: string;
23
+ savedSuccessfully?: string;
24
+ createdSuccessfully?: string;
25
+ updatedSuccessfully?: string;
26
+ confirmDeleteTitle?: string;
27
+ confirmBulkDeleteTitle?: string;
28
+ confirmDeleteDescription?: string;
29
+ confirmBulkDeleteDescription?: string;
30
+ deleteSelected?: string;
31
+ actions?: string;
32
+ add?: string;
33
+ addNew?: string;
34
+ }
35
+
36
+ // 1. Config Context (Static - rarely changes)
37
+ export interface CrudConfigContextValue {
38
+ config: EntityConfig | null;
39
+ setConfig: (config: EntityConfig) => void;
40
+ permissions: CrudPermissions;
41
+ setPermissions: (permissions: CrudPermissions) => void;
42
+ translations: CrudTranslations;
43
+ setTranslations: (translations: CrudTranslations) => void;
44
+ }
45
+
46
+ export const CrudConfigContext = createContext<
47
+ CrudConfigContextValue | undefined
48
+ >(undefined);
49
+
50
+ export function useCrudConfig() {
51
+ const context = useContext(CrudConfigContext);
52
+ if (context === undefined) {
53
+ throw new Error("useCrudConfig must be used within CrudConfigProvider");
54
+ }
55
+ return context;
56
+ }
57
+
58
+ // 2. State Context (Dynamic - changes often)
59
+ export interface CrudStateContextValue {
60
+ // Search
61
+ search: string;
62
+ setSearch: (search: string) => void;
63
+
64
+ // Filters
65
+ filters: ActiveFilter[];
66
+ setFilters: (filters: ActiveFilter[]) => void;
67
+ addFilter: (filter: ActiveFilter) => void;
68
+ removeFilter: (filterName: string) => void;
69
+ updateFilter: (filterName: string, value: unknown) => void;
70
+ clearFilters: () => void;
71
+
72
+ // Pagination
73
+ pagination: {
74
+ page: number;
75
+ pageSize: number;
76
+ };
77
+ setPagination: (pagination: { page: number; pageSize: number }) => void;
78
+ setPage: (page: number) => void;
79
+ setPageSize: (pageSize: number) => void;
80
+
81
+ // Sorting
82
+ sorting: SortingState | null;
83
+ setSorting: (sorting: SortingState | null) => void;
84
+
85
+ // Query params builder
86
+ getQueryParams: () => CrudQueryParams;
87
+ }
88
+
89
+ export const CrudStateContext = createContext<
90
+ CrudStateContextValue | undefined
91
+ >(undefined);
92
+
93
+ export function useCrudState() {
94
+ const context = useContext(CrudStateContext);
95
+ if (context === undefined) {
96
+ throw new Error("useCrudState must be used within CrudStateProvider");
97
+ }
98
+ return context;
99
+ }
100
+
101
+ // 3. Selection Context (Very Dynamic - changes frequently)
102
+ export interface CrudSelectionContextValue {
103
+ selectedRows: Set<string>;
104
+ setSelectedRows: (selectedRows: Set<string>) => void;
105
+ toggleRowSelection: (rowId: string) => void;
106
+ selectAllRows: (rowIds: string[]) => void;
107
+ clearSelection: () => void;
108
+ }
109
+
110
+ export const CrudSelectionContext = createContext<
111
+ CrudSelectionContextValue | undefined
112
+ >(undefined);
113
+
114
+ export function useCrudSelection() {
115
+ const context = useContext(CrudSelectionContext);
116
+ if (context === undefined) {
117
+ throw new Error(
118
+ "useCrudSelection must be used within CrudSelectionProvider",
119
+ );
120
+ }
121
+ return context;
122
+ }
@@ -0,0 +1,145 @@
1
+ "use client";
2
+
3
+ import { AlertTriangle } from "lucide-react";
4
+
5
+ import type { EntityConfig } from "../../types";
6
+
7
+ import {
8
+ AlertDialog,
9
+ AlertDialogAction,
10
+ AlertDialogCancel,
11
+ AlertDialogContent,
12
+ AlertDialogDescription,
13
+ AlertDialogFooter,
14
+ AlertDialogHeader,
15
+ AlertDialogTitle,
16
+ } from "../../ui";
17
+
18
+ interface CrudDeleteDialogProps {
19
+ open: boolean;
20
+ onOpenChange: (open: boolean) => void;
21
+ config: EntityConfig;
22
+ itemData?: Record<string, unknown>;
23
+ itemCount?: number;
24
+ onConfirm: () => void;
25
+ loading?: boolean;
26
+ translations?: {
27
+ confirmDeleteTitle?: string;
28
+ confirmBulkDeleteTitle?: string;
29
+ confirmDeleteDescription?: string;
30
+ confirmBulkDeleteDescription?: string;
31
+ cancel?: string;
32
+ delete?: string;
33
+ deleting?: string;
34
+ };
35
+ }
36
+
37
+ export function CrudDeleteDialog({
38
+ open,
39
+ onOpenChange,
40
+ config,
41
+ itemData,
42
+ itemCount,
43
+ onConfirm,
44
+ loading = false,
45
+ translations,
46
+ }: CrudDeleteDialogProps) {
47
+ const isBulkDelete = itemCount !== undefined && itemCount > 1;
48
+ const displayValue = itemData
49
+ ? String(
50
+ itemData[config.displayField] ||
51
+ itemData[config.idField] ||
52
+ "this item",
53
+ )
54
+ : undefined;
55
+
56
+ // Translation helpers with fallbacks
57
+ const t = {
58
+ cancel: translations?.cancel || "Cancel",
59
+ delete: translations?.delete || "Delete",
60
+ deleting: translations?.deleting || "Deleting...",
61
+ };
62
+
63
+ // Build title
64
+ const getTitle = () => {
65
+ if (isBulkDelete) {
66
+ if (translations?.confirmBulkDeleteTitle) {
67
+ return translations.confirmBulkDeleteTitle
68
+ .replace("{{count}}", String(itemCount))
69
+ .replace("{{entities}}", config.pluralLabel.toLowerCase());
70
+ }
71
+ return `Delete ${itemCount} ${config.pluralLabel.toLowerCase()}?`;
72
+ } else {
73
+ if (translations?.confirmDeleteTitle) {
74
+ return translations.confirmDeleteTitle.replace(
75
+ "{{entity}}",
76
+ config.label,
77
+ );
78
+ }
79
+ return `Delete ${config.label}?`;
80
+ }
81
+ };
82
+
83
+ // Build description
84
+ const getDescription = () => {
85
+ if (isBulkDelete) {
86
+ if (translations?.confirmBulkDeleteDescription) {
87
+ return translations.confirmBulkDeleteDescription
88
+ .replace("{{count}}", String(itemCount))
89
+ .replace("{{entities}}", config.pluralLabel.toLowerCase());
90
+ }
91
+ return (
92
+ <>
93
+ Are you sure you want to delete <strong>{itemCount}</strong>{" "}
94
+ {config.pluralLabel.toLowerCase()}? This action cannot be undone.
95
+ </>
96
+ );
97
+ } else {
98
+ if (translations?.confirmDeleteDescription && displayValue) {
99
+ return translations.confirmDeleteDescription.replace(
100
+ "{{name}}",
101
+ displayValue,
102
+ );
103
+ }
104
+ return (
105
+ <>
106
+ Are you sure you want to delete{" "}
107
+ {displayValue ? (
108
+ <strong>&quot;{displayValue}&quot;</strong>
109
+ ) : (
110
+ `this ${config.label.toLowerCase()}`
111
+ )}
112
+ ? This action cannot be undone.
113
+ </>
114
+ );
115
+ }
116
+ };
117
+
118
+ return (
119
+ <AlertDialog open={open} onOpenChange={onOpenChange}>
120
+ <AlertDialogContent>
121
+ <AlertDialogHeader>
122
+ <div className="flex items-center gap-3">
123
+ <div className="rounded-full bg-destructive/10 p-2">
124
+ <AlertTriangle className="h-5 w-5 text-destructive" />
125
+ </div>
126
+ <AlertDialogTitle>{getTitle()}</AlertDialogTitle>
127
+ </div>
128
+ <AlertDialogDescription className="pt-2">
129
+ {getDescription()}
130
+ </AlertDialogDescription>
131
+ </AlertDialogHeader>
132
+ <AlertDialogFooter>
133
+ <AlertDialogCancel disabled={loading}>{t.cancel}</AlertDialogCancel>
134
+ <AlertDialogAction
135
+ onClick={onConfirm}
136
+ disabled={loading}
137
+ className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
138
+ >
139
+ {loading ? t.deleting : t.delete}
140
+ </AlertDialogAction>
141
+ </AlertDialogFooter>
142
+ </AlertDialogContent>
143
+ </AlertDialog>
144
+ );
145
+ }
@@ -0,0 +1,406 @@
1
+ "use client";
2
+
3
+ import { useEffect, useRef, useState } from "react";
4
+ import { CheckCircle2, Loader2 } from "lucide-react";
5
+
6
+ import type { EntityConfig } from "../../types";
7
+
8
+ import {
9
+ Dialog,
10
+ DialogContent,
11
+ DialogDescription,
12
+ DialogHeader,
13
+ DialogTitle,
14
+ } from "../../ui";
15
+ import { DynamicIcon } from "../../ui";
16
+ import { CrudForm } from "./crud-form";
17
+ import { useCrudConfig } from "./crud-context";
18
+
19
+ interface CrudDialogProps {
20
+ open: boolean;
21
+ onOpenChange: (open: boolean) => void;
22
+ config: EntityConfig;
23
+ initialData?: Record<string, unknown>;
24
+ mode?: "create" | "edit";
25
+ onSubmit: (data: Record<string, unknown>) => Promise<void> | void;
26
+ }
27
+
28
+ export function CrudDialog({
29
+ open,
30
+ onOpenChange,
31
+ config,
32
+ initialData,
33
+ mode = "create",
34
+ onSubmit,
35
+ }: CrudDialogProps) {
36
+ const { translations } = useCrudConfig();
37
+ const [showSuccess, setShowSuccess] = useState(false);
38
+ const [isSubmitting, setIsSubmitting] = useState(false);
39
+ const [formProgress, setFormProgress] = useState<number | null>(null);
40
+ const formRef = useRef<HTMLFormElement>(null);
41
+ const progressSubscriptionRef = useRef<(() => void) | null>(null);
42
+ const successTimeoutRef = useRef<NodeJS.Timeout | null>(null);
43
+ const focusTimeoutRef = useRef<NodeJS.Timeout | null>(null);
44
+
45
+ // Track open state to handle race conditions
46
+ const isOpenRef = useRef(open);
47
+ useEffect(() => {
48
+ isOpenRef.current = open;
49
+ }, [open]);
50
+
51
+ const handleSubmit = async (data: Record<string, unknown>) => {
52
+ try {
53
+ setIsSubmitting(true);
54
+ await onSubmit(data);
55
+
56
+ // Check if dialog was closed by parent (CrudPage) during submission
57
+ if (!isOpenRef.current) {
58
+ setIsSubmitting(false);
59
+ return;
60
+ }
61
+
62
+ setIsSubmitting(false);
63
+ setShowSuccess(true);
64
+
65
+ // Clear any existing timeout
66
+ if (successTimeoutRef.current) {
67
+ clearTimeout(successTimeoutRef.current);
68
+ }
69
+
70
+ successTimeoutRef.current = setTimeout(() => {
71
+ // Double check open state before closing
72
+ if (isOpenRef.current) {
73
+ setShowSuccess(false);
74
+ onOpenChange(false);
75
+ }
76
+ successTimeoutRef.current = null;
77
+ }, 1500);
78
+ } catch (error) {
79
+ if (isOpenRef.current) {
80
+ setIsSubmitting(false);
81
+ }
82
+ // Error handling is done in CrudForm
83
+ }
84
+ };
85
+
86
+ const handleCancel = () => {
87
+ if (!isSubmitting) {
88
+ onOpenChange(false);
89
+ }
90
+ };
91
+
92
+ // Focus management - focus vào field đầu tiên khi mở dialog
93
+ useEffect(() => {
94
+ if (open) {
95
+ // Clear any existing timeout
96
+ if (focusTimeoutRef.current) {
97
+ clearTimeout(focusTimeoutRef.current);
98
+ }
99
+
100
+ focusTimeoutRef.current = setTimeout(() => {
101
+ const firstInput = document.querySelector<HTMLInputElement>(
102
+ 'form input:not([type="hidden"]), form textarea, form select',
103
+ );
104
+ firstInput?.focus();
105
+ focusTimeoutRef.current = null;
106
+ }, 100);
107
+ } else {
108
+ // Reset states when dialog closes
109
+ setShowSuccess(false);
110
+ setIsSubmitting(false);
111
+ setFormProgress(null);
112
+ // Cleanup progress subscription
113
+ if (
114
+ progressSubscriptionRef.current &&
115
+ typeof progressSubscriptionRef.current === "function"
116
+ ) {
117
+ try {
118
+ progressSubscriptionRef.current();
119
+ } catch (error) {
120
+ console.error("Error cleaning up progress subscription:", error);
121
+ }
122
+ progressSubscriptionRef.current = null;
123
+ }
124
+ }
125
+
126
+ // Cleanup timeouts when component unmounts or dialog closes
127
+ return () => {
128
+ if (successTimeoutRef.current) {
129
+ clearTimeout(successTimeoutRef.current);
130
+ successTimeoutRef.current = null;
131
+ }
132
+ if (focusTimeoutRef.current) {
133
+ clearTimeout(focusTimeoutRef.current);
134
+ focusTimeoutRef.current = null;
135
+ }
136
+ };
137
+ }, [open]);
138
+
139
+ // Keyboard shortcuts
140
+ useEffect(() => {
141
+ if (!open) return;
142
+
143
+ const handleKeyDown = (e: KeyboardEvent) => {
144
+ // Don't handle keyboard events if user is typing in a popover (combobox search)
145
+ const target = e.target as HTMLElement;
146
+ if (
147
+ target.closest('[data-slot="popover-content"]') ||
148
+ target.closest('[data-slot="command-input"]') ||
149
+ target.closest('[data-slot="command-input-wrapper"]')
150
+ ) {
151
+ return; // Let popover handle keyboard events
152
+ }
153
+
154
+ // Esc to close (only if not submitting)
155
+ if (e.key === "Escape" && !showSuccess && !isSubmitting) {
156
+ onOpenChange(false);
157
+ return;
158
+ }
159
+
160
+ // Ctrl/Cmd + Enter to submit
161
+ if ((e.ctrlKey || e.metaKey) && e.key === "Enter" && !isSubmitting) {
162
+ e.preventDefault();
163
+ const submitButton = formRef.current?.querySelector<HTMLButtonElement>(
164
+ 'button[type="submit"]',
165
+ );
166
+ if (submitButton && !submitButton.disabled) {
167
+ submitButton.click();
168
+ }
169
+ }
170
+ };
171
+
172
+ window.addEventListener("keydown", handleKeyDown);
173
+ return () => window.removeEventListener("keydown", handleKeyDown);
174
+ }, [open, showSuccess, isSubmitting, onOpenChange]);
175
+
176
+ return (
177
+ <Dialog open={open} onOpenChange={onOpenChange} modal={true}>
178
+ <DialogContent
179
+ className="max-w-2xl max-h-[90vh] sm:max-h-[90vh] h-[100vh] sm:h-auto rounded-none sm:rounded-lg overflow-hidden flex flex-col p-0"
180
+ onInteractOutside={(e) => {
181
+ // Prevent closing dialog when submitting or clicking on Popover
182
+ if (isSubmitting || showSuccess) {
183
+ e.preventDefault();
184
+ return;
185
+ }
186
+ const target = e.target as HTMLElement;
187
+ if (target.closest('[data-slot="popover-content"]')) {
188
+ e.preventDefault();
189
+ }
190
+ }}
191
+ onEscapeKeyDown={(e) => {
192
+ // Don't close dialog if popover is open or submitting
193
+ if (isSubmitting || showSuccess) {
194
+ e.preventDefault();
195
+ return;
196
+ }
197
+ const popoverOpen = document.querySelector(
198
+ '[data-slot="popover-content"][data-state="open"]',
199
+ );
200
+ if (popoverOpen) {
201
+ e.preventDefault();
202
+ return;
203
+ }
204
+ }}
205
+ onPointerDownOutside={(e) => {
206
+ // Prevent closing dialog when submitting or clicking on Popover
207
+ if (isSubmitting || showSuccess) {
208
+ e.preventDefault();
209
+ return;
210
+ }
211
+ const target = e.target as HTMLElement;
212
+ if (target.closest('[data-slot="popover-content"]')) {
213
+ e.preventDefault();
214
+ }
215
+ }}
216
+ >
217
+ {/* Loading Overlay */}
218
+ {isSubmitting && (
219
+ <div className="absolute inset-0 bg-background/80 backdrop-blur-sm flex items-center justify-center z-50 rounded-lg">
220
+ <div className="flex flex-col items-center gap-3 animate-in fade-in zoom-in-95 duration-200">
221
+ <Loader2 className="h-8 w-8 animate-spin text-primary" />
222
+ <p className="text-sm font-medium text-muted-foreground">
223
+ {mode === "create"
224
+ ? translations?.creating || "Creating..."
225
+ : translations?.updating || "Updating..."}
226
+ </p>
227
+ </div>
228
+ </div>
229
+ )}
230
+
231
+ {/* Success Animation Overlay */}
232
+ {showSuccess && (
233
+ <div className="absolute inset-0 bg-background/95 backdrop-blur-sm flex items-center justify-center z-50 rounded-lg">
234
+ <div className="flex flex-col items-center gap-3 animate-in fade-in zoom-in-95 duration-300">
235
+ <CheckCircle2 className="h-12 w-12 text-green-500" />
236
+ <p className="text-lg font-semibold">
237
+ {translations?.savedSuccessfully || "Saved successfully!"}
238
+ </p>
239
+ </div>
240
+ </div>
241
+ )}
242
+
243
+ {/* Sticky Header với Icon */}
244
+ <DialogHeader className="sticky top-0 z-10 bg-background border-b px-4 sm:px-6 pt-4 sm:pt-6 pb-3 sm:pb-4 mb-0">
245
+ <div className="flex items-start justify-between gap-3">
246
+ <div className="flex-1 min-w-0">
247
+ <div className="flex items-center gap-2 sm:gap-3">
248
+ {config.iconName && (
249
+ <div className="p-1.5 rounded-lg bg-primary/10 shrink-0">
250
+ <DynamicIcon
251
+ name={config.iconName as any}
252
+ className="h-4 w-4 sm:h-5 sm:w-5 text-primary"
253
+ />
254
+ </div>
255
+ )}
256
+ <DialogTitle className="text-lg sm:text-xl font-bold">
257
+ {mode === "create"
258
+ ? translations?.add
259
+ ? `${translations.add} ${config.label}`
260
+ : `Create ${config.label}`
261
+ : translations?.edit
262
+ ? `${translations.edit} ${config.label}`
263
+ : `Edit ${config.label}`}
264
+ </DialogTitle>
265
+ </div>
266
+ <DialogDescription className="mt-1 text-xs text-muted-foreground">
267
+ {mode === "create"
268
+ ? translations?.addNew
269
+ ? `${translations.addNew} ${config.label.toLowerCase()}`
270
+ : `Add a new ${config.label.toLowerCase()}`
271
+ : translations?.update
272
+ ? `${translations.update} ${config.label.toLowerCase()}`
273
+ : `Update ${config.label.toLowerCase()} information`}
274
+ </DialogDescription>
275
+ </div>
276
+ {/* Form Progress in Header */}
277
+ {formProgress !== null && (
278
+ <div className="shrink-0">
279
+ <div className="flex items-center gap-2 min-w-[80px]">
280
+ <div className="flex-1 min-w-[60px]">
281
+ <div className="h-1.5 w-full bg-muted rounded-full overflow-hidden">
282
+ <div
283
+ className="h-full bg-primary transition-all duration-300"
284
+ style={{ width: `${formProgress}%` }}
285
+ />
286
+ </div>
287
+ </div>
288
+ <span className="text-xs font-semibold text-muted-foreground tabular-nums whitespace-nowrap">
289
+ {formProgress}%
290
+ </span>
291
+ </div>
292
+ </div>
293
+ )}
294
+ </div>
295
+ </DialogHeader>
296
+
297
+ {/* Scrollable Form Content */}
298
+ <div className="flex-1 overflow-y-auto px-4 sm:px-6 py-4">
299
+ <CrudForm
300
+ key={`${config.name}-${mode}-${initialData?.id || "new"}`}
301
+ ref={formRef}
302
+ config={config}
303
+ initialData={initialData}
304
+ onSubmit={handleSubmit}
305
+ onCancel={handleCancel}
306
+ submitLabel={
307
+ mode === "create"
308
+ ? translations?.create || "Create"
309
+ : translations?.update || "Update"
310
+ }
311
+ submittingLabel={
312
+ mode === "create"
313
+ ? translations?.creating || "Creating..."
314
+ : translations?.updating || "Updating..."
315
+ }
316
+ cancelLabel={translations?.cancel || "Cancel"}
317
+ isSubmitting={isSubmitting}
318
+ mode={mode}
319
+ enableAutoSave={true}
320
+ onFormReady={(form) => {
321
+ // Cleanup previous subscription if exists
322
+ if (
323
+ progressSubscriptionRef.current &&
324
+ typeof progressSubscriptionRef.current === "function"
325
+ ) {
326
+ try {
327
+ progressSubscriptionRef.current();
328
+ } catch (error) {
329
+ console.error(
330
+ "Error cleaning up previous subscription:",
331
+ error,
332
+ );
333
+ }
334
+ progressSubscriptionRef.current = null;
335
+ }
336
+
337
+ // Watch form values and update progress
338
+ // Filter out: hidden fields and display-only fields (DTO fields)
339
+ const visibleFields = config.fields.filter(
340
+ (field) => !field.hideInForm && !field.isDisplayOnly,
341
+ );
342
+ const totalFields = visibleFields.length;
343
+
344
+ if (totalFields > 5) {
345
+ // Calculate initial progress
346
+ const initialValues = form.getValues();
347
+ const initialFilledFields = visibleFields.filter((field) => {
348
+ const value = initialValues[field.name];
349
+ return (
350
+ value !== undefined &&
351
+ value !== null &&
352
+ value !== "" &&
353
+ !(Array.isArray(value) && value.length === 0)
354
+ );
355
+ }).length;
356
+ setFormProgress(
357
+ Math.round((initialFilledFields / totalFields) * 100),
358
+ );
359
+
360
+ // Watch only visible fields instead of all fields for better performance
361
+ const visibleFieldNames = visibleFields.map(
362
+ (field) => field.name,
363
+ );
364
+ try {
365
+ // form.watch() returns an unsubscribe function directly, not an object with unsubscribe()
366
+ const unsubscribe = form.watch(
367
+ visibleFieldNames,
368
+ (formValues: Record<string, unknown>) => {
369
+ const filledFields = visibleFields.filter((field) => {
370
+ const value = formValues[field.name];
371
+ return (
372
+ value !== undefined &&
373
+ value !== null &&
374
+ value !== "" &&
375
+ !(Array.isArray(value) && value.length === 0)
376
+ );
377
+ }).length;
378
+
379
+ const progress = Math.round(
380
+ (filledFields / totalFields) * 100,
381
+ );
382
+ setFormProgress(progress);
383
+ },
384
+ );
385
+
386
+ // Store unsubscribe function directly, but only if it's actually a function
387
+ if (typeof unsubscribe === "function") {
388
+ progressSubscriptionRef.current = unsubscribe;
389
+ } else {
390
+ progressSubscriptionRef.current = null;
391
+ }
392
+ } catch (error) {
393
+ console.error("Error setting up form watch:", error);
394
+ progressSubscriptionRef.current = null;
395
+ }
396
+ } else {
397
+ setFormProgress(null);
398
+ progressSubscriptionRef.current = null;
399
+ }
400
+ }}
401
+ />
402
+ </div>
403
+ </DialogContent>
404
+ </Dialog>
405
+ );
406
+ }