@hed-hog/core 0.0.355 → 0.0.358

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 (287) hide show
  1. package/dist/ai/ai.service.d.ts +3 -12
  2. package/dist/ai/ai.service.d.ts.map +1 -1
  3. package/dist/ai/ai.service.js +34 -125
  4. package/dist/ai/ai.service.js.map +1 -1
  5. package/dist/auth/guards/auth.guard.d.ts +6 -1
  6. package/dist/auth/guards/auth.guard.d.ts.map +1 -1
  7. package/dist/auth/guards/auth.guard.js +33 -2
  8. package/dist/auth/guards/auth.guard.js.map +1 -1
  9. package/dist/core.module.d.ts.map +1 -1
  10. package/dist/core.module.js +3 -3
  11. package/dist/core.module.js.map +1 -1
  12. package/dist/file/file.controller.d.ts +4 -2
  13. package/dist/file/file.controller.d.ts.map +1 -1
  14. package/dist/file/file.controller.js +44 -38
  15. package/dist/file/file.controller.js.map +1 -1
  16. package/dist/file/file.service.d.ts +2 -2
  17. package/dist/file/file.service.d.ts.map +1 -1
  18. package/dist/file/file.service.js +58 -89
  19. package/dist/file/file.service.js.map +1 -1
  20. package/dist/file/provider/azure.provider.d.ts.map +1 -1
  21. package/dist/file/provider/azure.provider.js +10 -15
  22. package/dist/file/provider/azure.provider.js.map +1 -1
  23. package/dist/file/provider/gcs.provider.d.ts.map +1 -1
  24. package/dist/file/provider/gcs.provider.js +8 -9
  25. package/dist/file/provider/gcs.provider.js.map +1 -1
  26. package/dist/file/provider/local.provider.d.ts.map +1 -1
  27. package/dist/file/provider/local.provider.js +11 -10
  28. package/dist/file/provider/local.provider.js.map +1 -1
  29. package/dist/file/provider/provider.enum.d.ts +2 -1
  30. package/dist/file/provider/provider.enum.d.ts.map +1 -1
  31. package/dist/file/provider/provider.enum.js +1 -0
  32. package/dist/file/provider/provider.enum.js.map +1 -1
  33. package/dist/file/provider/provider.factory.d.ts +1 -1
  34. package/dist/file/provider/provider.factory.d.ts.map +1 -1
  35. package/dist/file/provider/provider.factory.js +18 -5
  36. package/dist/file/provider/provider.factory.js.map +1 -1
  37. package/dist/file/provider/s3.provider.d.ts.map +1 -1
  38. package/dist/file/provider/s3.provider.js +4 -20
  39. package/dist/file/provider/s3.provider.js.map +1 -1
  40. package/dist/file/provider/spaces.provider.d.ts +27 -0
  41. package/dist/file/provider/spaces.provider.d.ts.map +1 -0
  42. package/dist/file/provider/spaces.provider.js +203 -0
  43. package/dist/file/provider/spaces.provider.js.map +1 -0
  44. package/dist/index.d.ts +4 -3
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +4 -3
  47. package/dist/index.js.map +1 -1
  48. package/dist/integration-profile/dto/create.dto.d.ts +9 -0
  49. package/dist/integration-profile/dto/create.dto.d.ts.map +1 -0
  50. package/dist/integration-profile/dto/create.dto.js +54 -0
  51. package/dist/integration-profile/dto/create.dto.js.map +1 -0
  52. package/dist/integration-profile/dto/export.dto.d.ts +5 -0
  53. package/dist/integration-profile/dto/export.dto.d.ts.map +1 -0
  54. package/dist/integration-profile/dto/export.dto.js +29 -0
  55. package/dist/integration-profile/dto/export.dto.js.map +1 -0
  56. package/dist/integration-profile/dto/import-confirm.dto.d.ts +17 -0
  57. package/dist/integration-profile/dto/import-confirm.dto.d.ts.map +1 -0
  58. package/dist/integration-profile/dto/import-confirm.dto.js +77 -0
  59. package/dist/integration-profile/dto/import-confirm.dto.js.map +1 -0
  60. package/dist/integration-profile/dto/update.dto.d.ts.map +1 -0
  61. package/dist/integration-profile/dto/update.dto.js.map +1 -0
  62. package/dist/integration-profile/integration-profile.controller.d.ts +142 -0
  63. package/dist/integration-profile/integration-profile.controller.d.ts.map +1 -0
  64. package/dist/integration-profile/integration-profile.controller.js +166 -0
  65. package/dist/integration-profile/integration-profile.controller.js.map +1 -0
  66. package/dist/integration-profile/integration-profile.module.d.ts +3 -0
  67. package/dist/integration-profile/integration-profile.module.d.ts.map +1 -0
  68. package/dist/{mail-profile/mail-profile.module.js → integration-profile/integration-profile.module.js} +11 -13
  69. package/dist/integration-profile/integration-profile.module.js.map +1 -0
  70. package/dist/integration-profile/integration-profile.service.d.ts +160 -0
  71. package/dist/integration-profile/integration-profile.service.d.ts.map +1 -0
  72. package/dist/integration-profile/integration-profile.service.js +609 -0
  73. package/dist/integration-profile/integration-profile.service.js.map +1 -0
  74. package/dist/integration-profile/integration-profile.utils.d.ts +30 -0
  75. package/dist/integration-profile/integration-profile.utils.d.ts.map +1 -0
  76. package/dist/integration-profile/integration-profile.utils.js +156 -0
  77. package/dist/integration-profile/integration-profile.utils.js.map +1 -0
  78. package/dist/mail/mail.module.d.ts.map +1 -1
  79. package/dist/mail/mail.module.js +36 -23
  80. package/dist/mail/mail.module.js.map +1 -1
  81. package/dist/mail/mail.service.d.ts +4 -2
  82. package/dist/mail/mail.service.d.ts.map +1 -1
  83. package/dist/mail/mail.service.js +33 -51
  84. package/dist/mail/mail.service.js.map +1 -1
  85. package/dist/mcp/guards/mcp-auth.guard.js +1 -1
  86. package/dist/mcp/guards/mcp-auth.guard.js.map +1 -1
  87. package/dist/mcp/mcp-api-key.controller.d.ts +2 -0
  88. package/dist/mcp/mcp-api-key.controller.d.ts.map +1 -1
  89. package/dist/mcp/mcp-api-key.controller.js +1 -1
  90. package/dist/mcp/mcp-api-key.controller.js.map +1 -1
  91. package/dist/mcp/mcp-api-key.service.d.ts +2 -1
  92. package/dist/mcp/mcp-api-key.service.d.ts.map +1 -1
  93. package/dist/mcp/mcp-api-key.service.js +6 -2
  94. package/dist/mcp/mcp-api-key.service.js.map +1 -1
  95. package/dist/mcp/mcp.module.d.ts.map +1 -1
  96. package/dist/mcp/mcp.module.js +3 -5
  97. package/dist/mcp/mcp.module.js.map +1 -1
  98. package/dist/mcp-chat/mcp-chat.service.d.ts.map +1 -1
  99. package/dist/mcp-chat/mcp-chat.service.js +17 -16
  100. package/dist/mcp-chat/mcp-chat.service.js.map +1 -1
  101. package/dist/oauth/interfaces/OAuthProvider.d.ts +1 -1
  102. package/dist/oauth/interfaces/OAuthProvider.d.ts.map +1 -1
  103. package/dist/oauth/oauth.controller.d.ts +4 -1
  104. package/dist/oauth/oauth.controller.d.ts.map +1 -1
  105. package/dist/oauth/oauth.controller.js +31 -5
  106. package/dist/oauth/oauth.controller.js.map +1 -1
  107. package/dist/oauth/oauth.service.d.ts +12 -1
  108. package/dist/oauth/oauth.service.d.ts.map +1 -1
  109. package/dist/oauth/oauth.service.js +125 -23
  110. package/dist/oauth/oauth.service.js.map +1 -1
  111. package/dist/oauth/providers/abstract.provider.d.ts +1 -1
  112. package/dist/oauth/providers/abstract.provider.d.ts.map +1 -1
  113. package/dist/oauth/providers/facebook.provider.d.ts +5 -2
  114. package/dist/oauth/providers/facebook.provider.d.ts.map +1 -1
  115. package/dist/oauth/providers/facebook.provider.js +34 -19
  116. package/dist/oauth/providers/facebook.provider.js.map +1 -1
  117. package/dist/oauth/providers/github.provider.d.ts +6 -2
  118. package/dist/oauth/providers/github.provider.d.ts.map +1 -1
  119. package/dist/oauth/providers/github.provider.js +43 -20
  120. package/dist/oauth/providers/github.provider.js.map +1 -1
  121. package/dist/oauth/providers/google.provider.d.ts +5 -2
  122. package/dist/oauth/providers/google.provider.d.ts.map +1 -1
  123. package/dist/oauth/providers/google.provider.js +34 -19
  124. package/dist/oauth/providers/google.provider.js.map +1 -1
  125. package/dist/oauth/providers/microsoft-entra-id.provider.d.ts +5 -2
  126. package/dist/oauth/providers/microsoft-entra-id.provider.d.ts.map +1 -1
  127. package/dist/oauth/providers/microsoft-entra-id.provider.js +43 -40
  128. package/dist/oauth/providers/microsoft-entra-id.provider.js.map +1 -1
  129. package/dist/oauth/providers/microsoft.provider.d.ts +5 -2
  130. package/dist/oauth/providers/microsoft.provider.d.ts.map +1 -1
  131. package/dist/oauth/providers/microsoft.provider.js +34 -19
  132. package/dist/oauth/providers/microsoft.provider.js.map +1 -1
  133. package/dist/profile/profile.controller.js +1 -1
  134. package/dist/profile/profile.controller.js.map +1 -1
  135. package/dist/security/security.service.d.ts +2 -0
  136. package/dist/security/security.service.d.ts.map +1 -1
  137. package/dist/security/security.service.js +21 -0
  138. package/dist/security/security.service.js.map +1 -1
  139. package/dist/setting/setting.service.d.ts +26 -1
  140. package/dist/setting/setting.service.d.ts.map +1 -1
  141. package/dist/setting/setting.service.js +240 -7
  142. package/dist/setting/setting.service.js.map +1 -1
  143. package/dist/user/user.controller.d.ts +2 -1
  144. package/dist/user/user.controller.d.ts.map +1 -1
  145. package/dist/user/user.controller.js +5 -4
  146. package/dist/user/user.controller.js.map +1 -1
  147. package/dist/user/user.service.d.ts +4 -2
  148. package/dist/user/user.service.d.ts.map +1 -1
  149. package/dist/user/user.service.js +19 -4
  150. package/dist/user/user.service.js.map +1 -1
  151. package/dist/webhook-integration/dto/webhook-action.dto.d.ts +17 -2
  152. package/dist/webhook-integration/dto/webhook-action.dto.d.ts.map +1 -1
  153. package/dist/webhook-integration/dto/webhook-action.dto.js +93 -2
  154. package/dist/webhook-integration/dto/webhook-action.dto.js.map +1 -1
  155. package/dist/webhook-integration/event-webhook.controller.d.ts +1 -0
  156. package/dist/webhook-integration/event-webhook.controller.d.ts.map +1 -1
  157. package/dist/webhook-integration/event-webhook.controller.js +10 -0
  158. package/dist/webhook-integration/event-webhook.controller.js.map +1 -1
  159. package/dist/webhook-integration/event-webhook.service.d.ts +2 -0
  160. package/dist/webhook-integration/event-webhook.service.d.ts.map +1 -1
  161. package/dist/webhook-integration/event-webhook.service.js +30 -0
  162. package/dist/webhook-integration/event-webhook.service.js.map +1 -1
  163. package/dist/webhook-integration/webhook-command.registry.d.ts +15 -0
  164. package/dist/webhook-integration/webhook-command.registry.d.ts.map +1 -0
  165. package/dist/webhook-integration/webhook-command.registry.js +36 -0
  166. package/dist/webhook-integration/webhook-command.registry.js.map +1 -0
  167. package/dist/webhook-integration/webhook-integration.controller.d.ts +20 -0
  168. package/dist/webhook-integration/webhook-integration.controller.d.ts.map +1 -1
  169. package/dist/webhook-integration/webhook-integration.controller.js +48 -0
  170. package/dist/webhook-integration/webhook-integration.controller.js.map +1 -1
  171. package/dist/webhook-integration/webhook-integration.module.d.ts.map +1 -1
  172. package/dist/webhook-integration/webhook-integration.module.js +5 -0
  173. package/dist/webhook-integration/webhook-integration.module.js.map +1 -1
  174. package/dist/webhook-integration/webhook-integration.service.d.ts +27 -2
  175. package/dist/webhook-integration/webhook-integration.service.d.ts.map +1 -1
  176. package/dist/webhook-integration/webhook-integration.service.js +377 -50
  177. package/dist/webhook-integration/webhook-integration.service.js.map +1 -1
  178. package/hedhog/data/file_provider.yaml +9 -1
  179. package/hedhog/data/integration_provider.yaml +148 -0
  180. package/hedhog/data/integration_type.yaml +29 -0
  181. package/hedhog/data/menu.yaml +6 -21
  182. package/hedhog/data/role.yaml +8 -0
  183. package/hedhog/data/route.yaml +73 -373
  184. package/hedhog/data/setting_group.yaml +209 -365
  185. package/hedhog/data/setting_subgroup.yaml +21 -13
  186. package/hedhog/frontend/app/account/components/connected-accounts.tsx.ejs +25 -20
  187. package/hedhog/frontend/app/account/components/tokens.tsx.ejs +421 -0
  188. package/hedhog/frontend/app/account/layout.tsx.ejs +6 -3
  189. package/hedhog/frontend/app/account/mcp-tokens/page.tsx.ejs +2 -2
  190. package/hedhog/frontend/app/account/tokens/page.tsx.ejs +5 -0
  191. package/hedhog/frontend/app/configurations/[slug]/components/setting-field.tsx.ejs +938 -225
  192. package/hedhog/frontend/app/configurations/layout.tsx.ejs +1 -1
  193. package/hedhog/frontend/app/dashboard/dashboard-home-tabs.tsx.ejs +3 -1
  194. package/hedhog/frontend/app/integration/profiles/page.tsx.ejs +1830 -0
  195. package/hedhog/frontend/app/integrations-webhooks/page.tsx.ejs +829 -128
  196. package/hedhog/frontend/app/mail/profiles/page.tsx.ejs +1 -820
  197. package/hedhog/frontend/app/setting-list/page.tsx.ejs +229 -395
  198. package/hedhog/frontend/app/users/permissions.tsx.ejs +79 -2
  199. package/hedhog/frontend/app/users/user-sheet.tsx.ejs +7 -2
  200. package/hedhog/frontend/messages/en.json +239 -3
  201. package/hedhog/frontend/messages/pt.json +239 -3
  202. package/hedhog/table/file.yaml +1 -1
  203. package/hedhog/table/integration_profile.yaml +34 -0
  204. package/hedhog/table/integration_provider.yaml +23 -0
  205. package/hedhog/table/integration_type.yaml +19 -0
  206. package/hedhog/table/mcp_api_key.yaml +4 -0
  207. package/hedhog/table/webhook_action.yaml +141 -85
  208. package/package.json +4 -3
  209. package/src/ai/ai.service.ts +35 -153
  210. package/src/auth/guards/auth.guard.ts +32 -0
  211. package/src/core.module.ts +3 -3
  212. package/src/file/file.controller.ts +51 -42
  213. package/src/file/file.service.ts +67 -104
  214. package/src/file/provider/azure.provider.ts +14 -28
  215. package/src/file/provider/gcs.provider.ts +9 -9
  216. package/src/file/provider/local.provider.ts +10 -13
  217. package/src/file/provider/provider.enum.ts +1 -0
  218. package/src/file/provider/provider.factory.ts +21 -6
  219. package/src/file/provider/s3.provider.ts +4 -20
  220. package/src/file/provider/spaces.provider.ts +246 -0
  221. package/src/index.ts +6 -3
  222. package/src/integration-profile/dto/create.dto.ts +44 -0
  223. package/src/integration-profile/dto/export.dto.ts +15 -0
  224. package/src/integration-profile/dto/import-confirm.dto.ts +61 -0
  225. package/src/integration-profile/integration-profile.controller.ts +131 -0
  226. package/src/integration-profile/integration-profile.module.ts +16 -0
  227. package/src/integration-profile/integration-profile.service.ts +793 -0
  228. package/src/integration-profile/integration-profile.utils.ts +173 -0
  229. package/src/mail/mail.module.ts +41 -22
  230. package/src/mail/mail.service.ts +40 -64
  231. package/src/mcp/guards/mcp-auth.guard.ts +1 -1
  232. package/src/mcp/mcp-api-key.controller.ts +2 -2
  233. package/src/mcp/mcp-api-key.service.ts +6 -2
  234. package/src/mcp/mcp.module.ts +3 -5
  235. package/src/mcp-chat/mcp-chat.service.ts +23 -20
  236. package/src/oauth/interfaces/OAuthProvider.ts +1 -1
  237. package/src/oauth/oauth.controller.ts +30 -2
  238. package/src/oauth/oauth.service.ts +194 -35
  239. package/src/oauth/providers/abstract.provider.ts +1 -1
  240. package/src/oauth/providers/facebook.provider.ts +36 -20
  241. package/src/oauth/providers/github.provider.ts +46 -23
  242. package/src/oauth/providers/google.provider.ts +36 -20
  243. package/src/oauth/providers/microsoft-entra-id.provider.ts +42 -46
  244. package/src/oauth/providers/microsoft.provider.ts +36 -21
  245. package/src/profile/profile.controller.ts +2 -2
  246. package/src/security/security.service.ts +34 -1
  247. package/src/setting/setting.service.ts +307 -9
  248. package/src/user/user.controller.ts +4 -1
  249. package/src/user/user.service.ts +20 -2
  250. package/src/webhook-integration/dto/webhook-action.dto.ts +85 -4
  251. package/src/webhook-integration/event-webhook.controller.ts +55 -50
  252. package/src/webhook-integration/event-webhook.service.ts +422 -384
  253. package/src/webhook-integration/webhook-command.registry.ts +42 -0
  254. package/src/webhook-integration/webhook-integration.controller.ts +25 -0
  255. package/src/webhook-integration/webhook-integration.module.ts +50 -45
  256. package/src/webhook-integration/webhook-integration.service.ts +444 -53
  257. package/dist/mail-profile/dto/create.dto.d.ts +0 -23
  258. package/dist/mail-profile/dto/create.dto.d.ts.map +0 -1
  259. package/dist/mail-profile/dto/create.dto.js +0 -139
  260. package/dist/mail-profile/dto/create.dto.js.map +0 -1
  261. package/dist/mail-profile/dto/update.dto.d.ts.map +0 -1
  262. package/dist/mail-profile/dto/update.dto.js.map +0 -1
  263. package/dist/mail-profile/mail-profile.controller.d.ts +0 -94
  264. package/dist/mail-profile/mail-profile.controller.d.ts.map +0 -1
  265. package/dist/mail-profile/mail-profile.controller.js +0 -95
  266. package/dist/mail-profile/mail-profile.controller.js.map +0 -1
  267. package/dist/mail-profile/mail-profile.module.d.ts +0 -3
  268. package/dist/mail-profile/mail-profile.module.d.ts.map +0 -1
  269. package/dist/mail-profile/mail-profile.module.js.map +0 -1
  270. package/dist/mail-profile/mail-profile.service.d.ts +0 -109
  271. package/dist/mail-profile/mail-profile.service.d.ts.map +0 -1
  272. package/dist/mail-profile/mail-profile.service.js +0 -278
  273. package/dist/mail-profile/mail-profile.service.js.map +0 -1
  274. package/dist/mcp/tools/core-mail-profile.mcp-tools.d.ts +0 -32
  275. package/dist/mcp/tools/core-mail-profile.mcp-tools.d.ts.map +0 -1
  276. package/dist/mcp/tools/core-mail-profile.mcp-tools.js +0 -178
  277. package/dist/mcp/tools/core-mail-profile.mcp-tools.js.map +0 -1
  278. package/hedhog/data/mail_profile.yaml +0 -18
  279. package/hedhog/table/mail_profile.yaml +0 -75
  280. package/src/mail-profile/dto/create.dto.ts +0 -145
  281. package/src/mail-profile/mail-profile.controller.ts +0 -56
  282. package/src/mail-profile/mail-profile.module.ts +0 -18
  283. package/src/mail-profile/mail-profile.service.ts +0 -308
  284. package/src/mcp/tools/core-mail-profile.mcp-tools.ts +0 -128
  285. /package/dist/{mail-profile → integration-profile}/dto/update.dto.d.ts +0 -0
  286. /package/dist/{mail-profile → integration-profile}/dto/update.dto.js +0 -0
  287. /package/src/{mail-profile → integration-profile}/dto/update.dto.ts +0 -0
