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,9 +1,9 @@
1
1
  import { NextRequest, NextResponse } from 'next/server';
2
- import { auth } from '@/lib/auth';
3
2
  import { prisma } from '@/lib/prisma';
3
+ import { getAuthUser } from '@/lib/get-auth-user';
4
4
  import { logAppointmentCreated, createInteraction } from '@/lib/contact-interactions';
5
5
  import nodemailer from 'nodemailer';
6
- import { decrypt } from '@/lib/encryption';
6
+ import { decrypt, encrypt } from '@/lib/encryption';
7
7
  import { render } from '@react-email/render';
8
8
  import { MeetConfirmationEmailTemplate } from '@/components/meet-confirmation-email-template';
9
9
  import React from 'react';
@@ -22,62 +22,81 @@ function htmlToText(html: string): string {
22
22
  // GET /api/tasks - Récupérer les tâches de l'utilisateur
23
23
  export async function GET(request: NextRequest) {
24
24
  try {
25
- const session = await auth.api.getSession({
26
- headers: request.headers,
27
- });
25
+ const authUser = await getAuthUser();
28
26
 
29
- if (!session) {
27
+ if (!authUser) {
30
28
  return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
31
29
  }
32
30
 
31
+ const { session, permissions } = authUser;
32
+ const canViewAll = permissions.includes('tasks.view_all');
33
+ const canViewOwn = permissions.includes('tasks.view_own');
34
+
35
+ if (!canViewAll && !canViewOwn) {
36
+ return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
37
+ }
38
+
33
39
  const { searchParams } = new URL(request.url);
34
40
  const startDate = searchParams.get('startDate');
35
41
  const endDate = searchParams.get('endDate');
36
- const assignedTo = searchParams.get('assignedTo'); // Pour les admins
37
- const contactId = searchParams.get('contactId'); // Filtrer par contact
42
+ const assignedTo = searchParams.get('assignedTo');
43
+ const contactId = searchParams.get('contactId');
38
44
  const showOtherUsers = searchParams.get('showOtherUsers') === 'true';
39
45
 
40
- // Construire les filtres
41
- const where: any = {
42
- scheduledAt: {
43
- gte: startDate ? new Date(startDate) : new Date(),
46
+ // Construire les filtres de base
47
+ // Pour les événements multi-jours, on récupère les tâches avec une marge
48
+ // Les tâches qui commencent jusqu'à 30 jours avant le début de la période
49
+ // peuvent potentiellement être affichées (événements longs)
50
+ const baseFilters: any = {};
51
+
52
+ if (startDate) {
53
+ const startDateObj = new Date(startDate);
54
+ // Marge de 30 jours avant pour capturer les événements longs
55
+ const marginStart = new Date(startDateObj);
56
+ marginStart.setDate(marginStart.getDate() - 30);
57
+
58
+ baseFilters.scheduledAt = {
59
+ gte: marginStart,
44
60
  lte: endDate ? new Date(endDate) : undefined,
45
- },
46
- };
61
+ };
62
+
63
+ if (!endDate) {
64
+ delete baseFilters.scheduledAt.lte;
65
+ }
66
+ } else {
67
+ baseFilters.scheduledAt = {
68
+ gte: new Date(),
69
+ lte: endDate ? new Date(endDate) : undefined,
70
+ };
71
+
72
+ if (!endDate) {
73
+ delete baseFilters.scheduledAt.lte;
74
+ }
75
+ }
47
76
 
48
77
  // Filtrer par contact si fourni
49
78
  if (contactId) {
50
- where.contactId = contactId;
79
+ baseFilters.contactId = contactId;
51
80
  }
52
81
 
53
- // Vérifier la permission pour voir les événements des autres utilisateurs
54
- if (showOtherUsers) {
55
- const { checkPermission } = await import('@/lib/check-permission');
56
- const hasPermission = await checkPermission('tasks.view_other_users_events');
57
- if (hasPermission) {
58
- // Ne pas filtrer par utilisateur, voir tous les événements
59
- delete where.assignedUserId;
60
- } else {
61
- // Pas de permission, voir uniquement ses propres tâches
62
- where.assignedUserId = session.user.id;
63
- }
64
- } else if (assignedTo && assignedTo !== session.user.id) {
65
- const { checkPermission } = await import('@/lib/check-permission');
66
- const canViewOthers = await checkPermission('tasks.view_other_users_events');
67
- if (canViewOthers) {
68
- where.assignedUserId = assignedTo;
69
- } else {
70
- // Pas de permission, voir uniquement ses propres tâches
71
- where.assignedUserId = session.user.id;
82
+ // Construire les filtres d'utilisateur basés sur les permissions
83
+ let userFilters: any = {};
84
+
85
+ if (showOtherUsers && permissions.includes('tasks.view_other_users_events')) {
86
+ userFilters = {};
87
+ } else if (canViewAll) {
88
+ if (assignedTo) {
89
+ userFilters = { assignedUserId: assignedTo };
72
90
  }
73
91
  } else {
74
- // Par défaut, voir ses propres tâches
75
- where.assignedUserId = session.user.id;
92
+ userFilters = { assignedUserId: session.user.id };
76
93
  }
77
94
 
78
- if (!endDate) {
79
- delete where.scheduledAt.lte;
80
- }
95
+ // Combiner tous les filtres
96
+ const where: any = {
97
+ ...baseFilters,
98
+ ...userFilters,
99
+ };
81
100
 
82
101
  const tasks = await prisma.task.findMany({
83
102
  where,
@@ -96,7 +115,6 @@ export async function GET(request: NextRequest) {
96
115
  id: true,
97
116
  name: true,
98
117
  email: true,
99
- eventColor: true,
100
118
  },
101
119
  },
102
120
  createdBy: {
@@ -115,22 +133,32 @@ export async function GET(request: NextRequest) {
115
133
  return NextResponse.json(tasks);
116
134
  } catch (error: any) {
117
135
  console.error('Erreur lors de la récupération des tâches:', error);
118
- return NextResponse.json({ error: error.message || 'Erreur serveur' }, { status: 500 });
136
+ return NextResponse.json(
137
+ {
138
+ error:
139
+ process.env.NODE_ENV === 'development'
140
+ ? error.message || 'Erreur serveur'
141
+ : 'Erreur serveur',
142
+ },
143
+ { status: 500 },
144
+ );
119
145
  }
120
146
  }
121
147
 
122
148
  // POST /api/tasks - Créer une nouvelle tâche
123
149
  export async function POST(request: NextRequest) {
124
150
  try {
125
- const session = await auth.api.getSession({
126
- headers: request.headers,
127
- });
151
+ const [authUser, body] = await Promise.all([getAuthUser(), request.json()]);
128
152
 
129
- if (!session) {
153
+ if (!authUser) {
130
154
  return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
131
155
  }
132
156
 
133
- const body = await request.json();
157
+ const { session } = authUser;
158
+
159
+ if (!authUser.permissions.includes('tasks.create')) {
160
+ return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
161
+ }
134
162
  const {
135
163
  type,
136
164
  title,
@@ -143,6 +171,12 @@ export async function POST(request: NextRequest) {
143
171
  internalNote,
144
172
  attendees = [],
145
173
  addToGoogleCalendar = true,
174
+ // Champs pour les rendez-vous physiques
175
+ location,
176
+ locationAddress,
177
+ locationCity,
178
+ locationPostalCode,
179
+ isAtHome = false,
146
180
  } = body;
147
181
 
148
182
  // Validation
@@ -153,20 +187,20 @@ export async function POST(request: NextRequest) {
153
187
  );
154
188
  }
155
189
 
190
+ // Vérifier si l'utilisateur est admin
191
+ const user = await prisma.user.findUnique({
192
+ where: { id: session.user.id },
193
+ select: { role: true },
194
+ });
195
+
156
196
  // Déterminer l'utilisateur assigné
157
197
  let finalAssignedUserId: string;
158
- if (assignedUserId && assignedUserId !== session.user.id) {
159
- const { checkPermission: checkPerm } = await import('@/lib/check-permission');
160
- const canAssignOthers = await checkPerm('tasks.view_other_users_events');
161
- if (canAssignOthers) {
162
- // Utilisateur avec permission peut assigner à n'importe qui
163
- finalAssignedUserId = assignedUserId;
164
- } else {
165
- // Pas de permission, s'assigne automatiquement
166
- finalAssignedUserId = session.user.id;
167
- }
198
+ if (assignedUserId && user?.role === 'ADMIN') {
199
+ // Admin peut assigner à n'importe qui
200
+ finalAssignedUserId = assignedUserId;
168
201
  } else {
169
- finalAssignedUserId = assignedUserId || session.user.id;
202
+ // Utilisateur normal s'assigne automatiquement
203
+ finalAssignedUserId = session.user.id;
170
204
  }
171
205
 
172
206
  // Vérifier que le contact existe si fourni
@@ -193,6 +227,12 @@ export async function POST(request: NextRequest) {
193
227
  typeof reminderMinutesBefore === 'number' ? reminderMinutesBefore : null,
194
228
  notifyContact: notifyContact === true,
195
229
  internalNote: internalNote || null,
230
+ // Champs d'adresse pour les rendez-vous physiques
231
+ location: location || null,
232
+ locationAddress: locationAddress || null,
233
+ locationCity: locationCity || null,
234
+ locationPostalCode: locationPostalCode || null,
235
+ isAtHome: isAtHome === true,
196
236
  },
197
237
  include: {
198
238
  contact: {
@@ -209,7 +249,6 @@ export async function POST(request: NextRequest) {
209
249
  id: true,
210
250
  name: true,
211
251
  email: true,
212
- eventColor: true,
213
252
  },
214
253
  },
215
254
  createdBy: {
@@ -241,44 +280,55 @@ export async function POST(request: NextRequest) {
241
280
  // Si c'est un rendez-vous (physique) ou une tâche et que l'utilisateur veut l'ajouter à Google Calendar
242
281
  if ((type === 'MEETING' || type === 'TASK') && addToGoogleCalendar) {
243
282
  try {
244
- const googleAccount = await prisma.userGoogleAccount.findUnique({
245
- where: { userId: session.user.id },
246
- });
247
-
248
- if (googleAccount) {
249
- const accessToken = await getValidAccessToken(
250
- googleAccount.accessToken,
251
- googleAccount.refreshToken,
252
- googleAccount.tokenExpiresAt,
253
- );
254
-
255
- // Durée par défaut de 60 minutes pour les rendez-vous physiques, 30 minutes pour les tâches
256
- const startDate = new Date(scheduledAt);
257
- const duration = type === 'MEETING' ? 60 : 30;
258
- const endDate = new Date(startDate.getTime() + duration * 60 * 1000);
259
-
260
- const googleEvent = await createGoogleCalendarEvent(accessToken, {
261
- summary: title || (type === 'MEETING' ? 'Rendez-vous' : 'Tâche'),
262
- description: htmlToText(description),
263
- start: {
264
- dateTime: startDate.toISOString(),
265
- timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
266
- },
267
- end: {
268
- dateTime: endDate.toISOString(),
269
- timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
270
- },
271
- attendees: allAttendees.length > 0 ? allAttendees : undefined,
272
- });
273
-
274
- // Sauvegarder l'ID de l'évènement pour synchroniser les modifications/suppressions
275
- await prisma.task.update({
276
- where: { id: task.id },
283
+ const { getUserGoogleAccount } = await import('@/lib/google-calendar');
284
+ const googleAccount = await getUserGoogleAccount(session.user.id);
285
+
286
+ const accessToken = await getValidAccessToken(
287
+ googleAccount.accessToken,
288
+ googleAccount.refreshToken,
289
+ googleAccount.tokenExpiresAt,
290
+ );
291
+
292
+ // Mettre à jour le token si nécessaire
293
+ if (accessToken !== googleAccount.accessToken) {
294
+ const tokenExpiresAt = new Date();
295
+ tokenExpiresAt.setSeconds(tokenExpiresAt.getSeconds() + 3600);
296
+ await prisma.userGoogleAccount.update({
297
+ where: { userId: session.user.id },
277
298
  data: {
278
- googleEventId: googleEvent.id,
299
+ accessToken: encrypt(accessToken),
300
+ tokenExpiresAt,
279
301
  },
280
302
  });
281
303
  }
304
+
305
+ // Durée par défaut de 60 minutes pour les rendez-vous physiques, 30 minutes pour les tâches
306
+ const startDate = new Date(scheduledAt);
307
+ const duration = type === 'MEETING' ? 60 : 30;
308
+ const endDate = new Date(startDate.getTime() + duration * 60 * 1000);
309
+
310
+ const googleEvent = await createGoogleCalendarEvent(accessToken, {
311
+ summary: title || (type === 'MEETING' ? 'Rendez-vous' : 'Tâche'),
312
+ description: htmlToText(description),
313
+ start: {
314
+ dateTime: startDate.toISOString(),
315
+ timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
316
+ },
317
+ end: {
318
+ dateTime: endDate.toISOString(),
319
+ timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
320
+ },
321
+ attendees: allAttendees.length > 0 ? allAttendees : undefined,
322
+ location: location || undefined, // Ajouter l'adresse du rendez-vous
323
+ });
324
+
325
+ // Sauvegarder l'ID de l'évènement pour synchroniser les modifications/suppressions
326
+ await prisma.task.update({
327
+ where: { id: task.id },
328
+ data: {
329
+ googleEventId: googleEvent.id,
330
+ },
331
+ });
282
332
  } catch (googleError: any) {
283
333
  console.error('Erreur lors de la création de lévènement Google Calendar:', googleError);
284
334
  // On ne bloque pas la création de la tâche si Google Calendar échoue
@@ -299,7 +349,8 @@ export async function POST(request: NextRequest) {
299
349
  );
300
350
 
301
351
  // Envoyer un email de notification si demandé (contact ou invités)
302
- if (notifyContact) {
352
+ // Vérifier explicitement que notifyContact est true
353
+ if (notifyContact === true) {
303
354
  try {
304
355
  // Récupérer la configuration SMTP
305
356
  const smtpConfig = await prisma.smtpConfig.findUnique({
@@ -312,6 +363,7 @@ export async function POST(request: NextRequest) {
312
363
  try {
313
364
  password = decrypt(smtpConfig.password);
314
365
  } catch (error) {
366
+ console.error(error);
315
367
  password = smtpConfig.password;
316
368
  }
317
369
 
@@ -378,7 +430,7 @@ export async function POST(request: NextRequest) {
378
430
  ? `"${smtpConfig.fromName}" <${smtpConfig.fromEmail}>`
379
431
  : smtpConfig.fromEmail,
380
432
  to: recipient.email,
381
- subject: `Confirmation de rendez-vous${title ? ` : ${title}` : ''}`,
433
+ subject: `Confirmation de rendez-vous ${title ? `: ${title}` : ''}`,
382
434
  text: emailText,
383
435
  html: emailHtml,
384
436
  });
@@ -416,6 +468,14 @@ export async function POST(request: NextRequest) {
416
468
  return NextResponse.json(task, { status: 201 });
417
469
  } catch (error: any) {
418
470
  console.error('Erreur lors de la création de la tâche:', error);
419
- return NextResponse.json({ error: error.message || 'Erreur serveur' }, { status: 500 });
471
+ return NextResponse.json(
472
+ {
473
+ error:
474
+ process.env.NODE_ENV === 'development'
475
+ ? error.message || 'Erreur serveur'
476
+ : 'Erreur serveur',
477
+ },
478
+ { status: 500 },
479
+ );
420
480
  }
421
481
  }
@@ -1,8 +1,9 @@
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';
4
5
 
5
- // GET /api/templates/[id] - Récupérer un template spécifique
6
+ // GET /api/templates/[id] - Récupérer un template spécifique (commun à tous les utilisateurs)
6
7
  export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
7
8
  try {
8
9
  const session = await auth.api.getSession({
@@ -13,12 +14,16 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
13
14
  return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
14
15
  }
15
16
 
17
+ const canView = await checkPermission('templates.view');
18
+ if (!canView) {
19
+ return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
20
+ }
21
+
16
22
  const { id } = await params;
17
23
 
18
- const template = await prisma.template.findFirst({
24
+ const template = await prisma.template.findUnique({
19
25
  where: {
20
26
  id,
21
- userId: session.user.id,
22
27
  },
23
28
  });
24
29
 
@@ -33,7 +38,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
33
38
  }
34
39
  }
35
40
 
36
- // PUT /api/templates/[id] - Mettre à jour un template
41
+ // PUT /api/templates/[id] - Mettre à jour un template (commun à tous les utilisateurs)
37
42
  export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
38
43
  try {
39
44
  const session = await auth.api.getSession({
@@ -44,15 +49,18 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
44
49
  return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
45
50
  }
46
51
 
52
+ const canEdit = await checkPermission('templates.edit');
53
+ if (!canEdit) {
54
+ return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
55
+ }
56
+
47
57
  const { id } = await params;
48
58
  const body = await request.json();
49
59
  const { name, type, subject, content } = body;
50
60
 
51
- // Vérifier que le template existe et appartient à l'utilisateur
52
- const existing = await prisma.template.findFirst({
61
+ const existing = await prisma.template.findUnique({
53
62
  where: {
54
63
  id,
55
- userId: session.user.id,
56
64
  },
57
65
  });
58
66
 
@@ -60,7 +68,6 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
60
68
  return NextResponse.json({ error: 'Template non trouvé' }, { status: 404 });
61
69
  }
62
70
 
63
- // Validation
64
71
  if (!name || !type || !content) {
65
72
  return NextResponse.json(
66
73
  { error: 'Le nom, le type et le contenu sont requis' },
@@ -75,7 +82,6 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
75
82
  );
76
83
  }
77
84
 
78
- // Pour EMAIL, le sujet est requis
79
85
  if (type === 'EMAIL' && !subject) {
80
86
  return NextResponse.json(
81
87
  { error: 'Le sujet est requis pour les templates EMAIL' },
@@ -100,7 +106,7 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
100
106
  }
101
107
  }
102
108
 
103
- // DELETE /api/templates/[id] - Supprimer un template
109
+ // DELETE /api/templates/[id] - Supprimer un template (commun à tous les utilisateurs)
104
110
  export async function DELETE(
105
111
  request: NextRequest,
106
112
  { params }: { params: Promise<{ id: string }> },
@@ -114,13 +120,16 @@ export async function DELETE(
114
120
  return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
115
121
  }
116
122
 
123
+ const canDelete = await checkPermission('templates.delete');
124
+ if (!canDelete) {
125
+ return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
126
+ }
127
+
117
128
  const { id } = await params;
118
129
 
119
- // Vérifier que le template existe et appartient à l'utilisateur
120
- const existing = await prisma.template.findFirst({
130
+ const existing = await prisma.template.findUnique({
121
131
  where: {
122
132
  id,
123
- userId: session.user.id,
124
133
  },
125
134
  });
126
135
 
@@ -1,8 +1,9 @@
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';
4
5
 
5
- // GET /api/templates - Récupérer tous les templates de l'utilisateur
6
+ // GET /api/templates - Récupérer tous les templates (communs à tous les utilisateurs)
6
7
  export async function GET(request: NextRequest) {
7
8
  try {
8
9
  const session = await auth.api.getSession({
@@ -13,12 +14,15 @@ export async function GET(request: NextRequest) {
13
14
  return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
14
15
  }
15
16
 
17
+ const canView = await checkPermission('templates.view');
18
+ if (!canView) {
19
+ return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
20
+ }
21
+
16
22
  const { searchParams } = new URL(request.url);
17
23
  const type = searchParams.get('type'); // EMAIL, SMS, NOTE
18
24
 
19
- const where: any = {
20
- userId: session.user.id,
21
- };
25
+ const where: any = {};
22
26
 
23
27
  if (type) {
24
28
  where.type = type;
@@ -47,6 +51,11 @@ export async function POST(request: NextRequest) {
47
51
  return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
48
52
  }
49
53
 
54
+ const canCreate = await checkPermission('templates.create');
55
+ if (!canCreate) {
56
+ return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
57
+ }
58
+
50
59
  const body = await request.json();
51
60
  const { name, type, subject, content } = body;
52
61
 
@@ -86,6 +95,14 @@ export async function POST(request: NextRequest) {
86
95
  return NextResponse.json(template, { status: 201 });
87
96
  } catch (error: any) {
88
97
  console.error('Erreur lors de la création du template:', error);
89
- return NextResponse.json({ error: error.message || 'Erreur serveur' }, { status: 500 });
98
+ return NextResponse.json(
99
+ {
100
+ error:
101
+ process.env.NODE_ENV === 'development'
102
+ ? error.message || 'Erreur serveur'
103
+ : 'Erreur serveur',
104
+ },
105
+ { status: 500 },
106
+ );
90
107
  }
91
108
  }
@@ -0,0 +1,95 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { prisma } from '@/lib/prisma';
3
+ import { checkPermission } from '@/lib/check-permission';
4
+ import { auth } from '@/lib/auth';
5
+
6
+ export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
7
+ try {
8
+ const [session, { id }] = await Promise.all([
9
+ auth.api.getSession({ headers: request.headers }),
10
+ params,
11
+ ]);
12
+ if (!session) {
13
+ return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
14
+ }
15
+
16
+ const hasPermission = await checkPermission('users.create');
17
+ if (!hasPermission) {
18
+ return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
19
+ }
20
+
21
+ const user = await prisma.user.findUnique({
22
+ where: { id },
23
+ include: {
24
+ accounts: {
25
+ where: { providerId: 'credential' },
26
+ select: { id: true },
27
+ },
28
+ },
29
+ });
30
+
31
+ if (!user) {
32
+ return NextResponse.json({ error: 'Utilisateur non trouvé' }, { status: 404 });
33
+ }
34
+
35
+ if (user.accounts.length > 0 || user.emailVerified) {
36
+ return NextResponse.json(
37
+ { error: 'Cet utilisateur a déjà complété son inscription' },
38
+ { status: 400 },
39
+ );
40
+ }
41
+
42
+ const token = crypto.randomUUID();
43
+ const expiresAt = new Date();
44
+ expiresAt.setDate(expiresAt.getDate() + 1);
45
+
46
+ await prisma.$transaction([
47
+ prisma.verification.deleteMany({
48
+ where: { identifier: user.email },
49
+ }),
50
+ prisma.verification.create({
51
+ data: {
52
+ id: crypto.randomUUID(),
53
+ identifier: user.email,
54
+ value: token,
55
+ expiresAt,
56
+ },
57
+ }),
58
+ ]);
59
+
60
+ const baseUrl = process.env.BETTER_AUTH_URL || 'http://localhost:3000';
61
+ const invitationUrl = `${baseUrl}/invite/${token}`;
62
+
63
+ const cookieHeader = request.headers.get('cookie') || '';
64
+ const emailResponse = await fetch(`${baseUrl}/api/send`, {
65
+ method: 'POST',
66
+ headers: {
67
+ 'Content-Type': 'application/json',
68
+ Cookie: cookieHeader,
69
+ },
70
+ body: JSON.stringify({
71
+ to: user.email,
72
+ subject: 'Invitation à rejoindre le CRM',
73
+ template: 'invitation',
74
+ invitationUrl,
75
+ name: user.name,
76
+ }),
77
+ });
78
+
79
+ if (!emailResponse.ok) {
80
+ const errorData = await emailResponse.json().catch(() => ({}));
81
+ console.error("Erreur lors de l'envoi de l'email de réinvitation:", errorData);
82
+ return NextResponse.json(
83
+ { error: errorData.error || "Erreur lors de l'envoi de l'email" },
84
+ { status: 500 },
85
+ );
86
+ }
87
+
88
+ return NextResponse.json({
89
+ message: "Email d'invitation renvoyé avec succès",
90
+ });
91
+ } catch (error: any) {
92
+ console.error("Erreur lors du renvoi de l'invitation:", error);
93
+ return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
94
+ }
95
+ }
@@ -3,6 +3,7 @@ import { prisma } from '@/lib/prisma';
3
3
  import { checkPermission } from '@/lib/check-permission';
4
4
  import { auth } from '@/lib/auth';
5
5
  import { logAudit } from '@/lib/audit-log';
6
+ import { resolveRoleFromCustomRoleName } from '@/lib/roles';
6
7
 
7
8
  // GET /api/users/[id] - Récupérer un utilisateur spécifique
8
9
  export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
@@ -86,12 +87,26 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
86
87
  return NextResponse.json({ error: 'Utilisateur non trouvé' }, { status: 404 });
87
88
  }
88
89
 
89
- // Mettre à jour l'utilisateur
90
+ // Résoudre le rôle enum si le profil change
91
+ let resolvedRole: string | undefined;
92
+ if (customRoleId !== undefined) {
93
+ if (customRoleId) {
94
+ const customRole = await prisma.customRole.findUnique({
95
+ where: { id: customRoleId },
96
+ select: { name: true },
97
+ });
98
+ resolvedRole = resolveRoleFromCustomRoleName(customRole?.name);
99
+ } else {
100
+ resolvedRole = 'USER';
101
+ }
102
+ }
103
+
90
104
  const updatedUser = await prisma.user.update({
91
105
  where: { id },
92
106
  data: {
93
107
  ...(name && { name }),
94
108
  ...(customRoleId !== undefined && { customRoleId: customRoleId || null }),
109
+ ...(resolvedRole && { role: resolvedRole as any }),
95
110
  ...(typeof active === 'boolean' && { active }),
96
111
  },
97
112
  include: {