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.
- package/bin/create-crm-tmp.js +56 -35
- package/package.json +1 -1
- package/template/README.md +230 -115
- package/template/eslint.config.mjs +13 -0
- package/template/next.config.ts +14 -0
- package/template/package.json +15 -2
- package/template/prisma/migrations/20260318095700_init_db/migration.sql +978 -0
- package/template/prisma/migrations/migration_lock.toml +3 -0
- package/template/prisma/schema.prisma +132 -637
- package/template/src/app/(auth)/invite/[token]/page.tsx +10 -8
- package/template/src/app/(auth)/layout.tsx +1 -1
- package/template/src/app/(auth)/reset-password/complete/page.tsx +11 -8
- package/template/src/app/(auth)/reset-password/page.tsx +4 -4
- package/template/src/app/(auth)/reset-password/verify/page.tsx +4 -4
- package/template/src/app/(auth)/signin/page.tsx +14 -6
- package/template/src/app/(dashboard)/agenda/page.tsx +2243 -988
- package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +18 -104
- package/template/src/app/(dashboard)/automatisation/page.tsx +10 -26
- package/template/src/app/(dashboard)/closing/page.tsx +78 -62
- package/template/src/app/(dashboard)/contacts/[id]/page.tsx +2082 -1080
- package/template/src/app/(dashboard)/contacts/companies/[id]/page.tsx +46 -47
- package/template/src/app/(dashboard)/contacts/page.tsx +1062 -780
- package/template/src/app/(dashboard)/dashboard/page.tsx +533 -37
- package/template/src/app/(dashboard)/dev/page.tsx +1291 -0
- package/template/src/app/(dashboard)/layout.tsx +6 -2
- package/template/src/app/(dashboard)/settings/page.tsx +797 -2582
- package/template/src/app/(dashboard)/templates/page.tsx +55 -54
- package/template/src/app/(dashboard)/users/list/page.tsx +51 -48
- package/template/src/app/(dashboard)/users/page.tsx +1 -1
- package/template/src/app/(dashboard)/users/permissions/page.tsx +2 -2
- package/template/src/app/(dashboard)/users/roles/page.tsx +7 -5
- package/template/src/app/api/agenda/google-events/route.ts +92 -0
- package/template/src/app/api/auth/check-active/route.ts +3 -2
- package/template/src/app/api/auth/google/route.ts +2 -1
- package/template/src/app/api/auth/google/status/route.ts +7 -31
- package/template/src/app/api/companies/[id]/activities/route.ts +1 -3
- package/template/src/app/api/companies/[id]/route.ts +1 -2
- package/template/src/app/api/companies/route.ts +42 -12
- package/template/src/app/api/contacts/[id]/files/[fileId]/preview/route.ts +9 -31
- package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +14 -32
- package/template/src/app/api/contacts/[id]/files/route.ts +112 -212
- package/template/src/app/api/contacts/[id]/interactions/[interactionId]/route.ts +27 -1
- package/template/src/app/api/contacts/[id]/interactions/route.ts +16 -16
- package/template/src/app/api/contacts/[id]/kyc/route.ts +21 -11
- package/template/src/app/api/contacts/[id]/meet/route.ts +19 -2
- package/template/src/app/api/contacts/[id]/route.ts +106 -34
- package/template/src/app/api/contacts/[id]/send-email/route.ts +27 -11
- package/template/src/app/api/contacts/[id]/workflows/run/route.ts +6 -0
- package/template/src/app/api/contacts/export/route.ts +9 -13
- package/template/src/app/api/contacts/import/route.ts +55 -25
- package/template/src/app/api/contacts/import-preview/route.ts +1 -1
- package/template/src/app/api/contacts/origins/route.ts +63 -0
- package/template/src/app/api/contacts/route.ts +153 -41
- package/template/src/app/api/cron/cleanup-editor-images/route.ts +166 -0
- package/template/src/app/api/dashboard/widgets/[id]/route.ts +44 -0
- package/template/src/app/api/dashboard/widgets/route.ts +181 -0
- package/template/src/app/api/dev/reminders/test/route.ts +114 -0
- package/template/src/app/api/editor/upload-image/route.ts +61 -0
- package/template/src/app/api/integrations/google-sheet/jobs/[jobId]/route.ts +47 -0
- package/template/src/app/api/integrations/google-sheet/jobs/usage/route.ts +50 -0
- package/template/src/app/api/integrations/google-sheet/sync/route.ts +24 -556
- package/template/src/app/api/jobs/google-sheet/process/route.ts +84 -0
- package/template/src/app/api/jobs/google-sheet/schedule/route.ts +50 -0
- package/template/src/app/api/reminders/clear/route.ts +120 -0
- package/template/src/app/api/reminders/clear/undo/route.ts +112 -0
- package/template/src/app/api/reminders/route.ts +164 -39
- package/template/src/app/api/reminders/state/route.ts +164 -0
- package/template/src/app/api/reset-password/request/route.ts +1 -1
- package/template/src/app/api/reset-password/verify/route.ts +1 -1
- package/template/src/app/api/send/route.ts +16 -4
- package/template/src/app/api/settings/google-ads/route.ts +14 -0
- package/template/src/app/api/settings/google-calendar/calendars/route.ts +97 -0
- package/template/src/app/api/settings/google-calendar/route.ts +124 -0
- package/template/src/app/api/settings/google-sheet/[id]/route.ts +28 -0
- package/template/src/app/api/settings/google-sheet/auto-map/route.ts +37 -4
- package/template/src/app/api/settings/google-sheet/preview/route.ts +9 -3
- package/template/src/app/api/settings/google-sheet/route.ts +14 -0
- package/template/src/app/api/settings/integrations/logs/route.ts +93 -0
- package/template/src/app/api/settings/integrations/notifications/route.ts +67 -0
- package/template/src/app/api/settings/meta-leads/[id]/route.ts +0 -1
- package/template/src/app/api/settings/meta-leads/route.ts +14 -2
- package/template/src/app/api/settings/smtp/route.ts +53 -6
- package/template/src/app/api/tasks/[id]/attendees/route.ts +24 -8
- package/template/src/app/api/tasks/[id]/route.ts +234 -58
- package/template/src/app/api/tasks/meet/route.ts +27 -19
- package/template/src/app/api/tasks/route.ts +62 -17
- package/template/src/app/api/users/[id]/route.ts +20 -14
- package/template/src/app/api/users/list/route.ts +57 -19
- package/template/src/app/api/webhooks/google-ads/route.ts +34 -14
- package/template/src/app/api/webhooks/meta-leads/route.ts +32 -12
- package/template/src/app/api/workflows/[id]/route.ts +0 -4
- package/template/src/app/api/workflows/process/route.ts +22 -51
- package/template/src/app/api/workflows/route.ts +0 -4
- package/template/src/app/globals.css +342 -4
- package/template/src/app/layout.tsx +11 -3
- package/template/src/app/page.tsx +1 -1
- package/template/src/components/address-autocomplete.tsx +7 -6
- package/template/src/components/config-error-alert.tsx +46 -0
- package/template/src/components/contacts/filter-bar.tsx +12 -3
- package/template/src/components/contacts/filter-builder.tsx +28 -43
- package/template/src/components/contacts/save-view-dialog.tsx +1 -1
- package/template/src/components/contacts/views-tab-bar.tsx +15 -6
- package/template/src/components/dashboard/activity-chart.tsx +41 -28
- package/template/src/components/dashboard/add-widget-dialog.tsx +157 -0
- package/template/src/components/dashboard/color-picker.tsx +64 -0
- package/template/src/components/dashboard/contacts-chart.tsx +69 -0
- package/template/src/components/dashboard/interactions-by-type-chart.tsx +121 -0
- package/template/src/components/dashboard/recent-activity.tsx +154 -0
- package/template/src/components/dashboard/stat-card.tsx +40 -40
- package/template/src/components/dashboard/status-distribution-chart.tsx +81 -0
- package/template/src/components/dashboard/tasks-pie-chart.tsx +37 -34
- package/template/src/components/dashboard/top-contacts-list.tsx +113 -0
- package/template/src/components/dashboard/upcoming-tasks-list.tsx +72 -81
- package/template/src/components/dashboard/widget-wrapper.tsx +36 -0
- package/template/src/components/date-picker.tsx +9 -6
- package/template/src/components/editor/upload-editor-image.ts +42 -0
- package/template/src/components/editor.tsx +161 -22
- package/template/src/components/email-template.tsx +2 -2
- package/template/src/components/global-search.tsx +30 -28
- package/template/src/components/header.tsx +178 -80
- package/template/src/components/inactive-account-guard.tsx +58 -0
- package/template/src/components/integration-notifications-listener.tsx +12 -0
- package/template/src/components/invitation-email-template.tsx +2 -2
- package/template/src/components/meet-cancellation-email-template.tsx +3 -3
- package/template/src/components/meet-confirmation-email-template.tsx +3 -3
- package/template/src/components/meet-update-email-template.tsx +3 -3
- package/template/src/components/page-header.tsx +5 -5
- package/template/src/components/protected-page.tsx +1 -1
- package/template/src/components/reset-password-email-template.tsx +2 -2
- package/template/src/components/settings/integrations/GoogleAdsIntegration.tsx +428 -0
- package/template/src/components/settings/integrations/GoogleSheetConfigMonitoringModal.tsx +680 -0
- package/template/src/components/settings/integrations/GoogleSheetIntegration.tsx +809 -0
- package/template/src/components/settings/integrations/ImportResultDialog.tsx +124 -0
- package/template/src/components/settings/integrations/IntegrationLogPanel.tsx +57 -0
- package/template/src/components/settings/integrations/IntegrationLogsTable.tsx +186 -0
- package/template/src/components/settings/integrations/MetaLeadIntegration.tsx +451 -0
- package/template/src/components/sidebar.tsx +45 -26
- package/template/src/components/skeleton.tsx +40 -43
- package/template/src/components/ui/accordion.tsx +2 -2
- package/template/src/components/ui/alert-dialog.tsx +1 -1
- package/template/src/components/ui/button.tsx +20 -9
- package/template/src/components/ui/components.tsx +1 -1
- package/template/src/components/ui/date-picker.tsx +422 -0
- package/template/src/components/ui/datetime-picker.tsx +338 -0
- package/template/src/components/ui/status-select.tsx +271 -0
- package/template/src/components/ui/tooltip.tsx +37 -0
- package/template/src/components/view-as-modal.tsx +13 -7
- package/template/src/contexts/app-toast-context.tsx +245 -57
- package/template/src/contexts/dashboard-theme-context.tsx +53 -0
- package/template/src/contexts/sidebar-context.tsx +22 -17
- package/template/src/contexts/task-reminder-context.tsx +134 -160
- package/template/src/contexts/view-as-context.tsx +33 -6
- package/template/src/hooks/use-focus-trap.ts +2 -2
- package/template/src/hooks/useIntegrationNotifications.ts +49 -0
- package/template/src/lib/auth.ts +8 -1
- package/template/src/lib/config-links.ts +14 -0
- package/template/src/lib/contact-duplicate.ts +79 -61
- package/template/src/lib/contact-interactions.ts +21 -21
- package/template/src/lib/contact-view-filters.ts +24 -64
- package/template/src/lib/contacts-list-url.ts +190 -0
- package/template/src/lib/dashboard-stats.ts +65 -7
- package/template/src/lib/dashboard-themes.ts +135 -0
- package/template/src/lib/date-utils.ts +127 -0
- package/template/src/lib/default-widgets.ts +12 -0
- package/template/src/lib/editor-html-image-dimensions.ts +172 -0
- package/template/src/lib/editor-image-limits.ts +19 -0
- package/template/src/lib/email-html-sanitize.ts +19 -0
- package/template/src/lib/encryption.ts +9 -6
- package/template/src/lib/fr-geography.ts +192 -0
- package/template/src/lib/google-calendar-agenda.ts +201 -0
- package/template/src/lib/google-calendar.ts +255 -5
- package/template/src/lib/google-sheet-sync-jobs.ts +96 -0
- package/template/src/lib/google-sheet-sync-runner.ts +514 -0
- package/template/src/lib/integration-import-log.ts +21 -0
- package/template/src/lib/permissions.ts +40 -10
- package/template/src/lib/prisma.ts +4 -1
- package/template/src/lib/qstash.ts +65 -0
- package/template/src/lib/reminder-state-server.ts +80 -0
- package/template/src/lib/reminder-state.ts +29 -0
- package/template/src/lib/supabase-storage.ts +113 -0
- package/template/src/lib/template-variables.ts +164 -23
- package/template/src/lib/utils.ts +45 -0
- package/template/src/lib/widget-registry.ts +173 -0
- package/template/src/lib/workflow-executor.ts +16 -70
- package/template/src/proxy.ts +1 -0
- package/template/vercel.json +3 -10
- package/template/skills-lock.json +0 -25
- package/template/src/components/dashboard/dashboard-content.tsx +0 -79
- package/template/src/lib/google-drive.ts +0 -1101
- 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,
|
|
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
|
-
//
|
|
143
|
-
const [
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
|
441
|
+
// Charger les utilisateurs et les motifs de fermeture (admin uniquement)
|
|
534
442
|
useEffect(() => {
|
|
535
443
|
if (!isAdmin) return;
|
|
536
444
|
|
|
537
|
-
const
|
|
445
|
+
const fetchAdminData = async () => {
|
|
538
446
|
try {
|
|
539
|
-
|
|
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
|
-
|
|
459
|
+
setIntegrationUsers(usersData);
|
|
583
460
|
}
|
|
584
461
|
} catch (error) {
|
|
585
|
-
console.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
|
-
|
|
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
|
|
629
|
-
|
|
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
|
-
|
|
643
|
-
|
|
644
|
-
|
|
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
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
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
|
-
|
|
654
|
-
|
|
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
|
-
|
|
528
|
+
setGCalPrefsSaving(false);
|
|
657
529
|
}
|
|
658
530
|
};
|
|
659
531
|
|
|
660
|
-
const
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
559
|
+
console.error('Erreur lors de la déconnexion Google Calendar:', error);
|
|
692
560
|
} finally {
|
|
693
|
-
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
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
|
-
|
|
1149
|
-
|
|
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
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
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
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
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'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
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
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'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-
|
|
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
|
|
1674
|
-
<p className="mt-1 truncate text-sm
|
|
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
|
|
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
|
|
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
|
-
<
|
|
2188
|
-
|
|
2189
|
-
<div>
|
|
2190
|
-
<
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
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={() =>
|
|
2275
|
-
className="
|
|
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
|
-
|
|
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
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
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'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
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
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
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
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'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'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'application</span>{' '}
|
|
1806
|
+
dédié et le renseigner dans le champ "Mot de passe"
|
|
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'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="
|
|
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 : 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 : maximum{' '}
|
|
1861
|
+
{Math.round(SIGNATURE_MAX_IMAGE_BYTES / 1024)} 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
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
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-
|
|
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
|
-
|
|
2095
|
+
</div>
|
|
2587
2096
|
|
|
2588
|
-
|
|
2589
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2346
|
+
et Google Meet. Vous pouvez choisir un calendrier partagé à la création et
|
|
2347
|
+
afficher plusieurs agendas dans l'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
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
<
|
|
2951
|
-
|
|
2952
|
-
|
|
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
|
-
|
|
2959
|
-
|
|
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'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 "+ Ajouter" 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
|
-
|
|
3075
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3081
|
-
|
|
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
|
-
|
|
2408
|
+
Connecter
|
|
3084
2409
|
</button>
|
|
3085
|
-
|
|
3086
|
-
|
|
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 "+ Ajouter" 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 "+ Ajouter" 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
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
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
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
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'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
|
-
|
|
3583
|
-
</
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
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'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'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
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
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
|
-
|
|
3714
|
-
|
|
3715
|
-
<
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
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 "Téléphone" est obligatoire pour l'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'import
|
|
3828
|
-
</li>
|
|
3829
|
-
<li>
|
|
3830
|
-
• Laissez vide le nom de colonne Google Sheets pour ignorer ce mapping
|
|
3831
|
-
</li>
|
|
3832
|
-
<li>
|
|
3833
|
-
• Pour "Origine", utilisez le nom de la plateforme (ex:
|
|
3834
|
-
Facebook, Instagram, Google, LinkedIn, Site Web)
|
|
3835
|
-
</li>
|
|
3836
|
-
<li>
|
|
3837
|
-
• Les colonnes marquées "Ajouter comme note" seront regroupées
|
|
3838
|
-
dans une seule note avec le format "Question : Réponse"
|
|
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
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
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'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'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
|
);
|