create-crm-tmp 1.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 (187) hide show
  1. package/bin/create-crm-tmp.js +93 -0
  2. package/package.json +25 -0
  3. package/template/.prettierignore +33 -0
  4. package/template/.prettierrc.json +25 -0
  5. package/template/README.md +173 -0
  6. package/template/eslint.config.mjs +18 -0
  7. package/template/exemple-contacts.csv +11 -0
  8. package/template/next.config.ts +8 -0
  9. package/template/package.json +64 -0
  10. package/template/postcss.config.mjs +7 -0
  11. package/template/prisma/migrations/20251126144728_init/migration.sql +78 -0
  12. package/template/prisma/migrations/20251126155204_add_user_roles/migration.sql +5 -0
  13. package/template/prisma/migrations/20251128095126_add_company_info/migration.sql +19 -0
  14. package/template/prisma/migrations/20251128123321_add_smtp_config/migration.sql +22 -0
  15. package/template/prisma/migrations/20251128132303_add_status/migration.sql +23 -0
  16. package/template/prisma/migrations/20251201102207_add_user_active/migration.sql +75 -0
  17. package/template/prisma/migrations/20251201105507_add_email_signature/migration.sql +2 -0
  18. package/template/prisma/migrations/20251201151122_add_tasks/migration.sql +45 -0
  19. package/template/prisma/migrations/20251202111854_add_task_reminder/migration.sql +2 -0
  20. package/template/prisma/migrations/20251202135859_add_google_meet_integration/migration.sql +27 -0
  21. package/template/prisma/migrations/20251203103317_add_meta_lead_integration/migration.sql +20 -0
  22. package/template/prisma/migrations/20251203104002_add_google_ads_integration/migration.sql +18 -0
  23. package/template/prisma/migrations/20251203112122_add_google_sheet_integration/migration.sql +32 -0
  24. package/template/prisma/migrations/20251203153853_allow_multiple_integration_configs/migration.sql +20 -0
  25. package/template/prisma/migrations/20251205141705_update_user_roles/migration.sql +12 -0
  26. package/template/prisma/migrations/20251205150000_add_commercial_and_telepro_assignment/migration.sql +21 -0
  27. package/template/prisma/migrations/20251205160000_add_interaction_logging/migration.sql +11 -0
  28. package/template/prisma/migrations/20251208090314_add_automatic_interaction_types/migration.sql +12 -0
  29. package/template/prisma/migrations/20251208094843_mg/migration.sql +14 -0
  30. package/template/prisma/migrations/20251208100000_add_company_support/migration.sql +14 -0
  31. package/template/prisma/migrations/20251208110000_add_templates/migration.sql +26 -0
  32. package/template/prisma/migrations/20251208141304_add_video_conference_task_type/migration.sql +2 -0
  33. package/template/prisma/migrations/20251209104759_add_internal_note_to_task/migration.sql +2 -0
  34. package/template/prisma/migrations/20251209134803_add_company_field/migration.sql +2 -0
  35. package/template/prisma/migrations/20251209150000_rename_company_to_company_name/migration.sql +3 -0
  36. package/template/prisma/migrations/20251209150016_add_email_tracking/migration.sql +21 -0
  37. package/template/prisma/migrations/20251209155908_add_notify_contact_to_task/migration.sql +2 -0
  38. package/template/prisma/migrations/20251210110019_add_appointment_types/migration.sql +10 -0
  39. package/template/prisma/migrations/20251210113928_add_contact_files/migration.sql +26 -0
  40. package/template/prisma/migrations/20251212132339_add_custom_roles/migration.sql +24 -0
  41. package/template/prisma/migrations/20251215104448_add_file_interaction_types/migration.sql +11 -0
  42. package/template/prisma/migrations/20251215145616_add_closing_reasons/migration.sql +12 -0
  43. package/template/prisma/migrations/20251216140850_add_log_users/migration.sql +25 -0
  44. package/template/prisma/migrations/20251216151000_rename_perdu_to_ferme/migration.sql +8 -0
  45. package/template/prisma/migrations/20251216162318_add_column_mappings_to_google_sheet/migration.sql +2 -0
  46. package/template/prisma/migrations/20251216185127_add_workflows/migration.sql +80 -0
  47. package/template/prisma/migrations/20251216192237_add_scheduled_workflow_actions/migration.sql +32 -0
  48. package/template/prisma/migrations/migration_lock.toml +3 -0
  49. package/template/prisma/schema.prisma +582 -0
  50. package/template/prisma.config.ts +14 -0
  51. package/template/src/app/(auth)/invite/[token]/page.tsx +200 -0
  52. package/template/src/app/(auth)/layout.tsx +3 -0
  53. package/template/src/app/(auth)/reset-password/complete/page.tsx +213 -0
  54. package/template/src/app/(auth)/reset-password/page.tsx +146 -0
  55. package/template/src/app/(auth)/reset-password/verify/page.tsx +183 -0
  56. package/template/src/app/(auth)/signin/page.tsx +166 -0
  57. package/template/src/app/(dashboard)/agenda/page.tsx +3051 -0
  58. package/template/src/app/(dashboard)/automatisation/[id]/page.tsx +24 -0
  59. package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +905 -0
  60. package/template/src/app/(dashboard)/automatisation/new/page.tsx +20 -0
  61. package/template/src/app/(dashboard)/automatisation/page.tsx +337 -0
  62. package/template/src/app/(dashboard)/closing/page.tsx +1052 -0
  63. package/template/src/app/(dashboard)/contacts/[id]/page.tsx +6028 -0
  64. package/template/src/app/(dashboard)/contacts/page.tsx +3713 -0
  65. package/template/src/app/(dashboard)/dashboard/page.tsx +186 -0
  66. package/template/src/app/(dashboard)/layout.tsx +30 -0
  67. package/template/src/app/(dashboard)/settings/page.tsx +4070 -0
  68. package/template/src/app/(dashboard)/templates/page.tsx +567 -0
  69. package/template/src/app/(dashboard)/users/list/page.tsx +507 -0
  70. package/template/src/app/(dashboard)/users/page.tsx +457 -0
  71. package/template/src/app/(dashboard)/users/permissions/page.tsx +181 -0
  72. package/template/src/app/(dashboard)/users/roles/page.tsx +434 -0
  73. package/template/src/app/api/audit-logs/route.ts +57 -0
  74. package/template/src/app/api/auth/[...all]/route.ts +4 -0
  75. package/template/src/app/api/auth/check-active/route.ts +31 -0
  76. package/template/src/app/api/auth/google/callback/route.ts +94 -0
  77. package/template/src/app/api/auth/google/disconnect/route.ts +32 -0
  78. package/template/src/app/api/auth/google/route.ts +34 -0
  79. package/template/src/app/api/auth/google/status/route.ts +32 -0
  80. package/template/src/app/api/closing-reasons/route.ts +27 -0
  81. package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +94 -0
  82. package/template/src/app/api/contacts/[id]/files/route.ts +269 -0
  83. package/template/src/app/api/contacts/[id]/interactions/[interactionId]/route.ts +91 -0
  84. package/template/src/app/api/contacts/[id]/interactions/route.ts +103 -0
  85. package/template/src/app/api/contacts/[id]/meet/route.ts +296 -0
  86. package/template/src/app/api/contacts/[id]/route.ts +322 -0
  87. package/template/src/app/api/contacts/[id]/send-email/route.ts +254 -0
  88. package/template/src/app/api/contacts/export/route.ts +270 -0
  89. package/template/src/app/api/contacts/import/route.ts +381 -0
  90. package/template/src/app/api/contacts/route.ts +283 -0
  91. package/template/src/app/api/dashboard/stats/route.ts +299 -0
  92. package/template/src/app/api/email/track/[id]/route.ts +68 -0
  93. package/template/src/app/api/integrations/google-sheet/sync/route.ts +526 -0
  94. package/template/src/app/api/invite/complete/route.ts +88 -0
  95. package/template/src/app/api/invite/validate/route.ts +55 -0
  96. package/template/src/app/api/reminders/route.ts +95 -0
  97. package/template/src/app/api/reset-password/complete/route.ts +73 -0
  98. package/template/src/app/api/reset-password/request/route.ts +84 -0
  99. package/template/src/app/api/reset-password/validate/route.ts +49 -0
  100. package/template/src/app/api/reset-password/verify/route.ts +74 -0
  101. package/template/src/app/api/roles/[id]/route.ts +183 -0
  102. package/template/src/app/api/roles/route.ts +140 -0
  103. package/template/src/app/api/send/route.ts +282 -0
  104. package/template/src/app/api/settings/change-password/route.ts +95 -0
  105. package/template/src/app/api/settings/closing-reasons/[id]/route.ts +84 -0
  106. package/template/src/app/api/settings/closing-reasons/route.ts +74 -0
  107. package/template/src/app/api/settings/company/route.ts +121 -0
  108. package/template/src/app/api/settings/google-ads/[id]/route.ts +117 -0
  109. package/template/src/app/api/settings/google-ads/route.ts +122 -0
  110. package/template/src/app/api/settings/google-sheet/[id]/route.ts +230 -0
  111. package/template/src/app/api/settings/google-sheet/auto-map/route.ts +196 -0
  112. package/template/src/app/api/settings/google-sheet/route.ts +254 -0
  113. package/template/src/app/api/settings/meta-leads/[id]/route.ts +123 -0
  114. package/template/src/app/api/settings/meta-leads/route.ts +132 -0
  115. package/template/src/app/api/settings/profile/route.ts +42 -0
  116. package/template/src/app/api/settings/smtp/route.ts +130 -0
  117. package/template/src/app/api/settings/smtp/test/route.ts +121 -0
  118. package/template/src/app/api/settings/statuses/[id]/route.ts +101 -0
  119. package/template/src/app/api/settings/statuses/route.ts +83 -0
  120. package/template/src/app/api/statuses/route.ts +25 -0
  121. package/template/src/app/api/tasks/[id]/attendees/route.ts +76 -0
  122. package/template/src/app/api/tasks/[id]/route.ts +728 -0
  123. package/template/src/app/api/tasks/meet/route.ts +240 -0
  124. package/template/src/app/api/tasks/route.ts +417 -0
  125. package/template/src/app/api/templates/[id]/route.ts +140 -0
  126. package/template/src/app/api/templates/route.ts +91 -0
  127. package/template/src/app/api/users/[id]/route.ts +168 -0
  128. package/template/src/app/api/users/list/route.ts +45 -0
  129. package/template/src/app/api/users/me/route.ts +48 -0
  130. package/template/src/app/api/users/route.ts +250 -0
  131. package/template/src/app/api/webhooks/google-ads/route.ts +208 -0
  132. package/template/src/app/api/webhooks/meta-leads/route.ts +258 -0
  133. package/template/src/app/api/workflows/[id]/route.ts +192 -0
  134. package/template/src/app/api/workflows/process/route.ts +293 -0
  135. package/template/src/app/api/workflows/route.ts +124 -0
  136. package/template/src/app/favicon.ico +0 -0
  137. package/template/src/app/globals.css +1416 -0
  138. package/template/src/app/layout.tsx +31 -0
  139. package/template/src/app/page.tsx +32 -0
  140. package/template/src/components/dashboard/activity-chart.tsx +67 -0
  141. package/template/src/components/dashboard/contacts-chart.tsx +63 -0
  142. package/template/src/components/dashboard/recent-activity.tsx +164 -0
  143. package/template/src/components/dashboard/sales-analytics-chart.tsx +81 -0
  144. package/template/src/components/dashboard/stat-card.tsx +61 -0
  145. package/template/src/components/dashboard/status-distribution-chart.tsx +45 -0
  146. package/template/src/components/dashboard/tasks-pie-chart.tsx +88 -0
  147. package/template/src/components/dashboard/top-contacts-list.tsx +129 -0
  148. package/template/src/components/dashboard/upcoming-tasks-list.tsx +126 -0
  149. package/template/src/components/editor.tsx +856 -0
  150. package/template/src/components/email-template.tsx +35 -0
  151. package/template/src/components/header.tsx +320 -0
  152. package/template/src/components/invitation-email-template.tsx +79 -0
  153. package/template/src/components/meet-cancellation-email-template.tsx +120 -0
  154. package/template/src/components/meet-confirmation-email-template.tsx +156 -0
  155. package/template/src/components/meet-update-email-template.tsx +209 -0
  156. package/template/src/components/page-header.tsx +61 -0
  157. package/template/src/components/reset-password-email-template.tsx +79 -0
  158. package/template/src/components/sidebar.tsx +294 -0
  159. package/template/src/components/skeleton.tsx +380 -0
  160. package/template/src/components/ui/commands.tsx +396 -0
  161. package/template/src/components/ui/components.tsx +150 -0
  162. package/template/src/components/ui/theme.tsx +5 -0
  163. package/template/src/components/view-as-banner.tsx +45 -0
  164. package/template/src/components/view-as-modal.tsx +186 -0
  165. package/template/src/contexts/mobile-menu-context.tsx +31 -0
  166. package/template/src/contexts/sidebar-context.tsx +107 -0
  167. package/template/src/contexts/task-reminder-context.tsx +239 -0
  168. package/template/src/contexts/view-as-context.tsx +84 -0
  169. package/template/src/hooks/use-user-role.ts +82 -0
  170. package/template/src/lib/audit-log.ts +45 -0
  171. package/template/src/lib/auth-client.ts +16 -0
  172. package/template/src/lib/auth.ts +35 -0
  173. package/template/src/lib/check-permission.ts +193 -0
  174. package/template/src/lib/contact-duplicate.ts +112 -0
  175. package/template/src/lib/contact-interactions.ts +371 -0
  176. package/template/src/lib/encryption.ts +99 -0
  177. package/template/src/lib/google-calendar.ts +300 -0
  178. package/template/src/lib/google-drive.ts +372 -0
  179. package/template/src/lib/permissions.ts +412 -0
  180. package/template/src/lib/prisma.ts +32 -0
  181. package/template/src/lib/roles.ts +120 -0
  182. package/template/src/lib/template-variables.ts +76 -0
  183. package/template/src/lib/utils.ts +46 -0
  184. package/template/src/lib/workflow-executor.ts +482 -0
  185. package/template/src/proxy.ts +91 -0
  186. package/template/tsconfig.json +34 -0
  187. package/template/vercel.json +8 -0
