@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,642 @@
1
+ "use client";
2
+
3
+ import {
4
+ forwardRef,
5
+ useCallback,
6
+ useEffect,
7
+ useMemo,
8
+ useRef,
9
+ useState,
10
+ } from "react";
11
+ import { zodResolver } from "@hookform/resolvers/zod";
12
+ import { useForm } from "react-hook-form";
13
+ import { AlertCircle, Loader2, Save } from "lucide-react";
14
+
15
+ import type { EntityConfig, FieldConfig } from "../../types";
16
+
17
+ import { filterDisplayOnlyFields, sortFieldsByOrder } from "../lib/crud-utils";
18
+ import {
19
+ buildSchemaFromConfig,
20
+ getDefaultValuesFromConfig,
21
+ } from "../lib/crud-validator";
22
+ import {
23
+ calculateFieldValue,
24
+ getDependentFields,
25
+ } from "../lib/field-calculator";
26
+
27
+ import {
28
+ Accordion,
29
+ AccordionContent,
30
+ AccordionItem,
31
+ AccordionTrigger,
32
+ } from "../../ui";
33
+ import { Alert, AlertDescription, AlertTitle } from "../../ui";
34
+ import { Badge } from "../../ui";
35
+ import { Button } from "../../ui";
36
+ import {
37
+ Card,
38
+ CardContent,
39
+ CardDescription,
40
+ CardHeader,
41
+ CardTitle,
42
+ } from "../../ui";
43
+ import { Form } from "../../ui";
44
+ import { Progress } from "../../ui";
45
+ import { CrudFieldRenderer } from "./crud-field-renderer";
46
+
47
+ interface CrudFormProps {
48
+ config: EntityConfig;
49
+ initialData?: Record<string, unknown>;
50
+ onSubmit: (data: Record<string, unknown>) => Promise<void> | void;
51
+ onCancel?: () => void;
52
+ submitLabel?: string;
53
+ cancelLabel?: string;
54
+ submittingLabel?: string;
55
+ isSubmitting?: boolean;
56
+ mode?: "create" | "edit";
57
+ enableAutoSave?: boolean;
58
+ onFormReady?: (form: ReturnType<typeof useForm>) => void;
59
+ }
60
+
61
+ const CrudFormComponent = forwardRef<HTMLFormElement, CrudFormProps>(
62
+ function CrudForm(
63
+ {
64
+ config,
65
+ initialData,
66
+ onSubmit,
67
+ onCancel,
68
+ submitLabel = "Submit",
69
+ cancelLabel = "Cancel",
70
+ submittingLabel = "Submitting...",
71
+ isSubmitting: externalIsSubmitting,
72
+ mode = "create",
73
+ enableAutoSave = true,
74
+ onFormReady,
75
+ },
76
+ ref,
77
+ ) {
78
+ const computedFields = useMemo(() => {
79
+ return config.fields.map((field) => {
80
+ const disableForMode =
81
+ (mode === "create" && field.disableOnCreate) ||
82
+ (mode === "edit" && field.disableOnEdit);
83
+
84
+ const shouldDisable = field.disabled || Boolean(disableForMode);
85
+
86
+ if (shouldDisable === field.disabled) {
87
+ return field;
88
+ }
89
+
90
+ const modifiedField = {
91
+ ...field,
92
+ disabled: shouldDisable,
93
+ };
94
+
95
+ return modifiedField;
96
+ });
97
+ }, [config.fields, mode]);
98
+
99
+ const schema = buildSchemaFromConfig(computedFields);
100
+ const defaultValues = initialData || getDefaultValuesFromConfig(config);
101
+ const draftKey = `draft-${config.name}-${mode}`;
102
+ const saveTimeoutRef = useRef<NodeJS.Timeout | null>(null);
103
+ const [hasDraft, setHasDraft] = useState(false);
104
+ const [lastSaved, setLastSaved] = useState<Date | null>(null);
105
+
106
+ const form = useForm({
107
+ resolver: zodResolver(schema),
108
+ defaultValues,
109
+ });
110
+
111
+ // Notify parent when form is ready
112
+ useEffect(() => {
113
+ if (onFormReady) {
114
+ onFormReady(form);
115
+ }
116
+ }, [form, onFormReady]);
117
+
118
+ // Load draft from localStorage when form initializes (only for create mode)
119
+ useEffect(() => {
120
+ if (
121
+ mode === "create" &&
122
+ enableAutoSave &&
123
+ typeof window !== "undefined" &&
124
+ !initialData
125
+ ) {
126
+ try {
127
+ const draft = localStorage.getItem(draftKey);
128
+ if (draft) {
129
+ const draftData = JSON.parse(draft);
130
+ // Only load draft if it's not empty (has at least one non-empty field)
131
+ const hasData = Object.values(draftData).some(
132
+ (value) => value !== undefined && value !== null && value !== "",
133
+ );
134
+ if (hasData) {
135
+ form.reset(draftData);
136
+ setHasDraft(true);
137
+ }
138
+ }
139
+ } catch (error) {
140
+ console.error("Error loading draft:", error);
141
+ }
142
+ }
143
+ }, [mode, enableAutoSave, draftKey, initialData, form]);
144
+
145
+ // Auto-save draft to localStorage
146
+ useEffect(() => {
147
+ if (!enableAutoSave || mode !== "create") return;
148
+
149
+ let unsubscribe: (() => void) | { unsubscribe: () => void } | undefined;
150
+
151
+ try {
152
+ // form.watch() may return an unsubscribe function or a Subscription object
153
+ unsubscribe = form.watch((data: Record<string, unknown>) => {
154
+ // Clear existing timeout
155
+ if (saveTimeoutRef.current) {
156
+ clearTimeout(saveTimeoutRef.current);
157
+ }
158
+
159
+ // Debounce save to localStorage (1 second delay)
160
+ saveTimeoutRef.current = setTimeout(() => {
161
+ try {
162
+ if (typeof window !== "undefined") {
163
+ // Only save if there's actual data
164
+ const hasData = Object.values(data).some(
165
+ (value) =>
166
+ value !== undefined && value !== null && value !== "",
167
+ );
168
+ if (hasData) {
169
+ localStorage.setItem(draftKey, JSON.stringify(data));
170
+ setHasDraft(true);
171
+ setLastSaved(new Date());
172
+ } else {
173
+ // Remove draft if form is empty
174
+ localStorage.removeItem(draftKey);
175
+ setHasDraft(false);
176
+ setLastSaved(null);
177
+ }
178
+ }
179
+ } catch (error) {
180
+ console.error("Error saving draft:", error);
181
+ }
182
+ }, 1000);
183
+ });
184
+ } catch (error) {
185
+ console.error("Error setting up form watch:", error);
186
+ }
187
+
188
+ return () => {
189
+ // Handle unsubscribe - can be a function or a Subscription object
190
+ if (unsubscribe) {
191
+ try {
192
+ if (typeof unsubscribe === "function") {
193
+ unsubscribe();
194
+ } else if (
195
+ unsubscribe &&
196
+ typeof unsubscribe === "object" &&
197
+ "unsubscribe" in unsubscribe
198
+ ) {
199
+ unsubscribe.unsubscribe();
200
+ }
201
+ } catch (error) {
202
+ console.error("Error unsubscribing from form watch:", error);
203
+ }
204
+ }
205
+ if (saveTimeoutRef.current) {
206
+ clearTimeout(saveTimeoutRef.current);
207
+ }
208
+ };
209
+ }, [form, enableAutoSave, mode, draftKey]);
210
+
211
+ // Clear draft after successful submit
212
+ const handleSubmit = async (data: Record<string, unknown>) => {
213
+ try {
214
+ // Filter out display-only fields before submitting to API
215
+ const filteredData = filterDisplayOnlyFields(data, config);
216
+
217
+ // Debug log for user-supplier form
218
+ if (config.name === "user-supplier") {
219
+ console.log("Form submit data:", {
220
+ userId: filteredData.userId,
221
+ supplierId: filteredData.supplierId,
222
+ userIdType: typeof filteredData.userId,
223
+ supplierIdType: typeof filteredData.supplierId,
224
+ fullData: filteredData,
225
+ });
226
+ }
227
+
228
+ await onSubmit(filteredData);
229
+ // Clear draft after successful submit
230
+ if (
231
+ mode === "create" &&
232
+ enableAutoSave &&
233
+ typeof window !== "undefined"
234
+ ) {
235
+ localStorage.removeItem(draftKey);
236
+ setHasDraft(false);
237
+ setLastSaved(null);
238
+ }
239
+ } catch (error) {
240
+ console.error("Form submission error:", error);
241
+ }
242
+ };
243
+
244
+ // Clear draft when canceling
245
+ const handleCancel = () => {
246
+ if (
247
+ mode === "create" &&
248
+ enableAutoSave &&
249
+ typeof window !== "undefined"
250
+ ) {
251
+ // Optionally clear draft on cancel (or keep it for next time)
252
+ // localStorage.removeItem(draftKey)
253
+ }
254
+ onCancel?.();
255
+ };
256
+
257
+ const { isSubmitting: formIsSubmitting, errors } = form.formState;
258
+ const isSubmitting = externalIsSubmitting ?? formIsSubmitting;
259
+
260
+ // ✅ Get all fields that need to be watched for calculations and showWhen
261
+ const fieldsToWatch = useMemo(() => {
262
+ const watchFields = new Set<string>();
263
+ computedFields.forEach((field) => {
264
+ // Watch fields for calculations
265
+ if (field.calculate?.dependsOn) {
266
+ field.calculate.dependsOn.forEach((depField) => {
267
+ watchFields.add(depField);
268
+ });
269
+ }
270
+ // Watch fields for cascading
271
+ if (field.cascade?.triggerField) {
272
+ watchFields.add(field.cascade.triggerField);
273
+ }
274
+ // Watch fields for showWhen conditions
275
+ if (field.showWhen?.field) {
276
+ watchFields.add(field.showWhen.field);
277
+ }
278
+ });
279
+ return Array.from(watchFields);
280
+ }, [computedFields]);
281
+
282
+ // ✅ Watch form values - watch() without args returns all values as an object
283
+ const formValues = form.watch();
284
+
285
+ // Handle field calculations
286
+ useEffect(() => {
287
+ computedFields.forEach((field) => {
288
+ if (field.calculate) {
289
+ const { dependsOn, mode = "onChange" } = field.calculate;
290
+
291
+ // Check if any dependent field has changed
292
+ const hasDependentValues = dependsOn.every(
293
+ (depField) =>
294
+ formValues[depField] !== undefined &&
295
+ formValues[depField] !== null &&
296
+ formValues[depField] !== "",
297
+ );
298
+
299
+ if (hasDependentValues) {
300
+ const calculatedValue = calculateFieldValue(field, formValues);
301
+ const currentValue = form.getValues(field.name);
302
+
303
+ // Only update if value changed
304
+ if (
305
+ calculatedValue !== currentValue &&
306
+ calculatedValue !== undefined
307
+ ) {
308
+ form.setValue(field.name, calculatedValue, {
309
+ shouldValidate: false,
310
+ shouldDirty: false,
311
+ });
312
+ }
313
+ }
314
+ }
315
+ });
316
+ }, [form, formValues, computedFields]);
317
+
318
+ // Handle field cascading
319
+ useEffect(() => {
320
+ computedFields.forEach((field) => {
321
+ if (field.cascade) {
322
+ const { triggerField, action } = field.cascade;
323
+ const triggerValue = formValues[triggerField];
324
+ const previousTriggerValue = form.getValues(
325
+ `_previous_${triggerField}`,
326
+ );
327
+
328
+ // Check if trigger field changed
329
+ if (triggerValue !== previousTriggerValue) {
330
+ if (action === "clear") {
331
+ form.setValue(field.name, undefined, {
332
+ shouldValidate: false,
333
+ shouldDirty: false,
334
+ });
335
+ } else if (action === "reset") {
336
+ const defaultValue = field.defaultValue;
337
+ form.setValue(field.name, defaultValue, {
338
+ shouldValidate: false,
339
+ shouldDirty: false,
340
+ });
341
+ }
342
+
343
+ // Store current trigger value for next comparison
344
+ form.setValue(`_previous_${triggerField}`, triggerValue, {
345
+ shouldValidate: false,
346
+ shouldDirty: false,
347
+ });
348
+ }
349
+ }
350
+ });
351
+ }, [form, formValues, computedFields]);
352
+
353
+ // Get field labels for error summary
354
+ const getFieldLabel = (fieldName: string) => {
355
+ const field = computedFields.find((f) => f.name === fieldName);
356
+ return field?.label || fieldName;
357
+ };
358
+
359
+ // Check if field should be visible based on showWhen condition
360
+ const isFieldVisible = useMemo(() => {
361
+ return (field: FieldConfig): boolean => {
362
+ if (!field.showWhen) return true;
363
+
364
+ const {
365
+ field: watchField,
366
+ value,
367
+ operator = "equals",
368
+ } = field.showWhen;
369
+ const watchValue = formValues[watchField];
370
+
371
+ switch (operator) {
372
+ case "equals":
373
+ return watchValue === value;
374
+ case "notEquals":
375
+ return watchValue !== value;
376
+ case "contains":
377
+ return String(watchValue).includes(String(value));
378
+ case "greaterThan":
379
+ return Number(watchValue) > Number(value);
380
+ case "lessThan":
381
+ return Number(watchValue) < Number(value);
382
+ case "isEmpty":
383
+ return (
384
+ watchValue === undefined ||
385
+ watchValue === null ||
386
+ watchValue === ""
387
+ );
388
+ case "isNotEmpty":
389
+ return (
390
+ watchValue !== undefined &&
391
+ watchValue !== null &&
392
+ watchValue !== ""
393
+ );
394
+ default:
395
+ return watchValue === value;
396
+ }
397
+ };
398
+ }, [formValues]);
399
+
400
+ // Error summary
401
+ const errorEntries = Object.entries(errors);
402
+ const hasErrors = errorEntries.length > 0;
403
+
404
+ // Group fields by section
405
+ const groupedFields = useMemo(() => {
406
+ // Sort fields by order first, then filter
407
+ const sortedFields = sortFieldsByOrder(computedFields);
408
+ // Filter out: hidden fields, display-only fields (DTO fields), and fields that don't meet visibility conditions
409
+ const visibleFields = sortedFields.filter(
410
+ (field) =>
411
+ !field.hideInForm && !field.isDisplayOnly && isFieldVisible(field),
412
+ );
413
+
414
+ // If formSections is defined, use it
415
+ if (config.formSections && config.formSections.length > 0) {
416
+ return config.formSections.map((section) => ({
417
+ ...section,
418
+ fields: visibleFields.filter((field) =>
419
+ section.fields.includes(field.name),
420
+ ),
421
+ }));
422
+ }
423
+
424
+ // Otherwise, group by field.section property
425
+ const sectionsMap = new Map<string, FieldConfig[]>();
426
+ const ungroupedFields: FieldConfig[] = [];
427
+
428
+ visibleFields.forEach((field) => {
429
+ if (field.section) {
430
+ if (!sectionsMap.has(field.section)) {
431
+ sectionsMap.set(field.section, []);
432
+ }
433
+ sectionsMap.get(field.section)!.push(field);
434
+ } else {
435
+ ungroupedFields.push(field);
436
+ }
437
+ });
438
+
439
+ const sections: Array<{
440
+ title: string;
441
+ description?: string;
442
+ fields: FieldConfig[];
443
+ }> = [];
444
+
445
+ // Add grouped sections
446
+ sectionsMap.forEach((fields, title) => {
447
+ sections.push({ title, fields });
448
+ });
449
+
450
+ // Add ungrouped fields as a default section if there are any
451
+ if (ungroupedFields.length > 0) {
452
+ sections.push({ title: "", fields: ungroupedFields });
453
+ }
454
+
455
+ return sections.length > 0
456
+ ? sections
457
+ : [{ title: "", fields: visibleFields }];
458
+ }, [computedFields, config.formSections, isFieldVisible]);
459
+
460
+ // ✅ Progress calculation - use form.getValues() to get all values, not just watched ones
461
+ const progress = useMemo(() => {
462
+ const allFormValues = form.getValues(); // Get all form values for progress calculation
463
+ // Filter out: hidden fields, display-only fields (DTO fields), and fields that don't meet visibility conditions
464
+ const visibleFields = computedFields.filter(
465
+ (field) =>
466
+ !field.hideInForm && !field.isDisplayOnly && isFieldVisible(field),
467
+ );
468
+ const totalFields = visibleFields.length;
469
+ if (totalFields === 0) return 100;
470
+
471
+ const filledFields = visibleFields.filter((field) => {
472
+ const value = allFormValues[field.name];
473
+ return (
474
+ value !== undefined &&
475
+ value !== null &&
476
+ value !== "" &&
477
+ !(Array.isArray(value) && value.length === 0)
478
+ );
479
+ }).length;
480
+
481
+ return Math.round((filledFields / totalFields) * 100);
482
+ }, [form, computedFields, isFieldVisible]);
483
+
484
+ // Render field with two-column layout
485
+ const renderField = (field: FieldConfig) => {
486
+ const fullWidth =
487
+ field.fullWidth ||
488
+ field.type === "textarea" ||
489
+ field.type === "multiselect" ||
490
+ (field.type === "text" && field.rows && field.rows > 3);
491
+
492
+ return (
493
+ <div key={field.name} className={fullWidth ? "md:col-span-2" : ""}>
494
+ <CrudFieldRenderer field={field} mode={mode} />
495
+ </div>
496
+ );
497
+ };
498
+
499
+ // Check if we should use sections (if more than 1 section or sections have titles)
500
+ const useSections =
501
+ groupedFields.length > 1 || groupedFields.some((s) => s.title);
502
+
503
+ return (
504
+ <Form {...form}>
505
+ <form
506
+ ref={ref}
507
+ onSubmit={form.handleSubmit(handleSubmit)}
508
+ className="space-y-6"
509
+ >
510
+ {/* Draft Indicator */}
511
+ {hasDraft && mode === "create" && enableAutoSave && (
512
+ <Alert className="mb-4 border-blue-200 bg-blue-50/50 dark:bg-blue-950/20">
513
+ <Save className="h-4 w-4 text-blue-600" />
514
+ <AlertTitle className="text-blue-900 dark:text-blue-100">
515
+ Draft saved
516
+ </AlertTitle>
517
+ <AlertDescription className="text-blue-800 dark:text-blue-200">
518
+ {lastSaved
519
+ ? `Your changes were saved automatically at ${lastSaved.toLocaleTimeString()}.`
520
+ : "Your changes are being saved automatically."}
521
+ </AlertDescription>
522
+ </Alert>
523
+ )}
524
+
525
+ {/* Error Summary */}
526
+ {hasErrors && (
527
+ <Alert variant="destructive" className="mb-4">
528
+ <AlertCircle className="h-4 w-4" />
529
+ <AlertTitle>Please fix the following errors:</AlertTitle>
530
+ <AlertDescription>
531
+ <ul className="list-disc list-inside space-y-1 mt-2 text-sm">
532
+ {errorEntries.map(([fieldName, error]) => (
533
+ <li key={fieldName}>
534
+ <strong>{getFieldLabel(fieldName)}</strong>:{" "}
535
+ {error?.message as string}
536
+ </li>
537
+ ))}
538
+ </ul>
539
+ </AlertDescription>
540
+ </Alert>
541
+ )}
542
+
543
+ {/* Form Fields with Sections */}
544
+ {useSections ? (
545
+ <Accordion
546
+ type="multiple"
547
+ defaultValue={groupedFields
548
+ .map((_, index) => `section-${index}`)
549
+ .filter(
550
+ (_, index) =>
551
+ config.formSections?.[index]?.defaultOpen !== false,
552
+ )}
553
+ className="space-y-4"
554
+ >
555
+ {groupedFields.map((section, sectionIndex) => {
556
+ if (section.fields.length === 0) return null;
557
+
558
+ return (
559
+ <AccordionItem
560
+ key={`section-${sectionIndex}`}
561
+ value={`section-${sectionIndex}`}
562
+ className="border rounded-lg px-4"
563
+ >
564
+ {section.title && (
565
+ <AccordionTrigger className="hover:no-underline">
566
+ <div className="flex flex-col items-start text-left">
567
+ <CardTitle className="text-base font-semibold">
568
+ {section.title}
569
+ </CardTitle>
570
+ {section.description && (
571
+ <CardDescription className="text-xs mt-0.5">
572
+ {section.description}
573
+ </CardDescription>
574
+ )}
575
+ </div>
576
+ </AccordionTrigger>
577
+ )}
578
+ <AccordionContent>
579
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6 pt-2">
580
+ {section.fields.map(renderField)}
581
+ </div>
582
+ </AccordionContent>
583
+ </AccordionItem>
584
+ );
585
+ })}
586
+ </Accordion>
587
+ ) : (
588
+ /* Simple two-column layout without sections */
589
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
590
+ {groupedFields[0]?.fields.map(renderField)}
591
+ </div>
592
+ )}
593
+
594
+ {/* Form Actions */}
595
+ <div className="flex items-center justify-between gap-2 pt-4 border-t sticky bottom-0 bg-background -mx-4 sm:-mx-6 px-4 sm:px-6 pb-0">
596
+ {/* Draft indicator on left */}
597
+ {hasDraft && mode === "create" && enableAutoSave && (
598
+ <Badge variant="secondary" className="text-xs">
599
+ <Save className="mr-1 h-3 w-3" />
600
+ Draft saved
601
+ </Badge>
602
+ )}
603
+
604
+ {/* Action buttons on right */}
605
+ <div className="flex gap-2 ml-auto">
606
+ {onCancel && (
607
+ <Button
608
+ type="button"
609
+ variant="outline"
610
+ onClick={handleCancel}
611
+ disabled={isSubmitting}
612
+ className="min-w-[80px]"
613
+ >
614
+ {cancelLabel}
615
+ </Button>
616
+ )}
617
+ <Button
618
+ type="submit"
619
+ disabled={isSubmitting}
620
+ className="min-w-[100px]"
621
+ >
622
+ {isSubmitting ? (
623
+ <>
624
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
625
+ <span className="hidden sm:inline">{submittingLabel}</span>
626
+ <span className="sm:hidden">...</span>
627
+ </>
628
+ ) : (
629
+ submitLabel
630
+ )}
631
+ </Button>
632
+ </div>
633
+ </div>
634
+ </form>
635
+ </Form>
636
+ );
637
+ },
638
+ );
639
+
640
+ CrudFormComponent.displayName = "CrudForm";
641
+
642
+ export const CrudForm = CrudFormComponent;