create-crm-tmp 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. package/bin/create-crm-tmp.js +56 -35
  2. package/package.json +1 -1
  3. package/template/README.md +230 -115
  4. package/template/eslint.config.mjs +13 -0
  5. package/template/next.config.ts +14 -0
  6. package/template/package.json +15 -2
  7. package/template/prisma/migrations/20260318095700_init_db/migration.sql +978 -0
  8. package/template/prisma/migrations/migration_lock.toml +3 -0
  9. package/template/prisma/schema.prisma +132 -637
  10. package/template/src/app/(auth)/invite/[token]/page.tsx +10 -8
  11. package/template/src/app/(auth)/layout.tsx +1 -1
  12. package/template/src/app/(auth)/reset-password/complete/page.tsx +11 -8
  13. package/template/src/app/(auth)/reset-password/page.tsx +4 -4
  14. package/template/src/app/(auth)/reset-password/verify/page.tsx +4 -4
  15. package/template/src/app/(auth)/signin/page.tsx +14 -6
  16. package/template/src/app/(dashboard)/agenda/page.tsx +2243 -988
  17. package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +18 -104
  18. package/template/src/app/(dashboard)/automatisation/page.tsx +10 -26
  19. package/template/src/app/(dashboard)/closing/page.tsx +78 -62
  20. package/template/src/app/(dashboard)/contacts/[id]/page.tsx +2082 -1080
  21. package/template/src/app/(dashboard)/contacts/companies/[id]/page.tsx +46 -47
  22. package/template/src/app/(dashboard)/contacts/page.tsx +1062 -780
  23. package/template/src/app/(dashboard)/dashboard/page.tsx +533 -37
  24. package/template/src/app/(dashboard)/dev/page.tsx +1291 -0
  25. package/template/src/app/(dashboard)/layout.tsx +6 -2
  26. package/template/src/app/(dashboard)/settings/page.tsx +797 -2582
  27. package/template/src/app/(dashboard)/templates/page.tsx +55 -54
  28. package/template/src/app/(dashboard)/users/list/page.tsx +51 -48
  29. package/template/src/app/(dashboard)/users/page.tsx +1 -1
  30. package/template/src/app/(dashboard)/users/permissions/page.tsx +2 -2
  31. package/template/src/app/(dashboard)/users/roles/page.tsx +7 -5
  32. package/template/src/app/api/agenda/google-events/route.ts +92 -0
  33. package/template/src/app/api/auth/check-active/route.ts +3 -2
  34. package/template/src/app/api/auth/google/route.ts +2 -1
  35. package/template/src/app/api/auth/google/status/route.ts +7 -31
  36. package/template/src/app/api/companies/[id]/activities/route.ts +1 -3
  37. package/template/src/app/api/companies/[id]/route.ts +1 -2
  38. package/template/src/app/api/companies/route.ts +42 -12
  39. package/template/src/app/api/contacts/[id]/files/[fileId]/preview/route.ts +9 -31
  40. package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +14 -32
  41. package/template/src/app/api/contacts/[id]/files/route.ts +112 -212
  42. package/template/src/app/api/contacts/[id]/interactions/[interactionId]/route.ts +27 -1
  43. package/template/src/app/api/contacts/[id]/interactions/route.ts +16 -16
  44. package/template/src/app/api/contacts/[id]/kyc/route.ts +21 -11
  45. package/template/src/app/api/contacts/[id]/meet/route.ts +19 -2
  46. package/template/src/app/api/contacts/[id]/route.ts +106 -34
  47. package/template/src/app/api/contacts/[id]/send-email/route.ts +27 -11
  48. package/template/src/app/api/contacts/[id]/workflows/run/route.ts +6 -0
  49. package/template/src/app/api/contacts/export/route.ts +9 -13
  50. package/template/src/app/api/contacts/import/route.ts +55 -25
  51. package/template/src/app/api/contacts/import-preview/route.ts +1 -1
  52. package/template/src/app/api/contacts/origins/route.ts +63 -0
  53. package/template/src/app/api/contacts/route.ts +153 -41
  54. package/template/src/app/api/cron/cleanup-editor-images/route.ts +166 -0
  55. package/template/src/app/api/dashboard/widgets/[id]/route.ts +44 -0
  56. package/template/src/app/api/dashboard/widgets/route.ts +181 -0
  57. package/template/src/app/api/dev/reminders/test/route.ts +114 -0
  58. package/template/src/app/api/editor/upload-image/route.ts +61 -0
  59. package/template/src/app/api/integrations/google-sheet/jobs/[jobId]/route.ts +47 -0
  60. package/template/src/app/api/integrations/google-sheet/jobs/usage/route.ts +50 -0
  61. package/template/src/app/api/integrations/google-sheet/sync/route.ts +24 -556
  62. package/template/src/app/api/jobs/google-sheet/process/route.ts +84 -0
  63. package/template/src/app/api/jobs/google-sheet/schedule/route.ts +50 -0
  64. package/template/src/app/api/reminders/clear/route.ts +120 -0
  65. package/template/src/app/api/reminders/clear/undo/route.ts +112 -0
  66. package/template/src/app/api/reminders/route.ts +164 -39
  67. package/template/src/app/api/reminders/state/route.ts +164 -0
  68. package/template/src/app/api/reset-password/request/route.ts +1 -1
  69. package/template/src/app/api/reset-password/verify/route.ts +1 -1
  70. package/template/src/app/api/send/route.ts +16 -4
  71. package/template/src/app/api/settings/google-ads/route.ts +14 -0
  72. package/template/src/app/api/settings/google-calendar/calendars/route.ts +97 -0
  73. package/template/src/app/api/settings/google-calendar/route.ts +124 -0
  74. package/template/src/app/api/settings/google-sheet/[id]/route.ts +28 -0
  75. package/template/src/app/api/settings/google-sheet/auto-map/route.ts +37 -4
  76. package/template/src/app/api/settings/google-sheet/preview/route.ts +9 -3
  77. package/template/src/app/api/settings/google-sheet/route.ts +14 -0
  78. package/template/src/app/api/settings/integrations/logs/route.ts +93 -0
  79. package/template/src/app/api/settings/integrations/notifications/route.ts +67 -0
  80. package/template/src/app/api/settings/meta-leads/[id]/route.ts +0 -1
  81. package/template/src/app/api/settings/meta-leads/route.ts +14 -2
  82. package/template/src/app/api/settings/smtp/route.ts +53 -6
  83. package/template/src/app/api/tasks/[id]/attendees/route.ts +24 -8
  84. package/template/src/app/api/tasks/[id]/route.ts +234 -58
  85. package/template/src/app/api/tasks/meet/route.ts +27 -19
  86. package/template/src/app/api/tasks/route.ts +62 -17
  87. package/template/src/app/api/users/[id]/route.ts +20 -14
  88. package/template/src/app/api/users/list/route.ts +57 -19
  89. package/template/src/app/api/webhooks/google-ads/route.ts +34 -14
  90. package/template/src/app/api/webhooks/meta-leads/route.ts +32 -12
  91. package/template/src/app/api/workflows/[id]/route.ts +0 -4
  92. package/template/src/app/api/workflows/process/route.ts +22 -51
  93. package/template/src/app/api/workflows/route.ts +0 -4
  94. package/template/src/app/globals.css +342 -4
  95. package/template/src/app/layout.tsx +11 -3
  96. package/template/src/app/page.tsx +1 -1
  97. package/template/src/components/address-autocomplete.tsx +7 -6
  98. package/template/src/components/config-error-alert.tsx +46 -0
  99. package/template/src/components/contacts/filter-bar.tsx +12 -3
  100. package/template/src/components/contacts/filter-builder.tsx +28 -43
  101. package/template/src/components/contacts/save-view-dialog.tsx +1 -1
  102. package/template/src/components/contacts/views-tab-bar.tsx +15 -6
  103. package/template/src/components/dashboard/activity-chart.tsx +41 -28
  104. package/template/src/components/dashboard/add-widget-dialog.tsx +157 -0
  105. package/template/src/components/dashboard/color-picker.tsx +64 -0
  106. package/template/src/components/dashboard/contacts-chart.tsx +69 -0
  107. package/template/src/components/dashboard/interactions-by-type-chart.tsx +121 -0
  108. package/template/src/components/dashboard/recent-activity.tsx +154 -0
  109. package/template/src/components/dashboard/stat-card.tsx +40 -40
  110. package/template/src/components/dashboard/status-distribution-chart.tsx +81 -0
  111. package/template/src/components/dashboard/tasks-pie-chart.tsx +37 -34
  112. package/template/src/components/dashboard/top-contacts-list.tsx +113 -0
  113. package/template/src/components/dashboard/upcoming-tasks-list.tsx +72 -81
  114. package/template/src/components/dashboard/widget-wrapper.tsx +36 -0
  115. package/template/src/components/date-picker.tsx +9 -6
  116. package/template/src/components/editor/upload-editor-image.ts +42 -0
  117. package/template/src/components/editor.tsx +161 -22
  118. package/template/src/components/email-template.tsx +2 -2
  119. package/template/src/components/global-search.tsx +30 -28
  120. package/template/src/components/header.tsx +178 -80
  121. package/template/src/components/inactive-account-guard.tsx +58 -0
  122. package/template/src/components/integration-notifications-listener.tsx +12 -0
  123. package/template/src/components/invitation-email-template.tsx +2 -2
  124. package/template/src/components/meet-cancellation-email-template.tsx +3 -3
  125. package/template/src/components/meet-confirmation-email-template.tsx +3 -3
  126. package/template/src/components/meet-update-email-template.tsx +3 -3
  127. package/template/src/components/page-header.tsx +5 -5
  128. package/template/src/components/protected-page.tsx +1 -1
  129. package/template/src/components/reset-password-email-template.tsx +2 -2
  130. package/template/src/components/settings/integrations/GoogleAdsIntegration.tsx +428 -0
  131. package/template/src/components/settings/integrations/GoogleSheetConfigMonitoringModal.tsx +680 -0
  132. package/template/src/components/settings/integrations/GoogleSheetIntegration.tsx +809 -0
  133. package/template/src/components/settings/integrations/ImportResultDialog.tsx +124 -0
  134. package/template/src/components/settings/integrations/IntegrationLogPanel.tsx +57 -0
  135. package/template/src/components/settings/integrations/IntegrationLogsTable.tsx +186 -0
  136. package/template/src/components/settings/integrations/MetaLeadIntegration.tsx +451 -0
  137. package/template/src/components/sidebar.tsx +45 -26
  138. package/template/src/components/skeleton.tsx +40 -43
  139. package/template/src/components/ui/accordion.tsx +2 -2
  140. package/template/src/components/ui/alert-dialog.tsx +1 -1
  141. package/template/src/components/ui/button.tsx +20 -9
  142. package/template/src/components/ui/components.tsx +1 -1
  143. package/template/src/components/ui/date-picker.tsx +422 -0
  144. package/template/src/components/ui/datetime-picker.tsx +338 -0
  145. package/template/src/components/ui/status-select.tsx +271 -0
  146. package/template/src/components/ui/tooltip.tsx +37 -0
  147. package/template/src/components/view-as-modal.tsx +13 -7
  148. package/template/src/contexts/app-toast-context.tsx +245 -57
  149. package/template/src/contexts/dashboard-theme-context.tsx +53 -0
  150. package/template/src/contexts/sidebar-context.tsx +22 -17
  151. package/template/src/contexts/task-reminder-context.tsx +134 -160
  152. package/template/src/contexts/view-as-context.tsx +33 -6
  153. package/template/src/hooks/use-focus-trap.ts +2 -2
  154. package/template/src/hooks/useIntegrationNotifications.ts +49 -0
  155. package/template/src/lib/auth.ts +8 -1
  156. package/template/src/lib/config-links.ts +14 -0
  157. package/template/src/lib/contact-duplicate.ts +79 -61
  158. package/template/src/lib/contact-interactions.ts +21 -21
  159. package/template/src/lib/contact-view-filters.ts +24 -64
  160. package/template/src/lib/contacts-list-url.ts +190 -0
  161. package/template/src/lib/dashboard-stats.ts +65 -7
  162. package/template/src/lib/dashboard-themes.ts +135 -0
  163. package/template/src/lib/date-utils.ts +127 -0
  164. package/template/src/lib/default-widgets.ts +12 -0
  165. package/template/src/lib/editor-html-image-dimensions.ts +172 -0
  166. package/template/src/lib/editor-image-limits.ts +19 -0
  167. package/template/src/lib/email-html-sanitize.ts +19 -0
  168. package/template/src/lib/encryption.ts +9 -6
  169. package/template/src/lib/fr-geography.ts +192 -0
  170. package/template/src/lib/google-calendar-agenda.ts +201 -0
  171. package/template/src/lib/google-calendar.ts +255 -5
  172. package/template/src/lib/google-sheet-sync-jobs.ts +96 -0
  173. package/template/src/lib/google-sheet-sync-runner.ts +514 -0
  174. package/template/src/lib/integration-import-log.ts +21 -0
  175. package/template/src/lib/permissions.ts +40 -10
  176. package/template/src/lib/prisma.ts +4 -1
  177. package/template/src/lib/qstash.ts +65 -0
  178. package/template/src/lib/reminder-state-server.ts +80 -0
  179. package/template/src/lib/reminder-state.ts +29 -0
  180. package/template/src/lib/supabase-storage.ts +113 -0
  181. package/template/src/lib/template-variables.ts +164 -23
  182. package/template/src/lib/utils.ts +45 -0
  183. package/template/src/lib/widget-registry.ts +173 -0
  184. package/template/src/lib/workflow-executor.ts +16 -70
  185. package/template/src/proxy.ts +1 -0
  186. package/template/vercel.json +3 -10
  187. package/template/skills-lock.json +0 -25
  188. package/template/src/components/dashboard/dashboard-content.tsx +0 -79
  189. package/template/src/lib/google-drive.ts +0 -1101
  190. package/template/src/types/yousign.ts +0 -52
@@ -1,14 +1,15 @@
1
1
  'use client';
2
2
 
3
- import { useState, useEffect, useRef, useMemo, useCallback } from 'react';
3
+ import { useState, useEffect, useRef, useMemo, useCallback, type ReactElement } from 'react';
4
4
  import { useUserRole } from '@/hooks/use-user-role';
