create-crm-tmp 2.0.0 → 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 (190) hide show
  1. package/bin/create-crm-tmp.js +56 -35
  2. package/package.json +1 -1
  3. package/template/README.md +230 -115
  4. package/template/eslint.config.mjs +13 -0
  5. package/template/next.config.ts +14 -0
  6. package/template/package.json +15 -2
  7. package/template/prisma/migrations/20260318095700_init_db/migration.sql +978 -0
  8. package/template/prisma/migrations/migration_lock.toml +3 -0
  9. package/template/prisma/schema.prisma +132 -637
  10. package/template/src/app/(auth)/invite/[token]/page.tsx +10 -8
  11. package/template/src/app/(auth)/layout.tsx +1 -1
  12. package/template/src/app/(auth)/reset-password/complete/page.tsx +11 -8
  13. package/template/src/app/(auth)/reset-password/page.tsx +4 -4
  14. package/template/src/app/(auth)/reset-password/verify/page.tsx +4 -4
  15. package/template/src/app/(auth)/signin/page.tsx +14 -6
  16. package/template/src/app/(dashboard)/agenda/page.tsx +2243 -988
  17. package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +18 -104
  18. package/template/src/app/(dashboard)/automatisation/page.tsx +10 -26
  19. package/template/src/app/(dashboard)/closing/page.tsx +78 -62
  20. package/template/src/app/(dashboard)/contacts/[id]/page.tsx +2082 -1080
  21. package/template/src/app/(dashboard)/contacts/companies/[id]/page.tsx +46 -47
  22. package/template/src/app/(dashboard)/contacts/page.tsx +1062 -780
  23. package/template/src/app/(dashboard)/dashboard/page.tsx +533 -37
  24. package/template/src/app/(dashboard)/dev/page.tsx +1291 -0
  25. package/template/src/app/(dashboard)/layout.tsx +6 -2
  26. package/template/src/app/(dashboard)/settings/page.tsx +797 -2582
  27. package/template/src/app/(dashboard)/templates/page.tsx +55 -54
  28. package/template/src/app/(dashboard)/users/list/page.tsx +51 -48
  29. package/template/src/app/(dashboard)/users/page.tsx +1 -1
  30. package/template/src/app/(dashboard)/users/permissions/page.tsx +2 -2
  31. package/template/src/app/(dashboard)/users/roles/page.tsx +7 -5
  32. package/template/src/app/api/agenda/google-events/route.ts +92 -0
  33. package/template/src/app/api/auth/check-active/route.ts +3 -2
  34. package/template/src/app/api/auth/google/route.ts +2 -1
  35. package/template/src/app/api/auth/google/status/route.ts +7 -31
  36. package/template/src/app/api/companies/[id]/activities/route.ts +1 -3
  37. package/template/src/app/api/companies/[id]/route.ts +1 -2
  38. package/template/src/app/api/companies/route.ts +42 -12
  39. package/template/src/app/api/contacts/[id]/files/[fileId]/preview/route.ts +9 -31
  40. package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +14 -32
  41. package/template/src/app/api/contacts/[id]/files/route.ts +112 -212
  42. package/template/src/app/api/contacts/[id]/interactions/[interactionId]/route.ts +27 -1
  43. package/template/src/app/api/contacts/[id]/interactions/route.ts +16 -16
  44. package/template/src/app/api/contacts/[id]/kyc/route.ts +21 -11
  45. package/template/src/app/api/contacts/[id]/meet/route.ts +19 -2
  46. package/template/src/app/api/contacts/[id]/route.ts +106 -34
  47. package/template/src/app/api/contacts/[id]/send-email/route.ts +27 -11
  48. package/template/src/app/api/contacts/[id]/workflows/run/route.ts +6 -0
  49. package/template/src/app/api/contacts/export/route.ts +9 -13
  50. package/template/src/app/api/contacts/import/route.ts +55 -25
  51. package/template/src/app/api/contacts/import-preview/route.ts +1 -1
  52. package/template/src/app/api/contacts/origins/route.ts +63 -0
  53. package/template/src/app/api/contacts/route.ts +153 -41
  54. package/template/src/app/api/cron/cleanup-editor-images/route.ts +166 -0
  55. package/template/src/app/api/dashboard/widgets/[id]/route.ts +44 -0
  56. package/template/src/app/api/dashboard/widgets/route.ts +181 -0
  57. package/template/src/app/api/dev/reminders/test/route.ts +114 -0
  58. package/template/src/app/api/editor/upload-image/route.ts +61 -0
  59. package/template/src/app/api/integrations/google-sheet/jobs/[jobId]/route.ts +47 -0
  60. package/template/src/app/api/integrations/google-sheet/jobs/usage/route.ts +50 -0
  61. package/template/src/app/api/integrations/google-sheet/sync/route.ts +24 -556
  62. package/template/src/app/api/jobs/google-sheet/process/route.ts +84 -0
  63. package/template/src/app/api/jobs/google-sheet/schedule/route.ts +50 -0
  64. package/template/src/app/api/reminders/clear/route.ts +120 -0
  65. package/template/src/app/api/reminders/clear/undo/route.ts +112 -0
  66. package/template/src/app/api/reminders/route.ts +164 -39
  67. package/template/src/app/api/reminders/state/route.ts +164 -0
  68. package/template/src/app/api/reset-password/request/route.ts +1 -1
  69. package/template/src/app/api/reset-password/verify/route.ts +1 -1
  70. package/template/src/app/api/send/route.ts +16 -4
  71. package/template/src/app/api/settings/google-ads/route.ts +14 -0
  72. package/template/src/app/api/settings/google-calendar/calendars/route.ts +97 -0
  73. package/template/src/app/api/settings/google-calendar/route.ts +124 -0
  74. package/template/src/app/api/settings/google-sheet/[id]/route.ts +28 -0
  75. package/template/src/app/api/settings/google-sheet/auto-map/route.ts +37 -4
  76. package/template/src/app/api/settings/google-sheet/preview/route.ts +9 -3
  77. package/template/src/app/api/settings/google-sheet/route.ts +14 -0
  78. package/template/src/app/api/settings/integrations/logs/route.ts +93 -0
  79. package/template/src/app/api/settings/integrations/notifications/route.ts +67 -0
  80. package/template/src/app/api/settings/meta-leads/[id]/route.ts +0 -1
  81. package/template/src/app/api/settings/meta-leads/route.ts +14 -2
  82. package/template/src/app/api/settings/smtp/route.ts +53 -6
  83. package/template/src/app/api/tasks/[id]/attendees/route.ts +24 -8
  84. package/template/src/app/api/tasks/[id]/route.ts +234 -58
  85. package/template/src/app/api/tasks/meet/route.ts +27 -19
  86. package/template/src/app/api/tasks/route.ts +62 -17
  87. package/template/src/app/api/users/[id]/route.ts +20 -14
  88. package/template/src/app/api/users/list/route.ts +57 -19
  89. package/template/src/app/api/webhooks/google-ads/route.ts +34 -14
  90. package/template/src/app/api/webhooks/meta-leads/route.ts +32 -12
  91. package/template/src/app/api/workflows/[id]/route.ts +0 -4
  92. package/template/src/app/api/workflows/process/route.ts +22 -51
  93. package/template/src/app/api/workflows/route.ts +0 -4
  94. package/template/src/app/globals.css +342 -4
  95. package/template/src/app/layout.tsx +11 -3
  96. package/template/src/app/page.tsx +1 -1
  97. package/template/src/components/address-autocomplete.tsx +7 -6
  98. package/template/src/components/config-error-alert.tsx +46 -0
  99. package/template/src/components/contacts/filter-bar.tsx +12 -3
  100. package/template/src/components/contacts/filter-builder.tsx +28 -43
  101. package/template/src/components/contacts/save-view-dialog.tsx +1 -1
  102. package/template/src/components/contacts/views-tab-bar.tsx +15 -6
  103. package/template/src/components/dashboard/activity-chart.tsx +41 -28
  104. package/template/src/components/dashboard/add-widget-dialog.tsx +157 -0
  105. package/template/src/components/dashboard/color-picker.tsx +64 -0
  106. package/template/src/components/dashboard/contacts-chart.tsx +69 -0
  107. package/template/src/components/dashboard/interactions-by-type-chart.tsx +121 -0
  108. package/template/src/components/dashboard/recent-activity.tsx +154 -0
  109. package/template/src/components/dashboard/stat-card.tsx +40 -40
  110. package/template/src/components/dashboard/status-distribution-chart.tsx +81 -0
  111. package/template/src/components/dashboard/tasks-pie-chart.tsx +37 -34
  112. package/template/src/components/dashboard/top-contacts-list.tsx +113 -0
  113. package/template/src/components/dashboard/upcoming-tasks-list.tsx +72 -81
  114. package/template/src/components/dashboard/widget-wrapper.tsx +36 -0
  115. package/template/src/components/date-picker.tsx +9 -6
  116. package/template/src/components/editor/upload-editor-image.ts +42 -0
  117. package/template/src/components/editor.tsx +161 -22
  118. package/template/src/components/email-template.tsx +2 -2
  119. package/template/src/components/global-search.tsx +30 -28
  120. package/template/src/components/header.tsx +178 -80
  121. package/template/src/components/inactive-account-guard.tsx +58 -0
  122. package/template/src/components/integration-notifications-listener.tsx +12 -0
  123. package/template/src/components/invitation-email-template.tsx +2 -2
  124. package/template/src/components/meet-cancellation-email-template.tsx +3 -3
  125. package/template/src/components/meet-confirmation-email-template.tsx +3 -3
  126. package/template/src/components/meet-update-email-template.tsx +3 -3
  127. package/template/src/components/page-header.tsx +5 -5
  128. package/template/src/components/protected-page.tsx +1 -1
  129. package/template/src/components/reset-password-email-template.tsx +2 -2
  130. package/template/src/components/settings/integrations/GoogleAdsIntegration.tsx +428 -0
  131. package/template/src/components/settings/integrations/GoogleSheetConfigMonitoringModal.tsx +680 -0
  132. package/template/src/components/settings/integrations/GoogleSheetIntegration.tsx +809 -0
  133. package/template/src/components/settings/integrations/ImportResultDialog.tsx +124 -0
  134. package/template/src/components/settings/integrations/IntegrationLogPanel.tsx +57 -0
  135. package/template/src/components/settings/integrations/IntegrationLogsTable.tsx +186 -0
  136. package/template/src/components/settings/integrations/MetaLeadIntegration.tsx +451 -0
  137. package/template/src/components/sidebar.tsx +45 -26
  138. package/template/src/components/skeleton.tsx +40 -43
  139. package/template/src/components/ui/accordion.tsx +2 -2
  140. package/template/src/components/ui/alert-dialog.tsx +1 -1
  141. package/template/src/components/ui/button.tsx +20 -9
  142. package/template/src/components/ui/components.tsx +1 -1
  143. package/template/src/components/ui/date-picker.tsx +422 -0
  144. package/template/src/components/ui/datetime-picker.tsx +338 -0
  145. package/template/src/components/ui/status-select.tsx +271 -0
  146. package/template/src/components/ui/tooltip.tsx +37 -0
  147. package/template/src/components/view-as-modal.tsx +13 -7
  148. package/template/src/contexts/app-toast-context.tsx +245 -57
  149. package/template/src/contexts/dashboard-theme-context.tsx +53 -0
  150. package/template/src/contexts/sidebar-context.tsx +22 -17
  151. package/template/src/contexts/task-reminder-context.tsx +134 -160
  152. package/template/src/contexts/view-as-context.tsx +33 -6
  153. package/template/src/hooks/use-focus-trap.ts +2 -2
  154. package/template/src/hooks/useIntegrationNotifications.ts +49 -0
  155. package/template/src/lib/auth.ts +8 -1
  156. package/template/src/lib/config-links.ts +14 -0
  157. package/template/src/lib/contact-duplicate.ts +79 -61
  158. package/template/src/lib/contact-interactions.ts +21 -21
  159. package/template/src/lib/contact-view-filters.ts +24 -64
  160. package/template/src/lib/contacts-list-url.ts +190 -0
  161. package/template/src/lib/dashboard-stats.ts +65 -7
  162. package/template/src/lib/dashboard-themes.ts +135 -0
  163. package/template/src/lib/date-utils.ts +127 -0
  164. package/template/src/lib/default-widgets.ts +12 -0
  165. package/template/src/lib/editor-html-image-dimensions.ts +172 -0
  166. package/template/src/lib/editor-image-limits.ts +19 -0
  167. package/template/src/lib/email-html-sanitize.ts +19 -0
  168. package/template/src/lib/encryption.ts +9 -6
  169. package/template/src/lib/fr-geography.ts +192 -0
  170. package/template/src/lib/google-calendar-agenda.ts +201 -0
  171. package/template/src/lib/google-calendar.ts +255 -5
  172. package/template/src/lib/google-sheet-sync-jobs.ts +96 -0
  173. package/template/src/lib/google-sheet-sync-runner.ts +514 -0
  174. package/template/src/lib/integration-import-log.ts +21 -0
  175. package/template/src/lib/permissions.ts +40 -10
  176. package/template/src/lib/prisma.ts +4 -1
  177. package/template/src/lib/qstash.ts +65 -0
  178. package/template/src/lib/reminder-state-server.ts +80 -0
  179. package/template/src/lib/reminder-state.ts +29 -0
  180. package/template/src/lib/supabase-storage.ts +113 -0
  181. package/template/src/lib/template-variables.ts +164 -23
  182. package/template/src/lib/utils.ts +45 -0
  183. package/template/src/lib/widget-registry.ts +173 -0
  184. package/template/src/lib/workflow-executor.ts +16 -70
  185. package/template/src/proxy.ts +1 -0
  186. package/template/vercel.json +3 -10
  187. package/template/skills-lock.json +0 -25
  188. package/template/src/components/dashboard/dashboard-content.tsx +0 -79
  189. package/template/src/lib/google-drive.ts +0 -1101
  190. package/template/src/types/yousign.ts +0 -52
