create-crm-tmp 1.1.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (220) hide show
  1. package/package.json +1 -1
  2. package/template/.prettierignore +2 -0
  3. package/template/README.md +53 -67
  4. package/template/components.json +22 -0
  5. package/template/exemple-contacts.csv +54 -0
  6. package/template/next.config.ts +27 -1
  7. package/template/package.json +64 -27
  8. package/template/prisma/schema.prisma +821 -72
  9. package/template/skills-lock.json +25 -0
  10. package/template/src/app/(auth)/invite/[token]/page.tsx +21 -24
  11. package/template/src/app/(auth)/reset-password/complete/page.tsx +12 -21
  12. package/template/src/app/(auth)/reset-password/page.tsx +12 -8
  13. package/template/src/app/(auth)/reset-password/verify/page.tsx +12 -8
  14. package/template/src/app/(auth)/signin/page.tsx +20 -17
  15. package/template/src/app/(dashboard)/agenda/page.tsx +2231 -2188
  16. package/template/src/app/(dashboard)/automatisation/[id]/page.tsx +10 -7
  17. package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +680 -323
  18. package/template/src/app/(dashboard)/automatisation/new/page.tsx +11 -8
  19. package/template/src/app/(dashboard)/automatisation/page.tsx +473 -180
  20. package/template/src/app/(dashboard)/closing/page.tsx +500 -468
  21. package/template/src/app/(dashboard)/contacts/[id]/page.tsx +5035 -4126
  22. package/template/src/app/(dashboard)/contacts/companies/[id]/page.tsx +1703 -0
  23. package/template/src/app/(dashboard)/contacts/loading.tsx +13 -0
  24. package/template/src/app/(dashboard)/contacts/page.tsx +3776 -2064
  25. package/template/src/app/(dashboard)/dashboard/page.tsx +37 -519
  26. package/template/src/app/(dashboard)/error.tsx +37 -0
  27. package/template/src/app/(dashboard)/layout.tsx +1 -1
  28. package/template/src/app/(dashboard)/loading.tsx +5 -0
  29. package/template/src/app/(dashboard)/settings/loading.tsx +19 -0
  30. package/template/src/app/(dashboard)/settings/page.tsx +2685 -2489
  31. package/template/src/app/(dashboard)/templates/page.tsx +500 -300
  32. package/template/src/app/(dashboard)/users/list/page.tsx +356 -350
  33. package/template/src/app/(dashboard)/users/page.tsx +279 -310
  34. package/template/src/app/(dashboard)/users/permissions/page.tsx +104 -99
  35. package/template/src/app/(dashboard)/users/roles/page.tsx +164 -137
  36. package/template/src/app/api/audit-logs/route.ts +1 -1
  37. package/template/src/app/api/auth/google/callback/route.ts +8 -5
  38. package/template/src/app/api/auth/google/disconnect/route.ts +2 -2
  39. package/template/src/app/api/companies/[id]/activities/route.ts +131 -0
  40. package/template/src/app/api/companies/[id]/route.ts +195 -0
  41. package/template/src/app/api/companies/export/route.ts +206 -0
  42. package/template/src/app/api/companies/route.ts +166 -0
  43. package/template/src/app/api/contact-views/[id]/pin/route.ts +69 -0
  44. package/template/src/app/api/contact-views/[id]/route.ts +197 -0
  45. package/template/src/app/api/contact-views/route.ts +146 -0
  46. package/template/src/app/api/contacts/[id]/files/[fileId]/preview/route.ts +77 -0
  47. package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +7 -17
  48. package/template/src/app/api/contacts/[id]/files/route.ts +83 -44
  49. package/template/src/app/api/contacts/[id]/interactions/route.ts +37 -0
  50. package/template/src/app/api/contacts/[id]/kyc/route.ts +71 -0
  51. package/template/src/app/api/contacts/[id]/meet/route.ts +38 -29
  52. package/template/src/app/api/contacts/[id]/route.ts +111 -20
  53. package/template/src/app/api/contacts/[id]/send-email/route.ts +6 -0
  54. package/template/src/app/api/contacts/[id]/workflows/run/route.ts +61 -0
  55. package/template/src/app/api/contacts/export/route.ts +12 -17
  56. package/template/src/app/api/contacts/import/route.ts +22 -19
  57. package/template/src/app/api/contacts/import-preview/route.ts +139 -0
  58. package/template/src/app/api/contacts/route.ts +202 -49
  59. package/template/src/app/api/dashboard/stats/route.ts +9 -292
  60. package/template/src/app/api/integrations/google-sheet/sync/route.ts +203 -185
  61. package/template/src/app/api/invite/complete/route.ts +20 -23
  62. package/template/src/app/api/reminders/route.ts +1 -0
  63. package/template/src/app/api/reset-password/complete/route.ts +11 -13
  64. package/template/src/app/api/send/route.ts +9 -85
  65. package/template/src/app/api/settings/closing-reasons/[id]/route.ts +10 -21
  66. package/template/src/app/api/settings/closing-reasons/route.ts +10 -21
  67. package/template/src/app/api/settings/company/route.ts +19 -26
  68. package/template/src/app/api/settings/google-ads/[id]/route.ts +20 -23
  69. package/template/src/app/api/settings/google-ads/route.ts +20 -23
  70. package/template/src/app/api/settings/google-sheet/[id]/route.ts +20 -23
  71. package/template/src/app/api/settings/google-sheet/auto-map/route.ts +23 -32
  72. package/template/src/app/api/settings/google-sheet/preview/route.ts +104 -0
  73. package/template/src/app/api/settings/google-sheet/route.ts +20 -23
  74. package/template/src/app/api/settings/meta-leads/[id]/route.ts +20 -23
  75. package/template/src/app/api/settings/meta-leads/route.ts +20 -23
  76. package/template/src/app/api/settings/statuses/[id]/route.ts +33 -23
  77. package/template/src/app/api/settings/statuses/route.ts +24 -22
  78. package/template/src/app/api/statuses/route.ts +2 -5
  79. package/template/src/app/api/tasks/[id]/attendees/route.ts +14 -7
  80. package/template/src/app/api/tasks/[id]/route.ts +161 -137
  81. package/template/src/app/api/tasks/meet/route.ts +11 -8
  82. package/template/src/app/api/tasks/route.ts +155 -95
  83. package/template/src/app/api/templates/[id]/route.ts +22 -13
  84. package/template/src/app/api/templates/route.ts +22 -5
  85. package/template/src/app/api/users/[id]/resend-invite/route.ts +95 -0
  86. package/template/src/app/api/users/[id]/route.ts +16 -1
  87. package/template/src/app/api/users/commercials/route.ts +38 -0
  88. package/template/src/app/api/users/for-agenda/route.ts +1 -2
  89. package/template/src/app/api/users/route.ts +94 -55
  90. package/template/src/app/api/webhooks/google-ads/route.ts +20 -1
  91. package/template/src/app/api/webhooks/meta-leads/route.ts +18 -1
  92. package/template/src/app/api/workflows/[id]/route.ts +33 -6
  93. package/template/src/app/api/workflows/process/route.ts +509 -146
  94. package/template/src/app/api/workflows/route.ts +46 -4
  95. package/template/src/app/globals.css +210 -101
  96. package/template/src/app/layout.tsx +19 -8
  97. package/template/src/app/page.tsx +37 -7
  98. package/template/src/components/address-autocomplete.tsx +232 -0
  99. package/template/src/components/contacts/filter-bar.tsx +181 -0
  100. package/template/src/components/contacts/filter-builder.tsx +589 -0
  101. package/template/src/components/contacts/save-view-dialog.tsx +160 -0
  102. package/template/src/components/contacts/views-tab-bar.tsx +440 -0
  103. package/template/src/components/dashboard/activity-chart.tsx +31 -39
  104. package/template/src/components/dashboard/dashboard-content.tsx +79 -0
  105. package/template/src/components/dashboard/stat-card.tsx +40 -42
  106. package/template/src/components/dashboard/tasks-pie-chart.tsx +34 -37
  107. package/template/src/components/dashboard/upcoming-tasks-list.tsx +78 -72
  108. package/template/src/components/date-picker.tsx +396 -0
  109. package/template/src/components/editor.tsx +27 -13
  110. package/template/src/components/email-template.tsx +4 -2
  111. package/template/src/components/global-search.tsx +358 -0
  112. package/template/src/components/header.tsx +57 -62
  113. package/template/src/components/invitation-email-template.tsx +4 -2
  114. package/template/src/components/lazy-editor.tsx +11 -0
  115. package/template/src/components/meet-cancellation-email-template.tsx +11 -3
  116. package/template/src/components/meet-confirmation-email-template.tsx +10 -3
  117. package/template/src/components/meet-update-email-template.tsx +10 -3
  118. package/template/src/components/page-header.tsx +19 -15
  119. package/template/src/components/protected-page.tsx +94 -0
  120. package/template/src/components/reset-password-email-template.tsx +4 -2
  121. package/template/src/components/sidebar.tsx +92 -94
  122. package/template/src/components/skeleton.tsx +128 -42
  123. package/template/src/components/ui/accordion.tsx +64 -0
  124. package/template/src/components/ui/alert-dialog.tsx +139 -0
  125. package/template/src/components/ui/button.tsx +60 -0
  126. package/template/src/components/view-as-banner.tsx +1 -1
  127. package/template/src/components/view-as-modal.tsx +21 -16
  128. package/template/src/config/nav-pages.ts +108 -0
  129. package/template/src/contexts/app-toast-context.tsx +174 -0
  130. package/template/src/contexts/sidebar-context.tsx +16 -47
  131. package/template/src/contexts/task-reminder-context.tsx +6 -6
  132. package/template/src/contexts/view-as-context.tsx +11 -16
  133. package/template/src/hooks/use-alert.tsx +65 -0
  134. package/template/src/hooks/use-confirm.tsx +87 -0
  135. package/template/src/hooks/use-contact-views.ts +140 -0
  136. package/template/src/hooks/use-contacts.ts +69 -0
  137. package/template/src/hooks/use-fetch.ts +17 -0
  138. package/template/src/hooks/use-focus-trap.ts +73 -0
  139. package/template/src/hooks/use-statuses.ts +22 -0
  140. package/template/src/lib/address-api.ts +155 -0
  141. package/template/src/lib/cache.ts +73 -0
  142. package/template/src/lib/check-permission.ts +12 -177
  143. package/template/src/lib/contact-interactions.ts +3 -1
  144. package/template/src/lib/contact-view-filters.ts +341 -0
  145. package/template/src/lib/dashboard-stats.ts +224 -0
  146. package/template/src/lib/date-utils.ts +49 -0
  147. package/template/src/lib/get-auth-user.ts +25 -0
  148. package/template/src/lib/google-calendar.ts +54 -12
  149. package/template/src/lib/google-drive.ts +796 -75
  150. package/template/src/lib/google-fetch.ts +63 -0
  151. package/template/src/lib/local-storage.ts +34 -0
  152. package/template/src/lib/permissions.ts +245 -47
  153. package/template/src/lib/prisma.ts +11 -11
  154. package/template/src/lib/roles.ts +14 -39
  155. package/template/src/lib/template-variables.ts +67 -33
  156. package/template/src/lib/utils.ts +26 -2
  157. package/template/src/lib/workflow-executor.ts +445 -229
  158. package/template/src/proxy.ts +34 -73
  159. package/template/src/types/contact-views.ts +351 -0
  160. package/template/src/types/yousign.ts +52 -0
  161. package/template/vercel.json +12 -0
  162. package/template/WORKFLOWS_CRON.md +0 -185
  163. package/template/prisma/migrations/20251126144728_init/migration.sql +0 -78
  164. package/template/prisma/migrations/20251126155204_add_user_roles/migration.sql +0 -5
  165. package/template/prisma/migrations/20251128095126_add_company_info/migration.sql +0 -19
  166. package/template/prisma/migrations/20251128123321_add_smtp_config/migration.sql +0 -22
  167. package/template/prisma/migrations/20251128132303_add_status/migration.sql +0 -23
  168. package/template/prisma/migrations/20251201102207_add_user_active/migration.sql +0 -75
  169. package/template/prisma/migrations/20251201105507_add_email_signature/migration.sql +0 -2
  170. package/template/prisma/migrations/20251201151122_add_tasks/migration.sql +0 -45
  171. package/template/prisma/migrations/20251202111854_add_task_reminder/migration.sql +0 -2
  172. package/template/prisma/migrations/20251202135859_add_google_meet_integration/migration.sql +0 -27
  173. package/template/prisma/migrations/20251203103317_add_meta_lead_integration/migration.sql +0 -20
  174. package/template/prisma/migrations/20251203104002_add_google_ads_integration/migration.sql +0 -18
  175. package/template/prisma/migrations/20251203112122_add_google_sheet_integration/migration.sql +0 -32
  176. package/template/prisma/migrations/20251203153853_allow_multiple_integration_configs/migration.sql +0 -20
  177. package/template/prisma/migrations/20251205141705_update_user_roles/migration.sql +0 -12
  178. package/template/prisma/migrations/20251205150000_add_commercial_and_telepro_assignment/migration.sql +0 -21
  179. package/template/prisma/migrations/20251205160000_add_interaction_logging/migration.sql +0 -11
  180. package/template/prisma/migrations/20251208090314_add_automatic_interaction_types/migration.sql +0 -12
  181. package/template/prisma/migrations/20251208094843_mg/migration.sql +0 -14
  182. package/template/prisma/migrations/20251208100000_add_company_support/migration.sql +0 -14
  183. package/template/prisma/migrations/20251208110000_add_templates/migration.sql +0 -26
  184. package/template/prisma/migrations/20251208141304_add_video_conference_task_type/migration.sql +0 -2
  185. package/template/prisma/migrations/20251209104759_add_internal_note_to_task/migration.sql +0 -2
  186. package/template/prisma/migrations/20251209134803_add_company_field/migration.sql +0 -2
  187. package/template/prisma/migrations/20251209150000_rename_company_to_company_name/migration.sql +0 -3
  188. package/template/prisma/migrations/20251209150016_add_email_tracking/migration.sql +0 -21
  189. package/template/prisma/migrations/20251209155908_add_notify_contact_to_task/migration.sql +0 -2
  190. package/template/prisma/migrations/20251210110019_add_appointment_types/migration.sql +0 -10
  191. package/template/prisma/migrations/20251210113928_add_contact_files/migration.sql +0 -26
  192. package/template/prisma/migrations/20251212132339_add_custom_roles/migration.sql +0 -24
  193. package/template/prisma/migrations/20251215104448_add_file_interaction_types/migration.sql +0 -11
  194. package/template/prisma/migrations/20251215145616_add_closing_reasons/migration.sql +0 -12
  195. package/template/prisma/migrations/20251216140850_add_log_users/migration.sql +0 -25
  196. package/template/prisma/migrations/20251216151000_rename_perdu_to_ferme/migration.sql +0 -8
  197. package/template/prisma/migrations/20251216162318_add_column_mappings_to_google_sheet/migration.sql +0 -2
  198. package/template/prisma/migrations/20251216185127_add_workflows/migration.sql +0 -80
  199. package/template/prisma/migrations/20251216192237_add_scheduled_workflow_actions/migration.sql +0 -32
  200. package/template/prisma/migrations/20251220000000_add_task_interaction_type/migration.sql +0 -4
  201. package/template/prisma/migrations/20251221000000_add_task_type/migration.sql +0 -3
  202. package/template/prisma/migrations/20251221000001_add_event_color/migration.sql +0 -23
  203. package/template/prisma/migrations/20260210114913_add_dashboard_widget/migration.sql +0 -20
  204. package/template/prisma/migrations/migration_lock.toml +0 -3
  205. package/template/src/app/(dashboard)/users/layout.tsx +0 -30
  206. package/template/src/app/api/dashboard/widgets/[id]/route.ts +0 -47
  207. package/template/src/app/api/dashboard/widgets/route.ts +0 -181
  208. package/template/src/components/dashboard/add-widget-dialog.tsx +0 -161
  209. package/template/src/components/dashboard/color-picker.tsx +0 -65
  210. package/template/src/components/dashboard/contacts-chart.tsx +0 -69
  211. package/template/src/components/dashboard/interactions-by-type-chart.tsx +0 -121
  212. package/template/src/components/dashboard/recent-activity.tsx +0 -157
  213. package/template/src/components/dashboard/sales-analytics-chart.tsx +0 -77
  214. package/template/src/components/dashboard/status-distribution-chart.tsx +0 -82
  215. package/template/src/components/dashboard/top-contacts-list.tsx +0 -119
  216. package/template/src/components/dashboard/widget-wrapper.tsx +0 -39
  217. package/template/src/contexts/dashboard-theme-context.tsx +0 -58
  218. package/template/src/lib/dashboard-themes.ts +0 -140
  219. package/template/src/lib/default-widgets.ts +0 -14
  220. package/template/src/lib/widget-registry.ts +0 -177
