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.
- package/bin/create-crm-tmp.js +93 -0
- package/package.json +25 -0
- package/template/.prettierignore +33 -0
- package/template/.prettierrc.json +25 -0
- package/template/README.md +173 -0
- package/template/eslint.config.mjs +18 -0
- package/template/exemple-contacts.csv +11 -0
- package/template/next.config.ts +8 -0
- package/template/package.json +64 -0
- package/template/postcss.config.mjs +7 -0
- package/template/prisma/migrations/20251126144728_init/migration.sql +78 -0
- package/template/prisma/migrations/20251126155204_add_user_roles/migration.sql +5 -0
- package/template/prisma/migrations/20251128095126_add_company_info/migration.sql +19 -0
- package/template/prisma/migrations/20251128123321_add_smtp_config/migration.sql +22 -0
- package/template/prisma/migrations/20251128132303_add_status/migration.sql +23 -0
- package/template/prisma/migrations/20251201102207_add_user_active/migration.sql +75 -0
- package/template/prisma/migrations/20251201105507_add_email_signature/migration.sql +2 -0
- package/template/prisma/migrations/20251201151122_add_tasks/migration.sql +45 -0
- package/template/prisma/migrations/20251202111854_add_task_reminder/migration.sql +2 -0
- package/template/prisma/migrations/20251202135859_add_google_meet_integration/migration.sql +27 -0
- package/template/prisma/migrations/20251203103317_add_meta_lead_integration/migration.sql +20 -0
- package/template/prisma/migrations/20251203104002_add_google_ads_integration/migration.sql +18 -0
- package/template/prisma/migrations/20251203112122_add_google_sheet_integration/migration.sql +32 -0
- package/template/prisma/migrations/20251203153853_allow_multiple_integration_configs/migration.sql +20 -0
- package/template/prisma/migrations/20251205141705_update_user_roles/migration.sql +12 -0
- package/template/prisma/migrations/20251205150000_add_commercial_and_telepro_assignment/migration.sql +21 -0
- package/template/prisma/migrations/20251205160000_add_interaction_logging/migration.sql +11 -0
- package/template/prisma/migrations/20251208090314_add_automatic_interaction_types/migration.sql +12 -0
- package/template/prisma/migrations/20251208094843_mg/migration.sql +14 -0
- package/template/prisma/migrations/20251208100000_add_company_support/migration.sql +14 -0
- package/template/prisma/migrations/20251208110000_add_templates/migration.sql +26 -0
- package/template/prisma/migrations/20251208141304_add_video_conference_task_type/migration.sql +2 -0
- package/template/prisma/migrations/20251209104759_add_internal_note_to_task/migration.sql +2 -0
- package/template/prisma/migrations/20251209134803_add_company_field/migration.sql +2 -0
- package/template/prisma/migrations/20251209150000_rename_company_to_company_name/migration.sql +3 -0
- package/template/prisma/migrations/20251209150016_add_email_tracking/migration.sql +21 -0
- package/template/prisma/migrations/20251209155908_add_notify_contact_to_task/migration.sql +2 -0
- package/template/prisma/migrations/20251210110019_add_appointment_types/migration.sql +10 -0
- package/template/prisma/migrations/20251210113928_add_contact_files/migration.sql +26 -0
- package/template/prisma/migrations/20251212132339_add_custom_roles/migration.sql +24 -0
- package/template/prisma/migrations/20251215104448_add_file_interaction_types/migration.sql +11 -0
- package/template/prisma/migrations/20251215145616_add_closing_reasons/migration.sql +12 -0
- package/template/prisma/migrations/20251216140850_add_log_users/migration.sql +25 -0
- package/template/prisma/migrations/20251216151000_rename_perdu_to_ferme/migration.sql +8 -0
- package/template/prisma/migrations/20251216162318_add_column_mappings_to_google_sheet/migration.sql +2 -0
- package/template/prisma/migrations/20251216185127_add_workflows/migration.sql +80 -0
- package/template/prisma/migrations/20251216192237_add_scheduled_workflow_actions/migration.sql +32 -0
- package/template/prisma/migrations/migration_lock.toml +3 -0
- package/template/prisma/schema.prisma +582 -0
- package/template/prisma.config.ts +14 -0
- package/template/src/app/(auth)/invite/[token]/page.tsx +200 -0
- package/template/src/app/(auth)/layout.tsx +3 -0
- package/template/src/app/(auth)/reset-password/complete/page.tsx +213 -0
- package/template/src/app/(auth)/reset-password/page.tsx +146 -0
- package/template/src/app/(auth)/reset-password/verify/page.tsx +183 -0
- package/template/src/app/(auth)/signin/page.tsx +166 -0
- package/template/src/app/(dashboard)/agenda/page.tsx +3051 -0
- package/template/src/app/(dashboard)/automatisation/[id]/page.tsx +24 -0
- package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +905 -0
- package/template/src/app/(dashboard)/automatisation/new/page.tsx +20 -0
- package/template/src/app/(dashboard)/automatisation/page.tsx +337 -0
- package/template/src/app/(dashboard)/closing/page.tsx +1052 -0
- package/template/src/app/(dashboard)/contacts/[id]/page.tsx +6028 -0
- package/template/src/app/(dashboard)/contacts/page.tsx +3713 -0
- package/template/src/app/(dashboard)/dashboard/page.tsx +186 -0
- package/template/src/app/(dashboard)/layout.tsx +30 -0
- package/template/src/app/(dashboard)/settings/page.tsx +4070 -0
- package/template/src/app/(dashboard)/templates/page.tsx +567 -0
- package/template/src/app/(dashboard)/users/list/page.tsx +507 -0
- package/template/src/app/(dashboard)/users/page.tsx +457 -0
- package/template/src/app/(dashboard)/users/permissions/page.tsx +181 -0
- package/template/src/app/(dashboard)/users/roles/page.tsx +434 -0
- package/template/src/app/api/audit-logs/route.ts +57 -0
- package/template/src/app/api/auth/[...all]/route.ts +4 -0
- package/template/src/app/api/auth/check-active/route.ts +31 -0
- package/template/src/app/api/auth/google/callback/route.ts +94 -0
- package/template/src/app/api/auth/google/disconnect/route.ts +32 -0
- package/template/src/app/api/auth/google/route.ts +34 -0
- package/template/src/app/api/auth/google/status/route.ts +32 -0
- package/template/src/app/api/closing-reasons/route.ts +27 -0
- package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +94 -0
- package/template/src/app/api/contacts/[id]/files/route.ts +269 -0
- package/template/src/app/api/contacts/[id]/interactions/[interactionId]/route.ts +91 -0
- package/template/src/app/api/contacts/[id]/interactions/route.ts +103 -0
- package/template/src/app/api/contacts/[id]/meet/route.ts +296 -0
- package/template/src/app/api/contacts/[id]/route.ts +322 -0
- package/template/src/app/api/contacts/[id]/send-email/route.ts +254 -0
- package/template/src/app/api/contacts/export/route.ts +270 -0
- package/template/src/app/api/contacts/import/route.ts +381 -0
- package/template/src/app/api/contacts/route.ts +283 -0
- package/template/src/app/api/dashboard/stats/route.ts +299 -0
- package/template/src/app/api/email/track/[id]/route.ts +68 -0
- package/template/src/app/api/integrations/google-sheet/sync/route.ts +526 -0
- package/template/src/app/api/invite/complete/route.ts +88 -0
- package/template/src/app/api/invite/validate/route.ts +55 -0
- package/template/src/app/api/reminders/route.ts +95 -0
- package/template/src/app/api/reset-password/complete/route.ts +73 -0
- package/template/src/app/api/reset-password/request/route.ts +84 -0
- package/template/src/app/api/reset-password/validate/route.ts +49 -0
- package/template/src/app/api/reset-password/verify/route.ts +74 -0
- package/template/src/app/api/roles/[id]/route.ts +183 -0
- package/template/src/app/api/roles/route.ts +140 -0
- package/template/src/app/api/send/route.ts +282 -0
- package/template/src/app/api/settings/change-password/route.ts +95 -0
- package/template/src/app/api/settings/closing-reasons/[id]/route.ts +84 -0
- package/template/src/app/api/settings/closing-reasons/route.ts +74 -0
- package/template/src/app/api/settings/company/route.ts +121 -0
- package/template/src/app/api/settings/google-ads/[id]/route.ts +117 -0
- package/template/src/app/api/settings/google-ads/route.ts +122 -0
- package/template/src/app/api/settings/google-sheet/[id]/route.ts +230 -0
- package/template/src/app/api/settings/google-sheet/auto-map/route.ts +196 -0
- package/template/src/app/api/settings/google-sheet/route.ts +254 -0
- package/template/src/app/api/settings/meta-leads/[id]/route.ts +123 -0
- package/template/src/app/api/settings/meta-leads/route.ts +132 -0
- package/template/src/app/api/settings/profile/route.ts +42 -0
- package/template/src/app/api/settings/smtp/route.ts +130 -0
- package/template/src/app/api/settings/smtp/test/route.ts +121 -0
- package/template/src/app/api/settings/statuses/[id]/route.ts +101 -0
- package/template/src/app/api/settings/statuses/route.ts +83 -0
- package/template/src/app/api/statuses/route.ts +25 -0
- package/template/src/app/api/tasks/[id]/attendees/route.ts +76 -0
- package/template/src/app/api/tasks/[id]/route.ts +728 -0
- package/template/src/app/api/tasks/meet/route.ts +240 -0
- package/template/src/app/api/tasks/route.ts +417 -0
- package/template/src/app/api/templates/[id]/route.ts +140 -0
- package/template/src/app/api/templates/route.ts +91 -0
- package/template/src/app/api/users/[id]/route.ts +168 -0
- package/template/src/app/api/users/list/route.ts +45 -0
- package/template/src/app/api/users/me/route.ts +48 -0
- package/template/src/app/api/users/route.ts +250 -0
- package/template/src/app/api/webhooks/google-ads/route.ts +208 -0
- package/template/src/app/api/webhooks/meta-leads/route.ts +258 -0
- package/template/src/app/api/workflows/[id]/route.ts +192 -0
- package/template/src/app/api/workflows/process/route.ts +293 -0
- package/template/src/app/api/workflows/route.ts +124 -0
- package/template/src/app/favicon.ico +0 -0
- package/template/src/app/globals.css +1416 -0
- package/template/src/app/layout.tsx +31 -0
- package/template/src/app/page.tsx +32 -0
- package/template/src/components/dashboard/activity-chart.tsx +67 -0
- package/template/src/components/dashboard/contacts-chart.tsx +63 -0
- package/template/src/components/dashboard/recent-activity.tsx +164 -0
- package/template/src/components/dashboard/sales-analytics-chart.tsx +81 -0
- package/template/src/components/dashboard/stat-card.tsx +61 -0
- package/template/src/components/dashboard/status-distribution-chart.tsx +45 -0
- package/template/src/components/dashboard/tasks-pie-chart.tsx +88 -0
- package/template/src/components/dashboard/top-contacts-list.tsx +129 -0
- package/template/src/components/dashboard/upcoming-tasks-list.tsx +126 -0
- package/template/src/components/editor.tsx +856 -0
- package/template/src/components/email-template.tsx +35 -0
- package/template/src/components/header.tsx +320 -0
- package/template/src/components/invitation-email-template.tsx +79 -0
- package/template/src/components/meet-cancellation-email-template.tsx +120 -0
- package/template/src/components/meet-confirmation-email-template.tsx +156 -0
- package/template/src/components/meet-update-email-template.tsx +209 -0
- package/template/src/components/page-header.tsx +61 -0
- package/template/src/components/reset-password-email-template.tsx +79 -0
- package/template/src/components/sidebar.tsx +294 -0
- package/template/src/components/skeleton.tsx +380 -0
- package/template/src/components/ui/commands.tsx +396 -0
- package/template/src/components/ui/components.tsx +150 -0
- package/template/src/components/ui/theme.tsx +5 -0
- package/template/src/components/view-as-banner.tsx +45 -0
- package/template/src/components/view-as-modal.tsx +186 -0
- package/template/src/contexts/mobile-menu-context.tsx +31 -0
- package/template/src/contexts/sidebar-context.tsx +107 -0
- package/template/src/contexts/task-reminder-context.tsx +239 -0
- package/template/src/contexts/view-as-context.tsx +84 -0
- package/template/src/hooks/use-user-role.ts +82 -0
- package/template/src/lib/audit-log.ts +45 -0
- package/template/src/lib/auth-client.ts +16 -0
- package/template/src/lib/auth.ts +35 -0
- package/template/src/lib/check-permission.ts +193 -0
- package/template/src/lib/contact-duplicate.ts +112 -0
- package/template/src/lib/contact-interactions.ts +371 -0
- package/template/src/lib/encryption.ts +99 -0
- package/template/src/lib/google-calendar.ts +300 -0
- package/template/src/lib/google-drive.ts +372 -0
- package/template/src/lib/permissions.ts +412 -0
- package/template/src/lib/prisma.ts +32 -0
- package/template/src/lib/roles.ts +120 -0
- package/template/src/lib/template-variables.ts +76 -0
- package/template/src/lib/utils.ts +46 -0
- package/template/src/lib/workflow-executor.ts +482 -0
- package/template/src/proxy.ts +91 -0
- package/template/tsconfig.json +34 -0
- package/template/vercel.json +8 -0
|
@@ -0,0 +1,728 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { auth } from '@/lib/auth';
|
|
3
|
+
import { prisma } from '@/lib/prisma';
|
|
4
|
+
import {
|
|
5
|
+
getValidAccessToken,
|
|
6
|
+
updateGoogleCalendarEvent,
|
|
7
|
+
extractMeetLink,
|
|
8
|
+
deleteGoogleCalendarEvent,
|
|
9
|
+
getGoogleCalendarEvent,
|
|
10
|
+
} from '@/lib/google-calendar';
|
|
11
|
+
import nodemailer from 'nodemailer';
|
|
12
|
+
import { decrypt } from '@/lib/encryption';
|
|
13
|
+
import { logAppointmentCancelled, logAppointmentChanged } from '@/lib/contact-interactions';
|
|
14
|
+
import { render } from '@react-email/render';
|
|
15
|
+
import { MeetUpdateEmailTemplate } from '@/components/meet-update-email-template';
|
|
16
|
+
import { MeetCancellationEmailTemplate } from '@/components/meet-cancellation-email-template';
|
|
17
|
+
import React from 'react';
|
|
18
|
+
|
|
19
|
+
function htmlToText(html: string): string {
|
|
20
|
+
if (!html) return '';
|
|
21
|
+
return html
|
|
22
|
+
.replace(/<br\s*\/?>/gi, '\n')
|
|
23
|
+
.replace(/<\/p>/gi, '\n\n')
|
|
24
|
+
.replace(/<[^>]+>/g, '')
|
|
25
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
26
|
+
.trim();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// GET /api/tasks/[id] - Récupérer une tâche spécifique
|
|
30
|
+
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
|
31
|
+
try {
|
|
32
|
+
const session = await auth.api.getSession({
|
|
33
|
+
headers: request.headers,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
if (!session) {
|
|
37
|
+
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const { id } = await params;
|
|
41
|
+
|
|
42
|
+
const task = await prisma.task.findUnique({
|
|
43
|
+
where: { id },
|
|
44
|
+
include: {
|
|
45
|
+
contact: {
|
|
46
|
+
select: {
|
|
47
|
+
id: true,
|
|
48
|
+
firstName: true,
|
|
49
|
+
lastName: true,
|
|
50
|
+
email: true,
|
|
51
|
+
phone: true,
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
assignedUser: {
|
|
55
|
+
select: {
|
|
56
|
+
id: true,
|
|
57
|
+
name: true,
|
|
58
|
+
email: true,
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
createdBy: {
|
|
62
|
+
select: {
|
|
63
|
+
id: true,
|
|
64
|
+
name: true,
|
|
65
|
+
email: true,
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
if (!task) {
|
|
72
|
+
return NextResponse.json({ error: 'Tâche non trouvée' }, { status: 404 });
|
|
73
|
+
}
|
|
74
|
+
|
|
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') {
|
|
82
|
+
return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return NextResponse.json(task);
|
|
86
|
+
} catch (error: any) {
|
|
87
|
+
console.error('Erreur lors de la récupération de la tâche:', error);
|
|
88
|
+
return NextResponse.json({ error: error.message || 'Erreur serveur' }, { status: 500 });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// PUT /api/tasks/[id] - Mettre à jour une tâche
|
|
93
|
+
export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
|
94
|
+
try {
|
|
95
|
+
const session = await auth.api.getSession({
|
|
96
|
+
headers: request.headers,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
if (!session) {
|
|
100
|
+
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const { id } = await params;
|
|
104
|
+
const body = await request.json();
|
|
105
|
+
const {
|
|
106
|
+
type,
|
|
107
|
+
title,
|
|
108
|
+
description,
|
|
109
|
+
priority,
|
|
110
|
+
scheduledAt,
|
|
111
|
+
assignedUserId,
|
|
112
|
+
completed,
|
|
113
|
+
reminderMinutesBefore,
|
|
114
|
+
durationMinutes,
|
|
115
|
+
attendees,
|
|
116
|
+
notifyContact,
|
|
117
|
+
internalNote,
|
|
118
|
+
} = body;
|
|
119
|
+
|
|
120
|
+
// Vérifier que la tâche existe
|
|
121
|
+
const existingTask = await prisma.task.findUnique({
|
|
122
|
+
where: { id },
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
if (!existingTask) {
|
|
126
|
+
return NextResponse.json({ error: 'Tâche non trouvée' }, { status: 404 });
|
|
127
|
+
}
|
|
128
|
+
|
|
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') {
|
|
136
|
+
return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Préparer les données de mise à jour
|
|
140
|
+
const updateData: any = {};
|
|
141
|
+
if (type !== undefined) updateData.type = type;
|
|
142
|
+
if (title !== undefined) updateData.title = title || null;
|
|
143
|
+
if (description !== undefined) updateData.description = description;
|
|
144
|
+
if (priority !== undefined) updateData.priority = priority;
|
|
145
|
+
if (scheduledAt !== undefined) updateData.scheduledAt = new Date(scheduledAt);
|
|
146
|
+
if (reminderMinutesBefore !== undefined) {
|
|
147
|
+
updateData.reminderMinutesBefore =
|
|
148
|
+
typeof reminderMinutesBefore === 'number' ? reminderMinutesBefore : null;
|
|
149
|
+
}
|
|
150
|
+
if (durationMinutes !== undefined) {
|
|
151
|
+
updateData.durationMinutes = durationMinutes || null;
|
|
152
|
+
}
|
|
153
|
+
if (notifyContact !== undefined) {
|
|
154
|
+
updateData.notifyContact = notifyContact === true;
|
|
155
|
+
}
|
|
156
|
+
if (internalNote !== undefined) {
|
|
157
|
+
updateData.internalNote = internalNote || null;
|
|
158
|
+
}
|
|
159
|
+
if (completed !== undefined) {
|
|
160
|
+
updateData.completed = completed;
|
|
161
|
+
updateData.completedAt = completed ? new Date() : null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Seuls les admins peuvent changer l'assignation
|
|
165
|
+
if (assignedUserId !== undefined && user?.role === 'ADMIN') {
|
|
166
|
+
updateData.assignedUserId = assignedUserId;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Si la tâche a un googleEventId, synchroniser avec Google Calendar
|
|
170
|
+
if (existingTask.googleEventId) {
|
|
171
|
+
try {
|
|
172
|
+
const googleAccount = await prisma.userGoogleAccount.findUnique({
|
|
173
|
+
where: { userId: session.user.id },
|
|
174
|
+
});
|
|
175
|
+
|
|
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
|
+
}
|
|
195
|
+
|
|
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
|
+
}
|
|
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
|
+
});
|
|
237
|
+
|
|
238
|
+
googleUpdate.attendees = allAttendees.length > 0 ? allAttendees : undefined;
|
|
239
|
+
}
|
|
240
|
+
|
|
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
|
+
}
|
|
253
|
+
}
|
|
254
|
+
} catch (googleError: any) {
|
|
255
|
+
console.error('Erreur lors de la synchronisation avec Google Calendar:', googleError);
|
|
256
|
+
// On continue quand même la mise à jour de la tâche locale
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Vérifier si la date/heure ou la durée a changé pour un Google Meet
|
|
261
|
+
const hasDateChanged = Boolean(
|
|
262
|
+
existingTask.googleEventId &&
|
|
263
|
+
scheduledAt !== undefined &&
|
|
264
|
+
new Date(scheduledAt).getTime() !== existingTask.scheduledAt.getTime(),
|
|
265
|
+
);
|
|
266
|
+
const hasDurationChanged = Boolean(
|
|
267
|
+
existingTask.googleEventId &&
|
|
268
|
+
durationMinutes !== undefined &&
|
|
269
|
+
durationMinutes !== existingTask.durationMinutes,
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
const task = await prisma.task.update({
|
|
273
|
+
where: { id },
|
|
274
|
+
data: updateData,
|
|
275
|
+
include: {
|
|
276
|
+
contact: {
|
|
277
|
+
select: {
|
|
278
|
+
id: true,
|
|
279
|
+
firstName: true,
|
|
280
|
+
lastName: true,
|
|
281
|
+
email: true,
|
|
282
|
+
phone: true,
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
assignedUser: {
|
|
286
|
+
select: {
|
|
287
|
+
id: true,
|
|
288
|
+
name: true,
|
|
289
|
+
email: true,
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
createdBy: {
|
|
293
|
+
select: {
|
|
294
|
+
id: true,
|
|
295
|
+
name: true,
|
|
296
|
+
email: true,
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// Créer une interaction pour la modification si c'est un rendez-vous
|
|
303
|
+
if (
|
|
304
|
+
task.contactId &&
|
|
305
|
+
(existingTask.type === 'MEETING' || existingTask.type === 'VIDEO_CONFERENCE')
|
|
306
|
+
) {
|
|
307
|
+
try {
|
|
308
|
+
await logAppointmentChanged(
|
|
309
|
+
task.contactId,
|
|
310
|
+
task.id,
|
|
311
|
+
task.scheduledAt,
|
|
312
|
+
task.title,
|
|
313
|
+
session.user.id,
|
|
314
|
+
existingTask.type === 'VIDEO_CONFERENCE',
|
|
315
|
+
);
|
|
316
|
+
} catch (interactionError: any) {
|
|
317
|
+
console.error(
|
|
318
|
+
"Erreur lors de la création de l'interaction de modification:",
|
|
319
|
+
interactionError,
|
|
320
|
+
);
|
|
321
|
+
// On continue même si l'interaction échoue
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Déterminer si on doit envoyer un email de modification
|
|
326
|
+
const shouldNotifyForGoogleMeet =
|
|
327
|
+
existingTask.googleEventId && task.contact?.email && (hasDateChanged || hasDurationChanged);
|
|
328
|
+
|
|
329
|
+
// Pour les rendez-vous physiques, envoyer un email si :
|
|
330
|
+
// - Le contact avait été prévenu initialement (existingTask.notifyContact === true)
|
|
331
|
+
// - OU l'utilisateur demande explicitement de prévenir (notifyContact === true)
|
|
332
|
+
const shouldNotifyForPhysicalMeeting =
|
|
333
|
+
existingTask.type === 'MEETING' &&
|
|
334
|
+
!existingTask.googleEventId &&
|
|
335
|
+
task.contact?.email &&
|
|
336
|
+
(existingTask.notifyContact === true || notifyContact === true);
|
|
337
|
+
|
|
338
|
+
// Envoyer un email de modification si nécessaire
|
|
339
|
+
if (shouldNotifyForGoogleMeet || shouldNotifyForPhysicalMeeting) {
|
|
340
|
+
try {
|
|
341
|
+
// Récupérer la configuration SMTP
|
|
342
|
+
const smtpConfig = await prisma.smtpConfig.findUnique({
|
|
343
|
+
where: { userId: session.user.id },
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
if (smtpConfig && task.googleMeetLink) {
|
|
347
|
+
// Récupérer les invités depuis Google Calendar
|
|
348
|
+
let allRecipients: string[] = [];
|
|
349
|
+
if (task.contact?.email) {
|
|
350
|
+
allRecipients.push(task.contact.email);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Pour Google Meet uniquement, récupérer les invités depuis Google Calendar
|
|
354
|
+
if (existingTask.googleEventId && task.googleMeetLink) {
|
|
355
|
+
try {
|
|
356
|
+
const googleAccount = await prisma.userGoogleAccount.findUnique({
|
|
357
|
+
where: { userId: session.user.id },
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
if (googleAccount && existingTask.googleEventId) {
|
|
361
|
+
const accessToken = await getValidAccessToken(
|
|
362
|
+
googleAccount.accessToken,
|
|
363
|
+
googleAccount.refreshToken,
|
|
364
|
+
googleAccount.tokenExpiresAt,
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
const googleEvent = await getGoogleCalendarEvent(
|
|
368
|
+
accessToken,
|
|
369
|
+
existingTask.googleEventId,
|
|
370
|
+
);
|
|
371
|
+
if (googleEvent.attendees) {
|
|
372
|
+
googleEvent.attendees.forEach((attendee) => {
|
|
373
|
+
if (attendee.email && !allRecipients.includes(attendee.email)) {
|
|
374
|
+
allRecipients.push(attendee.email);
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
} catch (googleError: any) {
|
|
380
|
+
console.error('Erreur lors de la récupération des invités:', googleError);
|
|
381
|
+
// On continue avec au moins le contact
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Déchiffrer le mot de passe SMTP
|
|
386
|
+
let password: string;
|
|
387
|
+
try {
|
|
388
|
+
password = decrypt(smtpConfig.password);
|
|
389
|
+
} catch (error) {
|
|
390
|
+
password = smtpConfig.password;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Créer le transporteur SMTP
|
|
394
|
+
const transporter = nodemailer.createTransport({
|
|
395
|
+
host: smtpConfig.host,
|
|
396
|
+
port: smtpConfig.port,
|
|
397
|
+
secure: smtpConfig.secure,
|
|
398
|
+
auth: {
|
|
399
|
+
user: smtpConfig.username,
|
|
400
|
+
pass: password,
|
|
401
|
+
},
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
// Récupérer le nom de l'organisateur
|
|
405
|
+
const organizer = await prisma.user.findUnique({
|
|
406
|
+
where: { id: session.user.id },
|
|
407
|
+
select: { name: true },
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
const contactName =
|
|
411
|
+
`${task.contact?.firstName || ''} ${task.contact?.lastName || ''}`.trim() ||
|
|
412
|
+
'Cher client';
|
|
413
|
+
const organizerName = organizer?.name || session.user.name || 'Organisateur';
|
|
414
|
+
|
|
415
|
+
const oldScheduledAt = existingTask.scheduledAt.toISOString();
|
|
416
|
+
const newScheduledAt = task.scheduledAt.toISOString();
|
|
417
|
+
const oldDuration = existingTask.durationMinutes ?? 30;
|
|
418
|
+
const newDuration = task.durationMinutes ?? 30;
|
|
419
|
+
|
|
420
|
+
// Générer le contenu HTML de l'email avec le composant React
|
|
421
|
+
const emailComponent = React.createElement(MeetUpdateEmailTemplate, {
|
|
422
|
+
contactName,
|
|
423
|
+
title: task.title || 'Rendez-vous',
|
|
424
|
+
oldScheduledAt,
|
|
425
|
+
newScheduledAt,
|
|
426
|
+
oldDuration,
|
|
427
|
+
newDuration,
|
|
428
|
+
hasDateChanged,
|
|
429
|
+
hasDurationChanged,
|
|
430
|
+
meetLink: task.googleMeetLink || undefined,
|
|
431
|
+
description: task.description,
|
|
432
|
+
organizerName,
|
|
433
|
+
signature: smtpConfig.signature || undefined,
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
const emailHtml = await render(emailComponent);
|
|
437
|
+
const emailText = htmlToText(emailHtml);
|
|
438
|
+
|
|
439
|
+
// Envoyer un email individuel à chaque destinataire pour préserver la confidentialité
|
|
440
|
+
if (allRecipients.length > 0) {
|
|
441
|
+
for (const recipientEmail of allRecipients) {
|
|
442
|
+
try {
|
|
443
|
+
await transporter.sendMail({
|
|
444
|
+
from: smtpConfig.fromName
|
|
445
|
+
? `"${smtpConfig.fromName}" <${smtpConfig.fromEmail}>`
|
|
446
|
+
: smtpConfig.fromEmail,
|
|
447
|
+
to: recipientEmail,
|
|
448
|
+
subject: `Modification de rendez-vous : ${task.title || 'Rendez-vous'}`,
|
|
449
|
+
text: emailText,
|
|
450
|
+
html: emailHtml,
|
|
451
|
+
});
|
|
452
|
+
} catch (individualEmailError: any) {
|
|
453
|
+
// Logger l'erreur mais continuer avec les autres destinataires
|
|
454
|
+
console.error(
|
|
455
|
+
`Erreur lors de l'envoi de l'email à ${recipientEmail}:`,
|
|
456
|
+
individualEmailError,
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
} catch (emailError: any) {
|
|
463
|
+
// Ne pas faire échouer la mise à jour si l'email échoue
|
|
464
|
+
console.error("Erreur lors de l'envoi de l'email de modification:", emailError);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return NextResponse.json(task);
|
|
469
|
+
} catch (error: any) {
|
|
470
|
+
console.error('Erreur lors de la mise à jour de la tâche:', error);
|
|
471
|
+
return NextResponse.json({ error: error.message || 'Erreur serveur' }, { status: 500 });
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// DELETE /api/tasks/[id] - Supprimer une tâche
|
|
476
|
+
export async function DELETE(
|
|
477
|
+
request: NextRequest,
|
|
478
|
+
{ params }: { params: Promise<{ id: string }> },
|
|
479
|
+
) {
|
|
480
|
+
try {
|
|
481
|
+
const session = await auth.api.getSession({
|
|
482
|
+
headers: request.headers,
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
if (!session) {
|
|
486
|
+
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const { id } = await params;
|
|
490
|
+
const body = await request.json().catch(() => ({}));
|
|
491
|
+
const { notifyContact } = body;
|
|
492
|
+
|
|
493
|
+
// Vérifier que la tâche existe
|
|
494
|
+
const task = await prisma.task.findUnique({
|
|
495
|
+
where: { id },
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
if (!task) {
|
|
499
|
+
return NextResponse.json({ error: 'Tâche non trouvée' }, { status: 404 });
|
|
500
|
+
}
|
|
501
|
+
|
|
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') {
|
|
509
|
+
return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Récupérer les informations du contact avant suppression pour l'email
|
|
513
|
+
const taskWithContact = await prisma.task.findUnique({
|
|
514
|
+
where: { id },
|
|
515
|
+
include: {
|
|
516
|
+
contact: {
|
|
517
|
+
select: {
|
|
518
|
+
id: true,
|
|
519
|
+
firstName: true,
|
|
520
|
+
lastName: true,
|
|
521
|
+
email: true,
|
|
522
|
+
},
|
|
523
|
+
},
|
|
524
|
+
assignedUser: {
|
|
525
|
+
select: {
|
|
526
|
+
id: true,
|
|
527
|
+
name: true,
|
|
528
|
+
email: true,
|
|
529
|
+
},
|
|
530
|
+
},
|
|
531
|
+
createdBy: {
|
|
532
|
+
select: {
|
|
533
|
+
id: true,
|
|
534
|
+
name: true,
|
|
535
|
+
email: true,
|
|
536
|
+
},
|
|
537
|
+
},
|
|
538
|
+
},
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
// Récupérer les invités depuis Google Calendar AVANT suppression pour l'email
|
|
542
|
+
let allRecipients: string[] = [];
|
|
543
|
+
if (task.googleEventId && taskWithContact?.contact?.email) {
|
|
544
|
+
allRecipients.push(taskWithContact.contact.email!);
|
|
545
|
+
|
|
546
|
+
try {
|
|
547
|
+
const googleAccount = await prisma.userGoogleAccount.findUnique({
|
|
548
|
+
where: { userId: session.user.id },
|
|
549
|
+
});
|
|
550
|
+
|
|
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
|
+
}
|
|
570
|
+
|
|
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
|
+
}
|
|
580
|
+
|
|
581
|
+
// Supprimer l'événement Google Calendar
|
|
582
|
+
await deleteGoogleCalendarEvent(accessToken, task.googleEventId);
|
|
583
|
+
}
|
|
584
|
+
} catch (googleError: any) {
|
|
585
|
+
console.error("Erreur lors de la suppression de l'événement Google Calendar:", googleError);
|
|
586
|
+
// On continue quand même la suppression de la tâche
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Créer une interaction pour l'annulation si c'est un Google Meet ou un RDV
|
|
591
|
+
if (task.contactId && (task.type === 'VIDEO_CONFERENCE' || task.type === 'MEETING')) {
|
|
592
|
+
try {
|
|
593
|
+
console.log('Création interaction annulation pour task:', {
|
|
594
|
+
contactId: task.contactId,
|
|
595
|
+
taskId: task.id,
|
|
596
|
+
type: task.type,
|
|
597
|
+
title: task.title,
|
|
598
|
+
});
|
|
599
|
+
await logAppointmentCancelled(
|
|
600
|
+
task.contactId,
|
|
601
|
+
task.id,
|
|
602
|
+
task.scheduledAt,
|
|
603
|
+
task.title,
|
|
604
|
+
session.user.id,
|
|
605
|
+
task.type === 'VIDEO_CONFERENCE',
|
|
606
|
+
);
|
|
607
|
+
console.log("Interaction d'annulation créée avec succès");
|
|
608
|
+
} catch (interactionError: any) {
|
|
609
|
+
console.error(
|
|
610
|
+
"Erreur lors de la création de l'interaction d'annulation:",
|
|
611
|
+
interactionError,
|
|
612
|
+
);
|
|
613
|
+
// On continue même si l'interaction échoue
|
|
614
|
+
}
|
|
615
|
+
} else {
|
|
616
|
+
console.log("Pas de création d'interaction - conditions non remplies:", {
|
|
617
|
+
contactId: task.contactId,
|
|
618
|
+
type: task.type,
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Supprimer la tâche
|
|
623
|
+
await prisma.task.delete({
|
|
624
|
+
where: { id },
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
// Déterminer si on doit envoyer un email d'annulation
|
|
628
|
+
const shouldNotifyForGoogleMeet =
|
|
629
|
+
task.googleEventId && allRecipients.length > 0 && task.googleMeetLink;
|
|
630
|
+
|
|
631
|
+
// Pour les rendez-vous physiques, envoyer un email si :
|
|
632
|
+
// - Le contact avait été prévenu initialement (task.notifyContact === true)
|
|
633
|
+
// - OU l'utilisateur demande explicitement de prévenir (notifyContact === true)
|
|
634
|
+
const shouldNotifyForPhysicalMeeting =
|
|
635
|
+
task.type === 'MEETING' &&
|
|
636
|
+
!task.googleEventId &&
|
|
637
|
+
taskWithContact?.contact?.email &&
|
|
638
|
+
(task.notifyContact === true || notifyContact === true);
|
|
639
|
+
|
|
640
|
+
// Envoyer un email d'annulation si nécessaire
|
|
641
|
+
if (shouldNotifyForGoogleMeet || shouldNotifyForPhysicalMeeting) {
|
|
642
|
+
try {
|
|
643
|
+
// Récupérer la configuration SMTP
|
|
644
|
+
const smtpConfig = await prisma.smtpConfig.findUnique({
|
|
645
|
+
where: { userId: session.user.id },
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
if (smtpConfig) {
|
|
649
|
+
// Déchiffrer le mot de passe SMTP
|
|
650
|
+
let password: string;
|
|
651
|
+
try {
|
|
652
|
+
password = decrypt(smtpConfig.password);
|
|
653
|
+
} catch (error) {
|
|
654
|
+
password = smtpConfig.password;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// Créer le transporteur SMTP
|
|
658
|
+
const transporter = nodemailer.createTransport({
|
|
659
|
+
host: smtpConfig.host,
|
|
660
|
+
port: smtpConfig.port,
|
|
661
|
+
secure: smtpConfig.secure,
|
|
662
|
+
auth: {
|
|
663
|
+
user: smtpConfig.username,
|
|
664
|
+
pass: password,
|
|
665
|
+
},
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
// Récupérer le nom de l'organisateur
|
|
669
|
+
const organizer = await prisma.user.findUnique({
|
|
670
|
+
where: { id: session.user.id },
|
|
671
|
+
select: { name: true },
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
const contactName =
|
|
675
|
+
`${taskWithContact?.contact?.firstName || ''} ${taskWithContact?.contact?.lastName || ''}`.trim() ||
|
|
676
|
+
'Cher client';
|
|
677
|
+
const organizerName = organizer?.name || session.user.name || 'Organisateur';
|
|
678
|
+
|
|
679
|
+
// Générer le contenu HTML de l'email avec le composant React
|
|
680
|
+
const emailComponent = React.createElement(MeetCancellationEmailTemplate, {
|
|
681
|
+
contactName,
|
|
682
|
+
title: task.title || 'Rendez-vous',
|
|
683
|
+
scheduledAt: task.scheduledAt.toISOString(),
|
|
684
|
+
durationMinutes: task.googleMeetLink ? (task.durationMinutes ?? 30) : undefined,
|
|
685
|
+
meetLink: task.googleMeetLink || undefined,
|
|
686
|
+
description: task.description,
|
|
687
|
+
organizerName,
|
|
688
|
+
signature: smtpConfig.signature || undefined,
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
const emailHtml = await render(emailComponent);
|
|
692
|
+
const emailText = htmlToText(emailHtml);
|
|
693
|
+
|
|
694
|
+
// Envoyer un email individuel à chaque destinataire pour préserver la confidentialité
|
|
695
|
+
if (allRecipients.length > 0) {
|
|
696
|
+
for (const recipientEmail of allRecipients) {
|
|
697
|
+
try {
|
|
698
|
+
await transporter.sendMail({
|
|
699
|
+
from: smtpConfig.fromName
|
|
700
|
+
? `"${smtpConfig.fromName}" <${smtpConfig.fromEmail}>`
|
|
701
|
+
: smtpConfig.fromEmail,
|
|
702
|
+
to: recipientEmail,
|
|
703
|
+
subject: `Annulation de rendez-vous : ${task.title || 'Rendez-vous'}`,
|
|
704
|
+
text: emailText,
|
|
705
|
+
html: emailHtml,
|
|
706
|
+
});
|
|
707
|
+
} catch (individualEmailError: any) {
|
|
708
|
+
// Logger l'erreur mais continuer avec les autres destinataires
|
|
709
|
+
console.error(
|
|
710
|
+
`Erreur lors de l'envoi de l'email à ${recipientEmail}:`,
|
|
711
|
+
individualEmailError,
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
} catch (emailError: any) {
|
|
718
|
+
// Ne pas faire échouer la suppression si l'email échoue
|
|
719
|
+
console.error("Erreur lors de l'envoi de l'email d'annulation:", emailError);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
return NextResponse.json({ success: true });
|
|
724
|
+
} catch (error: any) {
|
|
725
|
+
console.error('Erreur lors de la suppression de la tâche:', error);
|
|
726
|
+
return NextResponse.json({ error: error.message || 'Erreur serveur' }, { status: 500 });
|
|
727
|
+
}
|
|
728
|
+
}
|