create-crm-tmp 1.1.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (220) hide show
  1. package/package.json +1 -1
  2. package/template/.prettierignore +2 -0
  3. package/template/README.md +53 -67
  4. package/template/components.json +22 -0
  5. package/template/exemple-contacts.csv +54 -0
  6. package/template/next.config.ts +27 -1
  7. package/template/package.json +64 -27
  8. package/template/prisma/schema.prisma +821 -72
  9. package/template/skills-lock.json +25 -0
  10. package/template/src/app/(auth)/invite/[token]/page.tsx +21 -24
  11. package/template/src/app/(auth)/reset-password/complete/page.tsx +12 -21
  12. package/template/src/app/(auth)/reset-password/page.tsx +12 -8
  13. package/template/src/app/(auth)/reset-password/verify/page.tsx +12 -8
  14. package/template/src/app/(auth)/signin/page.tsx +20 -17
  15. package/template/src/app/(dashboard)/agenda/page.tsx +2231 -2188
  16. package/template/src/app/(dashboard)/automatisation/[id]/page.tsx +10 -7
  17. package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +680 -323
  18. package/template/src/app/(dashboard)/automatisation/new/page.tsx +11 -8
  19. package/template/src/app/(dashboard)/automatisation/page.tsx +473 -180
  20. package/template/src/app/(dashboard)/closing/page.tsx +500 -468
  21. package/template/src/app/(dashboard)/contacts/[id]/page.tsx +5035 -4126
  22. package/template/src/app/(dashboard)/contacts/companies/[id]/page.tsx +1703 -0
  23. package/template/src/app/(dashboard)/contacts/loading.tsx +13 -0
  24. package/template/src/app/(dashboard)/contacts/page.tsx +3776 -2064
  25. package/template/src/app/(dashboard)/dashboard/page.tsx +37 -519
  26. package/template/src/app/(dashboard)/error.tsx +37 -0
  27. package/template/src/app/(dashboard)/layout.tsx +1 -1
  28. package/template/src/app/(dashboard)/loading.tsx +5 -0
  29. package/template/src/app/(dashboard)/settings/loading.tsx +19 -0
  30. package/template/src/app/(dashboard)/settings/page.tsx +2685 -2489
  31. package/template/src/app/(dashboard)/templates/page.tsx +500 -300
  32. package/template/src/app/(dashboard)/users/list/page.tsx +356 -350
  33. package/template/src/app/(dashboard)/users/page.tsx +279 -310
  34. package/template/src/app/(dashboard)/users/permissions/page.tsx +104 -99
  35. package/template/src/app/(dashboard)/users/roles/page.tsx +164 -137
  36. package/template/src/app/api/audit-logs/route.ts +1 -1
  37. package/template/src/app/api/auth/google/callback/route.ts +8 -5
  38. package/template/src/app/api/auth/google/disconnect/route.ts +2 -2
  39. package/template/src/app/api/companies/[id]/activities/route.ts +131 -0
  40. package/template/src/app/api/companies/[id]/route.ts +195 -0
  41. package/template/src/app/api/companies/export/route.ts +206 -0
  42. package/template/src/app/api/companies/route.ts +166 -0
  43. package/template/src/app/api/contact-views/[id]/pin/route.ts +69 -0
  44. package/template/src/app/api/contact-views/[id]/route.ts +197 -0
  45. package/template/src/app/api/contact-views/route.ts +146 -0
  46. package/template/src/app/api/contacts/[id]/files/[fileId]/preview/route.ts +77 -0
  47. package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +7 -17
  48. package/template/src/app/api/contacts/[id]/files/route.ts +83 -44
  49. package/template/src/app/api/contacts/[id]/interactions/route.ts +37 -0
  50. package/template/src/app/api/contacts/[id]/kyc/route.ts +71 -0
  51. package/template/src/app/api/contacts/[id]/meet/route.ts +38 -29
  52. package/template/src/app/api/contacts/[id]/route.ts +111 -20
  53. package/template/src/app/api/contacts/[id]/send-email/route.ts +6 -0
  54. package/template/src/app/api/contacts/[id]/workflows/run/route.ts +61 -0
  55. package/template/src/app/api/contacts/export/route.ts +12 -17
  56. package/template/src/app/api/contacts/import/route.ts +22 -19
  57. package/template/src/app/api/contacts/import-preview/route.ts +139 -0
  58. package/template/src/app/api/contacts/route.ts +202 -49
  59. package/template/src/app/api/dashboard/stats/route.ts +9 -292
  60. package/template/src/app/api/integrations/google-sheet/sync/route.ts +203 -185
  61. package/template/src/app/api/invite/complete/route.ts +20 -23
  62. package/template/src/app/api/reminders/route.ts +1 -0
  63. package/template/src/app/api/reset-password/complete/route.ts +11 -13
  64. package/template/src/app/api/send/route.ts +9 -85
  65. package/template/src/app/api/settings/closing-reasons/[id]/route.ts +10 -21
  66. package/template/src/app/api/settings/closing-reasons/route.ts +10 -21
  67. package/template/src/app/api/settings/company/route.ts +19 -26
  68. package/template/src/app/api/settings/google-ads/[id]/route.ts +20 -23
  69. package/template/src/app/api/settings/google-ads/route.ts +20 -23
  70. package/template/src/app/api/settings/google-sheet/[id]/route.ts +20 -23
  71. package/template/src/app/api/settings/google-sheet/auto-map/route.ts +23 -32
  72. package/template/src/app/api/settings/google-sheet/preview/route.ts +104 -0
  73. package/template/src/app/api/settings/google-sheet/route.ts +20 -23
  74. package/template/src/app/api/settings/meta-leads/[id]/route.ts +20 -23
  75. package/template/src/app/api/settings/meta-leads/route.ts +20 -23
  76. package/template/src/app/api/settings/statuses/[id]/route.ts +33 -23
  77. package/template/src/app/api/settings/statuses/route.ts +24 -22
  78. package/template/src/app/api/statuses/route.ts +2 -5
  79. package/template/src/app/api/tasks/[id]/attendees/route.ts +14 -7
  80. package/template/src/app/api/tasks/[id]/route.ts +161 -137
  81. package/template/src/app/api/tasks/meet/route.ts +11 -8
  82. package/template/src/app/api/tasks/route.ts +155 -95
  83. package/template/src/app/api/templates/[id]/route.ts +22 -13
  84. package/template/src/app/api/templates/route.ts +22 -5
  85. package/template/src/app/api/users/[id]/resend-invite/route.ts +95 -0
  86. package/template/src/app/api/users/[id]/route.ts +16 -1
  87. package/template/src/app/api/users/commercials/route.ts +38 -0
  88. package/template/src/app/api/users/for-agenda/route.ts +1 -2
  89. package/template/src/app/api/users/route.ts +94 -55
  90. package/template/src/app/api/webhooks/google-ads/route.ts +20 -1
  91. package/template/src/app/api/webhooks/meta-leads/route.ts +18 -1
  92. package/template/src/app/api/workflows/[id]/route.ts +33 -6
  93. package/template/src/app/api/workflows/process/route.ts +509 -146
  94. package/template/src/app/api/workflows/route.ts +46 -4
  95. package/template/src/app/globals.css +210 -101
  96. package/template/src/app/layout.tsx +19 -8
  97. package/template/src/app/page.tsx +37 -7
  98. package/template/src/components/address-autocomplete.tsx +232 -0
  99. package/template/src/components/contacts/filter-bar.tsx +181 -0
  100. package/template/src/components/contacts/filter-builder.tsx +589 -0
  101. package/template/src/components/contacts/save-view-dialog.tsx +160 -0
  102. package/template/src/components/contacts/views-tab-bar.tsx +440 -0
  103. package/template/src/components/dashboard/activity-chart.tsx +31 -39
  104. package/template/src/components/dashboard/dashboard-content.tsx +79 -0
  105. package/template/src/components/dashboard/stat-card.tsx +40 -42
  106. package/template/src/components/dashboard/tasks-pie-chart.tsx +34 -37
  107. package/template/src/components/dashboard/upcoming-tasks-list.tsx +78 -72
  108. package/template/src/components/date-picker.tsx +396 -0
  109. package/template/src/components/editor.tsx +27 -13
  110. package/template/src/components/email-template.tsx +4 -2
  111. package/template/src/components/global-search.tsx +358 -0
  112. package/template/src/components/header.tsx +57 -62
  113. package/template/src/components/invitation-email-template.tsx +4 -2
  114. package/template/src/components/lazy-editor.tsx +11 -0
  115. package/template/src/components/meet-cancellation-email-template.tsx +11 -3
  116. package/template/src/components/meet-confirmation-email-template.tsx +10 -3
  117. package/template/src/components/meet-update-email-template.tsx +10 -3
  118. package/template/src/components/page-header.tsx +19 -15
  119. package/template/src/components/protected-page.tsx +94 -0
  120. package/template/src/components/reset-password-email-template.tsx +4 -2
  121. package/template/src/components/sidebar.tsx +92 -94
  122. package/template/src/components/skeleton.tsx +128 -42
  123. package/template/src/components/ui/accordion.tsx +64 -0
  124. package/template/src/components/ui/alert-dialog.tsx +139 -0
  125. package/template/src/components/ui/button.tsx +60 -0
  126. package/template/src/components/view-as-banner.tsx +1 -1
  127. package/template/src/components/view-as-modal.tsx +21 -16
  128. package/template/src/config/nav-pages.ts +108 -0
  129. package/template/src/contexts/app-toast-context.tsx +174 -0
  130. package/template/src/contexts/sidebar-context.tsx +16 -47
  131. package/template/src/contexts/task-reminder-context.tsx +6 -6
  132. package/template/src/contexts/view-as-context.tsx +11 -16
  133. package/template/src/hooks/use-alert.tsx +65 -0
  134. package/template/src/hooks/use-confirm.tsx +87 -0
  135. package/template/src/hooks/use-contact-views.ts +140 -0
  136. package/template/src/hooks/use-contacts.ts +69 -0
  137. package/template/src/hooks/use-fetch.ts +17 -0
  138. package/template/src/hooks/use-focus-trap.ts +73 -0
  139. package/template/src/hooks/use-statuses.ts +22 -0
  140. package/template/src/lib/address-api.ts +155 -0
  141. package/template/src/lib/cache.ts +73 -0
  142. package/template/src/lib/check-permission.ts +12 -177
  143. package/template/src/lib/contact-interactions.ts +3 -1
  144. package/template/src/lib/contact-view-filters.ts +341 -0
  145. package/template/src/lib/dashboard-stats.ts +224 -0
  146. package/template/src/lib/date-utils.ts +49 -0
  147. package/template/src/lib/get-auth-user.ts +25 -0
  148. package/template/src/lib/google-calendar.ts +54 -12
  149. package/template/src/lib/google-drive.ts +796 -75
  150. package/template/src/lib/google-fetch.ts +63 -0
  151. package/template/src/lib/local-storage.ts +34 -0
  152. package/template/src/lib/permissions.ts +245 -47
  153. package/template/src/lib/prisma.ts +11 -11
  154. package/template/src/lib/roles.ts +14 -39
  155. package/template/src/lib/template-variables.ts +67 -33
  156. package/template/src/lib/utils.ts +26 -2
  157. package/template/src/lib/workflow-executor.ts +445 -229
  158. package/template/src/proxy.ts +34 -73
  159. package/template/src/types/contact-views.ts +351 -0
  160. package/template/src/types/yousign.ts +52 -0
  161. package/template/vercel.json +12 -0
  162. package/template/WORKFLOWS_CRON.md +0 -185
  163. package/template/prisma/migrations/20251126144728_init/migration.sql +0 -78
  164. package/template/prisma/migrations/20251126155204_add_user_roles/migration.sql +0 -5
  165. package/template/prisma/migrations/20251128095126_add_company_info/migration.sql +0 -19
  166. package/template/prisma/migrations/20251128123321_add_smtp_config/migration.sql +0 -22
  167. package/template/prisma/migrations/20251128132303_add_status/migration.sql +0 -23
  168. package/template/prisma/migrations/20251201102207_add_user_active/migration.sql +0 -75
  169. package/template/prisma/migrations/20251201105507_add_email_signature/migration.sql +0 -2
  170. package/template/prisma/migrations/20251201151122_add_tasks/migration.sql +0 -45
  171. package/template/prisma/migrations/20251202111854_add_task_reminder/migration.sql +0 -2
  172. package/template/prisma/migrations/20251202135859_add_google_meet_integration/migration.sql +0 -27
  173. package/template/prisma/migrations/20251203103317_add_meta_lead_integration/migration.sql +0 -20
  174. package/template/prisma/migrations/20251203104002_add_google_ads_integration/migration.sql +0 -18
  175. package/template/prisma/migrations/20251203112122_add_google_sheet_integration/migration.sql +0 -32
  176. package/template/prisma/migrations/20251203153853_allow_multiple_integration_configs/migration.sql +0 -20
  177. package/template/prisma/migrations/20251205141705_update_user_roles/migration.sql +0 -12
  178. package/template/prisma/migrations/20251205150000_add_commercial_and_telepro_assignment/migration.sql +0 -21
  179. package/template/prisma/migrations/20251205160000_add_interaction_logging/migration.sql +0 -11
  180. package/template/prisma/migrations/20251208090314_add_automatic_interaction_types/migration.sql +0 -12
  181. package/template/prisma/migrations/20251208094843_mg/migration.sql +0 -14
  182. package/template/prisma/migrations/20251208100000_add_company_support/migration.sql +0 -14
  183. package/template/prisma/migrations/20251208110000_add_templates/migration.sql +0 -26
  184. package/template/prisma/migrations/20251208141304_add_video_conference_task_type/migration.sql +0 -2
  185. package/template/prisma/migrations/20251209104759_add_internal_note_to_task/migration.sql +0 -2
  186. package/template/prisma/migrations/20251209134803_add_company_field/migration.sql +0 -2
  187. package/template/prisma/migrations/20251209150000_rename_company_to_company_name/migration.sql +0 -3
  188. package/template/prisma/migrations/20251209150016_add_email_tracking/migration.sql +0 -21
  189. package/template/prisma/migrations/20251209155908_add_notify_contact_to_task/migration.sql +0 -2
  190. package/template/prisma/migrations/20251210110019_add_appointment_types/migration.sql +0 -10
  191. package/template/prisma/migrations/20251210113928_add_contact_files/migration.sql +0 -26
  192. package/template/prisma/migrations/20251212132339_add_custom_roles/migration.sql +0 -24
  193. package/template/prisma/migrations/20251215104448_add_file_interaction_types/migration.sql +0 -11
  194. package/template/prisma/migrations/20251215145616_add_closing_reasons/migration.sql +0 -12
  195. package/template/prisma/migrations/20251216140850_add_log_users/migration.sql +0 -25
  196. package/template/prisma/migrations/20251216151000_rename_perdu_to_ferme/migration.sql +0 -8
  197. package/template/prisma/migrations/20251216162318_add_column_mappings_to_google_sheet/migration.sql +0 -2
  198. package/template/prisma/migrations/20251216185127_add_workflows/migration.sql +0 -80
  199. package/template/prisma/migrations/20251216192237_add_scheduled_workflow_actions/migration.sql +0 -32
  200. package/template/prisma/migrations/20251220000000_add_task_interaction_type/migration.sql +0 -4
  201. package/template/prisma/migrations/20251221000000_add_task_type/migration.sql +0 -3
  202. package/template/prisma/migrations/20251221000001_add_event_color/migration.sql +0 -23
  203. package/template/prisma/migrations/20260210114913_add_dashboard_widget/migration.sql +0 -20
  204. package/template/prisma/migrations/migration_lock.toml +0 -3
  205. package/template/src/app/(dashboard)/users/layout.tsx +0 -30
  206. package/template/src/app/api/dashboard/widgets/[id]/route.ts +0 -47
  207. package/template/src/app/api/dashboard/widgets/route.ts +0 -181
  208. package/template/src/components/dashboard/add-widget-dialog.tsx +0 -161
  209. package/template/src/components/dashboard/color-picker.tsx +0 -65
  210. package/template/src/components/dashboard/contacts-chart.tsx +0 -69
  211. package/template/src/components/dashboard/interactions-by-type-chart.tsx +0 -121
  212. package/template/src/components/dashboard/recent-activity.tsx +0 -157
  213. package/template/src/components/dashboard/sales-analytics-chart.tsx +0 -77
  214. package/template/src/components/dashboard/status-distribution-chart.tsx +0 -82
  215. package/template/src/components/dashboard/top-contacts-list.tsx +0 -119
  216. package/template/src/components/dashboard/widget-wrapper.tsx +0 -39
  217. package/template/src/contexts/dashboard-theme-context.tsx +0 -58
  218. package/template/src/lib/dashboard-themes.ts +0 -140
  219. package/template/src/lib/default-widgets.ts +0 -14
  220. package/template/src/lib/widget-registry.ts +0 -177