@@ -1,6 +1,7 @@
1
1
  import { NextRequest, NextResponse } from 'next/server';
2
2
  import { auth } from '@/lib/auth';
3
3
  import { prisma } from '@/lib/prisma';
4
+ import { checkPermission } from '@/lib/check-permission';
4
5
  import { deleteFileFromDrive } from '@/lib/google-drive';
5
6
  import { logFileDeleted } from '@/lib/contact-interactions';
6
7
 
@@ -18,9 +19,13 @@ export async function DELETE(
18
19
  return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
19
20
  }
20
21
 
22
+ const canDeleteFiles = await checkPermission('contacts.delete_files');
23
+ if (!canDeleteFiles) {
24
+ return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
25
+ }
26
+
21
27
  const { id: contactId, fileId } = await params;
22
28
 
23
- // Vérifier que le contact existe
24
29
  const contact = await prisma.contact.findUnique({
25
30
  where: { id: contactId },
26
31
  });
@@ -29,7 +34,6 @@ export async function DELETE(
29
34
  return NextResponse.json({ error: 'Contact non trouvé' }, { status: 404 });
30
35
  }
31
36
 
32
- // Vérifier que le fichier existe et appartient au contact
33
37
  const file = await prisma.contactFile.findUnique({
34
38
  where: { id: fileId },
35
39
  });
