@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,290 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import {
3
+ parseCSV,
4
+ parseJSON,
5
+ validateImportData,
6
+ exportToCSV,
7
+ exportToJSON,
8
+ exportToXLSX,
9
+ } from "./import-export-service";
10
+ import type { EntityConfig } from "../../types";
11
+ import * as XLSX from "xlsx";
12
+
13
+ // Polyfill File.text() if missing or just mock it on instances
14
+ function createFile(content: string, name: string, type: string) {
15
+ const file = new File([content], name, { type });
16
+ // Mock text method
17
+ Object.defineProperty(file, "text", {
18
+ value: () => Promise.resolve(content),
19
+ writable: true,
20
+ });
21
+ // Mock arrayBuffer
22
+ Object.defineProperty(file, "arrayBuffer", {
23
+ value: () => Promise.resolve(new TextEncoder().encode(content)),
24
+ writable: true,
25
+ });
26
+ return file;
27
+ }
28
+
29
+ // Mock xlsx
30
+ vi.mock("xlsx", () => ({
31
+ read: vi.fn(() => ({
32
+ SheetNames: ["Sheet1"],
33
+ Sheets: {
34
+ Sheet1: {},
35
+ },
36
+ })),
37
+ utils: {
38
+ sheet_to_json: vi.fn(() => [{ name: "Alice", age: 30 }]),
39
+ book_new: vi.fn(() => ({})),
40
+ aoa_to_sheet: vi.fn(),
41
+ book_append_sheet: vi.fn(),
42
+ json_to_sheet: vi.fn(),
43
+ },
44
+ write: vi.fn(() => new ArrayBuffer(8)),
45
+ }));
46
+
47
+ describe("ImportExportService", () => {
48
+ describe("parseCSV", () => {
49
+ it("should parse valid CSV", async () => {
50
+ const csvContent = "name,age\nAlice,30\nBob,25";
51
+ const file = createFile(csvContent, "test.csv", "text/csv");
52
+ const result = await parseCSV(file);
53
+ expect(result).toHaveLength(2);
54
+ expect(result[0]).toEqual({ name: "Alice", age: "30" });
55
+ expect(result[1]).toEqual({ name: "Bob", age: "25" });
56
+ });
57
+
58
+ it("should handle empty file", async () => {
59
+ const file = createFile("", "empty.csv", "text/csv");
60
+ const result = await parseCSV(file);
61
+ expect(result).toEqual([]);
62
+ });
63
+ });
64
+
65
+ describe("parseJSON", () => {
66
+ it("should parse valid JSON array", async () => {
67
+ const jsonContent = JSON.stringify([{ name: "Alice", age: 30 }]);
68
+ const file = createFile(jsonContent, "test.json", "application/json");
69
+ const result = await parseJSON(file);
70
+ expect(result).toHaveLength(1);
71
+ expect(result[0]).toEqual({ name: "Alice", age: 30 });
72
+ });
73
+
74
+ it("should parse single JSON object", async () => {
75
+ const jsonContent = JSON.stringify({ name: "Alice", age: 30 });
76
+ const file = createFile(jsonContent, "test.json", "application/json");
77
+ const result = await parseJSON(file);
78
+ expect(result).toHaveLength(1);
79
+ expect(result[0]).toEqual({ name: "Alice", age: 30 });
80
+ });
81
+ });
82
+
83
+ describe("validateImportData", () => {
84
+ const mockConfig: EntityConfig = {
85
+ fields: [
86
+ { name: "name", label: "Name", type: "text", required: true },
87
+ { name: "age", label: "Age", type: "number" },
88
+ { name: "email", label: "Email", type: "email" },
89
+ ],
90
+ apiEndpoint: "/api/test",
91
+ name: "test",
92
+ label: "Test",
93
+ pluralLabel: "Tests",
94
+ idField: "id",
95
+ displayField: "name",
96
+ } as unknown as EntityConfig; // Partial mock
97
+
98
+ it("should validate valid data", () => {
99
+ const data = [{ name: "Alice", age: "30", email: "alice@example.com" }];
100
+ const result = validateImportData(data, mockConfig);
101
+ expect(result.success).toBe(true);
102
+ expect(result.errors).toHaveLength(0);
103
+ });
104
+
105
+ it("should catch required field errors", () => {
106
+ const data = [{ age: "30" }]; // missing name
107
+ const result = validateImportData(data, mockConfig);
108
+ expect(result.success).toBe(false);
109
+ expect(result.errors[0].message).toContain("Name là bắt buộc");
110
+ });
111
+
112
+ it("should catch type errors", () => {
113
+ const data = [
114
+ { name: "Bob", age: "invalid" },
115
+ { name: "Charlie", email: "not-an-email" },
116
+ ];
117
+ const result = validateImportData(data, mockConfig);
118
+ expect(result.success).toBe(false);
119
+ expect(result.errors).toHaveLength(2);
120
+ });
121
+
122
+ it("should validate case-insensitive options", () => {
123
+ const configWithOptions: EntityConfig = {
124
+ ...mockConfig,
125
+ fields: [
126
+ ...mockConfig.fields,
127
+ {
128
+ name: "status",
129
+ label: "Status",
130
+ type: "select",
131
+ options: [
132
+ { label: "Active", value: "active" },
133
+ { label: "Inactive", value: "inactive" },
134
+ ],
135
+ },
136
+ ],
137
+ };
138
+
139
+ const data = [
140
+ { name: "User1", status: "Active" },
141
+ { name: "User2", status: "ACTIVE" },
142
+ { name: "User3", status: "active" },
143
+ ];
144
+ const result = validateImportData(data, configWithOptions);
145
+ expect(result.success).toBe(true);
146
+ expect(result.errors).toHaveLength(0);
147
+ });
148
+
149
+ it("should reject invalid options", () => {
150
+ const configWithOptions: EntityConfig = {
151
+ ...mockConfig,
152
+ fields: [
153
+ ...mockConfig.fields,
154
+ {
155
+ name: "status",
156
+ label: "Status",
157
+ type: "select",
158
+ options: ["red", "blue"],
159
+ },
160
+ ],
161
+ };
162
+
163
+ const data = [{ name: "User1", status: "green" }];
164
+ const result = validateImportData(data, configWithOptions);
165
+ expect(result.success).toBe(false);
166
+ expect(result.errors[0].message).toContain("Status không hợp lệ");
167
+ });
168
+ });
169
+
170
+ describe("exportToCSV", () => {
171
+ it("should export data to CSV", () => {
172
+ const data = [{ name: "Alice", age: 30 }];
173
+ const result = exportToCSV(data);
174
+ expect(result).toContain("name,age");
175
+ expect(result).toContain("Alice,30");
176
+ });
177
+
178
+ it("should handle specific fields", () => {
179
+ const data = [{ name: "Alice", age: 30, hidden: "secret" }];
180
+ const result = exportToCSV(data, ["name"]);
181
+ expect(result).toContain("name");
182
+ expect(result).not.toContain("age");
183
+ expect(result).toContain("Alice");
184
+ });
185
+ });
186
+
187
+ describe("exportToJSON", () => {
188
+ it("should export data to JSON", () => {
189
+ const data = [{ name: "Alice", age: 30 }];
190
+ const result = exportToJSON(data);
191
+ expect(JSON.parse(result)).toEqual(data);
192
+ });
193
+
194
+ it("should handle specific fields", () => {
195
+ const data = [{ name: "Alice", age: 30, hidden: "secret" }];
196
+ const result = exportToJSON(data, ["name"]);
197
+ const parsed = JSON.parse(result);
198
+ expect(parsed[0]).toHaveProperty("name");
199
+ expect(parsed[0]).not.toHaveProperty("age");
200
+ });
201
+ });
202
+
203
+ // Since we mocked xlsx, we can test using those mocks
204
+ describe("XLSX functions", () => {
205
+ it("should parse XLSX", async () => {
206
+ const { parseXLSX } = await import("./import-export-service");
207
+ const file = createFile(
208
+ "dummy",
209
+ "test.xlsx",
210
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
211
+ );
212
+ const result = await parseXLSX(file);
213
+ expect(result).toEqual([{ name: "Alice", age: 30 }]);
214
+ });
215
+
216
+ it("should generate XLSX template", async () => {
217
+ const { generateXLSXTemplate } = await import("./import-export-service");
218
+ const mockConfig: EntityConfig = {
219
+ fields: [{ name: "name", label: "Name", type: "text" }],
220
+ apiEndpoint: "/api",
221
+ name: "test",
222
+ label: "Test",
223
+ pluralLabel: "Tests",
224
+ idField: "id",
225
+ displayField: "name",
226
+ } as any;
227
+ const blob = await generateXLSXTemplate(mockConfig);
228
+ expect(blob).toBeInstanceOf(Blob);
229
+ });
230
+
231
+ it("should export to XLSX", async () => {
232
+ const { exportToXLSX } = await import("./import-export-service");
233
+ const data = [{ name: "Alice" }];
234
+ const blob = await exportToXLSX(data);
235
+ expect(blob).toBeInstanceOf(Blob);
236
+ });
237
+
238
+ it("should export to XLSX with specific fields", async () => {
239
+ const { exportToXLSX } = await import("./import-export-service");
240
+ const data = [{ name: "Alice", age: 30 }];
241
+ const blob = await exportToXLSX(data, ["name"]);
242
+ expect(blob).toBeInstanceOf(Blob);
243
+ // Verify filtered data passed to xlsx (via mock)
244
+ const XLSX = await import("xlsx");
245
+ expect(XLSX.utils.json_to_sheet).toHaveBeenCalledWith(
246
+ expect.arrayContaining([expect.objectContaining({ name: "Alice" })]),
247
+ );
248
+ // Should NOT check for age absence strictly on the mock call args because json_to_sheet receives the mapped array
249
+ // But we can check expected structure if we spy carefully.
250
+ // In this mock, we just check call happened.
251
+ });
252
+ });
253
+
254
+ describe("downloadFile", () => {
255
+ it("should trigger download", async () => {
256
+ const { downloadFile } = await import("./import-export-service");
257
+ const blob = new Blob(["test"]);
258
+
259
+ // Mock DOM
260
+ const link = {
261
+ href: "",
262
+ download: "",
263
+ click: vi.fn(),
264
+ style: {},
265
+ };
266
+ const createElementSpy = vi
267
+ .spyOn(document, "createElement")
268
+ .mockReturnValue(link as any);
269
+ const appendChildSpy = vi
270
+ .spyOn(document.body, "appendChild")
271
+ .mockImplementation(() => link as any);
272
+ const removeChildSpy = vi
273
+ .spyOn(document.body, "removeChild")
274
+ .mockImplementation(() => link as any);
275
+
276
+ // Mock URL
277
+ global.URL.createObjectURL = vi.fn(() => "blob:url");
278
+ global.URL.revokeObjectURL = vi.fn();
279
+
280
+ downloadFile(blob, "test.csv");
281
+
282
+ expect(createElementSpy).toHaveBeenCalledWith("a");
283
+ expect(link.download).toBe("test.csv");
284
+ expect(link.click).toHaveBeenCalled();
285
+ expect(appendChildSpy).toHaveBeenCalled();
286
+ expect(removeChildSpy).toHaveBeenCalled();
287
+ expect(global.URL.revokeObjectURL).toHaveBeenCalledWith("blob:url");
288
+ });
289
+ });
290
+ });
@@ -0,0 +1,352 @@
1
+ import type { EntityConfig, ImportOptions, ImportResult } from "../../types";
2
+
3
+ function getRowValue(
4
+ row: Record<string, unknown>,
5
+ field: { name: string; label: string },
6
+ ): unknown {
7
+ // Prefer machine key (field.name), but support human header (field.label)
8
+ // for backward compatibility with older templates.
9
+ if (Object.prototype.hasOwnProperty.call(row, field.name)) {
10
+ return row[field.name];
11
+ }
12
+ if (Object.prototype.hasOwnProperty.call(row, field.label)) {
13
+ return row[field.label];
14
+ }
15
+ return undefined;
16
+ }
17
+
18
+ /**
19
+ * Parse CSV file
20
+ */
21
+ export async function parseCSV(file: File): Promise<Record<string, unknown>[]> {
22
+ const text = await file.text();
23
+ const lines = text.split("\n").filter((line) => line.trim());
24
+ if (lines.length === 0) return [];
25
+
26
+ const headers = lines[0].split(",").map((h) => h.trim());
27
+ const rows: Record<string, unknown>[] = [];
28
+
29
+ for (let i = 1; i < lines.length; i++) {
30
+ const values = lines[i].split(",").map((v) => v.trim());
31
+ const row: Record<string, unknown> = {};
32
+ headers.forEach((header, index) => {
33
+ row[header] = values[index] || "";
34
+ });
35
+ rows.push(row);
36
+ }
37
+
38
+ return rows;
39
+ }
40
+
41
+ /**
42
+ * Parse JSON file
43
+ */
44
+ export async function parseJSON(
45
+ file: File,
46
+ ): Promise<Record<string, unknown>[]> {
47
+ const text = await file.text();
48
+ const data = JSON.parse(text);
49
+ return Array.isArray(data) ? data : [data];
50
+ }
51
+
52
+ /**
53
+ * Parse XLSX file (requires xlsx library)
54
+ */
55
+ export async function parseXLSX(
56
+ file: File,
57
+ ): Promise<Record<string, unknown>[]> {
58
+ // Dynamic import to avoid bundling xlsx if not needed
59
+ const XLSX = await import("xlsx");
60
+ const arrayBuffer = await file.arrayBuffer();
61
+ const workbook = XLSX.read(arrayBuffer, { type: "array" });
62
+ const firstSheet = workbook.Sheets[workbook.SheetNames[0]];
63
+ return XLSX.utils.sheet_to_json(firstSheet);
64
+ }
65
+
66
+ /**
67
+ * Parse file based on format
68
+ */
69
+ export async function parseFile(
70
+ file: File,
71
+ format: ImportOptions["format"],
72
+ ): Promise<Record<string, unknown>[]> {
73
+ switch (format) {
74
+ case "csv":
75
+ return parseCSV(file);
76
+ case "json":
77
+ return parseJSON(file);
78
+ case "xlsx":
79
+ return parseXLSX(file);
80
+ default:
81
+ throw new Error(`Unsupported format: ${format}`);
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Validate import data against field configs
87
+ */
88
+ export function validateImportData(
89
+ data: Record<string, unknown>[],
90
+ config: EntityConfig,
91
+ options: Partial<ImportOptions> = {},
92
+ ): ImportResult {
93
+ const errors: ImportResult["errors"] = [];
94
+ let imported = 0;
95
+ let failed = 0;
96
+
97
+ data.forEach((row, index) => {
98
+ const rowErrors: string[] = [];
99
+
100
+ config.fields.forEach((field) => {
101
+ if (field.hideInForm) return;
102
+
103
+ const value = getRowValue(row, { name: field.name, label: field.label });
104
+
105
+ // Required check
106
+ if (
107
+ field.required &&
108
+ (value === null || value === undefined || value === "")
109
+ ) {
110
+ rowErrors.push(`${field.label} là bắt buộc`);
111
+ }
112
+
113
+ // List validation (check if value is in options)
114
+ if (
115
+ value !== null &&
116
+ value !== undefined &&
117
+ value !== "" &&
118
+ field.options &&
119
+ field.options.length > 0
120
+ ) {
121
+ const stringValue = String(value).toLowerCase();
122
+ const validOption = field.options.some((opt) => {
123
+ const isObject = typeof opt === "object" && opt !== null;
124
+ const optValue = isObject ? opt.value : opt;
125
+ return String(optValue).toLowerCase() === stringValue;
126
+ });
127
+
128
+ if (!validOption) {
129
+ // Collect valid labels for error message
130
+ const validLabels = field.options
131
+ .map((opt) => {
132
+ const isObject = typeof opt === "object" && opt !== null;
133
+ return isObject ? (opt as any).label : String(opt);
134
+ })
135
+ .join(", ");
136
+ rowErrors.push(
137
+ `${field.label} không hợp lệ. Chỉ chấp nhận: ${validLabels}`,
138
+ );
139
+ }
140
+ } else if (value !== null && value !== undefined && value !== "") {
141
+ // Type validation (only if not a list field or validated above)
142
+ switch (field.type) {
143
+ case "email":
144
+ if (
145
+ typeof value === "string" &&
146
+ !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
147
+ ) {
148
+ rowErrors.push(`${field.label} phải là email hợp lệ`);
149
+ }
150
+ break;
151
+ case "number":
152
+ case "integer":
153
+ if (isNaN(Number(value))) {
154
+ rowErrors.push(`${field.label} phải là số`);
155
+ }
156
+ break;
157
+ }
158
+ }
159
+ // End validation
160
+ });
161
+
162
+ if (rowErrors.length > 0) {
163
+ failed++;
164
+ rowErrors.forEach((error) => {
165
+ errors.push({
166
+ row: index + 1,
167
+ field: "",
168
+ message: error,
169
+ });
170
+ });
171
+ } else {
172
+ imported++;
173
+ }
174
+ });
175
+
176
+ return {
177
+ success: failed === 0 || options.skipErrors === true,
178
+ imported,
179
+ failed,
180
+ errors,
181
+ };
182
+ }
183
+
184
+ /**
185
+ * Generate CSV template from config
186
+ */
187
+ export function generateCSVTemplate(config: EntityConfig): string {
188
+ const headers = config.fields
189
+ .filter((field) => !field.hideInForm || field.showInImport)
190
+ .map((field) => field.label);
191
+
192
+ return headers.join(",") + "\n";
193
+ }
194
+
195
+ /**
196
+ * Generate JSON template from config
197
+ */
198
+ export function generateJSONTemplate(
199
+ config: EntityConfig,
200
+ ): Record<string, unknown> {
201
+ const template: Record<string, unknown> = {};
202
+
203
+ config.fields
204
+ .filter((field) => !field.hideInForm || field.showInImport)
205
+ .forEach((field) => {
206
+ // For JSON, we still use technical names as keys because JSON is developer-centric
207
+ // and usually requires strict structure.
208
+ template[field.name] = field.defaultValue || "";
209
+ });
210
+
211
+ return template;
212
+ }
213
+
214
+ /**
215
+ * Generate XLSX template (requires xlsx library)
216
+ */
217
+ export async function generateXLSXTemplate(
218
+ config: EntityConfig,
219
+ ): Promise<Blob> {
220
+ const XLSX = await import("xlsx");
221
+
222
+ const importHeaders = config.fields
223
+ .filter((field) => !field.hideInForm || field.showInImport)
224
+ .map((field) => field.label);
225
+
226
+ const workbook = XLSX.utils.book_new();
227
+
228
+ // Sheet 1: actual import template (machine headers)
229
+ const templateSheet = XLSX.utils.aoa_to_sheet([importHeaders]);
230
+ XLSX.utils.book_append_sheet(workbook, templateSheet, "Import");
231
+
232
+ // Sheet 2: guide for humans (doesn't affect parsing because parser reads first sheet)
233
+ const guideRows = [
234
+ ["field", "label", "type", "required"],
235
+ ...config.fields
236
+ .filter((field) => !field.hideInForm || field.showInImport)
237
+ .map((field) => [
238
+ field.name,
239
+ field.label,
240
+ field.type,
241
+ field.required ? "yes" : "no",
242
+ ]),
243
+ ];
244
+ const guideSheet = XLSX.utils.aoa_to_sheet(guideRows);
245
+ XLSX.utils.book_append_sheet(workbook, guideSheet, "Guide");
246
+
247
+ const buffer = XLSX.write(workbook, { type: "array", bookType: "xlsx" });
248
+ return new Blob([buffer], {
249
+ type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
250
+ });
251
+ }
252
+
253
+ /**
254
+ * Export data to CSV
255
+ */
256
+ export function exportToCSV(
257
+ data: Record<string, unknown>[],
258
+ fields?: string[],
259
+ ): string {
260
+ if (data.length === 0) return "";
261
+
262
+ const keys = fields || Object.keys(data[0]);
263
+ const headers = keys.join(",");
264
+ const rows = data.map((row) =>
265
+ keys.map((key) => String(row[key] || "")).join(","),
266
+ );
267
+
268
+ return [headers, ...rows].join("\n");
269
+ }
270
+
271
+ /**
272
+ * Export data to JSON
273
+ */
274
+ export function exportToJSON(
275
+ data: Record<string, unknown>[],
276
+ fields?: string[],
277
+ ): string {
278
+ if (fields) {
279
+ const filtered = data.map((row) => {
280
+ const filteredRow: Record<string, unknown> = {};
281
+ fields.forEach((field) => {
282
+ filteredRow[field] = row[field];
283
+ });
284
+ return filteredRow;
285
+ });
286
+ return JSON.stringify(filtered, null, 2);
287
+ }
288
+
289
+ return JSON.stringify(data, null, 2);
290
+ }
291
+
292
+ /**
293
+ * Export data to XLSX (requires xlsx library)
294
+ */
295
+ export async function exportToXLSX(
296
+ data: Record<string, unknown>[],
297
+ fields?: string[],
298
+ ): Promise<Blob> {
299
+ const XLSX = await import("xlsx");
300
+
301
+ let exportData: Record<string, unknown>[];
302
+ if (fields) {
303
+ exportData = data.map((row) => {
304
+ const filteredRow: Record<string, unknown> = {};
305
+ fields.forEach((field) => {
306
+ const value = row[field];
307
+ filteredRow[field] = Array.isArray(value)
308
+ ? value.join(", ")
309
+ : typeof value === "object" && value !== null
310
+ ? JSON.stringify(value)
311
+ : value;
312
+ });
313
+ return filteredRow;
314
+ });
315
+ } else {
316
+ exportData = data.map((row) => {
317
+ const newRow: Record<string, unknown> = {};
318
+ Object.keys(row).forEach((key) => {
319
+ const value = row[key];
320
+ newRow[key] = Array.isArray(value)
321
+ ? value.join(", ")
322
+ : typeof value === "object" && value !== null
323
+ ? JSON.stringify(value)
324
+ : value;
325
+ });
326
+ return newRow;
327
+ });
328
+ }
329
+
330
+ const worksheet = XLSX.utils.json_to_sheet(exportData);
331
+ const workbook = XLSX.utils.book_new();
332
+ XLSX.utils.book_append_sheet(workbook, worksheet, "Data");
333
+
334
+ const buffer = XLSX.write(workbook, { type: "array", bookType: "xlsx" });
335
+ return new Blob([buffer], {
336
+ type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
337
+ });
338
+ }
339
+
340
+ /**
341
+ * Download file
342
+ */
343
+ export function downloadFile(blob: Blob, filename: string): void {
344
+ const url = URL.createObjectURL(blob);
345
+ const link = document.createElement("a");
346
+ link.href = url;
347
+ link.download = filename;
348
+ document.body.appendChild(link);
349
+ link.click();
350
+ document.body.removeChild(link);
351
+ URL.revokeObjectURL(url);
352
+ }