@orbe-agro/client-core 5.6.188 → 5.6.192

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 (118) hide show
  1. package/dist/@ecme/assets/styles/app.css +17 -0
  2. package/dist/@ecme/assets/styles/tailwind/index.css +17 -0
  3. package/dist/@ecme/components/layouts/PostLoginLayout/components/CollapsibleSide.stories.js.map +1 -1
  4. package/dist/@ecme/components/layouts/PostLoginLayout/components/ContentOverlay.stories.js.map +1 -1
  5. package/dist/@ecme/components/layouts/PostLoginLayout/components/FrameLessSide.stories.js.map +1 -1
  6. package/dist/@ecme/components/layouts/PostLoginLayout/components/StackedSide.stories.js.map +1 -1
  7. package/dist/@ecme/components/layouts/PostLoginLayout/components/TopBarClassic.stories.js.map +1 -1
  8. package/dist/@ecme/components/template/Footer.js +1 -1
  9. package/dist/@ecme/components/template/Header.stories.js.map +1 -1
  10. package/dist/@ecme/components/template/Search.js +276 -72
  11. package/dist/@ecme/components/template/Search.js.map +1 -1
  12. package/dist/@ecme/components/template/Search.stories.js +18 -2
  13. package/dist/@ecme/components/template/Search.stories.js.map +1 -1
  14. package/dist/@ecme/index.js +0 -3
  15. package/dist/@ecme/index.js.map +1 -1
  16. package/dist/@ecme/locales/index.js +1 -4
  17. package/dist/@ecme/locales/index.js.map +1 -1
  18. package/dist/@ecme/mock/data/favoritosMenuData.js +5 -0
  19. package/dist/@ecme/mock/data/favoritosMenuData.js.map +1 -0
  20. package/dist/@ecme/mock/fakeApi/favoritosMenuFakeApi.js +64 -0
  21. package/dist/@ecme/mock/fakeApi/favoritosMenuFakeApi.js.map +1 -0
  22. package/dist/@ecme/mock/index.js +1 -0
  23. package/dist/@ecme/mock/index.js.map +1 -1
  24. package/dist/@types/@ecme/components/layouts/PostLoginLayout/components/CollapsibleSide.stories.d.ts +7 -0
  25. package/dist/@types/@ecme/components/layouts/PostLoginLayout/components/CollapsibleSide.stories.d.ts.map +1 -0
  26. package/dist/@types/@ecme/components/layouts/PostLoginLayout/components/ContentOverlay.stories.d.ts +7 -0
  27. package/dist/@types/@ecme/components/layouts/PostLoginLayout/components/ContentOverlay.stories.d.ts.map +1 -0
  28. package/dist/@types/@ecme/components/layouts/PostLoginLayout/components/FrameLessSide.stories.d.ts +7 -0
  29. package/dist/@types/@ecme/components/layouts/PostLoginLayout/components/FrameLessSide.stories.d.ts.map +1 -0
  30. package/dist/@types/@ecme/components/layouts/PostLoginLayout/components/StackedSide.stories.d.ts +7 -0
  31. package/dist/@types/@ecme/components/layouts/PostLoginLayout/components/StackedSide.stories.d.ts.map +1 -0
  32. package/dist/@types/@ecme/components/layouts/PostLoginLayout/components/TopBarClassic.stories.d.ts +7 -0
  33. package/dist/@types/@ecme/components/layouts/PostLoginLayout/components/TopBarClassic.stories.d.ts.map +1 -0
  34. package/dist/@types/@ecme/components/template/Header.stories.d.ts +7 -0
  35. package/dist/@types/@ecme/components/template/Header.stories.d.ts.map +1 -0
  36. package/dist/@types/@ecme/components/template/Search.d.ts.map +1 -1
  37. package/dist/@types/@ecme/components/template/Search.stories.d.ts +9 -0
  38. package/dist/@types/@ecme/components/template/Search.stories.d.ts.map +1 -0
  39. package/dist/@types/@ecme/locales/index.d.ts +0 -1
  40. package/dist/@types/@ecme/locales/index.d.ts.map +1 -1
  41. package/dist/@types/@ecme/mock/data/favoritosMenuData.d.ts +3 -0
  42. package/dist/@types/@ecme/mock/data/favoritosMenuData.d.ts.map +1 -0
  43. package/dist/@types/@ecme/mock/fakeApi/favoritosMenuFakeApi.d.ts +2 -0
  44. package/dist/@types/@ecme/mock/fakeApi/favoritosMenuFakeApi.d.ts.map +1 -0
  45. package/dist/@types/@ecme/mock/index.d.ts +1 -0
  46. package/dist/@types/@ecme/mock/index.d.ts.map +1 -1
  47. package/dist/@types/base/@types/api/request/dm/TCreateFavoritoMenuRequest.d.ts +4 -0
  48. package/dist/@types/base/@types/api/request/dm/TCreateFavoritoMenuRequest.d.ts.map +1 -0
  49. package/dist/@types/base/@types/api/request/dm/TReorderFavoritosMenuRequest.d.ts +4 -0
  50. package/dist/@types/base/@types/api/request/dm/TReorderFavoritosMenuRequest.d.ts.map +1 -0
  51. package/dist/@types/base/@types/api/response/dadosMestres/TFavoritoMenuResponse.d.ts +8 -0
  52. package/dist/@types/base/@types/api/response/dadosMestres/TFavoritoMenuResponse.d.ts.map +1 -0
  53. package/dist/@types/base/@types/models/performance/ppr.d.ts +48 -0
  54. package/dist/@types/base/@types/models/performance/ppr.d.ts.map +1 -1
  55. package/dist/@types/base/configs/endpoints.config/DadosMestres/FavoritoMenu/favoritoMenu.endpoint.config.d.ts +8 -0
  56. package/dist/@types/base/configs/endpoints.config/DadosMestres/FavoritoMenu/favoritoMenu.endpoint.config.d.ts.map +1 -0
  57. package/dist/@types/base/configs/endpoints.config/endpoints.navigation.d.ts +7 -0
  58. package/dist/@types/base/configs/endpoints.config/endpoints.navigation.d.ts.map +1 -1
  59. package/dist/@types/base/configs/endpoints.config/performance/config/ppr.d.ts +1 -0
  60. package/dist/@types/base/configs/endpoints.config/performance/config/ppr.d.ts.map +1 -1
  61. package/dist/@types/base/configs/endpoints.config/performance/performance.d.ts +1 -0
  62. package/dist/@types/base/configs/endpoints.config/performance/performance.d.ts.map +1 -1
  63. package/dist/@types/base/services/modules/DadosMestres/FavoritoMenu/FavoritoMenuService.d.ts +8 -0
  64. package/dist/@types/base/services/modules/DadosMestres/FavoritoMenu/FavoritoMenuService.d.ts.map +1 -0
  65. package/dist/@types/base/services/modules/performance/PerformanceService.d.ts +3 -1
  66. package/dist/@types/base/services/modules/performance/PerformanceService.d.ts.map +1 -1
  67. package/dist/@types/base/utils/functions/stringUtils.d.ts.map +1 -1
  68. package/dist/base/@types/api/request/dm/TCreateFavoritoMenuRequest.js +2 -0
  69. package/dist/base/@types/api/request/dm/TCreateFavoritoMenuRequest.js.map +1 -0
  70. package/dist/base/@types/api/request/dm/TReorderFavoritosMenuRequest.js +2 -0
  71. package/dist/base/@types/api/request/dm/TReorderFavoritosMenuRequest.js.map +1 -0
  72. package/dist/base/@types/api/response/dadosMestres/TFavoritoMenuResponse.js +2 -0
  73. package/dist/base/@types/api/response/dadosMestres/TFavoritoMenuResponse.js.map +1 -0
  74. package/dist/base/assets/styles/init.css +18 -0
  75. package/dist/base/configs/api.config.js.map +1 -1
  76. package/dist/base/configs/endpoints.config/DadosMestres/FavoritoMenu/favoritoMenu.endpoint.config.js +11 -0
  77. package/dist/base/configs/endpoints.config/DadosMestres/FavoritoMenu/favoritoMenu.endpoint.config.js.map +1 -0
  78. package/dist/base/configs/endpoints.config/endpoints.navigation.js +2 -0
  79. package/dist/base/configs/endpoints.config/endpoints.navigation.js.map +1 -1
  80. package/dist/base/configs/endpoints.config/performance/config/ppr.js +2 -1
  81. package/dist/base/configs/endpoints.config/performance/config/ppr.js.map +1 -1
  82. package/dist/base/configs/navigation.config/submenu/performance/config/exportarResultadosPpr.submenu.js +1 -1
  83. package/dist/base/configs/navigation.config/submenu/performance/config/exportarResultadosPpr.submenu.js.map +1 -1
  84. package/dist/base/index.js +3 -1
  85. package/dist/base/services/index.js +3 -1
  86. package/dist/base/services/modules/DadosMestres/FavoritoMenu/FavoritoMenuService.js +35 -0
  87. package/dist/base/services/modules/DadosMestres/FavoritoMenu/FavoritoMenuService.js.map +1 -0
  88. package/dist/base/services/modules/index.js +3 -1
  89. package/dist/base/services/modules/performance/PerformanceService.js +15 -0
  90. package/dist/base/services/modules/performance/PerformanceService.js.map +1 -1
  91. package/dist/base/services/modules/performance/index.js +3 -1
  92. package/dist/base/utils/functions/stringUtils.js +2 -0
  93. package/dist/base/utils/functions/stringUtils.js.map +1 -1
  94. package/lib/@ecme/components/layouts/PostLoginLayout/components/CollapsibleSide.stories.tsx +2 -2
  95. package/lib/@ecme/components/layouts/PostLoginLayout/components/ContentOverlay.stories.tsx +2 -2
  96. package/lib/@ecme/components/layouts/PostLoginLayout/components/FrameLessSide.stories.tsx +2 -2
  97. package/lib/@ecme/components/layouts/PostLoginLayout/components/StackedSide.stories.tsx +2 -2
  98. package/lib/@ecme/components/layouts/PostLoginLayout/components/TopBarClassic.stories.tsx +2 -2
  99. package/lib/@ecme/components/template/Header.stories.tsx +2 -2
  100. package/lib/@ecme/components/template/Search.stories.tsx +23 -5
  101. package/lib/@ecme/components/template/Search.tsx +366 -83
  102. package/lib/@ecme/locales/index.ts +0 -4
  103. package/lib/@ecme/mock/data/favoritosMenuData.ts +3 -0
  104. package/lib/@ecme/mock/fakeApi/favoritosMenuFakeApi.ts +87 -0
  105. package/lib/@ecme/mock/index.ts +1 -0
  106. package/lib/base/@types/api/request/dm/TCreateFavoritoMenuRequest.ts +3 -0
  107. package/lib/base/@types/api/request/dm/TReorderFavoritosMenuRequest.ts +3 -0
  108. package/lib/base/@types/api/response/dadosMestres/TFavoritoMenuResponse.ts +7 -0
  109. package/lib/base/@types/models/performance/ppr.ts +67 -0
  110. package/lib/base/configs/api.config.ts +2 -2
  111. package/lib/base/configs/endpoints.config/DadosMestres/FavoritoMenu/favoritoMenu.endpoint.config.ts +10 -0
  112. package/lib/base/configs/endpoints.config/endpoints.navigation.ts +2 -0
  113. package/lib/base/configs/endpoints.config/performance/config/ppr.ts +1 -0
  114. package/lib/base/configs/navigation.config/submenu/performance/config/exportarResultadosPpr.submenu.ts +1 -1
  115. package/lib/base/services/modules/DadosMestres/FavoritoMenu/FavoritoMenuService.ts +35 -0
  116. package/lib/base/services/modules/performance/PerformanceService.ts +16 -0
  117. package/lib/base/utils/functions/stringUtils.ts +2 -0
  118. package/package.json +1 -1