@@ -42,26 +46,12 @@ export async function DELETE(
42
46
  return NextResponse.json({ error: 'Fichier non associé à ce contact' }, { status: 403 });
43
47
  }
44
48
 
45
- // Vérifier les permissions (seul l'uploader ou un admin peut supprimer)
46
- const user = await prisma.user.findUnique({
47
- where: { id: session.user.id },
48
- select: { role: true },
49
- });
50
-
51
- if (file.uploadedById !== session.user.id && user?.role !== 'ADMIN') {
52
- return NextResponse.json(
53
- { error: "Vous n'avez pas la permission de supprimer ce fichier" },
54
- { status: 403 },
55
- );
56
- }
57
-
58
49
  // Sauvegarder les informations du fichier avant suppression pour l'interaction
59
50
  const fileName = file.fileName;
60
51
  const fileSize = file.fileSize;
61
52
 
62
- // Supprimer le fichier de Google Drive (via le compte admin)
53
+ // Supprimer le fichier de Google Drive
63
54
  try {
64
- // On passe un userId quelconque car deleteFileFromDrive utilise getAdminGoogleAccount()
65
55
  await deleteFileFromDrive(session.user.id, file.googleDriveFileId);
66
56
  } catch (error) {
67
57
  console.error('Erreur lors de la suppression du fichier de Google Drive:', error);
@@ -1,6 +1,7 @@
1
1
  import { NextRequest, NextResponse } from 'next/server';
2
2
  import { auth } from '@/lib/auth';
3
3
  import { prisma } from '@/lib/prisma';
4
+ import { checkPermission } from '@/lib/check-permission';
4
5
  import { uploadFileToDrive, getFileInfo } from '@/lib/google-drive';
5
6
  import { logFileUploaded, logFileReplaced } from '@/lib/contact-interactions';
6
7
 
@@ -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 canUpload = await checkPermission('contacts.upload_files');
20
+ if (!canUpload) {
21
+ return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
22
+ }
23
+
18
24
  const { id: contactId } = await params;
19
25
 
20
26
  // Vérifier que le contact existe
@@ -31,22 +37,13 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
31
37
  return NextResponse.json({ error: 'Contact non trouvé' }, { status: 404 });
32
38
  }
