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,250 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { prisma } from '@/lib/prisma';
|
|
3
|
+
import { checkPermission } from '@/lib/check-permission';
|
|
4
|
+
import { auth } from '@/lib/auth';
|
|
5
|
+
import { logAudit } from '@/lib/audit-log';
|
|
6
|
+
|
|
7
|
+
// GET /api/users - Liste tous les utilisateurs (admin seulement)
|
|
8
|
+
export async function GET(request: NextRequest) {
|
|
9
|
+
try {
|
|
10
|
+
const session = await auth.api.getSession({
|
|
11
|
+
headers: request.headers,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
if (!session) {
|
|
15
|
+
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Vérifier que l'utilisateur a la permission de voir les utilisateurs
|
|
19
|
+
const hasPermission = await checkPermission('users.view');
|
|
20
|
+
if (!hasPermission) {
|
|
21
|
+
return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const users = await prisma.user.findMany({
|
|
25
|
+
include: {
|
|
26
|
+
customRole: {
|
|
27
|
+
select: {
|
|
28
|
+
id: true,
|
|
29
|
+
name: true,
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
orderBy: {
|
|
34
|
+
createdAt: 'desc',
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Mapper les utilisateurs avec le profil
|
|
39
|
+
const usersWithRole = users.map((user: any) => ({
|
|
40
|
+
id: user.id,
|
|
41
|
+
name: user.name,
|
|
42
|
+
email: user.email,
|
|
43
|
+
role: user.role || 'USER',
|
|
44
|
+
customRoleId: user.customRoleId,
|
|
45
|
+
customRole: user.customRole,
|
|
46
|
+
emailVerified: user.emailVerified,
|
|
47
|
+
active: user.active,
|
|
48
|
+
createdAt: user.createdAt,
|
|
49
|
+
updatedAt: user.updatedAt,
|
|
50
|
+
image: user.image,
|
|
51
|
+
}));
|
|
52
|
+
|
|
53
|
+
return NextResponse.json(usersWithRole);
|
|
54
|
+
} catch (error: any) {
|
|
55
|
+
console.error('Erreur lors de la récupération des utilisateurs:', error);
|
|
56
|
+
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// POST /api/users - Créer un nouvel utilisateur (admin seulement)
|
|
61
|
+
export async function POST(request: NextRequest) {
|
|
62
|
+
try {
|
|
63
|
+
const session = await auth.api.getSession({
|
|
64
|
+
headers: request.headers,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
if (!session) {
|
|
68
|
+
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Vérifier que l'utilisateur a la permission de créer des utilisateurs
|
|
72
|
+
const hasPermission = await checkPermission('users.create');
|
|
73
|
+
if (!hasPermission) {
|
|
74
|
+
return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const body = await request.json();
|
|
78
|
+
const { name, email, customRoleId } = body;
|
|
79
|
+
|
|
80
|
+
// Validation
|
|
81
|
+
if (!name || !email) {
|
|
82
|
+
return NextResponse.json({ error: 'Nom et email requis' }, { status: 400 });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!customRoleId) {
|
|
86
|
+
return NextResponse.json({ error: 'Le profil est requis' }, { status: 400 });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Vérifier si l'email existe déjà
|
|
90
|
+
const existingUser = await prisma.user.findUnique({
|
|
91
|
+
where: { email },
|
|
92
|
+
include: {
|
|
93
|
+
accounts: {
|
|
94
|
+
where: { providerId: 'credential' },
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
if (existingUser) {
|
|
100
|
+
// Si l'utilisateur existe déjà avec un compte, erreur
|
|
101
|
+
if (existingUser.accounts.length > 0) {
|
|
102
|
+
return NextResponse.json({ error: 'Cet email est déjà utilisé' }, { status: 400 });
|
|
103
|
+
}
|
|
104
|
+
// Si l'utilisateur existe mais sans compte, on peut régénérer un token
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let user;
|
|
108
|
+
if (existingUser && existingUser.accounts.length === 0) {
|
|
109
|
+
// Utilisateur existe déjà sans compte, on met à jour et régénère le token
|
|
110
|
+
user = await prisma.user.update({
|
|
111
|
+
where: { id: existingUser.id },
|
|
112
|
+
data: {
|
|
113
|
+
name,
|
|
114
|
+
customRoleId,
|
|
115
|
+
active: true,
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
} else {
|
|
119
|
+
// Créer l'utilisateur SANS mot de passe (sans Account)
|
|
120
|
+
user = await prisma.user.create({
|
|
121
|
+
data: {
|
|
122
|
+
id: crypto.randomUUID(),
|
|
123
|
+
name,
|
|
124
|
+
email,
|
|
125
|
+
role: 'USER', // Rôle par défaut, mais les permissions viendront du customRole
|
|
126
|
+
customRoleId,
|
|
127
|
+
emailVerified: false, // Pas encore vérifié
|
|
128
|
+
active: true,
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Récupérer le nom du profil pour les métadonnées
|
|
134
|
+
let customRoleName: string | null = null;
|
|
135
|
+
if (customRoleId) {
|
|
136
|
+
const customRole = await prisma.customRole.findUnique({
|
|
137
|
+
where: { id: customRoleId },
|
|
138
|
+
select: { name: true },
|
|
139
|
+
});
|
|
140
|
+
customRoleName = customRole?.name || null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Log d'audit : création ou réactivation d'utilisateur
|
|
144
|
+
await logAudit({
|
|
145
|
+
actorId: session.user.id,
|
|
146
|
+
targetUserId: user.id,
|
|
147
|
+
action: 'USER_CREATED',
|
|
148
|
+
entityType: 'USER',
|
|
149
|
+
entityId: user.id,
|
|
150
|
+
metadata: {
|
|
151
|
+
name: user.name,
|
|
152
|
+
email: user.email,
|
|
153
|
+
customRoleId,
|
|
154
|
+
customRoleName,
|
|
155
|
+
wasExisting: Boolean(existingUser),
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// Générer un token d'invitation
|
|
160
|
+
const token = crypto.randomUUID();
|
|
161
|
+
const expiresAt = new Date();
|
|
162
|
+
expiresAt.setDate(expiresAt.getDate() + 1); // Valide 1 jour
|
|
163
|
+
|
|
164
|
+
// Supprimer les anciens tokens pour cet email
|
|
165
|
+
await prisma.verification.deleteMany({
|
|
166
|
+
where: {
|
|
167
|
+
identifier: email,
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Créer le nouveau token
|
|
172
|
+
await prisma.verification.create({
|
|
173
|
+
data: {
|
|
174
|
+
id: crypto.randomUUID(),
|
|
175
|
+
identifier: email,
|
|
176
|
+
value: token,
|
|
177
|
+
expiresAt,
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// Envoyer l'email d'invitation
|
|
182
|
+
const baseUrl = process.env.BETTER_AUTH_URL || 'http://localhost:3000';
|
|
183
|
+
const invitationUrl = `${baseUrl}/invite/${token}`;
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
// Transmettre les cookies de session pour que /api/send puisse identifier l'utilisateur connecté
|
|
187
|
+
const cookieHeader = request.headers.get('cookie') || '';
|
|
188
|
+
const emailResponse = await fetch(`${baseUrl}/api/send`, {
|
|
189
|
+
method: 'POST',
|
|
190
|
+
headers: {
|
|
191
|
+
'Content-Type': 'application/json',
|
|
192
|
+
Cookie: cookieHeader,
|
|
193
|
+
},
|
|
194
|
+
body: JSON.stringify({
|
|
195
|
+
to: email,
|
|
196
|
+
subject: 'Invitation à rejoindre le CRM',
|
|
197
|
+
template: 'invitation',
|
|
198
|
+
invitationUrl,
|
|
199
|
+
name,
|
|
200
|
+
}),
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
if (!emailResponse.ok) {
|
|
204
|
+
const errorData = await emailResponse.json().catch(() => ({}));
|
|
205
|
+
console.error("❌ Erreur lors de l'envoi de l'email:", {
|
|
206
|
+
status: emailResponse.status,
|
|
207
|
+
statusText: emailResponse.statusText,
|
|
208
|
+
error: errorData,
|
|
209
|
+
});
|
|
210
|
+
} else {
|
|
211
|
+
const successData = await emailResponse.json().catch(() => ({}));
|
|
212
|
+
console.log("✅ Email d'invitation envoyé avec succès:", successData);
|
|
213
|
+
}
|
|
214
|
+
} catch (emailError: any) {
|
|
215
|
+
console.error("❌ Erreur lors de l'envoi de l'email:", emailError);
|
|
216
|
+
console.error('Détails:', {
|
|
217
|
+
message: emailError.message,
|
|
218
|
+
stack: emailError.stack,
|
|
219
|
+
});
|
|
220
|
+
// On continue même si l'email échoue, l'utilisateur est créé
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return NextResponse.json(
|
|
224
|
+
{
|
|
225
|
+
id: user.id,
|
|
226
|
+
name: user.name,
|
|
227
|
+
email: user.email,
|
|
228
|
+
role: user.role || 'USER',
|
|
229
|
+
emailVerified: user.emailVerified,
|
|
230
|
+
active: user.active,
|
|
231
|
+
createdAt: user.createdAt,
|
|
232
|
+
message: "Utilisateur créé, email d'invitation envoyé",
|
|
233
|
+
},
|
|
234
|
+
{ status: 201 },
|
|
235
|
+
);
|
|
236
|
+
} catch (error: any) {
|
|
237
|
+
console.error("Erreur lors de la création de l'utilisateur:", error);
|
|
238
|
+
|
|
239
|
+
// Gérer les erreurs spécifiques
|
|
240
|
+
if (
|
|
241
|
+
error.message?.includes('email') ||
|
|
242
|
+
error.message?.includes('Email') ||
|
|
243
|
+
error.message?.includes('already exists')
|
|
244
|
+
) {
|
|
245
|
+
return NextResponse.json({ error: 'Cet email est déjà utilisé' }, { status: 400 });
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return NextResponse.json({ error: error.message || 'Erreur serveur' }, { status: 500 });
|
|
249
|
+
}
|
|
250
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { prisma } from '@/lib/prisma';
|
|
3
|
+
import { handleContactDuplicate } from '@/lib/contact-duplicate';
|
|
4
|
+
import { normalizePhoneNumber } from '@/lib/utils';
|
|
5
|
+
|
|
6
|
+
interface GoogleAdsUserColumnData {
|
|
7
|
+
columnName: string;
|
|
8
|
+
stringValue?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface GoogleAdsLeadNotification {
|
|
12
|
+
googleKey: string;
|
|
13
|
+
customerId?: string;
|
|
14
|
+
leadResourceName?: string;
|
|
15
|
+
userColumnData?: GoogleAdsUserColumnData[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// POST /api/webhooks/google-ads - Réception des leads Google Ads (lead form extensions)
|
|
19
|
+
export async function POST(request: NextRequest) {
|
|
20
|
+
try {
|
|
21
|
+
const body = await request.json();
|
|
22
|
+
|
|
23
|
+
const notification: GoogleAdsLeadNotification | undefined = body?.leadNotification;
|
|
24
|
+
|
|
25
|
+
if (!notification || !notification.googleKey) {
|
|
26
|
+
console.warn('Webhook Google Ads reçu avec un format inattendu:', body);
|
|
27
|
+
return NextResponse.json({ received: true });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const client = prisma as any;
|
|
31
|
+
|
|
32
|
+
// Récupérer toutes les configurations actives
|
|
33
|
+
const configs = await client.googleAdsLeadConfig.findMany({
|
|
34
|
+
where: { active: true },
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
if (!configs || configs.length === 0) {
|
|
38
|
+
console.warn(
|
|
39
|
+
'Webhook Google Ads reçu mais aucune configuration active GoogleAdsLeadConfig trouvée.',
|
|
40
|
+
);
|
|
41
|
+
return NextResponse.json({ received: true });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Trouver la configuration correspondante à la clé
|
|
45
|
+
const config = configs.find((c: any) => c.webhookKey === notification.googleKey);
|
|
46
|
+
|
|
47
|
+
if (!config) {
|
|
48
|
+
console.warn('Clé Google Ads invalide reçue sur le webhook.');
|
|
49
|
+
return NextResponse.json({ error: 'Clé invalide' }, { status: 403 });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const userColumns = notification.userColumnData || [];
|
|
53
|
+
|
|
54
|
+
const getField = (name: string): string | undefined => {
|
|
55
|
+
const field = userColumns.find((f) => f.columnName === name);
|
|
56
|
+
return field?.stringValue;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const fullName = getField('FULL_NAME') || getField('NAME');
|
|
60
|
+
let firstName = getField('FIRST_NAME');
|
|
61
|
+
let lastName = getField('LAST_NAME');
|
|
62
|
+
const email = getField('EMAIL');
|
|
63
|
+
const phone = getField('PHONE_NUMBER') || getField('PHONE');
|
|
64
|
+
|
|
65
|
+
if ((!firstName || !lastName) && fullName) {
|
|
66
|
+
const parts = fullName.split(' ');
|
|
67
|
+
firstName = firstName || parts.slice(0, -1).join(' ') || parts[0];
|
|
68
|
+
lastName = lastName || parts.slice(-1).join(' ');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!phone && !email) {
|
|
72
|
+
console.warn(
|
|
73
|
+
'Lead Google Ads reçu sans téléphone ni email, impossible de créer un contact. Notification:',
|
|
74
|
+
notification,
|
|
75
|
+
);
|
|
76
|
+
return NextResponse.json({ received: true });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Normaliser le numéro de téléphone si présent
|
|
80
|
+
const normalizedPhone = phone ? normalizePhoneNumber(phone) : '';
|
|
81
|
+
|
|
82
|
+
// Déterminer l'utilisateur pour createdById (nécessaire pour créer le contact)
|
|
83
|
+
let createdById = config.defaultAssignedUserId || null;
|
|
84
|
+
if (!createdById) {
|
|
85
|
+
const adminUser = await client.user.findFirst({
|
|
86
|
+
where: { role: 'ADMIN' },
|
|
87
|
+
orderBy: { createdAt: 'asc' },
|
|
88
|
+
});
|
|
89
|
+
if (adminUser) {
|
|
90
|
+
createdById = adminUser.id;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!createdById) {
|
|
95
|
+
console.warn(
|
|
96
|
+
'Lead Google Ads reçu mais aucun utilisateur pour créer le contact trouvé. Notification:',
|
|
97
|
+
notification,
|
|
98
|
+
);
|
|
99
|
+
return NextResponse.json({ received: true });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Déterminer l'assignation selon le rôle de l'utilisateur par défaut
|
|
103
|
+
let assignedCommercialId: string | null = null;
|
|
104
|
+
let assignedTeleproId: string | null = null;
|
|
105
|
+
|
|
106
|
+
if (config.defaultAssignedUserId) {
|
|
107
|
+
const defaultUser = await client.user.findUnique({
|
|
108
|
+
where: { id: config.defaultAssignedUserId },
|
|
109
|
+
select: { role: true },
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
if (defaultUser) {
|
|
113
|
+
if (
|
|
114
|
+
defaultUser.role === 'COMMERCIAL' ||
|
|
115
|
+
defaultUser.role === 'ADMIN' ||
|
|
116
|
+
defaultUser.role === 'MANAGER'
|
|
117
|
+
) {
|
|
118
|
+
assignedCommercialId = config.defaultAssignedUserId;
|
|
119
|
+
} else if (defaultUser.role === 'TELEPRO') {
|
|
120
|
+
assignedTeleproId = config.defaultAssignedUserId;
|
|
121
|
+
}
|
|
122
|
+
// Sinon, on ne assigne pas (null pour les deux)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Vérifier si c'est un doublon (nom, prénom ET email)
|
|
127
|
+
const duplicateContactId = await handleContactDuplicate(
|
|
128
|
+
firstName,
|
|
129
|
+
lastName,
|
|
130
|
+
email,
|
|
131
|
+
`Google Ads - ${config.name}`,
|
|
132
|
+
createdById,
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
let contact;
|
|
136
|
+
if (duplicateContactId) {
|
|
137
|
+
// C'est un doublon, récupérer le contact existant
|
|
138
|
+
contact = await client.contact.findUnique({
|
|
139
|
+
where: { id: duplicateContactId },
|
|
140
|
+
});
|
|
141
|
+
} else {
|
|
142
|
+
// Vérifier si un contact existe déjà (par téléphone uniquement)
|
|
143
|
+
contact =
|
|
144
|
+
(email &&
|
|
145
|
+
(await client.contact.findFirst({
|
|
146
|
+
where: {
|
|
147
|
+
OR: [
|
|
148
|
+
{ email: email.toLowerCase() },
|
|
149
|
+
normalizedPhone ? { phone: normalizedPhone } : undefined,
|
|
150
|
+
].filter(Boolean) as any,
|
|
151
|
+
},
|
|
152
|
+
}))) ||
|
|
153
|
+
(normalizedPhone &&
|
|
154
|
+
(await client.contact.findFirst({
|
|
155
|
+
where: { phone: normalizedPhone },
|
|
156
|
+
})));
|
|
157
|
+
|
|
158
|
+
if (!contact) {
|
|
159
|
+
contact = await client.contact.create({
|
|
160
|
+
data: {
|
|
161
|
+
firstName: firstName || null,
|
|
162
|
+
lastName: lastName || null,
|
|
163
|
+
email: email ? email.toLowerCase() : null,
|
|
164
|
+
phone: normalizedPhone,
|
|
165
|
+
origin: `Google Ads - ${config.name}`,
|
|
166
|
+
statusId: config.defaultStatusId || null,
|
|
167
|
+
assignedCommercialId: assignedCommercialId,
|
|
168
|
+
assignedTeleproId: assignedTeleproId,
|
|
169
|
+
createdById: createdById,
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
} else {
|
|
173
|
+
await client.contact.update({
|
|
174
|
+
where: { id: contact.id },
|
|
175
|
+
data: {
|
|
176
|
+
firstName: contact.firstName || firstName || null,
|
|
177
|
+
lastName: contact.lastName || lastName || null,
|
|
178
|
+
email: contact.email || (email ? email.toLowerCase() : null),
|
|
179
|
+
origin: contact.origin || `Google Ads - ${config.name}`,
|
|
180
|
+
statusId: contact.statusId || config.defaultStatusId || null,
|
|
181
|
+
// Ne pas écraser les assignations existantes
|
|
182
|
+
assignedCommercialId: contact.assignedCommercialId || assignedCommercialId,
|
|
183
|
+
assignedTeleproId: contact.assignedTeleproId || assignedTeleproId,
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Créer une interaction "Lead Google Ads"
|
|
190
|
+
await client.interaction.create({
|
|
191
|
+
data: {
|
|
192
|
+
contactId: contact.id,
|
|
193
|
+
type: 'NOTE',
|
|
194
|
+
title: `Lead Google Ads - ${config.name}`,
|
|
195
|
+
content: `Lead importé automatiquement depuis Google Ads (${config.name}, client: ${
|
|
196
|
+
notification.customerId || 'inconnu'
|
|
197
|
+
}).`,
|
|
198
|
+
userId: createdById,
|
|
199
|
+
date: new Date(),
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
return NextResponse.json({ received: true });
|
|
204
|
+
} catch (error: any) {
|
|
205
|
+
console.error('Erreur lors du traitement du webhook Google Ads Lead Forms:', error);
|
|
206
|
+
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
|
|
207
|
+
}
|
|
208
|
+
}
|