@@ -11,10 +11,24 @@ import debounce from 'lodash/debounce'
11
11
  import { useEffect, useMemo, useRef, useState } from 'react'
12
12
  import Highlighter from 'react-highlight-words'
13
13
  import { useTranslation } from 'react-i18next'
14
- import { HiChevronRight, HiOutlineSearch } from 'react-icons/hi'
14
+ import {
15
+ HiChevronRight,
16
+ HiOutlineSearch,
17
+ HiOutlineStar,
18
+ HiStar,
19
+ HiOutlineTrash,
20
+ } from 'react-icons/hi'
15
21
  import { PiMagnifyingGlassDuotone } from 'react-icons/pi'
16
22
  import { Link } from 'react-router-dom'
17
23
  import { Spinner } from '../ui'
24
+ import Notification from '@/components/ui/Notification'
25
+ import toast from '@/components/ui/toast'
26
+ import {
27
+ apiFindFavoritosByUsuario,
28
+ apiDeleteFavoritoMenu,
29
+ apiCreateFavoritoMenu,
30
+ } from '@base/services/modules/DadosMestres/FavoritoMenu/FavoritoMenuService'
31
+ import { TFavoritoMenuResponse } from '@base/@types/api/response/dadosMestres/TFavoritoMenuResponse'
18
32
 
