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
|
@@ -50,6 +50,44 @@ export async function PUT(request: NextRequest) {
|
|
|
50
50
|
const body = await request.json();
|
|
51
51
|
const { host, port, secure, username, password, fromEmail, fromName, signature } = body;
|
|
52
52
|
|
|
53
|
+
const smtpFieldKeys = ['host', 'port', 'secure', 'username', 'password', 'fromEmail', 'fromName'];
|
|
54
|
+
const hasSmtpFields = smtpFieldKeys.some((key) => Object.hasOwn(body, key));
|
|
55
|
+
const hasSignatureField = Object.hasOwn(body, 'signature');
|
|
56
|
+
|
|
57
|
+
// Mise à jour partielle: signature seule (sans revalidation SMTP)
|
|
58
|
+
if (hasSignatureField && !hasSmtpFields) {
|
|
59
|
+
const existingConfig = await prisma.smtpConfig.findUnique({
|
|
60
|
+
where: { userId: session.user.id },
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
if (!existingConfig) {
|
|
64
|
+
return NextResponse.json(
|
|
65
|
+
{ error: "Aucune configuration SMTP existante pour enregistrer la signature" },
|
|
66
|
+
{ status: 400 },
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const updatedConfig = await prisma.smtpConfig.update({
|
|
71
|
+
where: { userId: session.user.id },
|
|
72
|
+
data: { signature: signature || null },
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return NextResponse.json({
|
|
76
|
+
success: true,
|
|
77
|
+
config: {
|
|
78
|
+
id: updatedConfig.id,
|
|
79
|
+
host: updatedConfig.host,
|
|
80
|
+
port: updatedConfig.port,
|
|
81
|
+
secure: updatedConfig.secure,
|
|
82
|
+
username: updatedConfig.username,
|
|
83
|
+
fromEmail: updatedConfig.fromEmail,
|
|
84
|
+
fromName: updatedConfig.fromName,
|
|
85
|
+
signature: updatedConfig.signature,
|
|
86
|
+
},
|
|
87
|
+
message: 'Signature sauvegardée avec succès',
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
53
91
|
// Validation
|
|
54
92
|
if (!host || !port || !username || !password || !fromEmail) {
|
|
55
93
|
return NextResponse.json(
|
|
@@ -58,6 +96,15 @@ export async function PUT(request: NextRequest) {
|
|
|
58
96
|
);
|
|
59
97
|
}
|
|
60
98
|
|
|
99
|
+
const hostTrimmed = host.trim().toLowerCase();
|
|
100
|
+
const validHostnameRegex = /^([a-z0-9]([a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}$/;
|
|
101
|
+
if (!validHostnameRegex.test(hostTrimmed)) {
|
|
102
|
+
return NextResponse.json(
|
|
103
|
+
{ error: 'Le nom d\'hôte SMTP est invalide. Vérifiez qu\'il contient un domaine complet (ex: smtp.ionos.fr)' },
|
|
104
|
+
{ status: 400 },
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
61
108
|
if (port < 1 || port > 65535) {
|
|
62
109
|
return NextResponse.json({ error: 'Le port doit être entre 1 et 65535' }, { status: 400 });
|
|
63
110
|
}
|
|
@@ -80,22 +127,22 @@ export async function PUT(request: NextRequest) {
|
|
|
80
127
|
smtpConfig = await prisma.smtpConfig.upsert({
|
|
81
128
|
where: { userId: session.user.id },
|
|
82
129
|
update: {
|
|
83
|
-
host,
|
|
84
|
-
port: parseInt(port),
|
|
130
|
+
host: hostTrimmed,
|
|
131
|
+
port: Number.parseInt(port, 10),
|
|
85
132
|
secure: secure === true || secure === 'true',
|
|
86
133
|
username,
|
|
87
|
-
password: encryptedPassword,
|
|
134
|
+
password: encryptedPassword,
|
|
88
135
|
fromEmail,
|
|
89
136
|
fromName: fromName || null,
|
|
90
137
|
signature: signature || null,
|
|
91
138
|
},
|
|
92
139
|
create: {
|
|
93
140
|
userId: session.user.id,
|
|
94
|
-
host,
|
|
95
|
-
port: parseInt(port),
|
|
141
|
+
host: hostTrimmed,
|
|
142
|
+
port: Number.parseInt(port, 10),
|
|
96
143
|
secure: secure === true || secure === 'true',
|
|
97
144
|
username,
|
|
98
|
-
password: encryptedPassword,
|
|
145
|
+
password: encryptedPassword,
|
|
99
146
|
fromEmail,
|
|
100
147
|
fromName: fromName || null,
|
|
101
148
|
signature: signature || null,
|
|
@@ -1,7 +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 {
|
|
4
|
+
import { checkPermission } from '@/lib/check-permission';
|
|
5
|
+
import {
|
|
6
|
+
getValidAccessToken,
|
|
7
|
+
getGoogleCalendarEvent,
|
|
8
|
+
resolveTaskGoogleCalendarId,
|
|
9
|
+
} from '@/lib/google-calendar';
|
|
5
10
|
|
|
6
11
|
// GET /api/tasks/[id]/attendees - Récupérer les invités d'un Google Meet
|
|
7
12
|
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
|
@@ -21,6 +26,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
|
|
21
26
|
select: {
|
|
22
27
|
id: true,
|
|
23
28
|
googleEventId: true,
|
|
29
|
+
googleCalendarId: true,
|
|
24
30
|
contactId: true,
|
|
25
31
|
contact: {
|
|
26
32
|
select: {
|
|
@@ -36,12 +42,17 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
|
|
36
42
|
}
|
|
37
43
|
|
|
38
44
|
// Vérifier les permissions
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
45
|
+
const [canEditOwn, canEditAll] = await Promise.all([
|
|
46
|
+
checkPermission('tasks.edit_own'),
|
|
47
|
+
checkPermission('tasks.edit_all'),
|
|
48
|
+
]);
|
|
43
49
|
|
|
44
|
-
if (
|
|
50
|
+
if (!canEditAll && !canEditOwn) {
|
|
51
|
+
return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Vérifier la propriété si l'utilisateur ne peut modifier que ses tâches
|
|
55
|
+
if (!canEditAll && canEditOwn && task.assignedUserId !== session.user.id) {
|
|
45
56
|
return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
|
|
46
57
|
}
|
|
47
58
|
|
|
@@ -55,7 +66,8 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
|
|
55
66
|
{
|
|
56
67
|
error:
|
|
57
68
|
error.message ||
|
|
58
|
-
'Veuillez connecter votre compte Google dans les paramètres pour
|
|
69
|
+
'Veuillez connecter votre compte Google dans les paramètres pour gérer les invités du Meet.',
|
|
70
|
+
configLink: '/settings?section=integrations',
|
|
59
71
|
},
|
|
60
72
|
{ status: 400 },
|
|
61
73
|
);
|
|
@@ -69,7 +81,11 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
|
|
69
81
|
);
|
|
70
82
|
|
|
71
83
|
// Récupérer l'événement Google Calendar
|
|
72
|
-
const googleEvent = await getGoogleCalendarEvent(
|
|
84
|
+
const googleEvent = await getGoogleCalendarEvent(
|
|
85
|
+
accessToken,
|
|
86
|
+
resolveTaskGoogleCalendarId(task.googleCalendarId),
|
|
87
|
+
task.googleEventId,
|
|
88
|
+
);
|
|
73
89
|
|
|
74
90
|
// Extraire les emails des invités (inclure tous les invités, y compris le contact)
|
|
75
91
|
const attendees =
|
|
@@ -4,15 +4,23 @@ import { prisma } from '@/lib/prisma';
|
|
|
4
4
|
import { checkPermission } from '@/lib/check-permission';
|
|
5
5
|
import { executeWorkflowsOnTaskCompleted } from '@/lib/workflow-executor';
|
|
6
6
|
import {
|
|
7
|
+
appendGoogleCalendarContactFooter,
|
|
8
|
+
createGoogleCalendarEvent,
|
|
7
9
|
getValidAccessToken,
|
|
8
10
|
updateGoogleCalendarEvent,
|
|
9
11
|
extractMeetLink,
|
|
10
12
|
deleteGoogleCalendarEvent,
|
|
11
13
|
getGoogleCalendarEvent,
|
|
14
|
+
resolveTaskGoogleCalendarId,
|
|
12
15
|
} from '@/lib/google-calendar';
|
|
13
16
|
import nodemailer from 'nodemailer';
|
|
14
17
|
import { decrypt, encrypt } from '@/lib/encryption';
|
|
15
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
createInteraction,
|
|
20
|
+
logAppointmentCancelled,
|
|
21
|
+
logAppointmentChanged,
|
|
22
|
+
deleteInteractionsLinkedToTask,
|
|
23
|
+
} from '@/lib/contact-interactions';
|
|
16
24
|
import { render } from '@react-email/render';
|
|
17
25
|
import { MeetUpdateEmailTemplate } from '@/components/meet-update-email-template';
|
|
18
26
|
import { MeetCancellationEmailTemplate } from '@/components/meet-cancellation-email-template';
|
|
@@ -135,6 +143,7 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
|
|
135
143
|
locationCity,
|
|
136
144
|
locationPostalCode,
|
|
137
145
|
isAtHome,
|
|
146
|
+
googleCalendarId: bodyGoogleCalendarId,
|
|
138
147
|
} = body;
|
|
139
148
|
|
|
140
149
|
// Vérifier que la tâche existe
|
|
@@ -182,9 +191,30 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
|
|
182
191
|
if (locationPostalCode !== undefined)
|
|
183
192
|
updateData.locationPostalCode = locationPostalCode || null;
|
|
184
193
|
if (isAtHome !== undefined) updateData.isAtHome = isAtHome === true;
|
|
194
|
+
if (bodyGoogleCalendarId !== undefined) {
|
|
195
|
+
const normalizedCalendarId =
|
|
196
|
+
typeof bodyGoogleCalendarId === 'string' && bodyGoogleCalendarId.trim() !== ''
|
|
197
|
+
? bodyGoogleCalendarId.trim()
|
|
198
|
+
: null;
|
|
199
|
+
updateData.googleCalendarId =
|
|
200
|
+
normalizedCalendarId && normalizedCalendarId !== 'primary' ? normalizedCalendarId : null;
|
|
201
|
+
}
|
|
185
202
|
|
|
186
|
-
const
|
|
187
|
-
|
|
203
|
+
const canAssignFull = await checkPermission('tasks.assign');
|
|
204
|
+
const canAssignToSales = await checkPermission('tasks.assign_to_sales');
|
|
205
|
+
if (assignedUserId !== undefined && (canAssignFull || canAssignToSales)) {
|
|
206
|
+
if (canAssignToSales && !canAssignFull && assignedUserId) {
|
|
207
|
+
const target = await prisma.user.findUnique({
|
|
208
|
+
where: { id: assignedUserId },
|
|
209
|
+
select: { role: true },
|
|
210
|
+
});
|
|
211
|
+
if (!target || (target.role !== 'COMMERCIAL' && target.role !== 'TELEPRO')) {
|
|
212
|
+
return NextResponse.json(
|
|
213
|
+
{ error: "Vous ne pouvez assigner une tâche qu'à un commercial ou un télépro" },
|
|
214
|
+
{ status: 403 },
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
188
218
|
updateData.assignedUserId = assignedUserId;
|
|
189
219
|
}
|
|
190
220
|
|
|
@@ -216,7 +246,28 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
|
|
216
246
|
// Préparer les données de mise à jour pour Google Calendar
|
|
217
247
|
const googleUpdate: any = {};
|
|
218
248
|
if (title !== undefined) googleUpdate.summary = title;
|
|
219
|
-
if (description !== undefined)
|
|
249
|
+
if (description !== undefined) {
|
|
250
|
+
if (existingTask.type === 'TASK') {
|
|
251
|
+
let descPlain = htmlToText(description);
|
|
252
|
+
if (existingTask.contactId) {
|
|
253
|
+
const contactForFooter = await prisma.contact.findUnique({
|
|
254
|
+
where: { id: existingTask.contactId },
|
|
255
|
+
select: { firstName: true, lastName: true, email: true },
|
|
256
|
+
});
|
|
257
|
+
const footerEmail = contactForFooter?.email;
|
|
258
|
+
if (footerEmail) {
|
|
259
|
+
descPlain = appendGoogleCalendarContactFooter(descPlain, {
|
|
260
|
+
firstName: contactForFooter.firstName,
|
|
261
|
+
lastName: contactForFooter.lastName,
|
|
262
|
+
email: footerEmail,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
googleUpdate.description = descPlain;
|
|
267
|
+
} else {
|
|
268
|
+
googleUpdate.description = description;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
220
271
|
if (location !== undefined) googleUpdate.location = location || undefined;
|
|
221
272
|
|
|
222
273
|
if (scheduledAt !== undefined) {
|
|
@@ -234,9 +285,17 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
|
|
234
285
|
};
|
|
235
286
|
}
|
|
236
287
|
|
|
288
|
+
const requestedGoogleCalendarId =
|
|
289
|
+
typeof bodyGoogleCalendarId === 'string' && bodyGoogleCalendarId.trim() !== ''
|
|
290
|
+
? bodyGoogleCalendarId.trim()
|
|
291
|
+
: resolveTaskGoogleCalendarId(existingTask.googleCalendarId);
|
|
292
|
+
const gCalId = resolveTaskGoogleCalendarId(existingTask.googleCalendarId);
|
|
293
|
+
const targetGCalId = resolveTaskGoogleCalendarId(
|
|
294
|
+
requestedGoogleCalendarId === 'primary' ? null : requestedGoogleCalendarId,
|
|
295
|
+
);
|
|
296
|
+
|
|
237
297
|
// Mettre à jour les invités si fournis
|
|
238
298
|
if (attendees !== undefined && Array.isArray(attendees)) {
|
|
239
|
-
// Récupérer le contact pour l'inclure dans la liste
|
|
240
299
|
const contact = existingTask.contactId
|
|
241
300
|
? await prisma.contact.findUnique({
|
|
242
301
|
where: { id: existingTask.contactId },
|
|
@@ -244,12 +303,10 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
|
|
244
303
|
})
|
|
245
304
|
: null;
|
|
246
305
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
if (contact?.email) {
|
|
306
|
+
const allAttendees: Array<{ email: string }> = [];
|
|
307
|
+
if (existingTask.type !== 'TASK' && contact?.email) {
|
|
250
308
|
allAttendees.push({ email: contact.email });
|
|
251
309
|
}
|
|
252
|
-
// Ajouter les autres invités (exclure le contact s'il est déjà dans la liste)
|
|
253
310
|
attendees.forEach((email: string) => {
|
|
254
311
|
if (email && email.trim() !== '' && email !== contact?.email) {
|
|
255
312
|
allAttendees.push({ email: email.trim() });
|
|
@@ -257,14 +314,73 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
|
|
257
314
|
});
|
|
258
315
|
|
|
259
316
|
googleUpdate.attendees = allAttendees.length > 0 ? allAttendees : undefined;
|
|
317
|
+
} else if (
|
|
318
|
+
existingTask.type === 'TASK' &&
|
|
319
|
+
existingTask.contactId &&
|
|
320
|
+
attendees === undefined
|
|
321
|
+
) {
|
|
322
|
+
const contact = await prisma.contact.findUnique({
|
|
323
|
+
where: { id: existingTask.contactId },
|
|
324
|
+
select: { email: true },
|
|
325
|
+
});
|
|
326
|
+
if (contact?.email) {
|
|
327
|
+
try {
|
|
328
|
+
const ge = await getGoogleCalendarEvent(
|
|
329
|
+
accessToken,
|
|
330
|
+
gCalId,
|
|
331
|
+
existingTask.googleEventId,
|
|
332
|
+
);
|
|
333
|
+
const contactLower = contact.email.toLowerCase();
|
|
334
|
+
const hadContact = ge.attendees?.some(
|
|
335
|
+
(a) => a.email?.toLowerCase() === contactLower,
|
|
336
|
+
);
|
|
337
|
+
if (hadContact) {
|
|
338
|
+
const filtered = (ge.attendees || [])
|
|
339
|
+
.filter(
|
|
340
|
+
(a) =>
|
|
341
|
+
a.email && a.email.toLowerCase() !== contactLower,
|
|
342
|
+
)
|
|
343
|
+
.map((a) => ({ email: a.email! }));
|
|
344
|
+
googleUpdate.attendees = filtered.length > 0 ? filtered : [];
|
|
345
|
+
}
|
|
346
|
+
} catch (stripErr) {
|
|
347
|
+
console.error(
|
|
348
|
+
'Impossible de retirer le contact des invités Google Calendar:',
|
|
349
|
+
stripErr,
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
260
353
|
}
|
|
261
354
|
|
|
262
355
|
// Mettre à jour l'évènement Google Calendar
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
356
|
+
const calendarChanged = targetGCalId !== gCalId;
|
|
357
|
+
let updatedGoogleEvent;
|
|
358
|
+
if (calendarChanged) {
|
|
359
|
+
const currentGoogleEvent = await getGoogleCalendarEvent(
|
|
360
|
+
accessToken,
|
|
361
|
+
gCalId,
|
|
362
|
+
existingTask.googleEventId,
|
|
363
|
+
);
|
|
364
|
+
const createdGoogleEvent = await createGoogleCalendarEvent(accessToken, targetGCalId, {
|
|
365
|
+
summary: googleUpdate.summary ?? currentGoogleEvent.summary,
|
|
366
|
+
description: googleUpdate.description ?? currentGoogleEvent.description,
|
|
367
|
+
location: googleUpdate.location ?? currentGoogleEvent.location,
|
|
368
|
+
start: googleUpdate.start ?? currentGoogleEvent.start,
|
|
369
|
+
end: googleUpdate.end ?? currentGoogleEvent.end,
|
|
370
|
+
attendees: googleUpdate.attendees ?? currentGoogleEvent.attendees,
|
|
371
|
+
});
|
|
372
|
+
await deleteGoogleCalendarEvent(accessToken, gCalId, existingTask.googleEventId);
|
|
373
|
+
updatedGoogleEvent = createdGoogleEvent;
|
|
374
|
+
updateData.googleEventId = createdGoogleEvent.id;
|
|
375
|
+
updateData.googleCalendarId = targetGCalId === 'primary' ? null : targetGCalId;
|
|
376
|
+
} else {
|
|
377
|
+
updatedGoogleEvent = await updateGoogleCalendarEvent(
|
|
378
|
+
accessToken,
|
|
379
|
+
gCalId,
|
|
380
|
+
existingTask.googleEventId,
|
|
381
|
+
googleUpdate,
|
|
382
|
+
);
|
|
383
|
+
}
|
|
268
384
|
|
|
269
385
|
// Mettre à jour le lien Meet si nécessaire
|
|
270
386
|
const meetLink = extractMeetLink(updatedGoogleEvent);
|
|
@@ -319,11 +435,8 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
|
|
319
435
|
},
|
|
320
436
|
});
|
|
321
437
|
|
|
322
|
-
//
|
|
323
|
-
if (
|
|
324
|
-
task.contactId &&
|
|
325
|
-
(existingTask.type === 'MEETING' || existingTask.type === 'VIDEO_CONFERENCE')
|
|
326
|
-
) {
|
|
438
|
+
// Interaction de modification uniquement pour les RDV physiques (pas la visio)
|
|
439
|
+
if (task.contactId && existingTask.type === 'MEETING') {
|
|
327
440
|
try {
|
|
328
441
|
await logAppointmentChanged(
|
|
329
442
|
task.contactId,
|
|
@@ -331,14 +444,64 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
|
|
331
444
|
task.scheduledAt,
|
|
332
445
|
task.title,
|
|
333
446
|
session.user.id,
|
|
334
|
-
|
|
447
|
+
false,
|
|
335
448
|
);
|
|
336
449
|
} catch (interactionError: any) {
|
|
337
450
|
console.error(
|
|
338
451
|
"Erreur lors de la création de l'interaction de modification:",
|
|
339
452
|
interactionError,
|
|
340
453
|
);
|
|
341
|
-
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Garder l'interaction « TASK » alignée avec les modifications (titre, contenu, date)
|
|
458
|
+
if (
|
|
459
|
+
task.contactId &&
|
|
460
|
+
task.type !== 'MEETING' &&
|
|
461
|
+
task.type !== 'VIDEO_CONFERENCE' &&
|
|
462
|
+
(title !== undefined || description !== undefined || scheduledAt !== undefined)
|
|
463
|
+
) {
|
|
464
|
+
try {
|
|
465
|
+
const interactionDate = task.scheduledAt instanceof Date ? task.scheduledAt : new Date(task.scheduledAt);
|
|
466
|
+
const interactionContent = description !== undefined ? description : task.description || '';
|
|
467
|
+
const updated = await prisma.interaction.updateMany({
|
|
468
|
+
where: {
|
|
469
|
+
contactId: task.contactId,
|
|
470
|
+
type: 'TASK',
|
|
471
|
+
metadata: {
|
|
472
|
+
path: ['taskId'],
|
|
473
|
+
equals: task.id,
|
|
474
|
+
},
|
|
475
|
+
},
|
|
476
|
+
data: {
|
|
477
|
+
title: task.title || null,
|
|
478
|
+
content: interactionContent,
|
|
479
|
+
date: interactionDate,
|
|
480
|
+
userId: session.user.id,
|
|
481
|
+
metadata: {
|
|
482
|
+
taskId: task.id,
|
|
483
|
+
scheduledAt: interactionDate.toISOString(),
|
|
484
|
+
},
|
|
485
|
+
},
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
// Sécurité: si aucune interaction liée n'existe, la recréer pour garder la timeline cohérente.
|
|
489
|
+
if (updated.count === 0) {
|
|
490
|
+
await createInteraction({
|
|
491
|
+
contactId: task.contactId,
|
|
492
|
+
type: 'TASK' as any,
|
|
493
|
+
title: task.title || null,
|
|
494
|
+
content: interactionContent,
|
|
495
|
+
userId: session.user.id,
|
|
496
|
+
date: interactionDate,
|
|
497
|
+
metadata: {
|
|
498
|
+
taskId: task.id,
|
|
499
|
+
scheduledAt: interactionDate.toISOString(),
|
|
500
|
+
},
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
} catch (interactionSyncError: any) {
|
|
504
|
+
console.error("Erreur lors de la synchronisation de l'interaction TASK:", interactionSyncError);
|
|
342
505
|
}
|
|
343
506
|
}
|
|
344
507
|
|
|
@@ -363,15 +526,15 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
|
|
363
526
|
where: { userId: session.user.id },
|
|
364
527
|
});
|
|
365
528
|
|
|
366
|
-
if (smtpConfig
|
|
367
|
-
//
|
|
368
|
-
|
|
369
|
-
if (task.contact?.email) {
|
|
529
|
+
if (smtpConfig) {
|
|
530
|
+
// Construire la liste des destinataires selon le type de rendez-vous
|
|
531
|
+
const allRecipients: string[] = [];
|
|
532
|
+
if (task.contact?.email && !allRecipients.includes(task.contact.email)) {
|
|
370
533
|
allRecipients.push(task.contact.email);
|
|
371
534
|
}
|
|
372
535
|
|
|
373
536
|
// Pour Google Meet uniquement, récupérer les invités depuis Google Calendar
|
|
374
|
-
if (
|
|
537
|
+
if (shouldNotifyForGoogleMeet && existingTask.googleEventId) {
|
|
375
538
|
try {
|
|
376
539
|
const { getUserGoogleAccount } = await import('@/lib/google-calendar');
|
|
377
540
|
const googleAccount = await getUserGoogleAccount(session.user.id);
|
|
@@ -385,6 +548,7 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
|
|
385
548
|
|
|
386
549
|
const googleEvent = await getGoogleCalendarEvent(
|
|
387
550
|
accessToken,
|
|
551
|
+
resolveTaskGoogleCalendarId(existingTask.googleCalendarId),
|
|
388
552
|
existingTask.googleEventId,
|
|
389
553
|
);
|
|
390
554
|
if (googleEvent.attendees) {
|
|
@@ -446,7 +610,7 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
|
|
446
610
|
newDuration,
|
|
447
611
|
hasDateChanged,
|
|
448
612
|
hasDurationChanged,
|
|
449
|
-
meetLink: task.googleMeetLink || undefined,
|
|
613
|
+
meetLink: shouldNotifyForGoogleMeet ? task.googleMeetLink || undefined : undefined,
|
|
450
614
|
description: task.description,
|
|
451
615
|
organizerName,
|
|
452
616
|
signature: smtpConfig.signature || undefined,
|
|
@@ -565,11 +729,13 @@ export async function DELETE(
|
|
|
565
729
|
},
|
|
566
730
|
});
|
|
567
731
|
|
|
568
|
-
// Récupérer les invités depuis Google Calendar AVANT suppression pour l'email
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
732
|
+
// Récupérer les invités depuis Google Calendar AVANT suppression pour l'email ; supprimer l’événement dès que googleEventId est défini (avec ou sans contact)
|
|
733
|
+
const allRecipients: string[] = [];
|
|
734
|
+
const contactEmail = taskWithContact?.contact?.email;
|
|
735
|
+
if (contactEmail && !allRecipients.includes(contactEmail)) {
|
|
736
|
+
allRecipients.push(contactEmail);
|
|
737
|
+
}
|
|
738
|
+
if (task.googleEventId) {
|
|
573
739
|
try {
|
|
574
740
|
const { getUserGoogleAccount } = await import('@/lib/google-calendar');
|
|
575
741
|
const googleAccount = await getUserGoogleAccount(session.user.id);
|
|
@@ -593,54 +759,64 @@ export async function DELETE(
|
|
|
593
759
|
});
|
|
594
760
|
}
|
|
595
761
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
762
|
+
const gCalIdDel = resolveTaskGoogleCalendarId(task.googleCalendarId);
|
|
763
|
+
try {
|
|
764
|
+
const googleEvent = await getGoogleCalendarEvent(
|
|
765
|
+
accessToken,
|
|
766
|
+
gCalIdDel,
|
|
767
|
+
task.googleEventId,
|
|
768
|
+
);
|
|
769
|
+
if (googleEvent.attendees) {
|
|
770
|
+
googleEvent.attendees.forEach((attendee) => {
|
|
771
|
+
if (attendee.email && !allRecipients.includes(attendee.email)) {
|
|
772
|
+
allRecipients.push(attendee.email);
|
|
773
|
+
}
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
} catch (getErr) {
|
|
777
|
+
console.warn(
|
|
778
|
+
"Impossible de lire l'événement Google avant suppression (invités email) :",
|
|
779
|
+
getErr,
|
|
780
|
+
);
|
|
604
781
|
}
|
|
605
782
|
|
|
606
|
-
|
|
607
|
-
await deleteGoogleCalendarEvent(accessToken, task.googleEventId);
|
|
783
|
+
await deleteGoogleCalendarEvent(accessToken, gCalIdDel, task.googleEventId);
|
|
608
784
|
} catch (googleError: any) {
|
|
609
785
|
console.error("Erreur lors de la suppression de l'événement Google Calendar:", googleError);
|
|
610
786
|
// On continue quand même la suppression de la tâche
|
|
611
787
|
}
|
|
612
788
|
}
|
|
613
789
|
|
|
614
|
-
//
|
|
615
|
-
if (task.contactId
|
|
790
|
+
// Supprimer les interactions liées à cette tâche (ex. activité « Tâche », RDV créé/modifié)
|
|
791
|
+
if (task.contactId) {
|
|
792
|
+
try {
|
|
793
|
+
const removed = await deleteInteractionsLinkedToTask(task.contactId, id);
|
|
794
|
+
if (removed.count > 0) {
|
|
795
|
+
console.log(`Interactions liées à la tâche ${id} supprimées: ${removed.count}`);
|
|
796
|
+
}
|
|
797
|
+
} catch (cleanupError: unknown) {
|
|
798
|
+
console.error('Erreur lors de la suppression des interactions liées à la tâche:', cleanupError);
|
|
799
|
+
// On continue : la suppression de la tâche reste prioritaire
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// Interaction d'annulation uniquement pour les RDV physiques (pas la visio)
|
|
804
|
+
if (task.contactId && task.type === 'MEETING') {
|
|
616
805
|
try {
|
|
617
|
-
console.log('Création interaction annulation pour task:', {
|
|
618
|
-
contactId: task.contactId,
|
|
619
|
-
taskId: task.id,
|
|
620
|
-
type: task.type,
|
|
621
|
-
title: task.title,
|
|
622
|
-
});
|
|
623
806
|
await logAppointmentCancelled(
|
|
624
807
|
task.contactId,
|
|
625
808
|
task.id,
|
|
626
809
|
task.scheduledAt,
|
|
627
810
|
task.title,
|
|
628
811
|
session.user.id,
|
|
629
|
-
|
|
812
|
+
false,
|
|
630
813
|
);
|
|
631
|
-
console.log("Interaction d'annulation créée avec succès");
|
|
632
814
|
} catch (interactionError: any) {
|
|
633
815
|
console.error(
|
|
634
816
|
"Erreur lors de la création de l'interaction d'annulation:",
|
|
635
817
|
interactionError,
|
|
636
818
|
);
|
|
637
|
-
// On continue même si l'interaction échoue
|
|
638
819
|
}
|
|
639
|
-
} else {
|
|
640
|
-
console.log("Pas de création d'interaction - conditions non remplies:", {
|
|
641
|
-
contactId: task.contactId,
|
|
642
|
-
type: task.type,
|
|
643
|
-
});
|
|
644
820
|
}
|
|
645
821
|
|
|
646
822
|
// Supprimer la tâche
|