@hed-hog/core 0.0.370 → 0.0.371

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 (248) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +2 -0
  3. package/dist/ai/ai.service.d.ts.map +1 -1
  4. package/dist/ai/ai.service.js +10 -7
  5. package/dist/ai/ai.service.js.map +1 -1
  6. package/dist/app-command/app-command.module.d.ts +3 -0
  7. package/dist/app-command/app-command.module.d.ts.map +1 -0
  8. package/dist/app-command/app-command.module.js +22 -0
  9. package/dist/app-command/app-command.module.js.map +1 -0
  10. package/dist/app-command/app-command.registry.d.ts +22 -0
  11. package/dist/app-command/app-command.registry.d.ts.map +1 -0
  12. package/dist/{webhook-integration/webhook-command.registry.js → app-command/app-command.registry.js} +6 -6
  13. package/dist/app-command/app-command.registry.js.map +1 -0
  14. package/dist/auth/auth.controller.d.ts +2 -2
  15. package/dist/auth/auth.controller.d.ts.map +1 -1
  16. package/dist/auth/auth.controller.js +20 -7
  17. package/dist/auth/auth.controller.js.map +1 -1
  18. package/dist/auth/auth.service.d.ts +3 -3
  19. package/dist/auth/auth.service.d.ts.map +1 -1
  20. package/dist/auth/auth.service.js +10 -36
  21. package/dist/auth/auth.service.js.map +1 -1
  22. package/dist/auth/dto/forget.dto.d.ts +1 -0
  23. package/dist/auth/dto/forget.dto.d.ts.map +1 -1
  24. package/dist/auth/dto/forget.dto.js +5 -0
  25. package/dist/auth/dto/forget.dto.js.map +1 -1
  26. package/dist/auth/guards/auth.guard.d.ts.map +1 -1
  27. package/dist/auth/guards/auth.guard.js +4 -1
  28. package/dist/auth/guards/auth.guard.js.map +1 -1
  29. package/dist/challenge/challenge.service.d.ts +8 -7
  30. package/dist/challenge/challenge.service.d.ts.map +1 -1
  31. package/dist/challenge/challenge.service.js +27 -19
  32. package/dist/challenge/challenge.service.js.map +1 -1
  33. package/dist/file/dto/upload.dto.d.ts +3 -0
  34. package/dist/file/dto/upload.dto.d.ts.map +1 -1
  35. package/dist/file/dto/upload.dto.js +22 -0
  36. package/dist/file/dto/upload.dto.js.map +1 -1
  37. package/dist/file/file.controller.d.ts +2 -1
  38. package/dist/file/file.controller.d.ts.map +1 -1
  39. package/dist/file/file.controller.js +29 -16
  40. package/dist/file/file.controller.js.map +1 -1
  41. package/dist/file/file.service.d.ts +51 -1
  42. package/dist/file/file.service.d.ts.map +1 -1
  43. package/dist/file/file.service.js +250 -18
  44. package/dist/file/file.service.js.map +1 -1
  45. package/dist/file/image-optimization.service.d.ts +17 -4
  46. package/dist/file/image-optimization.service.d.ts.map +1 -1
  47. package/dist/file/image-optimization.service.js +101 -46
  48. package/dist/file/image-optimization.service.js.map +1 -1
  49. package/dist/file/provider/s3.provider.d.ts.map +1 -1
  50. package/dist/file/provider/s3.provider.js +15 -7
  51. package/dist/file/provider/s3.provider.js.map +1 -1
  52. package/dist/file/provider/spaces.provider.d.ts.map +1 -1
  53. package/dist/file/provider/spaces.provider.js +18 -8
  54. package/dist/file/provider/spaces.provider.js.map +1 -1
  55. package/dist/index.d.ts +4 -1
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +4 -1
  58. package/dist/index.js.map +1 -1
  59. package/dist/integration/services/event-subscriber.registry.d.ts +5 -0
  60. package/dist/integration/services/event-subscriber.registry.d.ts.map +1 -1
  61. package/dist/integration/services/event-subscriber.registry.js +15 -0
  62. package/dist/integration/services/event-subscriber.registry.js.map +1 -1
  63. package/dist/integration/services/outbox.processor.d.ts.map +1 -1
  64. package/dist/integration/services/outbox.processor.js +2 -4
  65. package/dist/integration/services/outbox.processor.js.map +1 -1
  66. package/dist/integration/services/outbox.service.d.ts +4 -2
  67. package/dist/integration/services/outbox.service.d.ts.map +1 -1
  68. package/dist/integration/services/outbox.service.js +26 -15
  69. package/dist/integration/services/outbox.service.js.map +1 -1
  70. package/dist/integration-profile/integration-credential-crypto.service.d.ts +43 -0
  71. package/dist/integration-profile/integration-credential-crypto.service.d.ts.map +1 -0
  72. package/dist/integration-profile/integration-credential-crypto.service.js +142 -0
  73. package/dist/integration-profile/integration-credential-crypto.service.js.map +1 -0
  74. package/dist/integration-profile/integration-profile.controller.d.ts +6 -7
  75. package/dist/integration-profile/integration-profile.controller.d.ts.map +1 -1
  76. package/dist/integration-profile/integration-profile.controller.js +4 -3
  77. package/dist/integration-profile/integration-profile.controller.js.map +1 -1
  78. package/dist/integration-profile/integration-profile.module.d.ts.map +1 -1
  79. package/dist/integration-profile/integration-profile.module.js +5 -2
  80. package/dist/integration-profile/integration-profile.module.js.map +1 -1
  81. package/dist/integration-profile/integration-profile.secrets.d.ts +16 -0
  82. package/dist/integration-profile/integration-profile.secrets.d.ts.map +1 -0
  83. package/dist/integration-profile/integration-profile.secrets.js +56 -0
  84. package/dist/integration-profile/integration-profile.secrets.js.map +1 -0
  85. package/dist/integration-profile/integration-profile.service.d.ts +125 -8
  86. package/dist/integration-profile/integration-profile.service.d.ts.map +1 -1
  87. package/dist/integration-profile/integration-profile.service.js +257 -68
  88. package/dist/integration-profile/integration-profile.service.js.map +1 -1
  89. package/dist/integration-profile/integration-profile.utils.d.ts +17 -0
  90. package/dist/integration-profile/integration-profile.utils.d.ts.map +1 -1
  91. package/dist/integration-profile/integration-profile.utils.js +31 -0
  92. package/dist/integration-profile/integration-profile.utils.js.map +1 -1
  93. package/dist/language/en.json +178 -0
  94. package/dist/language/pt.json +178 -0
  95. package/dist/mail/mail.controller.d.ts.map +1 -1
  96. package/dist/mail/mail.controller.js +5 -28
  97. package/dist/mail/mail.controller.js.map +1 -1
  98. package/dist/mail/mail.module.js +18 -18
  99. package/dist/mail/mail.module.js.map +1 -1
  100. package/dist/mail/mail.service.d.ts +13 -2
  101. package/dist/mail/mail.service.d.ts.map +1 -1
  102. package/dist/mail/mail.service.js +25 -15
  103. package/dist/mail/mail.service.js.map +1 -1
  104. package/dist/mcp-chat/mcp-chat.service.js +6 -5
  105. package/dist/mcp-chat/mcp-chat.service.js.map +1 -1
  106. package/dist/oauth/providers/facebook.provider.js +5 -5
  107. package/dist/oauth/providers/facebook.provider.js.map +1 -1
  108. package/dist/oauth/providers/github.provider.js +5 -5
  109. package/dist/oauth/providers/github.provider.js.map +1 -1
  110. package/dist/oauth/providers/google.provider.js +5 -5
  111. package/dist/oauth/providers/google.provider.js.map +1 -1
  112. package/dist/oauth/providers/microsoft-entra-id.provider.js +5 -5
  113. package/dist/oauth/providers/microsoft-entra-id.provider.js.map +1 -1
  114. package/dist/oauth/providers/microsoft.provider.js +5 -5
  115. package/dist/oauth/providers/microsoft.provider.js.map +1 -1
  116. package/dist/pdf/pdf.service.d.ts +4 -0
  117. package/dist/pdf/pdf.service.d.ts.map +1 -1
  118. package/dist/pdf/pdf.service.js +16 -9
  119. package/dist/pdf/pdf.service.js.map +1 -1
  120. package/dist/profile/profile.controller.d.ts +7 -7
  121. package/dist/profile/profile.controller.d.ts.map +1 -1
  122. package/dist/profile/profile.controller.js +28 -21
  123. package/dist/profile/profile.controller.js.map +1 -1
  124. package/dist/profile/profile.service.d.ts +7 -7
  125. package/dist/profile/profile.service.d.ts.map +1 -1
  126. package/dist/profile/profile.service.js +21 -25
  127. package/dist/profile/profile.service.js.map +1 -1
  128. package/dist/setting/setting.service.d.ts +1 -0
  129. package/dist/setting/setting.service.d.ts.map +1 -1
  130. package/dist/setting/setting.service.js +15 -2
  131. package/dist/setting/setting.service.js.map +1 -1
  132. package/dist/token/token.service.d.ts.map +1 -1
  133. package/dist/token/token.service.js +2 -1
  134. package/dist/token/token.service.js.map +1 -1
  135. package/dist/user/user.controller.d.ts +9 -0
  136. package/dist/user/user.controller.d.ts.map +1 -1
  137. package/dist/user/user.controller.js +13 -0
  138. package/dist/user/user.controller.js.map +1 -1
  139. package/dist/user/user.service.d.ts +7 -0
  140. package/dist/user/user.service.d.ts.map +1 -1
  141. package/dist/user/user.service.js +19 -0
  142. package/dist/user/user.service.js.map +1 -1
  143. package/dist/utils/webauthn-origin.d.ts +11 -0
  144. package/dist/utils/webauthn-origin.d.ts.map +1 -0
  145. package/dist/utils/webauthn-origin.js +37 -0
  146. package/dist/utils/webauthn-origin.js.map +1 -0
  147. package/dist/webhook-integration/webhook-integration.controller.d.ts +13 -0
  148. package/dist/webhook-integration/webhook-integration.controller.d.ts.map +1 -1
  149. package/dist/webhook-integration/webhook-integration.controller.js +29 -0
  150. package/dist/webhook-integration/webhook-integration.controller.js.map +1 -1
  151. package/dist/webhook-integration/webhook-integration.module.js +3 -3
  152. package/dist/webhook-integration/webhook-integration.module.js.map +1 -1
  153. package/dist/webhook-integration/webhook-integration.service.d.ts +18 -2
  154. package/dist/webhook-integration/webhook-integration.service.d.ts.map +1 -1
  155. package/dist/webhook-integration/webhook-integration.service.js +110 -6
  156. package/dist/webhook-integration/webhook-integration.service.js.map +1 -1
  157. package/hedhog/data/integration_provider.yaml +9 -0
  158. package/hedhog/data/integration_type.yaml +6 -0
  159. package/hedhog/data/menu.yaml +6 -0
  160. package/hedhog/data/role.yaml +7 -0
  161. package/hedhog/data/route.yaml +36 -0
  162. package/hedhog/data/setting_group.yaml +119 -13
  163. package/hedhog/frontend/app/access-log/page.tsx.ejs +35 -3
  164. package/hedhog/frontend/app/account/components/active-sessions.tsx.ejs +36 -3
  165. package/hedhog/frontend/app/account/components/mcp-tokens.tsx.ejs +39 -4
  166. package/hedhog/frontend/app/account/components/tokens.tsx.ejs +39 -4
  167. package/hedhog/frontend/app/ai_agent/page.tsx.ejs +35 -2
  168. package/hedhog/frontend/app/configurations/[slug]/components/byte-size-input.tsx.ejs +160 -0
  169. package/hedhog/frontend/app/configurations/[slug]/components/duration-input.tsx.ejs +157 -0
  170. package/hedhog/frontend/app/configurations/[slug]/components/setting-field.tsx.ejs +244 -63
  171. package/hedhog/frontend/app/dashboard/[slug]/dashboard-content.tsx.ejs +33 -1
  172. package/hedhog/frontend/app/dashboard/management/tabs/component-roles-tab.tsx.ejs +35 -3
  173. package/hedhog/frontend/app/dashboard/management/tabs/components-tab.tsx.ejs +36 -4
  174. package/hedhog/frontend/app/dashboard/management/tabs/dashboard-roles-tab.tsx.ejs +35 -3
  175. package/hedhog/frontend/app/dashboard/management/tabs/dashboards-tab.tsx.ejs +36 -4
  176. package/hedhog/frontend/app/dashboard/management/tabs/items-tab.tsx.ejs +35 -3
  177. package/hedhog/frontend/app/integration/profiles/page.tsx.ejs +141 -29
  178. package/hedhog/frontend/app/integrations-webhooks/page.tsx.ejs +118 -11
  179. package/hedhog/frontend/app/mail/log/page.tsx.ejs +37 -4
  180. package/hedhog/frontend/app/menu/page.tsx.ejs +34 -2
  181. package/hedhog/frontend/app/preferences/page.tsx.ejs +77 -0
  182. package/hedhog/frontend/app/roles/menus.tsx.ejs +35 -3
  183. package/hedhog/frontend/app/roles/page.tsx.ejs +34 -2
  184. package/hedhog/frontend/app/roles/routes.tsx.ejs +36 -4
  185. package/hedhog/frontend/app/roles/users.tsx.ejs +36 -4
  186. package/hedhog/frontend/app/routes/page.tsx.ejs +34 -2
  187. package/hedhog/frontend/app/setting-list/page.tsx.ejs +34 -2
  188. package/hedhog/frontend/app/users/page.tsx.ejs +51 -21
  189. package/hedhog/frontend/messages/en.json +69 -4
  190. package/hedhog/frontend/messages/pt.json +69 -4
  191. package/hedhog/table/setting.yaml +2 -0
  192. package/hedhog/table/webhook_call_log.yaml +49 -46
  193. package/package.json +8 -3
  194. package/src/ai/ai.service.ts +9 -6
  195. package/src/app-command/app-command.module.ts +9 -0
  196. package/src/{webhook-integration/webhook-command.registry.ts → app-command/app-command.registry.ts} +9 -9
  197. package/src/auth/auth.controller.ts +20 -4
  198. package/src/auth/auth.service.ts +10 -36
  199. package/src/auth/dto/forget.dto.ts +5 -1
  200. package/src/auth/guards/auth.guard.ts +4 -1
  201. package/src/challenge/challenge.service.ts +30 -19
  202. package/src/file/dto/upload.dto.ts +25 -1
  203. package/src/file/file.controller.ts +31 -18
  204. package/src/file/file.service.ts +341 -16
  205. package/src/file/image-optimization.service.ts +111 -46
  206. package/src/file/provider/s3.provider.ts +16 -7
  207. package/src/file/provider/spaces.provider.ts +15 -5
  208. package/src/index.ts +4 -1
  209. package/src/integration/services/event-subscriber.registry.ts +15 -0
  210. package/src/integration/services/outbox.processor.ts +2 -5
  211. package/src/integration/services/outbox.service.ts +35 -17
  212. package/src/integration-profile/integration-credential-crypto.service.ts +146 -0
  213. package/src/integration-profile/integration-profile.controller.ts +2 -0
  214. package/src/integration-profile/integration-profile.module.ts +5 -2
  215. package/src/integration-profile/integration-profile.secrets.ts +51 -0
  216. package/src/integration-profile/integration-profile.service.ts +335 -54
  217. package/src/integration-profile/integration-profile.utils.ts +56 -0
  218. package/src/language/en.json +5 -1
  219. package/src/language/pt.json +5 -1
  220. package/src/mail/mail.controller.ts +5 -29
  221. package/src/mail/mail.module.ts +5 -5
  222. package/src/mail/mail.service.ts +37 -13
  223. package/src/mcp-chat/mcp-chat.service.ts +5 -5
  224. package/src/oauth/oauth-callback-coordinator.service.spec.ts +159 -0
  225. package/src/oauth/providers/facebook.provider.ts +3 -3
  226. package/src/oauth/providers/github.provider.ts +3 -3
  227. package/src/oauth/providers/google.provider.ts +3 -3
  228. package/src/oauth/providers/microsoft-entra-id.provider.ts +3 -3
  229. package/src/oauth/providers/microsoft.provider.ts +3 -3
  230. package/src/pdf/pdf.service.ts +19 -6
  231. package/src/profile/profile.controller.ts +21 -14
  232. package/src/profile/profile.service.ts +23 -20
  233. package/src/security/security.service.spec.ts +222 -0
  234. package/src/setting/setting.service.ts +15 -2
  235. package/src/token/token.service.ts +2 -1
  236. package/src/user/user.controller.ts +26 -16
  237. package/src/user/user.service.ts +25 -0
  238. package/src/utils/webauthn-origin.ts +38 -0
  239. package/src/webhook-integration/webhook-integration.controller.ts +23 -0
  240. package/src/webhook-integration/webhook-integration.module.ts +3 -3
  241. package/src/webhook-integration/webhook-integration.service.ts +115 -5
  242. package/dist/webhook-integration/webhook-command.registry.d.ts +0 -22
  243. package/dist/webhook-integration/webhook-command.registry.d.ts.map +0 -1
  244. package/dist/webhook-integration/webhook-command.registry.js.map +0 -1
  245. package/src/menu/menu.service.spec.ts +0 -247
  246. package/src/route/route.service.spec.ts +0 -300
  247. package/src/screen/screen.service.spec.ts +0 -298
  248. package/src/user/user.service.spec.ts +0 -294
