create-crm-tmp 1.1.3 → 2.1.0

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 (276) hide show
  1. package/bin/create-crm-tmp.js +56 -35
  2. package/package.json +1 -1
  3. package/template/.prettierignore +2 -0
  4. package/template/README.md +230 -115
  5. package/template/components.json +22 -0
  6. package/template/eslint.config.mjs +13 -0
  7. package/template/exemple-contacts.csv +54 -0
  8. package/template/next.config.ts +41 -1
  9. package/template/package.json +63 -15
  10. package/template/prisma/migrations/20260318095700_init_db/migration.sql +978 -0
  11. package/template/prisma/schema.prisma +311 -67
  12. package/template/src/app/(auth)/invite/[token]/page.tsx +28 -29
  13. package/template/src/app/(auth)/layout.tsx +1 -1
  14. package/template/src/app/(auth)/reset-password/complete/page.tsx +21 -27
  15. package/template/src/app/(auth)/reset-password/page.tsx +14 -10
  16. package/template/src/app/(auth)/reset-password/verify/page.tsx +14 -10
  17. package/template/src/app/(auth)/signin/page.tsx +34 -23
  18. package/template/src/app/(dashboard)/agenda/page.tsx +3655 -2357
  19. package/template/src/app/(dashboard)/automatisation/[id]/page.tsx +10 -7
  20. package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +609 -338
  21. package/template/src/app/(dashboard)/automatisation/new/page.tsx +11 -8
  22. package/template/src/app/(dashboard)/automatisation/page.tsx +463 -186
  23. package/template/src/app/(dashboard)/closing/page.tsx +517 -469
  24. package/template/src/app/(dashboard)/contacts/[id]/page.tsx +6151 -4210
  25. package/template/src/app/(dashboard)/contacts/companies/[id]/page.tsx +1702 -0
  26. package/template/src/app/(dashboard)/contacts/loading.tsx +13 -0
  27. package/template/src/app/(dashboard)/contacts/page.tsx +4124 -2130
  28. package/template/src/app/(dashboard)/dashboard/page.tsx +119 -105
  29. package/template/src/app/(dashboard)/dev/page.tsx +1291 -0
  30. package/template/src/app/(dashboard)/error.tsx +37 -0
  31. package/template/src/app/(dashboard)/layout.tsx +6 -2
  32. package/template/src/app/(dashboard)/loading.tsx +5 -0
  33. package/template/src/app/(dashboard)/settings/loading.tsx +19 -0
  34. package/template/src/app/(dashboard)/settings/page.tsx +1773 -3362
  35. package/template/src/app/(dashboard)/templates/page.tsx +504 -303
  36. package/template/src/app/(dashboard)/users/list/page.tsx +364 -355
  37. package/template/src/app/(dashboard)/users/page.tsx +279 -310
  38. package/template/src/app/(dashboard)/users/permissions/page.tsx +104 -99
  39. package/template/src/app/(dashboard)/users/roles/page.tsx +169 -140
  40. package/template/src/app/api/agenda/google-events/route.ts +92 -0
  41. package/template/src/app/api/audit-logs/route.ts +1 -1
  42. package/template/src/app/api/auth/check-active/route.ts +3 -2
  43. package/template/src/app/api/auth/google/callback/route.ts +8 -5
  44. package/template/src/app/api/auth/google/disconnect/route.ts +2 -2
  45. package/template/src/app/api/auth/google/route.ts +2 -1
  46. package/template/src/app/api/auth/google/status/route.ts +7 -31
  47. package/template/src/app/api/companies/[id]/activities/route.ts +129 -0
  48. package/template/src/app/api/companies/[id]/route.ts +194 -0
  49. package/template/src/app/api/companies/export/route.ts +206 -0
  50. package/template/src/app/api/companies/route.ts +196 -0
  51. package/template/src/app/api/contact-views/[id]/pin/route.ts +69 -0
  52. package/template/src/app/api/contact-views/[id]/route.ts +197 -0
  53. package/template/src/app/api/contact-views/route.ts +146 -0
  54. package/template/src/app/api/contacts/[id]/files/[fileId]/preview/route.ts +55 -0
  55. package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +20 -48
  56. package/template/src/app/api/contacts/[id]/files/route.ts +125 -186
  57. package/template/src/app/api/contacts/[id]/interactions/[interactionId]/route.ts +27 -1
  58. package/template/src/app/api/contacts/[id]/interactions/route.ts +45 -8
  59. package/template/src/app/api/contacts/[id]/kyc/route.ts +81 -0
  60. package/template/src/app/api/contacts/[id]/meet/route.ts +55 -29
  61. package/template/src/app/api/contacts/[id]/route.ts +184 -21
  62. package/template/src/app/api/contacts/[id]/send-email/route.ts +33 -11
  63. package/template/src/app/api/contacts/[id]/workflows/run/route.ts +67 -0
  64. package/template/src/app/api/contacts/export/route.ts +22 -31
  65. package/template/src/app/api/contacts/import/route.ts +77 -44
  66. package/template/src/app/api/contacts/import-preview/route.ts +139 -0
  67. package/template/src/app/api/contacts/origins/route.ts +63 -0
  68. package/template/src/app/api/contacts/route.ts +322 -57
  69. package/template/src/app/api/cron/cleanup-editor-images/route.ts +166 -0
  70. package/template/src/app/api/dashboard/stats/route.ts +9 -292
  71. package/template/src/app/api/dashboard/widgets/[id]/route.ts +0 -3
  72. package/template/src/app/api/dashboard/widgets/route.ts +19 -19
  73. package/template/src/app/api/dev/reminders/test/route.ts +114 -0
  74. package/template/src/app/api/editor/upload-image/route.ts +61 -0
  75. package/template/src/app/api/integrations/google-sheet/jobs/[jobId]/route.ts +47 -0
  76. package/template/src/app/api/integrations/google-sheet/jobs/usage/route.ts +50 -0
  77. package/template/src/app/api/integrations/google-sheet/sync/route.ts +28 -542
  78. package/template/src/app/api/invite/complete/route.ts +20 -23
  79. package/template/src/app/api/jobs/google-sheet/process/route.ts +84 -0
  80. package/template/src/app/api/jobs/google-sheet/schedule/route.ts +50 -0
  81. package/template/src/app/api/reminders/clear/route.ts +120 -0
  82. package/template/src/app/api/reminders/clear/undo/route.ts +112 -0
  83. package/template/src/app/api/reminders/route.ts +165 -39
  84. package/template/src/app/api/reminders/state/route.ts +164 -0
  85. package/template/src/app/api/reset-password/complete/route.ts +11 -13
  86. package/template/src/app/api/reset-password/request/route.ts +1 -1
  87. package/template/src/app/api/reset-password/verify/route.ts +1 -1
  88. package/template/src/app/api/send/route.ts +25 -47
  89. package/template/src/app/api/settings/closing-reasons/[id]/route.ts +10 -21
  90. package/template/src/app/api/settings/closing-reasons/route.ts +10 -21
  91. package/template/src/app/api/settings/company/route.ts +19 -26
  92. package/template/src/app/api/settings/google-ads/[id]/route.ts +20 -23
  93. package/template/src/app/api/settings/google-ads/route.ts +34 -23
  94. package/template/src/app/api/settings/google-calendar/calendars/route.ts +97 -0
  95. package/template/src/app/api/settings/google-calendar/route.ts +124 -0
  96. package/template/src/app/api/settings/google-sheet/[id]/route.ts +48 -23
  97. package/template/src/app/api/settings/google-sheet/auto-map/route.ts +56 -32
  98. package/template/src/app/api/settings/google-sheet/preview/route.ts +110 -0
  99. package/template/src/app/api/settings/google-sheet/route.ts +34 -23
  100. package/template/src/app/api/settings/integrations/logs/route.ts +93 -0
  101. package/template/src/app/api/settings/integrations/notifications/route.ts +67 -0
  102. package/template/src/app/api/settings/meta-leads/[id]/route.ts +20 -24
  103. package/template/src/app/api/settings/meta-leads/route.ts +34 -25
  104. package/template/src/app/api/settings/smtp/route.ts +53 -6
  105. package/template/src/app/api/settings/statuses/[id]/route.ts +29 -32
  106. package/template/src/app/api/settings/statuses/route.ts +24 -22
  107. package/template/src/app/api/statuses/route.ts +2 -5
  108. package/template/src/app/api/tasks/[id]/attendees/route.ts +36 -13
  109. package/template/src/app/api/tasks/[id]/route.ts +357 -145
  110. package/template/src/app/api/tasks/meet/route.ts +37 -26
  111. package/template/src/app/api/tasks/route.ts +201 -96
  112. package/template/src/app/api/templates/[id]/route.ts +22 -13
  113. package/template/src/app/api/templates/route.ts +22 -5
  114. package/template/src/app/api/users/[id]/resend-invite/route.ts +95 -0
  115. package/template/src/app/api/users/[id]/route.ts +22 -16
  116. package/template/src/app/api/users/commercials/route.ts +38 -0
  117. package/template/src/app/api/users/for-agenda/route.ts +1 -2
  118. package/template/src/app/api/users/list/route.ts +57 -19
  119. package/template/src/app/api/users/route.ts +89 -34
  120. package/template/src/app/api/webhooks/google-ads/route.ts +40 -1
  121. package/template/src/app/api/webhooks/meta-leads/route.ts +38 -1
  122. package/template/src/app/api/workflows/[id]/route.ts +29 -6
  123. package/template/src/app/api/workflows/process/route.ts +505 -170
  124. package/template/src/app/api/workflows/route.ts +42 -4
  125. package/template/src/app/globals.css +512 -32
  126. package/template/src/app/layout.tsx +28 -9
  127. package/template/src/app/page.tsx +37 -7
  128. package/template/src/components/address-autocomplete.tsx +233 -0
  129. package/template/src/components/config-error-alert.tsx +46 -0
  130. package/template/src/components/contacts/filter-bar.tsx +190 -0
  131. package/template/src/components/contacts/filter-builder.tsx +574 -0
  132. package/template/src/components/contacts/save-view-dialog.tsx +160 -0
  133. package/template/src/components/contacts/views-tab-bar.tsx +449 -0
  134. package/template/src/components/dashboard/activity-chart.tsx +6 -1
  135. package/template/src/components/dashboard/add-widget-dialog.tsx +13 -17
  136. package/template/src/components/dashboard/color-picker.tsx +7 -8
  137. package/template/src/components/dashboard/recent-activity.tsx +2 -5
  138. package/template/src/components/dashboard/stat-card.tsx +1 -3
  139. package/template/src/components/dashboard/status-distribution-chart.tsx +0 -1
  140. package/template/src/components/dashboard/top-contacts-list.tsx +7 -13
  141. package/template/src/components/dashboard/upcoming-tasks-list.tsx +2 -5
  142. package/template/src/components/dashboard/widget-wrapper.tsx +3 -6
  143. package/template/src/components/date-picker.tsx +399 -0
  144. package/template/src/components/editor/upload-editor-image.ts +42 -0
  145. package/template/src/components/editor.tsx +188 -35
  146. package/template/src/components/email-template.tsx +4 -2
  147. package/template/src/components/global-search.tsx +360 -0
  148. package/template/src/components/header.tsx +200 -107
  149. package/template/src/components/inactive-account-guard.tsx +58 -0
  150. package/template/src/components/integration-notifications-listener.tsx +12 -0
  151. package/template/src/components/invitation-email-template.tsx +4 -2
  152. package/template/src/components/lazy-editor.tsx +11 -0
  153. package/template/src/components/meet-cancellation-email-template.tsx +11 -3
  154. package/template/src/components/meet-confirmation-email-template.tsx +10 -3
  155. package/template/src/components/meet-update-email-template.tsx +10 -3
  156. package/template/src/components/page-header.tsx +19 -15
  157. package/template/src/components/protected-page.tsx +94 -0
  158. package/template/src/components/reset-password-email-template.tsx +4 -2
  159. package/template/src/components/settings/integrations/GoogleAdsIntegration.tsx +428 -0
  160. package/template/src/components/settings/integrations/GoogleSheetConfigMonitoringModal.tsx +680 -0
  161. package/template/src/components/settings/integrations/GoogleSheetIntegration.tsx +809 -0
  162. package/template/src/components/settings/integrations/ImportResultDialog.tsx +124 -0
  163. package/template/src/components/settings/integrations/IntegrationLogPanel.tsx +57 -0
  164. package/template/src/components/settings/integrations/IntegrationLogsTable.tsx +186 -0
  165. package/template/src/components/settings/integrations/MetaLeadIntegration.tsx +451 -0
  166. package/template/src/components/sidebar.tsx +117 -100
  167. package/template/src/components/skeleton.tsx +128 -45
  168. package/template/src/components/ui/accordion.tsx +64 -0
  169. package/template/src/components/ui/alert-dialog.tsx +139 -0
  170. package/template/src/components/ui/button.tsx +71 -0
  171. package/template/src/components/ui/components.tsx +1 -1
  172. package/template/src/components/ui/date-picker.tsx +422 -0
  173. package/template/src/components/ui/datetime-picker.tsx +338 -0
  174. package/template/src/components/ui/status-select.tsx +271 -0
  175. package/template/src/components/ui/tooltip.tsx +37 -0
  176. package/template/src/components/view-as-banner.tsx +1 -1
  177. package/template/src/components/view-as-modal.tsx +30 -19
  178. package/template/src/config/nav-pages.ts +108 -0
  179. package/template/src/contexts/app-toast-context.tsx +362 -0
  180. package/template/src/contexts/dashboard-theme-context.tsx +2 -7
  181. package/template/src/contexts/sidebar-context.tsx +27 -53
  182. package/template/src/contexts/task-reminder-context.tsx +134 -160
  183. package/template/src/contexts/view-as-context.tsx +32 -10
  184. package/template/src/hooks/use-alert.tsx +65 -0
  185. package/template/src/hooks/use-confirm.tsx +87 -0
  186. package/template/src/hooks/use-contact-views.ts +140 -0
  187. package/template/src/hooks/use-contacts.ts +69 -0
  188. package/template/src/hooks/use-fetch.ts +17 -0
  189. package/template/src/hooks/use-focus-trap.ts +73 -0
  190. package/template/src/hooks/use-statuses.ts +22 -0
  191. package/template/src/hooks/useIntegrationNotifications.ts +49 -0
  192. package/template/src/lib/address-api.ts +155 -0
  193. package/template/src/lib/auth.ts +8 -1
  194. package/template/src/lib/cache.ts +73 -0
  195. package/template/src/lib/check-permission.ts +12 -177
  196. package/template/src/lib/config-links.ts +14 -0
  197. package/template/src/lib/contact-duplicate.ts +79 -61
  198. package/template/src/lib/contact-interactions.ts +24 -22
  199. package/template/src/lib/contact-view-filters.ts +301 -0
  200. package/template/src/lib/contacts-list-url.ts +190 -0
  201. package/template/src/lib/dashboard-stats.ts +282 -0
  202. package/template/src/lib/dashboard-themes.ts +0 -5
  203. package/template/src/lib/date-utils.ts +176 -0
  204. package/template/src/lib/default-widgets.ts +0 -2
  205. package/template/src/lib/editor-html-image-dimensions.ts +172 -0
  206. package/template/src/lib/editor-image-limits.ts +19 -0
  207. package/template/src/lib/email-html-sanitize.ts +19 -0
  208. package/template/src/lib/encryption.ts +9 -6
  209. package/template/src/lib/fr-geography.ts +192 -0
  210. package/template/src/lib/get-auth-user.ts +25 -0
  211. package/template/src/lib/google-calendar-agenda.ts +201 -0
  212. package/template/src/lib/google-calendar.ts +309 -17
  213. package/template/src/lib/google-fetch.ts +63 -0
  214. package/template/src/lib/google-sheet-sync-jobs.ts +96 -0
  215. package/template/src/lib/google-sheet-sync-runner.ts +514 -0
  216. package/template/src/lib/integration-import-log.ts +21 -0
  217. package/template/src/lib/local-storage.ts +34 -0
  218. package/template/src/lib/permissions.ts +268 -40
  219. package/template/src/lib/prisma.ts +15 -12
  220. package/template/src/lib/qstash.ts +65 -0
  221. package/template/src/lib/reminder-state-server.ts +80 -0
  222. package/template/src/lib/reminder-state.ts +29 -0
  223. package/template/src/lib/roles.ts +12 -15
  224. package/template/src/lib/supabase-storage.ts +113 -0
  225. package/template/src/lib/template-variables.ts +204 -29
  226. package/template/src/lib/utils.ts +71 -11
  227. package/template/src/lib/widget-registry.ts +0 -4
  228. package/template/src/lib/workflow-executor.ts +391 -228
  229. package/template/src/proxy.ts +35 -73
  230. package/template/src/types/contact-views.ts +351 -0
  231. package/template/vercel.json +5 -0
  232. package/template/WORKFLOWS_CRON.md +0 -185
  233. package/template/prisma/migrations/20251126144728_init/migration.sql +0 -78
  234. package/template/prisma/migrations/20251126155204_add_user_roles/migration.sql +0 -5
  235. package/template/prisma/migrations/20251128095126_add_company_info/migration.sql +0 -19
  236. package/template/prisma/migrations/20251128123321_add_smtp_config/migration.sql +0 -22
  237. package/template/prisma/migrations/20251128132303_add_status/migration.sql +0 -23
  238. package/template/prisma/migrations/20251201102207_add_user_active/migration.sql +0 -75
  239. package/template/prisma/migrations/20251201105507_add_email_signature/migration.sql +0 -2
  240. package/template/prisma/migrations/20251201151122_add_tasks/migration.sql +0 -45
  241. package/template/prisma/migrations/20251202111854_add_task_reminder/migration.sql +0 -2
  242. package/template/prisma/migrations/20251202135859_add_google_meet_integration/migration.sql +0 -27
  243. package/template/prisma/migrations/20251203103317_add_meta_lead_integration/migration.sql +0 -20
  244. package/template/prisma/migrations/20251203104002_add_google_ads_integration/migration.sql +0 -18
  245. package/template/prisma/migrations/20251203112122_add_google_sheet_integration/migration.sql +0 -32
  246. package/template/prisma/migrations/20251203153853_allow_multiple_integration_configs/migration.sql +0 -20
  247. package/template/prisma/migrations/20251205141705_update_user_roles/migration.sql +0 -12
  248. package/template/prisma/migrations/20251205150000_add_commercial_and_telepro_assignment/migration.sql +0 -21
  249. package/template/prisma/migrations/20251205160000_add_interaction_logging/migration.sql +0 -11
  250. package/template/prisma/migrations/20251208090314_add_automatic_interaction_types/migration.sql +0 -12
  251. package/template/prisma/migrations/20251208094843_mg/migration.sql +0 -14
  252. package/template/prisma/migrations/20251208100000_add_company_support/migration.sql +0 -14
  253. package/template/prisma/migrations/20251208110000_add_templates/migration.sql +0 -26
  254. package/template/prisma/migrations/20251208141304_add_video_conference_task_type/migration.sql +0 -2
  255. package/template/prisma/migrations/20251209104759_add_internal_note_to_task/migration.sql +0 -2
  256. package/template/prisma/migrations/20251209134803_add_company_field/migration.sql +0 -2
  257. package/template/prisma/migrations/20251209150000_rename_company_to_company_name/migration.sql +0 -3
  258. package/template/prisma/migrations/20251209150016_add_email_tracking/migration.sql +0 -21
  259. package/template/prisma/migrations/20251209155908_add_notify_contact_to_task/migration.sql +0 -2
  260. package/template/prisma/migrations/20251210110019_add_appointment_types/migration.sql +0 -10
  261. package/template/prisma/migrations/20251210113928_add_contact_files/migration.sql +0 -26
  262. package/template/prisma/migrations/20251212132339_add_custom_roles/migration.sql +0 -24
  263. package/template/prisma/migrations/20251215104448_add_file_interaction_types/migration.sql +0 -11
  264. package/template/prisma/migrations/20251215145616_add_closing_reasons/migration.sql +0 -12
  265. package/template/prisma/migrations/20251216140850_add_log_users/migration.sql +0 -25
  266. package/template/prisma/migrations/20251216151000_rename_perdu_to_ferme/migration.sql +0 -8
  267. package/template/prisma/migrations/20251216162318_add_column_mappings_to_google_sheet/migration.sql +0 -2
  268. package/template/prisma/migrations/20251216185127_add_workflows/migration.sql +0 -80
  269. package/template/prisma/migrations/20251216192237_add_scheduled_workflow_actions/migration.sql +0 -32
  270. package/template/prisma/migrations/20251220000000_add_task_interaction_type/migration.sql +0 -4
  271. package/template/prisma/migrations/20251221000000_add_task_type/migration.sql +0 -3
  272. package/template/prisma/migrations/20251221000001_add_event_color/migration.sql +0 -23
  273. package/template/prisma/migrations/20260210114913_add_dashboard_widget/migration.sql +0 -20
  274. package/template/prisma/migrations/20260226093949_fix_cascade_on_user_delete/migration.sql +0 -69
  275. package/template/src/app/(dashboard)/users/layout.tsx +0 -30
  276. package/template/src/lib/google-drive.ts +0 -380
