create-crm-tmp 1.1.3 → 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/.prettierignore +2 -0
- package/template/README.md +230 -115
- package/template/components.json +22 -0
- package/template/eslint.config.mjs +13 -0
- package/template/exemple-contacts.csv +54 -0
- package/template/next.config.ts +41 -1
- package/template/package.json +63 -15
- package/template/prisma/migrations/20260318095700_init_db/migration.sql +978 -0
- package/template/prisma/schema.prisma +311 -67
- package/template/src/app/(auth)/invite/[token]/page.tsx +28 -29
- package/template/src/app/(auth)/layout.tsx +1 -1
- package/template/src/app/(auth)/reset-password/complete/page.tsx +21 -27
- package/template/src/app/(auth)/reset-password/page.tsx +14 -10
- package/template/src/app/(auth)/reset-password/verify/page.tsx +14 -10
- package/template/src/app/(auth)/signin/page.tsx +34 -23
- package/template/src/app/(dashboard)/agenda/page.tsx +3655 -2357
- package/template/src/app/(dashboard)/automatisation/[id]/page.tsx +10 -7
- package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +609 -338
- package/template/src/app/(dashboard)/automatisation/new/page.tsx +11 -8
- package/template/src/app/(dashboard)/automatisation/page.tsx +463 -186
- package/template/src/app/(dashboard)/closing/page.tsx +517 -469
- package/template/src/app/(dashboard)/contacts/[id]/page.tsx +6151 -4210
- package/template/src/app/(dashboard)/contacts/companies/[id]/page.tsx +1702 -0
- package/template/src/app/(dashboard)/contacts/loading.tsx +13 -0
- package/template/src/app/(dashboard)/contacts/page.tsx +4124 -2130
- package/template/src/app/(dashboard)/dashboard/page.tsx +119 -105
- package/template/src/app/(dashboard)/dev/page.tsx +1291 -0
- package/template/src/app/(dashboard)/error.tsx +37 -0
- package/template/src/app/(dashboard)/layout.tsx +6 -2
- package/template/src/app/(dashboard)/loading.tsx +5 -0
- package/template/src/app/(dashboard)/settings/loading.tsx +19 -0
- package/template/src/app/(dashboard)/settings/page.tsx +1773 -3362
- package/template/src/app/(dashboard)/templates/page.tsx +504 -303
- package/template/src/app/(dashboard)/users/list/page.tsx +364 -355
- package/template/src/app/(dashboard)/users/page.tsx +279 -310
- package/template/src/app/(dashboard)/users/permissions/page.tsx +104 -99
- package/template/src/app/(dashboard)/users/roles/page.tsx +169 -140
- package/template/src/app/api/agenda/google-events/route.ts +92 -0
- package/template/src/app/api/audit-logs/route.ts +1 -1
- package/template/src/app/api/auth/check-active/route.ts +3 -2
- package/template/src/app/api/auth/google/callback/route.ts +8 -5
- package/template/src/app/api/auth/google/disconnect/route.ts +2 -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 +129 -0
- package/template/src/app/api/companies/[id]/route.ts +194 -0
- package/template/src/app/api/companies/export/route.ts +206 -0
- package/template/src/app/api/companies/route.ts +196 -0
- package/template/src/app/api/contact-views/[id]/pin/route.ts +69 -0
- package/template/src/app/api/contact-views/[id]/route.ts +197 -0
- package/template/src/app/api/contact-views/route.ts +146 -0
- package/template/src/app/api/contacts/[id]/files/[fileId]/preview/route.ts +55 -0
- package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +20 -48
- package/template/src/app/api/contacts/[id]/files/route.ts +125 -186
- package/template/src/app/api/contacts/[id]/interactions/[interactionId]/route.ts +27 -1
- package/template/src/app/api/contacts/[id]/interactions/route.ts +45 -8
- package/template/src/app/api/contacts/[id]/kyc/route.ts +81 -0
- package/template/src/app/api/contacts/[id]/meet/route.ts +55 -29
- package/template/src/app/api/contacts/[id]/route.ts +184 -21
- package/template/src/app/api/contacts/[id]/send-email/route.ts +33 -11
- package/template/src/app/api/contacts/[id]/workflows/run/route.ts +67 -0
- package/template/src/app/api/contacts/export/route.ts +22 -31
- package/template/src/app/api/contacts/import/route.ts +77 -44
- package/template/src/app/api/contacts/import-preview/route.ts +139 -0
- package/template/src/app/api/contacts/origins/route.ts +63 -0
- package/template/src/app/api/contacts/route.ts +322 -57
- package/template/src/app/api/cron/cleanup-editor-images/route.ts +166 -0
- package/template/src/app/api/dashboard/stats/route.ts +9 -292
- package/template/src/app/api/dashboard/widgets/[id]/route.ts +0 -3
- package/template/src/app/api/dashboard/widgets/route.ts +19 -19
- 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 +28 -542
- package/template/src/app/api/invite/complete/route.ts +20 -23
- 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 +165 -39
- package/template/src/app/api/reminders/state/route.ts +164 -0
- package/template/src/app/api/reset-password/complete/route.ts +11 -13
- 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 +25 -47
- package/template/src/app/api/settings/closing-reasons/[id]/route.ts +10 -21
- package/template/src/app/api/settings/closing-reasons/route.ts +10 -21
- package/template/src/app/api/settings/company/route.ts +19 -26
- package/template/src/app/api/settings/google-ads/[id]/route.ts +20 -23
- package/template/src/app/api/settings/google-ads/route.ts +34 -23
- 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 +48 -23
- package/template/src/app/api/settings/google-sheet/auto-map/route.ts +56 -32
- package/template/src/app/api/settings/google-sheet/preview/route.ts +110 -0
- package/template/src/app/api/settings/google-sheet/route.ts +34 -23
- 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 +20 -24
- package/template/src/app/api/settings/meta-leads/route.ts +34 -25
- package/template/src/app/api/settings/smtp/route.ts +53 -6
- package/template/src/app/api/settings/statuses/[id]/route.ts +29 -32
- package/template/src/app/api/settings/statuses/route.ts +24 -22
- package/template/src/app/api/statuses/route.ts +2 -5
- package/template/src/app/api/tasks/[id]/attendees/route.ts +36 -13
- package/template/src/app/api/tasks/[id]/route.ts +357 -145
- package/template/src/app/api/tasks/meet/route.ts +37 -26
- package/template/src/app/api/tasks/route.ts +201 -96
- package/template/src/app/api/templates/[id]/route.ts +22 -13
- package/template/src/app/api/templates/route.ts +22 -5
- package/template/src/app/api/users/[id]/resend-invite/route.ts +95 -0
- package/template/src/app/api/users/[id]/route.ts +22 -16
- package/template/src/app/api/users/commercials/route.ts +38 -0
- package/template/src/app/api/users/for-agenda/route.ts +1 -2
- package/template/src/app/api/users/list/route.ts +57 -19
- package/template/src/app/api/users/route.ts +89 -34
- package/template/src/app/api/webhooks/google-ads/route.ts +40 -1
- package/template/src/app/api/webhooks/meta-leads/route.ts +38 -1
- package/template/src/app/api/workflows/[id]/route.ts +29 -6
- package/template/src/app/api/workflows/process/route.ts +505 -170
- package/template/src/app/api/workflows/route.ts +42 -4
- package/template/src/app/globals.css +512 -32
- package/template/src/app/layout.tsx +28 -9
- package/template/src/app/page.tsx +37 -7
- package/template/src/components/address-autocomplete.tsx +233 -0
- package/template/src/components/config-error-alert.tsx +46 -0
- package/template/src/components/contacts/filter-bar.tsx +190 -0
- package/template/src/components/contacts/filter-builder.tsx +574 -0
- package/template/src/components/contacts/save-view-dialog.tsx +160 -0
- package/template/src/components/contacts/views-tab-bar.tsx +449 -0
- package/template/src/components/dashboard/activity-chart.tsx +6 -1
- package/template/src/components/dashboard/add-widget-dialog.tsx +13 -17
- package/template/src/components/dashboard/color-picker.tsx +7 -8
- package/template/src/components/dashboard/recent-activity.tsx +2 -5
- package/template/src/components/dashboard/stat-card.tsx +1 -3
- package/template/src/components/dashboard/status-distribution-chart.tsx +0 -1
- package/template/src/components/dashboard/top-contacts-list.tsx +7 -13
- package/template/src/components/dashboard/upcoming-tasks-list.tsx +2 -5
- package/template/src/components/dashboard/widget-wrapper.tsx +3 -6
- package/template/src/components/date-picker.tsx +399 -0
- package/template/src/components/editor/upload-editor-image.ts +42 -0
- package/template/src/components/editor.tsx +188 -35
- package/template/src/components/email-template.tsx +4 -2
- package/template/src/components/global-search.tsx +360 -0
- package/template/src/components/header.tsx +200 -107
- 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 +4 -2
- package/template/src/components/lazy-editor.tsx +11 -0
- package/template/src/components/meet-cancellation-email-template.tsx +11 -3
- package/template/src/components/meet-confirmation-email-template.tsx +10 -3
- package/template/src/components/meet-update-email-template.tsx +10 -3
- package/template/src/components/page-header.tsx +19 -15
- package/template/src/components/protected-page.tsx +94 -0
- package/template/src/components/reset-password-email-template.tsx +4 -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 +117 -100
- package/template/src/components/skeleton.tsx +128 -45
- package/template/src/components/ui/accordion.tsx +64 -0
- package/template/src/components/ui/alert-dialog.tsx +139 -0
- package/template/src/components/ui/button.tsx +71 -0
- 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-banner.tsx +1 -1
- package/template/src/components/view-as-modal.tsx +30 -19
- package/template/src/config/nav-pages.ts +108 -0
- package/template/src/contexts/app-toast-context.tsx +362 -0
- package/template/src/contexts/dashboard-theme-context.tsx +2 -7
- package/template/src/contexts/sidebar-context.tsx +27 -53
- package/template/src/contexts/task-reminder-context.tsx +134 -160
- package/template/src/contexts/view-as-context.tsx +32 -10
- package/template/src/hooks/use-alert.tsx +65 -0
- package/template/src/hooks/use-confirm.tsx +87 -0
- package/template/src/hooks/use-contact-views.ts +140 -0
- package/template/src/hooks/use-contacts.ts +69 -0
- package/template/src/hooks/use-fetch.ts +17 -0
- package/template/src/hooks/use-focus-trap.ts +73 -0
- package/template/src/hooks/use-statuses.ts +22 -0
- package/template/src/hooks/useIntegrationNotifications.ts +49 -0
- package/template/src/lib/address-api.ts +155 -0
- package/template/src/lib/auth.ts +8 -1
- package/template/src/lib/cache.ts +73 -0
- package/template/src/lib/check-permission.ts +12 -177
- 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 +24 -22
- package/template/src/lib/contact-view-filters.ts +301 -0
- package/template/src/lib/contacts-list-url.ts +190 -0
- package/template/src/lib/dashboard-stats.ts +282 -0
- package/template/src/lib/dashboard-themes.ts +0 -5
- package/template/src/lib/date-utils.ts +176 -0
- package/template/src/lib/default-widgets.ts +0 -2
- 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/get-auth-user.ts +25 -0
- package/template/src/lib/google-calendar-agenda.ts +201 -0
- package/template/src/lib/google-calendar.ts +309 -17
- package/template/src/lib/google-fetch.ts +63 -0
- 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/local-storage.ts +34 -0
- package/template/src/lib/permissions.ts +268 -40
- package/template/src/lib/prisma.ts +15 -12
- 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/roles.ts +12 -15
- package/template/src/lib/supabase-storage.ts +113 -0
- package/template/src/lib/template-variables.ts +204 -29
- package/template/src/lib/utils.ts +71 -11
- package/template/src/lib/widget-registry.ts +0 -4
- package/template/src/lib/workflow-executor.ts +391 -228
- package/template/src/proxy.ts +35 -73
- package/template/src/types/contact-views.ts +351 -0
- package/template/vercel.json +5 -0
- package/template/WORKFLOWS_CRON.md +0 -185
- package/template/prisma/migrations/20251126144728_init/migration.sql +0 -78
- package/template/prisma/migrations/20251126155204_add_user_roles/migration.sql +0 -5
- package/template/prisma/migrations/20251128095126_add_company_info/migration.sql +0 -19
- package/template/prisma/migrations/20251128123321_add_smtp_config/migration.sql +0 -22
- package/template/prisma/migrations/20251128132303_add_status/migration.sql +0 -23
- package/template/prisma/migrations/20251201102207_add_user_active/migration.sql +0 -75
- package/template/prisma/migrations/20251201105507_add_email_signature/migration.sql +0 -2
- package/template/prisma/migrations/20251201151122_add_tasks/migration.sql +0 -45
- package/template/prisma/migrations/20251202111854_add_task_reminder/migration.sql +0 -2
- package/template/prisma/migrations/20251202135859_add_google_meet_integration/migration.sql +0 -27
- package/template/prisma/migrations/20251203103317_add_meta_lead_integration/migration.sql +0 -20
- package/template/prisma/migrations/20251203104002_add_google_ads_integration/migration.sql +0 -18
- package/template/prisma/migrations/20251203112122_add_google_sheet_integration/migration.sql +0 -32
- package/template/prisma/migrations/20251203153853_allow_multiple_integration_configs/migration.sql +0 -20
- package/template/prisma/migrations/20251205141705_update_user_roles/migration.sql +0 -12
- package/template/prisma/migrations/20251205150000_add_commercial_and_telepro_assignment/migration.sql +0 -21
- package/template/prisma/migrations/20251205160000_add_interaction_logging/migration.sql +0 -11
- package/template/prisma/migrations/20251208090314_add_automatic_interaction_types/migration.sql +0 -12
- package/template/prisma/migrations/20251208094843_mg/migration.sql +0 -14
- package/template/prisma/migrations/20251208100000_add_company_support/migration.sql +0 -14
- package/template/prisma/migrations/20251208110000_add_templates/migration.sql +0 -26
- package/template/prisma/migrations/20251208141304_add_video_conference_task_type/migration.sql +0 -2
- package/template/prisma/migrations/20251209104759_add_internal_note_to_task/migration.sql +0 -2
- package/template/prisma/migrations/20251209134803_add_company_field/migration.sql +0 -2
- package/template/prisma/migrations/20251209150000_rename_company_to_company_name/migration.sql +0 -3
- package/template/prisma/migrations/20251209150016_add_email_tracking/migration.sql +0 -21
- package/template/prisma/migrations/20251209155908_add_notify_contact_to_task/migration.sql +0 -2
- package/template/prisma/migrations/20251210110019_add_appointment_types/migration.sql +0 -10
- package/template/prisma/migrations/20251210113928_add_contact_files/migration.sql +0 -26
- package/template/prisma/migrations/20251212132339_add_custom_roles/migration.sql +0 -24
- package/template/prisma/migrations/20251215104448_add_file_interaction_types/migration.sql +0 -11
- package/template/prisma/migrations/20251215145616_add_closing_reasons/migration.sql +0 -12
- package/template/prisma/migrations/20251216140850_add_log_users/migration.sql +0 -25
- package/template/prisma/migrations/20251216151000_rename_perdu_to_ferme/migration.sql +0 -8
- package/template/prisma/migrations/20251216162318_add_column_mappings_to_google_sheet/migration.sql +0 -2
- package/template/prisma/migrations/20251216185127_add_workflows/migration.sql +0 -80
- package/template/prisma/migrations/20251216192237_add_scheduled_workflow_actions/migration.sql +0 -32
- package/template/prisma/migrations/20251220000000_add_task_interaction_type/migration.sql +0 -4
- package/template/prisma/migrations/20251221000000_add_task_type/migration.sql +0 -3
- package/template/prisma/migrations/20251221000001_add_event_color/migration.sql +0 -23
- package/template/prisma/migrations/20260210114913_add_dashboard_widget/migration.sql +0 -20
- package/template/prisma/migrations/20260226093949_fix_cascade_on_user_delete/migration.sql +0 -69
- package/template/src/app/(dashboard)/users/layout.tsx +0 -30
- package/template/src/lib/google-drive.ts +0 -380
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server';
|
|
2
2
|
import { auth } from '@/lib/auth';
|
|
3
3
|
import { prisma } from '@/lib/prisma';
|
|
4
|
-
import {
|
|
4
|
+
import { checkPermission } from '@/lib/check-permission';
|
|
5
|
+
import { BUCKETS, createSignedDownloadUrl } from '@/lib/supabase-storage';
|
|
5
6
|
|
|
6
7
|
// POST /api/contacts/export - Exporter des contacts en CSV ou Excel
|
|
7
8
|
export async function POST(request: NextRequest) {
|
|
@@ -14,17 +15,9 @@ export async function POST(request: NextRequest) {
|
|
|
14
15
|
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
select: { role: true },
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
if (user?.role !== 'ADMIN') {
|
|
24
|
-
return NextResponse.json(
|
|
25
|
-
{ error: 'Accès refusé. Seuls les administrateurs peuvent exporter des contacts.' },
|
|
26
|
-
{ status: 403 },
|
|
27
|
-
);
|
|
18
|
+
const canExport = await checkPermission('contacts.export');
|
|
19
|
+
if (!canExport) {
|
|
20
|
+
return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
|
|
28
21
|
}
|
|
29
22
|
|
|
30
23
|
const body = await request.json();
|
|
@@ -38,14 +31,13 @@ export async function POST(request: NextRequest) {
|
|
|
38
31
|
}
|
|
39
32
|
|
|
40
33
|
// Construire la requête pour récupérer les contacts
|
|
41
|
-
const where: any =
|
|
34
|
+
const where: any =
|
|
35
|
+
contactIds && Array.isArray(contactIds) && contactIds.length > 0
|
|
36
|
+
? { id: { in: contactIds } }
|
|
37
|
+
: {};
|
|
42
38
|
|
|
43
|
-
|
|
44
|
-
if (contactIds && Array.isArray(contactIds) && contactIds.length > 0) {
|
|
45
|
-
where.id = { in: contactIds };
|
|
46
|
-
}
|
|
39
|
+
const MAX_EXPORT = 10_000;
|
|
47
40
|
|
|
48
|
-
// Récupérer les contacts avec toutes les relations nécessaires (notes et fichiers inclus)
|
|
49
41
|
const contacts = await prisma.contact.findMany({
|
|
50
42
|
where,
|
|
51
43
|
include: {
|
|
@@ -72,19 +64,22 @@ export async function POST(request: NextRequest) {
|
|
|
72
64
|
},
|
|
73
65
|
},
|
|
74
66
|
orderBy: { createdAt: 'desc' },
|
|
67
|
+
take: 50,
|
|
75
68
|
},
|
|
76
69
|
files: {
|
|
77
70
|
select: {
|
|
78
71
|
fileName: true,
|
|
79
|
-
|
|
72
|
+
storagePath: true,
|
|
80
73
|
fileSize: true,
|
|
81
74
|
mimeType: true,
|
|
82
75
|
createdAt: true,
|
|
83
76
|
},
|
|
84
77
|
orderBy: { createdAt: 'desc' },
|
|
78
|
+
take: 20,
|
|
85
79
|
},
|
|
86
80
|
},
|
|
87
81
|
orderBy: { createdAt: 'desc' },
|
|
82
|
+
take: MAX_EXPORT,
|
|
88
83
|
});
|
|
89
84
|
|
|
90
85
|
if (contacts.length === 0) {
|
|
@@ -129,7 +124,7 @@ export async function POST(request: NextRequest) {
|
|
|
129
124
|
minute: '2-digit',
|
|
130
125
|
})
|
|
131
126
|
: '';
|
|
132
|
-
const author = interaction.user?.name || '
|
|
127
|
+
const author = interaction.user?.name || 'Inconnu';
|
|
133
128
|
const title = interaction.title ? `${interaction.title}: ` : '';
|
|
134
129
|
const content = interaction.content || '';
|
|
135
130
|
|
|
@@ -138,21 +133,17 @@ export async function POST(request: NextRequest) {
|
|
|
138
133
|
.join('\n\n');
|
|
139
134
|
};
|
|
140
135
|
|
|
141
|
-
|
|
142
|
-
const formatFiles = async (files: any[], userId: string) => {
|
|
136
|
+
const formatFiles = async (files: any[]) => {
|
|
143
137
|
if (!files || files.length === 0) return '';
|
|
144
138
|
|
|
145
139
|
const fileInfos = await Promise.allSettled(
|
|
146
140
|
files.map(async (file) => {
|
|
141
|
+
const sizeKB = (file.fileSize / 1024).toFixed(2);
|
|
147
142
|
try {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
return `${file.fileName} (${sizeKB} KB)
|
|
152
|
-
} catch (error) {
|
|
153
|
-
// Si échec, utiliser un lien basique
|
|
154
|
-
const sizeKB = (file.fileSize / 1024).toFixed(2);
|
|
155
|
-
return `${file.fileName} (${sizeKB} KB) - https://drive.google.com/file/d/${file.googleDriveFileId}/view`;
|
|
143
|
+
const url = await createSignedDownloadUrl(BUCKETS.CONTACTS, file.storagePath);
|
|
144
|
+
return `${file.fileName} (${sizeKB} KB) - ${url}`;
|
|
145
|
+
} catch {
|
|
146
|
+
return `${file.fileName} (${sizeKB} KB)`;
|
|
156
147
|
}
|
|
157
148
|
}),
|
|
158
149
|
);
|
|
@@ -168,7 +159,7 @@ export async function POST(request: NextRequest) {
|
|
|
168
159
|
const rows = await Promise.all(
|
|
169
160
|
contacts.map(async (contact) => {
|
|
170
161
|
const notes = formatNotes(contact.interactions || []);
|
|
171
|
-
const files = await formatFiles(contact.files || []
|
|
162
|
+
const files = await formatFiles(contact.files || []);
|
|
172
163
|
|
|
173
164
|
return [
|
|
174
165
|
contact.civility || '',
|
|
@@ -1,8 +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
|
-
import {
|
|
5
|
-
import {
|
|
5
|
+
import { checkPermission } from '@/lib/check-permission';
|
|
6
|
+
import { handleContactDuplicate, markContactAsDuplicate } from '@/lib/contact-duplicate';
|
|
7
|
+
import { normalizePhoneNumber, parseImportDate } from '@/lib/utils';
|
|
6
8
|
|
|
7
9
|
// POST /api/contacts/import - Importer des contacts depuis un fichier CSV/Excel
|
|
8
10
|
export async function POST(request: NextRequest) {
|
|
@@ -15,10 +17,18 @@ export async function POST(request: NextRequest) {
|
|
|
15
17
|
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
|
|
16
18
|
}
|
|
17
19
|
|
|
20
|
+
const canImport = await checkPermission('contacts.import');
|
|
21
|
+
if (!canImport) {
|
|
22
|
+
return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
|
|
23
|
+
}
|
|
24
|
+
|
|
18
25
|
const formData = await request.formData();
|
|
19
26
|
const file = formData.get('file') as File;
|
|
20
27
|
const fieldMappingsJson = formData.get('fieldMappings') as string;
|
|
21
28
|
const skipFirstRow = formData.get('skipFirstRow') === 'true';
|
|
29
|
+
const selectedSheetName = (formData.get('sheetName') as string) || undefined;
|
|
30
|
+
const headerRowStr = formData.get('headerRow') as string | null;
|
|
31
|
+
const headerRow = headerRowStr ? Number.parseInt(headerRowStr, 10) : 0;
|
|
22
32
|
|
|
23
33
|
// Récupérer les valeurs par défaut
|
|
24
34
|
const defaultStatusId = formData.get('defaultStatusId') as string | null;
|
|
@@ -64,14 +74,15 @@ export async function POST(request: NextRequest) {
|
|
|
64
74
|
const text = await file.text();
|
|
65
75
|
rows = parseCSV(text);
|
|
66
76
|
} else if (fileExtension === 'xlsx' || fileExtension === 'xls') {
|
|
67
|
-
// Parser Excel
|
|
68
77
|
try {
|
|
69
|
-
const XLSX = require('xlsx');
|
|
70
78
|
const buffer = await file.arrayBuffer();
|
|
71
79
|
const workbook = XLSX.read(buffer, { type: 'array' });
|
|
72
|
-
const sheetName =
|
|
80
|
+
const sheetName =
|
|
81
|
+
selectedSheetName && workbook.SheetNames.includes(selectedSheetName)
|
|
82
|
+
? selectedSheetName
|
|
83
|
+
: workbook.SheetNames[0];
|
|
73
84
|
const worksheet = workbook.Sheets[sheetName];
|
|
74
|
-
rows = XLSX.utils.sheet_to_json(worksheet, { raw: false });
|
|
85
|
+
rows = XLSX.utils.sheet_to_json(worksheet, { raw: false, range: headerRow });
|
|
75
86
|
} catch (error) {
|
|
76
87
|
return NextResponse.json(
|
|
77
88
|
{ error: 'Erreur lors du parsing Excel. Assurez-vous que xlsx est installé.' },
|
|
@@ -92,7 +103,11 @@ export async function POST(request: NextRequest) {
|
|
|
92
103
|
// Ignorer la première ligne si c'est un en-tête
|
|
93
104
|
const dataRows = skipFirstRow ? rows.slice(1) : rows;
|
|
94
105
|
|
|
95
|
-
//
|
|
106
|
+
// Pré-charger tous les statuts pour éviter des requêtes N+1 dans la boucle
|
|
107
|
+
const allStatuses = await prisma.status.findMany();
|
|
108
|
+
const statusByName = new Map(allStatuses.map((s) => [s.name, s.id]));
|
|
109
|
+
const defaultNewStatusId = statusByName.get('Nouveau') || null;
|
|
110
|
+
|
|
96
111
|
const contactsToCreate: any[] = [];
|
|
97
112
|
const errors: string[] = [];
|
|
98
113
|
const skipped: number[] = [];
|
|
@@ -102,38 +117,26 @@ export async function POST(request: NextRequest) {
|
|
|
102
117
|
const rowNumber = skipFirstRow ? i + 2 : i + 1;
|
|
103
118
|
|
|
104
119
|
try {
|
|
105
|
-
// Mapper les colonnes selon le mapping fourni
|
|
106
120
|
const phone = getValueFromRow(row, mapping.phone);
|
|
107
121
|
if (!phone) {
|
|
108
122
|
skipped.push(rowNumber);
|
|
109
|
-
continue;
|
|
123
|
+
continue;
|
|
110
124
|
}
|
|
111
125
|
|
|
112
|
-
// Normaliser le numéro de téléphone au format : 0X XX XX XX XX
|
|
113
126
|
const normalizedPhone = normalizePhoneNumber(phone.toString());
|
|
114
127
|
|
|
115
|
-
// Déterminer le statusId : utiliser le mapping si fourni, sinon le statut par défaut fourni
|
|
116
128
|
let statusId = null;
|
|
117
129
|
if (mapping.statusId) {
|
|
118
130
|
const mappedStatus = getValueFromRow(row, mapping.statusId);
|
|
119
131
|
if (mappedStatus) {
|
|
120
|
-
|
|
121
|
-
const status = await prisma.status.findUnique({
|
|
122
|
-
where: { name: mappedStatus },
|
|
123
|
-
});
|
|
124
|
-
statusId = status?.id || null;
|
|
132
|
+
statusId = statusByName.get(mappedStatus) || null;
|
|
125
133
|
}
|
|
126
134
|
}
|
|
127
|
-
// Si aucun statut n'a été trouvé via le mapping, utiliser le statut par défaut fourni
|
|
128
135
|
if (!statusId && defaultStatusId) {
|
|
129
136
|
statusId = defaultStatusId;
|
|
130
137
|
}
|
|
131
|
-
// Si toujours aucun statut, utiliser "Nouveau" par défaut
|
|
132
138
|
if (!statusId) {
|
|
133
|
-
|
|
134
|
-
where: { name: 'Nouveau' },
|
|
135
|
-
});
|
|
136
|
-
statusId = nouveauStatus?.id || null;
|
|
139
|
+
statusId = defaultNewStatusId;
|
|
137
140
|
}
|
|
138
141
|
|
|
139
142
|
// Déterminer l'origine : utiliser le mapping si fourni, sinon la valeur par défaut fournie
|
|
@@ -148,6 +151,32 @@ export async function POST(request: NextRequest) {
|
|
|
148
151
|
assignedCommercialId = defaultCommercialId;
|
|
149
152
|
}
|
|
150
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
|
+
|
|
151
180
|
const contactData: any = {
|
|
152
181
|
phone: normalizedPhone,
|
|
153
182
|
civility: getValueFromRow(row, mapping.civility) || null,
|
|
@@ -160,6 +189,10 @@ export async function POST(request: NextRequest) {
|
|
|
160
189
|
address: getValueFromRow(row, mapping.address) || null,
|
|
161
190
|
city: getValueFromRow(row, mapping.city) || null,
|
|
162
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,
|
|
163
196
|
origin: origin,
|
|
164
197
|
statusId: statusId,
|
|
165
198
|
assignedCommercialId: assignedCommercialId,
|
|
@@ -167,6 +200,7 @@ export async function POST(request: NextRequest) {
|
|
|
167
200
|
? getValueFromRow(row, mapping.assignedTeleproId) || null
|
|
168
201
|
: null,
|
|
169
202
|
createdById: session.user.id,
|
|
203
|
+
...(createdAtImport && { createdAt: createdAtImport }),
|
|
170
204
|
};
|
|
171
205
|
|
|
172
206
|
// Collecter les notes à ajouter
|
|
@@ -197,6 +231,7 @@ export async function POST(request: NextRequest) {
|
|
|
197
231
|
// Créer les contacts en lot
|
|
198
232
|
const createdContacts = [];
|
|
199
233
|
const duplicateErrors = [];
|
|
234
|
+
const duplicateContactIds: string[] = [];
|
|
200
235
|
|
|
201
236
|
for (const contactDataWithNotes of contactsToCreate) {
|
|
202
237
|
// Extraire les notes et les données du contact
|
|
@@ -214,28 +249,10 @@ export async function POST(request: NextRequest) {
|
|
|
214
249
|
);
|
|
215
250
|
|
|
216
251
|
if (duplicateContactId) {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
status: true,
|
|
222
|
-
assignedCommercial: {
|
|
223
|
-
select: { id: true, name: true, email: true },
|
|
224
|
-
},
|
|
225
|
-
assignedTelepro: {
|
|
226
|
-
select: { id: true, name: true, email: true },
|
|
227
|
-
},
|
|
228
|
-
createdBy: {
|
|
229
|
-
select: { id: true, name: true, email: true },
|
|
230
|
-
},
|
|
231
|
-
},
|
|
232
|
-
});
|
|
233
|
-
if (existingContact) {
|
|
234
|
-
createdContacts.push(existingContact);
|
|
235
|
-
duplicateErrors.push(
|
|
236
|
-
`Contact ${contactData.firstName} ${contactData.lastName} (${contactData.email}) - doublon détecté`,
|
|
237
|
-
);
|
|
238
|
-
}
|
|
252
|
+
duplicateContactIds.push(duplicateContactId);
|
|
253
|
+
duplicateErrors.push(
|
|
254
|
+
`Contact ${contactData.firstName} ${contactData.lastName} (${contactData.email}) - doublon détecté`,
|
|
255
|
+
);
|
|
239
256
|
continue;
|
|
240
257
|
}
|
|
241
258
|
|
|
@@ -245,6 +262,13 @@ export async function POST(request: NextRequest) {
|
|
|
245
262
|
});
|
|
246
263
|
|
|
247
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
|
+
);
|
|
248
272
|
duplicateErrors.push(`Téléphone ${contactData.phone} déjà existant`);
|
|
249
273
|
continue;
|
|
250
274
|
}
|
|
@@ -307,6 +331,15 @@ export async function POST(request: NextRequest) {
|
|
|
307
331
|
}
|
|
308
332
|
}
|
|
309
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
|
+
|
|
310
343
|
return NextResponse.json({
|
|
311
344
|
success: true,
|
|
312
345
|
imported: createdContacts.length,
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import * as XLSX from 'xlsx';
|
|
3
|
+
import { auth } from '@/lib/auth';
|
|
4
|
+
import { checkPermission } from '@/lib/check-permission';
|
|
5
|
+
|
|
6
|
+
function parseCsv(
|
|
7
|
+
text: string,
|
|
8
|
+
headerRow = 0,
|
|
9
|
+
): {
|
|
10
|
+
headers: string[];
|
|
11
|
+
preview: Record<string, string>[];
|
|
12
|
+
rawRows: string[][];
|
|
13
|
+
} | null {
|
|
14
|
+
const lines = text.split('\n').filter((line) => line.trim() !== '');
|
|
15
|
+
if (lines.length === 0) return null;
|
|
16
|
+
|
|
17
|
+
const delimiter = lines[0].includes(';') ? ';' : ',';
|
|
18
|
+
const strip = (s: string) => s.trim().replaceAll(/(^")|("$)/g, '');
|
|
19
|
+
|
|
20
|
+
const rawRows: string[][] = lines.slice(0, 20).map((line) => line.split(delimiter).map(strip));
|
|
21
|
+
|
|
22
|
+
if (headerRow >= lines.length) return null;
|
|
23
|
+
|
|
24
|
+
const headers = lines[headerRow].split(delimiter).map(strip);
|
|
25
|
+
|
|
26
|
+
const preview: Record<string, string>[] = [];
|
|
27
|
+
for (let i = headerRow + 1; i < Math.min(headerRow + 6, lines.length); i++) {
|
|
28
|
+
const values = lines[i].split(delimiter).map(strip);
|
|
29
|
+
const row: Record<string, string> = {};
|
|
30
|
+
headers.forEach((header, index) => {
|
|
31
|
+
row[header] = values[index] || '';
|
|
32
|
+
});
|
|
33
|
+
preview.push(row);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return { headers, preview, rawRows };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function parseExcel(
|
|
40
|
+
buffer: ArrayBuffer,
|
|
41
|
+
sheetName?: string,
|
|
42
|
+
headerRow = 0,
|
|
43
|
+
): {
|
|
44
|
+
sheetNames: string[];
|
|
45
|
+
headers: string[];
|
|
46
|
+
preview: Record<string, string>[];
|
|
47
|
+
rawRows: string[][];
|
|
48
|
+
} | null {
|
|
49
|
+
const workbook = XLSX.read(buffer, { type: 'array' });
|
|
50
|
+
const sheetNames: string[] = workbook.SheetNames;
|
|
51
|
+
|
|
52
|
+
const targetSheet = sheetName && sheetNames.includes(sheetName) ? sheetName : sheetNames[0];
|
|
53
|
+
const worksheet = workbook.Sheets[targetSheet];
|
|
54
|
+
|
|
55
|
+
const allRows: string[][] = XLSX.utils.sheet_to_json(worksheet, {
|
|
56
|
+
header: 1,
|
|
57
|
+
raw: false,
|
|
58
|
+
defval: '',
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
if (allRows.length === 0) return null;
|
|
62
|
+
|
|
63
|
+
const rawRows = allRows
|
|
64
|
+
.slice(0, 20)
|
|
65
|
+
.map((row: unknown[]) => row.map((cell) => String(cell ?? '')));
|
|
66
|
+
|
|
67
|
+
if (headerRow >= allRows.length) return null;
|
|
68
|
+
|
|
69
|
+
const headers = allRows[headerRow]
|
|
70
|
+
.map((cell: unknown) => String(cell ?? '').trim())
|
|
71
|
+
.filter(Boolean);
|
|
72
|
+
|
|
73
|
+
const preview: Record<string, string>[] = [];
|
|
74
|
+
for (let i = headerRow + 1; i < Math.min(headerRow + 6, allRows.length); i++) {
|
|
75
|
+
const values = allRows[i];
|
|
76
|
+
const row: Record<string, string> = {};
|
|
77
|
+
headers.forEach((header, index) => {
|
|
78
|
+
row[header] = String(values[index] ?? '');
|
|
79
|
+
});
|
|
80
|
+
preview.push(row);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return { sheetNames, headers, preview, rawRows };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function POST(request: NextRequest) {
|
|
87
|
+
try {
|
|
88
|
+
const session = await auth.api.getSession({ headers: request.headers });
|
|
89
|
+
if (!session) {
|
|
90
|
+
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const hasPermission = await checkPermission('contacts.create');
|
|
94
|
+
if (!hasPermission) {
|
|
95
|
+
return NextResponse.json({ error: 'Permission refusée' }, { status: 403 });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const formData = await request.formData();
|
|
99
|
+
const file = formData.get('file') as File | null;
|
|
100
|
+
const sheetName = (formData.get('sheetName') as string) || undefined;
|
|
101
|
+
const headerRowStr = formData.get('headerRow') as string | null;
|
|
102
|
+
const headerRow = headerRowStr ? Number.parseInt(headerRowStr, 10) : 0;
|
|
103
|
+
|
|
104
|
+
if (!file) {
|
|
105
|
+
return NextResponse.json({ error: 'Aucun fichier fourni' }, { status: 400 });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const fileExtension = file.name.toLowerCase().split('.').pop();
|
|
109
|
+
|
|
110
|
+
if (fileExtension === 'csv') {
|
|
111
|
+
const result = parseCsv(await file.text(), headerRow);
|
|
112
|
+
if (!result) {
|
|
113
|
+
return NextResponse.json({ error: 'Le fichier est vide' }, { status: 400 });
|
|
114
|
+
}
|
|
115
|
+
return NextResponse.json(result);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (fileExtension === 'xlsx' || fileExtension === 'xls') {
|
|
119
|
+
try {
|
|
120
|
+
const result = parseExcel(await file.arrayBuffer(), sheetName, headerRow);
|
|
121
|
+
if (!result) {
|
|
122
|
+
return NextResponse.json({ error: 'Le fichier est vide' }, { status: 400 });
|
|
123
|
+
}
|
|
124
|
+
return NextResponse.json(result);
|
|
125
|
+
} catch {
|
|
126
|
+
return NextResponse.json({ error: 'Erreur lors du parsing Excel.' }, { status: 400 });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return NextResponse.json(
|
|
131
|
+
{ error: 'Format de fichier non supporté. Utilisez CSV ou Excel (.xlsx, .xls)' },
|
|
132
|
+
{ status: 400 },
|
|
133
|
+
);
|
|
134
|
+
} catch (error: unknown) {
|
|
135
|
+
console.error('Erreur import-preview:', error);
|
|
136
|
+
const message = error instanceof Error ? error.message : 'Erreur serveur';
|
|
137
|
+
return NextResponse.json({ error: message }, { status: 500 });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -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
|
+
}
|