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