@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,906 @@
1
+ // @goerp/core/utils
2
+ // Utility functions for GoERP platform
3
+
4
+ import { clsx } from "clsx";
5
+ import type { ClassValue } from "clsx";
6
+ import { twMerge } from "tailwind-merge";
7
+
8
+ // Import types from core/types
9
+ import type { LocaleType, FormatStyleType } from "../types";
10
+
11
+ // ============================================================================
12
+ // Class Names
13
+ // ============================================================================
14
+
15
+ /**
16
+ * Merge Tailwind CSS classes with clsx
17
+ */
18
+ export function cn(...inputs: ClassValue[]) {
19
+ return twMerge(clsx(inputs));
20
+ }
21
+
22
+ // ============================================================================
23
+ // String Utilities
24
+ // ============================================================================
25
+
26
+ /**
27
+ * Get initials from full name
28
+ */
29
+ export function getInitials(fullName: string): string {
30
+ if (fullName.length === 0) return "";
31
+ const names = fullName.split(" ");
32
+ const initials = names.map((name) => name.charAt(0).toUpperCase()).join("");
33
+ return initials;
34
+ }
35
+
36
+ /**
37
+ * Slugify string
38
+ */
39
+ export function slugify(text: string): string {
40
+ return text
41
+ .toLowerCase()
42
+ .normalize("NFD")
43
+ .replace(/[\u0300-\u036f]/g, "")
44
+ .replace(/[^a-z0-9]+/g, "-")
45
+ .replace(/(^-|-$)/g, "");
46
+ }
47
+
48
+ /**
49
+ * Convert camelCase to Title Case
50
+ */
51
+ export function camelCaseToTitleCase(camelCaseStr: string): string {
52
+ return camelCaseStr
53
+ .replace(/([A-Z])/g, " $1")
54
+ .replace(/^./, (char) => char.toUpperCase());
55
+ }
56
+
57
+ /**
58
+ * Convert Title Case to camelCase
59
+ */
60
+ export function titleCaseToCamelCase(titleCaseStr: string): string {
61
+ return titleCaseStr
62
+ .toLowerCase()
63
+ .replace(/[-_\s]+(.)/g, (_, char) => char.toUpperCase())
64
+ .replace(/[-_]/g, "");
65
+ }
66
+
67
+ // ============================================================================
68
+ // Number Utilities
69
+ // ============================================================================
70
+
71
+ export const isEven = (num: number) => num % 2 === 0;
72
+ export const isNonNegative = (num: number) => num >= 0;
73
+
74
+ /**
75
+ * Đọc số tiền thành chữ tiếng Việt
76
+ * Dùng chung cho tất cả các phiếu in (hợp đồng, phiếu thu, phiếu chi, phiếu nhập/xuất kho, v.v.)
77
+ * @param amount - Số tiền (VND)
78
+ * @returns Chuỗi đọc bằng chữ, VD: "Bảy mươi hai triệu một trăm năm mươi chín nghìn tám trăm hai mươi đồng"
79
+ */
80
+ export function readMoney(amount: number): string {
81
+ if (!Number.isFinite(amount)) return "Không đồng"
82
+
83
+ const isNegative = amount < 0
84
+
85
+ // Bỏ phần thập phân - VND không có đơn vị lẻ
86
+ amount = Math.floor(Math.abs(amount))
87
+
88
+ if (amount === 0) return "Không đồng"
89
+
90
+ const unit = ["", "nghìn", "triệu", "tỷ", "nghìn tỷ", "triệu tỷ"]
91
+ const digit = [
92
+ "không",
93
+ "một",
94
+ "hai",
95
+ "ba",
96
+ "bốn",
97
+ "năm",
98
+ "sáu",
99
+ "bảy",
100
+ "tám",
101
+ "chín",
102
+ ]
103
+
104
+ let str = amount.toString()
105
+ const groups: string[] = []
106
+ while (str.length > 0) {
107
+ groups.push(str.slice(-3))
108
+ str = str.slice(0, -3)
109
+ }
110
+
111
+ let result = ""
112
+ for (let i = 0; i < groups.length; i++) {
113
+ const group = groups[i]
114
+ if (group === "000") continue
115
+
116
+ const [a, b, c] = group.padStart(3, "0").split("").map(Number)
117
+ let groupResult = ""
118
+
119
+ const hasHundreds = a !== 0 || groups.length > 1
120
+
121
+ if (hasHundreds) {
122
+ groupResult += `${digit[a]} trăm `
123
+ }
124
+
125
+ if (b === 0 && c !== 0) {
126
+ if (hasHundreds) groupResult += "lẻ "
127
+ } else if (b === 1) {
128
+ groupResult += "mười "
129
+ } else if (b > 1) {
130
+ groupResult += `${digit[b]} mươi `
131
+ }
132
+
133
+ if (c === 1 && b > 1) {
134
+ groupResult += "mốt "
135
+ } else if (c === 5 && b > 0) {
136
+ groupResult += "lăm "
137
+ } else if (c !== 0) {
138
+ groupResult += `${digit[c]} `
139
+ }
140
+
141
+ if (i === groups.length - 1 && a === 0) {
142
+ groupResult = groupResult.replace("không trăm ", "")
143
+ if (b === 0) groupResult = groupResult.replace("lẻ ", "")
144
+ }
145
+
146
+ if (groupResult.trim() !== "") {
147
+ result = `${groupResult.trim()} ${unit[i]} ${result}`
148
+ }
149
+ }
150
+
151
+ result = result.trim() + " đồng"
152
+
153
+ if (isNegative) {
154
+ return "Âm " + result
155
+ }
156
+
157
+ return result.charAt(0).toUpperCase() + result.slice(1)
158
+ }
159
+
160
+ /**
161
+ * Format currency with locale
162
+ */
163
+ export function formatCurrency(
164
+ value: number,
165
+ locales: LocaleType = "vi",
166
+ currency: string = "VND",
167
+ ): string {
168
+ return new Intl.NumberFormat(locales === "vi" ? "vi-VN" : locales, {
169
+ style: "decimal",
170
+ maximumFractionDigits: 0,
171
+ }).format(value);
172
+ }
173
+
174
+ /**
175
+ * Format number with locale
176
+ */
177
+ export function formatNumber(
178
+ value: number,
179
+ options?: {
180
+ locale?: string;
181
+ minimumFractionDigits?: number;
182
+ maximumFractionDigits?: number;
183
+ },
184
+ ): string {
185
+ const {
186
+ locale = "vi-VN",
187
+ minimumFractionDigits = 0,
188
+ maximumFractionDigits = 2,
189
+ } = options || {};
190
+
191
+ return new Intl.NumberFormat(locale, {
192
+ minimumFractionDigits,
193
+ maximumFractionDigits,
194
+ }).format(value);
195
+ }
196
+
197
+ /**
198
+ * Format percent
199
+ */
200
+ export function formatPercent(
201
+ value: number,
202
+ locales: LocaleType = "vi",
203
+ ): string {
204
+ return new Intl.NumberFormat(locales === "vi" ? "vi-VN" : locales, {
205
+ style: "percent",
206
+ maximumFractionDigits: 0,
207
+ }).format(value);
208
+ }
209
+
210
+ /**
211
+ * Format number to compact (e.g., 1K, 1M)
212
+ */
213
+ export function formatNumberToCompact(
214
+ value: number,
215
+ locales: LocaleType = "vi",
216
+ ): string {
217
+ return new Intl.NumberFormat(locales === "vi" ? "vi-VN" : locales, {
218
+ notation: "compact",
219
+ compactDisplay: "short",
220
+ }).format(value);
221
+ }
222
+
223
+ /**
224
+ * Format file size
225
+ */
226
+ export function formatFileSize(bytes: number, decimals: number = 2): string {
227
+ if (bytes === 0) return "0 Bytes";
228
+
229
+ const k = 1000;
230
+ const dm = decimals < 0 ? 0 : decimals;
231
+ const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB"];
232
+
233
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
234
+
235
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i];
236
+ }
237
+
238
+ /**
239
+ * Format unread count
240
+ */
241
+ export function formatUnreadCount(unreadCount: number): string | number {
242
+ return unreadCount >= 100 ? "+99" : unreadCount;
243
+ }
244
+
245
+ // ============================================================================
246
+ // Date Utilities
247
+ // ============================================================================
248
+
249
+ /**
250
+ * Format date with Vietnamese locale (Asia/Ho_Chi_Minh timezone)
251
+ */
252
+ export function formatDate(
253
+ date: Date | string,
254
+ options?: {
255
+ locale?: string;
256
+ format?: "short" | "medium" | "long" | "full";
257
+ },
258
+ ): string {
259
+ const { locale = "vi-VN", format = "medium" } = options || {};
260
+ const dateObj = typeof date === "string" ? new Date(date) : date;
261
+
262
+ const formatOptions: Record<string, Intl.DateTimeFormatOptions> = {
263
+ short: { day: "2-digit", month: "2-digit", year: "numeric", timeZone: "Asia/Ho_Chi_Minh" },
264
+ medium: { day: "2-digit", month: "short", year: "numeric", timeZone: "Asia/Ho_Chi_Minh" },
265
+ long: { day: "numeric", month: "long", year: "numeric", timeZone: "Asia/Ho_Chi_Minh" },
266
+ full: { weekday: "long", day: "numeric", month: "long", year: "numeric", timeZone: "Asia/Ho_Chi_Minh" },
267
+ };
268
+
269
+ return new Intl.DateTimeFormat(locale, formatOptions[format]).format(dateObj);
270
+ }
271
+
272
+ /**
273
+ * Format datetime with Vietnamese locale (Asia/Ho_Chi_Minh timezone)
274
+ */
275
+ export function formatDateTime(
276
+ date: Date | string,
277
+ options?: {
278
+ locale?: string;
279
+ },
280
+ ): string {
281
+ const { locale = "vi-VN" } = options || {};
282
+ const dateObj = typeof date === "string" ? new Date(date) : date;
283
+
284
+ return new Intl.DateTimeFormat(locale, {
285
+ day: "2-digit",
286
+ month: "2-digit",
287
+ year: "numeric",
288
+ hour: "2-digit",
289
+ minute: "2-digit",
290
+ timeZone: "Asia/Ho_Chi_Minh",
291
+ }).format(dateObj);
292
+ }
293
+
294
+ /**
295
+ * Format relative date (Today, Yesterday, or date) - Asia/Ho_Chi_Minh timezone
296
+ */
297
+ export function formatRelativeDate(value?: string | number | Date): string {
298
+ if (!value) return "No Date";
299
+
300
+ const date = new Date(value);
301
+ const today = new Date();
302
+ const yesterday = new Date();
303
+ yesterday.setDate(today.getDate() - 1);
304
+
305
+ // Compare dates in Vietnam timezone
306
+ const vnDateStr = new Intl.DateTimeFormat("en-CA", { timeZone: "Asia/Ho_Chi_Minh", year: "numeric", month: "2-digit", day: "2-digit" }).format(date);
307
+ const vnTodayStr = new Intl.DateTimeFormat("en-CA", { timeZone: "Asia/Ho_Chi_Minh", year: "numeric", month: "2-digit", day: "2-digit" }).format(today);
308
+ const vnYesterdayStr = new Intl.DateTimeFormat("en-CA", { timeZone: "Asia/Ho_Chi_Minh", year: "numeric", month: "2-digit", day: "2-digit" }).format(yesterday);
309
+
310
+ if (vnDateStr === vnTodayStr) return "Today";
311
+ if (vnDateStr === vnYesterdayStr) return "Yesterday";
312
+
313
+ return formatDate(date);
314
+ }
315
+
316
+ /**
317
+ * Check if date is before today (Asia/Ho_Chi_Minh timezone)
318
+ */
319
+ export function isBeforeToday(date: Date): boolean {
320
+ const vnTodayStr = new Intl.DateTimeFormat("en-CA", { timeZone: "Asia/Ho_Chi_Minh", year: "numeric", month: "2-digit", day: "2-digit" }).format(new Date());
321
+ const vnDateStr = new Intl.DateTimeFormat("en-CA", { timeZone: "Asia/Ho_Chi_Minh", year: "numeric", month: "2-digit", day: "2-digit" }).format(date);
322
+ return vnDateStr < vnTodayStr;
323
+ }
324
+
325
+ // ============================================================================
326
+ // Path Utilities
327
+ // ============================================================================
328
+
329
+ /**
330
+ * Ensure path has prefix
331
+ */
332
+ export function ensureWithPrefix(value: string, prefix: string): string {
333
+ return value.startsWith(prefix) ? value : `${prefix}${value}`;
334
+ }
335
+
336
+ /**
337
+ * Ensure path has suffix
338
+ */
339
+ export function ensureWithSuffix(value: string, suffix: string): string {
340
+ return value.endsWith(suffix) ? value : `${value}${suffix}`;
341
+ }
342
+
343
+ /**
344
+ * Ensure path without prefix
345
+ */
346
+ export function ensureWithoutPrefix(value: string, prefix: string): string {
347
+ return value.startsWith(prefix) ? value.slice(prefix.length) : value;
348
+ }
349
+
350
+ /**
351
+ * Ensure path without suffix
352
+ */
353
+ export function ensureWithoutSuffix(value: string, suffix: string): string {
354
+ return value.endsWith(suffix) ? value.slice(0, -suffix.length) : value;
355
+ }
356
+
357
+ /**
358
+ * Check if pathname is active
359
+ */
360
+ export function isActivePathname(
361
+ basePathname: string,
362
+ currentPathname: string,
363
+ exactMatch: boolean = false,
364
+ ): boolean {
365
+ if (typeof basePathname !== "string" || typeof currentPathname !== "string") {
366
+ throw new Error("Both basePathname and currentPathname must be strings");
367
+ }
368
+
369
+ if (exactMatch) {
370
+ return basePathname === currentPathname;
371
+ }
372
+
373
+ return (
374
+ currentPathname.startsWith(basePathname) &&
375
+ (currentPathname.length === basePathname.length ||
376
+ currentPathname[basePathname.length] === "/")
377
+ );
378
+ }
379
+
380
+ // ============================================================================
381
+ // General Utilities
382
+ // ============================================================================
383
+
384
+ /**
385
+ * Wait/sleep function
386
+ */
387
+ export function wait(ms: number = 250): Promise<void> {
388
+ return new Promise((resolve) => setTimeout(resolve, ms));
389
+ }
390
+
391
+ /**
392
+ * Debounce function
393
+ */
394
+ export function debounce<T extends (...args: unknown[]) => unknown>(
395
+ func: T,
396
+ wait: number,
397
+ ): (...args: Parameters<T>) => void {
398
+ let timeout: ReturnType<typeof setTimeout> | null = null;
399
+
400
+ return (...args: Parameters<T>) => {
401
+ if (timeout) {
402
+ clearTimeout(timeout);
403
+ }
404
+ timeout = setTimeout(() => func(...args), wait);
405
+ };
406
+ }
407
+
408
+ /**
409
+ * Deep clone object
410
+ */
411
+ export function deepClone<T>(obj: T): T {
412
+ return JSON.parse(JSON.stringify(obj));
413
+ }
414
+
415
+ /**
416
+ * Check if value is empty
417
+ */
418
+ export function isEmpty(value: unknown): boolean {
419
+ if (value === null || value === undefined) return true;
420
+ if (typeof value === "string") return value.trim() === "";
421
+ if (Array.isArray(value)) return value.length === 0;
422
+ if (typeof value === "object") return Object.keys(value).length === 0;
423
+ return false;
424
+ }
425
+
426
+ /**
427
+ * Generate a random ID
428
+ */
429
+ export function generateId(prefix?: string): string {
430
+ const id = Math.random().toString(36).substring(2, 11);
431
+ return prefix ? `${prefix}_${id}` : id;
432
+ }
433
+
434
+ /**
435
+ * Get dictionary value safely
436
+ */
437
+ export function getDictionaryValue(
438
+ key: string,
439
+ section: Record<string, unknown>,
440
+ fallback?: string,
441
+ ): string {
442
+ const value = section[key];
443
+
444
+ if (typeof value !== "string") {
445
+ if (fallback !== undefined) {
446
+ return fallback;
447
+ }
448
+
449
+ const normalizedKey = key.replace(/[-_]/g, "");
450
+ const normalizedValue = section[normalizedKey];
451
+
452
+ if (typeof normalizedValue === "string") {
453
+ return normalizedValue;
454
+ }
455
+
456
+ return key;
457
+ }
458
+
459
+ return value;
460
+ }
461
+
462
+ /**
463
+ * Format overview card value based on style
464
+ */
465
+ export function formatOverviewCardValue(
466
+ value: number,
467
+ formatStyle: FormatStyleType,
468
+ ): string | number {
469
+ switch (formatStyle) {
470
+ case "percent":
471
+ return formatPercent(value);
472
+ case "currency":
473
+ return formatCurrency(value);
474
+ default:
475
+ return value.toLocaleString("vi-VN", {
476
+ maximumFractionDigits: 0,
477
+ });
478
+ }
479
+ }
480
+
481
+ // ============================================================================
482
+ // Additional Utilities (migrated from shared-utils)
483
+ // ============================================================================
484
+
485
+ /**
486
+ * Get credit card brand name from number
487
+ */
488
+ export function getCreditCardBrandName(number: string): string {
489
+ const re = {
490
+ visa: /^4/,
491
+ mastercard: /^5[1-5]/,
492
+ amex: /^3[47]/,
493
+ discover: /^6(?:011|5)/,
494
+ };
495
+
496
+ for (const [type, regex] of Object.entries(re)) {
497
+ if (regex.test(number)) return type;
498
+ }
499
+ return "unknown";
500
+ }
501
+
502
+ /**
503
+ * Convert rem to pixels
504
+ */
505
+ export function remToPx(rem: number): number {
506
+ if (typeof document === "undefined") return rem * 16;
507
+ const rootFontSize = parseFloat(
508
+ getComputedStyle(document.documentElement).fontSize,
509
+ );
510
+ return rem * rootFontSize;
511
+ }
512
+
513
+ /**
514
+ * Check if string is a valid URL
515
+ */
516
+ export function isUrl(text: string): boolean {
517
+ try {
518
+ new URL(text);
519
+ return true;
520
+ } catch {
521
+ return false;
522
+ }
523
+ }
524
+
525
+ /**
526
+ * Rating to percentage string
527
+ */
528
+ export function ratingToPercentage(
529
+ rating: number,
530
+ maxRating: number,
531
+ fractionDigits: number = 0,
532
+ ): string {
533
+ const value = ((rating / maxRating) * 100).toFixed(fractionDigits);
534
+ return value + "%";
535
+ }
536
+
537
+ /**
538
+ * Ensure redirect pathname with query params
539
+ */
540
+ export function ensureRedirectPathname(
541
+ basePathname: string,
542
+ redirectPathname: string,
543
+ ): string {
544
+ const searchParams = new URLSearchParams({
545
+ redirectTo: ensureWithoutSuffix(redirectPathname, "/"),
546
+ });
547
+
548
+ return ensureWithSuffix(basePathname, "?" + searchParams.toString());
549
+ }
550
+
551
+ /**
552
+ * Get discounted price
553
+ */
554
+ export function getDiscountedPrice(
555
+ price: number,
556
+ discountRate: number,
557
+ isAnnual: boolean = false,
558
+ ): number {
559
+ if (isAnnual) {
560
+ const annualPrice = price * 12;
561
+ const discountedAnnualPrice = annualPrice * (1 - discountRate);
562
+ return discountedAnnualPrice / 12;
563
+ } else {
564
+ return price * (1 - discountRate);
565
+ }
566
+ }
567
+
568
+ /**
569
+ * Convert time string to Date
570
+ */
571
+ export function timeToDate(timeString: string, baseDate = new Date()): Date {
572
+ if (!/^\d{2}:\d{2}$/.test(timeString)) {
573
+ throw new Error("Invalid time format. Use 'HH:mm'.");
574
+ }
575
+
576
+ const [hours, minutes] = timeString.split(":").map(Number);
577
+ const date = new Date(baseDate);
578
+
579
+ date.setHours(hours, minutes, 0, 0);
580
+
581
+ return date;
582
+ }
583
+
584
+ /**
585
+ * Format file type
586
+ */
587
+ export function formatFileType(type: string): string {
588
+ return type.slice(0, type.lastIndexOf("/"));
589
+ }
590
+
591
+ /**
592
+ * Format date with time (Asia/Ho_Chi_Minh timezone)
593
+ */
594
+ export function formatDateWithTime(value: string | number | Date): string {
595
+ const date = new Date(value);
596
+ return new Intl.DateTimeFormat("vi-VN", {
597
+ day: "2-digit",
598
+ month: "2-digit",
599
+ year: "numeric",
600
+ hour: "2-digit",
601
+ minute: "2-digit",
602
+ timeZone: "Asia/Ho_Chi_Minh",
603
+ }).format(date);
604
+ }
605
+
606
+ /**
607
+ * Format date short (MMM dd) - Asia/Ho_Chi_Minh timezone
608
+ */
609
+ export function formatDateShort(value: string | number | Date): string {
610
+ const date = new Date(value);
611
+ return new Intl.DateTimeFormat("en-US", {
612
+ month: "short",
613
+ day: "2-digit",
614
+ timeZone: "Asia/Ho_Chi_Minh",
615
+ }).format(date);
616
+ }
617
+
618
+ /**
619
+ * Format time (Asia/Ho_Chi_Minh timezone)
620
+ */
621
+ export function formatTime(value: string | number | Date): string {
622
+ const date = new Date(value);
623
+ return new Intl.DateTimeFormat("en-US", {
624
+ hour: "numeric",
625
+ minute: "2-digit",
626
+ hour12: true,
627
+ timeZone: "Asia/Ho_Chi_Minh",
628
+ }).format(date);
629
+ }
630
+
631
+ /**
632
+ * Format duration from milliseconds
633
+ */
634
+ export function formatDuration(value: string | number | Date): string {
635
+ const numberValue = Number(value);
636
+ const isNegative = numberValue < 0;
637
+ const absoluteValue = Math.abs(numberValue);
638
+
639
+ const hours = Math.floor(absoluteValue / 3600000);
640
+ const minutes = Math.floor((absoluteValue % 3600000) / 60000);
641
+ const seconds = Math.floor((absoluteValue % 60000) / 1000);
642
+
643
+ const parts = [];
644
+ if (hours) parts.push(`${hours}h`);
645
+ if (minutes) parts.push(`${minutes}m`);
646
+ if (seconds) parts.push(`${seconds}s`);
647
+
648
+ const formattedDuration = parts.join(" ") || "0s";
649
+
650
+ return isNegative ? `-${formattedDuration}` : formattedDuration;
651
+ }
652
+
653
+ /**
654
+ * Format distance to now
655
+ */
656
+ export function formatDistance(value: string | number | Date): string {
657
+ const date = new Date(value);
658
+ const now = new Date();
659
+ const diffMs = now.getTime() - date.getTime();
660
+ const diffMins = Math.floor(diffMs / 60000);
661
+ const diffHours = Math.floor(diffMs / 3600000);
662
+ const diffDays = Math.floor(diffMs / 86400000);
663
+
664
+ if (diffMins < 1) return "just now";
665
+ if (diffMins < 60) return `${diffMins} mins ago`;
666
+ if (diffHours < 24) return `${diffHours} hrs ago`;
667
+ if (diffDays < 30) return `${diffDays} days ago`;
668
+ if (diffDays < 365) return `${Math.floor(diffDays / 30)} months ago`;
669
+ return `${Math.floor(diffDays / 365)} years ago`;
670
+ }
671
+
672
+ // Re-export status constants from configs
673
+ export {
674
+ STATUS_COLORS,
675
+ STATUS_ACTIVE,
676
+ STATUS_INACTIVE,
677
+ STATUS_OPTIONS,
678
+ STATUS_VALUES,
679
+ booleanToStatus,
680
+ statusToBoolean,
681
+ } from "../configs/status";
682
+
683
+ // ============================================================================
684
+ // Localization Utilities
685
+ // ============================================================================
686
+
687
+ const LOCALE_LIST = ["vi", "en"];
688
+
689
+ export function isPathnameMissingLocale(pathname: string) {
690
+ return !LOCALE_LIST.some((locale) => pathname.startsWith(`/${locale}`));
691
+ }
692
+
693
+ export function getLocaleFromPathname(pathname: string) {
694
+ return LOCALE_LIST.find((locale) => pathname.startsWith(`/${locale}`));
695
+ }
696
+
697
+ export function ensureLocalizedPathname(pathname: string, locale: string) {
698
+ if (!pathname || !locale)
699
+ throw new Error("Pathname or Locale cannot be empty");
700
+ return isPathnameMissingLocale(pathname)
701
+ ? `${ensureWithPrefix(locale, "/")}${ensureWithPrefix(pathname, "/")}`
702
+ : pathname;
703
+ }
704
+
705
+ export function relocalizePathname(pathname: string, locale: string) {
706
+ if (!pathname || !locale)
707
+ throw new Error("Pathname or Locale cannot be empty");
708
+ const segments = pathname.split("/");
709
+ segments[1] = locale;
710
+ return segments.join("/");
711
+ }
712
+
713
+ // ============================================================================
714
+ // Logger
715
+ // ============================================================================
716
+
717
+ type LogLevel = "info" | "warn" | "error" | "debug";
718
+
719
+ interface LogEntry {
720
+ timestamp: string;
721
+ level: LogLevel;
722
+ message: string;
723
+ context?: Record<string, unknown>;
724
+ error?: Error | unknown;
725
+ }
726
+
727
+ class Logger {
728
+ private log(
729
+ level: LogLevel,
730
+ message: string,
731
+ context?: Record<string, unknown>,
732
+ error?: unknown,
733
+ ) {
734
+ const entry: LogEntry = {
735
+ timestamp: new Date().toISOString(),
736
+ level,
737
+ message,
738
+ context,
739
+ };
740
+
741
+ if (error instanceof Error) {
742
+ entry.error = {
743
+ name: error.name,
744
+ message: error.message,
745
+ stack: error.stack,
746
+ };
747
+ } else if (error) {
748
+ entry.error = error;
749
+ }
750
+
751
+ const logString = JSON.stringify(entry);
752
+ switch (level) {
753
+ case "error":
754
+ console.error(logString);
755
+ break;
756
+ case "warn":
757
+ console.warn(logString);
758
+ break;
759
+ case "debug":
760
+ if (process.env.NODE_ENV === "development") console.debug(logString);
761
+ break;
762
+ default:
763
+ console.log(logString);
764
+ }
765
+ }
766
+
767
+ info(message: string, context?: Record<string, unknown>) {
768
+ this.log("info", message, context);
769
+ }
770
+ warn(message: string, context?: Record<string, unknown>) {
771
+ this.log("warn", message, context);
772
+ }
773
+ error(message: string, error?: unknown, context?: Record<string, unknown>) {
774
+ this.log("error", message, context, error);
775
+ }
776
+ debug(message: string, context?: Record<string, unknown>) {
777
+ this.log("debug", message, context);
778
+ }
779
+ }
780
+
781
+ export const logger = new Logger();
782
+
783
+ // ============================================================================
784
+ // Tab Navigation Utilities
785
+ // ============================================================================
786
+
787
+ import type {
788
+ NavigationType,
789
+ NavigationNestedItemWithHrefType,
790
+ NavigationNestedItemWithItemsType,
791
+ DynamicIconNameType,
792
+ } from "../types";
793
+
794
+ export function shouldExcludeFromTabs(pathname: string): boolean {
795
+ const excludePatterns = [
796
+ "/sign-in",
797
+ "/sign-out",
798
+ "/forgot-password",
799
+ "/new-password",
800
+ "/verify-email",
801
+ "/register",
802
+ "/unauthorized",
803
+ "/not-found",
804
+ "/maintenance",
805
+ "/coming-soon",
806
+ ];
807
+ if (
808
+ pathname.includes("/ui/") ||
809
+ pathname.includes("/colors") ||
810
+ pathname.includes("/typography")
811
+ )
812
+ return true;
813
+ return excludePatterns.some((pattern) => pathname.includes(pattern));
814
+ }
815
+
816
+ export function normalizePathname(pathname: string): string {
817
+ return pathname.replace(/^\/[a-z]{2}(\/|$)/, "/") || "/";
818
+ }
819
+
820
+ function formatSegmentLabel(segment: string): string {
821
+ return segment
822
+ .replace(/[-_]+/g, " ")
823
+ .replace(/\b\w/g, (char) => char.toUpperCase());
824
+ }
825
+
826
+ export function findRouteTitle(
827
+ pathname: string,
828
+ navigations: NavigationType[],
829
+ ): string | null {
830
+ const result = findRouteInfo(pathname, navigations);
831
+ return result?.title || null;
832
+ }
833
+
834
+ export function findRouteIcon(
835
+ pathname: string,
836
+ navigations: NavigationType[],
837
+ ): DynamicIconNameType | null {
838
+ const result = findRouteInfo(pathname, navigations);
839
+ return (result?.iconName as DynamicIconNameType) || null;
840
+ }
841
+
842
+ interface RouteInfo {
843
+ title: string;
844
+ iconName?: DynamicIconNameType;
845
+ }
846
+
847
+ function findRouteInfo(
848
+ pathname: string,
849
+ navigations: NavigationType[],
850
+ ): RouteInfo | null {
851
+ const normalizedPath = normalizePathname(pathname);
852
+ let exactMatch: RouteInfo | null = null;
853
+ const prefixMatches: Array<{ itemPath: string; routeInfo: RouteInfo }> = [];
854
+
855
+ function searchItems(
856
+ items:
857
+ | NavigationType["items"]
858
+ | Array<
859
+ NavigationNestedItemWithHrefType | NavigationNestedItemWithItemsType
860
+ >
861
+ | undefined,
862
+ ): void {
863
+ if (!items) return;
864
+ for (const item of items) {
865
+ if ("href" in item && item.href) {
866
+ const itemPath = normalizePathname(item.href);
867
+ if (normalizedPath === itemPath) {
868
+ exactMatch = {
869
+ title: item.title,
870
+ iconName:
871
+ "iconName" in item
872
+ ? (item.iconName as DynamicIconNameType | undefined)
873
+ : undefined,
874
+ };
875
+ } else if (normalizedPath.startsWith(itemPath + "/")) {
876
+ prefixMatches.push({
877
+ itemPath,
878
+ routeInfo: {
879
+ title: item.title,
880
+ iconName:
881
+ "iconName" in item
882
+ ? (item.iconName as DynamicIconNameType | undefined)
883
+ : undefined,
884
+ },
885
+ });
886
+ }
887
+ }
888
+ if ("items" in item && item.items) searchItems(item.items);
889
+ }
890
+ }
891
+
892
+ for (const nav of navigations) {
893
+ searchItems(nav.items);
894
+ if (exactMatch) return exactMatch;
895
+ }
896
+
897
+ if (prefixMatches.length > 0) {
898
+ prefixMatches.sort((a, b) => b.itemPath.length - a.itemPath.length);
899
+ return prefixMatches[0].routeInfo;
900
+ }
901
+
902
+ const segments = normalizedPath.split("/").filter(Boolean);
903
+ return segments.length > 0
904
+ ? { title: formatSegmentLabel(segments[segments.length - 1]) }
905
+ : { title: "Home" };
906
+ }