create-crm-tmp 1.1.3 → 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 (276) hide show
  1. package/bin/create-crm-tmp.js +56 -35
  2. package/package.json +1 -1
  3. package/template/.prettierignore +2 -0
  4. package/template/README.md +230 -115
  5. package/template/components.json +22 -0
  6. package/template/eslint.config.mjs +13 -0
  7. package/template/exemple-contacts.csv +54 -0
  8. package/template/next.config.ts +41 -1
  9. package/template/package.json +63 -15
  10. package/template/prisma/migrations/20260318095700_init_db/migration.sql +978 -0
  11. package/template/prisma/schema.prisma +311 -67
  12. package/template/src/app/(auth)/invite/[token]/page.tsx +28 -29
  13. package/template/src/app/(auth)/layout.tsx +1 -1
  14. package/template/src/app/(auth)/reset-password/complete/page.tsx +21 -27
  15. package/template/src/app/(auth)/reset-password/page.tsx +14 -10
  16. package/template/src/app/(auth)/reset-password/verify/page.tsx +14 -10
  17. package/template/src/app/(auth)/signin/page.tsx +34 -23
  18. package/template/src/app/(dashboard)/agenda/page.tsx +3655 -2357
  19. package/template/src/app/(dashboard)/automatisation/[id]/page.tsx +10 -7
  20. package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +609 -338
  21. package/template/src/app/(dashboard)/automatisation/new/page.tsx +11 -8
  22. package/template/src/app/(dashboard)/automatisation/page.tsx +463 -186
  23. package/template/src/app/(dashboard)/closing/page.tsx +517 -469
  24. package/template/src/app/(dashboard)/contacts/[id]/page.tsx +6151 -4210
  25. package/template/src/app/(dashboard)/contacts/companies/[id]/page.tsx +1702 -0
  26. package/template/src/app/(dashboard)/contacts/loading.tsx +13 -0
  27. package/template/src/app/(dashboard)/contacts/page.tsx +4124 -2130
  28. package/template/src/app/(dashboard)/dashboard/page.tsx +119 -105
  29. package/template/src/app/(dashboard)/dev/page.tsx +1291 -0
  30. package/template/src/app/(dashboard)/error.tsx +37 -0
  31. package/template/src/app/(dashboard)/layout.tsx +6 -2
  32. package/template/src/app/(dashboard)/loading.tsx +5 -0
  33. package/template/src/app/(dashboard)/settings/loading.tsx +19 -0
  34. package/template/src/app/(dashboard)/settings/page.tsx +1773 -3362
  35. package/template/src/app/(dashboard)/templates/page.tsx +504 -303
  36. package/template/src/app/(dashboard)/users/list/page.tsx +364 -355
  37. package/template/src/app/(dashboard)/users/page.tsx +279 -310
  38. package/template/src/app/(dashboard)/users/permissions/page.tsx +104 -99
  39. package/template/src/app/(dashboard)/users/roles/page.tsx +169 -140
  40. package/template/src/app/api/agenda/google-events/route.ts +92 -0
  41. package/template/src/app/api/audit-logs/route.ts +1 -1
  42. package/template/src/app/api/auth/check-active/route.ts +3 -2
  43. package/template/src/app/api/auth/google/callback/route.ts +8 -5
  44. package/template/src/app/api/auth/google/disconnect/route.ts +2 -2
  45. package/template/src/app/api/auth/google/route.ts +2 -1
  46. package/template/src/app/api/auth/google/status/route.ts +7 -31
  47. package/template/src/app/api/companies/[id]/activities/route.ts +129 -0
  48. package/template/src/app/api/companies/[id]/route.ts +194 -0
  49. package/template/src/app/api/companies/export/route.ts +206 -0
  50. package/template/src/app/api/companies/route.ts +196 -0
  51. package/template/src/app/api/contact-views/[id]/pin/route.ts +69 -0
  52. package/template/src/app/api/contact-views/[id]/route.ts +197 -0
  53. package/template/src/app/api/contact-views/route.ts +146 -0
  54. package/template/src/app/api/contacts/[id]/files/[fileId]/preview/route.ts +55 -0
  55. package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +20 -48
  56. package/template/src/app/api/contacts/[id]/files/route.ts +125 -186
  57. package/template/src/app/api/contacts/[id]/interactions/[interactionId]/route.ts +27 -1
  58. package/template/src/app/api/contacts/[id]/interactions/route.ts +45 -8
  59. package/template/src/app/api/contacts/[id]/kyc/route.ts +81 -0
  60. package/template/src/app/api/contacts/[id]/meet/route.ts +55 -29
  61. package/template/src/app/api/contacts/[id]/route.ts +184 -21
  62. package/template/src/app/api/contacts/[id]/send-email/route.ts +33 -11
  63. package/template/src/app/api/contacts/[id]/workflows/run/route.ts +67 -0
  64. package/template/src/app/api/contacts/export/route.ts +22 -31
  65. package/template/src/app/api/contacts/import/route.ts +77 -44
  66. package/template/src/app/api/contacts/import-preview/route.ts +139 -0
  67. package/template/src/app/api/contacts/origins/route.ts +63 -0
  68. package/template/src/app/api/contacts/route.ts +322 -57
  69. package/template/src/app/api/cron/cleanup-editor-images/route.ts +166 -0
  70. package/template/src/app/api/dashboard/stats/route.ts +9 -292
  71. package/template/src/app/api/dashboard/widgets/[id]/route.ts +0 -3
  72. package/template/src/app/api/dashboard/widgets/route.ts +19 -19
  73. package/template/src/app/api/dev/reminders/test/route.ts +114 -0
  74. package/template/src/app/api/editor/upload-image/route.ts +61 -0
  75. package/template/src/app/api/integrations/google-sheet/jobs/[jobId]/route.ts +47 -0
  76. package/template/src/app/api/integrations/google-sheet/jobs/usage/route.ts +50 -0
  77. package/template/src/app/api/integrations/google-sheet/sync/route.ts +28 -542
  78. package/template/src/app/api/invite/complete/route.ts +20 -23
  79. package/template/src/app/api/jobs/google-sheet/process/route.ts +84 -0
  80. package/template/src/app/api/jobs/google-sheet/schedule/route.ts +50 -0
  81. package/template/src/app/api/reminders/clear/route.ts +120 -0
  82. package/template/src/app/api/reminders/clear/undo/route.ts +112 -0
  83. package/template/src/app/api/reminders/route.ts +165 -39
  84. package/template/src/app/api/reminders/state/route.ts +164 -0
  85. package/template/src/app/api/reset-password/complete/route.ts +11 -13
  86. package/template/src/app/api/reset-password/request/route.ts +1 -1
  87. package/template/src/app/api/reset-password/verify/route.ts +1 -1
  88. package/template/src/app/api/send/route.ts +25 -47
  89. package/template/src/app/api/settings/closing-reasons/[id]/route.ts +10 -21
  90. package/template/src/app/api/settings/closing-reasons/route.ts +10 -21
  91. package/template/src/app/api/settings/company/route.ts +19 -26
  92. package/template/src/app/api/settings/google-ads/[id]/route.ts +20 -23
  93. package/template/src/app/api/settings/google-ads/route.ts +34 -23
  94. package/template/src/app/api/settings/google-calendar/calendars/route.ts +97 -0
  95. package/template/src/app/api/settings/google-calendar/route.ts +124 -0
  96. package/template/src/app/api/settings/google-sheet/[id]/route.ts +48 -23
  97. package/template/src/app/api/settings/google-sheet/auto-map/route.ts +56 -32
  98. package/template/src/app/api/settings/google-sheet/preview/route.ts +110 -0
  99. package/template/src/app/api/settings/google-sheet/route.ts +34 -23
  100. package/template/src/app/api/settings/integrations/logs/route.ts +93 -0
  101. package/template/src/app/api/settings/integrations/notifications/route.ts +67 -0
  102. package/template/src/app/api/settings/meta-leads/[id]/route.ts +20 -24
  103. package/template/src/app/api/settings/meta-leads/route.ts +34 -25
  104. package/template/src/app/api/settings/smtp/route.ts +53 -6
  105. package/template/src/app/api/settings/statuses/[id]/route.ts +29 -32
  106. package/template/src/app/api/settings/statuses/route.ts +24 -22
  107. package/template/src/app/api/statuses/route.ts +2 -5
  108. package/template/src/app/api/tasks/[id]/attendees/route.ts +36 -13
  109. package/template/src/app/api/tasks/[id]/route.ts +357 -145
  110. package/template/src/app/api/tasks/meet/route.ts +37 -26
  111. package/template/src/app/api/tasks/route.ts +201 -96
  112. package/template/src/app/api/templates/[id]/route.ts +22 -13
  113. package/template/src/app/api/templates/route.ts +22 -5
  114. package/template/src/app/api/users/[id]/resend-invite/route.ts +95 -0
  115. package/template/src/app/api/users/[id]/route.ts +22 -16
  116. package/template/src/app/api/users/commercials/route.ts +38 -0
  117. package/template/src/app/api/users/for-agenda/route.ts +1 -2
  118. package/template/src/app/api/users/list/route.ts +57 -19
  119. package/template/src/app/api/users/route.ts +89 -34
  120. package/template/src/app/api/webhooks/google-ads/route.ts +40 -1
  121. package/template/src/app/api/webhooks/meta-leads/route.ts +38 -1
  122. package/template/src/app/api/workflows/[id]/route.ts +29 -6
  123. package/template/src/app/api/workflows/process/route.ts +505 -170
  124. package/template/src/app/api/workflows/route.ts +42 -4
  125. package/template/src/app/globals.css +512 -32
  126. package/template/src/app/layout.tsx +28 -9
  127. package/template/src/app/page.tsx +37 -7
  128. package/template/src/components/address-autocomplete.tsx +233 -0
  129. package/template/src/components/config-error-alert.tsx +46 -0
  130. package/template/src/components/contacts/filter-bar.tsx +190 -0
  131. package/template/src/components/contacts/filter-builder.tsx +574 -0
  132. package/template/src/components/contacts/save-view-dialog.tsx +160 -0
  133. package/template/src/components/contacts/views-tab-bar.tsx +449 -0
  134. package/template/src/components/dashboard/activity-chart.tsx +6 -1
  135. package/template/src/components/dashboard/add-widget-dialog.tsx +13 -17
  136. package/template/src/components/dashboard/color-picker.tsx +7 -8
  137. package/template/src/components/dashboard/recent-activity.tsx +2 -5
  138. package/template/src/components/dashboard/stat-card.tsx +1 -3
  139. package/template/src/components/dashboard/status-distribution-chart.tsx +0 -1
  140. package/template/src/components/dashboard/top-contacts-list.tsx +7 -13
  141. package/template/src/components/dashboard/upcoming-tasks-list.tsx +2 -5
  142. package/template/src/components/dashboard/widget-wrapper.tsx +3 -6
  143. package/template/src/components/date-picker.tsx +399 -0
  144. package/template/src/components/editor/upload-editor-image.ts +42 -0
  145. package/template/src/components/editor.tsx +188 -35
  146. package/template/src/components/email-template.tsx +4 -2
  147. package/template/src/components/global-search.tsx +360 -0
  148. package/template/src/components/header.tsx +200 -107
  149. package/template/src/components/inactive-account-guard.tsx +58 -0
  150. package/template/src/components/integration-notifications-listener.tsx +12 -0
  151. package/template/src/components/invitation-email-template.tsx +4 -2
  152. package/template/src/components/lazy-editor.tsx +11 -0
  153. package/template/src/components/meet-cancellation-email-template.tsx +11 -3
  154. package/template/src/components/meet-confirmation-email-template.tsx +10 -3
  155. package/template/src/components/meet-update-email-template.tsx +10 -3
  156. package/template/src/components/page-header.tsx +19 -15
  157. package/template/src/components/protected-page.tsx +94 -0
  158. package/template/src/components/reset-password-email-template.tsx +4 -2
  159. package/template/src/components/settings/integrations/GoogleAdsIntegration.tsx +428 -0
  160. package/template/src/components/settings/integrations/GoogleSheetConfigMonitoringModal.tsx +680 -0
  161. package/template/src/components/settings/integrations/GoogleSheetIntegration.tsx +809 -0
  162. package/template/src/components/settings/integrations/ImportResultDialog.tsx +124 -0
  163. package/template/src/components/settings/integrations/IntegrationLogPanel.tsx +57 -0
  164. package/template/src/components/settings/integrations/IntegrationLogsTable.tsx +186 -0
  165. package/template/src/components/settings/integrations/MetaLeadIntegration.tsx +451 -0
  166. package/template/src/components/sidebar.tsx +117 -100
  167. package/template/src/components/skeleton.tsx +128 -45
  168. package/template/src/components/ui/accordion.tsx +64 -0
  169. package/template/src/components/ui/alert-dialog.tsx +139 -0
  170. package/template/src/components/ui/button.tsx +71 -0
  171. package/template/src/components/ui/components.tsx +1 -1
  172. package/template/src/components/ui/date-picker.tsx +422 -0
  173. package/template/src/components/ui/datetime-picker.tsx +338 -0
  174. package/template/src/components/ui/status-select.tsx +271 -0
  175. package/template/src/components/ui/tooltip.tsx +37 -0
  176. package/template/src/components/view-as-banner.tsx +1 -1
  177. package/template/src/components/view-as-modal.tsx +30 -19
  178. package/template/src/config/nav-pages.ts +108 -0
  179. package/template/src/contexts/app-toast-context.tsx +362 -0
  180. package/template/src/contexts/dashboard-theme-context.tsx +2 -7
  181. package/template/src/contexts/sidebar-context.tsx +27 -53
  182. package/template/src/contexts/task-reminder-context.tsx +134 -160
  183. package/template/src/contexts/view-as-context.tsx +32 -10
  184. package/template/src/hooks/use-alert.tsx +65 -0
  185. package/template/src/hooks/use-confirm.tsx +87 -0
  186. package/template/src/hooks/use-contact-views.ts +140 -0
  187. package/template/src/hooks/use-contacts.ts +69 -0
  188. package/template/src/hooks/use-fetch.ts +17 -0
  189. package/template/src/hooks/use-focus-trap.ts +73 -0
  190. package/template/src/hooks/use-statuses.ts +22 -0
  191. package/template/src/hooks/useIntegrationNotifications.ts +49 -0
  192. package/template/src/lib/address-api.ts +155 -0
  193. package/template/src/lib/auth.ts +8 -1
  194. package/template/src/lib/cache.ts +73 -0
  195. package/template/src/lib/check-permission.ts +12 -177
  196. package/template/src/lib/config-links.ts +14 -0
  197. package/template/src/lib/contact-duplicate.ts +79 -61
  198. package/template/src/lib/contact-interactions.ts +24 -22
  199. package/template/src/lib/contact-view-filters.ts +301 -0
  200. package/template/src/lib/contacts-list-url.ts +190 -0
  201. package/template/src/lib/dashboard-stats.ts +282 -0
  202. package/template/src/lib/dashboard-themes.ts +0 -5
  203. package/template/src/lib/date-utils.ts +176 -0
  204. package/template/src/lib/default-widgets.ts +0 -2
  205. package/template/src/lib/editor-html-image-dimensions.ts +172 -0
  206. package/template/src/lib/editor-image-limits.ts +19 -0
  207. package/template/src/lib/email-html-sanitize.ts +19 -0
  208. package/template/src/lib/encryption.ts +9 -6
  209. package/template/src/lib/fr-geography.ts +192 -0
  210. package/template/src/lib/get-auth-user.ts +25 -0
  211. package/template/src/lib/google-calendar-agenda.ts +201 -0
  212. package/template/src/lib/google-calendar.ts +309 -17
  213. package/template/src/lib/google-fetch.ts +63 -0
  214. package/template/src/lib/google-sheet-sync-jobs.ts +96 -0
  215. package/template/src/lib/google-sheet-sync-runner.ts +514 -0
  216. package/template/src/lib/integration-import-log.ts +21 -0
  217. package/template/src/lib/local-storage.ts +34 -0
  218. package/template/src/lib/permissions.ts +268 -40
  219. package/template/src/lib/prisma.ts +15 -12
  220. package/template/src/lib/qstash.ts +65 -0
  221. package/template/src/lib/reminder-state-server.ts +80 -0
  222. package/template/src/lib/reminder-state.ts +29 -0
  223. package/template/src/lib/roles.ts +12 -15
  224. package/template/src/lib/supabase-storage.ts +113 -0
  225. package/template/src/lib/template-variables.ts +204 -29
  226. package/template/src/lib/utils.ts +71 -11
  227. package/template/src/lib/widget-registry.ts +0 -4
  228. package/template/src/lib/workflow-executor.ts +391 -228
  229. package/template/src/proxy.ts +35 -73
  230. package/template/src/types/contact-views.ts +351 -0
  231. package/template/vercel.json +5 -0
  232. package/template/WORKFLOWS_CRON.md +0 -185
  233. package/template/prisma/migrations/20251126144728_init/migration.sql +0 -78
  234. package/template/prisma/migrations/20251126155204_add_user_roles/migration.sql +0 -5
  235. package/template/prisma/migrations/20251128095126_add_company_info/migration.sql +0 -19
  236. package/template/prisma/migrations/20251128123321_add_smtp_config/migration.sql +0 -22
  237. package/template/prisma/migrations/20251128132303_add_status/migration.sql +0 -23
  238. package/template/prisma/migrations/20251201102207_add_user_active/migration.sql +0 -75
  239. package/template/prisma/migrations/20251201105507_add_email_signature/migration.sql +0 -2
  240. package/template/prisma/migrations/20251201151122_add_tasks/migration.sql +0 -45
  241. package/template/prisma/migrations/20251202111854_add_task_reminder/migration.sql +0 -2
  242. package/template/prisma/migrations/20251202135859_add_google_meet_integration/migration.sql +0 -27
  243. package/template/prisma/migrations/20251203103317_add_meta_lead_integration/migration.sql +0 -20
  244. package/template/prisma/migrations/20251203104002_add_google_ads_integration/migration.sql +0 -18
  245. package/template/prisma/migrations/20251203112122_add_google_sheet_integration/migration.sql +0 -32
  246. package/template/prisma/migrations/20251203153853_allow_multiple_integration_configs/migration.sql +0 -20
  247. package/template/prisma/migrations/20251205141705_update_user_roles/migration.sql +0 -12
  248. package/template/prisma/migrations/20251205150000_add_commercial_and_telepro_assignment/migration.sql +0 -21
  249. package/template/prisma/migrations/20251205160000_add_interaction_logging/migration.sql +0 -11
  250. package/template/prisma/migrations/20251208090314_add_automatic_interaction_types/migration.sql +0 -12
  251. package/template/prisma/migrations/20251208094843_mg/migration.sql +0 -14
  252. package/template/prisma/migrations/20251208100000_add_company_support/migration.sql +0 -14
  253. package/template/prisma/migrations/20251208110000_add_templates/migration.sql +0 -26
  254. package/template/prisma/migrations/20251208141304_add_video_conference_task_type/migration.sql +0 -2
  255. package/template/prisma/migrations/20251209104759_add_internal_note_to_task/migration.sql +0 -2
  256. package/template/prisma/migrations/20251209134803_add_company_field/migration.sql +0 -2
  257. package/template/prisma/migrations/20251209150000_rename_company_to_company_name/migration.sql +0 -3
  258. package/template/prisma/migrations/20251209150016_add_email_tracking/migration.sql +0 -21
  259. package/template/prisma/migrations/20251209155908_add_notify_contact_to_task/migration.sql +0 -2
  260. package/template/prisma/migrations/20251210110019_add_appointment_types/migration.sql +0 -10
  261. package/template/prisma/migrations/20251210113928_add_contact_files/migration.sql +0 -26
  262. package/template/prisma/migrations/20251212132339_add_custom_roles/migration.sql +0 -24
  263. package/template/prisma/migrations/20251215104448_add_file_interaction_types/migration.sql +0 -11
  264. package/template/prisma/migrations/20251215145616_add_closing_reasons/migration.sql +0 -12
  265. package/template/prisma/migrations/20251216140850_add_log_users/migration.sql +0 -25
  266. package/template/prisma/migrations/20251216151000_rename_perdu_to_ferme/migration.sql +0 -8
  267. package/template/prisma/migrations/20251216162318_add_column_mappings_to_google_sheet/migration.sql +0 -2
  268. package/template/prisma/migrations/20251216185127_add_workflows/migration.sql +0 -80
  269. package/template/prisma/migrations/20251216192237_add_scheduled_workflow_actions/migration.sql +0 -32
  270. package/template/prisma/migrations/20251220000000_add_task_interaction_type/migration.sql +0 -4
  271. package/template/prisma/migrations/20251221000000_add_task_type/migration.sql +0 -3
  272. package/template/prisma/migrations/20251221000001_add_event_color/migration.sql +0 -23
  273. package/template/prisma/migrations/20260210114913_add_dashboard_widget/migration.sql +0 -20
  274. package/template/prisma/migrations/20260226093949_fix_cascade_on_user_delete/migration.sql +0 -69
  275. package/template/src/app/(dashboard)/users/layout.tsx +0 -30
  276. package/template/src/lib/google-drive.ts +0 -380