@@ -124,6 +124,8 @@ function getErrorMessage(error: unknown, fallback: string) {
124
124
  return fallback;
125
125
  }
126
126
 
127
+ const DEFAULT_PAGE_SIZES = [6, 12, 24, 48, 96] as const;
128
+
127
129
  export default function AiAgentPage() {
128
130
  const { request, currentLocaleCode, getSettingValue } = useApp();
129
131
  const t = useTranslations('core.AiAgentPage');
@@ -133,11 +135,41 @@ export default function AiAgentPage() {
133
135
  model: z.string().optional(),
134
136
  instructions: z.string().optional(),
135
137
  });
138
+ const { data: generalSettings } = useQuery<{
139
+ data: Array<{ slug: string; value: string }>;
140
+ }>({
141
+ queryKey: ['setting-group-general'],
142
+ queryFn: async () => {
143
+ const response = await request<{
144
+ data: Array<{ slug: string; value: string }>;
145
+ }>({ url: '/setting/group/general', method: 'GET' });
146
+ return response.data;
147
+ },
148
+ staleTime: 5 * 60 * 1000,
149
+ });
150
+
151
+ const pageSizeOptions = useMemo(() => {
152
+ const setting = generalSettings?.data?.find(
153
+ (s) => s.slug === 'pagination-page-sizes'
154
+ );
155
+ if (!setting?.value) return DEFAULT_PAGE_SIZES;
156
+ try {
157
+ const parsed = JSON.parse(setting.value) as string[];
158
+ const sizes = parsed
159
+ .map(Number)
160
+ .filter((n) => !isNaN(n) && n > 0)
161
+ .sort((a, b) => a - b);
162
+ return sizes.length > 0 ? sizes : DEFAULT_PAGE_SIZES;
163
+ } catch {
164
+ return DEFAULT_PAGE_SIZES;
165
+ }
166
+ }, [generalSettings]);
167
+
136
168
  const [page, setPage] = useState(1);
