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,8 @@
1
1
  import { NextRequest, NextResponse } from 'next/server';
2
2
  import { auth } from '@/lib/auth';
3
3
  import { prisma } from '@/lib/prisma';
4
+ import { checkPermission } from '@/lib/check-permission';
5
+ import { executeWorkflowsOnTaskCompleted } from '@/lib/workflow-executor';
4
6
  import {
5
7
  getValidAccessToken,
6
8
  updateGoogleCalendarEvent,
@@ -9,7 +11,7 @@ import {
9
11
  getGoogleCalendarEvent,
10
12
  } from '@/lib/google-calendar';
11
13
  import nodemailer from 'nodemailer';
12
- import { decrypt } from '@/lib/encryption';
14
+ import { decrypt, encrypt } from '@/lib/encryption';
13
15
  import { logAppointmentCancelled, logAppointmentChanged } from '@/lib/contact-interactions';
14
16
  import { render } from '@react-email/render';
15
17
  import { MeetUpdateEmailTemplate } from '@/components/meet-update-email-template';
@@ -37,6 +39,15 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
37
39
  return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
38
40
  }
39
41
 
42
+ const [canViewAll, canViewOwn] = await Promise.all([
43
+ checkPermission('tasks.view_all'),
44
+ checkPermission('tasks.view_own'),
45
+ ]);
46
+
47
+ if (!canViewAll && !canViewOwn) {
48
+ return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
49
+ }
50
+
40
51
  const { id } = await params;
41
52
 
42
53
  const task = await prisma.task.findUnique({
@@ -72,13 +83,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
72
83
  return NextResponse.json({ error: 'Tâche non trouvée' }, { status: 404 });
73
84
  }
74
85
 
75
- // Vérifier que l'utilisateur peut voir cette tâche
76
- const user = await prisma.user.findUnique({
77
- where: { id: session.user.id },
78
- select: { role: true },
79
- });
80
-
81
- if (task.assignedUserId !== session.user.id && user?.role !== 'ADMIN') {
86
+ if (task.assignedUserId !== session.user.id && !canViewAll) {
82
87
  return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
83
88
  }
84
89
 
@@ -100,6 +105,15 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
100
105
  return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
101
106
  }
102
107
 
108
+ const [canEditAll, canEditOwn] = await Promise.all([
109
+ checkPermission('tasks.edit_all'),
110
+ checkPermission('tasks.edit_own'),
111
+ ]);
112
+
113
+ if (!canEditAll && !canEditOwn) {
114
+ return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
115
+ }
116
+
103
117
  const { id } = await params;
104
118
  const body = await request.json();
105
119
  const {
@@ -115,6 +129,12 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
115
129
  attendees,
116
130
  notifyContact,
117
131
  internalNote,
132
+ // Champs d'adresse pour les rendez-vous physiques
133
+ location,
134
+ locationAddress,
135
+ locationCity,
136
+ locationPostalCode,
137
+ isAtHome,
118
138
  } = body;
119
139
 
120
140
  // Vérifier que la tâche existe
@@ -126,13 +146,7 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
126
146
  return NextResponse.json({ error: 'Tâche non trouvée' }, { status: 404 });
127
147
  }
128
148
 
129
- // Vérifier les permissions
130
- const user = await prisma.user.findUnique({
131
- where: { id: session.user.id },
132
- select: { role: true },
133
- });
134
-
135
- if (existingTask.assignedUserId !== session.user.id && user?.role !== 'ADMIN') {
149
+ if (!canEditAll && existingTask.assignedUserId !== session.user.id) {
136
150
  return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
137
151
  }
138
152
 
@@ -161,95 +175,101 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
161
175
  updateData.completedAt = completed ? new Date() : null;
162
176
  }
163
177
 
