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
@@ -0,0 +1,809 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useCallback } from 'react';
4
+ import { Plus, Trash2, ArrowRight } from 'lucide-react';
5
+ import { cn, indexToColumn, devToast } from '@/lib/utils';
6
+ import { useAppToast } from '@/contexts/app-toast-context';
7
+ import { useConfirm } from '@/hooks/use-confirm';
8
+ import { useFetch } from '@/hooks/use-fetch';
9
+ import { CONFIG_LINKS } from '@/lib/config-links';
10
+ import { ImportResultDialog, type ImportResultItem } from './ImportResultDialog';
11
+ import { StatusSelect } from '@/components/ui/status-select';
12
+ import { GoogleSheetConfigMonitoringModal } from './GoogleSheetConfigMonitoringModal';
13
+ import { ConfigErrorAlert } from '@/components/config-error-alert';
14
+
15
+ interface ColumnMapping {
16
+ id: string;
17
+ columnName: string;
18
+ action: 'map' | 'note' | 'ignore';
19
+ crmField?: string;
20
+ }
21
+
22
+ interface GoogleSheetConfig {
23
+ id: string;
24
+ name: string;
25
+ active: boolean;
26
+ spreadsheetId: string;
27
+ sheetName: string;
28
+ headerRow: number;
29
+ phoneColumn: string;
30
+ firstNameColumn: string | null;
31
+ lastNameColumn: string | null;
32
+ emailColumn: string | null;
33
+ cityColumn: string | null;
34
+ postalCodeColumn: string | null;
35
+ originColumn: string | null;
36
+ columnMappings?: ColumnMapping[];
37
+ defaultStatusId: string | null;
38
+ defaultAssignedUserId: string | null;
39
+ }
40
+
41
+ interface GoogleSheetIntegrationProps {
42
+ statuses: Array<{ id: string; name: string; color: string }>;
43
+ users: Array<{ id: string; name: string; email: string }>;
44
+ }
45
+
46
+ type GoogleSheetJobStatus = 'QUEUED' | 'RUNNING' | 'SUCCEEDED' | 'FAILED';
47
+ type GoogleSheetJobResult = {
48
+ totalImported?: number;
49
+ totalUpdated?: number;
50
+ totalSkipped?: number;
51
+ results?: ImportResultItem[];
52
+ message?: string;
53
+ };
54
+
55
+ const INITIAL_FORM_DATA = {
56
+ name: '',
57
+ active: true,
58
+ sheetUrl: '',
59
+ sheetName: '',
60
+ headerRow: '1',
61
+ defaultStatusId: null as string | null,
62
+ defaultAssignedUserId: null as string | null,
63
+ };
64
+
65
+ export function GoogleSheetIntegration({
66
+ statuses,
67
+ users,
68
+ }: Readonly<GoogleSheetIntegrationProps>) {
69
+ const toast = useAppToast();
70
+ const { confirm, ConfirmDialog } = useConfirm();
71
+ const { data: googleStatus } = useFetch<{
72
+ calendar?: { connected?: boolean; email?: string | null };
73
+ }>('/api/auth/google/status');
74
+ /** Aligné sur GET /api/auth/google/status : { calendar: { connected, email } } */
75
+ const googleStatusLoaded = googleStatus !== undefined;
76
+ const googleConnected = Boolean(googleStatus?.calendar?.connected);
77
+
78
+ const [loading, setLoading] = useState(true);
79
+ const [saving, setSaving] = useState(false);
80
+ const [syncing, setSyncing] = useState(false);
81
+ const [configs, setConfigs] = useState<GoogleSheetConfig[]>([]);
82
+
83
+ const [showModal, setShowModal] = useState(false);
84
+ const [editingConfig, setEditingConfig] = useState<string | null>(null);
85
+ const [step, setStep] = useState<1 | 2>(1);
86
+ const [formData, setFormData] = useState(INITIAL_FORM_DATA);
87
+
88
+ const [mappings, setMappings] = useState<ColumnMapping[]>([]);
89
+ const [preview, setPreview] = useState<Array<Record<string, string>>>([]);
90
+ const [headers, setHeaders] = useState<string[]>([]);
91
+
92
+ const [gsPreviewStep, setGsPreviewStep] = useState<'url' | 'sheet' | 'header'>('url');
93
+ const [gsAvailableSheets, setGsAvailableSheets] = useState<string[]>([]);
94
+ const [gsRawRows, setGsRawRows] = useState<string[][]>([]);
95
+ const [gsSelectedHeaderRow, setGsSelectedHeaderRow] = useState(0);
96
+ const [gsLoadingPreview, setGsLoadingPreview] = useState(false);
97
+ const [gsConfigError, setGsConfigError] = useState<string | null>(null);
98
+ const [gsConfigErrorConfigLink, setGsConfigErrorConfigLink] = useState<string | null>(null);
99
+
100
+ const [showImportResult, setShowImportResult] = useState(false);
101
+ const [importResults, setImportResults] = useState<ImportResultItem[]>([]);
102
+ const [importTotals, setImportTotals] = useState({ totalImported: 0, totalUpdated: 0, totalSkipped: 0 });
103
+ const [importConfigName, setImportConfigName] = useState<string | undefined>();
104
+ const [showMonitoringModal, setShowMonitoringModal] = useState(false);
105
+ const [selectedMonitoringConfig, setSelectedMonitoringConfig] = useState<GoogleSheetConfig | null>(null);
106
+
107
+ const pollSyncJob = useCallback(
108
+ async (jobId: string): Promise<GoogleSheetJobResult> => {
109
+ const timeoutMs = 5 * 60 * 1000;
110
+ const intervalMs = 1500;
111
+ const start = Date.now();
112
+
113
+ while (Date.now() - start < timeoutMs) {
114
+ const res = await fetch(`/api/integrations/google-sheet/jobs/${jobId}`);
115
+ const data = await res.json();
116
+ if (!res.ok) {
117
+ throw new Error(data.error || 'Impossible de suivre la synchronisation');
118
+ }
119
+
120
+ const status = data.status as GoogleSheetJobStatus;
121
+ if (status === 'SUCCEEDED') {
122
+ return (data.result ?? {}) as GoogleSheetJobResult;
123
+ }
124
+ if (status === 'FAILED') {
125
+ throw new Error(data.error || 'La synchronisation a échoué');
126
+ }
127
+
128
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
129
+ }
130
+
131
+ throw new Error('La synchronisation est en cours depuis trop longtemps. Réessayez.');
132
+ },
133
+ [],
134
+ );
135
+
136
+ const loadConfigs = useCallback(async () => {
137
+ try {
138
+ const res = await fetch('/api/settings/google-sheet');
139
+ if (res.ok) {
140
+ const data = await res.json();
141
+ setConfigs(Array.isArray(data) ? data : []);
142
+ }
143
+ } catch {
144
+ // silent
145
+ } finally {
146
+ setLoading(false);
147
+ }
148
+ }, []);
149
+
150
+ useEffect(() => {
151
+ loadConfigs();
152
+ }, [loadConfigs]);
153
+
154
+ const resetModal = () => {
155
+ setShowModal(false);
156
+ setEditingConfig(null);
157
+ setStep(1);
158
+ setFormData(INITIAL_FORM_DATA);
159
+ setMappings([]);
160
+ setPreview([]);
161
+ setHeaders([]);
162
+ setGsPreviewStep('url');
163
+ setGsAvailableSheets([]);
164
+ setGsRawRows([]);
165
+ setGsSelectedHeaderRow(0);
166
+ setGsConfigError(null);
167
+ setGsConfigErrorConfigLink(null);
168
+ };
169
+
170
+ const handleAutoMap = async (): Promise<boolean> => {
171
+ try {
172
+ if (!formData.sheetUrl || !formData.sheetName || !formData.headerRow) {
173
+ toast.error('Veuillez renseigner le lien du Google Sheet, le nom de l\u2019onglet et la ligne des en-têtes.');
174
+ return false;
175
+ }
176
+ const res = await fetch('/api/settings/google-sheet/auto-map', {
177
+ method: 'POST',
178
+ headers: { 'Content-Type': 'application/json' },
179
+ body: JSON.stringify({
180
+ sheetUrl: formData.sheetUrl,
181
+ sheetName: formData.sheetName,
182
+ headerRow: formData.headerRow || '1',
183
+ }),
184
+ });
185
+ const data = await res.json();
186
+ if (!res.ok) {
187
+ setGsConfigError(data.error || 'Erreur lors du mapping automatique');
188
+ setGsConfigErrorConfigLink(data.configLink || null);
189
+ return false;
190
+ }
191
+
192
+ setGsConfigError(null);
193
+ setGsConfigErrorConfigLink(null);
194
+ const h = data.headers || [];
195
+ const autoMapping = data.mapping || {};
196
+ const p = data.preview || [];
197
+
198
+ setHeaders(h);
199
+ setPreview(p);
200
+
201
+ const crmFieldMap: Record<string, string> = {
202
+ phoneColumn: 'phone',
203
+ firstNameColumn: 'firstName',
204
+ lastNameColumn: 'lastName',
205
+ emailColumn: 'email',
206
+ cityColumn: 'city',
207
+ postalCodeColumn: 'postalCode',
208
+ companyNameColumn: 'companyName',
209
+ originColumn: 'origin',
210
+ websiteColumn: 'website',
211
+ linkedinColumn: 'linkedin',
212
+ facebookColumn: 'facebook',
213
+ twitterColumn: 'twitter',
214
+ instagramColumn: 'instagram',
215
+ };
216
+
217
+ const initial: ColumnMapping[] = h.map((header: string, index: number) => {
218
+ if (!header) return null;
219
+ const col = indexToColumn(index);
220
+ const mapped = Object.entries(autoMapping).find(([, v]) => v === col)?.[0];
221
+ return {
222
+ id: `mapping-${Date.now()}-${Math.random()}`,
223
+ columnName: header,
224
+ action: mapped ? 'map' : 'ignore',
225
+ crmField: mapped ? crmFieldMap[mapped] : undefined,
226
+ } as ColumnMapping;
227
+ }).filter(Boolean) as ColumnMapping[];
228
+
229
+ setMappings(initial);
230
+ toast.success('Colonnes détectées. Configurez le mapping.');
231
+ return true;
232
+ } catch (err: unknown) {
233
+ toast.error(devToast('Erreur lors du mapping automatique', err));
234
+ setGsConfigError(null);
235
+ setGsConfigErrorConfigLink(null);
236
+ return false;
237
+ }
238
+ };
239
+
240
+ const handleSubmit = async (e: React.FormEvent) => {
241
+ e.preventDefault();
242
+ setSaving(true);
243
+
244
+ try {
245
+ const phoneMapping = mappings.find(
246
+ (m) => m.action === 'map' && m.crmField === 'phone' && m.columnName.trim() !== '',
247
+ );
248
+ if (!phoneMapping) {
249
+ toast.error('Le mapping du téléphone est obligatoire');
250
+ setSaving(false);
251
+ return;
252
+ }
253
+
254
+ const url = editingConfig
255
+ ? `/api/settings/google-sheet/${editingConfig}`
256
+ : '/api/settings/google-sheet';
257
+ const method = editingConfig ? 'PUT' : 'POST';
258
+
259
+ const res = await fetch(url, {
260
+ method,
261
+ headers: { 'Content-Type': 'application/json' },
262
+ body: JSON.stringify({ ...formData, columnMappings: mappings }),
263
+ });
264
+ const data = await res.json();
265
+ if (!res.ok) throw new Error(data.error || 'Erreur lors de la sauvegarde');
266
+
267
+ const createdId = data.config?.id ?? data.id;
268
+ const createdName = data.config?.name ?? data.name;
269
+ const wasCreating = !editingConfig;
270
+
271
+ toast.success(
272
+ editingConfig
273
+ ? 'Configuration Google Sheets mise à jour !'
274
+ : 'Configuration Google Sheets créée !',
275
+ );
276
+ resetModal();
277
+ await loadConfigs();
278
+
279
+ if (wasCreating && createdId) {
280
+ setSyncing(true);
281
+ try {
282
+ toast.success('Synchronisation lancée en arrière-plan...');
283
+ const enqueueRes = await fetch('/api/integrations/google-sheet/sync', {
284
+ method: 'POST',
285
+ headers: { 'Content-Type': 'application/json' },
286
+ body: JSON.stringify({ configId: createdId }),
287
+ });
288
+ const enqueueData = await enqueueRes.json();
289
+ if (!enqueueRes.ok) {
290
+ throw new Error(enqueueData.error || 'Erreur lors du lancement de la synchronisation');
291
+ }
292
+
293
+ const syncData = await pollSyncJob(enqueueData.jobId);
294
+ const results = syncData.results ?? [];
295
+ setImportResults(results);
296
+ setImportTotals({
297
+ totalImported: syncData.totalImported ?? 0,
298
+ totalUpdated: syncData.totalUpdated ?? 0,
299
+ totalSkipped: syncData.totalSkipped ?? 0,
300
+ });
301
+ setImportConfigName(createdName);
302
+ setShowImportResult(true);
303
+ } catch (syncErr: unknown) {
304
+ setImportResults([{
305
+ configId: createdId,
306
+ configName: createdName ?? 'Configuration',
307
+ imported: 0,
308
+ error: syncErr instanceof Error ? syncErr.message : 'Erreur lors de l\'import',
309
+ }]);
310
+ setImportConfigName(createdName);
311
+ setShowImportResult(true);
312
+ } finally {
313
+ setSyncing(false);
314
+ await loadConfigs();
315
+ }
316
+ }
317
+ } catch (err: unknown) {
318
+ toast.error(devToast('Impossible de sauvegarder la configuration Google Sheets. Veuillez réessayer.', err));
319
+ } finally {
320
+ setSaving(false);
321
+ }
322
+ };
323
+
324
+ const handleSync = async (configId?: string) => {
325
+ setSyncing(true);
326
+ try {
327
+ toast.success('Synchronisation lancée en arrière-plan...');
328
+ const enqueueRes = await fetch('/api/integrations/google-sheet/sync', {
329
+ method: 'POST',
330
+ headers: { 'Content-Type': 'application/json' },
331
+ body: configId ? JSON.stringify({ configId }) : '{}',
332
+ });
333
+ const enqueueData = await enqueueRes.json();
334
+ if (!enqueueRes.ok) throw new Error(enqueueData.error || 'Erreur lors de la synchronisation');
335
+
336
+ const data = await pollSyncJob(enqueueData.jobId);
337
+ const results = data.results ?? [];
338
+ setImportResults(results);
339
+ setImportTotals({
340
+ totalImported: data.totalImported ?? 0,
341
+ totalUpdated: data.totalUpdated ?? 0,
342
+ totalSkipped: data.totalSkipped ?? 0,
343
+ });
344
+ setImportConfigName(results.length === 1 ? results[0]?.configName : undefined);
345
+ setShowImportResult(true);
346
+ } catch (err: unknown) {
347
+ toast.error(devToast('Impossible de lancer la synchronisation. Veuillez réessayer.', err));
348
+ } finally {
349
+ setSyncing(false);
350
+ }
351
+ };
352
+
353
+ const openMonitoringModal = (config: GoogleSheetConfig) => {
354
+ setSelectedMonitoringConfig(config);
355
+ setShowMonitoringModal(true);
356
+ };
357
+
358
+ const handleDelete = async (id: string) => {
359
+ const ok = await confirm({
360
+ title: 'Supprimer la configuration Google Sheet',
361
+ description: 'Êtes-vous sûr de vouloir supprimer cette configuration ?',
362
+ confirmText: 'Supprimer',
363
+ cancelText: 'Annuler',
364
+ variant: 'destructive',
365
+ });
366
+ if (!ok) return;
367
+ try {
368
+ const res = await fetch(`/api/settings/google-sheet/${id}`, { method: 'DELETE' });
369
+ if (!res.ok) {
370
+ const data = await res.json();
371
+ throw new Error(data.error || 'Erreur lors de la suppression');
372
+ }
373
+ toast.success('Configuration supprimée !');
374
+ await loadConfigs();
375
+ } catch (err: unknown) {
376
+ toast.error(devToast('Impossible de supprimer la configuration. Veuillez réessayer.', err));
377
+ }
378
+ };
379
+
380
+ return (
381
+ <>
382
+ <div className="rounded-lg bg-white p-6 shadow-sm">
383
+ <div className="flex items-center justify-between">
384
+ <div>
385
+ <h2 className="text-lg font-bold text-gray-900">Intégration Google Sheets</h2>
386
+ <p className="mt-1 text-sm text-gray-600">
387
+ Importez automatiquement des contacts à partir d&apos;un Google Sheet.
388
+ </p>
389
+ </div>
390
+ <div className="flex items-center gap-2">
391
+ <button
392
+ type="button"
393
+ onClick={() => {
394
+ if (googleStatusLoaded && !googleConnected) {
395
+ toast.errorConfigRequired(
396
+ 'Connectez votre compte Google dans les paramètres avant de synchroniser.',
397
+ CONFIG_LINKS.googleSheet,
398
+ );
399
+ return;
400
+ }
401
+ handleSync();
402
+ }}
403
+ disabled={syncing}
404
+ className="cursor-pointer rounded-lg border border-blue-600 bg-white px-4 py-2 text-sm font-medium text-blue-700 transition-colors hover:bg-blue-50 disabled:cursor-not-allowed disabled:opacity-50"
405
+ >
406
+ {syncing ? 'Synchronisation...' : 'Synchroniser'}
407
+ </button>
408
+ <button
409
+ type="button"
410
+ onClick={() => {
411
+ if (googleStatusLoaded && !googleConnected) {
412
+ toast.errorConfigRequired(
413
+ 'Connectez votre compte Google dans les paramètres avant d\'ajouter une configuration.',
414
+ CONFIG_LINKS.googleSheet,
415
+ );
416
+ return;
417
+ }
418
+ setEditingConfig(null);
419
+ setFormData(INITIAL_FORM_DATA);
420
+ setMappings([]);
421
+ setStep(1);
422
+ setShowModal(true);
423
+ }}
424
+ className="cursor-pointer rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700"
425
+ >
426
+ + Ajouter
427
+ </button>
428
+ </div>
429
+ </div>
430
+
431
+ {loading ? (
432
+ <div className="mt-6 text-center text-gray-500">Chargement...</div>
433
+ ) : configs.length === 0 ? (
434
+ <div className="mt-6 rounded-lg border border-dashed border-gray-300 bg-gray-50 p-8 text-center">
435
+ <p className="text-sm text-gray-600">Aucune configuration Google Sheets</p>
436
+ <p className="mt-1 text-xs text-gray-500">
437
+ Cliquez sur &quot;+ Ajouter&quot; pour créer votre première configuration
438
+ </p>
439
+ </div>
440
+ ) : (
441
+ <div className="mt-6 space-y-3">
442
+ {configs.map((config) => (
443
+ <div
444
+ key={config.id}
445
+ className="flex items-center justify-between rounded-lg border border-gray-200 bg-gray-50 p-4"
446
+ >
447
+ <div className="flex-1">
448
+ <div className="flex items-center gap-2">
449
+ <h3 className="font-medium text-gray-900">{config.name}</h3>
450
+ {config.active ? (
451
+ <span className="rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800">
452
+ Actif
453
+ </span>
454
+ ) : (
455
+ <span className="rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-800">
456
+ Inactif
457
+ </span>
458
+ )}
459
+ </div>
460
+ <p className="mt-1 text-xs text-gray-500">
461
+ {config.sheetName} - Ligne {config.headerRow}
462
+ </p>
463
+ <p className="mt-0.5 text-xs text-gray-400">
464
+ Lien : https://docs.google.com/spreadsheets/d/{config.spreadsheetId}
465
+ </p>
466
+ </div>
467
+ <div className="flex flex-wrap items-center gap-2">
468
+ <button
469
+ type="button"
470
+ onClick={() => openMonitoringModal(config)}
471
+ 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"
472
+ >
473
+ Gérer
474
+ </button>
475
+ <button
476
+ type="button"
477
+ onClick={() => handleDelete(config.id)}
478
+ className="cursor-pointer rounded-lg border border-rose-200 px-3 py-1.5 text-xs font-medium text-rose-700 transition-colors hover:bg-rose-50"
479
+ >
480
+ Supprimer
481
+ </button>
482
+ </div>
483
+ </div>
484
+ ))}
485
+ </div>
486
+ )}
487
+ </div>
488
+
489
+ <GoogleSheetConfigMonitoringModal
490
+ open={showMonitoringModal}
491
+ onClose={() => {
492
+ setShowMonitoringModal(false);
493
+ setSelectedMonitoringConfig(null);
494
+ }}
495
+ config={selectedMonitoringConfig}
496
+ statuses={statuses}
497
+ users={users}
498
+ onSaved={loadConfigs}
499
+ />
500
+
501
+ {/* Google Sheet Modal */}
502
+ {showModal && (
503
+ <div className="ui-fade-in fixed inset-0 z-50 flex min-h-dvh items-center justify-center bg-gray-500/20 p-4 backdrop-blur-sm sm:p-6">
504
+ <div className="ui-scale-in flex max-h-[90vh] w-full max-w-5xl flex-col rounded-lg bg-white p-6 shadow-xl sm:p-8">
505
+ <div className="shrink-0 border-b border-slate-200 pb-4">
506
+ <div className="flex items-center justify-between">
507
+ <h2 className="text-xl font-bold text-slate-900 sm:text-2xl">
508
+ {editingConfig ? 'Modifier' : 'Ajouter'} une configuration Google Sheets
509
+ </h2>
510
+ <button type="button" onClick={resetModal} className="cursor-pointer rounded-xl p-2 text-slate-400 transition-colors hover:bg-slate-100">
511
+ <svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
512
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
513
+ </svg>
514
+ </button>
515
+ </div>
516
+ </div>
517
+
518
+ <form id="google-sheet-form" onSubmit={handleSubmit} className="flex-1 space-y-4 overflow-y-auto pt-4 [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
519
+ {gsConfigError && (
520
+ <ConfigErrorAlert
521
+ message={gsConfigError}
522
+ configLink={gsConfigErrorConfigLink || undefined}
523
+ linkLabel="Configurer dans les paramètres"
524
+ />
525
+ )}
526
+ {step === 1 && (
527
+ <>
528
+ <div>
529
+ <label htmlFor="gs-config-name" className="block text-sm font-medium text-gray-700">Nom de la configuration *</label>
530
+ <input id="gs-config-name" type="text" required value={formData.name} onChange={(e) => setFormData((p) => ({ ...p, name: e.target.value }))} 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" placeholder="Ex: Contacts Ventes" />
531
+ </div>
532
+
533
+ {gsPreviewStep !== 'url' && (
534
+ <div className="flex items-center gap-2 text-xs font-medium text-gray-500">
535
+ <span className="rounded-full bg-gray-100 px-2.5 py-0.5 text-gray-500">1. Lien</span>
536
+ <span className="text-gray-300">/</span>
537
+ {gsAvailableSheets.length > 1 && (
538
+ <>
539
+ <span className={cn('rounded-full px-2.5 py-0.5', gsPreviewStep === 'sheet' ? 'bg-blue-100 text-blue-700' : 'bg-gray-100 text-gray-500')}>2. Feuille</span>
540
+ <span className="text-gray-300">/</span>
541
+ </>
542
+ )}
543
+ <span className={cn('rounded-full px-2.5 py-0.5', gsPreviewStep === 'header' ? 'bg-blue-100 text-blue-700' : 'bg-gray-100 text-gray-500')}>
544
+ {gsAvailableSheets.length > 1 ? '3' : '2'}. En-tête
545
+ </span>
546
+ </div>
547
+ )}
548
+
549
+ {gsPreviewStep === 'url' && (
550
+ <div>
551
+ <label htmlFor="gs-sheet-url" className="block text-sm font-medium text-gray-700">Lien du Google Sheet *</label>
552
+ <input id="gs-sheet-url" type="url" required value={formData.sheetUrl} onChange={(e) => { setFormData((p) => ({ ...p, sheetUrl: e.target.value, sheetName: '', headerRow: '1' })); setGsAvailableSheets([]); setGsRawRows([]); setGsSelectedHeaderRow(0); setPreview([]); setHeaders([]); }} 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" placeholder="https://docs.google.com/spreadsheets/d/..." />
553
+ </div>
554
+ )}
555
+
556
+ {gsPreviewStep === 'sheet' && (
557
+ <div className="space-y-6">
558
+ <div className="rounded-lg border border-gray-200 p-4">
559
+ <div className="flex items-center justify-between">
560
+ <p className="text-sm font-medium text-gray-900">{formData.sheetUrl.length > 60 ? formData.sheetUrl.slice(0, 60) + '...' : formData.sheetUrl}</p>
561
+ <button type="button" onClick={() => { setGsPreviewStep('url'); setGsAvailableSheets([]); setGsRawRows([]); }} className="cursor-pointer rounded-lg p-1 text-gray-400 hover:text-gray-600">
562
+ <svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /></svg>
563
+ </button>
564
+ </div>
565
+ </div>
566
+ <div>
567
+ <h3 className="mb-2 text-base font-semibold text-gray-900">Choisir une feuille</h3>
568
+ <p className="mb-4 text-sm text-gray-600">Ce fichier contient {gsAvailableSheets.length} feuilles. Sélectionnez celle qui contient les contacts.</p>
569
+ <div className="space-y-2">
570
+ {gsAvailableSheets.map((name, idx) => (
571
+ <button key={name} type="button" disabled={gsLoadingPreview} onClick={async () => {
572
+ setFormData((p) => ({ ...p, sheetName: name }));
573
+ setGsLoadingPreview(true);
574
+ try {
575
+ const res = await fetch('/api/settings/google-sheet/preview', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sheetUrl: formData.sheetUrl, sheetName: name }) });
576
+ const data = await res.json();
577
+ if (!res.ok) {
578
+ setGsConfigError(data.error || 'Erreur');
579
+ setGsConfigErrorConfigLink(data.configLink || null);
580
+ return;
581
+ }
582
+ setGsConfigError(null);
583
+ setGsConfigErrorConfigLink(null);
584
+ setGsRawRows(data.rawRows || []);
585
+ setGsSelectedHeaderRow(0);
586
+ setGsPreviewStep('header');
587
+ } catch (err: unknown) {
588
+ toast.error(devToast('Impossible de charger l\'aperçu des données. Vérifiez le lien du Google Sheet.', err));
589
+ setGsConfigError(null);
590
+ setGsConfigErrorConfigLink(null);
591
+ } finally { setGsLoadingPreview(false); }
592
+ }} className={cn('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', formData.sheetName === name ? 'border-blue-500 bg-blue-50 text-blue-700' : 'border-gray-200 text-gray-700 hover:border-gray-300 hover:bg-gray-50')}>
593
+ <span className={cn('flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-xs font-bold', formData.sheetName === name ? 'bg-blue-100 text-blue-700' : 'bg-gray-100 text-gray-500')}>{idx + 1}</span>
594
+ {name}
595
+ </button>
596
+ ))}
597
+ </div>
598
+ </div>
599
+ </div>
600
+ )}
601
+
602
+ {gsPreviewStep === 'header' && (
603
+ <div className="space-y-6">
604
+ <div className="rounded-lg border border-gray-200 p-4">
605
+ <div className="flex items-center justify-between">
606
+ <p className="text-sm font-medium text-gray-900">
607
+ {formData.sheetUrl.length > 50 ? formData.sheetUrl.slice(0, 50) + '...' : formData.sheetUrl}
608
+ {formData.sheetName && <span className="ml-2 text-xs text-gray-500">— {formData.sheetName}</span>}
609
+ </p>
610
+ <button type="button" onClick={() => setGsPreviewStep(gsAvailableSheets.length > 1 ? 'sheet' : 'url')} className="cursor-pointer rounded-lg p-1 text-gray-400 hover:text-gray-600">
611
+ <svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /></svg>
612
+ </button>
613
+ </div>
614
+ </div>
615
+ <div>
616
+ <h3 className="mb-2 text-base font-semibold text-gray-900">Sélectionner la ligne d&apos;en-tête</h3>
617
+ <p className="mb-4 text-sm text-gray-600">Cliquez sur la ligne qui contient les noms de colonnes.</p>
618
+ {gsRawRows.length > 0 ? (
619
+ <div className="overflow-x-auto rounded-lg border border-gray-200">
620
+ <table className="min-w-full divide-y divide-gray-200">
621
+ <thead className="bg-gray-50">
622
+ <tr>
623
+ <th className="px-3 py-2 text-left text-xs font-medium text-gray-500">#</th>
624
+ {gsRawRows[0]?.map((_, colIdx) => (
625
+ <th key={`col-${colIdx}`} className="px-3 py-2 text-left text-xs font-medium text-gray-500">Col {colIdx + 1}</th>
626
+ ))}
627
+ </tr>
628
+ </thead>
629
+ <tbody className="divide-y divide-gray-100 bg-white">
630
+ {gsRawRows.map((row, rowIdx) => (
631
+ <tr key={`row-${rowIdx}`} onClick={() => { setGsSelectedHeaderRow(rowIdx); setFormData((p) => ({ ...p, headerRow: String(rowIdx + 1) })); }} className={cn('cursor-pointer transition-colors', gsSelectedHeaderRow === rowIdx ? 'bg-blue-50 ring-2 ring-blue-500 ring-inset' : rowIdx < gsSelectedHeaderRow ? 'bg-gray-50 text-gray-400' : 'hover:bg-gray-50')}>
632
+ <td className="px-3 py-2 text-xs font-medium text-gray-400">{rowIdx + 1}</td>
633
+ {row.map((cell, colIdx) => (
634
+ <td key={`cell-${rowIdx}-${colIdx}`} className={cn('max-w-[200px] truncate px-3 py-2 text-xs', gsSelectedHeaderRow === rowIdx ? 'font-semibold text-blue-700' : 'text-gray-900')}>
635
+ {cell || <span className="text-gray-300">—</span>}
636
+ </td>
637
+ ))}
638
+ </tr>
639
+ ))}
640
+ </tbody>
641
+ </table>
642
+ </div>
643
+ ) : (
644
+ <p className="text-sm text-gray-500">Aucune donnée disponible.</p>
645
+ )}
646
+ </div>
647
+ </div>
648
+ )}
649
+ </>
650
+ )}
651
+
652
+ {step === 2 && (
653
+ <>
654
+ <div className="grid grid-cols-1 gap-4 md:grid-cols-2">
655
+ <div>
656
+ <label htmlFor="gs-assigned-user" className="block text-sm font-medium text-gray-700">Utilisateur assigné par défaut</label>
657
+ <select id="gs-assigned-user" value={formData.defaultAssignedUserId || ''} onChange={(e) => setFormData((p) => ({ ...p, defaultAssignedUserId: e.target.value || null }))} 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">
658
+ <option value="">Aucun utilisateur par défaut</option>
659
+ {users.map((user) => (<option key={user.id} value={user.id}>{user.name} ({user.email})</option>))}
660
+ </select>
661
+ </div>
662
+ <div>
663
+ <label htmlFor="gs-default-status" className="block text-sm font-medium text-gray-700">Statut par défaut</label>
664
+ <StatusSelect
665
+ id="gs-default-status"
666
+ statuses={statuses}
667
+ value={formData.defaultStatusId || ''}
668
+ onChange={(v) => setFormData((p) => ({ ...p, defaultStatusId: v || null }))}
669
+ placeholder="Aucun statut par défaut"
670
+ className="mt-1"
671
+ />
672
+ </div>
673
+ </div>
674
+
675
+ <div className="flex items-center justify-between">
676
+ <h3 className="text-base font-semibold text-gray-900">Correspondance des champs</h3>
677
+ <button type="button" onClick={() => setMappings((p) => [...p, { id: `mapping-${Date.now()}-${Math.random()}`, columnName: '', action: 'ignore' }])} 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">
678
+ <Plus className="h-4 w-4" /> Ajouter un champ
679
+ </button>
680
+ </div>
681
+
682
+ <div className="mt-4 space-y-3">
683
+ {mappings.map((mapping) => (
684
+ <div key={mapping.id} className="flex items-center gap-3 rounded-lg border border-gray-200 bg-gray-50 p-3">
685
+ <div className="flex-1">
686
+ <input type="text" value={mapping.columnName} onChange={(e) => setMappings((p) => p.map((m) => m.id === mapping.id ? { ...m, columnName: e.target.value } : m))} placeholder="Nom de la colonne Google Sheets" 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-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50" />
687
+ </div>
688
+ <ArrowRight className="h-5 w-5 shrink-0 text-gray-400" />
689
+ <div className="flex-1">
690
+ <select value={mapping.action} onChange={(e) => { const a = e.target.value as 'map' | 'note' | 'ignore'; setMappings((p) => p.map((m) => m.id === mapping.id ? { ...m, action: a, crmField: a === 'map' ? (m.crmField || 'firstName') : undefined } : m)); }} 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-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50">
691
+ <option value="map">Mapper vers un champ</option>
692
+ <option value="note">Ajouter comme note</option>
693
+ <option value="ignore">-- Ne pas importer --</option>
694
+ </select>
695
+ </div>
696
+ {mapping.action === 'map' && (
697
+ <>
698
+ <ArrowRight className="h-5 w-5 shrink-0 text-gray-400" />
699
+ <div className="flex-1">
700
+ <select value={mapping.crmField || ''} onChange={(e) => setMappings((p) => p.map((m) => m.id === mapping.id ? { ...m, crmField: e.target.value } : m))} 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-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50">
701
+ <option value="">Sélectionnez un champ</option>
702
+ <option value="phone">Téléphone *</option>
703
+ <option value="firstName">Prénom</option>
704
+ <option value="lastName">Nom</option>
705
+ <option value="email">Email</option>
706
+ <option value="civility">Civilité</option>
707
+ <option value="secondaryPhone">Téléphone secondaire</option>
708
+ <option value="address">Adresse</option>
709
+ <option value="city">Ville</option>
710
+ <option value="postalCode">Code postal</option>
711
+ <option value="companyName">Société</option>
712
+ <option value="origin">Origine</option>
713
+ <option value="website">Site internet</option>
714
+ <option value="jobTitle">Intitulé du poste</option>
715
+ <option value="createdAt">Date de création</option>
716
+ <option value="linkedin">LinkedIn</option>
717
+ <option value="facebook">Facebook</option>
718
+ <option value="twitter">Twitter</option>
719
+ <option value="instagram">Instagram</option>
720
+ </select>
721
+ </div>
722
+ </>
723
+ )}
724
+ <button type="button" onClick={() => setMappings((p) => p.filter((m) => m.id !== mapping.id))} className="cursor-pointer rounded-lg p-2 text-gray-400 transition-colors hover:bg-gray-200 hover:text-blue-600">
725
+ <Trash2 className="h-4 w-4" />
726
+ </button>
727
+ </div>
728
+ ))}
729
+ </div>
730
+
731
+ <div className="mt-6 rounded-lg border border-gray-200 bg-gray-50 p-4">
732
+ <ul className="space-y-2 text-xs text-gray-600">
733
+ <li>• Le champ &quot;Téléphone&quot; est obligatoire pour l&#39;import.</li>
734
+ <li>• Les colonnes &quot;-- Ne pas importer --&quot; seront ignorées</li>
735
+ <li>• Les colonnes &quot;Ajouter comme note&quot; seront regroupées dans une seule note</li>
736
+ </ul>
737
+ </div>
738
+
739
+ {preview.length > 0 && headers.length > 0 && (
740
+ <div>
741
+ <h3 className="mb-3 text-sm font-semibold text-gray-900">Aperçu (5 premières lignes)</h3>
742
+ <div className="overflow-x-auto rounded-lg border border-gray-200">
743
+ <table className="min-w-full divide-y divide-gray-200">
744
+ <thead className="bg-gray-50">
745
+ <tr>{headers.map((h) => (<th key={h} className="px-4 py-2 text-left text-xs font-medium text-gray-700">{h}</th>))}</tr>
746
+ </thead>
747
+ <tbody className="divide-y divide-gray-200 bg-white">
748
+ {preview.map((row, idx) => (<tr key={`preview-${idx}`}>{headers.map((h) => (<td key={h} className="px-4 py-2 text-xs whitespace-nowrap text-gray-900">{row[h] || '-'}</td>))}</tr>))}
749
+ </tbody>
750
+ </table>
751
+ </div>
752
+ </div>
753
+ )}
754
+ </>
755
+ )}
756
+ </form>
757
+
758
+ <div className="shrink-0 border-t border-slate-200 pt-4">
759
+ <div className="flex flex-col gap-3 sm:flex-row sm:justify-end">
760
+ <button type="button" onClick={resetModal} className="w-full cursor-pointer rounded-xl border border-slate-300 bg-white px-4 py-2 text-sm font-semibold text-slate-700 transition-colors hover:bg-slate-50 sm:w-auto">Annuler</button>
761
+ {step === 1 && gsPreviewStep === 'url' ? (
762
+ <button type="button" disabled={gsLoadingPreview || !formData.sheetUrl} onClick={async () => {
763
+ setGsLoadingPreview(true);
764
+ setGsConfigError(null);
765
+ setGsConfigErrorConfigLink(null);
766
+ try {
767
+ const res = await fetch('/api/settings/google-sheet/preview', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sheetUrl: formData.sheetUrl }) });
768
+ const data = await res.json();
769
+ if (!res.ok) {
770
+ setGsConfigError(data.error || 'Erreur');
771
+ setGsConfigErrorConfigLink(data.configLink || null);
772
+ if (!data.configLink) toast.error('Impossible de charger l\'aperçu des données. Vérifiez le lien du Google Sheet.');
773
+ return;
774
+ }
775
+ setGsAvailableSheets(data.sheetNames || []);
776
+ setGsRawRows(data.rawRows || []);
777
+ if (data.sheetNames?.length > 1) { setGsPreviewStep('sheet'); } else { setFormData((p) => ({ ...p, sheetName: data.sheetNames?.[0] || '' })); setGsPreviewStep('header'); }
778
+ } catch (err: unknown) { toast.error(devToast('Impossible de charger l\'aperçu. Vérifiez votre connexion.', err)); setGsConfigError(null); setGsConfigErrorConfigLink(null); } finally { setGsLoadingPreview(false); }
779
+ }} className="w-full cursor-pointer rounded-xl bg-blue-600 px-4 py-2 text-sm font-semibold text-white transition-colors hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto">
780
+ {gsLoadingPreview ? 'Chargement...' : 'Charger les feuilles'}
781
+ </button>
782
+ ) : step === 1 ? (
783
+ <button type="button" disabled={saving || gsPreviewStep !== 'header' || gsRawRows.length === 0} onClick={async () => { const ok = await handleAutoMap(); if (ok) setStep(2); }} className="w-full cursor-pointer rounded-xl bg-blue-600 px-4 py-2 text-sm font-semibold text-white transition-colors hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto">
784
+ Étape suivante
785
+ </button>
786
+ ) : (
787
+ <button type="submit" form="google-sheet-form" disabled={saving} className="w-full cursor-pointer rounded-xl bg-blue-600 px-4 py-2 text-sm font-semibold text-white transition-colors hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto">
788
+ {saving ? 'Enregistrement...' : 'Enregistrer'}
789
+ </button>
790
+ )}
791
+ </div>
792
+ </div>
793
+ </div>
794
+ </div>
795
+ )}
796
+
797
+ <ImportResultDialog
798
+ open={showImportResult}
799
+ onClose={() => setShowImportResult(false)}
800
+ configName={importConfigName}
801
+ results={importResults}
802
+ totalImported={importTotals.totalImported}
803
+ totalUpdated={importTotals.totalUpdated}
804
+ totalSkipped={importTotals.totalSkipped}
805
+ />
806
+ <ConfirmDialog />
807
+ </>
808
+ );
809
+ }