@hed-hog/core 0.0.368 → 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 (271) 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_event_catalog.yaml +118 -118
  158. package/hedhog/data/integration_provider.yaml +9 -0
  159. package/hedhog/data/integration_type.yaml +6 -0
  160. package/hedhog/data/menu.yaml +6 -0
  161. package/hedhog/data/role.yaml +7 -0
  162. package/hedhog/data/route.yaml +192 -207
  163. package/hedhog/data/setting_group.yaml +119 -13
  164. package/hedhog/frontend/app/access-log/page.tsx.ejs +35 -3
  165. package/hedhog/frontend/app/account/components/active-sessions.tsx.ejs +36 -3
  166. package/hedhog/frontend/app/account/components/mcp-tokens.tsx.ejs +39 -4
  167. package/hedhog/frontend/app/account/components/tokens.tsx.ejs +39 -4
  168. package/hedhog/frontend/app/ai_agent/page.tsx.ejs +35 -2
  169. package/hedhog/frontend/app/configurations/[slug]/components/byte-size-input.tsx.ejs +160 -0
  170. package/hedhog/frontend/app/configurations/[slug]/components/duration-input.tsx.ejs +157 -0
  171. package/hedhog/frontend/app/configurations/[slug]/components/setting-field.tsx.ejs +244 -63
  172. package/hedhog/frontend/app/dashboard/[slug]/dashboard-content.tsx.ejs +33 -1
  173. package/hedhog/frontend/app/dashboard/management/tabs/component-roles-tab.tsx.ejs +35 -3
  174. package/hedhog/frontend/app/dashboard/management/tabs/components-tab.tsx.ejs +36 -4
  175. package/hedhog/frontend/app/dashboard/management/tabs/dashboard-roles-tab.tsx.ejs +35 -3
  176. package/hedhog/frontend/app/dashboard/management/tabs/dashboards-tab.tsx.ejs +36 -4
  177. package/hedhog/frontend/app/dashboard/management/tabs/items-tab.tsx.ejs +35 -3
  178. package/hedhog/frontend/app/integration/profiles/page.tsx.ejs +141 -29
  179. package/hedhog/frontend/app/integrations-webhooks/page.tsx.ejs +118 -11
  180. package/hedhog/frontend/app/mail/log/page.tsx.ejs +37 -4
  181. package/hedhog/frontend/app/menu/page.tsx.ejs +34 -2
  182. package/hedhog/frontend/app/preferences/page.tsx.ejs +77 -0
  183. package/hedhog/frontend/app/roles/menus.tsx.ejs +35 -3
  184. package/hedhog/frontend/app/roles/page.tsx.ejs +34 -2
  185. package/hedhog/frontend/app/roles/routes.tsx.ejs +36 -4
  186. package/hedhog/frontend/app/roles/users.tsx.ejs +36 -4
  187. package/hedhog/frontend/app/routes/page.tsx.ejs +34 -2
  188. package/hedhog/frontend/app/setting-list/page.tsx.ejs +34 -2
  189. package/hedhog/frontend/app/users/page.tsx.ejs +51 -21
  190. package/hedhog/frontend/messages/en.json +69 -4
  191. package/hedhog/frontend/messages/pt.json +69 -4
  192. package/hedhog/table/event_webhook.yaml +55 -55
  193. package/hedhog/table/event_webhook_delivery_log.yaml +66 -66
  194. package/hedhog/table/event_webhook_event.yaml +26 -26
  195. package/hedhog/table/integration_event_catalog.yaml +41 -41
  196. package/hedhog/table/mcp_conversation.yaml +12 -12
  197. package/hedhog/table/mcp_conversation_message.yaml +23 -23
  198. package/hedhog/table/mcp_tool_call_log.yaml +22 -22
  199. package/hedhog/table/route.yaml +33 -33
  200. package/hedhog/table/setting.yaml +2 -0
  201. package/hedhog/table/webhook_call_log.yaml +3 -0
  202. package/hedhog/table/webhook_integration.yaml +43 -43
  203. package/package.json +9 -4
  204. package/src/ai/ai.service.ts +9 -6
  205. package/src/app-command/app-command.module.ts +9 -0
  206. package/src/{webhook-integration/webhook-command.registry.ts → app-command/app-command.registry.ts} +9 -9
  207. package/src/auth/auth.controller.ts +20 -4
  208. package/src/auth/auth.service.ts +10 -36
  209. package/src/auth/dto/forget.dto.ts +5 -1
  210. package/src/auth/guards/auth.guard.ts +4 -1
  211. package/src/challenge/challenge.service.ts +30 -19
  212. package/src/file/dto/upload.dto.ts +25 -1
  213. package/src/file/file.controller.ts +31 -18
  214. package/src/file/file.service.ts +341 -16
  215. package/src/file/image-optimization.service.ts +111 -46
  216. package/src/file/provider/s3.provider.ts +16 -7
  217. package/src/file/provider/spaces.provider.ts +15 -5
  218. package/src/index.ts +4 -1
  219. package/src/integration/services/event-subscriber.registry.ts +15 -0
  220. package/src/integration/services/outbox.processor.ts +2 -5
  221. package/src/integration/services/outbox.service.ts +35 -17
  222. package/src/integration/types/event.types.ts +4 -4
  223. package/src/integration-profile/integration-credential-crypto.service.ts +146 -0
  224. package/src/integration-profile/integration-profile.controller.ts +2 -0
  225. package/src/integration-profile/integration-profile.module.ts +5 -2
  226. package/src/integration-profile/integration-profile.secrets.ts +51 -0
  227. package/src/integration-profile/integration-profile.service.ts +335 -54
  228. package/src/integration-profile/integration-profile.utils.ts +56 -0
  229. package/src/language/en.json +5 -1
  230. package/src/language/pt.json +5 -1
  231. package/src/mail/mail.controller.ts +5 -29
  232. package/src/mail/mail.module.ts +5 -5
  233. package/src/mail/mail.service.ts +37 -13
  234. package/src/mcp/decorators/mcp-tool.decorator.ts +7 -7
  235. package/src/mcp/guards/mcp-permission.guard.ts +51 -51
  236. package/src/mcp/interfaces/mcp-tool-definition.interface.ts +6 -6
  237. package/src/mcp/mcp-server.service.ts +87 -87
  238. package/src/mcp/types/mcp-context.type.ts +6 -6
  239. package/src/mcp-chat/dto/create-conversation.dto.ts +7 -7
  240. package/src/mcp-chat/dto/send-message.dto.ts +7 -7
  241. package/src/mcp-chat/mcp-chat.service.ts +5 -5
  242. package/src/notification/events/notification-stale-progress-check.event.ts +5 -5
  243. package/src/oauth/oauth-callback-coordinator.service.spec.ts +159 -0
  244. package/src/oauth/providers/facebook.provider.ts +3 -3
  245. package/src/oauth/providers/github.provider.ts +3 -3
  246. package/src/oauth/providers/google.provider.ts +3 -3
  247. package/src/oauth/providers/microsoft-entra-id.provider.ts +3 -3
  248. package/src/oauth/providers/microsoft.provider.ts +3 -3
  249. package/src/pdf/pdf.service.ts +19 -6
  250. package/src/profile/profile.controller.ts +21 -14
  251. package/src/profile/profile.service.ts +23 -20
  252. package/src/route/dto/create.dto.ts +32 -32
  253. package/src/route/dto/update.dto.ts +31 -31
  254. package/src/security/security.service.spec.ts +222 -0
  255. package/src/setting/setting.service.ts +15 -2
  256. package/src/token/token.service.ts +2 -1
  257. package/src/user/user.controller.ts +26 -16
  258. package/src/user/user.service.ts +25 -0
  259. package/src/utils/webauthn-origin.ts +38 -0
  260. package/src/webhook-integration/integration-event-catalog.controller.ts +50 -50
  261. package/src/webhook-integration/integration-event-catalog.service.ts +117 -117
  262. package/src/webhook-integration/webhook-integration.controller.ts +23 -0
  263. package/src/webhook-integration/webhook-integration.module.ts +3 -3
  264. package/src/webhook-integration/webhook-integration.service.ts +115 -5
  265. package/dist/webhook-integration/webhook-command.registry.d.ts +0 -22
  266. package/dist/webhook-integration/webhook-command.registry.d.ts.map +0 -1
  267. package/dist/webhook-integration/webhook-command.registry.js.map +0 -1
  268. package/src/menu/menu.service.spec.ts +0 -247
  269. package/src/route/route.service.spec.ts +0 -300
  270. package/src/screen/screen.service.spec.ts +0 -298
  271. package/src/user/user.service.spec.ts +0 -294
@@ -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
+ }