@@ -0,0 +1,240 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { auth } from '@/lib/auth';
3
+ import { prisma } from '@/lib/prisma';
4
+ import {
5
+ getValidAccessToken,
6
+ createGoogleCalendarEvent,
7
+ extractMeetLink,
8
+ } from '@/lib/google-calendar';
9
+ import nodemailer from 'nodemailer';
10
+ import { decrypt } from '@/lib/encryption';
11
+ import { render } from '@react-email/render';
12
+ import { MeetConfirmationEmailTemplate } from '@/components/meet-confirmation-email-template';
13
+ import React from 'react';
14
+
15
+ function htmlToText(html: string): string {
16
+ if (!html) return '';
17
+ return html
18
+ .replace(/<br\s*\/?>/gi, '\n')
19
+ .replace(/<\/p>/gi, '\n\n')
20
+ .replace(/<[^>]+>/g, '')
21
+ .replace(/\n{3,}/g, '\n\n')
22
+ .trim();
23
+ }
24
+
25
+ /**
26
+ * POST /api/tasks/meet
27
+ * Crée un Google Meet sans contact
28
+ */
29
+ export async function POST(request: NextRequest) {
30
+ try {
31
+ const session = await auth.api.getSession({
32
+ headers: request.headers,
33
+ });
34
+
35
+ if (!session) {
36
+ return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
37
+ }
38
+
39
+ const body = await request.json();
40
+ const {
41
+ title,
42
+ description,
43
+ scheduledAt,
44
+ durationMinutes = 30,
45
+ attendees = [],
46
+ reminderMinutesBefore,
47
+ internalNote,
48
+ } = body;
49
+
50
+ // Validation
51
+ if (!title || !scheduledAt) {
52
+ return NextResponse.json({ error: 'Le titre et la date/heure sont requis' }, { status: 400 });
53
+ }
54
+
55
+ // Vérifier que l'utilisateur a un compte Google connecté
56
+ const googleAccount = await prisma.userGoogleAccount.findUnique({
57
+ where: { userId: session.user.id },
58
+ });
59
+
60
+ if (!googleAccount) {
61
+ return NextResponse.json(
62
+ { error: 'Veuillez connecter votre compte Google dans les paramètres' },
63
+ { status: 400 },
64
+ );
65
+ }
66
+
67
+ // Obtenir un token valide
68
+ const accessToken = await getValidAccessToken(
69
+ googleAccount.accessToken,
70
+ googleAccount.refreshToken,
71
+ googleAccount.tokenExpiresAt,
72
+ );
73
+
74
+ // Mettre à jour le token si nécessaire
75
+ if (accessToken !== googleAccount.accessToken) {
76
+ const tokenExpiresAt = new Date();
77
+ tokenExpiresAt.setSeconds(tokenExpiresAt.getSeconds() + 3600);
78
+ await prisma.userGoogleAccount.update({
79
+ where: { userId: session.user.id },
80
+ data: {
81
+ accessToken,
82
+ tokenExpiresAt,
83
+ },
84
+ });
85
+ }
86
+
87
+ // Préparer les dates pour Google Calendar
88
+ const startDate = new Date(scheduledAt);
89
+ const endDate = new Date(startDate.getTime() + durationMinutes * 60 * 1000);
90
+
91
+ // Construire la liste des invités
92
+ const allAttendees = attendees
93
+ .filter((email: string) => email && email.trim() !== '')
94
+ .map((email: string) => ({ email: email.trim() }));
95
+
96
+ // Créer l'évènement Google Calendar avec Meet
97
+ const googleEvent = await createGoogleCalendarEvent(accessToken, {
98
+ summary: title,
99
+ description: description || '',
100
+ start: {
101
+ dateTime: startDate.toISOString(),
102
+ timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
103
+ },
104
+ end: {
105
+ dateTime: endDate.toISOString(),
106
+ timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
107
+ },
108
+ attendees: allAttendees.length > 0 ? allAttendees : undefined,
109
+ conferenceData: {
110
+ createRequest: {
111
+ requestId: `meet-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
112
+ conferenceSolutionKey: {
113
+ type: 'hangoutsMeet',
114
+ },
115
+ },
116
+ },
117
+ conferenceDataVersion: 1,
118
+ });
119
+
120
+ const meetLink = extractMeetLink(googleEvent);
121
+
122
+ // Créer la tâche dans le CRM (sans contact)
123
+ const task = await prisma.task.create({
124
+ data: {
125
+ type: 'VIDEO_CONFERENCE',
126
+ title,
127
+ description: description || '',
128
+ priority: 'MEDIUM',
129
+ scheduledAt: startDate,
130
+ reminderMinutesBefore: reminderMinutesBefore || null,
131
+ assignedUserId: session.user.id,
132
+ createdById: session.user.id,
133
+ googleEventId: googleEvent.id,
134
+ googleMeetLink: meetLink,
135
+ durationMinutes,
136
+ internalNote: internalNote || null,
137
+ },
138
+ include: {
139
+ assignedUser: {
140
+ select: {
141
+ id: true,
142
+ name: true,
143
+ email: true,
144
+ },
145
+ },
146
+ createdBy: {
147
+ select: {
148
+ id: true,
149
+ name: true,
150
+ email: true,
151
+ },
152
+ },
153
+ },
154
+ });
155
+
156
+ // Envoyer l'email de confirmation aux invités si un email est disponible
157
+ if (allAttendees.length > 0 && meetLink) {
158
+ try {
159
+ // Récupérer la configuration SMTP
160
+ const smtpConfig = await prisma.smtpConfig.findUnique({
161
+ where: { userId: session.user.id },
162
+ });
163
+
164
+ if (smtpConfig) {
165
+ // Déchiffrer le mot de passe SMTP
166
+ let password: string;
167
+ try {
168
+ password = decrypt(smtpConfig.password);
169
+ } catch (error) {
170
+ password = smtpConfig.password;
171
+ }
172
+
173
+ // Créer le transporteur SMTP
174
+ const transporter = nodemailer.createTransport({
175
+ host: smtpConfig.host,
176
+ port: smtpConfig.port,
177
+ secure: smtpConfig.secure,
178
+ auth: {
179
+ user: smtpConfig.username,
180
+ pass: password,
181
+ },
182
+ });
183
+
184
+ // Récupérer le nom de l'organisateur
185
+ const organizer = await prisma.user.findUnique({
186
+ where: { id: session.user.id },
187
+ select: { name: true },
188
+ });
189
+
190
+ const organizerName = organizer?.name || session.user.name || 'Organisateur';
191
+
192
+ // Envoyer un email individuel à chaque destinataire
193
+ for (const attendee of allAttendees) {
194
+ try {
195
+ // Générer le contenu HTML de l'email avec le composant React
196
+ const emailComponent = React.createElement(MeetConfirmationEmailTemplate, {
197
+ contactName: attendee.email.split('@')[0], // Utiliser le nom d'utilisateur de l'email
198
+ title,
199
+ scheduledAt: startDate.toISOString(),
200
+ durationMinutes,
201
+ meetLink: meetLink,
202
+ description,
203
+ organizerName,
204
+ signature: smtpConfig.signature || undefined,
205
+ });
206
+
207
+ const emailHtml = await render(emailComponent);
208
+ const emailText = htmlToText(emailHtml);
209
+
210
+ await transporter.sendMail({
211
+ from: smtpConfig.fromName
212
+ ? `"${smtpConfig.fromName}" <${smtpConfig.fromEmail}>`
213
+ : smtpConfig.fromEmail,
214
+ to: attendee.email,
215
+ subject: `Confirmation de rendez-vous : ${title}`,
216
+ text: emailText,
217
+ html: emailHtml,
218
+ });
219
+ } catch (individualEmailError: any) {
220
+ // Logger l'erreur mais continuer avec les autres destinataires
221
+ console.error(
222
+ `Erreur lors de l'envoi de l'email à ${attendee.email}:`,
223
+ individualEmailError,
224
+ );
225
+ }
226
+ }
227
+ }
228
+ } catch (emailError: any) {
229
+ // Ne pas faire échouer la création du Meet si l'email échoue
230
+ console.error("Erreur lors de l'envoi de l'email de confirmation:", emailError);
231
+ }
232
+ }
233
+
234
+ return NextResponse.json(task, { status: 201 });
235
+ } catch (error: any) {
236
+ console.error('Erreur lors de la création du Google Meet:', error);
237
+ return NextResponse.json({ error: error.message || 'Erreur serveur' }, { status: 500 });
238
+ }
239
+ }
240
+
@@ -0,0 +1,417 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { auth } from '@/lib/auth';
3
+ import { prisma } from '@/lib/prisma';
4
+ import { logAppointmentCreated, createInteraction } from '@/lib/contact-interactions';
5
+ import nodemailer from 'nodemailer';
6
+ import { decrypt } from '@/lib/encryption';
7
+ import { render } from '@react-email/render';
8
+ import { MeetConfirmationEmailTemplate } from '@/components/meet-confirmation-email-template';
9
+ import React from 'react';
10
+ import { createGoogleCalendarEvent, getValidAccessToken } from '@/lib/google-calendar';
11
+
12
+ function htmlToText(html: string): string {
13
+ if (!html) return '';
14
+ return html
15
+ .replace(/<br\s*\/?>/gi, '\n')
16
+ .replace(/<\/p>/gi, '\n\n')
17
+ .replace(/<[^>]+>/g, '')
18
+ .replace(/\n{3,}/g, '\n\n')
19
+ .trim();
20
+ }
21
+
22
+ // GET /api/tasks - Récupérer les tâches de l'utilisateur
23
+ export async function GET(request: NextRequest) {
24
+ try {
25
+ const session = await auth.api.getSession({
26
+ headers: request.headers,
27
+ });
28
+
29
+ if (!session) {
30
+ return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
31
+ }
32
+
33
+ const { searchParams } = new URL(request.url);
34
+ const startDate = searchParams.get('startDate');
35
+ const endDate = searchParams.get('endDate');
36
+ const assignedTo = searchParams.get('assignedTo'); // Pour les admins
37
+ const contactId = searchParams.get('contactId'); // Filtrer par contact
38
+
39
+ // Construire les filtres
40
+ const where: any = {
41
+ scheduledAt: {
42
+ gte: startDate ? new Date(startDate) : new Date(),
43
+ lte: endDate ? new Date(endDate) : undefined,
44
+ },
45
+ };
46
+
47
+ // Filtrer par contact si fourni
48
+ if (contactId) {
49
+ where.contactId = contactId;
50
+ }
51
+
52
+ // Si admin demande les tâches d'un autre utilisateur
53
+ if (assignedTo && assignedTo !== session.user.id) {
54
+ const user = await prisma.user.findUnique({
55
+ where: { id: session.user.id },
56
+ select: { role: true },
57
+ });
58
+ if (user?.role === 'ADMIN') {
59
+ where.assignedUserId = assignedTo;
60
+ } else {
61
+ // Non-admin ne peut voir que ses propres tâches
62
+ where.assignedUserId = session.user.id;
63
+ }
64
+ } else {
65
+ // Par défaut, voir ses propres tâches
66
+ where.assignedUserId = session.user.id;
67
+ }
68
+
69
+ if (!endDate) {
70
+ delete where.scheduledAt.lte;
71
+ }
72
+
73
+ const tasks = await prisma.task.findMany({
74
+ where,
75
+ include: {
76
+ contact: {
77
+ select: {
78
+ id: true,
79
+ firstName: true,
80
+ lastName: true,
81
+ email: true,
82
+ phone: true,
83
+ },
84
+ },
85
+ assignedUser: {
86
+ select: {
87
+ id: true,
88
+ name: true,
89
+ email: true,
90
+ },
91
+ },
92
+ createdBy: {
93
+ select: {
94
+ id: true,
95
+ name: true,
96
+ email: true,
97
+ },
98
+ },
99
+ },
100
+ orderBy: {
101
+ scheduledAt: 'asc',
102
+ },
103
+ });
104
+
105
+ return NextResponse.json(tasks);
106
+ } catch (error: any) {
107
+ console.error('Erreur lors de la récupération des tâches:', error);
108
+ return NextResponse.json({ error: error.message || 'Erreur serveur' }, { status: 500 });
109
+ }
110
+ }
111
+
112
+ // POST /api/tasks - Créer une nouvelle tâche
113
+ export async function POST(request: NextRequest) {
114
+ try {
115
+ const session = await auth.api.getSession({
116
+ headers: request.headers,
117
+ });
118
+
119
+ if (!session) {
120
+ return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
121
+ }
122
+
123
+ const body = await request.json();
124
+ const {
125
+ type,
126
+ title,
127
+ description,
128
+ priority,
129
+ scheduledAt,
130
+ contactId,
131
+ assignedUserId,
132
+ reminderMinutesBefore,
133
+ notifyContact,
134
+ internalNote,
135
+ attendees = [],
136
+ } = body;
137
+
138
+ // Validation
139
+ if (!type || !description || !scheduledAt) {
140
+ return NextResponse.json(
141
+ { error: 'Le type, la description et la date sont requis' },
142
+ { status: 400 },
143
+ );
144
+ }
145
+
146
+ // Vérifier si l'utilisateur est admin
147
+ const user = await prisma.user.findUnique({
148
+ where: { id: session.user.id },
149
+ select: { role: true },
150
+ });
151
+
152
+ // Déterminer l'utilisateur assigné
153
+ let finalAssignedUserId: string;
154
+ if (assignedUserId && user?.role === 'ADMIN') {
155
+ // Admin peut assigner à n'importe qui
156
+ finalAssignedUserId = assignedUserId;
157
+ } else {
158
+ // Utilisateur normal s'assigne automatiquement
159
+ finalAssignedUserId = session.user.id;
160
+ }
161
+
162
+ // Vérifier que le contact existe si fourni
163
+ if (contactId) {
164
+ const contact = await prisma.contact.findUnique({
165
+ where: { id: contactId },
166
+ });
167
+ if (!contact) {
168
+ return NextResponse.json({ error: 'Contact non trouvé' }, { status: 404 });
169
+ }
170
+ }
171
+
172
+ // Créer la tâche
173
+ const task = await prisma.task.create({
174
+ data: {
175
+ type,
176
+ title: title || null,
177
+ description,
178
+ priority: priority || 'MEDIUM',
179
+ scheduledAt: new Date(scheduledAt),
180
+ contactId: contactId || null,
181
+ assignedUserId: finalAssignedUserId,
182
+ createdById: session.user.id,
183
+ reminderMinutesBefore:
184
+ typeof reminderMinutesBefore === 'number' ? reminderMinutesBefore : null,
185
+ notifyContact: notifyContact === true,
186
+ internalNote: internalNote || null,
187
+ },
188
+ include: {
189
+ contact: {
190
+ select: {
191
+ id: true,
192
+ firstName: true,
193
+ lastName: true,
194
+ email: true,
195
+ phone: true,
196
+ },
197
+ },
198
+ assignedUser: {
199
+ select: {
200
+ id: true,
201
+ name: true,
202
+ email: true,
203
+ },
204
+ },
205
+ createdBy: {
206
+ select: {
207
+ id: true,
208
+ name: true,
209
+ email: true,
210
+ },
211
+ },
212
+ },
213
+ });
214
+
215
+ // Construire la liste des invités (contact + invités additionnels, ou seulement invités si pas de contact)
216
+ // Cette liste sera utilisée pour Google Calendar ET pour les emails
217
+ const allAttendees: Array<{ email: string }> = [];
218
+ if (task.contact?.email) {
219
+ allAttendees.push({ email: task.contact.email });
220
+ }
221
+ attendees.forEach((email: string) => {
222
+ if (email && email.trim() !== '') {
223
+ const trimmedEmail = email.trim();
224
+ // Éviter les doublons
225
+ if (!allAttendees.some((a) => a.email === trimmedEmail)) {
226
+ allAttendees.push({ email: trimmedEmail });
227
+ }
228
+ }
229
+ });
230
+
231
+ // Si c'est un rendez-vous (physique) et que l'utilisateur a connecté son Google Agenda,
232
+ // on crée aussi un évènement dans son calendrier, avec possibilité d'inviter d'autres personnes.
233
+ if (type === 'MEETING') {
234
+ try {
235
+ const googleAccount = await prisma.userGoogleAccount.findUnique({
236
+ where: { userId: session.user.id },
237
+ });
238
+
239
+ if (googleAccount) {
240
+ const accessToken = await getValidAccessToken(
241
+ googleAccount.accessToken,
242
+ googleAccount.refreshToken,
243
+ googleAccount.tokenExpiresAt,
244
+ );
245
+
246
+ // Durée par défaut de 60 minutes pour les rendez-vous physiques
247
+ const startDate = new Date(scheduledAt);
248
+ const endDate = new Date(startDate.getTime() + 60 * 60 * 1000);
249
+
250
+ const googleEvent = await createGoogleCalendarEvent(accessToken, {
251
+ summary: title || 'Rendez-vous',
252
+ description: htmlToText(description),
253
+ start: {
254
+ dateTime: startDate.toISOString(),
255
+ timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
256
+ },
257
+ end: {
258
+ dateTime: endDate.toISOString(),
259
+ timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
260
+ },
261
+ attendees: allAttendees.length > 0 ? allAttendees : undefined,
262
+ });
263
+
264
+ // Sauvegarder l'ID de l'évènement pour synchroniser les modifications/suppressions
265
+ await prisma.task.update({
266
+ where: { id: task.id },
267
+ data: {
268
+ googleEventId: googleEvent.id,
269
+ },
270
+ });
271
+ }
272
+ } catch (googleError: any) {
273
+ console.error('Erreur lors de la création de lévènement Google Calendar pour le RDV:', googleError);
274
+ // On ne bloque pas la création de la tâche si Google Calendar échoue
275
+ }
276
+ }
277
+
278
+ // Si la tâche est liée à un contact, créer aussi une interaction
279
+ if (contactId) {
280
+ try {
281
+ if (type === 'MEETING') {
282
+ // Pour les rendez-vous, utiliser la fonction spécialisée
283
+ await logAppointmentCreated(
284
+ contactId,
285
+ task.id,
286
+ new Date(scheduledAt),
287
+ title,
288
+ session.user.id,
289
+ );
290
+
291
+ // Envoyer un email de notification si demandé (contact ou invités)
292
+ if (notifyContact) {
293
+ try {
294
+ // Récupérer la configuration SMTP
295
+ const smtpConfig = await prisma.smtpConfig.findUnique({
296
+ where: { userId: session.user.id },
297
+ });
298
+
299
+ if (smtpConfig) {
300
+ // Déchiffrer le mot de passe SMTP
301
+ let password: string;
302
+ try {
303
+ password = decrypt(smtpConfig.password);
304
+ } catch (error) {
305
+ password = smtpConfig.password;
306
+ }
307
+
308
+ // Créer le transporteur SMTP
309
+ const transporter = nodemailer.createTransport({
310
+ host: smtpConfig.host,
311
+ port: smtpConfig.port,
312
+ secure: smtpConfig.secure,
313
+ auth: {
314
+ user: smtpConfig.username,
315
+ pass: password,
316
+ },
317
+ });
318
+
319
+ // Récupérer le nom de l'organisateur
320
+ const organizer = await prisma.user.findUnique({
321
+ where: { id: session.user.id },
322
+ select: { name: true },
323
+ });
324
+
325
+ const organizerName = organizer?.name || session.user.name || 'Organisateur';
326
+ const scheduledDate = new Date(scheduledAt);
327
+
328
+ // Liste des destinataires : contact (si existe) + invités
329
+ const recipients: Array<{ email: string; name: string }> = [];
330
+
331
+ if (task.contact?.email) {
332
+ const contactName =
333
+ `${task.contact.firstName || ''} ${task.contact.lastName || ''}`.trim() ||
334
+ 'Cher client';
335
+ recipients.push({ email: task.contact.email, name: contactName });
336
+ }
337
+
338
+ // Ajouter les invités additionnels
339
+ allAttendees.forEach((attendee) => {
340
+ if (!recipients.some((r) => r.email === attendee.email)) {
341
+ recipients.push({
342
+ email: attendee.email,
343
+ name: attendee.email.split('@')[0], // Utiliser le nom d'utilisateur de l'email
344
+ });
345
+ }
346
+ });
347
+
348
+ // Envoyer un email individuel à chaque destinataire
349
+ for (const recipient of recipients) {
350
+ try {
351
+ // Générer le contenu HTML de l'email avec le composant React
352
+ const emailComponent = React.createElement(MeetConfirmationEmailTemplate, {
353
+ contactName: recipient.name,
354
+ title: title || 'Rendez-vous',
355
+ scheduledAt: scheduledDate.toISOString(),
356
+ durationMinutes: 0, // Pas de durée pour les rendez-vous physiques
357
+ description,
358
+ organizerName,
359
+ signature: smtpConfig.signature || undefined,
360
+ });
361
+
362
+ const emailHtml = await render(emailComponent);
363
+ const emailText = htmlToText(emailHtml);
364
+
365
+ // Envoyer l'email
366
+ await transporter.sendMail({
367
+ from: smtpConfig.fromName
368
+ ? `"${smtpConfig.fromName}" <${smtpConfig.fromEmail}>`
369
+ : smtpConfig.fromEmail,
370
+ to: recipient.email,
371
+ subject: `Confirmation de rendez-vous${title ? ` : ${title}` : ''}`,
372
+ text: emailText,
373
+ html: emailHtml,
374
+ });
375
+ } catch (individualEmailError: any) {
376
+ // Logger l'erreur mais continuer avec les autres destinataires
377
+ console.error(
378
+ `Erreur lors de l'envoi de l'email à ${recipient.email}:`,
379
+ individualEmailError,
380
+ );
381
+ }
382
+ }
383
+ }
384
+ } catch (emailError: any) {
385
+ // Ne pas faire échouer la création de la tâche si l'email échoue
386
+ console.error("Erreur lors de l'envoi de l'email de notification:", emailError);
387
+ }
388
+ }
389
+ } else {
390
+ // Pour les autres types de tâches, créer une interaction standard
391
+ const interactionTypeMap: Record<string, string> = {
392
+ CALL: 'CALL',
393
+ EMAIL: 'EMAIL',
394
+ OTHER: 'NOTE',
395
+ };
396
+
397
+ await createInteraction({
398
+ contactId,
399
+ type: (interactionTypeMap[type] || 'NOTE') as any,
400
+ title: title || `Tâche ${type}`,
401
+ content: description,
402
+ userId: session.user.id,
403
+ date: new Date(scheduledAt),
404
+ });
405
+ }
406
+ } catch (error) {
407
+ // Ne pas faire échouer la création de la tâche si l'interaction échoue
408
+ console.error("Erreur lors de la création de l'interaction:", error);
409
+ }
410
+ }
411
+
412
+ return NextResponse.json(task, { status: 201 });
413
+ } catch (error: any) {
414
+ console.error('Erreur lors de la création de la tâche:', error);
415
+ return NextResponse.json({ error: error.message || 'Erreur serveur' }, { status: 500 });
416
+ }
417
+ }