create-crm-tmp 1.0.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 +93 -0
- package/package.json +25 -0
- package/template/.prettierignore +33 -0
- package/template/.prettierrc.json +25 -0
- package/template/README.md +173 -0
- package/template/eslint.config.mjs +18 -0
- package/template/exemple-contacts.csv +11 -0
- package/template/next.config.ts +8 -0
- package/template/package.json +64 -0
- package/template/postcss.config.mjs +7 -0
- package/template/prisma/migrations/20251126144728_init/migration.sql +78 -0
- package/template/prisma/migrations/20251126155204_add_user_roles/migration.sql +5 -0
- package/template/prisma/migrations/20251128095126_add_company_info/migration.sql +19 -0
- package/template/prisma/migrations/20251128123321_add_smtp_config/migration.sql +22 -0
- package/template/prisma/migrations/20251128132303_add_status/migration.sql +23 -0
- package/template/prisma/migrations/20251201102207_add_user_active/migration.sql +75 -0
- package/template/prisma/migrations/20251201105507_add_email_signature/migration.sql +2 -0
- package/template/prisma/migrations/20251201151122_add_tasks/migration.sql +45 -0
- package/template/prisma/migrations/20251202111854_add_task_reminder/migration.sql +2 -0
- package/template/prisma/migrations/20251202135859_add_google_meet_integration/migration.sql +27 -0
- package/template/prisma/migrations/20251203103317_add_meta_lead_integration/migration.sql +20 -0
- package/template/prisma/migrations/20251203104002_add_google_ads_integration/migration.sql +18 -0
- package/template/prisma/migrations/20251203112122_add_google_sheet_integration/migration.sql +32 -0
- package/template/prisma/migrations/20251203153853_allow_multiple_integration_configs/migration.sql +20 -0
- package/template/prisma/migrations/20251205141705_update_user_roles/migration.sql +12 -0
- package/template/prisma/migrations/20251205150000_add_commercial_and_telepro_assignment/migration.sql +21 -0
- package/template/prisma/migrations/20251205160000_add_interaction_logging/migration.sql +11 -0
- package/template/prisma/migrations/20251208090314_add_automatic_interaction_types/migration.sql +12 -0
- package/template/prisma/migrations/20251208094843_mg/migration.sql +14 -0
- package/template/prisma/migrations/20251208100000_add_company_support/migration.sql +14 -0
- package/template/prisma/migrations/20251208110000_add_templates/migration.sql +26 -0
- package/template/prisma/migrations/20251208141304_add_video_conference_task_type/migration.sql +2 -0
- package/template/prisma/migrations/20251209104759_add_internal_note_to_task/migration.sql +2 -0
- package/template/prisma/migrations/20251209134803_add_company_field/migration.sql +2 -0
- package/template/prisma/migrations/20251209150000_rename_company_to_company_name/migration.sql +3 -0
- package/template/prisma/migrations/20251209150016_add_email_tracking/migration.sql +21 -0
- package/template/prisma/migrations/20251209155908_add_notify_contact_to_task/migration.sql +2 -0
- package/template/prisma/migrations/20251210110019_add_appointment_types/migration.sql +10 -0
- package/template/prisma/migrations/20251210113928_add_contact_files/migration.sql +26 -0
- package/template/prisma/migrations/20251212132339_add_custom_roles/migration.sql +24 -0
- package/template/prisma/migrations/20251215104448_add_file_interaction_types/migration.sql +11 -0
- package/template/prisma/migrations/20251215145616_add_closing_reasons/migration.sql +12 -0
- package/template/prisma/migrations/20251216140850_add_log_users/migration.sql +25 -0
- package/template/prisma/migrations/20251216151000_rename_perdu_to_ferme/migration.sql +8 -0
- package/template/prisma/migrations/20251216162318_add_column_mappings_to_google_sheet/migration.sql +2 -0
- package/template/prisma/migrations/20251216185127_add_workflows/migration.sql +80 -0
- package/template/prisma/migrations/20251216192237_add_scheduled_workflow_actions/migration.sql +32 -0
- package/template/prisma/migrations/migration_lock.toml +3 -0
- package/template/prisma/schema.prisma +582 -0
- package/template/prisma.config.ts +14 -0
- package/template/src/app/(auth)/invite/[token]/page.tsx +200 -0
- package/template/src/app/(auth)/layout.tsx +3 -0
- package/template/src/app/(auth)/reset-password/complete/page.tsx +213 -0
- package/template/src/app/(auth)/reset-password/page.tsx +146 -0
- package/template/src/app/(auth)/reset-password/verify/page.tsx +183 -0
- package/template/src/app/(auth)/signin/page.tsx +166 -0
- package/template/src/app/(dashboard)/agenda/page.tsx +3051 -0
- package/template/src/app/(dashboard)/automatisation/[id]/page.tsx +24 -0
- package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +905 -0
- package/template/src/app/(dashboard)/automatisation/new/page.tsx +20 -0
- package/template/src/app/(dashboard)/automatisation/page.tsx +337 -0
- package/template/src/app/(dashboard)/closing/page.tsx +1052 -0
- package/template/src/app/(dashboard)/contacts/[id]/page.tsx +6028 -0
- package/template/src/app/(dashboard)/contacts/page.tsx +3713 -0
- package/template/src/app/(dashboard)/dashboard/page.tsx +186 -0
- package/template/src/app/(dashboard)/layout.tsx +30 -0
- package/template/src/app/(dashboard)/settings/page.tsx +4070 -0
- package/template/src/app/(dashboard)/templates/page.tsx +567 -0
- package/template/src/app/(dashboard)/users/list/page.tsx +507 -0
- package/template/src/app/(dashboard)/users/page.tsx +457 -0
- package/template/src/app/(dashboard)/users/permissions/page.tsx +181 -0
- package/template/src/app/(dashboard)/users/roles/page.tsx +434 -0
- package/template/src/app/api/audit-logs/route.ts +57 -0
- package/template/src/app/api/auth/[...all]/route.ts +4 -0
- package/template/src/app/api/auth/check-active/route.ts +31 -0
- package/template/src/app/api/auth/google/callback/route.ts +94 -0
- package/template/src/app/api/auth/google/disconnect/route.ts +32 -0
- package/template/src/app/api/auth/google/route.ts +34 -0
- package/template/src/app/api/auth/google/status/route.ts +32 -0
- package/template/src/app/api/closing-reasons/route.ts +27 -0
- package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +94 -0
- package/template/src/app/api/contacts/[id]/files/route.ts +269 -0
- package/template/src/app/api/contacts/[id]/interactions/[interactionId]/route.ts +91 -0
- package/template/src/app/api/contacts/[id]/interactions/route.ts +103 -0
- package/template/src/app/api/contacts/[id]/meet/route.ts +296 -0
- package/template/src/app/api/contacts/[id]/route.ts +322 -0
- package/template/src/app/api/contacts/[id]/send-email/route.ts +254 -0
- package/template/src/app/api/contacts/export/route.ts +270 -0
- package/template/src/app/api/contacts/import/route.ts +381 -0
- package/template/src/app/api/contacts/route.ts +283 -0
- package/template/src/app/api/dashboard/stats/route.ts +299 -0
- package/template/src/app/api/email/track/[id]/route.ts +68 -0
- package/template/src/app/api/integrations/google-sheet/sync/route.ts +526 -0
- package/template/src/app/api/invite/complete/route.ts +88 -0
- package/template/src/app/api/invite/validate/route.ts +55 -0
- package/template/src/app/api/reminders/route.ts +95 -0
- package/template/src/app/api/reset-password/complete/route.ts +73 -0
- package/template/src/app/api/reset-password/request/route.ts +84 -0
- package/template/src/app/api/reset-password/validate/route.ts +49 -0
- package/template/src/app/api/reset-password/verify/route.ts +74 -0
- package/template/src/app/api/roles/[id]/route.ts +183 -0
- package/template/src/app/api/roles/route.ts +140 -0
- package/template/src/app/api/send/route.ts +282 -0
- package/template/src/app/api/settings/change-password/route.ts +95 -0
- package/template/src/app/api/settings/closing-reasons/[id]/route.ts +84 -0
- package/template/src/app/api/settings/closing-reasons/route.ts +74 -0
- package/template/src/app/api/settings/company/route.ts +121 -0
- package/template/src/app/api/settings/google-ads/[id]/route.ts +117 -0
- package/template/src/app/api/settings/google-ads/route.ts +122 -0
- package/template/src/app/api/settings/google-sheet/[id]/route.ts +230 -0
- package/template/src/app/api/settings/google-sheet/auto-map/route.ts +196 -0
- package/template/src/app/api/settings/google-sheet/route.ts +254 -0
- package/template/src/app/api/settings/meta-leads/[id]/route.ts +123 -0
- package/template/src/app/api/settings/meta-leads/route.ts +132 -0
- package/template/src/app/api/settings/profile/route.ts +42 -0
- package/template/src/app/api/settings/smtp/route.ts +130 -0
- package/template/src/app/api/settings/smtp/test/route.ts +121 -0
- package/template/src/app/api/settings/statuses/[id]/route.ts +101 -0
- package/template/src/app/api/settings/statuses/route.ts +83 -0
- package/template/src/app/api/statuses/route.ts +25 -0
- package/template/src/app/api/tasks/[id]/attendees/route.ts +76 -0
- package/template/src/app/api/tasks/[id]/route.ts +728 -0
- package/template/src/app/api/tasks/meet/route.ts +240 -0
- package/template/src/app/api/tasks/route.ts +417 -0
- package/template/src/app/api/templates/[id]/route.ts +140 -0
- package/template/src/app/api/templates/route.ts +91 -0
- package/template/src/app/api/users/[id]/route.ts +168 -0
- package/template/src/app/api/users/list/route.ts +45 -0
- package/template/src/app/api/users/me/route.ts +48 -0
- package/template/src/app/api/users/route.ts +250 -0
- package/template/src/app/api/webhooks/google-ads/route.ts +208 -0
- package/template/src/app/api/webhooks/meta-leads/route.ts +258 -0
- package/template/src/app/api/workflows/[id]/route.ts +192 -0
- package/template/src/app/api/workflows/process/route.ts +293 -0
- package/template/src/app/api/workflows/route.ts +124 -0
- package/template/src/app/favicon.ico +0 -0
- package/template/src/app/globals.css +1416 -0
- package/template/src/app/layout.tsx +31 -0
- package/template/src/app/page.tsx +32 -0
- package/template/src/components/dashboard/activity-chart.tsx +67 -0
- package/template/src/components/dashboard/contacts-chart.tsx +63 -0
- package/template/src/components/dashboard/recent-activity.tsx +164 -0
- package/template/src/components/dashboard/sales-analytics-chart.tsx +81 -0
- package/template/src/components/dashboard/stat-card.tsx +61 -0
- package/template/src/components/dashboard/status-distribution-chart.tsx +45 -0
- package/template/src/components/dashboard/tasks-pie-chart.tsx +88 -0
- package/template/src/components/dashboard/top-contacts-list.tsx +129 -0
- package/template/src/components/dashboard/upcoming-tasks-list.tsx +126 -0
- package/template/src/components/editor.tsx +856 -0
- package/template/src/components/email-template.tsx +35 -0
- package/template/src/components/header.tsx +320 -0
- package/template/src/components/invitation-email-template.tsx +79 -0
- package/template/src/components/meet-cancellation-email-template.tsx +120 -0
- package/template/src/components/meet-confirmation-email-template.tsx +156 -0
- package/template/src/components/meet-update-email-template.tsx +209 -0
- package/template/src/components/page-header.tsx +61 -0
- package/template/src/components/reset-password-email-template.tsx +79 -0
- package/template/src/components/sidebar.tsx +294 -0
- package/template/src/components/skeleton.tsx +380 -0
- package/template/src/components/ui/commands.tsx +396 -0
- package/template/src/components/ui/components.tsx +150 -0
- package/template/src/components/ui/theme.tsx +5 -0
- package/template/src/components/view-as-banner.tsx +45 -0
- package/template/src/components/view-as-modal.tsx +186 -0
- package/template/src/contexts/mobile-menu-context.tsx +31 -0
- package/template/src/contexts/sidebar-context.tsx +107 -0
- package/template/src/contexts/task-reminder-context.tsx +239 -0
- package/template/src/contexts/view-as-context.tsx +84 -0
- package/template/src/hooks/use-user-role.ts +82 -0
- package/template/src/lib/audit-log.ts +45 -0
- package/template/src/lib/auth-client.ts +16 -0
- package/template/src/lib/auth.ts +35 -0
- package/template/src/lib/check-permission.ts +193 -0
- package/template/src/lib/contact-duplicate.ts +112 -0
- package/template/src/lib/contact-interactions.ts +371 -0
- package/template/src/lib/encryption.ts +99 -0
- package/template/src/lib/google-calendar.ts +300 -0
- package/template/src/lib/google-drive.ts +372 -0
- package/template/src/lib/permissions.ts +412 -0
- package/template/src/lib/prisma.ts +32 -0
- package/template/src/lib/roles.ts +120 -0
- package/template/src/lib/template-variables.ts +76 -0
- package/template/src/lib/utils.ts +46 -0
- package/template/src/lib/workflow-executor.ts +482 -0
- package/template/src/proxy.ts +91 -0
- package/template/tsconfig.json +34 -0
- package/template/vercel.json +8 -0
|
@@ -0,0 +1,4070 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useRef } from 'react';
|
|
4
|
+
import { useRouter } from 'next/navigation';
|
|
5
|
+
import { useSession } from '@/lib/auth-client';
|
|
6
|
+
import { useUserRole } from '@/hooks/use-user-role';
|
|
7
|
+
import { PageHeader } from '@/components/page-header';
|
|
8
|
+
import { cn } from '@/lib/utils';
|
|
9
|
+
import Link from 'next/link';
|
|
10
|
+
import { Editor, type DefaultTemplateRef } from '@/components/editor';
|
|
11
|
+
import {
|
|
12
|
+
Eye,
|
|
13
|
+
EyeOff,
|
|
14
|
+
Plus,
|
|
15
|
+
Trash2,
|
|
16
|
+
ArrowRight,
|
|
17
|
+
Settings,
|
|
18
|
+
Globe,
|
|
19
|
+
Grid3x3,
|
|
20
|
+
Monitor,
|
|
21
|
+
DollarSign,
|
|
22
|
+
Wrench,
|
|
23
|
+
User,
|
|
24
|
+
Lock,
|
|
25
|
+
Bell,
|
|
26
|
+
Plug,
|
|
27
|
+
RefreshCw,
|
|
28
|
+
} from 'lucide-react';
|
|
29
|
+
|
|
30
|
+
// Fonction utilitaire pour convertir un index en lettre de colonne (A, B, C, ...)
|
|
31
|
+
function indexToColumn(index: number): string {
|
|
32
|
+
let col = '';
|
|
33
|
+
let n = index + 1;
|
|
34
|
+
while (n > 0) {
|
|
35
|
+
const remainder = (n - 1) % 26;
|
|
36
|
+
col = String.fromCharCode(65 + remainder) + col;
|
|
37
|
+
n = Math.floor((n - 1) / 26);
|
|
38
|
+
}
|
|
39
|
+
return col;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export default function SettingsPage() {
|
|
43
|
+
const router = useRouter();
|
|
44
|
+
const { data: session } = useSession();
|
|
45
|
+
const { isAdmin } = useUserRole();
|
|
46
|
+
const [showPasswordForm, setShowPasswordForm] = useState(false);
|
|
47
|
+
const [passwordData, setPasswordData] = useState({
|
|
48
|
+
currentPassword: '',
|
|
49
|
+
newPassword: '',
|
|
50
|
+
confirmPassword: '',
|
|
51
|
+
});
|
|
52
|
+
const [passwordError, setPasswordError] = useState('');
|
|
53
|
+
const [passwordSuccess, setPasswordSuccess] = useState('');
|
|
54
|
+
const [passwordLoading, setPasswordLoading] = useState(false);
|
|
55
|
+
|
|
56
|
+
// État pour la modification du nom
|
|
57
|
+
const [showNameForm, setShowNameForm] = useState(false);
|
|
58
|
+
const [nameValue, setNameValue] = useState(session?.user?.name || '');
|
|
59
|
+
const [nameError, setNameError] = useState('');
|
|
60
|
+
const [nameSuccess, setNameSuccess] = useState('');
|
|
61
|
+
const [nameLoading, setNameLoading] = useState(false);
|
|
62
|
+
|
|
63
|
+
// État pour les informations de l'entreprise
|
|
64
|
+
const [companyData, setCompanyData] = useState({
|
|
65
|
+
name: '',
|
|
66
|
+
address: '',
|
|
67
|
+
city: '',
|
|
68
|
+
postalCode: '',
|
|
69
|
+
country: '',
|
|
70
|
+
phone: '',
|
|
71
|
+
email: '',
|
|
72
|
+
website: '',
|
|
73
|
+
siret: '',
|
|
74
|
+
vatNumber: '',
|
|
75
|
+
logo: '',
|
|
76
|
+
});
|
|
77
|
+
const [companyLoading, setCompanyLoading] = useState(true);
|
|
78
|
+
const [companySaving, setCompanySaving] = useState(false);
|
|
79
|
+
const [companyError, setCompanyError] = useState('');
|
|
80
|
+
const [companySuccess, setCompanySuccess] = useState('');
|
|
81
|
+
|
|
82
|
+
// État pour la configuration SMTP
|
|
83
|
+
const [smtpData, setSmtpData] = useState({
|
|
84
|
+
host: '',
|
|
85
|
+
port: '587',
|
|
86
|
+
secure: false,
|
|
87
|
+
username: '',
|
|
88
|
+
password: '',
|
|
89
|
+
fromEmail: '',
|
|
90
|
+
fromName: '',
|
|
91
|
+
signature: '',
|
|
92
|
+
});
|
|
93
|
+
const [smtpLoading, setSmtpLoading] = useState(true);
|
|
94
|
+
const [smtpSaving, setSmtpSaving] = useState(false);
|
|
95
|
+
const [smtpError, setSmtpError] = useState('');
|
|
96
|
+
const [smtpSuccess, setSmtpSuccess] = useState('');
|
|
97
|
+
const [smtpTesting, setSmtpTesting] = useState(false);
|
|
98
|
+
const [showSmtpPassword, setShowSmtpPassword] = useState(false);
|
|
99
|
+
const [smtpTestResult, setSmtpTestResult] = useState<{
|
|
100
|
+
success: boolean;
|
|
101
|
+
message: string;
|
|
102
|
+
} | null>(null);
|
|
103
|
+
const [smtpConfigured, setSmtpConfigured] = useState(false);
|
|
104
|
+
const smtpSignatureEditorRef = useRef<DefaultTemplateRef | null>(null);
|
|
105
|
+
|
|
106
|
+
// État pour la gestion des statuts
|
|
107
|
+
interface Status {
|
|
108
|
+
id: string;
|
|
109
|
+
name: string;
|
|
110
|
+
color: string;
|
|
111
|
+
order: number;
|
|
112
|
+
}
|
|
113
|
+
const [statuses, setStatuses] = useState<Status[]>([]);
|
|
114
|
+
const [statusesLoading, setStatusesLoading] = useState(true);
|
|
115
|
+
const [showStatusForm, setShowStatusForm] = useState(false);
|
|
116
|
+
const [editingStatus, setEditingStatus] = useState<Status | null>(null);
|
|
117
|
+
const [statusFormData, setStatusFormData] = useState({
|
|
118
|
+
name: '',
|
|
119
|
+
color: '#3B82F6',
|
|
120
|
+
});
|
|
121
|
+
const [statusError, setStatusError] = useState('');
|
|
122
|
+
const [statusSuccess, setStatusSuccess] = useState('');
|
|
123
|
+
const [statusSaving, setStatusSaving] = useState(false);
|
|
124
|
+
|
|
125
|
+
// État pour Google Calendar
|
|
126
|
+
const [googleAccount, setGoogleAccount] = useState<{
|
|
127
|
+
email: string | null;
|
|
128
|
+
connected: boolean;
|
|
129
|
+
} | null>(null);
|
|
130
|
+
const [googleLoading, setGoogleLoading] = useState(true);
|
|
131
|
+
const [googleDisconnecting, setGoogleDisconnecting] = useState(false);
|
|
132
|
+
|
|
133
|
+
// État pour l'intégration Meta Lead Ads (admin uniquement)
|
|
134
|
+
const [metaLeadLoading, setMetaLeadLoading] = useState(true);
|
|
135
|
+
const [metaLeadSaving, setMetaLeadSaving] = useState(false);
|
|
136
|
+
const [metaLeadError, setMetaLeadError] = useState('');
|
|
137
|
+
const [metaLeadSuccess, setMetaLeadSuccess] = useState('');
|
|
138
|
+
const [metaLeadUsers, setMetaLeadUsers] = useState<{ id: string; name: string; email: string }[]>(
|
|
139
|
+
[],
|
|
140
|
+
);
|
|
141
|
+
const [metaLeadConfigs, setMetaLeadConfigs] = useState<
|
|
142
|
+
Array<{
|
|
143
|
+
id: string;
|
|
144
|
+
name: string;
|
|
145
|
+
active: boolean;
|
|
146
|
+
pageId: string;
|
|
147
|
+
verifyToken: string;
|
|
148
|
+
defaultStatusId: string | null;
|
|
149
|
+
defaultAssignedUserId: string | null;
|
|
150
|
+
}>
|
|
151
|
+
>([]);
|
|
152
|
+
const [showMetaLeadModal, setShowMetaLeadModal] = useState(false);
|
|
153
|
+
const [editingMetaLeadConfig, setEditingMetaLeadConfig] = useState<string | null>(null);
|
|
154
|
+
const [metaLeadFormData, setMetaLeadFormData] = useState<{
|
|
155
|
+
name: string;
|
|
156
|
+
active: boolean;
|
|
157
|
+
pageId: string;
|
|
158
|
+
accessToken: string;
|
|
159
|
+
verifyToken: string;
|
|
160
|
+
defaultStatusId: string | null;
|
|
161
|
+
defaultAssignedUserId: string | null;
|
|
162
|
+
}>({
|
|
163
|
+
name: '',
|
|
164
|
+
active: true,
|
|
165
|
+
pageId: '',
|
|
166
|
+
accessToken: '',
|
|
167
|
+
verifyToken: '',
|
|
168
|
+
defaultStatusId: null,
|
|
169
|
+
defaultAssignedUserId: null,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// État pour l'intégration Google Ads Lead Forms (admin uniquement)
|
|
173
|
+
const [googleAdsLoading, setGoogleAdsLoading] = useState(true);
|
|
174
|
+
const [googleAdsSaving, setGoogleAdsSaving] = useState(false);
|
|
175
|
+
const [googleAdsError, setGoogleAdsError] = useState('');
|
|
176
|
+
const [googleAdsSuccess, setGoogleAdsSuccess] = useState('');
|
|
177
|
+
const [googleAdsConfigs, setGoogleAdsConfigs] = useState<
|
|
178
|
+
Array<{
|
|
179
|
+
id: string;
|
|
180
|
+
name: string;
|
|
181
|
+
active: boolean;
|
|
182
|
+
webhookKey: string;
|
|
183
|
+
defaultStatusId: string | null;
|
|
184
|
+
defaultAssignedUserId: string | null;
|
|
185
|
+
}>
|
|
186
|
+
>([]);
|
|
187
|
+
const [showGoogleAdsModal, setShowGoogleAdsModal] = useState(false);
|
|
188
|
+
const [editingGoogleAdsConfig, setEditingGoogleAdsConfig] = useState<string | null>(null);
|
|
189
|
+
const [googleAdsFormData, setGoogleAdsFormData] = useState<{
|
|
190
|
+
name: string;
|
|
191
|
+
active: boolean;
|
|
192
|
+
webhookKey: string;
|
|
193
|
+
defaultStatusId: string | null;
|
|
194
|
+
defaultAssignedUserId: string | null;
|
|
195
|
+
}>({
|
|
196
|
+
name: '',
|
|
197
|
+
active: true,
|
|
198
|
+
webhookKey: '',
|
|
199
|
+
defaultStatusId: null,
|
|
200
|
+
defaultAssignedUserId: null,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// État pour l'intégration Google Sheets (admin uniquement)
|
|
204
|
+
const [googleSheetLoading, setGoogleSheetLoading] = useState(true);
|
|
205
|
+
const [googleSheetSaving, setGoogleSheetSaving] = useState(false);
|
|
206
|
+
const [googleSheetSyncing, setGoogleSheetSyncing] = useState(false);
|
|
207
|
+
const [googleSheetError, setGoogleSheetError] = useState('');
|
|
208
|
+
const [googleSheetSuccess, setGoogleSheetSuccess] = useState('');
|
|
209
|
+
// Motifs de fermeture (ClosingReasons)
|
|
210
|
+
const [closingReasons, setClosingReasons] = useState<Array<{ id: string; name: string }>>([]);
|
|
211
|
+
const [closingReasonsLoading, setClosingReasonsLoading] = useState(false);
|
|
212
|
+
const [showClosingReasonForm, setShowClosingReasonForm] = useState(false);
|
|
213
|
+
const [closingReasonFormData, setClosingReasonFormData] = useState<{
|
|
214
|
+
id: string | null;
|
|
215
|
+
name: string;
|
|
216
|
+
}>({
|
|
217
|
+
id: null,
|
|
218
|
+
name: '',
|
|
219
|
+
});
|
|
220
|
+
const [closingReasonError, setClosingReasonError] = useState('');
|
|
221
|
+
const [closingReasonSuccess, setClosingReasonSuccess] = useState('');
|
|
222
|
+
const [googleSheetConfigs, setGoogleSheetConfigs] = useState<
|
|
223
|
+
Array<{
|
|
224
|
+
id: string;
|
|
225
|
+
name: string;
|
|
226
|
+
active: boolean;
|
|
227
|
+
spreadsheetId: string;
|
|
228
|
+
sheetName: string;
|
|
229
|
+
headerRow: number;
|
|
230
|
+
phoneColumn: string;
|
|
231
|
+
firstNameColumn: string | null;
|
|
232
|
+
lastNameColumn: string | null;
|
|
233
|
+
emailColumn: string | null;
|
|
234
|
+
cityColumn: string | null;
|
|
235
|
+
postalCodeColumn: string | null;
|
|
236
|
+
originColumn: string | null;
|
|
237
|
+
defaultStatusId: string | null;
|
|
238
|
+
defaultAssignedUserId: string | null;
|
|
239
|
+
}>
|
|
240
|
+
>([]);
|
|
241
|
+
const [showGoogleSheetModal, setShowGoogleSheetModal] = useState(false);
|
|
242
|
+
const [editingGoogleSheetConfig, setEditingGoogleSheetConfig] = useState<string | null>(null);
|
|
243
|
+
const [googleSheetStep, setGoogleSheetStep] = useState<1 | 2>(1);
|
|
244
|
+
const [googleSheetFormData, setGoogleSheetFormData] = useState<{
|
|
245
|
+
name: string;
|
|
246
|
+
active: boolean;
|
|
247
|
+
sheetUrl: string;
|
|
248
|
+
sheetName: string;
|
|
249
|
+
headerRow: string;
|
|
250
|
+
defaultStatusId: string | null;
|
|
251
|
+
defaultAssignedUserId: string | null;
|
|
252
|
+
}>({
|
|
253
|
+
name: '',
|
|
254
|
+
active: true,
|
|
255
|
+
sheetUrl: '',
|
|
256
|
+
sheetName: '',
|
|
257
|
+
headerRow: '1',
|
|
258
|
+
defaultStatusId: null,
|
|
259
|
+
defaultAssignedUserId: null,
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// Structure pour les mappings de colonnes
|
|
263
|
+
interface ColumnMapping {
|
|
264
|
+
id: string;
|
|
265
|
+
columnName: string; // Nom de la colonne dans Google Sheets
|
|
266
|
+
action: 'map' | 'note' | 'ignore';
|
|
267
|
+
crmField?: string; // Champ CRM si action = 'map'
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const [googleSheetMappings, setGoogleSheetMappings] = useState<ColumnMapping[]>([]);
|
|
271
|
+
const [googleSheetPreview, setGoogleSheetPreview] = useState<Array<Record<string, string>>>([]);
|
|
272
|
+
const [googleSheetHeaders, setGoogleSheetHeaders] = useState<string[]>([]);
|
|
273
|
+
|
|
274
|
+
// États pour la navigation
|
|
275
|
+
type MainSection = 'general' | 'app' | 'system' | 'integrations';
|
|
276
|
+
type GeneralSubSection = 'profile' | 'security' | 'company';
|
|
277
|
+
const [mainSection, setMainSection] = useState<MainSection>('general');
|
|
278
|
+
const [generalSubSection, setGeneralSubSection] = useState<GeneralSubSection>('profile');
|
|
279
|
+
|
|
280
|
+
// Mettre à jour le nom quand la session change
|
|
281
|
+
useEffect(() => {
|
|
282
|
+
if (session?.user?.name) {
|
|
283
|
+
setNameValue(session.user.name);
|
|
284
|
+
}
|
|
285
|
+
}, [session?.user?.name]);
|
|
286
|
+
|
|
287
|
+
// Charger les informations de l'entreprise au montage (si admin)
|
|
288
|
+
useEffect(() => {
|
|
289
|
+
if (isAdmin) {
|
|
290
|
+
const fetchCompanyData = async () => {
|
|
291
|
+
try {
|
|
292
|
+
setCompanyLoading(true);
|
|
293
|
+
const response = await fetch('/api/settings/company');
|
|
294
|
+
if (response.ok) {
|
|
295
|
+
const data = await response.json();
|
|
296
|
+
setCompanyData({
|
|
297
|
+
name: data.name || '',
|
|
298
|
+
address: data.address || '',
|
|
299
|
+
city: data.city || '',
|
|
300
|
+
postalCode: data.postalCode || '',
|
|
301
|
+
country: data.country || '',
|
|
302
|
+
phone: data.phone || '',
|
|
303
|
+
email: data.email || '',
|
|
304
|
+
website: data.website || '',
|
|
305
|
+
siret: data.siret || '',
|
|
306
|
+
vatNumber: data.vatNumber || '',
|
|
307
|
+
logo: data.logo || '',
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
} catch (error) {
|
|
311
|
+
console.error("Erreur lors du chargement des informations de l'entreprise:", error);
|
|
312
|
+
} finally {
|
|
313
|
+
setCompanyLoading(false);
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
fetchCompanyData();
|
|
317
|
+
}
|
|
318
|
+
}, [isAdmin]);
|
|
319
|
+
|
|
320
|
+
// Charger la configuration SMTP au montage
|
|
321
|
+
useEffect(() => {
|
|
322
|
+
const fetchSmtpData = async () => {
|
|
323
|
+
try {
|
|
324
|
+
setSmtpLoading(true);
|
|
325
|
+
const response = await fetch('/api/settings/smtp');
|
|
326
|
+
if (response.ok) {
|
|
327
|
+
const data = await response.json();
|
|
328
|
+
if (data) {
|
|
329
|
+
setSmtpData({
|
|
330
|
+
host: data.host || '',
|
|
331
|
+
port: data.port?.toString() || '587',
|
|
332
|
+
secure: data.secure || false,
|
|
333
|
+
username: data.username || '',
|
|
334
|
+
password: '', // Ne pas charger le mot de passe
|
|
335
|
+
fromEmail: data.fromEmail || '',
|
|
336
|
+
fromName: data.fromName || '',
|
|
337
|
+
signature: data.signature || '',
|
|
338
|
+
});
|
|
339
|
+
setSmtpConfigured(true);
|
|
340
|
+
// Injecter la signature existante dans l'éditeur si disponible
|
|
341
|
+
if (smtpSignatureEditorRef.current && data.signature) {
|
|
342
|
+
smtpSignatureEditorRef.current.injectHTML(data.signature);
|
|
343
|
+
}
|
|
344
|
+
} else {
|
|
345
|
+
setSmtpConfigured(false);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
} catch (error) {
|
|
349
|
+
console.error('Erreur lors du chargement de la config SMTP:', error);
|
|
350
|
+
setSmtpConfigured(false);
|
|
351
|
+
} finally {
|
|
352
|
+
setSmtpLoading(false);
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
fetchSmtpData();
|
|
356
|
+
}, []);
|
|
357
|
+
|
|
358
|
+
// Synchroniser l'éditeur de signature quand la signature change dans le state
|
|
359
|
+
useEffect(() => {
|
|
360
|
+
if (smtpSignatureEditorRef.current && smtpData.signature) {
|
|
361
|
+
smtpSignatureEditorRef.current.injectHTML(smtpData.signature);
|
|
362
|
+
}
|
|
363
|
+
}, [smtpData.signature]);
|
|
364
|
+
|
|
365
|
+
// Charger les statuts au montage (si admin)
|
|
366
|
+
useEffect(() => {
|
|
367
|
+
if (isAdmin) {
|
|
368
|
+
const fetchStatuses = async () => {
|
|
369
|
+
try {
|
|
370
|
+
setStatusesLoading(true);
|
|
371
|
+
const response = await fetch('/api/settings/statuses');
|
|
372
|
+
if (response.ok) {
|
|
373
|
+
const data = await response.json();
|
|
374
|
+
setStatuses(data);
|
|
375
|
+
}
|
|
376
|
+
} catch (error) {
|
|
377
|
+
console.error('Erreur lors du chargement des statuts:', error);
|
|
378
|
+
} finally {
|
|
379
|
+
setStatusesLoading(false);
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
fetchStatuses();
|
|
383
|
+
}
|
|
384
|
+
}, [isAdmin]);
|
|
385
|
+
|
|
386
|
+
// Charger le statut de connexion Google
|
|
387
|
+
useEffect(() => {
|
|
388
|
+
const fetchGoogleAccount = async () => {
|
|
389
|
+
try {
|
|
390
|
+
setGoogleLoading(true);
|
|
391
|
+
const response = await fetch('/api/auth/google/status');
|
|
392
|
+
if (response.ok) {
|
|
393
|
+
const data = await response.json();
|
|
394
|
+
setGoogleAccount(data);
|
|
395
|
+
} else {
|
|
396
|
+
setGoogleAccount({ email: null, connected: false });
|
|
397
|
+
}
|
|
398
|
+
} catch (error) {
|
|
399
|
+
console.error('Erreur lors du chargement du compte Google:', error);
|
|
400
|
+
setGoogleAccount({ email: null, connected: false });
|
|
401
|
+
} finally {
|
|
402
|
+
setGoogleLoading(false);
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
fetchGoogleAccount();
|
|
406
|
+
}, []);
|
|
407
|
+
|
|
408
|
+
// Charger la configuration Meta Lead Ads et la liste des utilisateurs (admin uniquement)
|
|
409
|
+
useEffect(() => {
|
|
410
|
+
if (!isAdmin) return;
|
|
411
|
+
|
|
412
|
+
const fetchLeadConfigs = async () => {
|
|
413
|
+
try {
|
|
414
|
+
setMetaLeadLoading(true);
|
|
415
|
+
setGoogleAdsLoading(true);
|
|
416
|
+
setGoogleSheetLoading(true);
|
|
417
|
+
setMetaLeadError('');
|
|
418
|
+
setGoogleAdsError('');
|
|
419
|
+
setGoogleSheetError('');
|
|
420
|
+
|
|
421
|
+
const [
|
|
422
|
+
metaConfigRes,
|
|
423
|
+
googleAdsConfigRes,
|
|
424
|
+
googleSheetConfigRes,
|
|
425
|
+
closingReasonsRes,
|
|
426
|
+
usersRes,
|
|
427
|
+
] = await Promise.all([
|
|
428
|
+
fetch('/api/settings/meta-leads'),
|
|
429
|
+
fetch('/api/settings/google-ads'),
|
|
430
|
+
fetch('/api/settings/google-sheet'),
|
|
431
|
+
fetch('/api/settings/closing-reasons'),
|
|
432
|
+
fetch('/api/users/list'),
|
|
433
|
+
]);
|
|
434
|
+
|
|
435
|
+
if (metaConfigRes.ok) {
|
|
436
|
+
const configsData = await metaConfigRes.json();
|
|
437
|
+
setMetaLeadConfigs(Array.isArray(configsData) ? configsData : []);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (googleAdsConfigRes.ok) {
|
|
441
|
+
const configsData = await googleAdsConfigRes.json();
|
|
442
|
+
setGoogleAdsConfigs(Array.isArray(configsData) ? configsData : []);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (googleSheetConfigRes.ok) {
|
|
446
|
+
const configsData = await googleSheetConfigRes.json();
|
|
447
|
+
setGoogleSheetConfigs(Array.isArray(configsData) ? configsData : []);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (closingReasonsRes.ok) {
|
|
451
|
+
const reasonsData = await closingReasonsRes.json();
|
|
452
|
+
setClosingReasons(Array.isArray(reasonsData) ? reasonsData : []);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (usersRes.ok) {
|
|
456
|
+
const usersData = await usersRes.json();
|
|
457
|
+
setMetaLeadUsers(usersData);
|
|
458
|
+
}
|
|
459
|
+
} catch (error) {
|
|
460
|
+
console.error("Erreur lors du chargement de l'intégration Meta Lead Ads:", error);
|
|
461
|
+
setMetaLeadError("Erreur lors du chargement de l'intégration Meta Lead Ads");
|
|
462
|
+
} finally {
|
|
463
|
+
setMetaLeadLoading(false);
|
|
464
|
+
setGoogleAdsLoading(false);
|
|
465
|
+
setGoogleSheetLoading(false);
|
|
466
|
+
setClosingReasonsLoading(false);
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
fetchLeadConfigs();
|
|
471
|
+
}, [isAdmin]);
|
|
472
|
+
|
|
473
|
+
// Gérer les messages de succès/erreur depuis l'URL
|
|
474
|
+
useEffect(() => {
|
|
475
|
+
const params = new URLSearchParams(window.location.search);
|
|
476
|
+
const success = params.get('success');
|
|
477
|
+
const error = params.get('error');
|
|
478
|
+
|
|
479
|
+
if (success === 'google_connected') {
|
|
480
|
+
setGoogleAccount({ email: null, connected: true });
|
|
481
|
+
// Recharger le statut
|
|
482
|
+
fetch('/api/auth/google/status')
|
|
483
|
+
.then((res) => res.json())
|
|
484
|
+
.then((data) => setGoogleAccount(data))
|
|
485
|
+
.catch(() => {});
|
|
486
|
+
// Nettoyer l'URL
|
|
487
|
+
window.history.replaceState({}, '', '/settings');
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (error) {
|
|
491
|
+
console.error('Erreur Google:', error);
|
|
492
|
+
// Nettoyer l'URL
|
|
493
|
+
window.history.replaceState({}, '', '/settings');
|
|
494
|
+
}
|
|
495
|
+
}, []);
|
|
496
|
+
|
|
497
|
+
const handleGoogleConnect = () => {
|
|
498
|
+
window.location.href = '/api/auth/google';
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
const handleGoogleDisconnect = async () => {
|
|
502
|
+
if (!confirm('Êtes-vous sûr de vouloir déconnecter votre compte Google ?')) {
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
try {
|
|
507
|
+
setGoogleDisconnecting(true);
|
|
508
|
+
const response = await fetch('/api/auth/google/disconnect', {
|
|
509
|
+
method: 'POST',
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
if (response.ok) {
|
|
513
|
+
setGoogleAccount({ email: null, connected: false });
|
|
514
|
+
} else {
|
|
515
|
+
const data = await response.json();
|
|
516
|
+
console.error('Erreur lors de la déconnexion:', data.error);
|
|
517
|
+
}
|
|
518
|
+
} catch (error) {
|
|
519
|
+
console.error('Erreur lors de la déconnexion Google:', error);
|
|
520
|
+
} finally {
|
|
521
|
+
setGoogleDisconnecting(false);
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
const handleCompanySubmit = async (e: React.FormEvent) => {
|
|
526
|
+
e.preventDefault();
|
|
527
|
+
setCompanyError('');
|
|
528
|
+
setCompanySuccess('');
|
|
529
|
+
setCompanySaving(true);
|
|
530
|
+
|
|
531
|
+
try {
|
|
532
|
+
const response = await fetch('/api/settings/company', {
|
|
533
|
+
method: 'PUT',
|
|
534
|
+
headers: { 'Content-Type': 'application/json' },
|
|
535
|
+
body: JSON.stringify(companyData),
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
const data = await response.json();
|
|
539
|
+
|
|
540
|
+
if (!response.ok) {
|
|
541
|
+
throw new Error(data.error || 'Erreur lors de la mise à jour');
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
setCompanySuccess("✅ Informations de l'entreprise mises à jour avec succès !");
|
|
545
|
+
setTimeout(() => {
|
|
546
|
+
setCompanySuccess('');
|
|
547
|
+
}, 5000);
|
|
548
|
+
} catch (err: any) {
|
|
549
|
+
setCompanyError(err.message);
|
|
550
|
+
} finally {
|
|
551
|
+
setCompanySaving(false);
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
const handleNameUpdate = async (e: React.FormEvent) => {
|
|
556
|
+
e.preventDefault();
|
|
557
|
+
setNameError('');
|
|
558
|
+
setNameSuccess('');
|
|
559
|
+
|
|
560
|
+
if (!nameValue.trim()) {
|
|
561
|
+
setNameError('Le nom ne peut pas être vide');
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
setNameLoading(true);
|
|
566
|
+
|
|
567
|
+
try {
|
|
568
|
+
const response = await fetch('/api/settings/profile', {
|
|
569
|
+
method: 'PUT',
|
|
570
|
+
headers: { 'Content-Type': 'application/json' },
|
|
571
|
+
body: JSON.stringify({ name: nameValue.trim() }),
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
const data = await response.json();
|
|
575
|
+
|
|
576
|
+
if (!response.ok) {
|
|
577
|
+
throw new Error(data.error || 'Erreur lors de la mise à jour du nom');
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
setNameSuccess('✅ Nom mis à jour avec succès !');
|
|
581
|
+
setShowNameForm(false);
|
|
582
|
+
|
|
583
|
+
// Rafraîchir la page pour mettre à jour la session
|
|
584
|
+
router.refresh();
|
|
585
|
+
|
|
586
|
+
// Effacer le message après 5 secondes
|
|
587
|
+
setTimeout(() => {
|
|
588
|
+
setNameSuccess('');
|
|
589
|
+
}, 5000);
|
|
590
|
+
} catch (err: any) {
|
|
591
|
+
setNameError(err.message);
|
|
592
|
+
} finally {
|
|
593
|
+
setNameLoading(false);
|
|
594
|
+
}
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
const handleSmtpTest = async () => {
|
|
598
|
+
setSmtpTestResult(null);
|
|
599
|
+
setSmtpError('');
|
|
600
|
+
setSmtpTesting(true);
|
|
601
|
+
|
|
602
|
+
try {
|
|
603
|
+
// Récupérer la signature actuelle depuis l'éditeur (HTML)
|
|
604
|
+
let signatureHtml = smtpData.signature;
|
|
605
|
+
if (smtpSignatureEditorRef.current) {
|
|
606
|
+
try {
|
|
607
|
+
signatureHtml = await smtpSignatureEditorRef.current.getHTML();
|
|
608
|
+
} catch {
|
|
609
|
+
// on ignore l'erreur, on garde la valeur du state
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const payload = { ...smtpData, signature: signatureHtml };
|
|
614
|
+
|
|
615
|
+
const response = await fetch('/api/settings/smtp/test', {
|
|
616
|
+
method: 'POST',
|
|
617
|
+
headers: { 'Content-Type': 'application/json' },
|
|
618
|
+
body: JSON.stringify(payload),
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
const data = await response.json();
|
|
622
|
+
|
|
623
|
+
if (data.success) {
|
|
624
|
+
setSmtpTestResult({
|
|
625
|
+
success: true,
|
|
626
|
+
message: data.message || 'Connexion SMTP réussie !',
|
|
627
|
+
});
|
|
628
|
+
setSmtpConfigured(true);
|
|
629
|
+
|
|
630
|
+
// Si le test est concluant, on enregistre automatiquement la configuration
|
|
631
|
+
try {
|
|
632
|
+
setSmtpSaving(true);
|
|
633
|
+
const saveResponse = await fetch('/api/settings/smtp', {
|
|
634
|
+
method: 'PUT',
|
|
635
|
+
headers: { 'Content-Type': 'application/json' },
|
|
636
|
+
body: JSON.stringify(payload),
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
const saveData = await saveResponse.json();
|
|
640
|
+
|
|
641
|
+
if (!saveResponse.ok) {
|
|
642
|
+
throw new Error(saveData.error || 'Erreur lors de la sauvegarde de la configuration');
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
setSmtpSuccess('✅ Configuration SMTP testée et sauvegardée avec succès !');
|
|
646
|
+
} catch (saveErr: any) {
|
|
647
|
+
// On affiche l’erreur de sauvegarde mais on garde le succès du test
|
|
648
|
+
setSmtpError(saveErr.message || 'La connexion fonctionne mais la sauvegarde a échoué.');
|
|
649
|
+
} finally {
|
|
650
|
+
setSmtpSaving(false);
|
|
651
|
+
}
|
|
652
|
+
} else {
|
|
653
|
+
setSmtpTestResult({
|
|
654
|
+
success: false,
|
|
655
|
+
message: data.message || 'Échec de la connexion SMTP',
|
|
656
|
+
});
|
|
657
|
+
setSmtpConfigured(false);
|
|
658
|
+
}
|
|
659
|
+
} catch (err: any) {
|
|
660
|
+
setSmtpTestResult({
|
|
661
|
+
success: false,
|
|
662
|
+
message: err.message || 'Erreur lors du test de connexion',
|
|
663
|
+
});
|
|
664
|
+
} finally {
|
|
665
|
+
setSmtpTesting(false);
|
|
666
|
+
}
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
const handleSmtpSubmit = async (e: React.FormEvent) => {
|
|
670
|
+
e.preventDefault();
|
|
671
|
+
setSmtpError('');
|
|
672
|
+
setSmtpSuccess('');
|
|
673
|
+
setSmtpSaving(true);
|
|
674
|
+
|
|
675
|
+
try {
|
|
676
|
+
// Récupérer la signature actuelle depuis l'éditeur (HTML)
|
|
677
|
+
let signatureHtml = smtpData.signature;
|
|
678
|
+
if (smtpSignatureEditorRef.current) {
|
|
679
|
+
try {
|
|
680
|
+
signatureHtml = await smtpSignatureEditorRef.current.getHTML();
|
|
681
|
+
} catch {
|
|
682
|
+
// on ignore l'erreur, on garde la valeur du state
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const payload = { ...smtpData, signature: signatureHtml };
|
|
687
|
+
|
|
688
|
+
const response = await fetch('/api/settings/smtp', {
|
|
689
|
+
method: 'PUT',
|
|
690
|
+
headers: { 'Content-Type': 'application/json' },
|
|
691
|
+
body: JSON.stringify(payload),
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
const data = await response.json();
|
|
695
|
+
|
|
696
|
+
if (!response.ok) {
|
|
697
|
+
throw new Error(data.error || 'Erreur lors de la sauvegarde');
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
setSmtpSuccess('✅ Configuration SMTP sauvegardée avec succès !');
|
|
701
|
+
// Si le test a réussi précédemment, garder l'indicateur de configuration
|
|
702
|
+
if (smtpTestResult?.success) {
|
|
703
|
+
setSmtpConfigured(true);
|
|
704
|
+
}
|
|
705
|
+
setTimeout(() => {
|
|
706
|
+
setSmtpSuccess('');
|
|
707
|
+
}, 5000);
|
|
708
|
+
} catch (err: any) {
|
|
709
|
+
setSmtpError(err.message);
|
|
710
|
+
} finally {
|
|
711
|
+
setSmtpSaving(false);
|
|
712
|
+
}
|
|
713
|
+
};
|
|
714
|
+
|
|
715
|
+
const handleStatusSubmit = async (e: React.FormEvent) => {
|
|
716
|
+
e.preventDefault();
|
|
717
|
+
setStatusError('');
|
|
718
|
+
setStatusSuccess('');
|
|
719
|
+
setStatusSaving(true);
|
|
720
|
+
|
|
721
|
+
try {
|
|
722
|
+
const url = editingStatus
|
|
723
|
+
? `/api/settings/statuses/${editingStatus.id}`
|
|
724
|
+
: '/api/settings/statuses';
|
|
725
|
+
const method = editingStatus ? 'PUT' : 'POST';
|
|
726
|
+
|
|
727
|
+
const response = await fetch(url, {
|
|
728
|
+
method,
|
|
729
|
+
headers: { 'Content-Type': 'application/json' },
|
|
730
|
+
body: JSON.stringify(statusFormData),
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
const data = await response.json();
|
|
734
|
+
|
|
735
|
+
if (!response.ok) {
|
|
736
|
+
throw new Error(data.error || 'Erreur lors de la sauvegarde');
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
setStatusSuccess(
|
|
740
|
+
editingStatus ? '✅ Statut modifié avec succès !' : '✅ Statut créé avec succès !',
|
|
741
|
+
);
|
|
742
|
+
setShowStatusForm(false);
|
|
743
|
+
setEditingStatus(null);
|
|
744
|
+
setStatusFormData({ name: '', color: '#3B82F6' });
|
|
745
|
+
|
|
746
|
+
// Recharger les statuts
|
|
747
|
+
const statusesResponse = await fetch('/api/settings/statuses');
|
|
748
|
+
if (statusesResponse.ok) {
|
|
749
|
+
const statusesData = await statusesResponse.json();
|
|
750
|
+
setStatuses(statusesData);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
setTimeout(() => {
|
|
754
|
+
setStatusSuccess('');
|
|
755
|
+
}, 5000);
|
|
756
|
+
} catch (err: any) {
|
|
757
|
+
setStatusError(err.message);
|
|
758
|
+
} finally {
|
|
759
|
+
setStatusSaving(false);
|
|
760
|
+
}
|
|
761
|
+
};
|
|
762
|
+
|
|
763
|
+
const handleStatusDelete = async (id: string) => {
|
|
764
|
+
if (!confirm('Êtes-vous sûr de vouloir supprimer ce statut ?')) {
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
try {
|
|
769
|
+
const response = await fetch(`/api/settings/statuses/${id}`, {
|
|
770
|
+
method: 'DELETE',
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
const data = await response.json();
|
|
774
|
+
|
|
775
|
+
if (!response.ok) {
|
|
776
|
+
throw new Error(data.error || 'Erreur lors de la suppression');
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
setStatusSuccess('✅ Statut supprimé avec succès !');
|
|
780
|
+
setTimeout(() => {
|
|
781
|
+
setStatusSuccess('');
|
|
782
|
+
}, 5000);
|
|
783
|
+
|
|
784
|
+
// Recharger les statuts
|
|
785
|
+
const statusesResponse = await fetch('/api/settings/statuses');
|
|
786
|
+
if (statusesResponse.ok) {
|
|
787
|
+
const statusesData = await statusesResponse.json();
|
|
788
|
+
setStatuses(statusesData);
|
|
789
|
+
}
|
|
790
|
+
} catch (err: any) {
|
|
791
|
+
setStatusError(err.message);
|
|
792
|
+
}
|
|
793
|
+
};
|
|
794
|
+
|
|
795
|
+
const handleStatusEdit = (status: Status) => {
|
|
796
|
+
setEditingStatus(status);
|
|
797
|
+
setStatusFormData({
|
|
798
|
+
name: status.name,
|
|
799
|
+
color: status.color,
|
|
800
|
+
});
|
|
801
|
+
setShowStatusForm(true);
|
|
802
|
+
setStatusError('');
|
|
803
|
+
setStatusSuccess('');
|
|
804
|
+
};
|
|
805
|
+
|
|
806
|
+
const handlePasswordChange = async (e: React.FormEvent) => {
|
|
807
|
+
e.preventDefault();
|
|
808
|
+
setPasswordError('');
|
|
809
|
+
setPasswordSuccess('');
|
|
810
|
+
|
|
811
|
+
if (passwordData.newPassword !== passwordData.confirmPassword) {
|
|
812
|
+
setPasswordError('Les nouveaux mots de passe ne correspondent pas');
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
if (passwordData.newPassword.length < 6) {
|
|
817
|
+
setPasswordError('Le nouveau mot de passe doit contenir au moins 6 caractères');
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
setPasswordLoading(true);
|
|
822
|
+
|
|
823
|
+
try {
|
|
824
|
+
const response = await fetch('/api/settings/change-password', {
|
|
825
|
+
method: 'POST',
|
|
826
|
+
headers: { 'Content-Type': 'application/json' },
|
|
827
|
+
body: JSON.stringify(passwordData),
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
const data = await response.json();
|
|
831
|
+
|
|
832
|
+
if (!response.ok) {
|
|
833
|
+
throw new Error(data.error || 'Erreur lors de la modification du mot de passe');
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
setPasswordSuccess('✅ Mot de passe modifié avec succès !');
|
|
837
|
+
setPasswordData({
|
|
838
|
+
currentPassword: '',
|
|
839
|
+
newPassword: '',
|
|
840
|
+
confirmPassword: '',
|
|
841
|
+
});
|
|
842
|
+
setShowPasswordForm(false);
|
|
843
|
+
|
|
844
|
+
// Effacer le message après 5 secondes
|
|
845
|
+
setTimeout(() => {
|
|
846
|
+
setPasswordSuccess('');
|
|
847
|
+
}, 5000);
|
|
848
|
+
} catch (err: any) {
|
|
849
|
+
setPasswordError(err.message);
|
|
850
|
+
} finally {
|
|
851
|
+
setPasswordLoading(false);
|
|
852
|
+
}
|
|
853
|
+
};
|
|
854
|
+
|
|
855
|
+
const handleMetaLeadSubmit = async (e: React.FormEvent) => {
|
|
856
|
+
e.preventDefault();
|
|
857
|
+
setMetaLeadError('');
|
|
858
|
+
setMetaLeadSuccess('');
|
|
859
|
+
setMetaLeadSaving(true);
|
|
860
|
+
|
|
861
|
+
try {
|
|
862
|
+
const url = editingMetaLeadConfig
|
|
863
|
+
? `/api/settings/meta-leads/${editingMetaLeadConfig}`
|
|
864
|
+
: '/api/settings/meta-leads';
|
|
865
|
+
const method = editingMetaLeadConfig ? 'PUT' : 'POST';
|
|
866
|
+
|
|
867
|
+
const response = await fetch(url, {
|
|
868
|
+
method,
|
|
869
|
+
headers: { 'Content-Type': 'application/json' },
|
|
870
|
+
body: JSON.stringify(metaLeadFormData),
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
const data = await response.json();
|
|
874
|
+
|
|
875
|
+
if (!response.ok) {
|
|
876
|
+
throw new Error(data.error || 'Erreur lors de la sauvegarde de la configuration Meta');
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
setMetaLeadSuccess(
|
|
880
|
+
editingMetaLeadConfig
|
|
881
|
+
? '✅ Configuration Meta Lead Ads mise à jour avec succès !'
|
|
882
|
+
: '✅ Configuration Meta Lead Ads créée avec succès !',
|
|
883
|
+
);
|
|
884
|
+
setShowMetaLeadModal(false);
|
|
885
|
+
setEditingMetaLeadConfig(null);
|
|
886
|
+
setMetaLeadFormData({
|
|
887
|
+
name: '',
|
|
888
|
+
active: true,
|
|
889
|
+
pageId: '',
|
|
890
|
+
accessToken: '',
|
|
891
|
+
verifyToken: '',
|
|
892
|
+
defaultStatusId: null,
|
|
893
|
+
defaultAssignedUserId: null,
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
// Recharger les configurations
|
|
897
|
+
const configsRes = await fetch('/api/settings/meta-leads');
|
|
898
|
+
if (configsRes.ok) {
|
|
899
|
+
const configsData = await configsRes.json();
|
|
900
|
+
setMetaLeadConfigs(Array.isArray(configsData) ? configsData : []);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
setTimeout(() => setMetaLeadSuccess(''), 5000);
|
|
904
|
+
} catch (error: any) {
|
|
905
|
+
setMetaLeadError(error.message || 'Erreur lors de la sauvegarde de la configuration Meta');
|
|
906
|
+
} finally {
|
|
907
|
+
setMetaLeadSaving(false);
|
|
908
|
+
}
|
|
909
|
+
};
|
|
910
|
+
|
|
911
|
+
const handleGoogleAdsSubmit = async (e: React.FormEvent) => {
|
|
912
|
+
e.preventDefault();
|
|
913
|
+
setGoogleAdsError('');
|
|
914
|
+
setGoogleAdsSuccess('');
|
|
915
|
+
setGoogleAdsSaving(true);
|
|
916
|
+
|
|
917
|
+
try {
|
|
918
|
+
const url = editingGoogleAdsConfig
|
|
919
|
+
? `/api/settings/google-ads/${editingGoogleAdsConfig}`
|
|
920
|
+
: '/api/settings/google-ads';
|
|
921
|
+
const method = editingGoogleAdsConfig ? 'PUT' : 'POST';
|
|
922
|
+
|
|
923
|
+
const response = await fetch(url, {
|
|
924
|
+
method,
|
|
925
|
+
headers: { 'Content-Type': 'application/json' },
|
|
926
|
+
body: JSON.stringify(googleAdsFormData),
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
const data = await response.json();
|
|
930
|
+
|
|
931
|
+
if (!response.ok) {
|
|
932
|
+
throw new Error(
|
|
933
|
+
data.error || 'Erreur lors de la sauvegarde de la configuration Google Ads',
|
|
934
|
+
);
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
setGoogleAdsSuccess(
|
|
938
|
+
editingGoogleAdsConfig
|
|
939
|
+
? '✅ Configuration Google Ads mise à jour avec succès !'
|
|
940
|
+
: '✅ Configuration Google Ads créée avec succès !',
|
|
941
|
+
);
|
|
942
|
+
setShowGoogleAdsModal(false);
|
|
943
|
+
setEditingGoogleAdsConfig(null);
|
|
944
|
+
setGoogleAdsFormData({
|
|
945
|
+
name: '',
|
|
946
|
+
active: true,
|
|
947
|
+
webhookKey: '',
|
|
948
|
+
defaultStatusId: null,
|
|
949
|
+
defaultAssignedUserId: null,
|
|
950
|
+
});
|
|
951
|
+
|
|
952
|
+
// Recharger les configurations
|
|
953
|
+
const configsRes = await fetch('/api/settings/google-ads');
|
|
954
|
+
if (configsRes.ok) {
|
|
955
|
+
const configsData = await configsRes.json();
|
|
956
|
+
setGoogleAdsConfigs(Array.isArray(configsData) ? configsData : []);
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
setTimeout(() => setGoogleAdsSuccess(''), 5000);
|
|
960
|
+
} catch (error: any) {
|
|
961
|
+
setGoogleAdsError(error.message || 'Erreur lors de la sauvegarde de la configuration Google');
|
|
962
|
+
} finally {
|
|
963
|
+
setGoogleAdsSaving(false);
|
|
964
|
+
}
|
|
965
|
+
};
|
|
966
|
+
|
|
967
|
+
const handleGoogleSheetSubmit = async (e: React.FormEvent) => {
|
|
968
|
+
e.preventDefault();
|
|
969
|
+
setGoogleSheetError('');
|
|
970
|
+
setGoogleSheetSuccess('');
|
|
971
|
+
setGoogleSheetSaving(true);
|
|
972
|
+
|
|
973
|
+
try {
|
|
974
|
+
// Vérifier qu'au moins un mapping téléphone est configuré
|
|
975
|
+
const phoneMapping = googleSheetMappings.find(
|
|
976
|
+
(m) => m.action === 'map' && m.crmField === 'phone' && m.columnName.trim() !== '',
|
|
977
|
+
);
|
|
978
|
+
|
|
979
|
+
if (!phoneMapping) {
|
|
980
|
+
setGoogleSheetError('Le mapping du téléphone est obligatoire');
|
|
981
|
+
setGoogleSheetSaving(false);
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
const url = editingGoogleSheetConfig
|
|
986
|
+
? `/api/settings/google-sheet/${editingGoogleSheetConfig}`
|
|
987
|
+
: '/api/settings/google-sheet';
|
|
988
|
+
const method = editingGoogleSheetConfig ? 'PUT' : 'POST';
|
|
989
|
+
|
|
990
|
+
const response = await fetch(url, {
|
|
991
|
+
method,
|
|
992
|
+
headers: { 'Content-Type': 'application/json' },
|
|
993
|
+
body: JSON.stringify({
|
|
994
|
+
...googleSheetFormData,
|
|
995
|
+
columnMappings: googleSheetMappings,
|
|
996
|
+
}),
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
const data = await response.json();
|
|
1000
|
+
|
|
1001
|
+
if (!response.ok) {
|
|
1002
|
+
throw new Error(
|
|
1003
|
+
data.error || 'Erreur lors de la sauvegarde de la configuration Google Sheets',
|
|
1004
|
+
);
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
setGoogleSheetSuccess(
|
|
1008
|
+
editingGoogleSheetConfig
|
|
1009
|
+
? '✅ Configuration Google Sheets mise à jour avec succès !'
|
|
1010
|
+
: '✅ Configuration Google Sheets créée avec succès !',
|
|
1011
|
+
);
|
|
1012
|
+
setShowGoogleSheetModal(false);
|
|
1013
|
+
setEditingGoogleSheetConfig(null);
|
|
1014
|
+
setGoogleSheetStep(1);
|
|
1015
|
+
setGoogleSheetFormData({
|
|
1016
|
+
name: '',
|
|
1017
|
+
active: true,
|
|
1018
|
+
sheetUrl: '',
|
|
1019
|
+
sheetName: '',
|
|
1020
|
+
headerRow: '1',
|
|
1021
|
+
defaultStatusId: null,
|
|
1022
|
+
defaultAssignedUserId: null,
|
|
1023
|
+
});
|
|
1024
|
+
setGoogleSheetMappings([]);
|
|
1025
|
+
|
|
1026
|
+
// Recharger les configurations
|
|
1027
|
+
const configsRes = await fetch('/api/settings/google-sheet');
|
|
1028
|
+
if (configsRes.ok) {
|
|
1029
|
+
const configsData = await configsRes.json();
|
|
1030
|
+
setGoogleSheetConfigs(Array.isArray(configsData) ? configsData : []);
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
setTimeout(() => setGoogleSheetSuccess(''), 5000);
|
|
1034
|
+
} catch (error: any) {
|
|
1035
|
+
setGoogleSheetError(
|
|
1036
|
+
error.message || 'Erreur lors de la sauvegarde de la configuration Google Sheets',
|
|
1037
|
+
);
|
|
1038
|
+
} finally {
|
|
1039
|
+
setGoogleSheetSaving(false);
|
|
1040
|
+
}
|
|
1041
|
+
};
|
|
1042
|
+
|
|
1043
|
+
// Mapping automatique des colonnes Google Sheets (comme pour l'import CSV)
|
|
1044
|
+
const handleGoogleSheetAutoMap = async (): Promise<boolean> => {
|
|
1045
|
+
try {
|
|
1046
|
+
setGoogleSheetError('');
|
|
1047
|
+
setGoogleSheetSuccess('');
|
|
1048
|
+
|
|
1049
|
+
if (
|
|
1050
|
+
!googleSheetFormData.sheetUrl ||
|
|
1051
|
+
!googleSheetFormData.sheetName ||
|
|
1052
|
+
!googleSheetFormData.headerRow
|
|
1053
|
+
) {
|
|
1054
|
+
setGoogleSheetError(
|
|
1055
|
+
'Veuillez renseigner le lien du Google Sheet, le nom de l’onglet et la ligne des en-têtes avant de continuer.',
|
|
1056
|
+
);
|
|
1057
|
+
return false;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
const response = await fetch('/api/settings/google-sheet/auto-map', {
|
|
1061
|
+
method: 'POST',
|
|
1062
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1063
|
+
body: JSON.stringify({
|
|
1064
|
+
sheetUrl: googleSheetFormData.sheetUrl,
|
|
1065
|
+
sheetName: googleSheetFormData.sheetName,
|
|
1066
|
+
headerRow: googleSheetFormData.headerRow || '1',
|
|
1067
|
+
}),
|
|
1068
|
+
});
|
|
1069
|
+
|
|
1070
|
+
const data = await response.json();
|
|
1071
|
+
|
|
1072
|
+
if (!response.ok) {
|
|
1073
|
+
throw new Error(
|
|
1074
|
+
data.error || 'Erreur lors du mapping automatique des colonnes Google Sheets',
|
|
1075
|
+
);
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
const headers = data.headers || [];
|
|
1079
|
+
const autoMapping = data.mapping || {};
|
|
1080
|
+
const preview = data.preview || [];
|
|
1081
|
+
|
|
1082
|
+
// Stocker les headers et l'aperçu
|
|
1083
|
+
setGoogleSheetHeaders(headers);
|
|
1084
|
+
setGoogleSheetPreview(preview);
|
|
1085
|
+
|
|
1086
|
+
// Initialiser les mappings avec les colonnes trouvées
|
|
1087
|
+
const initialMappings: ColumnMapping[] = [];
|
|
1088
|
+
|
|
1089
|
+
// Créer un mapping pour chaque colonne trouvée
|
|
1090
|
+
headers.forEach((header: string, index: number) => {
|
|
1091
|
+
if (!header) return;
|
|
1092
|
+
|
|
1093
|
+
// L'API auto-map retourne les index de colonnes (A, B, C), mais on utilise les noms réels
|
|
1094
|
+
// Chercher si cette colonne (par index) a été auto-mappée
|
|
1095
|
+
const columnLetter = indexToColumn(index);
|
|
1096
|
+
const mappedField = Object.entries(autoMapping).find(
|
|
1097
|
+
([, columnLetterFromMapping]) => columnLetterFromMapping === columnLetter,
|
|
1098
|
+
)?.[0];
|
|
1099
|
+
|
|
1100
|
+
if (mappedField) {
|
|
1101
|
+
// Convertir l'ancien format vers le nouveau
|
|
1102
|
+
const crmFieldMap: Record<string, string> = {
|
|
1103
|
+
phoneColumn: 'phone',
|
|
1104
|
+
firstNameColumn: 'firstName',
|
|
1105
|
+
lastNameColumn: 'lastName',
|
|
1106
|
+
emailColumn: 'email',
|
|
1107
|
+
cityColumn: 'city',
|
|
1108
|
+
postalCodeColumn: 'postalCode',
|
|
1109
|
+
originColumn: 'origin',
|
|
1110
|
+
};
|
|
1111
|
+
|
|
1112
|
+
initialMappings.push({
|
|
1113
|
+
id: `mapping-${Date.now()}-${Math.random()}`,
|
|
1114
|
+
columnName: header, // Utiliser le nom réel de la colonne
|
|
1115
|
+
action: 'map',
|
|
1116
|
+
crmField: crmFieldMap[mappedField] || undefined,
|
|
1117
|
+
});
|
|
1118
|
+
} else {
|
|
1119
|
+
// Colonne non mappée automatiquement, laisser l'utilisateur choisir
|
|
1120
|
+
initialMappings.push({
|
|
1121
|
+
id: `mapping-${Date.now()}-${Math.random()}`,
|
|
1122
|
+
columnName: header,
|
|
1123
|
+
action: 'ignore',
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1128
|
+
setGoogleSheetMappings(initialMappings);
|
|
1129
|
+
|
|
1130
|
+
setGoogleSheetSuccess(
|
|
1131
|
+
'✅ Colonnes détectées. Configurez maintenant le mapping des colonnes.',
|
|
1132
|
+
);
|
|
1133
|
+
return true;
|
|
1134
|
+
} catch (error: any) {
|
|
1135
|
+
console.error('Erreur lors du mapping automatique Google Sheets:', error);
|
|
1136
|
+
setGoogleSheetError(
|
|
1137
|
+
error.message || 'Erreur lors du mapping automatique des colonnes Google Sheets',
|
|
1138
|
+
);
|
|
1139
|
+
return false;
|
|
1140
|
+
}
|
|
1141
|
+
};
|
|
1142
|
+
|
|
1143
|
+
const handleGoogleSheetSync = async () => {
|
|
1144
|
+
setGoogleSheetError('');
|
|
1145
|
+
setGoogleSheetSuccess('');
|
|
1146
|
+
setGoogleSheetSyncing(true);
|
|
1147
|
+
|
|
1148
|
+
try {
|
|
1149
|
+
const response = await fetch('/api/integrations/google-sheet/sync', {
|
|
1150
|
+
method: 'POST',
|
|
1151
|
+
});
|
|
1152
|
+
|
|
1153
|
+
const data = await response.json();
|
|
1154
|
+
|
|
1155
|
+
if (!response.ok) {
|
|
1156
|
+
throw new Error(data.error || 'Erreur lors de la synchronisation Google Sheets');
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
const totalImported = data.totalImported || data.imported || 0;
|
|
1160
|
+
const totalUpdated = data.totalUpdated || data.updated || 0;
|
|
1161
|
+
const totalSkipped = data.totalSkipped || data.skipped || 0;
|
|
1162
|
+
setGoogleSheetSuccess(
|
|
1163
|
+
`✅ Synchronisation terminée : ${totalImported} nouveau(x) contact(s), ${totalUpdated} mis à jour, ${totalSkipped} ignoré(s).`,
|
|
1164
|
+
);
|
|
1165
|
+
setTimeout(() => setGoogleSheetSuccess(''), 8000);
|
|
1166
|
+
} catch (error: any) {
|
|
1167
|
+
setGoogleSheetError(
|
|
1168
|
+
error.message || 'Erreur lors de la synchronisation des contacts Google Sheets',
|
|
1169
|
+
);
|
|
1170
|
+
} finally {
|
|
1171
|
+
setGoogleSheetSyncing(false);
|
|
1172
|
+
}
|
|
1173
|
+
};
|
|
1174
|
+
|
|
1175
|
+
// Fonctions pour gérer les configurations Meta Lead Ads
|
|
1176
|
+
const handleEditMetaLead = (config: (typeof metaLeadConfigs)[0]) => {
|
|
1177
|
+
setEditingMetaLeadConfig(config.id);
|
|
1178
|
+
setMetaLeadFormData({
|
|
1179
|
+
name: config.name,
|
|
1180
|
+
active: config.active,
|
|
1181
|
+
pageId: config.pageId,
|
|
1182
|
+
accessToken: '', // Ne pas charger le token
|
|
1183
|
+
verifyToken: config.verifyToken,
|
|
1184
|
+
defaultStatusId: config.defaultStatusId,
|
|
1185
|
+
defaultAssignedUserId: config.defaultAssignedUserId,
|
|
1186
|
+
});
|
|
1187
|
+
setShowMetaLeadModal(true);
|
|
1188
|
+
setMetaLeadError('');
|
|
1189
|
+
setMetaLeadSuccess('');
|
|
1190
|
+
};
|
|
1191
|
+
|
|
1192
|
+
const handleDeleteMetaLead = async (id: string) => {
|
|
1193
|
+
if (!confirm('Êtes-vous sûr de vouloir supprimer cette configuration ?')) {
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
try {
|
|
1198
|
+
const response = await fetch(`/api/settings/meta-leads/${id}`, {
|
|
1199
|
+
method: 'DELETE',
|
|
1200
|
+
});
|
|
1201
|
+
|
|
1202
|
+
if (!response.ok) {
|
|
1203
|
+
const data = await response.json();
|
|
1204
|
+
throw new Error(data.error || 'Erreur lors de la suppression');
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
setMetaLeadSuccess('✅ Configuration supprimée avec succès !');
|
|
1208
|
+
setTimeout(() => setMetaLeadSuccess(''), 5000);
|
|
1209
|
+
|
|
1210
|
+
// Recharger les configurations
|
|
1211
|
+
const configsRes = await fetch('/api/settings/meta-leads');
|
|
1212
|
+
if (configsRes.ok) {
|
|
1213
|
+
const configsData = await configsRes.json();
|
|
1214
|
+
setMetaLeadConfigs(Array.isArray(configsData) ? configsData : []);
|
|
1215
|
+
}
|
|
1216
|
+
} catch (error: any) {
|
|
1217
|
+
setMetaLeadError(error.message || 'Erreur lors de la suppression');
|
|
1218
|
+
}
|
|
1219
|
+
};
|
|
1220
|
+
|
|
1221
|
+
// Fonctions pour gérer les configurations Google Ads
|
|
1222
|
+
const handleEditGoogleAds = (config: (typeof googleAdsConfigs)[0]) => {
|
|
1223
|
+
setEditingGoogleAdsConfig(config.id);
|
|
1224
|
+
setGoogleAdsFormData({
|
|
1225
|
+
name: config.name,
|
|
1226
|
+
active: config.active,
|
|
1227
|
+
webhookKey: config.webhookKey,
|
|
1228
|
+
defaultStatusId: config.defaultStatusId,
|
|
1229
|
+
defaultAssignedUserId: config.defaultAssignedUserId,
|
|
1230
|
+
});
|
|
1231
|
+
setShowGoogleAdsModal(true);
|
|
1232
|
+
setGoogleAdsError('');
|
|
1233
|
+
setGoogleAdsSuccess('');
|
|
1234
|
+
};
|
|
1235
|
+
|
|
1236
|
+
const handleDeleteGoogleAds = async (id: string) => {
|
|
1237
|
+
if (!confirm('Êtes-vous sûr de vouloir supprimer cette configuration ?')) {
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
try {
|
|
1242
|
+
const response = await fetch(`/api/settings/google-ads/${id}`, {
|
|
1243
|
+
method: 'DELETE',
|
|
1244
|
+
});
|
|
1245
|
+
|
|
1246
|
+
if (!response.ok) {
|
|
1247
|
+
const data = await response.json();
|
|
1248
|
+
throw new Error(data.error || 'Erreur lors de la suppression');
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
setGoogleAdsSuccess('✅ Configuration supprimée avec succès !');
|
|
1252
|
+
setTimeout(() => setGoogleAdsSuccess(''), 5000);
|
|
1253
|
+
|
|
1254
|
+
// Recharger les configurations
|
|
1255
|
+
const configsRes = await fetch('/api/settings/google-ads');
|
|
1256
|
+
if (configsRes.ok) {
|
|
1257
|
+
const configsData = await configsRes.json();
|
|
1258
|
+
setGoogleAdsConfigs(Array.isArray(configsData) ? configsData : []);
|
|
1259
|
+
}
|
|
1260
|
+
} catch (error: any) {
|
|
1261
|
+
setGoogleAdsError(error.message || 'Erreur lors de la suppression');
|
|
1262
|
+
}
|
|
1263
|
+
};
|
|
1264
|
+
|
|
1265
|
+
// Fonctions pour gérer les configurations Google Sheets
|
|
1266
|
+
const handleEditGoogleSheet = (config: (typeof googleSheetConfigs)[0]) => {
|
|
1267
|
+
setEditingGoogleSheetConfig(config.id);
|
|
1268
|
+
setGoogleSheetFormData({
|
|
1269
|
+
name: config.name,
|
|
1270
|
+
active: config.active,
|
|
1271
|
+
sheetUrl: config.spreadsheetId
|
|
1272
|
+
? `https://docs.google.com/spreadsheets/d/${config.spreadsheetId}/edit`
|
|
1273
|
+
: '',
|
|
1274
|
+
sheetName: config.sheetName,
|
|
1275
|
+
headerRow: config.headerRow.toString(),
|
|
1276
|
+
defaultStatusId: config.defaultStatusId,
|
|
1277
|
+
defaultAssignedUserId: config.defaultAssignedUserId,
|
|
1278
|
+
});
|
|
1279
|
+
|
|
1280
|
+
// Charger les mappings existants (si disponibles) ou convertir l'ancien format
|
|
1281
|
+
if ((config as any).columnMappings && Array.isArray((config as any).columnMappings)) {
|
|
1282
|
+
setGoogleSheetMappings((config as any).columnMappings);
|
|
1283
|
+
} else {
|
|
1284
|
+
// Convertir l'ancien format vers le nouveau
|
|
1285
|
+
const mappings: ColumnMapping[] = [];
|
|
1286
|
+
const oldMappings = [
|
|
1287
|
+
{ column: config.phoneColumn, field: 'phone' },
|
|
1288
|
+
{ column: config.firstNameColumn, field: 'firstName' },
|
|
1289
|
+
{ column: config.lastNameColumn, field: 'lastName' },
|
|
1290
|
+
{ column: config.emailColumn, field: 'email' },
|
|
1291
|
+
{ column: config.cityColumn, field: 'city' },
|
|
1292
|
+
{ column: config.postalCodeColumn, field: 'postalCode' },
|
|
1293
|
+
{ column: config.originColumn, field: 'origin' },
|
|
1294
|
+
];
|
|
1295
|
+
|
|
1296
|
+
oldMappings.forEach(({ column, field }) => {
|
|
1297
|
+
if (column) {
|
|
1298
|
+
mappings.push({
|
|
1299
|
+
id: `mapping-${Date.now()}-${Math.random()}`,
|
|
1300
|
+
columnName: column,
|
|
1301
|
+
action: 'map',
|
|
1302
|
+
crmField: field,
|
|
1303
|
+
});
|
|
1304
|
+
}
|
|
1305
|
+
});
|
|
1306
|
+
|
|
1307
|
+
setGoogleSheetMappings(mappings);
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
setGoogleSheetStep(2); // Aller directement à l'étape 2 pour l'édition
|
|
1311
|
+
setShowGoogleSheetModal(true);
|
|
1312
|
+
setGoogleSheetError('');
|
|
1313
|
+
setGoogleSheetSuccess('');
|
|
1314
|
+
};
|
|
1315
|
+
|
|
1316
|
+
const handleDeleteGoogleSheet = async (id: string) => {
|
|
1317
|
+
if (!confirm('Êtes-vous sûr de vouloir supprimer cette configuration ?')) {
|
|
1318
|
+
return;
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
try {
|
|
1322
|
+
const response = await fetch(`/api/settings/google-sheet/${id}`, {
|
|
1323
|
+
method: 'DELETE',
|
|
1324
|
+
});
|
|
1325
|
+
|
|
1326
|
+
if (!response.ok) {
|
|
1327
|
+
const data = await response.json();
|
|
1328
|
+
throw new Error(data.error || 'Erreur lors de la suppression');
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
setGoogleSheetSuccess('✅ Configuration supprimée avec succès !');
|
|
1332
|
+
setTimeout(() => setGoogleSheetSuccess(''), 5000);
|
|
1333
|
+
|
|
1334
|
+
// Recharger les configurations
|
|
1335
|
+
const configsRes = await fetch('/api/settings/google-sheet');
|
|
1336
|
+
if (configsRes.ok) {
|
|
1337
|
+
const configsData = await configsRes.json();
|
|
1338
|
+
setGoogleSheetConfigs(Array.isArray(configsData) ? configsData : []);
|
|
1339
|
+
}
|
|
1340
|
+
} catch (error: any) {
|
|
1341
|
+
setGoogleSheetError(error.message || 'Erreur lors de la suppression');
|
|
1342
|
+
}
|
|
1343
|
+
};
|
|
1344
|
+
|
|
1345
|
+
return (
|
|
1346
|
+
<div className="bg-crms-bg flex h-full flex-col">
|
|
1347
|
+
{/* Header avec breadcrumbs */}
|
|
1348
|
+
<div className="border-b border-gray-200 bg-white px-4 py-4 sm:px-6 lg:px-8">
|
|
1349
|
+
<div className="flex items-center justify-between">
|
|
1350
|
+
<div>
|
|
1351
|
+
<h1 className="text-2xl font-bold text-gray-900">Paramètres</h1>
|
|
1352
|
+
<p className="mt-1 text-sm text-gray-500">Home {'>'} Paramètres</p>
|
|
1353
|
+
</div>
|
|
1354
|
+
<div className="flex items-center gap-2">
|
|
1355
|
+
<button
|
|
1356
|
+
type="button"
|
|
1357
|
+
onClick={() => window.location.reload()}
|
|
1358
|
+
className="cursor-pointer rounded-lg p-2 text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-600"
|
|
1359
|
+
aria-label="Actualiser"
|
|
1360
|
+
>
|
|
1361
|
+
<RefreshCw className="h-5 w-5" />
|
|
1362
|
+
</button>
|
|
1363
|
+
<button
|
|
1364
|
+
type="button"
|
|
1365
|
+
className="cursor-pointer rounded-lg p-2 text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-600"
|
|
1366
|
+
aria-label="Paramètres"
|
|
1367
|
+
>
|
|
1368
|
+
<Settings className="h-5 w-5" />
|
|
1369
|
+
</button>
|
|
1370
|
+
</div>
|
|
1371
|
+
</div>
|
|
1372
|
+
</div>
|
|
1373
|
+
|
|
1374
|
+
{/* Navbar horizontale avec sections principales */}
|
|
1375
|
+
<div className="border-b border-gray-200 bg-white px-4 sm:px-6 lg:px-8">
|
|
1376
|
+
<div className="flex gap-1 overflow-x-auto">
|
|
1377
|
+
<button
|
|
1378
|
+
type="button"
|
|
1379
|
+
onClick={() => setMainSection('general')}
|
|
1380
|
+
className={cn(
|
|
1381
|
+
'flex cursor-pointer items-center gap-2 border-b-2 px-4 py-3 text-sm font-medium whitespace-nowrap transition-colors',
|
|
1382
|
+
mainSection === 'general'
|
|
1383
|
+
? 'border-indigo-600 text-indigo-600'
|
|
1384
|
+
: 'border-transparent text-gray-600 hover:border-gray-300 hover:text-gray-900',
|
|
1385
|
+
)}
|
|
1386
|
+
>
|
|
1387
|
+
<Settings className="h-4 w-4" />
|
|
1388
|
+
Paramètres Généraux
|
|
1389
|
+
</button>
|
|
1390
|
+
{isAdmin && (
|
|
1391
|
+
<button
|
|
1392
|
+
type="button"
|
|
1393
|
+
onClick={() => setMainSection('app')}
|
|
1394
|
+
className={cn(
|
|
1395
|
+
'flex cursor-pointer items-center gap-2 border-b-2 px-4 py-3 text-sm font-medium whitespace-nowrap transition-colors',
|
|
1396
|
+
mainSection === 'app'
|
|
1397
|
+
? 'border-indigo-600 text-indigo-600'
|
|
1398
|
+
: 'border-transparent text-gray-600 hover:border-gray-300 hover:text-gray-900',
|
|
1399
|
+
)}
|
|
1400
|
+
>
|
|
1401
|
+
<Grid3x3 className="h-4 w-4" />
|
|
1402
|
+
Paramètres de l'Application
|
|
1403
|
+
</button>
|
|
1404
|
+
)}
|
|
1405
|
+
<button
|
|
1406
|
+
type="button"
|
|
1407
|
+
onClick={() => setMainSection('system')}
|
|
1408
|
+
className={cn(
|
|
1409
|
+
'flex cursor-pointer items-center gap-2 border-b-2 px-4 py-3 text-sm font-medium whitespace-nowrap transition-colors',
|
|
1410
|
+
mainSection === 'system'
|
|
1411
|
+
? 'border-indigo-600 text-indigo-600'
|
|
1412
|
+
: 'border-transparent text-gray-600 hover:border-gray-300 hover:text-gray-900',
|
|
1413
|
+
)}
|
|
1414
|
+
>
|
|
1415
|
+
<Monitor className="h-4 w-4" />
|
|
1416
|
+
Paramètres Système
|
|
1417
|
+
</button>
|
|
1418
|
+
{isAdmin && (
|
|
1419
|
+
<button
|
|
1420
|
+
type="button"
|
|
1421
|
+
onClick={() => setMainSection('integrations')}
|
|
1422
|
+
className={cn(
|
|
1423
|
+
'flex cursor-pointer items-center gap-2 border-b-2 px-4 py-3 text-sm font-medium whitespace-nowrap transition-colors',
|
|
1424
|
+
mainSection === 'integrations'
|
|
1425
|
+
? 'border-indigo-600 text-indigo-600'
|
|
1426
|
+
: 'border-transparent text-gray-600 hover:border-gray-300 hover:text-gray-900',
|
|
1427
|
+
)}
|
|
1428
|
+
>
|
|
1429
|
+
<Plug className="h-4 w-4" />
|
|
1430
|
+
Intégrations
|
|
1431
|
+
</button>
|
|
1432
|
+
)}
|
|
1433
|
+
</div>
|
|
1434
|
+
</div>
|
|
1435
|
+
|
|
1436
|
+
{/* Content avec panneau principal */}
|
|
1437
|
+
<div className="flex flex-1 overflow-hidden">
|
|
1438
|
+
{/* Panneau de contenu principal */}
|
|
1439
|
+
<div className="bg-crms-bg flex-1 overflow-y-auto p-4 sm:p-6 lg:p-8">
|
|
1440
|
+
<div className="mx-auto max-w-4xl space-y-4 sm:space-y-6">
|
|
1441
|
+
{/* Section Paramètres Généraux */}
|
|
1442
|
+
{mainSection === 'general' && (
|
|
1443
|
+
<div className="space-y-6">
|
|
1444
|
+
{/* Section Profil */}
|
|
1445
|
+
<div className="rounded-lg bg-white p-6 shadow-sm">
|
|
1446
|
+
<div className="mb-1 flex items-center justify-between">
|
|
1447
|
+
<div>
|
|
1448
|
+
<h2 className="text-lg font-bold text-gray-900">Profil</h2>
|
|
1449
|
+
</div>
|
|
1450
|
+
</div>
|
|
1451
|
+
<p className="mb-6 text-sm text-gray-600">Gérez vos informations personnelles</p>
|
|
1452
|
+
|
|
1453
|
+
<div className="space-y-4">
|
|
1454
|
+
{/* Nom - Modifiable */}
|
|
1455
|
+
<div className="border-b border-gray-100 pb-4">
|
|
1456
|
+
{!showNameForm ? (
|
|
1457
|
+
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
1458
|
+
<div className="min-w-0 flex-1">
|
|
1459
|
+
<p className="font-medium text-gray-900">Nom</p>
|
|
1460
|
+
<p className="mt-1 truncate text-sm text-gray-600">
|
|
1461
|
+
{session?.user?.name || 'Non défini'}
|
|
1462
|
+
</p>
|
|
1463
|
+
</div>
|
|
1464
|
+
<button
|
|
1465
|
+
onClick={() => {
|
|
1466
|
+
setShowNameForm(true);
|
|
1467
|
+
setNameValue(session?.user?.name || '');
|
|
1468
|
+
setNameError('');
|
|
1469
|
+
setNameSuccess('');
|
|
1470
|
+
}}
|
|
1471
|
+
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"
|
|
1472
|
+
>
|
|
1473
|
+
Modifier
|
|
1474
|
+
</button>
|
|
1475
|
+
</div>
|
|
1476
|
+
) : (
|
|
1477
|
+
<form onSubmit={handleNameUpdate} className="space-y-4">
|
|
1478
|
+
{nameSuccess && (
|
|
1479
|
+
<div className="rounded-lg bg-green-50 p-4 text-sm text-green-600">
|
|
1480
|
+
{nameSuccess}
|
|
1481
|
+
</div>
|
|
1482
|
+
)}
|
|
1483
|
+
|
|
1484
|
+
{nameError && (
|
|
1485
|
+
<div className="rounded-lg bg-indigo-50 p-4 text-sm text-indigo-600">
|
|
1486
|
+
{nameError}
|
|
1487
|
+
</div>
|
|
1488
|
+
)}
|
|
1489
|
+
|
|
1490
|
+
<div>
|
|
1491
|
+
<label className="block text-sm font-medium text-gray-700">Nom</label>
|
|
1492
|
+
<input
|
|
1493
|
+
type="text"
|
|
1494
|
+
required
|
|
1495
|
+
value={nameValue}
|
|
1496
|
+
onChange={(e) => setNameValue(e.target.value)}
|
|
1497
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
|
1498
|
+
placeholder="Votre nom"
|
|
1499
|
+
/>
|
|
1500
|
+
</div>
|
|
1501
|
+
|
|
1502
|
+
<div className="flex flex-col gap-3 sm:flex-row">
|
|
1503
|
+
<button
|
|
1504
|
+
type="submit"
|
|
1505
|
+
disabled={nameLoading}
|
|
1506
|
+
className="w-full cursor-pointer rounded-lg bg-indigo-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-indigo-700 focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto"
|
|
1507
|
+
>
|
|
1508
|
+
{nameLoading ? 'Enregistrement...' : 'Enregistrer'}
|
|
1509
|
+
</button>
|
|
1510
|
+
<button
|
|
1511
|
+
type="button"
|
|
1512
|
+
onClick={() => {
|
|
1513
|
+
setShowNameForm(false);
|
|
1514
|
+
setNameValue(session?.user?.name || '');
|
|
1515
|
+
setNameError('');
|
|
1516
|
+
setNameSuccess('');
|
|
1517
|
+
}}
|
|
1518
|
+
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"
|
|
1519
|
+
>
|
|
1520
|
+
Annuler
|
|
1521
|
+
</button>
|
|
1522
|
+
</div>
|
|
1523
|
+
</form>
|
|
1524
|
+
)}
|
|
1525
|
+
</div>
|
|
1526
|
+
|
|
1527
|
+
{/* Email - Lecture seule */}
|
|
1528
|
+
<div className="pb-0">
|
|
1529
|
+
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
1530
|
+
<div className="min-w-0 flex-1">
|
|
1531
|
+
<p className="font-medium text-gray-900">Email</p>
|
|
1532
|
+
<p className="mt-1 truncate text-sm text-gray-600">
|
|
1533
|
+
{session?.user?.email || 'Non défini'}
|
|
1534
|
+
</p>
|
|
1535
|
+
<p className="mt-1 text-xs text-gray-500">
|
|
1536
|
+
L'email ne peut pas être modifié
|
|
1537
|
+
</p>
|
|
1538
|
+
</div>
|
|
1539
|
+
</div>
|
|
1540
|
+
</div>
|
|
1541
|
+
</div>
|
|
1542
|
+
</div>
|
|
1543
|
+
|
|
1544
|
+
{/* Section Sécurité */}
|
|
1545
|
+
<div className="rounded-lg bg-white p-6 shadow-sm">
|
|
1546
|
+
<h2 className="mb-6 text-lg font-bold text-gray-900">Sécurité</h2>
|
|
1547
|
+
<div className="mt-4 sm:mt-6">
|
|
1548
|
+
{!showPasswordForm ? (
|
|
1549
|
+
<div className="flex flex-col gap-3 border-b border-gray-100 pb-4 sm:flex-row sm:items-center sm:justify-between">
|
|
1550
|
+
<div className="min-w-0 flex-1">
|
|
1551
|
+
<p className="font-medium text-gray-900">Mot de passe</p>
|
|
1552
|
+
<p className="mt-1 text-sm text-gray-600">••••••••</p>
|
|
1553
|
+
</div>
|
|
1554
|
+
<button
|
|
1555
|
+
onClick={() => setShowPasswordForm(true)}
|
|
1556
|
+
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"
|
|
1557
|
+
>
|
|
1558
|
+
Modifier
|
|
1559
|
+
</button>
|
|
1560
|
+
</div>
|
|
1561
|
+
) : (
|
|
1562
|
+
<form onSubmit={handlePasswordChange} className="space-y-4">
|
|
1563
|
+
{passwordSuccess && (
|
|
1564
|
+
<div className="rounded-lg bg-green-50 p-4 text-sm text-green-600">
|
|
1565
|
+
{passwordSuccess}
|
|
1566
|
+
</div>
|
|
1567
|
+
)}
|
|
1568
|
+
|
|
1569
|
+
{passwordError && (
|
|
1570
|
+
<div className="rounded-lg bg-indigo-50 p-4 text-sm text-indigo-600">
|
|
1571
|
+
{passwordError}
|
|
1572
|
+
</div>
|
|
1573
|
+
)}
|
|
1574
|
+
|
|
1575
|
+
<div>
|
|
1576
|
+
<label className="block text-sm font-medium text-gray-700">
|
|
1577
|
+
Mot de passe actuel
|
|
1578
|
+
</label>
|
|
1579
|
+
<input
|
|
1580
|
+
type="password"
|
|
1581
|
+
required
|
|
1582
|
+
value={passwordData.currentPassword}
|
|
1583
|
+
onChange={(e) =>
|
|
1584
|
+
setPasswordData({
|
|
1585
|
+
...passwordData,
|
|
1586
|
+
currentPassword: e.target.value,
|
|
1587
|
+
})
|
|
1588
|
+
}
|
|
1589
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
|
1590
|
+
placeholder="••••••••"
|
|
1591
|
+
/>
|
|
1592
|
+
</div>
|
|
1593
|
+
|
|
1594
|
+
<div>
|
|
1595
|
+
<label className="block text-sm font-medium text-gray-700">
|
|
1596
|
+
Nouveau mot de passe
|
|
1597
|
+
</label>
|
|
1598
|
+
<input
|
|
1599
|
+
type="password"
|
|
1600
|
+
required
|
|
1601
|
+
minLength={6}
|
|
1602
|
+
value={passwordData.newPassword}
|
|
1603
|
+
onChange={(e) =>
|
|
1604
|
+
setPasswordData({
|
|
1605
|
+
...passwordData,
|
|
1606
|
+
newPassword: e.target.value,
|
|
1607
|
+
})
|
|
1608
|
+
}
|
|
1609
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
|
1610
|
+
placeholder="••••••••"
|
|
1611
|
+
/>
|
|
1612
|
+
<p className="mt-1 text-xs text-gray-500">Minimum 6 caractères</p>
|
|
1613
|
+
</div>
|
|
1614
|
+
|
|
1615
|
+
<div>
|
|
1616
|
+
<label className="block text-sm font-medium text-gray-700">
|
|
1617
|
+
Confirmer le nouveau mot de passe
|
|
1618
|
+
</label>
|
|
1619
|
+
<input
|
|
1620
|
+
type="password"
|
|
1621
|
+
required
|
|
1622
|
+
minLength={6}
|
|
1623
|
+
value={passwordData.confirmPassword}
|
|
1624
|
+
onChange={(e) =>
|
|
1625
|
+
setPasswordData({
|
|
1626
|
+
...passwordData,
|
|
1627
|
+
confirmPassword: e.target.value,
|
|
1628
|
+
})
|
|
1629
|
+
}
|
|
1630
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
|
1631
|
+
placeholder="••••••••"
|
|
1632
|
+
/>
|
|
1633
|
+
</div>
|
|
1634
|
+
|
|
1635
|
+
<div className="flex flex-col gap-3 sm:flex-row">
|
|
1636
|
+
<button
|
|
1637
|
+
type="submit"
|
|
1638
|
+
disabled={passwordLoading}
|
|
1639
|
+
className="w-full cursor-pointer rounded-lg bg-indigo-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-indigo-700 focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto"
|
|
1640
|
+
>
|
|
1641
|
+
{passwordLoading ? 'Modification...' : 'Modifier le mot de passe'}
|
|
1642
|
+
</button>
|
|
1643
|
+
<button
|
|
1644
|
+
type="button"
|
|
1645
|
+
onClick={() => {
|
|
1646
|
+
setShowPasswordForm(false);
|
|
1647
|
+
setPasswordData({
|
|
1648
|
+
currentPassword: '',
|
|
1649
|
+
newPassword: '',
|
|
1650
|
+
confirmPassword: '',
|
|
1651
|
+
});
|
|
1652
|
+
setPasswordError('');
|
|
1653
|
+
setPasswordSuccess('');
|
|
1654
|
+
}}
|
|
1655
|
+
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"
|
|
1656
|
+
>
|
|
1657
|
+
Annuler
|
|
1658
|
+
</button>
|
|
1659
|
+
</div>
|
|
1660
|
+
</form>
|
|
1661
|
+
)}
|
|
1662
|
+
</div>
|
|
1663
|
+
</div>
|
|
1664
|
+
|
|
1665
|
+
{/* Section Entreprise */}
|
|
1666
|
+
{isAdmin && (
|
|
1667
|
+
<div className="rounded-lg bg-white p-6 shadow-sm">
|
|
1668
|
+
<h2 className="mb-1 text-lg font-bold text-gray-900">
|
|
1669
|
+
Informations de l'entreprise
|
|
1670
|
+
</h2>
|
|
1671
|
+
<p className="mb-6 text-sm text-gray-600">
|
|
1672
|
+
Gérez les informations de votre entreprise (visible uniquement par les
|
|
1673
|
+
administrateurs)
|
|
1674
|
+
</p>
|
|
1675
|
+
|
|
1676
|
+
{companySuccess && (
|
|
1677
|
+
<div className="mt-4 rounded-lg border border-green-200 bg-green-50 p-4">
|
|
1678
|
+
<div className="flex items-center">
|
|
1679
|
+
<svg
|
|
1680
|
+
className="mr-3 h-5 w-5 text-green-600"
|
|
1681
|
+
fill="none"
|
|
1682
|
+
stroke="currentColor"
|
|
1683
|
+
viewBox="0 0 24 24"
|
|
1684
|
+
>
|
|
1685
|
+
<path
|
|
1686
|
+
strokeLinecap="round"
|
|
1687
|
+
strokeLinejoin="round"
|
|
1688
|
+
strokeWidth={2}
|
|
1689
|
+
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
1690
|
+
/>
|
|
1691
|
+
</svg>
|
|
1692
|
+
<p className="text-sm font-medium text-green-800">{companySuccess}</p>
|
|
1693
|
+
<button
|
|
1694
|
+
onClick={() => setCompanySuccess('')}
|
|
1695
|
+
className="ml-auto cursor-pointer text-green-600 hover:text-green-800"
|
|
1696
|
+
>
|
|
1697
|
+
<svg
|
|
1698
|
+
className="h-5 w-5"
|
|
1699
|
+
fill="none"
|
|
1700
|
+
stroke="currentColor"
|
|
1701
|
+
viewBox="0 0 24 24"
|
|
1702
|
+
>
|
|
1703
|
+
<path
|
|
1704
|
+
strokeLinecap="round"
|
|
1705
|
+
strokeLinejoin="round"
|
|
1706
|
+
strokeWidth={2}
|
|
1707
|
+
d="M6 18L18 6M6 6l12 12"
|
|
1708
|
+
/>
|
|
1709
|
+
</svg>
|
|
1710
|
+
</button>
|
|
1711
|
+
</div>
|
|
1712
|
+
</div>
|
|
1713
|
+
)}
|
|
1714
|
+
|
|
1715
|
+
{companyError && (
|
|
1716
|
+
<div className="mt-4 rounded-lg bg-indigo-50 p-4 text-sm text-indigo-600">
|
|
1717
|
+
{companyError}
|
|
1718
|
+
</div>
|
|
1719
|
+
)}
|
|
1720
|
+
|
|
1721
|
+
{companyLoading ? (
|
|
1722
|
+
<div className="mt-6 text-center text-gray-500">Chargement...</div>
|
|
1723
|
+
) : (
|
|
1724
|
+
<form onSubmit={handleCompanySubmit} className="mt-6 space-y-4">
|
|
1725
|
+
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
1726
|
+
<div>
|
|
1727
|
+
<label className="block text-sm font-medium text-gray-700">
|
|
1728
|
+
Nom de l'entreprise *
|
|
1729
|
+
</label>
|
|
1730
|
+
<input
|
|
1731
|
+
type="text"
|
|
1732
|
+
required
|
|
1733
|
+
value={companyData.name}
|
|
1734
|
+
onChange={(e) =>
|
|
1735
|
+
setCompanyData({ ...companyData, name: e.target.value })
|
|
1736
|
+
}
|
|
1737
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
|
1738
|
+
placeholder="Nom de l'entreprise"
|
|
1739
|
+
/>
|
|
1740
|
+
</div>
|
|
1741
|
+
|
|
1742
|
+
<div>
|
|
1743
|
+
<label className="block text-sm font-medium text-gray-700">Email</label>
|
|
1744
|
+
<input
|
|
1745
|
+
type="email"
|
|
1746
|
+
value={companyData.email}
|
|
1747
|
+
onChange={(e) =>
|
|
1748
|
+
setCompanyData({ ...companyData, email: e.target.value })
|
|
1749
|
+
}
|
|
1750
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
|
1751
|
+
placeholder="contact@entreprise.com"
|
|
1752
|
+
/>
|
|
1753
|
+
</div>
|
|
1754
|
+
|
|
1755
|
+
<div>
|
|
1756
|
+
<label className="block text-sm font-medium text-gray-700">
|
|
1757
|
+
Téléphone
|
|
1758
|
+
</label>
|
|
1759
|
+
<input
|
|
1760
|
+
type="tel"
|
|
1761
|
+
value={companyData.phone}
|
|
1762
|
+
onChange={(e) =>
|
|
1763
|
+
setCompanyData({ ...companyData, phone: e.target.value })
|
|
1764
|
+
}
|
|
1765
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
|
1766
|
+
placeholder="+33 1 23 45 67 89"
|
|
1767
|
+
/>
|
|
1768
|
+
</div>
|
|
1769
|
+
|
|
1770
|
+
<div>
|
|
1771
|
+
<label className="block text-sm font-medium text-gray-700">
|
|
1772
|
+
Site web
|
|
1773
|
+
</label>
|
|
1774
|
+
<input
|
|
1775
|
+
type="url"
|
|
1776
|
+
value={companyData.website}
|
|
1777
|
+
onChange={(e) =>
|
|
1778
|
+
setCompanyData({ ...companyData, website: 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:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
|
1781
|
+
placeholder="https://www.entreprise.com"
|
|
1782
|
+
/>
|
|
1783
|
+
</div>
|
|
1784
|
+
|
|
1785
|
+
<div className="md:col-span-2">
|
|
1786
|
+
<label className="block text-sm font-medium text-gray-700">
|
|
1787
|
+
Adresse
|
|
1788
|
+
</label>
|
|
1789
|
+
<input
|
|
1790
|
+
type="text"
|
|
1791
|
+
value={companyData.address}
|
|
1792
|
+
onChange={(e) =>
|
|
1793
|
+
setCompanyData({ ...companyData, address: 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:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
|
1796
|
+
placeholder="123 Rue de la République"
|
|
1797
|
+
/>
|
|
1798
|
+
</div>
|
|
1799
|
+
|
|
1800
|
+
<div>
|
|
1801
|
+
<label className="block text-sm font-medium text-gray-700">Ville</label>
|
|
1802
|
+
<input
|
|
1803
|
+
type="text"
|
|
1804
|
+
value={companyData.city}
|
|
1805
|
+
onChange={(e) =>
|
|
1806
|
+
setCompanyData({ ...companyData, city: e.target.value })
|
|
1807
|
+
}
|
|
1808
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
|
1809
|
+
placeholder="Paris"
|
|
1810
|
+
/>
|
|
1811
|
+
</div>
|
|
1812
|
+
|
|
1813
|
+
<div>
|
|
1814
|
+
<label className="block text-sm font-medium text-gray-700">
|
|
1815
|
+
Code postal
|
|
1816
|
+
</label>
|
|
1817
|
+
<input
|
|
1818
|
+
type="text"
|
|
1819
|
+
value={companyData.postalCode}
|
|
1820
|
+
onChange={(e) =>
|
|
1821
|
+
setCompanyData({ ...companyData, postalCode: e.target.value })
|
|
1822
|
+
}
|
|
1823
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
|
1824
|
+
placeholder="75001"
|
|
1825
|
+
/>
|
|
1826
|
+
</div>
|
|
1827
|
+
|
|
1828
|
+
<div>
|
|
1829
|
+
<label className="block text-sm font-medium text-gray-700">Pays</label>
|
|
1830
|
+
<input
|
|
1831
|
+
type="text"
|
|
1832
|
+
value={companyData.country}
|
|
1833
|
+
onChange={(e) =>
|
|
1834
|
+
setCompanyData({ ...companyData, country: e.target.value })
|
|
1835
|
+
}
|
|
1836
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
|
1837
|
+
placeholder="France"
|
|
1838
|
+
/>
|
|
1839
|
+
</div>
|
|
1840
|
+
|
|
1841
|
+
<div>
|
|
1842
|
+
<label className="block text-sm font-medium text-gray-700">SIRET</label>
|
|
1843
|
+
<input
|
|
1844
|
+
type="text"
|
|
1845
|
+
value={companyData.siret}
|
|
1846
|
+
onChange={(e) =>
|
|
1847
|
+
setCompanyData({ ...companyData, siret: e.target.value })
|
|
1848
|
+
}
|
|
1849
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
|
1850
|
+
placeholder="123 456 789 00012"
|
|
1851
|
+
/>
|
|
1852
|
+
</div>
|
|
1853
|
+
|
|
1854
|
+
<div className="md:col-span-2">
|
|
1855
|
+
<label className="block text-sm font-medium text-gray-700">
|
|
1856
|
+
Numéro TVA
|
|
1857
|
+
</label>
|
|
1858
|
+
<input
|
|
1859
|
+
type="text"
|
|
1860
|
+
value={companyData.vatNumber}
|
|
1861
|
+
onChange={(e) =>
|
|
1862
|
+
setCompanyData({ ...companyData, vatNumber: e.target.value })
|
|
1863
|
+
}
|
|
1864
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
|
1865
|
+
placeholder="FR12 345678901"
|
|
1866
|
+
/>
|
|
1867
|
+
</div>
|
|
1868
|
+
|
|
1869
|
+
<div className="md:col-span-2">
|
|
1870
|
+
<label className="block text-sm font-medium text-gray-700">
|
|
1871
|
+
URL du logo
|
|
1872
|
+
</label>
|
|
1873
|
+
<input
|
|
1874
|
+
type="url"
|
|
1875
|
+
value={companyData.logo}
|
|
1876
|
+
onChange={(e) =>
|
|
1877
|
+
setCompanyData({ ...companyData, logo: e.target.value })
|
|
1878
|
+
}
|
|
1879
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
|
1880
|
+
placeholder="https://example.com/logo.png"
|
|
1881
|
+
/>
|
|
1882
|
+
</div>
|
|
1883
|
+
</div>
|
|
1884
|
+
|
|
1885
|
+
<div className="flex justify-end gap-3 pt-4">
|
|
1886
|
+
<button
|
|
1887
|
+
type="submit"
|
|
1888
|
+
disabled={companySaving}
|
|
1889
|
+
className="cursor-pointer rounded-lg bg-indigo-600 px-6 py-2 text-sm font-medium text-white transition-colors hover:bg-indigo-700 focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
|
1890
|
+
>
|
|
1891
|
+
{companySaving ? 'Enregistrement...' : 'Enregistrer'}
|
|
1892
|
+
</button>
|
|
1893
|
+
</div>
|
|
1894
|
+
</form>
|
|
1895
|
+
)}
|
|
1896
|
+
</div>
|
|
1897
|
+
)}
|
|
1898
|
+
</div>
|
|
1899
|
+
)}
|
|
1900
|
+
|
|
1901
|
+
{/* Section Configuration SMTP - Paramètres Système */}
|
|
1902
|
+
{mainSection === 'system' && (
|
|
1903
|
+
<div className="rounded-lg bg-white p-6 shadow-sm">
|
|
1904
|
+
<div className="flex items-center justify-between">
|
|
1905
|
+
<div>
|
|
1906
|
+
<h2 className="text-lg font-bold text-gray-900">Configuration SMTP</h2>
|
|
1907
|
+
<p className="mt-1 text-sm text-gray-600">
|
|
1908
|
+
Configurez votre serveur SMTP pour envoyer des emails avec votre email de
|
|
1909
|
+
société
|
|
1910
|
+
</p>
|
|
1911
|
+
</div>
|
|
1912
|
+
{smtpConfigured && (
|
|
1913
|
+
<div className="flex items-center gap-2 rounded-full bg-green-100 px-3 py-1.5">
|
|
1914
|
+
<svg
|
|
1915
|
+
className="h-4 w-4 text-green-600"
|
|
1916
|
+
fill="none"
|
|
1917
|
+
stroke="currentColor"
|
|
1918
|
+
viewBox="0 0 24 24"
|
|
1919
|
+
>
|
|
1920
|
+
<path
|
|
1921
|
+
strokeLinecap="round"
|
|
1922
|
+
strokeLinejoin="round"
|
|
1923
|
+
strokeWidth={2}
|
|
1924
|
+
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
1925
|
+
/>
|
|
1926
|
+
</svg>
|
|
1927
|
+
<span className="text-xs font-medium text-green-800">Fonctionnel</span>
|
|
1928
|
+
</div>
|
|
1929
|
+
)}
|
|
1930
|
+
</div>
|
|
1931
|
+
|
|
1932
|
+
{smtpSuccess && (
|
|
1933
|
+
<div className="mt-4 rounded-lg border border-green-200 bg-green-50 p-4">
|
|
1934
|
+
<div className="flex items-center">
|
|
1935
|
+
<svg
|
|
1936
|
+
className="mr-3 h-5 w-5 text-green-600"
|
|
1937
|
+
fill="none"
|
|
1938
|
+
stroke="currentColor"
|
|
1939
|
+
viewBox="0 0 24 24"
|
|
1940
|
+
>
|
|
1941
|
+
<path
|
|
1942
|
+
strokeLinecap="round"
|
|
1943
|
+
strokeLinejoin="round"
|
|
1944
|
+
strokeWidth={2}
|
|
1945
|
+
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
1946
|
+
/>
|
|
1947
|
+
</svg>
|
|
1948
|
+
<p className="text-sm font-medium text-green-800">{smtpSuccess}</p>
|
|
1949
|
+
<button
|
|
1950
|
+
onClick={() => setSmtpSuccess('')}
|
|
1951
|
+
className="ml-auto cursor-pointer text-green-600 hover:text-green-800"
|
|
1952
|
+
>
|
|
1953
|
+
<svg
|
|
1954
|
+
className="h-5 w-5"
|
|
1955
|
+
fill="none"
|
|
1956
|
+
stroke="currentColor"
|
|
1957
|
+
viewBox="0 0 24 24"
|
|
1958
|
+
>
|
|
1959
|
+
<path
|
|
1960
|
+
strokeLinecap="round"
|
|
1961
|
+
strokeLinejoin="round"
|
|
1962
|
+
strokeWidth={2}
|
|
1963
|
+
d="M6 18L18 6M6 6l12 12"
|
|
1964
|
+
/>
|
|
1965
|
+
</svg>
|
|
1966
|
+
</button>
|
|
1967
|
+
</div>
|
|
1968
|
+
</div>
|
|
1969
|
+
)}
|
|
1970
|
+
|
|
1971
|
+
{smtpError && (
|
|
1972
|
+
<div className="mt-4 rounded-lg bg-indigo-50 p-4 text-sm text-indigo-600">
|
|
1973
|
+
{smtpError}
|
|
1974
|
+
</div>
|
|
1975
|
+
)}
|
|
1976
|
+
|
|
1977
|
+
{smtpTestResult && (
|
|
1978
|
+
<div
|
|
1979
|
+
className={cn(
|
|
1980
|
+
'mt-4 rounded-lg p-4',
|
|
1981
|
+
smtpTestResult.success
|
|
1982
|
+
? 'border border-green-200 bg-green-50'
|
|
1983
|
+
: 'border border-indigo-200 bg-indigo-50',
|
|
1984
|
+
)}
|
|
1985
|
+
>
|
|
1986
|
+
<div className="flex items-center">
|
|
1987
|
+
{smtpTestResult.success ? (
|
|
1988
|
+
<svg
|
|
1989
|
+
className="mr-3 h-5 w-5 text-green-600"
|
|
1990
|
+
fill="none"
|
|
1991
|
+
stroke="currentColor"
|
|
1992
|
+
viewBox="0 0 24 24"
|
|
1993
|
+
>
|
|
1994
|
+
<path
|
|
1995
|
+
strokeLinecap="round"
|
|
1996
|
+
strokeLinejoin="round"
|
|
1997
|
+
strokeWidth={2}
|
|
1998
|
+
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
1999
|
+
/>
|
|
2000
|
+
</svg>
|
|
2001
|
+
) : (
|
|
2002
|
+
<svg
|
|
2003
|
+
className="mr-3 h-5 w-5 text-indigo-600"
|
|
2004
|
+
fill="none"
|
|
2005
|
+
stroke="currentColor"
|
|
2006
|
+
viewBox="0 0 24 24"
|
|
2007
|
+
>
|
|
2008
|
+
<path
|
|
2009
|
+
strokeLinecap="round"
|
|
2010
|
+
strokeLinejoin="round"
|
|
2011
|
+
strokeWidth={2}
|
|
2012
|
+
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
2013
|
+
/>
|
|
2014
|
+
</svg>
|
|
2015
|
+
)}
|
|
2016
|
+
<p
|
|
2017
|
+
className={cn(
|
|
2018
|
+
'text-sm font-medium',
|
|
2019
|
+
smtpTestResult.success ? 'text-green-800' : 'text-indigo-800',
|
|
2020
|
+
)}
|
|
2021
|
+
>
|
|
2022
|
+
{smtpTestResult.message}
|
|
2023
|
+
</p>
|
|
2024
|
+
<button
|
|
2025
|
+
onClick={() => setSmtpTestResult(null)}
|
|
2026
|
+
className="ml-auto cursor-pointer text-gray-600 hover:text-gray-800"
|
|
2027
|
+
>
|
|
2028
|
+
<svg
|
|
2029
|
+
className="h-5 w-5"
|
|
2030
|
+
fill="none"
|
|
2031
|
+
stroke="currentColor"
|
|
2032
|
+
viewBox="0 0 24 24"
|
|
2033
|
+
>
|
|
2034
|
+
<path
|
|
2035
|
+
strokeLinecap="round"
|
|
2036
|
+
strokeLinejoin="round"
|
|
2037
|
+
strokeWidth={2}
|
|
2038
|
+
d="M6 18L18 6M6 6l12 12"
|
|
2039
|
+
/>
|
|
2040
|
+
</svg>
|
|
2041
|
+
</button>
|
|
2042
|
+
</div>
|
|
2043
|
+
</div>
|
|
2044
|
+
)}
|
|
2045
|
+
|
|
2046
|
+
{smtpLoading ? (
|
|
2047
|
+
<div className="mt-6 text-center text-gray-500">Chargement...</div>
|
|
2048
|
+
) : (
|
|
2049
|
+
<form onSubmit={handleSmtpSubmit} className="mt-6 space-y-4">
|
|
2050
|
+
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
2051
|
+
<div>
|
|
2052
|
+
<label className="block text-sm font-medium text-gray-700">
|
|
2053
|
+
Serveur SMTP (Host) *
|
|
2054
|
+
</label>
|
|
2055
|
+
<input
|
|
2056
|
+
type="text"
|
|
2057
|
+
required
|
|
2058
|
+
value={smtpData.host}
|
|
2059
|
+
onChange={(e) => setSmtpData({ ...smtpData, host: e.target.value })}
|
|
2060
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
|
2061
|
+
placeholder="smtp.gmail.com"
|
|
2062
|
+
/>
|
|
2063
|
+
</div>
|
|
2064
|
+
|
|
2065
|
+
<div>
|
|
2066
|
+
<label className="block text-sm font-medium text-gray-700">Port *</label>
|
|
2067
|
+
<input
|
|
2068
|
+
type="number"
|
|
2069
|
+
required
|
|
2070
|
+
min="1"
|
|
2071
|
+
max="65535"
|
|
2072
|
+
value={smtpData.port}
|
|
2073
|
+
onChange={(e) => setSmtpData({ ...smtpData, port: e.target.value })}
|
|
2074
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
|
2075
|
+
placeholder="587"
|
|
2076
|
+
/>
|
|
2077
|
+
<p className="mt-1 text-xs text-gray-500">
|
|
2078
|
+
Ports courants : 587 (TLS), 465 (SSL), 25 (non sécurisé)
|
|
2079
|
+
</p>
|
|
2080
|
+
</div>
|
|
2081
|
+
|
|
2082
|
+
<div className="flex items-center">
|
|
2083
|
+
<input
|
|
2084
|
+
type="checkbox"
|
|
2085
|
+
id="smtp-secure"
|
|
2086
|
+
checked={smtpData.secure}
|
|
2087
|
+
onChange={(e) => setSmtpData({ ...smtpData, secure: e.target.checked })}
|
|
2088
|
+
className="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
|
2089
|
+
/>
|
|
2090
|
+
<label
|
|
2091
|
+
htmlFor="smtp-secure"
|
|
2092
|
+
className="ml-2 text-sm font-medium text-gray-700"
|
|
2093
|
+
>
|
|
2094
|
+
Connexion sécurisée (SSL/TLS)
|
|
2095
|
+
</label>
|
|
2096
|
+
</div>
|
|
2097
|
+
|
|
2098
|
+
<div className="md:col-span-2">
|
|
2099
|
+
<label className="block text-sm font-medium text-gray-700">
|
|
2100
|
+
Nom d'utilisateur *
|
|
2101
|
+
</label>
|
|
2102
|
+
<input
|
|
2103
|
+
type="text"
|
|
2104
|
+
required
|
|
2105
|
+
value={smtpData.username}
|
|
2106
|
+
onChange={(e) => setSmtpData({ ...smtpData, username: e.target.value })}
|
|
2107
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
|
2108
|
+
placeholder="votre.email@exemple.com"
|
|
2109
|
+
/>
|
|
2110
|
+
</div>
|
|
2111
|
+
|
|
2112
|
+
<div className="md:col-span-2">
|
|
2113
|
+
<label className="block text-sm font-medium text-gray-700">
|
|
2114
|
+
Mot de passe *
|
|
2115
|
+
</label>
|
|
2116
|
+
<div className="relative mt-1">
|
|
2117
|
+
<input
|
|
2118
|
+
type={showSmtpPassword ? 'text' : 'password'}
|
|
2119
|
+
required
|
|
2120
|
+
value={smtpData.password}
|
|
2121
|
+
onChange={(e) => setSmtpData({ ...smtpData, password: e.target.value })}
|
|
2122
|
+
className="block w-full rounded-lg border border-gray-300 px-4 py-2 pr-10 text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
|
2123
|
+
placeholder="••••••••"
|
|
2124
|
+
/>
|
|
2125
|
+
<button
|
|
2126
|
+
type="button"
|
|
2127
|
+
onClick={() => setShowSmtpPassword((prev) => !prev)}
|
|
2128
|
+
className="absolute inset-y-0 right-0 flex cursor-pointer items-center pr-3 text-gray-400 hover:text-gray-600"
|
|
2129
|
+
aria-label={
|
|
2130
|
+
showSmtpPassword
|
|
2131
|
+
? 'Masquer le mot de passe'
|
|
2132
|
+
: 'Afficher le mot de passe'
|
|
2133
|
+
}
|
|
2134
|
+
>
|
|
2135
|
+
{showSmtpPassword ? <EyeOff /> : <Eye />}
|
|
2136
|
+
</button>
|
|
2137
|
+
</div>
|
|
2138
|
+
<p className="mt-1 text-xs text-gray-500">
|
|
2139
|
+
Pour Gmail, utilisez un mot de passe d'application
|
|
2140
|
+
</p>
|
|
2141
|
+
</div>
|
|
2142
|
+
|
|
2143
|
+
<div>
|
|
2144
|
+
<label className="block text-sm font-medium text-gray-700">
|
|
2145
|
+
Email expéditeur (From) *
|
|
2146
|
+
</label>
|
|
2147
|
+
<input
|
|
2148
|
+
type="email"
|
|
2149
|
+
required
|
|
2150
|
+
value={smtpData.fromEmail}
|
|
2151
|
+
onChange={(e) => setSmtpData({ ...smtpData, fromEmail: e.target.value })}
|
|
2152
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
|
2153
|
+
placeholder="votre.email@exemple.com"
|
|
2154
|
+
/>
|
|
2155
|
+
</div>
|
|
2156
|
+
|
|
2157
|
+
<div>
|
|
2158
|
+
<label className="block text-sm font-medium text-gray-700">
|
|
2159
|
+
Nom expéditeur (optionnel)
|
|
2160
|
+
</label>
|
|
2161
|
+
<input
|
|
2162
|
+
type="text"
|
|
2163
|
+
value={smtpData.fromName}
|
|
2164
|
+
onChange={(e) => setSmtpData({ ...smtpData, fromName: e.target.value })}
|
|
2165
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
|
2166
|
+
placeholder="Votre Nom"
|
|
2167
|
+
/>
|
|
2168
|
+
</div>
|
|
2169
|
+
</div>
|
|
2170
|
+
|
|
2171
|
+
<div className="mt-4 rounded-lg bg-indigo-50 p-3 text-xs text-indigo-900">
|
|
2172
|
+
<p className="font-medium">Utiliser Gmail avec SMTP</p>
|
|
2173
|
+
<p className="mt-1">
|
|
2174
|
+
Si vous utilisez une adresse Gmail, vous devez créer un{' '}
|
|
2175
|
+
<span className="font-semibold">mot de passe d'application</span> dédié
|
|
2176
|
+
et le renseigner dans le champ "Mot de passe" ci-dessus.
|
|
2177
|
+
Rendez-vous sur{' '}
|
|
2178
|
+
<Link
|
|
2179
|
+
href="https://myaccount.google.com/apppasswords"
|
|
2180
|
+
target="_blank"
|
|
2181
|
+
rel="noreferrer"
|
|
2182
|
+
className="font-semibold underline"
|
|
2183
|
+
>
|
|
2184
|
+
la page des mots de passe d'application Google
|
|
2185
|
+
</Link>{' '}
|
|
2186
|
+
pour en générer un (compte Google protégé par la validation en deux étapes
|
|
2187
|
+
requis).
|
|
2188
|
+
</p>
|
|
2189
|
+
</div>
|
|
2190
|
+
|
|
2191
|
+
<div className="mt-6 rounded-lg border border-dashed border-gray-200 bg-gray-50 p-4">
|
|
2192
|
+
<label className="mb-2 block text-sm font-medium text-gray-700">
|
|
2193
|
+
Votre signature (optionnel)
|
|
2194
|
+
</label>
|
|
2195
|
+
<p className="mb-2 text-xs text-gray-500">
|
|
2196
|
+
Cette signature sera ajoutée à la fin de tous les emails envoyés avec cette
|
|
2197
|
+
configuration SMTP (par exemple : nom, fonction, coordonnées, mentions
|
|
2198
|
+
légales…).
|
|
2199
|
+
</p>
|
|
2200
|
+
<div className="mt-1">
|
|
2201
|
+
<Editor
|
|
2202
|
+
ref={smtpSignatureEditorRef}
|
|
2203
|
+
onReady={(methods) => {
|
|
2204
|
+
// garder la ref synchronisée
|
|
2205
|
+
smtpSignatureEditorRef.current = methods;
|
|
2206
|
+
// injecter la signature existante dès que l’éditeur est prêt
|
|
2207
|
+
if (smtpData.signature) {
|
|
2208
|
+
methods.injectHTML(smtpData.signature);
|
|
2209
|
+
}
|
|
2210
|
+
}}
|
|
2211
|
+
/>
|
|
2212
|
+
</div>
|
|
2213
|
+
</div>
|
|
2214
|
+
|
|
2215
|
+
<div className="flex flex-col gap-3 pt-4 sm:flex-row sm:justify-end">
|
|
2216
|
+
<button
|
|
2217
|
+
type="button"
|
|
2218
|
+
onClick={handleSmtpTest}
|
|
2219
|
+
disabled={smtpTesting || smtpSaving}
|
|
2220
|
+
className="w-full cursor-pointer rounded-lg border border-indigo-600 px-6 py-2 text-sm font-medium text-indigo-600 transition-colors hover:bg-indigo-50 focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto"
|
|
2221
|
+
>
|
|
2222
|
+
{smtpTesting ? 'Test en cours...' : 'Tester la connexion'}
|
|
2223
|
+
</button>
|
|
2224
|
+
<button
|
|
2225
|
+
type="submit"
|
|
2226
|
+
disabled={smtpSaving || smtpTesting}
|
|
2227
|
+
className="w-full cursor-pointer rounded-lg bg-indigo-600 px-6 py-2 text-sm font-medium text-white transition-colors hover:bg-indigo-700 focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto"
|
|
2228
|
+
>
|
|
2229
|
+
{smtpSaving ? 'Enregistrement...' : 'Enregistrer'}
|
|
2230
|
+
</button>
|
|
2231
|
+
</div>
|
|
2232
|
+
</form>
|
|
2233
|
+
)}
|
|
2234
|
+
</div>
|
|
2235
|
+
)}
|
|
2236
|
+
|
|
2237
|
+
{/* Section Gestion des statuts - Paramètres de l'Application */}
|
|
2238
|
+
{mainSection === 'app' && (
|
|
2239
|
+
<>
|
|
2240
|
+
{isAdmin ? (
|
|
2241
|
+
<div className="space-y-6">
|
|
2242
|
+
<div className="rounded-lg bg-white p-6 shadow-sm">
|
|
2243
|
+
<div className="flex items-center justify-between">
|
|
2244
|
+
<div>
|
|
2245
|
+
<h2 className="text-lg font-bold text-gray-900">Gestion des statuts</h2>
|
|
2246
|
+
<p className="mt-1 text-sm text-gray-600">
|
|
2247
|
+
Gérez les statuts pour catégoriser vos contacts
|
|
2248
|
+
</p>
|
|
2249
|
+
</div>
|
|
2250
|
+
{!showStatusForm && (
|
|
2251
|
+
<button
|
|
2252
|
+
onClick={() => {
|
|
2253
|
+
setShowStatusForm(true);
|
|
2254
|
+
setEditingStatus(null);
|
|
2255
|
+
setStatusFormData({ name: '', color: '#3B82F6' });
|
|
2256
|
+
setStatusError('');
|
|
2257
|
+
setStatusSuccess('');
|
|
2258
|
+
}}
|
|
2259
|
+
className="cursor-pointer rounded-lg bg-indigo-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-indigo-700"
|
|
2260
|
+
>
|
|
2261
|
+
+ Ajouter un statut
|
|
2262
|
+
</button>
|
|
2263
|
+
)}
|
|
2264
|
+
</div>
|
|
2265
|
+
|
|
2266
|
+
{statusSuccess && (
|
|
2267
|
+
<div className="mt-4 rounded-lg border border-green-200 bg-green-50 p-4">
|
|
2268
|
+
<div className="flex items-center">
|
|
2269
|
+
<svg
|
|
2270
|
+
className="mr-3 h-5 w-5 text-green-600"
|
|
2271
|
+
fill="none"
|
|
2272
|
+
stroke="currentColor"
|
|
2273
|
+
viewBox="0 0 24 24"
|
|
2274
|
+
>
|
|
2275
|
+
<path
|
|
2276
|
+
strokeLinecap="round"
|
|
2277
|
+
strokeLinejoin="round"
|
|
2278
|
+
strokeWidth={2}
|
|
2279
|
+
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
2280
|
+
/>
|
|
2281
|
+
</svg>
|
|
2282
|
+
<p className="text-sm font-medium text-green-800">{statusSuccess}</p>
|
|
2283
|
+
<button
|
|
2284
|
+
onClick={() => setStatusSuccess('')}
|
|
2285
|
+
className="ml-auto cursor-pointer text-green-600 hover:text-green-800"
|
|
2286
|
+
>
|
|
2287
|
+
<svg
|
|
2288
|
+
className="h-5 w-5"
|
|
2289
|
+
fill="none"
|
|
2290
|
+
stroke="currentColor"
|
|
2291
|
+
viewBox="0 0 24 24"
|
|
2292
|
+
>
|
|
2293
|
+
<path
|
|
2294
|
+
strokeLinecap="round"
|
|
2295
|
+
strokeLinejoin="round"
|
|
2296
|
+
strokeWidth={2}
|
|
2297
|
+
d="M6 18L18 6M6 6l12 12"
|
|
2298
|
+
/>
|
|
2299
|
+
</svg>
|
|
2300
|
+
</button>
|
|
2301
|
+
</div>
|
|
2302
|
+
</div>
|
|
2303
|
+
)}
|
|
2304
|
+
|
|
2305
|
+
{statusError && (
|
|
2306
|
+
<div className="mt-4 rounded-lg bg-indigo-50 p-4 text-sm text-indigo-600">
|
|
2307
|
+
{statusError}
|
|
2308
|
+
</div>
|
|
2309
|
+
)}
|
|
2310
|
+
|
|
2311
|
+
{showStatusForm ? (
|
|
2312
|
+
<form onSubmit={handleStatusSubmit} className="mt-6 space-y-4">
|
|
2313
|
+
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
2314
|
+
<div>
|
|
2315
|
+
<label className="block text-sm font-medium text-gray-700">
|
|
2316
|
+
Nom du statut *
|
|
2317
|
+
</label>
|
|
2318
|
+
<input
|
|
2319
|
+
type="text"
|
|
2320
|
+
required
|
|
2321
|
+
value={statusFormData.name}
|
|
2322
|
+
onChange={(e) =>
|
|
2323
|
+
setStatusFormData({ ...statusFormData, name: e.target.value })
|
|
2324
|
+
}
|
|
2325
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
|
2326
|
+
placeholder="Ex: Nouveau"
|
|
2327
|
+
/>
|
|
2328
|
+
</div>
|
|
2329
|
+
|
|
2330
|
+
<div>
|
|
2331
|
+
<label className="block text-sm font-medium text-gray-700">
|
|
2332
|
+
Couleur *
|
|
2333
|
+
</label>
|
|
2334
|
+
<div className="mt-1 flex items-center gap-3">
|
|
2335
|
+
<input
|
|
2336
|
+
type="color"
|
|
2337
|
+
value={statusFormData.color}
|
|
2338
|
+
onChange={(e) =>
|
|
2339
|
+
setStatusFormData({ ...statusFormData, color: e.target.value })
|
|
2340
|
+
}
|
|
2341
|
+
className="h-10 w-20 cursor-pointer rounded-lg border border-gray-300"
|
|
2342
|
+
/>
|
|
2343
|
+
<input
|
|
2344
|
+
type="text"
|
|
2345
|
+
value={statusFormData.color}
|
|
2346
|
+
onChange={(e) =>
|
|
2347
|
+
setStatusFormData({ ...statusFormData, color: e.target.value })
|
|
2348
|
+
}
|
|
2349
|
+
className="block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
|
2350
|
+
placeholder="#3B82F6"
|
|
2351
|
+
/>
|
|
2352
|
+
</div>
|
|
2353
|
+
</div>
|
|
2354
|
+
</div>
|
|
2355
|
+
|
|
2356
|
+
<div className="flex flex-col gap-3 pt-4 sm:flex-row sm:justify-end">
|
|
2357
|
+
<button
|
|
2358
|
+
type="button"
|
|
2359
|
+
onClick={() => {
|
|
2360
|
+
setShowStatusForm(false);
|
|
2361
|
+
setEditingStatus(null);
|
|
2362
|
+
setStatusFormData({ name: '', color: '#3B82F6' });
|
|
2363
|
+
setStatusError('');
|
|
2364
|
+
setStatusSuccess('');
|
|
2365
|
+
}}
|
|
2366
|
+
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"
|
|
2367
|
+
>
|
|
2368
|
+
Annuler
|
|
2369
|
+
</button>
|
|
2370
|
+
<button
|
|
2371
|
+
type="submit"
|
|
2372
|
+
disabled={statusSaving}
|
|
2373
|
+
className="w-full cursor-pointer rounded-lg bg-indigo-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-indigo-700 focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto"
|
|
2374
|
+
>
|
|
2375
|
+
{statusSaving
|
|
2376
|
+
? 'Enregistrement...'
|
|
2377
|
+
: editingStatus
|
|
2378
|
+
? 'Modifier'
|
|
2379
|
+
: 'Créer'}
|
|
2380
|
+
</button>
|
|
2381
|
+
</div>
|
|
2382
|
+
</form>
|
|
2383
|
+
) : (
|
|
2384
|
+
<div className="mt-6">
|
|
2385
|
+
{statusesLoading ? (
|
|
2386
|
+
<div className="text-center text-gray-500">Chargement...</div>
|
|
2387
|
+
) : statuses.length === 0 ? (
|
|
2388
|
+
<div className="text-center text-gray-500">
|
|
2389
|
+
Aucun statut. Cliquez sur "Ajouter un statut" pour en créer
|
|
2390
|
+
un.
|
|
2391
|
+
</div>
|
|
2392
|
+
) : (
|
|
2393
|
+
<div className="space-y-2">
|
|
2394
|
+
{statuses.map((status) => (
|
|
2395
|
+
<div
|
|
2396
|
+
key={status.id}
|
|
2397
|
+
className="flex items-center justify-between rounded-lg border border-gray-200 p-4"
|
|
2398
|
+
>
|
|
2399
|
+
<div className="flex items-center gap-3">
|
|
2400
|
+
<div
|
|
2401
|
+
className="h-6 w-6 rounded-full"
|
|
2402
|
+
style={{ backgroundColor: status.color }}
|
|
2403
|
+
/>
|
|
2404
|
+
<span className="font-medium text-gray-900">{status.name}</span>
|
|
2405
|
+
</div>
|
|
2406
|
+
<div className="flex items-center gap-2">
|
|
2407
|
+
<button
|
|
2408
|
+
onClick={() => handleStatusEdit(status)}
|
|
2409
|
+
className="cursor-pointer rounded-lg border border-gray-300 px-3 py-1.5 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-50"
|
|
2410
|
+
>
|
|
2411
|
+
Modifier
|
|
2412
|
+
</button>
|
|
2413
|
+
<button
|
|
2414
|
+
onClick={() => handleStatusDelete(status.id)}
|
|
2415
|
+
className="cursor-pointer rounded-lg border border-indigo-300 px-3 py-1.5 text-sm font-medium text-indigo-700 transition-colors hover:bg-indigo-50"
|
|
2416
|
+
>
|
|
2417
|
+
Supprimer
|
|
2418
|
+
</button>
|
|
2419
|
+
</div>
|
|
2420
|
+
</div>
|
|
2421
|
+
))}
|
|
2422
|
+
</div>
|
|
2423
|
+
)}
|
|
2424
|
+
</div>
|
|
2425
|
+
)}
|
|
2426
|
+
</div>
|
|
2427
|
+
|
|
2428
|
+
{/* Section Motifs de fermeture - Paramètres de l'Application */}
|
|
2429
|
+
<div className="rounded-lg bg-white p-6 shadow-sm">
|
|
2430
|
+
<div className="flex items-center justify-between gap-2">
|
|
2431
|
+
<div>
|
|
2432
|
+
<h2 className="text-lg font-bold text-gray-900">Motifs de fermeture</h2>
|
|
2433
|
+
<p className="mt-1 text-sm text-gray-600">
|
|
2434
|
+
Gérez la liste des motifs de fermeture utilisés lorsque les contacts
|
|
2435
|
+
passent au statut "Fermé".
|
|
2436
|
+
</p>
|
|
2437
|
+
</div>
|
|
2438
|
+
{!showClosingReasonForm && (
|
|
2439
|
+
<button
|
|
2440
|
+
onClick={() => {
|
|
2441
|
+
setShowClosingReasonForm(true);
|
|
2442
|
+
setClosingReasonFormData({ id: null, name: '' });
|
|
2443
|
+
setClosingReasonError('');
|
|
2444
|
+
setClosingReasonSuccess('');
|
|
2445
|
+
}}
|
|
2446
|
+
className="cursor-pointer rounded-lg bg-indigo-600 px-4 py-2 text-sm font-medium text-nowrap text-white transition-colors hover:bg-indigo-700"
|
|
2447
|
+
>
|
|
2448
|
+
+ Ajouter un motif
|
|
2449
|
+
</button>
|
|
2450
|
+
)}
|
|
2451
|
+
</div>
|
|
2452
|
+
|
|
2453
|
+
{closingReasonSuccess && (
|
|
2454
|
+
<div className="mt-4 rounded-lg border border-green-200 bg-green-50 p-4">
|
|
2455
|
+
<div className="flex items-center">
|
|
2456
|
+
<svg
|
|
2457
|
+
className="mr-3 h-5 w-5 text-green-600"
|
|
2458
|
+
fill="none"
|
|
2459
|
+
stroke="currentColor"
|
|
2460
|
+
viewBox="0 0 24 24"
|
|
2461
|
+
>
|
|
2462
|
+
<path
|
|
2463
|
+
strokeLinecap="round"
|
|
2464
|
+
strokeLinejoin="round"
|
|
2465
|
+
strokeWidth={2}
|
|
2466
|
+
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
2467
|
+
/>
|
|
2468
|
+
</svg>
|
|
2469
|
+
<p className="text-sm font-medium text-green-800">
|
|
2470
|
+
{closingReasonSuccess}
|
|
2471
|
+
</p>
|
|
2472
|
+
<button
|
|
2473
|
+
onClick={() => setClosingReasonSuccess('')}
|
|
2474
|
+
className="ml-auto cursor-pointer text-green-600 hover:text-green-800"
|
|
2475
|
+
>
|
|
2476
|
+
<svg
|
|
2477
|
+
className="h-5 w-5"
|
|
2478
|
+
fill="none"
|
|
2479
|
+
stroke="currentColor"
|
|
2480
|
+
viewBox="0 0 24 24"
|
|
2481
|
+
>
|
|
2482
|
+
<path
|
|
2483
|
+
strokeLinecap="round"
|
|
2484
|
+
strokeLinejoin="round"
|
|
2485
|
+
strokeWidth={2}
|
|
2486
|
+
d="M6 18L18 6M6 6l12 12"
|
|
2487
|
+
/>
|
|
2488
|
+
</svg>
|
|
2489
|
+
</button>
|
|
2490
|
+
</div>
|
|
2491
|
+
</div>
|
|
2492
|
+
)}
|
|
2493
|
+
|
|
2494
|
+
{closingReasonError && (
|
|
2495
|
+
<div className="mt-4 rounded-lg bg-indigo-50 p-4 text-sm text-indigo-600">
|
|
2496
|
+
{closingReasonError}
|
|
2497
|
+
</div>
|
|
2498
|
+
)}
|
|
2499
|
+
|
|
2500
|
+
{/* Formulaire d'ajout / édition */}
|
|
2501
|
+
{showClosingReasonForm && (
|
|
2502
|
+
<form
|
|
2503
|
+
className="mt-6 flex flex-col sm:flex-row sm:items-center sm:justify-between"
|
|
2504
|
+
onSubmit={async (e) => {
|
|
2505
|
+
e.preventDefault();
|
|
2506
|
+
setClosingReasonError('');
|
|
2507
|
+
setClosingReasonSuccess('');
|
|
2508
|
+
try {
|
|
2509
|
+
const isEdit = !!closingReasonFormData.id;
|
|
2510
|
+
const url = isEdit
|
|
2511
|
+
? `/api/settings/closing-reasons/${closingReasonFormData.id}`
|
|
2512
|
+
: '/api/settings/closing-reasons';
|
|
2513
|
+
const method = isEdit ? 'PUT' : 'POST';
|
|
2514
|
+
|
|
2515
|
+
const response = await fetch(url, {
|
|
2516
|
+
method,
|
|
2517
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2518
|
+
body: JSON.stringify({ name: closingReasonFormData.name }),
|
|
2519
|
+
});
|
|
2520
|
+
|
|
2521
|
+
const data = await response.json();
|
|
2522
|
+
if (!response.ok) {
|
|
2523
|
+
throw new Error(
|
|
2524
|
+
data.error || 'Erreur lors de la sauvegarde du motif',
|
|
2525
|
+
);
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
setClosingReasonSuccess(
|
|
2529
|
+
isEdit
|
|
2530
|
+
? 'Motif de fermeture mis à jour avec succès.'
|
|
2531
|
+
: 'Motif de fermeture créé avec succès.',
|
|
2532
|
+
);
|
|
2533
|
+
setClosingReasonFormData({ id: null, name: '' });
|
|
2534
|
+
setShowClosingReasonForm(false);
|
|
2535
|
+
|
|
2536
|
+
// Recharger la liste
|
|
2537
|
+
const res = await fetch('/api/settings/closing-reasons');
|
|
2538
|
+
if (res.ok) {
|
|
2539
|
+
const reasonsData = await res.json();
|
|
2540
|
+
setClosingReasons(Array.isArray(reasonsData) ? reasonsData : []);
|
|
2541
|
+
}
|
|
2542
|
+
} catch (error: any) {
|
|
2543
|
+
console.error('Erreur motif de fermeture:', error);
|
|
2544
|
+
setClosingReasonError(
|
|
2545
|
+
error.message ||
|
|
2546
|
+
'Erreur lors de la sauvegarde du motif de fermeture',
|
|
2547
|
+
);
|
|
2548
|
+
}
|
|
2549
|
+
}}
|
|
2550
|
+
>
|
|
2551
|
+
<div className="flex gap-4">
|
|
2552
|
+
<div className="w-full">
|
|
2553
|
+
<label className="block text-sm font-medium text-gray-700">
|
|
2554
|
+
Nom du motif *
|
|
2555
|
+
</label>
|
|
2556
|
+
<input
|
|
2557
|
+
type="text"
|
|
2558
|
+
required
|
|
2559
|
+
value={closingReasonFormData.name}
|
|
2560
|
+
onChange={(e) =>
|
|
2561
|
+
setClosingReasonFormData((prev) => ({
|
|
2562
|
+
...prev,
|
|
2563
|
+
name: e.target.value,
|
|
2564
|
+
}))
|
|
2565
|
+
}
|
|
2566
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
|
2567
|
+
placeholder="Ex: Faux numéro"
|
|
2568
|
+
/>
|
|
2569
|
+
</div>
|
|
2570
|
+
</div>
|
|
2571
|
+
|
|
2572
|
+
<div className="flex h-fit flex-col gap-3 pt-4 sm:flex-row sm:justify-end">
|
|
2573
|
+
<button
|
|
2574
|
+
type="button"
|
|
2575
|
+
onClick={() => {
|
|
2576
|
+
setShowClosingReasonForm(false);
|
|
2577
|
+
setClosingReasonFormData({ id: null, name: '' });
|
|
2578
|
+
setClosingReasonError('');
|
|
2579
|
+
setClosingReasonSuccess('');
|
|
2580
|
+
}}
|
|
2581
|
+
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"
|
|
2582
|
+
>
|
|
2583
|
+
Annuler
|
|
2584
|
+
</button>
|
|
2585
|
+
<button
|
|
2586
|
+
type="submit"
|
|
2587
|
+
className="w-full cursor-pointer rounded-lg bg-indigo-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-indigo-700 focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto"
|
|
2588
|
+
>
|
|
2589
|
+
{closingReasonFormData.id ? 'Mettre à jour' : 'Créer'}
|
|
2590
|
+
</button>
|
|
2591
|
+
</div>
|
|
2592
|
+
</form>
|
|
2593
|
+
)}
|
|
2594
|
+
|
|
2595
|
+
{/* Liste des motifs existants */}
|
|
2596
|
+
<div className="mt-6">
|
|
2597
|
+
{closingReasonsLoading ? (
|
|
2598
|
+
<div className="text-center text-gray-500">Chargement...</div>
|
|
2599
|
+
) : closingReasons.length === 0 ? (
|
|
2600
|
+
<div className="text-center text-gray-500">
|
|
2601
|
+
Aucun motif. Ajoutez un motif pour commencer.
|
|
2602
|
+
</div>
|
|
2603
|
+
) : (
|
|
2604
|
+
<div className="space-y-2">
|
|
2605
|
+
{closingReasons.map((reason) => (
|
|
2606
|
+
<div
|
|
2607
|
+
key={reason.id}
|
|
2608
|
+
className="flex items-center justify-between rounded-lg border border-gray-200 p-4"
|
|
2609
|
+
>
|
|
2610
|
+
<div className="flex items-center gap-3">
|
|
2611
|
+
<div className="h-6 w-6 rounded-full bg-gray-600" />
|
|
2612
|
+
<span className="font-medium text-gray-900">{reason.name}</span>
|
|
2613
|
+
</div>
|
|
2614
|
+
<div className="flex items-center gap-2">
|
|
2615
|
+
<button
|
|
2616
|
+
type="button"
|
|
2617
|
+
onClick={() => {
|
|
2618
|
+
setClosingReasonFormData({
|
|
2619
|
+
id: reason.id,
|
|
2620
|
+
name: reason.name,
|
|
2621
|
+
});
|
|
2622
|
+
setClosingReasonError('');
|
|
2623
|
+
setClosingReasonSuccess('');
|
|
2624
|
+
setShowClosingReasonForm(true);
|
|
2625
|
+
}}
|
|
2626
|
+
className="cursor-pointer rounded-lg border border-gray-300 px-3 py-1.5 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-50"
|
|
2627
|
+
>
|
|
2628
|
+
Modifier
|
|
2629
|
+
</button>
|
|
2630
|
+
<button
|
|
2631
|
+
type="button"
|
|
2632
|
+
onClick={async () => {
|
|
2633
|
+
if (
|
|
2634
|
+
!confirm(
|
|
2635
|
+
`Supprimer le motif "${reason.name}" ? Cela n'affectera pas les contacts existants.`,
|
|
2636
|
+
)
|
|
2637
|
+
) {
|
|
2638
|
+
return;
|
|
2639
|
+
}
|
|
2640
|
+
try {
|
|
2641
|
+
setClosingReasonError('');
|
|
2642
|
+
setClosingReasonSuccess('');
|
|
2643
|
+
const res = await fetch(
|
|
2644
|
+
`/api/settings/closing-reasons/${reason.id}`,
|
|
2645
|
+
{
|
|
2646
|
+
method: 'DELETE',
|
|
2647
|
+
},
|
|
2648
|
+
);
|
|
2649
|
+
const data = await res.json();
|
|
2650
|
+
if (!res.ok) {
|
|
2651
|
+
throw new Error(
|
|
2652
|
+
data.error || 'Erreur lors de la suppression du motif',
|
|
2653
|
+
);
|
|
2654
|
+
}
|
|
2655
|
+
setClosingReasonSuccess(
|
|
2656
|
+
'Motif de fermeture supprimé avec succès.',
|
|
2657
|
+
);
|
|
2658
|
+
setClosingReasons((prev) =>
|
|
2659
|
+
prev.filter((r) => r.id !== reason.id),
|
|
2660
|
+
);
|
|
2661
|
+
} catch (error: any) {
|
|
2662
|
+
console.error('Erreur suppression motif:', error);
|
|
2663
|
+
setClosingReasonError(
|
|
2664
|
+
error.message || 'Erreur lors de la suppression du motif',
|
|
2665
|
+
);
|
|
2666
|
+
}
|
|
2667
|
+
}}
|
|
2668
|
+
className="cursor-pointer rounded-lg border border-indigo-300 px-3 py-1.5 text-sm font-medium text-indigo-700 transition-colors hover:bg-indigo-50"
|
|
2669
|
+
>
|
|
2670
|
+
Supprimer
|
|
2671
|
+
</button>
|
|
2672
|
+
</div>
|
|
2673
|
+
</div>
|
|
2674
|
+
))}
|
|
2675
|
+
</div>
|
|
2676
|
+
)}
|
|
2677
|
+
</div>
|
|
2678
|
+
</div>
|
|
2679
|
+
</div>
|
|
2680
|
+
) : (
|
|
2681
|
+
<div className="rounded-lg bg-white p-6 shadow-sm">
|
|
2682
|
+
<div className="py-12 text-center">
|
|
2683
|
+
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-indigo-100">
|
|
2684
|
+
<Grid3x3 className="h-6 w-6 text-indigo-600" />
|
|
2685
|
+
</div>
|
|
2686
|
+
<h3 className="mt-4 text-lg font-semibold text-gray-900">Accès restreint</h3>
|
|
2687
|
+
<p className="mt-2 text-sm text-gray-600">
|
|
2688
|
+
Cette section est réservée aux administrateurs.
|
|
2689
|
+
</p>
|
|
2690
|
+
</div>
|
|
2691
|
+
</div>
|
|
2692
|
+
)}
|
|
2693
|
+
</>
|
|
2694
|
+
)}
|
|
2695
|
+
|
|
2696
|
+
{/* Section Intégrations */}
|
|
2697
|
+
{mainSection === 'integrations' && (
|
|
2698
|
+
<>
|
|
2699
|
+
{isAdmin ? (
|
|
2700
|
+
<div className="space-y-6">
|
|
2701
|
+
{/* Google */}
|
|
2702
|
+
<div className="rounded-lg bg-white p-6 shadow-sm">
|
|
2703
|
+
<div className="flex items-center justify-between">
|
|
2704
|
+
<div>
|
|
2705
|
+
<h2 className="text-lg font-bold text-gray-900">Intégration Google</h2>
|
|
2706
|
+
<p className="mt-1 text-sm text-gray-600">
|
|
2707
|
+
Connectez votre compte Google pour utiliser Google Calendar et Google
|
|
2708
|
+
Drive.
|
|
2709
|
+
</p>
|
|
2710
|
+
</div>
|
|
2711
|
+
</div>
|
|
2712
|
+
|
|
2713
|
+
<div className="mt-6">
|
|
2714
|
+
<div className="rounded-lg border border-gray-200 bg-white p-4 shadow-sm">
|
|
2715
|
+
<div className="flex items-center justify-between">
|
|
2716
|
+
<div className="flex items-center gap-3">
|
|
2717
|
+
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-blue-100">
|
|
2718
|
+
<Grid3x3 className="h-6 w-6 text-blue-600" />
|
|
2719
|
+
</div>
|
|
2720
|
+
<div>
|
|
2721
|
+
<h3 className="font-medium text-gray-900">
|
|
2722
|
+
Google Calendar & Drive
|
|
2723
|
+
</h3>
|
|
2724
|
+
<p className="mt-1 text-xs text-gray-500">
|
|
2725
|
+
Accédez à votre calendrier et à vos fichiers Google Drive
|
|
2726
|
+
</p>
|
|
2727
|
+
</div>
|
|
2728
|
+
</div>
|
|
2729
|
+
</div>
|
|
2730
|
+
{googleLoading ? (
|
|
2731
|
+
<div className="mt-4 text-center text-sm text-gray-500">
|
|
2732
|
+
Chargement...
|
|
2733
|
+
</div>
|
|
2734
|
+
) : (
|
|
2735
|
+
<div className="mt-4 flex items-center justify-between">
|
|
2736
|
+
{googleAccount?.connected ? (
|
|
2737
|
+
<>
|
|
2738
|
+
<div className="flex items-center gap-2">
|
|
2739
|
+
<span className="rounded-full bg-green-100 px-3 py-1 text-xs font-medium text-green-700">
|
|
2740
|
+
Connecté
|
|
2741
|
+
</span>
|
|
2742
|
+
{googleAccount.email && (
|
|
2743
|
+
<span className="text-xs text-gray-600">
|
|
2744
|
+
{googleAccount.email}
|
|
2745
|
+
</span>
|
|
2746
|
+
)}
|
|
2747
|
+
</div>
|
|
2748
|
+
<button
|
|
2749
|
+
type="button"
|
|
2750
|
+
onClick={handleGoogleDisconnect}
|
|
2751
|
+
disabled={googleDisconnecting}
|
|
2752
|
+
className="cursor-pointer rounded-lg border border-indigo-300 bg-white px-3 py-1.5 text-xs font-medium text-indigo-700 transition-colors hover:bg-indigo-50 disabled:cursor-not-allowed disabled:opacity-50"
|
|
2753
|
+
>
|
|
2754
|
+
{googleDisconnecting ? 'Déconnexion...' : 'Déconnecter'}
|
|
2755
|
+
</button>
|
|
2756
|
+
</>
|
|
2757
|
+
) : (
|
|
2758
|
+
<>
|
|
2759
|
+
<span className="rounded-full bg-gray-100 px-3 py-1 text-xs font-medium text-gray-700">
|
|
2760
|
+
Non connecté
|
|
2761
|
+
</span>
|
|
2762
|
+
<button
|
|
2763
|
+
type="button"
|
|
2764
|
+
onClick={handleGoogleConnect}
|
|
2765
|
+
className="cursor-pointer rounded-lg bg-indigo-600 px-3 py-1.5 text-xs font-medium text-white transition-colors hover:bg-indigo-700"
|
|
2766
|
+
>
|
|
2767
|
+
Connecter
|
|
2768
|
+
</button>
|
|
2769
|
+
</>
|
|
2770
|
+
)}
|
|
2771
|
+
</div>
|
|
2772
|
+
)}
|
|
2773
|
+
</div>
|
|
2774
|
+
</div>
|
|
2775
|
+
</div>
|
|
2776
|
+
|
|
2777
|
+
{/* Google Sheets */}
|
|
2778
|
+
<div className="rounded-lg bg-white p-6 shadow-sm">
|
|
2779
|
+
<div className="flex items-center justify-between">
|
|
2780
|
+
<div>
|
|
2781
|
+
<h2 className="text-lg font-bold text-gray-900">
|
|
2782
|
+
Intégration Google Sheets
|
|
2783
|
+
</h2>
|
|
2784
|
+
<p className="mt-1 text-sm text-gray-600">
|
|
2785
|
+
Importez automatiquement des contacts à partir d'un Google Sheet.
|
|
2786
|
+
</p>
|
|
2787
|
+
</div>
|
|
2788
|
+
<div className="flex items-center gap-2">
|
|
2789
|
+
<button
|
|
2790
|
+
type="button"
|
|
2791
|
+
onClick={handleGoogleSheetSync}
|
|
2792
|
+
disabled={googleSheetSyncing}
|
|
2793
|
+
className="cursor-pointer rounded-lg border border-indigo-600 px-4 py-2 text-sm font-medium text-indigo-600 transition-colors hover:bg-indigo-50 disabled:cursor-not-allowed disabled:opacity-50"
|
|
2794
|
+
>
|
|
2795
|
+
{googleSheetSyncing ? 'Synchronisation...' : 'Synchroniser'}
|
|
2796
|
+
</button>
|
|
2797
|
+
<button
|
|
2798
|
+
onClick={() => {
|
|
2799
|
+
setEditingGoogleSheetConfig(null);
|
|
2800
|
+
setGoogleSheetFormData({
|
|
2801
|
+
name: '',
|
|
2802
|
+
active: true,
|
|
2803
|
+
sheetUrl: '',
|
|
2804
|
+
sheetName: '',
|
|
2805
|
+
headerRow: '1',
|
|
2806
|
+
defaultStatusId: null,
|
|
2807
|
+
defaultAssignedUserId: null,
|
|
2808
|
+
});
|
|
2809
|
+
setGoogleSheetMappings([]);
|
|
2810
|
+
setGoogleSheetStep(1);
|
|
2811
|
+
setShowGoogleSheetModal(true);
|
|
2812
|
+
setGoogleSheetError('');
|
|
2813
|
+
setGoogleSheetSuccess('');
|
|
2814
|
+
}}
|
|
2815
|
+
className="cursor-pointer rounded-lg bg-indigo-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-indigo-700"
|
|
2816
|
+
>
|
|
2817
|
+
+ Ajouter
|
|
2818
|
+
</button>
|
|
2819
|
+
</div>
|
|
2820
|
+
</div>
|
|
2821
|
+
|
|
2822
|
+
{googleSheetSuccess && (
|
|
2823
|
+
<div className="mt-4 rounded-lg border border-green-200 bg-green-50 p-4">
|
|
2824
|
+
<div className="flex items-center">
|
|
2825
|
+
<svg
|
|
2826
|
+
className="mr-3 h-5 w-5 text-green-600"
|
|
2827
|
+
fill="none"
|
|
2828
|
+
stroke="currentColor"
|
|
2829
|
+
viewBox="0 0 24 24"
|
|
2830
|
+
>
|
|
2831
|
+
<path
|
|
2832
|
+
strokeLinecap="round"
|
|
2833
|
+
strokeLinejoin="round"
|
|
2834
|
+
strokeWidth={2}
|
|
2835
|
+
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
2836
|
+
/>
|
|
2837
|
+
</svg>
|
|
2838
|
+
<p className="text-sm font-medium text-green-800">
|
|
2839
|
+
{googleSheetSuccess}
|
|
2840
|
+
</p>
|
|
2841
|
+
<button
|
|
2842
|
+
onClick={() => setGoogleSheetSuccess('')}
|
|
2843
|
+
className="ml-auto cursor-pointer text-green-600 hover:text-green-800"
|
|
2844
|
+
>
|
|
2845
|
+
<svg
|
|
2846
|
+
className="h-5 w-5"
|
|
2847
|
+
fill="none"
|
|
2848
|
+
stroke="currentColor"
|
|
2849
|
+
viewBox="0 0 24 24"
|
|
2850
|
+
>
|
|
2851
|
+
<path
|
|
2852
|
+
strokeLinecap="round"
|
|
2853
|
+
strokeLinejoin="round"
|
|
2854
|
+
strokeWidth={2}
|
|
2855
|
+
d="M6 18L18 6M6 6l12 12"
|
|
2856
|
+
/>
|
|
2857
|
+
</svg>
|
|
2858
|
+
</button>
|
|
2859
|
+
</div>
|
|
2860
|
+
</div>
|
|
2861
|
+
)}
|
|
2862
|
+
|
|
2863
|
+
{googleSheetError && (
|
|
2864
|
+
<div className="mt-4 rounded-lg bg-indigo-50 p-4 text-sm text-indigo-600">
|
|
2865
|
+
{googleSheetError}
|
|
2866
|
+
</div>
|
|
2867
|
+
)}
|
|
2868
|
+
|
|
2869
|
+
{googleSheetLoading ? (
|
|
2870
|
+
<div className="mt-6 text-center text-gray-500">Chargement...</div>
|
|
2871
|
+
) : googleSheetConfigs.length === 0 ? (
|
|
2872
|
+
<div className="mt-6 rounded-lg border border-dashed border-gray-300 bg-gray-50 p-8 text-center">
|
|
2873
|
+
<p className="text-sm text-gray-600">
|
|
2874
|
+
Aucune configuration Google Sheets
|
|
2875
|
+
</p>
|
|
2876
|
+
<p className="mt-1 text-xs text-gray-500">
|
|
2877
|
+
Cliquez sur "+ Ajouter" pour créer votre première
|
|
2878
|
+
configuration
|
|
2879
|
+
</p>
|
|
2880
|
+
</div>
|
|
2881
|
+
) : (
|
|
2882
|
+
<div className="mt-6 space-y-3">
|
|
2883
|
+
{googleSheetConfigs.map((config) => (
|
|
2884
|
+
<div
|
|
2885
|
+
key={config.id}
|
|
2886
|
+
className="flex items-center justify-between rounded-lg border border-gray-200 bg-gray-50 p-4"
|
|
2887
|
+
>
|
|
2888
|
+
<div className="flex-1">
|
|
2889
|
+
<div className="flex items-center gap-2">
|
|
2890
|
+
<h3 className="font-medium text-gray-900">{config.name}</h3>
|
|
2891
|
+
{config.active ? (
|
|
2892
|
+
<span className="rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800">
|
|
2893
|
+
Actif
|
|
2894
|
+
</span>
|
|
2895
|
+
) : (
|
|
2896
|
+
<span className="rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-800">
|
|
2897
|
+
Inactif
|
|
2898
|
+
</span>
|
|
2899
|
+
)}
|
|
2900
|
+
</div>
|
|
2901
|
+
<p className="mt-1 text-xs text-gray-500">
|
|
2902
|
+
{config.sheetName} - Ligne {config.headerRow}
|
|
2903
|
+
</p>
|
|
2904
|
+
</div>
|
|
2905
|
+
<div className="flex items-center gap-2">
|
|
2906
|
+
<button
|
|
2907
|
+
onClick={() => handleEditGoogleSheet(config)}
|
|
2908
|
+
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"
|
|
2909
|
+
>
|
|
2910
|
+
Modifier
|
|
2911
|
+
</button>
|
|
2912
|
+
<button
|
|
2913
|
+
onClick={() => handleDeleteGoogleSheet(config.id)}
|
|
2914
|
+
className="cursor-pointer rounded-lg border border-indigo-300 px-3 py-1.5 text-xs font-medium text-indigo-700 transition-colors hover:bg-indigo-50"
|
|
2915
|
+
>
|
|
2916
|
+
Supprimer
|
|
2917
|
+
</button>
|
|
2918
|
+
</div>
|
|
2919
|
+
</div>
|
|
2920
|
+
))}
|
|
2921
|
+
</div>
|
|
2922
|
+
)}
|
|
2923
|
+
</div>
|
|
2924
|
+
|
|
2925
|
+
{/* Meta Lead Ads */}
|
|
2926
|
+
<div className="rounded-lg bg-white p-6 shadow-sm">
|
|
2927
|
+
<div className="flex items-center justify-between">
|
|
2928
|
+
<div>
|
|
2929
|
+
<h2 className="text-lg font-bold text-gray-900">
|
|
2930
|
+
Intégration Meta Lead Ads
|
|
2931
|
+
</h2>
|
|
2932
|
+
<p className="mt-1 text-sm text-gray-600">
|
|
2933
|
+
Recevez automatiquement les leads depuis Facebook Lead Ads.
|
|
2934
|
+
</p>
|
|
2935
|
+
</div>
|
|
2936
|
+
<button
|
|
2937
|
+
onClick={() => {
|
|
2938
|
+
setEditingMetaLeadConfig(null);
|
|
2939
|
+
setMetaLeadFormData({
|
|
2940
|
+
name: '',
|
|
2941
|
+
active: true,
|
|
2942
|
+
pageId: '',
|
|
2943
|
+
accessToken: '',
|
|
2944
|
+
verifyToken: '',
|
|
2945
|
+
defaultStatusId: null,
|
|
2946
|
+
defaultAssignedUserId: null,
|
|
2947
|
+
});
|
|
2948
|
+
setShowMetaLeadModal(true);
|
|
2949
|
+
setMetaLeadError('');
|
|
2950
|
+
setMetaLeadSuccess('');
|
|
2951
|
+
}}
|
|
2952
|
+
className="cursor-pointer rounded-lg bg-indigo-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-indigo-700"
|
|
2953
|
+
>
|
|
2954
|
+
+ Ajouter
|
|
2955
|
+
</button>
|
|
2956
|
+
</div>
|
|
2957
|
+
|
|
2958
|
+
{metaLeadSuccess && (
|
|
2959
|
+
<div className="mt-4 rounded-lg border border-green-200 bg-green-50 p-4">
|
|
2960
|
+
<div className="flex items-center">
|
|
2961
|
+
<svg
|
|
2962
|
+
className="mr-3 h-5 w-5 text-green-600"
|
|
2963
|
+
fill="none"
|
|
2964
|
+
stroke="currentColor"
|
|
2965
|
+
viewBox="0 0 24 24"
|
|
2966
|
+
>
|
|
2967
|
+
<path
|
|
2968
|
+
strokeLinecap="round"
|
|
2969
|
+
strokeLinejoin="round"
|
|
2970
|
+
strokeWidth={2}
|
|
2971
|
+
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
2972
|
+
/>
|
|
2973
|
+
</svg>
|
|
2974
|
+
<p className="text-sm font-medium text-green-800">{metaLeadSuccess}</p>
|
|
2975
|
+
<button
|
|
2976
|
+
onClick={() => setMetaLeadSuccess('')}
|
|
2977
|
+
className="ml-auto cursor-pointer text-green-600 hover:text-green-800"
|
|
2978
|
+
>
|
|
2979
|
+
<svg
|
|
2980
|
+
className="h-5 w-5"
|
|
2981
|
+
fill="none"
|
|
2982
|
+
stroke="currentColor"
|
|
2983
|
+
viewBox="0 0 24 24"
|
|
2984
|
+
>
|
|
2985
|
+
<path
|
|
2986
|
+
strokeLinecap="round"
|
|
2987
|
+
strokeLinejoin="round"
|
|
2988
|
+
strokeWidth={2}
|
|
2989
|
+
d="M6 18L18 6M6 6l12 12"
|
|
2990
|
+
/>
|
|
2991
|
+
</svg>
|
|
2992
|
+
</button>
|
|
2993
|
+
</div>
|
|
2994
|
+
</div>
|
|
2995
|
+
)}
|
|
2996
|
+
|
|
2997
|
+
{metaLeadError && (
|
|
2998
|
+
<div className="mt-4 rounded-lg bg-indigo-50 p-4 text-sm text-indigo-600">
|
|
2999
|
+
{metaLeadError}
|
|
3000
|
+
</div>
|
|
3001
|
+
)}
|
|
3002
|
+
|
|
3003
|
+
{metaLeadLoading ? (
|
|
3004
|
+
<div className="mt-6 text-center text-gray-500">Chargement...</div>
|
|
3005
|
+
) : metaLeadConfigs.length === 0 ? (
|
|
3006
|
+
<div className="mt-6 rounded-lg border border-dashed border-gray-300 bg-gray-50 p-8 text-center">
|
|
3007
|
+
<p className="text-sm text-gray-600">
|
|
3008
|
+
Aucune configuration Meta Lead Ads
|
|
3009
|
+
</p>
|
|
3010
|
+
<p className="mt-1 text-xs text-gray-500">
|
|
3011
|
+
Cliquez sur "+ Ajouter" pour créer votre première
|
|
3012
|
+
configuration
|
|
3013
|
+
</p>
|
|
3014
|
+
</div>
|
|
3015
|
+
) : (
|
|
3016
|
+
<div className="mt-6 space-y-3">
|
|
3017
|
+
{metaLeadConfigs.map((config) => (
|
|
3018
|
+
<div
|
|
3019
|
+
key={config.id}
|
|
3020
|
+
className="flex items-center justify-between rounded-lg border border-gray-200 bg-gray-50 p-4"
|
|
3021
|
+
>
|
|
3022
|
+
<div className="flex-1">
|
|
3023
|
+
<div className="flex items-center gap-2">
|
|
3024
|
+
<h3 className="font-medium text-gray-900">{config.name}</h3>
|
|
3025
|
+
{config.active ? (
|
|
3026
|
+
<span className="rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800">
|
|
3027
|
+
Actif
|
|
3028
|
+
</span>
|
|
3029
|
+
) : (
|
|
3030
|
+
<span className="rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-800">
|
|
3031
|
+
Inactif
|
|
3032
|
+
</span>
|
|
3033
|
+
)}
|
|
3034
|
+
</div>
|
|
3035
|
+
<p className="mt-1 text-xs text-gray-500">
|
|
3036
|
+
Page ID: {config.pageId}
|
|
3037
|
+
</p>
|
|
3038
|
+
</div>
|
|
3039
|
+
<div className="flex items-center gap-2">
|
|
3040
|
+
<button
|
|
3041
|
+
onClick={() => handleEditMetaLead(config)}
|
|
3042
|
+
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"
|
|
3043
|
+
>
|
|
3044
|
+
Modifier
|
|
3045
|
+
</button>
|
|
3046
|
+
<button
|
|
3047
|
+
onClick={() => handleDeleteMetaLead(config.id)}
|
|
3048
|
+
className="cursor-pointer rounded-lg border border-indigo-300 px-3 py-1.5 text-xs font-medium text-indigo-700 transition-colors hover:bg-indigo-50"
|
|
3049
|
+
>
|
|
3050
|
+
Supprimer
|
|
3051
|
+
</button>
|
|
3052
|
+
</div>
|
|
3053
|
+
</div>
|
|
3054
|
+
))}
|
|
3055
|
+
</div>
|
|
3056
|
+
)}
|
|
3057
|
+
</div>
|
|
3058
|
+
|
|
3059
|
+
{/* Google Ads */}
|
|
3060
|
+
<div className="rounded-lg bg-white p-6 shadow-sm">
|
|
3061
|
+
<div className="flex items-center justify-between">
|
|
3062
|
+
<div>
|
|
3063
|
+
<h2 className="text-lg font-bold text-gray-900">
|
|
3064
|
+
Intégration Google Ads
|
|
3065
|
+
</h2>
|
|
3066
|
+
<p className="mt-1 text-sm text-gray-600">
|
|
3067
|
+
Recevez automatiquement les leads depuis Google Ads Lead Forms.
|
|
3068
|
+
</p>
|
|
3069
|
+
</div>
|
|
3070
|
+
<button
|
|
3071
|
+
onClick={() => {
|
|
3072
|
+
setEditingGoogleAdsConfig(null);
|
|
3073
|
+
setGoogleAdsFormData({
|
|
3074
|
+
name: '',
|
|
3075
|
+
active: true,
|
|
3076
|
+
webhookKey: '',
|
|
3077
|
+
defaultStatusId: null,
|
|
3078
|
+
defaultAssignedUserId: null,
|
|
3079
|
+
});
|
|
3080
|
+
setShowGoogleAdsModal(true);
|
|
3081
|
+
setGoogleAdsError('');
|
|
3082
|
+
setGoogleAdsSuccess('');
|
|
3083
|
+
}}
|
|
3084
|
+
className="cursor-pointer rounded-lg bg-indigo-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-indigo-700"
|
|
3085
|
+
>
|
|
3086
|
+
+ Ajouter
|
|
3087
|
+
</button>
|
|
3088
|
+
</div>
|
|
3089
|
+
|
|
3090
|
+
{googleAdsSuccess && (
|
|
3091
|
+
<div className="mt-4 rounded-lg border border-green-200 bg-green-50 p-4">
|
|
3092
|
+
<div className="flex items-center">
|
|
3093
|
+
<svg
|
|
3094
|
+
className="mr-3 h-5 w-5 text-green-600"
|
|
3095
|
+
fill="none"
|
|
3096
|
+
stroke="currentColor"
|
|
3097
|
+
viewBox="0 0 24 24"
|
|
3098
|
+
>
|
|
3099
|
+
<path
|
|
3100
|
+
strokeLinecap="round"
|
|
3101
|
+
strokeLinejoin="round"
|
|
3102
|
+
strokeWidth={2}
|
|
3103
|
+
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
3104
|
+
/>
|
|
3105
|
+
</svg>
|
|
3106
|
+
<p className="text-sm font-medium text-green-800">{googleAdsSuccess}</p>
|
|
3107
|
+
<button
|
|
3108
|
+
onClick={() => setGoogleAdsSuccess('')}
|
|
3109
|
+
className="ml-auto cursor-pointer text-green-600 hover:text-green-800"
|
|
3110
|
+
>
|
|
3111
|
+
<svg
|
|
3112
|
+
className="h-5 w-5"
|
|
3113
|
+
fill="none"
|
|
3114
|
+
stroke="currentColor"
|
|
3115
|
+
viewBox="0 0 24 24"
|
|
3116
|
+
>
|
|
3117
|
+
<path
|
|
3118
|
+
strokeLinecap="round"
|
|
3119
|
+
strokeLinejoin="round"
|
|
3120
|
+
strokeWidth={2}
|
|
3121
|
+
d="M6 18L18 6M6 6l12 12"
|
|
3122
|
+
/>
|
|
3123
|
+
</svg>
|
|
3124
|
+
</button>
|
|
3125
|
+
</div>
|
|
3126
|
+
</div>
|
|
3127
|
+
)}
|
|
3128
|
+
|
|
3129
|
+
{googleAdsError && (
|
|
3130
|
+
<div className="mt-4 rounded-lg bg-indigo-50 p-4 text-sm text-indigo-600">
|
|
3131
|
+
{googleAdsError}
|
|
3132
|
+
</div>
|
|
3133
|
+
)}
|
|
3134
|
+
|
|
3135
|
+
{googleAdsLoading ? (
|
|
3136
|
+
<div className="mt-6 text-center text-gray-500">Chargement...</div>
|
|
3137
|
+
) : googleAdsConfigs.length === 0 ? (
|
|
3138
|
+
<div className="mt-6 rounded-lg border border-dashed border-gray-300 bg-gray-50 p-8 text-center">
|
|
3139
|
+
<p className="text-sm text-gray-600">Aucune configuration Google Ads</p>
|
|
3140
|
+
<p className="mt-1 text-xs text-gray-500">
|
|
3141
|
+
Cliquez sur "+ Ajouter" pour créer votre première
|
|
3142
|
+
configuration
|
|
3143
|
+
</p>
|
|
3144
|
+
</div>
|
|
3145
|
+
) : (
|
|
3146
|
+
<div className="mt-6 space-y-3">
|
|
3147
|
+
{googleAdsConfigs.map((config) => (
|
|
3148
|
+
<div
|
|
3149
|
+
key={config.id}
|
|
3150
|
+
className="flex items-center justify-between rounded-lg border border-gray-200 bg-gray-50 p-4"
|
|
3151
|
+
>
|
|
3152
|
+
<div className="flex-1">
|
|
3153
|
+
<div className="flex items-center gap-2">
|
|
3154
|
+
<h3 className="font-medium text-gray-900">{config.name}</h3>
|
|
3155
|
+
{config.active ? (
|
|
3156
|
+
<span className="rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800">
|
|
3157
|
+
Actif
|
|
3158
|
+
</span>
|
|
3159
|
+
) : (
|
|
3160
|
+
<span className="rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-800">
|
|
3161
|
+
Inactif
|
|
3162
|
+
</span>
|
|
3163
|
+
)}
|
|
3164
|
+
</div>
|
|
3165
|
+
<p className="mt-1 text-xs text-gray-500">
|
|
3166
|
+
Webhook Key: {config.webhookKey}
|
|
3167
|
+
</p>
|
|
3168
|
+
</div>
|
|
3169
|
+
<div className="flex items-center gap-2">
|
|
3170
|
+
<button
|
|
3171
|
+
onClick={() => handleEditGoogleAds(config)}
|
|
3172
|
+
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"
|
|
3173
|
+
>
|
|
3174
|
+
Modifier
|
|
3175
|
+
</button>
|
|
3176
|
+
<button
|
|
3177
|
+
onClick={() => handleDeleteGoogleAds(config.id)}
|
|
3178
|
+
className="cursor-pointer rounded-lg border border-indigo-300 px-3 py-1.5 text-xs font-medium text-indigo-700 transition-colors hover:bg-indigo-50"
|
|
3179
|
+
>
|
|
3180
|
+
Supprimer
|
|
3181
|
+
</button>
|
|
3182
|
+
</div>
|
|
3183
|
+
</div>
|
|
3184
|
+
))}
|
|
3185
|
+
</div>
|
|
3186
|
+
)}
|
|
3187
|
+
</div>
|
|
3188
|
+
</div>
|
|
3189
|
+
) : (
|
|
3190
|
+
<div className="rounded-lg bg-white p-6 shadow-sm">
|
|
3191
|
+
<div className="py-12 text-center">
|
|
3192
|
+
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-indigo-100">
|
|
3193
|
+
<Plug className="h-6 w-6 text-indigo-600" />
|
|
3194
|
+
</div>
|
|
3195
|
+
<h3 className="mt-4 text-lg font-semibold text-gray-900">Accès restreint</h3>
|
|
3196
|
+
<p className="mt-2 text-sm text-gray-600">
|
|
3197
|
+
Cette section est réservée aux administrateurs.
|
|
3198
|
+
</p>
|
|
3199
|
+
</div>
|
|
3200
|
+
</div>
|
|
3201
|
+
)}
|
|
3202
|
+
</>
|
|
3203
|
+
)}
|
|
3204
|
+
</div>
|
|
3205
|
+
</div>
|
|
3206
|
+
</div>
|
|
3207
|
+
|
|
3208
|
+
{/* Modals - doivent être en dehors des sections conditionnelles */}
|
|
3209
|
+
{showGoogleSheetModal && (
|
|
3210
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-gray-500/20 p-4 backdrop-blur-sm sm:p-6">
|
|
3211
|
+
<div className="flex max-h-[90vh] w-full max-w-5xl flex-col rounded-lg bg-white p-6 shadow-xl sm:p-8">
|
|
3212
|
+
{/* En-tête fixe */}
|
|
3213
|
+
<div className="shrink-0 border-b border-gray-100 pb-4">
|
|
3214
|
+
<div className="flex items-center justify-between">
|
|
3215
|
+
<h2 className="text-xl font-bold text-gray-900 sm:text-2xl">
|
|
3216
|
+
{editingGoogleSheetConfig ? 'Modifier' : 'Ajouter'} une configuration Google
|
|
3217
|
+
Sheets
|
|
3218
|
+
</h2>
|
|
3219
|
+
<button
|
|
3220
|
+
type="button"
|
|
3221
|
+
onClick={() => {
|
|
3222
|
+
setShowGoogleSheetModal(false);
|
|
3223
|
+
setEditingGoogleSheetConfig(null);
|
|
3224
|
+
setGoogleSheetStep(1);
|
|
3225
|
+
setGoogleSheetFormData({
|
|
3226
|
+
name: '',
|
|
3227
|
+
active: true,
|
|
3228
|
+
sheetUrl: '',
|
|
3229
|
+
sheetName: '',
|
|
3230
|
+
headerRow: '1',
|
|
3231
|
+
defaultStatusId: null,
|
|
3232
|
+
defaultAssignedUserId: null,
|
|
3233
|
+
});
|
|
3234
|
+
setGoogleSheetMappings([]);
|
|
3235
|
+
setGoogleSheetError('');
|
|
3236
|
+
setGoogleSheetPreview([]);
|
|
3237
|
+
setGoogleSheetHeaders([]);
|
|
3238
|
+
}}
|
|
3239
|
+
className="cursor-pointer rounded-lg p-2 text-gray-400 transition-colors hover:bg-gray-100"
|
|
3240
|
+
>
|
|
3241
|
+
<svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
3242
|
+
<path
|
|
3243
|
+
strokeLinecap="round"
|
|
3244
|
+
strokeLinejoin="round"
|
|
3245
|
+
strokeWidth={2}
|
|
3246
|
+
d="M6 18L18 6M6 6l12 12"
|
|
3247
|
+
/>
|
|
3248
|
+
</svg>
|
|
3249
|
+
</button>
|
|
3250
|
+
</div>
|
|
3251
|
+
</div>
|
|
3252
|
+
|
|
3253
|
+
{/* Contenu scrollable */}
|
|
3254
|
+
<form
|
|
3255
|
+
id="google-sheet-form"
|
|
3256
|
+
onSubmit={handleGoogleSheetSubmit}
|
|
3257
|
+
className="flex-1 space-y-4 overflow-y-auto pt-4 [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
|
|
3258
|
+
>
|
|
3259
|
+
{/* Étape 1 : informations de base */}
|
|
3260
|
+
{googleSheetStep === 1 && (
|
|
3261
|
+
<>
|
|
3262
|
+
<div>
|
|
3263
|
+
<label className="block text-sm font-medium text-gray-700">
|
|
3264
|
+
Nom de la configuration *
|
|
3265
|
+
</label>
|
|
3266
|
+
<input
|
|
3267
|
+
type="text"
|
|
3268
|
+
required
|
|
3269
|
+
value={googleSheetFormData.name}
|
|
3270
|
+
onChange={(e) =>
|
|
3271
|
+
setGoogleSheetFormData((prev) => ({
|
|
3272
|
+
...prev,
|
|
3273
|
+
name: e.target.value,
|
|
3274
|
+
}))
|
|
3275
|
+
}
|
|
3276
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
|
3277
|
+
placeholder="Ex: Contacts Ventes"
|
|
3278
|
+
/>
|
|
3279
|
+
</div>
|
|
3280
|
+
|
|
3281
|
+
<div className="flex items-center">
|
|
3282
|
+
<input
|
|
3283
|
+
id="google-sheet-active"
|
|
3284
|
+
type="checkbox"
|
|
3285
|
+
checked={googleSheetFormData.active}
|
|
3286
|
+
onChange={(e) =>
|
|
3287
|
+
setGoogleSheetFormData((prev) => ({
|
|
3288
|
+
...prev,
|
|
3289
|
+
active: e.target.checked,
|
|
3290
|
+
}))
|
|
3291
|
+
}
|
|
3292
|
+
className="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
|
3293
|
+
/>
|
|
3294
|
+
<label
|
|
3295
|
+
htmlFor="google-sheet-active"
|
|
3296
|
+
className="ml-2 text-sm font-medium text-gray-700"
|
|
3297
|
+
>
|
|
3298
|
+
Activer l'import automatique depuis Google Sheets
|
|
3299
|
+
</label>
|
|
3300
|
+
</div>
|
|
3301
|
+
|
|
3302
|
+
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
3303
|
+
<div className="md:col-span-2">
|
|
3304
|
+
<label className="block text-sm font-medium text-gray-700">
|
|
3305
|
+
Lien du Google Sheet *
|
|
3306
|
+
</label>
|
|
3307
|
+
<input
|
|
3308
|
+
type="url"
|
|
3309
|
+
required
|
|
3310
|
+
value={googleSheetFormData.sheetUrl}
|
|
3311
|
+
onChange={(e) => {
|
|
3312
|
+
setGoogleSheetFormData((prev) => ({
|
|
3313
|
+
...prev,
|
|
3314
|
+
sheetUrl: e.target.value,
|
|
3315
|
+
}));
|
|
3316
|
+
setGoogleSheetPreview([]);
|
|
3317
|
+
setGoogleSheetHeaders([]);
|
|
3318
|
+
}}
|
|
3319
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
|
3320
|
+
placeholder="https://docs.google.com/spreadsheets/d/..."
|
|
3321
|
+
/>
|
|
3322
|
+
</div>
|
|
3323
|
+
|
|
3324
|
+
<div>
|
|
3325
|
+
<label className="block text-sm font-medium text-gray-700">
|
|
3326
|
+
Nom de l'onglet (Sheet) *
|
|
3327
|
+
</label>
|
|
3328
|
+
<input
|
|
3329
|
+
type="text"
|
|
3330
|
+
required
|
|
3331
|
+
value={googleSheetFormData.sheetName}
|
|
3332
|
+
onChange={(e) => {
|
|
3333
|
+
setGoogleSheetFormData((prev) => ({
|
|
3334
|
+
...prev,
|
|
3335
|
+
sheetName: e.target.value,
|
|
3336
|
+
}));
|
|
3337
|
+
setGoogleSheetPreview([]);
|
|
3338
|
+
setGoogleSheetHeaders([]);
|
|
3339
|
+
}}
|
|
3340
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
|
3341
|
+
placeholder="Feuille 1"
|
|
3342
|
+
/>
|
|
3343
|
+
</div>
|
|
3344
|
+
|
|
3345
|
+
<div>
|
|
3346
|
+
<label className="block text-sm font-medium text-gray-700">
|
|
3347
|
+
Ligne des en-têtes *
|
|
3348
|
+
</label>
|
|
3349
|
+
<input
|
|
3350
|
+
type="number"
|
|
3351
|
+
min={1}
|
|
3352
|
+
required
|
|
3353
|
+
value={googleSheetFormData.headerRow}
|
|
3354
|
+
onChange={(e) => {
|
|
3355
|
+
setGoogleSheetFormData((prev) => ({
|
|
3356
|
+
...prev,
|
|
3357
|
+
headerRow: e.target.value,
|
|
3358
|
+
}));
|
|
3359
|
+
setGoogleSheetPreview([]);
|
|
3360
|
+
setGoogleSheetHeaders([]);
|
|
3361
|
+
}}
|
|
3362
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
|
3363
|
+
placeholder="1"
|
|
3364
|
+
/>
|
|
3365
|
+
</div>
|
|
3366
|
+
</div>
|
|
3367
|
+
</>
|
|
3368
|
+
)}
|
|
3369
|
+
|
|
3370
|
+
{/* Étape 2 : mapping manuel des colonnes + valeurs par défaut */}
|
|
3371
|
+
{googleSheetStep === 2 && (
|
|
3372
|
+
<>
|
|
3373
|
+
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
3374
|
+
<div>
|
|
3375
|
+
<label className="block text-sm font-medium text-gray-700">
|
|
3376
|
+
Utilisateur assigné par défaut (optionnel)
|
|
3377
|
+
</label>
|
|
3378
|
+
<select
|
|
3379
|
+
value={googleSheetFormData.defaultAssignedUserId || ''}
|
|
3380
|
+
onChange={(e) =>
|
|
3381
|
+
setGoogleSheetFormData((prev) => ({
|
|
3382
|
+
...prev,
|
|
3383
|
+
defaultAssignedUserId: e.target.value || null,
|
|
3384
|
+
}))
|
|
3385
|
+
}
|
|
3386
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
|
3387
|
+
>
|
|
3388
|
+
<option value="">Aucun utilisateur par défaut</option>
|
|
3389
|
+
{metaLeadUsers.map((user) => (
|
|
3390
|
+
<option key={user.id} value={user.id}>
|
|
3391
|
+
{user.name} ({user.email})
|
|
3392
|
+
</option>
|
|
3393
|
+
))}
|
|
3394
|
+
</select>
|
|
3395
|
+
</div>
|
|
3396
|
+
|
|
3397
|
+
<div>
|
|
3398
|
+
<label className="block text-sm font-medium text-gray-700">
|
|
3399
|
+
Statut par défaut
|
|
3400
|
+
</label>
|
|
3401
|
+
<select
|
|
3402
|
+
value={googleSheetFormData.defaultStatusId || ''}
|
|
3403
|
+
onChange={(e) =>
|
|
3404
|
+
setGoogleSheetFormData((prev) => ({
|
|
3405
|
+
...prev,
|
|
3406
|
+
defaultStatusId: e.target.value || null,
|
|
3407
|
+
}))
|
|
3408
|
+
}
|
|
3409
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
|
3410
|
+
>
|
|
3411
|
+
<option value="">Aucun statut par défaut</option>
|
|
3412
|
+
{statuses.map((status) => (
|
|
3413
|
+
<option key={status.id} value={status.id}>
|
|
3414
|
+
{status.name}
|
|
3415
|
+
</option>
|
|
3416
|
+
))}
|
|
3417
|
+
</select>
|
|
3418
|
+
</div>
|
|
3419
|
+
</div>
|
|
3420
|
+
|
|
3421
|
+
<div className="flex items-center justify-between">
|
|
3422
|
+
<h3 className="text-base font-semibold text-gray-900">
|
|
3423
|
+
Correspondance des champs
|
|
3424
|
+
</h3>
|
|
3425
|
+
<button
|
|
3426
|
+
type="button"
|
|
3427
|
+
onClick={() => {
|
|
3428
|
+
setGoogleSheetMappings((prev) => [
|
|
3429
|
+
...prev,
|
|
3430
|
+
{
|
|
3431
|
+
id: `mapping-${Date.now()}-${Math.random()}`,
|
|
3432
|
+
columnName: '',
|
|
3433
|
+
action: 'ignore',
|
|
3434
|
+
},
|
|
3435
|
+
]);
|
|
3436
|
+
}}
|
|
3437
|
+
className="flex cursor-pointer items-center gap-1 rounded-lg border border-indigo-600 bg-white px-3 py-1.5 text-sm font-medium text-indigo-600 transition-colors hover:bg-indigo-50"
|
|
3438
|
+
>
|
|
3439
|
+
<Plus className="h-4 w-4" />
|
|
3440
|
+
Ajouter un champ
|
|
3441
|
+
</button>
|
|
3442
|
+
</div>
|
|
3443
|
+
|
|
3444
|
+
<div className="mt-4 space-y-3">
|
|
3445
|
+
{googleSheetMappings.map((mapping) => (
|
|
3446
|
+
<div
|
|
3447
|
+
key={mapping.id}
|
|
3448
|
+
className="flex items-center gap-3 rounded-lg border border-gray-200 bg-gray-50 p-3"
|
|
3449
|
+
>
|
|
3450
|
+
<div className="flex-1">
|
|
3451
|
+
<input
|
|
3452
|
+
type="text"
|
|
3453
|
+
value={mapping.columnName}
|
|
3454
|
+
onChange={(e) => {
|
|
3455
|
+
setGoogleSheetMappings((prev) =>
|
|
3456
|
+
prev.map((m) =>
|
|
3457
|
+
m.id === mapping.id ? { ...m, columnName: e.target.value } : m,
|
|
3458
|
+
),
|
|
3459
|
+
);
|
|
3460
|
+
}}
|
|
3461
|
+
placeholder="Nom de la colonne dans Google Sheets"
|
|
3462
|
+
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm text-gray-900 placeholder:text-gray-400 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
|
3463
|
+
/>
|
|
3464
|
+
</div>
|
|
3465
|
+
|
|
3466
|
+
<div className="flex items-center">
|
|
3467
|
+
<ArrowRight className="h-5 w-5 text-gray-400" />
|
|
3468
|
+
</div>
|
|
3469
|
+
|
|
3470
|
+
<div className="flex-1">
|
|
3471
|
+
<select
|
|
3472
|
+
value={mapping.action}
|
|
3473
|
+
onChange={(e) => {
|
|
3474
|
+
const newAction = e.target.value as 'map' | 'note' | 'ignore';
|
|
3475
|
+
setGoogleSheetMappings((prev) =>
|
|
3476
|
+
prev.map((m) =>
|
|
3477
|
+
m.id === mapping.id
|
|
3478
|
+
? {
|
|
3479
|
+
...m,
|
|
3480
|
+
action: newAction,
|
|
3481
|
+
crmField:
|
|
3482
|
+
newAction === 'map' && m.crmField
|
|
3483
|
+
? m.crmField
|
|
3484
|
+
: newAction === 'map'
|
|
3485
|
+
? 'firstName'
|
|
3486
|
+
: undefined,
|
|
3487
|
+
}
|
|
3488
|
+
: m,
|
|
3489
|
+
),
|
|
3490
|
+
);
|
|
3491
|
+
}}
|
|
3492
|
+
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
|
3493
|
+
>
|
|
3494
|
+
<option value="map">Mapper vers un champ</option>
|
|
3495
|
+
<option value="note">Ajouter comme note</option>
|
|
3496
|
+
<option value="ignore">-- Ne pas importer --</option>
|
|
3497
|
+
</select>
|
|
3498
|
+
</div>
|
|
3499
|
+
|
|
3500
|
+
{mapping.action === 'map' && (
|
|
3501
|
+
<>
|
|
3502
|
+
<div className="flex items-center">
|
|
3503
|
+
<ArrowRight className="h-5 w-5 text-gray-400" />
|
|
3504
|
+
</div>
|
|
3505
|
+
<div className="flex-1">
|
|
3506
|
+
<select
|
|
3507
|
+
value={mapping.crmField || ''}
|
|
3508
|
+
onChange={(e) => {
|
|
3509
|
+
setGoogleSheetMappings((prev) =>
|
|
3510
|
+
prev.map((m) =>
|
|
3511
|
+
m.id === mapping.id ? { ...m, crmField: e.target.value } : m,
|
|
3512
|
+
),
|
|
3513
|
+
);
|
|
3514
|
+
}}
|
|
3515
|
+
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
|
3516
|
+
>
|
|
3517
|
+
<option value="">Sélectionnez un champ</option>
|
|
3518
|
+
<option value="phone">Téléphone *</option>
|
|
3519
|
+
<option value="firstName">Prénom</option>
|
|
3520
|
+
<option value="lastName">Nom</option>
|
|
3521
|
+
<option value="email">Email</option>
|
|
3522
|
+
<option value="civility">Civilité</option>
|
|
3523
|
+
<option value="secondaryPhone">Téléphone secondaire</option>
|
|
3524
|
+
<option value="address">Adresse</option>
|
|
3525
|
+
<option value="city">Ville</option>
|
|
3526
|
+
<option value="postalCode">Code postal</option>
|
|
3527
|
+
<option value="origin">Origine</option>
|
|
3528
|
+
</select>
|
|
3529
|
+
</div>
|
|
3530
|
+
</>
|
|
3531
|
+
)}
|
|
3532
|
+
|
|
3533
|
+
<button
|
|
3534
|
+
type="button"
|
|
3535
|
+
onClick={() => {
|
|
3536
|
+
setGoogleSheetMappings((prev) =>
|
|
3537
|
+
prev.filter((m) => m.id !== mapping.id),
|
|
3538
|
+
);
|
|
3539
|
+
}}
|
|
3540
|
+
className="cursor-pointer rounded-lg p-2 text-gray-400 transition-colors hover:bg-gray-200 hover:text-indigo-600"
|
|
3541
|
+
>
|
|
3542
|
+
<Trash2 className="h-4 w-4" />
|
|
3543
|
+
</button>
|
|
3544
|
+
</div>
|
|
3545
|
+
))}
|
|
3546
|
+
</div>
|
|
3547
|
+
|
|
3548
|
+
<div className="mt-6 rounded-lg border border-gray-200 bg-gray-50 p-4">
|
|
3549
|
+
<ul className="space-y-2 text-xs text-gray-600">
|
|
3550
|
+
<li>
|
|
3551
|
+
• Le champ "Téléphone" est obligatoire pour l'import.
|
|
3552
|
+
Configurez également les autres champs que vous souhaitez importer.
|
|
3553
|
+
</li>
|
|
3554
|
+
<li>
|
|
3555
|
+
• Les colonnes sans correspondance de champ sélectionnée (-- Ne pas importer
|
|
3556
|
+
--) seront ignorées lors de l'import
|
|
3557
|
+
</li>
|
|
3558
|
+
<li>
|
|
3559
|
+
• Laissez vide le nom de colonne Google Sheets pour ignorer ce mapping
|
|
3560
|
+
</li>
|
|
3561
|
+
<li>
|
|
3562
|
+
• Pour "Origine", utilisez le nom de la plateforme (ex: Facebook,
|
|
3563
|
+
Instagram, Google, LinkedIn, Site Web)
|
|
3564
|
+
</li>
|
|
3565
|
+
<li>
|
|
3566
|
+
• Les colonnes marquées "Ajouter comme note" seront regroupées
|
|
3567
|
+
dans une seule note avec le format "Question : Réponse"
|
|
3568
|
+
</li>
|
|
3569
|
+
</ul>
|
|
3570
|
+
</div>
|
|
3571
|
+
|
|
3572
|
+
{googleSheetPreview.length > 0 && googleSheetHeaders.length > 0 && (
|
|
3573
|
+
<div>
|
|
3574
|
+
<h3 className="mb-3 text-sm font-semibold text-gray-900">
|
|
3575
|
+
Aperçu (5 premières lignes)
|
|
3576
|
+
</h3>
|
|
3577
|
+
<div className="overflow-x-auto rounded-lg border border-gray-200">
|
|
3578
|
+
<table className="min-w-full divide-y divide-gray-200">
|
|
3579
|
+
<thead className="bg-gray-50">
|
|
3580
|
+
<tr>
|
|
3581
|
+
{googleSheetHeaders.map((header) => (
|
|
3582
|
+
<th
|
|
3583
|
+
key={header}
|
|
3584
|
+
className="px-4 py-2 text-left text-xs font-medium text-gray-700"
|
|
3585
|
+
>
|
|
3586
|
+
{header}
|
|
3587
|
+
</th>
|
|
3588
|
+
))}
|
|
3589
|
+
</tr>
|
|
3590
|
+
</thead>
|
|
3591
|
+
<tbody className="divide-y divide-gray-200 bg-white">
|
|
3592
|
+
{googleSheetPreview.map((row, idx) => (
|
|
3593
|
+
<tr key={idx}>
|
|
3594
|
+
{googleSheetHeaders.map((header) => (
|
|
3595
|
+
<td
|
|
3596
|
+
key={header}
|
|
3597
|
+
className="px-4 py-2 text-xs whitespace-nowrap text-gray-900"
|
|
3598
|
+
>
|
|
3599
|
+
{row[header] || '-'}
|
|
3600
|
+
</td>
|
|
3601
|
+
))}
|
|
3602
|
+
</tr>
|
|
3603
|
+
))}
|
|
3604
|
+
</tbody>
|
|
3605
|
+
</table>
|
|
3606
|
+
</div>
|
|
3607
|
+
</div>
|
|
3608
|
+
)}
|
|
3609
|
+
</>
|
|
3610
|
+
)}
|
|
3611
|
+
|
|
3612
|
+
{googleSheetError && (
|
|
3613
|
+
<div className="rounded-lg bg-indigo-50 p-4 text-sm text-indigo-600">
|
|
3614
|
+
{googleSheetError}
|
|
3615
|
+
</div>
|
|
3616
|
+
)}
|
|
3617
|
+
</form>
|
|
3618
|
+
|
|
3619
|
+
{/* Pied de modal fixe */}
|
|
3620
|
+
<div className="shrink-0 border-t border-gray-100 pt-4">
|
|
3621
|
+
<div className="flex flex-col gap-3 sm:flex-row sm:justify-end">
|
|
3622
|
+
<button
|
|
3623
|
+
type="button"
|
|
3624
|
+
onClick={() => {
|
|
3625
|
+
setShowGoogleSheetModal(false);
|
|
3626
|
+
setEditingGoogleSheetConfig(null);
|
|
3627
|
+
setGoogleSheetStep(1);
|
|
3628
|
+
setGoogleSheetFormData({
|
|
3629
|
+
name: '',
|
|
3630
|
+
active: true,
|
|
3631
|
+
sheetUrl: '',
|
|
3632
|
+
sheetName: '',
|
|
3633
|
+
headerRow: '1',
|
|
3634
|
+
defaultStatusId: null,
|
|
3635
|
+
defaultAssignedUserId: null,
|
|
3636
|
+
});
|
|
3637
|
+
setGoogleSheetMappings([]);
|
|
3638
|
+
setGoogleSheetError('');
|
|
3639
|
+
setGoogleSheetPreview([]);
|
|
3640
|
+
setGoogleSheetHeaders([]);
|
|
3641
|
+
}}
|
|
3642
|
+
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"
|
|
3643
|
+
>
|
|
3644
|
+
Annuler
|
|
3645
|
+
</button>
|
|
3646
|
+
|
|
3647
|
+
{googleSheetStep === 1 ? (
|
|
3648
|
+
<button
|
|
3649
|
+
type="button"
|
|
3650
|
+
onClick={async () => {
|
|
3651
|
+
const ok = await handleGoogleSheetAutoMap();
|
|
3652
|
+
if (ok) {
|
|
3653
|
+
setGoogleSheetStep(2);
|
|
3654
|
+
}
|
|
3655
|
+
}}
|
|
3656
|
+
className="w-full cursor-pointer rounded-lg bg-indigo-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-indigo-700 disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto"
|
|
3657
|
+
disabled={googleSheetSaving}
|
|
3658
|
+
>
|
|
3659
|
+
Étape suivante
|
|
3660
|
+
</button>
|
|
3661
|
+
) : (
|
|
3662
|
+
<button
|
|
3663
|
+
type="submit"
|
|
3664
|
+
form="google-sheet-form"
|
|
3665
|
+
disabled={googleSheetSaving}
|
|
3666
|
+
className="w-full cursor-pointer rounded-lg bg-indigo-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-indigo-700 disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto"
|
|
3667
|
+
>
|
|
3668
|
+
{googleSheetSaving ? 'Enregistrement...' : 'Enregistrer'}
|
|
3669
|
+
</button>
|
|
3670
|
+
)}
|
|
3671
|
+
</div>
|
|
3672
|
+
</div>
|
|
3673
|
+
</div>
|
|
3674
|
+
</div>
|
|
3675
|
+
)}
|
|
3676
|
+
|
|
3677
|
+
{showMetaLeadModal && (
|
|
3678
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-gray-500/20 p-4 backdrop-blur-sm sm:p-6">
|
|
3679
|
+
<div className="flex max-h-[90vh] w-full max-w-2xl flex-col rounded-lg bg-white p-6 shadow-xl sm:p-8">
|
|
3680
|
+
<div className="shrink-0 border-b border-gray-100 pb-4">
|
|
3681
|
+
<div className="flex items-center justify-between">
|
|
3682
|
+
<h2 className="text-xl font-bold text-gray-900 sm:text-2xl">
|
|
3683
|
+
{editingMetaLeadConfig ? 'Modifier' : 'Ajouter'} une configuration Meta Lead Ads
|
|
3684
|
+
</h2>
|
|
3685
|
+
<button
|
|
3686
|
+
type="button"
|
|
3687
|
+
onClick={() => {
|
|
3688
|
+
setShowMetaLeadModal(false);
|
|
3689
|
+
setEditingMetaLeadConfig(null);
|
|
3690
|
+
setMetaLeadFormData({
|
|
3691
|
+
name: '',
|
|
3692
|
+
active: true,
|
|
3693
|
+
pageId: '',
|
|
3694
|
+
accessToken: '',
|
|
3695
|
+
verifyToken: '',
|
|
3696
|
+
defaultStatusId: null,
|
|
3697
|
+
defaultAssignedUserId: null,
|
|
3698
|
+
});
|
|
3699
|
+
setMetaLeadError('');
|
|
3700
|
+
setMetaLeadSuccess('');
|
|
3701
|
+
}}
|
|
3702
|
+
className="cursor-pointer rounded-lg p-2 text-gray-400 transition-colors hover:bg-gray-100"
|
|
3703
|
+
>
|
|
3704
|
+
<svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
3705
|
+
<path
|
|
3706
|
+
strokeLinecap="round"
|
|
3707
|
+
strokeLinejoin="round"
|
|
3708
|
+
strokeWidth={2}
|
|
3709
|
+
d="M6 18L18 6M6 6l12 12"
|
|
3710
|
+
/>
|
|
3711
|
+
</svg>
|
|
3712
|
+
</button>
|
|
3713
|
+
</div>
|
|
3714
|
+
</div>
|
|
3715
|
+
|
|
3716
|
+
<form
|
|
3717
|
+
id="meta-lead-form"
|
|
3718
|
+
onSubmit={handleMetaLeadSubmit}
|
|
3719
|
+
className="flex-1 space-y-4 overflow-y-auto pt-4"
|
|
3720
|
+
>
|
|
3721
|
+
<div>
|
|
3722
|
+
<label className="block text-sm font-medium text-gray-700">
|
|
3723
|
+
Nom de la configuration *
|
|
3724
|
+
</label>
|
|
3725
|
+
<input
|
|
3726
|
+
type="text"
|
|
3727
|
+
required
|
|
3728
|
+
value={metaLeadFormData.name}
|
|
3729
|
+
onChange={(e) =>
|
|
3730
|
+
setMetaLeadFormData((prev) => ({ ...prev, name: e.target.value }))
|
|
3731
|
+
}
|
|
3732
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
|
3733
|
+
placeholder="Ex: Facebook Lead Ads"
|
|
3734
|
+
/>
|
|
3735
|
+
</div>
|
|
3736
|
+
|
|
3737
|
+
<div className="flex items-center">
|
|
3738
|
+
<input
|
|
3739
|
+
id="meta-lead-active"
|
|
3740
|
+
type="checkbox"
|
|
3741
|
+
checked={metaLeadFormData.active}
|
|
3742
|
+
onChange={(e) =>
|
|
3743
|
+
setMetaLeadFormData((prev) => ({ ...prev, active: e.target.checked }))
|
|
3744
|
+
}
|
|
3745
|
+
className="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
|
3746
|
+
/>
|
|
3747
|
+
<label
|
|
3748
|
+
htmlFor="meta-lead-active"
|
|
3749
|
+
className="ml-2 text-sm font-medium text-gray-700"
|
|
3750
|
+
>
|
|
3751
|
+
Activer l'intégration Meta Lead Ads
|
|
3752
|
+
</label>
|
|
3753
|
+
</div>
|
|
3754
|
+
|
|
3755
|
+
<div>
|
|
3756
|
+
<label className="block text-sm font-medium text-gray-700">Page ID *</label>
|
|
3757
|
+
<input
|
|
3758
|
+
type="text"
|
|
3759
|
+
required
|
|
3760
|
+
value={metaLeadFormData.pageId}
|
|
3761
|
+
onChange={(e) =>
|
|
3762
|
+
setMetaLeadFormData((prev) => ({ ...prev, pageId: e.target.value }))
|
|
3763
|
+
}
|
|
3764
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
|
3765
|
+
placeholder="Page ID Facebook"
|
|
3766
|
+
/>
|
|
3767
|
+
</div>
|
|
3768
|
+
|
|
3769
|
+
<div>
|
|
3770
|
+
<label className="block text-sm font-medium text-gray-700">Access Token *</label>
|
|
3771
|
+
<input
|
|
3772
|
+
type="text"
|
|
3773
|
+
required
|
|
3774
|
+
value={metaLeadFormData.accessToken}
|
|
3775
|
+
onChange={(e) =>
|
|
3776
|
+
setMetaLeadFormData((prev) => ({ ...prev, accessToken: e.target.value }))
|
|
3777
|
+
}
|
|
3778
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
|
3779
|
+
placeholder="Access Token"
|
|
3780
|
+
/>
|
|
3781
|
+
</div>
|
|
3782
|
+
|
|
3783
|
+
<div>
|
|
3784
|
+
<label className="block text-sm font-medium text-gray-700">Verify Token *</label>
|
|
3785
|
+
<input
|
|
3786
|
+
type="text"
|
|
3787
|
+
required
|
|
3788
|
+
value={metaLeadFormData.verifyToken}
|
|
3789
|
+
onChange={(e) =>
|
|
3790
|
+
setMetaLeadFormData((prev) => ({ ...prev, verifyToken: e.target.value }))
|
|
3791
|
+
}
|
|
3792
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
|
3793
|
+
placeholder="Verify Token"
|
|
3794
|
+
/>
|
|
3795
|
+
</div>
|
|
3796
|
+
|
|
3797
|
+
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
3798
|
+
<div>
|
|
3799
|
+
<label className="block text-sm font-medium text-gray-700">
|
|
3800
|
+
Utilisateur assigné par défaut (optionnel)
|
|
3801
|
+
</label>
|
|
3802
|
+
<select
|
|
3803
|
+
value={metaLeadFormData.defaultAssignedUserId || ''}
|
|
3804
|
+
onChange={(e) =>
|
|
3805
|
+
setMetaLeadFormData((prev) => ({
|
|
3806
|
+
...prev,
|
|
3807
|
+
defaultAssignedUserId: e.target.value || null,
|
|
3808
|
+
}))
|
|
3809
|
+
}
|
|
3810
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
|
3811
|
+
>
|
|
3812
|
+
<option value="">Aucun utilisateur par défaut</option>
|
|
3813
|
+
{metaLeadUsers.map((user) => (
|
|
3814
|
+
<option key={user.id} value={user.id}>
|
|
3815
|
+
{user.name} ({user.email})
|
|
3816
|
+
</option>
|
|
3817
|
+
))}
|
|
3818
|
+
</select>
|
|
3819
|
+
</div>
|
|
3820
|
+
|
|
3821
|
+
<div>
|
|
3822
|
+
<label className="block text-sm font-medium text-gray-700">
|
|
3823
|
+
Statut par défaut
|
|
3824
|
+
</label>
|
|
3825
|
+
<select
|
|
3826
|
+
value={metaLeadFormData.defaultStatusId || ''}
|
|
3827
|
+
onChange={(e) =>
|
|
3828
|
+
setMetaLeadFormData((prev) => ({
|
|
3829
|
+
...prev,
|
|
3830
|
+
defaultStatusId: e.target.value || null,
|
|
3831
|
+
}))
|
|
3832
|
+
}
|
|
3833
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
|
3834
|
+
>
|
|
3835
|
+
<option value="">Aucun statut par défaut</option>
|
|
3836
|
+
{statuses.map((status) => (
|
|
3837
|
+
<option key={status.id} value={status.id}>
|
|
3838
|
+
{status.name}
|
|
3839
|
+
</option>
|
|
3840
|
+
))}
|
|
3841
|
+
</select>
|
|
3842
|
+
</div>
|
|
3843
|
+
</div>
|
|
3844
|
+
|
|
3845
|
+
{metaLeadError && (
|
|
3846
|
+
<div className="rounded-lg bg-indigo-50 p-4 text-sm text-indigo-600">
|
|
3847
|
+
{metaLeadError}
|
|
3848
|
+
</div>
|
|
3849
|
+
)}
|
|
3850
|
+
</form>
|
|
3851
|
+
|
|
3852
|
+
<div className="shrink-0 border-t border-gray-100 pt-4">
|
|
3853
|
+
<div className="flex flex-col gap-3 sm:flex-row sm:justify-end">
|
|
3854
|
+
<button
|
|
3855
|
+
type="button"
|
|
3856
|
+
onClick={() => {
|
|
3857
|
+
setShowMetaLeadModal(false);
|
|
3858
|
+
setEditingMetaLeadConfig(null);
|
|
3859
|
+
setMetaLeadFormData({
|
|
3860
|
+
name: '',
|
|
3861
|
+
active: true,
|
|
3862
|
+
pageId: '',
|
|
3863
|
+
accessToken: '',
|
|
3864
|
+
verifyToken: '',
|
|
3865
|
+
defaultStatusId: null,
|
|
3866
|
+
defaultAssignedUserId: null,
|
|
3867
|
+
});
|
|
3868
|
+
setMetaLeadError('');
|
|
3869
|
+
setMetaLeadSuccess('');
|
|
3870
|
+
}}
|
|
3871
|
+
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"
|
|
3872
|
+
>
|
|
3873
|
+
Annuler
|
|
3874
|
+
</button>
|
|
3875
|
+
<button
|
|
3876
|
+
type="submit"
|
|
3877
|
+
form="meta-lead-form"
|
|
3878
|
+
disabled={metaLeadSaving}
|
|
3879
|
+
className="w-full cursor-pointer rounded-lg bg-indigo-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-indigo-700 disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto"
|
|
3880
|
+
>
|
|
3881
|
+
{metaLeadSaving ? 'Enregistrement...' : 'Enregistrer'}
|
|
3882
|
+
</button>
|
|
3883
|
+
</div>
|
|
3884
|
+
</div>
|
|
3885
|
+
</div>
|
|
3886
|
+
</div>
|
|
3887
|
+
)}
|
|
3888
|
+
|
|
3889
|
+
{showGoogleAdsModal && (
|
|
3890
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-gray-500/20 p-4 backdrop-blur-sm sm:p-6">
|
|
3891
|
+
<div className="flex max-h-[90vh] w-full max-w-2xl flex-col rounded-lg bg-white p-6 shadow-xl sm:p-8">
|
|
3892
|
+
<div className="shrink-0 border-b border-gray-100 pb-4">
|
|
3893
|
+
<div className="flex items-center justify-between">
|
|
3894
|
+
<h2 className="text-xl font-bold text-gray-900 sm:text-2xl">
|
|
3895
|
+
{editingGoogleAdsConfig ? 'Modifier' : 'Ajouter'} une configuration Google Ads
|
|
3896
|
+
</h2>
|
|
3897
|
+
<button
|
|
3898
|
+
type="button"
|
|
3899
|
+
onClick={() => {
|
|
3900
|
+
setShowGoogleAdsModal(false);
|
|
3901
|
+
setEditingGoogleAdsConfig(null);
|
|
3902
|
+
setGoogleAdsFormData({
|
|
3903
|
+
name: '',
|
|
3904
|
+
active: true,
|
|
3905
|
+
webhookKey: '',
|
|
3906
|
+
defaultStatusId: null,
|
|
3907
|
+
defaultAssignedUserId: null,
|
|
3908
|
+
});
|
|
3909
|
+
setGoogleAdsError('');
|
|
3910
|
+
setGoogleAdsSuccess('');
|
|
3911
|
+
}}
|
|
3912
|
+
className="cursor-pointer rounded-lg p-2 text-gray-400 transition-colors hover:bg-gray-100"
|
|
3913
|
+
>
|
|
3914
|
+
<svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
3915
|
+
<path
|
|
3916
|
+
strokeLinecap="round"
|
|
3917
|
+
strokeLinejoin="round"
|
|
3918
|
+
strokeWidth={2}
|
|
3919
|
+
d="M6 18L18 6M6 6l12 12"
|
|
3920
|
+
/>
|
|
3921
|
+
</svg>
|
|
3922
|
+
</button>
|
|
3923
|
+
</div>
|
|
3924
|
+
</div>
|
|
3925
|
+
|
|
3926
|
+
<form
|
|
3927
|
+
id="google-ads-form"
|
|
3928
|
+
onSubmit={handleGoogleAdsSubmit}
|
|
3929
|
+
className="flex-1 space-y-4 overflow-y-auto pt-4"
|
|
3930
|
+
>
|
|
3931
|
+
<div>
|
|
3932
|
+
<label className="block text-sm font-medium text-gray-700">
|
|
3933
|
+
Nom de la configuration *
|
|
3934
|
+
</label>
|
|
3935
|
+
<input
|
|
3936
|
+
type="text"
|
|
3937
|
+
required
|
|
3938
|
+
value={googleAdsFormData.name}
|
|
3939
|
+
onChange={(e) =>
|
|
3940
|
+
setGoogleAdsFormData((prev) => ({ ...prev, name: e.target.value }))
|
|
3941
|
+
}
|
|
3942
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
|
3943
|
+
placeholder="Ex: Google Ads Lead Forms"
|
|
3944
|
+
/>
|
|
3945
|
+
</div>
|
|
3946
|
+
|
|
3947
|
+
<div className="flex items-center">
|
|
3948
|
+
<input
|
|
3949
|
+
id="google-ads-active"
|
|
3950
|
+
type="checkbox"
|
|
3951
|
+
checked={googleAdsFormData.active}
|
|
3952
|
+
onChange={(e) =>
|
|
3953
|
+
setGoogleAdsFormData((prev) => ({ ...prev, active: e.target.checked }))
|
|
3954
|
+
}
|
|
3955
|
+
className="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
|
3956
|
+
/>
|
|
3957
|
+
<label
|
|
3958
|
+
htmlFor="google-ads-active"
|
|
3959
|
+
className="ml-2 text-sm font-medium text-gray-700"
|
|
3960
|
+
>
|
|
3961
|
+
Activer l'intégration Google Ads
|
|
3962
|
+
</label>
|
|
3963
|
+
</div>
|
|
3964
|
+
|
|
3965
|
+
<div>
|
|
3966
|
+
<label className="block text-sm font-medium text-gray-700">Webhook Key *</label>
|
|
3967
|
+
<input
|
|
3968
|
+
type="text"
|
|
3969
|
+
required
|
|
3970
|
+
value={googleAdsFormData.webhookKey}
|
|
3971
|
+
onChange={(e) =>
|
|
3972
|
+
setGoogleAdsFormData((prev) => ({ ...prev, webhookKey: e.target.value }))
|
|
3973
|
+
}
|
|
3974
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
|
3975
|
+
placeholder="Webhook Key"
|
|
3976
|
+
/>
|
|
3977
|
+
</div>
|
|
3978
|
+
|
|
3979
|
+
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
3980
|
+
<div>
|
|
3981
|
+
<label className="block text-sm font-medium text-gray-700">
|
|
3982
|
+
Utilisateur assigné par défaut (optionnel)
|
|
3983
|
+
</label>
|
|
3984
|
+
<select
|
|
3985
|
+
value={googleAdsFormData.defaultAssignedUserId || ''}
|
|
3986
|
+
onChange={(e) =>
|
|
3987
|
+
setGoogleAdsFormData((prev) => ({
|
|
3988
|
+
...prev,
|
|
3989
|
+
defaultAssignedUserId: e.target.value || null,
|
|
3990
|
+
}))
|
|
3991
|
+
}
|
|
3992
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
|
3993
|
+
>
|
|
3994
|
+
<option value="">Aucun utilisateur par défaut</option>
|
|
3995
|
+
{metaLeadUsers.map((user) => (
|
|
3996
|
+
<option key={user.id} value={user.id}>
|
|
3997
|
+
{user.name} ({user.email})
|
|
3998
|
+
</option>
|
|
3999
|
+
))}
|
|
4000
|
+
</select>
|
|
4001
|
+
</div>
|
|
4002
|
+
|
|
4003
|
+
<div>
|
|
4004
|
+
<label className="block text-sm font-medium text-gray-700">
|
|
4005
|
+
Statut par défaut
|
|
4006
|
+
</label>
|
|
4007
|
+
<select
|
|
4008
|
+
value={googleAdsFormData.defaultStatusId || ''}
|
|
4009
|
+
onChange={(e) =>
|
|
4010
|
+
setGoogleAdsFormData((prev) => ({
|
|
4011
|
+
...prev,
|
|
4012
|
+
defaultStatusId: e.target.value || null,
|
|
4013
|
+
}))
|
|
4014
|
+
}
|
|
4015
|
+
className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
|
|
4016
|
+
>
|
|
4017
|
+
<option value="">Aucun statut par défaut</option>
|
|
4018
|
+
{statuses.map((status) => (
|
|
4019
|
+
<option key={status.id} value={status.id}>
|
|
4020
|
+
{status.name}
|
|
4021
|
+
</option>
|
|
4022
|
+
))}
|
|
4023
|
+
</select>
|
|
4024
|
+
</div>
|
|
4025
|
+
</div>
|
|
4026
|
+
|
|
4027
|
+
{googleAdsError && (
|
|
4028
|
+
<div className="rounded-lg bg-indigo-50 p-4 text-sm text-indigo-600">
|
|
4029
|
+
{googleAdsError}
|
|
4030
|
+
</div>
|
|
4031
|
+
)}
|
|
4032
|
+
</form>
|
|
4033
|
+
|
|
4034
|
+
<div className="shrink-0 border-t border-gray-100 pt-4">
|
|
4035
|
+
<div className="flex flex-col gap-3 sm:flex-row sm:justify-end">
|
|
4036
|
+
<button
|
|
4037
|
+
type="button"
|
|
4038
|
+
onClick={() => {
|
|
4039
|
+
setShowGoogleAdsModal(false);
|
|
4040
|
+
setEditingGoogleAdsConfig(null);
|
|
4041
|
+
setGoogleAdsFormData({
|
|
4042
|
+
name: '',
|
|
4043
|
+
active: true,
|
|
4044
|
+
webhookKey: '',
|
|
4045
|
+
defaultStatusId: null,
|
|
4046
|
+
defaultAssignedUserId: null,
|
|
4047
|
+
});
|
|
4048
|
+
setGoogleAdsError('');
|
|
4049
|
+
setGoogleAdsSuccess('');
|
|
4050
|
+
}}
|
|
4051
|
+
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"
|
|
4052
|
+
>
|
|
4053
|
+
Annuler
|
|
4054
|
+
</button>
|
|
4055
|
+
<button
|
|
4056
|
+
type="submit"
|
|
4057
|
+
form="google-ads-form"
|
|
4058
|
+
disabled={googleAdsSaving}
|
|
4059
|
+
className="w-full cursor-pointer rounded-lg bg-indigo-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-indigo-700 disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto"
|
|
4060
|
+
>
|
|
4061
|
+
{googleAdsSaving ? 'Enregistrement...' : 'Enregistrer'}
|
|
4062
|
+
</button>
|
|
4063
|
+
</div>
|
|
4064
|
+
</div>
|
|
4065
|
+
</div>
|
|
4066
|
+
</div>
|
|
4067
|
+
)}
|
|
4068
|
+
</div>
|
|
4069
|
+
);
|
|
4070
|
+
}
|