@@ -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,8 +4,8 @@ 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
11
  function htmlToText(html: string): string {
@@ -102,6 +102,13 @@ export async function POST(request: Request) {
102
102
  return Response.json({ error: 'Non authentifié' }, { status: 401 });
103
103
  }
104
104
 
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
+
105
112
  let smtpConfig, smtpError;
106
113
  if (isResetPassword) {
107
114
  const result = await getAnyAdminSmtpConfig();
@@ -116,8 +123,11 @@ export async function POST(request: Request) {
116
123
  if (!smtpConfig) {
117
124
  const errorMsg =
118
125
  smtpError ||
119
- 'Aucune configuration SMTP trouvée. Veuillez configurer SMTP dans les paramètres.';
120
- 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
+ );
121
131
  }
122
132
 
123
133
  let emailComponent: React.ReactElement;
@@ -139,6 +149,7 @@ export async function POST(request: Request) {
139
149
  });
140
150
  }
141
151
 
152
+ const { render } = await import('@react-email/render');
142
153
  const emailHtml = await render(emailComponent);
143
154
  const emailText = htmlToText(emailHtml);
144
155
 
@@ -183,8 +194,9 @@ export async function POST(request: Request) {
183
194
  return Response.json(
184
195
  {
185
196
  error:
186
- "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.",
187
198
  details: error.message,
199
+ configLink: '/settings?section=system',
188
200
  },
189
201
  { status: 400 },
190
202
  );
@@ -100,6 +100,20 @@ export async function POST(request: NextRequest) {
100
100
  },
101
101
  });