33
39
 
34
- // Vérifier qu'un administrateur a configuré Google Drive
35
- const adminUser = await prisma.user.findFirst({
36
- where: { role: 'ADMIN' },
37
- include: { googleAccount: true },
38
- orderBy: { createdAt: 'asc' },
39
- });
40
+ // Vérifier si c'est pour une transaction (via query param)
41
+ const { searchParams } = new URL(request.url);
42
+ const isForTransaction = searchParams.get('transaction') === 'true';
43
+ const isIdentityDocument = searchParams.get('isIdentityDocument') === 'true';
40
44
 
41
- if (!adminUser || !adminUser.googleAccount) {
42
- return NextResponse.json(
43
- {
44
- error:
45
- 'Aucun compte Google Drive configuré. Veuillez demander à un administrateur de connecter son compte Google Drive dans les paramètres.',
46
- },
47
- { status: 400 },
48
- );
49
- }
45
+ // Récupérer le compte Google de l'admin (tous les utilisateurs utilisent la config admin)
46
+ // Note: uploadFileToDrive utilise déjà getAdminGoogleAccount, donc pas besoin de vérifier ici
50
47
 
51
48
  // Récupérer le FormData
52
49
  const formData = await request.formData();
@@ -80,17 +77,23 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
80
77
  let contactFile;
