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,254 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { auth } from '@/lib/auth';
|
|
3
|
+
import { prisma } from '@/lib/prisma';
|
|
4
|
+
import nodemailer from 'nodemailer';
|
|
5
|
+
import { decrypt } from '@/lib/encryption';
|
|
6
|
+
|
|
7
|
+
// POST /api/contacts/[id]/send-email - Envoyer un email au contact
|
|
8
|
+
function htmlToText(html: string): string {
|
|
9
|
+
if (!html) return '';
|
|
10
|
+
return html
|
|
11
|
+
.replace(/<br\s*\/?>/gi, '\n')
|
|
12
|
+
.replace(/<\/p>/gi, '\n\n')
|
|
13
|
+
.replace(/<[^>]+>/g, '')
|
|
14
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
15
|
+
.trim();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
|
19
|
+
try {
|
|
20
|
+
const session = await auth.api.getSession({
|
|
21
|
+
headers: request.headers,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
if (!session) {
|
|
25
|
+
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const { id } = await params;
|
|
29
|
+
|
|
30
|
+
// Récupérer FormData
|
|
31
|
+
const formData = await request.formData();
|
|
32
|
+
const to = formData.get('to') as string;
|
|
33
|
+
const cc = formData.get('cc') as string;
|
|
34
|
+
const bcc = formData.get('bcc') as string;
|
|
35
|
+
const subject = formData.get('subject') as string;
|
|
36
|
+
const content = formData.get('content') as string;
|
|
37
|
+
const attachmentCount = parseInt(formData.get('attachmentCount') as string) || 0;
|
|
38
|
+
|
|
39
|
+
// Validation
|
|
40
|
+
if (!to || !subject || !content) {
|
|
41
|
+
return NextResponse.json(
|
|
42
|
+
{ error: 'Le destinataire, le sujet et le contenu sont requis' },
|
|
43
|
+
{ status: 400 },
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Récupérer le contact
|
|
48
|
+
const contact = await prisma.contact.findUnique({
|
|
49
|
+
where: { id },
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
if (!contact) {
|
|
53
|
+
return NextResponse.json({ error: 'Contact non trouvé' }, { status: 404 });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Récupérer la configuration SMTP de l'utilisateur
|
|
57
|
+
const smtpConfig = await prisma.smtpConfig.findUnique({
|
|
58
|
+
where: { userId: session.user.id },
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
if (!smtpConfig) {
|
|
62
|
+
return NextResponse.json(
|
|
63
|
+
{
|
|
64
|
+
error:
|
|
65
|
+
'Configuration SMTP non trouvée. Veuillez configurer votre SMTP dans les paramètres.',
|
|
66
|
+
},
|
|
67
|
+
{ status: 400 },
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Déchiffrer le mot de passe SMTP
|
|
72
|
+
let password: string;
|
|
73
|
+
try {
|
|
74
|
+
password = decrypt(smtpConfig.password);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
// Si le déchiffrement échoue, utiliser le mot de passe tel quel (ancien format non chiffré)
|
|
77
|
+
password = smtpConfig.password;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Créer le transporteur SMTP
|
|
81
|
+
const transporter = nodemailer.createTransport({
|
|
82
|
+
host: smtpConfig.host,
|
|
83
|
+
port: smtpConfig.port,
|
|
84
|
+
secure: smtpConfig.secure,
|
|
85
|
+
auth: {
|
|
86
|
+
user: smtpConfig.username,
|
|
87
|
+
pass: password,
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Construire les variantes texte / HTML avec la signature (si définie)
|
|
92
|
+
let baseHtml = content || '';
|
|
93
|
+
const baseText = htmlToText(baseHtml);
|
|
94
|
+
|
|
95
|
+
// Nettoyer les espaces en fin de contenu HTML
|
|
96
|
+
baseHtml = baseHtml.trim();
|
|
97
|
+
|
|
98
|
+
// Ajouter la signature de manière propre avec un espacement raisonnable
|
|
99
|
+
let signatureHtml = '';
|
|
100
|
+
let signatureText = '';
|
|
101
|
+
|
|
102
|
+
if (smtpConfig.signature) {
|
|
103
|
+
const signatureContent = smtpConfig.signature.trim();
|
|
104
|
+
// Ajouter un seul saut de ligne pour un espacement naturel
|
|
105
|
+
if (baseHtml.length > 0) {
|
|
106
|
+
signatureHtml = `<br>${signatureContent}`;
|
|
107
|
+
} else {
|
|
108
|
+
signatureHtml = signatureContent;
|
|
109
|
+
}
|
|
110
|
+
signatureText = `\n\n${htmlToText(signatureContent)}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Préparer les destinataires
|
|
114
|
+
const toEmails = to
|
|
115
|
+
.split(',')
|
|
116
|
+
.map((email) => email.trim())
|
|
117
|
+
.filter((email) => email);
|
|
118
|
+
const ccEmails = cc
|
|
119
|
+
? cc
|
|
120
|
+
.split(',')
|
|
121
|
+
.map((email) => email.trim())
|
|
122
|
+
.filter((email) => email)
|
|
123
|
+
: [];
|
|
124
|
+
const bccEmails = bcc
|
|
125
|
+
? bcc
|
|
126
|
+
.split(',')
|
|
127
|
+
.map((email) => email.trim())
|
|
128
|
+
.filter((email) => email)
|
|
129
|
+
: [];
|
|
130
|
+
|
|
131
|
+
// Récupérer les pièces jointes
|
|
132
|
+
const attachments: Array<{ filename: string; content: Buffer }> = [];
|
|
133
|
+
for (let i = 0; i < attachmentCount; i++) {
|
|
134
|
+
const file = formData.get(`attachment_${i}`) as File | null;
|
|
135
|
+
if (file) {
|
|
136
|
+
const buffer = Buffer.from(await file.arrayBuffer());
|
|
137
|
+
attachments.push({
|
|
138
|
+
filename: file.name,
|
|
139
|
+
content: buffer,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Créer d'abord l'interaction pour avoir son ID
|
|
145
|
+
const attachmentNames = attachments.map((att) => att.filename);
|
|
146
|
+
const metadata: any = {};
|
|
147
|
+
if (attachmentNames.length > 0) {
|
|
148
|
+
metadata.attachments = attachmentNames;
|
|
149
|
+
}
|
|
150
|
+
if (ccEmails.length > 0) {
|
|
151
|
+
metadata.cc = ccEmails;
|
|
152
|
+
}
|
|
153
|
+
if (bccEmails.length > 0) {
|
|
154
|
+
metadata.bcc = bccEmails;
|
|
155
|
+
}
|
|
156
|
+
metadata.to = toEmails;
|
|
157
|
+
metadata.htmlContent = `${baseHtml}${signatureHtml}`; // Stocker le HTML original (sans tracking) pour l'affichage
|
|
158
|
+
|
|
159
|
+
const interaction = await prisma.interaction.create({
|
|
160
|
+
data: {
|
|
161
|
+
contactId: id,
|
|
162
|
+
type: 'EMAIL',
|
|
163
|
+
title: subject,
|
|
164
|
+
content: baseText, // Texte brut pour la recherche/affichage simple
|
|
165
|
+
userId: session.user.id,
|
|
166
|
+
date: new Date(),
|
|
167
|
+
metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Créer le tracking pour cet email
|
|
172
|
+
const emailTracking = await prisma.emailTracking.create({
|
|
173
|
+
data: {
|
|
174
|
+
interactionId: interaction.id,
|
|
175
|
+
openCount: 0,
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Ajouter le pixel de tracking dans le HTML
|
|
180
|
+
// Utiliser l'URL absolue depuis les variables d'environnement
|
|
181
|
+
const baseUrl =
|
|
182
|
+
process.env.NEXT_PUBLIC_APP_URL || process.env.BETTER_AUTH_URL || 'http://localhost:3000';
|
|
183
|
+
// Ajouter un timestamp pour éviter le cache du client email
|
|
184
|
+
const timestamp = Date.now();
|
|
185
|
+
const trackingPixelUrl = `${baseUrl}/api/email/track/${emailTracking.id}?t=${timestamp}`;
|
|
186
|
+
|
|
187
|
+
const trackingPixel = `<img src="${trackingPixelUrl}" width="1" height="1" style="display:none;" alt="" />`;
|
|
188
|
+
|
|
189
|
+
// Insérer le pixel de tracking avant la fermeture du body ou à la fin du contenu
|
|
190
|
+
let htmlWithTracking = `${baseHtml}${signatureHtml}`;
|
|
191
|
+
// Si le HTML contient déjà une balise </body>, insérer avant, sinon à la fin
|
|
192
|
+
if (htmlWithTracking.includes('</body>')) {
|
|
193
|
+
htmlWithTracking = htmlWithTracking.replace('</body>', `${trackingPixel}</body>`);
|
|
194
|
+
} else {
|
|
195
|
+
htmlWithTracking = `${htmlWithTracking}${trackingPixel}`;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Envoyer l'email avec le pixel de tracking
|
|
199
|
+
const mailOptions: any = {
|
|
200
|
+
from: smtpConfig.fromName
|
|
201
|
+
? `"${smtpConfig.fromName}" <${smtpConfig.fromEmail}>`
|
|
202
|
+
: smtpConfig.fromEmail,
|
|
203
|
+
to: toEmails.join(', '),
|
|
204
|
+
subject: subject,
|
|
205
|
+
text: `${baseText}${signatureText}`,
|
|
206
|
+
html: htmlWithTracking,
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
if (ccEmails.length > 0) {
|
|
210
|
+
mailOptions.cc = ccEmails.join(', ');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (bccEmails.length > 0) {
|
|
214
|
+
mailOptions.bcc = bccEmails.join(', ');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (attachments.length > 0) {
|
|
218
|
+
mailOptions.attachments = attachments;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
await transporter.sendMail(mailOptions);
|
|
222
|
+
|
|
223
|
+
// Récupérer l'interaction avec les relations pour la réponse
|
|
224
|
+
const interactionWithUser = await prisma.interaction.findUnique({
|
|
225
|
+
where: { id: interaction.id },
|
|
226
|
+
include: {
|
|
227
|
+
user: {
|
|
228
|
+
select: { id: true, name: true, email: true },
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
return NextResponse.json({
|
|
234
|
+
success: true,
|
|
235
|
+
message: 'Email envoyé avec succès',
|
|
236
|
+
interaction: interactionWithUser,
|
|
237
|
+
});
|
|
238
|
+
} catch (error: any) {
|
|
239
|
+
console.error("Erreur lors de l'envoi de l'email:", error);
|
|
240
|
+
|
|
241
|
+
// Gérer les erreurs spécifiques de nodemailer
|
|
242
|
+
if (error.code === 'EAUTH' || error.code === 'ECONNECTION') {
|
|
243
|
+
return NextResponse.json(
|
|
244
|
+
{ error: "Erreur d'authentification SMTP. Vérifiez votre configuration." },
|
|
245
|
+
{ status: 400 },
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return NextResponse.json(
|
|
250
|
+
{ error: error.message || "Erreur lors de l'envoi de l'email" },
|
|
251
|
+
{ status: 500 },
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { auth } from '@/lib/auth';
|
|
3
|
+
import { prisma } from '@/lib/prisma';
|
|
4
|
+
import { getFileInfo } from '@/lib/google-drive';
|
|
5
|
+
|
|
6
|
+
// POST /api/contacts/export - Exporter des contacts en CSV ou Excel
|
|
7
|
+
export async function POST(request: NextRequest) {
|
|
8
|
+
try {
|
|
9
|
+
const session = await auth.api.getSession({
|
|
10
|
+
headers: request.headers,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
if (!session) {
|
|
14
|
+
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Vérifier que l'utilisateur est admin
|
|
18
|
+
const user = await prisma.user.findUnique({
|
|
19
|
+
where: { id: session.user.id },
|
|
20
|
+
select: { role: true },
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
if (user?.role !== 'ADMIN') {
|
|
24
|
+
return NextResponse.json(
|
|
25
|
+
{ error: 'Accès refusé. Seuls les administrateurs peuvent exporter des contacts.' },
|
|
26
|
+
{ status: 403 },
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const body = await request.json();
|
|
31
|
+
const { contactIds, format } = body; // format: 'csv' ou 'excel'
|
|
32
|
+
|
|
33
|
+
if (!format || (format !== 'csv' && format !== 'excel')) {
|
|
34
|
+
return NextResponse.json({ error: 'Format invalide. Utilisez "csv" ou "excel".' }, { status: 400 });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Construire la requête pour récupérer les contacts
|
|
38
|
+
const where: any = { isCompany: false };
|
|
39
|
+
|
|
40
|
+
// Si des IDs sont fournis, exporter seulement ces contacts
|
|
41
|
+
if (contactIds && Array.isArray(contactIds) && contactIds.length > 0) {
|
|
42
|
+
where.id = { in: contactIds };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Récupérer les contacts avec toutes les relations nécessaires (notes et fichiers inclus)
|
|
46
|
+
const contacts = await prisma.contact.findMany({
|
|
47
|
+
where,
|
|
48
|
+
include: {
|
|
49
|
+
status: {
|
|
50
|
+
select: { name: true },
|
|
51
|
+
},
|
|
52
|
+
assignedCommercial: {
|
|
53
|
+
select: { name: true, email: true },
|
|
54
|
+
},
|
|
55
|
+
assignedTelepro: {
|
|
56
|
+
select: { name: true, email: true },
|
|
57
|
+
},
|
|
58
|
+
createdBy: {
|
|
59
|
+
select: { name: true, email: true },
|
|
60
|
+
},
|
|
61
|
+
interactions: {
|
|
62
|
+
select: {
|
|
63
|
+
type: true,
|
|
64
|
+
title: true,
|
|
65
|
+
content: true,
|
|
66
|
+
createdAt: true,
|
|
67
|
+
user: {
|
|
68
|
+
select: { name: true },
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
orderBy: { createdAt: 'desc' },
|
|
72
|
+
},
|
|
73
|
+
files: {
|
|
74
|
+
select: {
|
|
75
|
+
fileName: true,
|
|
76
|
+
googleDriveFileId: true,
|
|
77
|
+
fileSize: true,
|
|
78
|
+
mimeType: true,
|
|
79
|
+
createdAt: true,
|
|
80
|
+
},
|
|
81
|
+
orderBy: { createdAt: 'desc' },
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
orderBy: { createdAt: 'desc' },
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
if (contacts.length === 0) {
|
|
88
|
+
return NextResponse.json({ error: 'Aucun contact à exporter' }, { status: 400 });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Préparer les données pour l'export avec notes et fichiers
|
|
92
|
+
const headers = [
|
|
93
|
+
'Civilité',
|
|
94
|
+
'Prénom',
|
|
95
|
+
'Nom',
|
|
96
|
+
'Téléphone',
|
|
97
|
+
'Téléphone secondaire',
|
|
98
|
+
'Email',
|
|
99
|
+
'Adresse',
|
|
100
|
+
'Ville',
|
|
101
|
+
'Code postal',
|
|
102
|
+
'Origine',
|
|
103
|
+
'Statut',
|
|
104
|
+
'Commercial',
|
|
105
|
+
'Email Commercial',
|
|
106
|
+
'Télépro',
|
|
107
|
+
'Email Télépro',
|
|
108
|
+
'Créé le',
|
|
109
|
+
'Modifié le',
|
|
110
|
+
'Notes',
|
|
111
|
+
'Fichiers',
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
// Fonction pour formater les notes
|
|
115
|
+
const formatNotes = (interactions: any[]) => {
|
|
116
|
+
if (!interactions || interactions.length === 0) return '';
|
|
117
|
+
|
|
118
|
+
return interactions
|
|
119
|
+
.map((interaction) => {
|
|
120
|
+
const date = interaction.createdAt
|
|
121
|
+
? new Date(interaction.createdAt).toLocaleString('fr-FR', {
|
|
122
|
+
day: '2-digit',
|
|
123
|
+
month: '2-digit',
|
|
124
|
+
year: 'numeric',
|
|
125
|
+
hour: '2-digit',
|
|
126
|
+
minute: '2-digit',
|
|
127
|
+
})
|
|
128
|
+
: '';
|
|
129
|
+
const author = interaction.user?.name || 'Inconnu';
|
|
130
|
+
const title = interaction.title ? `${interaction.title}: ` : '';
|
|
131
|
+
const content = interaction.content || '';
|
|
132
|
+
|
|
133
|
+
return `[${date}] ${author} - ${title}${content}`;
|
|
134
|
+
})
|
|
135
|
+
.join('\n\n');
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// Fonction pour formater les fichiers (essayer de récupérer les liens si possible)
|
|
139
|
+
const formatFiles = async (files: any[], userId: string) => {
|
|
140
|
+
if (!files || files.length === 0) return '';
|
|
141
|
+
|
|
142
|
+
const fileInfos = await Promise.allSettled(
|
|
143
|
+
files.map(async (file) => {
|
|
144
|
+
try {
|
|
145
|
+
// Essayer de récupérer le lien depuis Google Drive
|
|
146
|
+
const fileInfo = await getFileInfo(userId, file.googleDriveFileId);
|
|
147
|
+
const sizeKB = (file.fileSize / 1024).toFixed(2);
|
|
148
|
+
return `${file.fileName} (${sizeKB} KB) - ${fileInfo.webViewLink}`;
|
|
149
|
+
} catch (error) {
|
|
150
|
+
// Si échec, utiliser un lien basique
|
|
151
|
+
const sizeKB = (file.fileSize / 1024).toFixed(2);
|
|
152
|
+
return `${file.fileName} (${sizeKB} KB) - https://drive.google.com/file/d/${file.googleDriveFileId}/view`;
|
|
153
|
+
}
|
|
154
|
+
}),
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
return fileInfos
|
|
158
|
+
.map((result) => (result.status === 'fulfilled' ? result.value : result.reason?.message || 'Erreur'))
|
|
159
|
+
.join('\n');
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// Préparer les lignes avec notes et fichiers
|
|
163
|
+
const rows = await Promise.all(
|
|
164
|
+
contacts.map(async (contact) => {
|
|
165
|
+
const notes = formatNotes(contact.interactions || []);
|
|
166
|
+
const files = await formatFiles(contact.files || [], session.user.id);
|
|
167
|
+
|
|
168
|
+
return [
|
|
169
|
+
contact.civility || '',
|
|
170
|
+
contact.firstName || '',
|
|
171
|
+
contact.lastName || '',
|
|
172
|
+
contact.phone || '',
|
|
173
|
+
contact.secondaryPhone || '',
|
|
174
|
+
contact.email || '',
|
|
175
|
+
contact.address || '',
|
|
176
|
+
contact.city || '',
|
|
177
|
+
contact.postalCode || '',
|
|
178
|
+
contact.origin || '',
|
|
179
|
+
contact.status?.name || '',
|
|
180
|
+
contact.assignedCommercial?.name || '',
|
|
181
|
+
contact.assignedCommercial?.email || '',
|
|
182
|
+
contact.assignedTelepro?.name || '',
|
|
183
|
+
contact.assignedTelepro?.email || '',
|
|
184
|
+
contact.createdAt ? new Date(contact.createdAt).toLocaleString('fr-FR') : '',
|
|
185
|
+
contact.updatedAt ? new Date(contact.updatedAt).toLocaleString('fr-FR') : '',
|
|
186
|
+
notes,
|
|
187
|
+
files,
|
|
188
|
+
];
|
|
189
|
+
}),
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
if (format === 'csv') {
|
|
193
|
+
// Générer CSV
|
|
194
|
+
const csvContent = [
|
|
195
|
+
headers.join(','),
|
|
196
|
+
...rows.map((row) =>
|
|
197
|
+
row
|
|
198
|
+
.map((cell) => {
|
|
199
|
+
// Échapper les guillemets et les virgules
|
|
200
|
+
const cellStr = String(cell || '').replace(/"/g, '""');
|
|
201
|
+
return `"${cellStr}"`;
|
|
202
|
+
})
|
|
203
|
+
.join(','),
|
|
204
|
+
),
|
|
205
|
+
].join('\n');
|
|
206
|
+
|
|
207
|
+
// Ajouter BOM pour Excel UTF-8
|
|
208
|
+
const csvWithBOM = '\uFEFF' + csvContent;
|
|
209
|
+
|
|
210
|
+
return new NextResponse(csvWithBOM, {
|
|
211
|
+
headers: {
|
|
212
|
+
'Content-Type': 'text/csv; charset=utf-8',
|
|
213
|
+
'Content-Disposition': `attachment; filename="contacts_${new Date().toISOString().split('T')[0]}.csv"`,
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
} else if (format === 'excel') {
|
|
217
|
+
// Pour Excel, nous allons utiliser une bibliothèque
|
|
218
|
+
// Vérifions d'abord si xlsx est disponible, sinon on génère un CSV
|
|
219
|
+
try {
|
|
220
|
+
const XLSX = await import('xlsx');
|
|
221
|
+
const worksheet = XLSX.utils.aoa_to_sheet([headers, ...rows]);
|
|
222
|
+
const workbook = XLSX.utils.book_new();
|
|
223
|
+
XLSX.utils.book_append_sheet(workbook, worksheet, 'Contacts');
|
|
224
|
+
|
|
225
|
+
// Générer le buffer Excel
|
|
226
|
+
const excelBuffer = XLSX.write(workbook, {
|
|
227
|
+
type: 'buffer',
|
|
228
|
+
bookType: 'xlsx',
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
return new NextResponse(excelBuffer, {
|
|
232
|
+
headers: {
|
|
233
|
+
'Content-Type':
|
|
234
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
235
|
+
'Content-Disposition': `attachment; filename="contacts_${new Date().toISOString().split('T')[0]}.xlsx"`,
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
} catch (error) {
|
|
239
|
+
// Si xlsx n'est pas disponible, générer un CSV à la place
|
|
240
|
+
console.warn('Bibliothèque xlsx non disponible, génération CSV à la place');
|
|
241
|
+
const csvContent = [
|
|
242
|
+
headers.join(','),
|
|
243
|
+
...rows.map((row) =>
|
|
244
|
+
row
|
|
245
|
+
.map((cell) => {
|
|
246
|
+
const cellStr = String(cell || '').replace(/"/g, '""');
|
|
247
|
+
return `"${cellStr}"`;
|
|
248
|
+
})
|
|
249
|
+
.join(','),
|
|
250
|
+
),
|
|
251
|
+
].join('\n');
|
|
252
|
+
|
|
253
|
+
const csvWithBOM = '\uFEFF' + csvContent;
|
|
254
|
+
|
|
255
|
+
return new NextResponse(csvWithBOM, {
|
|
256
|
+
headers: {
|
|
257
|
+
'Content-Type': 'text/csv; charset=utf-8',
|
|
258
|
+
'Content-Disposition': `attachment; filename="contacts_${new Date().toISOString().split('T')[0]}.csv"`,
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
} else {
|
|
263
|
+
return NextResponse.json({ error: 'Format non supporté' }, { status: 400 });
|
|
264
|
+
}
|
|
265
|
+
} catch (error: any) {
|
|
266
|
+
console.error('Erreur lors de l\'export des contacts:', error);
|
|
267
|
+
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|