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,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
|
+
}
|