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,1291 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { notFound } from 'next/navigation';
|
|
5
|
+
import { useAppToast } from '@/contexts/app-toast-context';
|
|
6
|
+
import { requestRemindersRefresh } from '@/lib/reminder-state';
|
|
7
|
+
import { devToast } from '@/lib/utils';
|
|
8
|
+
import {
|
|
9
|
+
Bell,
|
|
10
|
+
Plus,
|
|
11
|
+
Trash2,
|
|
12
|
+
RefreshCw,
|
|
13
|
+
Clock,
|
|
14
|
+
Zap,
|
|
15
|
+
FlaskConical,
|
|
16
|
+
Search,
|
|
17
|
+
Send,
|
|
18
|
+
User,
|
|
19
|
+
Building2,
|
|
20
|
+
Shield,
|
|
21
|
+
ScrollText,
|
|
22
|
+
Play,
|
|
23
|
+
Mail,
|
|
24
|
+
} from 'lucide-react';
|
|
25
|
+
|
|
26
|
+
const isDevMode = process.env.NODE_ENV === 'development';
|
|
27
|
+
|
|
28
|
+
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
interface CreatedTask {
|
|
31
|
+
id: string;
|
|
32
|
+
type: string;
|
|
33
|
+
title: string;
|
|
34
|
+
scheduledAt: string;
|
|
35
|
+
reminderMinutesBefore: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface CreatedContact {
|
|
39
|
+
id: string;
|
|
40
|
+
firstName: string | null;
|
|
41
|
+
lastName: string | null;
|
|
42
|
+
phone: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface CreatedCompany {
|
|
46
|
+
id: string;
|
|
47
|
+
name: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface AuditLog {
|
|
51
|
+
id: string;
|
|
52
|
+
action: string;
|
|
53
|
+
entityType: string;
|
|
54
|
+
entityId: string | null;
|
|
55
|
+
metadata: Record<string, unknown> | null;
|
|
56
|
+
createdAt: string;
|
|
57
|
+
actor: { name: string } | null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface Workflow {
|
|
61
|
+
id: string;
|
|
62
|
+
name: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ─── Composant Section (wrapper réutilisable) ───────────────────────────────
|
|
66
|
+
|
|
67
|
+
function Section({
|
|
68
|
+
title,
|
|
69
|
+
icon: Icon,
|
|
70
|
+
children,
|
|
71
|
+
className = '',
|
|
72
|
+
}: {
|
|
73
|
+
title: string;
|
|
74
|
+
icon: React.ComponentType<{ className?: string }>;
|
|
75
|
+
children: React.ReactNode;
|
|
76
|
+
className?: string;
|
|
77
|
+
}) {
|
|
78
|
+
return (
|
|
79
|
+
<section className={`rounded-xl border border-gray-200 bg-white p-5 shadow-sm ${className}`}>
|
|
80
|
+
<h2 className="mb-4 flex items-center gap-2 text-base font-semibold text-gray-900">
|
|
81
|
+
<Icon className="h-4 w-4" />
|
|
82
|
+
{title}
|
|
83
|
+
</h2>
|
|
84
|
+
{children}
|
|
85
|
+
</section>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ─── Page ────────────────────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
export default function DevToolsPage() {
|
|
92
|
+
if (!isDevMode) notFound();
|
|
93
|
+
|
|
94
|
+
const toast = useAppToast();
|
|
95
|
+
|
|
96
|
+
// --- Task / Reminder state ---
|
|
97
|
+
const [reminderMinutes, setReminderMinutes] = useState(1);
|
|
98
|
+
const [taskInMinutes, setTaskInMinutes] = useState(2);
|
|
99
|
+
const [taskType, setTaskType] = useState('CALL');
|
|
100
|
+
const [taskTitle, setTaskTitle] = useState('Test rappel dev');
|
|
101
|
+
const [taskPriority, setTaskPriority] = useState('HIGH');
|
|
102
|
+
const [creating, setCreating] = useState(false);
|
|
103
|
+
const [createdTasks, setCreatedTasks] = useState<CreatedTask[]>([]);
|
|
104
|
+
const [reminders, setReminders] = useState<any[]>([]);
|
|
105
|
+
const [loadingReminders, setLoadingReminders] = useState(false);
|
|
106
|
+
|
|
107
|
+
// --- Dev reminder test state (from settings) ---
|
|
108
|
+
const [devReminderLoading, setDevReminderLoading] = useState(false);
|
|
109
|
+
const [devReminderForm, setDevReminderForm] = useState({
|
|
110
|
+
title: 'Rappel de test',
|
|
111
|
+
reminderMinutesBefore: 1,
|
|
112
|
+
offsetMinutes: 2,
|
|
113
|
+
sendNow: false,
|
|
114
|
+
});
|
|
115
|
+
const [devContactSearch, setDevContactSearch] = useState('');
|
|
116
|
+
const [devContactLoading, setDevContactLoading] = useState(false);
|
|
117
|
+
const [devContactResults, setDevContactResults] = useState<
|
|
118
|
+
Array<{ id: string; firstName: string | null; lastName: string | null; phone: string }>
|
|
119
|
+
>([]);
|
|
120
|
+
const [devSelectedContact, setDevSelectedContact] = useState<{
|
|
121
|
+
id: string;
|
|
122
|
+
firstName: string | null;
|
|
123
|
+
lastName: string | null;
|
|
124
|
+
phone: string;
|
|
125
|
+
} | null>(null);
|
|
126
|
+
|
|
127
|
+
// --- Quick contact creation ---
|
|
128
|
+
const [contactForm, setContactForm] = useState({ firstName: '', lastName: '', phone: '', email: '' });
|
|
129
|
+
const [contactCreating, setContactCreating] = useState(false);
|
|
130
|
+
const [createdContacts, setCreatedContacts] = useState<CreatedContact[]>([]);
|
|
131
|
+
|
|
132
|
+
// --- Quick company creation ---
|
|
133
|
+
const [companyForm, setCompanyForm] = useState({ name: '', phone: '', email: '' });
|
|
134
|
+
const [companyCreating, setCompanyCreating] = useState(false);
|
|
135
|
+
const [createdCompanies, setCreatedCompanies] = useState<CreatedCompany[]>([]);
|
|
136
|
+
|
|
137
|
+
// --- Workflow trigger ---
|
|
138
|
+
const [workflowContactSearch, setWorkflowContactSearch] = useState('');
|
|
139
|
+
const [workflowContactResults, setWorkflowContactResults] = useState<
|
|
140
|
+
Array<{ id: string; firstName: string | null; lastName: string | null; phone: string }>
|
|
141
|
+
>([]);
|
|
142
|
+
const [workflowSelectedContact, setWorkflowSelectedContact] = useState<{
|
|
143
|
+
id: string;
|
|
144
|
+
firstName: string | null;
|
|
145
|
+
lastName: string | null;
|
|
146
|
+
} | null>(null);
|
|
147
|
+
const [availableWorkflows, setAvailableWorkflows] = useState<Workflow[]>([]);
|
|
148
|
+
const [workflowLoading, setWorkflowLoading] = useState(false);
|
|
149
|
+
|
|
150
|
+
// --- SMTP test ---
|
|
151
|
+
const [smtpTesting, setSmtpTesting] = useState(false);
|
|
152
|
+
const [smtpConfig, setSmtpConfig] = useState<{
|
|
153
|
+
host: string;
|
|
154
|
+
port: number;
|
|
155
|
+
secure: boolean;
|
|
156
|
+
username: string;
|
|
157
|
+
fromEmail: string;
|
|
158
|
+
fromName: string;
|
|
159
|
+
signature: string | null;
|
|
160
|
+
} | null>(null);
|
|
161
|
+
const [smtpLoaded, setSmtpLoaded] = useState(false);
|
|
162
|
+
const [smtpPassword, setSmtpPassword] = useState('');
|
|
163
|
+
|
|
164
|
+
// --- Permissions ---
|
|
165
|
+
const [userInfo, setUserInfo] = useState<any>(null);
|
|
166
|
+
const [loadingUser, setLoadingUser] = useState(false);
|
|
167
|
+
|
|
168
|
+
// --- Audit logs ---
|
|
169
|
+
const [auditLogs, setAuditLogs] = useState<AuditLog[]>([]);
|
|
170
|
+
const [loadingAudit, setLoadingAudit] = useState(false);
|
|
171
|
+
|
|
172
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
173
|
+
// Handlers
|
|
174
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
175
|
+
|
|
176
|
+
// --- Task creation (via /api/tasks) ---
|
|
177
|
+
const createTestTask = async () => {
|
|
178
|
+
setCreating(true);
|
|
179
|
+
try {
|
|
180
|
+
const scheduledAt = new Date(Date.now() + taskInMinutes * 60 * 1000);
|
|
181
|
+
const res = await fetch('/api/tasks', {
|
|
182
|
+
method: 'POST',
|
|
183
|
+
headers: { 'Content-Type': 'application/json' },
|
|
184
|
+
body: JSON.stringify({
|
|
185
|
+
type: taskType,
|
|
186
|
+
title: taskTitle || `Test ${taskType} - rappel ${reminderMinutes}min`,
|
|
187
|
+
description: `<p>Tâche de test créée depuis /dev. Programmée dans ${taskInMinutes} min, rappel ${reminderMinutes} min avant.</p>`,
|
|
188
|
+
scheduledAt: scheduledAt.toISOString(),
|
|
189
|
+
reminderMinutesBefore: reminderMinutes,
|
|
190
|
+
priority: taskPriority,
|
|
191
|
+
}),
|
|
192
|
+
});
|
|
193
|
+
const data = await res.json();
|
|
194
|
+
if (!res.ok) {
|
|
195
|
+
toast.error(data.error || 'Erreur lors de la création');
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
setCreatedTasks((prev) => [
|
|
199
|
+
{
|
|
200
|
+
id: data.id,
|
|
201
|
+
type: data.type,
|
|
202
|
+
title: data.title || data.type,
|
|
203
|
+
scheduledAt: data.scheduledAt,
|
|
204
|
+
reminderMinutesBefore: data.reminderMinutesBefore,
|
|
205
|
+
},
|
|
206
|
+
...prev,
|
|
207
|
+
]);
|
|
208
|
+
const reminderAt = new Date(scheduledAt.getTime() - reminderMinutes * 60 * 1000);
|
|
209
|
+
const secsUntilReminder = Math.round((reminderAt.getTime() - Date.now()) / 1000);
|
|
210
|
+
toast.success(
|
|
211
|
+
`Tâche créée. Rappel dans ${secsUntilReminder > 0 ? `${secsUntilReminder}s` : 'maintenant (déjà passé)'}`,
|
|
212
|
+
);
|
|
213
|
+
} catch (err: any) {
|
|
214
|
+
toast.error(err.message || 'Erreur réseau');
|
|
215
|
+
} finally {
|
|
216
|
+
setCreating(false);
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const createQuickReminder = async (label: string, taskMin: number, reminderMin: number) => {
|
|
221
|
+
setCreating(true);
|
|
222
|
+
try {
|
|
223
|
+
const scheduledAt = new Date(Date.now() + taskMin * 60 * 1000);
|
|
224
|
+
const res = await fetch('/api/tasks', {
|
|
225
|
+
method: 'POST',
|
|
226
|
+
headers: { 'Content-Type': 'application/json' },
|
|
227
|
+
body: JSON.stringify({
|
|
228
|
+
type: 'CALL',
|
|
229
|
+
title: label,
|
|
230
|
+
description: `<p>Quick test: ${label}</p>`,
|
|
231
|
+
scheduledAt: scheduledAt.toISOString(),
|
|
232
|
+
reminderMinutesBefore: reminderMin,
|
|
233
|
+
priority: 'HIGH',
|
|
234
|
+
}),
|
|
235
|
+
});
|
|
236
|
+
const data = await res.json();
|
|
237
|
+
if (!res.ok) {
|
|
238
|
+
toast.error(data.error || 'Erreur');
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
setCreatedTasks((prev) => [
|
|
242
|
+
{
|
|
243
|
+
id: data.id,
|
|
244
|
+
type: data.type,
|
|
245
|
+
title: data.title,
|
|
246
|
+
scheduledAt: data.scheduledAt,
|
|
247
|
+
reminderMinutesBefore: data.reminderMinutesBefore,
|
|
248
|
+
},
|
|
249
|
+
...prev,
|
|
250
|
+
]);
|
|
251
|
+
toast.success(`"${label}" créé`);
|
|
252
|
+
} catch (err: any) {
|
|
253
|
+
toast.error(err.message);
|
|
254
|
+
} finally {
|
|
255
|
+
setCreating(false);
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const fetchReminders = async () => {
|
|
260
|
+
setLoadingReminders(true);
|
|
261
|
+
try {
|
|
262
|
+
const res = await fetch('/api/reminders');
|
|
263
|
+
const data = await res.json();
|
|
264
|
+
setReminders(Array.isArray(data) ? data : []);
|
|
265
|
+
toast.info(`${Array.isArray(data) ? data.length : 0} rappel(s) trouvé(s)`);
|
|
266
|
+
} catch (err: any) {
|
|
267
|
+
toast.error(err.message);
|
|
268
|
+
} finally {
|
|
269
|
+
setLoadingReminders(false);
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
const deleteTask = async (taskId: string) => {
|
|
274
|
+
try {
|
|
275
|
+
const res = await fetch(`/api/tasks/${taskId}`, { method: 'DELETE' });
|
|
276
|
+
if (res.ok) {
|
|
277
|
+
setCreatedTasks((prev) => prev.filter((t) => t.id !== taskId));
|
|
278
|
+
toast.success('Tâche supprimée');
|
|
279
|
+
} else {
|
|
280
|
+
const data = await res.json();
|
|
281
|
+
toast.error(data.error || 'Erreur suppression');
|
|
282
|
+
}
|
|
283
|
+
} catch (err: any) {
|
|
284
|
+
toast.error(err.message);
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
// --- Dev reminder test (from settings, via /api/dev/reminders/test) ---
|
|
289
|
+
const handleCreateDevReminder = async (e: React.FormEvent<HTMLFormElement>) => {
|
|
290
|
+
e.preventDefault();
|
|
291
|
+
const offsetMinutes = Number(devReminderForm.offsetMinutes);
|
|
292
|
+
const reminderMinutesBefore = Number(devReminderForm.reminderMinutesBefore);
|
|
293
|
+
if (Number.isNaN(offsetMinutes) || offsetMinutes < 1) {
|
|
294
|
+
toast.error("Le délai avant l'échéance doit être d'au moins 1 minute.");
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
if (Number.isNaN(reminderMinutesBefore) || reminderMinutesBefore < 1) {
|
|
298
|
+
toast.error('Le rappel doit être supérieur ou égal à 1 minute.');
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
setDevReminderLoading(true);
|
|
302
|
+
try {
|
|
303
|
+
const scheduledAt = new Date(Date.now() + offsetMinutes * 60 * 1000).toISOString();
|
|
304
|
+
const response = await fetch('/api/dev/reminders/test', {
|
|
305
|
+
method: 'POST',
|
|
306
|
+
headers: { 'Content-Type': 'application/json' },
|
|
307
|
+
body: JSON.stringify({
|
|
308
|
+
title: devReminderForm.title,
|
|
309
|
+
reminderMinutesBefore,
|
|
310
|
+
scheduledAt,
|
|
311
|
+
sendNow: devReminderForm.sendNow,
|
|
312
|
+
contactId: devSelectedContact?.id ?? undefined,
|
|
313
|
+
}),
|
|
314
|
+
});
|
|
315
|
+
const data = await response.json();
|
|
316
|
+
if (!response.ok) {
|
|
317
|
+
throw new Error(data.error || 'Impossible de créer le rappel de test.');
|
|
318
|
+
}
|
|
319
|
+
toast.success('Rappel de test créé.');
|
|
320
|
+
requestRemindersRefresh();
|
|
321
|
+
if (data.immediateNotification?.message) {
|
|
322
|
+
toast.persistent(data.immediateNotification.tone ?? 'warning', data.immediateNotification.message, {
|
|
323
|
+
actionLink: data.immediateNotification.link,
|
|
324
|
+
actionLabel: data.immediateNotification.actionLabel,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
} catch (error) {
|
|
328
|
+
toast.error(devToast('Impossible de créer le rappel.', error));
|
|
329
|
+
} finally {
|
|
330
|
+
setDevReminderLoading(false);
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
const handleSearchContacts = async (
|
|
335
|
+
search: string,
|
|
336
|
+
setResults: (r: Array<{ id: string; firstName: string | null; lastName: string | null; phone: string }>) => void,
|
|
337
|
+
setLoading: (l: boolean) => void,
|
|
338
|
+
) => {
|
|
339
|
+
const trimmed = search.trim();
|
|
340
|
+
if (!trimmed) {
|
|
341
|
+
setResults([]);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
setLoading(true);
|
|
345
|
+
try {
|
|
346
|
+
const response = await fetch(`/api/contacts?search=${encodeURIComponent(trimmed)}&page=1&limit=8`);
|
|
347
|
+
const data = await response.json();
|
|
348
|
+
if (!response.ok) throw new Error(data.error || 'Erreur recherche');
|
|
349
|
+
setResults(Array.isArray(data.contacts) ? data.contacts : []);
|
|
350
|
+
} catch (error) {
|
|
351
|
+
toast.error(devToast('Erreur recherche contacts.', error));
|
|
352
|
+
} finally {
|
|
353
|
+
setLoading(false);
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
// --- Contact creation ---
|
|
358
|
+
const createContact = async () => {
|
|
359
|
+
if (!contactForm.phone.trim()) {
|
|
360
|
+
toast.error('Le téléphone est requis.');
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
setContactCreating(true);
|
|
364
|
+
try {
|
|
365
|
+
const res = await fetch('/api/contacts', {
|
|
366
|
+
method: 'POST',
|
|
367
|
+
headers: { 'Content-Type': 'application/json' },
|
|
368
|
+
body: JSON.stringify({
|
|
369
|
+
firstName: contactForm.firstName || undefined,
|
|
370
|
+
lastName: contactForm.lastName || undefined,
|
|
371
|
+
phone: contactForm.phone,
|
|
372
|
+
email: contactForm.email || undefined,
|
|
373
|
+
}),
|
|
374
|
+
});
|
|
375
|
+
const data = await res.json();
|
|
376
|
+
if (!res.ok) {
|
|
377
|
+
toast.error(data.error || 'Erreur création contact');
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
setCreatedContacts((prev) => [
|
|
381
|
+
{ id: data.id, firstName: data.firstName, lastName: data.lastName, phone: data.phone },
|
|
382
|
+
...prev,
|
|
383
|
+
]);
|
|
384
|
+
toast.success(`Contact "${data.firstName || ''} ${data.lastName || ''}".trim() créé`);
|
|
385
|
+
setContactForm({ firstName: '', lastName: '', phone: '', email: '' });
|
|
386
|
+
} catch (err: any) {
|
|
387
|
+
toast.error(err.message);
|
|
388
|
+
} finally {
|
|
389
|
+
setContactCreating(false);
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
// --- Company creation ---
|
|
394
|
+
const createCompany = async () => {
|
|
395
|
+
if (!companyForm.name.trim()) {
|
|
396
|
+
toast.error('Le nom est requis.');
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
setCompanyCreating(true);
|
|
400
|
+
try {
|
|
401
|
+
const res = await fetch('/api/companies', {
|
|
402
|
+
method: 'POST',
|
|
403
|
+
headers: { 'Content-Type': 'application/json' },
|
|
404
|
+
body: JSON.stringify({
|
|
405
|
+
name: companyForm.name,
|
|
406
|
+
phone: companyForm.phone || undefined,
|
|
407
|
+
email: companyForm.email || undefined,
|
|
408
|
+
}),
|
|
409
|
+
});
|
|
410
|
+
const data = await res.json();
|
|
411
|
+
if (!res.ok) {
|
|
412
|
+
toast.error(data.error || 'Erreur création entreprise');
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
setCreatedCompanies((prev) => [{ id: data.id, name: data.name }, ...prev]);
|
|
416
|
+
toast.success(`Entreprise "${data.name}" créée`);
|
|
417
|
+
setCompanyForm({ name: '', phone: '', email: '' });
|
|
418
|
+
} catch (err: any) {
|
|
419
|
+
toast.error(err.message);
|
|
420
|
+
} finally {
|
|
421
|
+
setCompanyCreating(false);
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
// --- Workflow trigger ---
|
|
426
|
+
const searchWorkflowContacts = async () => {
|
|
427
|
+
await handleSearchContacts(workflowContactSearch, setWorkflowContactResults, () => {});
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
const selectWorkflowContact = async (contact: { id: string; firstName: string | null; lastName: string | null }) => {
|
|
431
|
+
setWorkflowSelectedContact(contact);
|
|
432
|
+
setWorkflowContactResults([]);
|
|
433
|
+
try {
|
|
434
|
+
const res = await fetch(`/api/contacts/${contact.id}/workflows/run`);
|
|
435
|
+
const data = await res.json();
|
|
436
|
+
setAvailableWorkflows(Array.isArray(data) ? data : []);
|
|
437
|
+
} catch {
|
|
438
|
+
setAvailableWorkflows([]);
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
const triggerWorkflow = async (workflowId: string) => {
|
|
443
|
+
if (!workflowSelectedContact) return;
|
|
444
|
+
setWorkflowLoading(true);
|
|
445
|
+
try {
|
|
446
|
+
const res = await fetch(`/api/contacts/${workflowSelectedContact.id}/workflows/run`, {
|
|
447
|
+
method: 'POST',
|
|
448
|
+
headers: { 'Content-Type': 'application/json' },
|
|
449
|
+
body: JSON.stringify({ workflowId }),
|
|
450
|
+
});
|
|
451
|
+
const data = await res.json();
|
|
452
|
+
if (!res.ok) {
|
|
453
|
+
toast.error(data.error || 'Erreur exécution workflow');
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
toast.success(data.message || 'Workflow exécuté');
|
|
457
|
+
} catch (err: any) {
|
|
458
|
+
toast.error(err.message);
|
|
459
|
+
} finally {
|
|
460
|
+
setWorkflowLoading(false);
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
// --- SMTP test ---
|
|
465
|
+
const loadSmtpConfig = async () => {
|
|
466
|
+
try {
|
|
467
|
+
const res = await fetch('/api/settings/smtp');
|
|
468
|
+
const data = await res.json();
|
|
469
|
+
if (data && data.host) {
|
|
470
|
+
setSmtpConfig(data);
|
|
471
|
+
setSmtpLoaded(true);
|
|
472
|
+
} else {
|
|
473
|
+
toast.warning('Aucune configuration SMTP trouvée. Configurez-la dans Paramètres > Système.');
|
|
474
|
+
setSmtpLoaded(true);
|
|
475
|
+
}
|
|
476
|
+
} catch (err: any) {
|
|
477
|
+
toast.error(err.message);
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
const testSmtp = async () => {
|
|
482
|
+
if (!smtpConfig) {
|
|
483
|
+
toast.error('Aucune configuration SMTP disponible.');
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
if (!smtpPassword) {
|
|
487
|
+
toast.error('Le mot de passe SMTP est requis pour tester.');
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
setSmtpTesting(true);
|
|
491
|
+
try {
|
|
492
|
+
const res = await fetch('/api/settings/smtp/test', {
|
|
493
|
+
method: 'POST',
|
|
494
|
+
headers: { 'Content-Type': 'application/json' },
|
|
495
|
+
body: JSON.stringify({
|
|
496
|
+
...smtpConfig,
|
|
497
|
+
password: smtpPassword,
|
|
498
|
+
}),
|
|
499
|
+
});
|
|
500
|
+
const data = await res.json();
|
|
501
|
+
if (data.success) {
|
|
502
|
+
toast.success(data.message || 'Connexion SMTP réussie — email de test envoyé');
|
|
503
|
+
} else {
|
|
504
|
+
toast.error(data.error || data.message || 'Erreur SMTP');
|
|
505
|
+
}
|
|
506
|
+
} catch (err: any) {
|
|
507
|
+
toast.error(err.message);
|
|
508
|
+
} finally {
|
|
509
|
+
setSmtpTesting(false);
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
// --- Current user permissions ---
|
|
514
|
+
const fetchUserInfo = async () => {
|
|
515
|
+
setLoadingUser(true);
|
|
516
|
+
try {
|
|
517
|
+
const res = await fetch('/api/users/me');
|
|
518
|
+
const data = await res.json();
|
|
519
|
+
setUserInfo(data);
|
|
520
|
+
} catch (err: any) {
|
|
521
|
+
toast.error(err.message);
|
|
522
|
+
} finally {
|
|
523
|
+
setLoadingUser(false);
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
// --- Audit logs ---
|
|
528
|
+
const fetchAuditLogs = async () => {
|
|
529
|
+
setLoadingAudit(true);
|
|
530
|
+
try {
|
|
531
|
+
const res = await fetch('/api/audit-logs?limit=10');
|
|
532
|
+
const data = await res.json();
|
|
533
|
+
setAuditLogs(Array.isArray(data) ? data : []);
|
|
534
|
+
toast.info(`${Array.isArray(data) ? data.length : 0} log(s) trouvé(s)`);
|
|
535
|
+
} catch (err: any) {
|
|
536
|
+
toast.error(err.message);
|
|
537
|
+
} finally {
|
|
538
|
+
setLoadingAudit(false);
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
543
|
+
// Render
|
|
544
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
545
|
+
|
|
546
|
+
return (
|
|
547
|
+
<div className="mx-auto max-w-3xl space-y-8 p-6">
|
|
548
|
+
{/* ── Header ─────────────────────────────────────────────────────────── */}
|
|
549
|
+
<div className="flex items-center gap-3">
|
|
550
|
+
<FlaskConical className="h-7 w-7 text-purple-600" />
|
|
551
|
+
<div>
|
|
552
|
+
<h1 className="text-2xl font-bold text-gray-900">Outils de développement</h1>
|
|
553
|
+
<p className="text-sm text-gray-500">
|
|
554
|
+
Page de dev pour tester rappels, toasts, contacts, workflows et plus.
|
|
555
|
+
</p>
|
|
556
|
+
</div>
|
|
557
|
+
</div>
|
|
558
|
+
|
|
559
|
+
{/* ── B. Presets rapides ──────────────────────────────────────────────── */}
|
|
560
|
+
<section className="ui-slide-up rounded-xl border border-purple-200 bg-purple-50 p-5">
|
|
561
|
+
<h2 className="mb-3 flex items-center gap-2 text-base font-semibold text-purple-900">
|
|
562
|
+
<Zap className="h-4 w-4" />
|
|
563
|
+
Presets rapides
|
|
564
|
+
</h2>
|
|
565
|
+
<div className="flex flex-wrap gap-2">
|
|
566
|
+
<button
|
|
567
|
+
onClick={() => createQuickReminder('Rappel immédiat', 1, 1)}
|
|
568
|
+
disabled={creating}
|
|
569
|
+
className="rounded-lg bg-red-600 px-3 py-2 text-sm font-medium text-white transition hover:bg-red-700 disabled:opacity-50"
|
|
570
|
+
>
|
|
571
|
+
Rappel immédiat (1min)
|
|
572
|
+
</button>
|
|
573
|
+
<button
|
|
574
|
+
onClick={() => createQuickReminder('Rappel dans 2min', 5, 3)}
|
|
575
|
+
disabled={creating}
|
|
576
|
+
className="rounded-lg bg-orange-600 px-3 py-2 text-sm font-medium text-white transition hover:bg-orange-700 disabled:opacity-50"
|
|
577
|
+
>
|
|
578
|
+
Rappel dans 2min (tâche 5min)
|
|
579
|
+
</button>
|
|
580
|
+
<button
|
|
581
|
+
onClick={() => createQuickReminder('Rappel dans 5min', 10, 5)}
|
|
582
|
+
disabled={creating}
|
|
583
|
+
className="rounded-lg bg-primary px-3 py-2 text-sm font-medium text-primary-foreground transition hover:bg-primary/90 disabled:opacity-50"
|
|
584
|
+
>
|
|
585
|
+
Rappel dans 5min (tâche 10min)
|
|
586
|
+
</button>
|
|
587
|
+
<button
|
|
588
|
+
onClick={() => createQuickReminder('Tâche déjà en rappel', 30, 60)}
|
|
589
|
+
disabled={creating}
|
|
590
|
+
className="rounded-lg bg-green-600 px-3 py-2 text-sm font-medium text-white transition hover:bg-green-700 disabled:opacity-50"
|
|
591
|
+
>
|
|
592
|
+
Rappel déjà passé (tâche 30min)
|
|
593
|
+
</button>
|
|
594
|
+
</div>
|
|
595
|
+
</section>
|
|
596
|
+
|
|
597
|
+
{/* ── C. Création tâche personnalisée ─────────────────────────────────── */}
|
|
598
|
+
<Section title="Créer une tâche avec rappel" icon={Plus}>
|
|
599
|
+
<div className="grid grid-cols-2 gap-4">
|
|
600
|
+
<div>
|
|
601
|
+
<label className="mb-1 block text-sm font-medium text-gray-700">Type</label>
|
|
602
|
+
<select
|
|
603
|
+
value={taskType}
|
|
604
|
+
onChange={(e) => setTaskType(e.target.value)}
|
|
605
|
+
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm"
|
|
606
|
+
>
|
|
607
|
+
<option value="CALL">Appel</option>
|
|
608
|
+
<option value="MEETING">Rendez-vous</option>
|
|
609
|
+
<option value="EMAIL">Email</option>
|
|
610
|
+
<option value="VIDEO_CONFERENCE">Visioconférence</option>
|
|
611
|
+
<option value="TASK">Tâche</option>
|
|
612
|
+
<option value="OTHER">Autre</option>
|
|
613
|
+
</select>
|
|
614
|
+
</div>
|
|
615
|
+
<div>
|
|
616
|
+
<label className="mb-1 block text-sm font-medium text-gray-700">Priorité</label>
|
|
617
|
+
<select
|
|
618
|
+
value={taskPriority}
|
|
619
|
+
onChange={(e) => setTaskPriority(e.target.value)}
|
|
620
|
+
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm"
|
|
621
|
+
>
|
|
622
|
+
<option value="LOW">Basse</option>
|
|
623
|
+
<option value="MEDIUM">Moyenne</option>
|
|
624
|
+
<option value="HIGH">Haute</option>
|
|
625
|
+
<option value="URGENT">Urgente</option>
|
|
626
|
+
</select>
|
|
627
|
+
</div>
|
|
628
|
+
<div className="col-span-2">
|
|
629
|
+
<label className="mb-1 block text-sm font-medium text-gray-700">Titre</label>
|
|
630
|
+
<input
|
|
631
|
+
type="text"
|
|
632
|
+
value={taskTitle}
|
|
633
|
+
onChange={(e) => setTaskTitle(e.target.value)}
|
|
634
|
+
placeholder="Titre de la tâche..."
|
|
635
|
+
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm"
|
|
636
|
+
/>
|
|
637
|
+
</div>
|
|
638
|
+
<div>
|
|
639
|
+
<label className="mb-1 block text-sm font-medium text-gray-700">
|
|
640
|
+
<Clock className="mr-1 inline h-3.5 w-3.5" />
|
|
641
|
+
Tâche dans (minutes)
|
|
642
|
+
</label>
|
|
643
|
+
<input
|
|
644
|
+
type="number"
|
|
645
|
+
min={1}
|
|
646
|
+
max={1440}
|
|
647
|
+
value={taskInMinutes}
|
|
648
|
+
onChange={(e) => setTaskInMinutes(Number(e.target.value))}
|
|
649
|
+
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm"
|
|
650
|
+
/>
|
|
651
|
+
</div>
|
|
652
|
+
<div>
|
|
653
|
+
<label className="mb-1 block text-sm font-medium text-gray-700">
|
|
654
|
+
<Bell className="mr-1 inline h-3.5 w-3.5" />
|
|
655
|
+
Rappel (minutes avant)
|
|
656
|
+
</label>
|
|
657
|
+
<input
|
|
658
|
+
type="number"
|
|
659
|
+
min={0}
|
|
660
|
+
max={1440}
|
|
661
|
+
value={reminderMinutes}
|
|
662
|
+
onChange={(e) => setReminderMinutes(Number(e.target.value))}
|
|
663
|
+
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm"
|
|
664
|
+
/>
|
|
665
|
+
</div>
|
|
666
|
+
</div>
|
|
667
|
+
<div className="mt-4 flex items-center gap-3">
|
|
668
|
+
<button
|
|
669
|
+
onClick={createTestTask}
|
|
670
|
+
disabled={creating}
|
|
671
|
+
className="flex items-center gap-2 rounded-lg bg-gray-900 px-4 py-2 text-sm font-medium text-white transition hover:bg-gray-800 disabled:opacity-50"
|
|
672
|
+
>
|
|
673
|
+
<Plus className="h-4 w-4" />
|
|
674
|
+
{creating ? 'Création...' : 'Créer la tâche'}
|
|
675
|
+
</button>
|
|
676
|
+
<span className="text-xs text-gray-500">
|
|
677
|
+
{reminderMinutes < taskInMinutes
|
|
678
|
+
? `Le rappel se déclenchera dans ~${taskInMinutes - reminderMinutes} min`
|
|
679
|
+
: 'Le rappel sera immédiat (déjà passé)'}
|
|
680
|
+
</span>
|
|
681
|
+
</div>
|
|
682
|
+
</Section>
|
|
683
|
+
|
|
684
|
+
{/* ── D. Test rappel dev (from settings) ─────────────────────────────── */}
|
|
685
|
+
<Section title="Test rappel dev (API /api/dev/reminders/test)" icon={Bell}>
|
|
686
|
+
<form onSubmit={handleCreateDevReminder} className="space-y-4">
|
|
687
|
+
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
|
688
|
+
<div className="md:col-span-3">
|
|
689
|
+
<label className="block text-sm font-medium text-gray-700">Titre</label>
|
|
690
|
+
<input
|
|
691
|
+
type="text"
|
|
692
|
+
value={devReminderForm.title}
|
|
693
|
+
onChange={(e) => setDevReminderForm((prev) => ({ ...prev, title: e.target.value }))}
|
|
694
|
+
className="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 text-sm"
|
|
695
|
+
placeholder="Rappel de test"
|
|
696
|
+
/>
|
|
697
|
+
</div>
|
|
698
|
+
|
|
699
|
+
<div className="space-y-3 md:col-span-3">
|
|
700
|
+
<label className="block text-sm font-medium text-gray-700">Contact lié (optionnel)</label>
|
|
701
|
+
<div className="flex gap-2">
|
|
702
|
+
<input
|
|
703
|
+
type="text"
|
|
704
|
+
value={devContactSearch}
|
|
705
|
+
onChange={(e) => setDevContactSearch(e.target.value)}
|
|
706
|
+
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm"
|
|
707
|
+
placeholder="Nom, prénom, email ou téléphone"
|
|
708
|
+
/>
|
|
709
|
+
<button
|
|
710
|
+
type="button"
|
|
711
|
+
onClick={() => handleSearchContacts(devContactSearch, setDevContactResults, setDevContactLoading)}
|
|
712
|
+
disabled={devContactLoading}
|
|
713
|
+
className="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 disabled:opacity-50"
|
|
714
|
+
>
|
|
715
|
+
{devContactLoading ? 'Recherche...' : 'Rechercher'}
|
|
716
|
+
</button>
|
|
717
|
+
</div>
|
|
718
|
+
{devSelectedContact && (
|
|
719
|
+
<div className="rounded-lg border border-blue-200 bg-blue-50 px-3 py-2 text-sm text-blue-900">
|
|
720
|
+
Contact sélectionné:{' '}
|
|
721
|
+
<span className="font-medium">
|
|
722
|
+
{`${devSelectedContact.firstName || ''} ${devSelectedContact.lastName || ''}`.trim() ||
|
|
723
|
+
devSelectedContact.phone}
|
|
724
|
+
</span>{' '}
|
|
725
|
+
<button
|
|
726
|
+
type="button"
|
|
727
|
+
onClick={() => setDevSelectedContact(null)}
|
|
728
|
+
className="ml-2 cursor-pointer text-blue-700 underline underline-offset-2"
|
|
729
|
+
>
|
|
730
|
+
Retirer
|
|
731
|
+
</button>
|
|
732
|
+
</div>
|
|
733
|
+
)}
|
|
734
|
+
{!devSelectedContact && devContactResults.length > 0 && (
|
|
735
|
+
<div className="max-h-44 space-y-2 overflow-y-auto rounded-lg border border-gray-200 p-2">
|
|
736
|
+
{devContactResults.map((contact) => {
|
|
737
|
+
const fullName =
|
|
738
|
+
`${contact.firstName || ''} ${contact.lastName || ''}`.trim() || 'Contact sans nom';
|
|
739
|
+
return (
|
|
740
|
+
<button
|
|
741
|
+
key={contact.id}
|
|
742
|
+
type="button"
|
|
743
|
+
onClick={() => setDevSelectedContact(contact)}
|
|
744
|
+
className="w-full cursor-pointer rounded-md border border-gray-200 px-3 py-2 text-left text-sm transition-colors hover:bg-gray-50"
|
|
745
|
+
>
|
|
746
|
+
<p className="font-medium text-gray-900">{fullName}</p>
|
|
747
|
+
<p className="text-xs text-gray-600">{contact.phone}</p>
|
|
748
|
+
</button>
|
|
749
|
+
);
|
|
750
|
+
})}
|
|
751
|
+
</div>
|
|
752
|
+
)}
|
|
753
|
+
</div>
|
|
754
|
+
|
|
755
|
+
<div>
|
|
756
|
+
<label className="block text-sm font-medium text-gray-700">Rappel (minutes avant)</label>
|
|
757
|
+
<input
|
|
758
|
+
type="number"
|
|
759
|
+
min={1}
|
|
760
|
+
value={devReminderForm.reminderMinutesBefore}
|
|
761
|
+
onChange={(e) =>
|
|
762
|
+
setDevReminderForm((prev) => ({ ...prev, reminderMinutesBefore: Number(e.target.value) }))
|
|
763
|
+
}
|
|
764
|
+
className="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 text-sm"
|
|
765
|
+
/>
|
|
766
|
+
</div>
|
|
767
|
+
<div>
|
|
768
|
+
<label className="block text-sm font-medium text-gray-700">Échéance dans (minutes)</label>
|
|
769
|
+
<input
|
|
770
|
+
type="number"
|
|
771
|
+
min={1}
|
|
772
|
+
value={devReminderForm.offsetMinutes}
|
|
773
|
+
onChange={(e) =>
|
|
774
|
+
setDevReminderForm((prev) => ({ ...prev, offsetMinutes: Number(e.target.value) }))
|
|
775
|
+
}
|
|
776
|
+
className="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 text-sm"
|
|
777
|
+
/>
|
|
778
|
+
</div>
|
|
779
|
+
<div className="flex items-center gap-3 pt-6">
|
|
780
|
+
<label className="relative inline-flex cursor-pointer items-center">
|
|
781
|
+
<input
|
|
782
|
+
aria-label="Envoyer une notification immédiate"
|
|
783
|
+
type="checkbox"
|
|
784
|
+
checked={devReminderForm.sendNow}
|
|
785
|
+
onChange={(e) => setDevReminderForm((prev) => ({ ...prev, sendNow: e.target.checked }))}
|
|
786
|
+
className="peer sr-only"
|
|
787
|
+
/>
|
|
788
|
+
<div className="peer h-5 w-9 rounded-full bg-gray-200 peer-checked:bg-blue-600 peer-focus:ring-2 peer-focus:ring-gray-400/30 after:absolute after:top-[2px] after:left-[2px] after:h-4 after:w-4 after:rounded-full after:border after:border-gray-300 after:bg-white after:transition-transform after:content-[''] peer-checked:after:translate-x-full peer-checked:after:border-white" />
|
|
789
|
+
</label>
|
|
790
|
+
<span className="text-sm text-gray-700">Notification immédiate</span>
|
|
791
|
+
</div>
|
|
792
|
+
</div>
|
|
793
|
+
<div className="flex justify-end">
|
|
794
|
+
<button
|
|
795
|
+
type="submit"
|
|
796
|
+
disabled={devReminderLoading}
|
|
797
|
+
className="cursor-pointer rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700 disabled:opacity-50"
|
|
798
|
+
>
|
|
799
|
+
{devReminderLoading ? 'Création...' : 'Créer un rappel de test'}
|
|
800
|
+
</button>
|
|
801
|
+
</div>
|
|
802
|
+
</form>
|
|
803
|
+
</Section>
|
|
804
|
+
|
|
805
|
+
{/* ── E. Rappels actifs ──────────────────────────────────────────────── */}
|
|
806
|
+
<Section title="Rappels actifs (API /api/reminders)" icon={Bell}>
|
|
807
|
+
<div className="mb-4 flex items-center justify-between">
|
|
808
|
+
<button
|
|
809
|
+
onClick={fetchReminders}
|
|
810
|
+
disabled={loadingReminders}
|
|
811
|
+
className="flex items-center gap-1.5 rounded-lg border border-gray-300 bg-white px-3 py-1.5 text-sm font-medium text-gray-700 transition hover:bg-gray-50 disabled:opacity-50"
|
|
812
|
+
>
|
|
813
|
+
<RefreshCw className={`h-3.5 w-3.5 ${loadingReminders ? 'animate-spin' : ''}`} />
|
|
814
|
+
Rafraîchir
|
|
815
|
+
</button>
|
|
816
|
+
</div>
|
|
817
|
+
{reminders.length === 0 ? (
|
|
818
|
+
<p className="text-sm text-gray-500">
|
|
819
|
+
Aucun rappel. Cliquez sur "Rafraîchir" pour charger.
|
|
820
|
+
</p>
|
|
821
|
+
) : (
|
|
822
|
+
<div className="space-y-2">
|
|
823
|
+
{reminders.map((r: any) => {
|
|
824
|
+
const reminderTime = new Date(r.reminderTime);
|
|
825
|
+
const scheduledAt = new Date(r.scheduledAt);
|
|
826
|
+
const now = new Date();
|
|
827
|
+
const isActive = reminderTime <= now && now < scheduledAt;
|
|
828
|
+
const isPast = now >= scheduledAt;
|
|
829
|
+
return (
|
|
830
|
+
<div
|
|
831
|
+
key={r.id}
|
|
832
|
+
className={`flex items-center justify-between rounded-lg border p-3 text-sm ${
|
|
833
|
+
isActive
|
|
834
|
+
? 'border-orange-300 bg-orange-50'
|
|
835
|
+
: isPast
|
|
836
|
+
? 'border-gray-200 bg-gray-50 opacity-60'
|
|
837
|
+
: 'border-blue-200 bg-blue-50'
|
|
838
|
+
}`}
|
|
839
|
+
>
|
|
840
|
+
<div>
|
|
841
|
+
<div className="font-medium text-gray-900">
|
|
842
|
+
{r.title || r.type}
|
|
843
|
+
<span
|
|
844
|
+
className={`ml-2 inline-block rounded-full px-2 py-0.5 text-xs font-medium ${
|
|
845
|
+
r.priority === 'URGENT'
|
|
846
|
+
? 'bg-red-100 text-red-700'
|
|
847
|
+
: r.priority === 'HIGH'
|
|
848
|
+
? 'bg-orange-100 text-orange-700'
|
|
849
|
+
: r.priority === 'MEDIUM'
|
|
850
|
+
? 'bg-yellow-100 text-yellow-700'
|
|
851
|
+
: 'bg-gray-100 text-gray-600'
|
|
852
|
+
}`}
|
|
853
|
+
>
|
|
854
|
+
{r.priority}
|
|
855
|
+
</span>
|
|
856
|
+
{isActive && (
|
|
857
|
+
<span className="ml-2 inline-block rounded-full bg-orange-200 px-2 py-0.5 text-xs font-medium text-orange-800">
|
|
858
|
+
ACTIF
|
|
859
|
+
</span>
|
|
860
|
+
)}
|
|
861
|
+
</div>
|
|
862
|
+
<div className="mt-0.5 text-xs text-gray-500">
|
|
863
|
+
Rappel: {reminderTime.toLocaleTimeString('fr-FR')} | Tâche:{' '}
|
|
864
|
+
{scheduledAt.toLocaleTimeString('fr-FR')} | {r.reminderMinutesBefore}min avant
|
|
865
|
+
</div>
|
|
866
|
+
{r.contact && (
|
|
867
|
+
<div className="mt-0.5 text-xs text-gray-400">
|
|
868
|
+
Contact: {r.contact.firstName} {r.contact.lastName}
|
|
869
|
+
</div>
|
|
870
|
+
)}
|
|
871
|
+
</div>
|
|
872
|
+
</div>
|
|
873
|
+
);
|
|
874
|
+
})}
|
|
875
|
+
</div>
|
|
876
|
+
)}
|
|
877
|
+
</Section>
|
|
878
|
+
|
|
879
|
+
{/* ── F. Tâches créées cette session ─────────────────────────────────── */}
|
|
880
|
+
{createdTasks.length > 0 && (
|
|
881
|
+
<Section title={`Tâches créées cette session (${createdTasks.length})`} icon={Clock}>
|
|
882
|
+
<div className="space-y-2">
|
|
883
|
+
{createdTasks.map((task) => {
|
|
884
|
+
const scheduledAt = new Date(task.scheduledAt);
|
|
885
|
+
const reminderAt = new Date(scheduledAt.getTime() - task.reminderMinutesBefore * 60 * 1000);
|
|
886
|
+
return (
|
|
887
|
+
<div
|
|
888
|
+
key={task.id}
|
|
889
|
+
className="flex items-center justify-between rounded-lg border border-gray-200 p-3 text-sm"
|
|
890
|
+
>
|
|
891
|
+
<div>
|
|
892
|
+
<div className="font-medium text-gray-900">{task.title}</div>
|
|
893
|
+
<div className="text-xs text-gray-500">
|
|
894
|
+
{task.type} | Tâche: {scheduledAt.toLocaleTimeString('fr-FR')} | Rappel:{' '}
|
|
895
|
+
{reminderAt.toLocaleTimeString('fr-FR')} ({task.reminderMinutesBefore}min avant)
|
|
896
|
+
</div>
|
|
897
|
+
</div>
|
|
898
|
+
<button
|
|
899
|
+
onClick={() => deleteTask(task.id)}
|
|
900
|
+
className="rounded-lg p-1.5 text-gray-400 transition hover:bg-red-50 hover:text-red-600"
|
|
901
|
+
title="Supprimer"
|
|
902
|
+
>
|
|
903
|
+
<Trash2 className="h-4 w-4" />
|
|
904
|
+
</button>
|
|
905
|
+
</div>
|
|
906
|
+
);
|
|
907
|
+
})}
|
|
908
|
+
</div>
|
|
909
|
+
</Section>
|
|
910
|
+
)}
|
|
911
|
+
|
|
912
|
+
{/* ── G. Création rapide de contact ──────────────────────────────────── */}
|
|
913
|
+
<Section title="Création rapide de contact" icon={User}>
|
|
914
|
+
<div className="grid grid-cols-2 gap-4">
|
|
915
|
+
<div>
|
|
916
|
+
<label className="mb-1 block text-sm font-medium text-gray-700">Prénom</label>
|
|
917
|
+
<input
|
|
918
|
+
type="text"
|
|
919
|
+
value={contactForm.firstName}
|
|
920
|
+
onChange={(e) => setContactForm((prev) => ({ ...prev, firstName: e.target.value }))}
|
|
921
|
+
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm"
|
|
922
|
+
/>
|
|
923
|
+
</div>
|
|
924
|
+
<div>
|
|
925
|
+
<label className="mb-1 block text-sm font-medium text-gray-700">Nom</label>
|
|
926
|
+
<input
|
|
927
|
+
type="text"
|
|
928
|
+
value={contactForm.lastName}
|
|
929
|
+
onChange={(e) => setContactForm((prev) => ({ ...prev, lastName: e.target.value }))}
|
|
930
|
+
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm"
|
|
931
|
+
/>
|
|
932
|
+
</div>
|
|
933
|
+
<div>
|
|
934
|
+
<label className="mb-1 block text-sm font-medium text-gray-700">
|
|
935
|
+
Téléphone <span className="text-red-500">*</span>
|
|
936
|
+
</label>
|
|
937
|
+
<input
|
|
938
|
+
type="text"
|
|
939
|
+
value={contactForm.phone}
|
|
940
|
+
onChange={(e) => setContactForm((prev) => ({ ...prev, phone: e.target.value }))}
|
|
941
|
+
placeholder="06 12 34 56 78"
|
|
942
|
+
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm"
|
|
943
|
+
/>
|
|
944
|
+
</div>
|
|
945
|
+
<div>
|
|
946
|
+
<label className="mb-1 block text-sm font-medium text-gray-700">Email</label>
|
|
947
|
+
<input
|
|
948
|
+
type="email"
|
|
949
|
+
value={contactForm.email}
|
|
950
|
+
onChange={(e) => setContactForm((prev) => ({ ...prev, email: e.target.value }))}
|
|
951
|
+
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm"
|
|
952
|
+
/>
|
|
953
|
+
</div>
|
|
954
|
+
</div>
|
|
955
|
+
<button
|
|
956
|
+
onClick={createContact}
|
|
957
|
+
disabled={contactCreating}
|
|
958
|
+
className="mt-4 flex items-center gap-2 rounded-lg bg-gray-900 px-4 py-2 text-sm font-medium text-white transition hover:bg-gray-800 disabled:opacity-50"
|
|
959
|
+
>
|
|
960
|
+
<Plus className="h-4 w-4" />
|
|
961
|
+
{contactCreating ? 'Création...' : 'Créer le contact'}
|
|
962
|
+
</button>
|
|
963
|
+
{createdContacts.length > 0 && (
|
|
964
|
+
<div className="mt-4 space-y-2">
|
|
965
|
+
{createdContacts.map((c) => (
|
|
966
|
+
<div key={c.id} className="rounded-lg border border-gray-200 px-3 py-2 text-sm">
|
|
967
|
+
<a href={`/contacts/${c.id}`} className="font-medium text-blue-600 hover:underline">
|
|
968
|
+
{`${c.firstName || ''} ${c.lastName || ''}`.trim() || c.phone}
|
|
969
|
+
</a>
|
|
970
|
+
<span className="ml-2 text-xs text-gray-400">{c.id}</span>
|
|
971
|
+
</div>
|
|
972
|
+
))}
|
|
973
|
+
</div>
|
|
974
|
+
)}
|
|
975
|
+
</Section>
|
|
976
|
+
|
|
977
|
+
{/* ── H. Création rapide d'entreprise ────────────────────────────────── */}
|
|
978
|
+
<Section title="Création rapide d'entreprise" icon={Building2}>
|
|
979
|
+
<div className="grid grid-cols-3 gap-4">
|
|
980
|
+
<div>
|
|
981
|
+
<label className="mb-1 block text-sm font-medium text-gray-700">
|
|
982
|
+
Nom <span className="text-red-500">*</span>
|
|
983
|
+
</label>
|
|
984
|
+
<input
|
|
985
|
+
type="text"
|
|
986
|
+
value={companyForm.name}
|
|
987
|
+
onChange={(e) => setCompanyForm((prev) => ({ ...prev, name: e.target.value }))}
|
|
988
|
+
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm"
|
|
989
|
+
/>
|
|
990
|
+
</div>
|
|
991
|
+
<div>
|
|
992
|
+
<label className="mb-1 block text-sm font-medium text-gray-700">Téléphone</label>
|
|
993
|
+
<input
|
|
994
|
+
type="text"
|
|
995
|
+
value={companyForm.phone}
|
|
996
|
+
onChange={(e) => setCompanyForm((prev) => ({ ...prev, phone: e.target.value }))}
|
|
997
|
+
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm"
|
|
998
|
+
/>
|
|
999
|
+
</div>
|
|
1000
|
+
<div>
|
|
1001
|
+
<label className="mb-1 block text-sm font-medium text-gray-700">Email</label>
|
|
1002
|
+
<input
|
|
1003
|
+
type="email"
|
|
1004
|
+
value={companyForm.email}
|
|
1005
|
+
onChange={(e) => setCompanyForm((prev) => ({ ...prev, email: e.target.value }))}
|
|
1006
|
+
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm"
|
|
1007
|
+
/>
|
|
1008
|
+
</div>
|
|
1009
|
+
</div>
|
|
1010
|
+
<button
|
|
1011
|
+
onClick={createCompany}
|
|
1012
|
+
disabled={companyCreating}
|
|
1013
|
+
className="mt-4 flex items-center gap-2 rounded-lg bg-gray-900 px-4 py-2 text-sm font-medium text-white transition hover:bg-gray-800 disabled:opacity-50"
|
|
1014
|
+
>
|
|
1015
|
+
<Plus className="h-4 w-4" />
|
|
1016
|
+
{companyCreating ? 'Création...' : "Créer l'entreprise"}
|
|
1017
|
+
</button>
|
|
1018
|
+
{createdCompanies.length > 0 && (
|
|
1019
|
+
<div className="mt-4 space-y-2">
|
|
1020
|
+
{createdCompanies.map((c) => (
|
|
1021
|
+
<div key={c.id} className="rounded-lg border border-gray-200 px-3 py-2 text-sm">
|
|
1022
|
+
<a href={`/contacts?entity=companies`} className="font-medium text-blue-600 hover:underline">
|
|
1023
|
+
{c.name}
|
|
1024
|
+
</a>
|
|
1025
|
+
<span className="ml-2 text-xs text-gray-400">{c.id}</span>
|
|
1026
|
+
</div>
|
|
1027
|
+
))}
|
|
1028
|
+
</div>
|
|
1029
|
+
)}
|
|
1030
|
+
</Section>
|
|
1031
|
+
|
|
1032
|
+
{/* ── I. Déclenchement manuel de workflow ─────────────────────────────── */}
|
|
1033
|
+
<Section title="Déclenchement manuel de workflow" icon={Play}>
|
|
1034
|
+
<div className="space-y-4">
|
|
1035
|
+
<div>
|
|
1036
|
+
<label className="mb-1 block text-sm font-medium text-gray-700">Rechercher un contact</label>
|
|
1037
|
+
<div className="flex gap-2">
|
|
1038
|
+
<input
|
|
1039
|
+
type="text"
|
|
1040
|
+
value={workflowContactSearch}
|
|
1041
|
+
onChange={(e) => setWorkflowContactSearch(e.target.value)}
|
|
1042
|
+
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm"
|
|
1043
|
+
placeholder="Nom, téléphone, email..."
|
|
1044
|
+
/>
|
|
1045
|
+
<button
|
|
1046
|
+
onClick={searchWorkflowContacts}
|
|
1047
|
+
className="rounded-lg border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"
|
|
1048
|
+
>
|
|
1049
|
+
<Search className="h-4 w-4" />
|
|
1050
|
+
</button>
|
|
1051
|
+
</div>
|
|
1052
|
+
</div>
|
|
1053
|
+
|
|
1054
|
+
{workflowContactResults.length > 0 && !workflowSelectedContact && (
|
|
1055
|
+
<div className="max-h-44 space-y-2 overflow-y-auto rounded-lg border border-gray-200 p-2">
|
|
1056
|
+
{workflowContactResults.map((c) => (
|
|
1057
|
+
<button
|
|
1058
|
+
key={c.id}
|
|
1059
|
+
onClick={() => selectWorkflowContact(c)}
|
|
1060
|
+
className="w-full cursor-pointer rounded-md border border-gray-200 px-3 py-2 text-left text-sm hover:bg-gray-50"
|
|
1061
|
+
>
|
|
1062
|
+
{`${c.firstName || ''} ${c.lastName || ''}`.trim() || c.phone}
|
|
1063
|
+
</button>
|
|
1064
|
+
))}
|
|
1065
|
+
</div>
|
|
1066
|
+
)}
|
|
1067
|
+
|
|
1068
|
+
{workflowSelectedContact && (
|
|
1069
|
+
<div className="rounded-lg border border-blue-200 bg-blue-50 px-3 py-2 text-sm text-blue-900">
|
|
1070
|
+
Contact:{' '}
|
|
1071
|
+
<span className="font-medium">
|
|
1072
|
+
{`${workflowSelectedContact.firstName || ''} ${workflowSelectedContact.lastName || ''}`.trim()}
|
|
1073
|
+
</span>{' '}
|
|
1074
|
+
<button
|
|
1075
|
+
onClick={() => {
|
|
1076
|
+
setWorkflowSelectedContact(null);
|
|
1077
|
+
setAvailableWorkflows([]);
|
|
1078
|
+
}}
|
|
1079
|
+
className="ml-2 cursor-pointer text-blue-700 underline underline-offset-2"
|
|
1080
|
+
>
|
|
1081
|
+
Changer
|
|
1082
|
+
</button>
|
|
1083
|
+
</div>
|
|
1084
|
+
)}
|
|
1085
|
+
|
|
1086
|
+
{workflowSelectedContact && availableWorkflows.length === 0 && (
|
|
1087
|
+
<p className="text-sm text-gray-500">Aucun workflow manuel disponible pour ce contact.</p>
|
|
1088
|
+
)}
|
|
1089
|
+
|
|
1090
|
+
{availableWorkflows.length > 0 && (
|
|
1091
|
+
<div className="space-y-2">
|
|
1092
|
+
{availableWorkflows.map((wf) => (
|
|
1093
|
+
<div key={wf.id} className="flex items-center justify-between rounded-lg border border-gray-200 p-3">
|
|
1094
|
+
<span className="text-sm font-medium text-gray-900">{wf.name}</span>
|
|
1095
|
+
<button
|
|
1096
|
+
onClick={() => triggerWorkflow(wf.id)}
|
|
1097
|
+
disabled={workflowLoading}
|
|
1098
|
+
className="rounded-lg bg-purple-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-purple-700 disabled:opacity-50"
|
|
1099
|
+
>
|
|
1100
|
+
<Play className="mr-1 inline h-3.5 w-3.5" />
|
|
1101
|
+
Exécuter
|
|
1102
|
+
</button>
|
|
1103
|
+
</div>
|
|
1104
|
+
))}
|
|
1105
|
+
</div>
|
|
1106
|
+
)}
|
|
1107
|
+
</div>
|
|
1108
|
+
</Section>
|
|
1109
|
+
|
|
1110
|
+
{/* ── J. Test SMTP ───────────────────────────────────────────────────── */}
|
|
1111
|
+
<Section title="Test SMTP" icon={Mail}>
|
|
1112
|
+
<p className="mb-3 text-sm text-gray-500">
|
|
1113
|
+
Teste la connexion SMTP configurée et envoie un email de test à votre adresse.
|
|
1114
|
+
</p>
|
|
1115
|
+
{!smtpLoaded ? (
|
|
1116
|
+
<button
|
|
1117
|
+
onClick={loadSmtpConfig}
|
|
1118
|
+
className="flex items-center gap-2 rounded-lg border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 transition hover:bg-gray-50"
|
|
1119
|
+
>
|
|
1120
|
+
<RefreshCw className="h-4 w-4" />
|
|
1121
|
+
Charger la configuration SMTP
|
|
1122
|
+
</button>
|
|
1123
|
+
) : !smtpConfig ? (
|
|
1124
|
+
<p className="text-sm text-yellow-600">
|
|
1125
|
+
Aucune configuration SMTP trouvée. Configurez-la dans Paramètres > Système.
|
|
1126
|
+
</p>
|
|
1127
|
+
) : (
|
|
1128
|
+
<div className="space-y-3">
|
|
1129
|
+
<div className="rounded-lg border border-gray-200 p-3 text-sm">
|
|
1130
|
+
<p><span className="font-medium">Serveur:</span> {smtpConfig.host}:{smtpConfig.port} ({smtpConfig.secure ? 'SSL' : 'STARTTLS'})</p>
|
|
1131
|
+
<p><span className="font-medium">Utilisateur:</span> {smtpConfig.username}</p>
|
|
1132
|
+
<p><span className="font-medium">Expéditeur:</span> {smtpConfig.fromName} <{smtpConfig.fromEmail}></p>
|
|
1133
|
+
</div>
|
|
1134
|
+
<div>
|
|
1135
|
+
<label className="mb-1 block text-sm font-medium text-gray-700">Mot de passe SMTP</label>
|
|
1136
|
+
<input
|
|
1137
|
+
type="password"
|
|
1138
|
+
value={smtpPassword}
|
|
1139
|
+
onChange={(e) => setSmtpPassword(e.target.value)}
|
|
1140
|
+
placeholder="Entrez le mot de passe SMTP..."
|
|
1141
|
+
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm"
|
|
1142
|
+
/>
|
|
1143
|
+
</div>
|
|
1144
|
+
<button
|
|
1145
|
+
onClick={testSmtp}
|
|
1146
|
+
disabled={smtpTesting || !smtpPassword}
|
|
1147
|
+
className="flex items-center gap-2 rounded-lg bg-gray-900 px-4 py-2 text-sm font-medium text-white transition hover:bg-gray-800 disabled:opacity-50"
|
|
1148
|
+
>
|
|
1149
|
+
<Send className="h-4 w-4" />
|
|
1150
|
+
{smtpTesting ? 'Test en cours...' : 'Tester la connexion SMTP'}
|
|
1151
|
+
</button>
|
|
1152
|
+
</div>
|
|
1153
|
+
)}
|
|
1154
|
+
</Section>
|
|
1155
|
+
|
|
1156
|
+
{/* ── K. Permissions utilisateur courant ──────────────────────────────── */}
|
|
1157
|
+
<Section title="Permissions utilisateur courant" icon={Shield}>
|
|
1158
|
+
<button
|
|
1159
|
+
onClick={fetchUserInfo}
|
|
1160
|
+
disabled={loadingUser}
|
|
1161
|
+
className="mb-4 flex items-center gap-1.5 rounded-lg border border-gray-300 bg-white px-3 py-1.5 text-sm font-medium text-gray-700 transition hover:bg-gray-50 disabled:opacity-50"
|
|
1162
|
+
>
|
|
1163
|
+
<RefreshCw className={`h-3.5 w-3.5 ${loadingUser ? 'animate-spin' : ''}`} />
|
|
1164
|
+
Charger
|
|
1165
|
+
</button>
|
|
1166
|
+
{userInfo && (
|
|
1167
|
+
<div className="space-y-3">
|
|
1168
|
+
<div className="rounded-lg border border-gray-200 p-3 text-sm">
|
|
1169
|
+
<p>
|
|
1170
|
+
<span className="font-medium">Nom:</span> {userInfo.name}
|
|
1171
|
+
</p>
|
|
1172
|
+
<p>
|
|
1173
|
+
<span className="font-medium">Email:</span> {userInfo.email}
|
|
1174
|
+
</p>
|
|
1175
|
+
<p>
|
|
1176
|
+
<span className="font-medium">Rôle:</span> {userInfo.role}
|
|
1177
|
+
{userInfo.customRole && ` (${userInfo.customRole.name})`}
|
|
1178
|
+
</p>
|
|
1179
|
+
</div>
|
|
1180
|
+
{userInfo.customRole?.permissions && (
|
|
1181
|
+
<div className="rounded-lg border border-gray-200 p-3">
|
|
1182
|
+
<p className="mb-2 text-sm font-medium text-gray-700">
|
|
1183
|
+
Permissions ({userInfo.customRole.permissions.length})
|
|
1184
|
+
</p>
|
|
1185
|
+
<div className="flex flex-wrap gap-1">
|
|
1186
|
+
{userInfo.customRole.permissions.map((p: string) => (
|
|
1187
|
+
<span key={p} className="rounded-full bg-gray-100 px-2 py-0.5 text-xs text-gray-700">
|
|
1188
|
+
{p}
|
|
1189
|
+
</span>
|
|
1190
|
+
))}
|
|
1191
|
+
</div>
|
|
1192
|
+
</div>
|
|
1193
|
+
)}
|
|
1194
|
+
</div>
|
|
1195
|
+
)}
|
|
1196
|
+
</Section>
|
|
1197
|
+
|
|
1198
|
+
{/* ── L. Logs d'audit récents ────────────────────────────────────────── */}
|
|
1199
|
+
<Section title="Logs d'audit récents" icon={ScrollText}>
|
|
1200
|
+
<button
|
|
1201
|
+
onClick={fetchAuditLogs}
|
|
1202
|
+
disabled={loadingAudit}
|
|
1203
|
+
className="mb-4 flex items-center gap-1.5 rounded-lg border border-gray-300 bg-white px-3 py-1.5 text-sm font-medium text-gray-700 transition hover:bg-gray-50 disabled:opacity-50"
|
|
1204
|
+
>
|
|
1205
|
+
<RefreshCw className={`h-3.5 w-3.5 ${loadingAudit ? 'animate-spin' : ''}`} />
|
|
1206
|
+
Charger
|
|
1207
|
+
</button>
|
|
1208
|
+
{auditLogs.length === 0 ? (
|
|
1209
|
+
<p className="text-sm text-gray-500">Aucun log. Cliquez sur "Charger".</p>
|
|
1210
|
+
) : (
|
|
1211
|
+
<div className="space-y-2">
|
|
1212
|
+
{auditLogs.map((log) => (
|
|
1213
|
+
<div key={log.id} className="rounded-lg border border-gray-200 p-3 text-sm">
|
|
1214
|
+
<div className="flex items-center justify-between">
|
|
1215
|
+
<span className="font-medium text-gray-900">{log.action}</span>
|
|
1216
|
+
<span className="text-xs text-gray-400">
|
|
1217
|
+
{new Date(log.createdAt).toLocaleString('fr-FR')}
|
|
1218
|
+
</span>
|
|
1219
|
+
</div>
|
|
1220
|
+
<div className="mt-0.5 text-xs text-gray-500">
|
|
1221
|
+
{log.entityType}
|
|
1222
|
+
{log.entityId && ` #${log.entityId.slice(0, 8)}`}
|
|
1223
|
+
{log.actor && ` — par ${log.actor.name}`}
|
|
1224
|
+
</div>
|
|
1225
|
+
</div>
|
|
1226
|
+
))}
|
|
1227
|
+
</div>
|
|
1228
|
+
)}
|
|
1229
|
+
</Section>
|
|
1230
|
+
|
|
1231
|
+
{/* ── M. Test des toasts ─────────────────────────────────────────────── */}
|
|
1232
|
+
<Section title="Test des toasts" icon={Bell}>
|
|
1233
|
+
<div className="flex flex-wrap gap-2">
|
|
1234
|
+
<button
|
|
1235
|
+
onClick={() => toast.success('Opération réussie')}
|
|
1236
|
+
className="rounded-lg bg-green-600 px-3 py-2 text-sm font-medium text-white hover:bg-green-700"
|
|
1237
|
+
>
|
|
1238
|
+
Success
|
|
1239
|
+
</button>
|
|
1240
|
+
<button
|
|
1241
|
+
onClick={() => toast.error('Une erreur est survenue')}
|
|
1242
|
+
className="rounded-lg bg-red-600 px-3 py-2 text-sm font-medium text-white hover:bg-red-700"
|
|
1243
|
+
>
|
|
1244
|
+
Error
|
|
1245
|
+
</button>
|
|
1246
|
+
<button
|
|
1247
|
+
onClick={() => toast.info('Information importante')}
|
|
1248
|
+
className="rounded-lg bg-primary px-3 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
|
|
1249
|
+
>
|
|
1250
|
+
Info
|
|
1251
|
+
</button>
|
|
1252
|
+
<button
|
|
1253
|
+
onClick={() => toast.warning('Attention requise')}
|
|
1254
|
+
className="rounded-lg bg-yellow-600 px-3 py-2 text-sm font-medium text-white hover:bg-yellow-700"
|
|
1255
|
+
>
|
|
1256
|
+
Warning
|
|
1257
|
+
</button>
|
|
1258
|
+
<button
|
|
1259
|
+
onClick={() => toast.errorConfigRequired('SMTP non configuré', '/settings?section=system')}
|
|
1260
|
+
className="rounded-lg bg-red-800 px-3 py-2 text-sm font-medium text-white hover:bg-red-900"
|
|
1261
|
+
>
|
|
1262
|
+
Config Error (persistent)
|
|
1263
|
+
</button>
|
|
1264
|
+
<button
|
|
1265
|
+
onClick={() =>
|
|
1266
|
+
toast.persistent('info', 'Action annulable', {
|
|
1267
|
+
actionLabel: 'Annuler',
|
|
1268
|
+
actionOnClick: () => toast.success('Annulé !'),
|
|
1269
|
+
autoDismissMs: 8000,
|
|
1270
|
+
})
|
|
1271
|
+
}
|
|
1272
|
+
className="rounded-lg bg-purple-600 px-3 py-2 text-sm font-medium text-white hover:bg-purple-700"
|
|
1273
|
+
>
|
|
1274
|
+
Persistent + Undo
|
|
1275
|
+
</button>
|
|
1276
|
+
<button
|
|
1277
|
+
onClick={() => {
|
|
1278
|
+
toast.success('Toast 1');
|
|
1279
|
+
setTimeout(() => toast.info('Toast 2'), 300);
|
|
1280
|
+
setTimeout(() => toast.warning('Toast 3'), 600);
|
|
1281
|
+
setTimeout(() => toast.error('Toast 4'), 900);
|
|
1282
|
+
}}
|
|
1283
|
+
className="rounded-lg bg-gray-800 px-3 py-2 text-sm font-medium text-white hover:bg-gray-900"
|
|
1284
|
+
>
|
|
1285
|
+
Stack (x4)
|
|
1286
|
+
</button>
|
|
1287
|
+
</div>
|
|
1288
|
+
</Section>
|
|
1289
|
+
</div>
|
|
1290
|
+
);
|
|
1291
|
+
}
|