@@ -1,14 +1,15 @@
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
- import { createInteraction } from '@/lib/contact-interactions';
10
11
  import nodemailer from 'nodemailer';
11
- import { decrypt } from '@/lib/encryption';
12
+ import { decrypt, encrypt } from '@/lib/encryption';
12
13
  import { render } from '@react-email/render';
13
14
  import { MeetConfirmationEmailTemplate } from '@/components/meet-confirmation-email-template';
14
15
  import React from 'react';
@@ -37,6 +38,16 @@ export async function POST(request: NextRequest) {
37
38
  return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
38
39
  }
39
40
 
41
+ const [canCreate, canEditOwn, canEditAll] = await Promise.all([
42
+ checkPermission('tasks.create'),
43
+ checkPermission('tasks.edit_own'),
44
+ checkPermission('tasks.edit_all'),
45
+ ]);
46
+
47
+ if (!canCreate && !canEditOwn && !canEditAll) {
48
+ return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
49
+ }
50
+
40
51
  const body = await request.json();
41
52
  const {
42
53
  title,
@@ -48,6 +59,7 @@ export async function POST(request: NextRequest) {
48
59
  internalNote,
49
60
  contactId,
50
61
  addToGoogleCalendar = true,
62
+ googleCalendarId: bodyGoogleCalendarId,
51
63
  } = body;
52
64
 
53
65
  // Validation
@@ -58,13 +70,17 @@ export async function POST(request: NextRequest) {
58
70
  // Vérifier que l'utilisateur a un compte Google connecté seulement si on veut ajouter à Google Calendar
59
71
  let googleAccount = null;
60
72
  if (addToGoogleCalendar) {
61
- googleAccount = await prisma.userGoogleAccount.findUnique({
62
- where: { userId: session.user.id },
63
- });
64
-
65
- if (!googleAccount) {
73
+ try {
74
+ const { getUserGoogleAccount } = await import('@/lib/google-calendar');
75
+ googleAccount = await getUserGoogleAccount(session.user.id);
76
+ } catch (error: any) {
66
77
  return NextResponse.json(
67
- { error: 'Veuillez connecter votre compte Google dans les paramètres' },
78
+ {
79
+ error:
80
+ error.message ||
81
+ 'Veuillez connecter votre compte Google dans les paramètres pour créer une visioconférence.',
82
+ configLink: '/settings?section=integrations',
83
+ },
68
84
  { status: 400 },
69
85
  );
70
86
  }
@@ -81,6 +97,7 @@ export async function POST(request: NextRequest) {
81
97
 
82
98
  let googleEventId: string | null = null;
83
99
  let meetLink: string | null = null;
100
+ let storedGoogleCalendarId: string | null = null;
84
101
 
85
102
  // Créer l'évènement Google Calendar avec Meet seulement si demandé
86
103
  if (addToGoogleCalendar && googleAccount) {
@@ -99,14 +116,21 @@ export async function POST(request: NextRequest) {
99
116
  await prisma.userGoogleAccount.update({
100
117
  where: { userId: session.user.id },
101
118
  data: {
102
- accessToken,
119
+ accessToken: encrypt(accessToken),
103
120
  tokenExpiresAt,
104
121
  },
105
122
  });
106
123
  }
107
124
 
125
+ const targetCalendarId =
126
+ typeof bodyGoogleCalendarId === 'string' && bodyGoogleCalendarId.trim() !== ''
127
+ ? bodyGoogleCalendarId.trim()
128
+ : googleAccount.defaultGoogleCalendarId?.trim() || 'primary';
129
+
130
+ await assertWritableGoogleCalendar(accessToken, targetCalendarId);
131
+
108
132
  // Créer l'évènement Google Calendar avec Meet
109
- const googleEvent = await createGoogleCalendarEvent(accessToken, {
133
+ const googleEvent = await createGoogleCalendarEvent(accessToken, targetCalendarId, {
110
134
  summary: title,
111
135
  description: description || '',
112
136
  start: {
@@ -131,6 +155,7 @@ export async function POST(request: NextRequest) {
131
155
 
132
156
  googleEventId = googleEvent.id;
133
157
  meetLink = extractMeetLink(googleEvent);
158
+ storedGoogleCalendarId = targetCalendarId === 'primary' ? null : targetCalendarId;
134
159
  } catch (googleError: any) {
135
160
  console.error("Erreur lors de la création de l'évènement Google Calendar:", googleError);
136
161
  // On continue quand même la création de la tâche
@@ -149,6 +174,7 @@ export async function POST(request: NextRequest) {
149
174
  createdById: session.user.id,
150
175
  contactId: contactId || null,
151
176
  googleEventId: googleEventId,
177
+ googleCalendarId: storedGoogleCalendarId,
152
178
  googleMeetLink: meetLink,
153
179
  durationMinutes,
154
180
  internalNote: internalNote || null,
@@ -249,22 +275,7 @@ export async function POST(request: NextRequest) {
249
275
  }
250
276
  }
251
277
 
252
- // Si la tâche est liée à un contact, créer aussi une interaction
253
- if (contactId) {
254
- try {
255
- await createInteraction({
256
- contactId,
257
- type: 'APPOINTMENT_CREATED' as any,
258
- title: title || null,
259
- content: description || '',
260
- userId: session.user.id,
261
- date: startDate,
262
- });
263
- } catch (error) {
264
- // Ne pas faire échouer la création du Meet si l'interaction échoue
265
- console.error("Erreur lors de la création de l'interaction:", error);
266
- }
267
- }
278
+ // Pas d'interaction pour un Google Meet : affichage via la tâche tant qu'elle existe en base.
268
279
 
269
280
  return NextResponse.json(task, { status: 201 });
270
281
  } catch (error: any) {
@@ -1,13 +1,18 @@
1
1
  import { NextRequest, NextResponse } from 'next/server';
2
- import { auth } from '@/lib/auth';
3
2
  import { prisma } from '@/lib/prisma';
3
+ import { getAuthUser } from '@/lib/get-auth-user';
4
+ import { checkPermission } from '@/lib/check-permission';
4
5
  import { logAppointmentCreated, createInteraction } from '@/lib/contact-interactions';
5
6
  import nodemailer from 'nodemailer';
6
- import { decrypt } from '@/lib/encryption';
7
- import { render } from '@react-email/render';
7
+ import { decrypt, encrypt } from '@/lib/encryption';
8
8
  import { MeetConfirmationEmailTemplate } from '@/components/meet-confirmation-email-template';
9
9
  import React from 'react';
10
- import { createGoogleCalendarEvent, getValidAccessToken } from '@/lib/google-calendar';
10
+ import {
11
+ appendGoogleCalendarContactFooter,
12
+ assertWritableGoogleCalendar,
13
+ createGoogleCalendarEvent,
14
+ getValidAccessToken,
15
+ } from '@/lib/google-calendar';
11
16
 
12
17
  function htmlToText(html: string): string {
13
18
  if (!html) return '';
@@ -22,62 +27,81 @@ function htmlToText(html: string): string {
22
27
  // GET /api/tasks - Récupérer les tâches de l'utilisateur
23
28
  export async function GET(request: NextRequest) {
24
29
  try {
25
- const session = await auth.api.getSession({
26
- headers: request.headers,
27
- });
30
+ const authUser = await getAuthUser();
28
31
 
29
- if (!session) {
32
+ if (!authUser) {
30
33
  return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
31
34
  }
32
35
 
36
+ const { session, permissions } = authUser;
37
+ const canViewAll = permissions.includes('tasks.view_all');
38
+ const canViewOwn = permissions.includes('tasks.view_own');
39
+
40
+ if (!canViewAll && !canViewOwn) {
41
+ return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
42
+ }
43
+
33
44
  const { searchParams } = new URL(request.url);
34
45
  const startDate = searchParams.get('startDate');
35
46
  const endDate = searchParams.get('endDate');
36
- const assignedTo = searchParams.get('assignedTo'); // Pour les admins
37
- const contactId = searchParams.get('contactId'); // Filtrer par contact
47
+ const assignedTo = searchParams.get('assignedTo');
48
+ const contactId = searchParams.get('contactId');
38
49
  const showOtherUsers = searchParams.get('showOtherUsers') === 'true';
39
50
 
40
- // Construire les filtres
41
- const where: any = {
42
- scheduledAt: {
43
- gte: startDate ? new Date(startDate) : new Date(),
51
+ // Construire les filtres de base
52
+ // Pour les événements multi-jours, on récupère les tâches avec une marge
53
+ // Les tâches qui commencent jusqu'à 30 jours avant le début de la période
54
+ // peuvent potentiellement être affichées (événements longs)
55
+ const baseFilters: any = {};
56
+
57
+ if (startDate) {
58
+ const startDateObj = new Date(startDate);
59
+ // Marge de 30 jours avant pour capturer les événements longs
60
+ const marginStart = new Date(startDateObj);
61
+ marginStart.setDate(marginStart.getDate() - 30);
62
+
63
+ baseFilters.scheduledAt = {
64
+ gte: marginStart,
44
65
  lte: endDate ? new Date(endDate) : undefined,
45
- },
46
- };
66
+ };
67
+
68
+ if (!endDate) {
69
+ delete baseFilters.scheduledAt.lte;
70
+ }
71
+ } else {
72
+ baseFilters.scheduledAt = {
73
+ gte: new Date(),
74
+ lte: endDate ? new Date(endDate) : undefined,
75
+ };
76
+
77
+ if (!endDate) {
78
+ delete baseFilters.scheduledAt.lte;
79
+ }
80
+ }
47
81
 
48
82
  // Filtrer par contact si fourni
49
83
  if (contactId) {
50
- where.contactId = contactId;
84
+ baseFilters.contactId = contactId;
51
85
  }
52
86
 
53
- // Vérifier la permission pour voir les événements des autres utilisateurs
54
- if (showOtherUsers) {
55
- const { checkPermission } = await import('@/lib/check-permission');
56
- const hasPermission = await checkPermission('tasks.view_other_users_events');
57
- if (hasPermission) {
58
- // Ne pas filtrer par utilisateur, voir tous les événements
59
- delete where.assignedUserId;
60
- } else {
61
- // Pas de permission, voir uniquement ses propres tâches
62
- where.assignedUserId = session.user.id;
63
- }
64
- } else if (assignedTo && assignedTo !== session.user.id) {
65
- const { checkPermission } = await import('@/lib/check-permission');
66
- const canViewOthers = await checkPermission('tasks.view_other_users_events');
67
- if (canViewOthers) {
68
- where.assignedUserId = assignedTo;
69
- } else {
70
- // Pas de permission, voir uniquement ses propres tâches
71
- where.assignedUserId = session.user.id;
87
+ // Construire les filtres d'utilisateur basés sur les permissions
88
+ let userFilters: any = {};
89
+
90
+ if (showOtherUsers && permissions.includes('tasks.view_other_users_events')) {
91
+ userFilters = {};
92
+ } else if (canViewAll) {
93
+ if (assignedTo) {
94
+ userFilters = { assignedUserId: assignedTo };
72
95
  }
73
96
  } else {
74
- // Par défaut, voir ses propres tâches
75
- where.assignedUserId = session.user.id;
97
+ userFilters = { assignedUserId: session.user.id };
76
98
  }
77
99
 
78
- if (!endDate) {
79
- delete where.scheduledAt.lte;
80
- }
100
+ // Combiner tous les filtres
101
+ const where: any = {
102
+ ...baseFilters,
103
+ ...userFilters,
104
+ };
81
105
 
82
106
  const tasks = await prisma.task.findMany({
83
107
  where,
@@ -96,7 +120,6 @@ export async function GET(request: NextRequest) {
96
120
  id: true,
97
121
  name: true,
98
122
  email: true,
99
- eventColor: true,
100
123
  },
101
124
  },
102
125
  createdBy: {
@@ -115,22 +138,32 @@ export async function GET(request: NextRequest) {
115
138
  return NextResponse.json(tasks);
116
139
  } catch (error: any) {
117
140
  console.error('Erreur lors de la récupération des tâches:', error);
118
- return NextResponse.json({ error: error.message || 'Erreur serveur' }, { status: 500 });
141
+ return NextResponse.json(
142
+ {
143
+ error:
144
+ process.env.NODE_ENV === 'development'
145
+ ? error.message || 'Erreur serveur'
146
+ : 'Erreur serveur',
147
+ },
148
+ { status: 500 },
149
+ );
119
150
  }
120
151
  }
121
152
 
122
153
  // POST /api/tasks - Créer une nouvelle tâche
123
154
  export async function POST(request: NextRequest) {
124
155
  try {
125
- const session = await auth.api.getSession({
126
- headers: request.headers,
127
- });
156
+ const [authUser, body] = await Promise.all([getAuthUser(), request.json()]);
128
157
 
129
- if (!session) {
158
+ if (!authUser) {
130
159
  return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
131
160
  }
132
161
 
133
- const body = await request.json();
162
+ const { session } = authUser;
163
+
164
+ if (!authUser.permissions.includes('tasks.create')) {
165
+ return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
166
+ }
134
167
  const {
135
168
  type,
136
169
  title,
@@ -143,6 +176,13 @@ export async function POST(request: NextRequest) {
143
176
  internalNote,
144
177
  attendees = [],
145
178
  addToGoogleCalendar = true,
179
+ googleCalendarId: bodyGoogleCalendarId,
180
+ // Champs pour les rendez-vous physiques
181
+ location,
182
+ locationAddress,
183
+ locationCity,
184
+ locationPostalCode,
185
+ isAtHome = false,
146
186
  } = body;
147
187
 
148
188
  // Validation
@@ -153,20 +193,30 @@ export async function POST(request: NextRequest) {
153
193
  );
154
194
  }
155
195
 
156
- // Déterminer l'utilisateur assigné
196
+ // Déterminer l'utilisateur assigné (selon permissions d'assignation)
197
+ const canAssignFull = await checkPermission('tasks.assign');
198
+ const canAssignToSales = await checkPermission('tasks.assign_to_sales');
199
+
157
200
  let finalAssignedUserId: string;
158
- if (assignedUserId && assignedUserId !== session.user.id) {
159
- const { checkPermission: checkPerm } = await import('@/lib/check-permission');
160
- const canAssignOthers = await checkPerm('tasks.view_other_users_events');
161
- if (canAssignOthers) {
162
- // Utilisateur avec permission peut assigner à n'importe qui
201
+ if (assignedUserId && (canAssignFull || canAssignToSales)) {
202
+ if (canAssignFull) {
163
203
  finalAssignedUserId = assignedUserId;
164
204
  } else {
165
- // Pas de permission, s'assigne automatiquement
166
- finalAssignedUserId = session.user.id;
205
+ // assign_to_sales : uniquement COMMERCIAL ou TELEPRO
206
+ const target = await prisma.user.findUnique({
207
+ where: { id: assignedUserId },
208
+ select: { role: true },
209
+ });
210
+ if (!target || (target.role !== 'COMMERCIAL' && target.role !== 'TELEPRO')) {
211
+ return NextResponse.json(
212
+ { error: "Vous ne pouvez assigner une tâche qu'à un commercial ou un télépro" },
213
+ { status: 403 },
214
+ );
215
+ }
216
+ finalAssignedUserId = assignedUserId;
167
217
  }
168
218
  } else {
169
- finalAssignedUserId = assignedUserId || session.user.id;
219
+ finalAssignedUserId = session.user.id;
170
220
  }
171
221
 
172
222
  // Vérifier que le contact existe si fourni
@@ -193,6 +243,12 @@ export async function POST(request: NextRequest) {
193
243
  typeof reminderMinutesBefore === 'number' ? reminderMinutesBefore : null,
194
244
  notifyContact: notifyContact === true,
195
245
  internalNote: internalNote || null,
246
+ // Champs d'adresse pour les rendez-vous physiques
247
+ location: location || null,
248
+ locationAddress: locationAddress || null,
249
+ locationCity: locationCity || null,
250
+ locationPostalCode: locationPostalCode || null,
251
+ isAtHome: isAtHome === true,
196
252
  },
197
253
  include: {
198
254
  contact: {
@@ -209,7 +265,6 @@ export async function POST(request: NextRequest) {
209
265
  id: true,
210
266
  name: true,
211
267
  email: true,
212
- eventColor: true,
213
268
  },
214
269
  },
215
270
  createdBy: {
@@ -241,44 +296,79 @@ export async function POST(request: NextRequest) {
241
296
  // Si c'est un rendez-vous (physique) ou une tâche et que l'utilisateur veut l'ajouter à Google Calendar
242
297
  if ((type === 'MEETING' || type === 'TASK') && addToGoogleCalendar) {
243
298
  try {
244
- const googleAccount = await prisma.userGoogleAccount.findUnique({
245
- where: { userId: session.user.id },
246
- });
247
-
248
- if (googleAccount) {
249
- const accessToken = await getValidAccessToken(
250
- googleAccount.accessToken,
251
- googleAccount.refreshToken,
252
- googleAccount.tokenExpiresAt,
253
- );
254
-
255
- // Durée par défaut de 60 minutes pour les rendez-vous physiques, 30 minutes pour les tâches
256
- const startDate = new Date(scheduledAt);
257
- const duration = type === 'MEETING' ? 60 : 30;
258
- const endDate = new Date(startDate.getTime() + duration * 60 * 1000);
259
-
260
- const googleEvent = await createGoogleCalendarEvent(accessToken, {
261
- summary: title || (type === 'MEETING' ? 'Rendez-vous' : 'Tâche'),
262
- description: htmlToText(description),
263
- start: {
264
- dateTime: startDate.toISOString(),
265
- timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
266
- },
267
- end: {
268
- dateTime: endDate.toISOString(),
269
- timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
299
+ const { getUserGoogleAccount } = await import('@/lib/google-calendar');
300
+ const googleAccount = await getUserGoogleAccount(session.user.id);
301
+
302
+ const accessToken = await getValidAccessToken(
303
+ googleAccount.accessToken,
304
+ googleAccount.refreshToken,
305
+ googleAccount.tokenExpiresAt,
306
+ );
307
+
308
+ // Mettre à jour le token si nécessaire
309
+ if (accessToken !== googleAccount.accessToken) {
310
+ const tokenExpiresAt = new Date();
311
+ tokenExpiresAt.setSeconds(tokenExpiresAt.getSeconds() + 3600);
312
+ await prisma.userGoogleAccount.update({
313
+ where: { userId: session.user.id },
314
+ data: {
315
+ accessToken: encrypt(accessToken),
316
+ tokenExpiresAt,
270
317
  },
271
- attendees: allAttendees.length > 0 ? allAttendees : undefined,
272
318
  });
319
+ }
273
320
 
274
- // Sauvegarder l'ID de l'évènement pour synchroniser les modifications/suppressions
275
- await prisma.task.update({
276
- where: { id: task.id },
277
- data: {
278
- googleEventId: googleEvent.id,
279
- },
321
+ // Durée par défaut de 60 minutes pour les rendez-vous physiques, 30 minutes pour les tâches
322
+ const startDate = new Date(scheduledAt);
323
+ const duration = type === 'MEETING' ? 60 : 30;
324
+ const endDate = new Date(startDate.getTime() + duration * 60 * 1000);
325
+
326
+ const targetCalendarId =
327
+ typeof bodyGoogleCalendarId === 'string' && bodyGoogleCalendarId.trim() !== ''
328
+ ? bodyGoogleCalendarId.trim()
329
+ : googleAccount.defaultGoogleCalendarId?.trim() || 'primary';
330
+
331
+ await assertWritableGoogleCalendar(accessToken, targetCalendarId);
332
+
333
+ const googleAttendees =
334
+ type === 'TASK' && task.contact?.email
335
+ ? allAttendees.filter(
336
+ (a) => a.email.toLowerCase() !== task.contact!.email!.toLowerCase(),
337
+ )
338
+ : allAttendees;
339
+
340
+ let googleDescription = htmlToText(description);
341
+ if (type === 'TASK' && task.contact?.email) {
342
+ googleDescription = appendGoogleCalendarContactFooter(googleDescription, {
343
+ firstName: task.contact.firstName,
344
+ lastName: task.contact.lastName,
345
+ email: task.contact.email,
280
346
  });
281
347
  }
348
+
349
+ const googleEvent = await createGoogleCalendarEvent(accessToken, targetCalendarId, {
350
+ summary: title || (type === 'MEETING' ? 'Rendez-vous' : 'Tâche'),
351
+ description: googleDescription,
352
+ start: {
353
+ dateTime: startDate.toISOString(),
354
+ timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
355
+ },
356
+ end: {
357
+ dateTime: endDate.toISOString(),
358
+ timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
359
+ },
360
+ attendees: googleAttendees.length > 0 ? googleAttendees : undefined,
361
+ location: location || undefined, // Ajouter l'adresse du rendez-vous
362
+ });
363
+
364
+ // Sauvegarder l'ID de l'évènement pour synchroniser les modifications/suppressions
365
+ await prisma.task.update({
366
+ where: { id: task.id },
367
+ data: {
368
+ googleEventId: googleEvent.id,
369
+ googleCalendarId: targetCalendarId === 'primary' ? null : targetCalendarId,
370
+ },
371
+ });
282
372
  } catch (googleError: any) {
283
373
  console.error('Erreur lors de la création de lévènement Google Calendar:', googleError);
284
374
  // On ne bloque pas la création de la tâche si Google Calendar échoue
@@ -299,7 +389,8 @@ export async function POST(request: NextRequest) {
299
389
  );
300
390
 
301
391
  // Envoyer un email de notification si demandé (contact ou invités)
302
- if (notifyContact) {
392
+ // Vérifier explicitement que notifyContact est true
393
+ if (notifyContact === true) {
303
394
  try {
304
395
  // Récupérer la configuration SMTP
305
396
  const smtpConfig = await prisma.smtpConfig.findUnique({
@@ -312,6 +403,7 @@ export async function POST(request: NextRequest) {
312
403
  try {
313
404
  password = decrypt(smtpConfig.password);
314
405
  } catch (error) {
406
+ console.error(error);
315
407
  password = smtpConfig.password;
316
408
  }
317
409
 
@@ -358,6 +450,7 @@ export async function POST(request: NextRequest) {
358
450
  // Envoyer un email individuel à chaque destinataire
359
451
  for (const recipient of recipients) {
360
452
  try {
453
+ const { render } = await import('@react-email/render');
361
454
  // Générer le contenu HTML de l'email avec le composant React
362
455
  const emailComponent = React.createElement(MeetConfirmationEmailTemplate, {
363
456
  contactName: recipient.name,
@@ -378,7 +471,7 @@ export async function POST(request: NextRequest) {
378
471
  ? `"${smtpConfig.fromName}" <${smtpConfig.fromEmail}>`
379
472
  : smtpConfig.fromEmail,
380
473
  to: recipient.email,
381
- subject: `Confirmation de rendez-vous${title ? ` : ${title}` : ''}`,
474
+ subject: `Confirmation de rendez-vous ${title ? `: ${title}` : ''}`,
382
475
  text: emailText,
383
476
  html: emailHtml,
384
477
  });
@@ -396,8 +489,8 @@ export async function POST(request: NextRequest) {
396
489
  console.error("Erreur lors de l'envoi de l'email de notification:", emailError);
397
490
  }
398
491
  }
399
- } else {
400
- // Pour les autres types de tâches, créer une interaction de type TASK
492
+ } else if (type !== 'VIDEO_CONFERENCE') {
493
+ // Pas d'interaction pour la visio (annulable) : le fil d'activité lit la tâche en base.
401
494
  await createInteraction({
402
495
  contactId,
403
496
  type: 'TASK' as any,
@@ -405,6 +498,10 @@ export async function POST(request: NextRequest) {
405
498
  content: description,
406
499
  userId: session.user.id,
407
500
  date: new Date(scheduledAt),
501
+ metadata: {
502
+ taskId: task.id,
503
+ scheduledAt: new Date(scheduledAt).toISOString(),
504
+ },
408
505
  });
409
506
  }
410
507
  } catch (error) {
@@ -416,6 +513,14 @@ export async function POST(request: NextRequest) {
416
513
  return NextResponse.json(task, { status: 201 });
417
514
  } catch (error: any) {
418
515
  console.error('Erreur lors de la création de la tâche:', error);
419
- return NextResponse.json({ error: error.message || 'Erreur serveur' }, { status: 500 });
516
+ return NextResponse.json(
517
+ {
518
+ error:
519
+ process.env.NODE_ENV === 'development'
520
+ ? error.message || 'Erreur serveur'
521
+ : 'Erreur serveur',
522
+ },
523
+ { status: 500 },
524
+ );
420
525
  }
421
526
  }