create-crm-tmp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/bin/create-crm-tmp.js +93 -0
  2. package/package.json +25 -0
  3. package/template/.prettierignore +33 -0
  4. package/template/.prettierrc.json +25 -0
  5. package/template/README.md +173 -0
  6. package/template/eslint.config.mjs +18 -0
  7. package/template/exemple-contacts.csv +11 -0
  8. package/template/next.config.ts +8 -0
  9. package/template/package.json +64 -0
  10. package/template/postcss.config.mjs +7 -0
  11. package/template/prisma/migrations/20251126144728_init/migration.sql +78 -0
  12. package/template/prisma/migrations/20251126155204_add_user_roles/migration.sql +5 -0
  13. package/template/prisma/migrations/20251128095126_add_company_info/migration.sql +19 -0
  14. package/template/prisma/migrations/20251128123321_add_smtp_config/migration.sql +22 -0
  15. package/template/prisma/migrations/20251128132303_add_status/migration.sql +23 -0
  16. package/template/prisma/migrations/20251201102207_add_user_active/migration.sql +75 -0
  17. package/template/prisma/migrations/20251201105507_add_email_signature/migration.sql +2 -0
  18. package/template/prisma/migrations/20251201151122_add_tasks/migration.sql +45 -0
  19. package/template/prisma/migrations/20251202111854_add_task_reminder/migration.sql +2 -0
  20. package/template/prisma/migrations/20251202135859_add_google_meet_integration/migration.sql +27 -0
  21. package/template/prisma/migrations/20251203103317_add_meta_lead_integration/migration.sql +20 -0
  22. package/template/prisma/migrations/20251203104002_add_google_ads_integration/migration.sql +18 -0
  23. package/template/prisma/migrations/20251203112122_add_google_sheet_integration/migration.sql +32 -0
  24. package/template/prisma/migrations/20251203153853_allow_multiple_integration_configs/migration.sql +20 -0
  25. package/template/prisma/migrations/20251205141705_update_user_roles/migration.sql +12 -0
  26. package/template/prisma/migrations/20251205150000_add_commercial_and_telepro_assignment/migration.sql +21 -0
  27. package/template/prisma/migrations/20251205160000_add_interaction_logging/migration.sql +11 -0
  28. package/template/prisma/migrations/20251208090314_add_automatic_interaction_types/migration.sql +12 -0
  29. package/template/prisma/migrations/20251208094843_mg/migration.sql +14 -0
  30. package/template/prisma/migrations/20251208100000_add_company_support/migration.sql +14 -0
  31. package/template/prisma/migrations/20251208110000_add_templates/migration.sql +26 -0
  32. package/template/prisma/migrations/20251208141304_add_video_conference_task_type/migration.sql +2 -0
  33. package/template/prisma/migrations/20251209104759_add_internal_note_to_task/migration.sql +2 -0
  34. package/template/prisma/migrations/20251209134803_add_company_field/migration.sql +2 -0
  35. package/template/prisma/migrations/20251209150000_rename_company_to_company_name/migration.sql +3 -0
  36. package/template/prisma/migrations/20251209150016_add_email_tracking/migration.sql +21 -0
  37. package/template/prisma/migrations/20251209155908_add_notify_contact_to_task/migration.sql +2 -0
  38. package/template/prisma/migrations/20251210110019_add_appointment_types/migration.sql +10 -0
  39. package/template/prisma/migrations/20251210113928_add_contact_files/migration.sql +26 -0
  40. package/template/prisma/migrations/20251212132339_add_custom_roles/migration.sql +24 -0
  41. package/template/prisma/migrations/20251215104448_add_file_interaction_types/migration.sql +11 -0
  42. package/template/prisma/migrations/20251215145616_add_closing_reasons/migration.sql +12 -0
  43. package/template/prisma/migrations/20251216140850_add_log_users/migration.sql +25 -0
  44. package/template/prisma/migrations/20251216151000_rename_perdu_to_ferme/migration.sql +8 -0
  45. package/template/prisma/migrations/20251216162318_add_column_mappings_to_google_sheet/migration.sql +2 -0
  46. package/template/prisma/migrations/20251216185127_add_workflows/migration.sql +80 -0
  47. package/template/prisma/migrations/20251216192237_add_scheduled_workflow_actions/migration.sql +32 -0
  48. package/template/prisma/migrations/migration_lock.toml +3 -0
  49. package/template/prisma/schema.prisma +582 -0
  50. package/template/prisma.config.ts +14 -0
  51. package/template/src/app/(auth)/invite/[token]/page.tsx +200 -0
  52. package/template/src/app/(auth)/layout.tsx +3 -0
  53. package/template/src/app/(auth)/reset-password/complete/page.tsx +213 -0
  54. package/template/src/app/(auth)/reset-password/page.tsx +146 -0
  55. package/template/src/app/(auth)/reset-password/verify/page.tsx +183 -0
  56. package/template/src/app/(auth)/signin/page.tsx +166 -0
  57. package/template/src/app/(dashboard)/agenda/page.tsx +3051 -0
  58. package/template/src/app/(dashboard)/automatisation/[id]/page.tsx +24 -0
  59. package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +905 -0
  60. package/template/src/app/(dashboard)/automatisation/new/page.tsx +20 -0
  61. package/template/src/app/(dashboard)/automatisation/page.tsx +337 -0
  62. package/template/src/app/(dashboard)/closing/page.tsx +1052 -0
  63. package/template/src/app/(dashboard)/contacts/[id]/page.tsx +6028 -0
  64. package/template/src/app/(dashboard)/contacts/page.tsx +3713 -0
  65. package/template/src/app/(dashboard)/dashboard/page.tsx +186 -0
  66. package/template/src/app/(dashboard)/layout.tsx +30 -0
  67. package/template/src/app/(dashboard)/settings/page.tsx +4070 -0
  68. package/template/src/app/(dashboard)/templates/page.tsx +567 -0
  69. package/template/src/app/(dashboard)/users/list/page.tsx +507 -0
  70. package/template/src/app/(dashboard)/users/page.tsx +457 -0
  71. package/template/src/app/(dashboard)/users/permissions/page.tsx +181 -0
  72. package/template/src/app/(dashboard)/users/roles/page.tsx +434 -0
  73. package/template/src/app/api/audit-logs/route.ts +57 -0
  74. package/template/src/app/api/auth/[...all]/route.ts +4 -0
  75. package/template/src/app/api/auth/check-active/route.ts +31 -0
  76. package/template/src/app/api/auth/google/callback/route.ts +94 -0
  77. package/template/src/app/api/auth/google/disconnect/route.ts +32 -0
  78. package/template/src/app/api/auth/google/route.ts +34 -0
  79. package/template/src/app/api/auth/google/status/route.ts +32 -0
  80. package/template/src/app/api/closing-reasons/route.ts +27 -0
  81. package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +94 -0
  82. package/template/src/app/api/contacts/[id]/files/route.ts +269 -0
  83. package/template/src/app/api/contacts/[id]/interactions/[interactionId]/route.ts +91 -0
  84. package/template/src/app/api/contacts/[id]/interactions/route.ts +103 -0
  85. package/template/src/app/api/contacts/[id]/meet/route.ts +296 -0
  86. package/template/src/app/api/contacts/[id]/route.ts +322 -0
  87. package/template/src/app/api/contacts/[id]/send-email/route.ts +254 -0
  88. package/template/src/app/api/contacts/export/route.ts +270 -0
  89. package/template/src/app/api/contacts/import/route.ts +381 -0
  90. package/template/src/app/api/contacts/route.ts +283 -0
  91. package/template/src/app/api/dashboard/stats/route.ts +299 -0
  92. package/template/src/app/api/email/track/[id]/route.ts +68 -0
  93. package/template/src/app/api/integrations/google-sheet/sync/route.ts +526 -0
  94. package/template/src/app/api/invite/complete/route.ts +88 -0
  95. package/template/src/app/api/invite/validate/route.ts +55 -0
  96. package/template/src/app/api/reminders/route.ts +95 -0
  97. package/template/src/app/api/reset-password/complete/route.ts +73 -0
  98. package/template/src/app/api/reset-password/request/route.ts +84 -0
  99. package/template/src/app/api/reset-password/validate/route.ts +49 -0
  100. package/template/src/app/api/reset-password/verify/route.ts +74 -0
  101. package/template/src/app/api/roles/[id]/route.ts +183 -0
  102. package/template/src/app/api/roles/route.ts +140 -0
  103. package/template/src/app/api/send/route.ts +282 -0
  104. package/template/src/app/api/settings/change-password/route.ts +95 -0
  105. package/template/src/app/api/settings/closing-reasons/[id]/route.ts +84 -0
  106. package/template/src/app/api/settings/closing-reasons/route.ts +74 -0
  107. package/template/src/app/api/settings/company/route.ts +121 -0
  108. package/template/src/app/api/settings/google-ads/[id]/route.ts +117 -0
  109. package/template/src/app/api/settings/google-ads/route.ts +122 -0
  110. package/template/src/app/api/settings/google-sheet/[id]/route.ts +230 -0
  111. package/template/src/app/api/settings/google-sheet/auto-map/route.ts +196 -0
  112. package/template/src/app/api/settings/google-sheet/route.ts +254 -0
  113. package/template/src/app/api/settings/meta-leads/[id]/route.ts +123 -0
  114. package/template/src/app/api/settings/meta-leads/route.ts +132 -0
  115. package/template/src/app/api/settings/profile/route.ts +42 -0
  116. package/template/src/app/api/settings/smtp/route.ts +130 -0
  117. package/template/src/app/api/settings/smtp/test/route.ts +121 -0
  118. package/template/src/app/api/settings/statuses/[id]/route.ts +101 -0
  119. package/template/src/app/api/settings/statuses/route.ts +83 -0
  120. package/template/src/app/api/statuses/route.ts +25 -0
  121. package/template/src/app/api/tasks/[id]/attendees/route.ts +76 -0
  122. package/template/src/app/api/tasks/[id]/route.ts +728 -0
  123. package/template/src/app/api/tasks/meet/route.ts +240 -0
  124. package/template/src/app/api/tasks/route.ts +417 -0
  125. package/template/src/app/api/templates/[id]/route.ts +140 -0
  126. package/template/src/app/api/templates/route.ts +91 -0
  127. package/template/src/app/api/users/[id]/route.ts +168 -0
  128. package/template/src/app/api/users/list/route.ts +45 -0
  129. package/template/src/app/api/users/me/route.ts +48 -0
  130. package/template/src/app/api/users/route.ts +250 -0
  131. package/template/src/app/api/webhooks/google-ads/route.ts +208 -0
  132. package/template/src/app/api/webhooks/meta-leads/route.ts +258 -0
  133. package/template/src/app/api/workflows/[id]/route.ts +192 -0
  134. package/template/src/app/api/workflows/process/route.ts +293 -0
  135. package/template/src/app/api/workflows/route.ts +124 -0
  136. package/template/src/app/favicon.ico +0 -0
  137. package/template/src/app/globals.css +1416 -0
  138. package/template/src/app/layout.tsx +31 -0
  139. package/template/src/app/page.tsx +32 -0
  140. package/template/src/components/dashboard/activity-chart.tsx +67 -0
  141. package/template/src/components/dashboard/contacts-chart.tsx +63 -0
  142. package/template/src/components/dashboard/recent-activity.tsx +164 -0
  143. package/template/src/components/dashboard/sales-analytics-chart.tsx +81 -0
  144. package/template/src/components/dashboard/stat-card.tsx +61 -0
  145. package/template/src/components/dashboard/status-distribution-chart.tsx +45 -0
  146. package/template/src/components/dashboard/tasks-pie-chart.tsx +88 -0
  147. package/template/src/components/dashboard/top-contacts-list.tsx +129 -0
  148. package/template/src/components/dashboard/upcoming-tasks-list.tsx +126 -0
  149. package/template/src/components/editor.tsx +856 -0
  150. package/template/src/components/email-template.tsx +35 -0
  151. package/template/src/components/header.tsx +320 -0
  152. package/template/src/components/invitation-email-template.tsx +79 -0
  153. package/template/src/components/meet-cancellation-email-template.tsx +120 -0
  154. package/template/src/components/meet-confirmation-email-template.tsx +156 -0
  155. package/template/src/components/meet-update-email-template.tsx +209 -0
  156. package/template/src/components/page-header.tsx +61 -0
  157. package/template/src/components/reset-password-email-template.tsx +79 -0
  158. package/template/src/components/sidebar.tsx +294 -0
  159. package/template/src/components/skeleton.tsx +380 -0
  160. package/template/src/components/ui/commands.tsx +396 -0
  161. package/template/src/components/ui/components.tsx +150 -0
  162. package/template/src/components/ui/theme.tsx +5 -0
  163. package/template/src/components/view-as-banner.tsx +45 -0
  164. package/template/src/components/view-as-modal.tsx +186 -0
  165. package/template/src/contexts/mobile-menu-context.tsx +31 -0
  166. package/template/src/contexts/sidebar-context.tsx +107 -0
  167. package/template/src/contexts/task-reminder-context.tsx +239 -0
  168. package/template/src/contexts/view-as-context.tsx +84 -0
  169. package/template/src/hooks/use-user-role.ts +82 -0
  170. package/template/src/lib/audit-log.ts +45 -0
  171. package/template/src/lib/auth-client.ts +16 -0
  172. package/template/src/lib/auth.ts +35 -0
  173. package/template/src/lib/check-permission.ts +193 -0
  174. package/template/src/lib/contact-duplicate.ts +112 -0
  175. package/template/src/lib/contact-interactions.ts +371 -0
  176. package/template/src/lib/encryption.ts +99 -0
  177. package/template/src/lib/google-calendar.ts +300 -0
  178. package/template/src/lib/google-drive.ts +372 -0
  179. package/template/src/lib/permissions.ts +412 -0
  180. package/template/src/lib/prisma.ts +32 -0
  181. package/template/src/lib/roles.ts +120 -0
  182. package/template/src/lib/template-variables.ts +76 -0
  183. package/template/src/lib/utils.ts +46 -0
  184. package/template/src/lib/workflow-executor.ts +482 -0
  185. package/template/src/proxy.ts +91 -0
  186. package/template/tsconfig.json +34 -0
  187. package/template/vercel.json +8 -0
