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
@@ -1,19 +1,32 @@
1
1
  'use client';
2
2
 
3
- import { useState, useEffect, useRef } from 'react';
3
+ import { useState, useEffect, useRef, useMemo } from 'react';
4
+ import useSWR from 'swr';
4
5
  import { useRouter, useSearchParams } from 'next/navigation';
5
6
  import { useSession } from '@/lib/auth-client';
6
7
  import { useUserRole } from '@/hooks/use-user-role';
7
- import { cn, indexToColumn } from '@/lib/utils';
8
+ import { cn, devToast } from '@/lib/utils';
9
+ import {
10
+ DEFAULT_GOOGLE_AGENDA_EVENT_COLOR,
11
+ normalizeAgendaGoogleEventColor,
12
+ } from '@/lib/google-calendar-agenda';
13
+
14
+ const GOOGLE_AGENDA_COLOR_PRESETS = [
15
+ '#3b82f6',
16
+ '#22c55e',
17
+ '#a855f7',
18
+ '#f97316',
19
+ '#ef4444',
20
+ '#14b8a6',
21
+ '#ec4899',
22
+ '#eab308',
23
+ ] as const;
8
24
  import Link from 'next/link';
9
25
  import { LazyEditor as Editor, type DefaultTemplateRef } from '@/components/lazy-editor';
10
26
  import AddressAutocomplete from '@/components/address-autocomplete';