@@ -1,12 +1,13 @@
1
1
  import { NextRequest, NextResponse } from 'next/server';
2
- import { prisma } from '@/lib/prisma';
3
- import { requireAdmin } from '@/lib/roles';
2
+ import { auth } from '@/lib/auth';
3
+ import { checkPermission } from '@/lib/check-permission';
4
4
  import { getValidAccessToken } from '@/lib/google-calendar';
5
+ import { indexToColumn } from '@/lib/utils';
6
+ import { googleFetch } from '@/lib/google-fetch';
5
7
 
6
8
  function extractSpreadsheetId(sheetUrlOrId: string): string {
7
9
  if (!sheetUrlOrId) return sheetUrlOrId;
8
10
 
9
- // Si c'est déjà un ID simple, on le renvoie
10
11
  if (!sheetUrlOrId.includes('https://')) {
11
12
  return sheetUrlOrId;
12
13
  }
@@ -19,21 +20,18 @@ function extractSpreadsheetId(sheetUrlOrId: string): string {
19
20
  return sheetUrlOrId;
20
21
  }
21
22
 
22
- function indexToColumn(index: number): string {
23
- let col = '';
24
- let n = index + 1;
25
- while (n > 0) {
26
- const rem = (n - 1) % 26;
27
- col = String.fromCharCode(65 + rem) + col;
28
- n = Math.floor((n - 1) / 26);
29
- }
30
- return col;
31
- }
32
-
33
23
  // POST /api/settings/google-sheet/auto-map - Proposer un mapping automatique des colonnes
34
24
  export async function POST(request: NextRequest) {
35
25
  try {
36
- const session = await requireAdmin(request.headers);
26
+ const session = await auth.api.getSession({ headers: request.headers });
27
+ if (!session) {
28
+ return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
29
+ }
30
+
31
+ const hasPermission = await checkPermission('integrations.google_sheets.manage');
32
+ if (!hasPermission) {
33
+ return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
34
+ }
37
35
 
38
36
  const body = await request.json();
39
37
  const { sheetUrl, sheetName, headerRow } = body || {};
@@ -58,16 +56,18 @@ export async function POST(request: NextRequest) {
58
56
  );
59
57
  }
60
58
 
61
- // Récupérer le compte Google de l'utilisateur admin courant
62
- const googleAccount = await prisma.userGoogleAccount.findUnique({
63
- where: { userId: session.user.id },
64
- });
65
-
66
- if (!googleAccount) {
59
+ // Récupérer le compte Google de l'admin pour Google Sheets (utilise Drive)
60
+ // Note: Cette fonction n'existe plus dans google-calendar, utiliser celle de google-drive
61
+ const { getAdminGoogleAccount } = await import('@/lib/google-drive');
62
+ let googleAccount;
63
+ try {
64
+ googleAccount = await getAdminGoogleAccount();
65
+ } catch (error: any) {
67
66
  return NextResponse.json(
68
67
  {
69
68
  error:
70
- 'Aucun compte Google connecté. Veuillez connecter votre compte Google dans les paramètres avant de configurer Google Sheets.',
69
+ error.message ||
70
+ 'Aucun compte Google configuré. Veuillez demander à un administrateur de connecter son compte Google dans les paramètres.',
71
71
  },
72
72
  { status: 400 },
73
73
  );
@@ -80,7 +80,7 @@ export async function POST(request: NextRequest) {
80
80
  );
81
81
 
82
82
  const range = encodeURIComponent(sheetName);
83
- const response = await fetch(
83
+ const response = await googleFetch(
84
84
  `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}/values/${range}`,
85
85
  {
86
86
  headers: {
@@ -184,15 +184,6 @@ export async function POST(request: NextRequest) {
184
184
  });
185
185
  } catch (error: any) {
186
186
  console.error('Erreur lors du mapping automatique Google Sheets:', error);
187
-
188
- if (error.message === 'Non authentifié') {
189
- return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
190
- }
191
-
192
- if (error.message === 'Permissions insuffisantes') {
193
- return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
194
- }
195
-
196
187
  return NextResponse.json(
197
188
  { error: error.message || 'Erreur serveur lors du mapping automatique' },
198
189
  { status: 500 },
@@ -0,0 +1,104 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { auth } from '@/lib/auth';
3
+ import { checkPermission } from '@/lib/check-permission';
4
+ import { getValidAccessToken } from '@/lib/google-calendar';
5
+ import { googleFetch } from '@/lib/google-fetch';
6
+
7
+ function extractSpreadsheetId(sheetUrlOrId: string): string {
8
+ if (!sheetUrlOrId) return sheetUrlOrId;
9
+ if (!sheetUrlOrId.includes('https://')) return sheetUrlOrId;
10
+ const match = sheetUrlOrId.match(/spreadsheets\/d\/([a-zA-Z0-9-_]+)/);
11
+ return match?.[1] ?? sheetUrlOrId;
12
+ }
13
+
14
+ export async function POST(request: NextRequest) {
15
+ try {
16
+ const session = await auth.api.getSession({ headers: request.headers });
17
+ if (!session) {
18
+ return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
19
+ }
20
+
21
+ const hasPermission = await checkPermission('integrations.google_sheets.manage');
22
+ if (!hasPermission) {
23
+ return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
24
+ }
25
+
26
+ const body = await request.json();
27
+ const { sheetUrl, sheetName } = body || {};
28
+
29
+ if (!sheetUrl) {
30
+ return NextResponse.json(
31
+ { error: 'Le lien du Google Sheet est obligatoire.' },
32
+ { status: 400 },
33
+ );
34
+ }
35
+
36
+ const spreadsheetId = extractSpreadsheetId(sheetUrl);
37
+
38
+ const { getAdminGoogleAccount } = await import('@/lib/google-drive');
39
+ let googleAccount;
40
+ try {
41
+ googleAccount = await getAdminGoogleAccount();
42
+ } catch (error: unknown) {
43
+ const message = error instanceof Error ? error.message : 'Aucun compte Google configuré.';
44
+ return NextResponse.json({ error: message }, { status: 400 });
45
+ }
46
+
47
+ const accessToken = await getValidAccessToken(
48
+ googleAccount.accessToken,
49
+ googleAccount.refreshToken,
50
+ googleAccount.tokenExpiresAt,
51
+ );
52
+
53
+ // Fetch spreadsheet metadata to get all sheet names
54
+ const metaResponse = await googleFetch(
55
+ `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}?fields=sheets.properties.title`,
56
+ { headers: { Authorization: `Bearer ${accessToken}` } },
57
+ );
58
+
59
+ if (!metaResponse.ok) {
60
+ return NextResponse.json(
61
+ { error: 'Impossible de lire les métadonnées du Google Sheet.' },
62
+ { status: 400 },
63
+ );
64
+ }
65
+
66
+ const metaData = await metaResponse.json();
67
+ const sheetNames: string[] = (metaData.sheets || []).map(
68
+ (s: { properties: { title: string } }) => s.properties.title,
69
+ );
70
+
71
+ if (sheetNames.length === 0) {
72
+ return NextResponse.json(
73
+ { error: 'Le Google Sheet ne contient aucune feuille.' },
74
+ { status: 400 },
75
+ );
76
+ }
77
+
78
+ const targetSheet = sheetName && sheetNames.includes(sheetName) ? sheetName : sheetNames[0];
79
+
80
+ const range = encodeURIComponent(targetSheet);
81
+ const dataResponse = await googleFetch(
82
+ `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}/values/${range}`,
83
+ { headers: { Authorization: `Bearer ${accessToken}` } },
84
+ );
85
+
86
+ if (!dataResponse.ok) {
87
+ return NextResponse.json(
88
+ { error: 'Impossible de lire les données de la feuille.' },
89
+ { status: 400 },
90
+ );
91
+ }
92
+
93
+ const data = await dataResponse.json();
94
+ const values: string[][] = data.values || [];
95
+
96
+ const rawRows = values.slice(0, 20).map((row) => row.map((cell) => String(cell ?? '')));
97
+
98
+ return NextResponse.json({ sheetNames, rawRows });
99
+ } catch (error: unknown) {
100
+ console.error('Erreur google-sheet preview:', error);
101
+ const message = error instanceof Error ? error.message : 'Erreur serveur';
102
+ return NextResponse.json({ error: message }, { status: 500 });
103
+ }
104
+ }
@@ -1,6 +1,7 @@
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
  function extractSpreadsheetId(sheetUrlOrId: string): string {
6
7
  if (!sheetUrlOrId) return sheetUrlOrId;
@@ -18,10 +19,17 @@ function extractSpreadsheetId(sheetUrlOrId: string): string {
18
19
  return sheetUrlOrId;
19
20
  }
20
21
 
21
- // GET /api/settings/google-sheet - Récupérer toutes les configurations Google Sheets (admin uniquement)
22
+ // GET /api/settings/google-sheet - Récupérer toutes les configurations Google Sheets
22
23
  export async function GET(request: NextRequest) {
23
24
  try {
24
- await requireAdmin(request.headers);
25
+ const session = await auth.api.getSession({ headers: request.headers });
26
+ if (!session) {
27
+ return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
28
+ }
29
+ const hasPermission = await checkPermission('integrations.google_sheets.manage');
30
+ if (!hasPermission) {
31
+ return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
32
+ }
25
33
 
26
34
  const client = prisma as any;
27
35
 
@@ -76,23 +84,21 @@ export async function GET(request: NextRequest) {
76
84
  );
77
85
  } catch (error: any) {
78
86
  console.error('Erreur lors de la récupération des configurations Google Sheets:', error);
79
-
80
- if (error.message === 'Non authentifié') {
81
- return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
82
- }
83
-
84
- if (error.message === 'Permissions insuffisantes') {
85
- return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
86
- }
87
-
88
87
  return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
89
88
  }
90
89
  }
91
90
 
92
- // POST /api/settings/google-sheet - Créer une nouvelle configuration (admin uniquement)
91
+ // POST /api/settings/google-sheet - Créer une nouvelle configuration
93
92
  export async function POST(request: NextRequest) {
94
93
  try {
95
- const session = await requireAdmin(request.headers);
94
+ const session = await auth.api.getSession({ headers: request.headers });
95
+ if (!session) {
96
+ return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
97
+ }
98
+ const hasPermission = await checkPermission('integrations.google_sheets.manage');
99
+ if (!hasPermission) {
100
+ return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
101
+ }
96
102
 
97
103
  const body = await request.json();
98
104
  const {
@@ -240,15 +246,6 @@ export async function POST(request: NextRequest) {
240
246
  });
241
247
  } catch (error: any) {
242
248
  console.error('Erreur lors de la création de la configuration Google Sheets:', error);
243
-
244
- if (error.message === 'Non authentifié') {
245
- return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
246
- }
247
-
248
- if (error.message === 'Permissions insuffisantes') {
249
- return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
250
- }
251
-
252
249
  return NextResponse.json({ error: error.message || 'Erreur serveur' }, { status: 500 });
253
250
  }
254
251
  }
@@ -1,12 +1,20 @@
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
  import { encrypt } from '@/lib/encryption';
5
6
 
6
- // PUT /api/settings/meta-leads/[id] - Mettre à jour une configuration (admin uniquement)
7
+ // PUT /api/settings/meta-leads/[id] - Mettre à jour une configuration
7
8
  export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
8
9
  try {
9
- await requireAdmin(request.headers);
10
+ const session = await auth.api.getSession({ headers: request.headers });
11
+ if (!session) {
12
+ return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
13
+ }
14
+ const hasPermission = await checkPermission('integrations.meta.manage');
15
+ if (!hasPermission) {
16
+ return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
17
+ }
10
18
 
11
19
  const { id } = await params;
12
20
  const body = await request.json();
@@ -68,26 +76,24 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
68
76
  });
69
77
  } catch (error: any) {
70
78
  console.error('Erreur lors de la mise à jour de la configuration Meta Lead Ads:', error);
71
-
72
- if (error.message === 'Non authentifié') {
73
- return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
74
- }
75
-
76
- if (error.message === 'Permissions insuffisantes') {
77
- return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
78
- }
79
-
80
79
  return NextResponse.json({ error: error.message || 'Erreur serveur' }, { status: 500 });
81
80
  }
82
81
  }
83
82
 
84
- // DELETE /api/settings/meta-leads/[id] - Supprimer une configuration (admin uniquement)
83
+ // DELETE /api/settings/meta-leads/[id] - Supprimer une configuration
85
84
  export async function DELETE(
86
85
  request: NextRequest,
87
86
  { params }: { params: Promise<{ id: string }> },
88
87
  ) {
89
88
  try {
90
- await requireAdmin(request.headers);
89
+ const session = await auth.api.getSession({ headers: request.headers });
90
+ if (!session) {
91
+ return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
92
+ }
93
+ const hasPermission = await checkPermission('integrations.meta.manage');
94
+ if (!hasPermission) {
95
+ return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
96
+ }
91
97
 
92
98
  const { id } = await params;
93
99
 
@@ -109,15 +115,6 @@ export async function DELETE(
109
115
  });
110
116
  } catch (error: any) {
111
117
  console.error('Erreur lors de la suppression de la configuration Meta Lead Ads:', error);
112
-
113
- if (error.message === 'Non authentifié') {
114
- return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
115
- }
116
-
117
- if (error.message === 'Permissions insuffisantes') {
118
- return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
119
- }
120
-
121
118
  return NextResponse.json({ error: error.message || 'Erreur serveur' }, { status: 500 });
122
119
  }
123
120
  }
@@ -1,12 +1,20 @@
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
  import { encrypt } from '@/lib/encryption';
5
6
 
6
- // GET /api/settings/meta-leads - Récupérer toutes les configurations Meta Lead Ads (admin uniquement)
7
+ // GET /api/settings/meta-leads - Récupérer toutes les configurations Meta Lead Ads
7
8
  export async function GET(request: NextRequest) {
8
9
  try {
9
- await requireAdmin(request.headers);
10
+ const session = await auth.api.getSession({ headers: request.headers });
11
+ if (!session) {
12
+ return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
13
+ }
14
+ const hasPermission = await checkPermission('integrations.meta.manage');
15
+ if (!hasPermission) {
16
+ return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
17
+ }
10
18
 
11
19
  const configs = await prisma.metaLeadConfig.findMany({
12
20
  include: {
@@ -41,23 +49,21 @@ export async function GET(request: NextRequest) {
41
49
  );
42
50
  } catch (error: any) {
43
51
  console.error('Erreur lors de la récupération des configurations Meta Lead Ads:', error);
44
-
45
- if (error.message === 'Non authentifié') {
46
- return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
47
- }
48
-
49
- if (error.message === 'Permissions insuffisantes') {
50
- return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
51
- }
52
-
53
52
  return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
54
53
  }
55
54
  }
56
55
 
57
- // POST /api/settings/meta-leads - Créer une nouvelle configuration (admin uniquement)
56
+ // POST /api/settings/meta-leads - Créer une nouvelle configuration
58
57
  export async function POST(request: NextRequest) {
59
58
  try {
60
- await requireAdmin(request.headers);
59
+ const session = await auth.api.getSession({ headers: request.headers });
60
+ if (!session) {
61
+ return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
62
+ }
63
+ const hasPermission = await checkPermission('integrations.meta.manage');
64
+ if (!hasPermission) {
65
+ return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
66
+ }
61
67
 
62
68
  const body = await request.json();
63
69
  const {
@@ -118,15 +124,6 @@ export async function POST(request: NextRequest) {
118
124
  });
119
125
  } catch (error: any) {
120
126
  console.error('Erreur lors de la création de la configuration Meta Lead Ads:', error);
121
-
122
- if (error.message === 'Non authentifié') {
123
- return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
124
- }
125
-
126
- if (error.message === 'Permissions insuffisantes') {
127
- return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
128
- }
129
-
130
127
  return NextResponse.json({ error: error.message || 'Erreur serveur' }, { status: 500 });
131
128
  }
132
129
  }
@@ -1,15 +1,21 @@
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';
5
+ import { invalidateCachePrefix } from '@/lib/cache';
4
6
 
5
7
  // PUT /api/settings/statuses/[id] - Mettre à jour un statut
6
8
  export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
7
9
  try {
8
- await requireAdmin(request.headers);
10
+ const session = await auth.api.getSession({ headers: request.headers });
11
+ if (!session) return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
12
+ const canManage = await checkPermission('settings.status.manage');
13
+ if (!canManage) return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
14
+
9
15
  const { id } = await params;
10
16
 
11
17
  const body = await request.json();
12
- const { name, color, order } = body;
18
+ const { name, color, order, requiresClosingReason } = body;
13
19
 
14
20
  // Validation
15
21
  if (!name || !color) {
@@ -25,6 +31,13 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
25
31
  return NextResponse.json({ error: 'Statut non trouvé' }, { status: 404 });
26
32
  }
27
33
 
34
+ if (existing.isSystem && name !== existing.name) {
35
+ return NextResponse.json(
36
+ { error: 'Ce statut est utilisé par le système et ne peut pas être renommé' },
37
+ { status: 403 },
38
+ );
39
+ }
40
+
28
41
  // Vérifier si le nom existe déjà pour un autre statut
29
42
  const nameConflict = await prisma.status.findFirst({
30
43
  where: {
@@ -43,21 +56,15 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
43
56
  name,
44
57
  color,
45
58
  ...(order !== undefined && { order }),
59
+ ...(requiresClosingReason !== undefined && { requiresClosingReason }),
46
60
  },
47
61
  });
48
62
 
63
+ invalidateCachePrefix('status');
64
+
49
65
  return NextResponse.json(updatedStatus);
50
66
  } catch (error: any) {
51
67
  console.error('Erreur lors de la mise à jour du statut:', error);
52
-
53
- if (error.message === 'Non authentifié') {
54
- return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
55
- }
56
-
57
- if (error.message === 'Permissions insuffisantes') {
58
- return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
59
- }
60
-
61
68
  return NextResponse.json({ error: error.message || 'Erreur serveur' }, { status: 500 });
62
69
  }
63
70
  }
@@ -68,10 +75,13 @@ export async function DELETE(
68
75
  { params }: { params: Promise<{ id: string }> },
69
76
  ) {
70
77
  try {
71
- await requireAdmin(request.headers);
78
+ const session = await auth.api.getSession({ headers: request.headers });
79
+ if (!session) return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
80
+ const canManage = await checkPermission('settings.status.manage');
81
+ if (!canManage) return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
82
+
72
83
  const { id } = await params;
73
84
 
74
- // Vérifier si le statut existe
75
85
  const existing = await prisma.status.findUnique({
76
86
  where: { id },
77
87
  });
@@ -80,22 +90,22 @@ export async function DELETE(
80
90
  return NextResponse.json({ error: 'Statut non trouvé' }, { status: 404 });
81
91
  }
82
92
 
93
+ if (existing.isSystem) {
94
+ return NextResponse.json(
95
+ { error: 'Ce statut est utilisé par le système et ne peut pas être supprimé' },
96
+ { status: 403 },
97
+ );
98
+ }
99
+
83
100
  await prisma.status.delete({
84
101
  where: { id },
85
102
  });
86
103
 
104
+ invalidateCachePrefix('status');
105
+
87
106
  return NextResponse.json({ success: true, message: 'Statut supprimé avec succès' });
88
107
  } catch (error: any) {
89
108
  console.error('Erreur lors de la suppression du statut:', error);
90
-
91
- if (error.message === 'Non authentifié') {
92
- return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
93
- }
94
-
95
- if (error.message === 'Permissions insuffisantes') {
96
- return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
97
- }
98
-
99
109
  return NextResponse.json({ error: error.message || 'Erreur serveur' }, { status: 500 });
100
110
  }
101
111
  }
@@ -1,11 +1,16 @@
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';
5
+ import { invalidateCachePrefix } from '@/lib/cache';
4
6
 
5
7
  // GET /api/settings/statuses - Récupérer tous les statuts
6
8
  export async function GET(request: NextRequest) {
7
9
  try {
8
- await requireAdmin(request.headers);
10
+ const session = await auth.api.getSession({ headers: request.headers });
11
+ if (!session) return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
12
+ const canManage = await checkPermission('settings.status.manage');
13
+ if (!canManage) return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
9
14
 
10
15
  const statuses = await prisma.status.findMany({
11
16
  orderBy: { order: 'asc' },
@@ -14,15 +19,6 @@ export async function GET(request: NextRequest) {
14
19
  return NextResponse.json(statuses);
15
20
  } catch (error: any) {
16
21
  console.error('Erreur lors de la récupération des statuts:', 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
22
  return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
27
23
  }
28
24
  }
@@ -30,10 +26,13 @@ export async function GET(request: NextRequest) {
30
26
  // POST /api/settings/statuses - Créer un nouveau statut
31
27
  export async function POST(request: NextRequest) {
32
28
  try {
33
- await requireAdmin(request.headers);
29
+ const session = await auth.api.getSession({ headers: request.headers });
30
+ if (!session) return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
31
+ const canManage = await checkPermission('settings.status.manage');
32
+ if (!canManage) return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
34
33
 
35
34
  const body = await request.json();
36
- const { name, color, order } = body;
35
+ const { name, color, order, requiresClosingReason } = body;
37
36
 
38
37
  // Validation
39
38
  if (!name || !color) {
@@ -63,21 +62,24 @@ export async function POST(request: NextRequest) {
63
62
  name,
64
63
  color,
65
64
  order: statusOrder,
65
+ ...(requiresClosingReason !== undefined && { requiresClosingReason }),
66
66
  },
67
67
  });
68
68
 
69
+ invalidateCachePrefix('status');
70
+
69
71
  return NextResponse.json(status, { status: 201 });
70
72
  } catch (error: any) {
71
73
  console.error('Erreur lors de la création du statut:', error);
72
74
 
73
- if (error.message === 'Non authentifié') {
74
- return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
75
- }
76
-
77
- if (error.message === 'Permissions insuffisantes') {
78
- return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
79
- }
80
-
81
- return NextResponse.json({ error: error.message || 'Erreur serveur' }, { status: 500 });
75
+ return NextResponse.json(
76
+ {
77
+ error:
78
+ process.env.NODE_ENV === 'development'
79
+ ? error.message || 'Erreur serveur'
80
+ : 'Erreur serveur',
81
+ },
82
+ { status: 500 },
83
+ );
82
84
  }
83
85
  }
@@ -1,8 +1,7 @@
1
1
  import { NextRequest, NextResponse } from 'next/server';
2
2
  import { auth } from '@/lib/auth';
3
- import { prisma } from '@/lib/prisma';
3
+ import { getCachedStatuses } from '@/lib/cache';
4
4
 
5
- // GET /api/statuses - Récupérer tous les statuts (accessible à tous les utilisateurs authentifiés)
6
5
  export async function GET(request: NextRequest) {
7
6
  try {
8
7
  const session = await auth.api.getSession({
@@ -13,9 +12,7 @@ export async function GET(request: NextRequest) {
13
12
  return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
14
13
  }
15
14
 
16
- const statuses = await prisma.status.findMany({
17
- orderBy: { order: 'asc' },
18
- });
15
+ const statuses = await getCachedStatuses();
19
16
 
20
17
  return NextResponse.json(statuses);
21
18
  } catch (error: any) {
@@ -45,13 +45,20 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
45
45
  return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
46
46
  }
47
47
 
48
- // Récupérer le compte Google
49
- const googleAccount = await prisma.userGoogleAccount.findUnique({
50
- where: { userId: session.user.id },
51
- });
52
-
53
- if (!googleAccount) {
54
- return NextResponse.json({ error: 'Compte Google non connecté' }, { status: 400 });
48
+ // Récupérer le compte Google de l'utilisateur courant
49
+ const { getUserGoogleAccount } = await import('@/lib/google-calendar');
50
+ let googleAccount;
51
+ try {
52
+ googleAccount = await getUserGoogleAccount(session.user.id);
53
+ } catch (error: any) {
54
+ return NextResponse.json(
55
+ {
56
+ error:
57
+ error.message ||
58
+ 'Veuillez connecter votre compte Google dans les paramètres pour utiliser Google Calendar.',
59
+ },
60
+ { status: 400 },
61
+ );
55
62
  }
56
63
 
57
64
  // Obtenir un token valide