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.
Files changed (190) hide show
  1. package/bin/create-crm-tmp.js +56 -35
  2. package/package.json +1 -1
  3. package/template/README.md +230 -115
  4. package/template/eslint.config.mjs +13 -0
  5. package/template/next.config.ts +14 -0
  6. package/template/package.json +15 -2
  7. package/template/prisma/migrations/20260318095700_init_db/migration.sql +978 -0
  8. package/template/prisma/migrations/migration_lock.toml +3 -0
  9. package/template/prisma/schema.prisma +132 -637
  10. package/template/src/app/(auth)/invite/[token]/page.tsx +10 -8
  11. package/template/src/app/(auth)/layout.tsx +1 -1
  12. package/template/src/app/(auth)/reset-password/complete/page.tsx +11 -8
  13. package/template/src/app/(auth)/reset-password/page.tsx +4 -4
  14. package/template/src/app/(auth)/reset-password/verify/page.tsx +4 -4
  15. package/template/src/app/(auth)/signin/page.tsx +14 -6
  16. package/template/src/app/(dashboard)/agenda/page.tsx +2243 -988
  17. package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +18 -104
  18. package/template/src/app/(dashboard)/automatisation/page.tsx +10 -26
  19. package/template/src/app/(dashboard)/closing/page.tsx +78 -62
  20. package/template/src/app/(dashboard)/contacts/[id]/page.tsx +2082 -1080
  21. package/template/src/app/(dashboard)/contacts/companies/[id]/page.tsx +46 -47
  22. package/template/src/app/(dashboard)/contacts/page.tsx +1062 -780
  23. package/template/src/app/(dashboard)/dashboard/page.tsx +533 -37
  24. package/template/src/app/(dashboard)/dev/page.tsx +1291 -0
  25. package/template/src/app/(dashboard)/layout.tsx +6 -2
  26. package/template/src/app/(dashboard)/settings/page.tsx +797 -2582
  27. package/template/src/app/(dashboard)/templates/page.tsx +55 -54
  28. package/template/src/app/(dashboard)/users/list/page.tsx +51 -48
  29. package/template/src/app/(dashboard)/users/page.tsx +1 -1
  30. package/template/src/app/(dashboard)/users/permissions/page.tsx +2 -2
  31. package/template/src/app/(dashboard)/users/roles/page.tsx +7 -5
  32. package/template/src/app/api/agenda/google-events/route.ts +92 -0
  33. package/template/src/app/api/auth/check-active/route.ts +3 -2
  34. package/template/src/app/api/auth/google/route.ts +2 -1
  35. package/template/src/app/api/auth/google/status/route.ts +7 -31
  36. package/template/src/app/api/companies/[id]/activities/route.ts +1 -3
  37. package/template/src/app/api/companies/[id]/route.ts +1 -2
  38. package/template/src/app/api/companies/route.ts +42 -12
  39. package/template/src/app/api/contacts/[id]/files/[fileId]/preview/route.ts +9 -31
  40. package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +14 -32
  41. package/template/src/app/api/contacts/[id]/files/route.ts +112 -212
  42. package/template/src/app/api/contacts/[id]/interactions/[interactionId]/route.ts +27 -1
  43. package/template/src/app/api/contacts/[id]/interactions/route.ts +16 -16
  44. package/template/src/app/api/contacts/[id]/kyc/route.ts +21 -11
  45. package/template/src/app/api/contacts/[id]/meet/route.ts +19 -2
  46. package/template/src/app/api/contacts/[id]/route.ts +106 -34
  47. package/template/src/app/api/contacts/[id]/send-email/route.ts +27 -11
  48. package/template/src/app/api/contacts/[id]/workflows/run/route.ts +6 -0
  49. package/template/src/app/api/contacts/export/route.ts +9 -13
  50. package/template/src/app/api/contacts/import/route.ts +55 -25
  51. package/template/src/app/api/contacts/import-preview/route.ts +1 -1
  52. package/template/src/app/api/contacts/origins/route.ts +63 -0
  53. package/template/src/app/api/contacts/route.ts +153 -41
  54. package/template/src/app/api/cron/cleanup-editor-images/route.ts +166 -0
  55. package/template/src/app/api/dashboard/widgets/[id]/route.ts +44 -0
  56. package/template/src/app/api/dashboard/widgets/route.ts +181 -0
  57. package/template/src/app/api/dev/reminders/test/route.ts +114 -0
  58. package/template/src/app/api/editor/upload-image/route.ts +61 -0
  59. package/template/src/app/api/integrations/google-sheet/jobs/[jobId]/route.ts +47 -0
  60. package/template/src/app/api/integrations/google-sheet/jobs/usage/route.ts +50 -0
  61. package/template/src/app/api/integrations/google-sheet/sync/route.ts +24 -556
  62. package/template/src/app/api/jobs/google-sheet/process/route.ts +84 -0
  63. package/template/src/app/api/jobs/google-sheet/schedule/route.ts +50 -0
  64. package/template/src/app/api/reminders/clear/route.ts +120 -0
  65. package/template/src/app/api/reminders/clear/undo/route.ts +112 -0
  66. package/template/src/app/api/reminders/route.ts +164 -39
  67. package/template/src/app/api/reminders/state/route.ts +164 -0
  68. package/template/src/app/api/reset-password/request/route.ts +1 -1
  69. package/template/src/app/api/reset-password/verify/route.ts +1 -1
  70. package/template/src/app/api/send/route.ts +16 -4
  71. package/template/src/app/api/settings/google-ads/route.ts +14 -0
  72. package/template/src/app/api/settings/google-calendar/calendars/route.ts +97 -0
  73. package/template/src/app/api/settings/google-calendar/route.ts +124 -0
  74. package/template/src/app/api/settings/google-sheet/[id]/route.ts +28 -0
  75. package/template/src/app/api/settings/google-sheet/auto-map/route.ts +37 -4
  76. package/template/src/app/api/settings/google-sheet/preview/route.ts +9 -3
  77. package/template/src/app/api/settings/google-sheet/route.ts +14 -0
  78. package/template/src/app/api/settings/integrations/logs/route.ts +93 -0
  79. package/template/src/app/api/settings/integrations/notifications/route.ts +67 -0
  80. package/template/src/app/api/settings/meta-leads/[id]/route.ts +0 -1
  81. package/template/src/app/api/settings/meta-leads/route.ts +14 -2
  82. package/template/src/app/api/settings/smtp/route.ts +53 -6
  83. package/template/src/app/api/tasks/[id]/attendees/route.ts +24 -8
  84. package/template/src/app/api/tasks/[id]/route.ts +234 -58
  85. package/template/src/app/api/tasks/meet/route.ts +27 -19
  86. package/template/src/app/api/tasks/route.ts +62 -17
  87. package/template/src/app/api/users/[id]/route.ts +20 -14
  88. package/template/src/app/api/users/list/route.ts +57 -19
  89. package/template/src/app/api/webhooks/google-ads/route.ts +34 -14
  90. package/template/src/app/api/webhooks/meta-leads/route.ts +32 -12
  91. package/template/src/app/api/workflows/[id]/route.ts +0 -4
  92. package/template/src/app/api/workflows/process/route.ts +22 -51
  93. package/template/src/app/api/workflows/route.ts +0 -4
  94. package/template/src/app/globals.css +342 -4
  95. package/template/src/app/layout.tsx +11 -3
  96. package/template/src/app/page.tsx +1 -1
  97. package/template/src/components/address-autocomplete.tsx +7 -6
  98. package/template/src/components/config-error-alert.tsx +46 -0
  99. package/template/src/components/contacts/filter-bar.tsx +12 -3
  100. package/template/src/components/contacts/filter-builder.tsx +28 -43
  101. package/template/src/components/contacts/save-view-dialog.tsx +1 -1
  102. package/template/src/components/contacts/views-tab-bar.tsx +15 -6
  103. package/template/src/components/dashboard/activity-chart.tsx +41 -28
  104. package/template/src/components/dashboard/add-widget-dialog.tsx +157 -0
  105. package/template/src/components/dashboard/color-picker.tsx +64 -0
  106. package/template/src/components/dashboard/contacts-chart.tsx +69 -0
  107. package/template/src/components/dashboard/interactions-by-type-chart.tsx +121 -0
  108. package/template/src/components/dashboard/recent-activity.tsx +154 -0
  109. package/template/src/components/dashboard/stat-card.tsx +40 -40
  110. package/template/src/components/dashboard/status-distribution-chart.tsx +81 -0
  111. package/template/src/components/dashboard/tasks-pie-chart.tsx +37 -34
  112. package/template/src/components/dashboard/top-contacts-list.tsx +113 -0
  113. package/template/src/components/dashboard/upcoming-tasks-list.tsx +72 -81
  114. package/template/src/components/dashboard/widget-wrapper.tsx +36 -0
  115. package/template/src/components/date-picker.tsx +9 -6
  116. package/template/src/components/editor/upload-editor-image.ts +42 -0
  117. package/template/src/components/editor.tsx +161 -22
  118. package/template/src/components/email-template.tsx +2 -2
  119. package/template/src/components/global-search.tsx +30 -28
  120. package/template/src/components/header.tsx +178 -80
  121. package/template/src/components/inactive-account-guard.tsx +58 -0
  122. package/template/src/components/integration-notifications-listener.tsx +12 -0
  123. package/template/src/components/invitation-email-template.tsx +2 -2
  124. package/template/src/components/meet-cancellation-email-template.tsx +3 -3
  125. package/template/src/components/meet-confirmation-email-template.tsx +3 -3
  126. package/template/src/components/meet-update-email-template.tsx +3 -3
  127. package/template/src/components/page-header.tsx +5 -5
  128. package/template/src/components/protected-page.tsx +1 -1
  129. package/template/src/components/reset-password-email-template.tsx +2 -2
  130. package/template/src/components/settings/integrations/GoogleAdsIntegration.tsx +428 -0
  131. package/template/src/components/settings/integrations/GoogleSheetConfigMonitoringModal.tsx +680 -0
  132. package/template/src/components/settings/integrations/GoogleSheetIntegration.tsx +809 -0
  133. package/template/src/components/settings/integrations/ImportResultDialog.tsx +124 -0
  134. package/template/src/components/settings/integrations/IntegrationLogPanel.tsx +57 -0
  135. package/template/src/components/settings/integrations/IntegrationLogsTable.tsx +186 -0
  136. package/template/src/components/settings/integrations/MetaLeadIntegration.tsx +451 -0
  137. package/template/src/components/sidebar.tsx +45 -26
  138. package/template/src/components/skeleton.tsx +40 -43
  139. package/template/src/components/ui/accordion.tsx +2 -2
  140. package/template/src/components/ui/alert-dialog.tsx +1 -1
  141. package/template/src/components/ui/button.tsx +20 -9
  142. package/template/src/components/ui/components.tsx +1 -1
  143. package/template/src/components/ui/date-picker.tsx +422 -0
  144. package/template/src/components/ui/datetime-picker.tsx +338 -0
  145. package/template/src/components/ui/status-select.tsx +271 -0
  146. package/template/src/components/ui/tooltip.tsx +37 -0
  147. package/template/src/components/view-as-modal.tsx +13 -7
  148. package/template/src/contexts/app-toast-context.tsx +245 -57
  149. package/template/src/contexts/dashboard-theme-context.tsx +53 -0
  150. package/template/src/contexts/sidebar-context.tsx +22 -17
  151. package/template/src/contexts/task-reminder-context.tsx +134 -160
  152. package/template/src/contexts/view-as-context.tsx +33 -6
  153. package/template/src/hooks/use-focus-trap.ts +2 -2
  154. package/template/src/hooks/useIntegrationNotifications.ts +49 -0
  155. package/template/src/lib/auth.ts +8 -1
  156. package/template/src/lib/config-links.ts +14 -0
  157. package/template/src/lib/contact-duplicate.ts +79 -61
  158. package/template/src/lib/contact-interactions.ts +21 -21
  159. package/template/src/lib/contact-view-filters.ts +24 -64
  160. package/template/src/lib/contacts-list-url.ts +190 -0
  161. package/template/src/lib/dashboard-stats.ts +65 -7
  162. package/template/src/lib/dashboard-themes.ts +135 -0
  163. package/template/src/lib/date-utils.ts +127 -0
  164. package/template/src/lib/default-widgets.ts +12 -0
  165. package/template/src/lib/editor-html-image-dimensions.ts +172 -0
  166. package/template/src/lib/editor-image-limits.ts +19 -0
  167. package/template/src/lib/email-html-sanitize.ts +19 -0
  168. package/template/src/lib/encryption.ts +9 -6
  169. package/template/src/lib/fr-geography.ts +192 -0
  170. package/template/src/lib/google-calendar-agenda.ts +201 -0
  171. package/template/src/lib/google-calendar.ts +255 -5
  172. package/template/src/lib/google-sheet-sync-jobs.ts +96 -0
  173. package/template/src/lib/google-sheet-sync-runner.ts +514 -0
  174. package/template/src/lib/integration-import-log.ts +21 -0
  175. package/template/src/lib/permissions.ts +40 -10
  176. package/template/src/lib/prisma.ts +4 -1
  177. package/template/src/lib/qstash.ts +65 -0
  178. package/template/src/lib/reminder-state-server.ts +80 -0
  179. package/template/src/lib/reminder-state.ts +29 -0
  180. package/template/src/lib/supabase-storage.ts +113 -0
  181. package/template/src/lib/template-variables.ts +164 -23
  182. package/template/src/lib/utils.ts +45 -0
  183. package/template/src/lib/widget-registry.ts +173 -0
  184. package/template/src/lib/workflow-executor.ts +16 -70
  185. package/template/src/proxy.ts +1 -0
  186. package/template/vercel.json +3 -10
  187. package/template/skills-lock.json +0 -25
  188. package/template/src/components/dashboard/dashboard-content.tsx +0 -79
  189. package/template/src/lib/google-drive.ts +0 -1101
  190. 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 { interactionId } = await params;
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 prisma.contact.findUnique({
21
- where: { id },
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 prisma.contact.findUnique({
93
- where: { id },
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 utiliser Google Calendar.',
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: updateData,
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 prisma.contact.findUnique({
55
- where: { id },
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
- { error: "Erreur d'authentification SMTP. Vérifiez votre configuration." },
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 { getFileInfo } from '@/lib/google-drive';
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
- googleDriveFileId: true,
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
- // Fonction pour formater les fichiers (essayer de récupérer les liens si possible)
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
- // Essayer de récupérer le lien depuis Google Drive
144
- const fileInfo = await getFileInfo(userId, file.googleDriveFileId);
145
- const sizeKB = (file.fileSize / 1024).toFixed(2);
146
- return `${file.fileName} (${sizeKB} KB) - ${fileInfo.webViewLink}`;
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 || [], session.user.id);
162
+ const files = await formatFiles(contact.files || []);
167
163
 
168
164
  return [
169
165
  contact.civility || '',