81
78
 
82
79
  if (existingFile) {
83
- // Si le fichier existe déjà, supprimer l'ancien fichier de Google Drive (via le compte admin)
80
+ // Si le fichier existe déjà, supprimer l'ancien fichier de Google Drive
84
81
  try {
85
82
  const { deleteFileFromDrive } = await import('@/lib/google-drive');
86
- await deleteFileFromDrive(adminUser.id, existingFile.googleDriveFileId);
83
+ await deleteFileFromDrive(session.user.id, existingFile.googleDriveFileId);
87
84
  } catch (error) {
88
85
  console.error("Erreur lors de la suppression de l'ancien fichier:", error);
89
86
  // On continue même si la suppression échoue
90
87
  }
91
88
 
92
- // Uploader le nouveau fichier vers Google Drive (via le compte admin)
93
- const { fileId } = await uploadFileToDrive(adminUser.id, contactId, contactName, file);
89
+ // Uploader le nouveau fichier vers Google Drive
90
+ const { fileId } = await uploadFileToDrive(
91
+ session.user.id,
92
+ contactId,
93
+ contactName,
94
+ file,
95
+ isForTransaction,
96
+ );
94
97
 
95
98
  // Mettre à jour l'enregistrement existant
96
99
  contactFile = await prisma.contactFile.update({
@@ -99,6 +102,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
99
102
  fileSize: file.size,
100
103
  mimeType: file.type || 'application/octet-stream',
101
104
  googleDriveFileId: fileId,
105
+ isIdentityDocument,
102
106
  uploadedById: session.user.id,
103
107
  updatedAt: new Date(),
104
108
  },
@@ -124,8 +128,14 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
124
128
  // On continue même si l'interaction échoue
125
129
  }
126
130
  } else {
127
- // Uploader le fichier vers Google Drive (via le compte admin)
128
- const { fileId } = await uploadFileToDrive(adminUser.id, contactId, contactName, file);
131
+ // Uploader le fichier vers Google Drive
132
+ const { fileId } = await uploadFileToDrive(
133
+ session.user.id,
134
+ contactId,
135
+ contactName,
136
+ file,
137
+ isForTransaction,
138
+ );
129
139
 
130
140
  // Créer un nouvel enregistrement dans la base de données
131
141
  contactFile = await prisma.contactFile.create({
@@ -135,6 +145,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
135
145
  fileSize: file.size,
136
146
  mimeType: file.type || 'application/octet-stream',
137
147
  googleDriveFileId: fileId,
148
+ isIdentityDocument,
138
149
  uploadedById: session.user.id,
139
150
  },
140
151
  include: {
@@ -157,15 +168,26 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
157
168
  }
158
169
  }
159
170
 
160
- // Récupérer le webViewLink pour la réponse (via le compte admin)
161
- const fileInfo = await getFileInfo(adminUser.id, contactFile.googleDriveFileId);
171
+ // Récupérer le webViewLink pour la réponse
172
+ const fileInfo = await getFileInfo(session.user.id, contactFile.googleDriveFileId);
162
173
  const webViewLink = fileInfo.webViewLink;
163
174
 
175
+ // Si c'est pour une transaction, mettre à jour automatiquement idDocumentUrl du contact
176
+ if (isForTransaction) {
177
+ await prisma.contact.update({
178
+ where: { id: contactId },
179
+ data: {
180
+ idDocumentUrl: webViewLink,
181
+ },
182
+ });
183
+ }
184
+
164
185
  return NextResponse.json({
165
186
  id: contactFile.id,
166
187
  fileName: contactFile.fileName,
167
188
  fileSize: contactFile.fileSize,
168
189
  mimeType: contactFile.mimeType,
190
+ isIdentityDocument: contactFile.isIdentityDocument,
169
191
  webViewLink,
170
192
  uploadedBy: contactFile.uploadedBy,
171
193
  createdAt: contactFile.createdAt,
@@ -173,6 +195,24 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
173
195
  });
174
196
  } catch (error: any) {
175
197
  console.error("Erreur lors de l'upload du fichier:", error);
198
+
199
+ // Détecter les erreurs d'authentification Google (token expiré/révoqué)
200
+ if (
201
+ error.message?.includes('401') ||
202
+ error.message?.includes('UNAUTHENTICATED') ||
203
+ error.message?.includes('Invalid Credentials') ||
204
+ error.message?.includes('invalid_grant')
205
+ ) {
206
+ return NextResponse.json(
207
+ {
208
+ error:
209
+ '🔒 La connexion Google Drive a expiré. Veuillez demander à un administrateur de reconnecter son compte Google dans Paramètres > Intégrations > Google Drive.',
210
+ },
211
+ { status: 401 },
212
+ );
213
+ }
214
+
215
+ // Autres erreurs
176
216
  return NextResponse.json(
177
217
  { error: error.message || "Erreur lors de l'upload du fichier" },
178
218
  { status: 500 },
@@ -191,6 +231,11 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
191
231
  return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
192
232
  }
193
233
 
234
+ const canViewFiles = await checkPermission('contacts.view_files');
235
+ if (!canViewFiles) {
236
+ return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
237
+ }
238
+
194
239
  const { id: contactId } = await params;
195
240
 
196
241
  // Vérifier que le contact existe
@@ -219,30 +264,23 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
219
264
  },