@@ -0,0 +1,3051 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useRef, useMemo } from 'react';
4
+ import { useUserRole } from '@/hooks/use-user-role';
5
+ import { cn } from '@/lib/utils';
6
+ import {
7
+ Calendar,
8
+ Clock,
9
+ User,
10
+ CheckCircle2,
11
+ Circle,
12
+ ChevronLeft,
13
+ ChevronRight,
14
+ Video,
15
+ ExternalLink,
16
+ X,
17
+ Bookmark,
18
+ } from 'lucide-react';
19
+ import { Editor, type DefaultTemplateRef } from '@/components/editor';
20
+ import Link from 'next/link';
21
+ import { AgendaMonthSkeleton, AgendaWeekSkeleton, AgendaDaySkeleton } from '@/components/skeleton';
22
+
23
+ interface Task {
24
+ id: string;
25
+ type: 'CALL' | 'MEETING' | 'EMAIL' | 'OTHER' | 'VIDEO_CONFERENCE';
26
+ title: string | null;
27
+ description: string;
28
+ priority: 'LOW' | 'MEDIUM' | 'HIGH' | 'URGENT';
29
+ scheduledAt: string;
30
+ completed: boolean;
31
+ reminderMinutesBefore?: number | null;
32
+ googleMeetLink?: string | null;
33
+ durationMinutes?: number | null;
34
+ contact: {
35
+ id: string;
36
+ firstName: string | null;
37
+ lastName: string | null;
38
+ } | null;
39
+ assignedUser: {
40
+ id: string;
41
+ name: string;
42
+ };
43
+ }
44
+
45
+ const PRIORITY_COLORS = {
46
+ LOW: '#10B981', // Vert
47
+ MEDIUM: '#3B82F6', // Bleu
48
+ HIGH: '#F59E0B', // Orange
49
+ URGENT: '#EF4444', // Rouge
50
+ };
51
+
52
+ const PRIORITY_LABELS = {
53
+ LOW: 'Faible',
54
+ MEDIUM: 'Moyenne',
55
+ HIGH: 'Haute',
56
+ URGENT: 'Urgente',
57
+ };
58
+
59
+ // Couleurs par type d'événement
60
+ const TYPE_COLORS = {
61
+ CALL: '#8B5CF6', // Violet pour les appels
62
+ EMAIL: '#6366F1', // Indigo pour les emails
63
+ OTHER: '#64748B', // Gris pour autres tâches
64
+ MEETING: '#3B82F6', // Bleu pour les rendez-vous
65
+ VIDEO_CONFERENCE: '#10B981', // Vert pour Google Meet
66
+ };
67
+
68
+ const TASK_TYPE_LABELS = {
69
+ CALL: 'Appel téléphonique',
70
+ MEETING: 'RDV',
71
+ EMAIL: 'Email',
72
+ OTHER: 'Autre',
73
+ VIDEO_CONFERENCE: 'Google Meet',
74
+ };
75
+
76
+ const HOURS = Array.from({ length: 17 }, (_, i) => i + 6); // 6h à 22h
77
+
78
+ const formatHourLabel = (hour: number) => {
79
+ return new Date(0, 0, 0, hour).toLocaleTimeString('fr-FR', {
80
+ hour: 'numeric',
81
+ minute: '2-digit',
82
+ });
83
+ };
84
+
85
+ export default function AgendaPage() {
86
+ const { isAdmin } = useUserRole();
87
+ const [tasks, setTasks] = useState<Task[]>([]);
88
+ const [loading, setLoading] = useState(true);
89
+ const [currentDate, setCurrentDate] = useState(new Date());
90
+ const [view, setView] = useState<'month' | 'week' | 'day'>('week');
91
+ const [showCreateTaskModal, setShowCreateTaskModal] = useState(false);
92
+ const [googleConnected, setGoogleConnected] = useState(false);
93
+ const [currentTime, setCurrentTime] = useState<Date | null>(null);
94
+
95
+ // Mettre à jour l'heure actuelle en continu côté client uniquement
96
+ useEffect(() => {
97
+ const updateTime = () => setCurrentTime(new Date());
98
+ updateTime(); // première synchro immédiate après le montage
99
+
100
+ const interval = setInterval(updateTime, 1000); // Mise à jour chaque seconde
101
+ return () => clearInterval(interval);
102
+ }, []);
103
+
104
+ // Filtres par type d'événement et priorité
105
+ const [filters, setFilters] = useState<{
106
+ tasks: boolean; // Tâches (CALL, EMAIL, OTHER)
107
+ meetings: boolean; // Rendez-vous physiques (MEETING)
108
+ googleMeets: boolean; // Google Meet (VIDEO_CONFERENCE)
109
+ priorities: {
110
+ LOW: boolean;
111
+ MEDIUM: boolean;
112
+ HIGH: boolean;
113
+ URGENT: boolean;
114
+ };
115
+ }>({
116
+ tasks: true,
117
+ meetings: true,
118
+ googleMeets: true,
119
+ priorities: {
120
+ LOW: true,
121
+ MEDIUM: true,
122
+ HIGH: true,
123
+ URGENT: true,
124
+ },
125
+ });
126
+ const [showFiltersMenu, setShowFiltersMenu] = useState(false);
127
+ const filtersMenuRef = useRef<HTMLDivElement>(null);
128
+ const [showCreateMeetModal, setShowCreateMeetModal] = useState(false);
129
+ const [showCreateMeetingModal, setShowCreateMeetingModal] = useState(false);
130
+
131
+ // États pour Google Meet
132
+ const meetEditorRef = useRef<DefaultTemplateRef | null>(null);
133
+ const [creatingMeet, setCreatingMeet] = useState(false);
134
+ const [meetError, setMeetError] = useState('');
135
+ const [meetData, setMeetData] = useState<{
136
+ title: string;
137
+ description: string;
138
+ scheduledAt: string;
139
+ durationMinutes: number;
140
+ attendees: string[];
141
+ reminderMinutesBefore: number | null;
142
+ internalNote: string;
143
+ }>({
144
+ title: '',
145
+ description: '',
146
+ scheduledAt: '',
147
+ durationMinutes: 30,
148
+ attendees: [],
149
+ reminderMinutesBefore: null,
150
+ internalNote: '',
151
+ });
152
+
153
+ // États pour rendez-vous physique
154
+ const meetingEditorRef = useRef<DefaultTemplateRef | null>(null);
155
+ const [creatingMeeting, setCreatingMeeting] = useState(false);
156
+ const [meetingError, setMeetingError] = useState('');
157
+ const [meetingData, setMeetingData] = useState<{
158
+ title: string;
159
+ description: string;
160
+ scheduledAt: string;
161
+ priority: 'LOW' | 'MEDIUM' | 'HIGH' | 'URGENT';
162
+ reminderMinutesBefore: number | null;
163
+ notifyContact: boolean;
164
+ internalNote: string;
165
+ attendees: string[];
166
+ }>({
167
+ title: '',
168
+ description: '',
169
+ scheduledAt: '',
170
+ priority: 'MEDIUM',
171
+ reminderMinutesBefore: null,
172
+ notifyContact: false,
173
+ internalNote: '',
174
+ attendees: [],
175
+ });
176
+
177
+ // Vérifier si Google est connecté pour Google Meet
178
+ useEffect(() => {
179
+ const checkGoogleConnection = async () => {
180
+ try {
181
+ const response = await fetch('/api/auth/google/status');
182
+ if (response.ok) {
183
+ const data = await response.json();
184
+ setGoogleConnected(!!data.connected);
185
+ } else {
186
+ setGoogleConnected(false);
187
+ }
188
+ } catch (error) {
189
+ console.error('Erreur lors de la vérification de Google:', error);
190
+ setGoogleConnected(false);
191
+ }
192
+ };
193
+
194
+ checkGoogleConnection();
195
+ }, []);
196
+
197
+ // Édition d'un Google Meet
198
+ const [showEditMeetModal, setShowEditMeetModal] = useState(false);
199
+ const [editingMeetTask, setEditingMeetTask] = useState<Task | null>(null);
200
+ const [editMeetData, setEditMeetData] = useState<{
201
+ scheduledAt: string;
202
+ durationMinutes: number;
203
+ }>({
204
+ scheduledAt: '',
205
+ durationMinutes: 30,
206
+ });
207
+ const [editMeetLoading, setEditMeetLoading] = useState(false);
208
+ const [editMeetError, setEditMeetError] = useState('');
209
+ const [createTaskError, setCreateTaskError] = useState('');
210
+ const [creatingTask, setCreatingTask] = useState(false);
211
+ const [createTaskData, setCreateTaskData] = useState<{
212
+ type: 'CALL' | 'MEETING' | 'EMAIL' | 'OTHER';
213
+ title: string;
214
+ description: string;
215
+ scheduledAt: string;
216
+ priority: 'LOW' | 'MEDIUM' | 'HIGH' | 'URGENT';
217
+ reminderMinutesBefore: number | null;
218
+ }>({
219
+ type: 'OTHER',
220
+ title: '',
221
+ description: '',
222
+ scheduledAt: '',
223
+ priority: 'MEDIUM',
224
+ reminderMinutesBefore: null,
225
+ });
226
+
227
+ // Détail / édition d'une tâche générique (non liée à un contact)
228
+ const [showTaskDetailModal, setShowTaskDetailModal] = useState(false);
229
+ const [selectedTask, setSelectedTask] = useState<Task | null>(null);
230
+ const [taskDetailError, setTaskDetailError] = useState('');
231
+ const [taskDetailLoading, setTaskDetailLoading] = useState(false);
232
+ const [taskDeleteLoading, setTaskDeleteLoading] = useState(false);
233
+ const [taskDetailData, setTaskDetailData] = useState<{
234
+ title: string;
235
+ description: string;
236
+ scheduledAt: string;
237
+ priority: 'LOW' | 'MEDIUM' | 'HIGH' | 'URGENT';
238
+ completed: boolean;
239
+ }>({
240
+ title: '',
241
+ description: '',
242
+ scheduledAt: '',
243
+ priority: 'MEDIUM',
244
+ completed: false,
245
+ });
246
+
247
+ // Synchronisation automatique Google Sheets
248
+ useEffect(() => {
249
+ const syncGoogleSheet = async () => {
250
+ try {
251
+ await fetch('/api/integrations/google-sheet/sync', {
252
+ method: 'POST',
253
+ });
254
+ } catch (err) {
255
+ console.error('Erreur lors de la synchronisation Google Sheets:', err);
256
+ }
257
+ };
258
+
259
+ syncGoogleSheet();
260
+ }, []);
261
+
262
+ // Fermer le menu de filtres en cliquant en dehors
263
+ useEffect(() => {
264
+ const handleClickOutside = (event: MouseEvent) => {
265
+ if (filtersMenuRef.current && !filtersMenuRef.current.contains(event.target as Node)) {
266
+ setShowFiltersMenu(false);
267
+ }
268
+ };
269
+
270
+ if (showFiltersMenu) {
271
+ document.addEventListener('mousedown', handleClickOutside);
272
+ }
273
+
274
+ return () => {
275
+ document.removeEventListener('mousedown', handleClickOutside);
276
+ };
277
+ }, [showFiltersMenu]);
278
+
279
+ // Calculer le début et la fin du mois/semaine/jour
280
+ const getDateRange = () => {
281
+ const year = currentDate.getFullYear();
282
+ const month = currentDate.getMonth();
283
+
284
+ if (view === 'month') {
285
+ const start = new Date(year, month, 1);
286
+ const end = new Date(year, month + 1, 0, 23, 59, 59);
287
+ return { start, end };
288
+ } else if (view === 'week') {
289
+ const start = new Date(currentDate);
290
+ start.setDate(start.getDate() - start.getDay() + 1); // Lundi
291
+ const end = new Date(start);
292
+ end.setDate(end.getDate() + 6);
293
+ end.setHours(23, 59, 59);
294
+ return { start, end };
295
+ } else {
296
+ const start = new Date(currentDate);
297
+ start.setHours(0, 0, 0);
298
+ const end = new Date(currentDate);
299
+ end.setHours(23, 59, 59);
300
+ return { start, end };
301
+ }
302
+ };
303
+
304
+ useEffect(() => {
305
+ fetchTasks();
306
+ }, [currentDate, view]);
307
+
308
+ const fetchTasks = async () => {
309
+ try {
310
+ setLoading(true);
311
+ const { start, end } = getDateRange();
312
+ const response = await fetch(
313
+ `/api/tasks?startDate=${start.toISOString()}&endDate=${end.toISOString()}`,
314
+ );
315
+ if (response.ok) {
316
+ const data = await response.json();
317
+ setTasks(data);
318
+ }
319
+ } catch (error) {
320
+ console.error('Erreur lors du chargement des tâches:', error);
321
+ } finally {
322
+ setLoading(false);
323
+ }
324
+ };
325
+
326
+ // Filtrer les tâches selon les filtres sélectionnés
327
+ const filteredTasks = useMemo(() => {
328
+ return tasks.filter((task) => {
329
+ // Filtre par type
330
+ let typeMatch = false;
331
+ if (task.type === 'VIDEO_CONFERENCE') {
332
+ typeMatch = filters.googleMeets;
333
+ } else if (task.type === 'MEETING') {
334
+ typeMatch = filters.meetings;
335
+ } else if (['CALL', 'EMAIL', 'OTHER'].includes(task.type)) {
336
+ typeMatch = filters.tasks;
337
+ } else {
338
+ typeMatch = true;
339
+ }
340
+
341
+ // Filtre par priorité
342
+ const priorityMatch = filters.priorities[task.priority];
343
+
344
+ return typeMatch && priorityMatch;
345
+ });
346
+ }, [tasks, filters]);
347
+
348
+ const navigateDate = (direction: 'prev' | 'next') => {
349
+ const newDate = new Date(currentDate);
350
+ if (view === 'month') {
351
+ newDate.setMonth(newDate.getMonth() + (direction === 'next' ? 1 : -1));
352
+ } else if (view === 'week') {
353
+ newDate.setDate(newDate.getDate() + (direction === 'next' ? 7 : -7));
354
+ } else {
355
+ newDate.setDate(newDate.getDate() + (direction === 'next' ? 1 : -1));
356
+ }
357
+ setCurrentDate(newDate);
358
+ };
359
+
360
+ const goToToday = () => {
361
+ setCurrentDate(new Date());
362
+ };
363
+
364
+ const getDaysInMonth = () => {
365
+ const year = currentDate.getFullYear();
366
+ const month = currentDate.getMonth();
367
+ const firstDay = new Date(year, month, 1);
368
+ const lastDay = new Date(year, month + 1, 0);
369
+ const daysInMonth = lastDay.getDate();
370
+ const startingDayOfWeek = firstDay.getDay() === 0 ? 6 : firstDay.getDay() - 1; // Lundi = 0
371
+
372
+ const days = [];
373
+ // Jours du mois précédent
374
+ const prevMonth = new Date(year, month - 1, 0);
375
+ for (let i = startingDayOfWeek - 1; i >= 0; i--) {
376
+ days.push({
377
+ date: new Date(year, month - 1, prevMonth.getDate() - i),
378
+ isCurrentMonth: false,
379
+ });
380
+ }
381
+ // Jours du mois actuel
382
+ for (let i = 1; i <= daysInMonth; i++) {
383
+ days.push({
384
+ date: new Date(year, month, i),
385
+ isCurrentMonth: true,
386
+ });
387
+ }
388
+ // Jours du mois suivant pour compléter la grille
389
+ const remainingDays = 42 - days.length; // 6 semaines * 7 jours
390
+ for (let i = 1; i <= remainingDays; i++) {
391
+ days.push({
392
+ date: new Date(year, month + 1, i),
393
+ isCurrentMonth: false,
394
+ });
395
+ }
396
+
397
+ return days;
398
+ };
399
+
400
+ const getTasksForDate = (date: Date) => {
401
+ return filteredTasks.filter((task) => {
402
+ const taskDate = new Date(task.scheduledAt);
403
+ return (
404
+ taskDate.getDate() === date.getDate() &&
405
+ taskDate.getMonth() === date.getMonth() &&
406
+ taskDate.getFullYear() === date.getFullYear()
407
+ );
408
+ });
409
+ };
410
+
411
+ const formatDate = (date: Date) => {
412
+ return date.toLocaleDateString('fr-FR', {
413
+ year: 'numeric',
414
+ month: 'long',
415
+ day: 'numeric',
416
+ });
417
+ };
418
+
419
+ const formatTime = (dateString: string) => {
420
+ return new Date(dateString).toLocaleTimeString('fr-FR', {
421
+ hour: '2-digit',
422
+ minute: '2-digit',
423
+ });
424
+ };
425
+
426
+ const getWeekDays = () => {
427
+ const start = new Date(currentDate);
428
+ const day = start.getDay() === 0 ? 7 : start.getDay(); // dimanche = 7
429
+ start.setDate(start.getDate() - (day - 1)); // lundi
430
+
431
+ const days: Date[] = [];
432
+ for (let i = 0; i < 7; i++) {
433
+ const d = new Date(start);
434
+ d.setDate(start.getDate() + i);
435
+ days.push(d);
436
+ }
437
+ return days;
438
+ };
439
+
440
+ const formatHeaderDate = () => {
441
+ if (view === 'month') {
442
+ // Exemple : "Décembre 2025"
443
+ const str = currentDate.toLocaleDateString('fr-FR', {
444
+ year: 'numeric',
445
+ month: 'long',
446
+ });
447
+ return str.charAt(0).toUpperCase() + str.slice(1);
448
+ }
449
+
450
+ if (view === 'week') {
451
+ const days = getWeekDays();
452
+ const start = days[0];
453
+ const end = days[6];
454
+
455
+ const startStr = start.toLocaleDateString('fr-FR', {
456
+ day: 'numeric',
457
+ month: 'long',
458
+ });
459
+
460
+ // Si le mois ou l'année change dans la semaine, on les affiche dans la partie "fin"
461
+ const endStr = end.toLocaleDateString('fr-FR', {
462
+ day: 'numeric',
463
+ month: 'long',
464
+ year: 'numeric',
465
+ });
466
+
467
+ return `Du ${startStr} au ${endStr}`;
468
+ }
469
+
470
+ // Vue jour : "16 décembre 2025"
471
+ return currentDate.toLocaleDateString('fr-FR', {
472
+ year: 'numeric',
473
+ month: 'long',
474
+ day: 'numeric',
475
+ });
476
+ };
477
+
478
+ const toggleTaskComplete = async (taskId: string, currentStatus: boolean) => {
479
+ try {
480
+ const response = await fetch(`/api/tasks/${taskId}`, {
481
+ method: 'PUT',
482
+ headers: { 'Content-Type': 'application/json' },
483
+ body: JSON.stringify({ completed: !currentStatus }),
484
+ });
485
+
486
+ if (response.ok) {
487
+ fetchTasks();
488
+ }
489
+ } catch (error) {
490
+ console.error('Erreur lors de la mise à jour de la tâche:', error);
491
+ }
492
+ };
493
+
494
+ const handleCreateTaskFromAgenda = async (e: React.FormEvent) => {
495
+ e.preventDefault();
496
+ setCreateTaskError('');
497
+ setCreatingTask(true);
498
+
499
+ try {
500
+ if (!createTaskData.scheduledAt) {
501
+ setCreateTaskError('La date/heure est requise');
502
+ setCreatingTask(false);
503
+ return;
504
+ }
505
+
506
+ if (!createTaskData.description.trim()) {
507
+ setCreateTaskError('La description est requise');
508
+ setCreatingTask(false);
509
+ return;
510
+ }
511
+
512
+ const response = await fetch('/api/tasks', {
513
+ method: 'POST',
514
+ headers: { 'Content-Type': 'application/json' },
515
+ body: JSON.stringify({
516
+ type: createTaskData.type,
517
+ title: createTaskData.title || null,
518
+ description: createTaskData.description,
519
+ priority: createTaskData.priority,
520
+ scheduledAt: createTaskData.scheduledAt,
521
+ // Pas de contactId ni d'assignation explicite : la tâche sera créée
522
+ // uniquement pour l'utilisateur connecté côté backend
523
+ reminderMinutesBefore:
524
+ typeof createTaskData.reminderMinutesBefore === 'number'
525
+ ? createTaskData.reminderMinutesBefore
526
+ : null,
527
+ }),
528
+ });
529
+
530
+ const data = await response.json();
531
+ if (!response.ok) {
532
+ throw new Error(data.error || 'Erreur lors de la création de la tâche');
533
+ }
534
+
535
+ setShowCreateTaskModal(false);
536
+ setCreateTaskData({
537
+ type: 'OTHER',
538
+ title: '',
539
+ description: '',
540
+ scheduledAt: '',
541
+ priority: 'MEDIUM',
542
+ reminderMinutesBefore: null,
543
+ });
544
+ await fetchTasks();
545
+ } catch (error: any) {
546
+ setCreateTaskError(error.message || 'Erreur lors de la création de la tâche');
547
+ } finally {
548
+ setCreatingTask(false);
549
+ }
550
+ };
551
+
552
+ const handleCreateMeet = async (e: React.FormEvent) => {
553
+ e.preventDefault();
554
+ setMeetError('');
555
+ setCreatingMeet(true);
556
+
557
+ try {
558
+ if (!meetEditorRef.current) {
559
+ setMeetError("L'éditeur n'est pas prêt. Veuillez réessayer.");
560
+ setCreatingMeet(false);
561
+ return;
562
+ }
563
+
564
+ const htmlContent = await meetEditorRef.current.getHTML();
565
+
566
+ if (!meetData.scheduledAt) {
567
+ setMeetError('La date/heure est requise');
568
+ setCreatingMeet(false);
569
+ return;
570
+ }
571
+
572
+ if (!meetData.title) {
573
+ setMeetError('Le titre est requis');
574
+ setCreatingMeet(false);
575
+ return;
576
+ }
577
+
578
+ if (!googleConnected) {
579
+ setMeetError(
580
+ 'Vous devez connecter votre compte Google dans les paramètres avant de pouvoir créer un Google Meet.',
581
+ );
582
+ setCreatingMeet(false);
583
+ return;
584
+ }
585
+
586
+ const response = await fetch('/api/tasks/meet', {
587
+ method: 'POST',
588
+ headers: { 'Content-Type': 'application/json' },
589
+ body: JSON.stringify({
590
+ title: meetData.title,
591
+ description: htmlContent || '',
592
+ scheduledAt: meetData.scheduledAt,
593
+ durationMinutes: meetData.durationMinutes,
594
+ attendees: meetData.attendees.filter((email) => email.trim() !== ''),
595
+ reminderMinutesBefore: meetData.reminderMinutesBefore,
596
+ internalNote: meetData.internalNote || null,
597
+ }),
598
+ });
599
+
600
+ const data = await response.json();
601
+
602
+ if (!response.ok) {
603
+ throw new Error(data.error || 'Erreur lors de la création du Google Meet');
604
+ }
605
+
606
+ setShowCreateMeetModal(false);
607
+ setMeetData({
608
+ title: '',
609
+ description: '',
610
+ scheduledAt: '',
611
+ durationMinutes: 30,
612
+ attendees: [],
613
+ reminderMinutesBefore: null,
614
+ internalNote: '',
615
+ });
616
+ meetEditorRef.current?.injectHTML('');
617
+ await fetchTasks();
618
+ } catch (err: any) {
619
+ setMeetError(err.message);
620
+ } finally {
621
+ setCreatingMeet(false);
622
+ }
623
+ };
624
+
625
+ const handleCreateMeeting = async (e: React.FormEvent) => {
626
+ e.preventDefault();
627
+ setMeetingError('');
628
+ setCreatingMeeting(true);
629
+
630
+ try {
631
+ if (!meetingEditorRef.current) {
632
+ setMeetingError("L'éditeur n'est pas prêt. Veuillez réessayer.");
633
+ setCreatingMeeting(false);
634
+ return;
635
+ }
636
+
637
+ const htmlContent = await meetingEditorRef.current.getHTML();
638
+ const plainText = (htmlContent || '')
639
+ .replace(/<br\s*\/?>/gi, '\n')
640
+ .replace(/<\/p>/gi, '\n\n')
641
+ .replace(/<[^>]+>/g, '')
642
+ .replace(/&nbsp;/g, ' ')
643
+ .replace(/\s+/g, ' ')
644
+ .trim();
645
+
646
+ if (!meetingData.scheduledAt) {
647
+ setMeetingError('La date/heure est requise');
648
+ setCreatingMeeting(false);
649
+ return;
650
+ }
651
+
652
+ if (!plainText) {
653
+ setMeetingError('La description est requise');
654
+ setCreatingMeeting(false);
655
+ return;
656
+ }
657
+
658
+ const response = await fetch('/api/tasks', {
659
+ method: 'POST',
660
+ headers: { 'Content-Type': 'application/json' },
661
+ body: JSON.stringify({
662
+ type: 'MEETING',
663
+ title: meetingData.title || null,
664
+ description: htmlContent || '',
665
+ priority: meetingData.priority,
666
+ scheduledAt: meetingData.scheduledAt,
667
+ reminderMinutesBefore: meetingData.reminderMinutesBefore ?? null,
668
+ notifyContact: meetingData.notifyContact,
669
+ internalNote: meetingData.internalNote || null,
670
+ attendees: meetingData.attendees.filter((email) => email.trim() !== ''),
671
+ }),
672
+ });
673
+
674
+ const data = await response.json();
675
+
676
+ if (!response.ok) {
677
+ throw new Error(data.error || 'Erreur lors de la création du rendez-vous');
678
+ }
679
+
680
+ setShowCreateMeetingModal(false);
681
+ setMeetingData({
682
+ title: '',
683
+ description: '',
684
+ scheduledAt: '',
685
+ priority: 'MEDIUM',
686
+ reminderMinutesBefore: null,
687
+ notifyContact: false,
688
+ internalNote: '',
689
+ attendees: [],
690
+ });
691
+ meetingEditorRef.current?.injectHTML('');
692
+ await fetchTasks();
693
+ } catch (err: any) {
694
+ setMeetingError(err.message);
695
+ } finally {
696
+ setCreatingMeeting(false);
697
+ }
698
+ };
699
+
700
+ const openTaskDetailModal = (task: Task) => {
701
+ // On n'ouvre la modal que pour les tâches non liées à un contact (tâches "perso")
702
+ if (task.contact) return;
703
+
704
+ setSelectedTask(task);
705
+ setTaskDetailData({
706
+ title: task.title || '',
707
+ description: task.description || '',
708
+ scheduledAt: task.scheduledAt,
709
+ priority: task.priority,
710
+ completed: task.completed,
711
+ });
712
+ setTaskDetailError('');
713
+ setShowTaskDetailModal(true);
714
+ };
715
+
716
+ const handleUpdateTaskDetail = async (e: React.FormEvent) => {
717
+ e.preventDefault();
718
+ if (!selectedTask) return;
719
+
720
+ setTaskDetailError('');
721
+ setTaskDetailLoading(true);
722
+
723
+ try {
724
+ if (!taskDetailData.scheduledAt) {
725
+ setTaskDetailError('La date/heure est requise');
726
+ setTaskDetailLoading(false);
727
+ return;
728
+ }
729
+
730
+ if (!taskDetailData.description.trim()) {
731
+ setTaskDetailError('La description est requise');
732
+ setTaskDetailLoading(false);
733
+ return;
734
+ }
735
+
736
+ const response = await fetch(`/api/tasks/${selectedTask.id}`, {
737
+ method: 'PUT',
738
+ headers: { 'Content-Type': 'application/json' },
739
+ body: JSON.stringify({
740
+ title: taskDetailData.title || null,
741
+ description: taskDetailData.description,
742
+ priority: taskDetailData.priority,
743
+ scheduledAt: taskDetailData.scheduledAt,
744
+ completed: taskDetailData.completed,
745
+ }),
746
+ });
747
+
748
+ const data = await response.json();
749
+ if (!response.ok) {
750
+ throw new Error(data.error || 'Erreur lors de la mise à jour de la tâche');
751
+ }
752
+
753
+ setShowTaskDetailModal(false);
754
+ setSelectedTask(null);
755
+ await fetchTasks();
756
+ } catch (error: any) {
757
+ setTaskDetailError(error.message || 'Erreur lors de la mise à jour de la tâche');
758
+ } finally {
759
+ setTaskDetailLoading(false);
760
+ }
761
+ };
762
+
763
+ const handleDeleteTask = async () => {
764
+ if (!selectedTask) return;
765
+
766
+ if (!confirm('Voulez-vous vraiment supprimer cette tâche ?')) {
767
+ return;
768
+ }
769
+
770
+ setTaskDeleteLoading(true);
771
+ setTaskDetailError('');
772
+
773
+ try {
774
+ const response = await fetch(`/api/tasks/${selectedTask.id}`, {
775
+ method: 'DELETE',
776
+ });
777
+
778
+ const data = await response.json();
779
+ if (!response.ok) {
780
+ throw new Error(data.error || 'Erreur lors de la suppression de la tâche');
781
+ }
782
+
783
+ setShowTaskDetailModal(false);
784
+ setSelectedTask(null);
785
+ await fetchTasks();
786
+ } catch (error: any) {
787
+ setTaskDetailError(error.message || 'Erreur lors de la suppression de la tâche');
788
+ } finally {
789
+ setTaskDeleteLoading(false);
790
+ }
791
+ };
792
+
793
+ const openEditMeetModal = (task: Task) => {
794
+ const scheduled = new Date(task.scheduledAt);
795
+ setEditingMeetTask(task);
796
+ setEditMeetData({
797
+ scheduledAt: scheduled.toISOString(),
798
+ durationMinutes: task.durationMinutes ?? 30,
799
+ });
800
+ setEditMeetError('');
801
+ setShowEditMeetModal(true);
802
+ };
803
+
804
+ const handleUpdateMeet = async (e: React.FormEvent) => {
805
+ e.preventDefault();
806
+ if (!editingMeetTask) return;
807
+
808
+ setEditMeetError('');
809
+ setEditMeetLoading(true);
810
+
811
+ try {
812
+ if (!editMeetData.scheduledAt) {
813
+ setEditMeetError('La date/heure est requise');
814
+ setEditMeetLoading(false);
815
+ return;
816
+ }
817
+
818
+ const response = await fetch(`/api/tasks/${editingMeetTask.id}`, {
819
+ method: 'PUT',
820
+ headers: { 'Content-Type': 'application/json' },
821
+ body: JSON.stringify({
822
+ scheduledAt: editMeetData.scheduledAt,
823
+ durationMinutes: editMeetData.durationMinutes,
824
+ }),
825
+ });
826
+
827
+ const data = await response.json();
828
+ if (!response.ok) {
829
+ throw new Error(data.error || 'Erreur lors de la mise à jour du rendez-vous');
830
+ }
831
+
832
+ setShowEditMeetModal(false);
833
+ setEditingMeetTask(null);
834
+ setEditMeetLoading(false);
835
+ await fetchTasks();
836
+ } catch (error: any) {
837
+ setEditMeetError(error.message || 'Erreur lors de la mise à jour du rendez-vous');
838
+ setEditMeetLoading(false);
839
+ }
840
+ };
841
+
842
+ // Ne pas afficher le skeleton global, on l'affiche par vue
843
+
844
+ // Fonction pour formater la date pour l'affichage dans le header
845
+ const formatHeaderDateRange = () => {
846
+ if (view === 'month') {
847
+ const year = currentDate.getFullYear();
848
+ const month = currentDate.getMonth();
849
+ const start = new Date(year, month, 1);
850
+ const end = new Date(year, month + 1, 0);
851
+ const startStr = start.toLocaleDateString('fr-FR', {
852
+ day: 'numeric',
853
+ month: 'short',
854
+ year: 'numeric',
855
+ });
856
+ const endStr = end.toLocaleDateString('fr-FR', {
857
+ day: 'numeric',
858
+ month: 'short',
859
+ year: 'numeric',
860
+ });
861
+ return `${startStr} - ${endStr}`;
862
+ } else if (view === 'week') {
863
+ const days = getWeekDays();
864
+ const start = days[0];
865
+ const end = days[6];
866
+ const startStr = start.toLocaleDateString('fr-FR', {
867
+ day: 'numeric',
868
+ month: 'short',
869
+ year: 'numeric',
870
+ });
871
+ const endStr = end.toLocaleDateString('fr-FR', {
872
+ day: 'numeric',
873
+ month: 'short',
874
+ year: 'numeric',
875
+ });
876
+ return `${startStr} - ${endStr}`;
877
+ } else {
878
+ return currentDate.toLocaleDateString('fr-FR', {
879
+ day: 'numeric',
880
+ month: 'short',
881
+ year: 'numeric',
882
+ });
883
+ }
884
+ };
885
+
886
+ const formatCurrentMonthYear = () => {
887
+ return currentDate.toLocaleDateString('fr-FR', {
888
+ year: 'numeric',
889
+ month: 'long',
890
+ });
891
+ };
892
+
893
+ const formatCurrentDay = () => {
894
+ const day = currentDate.getDate();
895
+ const month = currentDate.toLocaleDateString('fr-FR', { month: 'short' }).toUpperCase();
896
+ return { day, month };
897
+ };
898
+
899
+ return (
900
+ <div className="bg-crms-bg flex h-full flex-col">
901
+ {/* Header avec titre et navigation */}
902
+ <div className="border-b border-gray-200 bg-white px-4 py-4 sm:px-6 lg:px-8">
903
+ {/* Titre et informations de date */}
904
+ <div className="mb-4 flex items-start justify-between gap-4">
905
+ <div className="flex items-start gap-4">
906
+ {/* Date actuelle en grand */}
907
+ <div className="flex flex-col">
908
+ <div className="text-3xl font-bold text-gray-900">{formatCurrentDay().day}</div>
909
+ <div className="text-xs font-medium text-gray-500 uppercase">
910
+ {formatCurrentDay().month}
911
+ </div>
912
+ </div>
913
+ <div className="flex flex-col">
914
+ <h1 className="text-2xl font-bold text-gray-900">Agenda</h1>
915
+ <p className="mt-1 text-sm text-gray-500">{formatCurrentMonthYear()}</p>
916
+ <p className="text-xs text-gray-400">{formatHeaderDateRange()}</p>
917
+ </div>
918
+ </div>
919
+
920
+ {/* Actions à droite */}
921
+ <div className="flex items-center gap-2">
922
+ <button
923
+ type="button"
924
+ onClick={() => setShowCreateMeetModal(true)}
925
+ className="inline-flex cursor-pointer items-center gap-2 rounded-lg bg-green-600 px-3 py-2 text-xs font-semibold text-white shadow-sm transition-colors hover:bg-green-700 sm:px-4 sm:text-sm"
926
+ >
927
+ <Video className="h-4 w-4" />
928
+ <span className="hidden sm:inline">Google Meet</span>
929
+ </button>
930
+ <button
931
+ type="button"
932
+ onClick={() => setShowCreateMeetingModal(true)}
933
+ className="inline-flex cursor-pointer items-center gap-2 rounded-lg bg-blue-600 px-3 py-2 text-xs font-semibold text-white shadow-sm transition-colors hover:bg-blue-700 sm:px-4 sm:text-sm"
934
+ >
935
+ <Calendar className="h-4 w-4" />
936
+ <span className="hidden sm:inline">Rendez-vous</span>
937
+ </button>
938
+ <button
939
+ type="button"
940
+ onClick={() => setShowCreateTaskModal(true)}
941
+ className="inline-flex cursor-pointer items-center gap-2 rounded-lg bg-indigo-600 px-3 py-2 text-xs font-semibold text-white shadow-sm transition-colors hover:bg-indigo-700 sm:px-4 sm:text-sm"
942
+ >
943
+ <Bookmark className="h-4 w-4" />
944
+ <span className="hidden sm:inline">Tâche</span>
945
+ <span className="sm:hidden">Ajouter</span>
946
+ </button>
947
+ </div>
948
+ </div>
949
+
950
+ {/* Filtres et navigation */}
951
+ <div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
952
+ {/* Filtres d'événements */}
953
+ <div className="flex items-center gap-2">
954
+ <button
955
+ type="button"
956
+ onClick={() =>
957
+ setFilters({
958
+ tasks: true,
959
+ meetings: true,
960
+ googleMeets: true,
961
+ priorities: {
962
+ LOW: true,
963
+ MEDIUM: true,
964
+ HIGH: true,
965
+ URGENT: true,
966
+ },
967
+ })
968
+ }
969
+ className={cn(
970
+ 'cursor-pointer rounded-full px-3 py-1.5 text-xs font-medium transition-colors',
971
+ filters.tasks && filters.meetings && filters.googleMeets
972
+ ? 'bg-gray-100 text-gray-900'
973
+ : 'bg-white text-gray-600 hover:bg-gray-50',
974
+ )}
975
+ >
976
+ Tous les événements
977
+ </button>
978
+ <button
979
+ type="button"
980
+ onClick={() =>
981
+ setFilters({
982
+ tasks: true,
983
+ meetings: false,
984
+ googleMeets: false,
985
+ priorities: filters.priorities,
986
+ })
987
+ }
988
+ className={cn(
989
+ 'cursor-pointer rounded-full px-3 py-1.5 text-xs font-medium transition-colors',
990
+ filters.tasks && !filters.meetings && !filters.googleMeets
991
+ ? 'bg-gray-100 text-gray-900'
992
+ : 'bg-white text-gray-600 hover:bg-gray-50',
993
+ )}
994
+ >
995
+ Tâches
996
+ </button>
997
+ <button
998
+ type="button"
999
+ onClick={() =>
1000
+ setFilters({
1001
+ tasks: false,
1002
+ meetings: true,
1003
+ googleMeets: false,
1004
+ priorities: filters.priorities,
1005
+ })
1006
+ }
1007
+ className={cn(
1008
+ 'cursor-pointer rounded-full px-3 py-1.5 text-xs font-medium transition-colors',
1009
+ !filters.tasks && filters.meetings && !filters.googleMeets
1010
+ ? 'bg-gray-100 text-gray-900'
1011
+ : 'bg-white text-gray-600 hover:bg-gray-50',
1012
+ )}
1013
+ >
1014
+ Rendez-vous
1015
+ </button>
1016
+ <button
1017
+ type="button"
1018
+ onClick={() =>
1019
+ setFilters({
1020
+ tasks: false,
1021
+ meetings: false,
1022
+ googleMeets: true,
1023
+ priorities: filters.priorities,
1024
+ })
1025
+ }
1026
+ className={cn(
1027
+ 'cursor-pointer rounded-full px-3 py-1.5 text-xs font-medium transition-colors',
1028
+ !filters.tasks && !filters.meetings && filters.googleMeets
1029
+ ? 'bg-gray-100 text-gray-900'
1030
+ : 'bg-white text-gray-600 hover:bg-gray-50',
1031
+ )}
1032
+ >
1033
+ Google Meet
1034
+ </button>
1035
+ </div>
1036
+
1037
+ {/* Filtres par priorité */}
1038
+ <div className="flex items-center gap-2">
1039
+ <span className="text-xs font-medium text-gray-500">Priorité:</span>
1040
+ <button
1041
+ type="button"
1042
+ onClick={() =>
1043
+ setFilters({
1044
+ ...filters,
1045
+ priorities: {
1046
+ ...filters.priorities,
1047
+ LOW: !filters.priorities.LOW,
1048
+ },
1049
+ })
1050
+ }
1051
+ className={cn(
1052
+ 'cursor-pointer rounded-full px-2.5 py-1 text-xs font-medium transition-colors',
1053
+ filters.priorities.LOW
1054
+ ? 'bg-green-100 text-green-700'
1055
+ : 'bg-white text-gray-400 hover:bg-gray-50',
1056
+ )}
1057
+ >
1058
+ Faible
1059
+ </button>
1060
+ <button
1061
+ type="button"
1062
+ onClick={() =>
1063
+ setFilters({
1064
+ ...filters,
1065
+ priorities: {
1066
+ ...filters.priorities,
1067
+ MEDIUM: !filters.priorities.MEDIUM,
1068
+ },
1069
+ })
1070
+ }
1071
+ className={cn(
1072
+ 'cursor-pointer rounded-full px-2.5 py-1 text-xs font-medium transition-colors',
1073
+ filters.priorities.MEDIUM
1074
+ ? 'bg-blue-100 text-blue-700'
1075
+ : 'bg-white text-gray-400 hover:bg-gray-50',
1076
+ )}
1077
+ >
1078
+ Moyenne
1079
+ </button>
1080
+ <button
1081
+ type="button"
1082
+ onClick={() =>
1083
+ setFilters({
1084
+ ...filters,
1085
+ priorities: {
1086
+ ...filters.priorities,
1087
+ HIGH: !filters.priorities.HIGH,
1088
+ },
1089
+ })
1090
+ }
1091
+ className={cn(
1092
+ 'cursor-pointer rounded-full px-2.5 py-1 text-xs font-medium transition-colors',
1093
+ filters.priorities.HIGH
1094
+ ? 'bg-orange-100 text-orange-700'
1095
+ : 'bg-white text-gray-400 hover:bg-gray-50',
1096
+ )}
1097
+ >
1098
+ Haute
1099
+ </button>
1100
+ <button
1101
+ type="button"
1102
+ onClick={() =>
1103
+ setFilters({
1104
+ ...filters,
1105
+ priorities: {
1106
+ ...filters.priorities,
1107
+ URGENT: !filters.priorities.URGENT,
1108
+ },
1109
+ })
1110
+ }
1111
+ className={cn(
1112
+ 'cursor-pointer rounded-full px-2.5 py-1 text-xs font-medium transition-colors',
1113
+ filters.priorities.URGENT
1114
+ ? 'bg-red-100 text-red-700'
1115
+ : 'bg-white text-gray-400 hover:bg-gray-50',
1116
+ )}
1117
+ >
1118
+ Urgente
1119
+ </button>
1120
+ </div>
1121
+
1122
+ {/* Navigation et sélecteur de vue */}
1123
+ <div className="flex items-center gap-2">
1124
+ {/* Navigation de date */}
1125
+ <div className="flex items-center gap-1 rounded-lg border border-gray-200 bg-white p-1">
1126
+ <button
1127
+ onClick={() => navigateDate('prev')}
1128
+ className="cursor-pointer rounded-md p-1.5 text-gray-600 transition-colors hover:bg-gray-100"
1129
+ aria-label="Date précédente"
1130
+ >
1131
+ <ChevronLeft className="h-4 w-4" />
1132
+ </button>
1133
+ <button
1134
+ onClick={goToToday}
1135
+ className="cursor-pointer rounded-md px-3 py-1.5 text-xs font-medium text-gray-700 transition-colors hover:bg-gray-100"
1136
+ >
1137
+ Aujourd'hui
1138
+ </button>
1139
+ <button
1140
+ onClick={() => navigateDate('next')}
1141
+ className="cursor-pointer rounded-md p-1.5 text-gray-600 transition-colors hover:bg-gray-100"
1142
+ aria-label="Date suivante"
1143
+ >
1144
+ <ChevronRight className="h-4 w-4" />
1145
+ </button>
1146
+ </div>
1147
+
1148
+ {/* Sélecteur de vue */}
1149
+ <select
1150
+ value={view}
1151
+ onChange={(e) => setView(e.target.value as 'month' | 'week' | 'day')}
1152
+ className="cursor-pointer rounded-lg border border-gray-200 bg-white px-3 py-1.5 text-xs font-medium text-gray-700 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
1153
+ >
1154
+ <option value="month">Vue mois</option>
1155
+ <option value="week">Vue semaine</option>
1156
+ <option value="day">Vue jour</option>
1157
+ </select>
1158
+ </div>
1159
+ </div>
1160
+ </div>
1161
+
1162
+ <div className="flex-1 overflow-y-auto p-4 sm:p-6 lg:p-8">
1163
+ {view === 'month' && (
1164
+ <>
1165
+ {loading ? (
1166
+ <AgendaMonthSkeleton />
1167
+ ) : (
1168
+ <div className="overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm">
1169
+ <div className="grid grid-cols-7 border-b border-gray-200 bg-gray-50/60">
1170
+ {['Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam', 'Dim'].map((day) => (
1171
+ <div
1172
+ key={day}
1173
+ className="border-r border-gray-200 px-4 py-3 text-center text-xs font-medium tracking-wide text-gray-500 uppercase last:border-r-0"
1174
+ >
1175
+ {day}
1176
+ </div>
1177
+ ))}
1178
+ </div>
1179
+ <div className="grid grid-cols-7 divide-x divide-y divide-gray-100">
1180
+ {getDaysInMonth().map((day, index) => {
1181
+ const dayTasks = getTasksForDate(day.date);
1182
+ const isToday = day.date.toDateString() === new Date().toDateString();
1183
+
1184
+ return (
1185
+ <div
1186
+ key={index}
1187
+ onClick={() => {
1188
+ setCurrentDate(day.date);
1189
+ setView('day');
1190
+ }}
1191
+ className={cn(
1192
+ 'min-h-[120px] cursor-pointer p-2 transition-colors hover:bg-gray-50/50',
1193
+ !day.isCurrentMonth && 'bg-gray-50/30',
1194
+ isToday && 'bg-indigo-50/40',
1195
+ )}
1196
+ >
1197
+ <div className="relative">
1198
+ <div
1199
+ className={cn(
1200
+ 'mb-2 text-sm font-semibold',
1201
+ isToday
1202
+ ? 'text-indigo-600'
1203
+ : day.isCurrentMonth
1204
+ ? 'text-gray-900'
1205
+ : 'text-gray-400',
1206
+ )}
1207
+ >
1208
+ {day.date.getDate()}
1209
+ </div>
1210
+ {/* Indicateur d'heure actuelle dans la vue mois */}
1211
+ {isToday && (
1212
+ <div className="absolute top-0 right-0 h-1.5 w-1.5 rounded-full bg-blue-500" />
1213
+ )}
1214
+ </div>
1215
+ <div className="space-y-1" onClick={(e) => e.stopPropagation()}>
1216
+ {dayTasks.slice(0, 3).map((task) => {
1217
+ const typeColor = TYPE_COLORS[task.type] || '#64748B';
1218
+ return (
1219
+ <Link
1220
+ key={task.id}
1221
+ href={task.contact ? `/contacts/${task.contact.id}` : '#'}
1222
+ className={cn(
1223
+ 'block rounded px-2 py-1 text-xs transition-colors',
1224
+ task.contact
1225
+ ? 'cursor-pointer hover:opacity-80'
1226
+ : 'cursor-default',
1227
+ )}
1228
+ style={{
1229
+ backgroundColor: `${typeColor}15`,
1230
+ borderLeft: `3px solid ${typeColor}`,
1231
+ }}
1232
+ title={`${TASK_TYPE_LABELS[task.type]} - ${task.title || 'Sans titre'}`}
1233
+ onClick={(e) => {
1234
+ if (!task.contact) {
1235
+ e.preventDefault();
1236
+ }
1237
+ e.stopPropagation();
1238
+ }}
1239
+ >
1240
+ <div className="flex items-center gap-1.5">
1241
+ <div className="flex-1 truncate font-medium text-gray-900">
1242
+ {formatTime(task.scheduledAt)}{' '}
1243
+ {task.title || TASK_TYPE_LABELS[task.type]}
1244
+ </div>
1245
+ <span
1246
+ className="shrink-0 rounded-full px-1.5 py-0.5 text-[10px] font-medium"
1247
+ style={{
1248
+ backgroundColor: `${PRIORITY_COLORS[task.priority]}20`,
1249
+ color: PRIORITY_COLORS[task.priority],
1250
+ }}
1251
+ >
1252
+ {PRIORITY_LABELS[task.priority].charAt(0)}
1253
+ </span>
1254
+ </div>
1255
+ </Link>
1256
+ );
1257
+ })}
1258
+ {dayTasks.length > 3 && (
1259
+ <div className="px-2 text-xs text-gray-500">
1260
+ +{dayTasks.length - 3} autre(s)
1261
+ </div>
1262
+ )}
1263
+ </div>
1264
+ </div>
1265
+ );
1266
+ })}
1267
+ </div>
1268
+ </div>
1269
+ )}
1270
+ </>
1271
+ )}
1272
+
1273
+ {/* Vue semaine */}
1274
+ {view === 'week' && (
1275
+ <>
1276
+ {loading ? (
1277
+ <AgendaWeekSkeleton />
1278
+ ) : (
1279
+ <div className="overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm">
1280
+ <div className="overflow-auto">
1281
+ <div
1282
+ className="grid border-b border-gray-200 bg-gray-50/60"
1283
+ style={{ gridTemplateColumns: '59px repeat(7, 1fr)' }}
1284
+ >
1285
+ <div className="px-2 py-3 text-right text-xs font-medium text-gray-500"></div>
1286
+ {getWeekDays().map((day) => {
1287
+ const isToday = day.toDateString() === new Date().toDateString();
1288
+ return (
1289
+ <div
1290
+ key={day.toISOString()}
1291
+ className={cn('border-l border-gray-200 px-3 py-3 text-center')}
1292
+ >
1293
+ <div className="text-xs font-medium text-gray-500 uppercase">
1294
+ {day.toLocaleDateString('fr-FR', { weekday: 'short' }).replace('.', '')}
1295
+ </div>
1296
+ <div
1297
+ className={cn(
1298
+ 'mx-auto mt-2 flex h-8 w-8 items-center justify-center rounded-full text-sm font-semibold',
1299
+ isToday ? 'bg-gray-900 text-white' : 'text-gray-900',
1300
+ )}
1301
+ >
1302
+ {day.getDate()}
1303
+ </div>
1304
+ </div>
1305
+ );
1306
+ })}
1307
+ </div>
1308
+ <div
1309
+ className="relative grid"
1310
+ style={{ gridTemplateColumns: '60px repeat(7, 1fr)' }}
1311
+ >
1312
+ {/* Colonne des heures */}
1313
+ <div className="relative border-r border-gray-200 bg-gray-50/40">
1314
+ {HOURS.map((hour) => {
1315
+ // Vérifier si l'heure actuelle est dans cette plage horaire
1316
+ const weekDays = getWeekDays();
1317
+ const isCurrentWeek =
1318
+ currentTime &&
1319
+ weekDays.some(
1320
+ (day) => day.toDateString() === currentTime.toDateString(),
1321
+ );
1322
+ const shouldHideHour =
1323
+ isCurrentWeek &&
1324
+ currentTime &&
1325
+ currentTime.getHours() === hour &&
1326
+ currentTime.getMinutes() >= 0 &&
1327
+ currentTime.getMinutes() < 60;
1328
+
1329
+ return (
1330
+ <div
1331
+ key={hour}
1332
+ className="relative flex h-16 items-start justify-end border-b border-gray-100 pt-1 pr-2 text-xs text-gray-500"
1333
+ >
1334
+ {!shouldHideHour && formatHourLabel(hour)}
1335
+ </div>
1336
+ );
1337
+ })}
1338
+ {/* Heure actuelle dans la colonne des heures (calculée uniquement côté client) */}
1339
+ {currentTime &&
1340
+ (() => {
1341
+ const weekDays = getWeekDays();
1342
+ const isCurrentWeek = weekDays.some(
1343
+ (day) => day.toDateString() === currentTime.toDateString(),
1344
+ );
1345
+ if (isCurrentWeek) {
1346
+ const currentHour = currentTime.getHours();
1347
+ const currentMinutes = currentTime.getMinutes();
1348
+ const slotHeight = 64;
1349
+ const currentTimeMinutes = currentHour * 60 + currentMinutes;
1350
+ const top = (currentTimeMinutes - HOURS[0] * 60) * (slotHeight / 60);
1351
+ if (top >= 0 && top <= HOURS.length * slotHeight) {
1352
+ return (
1353
+ <div
1354
+ className="absolute right-0 z-20 flex items-center"
1355
+ style={{ top: `${top}px`, transform: 'translateY(-50%)' }}
1356
+ >
1357
+ <div className="mr-1 h-2 w-2 rounded-full bg-blue-500" />
1358
+ <span className="pr-2 text-xs font-medium text-blue-600">
1359
+ {currentTime.toLocaleTimeString('fr-FR', {
1360
+ hour: '2-digit',
1361
+ minute: '2-digit',
1362
+ })}
1363
+ </span>
1364
+ </div>
1365
+ );
1366
+ }
1367
+ }
1368
+ return null;
1369
+ })()}
1370
+ </div>
1371
+
1372
+ {/* Colonnes des jours */}
1373
+ {getWeekDays().map((day) => {
1374
+ // Récupérer toutes les tâches du jour
1375
+ const dayTasks = filteredTasks.filter((task) => {
1376
+ const d = new Date(task.scheduledAt);
1377
+ return (
1378
+ d.getFullYear() === day.getFullYear() &&
1379
+ d.getMonth() === day.getMonth() &&
1380
+ d.getDate() === day.getDate()
1381
+ );
1382
+ });
1383
+
1384
+ const isToday =
1385
+ currentTime && day.toDateString() === currentTime.toDateString();
1386
+ const slotHeight = 64;
1387
+ const currentTimeMinutes =
1388
+ currentTime?.getHours() != null
1389
+ ? currentTime.getHours() * 60 + currentTime.getMinutes()
1390
+ : 0;
1391
+ const currentTimeTop =
1392
+ isToday && currentTime
1393
+ ? (currentTimeMinutes - HOURS[0] * 60) * (slotHeight / 60)
1394
+ : -1;
1395
+
1396
+ return (
1397
+ <div key={day.toISOString()} className="relative border-l border-gray-100">
1398
+ {/* Barre de l'heure actuelle */}
1399
+ {isToday &&
1400
+ currentTimeTop >= 0 &&
1401
+ currentTimeTop <= HOURS.length * slotHeight && (
1402
+ <div
1403
+ className="absolute right-0 left-0 z-20 flex items-center"
1404
+ style={{
1405
+ top: `${currentTimeTop}px`,
1406
+ transform: 'translateY(-50%)',
1407
+ }}
1408
+ >
1409
+ <div className="h-0.5 w-full border-t border-b border-dashed border-blue-400 bg-blue-500" />
1410
+ <div className="absolute left-0 h-2 w-2 -translate-x-1/2 rounded-full bg-blue-500" />
1411
+ </div>
1412
+ )}
1413
+
1414
+ {/* Afficher les tâches positionnées absolument */}
1415
+ {dayTasks.map((task) => {
1416
+ const d = new Date(task.scheduledAt);
1417
+ const taskHour = d.getHours();
1418
+ const taskMinutes = d.getMinutes();
1419
+ const duration = task.durationMinutes || 30;
1420
+ const startMinutes = taskHour * 60 + taskMinutes;
1421
+ const endMinutes = startMinutes + duration;
1422
+ const top = (startMinutes - HOURS[0] * 60) * (slotHeight / 60);
1423
+ const height = duration * (slotHeight / 60);
1424
+
1425
+ const typeColor = TYPE_COLORS[task.type] || '#64748B';
1426
+ return (
1427
+ <div
1428
+ key={task.id}
1429
+ className="absolute right-1.5 left-1.5 z-10 rounded px-2 py-1 text-xs shadow-sm"
1430
+ style={{
1431
+ top: `${top}px`,
1432
+ height: `${height}px`,
1433
+ minHeight: '20px',
1434
+ backgroundColor: `${typeColor}15`,
1435
+ borderLeft: `3px solid ${typeColor}`,
1436
+ }}
1437
+ >
1438
+ <div className="flex items-center gap-1.5">
1439
+ <div className="flex-1 truncate font-medium text-gray-900">
1440
+ {task.title || TASK_TYPE_LABELS[task.type]}
1441
+ </div>
1442
+ <span
1443
+ className="shrink-0 rounded-full px-1.5 py-0.5 text-[10px] font-medium"
1444
+ style={{
1445
+ backgroundColor: `${PRIORITY_COLORS[task.priority]}20`,
1446
+ color: PRIORITY_COLORS[task.priority],
1447
+ }}
1448
+ >
1449
+ {PRIORITY_LABELS[task.priority].charAt(0)}
1450
+ </span>
1451
+ </div>
1452
+ {task.contact && (
1453
+ <div className="mt-0.5 truncate text-[10px] text-gray-600">
1454
+ {task.contact.firstName} {task.contact.lastName}
1455
+ </div>
1456
+ )}
1457
+ </div>
1458
+ );
1459
+ })}
1460
+
1461
+ {/* Grille des heures */}
1462
+ {HOURS.map((hour) => (
1463
+ <div key={hour} className="h-16 border-b border-gray-100" />
1464
+ ))}
1465
+ </div>
1466
+ );
1467
+ })}
1468
+ </div>
1469
+ </div>
1470
+ </div>
1471
+ )}
1472
+ </>
1473
+ )}
1474
+
1475
+ {/* Liste des tâches de la semaine (en bas) */}
1476
+ {view === 'week' && !loading && (
1477
+ <div className="mt-6">
1478
+ <h2 className="mb-4 text-lg font-semibold text-gray-900">
1479
+ Toutes les tâches de la semaine
1480
+ </h2>
1481
+ <div className="space-y-3">
1482
+ {filteredTasks.length === 0 ? (
1483
+ <div className="rounded-xl border border-gray-200 bg-white p-8 text-center shadow-sm">
1484
+ <p className="text-gray-500">Aucune tâche pour cette semaine</p>
1485
+ </div>
1486
+ ) : (
1487
+ filteredTasks.map((task) => {
1488
+ const TaskContent = (
1489
+ <div className="flex items-start justify-between">
1490
+ <div className="flex-1">
1491
+ <div className="flex items-center gap-2">
1492
+ <button
1493
+ type="button"
1494
+ onClick={(e) => {
1495
+ e.stopPropagation();
1496
+ toggleTaskComplete(task.id, task.completed);
1497
+ }}
1498
+ className="cursor-pointer text-gray-400 hover:text-indigo-600"
1499
+ >
1500
+ {task.completed ? (
1501
+ <CheckCircle2 className="h-5 w-5 text-indigo-600" />
1502
+ ) : (
1503
+ <Circle className="h-5 w-5" />
1504
+ )}
1505
+ </button>
1506
+ <div className="flex-1">
1507
+ <div className="flex items-center gap-2">
1508
+ <span
1509
+ className="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium"
1510
+ style={{
1511
+ backgroundColor: `${PRIORITY_COLORS[task.priority]}20`,
1512
+ color: PRIORITY_COLORS[task.priority],
1513
+ }}
1514
+ >
1515
+ {PRIORITY_LABELS[task.priority]}
1516
+ </span>
1517
+ <span className="text-sm text-gray-500">
1518
+ {TASK_TYPE_LABELS[task.type]}
1519
+ </span>
1520
+ </div>
1521
+ <h3
1522
+ className={cn(
1523
+ 'mt-1 text-base font-semibold',
1524
+ task.completed ? 'text-gray-400 line-through' : 'text-gray-900',
1525
+ )}
1526
+ >
1527
+ {task.title || TASK_TYPE_LABELS[task.type]}
1528
+ </h3>
1529
+ {task.contact && (
1530
+ <div className="mt-1 inline-flex items-center gap-1 text-sm text-indigo-600">
1531
+ <User className="h-4 w-4" />
1532
+ <span>
1533
+ {task.contact.firstName} {task.contact.lastName}
1534
+ </span>
1535
+ </div>
1536
+ )}
1537
+ {task.googleMeetLink && (
1538
+ <div className="mt-2 flex flex-wrap items-center gap-2">
1539
+ <button
1540
+ type="button"
1541
+ onClick={(e) => {
1542
+ e.stopPropagation();
1543
+ if (task.googleMeetLink) {
1544
+ window.open(
1545
+ task.googleMeetLink,
1546
+ '_blank',
1547
+ 'noopener,noreferrer',
1548
+ );
1549
+ }
1550
+ }}
1551
+ className="inline-flex items-center gap-1.5 rounded-lg border border-indigo-200 bg-indigo-50 px-3 py-1.5 text-sm font-medium text-indigo-700 transition-colors hover:bg-indigo-100"
1552
+ >
1553
+ <Video className="h-4 w-4" />
1554
+ <span>Rejoindre Google Meet</span>
1555
+ <ExternalLink className="h-3 w-3" />
1556
+ </button>
1557
+ <button
1558
+ type="button"
1559
+ onClick={(e) => {
1560
+ e.stopPropagation();
1561
+ openEditMeetModal(task);
1562
+ }}
1563
+ className="inline-flex cursor-pointer items-center gap-1.5 rounded-lg border border-gray-300 bg-white px-3 py-1.5 text-xs font-medium text-gray-700 transition-colors hover:bg-gray-50"
1564
+ >
1565
+ Modifier le rendez-vous
1566
+ </button>
1567
+ </div>
1568
+ )}
1569
+ <div className="mt-2 flex items-center gap-4 text-sm text-gray-500">
1570
+ <div className="flex items-center gap-1">
1571
+ <Clock className="h-4 w-4" />
1572
+ {formatTime(task.scheduledAt)}
1573
+ </div>
1574
+ <div className="flex items-center gap-1">
1575
+ <Calendar className="h-4 w-4" />
1576
+ {new Date(task.scheduledAt).toLocaleDateString('fr-FR')}
1577
+ </div>
1578
+ {isAdmin && (
1579
+ <div className="flex items-center gap-1">
1580
+ <User className="h-4 w-4" />
1581
+ {task.assignedUser.name}
1582
+ </div>
1583
+ )}
1584
+ </div>
1585
+ </div>
1586
+ </div>
1587
+ </div>
1588
+ </div>
1589
+ );
1590
+
1591
+ if (task.contact) {
1592
+ return (
1593
+ <Link
1594
+ key={task.id}
1595
+ href={`/contacts/${task.contact.id}`}
1596
+ onClick={(e) => {
1597
+ // Empêcher la navigation si on clique sur un bouton ou un lien
1598
+ const target = e.target as HTMLElement;
1599
+ if (
1600
+ target.tagName === 'BUTTON' ||
1601
+ target.closest('button') ||
1602
+ target.tagName === 'A' ||
1603
+ target.closest('a')
1604
+ ) {
1605
+ e.preventDefault();
1606
+ }
1607
+ }}
1608
+ className="block rounded-xl border border-gray-200 bg-white p-4 shadow-sm transition-colors hover:border-indigo-300 hover:shadow-md"
1609
+ >
1610
+ {TaskContent}
1611
+ </Link>
1612
+ );
1613
+ }
1614
+
1615
+ return (
1616
+ <div
1617
+ key={task.id}
1618
+ className="cursor-pointer rounded-xl border border-gray-200 bg-white p-4 shadow-sm transition-colors hover:border-indigo-300 hover:shadow-md"
1619
+ onClick={() => openTaskDetailModal(task)}
1620
+ >
1621
+ {TaskContent}
1622
+ </div>
1623
+ );
1624
+ })
1625
+ )}
1626
+ </div>
1627
+ </div>
1628
+ )}
1629
+
1630
+ {/* Vue jour : liste des tâches */}
1631
+ {view === 'day' && (
1632
+ <>
1633
+ {loading ? (
1634
+ <AgendaDaySkeleton />
1635
+ ) : (
1636
+ <div className="space-y-3">
1637
+ {filteredTasks.length === 0 ? (
1638
+ <div className="rounded-xl border border-gray-200 bg-white p-8 text-center shadow-sm">
1639
+ <p className="text-gray-500">Aucune tâche pour cette journée</p>
1640
+ </div>
1641
+ ) : (
1642
+ filteredTasks.map((task) => {
1643
+ const TaskContent = (
1644
+ <div className="flex items-start justify-between">
1645
+ <div className="flex-1">
1646
+ <div className="flex items-center gap-2">
1647
+ <button
1648
+ type="button"
1649
+ onClick={(e) => {
1650
+ e.stopPropagation();
1651
+ toggleTaskComplete(task.id, task.completed);
1652
+ }}
1653
+ className="cursor-pointer text-gray-400 hover:text-indigo-600"
1654
+ >
1655
+ {task.completed ? (
1656
+ <CheckCircle2 className="h-5 w-5 text-indigo-600" />
1657
+ ) : (
1658
+ <Circle className="h-5 w-5" />
1659
+ )}
1660
+ </button>
1661
+ <div className="flex-1">
1662
+ <div className="flex items-center gap-2">
1663
+ <span
1664
+ className="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium"
1665
+ style={{
1666
+ backgroundColor: `${PRIORITY_COLORS[task.priority]}20`,
1667
+ color: PRIORITY_COLORS[task.priority],
1668
+ }}
1669
+ >
1670
+ {PRIORITY_LABELS[task.priority]}
1671
+ </span>
1672
+ <span className="text-sm text-gray-500">
1673
+ {TASK_TYPE_LABELS[task.type]}
1674
+ </span>
1675
+ </div>
1676
+ <h3
1677
+ className={cn(
1678
+ 'mt-1 text-base font-semibold',
1679
+ task.completed ? 'text-gray-400 line-through' : 'text-gray-900',
1680
+ )}
1681
+ >
1682
+ {task.title || TASK_TYPE_LABELS[task.type]}
1683
+ </h3>
1684
+ {task.contact && (
1685
+ <div className="mt-1 inline-flex items-center gap-1 text-sm text-indigo-600">
1686
+ <User className="h-4 w-4" />
1687
+ <span>
1688
+ {task.contact.firstName} {task.contact.lastName}
1689
+ </span>
1690
+ </div>
1691
+ )}
1692
+ {task.googleMeetLink && (
1693
+ <div className="mt-2 flex flex-wrap items-center gap-2">
1694
+ <button
1695
+ type="button"
1696
+ onClick={(e) => {
1697
+ e.stopPropagation();
1698
+ if (task.googleMeetLink) {
1699
+ window.open(
1700
+ task.googleMeetLink,
1701
+ '_blank',
1702
+ 'noopener,noreferrer',
1703
+ );
1704
+ }
1705
+ }}
1706
+ className="inline-flex cursor-pointer items-center gap-1.5 rounded-lg border border-indigo-200 bg-indigo-50 px-3 py-1.5 text-sm font-medium text-indigo-700 transition-colors hover:bg-indigo-100"
1707
+ >
1708
+ <Video className="h-4 w-4" />
1709
+ <span>Rejoindre Google Meet</span>
1710
+ <ExternalLink className="h-3 w-3" />
1711
+ </button>
1712
+ <button
1713
+ type="button"
1714
+ onClick={(e) => {
1715
+ e.stopPropagation();
1716
+ openEditMeetModal(task);
1717
+ }}
1718
+ className="inline-flex cursor-pointer items-center gap-1.5 rounded-lg border border-gray-300 bg-white px-3 py-1.5 text-xs font-medium text-gray-700 transition-colors hover:bg-gray-50"
1719
+ >
1720
+ Modifier le rendez-vous
1721
+ </button>
1722
+ </div>
1723
+ )}
1724
+ <div className="mt-2 flex items-center gap-4 text-sm text-gray-500">
1725
+ <div className="flex items-center gap-1">
1726
+ <Clock className="h-4 w-4" />
1727
+ {formatTime(task.scheduledAt)}
1728
+ </div>
1729
+ <div className="flex items-center gap-1">
1730
+ <Calendar className="h-4 w-4" />
1731
+ {new Date(task.scheduledAt).toLocaleDateString('fr-FR')}
1732
+ </div>
1733
+ {isAdmin && (
1734
+ <div className="flex items-center gap-1">
1735
+ <User className="h-4 w-4" />
1736
+ {task.assignedUser.name}
1737
+ </div>
1738
+ )}
1739
+ </div>
1740
+ </div>
1741
+ </div>
1742
+ </div>
1743
+ </div>
1744
+ );
1745
+
1746
+ if (task.contact) {
1747
+ return (
1748
+ <Link
1749
+ key={task.id}
1750
+ href={`/contacts/${task.contact.id}`}
1751
+ onClick={(e) => {
1752
+ // Empêcher la navigation si on clique sur un bouton ou un lien
1753
+ const target = e.target as HTMLElement;
1754
+ if (
1755
+ target.tagName === 'BUTTON' ||
1756
+ target.closest('button') ||
1757
+ target.tagName === 'A' ||
1758
+ target.closest('a')
1759
+ ) {
1760
+ e.preventDefault();
1761
+ }
1762
+ }}
1763
+ className="block rounded-xl border border-gray-200 bg-white p-4 shadow-sm transition-colors hover:border-indigo-300 hover:shadow-md"
1764
+ >
1765
+ {TaskContent}
1766
+ </Link>
1767
+ );
1768
+ }
1769
+
1770
+ return (
1771
+ <div
1772
+ key={task.id}
1773
+ className="cursor-pointer rounded-xl border border-gray-200 bg-white p-4 shadow-sm transition-colors hover:border-indigo-300 hover:shadow-md"
1774
+ onClick={() => openTaskDetailModal(task)}
1775
+ >
1776
+ {TaskContent}
1777
+ </div>
1778
+ );
1779
+ })
1780
+ )}
1781
+ </div>
1782
+ )}
1783
+ </>
1784
+ )}
1785
+
1786
+ {/* Liste des tâches du mois (en bas) */}
1787
+ {view === 'month' && !loading && (
1788
+ <div className="mt-6">
1789
+ <h2 className="mb-4 text-lg font-semibold text-gray-900">Toutes les tâches du mois</h2>
1790
+ <div className="space-y-3">
1791
+ {filteredTasks.length === 0 ? (
1792
+ <div className="rounded-xl border border-gray-200 bg-white p-8 text-center shadow-sm">
1793
+ <p className="text-gray-500">Aucune tâche pour ce mois</p>
1794
+ </div>
1795
+ ) : (
1796
+ filteredTasks.map((task) => {
1797
+ const TaskContent = (
1798
+ <div className="flex items-start justify-between">
1799
+ <div className="flex-1">
1800
+ <div className="flex items-center gap-2">
1801
+ <button
1802
+ type="button"
1803
+ onClick={(e) => {
1804
+ e.stopPropagation();
1805
+ toggleTaskComplete(task.id, task.completed);
1806
+ }}
1807
+ className="cursor-pointer text-gray-400 hover:text-indigo-600"
1808
+ >
1809
+ {task.completed ? (
1810
+ <CheckCircle2 className="h-5 w-5 text-indigo-600" />
1811
+ ) : (
1812
+ <Circle className="h-5 w-5" />
1813
+ )}
1814
+ </button>
1815
+ <div className="flex-1">
1816
+ <div className="flex items-center gap-2">
1817
+ <span
1818
+ className="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium"
1819
+ style={{
1820
+ backgroundColor: `${PRIORITY_COLORS[task.priority]}20`,
1821
+ color: PRIORITY_COLORS[task.priority],
1822
+ }}
1823
+ >
1824
+ {PRIORITY_LABELS[task.priority]}
1825
+ </span>
1826
+ <span className="text-sm text-gray-500">
1827
+ {TASK_TYPE_LABELS[task.type]}
1828
+ </span>
1829
+ </div>
1830
+ <h3
1831
+ className={cn(
1832
+ 'mt-1 text-base font-semibold',
1833
+ task.completed ? 'text-gray-400 line-through' : 'text-gray-900',
1834
+ )}
1835
+ >
1836
+ {task.title || TASK_TYPE_LABELS[task.type]}
1837
+ </h3>
1838
+ {task.contact && (
1839
+ <div className="mt-1 inline-flex items-center gap-1 text-sm text-indigo-600">
1840
+ <User className="h-4 w-4" />
1841
+ <span>
1842
+ {task.contact.firstName} {task.contact.lastName}
1843
+ </span>
1844
+ </div>
1845
+ )}
1846
+ {task.googleMeetLink && (
1847
+ <div className="mt-2 flex flex-wrap items-center gap-2">
1848
+ <button
1849
+ type="button"
1850
+ onClick={(e) => {
1851
+ e.stopPropagation();
1852
+ if (task.googleMeetLink) {
1853
+ window.open(
1854
+ task.googleMeetLink,
1855
+ '_blank',
1856
+ 'noopener,noreferrer',
1857
+ );
1858
+ }
1859
+ }}
1860
+ className="inline-flex items-center gap-1.5 rounded-lg border border-indigo-200 bg-indigo-50 px-3 py-1.5 text-sm font-medium text-indigo-700 transition-colors hover:bg-indigo-100"
1861
+ >
1862
+ <Video className="h-4 w-4" />
1863
+ <span>Rejoindre Google Meet</span>
1864
+ <ExternalLink className="h-3 w-3" />
1865
+ </button>
1866
+ <button
1867
+ type="button"
1868
+ onClick={(e) => {
1869
+ e.stopPropagation();
1870
+ openEditMeetModal(task);
1871
+ }}
1872
+ className="inline-flex cursor-pointer items-center gap-1.5 rounded-lg border border-gray-300 bg-white px-3 py-1.5 text-xs font-medium text-gray-700 transition-colors hover:bg-gray-50"
1873
+ >
1874
+ Modifier le rendez-vous
1875
+ </button>
1876
+ </div>
1877
+ )}
1878
+ <div className="mt-2 flex items-center gap-4 text-sm text-gray-500">
1879
+ <div className="flex items-center gap-1">
1880
+ <Clock className="h-4 w-4" />
1881
+ {formatTime(task.scheduledAt)}
1882
+ </div>
1883
+ <div className="flex items-center gap-1">
1884
+ <Calendar className="h-4 w-4" />
1885
+ {new Date(task.scheduledAt).toLocaleDateString('fr-FR')}
1886
+ </div>
1887
+ {isAdmin && (
1888
+ <div className="flex items-center gap-1">
1889
+ <User className="h-4 w-4" />
1890
+ {task.assignedUser.name}
1891
+ </div>
1892
+ )}
1893
+ </div>
1894
+ </div>
1895
+ </div>
1896
+ </div>
1897
+ </div>
1898
+ );
1899
+
1900
+ if (task.contact) {
1901
+ return (
1902
+ <Link
1903
+ key={task.id}
1904
+ href={`/contacts/${task.contact.id}`}
1905
+ onClick={(e) => {
1906
+ // Empêcher la navigation si on clique sur un bouton ou un lien
1907
+ const target = e.target as HTMLElement;
1908
+ if (
1909
+ target.tagName === 'BUTTON' ||
1910
+ target.closest('button') ||
1911
+ target.tagName === 'A' ||
1912
+ target.closest('a')
1913
+ ) {
1914
+ e.preventDefault();
1915
+ }
1916
+ }}
1917
+ className="block rounded-xl border border-gray-200 bg-white p-4 shadow-sm transition-colors hover:border-indigo-300 hover:shadow-md"
1918
+ >
1919
+ {TaskContent}
1920
+ </Link>
1921
+ );
1922
+ }
1923
+
1924
+ return (
1925
+ <div
1926
+ key={task.id}
1927
+ className="cursor-pointer rounded-xl border border-gray-200 bg-white p-4 shadow-sm transition-colors hover:border-indigo-300 hover:shadow-md"
1928
+ onClick={() => openTaskDetailModal(task)}
1929
+ >
1930
+ {TaskContent}
1931
+ </div>
1932
+ );
1933
+ })
1934
+ )}
1935
+ </div>
1936
+ </div>
1937
+ )}
1938
+ </div>
1939
+
1940
+ {/* Modal d'édition de Google Meet */}
1941
+ {showEditMeetModal && editingMeetTask && (
1942
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-gray-500/20 p-4 backdrop-blur-sm sm:p-6">
1943
+ <div className="flex max-h-[90vh] w-full max-w-2xl flex-col rounded-lg bg-white p-6 shadow-xl sm:p-8">
1944
+ {/* En-tête fixe */}
1945
+ <div className="shrink-0 border-b border-gray-100 pb-4">
1946
+ <div className="flex items-center justify-between">
1947
+ <h2 className="text-xl font-bold text-gray-900 sm:text-2xl">
1948
+ Modifier le Google Meet
1949
+ </h2>
1950
+ <button
1951
+ type="button"
1952
+ onClick={() => {
1953
+ setShowEditMeetModal(false);
1954
+ setEditingMeetTask(null);
1955
+ setEditMeetError('');
1956
+ }}
1957
+ className="cursor-pointer rounded-lg p-2 text-gray-400 transition-colors hover:bg-gray-100"
1958
+ >
1959
+ <svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1960
+ <path
1961
+ strokeLinecap="round"
1962
+ strokeLinejoin="round"
1963
+ strokeWidth={2}
1964
+ d="M6 18L18 6M6 6l12 12"
1965
+ />
1966
+ </svg>
1967
+ </button>
1968
+ </div>
1969
+ {editingMeetTask.contact && (
1970
+ <p className="mt-1 text-sm text-gray-500">
1971
+ {editingMeetTask.contact.firstName} {editingMeetTask.contact.lastName}
1972
+ </p>
1973
+ )}
1974
+ </div>
1975
+
1976
+ {/* Contenu scrollable */}
1977
+ <form
1978
+ id="edit-meet-form"
1979
+ onSubmit={handleUpdateMeet}
1980
+ className="flex-1 space-y-4 overflow-y-auto pt-4 [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
1981
+ >
1982
+ <div className="space-y-2">
1983
+ <p className="text-sm font-medium text-gray-700">Date & heure *</p>
1984
+ <div className="grid grid-cols-[3fr,2fr] gap-2">
1985
+ <input
1986
+ type="date"
1987
+ required
1988
+ value={
1989
+ editMeetData.scheduledAt
1990
+ ? editMeetData.scheduledAt.split('T')[0]
1991
+ : new Date(editingMeetTask.scheduledAt).toISOString().split('T')[0]
1992
+ }
1993
+ onChange={(e) => {
1994
+ const time =
1995
+ editMeetData.scheduledAt && editMeetData.scheduledAt.includes('T')
1996
+ ? editMeetData.scheduledAt.split('T')[1]
1997
+ : new Date(editingMeetTask.scheduledAt)
1998
+ .toISOString()
1999
+ .split('T')[1]
2000
+ .slice(0, 5);
2001
+ setEditMeetData({
2002
+ ...editMeetData,
2003
+ scheduledAt: `${e.target.value}T${time || '09:00'}`,
2004
+ });
2005
+ }}
2006
+ className="block w-full rounded-xl border border-gray-300 px-3 py-2 text-sm text-gray-900 shadow-sm focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
2007
+ />
2008
+ <input
2009
+ type="time"
2010
+ required
2011
+ value={
2012
+ editMeetData.scheduledAt && editMeetData.scheduledAt.includes('T')
2013
+ ? editMeetData.scheduledAt.split('T')[1].slice(0, 5)
2014
+ : new Date(editingMeetTask.scheduledAt)
2015
+ .toISOString()
2016
+ .split('T')[1]
2017
+ .slice(0, 5)
2018
+ }
2019
+ onChange={(e) => {
2020
+ const datePart =
2021
+ editMeetData.scheduledAt && editMeetData.scheduledAt.includes('T')
2022
+ ? editMeetData.scheduledAt.split('T')[0]
2023
+ : new Date(editingMeetTask.scheduledAt).toISOString().split('T')[0];
2024
+ setEditMeetData({
2025
+ ...editMeetData,
2026
+ scheduledAt: `${datePart}T${e.target.value || '09:00'}`,
2027
+ });
2028
+ }}
2029
+ className="block w-full rounded-xl border border-gray-300 px-3 py-2 text-sm text-gray-900 shadow-sm focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
2030
+ />
2031
+ </div>
2032
+ </div>
2033
+
2034
+ <div className="space-y-2">
2035
+ <label className="block text-sm font-medium text-gray-700">Durée (minutes)</label>
2036
+ <select
2037
+ value={editMeetData.durationMinutes}
2038
+ onChange={(e) =>
2039
+ setEditMeetData({
2040
+ ...editMeetData,
2041
+ durationMinutes: Number(e.target.value),
2042
+ })
2043
+ }
2044
+ className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
2045
+ >
2046
+ <option value={15}>15 minutes</option>
2047
+ <option value={30}>30 minutes</option>
2048
+ <option value={45}>45 minutes</option>
2049
+ <option value={60}>1 heure</option>
2050
+ <option value={90}>1h30</option>
2051
+ <option value={120}>2 heures</option>
2052
+ </select>
2053
+ </div>
2054
+
2055
+ {editingMeetTask.googleMeetLink && (
2056
+ <div className="space-y-1">
2057
+ <p className="text-sm font-medium text-gray-700">Lien Google Meet</p>
2058
+ <a
2059
+ href={editingMeetTask.googleMeetLink}
2060
+ target="_blank"
2061
+ rel="noopener noreferrer"
2062
+ className="inline-flex items-center gap-1.5 text-sm text-indigo-600 hover:text-indigo-700"
2063
+ >
2064
+ <Video className="h-4 w-4" />
2065
+ <span className="truncate">{editingMeetTask.googleMeetLink}</span>
2066
+ <ExternalLink className="h-3 w-3" />
2067
+ </a>
2068
+ </div>
2069
+ )}
2070
+
2071
+ {editMeetError && (
2072
+ <div className="rounded-lg bg-red-50 p-3 text-sm text-red-600">{editMeetError}</div>
2073
+ )}
2074
+ </form>
2075
+
2076
+ {/* Pied de modal fixe */}
2077
+ <div className="shrink-0 border-t border-gray-100 pt-4">
2078
+ <div className="flex flex-col gap-3 sm:flex-row sm:justify-end">
2079
+ <button
2080
+ type="button"
2081
+ onClick={() => {
2082
+ setShowEditMeetModal(false);
2083
+ setEditingMeetTask(null);
2084
+ setEditMeetError('');
2085
+ }}
2086
+ className="w-full cursor-pointer rounded-xl border border-gray-300 bg-white px-4 py-2.5 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-50 sm:w-auto"
2087
+ >
2088
+ Annuler
2089
+ </button>
2090
+ <button
2091
+ type="submit"
2092
+ form="edit-meet-form"
2093
+ disabled={editMeetLoading}
2094
+ className="w-full cursor-pointer rounded-xl bg-indigo-600 px-4 py-2.5 text-sm font-medium text-white shadow-sm transition-colors hover:bg-indigo-700 focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto"
2095
+ >
2096
+ {editMeetLoading ? 'Enregistrement...' : 'Enregistrer les modifications'}
2097
+ </button>
2098
+ </div>
2099
+ </div>
2100
+ </div>
2101
+ </div>
2102
+ )}
2103
+
2104
+ {/* Modal de création de tâche (générique, non liée à un contact) */}
2105
+ {showCreateTaskModal && (
2106
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-gray-500/20 p-6 backdrop-blur-sm sm:p-8">
2107
+ <div className="flex max-h-[90vh] w-full max-w-4xl flex-col rounded-2xl bg-white p-6 shadow-xl">
2108
+ {/* En-tête fixe */}
2109
+ <div className="shrink-0 border-b border-gray-100 pb-4">
2110
+ <div className="flex items-start justify-between gap-4">
2111
+ <div>
2112
+ <h2 className="text-lg font-semibold text-gray-900 sm:text-xl">
2113
+ Créer une tâche
2114
+ </h2>
2115
+ <p className="mt-1 text-xs text-gray-500 sm:text-sm">
2116
+ Cette tâche sera créée pour vous et ne sera liée à aucun contact.
2117
+ </p>
2118
+ </div>
2119
+ <button
2120
+ type="button"
2121
+ onClick={() => {
2122
+ setShowCreateTaskModal(false);
2123
+ setCreateTaskError('');
2124
+ setCreateTaskData({
2125
+ type: 'OTHER',
2126
+ title: '',
2127
+ description: '',
2128
+ scheduledAt: '',
2129
+ priority: 'MEDIUM',
2130
+ reminderMinutesBefore: null,
2131
+ });
2132
+ }}
2133
+ className="cursor-pointer rounded-full p-2 text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-600"
2134
+ aria-label="Fermer la modal"
2135
+ >
2136
+ <svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
2137
+ <path
2138
+ strokeLinecap="round"
2139
+ strokeLinejoin="round"
2140
+ strokeWidth={2}
2141
+ d="M6 18L18 6M6 6l12 12"
2142
+ />
2143
+ </svg>
2144
+ </button>
2145
+ </div>
2146
+ </div>
2147
+
2148
+ {/* Contenu scrollable */}
2149
+ <form
2150
+ id="create-task-form"
2151
+ onSubmit={handleCreateTaskFromAgenda}
2152
+ className="flex-1 space-y-6 overflow-y-auto px-2 pt-4 pb-2 [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
2153
+ >
2154
+ {/* Titre */}
2155
+ <div>
2156
+ <label className="block text-sm font-medium text-gray-700">Titre (optionnel)</label>
2157
+ <input
2158
+ type="text"
2159
+ value={createTaskData.title}
2160
+ onChange={(e) =>
2161
+ setCreateTaskData({
2162
+ ...createTaskData,
2163
+ title: e.target.value,
2164
+ })
2165
+ }
2166
+ className="mt-1 block w-full rounded-xl border border-gray-300 px-4 py-2.5 text-sm text-gray-900 shadow-sm focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
2167
+ placeholder="Ex : Relance client"
2168
+ />
2169
+ </div>
2170
+
2171
+ {/* Date & heure + priorité */}
2172
+ <div className="grid gap-4 md:grid-cols-2">
2173
+ <div className="space-y-2">
2174
+ <p className="text-sm font-medium text-gray-700">Date & heure *</p>
2175
+ <div className="grid grid-cols-[3fr,2fr] gap-2">
2176
+ <input
2177
+ type="date"
2178
+ required
2179
+ value={
2180
+ createTaskData.scheduledAt ? createTaskData.scheduledAt.split('T')[0] : ''
2181
+ }
2182
+ onChange={(e) => {
2183
+ const time =
2184
+ createTaskData.scheduledAt && createTaskData.scheduledAt.includes('T')
2185
+ ? createTaskData.scheduledAt.split('T')[1]
2186
+ : '09:00';
2187
+ setCreateTaskData({
2188
+ ...createTaskData,
2189
+ scheduledAt: `${e.target.value}T${time}`,
2190
+ });
2191
+ }}
2192
+ className="block w-full rounded-xl border border-gray-300 px-2 py-1.5 text-xs text-gray-900 shadow-sm focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
2193
+ />
2194
+ <input
2195
+ type="time"
2196
+ required
2197
+ value={
2198
+ createTaskData.scheduledAt && createTaskData.scheduledAt.includes('T')
2199
+ ? createTaskData.scheduledAt.split('T')[1].slice(0, 5)
2200
+ : ''
2201
+ }
2202
+ onChange={(e) => {
2203
+ const datePart =
2204
+ createTaskData.scheduledAt && createTaskData.scheduledAt.includes('T')
2205
+ ? createTaskData.scheduledAt.split('T')[0]
2206
+ : new Date().toISOString().split('T')[0];
2207
+ setCreateTaskData({
2208
+ ...createTaskData,
2209
+ scheduledAt: `${datePart}T${e.target.value || '09:00'}`,
2210
+ });
2211
+ }}
2212
+ className="block w-full rounded-xl border border-gray-300 px-2 py-1.5 text-xs text-gray-900 shadow-sm focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
2213
+ />
2214
+ </div>
2215
+ </div>
2216
+
2217
+ <div className="space-y-2">
2218
+ <p className="text-sm font-medium text-gray-700">Priorité</p>
2219
+ <div className="grid grid-cols-2 gap-2 sm:grid-cols-4">
2220
+ {[
2221
+ { value: 'LOW' as const, label: 'Faible' },
2222
+ { value: 'MEDIUM' as const, label: 'Moyenne' },
2223
+ { value: 'HIGH' as const, label: 'Haute' },
2224
+ { value: 'URGENT' as const, label: 'Urgente' },
2225
+ ].map((option) => {
2226
+ const isActive = createTaskData.priority === option.value;
2227
+ return (
2228
+ <button
2229
+ key={option.value}
2230
+ type="button"
2231
+ onClick={() =>
2232
+ setCreateTaskData({
2233
+ ...createTaskData,
2234
+ priority: option.value,
2235
+ })
2236
+ }
2237
+ className={cn(
2238
+ 'cursor-pointer rounded-xl border px-3 py-2 text-xs font-medium transition-colors sm:text-sm',
2239
+ isActive
2240
+ ? 'border-indigo-500 bg-indigo-50 text-indigo-700'
2241
+ : 'border-gray-200 bg-white text-gray-700 hover:border-indigo-200 hover:bg-indigo-50/60',
2242
+ )}
2243
+ >
2244
+ {option.label}
2245
+ </button>
2246
+ );
2247
+ })}
2248
+ </div>
2249
+ </div>
2250
+ </div>
2251
+
2252
+ {/* Rappel */}
2253
+ <div className="space-y-2">
2254
+ <p className="text-sm font-medium text-gray-700">Rappel</p>
2255
+ <select
2256
+ value={createTaskData.reminderMinutesBefore ?? ''}
2257
+ onChange={(e) =>
2258
+ setCreateTaskData({
2259
+ ...createTaskData,
2260
+ reminderMinutesBefore: e.target.value ? Number(e.target.value) : null,
2261
+ })
2262
+ }
2263
+ className="mt-1 block w-full rounded-xl border border-gray-300 px-4 py-2.5 text-sm text-gray-900 shadow-sm focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
2264
+ >
2265
+ <option value="">Aucun rappel</option>
2266
+ <option value="5">5 minutes avant</option>
2267
+ <option value="15">15 minutes avant</option>
2268
+ <option value="30">30 minutes avant</option>
2269
+ <option value="60">1 heure avant</option>
2270
+ <option value="120">2 heures avant</option>
2271
+ </select>
2272
+ </div>
2273
+
2274
+ {/* Description */}
2275
+ <div className="space-y-2">
2276
+ <label className="block text-sm font-medium text-gray-700">Description *</label>
2277
+ <textarea
2278
+ value={createTaskData.description}
2279
+ onChange={(e) =>
2280
+ setCreateTaskData({
2281
+ ...createTaskData,
2282
+ description: e.target.value,
2283
+ })
2284
+ }
2285
+ rows={4}
2286
+ className="mt-1 block w-full rounded-xl border border-gray-300 px-4 py-2.5 text-sm text-gray-900 shadow-sm focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
2287
+ placeholder="Ajoutez les détails de la tâche (contexte, actions à mener, etc.)"
2288
+ />
2289
+ </div>
2290
+
2291
+ {createTaskError && (
2292
+ <div className="rounded-lg bg-red-50 p-4 text-sm text-red-600">
2293
+ {createTaskError}
2294
+ </div>
2295
+ )}
2296
+ </form>
2297
+
2298
+ {/* Pied de modal fixe */}
2299
+ <div className="shrink-0 border-t border-gray-100 pt-4">
2300
+ <div className="flex flex-col gap-3 sm:flex-row sm:justify-end">
2301
+ <button
2302
+ type="button"
2303
+ onClick={() => {
2304
+ setShowCreateTaskModal(false);
2305
+ setCreateTaskError('');
2306
+ setCreateTaskData({
2307
+ type: 'OTHER',
2308
+ title: '',
2309
+ description: '',
2310
+ scheduledAt: '',
2311
+ priority: 'MEDIUM',
2312
+ reminderMinutesBefore: null,
2313
+ });
2314
+ }}
2315
+ className="w-full cursor-pointer rounded-xl border border-gray-300 bg-white px-4 py-2.5 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-50 sm:w-auto"
2316
+ >
2317
+ Annuler
2318
+ </button>
2319
+ <button
2320
+ type="submit"
2321
+ form="create-task-form"
2322
+ disabled={creatingTask}
2323
+ className="w-full cursor-pointer rounded-xl bg-indigo-600 px-4 py-2.5 text-sm font-medium text-white shadow-sm transition-colors hover:bg-indigo-700 focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto"
2324
+ >
2325
+ {creatingTask ? 'Création...' : 'Créer la tâche'}
2326
+ </button>
2327
+ </div>
2328
+ </div>
2329
+ </div>
2330
+ </div>
2331
+ )}
2332
+
2333
+ {/* Modal de création de Google Meet */}
2334
+ {showCreateMeetModal && (
2335
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-gray-500/20 p-4 backdrop-blur-sm sm:p-6">
2336
+ <div className="flex max-h-[90vh] w-full max-w-5xl flex-col rounded-lg bg-white p-6 shadow-xl sm:p-8">
2337
+ {/* En-tête fixe */}
2338
+ <div className="shrink-0 border-b border-gray-100 pb-4">
2339
+ <div className="flex items-center justify-between">
2340
+ <h2 className="text-xl font-bold text-gray-900 sm:text-2xl">
2341
+ Programmer une visio-conférence
2342
+ </h2>
2343
+ <button
2344
+ onClick={() => {
2345
+ setShowCreateMeetModal(false);
2346
+ setMeetData({
2347
+ title: '',
2348
+ description: '',
2349
+ scheduledAt: '',
2350
+ durationMinutes: 30,
2351
+ attendees: [],
2352
+ reminderMinutesBefore: null,
2353
+ internalNote: '',
2354
+ });
2355
+ setMeetError('');
2356
+ }}
2357
+ className="cursor-pointer rounded-lg p-2 text-gray-400 transition-colors hover:bg-gray-100"
2358
+ type="button"
2359
+ >
2360
+ <X className="h-6 w-6" />
2361
+ </button>
2362
+ </div>
2363
+ </div>
2364
+
2365
+ {/* Contenu scrollable */}
2366
+ <form
2367
+ id="meet-form"
2368
+ onSubmit={handleCreateMeet}
2369
+ className="flex-1 space-y-4 overflow-y-auto px-1 pt-4 pb-2 [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
2370
+ >
2371
+ <div>
2372
+ <label className="block text-sm font-medium text-gray-700">
2373
+ Titre de la réunion *
2374
+ </label>
2375
+ <input
2376
+ type="text"
2377
+ required
2378
+ value={meetData.title}
2379
+ onChange={(e) => setMeetData({ ...meetData, title: e.target.value })}
2380
+ className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
2381
+ placeholder="Ex: Rendez-vous avec..."
2382
+ />
2383
+ </div>
2384
+
2385
+ <div className="grid gap-4 md:grid-cols-2">
2386
+ <div className="space-y-2">
2387
+ <p className="text-sm font-medium text-gray-700">Date & heure *</p>
2388
+ <div className="grid grid-cols-[3fr,2fr] gap-2">
2389
+ <input
2390
+ type="date"
2391
+ required
2392
+ value={meetData.scheduledAt ? meetData.scheduledAt.split('T')[0] : ''}
2393
+ onChange={(e) => {
2394
+ const time =
2395
+ meetData.scheduledAt && meetData.scheduledAt.includes('T')
2396
+ ? meetData.scheduledAt.split('T')[1]
2397
+ : '';
2398
+ setMeetData({
2399
+ ...meetData,
2400
+ scheduledAt: time
2401
+ ? `${e.target.value}T${time}`
2402
+ : `${e.target.value}T09:00`,
2403
+ });
2404
+ }}
2405
+ className="block w-full rounded-xl border border-gray-300 px-2 py-1.5 text-xs text-gray-900 shadow-sm focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
2406
+ />
2407
+ <input
2408
+ type="time"
2409
+ value={
2410
+ meetData.scheduledAt && meetData.scheduledAt.includes('T')
2411
+ ? meetData.scheduledAt.split('T')[1].slice(0, 5)
2412
+ : ''
2413
+ }
2414
+ onChange={(e) => {
2415
+ const datePart =
2416
+ meetData.scheduledAt && meetData.scheduledAt.includes('T')
2417
+ ? meetData.scheduledAt.split('T')[0]
2418
+ : new Date().toISOString().split('T')[0];
2419
+ setMeetData({
2420
+ ...meetData,
2421
+ scheduledAt: `${datePart}T${e.target.value || '09:00'}`,
2422
+ });
2423
+ }}
2424
+ className="block w-full rounded-xl border border-gray-300 px-2 py-1.5 text-xs text-gray-900 shadow-sm focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
2425
+ />
2426
+ </div>
2427
+ </div>
2428
+
2429
+ <div className="space-y-2">
2430
+ <p className="text-sm font-medium text-gray-700">Durée (minutes)</p>
2431
+ <select
2432
+ value={meetData.durationMinutes}
2433
+ onChange={(e) =>
2434
+ setMeetData({
2435
+ ...meetData,
2436
+ durationMinutes: Number(e.target.value),
2437
+ })
2438
+ }
2439
+ className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
2440
+ >
2441
+ <option value="15">15 minutes</option>
2442
+ <option value="30">30 minutes</option>
2443
+ <option value="45">45 minutes</option>
2444
+ <option value="60">1 heure</option>
2445
+ <option value="90">1h30</option>
2446
+ <option value="120">2 heures</option>
2447
+ </select>
2448
+ </div>
2449
+ </div>
2450
+
2451
+ <div>
2452
+ <label className="block text-sm font-medium text-gray-700">Rappel</label>
2453
+ <select
2454
+ value={meetData.reminderMinutesBefore ?? ''}
2455
+ onChange={(e) =>
2456
+ setMeetData({
2457
+ ...meetData,
2458
+ reminderMinutesBefore: e.target.value ? Number(e.target.value) : null,
2459
+ })
2460
+ }
2461
+ className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
2462
+ >
2463
+ <option value="">Aucun rappel</option>
2464
+ <option value="5">5 minutes avant</option>
2465
+ <option value="15">15 minutes avant</option>
2466
+ <option value="30">30 minutes avant</option>
2467
+ <option value="60">1 heure avant</option>
2468
+ <option value="120">2 heures avant</option>
2469
+ </select>
2470
+ </div>
2471
+
2472
+ <div>
2473
+ <label className="block text-sm font-medium text-gray-700">
2474
+ Invités (emails, un par ligne)
2475
+ </label>
2476
+ <textarea
2477
+ value={meetData.attendees.join('\n')}
2478
+ onChange={(e) =>
2479
+ setMeetData({
2480
+ ...meetData,
2481
+ attendees: e.target.value
2482
+ .split('\n')
2483
+ .map((email) => email.trim())
2484
+ .filter((email) => email !== ''),
2485
+ })
2486
+ }
2487
+ rows={4}
2488
+ className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
2489
+ placeholder={`email1@example.com
2490
+ email2@example.com`}
2491
+ />
2492
+ <p className="mt-1 text-xs text-gray-500">
2493
+ Les invités recevront un email de confirmation avec le lien Google Meet.
2494
+ </p>
2495
+ </div>
2496
+
2497
+ <div className="space-y-2">
2498
+ <label className="block text-sm font-medium text-gray-700">Description</label>
2499
+ <Editor ref={meetEditorRef} />
2500
+ <p className="text-xs text-gray-500">
2501
+ Ajoutez des détails sur cette réunion (ordre du jour, points à aborder…). Cette
2502
+ description sera partagée avec les participants.
2503
+ </p>
2504
+ </div>
2505
+
2506
+ <div>
2507
+ <label className="block text-sm font-medium text-gray-700">
2508
+ Note personnelle (optionnel)
2509
+ </label>
2510
+ <textarea
2511
+ value={meetData.internalNote}
2512
+ onChange={(e) => setMeetData({ ...meetData, internalNote: e.target.value })}
2513
+ rows={3}
2514
+ className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
2515
+ placeholder="Ajoutez une note personnelle qui ne sera pas partagée dans l'email..."
2516
+ />
2517
+ <p className="mt-1 text-xs text-gray-500">
2518
+ Cette note est uniquement visible dans le CRM et ne sera pas envoyée aux
2519
+ participants.
2520
+ </p>
2521
+ </div>
2522
+
2523
+ {meetError && (
2524
+ <div className="rounded-lg bg-red-50 p-4 text-sm text-red-600">{meetError}</div>
2525
+ )}
2526
+ </form>
2527
+
2528
+ {/* Pied de modal fixe */}
2529
+ <div className="shrink-0 border-t border-gray-100 pt-4">
2530
+ <div className="flex flex-col gap-3 sm:flex-row sm:justify-end">
2531
+ <button
2532
+ type="button"
2533
+ onClick={() => {
2534
+ setShowCreateMeetModal(false);
2535
+ setMeetData({
2536
+ title: '',
2537
+ description: '',
2538
+ scheduledAt: '',
2539
+ durationMinutes: 30,
2540
+ attendees: [],
2541
+ reminderMinutesBefore: null,
2542
+ internalNote: '',
2543
+ });
2544
+ setMeetError('');
2545
+ }}
2546
+ className="w-full cursor-pointer rounded-xl border border-gray-300 bg-white px-4 py-2.5 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-50 sm:w-auto"
2547
+ >
2548
+ Annuler
2549
+ </button>
2550
+ <button
2551
+ type="submit"
2552
+ form="meet-form"
2553
+ disabled={creatingMeet}
2554
+ className="w-full cursor-pointer rounded-xl bg-indigo-600 px-4 py-2.5 text-sm font-medium text-white shadow-sm transition-colors hover:bg-indigo-700 focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto"
2555
+ >
2556
+ {creatingMeet ? 'Création...' : 'Créer le Google Meet'}
2557
+ </button>
2558
+ </div>
2559
+ </div>
2560
+ </div>
2561
+ </div>
2562
+ )}
2563
+
2564
+ {/* Modal de création de rendez-vous physique */}
2565
+ {showCreateMeetingModal && (
2566
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-gray-500/20 p-4 backdrop-blur-sm sm:p-6">
2567
+ <div className="flex max-h-[90vh] w-full max-w-5xl flex-col rounded-lg bg-white p-6 shadow-xl sm:p-8">
2568
+ {/* En-tête fixe */}
2569
+ <div className="shrink-0 border-b border-gray-100 pb-4">
2570
+ <div className="flex items-center justify-between">
2571
+ <h2 className="text-xl font-bold text-gray-900 sm:text-2xl">
2572
+ Programmer un rendez-vous
2573
+ </h2>
2574
+ <button
2575
+ onClick={() => {
2576
+ setShowCreateMeetingModal(false);
2577
+ setMeetingData({
2578
+ title: '',
2579
+ description: '',
2580
+ scheduledAt: '',
2581
+ priority: 'MEDIUM',
2582
+ reminderMinutesBefore: null,
2583
+ notifyContact: false,
2584
+ internalNote: '',
2585
+ attendees: [],
2586
+ });
2587
+ setMeetingError('');
2588
+ }}
2589
+ className="cursor-pointer rounded-lg p-2 text-gray-400 transition-colors hover:bg-gray-100"
2590
+ type="button"
2591
+ >
2592
+ <X className="h-6 w-6" />
2593
+ </button>
2594
+ </div>
2595
+ </div>
2596
+
2597
+ {/* Contenu scrollable */}
2598
+ <form
2599
+ id="meeting-form"
2600
+ onSubmit={handleCreateMeeting}
2601
+ className="flex-1 space-y-6 overflow-y-auto px-2 pt-4 pb-2 [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
2602
+ >
2603
+ <div>
2604
+ <label className="block text-sm font-medium text-gray-700">
2605
+ Titre du rendez-vous (optionnel)
2606
+ </label>
2607
+ <input
2608
+ type="text"
2609
+ value={meetingData.title}
2610
+ onChange={(e) => setMeetingData({ ...meetingData, title: e.target.value })}
2611
+ className="mt-1 block w-full rounded-xl border border-gray-300 px-4 py-2.5 text-sm text-gray-900 shadow-sm focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
2612
+ placeholder="Ex : Rendez-vous avec le client"
2613
+ />
2614
+ </div>
2615
+
2616
+ <div className="grid gap-4 md:grid-cols-2">
2617
+ <div className="space-y-2">
2618
+ <p className="text-sm font-medium text-gray-700">Date & heure *</p>
2619
+ <div className="grid grid-cols-[3fr,2fr] gap-2">
2620
+ <input
2621
+ type="date"
2622
+ required
2623
+ value={meetingData.scheduledAt ? meetingData.scheduledAt.split('T')[0] : ''}
2624
+ onChange={(e) => {
2625
+ const time =
2626
+ meetingData.scheduledAt && meetingData.scheduledAt.includes('T')
2627
+ ? meetingData.scheduledAt.split('T')[1]
2628
+ : '';
2629
+ setMeetingData({
2630
+ ...meetingData,
2631
+ scheduledAt: time
2632
+ ? `${e.target.value}T${time}`
2633
+ : `${e.target.value}T09:00`,
2634
+ });
2635
+ }}
2636
+ className="block w-full rounded-xl border border-gray-300 px-2 py-1.5 text-xs text-gray-900 shadow-sm focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
2637
+ />
2638
+ <input
2639
+ type="time"
2640
+ value={
2641
+ meetingData.scheduledAt && meetingData.scheduledAt.includes('T')
2642
+ ? meetingData.scheduledAt.split('T')[1].slice(0, 5)
2643
+ : ''
2644
+ }
2645
+ onChange={(e) => {
2646
+ const datePart =
2647
+ meetingData.scheduledAt && meetingData.scheduledAt.includes('T')
2648
+ ? meetingData.scheduledAt.split('T')[0]
2649
+ : new Date().toISOString().split('T')[0];
2650
+ setMeetingData({
2651
+ ...meetingData,
2652
+ scheduledAt: `${datePart}T${e.target.value || '09:00'}`,
2653
+ });
2654
+ }}
2655
+ className="block w-full rounded-xl border border-gray-300 px-2 py-1.5 text-xs text-gray-900 shadow-sm focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
2656
+ />
2657
+ </div>
2658
+ </div>
2659
+
2660
+ <div className="space-y-2">
2661
+ <p className="text-sm font-medium text-gray-700">Priorité</p>
2662
+ <div className="grid grid-cols-2 gap-2 sm:grid-cols-4">
2663
+ {[
2664
+ { value: 'LOW' as const, label: 'Faible' },
2665
+ { value: 'MEDIUM' as const, label: 'Moyenne' },
2666
+ { value: 'HIGH' as const, label: 'Haute' },
2667
+ { value: 'URGENT' as const, label: 'Urgente' },
2668
+ ].map((option) => {
2669
+ const isActive = meetingData.priority === option.value;
2670
+ return (
2671
+ <button
2672
+ key={option.value}
2673
+ type="button"
2674
+ onClick={() =>
2675
+ setMeetingData({
2676
+ ...meetingData,
2677
+ priority: option.value,
2678
+ })
2679
+ }
2680
+ className={cn(
2681
+ 'cursor-pointer rounded-xl border px-3 py-2 text-xs font-medium transition-colors sm:text-sm',
2682
+ isActive
2683
+ ? 'border-indigo-500 bg-indigo-50 text-indigo-700'
2684
+ : 'border-gray-200 bg-white text-gray-700 hover:border-indigo-200 hover:bg-indigo-50/60',
2685
+ )}
2686
+ >
2687
+ {option.label}
2688
+ </button>
2689
+ );
2690
+ })}
2691
+ </div>
2692
+ </div>
2693
+ </div>
2694
+
2695
+ <div className="space-y-2">
2696
+ <p className="text-sm font-medium text-gray-700">Rappel</p>
2697
+ <select
2698
+ value={meetingData.reminderMinutesBefore ?? ''}
2699
+ onChange={(e) =>
2700
+ setMeetingData({
2701
+ ...meetingData,
2702
+ reminderMinutesBefore: e.target.value ? Number(e.target.value) : null,
2703
+ })
2704
+ }
2705
+ className="mt-1 block w-full rounded-xl border border-gray-300 px-4 py-2.5 text-sm text-gray-900 shadow-sm focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
2706
+ >
2707
+ <option value="">Aucun rappel</option>
2708
+ <option value="5">5 minutes avant</option>
2709
+ <option value="15">15 minutes avant</option>
2710
+ <option value="30">30 minutes avant</option>
2711
+ <option value="60">1 heure avant</option>
2712
+ <option value="120">2 heures avant</option>
2713
+ </select>
2714
+ </div>
2715
+
2716
+ <div>
2717
+ <label className="block text-sm font-medium text-gray-700">
2718
+ Invités (emails, un par ligne)
2719
+ </label>
2720
+ <textarea
2721
+ value={meetingData.attendees.join('\n')}
2722
+ onChange={(e) =>
2723
+ setMeetingData({
2724
+ ...meetingData,
2725
+ attendees: e.target.value
2726
+ .split('\n')
2727
+ .map((email) => email.trim())
2728
+ .filter((email) => email !== ''),
2729
+ })
2730
+ }
2731
+ rows={4}
2732
+ className="mt-1 block w-full rounded-xl border border-gray-300 px-4 py-2.5 text-sm text-gray-900 shadow-sm focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
2733
+ placeholder={`email1@example.com
2734
+ email2@example.com`}
2735
+ />
2736
+ <p className="mt-1 text-xs text-gray-500">
2737
+ Les invités recevront un email de confirmation et le rendez-vous apparaîtra dans
2738
+ leur Google Calendar.
2739
+ </p>
2740
+ </div>
2741
+
2742
+ <div className="space-y-2">
2743
+ <label className="block text-sm font-medium text-gray-700">Description *</label>
2744
+ <Editor ref={meetingEditorRef} />
2745
+ <p className="text-xs text-gray-500">
2746
+ Ajoutez des détails sur ce rendez-vous (contexte, points à aborder, informations
2747
+ importantes…).
2748
+ </p>
2749
+ </div>
2750
+
2751
+ <div className="rounded-lg border border-blue-200 bg-blue-50 p-4">
2752
+ <label className="flex cursor-pointer items-start gap-3">
2753
+ <input
2754
+ type="checkbox"
2755
+ checked={meetingData.notifyContact}
2756
+ onChange={(e) =>
2757
+ setMeetingData({ ...meetingData, notifyContact: e.target.checked })
2758
+ }
2759
+ className="mt-0.5 h-4 w-4 cursor-pointer rounded border-gray-300 text-indigo-600 focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
2760
+ />
2761
+ <div className="flex-1">
2762
+ <p className="text-sm font-medium text-blue-900">
2763
+ Envoyer un email de confirmation
2764
+ </p>
2765
+ <p className="mt-1 text-xs text-blue-700">
2766
+ Un email de confirmation sera envoyé aux invités après la création du
2767
+ rendez-vous.
2768
+ </p>
2769
+ </div>
2770
+ </label>
2771
+ </div>
2772
+
2773
+ <div>
2774
+ <label className="block text-sm font-medium text-gray-700">Note personnelle</label>
2775
+ <p className="mt-1 text-xs text-gray-500">
2776
+ Cette note ne sera pas visible par les invités s'ils sont prévenus par email.
2777
+ </p>
2778
+ <textarea
2779
+ value={meetingData.internalNote}
2780
+ onChange={(e) => setMeetingData({ ...meetingData, internalNote: e.target.value })}
2781
+ rows={3}
2782
+ className="mt-1 block w-full rounded-xl border border-gray-300 px-4 py-2.5 text-sm text-gray-900 shadow-sm focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
2783
+ placeholder="Ajoutez une note personnelle qui ne sera pas partagée..."
2784
+ />
2785
+ </div>
2786
+
2787
+ {meetingError && (
2788
+ <div className="rounded-lg bg-red-50 p-4 text-sm text-red-600">{meetingError}</div>
2789
+ )}
2790
+ </form>
2791
+
2792
+ {/* Pied de modal fixe */}
2793
+ <div className="shrink-0 border-t border-gray-100 pt-4">
2794
+ <div className="flex flex-col gap-3 sm:flex-row sm:justify-end">
2795
+ <button
2796
+ type="button"
2797
+ onClick={() => {
2798
+ setShowCreateMeetingModal(false);
2799
+ setMeetingData({
2800
+ title: '',
2801
+ description: '',
2802
+ scheduledAt: '',
2803
+ priority: 'MEDIUM',
2804
+ reminderMinutesBefore: null,
2805
+ notifyContact: false,
2806
+ internalNote: '',
2807
+ attendees: [],
2808
+ });
2809
+ setMeetingError('');
2810
+ }}
2811
+ className="w-full cursor-pointer rounded-xl border border-gray-300 bg-white px-4 py-2.5 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-50 sm:w-auto"
2812
+ >
2813
+ Annuler
2814
+ </button>
2815
+ <button
2816
+ type="submit"
2817
+ form="meeting-form"
2818
+ disabled={creatingMeeting}
2819
+ className="w-full cursor-pointer rounded-xl bg-indigo-600 px-4 py-2.5 text-sm font-medium text-white shadow-sm transition-colors hover:bg-indigo-700 focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto"
2820
+ >
2821
+ {creatingMeeting ? 'Création...' : 'Créer le rendez-vous'}
2822
+ </button>
2823
+ </div>
2824
+ </div>
2825
+ </div>
2826
+ </div>
2827
+ )}
2828
+
2829
+ {/* Modal de détail / édition d'une tâche générique */}
2830
+ {showTaskDetailModal && selectedTask && !selectedTask.contact && (
2831
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-gray-500/20 p-6 backdrop-blur-sm sm:p-8">
2832
+ <div className="flex max-h-[90vh] w-full max-w-4xl flex-col rounded-2xl bg-white p-6 shadow-xl">
2833
+ {/* En-tête */}
2834
+ <div className="shrink-0 border-b border-gray-100 pb-4">
2835
+ <div className="flex items-start justify-between gap-4">
2836
+ <div>
2837
+ <h2 className="text-lg font-semibold text-gray-900 sm:text-xl">
2838
+ Détail de la tâche
2839
+ </h2>
2840
+ <p className="mt-1 text-xs text-gray-500 sm:text-sm">
2841
+ Tâche personnelle non liée à un contact.
2842
+ </p>
2843
+ </div>
2844
+ <button
2845
+ type="button"
2846
+ onClick={() => {
2847
+ setShowTaskDetailModal(false);
2848
+ setSelectedTask(null);
2849
+ setTaskDetailError('');
2850
+ }}
2851
+ className="cursor-pointer rounded-full p-2 text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-600"
2852
+ aria-label="Fermer la modal"
2853
+ >
2854
+ <svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
2855
+ <path
2856
+ strokeLinecap="round"
2857
+ strokeLinejoin="round"
2858
+ strokeWidth={2}
2859
+ d="M6 18L18 6M6 6l12 12"
2860
+ />
2861
+ </svg>
2862
+ </button>
2863
+ </div>
2864
+ </div>
2865
+
2866
+ {/* Contenu */}
2867
+ <form
2868
+ id="task-detail-form"
2869
+ onSubmit={handleUpdateTaskDetail}
2870
+ className="flex-1 space-y-6 overflow-y-auto px-2 pt-4 pb-2 [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
2871
+ >
2872
+ {/* Titre + statut */}
2873
+ <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
2874
+ <div className="flex-1">
2875
+ <label className="block text-sm font-medium text-gray-700">
2876
+ Titre (optionnel)
2877
+ </label>
2878
+ <input
2879
+ type="text"
2880
+ value={taskDetailData.title}
2881
+ onChange={(e) =>
2882
+ setTaskDetailData({
2883
+ ...taskDetailData,
2884
+ title: e.target.value,
2885
+ })
2886
+ }
2887
+ className="mt-1 block w-full rounded-xl border border-gray-300 px-4 py-2.5 text-sm text-gray-900 shadow-sm focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
2888
+ placeholder="Ex : Relance client"
2889
+ />
2890
+ </div>
2891
+ <label className="flex cursor-pointer items-center gap-2 text-sm text-gray-700">
2892
+ <input
2893
+ type="checkbox"
2894
+ checked={taskDetailData.completed}
2895
+ onChange={(e) =>
2896
+ setTaskDetailData({
2897
+ ...taskDetailData,
2898
+ completed: e.target.checked,
2899
+ })
2900
+ }
2901
+ className="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
2902
+ />
2903
+ <span>Marquer comme terminée</span>
2904
+ </label>
2905
+ </div>
2906
+
2907
+ {/* Date & heure + priorité */}
2908
+ <div className="grid gap-4 md:grid-cols-2">
2909
+ <div className="space-y-2">
2910
+ <p className="text-sm font-medium text-gray-700">Date & heure *</p>
2911
+ <div className="grid grid-cols-[3fr,2fr] gap-2">
2912
+ <input
2913
+ type="date"
2914
+ required
2915
+ value={
2916
+ taskDetailData.scheduledAt ? taskDetailData.scheduledAt.split('T')[0] : ''
2917
+ }
2918
+ onChange={(e) => {
2919
+ const time =
2920
+ taskDetailData.scheduledAt && taskDetailData.scheduledAt.includes('T')
2921
+ ? taskDetailData.scheduledAt.split('T')[1]
2922
+ : '09:00';
2923
+ setTaskDetailData({
2924
+ ...taskDetailData,
2925
+ scheduledAt: `${e.target.value}T${time}`,
2926
+ });
2927
+ }}
2928
+ className="block w-full rounded-xl border border-gray-300 px-2 py-1.5 text-xs text-gray-900 shadow-sm focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
2929
+ />
2930
+ <input
2931
+ type="time"
2932
+ required
2933
+ value={
2934
+ taskDetailData.scheduledAt && taskDetailData.scheduledAt.includes('T')
2935
+ ? taskDetailData.scheduledAt.split('T')[1].slice(0, 5)
2936
+ : ''
2937
+ }
2938
+ onChange={(e) => {
2939
+ const datePart =
2940
+ taskDetailData.scheduledAt && taskDetailData.scheduledAt.includes('T')
2941
+ ? taskDetailData.scheduledAt.split('T')[0]
2942
+ : new Date().toISOString().split('T')[0];
2943
+ setTaskDetailData({
2944
+ ...taskDetailData,
2945
+ scheduledAt: `${datePart}T${e.target.value || '09:00'}`,
2946
+ });
2947
+ }}
2948
+ className="block w-full rounded-xl border border-gray-300 px-2 py-1.5 text-xs text-gray-900 shadow-sm focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
2949
+ />
2950
+ </div>
2951
+ </div>
2952
+
2953
+ <div className="space-y-2">
2954
+ <p className="text-sm font-medium text-gray-700">Priorité</p>
2955
+ <div className="grid grid-cols-2 gap-2 sm:grid-cols-4">
2956
+ {[
2957
+ { value: 'LOW' as const, label: 'Faible' },
2958
+ { value: 'MEDIUM' as const, label: 'Moyenne' },
2959
+ { value: 'HIGH' as const, label: 'Haute' },
2960
+ { value: 'URGENT' as const, label: 'Urgente' },
2961
+ ].map((option) => {
2962
+ const isActive = taskDetailData.priority === option.value;
2963
+ return (
2964
+ <button
2965
+ key={option.value}
2966
+ type="button"
2967
+ onClick={() =>
2968
+ setTaskDetailData({
2969
+ ...taskDetailData,
2970
+ priority: option.value,
2971
+ })
2972
+ }
2973
+ className={cn(
2974
+ 'cursor-pointer rounded-xl border px-3 py-2 text-xs font-medium transition-colors sm:text-sm',
2975
+ isActive
2976
+ ? 'border-indigo-500 bg-indigo-50 text-indigo-700'
2977
+ : 'border-gray-200 bg-white text-gray-700 hover:border-indigo-200 hover:bg-indigo-50/60',
2978
+ )}
2979
+ >
2980
+ {option.label}
2981
+ </button>
2982
+ );
2983
+ })}
2984
+ </div>
2985
+ </div>
2986
+ </div>
2987
+
2988
+ {/* Description */}
2989
+ <div className="space-y-2">
2990
+ <label className="block text-sm font-medium text-gray-700">Description *</label>
2991
+ <textarea
2992
+ value={taskDetailData.description}
2993
+ onChange={(e) =>
2994
+ setTaskDetailData({
2995
+ ...taskDetailData,
2996
+ description: e.target.value,
2997
+ })
2998
+ }
2999
+ rows={5}
3000
+ className="mt-1 block w-full rounded-xl border border-gray-300 px-4 py-2.5 text-sm text-gray-900 shadow-sm focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
3001
+ placeholder="Ajoutez les détails de la tâche..."
3002
+ />
3003
+ </div>
3004
+
3005
+ {taskDetailError && (
3006
+ <div className="rounded-lg bg-red-50 p-4 text-sm text-red-600">
3007
+ {taskDetailError}
3008
+ </div>
3009
+ )}
3010
+ </form>
3011
+
3012
+ {/* Pied de modal */}
3013
+ <div className="shrink-0 border-t border-gray-100 pt-4">
3014
+ <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
3015
+ <button
3016
+ type="button"
3017
+ onClick={handleDeleteTask}
3018
+ disabled={taskDeleteLoading}
3019
+ className="inline-flex w-full items-center justify-center rounded-xl border border-red-200 bg-red-50 px-4 py-2.5 text-sm font-medium text-red-700 shadow-sm transition-colors hover:bg-red-100 disabled:cursor-not-allowed disabled:opacity-60 sm:w-auto"
3020
+ >
3021
+ {taskDeleteLoading ? 'Suppression...' : 'Supprimer la tâche'}
3022
+ </button>
3023
+ <div className="flex flex-col gap-3 sm:flex-row sm:justify-end">
3024
+ <button
3025
+ type="button"
3026
+ onClick={() => {
3027
+ setShowTaskDetailModal(false);
3028
+ setSelectedTask(null);
3029
+ setTaskDetailError('');
3030
+ }}
3031
+ className="w-full cursor-pointer rounded-xl border border-gray-300 bg-white px-4 py-2.5 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-50 sm:w-auto"
3032
+ >
3033
+ Fermer
3034
+ </button>
3035
+ <button
3036
+ type="submit"
3037
+ form="task-detail-form"
3038
+ disabled={taskDetailLoading}
3039
+ className="w-full cursor-pointer rounded-xl bg-indigo-600 px-4 py-2.5 text-sm font-medium text-white shadow-sm transition-colors hover:bg-indigo-700 focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto"
3040
+ >
3041
+ {taskDetailLoading ? 'Enregistrement...' : 'Enregistrer les modifications'}
3042
+ </button>
3043
+ </div>
3044
+ </div>
3045
+ </div>
3046
+ </div>
3047
+ </div>
3048
+ )}
3049
+ </div>
3050
+ );
3051
+ }