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,526 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { prisma } from '@/lib/prisma';
|
|
3
|
+
import { getValidAccessToken, GoogleTokenError } from '@/lib/google-calendar';
|
|
4
|
+
import { handleContactDuplicate } from '@/lib/contact-duplicate';
|
|
5
|
+
|
|
6
|
+
// POST /api/integrations/google-sheet/sync - Synchroniser toutes les configurations actives
|
|
7
|
+
export async function POST(request: NextRequest) {
|
|
8
|
+
try {
|
|
9
|
+
const client = prisma as any;
|
|
10
|
+
|
|
11
|
+
// Récupérer toutes les configurations actives
|
|
12
|
+
const configs = await client.googleSheetSyncConfig.findMany({
|
|
13
|
+
where: { active: true },
|
|
14
|
+
include: {
|
|
15
|
+
ownerUser: true,
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
if (!configs || configs.length === 0) {
|
|
20
|
+
return NextResponse.json({
|
|
21
|
+
totalImported: 0,
|
|
22
|
+
totalUpdated: 0,
|
|
23
|
+
totalSkipped: 0,
|
|
24
|
+
results: [],
|
|
25
|
+
message: "Aucune configuration Google Sheets active n'a été trouvée.",
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const results: Array<{
|
|
30
|
+
configId: string;
|
|
31
|
+
configName: string;
|
|
32
|
+
imported: number;
|
|
33
|
+
updated: number;
|
|
34
|
+
skipped: number;
|
|
35
|
+
error?: string;
|
|
36
|
+
}> = [];
|
|
37
|
+
|
|
38
|
+
let totalImported = 0;
|
|
39
|
+
let totalUpdated = 0;
|
|
40
|
+
let totalSkipped = 0;
|
|
41
|
+
|
|
42
|
+
// Synchroniser chaque configuration
|
|
43
|
+
for (const config of configs) {
|
|
44
|
+
try {
|
|
45
|
+
// Récupérer le compte Google de l'utilisateur propriétaire
|
|
46
|
+
const googleAccount = await client.userGoogleAccount.findUnique({
|
|
47
|
+
where: { userId: config.ownerUserId },
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
if (!googleAccount) {
|
|
51
|
+
results.push({
|
|
52
|
+
configId: config.id,
|
|
53
|
+
configName: config.name,
|
|
54
|
+
imported: 0,
|
|
55
|
+
updated: 0,
|
|
56
|
+
skipped: 0,
|
|
57
|
+
error:
|
|
58
|
+
'Aucun compte Google connecté pour l’utilisateur propriétaire. Veuillez connecter votre compte Google dans les paramètres.',
|
|
59
|
+
});
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let accessToken: string;
|
|
64
|
+
try {
|
|
65
|
+
accessToken = await getValidAccessToken(
|
|
66
|
+
googleAccount.accessToken,
|
|
67
|
+
googleAccount.refreshToken,
|
|
68
|
+
googleAccount.tokenExpiresAt,
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
// Mettre à jour le token si nécessaire
|
|
72
|
+
if (accessToken !== googleAccount.accessToken) {
|
|
73
|
+
const tokenExpiresAt = new Date();
|
|
74
|
+
tokenExpiresAt.setSeconds(tokenExpiresAt.getSeconds() + 3600);
|
|
75
|
+
await client.userGoogleAccount.update({
|
|
76
|
+
where: { userId: config.ownerUserId },
|
|
77
|
+
data: {
|
|
78
|
+
accessToken,
|
|
79
|
+
tokenExpiresAt,
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
} catch (error: any) {
|
|
84
|
+
// Gérer spécifiquement les erreurs de token expiré/révoqué
|
|
85
|
+
if (error instanceof GoogleTokenError && error.isRevoked) {
|
|
86
|
+
results.push({
|
|
87
|
+
configId: config.id,
|
|
88
|
+
configName: config.name,
|
|
89
|
+
imported: 0,
|
|
90
|
+
updated: 0,
|
|
91
|
+
skipped: 0,
|
|
92
|
+
error: error.message,
|
|
93
|
+
});
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
// Relancer l'erreur si ce n'est pas une erreur de token
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const range = encodeURIComponent(config.sheetName);
|
|
101
|
+
const response = await fetch(
|
|
102
|
+
`https://sheets.googleapis.com/v4/spreadsheets/${config.spreadsheetId}/values/${range}`,
|
|
103
|
+
{
|
|
104
|
+
headers: {
|
|
105
|
+
Authorization: `Bearer ${accessToken}`,
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
if (!response.ok) {
|
|
111
|
+
const errorText = await response.text();
|
|
112
|
+
console.error(`Erreur lors de la lecture du Google Sheet ${config.name}:`, errorText);
|
|
113
|
+
results.push({
|
|
114
|
+
configId: config.id,
|
|
115
|
+
configName: config.name,
|
|
116
|
+
imported: 0,
|
|
117
|
+
updated: 0,
|
|
118
|
+
skipped: 0,
|
|
119
|
+
error: 'Impossible de lire les données depuis Google Sheets.',
|
|
120
|
+
});
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const data = await response.json();
|
|
125
|
+
const values: string[][] = data.values || [];
|
|
126
|
+
|
|
127
|
+
if (!values.length) {
|
|
128
|
+
results.push({
|
|
129
|
+
configId: config.id,
|
|
130
|
+
configName: config.name,
|
|
131
|
+
imported: 0,
|
|
132
|
+
updated: 0,
|
|
133
|
+
skipped: 0,
|
|
134
|
+
});
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const headerRowIndex = config.headerRow - 1;
|
|
139
|
+
const startRowIndex = Math.max(
|
|
140
|
+
headerRowIndex + 1,
|
|
141
|
+
(config.lastSyncedRow || headerRowIndex) + 1,
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
// Récupérer les headers
|
|
145
|
+
const headerRow = values[headerRowIndex] || [];
|
|
146
|
+
|
|
147
|
+
// Utiliser le nouveau format columnMappings
|
|
148
|
+
let columnMappings: Record<string, number> = {}; // crmField -> index
|
|
149
|
+
let noteFields: Array<{ name: string; index: number }> = [];
|
|
150
|
+
|
|
151
|
+
if (!config.columnMappings) {
|
|
152
|
+
results.push({
|
|
153
|
+
configId: config.id,
|
|
154
|
+
configName: config.name,
|
|
155
|
+
imported: 0,
|
|
156
|
+
updated: 0,
|
|
157
|
+
skipped: 0,
|
|
158
|
+
error:
|
|
159
|
+
"La configuration n'utilise pas le nouveau format de mapping. Veuillez reconfigurer cette intégration.",
|
|
160
|
+
});
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Parser les mappings
|
|
165
|
+
const mappings =
|
|
166
|
+
typeof config.columnMappings === 'string'
|
|
167
|
+
? JSON.parse(config.columnMappings)
|
|
168
|
+
: config.columnMappings;
|
|
169
|
+
|
|
170
|
+
if (!Array.isArray(mappings)) {
|
|
171
|
+
results.push({
|
|
172
|
+
configId: config.id,
|
|
173
|
+
configName: config.name,
|
|
174
|
+
imported: 0,
|
|
175
|
+
updated: 0,
|
|
176
|
+
skipped: 0,
|
|
177
|
+
error: 'Format de mapping invalide.',
|
|
178
|
+
});
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
mappings.forEach((mapping: any) => {
|
|
183
|
+
if (mapping.action === 'map' && mapping.crmField && mapping.columnName) {
|
|
184
|
+
// Trouver l'index de la colonne par son nom
|
|
185
|
+
const columnIndex = headerRow.findIndex(
|
|
186
|
+
(h: string) =>
|
|
187
|
+
h && h.trim().toLowerCase() === mapping.columnName.trim().toLowerCase(),
|
|
188
|
+
);
|
|
189
|
+
if (columnIndex !== -1) {
|
|
190
|
+
columnMappings[mapping.crmField] = columnIndex;
|
|
191
|
+
}
|
|
192
|
+
} else if (mapping.action === 'note' && mapping.columnName) {
|
|
193
|
+
const columnIndex = headerRow.findIndex(
|
|
194
|
+
(h: string) =>
|
|
195
|
+
h && h.trim().toLowerCase() === mapping.columnName.trim().toLowerCase(),
|
|
196
|
+
);
|
|
197
|
+
if (columnIndex !== -1) {
|
|
198
|
+
noteFields.push({ name: mapping.columnName, index: columnIndex });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// Vérifier que le téléphone est mappé (obligatoire)
|
|
204
|
+
if (columnMappings['phone'] === undefined) {
|
|
205
|
+
results.push({
|
|
206
|
+
configId: config.id,
|
|
207
|
+
configName: config.name,
|
|
208
|
+
imported: 0,
|
|
209
|
+
updated: 0,
|
|
210
|
+
skipped: 0,
|
|
211
|
+
error: "La colonne téléphone n'est pas correctement mappée.",
|
|
212
|
+
});
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const phoneIdx = columnMappings['phone'];
|
|
217
|
+
|
|
218
|
+
// Déterminer le statut par défaut à utiliser (configuré ou "Nouveau")
|
|
219
|
+
let effectiveDefaultStatusId = config.defaultStatusId || null;
|
|
220
|
+
if (!effectiveDefaultStatusId) {
|
|
221
|
+
const fallbackStatus = await client.status.findFirst({
|
|
222
|
+
where: { name: 'Nouveau' },
|
|
223
|
+
});
|
|
224
|
+
if (fallbackStatus) {
|
|
225
|
+
effectiveDefaultStatusId = fallbackStatus.id;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
let imported = 0;
|
|
230
|
+
let updated = 0;
|
|
231
|
+
let skipped = 0;
|
|
232
|
+
let maxProcessedRow = config.lastSyncedRow || headerRowIndex;
|
|
233
|
+
|
|
234
|
+
for (let rowIndex = startRowIndex; rowIndex < values.length; rowIndex++) {
|
|
235
|
+
const row = values[rowIndex];
|
|
236
|
+
if (!row) continue;
|
|
237
|
+
|
|
238
|
+
const phone = row[phoneIdx]?.trim();
|
|
239
|
+
if (!phone) {
|
|
240
|
+
skipped++;
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const firstName =
|
|
245
|
+
columnMappings['firstName'] !== undefined
|
|
246
|
+
? row[columnMappings['firstName']]?.trim() || undefined
|
|
247
|
+
: undefined;
|
|
248
|
+
const lastName =
|
|
249
|
+
columnMappings['lastName'] !== undefined
|
|
250
|
+
? row[columnMappings['lastName']]?.trim() || undefined
|
|
251
|
+
: undefined;
|
|
252
|
+
const email =
|
|
253
|
+
columnMappings['email'] !== undefined
|
|
254
|
+
? row[columnMappings['email']]?.trim() || undefined
|
|
255
|
+
: undefined;
|
|
256
|
+
const city =
|
|
257
|
+
columnMappings['city'] !== undefined
|
|
258
|
+
? row[columnMappings['city']]?.trim() || undefined
|
|
259
|
+
: undefined;
|
|
260
|
+
const postalCode =
|
|
261
|
+
columnMappings['postalCode'] !== undefined
|
|
262
|
+
? row[columnMappings['postalCode']]?.trim() || undefined
|
|
263
|
+
: undefined;
|
|
264
|
+
const origin =
|
|
265
|
+
columnMappings['origin'] !== undefined
|
|
266
|
+
? row[columnMappings['origin']]?.trim() || 'Google Sheets'
|
|
267
|
+
: 'Google Sheets';
|
|
268
|
+
|
|
269
|
+
// Collecter les notes si des colonnes sont configurées comme "note"
|
|
270
|
+
const noteContents: Array<{ label: string; value: string }> = [];
|
|
271
|
+
if (noteFields.length > 0) {
|
|
272
|
+
noteFields.forEach(({ name, index }) => {
|
|
273
|
+
if (row[index]) {
|
|
274
|
+
noteContents.push({
|
|
275
|
+
label: name,
|
|
276
|
+
value: row[index].trim(),
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Fonction pour échapper le HTML
|
|
283
|
+
const escapeHtml = (text: string): string => {
|
|
284
|
+
const map: { [key: string]: string } = {
|
|
285
|
+
'&': '&',
|
|
286
|
+
'<': '<',
|
|
287
|
+
'>': '>',
|
|
288
|
+
'"': '"',
|
|
289
|
+
"'": ''',
|
|
290
|
+
};
|
|
291
|
+
return text.replace(/[&<>"']/g, (m) => map[m]);
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
// Fonction pour formater le contenu de la note en HTML
|
|
295
|
+
const formatNoteContent = (noteItems: Array<{ label: string; value: string }>) => {
|
|
296
|
+
const escapedConfigName = escapeHtml(config.name);
|
|
297
|
+
|
|
298
|
+
if (noteItems.length === 0) {
|
|
299
|
+
return `<p style="font-size: 14px; line-height: 1.5; color: #374151; margin-bottom: 0;">Ce contact a été importé automatiquement depuis Google Sheets (${escapedConfigName}).</p>`;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
let html = `<p style="font-size: 14px; line-height: 1.5; color: #374151; margin-bottom: 16px;">Ce contact a été importé automatiquement depuis Google Sheets (${escapedConfigName}).</p>`;
|
|
303
|
+
|
|
304
|
+
html +=
|
|
305
|
+
'<div style="display: flex; flex-direction: column; gap: 12px; margin-top: 16px;">';
|
|
306
|
+
|
|
307
|
+
noteItems.forEach((item) => {
|
|
308
|
+
// Formater les valeurs qui sont des tableaux JSON
|
|
309
|
+
let formattedValue = item.value;
|
|
310
|
+
try {
|
|
311
|
+
const parsed = JSON.parse(item.value);
|
|
312
|
+
if (Array.isArray(parsed)) {
|
|
313
|
+
formattedValue = parsed.map((v) => String(v)).join(', ');
|
|
314
|
+
}
|
|
315
|
+
} catch {
|
|
316
|
+
// Ce n'est pas du JSON, on garde la valeur telle quelle
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Capitaliser le label pour le rendre plus humain
|
|
320
|
+
const humanLabel = item.label
|
|
321
|
+
.split(/(?=[A-Z])/)
|
|
322
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
323
|
+
.join(' ');
|
|
324
|
+
|
|
325
|
+
// Échapper le HTML pour éviter les injections XSS
|
|
326
|
+
const escapedLabel = escapeHtml(humanLabel);
|
|
327
|
+
const escapedValue = escapeHtml(String(formattedValue)).replace(/\n/g, '<br>');
|
|
328
|
+
|
|
329
|
+
html += `
|
|
330
|
+
<div style="padding: 12px 14px; background: linear-gradient(to right, #f8fafc 0%, #f1f5f9 100%); border-radius: 10px; border-left: 4px solid #6366f1; box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.05); transition: all 0.2s ease;">
|
|
331
|
+
<div style="font-size: 11px; font-weight: 700; color: #64748b; text-transform: uppercase; letter-spacing: 0.8px; margin-bottom: 6px;">
|
|
332
|
+
${escapedLabel}
|
|
333
|
+
</div>
|
|
334
|
+
<div style="font-size: 14px; line-height: 1.5; color: #0f172a; font-weight: 500;">
|
|
335
|
+
${escapedValue}
|
|
336
|
+
</div>
|
|
337
|
+
</div>
|
|
338
|
+
`;
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
html += '</div>';
|
|
342
|
+
return html;
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
// Déterminer l'assignation selon le rôle de l'utilisateur par défaut
|
|
346
|
+
let assignedCommercialId: string | null = null;
|
|
347
|
+
let assignedTeleproId: string | null = null;
|
|
348
|
+
|
|
349
|
+
if (config.defaultAssignedUserId) {
|
|
350
|
+
const defaultUser = await client.user.findUnique({
|
|
351
|
+
where: { id: config.defaultAssignedUserId },
|
|
352
|
+
select: { role: true },
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
if (defaultUser) {
|
|
356
|
+
if (
|
|
357
|
+
defaultUser.role === 'COMMERCIAL' ||
|
|
358
|
+
defaultUser.role === 'ADMIN' ||
|
|
359
|
+
defaultUser.role === 'MANAGER'
|
|
360
|
+
) {
|
|
361
|
+
assignedCommercialId = config.defaultAssignedUserId;
|
|
362
|
+
} else if (defaultUser.role === 'TELEPRO') {
|
|
363
|
+
assignedTeleproId = config.defaultAssignedUserId;
|
|
364
|
+
}
|
|
365
|
+
// Sinon, on ne assigne pas (null pour les deux)
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Vérifier si c'est un doublon (nom, prénom ET email)
|
|
370
|
+
const duplicateContactId = await handleContactDuplicate(
|
|
371
|
+
firstName,
|
|
372
|
+
lastName,
|
|
373
|
+
email,
|
|
374
|
+
origin,
|
|
375
|
+
config.defaultAssignedUserId || config.ownerUserId,
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
let contact;
|
|
379
|
+
let isNewContact = false;
|
|
380
|
+
|
|
381
|
+
if (duplicateContactId) {
|
|
382
|
+
// C'est un doublon, récupérer le contact existant
|
|
383
|
+
contact = await client.contact.findUnique({
|
|
384
|
+
where: { id: duplicateContactId },
|
|
385
|
+
});
|
|
386
|
+
updated++;
|
|
387
|
+
} else {
|
|
388
|
+
// Chercher un contact existant (par téléphone uniquement)
|
|
389
|
+
contact =
|
|
390
|
+
(email &&
|
|
391
|
+
(await client.contact.findFirst({
|
|
392
|
+
where: {
|
|
393
|
+
OR: [{ email: email.toLowerCase() }, { phone }],
|
|
394
|
+
},
|
|
395
|
+
}))) ||
|
|
396
|
+
(await client.contact.findFirst({
|
|
397
|
+
where: { phone },
|
|
398
|
+
}));
|
|
399
|
+
|
|
400
|
+
if (!contact) {
|
|
401
|
+
// Préparer les interactions à créer
|
|
402
|
+
const formattedContent = formatNoteContent(noteContents);
|
|
403
|
+
const interactionsToCreate: any[] = [
|
|
404
|
+
{
|
|
405
|
+
type: 'NOTE',
|
|
406
|
+
title: `Contact importé depuis Google Sheets: ${config.name}`,
|
|
407
|
+
content: formattedContent,
|
|
408
|
+
userId: config.defaultAssignedUserId || config.ownerUserId,
|
|
409
|
+
date: new Date(),
|
|
410
|
+
metadata: {
|
|
411
|
+
htmlContent: formattedContent,
|
|
412
|
+
isGoogleSheetsImport: true,
|
|
413
|
+
},
|
|
414
|
+
},
|
|
415
|
+
];
|
|
416
|
+
|
|
417
|
+
contact = await client.contact.create({
|
|
418
|
+
data: {
|
|
419
|
+
firstName: firstName || null,
|
|
420
|
+
lastName: lastName || null,
|
|
421
|
+
email: email ? email.toLowerCase() : null,
|
|
422
|
+
phone,
|
|
423
|
+
city: city || null,
|
|
424
|
+
postalCode: postalCode || null,
|
|
425
|
+
origin,
|
|
426
|
+
statusId: effectiveDefaultStatusId,
|
|
427
|
+
assignedCommercialId: assignedCommercialId,
|
|
428
|
+
assignedTeleproId: assignedTeleproId,
|
|
429
|
+
createdById: config.defaultAssignedUserId || config.ownerUserId,
|
|
430
|
+
interactions: {
|
|
431
|
+
create: interactionsToCreate,
|
|
432
|
+
},
|
|
433
|
+
},
|
|
434
|
+
});
|
|
435
|
+
isNewContact = true;
|
|
436
|
+
imported++;
|
|
437
|
+
} else {
|
|
438
|
+
await client.contact.update({
|
|
439
|
+
where: { id: contact.id },
|
|
440
|
+
data: {
|
|
441
|
+
firstName: contact.firstName || firstName || null,
|
|
442
|
+
lastName: contact.lastName || lastName || null,
|
|
443
|
+
email: contact.email || (email ? email.toLowerCase() : null),
|
|
444
|
+
city: contact.city || city || null,
|
|
445
|
+
postalCode: contact.postalCode || postalCode || null,
|
|
446
|
+
origin: contact.origin || origin,
|
|
447
|
+
statusId: contact.statusId || effectiveDefaultStatusId,
|
|
448
|
+
// Ne pas écraser les assignations existantes
|
|
449
|
+
assignedCommercialId: contact.assignedCommercialId || assignedCommercialId,
|
|
450
|
+
assignedTeleproId: contact.assignedTeleproId || assignedTeleproId,
|
|
451
|
+
},
|
|
452
|
+
});
|
|
453
|
+
updated++;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Créer une interaction de log uniquement pour les contacts mis à jour (pas les nouveaux qui ont déjà leur interaction)
|
|
458
|
+
if (contact && !isNewContact) {
|
|
459
|
+
// Contact mis à jour, créer l'interaction si nécessaire
|
|
460
|
+
const formattedContent = formatNoteContent(noteContents);
|
|
461
|
+
|
|
462
|
+
await client.interaction.create({
|
|
463
|
+
data: {
|
|
464
|
+
contactId: contact.id,
|
|
465
|
+
type: 'NOTE',
|
|
466
|
+
title: `Contact importé depuis Google Sheets: ${config.name}`,
|
|
467
|
+
content: formattedContent,
|
|
468
|
+
userId: config.defaultAssignedUserId || config.ownerUserId,
|
|
469
|
+
date: new Date(),
|
|
470
|
+
metadata: {
|
|
471
|
+
htmlContent: formattedContent,
|
|
472
|
+
isGoogleSheetsImport: true,
|
|
473
|
+
},
|
|
474
|
+
},
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (rowIndex > maxProcessedRow) {
|
|
479
|
+
maxProcessedRow = rowIndex;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (maxProcessedRow > (config.lastSyncedRow || headerRowIndex)) {
|
|
484
|
+
await client.googleSheetSyncConfig.update({
|
|
485
|
+
where: { id: config.id },
|
|
486
|
+
data: {
|
|
487
|
+
lastSyncedRow: maxProcessedRow,
|
|
488
|
+
},
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
totalImported += imported;
|
|
493
|
+
totalUpdated += updated;
|
|
494
|
+
totalSkipped += skipped;
|
|
495
|
+
|
|
496
|
+
results.push({
|
|
497
|
+
configId: config.id,
|
|
498
|
+
configName: config.name,
|
|
499
|
+
imported,
|
|
500
|
+
updated,
|
|
501
|
+
skipped,
|
|
502
|
+
});
|
|
503
|
+
} catch (error: any) {
|
|
504
|
+
console.error(`Erreur lors de la synchronisation de ${config.name}:`, error);
|
|
505
|
+
results.push({
|
|
506
|
+
configId: config.id,
|
|
507
|
+
configName: config.name,
|
|
508
|
+
imported: 0,
|
|
509
|
+
updated: 0,
|
|
510
|
+
skipped: 0,
|
|
511
|
+
error: error.message || 'Erreur lors de la synchronisation',
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
return NextResponse.json({
|
|
517
|
+
totalImported,
|
|
518
|
+
totalUpdated,
|
|
519
|
+
totalSkipped,
|
|
520
|
+
results,
|
|
521
|
+
});
|
|
522
|
+
} catch (error: any) {
|
|
523
|
+
console.error('Erreur lors de la synchronisation Google Sheets:', error);
|
|
524
|
+
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
|
|
525
|
+
}
|
|
526
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { prisma } from '@/lib/prisma';
|
|
3
|
+
import { hashPassword } from '@/lib/auth';
|
|
4
|
+
|
|
5
|
+
export async function POST(request: NextRequest) {
|
|
6
|
+
try {
|
|
7
|
+
const body = await request.json();
|
|
8
|
+
const { token, password } = body;
|
|
9
|
+
|
|
10
|
+
if (!token || !password) {
|
|
11
|
+
return NextResponse.json({ error: 'Token et mot de passe requis' }, { status: 400 });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (password.length < 6) {
|
|
15
|
+
return NextResponse.json(
|
|
16
|
+
{ error: 'Le mot de passe doit contenir au moins 6 caractères' },
|
|
17
|
+
{ status: 400 },
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Valider le token
|
|
22
|
+
const verification = await prisma.verification.findFirst({
|
|
23
|
+
where: {
|
|
24
|
+
value: token,
|
|
25
|
+
expiresAt: {
|
|
26
|
+
gt: new Date(),
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (!verification) {
|
|
32
|
+
return NextResponse.json({ error: 'Lien invalide ou expiré' }, { status: 400 });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Trouver l'utilisateur
|
|
36
|
+
const user = await prisma.user.findUnique({
|
|
37
|
+
where: { email: verification.identifier },
|
|
38
|
+
include: {
|
|
39
|
+
accounts: {
|
|
40
|
+
where: { providerId: 'credential' },
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (!user) {
|
|
46
|
+
return NextResponse.json({ error: 'Utilisateur non trouvé' }, { status: 404 });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Vérifier si le compte existe déjà
|
|
50
|
+
if (user.accounts.length > 0) {
|
|
51
|
+
return NextResponse.json({ error: 'Ce compte a déjà été activé' }, { status: 400 });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const newPassword = await hashPassword(password);
|
|
55
|
+
|
|
56
|
+
// Créer l'Account avec le mot de passe haché
|
|
57
|
+
await prisma.account.create({
|
|
58
|
+
data: {
|
|
59
|
+
id: crypto.randomUUID(),
|
|
60
|
+
accountId: user.id,
|
|
61
|
+
providerId: 'credential',
|
|
62
|
+
userId: user.id,
|
|
63
|
+
password: newPassword,
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Supprimer le token de vérification
|
|
68
|
+
await prisma.verification.delete({
|
|
69
|
+
where: { id: verification.id },
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Mettre à jour l'utilisateur comme vérifié
|
|
73
|
+
await prisma.user.update({
|
|
74
|
+
where: { id: user.id },
|
|
75
|
+
data: {
|
|
76
|
+
emailVerified: true,
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
return NextResponse.json({
|
|
81
|
+
success: true,
|
|
82
|
+
message: 'Mot de passe défini avec succès',
|
|
83
|
+
});
|
|
84
|
+
} catch (error: any) {
|
|
85
|
+
console.error('Erreur lors de la complétion:', error);
|
|
86
|
+
return NextResponse.json({ error: error.message || 'Erreur serveur' }, { status: 500 });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { prisma } from '@/lib/prisma';
|
|
3
|
+
|
|
4
|
+
export async function GET(request: NextRequest) {
|
|
5
|
+
try {
|
|
6
|
+
const { searchParams } = new URL(request.url);
|
|
7
|
+
const token = searchParams.get('token');
|
|
8
|
+
|
|
9
|
+
if (!token) {
|
|
10
|
+
return NextResponse.json({ error: 'Token manquant' }, { status: 400 });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Trouver le token de vérification
|
|
14
|
+
const verification = await prisma.verification.findFirst({
|
|
15
|
+
where: {
|
|
16
|
+
value: token,
|
|
17
|
+
expiresAt: {
|
|
18
|
+
gt: new Date(), // Pas expiré
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
if (!verification) {
|
|
24
|
+
return NextResponse.json({ error: 'Lien invalide ou expiré' }, { status: 400 });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Vérifier si l'utilisateur existe et n'a pas encore de compte
|
|
28
|
+
const user = await prisma.user.findUnique({
|
|
29
|
+
where: { email: verification.identifier },
|
|
30
|
+
include: {
|
|
31
|
+
accounts: {
|
|
32
|
+
where: { providerId: 'credential' },
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
if (!user) {
|
|
38
|
+
return NextResponse.json({ error: 'Utilisateur non trouvé' }, { status: 404 });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Vérifier si le compte existe déjà (mot de passe déjà défini)
|
|
42
|
+
if (user.accounts.length > 0) {
|
|
43
|
+
return NextResponse.json({ error: 'Ce compte a déjà été activé' }, { status: 400 });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return NextResponse.json({
|
|
47
|
+
email: user.email,
|
|
48
|
+
name: user.name,
|
|
49
|
+
valid: true,
|
|
50
|
+
});
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error('Erreur lors de la validation:', error);
|
|
53
|
+
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
|
|
54
|
+
}
|
|
55
|
+
}
|