@@ -1,6 +1,13 @@
1
1
  import { NextRequest, NextResponse } from 'next/server';
2
2
  import { prisma } from '@/lib/prisma';
3
- import { changeStatusImmediate, createTaskImmediate } from '@/lib/workflow-executor';
3
+ import {
4
+ changeStatusImmediate,
5
+ createTaskImmediate,
6
+ assignContactImmediate,
7
+ addNoteImmediate,
8
+ notifyUserImmediate,
9
+ getContactVariables,
10
+ } from '@/lib/workflow-executor';
4
11
  import { decrypt } from '@/lib/encryption';
5
12
  import nodemailer from 'nodemailer';
6
13
  import { replaceTemplateVariables } from '@/lib/template-variables';
@@ -8,183 +15,534 @@ import { replaceTemplateVariables } from '@/lib/template-variables';
8
15
  /**
9
16
  * GET /api/workflows/process
10
17
  * Endpoint appelé par Vercel Cron pour traiter les actions planifiées
18
+ * et les workflows TIME_BASED
11
19
  */
12
20
  export async function GET(request: NextRequest) {
13
21
  try {
14
- // Vérifier l'authentification Vercel Cron
15
- // Vercel envoie automatiquement le header Authorization: Bearer <secret>
16
- // si CRON_SECRET est défini dans les variables d'environnement
22
+ const isDevelopment = process.env.NODE_ENV === 'development';
17
23
  const cronSecret = process.env.CRON_SECRET;
24
+ const authHeader = request.headers.get('authorization');
18
25
 
19
- if (cronSecret) {
20
- const authHeader = request.headers.get('authorization');
21
- const expectedAuth = `Bearer ${cronSecret}`;
22
-
23
- if (authHeader !== expectedAuth) {
24
- // En développement, on peut être plus permissif pour les tests
25
- const isDevelopment = process.env.NODE_ENV === 'development';
26
+ if (!cronSecret && !isDevelopment) {
27
+ return NextResponse.json(
28
+ { error: 'Unauthorized - CRON_SECRET manquant en production' },
29
+ { status: 401 },
30
+ );
31
+ }
26
32
 
27
- if (!isDevelopment) {
28
- return NextResponse.json(
29
- { error: 'Unauthorized - Secret manquant ou invalide' },
30
- { status: 401 },
31
- );
32
- }
33
- }
33
+ if (cronSecret && authHeader !== `Bearer ${cronSecret}` && !isDevelopment) {
34
+ return NextResponse.json(
35
+ { error: 'Unauthorized - Secret manquant ou invalide' },
36
+ { status: 401 },
37
+ );
34
38
  }
35
39
 
36
- // Récupérer les actions à exécuter maintenant
37
- // On utilise une petite marge de 5 secondes pour éviter les problèmes de timing
38
40
  const now = new Date();
39
- const margin = new Date(now.getTime() + 5000); // 5 secondes de marge
40
41
 
41
- const actionsToExecute = await prisma.scheduledWorkflowAction.findMany({
42
- where: {
43
- executed: false,
44
- executeAt: {
45
- lte: margin, // Date d'exécution <= maintenant + marge
42
+ // 1) Traiter les actions planifiées (scheduled)
43
+ const scheduledResults = await processScheduledActions(now);
44
+
45
+ // 2) Traiter les workflows TIME_BASED
46
+ const timeBasedResults = await processTimeBasedWorkflows(now);
47
+
48
+ return NextResponse.json({
49
+ success: true,
50
+ scheduled: scheduledResults,
51
+ timeBased: timeBasedResults,
52
+ timestamp: now.toISOString(),
53
+ executionTime: `${Date.now() - now.getTime()}ms`,
54
+ });
55
+ } catch (error: any) {
56
+ console.error('Erreur lors du traitement des workflows:', error);
57
+ return NextResponse.json({ error: error.message || 'Erreur serveur' }, { status: 500 });
58
+ }
59
+ }
60
+
61
+ // ---------------------------------------------------------------------------
62
+ // Traitement des actions planifiées (scheduled)
63
+ // ---------------------------------------------------------------------------
64
+
65
+ async function processScheduledActions(now: Date) {
66
+ const margin = new Date(now.getTime() + 5000);
67
+
68
+ const actionsToExecute = await prisma.scheduledWorkflowAction.findMany({
69
+ where: {
70
+ executed: false,
71
+ executeAt: { lte: margin },
72
+ },
73
+ include: {
74
+ contact: {
75
+ include: {
76
+ status: true,
77
+ company: { select: { name: true, address: true, city: true, postalCode: true } },
78
+ assignedCommercial: { select: { name: true } },
79
+ assignedTelepro: { select: { name: true } },
46
80
  },
47
81
  },
48
- include: {
49
- contact: {
50
- include: {
51
- status: true,
52
- },
82
+ workflow: {
83
+ include: {
84
+ user: { include: { smtpConfig: true } },
53
85
  },
54
- workflow: {
55
- include: {
56
- user: {
57
- include: {
58
- smtpConfig: true,
59
- },
86
+ },
87
+ },
88
+ orderBy: { executeAt: 'asc' },
89
+ take: 50,
90
+ });
91
+
92
+ if (actionsToExecute.length === 0) {
93
+ return { processed: 0, success: 0, failed: 0, errors: [] };
94
+ }
95
+
96
+ const results = {
97
+ processed: actionsToExecute.length,
98
+ success: 0,
99
+ failed: 0,
100
+ errors: [] as Array<{ id: string; error: string }>,
101
+ };
102
+
103
+ for (const scheduledAction of actionsToExecute) {
104
+ try {
105
+ const actionData = scheduledAction.actionData as any;
106
+ const contact = scheduledAction.contact;
107
+ const workflow = scheduledAction.workflow;
108
+
109
+ switch (scheduledAction.actionType) {
110
+ case 'SEND_EMAIL':
111
+ await executeScheduledEmail(scheduledAction, actionData, contact, workflow);
112
+ break;
113
+
114
+ case 'SEND_SMS':
115
+ await executeScheduledSMS(scheduledAction, actionData, contact, workflow);
116
+ break;
117
+
118
+ case 'CHANGE_STATUS':
119
+ await changeStatusImmediate(contact.id, actionData.newStatusId);
120
+ break;
121
+
122
+ case 'CREATE_TASK':
123
+ await createTaskImmediate(
124
+ {
125
+ taskTitle: actionData.taskTitle,
126
+ taskDescription: actionData.taskDescription,
127
+ taskType: actionData.taskType,
128
+ taskPriority: actionData.taskPriority,
129
+ taskAssignedUserId: actionData.taskAssignedUserId,
60
130
  },
61
- },
131
+ workflow,
132
+ contact.id,
133
+ scheduledAction.executeAt,
134
+ );
135
+ break;
136
+
137
+ case 'ASSIGN_CONTACT':
138
+ await assignContactImmediate(
139
+ contact.id,
140
+ actionData.assignCommercialId,
141
+ actionData.assignTeleproId,
142
+ );
143
+ break;
144
+
145
+ case 'ADD_NOTE': {
146
+ const variables = getContactVariables(contact);
147
+ const noteContent = replaceTemplateVariables(actionData.noteContent || '', variables);
148
+ await addNoteImmediate(
149
+ contact.id,
150
+ noteContent,
151
+ actionData.userId || workflow.userId,
152
+ actionData.workflowName || workflow.name,
153
+ );
154
+ break;
155
+ }
156
+
157
+ case 'NOTIFY_USER':
158
+ await notifyUserImmediate(
159
+ {
160
+ notifyUserId: actionData.notifyUserId,
161
+ taskTitle: actionData.taskTitle,
162
+ taskDescription: actionData.taskDescription,
163
+ },
164
+ workflow,
165
+ contact.id,
166
+ scheduledAction.executeAt,
167
+ );
168
+ break;
169
+
170
+ case 'WAIT':
171
+ break;
172
+
173
+ default:
174
+ throw new Error(`Type d'action inconnu: ${scheduledAction.actionType}`);
175
+ }
176
+
177
+ await prisma.scheduledWorkflowAction.update({
178
+ where: { id: scheduledAction.id },
179
+ data: { executed: true, executedAt: new Date() },
180
+ });
181
+
182
+ results.success++;
183
+ } catch (error: any) {
184
+ const errorMessage = error.message || 'Erreur inconnue';
185
+
186
+ console.error(
187
+ `[Workflow Cron] Erreur lors de l'exécution de l'action ${scheduledAction.id}:`,
188
+ {
189
+ actionType: scheduledAction.actionType,
190
+ contactId: scheduledAction.contactId,
191
+ workflowId: scheduledAction.workflowId,
192
+ error: errorMessage,
62
193
  },
63
- },
64
- orderBy: {
65
- executeAt: 'asc', // Traiter les actions dans l'ordre chronologique
66
- },
67
- take: 50, // Limiter à 50 actions par exécution pour éviter les timeouts (limite Vercel: 60s)
68
- });
194
+ );
69
195
 
70
- if (actionsToExecute.length === 0) {
71
- return NextResponse.json({
72
- success: true,
73
- processed: 0,
74
- message: 'Aucune action à exécuter',
75
- timestamp: now.toISOString(),
196
+ const truncatedError =
197
+ errorMessage.length > 500 ? errorMessage.substring(0, 500) + '...' : errorMessage;
198
+
199
+ await prisma.scheduledWorkflowAction.update({
200
+ where: { id: scheduledAction.id },
201
+ data: { executed: true, executedAt: new Date(), error: truncatedError },
76
202
  });
203
+
204
+ results.failed++;
205
+ results.errors.push({ id: scheduledAction.id, error: truncatedError });
77
206
  }
207
+ }
78
208
 
79
- const results = {
80
- success: 0,
81
- failed: 0,
82
- errors: [] as Array<{ id: string; error: string }>,
83
- };
209
+ return results;
210
+ }
84
211
 
85
- // Exécuter chaque action
86
- for (const scheduledAction of actionsToExecute) {
87
- try {
88
- const actionData = scheduledAction.actionData as any;
89
- const contact = scheduledAction.contact;
90
- const workflow = scheduledAction.workflow;
212
+ // ---------------------------------------------------------------------------
213
+ // Traitement des workflows TIME_BASED
214
+ // ---------------------------------------------------------------------------
215
+
216
+ async function processTimeBasedWorkflows(now: Date) {
217
+ const workflows = await prisma.workflow.findMany({
218
+ where: { active: true, triggerType: 'TIME_BASED' },
219
+ include: {
220
+ actions: {
221
+ orderBy: { order: 'asc' },
222
+ include: {
223
+ emailTemplate: true,
224
+ newStatus: true,
225
+ conditionStatus: true,
226
+ },
227
+ },
228
+ user: { include: { smtpConfig: true } },
229
+ },
230
+ });
91
231
 
92
- switch (scheduledAction.actionType) {
93
- case 'SEND_EMAIL':
94
- await executeScheduledEmail(scheduledAction, actionData, contact, workflow);
95
- break;
232
+ if (workflows.length === 0) {
233
+ return { processed: 0, triggered: 0 };
234
+ }
96
235
 
97
- case 'SEND_SMS':
98
- await executeScheduledSMS(scheduledAction, actionData, contact, workflow);
99
- break;
236
+ let triggered = 0;
100
237
 
101
- case 'CHANGE_STATUS':
102
- await changeStatusImmediate(contact.id, actionData.newStatusId);
103
- break;
238
+ for (const workflow of workflows) {
239
+ try {
240
+ const delayMs =
241
+ (workflow.triggerTimeDays || 0) * 24 * 60 * 60 * 1000 +
242
+ (workflow.triggerTimeHours || 0) * 60 * 60 * 1000;
104
243
 
105
- case 'CREATE_TASK':
106
- await createTaskImmediate(
107
- {
108
- taskTitle: actionData.taskTitle,
109
- taskDescription: actionData.taskDescription,
110
- },
111
- workflow,
112
- contact.id,
113
- scheduledAction.executeAt,
114
- );
115
- break;
244
+ if (delayMs <= 0) continue;
116
245
 
117
- case 'WAIT':
118
- // L'action "Attendre" ne fait rien, elle est juste pour le délai
119
- break;
246
+ const thresholdDate = new Date(now.getTime() - delayMs);
247
+ const windowStart = new Date(thresholdDate.getTime() - 120_000); // 2 min window
120
248
 
121
- default:
122
- throw new Error(`Type d'action inconnu: ${scheduledAction.actionType}`);
123
- }
249
+ const contacts = await findContactsForTimeBased(
250
+ workflow.triggerTimeReference,
251
+ windowStart,
252
+ thresholdDate,
253
+ );
124
254
 
125
- // Marquer comme exécutée
126
- await prisma.scheduledWorkflowAction.update({
127
- where: { id: scheduledAction.id },
128
- data: {
129
- executed: true,
130
- executedAt: new Date(),
255
+ for (const contact of contacts) {
256
+ const alreadyTriggered = await prisma.scheduledWorkflowAction.findFirst({
257
+ where: {
258
+ workflowId: workflow.id,
259
+ contactId: contact.id,
260
+ createdAt: { gte: new Date(now.getTime() - 24 * 60 * 60 * 1000) },
131
261
  },
132
262
  });
133
263
 
134
- results.success++;
135
- } catch (error: any) {
136
- const errorMessage = error.message || 'Erreur inconnue';
137
- const errorStack = error.stack || '';
138
-
139
- console.error(
140
- `[Workflow Cron] Erreur lors de l'exécution de l'action ${scheduledAction.id}:`,
141
- {
142
- actionType: scheduledAction.actionType,
143
- contactId: scheduledAction.contactId,
144
- workflowId: scheduledAction.workflowId,
145
- error: errorMessage,
146
- stack: errorStack,
147
- },
148
- );
149
-
150
- // Marquer comme échouée avec le message d'erreur
151
- // On limite la taille du message d'erreur pour éviter les problèmes de base de données
152
- const truncatedError =
153
- errorMessage.length > 500 ? errorMessage.substring(0, 500) + '...' : errorMessage;
154
-
155
- await prisma.scheduledWorkflowAction.update({
156
- where: { id: scheduledAction.id },
157
- data: {
158
- executed: true, // Marquer comme exécutée pour ne pas réessayer indéfiniment
159
- executedAt: new Date(),
160
- error: truncatedError,
161
- },
162
- });
264
+ if (alreadyTriggered) continue;
163
265
 
164
- results.failed++;
165
- results.errors.push({
166
- id: scheduledAction.id,
167
- error: truncatedError,
168
- });
266
+ const { executeWorkflowActions } = await getExecuteWorkflowActions();
267
+ await executeWorkflowActions(workflow, contact.id, contact);
268
+ triggered++;
169
269
  }
270
+ } catch (error) {
271
+ console.error(`[TIME_BASED] Erreur pour workflow ${workflow.id}:`, error);
170
272
  }
273
+ }
171
274
 
172
- return NextResponse.json({
173
- success: true,
174
- processed: actionsToExecute.length,
175
- results,
176
- timestamp: now.toISOString(),
177
- executionTime: `${Date.now() - now.getTime()}ms`,
178
- });
179
- } catch (error: any) {
180
- console.error('Erreur lors du traitement des workflows:', error);
181
- return NextResponse.json({ error: error.message || 'Erreur serveur' }, { status: 500 });
275
+ return { processed: workflows.length, triggered };
276
+ }
277
+
278
+ async function findContactsForTimeBased(
279
+ reference: string | null,
280
+ windowStart: Date,
281
+ windowEnd: Date,
282
+ ) {
283
+ switch (reference) {
284
+ case 'LAST_STATUS_CHANGE': {
285
+ const interactions = await prisma.interaction.findMany({
286
+ where: {
287
+ type: 'STATUS_CHANGE',
288
+ createdAt: { gte: windowStart, lte: windowEnd },
289
+ },
290
+ select: { contactId: true },
291
+ distinct: ['contactId'],
292
+ take: 50,
293
+ });
294
+ const contactIds = interactions.map((i) => i.contactId);
295
+ if (contactIds.length === 0) return [];
296
+ return prisma.contact.findMany({
297
+ where: { id: { in: contactIds } },
298
+ include: { status: true, company: { select: { name: true, id: true } } },
299
+ });
300
+ }
301
+
302
+ case 'LAST_INTERACTION': {
303
+ const contacts = await prisma.$queryRaw<Array<{ id: string }>>`
304
+ SELECT c.id FROM contact c
305
+ WHERE (
306
+ SELECT MAX(i."createdAt") FROM interaction i WHERE i."contactId" = c.id
307
+ ) BETWEEN ${windowStart} AND ${windowEnd}
308
+ LIMIT 50
309
+ `;
310
+ if (contacts.length === 0) return [];
311
+ return prisma.contact.findMany({
312
+ where: { id: { in: contacts.map((c) => c.id) } },
313
+ include: { status: true, company: { select: { name: true, id: true } } },
314
+ });
315
+ }
316
+
317
+ case 'CONTACT_CREATED_DATE':
318
+ default: {
319
+ return prisma.contact.findMany({
320
+ where: { createdAt: { gte: windowStart, lte: windowEnd } },
321
+ include: { status: true, company: { select: { name: true, id: true } } },
322
+ take: 50,
323
+ });
324
+ }
182
325
  }
183
326
  }
184
327
 
185
- /**
186
- * Exécute une action email planifiée
187
- */
328
+ async function getExecuteWorkflowActions() {
329
+ const mod = await import('@/lib/workflow-executor');
330
+ return {
331
+ executeWorkflowActions: async (workflow: any, contactId: string, contact: any) => {
332
+ for (const action of workflow.actions) {
333
+ if (action.conditionOperator && action.conditionStatusId) {
334
+ const conditionMet =
335
+ action.conditionOperator === 'EQUALS'
336
+ ? contact.statusId === action.conditionStatusId
337
+ : contact.statusId !== action.conditionStatusId;
338
+ if (!conditionMet) continue;
339
+ }
340
+ if (action.conditionOrigin && (contact.origin || '') !== action.conditionOrigin) continue;
341
+ if (action.conditionHasCompany === true && !contact.companyId) continue;
342
+ if (action.conditionHasCompany === false && contact.companyId) continue;
343
+
344
+ const delayMs =
345
+ (action.delayDays || 0) * 24 * 60 * 60 * 1000 + (action.delayHours || 0) * 60 * 60 * 1000;
346
+ const executeAt = new Date(Date.now() + delayMs);
347
+
348
+ const variables = getContactVariables(contact);
349
+
350
+ switch (action.actionType) {
351
+ case 'SEND_EMAIL':
352
+ if (action.emailTemplate && workflow.user.smtpConfig) {
353
+ const subject = replaceTemplateVariables(
354
+ action.emailTemplate.subject || '',
355
+ variables,
356
+ );
357
+ const content = replaceTemplateVariables(
358
+ action.emailTemplate.content || '',
359
+ variables,
360
+ );
361
+ if (executeAt <= new Date()) {
362
+ await mod.sendEmailImmediate(
363
+ workflow,
364
+ contact,
365
+ action.emailTemplate,
366
+ subject,
367
+ content,
368
+ );
369
+ } else {
370
+ await prisma.scheduledWorkflowAction.create({
371
+ data: {
372
+ workflowId: workflow.id,
373
+ actionId: action.id,
374
+ contactId: contact.id,
375
+ actionType: 'SEND_EMAIL',
376
+ actionData: {
377
+ emailTemplateId: action.emailTemplateId,
378
+ templateSubject: action.emailTemplate.subject,
379
+ templateContent: action.emailTemplate.content,
380
+ templateName: action.emailTemplate.name,
381
+ smtpConfigId: workflow.user.smtpConfig.id,
382
+ userId: workflow.userId,
383
+ workflowName: workflow.name,
384
+ },
385
+ executeAt,
386
+ },
387
+ });
388
+ }
389
+ }
390
+ break;
391
+ case 'CHANGE_STATUS':
392
+ if (action.newStatusId) {
393
+ if (executeAt <= new Date()) {
394
+ await mod.changeStatusImmediate(contactId, action.newStatusId);
395
+ } else {
396
+ await prisma.scheduledWorkflowAction.create({
397
+ data: {
398
+ workflowId: workflow.id,
399
+ actionId: action.id,
400
+ contactId,
401
+ actionType: 'CHANGE_STATUS',
402
+ actionData: { newStatusId: action.newStatusId, workflowName: workflow.name },
403
+ executeAt,
404
+ },
405
+ });
406
+ }
407
+ }
408
+ break;
409
+ case 'CREATE_TASK':
410
+ if (action.taskTitle) {
411
+ if (executeAt <= new Date()) {
412
+ await mod.createTaskImmediate(action, workflow, contactId, executeAt);
413
+ } else {
414
+ await prisma.scheduledWorkflowAction.create({
415
+ data: {
416
+ workflowId: workflow.id,
417
+ actionId: action.id,
418
+ contactId,
419
+ actionType: 'CREATE_TASK',
420
+ actionData: {
421
+ taskTitle: action.taskTitle,
422
+ taskDescription: action.taskDescription || '',
423
+ taskType: action.taskType || 'OTHER',
424
+ taskPriority: action.taskPriority || 'MEDIUM',
425
+ taskAssignedUserId: action.taskAssignedUserId || workflow.userId,
426
+ userId: workflow.userId,
427
+ workflowName: workflow.name,
428
+ },
429
+ executeAt,
430
+ },
431
+ });
432
+ }
433
+ }
434
+ break;
435
+ case 'ASSIGN_CONTACT':
436
+ if (action.assignCommercialId || action.assignTeleproId) {
437
+ if (executeAt <= new Date()) {
438
+ await mod.assignContactImmediate(
439
+ contactId,
440
+ action.assignCommercialId,
441
+ action.assignTeleproId,
442
+ );
443
+ } else {
444
+ await prisma.scheduledWorkflowAction.create({
445
+ data: {
446
+ workflowId: workflow.id,
447
+ actionId: action.id,
448
+ contactId,
449
+ actionType: 'ASSIGN_CONTACT',
450
+ actionData: {
451
+ assignCommercialId: action.assignCommercialId,
452
+ assignTeleproId: action.assignTeleproId,
453
+ workflowName: workflow.name,
454
+ },
455
+ executeAt,
456
+ },
457
+ });
458
+ }
459
+ }
460
+ break;
461
+ case 'ADD_NOTE':
462
+ if (action.noteContent) {
463
+ const noteContent = replaceTemplateVariables(action.noteContent, variables);
464
+ if (executeAt <= new Date()) {
465
+ await mod.addNoteImmediate(contactId, noteContent, workflow.userId, workflow.name);
466
+ } else {
467
+ await prisma.scheduledWorkflowAction.create({
468
+ data: {
469
+ workflowId: workflow.id,
470
+ actionId: action.id,
471
+ contactId,
472
+ actionType: 'ADD_NOTE',
473
+ actionData: {
474
+ noteContent: action.noteContent,
475
+ userId: workflow.userId,
476
+ workflowName: workflow.name,
477
+ },
478
+ executeAt,
479
+ },
480
+ });
481
+ }
482
+ }
483
+ break;
484
+ case 'NOTIFY_USER':
485
+ if (action.notifyUserId && action.taskTitle) {
486
+ if (executeAt <= new Date()) {
487
+ await mod.notifyUserImmediate(action, workflow, contactId, executeAt);
488
+ } else {
489
+ await prisma.scheduledWorkflowAction.create({
490
+ data: {
491
+ workflowId: workflow.id,
492
+ actionId: action.id,
493
+ contactId,
494
+ actionType: 'NOTIFY_USER',
495
+ actionData: {
496
+ notifyUserId: action.notifyUserId,
497
+ taskTitle: action.taskTitle,
498
+ taskDescription: action.taskDescription || '',
499
+ userId: workflow.userId,
500
+ workflowName: workflow.name,
501
+ },
502
+ executeAt,
503
+ },
504
+ });
505
+ }
506
+ }
507
+ break;
508
+ case 'SEND_SMS':
509
+ if (action.smsMessage && contact.phone) {
510
+ let message = action.smsMessage;
511
+ for (const [key, value] of Object.entries(variables)) {
512
+ message = message.replaceAll(`{${key}}`, value as string);
513
+ }
514
+ if (executeAt <= new Date()) {
515
+ await mod.sendSMSImmediate(workflow, contact, message);
516
+ } else {
517
+ await prisma.scheduledWorkflowAction.create({
518
+ data: {
519
+ workflowId: workflow.id,
520
+ actionId: action.id,
521
+ contactId: contact.id,
522
+ actionType: 'SEND_SMS',
523
+ actionData: {
524
+ smsMessage: action.smsMessage,
525
+ userId: workflow.userId,
526
+ workflowName: workflow.name,
527
+ },
528
+ executeAt,
529
+ },
530
+ });
531
+ }
532
+ }
533
+ break;
534
+ case 'WAIT':
535
+ break;
536
+ }
537
+ }
538
+ },
539
+ };
540
+ }
541
+
542
+ // ---------------------------------------------------------------------------
543
+ // Helpers scheduled email / SMS
544
+ // ---------------------------------------------------------------------------
545
+
188
546
  async function executeScheduledEmail(
189
547
  scheduledAction: any,
190
548
  actionData: any,
@@ -195,40 +553,23 @@ async function executeScheduledEmail(
195
553
  throw new Error('Configuration SMTP non trouvée');
196
554
  }
197
555
 
198
- // Remplacer les variables dans le sujet et le contenu
199
- const variables = {
200
- firstName: contact.firstName || '',
201
- lastName: contact.lastName || '',
202
- civility: contact.civility || '',
203
- email: contact.email || '',
204
- phone: contact.phone || '',
205
- secondaryPhone: contact.secondaryPhone || '',
206
- address: contact.address || '',
207
- city: contact.city || '',
208
- postalCode: contact.postalCode || '',
209
- companyName: contact.companyName || '',
210
- };
556
+ const variables = getContactVariables(contact);
211
557
 
212
558
  const subject = replaceTemplateVariables(actionData.templateSubject || '', variables);
213
559
  const content = replaceTemplateVariables(actionData.templateContent || '', variables);
214
560
 
215
- // Déchiffrer le mot de passe SMTP
216
561
  let password: string;
217
562
  try {
218
563
  password = decrypt(workflow.user.smtpConfig.password);
219
- } catch (error) {
564
+ } catch {
220
565
  password = workflow.user.smtpConfig.password;
221
566
  }
222
567
 
223
- // Créer le transporteur SMTP
224
568
  const transporter = nodemailer.createTransport({
225
569
  host: workflow.user.smtpConfig.host,
226
570
  port: workflow.user.smtpConfig.port,
227
571
  secure: workflow.user.smtpConfig.secure,
228
- auth: {
229
- user: workflow.user.smtpConfig.username,
230
- pass: password,
231
- },
572
+ auth: { user: workflow.user.smtpConfig.username, pass: password },
232
573
  });
233
574
 
234
575
  await transporter.sendMail({
@@ -240,7 +581,6 @@ async function executeScheduledEmail(
240
581
  html: content,
241
582
  });
242
583
 
243
- // Créer une interaction pour tracer l'email envoyé
244
584
  await prisma.interaction.create({
245
585
  data: {
246
586
  contactId: contact.id,
@@ -253,32 +593,27 @@ async function executeScheduledEmail(
253
593
  });
254
594
  }
255
595
 
256
- /**
257
- * Exécute une action SMS planifiée
258
- */
259
596
  async function executeScheduledSMS(
260
597
  scheduledAction: any,
261
598
  actionData: any,
262
599
  contact: any,
263
600
  workflow: any,
264
601
  ) {
265
- // Remplacer les variables dans le message
266
602
  let message = actionData.smsMessage || '';
267
603
  const variables: Record<string, string> = {
268
604
  firstName: contact.firstName || '',
269
605
  lastName: contact.lastName || '',
270
606
  email: contact.email || '',
271
607
  phone: contact.phone || '',
272
- companyName: contact.companyName || '',
608
+ companyName: contact.company?.name || '',
273
609
  };
274
610
 
275
611
  for (const [key, value] of Object.entries(variables)) {
276
- message = message.replace(new RegExp(`{${key}}`, 'g'), value);
612
+ message = message.replaceAll(`{${key}}`, value);
277
613
  }
278
614
 
279
- // TODO: Implémenter l'envoi de SMS (nécessite une API SMS)
615
+ console.log(`SMS à envoyer à ${contact.phone}: ${message}`);
280
616
 
281
- // Créer une interaction pour tracer le SMS
282
617
  await prisma.interaction.create({
283
618
  data: {
284
619
  contactId: contact.id,