164
- // Seuls les admins peuvent changer l'assignation
165
- if (assignedUserId !== undefined && user?.role === 'ADMIN') {
178
+ // Champs d'adresse pour les rendez-vous physiques
179
+ if (location !== undefined) updateData.location = location || null;
180
+ if (locationAddress !== undefined) updateData.locationAddress = locationAddress || null;
181
+ if (locationCity !== undefined) updateData.locationCity = locationCity || null;
182
+ if (locationPostalCode !== undefined)
183
+ updateData.locationPostalCode = locationPostalCode || null;
184
+ if (isAtHome !== undefined) updateData.isAtHome = isAtHome === true;
185
+
186
+ const canAssign = await checkPermission('tasks.assign');
187
+ if (assignedUserId !== undefined && canAssign) {
166
188
  updateData.assignedUserId = assignedUserId;
167
189
  }
168
190
 
169
191
  // Si la tâche a un googleEventId, synchroniser avec Google Calendar
170
192
  if (existingTask.googleEventId) {
171
193
  try {
172
- const googleAccount = await prisma.userGoogleAccount.findUnique({
173
- where: { userId: session.user.id },
174
- });
194
+ const { getUserGoogleAccount } = await import('@/lib/google-calendar');
195
+ const googleAccount = await getUserGoogleAccount(session.user.id);
175
196
 
176
- if (googleAccount) {
177
- const accessToken = await getValidAccessToken(
178
- googleAccount.accessToken,
179
- googleAccount.refreshToken,
180
- googleAccount.tokenExpiresAt,
181
- );
182
-
183
- // Mettre à jour le token si nécessaire
184
- if (accessToken !== googleAccount.accessToken) {
185
- const tokenExpiresAt = new Date();
186
- tokenExpiresAt.setSeconds(tokenExpiresAt.getSeconds() + 3600);
187
- await prisma.userGoogleAccount.update({
188
- where: { userId: session.user.id },
189
- data: {
190
- accessToken,
191
- tokenExpiresAt,
192
- },
193
- });
194
- }
197
+ const accessToken = await getValidAccessToken(
198
+ googleAccount.accessToken,
199
+ googleAccount.refreshToken,
200
+ googleAccount.tokenExpiresAt,
201
+ );
195
202
 
196
- // Préparer les données de mise à jour pour Google Calendar
197
- const googleUpdate: any = {};
198
- if (title !== undefined) googleUpdate.summary = title;
199
- if (description !== undefined) googleUpdate.description = description;
200
-
201
- if (scheduledAt !== undefined) {
202
- const startDate = new Date(scheduledAt);
203
- const duration = durationMinutes || existingTask.durationMinutes || 30;
204
- const endDate = new Date(startDate.getTime() + duration * 60 * 1000);
205
-
206
- googleUpdate.start = {
207
- dateTime: startDate.toISOString(),
208
- timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
209
- };
210
- googleUpdate.end = {
211
- dateTime: endDate.toISOString(),
212
- timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
213
- };
214
- }
203
+ // Mettre à jour le token si nécessaire
204
+ if (accessToken !== googleAccount.accessToken) {
205
+ const tokenExpiresAt = new Date();
206
+ tokenExpiresAt.setSeconds(tokenExpiresAt.getSeconds() + 3600);
207
+ await prisma.userGoogleAccount.update({
208
+ where: { userId: session.user.id },
209
+ data: {
210
+ accessToken: encrypt(accessToken),
211
+ tokenExpiresAt,
212
+ },
213
+ });
214
+ }
215
215
 
216
- // Mettre à jour les invités si fournis
217
- if (attendees !== undefined && Array.isArray(attendees)) {
218
- // Récupérer le contact pour l'inclure dans la liste
219
- const contact = existingTask.contactId
220
- ? await prisma.contact.findUnique({
221
- where: { id: existingTask.contactId },
222
- select: { email: true },
223
- })
224
- : null;
225
-
226
- // Construire la liste des invités (contact + invités additionnels)
227
- const allAttendees = [];
228
- if (contact?.email) {
229
- allAttendees.push({ email: contact.email });
230
- }
231
- // Ajouter les autres invités (exclure le contact s'il est déjà dans la liste)
232
- attendees.forEach((email: string) => {
233
- if (email && email.trim() !== '' && email !== contact?.email) {
234
- allAttendees.push({ email: email.trim() });
235
- }
236
- });
216
+ // Préparer les données de mise à jour pour Google Calendar
217
+ const googleUpdate: any = {};
218
+ if (title !== undefined) googleUpdate.summary = title;
219
+ if (description !== undefined) googleUpdate.description = description;
220
+ if (location !== undefined) googleUpdate.location = location || undefined;
221
+
222
+ if (scheduledAt !== undefined) {
223
+ const startDate = new Date(scheduledAt);
224
+ const duration = durationMinutes || existingTask.durationMinutes || 30;
225
+ const endDate = new Date(startDate.getTime() + duration * 60 * 1000);
226
+
227
+ googleUpdate.start = {
228
+ dateTime: startDate.toISOString(),
229
+ timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
230
+ };
231
+ googleUpdate.end = {
232
+ dateTime: endDate.toISOString(),
233
+ timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
234
+ };
235
+ }
237
236
 
238
- googleUpdate.attendees = allAttendees.length > 0 ? allAttendees : undefined;
237
+ // Mettre à jour les invités si fournis
238
+ if (attendees !== undefined && Array.isArray(attendees)) {
239
+ // Récupérer le contact pour l'inclure dans la liste
240
+ const contact = existingTask.contactId
241
+ ? await prisma.contact.findUnique({
242
+ where: { id: existingTask.contactId },
243
+ select: { email: true },
244
+ })
245
+ : null;
246
+
247
+ // Construire la liste des invités (contact + invités additionnels)
248
+ const allAttendees = [];
249
+ if (contact?.email) {
250
+ allAttendees.push({ email: contact.email });
239
251
  }
252
+ // Ajouter les autres invités (exclure le contact s'il est déjà dans la liste)
253
+ attendees.forEach((email: string) => {
254
+ if (email && email.trim() !== '' && email !== contact?.email) {
255
+ allAttendees.push({ email: email.trim() });
256
+ }
257
+ });
240
258
 
241
- // Mettre à jour l'évènement Google Calendar
242
- const updatedGoogleEvent = await updateGoogleCalendarEvent(
243
- accessToken,
244
- existingTask.googleEventId,
245
- googleUpdate,
246
- );
247
-
248
- // Mettre à jour le lien Meet si nécessaire
249
- const meetLink = extractMeetLink(updatedGoogleEvent);
250
- if (meetLink) {
251
- updateData.googleMeetLink = meetLink;
252
- }
259
+ googleUpdate.attendees = allAttendees.length > 0 ? allAttendees : undefined;
260
+ }
261
+
262
+ // Mettre à jour l'évènement Google Calendar
263
+ const updatedGoogleEvent = await updateGoogleCalendarEvent(
264
+ accessToken,
265
+ existingTask.googleEventId,
266
+ googleUpdate,
267
+ );
268
+
269
+ // Mettre à jour le lien Meet si nécessaire
270
+ const meetLink = extractMeetLink(updatedGoogleEvent);
271
+ if (meetLink) {
272
+ updateData.googleMeetLink = meetLink;
253
273
  }
254
274
  } catch (googleError: any) {
255
275
  console.error('Erreur lors de la synchronisation avec Google Calendar:', googleError);
@@ -353,11 +373,10 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
353
373
  // Pour Google Meet uniquement, récupérer les invités depuis Google Calendar
354
374
  if (existingTask.googleEventId && task.googleMeetLink) {
355
375
  try {
356
- const googleAccount = await prisma.userGoogleAccount.findUnique({
357
- where: { userId: session.user.id },
358
- });
376
+ const { getUserGoogleAccount } = await import('@/lib/google-calendar');
377
+ const googleAccount = await getUserGoogleAccount(session.user.id);
359
378
 
360
- if (googleAccount && existingTask.googleEventId) {
379
+ if (existingTask.googleEventId) {
361
380
  const accessToken = await getValidAccessToken(
362
381
  googleAccount.accessToken,
363
382
  googleAccount.refreshToken,
@@ -465,6 +484,15 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
465
484
  }
466
485
  }
467
486
 
487
+ // Déclencher les workflows TASK_COMPLETED si la tâche passe à complétée et a un contact
488
+ if (completed === true && !existingTask.completed && task.contact?.id) {
489
+ try {
490
+ await executeWorkflowsOnTaskCompleted(task.contact.id, existingTask.type);
491
+ } catch (workflowError) {
492
+ console.error("Erreur lors de l'exécution des workflows:", workflowError);
493
+ }
494
+ }
495
+
468
496
  return NextResponse.json(task);
469
497
  } catch (error: any) {
470
498
  console.error('Erreur lors de la mise à jour de la tâche:', error);
@@ -486,11 +514,15 @@ export async function DELETE(
486
514
  return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
487
515
  }
488
516
 
517
+ const canDelete = await checkPermission('tasks.delete');
518
+ if (!canDelete) {
519
+ return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
520
+ }
521
+
489
522
  const { id } = await params;
490
523
  const body = await request.json().catch(() => ({}));
491
524
  const { notifyContact } = body;
492
525
 
493
- // Vérifier que la tâche existe
494
526
  const task = await prisma.task.findUnique({
495
527
  where: { id },
496
528
  });
@@ -499,13 +531,8 @@ export async function DELETE(
499
531
  return NextResponse.json({ error: 'Tâche non trouvée' }, { status: 404 });
500
532
  }
501
533
 
502
- // Vérifier les permissions
503
- const user = await prisma.user.findUnique({
504
- where: { id: session.user.id },
505
- select: { role: true },
506
- });
507
-
508
- if (task.assignedUserId !== session.user.id && user?.role !== 'ADMIN') {
534
+ const canEditAll = await checkPermission('tasks.edit_all');
535
+ if (task.assignedUserId !== session.user.id && !canEditAll) {
509
536
  return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
510
537
  }
511
538
 
@@ -544,43 +571,40 @@ export async function DELETE(
544
571
  allRecipients.push(taskWithContact.contact.email!);
545
572
 
546
573
  try {
547
- const googleAccount = await prisma.userGoogleAccount.findUnique({
548
- where: { userId: session.user.id },
549
- });
574
+ const { getUserGoogleAccount } = await import('@/lib/google-calendar');
575
+ const googleAccount = await getUserGoogleAccount(session.user.id);
550
576
 
551
- if (googleAccount) {
552
- const accessToken = await getValidAccessToken(
553
- googleAccount.accessToken,
554
- googleAccount.refreshToken,
555
- googleAccount.tokenExpiresAt,
556
- );
557
-
558
- // Mettre à jour le token si nécessaire
559
- if (accessToken !== googleAccount.accessToken) {
560
- const tokenExpiresAt = new Date();
561
- tokenExpiresAt.setSeconds(tokenExpiresAt.getSeconds() + 3600);
562
- await prisma.userGoogleAccount.update({
563
- where: { userId: session.user.id },
564
- data: {
565
- accessToken,
566
- tokenExpiresAt,
567
- },
568
- });
569
- }
577
+ const accessToken = await getValidAccessToken(
578
+ googleAccount.accessToken,
579
+ googleAccount.refreshToken,
580
+ googleAccount.tokenExpiresAt,
581
+ );
570
582
 
571
- // Récupérer les invités AVANT de supprimer l'événement
572
- const googleEvent = await getGoogleCalendarEvent(accessToken, task.googleEventId);
573
- if (googleEvent.attendees) {
574
- googleEvent.attendees.forEach((attendee) => {
575
- if (attendee.email && !allRecipients.includes(attendee.email)) {
576
- allRecipients.push(attendee.email);
577
- }
578
- });
579
- }
583
+ // Mettre à jour le token si nécessaire
584
+ if (accessToken !== googleAccount.accessToken) {
585
+ const tokenExpiresAt = new Date();
586
+ tokenExpiresAt.setSeconds(tokenExpiresAt.getSeconds() + 3600);
587
+ await prisma.userGoogleAccount.update({
588
+ where: { userId: session.user.id },
589
+ data: {
590
+ accessToken: encrypt(accessToken),
591
+ tokenExpiresAt,
592
+ },
593
+ });
594
+ }
580
595
 
581
- // Supprimer l'événement Google Calendar
582
- await deleteGoogleCalendarEvent(accessToken, task.googleEventId);
596
+ // Récupérer les invités AVANT de supprimer l'événement
597
+ const googleEvent = await getGoogleCalendarEvent(accessToken, task.googleEventId);
598
+ if (googleEvent.attendees) {
599
+ googleEvent.attendees.forEach((attendee) => {
600
+ if (attendee.email && !allRecipients.includes(attendee.email)) {
601
+ allRecipients.push(attendee.email);
602
+ }
603
+ });
583
604
  }
605
+
606
+ // Supprimer l'événement Google Calendar
607
+ await deleteGoogleCalendarEvent(accessToken, task.googleEventId);
584
608
  } catch (googleError: any) {
585
609
  console.error("Erreur lors de la suppression de l'événement Google Calendar:", googleError);
586
610
  // On continue quand même la suppression de la tâche
@@ -8,7 +8,7 @@ import {
8
8
  } from '@/lib/google-calendar';
9
9
  import { createInteraction } from '@/lib/contact-interactions';
10
10
  import nodemailer from 'nodemailer';
11
- import { decrypt } from '@/lib/encryption';
11
+ import { decrypt, encrypt } from '@/lib/encryption';
12
12
  import { render } from '@react-email/render';
13
13
  import { MeetConfirmationEmailTemplate } from '@/components/meet-confirmation-email-template';
14
14
  import React from 'react';
@@ -58,13 +58,16 @@ export async function POST(request: NextRequest) {
58
58
  // Vérifier que l'utilisateur a un compte Google connecté seulement si on veut ajouter à Google Calendar
59
59
  let googleAccount = null;
60
60
  if (addToGoogleCalendar) {
61
- googleAccount = await prisma.userGoogleAccount.findUnique({
62
- where: { userId: session.user.id },
63
- });
64
-
65
- if (!googleAccount) {
61
+ try {
62
+ const { getUserGoogleAccount } = await import('@/lib/google-calendar');
63
+ googleAccount = await getUserGoogleAccount(session.user.id);
64
+ } catch (error: any) {
66
65
  return NextResponse.json(
67
- { error: 'Veuillez connecter votre compte Google dans les paramètres' },
66
+ {
67
+ error:
68
+ error.message ||
69
+ 'Veuillez connecter votre compte Google dans les paramètres pour utiliser Google Calendar.',
70
+ },
68
71
  { status: 400 },
69
72
  );
70
73
  }
@@ -99,7 +102,7 @@ export async function POST(request: NextRequest) {
99
102
  await prisma.userGoogleAccount.update({
100
103
  where: { userId: session.user.id },
101
104
  data: {
102
- accessToken,
105
+ accessToken: encrypt(accessToken),
103
106
  tokenExpiresAt,
104
107
  },
105
108
  });