@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,536 @@
1
+ "use client";
2
+
3
+ import {
4
+ createContext,
5
+ useContext,
6
+ useEffect,
7
+ useReducer,
8
+ useCallback,
9
+ useRef,
10
+ } from "react";
11
+ import { usePathname, useRouter } from "next/navigation";
12
+ import type { ReactNode } from "react";
13
+
14
+ import type { DynamicIconNameType, NavigationType } from "../../types";
15
+ import {
16
+ findRouteTitle,
17
+ findRouteIcon,
18
+ normalizePathname,
19
+ shouldExcludeFromTabs,
20
+ } from "../../utils";
21
+
22
+ export interface Tab {
23
+ id: string;
24
+ path: string;
25
+ title: string;
26
+ createdAt: number;
27
+ badge?: number | string;
28
+ isLoading?: boolean;
29
+ hasUnsavedChanges?: boolean;
30
+ isPinned?: boolean;
31
+ iconName?: DynamicIconNameType;
32
+ }
33
+
34
+ interface TabNavigationState {
35
+ tabs: Tab[];
36
+ activeTabId: string | null;
37
+ }
38
+
39
+ type TabNavigationAction =
40
+ | {
41
+ type: "ADD_TAB";
42
+ payload: { path: string; title: string; iconName?: DynamicIconNameType };
43
+ }
44
+ | { type: "REMOVE_TAB"; payload: { id: string } }
45
+ | { type: "SET_ACTIVE_TAB"; payload: { id: string } }
46
+ | { type: "LOAD_STATE"; payload: TabNavigationState }
47
+ | { type: "CLEAR_TABS" }
48
+ | { type: "REMOVE_OTHER_TABS"; payload: { id: string } }
49
+ | { type: "REMOVE_TABS_TO_RIGHT"; payload: { id: string } }
50
+ | { type: "MOVE_TAB"; payload: { fromIndex: number; toIndex: number } };
51
+
52
+ const STORAGE_KEY = "tab-navigation-state";
53
+
54
+ function tabNavigationReducer(
55
+ state: TabNavigationState,
56
+ action: TabNavigationAction,
57
+ ): TabNavigationState {
58
+ switch (action.type) {
59
+ case "ADD_TAB": {
60
+ const { path, title, iconName } = action.payload;
61
+ const normalizedPath = normalizePathname(path);
62
+ const tabId = normalizedPath;
63
+
64
+ // Check if tab already exists
65
+ const existingTab = state.tabs.find((tab) => tab.id === tabId);
66
+ if (existingTab) {
67
+ return {
68
+ ...state,
69
+ activeTabId: tabId,
70
+ };
71
+ }
72
+
73
+ // Add new tab
74
+ const newTab: Tab = {
75
+ id: tabId,
76
+ path,
77
+ title,
78
+ createdAt: Date.now(),
79
+ iconName,
80
+ };
81
+
82
+ return {
83
+ tabs: [...state.tabs, newTab],
84
+ activeTabId: tabId,
85
+ };
86
+ }
87
+
88
+ case "REMOVE_TAB": {
89
+ const { id } = action.payload;
90
+ const filteredTabs = state.tabs.filter((tab) => tab.id !== id);
91
+
92
+ // If removing active tab, switch to another tab
93
+ let newActiveTabId = state.activeTabId;
94
+ if (id === state.activeTabId) {
95
+ const currentIndex = state.tabs.findIndex((tab) => tab.id === id);
96
+ // Try to activate tab to the right, then left, then first available
97
+ if (currentIndex < filteredTabs.length) {
98
+ newActiveTabId = filteredTabs[currentIndex]?.id ?? null;
99
+ } else if (filteredTabs.length > 0) {
100
+ newActiveTabId = filteredTabs[filteredTabs.length - 1]?.id ?? null;
101
+ } else {
102
+ newActiveTabId = null;
103
+ }
104
+ }
105
+
106
+ return {
107
+ tabs: filteredTabs,
108
+ activeTabId: newActiveTabId,
109
+ };
110
+ }
111
+
112
+ case "SET_ACTIVE_TAB": {
113
+ const { id } = action.payload;
114
+ // Only set active if tab exists
115
+ const tabExists = state.tabs.some((tab) => tab.id === id);
116
+ return {
117
+ ...state,
118
+ activeTabId: tabExists ? id : state.activeTabId,
119
+ };
120
+ }
121
+
122
+ case "LOAD_STATE": {
123
+ return action.payload;
124
+ }
125
+
126
+ case "CLEAR_TABS": {
127
+ return {
128
+ tabs: [],
129
+ activeTabId: null,
130
+ };
131
+ }
132
+
133
+ case "REMOVE_OTHER_TABS": {
134
+ const { id } = action.payload;
135
+ const tabToKeep = state.tabs.find((tab) => tab.id === id);
136
+ if (!tabToKeep) return state;
137
+
138
+ return {
139
+ tabs: [tabToKeep],
140
+ activeTabId: id,
141
+ };
142
+ }
143
+
144
+ case "REMOVE_TABS_TO_RIGHT": {
145
+ const { id } = action.payload;
146
+ const currentIndex = state.tabs.findIndex((tab) => tab.id === id);
147
+ if (currentIndex === -1) return state;
148
+
149
+ const filteredTabs = state.tabs.slice(0, currentIndex + 1);
150
+ return {
151
+ tabs: filteredTabs,
152
+ activeTabId: state.activeTabId,
153
+ };
154
+ }
155
+
156
+ case "MOVE_TAB": {
157
+ const { fromIndex, toIndex } = action.payload;
158
+ const newTabs = [...state.tabs];
159
+ const [movedTab] = newTabs.splice(fromIndex, 1);
160
+ newTabs.splice(toIndex, 0, movedTab);
161
+
162
+ return {
163
+ ...state,
164
+ tabs: newTabs,
165
+ };
166
+ }
167
+
168
+ default:
169
+ return state;
170
+ }
171
+ }
172
+
173
+ const initialState: TabNavigationState = {
174
+ tabs: [],
175
+ activeTabId: null,
176
+ };
177
+
178
+ interface TabNavigationContextValue {
179
+ tabs: Tab[];
180
+ activeTabId: string | null;
181
+ addTab: (path: string, title?: string) => void;
182
+ removeTab: (id: string) => void;
183
+ setActiveTab: (id: string) => void;
184
+ clearTabs: () => void;
185
+ removeOtherTabs: (id: string) => void;
186
+ removeTabsToRight: (id: string) => void;
187
+ goToNextTab: () => void;
188
+ goToPreviousTab: () => void;
189
+ goToTabByIndex: (index: number) => void;
190
+ closeAndGoToParent: () => void;
191
+ }
192
+
193
+ const TabNavigationContext = createContext<
194
+ TabNavigationContextValue | undefined
195
+ >(undefined);
196
+
197
+ export function TabNavigationProvider({
198
+ children,
199
+ navigations,
200
+ }: {
201
+ children: ReactNode;
202
+ navigations: NavigationType[];
203
+ }) {
204
+ const [state, dispatch] = useReducer(tabNavigationReducer, initialState);
205
+ const pathname = usePathname();
206
+ const router = useRouter();
207
+ const isInitialized = useRef(false);
208
+
209
+ // Load state from sessionStorage on mount
210
+ useEffect(() => {
211
+ if (typeof window === "undefined") return;
212
+
213
+ try {
214
+ const stored = sessionStorage.getItem(STORAGE_KEY);
215
+ if (stored) {
216
+ const parsedState = JSON.parse(stored) as TabNavigationState;
217
+ dispatch({ type: "LOAD_STATE", payload: parsedState });
218
+ }
219
+ } catch (error) {
220
+ console.error("Failed to load tab navigation state:", error);
221
+ }
222
+ isInitialized.current = true;
223
+ }, []);
224
+
225
+ // Save state to sessionStorage whenever it changes
226
+ useEffect(() => {
227
+ if (!isInitialized.current) return;
228
+
229
+ try {
230
+ sessionStorage.setItem(STORAGE_KEY, JSON.stringify(state));
231
+ } catch (error) {
232
+ console.error("Failed to save tab navigation state:", error);
233
+ }
234
+ }, [state]);
235
+
236
+ // Auto-add tab when pathname changes
237
+ useEffect(() => {
238
+ if (!pathname || !isInitialized.current) return;
239
+
240
+ // Skip excluded routes
241
+ if (shouldExcludeFromTabs(pathname)) {
242
+ return;
243
+ }
244
+
245
+ // Get title and icon from navigation data or use pathname
246
+ const title = findRouteTitle(pathname, navigations) || "Page";
247
+ const iconName = findRouteIcon(pathname, navigations) || undefined;
248
+ const normalizedPath = normalizePathname(pathname);
249
+
250
+ // Check if tab already exists
251
+ const existingTab = state.tabs.find((tab) => tab.id === normalizedPath);
252
+ if (existingTab) {
253
+ // Just set as active if not already active
254
+ if (state.activeTabId !== normalizedPath) {
255
+ dispatch({ type: "SET_ACTIVE_TAB", payload: { id: normalizedPath } });
256
+ }
257
+ } else {
258
+ // Add new tab
259
+ dispatch({
260
+ type: "ADD_TAB",
261
+ payload: { path: pathname, title, iconName },
262
+ });
263
+ }
264
+ // eslint-disable-next-line react-hooks/exhaustive-deps
265
+ }, [pathname, navigations]);
266
+
267
+ // Prefetch all tab routes when tabs change
268
+ useEffect(() => {
269
+ if (!isInitialized.current) return;
270
+
271
+ // Prefetch all tab routes in background
272
+ state.tabs.forEach((tab) => {
273
+ if (tab.path && tab.id !== state.activeTabId) {
274
+ router.prefetch(tab.path);
275
+ }
276
+ });
277
+ }, [state.tabs, state.activeTabId, router]);
278
+
279
+ const addTab = useCallback(
280
+ (path: string, title?: string) => {
281
+ const tabTitle = title || findRouteTitle(path, navigations) || "Page";
282
+ const iconName = findRouteIcon(path, navigations) || undefined;
283
+ dispatch({
284
+ type: "ADD_TAB",
285
+ payload: { path, title: tabTitle, iconName },
286
+ });
287
+ },
288
+ [navigations],
289
+ );
290
+
291
+ const removeTab = useCallback(
292
+ (id: string) => {
293
+ // Check if removing active tab before dispatch
294
+ const tabToRemove = state.tabs.find((tab) => tab.id === id);
295
+ const isRemovingActive = tabToRemove?.id === state.activeTabId;
296
+ const remainingTabs = state.tabs.filter((tab) => tab.id !== id);
297
+
298
+ dispatch({ type: "REMOVE_TAB", payload: { id } });
299
+
300
+ // Navigate to new active tab or dashboard if removing active tab
301
+ if (isRemovingActive) {
302
+ if (remainingTabs.length > 0) {
303
+ const nextTab = remainingTabs[remainingTabs.length - 1];
304
+ router.replace(nextTab.path, { scroll: false });
305
+ } else {
306
+ // Navigate to dashboard/home
307
+ const locale = pathname.split("/")[1];
308
+ router.replace(locale ? `/${locale}` : "/", { scroll: false });
309
+ }
310
+ }
311
+ },
312
+ [state.tabs, state.activeTabId, router, pathname],
313
+ );
314
+
315
+ const setActiveTab = useCallback(
316
+ (id: string) => {
317
+ const tab = state.tabs.find((t) => t.id === id);
318
+ if (tab) {
319
+ dispatch({ type: "SET_ACTIVE_TAB", payload: { id } });
320
+ // Use replace instead of push to avoid adding to history
321
+ // Use scroll: false to prevent scroll restoration
322
+ router.replace(tab.path, { scroll: false });
323
+ }
324
+ },
325
+ [state.tabs, router],
326
+ );
327
+
328
+ const clearTabs = useCallback(() => {
329
+ dispatch({ type: "CLEAR_TABS" });
330
+ }, []);
331
+
332
+ const removeOtherTabs = useCallback(
333
+ (id: string) => {
334
+ dispatch({ type: "REMOVE_OTHER_TABS", payload: { id } });
335
+ const tab = state.tabs.find((t) => t.id === id);
336
+ if (tab) {
337
+ router.replace(tab.path, { scroll: false });
338
+ }
339
+ },
340
+ [state.tabs, router],
341
+ );
342
+
343
+ const removeTabsToRight = useCallback((id: string) => {
344
+ dispatch({ type: "REMOVE_TABS_TO_RIGHT", payload: { id } });
345
+ }, []);
346
+
347
+ const goToNextTab = useCallback(() => {
348
+ if (state.tabs.length === 0) return;
349
+ const currentIndex = state.tabs.findIndex(
350
+ (tab) => tab.id === state.activeTabId,
351
+ );
352
+ const nextIndex =
353
+ currentIndex === -1 ? 0 : (currentIndex + 1) % state.tabs.length;
354
+ const nextTab = state.tabs[nextIndex];
355
+ if (nextTab) {
356
+ setActiveTab(nextTab.id);
357
+ }
358
+ }, [state.tabs, state.activeTabId, setActiveTab]);
359
+
360
+ const goToPreviousTab = useCallback(() => {
361
+ if (state.tabs.length === 0) return;
362
+ const currentIndex = state.tabs.findIndex(
363
+ (tab) => tab.id === state.activeTabId,
364
+ );
365
+ const prevIndex =
366
+ currentIndex === -1
367
+ ? state.tabs.length - 1
368
+ : (currentIndex - 1 + state.tabs.length) % state.tabs.length;
369
+ const prevTab = state.tabs[prevIndex];
370
+ if (prevTab) {
371
+ setActiveTab(prevTab.id);
372
+ }
373
+ }, [state.tabs, state.activeTabId, setActiveTab]);
374
+
375
+ const goToTabByIndex = useCallback(
376
+ (index: number) => {
377
+ if (index >= 0 && index < state.tabs.length) {
378
+ const tab = state.tabs[index];
379
+ if (tab) {
380
+ setActiveTab(tab.id);
381
+ }
382
+ }
383
+ },
384
+ [state.tabs, setActiveTab],
385
+ );
386
+
387
+ // Close current tab and navigate to parent tab (e.g., from detail page to list page)
388
+ const closeAndGoToParent = useCallback(() => {
389
+ const currentTab = state.tabs.find((t) => t.id === state.activeTabId);
390
+ if (!currentTab) {
391
+ router.back();
392
+ return;
393
+ }
394
+
395
+ // Find parent path (e.g., /vi/supplier-quotes/abc -> /vi/supplier-quotes)
396
+ const pathParts = currentTab.path.split("/");
397
+ // Remove the last segment (the ID or 'new')
398
+ const parentPath = pathParts.slice(0, -1).join("/") || "/";
399
+ const normalizedParent = normalizePathname(parentPath);
400
+
401
+ // Find parent tab in existing tabs
402
+ const parentTab = state.tabs.find((t) => t.id === normalizedParent);
403
+
404
+ // Get remaining tabs after removing current
405
+ const remainingTabs = state.tabs.filter((t) => t.id !== currentTab.id);
406
+
407
+ // Remove current tab
408
+ dispatch({ type: "REMOVE_TAB", payload: { id: currentTab.id } });
409
+
410
+ // Navigate to parent tab if exists, otherwise navigate to parent path
411
+ if (parentTab) {
412
+ router.replace(parentTab.path, { scroll: false });
413
+ } else if (remainingTabs.length > 0) {
414
+ // If no parent tab but there are other tabs, go to the last one
415
+ const lastTab = remainingTabs[remainingTabs.length - 1];
416
+ router.replace(lastTab.path, { scroll: false });
417
+ } else {
418
+ // No tabs left, navigate to parent path (will create a new tab)
419
+ router.replace(parentPath, { scroll: false });
420
+ }
421
+ }, [state.tabs, state.activeTabId, router]);
422
+
423
+ // Keyboard shortcuts
424
+ useEffect(() => {
425
+ const handleKeyDown = (e: KeyboardEvent) => {
426
+ // Don't handle if user is typing in input/textarea
427
+ const target = e.target as HTMLElement;
428
+ if (
429
+ target.tagName === "INPUT" ||
430
+ target.tagName === "TEXTAREA" ||
431
+ target.isContentEditable ||
432
+ target.closest('[role="dialog"]') ||
433
+ target.closest('[data-slot="command-input"]')
434
+ ) {
435
+ return;
436
+ }
437
+
438
+ const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0;
439
+ const ctrlOrCmd = isMac ? e.metaKey : e.ctrlKey;
440
+
441
+ // Ctrl/Cmd + 1-9: Switch to tab by index
442
+ if (ctrlOrCmd && e.key >= "1" && e.key <= "9") {
443
+ const index = parseInt(e.key) - 1;
444
+ if (index < state.tabs.length) {
445
+ e.preventDefault();
446
+ goToTabByIndex(index);
447
+ }
448
+ return;
449
+ }
450
+
451
+ // Ctrl/Cmd + W: Close current tab
452
+ if (ctrlOrCmd && e.key === "w") {
453
+ if (state.activeTabId) {
454
+ e.preventDefault();
455
+ removeTab(state.activeTabId);
456
+ }
457
+ return;
458
+ }
459
+
460
+ // Ctrl/Cmd + Tab: Next tab
461
+ if (ctrlOrCmd && e.key === "Tab" && !e.shiftKey) {
462
+ e.preventDefault();
463
+ goToNextTab();
464
+ return;
465
+ }
466
+
467
+ // Ctrl/Cmd + Shift + Tab: Previous tab
468
+ if (ctrlOrCmd && e.shiftKey && e.key === "Tab") {
469
+ e.preventDefault();
470
+ goToPreviousTab();
471
+ return;
472
+ }
473
+
474
+ // Ctrl/Cmd + Shift + W: Close all tabs except current
475
+ if (ctrlOrCmd && e.shiftKey && e.key === "w") {
476
+ if (state.activeTabId && state.tabs.length > 1) {
477
+ e.preventDefault();
478
+ removeOtherTabs(state.activeTabId);
479
+ }
480
+ return;
481
+ }
482
+ };
483
+
484
+ window.addEventListener("keydown", handleKeyDown);
485
+ return () => window.removeEventListener("keydown", handleKeyDown);
486
+ }, [
487
+ state.tabs,
488
+ state.activeTabId,
489
+ removeTab,
490
+ goToNextTab,
491
+ goToPreviousTab,
492
+ goToTabByIndex,
493
+ removeOtherTabs,
494
+ ]);
495
+
496
+ return (
497
+ <TabNavigationContext.Provider
498
+ value={{
499
+ tabs: state.tabs,
500
+ activeTabId: state.activeTabId,
501
+ addTab,
502
+ removeTab,
503
+ setActiveTab,
504
+ clearTabs,
505
+ removeOtherTabs,
506
+ removeTabsToRight,
507
+ goToNextTab,
508
+ goToPreviousTab,
509
+ goToTabByIndex,
510
+ closeAndGoToParent,
511
+ }}
512
+ >
513
+ {children}
514
+ </TabNavigationContext.Provider>
515
+ );
516
+ }
517
+
518
+ const noopTabNavigation = {
519
+ tabs: [] as Tab[],
520
+ activeTabId: null as string | null,
521
+ addTab: () => {},
522
+ removeTab: () => {},
523
+ setActiveTab: () => {},
524
+ clearTabs: () => {},
525
+ removeOtherTabs: () => {},
526
+ removeTabsToRight: () => {},
527
+ goToNextTab: () => {},
528
+ goToPreviousTab: () => {},
529
+ goToTabByIndex: () => {},
530
+ closeAndGoToParent: () => {},
531
+ };
532
+
533
+ export function useTabNavigation() {
534
+ const context = useContext(TabNavigationContext);
535
+ return context ?? noopTabNavigation;
536
+ }
@@ -0,0 +1,33 @@
1
+ "use client";
2
+
3
+ import { PanelLeft } from "lucide-react";
4
+
5
+ import { Button } from "../index";
6
+ import { useSidebar } from "../primitives/sidebar"; // Import specific hook from primitives
7
+ import { Tooltip, TooltipTrigger, TooltipContent } from "../primitives/client";
8
+
9
+ export function ToggleMobileSidebar() {
10
+ const { isMobile, openMobile, setOpenMobile } = useSidebar();
11
+
12
+ if (isMobile) {
13
+ return (
14
+ <Tooltip>
15
+ <TooltipTrigger asChild>
16
+ <Button
17
+ data-sidebar="trigger"
18
+ variant="ghost"
19
+ size="icon"
20
+ onClick={() => setOpenMobile(!openMobile)}
21
+ aria-label="Toggle Sidebar"
22
+ >
23
+ <PanelLeft className="h-4 w-4" />
24
+ </Button>
25
+ </TooltipTrigger>
26
+ <TooltipContent>
27
+ <p>Toggle Sidebar</p>
28
+ </TooltipContent>
29
+ </Tooltip>
30
+ );
31
+ }
32
+ return null;
33
+ }