220
265
  });
221
266
 
222
- // Vérifier qu'un administrateur a configuré Google Drive
223
- const adminUser = await prisma.user.findFirst({
224
- where: { role: 'ADMIN' },
225
- include: { googleAccount: true },
226
- orderBy: { createdAt: 'asc' },
227
- });
228
-
229
- // Pour chaque fichier, récupérer le lien de visualisation depuis Google Drive (via le compte admin)
267
+ // Pour chaque fichier, récupérer le lien de visualisation depuis Google Drive
230
268
  const filesWithLinks = await Promise.all(
231
269
  files.map(async (file) => {
232
270
  try {
233
- if (adminUser && adminUser.googleAccount) {
234
- const fileInfo = await getFileInfo(adminUser.id, file.googleDriveFileId);
235
- return {
236
- id: file.id,
237
- fileName: file.fileName,
238
- fileSize: file.fileSize,
239
- mimeType: file.mimeType,
240
- webViewLink: fileInfo.webViewLink,
241
- uploadedBy: file.uploadedBy,
242
- createdAt: file.createdAt,
243
- updatedAt: file.updatedAt,
244
- };
245
- }
271
+ // getFileInfo utilise déjà le compte admin
272
+ const fileInfo = await getFileInfo(session.user.id, file.googleDriveFileId);
273
+ return {
274
+ id: file.id,
275
+ fileName: file.fileName,
276
+ fileSize: file.fileSize,
277
+ mimeType: file.mimeType,
278
+ isIdentityDocument: file.isIdentityDocument,
279
+ webViewLink: fileInfo.webViewLink,
280
+ uploadedBy: file.uploadedBy,
281
+ createdAt: file.createdAt,
282
+ updatedAt: file.updatedAt,
283
+ };
246
284
  } catch (error) {
247
285
  console.error(
248
286
  `Erreur lors de la récupération du lien pour le fichier ${file.id}:`,
@@ -255,6 +293,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
255
293
  fileName: file.fileName,
256
294
  fileSize: file.fileSize,
257
295
  mimeType: file.mimeType,
296
+ isIdentityDocument: file.isIdentityDocument,
258
297
  webViewLink: `https://drive.google.com/file/d/${file.googleDriveFileId}/view`,
259
298
  uploadedBy: file.uploadedBy,
260
299
  createdAt: file.createdAt,
@@ -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
  // GET /api/contacts/[id]/interactions - Récupérer les interactions d'un contact
6
7
  export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
@@ -24,6 +25,24 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
24
25
  return NextResponse.json({ error: 'Contact non trouvé' }, { status: 404 });
25
26
  }
26
27
 
28
+ // Vérifier les permissions
29
+ const canViewAll = await checkPermission('contacts.view_all');
30
+ const canViewOwn = await checkPermission('contacts.view_own');
31
+
32
+ if (!canViewAll && !canViewOwn) {
33
+ return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
34
+ }
35
+
36
+ // Si l'utilisateur ne peut voir que ses propres contacts, vérifier l'assignation
37
+ if (!canViewAll && canViewOwn) {
38
+ const isAssigned =
39
+ contact.assignedCommercialId === session.user.id ||
40
+ contact.assignedTeleproId === session.user.id;
41
+ if (!isAssigned) {
42
+ return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
43
+ }
44
+ }
45
+
27
46
  const interactions = await prisma.interaction.findMany({
28
47
  where: { contactId: id },
29
48
  include: {
@@ -78,6 +97,24 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
78
97
  return NextResponse.json({ error: 'Contact non trouvé' }, { status: 404 });
79
98
  }
80
99
 
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
+ if (!canEditAll && !canEditOwn) {
105
+ return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
106
+ }
107
+
108
+ // Si l'utilisateur ne peut éditer que ses propres contacts, vérifier l'assignation
109
+ if (!canEditAll && canEditOwn) {
110
+ const isAssigned =
111
+ contact.assignedCommercialId === session.user.id ||
112
+ contact.assignedTeleproId === session.user.id;
113
+ if (!isAssigned) {
114
+ return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
115
+ }
116
+ }
117
+
81
118
  const interaction = await prisma.interaction.create({
82
119
  data: {
83
120
  contactId: id,
@@ -0,0 +1,71 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { auth } from '@/lib/auth';
3
+ import { prisma } from '@/lib/prisma';
4
+
5
+ // PUT /api/contacts/[id]/kyc - Mettre à jour les informations KYC d'un contact
6
+ export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
7
+ try {
8
+ const session = await auth.api.getSession({
9
+ headers: request.headers,
10
+ });
11
+
12
+ if (!session) {
13
+ return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
14
+ }
15
+
16
+ const { id } = await params;
17
+ const body = await request.json();
18
+
19
+ const { placeOfBirth, dateOfBirth, idType, idNumber, idExpiryDate, idDocumentUrl } = body;
20
+
21
+ // Vérifier que le contact existe
22
+ const contact = await prisma.contact.findUnique({
23
+ where: { id },
24
+ });
25
+
26
+ if (!contact) {
27
+ return NextResponse.json({ error: 'Contact non trouvé' }, { status: 404 });
28
+ }
29
+
30
+ // Mettre à jour les informations KYC
31
+ const updatedContact = await prisma.contact.update({
32
+ where: { id },
33
+ data: {
34
+ placeOfBirth: placeOfBirth || null,
35
+ dateOfBirth: dateOfBirth ? new Date(dateOfBirth) : null,
36
+ idType: idType || null,
37
+ idNumber: idNumber || null,
38
+ idExpiryDate: idExpiryDate ? new Date(idExpiryDate) : null,
39
+ idDocumentUrl: idDocumentUrl || null,
40
+ // Réinitialiser la vérification si les informations changent
41
+ idVerifiedByAdmin: false,
42
+ },
43
+ include: {
44
+ status: true,
45
+ assignedCommercial: {
46
+ select: {
47
+ id: true,
48
+ name: true,
49
+ email: true,
50
+ },
51
+ },
52
+ },
53
+ });
54
+
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
+ return NextResponse.json(updatedContact);
67
+ } catch (error: any) {
68
+ console.error('Erreur lors de la mise à jour des informations KYC:', error);
69
+ return NextResponse.json({ error: error.message || 'Erreur serveur' }, { status: 500 });
70
+ }
71
+ }
@@ -7,7 +7,7 @@ import {
7
7
  extractMeetLink,
8
8
  } from '@/lib/google-calendar';
9
9
  import nodemailer from 'nodemailer';
10
- import { decrypt } from '@/lib/encryption';
10
+ import { decrypt, encrypt } from '@/lib/encryption';
11
11
  import { render } from '@react-email/render';
12
12
  import { MeetConfirmationEmailTemplate } from '@/components/meet-confirmation-email-template';
13
13
  import React from 'react';
@@ -63,14 +63,18 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
63
63
  return NextResponse.json({ error: 'Contact non trouvé' }, { status: 404 });
64
64
  }
65
65
 
66
- // Vérifier que l'utilisateur a un compte Google connecté
67
- const googleAccount = await prisma.userGoogleAccount.findUnique({
68
- where: { userId: session.user.id },
69
- });
70
-
71
- if (!googleAccount) {
66
+ // Récupérer le compte Google de l'utilisateur courant
67
+ const { getUserGoogleAccount } = await import('@/lib/google-calendar');
68
+ let googleAccount;
69
+ try {
70
+ googleAccount = await getUserGoogleAccount(session.user.id);
71
+ } catch (error: any) {
72
72
  return NextResponse.json(
73
- { error: 'Veuillez connecter votre compte Google dans les paramètres' },
73
+ {
74
+ error:
75
+ error.message ||
76
+ 'Veuillez connecter votre compte Google dans les paramètres pour utiliser Google Calendar.',
77
+ },
74
78
  { status: 400 },
75
79
  );
76
80
  }
@@ -89,7 +93,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
89
93
  await prisma.userGoogleAccount.update({
90
94
  where: { userId: session.user.id },
91
95
  data: {
92
- accessToken,
96
+ accessToken: encrypt(accessToken),
93
97
  tokenExpiresAt,
94
98
  },
95
99
  });
@@ -112,28 +116,33 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
112
116
  });
113
117
 
114
118
  // Créer l'évènement Google Calendar avec Meet
115
- const googleEvent = await createGoogleCalendarEvent(accessToken, {
116
- summary: title,
117
- description: description || '',
118
- start: {
119
- dateTime: startDate.toISOString(),
120
- timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
121
- },
122
- end: {
123
- dateTime: endDate.toISOString(),
124
- timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
125
- },
126
- attendees: allAttendees.length > 0 ? allAttendees : undefined,
127
- conferenceData: {
128
- createRequest: {
129
- requestId: `meet-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
130
- conferenceSolutionKey: {
131
- type: 'hangoutsMeet',
119
+ let googleEvent;
120
+ try {
121
+ googleEvent = await createGoogleCalendarEvent(accessToken, {
122
+ summary: title,
123
+ description: description || '',
124
+ start: {
125
+ dateTime: startDate.toISOString(),
126
+ timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
127
+ },
128
+ end: {
129
+ dateTime: endDate.toISOString(),
130
+ timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
131
+ },
132
+ attendees: allAttendees.length > 0 ? allAttendees : undefined,
133
+ conferenceData: {
134
+ createRequest: {
135
+ requestId: `meet-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
136
+ conferenceSolutionKey: {
137
+ type: 'hangoutsMeet',
138
+ },
132
139
  },
133
140
  },
134
- },
135
- conferenceDataVersion: 1,
136
- });
141
+ conferenceDataVersion: 1,
142
+ });
143
+ } catch (error: any) {
144
+ throw error;
145
+ }
137
146
 
138
147
  const meetLink = extractMeetLink(googleEvent);
139
148