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,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
|
|
|
5
6
|
// PUT /api/contacts/[id]/interactions/[interactionId] - Mettre à jour une interaction
|
|
6
7
|
export async function PUT(
|
|
@@ -65,7 +66,32 @@ export async function DELETE(
|
|
|
65
66
|
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
|
|
66
67
|
}
|
|
67
68
|
|
|
68
|
-
const
|
|
69
|
+
const [canEditAll, canEditOwn] = await Promise.all([
|
|
70
|
+
checkPermission('contacts.edit_all'),
|
|
71
|
+
checkPermission('contacts.edit_own'),
|
|
72
|
+
]);
|
|
73
|
+
|
|
74
|
+
if (!canEditAll && !canEditOwn) {
|
|
75
|
+
return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const { id: contactId, interactionId } = await params;
|
|
79
|
+
|
|
80
|
+
// Vérifier la propriété si l'utilisateur ne peut modifier que ses contacts
|
|
81
|
+
if (!canEditAll && canEditOwn) {
|
|
82
|
+
const contact = await prisma.contact.findUnique({
|
|
83
|
+
where: { id: contactId },
|
|
84
|
+
});
|
|
85
|
+
if (contact) {
|
|
86
|
+
const isOwner =
|
|
87
|
+
contact.assignedCommercialId === session.user.id ||
|
|
88
|
+
contact.assignedTeleproId === session.user.id ||
|
|
89
|
+
contact.createdById === session.user.id;
|
|
90
|
+
if (!isOwner) {
|
|
91
|
+
return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
69
95
|
|
|
70
96
|
// Vérifier que l'interaction existe
|
|
71
97
|
const existing = await prisma.interaction.findUnique({
|
|
@@ -16,19 +16,19 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
|
|
16
16
|
|
|
17
17
|
const { id } = await params;
|
|
18
18
|
|
|
19
|
-
// Vérifier que le contact existe
|
|
20
|
-
const contact = await
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
// Vérifier que le contact existe et les permissions en parallèle
|
|
20
|
+
const [contact, canViewAll, canViewOwn] = await Promise.all([
|
|
21
|
+
prisma.contact.findUnique({
|
|
22
|
+
where: { id },
|
|
23
|
+
}),
|
|
24
|
+
checkPermission('contacts.view_all'),
|
|
25
|
+
checkPermission('contacts.view_own'),
|
|
26
|
+
]);
|
|
23
27
|
|
|
24
28
|
if (!contact) {
|
|
25
29
|
return NextResponse.json({ error: 'Contact non trouvé' }, { status: 404 });
|
|
26
30
|
}
|
|
27
31
|
|
|
28
|
-
// Vérifier les permissions
|
|
29
|
-
const canViewAll = await checkPermission('contacts.view_all');
|
|
30
|
-
const canViewOwn = await checkPermission('contacts.view_own');
|
|
31
|
-
|
|
32
32
|
if (!canViewAll && !canViewOwn) {
|
|
33
33
|
return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
|
|
34
34
|
}
|
|
@@ -88,19 +88,19 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
|
|
88
88
|
return NextResponse.json({ error: 'Le type et le contenu sont requis' }, { status: 400 });
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
// Vérifier que le contact existe
|
|
92
|
-
const contact = await
|
|
93
|
-
|
|
94
|
-
|
|
91
|
+
// Vérifier que le contact existe et les permissions d'édition en parallèle
|
|
92
|
+
const [contact, canEditAll, canEditOwn] = await Promise.all([
|
|
93
|
+
prisma.contact.findUnique({
|
|
94
|
+
where: { id },
|
|
95
|
+
}),
|
|
96
|
+
checkPermission('contacts.edit_all'),
|
|
97
|
+
checkPermission('contacts.edit_own'),
|
|
98
|
+
]);
|
|
95
99
|
|
|
96
100
|
if (!contact) {
|
|
97
101
|
return NextResponse.json({ error: 'Contact non trouvé' }, { status: 404 });
|
|
98
102
|
}
|
|
99
103
|
|
|
100
|
-
// Vérifier les permissions d'édition
|
|
101
|
-
const canEditAll = await checkPermission('contacts.edit_all');
|
|
102
|
-
const canEditOwn = await checkPermission('contacts.edit_own');
|
|
103
|
-
|
|
104
104
|
if (!canEditAll && !canEditOwn) {
|
|
105
105
|
return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
|
|
106
106
|
}
|
|
@@ -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
|
|
|
5
6
|
// PUT /api/contacts/[id]/kyc - Mettre à jour les informations KYC d'un contact
|
|
6
7
|
export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
|
@@ -13,6 +14,15 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
|
|
13
14
|
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
|
|
14
15
|
}
|
|
15
16
|
|
|
17
|
+
const [canEditAll, canEditOwn] = await Promise.all([
|
|
18
|
+
checkPermission('contacts.edit_all'),
|
|
19
|
+
checkPermission('contacts.edit_own'),
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
if (!canEditAll && !canEditOwn) {
|
|
23
|
+
return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
|
|
24
|
+
}
|
|
25
|
+
|
|
16
26
|
const { id } = await params;
|
|
17
27
|
const body = await request.json();
|
|
18
28
|
|
|
@@ -27,6 +37,17 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
|
|
27
37
|
return NextResponse.json({ error: 'Contact non trouvé' }, { status: 404 });
|
|
28
38
|
}
|
|
29
39
|
|
|
40
|
+
// Vérifier la propriété si l'utilisateur ne peut modifier que ses contacts
|
|
41
|
+
if (!canEditAll && canEditOwn) {
|
|
42
|
+
const isOwner =
|
|
43
|
+
contact.assignedCommercialId === session.user.id ||
|
|
44
|
+
contact.assignedTeleproId === session.user.id ||
|
|
45
|
+
contact.createdById === session.user.id;
|
|
46
|
+
if (!isOwner) {
|
|
47
|
+
return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
30
51
|
// Mettre à jour les informations KYC
|
|
31
52
|
const updatedContact = await prisma.contact.update({
|
|
32
53
|
where: { id },
|
|
@@ -52,17 +73,6 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
|
|
52
73
|
},
|
|
53
74
|
});
|
|
54
75
|
|
|
55
|
-
// Mettre à jour le statut des transactions en attente de vérification
|
|
56
|
-
await prisma.transaction.updateMany({
|
|
57
|
-
where: {
|
|
58
|
-
contactId: id,
|
|
59
|
-
status: 'PENDING_ID_VERIFICATION',
|
|
60
|
-
},
|
|
61
|
-
data: {
|
|
62
|
-
status: 'PENDING_ID_VERIFICATION', // Reste en attente jusqu'à vérification admin
|
|
63
|
-
},
|
|
64
|
-
});
|
|
65
|
-
|
|
66
76
|
return NextResponse.json(updatedContact);
|
|
67
77
|
} catch (error: any) {
|
|
68
78
|
console.error('Erreur lors de la mise à jour des informations KYC:', error);
|
|
@@ -1,10 +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 {
|
|
5
6
|
getValidAccessToken,
|
|
6
7
|
createGoogleCalendarEvent,
|
|
7
8
|
extractMeetLink,
|
|
9
|
+
assertWritableGoogleCalendar,
|
|
8
10
|
} from '@/lib/google-calendar';
|
|
9
11
|
import nodemailer from 'nodemailer';
|
|
10
12
|
import { decrypt, encrypt } from '@/lib/encryption';
|
|
@@ -36,6 +38,11 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
|
|
36
38
|
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
|
|
37
39
|
}
|
|
38
40
|
|
|
41
|
+
const canCreateTask = await checkPermission('tasks.create');
|
|
42
|
+
if (!canCreateTask) {
|
|
43
|
+
return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
|
|
44
|
+
}
|
|
45
|
+
|
|
39
46
|
const { id: contactId } = await params;
|
|
40
47
|
const body = await request.json();
|
|
41
48
|
const {
|
|
@@ -46,6 +53,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
|
|
46
53
|
attendees = [],
|
|
47
54
|
reminderMinutesBefore,
|
|
48
55
|
internalNote,
|
|
56
|
+
googleCalendarId: bodyGoogleCalendarId,
|
|
49
57
|
} = body;
|
|
50
58
|
|
|
51
59
|
// Validation
|
|
@@ -73,7 +81,8 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
|
|
73
81
|
{
|
|
74
82
|
error:
|
|
75
83
|
error.message ||
|
|
76
|
-
'Veuillez connecter votre compte Google dans les paramètres pour
|
|
84
|
+
'Veuillez connecter votre compte Google dans les paramètres pour créer une visioconférence.',
|
|
85
|
+
configLink: '/settings?section=integrations',
|
|
77
86
|
},
|
|
78
87
|
{ status: 400 },
|
|
79
88
|
);
|
|
@@ -115,10 +124,17 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
|
|
115
124
|
}
|
|
116
125
|
});
|
|
117
126
|
|
|
127
|
+
const targetCalendarId =
|
|
128
|
+
typeof bodyGoogleCalendarId === 'string' && bodyGoogleCalendarId.trim() !== ''
|
|
129
|
+
? bodyGoogleCalendarId.trim()
|
|
130
|
+
: googleAccount.defaultGoogleCalendarId?.trim() || 'primary';
|
|
131
|
+
|
|
132
|
+
await assertWritableGoogleCalendar(accessToken, targetCalendarId);
|
|
133
|
+
|
|
118
134
|
// Créer l'évènement Google Calendar avec Meet
|
|
119
135
|
let googleEvent;
|
|
120
136
|
try {
|
|
121
|
-
googleEvent = await createGoogleCalendarEvent(accessToken, {
|
|
137
|
+
googleEvent = await createGoogleCalendarEvent(accessToken, targetCalendarId, {
|
|
122
138
|
summary: title,
|
|
123
139
|
description: description || '',
|
|
124
140
|
start: {
|
|
@@ -159,6 +175,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
|
|
159
175
|
assignedUserId: session.user.id,
|
|
160
176
|
createdById: session.user.id,
|
|
161
177
|
googleEventId: googleEvent.id,
|
|
178
|
+
googleCalendarId: targetCalendarId === 'primary' ? null : targetCalendarId,
|
|
162
179
|
googleMeetLink: meetLink,
|
|
163
180
|
durationMinutes,
|
|
164
181
|
internalNote: internalNote || null,
|
|
@@ -45,27 +45,6 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
|
|
45
45
|
createdBy: {
|
|
46
46
|
select: { id: true, name: true, email: true },
|
|
47
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
|
-
},
|
|
69
48
|
interactions: {
|
|
70
49
|
include: {
|
|
71
50
|
user: {
|
|
@@ -102,7 +81,11 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
|
|
102
81
|
}
|
|
103
82
|
}
|
|
104
83
|
|
|
105
|
-
return NextResponse.json(contact
|
|
84
|
+
return NextResponse.json(contact, {
|
|
85
|
+
headers: {
|
|
86
|
+
'Cache-Control': 'private, no-store, max-age=0, must-revalidate',
|
|
87
|
+
},
|
|
88
|
+
});
|
|
106
89
|
} catch (error: any) {
|
|
107
90
|
console.error('Erreur lors de la récupération du contact:', error);
|
|
108
91
|
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
|
|
@@ -142,8 +125,11 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
|
|
142
125
|
city,
|
|
143
126
|
postalCode,
|
|
144
127
|
origin,
|
|
128
|
+
companyName,
|
|
145
129
|
companyId,
|
|
146
130
|
jobTitle,
|
|
131
|
+
website,
|
|
132
|
+
socialNetworks,
|
|
147
133
|
statusId,
|
|
148
134
|
closingReason,
|
|
149
135
|
assignedCommercialId,
|
|
@@ -180,6 +166,50 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
|
|
180
166
|
}
|
|
181
167
|
}
|
|
182
168
|
|
|
169
|
+
const canAssignFull = await checkPermission('contacts.assign');
|
|
170
|
+
const canAssignToSales = await checkPermission('contacts.assign_to_sales');
|
|
171
|
+
const isChangingCommercial =
|
|
172
|
+
assignedCommercialId !== undefined &&
|
|
173
|
+
(assignedCommercialId || null) !== (existing.assignedCommercialId || null);
|
|
174
|
+
const isChangingTelepro =
|
|
175
|
+
assignedTeleproId !== undefined &&
|
|
176
|
+
(assignedTeleproId || null) !== (existing.assignedTeleproId || null);
|
|
177
|
+
|
|
178
|
+
if (isChangingCommercial || isChangingTelepro) {
|
|
179
|
+
if (!canAssignFull && !canAssignToSales) {
|
|
180
|
+
return NextResponse.json(
|
|
181
|
+
{ error: "Vous n'avez pas le droit d'assigner des contacts" },
|
|
182
|
+
{ status: 403 },
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
if (canAssignToSales && !canAssignFull) {
|
|
186
|
+
if (isChangingCommercial && assignedCommercialId) {
|
|
187
|
+
const commercialUser = await prisma.user.findUnique({
|
|
188
|
+
where: { id: assignedCommercialId },
|
|
189
|
+
select: { role: true },
|
|
190
|
+
});
|
|
191
|
+
if (!commercialUser || commercialUser.role !== 'COMMERCIAL') {
|
|
192
|
+
return NextResponse.json(
|
|
193
|
+
{ error: "Vous ne pouvez assigner qu'à un commercial" },
|
|
194
|
+
{ status: 403 },
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (isChangingTelepro && assignedTeleproId) {
|
|
199
|
+
const teleproUser = await prisma.user.findUnique({
|
|
200
|
+
where: { id: assignedTeleproId },
|
|
201
|
+
select: { role: true },
|
|
202
|
+
});
|
|
203
|
+
if (!teleproUser || teleproUser.role !== 'TELEPRO') {
|
|
204
|
+
return NextResponse.json(
|
|
205
|
+
{ error: "Vous ne pouvez assigner qu'à un télépro" },
|
|
206
|
+
{ status: 403 },
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
183
213
|
// Validation : si phone est fourni, il ne peut pas être vide
|
|
184
214
|
if (phone !== undefined && !phone) {
|
|
185
215
|
return NextResponse.json({ error: 'Le téléphone ne peut pas être vide' }, { status: 400 });
|
|
@@ -200,8 +230,14 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
|
|
200
230
|
city: city !== undefined ? city || null : existing.city,
|
|
201
231
|
postalCode: postalCode !== undefined ? postalCode || null : existing.postalCode,
|
|
202
232
|
origin: origin !== undefined ? origin || null : existing.origin,
|
|
233
|
+
companyName: companyName !== undefined ? companyName || null : existing.companyName,
|
|
203
234
|
companyId: companyId !== undefined ? companyId || null : existing.companyId,
|
|
204
235
|
jobTitle: jobTitle !== undefined ? jobTitle || null : existing.jobTitle,
|
|
236
|
+
website: website !== undefined ? (website && website.trim() ? website : null) : existing.website,
|
|
237
|
+
socialNetworks:
|
|
238
|
+
socialNetworks !== undefined
|
|
239
|
+
? (Array.isArray(socialNetworks) && socialNetworks.length > 0 ? socialNetworks : null)
|
|
240
|
+
: existing.socialNetworks,
|
|
205
241
|
statusId: statusId !== undefined ? statusId || null : existing.statusId,
|
|
206
242
|
closingReason: closingReason !== undefined ? closingReason || null : existing.closingReason,
|
|
207
243
|
assignedCommercialId:
|
|
@@ -212,6 +248,47 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
|
|
212
248
|
assignedTeleproId !== undefined ? assignedTeleproId || null : existing.assignedTeleproId,
|
|
213
249
|
};
|
|
214
250
|
|
|
251
|
+
// Exclusivite mutuelle : companyName (texte libre) vs companyId (relation entreprise)
|
|
252
|
+
if (updateData.companyName) {
|
|
253
|
+
updateData.companyId = null;
|
|
254
|
+
} else if (updateData.companyId) {
|
|
255
|
+
updateData.companyName = null;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Prisma 7 : relations via connect/disconnect au lieu des scalaires (companyId, statusId, assignedCommercialId, assignedTeleproId)
|
|
259
|
+
const {
|
|
260
|
+
companyId: _c,
|
|
261
|
+
statusId: _s,
|
|
262
|
+
assignedCommercialId: _ac,
|
|
263
|
+
assignedTeleproId: _at,
|
|
264
|
+
...restUpdateData
|
|
265
|
+
} = updateData;
|
|
266
|
+
const dataForPrisma: Record<string, unknown> = { ...restUpdateData };
|
|
267
|
+
if (companyId !== undefined) {
|
|
268
|
+
dataForPrisma.company =
|
|
269
|
+
updateData.companyId != null && updateData.companyId !== ''
|
|
270
|
+
? { connect: { id: updateData.companyId } }
|
|
271
|
+
: { disconnect: true };
|
|
272
|
+
}
|
|
273
|
+
if (statusId !== undefined) {
|
|
274
|
+
dataForPrisma.status =
|
|
275
|
+
updateData.statusId != null && updateData.statusId !== ''
|
|
276
|
+
? { connect: { id: updateData.statusId } }
|
|
277
|
+
: { disconnect: true };
|
|
278
|
+
}
|
|
279
|
+
if (assignedCommercialId !== undefined) {
|
|
280
|
+
dataForPrisma.assignedCommercial =
|
|
281
|
+
updateData.assignedCommercialId != null && updateData.assignedCommercialId !== ''
|
|
282
|
+
? { connect: { id: updateData.assignedCommercialId } }
|
|
283
|
+
: { disconnect: true };
|
|
284
|
+
}
|
|
285
|
+
if (assignedTeleproId !== undefined) {
|
|
286
|
+
dataForPrisma.assignedTelepro =
|
|
287
|
+
updateData.assignedTeleproId != null && updateData.assignedTeleproId !== ''
|
|
288
|
+
? { connect: { id: updateData.assignedTeleproId } }
|
|
289
|
+
: { disconnect: true };
|
|
290
|
+
}
|
|
291
|
+
|
|
215
292
|
// Détecter les changements pour créer les interactions
|
|
216
293
|
const changes: Record<string, { old: any; new: any }> = {};
|
|
217
294
|
|
|
@@ -256,9 +333,10 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
|
|
256
333
|
// Mettre à jour le contact
|
|
257
334
|
const contact = await prisma.contact.update({
|
|
258
335
|
where: { id },
|
|
259
|
-
data:
|
|
336
|
+
data: dataForPrisma,
|
|
260
337
|
include: {
|
|
261
338
|
status: true,
|
|
339
|
+
company: { select: { id: true, name: true, phone: true, email: true } },
|
|
262
340
|
assignedCommercial: {
|
|
263
341
|
select: { id: true, name: true, email: true },
|
|
264
342
|
},
|
|
@@ -354,7 +432,11 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
|
|
354
432
|
console.error('Erreur lors de la création des interactions:', error);
|
|
355
433
|
}
|
|
356
434
|
|
|
357
|
-
return NextResponse.json(contact
|
|
435
|
+
return NextResponse.json(contact, {
|
|
436
|
+
headers: {
|
|
437
|
+
'Cache-Control': 'no-store',
|
|
438
|
+
},
|
|
439
|
+
});
|
|
358
440
|
} catch (error: any) {
|
|
359
441
|
console.error('Erreur lors de la mise à jour du contact:', error);
|
|
360
442
|
return NextResponse.json({ error: error.message || 'Erreur serveur' }, { status: 500 });
|
|
@@ -390,16 +472,6 @@ export async function DELETE(
|
|
|
390
472
|
return NextResponse.json({ error: 'Contact non trouvé' }, { status: 404 });
|
|
391
473
|
}
|
|
392
474
|
|
|
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
|
-
|
|
403
475
|
await prisma.contact.delete({
|
|
404
476
|
where: { id },
|
|
405
477
|
});
|
|
@@ -50,25 +50,26 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
|
|
50
50
|
);
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
// Récupérer le contact
|
|
54
|
-
const contact = await
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
// Récupérer le contact et la configuration SMTP en parallèle
|
|
54
|
+
const [contact, smtpConfig] = await Promise.all([
|
|
55
|
+
prisma.contact.findUnique({
|
|
56
|
+
where: { id },
|
|
57
|
+
}),
|
|
58
|
+
prisma.smtpConfig.findUnique({
|
|
59
|
+
where: { userId: session.user.id },
|
|
60
|
+
}),
|
|
61
|
+
]);
|
|
57
62
|
|
|
58
63
|
if (!contact) {
|
|
59
64
|
return NextResponse.json({ error: 'Contact non trouvé' }, { status: 404 });
|
|
60
65
|
}
|
|
61
66
|
|
|
62
|
-
// Récupérer la configuration SMTP de l'utilisateur
|
|
63
|
-
const smtpConfig = await prisma.smtpConfig.findUnique({
|
|
64
|
-
where: { userId: session.user.id },
|
|
65
|
-
});
|
|
66
|
-
|
|
67
67
|
if (!smtpConfig) {
|
|
68
68
|
return NextResponse.json(
|
|
69
69
|
{
|
|
70
70
|
error:
|
|
71
|
-
'Configuration SMTP non trouvée. Veuillez configurer votre SMTP dans les paramètres.',
|
|
71
|
+
'Configuration SMTP non trouvée. Veuillez configurer votre SMTP dans les paramètres pour envoyer des emails.',
|
|
72
|
+
configLink: '/settings?section=system',
|
|
72
73
|
},
|
|
73
74
|
{ status: 400 },
|
|
74
75
|
);
|
|
@@ -245,9 +246,24 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
|
|
245
246
|
console.error("Erreur lors de l'envoi de l'email:", error);
|
|
246
247
|
|
|
247
248
|
// Gérer les erreurs spécifiques de nodemailer
|
|
249
|
+
if (error.code === 'EDNS' || error.syscall === 'getaddrinfo') {
|
|
250
|
+
return NextResponse.json(
|
|
251
|
+
{
|
|
252
|
+
error:
|
|
253
|
+
`Impossible de résoudre le serveur SMTP "${error.hostname || ''}". Vérifiez le nom d'hôte dans votre configuration SMTP (ex: smtp.ionos.fr).`,
|
|
254
|
+
configLink: '/settings?section=system',
|
|
255
|
+
},
|
|
256
|
+
{ status: 400 },
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
248
260
|
if (error.code === 'EAUTH' || error.code === 'ECONNECTION') {
|
|
249
261
|
return NextResponse.json(
|
|
250
|
-
{
|
|
262
|
+
{
|
|
263
|
+
error:
|
|
264
|
+
"Erreur d'authentification SMTP. Veuillez vérifier votre configuration SMTP pour envoyer des emails.",
|
|
265
|
+
configLink: '/settings?section=system',
|
|
266
|
+
},
|
|
251
267
|
{ status: 400 },
|
|
252
268
|
);
|
|
253
269
|
}
|
|
@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
|
|
2
2
|
import { auth } from '@/lib/auth';
|
|
3
3
|
import { prisma } from '@/lib/prisma';
|
|
4
4
|
import { executeWorkflowManually } from '@/lib/workflow-executor';
|
|
5
|
+
import { checkPermission } from '@/lib/check-permission';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* POST /api/contacts/[id]/workflows/run
|
|
@@ -15,6 +16,11 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
|
|
15
16
|
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
const canViewWorkflows = await checkPermission('workflows.view');
|
|
20
|
+
if (!canViewWorkflows) {
|
|
21
|
+
return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
|
|
22
|
+
}
|
|
23
|
+
|
|
18
24
|
const { id: contactId } = await params;
|
|
19
25
|
const { workflowId } = await request.json();
|
|
20
26
|
|
|
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
|
|
2
2
|
import { auth } from '@/lib/auth';
|
|
3
3
|
import { prisma } from '@/lib/prisma';
|
|
4
4
|
import { checkPermission } from '@/lib/check-permission';
|
|
5
|
-
import {
|
|
5
|
+
import { BUCKETS, createSignedDownloadUrl } from '@/lib/supabase-storage';
|
|
6
6
|
|
|
7
7
|
// POST /api/contacts/export - Exporter des contacts en CSV ou Excel
|
|
8
8
|
export async function POST(request: NextRequest) {
|
|
@@ -69,7 +69,7 @@ export async function POST(request: NextRequest) {
|
|
|
69
69
|
files: {
|
|
70
70
|
select: {
|
|
71
71
|
fileName: true,
|
|
72
|
-
|
|
72
|
+
storagePath: true,
|
|
73
73
|
fileSize: true,
|
|
74
74
|
mimeType: true,
|
|
75
75
|
createdAt: true,
|
|
@@ -133,21 +133,17 @@ export async function POST(request: NextRequest) {
|
|
|
133
133
|
.join('\n\n');
|
|
134
134
|
};
|
|
135
135
|
|
|
136
|
-
|
|
137
|
-
const formatFiles = async (files: any[], userId: string) => {
|
|
136
|
+
const formatFiles = async (files: any[]) => {
|
|
138
137
|
if (!files || files.length === 0) return '';
|
|
139
138
|
|
|
140
139
|
const fileInfos = await Promise.allSettled(
|
|
141
140
|
files.map(async (file) => {
|
|
141
|
+
const sizeKB = (file.fileSize / 1024).toFixed(2);
|
|
142
142
|
try {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
return `${file.fileName} (${sizeKB} KB)
|
|
147
|
-
} catch (error) {
|
|
148
|
-
// Si échec, utiliser un lien basique
|
|
149
|
-
const sizeKB = (file.fileSize / 1024).toFixed(2);
|
|
150
|
-
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)`;
|
|
151
147
|
}
|
|
152
148
|
}),
|
|
153
149
|
);
|
|
@@ -163,7 +159,7 @@ export async function POST(request: NextRequest) {
|
|
|
163
159
|
const rows = await Promise.all(
|
|
164
160
|
contacts.map(async (contact) => {
|
|
165
161
|
const notes = formatNotes(contact.interactions || []);
|
|
166
|
-
const files = await formatFiles(contact.files || []
|
|
162
|
+
const files = await formatFiles(contact.files || []);
|
|
167
163
|
|
|
168
164
|
return [
|
|
169
165
|
contact.civility || '',
|