@@ -1,3 +1,7 @@
1
+ import {
2
+ IntegrationProfileSheet,
3
+ type IntegrationProfileSheetSavedProfile,
4
+ } from '@/components/integration-profile/integration-profile-sheet';
1
5
  import { Button } from '@/components/ui/button';
2
6
  import { Checkbox } from '@/components/ui/checkbox';
3
7
  import { EntityPicker } from '@/components/ui/entity-picker';
@@ -20,24 +24,190 @@ import {
20
24
  SettingTypeEnum,
21
25
  } from '@hed-hog/api-types';
22
26
  import { useApp } from '@hed-hog/next-app-provider';
23
- import { Eye, EyeOff, Upload } from 'lucide-react';
27
+ import { Eye, EyeOff, Pencil, Plus, Upload } from 'lucide-react';
24
28
  import { useTranslations } from 'next-intl';
25
29
  import { useEffect, useRef, useState } from 'react';
26
30
 
27
- type SettingFieldProps = {
28
- setting: Setting;
29
- className?: string;
30
- };
31
-
32
- export const SettingField = ({ setting, className }: SettingFieldProps) => {
31
+ type SettingFieldProps = {
32
+ setting: Setting;
33
+ className?: string;
34
+ };
35
+
36
+ type ApiPaginated<T> = {
37
+ data: T[];
38
+ page: number;
39
+ lastPage: number;
40
+ };
41
+
42
+ type IntegrationTypeOption = {
43
+ id: number;
44
+ slug: string;
45
+ };
46
+
47
+ type IntegrationProviderOption = {
48
+ id: number;
49
+ slug: string;
50
+ type_id: number;
51
+ integration_provider_locale?: {
52
+ name: string;
53
+ locale: { code: string };
54
+ }[];
55
+ };
56
+
57
+ type IntegrationProfileOption = {
58
+ id: number;
59
+ slug: string;
60
+ name: string;
61
+ integration_provider?: {
62
+ slug: string;
63
+ integration_provider_locale?: {
64
+ name: string;
65
+ locale: { code: string };
66
+ }[];
67
+ };
68
+ };
69
+
70
+ function getLocalizedIntegrationName(
71
+ locales:
72
+ | {
73
+ name: string;
74
+ locale: { code: string };
75
+ }[]
76
+ | undefined,
77
+ localeCode: string
78
+ ) {
79
+ if (!locales?.length) {
80
+ return '';
81
+ }
82
+
83
+ return (
84
+ locales.find((locale) => locale.locale.code === localeCode)?.name ??
85
+ locales[0]?.name ??
86
+ ''
87
+ );
88
+ }
89
+
90
+ const DAILY_REPORT_EMAIL_INTEGRATION_SETTING_SLUG =
91
+ 'operations.daily-report.email-integration-profile-id';
92
+ const DAILY_REPORT_WHATSAPP_INTEGRATION_SETTING_SLUG =
93
+ 'operations.daily-report.whatsapp-integration-profile-id';
94
+ const AI_PROFILE_SETTING_SLUGS = [
95
+ 'ai-gemini-profile-id',
96
+ 'ai-openai-profile-id',
97
+ 'mcp-ai-profile-id',
98
+ ];
99
+ const OAUTH_PROFILE_SLUG_TO_PROVIDER: Record<string, string> = {
100
+ 'oauth-google-profile-id': 'google-oauth',
101
+ 'oauth-facebook-profile-id': 'facebook-oauth',
102
+ 'oauth-github-profile-id': 'github-oauth',
103
+ 'oauth-microsoft-profile-id': 'microsoft-oauth',
104
+ 'oauth-microsoft-entra-id-profile-id': 'microsoft-entra-id-oauth',
105
+ };
106
+ const ADMIN_LOGIN_OAUTH_SETTING_SLUG = 'admin-login-page-oauth-providers';
107
+ const ADMIN_LOGIN_PROVIDER_ENABLED_SLUG_BY_PROVIDER: Record<string, string> = {
108
+ facebook: 'oauth-facebook-enabled',
109
+ github: 'oauth-github-enabled',
110
+ google: 'oauth-google-enabled',
111
+ microsoft: 'oauth-microsoft-enabled',
112
+ 'microsoft-entra-id': 'oauth-microsoft-entra-id-enabled',
113
+ };
114
+ const INTEGRATION_PROFILE_PICKER_SLUGS = [
115
+ 'file-storage-profile-id',
116
+ ...AI_PROFILE_SETTING_SLUGS,
117
+ ...Object.keys(OAUTH_PROFILE_SLUG_TO_PROVIDER),
118
+ ];
119
+
120
+ function toSlug(value: string) {
121
+ return (value || 'profile')
122
+ .toLowerCase()
123
+ .normalize('NFD')
124
+ .replace(/[\u0300-\u036f]/g, '')
125
+ .replace(/[^a-z0-9]+/g, '-')
126
+ .replace(/^-+|-+$/g, '');
127
+ }
128
+
129
+ function getDailyReportIntegrationTypeSlug(settingSlug: string) {
130
+ if (settingSlug === DAILY_REPORT_EMAIL_INTEGRATION_SETTING_SLUG) {
131
+ return 'email';
132
+ }
133
+
134
+ if (settingSlug === DAILY_REPORT_WHATSAPP_INTEGRATION_SETTING_SLUG) {
135
+ return 'whatsapp';
136
+ }
137
+
138
+ return null;
139
+ }
140
+
141
+ function buildIntegrationProfileConfig(
142
+ providerSlug: string,
143
+ values: Record<string, string>
144
+ ) {
145
+ const trim = (key: string) => String(values[key] ?? '').trim();
146
+
147
+ if (providerSlug === 'smtp') {
148
+ return {
149
+ host: trim('host'),
150
+ port: Number(trim('port') || 587),
151
+ username: trim('username'),
152
+ password: trim('password'),
153
+ secure: values.secure === 'true',
154
+ from_email: trim('from_email'),
155
+ from_name: trim('from_name'),
156
+ reply_to_email: trim('reply_to_email'),
157
+ };
158
+ }
159
+
160
+ if (providerSlug === 'gmail') {
161
+ return {
162
+ client_id: trim('client_id'),
163
+ client_secret: trim('client_secret'),
164
+ refresh_token: trim('refresh_token'),
165
+ from_email: trim('from_email'),
166
+ from_name: trim('from_name'),
167
+ };
168
+ }
169
+
170
+ if (providerSlug === 'ses') {
171
+ return {
172
+ access_key_id: trim('access_key_id'),
173
+ secret_access_key: trim('secret_access_key'),
174
+ region: trim('region'),
175
+ from_email: trim('from_email'),
176
+ from_name: trim('from_name'),
177
+ };
178
+ }
179
+
180
+ return {
181
+ host: trim('host'),
182
+ token: trim('token'),
183
+ instance_name: trim('instance_name'),
184
+ };
185
+ }
186
+
187
+ export const SettingField = ({ setting, className }: SettingFieldProps) => {
33
188
  const t = useTranslations('core.Configurations');
34
- const { showToastHandler, setSettingValue, request } = useApp();
189
+ const {
190
+ showToastHandler,
191
+ setSettingValue,
192
+ request,
193
+ currentLocaleCode,
194
+ getSettingValue,
195
+ } = useApp();
35
196
  const component =
36
197
  setting.component?.replaceAll('_', '-') || SettingComponentEnum.INPUT_TEXT;
37
198
  const [localValue, setLocalValue] = useState(setting.value);
199
+ const [storageProfileSelectedLabel, setStorageProfileSelectedLabel] =
200
+ useState('');
201
+ const [isStorageProfileSheetOpen, setIsStorageProfileSheetOpen] =
202
+ useState(false);
203
+ const [storageProfileEditingId, setStorageProfileEditingId] = useState<
204
+ number | null
205
+ >(null);
38
206
  const [showSecretValue, setShowSecretValue] = useState(false);
207
+ const [isUploading, setIsUploading] = useState(false);
39
208
  const debounceTimerRef = useRef<NodeJS.Timeout | undefined>(undefined);
40
209
  const isTextInputRef = useRef<boolean>(false);
210
+ const fileInputRef = useRef<HTMLInputElement>(null);
41
211
 
42
212
  const isThemeSetting = (slug: string) => {
43
213
  return slug.startsWith('theme-') || slug === 'menu-width';
@@ -45,7 +215,7 @@ export const SettingField = ({ setting, className }: SettingFieldProps) => {
45
215
 
46
216
  const applyThemeSettings = async () => {
47
217
  try {
48
- const response: any = await request({
218
+ const response = await request<{ setting?: Record<string, unknown> }>({
49
219
  url: '/setting/initial',
50
220
  method: 'GET',
51
221
  });
@@ -67,11 +237,11 @@ export const SettingField = ({ setting, className }: SettingFieldProps) => {
67
237
 
68
238
  debounceTimerRef.current = setTimeout(() => {
69
239
  if (localValue !== setting.value) {
70
- setSettingValue(setting.slug, localValue)
71
- .then(async () => {
72
- showToastHandler('success', t('settingUpdated'));
73
-
74
- if (['icon-url'].includes(setting.slug)) {
240
+ setSettingValue(setting.slug, localValue)
241
+ .then(async () => {
242
+ showToastHandler('success', t('settingUpdated'));
243
+
244
+ if (['icon-url'].includes(setting.slug)) {
75
245
  await revalidateSystemIcon();
76
246
  showToastHandler('success', t('iconRevalidated'));
77
247
  window.location.reload();
@@ -94,13 +264,49 @@ export const SettingField = ({ setting, className }: SettingFieldProps) => {
94
264
  };
95
265
  }, [localValue]);
96
266
 
97
- const handleChangeValue = (newValue: any, immediate = false) => {
267
+ useEffect(() => {
268
+ if (!INTEGRATION_PROFILE_PICKER_SLUGS.includes(setting.slug)) {
269
+ setStorageProfileSelectedLabel('');
270
+ return;
271
+ }
272
+
273
+ const profileId = Number(localValue);
274
+ if (!profileId) {
275
+ setStorageProfileSelectedLabel('');
276
+ return;
277
+ }
278
+
279
+ let isCurrent = true;
280
+
281
+ request<IntegrationProfileOption>({
282
+ url: `/integration-profile/${profileId}`,
283
+ method: 'GET',
284
+ })
285
+ .then((response) => {
286
+ if (!isCurrent) {
287
+ return;
288
+ }
289
+
290
+ setStorageProfileSelectedLabel(response.data?.name ?? '');
291
+ })
292
+ .catch(() => {
293
+ if (isCurrent) {
294
+ setStorageProfileSelectedLabel('');
295
+ }
296
+ });
297
+
298
+ return () => {
299
+ isCurrent = false;
300
+ };
301
+ }, [localValue, request, setting.slug]);
302
+
303
+ const handleChangeValue = (newValue: string, immediate = false) => {
98
304
  setLocalValue(newValue);
99
305
  if (immediate) {
100
- setSettingValue(setting.slug, newValue)
101
- .then(async () => {
102
- showToastHandler('success', t('settingUpdated'));
103
- if (isThemeSetting(setting.slug)) {
306
+ setSettingValue(setting.slug, newValue)
307
+ .then(async () => {
308
+ showToastHandler('success', t('settingUpdated'));
309
+ if (isThemeSetting(setting.slug)) {
104
310
  await applyThemeSettings();
105
311
  }
106
312
  })
@@ -133,7 +339,7 @@ export const SettingField = ({ setting, className }: SettingFieldProps) => {
133
339
  }
134
340
  };
135
341
 
136
- const formatValue = (val: any) => {
342
+ const formatValue = (val: unknown): string => {
137
343
  switch (setting.type) {
138
344
  case SettingTypeEnum.ARRAY:
139
345
  case SettingTypeEnum.JSON:
@@ -143,7 +349,7 @@ export const SettingField = ({ setting, className }: SettingFieldProps) => {
143
349
  case SettingTypeEnum.NUMBER:
144
350
  return val?.toString() || '0';
145
351
  default:
146
- return val;
352
+ return String(val ?? '');
147
353
  }
148
354
  };
149
355
 
@@ -189,13 +395,11 @@ export const SettingField = ({ setting, className }: SettingFieldProps) => {
189
395
  <Button
190
396
  type="button"
191
397
  variant="ghost"
192
- size="icon"
193
- className="absolute right-1 top-1/2 h-8 w-8 -translate-y-1/2"
194
- onClick={() => setShowSecretValue((prev) => !prev)}
195
- aria-label={
196
- showSecretValue ? t('hidePassword') : t('showPassword')
197
- }
198
- >
398
+ size="icon"
399
+ className="absolute right-1 top-1/2 h-8 w-8 -translate-y-1/2"
400
+ onClick={() => setShowSecretValue((prev) => !prev)}
401
+ aria-label={showSecretValue ? t('hidePassword') : t('showPassword')}
402
+ >
199
403
  {showSecretValue ? (
200
404
  <EyeOff className="h-4 w-4" />
201
405
  ) : (
@@ -207,8 +411,6 @@ export const SettingField = ({ setting, className }: SettingFieldProps) => {
207
411
 
208
412
  case SettingComponentEnum.INPUT_FILE:
209
413
  isTextInputRef.current = true;
210
- const fileInputRef = useRef<HTMLInputElement>(null);
211
- const [isUploading, setIsUploading] = useState(false);
212
414
 
213
415
  const handleFileUpload = async (
214
416
  e: React.ChangeEvent<HTMLInputElement>
@@ -221,7 +423,7 @@ export const SettingField = ({ setting, className }: SettingFieldProps) => {
221
423
  const formData = new FormData();
222
424
  formData.append('file', file);
223
425
 
224
- const response: any = await request({
426
+ const response = await request<{ id?: number | string }>({
225
427
  url: '/file',
226
428
  method: 'POST',
227
429
  data: formData,
@@ -236,7 +438,7 @@ export const SettingField = ({ setting, className }: SettingFieldProps) => {
236
438
  throw new Error(t('fileIdNotReturned'));
237
439
  }
238
440
 
239
- const openResponse: any = await request({
441
+ const openResponse = await request<{ url?: string }>({
240
442
  url: `/file/open/${fileId}`,
241
443
  method: 'PUT',
242
444
  });
@@ -312,7 +514,7 @@ export const SettingField = ({ setting, className }: SettingFieldProps) => {
312
514
  onCheckedChange={(checked) =>
313
515
  handleChangeValue(formatValue(checked), true)
314
516
  }
315
- className={cn(className)}
517
+ className={cn('w-8! min-w-8 max-w-8', className)}
316
518
  />
317
519
  );
318
520
 
@@ -322,6 +524,9 @@ export const SettingField = ({ setting, className }: SettingFieldProps) => {
322
524
  const selectedValues: string[] = Array.isArray(parsedValue)
323
525
  ? parsedValue
324
526
  : [];
527
+ const isAdminLoginOAuthSetting =
528
+ setting.slug === ADMIN_LOGIN_OAUTH_SETTING_SLUG;
529
+
325
530
  return (
326
531
  <div
327
532
  className={cn(
@@ -329,26 +534,50 @@ export const SettingField = ({ setting, className }: SettingFieldProps) => {
329
534
  className
330
535
  )}
331
536
  >
332
- {options.map((option) => (
333
- <div key={option.id} className="flex items-center space-x-2">
334
- <Checkbox
335
- className="bg-background"
336
- checked={selectedValues.includes(option.value)}
337
- onCheckedChange={(checked) => {
338
- let newValues = [...selectedValues];
339
- if (checked) {
340
- newValues.push(option.value);
341
- } else {
342
- newValues = newValues.filter(
343
- (val) => val !== option.value
344
- );
345
- }
346
- handleChangeValue(formatValue(newValues), true);
347
- }}
348
- />
349
- <Label>{option.value}</Label>
350
- </div>
351
- ))}
537
+ {options.map((option) => {
538
+ const enabledSettingSlug =
539
+ ADMIN_LOGIN_PROVIDER_ENABLED_SLUG_BY_PROVIDER[option.value];
540
+ const isProviderGloballyEnabled = enabledSettingSlug
541
+ ? getSettingValue(enabledSettingSlug) === true
542
+ : true;
543
+ const isOptionDisabled =
544
+ isAdminLoginOAuthSetting && !isProviderGloballyEnabled;
545
+
546
+ return (
547
+ <div key={option.id} className="flex items-center space-x-2">
548
+ <Checkbox
549
+ className="bg-background"
550
+ checked={selectedValues.includes(option.value)}
551
+ disabled={isOptionDisabled}
552
+ onCheckedChange={(checked) => {
553
+ if (isOptionDisabled) {
554
+ return;
555
+ }
556
+
557
+ let newValues = [...selectedValues];
558
+ if (checked) {
559
+ newValues.push(option.value);
560
+ } else {
561
+ newValues = newValues.filter(
562
+ (val) => val !== option.value
563
+ );
564
+ }
565
+ handleChangeValue(formatValue(newValues), true);
566
+ }}
567
+ />
568
+ <Label className={cn(isOptionDisabled && 'opacity-60')}>
569
+ {option.value}
570
+ </Label>
571
+ </div>
572
+ );
573
+ })}
574
+ {isAdminLoginOAuthSetting && (
575
+ <p className="text-xs text-muted-foreground">
576
+ {currentLocaleCode.startsWith('pt')
577
+ ? 'Providers desabilitados globalmente (oauth-*-enabled = false) ficam indisponíveis nesta lista.'
578
+ : 'Providers disabled globally (oauth-*-enabled = false) are unavailable in this list.'}
579
+ </p>
580
+ )}
352
581
  </div>
353
582
  );
354
583
  }
@@ -434,7 +663,351 @@ export const SettingField = ({ setting, className }: SettingFieldProps) => {
434
663
  );
435
664
 
436
665
  case SettingComponentEnum.ENTITY_PICKER:
437
- if (setting.slug !== 'mail-profile-id') {
666
+ const dailyReportIntegrationTypeSlug = getDailyReportIntegrationTypeSlug(
667
+ setting.slug
668
+ );
669
+
670
+ if (setting.slug === 'file-storage-profile-id') {
671
+ const loadStorageTypeId = async () => {
672
+ const typesResponse = await request<IntegrationTypeOption[]>({
673
+ url: '/integration-profile/types',
674
+ method: 'GET',
675
+ });
676
+ const types = (typesResponse.data ?? []) as IntegrationTypeOption[];
677
+ const typeId = types.find((type) => type.slug === 'storage')?.id;
678
+
679
+ if (!typeId) {
680
+ throw new Error('Integration type "storage" not found.');
681
+ }
682
+
683
+ return typeId;
684
+ };
685
+
686
+ return (
687
+ <>
688
+ <div className="flex w-full min-w-0 items-center gap-2 md:max-w-72">
689
+ <EntityPicker<IntegrationProfileOption & Record<string, unknown>>
690
+ placeholder={t('selectStorageProfile')}
691
+ emptyStateDescription={t('storageProfileEmptyState')}
692
+ value={parsedValue ? Number(parsedValue) : null}
693
+ onChange={(id, option) => {
694
+ setStorageProfileSelectedLabel(option?.name ?? '');
695
+ handleChangeValue(id != null ? String(id) : '', true);
696
+ }}
697
+ valueType="number"
698
+ initialSelectedLabel={storageProfileSelectedLabel}
699
+ showCreateButton={false}
700
+ clearable={false}
701
+ loadOptions={async ({ page, pageSize, search }) => {
702
+ const typeId = await loadStorageTypeId();
703
+ const qs = new URLSearchParams({
704
+ page: String(page),
705
+ pageSize: String(pageSize),
706
+ typeId: String(typeId),
707
+ });
708
+ if (search) qs.set('search', search);
709
+ const response = await request<
710
+ ApiPaginated<
711
+ IntegrationProfileOption & Record<string, unknown>
712
+ >
713
+ >({
714
+ url: `/integration-profile?${qs}`,
715
+ method: 'GET',
716
+ });
717
+ const paged = response.data;
718
+ return {
719
+ items: paged?.data ?? [],
720
+ hasMore: (paged?.page ?? 0) < (paged?.lastPage ?? 0),
721
+ };
722
+ }}
723
+ getOptionValue={(opt) => opt.id}
724
+ getOptionLabel={(opt) => String(opt.name ?? '')}
725
+ getOptionDescription={(opt) => {
726
+ const providerSlug = String(
727
+ opt.integration_provider?.slug ?? ''
728
+ );
729
+ const providerName = getLocalizedIntegrationName(
730
+ opt.integration_provider?.integration_provider_locale,
731
+ currentLocaleCode
732
+ );
733
+ return [providerName || providerSlug, opt.slug]
734
+ .filter(Boolean)
735
+ .join(' • ');
736
+ }}
737
+ className={cn('flex-1 min-w-0', className)}
738
+ />
739
+
740
+ <Button
741
+ type="button"
742
+ variant="outline"
743
+ size="icon"
744
+ className="h-10 w-10 shrink-0"
745
+ onClick={() => {
746
+ setStorageProfileEditingId(
747
+ parsedValue ? Number(parsedValue) : null
748
+ );
749
+ setIsStorageProfileSheetOpen(true);
750
+ }}
751
+ disabled={!parsedValue}
752
+ aria-label="Editar perfil de armazenamento"
753
+ title="Editar perfil de armazenamento"
754
+ >
755
+ <Pencil className="h-4 w-4" />
756
+ </Button>
757
+
758
+ <Button
759
+ type="button"
760
+ variant="outline"
761
+ size="icon"
762
+ className="h-10 w-10 shrink-0"
763
+ onClick={() => {
764
+ setStorageProfileEditingId(null);
765
+ setIsStorageProfileSheetOpen(true);
766
+ }}
767
+ aria-label="Criar perfil de armazenamento"
768
+ title="Criar perfil de armazenamento"
769
+ >
770
+ <Plus className="h-4 w-4" />
771
+ </Button>
772
+ </div>
773
+
774
+ <IntegrationProfileSheet
775
+ open={isStorageProfileSheetOpen}
776
+ onOpenChange={(nextOpen) => {
777
+ setIsStorageProfileSheetOpen(nextOpen);
778
+ if (!nextOpen) {
779
+ setStorageProfileEditingId(null);
780
+ }
781
+ }}
782
+ profileId={storageProfileEditingId}
783
+ initialTypeSlug="storage"
784
+ lockedTypeSlug="storage"
785
+ onSaved={(profile: IntegrationProfileSheetSavedProfile) => {
786
+ setStorageProfileSelectedLabel(profile.name ?? '');
787
+ handleChangeValue(String(profile.id), true);
788
+ }}
789
+ />
790
+ </>
791
+ );
792
+ }
793
+
794
+ if (AI_PROFILE_SETTING_SLUGS.includes(setting.slug)) {
795
+ return (
796
+ <>
797
+ <div className="flex w-full min-w-0 items-center gap-2 md:max-w-72">
798
+ <EntityPicker<IntegrationProfileOption & Record<string, unknown>>
799
+ placeholder={t('selectAiProfile')}
800
+ emptyStateDescription={t('aiProfileEmptyState')}
801
+ value={parsedValue ? Number(parsedValue) : null}
802
+ onChange={(id, option) => {
803
+ setStorageProfileSelectedLabel(option?.name ?? '');
804
+ handleChangeValue(id != null ? String(id) : '', true);
805
+ }}
806
+ valueType="number"
807
+ initialSelectedLabel={storageProfileSelectedLabel}
808
+ showCreateButton={false}
809
+ clearable={false}
810
+ loadOptions={async ({ page, pageSize, search }) => {
811
+ const qs = new URLSearchParams({
812
+ page: String(page),
813
+ pageSize: String(pageSize),
814
+ typeSlug: 'ai',
815
+ });
816
+ if (search) qs.set('search', search);
817
+ const response = await request<
818
+ ApiPaginated<
819
+ IntegrationProfileOption & Record<string, unknown>
820
+ >
821
+ >({
822
+ url: `/integration-profile?${qs}`,
823
+ method: 'GET',
824
+ });
825
+ const paged = response.data;
826
+ return {
827
+ items: paged?.data ?? [],
828
+ hasMore: (paged?.page ?? 0) < (paged?.lastPage ?? 0),
829
+ };
830
+ }}
831
+ getOptionValue={(opt) => opt.id}
832
+ getOptionLabel={(opt) => String(opt.name ?? '')}
833
+ getOptionDescription={(opt) => {
834
+ const providerSlug = String(
835
+ opt.integration_provider?.slug ?? ''
836
+ );
837
+ const providerName = getLocalizedIntegrationName(
838
+ opt.integration_provider?.integration_provider_locale,
839
+ currentLocaleCode
840
+ );
841
+ return [providerName || providerSlug, opt.slug]
842
+ .filter(Boolean)
843
+ .join(' • ');
844
+ }}
845
+ className={cn('flex-1 min-w-0', className)}
846
+ />
847
+
848
+ <Button
849
+ type="button"
850
+ variant="outline"
851
+ size="icon"
852
+ className="h-10 w-10 shrink-0"
853
+ onClick={() => {
854
+ setStorageProfileEditingId(
855
+ parsedValue ? Number(parsedValue) : null
856
+ );
857
+ setIsStorageProfileSheetOpen(true);
858
+ }}
859
+ disabled={!parsedValue}
860
+ aria-label="Editar perfil de IA"
861
+ title="Editar perfil de IA"
862
+ >
863
+ <Pencil className="h-4 w-4" />
864
+ </Button>
865
+
866
+ <Button
867
+ type="button"
868
+ variant="outline"
869
+ size="icon"
870
+ className="h-10 w-10 shrink-0"
871
+ onClick={() => {
872
+ setStorageProfileEditingId(null);
873
+ setIsStorageProfileSheetOpen(true);
874
+ }}
875
+ aria-label="Criar perfil de IA"
876
+ title="Criar perfil de IA"
877
+ >
878
+ <Plus className="h-4 w-4" />
879
+ </Button>
880
+ </div>
881
+
882
+ <IntegrationProfileSheet
883
+ open={isStorageProfileSheetOpen}
884
+ onOpenChange={(nextOpen) => {
885
+ setIsStorageProfileSheetOpen(nextOpen);
886
+ if (!nextOpen) setStorageProfileEditingId(null);
887
+ }}
888
+ profileId={storageProfileEditingId}
889
+ initialTypeSlug="ai"
890
+ lockedTypeSlug="ai"
891
+ onSaved={(profile: IntegrationProfileSheetSavedProfile) => {
892
+ setStorageProfileSelectedLabel(profile.name ?? '');
893
+ handleChangeValue(String(profile.id), true);
894
+ }}
895
+ />
896
+ </>
897
+ );
898
+ }
899
+
900
+ const oauthProviderSlug = OAUTH_PROFILE_SLUG_TO_PROVIDER[setting.slug];
901
+ if (oauthProviderSlug) {
902
+ return (
903
+ <>
904
+ <div className="flex w-full min-w-0 items-center gap-2 md:max-w-72">
905
+ <EntityPicker<IntegrationProfileOption & Record<string, unknown>>
906
+ placeholder={t('selectOAuthProfile')}
907
+ emptyStateDescription={t('oauthProfileEmptyState')}
908
+ value={parsedValue ? Number(parsedValue) : null}
909
+ onChange={(id, option) => {
910
+ setStorageProfileSelectedLabel(option?.name ?? '');
911
+ handleChangeValue(id != null ? String(id) : '', true);
912
+ }}
913
+ valueType="number"
914
+ initialSelectedLabel={storageProfileSelectedLabel}
915
+ showCreateButton={false}
916
+ clearable={false}
917
+ loadOptions={async ({ page, pageSize, search }) => {
918
+ const qs = new URLSearchParams({
919
+ page: String(page),
920
+ pageSize: String(pageSize),
921
+ providerSlug: oauthProviderSlug,
922
+ });
923
+ if (search) qs.set('search', search);
924
+ const response = await request<
925
+ ApiPaginated<
926
+ IntegrationProfileOption & Record<string, unknown>
927
+ >
928
+ >({
929
+ url: `/integration-profile?${qs}`,
930
+ method: 'GET',
931
+ });
932
+ const paged = response.data;
933
+ return {
934
+ items: paged?.data ?? [],
935
+ hasMore: (paged?.page ?? 0) < (paged?.lastPage ?? 0),
936
+ };
937
+ }}
938
+ getOptionValue={(opt) => opt.id}
939
+ getOptionLabel={(opt) => String(opt.name ?? '')}
940
+ getOptionDescription={(opt) => {
941
+ const providerSlug = String(
942
+ opt.integration_provider?.slug ?? ''
943
+ );
944
+ const providerName = getLocalizedIntegrationName(
945
+ opt.integration_provider?.integration_provider_locale,
946
+ currentLocaleCode
947
+ );
948
+ return [providerName || providerSlug, opt.slug]
949
+ .filter(Boolean)
950
+ .join(' • ');
951
+ }}
952
+ className={cn('flex-1 min-w-0', className)}
953
+ />
954
+
955
+ <Button
956
+ type="button"
957
+ variant="outline"
958
+ size="icon"
959
+ className="h-10 w-10 shrink-0"
960
+ onClick={() => {
961
+ setStorageProfileEditingId(
962
+ parsedValue ? Number(parsedValue) : null
963
+ );
964
+ setIsStorageProfileSheetOpen(true);
965
+ }}
966
+ disabled={!parsedValue}
967
+ aria-label="Editar perfil OAuth"
968
+ title="Editar perfil OAuth"
969
+ >
970
+ <Pencil className="h-4 w-4" />
971
+ </Button>
972
+
973
+ <Button
974
+ type="button"
975
+ variant="outline"
976
+ size="icon"
977
+ className="h-10 w-10 shrink-0"
978
+ onClick={() => {
979
+ setStorageProfileEditingId(null);
980
+ setIsStorageProfileSheetOpen(true);
981
+ }}
982
+ aria-label="Criar perfil OAuth"
983
+ title="Criar perfil OAuth"
984
+ >
985
+ <Plus className="h-4 w-4" />
986
+ </Button>
987
+ </div>
988
+
989
+ <IntegrationProfileSheet
990
+ open={isStorageProfileSheetOpen}
991
+ onOpenChange={(nextOpen) => {
992
+ setIsStorageProfileSheetOpen(nextOpen);
993
+ if (!nextOpen) setStorageProfileEditingId(null);
994
+ }}
995
+ profileId={storageProfileEditingId}
996
+ initialTypeSlug="oauth"
997
+ lockedTypeSlug="oauth"
998
+ onSaved={(profile: IntegrationProfileSheetSavedProfile) => {
999
+ setStorageProfileSelectedLabel(profile.name ?? '');
1000
+ handleChangeValue(String(profile.id), true);
1001
+ }}
1002
+ />
1003
+ </>
1004
+ );
1005
+ }
1006
+
1007
+ if (
1008
+ setting.slug !== 'mail-integration-profile-id' &&
1009
+ !dailyReportIntegrationTypeSlug
1010
+ ) {
438
1011
  return (
439
1012
  <Input
440
1013
  type="text"
@@ -444,198 +1017,338 @@ export const SettingField = ({ setting, className }: SettingFieldProps) => {
444
1017
  />
445
1018
  );
446
1019
  }
447
- return (
448
- <EntityPicker
449
- placeholder={t('selectMailProfile')}
450
- value={parsedValue ? Number(parsedValue) : null}
451
- onChange={(id) =>
452
- handleChangeValue(id != null ? String(id) : '', true)
453
- }
454
- valueType="number"
455
- loadOptions={async ({ page, pageSize, search }) => {
456
- const qs = new URLSearchParams({
457
- page: String(page),
458
- pageSize: String(pageSize),
459
- });
460
- if (search) qs.set('search', search);
461
- const response: any = await request({
462
- url: `/mail-profile?${qs}`,
463
- });
464
- const paged = response.data;
465
- return {
466
- items: paged?.data ?? [],
467
- hasMore: (paged?.page ?? 0) < (paged?.lastPage ?? 0),
468
- };
469
- }}
470
- getOptionValue={(opt) => opt.id as number}
471
- getOptionLabel={(opt) => String(opt.name ?? '')}
472
- getOptionDescription={(opt) => String((opt as any).from_email ?? '')}
473
- mapSearchToCreateValues={(search) => ({
474
- name: search,
475
- provider: 'SMTP',
476
- })}
477
- showCreateButton
478
- onCreate={async (values) => {
479
- const slug = (values.name || 'profile')
480
- .toLowerCase()
481
- .normalize('NFD')
482
- .replace(/[\u0300-\u036f]/g, '')
483
- .replace(/[^a-z0-9]+/g, '-')
484
- .replace(/^-+|-+$/g, '');
485
- const response: any = await request({
486
- url: '/mail-profile',
487
- method: 'POST',
488
- data: { ...values, slug },
489
- });
490
- return response.data;
491
- }}
492
- renderCreateContent={({ values, setValue }) => {
493
- const provider = values.provider || 'SMTP';
494
- return (
495
- <div className="space-y-4">
496
- <div className="space-y-1">
497
- <Label>{t('mailProfile.nameLabel')}</Label>
498
- <Input
499
- value={values.name || ''}
500
- onChange={(e) => setValue('name', e.target.value)}
501
- placeholder={t('mailProfile.namePlaceholder')}
502
- />
503
- </div>
504
- <div className="space-y-1">
505
- <Label>{t('mailProfile.fromEmailLabel')}</Label>
506
- <Input
507
- type="email"
508
- value={values.from_email || ''}
509
- onChange={(e) => setValue('from_email', e.target.value)}
510
- placeholder={t('mailProfile.fromEmailPlaceholder')}
511
- />
512
- </div>
513
- <div className="space-y-1">
514
- <Label>{t('mailProfile.providerLabel')}</Label>
515
- <Select
516
- value={provider}
517
- onValueChange={(v) => setValue('provider', v)}
518
- >
519
- <SelectTrigger className="w-full">
520
- <SelectValue />
521
- </SelectTrigger>
522
- <SelectContent>
523
- <SelectItem value="SMTP">SMTP</SelectItem>
524
- <SelectItem value="GMAIL">Gmail</SelectItem>
525
- <SelectItem value="SES">
526
- {t('mailProfile.providers.ses')}
527
- </SelectItem>
528
- </SelectContent>
529
- </Select>
530
- </div>
531
- {provider === 'SMTP' && (
532
- <>
533
- <div className="space-y-1">
534
- <Label>{t('mailProfile.smtpHostLabel')}</Label>
535
- <Input
536
- value={values.smtp_host || ''}
537
- onChange={(e) => setValue('smtp_host', e.target.value)}
538
- placeholder={t('mailProfile.smtpHostPlaceholder')}
539
- />
540
- </div>
541
- <div className="space-y-1">
542
- <Label>{t('mailProfile.smtpPortLabel')}</Label>
543
- <Input
544
- type="number"
545
- value={values.smtp_port || '587'}
546
- onChange={(e) => setValue('smtp_port', e.target.value)}
547
- placeholder={t('mailProfile.smtpPortPlaceholder')}
548
- />
549
- </div>
550
- <div className="space-y-1">
551
- <Label>{t('mailProfile.smtpUsernameLabel')}</Label>
552
- <Input
553
- value={values.smtp_username || ''}
554
- onChange={(e) =>
555
- setValue('smtp_username', e.target.value)
556
- }
557
- placeholder={t('mailProfile.smtpUsernamePlaceholder')}
558
- />
559
- </div>
560
- <div className="space-y-1">
561
- <Label>{t('mailProfile.smtpPasswordLabel')}</Label>
562
- <Input
563
- type="password"
564
- value={values.smtp_password || ''}
565
- onChange={(e) =>
566
- setValue('smtp_password', e.target.value)
567
- }
568
- />
569
- </div>
570
- </>
571
- )}
572
- {provider === 'GMAIL' && (
573
- <>
1020
+
1021
+ if (dailyReportIntegrationTypeSlug) {
1022
+ const loadIntegrationCatalog = async () => {
1023
+ const qs = new URLSearchParams({
1024
+ typeSlug: dailyReportIntegrationTypeSlug,
1025
+ });
1026
+ const providersResponse = await request<IntegrationProviderOption[]>({
1027
+ url: `/integration-profile/providers?${qs}`,
1028
+ method: 'GET',
1029
+ });
1030
+
1031
+ return {
1032
+ providers: (providersResponse.data ??
1033
+ []) as IntegrationProviderOption[],
1034
+ };
1035
+ };
1036
+
1037
+ return (
1038
+ <EntityPicker<IntegrationProfileOption & Record<string, unknown>>
1039
+ placeholder={
1040
+ dailyReportIntegrationTypeSlug === 'email'
1041
+ ? t('selectMailProfile')
1042
+ : t('selectWhatsAppProfile')
1043
+ }
1044
+ value={parsedValue ? Number(parsedValue) : null}
1045
+ onChange={(id) =>
1046
+ handleChangeValue(id != null ? String(id) : '', true)
1047
+ }
1048
+ valueType="number"
1049
+ loadOptions={async ({ page, pageSize, search }) => {
1050
+ const qs = new URLSearchParams({
1051
+ page: String(page),
1052
+ pageSize: String(pageSize),
1053
+ typeSlug: dailyReportIntegrationTypeSlug,
1054
+ });
1055
+ if (search) qs.set('search', search);
1056
+ const response = await request<
1057
+ ApiPaginated<IntegrationProfileOption & Record<string, unknown>>
1058
+ >({
1059
+ url: `/integration-profile?${qs}`,
1060
+ method: 'GET',
1061
+ });
1062
+ const paged = response.data;
1063
+ return {
1064
+ items: paged?.data ?? [],
1065
+ hasMore: (paged?.page ?? 0) < (paged?.lastPage ?? 0),
1066
+ };
1067
+ }}
1068
+ getOptionValue={(opt) => opt.id}
1069
+ getOptionLabel={(opt) => String(opt.name ?? '')}
1070
+ getOptionDescription={(opt) =>
1071
+ String(opt.integration_provider?.slug ?? opt.slug ?? '')
1072
+ }
1073
+ mapSearchToCreateValues={(search) =>
1074
+ dailyReportIntegrationTypeSlug === 'email'
1075
+ ? {
1076
+ name: search,
1077
+ provider: 'smtp',
1078
+ port: '587',
1079
+ }
1080
+ : {
1081
+ name: search,
1082
+ provider: 'evolution-api',
1083
+ port: '',
1084
+ }
1085
+ }
1086
+ showCreateButton
1087
+ onCreate={async (values) => {
1088
+ const providersCatalog = await loadIntegrationCatalog();
1089
+ const providerSlug =
1090
+ dailyReportIntegrationTypeSlug === 'email'
1091
+ ? values.provider || 'smtp'
1092
+ : 'evolution-api';
1093
+ const provider = providersCatalog.providers.find(
1094
+ (item) => item.slug === providerSlug
1095
+ );
1096
+
1097
+ if (!provider) {
1098
+ throw new Error(
1099
+ `Integration provider "${providerSlug}" not found.`
1100
+ );
1101
+ }
1102
+
1103
+ const name = String(values.name || 'Profile').trim();
1104
+ const response = await request<
1105
+ IntegrationProfileOption & Record<string, unknown>
1106
+ >({
1107
+ url: '/integration-profile',
1108
+ method: 'POST',
1109
+ data: {
1110
+ name,
1111
+ slug: toSlug(name),
1112
+ type_id: provider.type_id,
1113
+ provider_id: provider.id,
1114
+ config: buildIntegrationProfileConfig(providerSlug, values),
1115
+ is_active: true,
1116
+ },
1117
+ });
1118
+ return response.data;
1119
+ }}
1120
+ renderCreateContent={({ values, setValue }) => {
1121
+ if (dailyReportIntegrationTypeSlug === 'whatsapp') {
1122
+ return (
1123
+ <div className="space-y-4">
574
1124
  <div className="space-y-1">
575
- <Label>{t('mailProfile.clientIdLabel')}</Label>
1125
+ <Label>{t('whatsappProfile.nameLabel')}</Label>
576
1126
  <Input
577
- value={values.gmail_client_id || ''}
578
- onChange={(e) =>
579
- setValue('gmail_client_id', e.target.value)
580
- }
1127
+ value={values.name || ''}
1128
+ onChange={(e) => setValue('name', e.target.value)}
1129
+ placeholder={t('whatsappProfile.namePlaceholder')}
581
1130
  />
582
1131
  </div>
583
1132
  <div className="space-y-1">
584
- <Label>{t('mailProfile.clientSecretLabel')}</Label>
585
- <Input
586
- type="password"
587
- value={values.gmail_client_secret || ''}
588
- onChange={(e) =>
589
- setValue('gmail_client_secret', e.target.value)
590
- }
591
- />
1133
+ <Label>{t('whatsappProfile.providerLabel')}</Label>
1134
+ <Select
1135
+ value={values.provider || 'evolution-api'}
1136
+ onValueChange={(v) => setValue('provider', v)}
1137
+ >
1138
+ <SelectTrigger className="w-full">
1139
+ <SelectValue />
1140
+ </SelectTrigger>
1141
+ <SelectContent>
1142
+ <SelectItem value="evolution-api">
1143
+ Evolution API
1144
+ </SelectItem>
1145
+ </SelectContent>
1146
+ </Select>
592
1147
  </div>
593
1148
  <div className="space-y-1">
594
- <Label>{t('mailProfile.refreshTokenLabel')}</Label>
1149
+ <Label>{t('whatsappProfile.hostLabel')}</Label>
595
1150
  <Input
596
- value={values.gmail_refresh_token || ''}
597
- onChange={(e) =>
598
- setValue('gmail_refresh_token', e.target.value)
599
- }
1151
+ value={values.host || ''}
1152
+ onChange={(e) => setValue('host', e.target.value)}
1153
+ placeholder={t('whatsappProfile.hostPlaceholder')}
600
1154
  />
601
1155
  </div>
602
- </>
603
- )}
604
- {provider === 'SES' && (
605
- <>
606
1156
  <div className="space-y-1">
607
- <Label>{t('mailProfile.accessKeyIdLabel')}</Label>
1157
+ <Label>{t('whatsappProfile.instanceNameLabel')}</Label>
608
1158
  <Input
609
- value={values.ses_access_key_id || ''}
1159
+ value={values.instance_name || ''}
610
1160
  onChange={(e) =>
611
- setValue('ses_access_key_id', e.target.value)
1161
+ setValue('instance_name', e.target.value)
612
1162
  }
1163
+ placeholder={t(
1164
+ 'whatsappProfile.instanceNamePlaceholder'
1165
+ )}
613
1166
  />
614
1167
  </div>
615
1168
  <div className="space-y-1">
616
- <Label>{t('mailProfile.secretAccessKeyLabel')}</Label>
1169
+ <Label>{t('whatsappProfile.tokenLabel')}</Label>
617
1170
  <Input
618
1171
  type="password"
619
- value={values.ses_secret_access_key || ''}
620
- onChange={(e) =>
621
- setValue('ses_secret_access_key', e.target.value)
622
- }
623
- />
624
- </div>
625
- <div className="space-y-1">
626
- <Label>{t('mailProfile.regionLabel')}</Label>
627
- <Input
628
- value={values.ses_region || ''}
629
- onChange={(e) => setValue('ses_region', e.target.value)}
630
- placeholder={t('mailProfile.regionPlaceholder')}
1172
+ value={values.token || ''}
1173
+ onChange={(e) => setValue('token', e.target.value)}
1174
+ placeholder={t('whatsappProfile.tokenPlaceholder')}
631
1175
  />
632
1176
  </div>
633
- </>
634
- )}
635
- </div>
636
- );
637
- }}
638
- className={cn(className, 'w-full min-w-0 md:min-w-50')}
1177
+ </div>
1178
+ );
1179
+ }
1180
+
1181
+ const provider = values.provider || 'smtp';
1182
+ return (
1183
+ <div className="space-y-4">
1184
+ <div className="space-y-1">
1185
+ <Label>{t('mailProfile.nameLabel')}</Label>
1186
+ <Input
1187
+ value={values.name || ''}
1188
+ onChange={(e) => setValue('name', e.target.value)}
1189
+ placeholder={t('mailProfile.namePlaceholder')}
1190
+ />
1191
+ </div>
1192
+ <div className="space-y-1">
1193
+ <Label>{t('mailProfile.providerLabel')}</Label>
1194
+ <Select
1195
+ value={provider}
1196
+ onValueChange={(v) => setValue('provider', v)}
1197
+ >
1198
+ <SelectTrigger className="w-full">
1199
+ <SelectValue />
1200
+ </SelectTrigger>
1201
+ <SelectContent>
1202
+ <SelectItem value="smtp">SMTP</SelectItem>
1203
+ <SelectItem value="gmail">Gmail</SelectItem>
1204
+ <SelectItem value="ses">
1205
+ {t('mailProfile.providers.ses')}
1206
+ </SelectItem>
1207
+ </SelectContent>
1208
+ </Select>
1209
+ </div>
1210
+ <div className="space-y-1">
1211
+ <Label>{t('mailProfile.fromEmailLabel')}</Label>
1212
+ <Input
1213
+ type="email"
1214
+ value={values.from_email || ''}
1215
+ onChange={(e) => setValue('from_email', e.target.value)}
1216
+ placeholder={t('mailProfile.fromEmailPlaceholder')}
1217
+ />
1218
+ </div>
1219
+ <div className="space-y-1">
1220
+ <Label>{t('mailProfile.fromNameLabel')}</Label>
1221
+ <Input
1222
+ value={values.from_name || ''}
1223
+ onChange={(e) => setValue('from_name', e.target.value)}
1224
+ placeholder={t('mailProfile.fromNamePlaceholder')}
1225
+ />
1226
+ </div>
1227
+ {provider === 'smtp' && (
1228
+ <>
1229
+ <div className="space-y-1">
1230
+ <Label>{t('mailProfile.smtpHostLabel')}</Label>
1231
+ <Input
1232
+ value={values.host || ''}
1233
+ onChange={(e) => setValue('host', e.target.value)}
1234
+ placeholder={t('mailProfile.smtpHostPlaceholder')}
1235
+ />
1236
+ </div>
1237
+ <div className="space-y-1">
1238
+ <Label>{t('mailProfile.smtpPortLabel')}</Label>
1239
+ <Input
1240
+ type="number"
1241
+ value={values.port || '587'}
1242
+ onChange={(e) => setValue('port', e.target.value)}
1243
+ placeholder={t('mailProfile.smtpPortPlaceholder')}
1244
+ />
1245
+ </div>
1246
+ <div className="space-y-1">
1247
+ <Label>{t('mailProfile.smtpUsernameLabel')}</Label>
1248
+ <Input
1249
+ value={values.username || ''}
1250
+ onChange={(e) => setValue('username', e.target.value)}
1251
+ placeholder={t('mailProfile.smtpUsernamePlaceholder')}
1252
+ />
1253
+ </div>
1254
+ <div className="space-y-1">
1255
+ <Label>{t('mailProfile.smtpPasswordLabel')}</Label>
1256
+ <Input
1257
+ type="password"
1258
+ value={values.password || ''}
1259
+ onChange={(e) => setValue('password', e.target.value)}
1260
+ />
1261
+ </div>
1262
+ <div className="space-y-1">
1263
+ <Label>{t('mailProfile.replyToEmailLabel')}</Label>
1264
+ <Input
1265
+ type="email"
1266
+ value={values.reply_to_email || ''}
1267
+ onChange={(e) =>
1268
+ setValue('reply_to_email', e.target.value)
1269
+ }
1270
+ placeholder={t('mailProfile.replyToEmailPlaceholder')}
1271
+ />
1272
+ </div>
1273
+ </>
1274
+ )}
1275
+ {provider === 'gmail' && (
1276
+ <>
1277
+ <div className="space-y-1">
1278
+ <Label>{t('mailProfile.clientIdLabel')}</Label>
1279
+ <Input
1280
+ value={values.client_id || ''}
1281
+ onChange={(e) =>
1282
+ setValue('client_id', e.target.value)
1283
+ }
1284
+ />
1285
+ </div>
1286
+ <div className="space-y-1">
1287
+ <Label>{t('mailProfile.clientSecretLabel')}</Label>
1288
+ <Input
1289
+ type="password"
1290
+ value={values.client_secret || ''}
1291
+ onChange={(e) =>
1292
+ setValue('client_secret', e.target.value)
1293
+ }
1294
+ />
1295
+ </div>
1296
+ <div className="space-y-1">
1297
+ <Label>{t('mailProfile.refreshTokenLabel')}</Label>
1298
+ <Input
1299
+ value={values.refresh_token || ''}
1300
+ onChange={(e) =>
1301
+ setValue('refresh_token', e.target.value)
1302
+ }
1303
+ />
1304
+ </div>
1305
+ </>
1306
+ )}
1307
+ {provider === 'ses' && (
1308
+ <>
1309
+ <div className="space-y-1">
1310
+ <Label>{t('mailProfile.accessKeyIdLabel')}</Label>
1311
+ <Input
1312
+ value={values.access_key_id || ''}
1313
+ onChange={(e) =>
1314
+ setValue('access_key_id', e.target.value)
1315
+ }
1316
+ />
1317
+ </div>
1318
+ <div className="space-y-1">
1319
+ <Label>{t('mailProfile.secretAccessKeyLabel')}</Label>
1320
+ <Input
1321
+ type="password"
1322
+ value={values.secret_access_key || ''}
1323
+ onChange={(e) =>
1324
+ setValue('secret_access_key', e.target.value)
1325
+ }
1326
+ />
1327
+ </div>
1328
+ <div className="space-y-1">
1329
+ <Label>{t('mailProfile.regionLabel')}</Label>
1330
+ <Input
1331
+ value={values.region || ''}
1332
+ onChange={(e) => setValue('region', e.target.value)}
1333
+ placeholder={t('mailProfile.regionPlaceholder')}
1334
+ />
1335
+ </div>
1336
+ </>
1337
+ )}
1338
+ </div>
1339
+ );
1340
+ }}
1341
+ className={cn(className, 'w-full min-w-0 md:min-w-50')}
1342
+ />
1343
+ );
1344
+ }
1345
+
1346
+ return (
1347
+ <Input
1348
+ type="text"
1349
+ value={parsedValue || ''}
1350
+ onChange={(e) => handleChangeValue(formatValue(e.target.value))}
1351
+ className={cn(className, 'bg-background')}
639
1352
  />
640
1353
  );
641
1354