5
- import { cn } from '@/lib/utils';
5
+ import { cn, devToast } from '@/lib/utils';
6
6
  import {
7
7
  Calendar,
8
8
  Clock,
9
9
  User,
10
10
  CheckCircle2,
11
11
  Circle,
12
+ Loader2,
12
13
  ChevronLeft,
13
14
  ChevronRight,
14
15
  Video,
@@ -23,18 +24,62 @@ import {
23
24
  } from 'lucide-react';
24
25
  import { LazyEditor as Editor, type DefaultTemplateRef } from '@/components/lazy-editor';
25
26
  import Link from 'next/link';
26
- import {
27
- AgendaMonthSkeleton,
28
- AgendaWeekSkeleton,
29
- AgendaDaySkeleton,
30
- } from '@/components/skeleton';
27
+ import { AgendaMonthSkeleton, AgendaWeekSkeleton, AgendaDaySkeleton } from '@/components/skeleton';
31
28
  import { useSession } from '@/lib/auth-client';
32
29
  import { ProtectedPage } from '@/components/protected-page';
33
30
  import { useConfirm } from '@/hooks/use-confirm';
34
31
  import { useAppToast } from '@/contexts/app-toast-context';
35
32
  import { useFetch } from '@/hooks/use-fetch';
33
+ import {
34
+ Tooltip,
35
+ TooltipContent,
36
+ TooltipProvider,
37
+ TooltipTrigger,
38
+ } from '@/components/ui/tooltip';
39
+ import { DateTimePicker } from '@/components/ui/datetime-picker';
40
+ import { ConfigErrorAlert } from '@/components/config-error-alert';
41
+ import { CONFIG_LINKS } from '@/lib/config-links';
42
+ import { requestRemindersRefresh } from '@/lib/reminder-state';
43
+ import { scheduledAtPayloadToIso, toLocalDateTimeInput } from '@/lib/date-utils';
44
+ import {
45
+ googleAgendaEventCardChromeStyle,
46
+ googleAgendaEventChromeStyle,
47
+ googleAgendaEventLocalTimeRange,
48
+ googleCalendarsForEventTargetPicker,
49
+ localCalendarDateKey,
50
+ normalizeAgendaGoogleEventColor,
51
+ } from '@/lib/google-calendar-agenda';
36
52
  import useSWR from 'swr';
37
53
 
54
+ interface GoogleAgendaEventRow {
55
+ googleEventId: string;
56
+ calendarId: string;
57
+ title: string;
58
+ start: string;
59
+ end: string;
60
+ htmlLink?: string | null;
61
+ allDay: boolean;
62
+ allDayStartDate?: string;
63
+ allDayEndExclusive?: string;
64
+ }
65
+
66
+ /** Titre affiché sur la barre fusionnée (semaine) : horaires début–fin si événement horaire. */
67
+ function googleWeekMergedBarLabel(ev: GoogleAgendaEventRow): string {
68
+ if (ev.allDay) return ev.title;
69
+ const { start, end } = googleAgendaEventLocalTimeRange(ev);
70
+ const timeFmt: Intl.DateTimeFormatOptions = { hour: '2-digit', minute: '2-digit' };
71
+ const dayFmt: Intl.DateTimeFormatOptions = { day: '2-digit', month: '2-digit' };
72
+ const t0 = start.toLocaleTimeString('fr-FR', timeFmt);
73
+ const t1 = end.toLocaleTimeString('fr-FR', timeFmt);
74
+ const d0 = localCalendarDateKey(start);
75
+ const d1 = localCalendarDateKey(end);
76
+ const range =
77
+ d0 === d1
78
+ ? `${t0} – ${t1}`
79
+ : `${start.toLocaleDateString('fr-FR', dayFmt)} ${t0} – ${end.toLocaleDateString('fr-FR', dayFmt)} ${t1}`;
80
+ return `${ev.title} · ${range}`;
81
+ }
82
+
38
83
  interface Task {
39
84
  id: string;
40
85
  type: 'CALL' | 'MEETING' | 'EMAIL' | 'OTHER' | 'VIDEO_CONFERENCE' | 'TASK';
@@ -44,6 +89,8 @@ interface Task {
44
89
  completed: boolean;
45
90
  reminderMinutesBefore?: number | null;
46
91
  googleMeetLink?: string | null;
92
+ googleEventId?: string | null;
93
+ googleCalendarId?: string | null;
47
94
  durationMinutes?: number | null;
48
95
  internalNote?: string | null;
49
96
  contact: {
@@ -55,19 +102,49 @@ interface Task {
55
102
  id: string;
56
103
  name: string;
57
104
  };
105
+ createdBy?: {
106
+ id: string;
107
+ name: string | null;
108
+ } | null;
58
109
  }
59
110
 
60
-
61
111
  // Couleurs par type d'événement
62
- const TYPE_COLORS = {
63
- CALL: '#8B5CF6', // Violet pour les appels
64
- EMAIL: '#6366F1', // Indigo pour les emails
65
- OTHER: '#64748B', // Gris pour autres tâches
66
- TASK: '#6366F1', // Indigo pour les tâches
67
- MEETING: '#3B82F6', // Bleu pour les rendez-vous physiques
68
- VIDEO_CONFERENCE: '#10B981', // Vert pour Google Meet
112
+ const TYPE_COLORS: Record<string, { bg: string; border: string; text: string; icon: string }> = {
113
+ CALL: { bg: '#EDE9FE', border: '#8B5CF6', text: '#5B21B6', icon: '#7C3AED' },
114
+ EMAIL: { bg: '#E0E7FF', border: '#6366F1', text: '#3730A3', icon: '#4F46E5' },
115
+ OTHER: { bg: '#F1F5F9', border: '#94A3B8', text: '#334155', icon: '#64748B' },
116
+ TASK: { bg: '#DBEAFE', border: '#3B82F6', text: '#1E3A8A', icon: '#2563EB' },
117
+ MEETING: { bg: '#FEF3C7', border: '#F59E0B', text: '#92400E', icon: '#D97706' },
118
+ VIDEO_CONFERENCE: { bg: '#D1FAE5', border: '#10B981', text: '#064E3B', icon: '#059669' },
119
+ PHYSICAL_APPOINTMENT: { bg: '#FFE4E6', border: '#F43F5E', text: '#881337', icon: '#E11D48' },
69
120
  };
70
121
 
122
+ const DEFAULT_EVENT_COLOR = { bg: '#E0E7FF', border: '#6366F1', text: '#3730A3', icon: '#4F46E5' };
123
+
124
+ /** Palette distincte pour identifier les utilisateurs quand « Voir les autres utilisateurs » est actif */
125
+ const USER_COLORS: Array<{ bg: string; border: string; text: string; icon: string }> = [
126
+ { bg: '#DBEAFE', border: '#2563EB', text: '#1E3A8A', icon: '#1D4ED8' },
127
+ { bg: '#FEF3C7', border: '#D97706', text: '#92400E', icon: '#B45309' },
128
+ { bg: '#D1FAE5', border: '#059669', text: '#064E3B', icon: '#047857' },
129
+ { bg: '#EDE9FE', border: '#7C3AED', text: '#5B21B6', icon: '#6D28D9' },
130
+ { bg: '#FFE4E6', border: '#E11D48', text: '#881337', icon: '#BE123C' },
131
+ { bg: '#E0E7FF', border: '#4F46E5', text: '#3730A3', icon: '#4338CA' },
132
+ { bg: '#FCE7F3', border: '#DB2777', text: '#831843', icon: '#BE185D' },
133
+ { bg: '#CCFBF1', border: '#0D9488', text: '#134E4A', icon: '#0F766E' },
134
+ { bg: '#FFEDD5', border: '#EA580C', text: '#9A3412', icon: '#C2410C' },
135
+ { bg: '#E4E4E7', border: '#52525B', text: '#27272A', icon: '#3F3F46' },
136
+ { bg: '#FDE68A', border: '#F59E42', text: '#854D0E', icon: '#D97706' },
137
+ { bg: '#C7D2FE', border: '#6366F1', text: '#1E40AF', icon: '#3B82F6' },
138
+ { bg: '#BBF7D0', border: '#22C55E', text: '#166534', icon: '#16A34A' },
139
+ { bg: '#FDBA74', border: '#EA580C', text: '#9A3412', icon: '#C2410C' },
140
+ { bg: '#FECACA', border: '#DC2626', text: '#7F1D1D', icon: '#EF4444' },
141
+ { bg: '#F3E8FF', border: '#A21CAF', text: '#581C87', icon: '#C026D3' },
142
+ { bg: '#CCFBF1', border: '#14B8A6', text: '#115E59', icon: '#0E7490' },
143
+ { bg: '#FAE8FF', border: '#D946EF', text: '#701A75', icon: '#A21CAF' },
144
+ { bg: '#DCFCE7', border: '#22D3EE', text: '#0E7490', icon: '#0891B2' },
145
+ { bg: '#F1F5F9', border: '#64748B', text: '#334155', icon: '#64748B' },
146
+ ];
147
+
71
148
  const TASK_TYPE_LABELS = {
72
149
  CALL: 'Appel téléphonique',
73
150
  MEETING: 'RDV',
@@ -79,10 +156,177 @@ const TASK_TYPE_LABELS = {
79
156
 
80
157
  const HOURS = Array.from({ length: 17 }, (_, i) => i + 7); // 7h à minuit (0h)
81
158
 
82
- // Fonction pour obtenir la couleur d'un événement basée sur son type
83
- const getEventColor = (task: Task): string => {
84
- return TYPE_COLORS[task.type] || '#6366F1';
85
- };
159
+ /** Infobulle pour créneaux étroits (colonnes qui se chevauchent) rendu en portail pour éviter overflow. */
160
+ function WeekAgendaOverlapTooltip({
161
+ show,
162
+ title,
163
+ description,
164
+ footer,
165
+ children,
166
+ }: {
167
+ show: boolean;
168
+ title: string;
169
+ description?: string;
170
+ /** Troisième ligne (ex. « Par … »). */
171
+ footer?: string;
172
+ children: ReactElement;
173
+ }) {
174
+ if (!show) return children;
175
+ const detailCls =
176
+ 'text-popover-foreground/80 mt-1 text-[11px] leading-snug';
177
+ return (
178
+ <Tooltip>
179
+ <TooltipTrigger asChild>{children}</TooltipTrigger>
180
+ <TooltipContent side="top" align="start">
181
+ <p className="text-popover-foreground font-semibold leading-snug">{title}</p>
182
+ {description ? <p className={detailCls}>{description}</p> : null}
183
+ {footer ? <p className={detailCls}>{footer}</p> : null}
184
+ </TooltipContent>
185
+ </Tooltip>
186
+ );
187
+ }
188
+
189
+ interface EventLayout {
190
+ task: Task;
191
+ startMinutes: number;
192
+ endMinutes: number;
193
+ column: number;
194
+ totalColumns: number;
195
+ }
196
+
197
+ type WeekAgendaLayoutPayload =
198
+ | { kind: 'task'; task: Task }
199
+ | { kind: 'google'; event: GoogleAgendaEventRow };
200
+
201
+ interface CombinedEventLayout {
202
+ payload: WeekAgendaLayoutPayload;
203
+ startMinutes: number;
204
+ endMinutes: number;
205
+ column: number;
206
+ totalColumns: number;
207
+ }
208
+
209
+ function layoutOverlappingBySchedule(
210
+ entries: Array<{ id: string; payload: WeekAgendaLayoutPayload; startDate: Date; endDate: Date }>,
211
+ day: Date,
212
+ gridStartHour: number,
213
+ gridEndHour: number,
214
+ ): CombinedEventLayout[] {
215
+ const dayStart = new Date(day);
216
+ dayStart.setHours(0, 0, 0, 0);
217
+ const gridStart = new Date(day);
218
+ gridStart.setHours(gridStartHour, 0, 0, 0);
219
+ const gridEnd = new Date(day);
220
+ gridEnd.setHours(gridEndHour, 0, 0, 0);
221
+
222
+ const events = entries
223
+ .map((entry) => {
224
+ const taskStart = entry.startDate;
225
+ const taskEnd = entry.endDate;
226
+
227
+ let effStart = taskStart < dayStart ? dayStart : taskStart;
228
+ effStart = effStart < gridStart ? gridStart : effStart;
229
+ const effEnd = taskEnd > gridEnd ? gridEnd : taskEnd;
230
+
231
+ const startMin = effStart.getHours() * 60 + effStart.getMinutes();
232
+ const endMin = Math.max(startMin + 15, effEnd.getHours() * 60 + effEnd.getMinutes());
233
+
234
+ return { id: entry.id, payload: entry.payload, startMin, endMin };
235
+ })
236
+ .sort((a, b) => a.startMin - b.startMin || b.endMin - a.endMin);
237
+
238
+ const columns: typeof events[] = [];
239
+
240
+ for (const ev of events) {
241
+ let placed = false;
242
+ for (let c = 0; c < columns.length; c++) {
243
+ const lastInCol = columns[c][columns[c].length - 1];
244
+ if (lastInCol.endMin <= ev.startMin) {
245
+ columns[c].push(ev);
246
+ placed = true;
247
+ break;
248
+ }
249
+ }
250
+ if (!placed) {
251
+ columns.push([ev]);
252
+ }
253
+ }
254
+
255
+ const colIndex = new Map<string, number>();
256
+ columns.forEach((col, idx) => {
257
+ for (const ev of col) colIndex.set(ev.id, idx);
258
+ });
259
+
260
+ const groups: { id: string; payload: WeekAgendaLayoutPayload; startMin: number; endMin: number; column: number }[][] =
261
+ [];
262
+ const assigned = new Set<string>();
263
+
264
+ for (const ev of events) {
265
+ if (assigned.has(ev.id)) continue;
266
+
267
+ const group: typeof events = [ev];
268
+ assigned.add(ev.id);
269
+
270
+ let groupEnd = ev.endMin;
271
+ for (const other of events) {
272
+ if (assigned.has(other.id)) continue;
273
+ if (other.startMin < groupEnd) {
274
+ group.push(other);
275
+ assigned.add(other.id);
276
+ groupEnd = Math.max(groupEnd, other.endMin);
277
+ }
278
+ }
279
+
280
+ groups.push(group.map((e) => ({ ...e, column: colIndex.get(e.id) ?? 0 })));
281
+ }
282
+
283
+ const result: CombinedEventLayout[] = [];
284
+ for (const group of groups) {
285
+ const maxCol = Math.max(...group.map((e) => e.column)) + 1;
286
+ for (const e of group) {
287
+ result.push({
288
+ payload: e.payload,
289
+ startMinutes: e.startMin,
290
+ endMinutes: e.endMin,
291
+ column: e.column,
292
+ totalColumns: maxCol,
293
+ });
294
+ }
295
+ }
296
+
297
+ return result;
298
+ }
299
+
300
+ function layoutOverlappingEvents(
301
+ tasks: Task[],
302
+ day: Date,
303
+ gridStartHour: number,
304
+ gridEndHour: number,
305
+ ): EventLayout[] {
306
+ const entries = tasks.map((task) => {
307
+ const taskStart = new Date(task.scheduledAt);
308
+ const duration = task.durationMinutes || 30;
309
+ const taskEnd = new Date(taskStart.getTime() + duration * 60000);
310
+ return {
311
+ id: task.id,
312
+ payload: { kind: 'task' as const, task },
313
+ startDate: taskStart,
314
+ endDate: taskEnd,
315
+ };
316
+ });
317
+ return layoutOverlappingBySchedule(entries, day, gridStartHour, gridEndHour).map((x) => {
318
+ if (x.payload.kind !== 'task') {
319
+ throw new Error('layoutOverlappingEvents: tâche attendue');
320
+ }
321
+ return {
322
+ task: x.payload.task,
323
+ startMinutes: x.startMinutes,
324
+ endMinutes: x.endMinutes,
325
+ column: x.column,
326
+ totalColumns: x.totalColumns,
327
+ };
328
+ });
329
+ }
86
330
 
87
331
  const formatHourLabel = (hour: number) => {
88
332
  return new Date(0, 0, 0, hour).toLocaleTimeString('fr-FR', {
@@ -127,12 +371,29 @@ function getTasksUrl({
127
371
  return `/api/tasks?${sp.toString()}`;
128
372
  }
129
373
 
374
+ type GoogleCalendarOption = { id: string; summary: string; primary?: boolean; accessRole?: string };
375
+
376
+ function pickDefaultCalendarId(
377
+ data: { defaultGoogleCalendarId?: string | null } | undefined,
378
+ writable: GoogleCalendarOption[],
379
+ ): string {
380
+ const pref = data?.defaultGoogleCalendarId;
381
+ if (pref && writable.some((w) => w.id === pref)) return pref;
382
+ const p = writable.find((c) => c.primary);
383
+ return p?.id || writable[0]?.id || 'primary';
384
+ }
385
+
130
386
  export default function AgendaPage() {
131
387
  const { data: session } = useSession();
132
388
  const { isAdmin, hasPermission } = useUserRole();
133
389
  const { confirm, ConfirmDialog } = useConfirm();
134
390
  const toast = useAppToast();
135
391
  const [selectedUserIds, setSelectedUserIds] = useState<Set<string>>(new Set());
392
+ const userChipsScrollRef = useRef<HTMLDivElement>(null);
393
+ const [userChipsScrollEdges, setUserChipsScrollEdges] = useState({
394
+ canScrollPrev: false,
395
+ canScrollNext: false,
396
+ });
136
397
  const [currentDate, setCurrentDate] = useState(new Date());
137
398
  const [view, setView] = useState<'month' | 'week' | 'day'>('week');
138
399
  const [showCreateTaskModal, setShowCreateTaskModal] = useState(false);
@@ -160,6 +421,10 @@ export default function AgendaPage() {
160
421
  });
161
422
  const [showOtherUsersEvents, setShowOtherUsersEvents] = useState(false);
162
423
  const [showFiltersMenu, setShowFiltersMenu] = useState(false);
424
+ /** Listes bas de page : section événements Google Calendar repliable (mois / semaine / jour) */
425
+ const [monthGoogleEventsOpen, setMonthGoogleEventsOpen] = useState(false);
426
+ const [weekGoogleEventsOpen, setWeekGoogleEventsOpen] = useState(false);
427
+ const [dayGoogleEventsOpen, setDayGoogleEventsOpen] = useState(true);
163
428
  const filtersMenuRef = useRef<HTMLDivElement>(null);
164
429
  const [showCreateMeetModal, setShowCreateMeetModal] = useState(false);
165
430
  const [showCreateMeetingModal, setShowCreateMeetingModal] = useState(false);
@@ -178,6 +443,7 @@ export default function AgendaPage() {
178
443
  const meetEditorRef = useRef<DefaultTemplateRef | null>(null);
179
444
  const [creatingMeet, setCreatingMeet] = useState(false);
180
445
  const [meetError, setMeetError] = useState('');
446
+ const [meetErrorConfigLink, setMeetErrorConfigLink] = useState<string | null>(null);
181
447
  const [meetData, setMeetData] = useState<{
182
448
  title: string;
183
449
  description: string;
@@ -188,6 +454,7 @@ export default function AgendaPage() {
188
454
  internalNote: string;
189
455
  contactId: string;
190
456
  addToGoogleCalendar: boolean;
457
+ googleCalendarId: string;
191
458
  }>({
192
459
  title: '',
193
460
  description: '',
@@ -198,6 +465,7 @@ export default function AgendaPage() {
198
465
  internalNote: '',
199
466
  contactId: '',
200
467
  addToGoogleCalendar: true,
468
+ googleCalendarId: '',
201
469
  });
202
470
 
203
471
  // États pour rendez-vous physique
@@ -214,6 +482,7 @@ export default function AgendaPage() {
214
482
  attendees: string[];
215
483
  contactId: string;
216
484
  addToGoogleCalendar: boolean;
485
+ googleCalendarId: string;
217
486
  }>({
218
487
  title: '',
219
488
  description: '',
@@ -224,13 +493,63 @@ export default function AgendaPage() {
224
493
  attendees: [],
225
494
  contactId: '',
226
495
  addToGoogleCalendar: true,
496
+ googleCalendarId: '',
227
497
  });
228
498
 
229
- const { data: googleStatus } = useFetch<{ connected?: boolean }>('/api/auth/google/status');
499
+ const { data: googleStatus } = useFetch<{
500
+ calendar?: { connected?: boolean; email?: string | null };
501
+ }>('/api/auth/google/status');
230
502
  useEffect(() => {
231
- setGoogleConnected(Boolean(googleStatus?.connected));
503
+ setGoogleConnected(Boolean(googleStatus?.calendar?.connected));
232
504
  }, [googleStatus]);
233
505
 
506
+ const googleCalendarsKey = googleConnected ? '/api/settings/google-calendar/calendars' : null;
507
+ const { data: gCalPayload } = useSWR<{
508
+ calendars?: GoogleCalendarOption[];
509
+ defaultGoogleCalendarId?: string | null;
510
+ agendaVisibleGoogleCalendarIds?: string[];
511
+ agendaGoogleEventColor?: string | null;
512
+ error?: string;
513
+ needsGoogleReconnect?: boolean;
514
+ }>(googleCalendarsKey, async (url: string) => {
515
+ const response = await fetch(url);
516
+ return response.json();
517
+ });
518
+
519
+ const writableCalendars = useMemo(
520
+ () =>
521
+ (gCalPayload?.calendars ?? []).filter(
522
+ (c) => c.accessRole === 'owner' || c.accessRole === 'writer',
523
+ ),
524
+ [gCalPayload?.calendars],
525
+ );
526
+
527
+ /** Uniquement les calendriers cochés dans Paramètres → Afficher dans l'agenda (si préférence enregistrée). */
528
+ const targetGoogleCalendars = useMemo(
529
+ () =>
530
+ googleCalendarsForEventTargetPicker(
531
+ writableCalendars,
532
+ gCalPayload?.agendaVisibleGoogleCalendarIds,
533
+ ),
534
+ [writableCalendars, gCalPayload?.agendaVisibleGoogleCalendarIds],
535
+ );
536
+
537
+ const gCalChrome = useMemo(
538
+ () =>
539
+ googleAgendaEventChromeStyle(
540
+ normalizeAgendaGoogleEventColor(gCalPayload?.agendaGoogleEventColor),
541
+ ),
542
+ [gCalPayload?.agendaGoogleEventColor],
543
+ );
544
+
545
+ const gCalCardChrome = useMemo(
546
+ () =>
547
+ googleAgendaEventCardChromeStyle(
548
+ normalizeAgendaGoogleEventColor(gCalPayload?.agendaGoogleEventColor),
549
+ ),
550
+ [gCalPayload?.agendaGoogleEventColor],
551
+ );
552
+
234
553
  // Édition d'un Google Meet
235
554
  const [showEditMeetModal, setShowEditMeetModal] = useState(false);
236
555
  const [editingMeetTask, setEditingMeetTask] = useState<Task | null>(null);
@@ -253,6 +572,7 @@ export default function AgendaPage() {
253
572
  reminderMinutesBefore: number | null;
254
573
  contactId: string;
255
574
  addToGoogleCalendar: boolean;
575
+ googleCalendarId: string;
256
576
  }>({
257
577
  type: 'TASK',
258
578
  title: '',
@@ -261,6 +581,7 @@ export default function AgendaPage() {
261
581
  reminderMinutesBefore: null,
262
582
  contactId: '',
263
583
  addToGoogleCalendar: false,
584
+ googleCalendarId: '',
264
585
  });
265
586
 
266
587
  // Détail / édition d'une tâche générique (non liée à un contact)
@@ -270,18 +591,22 @@ export default function AgendaPage() {
270
591
  const [taskDetailLoading, setTaskDetailLoading] = useState(false);
271
592
  const [taskDeleteLoading, setTaskDeleteLoading] = useState(false);
272
593
  const taskDetailEditorRef = useRef<DefaultTemplateRef | null>(null);
273
- const hasTriggeredGoogleSheetSyncRef = useRef(false);
274
594
  const [taskDetailData, setTaskDetailData] = useState<{
275
595
  title: string;
276
596
  description: string;
277
597
  scheduledAt: string;
278
598
  completed: boolean;
599
+ googleCalendarId: string;
600
+ reminderMinutesBefore: number | null;
279
601
  }>({
280
602
  title: '',
281
603
  description: '',
282
604
  scheduledAt: '',
283
605
  completed: false,
606
+ googleCalendarId: '',
607
+ reminderMinutesBefore: null,
284
608
  });
609
+ const [taskCompleteQuickSaving, setTaskCompleteQuickSaving] = useState(false);
285
610
 
286
611
  const canViewOtherUsersEvents = hasPermission('tasks.view_other_users_events');
287
612
  const { data: contactsData } = useFetch<ContactsResponse>('/api/contacts?limit=1000');
@@ -291,11 +616,73 @@ export default function AgendaPage() {
291
616
  );
292
617
  const users = useMemo(() => usersData ?? [], [usersData]);
293
618
 
619
+ const userColorById = useMemo(() => {
620
+ const map = new Map<string, (typeof USER_COLORS)[number]>();
621
+ users.forEach((u, index) => {
622
+ map.set(u.id, USER_COLORS[index % USER_COLORS.length]);
623
+ });
624
+ return map;
625
+ }, [users]);
626
+
627
+ const getEventColor = useCallback(
628
+ (task: Task) => {
629
+ if (showOtherUsersEvents && task.assignedUser) {
630
+ return userColorById.get(task.assignedUser.id) ?? DEFAULT_EVENT_COLOR;
631
+ }
632
+ return TYPE_COLORS[task.type] || DEFAULT_EVENT_COLOR;
633
+ },
634
+ [showOtherUsersEvents, userColorById],
635
+ );
636
+
637
+ const scrollUserChipsBy = useCallback((direction: 'prev' | 'next') => {
638
+ const el = userChipsScrollRef.current;
639
+ if (!el) return;
640
+ const step = Math.min(280, Math.max(120, el.clientWidth * 0.75));
641
+ el.scrollBy({ left: direction === 'next' ? step : -step, behavior: 'smooth' });
642
+ }, []);
643
+
644
+ /** Défilement horizontal à la souris + mise à jour des flèches (pavé tactile inchangé). */
645
+ useEffect(() => {
646
+ if (!showOtherUsersEvents || users.length === 0) return;
647
+ const el = userChipsScrollRef.current;
648
+ if (!el) return;
649
+
650
+ const updateEdges = () => {
651
+ const { scrollLeft, scrollWidth, clientWidth } = el;
652
+ const maxScroll = scrollWidth - clientWidth;
653
+ setUserChipsScrollEdges({
654
+ canScrollPrev: scrollLeft > 1,
655
+ canScrollNext: maxScroll > 1 && scrollLeft < maxScroll - 1,
656
+ });
657
+ };
658
+
659
+ updateEdges();
660
+ el.addEventListener('scroll', updateEdges, { passive: true });
661
+
662
+ const onWheel = (e: WheelEvent) => {
663
+ if (el.scrollWidth <= el.clientWidth) return;
664
+ if (e.shiftKey) return;
665
+ if (Math.abs(e.deltaY) <= Math.abs(e.deltaX)) return;
666
+ e.preventDefault();
667
+ el.scrollLeft += e.deltaY;
668
+ };
669
+ el.addEventListener('wheel', onWheel, { passive: false });
670
+
671
+ const ro = new ResizeObserver(updateEdges);
672
+ ro.observe(el);
673
+ return () => {
674
+ el.removeEventListener('scroll', updateEdges);
675
+ el.removeEventListener('wheel', onWheel);
676
+ ro.disconnect();
677
+ };
678
+ }, [showOtherUsersEvents, users.length]);
679
+
294
680
  useEffect(() => {
295
681
  if (!meetError) return;
682
+ if (meetErrorConfigLink) return; // Affiché dans la modal via ConfigErrorAlert
296
683
  toast.error(meetError);
297
684
  setMeetError('');
298
- }, [meetError, toast]);
685
+ }, [meetError, meetErrorConfigLink, toast]);
299
686
  useEffect(() => {
300
687
  if (!meetingError) return;
301
688
  toast.error(meetingError);
@@ -322,26 +709,6 @@ export default function AgendaPage() {
322
709
  setSelectedUserIds(new Set(users.map((u) => u.id)));
323
710
  }, [canViewOtherUsersEvents, selectedUserIds.size, users]);
324
711
 
325
- // Synchronisation automatique Google Sheets
326
- useEffect(() => {
327
- if (hasTriggeredGoogleSheetSyncRef.current) {
328
- return;
329
- }
330
- hasTriggeredGoogleSheetSyncRef.current = true;
331
-
332
- const syncGoogleSheet = async () => {
333
- try {
334
- await fetch('/api/integrations/google-sheet/sync', {
335
- method: 'POST',
336
- });
337
- } catch (err) {
338
- console.error('Erreur lors de la synchronisation Google Sheets:', err);
339
- }
340
- };
341
-
342
- syncGoogleSheet();
343
- }, []);
344
-
345
712
  // Fermer le menu de filtres en cliquant en dehors
346
713
  useEffect(() => {
347
714
  const handleClickOutside = (event: MouseEvent) => {
@@ -484,6 +851,173 @@ export default function AgendaPage() {
484
851
  await mutateTasks();
485
852
  };
486
853
 
854
+ const googleEventsKey =
855
+ googleConnected && session
856
+ ? `/api/agenda/google-events?startDate=${encodeURIComponent(dateRange.start.toISOString())}&endDate=${encodeURIComponent(dateRange.end.toISOString())}`
857
+ : null;
858
+ const { data: googleEventsResponse } = useSWR<{ events: GoogleAgendaEventRow[] }>(
859
+ googleEventsKey,
860
+ async (url: string) => {
861
+ const response = await fetch(url);
862
+ if (!response.ok) throw new Error('Erreur événements Google');
863
+ return response.json();
864
+ },
865
+ );
866
+ const rawGoogleEvents = googleEventsResponse?.events ?? [];
867
+
868
+ const standaloneGoogleEvents = useMemo(() => {
869
+ const linked = new Set(
870
+ tasks.map((t) => t.googleEventId).filter((x): x is string => Boolean(x)),
871
+ );
872
+ return rawGoogleEvents.filter((e) => !linked.has(e.googleEventId));
873
+ }, [rawGoogleEvents, tasks]);
874
+
875
+ const googleEventsInDateRange = useMemo(() => {
876
+ const rangeStart = dateRange.start.getTime();
877
+ const rangeEnd = dateRange.end.getTime();
878
+ return standaloneGoogleEvents.filter((ev) => {
879
+ const { start: evS, end: evE } = googleAgendaEventLocalTimeRange(ev);
880
+ return evS.getTime() <= rangeEnd && evE.getTime() >= rangeStart;
881
+ });
882
+ }, [standaloneGoogleEvents, dateRange.start, dateRange.end]);
883
+
884
+ const googleForDayList = useMemo(() => {
885
+ return standaloneGoogleEvents.filter((ev) => {
886
+ if (ev.allDay && ev.allDayStartDate && ev.allDayEndExclusive) {
887
+ const k = localCalendarDateKey(currentDate);
888
+ return ev.allDayStartDate <= k && k < ev.allDayEndExclusive;
889
+ }
890
+ const dayStart = new Date(currentDate);
891
+ dayStart.setHours(0, 0, 0, 0);
892
+ const dayEnd = new Date(currentDate);
893
+ dayEnd.setHours(23, 59, 59, 999);
894
+ const taskStart = new Date(ev.start);
895
+ const taskEnd = new Date(ev.end);
896
+ return taskStart < dayEnd && taskEnd > dayStart;
897
+ });
898
+ }, [standaloneGoogleEvents, currentDate]);
899
+
900
+ const googleForDayListAllDay = useMemo(
901
+ () =>
902
+ [...googleForDayList]
903
+ .filter((ev) => ev.allDay)
904
+ .sort(
905
+ (a, b) =>
906
+ googleAgendaEventLocalTimeRange(a).start.getTime() -
907
+ googleAgendaEventLocalTimeRange(b).start.getTime(),
908
+ ),
909
+ [googleForDayList],
910
+ );
911
+
912
+ const googleForDayListTimed = useMemo(
913
+ () =>
914
+ [...googleForDayList]
915
+ .filter((ev) => !ev.allDay)
916
+ .sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime()),
917
+ [googleForDayList],
918
+ );
919
+
920
+ /** Barres fusionnées en haut de semaine (all-day + événements avec horaire sur plusieurs jours). */
921
+ const weekAllDayGoogleBars = useMemo(() => {
922
+ const anchor = new Date(currentDate);
923
+ const dow = anchor.getDay() === 0 ? 7 : anchor.getDay();
924
+ anchor.setDate(anchor.getDate() - (dow - 1));
925
+ const weekDays: Date[] = [];
926
+ for (let i = 0; i < 7; i++) {
927
+ const d = new Date(anchor);
928
+ d.setDate(anchor.getDate() + i);
929
+ weekDays.push(d);
930
+ }
931
+ const weekFirstKey = localCalendarDateKey(weekDays[0]);
932
+ const weekLastKey = localCalendarDateKey(weekDays[6]);
933
+
934
+ const seen = new Set<string>();
935
+ const candidates: Array<{
936
+ ev: GoogleAgendaEventRow;
937
+ spanStartKey: string;
938
+ spanEndKey: string;
939
+ }> = [];
940
+ for (const ev of standaloneGoogleEvents) {
941
+ const id = `${ev.calendarId}\0${ev.googleEventId}`;
942
+ if (seen.has(id)) continue;
943
+
944
+ let spanStartKey = '';
945
+ let spanEndKey = '';
946
+ if (ev.allDay && ev.allDayStartDate && ev.allDayEndExclusive) {
947
+ // all-day : end exclusif Google => end inclusif local = endExclusive - 1 jour
948
+ spanStartKey = ev.allDayStartDate;
949
+ const [ey, em, ed] = ev.allDayEndExclusive.split('-').map(Number);
950
+ const endExclusiveLocal = new Date(ey, em - 1, ed, 0, 0, 0, 0);
951
+ endExclusiveLocal.setDate(endExclusiveLocal.getDate() - 1);
952
+ spanEndKey = localCalendarDateKey(endExclusiveLocal);
953
+ } else {
954
+ const { start, end } = googleAgendaEventLocalTimeRange(ev);
955
+ const startKey = localCalendarDateKey(start);
956
+ const endKey = localCalendarDateKey(end);
957
+ // Évènement horaire multi-jours : affichage en barre haute comme Google Calendar.
958
+ if (startKey >= endKey) continue;
959
+ spanStartKey = startKey;
960
+ spanEndKey = endKey;
961
+ }
962
+
963
+ if (spanEndKey < weekFirstKey || spanStartKey > weekLastKey) continue;
964
+ seen.add(id);
965
+ candidates.push({ ev, spanStartKey, spanEndKey });
966
+ }
967
+
968
+ if (candidates.length === 0) return null;
969
+
970
+ type SpanItem = { ev: GoogleAgendaEventRow; startIdx: number; endIdx: number };
971
+ const withSpan: SpanItem[] = [];
972
+ for (const item of candidates) {
973
+ let firstIdx = -1;
974
+ let lastIdx = -1;
975
+ for (let i = 0; i < 7; i++) {
976
+ const k = localCalendarDateKey(weekDays[i]);
977
+ if (k >= item.spanStartKey && k <= item.spanEndKey) {
978
+ if (firstIdx < 0) firstIdx = i;
979
+ lastIdx = i;
980
+ }
981
+ }
982
+ if (firstIdx >= 0 && lastIdx >= 0) {
983
+ withSpan.push({ ev: item.ev, startIdx: firstIdx, endIdx: lastIdx });
984
+ }
985
+ }
986
+
987
+ if (withSpan.length === 0) return null;
988
+
989
+ withSpan.sort((a, b) => {
990
+ if (a.startIdx !== b.startIdx) return a.startIdx - b.startIdx;
991
+ return b.endIdx - b.startIdx - (a.endIdx - a.startIdx);
992
+ });
993
+
994
+ const lanes: Array<Array<{ start: number; end: number }>> = [];
995
+ const placed: Array<{
996
+ ev: GoogleAgendaEventRow;
997
+ startIdx: number;
998
+ endIdx: number;
999
+ lane: number;
1000
+ }> = [];
1001
+
1002
+ for (const item of withSpan) {
1003
+ const interval = { start: item.startIdx, end: item.endIdx };
1004
+ let lane = 0;
1005
+ while (true) {
1006
+ if (!lanes[lane]) lanes[lane] = [];
1007
+ const occupied = lanes[lane];
1008
+ const clash = occupied.some((o) => o.start <= interval.end && interval.start <= o.end);
1009
+ if (!clash) {
1010
+ occupied.push(interval);
1011
+ placed.push({ ev: item.ev, startIdx: item.startIdx, endIdx: item.endIdx, lane });
1012
+ break;
1013
+ }
1014
+ lane++;
1015
+ }
1016
+ }
1017
+
1018
+ return { placed, rowCount: lanes.length };
1019
+ }, [standaloneGoogleEvents, currentDate]);
1020
+
487
1021
  // Filtrer les tâches selon les filtres sélectionnés
488
1022
  const filteredTasks = useMemo(() => {
489
1023
  return tasks.filter((task) => {
@@ -578,6 +1112,22 @@ export default function AgendaPage() {
578
1112
  });
579
1113
  };
580
1114
 
1115
+ const getGoogleEventsForDate = (date: Date) => {
1116
+ return standaloneGoogleEvents.filter((ev) => {
1117
+ if (ev.allDay && ev.allDayStartDate && ev.allDayEndExclusive) {
1118
+ const k = localCalendarDateKey(date);
1119
+ return ev.allDayStartDate <= k && k < ev.allDayEndExclusive;
1120
+ }
1121
+ const taskStart = new Date(ev.start);
1122
+ const taskEnd = new Date(ev.end);
1123
+ const dayStart = new Date(date);
1124
+ dayStart.setHours(0, 0, 0, 0);
1125
+ const dayEnd = new Date(date);
1126
+ dayEnd.setHours(23, 59, 59, 999);
1127
+ return taskStart < dayEnd && taskEnd > dayStart;
1128
+ });
1129
+ };
1130
+
581
1131
  const formatTime = (dateString: string) => {
582
1132
  return new Date(dateString).toLocaleTimeString('fr-FR', {
583
1133
  hour: '2-digit',
@@ -600,18 +1150,73 @@ export default function AgendaPage() {
600
1150
  };
601
1151
 
602
1152
  const toggleTaskComplete = async (taskId: string, currentStatus: boolean) => {
1153
+ const next = !currentStatus;
1154
+ await mutateTasks(
1155
+ (prev) => {
1156
+ const list = Array.isArray(prev) ? prev : [];
1157
+ return list.map((t) => (t.id === taskId ? { ...t, completed: next } : t));
1158
+ },
1159
+ { revalidate: false },
1160
+ );
603
1161
  try {
604
1162
  const response = await fetch(`/api/tasks/${taskId}`, {
605
1163
  method: 'PUT',
606
1164
  headers: { 'Content-Type': 'application/json' },
607
- body: JSON.stringify({ completed: !currentStatus }),
1165
+ body: JSON.stringify({ completed: next }),
608
1166
  });
1167
+ if (!response.ok) {
1168
+ const data = await response.json().catch(() => ({}));
1169
+ toast.error(devToast('Impossible de mettre à jour le statut', data.error));
1170
+ await mutateTasks(undefined, { revalidate: true });
1171
+ return;
1172
+ }
1173
+ await mutateTasks(undefined, { revalidate: true });
1174
+ requestRemindersRefresh();
1175
+ } catch (e) {
1176
+ toast.error(devToast('Impossible de se connecter au serveur. Vérifiez votre connexion internet.', e));
1177
+ await mutateTasks(undefined, { revalidate: true });
1178
+ }
1179
+ };
609
1180
 
610
- if (response.ok) {
611
- fetchTasks();
1181
+ const handleAgendaModalQuickSetCompleted = async (nextCompleted: boolean) => {
1182
+ if (!selectedTask || taskCompleteQuickSaving) return;
1183
+ if (taskDetailData.completed === nextCompleted) return;
1184
+
1185
+ setTaskCompleteQuickSaving(true);
1186
+ const prevCompleted = taskDetailData.completed;
1187
+ const taskId = selectedTask.id;
1188
+
1189
+ setTaskDetailData((d) => ({ ...d, completed: nextCompleted }));
1190
+ setSelectedTask((t) => (t ? { ...t, completed: nextCompleted } : null));
1191
+ await mutateTasks(
1192
+ (prev) => {
1193
+ const list = Array.isArray(prev) ? prev : [];
1194
+ return list.map((x) => (x.id === taskId ? { ...x, completed: nextCompleted } : x));
1195
+ },
1196
+ { revalidate: false },
1197
+ );
1198
+
1199
+ try {
1200
+ const response = await fetch(`/api/tasks/${taskId}`, {
1201
+ method: 'PUT',
1202
+ headers: { 'Content-Type': 'application/json' },
1203
+ body: JSON.stringify({ completed: nextCompleted }),
1204
+ });
1205
+ const data = await response.json();
1206
+ if (!response.ok) {
1207
+ throw new Error(data.error || 'Impossible de mettre à jour le statut');
612
1208
  }
613
- } catch (error) {
614
- console.error('Erreur lors de la mise à jour de la tâche:', error);
1209
+ setTaskDetailData((d) => ({ ...d, completed: data.completed }));
1210
+ setSelectedTask((t) => (t && t.id === data.id ? { ...t, ...data } : t));
1211
+ await mutateTasks(undefined, { revalidate: true });
1212
+ requestRemindersRefresh();
1213
+ } catch (e) {
1214
+ setTaskDetailData((d) => ({ ...d, completed: prevCompleted }));
1215
+ setSelectedTask((t) => (t ? { ...t, completed: prevCompleted } : null));
1216
+ await mutateTasks(undefined, { revalidate: true });
1217
+ toast.error(devToast('Impossible de mettre à jour la tâche. Veuillez réessayer.', e));
1218
+ } finally {
1219
+ setTaskCompleteQuickSaving(false);
615
1220
  }
616
1221
  };
617
1222
 
@@ -640,13 +1245,17 @@ export default function AgendaPage() {
640
1245
  type: createTaskData.type,
641
1246
  title: createTaskData.title || null,
642
1247
  description: createTaskData.description,
643
- scheduledAt: createTaskData.scheduledAt,
1248
+ scheduledAt: scheduledAtPayloadToIso(createTaskData.scheduledAt),
644
1249
  contactId: createTaskData.contactId || undefined,
645
1250
  reminderMinutesBefore:
646
1251
  typeof createTaskData.reminderMinutesBefore === 'number'
647
1252
  ? createTaskData.reminderMinutesBefore
648
1253
  : null,
649
1254
  addToGoogleCalendar: createTaskData.addToGoogleCalendar,
1255
+ googleCalendarId:
1256
+ createTaskData.addToGoogleCalendar && createTaskData.googleCalendarId
1257
+ ? createTaskData.googleCalendarId
1258
+ : undefined,
650
1259
  }),
651
1260
  });
652
1261
 
@@ -664,12 +1273,23 @@ export default function AgendaPage() {
664
1273
  reminderMinutesBefore: null,
665
1274
  contactId: '',
666
1275
  addToGoogleCalendar: true,
1276
+ googleCalendarId: '',
667
1277
  });
668
1278
  setTaskContactSearch('');
669
1279
  setTaskContactDropdownOpen(false);
670
- await fetchTasks();
1280
+ await mutateTasks(
1281
+ (prev) => {
1282
+ const list = Array.isArray(prev) ? prev : [];
1283
+ if (list.some((t) => t.id === data.id)) return list;
1284
+ return [...list, data];
1285
+ },
1286
+ { revalidate: true },
1287
+ );
1288
+ requestRemindersRefresh();
671
1289
  } catch (error: unknown) {
672
- setCreateTaskError(error instanceof Error ? error.message : 'Erreur lors de la création de la tâche');
1290
+ setCreateTaskError(
1291
+ devToast('Erreur lors de la création de la tâche', error),
1292
+ );
673
1293
  } finally {
674
1294
  setCreatingTask(false);
675
1295
  }
@@ -705,6 +1325,7 @@ export default function AgendaPage() {
705
1325
  setMeetError(
706
1326
  'Vous devez connecter votre compte Google dans les paramètres avant de pouvoir créer un Google Meet.',
707
1327
  );
1328
+ setMeetErrorConfigLink(CONFIG_LINKS.googleCalendar);
708
1329
  setCreatingMeet(false);
709
1330
  return;
710
1331
  }
@@ -715,23 +1336,32 @@ export default function AgendaPage() {
715
1336
  body: JSON.stringify({
716
1337
  title: meetData.title,
717
1338
  description: htmlContent || '',
718
- scheduledAt: meetData.scheduledAt,
1339
+ scheduledAt: scheduledAtPayloadToIso(meetData.scheduledAt),
719
1340
  durationMinutes: meetData.durationMinutes,
720
1341
  attendees: meetData.attendees.filter((email) => email.trim() !== ''),
721
1342
  reminderMinutesBefore: meetData.reminderMinutesBefore,
722
1343
  internalNote: meetData.internalNote || null,
723
1344
  contactId: meetData.contactId || undefined,
724
1345
  addToGoogleCalendar: meetData.addToGoogleCalendar,
1346
+ googleCalendarId:
1347
+ meetData.addToGoogleCalendar && meetData.googleCalendarId
1348
+ ? meetData.googleCalendarId
1349
+ : undefined,
725
1350
  }),
726
1351
  });
727
1352
 
728
1353
  const data = await response.json();
729
1354
 
730
1355
  if (!response.ok) {
731
- throw new Error(data.error || 'Erreur lors de la création du Google Meet');
1356
+ setMeetError(devToast('Erreur lors de la création du Google Meet', data.error));
1357
+ if (data.configLink) setMeetErrorConfigLink(data.configLink);
1358
+ setCreatingMeet(false);
1359
+ return;
732
1360
  }
733
1361
 
734
1362
  setShowCreateMeetModal(false);
1363
+ setMeetError('');
1364
+ setMeetErrorConfigLink(null);
735
1365
  setMeetData({
736
1366
  title: '',
737
1367
  description: '',
@@ -742,13 +1372,23 @@ export default function AgendaPage() {
742
1372
  internalNote: '',
743
1373
  contactId: '',
744
1374
  addToGoogleCalendar: true,
1375
+ googleCalendarId: '',
745
1376
  });
746
1377
  setMeetContactSearch('');
747
1378
  setMeetContactDropdownOpen(false);
748
1379
  meetEditorRef.current?.injectHTML('');
749
- await fetchTasks();
1380
+ await mutateTasks(
1381
+ (prev) => {
1382
+ const list = Array.isArray(prev) ? prev : [];
1383
+ if (list.some((t) => t.id === data.id)) return list;
1384
+ return [...list, data];
1385
+ },
1386
+ { revalidate: true },
1387
+ );
1388
+ requestRemindersRefresh();
750
1389
  } catch (err: unknown) {
751
- setMeetError(err instanceof Error ? err.message : 'Erreur inconnue');
1390
+ setMeetError(devToast('Erreur lors de la création du Google Meet', err));
1391
+ setMeetErrorConfigLink(null);
752
1392
  } finally {
753
1393
  setCreatingMeet(false);
754
1394
  }
@@ -794,13 +1434,17 @@ export default function AgendaPage() {
794
1434
  type: 'MEETING',
795
1435
  title: meetingData.title || null,
796
1436
  description: htmlContent || '',
797
- scheduledAt: meetingData.scheduledAt,
1437
+ scheduledAt: scheduledAtPayloadToIso(meetingData.scheduledAt),
798
1438
  reminderMinutesBefore: meetingData.reminderMinutesBefore ?? null,
799
1439
  notifyContact: meetingData.notifyContact,
800
1440
  internalNote: meetingData.internalNote || null,
801
1441
  attendees: meetingData.attendees.filter((email) => email.trim() !== ''),
802
1442
  contactId: meetingData.contactId || undefined,
803
1443
  addToGoogleCalendar: meetingData.addToGoogleCalendar,
1444
+ googleCalendarId:
1445
+ meetingData.addToGoogleCalendar && meetingData.googleCalendarId
1446
+ ? meetingData.googleCalendarId
1447
+ : undefined,
804
1448
  }),
805
1449
  });
806
1450
 
@@ -821,13 +1465,22 @@ export default function AgendaPage() {
821
1465
  attendees: [],
822
1466
  contactId: '',
823
1467
  addToGoogleCalendar: true,
1468
+ googleCalendarId: '',
824
1469
  });
825
1470
  setMeetingContactSearch('');
826
1471
  setMeetingContactDropdownOpen(false);
827
1472
  meetingEditorRef.current?.injectHTML('');
828
- await fetchTasks();
1473
+ await mutateTasks(
1474
+ (prev) => {
1475
+ const list = Array.isArray(prev) ? prev : [];
1476
+ if (list.some((t) => t.id === data.id)) return list;
1477
+ return [...list, data];
1478
+ },
1479
+ { revalidate: true },
1480
+ );
1481
+ requestRemindersRefresh();
829
1482
  } catch (err: unknown) {
830
- setMeetingError(err instanceof Error ? err.message : 'Erreur inconnue');
1483
+ setMeetingError(devToast('Erreur lors de la création du rendez-vous', err));
831
1484
  } finally {
832
1485
  setCreatingMeeting(false);
833
1486
  }
@@ -838,8 +1491,10 @@ export default function AgendaPage() {
838
1491
  setTaskDetailData({
839
1492
  title: task.title || '',
840
1493
  description: task.description || '',
841
- scheduledAt: task.scheduledAt,
1494
+ scheduledAt: toLocalDateTimeInput(task.scheduledAt),
842
1495
  completed: task.completed,
1496
+ googleCalendarId: task.googleCalendarId || 'primary',
1497
+ reminderMinutesBefore: task.reminderMinutesBefore ?? null,
843
1498
  });
844
1499
  setTaskDetailError('');
845
1500
  setShowTaskDetailModal(true);
@@ -903,8 +1558,10 @@ export default function AgendaPage() {
903
1558
  body: JSON.stringify({
904
1559
  title: taskDetailData.title || null,
905
1560
  description: htmlContent,
906
- scheduledAt: taskDetailData.scheduledAt,
1561
+ scheduledAt: scheduledAtPayloadToIso(taskDetailData.scheduledAt),
907
1562
  completed: taskDetailData.completed,
1563
+ googleCalendarId: taskDetailData.googleCalendarId || undefined,
1564
+ reminderMinutesBefore: taskDetailData.reminderMinutesBefore ?? null,
908
1565
  }),
909
1566
  });
910
1567
 
@@ -913,11 +1570,23 @@ export default function AgendaPage() {
913
1570
  throw new Error(data.error || 'Erreur lors de la mise à jour de la tâche');
914
1571
  }
915
1572
 
1573
+ const previousCalendarId = selectedTask.googleCalendarId || 'primary';
1574
+ const nextCalendarId = taskDetailData.googleCalendarId || 'primary';
1575
+ if (previousCalendarId !== nextCalendarId) {
1576
+ const selectedCalendar = targetGoogleCalendars.find((c) => c.id === nextCalendarId);
1577
+ toast.success(
1578
+ `Calendrier mis à jour${selectedCalendar ? ` : ${selectedCalendar.summary}` : ''}`,
1579
+ );
1580
+ }
1581
+
916
1582
  setShowTaskDetailModal(false);
917
1583
  setSelectedTask(null);
918
1584
  await fetchTasks();
1585
+ requestRemindersRefresh();
919
1586
  } catch (error: unknown) {
920
- setTaskDetailError(error instanceof Error ? error.message : 'Erreur lors de la mise à jour de la tâche');
1587
+ setTaskDetailError(
1588
+ devToast('Erreur lors de la mise à jour de la tâche', error),
1589
+ );
921
1590
  } finally {
922
1591
  setTaskDetailLoading(false);
923
1592
  }
@@ -954,8 +1623,11 @@ export default function AgendaPage() {
954
1623
  setShowTaskDetailModal(false);
955
1624
  setSelectedTask(null);
956
1625
  await fetchTasks();
1626
+ requestRemindersRefresh();
957
1627
  } catch (error: unknown) {
958
- setTaskDetailError(error instanceof Error ? error.message : 'Erreur lors de la suppression de la tâche');
1628
+ setTaskDetailError(
1629
+ devToast('Erreur lors de la suppression de la tâche', error),
1630
+ );
959
1631
  } finally {
960
1632
  setTaskDeleteLoading(false);
961
1633
  }
@@ -965,7 +1637,7 @@ export default function AgendaPage() {
965
1637
  const scheduled = new Date(task.scheduledAt);
966
1638
  setEditingMeetTask(task);
967
1639
  setEditMeetData({
968
- scheduledAt: scheduled.toISOString(),
1640
+ scheduledAt: toLocalDateTimeInput(scheduled),
969
1641
  durationMinutes: task.durationMinutes ?? 30,
970
1642
  });
971
1643
  setEditMeetError('');
@@ -990,7 +1662,7 @@ export default function AgendaPage() {
990
1662
  method: 'PUT',
991
1663
  headers: { 'Content-Type': 'application/json' },
992
1664
  body: JSON.stringify({
993
- scheduledAt: editMeetData.scheduledAt,
1665
+ scheduledAt: scheduledAtPayloadToIso(editMeetData.scheduledAt),
994
1666
  durationMinutes: editMeetData.durationMinutes,
995
1667
  }),
996
1668
  });
@@ -1004,13 +1676,15 @@ export default function AgendaPage() {
1004
1676
  setEditingMeetTask(null);
1005
1677
  setEditMeetLoading(false);
1006
1678
  await fetchTasks();
1679
+ requestRemindersRefresh();
1007
1680
  } catch (error: unknown) {
1008
- setEditMeetError(error instanceof Error ? error.message : 'Erreur lors de la mise à jour du rendez-vous');
1681
+ setEditMeetError(
1682
+ devToast('Erreur lors de la mise à jour du rendez-vous', error),
1683
+ );
1009
1684
  setEditMeetLoading(false);
1010
1685
  }
1011
1686
  };
1012
1687
 
1013
-
1014
1688
  // Ne pas afficher le skeleton global, on l'affiche par vue
1015
1689
 
1016
1690
  // Fonction pour formater la date pour l'affichage dans le header
@@ -1070,23 +1744,24 @@ export default function AgendaPage() {
1070
1744
 
1071
1745
  return (
1072
1746
  <ProtectedPage requiredPermission={['tasks.view_all', 'tasks.view_own']}>
1747
+ <TooltipProvider delayDuration={400}>
1073
1748
  <div className="kb-tab-scope bg-surface-page flex h-full flex-col">
1074
1749
  {/* Header avec titre et navigation */}
1075
- <div className="border-b border-border bg-background/95 px-4 py-4 backdrop-blur-sm sm:px-6 lg:px-8">
1750
+ <div className="border-border bg-background/95 border-b px-4 py-4 backdrop-blur-sm sm:px-6 lg:px-8">
1076
1751
  {/* Titre et informations de date */}
1077
1752
  <div className="mb-4 flex items-start justify-between gap-4">
1078
1753
  <div className="flex items-start gap-4">
1079
1754
  {/* Date actuelle en grand */}
1080
1755
  <div className="flex flex-col">
1081
- <div className="text-3xl font-bold text-foreground">{formatCurrentDay().day}</div>
1082
- <div className="text-xs font-medium text-muted-foreground uppercase">
1756
+ <div className="text-foreground text-3xl font-bold">{formatCurrentDay().day}</div>
1757
+ <div className="text-muted-foreground text-xs font-medium uppercase">
1083
1758
  {formatCurrentDay().month}
1084
1759
  </div>
1085
1760
  </div>
1086
1761
  <div className="flex flex-col">
1087
- <h1 className="text-2xl font-bold text-foreground">Agenda</h1>
1088
- <p className="mt-1 text-sm text-muted-foreground">{formatCurrentMonthYear()}</p>
1089
- <p className="text-xs text-muted-foreground/80">{formatHeaderDateRange()}</p>
1762
+ <h1 className="text-foreground text-2xl font-bold">Agenda</h1>
1763
+ <p className="text-muted-foreground mt-1 text-sm">{formatCurrentMonthYear()}</p>
1764
+ <p className="text-muted-foreground/80 text-xs">{formatHeaderDateRange()}</p>
1090
1765
  </div>
1091
1766
  </div>
1092
1767
 
@@ -1094,24 +1769,52 @@ export default function AgendaPage() {
1094
1769
  <div className="flex items-center gap-2">
1095
1770
  <button
1096
1771
  type="button"
1097
- onClick={() => setShowCreateMeetModal(true)}
1098
- 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 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-green-500 focus-visible:ring-offset-2 sm:px-4 sm:text-sm"
1772
+ onClick={() => {
1773
+ if (!googleConnected) {
1774
+ toast.errorConfigRequired(
1775
+ 'Connectez votre compte Google dans les paramètres avant de créer un Google Meet.',
1776
+ CONFIG_LINKS.googleCalendar,
1777
+ );
1778
+ return;
1779
+ }
1780
+ setMeetData((d) => ({
1781
+ ...d,
1782
+ googleCalendarId: pickDefaultCalendarId(gCalPayload, targetGoogleCalendars),
1783
+ }));
1784
+ setShowCreateMeetModal(true);
1785
+ }}
1786
+ 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 focus-visible:ring-2 focus-visible:ring-green-500 focus-visible:ring-offset-2 focus-visible:outline-none sm:px-4 sm:text-sm"
1099
1787
  >
1100
1788
  <Video className="h-4 w-4" />
1101
1789
  <span className="hidden sm:inline">Google Meet</span>
1102
1790
  </button>
1103
1791
  <button
1104
1792
  type="button"
1105
- onClick={() => setShowCreateMeetingModal(true)}
1106
- 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 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400/30 focus-visible:ring-offset-2 sm:px-4 sm:text-sm"
1793
+ onClick={() => {
1794
+ setMeetingData((d) => ({
1795
+ ...d,
1796
+ googleCalendarId: pickDefaultCalendarId(gCalPayload, targetGoogleCalendars),
1797
+ }));
1798
+ setShowCreateMeetingModal(true);
1799
+ }}
1800
+ 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 focus-visible:ring-2 focus-visible:ring-gray-400/30 focus-visible:ring-offset-2 focus-visible:outline-none sm:px-4 sm:text-sm"
1107
1801
  >
1108
1802
  <Calendar className="h-4 w-4" />
1109
1803
  <span className="hidden sm:inline">Rendez-vous</span>
1110
1804
  </button>
1111
1805
  <button
1112
1806
  type="button"
1113
- onClick={() => setShowCreateTaskModal(true)}
1114
- className="inline-flex cursor-pointer items-center gap-2 rounded-lg bg-primary px-3 py-2 text-xs font-semibold text-primary-foreground shadow-sm transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 sm:px-4 sm:text-sm"
1807
+ onClick={() => {
1808
+ setCreateTaskData((d) => ({
1809
+ ...d,
1810
+ googleCalendarId:
1811
+ targetGoogleCalendars.length > 0
1812
+ ? pickDefaultCalendarId(gCalPayload, targetGoogleCalendars)
1813
+ : '',
1814
+ }));
1815
+ setShowCreateTaskModal(true);
1816
+ }}
1817
+ className="bg-primary text-primary-foreground hover:bg-primary/90 focus-visible:ring-primary inline-flex cursor-pointer items-center gap-2 rounded-lg px-3 py-2 text-xs font-semibold shadow-sm transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none sm:px-4 sm:text-sm"
1115
1818
  >
1116
1819
  <Bookmark className="h-4 w-4" />
1117
1820
  <span className="hidden sm:inline">Tâche</span>
@@ -1120,111 +1823,141 @@ export default function AgendaPage() {
1120
1823
  </div>
1121
1824
  </div>
1122
1825
 
1123
- {/* Filtres et navigation */}
1124
- <div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
1125
- {/* Filtres d'événements */}
1126
- <div className="flex items-center gap-2">
1127
- <button
1128
- type="button"
1129
- onClick={() =>
1130
- setFilters({
1131
- tasks: true,
1132
- meetings: true,
1133
- googleMeets: true,
1134
- })
1135
- }
1136
- className={cn(
1137
- 'cursor-pointer rounded-full px-3 py-1.5 text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 focus-visible:ring-offset-2',
1138
- filters.tasks && filters.meetings && filters.googleMeets
1139
- ? 'bg-muted text-foreground'
1140
- : 'bg-card text-muted-foreground hover:bg-muted',
1141
- )}
1142
- >
1143
- Tous les événements
1144
- </button>
1145
- <button
1146
- type="button"
1147
- onClick={() =>
1148
- setFilters({
1149
- tasks: true,
1150
- meetings: false,
1151
- googleMeets: false,
1152
- })
1153
- }
1154
- className={cn(
1155
- 'cursor-pointer rounded-full px-3 py-1.5 text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 focus-visible:ring-offset-2',
1156
- filters.tasks && !filters.meetings && !filters.googleMeets
1157
- ? 'bg-muted text-foreground'
1158
- : 'bg-card text-muted-foreground hover:bg-muted',
1159
- )}
1160
- >
1161
- Tâches
1162
- </button>
1163
- <button
1164
- type="button"
1165
- onClick={() =>
1166
- setFilters({
1167
- tasks: false,
1168
- meetings: true,
1169
- googleMeets: false,
1170
- })
1171
- }
1172
- className={cn(
1173
- 'cursor-pointer rounded-full px-3 py-1.5 text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 focus-visible:ring-offset-2',
1174
- !filters.tasks && filters.meetings && !filters.googleMeets
1175
- ? 'bg-muted text-foreground'
1176
- : 'bg-card text-muted-foreground hover:bg-muted',
1177
- )}
1178
- >
1179
- Rendez-vous
1180
- </button>
1181
- <button
1182
- type="button"
1183
- onClick={() =>
1184
- setFilters({
1185
- tasks: false,
1186
- meetings: false,
1187
- googleMeets: true,
1188
- })
1189
- }
1190
- className={cn(
1191
- 'cursor-pointer rounded-full px-3 py-1.5 text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 focus-visible:ring-offset-2',
1192
- !filters.tasks && !filters.meetings && filters.googleMeets
1193
- ? 'bg-muted text-foreground'
1194
- : 'bg-card text-muted-foreground hover:bg-muted',
1826
+ {/* Filtres et navigation : sur lg, une seule ligne — filtres | utilisateurs (scroll) | nav */}
1827
+ <div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between lg:gap-3">
1828
+ <div className="flex min-w-0 flex-1 flex-col gap-2 lg:flex-row lg:items-center lg:gap-2">
1829
+ {/* Filtres + toggle admin (ne rétrécit pas sur grand écran) */}
1830
+ <div className="flex shrink-0 flex-wrap items-center gap-2">
1831
+ <button
1832
+ type="button"
1833
+ onClick={() =>
1834
+ setFilters({
1835
+ tasks: true,
1836
+ meetings: true,
1837
+ googleMeets: true,
1838
+ })
1839
+ }
1840
+ className={cn(
1841
+ 'focus-visible:ring-primary/40 cursor-pointer rounded-full px-3 py-1.5 text-xs font-medium transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none',
1842
+ filters.tasks && filters.meetings && filters.googleMeets
1843
+ ? 'bg-muted text-foreground'
1844
+ : 'bg-card text-muted-foreground hover:bg-muted',
1845
+ )}
1846
+ >
1847
+ Tous les événements
1848
+ </button>
1849
+ <button
1850
+ type="button"
1851
+ onClick={() =>
1852
+ setFilters({
1853
+ tasks: true,
1854
+ meetings: false,
1855
+ googleMeets: false,
1856
+ })
1857
+ }
1858
+ className={cn(
1859
+ 'focus-visible:ring-primary/40 cursor-pointer rounded-full px-3 py-1.5 text-xs font-medium transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none',
1860
+ filters.tasks && !filters.meetings && !filters.googleMeets
1861
+ ? 'bg-muted text-foreground'
1862
+ : 'bg-card text-muted-foreground hover:bg-muted',
1863
+ )}
1864
+ >
1865
+ Tâches
1866
+ </button>
1867
+ <button
1868
+ type="button"
1869
+ onClick={() =>
1870
+ setFilters({
1871
+ tasks: false,
1872
+ meetings: true,
1873
+ googleMeets: false,
1874
+ })
1875
+ }
1876
+ className={cn(
1877
+ 'focus-visible:ring-primary/40 cursor-pointer rounded-full px-3 py-1.5 text-xs font-medium transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none',
1878
+ !filters.tasks && filters.meetings && !filters.googleMeets
1879
+ ? 'bg-muted text-foreground'
1880
+ : 'bg-card text-muted-foreground hover:bg-muted',
1881
+ )}
1882
+ >
1883
+ Rendez-vous
1884
+ </button>
1885
+ <button
1886
+ type="button"
1887
+ onClick={() =>
1888
+ setFilters({
1889
+ tasks: false,
1890
+ meetings: false,
1891
+ googleMeets: true,
1892
+ })
1893
+ }
1894
+ className={cn(
1895
+ 'focus-visible:ring-primary/40 cursor-pointer rounded-full px-3 py-1.5 text-xs font-medium transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none',
1896
+ !filters.tasks && !filters.meetings && filters.googleMeets
1897
+ ? 'bg-muted text-foreground'
1898
+ : 'bg-card text-muted-foreground hover:bg-muted',
1899
+ )}
1900
+ >
1901
+ Google Meet
1902
+ </button>
1903
+ {hasPermission('tasks.view_other_users_events') && (
1904
+ <>
1905
+ {/* Filtre par utilisateur (affiché quand "Voir les autres utilisateurs" est actif) */}
1906
+ <div className="bg-border mx-2 h-6 w-px" />
1907
+ <button
1908
+ type="button"
1909
+ onClick={() => {
1910
+ setShowOtherUsersEvents(!showOtherUsersEvents);
1911
+ // Réinitialiser le filtre utilisateur
1912
+ if (users.length > 0) {
1913
+ setSelectedUserIds(new Set(users.map((u) => u.id)));
1914
+ }
1915
+ }}
1916
+ className={cn(
1917
+ 'focus-visible:ring-primary/40 cursor-pointer rounded-full px-3 py-1.5 text-xs font-medium transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none',
1918
+ showOtherUsersEvents
1919
+ ? 'bg-muted text-foreground'
1920
+ : 'bg-card text-muted-foreground hover:bg-muted',
1921
+ )}
1922
+ >
1923
+ Voir les autres utilisateurs
1924
+ </button>
1925
+ </>
1195
1926
  )}
1196
- >
1197
- Google Meet
1198
- </button>
1199
- {hasPermission('tasks.view_other_users_events') && (
1927
+ </div>
1928
+
1929
+ {/* Bande utilisateurs : entre filtres et navigation sur grand écran (flex-1 + scroll) */}
1930
+ {hasPermission('tasks.view_other_users_events') && showOtherUsersEvents && (
1200
1931
  <>
1201
- {/* Filtre par utilisateur (affiché quand "Voir les autres utilisateurs" est actif) */}
1202
- <div className="mx-2 h-6 w-px bg-border" />
1203
- <button
1204
- type="button"
1205
- onClick={() => {
1206
- setShowOtherUsersEvents(!showOtherUsersEvents);
1207
- // Réinitialiser le filtre utilisateur
1208
- if (users.length > 0) {
1209
- setSelectedUserIds(new Set(users.map((u) => u.id)));
1210
- }
1211
- }}
1212
- className={cn(
1213
- 'cursor-pointer rounded-full px-3 py-1.5 text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 focus-visible:ring-offset-2',
1214
- showOtherUsersEvents
1215
- ? 'bg-muted text-foreground'
1216
- : 'bg-card text-muted-foreground hover:bg-muted',
1217
- )}
1218
- >
1219
- Voir les autres utilisateurs
1220
- </button>
1221
- {showOtherUsersEvents && (
1222
- <>
1223
- <div className="mx-2 h-6 w-px bg-border" />
1224
- {users.length > 0 ? (
1225
- <div className="flex flex-wrap items-center gap-2">
1932
+ <div className="bg-border mx-0.5 hidden h-6 w-px shrink-0 lg:block" aria-hidden />
1933
+ {users.length > 0 ? (
1934
+ <div className="border-border bg-muted/30 flex min-h-8 min-w-0 w-full flex-1 flex-col gap-1 rounded-lg border px-1.5 py-1 lg:min-w-0">
1935
+ <p className="text-muted-foreground px-0.5 text-[10px] font-medium tracking-wide uppercase lg:hidden">
1936
+ Utilisateurs
1937
+ </p>
1938
+ <div className="flex min-h-8 min-w-0 items-stretch gap-0">
1939
+ {(userChipsScrollEdges.canScrollPrev ||
1940
+ userChipsScrollEdges.canScrollNext) && (
1941
+ <button
1942
+ type="button"
1943
+ onClick={() => scrollUserChipsBy('prev')}
1944
+ disabled={!userChipsScrollEdges.canScrollPrev}
1945
+ className="text-muted-foreground hover:bg-muted hover:text-foreground focus-visible:ring-primary/40 shrink-0 cursor-pointer rounded-md border border-transparent p-0.5 transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-30"
1946
+ aria-label="Faire défiler la liste des utilisateurs vers la gauche"
1947
+ >
1948
+ <ChevronLeft className="h-3.5 w-3.5" />
1949
+ </button>
1950
+ )}
1951
+ <div
1952
+ ref={userChipsScrollRef}
1953
+ role="group"
1954
+ aria-label="Filtrer par utilisateur"
1955
+ title="Molette de la souris : faire défiler horizontalement"
1956
+ className="flex min-w-0 flex-1 flex-nowrap gap-1.5 overflow-x-auto overscroll-x-contain py-0.5 [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden"
1957
+ >
1226
1958
  {users.map((user) => {
1227
1959
  const isSelected = selectedUserIds.has(user.id);
1960
+ const colors = userColorById.get(user.id) ?? DEFAULT_EVENT_COLOR;
1228
1961
  return (
1229
1962
  <button
1230
1963
  key={user.id}
@@ -1239,49 +1972,76 @@ export default function AgendaPage() {
1239
1972
  setSelectedUserIds(newSelected);
1240
1973
  }}
1241
1974
  className={cn(
1242
- 'cursor-pointer rounded-full border px-3 py-1.5 text-xs font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 focus-visible:ring-offset-2',
1975
+ 'focus-visible:ring-primary/40 inline-flex shrink-0 cursor-pointer snap-start items-center gap-1.5 rounded-full border px-2 py-1 text-[11px] font-medium leading-tight transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none',
1243
1976
  isSelected
1244
- ? 'border-blue-600 bg-blue-600 text-blue-950 shadow-sm hover:bg-blue-700'
1245
- : 'border-border bg-card text-muted-foreground hover:bg-muted',
1977
+ ? 'border-transparent text-white shadow-sm hover:opacity-95'
1978
+ : 'border-border bg-card text-foreground hover:bg-muted/80',
1246
1979
  )}
1980
+ style={
1981
+ isSelected
1982
+ ? {
1983
+ backgroundColor: colors.border,
1984
+ borderColor: colors.border,
1985
+ }
1986
+ : {
1987
+ borderColor: `${colors.border}55`,
1988
+ }
1989
+ }
1247
1990
  title={user.name}
1248
1991
  >
1249
- <span className="max-w-[100px] truncate">{user.name}</span>
1992
+ <span
1993
+ className="h-1.5 w-1.5 shrink-0 rounded-full ring-1 ring-white/40"
1994
+ style={{ backgroundColor: colors.border }}
1995
+ aria-hidden
1996
+ />
1997
+ <span className="max-w-[120px] truncate">{user.name}</span>
1250
1998
  </button>
1251
1999
  );
1252
2000
  })}
1253
2001
  </div>
1254
- ) : (
1255
- <div className="text-xs text-muted-foreground">
1256
- Chargement des utilisateurs...
1257
- </div>
1258
- )}
1259
- </>
2002
+ {(userChipsScrollEdges.canScrollPrev ||
2003
+ userChipsScrollEdges.canScrollNext) && (
2004
+ <button
2005
+ type="button"
2006
+ onClick={() => scrollUserChipsBy('next')}
2007
+ disabled={!userChipsScrollEdges.canScrollNext}
2008
+ className="text-muted-foreground hover:bg-muted hover:text-foreground focus-visible:ring-primary/40 shrink-0 cursor-pointer rounded-md border border-transparent p-0.5 transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-30"
2009
+ aria-label="Faire défiler la liste des utilisateurs vers la droite"
2010
+ >
2011
+ <ChevronRight className="h-3.5 w-3.5" />
2012
+ </button>
2013
+ )}
2014
+ </div>
2015
+ </div>
2016
+ ) : (
2017
+ <div className="text-muted-foreground min-w-0 flex-1 text-xs">
2018
+ Chargement des utilisateurs...
2019
+ </div>
1260
2020
  )}
1261
2021
  </>
1262
2022
  )}
1263
2023
  </div>
1264
2024
 
1265
2025
  {/* Navigation et sélecteur de vue */}
1266
- <div className="flex items-center gap-2">
2026
+ <div className="flex shrink-0 items-center gap-2">
1267
2027
  {/* Navigation de date */}
1268
- <div className="flex items-center gap-1 rounded-lg border border-border bg-card p-1">
2028
+ <div className="border-border bg-card flex items-center gap-1 rounded-lg border p-1">
1269
2029
  <button
1270
2030
  onClick={() => navigateDate('prev')}
1271
- className="cursor-pointer rounded-md p-1.5 text-muted-foreground transition-colors duration-200 hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2"
2031
+ className="text-muted-foreground hover:bg-muted focus-visible:ring-primary cursor-pointer rounded-md p-1.5 transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
1272
2032
  aria-label="Date précédente"
1273
2033
  >
1274
2034
  <ChevronLeft className="h-4 w-4" />
1275
2035
  </button>
1276
2036
  <button
1277
2037
  onClick={goToToday}
1278
- className="cursor-pointer rounded-md px-3 py-1.5 text-xs font-medium text-muted-foreground transition-colors duration-200 hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2"
2038
+ className="text-muted-foreground hover:bg-muted focus-visible:ring-primary cursor-pointer rounded-md px-3 py-1.5 text-xs font-medium transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
1279
2039
  >
1280
2040
  Aujourd&apos;hui
1281
2041
  </button>
1282
2042
  <button
1283
2043
  onClick={() => navigateDate('next')}
1284
- className="cursor-pointer rounded-md p-1.5 text-muted-foreground transition-colors duration-200 hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2"
2044
+ className="text-muted-foreground hover:bg-muted focus-visible:ring-primary cursor-pointer rounded-md p-1.5 transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
1285
2045
  aria-label="Date suivante"
1286
2046
  >
1287
2047
  <ChevronRight className="h-4 w-4" />
@@ -1292,7 +2052,7 @@ export default function AgendaPage() {
1292
2052
  <select
1293
2053
  value={view}
1294
2054
  onChange={(e) => setView(e.target.value as 'month' | 'week' | 'day')}
1295
- className="cursor-pointer rounded-lg border border-border bg-card px-3 py-1.5 text-xs font-medium text-muted-foreground focus:border-primary/50 focus:ring-2 focus:ring-primary/20 focus:outline-none"
2055
+ className="border-border bg-card text-muted-foreground focus:border-primary/50 focus:ring-primary/20 cursor-pointer rounded-lg border px-3 py-1.5 text-xs font-medium focus:ring-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
1296
2056
  >
1297
2057
  <option value="month">Vue mois</option>
1298
2058
  <option value="week">Vue semaine</option>
@@ -1310,20 +2070,33 @@ export default function AgendaPage() {
1310
2070
  <AgendaMonthSkeleton />
1311
2071
  </div>
1312
2072
  ) : (
1313
- <div className="m-4 rounded-xl border border-border bg-card shadow-(--shadow-card) sm:m-6 lg:m-8">
1314
- <div className="sticky top-0 z-30 grid grid-cols-7 rounded-t-xl border-b border-border bg-muted shadow-md">
2073
+ <div className="border-border bg-card m-4 rounded-xl border shadow-(--shadow-card) sm:m-6 lg:m-8">
2074
+ <div className="border-border bg-muted sticky top-0 z-30 grid grid-cols-7 rounded-t-xl border-b shadow-md">
1315
2075
  {['Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam', 'Dim'].map((day) => (
1316
2076
  <div
1317
2077
  key={day}
1318
- className="border-r border-border px-4 py-4 text-center text-xs font-medium tracking-wide text-muted-foreground uppercase last:border-r-0"
2078
+ className="border-border text-muted-foreground border-r px-4 py-4 text-center text-xs font-medium tracking-wide uppercase last:border-r-0"
1319
2079
  >
1320
2080
  {day}
1321
2081
  </div>
1322
2082
  ))}
1323
2083
  </div>
1324
- <div className="grid grid-cols-7 divide-x divide-y divide-border">
2084
+ <div className="divide-border grid grid-cols-7 divide-x divide-y">
1325
2085
  {getDaysInMonth().map((day, index) => {
1326
2086
  const dayTasks = getTasksForDate(day.date);
2087
+ const dayGoogleMonth = getGoogleEventsForDate(day.date);
2088
+ const mergedForMonth = [
2089
+ ...dayTasks.map((t) => ({
2090
+ kind: 'task' as const,
2091
+ task: t,
2092
+ sort: new Date(t.scheduledAt).getTime(),
2093
+ })),
2094
+ ...dayGoogleMonth.map((ev) => ({
2095
+ kind: 'google' as const,
2096
+ ev,
2097
+ sort: googleAgendaEventLocalTimeRange(ev).start.getTime(),
2098
+ })),
2099
+ ].sort((a, b) => a.sort - b.sort);
1327
2100
  const isToday = day.date.toDateString() === new Date().toDateString();
1328
2101
 
1329
2102
  return (
@@ -1334,7 +2107,7 @@ export default function AgendaPage() {
1334
2107
  setView('day');
1335
2108
  }}
1336
2109
  className={cn(
1337
- 'min-h-[120px] cursor-pointer p-2 transition-colors duration-200 hover:bg-muted/60',
2110
+ 'hover:bg-muted/60 min-h-[120px] cursor-pointer p-2 transition-colors duration-200',
1338
2111
  !day.isCurrentMonth && 'bg-muted/40',
1339
2112
  isToday && 'bg-blue-50/50',
1340
2113
  )}
@@ -1358,71 +2131,91 @@ export default function AgendaPage() {
1358
2131
  )}
1359
2132
  </div>
1360
2133
  <div className="space-y-1" onClick={(e) => e.stopPropagation()}>
1361
- {dayTasks.slice(0, 3).map((task) => {
1362
- const eventColor = getEventColor(task);
1363
- return (
1364
- <div
1365
- key={task.id}
1366
- onClick={() => openTaskDetailModal(task)}
1367
- className={cn(
2134
+ {mergedForMonth.slice(0, 3).map((row) => {
2135
+ if (row.kind === 'google') {
2136
+ const ev = row.ev;
2137
+ return (
2138
+ <div
2139
+ key={`gcal-${ev.calendarId}-${ev.googleEventId}`}
2140
+ onClick={() => {
2141
+ if (ev.htmlLink) {
2142
+ globalThis.open(ev.htmlLink, '_blank', 'noopener,noreferrer');
2143
+ }
2144
+ }}
2145
+ className="block cursor-pointer rounded-md border border-solid px-2 py-1.5 text-xs transition-colors hover:brightness-[0.97]"
2146
+ style={{
2147
+ backgroundColor: gCalChrome.backgroundColor,
2148
+ borderColor: gCalChrome.borderColor,
2149
+ color: gCalChrome.color,
2150
+ }}
2151
+ title="Google Calendar"
2152
+ >
2153
+ <div className="flex items-center gap-1">
2154
+ <Calendar
2155
+ className="h-3.5 w-3.5 shrink-0"
2156
+ style={{ color: gCalChrome.iconColor }}
2157
+ />
2158
+ <span className="min-w-0 flex-1 truncate font-semibold">
2159
+ {ev.allDay ? 'Journée' : formatTime(ev.start)} {ev.title}
2160
+ </span>
2161
+ </div>
2162
+ </div>
2163
+ );
2164
+ }
2165
+ const task = row.task;
2166
+ const colors = getEventColor(task);
2167
+ return (
2168
+ <div
2169
+ key={task.id}
2170
+ onClick={() => openTaskDetailModal(task)}
2171
+ className={cn(
1368
2172
  'block cursor-pointer rounded-md px-2 py-1.5 text-xs transition-colors hover:opacity-90 hover:shadow-sm',
1369
2173
  task.completed && 'opacity-60',
1370
2174
  )}
1371
2175
  style={{
1372
- backgroundColor: `${eventColor}15`,
1373
- borderLeft: `3px solid ${eventColor}`,
2176
+ backgroundColor: colors.bg,
2177
+ borderLeft: `3px solid ${colors.border}`,
2178
+ color: colors.text,
1374
2179
  }}
1375
2180
  title={`${TASK_TYPE_LABELS[task.type]} - ${task.title || 'Sans titre'}`}
1376
2181
  >
1377
- <div className="flex flex-col gap-0.5">
1378
- <div className="flex items-center gap-1.5">
1379
- {task.type === 'VIDEO_CONFERENCE' && (
1380
- <Video className="h-3 w-3 shrink-0 text-green-600" />
1381
- )}
1382
- {task.type === 'MEETING' && (
1383
- <Users className="h-3 w-3 shrink-0 text-blue-600" />
2182
+ <div className="flex items-center gap-1">
2183
+ <button
2184
+ type="button"
2185
+ onClick={(e) => {
2186
+ e.stopPropagation();
2187
+ toggleTaskComplete(task.id, task.completed);
2188
+ }}
2189
+ className="shrink-0 rounded p-0.5 transition-colors hover:bg-white/70"
2190
+ style={{ color: colors.icon }}
2191
+ aria-label={
2192
+ task.completed
2193
+ ? 'Marquer comme non terminée'
2194
+ : 'Marquer comme terminée'
2195
+ }
2196
+ >
2197
+ {task.completed ? (
2198
+ <CheckCircle2 className="h-3.5 w-3.5" />
2199
+ ) : (
2200
+ <Circle className="h-3.5 w-3.5" />
1384
2201
  )}
1385
- {task.type === 'CALL' && (
1386
- <Phone className="h-3 w-3 shrink-0 text-purple-600" />
2202
+ </button>
2203
+ <div
2204
+ className={cn(
2205
+ 'min-w-0 flex-1 truncate leading-tight font-semibold',
2206
+ task.completed && 'line-through opacity-60',
1387
2207
  )}
1388
- <div
1389
- className={cn(
1390
- 'truncate leading-tight font-semibold',
1391
- task.completed
1392
- ? 'text-gray-400 line-through'
1393
- : 'text-gray-900',
1394
- )}
1395
- >
1396
- {formatTime(task.scheduledAt)}{' '}
1397
- {task.title || TASK_TYPE_LABELS[task.type]}
1398
- </div>
2208
+ >
2209
+ {formatTime(task.scheduledAt)}{' '}
2210
+ {task.title || TASK_TYPE_LABELS[task.type]}
1399
2211
  </div>
1400
- {task.contact && (
1401
- <div
1402
- className={cn(
1403
- 'truncate overflow-hidden text-[10px] leading-tight font-medium',
1404
- task.completed
1405
- ? 'text-gray-400 line-through'
1406
- : 'text-gray-700',
1407
- )}
1408
- title={`${task.contact.firstName} ${task.contact.lastName}`}
1409
- >
1410
- {task.contact.firstName} {task.contact.lastName}
1411
- </div>
1412
- )}
1413
- {showOtherUsersEvents &&
1414
- session?.user?.id !== task.assignedUser?.id && (
1415
- <div className="mt-0.5 text-right text-[9px] leading-tight text-gray-500 italic">
1416
- {task.assignedUser?.name || 'Ancien utilisateur'}
1417
- </div>
1418
- )}
1419
2212
  </div>
1420
2213
  </div>
1421
2214
  );
1422
2215
  })}
1423
- {dayTasks.length > 3 && (
2216
+ {mergedForMonth.length > 3 && (
1424
2217
  <div className="px-2 text-xs text-gray-500">
1425
- +{dayTasks.length - 3} autre(s)
2218
+ +{mergedForMonth.length - 3} autre(s)
1426
2219
  </div>
1427
2220
  )}
1428
2221
  </div>
@@ -1475,6 +2268,70 @@ export default function AgendaPage() {
1475
2268
  })}
1476
2269
  </div>
1477
2270
  <div className="overflow-auto">
2271
+ {weekAllDayGoogleBars ? (
2272
+ <div className="flex shrink-0 border-b border-gray-200 bg-slate-50/90">
2273
+ <div
2274
+ className="flex w-[60px] shrink-0 items-start justify-end border-r border-gray-200 py-2 pr-2 text-right text-[10px] font-medium leading-tight tracking-wide text-gray-400 uppercase"
2275
+ style={{
2276
+ minHeight:
2277
+ weekAllDayGoogleBars.rowCount > 0
2278
+ ? `${24 + weekAllDayGoogleBars.rowCount * 28}px`
2279
+ : undefined,
2280
+ }}
2281
+ >
2282
+ <span className="max-w-[52px]">Toute la journée</span>
2283
+ </div>
2284
+ <div className="relative min-h-9 min-w-0 flex-1">
2285
+ <div className="pointer-events-none absolute inset-0 grid grid-cols-7">
2286
+ {Array.from({ length: 7 }, (_, i) => (
2287
+ <div
2288
+ key={`allday-gridline-${i}`}
2289
+ className={cn('border-l border-gray-100', i === 0 && 'border-l-0')}
2290
+ />
2291
+ ))}
2292
+ </div>
2293
+ <div
2294
+ className="relative z-[1] grid grid-cols-7 gap-y-1 px-0.5 py-1.5"
2295
+ style={{
2296
+ gridTemplateRows: `repeat(${weekAllDayGoogleBars.rowCount}, minmax(24px, auto))`,
2297
+ }}
2298
+ >
2299
+ {weekAllDayGoogleBars.placed.map(
2300
+ ({ ev, startIdx, endIdx, lane }) => {
2301
+ const span = endIdx - startIdx + 1;
2302
+ const mergedLabel = googleWeekMergedBarLabel(ev);
2303
+ return (
2304
+ <button
2305
+ key={`gcal-allday-span-${ev.calendarId}-${ev.googleEventId}`}
2306
+ type="button"
2307
+ className="flex min-h-6 min-w-0 cursor-pointer items-center gap-1 truncate rounded border border-solid px-1.5 py-1 text-left text-[11px] font-semibold shadow-sm transition-colors hover:brightness-[0.97]"
2308
+ style={{
2309
+ gridColumn: `${startIdx + 1} / span ${span}`,
2310
+ gridRow: lane + 1,
2311
+ backgroundColor: gCalChrome.backgroundColor,
2312
+ borderColor: gCalChrome.borderColor,
2313
+ color: gCalChrome.color,
2314
+ }}
2315
+ onClick={() => {
2316
+ if (ev.htmlLink) {
2317
+ globalThis.open(ev.htmlLink, '_blank', 'noopener,noreferrer');
2318
+ }
2319
+ }}
2320
+ title={mergedLabel}
2321
+ >
2322
+ <Calendar
2323
+ className="h-3 w-3 shrink-0"
2324
+ style={{ color: gCalChrome.iconColor }}
2325
+ />
2326
+ <span className="min-w-0 truncate">{mergedLabel}</span>
2327
+ </button>
2328
+ );
2329
+ },
2330
+ )}
2331
+ </div>
2332
+ </div>
2333
+ </div>
2334
+ ) : null}
1478
2335
  <div
1479
2336
  className="relative grid"
1480
2337
  style={{ gridTemplateColumns: '60px repeat(7, 1fr)' }}
@@ -1543,6 +2400,35 @@ export default function AgendaPage() {
1543
2400
  {getWeekDays().map((day) => {
1544
2401
  // Récupérer toutes les tâches du jour (incluant celles qui continuent depuis un jour précédent)
1545
2402
  const dayTasks = getTasksForDate(day);
2403
+ const dayGoogleTimed = getGoogleEventsForDate(day).filter((ev) => {
2404
+ if (ev.allDay) return false;
2405
+ const { start, end } = googleAgendaEventLocalTimeRange(ev);
2406
+ const startKey = localCalendarDateKey(start);
2407
+ const endKey = localCalendarDateKey(end);
2408
+ // Les événements horaires multi-jours sont rendus dans la bande haute.
2409
+ return startKey === endKey;
2410
+ });
2411
+ const weekCombinedEntries = [
2412
+ ...dayTasks.map((task) => {
2413
+ const taskStart = new Date(task.scheduledAt);
2414
+ const duration = task.durationMinutes || 30;
2415
+ return {
2416
+ id: task.id,
2417
+ payload: { kind: 'task' as const, task },
2418
+ startDate: taskStart,
2419
+ endDate: new Date(taskStart.getTime() + duration * 60000),
2420
+ };
2421
+ }),
2422
+ ...dayGoogleTimed.map((ev) => {
2423
+ const { start, end } = googleAgendaEventLocalTimeRange(ev);
2424
+ return {
2425
+ id: `gcal-${ev.calendarId}-${ev.googleEventId}`,
2426
+ payload: { kind: 'google' as const, event: ev },
2427
+ startDate: start,
2428
+ endDate: end,
2429
+ };
2430
+ }),
2431
+ ];
1546
2432
 
1547
2433
  const isToday =
1548
2434
  currentTime && day.toDateString() === currentTime.toDateString();
@@ -1577,139 +2463,177 @@ export default function AgendaPage() {
1577
2463
  </div>
1578
2464
  )}
1579
2465
 
1580
- {/* Afficher les tâches positionnées absolument */}
1581
- {dayTasks.map((task) => {
1582
- const taskStart = new Date(task.scheduledAt);
1583
- const duration = task.durationMinutes || 30;
1584
- const taskEnd = new Date(taskStart.getTime() + duration * 60000);
1585
-
1586
- // Début et fin du jour actuel
1587
- const dayStart = new Date(day);
1588
- dayStart.setHours(0, 0, 0, 0);
1589
- const dayEnd = new Date(day);
1590
- dayEnd.setHours(23, 59, 59, 999);
1591
-
1592
- // Début de la grille visible (7h)
1593
- const gridStart = new Date(day);
1594
- gridStart.setHours(HOURS[0], 0, 0, 0);
1595
-
1596
- // Fin de la grille visible (dernière heure + 1)
1597
- const gridEnd = new Date(day);
1598
- gridEnd.setHours(HOURS[HOURS.length - 1] + 1, 0, 0, 0);
1599
-
1600
- // Calculer l'heure effective de début pour ce jour (dans la grille visible)
1601
- let effectiveStart = taskStart < dayStart ? dayStart : taskStart;
1602
- effectiveStart =
1603
- effectiveStart < gridStart ? gridStart : effectiveStart;
1604
-
1605
- let effectiveEnd = taskEnd > dayEnd ? dayEnd : taskEnd;
1606
- effectiveEnd = effectiveEnd > gridEnd ? gridEnd : effectiveEnd;
1607
-
1608
- // Calculer les minutes depuis le début du jour
1609
- const startHour = effectiveStart.getHours();
1610
- const startMinute = effectiveStart.getMinutes();
1611
- const startMinutes = startHour * 60 + startMinute;
1612
-
1613
- // Calculer la durée effective pour ce jour (en minutes)
1614
- const effectiveDuration = Math.round(
1615
- (effectiveEnd.getTime() - effectiveStart.getTime()) / 60000,
1616
- );
1617
-
1618
- const startKey = `${startMinutes}`;
1619
- const sameSlotTasks = dayTasks.filter((t) => {
1620
- const tStart = new Date(t.scheduledAt);
1621
- let tEffectiveStart = tStart < dayStart ? dayStart : tStart;
1622
- tEffectiveStart =
1623
- tEffectiveStart < gridStart ? gridStart : tEffectiveStart;
1624
- const th = tEffectiveStart.getHours();
1625
- const tm = tEffectiveStart.getMinutes();
1626
- const ts = th * 60 + tm;
1627
- return ts === startMinutes;
1628
- });
1629
- const slotIndex = sameSlotTasks.findIndex((t) => t.id === task.id);
1630
- const slotCount = sameSlotTasks.length || 1;
2466
+ {/* Afficher les tâches et événements Google positionnés absolument */}
2467
+ {layoutOverlappingBySchedule(
2468
+ weekCombinedEntries,
2469
+ day,
2470
+ HOURS[0],
2471
+ HOURS[HOURS.length - 1] + 1,
2472
+ ).map((layout) => {
2473
+ const { payload, startMinutes, endMinutes, column, totalColumns } =
2474
+ layout;
1631
2475
 
1632
2476
  const top = (startMinutes - HOURS[0] * 60) * (slotHeight / 60);
1633
- const height = effectiveDuration * (slotHeight / 60);
2477
+ const height = Math.max((endMinutes - startMinutes) * (slotHeight / 60), 24);
2478
+
2479
+ const widthPercent = 100 / totalColumns;
2480
+ const leftPercent = column * widthPercent;
2481
+
2482
+ if (payload.kind === 'google') {
2483
+ const ev = payload.event;
2484
+ const { start: gStart, end: gEnd } =
2485
+ googleAgendaEventLocalTimeRange(ev);
2486
+ const gRange = `${gStart.toLocaleTimeString('fr-FR', {
2487
+ hour: '2-digit',
2488
+ minute: '2-digit',
2489
+ })} – ${gEnd.toLocaleTimeString('fr-FR', {
2490
+ hour: '2-digit',
2491
+ minute: '2-digit',
2492
+ })}`;
2493
+ const overlapTooltip = totalColumns > 1;
2494
+ return (
2495
+ <WeekAgendaOverlapTooltip
2496
+ key={`gcal-${ev.calendarId}-${ev.googleEventId}`}
2497
+ show={overlapTooltip}
2498
+ title={ev.title || 'Événement Google Calendar'}
2499
+ description={`${gRange} · Google Calendar`}
2500
+ >
2501
+ <div
2502
+ className="absolute z-10 cursor-pointer overflow-hidden rounded-md border border-solid px-2 py-1 text-xs shadow-sm transition-[filter,box-shadow] hover:z-20 hover:brightness-[0.97]"
2503
+ style={{
2504
+ top: `${top}px`,
2505
+ height: `${height}px`,
2506
+ minHeight: '24px',
2507
+ left: `calc(${leftPercent}% + 2px)`,
2508
+ width: `calc(${widthPercent}% - 4px)`,
2509
+ backgroundColor: gCalChrome.backgroundColor,
2510
+ borderColor: gCalChrome.borderColor,
2511
+ color: gCalChrome.color,
2512
+ }}
2513
+ onClick={() => {
2514
+ if (ev.htmlLink) {
2515
+ globalThis.open(ev.htmlLink, '_blank', 'noopener,noreferrer');
2516
+ }
2517
+ }}
2518
+ title={
2519
+ overlapTooltip
2520
+ ? undefined
2521
+ : `Événement Google Calendar — ${ev.title} · ${gRange}`
2522
+ }
2523
+ >
2524
+ <div className="flex min-w-0 items-center gap-1">
2525
+ <Calendar
2526
+ className="h-3 w-3 shrink-0"
2527
+ style={{ color: gCalChrome.iconColor }}
2528
+ />
2529
+ <span className="truncate font-semibold">{ev.title}</span>
2530
+ </div>
2531
+ </div>
2532
+ </WeekAgendaOverlapTooltip>
2533
+ );
2534
+ }
1634
2535
 
1635
- const widthPercent = 100 / slotCount;
1636
- const leftPercent = slotIndex * widthPercent;
2536
+ const task = payload.task;
2537
+ const colors = getEventColor(task);
2538
+ const taskStartDt = new Date(task.scheduledAt);
2539
+ const taskEndDt = new Date(
2540
+ taskStartDt.getTime() +
2541
+ (task.durationMinutes || 30) * 60000,
2542
+ );
2543
+ const taskRange = `${formatTime(task.scheduledAt)} – ${taskEndDt.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })}`;
2544
+ const taskTitleLine = task.title || TASK_TYPE_LABELS[task.type];
2545
+ const contactLine = task.contact
2546
+ ? [task.contact.firstName, task.contact.lastName]
2547
+ .filter(Boolean)
2548
+ .join(' ')
2549
+ .trim()
2550
+ : '';
2551
+ const assigneeName =
2552
+ task.assignedUser?.name?.trim() || 'Ancien utilisateur';
2553
+ const taskTipDesc = [
2554
+ TASK_TYPE_LABELS[task.type],
2555
+ taskRange,
2556
+ contactLine || null,
2557
+ ]
2558
+ .filter(Boolean)
2559
+ .join(' · ');
2560
+ const overlapTooltip = totalColumns > 1;
1637
2561
 
1638
- const eventColor = getEventColor(task);
1639
2562
  return (
1640
- <div
1641
- key={`${task.id}-${startKey}`}
1642
- className={cn(
1643
- 'absolute z-10 cursor-pointer rounded-md px-2 py-1.5 text-xs shadow-sm transition-all hover:shadow-md',
1644
- task.completed && 'opacity-60',
1645
- )}
1646
- style={{
1647
- top: `${top}px`,
1648
- height: `${height}px`,
1649
- minHeight: '32px',
1650
- backgroundColor: `${eventColor}15`,
1651
- borderLeft: `3px solid ${eventColor}`,
1652
- left: `calc(${leftPercent}% + 4px)`,
1653
- width: `calc(${widthPercent}% - 8px)`,
1654
- }}
1655
- onClick={() => openTaskDetailModal(task)}
2563
+ <WeekAgendaOverlapTooltip
2564
+ key={task.id}
2565
+ show={overlapTooltip}
2566
+ title={taskTitleLine}
2567
+ description={taskTipDesc}
2568
+ footer={`Par ${assigneeName}`}
1656
2569
  >
1657
- <div className="flex h-full flex-col justify-between gap-0.5">
1658
- <div className="flex items-start gap-1.5">
1659
- {task.type === 'VIDEO_CONFERENCE' && (
1660
- <Video className="mt-0.5 h-3 w-3 shrink-0 text-green-600" />
1661
- )}
1662
- {task.type === 'MEETING' && (
1663
- <Users className="mt-0.5 h-3 w-3 shrink-0 text-blue-600" />
1664
- )}
1665
- {task.type === 'CALL' && (
1666
- <Phone className="mt-0.5 h-3 w-3 shrink-0 text-purple-600" />
1667
- )}
1668
- <div
1669
- className={cn(
1670
- 'flex-1 truncate leading-tight font-semibold',
1671
- task.completed
1672
- ? 'text-gray-400 line-through'
1673
- : 'text-gray-900',
1674
- )}
1675
- title={task.title || TASK_TYPE_LABELS[task.type]}
1676
- >
1677
- {task.title || TASK_TYPE_LABELS[task.type]}
1678
- </div>
1679
- {task.contact && (
1680
- <Link
1681
- href={`/contacts/${task.contact.id}`}
1682
- onClick={(e) => e.stopPropagation()}
1683
- className="mt-0.5 inline-flex h-4 w-4 shrink-0 items-center justify-center rounded-full bg-blue-100 text-blue-800 transition-colors hover:bg-blue-200"
1684
- title="Voir le contact"
2570
+ <div
2571
+ className={cn(
2572
+ 'absolute z-10 cursor-pointer overflow-hidden rounded-md px-2 py-1 text-xs shadow-sm transition-shadow hover:z-20 hover:shadow-md',
2573
+ task.completed && 'opacity-60',
2574
+ )}
2575
+ style={{
2576
+ top: `${top}px`,
2577
+ height: `${height}px`,
2578
+ minHeight: '24px',
2579
+ backgroundColor: colors.bg,
2580
+ borderLeft: `3px solid ${colors.border}`,
2581
+ color: colors.text,
2582
+ left: `calc(${leftPercent}% + 2px)`,
2583
+ width: `calc(${widthPercent}% - 4px)`,
2584
+ }}
2585
+ onClick={() => openTaskDetailModal(task)}
2586
+ >
2587
+ <div className="flex h-full flex-col overflow-hidden">
2588
+ <div className="flex min-w-0 items-start gap-1">
2589
+ <button
2590
+ type="button"
2591
+ onClick={(e) => {
2592
+ e.stopPropagation();
2593
+ toggleTaskComplete(task.id, task.completed);
2594
+ }}
2595
+ className="mt-0.5 shrink-0 rounded p-0.5 transition-colors hover:bg-white/40"
2596
+ style={{ color: colors.icon }}
2597
+ aria-label={
2598
+ task.completed
2599
+ ? 'Marquer comme non terminée'
2600
+ : 'Marquer comme terminée'
2601
+ }
1685
2602
  >
1686
- <Eye className="h-3 w-3" />
1687
- </Link>
1688
- )}
1689
- </div>
1690
- {/* <div className="flex min-w-0 flex-col gap-0.5">
1691
- {task.contact && (
2603
+ {task.completed ? (
2604
+ <CheckCircle2 className="h-3 w-3" />
2605
+ ) : (
2606
+ <Circle className="h-3 w-3" />
2607
+ )}
2608
+ </button>
1692
2609
  <div
1693
2610
  className={cn(
1694
- 'truncate overflow-hidden text-[10px] leading-tight font-medium',
1695
- task.completed
1696
- ? 'text-gray-400 line-through'
1697
- : 'text-gray-700',
2611
+ 'min-w-0 flex-1 truncate leading-tight font-semibold',
2612
+ task.completed && 'line-through opacity-60',
1698
2613
  )}
1699
- title={`${task.contact.firstName} ${task.contact.lastName}`}
2614
+ title={
2615
+ overlapTooltip
2616
+ ? undefined
2617
+ : task.title || TASK_TYPE_LABELS[task.type]
2618
+ }
1700
2619
  >
1701
- {task.contact.firstName} {task.contact.lastName}
2620
+ {task.title || TASK_TYPE_LABELS[task.type]}
1702
2621
  </div>
1703
- )}
1704
- {showOtherUsersEvents &&
1705
- session?.user?.id !== task.assignedUser?.id && (
1706
- <div className="text-right text-[9px] leading-tight text-gray-500 italic">
1707
- {task.assignedUser?.name || 'Ancien utilisateur'}
1708
- </div>
2622
+ {task.contact && (
2623
+ <Link
2624
+ href={`/contacts/${task.contact.id}`}
2625
+ onClick={(e) => e.stopPropagation()}
2626
+ className="mt-0.5 inline-flex h-3.5 w-3.5 shrink-0 items-center justify-center rounded-full transition-colors hover:bg-white/40"
2627
+ style={{ color: colors.icon }}
2628
+ title="Voir le contact"
2629
+ >
2630
+ <Eye className="h-2.5 w-2.5" />
2631
+ </Link>
1709
2632
  )}
1710
- </div> */}
2633
+ </div>
2634
+ </div>
1711
2635
  </div>
1712
- </div>
2636
+ </WeekAgendaOverlapTooltip>
1713
2637
  );
1714
2638
  })}
1715
2639
 
@@ -1734,133 +2658,207 @@ export default function AgendaPage() {
1734
2658
  Toutes les tâches de la semaine
1735
2659
  </h2>
1736
2660
  <div className="space-y-3">
1737
- {filteredTasks.length === 0 ? (
2661
+ {filteredTasks.length === 0 && googleEventsInDateRange.length === 0 ? (
1738
2662
  <div className="rounded-xl border border-gray-200 bg-white p-8 text-center shadow-sm">
1739
- <p className="text-gray-500">Aucune tâche pour cette semaine</p>
2663
+ <p className="text-gray-500">Aucun événement pour cette semaine</p>
1740
2664
  </div>
1741
2665
  ) : (
1742
- filteredTasks.map((task) => {
1743
- const TaskContent = (
1744
- <div className="flex items-start justify-between">
1745
- <div className="flex-1">
1746
- <div className="flex items-center gap-2">
1747
- <button
1748
- type="button"
1749
- onClick={(e) => {
1750
- e.stopPropagation();
1751
- toggleTaskComplete(task.id, task.completed);
1752
- }}
1753
- className="cursor-pointer text-gray-400 hover:text-blue-600"
1754
- >
1755
- {task.completed ? (
1756
- <CheckCircle2 className="h-5 w-5 text-blue-600" />
1757
- ) : (
1758
- <Circle className="h-5 w-5" />
1759
- )}
1760
- </button>
1761
- <div className="flex-1">
1762
- <div className="flex items-center gap-2">
1763
- {task.type === 'VIDEO_CONFERENCE' && (
1764
- <Video className="h-4 w-4 text-green-600" />
2666
+ <>
2667
+ {filteredTasks.map((task) => {
2668
+ const TaskContent = (
2669
+ <div className="flex items-start justify-between">
2670
+ <div className="flex-1">
2671
+ <div className="flex items-center gap-2">
2672
+ <button
2673
+ type="button"
2674
+ onClick={(e) => {
2675
+ e.stopPropagation();
2676
+ toggleTaskComplete(task.id, task.completed);
2677
+ }}
2678
+ className="cursor-pointer text-gray-400 hover:text-blue-600"
2679
+ >
2680
+ {task.completed ? (
2681
+ <CheckCircle2 className="h-5 w-5 text-blue-600" />
2682
+ ) : (
2683
+ <Circle className="h-5 w-5" />
1765
2684
  )}
1766
- {task.type === 'MEETING' && (
1767
- <Users className="h-4 w-4 text-blue-600" />
2685
+ </button>
2686
+ <div className="flex-1">
2687
+ <div className="flex items-center gap-2">
2688
+ {task.type === 'VIDEO_CONFERENCE' && (
2689
+ <Video className="h-4 w-4 text-green-600" />
2690
+ )}
2691
+ {task.type === 'MEETING' && (
2692
+ <Users className="h-4 w-4 text-blue-600" />
2693
+ )}
2694
+ {task.type === 'CALL' && (
2695
+ <Phone className="h-4 w-4 text-purple-600" />
2696
+ )}
2697
+ <span className="text-sm font-medium text-gray-600">
2698
+ {TASK_TYPE_LABELS[task.type]}
2699
+ </span>
2700
+ </div>
2701
+ <h3
2702
+ className={cn(
2703
+ 'mt-1 text-base font-semibold',
2704
+ task.completed ? 'text-gray-400 line-through' : 'text-gray-900',
2705
+ )}
2706
+ >
2707
+ {task.title || TASK_TYPE_LABELS[task.type]}
2708
+ </h3>
2709
+ {task.contact && (
2710
+ <div className="mt-2 inline-flex max-w-full items-center gap-1.5 overflow-hidden rounded-md bg-blue-50 px-2.5 py-1 text-sm font-medium text-blue-700">
2711
+ <User className="h-4 w-4 shrink-0" />
2712
+ <span className="truncate">
2713
+ {task.contact.firstName} {task.contact.lastName}
2714
+ </span>
2715
+ <Link
2716
+ href={`/contacts/${task.contact.id}`}
2717
+ onClick={(e) => e.stopPropagation()}
2718
+ className="ml-1 inline-flex h-4 w-4 shrink-0 items-center justify-center rounded-full bg-blue-100 text-blue-800 hover:bg-blue-200"
2719
+ title="Voir le contact"
2720
+ >
2721
+ <Eye className="h-3 w-3" />
2722
+ </Link>
2723
+ </div>
1768
2724
  )}
1769
- {task.type === 'CALL' && (
1770
- <Phone className="h-4 w-4 text-purple-600" />
2725
+ {task.googleMeetLink && (
2726
+ <div className="mt-2 flex flex-wrap items-center gap-2">
2727
+ <button
2728
+ type="button"
2729
+ onClick={(e) => {
2730
+ e.stopPropagation();
2731
+ if (task.googleMeetLink) {
2732
+ globalThis.open(
2733
+ task.googleMeetLink,
2734
+ '_blank',
2735
+ 'noopener,noreferrer',
2736
+ );
2737
+ }
2738
+ }}
2739
+ className="inline-flex items-center gap-1.5 rounded-lg border border-blue-200 bg-blue-50 px-3 py-1.5 text-sm font-medium text-blue-700 transition-colors hover:bg-blue-100"
2740
+ >
2741
+ <Video className="h-4 w-4" />
2742
+ <span>Rejoindre Google Meet</span>
2743
+ <ExternalLink className="h-3 w-3" />
2744
+ </button>
2745
+ <button
2746
+ type="button"
2747
+ onClick={(e) => {
2748
+ e.stopPropagation();
2749
+ openEditMeetModal(task);
2750
+ }}
2751
+ 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"
2752
+ >
2753
+ Modifier le rendez-vous
2754
+ </button>
2755
+ </div>
1771
2756
  )}
1772
- <span className="text-sm font-medium text-gray-600">
1773
- {TASK_TYPE_LABELS[task.type]}
1774
- </span>
2757
+ <div className="mt-2 flex items-center gap-4 text-sm text-gray-500">
2758
+ <div className="flex items-center gap-1">
2759
+ <Clock className="h-4 w-4" />
2760
+ {formatTime(task.scheduledAt)}
2761
+ </div>
2762
+ <div className="flex items-center gap-1">
2763
+ <Calendar className="h-4 w-4" />
2764
+ {new Date(task.scheduledAt).toLocaleDateString('fr-FR')}
2765
+ </div>
2766
+ {isAdmin && (
2767
+ <div className="flex items-center gap-1">
2768
+ <User className="h-4 w-4" />
2769
+ {task.assignedUser?.name || 'Ancien utilisateur'}
2770
+ </div>
2771
+ )}
2772
+ </div>
1775
2773
  </div>
1776
- <h3
1777
- className={cn(
1778
- 'mt-1 text-base font-semibold',
1779
- task.completed ? 'text-gray-400 line-through' : 'text-gray-900',
1780
- )}
2774
+ </div>
2775
+ </div>
2776
+ </div>
2777
+ );
2778
+
2779
+ return (
2780
+ <div
2781
+ key={task.id}
2782
+ className="cursor-pointer rounded-xl border border-gray-200 bg-white p-4 shadow-sm transition-colors hover:border-blue-300 hover:shadow-md"
2783
+ onClick={() => openTaskDetailModal(task)}
2784
+ >
2785
+ {TaskContent}
2786
+ </div>
2787
+ );
2788
+ })}
2789
+ {googleEventsInDateRange.length > 0 && (
2790
+ <div className="overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm">
2791
+ <button
2792
+ type="button"
2793
+ onClick={() => setWeekGoogleEventsOpen((o) => !o)}
2794
+ className="flex w-full items-center justify-between gap-3 px-5 py-3.5 text-left transition-colors hover:bg-gray-50 sm:px-6"
2795
+ aria-expanded={weekGoogleEventsOpen}
2796
+ >
2797
+ <div className="flex min-w-0 flex-1 items-center gap-2.5">
2798
+ <Calendar className="h-5 w-5 shrink-0 text-blue-600" />
2799
+ <span className="font-semibold text-gray-900">
2800
+ Événements Google Calendar
2801
+ </span>
2802
+ <span className="shrink-0 rounded-full bg-blue-100 px-2 py-0.5 text-xs font-medium text-blue-800">
2803
+ {googleEventsInDateRange.length}
2804
+ </span>
2805
+ </div>
2806
+ <ChevronDown
2807
+ className={cn(
2808
+ 'h-5 w-5 shrink-0 text-gray-500 transition-transform duration-200',
2809
+ weekGoogleEventsOpen && 'rotate-180',
2810
+ )}
2811
+ />
2812
+ </button>
2813
+ {weekGoogleEventsOpen && (
2814
+ <div className="space-y-3 border-t border-gray-100 bg-slate-50/80 px-5 py-4 sm:px-6">
2815
+ {googleEventsInDateRange.map((ev) => (
2816
+ <div
2817
+ key={`gcal-week-${ev.calendarId}-${ev.googleEventId}`}
2818
+ className="cursor-pointer rounded-xl border border-solid px-5 py-4 shadow-sm transition-colors hover:brightness-[0.98] sm:px-6"
2819
+ style={{
2820
+ backgroundColor: gCalCardChrome.backgroundColor,
2821
+ borderColor: gCalCardChrome.borderColor,
2822
+ }}
2823
+ onClick={() => {
2824
+ if (ev.htmlLink) {
2825
+ globalThis.open(ev.htmlLink, '_blank', 'noopener,noreferrer');
2826
+ }
2827
+ }}
1781
2828
  >
1782
- {task.title || TASK_TYPE_LABELS[task.type]}
1783
- </h3>
1784
- {task.contact && (
1785
- <div className="mt-2 inline-flex max-w-full items-center gap-1.5 overflow-hidden rounded-md bg-blue-50 px-2.5 py-1 text-sm font-medium text-blue-700">
1786
- <User className="h-4 w-4 shrink-0" />
1787
- <span className="truncate">
1788
- {task.contact.firstName} {task.contact.lastName}
1789
- </span>
1790
- <Link
1791
- href={`/contacts/${task.contact.id}`}
1792
- onClick={(e) => e.stopPropagation()}
1793
- className="ml-1 inline-flex h-4 w-4 shrink-0 items-center justify-center rounded-full bg-blue-100 text-blue-800 hover:bg-blue-200"
1794
- title="Voir le contact"
1795
- >
1796
- <Eye className="h-3 w-3" />
1797
- </Link>
1798
- </div>
1799
- )}
1800
- {task.googleMeetLink && (
1801
- <div className="mt-2 flex flex-wrap items-center gap-2">
1802
- <button
1803
- type="button"
1804
- onClick={(e) => {
1805
- e.stopPropagation();
1806
- if (task.googleMeetLink) {
1807
- globalThis.open(
1808
- task.googleMeetLink,
1809
- '_blank',
1810
- 'noopener,noreferrer',
1811
- );
1812
- }
1813
- }}
1814
- className="inline-flex items-center gap-1.5 rounded-lg border border-blue-200 bg-blue-50 px-3 py-1.5 text-sm font-medium text-blue-700 transition-colors hover:bg-blue-100"
1815
- >
1816
- <Video className="h-4 w-4" />
1817
- <span>Rejoindre Google Meet</span>
1818
- <ExternalLink className="h-3 w-3" />
1819
- </button>
1820
- <button
1821
- type="button"
1822
- onClick={(e) => {
1823
- e.stopPropagation();
1824
- openEditMeetModal(task);
1825
- }}
1826
- 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"
1827
- >
1828
- Modifier le rendez-vous
1829
- </button>
1830
- </div>
1831
- )}
1832
- <div className="mt-2 flex items-center gap-4 text-sm text-gray-500">
1833
- <div className="flex items-center gap-1">
1834
- <Clock className="h-4 w-4" />
1835
- {formatTime(task.scheduledAt)}
1836
- </div>
1837
- <div className="flex items-center gap-1">
2829
+ <div
2830
+ className="flex items-center gap-2 text-sm font-medium"
2831
+ style={{ color: gCalCardChrome.iconColor }}
2832
+ >
1838
2833
  <Calendar className="h-4 w-4" />
1839
- {new Date(task.scheduledAt).toLocaleDateString('fr-FR')}
2834
+ <span>Google Calendar</span>
1840
2835
  </div>
1841
- {isAdmin && (
2836
+ <h3
2837
+ className="mt-1 text-base font-semibold"
2838
+ style={{ color: gCalCardChrome.color }}
2839
+ >
2840
+ {ev.title}
2841
+ </h3>
2842
+ <div
2843
+ className="mt-2 flex flex-wrap items-center gap-4 text-sm"
2844
+ style={{ color: gCalCardChrome.iconColor }}
2845
+ >
2846
+ <div className="flex items-center gap-1">
2847
+ <Clock className="h-4 w-4" />
2848
+ {ev.allDay ? 'Journée entière' : formatTime(ev.start)}
2849
+ </div>
1842
2850
  <div className="flex items-center gap-1">
1843
- <User className="h-4 w-4" />
1844
- {task.assignedUser?.name || 'Ancien utilisateur'}
2851
+ <Calendar className="h-4 w-4" />
2852
+ {new Date(ev.start).toLocaleDateString('fr-FR')}
1845
2853
  </div>
1846
- )}
2854
+ </div>
1847
2855
  </div>
1848
- </div>
2856
+ ))}
1849
2857
  </div>
1850
- </div>
1851
- </div>
1852
- );
1853
-
1854
- return (
1855
- <div
1856
- key={task.id}
1857
- className="cursor-pointer rounded-xl border border-gray-200 bg-white p-4 shadow-sm transition-colors hover:border-blue-300 hover:shadow-md"
1858
- onClick={() => toggleTaskComplete(task.id, task.completed)}
1859
- >
1860
- {TaskContent}
2858
+ )}
1861
2859
  </div>
1862
- );
1863
- })
2860
+ )}
2861
+ </>
1864
2862
  )}
1865
2863
  </div>
1866
2864
  </div>
@@ -1875,23 +2873,273 @@ export default function AgendaPage() {
1875
2873
  </div>
1876
2874
  ) : (
1877
2875
  <div className="space-y-3 p-4 sm:p-6 lg:p-8">
1878
- {filteredTasks.length === 0 ? (
2876
+ {filteredTasks.length === 0 && googleForDayList.length === 0 ? (
1879
2877
  <div className="rounded-xl border border-gray-200 bg-white p-8 text-center shadow-sm">
1880
- <p className="text-gray-500">Aucune tâche pour cette journée</p>
2878
+ <p className="text-gray-500">Aucun événement pour cette journée</p>
1881
2879
  </div>
1882
2880
  ) : (
1883
- filteredTasks.map((task) => {
2881
+ <>
2882
+ {filteredTasks.map((task) => {
2883
+ const TaskContent = (
2884
+ <div className="flex items-start justify-between">
2885
+ <div className="flex-1">
2886
+ <div className="flex items-center gap-2">
2887
+ <button
2888
+ type="button"
2889
+ onClick={(e) => {
2890
+ e.stopPropagation();
2891
+ toggleTaskComplete(task.id, task.completed);
2892
+ }}
2893
+ className="cursor-pointer text-gray-400 hover:text-blue-600"
2894
+ >
2895
+ {task.completed ? (
2896
+ <CheckCircle2 className="h-5 w-5 text-blue-600" />
2897
+ ) : (
2898
+ <Circle className="h-5 w-5" />
2899
+ )}
2900
+ </button>
2901
+ <div className="flex-1">
2902
+ <div className="flex items-center gap-2">
2903
+ {task.type === 'VIDEO_CONFERENCE' && (
2904
+ <Video className="h-4 w-4 text-green-600" />
2905
+ )}
2906
+ {task.type === 'MEETING' && (
2907
+ <Users className="h-4 w-4 text-blue-600" />
2908
+ )}
2909
+ {task.type === 'CALL' && (
2910
+ <Phone className="h-4 w-4 text-purple-600" />
2911
+ )}
2912
+ <span className="text-sm font-medium text-gray-600">
2913
+ {TASK_TYPE_LABELS[task.type]}
2914
+ </span>
2915
+ </div>
2916
+ <h3
2917
+ className={cn(
2918
+ 'mt-1 text-base font-semibold',
2919
+ task.completed ? 'text-gray-400 line-through' : 'text-gray-900',
2920
+ )}
2921
+ >
2922
+ {task.title || TASK_TYPE_LABELS[task.type]}
2923
+ </h3>
2924
+ {task.contact && (
2925
+ <div className="mt-2 inline-flex max-w-full items-center gap-1.5 overflow-hidden rounded-md bg-blue-50 px-2.5 py-1 text-sm font-medium text-blue-700">
2926
+ <User className="h-4 w-4 shrink-0" />
2927
+ <span className="truncate">
2928
+ {task.contact.firstName} {task.contact.lastName}
2929
+ </span>
2930
+ </div>
2931
+ )}
2932
+ {task.googleMeetLink && (
2933
+ <div className="mt-2 flex flex-wrap items-center gap-2">
2934
+ <button
2935
+ type="button"
2936
+ onClick={(e) => {
2937
+ e.stopPropagation();
2938
+ if (task.googleMeetLink) {
2939
+ globalThis.open(
2940
+ task.googleMeetLink,
2941
+ '_blank',
2942
+ 'noopener,noreferrer',
2943
+ );
2944
+ }
2945
+ }}
2946
+ className="inline-flex cursor-pointer items-center gap-1.5 rounded-lg border border-blue-200 bg-blue-50 px-3 py-1.5 text-sm font-medium text-blue-700 transition-colors hover:bg-blue-100"
2947
+ >
2948
+ <Video className="h-4 w-4" />
2949
+ <span>Rejoindre Google Meet</span>
2950
+ <ExternalLink className="h-3 w-3" />
2951
+ </button>
2952
+ <button
2953
+ type="button"
2954
+ onClick={(e) => {
2955
+ e.stopPropagation();
2956
+ openEditMeetModal(task);
2957
+ }}
2958
+ 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"
2959
+ >
2960
+ Modifier le rendez-vous
2961
+ </button>
2962
+ </div>
2963
+ )}
2964
+ <div className="mt-2 flex items-center gap-4 text-sm text-gray-500">
2965
+ <div className="flex items-center gap-1">
2966
+ <Clock className="h-4 w-4" />
2967
+ {formatTime(task.scheduledAt)}
2968
+ </div>
2969
+ <div className="flex items-center gap-1">
2970
+ <Calendar className="h-4 w-4" />
2971
+ {new Date(task.scheduledAt).toLocaleDateString('fr-FR')}
2972
+ </div>
2973
+ {isAdmin && (
2974
+ <div className="flex items-center gap-1">
2975
+ <User className="h-4 w-4" />
2976
+ {task.assignedUser?.name || 'Ancien utilisateur'}
2977
+ </div>
2978
+ )}
2979
+ </div>
2980
+ </div>
2981
+ </div>
2982
+ </div>
2983
+ </div>
2984
+ );
2985
+
2986
+ return (
2987
+ <div
2988
+ key={task.id}
2989
+ className="cursor-pointer rounded-xl border border-gray-200 bg-white p-4 shadow-sm transition-colors hover:border-blue-300 hover:shadow-md"
2990
+ onClick={() => openTaskDetailModal(task)}
2991
+ >
2992
+ {TaskContent}
2993
+ </div>
2994
+ );
2995
+ })}
2996
+ {googleForDayList.length > 0 && (
2997
+ <div className="overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm">
2998
+ <button
2999
+ type="button"
3000
+ onClick={() => setDayGoogleEventsOpen((o) => !o)}
3001
+ className="flex w-full items-center justify-between gap-3 px-5 py-3.5 text-left transition-colors hover:bg-gray-50 sm:px-6"
3002
+ aria-expanded={dayGoogleEventsOpen}
3003
+ >
3004
+ <div className="flex min-w-0 flex-1 items-center gap-2.5">
3005
+ <Calendar className="h-5 w-5 shrink-0 text-blue-600" />
3006
+ <span className="font-semibold text-gray-900">
3007
+ Événements Google Calendar
3008
+ </span>
3009
+ <span className="shrink-0 rounded-full bg-blue-100 px-2 py-0.5 text-xs font-medium text-blue-800">
3010
+ {googleForDayList.length}
3011
+ </span>
3012
+ </div>
3013
+ <ChevronDown
3014
+ className={cn(
3015
+ 'h-5 w-5 shrink-0 text-gray-500 transition-transform duration-200',
3016
+ dayGoogleEventsOpen && 'rotate-180',
3017
+ )}
3018
+ />
3019
+ </button>
3020
+ {dayGoogleEventsOpen && (
3021
+ <div className="space-y-3 border-t border-gray-100 bg-slate-50/80 px-5 py-4 sm:px-6">
3022
+ {googleForDayListAllDay.length > 0 && (
3023
+ <div
3024
+ className="rounded-xl border border-solid p-3 shadow-sm"
3025
+ style={{
3026
+ backgroundColor: gCalCardChrome.backgroundColor,
3027
+ borderColor: gCalCardChrome.borderColor,
3028
+ }}
3029
+ >
3030
+ <p
3031
+ className="mb-2 text-xs font-semibold tracking-wide uppercase"
3032
+ style={{ color: gCalCardChrome.iconColor }}
3033
+ >
3034
+ Toute la journée
3035
+ </p>
3036
+ <div className="flex flex-col gap-2">
3037
+ {googleForDayListAllDay.map((ev) => (
3038
+ <button
3039
+ key={`gcal-day-allday-${ev.calendarId}-${ev.googleEventId}`}
3040
+ type="button"
3041
+ className="w-full cursor-pointer rounded-lg border border-solid px-3 py-2 text-left text-sm font-semibold shadow-sm transition-colors hover:brightness-[0.97]"
3042
+ style={{
3043
+ backgroundColor: gCalChrome.backgroundColor,
3044
+ borderColor: gCalChrome.borderColor,
3045
+ color: gCalChrome.color,
3046
+ }}
3047
+ onClick={() => {
3048
+ if (ev.htmlLink) {
3049
+ globalThis.open(
3050
+ ev.htmlLink,
3051
+ '_blank',
3052
+ 'noopener,noreferrer',
3053
+ );
3054
+ }
3055
+ }}
3056
+ >
3057
+ <div className="flex items-center gap-2">
3058
+ <Calendar
3059
+ className="h-4 w-4 shrink-0"
3060
+ style={{ color: gCalChrome.iconColor }}
3061
+ />
3062
+ <span className="min-w-0 flex-1 truncate">{ev.title}</span>
3063
+ </div>
3064
+ </button>
3065
+ ))}
3066
+ </div>
3067
+ </div>
3068
+ )}
3069
+ {googleForDayListTimed.map((ev) => (
3070
+ <div
3071
+ key={`gcal-day-${ev.calendarId}-${ev.googleEventId}`}
3072
+ className="cursor-pointer rounded-xl border border-solid px-5 py-4 shadow-sm transition-colors hover:brightness-[0.98] sm:px-6"
3073
+ style={{
3074
+ backgroundColor: gCalCardChrome.backgroundColor,
3075
+ borderColor: gCalCardChrome.borderColor,
3076
+ }}
3077
+ onClick={() => {
3078
+ if (ev.htmlLink) {
3079
+ globalThis.open(ev.htmlLink, '_blank', 'noopener,noreferrer');
3080
+ }
3081
+ }}
3082
+ >
3083
+ <div
3084
+ className="flex items-center gap-2 text-sm font-medium"
3085
+ style={{ color: gCalCardChrome.iconColor }}
3086
+ >
3087
+ <Calendar className="h-4 w-4" />
3088
+ <span>Google Calendar</span>
3089
+ </div>
3090
+ <h3
3091
+ className="mt-1 text-base font-semibold"
3092
+ style={{ color: gCalCardChrome.color }}
3093
+ >
3094
+ {ev.title}
3095
+ </h3>
3096
+ <div
3097
+ className="mt-2 flex flex-wrap items-center gap-4 text-sm"
3098
+ style={{ color: gCalCardChrome.iconColor }}
3099
+ >
3100
+ <div className="flex items-center gap-1">
3101
+ <Clock className="h-4 w-4" />
3102
+ {ev.allDay ? 'Journée entière' : formatTime(ev.start)}
3103
+ </div>
3104
+ </div>
3105
+ </div>
3106
+ ))}
3107
+ </div>
3108
+ )}
3109
+ </div>
3110
+ )}
3111
+ </>
3112
+ )}
3113
+ </div>
3114
+ )}
3115
+ </>
3116
+ )}
3117
+
3118
+ {/* Liste des tâches du mois (en bas) — même padding horizontal que la vue semaine */}
3119
+ {view === 'month' && !loading && (
3120
+ <div className="mt-6 px-4 pb-4 sm:px-6 sm:pb-6 lg:px-8 lg:pb-8">
3121
+ <h2 className="mb-4 text-lg font-semibold text-gray-900">
3122
+ Toutes les tâches du mois
3123
+ </h2>
3124
+ <div className="space-y-3">
3125
+ {filteredTasks.length === 0 && googleEventsInDateRange.length === 0 ? (
3126
+ <div className="rounded-xl border border-gray-200 bg-white p-8 text-center shadow-sm">
3127
+ <p className="text-gray-500">Aucun événement pour ce mois</p>
3128
+ </div>
3129
+ ) : (
3130
+ <>
3131
+ {filteredTasks.map((task) => {
1884
3132
  const TaskContent = (
1885
- <div className="flex items-start justify-between">
1886
- <div className="flex-1">
1887
- <div className="flex items-center gap-2">
3133
+ <div className="flex items-start justify-between gap-4">
3134
+ <div className="min-w-0 flex-1">
3135
+ <div className="flex items-start gap-3">
1888
3136
  <button
1889
3137
  type="button"
1890
3138
  onClick={(e) => {
1891
3139
  e.stopPropagation();
1892
3140
  toggleTaskComplete(task.id, task.completed);
1893
3141
  }}
1894
- className="cursor-pointer text-gray-400 hover:text-blue-600"
3142
+ className="mt-0.5 shrink-0 cursor-pointer text-gray-400 hover:text-blue-600"
1895
3143
  >
1896
3144
  {task.completed ? (
1897
3145
  <CheckCircle2 className="h-5 w-5 text-blue-600" />
@@ -1928,6 +3176,14 @@ export default function AgendaPage() {
1928
3176
  <span className="truncate">
1929
3177
  {task.contact.firstName} {task.contact.lastName}
1930
3178
  </span>
3179
+ <Link
3180
+ href={`/contacts/${task.contact.id}`}
3181
+ onClick={(e) => e.stopPropagation()}
3182
+ className="ml-1 inline-flex h-4 w-4 shrink-0 items-center justify-center rounded-full bg-blue-100 text-blue-800 hover:bg-blue-200"
3183
+ title="Voir le contact"
3184
+ >
3185
+ <Eye className="h-3 w-3" />
3186
+ </Link>
1931
3187
  </div>
1932
3188
  )}
1933
3189
  {task.googleMeetLink && (
@@ -1944,7 +3200,7 @@ export default function AgendaPage() {
1944
3200
  );
1945
3201
  }
1946
3202
  }}
1947
- className="inline-flex cursor-pointer items-center gap-1.5 rounded-lg border border-blue-200 bg-blue-50 px-3 py-1.5 text-sm font-medium text-blue-700 transition-colors hover:bg-blue-100"
3203
+ className="inline-flex items-center gap-1.5 rounded-lg border border-blue-200 bg-blue-50 px-3 py-1.5 text-sm font-medium text-blue-700 transition-colors hover:bg-blue-100"
1948
3204
  >
1949
3205
  <Video className="h-4 w-4" />
1950
3206
  <span>Rejoindre Google Meet</span>
@@ -1987,153 +3243,86 @@ export default function AgendaPage() {
1987
3243
  return (
1988
3244
  <div
1989
3245
  key={task.id}
1990
- className="cursor-pointer rounded-xl border border-gray-200 bg-white p-4 shadow-sm transition-colors hover:border-blue-300 hover:shadow-md"
3246
+ className="cursor-pointer rounded-xl border border-gray-200 bg-white px-5 py-4 shadow-sm transition-colors hover:border-blue-300 hover:shadow-md sm:px-6"
1991
3247
  onClick={() => openTaskDetailModal(task)}
1992
3248
  >
1993
3249
  {TaskContent}
1994
3250
  </div>
1995
3251
  );
1996
- })
1997
- )}
1998
- </div>
1999
- )}
2000
- </>
2001
- )}
2002
-
2003
- {/* Liste des tâches du mois (en bas) */}
2004
- {view === 'month' && !loading && (
2005
- <div className="mt-6">
2006
- <h2 className="mb-4 text-lg font-semibold text-gray-900">
2007
- Toutes les tâches du mois
2008
- </h2>
2009
- <div className="space-y-3">
2010
- {filteredTasks.length === 0 ? (
2011
- <div className="rounded-xl border border-gray-200 bg-white p-8 text-center shadow-sm">
2012
- <p className="text-gray-500">Aucune tâche pour ce mois</p>
2013
- </div>
2014
- ) : (
2015
- filteredTasks.map((task) => {
2016
- const TaskContent = (
2017
- <div className="flex items-start justify-between">
2018
- <div className="flex-1">
2019
- <div className="flex items-center gap-2">
2020
- <button
2021
- type="button"
2022
- onClick={(e) => {
2023
- e.stopPropagation();
2024
- toggleTaskComplete(task.id, task.completed);
2025
- }}
2026
- className="cursor-pointer text-gray-400 hover:text-blue-600"
2027
- >
2028
- {task.completed ? (
2029
- <CheckCircle2 className="h-5 w-5 text-blue-600" />
2030
- ) : (
2031
- <Circle className="h-5 w-5" />
2032
- )}
2033
- </button>
2034
- <div className="flex-1">
2035
- <div className="flex items-center gap-2">
2036
- {task.type === 'VIDEO_CONFERENCE' && (
2037
- <Video className="h-4 w-4 text-green-600" />
2038
- )}
2039
- {task.type === 'MEETING' && (
2040
- <Users className="h-4 w-4 text-blue-600" />
2041
- )}
2042
- {task.type === 'CALL' && (
2043
- <Phone className="h-4 w-4 text-purple-600" />
2044
- )}
2045
- <span className="text-sm font-medium text-gray-600">
2046
- {TASK_TYPE_LABELS[task.type]}
2047
- </span>
2048
- </div>
2049
- <h3
2050
- className={cn(
2051
- 'mt-1 text-base font-semibold',
2052
- task.completed ? 'text-gray-400 line-through' : 'text-gray-900',
2053
- )}
3252
+ })}
3253
+ {googleEventsInDateRange.length > 0 && (
3254
+ <div className="overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm">
3255
+ <button
3256
+ type="button"
3257
+ onClick={() => setMonthGoogleEventsOpen((o) => !o)}
3258
+ className="flex w-full items-center justify-between gap-3 px-5 py-3.5 text-left transition-colors hover:bg-gray-50 sm:px-6"
3259
+ aria-expanded={monthGoogleEventsOpen}
3260
+ >
3261
+ <div className="flex min-w-0 flex-1 items-center gap-2.5">
3262
+ <Calendar className="h-5 w-5 shrink-0 text-blue-600" />
3263
+ <span className="font-semibold text-gray-900">
3264
+ Événements Google Calendar
3265
+ </span>
3266
+ <span className="shrink-0 rounded-full bg-blue-100 px-2 py-0.5 text-xs font-medium text-blue-800">
3267
+ {googleEventsInDateRange.length}
3268
+ </span>
3269
+ </div>
3270
+ <ChevronDown
3271
+ className={cn(
3272
+ 'h-5 w-5 shrink-0 text-gray-500 transition-transform duration-200',
3273
+ monthGoogleEventsOpen && 'rotate-180',
3274
+ )}
3275
+ />
3276
+ </button>
3277
+ {monthGoogleEventsOpen && (
3278
+ <div className="space-y-3 border-t border-gray-100 bg-slate-50/80 px-5 py-4 sm:px-6">
3279
+ {googleEventsInDateRange.map((ev) => (
3280
+ <div
3281
+ key={`gcal-month-${ev.calendarId}-${ev.googleEventId}`}
3282
+ className="cursor-pointer rounded-xl border border-solid px-5 py-4 shadow-sm transition-colors hover:brightness-[0.98] sm:px-6"
3283
+ style={{
3284
+ backgroundColor: gCalCardChrome.backgroundColor,
3285
+ borderColor: gCalCardChrome.borderColor,
3286
+ }}
3287
+ onClick={() => {
3288
+ if (ev.htmlLink) {
3289
+ globalThis.open(ev.htmlLink, '_blank', 'noopener,noreferrer');
3290
+ }
3291
+ }}
2054
3292
  >
2055
- {task.title || TASK_TYPE_LABELS[task.type]}
2056
- </h3>
2057
- {task.contact && (
2058
- <div className="mt-2 inline-flex max-w-full items-center gap-1.5 overflow-hidden rounded-md bg-blue-50 px-2.5 py-1 text-sm font-medium text-blue-700">
2059
- <User className="h-4 w-4 shrink-0" />
2060
- <span className="truncate">
2061
- {task.contact.firstName} {task.contact.lastName}
2062
- </span>
2063
- <Link
2064
- href={`/contacts/${task.contact.id}`}
2065
- onClick={(e) => e.stopPropagation()}
2066
- className="ml-1 inline-flex h-4 w-4 shrink-0 items-center justify-center rounded-full bg-blue-100 text-blue-800 hover:bg-blue-200"
2067
- title="Voir le contact"
2068
- >
2069
- <Eye className="h-3 w-3" />
2070
- </Link>
2071
- </div>
2072
- )}
2073
- {task.googleMeetLink && (
2074
- <div className="mt-2 flex flex-wrap items-center gap-2">
2075
- <button
2076
- type="button"
2077
- onClick={(e) => {
2078
- e.stopPropagation();
2079
- if (task.googleMeetLink) {
2080
- globalThis.open(
2081
- task.googleMeetLink,
2082
- '_blank',
2083
- 'noopener,noreferrer',
2084
- );
2085
- }
2086
- }}
2087
- className="inline-flex items-center gap-1.5 rounded-lg border border-blue-200 bg-blue-50 px-3 py-1.5 text-sm font-medium text-blue-700 transition-colors hover:bg-blue-100"
2088
- >
2089
- <Video className="h-4 w-4" />
2090
- <span>Rejoindre Google Meet</span>
2091
- <ExternalLink className="h-3 w-3" />
2092
- </button>
2093
- <button
2094
- type="button"
2095
- onClick={(e) => {
2096
- e.stopPropagation();
2097
- openEditMeetModal(task);
2098
- }}
2099
- 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"
2100
- >
2101
- Modifier le rendez-vous
2102
- </button>
2103
- </div>
2104
- )}
2105
- <div className="mt-2 flex items-center gap-4 text-sm text-gray-500">
2106
- <div className="flex items-center gap-1">
2107
- <Clock className="h-4 w-4" />
2108
- {formatTime(task.scheduledAt)}
2109
- </div>
2110
- <div className="flex items-center gap-1">
3293
+ <div
3294
+ className="flex items-center gap-2 text-sm font-medium"
3295
+ style={{ color: gCalCardChrome.iconColor }}
3296
+ >
2111
3297
  <Calendar className="h-4 w-4" />
2112
- {new Date(task.scheduledAt).toLocaleDateString('fr-FR')}
3298
+ <span>Google Calendar</span>
2113
3299
  </div>
2114
- {isAdmin && (
3300
+ <h3
3301
+ className="mt-1 text-base font-semibold"
3302
+ style={{ color: gCalCardChrome.color }}
3303
+ >
3304
+ {ev.title}
3305
+ </h3>
3306
+ <div
3307
+ className="mt-2 flex flex-wrap items-center gap-4 text-sm"
3308
+ style={{ color: gCalCardChrome.iconColor }}
3309
+ >
2115
3310
  <div className="flex items-center gap-1">
2116
- <User className="h-4 w-4" />
2117
- {task.assignedUser?.name || 'Ancien utilisateur'}
3311
+ <Clock className="h-4 w-4" />
3312
+ {ev.allDay ? 'Journée entière' : formatTime(ev.start)}
2118
3313
  </div>
2119
- )}
3314
+ <div className="flex items-center gap-1">
3315
+ <Calendar className="h-4 w-4" />
3316
+ {new Date(ev.start).toLocaleDateString('fr-FR')}
3317
+ </div>
3318
+ </div>
2120
3319
  </div>
2121
- </div>
3320
+ ))}
2122
3321
  </div>
2123
- </div>
2124
- </div>
2125
- );
2126
-
2127
- return (
2128
- <div
2129
- key={task.id}
2130
- className="cursor-pointer rounded-xl border border-gray-200 bg-white p-4 shadow-sm transition-colors hover:border-blue-300 hover:shadow-md"
2131
- onClick={() => openTaskDetailModal(task)}
2132
- >
2133
- {TaskContent}
3322
+ )}
2134
3323
  </div>
2135
- );
2136
- })
3324
+ )}
3325
+ </>
2137
3326
  )}
2138
3327
  </div>
2139
3328
  </div>
@@ -2142,8 +3331,8 @@ export default function AgendaPage() {
2142
3331
 
2143
3332
  {/* Modal d'édition de Google Meet */}
2144
3333
  {showEditMeetModal && editingMeetTask && (
2145
- <div className="fixed inset-0 z-50 flex items-center justify-center bg-gray-500/20 p-4 backdrop-blur-sm sm:p-6">
2146
- <div className="flex max-h-[90vh] w-full max-w-2xl flex-col rounded-lg bg-white p-6 shadow-xl sm:p-8">
3334
+ <div className="ui-fade-in fixed inset-0 z-50 flex items-center justify-center bg-gray-500/20 p-4 backdrop-blur-sm sm:p-6">
3335
+ <div className="ui-scale-in flex max-h-[90vh] w-full max-w-2xl flex-col rounded-lg bg-white p-6 shadow-xl sm:p-8">
2147
3336
  {/* En-tête fixe */}
2148
3337
  <div className="shrink-0 border-b border-gray-100 pb-4">
2149
3338
  <div className="flex items-center justify-between">
@@ -2183,55 +3372,21 @@ export default function AgendaPage() {
2183
3372
  className="flex-1 space-y-4 overflow-y-auto pt-4 [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
2184
3373
  >
2185
3374
  <div className="space-y-2">
2186
- <p className="text-sm font-medium text-gray-700">Date & heure *</p>
2187
- <div className="grid grid-cols-[3fr,2fr] gap-2">
2188
- <input
2189
- type="date"
2190
- required
2191
- value={
2192
- editMeetData.scheduledAt
2193
- ? editMeetData.scheduledAt.split('T')[0]
2194
- : new Date(editingMeetTask.scheduledAt).toISOString().split('T')[0]
2195
- }
2196
- onChange={(e) => {
2197
- const time =
2198
- editMeetData.scheduledAt && editMeetData.scheduledAt.includes('T')
2199
- ? editMeetData.scheduledAt.split('T')[1]
2200
- : new Date(editingMeetTask.scheduledAt)
2201
- .toISOString()
2202
- .split('T')[1]
2203
- .slice(0, 5);
2204
- setEditMeetData({
2205
- ...editMeetData,
2206
- scheduledAt: `${e.target.value}T${time || '09:00'}`,
2207
- });
2208
- }}
2209
- className="block w-full rounded-xl border border-gray-300 px-3 py-2 text-sm text-gray-900 shadow-sm focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
2210
- />
2211
- <input
2212
- type="time"
2213
- required
2214
- value={
2215
- editMeetData.scheduledAt && editMeetData.scheduledAt.includes('T')
2216
- ? editMeetData.scheduledAt.split('T')[1].slice(0, 5)
2217
- : new Date(editingMeetTask.scheduledAt)
2218
- .toISOString()
2219
- .split('T')[1]
2220
- .slice(0, 5)
2221
- }
2222
- onChange={(e) => {
2223
- const datePart =
2224
- editMeetData.scheduledAt && editMeetData.scheduledAt.includes('T')
2225
- ? editMeetData.scheduledAt.split('T')[0]
2226
- : new Date(editingMeetTask.scheduledAt).toISOString().split('T')[0];
2227
- setEditMeetData({
2228
- ...editMeetData,
2229
- scheduledAt: `${datePart}T${e.target.value || '09:00'}`,
2230
- });
2231
- }}
2232
- className="block w-full rounded-xl border border-gray-300 px-3 py-2 text-sm text-gray-900 shadow-sm focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
2233
- />
2234
- </div>
3375
+ <label className="text-sm font-medium text-gray-700">Date & heure *</label>
3376
+ <DateTimePicker
3377
+ required
3378
+ value={
3379
+ editMeetData.scheduledAt
3380
+ ? toLocalDateTimeInput(editMeetData.scheduledAt)
3381
+ : ''
3382
+ }
3383
+ onChange={(v) =>
3384
+ setEditMeetData({
3385
+ ...editMeetData,
3386
+ scheduledAt: v,
3387
+ })
3388
+ }
3389
+ />
2235
3390
  </div>
2236
3391
 
2237
3392
  <div className="space-y-2">
@@ -2244,7 +3399,7 @@ export default function AgendaPage() {
2244
3399
  durationMinutes: Number(e.target.value),
2245
3400
  })
2246
3401
  }
2247
- className="mt-1 block w-full cursor-pointer rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
3402
+ className="mt-1 block w-full cursor-pointer rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
2248
3403
  >
2249
3404
  <option value={15}>15 minutes</option>
2250
3405
  <option value={30}>30 minutes</option>
@@ -2270,7 +3425,6 @@ export default function AgendaPage() {
2270
3425
  </a>
2271
3426
  </div>
2272
3427
  )}
2273
-
2274
3428
  </form>
2275
3429
 
2276
3430
  {/* Pied de modal fixe */}
@@ -2291,7 +3445,7 @@ export default function AgendaPage() {
2291
3445
  type="submit"
2292
3446
  form="edit-meet-form"
2293
3447
  disabled={editMeetLoading}
2294
- className="w-full cursor-pointer rounded-xl bg-blue-600 px-4 py-2.5 text-sm font-medium text-white shadow-sm transition-colors hover:bg-blue-700 focus:ring-2 focus:ring-gray-400/30 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto"
3448
+ className="w-full cursor-pointer rounded-xl bg-blue-600 px-4 py-2.5 text-sm font-medium text-white shadow-sm transition-colors hover:bg-blue-700 focus:ring-2 focus:ring-gray-400/30 focus:ring-offset-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto"
2295
3449
  >
2296
3450
  {editMeetLoading ? 'Enregistrement...' : 'Enregistrer les modifications'}
2297
3451
  </button>
@@ -2303,8 +3457,8 @@ export default function AgendaPage() {
2303
3457
 
2304
3458
  {/* Modal de création de tâche (générique, non liée à un contact) */}
2305
3459
  {showCreateTaskModal && (
2306
- <div className="fixed inset-0 z-50 flex items-center justify-center bg-gray-500/20 p-6 backdrop-blur-sm sm:p-8">
2307
- <div className="flex max-h-[90vh] w-full max-w-4xl flex-col rounded-2xl bg-white p-6 shadow-xl">
3460
+ <div className="ui-fade-in fixed inset-0 z-50 flex items-center justify-center bg-gray-500/20 p-6 backdrop-blur-sm sm:p-8">
3461
+ <div className="ui-scale-in flex max-h-[90vh] w-full max-w-4xl flex-col rounded-2xl bg-white p-6 shadow-xl">
2308
3462
  {/* En-tête fixe */}
2309
3463
  <div className="shrink-0 border-b border-gray-100 pb-4">
2310
3464
  <div className="flex items-start justify-between gap-4">
@@ -2329,6 +3483,7 @@ export default function AgendaPage() {
2329
3483
  reminderMinutesBefore: null,
2330
3484
  contactId: '',
2331
3485
  addToGoogleCalendar: false,
3486
+ googleCalendarId: '',
2332
3487
  });
2333
3488
  }}
2334
3489
  className="cursor-pointer rounded-full p-2 text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-600"
@@ -2343,28 +3498,73 @@ export default function AgendaPage() {
2343
3498
  <form
2344
3499
  id="create-task-form"
2345
3500
  onSubmit={handleCreateTaskFromAgenda}
2346
- 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"
3501
+ className="flex-1 space-y-4 overflow-y-auto px-2 pt-3 pb-2 [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
2347
3502
  >
2348
3503
  {/* Ajouter à Google Calendar */}
2349
- <div className="flex items-center gap-2 rounded-lg border border-gray-200 bg-gray-50 p-3">
2350
- <input
2351
- type="checkbox"
2352
- id="task-add-to-calendar"
2353
- checked={createTaskData.addToGoogleCalendar}
2354
- onChange={(e) =>
2355
- setCreateTaskData({
2356
- ...createTaskData,
2357
- addToGoogleCalendar: e.target.checked,
2358
- })
2359
- }
2360
- className="h-4 w-4 cursor-pointer rounded border-gray-300 text-blue-600 focus:ring-2 focus:ring-gray-400/30"
2361
- />
2362
- <label
2363
- htmlFor="task-add-to-calendar"
2364
- className="cursor-pointer text-sm font-medium text-gray-700"
2365
- >
2366
- Ajouter à Google Calendar
2367
- </label>
3504
+ <div className="space-y-3 rounded-lg border border-gray-200 bg-gray-50 p-3">
3505
+ <div className="flex items-center gap-2">
3506
+ <input
3507
+ type="checkbox"
3508
+ id="task-add-to-calendar"
3509
+ checked={createTaskData.addToGoogleCalendar}
3510
+ onChange={(e) => {
3511
+ const checked = e.target.checked;
3512
+ setCreateTaskData({
3513
+ ...createTaskData,
3514
+ addToGoogleCalendar: checked,
3515
+ googleCalendarId:
3516
+ checked &&
3517
+ !createTaskData.googleCalendarId &&
3518
+ targetGoogleCalendars.length > 0
3519
+ ? pickDefaultCalendarId(gCalPayload, targetGoogleCalendars)
3520
+ : createTaskData.googleCalendarId,
3521
+ });
3522
+ }}
3523
+ className="h-4 w-4 cursor-pointer rounded border-gray-300 text-blue-600 focus:ring-2 focus:ring-gray-400/30"
3524
+ />
3525
+ <label
3526
+ htmlFor="task-add-to-calendar"
3527
+ className="cursor-pointer text-sm font-medium text-gray-700"
3528
+ >
3529
+ Ajouter à Google Calendar
3530
+ </label>
3531
+ </div>
3532
+ {gCalPayload?.needsGoogleReconnect && (
3533
+ <p className="text-xs text-amber-800">
3534
+ Reconnectez Google dans Paramètres → Intégrations pour lister vos calendriers
3535
+ partagés.
3536
+ </p>
3537
+ )}
3538
+ {googleConnected &&
3539
+ createTaskData.addToGoogleCalendar &&
3540
+ targetGoogleCalendars.length > 0 && (
3541
+ <div>
3542
+ <label
3543
+ htmlFor="task-google-calendar"
3544
+ className="block text-sm font-medium text-gray-700"
3545
+ >
3546
+ Calendrier cible
3547
+ </label>
3548
+ <select
3549
+ id="task-google-calendar"
3550
+ value={createTaskData.googleCalendarId || 'primary'}
3551
+ onChange={(e) =>
3552
+ setCreateTaskData({
3553
+ ...createTaskData,
3554
+ googleCalendarId: e.target.value,
3555
+ })
3556
+ }
3557
+ className="mt-1 block w-full cursor-pointer rounded-xl border border-gray-300 bg-white px-4 py-2.5 text-sm text-gray-900 shadow-sm focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
3558
+ >
3559
+ {targetGoogleCalendars.map((c) => (
3560
+ <option key={c.id} value={c.id}>
3561
+ {c.summary}
3562
+ {c.primary ? ' (principal)' : ''}
3563
+ </option>
3564
+ ))}
3565
+ </select>
3566
+ </div>
3567
+ )}
2368
3568
  </div>
2369
3569
 
2370
3570
  {/* Contact (optionnel) */}
@@ -2407,7 +3607,7 @@ export default function AgendaPage() {
2407
3607
  placeholder="Rechercher un contact..."
2408
3608
  value={taskContactSearch}
2409
3609
  onChange={(e) => setTaskContactSearch(e.target.value)}
2410
- className="w-full rounded-lg border border-gray-300 py-2 pr-3 pl-9 text-sm text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
3610
+ className="w-full rounded-lg border border-gray-300 py-2 pr-3 pl-9 text-sm text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
2411
3611
  autoFocus
2412
3612
  />
2413
3613
  </div>
@@ -2479,56 +3679,27 @@ export default function AgendaPage() {
2479
3679
  title: e.target.value,
2480
3680
  })
2481
3681
  }
2482
- 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:ring-2 focus:ring-gray-400/30 focus:outline-none"
3682
+ 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:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
2483
3683
  placeholder="Ex : Relance client"
2484
3684
  />
2485
3685
  </div>
2486
3686
 
2487
- {/* Date & heure */}
2488
- <div className="grid gap-4 md:grid-cols-2">
2489
- <div className="space-y-2">
2490
- <p className="text-sm font-medium text-gray-700">Date & heure *</p>
2491
- <div className="grid grid-cols-[3fr,2fr] gap-2">
2492
- <input
2493
- type="date"
2494
- required
2495
- value={
2496
- createTaskData.scheduledAt ? createTaskData.scheduledAt.split('T')[0] : ''
2497
- }
2498
- onChange={(e) => {
2499
- const time =
2500
- createTaskData.scheduledAt && createTaskData.scheduledAt.includes('T')
2501
- ? createTaskData.scheduledAt.split('T')[1]
2502
- : '09:00';
2503
- setCreateTaskData({
2504
- ...createTaskData,
2505
- scheduledAt: `${e.target.value}T${time}`,
2506
- });
2507
- }}
2508
- className="block w-full rounded-xl border border-gray-300 px-2 py-1.5 text-xs text-gray-900 shadow-sm focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
2509
- />
2510
- <input
2511
- type="time"
2512
- required
2513
- value={
2514
- createTaskData.scheduledAt && createTaskData.scheduledAt.includes('T')
2515
- ? createTaskData.scheduledAt.split('T')[1].slice(0, 5)
2516
- : ''
2517
- }
2518
- onChange={(e) => {
2519
- const datePart =
2520
- createTaskData.scheduledAt && createTaskData.scheduledAt.includes('T')
2521
- ? createTaskData.scheduledAt.split('T')[0]
2522
- : new Date().toISOString().split('T')[0];
2523
- setCreateTaskData({
2524
- ...createTaskData,
2525
- scheduledAt: `${datePart}T${e.target.value || '09:00'}`,
2526
- });
2527
- }}
2528
- className="block w-full rounded-xl border border-gray-300 px-2 py-1.5 text-xs text-gray-900 shadow-sm focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
2529
- />
2530
- </div>
2531
- </div>
3687
+ <div className="space-y-2">
3688
+ <label className="text-sm font-medium text-gray-700">Date & heure *</label>
3689
+ <DateTimePicker
3690
+ required
3691
+ value={
3692
+ createTaskData.scheduledAt
3693
+ ? toLocalDateTimeInput(createTaskData.scheduledAt)
3694
+ : ''
3695
+ }
3696
+ onChange={(v) =>
3697
+ setCreateTaskData({
3698
+ ...createTaskData,
3699
+ scheduledAt: v,
3700
+ })
3701
+ }
3702
+ />
2532
3703
  </div>
2533
3704
 
2534
3705
  {/* Rappel */}
@@ -2542,7 +3713,7 @@ export default function AgendaPage() {
2542
3713
  reminderMinutesBefore: e.target.value ? Number(e.target.value) : null,
2543
3714
  })
2544
3715
  }
2545
- className="mt-1 block w-full cursor-pointer rounded-xl border border-gray-300 px-4 py-2.5 text-sm text-gray-900 shadow-sm focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
3716
+ className="mt-1 block w-full cursor-pointer rounded-xl border border-gray-300 px-4 py-2.5 text-sm text-gray-900 shadow-sm focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
2546
3717
  >
2547
3718
  <option value="">Aucun rappel</option>
2548
3719
  <option value="5">5 minutes avant</option>
@@ -2565,11 +3736,10 @@ export default function AgendaPage() {
2565
3736
  })
2566
3737
  }
2567
3738
  rows={4}
2568
- 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:ring-2 focus:ring-gray-400/30 focus:outline-none"
3739
+ 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:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
2569
3740
  placeholder="Ajoutez les détails de la tâche (contexte, actions à mener, etc.)"
2570
3741
  />
2571
3742
  </div>
2572
-
2573
3743
  </form>
2574
3744
 
2575
3745
  {/* Pied de modal fixe */}
@@ -2588,6 +3758,7 @@ export default function AgendaPage() {
2588
3758
  reminderMinutesBefore: null,
2589
3759
  contactId: '',
2590
3760
  addToGoogleCalendar: true,
3761
+ googleCalendarId: '',
2591
3762
  });
2592
3763
  }}
2593
3764
  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"
@@ -2598,7 +3769,7 @@ export default function AgendaPage() {
2598
3769
  type="submit"
2599
3770
  form="create-task-form"
2600
3771
  disabled={creatingTask}
2601
- className="w-full cursor-pointer rounded-xl bg-blue-600 px-4 py-2.5 text-sm font-medium text-white shadow-sm transition-colors hover:bg-blue-700 focus:ring-2 focus:ring-gray-400/30 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto"
3772
+ className="w-full cursor-pointer rounded-xl bg-blue-600 px-4 py-2.5 text-sm font-medium text-white shadow-sm transition-colors hover:bg-blue-700 focus:ring-2 focus:ring-gray-400/30 focus:ring-offset-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto"
2602
3773
  >
2603
3774
  {creatingTask ? 'Création...' : 'Créer la tâche'}
2604
3775
  </button>
@@ -2610,8 +3781,8 @@ export default function AgendaPage() {
2610
3781
 
2611
3782
  {/* Modal de création de Google Meet */}
2612
3783
  {showCreateMeetModal && (
2613
- <div className="fixed inset-0 z-50 flex items-center justify-center bg-gray-500/20 p-4 backdrop-blur-sm sm:p-6">
2614
- <div className="flex max-h-[90vh] w-full max-w-5xl flex-col rounded-lg bg-white p-6 shadow-xl sm:p-8">
3784
+ <div className="ui-fade-in fixed inset-0 z-50 flex items-center justify-center bg-gray-500/20 p-4 backdrop-blur-sm sm:p-6">
3785
+ <div className="ui-scale-in flex max-h-[90vh] w-full max-w-5xl flex-col rounded-lg bg-white p-6 shadow-xl sm:p-8">
2615
3786
  {/* En-tête fixe */}
2616
3787
  <div className="shrink-0 border-b border-gray-100 pb-4">
2617
3788
  <div className="flex items-center justify-between">
@@ -2631,8 +3802,10 @@ export default function AgendaPage() {
2631
3802
  internalNote: '',
2632
3803
  contactId: '',
2633
3804
  addToGoogleCalendar: true,
3805
+ googleCalendarId: '',
2634
3806
  });
2635
3807
  setMeetError('');
3808
+ setMeetErrorConfigLink(null);
2636
3809
  }}
2637
3810
  className="cursor-pointer rounded-lg p-2 text-gray-400 transition-colors hover:bg-gray-100"
2638
3811
  type="button"
@@ -2648,6 +3821,9 @@ export default function AgendaPage() {
2648
3821
  onSubmit={handleCreateMeet}
2649
3822
  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"
2650
3823
  >
3824
+ {meetError && meetErrorConfigLink && (
3825
+ <ConfigErrorAlert message={meetError} configLink={meetErrorConfigLink} />
3826
+ )}
2651
3827
  {/* Contact (optionnel) */}
2652
3828
  <div className="relative" ref={meetContactRef}>
2653
3829
  <label className="block text-sm font-medium text-gray-700">
@@ -2686,7 +3862,7 @@ export default function AgendaPage() {
2686
3862
  placeholder="Rechercher un contact..."
2687
3863
  value={meetContactSearch}
2688
3864
  onChange={(e) => setMeetContactSearch(e.target.value)}
2689
- className="w-full rounded-lg border border-gray-300 py-2 pr-3 pl-9 text-sm text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
3865
+ className="w-full rounded-lg border border-gray-300 py-2 pr-3 pl-9 text-sm text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
2690
3866
  autoFocus
2691
3867
  />
2692
3868
  </div>
@@ -2753,53 +3929,23 @@ export default function AgendaPage() {
2753
3929
  required
2754
3930
  value={meetData.title}
2755
3931
  onChange={(e) => setMeetData({ ...meetData, title: e.target.value })}
2756
- className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
3932
+ className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
2757
3933
  placeholder="Ex: Rendez-vous avec..."
2758
3934
  />
2759
3935
  </div>
2760
3936
 
2761
3937
  <div className="grid gap-4 md:grid-cols-2">
2762
3938
  <div className="space-y-2">
2763
- <p className="text-sm font-medium text-gray-700">Date & heure *</p>
2764
- <div className="grid grid-cols-[3fr,2fr] gap-2">
2765
- <input
2766
- type="date"
2767
- required
2768
- value={meetData.scheduledAt ? meetData.scheduledAt.split('T')[0] : ''}
2769
- onChange={(e) => {
2770
- const time =
2771
- meetData.scheduledAt && meetData.scheduledAt.includes('T')
2772
- ? meetData.scheduledAt.split('T')[1]
2773
- : '';
2774
- setMeetData({
2775
- ...meetData,
2776
- scheduledAt: time
2777
- ? `${e.target.value}T${time}`
2778
- : `${e.target.value}T09:00`,
2779
- });
2780
- }}
2781
- className="block w-full rounded-xl border border-gray-300 px-2 py-1.5 text-xs text-gray-900 shadow-sm focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
2782
- />
2783
- <input
2784
- type="time"
2785
- value={
2786
- meetData.scheduledAt && meetData.scheduledAt.includes('T')
2787
- ? meetData.scheduledAt.split('T')[1].slice(0, 5)
2788
- : ''
2789
- }
2790
- onChange={(e) => {
2791
- const datePart =
2792
- meetData.scheduledAt && meetData.scheduledAt.includes('T')
2793
- ? meetData.scheduledAt.split('T')[0]
2794
- : new Date().toISOString().split('T')[0];
2795
- setMeetData({
2796
- ...meetData,
2797
- scheduledAt: `${datePart}T${e.target.value || '09:00'}`,
2798
- });
2799
- }}
2800
- className="block w-full rounded-xl border border-gray-300 px-2 py-1.5 text-xs text-gray-900 shadow-sm focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
2801
- />
2802
- </div>
3939
+ <label className="text-sm font-medium text-gray-700">Date & heure *</label>
3940
+ <DateTimePicker
3941
+ required
3942
+ value={
3943
+ meetData.scheduledAt
3944
+ ? toLocalDateTimeInput(meetData.scheduledAt)
3945
+ : ''
3946
+ }
3947
+ onChange={(v) => setMeetData({ ...meetData, scheduledAt: v })}
3948
+ />
2803
3949
  </div>
2804
3950
 
2805
3951
  <div className="space-y-2">
@@ -2812,7 +3958,7 @@ export default function AgendaPage() {
2812
3958
  durationMinutes: Number(e.target.value),
2813
3959
  })
2814
3960
  }
2815
- className="mt-1 block w-full cursor-pointer rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
3961
+ className="mt-1 block w-full cursor-pointer rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
2816
3962
  >
2817
3963
  <option value="15">15 minutes</option>
2818
3964
  <option value="30">30 minutes</option>
@@ -2824,6 +3970,32 @@ export default function AgendaPage() {
2824
3970
  </div>
2825
3971
  </div>
2826
3972
 
3973
+ {targetGoogleCalendars.length > 0 && meetData.addToGoogleCalendar && (
3974
+ <div className="rounded-lg border border-gray-200 bg-gray-50 p-3">
3975
+ <label
3976
+ htmlFor="meet-google-calendar"
3977
+ className="block text-sm font-medium text-gray-700"
3978
+ >
3979
+ Calendrier Google (événement + lien Meet)
3980
+ </label>
3981
+ <select
3982
+ id="meet-google-calendar"
3983
+ value={meetData.googleCalendarId || 'primary'}
3984
+ onChange={(e) =>
3985
+ setMeetData({ ...meetData, googleCalendarId: e.target.value })
3986
+ }
3987
+ className="mt-1 block w-full cursor-pointer rounded-lg border border-gray-300 bg-white px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
3988
+ >
3989
+ {targetGoogleCalendars.map((c) => (
3990
+ <option key={c.id} value={c.id}>
3991
+ {c.summary}
3992
+ {c.primary ? ' (principal)' : ''}
3993
+ </option>
3994
+ ))}
3995
+ </select>
3996
+ </div>
3997
+ )}
3998
+
2827
3999
  <div>
2828
4000
  <label className="block text-sm font-medium text-gray-700">Rappel</label>
2829
4001
  <select
@@ -2834,7 +4006,7 @@ export default function AgendaPage() {
2834
4006
  reminderMinutesBefore: e.target.value ? Number(e.target.value) : null,
2835
4007
  })
2836
4008
  }
2837
- className="mt-1 block w-full cursor-pointer rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
4009
+ className="mt-1 block w-full cursor-pointer rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
2838
4010
  >
2839
4011
  <option value="">Aucun rappel</option>
2840
4012
  <option value="5">5 minutes avant</option>
@@ -2861,7 +4033,7 @@ export default function AgendaPage() {
2861
4033
  })
2862
4034
  }
2863
4035
  rows={4}
2864
- className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
4036
+ className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
2865
4037
  placeholder={`email1@example.com
2866
4038
  email2@example.com`}
2867
4039
  />
@@ -2887,7 +4059,7 @@ email2@example.com`}
2887
4059
  value={meetData.internalNote}
2888
4060
  onChange={(e) => setMeetData({ ...meetData, internalNote: e.target.value })}
2889
4061
  rows={3}
2890
- className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
4062
+ className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-2 text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
2891
4063
  placeholder="Ajoutez une note personnelle qui ne sera pas partagée dans l'email..."
2892
4064
  />
2893
4065
  <p className="mt-1 text-xs text-gray-500">
@@ -2895,7 +4067,6 @@ email2@example.com`}
2895
4067
  participants.
2896
4068
  </p>
2897
4069
  </div>
2898
-
2899
4070
  </form>
2900
4071
 
2901
4072
  {/* Pied de modal fixe */}
@@ -2915,6 +4086,7 @@ email2@example.com`}
2915
4086
  internalNote: '',
2916
4087
  contactId: '',
2917
4088
  addToGoogleCalendar: true,
4089
+ googleCalendarId: '',
2918
4090
  });
2919
4091
  setMeetError('');
2920
4092
  }}
@@ -2926,7 +4098,7 @@ email2@example.com`}
2926
4098
  type="submit"
2927
4099
  form="meet-form"
2928
4100
  disabled={creatingMeet}
2929
- className="w-full cursor-pointer rounded-xl bg-blue-600 px-4 py-2.5 text-sm font-medium text-white shadow-sm transition-colors hover:bg-blue-700 focus:ring-2 focus:ring-gray-400/30 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto"
4101
+ className="w-full cursor-pointer rounded-xl bg-blue-600 px-4 py-2.5 text-sm font-medium text-white shadow-sm transition-colors hover:bg-blue-700 focus:ring-2 focus:ring-gray-400/30 focus:ring-offset-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto"
2930
4102
  >
2931
4103
  {creatingMeet ? 'Création...' : 'Créer le Google Meet'}
2932
4104
  </button>
@@ -2938,8 +4110,8 @@ email2@example.com`}
2938
4110
 
2939
4111
  {/* Modal de création de rendez-vous physique */}
2940
4112
  {showCreateMeetingModal && (
2941
- <div className="fixed inset-0 z-50 flex items-center justify-center bg-gray-500/20 p-4 backdrop-blur-sm sm:p-6">
2942
- <div className="flex max-h-[90vh] w-full max-w-5xl flex-col rounded-lg bg-white p-6 shadow-xl sm:p-8">
4113
+ <div className="ui-fade-in fixed inset-0 z-50 flex items-center justify-center bg-gray-500/20 p-4 backdrop-blur-sm sm:p-6">
4114
+ <div className="ui-scale-in flex max-h-[90vh] w-full max-w-5xl flex-col rounded-lg bg-white p-6 shadow-xl sm:p-8">
2943
4115
  {/* En-tête fixe */}
2944
4116
  <div className="shrink-0 border-b border-gray-100 pb-4">
2945
4117
  <div className="flex items-center justify-between">
@@ -2959,6 +4131,7 @@ email2@example.com`}
2959
4131
  attendees: [],
2960
4132
  contactId: '',
2961
4133
  addToGoogleCalendar: true,
4134
+ googleCalendarId: '',
2962
4135
  });
2963
4136
  setMeetingError('');
2964
4137
  }}
@@ -2977,25 +4150,61 @@ email2@example.com`}
2977
4150
  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"
2978
4151
  >
2979
4152
  {/* Ajouter à Google Calendar */}
2980
- <div className="flex items-center gap-2 rounded-lg border border-gray-200 bg-gray-50 p-3">
2981
- <input
2982
- type="checkbox"
2983
- id="meeting-add-to-calendar"
2984
- checked={meetingData.addToGoogleCalendar}
2985
- onChange={(e) =>
2986
- setMeetingData({
2987
- ...meetingData,
2988
- addToGoogleCalendar: e.target.checked,
2989
- })
2990
- }
2991
- className="h-4 w-4 cursor-pointer rounded border-gray-300 text-blue-600 focus:ring-2 focus:ring-gray-400/30"
2992
- />
2993
- <label
2994
- htmlFor="meeting-add-to-calendar"
2995
- className="cursor-pointer text-sm font-medium text-gray-700"
2996
- >
2997
- Ajouter à Google Calendar
2998
- </label>
4153
+ <div className="space-y-3 rounded-lg border border-gray-200 bg-gray-50 p-3">
4154
+ <div className="flex items-center gap-2">
4155
+ <input
4156
+ type="checkbox"
4157
+ id="meeting-add-to-calendar"
4158
+ checked={meetingData.addToGoogleCalendar}
4159
+ onChange={(e) => {
4160
+ const checked = e.target.checked;
4161
+ setMeetingData({
4162
+ ...meetingData,
4163
+ addToGoogleCalendar: checked,
4164
+ googleCalendarId:
4165
+ checked &&
4166
+ !meetingData.googleCalendarId &&
4167
+ targetGoogleCalendars.length > 0
4168
+ ? pickDefaultCalendarId(gCalPayload, targetGoogleCalendars)
4169
+ : meetingData.googleCalendarId,
4170
+ });
4171
+ }}
4172
+ className="h-4 w-4 cursor-pointer rounded border-gray-300 text-blue-600 focus:ring-2 focus:ring-gray-400/30"
4173
+ />
4174
+ <label
4175
+ htmlFor="meeting-add-to-calendar"
4176
+ className="cursor-pointer text-sm font-medium text-gray-700"
4177
+ >
4178
+ Ajouter à Google Calendar
4179
+ </label>
4180
+ </div>
4181
+ {googleConnected &&
4182
+ meetingData.addToGoogleCalendar &&
4183
+ targetGoogleCalendars.length > 0 && (
4184
+ <div>
4185
+ <label
4186
+ htmlFor="meeting-google-calendar"
4187
+ className="block text-sm font-medium text-gray-700"
4188
+ >
4189
+ Calendrier cible
4190
+ </label>
4191
+ <select
4192
+ id="meeting-google-calendar"
4193
+ value={meetingData.googleCalendarId || 'primary'}
4194
+ onChange={(e) =>
4195
+ setMeetingData({ ...meetingData, googleCalendarId: e.target.value })
4196
+ }
4197
+ className="mt-1 block w-full cursor-pointer rounded-xl border border-gray-300 bg-white px-4 py-2.5 text-sm text-gray-900 shadow-sm focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
4198
+ >
4199
+ {targetGoogleCalendars.map((c) => (
4200
+ <option key={c.id} value={c.id}>
4201
+ {c.summary}
4202
+ {c.primary ? ' (principal)' : ''}
4203
+ </option>
4204
+ ))}
4205
+ </select>
4206
+ </div>
4207
+ )}
2999
4208
  </div>
3000
4209
 
3001
4210
  {/* Contact (optionnel) */}
@@ -3036,7 +4245,7 @@ email2@example.com`}
3036
4245
  placeholder="Rechercher un contact..."
3037
4246
  value={meetingContactSearch}
3038
4247
  onChange={(e) => setMeetingContactSearch(e.target.value)}
3039
- className="w-full rounded-lg border border-gray-300 py-2 pr-3 pl-9 text-sm text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
4248
+ className="w-full rounded-lg border border-gray-300 py-2 pr-3 pl-9 text-sm text-gray-900 focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
3040
4249
  autoFocus
3041
4250
  />
3042
4251
  </div>
@@ -3102,54 +4311,22 @@ email2@example.com`}
3102
4311
  type="text"
3103
4312
  value={meetingData.title}
3104
4313
  onChange={(e) => setMeetingData({ ...meetingData, title: e.target.value })}
3105
- 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:ring-2 focus:ring-gray-400/30 focus:outline-none"
4314
+ 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:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
3106
4315
  placeholder="Ex : Rendez-vous avec le client"
3107
4316
  />
3108
4317
  </div>
3109
4318
 
3110
- <div className="grid gap-4 md:grid-cols-2">
3111
- <div className="space-y-2">
3112
- <p className="text-sm font-medium text-gray-700">Date & heure *</p>
3113
- <div className="grid grid-cols-[3fr,2fr] gap-2">
3114
- <input
3115
- type="date"
3116
- required
3117
- value={meetingData.scheduledAt ? meetingData.scheduledAt.split('T')[0] : ''}
3118
- onChange={(e) => {
3119
- const time =
3120
- meetingData.scheduledAt && meetingData.scheduledAt.includes('T')
3121
- ? meetingData.scheduledAt.split('T')[1]
3122
- : '';
3123
- setMeetingData({
3124
- ...meetingData,
3125
- scheduledAt: time
3126
- ? `${e.target.value}T${time}`
3127
- : `${e.target.value}T09:00`,
3128
- });
3129
- }}
3130
- className="block w-full rounded-xl border border-gray-300 px-2 py-1.5 text-xs text-gray-900 shadow-sm focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
3131
- />
3132
- <input
3133
- type="time"
3134
- value={
3135
- meetingData.scheduledAt && meetingData.scheduledAt.includes('T')
3136
- ? meetingData.scheduledAt.split('T')[1].slice(0, 5)
3137
- : ''
3138
- }
3139
- onChange={(e) => {
3140
- const datePart =
3141
- meetingData.scheduledAt && meetingData.scheduledAt.includes('T')
3142
- ? meetingData.scheduledAt.split('T')[0]
3143
- : new Date().toISOString().split('T')[0];
3144
- setMeetingData({
3145
- ...meetingData,
3146
- scheduledAt: `${datePart}T${e.target.value || '09:00'}`,
3147
- });
3148
- }}
3149
- className="block w-full rounded-xl border border-gray-300 px-2 py-1.5 text-xs text-gray-900 shadow-sm focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
3150
- />
3151
- </div>
3152
- </div>
4319
+ <div className="space-y-2">
4320
+ <label className="text-sm font-medium text-gray-700">Date & heure *</label>
4321
+ <DateTimePicker
4322
+ required
4323
+ value={
4324
+ meetingData.scheduledAt
4325
+ ? toLocalDateTimeInput(meetingData.scheduledAt)
4326
+ : ''
4327
+ }
4328
+ onChange={(v) => setMeetingData({ ...meetingData, scheduledAt: v })}
4329
+ />
3153
4330
  </div>
3154
4331
 
3155
4332
  <div className="space-y-2">
@@ -3162,7 +4339,7 @@ email2@example.com`}
3162
4339
  reminderMinutesBefore: e.target.value ? Number(e.target.value) : null,
3163
4340
  })
3164
4341
  }
3165
- className="mt-1 block w-full cursor-pointer rounded-xl border border-gray-300 px-4 py-2.5 text-sm text-gray-900 shadow-sm focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
4342
+ className="mt-1 block w-full cursor-pointer rounded-xl border border-gray-300 px-4 py-2.5 text-sm text-gray-900 shadow-sm focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
3166
4343
  >
3167
4344
  <option value="">Aucun rappel</option>
3168
4345
  <option value="5">5 minutes avant</option>
@@ -3189,7 +4366,7 @@ email2@example.com`}
3189
4366
  })
3190
4367
  }
3191
4368
  rows={4}
3192
- 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:ring-2 focus:ring-gray-400/30 focus:outline-none"
4369
+ 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:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
3193
4370
  placeholder={`email1@example.com
3194
4371
  email2@example.com`}
3195
4372
  />
@@ -3235,7 +4412,8 @@ email2@example.com`}
3235
4412
  Note personnelle
3236
4413
  </label>
3237
4414
  <p className="mt-1 text-xs text-gray-500">
3238
- Cette note ne sera pas visible par les invités s&apos;ils sont prévenus par email.
4415
+ Cette note ne sera pas visible par les invités s&apos;ils sont prévenus par
4416
+ email.
3239
4417
  </p>
3240
4418
  <textarea
3241
4419
  value={meetingData.internalNote}
@@ -3243,11 +4421,10 @@ email2@example.com`}
3243
4421
  setMeetingData({ ...meetingData, internalNote: e.target.value })
3244
4422
  }
3245
4423
  rows={3}
3246
- 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:ring-2 focus:ring-gray-400/30 focus:outline-none"
4424
+ 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:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
3247
4425
  placeholder="Ajoutez une note personnelle qui ne sera pas partagée..."
3248
4426
  />
3249
4427
  </div>
3250
-
3251
4428
  </form>
3252
4429
 
3253
4430
  {/* Pied de modal fixe */}
@@ -3267,6 +4444,7 @@ email2@example.com`}
3267
4444
  attendees: [],
3268
4445
  contactId: '',
3269
4446
  addToGoogleCalendar: true,
4447
+ googleCalendarId: '',
3270
4448
  });
3271
4449
  setMeetingError('');
3272
4450
  }}
@@ -3278,7 +4456,7 @@ email2@example.com`}
3278
4456
  type="submit"
3279
4457
  form="meeting-form"
3280
4458
  disabled={creatingMeeting}
3281
- className="w-full cursor-pointer rounded-xl bg-blue-600 px-4 py-2.5 text-sm font-medium text-white shadow-sm transition-colors hover:bg-blue-700 focus:ring-2 focus:ring-gray-400/30 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto"
4459
+ className="w-full cursor-pointer rounded-xl bg-blue-600 px-4 py-2.5 text-sm font-medium text-white shadow-sm transition-colors hover:bg-blue-700 focus:ring-2 focus:ring-gray-400/30 focus:ring-offset-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto"
3282
4460
  >
3283
4461
  {creatingMeeting ? 'Création...' : 'Créer le rendez-vous'}
3284
4462
  </button>
@@ -3290,20 +4468,17 @@ email2@example.com`}
3290
4468
 
3291
4469
  {/* Modal de détail / édition d'une tâche */}
3292
4470
  {showTaskDetailModal && selectedTask && (
3293
- <div className="fixed inset-0 z-50 flex items-center justify-center bg-gray-500/20 p-6 backdrop-blur-sm sm:p-8">
3294
- <div className="flex max-h-[90vh] w-full max-w-4xl flex-col rounded-2xl bg-white p-6 shadow-xl">
4471
+ <div className="ui-fade-in fixed inset-0 z-50 flex items-center justify-center bg-gray-500/20 p-6 backdrop-blur-sm sm:p-8">
4472
+ <div className="ui-scale-in flex max-h-[90vh] w-full max-w-4xl flex-col rounded-2xl bg-white p-6 shadow-xl">
3295
4473
  {/* En-tête */}
3296
4474
  <div className="shrink-0 border-b border-gray-100 pb-4">
3297
4475
  <div className="flex items-start justify-between gap-4">
3298
- <div>
3299
- <h2 className="text-lg font-semibold text-gray-900 sm:text-xl">
3300
- Détail de l&apos;événement
3301
- </h2>
3302
- <p className="mt-1 text-xs text-gray-500 sm:text-sm">
4476
+ <div className="min-w-0">
4477
+ <h2 className="truncate text-lg font-semibold text-gray-900 sm:text-xl">
3303
4478
  {selectedTask.contact
3304
- ? `Événement lié au contact ${selectedTask.contact.firstName || ''} ${selectedTask.contact.lastName || ''}`.trim()
3305
- : 'Tâche personnelle non liée à un contact.'}
3306
- </p>
4479
+ ? `${TASK_TYPE_LABELS[selectedTask.type]} · ${[selectedTask.contact.firstName, selectedTask.contact.lastName].filter(Boolean).join(' ').trim()}`
4480
+ : TASK_TYPE_LABELS[selectedTask.type]}
4481
+ </h2>
3307
4482
  </div>
3308
4483
  <button
3309
4484
  type="button"
@@ -3331,73 +4506,121 @@ email2@example.com`}
3331
4506
  <form
3332
4507
  id="task-detail-form"
3333
4508
  onSubmit={handleUpdateTaskDetail}
3334
- 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"
4509
+ className="flex-1 space-y-4 overflow-y-auto px-2 pt-4 pb-2 [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
3335
4510
  >
3336
- {/* Informations du contact (si présent) */}
3337
- {selectedTask.contact && (
3338
- <div className="rounded-lg border border-blue-200 bg-blue-50 p-4">
3339
- <div className="flex items-center justify-between">
3340
- <div className="flex items-center gap-3">
3341
- <div className="flex h-10 w-10 items-center justify-center rounded-full bg-blue-100 text-blue-700">
3342
- <User className="h-5 w-5" />
3343
- </div>
3344
- <div>
3345
- <p className="text-sm font-medium text-gray-900">
3346
- {selectedTask.contact.firstName} {selectedTask.contact.lastName}
3347
- </p>
3348
- <p className="text-xs text-gray-500">Contact associé</p>
4511
+ {/* Bandeau compact : contact + créateur */}
4512
+ <div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
4513
+ {selectedTask.contact && (
4514
+ <div className="rounded-lg border border-blue-200 bg-blue-50 px-2.5 py-2">
4515
+ <div className="flex items-center justify-between gap-3">
4516
+ <div className="flex min-w-0 items-center gap-2.5">
4517
+ <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-blue-100 text-blue-700">
4518
+ <User className="h-4 w-4" />
4519
+ </div>
4520
+ <div className="min-w-0">
4521
+ <p className="truncate text-sm font-medium text-gray-900">
4522
+ {selectedTask.contact.firstName} {selectedTask.contact.lastName}
4523
+ </p>
4524
+ <p className="text-xs text-gray-500">Contact associé</p>
4525
+ </div>
3349
4526
  </div>
4527
+ <Link
4528
+ href={`/contacts/${selectedTask.contact.id}`}
4529
+ className="inline-flex shrink-0 cursor-pointer items-center gap-1.5 rounded-md border border-blue-300 bg-white px-2.5 py-1.5 text-xs font-medium text-blue-700 transition-colors hover:bg-blue-50"
4530
+ >
4531
+ <Eye className="h-3.5 w-3.5" />
4532
+ Voir
4533
+ </Link>
3350
4534
  </div>
3351
- <Link
3352
- href={`/contacts/${selectedTask.contact.id}`}
3353
- className="inline-flex cursor-pointer items-center gap-2 rounded-lg border border-blue-300 bg-white px-3 py-2 text-sm font-medium text-blue-700 transition-colors hover:bg-blue-50"
3354
- >
3355
- <Eye className="h-4 w-4" />
3356
- Voir le contact
3357
- </Link>
3358
4535
  </div>
3359
- </div>
3360
- )}
4536
+ )}
3361
4537
 
3362
- {/* Type de tâche */}
3363
- <div>
3364
- <label className="block text-sm font-medium text-gray-700">
3365
- Type d&apos;événement
3366
- </label>
3367
- <div className="mt-1 rounded-lg border border-gray-200 bg-gray-50 px-4 py-2.5 text-sm text-gray-700">
3368
- {TASK_TYPE_LABELS[selectedTask.type]}
4538
+ <div className="rounded-lg border border-gray-200 bg-gray-50 px-2.5 py-2">
4539
+ <div className="flex items-center gap-2.5">
4540
+ <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-white text-gray-700 shadow-sm">
4541
+ <User className="h-4 w-4" />
4542
+ </div>
4543
+ <div className="min-w-0">
4544
+ <p className="truncate text-sm font-medium text-gray-900">
4545
+ {selectedTask.createdBy?.name?.trim() || 'Utilisateur inconnu'}
4546
+ </p>
4547
+ <p className="text-xs text-gray-500">Créé par</p>
4548
+ </div>
4549
+ </div>
3369
4550
  </div>
3370
4551
  </div>
3371
4552
 
3372
- {/* Lien Google Meet (si présent) */}
3373
- {selectedTask.googleMeetLink && (
3374
- <div>
3375
- <label className="block text-sm font-medium text-gray-700">Google Meet</label>
3376
- <div className="mt-1">
4553
+ {/* Statut action immédiate, très visible */}
4554
+ <div
4555
+ className={cn(
4556
+ 'rounded-xl border-2 p-3 transition-colors',
4557
+ taskDetailData.completed
4558
+ ? 'border-emerald-200 bg-emerald-50/90'
4559
+ : 'border-amber-200 bg-amber-50/70',
4560
+ )}
4561
+ >
4562
+ <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
4563
+ <div>
4564
+ <p className="text-xs font-semibold tracking-wide text-gray-600 uppercase">
4565
+ Statut
4566
+ </p>
4567
+ <p className="mt-1 text-sm font-medium text-gray-900">
4568
+ {taskDetailData.completed
4569
+ ? 'Cet événement est marqué comme terminé'
4570
+ : 'Cet événement est à faire'}
4571
+ </p>
4572
+ </div>
4573
+ <div className="flex flex-wrap gap-2">
3377
4574
  <button
3378
4575
  type="button"
3379
- onClick={() => {
3380
- if (selectedTask.googleMeetLink) {
3381
- globalThis.open(
3382
- selectedTask.googleMeetLink,
3383
- '_blank',
3384
- 'noopener,noreferrer',
3385
- );
3386
- }
3387
- }}
3388
- className="inline-flex cursor-pointer items-center gap-2 rounded-lg border border-blue-200 bg-blue-50 px-4 py-2.5 text-sm font-medium text-blue-700 transition-colors hover:bg-blue-100"
4576
+ disabled={taskCompleteQuickSaving}
4577
+ onClick={() => handleAgendaModalQuickSetCompleted(false)}
4578
+ className={cn(
4579
+ 'inline-flex min-h-10 min-w-24 items-center justify-center rounded-lg px-3 py-1.5 text-sm font-semibold transition-colors',
4580
+ !taskDetailData.completed
4581
+ ? 'bg-white text-amber-900 shadow-sm ring-2 ring-amber-400'
4582
+ : 'border border-gray-300 bg-white text-gray-700 hover:bg-gray-50',
4583
+ )}
3389
4584
  >
3390
- <Video className="h-4 w-4" />
3391
- Rejoindre Google Meet
3392
- <ExternalLink className="h-3 w-3" />
4585
+ {taskCompleteQuickSaving && !taskDetailData.completed ? (
4586
+ <Loader2 className="h-4 w-4 animate-spin" />
4587
+ ) : (
4588
+ 'À faire'
4589
+ )}
4590
+ </button>
4591
+ <button
4592
+ type="button"
4593
+ disabled={taskCompleteQuickSaving}
4594
+ onClick={() => handleAgendaModalQuickSetCompleted(true)}
4595
+ className={cn(
4596
+ 'inline-flex min-h-10 min-w-24 items-center justify-center rounded-lg px-3 py-1.5 text-sm font-semibold transition-colors',
4597
+ taskDetailData.completed
4598
+ ? 'bg-emerald-600 text-white shadow-sm ring-2 ring-emerald-800/30'
4599
+ : 'border border-emerald-300 bg-white text-emerald-800 hover:bg-emerald-50',
4600
+ )}
4601
+ >
4602
+ {taskCompleteQuickSaving && taskDetailData.completed ? (
4603
+ <Loader2 className="h-4 w-4 animate-spin text-white" />
4604
+ ) : (
4605
+ 'Terminé'
4606
+ )}
3393
4607
  </button>
3394
4608
  </div>
3395
4609
  </div>
3396
- )}
4610
+ <p className="mt-2 hidden text-xs text-gray-600 sm:block">
4611
+ Le statut est enregistré tout de suite. Vous pouvez ensuite modifier le reste et
4612
+ cliquer sur « Enregistrer les modifications ».
4613
+ </p>
4614
+ </div>
3397
4615
 
3398
- {/* Titre + statut */}
3399
- <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
3400
- <div className="flex-1">
4616
+ <div className="grid grid-cols-1 gap-3 md:grid-cols-2">
4617
+ <div className="space-y-2">
4618
+ <label className="block text-sm font-medium text-gray-700">
4619
+ Type d&apos;événement
4620
+ </label>
4621
+ <div className="rounded-lg border border-gray-200 bg-gray-50 px-3 py-2 text-sm text-gray-700">
4622
+ {TASK_TYPE_LABELS[selectedTask.type]}
4623
+ </div>
3401
4624
  <label className="block text-sm font-medium text-gray-700">
3402
4625
  Titre (optionnel)
3403
4626
  </label>
@@ -3410,72 +4633,81 @@ email2@example.com`}
3410
4633
  title: e.target.value,
3411
4634
  })
3412
4635
  }
3413
- 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:ring-2 focus:ring-gray-400/30 focus:outline-none"
4636
+ className="block w-full rounded-xl border border-gray-300 px-3 py-2 text-sm text-gray-900 shadow-sm focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
3414
4637
  placeholder="Ex : Relance client"
3415
4638
  />
3416
4639
  </div>
3417
- <label className="flex cursor-pointer items-center gap-2 text-sm text-gray-700">
3418
- <input
3419
- type="checkbox"
3420
- checked={taskDetailData.completed}
3421
- onChange={(e) =>
4640
+ <div className="space-y-2">
4641
+ {googleConnected && targetGoogleCalendars.length > 0 && (
4642
+ <>
4643
+ <label
4644
+ htmlFor="task-detail-google-calendar"
4645
+ className="block text-sm font-medium text-gray-700"
4646
+ >
4647
+ Calendrier cible
4648
+ </label>
4649
+ <select
4650
+ id="task-detail-google-calendar"
4651
+ value={taskDetailData.googleCalendarId || 'primary'}
4652
+ onChange={(e) =>
4653
+ setTaskDetailData({
4654
+ ...taskDetailData,
4655
+ googleCalendarId: e.target.value,
4656
+ })
4657
+ }
4658
+ className="block w-full cursor-pointer rounded-xl border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 shadow-sm focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
4659
+ >
4660
+ {targetGoogleCalendars.map((c) => (
4661
+ <option key={c.id} value={c.id}>
4662
+ {c.summary}
4663
+ {c.primary ? ' (principal)' : ''}
4664
+ </option>
4665
+ ))}
4666
+ </select>
4667
+ </>
4668
+ )}
4669
+ <label className="block text-sm font-medium text-gray-700">Date & heure *</label>
4670
+ <DateTimePicker
4671
+ required
4672
+ value={
4673
+ taskDetailData.scheduledAt
4674
+ ? toLocalDateTimeInput(taskDetailData.scheduledAt)
4675
+ : ''
4676
+ }
4677
+ onChange={(v) =>
3422
4678
  setTaskDetailData({
3423
4679
  ...taskDetailData,
3424
- completed: e.target.checked,
4680
+ scheduledAt: v,
3425
4681
  })
3426
4682
  }
3427
- className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-2 focus:ring-gray-400/30 focus:ring-offset-2"
3428
4683
  />
3429
- <span>Marquer comme terminée</span>
3430
- </label>
4684
+ </div>
3431
4685
  </div>
3432
4686
 
3433
- {/* Date & heure + priorité */}
3434
- <div className="grid gap-4">
3435
- <div className="space-y-2">
3436
- <p className="text-sm font-medium text-gray-700">Date & heure *</p>
3437
- <div className="grid grid-cols-2 gap-2">
3438
- <input
3439
- type="date"
3440
- required
3441
- value={
3442
- taskDetailData.scheduledAt ? taskDetailData.scheduledAt.split('T')[0] : ''
3443
- }
3444
- onChange={(e) => {
3445
- const time =
3446
- taskDetailData.scheduledAt && taskDetailData.scheduledAt.includes('T')
3447
- ? taskDetailData.scheduledAt.split('T')[1]
3448
- : '09:00';
3449
- setTaskDetailData({
3450
- ...taskDetailData,
3451
- scheduledAt: `${e.target.value}T${time}`,
3452
- });
3453
- }}
3454
- className="block w-full rounded-xl border border-gray-300 px-2 py-1.5 text-xs text-gray-900 shadow-sm focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
3455
- />
3456
- <input
3457
- type="time"
3458
- required
3459
- value={
3460
- taskDetailData.scheduledAt && taskDetailData.scheduledAt.includes('T')
3461
- ? taskDetailData.scheduledAt.split('T')[1].slice(0, 5)
3462
- : ''
3463
- }
3464
- onChange={(e) => {
3465
- const datePart =
3466
- taskDetailData.scheduledAt && taskDetailData.scheduledAt.includes('T')
3467
- ? taskDetailData.scheduledAt.split('T')[0]
3468
- : new Date().toISOString().split('T')[0];
3469
- setTaskDetailData({
3470
- ...taskDetailData,
3471
- scheduledAt: `${datePart}T${e.target.value || '09:00'}`,
3472
- });
4687
+ {selectedTask.googleMeetLink && (
4688
+ <div>
4689
+ <label className="block text-sm font-medium text-gray-700">Google Meet</label>
4690
+ <div className="mt-1">
4691
+ <button
4692
+ type="button"
4693
+ onClick={() => {
4694
+ if (selectedTask.googleMeetLink) {
4695
+ globalThis.open(
4696
+ selectedTask.googleMeetLink,
4697
+ '_blank',
4698
+ 'noopener,noreferrer',
4699
+ );
4700
+ }
3473
4701
  }}
3474
- className="block w-full rounded-xl border border-gray-300 px-2 py-1.5 text-xs text-gray-900 shadow-sm focus:ring-2 focus:ring-gray-400/30 focus:outline-none"
3475
- />
4702
+ className="inline-flex cursor-pointer items-center gap-2 rounded-lg border border-blue-200 bg-blue-50 px-3 py-2 text-sm font-medium text-blue-700 transition-colors hover:bg-blue-100"
4703
+ >
4704
+ <Video className="h-4 w-4" />
4705
+ Rejoindre Google Meet
4706
+ <ExternalLink className="h-3 w-3" />
4707
+ </button>
3476
4708
  </div>
3477
4709
  </div>
3478
- </div>
4710
+ )}
3479
4711
 
3480
4712
  {/* Note personnelle (si présente) */}
3481
4713
  {selectedTask.internalNote && (
@@ -3502,6 +4734,28 @@ email2@example.com`}
3502
4734
  </p>
3503
4735
  </div>
3504
4736
 
4737
+ <div className="space-y-2">
4738
+ <p className="text-sm font-medium text-gray-700">Rappel</p>
4739
+ <select
4740
+ value={taskDetailData.reminderMinutesBefore ?? ''}
4741
+ onChange={(e) =>
4742
+ setTaskDetailData({
4743
+ ...taskDetailData,
4744
+ reminderMinutesBefore: e.target.value ? Number(e.target.value) : null,
4745
+ })
4746
+ }
4747
+ className="block w-full rounded-xl border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 shadow-sm focus:ring-2 focus:ring-gray-400/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
4748
+ >
4749
+ <option value="">Aucun rappel</option>
4750
+ <option value="5">5 minutes avant</option>
4751
+ <option value="10">10 minutes avant</option>
4752
+ <option value="15">15 minutes avant</option>
4753
+ <option value="30">30 minutes avant</option>
4754
+ <option value="60">1 heure avant</option>
4755
+ <option value="120">2 heures avant</option>
4756
+ <option value="1440">1 jour avant</option>
4757
+ </select>
4758
+ </div>
3505
4759
  </form>
3506
4760
 
3507
4761
  {/* Pied de modal */}
@@ -3531,7 +4785,7 @@ email2@example.com`}
3531
4785
  type="submit"
3532
4786
  form="task-detail-form"
3533
4787
  disabled={taskDetailLoading}
3534
- className="w-full cursor-pointer rounded-xl bg-blue-600 px-4 py-2.5 text-sm font-medium text-white shadow-sm transition-colors hover:bg-blue-700 focus:ring-2 focus:ring-gray-400/30 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto"
4788
+ className="w-full cursor-pointer rounded-xl bg-blue-600 px-4 py-2.5 text-sm font-medium text-white shadow-sm transition-colors hover:bg-blue-700 focus:ring-2 focus:ring-gray-400/30 focus:ring-offset-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 sm:w-auto"
3535
4789
  >
3536
4790
  {taskDetailLoading ? 'Enregistrement...' : 'Enregistrer les modifications'}
3537
4791
  </button>
@@ -3543,6 +4797,7 @@ email2@example.com`}
3543
4797
  )}
3544
4798
  </div>
3545
4799
  <ConfirmDialog />
4800
+ </TooltipProvider>
3546
4801
  </ProtectedPage>
3547
4802
  );
3548
4803
  }