create-crm-tmp 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/create-crm-tmp.js +56 -35
- package/package.json +1 -1
- package/template/README.md +230 -115
- package/template/eslint.config.mjs +13 -0
- package/template/next.config.ts +14 -0
- package/template/package.json +15 -2
- package/template/prisma/migrations/20260318095700_init_db/migration.sql +978 -0
- package/template/prisma/migrations/migration_lock.toml +3 -0
- package/template/prisma/schema.prisma +132 -637
- package/template/src/app/(auth)/invite/[token]/page.tsx +10 -8
- package/template/src/app/(auth)/layout.tsx +1 -1
- package/template/src/app/(auth)/reset-password/complete/page.tsx +11 -8
- package/template/src/app/(auth)/reset-password/page.tsx +4 -4
- package/template/src/app/(auth)/reset-password/verify/page.tsx +4 -4
- package/template/src/app/(auth)/signin/page.tsx +14 -6
- package/template/src/app/(dashboard)/agenda/page.tsx +2243 -988
- package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +18 -104
- package/template/src/app/(dashboard)/automatisation/page.tsx +10 -26
- package/template/src/app/(dashboard)/closing/page.tsx +78 -62
- package/template/src/app/(dashboard)/contacts/[id]/page.tsx +2082 -1080
- package/template/src/app/(dashboard)/contacts/companies/[id]/page.tsx +46 -47
- package/template/src/app/(dashboard)/contacts/page.tsx +1062 -780
- package/template/src/app/(dashboard)/dashboard/page.tsx +533 -37
- package/template/src/app/(dashboard)/dev/page.tsx +1291 -0
- package/template/src/app/(dashboard)/layout.tsx +6 -2
- package/template/src/app/(dashboard)/settings/page.tsx +797 -2582
- package/template/src/app/(dashboard)/templates/page.tsx +55 -54
- package/template/src/app/(dashboard)/users/list/page.tsx +51 -48
- package/template/src/app/(dashboard)/users/page.tsx +1 -1
- package/template/src/app/(dashboard)/users/permissions/page.tsx +2 -2
- package/template/src/app/(dashboard)/users/roles/page.tsx +7 -5
- package/template/src/app/api/agenda/google-events/route.ts +92 -0
- package/template/src/app/api/auth/check-active/route.ts +3 -2
- package/template/src/app/api/auth/google/route.ts +2 -1
- package/template/src/app/api/auth/google/status/route.ts +7 -31
- package/template/src/app/api/companies/[id]/activities/route.ts +1 -3
- package/template/src/app/api/companies/[id]/route.ts +1 -2
- package/template/src/app/api/companies/route.ts +42 -12
- package/template/src/app/api/contacts/[id]/files/[fileId]/preview/route.ts +9 -31
- package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +14 -32
- package/template/src/app/api/contacts/[id]/files/route.ts +112 -212
- package/template/src/app/api/contacts/[id]/interactions/[interactionId]/route.ts +27 -1
- package/template/src/app/api/contacts/[id]/interactions/route.ts +16 -16
- package/template/src/app/api/contacts/[id]/kyc/route.ts +21 -11
- package/template/src/app/api/contacts/[id]/meet/route.ts +19 -2
- package/template/src/app/api/contacts/[id]/route.ts +106 -34
- package/template/src/app/api/contacts/[id]/send-email/route.ts +27 -11
- package/template/src/app/api/contacts/[id]/workflows/run/route.ts +6 -0
- package/template/src/app/api/contacts/export/route.ts +9 -13
- package/template/src/app/api/contacts/import/route.ts +55 -25
- package/template/src/app/api/contacts/import-preview/route.ts +1 -1
- package/template/src/app/api/contacts/origins/route.ts +63 -0
- package/template/src/app/api/contacts/route.ts +153 -41
- package/template/src/app/api/cron/cleanup-editor-images/route.ts +166 -0
- package/template/src/app/api/dashboard/widgets/[id]/route.ts +44 -0
- package/template/src/app/api/dashboard/widgets/route.ts +181 -0
- package/template/src/app/api/dev/reminders/test/route.ts +114 -0
- package/template/src/app/api/editor/upload-image/route.ts +61 -0
- package/template/src/app/api/integrations/google-sheet/jobs/[jobId]/route.ts +47 -0
- package/template/src/app/api/integrations/google-sheet/jobs/usage/route.ts +50 -0
- package/template/src/app/api/integrations/google-sheet/sync/route.ts +24 -556
- package/template/src/app/api/jobs/google-sheet/process/route.ts +84 -0
- package/template/src/app/api/jobs/google-sheet/schedule/route.ts +50 -0
- package/template/src/app/api/reminders/clear/route.ts +120 -0
- package/template/src/app/api/reminders/clear/undo/route.ts +112 -0
- package/template/src/app/api/reminders/route.ts +164 -39
- package/template/src/app/api/reminders/state/route.ts +164 -0
- package/template/src/app/api/reset-password/request/route.ts +1 -1
- package/template/src/app/api/reset-password/verify/route.ts +1 -1
- package/template/src/app/api/send/route.ts +16 -4
- package/template/src/app/api/settings/google-ads/route.ts +14 -0
- package/template/src/app/api/settings/google-calendar/calendars/route.ts +97 -0
- package/template/src/app/api/settings/google-calendar/route.ts +124 -0
- package/template/src/app/api/settings/google-sheet/[id]/route.ts +28 -0
- package/template/src/app/api/settings/google-sheet/auto-map/route.ts +37 -4
- package/template/src/app/api/settings/google-sheet/preview/route.ts +9 -3
- package/template/src/app/api/settings/google-sheet/route.ts +14 -0
- package/template/src/app/api/settings/integrations/logs/route.ts +93 -0
- package/template/src/app/api/settings/integrations/notifications/route.ts +67 -0
- package/template/src/app/api/settings/meta-leads/[id]/route.ts +0 -1
- package/template/src/app/api/settings/meta-leads/route.ts +14 -2
- package/template/src/app/api/settings/smtp/route.ts +53 -6
- package/template/src/app/api/tasks/[id]/attendees/route.ts +24 -8
- package/template/src/app/api/tasks/[id]/route.ts +234 -58
- package/template/src/app/api/tasks/meet/route.ts +27 -19
- package/template/src/app/api/tasks/route.ts +62 -17
- package/template/src/app/api/users/[id]/route.ts +20 -14
- package/template/src/app/api/users/list/route.ts +57 -19
- package/template/src/app/api/webhooks/google-ads/route.ts +34 -14
- package/template/src/app/api/webhooks/meta-leads/route.ts +32 -12
- package/template/src/app/api/workflows/[id]/route.ts +0 -4
- package/template/src/app/api/workflows/process/route.ts +22 -51
- package/template/src/app/api/workflows/route.ts +0 -4
- package/template/src/app/globals.css +342 -4
- package/template/src/app/layout.tsx +11 -3
- package/template/src/app/page.tsx +1 -1
- package/template/src/components/address-autocomplete.tsx +7 -6
- package/template/src/components/config-error-alert.tsx +46 -0
- package/template/src/components/contacts/filter-bar.tsx +12 -3
- package/template/src/components/contacts/filter-builder.tsx +28 -43
- package/template/src/components/contacts/save-view-dialog.tsx +1 -1
- package/template/src/components/contacts/views-tab-bar.tsx +15 -6
- package/template/src/components/dashboard/activity-chart.tsx +41 -28
- package/template/src/components/dashboard/add-widget-dialog.tsx +157 -0
- package/template/src/components/dashboard/color-picker.tsx +64 -0
- package/template/src/components/dashboard/contacts-chart.tsx +69 -0
- package/template/src/components/dashboard/interactions-by-type-chart.tsx +121 -0
- package/template/src/components/dashboard/recent-activity.tsx +154 -0
- package/template/src/components/dashboard/stat-card.tsx +40 -40
- package/template/src/components/dashboard/status-distribution-chart.tsx +81 -0
- package/template/src/components/dashboard/tasks-pie-chart.tsx +37 -34
- package/template/src/components/dashboard/top-contacts-list.tsx +113 -0
- package/template/src/components/dashboard/upcoming-tasks-list.tsx +72 -81
- package/template/src/components/dashboard/widget-wrapper.tsx +36 -0
- package/template/src/components/date-picker.tsx +9 -6
- package/template/src/components/editor/upload-editor-image.ts +42 -0
- package/template/src/components/editor.tsx +161 -22
- package/template/src/components/email-template.tsx +2 -2
- package/template/src/components/global-search.tsx +30 -28
- package/template/src/components/header.tsx +178 -80
- package/template/src/components/inactive-account-guard.tsx +58 -0
- package/template/src/components/integration-notifications-listener.tsx +12 -0
- package/template/src/components/invitation-email-template.tsx +2 -2
- package/template/src/components/meet-cancellation-email-template.tsx +3 -3
- package/template/src/components/meet-confirmation-email-template.tsx +3 -3
- package/template/src/components/meet-update-email-template.tsx +3 -3
- package/template/src/components/page-header.tsx +5 -5
- package/template/src/components/protected-page.tsx +1 -1
- package/template/src/components/reset-password-email-template.tsx +2 -2
- package/template/src/components/settings/integrations/GoogleAdsIntegration.tsx +428 -0
- package/template/src/components/settings/integrations/GoogleSheetConfigMonitoringModal.tsx +680 -0
- package/template/src/components/settings/integrations/GoogleSheetIntegration.tsx +809 -0
- package/template/src/components/settings/integrations/ImportResultDialog.tsx +124 -0
- package/template/src/components/settings/integrations/IntegrationLogPanel.tsx +57 -0
- package/template/src/components/settings/integrations/IntegrationLogsTable.tsx +186 -0
- package/template/src/components/settings/integrations/MetaLeadIntegration.tsx +451 -0
- package/template/src/components/sidebar.tsx +45 -26
- package/template/src/components/skeleton.tsx +40 -43
- package/template/src/components/ui/accordion.tsx +2 -2
- package/template/src/components/ui/alert-dialog.tsx +1 -1
- package/template/src/components/ui/button.tsx +20 -9
- package/template/src/components/ui/components.tsx +1 -1
- package/template/src/components/ui/date-picker.tsx +422 -0
- package/template/src/components/ui/datetime-picker.tsx +338 -0
- package/template/src/components/ui/status-select.tsx +271 -0
- package/template/src/components/ui/tooltip.tsx +37 -0
- package/template/src/components/view-as-modal.tsx +13 -7
- package/template/src/contexts/app-toast-context.tsx +245 -57
- package/template/src/contexts/dashboard-theme-context.tsx +53 -0
- package/template/src/contexts/sidebar-context.tsx +22 -17
- package/template/src/contexts/task-reminder-context.tsx +134 -160
- package/template/src/contexts/view-as-context.tsx +33 -6
- package/template/src/hooks/use-focus-trap.ts +2 -2
- package/template/src/hooks/useIntegrationNotifications.ts +49 -0
- package/template/src/lib/auth.ts +8 -1
- package/template/src/lib/config-links.ts +14 -0
- package/template/src/lib/contact-duplicate.ts +79 -61
- package/template/src/lib/contact-interactions.ts +21 -21
- package/template/src/lib/contact-view-filters.ts +24 -64
- package/template/src/lib/contacts-list-url.ts +190 -0
- package/template/src/lib/dashboard-stats.ts +65 -7
- package/template/src/lib/dashboard-themes.ts +135 -0
- package/template/src/lib/date-utils.ts +127 -0
- package/template/src/lib/default-widgets.ts +12 -0
- package/template/src/lib/editor-html-image-dimensions.ts +172 -0
- package/template/src/lib/editor-image-limits.ts +19 -0
- package/template/src/lib/email-html-sanitize.ts +19 -0
- package/template/src/lib/encryption.ts +9 -6
- package/template/src/lib/fr-geography.ts +192 -0
- package/template/src/lib/google-calendar-agenda.ts +201 -0
- package/template/src/lib/google-calendar.ts +255 -5
- package/template/src/lib/google-sheet-sync-jobs.ts +96 -0
- package/template/src/lib/google-sheet-sync-runner.ts +514 -0
- package/template/src/lib/integration-import-log.ts +21 -0
- package/template/src/lib/permissions.ts +40 -10
- package/template/src/lib/prisma.ts +4 -1
- package/template/src/lib/qstash.ts +65 -0
- package/template/src/lib/reminder-state-server.ts +80 -0
- package/template/src/lib/reminder-state.ts +29 -0
- package/template/src/lib/supabase-storage.ts +113 -0
- package/template/src/lib/template-variables.ts +164 -23
- package/template/src/lib/utils.ts +45 -0
- package/template/src/lib/widget-registry.ts +173 -0
- package/template/src/lib/workflow-executor.ts +16 -70
- package/template/src/proxy.ts +1 -0
- package/template/vercel.json +3 -10
- package/template/skills-lock.json +0 -25
- package/template/src/components/dashboard/dashboard-content.tsx +0 -79
- package/template/src/lib/google-drive.ts +0 -1101
- package/template/src/types/yousign.ts +0 -52
|
@@ -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',
|
|
64
|
-
EMAIL: '#6366F1',
|
|
65
|
-
OTHER: '#
|
|
66
|
-
TASK: '#
|
|
67
|
-
MEETING: '#
|
|
68
|
-
VIDEO_CONFERENCE: '#10B981',
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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<{
|
|
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:
|
|
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
|
-
|
|
611
|
-
|
|
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
|
-
|
|
614
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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-
|
|
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
|
|
1082
|
-
<div className="text-xs font-medium
|
|
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
|
|
1088
|
-
<p className="mt-1 text-sm
|
|
1089
|
-
<p className="text-
|
|
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={() =>
|
|
1098
|
-
|
|
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={() =>
|
|
1106
|
-
|
|
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={() =>
|
|
1114
|
-
|
|
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
|
-
|
|
1126
|
-
|
|
1127
|
-
<
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
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
|
-
|
|
1198
|
-
|
|
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
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
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-
|
|
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-
|
|
1245
|
-
: 'border-border bg-card text-
|
|
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
|
|
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
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
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
|
|
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
|
|
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
|
|
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'hui
|
|
1281
2041
|
</button>
|
|
1282
2042
|
<button
|
|
1283
2043
|
onClick={() => navigateDate('next')}
|
|
1284
|
-
className="cursor-pointer rounded-md p-1.5
|
|
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
|
|
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
|
|
1314
|
-
<div className="sticky top-0 z-30 grid grid-cols-7 rounded-t-xl border-b
|
|
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-
|
|
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
|
|
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
|
|
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
|
-
{
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
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:
|
|
1373
|
-
borderLeft: `3px solid ${
|
|
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
|
|
1378
|
-
<
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
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
|
-
|
|
1386
|
-
|
|
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
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
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
|
-
{
|
|
2216
|
+
{mergedForMonth.length > 3 && (
|
|
1424
2217
|
<div className="px-2 text-xs text-gray-500">
|
|
1425
|
-
+{
|
|
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
|
|
1581
|
-
{
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
const
|
|
1588
|
-
|
|
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 =
|
|
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
|
|
1636
|
-
const
|
|
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
|
-
<
|
|
1641
|
-
key={
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
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
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
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
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
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
|
-
'
|
|
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={
|
|
2614
|
+
title={
|
|
2615
|
+
overlapTooltip
|
|
2616
|
+
? undefined
|
|
2617
|
+
: task.title || TASK_TYPE_LABELS[task.type]
|
|
2618
|
+
}
|
|
1700
2619
|
>
|
|
1701
|
-
{task.
|
|
2620
|
+
{task.title || TASK_TYPE_LABELS[task.type]}
|
|
1702
2621
|
</div>
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
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
|
-
|
|
2633
|
+
</div>
|
|
2634
|
+
</div>
|
|
1711
2635
|
</div>
|
|
1712
|
-
</
|
|
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">
|
|
2663
|
+
<p className="text-gray-500">Aucun événement pour cette semaine</p>
|
|
1740
2664
|
</div>
|
|
1741
2665
|
) : (
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
<div className="flex-
|
|
1746
|
-
<div className="flex
|
|
1747
|
-
<
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
e
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
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
|
-
|
|
1767
|
-
|
|
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.
|
|
1770
|
-
<
|
|
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
|
-
<
|
|
1773
|
-
|
|
1774
|
-
|
|
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
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
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
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
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
|
-
|
|
2834
|
+
<span>Google Calendar</span>
|
|
1840
2835
|
</div>
|
|
1841
|
-
|
|
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
|
-
<
|
|
1844
|
-
{
|
|
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
|
-
|
|
2856
|
+
))}
|
|
1849
2857
|
</div>
|
|
1850
|
-
|
|
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">
|
|
2878
|
+
<p className="text-gray-500">Aucun événement pour cette journée</p>
|
|
1881
2879
|
</div>
|
|
1882
2880
|
) : (
|
|
1883
|
-
|
|
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-
|
|
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
|
|
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
|
|
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
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
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
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
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
|
-
|
|
3298
|
+
<span>Google Calendar</span>
|
|
2113
3299
|
</div>
|
|
2114
|
-
|
|
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
|
-
<
|
|
2117
|
-
{
|
|
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
|
-
|
|
3320
|
+
))}
|
|
2122
3321
|
</div>
|
|
2123
|
-
|
|
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
|
-
<
|
|
2187
|
-
<
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
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-
|
|
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="
|
|
2350
|
-
<
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
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
|
-
|
|
2488
|
-
|
|
2489
|
-
<
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
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
|
-
<
|
|
2764
|
-
<
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
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="
|
|
2981
|
-
<
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
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="
|
|
3111
|
-
<
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
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'ils sont prévenus par
|
|
4415
|
+
Cette note ne sera pas visible par les invités s'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'é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
|
-
?
|
|
3305
|
-
:
|
|
3306
|
-
</
|
|
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-
|
|
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
|
-
{/*
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
<div className="
|
|
3340
|
-
<div className="flex items-center gap-3">
|
|
3341
|
-
<div className="flex
|
|
3342
|
-
<
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
<
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
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
|
-
|
|
3360
|
-
)}
|
|
4536
|
+
)}
|
|
3361
4537
|
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
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
|
-
{/*
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
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
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
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
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
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
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
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'é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="
|
|
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
|
-
<
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3430
|
-
</label>
|
|
4684
|
+
</div>
|
|
3431
4685
|
</div>
|
|
3432
4686
|
|
|
3433
|
-
{
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
<
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
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="
|
|
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
|
-
|
|
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
|
}
|