137
169
  const [pageSize, setPageSize] = usePersistedPageSize({
138
170
  storageKey: 'pagination:global:pageSize',
139
- defaultValue: 10,
140
- allowedValues: [6, 12, 24, 48],
171
+ defaultValue: 12,
172
+ allowedValues: pageSizeOptions,
141
173
  });
142
174
  const [searchQuery, setSearchQuery] = useState('');
143
175
  const debouncedSearch = useDebounce(searchQuery, 400);
@@ -440,6 +472,7 @@ export default function AiAgentPage() {
440
472
  setPageSize(value);
441
473
  setPage(1);
442
474
  }}
475
+ pageSizeOptions={pageSizeOptions}
443
476
  />
444
477
 
445
478
  <Sheet
@@ -0,0 +1,160 @@
1
+ 'use client';
2
+
3
+ import {
4
+ InputGroup,
5
+ InputGroupAddon,
6
+ InputGroupInput,
7
+ } from '@/components/ui/input-group';
8
+ import {
9
+ Select,
10
+ SelectContent,
11
+ SelectItem,
12
+ SelectTrigger,
13
+ SelectValue,
14
+ } from '@/components/ui/select';
15
+ import { cn } from '@/lib/utils';
16
+ import { useTranslations } from 'next-intl';
17
+ import { useState } from 'react';
18
+
19
+ type ByteUnit = 'bytes' | 'kb' | 'mb' | 'gb' | 'tb';
20
+
21
+ // Base binária (1024) — consistente com os valores existentes (ex.: 10485760 = 10 MB).
22
+ const UNIT_BYTES: Record<ByteUnit, number> = {
23
+ bytes: 1,
24
+ kb: 1024,
25
+ mb: 1024 ** 2,
26
+ gb: 1024 ** 3,
27
+ tb: 1024 ** 4,
28
+ };
29
+
30
+ // Da maior para a menor unidade — usado para escolher a melhor unidade de exibição.
31
+ const UNIT_ORDER: ByteUnit[] = ['tb', 'gb', 'mb', 'kb', 'bytes'];
32
+
33
+ const STORAGE_PREFIX = 'byte-unit:';
34
+
35
+ /** Maior unidade que divide o total exatamente (default: bytes). */
36
+ function pickBestUnit(totalBytes: number): ByteUnit {
37
+ if (totalBytes > 0) {
38
+ for (const unit of UNIT_ORDER) {
39
+ if (totalBytes % UNIT_BYTES[unit] === 0) {
40
+ return unit;
41
+ }
42
+ }
43
+ }
44
+ return 'bytes';
45
+ }
46
+
47
+ function readSavedUnit(persistKey?: string): ByteUnit | null {
48
+ if (!persistKey || typeof window === 'undefined') {
49
+ return null;
50
+ }
51
+ const saved = window.localStorage.getItem(STORAGE_PREFIX + persistKey);
52
+ return saved && saved in UNIT_BYTES ? (saved as ByteUnit) : null;
53
+ }
54
+
55
+ function toDisplay(totalBytes: number, unit: ByteUnit): string {
56
+ if (!totalBytes) {
57
+ return '0';
58
+ }
59
+ const value = totalBytes / UNIT_BYTES[unit];
60
+ // Evita ruído de ponto flutuante (ex.: 0.30000000000000004).
61
+ return String(Math.round(value * 10000) / 10000);
62
+ }
63
+
64
+ export interface ByteSizeInputProps {
65
+ /** Valor total em bytes (unidade canônica armazenada). */
66
+ value: number;
67
+ /** Chamado com o novo total em bytes. */
68
+ onChange: (totalBytes: number) => void;
69
+ /** Slug do setting — usado para lembrar a unidade escolhida no localStorage. */
70
+ persistKey?: string;
71
+ disabled?: boolean;
72
+ className?: string;
73
+ }
74
+
75
+ /**
76
+ * Input numérico com seletor de unidade (Bytes/KB/MB/GB/TB, base binária 1024). O valor é
77
+ * sempre persistido em **bytes**; o usuário escolhe apenas como visualizar/editar e a conversão
78
+ * de volta para bytes acontece a cada alteração. A unidade inicial é a maior que divide o valor
79
+ * exatamente (ou a última escolhida pelo usuário, se houver `persistKey`).
80
+ *
81
+ * Irmão do `DurationInput` (segundos → min/horas/dias).
82
+ */
83
+ export function ByteSizeInput({
84
+ value,
85
+ onChange,
86
+ persistKey,
87
+ disabled,
88
+ className,
89
+ }: ByteSizeInputProps) {
90
+ const t = useTranslations('core.Configurations');
91
+ const total = Math.max(0, Math.floor(Number(value) || 0));
92
+
93
+ const [unit, setUnit] = useState<ByteUnit>(
94
+ () => readSavedUnit(persistKey) ?? pickBestUnit(total)
95
+ );
96
+ // Texto digitado em andamento. Quando `null`, a exibição é derivada de `value` + `unit`
97
+ // (mantém-se sincronizado com mudanças externas sem precisar de efeito/setState).
98
+ const [editValue, setEditValue] = useState<string | null>(null);
99
+
100
+ const display = editValue ?? toDisplay(total, unit);
101
+
102
+ const handleInputChange = (raw: string) => {
103
+ setEditValue(raw);
104
+ const parsed = parseFloat(raw);
105
+ const bytes = Number.isFinite(parsed)
106
+ ? Math.max(0, Math.round(parsed * UNIT_BYTES[unit]))
107
+ : 0;
108
+ onChange(bytes);
109
+ };
110
+
111
+ const handleBlur = () => {
112
+ // Volta a exibir o valor canônico (reformatado) na unidade atual.
113
+ setEditValue(null);
114
+ };
115
+
116
+ const handleUnitChange = (nextUnit: ByteUnit) => {
117
+ // Troca de unidade é só exibição: mantém os bytes constantes, não dispara onChange.
118
+ setUnit(nextUnit);
119
+ setEditValue(null);
120
+ if (persistKey && typeof window !== 'undefined') {
121
+ window.localStorage.setItem(STORAGE_PREFIX + persistKey, nextUnit);
122
+ }
123
+ };
124
+
125
+ return (
126
+ <InputGroup className={cn('bg-background', className)}>
127
+ <InputGroupInput
128
+ type="number"
129
+ inputMode="decimal"
130
+ min={0}
131
+ step="any"
132
+ disabled={disabled}
133
+ value={display}
134
+ onChange={(e) => handleInputChange(e.target.value)}
135
+ onBlur={handleBlur}
136
+ />
137
+ <InputGroupAddon align="inline-end" className="pr-1">
138
+ <Select
139
+ value={unit}
140
+ onValueChange={(next) => handleUnitChange(next as ByteUnit)}
141
+ disabled={disabled}
142
+ >
143
+ <SelectTrigger
144
+ size="sm"
145
+ className="h-7 gap-1 border-0 bg-transparent px-2 shadow-none focus-visible:ring-0"
146
+ >
147
+ <SelectValue />
148
+ </SelectTrigger>
149
+ <SelectContent>
150
+ <SelectItem value="bytes">{t('byteUnitBytes')}</SelectItem>
151
+ <SelectItem value="kb">{t('byteUnitKB')}</SelectItem>
152
+ <SelectItem value="mb">{t('byteUnitMB')}</SelectItem>
153
+ <SelectItem value="gb">{t('byteUnitGB')}</SelectItem>
154
+ <SelectItem value="tb">{t('byteUnitTB')}</SelectItem>
155
+ </SelectContent>
156
+ </Select>
157
+ </InputGroupAddon>
158
+ </InputGroup>
159
+ );
160
+ }
@@ -0,0 +1,157 @@
1
+ 'use client';
2
+
3
+ import {
4
+ InputGroup,
5
+ InputGroupAddon,
6
+ InputGroupInput,
7
+ } from '@/components/ui/input-group';
8
+ import {
9
+ Select,
10
+ SelectContent,
11
+ SelectItem,
12
+ SelectTrigger,
13
+ SelectValue,
14
+ } from '@/components/ui/select';
15
+ import { cn } from '@/lib/utils';
16
+ import { useTranslations } from 'next-intl';
17
+ import { useState } from 'react';
18
+
19
+ type DurationUnit = 'seconds' | 'minutes' | 'hours' | 'days';
20
+
21
+ const UNIT_SECONDS: Record<DurationUnit, number> = {
22
+ seconds: 1,
23
+ minutes: 60,
24
+ hours: 3600,
25
+ days: 86400,
26
+ };
27
+
28
+ // Da maior para a menor unidade — usado para escolher a melhor unidade de exibição.
29
+ const UNIT_ORDER: DurationUnit[] = ['days', 'hours', 'minutes', 'seconds'];
30
+
31
+ const STORAGE_PREFIX = 'duration-unit:';
32
+
33
+ /** Maior unidade que divide o total exatamente (default: segundos). */
34
+ function pickBestUnit(totalSeconds: number): DurationUnit {
35
+ if (totalSeconds > 0) {
36
+ for (const unit of UNIT_ORDER) {
37
+ if (totalSeconds % UNIT_SECONDS[unit] === 0) {
38
+ return unit;
39
+ }
40
+ }
41
+ }
42
+ return 'seconds';
43
+ }
44
+
45
+ function readSavedUnit(persistKey?: string): DurationUnit | null {
46
+ if (!persistKey || typeof window === 'undefined') {
47
+ return null;
48
+ }
49
+ const saved = window.localStorage.getItem(STORAGE_PREFIX + persistKey);
50
+ return saved && saved in UNIT_SECONDS ? (saved as DurationUnit) : null;
51
+ }
52
+
53
+ function toDisplay(totalSeconds: number, unit: DurationUnit): string {
54
+ if (!totalSeconds) {
55
+ return '0';
56
+ }
57
+ const value = totalSeconds / UNIT_SECONDS[unit];
58
+ // Evita ruído de ponto flutuante (ex.: 0.30000000000000004).
59
+ return String(Math.round(value * 10000) / 10000);
60
+ }
61
+
62
+ export interface DurationInputProps {
63
+ /** Valor total em segundos (unidade canônica armazenada). */
64
+ value: number;
65
+ /** Chamado com o novo total em segundos. */
66
+ onChange: (totalSeconds: number) => void;
67
+ /** Slug do setting — usado para lembrar a unidade escolhida no localStorage. */
68
+ persistKey?: string;
69
+ disabled?: boolean;
70
+ className?: string;
71
+ }
72
+
73
+ /**
74
+ * Input numérico com seletor de unidade (segundos/minutos/horas/dias). O valor é sempre
75
+ * persistido em **segundos**; o usuário escolhe apenas como visualizar/editar e a conversão
76
+ * de volta para segundos acontece a cada alteração. A unidade inicial é a maior que divide
77
+ * o valor exatamente (ou a última escolhida pelo usuário, se houver `persistKey`).
78
+ *
79
+ * Não confundir com o `InputDuration` (campo mascarado mm:ss) usado na estrutura de cursos do LMS.
80
+ */
81
+ export function DurationInput({
82
+ value,
83
+ onChange,
84
+ persistKey,
85
+ disabled,
86
+ className,
87
+ }: DurationInputProps) {
88
+ const t = useTranslations('core.Configurations');
89
+ const total = Math.max(0, Math.floor(Number(value) || 0));
90
+
91
+ const [unit, setUnit] = useState<DurationUnit>(
92
+ () => readSavedUnit(persistKey) ?? pickBestUnit(total)
93
+ );
94
+ // Texto digitado em andamento. Quando `null`, a exibição é derivada de `value` + `unit`
95
+ // (mantém-se sincronizado com mudanças externas sem precisar de efeito/setState).
96
+ const [editValue, setEditValue] = useState<string | null>(null);
97
+
98
+ const display = editValue ?? toDisplay(total, unit);
99
+
100
+ const handleInputChange = (raw: string) => {
101
+ setEditValue(raw);
102
+ const parsed = parseFloat(raw);
103
+ const seconds = Number.isFinite(parsed)
104
+ ? Math.max(0, Math.round(parsed * UNIT_SECONDS[unit]))
105
+ : 0;
106
+ onChange(seconds);
107
+ };
108
+
109
+ const handleBlur = () => {
110
+ // Volta a exibir o valor canônico (reformatado) na unidade atual.
111
+ setEditValue(null);
112
+ };
113
+
114
+ const handleUnitChange = (nextUnit: DurationUnit) => {
115
+ // Troca de unidade é só exibição: mantém os segundos constantes, não dispara onChange.
116
+ setUnit(nextUnit);
117
+ setEditValue(null);
118
+ if (persistKey && typeof window !== 'undefined') {
119
+ window.localStorage.setItem(STORAGE_PREFIX + persistKey, nextUnit);
120
+ }
121
+ };
122
+
123
+ return (
124
+ <InputGroup className={cn('bg-background', className)}>
125
+ <InputGroupInput
126
+ type="number"
127
+ inputMode="decimal"
128
+ min={0}
129
+ step="any"
130
+ disabled={disabled}
131
+ value={display}
132
+ onChange={(e) => handleInputChange(e.target.value)}
133
+ onBlur={handleBlur}
134
+ />
135
+ <InputGroupAddon align="inline-end" className="pr-1">
136
+ <Select
137
+ value={unit}
138
+ onValueChange={(next) => handleUnitChange(next as DurationUnit)}
139
+ disabled={disabled}
140
+ >
141
+ <SelectTrigger
142
+ size="sm"
143
+ className="h-7 gap-1 border-0 bg-transparent px-2 shadow-none focus-visible:ring-0"
144
+ >
145
+ <SelectValue />
146
+ </SelectTrigger>
147
+ <SelectContent>
148
+ <SelectItem value="seconds">{t('durationUnitSeconds')}</SelectItem>
149
+ <SelectItem value="minutes">{t('durationUnitMinutes')}</SelectItem>
150
+ <SelectItem value="hours">{t('durationUnitHours')}</SelectItem>
151
+ <SelectItem value="days">{t('durationUnitDays')}</SelectItem>
152
+ </SelectContent>
153
+ </Select>
154
+ </InputGroupAddon>
155
+ </InputGroup>
156
+ );
157
+ }