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
@@ -0,0 +1,164 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { auth } from '@/lib/auth';
3
+ import { prisma } from '@/lib/prisma';
4
+ import { REMINDERS_CLEAR_ALL_ID } from '@/lib/reminder-state';
5
+ import {
6
+ computeExpiresAtForUpsert,
7
+ getRequestId,
8
+ isMissingReminderStateTableError,
9
+ logReminderEvent,
10
+ reminderStateNotExpired,
11
+ } from '@/lib/reminder-state-server';
12
+
13
+ export async function GET(request: NextRequest) {
14
+ const started = Date.now();
15
+ const requestId = getRequestId(request);
16
+ let degraded = false;
17
+
18
+ try {
19
+ const session = await auth.api.getSession({ headers: request.headers });
20
+ if (!session) {
21
+ return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
22
+ }
23
+
24
+ const now = new Date();
25
+ let states: Array<{ reminderId: string; status: string; updatedAt: Date }> = [];
26
+ let clearAllState: { status: string; updatedAt: Date; clearedCutoffAt: Date | null } | undefined;
27
+
28
+ try {
29
+ const rows = await prisma.userReminderState.findMany({
30
+ where: {
31
+ userId: session.user.id,
32
+ AND: [reminderStateNotExpired(now)],
33
+ },
34
+ select: {
35
+ reminderId: true,
36
+ status: true,
37
+ updatedAt: true,
38
+ clearedCutoffAt: true,
39
+ },
40
+ });
41
+ states = rows;
42
+ clearAllState = rows.find((state) => state.reminderId === REMINDERS_CLEAR_ALL_ID);
43
+ } catch (err) {
44
+ if (isMissingReminderStateTableError(err)) {
45
+ degraded = true;
46
+ logReminderEvent({
47
+ event: 'reminders_fallback_missing_table',
48
+ requestId,
49
+ userId: session.user.id,
50
+ degraded: true,
51
+ durationMs: Date.now() - started,
52
+ });
53
+ } else {
54
+ throw err;
55
+ }
56
+ }
57
+
58
+ const clearedAtIso =
59
+ clearAllState?.status === 'CLEARED'
60
+ ? (clearAllState.clearedCutoffAt ?? clearAllState.updatedAt).toISOString()
61
+ : null;
62
+
63
+ logReminderEvent({
64
+ event: 'reminder_state_read',
65
+ requestId,
66
+ userId: session.user.id,
67
+ countReturned: states.filter((s) => s.reminderId !== REMINDERS_CLEAR_ALL_ID).length,
68
+ degraded,
69
+ durationMs: Date.now() - started,
70
+ });
71
+
72
+ return NextResponse.json({
73
+ states: states.filter((state) => state.reminderId !== REMINDERS_CLEAR_ALL_ID),
74
+ clearedAt: clearedAtIso,
75
+ degraded,
76
+ });
77
+ } catch (error: unknown) {
78
+ const msg = error instanceof Error ? error.message : 'Erreur serveur';
79
+ console.error("Erreur lors de la récupération de l'état des rappels:", error);
80
+ return NextResponse.json({ error: msg }, { status: 500 });
81
+ }
82
+ }
83
+
84
+ export async function POST(request: NextRequest) {
85
+ const started = Date.now();
86
+ const requestId = getRequestId(request);
87
+
88
+ try {
89
+ const session = await auth.api.getSession({ headers: request.headers });
90
+ if (!session) {
91
+ return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
92
+ }
93
+
94
+ const body = await request.json().catch(() => ({}));
95
+ const reminderId = typeof body.reminderId === 'string' ? body.reminderId.trim() : '';
96
+ const status = typeof body.status === 'string' ? body.status.trim().toUpperCase() : '';
97
+ if (!reminderId) {
98
+ return NextResponse.json({ error: 'reminderId requis.' }, { status: 400 });
99
+ }
100
+ if (!['READ', 'DISMISSED'].includes(status)) {
101
+ return NextResponse.json({ error: 'status invalide.' }, { status: 400 });
102
+ }
103
+
104
+ const now = new Date();
105
+ const expiresAt = computeExpiresAtForUpsert(reminderId, status as 'READ' | 'DISMISSED', now);
106
+
107
+ try {
108
+ const state = await prisma.userReminderState.upsert({
109
+ where: {
110
+ userId_reminderId: {
111
+ userId: session.user.id,
112
+ reminderId,
113
+ },
114
+ },
115
+ update: {
116
+ status: status as 'READ' | 'DISMISSED',
117
+ expiresAt,
118
+ },
119
+ create: {
120
+ userId: session.user.id,
121
+ reminderId,
122
+ status: status as 'READ' | 'DISMISSED',
123
+ expiresAt,
124
+ },
125
+ select: {
126
+ reminderId: true,
127
+ status: true,
128
+ updatedAt: true,
129
+ },
130
+ });
131
+
132
+ logReminderEvent({
133
+ event: 'reminder_state_upsert',
134
+ requestId,
135
+ userId: session.user.id,
136
+ degraded: false,
137
+ durationMs: Date.now() - started,
138
+ extra: { reminderId, status },
139
+ });
140
+
141
+ return NextResponse.json({ success: true, state, degraded: false });
142
+ } catch (err) {
143
+ if (isMissingReminderStateTableError(err)) {
144
+ logReminderEvent({
145
+ event: 'reminders_fallback_missing_table',
146
+ requestId,
147
+ userId: session.user.id,
148
+ degraded: true,
149
+ durationMs: Date.now() - started,
150
+ });
151
+ return NextResponse.json({
152
+ success: true,
153
+ state: { reminderId, status, updatedAt: now.toISOString() },
154
+ degraded: true,
155
+ });
156
+ }
157
+ throw err;
158
+ }
159
+ } catch (error: unknown) {
160
+ const msg = error instanceof Error ? error.message : 'Erreur serveur';
161
+ console.error("Erreur lors de la mise à jour de l'état rappel:", error);
162
+ return NextResponse.json({ error: msg }, { status: 500 });
163
+ }
164
+ }
@@ -46,21 +46,19 @@ export async function POST(request: NextRequest) {
46
46
  return NextResponse.json({ error: 'Utilisateur non trouvé' }, { status: 404 });
47
47
  }
