create-crm-tmp 2.0.0 → 2.1.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 +56 -35
- package/package.json +1 -1
- package/template/README.md +230 -115
- package/template/eslint.config.mjs +13 -0
- package/template/next.config.ts +14 -0
- package/template/package.json +15 -2
- package/template/prisma/migrations/20260318095700_init_db/migration.sql +978 -0
- package/template/prisma/migrations/migration_lock.toml +3 -0
- package/template/prisma/schema.prisma +132 -637
- package/template/src/app/(auth)/invite/[token]/page.tsx +10 -8
- package/template/src/app/(auth)/layout.tsx +1 -1
- package/template/src/app/(auth)/reset-password/complete/page.tsx +11 -8
- package/template/src/app/(auth)/reset-password/page.tsx +4 -4
- package/template/src/app/(auth)/reset-password/verify/page.tsx +4 -4
- package/template/src/app/(auth)/signin/page.tsx +14 -6
- package/template/src/app/(dashboard)/agenda/page.tsx +2243 -988
- package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +18 -104
- package/template/src/app/(dashboard)/automatisation/page.tsx +10 -26
- package/template/src/app/(dashboard)/closing/page.tsx +78 -62
- package/template/src/app/(dashboard)/contacts/[id]/page.tsx +2082 -1080
- package/template/src/app/(dashboard)/contacts/companies/[id]/page.tsx +46 -47
- package/template/src/app/(dashboard)/contacts/page.tsx +1062 -780
- package/template/src/app/(dashboard)/dashboard/page.tsx +533 -37
- package/template/src/app/(dashboard)/dev/page.tsx +1291 -0
- package/template/src/app/(dashboard)/layout.tsx +6 -2
- package/template/src/app/(dashboard)/settings/page.tsx +797 -2582
- package/template/src/app/(dashboard)/templates/page.tsx +55 -54
- package/template/src/app/(dashboard)/users/list/page.tsx +51 -48
- package/template/src/app/(dashboard)/users/page.tsx +1 -1
- package/template/src/app/(dashboard)/users/permissions/page.tsx +2 -2
- package/template/src/app/(dashboard)/users/roles/page.tsx +7 -5
- package/template/src/app/api/agenda/google-events/route.ts +92 -0
- package/template/src/app/api/auth/check-active/route.ts +3 -2
- package/template/src/app/api/auth/google/route.ts +2 -1
- package/template/src/app/api/auth/google/status/route.ts +7 -31
- package/template/src/app/api/companies/[id]/activities/route.ts +1 -3
- package/template/src/app/api/companies/[id]/route.ts +1 -2
- package/template/src/app/api/companies/route.ts +42 -12
- package/template/src/app/api/contacts/[id]/files/[fileId]/preview/route.ts +9 -31
- package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +14 -32
- package/template/src/app/api/contacts/[id]/files/route.ts +112 -212
- package/template/src/app/api/contacts/[id]/interactions/[interactionId]/route.ts +27 -1
- package/template/src/app/api/contacts/[id]/interactions/route.ts +16 -16
- package/template/src/app/api/contacts/[id]/kyc/route.ts +21 -11
- package/template/src/app/api/contacts/[id]/meet/route.ts +19 -2
- package/template/src/app/api/contacts/[id]/route.ts +106 -34
- package/template/src/app/api/contacts/[id]/send-email/route.ts +27 -11
- package/template/src/app/api/contacts/[id]/workflows/run/route.ts +6 -0
- package/template/src/app/api/contacts/export/route.ts +9 -13
- package/template/src/app/api/contacts/import/route.ts +55 -25
- package/template/src/app/api/contacts/import-preview/route.ts +1 -1
- package/template/src/app/api/contacts/origins/route.ts +63 -0
- package/template/src/app/api/contacts/route.ts +153 -41
- package/template/src/app/api/cron/cleanup-editor-images/route.ts +166 -0
- package/template/src/app/api/dashboard/widgets/[id]/route.ts +44 -0
- package/template/src/app/api/dashboard/widgets/route.ts +181 -0
- package/template/src/app/api/dev/reminders/test/route.ts +114 -0
- package/template/src/app/api/editor/upload-image/route.ts +61 -0
- package/template/src/app/api/integrations/google-sheet/jobs/[jobId]/route.ts +47 -0
- package/template/src/app/api/integrations/google-sheet/jobs/usage/route.ts +50 -0
- package/template/src/app/api/integrations/google-sheet/sync/route.ts +24 -556
- package/template/src/app/api/jobs/google-sheet/process/route.ts +84 -0
- package/template/src/app/api/jobs/google-sheet/schedule/route.ts +50 -0
- package/template/src/app/api/reminders/clear/route.ts +120 -0
- package/template/src/app/api/reminders/clear/undo/route.ts +112 -0
- package/template/src/app/api/reminders/route.ts +164 -39
- package/template/src/app/api/reminders/state/route.ts +164 -0
- package/template/src/app/api/reset-password/request/route.ts +1 -1
- package/template/src/app/api/reset-password/verify/route.ts +1 -1
- package/template/src/app/api/send/route.ts +16 -4
- package/template/src/app/api/settings/google-ads/route.ts +14 -0
- package/template/src/app/api/settings/google-calendar/calendars/route.ts +97 -0
- package/template/src/app/api/settings/google-calendar/route.ts +124 -0
- package/template/src/app/api/settings/google-sheet/[id]/route.ts +28 -0
- package/template/src/app/api/settings/google-sheet/auto-map/route.ts +37 -4
- package/template/src/app/api/settings/google-sheet/preview/route.ts +9 -3
- package/template/src/app/api/settings/google-sheet/route.ts +14 -0
- package/template/src/app/api/settings/integrations/logs/route.ts +93 -0
- package/template/src/app/api/settings/integrations/notifications/route.ts +67 -0
- package/template/src/app/api/settings/meta-leads/[id]/route.ts +0 -1
- package/template/src/app/api/settings/meta-leads/route.ts +14 -2
- package/template/src/app/api/settings/smtp/route.ts +53 -6
- package/template/src/app/api/tasks/[id]/attendees/route.ts +24 -8
- package/template/src/app/api/tasks/[id]/route.ts +234 -58
- package/template/src/app/api/tasks/meet/route.ts +27 -19
- package/template/src/app/api/tasks/route.ts +62 -17
- package/template/src/app/api/users/[id]/route.ts +20 -14
- package/template/src/app/api/users/list/route.ts +57 -19
- package/template/src/app/api/webhooks/google-ads/route.ts +34 -14
- package/template/src/app/api/webhooks/meta-leads/route.ts +32 -12
- package/template/src/app/api/workflows/[id]/route.ts +0 -4
- package/template/src/app/api/workflows/process/route.ts +22 -51
- package/template/src/app/api/workflows/route.ts +0 -4
- package/template/src/app/globals.css +342 -4
- package/template/src/app/layout.tsx +11 -3
- package/template/src/app/page.tsx +1 -1
- package/template/src/components/address-autocomplete.tsx +7 -6
- package/template/src/components/config-error-alert.tsx +46 -0
- package/template/src/components/contacts/filter-bar.tsx +12 -3
- package/template/src/components/contacts/filter-builder.tsx +28 -43
- package/template/src/components/contacts/save-view-dialog.tsx +1 -1
- package/template/src/components/contacts/views-tab-bar.tsx +15 -6
- package/template/src/components/dashboard/activity-chart.tsx +41 -28
- package/template/src/components/dashboard/add-widget-dialog.tsx +157 -0
- package/template/src/components/dashboard/color-picker.tsx +64 -0
- package/template/src/components/dashboard/contacts-chart.tsx +69 -0
- package/template/src/components/dashboard/interactions-by-type-chart.tsx +121 -0
- package/template/src/components/dashboard/recent-activity.tsx +154 -0
- package/template/src/components/dashboard/stat-card.tsx +40 -40
- package/template/src/components/dashboard/status-distribution-chart.tsx +81 -0
- package/template/src/components/dashboard/tasks-pie-chart.tsx +37 -34
- package/template/src/components/dashboard/top-contacts-list.tsx +113 -0
- package/template/src/components/dashboard/upcoming-tasks-list.tsx +72 -81
- package/template/src/components/dashboard/widget-wrapper.tsx +36 -0
- package/template/src/components/date-picker.tsx +9 -6
- package/template/src/components/editor/upload-editor-image.ts +42 -0
- package/template/src/components/editor.tsx +161 -22
- package/template/src/components/email-template.tsx +2 -2
- package/template/src/components/global-search.tsx +30 -28
- package/template/src/components/header.tsx +178 -80
- package/template/src/components/inactive-account-guard.tsx +58 -0
- package/template/src/components/integration-notifications-listener.tsx +12 -0
- package/template/src/components/invitation-email-template.tsx +2 -2
- package/template/src/components/meet-cancellation-email-template.tsx +3 -3
- package/template/src/components/meet-confirmation-email-template.tsx +3 -3
- package/template/src/components/meet-update-email-template.tsx +3 -3
- package/template/src/components/page-header.tsx +5 -5
- package/template/src/components/protected-page.tsx +1 -1
- package/template/src/components/reset-password-email-template.tsx +2 -2
- package/template/src/components/settings/integrations/GoogleAdsIntegration.tsx +428 -0
- package/template/src/components/settings/integrations/GoogleSheetConfigMonitoringModal.tsx +680 -0
- package/template/src/components/settings/integrations/GoogleSheetIntegration.tsx +809 -0
- package/template/src/components/settings/integrations/ImportResultDialog.tsx +124 -0
- package/template/src/components/settings/integrations/IntegrationLogPanel.tsx +57 -0
- package/template/src/components/settings/integrations/IntegrationLogsTable.tsx +186 -0
- package/template/src/components/settings/integrations/MetaLeadIntegration.tsx +451 -0
- package/template/src/components/sidebar.tsx +45 -26
- package/template/src/components/skeleton.tsx +40 -43
- package/template/src/components/ui/accordion.tsx +2 -2
- package/template/src/components/ui/alert-dialog.tsx +1 -1
- package/template/src/components/ui/button.tsx +20 -9
- package/template/src/components/ui/components.tsx +1 -1
- package/template/src/components/ui/date-picker.tsx +422 -0
- package/template/src/components/ui/datetime-picker.tsx +338 -0
- package/template/src/components/ui/status-select.tsx +271 -0
- package/template/src/components/ui/tooltip.tsx +37 -0
- package/template/src/components/view-as-modal.tsx +13 -7
- package/template/src/contexts/app-toast-context.tsx +245 -57
- package/template/src/contexts/dashboard-theme-context.tsx +53 -0
- package/template/src/contexts/sidebar-context.tsx +22 -17
- package/template/src/contexts/task-reminder-context.tsx +134 -160
- package/template/src/contexts/view-as-context.tsx +33 -6
- package/template/src/hooks/use-focus-trap.ts +2 -2
- package/template/src/hooks/useIntegrationNotifications.ts +49 -0
- package/template/src/lib/auth.ts +8 -1
- package/template/src/lib/config-links.ts +14 -0
- package/template/src/lib/contact-duplicate.ts +79 -61
- package/template/src/lib/contact-interactions.ts +21 -21
- package/template/src/lib/contact-view-filters.ts +24 -64
- package/template/src/lib/contacts-list-url.ts +190 -0
- package/template/src/lib/dashboard-stats.ts +65 -7
- package/template/src/lib/dashboard-themes.ts +135 -0
- package/template/src/lib/date-utils.ts +127 -0
- package/template/src/lib/default-widgets.ts +12 -0
- package/template/src/lib/editor-html-image-dimensions.ts +172 -0
- package/template/src/lib/editor-image-limits.ts +19 -0
- package/template/src/lib/email-html-sanitize.ts +19 -0
- package/template/src/lib/encryption.ts +9 -6
- package/template/src/lib/fr-geography.ts +192 -0
- package/template/src/lib/google-calendar-agenda.ts +201 -0
- package/template/src/lib/google-calendar.ts +255 -5
- package/template/src/lib/google-sheet-sync-jobs.ts +96 -0
- package/template/src/lib/google-sheet-sync-runner.ts +514 -0
- package/template/src/lib/integration-import-log.ts +21 -0
- package/template/src/lib/permissions.ts +40 -10
- package/template/src/lib/prisma.ts +4 -1
- package/template/src/lib/qstash.ts +65 -0
- package/template/src/lib/reminder-state-server.ts +80 -0
- package/template/src/lib/reminder-state.ts +29 -0
- package/template/src/lib/supabase-storage.ts +113 -0
- package/template/src/lib/template-variables.ts +164 -23
- package/template/src/lib/utils.ts +45 -0
- package/template/src/lib/widget-registry.ts +173 -0
- package/template/src/lib/workflow-executor.ts +16 -70
- package/template/src/proxy.ts +1 -0
- package/template/vercel.json +3 -10
- package/template/skills-lock.json +0 -25
- package/template/src/components/dashboard/dashboard-content.tsx +0 -79
- package/template/src/lib/google-drive.ts +0 -1101
- package/template/src/types/yousign.ts +0 -52
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import * as XLSX from 'xlsx';
|
|
2
3
|
import { auth } from '@/lib/auth';
|
|
3
4
|
import { prisma } from '@/lib/prisma';
|
|
4
5
|
import { checkPermission } from '@/lib/check-permission';
|
|
5
|
-
import { handleContactDuplicate } from '@/lib/contact-duplicate';
|
|
6
|
-
import { normalizePhoneNumber } from '@/lib/utils';
|
|
6
|
+
import { handleContactDuplicate, markContactAsDuplicate } from '@/lib/contact-duplicate';
|
|
7
|
+
import { normalizePhoneNumber, parseImportDate } from '@/lib/utils';
|
|
7
8
|
|
|
8
9
|
// POST /api/contacts/import - Importer des contacts depuis un fichier CSV/Excel
|
|
9
10
|
export async function POST(request: NextRequest) {
|
|
@@ -74,7 +75,6 @@ export async function POST(request: NextRequest) {
|
|
|
74
75
|
rows = parseCSV(text);
|
|
75
76
|
} else if (fileExtension === 'xlsx' || fileExtension === 'xls') {
|
|
76
77
|
try {
|
|
77
|
-
const XLSX = require('xlsx');
|
|
78
78
|
const buffer = await file.arrayBuffer();
|
|
79
79
|
const workbook = XLSX.read(buffer, { type: 'array' });
|
|
80
80
|
const sheetName =
|
|
@@ -151,6 +151,32 @@ export async function POST(request: NextRequest) {
|
|
|
151
151
|
assignedCommercialId = defaultCommercialId;
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
+
// Construire socialNetworks depuis les colonnes mappées (linkedin, facebook, twitter, instagram)
|
|
155
|
+
const socialPlatforms: { platform: string; url: string }[] = [];
|
|
156
|
+
const socialKeys: { key: string; label: string }[] = [
|
|
157
|
+
{ key: 'linkedin', label: 'LinkedIn' },
|
|
158
|
+
{ key: 'facebook', label: 'Facebook' },
|
|
159
|
+
{ key: 'twitter', label: 'Twitter' },
|
|
160
|
+
{ key: 'instagram', label: 'Instagram' },
|
|
161
|
+
];
|
|
162
|
+
for (const { key, label } of socialKeys) {
|
|
163
|
+
const col = mapping[key];
|
|
164
|
+
if (col) {
|
|
165
|
+
const url = getValueFromRow(row, col);
|
|
166
|
+
if (url && String(url).trim()) {
|
|
167
|
+
let href = String(url).trim();
|
|
168
|
+
if (!href.startsWith('http://') && !href.startsWith('https://')) {
|
|
169
|
+
href = `https://${href}`;
|
|
170
|
+
}
|
|
171
|
+
socialPlatforms.push({ platform: label, url: href });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const createdAtImport = mapping.createdAt
|
|
177
|
+
? parseImportDate(getValueFromRow(row, mapping.createdAt))
|
|
178
|
+
: null;
|
|
179
|
+
|
|
154
180
|
const contactData: any = {
|
|
155
181
|
phone: normalizedPhone,
|
|
156
182
|
civility: getValueFromRow(row, mapping.civility) || null,
|
|
@@ -163,6 +189,10 @@ export async function POST(request: NextRequest) {
|
|
|
163
189
|
address: getValueFromRow(row, mapping.address) || null,
|
|
164
190
|
city: getValueFromRow(row, mapping.city) || null,
|
|
165
191
|
postalCode: getValueFromRow(row, mapping.postalCode) || null,
|
|
192
|
+
companyName: getValueFromRow(row, mapping.companyName) || null,
|
|
193
|
+
website: mapping.website ? (getValueFromRow(row, mapping.website) || null) : null,
|
|
194
|
+
jobTitle: getValueFromRow(row, mapping.jobTitle) || null,
|
|
195
|
+
socialNetworks: socialPlatforms.length > 0 ? socialPlatforms : null,
|
|
166
196
|
origin: origin,
|
|
167
197
|
statusId: statusId,
|
|
168
198
|
assignedCommercialId: assignedCommercialId,
|
|
@@ -170,6 +200,7 @@ export async function POST(request: NextRequest) {
|
|
|
170
200
|
? getValueFromRow(row, mapping.assignedTeleproId) || null
|
|
171
201
|
: null,
|
|
172
202
|
createdById: session.user.id,
|
|
203
|
+
...(createdAtImport && { createdAt: createdAtImport }),
|
|
173
204
|
};
|
|
174
205
|
|
|
175
206
|
// Collecter les notes à ajouter
|
|
@@ -200,6 +231,7 @@ export async function POST(request: NextRequest) {
|
|
|
200
231
|
// Créer les contacts en lot
|
|
201
232
|
const createdContacts = [];
|
|
202
233
|
const duplicateErrors = [];
|
|
234
|
+
const duplicateContactIds: string[] = [];
|
|
203
235
|
|
|
204
236
|
for (const contactDataWithNotes of contactsToCreate) {
|
|
205
237
|
// Extraire les notes et les données du contact
|
|
@@ -217,28 +249,10 @@ export async function POST(request: NextRequest) {
|
|
|
217
249
|
);
|
|
218
250
|
|
|
219
251
|
if (duplicateContactId) {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
status: true,
|
|
225
|
-
assignedCommercial: {
|
|
226
|
-
select: { id: true, name: true, email: true },
|
|
227
|
-
},
|
|
228
|
-
assignedTelepro: {
|
|
229
|
-
select: { id: true, name: true, email: true },
|
|
230
|
-
},
|
|
231
|
-
createdBy: {
|
|
232
|
-
select: { id: true, name: true, email: true },
|
|
233
|
-
},
|
|
234
|
-
},
|
|
235
|
-
});
|
|
236
|
-
if (existingContact) {
|
|
237
|
-
createdContacts.push(existingContact);
|
|
238
|
-
duplicateErrors.push(
|
|
239
|
-
`Contact ${contactData.firstName} ${contactData.lastName} (${contactData.email}) - doublon détecté`,
|
|
240
|
-
);
|
|
241
|
-
}
|
|
252
|
+
duplicateContactIds.push(duplicateContactId);
|
|
253
|
+
duplicateErrors.push(
|
|
254
|
+
`Contact ${contactData.firstName} ${contactData.lastName} (${contactData.email}) - doublon détecté`,
|
|
255
|
+
);
|
|
242
256
|
continue;
|
|
243
257
|
}
|
|
244
258
|
|
|
@@ -248,6 +262,13 @@ export async function POST(request: NextRequest) {
|
|
|
248
262
|
});
|
|
249
263
|
|
|
250
264
|
if (existingByPhone) {
|
|
265
|
+
duplicateContactIds.push(existingByPhone.id);
|
|
266
|
+
await markContactAsDuplicate(
|
|
267
|
+
existingByPhone.id,
|
|
268
|
+
contactData.origin || 'Import CSV/Excel',
|
|
269
|
+
session.user.id,
|
|
270
|
+
prisma,
|
|
271
|
+
);
|
|
251
272
|
duplicateErrors.push(`Téléphone ${contactData.phone} déjà existant`);
|
|
252
273
|
continue;
|
|
253
274
|
}
|
|
@@ -310,6 +331,15 @@ export async function POST(request: NextRequest) {
|
|
|
310
331
|
}
|
|
311
332
|
}
|
|
312
333
|
|
|
334
|
+
// Bumper le createdAt des doublons APRÈS la création des nouveaux contacts
|
|
335
|
+
// pour qu'ils remontent en haut de la liste (tri createdAt desc)
|
|
336
|
+
if (duplicateContactIds.length > 0) {
|
|
337
|
+
await prisma.contact.updateMany({
|
|
338
|
+
where: { id: { in: duplicateContactIds } },
|
|
339
|
+
data: { createdAt: new Date() },
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
313
343
|
return NextResponse.json({
|
|
314
344
|
success: true,
|
|
315
345
|
imported: createdContacts.length,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import * as XLSX from 'xlsx';
|
|
2
3
|
import { auth } from '@/lib/auth';
|
|
3
4
|
import { checkPermission } from '@/lib/check-permission';
|
|
4
5
|
|
|
@@ -45,7 +46,6 @@ function parseExcel(
|
|
|
45
46
|
preview: Record<string, string>[];
|
|
46
47
|
rawRows: string[][];
|
|
47
48
|
} | null {
|
|
48
|
-
const XLSX = require('xlsx');
|
|
49
49
|
const workbook = XLSX.read(buffer, { type: 'array' });
|
|
50
50
|
const sheetNames: string[] = workbook.SheetNames;
|
|
51
51
|
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { auth } from '@/lib/auth';
|
|
3
|
+
import { prisma } from '@/lib/prisma';
|
|
4
|
+
import { checkPermission } from '@/lib/check-permission';
|
|
5
|
+
|
|
6
|
+
/** Liste distincte des origines (tous les contacts visibles par l’utilisateur). */
|
|
7
|
+
export async function GET(request: Request) {
|
|
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
|
+
const [canViewAll, canViewOwn, canViewUnassigned] = await Promise.all([
|
|
18
|
+
checkPermission('contacts.view_all'),
|
|
19
|
+
checkPermission('contacts.view_own'),
|
|
20
|
+
checkPermission('contacts.view_unassigned'),
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
if (!canViewAll && !canViewOwn) {
|
|
24
|
+
return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const where: {
|
|
28
|
+
origin: { not: null };
|
|
29
|
+
AND?: object[];
|
|
30
|
+
} = {
|
|
31
|
+
origin: { not: null },
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
if (!canViewAll && canViewOwn) {
|
|
35
|
+
const ownershipConditions: object[] = [
|
|
36
|
+
{ assignedCommercialId: session.user.id },
|
|
37
|
+
{ assignedTeleproId: session.user.id },
|
|
38
|
+
{ createdById: session.user.id },
|
|
39
|
+
];
|
|
40
|
+
if (canViewUnassigned) {
|
|
41
|
+
ownershipConditions.push({
|
|
42
|
+
AND: [{ assignedCommercialId: null }, { assignedTeleproId: null }],
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
where.AND = [{ OR: ownershipConditions }];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const grouped = await prisma.contact.groupBy({
|
|
49
|
+
by: ['origin'],
|
|
50
|
+
where,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const origins = grouped
|
|
54
|
+
.map((g) => g.origin)
|
|
55
|
+
.filter((o): o is string => Boolean(o && String(o).trim()))
|
|
56
|
+
.sort((a, b) => a.localeCompare(b, 'fr'));
|
|
57
|
+
|
|
58
|
+
return NextResponse.json({ origins });
|
|
59
|
+
} catch (e) {
|
|
60
|
+
console.error('GET /api/contacts/origins:', e);
|
|
61
|
+
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { auth } from '@/lib/auth';
|
|
4
|
-
import { prisma } from '@/lib/prisma';
|
|
4
|
+
import { prisma, Prisma } from '@/lib/prisma';
|
|
5
5
|
import { checkPermission } from '@/lib/check-permission';
|
|
6
6
|
import { handleContactDuplicate } from '@/lib/contact-duplicate';
|
|
7
7
|
import { executeWorkflowsOnContactCreated } from '@/lib/workflow-executor';
|
|
8
8
|
import { normalizePhoneNumber } from '@/lib/utils';
|
|
9
9
|
import { buildPrismaWhereFromFilters } from '@/lib/contact-view-filters';
|
|
10
|
+
import {
|
|
11
|
+
expandRegionCodesToDepartmentCodes,
|
|
12
|
+
prismaPostalMatchesDepartmentsCondition,
|
|
13
|
+
} from '@/lib/fr-geography';
|
|
10
14
|
import type { ViewFilter, ViewSortConfig } from '@/types/contact-views';
|
|
11
15
|
|
|
16
|
+
const socialNetworkSchema = z.object({
|
|
17
|
+
platform: z.string().trim().min(1),
|
|
18
|
+
url: z.string().trim().url(),
|
|
19
|
+
});
|
|
20
|
+
|
|
12
21
|
const createContactSchema = z.object({
|
|
13
22
|
civility: z.enum(['M', 'MME', 'MLLE']).optional().nullable(),
|
|
14
23
|
firstName: z.string().trim().min(1).optional().nullable(),
|
|
@@ -20,14 +29,50 @@ const createContactSchema = z.object({
|
|
|
20
29
|
city: z.string().optional().nullable(),
|
|
21
30
|
postalCode: z.string().optional().nullable(),
|
|
22
31
|
origin: z.string().optional().nullable(),
|
|
32
|
+
companyName: z.string().trim().optional().nullable(),
|
|
23
33
|
companyId: z.string().optional().nullable(),
|
|
24
34
|
jobTitle: z.string().trim().optional().nullable(),
|
|
35
|
+
website: z.string().trim().url().optional().nullable().or(z.literal('')),
|
|
36
|
+
socialNetworks: z.array(socialNetworkSchema).optional().nullable(),
|
|
25
37
|
statusId: z.string().optional().nullable(),
|
|
26
38
|
closingReason: z.string().optional().nullable(),
|
|
27
39
|
assignedCommercialId: z.string().optional().nullable(),
|
|
28
40
|
assignedTeleproId: z.string().optional().nullable(),
|
|
29
41
|
});
|
|
30
42
|
|
|
43
|
+
function parseContactsListSortDirection(
|
|
44
|
+
sortDir: string | null,
|
|
45
|
+
sortOrder: string | null,
|
|
46
|
+
): 'asc' | 'desc' | null {
|
|
47
|
+
const raw = sortDir ?? sortOrder;
|
|
48
|
+
return raw === 'asc' || raw === 'desc' ? raw : null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Identifiants UI liste → orderBy Prisma (whitelist). */
|
|
52
|
+
function contactListPrismaOrderBy(
|
|
53
|
+
uiField: string,
|
|
54
|
+
direction: 'asc' | 'desc',
|
|
55
|
+
): Prisma.ContactOrderByWithRelationInput | null {
|
|
56
|
+
switch (uiField) {
|
|
57
|
+
case 'createdAt':
|
|
58
|
+
return { createdAt: direction };
|
|
59
|
+
case 'updatedAt':
|
|
60
|
+
return { updatedAt: direction };
|
|
61
|
+
case 'postalCode':
|
|
62
|
+
return { postalCode: direction };
|
|
63
|
+
case 'status':
|
|
64
|
+
return { status: { name: direction } };
|
|
65
|
+
case 'commercial':
|
|
66
|
+
return { assignedCommercial: { name: direction } };
|
|
67
|
+
case 'telepro':
|
|
68
|
+
return { assignedTelepro: { name: direction } };
|
|
69
|
+
case 'origin':
|
|
70
|
+
return { origin: direction };
|
|
71
|
+
default:
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
31
76
|
// GET /api/contacts - Récupérer tous les contacts avec filtres
|
|
32
77
|
export async function GET(request: NextRequest) {
|
|
33
78
|
try {
|
|
@@ -57,6 +102,8 @@ export async function GET(request: NextRequest) {
|
|
|
57
102
|
const assignedCommercialIds = searchParams.get('assignedCommercialIds');
|
|
58
103
|
const assignedTeleproIds = searchParams.get('assignedTeleproIds');
|
|
59
104
|
const origins = searchParams.get('origins');
|
|
105
|
+
const departmentCodesParam = searchParams.get('departmentCodes');
|
|
106
|
+
const regionCodesParam = searchParams.get('regionCodes');
|
|
60
107
|
const statusId = searchParams.get('statusId');
|
|
61
108
|
const assignedCommercialId = searchParams.get('assignedCommercialId');
|
|
62
109
|
const assignedTeleproId = searchParams.get('assignedTeleproId');
|
|
@@ -67,8 +114,9 @@ export async function GET(request: NextRequest) {
|
|
|
67
114
|
const updatedAtEnd = searchParams.get('updatedAtEnd');
|
|
68
115
|
const sortFieldParam = searchParams.get('sortField');
|
|
69
116
|
const sortDirParam = searchParams.get('sortDir');
|
|
117
|
+
const sortOrderParam = searchParams.get('sortOrder');
|
|
70
118
|
const page = Number.parseInt(searchParams.get('page') || '1');
|
|
71
|
-
const limit = Number.parseInt(searchParams.get('limit') || '50');
|
|
119
|
+
const limit = Math.min(Number.parseInt(searchParams.get('limit') || '50'), 200);
|
|
72
120
|
const skip = (page - 1) * limit;
|
|
73
121
|
|
|
74
122
|
const where: any = {};
|
|
@@ -119,13 +167,53 @@ export async function GET(request: NextRequest) {
|
|
|
119
167
|
}
|
|
120
168
|
|
|
121
169
|
if (search) {
|
|
170
|
+
const trimmedSearch = search.trim();
|
|
171
|
+
const searchTerms = trimmedSearch.split(/\s+/).filter(Boolean);
|
|
172
|
+
const firstTerm = searchTerms[0] || '';
|
|
173
|
+
const remainingTerms = searchTerms.slice(1).join(' ');
|
|
174
|
+
|
|
175
|
+
// Normaliser le numéro si la recherche ressemble à un téléphone
|
|
176
|
+
const digitsOnly = trimmedSearch.replace(/\D/g, '');
|
|
177
|
+
const looksLikePhone = digitsOnly.length >= 4;
|
|
178
|
+
const normalizedPhone = looksLikePhone ? normalizePhoneNumber(trimmedSearch) : null;
|
|
179
|
+
|
|
122
180
|
where.AND = where.AND || [];
|
|
123
181
|
where.AND.push({
|
|
124
182
|
OR: [
|
|
125
|
-
{ firstName: { contains:
|
|
126
|
-
{ lastName: { contains:
|
|
127
|
-
{ email: { contains:
|
|
128
|
-
{ phone: { contains:
|
|
183
|
+
{ firstName: { contains: trimmedSearch, mode: 'insensitive' } },
|
|
184
|
+
{ lastName: { contains: trimmedSearch, mode: 'insensitive' } },
|
|
185
|
+
{ email: { contains: trimmedSearch, mode: 'insensitive' } },
|
|
186
|
+
{ phone: { contains: trimmedSearch, mode: 'insensitive' } },
|
|
187
|
+
{ city: { contains: trimmedSearch, mode: 'insensitive' } },
|
|
188
|
+
{ postalCode: { contains: trimmedSearch, mode: 'insensitive' } },
|
|
189
|
+
{ address: { contains: trimmedSearch, mode: 'insensitive' } },
|
|
190
|
+
{ secondaryPhone: { contains: trimmedSearch, mode: 'insensitive' } },
|
|
191
|
+
{ origin: { contains: trimmedSearch, mode: 'insensitive' } },
|
|
192
|
+
{ jobTitle: { contains: trimmedSearch, mode: 'insensitive' } },
|
|
193
|
+
{ companyName: { contains: trimmedSearch, mode: 'insensitive' } },
|
|
194
|
+
{ company: { name: { contains: trimmedSearch, mode: 'insensitive' } } },
|
|
195
|
+
...(normalizedPhone
|
|
196
|
+
? [
|
|
197
|
+
{ phone: { contains: normalizedPhone, mode: 'insensitive' as const } },
|
|
198
|
+
{ secondaryPhone: { contains: normalizedPhone, mode: 'insensitive' as const } },
|
|
199
|
+
]
|
|
200
|
+
: []),
|
|
201
|
+
...(remainingTerms
|
|
202
|
+
? [
|
|
203
|
+
{
|
|
204
|
+
AND: [
|
|
205
|
+
{ firstName: { contains: firstTerm, mode: 'insensitive' } },
|
|
206
|
+
{ lastName: { contains: remainingTerms, mode: 'insensitive' } },
|
|
207
|
+
],
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
AND: [
|
|
211
|
+
{ firstName: { contains: remainingTerms, mode: 'insensitive' } },
|
|
212
|
+
{ lastName: { contains: firstTerm, mode: 'insensitive' } },
|
|
213
|
+
],
|
|
214
|
+
},
|
|
215
|
+
]
|
|
216
|
+
: []),
|
|
129
217
|
],
|
|
130
218
|
});
|
|
131
219
|
}
|
|
@@ -181,6 +269,25 @@ export async function GET(request: NextRequest) {
|
|
|
181
269
|
where.origin = origin;
|
|
182
270
|
}
|
|
183
271
|
|
|
272
|
+
if (departmentCodesParam) {
|
|
273
|
+
const codes = departmentCodesParam.split(',').filter(Boolean);
|
|
274
|
+
const geoCond = prismaPostalMatchesDepartmentsCondition(codes);
|
|
275
|
+
if (geoCond) {
|
|
276
|
+
where.AND = where.AND || [];
|
|
277
|
+
where.AND.push(geoCond);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (regionCodesParam) {
|
|
282
|
+
const codes = regionCodesParam.split(',').filter(Boolean);
|
|
283
|
+
const depts = expandRegionCodesToDepartmentCodes(codes);
|
|
284
|
+
const geoCond = prismaPostalMatchesDepartmentsCondition(depts);
|
|
285
|
+
if (geoCond) {
|
|
286
|
+
where.AND = where.AND || [];
|
|
287
|
+
where.AND.push(geoCond);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
184
291
|
if (createdAtStart || createdAtEnd) {
|
|
185
292
|
where.createdAt = {};
|
|
186
293
|
if (createdAtStart) {
|
|
@@ -216,11 +323,24 @@ export async function GET(request: NextRequest) {
|
|
|
216
323
|
}
|
|
217
324
|
|
|
218
325
|
// Determine sort order: explicit param > view config > default
|
|
219
|
-
let orderBy:
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
326
|
+
let orderBy: Prisma.ContactOrderByWithRelationInput = { createdAt: 'desc' };
|
|
327
|
+
const explicitDirection = parseContactsListSortDirection(sortDirParam, sortOrderParam);
|
|
328
|
+
if (sortFieldParam && explicitDirection) {
|
|
329
|
+
const mapped = contactListPrismaOrderBy(sortFieldParam, explicitDirection);
|
|
330
|
+
if (mapped) {
|
|
331
|
+
orderBy = mapped;
|
|
332
|
+
}
|
|
333
|
+
} else if (viewSortConfig?.field) {
|
|
334
|
+
const dir =
|
|
335
|
+
viewSortConfig.direction === 'asc' || viewSortConfig.direction === 'desc'
|
|
336
|
+
? viewSortConfig.direction
|
|
337
|
+
: null;
|
|
338
|
+
if (dir) {
|
|
339
|
+
const mapped = contactListPrismaOrderBy(viewSortConfig.field, dir);
|
|
340
|
+
if (mapped) {
|
|
341
|
+
orderBy = mapped;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
224
344
|
}
|
|
225
345
|
|
|
226
346
|
const [contacts, total] = await Promise.all([
|
|
@@ -238,28 +358,6 @@ export async function GET(request: NextRequest) {
|
|
|
238
358
|
createdBy: {
|
|
239
359
|
select: { id: true, name: true, email: true },
|
|
240
360
|
},
|
|
241
|
-
tourLinks: {
|
|
242
|
-
include: {
|
|
243
|
-
tour: {
|
|
244
|
-
select: {
|
|
245
|
-
id: true,
|
|
246
|
-
number: true,
|
|
247
|
-
},
|
|
248
|
-
},
|
|
249
|
-
},
|
|
250
|
-
},
|
|
251
|
-
transactions: {
|
|
252
|
-
select: {
|
|
253
|
-
id: true,
|
|
254
|
-
status: true,
|
|
255
|
-
totalAmountCents: true,
|
|
256
|
-
createdAt: true,
|
|
257
|
-
},
|
|
258
|
-
orderBy: {
|
|
259
|
-
createdAt: 'desc',
|
|
260
|
-
},
|
|
261
|
-
take: 1,
|
|
262
|
-
},
|
|
263
361
|
},
|
|
264
362
|
orderBy,
|
|
265
363
|
skip,
|
|
@@ -268,15 +366,22 @@ export async function GET(request: NextRequest) {
|
|
|
268
366
|
prisma.contact.count({ where }),
|
|
269
367
|
]);
|
|
270
368
|
|
|
271
|
-
return NextResponse.json(
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
369
|
+
return NextResponse.json(
|
|
370
|
+
{
|
|
371
|
+
contacts,
|
|
372
|
+
pagination: {
|
|
373
|
+
page,
|
|
374
|
+
limit,
|
|
375
|
+
total,
|
|
376
|
+
totalPages: Math.ceil(total / limit),
|
|
377
|
+
},
|
|
278
378
|
},
|
|
279
|
-
|
|
379
|
+
{
|
|
380
|
+
headers: {
|
|
381
|
+
'Cache-Control': 'private, no-store, max-age=0, must-revalidate',
|
|
382
|
+
},
|
|
383
|
+
},
|
|
384
|
+
);
|
|
280
385
|
} catch (error: any) {
|
|
281
386
|
console.error('Erreur lors de la récupération des contacts:', error);
|
|
282
387
|
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
|
|
@@ -323,8 +428,11 @@ export async function POST(request: NextRequest) {
|
|
|
323
428
|
city,
|
|
324
429
|
postalCode,
|
|
325
430
|
origin,
|
|
431
|
+
companyName,
|
|
326
432
|
companyId,
|
|
327
433
|
jobTitle,
|
|
434
|
+
website,
|
|
435
|
+
socialNetworks,
|
|
328
436
|
statusId,
|
|
329
437
|
closingReason,
|
|
330
438
|
assignedCommercialId,
|
|
@@ -374,8 +482,12 @@ export async function POST(request: NextRequest) {
|
|
|
374
482
|
city: city || null,
|
|
375
483
|
postalCode: postalCode || null,
|
|
376
484
|
origin: origin || null,
|
|
485
|
+
companyName: companyName && !companyId ? companyName : null,
|
|
377
486
|
companyId: companyId || null,
|
|
378
487
|
jobTitle: jobTitle || null,
|
|
488
|
+
website: website && website.trim() ? website : null,
|
|
489
|
+
socialNetworks:
|
|
490
|
+
socialNetworks && socialNetworks.length > 0 ? socialNetworks : Prisma.JsonNull,
|
|
379
491
|
statusId: statusId || null,
|
|
380
492
|
closingReason: closingReason || null,
|
|
381
493
|
assignedCommercialId: assignedCommercialId || null,
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { createClient } from '@supabase/supabase-js';
|
|
3
|
+
import { prisma } from '@/lib/prisma';
|
|
4
|
+
import { BUCKETS } from '@/lib/supabase-storage';
|
|
5
|
+
|
|
6
|
+
const BUCKET = BUCKETS.EDITOR_IMAGES;
|
|
7
|
+
const FOLDER = 'images';
|
|
8
|
+
// Ne supprimer que les images de plus de 24h (laisse le temps de sauvegarder)
|
|
9
|
+
const MIN_AGE_MS = 24 * 60 * 60 * 1000;
|
|
10
|
+
const LIST_LIMIT = 1000;
|
|
11
|
+
|
|
12
|
+
function getAdminClient() {
|
|
13
|
+
return createClient(
|
|
14
|
+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
15
|
+
process.env.SUPABASE_SERVICE_ROLE_KEY!,
|
|
16
|
+
{ auth: { autoRefreshToken: false, persistSession: false } },
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Collecte toutes les URLs d'images éditeur référencées en BDD.
|
|
22
|
+
*/
|
|
23
|
+
async function getReferencedImageUrls(): Promise<Set<string>> {
|
|
24
|
+
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
|
25
|
+
const prefix = `${supabaseUrl}/storage/v1/object/public/${BUCKET}/`;
|
|
26
|
+
|
|
27
|
+
const urls = new Set<string>();
|
|
28
|
+
|
|
29
|
+
function extractUrls(text: string | null) {
|
|
30
|
+
if (!text) return;
|
|
31
|
+
const regex = new RegExp(
|
|
32
|
+
prefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '[^"\'\\s<>]+',
|
|
33
|
+
'g',
|
|
34
|
+
);
|
|
35
|
+
for (const match of text.matchAll(regex)) {
|
|
36
|
+
urls.add(match[0]);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Templates
|
|
41
|
+
const templates = await prisma.template.findMany({ select: { content: true } });
|
|
42
|
+
for (const t of templates) extractUrls(t.content);
|
|
43
|
+
|
|
44
|
+
// Interactions (notes, emails)
|
|
45
|
+
const interactions = await prisma.interaction.findMany({
|
|
46
|
+
where: { content: { contains: BUCKET } },
|
|
47
|
+
select: { content: true },
|
|
48
|
+
});
|
|
49
|
+
for (const i of interactions) extractUrls(i.content);
|
|
50
|
+
|
|
51
|
+
// Tasks
|
|
52
|
+
const tasks = await prisma.task.findMany({
|
|
53
|
+
where: { description: { contains: BUCKET } },
|
|
54
|
+
select: { description: true },
|
|
55
|
+
});
|
|
56
|
+
for (const t of tasks) extractUrls(t.description);
|
|
57
|
+
|
|
58
|
+
// SMTP signatures
|
|
59
|
+
const smtpConfigs = await prisma.smtpConfig.findMany({
|
|
60
|
+
where: { signature: { not: null } },
|
|
61
|
+
select: { signature: true },
|
|
62
|
+
});
|
|
63
|
+
for (const s of smtpConfigs) extractUrls(s.signature);
|
|
64
|
+
|
|
65
|
+
// Workflow actions
|
|
66
|
+
const actions = await prisma.workflowAction.findMany({
|
|
67
|
+
where: {
|
|
68
|
+
OR: [
|
|
69
|
+
{ taskDescription: { contains: BUCKET } },
|
|
70
|
+
{ smsMessage: { contains: BUCKET } },
|
|
71
|
+
{ noteContent: { contains: BUCKET } },
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
select: { taskDescription: true, smsMessage: true, noteContent: true },
|
|
75
|
+
});
|
|
76
|
+
for (const a of actions) {
|
|
77
|
+
extractUrls(a.taskDescription);
|
|
78
|
+
extractUrls(a.smsMessage);
|
|
79
|
+
extractUrls(a.noteContent);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return urls;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// GET /api/cron/cleanup-editor-images
|
|
86
|
+
export async function GET(request: NextRequest) {
|
|
87
|
+
try {
|
|
88
|
+
const authHeader = request.headers.get('authorization');
|
|
89
|
+
if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
|
|
90
|
+
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const client = getAdminClient();
|
|
94
|
+
const now = Date.now();
|
|
95
|
+
|
|
96
|
+
// 1. Lister toutes les images du bucket
|
|
97
|
+
const allFiles: { name: string; created_at: string }[] = [];
|
|
98
|
+
let offset = 0;
|
|
99
|
+
while (true) {
|
|
100
|
+
const { data, error } = await client.storage.from(BUCKET).list(FOLDER, {
|
|
101
|
+
limit: LIST_LIMIT,
|
|
102
|
+
offset,
|
|
103
|
+
sortBy: { column: 'created_at', order: 'asc' },
|
|
104
|
+
});
|
|
105
|
+
if (error) throw new Error(`Erreur listing bucket: ${error.message}`);
|
|
106
|
+
if (!data || data.length === 0) break;
|
|
107
|
+
allFiles.push(...data.map((f) => ({ name: f.name, created_at: f.created_at ?? '' })));
|
|
108
|
+
if (data.length < LIST_LIMIT) break;
|
|
109
|
+
offset += LIST_LIMIT;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (allFiles.length === 0) {
|
|
113
|
+
return NextResponse.json({ deleted: 0, total: 0 });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 2. Filtrer : ne garder que les images > 24h
|
|
117
|
+
const oldFiles = allFiles.filter((f) => {
|
|
118
|
+
const createdAt = new Date(f.created_at).getTime();
|
|
119
|
+
return now - createdAt > MIN_AGE_MS;
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
if (oldFiles.length === 0) {
|
|
123
|
+
return NextResponse.json({ deleted: 0, total: allFiles.length, message: 'Aucune image ancienne' });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 3. Récupérer les URLs référencées en BDD
|
|
127
|
+
const referencedUrls = await getReferencedImageUrls();
|
|
128
|
+
|
|
129
|
+
// 4. Identifier les orphelines
|
|
130
|
+
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
|
131
|
+
const orphanPaths: string[] = [];
|
|
132
|
+
for (const file of oldFiles) {
|
|
133
|
+
const fullUrl = `${supabaseUrl}/storage/v1/object/public/${BUCKET}/${FOLDER}/${file.name}`;
|
|
134
|
+
if (!referencedUrls.has(fullUrl)) {
|
|
135
|
+
orphanPaths.push(`${FOLDER}/${file.name}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 5. Supprimer par batch de 100
|
|
140
|
+
let deleted = 0;
|
|
141
|
+
for (let i = 0; i < orphanPaths.length; i += 100) {
|
|
142
|
+
const batch = orphanPaths.slice(i, i + 100);
|
|
143
|
+
const { error } = await client.storage.from(BUCKET).remove(batch);
|
|
144
|
+
if (error) {
|
|
145
|
+
console.error(`Erreur suppression batch ${i}:`, error.message);
|
|
146
|
+
} else {
|
|
147
|
+
deleted += batch.length;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
console.log(
|
|
152
|
+
`[cleanup-editor-images] ${deleted} orphelines supprimées sur ${allFiles.length} total (${referencedUrls.size} référencées)`,
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
return NextResponse.json({
|
|
156
|
+
deleted,
|
|
157
|
+
total: allFiles.length,
|
|
158
|
+
referenced: referencedUrls.size,
|
|
159
|
+
orphans: orphanPaths.length,
|
|
160
|
+
});
|
|
161
|
+
} catch (error: unknown) {
|
|
162
|
+
console.error('[cleanup-editor-images] Erreur:', error);
|
|
163
|
+
const message = error instanceof Error ? error.message : 'Erreur serveur';
|
|
164
|
+
return NextResponse.json({ error: message }, { status: 500 });
|
|
165
|
+
}
|
|
166
|
+
}
|