create-crm-tmp 1.1.2 → 2.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/package.json +1 -1
- package/template/.prettierignore +2 -0
- package/template/README.md +53 -67
- package/template/components.json +22 -0
- package/template/exemple-contacts.csv +54 -0
- package/template/next.config.ts +27 -1
- package/template/package.json +64 -27
- package/template/prisma/schema.prisma +821 -72
- package/template/skills-lock.json +25 -0
- package/template/src/app/(auth)/invite/[token]/page.tsx +21 -24
- package/template/src/app/(auth)/reset-password/complete/page.tsx +12 -21
- package/template/src/app/(auth)/reset-password/page.tsx +12 -8
- package/template/src/app/(auth)/reset-password/verify/page.tsx +12 -8
- package/template/src/app/(auth)/signin/page.tsx +20 -17
- package/template/src/app/(dashboard)/agenda/page.tsx +2231 -2188
- package/template/src/app/(dashboard)/automatisation/[id]/page.tsx +10 -7
- package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +680 -323
- package/template/src/app/(dashboard)/automatisation/new/page.tsx +11 -8
- package/template/src/app/(dashboard)/automatisation/page.tsx +473 -180
- package/template/src/app/(dashboard)/closing/page.tsx +500 -468
- package/template/src/app/(dashboard)/contacts/[id]/page.tsx +5035 -4126
- package/template/src/app/(dashboard)/contacts/companies/[id]/page.tsx +1703 -0
- package/template/src/app/(dashboard)/contacts/loading.tsx +13 -0
- package/template/src/app/(dashboard)/contacts/page.tsx +3776 -2064
- package/template/src/app/(dashboard)/dashboard/page.tsx +37 -519
- package/template/src/app/(dashboard)/error.tsx +37 -0
- package/template/src/app/(dashboard)/layout.tsx +1 -1
- 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 +2685 -2489
- package/template/src/app/(dashboard)/templates/page.tsx +500 -300
- package/template/src/app/(dashboard)/users/list/page.tsx +356 -350
- 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 +164 -137
- package/template/src/app/api/audit-logs/route.ts +1 -1
- 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/companies/[id]/activities/route.ts +131 -0
- package/template/src/app/api/companies/[id]/route.ts +195 -0
- package/template/src/app/api/companies/export/route.ts +206 -0
- package/template/src/app/api/companies/route.ts +166 -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 +77 -0
- package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +7 -17
- package/template/src/app/api/contacts/[id]/files/route.ts +83 -44
- package/template/src/app/api/contacts/[id]/interactions/route.ts +37 -0
- package/template/src/app/api/contacts/[id]/kyc/route.ts +71 -0
- package/template/src/app/api/contacts/[id]/meet/route.ts +38 -29
- package/template/src/app/api/contacts/[id]/route.ts +111 -20
- package/template/src/app/api/contacts/[id]/send-email/route.ts +6 -0
- package/template/src/app/api/contacts/[id]/workflows/run/route.ts +61 -0
- package/template/src/app/api/contacts/export/route.ts +12 -17
- package/template/src/app/api/contacts/import/route.ts +22 -19
- package/template/src/app/api/contacts/import-preview/route.ts +139 -0
- package/template/src/app/api/contacts/route.ts +202 -49
- package/template/src/app/api/dashboard/stats/route.ts +9 -292
- package/template/src/app/api/integrations/google-sheet/sync/route.ts +203 -185
- package/template/src/app/api/invite/complete/route.ts +20 -23
- package/template/src/app/api/reminders/route.ts +1 -0
- package/template/src/app/api/reset-password/complete/route.ts +11 -13
- package/template/src/app/api/send/route.ts +9 -85
- 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 +20 -23
- package/template/src/app/api/settings/google-sheet/[id]/route.ts +20 -23
- package/template/src/app/api/settings/google-sheet/auto-map/route.ts +23 -32
- package/template/src/app/api/settings/google-sheet/preview/route.ts +104 -0
- package/template/src/app/api/settings/google-sheet/route.ts +20 -23
- package/template/src/app/api/settings/meta-leads/[id]/route.ts +20 -23
- package/template/src/app/api/settings/meta-leads/route.ts +20 -23
- package/template/src/app/api/settings/statuses/[id]/route.ts +33 -23
- 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 +14 -7
- package/template/src/app/api/tasks/[id]/route.ts +161 -137
- package/template/src/app/api/tasks/meet/route.ts +11 -8
- package/template/src/app/api/tasks/route.ts +155 -95
- 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 +16 -1
- 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/route.ts +94 -55
- package/template/src/app/api/webhooks/google-ads/route.ts +20 -1
- package/template/src/app/api/webhooks/meta-leads/route.ts +18 -1
- package/template/src/app/api/workflows/[id]/route.ts +33 -6
- package/template/src/app/api/workflows/process/route.ts +509 -146
- package/template/src/app/api/workflows/route.ts +46 -4
- package/template/src/app/globals.css +210 -101
- package/template/src/app/layout.tsx +19 -8
- package/template/src/app/page.tsx +37 -7
- package/template/src/components/address-autocomplete.tsx +232 -0
- package/template/src/components/contacts/filter-bar.tsx +181 -0
- package/template/src/components/contacts/filter-builder.tsx +589 -0
- package/template/src/components/contacts/save-view-dialog.tsx +160 -0
- package/template/src/components/contacts/views-tab-bar.tsx +440 -0
- package/template/src/components/dashboard/activity-chart.tsx +31 -39
- package/template/src/components/dashboard/dashboard-content.tsx +79 -0
- package/template/src/components/dashboard/stat-card.tsx +40 -42
- package/template/src/components/dashboard/tasks-pie-chart.tsx +34 -37
- package/template/src/components/dashboard/upcoming-tasks-list.tsx +78 -72
- package/template/src/components/date-picker.tsx +396 -0
- package/template/src/components/editor.tsx +27 -13
- package/template/src/components/email-template.tsx +4 -2
- package/template/src/components/global-search.tsx +358 -0
- package/template/src/components/header.tsx +57 -62
- 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/sidebar.tsx +92 -94
- package/template/src/components/skeleton.tsx +128 -42
- 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 +60 -0
- package/template/src/components/view-as-banner.tsx +1 -1
- package/template/src/components/view-as-modal.tsx +21 -16
- package/template/src/config/nav-pages.ts +108 -0
- package/template/src/contexts/app-toast-context.tsx +174 -0
- package/template/src/contexts/sidebar-context.tsx +16 -47
- package/template/src/contexts/task-reminder-context.tsx +6 -6
- package/template/src/contexts/view-as-context.tsx +11 -16
- 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/lib/address-api.ts +155 -0
- package/template/src/lib/cache.ts +73 -0
- package/template/src/lib/check-permission.ts +12 -177
- package/template/src/lib/contact-interactions.ts +3 -1
- package/template/src/lib/contact-view-filters.ts +341 -0
- package/template/src/lib/dashboard-stats.ts +224 -0
- package/template/src/lib/date-utils.ts +49 -0
- package/template/src/lib/get-auth-user.ts +25 -0
- package/template/src/lib/google-calendar.ts +54 -12
- package/template/src/lib/google-drive.ts +796 -75
- package/template/src/lib/google-fetch.ts +63 -0
- package/template/src/lib/local-storage.ts +34 -0
- package/template/src/lib/permissions.ts +245 -47
- package/template/src/lib/prisma.ts +11 -11
- package/template/src/lib/roles.ts +14 -39
- package/template/src/lib/template-variables.ts +67 -33
- package/template/src/lib/utils.ts +26 -2
- package/template/src/lib/workflow-executor.ts +445 -229
- package/template/src/proxy.ts +34 -73
- package/template/src/types/contact-views.ts +351 -0
- package/template/src/types/yousign.ts +52 -0
- package/template/vercel.json +12 -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/migration_lock.toml +0 -3
- package/template/src/app/(dashboard)/users/layout.tsx +0 -30
- package/template/src/app/api/dashboard/widgets/[id]/route.ts +0 -47
- package/template/src/app/api/dashboard/widgets/route.ts +0 -181
- package/template/src/components/dashboard/add-widget-dialog.tsx +0 -161
- package/template/src/components/dashboard/color-picker.tsx +0 -65
- package/template/src/components/dashboard/contacts-chart.tsx +0 -69
- package/template/src/components/dashboard/interactions-by-type-chart.tsx +0 -121
- package/template/src/components/dashboard/recent-activity.tsx +0 -157
- package/template/src/components/dashboard/sales-analytics-chart.tsx +0 -77
- package/template/src/components/dashboard/status-distribution-chart.tsx +0 -82
- package/template/src/components/dashboard/top-contacts-list.tsx +0 -119
- package/template/src/components/dashboard/widget-wrapper.tsx +0 -39
- package/template/src/contexts/dashboard-theme-context.tsx +0 -58
- package/template/src/lib/dashboard-themes.ts +0 -140
- package/template/src/lib/default-widgets.ts +0 -14
- package/template/src/lib/widget-registry.ts +0 -177
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server';
|
|
2
2
|
import { auth } from '@/lib/auth';
|
|
3
3
|
import { prisma } from '@/lib/prisma';
|
|
4
|
+
import { checkPermission } from '@/lib/check-permission';
|
|
4
5
|
import { logStatusChange, logContactUpdate, logAssignmentChange } from '@/lib/contact-interactions';
|
|
5
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
executeWorkflowsOnStatusChanged,
|
|
8
|
+
executeWorkflowsOnAssignmentChanged,
|
|
9
|
+
} from '@/lib/workflow-executor';
|
|
6
10
|
import { normalizePhoneNumber } from '@/lib/utils';
|
|
7
11
|
|
|
8
12
|
// GET /api/contacts/[id] - Récupérer un contact spécifique avec ses interactions
|
|
@@ -16,18 +20,22 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
|
|
16
20
|
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
|
|
17
21
|
}
|
|
18
22
|
|
|
23
|
+
const [canViewAll, canViewOwn] = await Promise.all([
|
|
24
|
+
checkPermission('contacts.view_all'),
|
|
25
|
+
checkPermission('contacts.view_own'),
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
if (!canViewAll && !canViewOwn) {
|
|
29
|
+
return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
|
|
30
|
+
}
|
|
31
|
+
|
|
19
32
|
const { id } = await params;
|
|
20
33
|
|
|
21
34
|
const contact = await prisma.contact.findUnique({
|
|
22
35
|
where: { id },
|
|
23
36
|
include: {
|
|
24
37
|
status: true,
|
|
25
|
-
|
|
26
|
-
select: { id: true, firstName: true, lastName: true, isCompany: true },
|
|
27
|
-
},
|
|
28
|
-
contacts: {
|
|
29
|
-
select: { id: true, firstName: true, lastName: true, isCompany: true },
|
|
30
|
-
},
|
|
38
|
+
company: { select: { id: true, name: true, phone: true, email: true } },
|
|
31
39
|
assignedCommercial: {
|
|
32
40
|
select: { id: true, name: true, email: true },
|
|
33
41
|
},
|
|
@@ -37,6 +45,27 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
|
|
37
45
|
createdBy: {
|
|
38
46
|
select: { id: true, name: true, email: true },
|
|
39
47
|
},
|
|
48
|
+
tourLinks: {
|
|
49
|
+
include: {
|
|
50
|
+
tour: {
|
|
51
|
+
select: {
|
|
52
|
+
id: true,
|
|
53
|
+
number: true,
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
transactions: {
|
|
59
|
+
select: {
|
|
60
|
+
id: true,
|
|
61
|
+
status: true,
|
|
62
|
+
totalAmountCents: true,
|
|
63
|
+
signedAt: true,
|
|
64
|
+
createdAt: true,
|
|
65
|
+
updatedAt: true,
|
|
66
|
+
},
|
|
67
|
+
orderBy: { createdAt: 'desc' },
|
|
68
|
+
},
|
|
40
69
|
interactions: {
|
|
41
70
|
include: {
|
|
42
71
|
user: {
|
|
@@ -60,6 +89,19 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
|
|
60
89
|
return NextResponse.json({ error: 'Contact non trouvé' }, { status: 404 });
|
|
61
90
|
}
|
|
62
91
|
|
|
92
|
+
// Vérifier l'accès si l'utilisateur ne peut voir que ses propres contacts
|
|
93
|
+
if (!canViewAll && canViewOwn) {
|
|
94
|
+
const isOwner =
|
|
95
|
+
contact.assignedCommercialId === session.user.id ||
|
|
96
|
+
contact.assignedTeleproId === session.user.id ||
|
|
97
|
+
contact.createdById === session.user.id;
|
|
98
|
+
const isUnassigned = !contact.assignedCommercialId && !contact.assignedTeleproId;
|
|
99
|
+
const canViewUnassigned = await checkPermission('contacts.view_unassigned');
|
|
100
|
+
if (!isOwner && !(isUnassigned && canViewUnassigned)) {
|
|
101
|
+
return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
63
105
|
return NextResponse.json(contact);
|
|
64
106
|
} catch (error: any) {
|
|
65
107
|
console.error('Erreur lors de la récupération du contact:', error);
|
|
@@ -78,6 +120,15 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
|
|
78
120
|
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
|
|
79
121
|
}
|
|
80
122
|
|
|
123
|
+
const [canEditAll, canEditOwn] = await Promise.all([
|
|
124
|
+
checkPermission('contacts.edit_all'),
|
|
125
|
+
checkPermission('contacts.edit_own'),
|
|
126
|
+
]);
|
|
127
|
+
|
|
128
|
+
if (!canEditAll && !canEditOwn) {
|
|
129
|
+
return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
|
|
130
|
+
}
|
|
131
|
+
|
|
81
132
|
const { id } = await params;
|
|
82
133
|
const body = await request.json();
|
|
83
134
|
const {
|
|
@@ -91,9 +142,8 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
|
|
91
142
|
city,
|
|
92
143
|
postalCode,
|
|
93
144
|
origin,
|
|
94
|
-
companyName,
|
|
95
|
-
isCompany,
|
|
96
145
|
companyId,
|
|
146
|
+
jobTitle,
|
|
97
147
|
statusId,
|
|
98
148
|
closingReason,
|
|
99
149
|
assignedCommercialId,
|
|
@@ -105,9 +155,7 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
|
|
105
155
|
where: { id },
|
|
106
156
|
include: {
|
|
107
157
|
status: true,
|
|
108
|
-
|
|
109
|
-
select: { id: true, firstName: true, lastName: true, isCompany: true },
|
|
110
|
-
},
|
|
158
|
+
company: { select: { id: true, name: true, phone: true, email: true } },
|
|
111
159
|
assignedCommercial: {
|
|
112
160
|
select: { id: true, name: true, email: true },
|
|
113
161
|
},
|
|
@@ -121,6 +169,17 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
|
|
121
169
|
return NextResponse.json({ error: 'Contact non trouvé' }, { status: 404 });
|
|
122
170
|
}
|
|
123
171
|
|
|
172
|
+
// Vérifier la propriété si l'utilisateur ne peut modifier que ses contacts
|
|
173
|
+
if (!canEditAll && canEditOwn) {
|
|
174
|
+
const isOwner =
|
|
175
|
+
existing.assignedCommercialId === session.user.id ||
|
|
176
|
+
existing.assignedTeleproId === session.user.id ||
|
|
177
|
+
existing.createdById === session.user.id;
|
|
178
|
+
if (!isOwner) {
|
|
179
|
+
return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
124
183
|
// Validation : si phone est fourni, il ne peut pas être vide
|
|
125
184
|
if (phone !== undefined && !phone) {
|
|
126
185
|
return NextResponse.json({ error: 'Le téléphone ne peut pas être vide' }, { status: 400 });
|
|
@@ -141,9 +200,8 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
|
|
141
200
|
city: city !== undefined ? city || null : existing.city,
|
|
142
201
|
postalCode: postalCode !== undefined ? postalCode || null : existing.postalCode,
|
|
143
202
|
origin: origin !== undefined ? origin || null : existing.origin,
|
|
144
|
-
companyName: companyName !== undefined ? companyName || null : existing.companyName,
|
|
145
|
-
isCompany: isCompany !== undefined ? isCompany === true : existing.isCompany,
|
|
146
203
|
companyId: companyId !== undefined ? companyId || null : existing.companyId,
|
|
204
|
+
jobTitle: jobTitle !== undefined ? jobTitle || null : existing.jobTitle,
|
|
147
205
|
statusId: statusId !== undefined ? statusId || null : existing.statusId,
|
|
148
206
|
closingReason: closingReason !== undefined ? closingReason || null : existing.closingReason,
|
|
149
207
|
assignedCommercialId:
|
|
@@ -188,8 +246,8 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
|
|
188
246
|
if (origin !== undefined && origin !== existing.origin) {
|
|
189
247
|
changes.origin = { old: existing.origin, new: origin };
|
|
190
248
|
}
|
|
191
|
-
if (
|
|
192
|
-
changes.
|
|
249
|
+
if (jobTitle !== undefined && jobTitle !== existing.jobTitle) {
|
|
250
|
+
changes.jobTitle = { old: existing.jobTitle, new: jobTitle };
|
|
193
251
|
}
|
|
194
252
|
if (closingReason !== undefined && closingReason !== existing.closingReason) {
|
|
195
253
|
changes.closingReason = { old: existing.closingReason, new: closingReason };
|
|
@@ -272,6 +330,21 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
|
|
272
330
|
);
|
|
273
331
|
}
|
|
274
332
|
|
|
333
|
+
// Déclencher les workflows CONTACT_ASSIGNMENT_CHANGED si l'assignation a changé
|
|
334
|
+
const commercialChanged =
|
|
335
|
+
assignedCommercialId !== undefined &&
|
|
336
|
+
normalizedExistingCommercial !== normalizedNewCommercial;
|
|
337
|
+
const teleproChanged =
|
|
338
|
+
assignedTeleproId !== undefined && normalizedExistingTelepro !== normalizedNewTelepro;
|
|
339
|
+
|
|
340
|
+
if (commercialChanged || teleproChanged) {
|
|
341
|
+
try {
|
|
342
|
+
await executeWorkflowsOnAssignmentChanged(id);
|
|
343
|
+
} catch (workflowError) {
|
|
344
|
+
console.error("Erreur lors de l'exécution des workflows assignation:", workflowError);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
275
348
|
// Changements de champs de contact
|
|
276
349
|
if (Object.keys(changes).length > 0) {
|
|
277
350
|
await logContactUpdate(id, changes, session.user.id);
|
|
@@ -294,13 +367,21 @@ export async function DELETE(
|
|
|
294
367
|
{ params }: { params: Promise<{ id: string }> },
|
|
295
368
|
) {
|
|
296
369
|
try {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
370
|
+
const session = await auth.api.getSession({
|
|
371
|
+
headers: request.headers,
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
if (!session) {
|
|
375
|
+
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const canDelete = await checkPermission('contacts.delete');
|
|
379
|
+
if (!canDelete) {
|
|
380
|
+
return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
|
|
381
|
+
}
|
|
300
382
|
|
|
301
383
|
const { id } = await params;
|
|
302
384
|
|
|
303
|
-
// Vérifier que le contact existe
|
|
304
385
|
const existing = await prisma.contact.findUnique({
|
|
305
386
|
where: { id },
|
|
306
387
|
});
|
|
@@ -309,6 +390,16 @@ export async function DELETE(
|
|
|
309
390
|
return NextResponse.json({ error: 'Contact non trouvé' }, { status: 404 });
|
|
310
391
|
}
|
|
311
392
|
|
|
393
|
+
const transactionsCount = await prisma.transaction.count({
|
|
394
|
+
where: { contactId: id },
|
|
395
|
+
});
|
|
396
|
+
if (transactionsCount > 0) {
|
|
397
|
+
return NextResponse.json(
|
|
398
|
+
{ error: 'Impossible de supprimer un contact ayant des transactions' },
|
|
399
|
+
{ status: 400 },
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
|
|
312
403
|
await prisma.contact.delete({
|
|
313
404
|
where: { id },
|
|
314
405
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server';
|
|
2
2
|
import { auth } from '@/lib/auth';
|
|
3
3
|
import { prisma } from '@/lib/prisma';
|
|
4
|
+
import { checkPermission } from '@/lib/check-permission';
|
|
4
5
|
import nodemailer from 'nodemailer';
|
|
5
6
|
import { decrypt } from '@/lib/encryption';
|
|
6
7
|
|
|
@@ -25,6 +26,11 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
|
|
25
26
|
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
|
|
26
27
|
}
|
|
27
28
|
|
|
29
|
+
const canSendEmail = await checkPermission('contacts.send_email');
|
|
30
|
+
if (!canSendEmail) {
|
|
31
|
+
return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
|
|
32
|
+
}
|
|
33
|
+
|
|
28
34
|
const { id } = await params;
|
|
29
35
|
|
|
30
36
|
// Récupérer FormData
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { auth } from '@/lib/auth';
|
|
3
|
+
import { prisma } from '@/lib/prisma';
|
|
4
|
+
import { executeWorkflowManually } from '@/lib/workflow-executor';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* POST /api/contacts/[id]/workflows/run
|
|
8
|
+
* Déclenche manuellement un workflow sur un contact
|
|
9
|
+
* Body: { workflowId: string }
|
|
10
|
+
*/
|
|
11
|
+
export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
|
12
|
+
try {
|
|
13
|
+
const session = await auth.api.getSession({ headers: request.headers });
|
|
14
|
+
if (!session) {
|
|
15
|
+
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const { id: contactId } = await params;
|
|
19
|
+
const { workflowId } = await request.json();
|
|
20
|
+
|
|
21
|
+
if (!workflowId) {
|
|
22
|
+
return NextResponse.json({ error: 'workflowId est requis' }, { status: 400 });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const contact = await prisma.contact.findUnique({ where: { id: contactId } });
|
|
26
|
+
if (!contact) {
|
|
27
|
+
return NextResponse.json({ error: 'Contact introuvable' }, { status: 404 });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
await executeWorkflowManually(workflowId, contactId);
|
|
31
|
+
|
|
32
|
+
return NextResponse.json({ success: true, message: 'Workflow exécuté avec succès' });
|
|
33
|
+
} catch (error: any) {
|
|
34
|
+
console.error('Erreur lors du déclenchement manuel du workflow:', error);
|
|
35
|
+
return NextResponse.json({ error: error.message || 'Erreur serveur' }, { status: 500 });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* GET /api/contacts/[id]/workflows/run
|
|
41
|
+
* Liste les workflows MANUAL actifs disponibles
|
|
42
|
+
*/
|
|
43
|
+
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
|
44
|
+
try {
|
|
45
|
+
const session = await auth.api.getSession({ headers: request.headers });
|
|
46
|
+
if (!session) {
|
|
47
|
+
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const workflows = await prisma.workflow.findMany({
|
|
51
|
+
where: { active: true, triggerType: 'MANUAL' },
|
|
52
|
+
select: { id: true, name: true, description: true },
|
|
53
|
+
orderBy: { name: 'asc' },
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return NextResponse.json(workflows);
|
|
57
|
+
} catch (error: any) {
|
|
58
|
+
console.error('Erreur lors de la récupération des workflows manuels:', error);
|
|
59
|
+
return NextResponse.json({ error: error.message || 'Erreur serveur' }, { status: 500 });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server';
|
|
2
2
|
import { auth } from '@/lib/auth';
|
|
3
3
|
import { prisma } from '@/lib/prisma';
|
|
4
|
+
import { checkPermission } from '@/lib/check-permission';
|
|
4
5
|
import { getFileInfo } from '@/lib/google-drive';
|
|
5
6
|
|
|
6
7
|
// POST /api/contacts/export - Exporter des contacts en CSV ou Excel
|
|
@@ -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,6 +64,7 @@ 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: {
|
|
@@ -82,9 +75,11 @@ export async function POST(request: NextRequest) {
|
|
|
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) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server';
|
|
2
2
|
import { auth } from '@/lib/auth';
|
|
3
3
|
import { prisma } from '@/lib/prisma';
|
|
4
|
+
import { checkPermission } from '@/lib/check-permission';
|
|
4
5
|
import { handleContactDuplicate } from '@/lib/contact-duplicate';
|
|
5
6
|
import { normalizePhoneNumber } from '@/lib/utils';
|
|
6
7
|
|
|
@@ -15,10 +16,18 @@ export async function POST(request: NextRequest) {
|
|
|
15
16
|
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
const canImport = await checkPermission('contacts.import');
|
|
20
|
+
if (!canImport) {
|
|
21
|
+
return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
|
|
22
|
+
}
|
|
23
|
+
|
|
18
24
|
const formData = await request.formData();
|
|
19
25
|
const file = formData.get('file') as File;
|
|
20
26
|
const fieldMappingsJson = formData.get('fieldMappings') as string;
|
|
21
27
|
const skipFirstRow = formData.get('skipFirstRow') === 'true';
|
|
28
|
+
const selectedSheetName = (formData.get('sheetName') as string) || undefined;
|
|
29
|
+
const headerRowStr = formData.get('headerRow') as string | null;
|
|
30
|
+
const headerRow = headerRowStr ? Number.parseInt(headerRowStr, 10) : 0;
|
|
22
31
|
|
|
23
32
|
// Récupérer les valeurs par défaut
|
|
24
33
|
const defaultStatusId = formData.get('defaultStatusId') as string | null;
|
|
@@ -64,14 +73,16 @@ export async function POST(request: NextRequest) {
|
|
|
64
73
|
const text = await file.text();
|
|
65
74
|
rows = parseCSV(text);
|
|
66
75
|
} else if (fileExtension === 'xlsx' || fileExtension === 'xls') {
|
|
67
|
-
// Parser Excel
|
|
68
76
|
try {
|
|
69
77
|
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
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { auth } from '@/lib/auth';
|
|
3
|
+
import { checkPermission } from '@/lib/check-permission';
|
|
4
|
+
|
|
5
|
+
function parseCsv(
|
|
6
|
+
text: string,
|
|
7
|
+
headerRow = 0,
|
|
8
|
+
): {
|
|
9
|
+
headers: string[];
|
|
10
|
+
preview: Record<string, string>[];
|
|
11
|
+
rawRows: string[][];
|
|
12
|
+
} | null {
|
|
13
|
+
const lines = text.split('\n').filter((line) => line.trim() !== '');
|
|
14
|
+
if (lines.length === 0) return null;
|
|
15
|
+
|
|
16
|
+
const delimiter = lines[0].includes(';') ? ';' : ',';
|
|
17
|
+
const strip = (s: string) => s.trim().replaceAll(/(^")|("$)/g, '');
|
|
18
|
+
|
|
19
|
+
const rawRows: string[][] = lines.slice(0, 20).map((line) => line.split(delimiter).map(strip));
|
|
20
|
+
|
|
21
|
+
if (headerRow >= lines.length) return null;
|
|
22
|
+
|
|
23
|
+
const headers = lines[headerRow].split(delimiter).map(strip);
|
|
24
|
+
|
|
25
|
+
const preview: Record<string, string>[] = [];
|
|
26
|
+
for (let i = headerRow + 1; i < Math.min(headerRow + 6, lines.length); i++) {
|
|
27
|
+
const values = lines[i].split(delimiter).map(strip);
|
|
28
|
+
const row: Record<string, string> = {};
|
|
29
|
+
headers.forEach((header, index) => {
|
|
30
|
+
row[header] = values[index] || '';
|
|
31
|
+
});
|
|
32
|
+
preview.push(row);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return { headers, preview, rawRows };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function parseExcel(
|
|
39
|
+
buffer: ArrayBuffer,
|
|
40
|
+
sheetName?: string,
|
|
41
|
+
headerRow = 0,
|
|
42
|
+
): {
|
|
43
|
+
sheetNames: string[];
|
|
44
|
+
headers: string[];
|
|
45
|
+
preview: Record<string, string>[];
|
|
46
|
+
rawRows: string[][];
|
|
47
|
+
} | null {
|
|
48
|
+
const XLSX = require('xlsx');
|
|
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
|
+
}
|