11
27
  import {
12
28
  Eye,
13
29
  EyeOff,
14
- Plus,
15
- Trash2,
16
- ArrowRight,
17
30
  Settings,
18
31
  Grid3x3,
19
32
  Monitor,
@@ -21,9 +34,15 @@ import {
21
34
  RefreshCw,
22
35
  } from 'lucide-react';
23
36
  import { ProtectedPage } from '@/components/protected-page';
37
+ import { SIGNATURE_MAX_IMAGE_BYTES } from '@/lib/editor-image-limits';
24
38
  import { useConfirm } from '@/hooks/use-confirm';
25
39
  import { useAppToast } from '@/contexts/app-toast-context';
26
40
 
41
+ import { IntegrationLogPanel } from '@/components/settings/integrations/IntegrationLogPanel';
42
+ import { GoogleSheetIntegration } from '@/components/settings/integrations/GoogleSheetIntegration';
43
+ import { MetaLeadIntegration } from '@/components/settings/integrations/MetaLeadIntegration';
44
+ import { GoogleAdsIntegration } from '@/components/settings/integrations/GoogleAdsIntegration';
45
+
27
46
  type MainSection = 'general' | 'app' | 'system' | 'integrations';
28
47
  const MAIN_SECTIONS = new Set<MainSection>(['general', 'app', 'system', 'integrations']);
29
48
 
@@ -39,6 +58,7 @@ export default function SettingsPage() {
39
58
  const { isAdmin } = useUserRole();
40
59
  const { confirm, ConfirmDialog } = useConfirm();
41
60
  const toast = useAppToast();
61
+
42
62
  const [showPasswordForm, setShowPasswordForm] = useState(false);
43
63
  const [passwordData, setPasswordData] = useState({
44
64
  currentPassword: '',
@@ -93,6 +113,10 @@ export default function SettingsPage() {
93
113
  const [smtpSuccess, setSmtpSuccess] = useState('');
94
114
  const [smtpTesting, setSmtpTesting] = useState(false);
95
115
  const [showSmtpPassword, setShowSmtpPassword] = useState(false);
116
+ const [showSmtpForm, setShowSmtpForm] = useState(true);
117
+ const [smtpSignatureSaving, setSmtpSignatureSaving] = useState(false);
118
+ const [smtpSignatureError, setSmtpSignatureError] = useState('');
119
+ const [smtpSignatureSuccess, setSmtpSignatureSuccess] = useState('');
96
120
  const [smtpTestResult, setSmtpTestResult] = useState<{
97
121
  success: boolean;
98
122
  message: string;
@@ -122,15 +146,6 @@ export default function SettingsPage() {
122
146
  const [statusSuccess, setStatusSuccess] = useState('');
123
147
  const [statusSaving, setStatusSaving] = useState(false);
124
148
 
125
- // État pour Google Calendar
126
- // État pour Google Drive (admin uniquement)
127
- const [googleDriveAccount, setGoogleDriveAccount] = useState<{
128
- email: string | null;
129
- connected: boolean;
130
- } | null>(null);
131
- const [googleDriveLoading, setGoogleDriveLoading] = useState(true);
132
- const [googleDriveDisconnecting, setGoogleDriveDisconnecting] = useState(false);
133
-
134
149
  // État pour Google Calendar (tous les utilisateurs)
135
150
  const [googleCalendarAccount, setGoogleCalendarAccount] = useState<{
136
151
  email: string | null;
@@ -138,83 +153,13 @@ export default function SettingsPage() {
138
153
  } | null>(null);
139
154
  const [googleCalendarLoading, setGoogleCalendarLoading] = useState(true);
140
155
  const [googleCalendarDisconnecting, setGoogleCalendarDisconnecting] = useState(false);
156
+ const [gCalPrefsSaving, setGCalPrefsSaving] = useState(false);
157
+ const [gCalDefaultId, setGCalDefaultId] = useState('');
158
+ const [gCalVisibleIds, setGCalVisibleIds] = useState<string[]>([]);
159
+ const [gCalEventColor, setGCalEventColor] = useState(DEFAULT_GOOGLE_AGENDA_EVENT_COLOR);
141
160
 
142
- // État pour l'intégration Meta Lead Ads (admin uniquement)
143
- const [metaLeadLoading, setMetaLeadLoading] = useState(true);
144
- const [metaLeadSaving, setMetaLeadSaving] = useState(false);
145
- const [metaLeadError, setMetaLeadError] = useState('');
146
- const [metaLeadSuccess, setMetaLeadSuccess] = useState('');
147
- const [metaLeadUsers, setMetaLeadUsers] = useState<{ id: string; name: string; email: string }[]>(
148
- [],
149
- );
150
- const [metaLeadConfigs, setMetaLeadConfigs] = useState<
151
- Array<{
152
- id: string;
153
- name: string;
154
- active: boolean;
155
- pageId: string;
156
- verifyToken: string;
157
- defaultStatusId: string | null;
158
- defaultAssignedUserId: string | null;
159
- }>
160
- >([]);
161
- const [showMetaLeadModal, setShowMetaLeadModal] = useState(false);
162
- const [editingMetaLeadConfig, setEditingMetaLeadConfig] = useState<string | null>(null);
163
- const [metaLeadFormData, setMetaLeadFormData] = useState<{
164
- name: string;
165
- active: boolean;
166
- pageId: string;
167
- accessToken: string;
168
- verifyToken: string;
169
- defaultStatusId: string | null;
170
- defaultAssignedUserId: string | null;
171
- }>({
172
- name: '',
173
- active: true,
174
- pageId: '',
175
- accessToken: '',
176
- verifyToken: '',
177
- defaultStatusId: null,
178
- defaultAssignedUserId: null,
179
- });
180
-
181
- // État pour l'intégration Google Ads Lead Forms (admin uniquement)
182
- const [googleAdsLoading, setGoogleAdsLoading] = useState(true);
183
- const [googleAdsSaving, setGoogleAdsSaving] = useState(false);
184
- const [googleAdsError, setGoogleAdsError] = useState('');
185
- const [googleAdsSuccess, setGoogleAdsSuccess] = useState('');
186
- const [googleAdsConfigs, setGoogleAdsConfigs] = useState<
187
- Array<{
188
- id: string;
189
- name: string;
190
- active: boolean;
191
- webhookKey: string;
192
- defaultStatusId: string | null;
193
- defaultAssignedUserId: string | null;
194
- }>
195
- >([]);
196
- const [showGoogleAdsModal, setShowGoogleAdsModal] = useState(false);
197
- const [editingGoogleAdsConfig, setEditingGoogleAdsConfig] = useState<string | null>(null);
198
- const [googleAdsFormData, setGoogleAdsFormData] = useState<{
199
- name: string;
200
- active: boolean;
201
- webhookKey: string;
202
- defaultStatusId: string | null;
203
- defaultAssignedUserId: string | null;
204
- }>({
205
- name: '',
206
- active: true,
207
- webhookKey: '',
208
- defaultStatusId: null,
209
- defaultAssignedUserId: null,
210
- });
211
-
212
- // État pour l'intégration Google Sheets (admin uniquement)
213
- const [googleSheetLoading, setGoogleSheetLoading] = useState(true);
214
- const [googleSheetSaving, setGoogleSheetSaving] = useState(false);
215
- const [googleSheetSyncing, setGoogleSheetSyncing] = useState(false);
216
- const [googleSheetError, setGoogleSheetError] = useState('');
217
- const [googleSheetSuccess, setGoogleSheetSuccess] = useState('');
161
+ // Utilisateurs pour les intégrations (admin uniquement)
162
+ const [integrationUsers, setIntegrationUsers] = useState<{ id: string; name: string; email: string }[]>([]);
218
163
  // Motifs de fermeture (ClosingReasons)
219
164
  const [closingReasons, setClosingReasons] = useState<Array<{ id: string; name: string }>>([]);
220
165
  const [closingReasonsLoading, setClosingReasonsLoading] = useState(false);
@@ -229,70 +174,56 @@ export default function SettingsPage() {
229
174
  const [closingReasonError, setClosingReasonError] = useState('');
230
175
  const [closingReasonSuccess, setClosingReasonSuccess] = useState('');
231
176
 
232
- const [googleSheetConfigs, setGoogleSheetConfigs] = useState<
233
- Array<{
234
- id: string;
235
- name: string;
236
- active: boolean;
237
- spreadsheetId: string;
238
- sheetName: string;
239
- headerRow: number;
240
- phoneColumn: string;
241
- firstNameColumn: string | null;
242
- lastNameColumn: string | null;
243
- emailColumn: string | null;
244
- cityColumn: string | null;
245
- postalCodeColumn: string | null;
246
- originColumn: string | null;
247
- defaultStatusId: string | null;
248
- defaultAssignedUserId: string | null;
249
- }>
250
- >([]);
251
- const [showGoogleSheetModal, setShowGoogleSheetModal] = useState(false);
252
- const [editingGoogleSheetConfig, setEditingGoogleSheetConfig] = useState<string | null>(null);
253
- const [googleSheetStep, setGoogleSheetStep] = useState<1 | 2>(1);
254
- const [googleSheetFormData, setGoogleSheetFormData] = useState<{
255
- name: string;
256
- active: boolean;
257
- sheetUrl: string;
258
- sheetName: string;
259
- headerRow: string;
260
- defaultStatusId: string | null;
261
- defaultAssignedUserId: string | null;
262
- }>({
263
- name: '',
264
- active: true,
265
- sheetUrl: '',
266
- sheetName: '',
267
- headerRow: '1',
268
- defaultStatusId: null,
269
- defaultAssignedUserId: null,
270
- });
271
-
272
- // Structure pour les mappings de colonnes
273
- interface ColumnMapping {
274
- id: string;
275
- columnName: string; // Nom de la colonne dans Google Sheets
276
- action: 'map' | 'note' | 'ignore';
277
- crmField?: string; // Champ CRM si action = 'map'
278
- }
279
-
280
- const [googleSheetMappings, setGoogleSheetMappings] = useState<ColumnMapping[]>([]);
281
- const [googleSheetPreview, setGoogleSheetPreview] = useState<Array<Record<string, string>>>([]);
282
- const [googleSheetHeaders, setGoogleSheetHeaders] = useState<string[]>([]);
283
-
284
- // Selecteur visuel de feuille / en-tete Google Sheet
285
- const [gsPreviewStep, setGsPreviewStep] = useState<'url' | 'sheet' | 'header'>('url');
286
- const [gsAvailableSheets, setGsAvailableSheets] = useState<string[]>([]);
287
- const [gsRawRows, setGsRawRows] = useState<string[][]>([]);
288
- const [gsSelectedHeaderRow, setGsSelectedHeaderRow] = useState<number>(0);
289
- const [gsLoadingPreview, setGsLoadingPreview] = useState(false);
177
+ // Panel des logs d'intégration
178
+ const [showLogPanel, setShowLogPanel] = useState(false);
179
+ const [logPanelType, setLogPanelType] = useState<'google_sheet' | 'meta_lead' | 'google_ads'>('google_sheet');
180
+ const [logPanelConfigId, setLogPanelConfigId] = useState<string | undefined>();
181
+ const [logPanelConfigName, setLogPanelConfigName] = useState<string | undefined>();
290
182
 
291
183
  // États pour la navigation
292
184
  const [mainSection, setMainSection] = useState<MainSection>(() =>
293
185
  resolveMainSection(searchParams.get('section')),
294
186
  );
295
187
 
188
+ const gCalDetailsKey =
189
+ mainSection === 'integrations' && googleCalendarAccount?.connected
190
+ ? '/api/settings/google-calendar/calendars'
191
+ : null;
192
+ const { data: gCalDetails, mutate: mutateGCalDetails } = useSWR<{
193
+ calendars?: Array<{ id: string; summary: string; primary?: boolean; accessRole?: string }>;
194
+ defaultGoogleCalendarId?: string | null;
195
+ agendaVisibleGoogleCalendarIds?: string[];
196
+ agendaGoogleEventColor?: string | null;
197
+ error?: string;
198
+ needsGoogleReconnect?: boolean;
199
+ }>(gCalDetailsKey, async (url: string) => {
200
+ const response = await fetch(url);
201
+ return response.json();
202
+ });
203
+
204
+ const writableGoogleCalendars = useMemo(
205
+ () =>
206
+ (gCalDetails?.calendars ?? []).filter(
207
+ (c) => c.accessRole === 'owner' || c.accessRole === 'writer',
208
+ ),
209
+ [gCalDetails?.calendars],
210
+ );
211
+
212
+ useEffect(() => {
213
+ if (!gCalDetails?.calendars?.length) return;
214
+ const def =
215
+ gCalDetails.defaultGoogleCalendarId ||
216
+ gCalDetails.calendars.find((c) => c.primary)?.id ||
217
+ 'primary';
218
+ setGCalDefaultId(def);
219
+ setGCalVisibleIds(
220
+ Array.isArray(gCalDetails.agendaVisibleGoogleCalendarIds)
221
+ ? [...gCalDetails.agendaVisibleGoogleCalendarIds]
222
+ : [],
223
+ );
224
+ setGCalEventColor(normalizeAgendaGoogleEventColor(gCalDetails.agendaGoogleEventColor));
225
+ }, [gCalDetails]);
226
+
296
227
  useEffect(() => {
297
228
  if (!nameError) return;
298
229
  toast.error(nameError);
@@ -333,6 +264,16 @@ export default function SettingsPage() {
333
264
  toast.success(smtpSuccess);
334
265
  setSmtpSuccess('');
335
266
  }, [smtpSuccess, toast]);
267
+ useEffect(() => {
268
+ if (!smtpSignatureError) return;
269
+ toast.error(smtpSignatureError);
270
+ setSmtpSignatureError('');
271
+ }, [smtpSignatureError, toast]);
272
+ useEffect(() => {
273
+ if (!smtpSignatureSuccess) return;
274
+ toast.success(smtpSignatureSuccess);
275
+ setSmtpSignatureSuccess('');
276
+ }, [smtpSignatureSuccess, toast]);
336
277
  useEffect(() => {
337
278
  if (!statusError) return;
338
279
  toast.error(statusError);
@@ -353,36 +294,6 @@ export default function SettingsPage() {
353
294
  toast.success(closingReasonSuccess);
354
295
  setClosingReasonSuccess('');
355
296
  }, [closingReasonSuccess, toast]);
356
- useEffect(() => {
357
- if (!googleSheetError) return;
358
- toast.error(googleSheetError);
359
- setGoogleSheetError('');
360
- }, [googleSheetError, toast]);
361
- useEffect(() => {
362
- if (!googleSheetSuccess) return;
363
- toast.success(googleSheetSuccess);
364
- setGoogleSheetSuccess('');
365
- }, [googleSheetSuccess, toast]);
366
- useEffect(() => {
367
- if (!metaLeadError) return;
368
- toast.error(metaLeadError);
369
- setMetaLeadError('');
370
- }, [metaLeadError, toast]);
371
- useEffect(() => {
372
- if (!metaLeadSuccess) return;
373
- toast.success(metaLeadSuccess);
374
- setMetaLeadSuccess('');
375
- }, [metaLeadSuccess, toast]);
376
- useEffect(() => {
377
- if (!googleAdsError) return;
378
- toast.error(googleAdsError);
379
- setGoogleAdsError('');
380
- }, [googleAdsError, toast]);
381
- useEffect(() => {
382
- if (!googleAdsSuccess) return;
383
- toast.success(googleAdsSuccess);
384
- setGoogleAdsSuccess('');
385
- }, [googleAdsSuccess, toast]);
386
297
  // Mettre à jour le nom quand la session change
387
298
  useEffect(() => {
388
299
  if (session?.user?.name) {
@@ -457,17 +368,20 @@ export default function SettingsPage() {
457
368
  signature: data.signature || '',
458
369
  });
459
370
  setSmtpConfigured(true);
371
+ setShowSmtpForm(false);
460
372
  // Injecter la signature existante dans l'éditeur si disponible
461
373
  if (smtpSignatureEditorRef.current && data.signature) {
462
374
  smtpSignatureEditorRef.current.injectHTML(data.signature);
463
375
  }
464
376
  } else {
465
377
  setSmtpConfigured(false);
378
+ setShowSmtpForm(true);
466
379
  }
467
380
  }
468
381
  } catch (error) {
469
382
  console.error('Erreur lors du chargement de la config SMTP:', error);
470
383
  setSmtpConfigured(false);
384
+ setShowSmtpForm(true);
471
385
  } finally {
472
386
  setSmtpLoading(false);
473
387
  }
@@ -503,75 +417,38 @@ export default function SettingsPage() {
503
417
  }
504
418
  }, [isAdmin]);
505
419
 
506
- // Charger le statut de connexion Google Drive et Calendar
507
420
  useEffect(() => {
508
421
  const fetchGoogleStatus = async () => {
509
422
  try {
510
- setGoogleDriveLoading(true);
511
423
  setGoogleCalendarLoading(true);
512
424
  const response = await fetch('/api/auth/google/status');
513
425
  if (response.ok) {
514
426
  const data = await response.json();
515
- setGoogleDriveAccount(data.drive || { email: null, connected: false });
516
427
  setGoogleCalendarAccount(data.calendar || { email: null, connected: false });
517
428
  } else {
518
- setGoogleDriveAccount({ email: null, connected: false });
519
429
  setGoogleCalendarAccount({ email: null, connected: false });
520
430
  }
521
431
  } catch (error) {
522
432
  console.error('Erreur lors du chargement du statut Google:', error);
523
- setGoogleDriveAccount({ email: null, connected: false });
524
433
  setGoogleCalendarAccount({ email: null, connected: false });
525
434
  } finally {
526
- setGoogleDriveLoading(false);
527
435
  setGoogleCalendarLoading(false);
528
436
  }
529
437
  };
530
438
  fetchGoogleStatus();
531
439
  }, []);
532
440
 
533
- // Charger la configuration Meta Lead Ads et la liste des utilisateurs (admin uniquement)
441
+ // Charger les utilisateurs et les motifs de fermeture (admin uniquement)
534
442
  useEffect(() => {
535
443
  if (!isAdmin) return;
536
444
 
537
- const fetchLeadConfigs = async () => {
445
+ const fetchAdminData = async () => {
538
446
  try {
539
- setMetaLeadLoading(true);
540
- setGoogleAdsLoading(true);
541
- setGoogleSheetLoading(true);
542
- setMetaLeadError('');
543
- setGoogleAdsError('');
544
- setGoogleSheetError('');
545
-
546
- const [
547
- metaConfigRes,
548
- googleAdsConfigRes,
549
- googleSheetConfigRes,
550
- closingReasonsRes,
551
- usersRes,
552
- ] = await Promise.all([
553
- fetch('/api/settings/meta-leads'),
554
- fetch('/api/settings/google-ads'),
555
- fetch('/api/settings/google-sheet'),
447
+ const [closingReasonsRes, usersRes] = await Promise.all([
556
448
  fetch('/api/settings/closing-reasons'),
557
449
  fetch('/api/users/list'),
558
450
  ]);
559
451
 
560
- if (metaConfigRes.ok) {
561
- const configsData = await metaConfigRes.json();
562
- setMetaLeadConfigs(Array.isArray(configsData) ? configsData : []);
563
- }
564
-
565
- if (googleAdsConfigRes.ok) {
566
- const configsData = await googleAdsConfigRes.json();
567
- setGoogleAdsConfigs(Array.isArray(configsData) ? configsData : []);
568
- }
569
-
570
- if (googleSheetConfigRes.ok) {
571
- const configsData = await googleSheetConfigRes.json();
572
- setGoogleSheetConfigs(Array.isArray(configsData) ? configsData : []);
573
- }
574
-
575
452
  if (closingReasonsRes.ok) {
576
453
  const reasonsData = await closingReasonsRes.json();
577
454
  setClosingReasons(Array.isArray(reasonsData) ? reasonsData : []);
@@ -579,20 +456,16 @@ export default function SettingsPage() {
579
456
 
580
457
  if (usersRes.ok) {
581
458
  const usersData = await usersRes.json();
582
- setMetaLeadUsers(usersData);
459
+ setIntegrationUsers(usersData);
583
460
  }
584
461
  } catch (error) {
585
- console.error("Erreur lors du chargement de l'intégration Meta Lead Ads:", error);
586
- setMetaLeadError("Erreur lors du chargement de l'intégration Meta Lead Ads");
462
+ console.error('Erreur lors du chargement des données admin:', error);
587
463
  } finally {
588
- setMetaLeadLoading(false);
589
- setGoogleAdsLoading(false);
590
- setGoogleSheetLoading(false);
591
464
  setClosingReasonsLoading(false);
592
465
  }
593
466
  };
594
467
 
595
- fetchLeadConfigs();
468
+ fetchAdminData();
596
469
  }, [isAdmin]);
597
470
 
598
471
  // Gérer les messages de succès/erreur depuis l'URL
@@ -606,7 +479,6 @@ export default function SettingsPage() {
606
479
  fetch('/api/auth/google/status')
607
480
  .then((res) => res.json())
608
481
  .then((data) => {
609
- setGoogleDriveAccount(data.drive || { email: null, connected: false });
610
482
  setGoogleCalendarAccount(data.calendar || { email: null, connected: false });
611
483
  })
612
484
  .catch(() => {});
@@ -625,47 +497,42 @@ export default function SettingsPage() {
625
497
  window.location.href = '/api/auth/google';
626
498
  };
627
499
 
628
- const handleGoogleCalendarDisconnect = async () => {
629
- const confirmed = await confirm({
630
- title: 'Déconnecter Google Calendar',
631
- description: 'Êtes-vous sûr de vouloir déconnecter votre compte Google Calendar ?',
632
- confirmText: 'Déconnecter',
633
- cancelText: 'Annuler',
634
- variant: 'destructive',
635
- });
636
-
637
- if (!confirmed) {
638
- return;
639
- }
640
-
500
+ const handleSaveGoogleCalendarPrefs = async () => {
501
+ setGCalPrefsSaving(true);
641
502
  try {
642
- setGoogleCalendarDisconnecting(true);
643
- const response = await fetch('/api/auth/google/disconnect', {
644
- method: 'POST',
645
- });
503
+ const body: Record<string, unknown> = {
504
+ agendaGoogleEventColor:
505
+ gCalEventColor.toLowerCase() === DEFAULT_GOOGLE_AGENDA_EVENT_COLOR.toLowerCase()
506
+ ? null
507
+ : gCalEventColor,
508
+ };
509
+ if ((gCalDetails?.calendars?.length ?? 0) > 0) {
510
+ body.defaultGoogleCalendarId = gCalDefaultId || null;
511
+ body.agendaVisibleGoogleCalendarIds = gCalVisibleIds;
512
+ }
646
513
 
647
- if (response.ok) {
648
- setGoogleCalendarAccount({ email: null, connected: false });
649
- } else {
650
- const data = await response.json();
651
- console.error('Erreur lors de la déconnexion:', data.error);
514
+ const response = await fetch('/api/settings/google-calendar', {
515
+ method: 'PATCH',
516
+ headers: { 'Content-Type': 'application/json' },
517
+ body: JSON.stringify(body),
518
+ });
519
+ const data = await response.json();
520
+ if (!response.ok) {
521
+ throw new Error(data.error || 'Erreur lors de la sauvegarde');
652
522
  }
653
- } catch (error) {
654
- console.error('Erreur lors de la déconnexion Google Calendar:', error);
523
+ toast.success('Préférences Google Calendar enregistrées');
524
+ await mutateGCalDetails();
525
+ } catch (e: unknown) {
526
+ toast.error(devToast('Impossible de sauvegarder les préférences Google Calendar. Veuillez réessayer.', e));
655
527
  } finally {
656
- setGoogleCalendarDisconnecting(false);
528
+ setGCalPrefsSaving(false);
657
529
  }
658
530
  };
659
531
 
660
- const handleGoogleDriveConnect = () => {
661
- window.location.href = '/api/auth/google';
662
- };
663
-
664
- const handleGoogleDriveDisconnect = async () => {
532
+ const handleGoogleCalendarDisconnect = async () => {
665
533
  const confirmed = await confirm({
666
- title: 'Déconnecter Google Drive',
667
- description:
668
- "Êtes-vous sûr de vouloir déconnecter le compte Google Drive de l'administrateur ?",
534
+ title: 'Déconnecter Google Calendar',
535
+ description: 'Êtes-vous sûr de vouloir déconnecter votre compte Google Calendar ?',
669
536
  confirmText: 'Déconnecter',
670
537
  cancelText: 'Annuler',
671
538
  variant: 'destructive',
@@ -676,21 +543,22 @@ export default function SettingsPage() {
676
543
  }
677
544
 
678
545
  try {
679
- setGoogleDriveDisconnecting(true);
546
+ setGoogleCalendarDisconnecting(true);
680
547
  const response = await fetch('/api/auth/google/disconnect', {
681
548
  method: 'POST',
682
549
  });
683
550
 
684
551
  if (response.ok) {
685
- setGoogleDriveAccount({ email: null, connected: false });
552
+ setGoogleCalendarAccount({ email: null, connected: false });
553
+ toast.success('Google Calendar déconnecté');
686
554
  } else {
687
555
  const data = await response.json();
688
556
  console.error('Erreur lors de la déconnexion:', data.error);
689
557
  }
690
558
  } catch (error) {
691
- console.error('Erreur lors de la déconnexion Google Drive:', error);
559
+ console.error('Erreur lors de la déconnexion Google Calendar:', error);
692
560
  } finally {
693
- setGoogleDriveDisconnecting(false);
561
+ setGoogleCalendarDisconnecting(false);
694
562
  }
695
563
  };
696
564
 
@@ -718,7 +586,7 @@ export default function SettingsPage() {
718
586
  setCompanySuccess('');
719
587
  }, 5000);
720
588
  } catch (err: any) {
721
- setCompanyError(err.message);
589
+ setCompanyError(devToast("Erreur lors de la sauvegarde de l'entreprise", err));
722
590
  } finally {
723
591
  setCompanySaving(false);
724
592
  }
@@ -760,7 +628,7 @@ export default function SettingsPage() {
760
628
  setNameSuccess('');
761
629
  }, 5000);
762
630
  } catch (err: any) {
763
- setNameError(err.message);
631
+ setNameError(devToast('Erreur lors de la mise à jour du nom', err));
764
632
  } finally {
765
633
  setNameLoading(false);
766
634
  }
@@ -815,9 +683,10 @@ export default function SettingsPage() {
815
683
  }
816
684
 
817
685
  setSmtpSuccess('✅ Configuration SMTP testée et sauvegardée avec succès !');
686
+ setShowSmtpForm(false);
818
687
  } catch (saveErr: any) {
819
688
  // On affiche l’erreur de sauvegarde mais on garde le succès du test
820
- setSmtpError(saveErr.message || 'La connexion fonctionne mais la sauvegarde a échoué.');
689
+ setSmtpError(devToast('La connexion fonctionne mais la sauvegarde a échoué.', saveErr));
821
690
  } finally {
822
691
  setSmtpSaving(false);
823
692
  }
@@ -870,6 +739,8 @@ export default function SettingsPage() {
870
739
  }
871
740
 
872
741
  setSmtpSuccess('✅ Configuration SMTP sauvegardée avec succès !');
742
+ setSmtpConfigured(true);
743
+ setShowSmtpForm(false);
873
744
  // Si le test a réussi précédemment, garder l'indicateur de configuration
874
745
  if (smtpTestResult?.success) {
875
746
  setSmtpConfigured(true);
@@ -878,12 +749,53 @@ export default function SettingsPage() {
878
749
  setSmtpSuccess('');
879
750
  }, 5000);
880
751
  } catch (err: any) {
881
- setSmtpError(err.message);
752
+ setSmtpError(devToast('Erreur lors de la sauvegarde SMTP', err));
882
753
  } finally {
883
754
  setSmtpSaving(false);
884
755
  }
885
756
  };
886
757
 
758
+ const handleSmtpSignatureSave = async () => {
759
+ setSmtpSignatureError('');
760
+ setSmtpSignatureSuccess('');
761
+ setSmtpSignatureSaving(true);
762
+
763
+ try {
764
+ if (!smtpConfigured) {
765
+ throw new Error(
766
+ 'Vous devez d’abord enregistrer la configuration SMTP avant de sauvegarder la signature.',
767
+ );
768
+ }
769
+
770
+ let signatureHtml = smtpData.signature;
771
+ if (smtpSignatureEditorRef.current) {
772
+ try {
773
+ signatureHtml = await smtpSignatureEditorRef.current.getHTML();
774
+ } catch {
775
+ // on ignore l'erreur, on garde la valeur du state
776
+ }
777
+ }
778
+
779
+ const response = await fetch('/api/settings/smtp', {
780
+ method: 'PUT',
781
+ headers: { 'Content-Type': 'application/json' },
782
+ body: JSON.stringify({ signature: signatureHtml }),
783
+ });
784
+ const data = await response.json();
785
+
786
+ if (!response.ok) {
787
+ throw new Error(data.error || 'Erreur lors de la sauvegarde de la signature');
788
+ }
789
+
790
+ setSmtpData((prev) => ({ ...prev, signature: signatureHtml }));
791
+ setSmtpSignatureSuccess('✅ Signature sauvegardée avec succès !');
792
+ } catch (err: any) {
793
+ setSmtpSignatureError(devToast('Erreur lors de la sauvegarde de la signature', err));
794
+ } finally {
795
+ setSmtpSignatureSaving(false);
796
+ }
797
+ };
798
+
887
799
  const handleStatusSubmit = async (e: React.FormEvent) => {
888
800
  e.preventDefault();
889
801
  setStatusError('');
@@ -926,7 +838,7 @@ export default function SettingsPage() {
926
838
  setStatusSuccess('');
927
839
  }, 5000);
928
840
  } catch (err: any) {
929
- setStatusError(err.message);
841
+ setStatusError(devToast('Erreur lors de la sauvegarde du statut', err));
930
842
  } finally {
931
843
  setStatusSaving(false);
932
844
  }
@@ -968,7 +880,7 @@ export default function SettingsPage() {
968
880
  setStatuses(statusesData);
969
881
  }
970
882
  } catch (err: any) {
971
- setStatusError(err.message);
883
+ setStatusError(devToast('Erreur lors de la suppression du statut', err));
972
884
  }
973
885
  };
974
886
 
@@ -1027,651 +939,140 @@ export default function SettingsPage() {
1027
939
  setPasswordSuccess('');
1028
940
  }, 5000);
1029
941
  } catch (err: any) {
1030
- setPasswordError(err.message);
942
+ setPasswordError(devToast('Erreur lors du changement de mot de passe', err));
1031
943
  } finally {
1032
944
  setPasswordLoading(false);
1033
945
  }
1034
946
  };
1035
947
 
1036
- const handleMetaLeadSubmit = async (e: React.FormEvent) => {
1037
- e.preventDefault();
1038
- setMetaLeadError('');
1039
- setMetaLeadSuccess('');
1040
- setMetaLeadSaving(true);
1041
-
1042
- try {
1043
- const url = editingMetaLeadConfig
1044
- ? `/api/settings/meta-leads/${editingMetaLeadConfig}`
1045
- : '/api/settings/meta-leads';
1046
- const method = editingMetaLeadConfig ? 'PUT' : 'POST';
1047
-
1048
- const response = await fetch(url, {
1049
- method,
1050
- headers: { 'Content-Type': 'application/json' },
1051
- body: JSON.stringify(metaLeadFormData),
1052
- });
1053
-
1054
- const data = await response.json();
1055
-
1056
- if (!response.ok) {
1057
- throw new Error(data.error || 'Erreur lors de la sauvegarde de la configuration Meta');
1058
- }
1059
-
1060
- setMetaLeadSuccess(
1061
- editingMetaLeadConfig
1062
- ? '✅ Configuration Meta Lead Ads mise à jour avec succès !'
1063
- : '✅ Configuration Meta Lead Ads créée avec succès !',
1064
- );
1065
- setShowMetaLeadModal(false);
1066
- setEditingMetaLeadConfig(null);
1067
- setMetaLeadFormData({
1068
- name: '',
1069
- active: true,
1070
- pageId: '',
1071
- accessToken: '',
1072
- verifyToken: '',
1073
- defaultStatusId: null,
1074
- defaultAssignedUserId: null,
1075
- });
1076
-
1077
- // Recharger les configurations
1078
- const configsRes = await fetch('/api/settings/meta-leads');
1079
- if (configsRes.ok) {
1080
- const configsData = await configsRes.json();
1081
- setMetaLeadConfigs(Array.isArray(configsData) ? configsData : []);
1082
- }
1083
-
1084
- setTimeout(() => setMetaLeadSuccess(''), 5000);
1085
- } catch (error: any) {
1086
- setMetaLeadError(error.message || 'Erreur lors de la sauvegarde de la configuration Meta');
1087
- } finally {
1088
- setMetaLeadSaving(false);
1089
- }
948
+ const handleOpenLogs = (type: 'google_sheet' | 'meta_lead' | 'google_ads') => (configId: string, configName: string) => {
949
+ setLogPanelType(type);
950
+ setLogPanelConfigId(configId);
951
+ setLogPanelConfigName(configName);
952
+ setShowLogPanel(true);
1090
953
  };
1091
954
 
1092
- const handleGoogleAdsSubmit = async (e: React.FormEvent) => {
1093
- e.preventDefault();
1094
- setGoogleAdsError('');
1095
- setGoogleAdsSuccess('');
1096
- setGoogleAdsSaving(true);
1097
-
1098
- try {
1099
- const url = editingGoogleAdsConfig
1100
- ? `/api/settings/google-ads/${editingGoogleAdsConfig}`
1101
- : '/api/settings/google-ads';
1102
- const method = editingGoogleAdsConfig ? 'PUT' : 'POST';
1103
-
1104
- const response = await fetch(url, {
1105
- method,
1106
- headers: { 'Content-Type': 'application/json' },
1107
- body: JSON.stringify(googleAdsFormData),
1108
- });
1109
-
1110
- const data = await response.json();
1111
-
1112
- if (!response.ok) {
1113
- throw new Error(
1114
- data.error || 'Erreur lors de la sauvegarde de la configuration Google Ads',
1115
- );
1116
- }
1117
-
1118
- setGoogleAdsSuccess(
1119
- editingGoogleAdsConfig
1120
- ? '✅ Configuration Google Ads mise à jour avec succès !'
1121
- : '✅ Configuration Google Ads créée avec succès !',
1122
- );
1123
- setShowGoogleAdsModal(false);
1124
- setEditingGoogleAdsConfig(null);
1125
- setGoogleAdsFormData({
1126
- name: '',
1127
- active: true,
1128
- webhookKey: '',
1129
- defaultStatusId: null,
1130
- defaultAssignedUserId: null,
1131
- });
1132
-
1133
- // Recharger les configurations
1134
- const configsRes = await fetch('/api/settings/google-ads');
1135
- if (configsRes.ok) {
1136
- const configsData = await configsRes.json();
1137
- setGoogleAdsConfigs(Array.isArray(configsData) ? configsData : []);
1138
- }
1139
-
1140
- setTimeout(() => setGoogleAdsSuccess(''), 5000);
1141
- } catch (error: any) {
1142
- setGoogleAdsError(error.message || 'Erreur lors de la sauvegarde de la configuration Google');
1143
- } finally {
1144
- setGoogleAdsSaving(false);
1145
- }
1146
- };
1147
955
 
1148
- const handleGoogleSheetSubmit = async (e: React.FormEvent) => {
1149
- e.preventDefault();
1150
- setGoogleSheetError('');
1151
- setGoogleSheetSuccess('');
1152
- setGoogleSheetSaving(true);
956
+ // Les handlers d'intégration ont été déplacés dans les composants dédiés
957
+ // (GoogleSheetIntegration, MetaLeadIntegration, GoogleAdsIntegration)
1153
958
 
1154
- try {
1155
- // Vérifier qu'au moins un mapping téléphone est configuré
1156
- const phoneMapping = googleSheetMappings.find(
1157
- (m) => m.action === 'map' && m.crmField === 'phone' && m.columnName.trim() !== '',
1158
- );
959
+ return (
960
+ <ProtectedPage requiredPermission="settings.view">
961
+ <div className="kb-tab-scope bg-surface-page flex h-full flex-col">
962
+ {/* Header avec breadcrumbs */}
963
+ <div className="border-border bg-background/95 border-b px-4 py-4 backdrop-blur-sm sm:px-6 lg:px-8">
964
+ <div className="flex items-center justify-between">
965
+ <div>
966
+ <h1 className="text-foreground text-2xl font-bold">Paramètres</h1>
967
+ <p className="text-muted-foreground mt-1 text-sm">Home {'>'} Paramètres</p>
968
+ </div>
969
+ <div className="flex items-center gap-2">
970
+ <button
971
+ type="button"
972
+ onClick={() => window.location.reload()}
973
+ className="text-muted-foreground hover:bg-muted hover:text-foreground focus-visible:ring-primary cursor-pointer rounded-lg p-2 transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
974
+ aria-label="Actualiser"
975
+ >
976
+ <RefreshCw className="h-5 w-5" />
977
+ </button>
978
+ <button
979
+ type="button"
980
+ className="text-muted-foreground hover:bg-muted hover:text-foreground focus-visible:ring-primary cursor-pointer rounded-lg p-2 transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
981
+ aria-label="Paramètres"
982
+ >
983
+ <Settings className="h-5 w-5" />
984
+ </button>
985
+ </div>
986
+ </div>
987
+ </div>
1159
988
 
1160
- if (!phoneMapping) {
1161
- setGoogleSheetError('Le mapping du téléphone est obligatoire');
1162
- setGoogleSheetSaving(false);
1163
- return;
1164
- }
989
+ {/* Navbar horizontale avec sections principales */}
990
+ <div className="border-border bg-background/95 border-b px-4 sm:px-6 lg:px-8">
991
+ <div className="flex gap-1 overflow-x-auto">
992
+ <button
993
+ type="button"
994
+ onClick={() => handleMainSectionChange('general')}
995
+ className={cn(
996
+ 'focus-visible:ring-primary/40 flex cursor-pointer items-center gap-2 border-b-2 px-4 py-3 text-sm font-medium whitespace-nowrap transition-colors focus-visible:ring-2 focus-visible:outline-none focus-visible:ring-inset',
997
+ mainSection === 'general'
998
+ ? 'border-blue-700 text-blue-700'
999
+ : 'text-muted-foreground hover:border-border hover:text-foreground border-transparent',
1000
+ )}
1001
+ >
1002
+ <Settings className="h-4 w-4" />
1003
+ Paramètres Généraux
1004
+ </button>
1005
+ {isAdmin && (
1006
+ <button
1007
+ type="button"
1008
+ onClick={() => handleMainSectionChange('app')}
1009
+ className={cn(
1010
+ 'focus-visible:ring-primary/40 flex cursor-pointer items-center gap-2 border-b-2 px-4 py-3 text-sm font-medium whitespace-nowrap transition-colors focus-visible:ring-2 focus-visible:outline-none focus-visible:ring-inset',
1011
+ mainSection === 'app'
1012
+ ? 'border-blue-700 text-blue-700'
1013
+ : 'text-muted-foreground hover:border-border hover:text-foreground border-transparent',
1014
+ )}
1015
+ >
1016
+ <Grid3x3 className="h-4 w-4" />
1017
+ Paramètres de l&apos;Application
1018
+ </button>
1019
+ )}
1020
+ <button
1021
+ type="button"
1022
+ onClick={() => handleMainSectionChange('system')}
1023
+ className={cn(
1024
+ 'focus-visible:ring-primary/40 flex cursor-pointer items-center gap-2 border-b-2 px-4 py-3 text-sm font-medium whitespace-nowrap transition-colors focus-visible:ring-2 focus-visible:outline-none focus-visible:ring-inset',
1025
+ mainSection === 'system'
1026
+ ? 'border-blue-700 text-blue-700'
1027
+ : 'text-muted-foreground hover:border-border hover:text-foreground border-transparent',
1028
+ )}
1029
+ >
1030
+ <Monitor className="h-4 w-4" />
1031
+ Paramètres Système
1032
+ </button>
1033
+ <button
1034
+ type="button"
1035
+ onClick={() => handleMainSectionChange('integrations')}
1036
+ className={cn(
1037
+ 'focus-visible:ring-primary/40 flex cursor-pointer items-center gap-2 border-b-2 px-4 py-3 text-sm font-medium whitespace-nowrap transition-colors focus-visible:ring-2 focus-visible:outline-none focus-visible:ring-inset',
1038
+ mainSection === 'integrations'
1039
+ ? 'border-blue-700 text-blue-700'
1040
+ : 'text-muted-foreground hover:border-border hover:text-foreground border-transparent',
1041
+ )}
1042
+ >
1043
+ <Plug className="h-4 w-4" />
1044
+ Intégrations
1045
+ </button>
1046
+ </div>
1047
+ </div>
1165
1048
 
1166
- const url = editingGoogleSheetConfig
1167
- ? `/api/settings/google-sheet/${editingGoogleSheetConfig}`
1168
- : '/api/settings/google-sheet';
1169
- const method = editingGoogleSheetConfig ? 'PUT' : 'POST';
1170
-
1171
- const response = await fetch(url, {
1172
- method,
1173
- headers: { 'Content-Type': 'application/json' },
1174
- body: JSON.stringify({
1175
- ...googleSheetFormData,
1176
- columnMappings: googleSheetMappings,
1177
- }),
1178
- });
1179
-
1180
- const data = await response.json();
1181
-
1182
- if (!response.ok) {
1183
- throw new Error(
1184
- data.error || 'Erreur lors de la sauvegarde de la configuration Google Sheets',
1185
- );
1186
- }
1187
-
1188
- setGoogleSheetSuccess(
1189
- editingGoogleSheetConfig
1190
- ? '✅ Configuration Google Sheets mise à jour avec succès !'
1191
- : '✅ Configuration Google Sheets créée avec succès !',
1192
- );
1193
- setShowGoogleSheetModal(false);
1194
- setEditingGoogleSheetConfig(null);
1195
- setGoogleSheetStep(1);
1196
- setGoogleSheetFormData({
1197
- name: '',
1198
- active: true,
1199
- sheetUrl: '',
1200
- sheetName: '',
1201
- headerRow: '1',
1202
- defaultStatusId: null,
1203
- defaultAssignedUserId: null,
1204
- });
1205
- setGoogleSheetMappings([]);
1206
- setGsPreviewStep('url');
1207
- setGsAvailableSheets([]);
1208
- setGsRawRows([]);
1209
- setGsSelectedHeaderRow(0);
1210
-
1211
- // Recharger les configurations
1212
- const configsRes = await fetch('/api/settings/google-sheet');
1213
- if (configsRes.ok) {
1214
- const configsData = await configsRes.json();
1215
- setGoogleSheetConfigs(Array.isArray(configsData) ? configsData : []);
1216
- }
1217
-
1218
- setTimeout(() => setGoogleSheetSuccess(''), 5000);
1219
- } catch (error: any) {
1220
- setGoogleSheetError(
1221
- error.message || 'Erreur lors de la sauvegarde de la configuration Google Sheets',
1222
- );
1223
- } finally {
1224
- setGoogleSheetSaving(false);
1225
- }
1226
- };
1227
-
1228
- // Mapping automatique des colonnes Google Sheets (comme pour l'import CSV)
1229
- const handleGoogleSheetAutoMap = async (): Promise<boolean> => {
1230
- try {
1231
- setGoogleSheetError('');
1232
- setGoogleSheetSuccess('');
1233
-
1234
- if (
1235
- !googleSheetFormData.sheetUrl ||
1236
- !googleSheetFormData.sheetName ||
1237
- !googleSheetFormData.headerRow
1238
- ) {
1239
- setGoogleSheetError(
1240
- 'Veuillez renseigner le lien du Google Sheet, le nom de l’onglet et la ligne des en-têtes avant de continuer.',
1241
- );
1242
- return false;
1243
- }
1244
-
1245
- const response = await fetch('/api/settings/google-sheet/auto-map', {
1246
- method: 'POST',
1247
- headers: { 'Content-Type': 'application/json' },
1248
- body: JSON.stringify({
1249
- sheetUrl: googleSheetFormData.sheetUrl,
1250
- sheetName: googleSheetFormData.sheetName,
1251
- headerRow: googleSheetFormData.headerRow || '1',
1252
- }),
1253
- });
1254
-
1255
- const data = await response.json();
1256
-
1257
- if (!response.ok) {
1258
- throw new Error(
1259
- data.error || 'Erreur lors du mapping automatique des colonnes Google Sheets',
1260
- );
1261
- }
1262
-
1263
- const headers = data.headers || [];
1264
- const autoMapping = data.mapping || {};
1265
- const preview = data.preview || [];
1266
-
1267
- // Stocker les headers et l'aperçu
1268
- setGoogleSheetHeaders(headers);
1269
- setGoogleSheetPreview(preview);
1270
-
1271
- // Initialiser les mappings avec les colonnes trouvées
1272
- const initialMappings: ColumnMapping[] = [];
1273
-
1274
- // Créer un mapping pour chaque colonne trouvée
1275
- headers.forEach((header: string, index: number) => {
1276
- if (!header) return;
1277
-
1278
- // L'API auto-map retourne les index de colonnes (A, B, C), mais on utilise les noms réels
1279
- // Chercher si cette colonne (par index) a été auto-mappée
1280
- const columnLetter = indexToColumn(index);
1281
- const mappedField = Object.entries(autoMapping).find(
1282
- ([, columnLetterFromMapping]) => columnLetterFromMapping === columnLetter,
1283
- )?.[0];
1284
-
1285
- if (mappedField) {
1286
- // Convertir l'ancien format vers le nouveau
1287
- const crmFieldMap: Record<string, string> = {
1288
- phoneColumn: 'phone',
1289
- firstNameColumn: 'firstName',
1290
- lastNameColumn: 'lastName',
1291
- emailColumn: 'email',
1292
- cityColumn: 'city',
1293
- postalCodeColumn: 'postalCode',
1294
- originColumn: 'origin',
1295
- };
1296
-
1297
- initialMappings.push({
1298
- id: `mapping-${Date.now()}-${Math.random()}`,
1299
- columnName: header, // Utiliser le nom réel de la colonne
1300
- action: 'map',
1301
- crmField: crmFieldMap[mappedField] || undefined,
1302
- });
1303
- } else {
1304
- // Colonne non mappée automatiquement, laisser l'utilisateur choisir
1305
- initialMappings.push({
1306
- id: `mapping-${Date.now()}-${Math.random()}`,
1307
- columnName: header,
1308
- action: 'ignore',
1309
- });
1310
- }
1311
- });
1312
-
1313
- setGoogleSheetMappings(initialMappings);
1314
-
1315
- setGoogleSheetSuccess(
1316
- '✅ Colonnes détectées. Configurez maintenant le mapping des colonnes.',
1317
- );
1318
- return true;
1319
- } catch (error: any) {
1320
- console.error('Erreur lors du mapping automatique Google Sheets:', error);
1321
- setGoogleSheetError(
1322
- error.message || 'Erreur lors du mapping automatique des colonnes Google Sheets',
1323
- );
1324
- return false;
1325
- }
1326
- };
1327
-
1328
- const handleGoogleSheetSync = async () => {
1329
- setGoogleSheetError('');
1330
- setGoogleSheetSuccess('');
1331
- setGoogleSheetSyncing(true);
1332
-
1333
- try {
1334
- const response = await fetch('/api/integrations/google-sheet/sync', {
1335
- method: 'POST',
1336
- });
1337
-
1338
- const data = await response.json();
1339
-
1340
- if (!response.ok) {
1341
- throw new Error(data.error || 'Erreur lors de la synchronisation Google Sheets');
1342
- }
1343
-
1344
- const totalImported = data.totalImported || data.imported || 0;
1345
- const totalUpdated = data.totalUpdated || data.updated || 0;
1346
- const totalSkipped = data.totalSkipped || data.skipped || 0;
1347
- setGoogleSheetSuccess(
1348
- `✅ Synchronisation terminée : ${totalImported} nouveau(x) contact(s), ${totalUpdated} mis à jour, ${totalSkipped} ignoré(s).`,
1349
- );
1350
- setTimeout(() => setGoogleSheetSuccess(''), 8000);
1351
- } catch (error: any) {
1352
- setGoogleSheetError(
1353
- error.message || 'Erreur lors de la synchronisation des contacts Google Sheets',
1354
- );
1355
- } finally {
1356
- setGoogleSheetSyncing(false);
1357
- }
1358
- };
1359
-
1360
- // Fonctions pour gérer les configurations Meta Lead Ads
1361
- const handleEditMetaLead = (config: (typeof metaLeadConfigs)[0]) => {
1362
- setEditingMetaLeadConfig(config.id);
1363
- setMetaLeadFormData({
1364
- name: config.name,
1365
- active: config.active,
1366
- pageId: config.pageId,
1367
- accessToken: '', // Ne pas charger le token
1368
- verifyToken: config.verifyToken,
1369
- defaultStatusId: config.defaultStatusId,
1370
- defaultAssignedUserId: config.defaultAssignedUserId,
1371
- });
1372
- setShowMetaLeadModal(true);
1373
- setMetaLeadError('');
1374
- setMetaLeadSuccess('');
1375
- };
1376
-
1377
- const handleDeleteMetaLead = async (id: string) => {
1378
- const confirmed = await confirm({
1379
- title: 'Supprimer la configuration Meta Lead',
1380
- description: 'Êtes-vous sûr de vouloir supprimer cette configuration ?',
1381
- confirmText: 'Supprimer',
1382
- cancelText: 'Annuler',
1383
- variant: 'destructive',
1384
- });
1385
-
1386
- if (!confirmed) {
1387
- return;
1388
- }
1389
-
1390
- try {
1391
- const response = await fetch(`/api/settings/meta-leads/${id}`, {
1392
- method: 'DELETE',
1393
- });
1394
-
1395
- if (!response.ok) {
1396
- const data = await response.json();
1397
- throw new Error(data.error || 'Erreur lors de la suppression');
1398
- }
1399
-
1400
- setMetaLeadSuccess('✅ Configuration supprimée avec succès !');
1401
- setTimeout(() => setMetaLeadSuccess(''), 5000);
1402
-
1403
- // Recharger les configurations
1404
- const configsRes = await fetch('/api/settings/meta-leads');
1405
- if (configsRes.ok) {
1406
- const configsData = await configsRes.json();
1407
- setMetaLeadConfigs(Array.isArray(configsData) ? configsData : []);
1408
- }
1409
- } catch (error: any) {
1410
- setMetaLeadError(error.message || 'Erreur lors de la suppression');
1411
- }
1412
- };
1413
-
1414
- // Fonctions pour gérer les configurations Google Ads
1415
- const handleEditGoogleAds = (config: (typeof googleAdsConfigs)[0]) => {
1416
- setEditingGoogleAdsConfig(config.id);
1417
- setGoogleAdsFormData({
1418
- name: config.name,
1419
- active: config.active,
1420
- webhookKey: config.webhookKey,
1421
- defaultStatusId: config.defaultStatusId,
1422
- defaultAssignedUserId: config.defaultAssignedUserId,
1423
- });
1424
- setShowGoogleAdsModal(true);
1425
- setGoogleAdsError('');
1426
- setGoogleAdsSuccess('');
1427
- };
1428
-
1429
- const handleDeleteGoogleAds = async (id: string) => {
1430
- const confirmed = await confirm({
1431
- title: 'Supprimer la configuration Google Ads',
1432
- description: 'Êtes-vous sûr de vouloir supprimer cette configuration ?',
1433
- confirmText: 'Supprimer',
1434
- cancelText: 'Annuler',
1435
- variant: 'destructive',
1436
- });
1437
-
1438
- if (!confirmed) {
1439
- return;
1440
- }
1441
-
1442
- try {
1443
- const response = await fetch(`/api/settings/google-ads/${id}`, {
1444
- method: 'DELETE',
1445
- });
1446
-
1447
- if (!response.ok) {
1448
- const data = await response.json();
1449
- throw new Error(data.error || 'Erreur lors de la suppression');
1450
- }
1451
-
1452
- setGoogleAdsSuccess('✅ Configuration supprimée avec succès !');
1453
- setTimeout(() => setGoogleAdsSuccess(''), 5000);
1454
-
1455
- // Recharger les configurations
1456
- const configsRes = await fetch('/api/settings/google-ads');
1457
- if (configsRes.ok) {
1458
- const configsData = await configsRes.json();
1459
- setGoogleAdsConfigs(Array.isArray(configsData) ? configsData : []);
1460
- }
1461
- } catch (error: any) {
1462
- setGoogleAdsError(error.message || 'Erreur lors de la suppression');
1463
- }
1464
- };
1465
-
1466
- // Fonctions pour gérer les configurations Google Sheets
1467
- const handleEditGoogleSheet = (config: (typeof googleSheetConfigs)[0]) => {
1468
- setEditingGoogleSheetConfig(config.id);
1469
- setGoogleSheetFormData({
1470
- name: config.name,
1471
- active: config.active,
1472
- sheetUrl: config.spreadsheetId
1473
- ? `https://docs.google.com/spreadsheets/d/${config.spreadsheetId}/edit`
1474
- : '',
1475
- sheetName: config.sheetName,
1476
- headerRow: config.headerRow.toString(),
1477
- defaultStatusId: config.defaultStatusId,
1478
- defaultAssignedUserId: config.defaultAssignedUserId,
1479
- });
1480
-
1481
- // Charger les mappings existants (si disponibles) ou convertir l'ancien format
1482
- if ((config as any).columnMappings && Array.isArray((config as any).columnMappings)) {
1483
- setGoogleSheetMappings((config as any).columnMappings);
1484
- } else {
1485
- // Convertir l'ancien format vers le nouveau
1486
- const mappings: ColumnMapping[] = [];
1487
- const oldMappings = [
1488
- { column: config.phoneColumn, field: 'phone' },
1489
- { column: config.firstNameColumn, field: 'firstName' },
1490
- { column: config.lastNameColumn, field: 'lastName' },
1491
- { column: config.emailColumn, field: 'email' },
1492
- { column: config.cityColumn, field: 'city' },
1493
- { column: config.postalCodeColumn, field: 'postalCode' },
1494
- { column: config.originColumn, field: 'origin' },
1495
- ];
1496
-
1497
- oldMappings.forEach(({ column, field }) => {
1498
- if (column) {
1499
- mappings.push({
1500
- id: `mapping-${Date.now()}-${Math.random()}`,
1501
- columnName: column,
1502
- action: 'map',
1503
- crmField: field,
1504
- });
1505
- }
1506
- });
1507
-
1508
- setGoogleSheetMappings(mappings);
1509
- }
1510
-
1511
- setGoogleSheetStep(2);
1512
- setGsPreviewStep('header');
1513
- setGsAvailableSheets([]);
1514
- setGsRawRows([]);
1515
- setGsSelectedHeaderRow(config.headerRow - 1);
1516
- setShowGoogleSheetModal(true);
1517
- setGoogleSheetError('');
1518
- setGoogleSheetSuccess('');
1519
- };
1520
-
1521
- const handleDeleteGoogleSheet = async (id: string) => {
1522
- const confirmed = await confirm({
1523
- title: 'Supprimer la configuration Google Sheet',
1524
- description: 'Êtes-vous sûr de vouloir supprimer cette configuration ?',
1525
- confirmText: 'Supprimer',
1526
- cancelText: 'Annuler',
1527
- variant: 'destructive',
1528
- });
1529
-
1530
- if (!confirmed) {
1531
- return;
1532
- }
1533
-
1534
- try {
1535
- const response = await fetch(`/api/settings/google-sheet/${id}`, {
1536
- method: 'DELETE',
1537
- });
1538
-
1539
- if (!response.ok) {
1540
- const data = await response.json();
1541
- throw new Error(data.error || 'Erreur lors de la suppression');
1542
- }
1543
-
1544
- setGoogleSheetSuccess('✅ Configuration supprimée avec succès !');
1545
- setTimeout(() => setGoogleSheetSuccess(''), 5000);
1546
-
1547
- // Recharger les configurations
1548
- const configsRes = await fetch('/api/settings/google-sheet');
1549
- if (configsRes.ok) {
1550
- const configsData = await configsRes.json();
1551
- setGoogleSheetConfigs(Array.isArray(configsData) ? configsData : []);
1552
- }
1553
- } catch (error: any) {
1554
- setGoogleSheetError(error.message || 'Erreur lors de la suppression');
1555
- }
1556
- };
1557
-
1558
- return (
1559
- <ProtectedPage requiredPermission="settings.view">
1560
- <div className="kb-tab-scope bg-surface-page flex h-full flex-col">
1561
- {/* Header avec breadcrumbs */}
1562
- <div className="border-b border-border bg-background/95 px-4 py-4 backdrop-blur-sm sm:px-6 lg:px-8">
1563
- <div className="flex items-center justify-between">
1564
- <div>
1565
- <h1 className="text-2xl font-bold text-foreground">Paramètres</h1>
1566
- <p className="mt-1 text-sm text-muted-foreground">Home {'>'} Paramètres</p>
1567
- </div>
1568
- <div className="flex items-center gap-2">
1569
- <button
1570
- type="button"
1571
- onClick={() => window.location.reload()}
1572
- className="cursor-pointer rounded-lg p-2 text-muted-foreground transition-colors duration-200 hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2"
1573
- aria-label="Actualiser"
1574
- >
1575
- <RefreshCw className="h-5 w-5" />
1576
- </button>
1577
- <button
1578
- type="button"
1579
- className="cursor-pointer rounded-lg p-2 text-muted-foreground transition-colors duration-200 hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2"
1580
- aria-label="Paramètres"
1581
- >
1582
- <Settings className="h-5 w-5" />
1583
- </button>
1584
- </div>
1585
- </div>
1586
- </div>
1587
-
1588
- {/* Navbar horizontale avec sections principales */}
1589
- <div className="border-b border-border bg-background/95 px-4 sm:px-6 lg:px-8">
1590
- <div className="flex gap-1 overflow-x-auto">
1591
- <button
1592
- type="button"
1593
- onClick={() => handleMainSectionChange('general')}
1594
- className={cn(
1595
- 'flex cursor-pointer items-center gap-2 border-b-2 px-4 py-3 text-sm font-medium whitespace-nowrap transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 focus-visible:ring-inset',
1596
- mainSection === 'general'
1597
- ? 'border-blue-700 text-blue-700'
1598
- : 'border-transparent text-muted-foreground hover:border-border hover:text-foreground',
1599
- )}
1600
- >
1601
- <Settings className="h-4 w-4" />
1602
- Paramètres Généraux
1603
- </button>
1604
- {isAdmin && (
1605
- <button
1606
- type="button"
1607
- onClick={() => handleMainSectionChange('app')}
1608
- className={cn(
1609
- 'flex cursor-pointer items-center gap-2 border-b-2 px-4 py-3 text-sm font-medium whitespace-nowrap transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 focus-visible:ring-inset',
1610
- mainSection === 'app'
1611
- ? 'border-blue-700 text-blue-700'
1612
- : 'border-transparent text-muted-foreground hover:border-border hover:text-foreground',
1613
- )}
1614
- >
1615
- <Grid3x3 className="h-4 w-4" />
1616
- Paramètres de l&apos;Application
1617
- </button>
1618
- )}
1619
- <button
1620
- type="button"
1621
- onClick={() => handleMainSectionChange('system')}
1622
- className={cn(
1623
- 'flex cursor-pointer items-center gap-2 border-b-2 px-4 py-3 text-sm font-medium whitespace-nowrap transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 focus-visible:ring-inset',
1624
- mainSection === 'system'
1625
- ? 'border-blue-700 text-blue-700'
1626
- : 'border-transparent text-muted-foreground hover:border-border hover:text-foreground',
1627
- )}
1628
- >
1629
- <Monitor className="h-4 w-4" />
1630
- Paramètres Système
1631
- </button>
1632
- <button
1633
- type="button"
1634
- onClick={() => handleMainSectionChange('integrations')}
1635
- className={cn(
1636
- 'flex cursor-pointer items-center gap-2 border-b-2 px-4 py-3 text-sm font-medium whitespace-nowrap transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 focus-visible:ring-inset',
1637
- mainSection === 'integrations'
1638
- ? 'border-blue-700 text-blue-700'
1639
- : 'border-transparent text-muted-foreground hover:border-border hover:text-foreground',
1640
- )}
1641
- >
1642
- <Plug className="h-4 w-4" />
1643
- Intégrations
1644
- </button>
1645
- </div>
1646
- </div>
1647
-
1648
- {/* Content avec panneau principal */}
1649
- <div className="flex flex-1 overflow-hidden">
1650
- {/* Panneau de contenu principal */}
1651
- <div className="bg-surface-page flex-1 overflow-y-auto p-4 sm:p-6 lg:p-8">
1652
- <div className="mx-auto max-w-4xl space-y-4 sm:space-y-6">
1653
- {/* Section Paramètres Généraux */}
1654
- {mainSection === 'general' && (
1655
- <div className="space-y-6">
1656
- {/* Section Profil */}
1657
- <div className="rounded-lg border border-border bg-card p-6 shadow-(--shadow-card)">
1658
- <div className="mb-1 flex items-center justify-between">
1659
- <div>
1660
- <h2 className="text-lg font-bold text-foreground">Profil</h2>
1661
- </div>
1662
- </div>
1663
- <p className="mb-6 text-sm text-muted-foreground">
1664
- Gérez vos informations personnelles
1665
- </p>
1049
+ {/* Content avec panneau principal */}
1050
+ <div className="flex flex-1 overflow-hidden">
1051
+ {/* Panneau de contenu principal */}
1052
+ <div className="bg-surface-page flex-1 overflow-y-auto p-4 sm:p-6 lg:p-8">
1053
+ <div className="mx-auto max-w-4xl space-y-4 sm:space-y-6">
1054
+ {/* Section Paramètres Généraux */}
1055
+ {mainSection === 'general' && (
1056
+ <div className="space-y-6">
1057
+ {/* Section Profil */}
1058
+ <div className="border-border bg-card rounded-lg border p-6 shadow-(--shadow-card)">
1059
+ <div className="mb-1 flex items-center justify-between">
1060
+ <div>
1061
+ <h2 className="text-foreground text-lg font-bold">Profil</h2>
1062
+ </div>
1063
+ </div>
1064
+ <p className="text-muted-foreground mb-6 text-sm">
1065
+ Gérez vos informations personnelles
1066
+ </p>
1666
1067
 
1667
1068
  <div className="space-y-4">
1668
1069
  {/* Nom - Modifiable */}
1669
- <div className="border-b border-border/70 pb-4">
1070
+ <div className="border-border/70 border-b pb-4">
1670
1071
  {!showNameForm ? (
1671
1072
  <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
1672
1073
  <div className="min-w-0 flex-1">
1673
- <p className="font-medium text-foreground">Nom</p>
1674
- <p className="mt-1 truncate text-sm text-muted-foreground">
1074
+ <p className="text-foreground font-medium">Nom</p>
1075
+ <p className="text-muted-foreground mt-1 truncate text-sm">
1675
1076
  {session?.user?.name || 'Non défini'}
1676
1077
  </p>
1677
1078
  </div>
@@ -1682,7 +1083,7 @@ export default function SettingsPage() {
1682
1083
  setNameError('');
1683
1084
  setNameSuccess('');
1684
1085
  }}
1685
- className="w-full cursor-pointer rounded-lg border border-border px-4 py-2 text-sm font-medium text-foreground transition-colors duration-200 hover:bg-muted sm:w-auto"
1086
+ className="border-border text-foreground hover:bg-muted w-full cursor-pointer rounded-lg border px-4 py-2 text-sm font-medium transition-colors duration-200 sm:w-auto"
1686
1087
  >
1687
1088
  Modifier
1688
1089
  </button>
@@ -1690,13 +1091,15 @@ export default function SettingsPage() {
1690
1091
  ) : (
1691
1092
  <form onSubmit={handleNameUpdate} className="space-y-4">
1692
1093
  <div>
1693
- <label className="block text-sm font-medium text-foreground">Nom</label>
1094
+ <label className="text-foreground block text-sm font-medium">
1095
+ Nom
1096
+ </label>
1694
1097
  <input
1695
1098
  type="text"
1696
1099
  required
1697
1100
  value={nameValue}
1698
1101
  onChange={(e) => setNameValue(e.target.value)}
1699
- className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
1102
+ className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
1700
1103
  placeholder="Votre nom"
1701
1104
  />
1702
1105
  </div>
@@ -1705,7 +1108,7 @@ export default function SettingsPage() {
1705
1108
  <button
1706
1109
  type="submit"
1707
1110
  disabled={nameLoading}
1708
- className="w-full cursor-pointer rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700 focus:ring-2 focus:ring-gray-400/30 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto"
1111
+ className="w-full cursor-pointer rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700 focus:ring-2 focus:ring-gray-400/30 focus:ring-offset-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto"
1709
1112
  >
1710
1113
  {nameLoading ? 'Enregistrement...' : 'Enregistrer'}
1711
1114
  </button>
@@ -1776,7 +1179,7 @@ export default function SettingsPage() {
1776
1179
  currentPassword: e.target.value,
1777
1180
  })
1778
1181
  }
1779
- className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
1182
+ className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
1780
1183
  placeholder="••••••••"
1781
1184
  />
1782
1185
  </div>
@@ -1796,7 +1199,7 @@ export default function SettingsPage() {
1796
1199
  newPassword: e.target.value,
1797
1200
  })
1798
1201
  }
1799
- className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
1202
+ className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
1800
1203
  placeholder="••••••••"
1801
1204
  />
1802
1205
  <p className="mt-1 text-xs text-gray-500">Minimum 6 caractères</p>
@@ -1817,7 +1220,7 @@ export default function SettingsPage() {
1817
1220
  confirmPassword: e.target.value,
1818
1221
  })
1819
1222
  }
1820
- className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
1223
+ className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
1821
1224
  placeholder="••••••••"
1822
1225
  />
1823
1226
  </div>
@@ -1826,7 +1229,7 @@ export default function SettingsPage() {
1826
1229
  <button
1827
1230
  type="submit"
1828
1231
  disabled={passwordLoading}
1829
- className="w-full cursor-pointer rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700 focus:ring-2 focus:ring-gray-400/30 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto"
1232
+ className="w-full cursor-pointer rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700 focus:ring-2 focus:ring-gray-400/30 focus:ring-offset-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto"
1830
1233
  >
1831
1234
  {passwordLoading ? 'Modification...' : 'Modifier le mot de passe'}
1832
1235
  </button>
@@ -1879,7 +1282,7 @@ export default function SettingsPage() {
1879
1282
  onChange={(e) =>
1880
1283
  setCompanyData({ ...companyData, name: e.target.value })
1881
1284
  }
1882
- className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
1285
+ className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
1883
1286
  placeholder="Nom de l'entreprise"
1884
1287
  />
1885
1288
  </div>
@@ -1897,7 +1300,7 @@ export default function SettingsPage() {
1897
1300
  legalRepresentative: e.target.value,
1898
1301
  })
1899
1302
  }
1900
- className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
1303
+ className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
1901
1304
  placeholder="Nom du représentant légal"
1902
1305
  />
1903
1306
  <p className="mt-1 text-xs text-gray-500">
@@ -1915,7 +1318,7 @@ export default function SettingsPage() {
1915
1318
  onChange={(e) =>
1916
1319
  setCompanyData({ ...companyData, email: e.target.value })
1917
1320
  }
1918
- className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
1321
+ className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
1919
1322
  placeholder="contact@entreprise.com"
1920
1323
  />
1921
1324
  </div>
@@ -1930,7 +1333,7 @@ export default function SettingsPage() {
1930
1333
  onChange={(e) =>
1931
1334
  setCompanyData({ ...companyData, phone: e.target.value })
1932
1335
  }
1933
- className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
1336
+ className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
1934
1337
  placeholder="+33 1 23 45 67 89"
1935
1338
  />
1936
1339
  </div>
@@ -1945,7 +1348,7 @@ export default function SettingsPage() {
1945
1348
  onChange={(e) =>
1946
1349
  setCompanyData({ ...companyData, website: e.target.value })
1947
1350
  }
1948
- className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
1351
+ className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
1949
1352
  placeholder="https://www.entreprise.com"
1950
1353
  />
1951
1354
  </div>
@@ -1984,7 +1387,7 @@ export default function SettingsPage() {
1984
1387
  onChange={(e) =>
1985
1388
  setCompanyData({ ...companyData, city: e.target.value })
1986
1389
  }
1987
- className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
1390
+ className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
1988
1391
  placeholder="Paris"
1989
1392
  />
1990
1393
  </div>
@@ -1999,7 +1402,7 @@ export default function SettingsPage() {
1999
1402
  onChange={(e) =>
2000
1403
  setCompanyData({ ...companyData, postalCode: e.target.value })
2001
1404
  }
2002
- className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
1405
+ className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
2003
1406
  placeholder="75001"
2004
1407
  />
2005
1408
  </div>
@@ -2014,7 +1417,7 @@ export default function SettingsPage() {
2014
1417
  onChange={(e) =>
2015
1418
  setCompanyData({ ...companyData, country: e.target.value })
2016
1419
  }
2017
- className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
1420
+ className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
2018
1421
  placeholder="France"
2019
1422
  />
2020
1423
  </div>
@@ -2029,7 +1432,7 @@ export default function SettingsPage() {
2029
1432
  onChange={(e) =>
2030
1433
  setCompanyData({ ...companyData, siret: e.target.value })
2031
1434
  }
2032
- className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
1435
+ className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
2033
1436
  placeholder="123 456 789 00012"
2034
1437
  />
2035
1438
  </div>
@@ -2044,7 +1447,7 @@ export default function SettingsPage() {
2044
1447
  onChange={(e) =>
2045
1448
  setCompanyData({ ...companyData, vatNumber: e.target.value })
2046
1449
  }
2047
- className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
1450
+ className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
2048
1451
  placeholder="FR12 345678901"
2049
1452
  />
2050
1453
  </div>
@@ -2059,7 +1462,7 @@ export default function SettingsPage() {
2059
1462
  onChange={(e) =>
2060
1463
  setCompanyData({ ...companyData, logo: e.target.value })
2061
1464
  }
2062
- className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
1465
+ className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
2063
1466
  placeholder="https://example.com/logo.png"
2064
1467
  />
2065
1468
  </div>
@@ -2069,7 +1472,7 @@ export default function SettingsPage() {
2069
1472
  <button
2070
1473
  type="submit"
2071
1474
  disabled={companySaving}
2072
- className="cursor-pointer rounded-lg bg-blue-600 px-6 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700 focus:ring-2 focus:ring-gray-400/30 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
1475
+ className="cursor-pointer rounded-lg bg-blue-600 px-6 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700 focus:ring-2 focus:ring-gray-400/30 focus:ring-offset-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50"
2073
1476
  >
2074
1477
  {companySaving ? 'Enregistrement...' : 'Enregistrer'}
2075
1478
  </button>
@@ -2184,160 +1587,267 @@ export default function SettingsPage() {
2184
1587
  {smtpLoading ? (
2185
1588
  <div className="mt-6 text-center text-gray-500">Chargement...</div>
2186
1589
  ) : (
2187
- <form onSubmit={handleSmtpSubmit} className="mt-6 space-y-4">
2188
- <div className="grid grid-cols-1 gap-4 md:grid-cols-2">
2189
- <div>
2190
- <label className="block text-sm font-medium text-gray-700">
2191
- Serveur SMTP (Host) *
2192
- </label>
2193
- <input
2194
- type="text"
2195
- required
2196
- value={smtpData.host}
2197
- onChange={(e) => setSmtpData({ ...smtpData, host: e.target.value })}
2198
- className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
2199
- placeholder="smtp.gmail.com"
2200
- />
2201
- </div>
2202
-
2203
- <div>
2204
- <label className="block text-sm font-medium text-gray-700">Port *</label>
2205
- <input
2206
- type="text"
2207
- inputMode="numeric"
2208
- required
2209
- value={smtpData.port}
2210
- onChange={(e) => {
2211
- const value = e.target.value;
2212
- if (value === '' || /^\d+$/.test(value)) {
2213
- const num = Number(value);
2214
- if (value === '' || (num >= 1 && num <= 65535)) {
2215
- setSmtpData({ ...smtpData, port: value });
2216
- }
2217
- }
2218
- }}
2219
- className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
2220
- placeholder="587"
2221
- />
2222
- <p className="mt-1 text-xs text-gray-500">
2223
- Ports courants : 587 (TLS), 465 (SSL), 25 (non sécurisé)
2224
- </p>
2225
- </div>
2226
-
2227
- <div className="flex items-center">
2228
- <input
2229
- type="checkbox"
2230
- id="smtp-secure"
2231
- checked={smtpData.secure}
2232
- onChange={(e) => setSmtpData({ ...smtpData, secure: e.target.checked })}
2233
- className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-gray-400/30"
2234
- />
2235
- <label
2236
- htmlFor="smtp-secure"
2237
- className="ml-2 text-sm font-medium text-gray-700"
2238
- >
2239
- Connexion sécurisée (SSL/TLS)
2240
- </label>
2241
- </div>
2242
-
2243
- <div className="md:col-span-2">
2244
- <label className="block text-sm font-medium text-gray-700">
2245
- Nom d'utilisateur *
2246
- </label>
2247
- <input
2248
- type="text"
2249
- required
2250
- value={smtpData.username}
2251
- onChange={(e) => setSmtpData({ ...smtpData, username: e.target.value })}
2252
- className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
2253
- placeholder="votre.email@exemple.com"
2254
- />
2255
- </div>
2256
-
2257
- <div className="md:col-span-2">
2258
- <label className="block text-sm font-medium text-gray-700">
2259
- Mot de passe *
2260
- </label>
2261
- <div className="relative mt-1">
2262
- <input
2263
- type={showSmtpPassword ? 'text' : 'password'}
2264
- required
2265
- value={smtpData.password}
2266
- onChange={(e) =>
2267
- setSmtpData({ ...smtpData, password: e.target.value })
2268
- }
2269
- className="block w-full rounded-lg border border-gray-300 px-4 py-2 pr-10 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
2270
- placeholder="••••••••"
2271
- />
1590
+ <div className="mt-6 space-y-4">
1591
+ {!showSmtpForm && smtpConfigured ? (
1592
+ <div className="overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm">
1593
+ <div className="flex flex-col gap-3 border-b border-gray-100 bg-gray-50/60 px-4 py-3 sm:flex-row sm:items-center sm:justify-between">
1594
+ <div className="min-w-0">
1595
+ <p className="text-sm font-semibold text-gray-900">
1596
+ Configuration SMTP enregistrée
1597
+ </p>
1598
+ <p className="mt-0.5 text-xs text-gray-600">
1599
+ Les emails partent avec cette configuration.
1600
+ </p>
1601
+ </div>
2272
1602
  <button
2273
1603
  type="button"
2274
- onClick={() => setShowSmtpPassword((prev) => !prev)}
2275
- className="absolute inset-y-0 right-0 flex cursor-pointer items-center pr-3 text-gray-400 hover:text-gray-600"
2276
- aria-label={
2277
- showSmtpPassword
2278
- ? 'Masquer le mot de passe'
2279
- : 'Afficher le mot de passe'
2280
- }
1604
+ onClick={() => setShowSmtpForm(true)}
1605
+ className="cursor-pointer rounded-lg border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-50 focus:ring-2 focus:ring-gray-400/30 focus:ring-offset-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
2281
1606
  >
2282
- {showSmtpPassword ? <EyeOff /> : <Eye />}
1607
+ Modifier
2283
1608
  </button>
2284
1609
  </div>
2285
- <p className="mt-1 text-xs text-gray-500">
2286
- Pour Gmail, utilisez un mot de passe d'application
2287
- </p>
2288
- </div>
2289
1610
 
2290
- <div>
2291
- <label className="block text-sm font-medium text-gray-700">
2292
- Email expéditeur (From) *
2293
- </label>
2294
- <input
2295
- type="email"
2296
- required
2297
- value={smtpData.fromEmail}
2298
- onChange={(e) =>
2299
- setSmtpData({ ...smtpData, fromEmail: e.target.value })
2300
- }
2301
- className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
2302
- placeholder="votre.email@exemple.com"
2303
- />
1611
+ <dl className="grid grid-cols-1 gap-0 sm:grid-cols-2">
1612
+ <div className="space-y-1 border-b border-gray-100 px-4 py-3 sm:border-r">
1613
+ <dt className="text-xs font-medium text-gray-500">Serveur SMTP</dt>
1614
+ <dd className="min-w-0 truncate text-sm font-medium text-gray-900">
1615
+ {smtpData.host || '-'}
1616
+ </dd>
1617
+ </div>
1618
+ <div className="space-y-1 border-b border-gray-100 px-4 py-3">
1619
+ <dt className="text-xs font-medium text-gray-500">Port</dt>
1620
+ <dd className="text-sm font-medium text-gray-900">
1621
+ {smtpData.port || '-'}
1622
+ </dd>
1623
+ </div>
1624
+ <div className="space-y-1 border-b border-gray-100 px-4 py-3 sm:border-r sm:border-b-0">
1625
+ <dt className="text-xs font-medium text-gray-500">Nom d&apos;utilisateur</dt>
1626
+ <dd className="min-w-0 truncate text-sm font-medium text-gray-900">
1627
+ {smtpData.username || '-'}
1628
+ </dd>
1629
+ </div>
1630
+ <div className="space-y-1 border-b border-gray-100 px-4 py-3 sm:border-b-0">
1631
+ <dt className="text-xs font-medium text-gray-500">Sécurité</dt>
1632
+ <dd>
1633
+ <span
1634
+ className={cn(
1635
+ 'inline-flex items-center rounded-full px-2.5 py-1 text-xs font-medium',
1636
+ smtpData.secure
1637
+ ? 'bg-green-100 text-green-800'
1638
+ : 'bg-gray-100 text-gray-700',
1639
+ )}
1640
+ >
1641
+ {smtpData.secure ? 'SSL/TLS activé' : 'Sans SSL/TLS'}
1642
+ </span>
1643
+ </dd>
1644
+ </div>
1645
+ <div className="space-y-1 border-t border-gray-100 px-4 py-3 sm:border-r">
1646
+ <dt className="text-xs font-medium text-gray-500">Email expéditeur</dt>
1647
+ <dd className="min-w-0 truncate text-sm font-medium text-gray-900">
1648
+ {smtpData.fromEmail || '-'}
1649
+ </dd>
1650
+ </div>
1651
+ <div className="space-y-1 border-t border-gray-100 px-4 py-3">
1652
+ <dt className="text-xs font-medium text-gray-500">Nom expéditeur</dt>
1653
+ <dd className="min-w-0 truncate text-sm font-medium text-gray-900">
1654
+ {smtpData.fromName || '-'}
1655
+ </dd>
1656
+ </div>
1657
+ </dl>
2304
1658
  </div>
1659
+ ) : (
1660
+ <form onSubmit={handleSmtpSubmit} className="space-y-4">
1661
+ <div className="grid grid-cols-1 gap-4 md:grid-cols-2">
1662
+ <div>
1663
+ <label className="block text-sm font-medium text-gray-700">
1664
+ Serveur SMTP (Host) *
1665
+ </label>
1666
+ <input
1667
+ type="text"
1668
+ required
1669
+ value={smtpData.host}
1670
+ onChange={(e) => setSmtpData({ ...smtpData, host: e.target.value })}
1671
+ className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
1672
+ placeholder="smtp.gmail.com"
1673
+ />
1674
+ </div>
2305
1675
 
2306
- <div>
2307
- <label className="block text-sm font-medium text-gray-700">
2308
- Nom expéditeur (optionnel)
2309
- </label>
2310
- <input
2311
- type="text"
2312
- value={smtpData.fromName}
2313
- onChange={(e) => setSmtpData({ ...smtpData, fromName: e.target.value })}
2314
- className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
2315
- placeholder="Votre Nom"
2316
- />
2317
- </div>
2318
- </div>
1676
+ <div>
1677
+ <label className="block text-sm font-medium text-gray-700">
1678
+ Port *
1679
+ </label>
1680
+ <input
1681
+ type="text"
1682
+ inputMode="numeric"
1683
+ required
1684
+ value={smtpData.port}
1685
+ onChange={(e) => {
1686
+ const value = e.target.value;
1687
+ if (value === '' || /^\d+$/.test(value)) {
1688
+ const num = Number(value);
1689
+ if (value === '' || (num >= 1 && num <= 65535)) {
1690
+ setSmtpData({ ...smtpData, port: value });
1691
+ }
1692
+ }
1693
+ }}
1694
+ className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
1695
+ placeholder="587"
1696
+ />
1697
+ <p className="mt-1 text-xs text-gray-500">
1698
+ Ports courants : 587 (TLS), 465 (SSL), 25 (non sécurisé)
1699
+ </p>
1700
+ </div>
2319
1701
 
2320
- <div className="mt-4 rounded-lg bg-blue-50 p-3 text-xs text-blue-900">
2321
- <p className="font-medium">Utiliser Gmail avec SMTP</p>
2322
- <p className="mt-1">
2323
- Si vous utilisez une adresse Gmail, vous devez créer un{' '}
2324
- <span className="font-semibold">mot de passe d&apos;application</span>{' '}
2325
- dédié et le renseigner dans le champ &quot;Mot de passe&quot; ci-dessus.
2326
- Rendez-vous sur{' '}
2327
- <Link
2328
- href="https://myaccount.google.com/apppasswords"
2329
- target="_blank"
2330
- rel="noreferrer"
2331
- className="font-semibold underline"
2332
- >
2333
- la page des mots de passe d&apos;application Google
2334
- </Link>{' '}
2335
- pour en générer un (compte Google protégé par la validation en deux étapes
2336
- requis).
2337
- </p>
2338
- </div>
1702
+ <div className="flex items-center">
1703
+ <input
1704
+ type="checkbox"
1705
+ id="smtp-secure"
1706
+ checked={smtpData.secure}
1707
+ onChange={(e) =>
1708
+ setSmtpData({ ...smtpData, secure: e.target.checked })
1709
+ }
1710
+ className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-gray-400/30"
1711
+ />
1712
+ <label
1713
+ htmlFor="smtp-secure"
1714
+ className="ml-2 text-sm font-medium text-gray-700"
1715
+ >
1716
+ Connexion sécurisée (SSL/TLS)
1717
+ </label>
1718
+ </div>
1719
+
1720
+ <div className="md:col-span-2">
1721
+ <label className="block text-sm font-medium text-gray-700">
1722
+ Nom d&apos;utilisateur *
1723
+ </label>
1724
+ <input
1725
+ type="text"
1726
+ required
1727
+ value={smtpData.username}
1728
+ onChange={(e) =>
1729
+ setSmtpData({ ...smtpData, username: e.target.value })
1730
+ }
1731
+ className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
1732
+ placeholder="votre.email@exemple.com"
1733
+ />
1734
+ </div>
1735
+
1736
+ <div className="md:col-span-2">
1737
+ <label className="block text-sm font-medium text-gray-700">
1738
+ Mot de passe *
1739
+ </label>
1740
+ <div className="relative mt-1">
1741
+ <input
1742
+ type={showSmtpPassword ? 'text' : 'password'}
1743
+ required
1744
+ value={smtpData.password}
1745
+ onChange={(e) =>
1746
+ setSmtpData({ ...smtpData, password: e.target.value })
1747
+ }
1748
+ className="block w-full rounded-lg border border-gray-300 px-4 py-2 pr-10 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
1749
+ placeholder="••••••••"
1750
+ />
1751
+ <button
1752
+ type="button"
1753
+ onClick={() => setShowSmtpPassword((prev) => !prev)}
1754
+ className="absolute inset-y-0 right-0 flex cursor-pointer items-center pr-3 text-gray-400 hover:text-gray-600"
1755
+ aria-label={
1756
+ showSmtpPassword
1757
+ ? 'Masquer le mot de passe'
1758
+ : 'Afficher le mot de passe'
1759
+ }
1760
+ >
1761
+ {showSmtpPassword ? <EyeOff /> : <Eye />}
1762
+ </button>
1763
+ </div>
1764
+ <p className="mt-1 text-xs text-gray-500">
1765
+ Pour Gmail, utilisez un mot de passe d&apos;application
1766
+ </p>
1767
+ </div>
1768
+
1769
+ <div>
1770
+ <label className="block text-sm font-medium text-gray-700">
1771
+ Email expéditeur (From) *
1772
+ </label>
1773
+ <input
1774
+ type="email"
1775
+ required
1776
+ value={smtpData.fromEmail}
1777
+ onChange={(e) =>
1778
+ setSmtpData({ ...smtpData, fromEmail: e.target.value })
1779
+ }
1780
+ className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
1781
+ placeholder="votre.email@exemple.com"
1782
+ />
1783
+ </div>
1784
+
1785
+ <div>
1786
+ <label className="block text-sm font-medium text-gray-700">
1787
+ Nom expéditeur (optionnel)
1788
+ </label>
1789
+ <input
1790
+ type="text"
1791
+ value={smtpData.fromName}
1792
+ onChange={(e) =>
1793
+ setSmtpData({ ...smtpData, fromName: e.target.value })
1794
+ }
1795
+ className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
1796
+ placeholder="Votre Nom"
1797
+ />
1798
+ </div>
1799
+ </div>
1800
+
1801
+ <div className="mt-4 rounded-lg bg-blue-50 p-3 text-xs text-blue-900">
1802
+ <p className="font-medium">Utiliser Gmail avec SMTP</p>
1803
+ <p className="mt-1">
1804
+ Si vous utilisez une adresse Gmail, vous devez créer un{' '}
1805
+ <span className="font-semibold">mot de passe d&apos;application</span>{' '}
1806
+ dédié et le renseigner dans le champ &quot;Mot de passe&quot;
1807
+ ci-dessus. Rendez-vous sur{' '}
1808
+ <Link
1809
+ href="https://myaccount.google.com/apppasswords"
1810
+ target="_blank"
1811
+ rel="noreferrer"
1812
+ className="font-semibold underline"
1813
+ >
1814
+ la page des mots de passe d&apos;application Google
1815
+ </Link>{' '}
1816
+ pour en générer un (compte Google protégé par la validation en deux
1817
+ étapes requis).
1818
+ </p>
1819
+ </div>
1820
+
1821
+ <div className="flex flex-col gap-3 pt-4 sm:flex-row sm:justify-end">
1822
+ {smtpConfigured && (
1823
+ <button
1824
+ type="button"
1825
+ onClick={() => setShowSmtpForm(false)}
1826
+ className="w-full cursor-pointer rounded-lg border border-gray-300 px-6 py-2 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-50 focus:ring-2 focus:ring-gray-400/30 focus:ring-offset-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 sm:w-auto"
1827
+ >
1828
+ Annuler
1829
+ </button>
1830
+ )}
1831
+ <button
1832
+ type="button"
1833
+ onClick={handleSmtpTest}
1834
+ disabled={smtpTesting || smtpSaving}
1835
+ className="w-full cursor-pointer rounded-lg border border-blue-600 px-6 py-2 text-sm font-medium text-blue-600 transition-colors hover:bg-blue-50 focus:ring-2 focus:ring-gray-400/30 focus:ring-offset-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto"
1836
+ >
1837
+ {smtpTesting ? 'Test en cours...' : 'Tester la connexion'}
1838
+ </button>
1839
+ <button
1840
+ type="submit"
1841
+ disabled={smtpSaving || smtpTesting}
1842
+ className="w-full cursor-pointer rounded-lg bg-blue-600 px-6 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700 focus:ring-2 focus:ring-gray-400/30 focus:ring-offset-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto"
1843
+ >
1844
+ {smtpSaving ? 'Enregistrement...' : 'Enregistrer'}
1845
+ </button>
1846
+ </div>
1847
+ </form>
1848
+ )}
2339
1849
 
2340
- <div className="mt-6 rounded-lg border border-dashed border-gray-200 bg-gray-50 p-4">
1850
+ <div className="rounded-lg border border-dashed border-gray-200 bg-gray-50 p-4">
2341
1851
  <label className="mb-2 block text-sm font-medium text-gray-700">
2342
1852
  Votre signature (optionnel)
2343
1853
  </label>
@@ -2346,8 +1856,14 @@ export default function SettingsPage() {
2346
1856
  cette configuration SMTP (par exemple&nbsp;: nom, fonction, coordonnées,
2347
1857
  mentions légales…).
2348
1858
  </p>
1859
+ <p className="mb-2 text-xs text-gray-500">
1860
+ Logo / image&nbsp;: maximum{' '}
1861
+ {Math.round(SIGNATURE_MAX_IMAGE_BYTES / 1024)}&nbsp;Ko par fichier
1862
+ (signature en base64 — privilégiez un PNG ou WebP compressé).
1863
+ </p>
2349
1864
  <div className="mt-1">
2350
1865
  <Editor
1866
+ maxImageBytes={SIGNATURE_MAX_IMAGE_BYTES}
2351
1867
  ref={smtpSignatureEditorRef}
2352
1868
  onReady={(methods) => {
2353
1869
  // garder la ref synchronisée
@@ -2359,36 +1875,29 @@ export default function SettingsPage() {
2359
1875
  }}
2360
1876
  />
2361
1877
  </div>
1878
+ <div className="mt-4 flex justify-end">
1879
+ <button
1880
+ type="button"
1881
+ onClick={handleSmtpSignatureSave}
1882
+ disabled={smtpSignatureSaving}
1883
+ className="cursor-pointer rounded-lg bg-blue-600 px-6 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700 focus:ring-2 focus:ring-gray-400/30 focus:ring-offset-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50"
1884
+ >
1885
+ {smtpSignatureSaving ? 'Enregistrement...' : 'Enregistrer la signature'}
1886
+ </button>
1887
+ </div>
2362
1888
  </div>
2363
-
2364
- <div className="flex flex-col gap-3 pt-4 sm:flex-row sm:justify-end">
2365
- <button
2366
- type="button"
2367
- onClick={handleSmtpTest}
2368
- disabled={smtpTesting || smtpSaving}
2369
- className="w-full cursor-pointer rounded-lg border border-blue-600 px-6 py-2 text-sm font-medium text-blue-600 transition-colors hover:bg-blue-50 focus:ring-2 focus:ring-gray-400/30 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto"
2370
- >
2371
- {smtpTesting ? 'Test en cours...' : 'Tester la connexion'}
2372
- </button>
2373
- <button
2374
- type="submit"
2375
- disabled={smtpSaving || smtpTesting}
2376
- className="w-full cursor-pointer rounded-lg bg-blue-600 px-6 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700 focus:ring-2 focus:ring-gray-400/30 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto"
2377
- >
2378
- {smtpSaving ? 'Enregistrement...' : 'Enregistrer'}
2379
- </button>
2380
- </div>
2381
- </form>
1889
+ </div>
2382
1890
  )}
2383
1891
  </div>
2384
1892
  )}
2385
1893
 
2386
1894
  {/* Section Gestion des statuts - Paramètres de l'Application */}
2387
1895
  {mainSection === 'app' && (
2388
- <>
2389
- {isAdmin ? (
2390
- <div className="space-y-6">
2391
- <div className="rounded-lg bg-white p-6 shadow-sm">
1896
+ isAdmin ? (
1897
+ <div className="space-y-6">
1898
+ {isAdmin && (
1899
+ <div className="space-y-6">
1900
+ <div className="rounded-lg bg-white p-6 shadow-sm">
2392
1901
  <div className="flex items-center justify-between">
2393
1902
  <div>
2394
1903
  <h2 className="text-lg font-bold text-gray-900">Gestion des statuts</h2>
@@ -2430,7 +1939,7 @@ export default function SettingsPage() {
2430
1939
  onChange={(e) =>
2431
1940
  setStatusFormData({ ...statusFormData, name: e.target.value })
2432
1941
  }
2433
- className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none disabled:cursor-not-allowed disabled:bg-gray-100"
1942
+ className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-gray-100"
2434
1943
  placeholder="Ex: Nouveau"
2435
1944
  disabled={editingStatus?.isSystem}
2436
1945
  />
@@ -2466,7 +1975,7 @@ export default function SettingsPage() {
2466
1975
  color: e.target.value,
2467
1976
  })
2468
1977
  }
2469
- className="block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
1978
+ className="block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
2470
1979
  placeholder="#3B82F6"
2471
1980
  />
2472
1981
  </div>
@@ -2486,7 +1995,7 @@ export default function SettingsPage() {
2486
1995
  }
2487
1996
  className="peer sr-only"
2488
1997
  />
2489
- <div className="peer h-5 w-9 rounded-full bg-gray-200 peer-checked:bg-blue-600 peer-focus:ring-2 peer-focus:ring-gray-400/30 after:absolute after:top-[2px] after:left-[2px] after:h-4 after:w-4 after:rounded-full after:border after:border-gray-300 after:bg-white after:transition-all after:content-[''] peer-checked:after:translate-x-full peer-checked:after:border-white" />
1998
+ <div className="peer h-5 w-9 rounded-full bg-gray-200 peer-checked:bg-blue-600 peer-focus:ring-2 peer-focus:ring-gray-400/30 after:absolute after:top-[2px] after:left-[2px] after:h-4 after:w-4 after:rounded-full after:border after:border-gray-300 after:bg-white after:transition-transform after:content-[''] peer-checked:after:translate-x-full peer-checked:after:border-white" />
2490
1999
  </label>
2491
2000
  <span className="text-sm text-gray-700">
2492
2001
  Demander un motif de fermeture
@@ -2514,7 +2023,7 @@ export default function SettingsPage() {
2514
2023
  <button
2515
2024
  type="submit"
2516
2025
  disabled={statusSaving}
2517
- className="w-full cursor-pointer rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700 focus:ring-2 focus:ring-gray-400/30 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto"
2026
+ className="w-full cursor-pointer rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700 focus:ring-2 focus:ring-gray-400/30 focus:ring-offset-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto"
2518
2027
  >
2519
2028
  {statusSaving
2520
2029
  ? 'Enregistrement...'
@@ -2583,10 +2092,10 @@ export default function SettingsPage() {
2583
2092
  )}
2584
2093
  </div>
2585
2094
  )}
2586
- </div>
2095
+ </div>
2587
2096
 
2588
- {/* Section Motifs de fermeture - Paramètres de l'Application */}
2589
- <div className="rounded-lg bg-white p-6 shadow-sm">
2097
+ {/* Section Motifs de fermeture - Paramètres de l'Application */}
2098
+ <div className="rounded-lg bg-white p-6 shadow-sm">
2590
2099
  <div className="flex items-center justify-between gap-2">
2591
2100
  <div>
2592
2101
  <h2 className="text-lg font-bold text-gray-900">Motifs de fermeture</h2>
@@ -2665,8 +2174,7 @@ export default function SettingsPage() {
2665
2174
  } catch (error: any) {
2666
2175
  console.error('Erreur motif de fermeture:', error);
2667
2176
  setClosingReasonError(
2668
- error.message ||
2669
- 'Erreur lors de la sauvegarde du motif de fermeture',
2177
+ devToast('Erreur lors de la sauvegarde du motif de fermeture', error),
2670
2178
  );
2671
2179
  }
2672
2180
  }}
@@ -2686,7 +2194,7 @@ export default function SettingsPage() {
2686
2194
  name: e.target.value,
2687
2195
  }))
2688
2196
  }
2689
- className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
2197
+ className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
2690
2198
  placeholder="Ex: Faux numéro"
2691
2199
  />
2692
2200
  </div>
@@ -2707,7 +2215,7 @@ export default function SettingsPage() {
2707
2215
  </button>
2708
2216
  <button
2709
2217
  type="submit"
2710
- className="w-full cursor-pointer rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700 focus:ring-2 focus:ring-gray-400/30 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto"
2218
+ className="w-full cursor-pointer rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700 focus:ring-2 focus:ring-gray-400/30 focus:ring-offset-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto"
2711
2219
  >
2712
2220
  {closingReasonFormData.id ? 'Mettre à jour' : 'Créer'}
2713
2221
  </button>
@@ -2789,8 +2297,7 @@ export default function SettingsPage() {
2789
2297
  } catch (error: any) {
2790
2298
  console.error('Erreur suppression motif:', error);
2791
2299
  setClosingReasonError(
2792
- error.message ||
2793
- 'Erreur lors de la suppression du motif',
2300
+ devToast('Erreur lors de la suppression du motif', error),
2794
2301
  );
2795
2302
  }
2796
2303
  }}
@@ -2804,109 +2311,29 @@ export default function SettingsPage() {
2804
2311
  </div>
2805
2312
  )}
2806
2313
  </div>
2807
- </div>
2808
- </div>
2809
- ) : (
2810
- <div className="rounded-lg bg-white p-6 shadow-sm">
2811
- <div className="py-12 text-center">
2812
- <div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-blue-100">
2813
- <Grid3x3 className="h-6 w-6 text-blue-600" />
2314
+ </div>
2814
2315
  </div>
2815
- <h3 className="mt-4 text-lg font-semibold text-gray-900">
2816
- Accès restreint
2817
- </h3>
2818
- <p className="mt-2 text-sm text-gray-600">
2819
- Cette section est réservée aux administrateurs.
2820
- </p>
2316
+ )}
2821
2317
  </div>
2318
+ ) : (
2319
+ <div className="rounded-lg bg-white p-6 shadow-sm">
2320
+ <div className="py-12 text-center">
2321
+ <div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-blue-100">
2322
+ <Grid3x3 className="h-6 w-6 text-blue-600" />
2323
+ </div>
2324
+ <h3 className="mt-4 text-lg font-semibold text-gray-900">Accès restreint</h3>
2325
+ <p className="mt-2 text-sm text-gray-600">
2326
+ Cette section est réservée aux administrateurs.
2327
+ </p>
2822
2328
  </div>
2823
- )}
2824
- </>
2329
+ </div>
2330
+ )
2825
2331
  )}
2826
2332
 
2827
2333
  {/* Section Intégrations */}
2828
2334
  {mainSection === 'integrations' && (
2829
2335
  <>
2830
2336
  <div className="space-y-6">
2831
- {/* Google Drive - Admin uniquement */}
2832
- {isAdmin && (
2833
- <div className="rounded-lg bg-white p-6 shadow-sm">
2834
- <div className="flex items-center justify-between">
2835
- <div>
2836
- <h2 className="text-lg font-bold text-gray-900">Google Drive</h2>
2837
- <p className="mt-1 text-sm text-gray-600">
2838
- Connectez votre compte Google Drive. Tous les fichiers uploadés par
2839
- les utilisateurs seront stockés dans votre Drive.
2840
- </p>
2841
- </div>
2842
- </div>
2843
-
2844
- <div className="mt-6">
2845
- <div className="rounded-lg border border-gray-200 bg-white p-4 shadow-sm">
2846
- <div className="flex items-center justify-between">
2847
- <div className="flex items-center gap-3">
2848
- <div className="flex h-10 w-10 items-center justify-center rounded-lg bg-blue-100">
2849
- <Grid3x3 className="h-6 w-6 text-blue-600" />
2850
- </div>
2851
- <div>
2852
- <h3 className="font-medium text-gray-900">Google Drive</h3>
2853
- <p className="mt-1 text-xs text-gray-500">
2854
- Stockage des fichiers dans votre Drive personnel
2855
- </p>
2856
- </div>
2857
- </div>
2858
- </div>
2859
- {googleDriveLoading ? (
2860
- <div className="mt-4 text-center text-sm text-gray-500">
2861
- Chargement...
2862
- </div>
2863
- ) : (
2864
- <div className="mt-4 flex items-center justify-between">
2865
- {googleDriveAccount?.connected ? (
2866
- <>
2867
- <div className="flex items-center gap-2">
2868
- <span className="rounded-full bg-green-100 px-3 py-1 text-xs font-medium text-green-700">
2869
- Connecté
2870
- </span>
2871
- {googleDriveAccount.email && (
2872
- <span className="text-xs text-gray-600">
2873
- <span className="font-medium">Configuré par</span>{' '}
2874
- <span className="text-gray-900">
2875
- {googleDriveAccount.email}
2876
- </span>
2877
- </span>
2878
- )}
2879
- </div>
2880
- <button
2881
- type="button"
2882
- onClick={handleGoogleDriveDisconnect}
2883
- disabled={googleDriveDisconnecting}
2884
- className="cursor-pointer rounded-lg border border-blue-300 bg-white px-3 py-1.5 text-xs font-medium text-blue-700 transition-colors hover:bg-blue-50 disabled:cursor-not-allowed disabled:opacity-50"
2885
- >
2886
- {googleDriveDisconnecting ? 'Déconnexion...' : 'Déconnecter'}
2887
- </button>
2888
- </>
2889
- ) : (
2890
- <>
2891
- <span className="rounded-full bg-gray-100 px-3 py-1 text-xs font-medium text-gray-700">
2892
- Non connecté
2893
- </span>
2894
- <button
2895
- type="button"
2896
- onClick={handleGoogleDriveConnect}
2897
- className="cursor-pointer rounded-lg bg-blue-600 px-3 py-1.5 text-xs font-medium text-white transition-colors hover:bg-blue-700"
2898
- >
2899
- Connecter
2900
- </button>
2901
- </>
2902
- )}
2903
- </div>
2904
- )}
2905
- </div>
2906
- </div>
2907
- </div>
2908
- )}
2909
-
2910
2337
  {/* Google Calendar - Tous les utilisateurs */}
2911
2338
  <div className="rounded-lg bg-white p-6 shadow-sm">
2912
2339
  <div className="flex items-center justify-between">
@@ -2916,7 +2343,9 @@ export default function SettingsPage() {
2916
2343
  </h2>
2917
2344
  <p className="mt-1 text-sm text-gray-600">
2918
2345
  Connectez votre compte Google pour synchroniser vos rendez-vous, tâches
2919
- et Google Meet avec votre calendrier personnel.
2346
+ et Google Meet. Vous pouvez choisir un calendrier partagé à la création et
2347
+ afficher plusieurs agendas dans l&apos;agenda du CRM. Après une mise à jour
2348
+ des autorisations Google, reconnectez-vous via « Connecter ».
2920
2349
  </p>
2921
2350
  </div>
2922
2351
  </div>
@@ -2943,1425 +2372,211 @@ export default function SettingsPage() {
2943
2372
  Chargement...
2944
2373
  </div>
2945
2374
  ) : (
2946
- <div className="mt-4 flex items-center justify-between">
2947
- {googleCalendarAccount?.connected ? (
2948
- <>
2949
- <div className="flex items-center gap-2">
2950
- <span className="rounded-full bg-green-100 px-3 py-1 text-xs font-medium text-green-700">
2951
- Connecté
2952
- </span>
2953
- {googleCalendarAccount.email && (
2954
- <span className="text-xs text-gray-600">
2955
- {googleCalendarAccount.email}
2375
+ <div className="mt-4 space-y-6">
2376
+ <div className="flex items-center justify-between">
2377
+ {googleCalendarAccount?.connected ? (
2378
+ <>
2379
+ <div className="flex items-center gap-2">
2380
+ <span className="rounded-full bg-green-100 px-3 py-1 text-xs font-medium text-green-700">
2381
+ Connecté
2956
2382
  </span>
2957
- )}
2958
- </div>
2959
- <button
2960
- type="button"
2961
- onClick={handleGoogleCalendarDisconnect}
2962
- disabled={googleCalendarDisconnecting}
2963
- className="cursor-pointer rounded-lg border border-blue-300 bg-white px-3 py-1.5 text-xs font-medium text-blue-700 transition-colors hover:bg-blue-50 disabled:cursor-not-allowed disabled:opacity-50"
2964
- >
2965
- {googleCalendarDisconnecting ? 'Déconnexion...' : 'Déconnecter'}
2966
- </button>
2967
- </>
2968
- ) : (
2969
- <>
2970
- <span className="rounded-full bg-gray-100 px-3 py-1 text-xs font-medium text-gray-700">
2971
- Non connecté
2972
- </span>
2973
- <button
2974
- type="button"
2975
- onClick={handleGoogleCalendarConnect}
2976
- className="cursor-pointer rounded-lg bg-blue-600 px-3 py-1.5 text-xs font-medium text-white transition-colors hover:bg-blue-700"
2977
- >
2978
- Connecter
2979
- </button>
2980
- </>
2981
- )}
2982
- </div>
2983
- )}
2984
- </div>
2985
- </div>
2986
- </div>
2987
-
2988
- {/* Google Sheets - Admin uniquement */}
2989
- {isAdmin && (
2990
- <>
2991
- <div className="rounded-lg bg-white p-6 shadow-sm">
2992
- <div className="flex items-center justify-between">
2993
- <div>
2994
- <h2 className="text-lg font-bold text-gray-900">
2995
- Intégration Google Sheets
2996
- </h2>
2997
- <p className="mt-1 text-sm text-gray-600">
2998
- Importez automatiquement des contacts à partir d&apos;un Google
2999
- Sheet.
3000
- </p>
3001
- </div>
3002
- <div className="flex items-center gap-2">
3003
- <button
3004
- type="button"
3005
- onClick={handleGoogleSheetSync}
3006
- disabled={googleSheetSyncing}
3007
- className="cursor-pointer rounded-lg border border-blue-600 px-4 py-2 text-sm font-medium text-blue-600 transition-colors hover:bg-blue-50 disabled:cursor-not-allowed disabled:opacity-50"
3008
- >
3009
- {googleSheetSyncing ? 'Synchronisation...' : 'Synchroniser'}
3010
- </button>
3011
- <button
3012
- onClick={() => {
3013
- setEditingGoogleSheetConfig(null);
3014
- setGoogleSheetFormData({
3015
- name: '',
3016
- active: true,
3017
- sheetUrl: '',
3018
- sheetName: '',
3019
- headerRow: '1',
3020
- defaultStatusId: null,
3021
- defaultAssignedUserId: null,
3022
- });
3023
- setGoogleSheetMappings([]);
3024
- setGoogleSheetStep(1);
3025
- setShowGoogleSheetModal(true);
3026
- setGoogleSheetError('');
3027
- setGoogleSheetSuccess('');
3028
- }}
3029
- className="cursor-pointer rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700"
3030
- >
3031
- + Ajouter
3032
- </button>
3033
- </div>
3034
- </div>
3035
-
3036
- {googleSheetLoading ? (
3037
- <div className="mt-6 text-center text-gray-500">Chargement...</div>
3038
- ) : googleSheetConfigs.length === 0 ? (
3039
- <div className="mt-6 rounded-lg border border-dashed border-gray-300 bg-gray-50 p-8 text-center">
3040
- <p className="text-sm text-gray-600">
3041
- Aucune configuration Google Sheets
3042
- </p>
3043
- <p className="mt-1 text-xs text-gray-500">
3044
- Cliquez sur &quot;+ Ajouter&quot; pour créer votre première
3045
- configuration
3046
- </p>
3047
- </div>
3048
- ) : (
3049
- <div className="mt-6 space-y-3">
3050
- {googleSheetConfigs.map((config) => (
3051
- <div
3052
- key={config.id}
3053
- className="flex items-center justify-between rounded-lg border border-gray-200 bg-gray-50 p-4"
3054
- >
3055
- <div className="flex-1">
3056
- <div className="flex items-center gap-2">
3057
- <h3 className="font-medium text-gray-900">{config.name}</h3>
3058
- {config.active ? (
3059
- <span className="rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800">
3060
- Actif
3061
- </span>
3062
- ) : (
3063
- <span className="rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-800">
3064
- Inactif
2383
+ {googleCalendarAccount.email && (
2384
+ <span className="text-xs text-gray-600">
2385
+ {googleCalendarAccount.email}
3065
2386
  </span>
3066
2387
  )}
3067
2388
  </div>
3068
- <p className="mt-1 text-xs text-gray-500">
3069
- {config.sheetName} - Ligne {config.headerRow}
3070
- </p>
3071
- </div>
3072
- <div className="flex items-center gap-2">
3073
2389
  <button
3074
- onClick={() => handleEditGoogleSheet(config)}
3075
- className="cursor-pointer rounded-lg border border-gray-300 px-3 py-1.5 text-xs font-medium text-gray-700 transition-colors hover:bg-gray-100"
2390
+ type="button"
2391
+ onClick={handleGoogleCalendarDisconnect}
2392
+ disabled={googleCalendarDisconnecting}
2393
+ className="cursor-pointer rounded-lg border border-blue-300 bg-white px-3 py-1.5 text-xs font-medium text-blue-700 transition-colors hover:bg-blue-50 disabled:cursor-not-allowed disabled:opacity-50"
3076
2394
  >
3077
- Modifier
2395
+ {googleCalendarDisconnecting ? 'Déconnexion...' : 'Déconnecter'}
3078
2396
  </button>
2397
+ </>
2398
+ ) : (
2399
+ <>
2400
+ <span className="rounded-full bg-gray-100 px-3 py-1 text-xs font-medium text-gray-700">
2401
+ Non connecté
2402
+ </span>
3079
2403
  <button
3080
- onClick={() => handleDeleteGoogleSheet(config.id)}
3081
- className="cursor-pointer rounded-lg border border-blue-300 px-3 py-1.5 text-xs font-medium text-blue-700 transition-colors hover:bg-blue-50"
2404
+ type="button"
2405
+ onClick={handleGoogleCalendarConnect}
2406
+ className="cursor-pointer rounded-lg bg-blue-600 px-3 py-1.5 text-xs font-medium text-white transition-colors hover:bg-blue-700"
3082
2407
  >
3083
- Supprimer
2408
+ Connecter
3084
2409
  </button>
3085
- </div>
3086
- </div>
3087
- ))}
3088
- </div>
3089
- )}
3090
- </div>
3091
- </>
3092
- )}
3093
-
3094
- {/* Meta Lead Ads */}
3095
- {isAdmin && (
3096
- <div className="rounded-lg bg-white p-6 shadow-sm">
3097
- <div className="flex items-center justify-between">
3098
- <div>
3099
- <h2 className="text-lg font-bold text-gray-900">
3100
- Intégration Meta Lead Ads
3101
- </h2>
3102
- <p className="mt-1 text-sm text-gray-600">
3103
- Recevez automatiquement les leads depuis Facebook Lead Ads.
3104
- </p>
3105
- </div>
3106
- <button
3107
- onClick={() => {
3108
- setEditingMetaLeadConfig(null);
3109
- setMetaLeadFormData({
3110
- name: '',
3111
- active: true,
3112
- pageId: '',
3113
- accessToken: '',
3114
- verifyToken: '',
3115
- defaultStatusId: null,
3116
- defaultAssignedUserId: null,
3117
- });
3118
- setShowMetaLeadModal(true);
3119
- setMetaLeadError('');
3120
- setMetaLeadSuccess('');
3121
- }}
3122
- className="cursor-pointer rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700"
3123
- >
3124
- + Ajouter
3125
- </button>
3126
- </div>
3127
-
3128
- {metaLeadLoading ? (
3129
- <div className="mt-6 text-center text-gray-500">Chargement...</div>
3130
- ) : metaLeadConfigs.length === 0 ? (
3131
- <div className="mt-6 rounded-lg border border-dashed border-gray-300 bg-gray-50 p-8 text-center">
3132
- <p className="text-sm text-gray-600">
3133
- Aucune configuration Meta Lead Ads
3134
- </p>
3135
- <p className="mt-1 text-xs text-gray-500">
3136
- Cliquez sur &quot;+ Ajouter&quot; pour créer votre première
3137
- configuration
3138
- </p>
3139
- </div>
3140
- ) : (
3141
- <div className="mt-6 space-y-3">
3142
- {metaLeadConfigs.map((config) => (
3143
- <div
3144
- key={config.id}
3145
- className="flex items-center justify-between rounded-lg border border-gray-200 bg-gray-50 p-4"
3146
- >
3147
- <div className="flex-1">
3148
- <div className="flex items-center gap-2">
3149
- <h3 className="font-medium text-gray-900">{config.name}</h3>
3150
- {config.active ? (
3151
- <span className="rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800">
3152
- Actif
3153
- </span>
3154
- ) : (
3155
- <span className="rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-800">
3156
- Inactif
3157
- </span>
3158
- )}
3159
- </div>
3160
- <p className="mt-1 text-xs text-gray-500">
3161
- Page ID: {config.pageId}
3162
- </p>
3163
- </div>
3164
- <div className="flex items-center gap-2">
3165
- <button
3166
- onClick={() => handleEditMetaLead(config)}
3167
- className="cursor-pointer rounded-lg border border-gray-300 px-3 py-1.5 text-xs font-medium text-gray-700 transition-colors hover:bg-gray-100"
3168
- >
3169
- Modifier
3170
- </button>
3171
- <button
3172
- onClick={() => handleDeleteMetaLead(config.id)}
3173
- className="cursor-pointer rounded-lg border border-blue-300 px-3 py-1.5 text-xs font-medium text-blue-700 transition-colors hover:bg-blue-50"
3174
- >
3175
- Supprimer
3176
- </button>
3177
- </div>
3178
- </div>
3179
- ))}
3180
- </div>
3181
- )}
3182
- </div>
3183
- )}
3184
-
3185
- {/* Google Ads */}
3186
- {isAdmin && (
3187
- <div className="rounded-lg bg-white p-6 shadow-sm">
3188
- <div className="flex items-center justify-between">
3189
- <div>
3190
- <h2 className="text-lg font-bold text-gray-900">
3191
- Intégration Google Ads
3192
- </h2>
3193
- <p className="mt-1 text-sm text-gray-600">
3194
- Recevez automatiquement les leads depuis Google Ads Lead Forms.
3195
- </p>
3196
- </div>
3197
- <button
3198
- onClick={() => {
3199
- setEditingGoogleAdsConfig(null);
3200
- setGoogleAdsFormData({
3201
- name: '',
3202
- active: true,
3203
- webhookKey: '',
3204
- defaultStatusId: null,
3205
- defaultAssignedUserId: null,
3206
- });
3207
- setShowGoogleAdsModal(true);
3208
- setGoogleAdsError('');
3209
- setGoogleAdsSuccess('');
3210
- }}
3211
- className="cursor-pointer rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700"
3212
- >
3213
- + Ajouter
3214
- </button>
3215
- </div>
3216
-
3217
- {googleAdsLoading ? (
3218
- <div className="mt-6 text-center text-gray-500">Chargement...</div>
3219
- ) : googleAdsConfigs.length === 0 ? (
3220
- <div className="mt-6 rounded-lg border border-dashed border-gray-300 bg-gray-50 p-8 text-center">
3221
- <p className="text-sm text-gray-600">Aucune configuration Google Ads</p>
3222
- <p className="mt-1 text-xs text-gray-500">
3223
- Cliquez sur &quot;+ Ajouter&quot; pour créer votre première
3224
- configuration
3225
- </p>
3226
- </div>
3227
- ) : (
3228
- <div className="mt-6 space-y-3">
3229
- {googleAdsConfigs.map((config) => (
3230
- <div
3231
- key={config.id}
3232
- className="flex items-center justify-between rounded-lg border border-gray-200 bg-gray-50 p-4"
3233
- >
3234
- <div className="flex-1">
3235
- <div className="flex items-center gap-2">
3236
- <h3 className="font-medium text-gray-900">{config.name}</h3>
3237
- {config.active ? (
3238
- <span className="rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800">
3239
- Actif
3240
- </span>
3241
- ) : (
3242
- <span className="rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-800">
3243
- Inactif
3244
- </span>
3245
- )}
3246
- </div>
3247
- <p className="mt-1 text-xs text-gray-500">
3248
- Webhook Key: {config.webhookKey}
3249
- </p>
3250
- </div>
3251
- <div className="flex items-center gap-2">
3252
- <button
3253
- onClick={() => handleEditGoogleAds(config)}
3254
- className="cursor-pointer rounded-lg border border-gray-300 px-3 py-1.5 text-xs font-medium text-gray-700 transition-colors hover:bg-gray-100"
3255
- >
3256
- Modifier
3257
- </button>
3258
- <button
3259
- onClick={() => handleDeleteGoogleAds(config.id)}
3260
- className="cursor-pointer rounded-lg border border-blue-300 px-3 py-1.5 text-xs font-medium text-blue-700 transition-colors hover:bg-blue-50"
3261
- >
3262
- Supprimer
3263
- </button>
3264
- </div>
2410
+ </>
2411
+ )}
3265
2412
  </div>
3266
- ))}
3267
- </div>
3268
- )}
3269
- </div>
3270
- )}
3271
- </div>
3272
- </>
3273
- )}
3274
- </div>
3275
- </div>
3276
- </div>
3277
-
3278
- {/* Modals - doivent être en dehors des sections conditionnelles */}
3279
- {showGoogleSheetModal && (
3280
- <div className="fixed inset-0 z-50 flex items-center justify-center bg-gray-500/20 p-4 backdrop-blur-sm sm:p-6">
3281
- <div className="flex max-h-[90vh] w-full max-w-5xl flex-col rounded-lg bg-white p-6 shadow-xl sm:p-8">
3282
- {/* En-tête fixe */}
3283
- <div className="shrink-0 border-b border-gray-100 pb-4">
3284
- <div className="flex items-center justify-between">
3285
- <h2 className="text-xl font-bold text-gray-900 sm:text-2xl">
3286
- {editingGoogleSheetConfig ? 'Modifier' : 'Ajouter'} une configuration Google
3287
- Sheets
3288
- </h2>
3289
- <button
3290
- type="button"
3291
- onClick={() => {
3292
- setShowGoogleSheetModal(false);
3293
- setEditingGoogleSheetConfig(null);
3294
- setGoogleSheetStep(1);
3295
- setGoogleSheetFormData({
3296
- name: '',
3297
- active: true,
3298
- sheetUrl: '',
3299
- sheetName: '',
3300
- headerRow: '1',
3301
- defaultStatusId: null,
3302
- defaultAssignedUserId: null,
3303
- });
3304
- setGoogleSheetMappings([]);
3305
- setGoogleSheetError('');
3306
- setGoogleSheetPreview([]);
3307
- setGoogleSheetHeaders([]);
3308
- setGsPreviewStep('url');
3309
- setGsAvailableSheets([]);
3310
- setGsRawRows([]);
3311
- setGsSelectedHeaderRow(0);
3312
- }}
3313
- className="cursor-pointer rounded-lg p-2 text-gray-400 transition-colors hover:bg-gray-100"
3314
- >
3315
- <svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
3316
- <path
3317
- strokeLinecap="round"
3318
- strokeLinejoin="round"
3319
- strokeWidth={2}
3320
- d="M6 18L18 6M6 6l12 12"
3321
- />
3322
- </svg>
3323
- </button>
3324
- </div>
3325
- </div>
3326
-
3327
- {/* Contenu scrollable */}
3328
- <form
3329
- id="google-sheet-form"
3330
- onSubmit={handleGoogleSheetSubmit}
3331
- className="flex-1 space-y-4 overflow-y-auto pt-4 [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
3332
- >
3333
- {/* Étape 1 : informations de base */}
3334
- {googleSheetStep === 1 && (
3335
- <>
3336
- <div>
3337
- <label className="block text-sm font-medium text-gray-700">
3338
- Nom de la configuration *
3339
- </label>
3340
- <input
3341
- type="text"
3342
- required
3343
- value={googleSheetFormData.name}
3344
- onChange={(e) =>
3345
- setGoogleSheetFormData((prev) => ({
3346
- ...prev,
3347
- name: e.target.value,
3348
- }))
3349
- }
3350
- className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
3351
- placeholder="Ex: Contacts Ventes"
3352
- />
3353
- </div>
3354
-
3355
- {/* Indicateur d'etapes */}
3356
- {gsPreviewStep !== 'url' && (
3357
- <div className="flex items-center gap-2 text-xs font-medium text-gray-500">
3358
- <span className="rounded-full bg-gray-100 px-2.5 py-0.5 text-gray-500">
3359
- 1. Lien
3360
- </span>
3361
- <span className="text-gray-300">/</span>
3362
- {gsAvailableSheets.length > 1 && (
3363
- <>
3364
- <span
3365
- className={cn(
3366
- 'rounded-full px-2.5 py-0.5',
3367
- gsPreviewStep === 'sheet'
3368
- ? 'bg-blue-100 text-blue-700'
3369
- : 'bg-gray-100 text-gray-500',
3370
- )}
3371
- >
3372
- 2. Feuille
3373
- </span>
3374
- <span className="text-gray-300">/</span>
3375
- </>
3376
- )}
3377
- <span
3378
- className={cn(
3379
- 'rounded-full px-2.5 py-0.5',
3380
- gsPreviewStep === 'header'
3381
- ? 'bg-blue-100 text-blue-700'
3382
- : 'bg-gray-100 text-gray-500',
3383
- )}
3384
- >
3385
- {gsAvailableSheets.length > 1 ? '3' : '2'}. En-tête
3386
- </span>
3387
- </div>
3388
- )}
3389
2413
 
3390
- {/* Sous-etape URL */}
3391
- {gsPreviewStep === 'url' && (
3392
- <div>
3393
- <label className="block text-sm font-medium text-gray-700">
3394
- Lien du Google Sheet *
3395
- </label>
3396
- <input
3397
- type="url"
3398
- required
3399
- value={googleSheetFormData.sheetUrl}
3400
- onChange={(e) => {
3401
- setGoogleSheetFormData((prev) => ({
3402
- ...prev,
3403
- sheetUrl: e.target.value,
3404
- sheetName: '',
3405
- headerRow: '1',
3406
- }));
3407
- setGsAvailableSheets([]);
3408
- setGsRawRows([]);
3409
- setGsSelectedHeaderRow(0);
3410
- setGoogleSheetPreview([]);
3411
- setGoogleSheetHeaders([]);
3412
- }}
3413
- className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
3414
- placeholder="https://docs.google.com/spreadsheets/d/..."
3415
- />
3416
- </div>
3417
- )}
3418
-
3419
- {/* Sous-etape Feuille */}
3420
- {gsPreviewStep === 'sheet' && (
3421
- <div className="space-y-6">
3422
- <div className="rounded-lg border border-gray-200 p-4">
3423
- <div className="flex items-center justify-between">
3424
- <div>
3425
- <p className="text-sm font-medium text-gray-900">
3426
- {googleSheetFormData.sheetUrl.length > 60
3427
- ? googleSheetFormData.sheetUrl.slice(0, 60) + '...'
3428
- : googleSheetFormData.sheetUrl}
3429
- </p>
3430
- </div>
3431
- <button
3432
- type="button"
3433
- onClick={() => {
3434
- setGsPreviewStep('url');
3435
- setGsAvailableSheets([]);
3436
- setGsRawRows([]);
3437
- }}
3438
- className="cursor-pointer rounded-lg p-1 text-gray-400 transition-colors hover:text-gray-600"
3439
- >
3440
- <svg
3441
- className="h-4 w-4"
3442
- fill="none"
3443
- stroke="currentColor"
3444
- viewBox="0 0 24 24"
3445
- >
3446
- <path
3447
- strokeLinecap="round"
3448
- strokeLinejoin="round"
3449
- strokeWidth={2}
3450
- d="M6 18L18 6M6 6l12 12"
3451
- />
3452
- </svg>
3453
- </button>
3454
- </div>
3455
- </div>
3456
- <div>
3457
- <h3 className="mb-2 text-base font-semibold text-gray-900">
3458
- Choisir une feuille
3459
- </h3>
3460
- <p className="mb-4 text-sm text-gray-600">
3461
- Ce fichier contient {gsAvailableSheets.length} feuilles. Sélectionnez
3462
- celle qui contient les contacts à importer.
3463
- </p>
3464
- <div className="space-y-2">
3465
- {gsAvailableSheets.map((name, idx) => (
3466
- <button
3467
- key={name}
3468
- type="button"
3469
- disabled={gsLoadingPreview}
3470
- onClick={async () => {
3471
- setGoogleSheetFormData((prev) => ({ ...prev, sheetName: name }));
3472
- setGsLoadingPreview(true);
3473
- setGoogleSheetError('');
3474
- try {
3475
- const res = await fetch('/api/settings/google-sheet/preview', {
3476
- method: 'POST',
3477
- headers: { 'Content-Type': 'application/json' },
3478
- body: JSON.stringify({
3479
- sheetUrl: googleSheetFormData.sheetUrl,
3480
- sheetName: name,
3481
- }),
3482
- });
3483
- const data = await res.json();
3484
- if (!res.ok) throw new Error(data.error || 'Erreur');
3485
- setGsRawRows(data.rawRows || []);
3486
- setGsSelectedHeaderRow(0);
3487
- setGsPreviewStep('header');
3488
- } catch (err: unknown) {
3489
- setGoogleSheetError(
3490
- err instanceof Error ? err.message : 'Erreur',
3491
- );
3492
- } finally {
3493
- setGsLoadingPreview(false);
3494
- }
3495
- }}
3496
- className={cn(
3497
- 'flex w-full items-center gap-3 rounded-lg border-2 px-4 py-3 text-left text-sm font-medium transition-colors disabled:opacity-50',
3498
- googleSheetFormData.sheetName === name
3499
- ? 'border-blue-500 bg-blue-50 text-blue-700'
3500
- : 'border-gray-200 text-gray-700 hover:border-gray-300 hover:bg-gray-50',
3501
- )}
3502
- >
3503
- <span
3504
- className={cn(
3505
- 'flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-xs font-bold',
3506
- googleSheetFormData.sheetName === name
3507
- ? 'bg-blue-100 text-blue-700'
3508
- : 'bg-gray-100 text-gray-500',
2414
+ {googleCalendarAccount?.connected && (
2415
+ <div className="border-t border-gray-100 pt-4">
2416
+ {gCalDetails?.needsGoogleReconnect && (
2417
+ <p className="mb-3 rounded-lg bg-amber-50 px-3 py-2 text-xs text-amber-900">
2418
+ Reconnectez votre compte avec le bouton « Déconnecter » puis «
2419
+ Connecter » pour autoriser la liste de vos calendriers (y compris
2420
+ partagés).
2421
+ </p>
3509
2422
  )}
3510
- >
3511
- {idx + 1}
3512
- </span>
3513
- {name}
3514
- </button>
3515
- ))}
3516
- </div>
3517
- </div>
3518
- </div>
3519
- )}
3520
-
3521
- {/* Sous-etape En-tete */}
3522
- {gsPreviewStep === 'header' && (
3523
- <div className="space-y-6">
3524
- <div className="rounded-lg border border-gray-200 p-4">
3525
- <div className="flex items-center justify-between">
3526
- <div>
3527
- <p className="text-sm font-medium text-gray-900">
3528
- {googleSheetFormData.sheetUrl.length > 50
3529
- ? googleSheetFormData.sheetUrl.slice(0, 50) + '...'
3530
- : googleSheetFormData.sheetUrl}
3531
- {googleSheetFormData.sheetName && (
3532
- <span className="ml-2 text-xs text-gray-500">
3533
- — {googleSheetFormData.sheetName}
3534
- </span>
3535
- )}
3536
- </p>
3537
- </div>
3538
- <button
3539
- type="button"
3540
- onClick={() =>
3541
- setGsPreviewStep(gsAvailableSheets.length > 1 ? 'sheet' : 'url')
3542
- }
3543
- className="cursor-pointer rounded-lg p-1 text-gray-400 transition-colors hover:text-gray-600"
3544
- >
3545
- <svg
3546
- className="h-4 w-4"
3547
- fill="none"
3548
- stroke="currentColor"
3549
- viewBox="0 0 24 24"
3550
- >
3551
- <path
3552
- strokeLinecap="round"
3553
- strokeLinejoin="round"
3554
- strokeWidth={2}
3555
- d="M6 18L18 6M6 6l12 12"
3556
- />
3557
- </svg>
3558
- </button>
3559
- </div>
3560
- </div>
3561
- <div>
3562
- <h3 className="mb-2 text-base font-semibold text-gray-900">
3563
- Sélectionner la ligne d&apos;en-tête
3564
- </h3>
3565
- <p className="mb-4 text-sm text-gray-600">
3566
- Cliquez sur la ligne qui contient les noms de colonnes (en-têtes). Les
3567
- lignes au-dessus seront ignorées.
3568
- </p>
3569
- {gsRawRows.length > 0 ? (
3570
- <div className="overflow-x-auto rounded-lg border border-gray-200">
3571
- <table className="min-w-full divide-y divide-gray-200">
3572
- <thead className="bg-gray-50">
3573
- <tr>
3574
- <th className="px-3 py-2 text-left text-xs font-medium text-gray-500">
3575
- #
3576
- </th>
3577
- {gsRawRows[0]?.map((_, colIdx) => (
3578
- <th
3579
- key={colIdx}
3580
- className="px-3 py-2 text-left text-xs font-medium text-gray-500"
2423
+ {gCalDetails?.error && !gCalDetails?.calendars?.length && (
2424
+ <p className="mb-3 text-xs text-red-600">{gCalDetails.error}</p>
2425
+ )}
2426
+ {writableGoogleCalendars.length > 0 && (
2427
+ <div className="space-y-2">
2428
+ <label
2429
+ htmlFor="settings-gcal-default"
2430
+ className="block text-sm font-medium text-gray-700"
3581
2431
  >
3582
- Col {colIdx + 1}
3583
- </th>
3584
- ))}
3585
- </tr>
3586
- </thead>
3587
- <tbody className="divide-y divide-gray-100 bg-white">
3588
- {gsRawRows.map((row, rowIdx) => (
3589
- <tr
3590
- key={rowIdx}
3591
- onClick={() => {
3592
- setGsSelectedHeaderRow(rowIdx);
3593
- setGoogleSheetFormData((prev) => ({
3594
- ...prev,
3595
- headerRow: String(rowIdx + 1),
3596
- }));
3597
- }}
3598
- className={cn(
3599
- 'cursor-pointer transition-colors',
3600
- gsSelectedHeaderRow === rowIdx
3601
- ? 'bg-blue-50 ring-2 ring-blue-500 ring-inset'
3602
- : rowIdx < gsSelectedHeaderRow
3603
- ? 'bg-gray-50 text-gray-400'
3604
- : 'hover:bg-gray-50',
3605
- )}
2432
+ Calendrier par défaut pour les nouveaux événements
2433
+ </label>
2434
+ <select
2435
+ id="settings-gcal-default"
2436
+ value={gCalDefaultId || 'primary'}
2437
+ onChange={(e) => setGCalDefaultId(e.target.value)}
2438
+ className="block w-full max-w-md rounded-lg border border-gray-300 px-3 py-2 text-sm text-gray-900"
2439
+ >
2440
+ {writableGoogleCalendars.map((c) => (
2441
+ <option key={c.id} value={c.id}>
2442
+ {c.summary}
2443
+ {c.primary ? ' (principal)' : ''}
2444
+ </option>
2445
+ ))}
2446
+ </select>
2447
+ </div>
2448
+ )}
2449
+ {(gCalDetails?.calendars?.length ?? 0) > 0 && (
2450
+ <div className="mt-4">
2451
+ <p className="text-sm font-medium text-gray-700">
2452
+ Afficher dans l&apos;agenda du CRM
2453
+ </p>
2454
+ <p className="mt-1 text-xs text-gray-500">
2455
+ Cochez les calendriers dont les événements apparaissent en plus
2456
+ des tâches du CRM (événements en lecture seule, lien vers Google).
2457
+ </p>
2458
+ <ul className="mt-2 max-h-48 space-y-2 overflow-y-auto rounded-lg border border-gray-100 p-2">
2459
+ {(gCalDetails?.calendars ?? []).map((c) => (
2460
+ <li key={c.id}>
2461
+ <label className="flex cursor-pointer items-center gap-2 text-sm text-gray-800">
2462
+ <input
2463
+ type="checkbox"
2464
+ className="rounded border-gray-300"
2465
+ checked={gCalVisibleIds.includes(c.id)}
2466
+ onChange={(e) => {
2467
+ setGCalVisibleIds((prev) =>
2468
+ e.target.checked
2469
+ ? [...prev, c.id]
2470
+ : prev.filter((id) => id !== c.id),
2471
+ );
2472
+ }}
2473
+ />
2474
+ <span className="truncate">{c.summary}</span>
2475
+ {c.primary && (
2476
+ <span className="text-xs text-gray-400">(principal)</span>
2477
+ )}
2478
+ </label>
2479
+ </li>
2480
+ ))}
2481
+ </ul>
2482
+ </div>
2483
+ )}
2484
+ {googleCalendarAccount.connected && (
2485
+ <div className="mt-4">
2486
+ <label
2487
+ htmlFor="settings-gcal-agenda-color"
2488
+ className="block text-sm font-medium text-gray-700"
2489
+ >
2490
+ Couleur des événements Google dans l&apos;agenda
2491
+ </label>
2492
+ <p className="mt-1 text-xs text-gray-500">
2493
+ Pastilles et cartes des événements importés (vues mois, semaine et
2494
+ jour).
2495
+ </p>
2496
+ <div className="mt-2 flex flex-wrap items-center gap-3">
2497
+ <input
2498
+ id="settings-gcal-agenda-color"
2499
+ type="color"
2500
+ value={gCalEventColor}
2501
+ onChange={(e) => setGCalEventColor(e.target.value)}
2502
+ className="h-9 w-14 cursor-pointer rounded border border-gray-300 bg-white p-0.5"
2503
+ title="Choisir une couleur"
2504
+ />
2505
+ <div className="flex flex-wrap gap-1.5">
2506
+ {GOOGLE_AGENDA_COLOR_PRESETS.map((hex) => (
2507
+ <button
2508
+ key={hex}
2509
+ type="button"
2510
+ title={hex}
2511
+ aria-label={`Couleur ${hex}`}
2512
+ onClick={() => setGCalEventColor(hex)}
2513
+ className={cn(
2514
+ 'h-7 w-7 rounded-full border-2 border-white shadow-sm ring-1 ring-gray-200 transition-transform hover:scale-110',
2515
+ gCalEventColor.toLowerCase() === hex.toLowerCase() &&
2516
+ 'ring-2 ring-gray-900 ring-offset-1',
2517
+ )}
2518
+ style={{ backgroundColor: hex }}
2519
+ />
2520
+ ))}
2521
+ </div>
2522
+ </div>
2523
+ </div>
2524
+ )}
2525
+ {googleCalendarAccount.connected && (
2526
+ <button
2527
+ type="button"
2528
+ onClick={handleSaveGoogleCalendarPrefs}
2529
+ disabled={gCalPrefsSaving}
2530
+ className="mt-4 cursor-pointer rounded-lg bg-gray-900 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-gray-800 disabled:opacity-50"
3606
2531
  >
3607
- <td className="px-3 py-2 text-xs font-medium text-gray-400">
3608
- {rowIdx + 1}
3609
- </td>
3610
- {row.map((cell, colIdx) => (
3611
- <td
3612
- key={colIdx}
3613
- className={cn(
3614
- 'max-w-[200px] truncate px-3 py-2 text-xs',
3615
- gsSelectedHeaderRow === rowIdx
3616
- ? 'font-semibold text-blue-700'
3617
- : 'text-gray-900',
3618
- )}
3619
- >
3620
- {cell || <span className="text-gray-300">—</span>}
3621
- </td>
3622
- ))}
3623
- </tr>
3624
- ))}
3625
- </tbody>
3626
- </table>
2532
+ {gCalPrefsSaving ? 'Enregistrement...' : 'Enregistrer les préférences'}
2533
+ </button>
2534
+ )}
2535
+ </div>
2536
+ )}
3627
2537
  </div>
3628
- ) : (
3629
- <p className="text-sm text-gray-500">
3630
- Aucune donnée disponible dans cette feuille.
3631
- </p>
3632
2538
  )}
3633
2539
  </div>
3634
2540
  </div>
3635
- )}
3636
- </>
3637
- )}
3638
-
3639
- {/* Étape 2 : mapping manuel des colonnes + valeurs par défaut */}
3640
- {googleSheetStep === 2 && (
3641
- <>
3642
- <div className="grid grid-cols-1 gap-4 md:grid-cols-2">
3643
- <div>
3644
- <label className="block text-sm font-medium text-gray-700">
3645
- Utilisateur assigné par défaut (optionnel)
3646
- </label>
3647
- <select
3648
- value={googleSheetFormData.defaultAssignedUserId || ''}
3649
- onChange={(e) =>
3650
- setGoogleSheetFormData((prev) => ({
3651
- ...prev,
3652
- defaultAssignedUserId: e.target.value || null,
3653
- }))
3654
- }
3655
- className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
3656
- >
3657
- <option value="">Aucun utilisateur par défaut</option>
3658
- {metaLeadUsers.map((user) => (
3659
- <option key={user.id} value={user.id}>
3660
- {user.name} ({user.email})
3661
- </option>
3662
- ))}
3663
- </select>
3664
- </div>
3665
-
3666
- <div>
3667
- <label className="block text-sm font-medium text-gray-700">
3668
- Statut par défaut
3669
- </label>
3670
- <select
3671
- value={googleSheetFormData.defaultStatusId || ''}
3672
- onChange={(e) =>
3673
- setGoogleSheetFormData((prev) => ({
3674
- ...prev,
3675
- defaultStatusId: e.target.value || null,
3676
- }))
3677
- }
3678
- className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
3679
- >
3680
- <option value="">Aucun statut par défaut</option>
3681
- {statuses.map((status) => (
3682
- <option key={status.id} value={status.id}>
3683
- {status.name}
3684
- </option>
3685
- ))}
3686
- </select>
3687
- </div>
3688
- </div>
3689
-
3690
- <div className="flex items-center justify-between">
3691
- <h3 className="text-base font-semibold text-gray-900">
3692
- Correspondance des champs
3693
- </h3>
3694
- <button
3695
- type="button"
3696
- onClick={() => {
3697
- setGoogleSheetMappings((prev) => [
3698
- ...prev,
3699
- {
3700
- id: `mapping-${Date.now()}-${Math.random()}`,
3701
- columnName: '',
3702
- action: 'ignore',
3703
- },
3704
- ]);
3705
- }}
3706
- className="flex cursor-pointer items-center gap-1 rounded-lg border border-blue-600 bg-white px-3 py-1.5 text-sm font-medium text-blue-600 transition-colors hover:bg-blue-50"
3707
- >
3708
- <Plus className="h-4 w-4" />
3709
- Ajouter un champ
3710
- </button>
3711
2541
  </div>
3712
2542
 
3713
- <div className="mt-4 space-y-3">
3714
- {googleSheetMappings.map((mapping) => (
3715
- <div
3716
- key={mapping.id}
3717
- className="flex items-center gap-3 rounded-lg border border-gray-200 bg-gray-50 p-3"
3718
- >
3719
- <div className="flex-1">
3720
- <input
3721
- type="text"
3722
- value={mapping.columnName}
3723
- onChange={(e) => {
3724
- setGoogleSheetMappings((prev) =>
3725
- prev.map((m) =>
3726
- m.id === mapping.id ? { ...m, columnName: e.target.value } : m,
3727
- ),
3728
- );
3729
- }}
3730
- placeholder="Nom de la colonne dans Google Sheets"
3731
- className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm text-gray-900 placeholder:text-gray-400 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
3732
- />
3733
- </div>
3734
-
3735
- <div className="flex items-center">
3736
- <ArrowRight className="h-5 w-5 text-gray-400" />
3737
- </div>
3738
-
3739
- <div className="flex-1">
3740
- <select
3741
- value={mapping.action}
3742
- onChange={(e) => {
3743
- const newAction = e.target.value as 'map' | 'note' | 'ignore';
3744
- setGoogleSheetMappings((prev) =>
3745
- prev.map((m) =>
3746
- m.id === mapping.id
3747
- ? {
3748
- ...m,
3749
- action: newAction,
3750
- crmField:
3751
- newAction === 'map' && m.crmField
3752
- ? m.crmField
3753
- : newAction === 'map'
3754
- ? 'firstName'
3755
- : undefined,
3756
- }
3757
- : m,
3758
- ),
3759
- );
3760
- }}
3761
- className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
3762
- >
3763
- <option value="map">Mapper vers un champ</option>
3764
- <option value="note">Ajouter comme note</option>
3765
- <option value="ignore">-- Ne pas importer --</option>
3766
- </select>
3767
- </div>
3768
-
3769
- {mapping.action === 'map' && (
3770
- <>
3771
- <div className="flex items-center">
3772
- <ArrowRight className="h-5 w-5 text-gray-400" />
3773
- </div>
3774
- <div className="flex-1">
3775
- <select
3776
- value={mapping.crmField || ''}
3777
- onChange={(e) => {
3778
- setGoogleSheetMappings((prev) =>
3779
- prev.map((m) =>
3780
- m.id === mapping.id
3781
- ? { ...m, crmField: e.target.value }
3782
- : m,
3783
- ),
3784
- );
3785
- }}
3786
- className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
3787
- >
3788
- <option value="">Sélectionnez un champ</option>
3789
- <option value="phone">Téléphone *</option>
3790
- <option value="firstName">Prénom</option>
3791
- <option value="lastName">Nom</option>
3792
- <option value="email">Email</option>
3793
- <option value="civility">Civilité</option>
3794
- <option value="secondaryPhone">Téléphone secondaire</option>
3795
- <option value="address">Adresse</option>
3796
- <option value="city">Ville</option>
3797
- <option value="postalCode">Code postal</option>
3798
- <option value="origin">Origine</option>
3799
- </select>
2543
+ {isAdmin && (
2544
+ <>
2545
+ <GoogleSheetIntegration
2546
+ statuses={statuses}
2547
+ users={integrationUsers}
2548
+ />
2549
+ <MetaLeadIntegration
2550
+ statuses={statuses}
2551
+ users={integrationUsers}
2552
+ onOpenLogs={handleOpenLogs('meta_lead')}
2553
+ />
2554
+ <GoogleAdsIntegration
2555
+ statuses={statuses}
2556
+ users={integrationUsers}
2557
+ onOpenLogs={handleOpenLogs('google_ads')}
2558
+ />
2559
+ </>
2560
+ )}
3800
2561
  </div>
3801
2562
  </>
3802
2563
  )}
3803
-
3804
- <button
3805
- type="button"
3806
- onClick={() => {
3807
- setGoogleSheetMappings((prev) =>
3808
- prev.filter((m) => m.id !== mapping.id),
3809
- );
3810
- }}
3811
- className="cursor-pointer rounded-lg p-2 text-gray-400 transition-colors hover:bg-gray-200 hover:text-blue-600"
3812
- >
3813
- <Trash2 className="h-4 w-4" />
3814
- </button>
3815
2564
  </div>
3816
- ))}
3817
2565
  </div>
3818
-
3819
- <div className="mt-6 rounded-lg border border-gray-200 bg-gray-50 p-4">
3820
- <ul className="space-y-2 text-xs text-gray-600">
3821
- <li>
3822
- • Le champ &quot;Téléphone&quot; est obligatoire pour l&#39;import.
3823
- Configurez également les autres champs que vous souhaitez importer.
3824
- </li>
3825
- <li>
3826
- • Les colonnes sans correspondance de champ sélectionnée (-- Ne pas
3827
- importer --) seront ignorées lors de l&apos;import
3828
- </li>
3829
- <li>
3830
- • Laissez vide le nom de colonne Google Sheets pour ignorer ce mapping
3831
- </li>
3832
- <li>
3833
- • Pour &quot;Origine&quot;, utilisez le nom de la plateforme (ex:
3834
- Facebook, Instagram, Google, LinkedIn, Site Web)
3835
- </li>
3836
- <li>
3837
- • Les colonnes marquées &quot;Ajouter comme note&quot; seront regroupées
3838
- dans une seule note avec le format &quot;Question : Réponse&quot;
3839
- </li>
3840
- </ul>
3841
2566
  </div>
3842
2567
 
3843
- {googleSheetPreview.length > 0 && googleSheetHeaders.length > 0 && (
3844
- <div>
3845
- <h3 className="mb-3 text-sm font-semibold text-gray-900">
3846
- Aperçu (5 premières lignes)
3847
- </h3>
3848
- <div className="overflow-x-auto rounded-lg border border-gray-200">
3849
- <table className="min-w-full divide-y divide-gray-200">
3850
- <thead className="bg-gray-50">
3851
- <tr>
3852
- {googleSheetHeaders.map((header) => (
3853
- <th
3854
- key={header}
3855
- className="px-4 py-2 text-left text-xs font-medium text-gray-700"
3856
- >
3857
- {header}
3858
- </th>
3859
- ))}
3860
- </tr>
3861
- </thead>
3862
- <tbody className="divide-y divide-gray-200 bg-white">
3863
- {googleSheetPreview.map((row, idx) => (
3864
- <tr key={idx}>
3865
- {googleSheetHeaders.map((header) => (
3866
- <td
3867
- key={header}
3868
- className="px-4 py-2 text-xs whitespace-nowrap text-gray-900"
3869
- >
3870
- {row[header] || '-'}
3871
- </td>
3872
- ))}
3873
- </tr>
3874
- ))}
3875
- </tbody>
3876
- </table>
3877
2568
  </div>
3878
- </div>
3879
- )}
3880
- </>
3881
- )}
3882
-
3883
- </form>
3884
-
3885
- {/* Pied de modal fixe */}
3886
- <div className="shrink-0 border-t border-gray-100 pt-4">
3887
- <div className="flex flex-col gap-3 sm:flex-row sm:justify-end">
3888
- <button
3889
- type="button"
3890
- onClick={() => {
3891
- setShowGoogleSheetModal(false);
3892
- setEditingGoogleSheetConfig(null);
3893
- setGoogleSheetStep(1);
3894
- setGoogleSheetFormData({
3895
- name: '',
3896
- active: true,
3897
- sheetUrl: '',
3898
- sheetName: '',
3899
- headerRow: '1',
3900
- defaultStatusId: null,
3901
- defaultAssignedUserId: null,
3902
- });
3903
- setGoogleSheetMappings([]);
3904
- setGoogleSheetError('');
3905
- setGoogleSheetPreview([]);
3906
- setGoogleSheetHeaders([]);
3907
- setGsPreviewStep('url');
3908
- setGsAvailableSheets([]);
3909
- setGsRawRows([]);
3910
- setGsSelectedHeaderRow(0);
3911
- }}
3912
- className="w-full cursor-pointer rounded-lg border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-50 sm:w-auto"
3913
- >
3914
- Annuler
3915
- </button>
3916
-
3917
- {googleSheetStep === 1 && gsPreviewStep === 'url' ? (
3918
- <button
3919
- type="button"
3920
- disabled={gsLoadingPreview || !googleSheetFormData.sheetUrl}
3921
- onClick={async () => {
3922
- setGsLoadingPreview(true);
3923
- setGoogleSheetError('');
3924
- try {
3925
- const res = await fetch('/api/settings/google-sheet/preview', {
3926
- method: 'POST',
3927
- headers: { 'Content-Type': 'application/json' },
3928
- body: JSON.stringify({ sheetUrl: googleSheetFormData.sheetUrl }),
3929
- });
3930
- const data = await res.json();
3931
- if (!res.ok) throw new Error(data.error || 'Erreur');
3932
- setGsAvailableSheets(data.sheetNames || []);
3933
- setGsRawRows(data.rawRows || []);
3934
- if (data.sheetNames?.length > 1) {
3935
- setGsPreviewStep('sheet');
3936
- } else {
3937
- const sheet = data.sheetNames?.[0] || '';
3938
- setGoogleSheetFormData((prev) => ({ ...prev, sheetName: sheet }));
3939
- setGsPreviewStep('header');
3940
- }
3941
- } catch (err: unknown) {
3942
- setGoogleSheetError(err instanceof Error ? err.message : 'Erreur');
3943
- } finally {
3944
- setGsLoadingPreview(false);
3945
- }
3946
- }}
3947
- className="w-full cursor-pointer rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto"
3948
- >
3949
- {gsLoadingPreview ? 'Chargement...' : 'Charger les feuilles'}
3950
- </button>
3951
- ) : googleSheetStep === 1 ? (
3952
- <button
3953
- type="button"
3954
- disabled={
3955
- googleSheetSaving || gsPreviewStep !== 'header' || gsRawRows.length === 0
3956
- }
3957
- onClick={async () => {
3958
- const ok = await handleGoogleSheetAutoMap();
3959
- if (ok) {
3960
- setGoogleSheetStep(2);
3961
- }
3962
- }}
3963
- className="w-full cursor-pointer rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto"
3964
- >
3965
- Étape suivante
3966
- </button>
3967
- ) : (
3968
- <button
3969
- type="submit"
3970
- form="google-sheet-form"
3971
- disabled={googleSheetSaving}
3972
- className="w-full cursor-pointer rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto"
3973
- >
3974
- {googleSheetSaving ? 'Enregistrement...' : 'Enregistrer'}
3975
- </button>
3976
- )}
3977
- </div>
3978
- </div>
3979
- </div>
3980
- </div>
3981
- )}
3982
-
3983
- {showMetaLeadModal && (
3984
- <div className="fixed inset-0 z-50 flex items-center justify-center bg-gray-500/20 p-4 backdrop-blur-sm sm:p-6">
3985
- <div className="flex max-h-[90vh] w-full max-w-2xl flex-col rounded-lg bg-white p-6 shadow-xl sm:p-8">
3986
- <div className="shrink-0 border-b border-gray-100 pb-4">
3987
- <div className="flex items-center justify-between">
3988
- <h2 className="text-xl font-bold text-gray-900 sm:text-2xl">
3989
- {editingMetaLeadConfig ? 'Modifier' : 'Ajouter'} une configuration Meta Lead Ads
3990
- </h2>
3991
- <button
3992
- type="button"
3993
- onClick={() => {
3994
- setShowMetaLeadModal(false);
3995
- setEditingMetaLeadConfig(null);
3996
- setMetaLeadFormData({
3997
- name: '',
3998
- active: true,
3999
- pageId: '',
4000
- accessToken: '',
4001
- verifyToken: '',
4002
- defaultStatusId: null,
4003
- defaultAssignedUserId: null,
4004
- });
4005
- setMetaLeadError('');
4006
- setMetaLeadSuccess('');
4007
- }}
4008
- className="cursor-pointer rounded-lg p-2 text-gray-400 transition-colors hover:bg-gray-100"
4009
- >
4010
- <svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
4011
- <path
4012
- strokeLinecap="round"
4013
- strokeLinejoin="round"
4014
- strokeWidth={2}
4015
- d="M6 18L18 6M6 6l12 12"
4016
- />
4017
- </svg>
4018
- </button>
4019
- </div>
4020
- </div>
4021
-
4022
- <form
4023
- id="meta-lead-form"
4024
- onSubmit={handleMetaLeadSubmit}
4025
- className="flex-1 space-y-4 overflow-y-auto pt-4"
4026
- >
4027
- <div>
4028
- <label className="block text-sm font-medium text-gray-700">
4029
- Nom de la configuration *
4030
- </label>
4031
- <input
4032
- type="text"
4033
- required
4034
- value={metaLeadFormData.name}
4035
- onChange={(e) =>
4036
- setMetaLeadFormData((prev) => ({ ...prev, name: e.target.value }))
4037
- }
4038
- className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
4039
- placeholder="Ex: Facebook Lead Ads"
4040
- />
4041
- </div>
4042
-
4043
- <div className="flex items-center">
4044
- <input
4045
- id="meta-lead-active"
4046
- type="checkbox"
4047
- checked={metaLeadFormData.active}
4048
- onChange={(e) =>
4049
- setMetaLeadFormData((prev) => ({ ...prev, active: e.target.checked }))
4050
- }
4051
- className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-gray-400/30"
4052
- />
4053
- <label
4054
- htmlFor="meta-lead-active"
4055
- className="ml-2 text-sm font-medium text-gray-700"
4056
- >
4057
- Activer l&apos;intégration Meta Lead Ads
4058
- </label>
4059
- </div>
4060
-
4061
- <div>
4062
- <label className="block text-sm font-medium text-gray-700">Page ID *</label>
4063
- <input
4064
- type="text"
4065
- required
4066
- value={metaLeadFormData.pageId}
4067
- onChange={(e) =>
4068
- setMetaLeadFormData((prev) => ({ ...prev, pageId: e.target.value }))
4069
- }
4070
- className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
4071
- placeholder="Page ID Facebook"
4072
- />
4073
- </div>
4074
-
4075
- <div>
4076
- <label className="block text-sm font-medium text-gray-700">Access Token *</label>
4077
- <input
4078
- type="text"
4079
- required
4080
- value={metaLeadFormData.accessToken}
4081
- onChange={(e) =>
4082
- setMetaLeadFormData((prev) => ({ ...prev, accessToken: e.target.value }))
4083
- }
4084
- className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
4085
- placeholder="Access Token"
4086
- />
4087
- </div>
4088
-
4089
- <div>
4090
- <label className="block text-sm font-medium text-gray-700">Verify Token *</label>
4091
- <input
4092
- type="text"
4093
- required
4094
- value={metaLeadFormData.verifyToken}
4095
- onChange={(e) =>
4096
- setMetaLeadFormData((prev) => ({ ...prev, verifyToken: e.target.value }))
4097
- }
4098
- className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
4099
- placeholder="Verify Token"
4100
- />
4101
- </div>
4102
-
4103
- <div className="grid grid-cols-1 gap-4 md:grid-cols-2">
4104
- <div>
4105
- <label className="block text-sm font-medium text-gray-700">
4106
- Utilisateur assigné par défaut (optionnel)
4107
- </label>
4108
- <select
4109
- value={metaLeadFormData.defaultAssignedUserId || ''}
4110
- onChange={(e) =>
4111
- setMetaLeadFormData((prev) => ({
4112
- ...prev,
4113
- defaultAssignedUserId: e.target.value || null,
4114
- }))
4115
- }
4116
- className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
4117
- >
4118
- <option value="">Aucun utilisateur par défaut</option>
4119
- {metaLeadUsers.map((user) => (
4120
- <option key={user.id} value={user.id}>
4121
- {user.name} ({user.email})
4122
- </option>
4123
- ))}
4124
- </select>
4125
- </div>
4126
-
4127
- <div>
4128
- <label className="block text-sm font-medium text-gray-700">
4129
- Statut par défaut
4130
- </label>
4131
- <select
4132
- value={metaLeadFormData.defaultStatusId || ''}
4133
- onChange={(e) =>
4134
- setMetaLeadFormData((prev) => ({
4135
- ...prev,
4136
- defaultStatusId: e.target.value || null,
4137
- }))
4138
- }
4139
- className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
4140
- >
4141
- <option value="">Aucun statut par défaut</option>
4142
- {statuses.map((status) => (
4143
- <option key={status.id} value={status.id}>
4144
- {status.name}
4145
- </option>
4146
- ))}
4147
- </select>
4148
- </div>
4149
- </div>
4150
-
4151
- </form>
4152
-
4153
- <div className="shrink-0 border-t border-gray-100 pt-4">
4154
- <div className="flex flex-col gap-3 sm:flex-row sm:justify-end">
4155
- <button
4156
- type="button"
4157
- onClick={() => {
4158
- setShowMetaLeadModal(false);
4159
- setEditingMetaLeadConfig(null);
4160
- setMetaLeadFormData({
4161
- name: '',
4162
- active: true,
4163
- pageId: '',
4164
- accessToken: '',
4165
- verifyToken: '',
4166
- defaultStatusId: null,
4167
- defaultAssignedUserId: null,
4168
- });
4169
- setMetaLeadError('');
4170
- setMetaLeadSuccess('');
4171
- }}
4172
- className="w-full cursor-pointer rounded-lg border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-50 sm:w-auto"
4173
- >
4174
- Annuler
4175
- </button>
4176
- <button
4177
- type="submit"
4178
- form="meta-lead-form"
4179
- disabled={metaLeadSaving}
4180
- className="w-full cursor-pointer rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto"
4181
- >
4182
- {metaLeadSaving ? 'Enregistrement...' : 'Enregistrer'}
4183
- </button>
4184
- </div>
4185
- </div>
4186
- </div>
4187
- </div>
4188
- )}
4189
-
4190
- {showGoogleAdsModal && (
4191
- <div className="fixed inset-0 z-50 flex items-center justify-center bg-gray-500/20 p-4 backdrop-blur-sm sm:p-6">
4192
- <div className="flex max-h-[90vh] w-full max-w-2xl flex-col rounded-lg bg-white p-6 shadow-xl sm:p-8">
4193
- <div className="shrink-0 border-b border-gray-100 pb-4">
4194
- <div className="flex items-center justify-between">
4195
- <h2 className="text-xl font-bold text-gray-900 sm:text-2xl">
4196
- {editingGoogleAdsConfig ? 'Modifier' : 'Ajouter'} une configuration Google Ads
4197
- </h2>
4198
- <button
4199
- type="button"
4200
- onClick={() => {
4201
- setShowGoogleAdsModal(false);
4202
- setEditingGoogleAdsConfig(null);
4203
- setGoogleAdsFormData({
4204
- name: '',
4205
- active: true,
4206
- webhookKey: '',
4207
- defaultStatusId: null,
4208
- defaultAssignedUserId: null,
4209
- });
4210
- setGoogleAdsError('');
4211
- setGoogleAdsSuccess('');
4212
- }}
4213
- className="cursor-pointer rounded-lg p-2 text-gray-400 transition-colors hover:bg-gray-100"
4214
- >
4215
- <svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
4216
- <path
4217
- strokeLinecap="round"
4218
- strokeLinejoin="round"
4219
- strokeWidth={2}
4220
- d="M6 18L18 6M6 6l12 12"
4221
- />
4222
- </svg>
4223
- </button>
4224
- </div>
4225
- </div>
4226
-
4227
- <form
4228
- id="google-ads-form"
4229
- onSubmit={handleGoogleAdsSubmit}
4230
- className="flex-1 space-y-4 overflow-y-auto pt-4"
4231
- >
4232
- <div>
4233
- <label className="block text-sm font-medium text-gray-700">
4234
- Nom de la configuration *
4235
- </label>
4236
- <input
4237
- type="text"
4238
- required
4239
- value={googleAdsFormData.name}
4240
- onChange={(e) =>
4241
- setGoogleAdsFormData((prev) => ({ ...prev, name: e.target.value }))
4242
- }
4243
- className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
4244
- placeholder="Ex: Google Ads Lead Forms"
4245
- />
4246
- </div>
4247
-
4248
- <div className="flex items-center">
4249
- <input
4250
- id="google-ads-active"
4251
- type="checkbox"
4252
- checked={googleAdsFormData.active}
4253
- onChange={(e) =>
4254
- setGoogleAdsFormData((prev) => ({ ...prev, active: e.target.checked }))
4255
- }
4256
- className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-gray-400/30"
4257
- />
4258
- <label
4259
- htmlFor="google-ads-active"
4260
- className="ml-2 text-sm font-medium text-gray-700"
4261
- >
4262
- Activer l&apos;intégration Google Ads
4263
- </label>
4264
- </div>
4265
-
4266
- <div>
4267
- <label className="block text-sm font-medium text-gray-700">Webhook Key *</label>
4268
- <input
4269
- type="text"
4270
- required
4271
- value={googleAdsFormData.webhookKey}
4272
- onChange={(e) =>
4273
- setGoogleAdsFormData((prev) => ({ ...prev, webhookKey: e.target.value }))
4274
- }
4275
- className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
4276
- placeholder="Webhook Key"
4277
- />
4278
- </div>
4279
-
4280
- <div className="grid grid-cols-1 gap-4 md:grid-cols-2">
4281
- <div>
4282
- <label className="block text-sm font-medium text-gray-700">
4283
- Utilisateur assigné par défaut (optionnel)
4284
- </label>
4285
- <select
4286
- value={googleAdsFormData.defaultAssignedUserId || ''}
4287
- onChange={(e) =>
4288
- setGoogleAdsFormData((prev) => ({
4289
- ...prev,
4290
- defaultAssignedUserId: e.target.value || null,
4291
- }))
4292
- }
4293
- className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
4294
- >
4295
- <option value="">Aucun utilisateur par défaut</option>
4296
- {metaLeadUsers.map((user) => (
4297
- <option key={user.id} value={user.id}>
4298
- {user.name} ({user.email})
4299
- </option>
4300
- ))}
4301
- </select>
4302
- </div>
4303
-
4304
- <div>
4305
- <label className="block text-sm font-medium text-gray-700">
4306
- Statut par défaut
4307
- </label>
4308
- <select
4309
- value={googleAdsFormData.defaultStatusId || ''}
4310
- onChange={(e) =>
4311
- setGoogleAdsFormData((prev) => ({
4312
- ...prev,
4313
- defaultStatusId: e.target.value || null,
4314
- }))
4315
- }
4316
- className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
4317
- >
4318
- <option value="">Aucun statut par défaut</option>
4319
- {statuses.map((status) => (
4320
- <option key={status.id} value={status.id}>
4321
- {status.name}
4322
- </option>
4323
- ))}
4324
- </select>
4325
- </div>
4326
- </div>
4327
-
4328
- </form>
4329
-
4330
- <div className="shrink-0 border-t border-gray-100 pt-4">
4331
- <div className="flex flex-col gap-3 sm:flex-row sm:justify-end">
4332
- <button
4333
- type="button"
4334
- onClick={() => {
4335
- setShowGoogleAdsModal(false);
4336
- setEditingGoogleAdsConfig(null);
4337
- setGoogleAdsFormData({
4338
- name: '',
4339
- active: true,
4340
- webhookKey: '',
4341
- defaultStatusId: null,
4342
- defaultAssignedUserId: null,
4343
- });
4344
- setGoogleAdsError('');
4345
- setGoogleAdsSuccess('');
4346
- }}
4347
- className="w-full cursor-pointer rounded-lg border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-50 sm:w-auto"
4348
- >
4349
- Annuler
4350
- </button>
4351
- <button
4352
- type="submit"
4353
- form="google-ads-form"
4354
- disabled={googleAdsSaving}
4355
- className="w-full cursor-pointer rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto"
4356
- >
4357
- {googleAdsSaving ? 'Enregistrement...' : 'Enregistrer'}
4358
- </button>
4359
- </div>
4360
- </div>
4361
- </div>
4362
- </div>
4363
- )}
4364
- </div>
2569
+ <IntegrationLogPanel
2570
+ open={showLogPanel}
2571
+ onClose={() => {
2572
+ setShowLogPanel(false);
2573
+ setLogPanelConfigId(undefined);
2574
+ setLogPanelConfigName(undefined);
2575
+ }}
2576
+ integrationType={logPanelType}
2577
+ configId={logPanelConfigId}
2578
+ configName={logPanelConfigName}
2579
+ />
4365
2580
  <ConfirmDialog />
4366
2581
  </ProtectedPage>
4367
2582
  );