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,680 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useMemo, useState, useCallback } from 'react';
4
+ import {
5
+ X,
6
+ Link2,
7
+ FileText,
8
+ Settings2,
9
+ ExternalLink,
10
+ Save,
11
+ Loader2,
12
+ Plus,
13
+ Trash2,
14
+ ArrowRight,
15
+ } from 'lucide-react';
16
+ import { cn, devToast } from '@/lib/utils';
17
+ import { useAppToast } from '@/contexts/app-toast-context';
18
+ import { IntegrationLogsTable } from './IntegrationLogsTable';
19
+ import { StatusSelect } from '@/components/ui/status-select';
20
+
21
+ type MonitoringTab = 'logs' | 'tableau' | 'modifier';
22
+
23
+ type MappingAction = 'map' | 'note' | 'ignore';
24
+
25
+ interface ColumnMapping {
26
+ id: string;
27
+ columnName: string;
28
+ action: MappingAction;
29
+ crmField?: string;
30
+ }
31
+
32
+ interface MonitoringConfig {
33
+ id: string;
34
+ name: string;
35
+ spreadsheetId: string;
36
+ sheetName: string;
37
+ headerRow: number;
38
+ active: boolean;
39
+ defaultStatusId: string | null;
40
+ defaultAssignedUserId: string | null;
41
+ phoneColumn?: string;
42
+ firstNameColumn?: string | null;
43
+ lastNameColumn?: string | null;
44
+ emailColumn?: string | null;
45
+ cityColumn?: string | null;
46
+ postalCodeColumn?: string | null;
47
+ originColumn?: string | null;
48
+ columnMappings?: ColumnMapping[];
49
+ }
50
+
51
+ interface GoogleSheetConfigMonitoringModalProps {
52
+ open: boolean;
53
+ onClose: () => void;
54
+ config: MonitoringConfig | null;
55
+ statuses: Array<{ id: string; name: string; color: string }>;
56
+ users: Array<{ id: string; name: string; email: string }>;
57
+ onSaved: () => Promise<void> | void;
58
+ }
59
+
60
+ function buildMappingsFromConfig(c: MonitoringConfig): ColumnMapping[] {
61
+ if (c.columnMappings && Array.isArray(c.columnMappings) && c.columnMappings.length > 0) {
62
+ return c.columnMappings.map((m) => ({
63
+ ...m,
64
+ id: m.id || `mapping-${Date.now()}-${Math.random()}`,
65
+ }));
66
+ }
67
+ const mappings: ColumnMapping[] = [];
68
+ const legacy = [
69
+ { column: c.phoneColumn, field: 'phone' },
70
+ { column: c.firstNameColumn, field: 'firstName' },
71
+ { column: c.lastNameColumn, field: 'lastName' },
72
+ { column: c.emailColumn, field: 'email' },
73
+ { column: c.cityColumn, field: 'city' },
74
+ { column: c.postalCodeColumn, field: 'postalCode' },
75
+ { column: c.originColumn, field: 'origin' },
76
+ ];
77
+ legacy.forEach(({ column, field }) => {
78
+ if (column) {
79
+ mappings.push({
80
+ id: `mapping-${Date.now()}-${Math.random()}`,
81
+ columnName: column,
82
+ action: 'map',
83
+ crmField: field,
84
+ });
85
+ }
86
+ });
87
+ return mappings;
88
+ }
89
+
90
+ export function GoogleSheetConfigMonitoringModal({
91
+ open,
92
+ onClose,
93
+ config,
94
+ statuses,
95
+ users,
96
+ onSaved,
97
+ }: Readonly<GoogleSheetConfigMonitoringModalProps>) {
98
+ const toast = useAppToast();
99
+ const [activeTab, setActiveTab] = useState<MonitoringTab>('logs');
100
+ const [saving, setSaving] = useState(false);
101
+ const [formData, setFormData] = useState({
102
+ name: '',
103
+ active: true,
104
+ sheetUrl: '',
105
+ sheetName: '',
106
+ headerRow: '1',
107
+ defaultStatusId: null as string | null,
108
+ defaultAssignedUserId: null as string | null,
109
+ });
110
+ const [mappings, setMappings] = useState<ColumnMapping[]>([]);
111
+ const [tableauRawRows, setTableauRawRows] = useState<string[][]>([]);
112
+ const [tableauLoading, setTableauLoading] = useState(false);
113
+ const [tableauError, setTableauError] = useState<string | null>(null);
114
+
115
+ const fetchTableauData = useCallback(async () => {
116
+ if (!config?.spreadsheetId || !config?.sheetName) return;
117
+ setTableauLoading(true);
118
+ setTableauError(null);
119
+ try {
120
+ const sheetUrl = `https://docs.google.com/spreadsheets/d/${config.spreadsheetId}/edit`;
121
+ const res = await fetch('/api/settings/google-sheet/preview', {
122
+ method: 'POST',
123
+ headers: { 'Content-Type': 'application/json' },
124
+ body: JSON.stringify({ sheetUrl, sheetName: config.sheetName }),
125
+ });
126
+ const data = await res.json();
127
+ if (!res.ok) throw new Error(data.error || 'Erreur');
128
+ setTableauRawRows(data.rawRows || []);
129
+ } catch (err: unknown) {
130
+ setTableauError(err instanceof Error ? err.message : 'Impossible de charger le tableau');
131
+ setTableauRawRows([]);
132
+ } finally {
133
+ setTableauLoading(false);
134
+ }
135
+ }, [config?.spreadsheetId, config?.sheetName]);
136
+
137
+ useEffect(() => {
138
+ if (!open || !config) return;
139
+ setActiveTab('logs');
140
+ setFormData({
141
+ name: config.name,
142
+ active: config.active,
143
+ sheetUrl: `https://docs.google.com/spreadsheets/d/${config.spreadsheetId}/edit`,
144
+ sheetName: config.sheetName,
145
+ headerRow: String(config.headerRow),
146
+ defaultStatusId: config.defaultStatusId,
147
+ defaultAssignedUserId: config.defaultAssignedUserId,
148
+ });
149
+ setMappings(buildMappingsFromConfig(config));
150
+ }, [open, config]);
151
+
152
+ const spreadsheetPublicUrl = useMemo(() => {
153
+ if (!config?.spreadsheetId) return '';
154
+ return `https://docs.google.com/spreadsheets/d/${config.spreadsheetId}`;
155
+ }, [config?.spreadsheetId]);
156
+
157
+ useEffect(() => {
158
+ if (open && config && activeTab === 'tableau') {
159
+ fetchTableauData();
160
+ }
161
+ }, [open, config, activeTab, fetchTableauData]);
162
+
163
+ const updateMappingColumnName = (id: string, value: string) => {
164
+ setMappings((prev) =>
165
+ prev.map((m) => (m.id === id ? { ...m, columnName: value } : m)),
166
+ );
167
+ };
168
+ const updateMappingAction = (id: string, action: MappingAction) => {
169
+ setMappings((prev) =>
170
+ prev.map((m) =>
171
+ m.id === id
172
+ ? { ...m, action, crmField: action === 'map' ? (m.crmField || 'firstName') : undefined }
173
+ : m,
174
+ ),
175
+ );
176
+ };
177
+ const updateMappingCrmField = (id: string, value: string) => {
178
+ setMappings((prev) =>
179
+ prev.map((m) => (m.id === id ? { ...m, crmField: value } : m)),
180
+ );
181
+ };
182
+ const removeMapping = (id: string) => {
183
+ setMappings((prev) => prev.filter((m) => m.id !== id));
184
+ };
185
+
186
+ const handleSave = async () => {
187
+ if (!config) return;
188
+ const phoneMapping = mappings.find(
189
+ (m) => m.action === 'map' && m.crmField === 'phone' && m.columnName.trim() !== '',
190
+ );
191
+ if (!phoneMapping) {
192
+ toast.error('Le mapping du téléphone est obligatoire');
193
+ return;
194
+ }
195
+ setSaving(true);
196
+ try {
197
+ const res = await fetch(`/api/settings/google-sheet/${config.id}`, {
198
+ method: 'PUT',
199
+ headers: { 'Content-Type': 'application/json' },
200
+ body: JSON.stringify({ ...formData, columnMappings: mappings }),
201
+ });
202
+ const data = await res.json();
203
+ if (!res.ok) throw new Error(data.error || 'Erreur lors de la sauvegarde');
204
+ toast.success('Configuration Google Sheets mise à jour');
205
+ await onSaved();
206
+ onClose();
207
+ } catch (error: unknown) {
208
+ toast.error(devToast('Impossible de mettre à jour la configuration. Veuillez réessayer.', error));
209
+ } finally {
210
+ setSaving(false);
211
+ }
212
+ };
213
+
214
+ if (!open || !config) return null;
215
+
216
+ const tabs: Array<{
217
+ id: MonitoringTab;
218
+ label: string;
219
+ icon: React.ComponentType<{ className?: string }>;
220
+ }> = [
221
+ { id: 'logs', label: 'Logs', icon: FileText },
222
+ { id: 'tableau', label: 'Tableau', icon: Link2 },
223
+ { id: 'modifier', label: 'Modifier', icon: Settings2 },
224
+ ];
225
+
226
+ return (
227
+ <div className="ui-fade-in fixed inset-0 z-50 flex min-h-dvh flex-col items-stretch justify-stretch bg-gray-500/20 p-0 backdrop-blur-sm sm:p-4">
228
+ <div className="flex flex-1 flex-col overflow-hidden rounded-none bg-white shadow-2xl sm:mx-auto sm:max-h-[90vh] sm:w-full sm:max-w-[min(1100px,90vw)] sm:rounded-2xl overscroll-contain">
229
+ {/* Header + Tabs */}
230
+ <div className="shrink-0 border-b border-slate-200 bg-white px-4 py-4 sm:px-6">
231
+ <div className="flex items-start justify-between gap-4">
232
+ <div className="min-w-0 flex-1">
233
+ <p className="text-[11px] font-semibold uppercase tracking-widest text-slate-500">
234
+ Monitoring Google Sheets
235
+ </p>
236
+ <h3 className="mt-1 truncate text-xl font-semibold text-slate-900">{config.name}</h3>
237
+ <p className="mt-0.5 text-sm text-slate-600">
238
+ Logs, tableau et édition complète de la configuration.
239
+ </p>
240
+ </div>
241
+ <button
242
+ type="button"
243
+ onClick={onClose}
244
+ className="shrink-0 rounded-xl p-2 text-slate-400 transition-colors duration-200 hover:bg-slate-100 hover:text-slate-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
245
+ aria-label="Fermer"
246
+ >
247
+ <X className="h-5 w-5" />
248
+ </button>
249
+ </div>
250
+
251
+ {/* Lien du tableau (intégré dans l'en-tête) */}
252
+ <div className="mt-4">
253
+ <p className="text-[11px] font-semibold uppercase tracking-widest text-slate-500">
254
+ Lien du tableau
255
+ </p>
256
+ <p className="mt-1.5 break-all rounded-xl border border-slate-200 bg-slate-50 px-3 py-2.5 text-sm text-slate-800">
257
+ {spreadsheetPublicUrl}
258
+ </p>
259
+ <p className="mt-1.5 text-xs text-slate-500">
260
+ Ouvrez le tableau dans un nouvel onglet pour visualiser ou modifier les données.
261
+ </p>
262
+ </div>
263
+
264
+ {/* Tabs: toute la largeur disponible */}
265
+ <div className="mt-4 flex w-full gap-1 rounded-xl bg-slate-100/80 p-1">
266
+ {tabs.map((tab) => {
267
+ const Icon = tab.icon;
268
+ const isActive = activeTab === tab.id;
269
+ return (
270
+ <button
271
+ key={tab.id}
272
+ type="button"
273
+ onClick={() => setActiveTab(tab.id)}
274
+ className={cn(
275
+ 'flex min-w-0 flex-1 items-center justify-center gap-2 rounded-lg px-3 py-2.5 text-sm font-medium transition-[color,background-color,box-shadow] duration-200',
276
+ isActive
277
+ ? 'bg-white text-blue-700 shadow-sm ring-1 ring-slate-200/80'
278
+ : 'text-slate-600 hover:bg-white/60 hover:text-slate-900',
279
+ )}
280
+ >
281
+ <Icon className="h-4 w-4 shrink-0" />
282
+ <span className="truncate">{tab.label}</span>
283
+ </button>
284
+ );
285
+ })}
286
+ </div>
287
+ </div>
288
+
289
+ {/* Content: full space, no centered box */}
290
+ <div className="flex min-h-0 flex-1 flex-col overflow-hidden">
291
+ <div className="flex-1 overflow-auto">
292
+ {activeTab === 'logs' && (
293
+ <div className="h-full p-4 sm:p-6 ui-fade-in">
294
+ <IntegrationLogsTable
295
+ integrationType="google_sheet"
296
+ configId={config.id}
297
+ enabled={open && activeTab === 'logs'}
298
+ />
299
+ </div>
300
+ )}
301
+
302
+ {activeTab === 'tableau' && (
303
+ <div className="flex h-full min-h-0 flex-1 flex-col overflow-auto p-4 sm:p-6 ui-fade-in">
304
+ <h3 className="mb-2 text-base font-semibold text-slate-900">
305
+ Contenu du tableau
306
+ </h3>
307
+ <p className="mb-4 text-sm text-slate-600">
308
+ Aperçu des données de la feuille «&nbsp;{config.sheetName}&nbsp;» (ligne d&apos;en-tête : {config.headerRow}).
309
+ </p>
310
+ {(() => {
311
+ if (tableauLoading) {
312
+ return (
313
+ <div className="flex flex-1 items-center justify-center py-12">
314
+ <Loader2 className="h-8 w-8 animate-spin text-slate-400" role="status" aria-label="Chargement" />
315
+ </div>
316
+ );
317
+ }
318
+ if (tableauError) {
319
+ return (
320
+ <div className="rounded-xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-800">
321
+ {tableauError}
322
+ </div>
323
+ );
324
+ }
325
+ if (tableauRawRows.length === 0) {
326
+ return (
327
+ <p className="text-sm text-slate-500">Aucune donnée disponible.</p>
328
+ );
329
+ }
330
+ return (
331
+ <div className="overflow-x-auto rounded-xl border border-slate-200 bg-white">
332
+ <table className="min-w-full divide-y divide-slate-200">
333
+ <thead className="bg-slate-50">
334
+ <tr>
335
+ <th className="px-3 py-2 text-left text-xs font-medium text-slate-500">
336
+ #
337
+ </th>
338
+ {tableauRawRows[0]?.map((_, colIdx) => (
339
+ <th
340
+ key={`col-${colIdx}`}
341
+ className="px-3 py-2 text-left text-xs font-medium text-slate-500"
342
+ >
343
+ Col {colIdx + 1}
344
+ </th>
345
+ ))}
346
+ </tr>
347
+ </thead>
348
+ <tbody className="divide-y divide-slate-100 bg-white">
349
+ {tableauRawRows.map((row, rowIdx) => {
350
+ const isHeaderRow = rowIdx + 1 === config.headerRow;
351
+ return (
352
+ <tr
353
+ key={`tableau-row-${rowIdx}`}
354
+ className={cn(
355
+ isHeaderRow
356
+ ? 'bg-blue-50 ring-2 ring-blue-500 ring-inset'
357
+ : 'hover:bg-slate-50/70',
358
+ )}
359
+ >
360
+ <td className="px-3 py-2 text-xs font-medium text-slate-500">
361
+ {rowIdx + 1}
362
+ </td>
363
+ {row.map((cell, colIdx) => (
364
+ <td
365
+ key={`tableau-cell-${rowIdx}-${colIdx}`}
366
+ className={cn(
367
+ 'max-w-[200px] truncate px-3 py-2 text-xs text-slate-900',
368
+ isHeaderRow && 'font-semibold text-blue-700',
369
+ )}
370
+ >
371
+ {cell || <span className="text-slate-300">—</span>}
372
+ </td>
373
+ ))}
374
+ </tr>
375
+ );
376
+ })}
377
+ </tbody>
378
+ </table>
379
+ </div>
380
+ );
381
+ })()}
382
+ </div>
383
+ )}
384
+
385
+ {activeTab === 'modifier' && (
386
+ <div className="p-4 sm:p-6 ui-fade-in">
387
+ <form
388
+ id="monitoring-modifier-form"
389
+ onSubmit={(e) => {
390
+ e.preventDefault();
391
+ handleSave();
392
+ }}
393
+ className="space-y-6"
394
+ >
395
+ <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
396
+ <div>
397
+ <label
398
+ htmlFor="gs-monitoring-name"
399
+ className="block text-sm font-medium text-slate-700"
400
+ >
401
+ Nom de la configuration
402
+ </label>
403
+ <input
404
+ id="gs-monitoring-name"
405
+ value={formData.name}
406
+ onChange={(e) =>
407
+ setFormData((prev) => ({ ...prev, name: e.target.value }))
408
+ }
409
+ autoComplete="off"
410
+ className="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2.5 text-sm text-slate-900 focus-visible:border-blue-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
411
+ />
412
+ </div>
413
+ <div>
414
+ <label
415
+ htmlFor="gs-monitoring-header-row"
416
+ className="block text-sm font-medium text-slate-700"
417
+ >
418
+ Ligne d&apos;en-tête
419
+ </label>
420
+ <input
421
+ id="gs-monitoring-header-row"
422
+ value={formData.headerRow}
423
+ onChange={(e) =>
424
+ setFormData((prev) => ({ ...prev, headerRow: e.target.value }))
425
+ }
426
+ autoComplete="off"
427
+ className="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2.5 text-sm text-slate-900 focus-visible:border-blue-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
428
+ />
429
+ </div>
430
+ </div>
431
+
432
+ <div>
433
+ <label
434
+ htmlFor="gs-monitoring-url"
435
+ className="block text-sm font-medium text-slate-700"
436
+ >
437
+ Lien Google Sheet
438
+ </label>
439
+ <input
440
+ id="gs-monitoring-url"
441
+ value={formData.sheetUrl}
442
+ onChange={(e) =>
443
+ setFormData((prev) => ({ ...prev, sheetUrl: e.target.value }))
444
+ }
445
+ placeholder="https://docs.google.com/spreadsheets/d/..."
446
+ autoComplete="url"
447
+ className="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2.5 text-sm text-slate-900 focus-visible:border-blue-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
448
+ />
449
+ </div>
450
+
451
+ <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
452
+ <div>
453
+ <label
454
+ htmlFor="gs-monitoring-sheet-name"
455
+ className="block text-sm font-medium text-slate-700"
456
+ >
457
+ Nom de l&apos;onglet
458
+ </label>
459
+ <input
460
+ id="gs-monitoring-sheet-name"
461
+ value={formData.sheetName}
462
+ onChange={(e) =>
463
+ setFormData((prev) => ({ ...prev, sheetName: e.target.value }))
464
+ }
465
+ autoComplete="off"
466
+ className="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2.5 text-sm text-slate-900 focus-visible:border-blue-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
467
+ />
468
+ </div>
469
+ <div className="flex items-end pb-2">
470
+ <label className="inline-flex cursor-pointer items-center gap-2 text-sm font-medium text-slate-700">
471
+ <input
472
+ type="checkbox"
473
+ checked={formData.active}
474
+ onChange={(e) =>
475
+ setFormData((prev) => ({ ...prev, active: e.target.checked }))
476
+ }
477
+ className="h-4 w-4 rounded border-slate-300 text-blue-600 focus:ring-blue-500"
478
+ />
479
+ <span className="ml-2">Intégration active</span>
480
+ </label>
481
+ </div>
482
+ </div>
483
+
484
+ <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
485
+ <div>
486
+ <label
487
+ htmlFor="gs-monitoring-user"
488
+ className="block text-sm font-medium text-slate-700"
489
+ >
490
+ Utilisateur par défaut
491
+ </label>
492
+ <select
493
+ id="gs-monitoring-user"
494
+ value={formData.defaultAssignedUserId || ''}
495
+ onChange={(e) =>
496
+ setFormData((prev) => ({
497
+ ...prev,
498
+ defaultAssignedUserId: e.target.value || null,
499
+ }))
500
+ }
501
+ className="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2.5 text-sm text-slate-900 focus-visible:border-blue-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
502
+ >
503
+ <option value="">Aucun utilisateur par défaut</option>
504
+ {users.map((user) => (
505
+ <option key={user.id} value={user.id}>
506
+ {user.name} ({user.email})
507
+ </option>
508
+ ))}
509
+ </select>
510
+ </div>
511
+ <div>
512
+ <label
513
+ htmlFor="gs-monitoring-status"
514
+ className="block text-sm font-medium text-slate-700"
515
+ >
516
+ Statut par défaut
517
+ </label>
518
+ <StatusSelect
519
+ id="gs-monitoring-status"
520
+ statuses={statuses}
521
+ value={formData.defaultStatusId || ''}
522
+ onChange={(v) =>
523
+ setFormData((prev) => ({ ...prev, defaultStatusId: v || null }))
524
+ }
525
+ placeholder="Aucun statut par défaut"
526
+ className="mt-1"
527
+ />
528
+ </div>
529
+ </div>
530
+
531
+ {/* Correspondance des champs - édition complète */}
532
+ <div className="border-t border-slate-200 pt-6">
533
+ <div className="flex flex-wrap items-center justify-between gap-2">
534
+ <h4 className="text-base font-semibold text-slate-900">
535
+ Correspondance des champs
536
+ </h4>
537
+ <button
538
+ type="button"
539
+ onClick={() =>
540
+ setMappings((prev) => [
541
+ ...prev,
542
+ {
543
+ id: `mapping-${Date.now()}-${Math.random()}`,
544
+ columnName: '',
545
+ action: 'ignore',
546
+ },
547
+ ])
548
+ }
549
+ className="inline-flex items-center gap-1.5 rounded-xl border border-blue-600 bg-white px-3 py-2 text-sm font-medium text-blue-600 transition-colors hover:bg-blue-50"
550
+ >
551
+ <Plus className="h-4 w-4" />
552
+ Ajouter un champ
553
+ </button>
554
+ </div>
555
+ <p className="mt-1 text-xs text-slate-500">
556
+ Le champ Téléphone est obligatoire. Vous pouvez ajouter, modifier ou
557
+ supprimer des lignes.
558
+ </p>
559
+ <div className="mt-4 space-y-3">
560
+ {mappings.map((mapping) => (
561
+ <div
562
+ key={mapping.id}
563
+ className="flex flex-wrap items-center gap-2 rounded-xl border border-slate-200 bg-slate-50/50 p-3 sm:gap-3"
564
+ >
565
+ <input
566
+ type="text"
567
+ value={mapping.columnName}
568
+ onChange={(e) => updateMappingColumnName(mapping.id, e.target.value)}
569
+ placeholder="Nom de la colonne Google Sheets"
570
+ className="min-w-[120px] flex-1 rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 focus-visible:border-blue-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
571
+ />
572
+ <ArrowRight className="h-4 w-4 shrink-0 text-slate-400" />
573
+ <select
574
+ value={mapping.action}
575
+ onChange={(e) =>
576
+ updateMappingAction(
577
+ mapping.id,
578
+ e.target.value as MappingAction,
579
+ )
580
+ }
581
+ className="min-w-[160px] flex-1 rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 focus-visible:border-blue-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
582
+ >
583
+ <option value="map">Mapper vers un champ</option>
584
+ <option value="note">Ajouter comme note</option>
585
+ <option value="ignore">Ne pas importer</option>
586
+ </select>
587
+ {mapping.action === 'map' && (
588
+ <>
589
+ <ArrowRight className="h-4 w-4 shrink-0 text-slate-400" />
590
+ <select
591
+ value={mapping.crmField || ''}
592
+ onChange={(e) =>
593
+ updateMappingCrmField(mapping.id, e.target.value)
594
+ }
595
+ className="min-w-[140px] flex-1 rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 focus-visible:border-blue-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
596
+ >
597
+ <option value="" disabled>Sélectionner un champ</option>
598
+ <option value="phone">Téléphone *</option>
599
+ <option value="firstName">Prénom</option>
600
+ <option value="lastName">Nom</option>
601
+ <option value="email">Email</option>
602
+ <option value="civility">Civilité</option>
603
+ <option value="secondaryPhone">Téléphone secondaire</option>
604
+ <option value="address">Adresse</option>
605
+ <option value="city">Ville</option>
606
+ <option value="postalCode">Code postal</option>
607
+ <option value="companyName">Société</option>
608
+ <option value="origin">Origine</option>
609
+ <option value="website">Site internet</option>
610
+ <option value="jobTitle">Intitulé du poste</option>
611
+ <option value="createdAt">Date de création</option>
612
+ <option value="linkedin">LinkedIn</option>
613
+ <option value="facebook">Facebook</option>
614
+ <option value="twitter">Twitter</option>
615
+ <option value="instagram">Instagram</option>
616
+ </select>
617
+ </>
618
+ )}
619
+ <button
620
+ type="button"
621
+ onClick={() => removeMapping(mapping.id)}
622
+ className="rounded-lg p-2 text-slate-400 transition-colors hover:bg-rose-100 hover:text-rose-600"
623
+ aria-label="Supprimer le mapping"
624
+ >
625
+ <Trash2 className="h-4 w-4" />
626
+ </button>
627
+ </div>
628
+ ))}
629
+ </div>
630
+ </div>
631
+ </form>
632
+ </div>
633
+ )}
634
+ </div>
635
+
636
+ {/* Sticky footer */}
637
+ <div className="shrink-0 border-t border-slate-200 bg-white px-4 py-3 sm:px-6">
638
+ <div className="flex flex-wrap items-center justify-between gap-3">
639
+ <button
640
+ type="button"
641
+ onClick={onClose}
642
+ className="rounded-xl border border-slate-300 bg-white px-4 py-2.5 text-sm font-medium text-slate-700 transition-colors hover:bg-slate-50"
643
+ >
644
+ Fermer
645
+ </button>
646
+ <div className="flex gap-2">
647
+ {activeTab === 'tableau' && (
648
+ <a
649
+ href={spreadsheetPublicUrl}
650
+ target="_blank"
651
+ rel="noopener noreferrer"
652
+ className="inline-flex items-center gap-2 rounded-xl bg-blue-600 px-4 py-2.5 text-sm font-semibold text-white transition-colors hover:bg-blue-700"
653
+ >
654
+ Ouvrir le tableau
655
+ <ExternalLink className="h-4 w-4" />
656
+ </a>
657
+ )}
658
+ {activeTab === 'modifier' && (
659
+ <button
660
+ type="submit"
661
+ form="monitoring-modifier-form"
662
+ disabled={saving}
663
+ className="inline-flex items-center gap-2 rounded-xl bg-blue-600 px-4 py-2.5 text-sm font-semibold text-white transition-colors hover:bg-blue-700 disabled:opacity-60"
664
+ >
665
+ {saving ? (
666
+ <Loader2 className="h-4 w-4 animate-spin" />
667
+ ) : (
668
+ <Save className="h-4 w-4" />
669
+ )}
670
+ {saving ? 'Enregistrement...' : 'Enregistrer les modifications'}
671
+ </button>
672
+ )}
673
+ </div>
674
+ </div>
675
+ </div>
676
+ </div>
677
+ </div>
678
+ </div>
679
+ );
680
+ }