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