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
@@ -3,39 +3,55 @@ import nodemailer from 'nodemailer';
3
3
  import { decrypt } from '@/lib/encryption';
4
4
  import { replaceTemplateVariables } from '@/lib/template-variables';
5
5
 
6
+ const WORKFLOW_INCLUDE = {
7
+ actions: {
8
+ orderBy: { order: 'asc' as const },
9
+ include: {
10
+ emailTemplate: true,
11
+ newStatus: true,
12
+ conditionStatus: true,
13
+ },
14
+ },
15
+ user: {
16
+ include: {
17
+ smtpConfig: true,
18
+ },
19
+ },
20
+ };
21
+
22
+ const CONTACT_INCLUDE = {
23
+ status: true,
24
+ company: { select: { name: true, id: true } },
25
+ };
26
+
27
+ function getContactVariables(contact: any) {
28
+ return {
29
+ firstName: contact.firstName || '',
30
+ lastName: contact.lastName || '',
31
+ civility: contact.civility || '',
32
+ email: contact.email || '',
33
+ phone: contact.phone || '',
34
+ secondaryPhone: contact.secondaryPhone || '',
35
+ address: contact.address || '',
36
+ city: contact.city || '',
37
+ postalCode: contact.postalCode || '',
38
+ companyName: contact.company?.name || '',
39
+ };
40
+ }
41
+
6
42
  /**
7
43
  * Exécute les workflows déclenchés par la création d'un contact
8
44
  */
