@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,247 @@
1
+ import { z } from "zod";
2
+
3
+ import type { EntityConfig, FieldConfig } from "../../types";
4
+
5
+ import { getFieldDefaultValue } from "./crud-utils";
6
+
7
+ /**
8
+ * Build Zod schema from FieldConfig
9
+ */
10
+ export function buildSchemaFromConfig(
11
+ fields: FieldConfig[],
12
+ ): z.ZodObject<Record<string, z.ZodTypeAny>> {
13
+ const schemaObject: Record<string, z.ZodTypeAny> = {};
14
+
15
+ fields.forEach((field) => {
16
+ if (field.hideInForm) {
17
+ return; // Skip hidden fields
18
+ }
19
+
20
+ let fieldSchema: z.ZodTypeAny;
21
+
22
+ // Base type schema
23
+ switch (field.type) {
24
+ case "text":
25
+ case "email":
26
+ case "url":
27
+ fieldSchema = z.string();
28
+ break;
29
+ case "number":
30
+ fieldSchema = z.preprocess(
31
+ (val) =>
32
+ val === "" || val === null || val === undefined
33
+ ? undefined
34
+ : Number(val),
35
+ z.number(),
36
+ );
37
+ break;
38
+ case "integer":
39
+ fieldSchema = z.preprocess(
40
+ (val) =>
41
+ val === "" || val === null || val === undefined
42
+ ? undefined
43
+ : Number(val),
44
+ z.number().int(),
45
+ );
46
+ break;
47
+ case "textarea":
48
+ fieldSchema = z.string();
49
+ break;
50
+ case "boolean":
51
+ fieldSchema = z.boolean();
52
+ break;
53
+ case "switch":
54
+ // Switch can be boolean or string (active/inactive)
55
+ // We default to string validation here as it's primarily used for status
56
+ fieldSchema = z.string().or(z.boolean());
57
+ break;
58
+ case "date":
59
+ case "datetime":
60
+ case "time":
61
+ fieldSchema = z.string().datetime().or(z.date()).or(z.null());
62
+ break;
63
+ case "select":
64
+ fieldSchema = z.string();
65
+ break;
66
+ case "multiselect":
67
+ fieldSchema = z.array(z.string());
68
+ break;
69
+ case "file":
70
+ case "image":
71
+ fieldSchema = z.instanceof(File).or(z.string()).or(z.null());
72
+ break;
73
+ case "json":
74
+ fieldSchema = z.unknown();
75
+ break;
76
+ case "custom":
77
+ // For custom fields, use unknown or provided validation
78
+ fieldSchema = field.validation || z.unknown();
79
+ break;
80
+ default:
81
+ fieldSchema = z.string();
82
+ }
83
+
84
+ // Apply required validation
85
+ if (field.required) {
86
+ if (field.type === "multiselect") {
87
+ // Array: use min length
88
+ fieldSchema = (fieldSchema as z.ZodArray<z.ZodString>).min(
89
+ 1,
90
+ `${field.label} is required`,
91
+ );
92
+ } else if (field.type === "boolean") {
93
+ // Boolean fields are always required (true/false)
94
+ // No need to add required message
95
+ } else if (
96
+ field.type === "text" ||
97
+ field.type === "email" ||
98
+ field.type === "url" ||
99
+ field.type === "textarea" ||
100
+ field.type === "select"
101
+ ) {
102
+ // String: use min length
103
+ fieldSchema = (fieldSchema as z.ZodString).min(
104
+ 1,
105
+ `${field.label} is required`,
106
+ );
107
+ } else if (field.type === "number" || field.type === "integer") {
108
+ // Number: Zod numbers are required by default if not optional
109
+ // No additional validation needed
110
+ } else if (
111
+ field.type === "date" ||
112
+ field.type === "datetime" ||
113
+ field.type === "time"
114
+ ) {
115
+ // Date/datetime: use refine to ensure it's not null/undefined
116
+ fieldSchema = (fieldSchema as z.ZodTypeAny).refine(
117
+ (val) => val !== null && val !== undefined,
118
+ {
119
+ message: `${field.label} is required`,
120
+ },
121
+ );
122
+ } else {
123
+ // Other types (file, image, json, custom): use refine
124
+ fieldSchema = (fieldSchema as z.ZodTypeAny).refine(
125
+ (val) => val !== null && val !== undefined,
126
+ {
127
+ message: `${field.label} is required`,
128
+ },
129
+ );
130
+ }
131
+ } else {
132
+ // Make optional - allow null and undefined
133
+ if (field.type === "multiselect") {
134
+ fieldSchema = (fieldSchema as z.ZodArray<z.ZodString>)
135
+ .nullable()
136
+ .optional();
137
+ } else {
138
+ fieldSchema = (fieldSchema as z.ZodTypeAny).nullable().optional();
139
+ }
140
+ }
141
+
142
+ // Apply custom validation if provided
143
+ if (field.validation) {
144
+ fieldSchema = field.validation as z.ZodTypeAny;
145
+ }
146
+
147
+ // Apply type-specific validations
148
+ if (field.type === "email") {
149
+ fieldSchema = z.string().email(`${field.label} must be a valid email`);
150
+ } else if (field.type === "url") {
151
+ fieldSchema = z.string().url(`${field.label} must be a valid URL`);
152
+ }
153
+
154
+ schemaObject[field.name] = fieldSchema;
155
+ });
156
+
157
+ return z.object(schemaObject);
158
+ }
159
+
160
+ /**
161
+ * Validate form data against EntityConfig
162
+ */
163
+ export function validateFormData(
164
+ data: Record<string, unknown>,
165
+ config: EntityConfig,
166
+ ): { valid: boolean; errors: Record<string, string> } {
167
+ const schema = buildSchemaFromConfig(config.fields);
168
+ const result = schema.safeParse(data);
169
+
170
+ if (result.success) {
171
+ return { valid: true, errors: {} };
172
+ }
173
+
174
+ const errors: Record<string, string> = {};
175
+ result.error.errors.forEach((error) => {
176
+ const fieldName = error.path[0] as string;
177
+ errors[fieldName] = error.message;
178
+ });
179
+
180
+ return { valid: false, errors };
181
+ }
182
+
183
+ /**
184
+ * Get default values object from config
185
+ */
186
+ export function getDefaultValuesFromConfig(
187
+ config: EntityConfig,
188
+ ): Record<string, unknown> {
189
+ const defaults: Record<string, unknown> = {};
190
+
191
+ config.fields.forEach((field) => {
192
+ if (!field.hideInForm) {
193
+ defaults[field.name] = getFieldDefaultValue(field);
194
+ }
195
+ });
196
+
197
+ return defaults;
198
+ }
199
+
200
+ /**
201
+ * Transform form data to match API format
202
+ */
203
+ export function transformFormData(
204
+ data: Record<string, unknown>,
205
+ config: EntityConfig,
206
+ ): Record<string, unknown> {
207
+ const transformed: Record<string, unknown> = {};
208
+
209
+ config.fields.forEach((field) => {
210
+ const value = data[field.name];
211
+
212
+ // Nếu có transform.output thì ưu tiên dùng (ví dụ: mảng -> CSV string)
213
+ if (field.transform?.output) {
214
+ transformed[field.name] = field.transform.output(value);
215
+ return;
216
+ }
217
+
218
+ // Handle different field types
219
+ switch (field.type) {
220
+ case "date":
221
+ case "datetime":
222
+ case "time":
223
+ if (value instanceof Date) {
224
+ transformed[field.name] = value.toISOString();
225
+ } else if (typeof value === "string") {
226
+ transformed[field.name] = value;
227
+ }
228
+ break;
229
+ case "number":
230
+ case "integer":
231
+ if (value !== null && value !== undefined && value !== "") {
232
+ transformed[field.name] = Number(value);
233
+ }
234
+ break;
235
+ case "boolean":
236
+ transformed[field.name] = Boolean(value);
237
+ break;
238
+ case "multiselect":
239
+ transformed[field.name] = Array.isArray(value) ? value : [];
240
+ break;
241
+ default:
242
+ transformed[field.name] = value;
243
+ }
244
+ });
245
+
246
+ return transformed;
247
+ }
@@ -0,0 +1,234 @@
1
+ import type { DataSource } from "../../types";
2
+
3
+ import { crudConfig } from "../../configs";
4
+
5
+ interface CachedData {
6
+ data: Array<{ label: string; value: string | number | boolean }>;
7
+ timestamp: number;
8
+ }
9
+
10
+ export class DataLoader {
11
+ private cache = new Map<string, CachedData>();
12
+ // Cache enabled để tránh duplicate API calls
13
+ // Configure via CRUD_CACHE_ENABLED env variable (default: true)
14
+ private cacheEnabled = crudConfig.dataLoader.cacheEnabled;
15
+ // Default cache time in milliseconds
16
+ // Configure via CRUD_CACHE_TIME_MS env variable (default: 300000 = 5 minutes)
17
+ private defaultCacheTime = crudConfig.dataLoader.defaultCacheTime;
18
+ // Track pending requests to prevent duplicate calls
19
+ private pendingRequests = new Map<
20
+ string,
21
+ Promise<Array<{ label: string; value: string | number | boolean }>>
22
+ >();
23
+
24
+ /**
25
+ * Load options from dataSource (static or API)
26
+ */
27
+ async loadOptions(
28
+ dataSource: DataSource,
29
+ ): Promise<Array<{ label: string; value: string | number | boolean }>> {
30
+ // Static data source
31
+ if (dataSource.type === "static" && dataSource.options) {
32
+ return dataSource.options;
33
+ }
34
+
35
+ // API data source
36
+ if (dataSource.type === "api" && dataSource.endpoint) {
37
+ return this.loadFromAPI(dataSource);
38
+ }
39
+
40
+ throw new Error("Invalid dataSource configuration");
41
+ }
42
+
43
+ /**
44
+ * Load options from API with caching
45
+ */
46
+ private async loadFromAPI(
47
+ dataSource: DataSource,
48
+ ): Promise<Array<{ label: string; value: string | number | boolean }>> {
49
+ const cacheKey = this.getCacheKey(dataSource);
50
+
51
+ // Check cache
52
+ if (this.cacheEnabled) {
53
+ const cached = this.cache.get(cacheKey);
54
+ if (cached) {
55
+ const cacheTime = dataSource.cacheTime || this.defaultCacheTime;
56
+ if (Date.now() - cached.timestamp < cacheTime) {
57
+ console.log("DataLoader: Using cached data for", cacheKey);
58
+ return cached.data;
59
+ }
60
+ // Cache expired, remove it
61
+ this.cache.delete(cacheKey);
62
+ }
63
+ }
64
+
65
+ // Check if there's already a pending request for this cacheKey
66
+ const pendingRequest = this.pendingRequests.get(cacheKey);
67
+ if (pendingRequest) {
68
+ console.log("DataLoader: Reusing pending request for", cacheKey);
69
+ return pendingRequest;
70
+ }
71
+
72
+ // Create new request promise
73
+ const requestPromise = this.fetchFromAPI(dataSource, cacheKey);
74
+
75
+ // Store pending request
76
+ this.pendingRequests.set(cacheKey, requestPromise);
77
+
78
+ // Clean up after request completes (success or error)
79
+ requestPromise
80
+ .finally(() => {
81
+ this.pendingRequests.delete(cacheKey);
82
+ })
83
+ .catch(() => {
84
+ // Error already logged in fetchFromAPI
85
+ });
86
+
87
+ return requestPromise;
88
+ }
89
+
90
+ /**
91
+ * Actually fetch data from API
92
+ */
93
+ private async fetchFromAPI(
94
+ dataSource: DataSource,
95
+ cacheKey: string,
96
+ ): Promise<Array<{ label: string; value: string | number | boolean }>> {
97
+ try {
98
+ // Build URL with params
99
+ const url = new URL(dataSource.endpoint!, window.location.origin);
100
+ if (dataSource.params) {
101
+ Object.entries(dataSource.params).forEach(([key, value]) => {
102
+ url.searchParams.append(key, String(value));
103
+ });
104
+ }
105
+
106
+ console.log("DataLoader: Fetching from API", url.toString());
107
+ // Fetch from API
108
+ const response = await fetch(url.toString());
109
+
110
+ // Check Content-Type first
111
+ const contentType = response.headers.get("content-type");
112
+ const isJSON = contentType && contentType.includes("application/json");
113
+
114
+ if (!response.ok) {
115
+ const errorText = await response.text();
116
+ console.error(
117
+ `API fetch failed (${response.status}):`,
118
+ errorText.substring(0, 200),
119
+ );
120
+ throw new Error(`Failed to fetch data: ${response.statusText}`);
121
+ }
122
+
123
+ // Check Content-Type to ensure it's JSON
124
+ if (!isJSON) {
125
+ const text = await response.text();
126
+ console.error("Invalid Content-Type:", contentType);
127
+ console.error("Response text:", text.substring(0, 200));
128
+ throw new Error(
129
+ `Invalid response format. Expected JSON but got ${contentType || "unknown"}. The endpoint might not exist or returned an error page.`,
130
+ );
131
+ }
132
+
133
+ const data = await response.json();
134
+ console.log("DataLoader received data:", {
135
+ endpoint: dataSource.endpoint,
136
+ data,
137
+ });
138
+
139
+ // Transform data
140
+ let options: Array<{ label: string; value: string | number | boolean }>;
141
+
142
+ if (dataSource.transform) {
143
+ // Use custom transform function
144
+ options = dataSource.transform(data);
145
+ } else if (Array.isArray(data)) {
146
+ // Default transformation - direct array
147
+ const labelField = dataSource.labelField || "label";
148
+ const valueField = dataSource.valueField || "value";
149
+
150
+ options = data.map((item) => ({
151
+ label: String(item[labelField]),
152
+ value: item[valueField],
153
+ }));
154
+ } else if (data.data && Array.isArray(data.data)) {
155
+ // Handle paginated response with 'data' property
156
+ const labelField = dataSource.labelField || "label";
157
+ const valueField = dataSource.valueField || "value";
158
+
159
+ options = data.data.map((item: Record<string, unknown>) => ({
160
+ label: String(item[labelField]),
161
+ value: item[valueField],
162
+ }));
163
+ } else if (data.items && Array.isArray(data.items)) {
164
+ // Handle paginated response with 'items' property (common in CRUD APIs)
165
+ const labelField = dataSource.labelField || "label";
166
+ const valueField = dataSource.valueField || "value";
167
+
168
+ options = data.items.map((item: Record<string, unknown>) => ({
169
+ label: String(item[labelField]),
170
+ value: item[valueField],
171
+ }));
172
+ } else {
173
+ console.error("Invalid API response format:", data);
174
+ throw new Error(
175
+ `Invalid API response format. Expected array, {data: []}, or {items: []}, but got: ${JSON.stringify(data).substring(0, 200)}`,
176
+ );
177
+ }
178
+
179
+ // Cache the result
180
+ if (this.cacheEnabled) {
181
+ this.cache.set(cacheKey, {
182
+ data: options,
183
+ timestamp: Date.now(),
184
+ });
185
+ }
186
+
187
+ return options;
188
+ } catch (error) {
189
+ console.error("Error loading data from API:", error);
190
+ throw error;
191
+ }
192
+ }
193
+
194
+ /**
195
+ * Generate cache key from dataSource
196
+ */
197
+ private getCacheKey(dataSource: DataSource): string {
198
+ if (dataSource.type === "static") {
199
+ return `static-${JSON.stringify(dataSource.options)}`;
200
+ }
201
+
202
+ const params = dataSource.params ? JSON.stringify(dataSource.params) : "";
203
+ return `${dataSource.endpoint}-${dataSource.labelField}-${dataSource.valueField}-${params}`;
204
+ }
205
+
206
+ /**
207
+ * Clear cache for a specific endpoint or all cache
208
+ */
209
+ clearCache(endpoint?: string): void {
210
+ if (endpoint) {
211
+ // Clear specific endpoint cache
212
+ const keysToDelete: string[] = [];
213
+ this.cache.forEach((_, key) => {
214
+ if (key.startsWith(endpoint)) {
215
+ keysToDelete.push(key);
216
+ }
217
+ });
218
+ keysToDelete.forEach((key) => this.cache.delete(key));
219
+ } else {
220
+ // Clear all cache
221
+ this.cache.clear();
222
+ }
223
+ }
224
+
225
+ /**
226
+ * Get cache size
227
+ */
228
+ getCacheSize(): number {
229
+ return this.cache.size;
230
+ }
231
+ }
232
+
233
+ // Singleton instance
234
+ export const dataLoader = new DataLoader();