48
48
 
49
- // Hasher le nouveau mot de passe
50
49
  const hashedPassword = await hashPassword(password);
51
50
 
52
- // Mettre à jour le mot de passe dans l'Account
53
- await prisma.account.update({
54
- where: { id: user.accounts[0].id },
55
- data: {
56
- password: hashedPassword,
57
- },
58
- });
59
-
60
- // Supprimer le token de vérification
61
- await prisma.verification.delete({
62
- where: { id: verification.id },
63
- });
51
+ await prisma.$transaction([
52
+ prisma.account.update({
53
+ where: { id: user.accounts[0].id },
54
+ data: {
55
+ password: hashedPassword,
56
+ },
57
+ }),
58
+ prisma.verification.delete({
59
+ where: { id: verification.id },
60
+ }),
61
+ ]);
64
62
 
65
63
  return NextResponse.json({
66
64
  success: true,
@@ -31,7 +31,7 @@ export async function POST(request: NextRequest) {
31
31
  }
32
32
 
33
33
  // Générer un code à 6 chiffres
34
- const code = Math.floor(100000 + Math.random() * 900000).toString();
34
+ const code = String(crypto.getRandomValues(new Uint32Array(1))[0] % 900000 + 100000);
35
35
 
36
36
  // Créer ou mettre à jour le token de vérification
37
37
  const expiresAt = new Date();
@@ -46,7 +46,7 @@ export async function POST(request: NextRequest) {
46
46
  // Générer un token pour la réinitialisation (différent du code)
47
47
  const resetToken = crypto.randomUUID();
48
48
  const expiresAt = new Date();
49
- expiresAt.setHours(expiresAt.getHours() + 1); // Valide 1 heure
49
+ expiresAt.setMinutes(expiresAt.getMinutes() + 30); // Valide 30 minutes
50
50
 
51
51
  // Supprimer l'ancien token de code
52
52
  await prisma.verification.delete({
@@ -4,12 +4,10 @@ import { ResetPasswordEmailTemplate } from '@/components/reset-password-email-te
4
4
  import { prisma } from '@/lib/prisma';
5
5
  import { decrypt } from '@/lib/encryption';
6
6
  import { auth } from '@/lib/auth';
7
+ import { checkPermission } from '@/lib/check-permission';
7
8
  import nodemailer from 'nodemailer';
8
- import { render } from '@react-email/render';
9
9
  import React from 'react';
10
10
 
11
-
12
-
13
11
  function htmlToText(html: string): string {
14
12
  if (!html) return '';
15
13
  return html
@@ -22,7 +20,6 @@ function htmlToText(html: string): string {
22
20
 
23
21
  async function getAdminSmtpConfig(userId: string) {
24
22
  try {
25
- // Récupérer la configuration SMTP de l'administrateur connecté
26
23
  const user = await prisma.user.findUnique({
27
24
  where: { id: userId },
28
25
  include: {
@@ -31,12 +28,10 @@ async function getAdminSmtpConfig(userId: string) {
31
28
  });
32
29
 
33
30
  if (!user) {
34
- console.error('❌ Utilisateur non trouvé:', userId);
35
31
  return { config: null, error: 'Utilisateur non trouvé' };
36
32
  }
37
33
 
38
34
  if (!user.smtpConfig) {
39
- console.error("❌ Aucune configuration SMTP trouvée pour l'utilisateur:", user.email);
40
35
  return {
41
36
  config: null,
42
37
  error:
@@ -56,8 +51,6 @@ async function getAdminSmtpConfig(userId: string) {
56
51
 
57
52
  async function getAnyAdminSmtpConfig() {
58
53
  try {
59
- // Récupérer la première configuration SMTP d'un administrateur
60
- // On cherche directement dans SmtpConfig et on joint avec User pour vérifier le rôle
61
54
  const smtpConfig = await prisma.smtpConfig.findFirst({
62
55
  where: {
63
56
  user: {
@@ -78,15 +71,6 @@ async function getAnyAdminSmtpConfig() {
78
71
  });
79
72
 
80
73
  if (!smtpConfig) {
81
- console.error('❌ Aucune configuration SMTP trouvée pour un administrateur');
82
-
83
- // Log supplémentaire pour debug : vérifier s'il y a des admins
84
- const adminCount = await prisma.user.count({
85
- where: { role: 'ADMIN' },
86
- });
87
- const smtpConfigCount = await prisma.smtpConfig.count();
88
- console.error('Debug - Admins:', adminCount, 'Configs SMTP:', smtpConfigCount);
89
-
90
74
  return {
91
75
  config: null,
92
76
  error: 'Aucune configuration SMTP trouvée. Veuillez configurer SMTP dans les paramètres.',
@@ -108,28 +92,29 @@ export async function POST(request: Request) {
108
92
  const body = await request.json();
109
93
  const { to, subject, template, ...emailData } = body;
110
94
 
111
- // Récupérer la session de l'utilisateur connecté (optionnel pour reset-password)
112
95
  const session = await auth.api.getSession({
113
96
  headers: request.headers,
114
97
  });
115
98
 
116
- // Pour le reset password, on n'a pas besoin de session
117
99
  const isResetPassword = template === 'reset-password';
118
100
 
119
101
  if (!isResetPassword && (!session || !session.user?.id)) {
120
- console.error('❌ Utilisateur non authentifié');
121
102
  return Response.json({ error: 'Non authentifié' }, { status: 401 });
122
103
  }
123
104
 
124
- // Récupérer la configuration SMTP
105
+ if (!isResetPassword) {
106
+ const canSendEmail = await checkPermission('contacts.send_email');
107
+ if (!canSendEmail) {
108
+ return Response.json({ error: 'Accès refusé' }, { status: 403 });
109
+ }
110
+ }
111
+
125
112
  let smtpConfig, smtpError;
126
113
  if (isResetPassword) {
127
- // Pour le reset password, utiliser n'importe quelle config SMTP d'admin
128
114
  const result = await getAnyAdminSmtpConfig();
129
115
  smtpConfig = result.config;
130
116
  smtpError = result.error;
131
117
  } else {
132
- // Pour les autres emails, utiliser la config de l'utilisateur connecté
133
118
  const result = await getAdminSmtpConfig(session!.user.id);
134
119
  smtpConfig = result.config;
135
120
  smtpError = result.error;
@@ -138,12 +123,13 @@ export async function POST(request: Request) {
138
123
  if (!smtpConfig) {
139
124
  const errorMsg =
140
125
  smtpError ||
141
- 'Aucune configuration SMTP trouvée. Veuillez configurer SMTP dans les paramètres.';
142
- console.error('❌', errorMsg);
143
- return Response.json({ error: errorMsg }, { status: 400 });
126
+ 'Aucune configuration SMTP trouvée. Veuillez configurer SMTP dans les paramètres pour finaliser votre action.';
127
+ return Response.json(
128
+ { error: errorMsg, configLink: '/settings?section=system' },
129
+ { status: 400 },
130
+ );
144
131
  }
145
132
 
146
- // Sélectionner le template approprié et le rendre en HTML
147
133
  let emailComponent: React.ReactElement;
148
134
  if (template === 'invitation') {
149
135
  emailComponent = React.createElement(InvitationEmailTemplate, {
@@ -163,23 +149,17 @@ export async function POST(request: Request) {
163
149
  });
164
150
  }
165
151
 
166
- // Rendre le composant React en HTML avec @react-email/render
152
+ const { render } = await import('@react-email/render');
167
153
  const emailHtml = await render(emailComponent);
168
154
  const emailText = htmlToText(emailHtml);
169
155
 
170
- // Déchiffrer le mot de passe SMTP
171
156
  let password: string;
172
157
  try {
173
158
  password = decrypt(smtpConfig.password);
174
- } catch (error) {
175
- // Si le déchiffrement échoue, utiliser le mot de passe tel quel (ancien format non chiffré)
176
- console.warn(
177
- '⚠️ Impossible de déchiffrer le mot de passe SMTP, utilisation du mot de passe brut',
178
- );
159
+ } catch {
179
160
  password = smtpConfig.password;
180
161
  }
181
162
 
182
- // Créer le transporteur SMTP
183
163
  const transporter = nodemailer.createTransport({
184
164
  host: smtpConfig.host,
185
165
  port: smtpConfig.port,
@@ -190,7 +170,6 @@ export async function POST(request: Request) {
190
170
  },
191
171
  });
192
172
 
193
- // Envoyer l'email
194
173
  const recipients = Array.isArray(to) ? to : [to];
195
174
  const mailOptions = {
196
175
  from: smtpConfig.fromName
@@ -209,21 +188,15 @@ export async function POST(request: Request) {
209
188
  message: 'Email envoyé avec succès',
210
189
  });
211
190
  } catch (error: any) {
212
- console.error("Erreur lors de l'envoi de l'email:", error);
213
- console.error("Détails de l'erreur:", {
214
- message: error.message,
215
- code: error.code,
216
- command: error.command,
217
- response: error.response,
218
- });
191
+ console.error("Erreur lors de l'envoi de l'email:", error);
219
192
 
220
- // Gérer les erreurs spécifiques de nodemailer
221
193
  if (error.code === 'EAUTH' || error.code === 'ECONNECTION') {
222
194
  return Response.json(
223
195
  {
224
196
  error:
225
- "Erreur d'authentification SMTP. Vérifiez votre configuration SMTP dans les paramètres.",
197
+ "Erreur d'authentification SMTP. Veuillez vérifier votre configuration SMTP pour finaliser votre action.",
226
198
  details: error.message,
199
+ configLink: '/settings?section=system',
227
200
  },
228
201
  { status: 400 },
229
202
  );
@@ -231,8 +204,13 @@ export async function POST(request: Request) {
231
204
 
232
205
  return Response.json(
233
206
  {
234
- error: error.message || "Erreur lors de l'envoi de l'email",
235
- details: error.code || 'UNKNOWN_ERROR',
207
+ error:
208
+ process.env.NODE_ENV === 'development'
209
+ ? error.message || "Erreur lors de l'envoi de l'email"
210
+ : "Erreur lors de l'envoi de l'email",
211
+ ...(process.env.NODE_ENV === 'development' && {
212
+ details: error.code || 'UNKNOWN_ERROR',
213
+ }),
236
214
  },
237
215
  { status: 500 },
238
216
  );
@@ -1,11 +1,15 @@
1
1
  import { NextRequest, NextResponse } from 'next/server';
2
2
  import { prisma } from '@/lib/prisma';
3
- import { requireAdmin } from '@/lib/roles';
3
+ import { checkPermission } from '@/lib/check-permission';
4
+ import { auth } from '@/lib/auth';
4
5
 
5
6
  // PUT /api/settings/closing-reasons/[id] - Mettre à jour un motif de fermeture (admin)
6
7
  export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
7
8
  try {
8
- await requireAdmin(request.headers);
9
+ const session = await auth.api.getSession({ headers: request.headers });
10
+ if (!session) return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
11
+ const canManage = await checkPermission('settings.closing_reasons.manage');
12
+ if (!canManage) return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
9
13
 
10
14
  const { id } = await params;
11
15
  const body = await request.json();
@@ -32,15 +36,6 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
32
36
  });
33
37
  } catch (error: any) {
34
38
  console.error('Erreur lors de la mise à jour du motif de fermeture:', error);
35
-
36
- if (error.message === 'Non authentifié') {
37
- return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
38
- }
39
-
40
- if (error.message === 'Permissions insuffisantes') {
41
- return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
42
- }
43
-
44
39
  return NextResponse.json({ error: error.message || 'Erreur serveur' }, { status: 500 });
45
40
  }
46
41
  }
@@ -51,7 +46,10 @@ export async function DELETE(
51
46
  { params }: { params: Promise<{ id: string }> },
52
47
  ) {
53
48
  try {
54
- await requireAdmin(request.headers);
49
+ const session = await auth.api.getSession({ headers: request.headers });
50
+ if (!session) return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
51
+ const canManage = await checkPermission('settings.closing_reasons.manage');
52
+ if (!canManage) return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
55
53
 
56
54
  const { id } = await params;
57
55
 
@@ -65,15 +63,6 @@ export async function DELETE(
65
63
  });
66
64
  } catch (error: any) {
67
65
  console.error('Erreur lors de la suppression du motif de fermeture:', error);
68
-
69
- if (error.message === 'Non authentifié') {
70
- return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
71
- }
72
-
73
- if (error.message === 'Permissions insuffisantes') {
74
- return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
75
- }
76
-
77
66
  return NextResponse.json({ error: error.message || 'Erreur serveur' }, { status: 500 });
78
67
  }
79
68
  }
@@ -1,11 +1,15 @@
1
1
  import { NextRequest, NextResponse } from 'next/server';
2
2
  import { prisma } from '@/lib/prisma';
3
- import { requireAdmin } from '@/lib/roles';
3
+ import { checkPermission } from '@/lib/check-permission';
4
+ import { auth } from '@/lib/auth';
4
5
 
5
6
  // GET /api/settings/closing-reasons - Récupérer tous les motifs de fermeture (admin)
6
7
  export async function GET(request: NextRequest) {
7
8
  try {
8
- await requireAdmin(request.headers);
9
+ const session = await auth.api.getSession({ headers: request.headers });
10
+ if (!session) return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
11
+ const canManage = await checkPermission('settings.closing_reasons.manage');
12
+ if (!canManage) return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
9
13
 
10
14
  const reasons = await prisma.closingReason.findMany({
11
15
  orderBy: { name: 'asc' },
@@ -14,15 +18,6 @@ export async function GET(request: NextRequest) {
14
18
  return NextResponse.json(reasons);
15
19
  } catch (error: any) {
16
20
  console.error('Erreur lors de la récupération des motifs de fermeture:', error);
17
-
18
- if (error.message === 'Non authentifié') {
19
- return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
20
- }
21
-
22
- if (error.message === 'Permissions insuffisantes') {
23
- return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
24
- }
25
-
26
21
  return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
27
22
  }
28
23
  }
@@ -30,7 +25,10 @@ export async function GET(request: NextRequest) {
30
25
  // POST /api/settings/closing-reasons - Créer un motif de fermeture (admin)
31
26
  export async function POST(request: NextRequest) {
32
27
  try {
33
- await requireAdmin(request.headers);
28
+ const session = await auth.api.getSession({ headers: request.headers });
29
+ if (!session) return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
30
+ const canManage = await checkPermission('settings.closing_reasons.manage');
31
+ if (!canManage) return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
34
32
 
35
33
  const body = await request.json();
36
34
  const { name } = body;
@@ -58,15 +56,6 @@ export async function POST(request: NextRequest) {
58
56
  );
59
57
  } catch (error: any) {
60
58
  console.error('Erreur lors de la création du motif de fermeture:', error);
61
-
62
- if (error.message === 'Non authentifié') {
63
- return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
64
- }
65
-
66
- if (error.message === 'Permissions insuffisantes') {
67
- return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
68
- }
69
-
70
59
  return NextResponse.json({ error: error.message || 'Erreur serveur' }, { status: 500 });
71
60
  }
72
61
  }
@@ -1,20 +1,24 @@
1
1
  import { NextRequest, NextResponse } from 'next/server';
2
2
  import { prisma } from '@/lib/prisma';
3
- import { requireAdmin } from '@/lib/roles';
3
+ import { checkPermission } from '@/lib/check-permission';
4
+ import { auth } from '@/lib/auth';
4
5
 
5
6
  // GET /api/settings/company - Récupérer les informations de l'entreprise
6
7
  export async function GET(request: NextRequest) {
7
8
  try {
8
- await requireAdmin(request.headers);
9
+ const session = await auth.api.getSession({ headers: request.headers });
10
+ if (!session) return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
11
+ const hasPermission = await checkPermission('settings.view');
12
+ if (!hasPermission) return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
9
13
 
10
14
  // Récupérer ou créer l'enregistrement de l'entreprise
11
- let company = await prisma.company.findUnique({
15
+ let company = await prisma.organization.findUnique({
12
16
  where: { id: 'company' },
13
17
  });
14
18
 
15
19
  // Si l'entreprise n'existe pas, la créer
16
20
  if (!company) {
17
- company = await prisma.company.create({
21
+ company = await prisma.organization.create({
18
22
  data: {
19
23
  id: 'company',
20
24
  },
@@ -24,15 +28,6 @@ export async function GET(request: NextRequest) {
24
28
  return NextResponse.json(company);
25
29
  } catch (error: any) {
26
30
  console.error("Erreur lors de la récupération des informations de l'entreprise:", error);
27
-
28
- if (error.message === 'Non authentifié') {
29
- return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
30
- }
31
-
32
- if (error.message === 'Permissions insuffisantes') {
33
- return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
34
- }
35
-
36
31
  return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
37
32
  }
38
33
  }
@@ -40,11 +35,15 @@ export async function GET(request: NextRequest) {
40
35
  // PUT /api/settings/company - Mettre à jour les informations de l'entreprise
41
36
  export async function PUT(request: NextRequest) {
42
37
  try {
43
- await requireAdmin(request.headers);
38
+ const session = await auth.api.getSession({ headers: request.headers });
39
+ if (!session) return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
40
+ const hasPermission = await checkPermission('settings.company.edit');
41
+ if (!hasPermission) return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
44
42
 
45
43
  const body = await request.json();
46
44
  const {
47
45
  name,
46
+ legalRepresentative,
48
47
  address,
49
48
  city,
50
49
  postalCode,
@@ -58,16 +57,17 @@ export async function PUT(request: NextRequest) {
58
57
  } = body;
59
58
 
60
59
  // Vérifier si l'entreprise existe
61
- let company = await prisma.company.findUnique({
60
+ let company = await prisma.organization.findUnique({
62
61
  where: { id: 'company' },
63
62
  });
64
63
 
65
64
  // Si l'entreprise n'existe pas, la créer
66
65
  if (!company) {
67
- company = await prisma.company.create({
66
+ company = await prisma.organization.create({
68
67
  data: {
69
68
  id: 'company',
70
69
  name,
70
+ legalRepresentative,
71
71
  address,
72
72
  city,
73
73
  postalCode,
@@ -82,10 +82,12 @@ export async function PUT(request: NextRequest) {
82
82
  });
83
83
  } else {
84
84
  // Mettre à jour l'entreprise
85
- company = await prisma.company.update({
85
+ company = await prisma.organization.update({
86
86
  where: { id: 'company' },
87
87
  data: {
88
88
  name: name !== undefined ? name : company.name,
89
+ legalRepresentative:
90
+ legalRepresentative !== undefined ? legalRepresentative : company.legalRepresentative,
89
91
  address: address !== undefined ? address : company.address,
90
92
  city: city !== undefined ? city : company.city,
91
93
  postalCode: postalCode !== undefined ? postalCode : company.postalCode,
@@ -107,15 +109,6 @@ export async function PUT(request: NextRequest) {
107
109
  });
108
110
  } catch (error: any) {
109
111
  console.error("Erreur lors de la mise à jour des informations de l'entreprise:", error);
110
-
111
- if (error.message === 'Non authentifié') {
112
- return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
113
- }
114
-
115
- if (error.message === 'Permissions insuffisantes') {
116
- return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
117
- }
118
-
119
112
  return NextResponse.json({ error: error.message || 'Erreur serveur' }, { status: 500 });
120
113
  }
121
114
  }