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