9
45
  export async function executeWorkflowsOnContactCreated(contactId: string) {
10
46
  try {
11
- // Récupérer tous les workflows actifs avec le déclencheur CONTACT_CREATED
12
47
  const workflows = await prisma.workflow.findMany({
13
- where: {
14
- active: true,
15
- triggerType: 'CONTACT_CREATED',
16
- },
17
- include: {
18
- actions: {
19
- orderBy: { order: 'asc' },
20
- include: {
21
- emailTemplate: true,
22
- newStatus: true,
23
- conditionStatus: true,
24
- },
25
- },
26
- user: {
27
- include: {
28
- smtpConfig: true,
29
- },
30
- },
31
- },
48
+ where: { active: true, triggerType: 'CONTACT_CREATED' },
49
+ include: WORKFLOW_INCLUDE,
32
50
  });
33
51
 
34
52
  const contact = await prisma.contact.findUnique({
35
53
  where: { id: contactId },
36
- include: {
37
- status: true,
38
- },
54
+ include: CONTACT_INCLUDE,
39
55
  });
40
56
 
41
57
  if (!contact) return;
@@ -57,52 +73,56 @@ export async function executeWorkflowsOnStatusChanged(
57
73
  newStatusId: string | null,
58
74
  ) {
59
75
  try {
60
- // Récupérer tous les workflows actifs avec le déclencheur STATUS_CHANGED
61
76
  const workflows = await prisma.workflow.findMany({
62
- where: {
63
- active: true,
64
- triggerType: 'STATUS_CHANGED',
65
- },
77
+ where: { active: true, triggerType: 'STATUS_CHANGED' },
66
78
  include: {
67
- actions: {
68
- orderBy: { order: 'asc' },
69
- include: {
70
- emailTemplate: true,
71
- newStatus: true,
72
- conditionStatus: true,
73
- },
74
- },
79
+ ...WORKFLOW_INCLUDE,
75
80
  triggerStatus: true,
76
81
  triggerToStatus: true,
77
- user: {
78
- include: {
79
- smtpConfig: true,
80
- },
81
- },
82
82
  },
83
83
  });
84
84
 
85
85
  const contact = await prisma.contact.findUnique({
86
86
  where: { id: contactId },
87
- include: {
88
- status: true,
89
- },
87
+ include: CONTACT_INCLUDE,
90
88
  });
91
89
 
92
90
  if (!contact) return;
93
91
 
94
- // Filtrer les workflows qui correspondent aux conditions de déclenchement
95
92
  const matchingWorkflows = workflows.filter((workflow) => {
96
- // Si le workflow spécifie un statut source, vérifier qu'il correspond
97
- if (workflow.triggerFromStatusId && oldStatusId !== workflow.triggerFromStatusId) {
93
+ if (workflow.triggerFromStatusId && oldStatusId !== workflow.triggerFromStatusId)
98
94
  return false;
99
- }
95
+ if (workflow.triggerToStatusId && newStatusId !== workflow.triggerToStatusId) return false;
96
+ return true;
97
+ });
100
98
 
101
- // Si le workflow spécifie un statut cible, vérifier qu'il correspond
102
- if (workflow.triggerToStatusId && newStatusId !== workflow.triggerToStatusId) {
103
- return false;
104
- }
99
+ for (const workflow of matchingWorkflows) {
100
+ await executeWorkflowActions(workflow, contactId, contact);
101
+ }
102
+ } catch (error) {
103
+ console.error("Erreur lors de l'exécution des workflows pour changement de statut:", error);
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Exécute les workflows déclenchés par la complétion d'une tâche
109
+ */
110
+ export async function executeWorkflowsOnTaskCompleted(contactId: string, taskType: string) {
111
+ try {
112
+ const workflows = await prisma.workflow.findMany({
113
+ where: { active: true, triggerType: 'TASK_COMPLETED' },
114
+ include: WORKFLOW_INCLUDE,
115
+ });
105
116
 
117
+ const contact = await prisma.contact.findUnique({
118
+ where: { id: contactId },
119
+ include: CONTACT_INCLUDE,
120
+ });
121
+
122
+ if (!contact) return;
123
+
124
+ const matchingWorkflows = workflows.filter((workflow) => {
125
+ if (workflow.triggerTaskType && workflow.triggerTaskType !== taskType) return false;
106
126
  return true;
107
127
  });
108
128
 
@@ -110,34 +130,155 @@ export async function executeWorkflowsOnStatusChanged(
110
130
  await executeWorkflowActions(workflow, contactId, contact);
111
131
  }
112
132
  } catch (error) {
113
- console.error("Erreur lors de l'exécution des workflows pour changement de statut:", error);
133
+ console.error("Erreur lors de l'exécution des workflows pour tâche complétée:", error);
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Exécute les workflows déclenchés par la création d'une transaction
139
+ */
140
+ export async function executeWorkflowsOnTransactionCreated(contactId: string) {
141
+ try {
142
+ const workflows = await prisma.workflow.findMany({
143
+ where: { active: true, triggerType: 'TRANSACTION_CREATED' },
144
+ include: WORKFLOW_INCLUDE,
145
+ });
146
+
147
+ const contact = await prisma.contact.findUnique({
148
+ where: { id: contactId },
149
+ include: CONTACT_INCLUDE,
150
+ });
151
+
152
+ if (!contact) return;
153
+
154
+ for (const workflow of workflows) {
155
+ await executeWorkflowActions(workflow, contactId, contact);
156
+ }
157
+ } catch (error) {
158
+ console.error("Erreur lors de l'exécution des workflows pour transaction créée:", error);
114
159
  }
115
160
  }
116
161
 
162
+ /**
163
+ * Exécute les workflows déclenchés par un changement de statut de transaction
164
+ */
165
+ export async function executeWorkflowsOnTransactionStatusChanged(
166
+ contactId: string,
167
+ oldStatus: string | null,
168
+ newStatus: string,
169
+ ) {
170
+ try {
171
+ const workflows = await prisma.workflow.findMany({
172
+ where: { active: true, triggerType: 'TRANSACTION_STATUS_CHANGED' },
173
+ include: WORKFLOW_INCLUDE,
174
+ });
175
+
176
+ const contact = await prisma.contact.findUnique({
177
+ where: { id: contactId },
178
+ include: CONTACT_INCLUDE,
179
+ });
180
+
181
+ if (!contact) return;
182
+
183
+ const matchingWorkflows = workflows.filter((workflow) => {
184
+ if (
185
+ workflow.triggerTransactionFromStatus &&
186
+ oldStatus !== workflow.triggerTransactionFromStatus
187
+ )
188
+ return false;
189
+ if (workflow.triggerTransactionToStatus && newStatus !== workflow.triggerTransactionToStatus)
190
+ return false;
191
+ return true;
192
+ });
193
+
194
+ for (const workflow of matchingWorkflows) {
195
+ await executeWorkflowActions(workflow, contactId, contact);
196
+ }
197
+ } catch (error) {
198
+ console.error(
199
+ "Erreur lors de l'exécution des workflows pour changement de statut de transaction:",
200
+ error,
201
+ );
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Exécute les workflows déclenchés par un changement d'assignation
207
+ */
208
+ export async function executeWorkflowsOnAssignmentChanged(contactId: string) {
209
+ try {
210
+ const workflows = await prisma.workflow.findMany({
211
+ where: { active: true, triggerType: 'CONTACT_ASSIGNMENT_CHANGED' },
212
+ include: WORKFLOW_INCLUDE,
213
+ });
214
+
215
+ const contact = await prisma.contact.findUnique({
216
+ where: { id: contactId },
217
+ include: CONTACT_INCLUDE,
218
+ });
219
+
220
+ if (!contact) return;
221
+
222
+ for (const workflow of workflows) {
223
+ await executeWorkflowActions(workflow, contactId, contact);
224
+ }
225
+ } catch (error) {
226
+ console.error("Erreur lors de l'exécution des workflows pour changement d'assignation:", error);
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Exécute manuellement un workflow sur un contact
232
+ */
233
+ export async function executeWorkflowManually(workflowId: string, contactId: string) {
234
+ const workflow = await prisma.workflow.findUnique({
235
+ where: { id: workflowId },
236
+ include: WORKFLOW_INCLUDE,
237
+ });
238
+
239
+ if (!workflow || !workflow.active || workflow.triggerType !== 'MANUAL') {
240
+ throw new Error('Workflow introuvable, inactif ou non manuel');
241
+ }
242
+
243
+ const contact = await prisma.contact.findUnique({
244
+ where: { id: contactId },
245
+ include: CONTACT_INCLUDE,
246
+ });
247
+
248
+ if (!contact) {
249
+ throw new Error('Contact introuvable');
250
+ }
251
+
252
+ await executeWorkflowActions(workflow, contactId, contact);
253
+ }
254
+
117
255
  /**
118
256
  * Exécute les actions d'un workflow pour un contact
119
257
  */
120
258
  async function executeWorkflowActions(workflow: any, contactId: string, contact: any) {
121
259
  for (const action of workflow.actions) {
122
- // Vérifier la condition si elle existe
260
+ // Vérifier la condition statut
123
261
  if (action.conditionOperator && action.conditionStatusId) {
124
- const contactStatusId = contact.statusId;
125
262
  const conditionMet =
126
263
  action.conditionOperator === 'EQUALS'
127
- ? contactStatusId === action.conditionStatusId
128
- : contactStatusId !== action.conditionStatusId;
264
+ ? contact.statusId === action.conditionStatusId
265
+ : contact.statusId !== action.conditionStatusId;
266
+ if (!conditionMet) continue;
267
+ }
129
268
 
130
- if (!conditionMet) {
131
- continue; // Ignorer cette action si la condition n'est pas remplie
132
- }
269
+ // Vérifier la condition origine
270
+ if (action.conditionOrigin) {
271
+ if ((contact.origin || '') !== action.conditionOrigin) continue;
133
272
  }
134
273
 
135
- // Calculer la date d'exécution avec le délai
274
+ // Vérifier la condition société
275
+ if (action.conditionHasCompany === true && !contact.companyId) continue;
276
+ if (action.conditionHasCompany === false && contact.companyId) continue;
277
+
136
278
  const delayMs =
137
279
  (action.delayDays || 0) * 24 * 60 * 60 * 1000 + (action.delayHours || 0) * 60 * 60 * 1000;
138
280
  const executeAt = new Date(Date.now() + delayMs);
139
281
 
140
- // Exécuter l'action selon son type
141
282
  switch (action.actionType) {
142
283
  case 'SEND_EMAIL':
143
284
  await executeSendEmailAction(action, workflow, contact, executeAt);
@@ -151,47 +292,39 @@ async function executeWorkflowActions(workflow: any, contactId: string, contact:
151
292
  case 'CREATE_TASK':
152
293
  await executeCreateTaskAction(action, workflow, contactId, executeAt);
153
294
  break;
295
+ case 'ASSIGN_CONTACT':
296
+ await executeAssignContactAction(action, workflow, contactId, executeAt);
297
+ break;
298
+ case 'ADD_NOTE':
299
+ await executeAddNoteAction(action, workflow, contactId, contact, executeAt);
300
+ break;
301
+ case 'NOTIFY_USER':
302
+ await executeNotifyUserAction(action, workflow, contactId, executeAt);
303
+ break;
154
304
  case 'WAIT':
155
- // L'action "Attendre" est gérée par le délai, rien à faire ici
156
305
  break;
157
306
  }
158
307
  }
159
308
  }
160
309
 
161
- /**
162
- * Exécute l'action "Envoyer un email"
163
- */
310
+ // ---------------------------------------------------------------------------
311
+ // Actions
312
+ // ---------------------------------------------------------------------------
313
+
164
314
  async function executeSendEmailAction(action: any, workflow: any, contact: any, executeAt: Date) {
165
- if (!action.emailTemplateId || !workflow.user.smtpConfig) {
166
- return;
167
- }
315
+ if (!action.emailTemplateId || !workflow.user.smtpConfig) return;
168
316
 
169
317
  try {
170
318
  const template = action.emailTemplate;
171
319
  if (!template) return;
172
320
 
173
- // Préparer les variables de template (même logique que l'envoi manuel)
174
- const variables = {
175
- firstName: contact.firstName || '',
176
- lastName: contact.lastName || '',
177
- civility: contact.civility || '',
178
- email: contact.email || '',
179
- phone: contact.phone || '',
180
- secondaryPhone: contact.secondaryPhone || '',
181
- address: contact.address || '',
182
- city: contact.city || '',
183
- postalCode: contact.postalCode || '',
184
- companyName: contact.companyName || '',
185
- };
186
-
321
+ const variables = getContactVariables(contact);
187
322
  const subject = replaceTemplateVariables(template.subject || '', variables);
188
323
  const content = replaceTemplateVariables(template.content || '', variables);
189
324
 
190
- // Si le délai est 0, envoyer immédiatement, sinon planifier
191
325
  if (executeAt <= new Date()) {
192
326
  await sendEmailImmediate(workflow, contact, template, subject, content);
193
327
  } else {
194
- // Planifier l'action pour exécution différée
195
328
  await prisma.scheduledWorkflowAction.create({
196
329
  data: {
197
330
  workflowId: workflow.id,
@@ -216,34 +349,19 @@ async function executeSendEmailAction(action: any, workflow: any, contact: any,
216
349
  }
217
350
  }
218
351
 
219
- /**
220
- * Exécute l'action "Envoyer un SMS"
221
- */
222
352
  async function executeSendSMSAction(action: any, workflow: any, contact: any, executeAt: Date) {
223
- if (!action.smsMessage || !contact.phone) {
224
- return;
225
- }
353
+ if (!action.smsMessage || !contact.phone) return;
226
354
 
227
355
  try {
228
- // Remplacer les variables dans le message
229
356
  let message = action.smsMessage;
230
- const variables: Record<string, string> = {
231
- firstName: contact.firstName || '',
232
- lastName: contact.lastName || '',
233
- email: contact.email || '',
234
- phone: contact.phone || '',
235
- companyName: contact.companyName || '',
236
- };
237
-
357
+ const variables: Record<string, string> = getContactVariables(contact);
238
358
  for (const [key, value] of Object.entries(variables)) {
239
359
  message = message.replace(new RegExp(`{${key}}`, 'g'), value);
240
360
  }
241
361
 
242
- // Si le délai est 0, envoyer immédiatement
243
362
  if (executeAt <= new Date()) {
244
363
  await sendSMSImmediate(workflow, contact, message);
245
364
  } else {
246
- // Planifier l'action pour exécution différée
247
365
  await prisma.scheduledWorkflowAction.create({
248
366
  data: {
249
367
  workflowId: workflow.id,
@@ -264,35 +382,25 @@ async function executeSendSMSAction(action: any, workflow: any, contact: any, ex
264
382
  }
265
383
  }
266
384
 
267
- /**
268
- * Exécute l'action "Changer le statut"
269
- */
270
385
  async function executeChangeStatusAction(
271
386
  action: any,
272
387
  workflow: any,
273
388
  contactId: string,
274
389
  executeAt: Date,
275
390
  ) {
276
- if (!action.newStatusId) {
277
- return;
278
- }
391
+ if (!action.newStatusId) return;
279
392
 
280
393
  try {
281
- // Si le délai est 0, changer immédiatement
282
394
  if (executeAt <= new Date()) {
283
395
  await changeStatusImmediate(contactId, action.newStatusId);
284
396
  } else {
285
- // Planifier l'action pour exécution différée
286
397
  await prisma.scheduledWorkflowAction.create({
287
398
  data: {
288
399
  workflowId: workflow.id,
289
400
  actionId: action.id,
290
- contactId: contactId,
401
+ contactId,
291
402
  actionType: 'CHANGE_STATUS',
292
- actionData: {
293
- newStatusId: action.newStatusId,
294
- workflowName: workflow.name,
295
- },
403
+ actionData: { newStatusId: action.newStatusId, workflowName: workflow.name },
296
404
  executeAt,
297
405
  },
298
406
  });
@@ -302,35 +410,30 @@ async function executeChangeStatusAction(
302
410
  }
303
411
  }
304
412
 
305
- /**
306
- * Exécute l'action "Créer une tâche"
307
- */
308
413
  async function executeCreateTaskAction(
309
414
  action: any,
310
415
  workflow: any,
311
416
  contactId: string,
312
417
  executeAt: Date,
313
418
  ) {
314
- if (!action.taskTitle) {
315
- return;
316
- }
419
+ if (!action.taskTitle) return;
317
420
 
318
421
  try {
319
- // Si le délai est 0, créer immédiatement
320
422
  if (executeAt <= new Date()) {
321
423
  await createTaskImmediate(action, workflow, contactId, executeAt);
322
424
  } else {
323
- // Planifier l'action pour exécution différée
324
425
  await prisma.scheduledWorkflowAction.create({
325
426
  data: {
326
427
  workflowId: workflow.id,
327
428
  actionId: action.id,
328
- contactId: contactId,
429
+ contactId,
329
430
  actionType: 'CREATE_TASK',
330
431
  actionData: {
331
432
  taskTitle: action.taskTitle,
332
433
  taskDescription: action.taskDescription || '',
333
- taskPriority: 'MEDIUM',
434
+ taskType: action.taskType || 'OTHER',
435
+ taskPriority: action.taskPriority || 'MEDIUM',
436
+ taskAssignedUserId: action.taskAssignedUserId || workflow.userId,
334
437
  userId: workflow.userId,
335
438
  workflowName: workflow.name,
336
439
  },
@@ -343,134 +446,247 @@ async function executeCreateTaskAction(
343
446
  }
344
447
  }
345
448
 
346
- /**
347
- * Fonctions d'exécution immédiate (utilisées aussi par le cron)
348
- */
349
-
350
- /**
351
- * Envoie un email immédiatement
352
- */
353
- export async function sendEmailImmediate(
449
+ async function executeAssignContactAction(
450
+ action: any,
354
451
  workflow: any,
355
- contact: any,
356
- template: any,
357
- subject: string,
358
- content: string,
452
+ contactId: string,
453
+ executeAt: Date,
359
454
  ) {
360
- try {
361
- if (!workflow.user.smtpConfig) {
362
- throw new Error('Configuration SMTP non trouvée');
363
- }
455
+ if (!action.assignCommercialId && !action.assignTeleproId) return;
364
456
 
365
- // Déchiffrer le mot de passe SMTP
366
- let password: string;
367
- try {
368
- password = decrypt(workflow.user.smtpConfig.password);
369
- } catch (error) {
370
- password = workflow.user.smtpConfig.password;
457
+ try {
458
+ if (executeAt <= new Date()) {
459
+ await assignContactImmediate(contactId, action.assignCommercialId, action.assignTeleproId);
460
+ } else {
461
+ await prisma.scheduledWorkflowAction.create({
462
+ data: {
463
+ workflowId: workflow.id,
464
+ actionId: action.id,
465
+ contactId,
466
+ actionType: 'ASSIGN_CONTACT',
467
+ actionData: {
468
+ assignCommercialId: action.assignCommercialId,
469
+ assignTeleproId: action.assignTeleproId,
470
+ workflowName: workflow.name,
471
+ },
472
+ executeAt,
473
+ },
474
+ });
371
475
  }
476
+ } catch (error) {
477
+ console.error("Erreur lors de l'assignation du contact:", error);
478
+ }
479
+ }
372
480
 
373
- // Créer le transporteur SMTP
374
- const transporter = nodemailer.createTransport({
375
- host: workflow.user.smtpConfig.host,
376
- port: workflow.user.smtpConfig.port,
377
- secure: workflow.user.smtpConfig.secure,
378
- auth: {
379
- user: workflow.user.smtpConfig.username,
380
- pass: password,
381
- },
382
- });
481
+ async function executeAddNoteAction(
482
+ action: any,
483
+ workflow: any,
484
+ contactId: string,
485
+ contact: any,
486
+ executeAt: Date,
487
+ ) {
488
+ if (!action.noteContent) return;
383
489
 
384
- await transporter.sendMail({
385
- from: workflow.user.smtpConfig.fromName
386
- ? `"${workflow.user.smtpConfig.fromName}" <${workflow.user.smtpConfig.fromEmail}>`
387
- : workflow.user.smtpConfig.fromEmail,
388
- to: contact.email,
389
- subject,
390
- html: content,
391
- });
490
+ try {
491
+ const variables = getContactVariables(contact);
492
+ const content = replaceTemplateVariables(action.noteContent, variables);
392
493
 
393
- // Créer une interaction pour tracer l'email envoyé
394
- await prisma.interaction.create({
395
- data: {
396
- contactId: contact.id,
397
- type: 'EMAIL',
398
- title: `Email automatique: ${template.name}`,
399
- content: `Email envoyé automatiquement via le workflow "${workflow.name}"`,
400
- userId: workflow.userId,
401
- date: new Date(),
402
- },
403
- });
494
+ if (executeAt <= new Date()) {
495
+ await addNoteImmediate(contactId, content, workflow.userId, workflow.name);
496
+ } else {
497
+ await prisma.scheduledWorkflowAction.create({
498
+ data: {
499
+ workflowId: workflow.id,
500
+ actionId: action.id,
501
+ contactId,
502
+ actionType: 'ADD_NOTE',
503
+ actionData: {
504
+ noteContent: action.noteContent,
505
+ userId: workflow.userId,
506
+ workflowName: workflow.name,
507
+ },
508
+ executeAt,
509
+ },
510
+ });
511
+ }
404
512
  } catch (error) {
405
- console.error("Erreur lors de l'envoi de l'email:", error);
406
- throw error;
513
+ console.error("Erreur lors de l'ajout de la note:", error);
407
514
  }
408
515
  }
409
516
 
410
- /**
411
- * Envoie un SMS immédiatement
412
- */
413
- export async function sendSMSImmediate(workflow: any, contact: any, message: string) {
517
+ async function executeNotifyUserAction(
518
+ action: any,
519
+ workflow: any,
520
+ contactId: string,
521
+ executeAt: Date,
522
+ ) {
523
+ if (!action.notifyUserId || !action.taskTitle) return;
524
+
414
525
  try {
415
- // TODO: Implémenter l'envoi de SMS (nécessite une API SMS)
416
- console.log(`SMS à envoyer à ${contact.phone}: ${message}`);
417
-
418
- // Créer une interaction pour tracer le SMS
419
- await prisma.interaction.create({
420
- data: {
421
- contactId: contact.id,
422
- type: 'SMS',
423
- title: 'SMS automatique',
424
- content: message,
425
- userId: workflow.userId,
426
- date: new Date(),
427
- },
428
- });
526
+ if (executeAt <= new Date()) {
527
+ await notifyUserImmediate(action, workflow, contactId, executeAt);
528
+ } else {
529
+ await prisma.scheduledWorkflowAction.create({
530
+ data: {
531
+ workflowId: workflow.id,
532
+ actionId: action.id,
533
+ contactId,
534
+ actionType: 'NOTIFY_USER',
535
+ actionData: {
536
+ notifyUserId: action.notifyUserId,
537
+ taskTitle: action.taskTitle,
538
+ taskDescription: action.taskDescription || '',
539
+ userId: workflow.userId,
540
+ workflowName: workflow.name,
541
+ },
542
+ executeAt,
543
+ },
544
+ });
545
+ }
429
546
  } catch (error) {
430
- console.error("Erreur lors de l'envoi du SMS:", error);
431
- throw error;
547
+ console.error("Erreur lors de la notification de l'utilisateur:", error);
432
548
  }
433
549
  }
434
550
 
435
- /**
436
- * Change le statut immédiatement
437
- */
438
- export async function changeStatusImmediate(contactId: string, newStatusId: string) {
551
+ // ---------------------------------------------------------------------------
552
+ // Fonctions d'exécution immédiate (utilisées aussi par le cron)
553
+ // ---------------------------------------------------------------------------
554
+
555
+ export async function sendEmailImmediate(
556
+ workflow: any,
557
+ contact: any,
558
+ template: any,
559
+ subject: string,
560
+ content: string,
561
+ ) {
562
+ if (!workflow.user.smtpConfig) {
563
+ throw new Error('Configuration SMTP non trouvée');
564
+ }
565
+
566
+ let password: string;
439
567
  try {
440
- await prisma.contact.update({
441
- where: { id: contactId },
442
- data: { statusId: newStatusId },
443
- });
444
- } catch (error) {
445
- console.error('Erreur lors du changement de statut:', error);
446
- throw error;
568
+ password = decrypt(workflow.user.smtpConfig.password);
569
+ } catch {
570
+ password = workflow.user.smtpConfig.password;
447
571
  }
572
+
573
+ const transporter = nodemailer.createTransport({
574
+ host: workflow.user.smtpConfig.host,
575
+ port: workflow.user.smtpConfig.port,
576
+ secure: workflow.user.smtpConfig.secure,
577
+ auth: { user: workflow.user.smtpConfig.username, pass: password },
578
+ });
579
+
580
+ await transporter.sendMail({
581
+ from: workflow.user.smtpConfig.fromName
582
+ ? `"${workflow.user.smtpConfig.fromName}" <${workflow.user.smtpConfig.fromEmail}>`
583
+ : workflow.user.smtpConfig.fromEmail,
584
+ to: contact.email,
585
+ subject,
586
+ html: content,
587
+ });
588
+
589
+ await prisma.interaction.create({
590
+ data: {
591
+ contactId: contact.id,
592
+ type: 'EMAIL',
593
+ title: `Email automatique: ${template.name}`,
594
+ content: `Email envoyé automatiquement via le workflow "${workflow.name}"`,
595
+ userId: workflow.userId,
596
+ date: new Date(),
597
+ },
598
+ });
599
+ }
600
+
601
+ export async function sendSMSImmediate(workflow: any, contact: any, message: string) {
602
+ console.log(`SMS à envoyer à ${contact.phone}: ${message}`);
603
+
604
+ await prisma.interaction.create({
605
+ data: {
606
+ contactId: contact.id,
607
+ type: 'SMS',
608
+ title: 'SMS automatique',
609
+ content: message,
610
+ userId: workflow.userId,
611
+ date: new Date(),
612
+ },
613
+ });
614
+ }
615
+
616
+ export async function changeStatusImmediate(contactId: string, newStatusId: string) {
617
+ await prisma.contact.update({
618
+ where: { id: contactId },
619
+ data: { statusId: newStatusId },
620
+ });
448
621
  }
449
622
 
450
- /**
451
- * Crée une tâche immédiatement
452
- */
453
623
  export async function createTaskImmediate(
454
624
  action: any,
455
625
  workflow: any,
456
626
  contactId: string,
457
627
  scheduledAt: Date,
458
628
  ) {
459
- try {
460
- await prisma.task.create({
461
- data: {
462
- type: 'OTHER',
463
- title: action.taskTitle,
464
- description: action.taskDescription || '',
465
- priority: 'MEDIUM',
466
- scheduledAt,
467
- contactId: contactId,
468
- assignedUserId: workflow.userId,
469
- createdById: workflow.userId,
470
- },
471
- });
472
- } catch (error) {
473
- console.error('Erreur lors de la création de la tâche:', error);
474
- throw error;
475
- }
629
+ await prisma.task.create({
630
+ data: {
631
+ type: action.taskType || 'OTHER',
632
+ title: action.taskTitle,
633
+ description: action.taskDescription || '',
634
+ priority: action.taskPriority || 'MEDIUM',
635
+ scheduledAt,
636
+ contactId,
637
+ assignedUserId: action.taskAssignedUserId || workflow.userId,
638
+ createdById: workflow.userId,
639
+ },
640
+ });
641
+ }
642
+
643
+ export async function assignContactImmediate(
644
+ contactId: string,
645
+ commercialId?: string | null,
646
+ teleproId?: string | null,
647
+ ) {
648
+ const data: any = {};
649
+ if (commercialId !== undefined && commercialId !== null) data.assignedCommercialId = commercialId;
650
+ if (teleproId !== undefined && teleproId !== null) data.assignedTeleproId = teleproId;
651
+ if (Object.keys(data).length === 0) return;
652
+
653
+ await prisma.contact.update({ where: { id: contactId }, data });
654
+ }
655
+
656
+ export async function addNoteImmediate(
657
+ contactId: string,
658
+ content: string,
659
+ userId: string | null,
660
+ workflowName: string,
661
+ ) {
662
+ await prisma.interaction.create({
663
+ data: {
664
+ contactId,
665
+ type: 'NOTE',
666
+ title: `Note automatique (${workflowName})`,
667
+ content,
668
+ userId,
669
+ date: new Date(),
670
+ },
671
+ });
672
+ }
673
+
674
+ export async function notifyUserImmediate(
675
+ action: any,
676
+ workflow: any,
677
+ contactId: string,
678
+ scheduledAt: Date,
679
+ ) {
680
+ await prisma.task.create({
681
+ data: {
682
+ type: 'OTHER',
683
+ title: action.taskTitle,
684
+ description: action.taskDescription || '',
685
+ priority: 'MEDIUM',
686
+ scheduledAt,
687
+ contactId,
688
+ assignedUserId: action.notifyUserId,
689
+ createdById: workflow.userId,
690
+ },
691
+ });
476
692
  }