19
33
  type SearchData = {
20
34
  key: string
@@ -55,7 +69,10 @@ const filterNavigationByAuthority = (
55
69
  if (filteredSubMenu.length > 0) {
56
70
  acc.push({ ...nav, subMenu: filteredSubMenu })
57
71
  }
58
- } else if ((nav.path && userAuthority.includes(nav.path)) || !nav.path) {
72
+ } else if (
73
+ (nav.path && userAuthority.includes(nav.path)) ||
74
+ !nav.path
75
+ ) {
59
76
  acc.push(nav)
60
77
  }
61
78
 
@@ -71,49 +88,110 @@ const ListItem = (props: {
71
88
  keyWord: string
72
89
  breadcrumbs: string[]
73
90
  onNavigate: () => void
91
+ menuKey: string
92
+ isFavorited: boolean
93
+ favToggling: boolean
94
+ onToggleFavorite: (menuKey: string) => void
74
95
  }) => {
75
- const { icon, label, url = '', keyWord, onNavigate, breadcrumbs } = props
96
+ const {
97
+ icon,
98
+ label,
99
+ url = '',
100
+ keyWord,
101
+ onNavigate,
102
+ breadcrumbs,
103
+ menuKey,
104
+ isFavorited,
105
+ favToggling,
106
+ onToggleFavorite,
107
+ } = props
76
108
 
77
109
  const makeBreadcrumbs = (breadcrumbs: string[]) => {
78
110
  return breadcrumbs.join(' / ')
79
111
  }
80
112
 
81
113
  return (
82
- <Link to={url} onClick={onNavigate}>
83
- <div
84
- className={classNames(
85
- 'flex items-center justify-between rounded-xl p-3 cursor-pointer user-select',
86
- 'hover:bg-gray-100 dark:hover:bg-gray-700',
87
- )}
88
- >
89
- <div className="flex items-center gap-2">
90
- {/* <div
91
- className={classNames(
92
- 'rounded-lg border-2 border-gray-200 shadow-sm text-xl group-hover:shadow h-10 w-10 flex items-center justify-center bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100',
93
- )}
94
- >
95
- {icon && navigationIcon[icon]}
96
- </div> */}
97
- <div className="text-gray-900 dark:text-gray-300">
98
- <Highlighter
99
- autoEscape
100
- highlightClassName={classNames(
101
- 'text-primary',
102
- 'underline bg-transparent font-semibold dark:text-white',
103
- )}
104
- searchWords={[keyWord]}
105
- textToHighlight={makeBreadcrumbs(breadcrumbs)}
106
- />
114
+ <div className="flex items-center gap-1 pr-4">
115
+ <Link to={url} onClick={onNavigate} className="flex-1">
116
+ <div
117
+ className={classNames(
118
+ 'flex items-center justify-between rounded-xl p-3 cursor-pointer user-select',
119
+ 'hover:bg-gray-100 dark:hover:bg-gray-700',
120
+ )}
121
+ >
122
+ <div className="w-72">
123
+ <div className="overflow-hidden text-gray-900 dark:text-gray-300">
124
+ <Highlighter
125
+ autoEscape
126
+ highlightClassName={classNames(
127
+ 'text-primary',
128
+ 'underline bg-transparent font-semibold dark:text-white',
129
+ )}
130
+ searchWords={[keyWord]}
131
+ textToHighlight={makeBreadcrumbs(breadcrumbs)}
132
+ />
133
+ </div>
107
134
  </div>
135
+ <HiChevronRight className="shrink-0 text-lg" />
108
136
  </div>
109
- <HiChevronRight className="text-lg" />
110
- </div>
111
- </Link>
137
+ </Link>
138
+
139
+ <button
140
+ type="button"
141
+ className="flex-shrink-0 rounded-lg p-2 text-lg transition-colors hover:bg-gray-100 dark:hover:bg-gray-700"
142
+ disabled={favToggling}
143
+ onClick={(e) => {
144
+ e.preventDefault()
145
+ e.stopPropagation()
146
+ onToggleFavorite(menuKey)
147
+ }}
148
+ >
149
+ {favToggling ? (
150
+ <Spinner size={18} />
151
+ ) : isFavorited ? (
152
+ <HiStar className="text-yellow-500" />
153
+ ) : (
154
+ <HiOutlineStar className="text-gray-400 hover:text-yellow-500" />
155
+ )}
156
+ </button>
157
+ </div>
112
158
  )
113
159
  }
114
160
 
115
161
  type NavigationTreeFlat = NavigationTree & { parents: string[] }
116
162
 
163
+ type NavItemInfo = {
164
+ key: string
165
+ path: string
166
+ title: string
167
+ }
168
+
169
+ const flattenNavTree = (
170
+ navTree: NavigationTree[],
171
+ ): Map<string, NavItemInfo> => {
172
+ const map = new Map<string, NavItemInfo>()
173
+
174
+ const walk = (items: NavigationTree[]) => {
175
+ for (const item of items) {
176
+ if (item.type === NAV_ITEM_TYPE_ITEM && item.path) {
177
+ map.set(item.key, {
178
+ key: item.key,
179
+ path: item.path,
180
+ title: item.title,
181
+ })
182
+ }
183
+ if (item.subMenu && item.subMenu.length > 0) {
184
+ walk(item.subMenu)
185
+ }
186
+ }
187
+ }
188
+
189
+ walk(navTree)
190
+ return map
191
+ }
192
+
193
+ const navItemMap = flattenNavTree(navigationConfig)
194
+
117
195
  const _Search = ({ className }: { className?: string }) => {
118
196
  const [loading, setLoading] = useState(false)
119
197
  const [searchDialogOpen, setSearchDialogOpen] = useState(false)
@@ -121,6 +199,124 @@ const _Search = ({ className }: { className?: string }) => {
121
199
  const [noResult, setNoResult] = useState(false)
122
200
  const userAuthority = useSessionUser((state) => state.user.authority)
123
201
 
202
+ // -- Favorites state --
203
+ const [favDialogOpen, setFavDialogOpen] = useState(false)
204
+ const [favLoading, setFavLoading] = useState(false)
205
+ const [favorites, setFavorites] = useState<TFavoritoMenuResponse[]>([])
206
+ const [removingId, setRemovingId] = useState<number | null>(null)
207
+ const [favoritedKeys, setFavoritedKeys] = useState<Set<string>>(new Set())
208
+ const [togglingKeys, setTogglingKeys] = useState<Set<string>>(new Set())
209
+ const userId = useSessionUser((state) => state.user.userId)
210
+
211
+ const loadFavorites = async () => {
212
+ if (!userId) return
213
+ setFavLoading(true)
214
+ try {
215
+ const data = await apiFindFavoritosByUsuario(Number(userId))
216
+ setFavorites(data)
217
+ setFavoritedKeys(new Set(data.map((f) => f.menuKey)))
218
+ } catch {
219
+ toast.push(
220
+ <Notification title="Erro" type="danger">
221
+ Falha ao carregar favoritos.
222
+ </Notification>,
223
+ )
224
+ setFavorites([])
225
+ setFavoritedKeys(new Set())
226
+ } finally {
227
+ setFavLoading(false)
228
+ }
229
+ }
230
+
231
+ const handleFavRemove = async (id: number) => {
232
+ setRemovingId(id)
233
+ try {
234
+ await apiDeleteFavoritoMenu(id)
235
+ setFavorites((prev) => prev.filter((f) => f.id !== id))
236
+ setFavoritedKeys((prev) => {
237
+ const next = new Set(prev)
238
+ const removed = favorites.find((f) => f.id === id)
239
+ if (removed) next.delete(removed.menuKey)
240
+ return next
241
+ })
242
+ } catch {
243
+ toast.push(
244
+ <Notification title="Erro" type="danger">
245
+ Falha ao remover favorito.
246
+ </Notification>,
247
+ )
248
+ } finally {
249
+ setRemovingId(null)
250
+ }
251
+ }
252
+
253
+ const handleFavToggle = async (menuKey: string) => {
254
+ if (!userId || togglingKeys.has(menuKey)) return
255
+ setTogglingKeys((prev) => new Set(prev).add(menuKey))
256
+ try {
257
+ if (favoritedKeys.has(menuKey)) {
258
+ const fav = favorites.find((f) => f.menuKey === menuKey)
259
+ if (fav) {
260
+ await apiDeleteFavoritoMenu(fav.id)
261
+ setFavorites((prev) => prev.filter((f) => f.id !== fav.id))
262
+ setFavoritedKeys((prev) => {
263
+ const next = new Set(prev)
264
+ next.delete(menuKey)
265
+ return next
266
+ })
267
+ }
268
+ } else {
269
+ const created = await apiCreateFavoritoMenu({ menuKey })
270
+ setFavorites((prev) => [...prev, created])
271
+ setFavoritedKeys((prev) => new Set(prev).add(menuKey))
272
+ }
273
+ } catch {
274
+ toast.push(
275
+ <Notification title="Erro" type="danger">
276
+ {favoritedKeys.has(menuKey)
277
+ ? 'Falha ao remover favorito.'
278
+ : 'Falha ao adicionar favorito.'}
279
+ </Notification>,
280
+ )
281
+ } finally {
282
+ setTogglingKeys((prev) => {
283
+ const next = new Set(prev)
284
+ next.delete(menuKey)
285
+ return next
286
+ })
287
+ }
288
+ }
289
+
290
+ const handleFavOpen = () => {
291
+ setFavDialogOpen(true)
292
+ }
293
+
294
+ const handleFavClose = () => {
295
+ setFavDialogOpen(false)
296
+ }
297
+
298
+ useEffect(() => {
299
+ if (favDialogOpen) {
300
+ loadFavorites()
301
+ }
302
+ }, [favDialogOpen, userId])
303
+
304
+ // Also load favorites when search opens (for star toggles in results)
305
+ useEffect(() => {
306
+ if (searchDialogOpen && userId) {
307
+ loadFavorites()
308
+ }
309
+ }, [searchDialogOpen, userId])
310
+
311
+ const enrichedFavorites = useMemo(() => {
312
+ return favorites.map((fav) => ({
313
+ ...fav,
314
+ navInfo: navItemMap.get(fav.menuKey),
315
+ }))
316
+ }, [favorites])
317
+
318
+ // -- end favorites state --
319
+
124
320
  const inputRef = useRef<HTMLInputElement>(null)
125
321
 
126
322
  const handleReset = () => {
@@ -243,12 +439,21 @@ const _Search = ({ className }: { className?: string }) => {
243
439
 
244
440
  return (
245
441
  <>
246
- <div
247
- className={classNames(className, 'text-2xl')}
248
- onClick={handleSearchOpen}
249
- >
250
- <PiMagnifyingGlassDuotone />
442
+ <div className="flex items-center gap-1">
443
+ <div
444
+ className={classNames(className, 'text-2xl')}
445
+ onClick={handleSearchOpen}
446
+ >
447
+ <PiMagnifyingGlassDuotone />
448
+ </div>
449
+ <div
450
+ className={classNames(className, 'text-2xl')}
451
+ onClick={handleFavOpen}
452
+ >
453
+ <HiOutlineStar />
454
+ </div>
251
455
  </div>
456
+ {/* Search dialog */}
252
457
  <Dialog
253
458
  contentClassName="p-0"
254
459
  isOpen={searchDialogOpen}
@@ -262,7 +467,9 @@ const _Search = ({ className }: { className?: string }) => {
262
467
  <input
263
468
  ref={inputRef}
264
469
  className="block w-full bg-transparent p-4 text-base text-gray-900 outline-none ring-0 dark:text-gray-100"
265
- placeholder={t('common.input.placeholder.search')}
470
+ placeholder={t(
471
+ 'common.input.placeholder.search',
472
+ )}
266
473
  onChange={handleSearch}
267
474
  />
268
475
  </div>
@@ -272,58 +479,134 @@ const _Search = ({ className }: { className?: string }) => {
272
479
  </div>
273
480
  <div className="px-5 py-6">
274
481
  {loading ? (
275
- <div
276
- className={classNames(
277
- 'flex items-center justify-center',
482
+ <div className="flex items-center justify-center">
483
+ <Spinner size={40} />
484
+ </div>
485
+ ) : (
486
+ <ScrollBar className="max-h-[350px] overflow-y-auto">
487
+ {searchResult.map((result) => (
488
+ <div key={result.title} className="mb-4">
489
+ <h6 className="mb-3">{result.title}</h6>
490
+ {result.data.map((data, index) => (
491
+ <ListItem
492
+ key={data.title + index}
493
+ icon={data.icon}
494
+ breadcrumbs={data.breadcrumbs}
495
+ label={data.title}
496
+ url={data.path}
497
+ keyWord={
498
+ inputRef.current?.value ||
499
+ ''
500
+ }
501
+ onNavigate={handleNavigate}
502
+ menuKey={data.key}
503
+ isFavorited={favoritedKeys.has(
504
+ data.key,
505
+ )}
506
+ favToggling={togglingKeys.has(
507
+ data.key,
508
+ )}
509
+ onToggleFavorite={
510
+ handleFavToggle
511
+ }
512
+ />
513
+ ))}
514
+ </div>
515
+ ))}
516
+ {searchResult.length === 0 && noResult && (
517
+ <div className="my-10 text-center text-lg">
518
+ <span className="pr-1">
519
+ {t('common.status.noResultsFor')}
520
+ </span>
521
+ <span className="heading-text">
522
+ {`'`}
523
+ {inputRef.current?.value}
524
+ {`'`}
525
+ </span>
526
+ </div>
278
527
  )}
279
- >
528
+ </ScrollBar>
529
+ )}
530
+ </div>
531
+ </div>
532
+ </Dialog>
533
+ {/* Favorites dialog */}
534
+ <Dialog
535
+ contentClassName="p-0"
536
+ isOpen={favDialogOpen}
537
+ closable={false}
538
+ onRequestClose={handleFavClose}
539
+ >
540
+ <div>
541
+ <div className="flex items-center justify-between border-b border-gray-200 px-4 py-4 dark:border-gray-600">
542
+ <div className="flex items-center gap-2">
543
+ <HiStar className="text-xl text-yellow-500" />
544
+ <h6 className="text-base font-semibold text-gray-900 dark:text-gray-100">
545
+ Favoritos
546
+ </h6>
547
+ </div>
548
+ <Button size="xs" onClick={handleFavClose}>
549
+ Esc
550
+ </Button>
551
+ </div>
552
+ <div className="px-5 py-6">
553
+ {favLoading ? (
554
+ <div className="flex items-center justify-center py-10">
280
555
  <Spinner size={40} />
281
556
  </div>
282
557
  ) : (
283
- <>
284
- <ScrollBar className="max-h-[350px] overflow-y-auto">
285
- {searchResult.map((result) => (
286
- <div
287
- key={result.title}
288
- className="mb-4"
289
- >
290
- <h6 className="mb-3">
291
- {result.title}
292
- </h6>
293
- {result.data.map((data, index) => (
294
- <ListItem
295
- key={data.title + index}
296
- icon={data.icon}
297
- breadcrumbs={
298
- data.breadcrumbs
558
+ <ScrollBar className="max-h-[350px] overflow-y-auto">
559
+ {enrichedFavorites.length === 0 ? (
560
+ <div className="my-10 text-center text-lg text-gray-500">
561
+ Nenhum favorito salvo.
562
+ </div>
563
+ ) : (
564
+ <div className="flex flex-col gap-1">
565
+ {enrichedFavorites.map((fav) => (
566
+ <div
567
+ key={fav.id}
568
+ className="flex items-center gap-1 pr-4"
569
+ >
570
+ <Link
571
+ to={
572
+ fav.navInfo?.path || '#'
299
573
  }
300
- label={data.title}
301
- url={data.path}
302
- keyWord={
303
- inputRef.current
304
- ?.value || ''
574
+ className="flex-1"
575
+ onClick={handleFavClose}
576
+ >
577
+ <div className="user-select flex cursor-pointer items-center justify-between rounded-xl p-3 hover:bg-gray-100 dark:hover:bg-gray-700">
578
+ <div className="w-72">
579
+ <div className="overflow-hidden text-gray-900 dark:text-gray-300">
580
+ {fav.navInfo
581
+ ?.title ||
582
+ fav.menuKey}
583
+ </div>
584
+ </div>
585
+ <HiChevronRight className="h-6 w-6 shrink-0" />
586
+ </div>
587
+ </Link>
588
+ <button
589
+ type="button"
590
+ className="flex-shrink-0 rounded-lg p-2 text-lg text-gray-400 transition-colors hover:bg-gray-100 hover:text-red-500 dark:hover:bg-gray-700"
591
+ disabled={
592
+ removingId === fav.id
305
593
  }
306
- onNavigate={handleNavigate}
307
- />
308
- ))}
309
- </div>
310
- ))}
311
- {searchResult.length === 0 && noResult && (
312
- <div className="my-10 text-center text-lg">
313
- <span className="pr-1">
314
- {t(
315
- 'common.status.noResultsFor',
316
- )}
317
- </span>
318
- <span className="heading-text">
319
- {`'`}
320
- {inputRef.current?.value}
321
- {`'`}
322
- </span>
323
- </div>
324
- )}
325
- </ScrollBar>
326
- </>
594
+ onClick={(e) => {
595
+ e.stopPropagation()
596
+ handleFavRemove(fav.id)
597
+ }}
598
+ >
599
+ {removingId === fav.id ? (
600
+ <Spinner size={18} />
601
+ ) : (
602
+ <HiOutlineTrash />
603
+ )}
604
+ </button>
605
+ </div>
606
+ ))}
607
+ </div>
608
+ )}
609
+ </ScrollBar>
327
610
  )}
328
611
  </div>
329
612
  </div>
@@ -1,8 +1,4 @@
1
1
  import i18n from './locales'
2
2
 
3
3
  export { dateLocales } from './locales'
4
- export {
5
- ensureAppLocaleResourcesLoaded,
6
- initAllAppLocaleResources,
7
- } from './initAll'
8
4
  export default i18n
@@ -0,0 +1,3 @@
1
+ import { TFavoritoMenuResponse } from '@base/@types/api/response/dadosMestres/TFavoritoMenuResponse'
2
+
3
+ export const favoritosMenuData: TFavoritoMenuResponse[] = []
@@ -0,0 +1,87 @@
1
+ import { mock } from '../MockAdapter'
2
+ import { favoritosMenuData } from '../data/favoritosMenuData'
3
+ import { TFavoritoMenuResponse } from '@base/@types/api/response/dadosMestres/TFavoritoMenuResponse'
4
+
5
+ const BASE_URL = '/dados-mestres/api/favoritos-menu'
6
+
7
+ const randomDelay = () => 300 + Math.random() * 1200 // 300-1500ms
8
+
9
+ type MockConfig = { data?: string; url?: string }
10
+ let favoritos: TFavoritoMenuResponse[] = [...favoritosMenuData]
11
+ let nextId = favoritos.length + 1
12
+
13
+ function withDelay<T>(fn: (config: MockConfig) => T): (config: MockConfig) => Promise<T> {
14
+ return (config) => {
15
+ const { promise, resolve } = Promise.withResolvers<T>()
16
+ setTimeout(() => resolve(fn(config)), randomDelay())
17
+ return promise
18
+ }
19
+ }
20
+
21
+ // POST / — criar favorito
22
+ mock.onPost(BASE_URL).reply(withDelay((config) => {
23
+ const body = JSON.parse(config.data ?? '{}')
24
+ const menuKey: string = body.menuKey
25
+
26
+ if (!menuKey) {
27
+ return [400, { status: 400, title: 'Bad Request', detail: 'menuKey is required' }]
28
+ }
29
+
30
+ const exists = favoritos.find((f) => f.menuKey === menuKey && f.ativo)
31
+ if (exists) {
32
+ return [400, { status: 400, title: 'Bad Request', detail: 'Menu already favorited' }]
33
+ }
34
+
35
+ const novo: TFavoritoMenuResponse = {
36
+ id: nextId++,
37
+ usuarioId: 1,
38
+ menuKey,
39
+ ordem: favoritos.filter((f) => f.ativo).length,
40
+ ativo: true,
41
+ }
42
+ favoritos.push(novo)
43
+ return [200, novo]
44
+ }))
45
+
46
+ // GET /usuario/{usuarioId} — listar favoritos
47
+ mock.onGet(new RegExp(`${BASE_URL}/usuario/\\d+`)).reply(withDelay(() => {
48
+ const ativos = favoritos
49
+ .filter((f) => f.ativo)
50
+ .sort((a, b) => a.ordem - b.ordem)
51
+ return [200, ativos]
52
+ }))
53
+
54
+ // DELETE /{id} — remover (soft-delete)
55
+ mock.onDelete(new RegExp(`${BASE_URL}/\\d+`)).reply(withDelay((config) => {
56
+ const id = Number(config.url?.split('/').pop())
57
+ const fav = favoritos.find((f) => f.id === id)
58
+
59
+ if (!fav || !fav.ativo) {
60
+ return [400, { status: 400, title: 'Bad Request', detail: 'Favorito not found' }]
61
+ }
62
+
63
+ fav.ativo = false
64
+ return [204]
65
+ }))
66
+
67
+ // PUT /reorder — reordenar
68
+ mock.onPut(`${BASE_URL}/reorder`).reply(withDelay((config) => {
69
+ const body = JSON.parse(config.data ?? '{}')
70
+ const orderedIds: number[] = body.orderedIds
71
+
72
+ if (!orderedIds || orderedIds.length === 0) {
73
+ return [400, { status: 400, title: 'Bad Request', detail: 'orderedIds is required' }]
74
+ }
75
+
76
+ const reordered: TFavoritoMenuResponse[] = []
77
+ for (let i = 0; i < orderedIds.length; i++) {
78
+ const fav = favoritos.find((f) => f.id === orderedIds[i] && f.ativo)
79
+ if (!fav) {
80
+ return [400, { status: 400, title: 'Bad Request', detail: `Favorito ${orderedIds[i]} not found` }]
81
+ }
82
+ fav.ordem = i
83
+ reordered.push(fav)
84
+ }
85
+
86
+ return [200, reordered]
87
+ }))
@@ -12,6 +12,7 @@ import './fakeApi/mailFakeApi'
12
12
  import './fakeApi/accountsFakeApi'
13
13
  import './fakeApi/aiFakeApi'
14
14
  import './fakeApi/chatFakeApi'
15
+ import './fakeApi/favoritosMenuFakeApi'
15
16
  import './fakeApi/helpCenterFakeApi'
16
17
  import './fakeApi/dashboardFakeApi'
17
18
 
@@ -0,0 +1,3 @@
1
+ export type TCreateFavoritoMenuRequest = {
2
+ menuKey: string
3
+ }
@@ -0,0 +1,3 @@
1
+ export type TReorderFavoritosMenuRequest = {
2
+ orderedIds: number[]
3
+ }
@@ -0,0 +1,7 @@
1
+ export type TFavoritoMenuResponse = {
2
+ id: number
3
+ usuarioId: number
4
+ menuKey: string
5
+ ordem: number
6
+ ativo: boolean
7
+ }