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,304 +1,21 @@
1
- import { NextRequest, NextResponse } from 'next/server';
2
- import { auth } from '@/lib/auth';
3
- import { headers } from 'next/headers';
4
- import { prisma } from '@/lib/prisma';
5
- import { checkPermission } from '@/lib/check-permission';
1
+ import { NextResponse } from 'next/server';
2
+ import { getAuthUser } from '@/lib/get-auth-user';
3
+ import { getDashboardStats } from '@/lib/dashboard-stats';
6
4
 
7
- export async function GET(req: NextRequest) {
5
+ export async function GET() {
8
6
  try {
9
- const session = await auth.api.getSession({
10
- headers: await headers(),
11
- });
7
+ const authUser = await getAuthUser();
12
8
 
13
- if (!session) {
9
+ if (!authUser) {
14
10
  return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
15
11
  }
16
12
 
17
- // Vérifier la permission d'accès au tableau de bord
18
- const canView = await checkPermission('dashboard.view');
19
- if (!canView) {
13
+ if (!authUser.permissions.includes('dashboard.view')) {
20
14
  return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
21
15
  }
22
16
 
23
- const userId = session.user.id;
24
- const userRole = session.user.role;
25
-
26
- // Déterminer les contacts visibles selon le rôle
27
- const contactFilter =
28
- userRole === 'ADMIN'
29
- ? {}
30
- : userRole === 'MANAGER'
31
- ? {}
32
- : userRole === 'COMMERCIAL'
33
- ? { assignedCommercialId: userId }
34
- : userRole === 'TELEPRO'
35
- ? { assignedTeleproId: userId }
36
- : { createdById: userId };
37
-
38
- // 1. Total des contacts
39
- const totalContacts = await prisma.contact.count({
40
- where: contactFilter,
41
- });
42
-
43
- // 2. Contacts créés ce mois
44
- const startOfMonth = new Date();
45
- startOfMonth.setDate(1);
46
- startOfMonth.setHours(0, 0, 0, 0);
47
-
48
- const contactsThisMonth = await prisma.contact.count({
49
- where: {
50
- ...contactFilter,
51
- createdAt: { gte: startOfMonth },
52
- },
53
- });
54
-
55
- const lastMonthStart = new Date(startOfMonth);
56
- lastMonthStart.setMonth(lastMonthStart.getMonth() - 1);
57
-
58
- const contactsLastMonth = await prisma.contact.count({
59
- where: {
60
- ...contactFilter,
61
- createdAt: {
62
- gte: lastMonthStart,
63
- lt: startOfMonth,
64
- },
65
- },
66
- });
67
-
68
- const contactsGrowth =
69
- contactsLastMonth > 0
70
- ? ((contactsThisMonth - contactsLastMonth) / contactsLastMonth) * 100
71
- : 0;
72
-
73
- // 3. Contacts par mois (12 derniers mois)
74
- const monthsData = [];
75
- for (let i = 11; i >= 0; i--) {
76
- const monthStart = new Date();
77
- monthStart.setMonth(monthStart.getMonth() - i);
78
- monthStart.setDate(1);
79
- monthStart.setHours(0, 0, 0, 0);
80
-
81
- const monthEnd = new Date(monthStart);
82
- monthEnd.setMonth(monthEnd.getMonth() + 1);
83
-
84
- const count = await prisma.contact.count({
85
- where: {
86
- ...contactFilter,
87
- createdAt: {
88
- gte: monthStart,
89
- lt: monthEnd,
90
- },
91
- },
92
- });
93
-
94
- monthsData.push({
95
- month: monthStart.toLocaleString('fr-FR', { month: 'short' }),
96
- count,
97
- });
98
- }
99
-
100
- // 4. Répartition par statut (pour le radar chart)
101
- const statusDistribution = await prisma.contact.groupBy({
102
- by: ['statusId'],
103
- where: contactFilter,
104
- _count: true,
105
- });
106
-
107
- const statuses = await prisma.status.findMany();
108
- const statusData = statuses.map((status) => ({
109
- name: status.name,
110
- value: statusDistribution.find((s) => s.statusId === status.id)?._count || 0,
111
- }));
112
-
113
- // 5. Tâches à venir (Top tasks)
114
- const upcomingTasks = await prisma.task.findMany({
115
- where: {
116
- assignedUserId: userId,
117
- completed: false,
118
- scheduledAt: { gte: new Date() },
119
- },
120
- include: {
121
- contact: true,
122
- assignedUser: true,
123
- },
124
- orderBy: { scheduledAt: 'asc' },
125
- take: 6,
126
- });
127
-
128
- // 6. Interactions récentes
129
- const recentInteractions = await prisma.interaction.findMany({
130
- where: {
131
- userId,
132
- },
133
- include: {
134
- contact: true,
135
- user: true,
136
- },
137
- orderBy: { createdAt: 'desc' },
138
- take: 5,
139
- });
140
-
141
- // 7. Statistiques des tâches
142
- const totalTasks = await prisma.task.count({
143
- where: { assignedUserId: userId },
144
- });
145
-
146
- const completedTasks = await prisma.task.count({
147
- where: {
148
- assignedUserId: userId,
149
- completed: true,
150
- },
151
- });
152
-
153
- const pendingTasks = totalTasks - completedTasks;
154
-
155
- // 8. Tâches par type ce mois
156
- const tasksThisMonthByType = await prisma.task.groupBy({
157
- by: ['type'],
158
- where: {
159
- assignedUserId: userId,
160
- createdAt: { gte: startOfMonth },
161
- },
162
- _count: true,
163
- });
164
-
165
- const tasksByType = tasksThisMonthByType.map((t) => ({
166
- type: t.type,
167
- count: t._count,
168
- }));
169
-
170
- // 9. Interactions par type ce mois
171
- const interactionsThisMonth = await prisma.interaction.groupBy({
172
- by: ['type'],
173
- where: {
174
- userId,
175
- createdAt: { gte: startOfMonth },
176
- },
177
- _count: true,
178
- });
179
-
180
- const interactionsByType = interactionsThisMonth.map((i) => ({
181
- type: i.type,
182
- count: i._count,
183
- }));
184
-
185
- // 10. Activité des 7 derniers jours
186
- const last7Days = [];
187
- for (let i = 6; i >= 0; i--) {
188
- const dayStart = new Date();
189
- dayStart.setDate(dayStart.getDate() - i);
190
- dayStart.setHours(0, 0, 0, 0);
191
-
192
- const dayEnd = new Date(dayStart);
193
- dayEnd.setDate(dayEnd.getDate() + 1);
194
-
195
- const interactionsCount = await prisma.interaction.count({
196
- where: {
197
- userId,
198
- createdAt: {
199
- gte: dayStart,
200
- lt: dayEnd,
201
- },
202
- },
203
- });
204
-
205
- const tasksCount = await prisma.task.count({
206
- where: {
207
- assignedUserId: userId,
208
- createdAt: {
209
- gte: dayStart,
210
- lt: dayEnd,
211
- },
212
- },
213
- });
214
-
215
- last7Days.push({
216
- date: dayStart.toLocaleDateString('fr-FR', {
217
- day: 'numeric',
218
- month: 'short',
219
- }),
220
- interactions: interactionsCount,
221
- tasks: tasksCount,
222
- });
223
- }
224
-
225
- // 11. Top contacts (ceux avec le plus d'interactions)
226
- const topContacts = await prisma.contact.findMany({
227
- where: contactFilter,
228
- include: {
229
- _count: {
230
- select: { interactions: true },
231
- },
232
- status: true,
233
- assignedCommercial: true,
234
- assignedTelepro: true,
235
- },
236
- orderBy: {
237
- createdAt: 'desc',
238
- },
239
- take: 4,
240
- });
241
-
242
- return NextResponse.json({
243
- overview: {
244
- totalContacts,
245
- contactsThisMonth,
246
- contactsGrowth: Math.round(contactsGrowth * 10) / 10,
247
- monthsData,
248
- },
249
- statusDistribution: statusData,
250
- tasks: {
251
- total: totalTasks,
252
- completed: completedTasks,
253
- pending: pendingTasks,
254
- upcoming: upcomingTasks.map((task) => ({
255
- id: task.id,
256
- title: task.title || 'Sans titre',
257
- description: task.description,
258
- type: task.type,
259
- scheduledAt: task.scheduledAt,
260
- contact: task.contact
261
- ? {
262
- id: task.contact.id,
263
- name:
264
- `${task.contact.firstName || ''} ${task.contact.lastName || ''}`.trim() ||
265
- task.contact.phone,
266
- }
267
- : null,
268
- priority: task.priority,
269
- })),
270
- byType: tasksByType,
271
- },
272
- interactions: {
273
- recent: recentInteractions.map((interaction) => ({
274
- id: interaction.id,
275
- type: interaction.type,
276
- title: interaction.title,
277
- content: interaction.content,
278
- date: interaction.date || interaction.createdAt,
279
- contact: {
280
- id: interaction.contact.id,
281
- name:
282
- `${interaction.contact.firstName || ''} ${interaction.contact.lastName || ''}`.trim() ||
283
- interaction.contact.phone,
284
- },
285
- })),
286
- byType: interactionsByType,
287
- },
288
- activity: {
289
- last7Days,
290
- },
291
- topContacts: topContacts.map((contact) => ({
292
- id: contact.id,
293
- name: `${contact.firstName || ''} ${contact.lastName || ''}`.trim() || contact.phone,
294
- phone: contact.phone,
295
- email: contact.email,
296
- status: contact.status?.name || 'Non défini',
297
- interactionsCount: contact._count.interactions,
298
- assignedCommercial: contact.assignedCommercial?.name,
299
- assignedTelepro: contact.assignedTelepro?.name,
300
- })),
301
- });
17
+ const stats = await getDashboardStats(authUser.session.user.id, authUser.permissions);
18
+ return NextResponse.json(stats);
302
19
  } catch (error) {
303
20
  console.error('Erreur lors de la récupération des statistiques:', error);
304
21
  return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
@@ -4,7 +4,6 @@ import { headers } from 'next/headers';
4
4
  import { prisma } from '@/lib/prisma';
5
5
  import { checkPermission } from '@/lib/check-permission';
6
6
 
7
- // DELETE - Supprimer un widget
8
7
  export async function DELETE(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
9
8
  try {
10
9
  const session = await auth.api.getSession({
@@ -15,7 +14,6 @@ export async function DELETE(req: NextRequest, { params }: { params: Promise<{ i
15
14
  return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
16
15
  }
17
16
 
18
- // Vérifier la permission de gestion des widgets
19
17
  const canManage = await checkPermission('dashboard.widgets.manage');
20
18
  if (!canManage) {
21
19
  return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
@@ -23,7 +21,6 @@ export async function DELETE(req: NextRequest, { params }: { params: Promise<{ i
23
21
 
24
22
  const { id } = await params;
25
23
 
26
- // Vérifier que le widget appartient à l'utilisateur
27
24
  const widget = await prisma.dashboardWidget.findFirst({
28
25
  where: {
29
26
  id,
@@ -5,7 +5,6 @@ import { prisma } from '@/lib/prisma';
5
5
  import { DEFAULT_WIDGETS } from '@/lib/default-widgets';
6
6
  import { checkPermission } from '@/lib/check-permission';
7
7
 
8
- // GET - Récupérer les widgets de l'utilisateur
9
8
  export async function GET() {
10
9
  try {
11
10
  const session = await auth.api.getSession({
@@ -16,17 +15,33 @@ export async function GET() {
16
15
  return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
17
16
  }
18
17
 
19
- // Vérifier la permission d'accès au tableau de bord
20
18
  const canView = await checkPermission('dashboard.view');
21
19
  if (!canView) {
22
20
  return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
23
21
  }
24
22
 
25
- const widgets = await prisma.dashboardWidget.findMany({
23
+ let widgets = await prisma.dashboardWidget.findMany({
26
24
  where: { userId: session.user.id },
27
25
  orderBy: [{ y: 'asc' }, { x: 'asc' }],
28
26
  });
29
27
 
28
+ if (widgets.length === 0) {
29
+ widgets = await prisma.$transaction(
30
+ DEFAULT_WIDGETS.map((dw) =>
31
+ prisma.dashboardWidget.create({
32
+ data: {
33
+ userId: session.user.id,
34
+ type: dw.type,
35
+ x: dw.x,
36
+ y: dw.y,
37
+ w: dw.w,
38
+ h: dw.h,
39
+ },
40
+ }),
41
+ ),
42
+ );
43
+ }
44
+
30
45
  return NextResponse.json(widgets);
31
46
  } catch (error) {
32
47
  console.error('Erreur lors de la récupération des widgets:', error);
@@ -34,7 +49,6 @@ export async function GET() {
34
49
  }
35
50
  }
36
51
 
37
- // POST - Créer un widget (ou initialiser le layout par défaut)
38
52
  export async function POST(req: NextRequest) {
39
53
  try {
40
54
  const session = await auth.api.getSession({
@@ -45,7 +59,6 @@ export async function POST(req: NextRequest) {
45
59
  return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
46
60
  }
47
61
 
48
- // Vérifier la permission de gestion des widgets
49
62
  const canManage = await checkPermission('dashboard.widgets.manage');
50
63
  if (!canManage) {
51
64
  return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
@@ -53,20 +66,11 @@ export async function POST(req: NextRequest) {
53
66
 
54
67
  const body = await req.json();
55
68
 
56
- // Si c'est une initialisation par défaut
57
69
  if (body.initDefault) {
58
- const existing = await prisma.dashboardWidget.count({
70
+ await prisma.dashboardWidget.deleteMany({
59
71
  where: { userId: session.user.id },
60
72
  });
61
73
 
62
- if (existing > 0) {
63
- const widgets = await prisma.dashboardWidget.findMany({
64
- where: { userId: session.user.id },
65
- orderBy: [{ y: 'asc' }, { x: 'asc' }],
66
- });
67
- return NextResponse.json(widgets);
68
- }
69
-
70
74
  const created = await prisma.$transaction(
71
75
  DEFAULT_WIDGETS.map((dw) =>
72
76
  prisma.dashboardWidget.create({
@@ -85,7 +89,6 @@ export async function POST(req: NextRequest) {
85
89
  return NextResponse.json(created, { status: 201 });
86
90
  }
87
91
 
88
- // Création d'un widget unique
89
92
  const {
90
93
  type,
91
94
  x,
@@ -104,7 +107,6 @@ export async function POST(req: NextRequest) {
104
107
  return NextResponse.json({ error: 'Le type de widget est requis' }, { status: 400 });
105
108
  }
106
109
 
107
- // Calculer la position Y max pour placer le nouveau widget en bas
108
110
  const lastWidget = await prisma.dashboardWidget.findFirst({
109
111
  where: { userId: session.user.id },
110
112
  orderBy: { y: 'desc' },
@@ -130,7 +132,6 @@ export async function POST(req: NextRequest) {
130
132
  }
131
133
  }
132
134
 
133
- // PUT - Mettre à jour les positions des widgets (batch)
134
135
  export async function PUT(req: NextRequest) {
135
136
  try {
136
137
  const session = await auth.api.getSession({
@@ -141,7 +142,6 @@ export async function PUT(req: NextRequest) {
141
142
  return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
142
143
  }
143
144
 
144
- // Vérifier la permission de gestion des widgets
145
145
  const canManage = await checkPermission('dashboard.widgets.manage');
146
146
  if (!canManage) {
147
147
  return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
@@ -0,0 +1,114 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { auth } from '@/lib/auth';
3
+ import { prisma } from '@/lib/prisma';
4
+
5
+ type TestReminderBody = {
6
+ title?: string;
7
+ description?: string;
8
+ scheduledAt?: string;
9
+ reminderMinutesBefore?: number;
10
+ sendNow?: boolean;
11
+ contactId?: string;
12
+ };
13
+
14
+ // WARNING: This endpoint is for local development only.
15
+ // It is protected by NODE_ENV and an optional DEV_API_SECRET.
16
+ // Never expose dev endpoints in production builds.
17
+ export async function POST(request: NextRequest) {
18
+ if (
19
+ process.env.NODE_ENV !== 'development' ||
20
+ (process.env.DEV_API_SECRET &&
21
+ process.env.DEV_API_SECRET !== request.headers.get('x-dev-secret'))
22
+ ) {
23
+ return NextResponse.json({ error: 'Not found' }, { status: 404 });
24
+ }
25
+
26
+ try {
27
+ const session = await auth.api.getSession({ headers: request.headers });
28
+ if (!session) {
29
+ return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
30
+ }
31
+
32
+ const body = (await request.json().catch(() => ({}))) as TestReminderBody;
33
+ const title = body.title?.trim() || 'Rappel de test';
34
+ const description = body.description?.trim() || 'Rappel généré en mode développement.';
35
+ const reminderMinutesBefore =
36
+ typeof body.reminderMinutesBefore === 'number' &&
37
+ Number.isInteger(body.reminderMinutesBefore) &&
38
+ body.reminderMinutesBefore > 0
39
+ ? body.reminderMinutesBefore
40
+ : 5;
41
+
42
+ const parsedDate = body.scheduledAt ? new Date(body.scheduledAt) : null;
43
+ const scheduledAt =
44
+ parsedDate && !Number.isNaN(parsedDate.getTime())
45
+ ? parsedDate
46
+ : new Date(Date.now() + (reminderMinutesBefore + 1) * 60 * 1000);
47
+
48
+ const normalizedContactId =
49
+ typeof body.contactId === 'string' && body.contactId.trim() !== '' ? body.contactId.trim() : null;
50
+ let contact:
51
+ | {
52
+ id: string;
53
+ firstName: string | null;
54
+ lastName: string | null;
55
+ }
56
+ | null = null;
57
+ if (normalizedContactId) {
58
+ contact = await prisma.contact.findUnique({
59
+ where: { id: normalizedContactId },
60
+ select: { id: true, firstName: true, lastName: true },
61
+ });
62
+ if (!contact) {
63
+ return NextResponse.json({ error: 'Contact introuvable.' }, { status: 404 });
64
+ }
65
+ }
66
+
67
+ const task = await prisma.task.create({
68
+ data: {
69
+ type: 'OTHER',
70
+ title,
71
+ description,
72
+ priority: 'MEDIUM',
73
+ scheduledAt,
74
+ reminderMinutesBefore,
75
+ assignedUserId: session.user.id,
76
+ createdById: session.user.id,
77
+ contactId: contact?.id ?? null,
78
+ },
79
+ select: {
80
+ id: true,
81
+ title: true,
82
+ scheduledAt: true,
83
+ reminderMinutesBefore: true,
84
+ contact: {
85
+ select: {
86
+ id: true,
87
+ firstName: true,
88
+ lastName: true,
89
+ },
90
+ },
91
+ },
92
+ });
93
+
94
+ const sendNow = body.sendNow === true;
95
+ const immediateNotification = sendNow
96
+ ? {
97
+ tone: 'warning' as const,
98
+ message: `Test immédiat: ${task.title || 'Rappel'} (${task.reminderMinutesBefore} min avant)`,
99
+ link: task.contact ? `/contacts/${task.contact.id}` : undefined,
100
+ actionLabel: task.contact ? 'Ouvrir le contact' : undefined,
101
+ }
102
+ : null;
103
+
104
+ return NextResponse.json({
105
+ success: true,
106
+ task,
107
+ immediateNotification,
108
+ });
109
+ } catch (error: any) {
110
+ console.error('Erreur lors de la création du rappel de test:', error);
111
+ return NextResponse.json({ error: error.message || 'Erreur serveur' }, { status: 500 });
112
+ }
113
+ }
114
+
@@ -0,0 +1,61 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { auth } from '@/lib/auth';
3
+ import {
4
+ BUCKETS,
5
+ buildEditorImagePath,
6
+ createSignedUploadUrl,
7
+ } from '@/lib/supabase-storage';
8
+
9
+ const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
10
+
11
+ // POST /api/editor/upload-image - Créer une URL d'upload signée pour une image éditeur
12
+ export async function POST(request: NextRequest) {
13
+ try {
14
+ const session = await auth.api.getSession({
15
+ headers: request.headers,
16
+ });
17
+ if (!session) {
18
+ return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
19
+ }
20
+
21
+ const body = await request.json();
22
+ const { fileName, fileSize, mimeType } = body;
23
+
24
+ if (!fileName || typeof fileSize !== 'number' || !mimeType) {
25
+ return NextResponse.json(
26
+ { error: 'Champs requis: fileName, fileSize (number), mimeType' },
27
+ { status: 400 },
28
+ );
29
+ }
30
+
31
+ const ALLOWED_IMAGE_TYPES = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'];
32
+ if (!ALLOWED_IMAGE_TYPES.includes(mimeType)) {
33
+ return NextResponse.json(
34
+ { error: 'Seuls les formats JPEG, PNG, WebP et GIF sont acceptés' },
35
+ { status: 400 },
36
+ );
37
+ }
38
+
39
+ if (fileSize > MAX_FILE_SIZE) {
40
+ return NextResponse.json(
41
+ { error: 'Image trop volumineuse. Taille maximale: 10MB' },
42
+ { status: 400 },
43
+ );
44
+ }
45
+
46
+ const storagePath = buildEditorImagePath(fileName);
47
+ const result = await createSignedUploadUrl(BUCKETS.EDITOR_IMAGES, storagePath);
48
+
49
+ const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
50
+ const publicUrl = `${supabaseUrl}/storage/v1/object/public/${BUCKETS.EDITOR_IMAGES}/${storagePath}`;
51
+
52
+ return NextResponse.json({
53
+ ...result,
54
+ publicUrl,
55
+ });
56
+ } catch (error: unknown) {
57
+ console.error("Erreur lors de la création de l'URL d'upload image:", error);
58
+ const message = error instanceof Error ? error.message : 'Erreur serveur';
59
+ return NextResponse.json({ error: message }, { status: 500 });
60
+ }
61
+ }
@@ -0,0 +1,47 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { auth } from '@/lib/auth';
3
+ import { checkPermission } from '@/lib/check-permission';
4
+ import { prisma } from '@/lib/prisma';
5
+
6
+ export async function GET(
7
+ request: NextRequest,
8
+ { params }: { params: Promise<{ jobId: string }> },
9
+ ) {
10
+ try {
11
+ const session = await auth.api.getSession({ headers: request.headers });
12
+ if (!session) {
13
+ return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
14
+ }
15
+
16
+ const { jobId } = await params;
17
+ const job = await prisma.googleSheetSyncJob.findUnique({
18
+ where: { id: jobId },
19
+ });
20
+
21
+ if (!job) {
22
+ return NextResponse.json({ error: 'Job introuvable' }, { status: 404 });
23
+ }
24
+ const isOwner = job.requestedByUserId === session.user.id;
25
+ const isScheduledOrSystem = job.requestedByUserId === null;
26
+ const canManageIntegrations = await checkPermission('integrations.google_sheets.manage');
27
+ if (!isOwner && !isScheduledOrSystem && !canManageIntegrations) {
28
+ return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
29
+ }
30
+
31
+ return NextResponse.json({
32
+ id: job.id,
33
+ status: job.status,
34
+ triggerType: job.triggerType,
35
+ configId: job.configId,
36
+ startedAt: job.startedAt,
37
+ finishedAt: job.finishedAt,
38
+ error: job.error,
39
+ result: job.result,
40
+ createdAt: job.createdAt,
41
+ updatedAt: job.updatedAt,
42
+ });
43
+ } catch (error) {
44
+ console.error('Erreur lecture job Google Sheet:', error);
45
+ return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
46
+ }
47
+ }