102
102
 
103
+ await prisma.integrationImportLog.create({
104
+ data: {
105
+ integrationType: 'google_ads',
106
+ configId: config.id,
107
+ configName: config.name,
108
+ action: 'created',
109
+ actorId: session.user.id,
110
+ totalImported: 0,
111
+ totalDuplicates: 0,
112
+ totalUpdated: 0,
113
+ totalErrors: 0,
114
+ },
115
+ });
116
+
103
117
  return NextResponse.json({
104
118
  success: true,
105
119
  config: {
@@ -0,0 +1,97 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { auth } from '@/lib/auth';
3
+ import { prisma } from '@/lib/prisma';
4
+ import { encrypt } from '@/lib/encryption';
5
+ import {
6
+ getValidAccessToken,
7
+ getUserGoogleAccount,
8
+ listGoogleCalendars,
9
+ } from '@/lib/google-calendar';
10
+
11
+ /**
12
+ * GET /api/settings/google-calendar/calendars
13
+ * Liste les calendriers Google de l'utilisateur + préférences CRM (défaut, affichage agenda).
14
+ */
15
+ export async function GET(request: NextRequest) {
16
+ try {
17
+ const session = await auth.api.getSession({
18
+ headers: request.headers,
19
+ });
20
+
21
+ if (!session) {
22
+ return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
23
+ }
24
+
25
+ let googleAccount;
26
+ try {
27
+ googleAccount = await getUserGoogleAccount(session.user.id);
28
+ } catch (e: any) {
29
+ return NextResponse.json(
30
+ {
31
+ error: e.message || 'Compte Google non connecté',
32
+ calendars: [],
33
+ defaultGoogleCalendarId: null,
34
+ agendaVisibleGoogleCalendarIds: [],
35
+ agendaGoogleEventColor: null,
36
+ },
37
+ { status: 200 },
38
+ );
39
+ }
40
+
41
+ const accessToken = await getValidAccessToken(
42
+ googleAccount.accessToken,
43
+ googleAccount.refreshToken,
44
+ googleAccount.tokenExpiresAt,
45
+ );
46
+
47
+ if (accessToken !== googleAccount.accessToken) {
48
+ const tokenExpiresAt = new Date();
49
+ tokenExpiresAt.setSeconds(tokenExpiresAt.getSeconds() + 3600);
50
+ await prisma.userGoogleAccount.update({
51
+ where: { userId: session.user.id },
52
+ data: {
53
+ accessToken: encrypt(accessToken),
54
+ tokenExpiresAt,
55
+ },
56
+ });
57
+ }
58
+
59
+ let calendars;
60
+ try {
61
+ calendars = await listGoogleCalendars(accessToken);
62
+ } catch (listErr: any) {
63
+ const msg = listErr?.message || String(listErr);
64
+ const needsReconnect =
65
+ /403|insufficient|calendarList|Invalid Credentials/i.test(msg) ||
66
+ msg.includes('Erreur lors de la liste des calendriers');
67
+ return NextResponse.json(
68
+ {
69
+ error: needsReconnect
70
+ ? 'Reconnectez votre compte Google (Paramètres → Intégrations) pour autoriser la liste des calendriers.'
71
+ : msg,
72
+ calendars: [],
73
+ defaultGoogleCalendarId: googleAccount.defaultGoogleCalendarId ?? null,
74
+ agendaVisibleGoogleCalendarIds: normalizeAgendaIds(googleAccount.agendaVisibleGoogleCalendarIds),
75
+ agendaGoogleEventColor: googleAccount.agendaGoogleEventColor ?? null,
76
+ needsGoogleReconnect: Boolean(needsReconnect),
77
+ },
78
+ { status: 200 },
79
+ );
80
+ }
81
+
82
+ return NextResponse.json({
83
+ calendars,
84
+ defaultGoogleCalendarId: googleAccount.defaultGoogleCalendarId ?? null,
85
+ agendaVisibleGoogleCalendarIds: normalizeAgendaIds(googleAccount.agendaVisibleGoogleCalendarIds),
86
+ agendaGoogleEventColor: googleAccount.agendaGoogleEventColor ?? null,
87
+ });
88
+ } catch (error: any) {
89
+ console.error('GET google-calendar calendars:', error);
90
+ return NextResponse.json({ error: error.message || 'Erreur serveur' }, { status: 500 });
91
+ }
92
+ }
93
+
94
+ function normalizeAgendaIds(raw: unknown): string[] {
95
+ if (!raw || !Array.isArray(raw)) return [];
96
+ return raw.filter((x): x is string => typeof x === 'string' && x.trim() !== '');
97
+ }
@@ -0,0 +1,124 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { auth } from '@/lib/auth';
3
+ import { prisma, Prisma } from '@/lib/prisma';
4
+ import { encrypt } from '@/lib/encryption';
5
+ import {
6
+ getValidAccessToken,
7
+ getUserGoogleAccount,
8
+ assertWritableGoogleCalendar,
9
+ assertKnownGoogleCalendar,
10
+ } from '@/lib/google-calendar';
11
+
12
+ /**
13
+ * PATCH /api/settings/google-calendar
14
+ * Préférences : calendrier par défaut à la création, calendriers visibles dans l'agenda CRM.
15
+ */
16
+ export async function PATCH(request: NextRequest) {
17
+ try {
18
+ const session = await auth.api.getSession({
19
+ headers: request.headers,
20
+ });
21
+
22
+ if (!session) {
23
+ return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
24
+ }
25
+
26
+ const body = await request.json();
27
+ const { defaultGoogleCalendarId, agendaVisibleGoogleCalendarIds, agendaGoogleEventColor } = body as {
28
+ defaultGoogleCalendarId?: string | null;
29
+ agendaVisibleGoogleCalendarIds?: string[];
30
+ agendaGoogleEventColor?: string | null;
31
+ };
32
+
33
+ const googleAccount = await getUserGoogleAccount(session.user.id);
34
+
35
+ const accessToken = await getValidAccessToken(
36
+ googleAccount.accessToken,
37
+ googleAccount.refreshToken,
38
+ googleAccount.tokenExpiresAt,
39
+ );
40
+
41
+ if (accessToken !== googleAccount.accessToken) {
42
+ const tokenExpiresAt = new Date();
43
+ tokenExpiresAt.setSeconds(tokenExpiresAt.getSeconds() + 3600);
44
+ await prisma.userGoogleAccount.update({
45
+ where: { userId: session.user.id },
46
+ data: {
47
+ accessToken: encrypt(accessToken),
48
+ tokenExpiresAt,
49
+ },
50
+ });
51
+ }
52
+
53
+ const data: {
54
+ defaultGoogleCalendarId?: string | null;
55
+ agendaVisibleGoogleCalendarIds?: Prisma.InputJsonValue;
56
+ agendaGoogleEventColor?: string | null;
57
+ } = {};
58
+
59
+ if ('defaultGoogleCalendarId' in body) {
60
+ if (defaultGoogleCalendarId === null || defaultGoogleCalendarId === '') {
61
+ data.defaultGoogleCalendarId = null;
62
+ } else if (typeof defaultGoogleCalendarId === 'string') {
63
+ const cal = defaultGoogleCalendarId.trim();
64
+ await assertWritableGoogleCalendar(accessToken, cal);
65
+ data.defaultGoogleCalendarId = cal === 'primary' ? null : cal;
66
+ } else {
67
+ return NextResponse.json({ error: 'defaultGoogleCalendarId invalide' }, { status: 400 });
68
+ }
69
+ }
70
+
71
+ if (agendaVisibleGoogleCalendarIds !== undefined) {
72
+ if (!Array.isArray(agendaVisibleGoogleCalendarIds)) {
73
+ return NextResponse.json(
74
+ { error: 'agendaVisibleGoogleCalendarIds doit être un tableau' },
75
+ { status: 400 },
76
+ );
77
+ }
78
+ const ids = [...new Set(agendaVisibleGoogleCalendarIds.filter((x) => typeof x === 'string' && x.trim() !== '').map((x: string) => x.trim()))];
79
+ await Promise.all(ids.map((id) => assertKnownGoogleCalendar(accessToken, id)));
80
+ data.agendaVisibleGoogleCalendarIds = ids;
81
+ }
82
+
83
+ if ('agendaGoogleEventColor' in body) {
84
+ if (agendaGoogleEventColor === null || agendaGoogleEventColor === '') {
85
+ data.agendaGoogleEventColor = null;
86
+ } else if (typeof agendaGoogleEventColor === 'string') {
87
+ const c = agendaGoogleEventColor.trim();
88
+ if (!/^#[0-9A-Fa-f]{6}$/.test(c)) {
89
+ return NextResponse.json(
90
+ { error: 'agendaGoogleEventColor doit être au format #RRGGBB' },
91
+ { status: 400 },
92
+ );
93
+ }
94
+ data.agendaGoogleEventColor = c;
95
+ } else {
96
+ return NextResponse.json({ error: 'agendaGoogleEventColor invalide' }, { status: 400 });
97
+ }
98
+ }
99
+
100
+ if (Object.keys(data).length === 0) {
101
+ return NextResponse.json({ error: 'Aucun champ à mettre à jour' }, { status: 400 });
102
+ }
103
+
104
+ const updated = await prisma.userGoogleAccount.update({
105
+ where: { userId: session.user.id },
106
+ data,
107
+ });
108
+
109
+ return NextResponse.json({
110
+ success: true,
111
+ defaultGoogleCalendarId: updated.defaultGoogleCalendarId ?? null,
112
+ agendaVisibleGoogleCalendarIds: normalizeAgendaIds(updated.agendaVisibleGoogleCalendarIds),
113
+ agendaGoogleEventColor: updated.agendaGoogleEventColor ?? null,
114
+ });
115
+ } catch (error: any) {
116
+ console.error('PATCH google-calendar settings:', error);
117
+ return NextResponse.json({ error: error.message || 'Erreur serveur' }, { status: 500 });
118
+ }
119
+ }
120
+
121
+ function normalizeAgendaIds(raw: unknown): string[] {
122
+ if (!raw || !Array.isArray(raw)) return [];
123
+ return raw.filter((x): x is string => typeof x === 'string' && x.trim() !== '');
124
+ }
@@ -159,6 +159,20 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
159
159
  data: updateData,
160
160
  });
161
161
 
162
+ await prisma.integrationImportLog.create({
163
+ data: {
164
+ integrationType: 'google_sheet',
165
+ configId: config.id,
166
+ configName: config.name,
167
+ action: 'updated',
168
+ actorId: session.user.id,
169
+ totalImported: 0,
170
+ totalDuplicates: 0,
171
+ totalUpdated: 0,
172
+ totalErrors: 0,
173
+ },
174
+ });
175
+
162
176
  return NextResponse.json({
163
177
  success: true,
164
178
  config: {
@@ -216,6 +230,20 @@ export async function DELETE(
216
230
  where: { id },
217
231
  });
218
232
 
233
+ await prisma.integrationImportLog.create({
234
+ data: {
235
+ integrationType: 'google_sheet',
236
+ configId: existingConfig.id,
237
+ configName: existingConfig.name,
238
+ action: 'deleted',
239
+ actorId: session.user.id,
240
+ totalImported: 0,
241
+ totalDuplicates: 0,
242
+ totalUpdated: 0,
243
+ totalErrors: 0,
244
+ },
245
+ });
246
+
219
247
  return NextResponse.json({
220
248
  success: true,
221
249
  message: 'Configuration Google Sheets supprimée avec succès.',
@@ -56,9 +56,7 @@ export async function POST(request: NextRequest) {
56
56
  );
57
57
  }
58
58
 
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');
59
+ const { getAdminGoogleAccount } = await import('@/lib/google-calendar');
62
60
  let googleAccount;
63
61
  try {
64
62
  googleAccount = await getAdminGoogleAccount();
@@ -67,7 +65,8 @@ export async function POST(request: NextRequest) {
67
65
  {
68
66
  error:
69
67
  error.message ||
70
- 'Aucun compte Google configuré. Veuillez demander à un administrateur de connecter son compte Google dans les paramètres.',
68
+ 'Aucun compte Google configuré. Veuillez demander à un administrateur de connecter son compte Google pour configurer l\'intégration Google Sheets.',
69
+ configLink: '/settings?section=integrations',
71
70
  },
72
71
  { status: 400 },
73
72
  );
@@ -125,6 +124,28 @@ export async function POST(request: NextRequest) {
125
124
  emailColumn: ['email', 'e-mail', 'mail', 'courriel', 'mél', 'mel'],
126
125
  cityColumn: ['ville', 'city', 'localité', 'locality'],
127
126
  postalCodeColumn: ['code postal', 'postal code', 'cp', 'zip', 'zipcode', 'code_postal'],
127
+ companyNameColumn: [
128
+ 'société',
129
+ 'societe',
130
+ 'company',
131
+ 'company name',
132
+ 'company_name',
133
+ 'company-name',
134
+ 'companyname',
135
+ 'organisation',
136
+ 'organization',
137
+ 'organisme',
138
+ 'org',
139
+ 'org name',
140
+ 'org_name',
141
+ 'org-name',
142
+ 'business',
143
+ 'business name',
144
+ 'business_name',
145
+ 'entreprise',
146
+ 'nom entreprise',
147
+ 'nom société',
148
+ ],
128
149
  originColumn: [
129
150
  'origine',
130
151
  'origin',
@@ -133,6 +154,18 @@ export async function POST(request: NextRequest) {
133
154
  'campaign',
134
155
  'origine de la campagne',
135
156
  ],
157
+ websiteColumn: [
158
+ 'site',
159
+ 'site web',
160
+ 'website',
161
+ 'url',
162
+ 'web',
163
+ 'site internet',
164
+ ],
165
+ linkedinColumn: ['linkedin', 'linkedin url', 'linkedin profile', 'linkedin profil'],
166
+ facebookColumn: ['facebook', 'facebook url', 'fb', 'facebook profile', 'facebook profil'],
167
+ twitterColumn: ['twitter', 'twitter url', 'x', 'x.com', 'twitter profile', 'twitter profil'],
168
+ instagramColumn: ['instagram', 'instagram url', 'insta', 'instagram profile', 'instagram profil'],
136
169
  };
137
170
 
138
171
  const mapping: Record<string, string> = {};
@@ -35,13 +35,19 @@ export async function POST(request: NextRequest) {
35
35
 
36
36
  const spreadsheetId = extractSpreadsheetId(sheetUrl);
37
37
 
38
- const { getAdminGoogleAccount } = await import('@/lib/google-drive');
38
+ const { getAdminGoogleAccount } = await import('@/lib/google-calendar');
39
39
  let googleAccount;
40
40
  try {
41
41
  googleAccount = await getAdminGoogleAccount();
42
42
  } catch (error: unknown) {
43
- const message = error instanceof Error ? error.message : 'Aucun compte Google configuré.';
44
- return NextResponse.json({ error: message }, { status: 400 });
43
+ const message =
44
+ error instanceof Error
45
+ ? error.message
46
+ : 'Aucun compte Google configuré. Veuillez demander à un administrateur de connecter son compte Google pour configurer l\'intégration Google Sheets.';
47
+ return NextResponse.json(
48
+ { error: message, configLink: '/settings?section=integrations' },
49
+ { status: 400 },
50
+ );
45
51
  }
46
52
 
47
53
  const accessToken = await getValidAccessToken(
@@ -223,6 +223,20 @@ export async function POST(request: NextRequest) {
223
223
  },
224
224
  });
225
225
 
226
+ await prisma.integrationImportLog.create({
227
+ data: {
228
+ integrationType: 'google_sheet',
229
+ configId: config.id,
230
+ configName: config.name,
231
+ action: 'created',
232
+ actorId: session.user.id,
233
+ totalImported: 0,
234
+ totalDuplicates: 0,
235
+ totalUpdated: 0,
236
+ totalErrors: 0,
237
+ },
238
+ });
239
+
226
240
  return NextResponse.json({
227
241
  success: true,
228
242
  config: {
@@ -0,0 +1,93 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { prisma, Prisma } from '@/lib/prisma';
3
+ import { checkPermissions } from '@/lib/check-permission';
4
+ import { auth } from '@/lib/auth';
5
+
6
+ const INTEGRATION_PERMISSIONS = [
7
+ 'integrations.google_sheets.manage',
8
+ 'integrations.meta.manage',
9
+ 'integrations.google_ads.manage',
10
+ ] as const;
11
+
12
+ function hasAnyIntegrationPermission(perms: Record<string, boolean>): boolean {
13
+ return INTEGRATION_PERMISSIONS.some((p) => perms[p]);
14
+ }
15
+
16
+ const SYNC_ACTIONS = ['synced', 'synced_manual', 'synced_auto'] as const;
17
+
18
+ // GET /api/settings/integrations/logs - Liste paginée des logs d'intégration
19
+ export async function GET(request: NextRequest) {
20
+ try {
21
+ const session = await auth.api.getSession({ headers: request.headers });
22
+ if (!session) {
23
+ return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
24
+ }
25
+ const perms = await checkPermissions(...INTEGRATION_PERMISSIONS);
26
+ if (!hasAnyIntegrationPermission(perms)) {
27
+ return NextResponse.json({ error: 'Accès refusé' }, { status: 403 });
28
+ }
29
+
30
+ const { searchParams } = new URL(request.url);
31
+ const integrationType = searchParams.get('integrationType') || undefined;
32
+ const configId = searchParams.get('configId') || undefined;
33
+ const page = Math.max(1, Number.parseInt(searchParams.get('page') || '1', 10));
34
+ const pageSize = Math.min(50, Math.max(10, Number.parseInt(searchParams.get('pageSize') || '20', 10)));
35
+ const since = searchParams.get('since') || undefined;
36
+
37
+ const where: Prisma.IntegrationImportLogWhereInput = {};
38
+ if (integrationType) where.integrationType = integrationType;
39
+ if (configId) where.configId = configId;
40
+ if (since) {
41
+ const sinceDate = new Date(since);
42
+ if (!Number.isNaN(sinceDate.getTime())) where.createdAt = { gte: sinceDate };
43
+ }
44
+ // Synchronisations : afficher si au moins un contact concerné OU une erreur (cohérent avec ce qui est persisté)
45
+ where.OR = [
46
+ { action: { notIn: [...SYNC_ACTIONS] } },
47
+ { action: { in: [...SYNC_ACTIONS] }, totalImported: { gt: 0 } },
48
+ { action: { in: [...SYNC_ACTIONS] }, totalDuplicates: { gt: 0 } },
49
+ { action: { in: [...SYNC_ACTIONS] }, totalUpdated: { gt: 0 } },
50
+ { action: { in: [...SYNC_ACTIONS] }, totalErrors: { gt: 0 } },
51
+ ];
52
+
53
+ const [logs, total] = await Promise.all([
54
+ prisma.integrationImportLog.findMany({
55
+ where,
56
+ include: {
57
+ actor: {
58
+ select: { id: true, name: true, email: true },
59
+ },
60
+ },
61
+ orderBy: { createdAt: 'desc' },
62
+ skip: (page - 1) * pageSize,
63
+ take: pageSize,
64
+ }),
65
+ prisma.integrationImportLog.count({ where }),
66
+ ]);
67
+
68
+ return NextResponse.json({
69
+ logs: logs.map((log) => ({
70
+ id: log.id,
71
+ integrationType: log.integrationType,
72
+ configId: log.configId,
73
+ configName: log.configName,
74
+ action: log.action,
75
+ actorId: log.actorId,
76
+ actor: log.actor,
77
+ totalImported: log.totalImported,
78
+ totalDuplicates: log.totalDuplicates,
79
+ totalUpdated: log.totalUpdated,
80
+ totalErrors: log.totalErrors,
81
+ errorDetails: log.errorDetails,
82
+ createdAt: log.createdAt,
83
+ })),
84
+ total,
85
+ page,
86
+ pageSize,
87
+ totalPages: Math.ceil(total / pageSize),
88
+ });
89
+ } catch (error: any) {
90
+ console.error('Erreur lors de la récupération des logs d\'intégration:', error);
91
+ return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
92
+ }
93
+ }
@@ -0,0 +1,67 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { prisma } from '@/lib/prisma';
3
+ import { checkPermissions } from '@/lib/check-permission';
4
+ import { auth } from '@/lib/auth';
5
+
6
+ const INTEGRATION_PERMISSIONS = [
7
+ 'integrations.google_sheets.manage',
8
+ 'integrations.meta.manage',
9
+ 'integrations.google_ads.manage',
10
+ ] as const;
11
+
12
+ function hasAnyIntegrationPermission(perms: Record<string, boolean>): boolean {
13
+ return INTEGRATION_PERMISSIONS.some((p) => perms[p]);
14
+ }
15
+
16
+ const NOTIFICATION_ACTIONS = ['synced', 'synced_manual', 'synced_auto', 'created'] as const;
17
+
18
+ // GET /api/settings/integrations/notifications?since=ISO_DATE
19
+ // Retourne les logs récents pour le polling (notifications admin)
20
+ export async function GET(request: NextRequest) {
21
+ try {
22
+ const session = await auth.api.getSession({ headers: request.headers });
23
+ if (!session) {
24
+ return NextResponse.json({ error: 'Non authentifié' }, { status: 401 });
25
+ }
26
+ const perms = await checkPermissions(...INTEGRATION_PERMISSIONS);
27
+ if (!hasAnyIntegrationPermission(perms)) {
28
+ return NextResponse.json({ notifications: [] });
29
+ }
30
+
31
+ const { searchParams } = new URL(request.url);
32
+ const sinceParam = searchParams.get('since');
33
+ if (!sinceParam) {
34
+ return NextResponse.json({ notifications: [] });
35
+ }
36
+ const since = new Date(sinceParam);
37
+ if (Number.isNaN(since.getTime())) {
38
+ return NextResponse.json({ notifications: [] });
39
+ }
40
+
41
+ const logs = await prisma.integrationImportLog.findMany({
42
+ where: {
43
+ createdAt: { gt: since },
44
+ action: { in: [...NOTIFICATION_ACTIONS] },
45
+ },
46
+ orderBy: { createdAt: 'asc' },
47
+ take: 50,
48
+ });
49
+
50
+ const notifications = logs.map((log) => ({
51
+ id: log.id,
52
+ integrationType: log.integrationType,
53
+ configName: log.configName,
54
+ action: log.action,
55
+ totalImported: log.totalImported,
56
+ totalDuplicates: log.totalDuplicates,
57
+ totalUpdated: log.totalUpdated,
58
+ totalErrors: log.totalErrors,
59
+ createdAt: log.createdAt,
60
+ }));
61
+
62
+ return NextResponse.json({ notifications });
63
+ } catch (error: any) {
64
+ console.error('Erreur lors de la récupération des notifications d\'intégration:', error);
65
+ return NextResponse.json({ notifications: [] });
66
+ }
67
+ }
@@ -67,7 +67,6 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
67
67
  id: config.id,
68
68
  name: config.name,
69
69
  pageId: config.pageId,
70
- verifyToken: config.verifyToken,
71
70
  active: config.active,
72
71
  defaultStatusId: config.defaultStatusId,
73
72
  defaultAssignedUserId: config.defaultAssignedUserId,
@@ -33,7 +33,6 @@ export async function GET(request: NextRequest) {
33
33
  id: config.id,
34
34
  name: config.name,
35
35
  pageId: config.pageId,
36
- verifyToken: config.verifyToken,
37
36
  active: config.active,
38
37
  defaultStatusId: config.defaultStatusId,
39
38
  defaultAssignedUserId: config.defaultAssignedUserId,
@@ -109,13 +108,26 @@ export async function POST(request: NextRequest) {
109
108
  },
110
109
  });
111
110
 
111
+ await prisma.integrationImportLog.create({
112
+ data: {
113
+ integrationType: 'meta_lead',
114
+ configId: config.id,
115
+ configName: config.name,
116
+ action: 'created',
117
+ actorId: session.user.id,
118
+ totalImported: 0,
119
+ totalDuplicates: 0,
120
+ totalUpdated: 0,
121
+ totalErrors: 0,
122
+ },
123
+ });
124
+
112
125
  return NextResponse.json({
113
126
  success: true,
114
127
  config: {
115
128
  id: config.id,
116
129
  name: config.name,
117
130
  pageId: config.pageId,
118
- verifyToken: config.verifyToken,
119
131
  active: config.active,
120
132
  defaultStatusId: config.defaultStatusId,
121
133
  defaultAssignedUserId: config.defaultAssignedUserId,