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,296 @@
|
|
|
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/contacts/[id]/meet
|
|
27
|
+
* Crée un Google Meet depuis un contact
|
|
28
|
+
*/
|
|
29
|
+
export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
|
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 { id: contactId } = await params;
|
|
40
|
+
const body = await request.json();
|
|
41
|
+
const {
|
|
42
|
+
title,
|
|
43
|
+
description,
|
|
44
|
+
scheduledAt,
|
|
45
|
+
durationMinutes = 30,
|
|
46
|
+
attendees = [],
|
|
47
|
+
reminderMinutesBefore,
|
|
48
|
+
internalNote,
|
|
49
|
+
} = body;
|
|
50
|
+
|
|
51
|
+
// Validation
|
|
52
|
+
if (!title || !scheduledAt) {
|
|
53
|
+
return NextResponse.json({ error: 'Le titre et la date/heure sont requis' }, { status: 400 });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Vérifier que le contact existe
|
|
57
|
+
const contact = await prisma.contact.findUnique({
|
|
58
|
+
where: { id: contactId },
|
|
59
|
+
select: { id: true, email: true, firstName: true, lastName: true },
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
if (!contact) {
|
|
63
|
+
return NextResponse.json({ error: 'Contact non trouvé' }, { status: 404 });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Vérifier que l'utilisateur a un compte Google connecté
|
|
67
|
+
const googleAccount = await prisma.userGoogleAccount.findUnique({
|
|
68
|
+
where: { userId: session.user.id },
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
if (!googleAccount) {
|
|
72
|
+
return NextResponse.json(
|
|
73
|
+
{ error: 'Veuillez connecter votre compte Google dans les paramètres' },
|
|
74
|
+
{ status: 400 },
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Obtenir un token valide
|
|
79
|
+
const accessToken = await getValidAccessToken(
|
|
80
|
+
googleAccount.accessToken,
|
|
81
|
+
googleAccount.refreshToken,
|
|
82
|
+
googleAccount.tokenExpiresAt,
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// Mettre à jour le token si nécessaire
|
|
86
|
+
if (accessToken !== googleAccount.accessToken) {
|
|
87
|
+
const tokenExpiresAt = new Date();
|
|
88
|
+
tokenExpiresAt.setSeconds(tokenExpiresAt.getSeconds() + 3600);
|
|
89
|
+
await prisma.userGoogleAccount.update({
|
|
90
|
+
where: { userId: session.user.id },
|
|
91
|
+
data: {
|
|
92
|
+
accessToken,
|
|
93
|
+
tokenExpiresAt,
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Préparer les dates pour Google Calendar
|
|
99
|
+
const startDate = new Date(scheduledAt);
|
|
100
|
+
const endDate = new Date(startDate.getTime() + durationMinutes * 60 * 1000);
|
|
101
|
+
|
|
102
|
+
// Construire la liste des invités (contact + invités additionnels)
|
|
103
|
+
const allAttendees = [];
|
|
104
|
+
if (contact.email) {
|
|
105
|
+
allAttendees.push({ email: contact.email });
|
|
106
|
+
}
|
|
107
|
+
// Ajouter les autres invités
|
|
108
|
+
attendees.forEach((email: string) => {
|
|
109
|
+
if (email && email !== contact.email) {
|
|
110
|
+
allAttendees.push({ email });
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Créer l'évènement Google Calendar avec Meet
|
|
115
|
+
const googleEvent = await createGoogleCalendarEvent(accessToken, {
|
|
116
|
+
summary: title,
|
|
117
|
+
description: description || '',
|
|
118
|
+
start: {
|
|
119
|
+
dateTime: startDate.toISOString(),
|
|
120
|
+
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
121
|
+
},
|
|
122
|
+
end: {
|
|
123
|
+
dateTime: endDate.toISOString(),
|
|
124
|
+
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
125
|
+
},
|
|
126
|
+
attendees: allAttendees.length > 0 ? allAttendees : undefined,
|
|
127
|
+
conferenceData: {
|
|
128
|
+
createRequest: {
|
|
129
|
+
requestId: `meet-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
130
|
+
conferenceSolutionKey: {
|
|
131
|
+
type: 'hangoutsMeet',
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
conferenceDataVersion: 1,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const meetLink = extractMeetLink(googleEvent);
|
|
139
|
+
|
|
140
|
+
// Créer la tâche dans le CRM
|
|
141
|
+
const task = await prisma.task.create({
|
|
142
|
+
data: {
|
|
143
|
+
type: 'VIDEO_CONFERENCE',
|
|
144
|
+
title,
|
|
145
|
+
description: description || '',
|
|
146
|
+
priority: 'MEDIUM',
|
|
147
|
+
scheduledAt: startDate,
|
|
148
|
+
reminderMinutesBefore: reminderMinutesBefore || null,
|
|
149
|
+
contactId: contactId,
|
|
150
|
+
assignedUserId: session.user.id,
|
|
151
|
+
createdById: session.user.id,
|
|
152
|
+
googleEventId: googleEvent.id,
|
|
153
|
+
googleMeetLink: meetLink,
|
|
154
|
+
durationMinutes,
|
|
155
|
+
internalNote: internalNote || null,
|
|
156
|
+
},
|
|
157
|
+
include: {
|
|
158
|
+
contact: {
|
|
159
|
+
select: {
|
|
160
|
+
id: true,
|
|
161
|
+
firstName: true,
|
|
162
|
+
lastName: true,
|
|
163
|
+
email: true,
|
|
164
|
+
phone: true,
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
assignedUser: {
|
|
168
|
+
select: {
|
|
169
|
+
id: true,
|
|
170
|
+
name: true,
|
|
171
|
+
email: true,
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
createdBy: {
|
|
175
|
+
select: {
|
|
176
|
+
id: true,
|
|
177
|
+
name: true,
|
|
178
|
+
email: true,
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Créer aussi une interaction pour le contact
|
|
185
|
+
await prisma.interaction.create({
|
|
186
|
+
data: {
|
|
187
|
+
contactId,
|
|
188
|
+
type: 'MEETING',
|
|
189
|
+
title: `Google Meet: ${title}`,
|
|
190
|
+
content:
|
|
191
|
+
description || `Réunion Google Meet programmée${meetLink ? `\n\nLien: ${meetLink}` : ''}`,
|
|
192
|
+
userId: session.user.id,
|
|
193
|
+
date: startDate,
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// Envoyer l'email de confirmation au contact si un email est disponible
|
|
198
|
+
if (contact.email && meetLink) {
|
|
199
|
+
try {
|
|
200
|
+
// Récupérer la configuration SMTP
|
|
201
|
+
const smtpConfig = await prisma.smtpConfig.findUnique({
|
|
202
|
+
where: { userId: session.user.id },
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
if (smtpConfig) {
|
|
206
|
+
// Déchiffrer le mot de passe SMTP
|
|
207
|
+
let password: string;
|
|
208
|
+
try {
|
|
209
|
+
password = decrypt(smtpConfig.password);
|
|
210
|
+
} catch (error) {
|
|
211
|
+
password = smtpConfig.password;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Créer le transporteur SMTP
|
|
215
|
+
const transporter = nodemailer.createTransport({
|
|
216
|
+
host: smtpConfig.host,
|
|
217
|
+
port: smtpConfig.port,
|
|
218
|
+
secure: smtpConfig.secure,
|
|
219
|
+
auth: {
|
|
220
|
+
user: smtpConfig.username,
|
|
221
|
+
pass: password,
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// Récupérer le nom de l'organisateur
|
|
226
|
+
const organizer = await prisma.user.findUnique({
|
|
227
|
+
where: { id: session.user.id },
|
|
228
|
+
select: { name: true },
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const contactName =
|
|
232
|
+
`${contact.firstName || ''} ${contact.lastName || ''}`.trim() || 'Cher client';
|
|
233
|
+
const organizerName = organizer?.name || session.user.name || 'Organisateur';
|
|
234
|
+
|
|
235
|
+
// Générer le contenu HTML de l'email avec le composant React
|
|
236
|
+
const emailComponent = React.createElement(MeetConfirmationEmailTemplate, {
|
|
237
|
+
contactName,
|
|
238
|
+
title,
|
|
239
|
+
scheduledAt: startDate.toISOString(),
|
|
240
|
+
durationMinutes,
|
|
241
|
+
meetLink: meetLink,
|
|
242
|
+
description,
|
|
243
|
+
organizerName,
|
|
244
|
+
signature: smtpConfig.signature || undefined,
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
const emailHtml = await render(emailComponent);
|
|
248
|
+
const emailText = htmlToText(emailHtml);
|
|
249
|
+
|
|
250
|
+
// Construire la liste de tous les destinataires (contact + invités additionnels)
|
|
251
|
+
const allRecipients: string[] = [];
|
|
252
|
+
if (contact.email) {
|
|
253
|
+
allRecipients.push(contact.email);
|
|
254
|
+
}
|
|
255
|
+
// Ajouter les autres invités
|
|
256
|
+
attendees.forEach((email: string) => {
|
|
257
|
+
if (email && email.trim() !== '' && email !== contact.email) {
|
|
258
|
+
allRecipients.push(email.trim());
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// Envoyer un email individuel à chaque destinataire pour préserver la confidentialité
|
|
263
|
+
if (allRecipients.length > 0) {
|
|
264
|
+
for (const recipientEmail of allRecipients) {
|
|
265
|
+
try {
|
|
266
|
+
await transporter.sendMail({
|
|
267
|
+
from: smtpConfig.fromName
|
|
268
|
+
? `"${smtpConfig.fromName}" <${smtpConfig.fromEmail}>`
|
|
269
|
+
: smtpConfig.fromEmail,
|
|
270
|
+
to: recipientEmail,
|
|
271
|
+
subject: `Confirmation de rendez-vous : ${title}`,
|
|
272
|
+
text: emailText,
|
|
273
|
+
html: emailHtml,
|
|
274
|
+
});
|
|
275
|
+
} catch (individualEmailError: any) {
|
|
276
|
+
// Logger l'erreur mais continuer avec les autres destinataires
|
|
277
|
+
console.error(
|
|
278
|
+
`Erreur lors de l'envoi de l'email à ${recipientEmail}:`,
|
|
279
|
+
individualEmailError,
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
} catch (emailError: any) {
|
|
286
|
+
// Ne pas faire échouer la création du Meet si l'email échoue
|
|
287
|
+
console.error("Erreur lors de l'envoi de l'email de confirmation:", emailError);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return NextResponse.json(task, { status: 201 });
|
|
292
|
+
} catch (error: any) {
|
|
293
|
+
console.error('Erreur lors de la création du Google Meet:', error);
|
|
294
|
+
return NextResponse.json({ error: error.message || 'Erreur serveur' }, { status: 500 });
|
|
295
|
+
}
|
|
296
|
+
}
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { auth } from '@/lib/auth';
|
|
3
|
+
import { prisma } from '@/lib/prisma';
|
|
4
|
+
import { logStatusChange, logContactUpdate, logAssignmentChange } from '@/lib/contact-interactions';
|
|
5
|
+
import { executeWorkflowsOnStatusChanged } from '@/lib/workflow-executor';
|
|
6
|
+
import { normalizePhoneNumber } from '@/lib/utils';
|
|
7
|
+
|
|
8
|
+
// GET /api/contacts/[id] - Récupérer un contact spécifique avec ses interactions
|
|
9
|
+
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
|
10
|
+
try {
|
|
11
|
+
const session = await auth.api.getSession({
|
|
12
|
+
headers: request.headers,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
if (!session) {
|
|
16
|
+
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const { id } = await params;
|
|
20
|
+
|
|
21
|
+
const contact = await prisma.contact.findUnique({
|
|
22
|
+
where: { id },
|
|
23
|
+
include: {
|
|
24
|
+
status: true,
|
|
25
|
+
companyRelation: {
|
|
26
|
+
select: { id: true, firstName: true, lastName: true, isCompany: true },
|
|
27
|
+
},
|
|
28
|
+
contacts: {
|
|
29
|
+
select: { id: true, firstName: true, lastName: true, isCompany: true },
|
|
30
|
+
},
|
|
31
|
+
assignedCommercial: {
|
|
32
|
+
select: { id: true, name: true, email: true },
|
|
33
|
+
},
|
|
34
|
+
assignedTelepro: {
|
|
35
|
+
select: { id: true, name: true, email: true },
|
|
36
|
+
},
|
|
37
|
+
createdBy: {
|
|
38
|
+
select: { id: true, name: true, email: true },
|
|
39
|
+
},
|
|
40
|
+
interactions: {
|
|
41
|
+
include: {
|
|
42
|
+
user: {
|
|
43
|
+
select: { id: true, name: true, email: true },
|
|
44
|
+
},
|
|
45
|
+
emailTracking: {
|
|
46
|
+
select: {
|
|
47
|
+
id: true,
|
|
48
|
+
openCount: true,
|
|
49
|
+
firstOpenedAt: true,
|
|
50
|
+
lastOpenedAt: true,
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
orderBy: { createdAt: 'desc' },
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (!contact) {
|
|
60
|
+
return NextResponse.json({ error: 'Contact non trouvé' }, { status: 404 });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return NextResponse.json(contact);
|
|
64
|
+
} catch (error: any) {
|
|
65
|
+
console.error('Erreur lors de la récupération du contact:', error);
|
|
66
|
+
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// PUT /api/contacts/[id] - Mettre à jour un contact
|
|
71
|
+
export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
|
72
|
+
try {
|
|
73
|
+
const session = await auth.api.getSession({
|
|
74
|
+
headers: request.headers,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
if (!session) {
|
|
78
|
+
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const { id } = await params;
|
|
82
|
+
const body = await request.json();
|
|
83
|
+
const {
|
|
84
|
+
civility,
|
|
85
|
+
firstName,
|
|
86
|
+
lastName,
|
|
87
|
+
phone,
|
|
88
|
+
secondaryPhone,
|
|
89
|
+
email,
|
|
90
|
+
address,
|
|
91
|
+
city,
|
|
92
|
+
postalCode,
|
|
93
|
+
origin,
|
|
94
|
+
companyName,
|
|
95
|
+
isCompany,
|
|
96
|
+
companyId,
|
|
97
|
+
statusId,
|
|
98
|
+
closingReason,
|
|
99
|
+
assignedCommercialId,
|
|
100
|
+
assignedTeleproId,
|
|
101
|
+
} = body;
|
|
102
|
+
|
|
103
|
+
// Vérifier que le contact existe avec toutes les relations nécessaires
|
|
104
|
+
const existing = await prisma.contact.findUnique({
|
|
105
|
+
where: { id },
|
|
106
|
+
include: {
|
|
107
|
+
status: true,
|
|
108
|
+
companyRelation: {
|
|
109
|
+
select: { id: true, firstName: true, lastName: true, isCompany: true },
|
|
110
|
+
},
|
|
111
|
+
assignedCommercial: {
|
|
112
|
+
select: { id: true, name: true, email: true },
|
|
113
|
+
},
|
|
114
|
+
assignedTelepro: {
|
|
115
|
+
select: { id: true, name: true, email: true },
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
if (!existing) {
|
|
121
|
+
return NextResponse.json({ error: 'Contact non trouvé' }, { status: 404 });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Validation : si phone est fourni, il ne peut pas être vide
|
|
125
|
+
if (phone !== undefined && !phone) {
|
|
126
|
+
return NextResponse.json({ error: 'Le téléphone ne peut pas être vide' }, { status: 400 });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Préparer les données de mise à jour
|
|
130
|
+
const updateData: any = {
|
|
131
|
+
civility: civility !== undefined ? civility || null : existing.civility,
|
|
132
|
+
firstName: firstName !== undefined ? firstName || null : existing.firstName,
|
|
133
|
+
lastName: lastName !== undefined ? lastName || null : existing.lastName,
|
|
134
|
+
phone: phone !== undefined ? normalizePhoneNumber(phone) : existing.phone,
|
|
135
|
+
secondaryPhone:
|
|
136
|
+
secondaryPhone !== undefined
|
|
137
|
+
? normalizePhoneNumber(secondaryPhone) || null
|
|
138
|
+
: existing.secondaryPhone,
|
|
139
|
+
email: email !== undefined ? email || null : existing.email,
|
|
140
|
+
address: address !== undefined ? address || null : existing.address,
|
|
141
|
+
city: city !== undefined ? city || null : existing.city,
|
|
142
|
+
postalCode: postalCode !== undefined ? postalCode || null : existing.postalCode,
|
|
143
|
+
origin: origin !== undefined ? origin || null : existing.origin,
|
|
144
|
+
companyName: companyName !== undefined ? companyName || null : existing.companyName,
|
|
145
|
+
isCompany: isCompany !== undefined ? isCompany === true : existing.isCompany,
|
|
146
|
+
companyId: companyId !== undefined ? companyId || null : existing.companyId,
|
|
147
|
+
statusId: statusId !== undefined ? statusId || null : existing.statusId,
|
|
148
|
+
closingReason:
|
|
149
|
+
closingReason !== undefined ? closingReason || null : existing.closingReason,
|
|
150
|
+
assignedCommercialId:
|
|
151
|
+
assignedCommercialId !== undefined
|
|
152
|
+
? assignedCommercialId || null
|
|
153
|
+
: existing.assignedCommercialId,
|
|
154
|
+
assignedTeleproId:
|
|
155
|
+
assignedTeleproId !== undefined ? assignedTeleproId || null : existing.assignedTeleproId,
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// Détecter les changements pour créer les interactions
|
|
159
|
+
const changes: Record<string, { old: any; new: any }> = {};
|
|
160
|
+
|
|
161
|
+
// Changements de champs de contact
|
|
162
|
+
if (civility !== undefined && civility !== existing.civility) {
|
|
163
|
+
changes.civility = { old: existing.civility, new: civility };
|
|
164
|
+
}
|
|
165
|
+
if (firstName !== undefined && firstName !== existing.firstName) {
|
|
166
|
+
changes.firstName = { old: existing.firstName, new: firstName };
|
|
167
|
+
}
|
|
168
|
+
if (lastName !== undefined && lastName !== existing.lastName) {
|
|
169
|
+
changes.lastName = { old: existing.lastName, new: lastName };
|
|
170
|
+
}
|
|
171
|
+
if (phone !== undefined && phone !== existing.phone) {
|
|
172
|
+
changes.phone = { old: existing.phone, new: phone };
|
|
173
|
+
}
|
|
174
|
+
if (secondaryPhone !== undefined && secondaryPhone !== existing.secondaryPhone) {
|
|
175
|
+
changes.secondaryPhone = { old: existing.secondaryPhone, new: secondaryPhone };
|
|
176
|
+
}
|
|
177
|
+
if (email !== undefined && email !== existing.email) {
|
|
178
|
+
changes.email = { old: existing.email, new: email };
|
|
179
|
+
}
|
|
180
|
+
if (address !== undefined && address !== existing.address) {
|
|
181
|
+
changes.address = { old: existing.address, new: address };
|
|
182
|
+
}
|
|
183
|
+
if (city !== undefined && city !== existing.city) {
|
|
184
|
+
changes.city = { old: existing.city, new: city };
|
|
185
|
+
}
|
|
186
|
+
if (postalCode !== undefined && postalCode !== existing.postalCode) {
|
|
187
|
+
changes.postalCode = { old: existing.postalCode, new: postalCode };
|
|
188
|
+
}
|
|
189
|
+
if (origin !== undefined && origin !== existing.origin) {
|
|
190
|
+
changes.origin = { old: existing.origin, new: origin };
|
|
191
|
+
}
|
|
192
|
+
if (companyName !== undefined && companyName !== existing.companyName) {
|
|
193
|
+
changes.companyName = { old: existing.companyName, new: companyName };
|
|
194
|
+
}
|
|
195
|
+
if (closingReason !== undefined && closingReason !== existing.closingReason) {
|
|
196
|
+
changes.closingReason = { old: existing.closingReason, new: closingReason };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Mettre à jour le contact
|
|
200
|
+
const contact = await prisma.contact.update({
|
|
201
|
+
where: { id },
|
|
202
|
+
data: updateData,
|
|
203
|
+
include: {
|
|
204
|
+
status: true,
|
|
205
|
+
assignedCommercial: {
|
|
206
|
+
select: { id: true, name: true, email: true },
|
|
207
|
+
},
|
|
208
|
+
assignedTelepro: {
|
|
209
|
+
select: { id: true, name: true, email: true },
|
|
210
|
+
},
|
|
211
|
+
createdBy: {
|
|
212
|
+
select: { id: true, name: true, email: true },
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// Créer les interactions pour les changements détectés
|
|
218
|
+
try {
|
|
219
|
+
// Changement de statut
|
|
220
|
+
if (statusId !== undefined && statusId !== existing.statusId) {
|
|
221
|
+
await logStatusChange(
|
|
222
|
+
id,
|
|
223
|
+
existing.statusId,
|
|
224
|
+
statusId,
|
|
225
|
+
session.user.id,
|
|
226
|
+
existing.status?.name || null,
|
|
227
|
+
contact.status?.name || null,
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
// Exécuter les workflows déclenchés par le changement de statut
|
|
231
|
+
try {
|
|
232
|
+
await executeWorkflowsOnStatusChanged(id, existing.statusId, statusId);
|
|
233
|
+
} catch (workflowError) {
|
|
234
|
+
// Ne pas faire échouer la mise à jour si les workflows échouent
|
|
235
|
+
console.error('Erreur lors de l\'exécution des workflows:', workflowError);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Changement d'assignation Commercial
|
|
240
|
+
// Normaliser les valeurs pour la comparaison (null, undefined, '' sont considérés comme équivalents)
|
|
241
|
+
const normalizedExistingCommercial = existing.assignedCommercialId || null;
|
|
242
|
+
const normalizedNewCommercial = assignedCommercialId || null;
|
|
243
|
+
|
|
244
|
+
if (
|
|
245
|
+
assignedCommercialId !== undefined &&
|
|
246
|
+
normalizedExistingCommercial !== normalizedNewCommercial
|
|
247
|
+
) {
|
|
248
|
+
await logAssignmentChange(
|
|
249
|
+
id,
|
|
250
|
+
'COMMERCIAL',
|
|
251
|
+
existing.assignedCommercialId,
|
|
252
|
+
assignedCommercialId,
|
|
253
|
+
session.user.id,
|
|
254
|
+
existing.assignedCommercial?.name || null,
|
|
255
|
+
contact.assignedCommercial?.name || null,
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Changement d'assignation Télépro
|
|
260
|
+
// Normaliser les valeurs pour la comparaison (null, undefined, '' sont considérés comme équivalents)
|
|
261
|
+
const normalizedExistingTelepro = existing.assignedTeleproId || null;
|
|
262
|
+
const normalizedNewTelepro = assignedTeleproId || null;
|
|
263
|
+
|
|
264
|
+
if (assignedTeleproId !== undefined && normalizedExistingTelepro !== normalizedNewTelepro) {
|
|
265
|
+
await logAssignmentChange(
|
|
266
|
+
id,
|
|
267
|
+
'TELEPRO',
|
|
268
|
+
existing.assignedTeleproId,
|
|
269
|
+
assignedTeleproId,
|
|
270
|
+
session.user.id,
|
|
271
|
+
existing.assignedTelepro?.name || null,
|
|
272
|
+
contact.assignedTelepro?.name || null,
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Changements de champs de contact
|
|
277
|
+
if (Object.keys(changes).length > 0) {
|
|
278
|
+
await logContactUpdate(id, changes, session.user.id);
|
|
279
|
+
}
|
|
280
|
+
} catch (error) {
|
|
281
|
+
// Ne pas faire échouer la mise à jour si l'enregistrement de l'interaction échoue
|
|
282
|
+
console.error('Erreur lors de la création des interactions:', error);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return NextResponse.json(contact);
|
|
286
|
+
} catch (error: any) {
|
|
287
|
+
console.error('Erreur lors de la mise à jour du contact:', error);
|
|
288
|
+
return NextResponse.json({ error: error.message || 'Erreur serveur' }, { status: 500 });
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// DELETE /api/contacts/[id] - Supprimer un contact
|
|
293
|
+
export async function DELETE(
|
|
294
|
+
request: NextRequest,
|
|
295
|
+
{ params }: { params: Promise<{ id: string }> },
|
|
296
|
+
) {
|
|
297
|
+
try {
|
|
298
|
+
// Vérifier que l'utilisateur est administrateur
|
|
299
|
+
const { requireAdmin } = await import('@/lib/roles');
|
|
300
|
+
await requireAdmin(request.headers);
|
|
301
|
+
|
|
302
|
+
const { id } = await params;
|
|
303
|
+
|
|
304
|
+
// Vérifier que le contact existe
|
|
305
|
+
const existing = await prisma.contact.findUnique({
|
|
306
|
+
where: { id },
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
if (!existing) {
|
|
310
|
+
return NextResponse.json({ error: 'Contact non trouvé' }, { status: 404 });
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
await prisma.contact.delete({
|
|
314
|
+
where: { id },
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
return NextResponse.json({ success: true, message: 'Contact supprimé avec succès' });
|
|
318
|
+
} catch (error: any) {
|
|
319
|
+
console.error('Erreur lors de la suppression du contact:', error);
|
|
320
|
+
return NextResponse.json({ error: error.message || 'Erreur serveur' }, { status: 500 });
|
|
321
|
+
}
|
|
322
|
+
}
|