@hed-hog/core 0.0.350 → 0.0.353

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 (284) hide show
  1. package/dist/access-log/access-log.controller.d.ts +16 -0
  2. package/dist/access-log/access-log.controller.d.ts.map +1 -0
  3. package/dist/access-log/access-log.controller.js +44 -0
  4. package/dist/access-log/access-log.controller.js.map +1 -0
  5. package/dist/access-log/access-log.interceptor.d.ts +9 -0
  6. package/dist/access-log/access-log.interceptor.d.ts.map +1 -0
  7. package/dist/access-log/access-log.interceptor.js +46 -0
  8. package/dist/access-log/access-log.interceptor.js.map +1 -0
  9. package/dist/access-log/access-log.module.d.ts +3 -0
  10. package/dist/access-log/access-log.module.d.ts.map +1 -0
  11. package/dist/access-log/access-log.module.js +29 -0
  12. package/dist/access-log/access-log.module.js.map +1 -0
  13. package/dist/access-log/access-log.service.d.ts +47 -0
  14. package/dist/access-log/access-log.service.d.ts.map +1 -0
  15. package/dist/access-log/access-log.service.js +113 -0
  16. package/dist/access-log/access-log.service.js.map +1 -0
  17. package/dist/access-log/dto/query-access-log.dto.d.ts +8 -0
  18. package/dist/access-log/dto/query-access-log.dto.d.ts.map +1 -0
  19. package/dist/access-log/dto/query-access-log.dto.js +42 -0
  20. package/dist/access-log/dto/query-access-log.dto.js.map +1 -0
  21. package/dist/auth/auth.controller.d.ts +9 -9
  22. package/dist/auth/auth.service.d.ts +19 -19
  23. package/dist/auth/auth.service.d.ts.map +1 -1
  24. package/dist/auth/auth.service.js +0 -4
  25. package/dist/auth/auth.service.js.map +1 -1
  26. package/dist/challenge/challenge.service.d.ts +2 -2
  27. package/dist/challenge/challenge.service.d.ts.map +1 -1
  28. package/dist/challenge/challenge.service.js +0 -1
  29. package/dist/challenge/challenge.service.js.map +1 -1
  30. package/dist/core/system.module.d.ts.map +1 -1
  31. package/dist/core/system.module.js +1 -0
  32. package/dist/core/system.module.js.map +1 -1
  33. package/dist/core.module.d.ts.map +1 -1
  34. package/dist/core.module.js +12 -0
  35. package/dist/core.module.js.map +1 -1
  36. package/dist/dashboard/dashboard-user/dashboard-user.controller.d.ts +3 -3
  37. package/dist/dashboard/dashboard-user/dashboard-user.service.d.ts +3 -3
  38. package/dist/dashboard/dashboard.module.d.ts.map +1 -1
  39. package/dist/dashboard/dashboard.module.js +4 -1
  40. package/dist/dashboard/dashboard.module.js.map +1 -1
  41. package/dist/file/file.controller.js +0 -1
  42. package/dist/file/file.controller.js.map +1 -1
  43. package/dist/file/file.service.d.ts +65 -0
  44. package/dist/file/file.service.d.ts.map +1 -1
  45. package/dist/file/file.service.js +151 -3
  46. package/dist/file/file.service.js.map +1 -1
  47. package/dist/file/provider/abstract.provider.d.ts +8 -0
  48. package/dist/file/provider/abstract.provider.d.ts.map +1 -1
  49. package/dist/file/provider/abstract.provider.js.map +1 -1
  50. package/dist/file/provider/azure.provider.d.ts +4 -0
  51. package/dist/file/provider/azure.provider.d.ts.map +1 -1
  52. package/dist/file/provider/azure.provider.js +14 -0
  53. package/dist/file/provider/azure.provider.js.map +1 -1
  54. package/dist/file/provider/gcs.provider.d.ts +4 -0
  55. package/dist/file/provider/gcs.provider.d.ts.map +1 -1
  56. package/dist/file/provider/gcs.provider.js +14 -0
  57. package/dist/file/provider/gcs.provider.js.map +1 -1
  58. package/dist/file/provider/local.provider.d.ts +4 -0
  59. package/dist/file/provider/local.provider.d.ts.map +1 -1
  60. package/dist/file/provider/local.provider.js +19 -0
  61. package/dist/file/provider/local.provider.js.map +1 -1
  62. package/dist/file/provider/provider.factory.d.ts.map +1 -1
  63. package/dist/file/provider/provider.factory.js +3 -0
  64. package/dist/file/provider/provider.factory.js.map +1 -1
  65. package/dist/file/provider/s3.provider.d.ts +4 -0
  66. package/dist/file/provider/s3.provider.d.ts.map +1 -1
  67. package/dist/file/provider/s3.provider.js +17 -0
  68. package/dist/file/provider/s3.provider.js.map +1 -1
  69. package/dist/mcp/mcp-audit.service.d.ts +3 -1
  70. package/dist/mcp/mcp-audit.service.d.ts.map +1 -1
  71. package/dist/mcp/mcp-audit.service.js +14 -2
  72. package/dist/mcp/mcp-audit.service.js.map +1 -1
  73. package/dist/mcp/mcp.controller.d.ts +3 -1
  74. package/dist/mcp/mcp.controller.d.ts.map +1 -1
  75. package/dist/mcp/mcp.controller.js +10 -2
  76. package/dist/mcp/mcp.controller.js.map +1 -1
  77. package/dist/mcp/mcp.module.d.ts.map +1 -1
  78. package/dist/mcp/mcp.module.js +72 -0
  79. package/dist/mcp/mcp.module.js.map +1 -1
  80. package/dist/mcp/tools/core-ai.mcp-tools.d.ts +45 -0
  81. package/dist/mcp/tools/core-ai.mcp-tools.d.ts.map +1 -0
  82. package/dist/mcp/tools/core-ai.mcp-tools.js +214 -0
  83. package/dist/mcp/tools/core-ai.mcp-tools.js.map +1 -0
  84. package/dist/mcp/tools/core-dashboard-items.mcp-tools.d.ts +21 -0
  85. package/dist/mcp/tools/core-dashboard-items.mcp-tools.d.ts.map +1 -0
  86. package/dist/mcp/tools/core-dashboard-items.mcp-tools.js +89 -0
  87. package/dist/mcp/tools/core-dashboard-items.mcp-tools.js.map +1 -0
  88. package/dist/mcp/tools/core-dashboard.mcp-tools.d.ts +29 -0
  89. package/dist/mcp/tools/core-dashboard.mcp-tools.d.ts.map +1 -0
  90. package/dist/mcp/tools/core-dashboard.mcp-tools.js +149 -0
  91. package/dist/mcp/tools/core-dashboard.mcp-tools.js.map +1 -0
  92. package/dist/mcp/tools/core-event-webhooks.mcp-tools.d.ts +30 -0
  93. package/dist/mcp/tools/core-event-webhooks.mcp-tools.d.ts.map +1 -0
  94. package/dist/mcp/tools/core-event-webhooks.mcp-tools.js +185 -0
  95. package/dist/mcp/tools/core-event-webhooks.mcp-tools.js.map +1 -0
  96. package/dist/mcp/tools/core-files.mcp-tools.d.ts +26 -0
  97. package/dist/mcp/tools/core-files.mcp-tools.d.ts.map +1 -0
  98. package/dist/mcp/tools/core-files.mcp-tools.js +140 -0
  99. package/dist/mcp/tools/core-files.mcp-tools.js.map +1 -0
  100. package/dist/mcp/tools/core-integration-events.mcp-tools.d.ts +26 -0
  101. package/dist/mcp/tools/core-integration-events.mcp-tools.d.ts.map +1 -0
  102. package/dist/mcp/tools/core-integration-events.mcp-tools.js +168 -0
  103. package/dist/mcp/tools/core-integration-events.mcp-tools.js.map +1 -0
  104. package/dist/mcp/tools/core-mail-profile.mcp-tools.d.ts +32 -0
  105. package/dist/mcp/tools/core-mail-profile.mcp-tools.d.ts.map +1 -0
  106. package/dist/mcp/tools/core-mail-profile.mcp-tools.js +178 -0
  107. package/dist/mcp/tools/core-mail-profile.mcp-tools.js.map +1 -0
  108. package/dist/mcp/tools/core-mail-sent.mcp-tools.d.ts +14 -0
  109. package/dist/mcp/tools/core-mail-sent.mcp-tools.d.ts.map +1 -0
  110. package/dist/mcp/tools/core-mail-sent.mcp-tools.js +106 -0
  111. package/dist/mcp/tools/core-mail-sent.mcp-tools.js.map +1 -0
  112. package/dist/mcp/tools/core-mail.mcp-tools.d.ts +36 -0
  113. package/dist/mcp/tools/core-mail.mcp-tools.d.ts.map +1 -0
  114. package/dist/mcp/tools/core-mail.mcp-tools.js +184 -0
  115. package/dist/mcp/tools/core-mail.mcp-tools.js.map +1 -0
  116. package/dist/mcp/tools/core-mcp-chat.mcp-tools.d.ts +21 -0
  117. package/dist/mcp/tools/core-mcp-chat.mcp-tools.d.ts.map +1 -0
  118. package/dist/mcp/tools/core-mcp-chat.mcp-tools.js +118 -0
  119. package/dist/mcp/tools/core-mcp-chat.mcp-tools.js.map +1 -0
  120. package/dist/mcp/tools/core-menus.mcp-tools.d.ts +40 -0
  121. package/dist/mcp/tools/core-menus.mcp-tools.d.ts.map +1 -0
  122. package/dist/mcp/tools/core-menus.mcp-tools.js +268 -0
  123. package/dist/mcp/tools/core-menus.mcp-tools.js.map +1 -0
  124. package/dist/mcp/tools/core-notifications.mcp-tools.d.ts +31 -0
  125. package/dist/mcp/tools/core-notifications.mcp-tools.d.ts.map +1 -0
  126. package/dist/mcp/tools/core-notifications.mcp-tools.js +178 -0
  127. package/dist/mcp/tools/core-notifications.mcp-tools.js.map +1 -0
  128. package/dist/mcp/tools/core-profile.mcp-tools.d.ts +20 -0
  129. package/dist/mcp/tools/core-profile.mcp-tools.d.ts.map +1 -0
  130. package/dist/mcp/tools/core-profile.mcp-tools.js +111 -0
  131. package/dist/mcp/tools/core-profile.mcp-tools.js.map +1 -0
  132. package/dist/mcp/tools/core-roles.mcp-tools.d.ts +70 -0
  133. package/dist/mcp/tools/core-roles.mcp-tools.d.ts.map +1 -0
  134. package/dist/mcp/tools/core-roles.mcp-tools.js +355 -0
  135. package/dist/mcp/tools/core-roles.mcp-tools.js.map +1 -0
  136. package/dist/mcp/tools/core-routes.mcp-tools.d.ts +52 -0
  137. package/dist/mcp/tools/core-routes.mcp-tools.d.ts.map +1 -0
  138. package/dist/mcp/tools/core-routes.mcp-tools.js +254 -0
  139. package/dist/mcp/tools/core-routes.mcp-tools.js.map +1 -0
  140. package/dist/mcp/tools/core-screens.mcp-tools.d.ts +47 -0
  141. package/dist/mcp/tools/core-screens.mcp-tools.d.ts.map +1 -0
  142. package/dist/mcp/tools/core-screens.mcp-tools.js +250 -0
  143. package/dist/mcp/tools/core-screens.mcp-tools.js.map +1 -0
  144. package/dist/mcp/tools/core-sessions.mcp-tools.d.ts +20 -0
  145. package/dist/mcp/tools/core-sessions.mcp-tools.d.ts.map +1 -0
  146. package/dist/mcp/tools/core-sessions.mcp-tools.js +128 -0
  147. package/dist/mcp/tools/core-sessions.mcp-tools.js.map +1 -0
  148. package/dist/mcp/tools/core-settings.mcp-tools.d.ts +25 -0
  149. package/dist/mcp/tools/core-settings.mcp-tools.d.ts.map +1 -1
  150. package/dist/mcp/tools/core-settings.mcp-tools.js +138 -0
  151. package/dist/mcp/tools/core-settings.mcp-tools.js.map +1 -1
  152. package/dist/mcp/tools/core-system.mcp-tools.d.ts +8 -0
  153. package/dist/mcp/tools/core-system.mcp-tools.d.ts.map +1 -0
  154. package/dist/mcp/tools/core-system.mcp-tools.js +40 -0
  155. package/dist/mcp/tools/core-system.mcp-tools.js.map +1 -0
  156. package/dist/mcp/tools/core-webhooks.mcp-tools.d.ts +46 -0
  157. package/dist/mcp/tools/core-webhooks.mcp-tools.d.ts.map +1 -0
  158. package/dist/mcp/tools/core-webhooks.mcp-tools.js +266 -0
  159. package/dist/mcp/tools/core-webhooks.mcp-tools.js.map +1 -0
  160. package/dist/mcp-chat/mcp-chat.controller.d.ts +1 -1
  161. package/dist/mcp-chat/mcp-chat.service.d.ts +25 -2
  162. package/dist/mcp-chat/mcp-chat.service.d.ts.map +1 -1
  163. package/dist/mcp-chat/mcp-chat.service.js +666 -36
  164. package/dist/mcp-chat/mcp-chat.service.js.map +1 -1
  165. package/dist/notification/notification.controller.d.ts +8 -4
  166. package/dist/notification/notification.controller.d.ts.map +1 -1
  167. package/dist/notification/notification.controller.js +12 -0
  168. package/dist/notification/notification.controller.js.map +1 -1
  169. package/dist/notification/notification.service.d.ts +19 -5
  170. package/dist/notification/notification.service.d.ts.map +1 -1
  171. package/dist/notification/notification.service.js +155 -9
  172. package/dist/notification/notification.service.js.map +1 -1
  173. package/dist/pdf/pdf.service.js +1 -1
  174. package/dist/pdf/pdf.service.js.map +1 -1
  175. package/dist/profile/profile.controller.d.ts +3 -3
  176. package/dist/profile/profile.controller.js +0 -1
  177. package/dist/profile/profile.controller.js.map +1 -1
  178. package/dist/profile/profile.module.d.ts.map +1 -1
  179. package/dist/profile/profile.module.js +1 -0
  180. package/dist/profile/profile.module.js.map +1 -1
  181. package/dist/profile/profile.service.d.ts +3 -3
  182. package/dist/session/session.controller.d.ts +2 -2
  183. package/dist/session/session.service.d.ts +6 -6
  184. package/dist/setting/setting.controller.d.ts +1 -1
  185. package/dist/setting/setting.service.d.ts +1 -1
  186. package/dist/setting/setting.service.d.ts.map +1 -1
  187. package/dist/setting/setting.service.js +1 -4
  188. package/dist/setting/setting.service.js.map +1 -1
  189. package/dist/task/task.service.d.ts +1 -0
  190. package/dist/task/task.service.d.ts.map +1 -1
  191. package/dist/task/task.service.js +25 -0
  192. package/dist/task/task.service.js.map +1 -1
  193. package/dist/token/token.service.d.ts.map +1 -1
  194. package/dist/token/token.service.js +0 -1
  195. package/dist/token/token.service.js.map +1 -1
  196. package/dist/user/user.controller.d.ts +9 -9
  197. package/dist/user/user.service.d.ts +21 -21
  198. package/dist/webhook-integration/dto/webhook-action.dto.d.ts +22 -0
  199. package/dist/webhook-integration/dto/webhook-action.dto.d.ts.map +1 -0
  200. package/dist/webhook-integration/dto/webhook-action.dto.js +120 -0
  201. package/dist/webhook-integration/dto/webhook-action.dto.js.map +1 -0
  202. package/dist/webhook-integration/webhook-integration.controller.d.ts +7 -5
  203. package/dist/webhook-integration/webhook-integration.controller.d.ts.map +1 -1
  204. package/dist/webhook-integration/webhook-integration.controller.js +18 -5
  205. package/dist/webhook-integration/webhook-integration.controller.js.map +1 -1
  206. package/dist/webhook-integration/webhook-integration.service.d.ts +14 -5
  207. package/dist/webhook-integration/webhook-integration.service.d.ts.map +1 -1
  208. package/dist/webhook-integration/webhook-integration.service.js +174 -41
  209. package/dist/webhook-integration/webhook-integration.service.js.map +1 -1
  210. package/hedhog/data/menu.yaml +15 -0
  211. package/hedhog/data/route.yaml +1304 -0
  212. package/hedhog/data/setting_group.yaml +67 -0
  213. package/hedhog/data/setting_subgroup.yaml +16 -0
  214. package/hedhog/frontend/app/access-log/page.tsx.ejs +772 -0
  215. package/hedhog/frontend/app/ai_agent/page.tsx.ejs +10 -3
  216. package/hedhog/frontend/app/dashboard/dashboard-home-tabs.tsx.ejs +33 -13
  217. package/hedhog/frontend/app/dashboard/management/tabs/components-tab.tsx.ejs +12 -5
  218. package/hedhog/frontend/app/dashboard/management/tabs/dashboards-tab.tsx.ejs +12 -5
  219. package/hedhog/frontend/app/integrations-webhooks/page.tsx.ejs +1817 -356
  220. package/hedhog/frontend/app/mcp_chat/chat-components.tsx.ejs +43 -5
  221. package/hedhog/frontend/app/mcp_chat/page.tsx.ejs +35 -7
  222. package/hedhog/frontend/app/menu/page.tsx.ejs +9 -3
  223. package/hedhog/frontend/app/roles/page.tsx.ejs +18 -6
  224. package/hedhog/frontend/app/routes/page.tsx.ejs +9 -3
  225. package/hedhog/frontend/app/setting-list/page.tsx.ejs +344 -171
  226. package/hedhog/frontend/app/users/user-access-sheet.tsx.ejs +310 -304
  227. package/hedhog/frontend/app/users/user-sheet.tsx.ejs +25 -7
  228. package/hedhog/frontend/messages/en.json +104 -3
  229. package/hedhog/frontend/messages/pt.json +104 -3
  230. package/hedhog/table/access_log.yaml +31 -0
  231. package/package.json +4 -3
  232. package/src/access-log/access-log.controller.ts +29 -0
  233. package/src/access-log/access-log.interceptor.ts +38 -0
  234. package/src/access-log/access-log.module.ts +16 -0
  235. package/src/access-log/access-log.service.ts +129 -0
  236. package/src/access-log/dto/query-access-log.dto.ts +23 -0
  237. package/src/auth/auth.service.ts +0 -7
  238. package/src/challenge/challenge.service.ts +0 -2
  239. package/src/core/system.module.ts +1 -0
  240. package/src/core.module.ts +12 -0
  241. package/src/dashboard/dashboard.module.ts +4 -1
  242. package/src/file/file.controller.ts +14 -14
  243. package/src/file/file.service.ts +198 -4
  244. package/src/file/provider/abstract.provider.ts +10 -0
  245. package/src/file/provider/azure.provider.ts +24 -0
  246. package/src/file/provider/gcs.provider.ts +20 -0
  247. package/src/file/provider/local.provider.ts +30 -2
  248. package/src/file/provider/provider.factory.ts +3 -0
  249. package/src/file/provider/s3.provider.ts +22 -0
  250. package/src/mcp/mcp-audit.service.ts +47 -36
  251. package/src/mcp/mcp.controller.ts +40 -30
  252. package/src/mcp/mcp.module.ts +72 -0
  253. package/src/mcp/tools/core-ai.mcp-tools.ts +156 -0
  254. package/src/mcp/tools/core-dashboard-items.mcp-tools.ts +63 -0
  255. package/src/mcp/tools/core-dashboard.mcp-tools.ts +103 -0
  256. package/src/mcp/tools/core-event-webhooks.mcp-tools.ts +134 -0
  257. package/src/mcp/tools/core-files.mcp-tools.ts +105 -0
  258. package/src/mcp/tools/core-integration-events.mcp-tools.ts +118 -0
  259. package/src/mcp/tools/core-mail-profile.mcp-tools.ts +128 -0
  260. package/src/mcp/tools/core-mail-sent.mcp-tools.ts +76 -0
  261. package/src/mcp/tools/core-mail.mcp-tools.ts +130 -0
  262. package/src/mcp/tools/core-mcp-chat.mcp-tools.ts +81 -0
  263. package/src/mcp/tools/core-menus.mcp-tools.ts +197 -0
  264. package/src/mcp/tools/core-notifications.mcp-tools.ts +128 -0
  265. package/src/mcp/tools/core-profile.mcp-tools.ts +74 -0
  266. package/src/mcp/tools/core-roles.mcp-tools.ts +288 -0
  267. package/src/mcp/tools/core-routes.mcp-tools.ts +196 -0
  268. package/src/mcp/tools/core-screens.mcp-tools.ts +186 -0
  269. package/src/mcp/tools/core-sessions.mcp-tools.ts +97 -0
  270. package/src/mcp/tools/core-settings.mcp-tools.ts +171 -53
  271. package/src/mcp/tools/core-system.mcp-tools.ts +19 -0
  272. package/src/mcp/tools/core-webhooks.mcp-tools.ts +199 -0
  273. package/src/mcp-chat/mcp-chat.service.ts +887 -38
  274. package/src/notification/notification.controller.ts +20 -8
  275. package/src/notification/notification.service.ts +209 -9
  276. package/src/pdf/pdf.service.ts +1 -1
  277. package/src/profile/profile.controller.ts +1 -1
  278. package/src/profile/profile.module.ts +1 -0
  279. package/src/setting/setting.service.ts +8 -11
  280. package/src/task/task.service.ts +31 -0
  281. package/src/token/token.service.ts +0 -2
  282. package/src/webhook-integration/dto/webhook-action.dto.ts +96 -0
  283. package/src/webhook-integration/webhook-integration.controller.ts +124 -103
  284. package/src/webhook-integration/webhook-integration.service.ts +267 -64
@@ -21,6 +21,7 @@ import {
21
21
  import { Badge } from '@/components/ui/badge';
22
22
  import { Button } from '@/components/ui/button';
23
23
  import { Checkbox } from '@/components/ui/checkbox';
24
+ import { EntityPicker } from '@/components/ui/entity-picker';
24
25
  import { Input } from '@/components/ui/input';
25
26
  import { KpiCardsGrid } from '@/components/ui/kpi-cards-grid';
26
27
  import { Label } from '@/components/ui/label';
@@ -51,11 +52,29 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
51
52
  import { Textarea } from '@/components/ui/textarea';
52
53
  import { usePagination } from '@/hooks/use-pagination';
53
54
  import { formatDate } from '@/lib/format-date';
55
+ import {
56
+ DndContext,
57
+ PointerSensor,
58
+ closestCenter,
59
+ useSensor,
60
+ useSensors,
61
+ type DragEndEvent,
62
+ } from '@dnd-kit/core';
63
+ import {
64
+ SortableContext,
65
+ arrayMove,
66
+ useSortable,
67
+ verticalListSortingStrategy,
68
+ } from '@dnd-kit/sortable';
69
+ import { CSS } from '@dnd-kit/utilities';
54
70
  import { useApp, useQuery } from '@hed-hog/next-app-provider';
71
+ import { useQueryClient } from '@tanstack/react-query';
55
72
  import {
56
73
  Activity,
57
74
  BookOpenText,
75
+ ChevronRight,
58
76
  Clock3,
77
+ GripVertical,
59
78
  Pencil,
60
79
  Plus,
61
80
  RotateCcw,
@@ -64,9 +83,23 @@ import {
64
83
  Webhook,
65
84
  } from 'lucide-react';
66
85
  import { useTranslations } from 'next-intl';
67
- import { type ReactNode, useMemo, useState } from 'react';
86
+ import dynamic from 'next/dynamic';
87
+ import { useEffect, useMemo, useRef, useState, type ReactNode } from 'react';
68
88
  import { toast } from 'sonner';
69
89
 
90
+ const RichTextEditor = dynamic(
91
+ () =>
92
+ import('@/components/rich-text-editor').then((mod) => ({
93
+ default: mod.RichTextEditor,
94
+ })),
95
+ {
96
+ ssr: false,
97
+ loading: () => (
98
+ <div className="min-h-56 animate-pulse rounded-md bg-muted" />
99
+ ),
100
+ }
101
+ );
102
+
70
103
  type Status = 'active' | 'inactive';
71
104
 
72
105
  type EventCatalog = {
@@ -98,6 +131,24 @@ type IncomingAction = {
98
131
  whatsapp_token?: string | null;
99
132
  };
100
133
 
134
+ type IncomingActionForm = {
135
+ type: IncomingAction['type'];
136
+ name: string;
137
+ order: number;
138
+ status: Status;
139
+ mail_profile_id: number | null;
140
+ mail_id: number | null;
141
+ email_to: string;
142
+ email_cc: string;
143
+ email_bcc: string;
144
+ whatsapp_target_type: 'phone' | 'group';
145
+ whatsapp_target: string;
146
+ whatsapp_template: string;
147
+ whatsapp_instance: string;
148
+ whatsapp_base_url: string;
149
+ whatsapp_token: string;
150
+ };
151
+
101
152
  type IncomingIntegration = {
102
153
  id: number;
103
154
  slug: string;
@@ -168,6 +219,69 @@ type SelectOption = {
168
219
  module?: string;
169
220
  };
170
221
 
222
+ type MailProfileProvider = 'SMTP' | 'GMAIL' | 'SES';
223
+
224
+ type MailProfileDetail = {
225
+ id: number;
226
+ slug: string;
227
+ name: string;
228
+ provider: MailProfileProvider;
229
+ from_name?: string | null;
230
+ from_email: string;
231
+ reply_to_name?: string | null;
232
+ reply_to_email?: string | null;
233
+ smtp_host?: string | null;
234
+ smtp_port?: number | null;
235
+ smtp_secure?: boolean;
236
+ smtp_username?: string | null;
237
+ smtp_password?: string | null;
238
+ gmail_client_id?: string | null;
239
+ gmail_client_secret?: string | null;
240
+ gmail_refresh_token?: string | null;
241
+ ses_access_key_id?: string | null;
242
+ ses_secret_access_key?: string | null;
243
+ ses_region?: string | null;
244
+ };
245
+
246
+ type MailTemplateDetail = {
247
+ id: number;
248
+ slug: string;
249
+ mail_id?: number;
250
+ subject?: string;
251
+ body?: string;
252
+ mail_var?: Array<{ name?: string }>;
253
+ locale?: { code: string };
254
+ };
255
+
256
+ const defaultMailProfileEditForm = {
257
+ slug: '',
258
+ name: '',
259
+ provider: 'SMTP' as MailProfileProvider,
260
+ from_name: '',
261
+ from_email: '',
262
+ reply_to_name: '',
263
+ reply_to_email: '',
264
+ smtp_host: '',
265
+ smtp_port: '587',
266
+ smtp_secure: true,
267
+ smtp_username: '',
268
+ smtp_password: '',
269
+ gmail_client_id: '',
270
+ gmail_client_secret: '',
271
+ gmail_refresh_token: '',
272
+ ses_access_key_id: '',
273
+ ses_secret_access_key: '',
274
+ ses_region: '',
275
+ };
276
+
277
+ const defaultMailTemplateEditForm = {
278
+ slug: '',
279
+ locale_code: 'en',
280
+ subject: '',
281
+ body: '',
282
+ variables: '',
283
+ };
284
+
171
285
  type PaginatedResponse<T> = {
172
286
  data: T[];
173
287
  totalItems: number;
@@ -198,13 +312,13 @@ const defaultIncomingForm = {
198
312
  body_schema: '',
199
313
  };
200
314
 
201
- const defaultActionForm = {
315
+ const defaultActionForm: IncomingActionForm = {
202
316
  type: 'email' as IncomingAction['type'],
203
317
  name: '',
204
318
  order: 0,
205
319
  status: 'active' as Status,
206
- mail_profile_id: '',
207
- mail_id: '',
320
+ mail_profile_id: null,
321
+ mail_id: null,
208
322
  email_to: '',
209
323
  email_cc: '',
210
324
  email_bcc: '',
@@ -260,6 +374,25 @@ function stringifyStringArray(values: string[]) {
260
374
  return filtered.length ? JSON.stringify(filtered, null, 2) : '';
261
375
  }
262
376
 
377
+ function stringifyTemplateVars(value?: Array<{ name?: string }>) {
378
+ if (!Array.isArray(value) || value.length === 0) {
379
+ return '';
380
+ }
381
+
382
+ return value
383
+ .map((item) => item?.name?.trim())
384
+ .filter((name): name is string => Boolean(name))
385
+ .join('\n');
386
+ }
387
+
388
+ function parseTemplateVars(value: string) {
389
+ return value
390
+ .split('\n')
391
+ .map((item) => item.trim())
392
+ .filter(Boolean)
393
+ .map((name) => ({ name }));
394
+ }
395
+
263
396
  function getPublicWebhookUrl(uuid: string, publicUrl?: string | null) {
264
397
  if (publicUrl) return publicUrl;
265
398
  const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL || '';
@@ -268,7 +401,9 @@ function getPublicWebhookUrl(uuid: string, publicUrl?: string | null) {
268
401
 
269
402
  export default function IntegrationsWebhooksPage() {
270
403
  const t = useTranslations('core.IntegrationsWebhooksPage');
271
- const { request, currentLocaleCode, getSettingValue } = useApp();
404
+ const { request, currentLocaleCode, getSettingValue, locales, accessToken } =
405
+ useApp();
406
+ const queryClient = useQueryClient();
272
407
  const pageSizeOptions = [6, 12, 24, 48] as const;
273
408
 
274
409
  const catalog = usePagination({
@@ -297,9 +432,34 @@ export default function IntegrationsWebhooksPage() {
297
432
  const [incomingSheetTab, setIncomingSheetTab] = useState('settings');
298
433
  const [oneTimeToken, setOneTimeToken] = useState<string | null>(null);
299
434
  const [allowedIpDraft, setAllowedIpDraft] = useState('');
300
- const [actionForm, setActionForm] = useState(defaultActionForm);
435
+ const [incomingActions, setIncomingActions] = useState<IncomingAction[]>([]);
436
+ const [actionSheetOpen, setActionSheetOpen] = useState(false);
437
+ const [actionEdit, setActionEdit] = useState<IncomingAction | null>(null);
438
+ const [actionForm, setActionForm] =
439
+ useState<IncomingActionForm>(defaultActionForm);
440
+ const [mailProfileEditorOpen, setMailProfileEditorOpen] = useState(false);
441
+ const [mailProfileEditId, setMailProfileEditId] = useState<number | null>(
442
+ null
443
+ );
444
+ const [mailProfileEditForm, setMailProfileEditForm] = useState(
445
+ defaultMailProfileEditForm
446
+ );
447
+ const [mailProfileIsTesting, setMailProfileIsTesting] = useState(false);
448
+ const [mailProfileTestedSignature, setMailProfileTestedSignature] = useState<
449
+ string | null
450
+ >(null);
451
+ const [mailTemplateEditorOpen, setMailTemplateEditorOpen] = useState(false);
452
+ const [mailTemplateEditId, setMailTemplateEditId] = useState<number | null>(
453
+ null
454
+ );
455
+ const [mailTemplateEditForm, setMailTemplateEditForm] = useState(
456
+ defaultMailTemplateEditForm
457
+ );
301
458
  const [incomingLogsPage, setIncomingLogsPage] = useState(1);
302
459
  const [incomingLogsPageSize, setIncomingLogsPageSize] = useState(12);
460
+ const [logOrder, setLogOrder] = useState<'desc' | 'asc'>('desc');
461
+ const [selectedLog, setSelectedLog] = useState<IncomingCallLog | null>(null);
462
+ const sseAbortRef = useRef<AbortController | null>(null);
303
463
 
304
464
  const [eventWebhookOpen, setEventWebhookOpen] = useState(false);
305
465
  const [eventWebhookEdit, setEventWebhookEdit] = useState<EventWebhook | null>(
@@ -330,7 +490,9 @@ export default function IntegrationsWebhooksPage() {
330
490
  placeholderData: (previous) => previous ?? [],
331
491
  });
332
492
 
333
- const { data: mailProfiles = [] } = useQuery<SelectOption[]>({
493
+ const { data: mailProfiles = [], refetch: refetchMailProfiles } = useQuery<
494
+ SelectOption[]
495
+ >({
334
496
  queryKey: ['webhook-mail-profiles', currentLocaleCode],
335
497
  queryFn: async () => {
336
498
  const response = await request<{ data: SelectOption[] }>({
@@ -342,7 +504,9 @@ export default function IntegrationsWebhooksPage() {
342
504
  placeholderData: (previous) => previous ?? [],
343
505
  });
344
506
 
345
- const { data: mailTemplates = [] } = useQuery<SelectOption[]>({
507
+ const { data: mailTemplates = [], refetch: refetchMailTemplates } = useQuery<
508
+ SelectOption[]
509
+ >({
346
510
  queryKey: ['webhook-mail-templates', currentLocaleCode],
347
511
  queryFn: async () => {
348
512
  const response = await request<{ data: SelectOption[] }>({
@@ -361,6 +525,7 @@ export default function IntegrationsWebhooksPage() {
361
525
  incomingEdit?.id,
362
526
  incomingLogsPage,
363
527
  incomingLogsPageSize,
528
+ logOrder,
364
529
  ],
365
530
  queryFn: async () => {
366
531
  if (!incomingEdit?.id) {
@@ -373,7 +538,7 @@ export default function IntegrationsWebhooksPage() {
373
538
  }
374
539
 
375
540
  const response = await request<PaginatedResponse<IncomingCallLog>>({
376
- url: `/webhook-integration/${incomingEdit.id}/log?page=${incomingLogsPage}&pageSize=${incomingLogsPageSize}`,
541
+ url: `/webhook-integration/${incomingEdit.id}/log?page=${incomingLogsPage}&pageSize=${incomingLogsPageSize}&sort=${logOrder}`,
377
542
  method: 'GET',
378
543
  });
379
544
 
@@ -395,6 +560,78 @@ export default function IntegrationsWebhooksPage() {
395
560
  },
396
561
  });
397
562
 
563
+ useEffect(() => {
564
+ if (!incomingEdit?.id || !incomingOpen || incomingSheetTab !== 'logs') {
565
+ sseAbortRef.current?.abort();
566
+ sseAbortRef.current = null;
567
+ return;
568
+ }
569
+
570
+ const controller = new AbortController();
571
+ sseAbortRef.current = controller;
572
+
573
+ const rawBase = process.env.NEXT_PUBLIC_API_BASE_URL || '/api';
574
+ const apiBase = rawBase.replace(/\/$/, '');
575
+ const url = `${apiBase}/webhook-integration/${incomingEdit.id}/log/stream`;
576
+
577
+ (async () => {
578
+ try {
579
+ const response = await fetch(url, {
580
+ method: 'GET',
581
+ headers: {
582
+ Accept: 'text/event-stream',
583
+ ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
584
+ },
585
+ signal: controller.signal,
586
+ });
587
+
588
+ if (!response.ok || !response.body) return;
589
+
590
+ const reader = response.body.getReader();
591
+ const decoder = new TextDecoder();
592
+ let buffer = '';
593
+
594
+ while (true) {
595
+ const { value, done } = await reader.read();
596
+ if (done) break;
597
+ buffer += decoder.decode(value, { stream: true });
598
+ const events = buffer.split('\n\n');
599
+ buffer = events.pop() ?? '';
600
+
601
+ for (const eventBlock of events) {
602
+ const dataLine = eventBlock
603
+ .split('\n')
604
+ .find((l) => l.startsWith('data:'));
605
+ if (!dataLine) continue;
606
+ try {
607
+ const payload = JSON.parse(dataLine.slice(5).trim());
608
+ if (payload?.type === 'log') {
609
+ queryClient.invalidateQueries({
610
+ queryKey: ['incoming-webhook-logs', incomingEdit.id],
611
+ });
612
+ }
613
+ } catch {
614
+ // ignore malformed frames
615
+ }
616
+ }
617
+ }
618
+ } catch {
619
+ // connection closed or aborted — ignore
620
+ }
621
+ })();
622
+
623
+ return () => {
624
+ controller.abort();
625
+ sseAbortRef.current = null;
626
+ };
627
+ }, [
628
+ incomingEdit?.id,
629
+ incomingOpen,
630
+ incomingSheetTab,
631
+ accessToken,
632
+ queryClient,
633
+ ]);
634
+
398
635
  const stats = useMemo(
399
636
  () => [
400
637
  {
@@ -427,6 +664,11 @@ export default function IntegrationsWebhooksPage() {
427
664
  );
428
665
  const incomingLogItems = incomingLogsResult?.data || [];
429
666
  const incomingLogsTotalItems = incomingLogsResult?.totalItems || 0;
667
+ const actionSensors = useSensors(
668
+ useSensor(PointerSensor, {
669
+ activationConstraint: { distance: 6 },
670
+ })
671
+ );
430
672
 
431
673
  const openCatalogCreate = () => {
432
674
  setCatalogEdit(null);
@@ -474,9 +716,14 @@ export default function IntegrationsWebhooksPage() {
474
716
  setIncomingEdit(null);
475
717
  setIncomingSheetTab('settings');
476
718
  setIncomingLogsPage(1);
719
+ setLogOrder('desc');
720
+ setSelectedLog(null);
721
+ setIncomingActions([]);
477
722
  setIncomingForm(defaultIncomingForm);
478
723
  setOneTimeToken(null);
479
724
  setAllowedIpDraft('');
725
+ setActionEdit(null);
726
+ setActionSheetOpen(false);
480
727
  setActionForm(defaultActionForm);
481
728
  setIncomingOpen(true);
482
729
  };
@@ -500,13 +747,266 @@ export default function IntegrationsWebhooksPage() {
500
747
  if (data.plainToken) {
501
748
  setOneTimeToken(data.plainToken);
502
749
  }
750
+ setIncomingActions(
751
+ [...(data.webhook_action || [])].sort(
752
+ (a, b) => a.order - b.order || a.id - b.id
753
+ )
754
+ );
503
755
  setAllowedIpDraft('');
756
+ setActionEdit(null);
757
+ setActionSheetOpen(false);
504
758
  setActionForm(defaultActionForm);
505
759
  setIncomingSheetTab('settings');
506
760
  setIncomingLogsPage(1);
761
+ setLogOrder('desc');
762
+ setSelectedLog(null);
507
763
  setIncomingOpen(true);
508
764
  };
509
765
 
766
+ const openActionCreate = () => {
767
+ setActionEdit(null);
768
+ setActionForm({
769
+ ...defaultActionForm,
770
+ order: incomingActions.length,
771
+ });
772
+ setActionSheetOpen(true);
773
+ };
774
+
775
+ const openActionEdit = (action: IncomingAction) => {
776
+ setActionEdit(action);
777
+ setActionForm({
778
+ type: action.type,
779
+ name: action.name,
780
+ order: Number(action.order || 0),
781
+ status: action.status,
782
+ mail_profile_id: action.mail_profile_id ?? null,
783
+ mail_id: action.mail_id ?? null,
784
+ email_to: action.email_to || '',
785
+ email_cc: action.email_cc || '',
786
+ email_bcc: action.email_bcc || '',
787
+ whatsapp_target_type: action.whatsapp_target_type || 'phone',
788
+ whatsapp_target: action.whatsapp_target || '',
789
+ whatsapp_template: action.whatsapp_template || '',
790
+ whatsapp_instance: action.whatsapp_instance || '',
791
+ whatsapp_base_url: action.whatsapp_base_url || '',
792
+ whatsapp_token: action.whatsapp_token || '',
793
+ });
794
+ setActionSheetOpen(true);
795
+ };
796
+
797
+ const openMailProfileEditor = async (id: number) => {
798
+ try {
799
+ const response = await request<MailProfileDetail>({
800
+ url: `/mail-profile/${id}`,
801
+ method: 'GET',
802
+ });
803
+
804
+ const data = response.data;
805
+ setMailProfileEditId(data.id);
806
+ setMailProfileEditForm({
807
+ slug: data.slug || '',
808
+ name: data.name || '',
809
+ provider: data.provider || 'SMTP',
810
+ from_name: data.from_name || '',
811
+ from_email: data.from_email || '',
812
+ reply_to_name: data.reply_to_name || '',
813
+ reply_to_email: data.reply_to_email || '',
814
+ smtp_host: data.smtp_host || '',
815
+ smtp_port: data.smtp_port ? String(data.smtp_port) : '587',
816
+ smtp_secure: Boolean(data.smtp_secure),
817
+ smtp_username: data.smtp_username || '',
818
+ smtp_password: data.smtp_password || '',
819
+ gmail_client_id: data.gmail_client_id || '',
820
+ gmail_client_secret: data.gmail_client_secret || '',
821
+ gmail_refresh_token: data.gmail_refresh_token || '',
822
+ ses_access_key_id: data.ses_access_key_id || '',
823
+ ses_secret_access_key: data.ses_secret_access_key || '',
824
+ ses_region: data.ses_region || '',
825
+ });
826
+ setMailProfileTestedSignature(null);
827
+ setMailProfileEditorOpen(true);
828
+ } catch {
829
+ toast.error(t('loadError'));
830
+ }
831
+ };
832
+
833
+ const createMailProfilePayload = () => ({
834
+ slug: mailProfileEditForm.slug,
835
+ name: mailProfileEditForm.name,
836
+ provider: mailProfileEditForm.provider,
837
+ from_name: mailProfileEditForm.from_name || undefined,
838
+ from_email: mailProfileEditForm.from_email,
839
+ reply_to_name: mailProfileEditForm.reply_to_name || undefined,
840
+ reply_to_email: mailProfileEditForm.reply_to_email || undefined,
841
+ smtp_host: mailProfileEditForm.smtp_host || undefined,
842
+ smtp_port: mailProfileEditForm.smtp_port
843
+ ? Number(mailProfileEditForm.smtp_port)
844
+ : undefined,
845
+ smtp_secure: mailProfileEditForm.smtp_secure,
846
+ smtp_username: mailProfileEditForm.smtp_username || undefined,
847
+ smtp_password: mailProfileEditForm.smtp_password || undefined,
848
+ gmail_client_id: mailProfileEditForm.gmail_client_id || undefined,
849
+ gmail_client_secret: mailProfileEditForm.gmail_client_secret || undefined,
850
+ gmail_refresh_token: mailProfileEditForm.gmail_refresh_token || undefined,
851
+ ses_access_key_id: mailProfileEditForm.ses_access_key_id || undefined,
852
+ ses_secret_access_key:
853
+ mailProfileEditForm.ses_secret_access_key || undefined,
854
+ ses_region: mailProfileEditForm.ses_region || undefined,
855
+ });
856
+
857
+ const currentMailProfileSignature = JSON.stringify(
858
+ createMailProfilePayload()
859
+ );
860
+ const canSubmitMailProfile =
861
+ mailProfileTestedSignature === currentMailProfileSignature;
862
+
863
+ const handleTestMailProfile = async () => {
864
+ const payload = createMailProfilePayload();
865
+
866
+ try {
867
+ setMailProfileIsTesting(true);
868
+ const { data } = await request<{ success: boolean; destination: string }>(
869
+ {
870
+ url: '/mail-profile/test',
871
+ method: 'POST',
872
+ data: payload,
873
+ }
874
+ );
875
+
876
+ setMailProfileTestedSignature(JSON.stringify(payload));
877
+ toast.success(
878
+ data.destination || payload.from_email
879
+ ? `Perfil testado com sucesso para ${data.destination || payload.from_email}`
880
+ : 'Perfil testado com sucesso.'
881
+ );
882
+ } catch (error) {
883
+ console.error('Error testing mail profile:', error);
884
+ setMailProfileTestedSignature(null);
885
+ toast.error('Erro ao testar o perfil de e-mail.');
886
+ } finally {
887
+ setMailProfileIsTesting(false);
888
+ }
889
+ };
890
+
891
+ const saveMailProfileEditor = async () => {
892
+ if (!mailProfileEditId) return;
893
+
894
+ try {
895
+ await request({
896
+ url: `/mail-profile/${mailProfileEditId}`,
897
+ method: 'PATCH',
898
+ data: {
899
+ slug: mailProfileEditForm.slug,
900
+ name: mailProfileEditForm.name,
901
+ provider: mailProfileEditForm.provider,
902
+ from_name: mailProfileEditForm.from_name || undefined,
903
+ from_email: mailProfileEditForm.from_email,
904
+ reply_to_name: mailProfileEditForm.reply_to_name || undefined,
905
+ reply_to_email: mailProfileEditForm.reply_to_email || undefined,
906
+ smtp_host: mailProfileEditForm.smtp_host || undefined,
907
+ smtp_port: mailProfileEditForm.smtp_port
908
+ ? Number(mailProfileEditForm.smtp_port)
909
+ : undefined,
910
+ smtp_secure: mailProfileEditForm.smtp_secure,
911
+ smtp_username: mailProfileEditForm.smtp_username || undefined,
912
+ smtp_password: mailProfileEditForm.smtp_password || undefined,
913
+ gmail_client_id: mailProfileEditForm.gmail_client_id || undefined,
914
+ gmail_client_secret:
915
+ mailProfileEditForm.gmail_client_secret || undefined,
916
+ gmail_refresh_token:
917
+ mailProfileEditForm.gmail_refresh_token || undefined,
918
+ ses_access_key_id: mailProfileEditForm.ses_access_key_id || undefined,
919
+ ses_secret_access_key:
920
+ mailProfileEditForm.ses_secret_access_key || undefined,
921
+ ses_region: mailProfileEditForm.ses_region || undefined,
922
+ },
923
+ });
924
+
925
+ await refetchMailProfiles();
926
+ toast.success(t('updated'));
927
+ setMailProfileTestedSignature(null);
928
+ setMailProfileEditorOpen(false);
929
+ } catch {
930
+ toast.error(t('saveError'));
931
+ }
932
+ };
933
+
934
+ const openMailTemplateEditor = async (id: number) => {
935
+ const defaultLocale = locales?.[0]?.code || 'en';
936
+ try {
937
+ const response = await request<MailTemplateDetail>({
938
+ url: `/mail/${id}?locale=${defaultLocale}`,
939
+ method: 'GET',
940
+ });
941
+
942
+ const data = response.data;
943
+ setMailTemplateEditId(data.mail_id || data.id);
944
+ setMailTemplateEditForm({
945
+ slug: data.slug || '',
946
+ locale_code: data.locale?.code || defaultLocale,
947
+ subject: data.subject || '',
948
+ body: data.body || '',
949
+ variables: stringifyTemplateVars(data.mail_var),
950
+ });
951
+ setMailTemplateEditorOpen(true);
952
+ } catch {
953
+ toast.error(t('loadError'));
954
+ }
955
+ };
956
+
957
+ const loadMailTemplateLocale = async (localeCode: string) => {
958
+ if (!mailTemplateEditId) return;
959
+
960
+ try {
961
+ const response = await request<MailTemplateDetail>({
962
+ url: `/mail/${mailTemplateEditId}?locale=${localeCode}`,
963
+ method: 'GET',
964
+ });
965
+
966
+ const data = response.data;
967
+ setMailTemplateEditForm((current) => ({
968
+ ...current,
969
+ locale_code: localeCode,
970
+ subject: data.subject || '',
971
+ body: data.body || '',
972
+ variables: stringifyTemplateVars(data.mail_var),
973
+ }));
974
+ } catch {
975
+ toast.error(t('loadError'));
976
+ }
977
+ };
978
+
979
+ const saveMailTemplateEditor = async () => {
980
+ if (!mailTemplateEditId) return;
981
+
982
+ try {
983
+ await request({
984
+ url: `/mail/${mailTemplateEditId}`,
985
+ method: 'PATCH',
986
+ data: {
987
+ slug: mailTemplateEditForm.slug,
988
+ mail_locale: [
989
+ {
990
+ locale_id:
991
+ locales.findIndex(
992
+ (locale) => locale.code === mailTemplateEditForm.locale_code
993
+ ) + 1 || 1,
994
+ subject: mailTemplateEditForm.subject,
995
+ body: mailTemplateEditForm.body,
996
+ },
997
+ ],
998
+ mail_var: parseTemplateVars(mailTemplateEditForm.variables),
999
+ },
1000
+ });
1001
+
1002
+ await refetchMailTemplates();
1003
+ toast.success(t('updated'));
1004
+ setMailTemplateEditorOpen(false);
1005
+ } catch {
1006
+ toast.error(t('saveError'));
1007
+ }
1008
+ };
1009
+
510
1010
  const setAllowedIpList = (items: string[]) => {
511
1011
  setIncomingForm((current) => ({
512
1012
  ...current,
@@ -573,18 +1073,74 @@ export default function IntegrationsWebhooksPage() {
573
1073
  const saveAction = async () => {
574
1074
  if (!incomingEdit) return;
575
1075
 
1076
+ const missingFields: string[] = [];
1077
+
1078
+ if (actionForm.type === 'email') {
1079
+ if (actionForm.mail_profile_id == null)
1080
+ missingFields.push('mail_profile_id');
1081
+ if (actionForm.mail_id == null) missingFields.push('mail_id');
1082
+ if (!actionForm.email_to.trim()) missingFields.push('email_to');
1083
+ }
1084
+
1085
+ if (actionForm.type === 'whatsapp_evolution') {
1086
+ if (!actionForm.whatsapp_target.trim()) {
1087
+ missingFields.push('whatsapp_target');
1088
+ }
1089
+ if (!actionForm.whatsapp_template.trim()) {
1090
+ missingFields.push('whatsapp_template');
1091
+ }
1092
+ if (!actionForm.whatsapp_instance.trim()) {
1093
+ missingFields.push('whatsapp_instance');
1094
+ }
1095
+ if (!actionForm.whatsapp_base_url.trim()) {
1096
+ missingFields.push('whatsapp_base_url');
1097
+ }
1098
+ if (!actionForm.whatsapp_token.trim()) {
1099
+ missingFields.push('whatsapp_token');
1100
+ }
1101
+ }
1102
+
1103
+ if (missingFields.length) {
1104
+ toast.error(
1105
+ `Preencha os campos obrigatorios da acao: ${missingFields.join(', ')}`
1106
+ );
1107
+ return;
1108
+ }
1109
+
576
1110
  try {
577
- await request({
578
- url: `/webhook-integration/${incomingEdit.id}/action`,
579
- method: 'POST',
580
- data: {
581
- ...actionForm,
582
- order: Number(actionForm.order || 0),
583
- mail_profile_id: actionForm.mail_profile_id || null,
584
- mail_id: actionForm.mail_id || null,
585
- },
586
- });
587
- toast.success(t('created'));
1111
+ const payload = {
1112
+ ...actionForm,
1113
+ order: Number(actionForm.order || 0),
1114
+ mail_profile_id: actionForm.mail_profile_id,
1115
+ mail_id: actionForm.mail_id,
1116
+ email_to: actionForm.email_to.trim(),
1117
+ email_cc: actionForm.email_cc.trim(),
1118
+ email_bcc: actionForm.email_bcc.trim(),
1119
+ whatsapp_target: actionForm.whatsapp_target.trim(),
1120
+ whatsapp_template: actionForm.whatsapp_template.trim(),
1121
+ whatsapp_instance: actionForm.whatsapp_instance.trim(),
1122
+ whatsapp_base_url: actionForm.whatsapp_base_url.trim(),
1123
+ whatsapp_token: actionForm.whatsapp_token.trim(),
1124
+ };
1125
+
1126
+ if (actionEdit) {
1127
+ await request({
1128
+ url: `/webhook-integration/${incomingEdit.id}/action/${actionEdit.id}`,
1129
+ method: 'PATCH',
1130
+ data: payload,
1131
+ });
1132
+ toast.success(t('updated'));
1133
+ } else {
1134
+ await request({
1135
+ url: `/webhook-integration/${incomingEdit.id}/action`,
1136
+ method: 'POST',
1137
+ data: payload,
1138
+ });
1139
+ toast.success(t('created'));
1140
+ }
1141
+
1142
+ setActionEdit(null);
1143
+ setActionSheetOpen(false);
588
1144
  setActionForm(defaultActionForm);
589
1145
  await openIncomingEdit(incomingEdit);
590
1146
  await incoming.refetch();
@@ -593,6 +1149,53 @@ export default function IntegrationsWebhooksPage() {
593
1149
  }
594
1150
  };
595
1151
 
1152
+ const handleIncomingActionDragEnd = async ({
1153
+ active,
1154
+ over,
1155
+ }: DragEndEvent) => {
1156
+ if (!incomingEdit || !over || active.id === over.id) {
1157
+ return;
1158
+ }
1159
+
1160
+ const current = [...incomingActions];
1161
+ const oldIndex = current.findIndex(
1162
+ (item) => String(item.id) === String(active.id)
1163
+ );
1164
+ const newIndex = current.findIndex(
1165
+ (item) => String(item.id) === String(over.id)
1166
+ );
1167
+
1168
+ if (oldIndex < 0 || newIndex < 0) {
1169
+ return;
1170
+ }
1171
+
1172
+ const reordered = arrayMove(current, oldIndex, newIndex).map(
1173
+ (item, index) => ({
1174
+ ...item,
1175
+ order: index,
1176
+ })
1177
+ );
1178
+
1179
+ setIncomingActions(reordered);
1180
+
1181
+ try {
1182
+ await Promise.all(
1183
+ reordered.map((item, index) =>
1184
+ request({
1185
+ url: `/webhook-integration/${incomingEdit.id}/action/${item.id}`,
1186
+ method: 'PATCH',
1187
+ data: { order: index },
1188
+ })
1189
+ )
1190
+ );
1191
+ await openIncomingEdit(incomingEdit);
1192
+ await incoming.refetch();
1193
+ } catch {
1194
+ setIncomingActions(current);
1195
+ toast.error(t('saveError'));
1196
+ }
1197
+ };
1198
+
596
1199
  const regenerateUuid = async () => {
597
1200
  if (!incomingEdit) return;
598
1201
  const response = await request<IncomingIntegration>({
@@ -1172,6 +1775,10 @@ export default function IntegrationsWebhooksPage() {
1172
1775
  setOneTimeToken(null);
1173
1776
  setIncomingSheetTab('settings');
1174
1777
  setIncomingLogsPage(1);
1778
+ setLogOrder('desc');
1779
+ setSelectedLog(null);
1780
+ setActionEdit(null);
1781
+ setActionSheetOpen(false);
1175
1782
  }
1176
1783
  }}
1177
1784
  >
@@ -1188,7 +1795,7 @@ export default function IntegrationsWebhooksPage() {
1188
1795
  className="mt-4"
1189
1796
  >
1190
1797
  <div className="px-3 sm:px-4">
1191
- <TabsList className="grid w-full grid-cols-2">
1798
+ <TabsList className="grid w-full grid-cols-3">
1192
1799
  <TabsTrigger value="settings">
1193
1800
  <Webhook className="mr-2 h-4 w-4" />
1194
1801
  {t('incomingSheetTabSettings')}
@@ -1197,6 +1804,10 @@ export default function IntegrationsWebhooksPage() {
1197
1804
  <Clock3 className="mr-2 h-4 w-4" />
1198
1805
  {t('incomingSheetTabLogs')}
1199
1806
  </TabsTrigger>
1807
+ <TabsTrigger value="actions" disabled={!incomingEdit}>
1808
+ <Activity className="mr-2 h-4 w-4" />
1809
+ {t('actionSection')}
1810
+ </TabsTrigger>
1200
1811
  </TabsList>
1201
1812
  </div>
1202
1813
  <TabsContent value="settings" className="mt-0">
@@ -1436,223 +2047,41 @@ export default function IntegrationsWebhooksPage() {
1436
2047
  onCancel={() => setIncomingOpen(false)}
1437
2048
  t={t}
1438
2049
  />
2050
+ </div>
2051
+ </TabsContent>
1439
2052
 
1440
- {incomingEdit && (
1441
- <div className="space-y-4 border-t pt-5">
1442
- <div className="flex items-center gap-2">
1443
- <Activity className="h-4 w-4" />
1444
- <h3 className="text-sm font-semibold">
1445
- {t('actionSection')}
1446
- </h3>
1447
- </div>
1448
- <div className="grid gap-3 md:grid-cols-3">
1449
- <Field label={t('actionType')}>
1450
- <Select
1451
- value={actionForm.type}
1452
- onValueChange={(value: IncomingAction['type']) =>
1453
- setActionForm({ ...actionForm, type: value })
1454
- }
1455
- >
1456
- <SelectTrigger className="w-full">
1457
- <SelectValue />
1458
- </SelectTrigger>
1459
- <SelectContent>
1460
- <SelectItem value="email">{t('email')}</SelectItem>
1461
- <SelectItem value="whatsapp_evolution">
1462
- {t('whatsapp')}
1463
- </SelectItem>
1464
- </SelectContent>
1465
- </Select>
1466
- </Field>
1467
- <Field label={t('name')}>
1468
- <Input
1469
- value={actionForm.name}
1470
- onChange={(event) =>
1471
- setActionForm({
1472
- ...actionForm,
1473
- name: event.target.value,
1474
- })
1475
- }
1476
- />
1477
- </Field>
1478
- <Field label={t('order')}>
1479
- <Input
1480
- type="number"
1481
- value={actionForm.order}
1482
- onChange={(event) =>
1483
- setActionForm({
1484
- ...actionForm,
1485
- order: Number(event.target.value),
1486
- })
1487
- }
1488
- />
1489
- </Field>
1490
- </div>
1491
- {actionForm.type === 'email' ? (
1492
- <div className="grid gap-3 md:grid-cols-2">
1493
- <Field label={t('mailProfile')}>
1494
- <Select
1495
- value={actionForm.mail_profile_id}
1496
- onValueChange={(value) =>
1497
- setActionForm({
1498
- ...actionForm,
1499
- mail_profile_id: value,
1500
- })
1501
- }
1502
- >
1503
- <SelectTrigger className="w-full">
1504
- <SelectValue placeholder={t('select')} />
1505
- </SelectTrigger>
1506
- <SelectContent>
1507
- {mailProfiles.map((profile) => (
1508
- <SelectItem
1509
- key={profile.id}
1510
- value={String(profile.id)}
1511
- >
1512
- {profile.name || profile.slug}
1513
- </SelectItem>
1514
- ))}
1515
- </SelectContent>
1516
- </Select>
1517
- </Field>
1518
- <Field label={t('mailTemplate')}>
1519
- <Select
1520
- value={actionForm.mail_id}
1521
- onValueChange={(value) =>
1522
- setActionForm({ ...actionForm, mail_id: value })
1523
- }
1524
- >
1525
- <SelectTrigger className="w-full">
1526
- <SelectValue placeholder={t('select')} />
1527
- </SelectTrigger>
1528
- <SelectContent>
1529
- {mailTemplates.map((template) => (
1530
- <SelectItem
1531
- key={template.id}
1532
- value={String(template.id)}
1533
- >
1534
- {template.slug}
1535
- </SelectItem>
1536
- ))}
1537
- </SelectContent>
1538
- </Select>
1539
- </Field>
1540
- <Field label={t('emailTo')}>
1541
- <Input
1542
- value={actionForm.email_to}
1543
- onChange={(event) =>
1544
- setActionForm({
1545
- ...actionForm,
1546
- email_to: event.target.value,
1547
- })
1548
- }
1549
- placeholder="{{email}}"
1550
- />
1551
- </Field>
1552
- <Field label={t('emailCc')}>
1553
- <Input
1554
- value={actionForm.email_cc}
1555
- onChange={(event) =>
1556
- setActionForm({
1557
- ...actionForm,
1558
- email_cc: event.target.value,
1559
- })
1560
- }
1561
- />
1562
- </Field>
1563
- </div>
1564
- ) : (
1565
- <div className="grid gap-3 md:grid-cols-2">
1566
- <Field label={t('whatsappTargetType')}>
1567
- <Select
1568
- value={actionForm.whatsapp_target_type}
1569
- onValueChange={(value) =>
1570
- setActionForm({
1571
- ...actionForm,
1572
- whatsapp_target_type: value,
1573
- })
1574
- }
1575
- >
1576
- <SelectTrigger className="w-full">
1577
- <SelectValue />
1578
- </SelectTrigger>
1579
- <SelectContent>
1580
- <SelectItem value="phone">
1581
- {t('phone')}
1582
- </SelectItem>
1583
- <SelectItem value="group">
1584
- {t('group')}
1585
- </SelectItem>
1586
- </SelectContent>
1587
- </Select>
1588
- </Field>
1589
- <Field label={t('whatsappTarget')}>
1590
- <Input
1591
- value={actionForm.whatsapp_target}
1592
- onChange={(event) =>
1593
- setActionForm({
1594
- ...actionForm,
1595
- whatsapp_target: event.target.value,
1596
- })
1597
- }
1598
- placeholder="{{phone}}"
1599
- />
1600
- </Field>
1601
- <Field label={t('whatsappInstance')}>
1602
- <Input
1603
- value={actionForm.whatsapp_instance}
1604
- onChange={(event) =>
1605
- setActionForm({
1606
- ...actionForm,
1607
- whatsapp_instance: event.target.value,
1608
- })
1609
- }
1610
- />
1611
- </Field>
1612
- <Field label={t('whatsappBaseUrl')}>
1613
- <Input
1614
- value={actionForm.whatsapp_base_url}
1615
- onChange={(event) =>
1616
- setActionForm({
1617
- ...actionForm,
1618
- whatsapp_base_url: event.target.value,
1619
- })
1620
- }
1621
- />
1622
- </Field>
1623
- <Field label={t('whatsappToken')}>
1624
- <Input
1625
- type="password"
1626
- value={actionForm.whatsapp_token}
1627
- onChange={(event) =>
1628
- setActionForm({
1629
- ...actionForm,
1630
- whatsapp_token: event.target.value,
1631
- })
1632
- }
1633
- />
1634
- </Field>
1635
- <Field label={t('whatsappTemplate')}>
1636
- <Textarea
1637
- value={actionForm.whatsapp_template}
1638
- onChange={(event) =>
1639
- setActionForm({
1640
- ...actionForm,
1641
- whatsapp_template: event.target.value,
1642
- })
1643
- }
1644
- placeholder="Olá {{name}}"
1645
- />
1646
- </Field>
1647
- </div>
1648
- )}
1649
- <div className="flex justify-end">
1650
- <Button onClick={saveAction}>{t('addAction')}</Button>
1651
- </div>
1652
- <div className="rounded-md border">
2053
+ <TabsContent
2054
+ value="actions"
2055
+ className="mt-0 space-y-4 px-3 pb-8 sm:px-4"
2056
+ >
2057
+ {!incomingEdit ? (
2058
+ <EmptyState
2059
+ icon={<Activity className="h-12 w-12" />}
2060
+ title={t('actionSection')}
2061
+ description={t('emptyIncomingDescription')}
2062
+ />
2063
+ ) : (
2064
+ <div className="space-y-4">
2065
+ <div className="flex items-center justify-between gap-3">
2066
+ <p className="text-sm text-muted-foreground">
2067
+ {t('actionsCount')}: {incomingActions.length}
2068
+ </p>
2069
+ <Button onClick={openActionCreate}>
2070
+ <Plus className="mr-2 h-4 w-4" />
2071
+ {t('addAction')}
2072
+ </Button>
2073
+ </div>
2074
+
2075
+ <div className="rounded-md border">
2076
+ <DndContext
2077
+ sensors={actionSensors}
2078
+ collisionDetection={closestCenter}
2079
+ onDragEnd={handleIncomingActionDragEnd}
2080
+ >
1653
2081
  <Table>
1654
2082
  <TableHeader>
1655
2083
  <TableRow>
2084
+ <TableHead className="w-8" />
1656
2085
  <TableHead>{t('name')}</TableHead>
1657
2086
  <TableHead>{t('actionType')}</TableHead>
1658
2087
  <TableHead>{t('order')}</TableHead>
@@ -1661,23 +2090,28 @@ export default function IntegrationsWebhooksPage() {
1661
2090
  </TableHead>
1662
2091
  </TableRow>
1663
2092
  </TableHeader>
1664
- <TableBody>
1665
- {incomingEdit.webhook_action?.map((action) => (
1666
- <TableRow key={action.id}>
1667
- <TableCell>{action.name}</TableCell>
1668
- <TableCell>
1669
- <Badge variant="secondary">
1670
- {action.type === 'email'
1671
- ? t('email')
1672
- : t('whatsapp')}
1673
- </Badge>
1674
- </TableCell>
1675
- <TableCell>{action.order}</TableCell>
1676
- <TableCell className="text-right">
1677
- <Button
1678
- variant="ghost"
1679
- size="icon"
1680
- onClick={() =>
2093
+ <SortableContext
2094
+ items={incomingActions.map((action) => action.id)}
2095
+ strategy={verticalListSortingStrategy}
2096
+ >
2097
+ <TableBody>
2098
+ {incomingActions.length === 0 ? (
2099
+ <TableRow>
2100
+ <TableCell
2101
+ colSpan={5}
2102
+ className="h-16 text-center text-sm text-muted-foreground"
2103
+ >
2104
+ {t('emptyIncomingDescription')}
2105
+ </TableCell>
2106
+ </TableRow>
2107
+ ) : (
2108
+ incomingActions.map((action) => (
2109
+ <SortableIncomingActionRow
2110
+ key={action.id}
2111
+ action={action}
2112
+ t={t}
2113
+ onEdit={openActionEdit}
2114
+ onDelete={() =>
1681
2115
  setDeleteTarget({
1682
2116
  kind: 'action',
1683
2117
  id: action.id,
@@ -1685,18 +2119,16 @@ export default function IntegrationsWebhooksPage() {
1685
2119
  name: action.name,
1686
2120
  })
1687
2121
  }
1688
- >
1689
- <Trash2 className="h-4 w-4 text-destructive" />
1690
- </Button>
1691
- </TableCell>
1692
- </TableRow>
1693
- ))}
1694
- </TableBody>
2122
+ />
2123
+ ))
2124
+ )}
2125
+ </TableBody>
2126
+ </SortableContext>
1695
2127
  </Table>
1696
- </div>
2128
+ </DndContext>
1697
2129
  </div>
1698
- )}
1699
- </div>
2130
+ </div>
2131
+ )}
1700
2132
  </TabsContent>
1701
2133
 
1702
2134
  <TabsContent
@@ -1716,102 +2148,98 @@ export default function IntegrationsWebhooksPage() {
1716
2148
  description={t('emptyIncomingLogsDescription')}
1717
2149
  />
1718
2150
  ) : (
1719
- <div className="space-y-4">
1720
- {incomingLogItems.map((log) => (
1721
- <div key={log.id} className="rounded-md border p-4">
1722
- <div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
1723
- <div className="flex items-center gap-2">
1724
- <Badge
1725
- variant="outline"
1726
- className={
1727
- log.status === 'success'
1728
- ? 'border-green-500/20 bg-green-500/10 text-green-600'
1729
- : 'border-red-500/20 bg-red-500/10 text-red-600'
1730
- }
1731
- >
1732
- {log.status === 'success'
1733
- ? t('logStatusSuccess')
1734
- : t('logStatusFailed')}
1735
- </Badge>
1736
- <span className="text-xs text-muted-foreground">
1737
- {formatDate(
1738
- log.created_at,
1739
- getSettingValue,
1740
- currentLocaleCode
1741
- )}
1742
- </span>
1743
- </div>
1744
- <div className="flex flex-wrap gap-2 text-xs text-muted-foreground">
1745
- <span>
1746
- {t('logDuration')}: {log.duration_ms}ms
1747
- </span>
1748
- <span>
1749
- {t('logRemoteIp')}: {log.remote_ip || '-'}
1750
- </span>
1751
- </div>
1752
- </div>
1753
-
1754
- {log.error_message ? (
1755
- <p className="mt-3 text-xs text-destructive">
1756
- {log.error_message}
1757
- </p>
1758
- ) : null}
2151
+ <div className="space-y-3">
2152
+ <div className="flex items-center justify-end">
2153
+ <Select
2154
+ value={logOrder}
2155
+ onValueChange={(v: 'desc' | 'asc') => {
2156
+ setLogOrder(v);
2157
+ setIncomingLogsPage(1);
2158
+ }}
2159
+ >
2160
+ <SelectTrigger className="w-52">
2161
+ <SelectValue />
2162
+ </SelectTrigger>
2163
+ <SelectContent>
2164
+ <SelectItem value="desc">
2165
+ {t('logOrderDesc')}
2166
+ </SelectItem>
2167
+ <SelectItem value="asc">{t('logOrderAsc')}</SelectItem>
2168
+ </SelectContent>
2169
+ </Select>
2170
+ </div>
1759
2171
 
1760
- {log.webhook_action_log?.length ? (
1761
- <div className="mt-4 rounded-md border">
1762
- <Table>
1763
- <TableHeader>
1764
- <TableRow>
1765
- <TableHead>{t('actionType')}</TableHead>
1766
- <TableHead>{t('name')}</TableHead>
1767
- <TableHead>{t('status')}</TableHead>
1768
- <TableHead>{t('logDuration')}</TableHead>
1769
- <TableHead>{t('logResponse')}</TableHead>
1770
- </TableRow>
1771
- </TableHeader>
1772
- <TableBody>
1773
- {log.webhook_action_log.map((actionLog) => (
1774
- <TableRow key={actionLog.id}>
1775
- <TableCell>
1776
- {actionLog.action_type === 'email'
1777
- ? t('email')
1778
- : t('whatsapp')}
1779
- </TableCell>
1780
- <TableCell>{actionLog.action_name}</TableCell>
1781
- <TableCell>
1782
- <Badge
1783
- variant="outline"
1784
- className={
1785
- actionLog.status === 'success'
1786
- ? 'border-green-500/20 bg-green-500/10 text-green-600'
1787
- : 'border-red-500/20 bg-red-500/10 text-red-600'
1788
- }
1789
- >
1790
- {actionLog.status === 'success'
1791
- ? t('logStatusSuccess')
1792
- : t('logStatusFailed')}
1793
- </Badge>
1794
- </TableCell>
1795
- <TableCell>
1796
- {actionLog.duration_ms}ms
1797
- </TableCell>
1798
- <TableCell>
1799
- <div className="max-h-24 overflow-auto rounded border bg-muted/30 p-2 font-mono text-xs">
1800
- {actionLog.error_message ||
1801
- stringifyJson(
1802
- actionLog.response_payload
1803
- ) ||
1804
- '-'}
1805
- </div>
1806
- </TableCell>
1807
- </TableRow>
1808
- ))}
1809
- </TableBody>
1810
- </Table>
1811
- </div>
1812
- ) : null}
1813
- </div>
1814
- ))}
2172
+ <div className="rounded-md border">
2173
+ <Table>
2174
+ <TableHeader>
2175
+ <TableRow>
2176
+ <TableHead className="w-28">{t('status')}</TableHead>
2177
+ <TableHead>{t('logDate')}</TableHead>
2178
+ <TableHead className="hidden sm:table-cell">
2179
+ {t('logRemoteIp')}
2180
+ </TableHead>
2181
+ <TableHead className="w-24 text-right">
2182
+ {t('logDuration')}
2183
+ </TableHead>
2184
+ <TableHead className="w-16 text-center">
2185
+ {t('actions')}
2186
+ </TableHead>
2187
+ <TableHead className="w-8" />
2188
+ </TableRow>
2189
+ </TableHeader>
2190
+ <TableBody>
2191
+ {incomingLogItems.map((log) => (
2192
+ <TableRow
2193
+ key={log.id}
2194
+ className="cursor-pointer"
2195
+ onClick={() => setSelectedLog(log)}
2196
+ >
2197
+ <TableCell>
2198
+ <Badge
2199
+ variant="outline"
2200
+ className={
2201
+ log.status === 'success'
2202
+ ? 'border-green-500/20 bg-green-500/10 text-green-600'
2203
+ : 'border-red-500/20 bg-red-500/10 text-red-600'
2204
+ }
2205
+ >
2206
+ {log.status === 'success'
2207
+ ? t('logStatusSuccess')
2208
+ : t('logStatusFailed')}
2209
+ </Badge>
2210
+ </TableCell>
2211
+ <TableCell className="text-sm text-muted-foreground">
2212
+ {formatDate(
2213
+ log.created_at,
2214
+ getSettingValue,
2215
+ currentLocaleCode
2216
+ )}
2217
+ </TableCell>
2218
+ <TableCell className="hidden text-xs text-muted-foreground sm:table-cell">
2219
+ {log.remote_ip || '-'}
2220
+ </TableCell>
2221
+ <TableCell className="text-right text-xs text-muted-foreground">
2222
+ {log.duration_ms}ms
2223
+ </TableCell>
2224
+ <TableCell className="text-center">
2225
+ {log.webhook_action_log?.length ? (
2226
+ <Badge variant="secondary" className="text-xs">
2227
+ {log.webhook_action_log.length}
2228
+ </Badge>
2229
+ ) : (
2230
+ <span className="text-xs text-muted-foreground">
2231
+ —
2232
+ </span>
2233
+ )}
2234
+ </TableCell>
2235
+ <TableCell>
2236
+ <ChevronRight className="h-4 w-4 text-muted-foreground" />
2237
+ </TableCell>
2238
+ </TableRow>
2239
+ ))}
2240
+ </TableBody>
2241
+ </Table>
2242
+ </div>
1815
2243
  </div>
1816
2244
  )}
1817
2245
 
@@ -1833,6 +2261,406 @@ export default function IntegrationsWebhooksPage() {
1833
2261
  ) : null}
1834
2262
  </TabsContent>
1835
2263
  </Tabs>
2264
+
2265
+ <Sheet
2266
+ open={actionSheetOpen}
2267
+ onOpenChange={(open) => {
2268
+ setActionSheetOpen(open);
2269
+ if (!open) {
2270
+ setActionEdit(null);
2271
+ setActionForm(defaultActionForm);
2272
+ }
2273
+ }}
2274
+ >
2275
+ <SheetContent
2276
+ side="right"
2277
+ className="w-full overflow-y-auto sm:max-w-3xl"
2278
+ >
2279
+ <SheetHeader>
2280
+ <SheetTitle>
2281
+ {actionEdit ? t('updated') : t('addAction')}
2282
+ </SheetTitle>
2283
+ <SheetDescription>{t('actionSection')}</SheetDescription>
2284
+ </SheetHeader>
2285
+ <div className="space-y-5 px-3 pb-8 sm:px-4">
2286
+ <div className="grid gap-3 md:grid-cols-3">
2287
+ <Field label={t('actionType')}>
2288
+ <Select
2289
+ value={actionForm.type}
2290
+ onValueChange={(value: IncomingAction['type']) =>
2291
+ setActionForm({ ...actionForm, type: value })
2292
+ }
2293
+ >
2294
+ <SelectTrigger className="w-full">
2295
+ <SelectValue />
2296
+ </SelectTrigger>
2297
+ <SelectContent>
2298
+ <SelectItem value="email">{t('email')}</SelectItem>
2299
+ <SelectItem value="whatsapp_evolution">
2300
+ {t('whatsapp')}
2301
+ </SelectItem>
2302
+ </SelectContent>
2303
+ </Select>
2304
+ </Field>
2305
+ <Field label={t('name')}>
2306
+ <Input
2307
+ value={actionForm.name}
2308
+ onChange={(event) =>
2309
+ setActionForm({
2310
+ ...actionForm,
2311
+ name: event.target.value,
2312
+ })
2313
+ }
2314
+ />
2315
+ </Field>
2316
+ <Field label={t('order')}>
2317
+ <Input
2318
+ type="number"
2319
+ value={actionForm.order}
2320
+ onChange={(event) =>
2321
+ setActionForm({
2322
+ ...actionForm,
2323
+ order: Number(event.target.value),
2324
+ })
2325
+ }
2326
+ />
2327
+ </Field>
2328
+ </div>
2329
+
2330
+ {actionForm.type === 'email' ? (
2331
+ <div className="grid gap-3 md:grid-cols-2">
2332
+ <Field label={t('mailProfile')}>
2333
+ <div className="flex items-start gap-2">
2334
+ <EntityPicker<SelectOption>
2335
+ value={actionForm.mail_profile_id}
2336
+ className="w-full"
2337
+ valueType="number"
2338
+ placeholder={t('select')}
2339
+ entityLabel={t('mailProfile')}
2340
+ options={mailProfiles}
2341
+ loadOptions={async ({ page, pageSize, search }) => {
2342
+ const response = await request<{
2343
+ data: SelectOption[];
2344
+ }>({
2345
+ url: `/mail-profile?page=${page}&pageSize=${pageSize}&search=${encodeURIComponent(search)}`,
2346
+ method: 'GET',
2347
+ });
2348
+
2349
+ return {
2350
+ items: response.data?.data || [],
2351
+ hasMore: false,
2352
+ };
2353
+ }}
2354
+ getOptionValue={(option) => option.id}
2355
+ getOptionLabel={(option) =>
2356
+ option.name || option.slug
2357
+ }
2358
+ getOptionDescription={(option) =>
2359
+ option.provider || option.slug
2360
+ }
2361
+ createFields={[
2362
+ {
2363
+ name: 'slug',
2364
+ label: 'Slug',
2365
+ required: true,
2366
+ },
2367
+ {
2368
+ name: 'name',
2369
+ label: t('name'),
2370
+ required: true,
2371
+ },
2372
+ {
2373
+ name: 'from_email',
2374
+ label: 'From e-mail',
2375
+ type: 'email',
2376
+ required: true,
2377
+ },
2378
+ {
2379
+ name: 'smtp_host',
2380
+ label: 'SMTP host',
2381
+ required: true,
2382
+ },
2383
+ {
2384
+ name: 'smtp_port',
2385
+ label: 'SMTP port',
2386
+ type: 'number',
2387
+ defaultValue: '587',
2388
+ required: true,
2389
+ },
2390
+ {
2391
+ name: 'smtp_username',
2392
+ label: 'SMTP username',
2393
+ required: true,
2394
+ },
2395
+ {
2396
+ name: 'smtp_password',
2397
+ label: 'SMTP password',
2398
+ type: 'password',
2399
+ required: true,
2400
+ },
2401
+ ]}
2402
+ createTitle="Criar perfil de e-mail"
2403
+ createDescription="Crie rapidamente um perfil SMTP para usar nesta ação."
2404
+ onCreate={async (values) => {
2405
+ const response = await request<SelectOption>({
2406
+ url: '/mail-profile',
2407
+ method: 'POST',
2408
+ data: {
2409
+ slug: values.slug,
2410
+ name: values.name,
2411
+ provider: 'SMTP',
2412
+ from_email: values.from_email,
2413
+ smtp_host: values.smtp_host,
2414
+ smtp_port: Number(values.smtp_port || 587),
2415
+ smtp_secure: false,
2416
+ smtp_username: values.smtp_username,
2417
+ smtp_password: values.smtp_password,
2418
+ },
2419
+ });
2420
+
2421
+ await refetchMailProfiles();
2422
+
2423
+ return {
2424
+ id: response.data.id,
2425
+ slug: response.data.slug,
2426
+ name: response.data.name,
2427
+ provider: response.data.provider,
2428
+ };
2429
+ }}
2430
+ onChange={(value) =>
2431
+ setActionForm((current) => ({
2432
+ ...current,
2433
+ mail_profile_id:
2434
+ typeof value === 'number'
2435
+ ? value
2436
+ : value != null && value !== ''
2437
+ ? Number(value)
2438
+ : null,
2439
+ }))
2440
+ }
2441
+ />
2442
+
2443
+ <Button
2444
+ type="button"
2445
+ variant="outline"
2446
+ size="icon"
2447
+ className="h-10 w-10 shrink-0"
2448
+ disabled={!actionForm.mail_profile_id}
2449
+ aria-label="Editar perfil de e-mail selecionado"
2450
+ title="Editar perfil de e-mail selecionado"
2451
+ onClick={() => {
2452
+ if (actionForm.mail_profile_id) {
2453
+ void openMailProfileEditor(
2454
+ actionForm.mail_profile_id
2455
+ );
2456
+ }
2457
+ }}
2458
+ >
2459
+ <Pencil className="h-4 w-4" />
2460
+ </Button>
2461
+ </div>
2462
+ </Field>
2463
+ <Field label={t('mailTemplate')}>
2464
+ <div className="flex items-start gap-2">
2465
+ <div className="min-w-0 flex-1">
2466
+ <EntityPicker<SelectOption>
2467
+ className="w-full"
2468
+ value={actionForm.mail_id}
2469
+ valueType="number"
2470
+ placeholder={t('select')}
2471
+ entityLabel={t('mailTemplate')}
2472
+ options={mailTemplates}
2473
+ loadOptions={async ({ page, pageSize, search }) => {
2474
+ const response = await request<{
2475
+ data: SelectOption[];
2476
+ }>({
2477
+ url: `/mail?page=${page}&pageSize=${pageSize}&search=${encodeURIComponent(search)}`,
2478
+ method: 'GET',
2479
+ });
2480
+
2481
+ return {
2482
+ items: response.data?.data || [],
2483
+ hasMore: false,
2484
+ };
2485
+ }}
2486
+ getOptionValue={(option) => option.id}
2487
+ getOptionLabel={(option) => option.slug}
2488
+ getOptionDescription={(option) =>
2489
+ option.subject || option.name
2490
+ }
2491
+ createFields={[
2492
+ {
2493
+ name: 'slug',
2494
+ label: 'Slug',
2495
+ required: true,
2496
+ },
2497
+ ]}
2498
+ createTitle="Criar template de e-mail"
2499
+ createDescription="Crie rapidamente um template para usar nesta ação."
2500
+ onCreate={async (values) => {
2501
+ const response = await request<SelectOption>({
2502
+ url: '/mail',
2503
+ method: 'POST',
2504
+ data: {
2505
+ slug: values.slug,
2506
+ },
2507
+ });
2508
+
2509
+ await refetchMailTemplates();
2510
+
2511
+ return {
2512
+ id: response.data.id,
2513
+ slug: response.data.slug,
2514
+ subject: response.data.subject,
2515
+ name: response.data.name,
2516
+ };
2517
+ }}
2518
+ onChange={(value) =>
2519
+ setActionForm((current) => ({
2520
+ ...current,
2521
+ mail_id:
2522
+ typeof value === 'number'
2523
+ ? value
2524
+ : value != null && value !== ''
2525
+ ? Number(value)
2526
+ : null,
2527
+ }))
2528
+ }
2529
+ />
2530
+ </div>
2531
+
2532
+ <Button
2533
+ type="button"
2534
+ variant="outline"
2535
+ size="icon"
2536
+ className="h-10 w-10 shrink-0"
2537
+ disabled={!actionForm.mail_id}
2538
+ aria-label="Editar template de e-mail selecionado"
2539
+ title="Editar template de e-mail selecionado"
2540
+ onClick={() => {
2541
+ if (actionForm.mail_id) {
2542
+ void openMailTemplateEditor(actionForm.mail_id);
2543
+ }
2544
+ }}
2545
+ >
2546
+ <Pencil className="h-4 w-4" />
2547
+ </Button>
2548
+ </div>
2549
+ </Field>
2550
+ <Field label={t('emailTo')}>
2551
+ <Input
2552
+ value={actionForm.email_to}
2553
+ onChange={(event) =>
2554
+ setActionForm({
2555
+ ...actionForm,
2556
+ email_to: event.target.value,
2557
+ })
2558
+ }
2559
+ placeholder="{{email}}"
2560
+ />
2561
+ </Field>
2562
+ <Field label={t('emailCc')}>
2563
+ <Input
2564
+ value={actionForm.email_cc}
2565
+ onChange={(event) =>
2566
+ setActionForm({
2567
+ ...actionForm,
2568
+ email_cc: event.target.value,
2569
+ })
2570
+ }
2571
+ />
2572
+ </Field>
2573
+ </div>
2574
+ ) : (
2575
+ <div className="grid gap-3 md:grid-cols-2">
2576
+ <Field label={t('whatsappTargetType')}>
2577
+ <Select
2578
+ value={actionForm.whatsapp_target_type}
2579
+ onValueChange={(value) =>
2580
+ setActionForm({
2581
+ ...actionForm,
2582
+ whatsapp_target_type: value as 'phone' | 'group',
2583
+ })
2584
+ }
2585
+ >
2586
+ <SelectTrigger className="w-full">
2587
+ <SelectValue />
2588
+ </SelectTrigger>
2589
+ <SelectContent>
2590
+ <SelectItem value="phone">{t('phone')}</SelectItem>
2591
+ <SelectItem value="group">{t('group')}</SelectItem>
2592
+ </SelectContent>
2593
+ </Select>
2594
+ </Field>
2595
+ <Field label={t('whatsappTarget')}>
2596
+ <Input
2597
+ value={actionForm.whatsapp_target}
2598
+ onChange={(event) =>
2599
+ setActionForm({
2600
+ ...actionForm,
2601
+ whatsapp_target: event.target.value,
2602
+ })
2603
+ }
2604
+ placeholder="{{phone}}"
2605
+ />
2606
+ </Field>
2607
+ <Field label={t('whatsappInstance')}>
2608
+ <Input
2609
+ value={actionForm.whatsapp_instance}
2610
+ onChange={(event) =>
2611
+ setActionForm({
2612
+ ...actionForm,
2613
+ whatsapp_instance: event.target.value,
2614
+ })
2615
+ }
2616
+ />
2617
+ </Field>
2618
+ <Field label={t('whatsappBaseUrl')}>
2619
+ <Input
2620
+ value={actionForm.whatsapp_base_url}
2621
+ onChange={(event) =>
2622
+ setActionForm({
2623
+ ...actionForm,
2624
+ whatsapp_base_url: event.target.value,
2625
+ })
2626
+ }
2627
+ />
2628
+ </Field>
2629
+ <Field label={t('whatsappToken')}>
2630
+ <Input
2631
+ type="password"
2632
+ value={actionForm.whatsapp_token}
2633
+ onChange={(event) =>
2634
+ setActionForm({
2635
+ ...actionForm,
2636
+ whatsapp_token: event.target.value,
2637
+ })
2638
+ }
2639
+ />
2640
+ </Field>
2641
+ <Field label={t('whatsappTemplate')}>
2642
+ <Textarea
2643
+ value={actionForm.whatsapp_template}
2644
+ onChange={(event) =>
2645
+ setActionForm({
2646
+ ...actionForm,
2647
+ whatsapp_template: event.target.value,
2648
+ })
2649
+ }
2650
+ placeholder="Olá {{name}}"
2651
+ />
2652
+ </Field>
2653
+ </div>
2654
+ )}
2655
+
2656
+ <SheetActions
2657
+ onSave={saveAction}
2658
+ onCancel={() => setActionSheetOpen(false)}
2659
+ t={t}
2660
+ />
2661
+ </div>
2662
+ </SheetContent>
2663
+ </Sheet>
1836
2664
  </SheetContent>
1837
2665
  </Sheet>
1838
2666
 
@@ -2019,6 +2847,577 @@ export default function IntegrationsWebhooksPage() {
2019
2847
  </SheetContent>
2020
2848
  </Sheet>
2021
2849
 
2850
+ <Sheet
2851
+ open={mailProfileEditorOpen}
2852
+ onOpenChange={(open) => {
2853
+ setMailProfileEditorOpen(open);
2854
+ if (!open) {
2855
+ setMailProfileEditId(null);
2856
+ setMailProfileEditForm(defaultMailProfileEditForm);
2857
+ setMailProfileTestedSignature(null);
2858
+ setMailProfileIsTesting(false);
2859
+ }
2860
+ }}
2861
+ >
2862
+ <SheetContent className="w-full overflow-y-auto sm:max-w-4xl">
2863
+ <SheetHeader>
2864
+ <SheetTitle>Editar perfil de e-mail</SheetTitle>
2865
+ <SheetDescription>
2866
+ Atualize o perfil selecionado sem sair desta tela.
2867
+ </SheetDescription>
2868
+ </SheetHeader>
2869
+
2870
+ <div className="space-y-6 px-3 pb-8 sm:px-4">
2871
+ <div className="grid gap-6 md:grid-cols-2">
2872
+ <Field label={t('name')}>
2873
+ <Input
2874
+ value={mailProfileEditForm.name}
2875
+ onChange={(event) =>
2876
+ setMailProfileEditForm((current) => ({
2877
+ ...current,
2878
+ name: event.target.value,
2879
+ }))
2880
+ }
2881
+ />
2882
+ </Field>
2883
+
2884
+ <Field label={t('technicalName')}>
2885
+ <Input
2886
+ value={mailProfileEditForm.slug}
2887
+ onChange={(event) =>
2888
+ setMailProfileEditForm((current) => ({
2889
+ ...current,
2890
+ slug: event.target.value,
2891
+ }))
2892
+ }
2893
+ />
2894
+ </Field>
2895
+
2896
+ <Field label={t('provider')}>
2897
+ <Select
2898
+ value={mailProfileEditForm.provider}
2899
+ onValueChange={(provider: MailProfileProvider) =>
2900
+ setMailProfileEditForm((current) => ({
2901
+ ...current,
2902
+ provider,
2903
+ }))
2904
+ }
2905
+ >
2906
+ <SelectTrigger className="w-full">
2907
+ <SelectValue />
2908
+ </SelectTrigger>
2909
+ <SelectContent>
2910
+ <SelectItem value="SMTP">SMTP</SelectItem>
2911
+ <SelectItem value="GMAIL">GMAIL</SelectItem>
2912
+ <SelectItem value="SES">SES</SelectItem>
2913
+ </SelectContent>
2914
+ </Select>
2915
+ </Field>
2916
+
2917
+ <Field label="From e-mail">
2918
+ <Input
2919
+ type="email"
2920
+ value={mailProfileEditForm.from_email}
2921
+ onChange={(event) =>
2922
+ setMailProfileEditForm((current) => ({
2923
+ ...current,
2924
+ from_email: event.target.value,
2925
+ }))
2926
+ }
2927
+ />
2928
+ </Field>
2929
+
2930
+ <Field label="From name">
2931
+ <Input
2932
+ value={mailProfileEditForm.from_name}
2933
+ onChange={(event) =>
2934
+ setMailProfileEditForm((current) => ({
2935
+ ...current,
2936
+ from_name: event.target.value,
2937
+ }))
2938
+ }
2939
+ />
2940
+ </Field>
2941
+
2942
+ <Field label="Reply-to e-mail">
2943
+ <Input
2944
+ type="email"
2945
+ value={mailProfileEditForm.reply_to_email}
2946
+ onChange={(event) =>
2947
+ setMailProfileEditForm((current) => ({
2948
+ ...current,
2949
+ reply_to_email: event.target.value,
2950
+ }))
2951
+ }
2952
+ />
2953
+ </Field>
2954
+
2955
+ <Field label="Reply-to name">
2956
+ <Input
2957
+ value={mailProfileEditForm.reply_to_name}
2958
+ onChange={(event) =>
2959
+ setMailProfileEditForm((current) => ({
2960
+ ...current,
2961
+ reply_to_name: event.target.value,
2962
+ }))
2963
+ }
2964
+ />
2965
+ </Field>
2966
+ </div>
2967
+
2968
+ {mailProfileEditForm.provider === 'SMTP' ? (
2969
+ <div className="grid gap-6 md:grid-cols-2">
2970
+ <Field label="SMTP host">
2971
+ <Input
2972
+ value={mailProfileEditForm.smtp_host}
2973
+ onChange={(event) =>
2974
+ setMailProfileEditForm((current) => ({
2975
+ ...current,
2976
+ smtp_host: event.target.value,
2977
+ }))
2978
+ }
2979
+ />
2980
+ </Field>
2981
+ <Field label="SMTP port">
2982
+ <Input
2983
+ type="number"
2984
+ value={mailProfileEditForm.smtp_port}
2985
+ onChange={(event) =>
2986
+ setMailProfileEditForm((current) => ({
2987
+ ...current,
2988
+ smtp_port: event.target.value,
2989
+ }))
2990
+ }
2991
+ />
2992
+ </Field>
2993
+ <Field label="SMTP username">
2994
+ <Input
2995
+ value={mailProfileEditForm.smtp_username}
2996
+ onChange={(event) =>
2997
+ setMailProfileEditForm((current) => ({
2998
+ ...current,
2999
+ smtp_username: event.target.value,
3000
+ }))
3001
+ }
3002
+ />
3003
+ </Field>
3004
+ <Field label="SMTP password">
3005
+ <Input
3006
+ type="password"
3007
+ value={mailProfileEditForm.smtp_password}
3008
+ onChange={(event) =>
3009
+ setMailProfileEditForm((current) => ({
3010
+ ...current,
3011
+ smtp_password: event.target.value,
3012
+ }))
3013
+ }
3014
+ />
3015
+ </Field>
3016
+ <div className="rounded-md border px-4 py-3 md:col-span-2">
3017
+ <div className="flex items-center justify-between">
3018
+ <span className="text-sm">SMTP secure</span>
3019
+ <Switch
3020
+ checked={mailProfileEditForm.smtp_secure}
3021
+ onCheckedChange={(checked) =>
3022
+ setMailProfileEditForm((current) => ({
3023
+ ...current,
3024
+ smtp_secure: checked,
3025
+ }))
3026
+ }
3027
+ />
3028
+ </div>
3029
+ </div>
3030
+ </div>
3031
+ ) : null}
3032
+
3033
+ {mailProfileEditForm.provider === 'GMAIL' ? (
3034
+ <div className="grid gap-6 md:grid-cols-2">
3035
+ <Field label="Gmail client id">
3036
+ <Input
3037
+ value={mailProfileEditForm.gmail_client_id}
3038
+ onChange={(event) =>
3039
+ setMailProfileEditForm((current) => ({
3040
+ ...current,
3041
+ gmail_client_id: event.target.value,
3042
+ }))
3043
+ }
3044
+ />
3045
+ </Field>
3046
+ <Field label="Gmail client secret">
3047
+ <Input
3048
+ type="password"
3049
+ value={mailProfileEditForm.gmail_client_secret}
3050
+ onChange={(event) =>
3051
+ setMailProfileEditForm((current) => ({
3052
+ ...current,
3053
+ gmail_client_secret: event.target.value,
3054
+ }))
3055
+ }
3056
+ />
3057
+ </Field>
3058
+ <Field label="Gmail refresh token">
3059
+ <Textarea
3060
+ className="md:col-span-2"
3061
+ value={mailProfileEditForm.gmail_refresh_token}
3062
+ onChange={(event) =>
3063
+ setMailProfileEditForm((current) => ({
3064
+ ...current,
3065
+ gmail_refresh_token: event.target.value,
3066
+ }))
3067
+ }
3068
+ />
3069
+ </Field>
3070
+ </div>
3071
+ ) : null}
3072
+
3073
+ {mailProfileEditForm.provider === 'SES' ? (
3074
+ <div className="grid gap-6 md:grid-cols-2">
3075
+ <Field label="SES access key id">
3076
+ <Input
3077
+ value={mailProfileEditForm.ses_access_key_id}
3078
+ onChange={(event) =>
3079
+ setMailProfileEditForm((current) => ({
3080
+ ...current,
3081
+ ses_access_key_id: event.target.value,
3082
+ }))
3083
+ }
3084
+ />
3085
+ </Field>
3086
+ <Field label="SES secret access key">
3087
+ <Input
3088
+ type="password"
3089
+ value={mailProfileEditForm.ses_secret_access_key}
3090
+ onChange={(event) =>
3091
+ setMailProfileEditForm((current) => ({
3092
+ ...current,
3093
+ ses_secret_access_key: event.target.value,
3094
+ }))
3095
+ }
3096
+ />
3097
+ </Field>
3098
+ <Field label="SES region">
3099
+ <Input
3100
+ value={mailProfileEditForm.ses_region}
3101
+ onChange={(event) =>
3102
+ setMailProfileEditForm((current) => ({
3103
+ ...current,
3104
+ ses_region: event.target.value,
3105
+ }))
3106
+ }
3107
+ />
3108
+ </Field>
3109
+ </div>
3110
+ ) : null}
3111
+
3112
+ <div className="flex justify-end border-t pt-4">
3113
+ {canSubmitMailProfile ? (
3114
+ <Button
3115
+ type="button"
3116
+ className="min-w-40"
3117
+ onClick={saveMailProfileEditor}
3118
+ >
3119
+ Salvar alterações
3120
+ </Button>
3121
+ ) : (
3122
+ <Button
3123
+ type="button"
3124
+ className="min-w-40"
3125
+ onClick={handleTestMailProfile}
3126
+ disabled={mailProfileIsTesting}
3127
+ >
3128
+ {mailProfileIsTesting ? 'Testando...' : 'Testar perfil'}
3129
+ </Button>
3130
+ )}
3131
+ </div>
3132
+ </div>
3133
+ </SheetContent>
3134
+ </Sheet>
3135
+
3136
+ <Sheet
3137
+ open={mailTemplateEditorOpen}
3138
+ onOpenChange={(open) => {
3139
+ setMailTemplateEditorOpen(open);
3140
+ if (!open) {
3141
+ setMailTemplateEditId(null);
3142
+ setMailTemplateEditForm(defaultMailTemplateEditForm);
3143
+ }
3144
+ }}
3145
+ >
3146
+ <SheetContent className="w-full overflow-y-auto sm:max-w-4xl">
3147
+ <SheetHeader>
3148
+ <SheetTitle>Editar template de e-mail</SheetTitle>
3149
+ <SheetDescription>
3150
+ Atualize o template selecionado sem sair desta tela.
3151
+ </SheetDescription>
3152
+ </SheetHeader>
3153
+
3154
+ <div className="space-y-6 px-3 pb-8 sm:px-4">
3155
+ <div className="grid gap-6 md:grid-cols-2">
3156
+ <Field label={t('technicalName')}>
3157
+ <Input
3158
+ value={mailTemplateEditForm.slug}
3159
+ onChange={(event) =>
3160
+ setMailTemplateEditForm((current) => ({
3161
+ ...current,
3162
+ slug: event.target.value,
3163
+ }))
3164
+ }
3165
+ />
3166
+ </Field>
3167
+
3168
+ <Field label="Locale">
3169
+ <Select
3170
+ value={mailTemplateEditForm.locale_code}
3171
+ onValueChange={(localeCode) => {
3172
+ void loadMailTemplateLocale(localeCode);
3173
+ }}
3174
+ >
3175
+ <SelectTrigger className="w-full">
3176
+ <SelectValue />
3177
+ </SelectTrigger>
3178
+ <SelectContent>
3179
+ {locales.map((locale) => (
3180
+ <SelectItem key={locale.code} value={locale.code}>
3181
+ {locale.name || locale.code}
3182
+ </SelectItem>
3183
+ ))}
3184
+ </SelectContent>
3185
+ </Select>
3186
+ </Field>
3187
+ </div>
3188
+
3189
+ <Field label="Subject">
3190
+ <Input
3191
+ value={mailTemplateEditForm.subject}
3192
+ onChange={(event) =>
3193
+ setMailTemplateEditForm((current) => ({
3194
+ ...current,
3195
+ subject: event.target.value,
3196
+ }))
3197
+ }
3198
+ />
3199
+ </Field>
3200
+
3201
+ <Field label="Body">
3202
+ <RichTextEditor
3203
+ value={mailTemplateEditForm.body}
3204
+ onChange={(value) =>
3205
+ setMailTemplateEditForm((current) => ({
3206
+ ...current,
3207
+ body: value,
3208
+ }))
3209
+ }
3210
+ />
3211
+ </Field>
3212
+
3213
+ <Field label="Variaveis (uma por linha)">
3214
+ <Textarea
3215
+ className="min-h-32 font-mono text-xs"
3216
+ value={mailTemplateEditForm.variables}
3217
+ onChange={(event) =>
3218
+ setMailTemplateEditForm((current) => ({
3219
+ ...current,
3220
+ variables: event.target.value,
3221
+ }))
3222
+ }
3223
+ placeholder="first_name\ninvoice_number"
3224
+ />
3225
+ </Field>
3226
+
3227
+ <SheetActions
3228
+ onSave={saveMailTemplateEditor}
3229
+ onCancel={() => setMailTemplateEditorOpen(false)}
3230
+ t={t}
3231
+ />
3232
+ </div>
3233
+ </SheetContent>
3234
+ </Sheet>
3235
+
3236
+ <Sheet
3237
+ open={Boolean(selectedLog)}
3238
+ onOpenChange={(open) => !open && setSelectedLog(null)}
3239
+ >
3240
+ <SheetContent
3241
+ side="right"
3242
+ className="w-full overflow-y-auto sm:max-w-3xl"
3243
+ >
3244
+ <SheetHeader>
3245
+ <SheetTitle>{t('logDetailTitle')}</SheetTitle>
3246
+ <SheetDescription>
3247
+ {selectedLog
3248
+ ? formatDate(
3249
+ selectedLog.created_at,
3250
+ getSettingValue,
3251
+ currentLocaleCode
3252
+ )
3253
+ : ''}
3254
+ </SheetDescription>
3255
+ </SheetHeader>
3256
+
3257
+ {selectedLog ? (
3258
+ <div className="space-y-6 px-3 pb-8 sm:px-4">
3259
+ <div className="flex flex-wrap gap-3 pt-2">
3260
+ <Badge
3261
+ variant="outline"
3262
+ className={
3263
+ selectedLog.status === 'success'
3264
+ ? 'border-green-500/20 bg-green-500/10 text-green-600'
3265
+ : 'border-red-500/20 bg-red-500/10 text-red-600'
3266
+ }
3267
+ >
3268
+ {selectedLog.status === 'success'
3269
+ ? t('logStatusSuccess')
3270
+ : t('logStatusFailed')}
3271
+ </Badge>
3272
+ <span className="text-sm text-muted-foreground">
3273
+ {t('logDuration')}: {selectedLog.duration_ms}ms
3274
+ </span>
3275
+ <span className="text-sm text-muted-foreground">
3276
+ {t('logRemoteIp')}: {selectedLog.remote_ip || '-'}
3277
+ </span>
3278
+ </div>
3279
+
3280
+ {selectedLog.error_message ? (
3281
+ <div>
3282
+ <p className="mb-1 text-xs font-medium text-destructive">
3283
+ {t('logError')}
3284
+ </p>
3285
+ <p className="rounded-md border border-red-200 bg-red-50 px-3 py-2 text-xs text-destructive dark:border-red-900 dark:bg-red-950">
3286
+ {selectedLog.error_message}
3287
+ </p>
3288
+ </div>
3289
+ ) : null}
3290
+
3291
+ {selectedLog.payload_summary ? (
3292
+ <div>
3293
+ <p className="mb-1 text-xs font-medium text-muted-foreground">
3294
+ {t('logPayload')}
3295
+ </p>
3296
+ <pre className="max-h-56 overflow-auto rounded-md border bg-muted/40 p-3 font-mono text-xs">
3297
+ {stringifyJson(selectedLog.payload_summary)}
3298
+ </pre>
3299
+ </div>
3300
+ ) : null}
3301
+
3302
+ {selectedLog.response_summary ? (
3303
+ <div>
3304
+ <p className="mb-1 text-xs font-medium text-muted-foreground">
3305
+ {t('logResponse')}
3306
+ </p>
3307
+ <pre className="max-h-56 overflow-auto rounded-md border bg-muted/40 p-3 font-mono text-xs">
3308
+ {stringifyJson(selectedLog.response_summary)}
3309
+ </pre>
3310
+ </div>
3311
+ ) : null}
3312
+
3313
+ {selectedLog.webhook_action_log?.length ? (
3314
+ <div>
3315
+ <p className="mb-2 text-xs font-medium text-muted-foreground">
3316
+ {t('actionSection')}
3317
+ </p>
3318
+ <div className="rounded-md border">
3319
+ <Table>
3320
+ <TableHeader>
3321
+ <TableRow>
3322
+ <TableHead>{t('actionType')}</TableHead>
3323
+ <TableHead>{t('name')}</TableHead>
3324
+ <TableHead>{t('status')}</TableHead>
3325
+ <TableHead className="w-20 text-right">
3326
+ {t('logDuration')}
3327
+ </TableHead>
3328
+ </TableRow>
3329
+ </TableHeader>
3330
+ <TableBody>
3331
+ {selectedLog.webhook_action_log.map((actionLog) => (
3332
+ <TableRow key={actionLog.id}>
3333
+ <TableCell>
3334
+ <Badge variant="secondary" className="text-xs">
3335
+ {actionLog.action_type === 'email'
3336
+ ? t('email')
3337
+ : t('whatsapp')}
3338
+ </Badge>
3339
+ </TableCell>
3340
+ <TableCell className="text-sm">
3341
+ {actionLog.action_name}
3342
+ </TableCell>
3343
+ <TableCell>
3344
+ <Badge
3345
+ variant="outline"
3346
+ className={
3347
+ actionLog.status === 'success'
3348
+ ? 'border-green-500/20 bg-green-500/10 text-green-600'
3349
+ : 'border-red-500/20 bg-red-500/10 text-red-600'
3350
+ }
3351
+ >
3352
+ {actionLog.status === 'success'
3353
+ ? t('logStatusSuccess')
3354
+ : t('logStatusFailed')}
3355
+ </Badge>
3356
+ </TableCell>
3357
+ <TableCell className="text-right text-xs text-muted-foreground">
3358
+ {actionLog.duration_ms}ms
3359
+ </TableCell>
3360
+ </TableRow>
3361
+ ))}
3362
+ </TableBody>
3363
+ </Table>
3364
+ </div>
3365
+
3366
+ {selectedLog.webhook_action_log.some(
3367
+ (al) =>
3368
+ al.request_payload ||
3369
+ al.response_payload ||
3370
+ al.error_message
3371
+ ) ? (
3372
+ <div className="mt-4 space-y-4">
3373
+ {selectedLog.webhook_action_log.map((actionLog) =>
3374
+ actionLog.request_payload ||
3375
+ actionLog.response_payload ||
3376
+ actionLog.error_message ? (
3377
+ <div
3378
+ key={actionLog.id}
3379
+ className="rounded-md border p-3 text-xs"
3380
+ >
3381
+ <p className="mb-2 font-medium">
3382
+ {actionLog.action_name}
3383
+ </p>
3384
+ {actionLog.error_message ? (
3385
+ <p className="mb-2 text-destructive">
3386
+ {actionLog.error_message}
3387
+ </p>
3388
+ ) : null}
3389
+ {actionLog.request_payload ? (
3390
+ <div className="mb-2">
3391
+ <p className="mb-1 text-muted-foreground">
3392
+ {t('logRequest')}
3393
+ </p>
3394
+ <pre className="max-h-32 overflow-auto rounded border bg-muted/40 p-2 font-mono">
3395
+ {stringifyJson(actionLog.request_payload)}
3396
+ </pre>
3397
+ </div>
3398
+ ) : null}
3399
+ {actionLog.response_payload ? (
3400
+ <div>
3401
+ <p className="mb-1 text-muted-foreground">
3402
+ {t('logResponse')}
3403
+ </p>
3404
+ <pre className="max-h-32 overflow-auto rounded border bg-muted/40 p-2 font-mono">
3405
+ {stringifyJson(actionLog.response_payload)}
3406
+ </pre>
3407
+ </div>
3408
+ ) : null}
3409
+ </div>
3410
+ ) : null
3411
+ )}
3412
+ </div>
3413
+ ) : null}
3414
+ </div>
3415
+ ) : null}
3416
+ </div>
3417
+ ) : null}
3418
+ </SheetContent>
3419
+ </Sheet>
3420
+
2022
3421
  <AlertDialog
2023
3422
  open={Boolean(deleteTarget)}
2024
3423
  onOpenChange={(open) => !open && setDeleteTarget(null)}
@@ -2054,6 +3453,68 @@ function Field({ label, children }: { label: string; children: ReactNode }) {
2054
3453
  );
2055
3454
  }
2056
3455
 
3456
+ function SortableIncomingActionRow({
3457
+ action,
3458
+ t,
3459
+ onEdit,
3460
+ onDelete,
3461
+ }: {
3462
+ action: IncomingAction;
3463
+ t: (key: string) => string;
3464
+ onEdit: (action: IncomingAction) => void;
3465
+ onDelete: () => void;
3466
+ }) {
3467
+ const {
3468
+ attributes,
3469
+ listeners,
3470
+ setNodeRef,
3471
+ transform,
3472
+ transition,
3473
+ isDragging,
3474
+ } = useSortable({ id: action.id });
3475
+
3476
+ const style: React.CSSProperties = {
3477
+ transform: CSS.Transform.toString(transform),
3478
+ transition,
3479
+ opacity: isDragging ? 0.4 : undefined,
3480
+ };
3481
+
3482
+ return (
3483
+ <TableRow
3484
+ ref={setNodeRef}
3485
+ style={style}
3486
+ className={isDragging ? 'bg-muted/40' : ''}
3487
+ >
3488
+ <TableCell className="w-8 pr-0">
3489
+ <button
3490
+ type="button"
3491
+ {...attributes}
3492
+ {...listeners}
3493
+ className="flex cursor-grab touch-none items-center justify-center rounded p-1 text-muted-foreground active:cursor-grabbing"
3494
+ aria-label={t('order')}
3495
+ >
3496
+ <GripVertical className="h-4 w-4" />
3497
+ </button>
3498
+ </TableCell>
3499
+ <TableCell>{action.name}</TableCell>
3500
+ <TableCell>
3501
+ <Badge variant="secondary">
3502
+ {action.type === 'email' ? t('email') : t('whatsapp')}
3503
+ </Badge>
3504
+ </TableCell>
3505
+ <TableCell>{action.order}</TableCell>
3506
+ <TableCell className="text-right">
3507
+ <Button variant="ghost" size="icon" onClick={() => onEdit(action)}>
3508
+ <Pencil className="h-4 w-4" />
3509
+ </Button>
3510
+ <Button variant="ghost" size="icon" onClick={onDelete}>
3511
+ <Trash2 className="h-4 w-4 text-destructive" />
3512
+ </Button>
3513
+ </TableCell>
3514
+ </TableRow>
3515
+ );
3516
+ }
3517
+
2057
3518
  function StatusSelect({
2058
3519
  value,
2059
3520
  onValueChange,