From 735b93a69f7de988c89a6203cc7132ce41e0d19c Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Nov 2025 22:03:32 +0000 Subject: [PATCH] =?UTF-8?q?Feature:=20Gestion=20=C3=A9v=C3=A9nements=20tou?= =?UTF-8?q?te=20la=20journ=C3=A9e=20et=20=C3=A9dition=20RDV?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/database.js | 21 ++++ routes/calendar.routes.js | 118 +++++++++++++++++- src/pages/Index.tsx | 209 +++++++++++++++++++++++++++++++- src/services/CalendarService.ts | 20 +++ 4 files changed, 359 insertions(+), 9 deletions(-) diff --git a/config/database.js b/config/database.js index 4649db5..9612588 100644 --- a/config/database.js +++ b/config/database.js @@ -286,6 +286,27 @@ function initDatabase() { } }); + // Migration pour calendar_events - ajouter all_day + db.all("PRAGMA table_info(calendar_events)", (err, columns) => { + if (err) { + logger.error('Erreur lors de la vérification de la structure de la table calendar_events:', err); + return; + } + + const hasAllDay = columns.some(col => col.name === 'all_day'); + + if (!hasAllDay) { + logger.info('Migration: Ajout de la colonne all_day à calendar_events...'); + db.run('ALTER TABLE calendar_events ADD COLUMN all_day BOOLEAN DEFAULT 0', (err) => { + if (err) { + logger.error('Erreur lors de l\'ajout de la colonne all_day:', err); + } else { + logger.info('✓ Colonne all_day ajoutée avec succès'); + } + }); + } + }); + // Créer l'utilisateur admin par défaut si la table est vide db.get('SELECT COUNT(*) as count FROM users', async (err, row) => { if (err) { diff --git a/routes/calendar.routes.js b/routes/calendar.routes.js index 5c592f9..516c13a 100644 --- a/routes/calendar.routes.js +++ b/routes/calendar.routes.js @@ -412,6 +412,7 @@ router.post('/sync', authenticateToken, async (req, res) => { for (const event of events) { const startTime = event.start.dateTime || event.start.date; const endTime = event.end.dateTime || event.end.date; + const isAllDay = !event.start.dateTime; // Si pas de dateTime, c'est un événement toute la journée // Vérifier si l'événement existe déjà const existing = await getOne( @@ -424,7 +425,7 @@ router.post('/sync', authenticateToken, async (req, res) => { await runQuery(` UPDATE calendar_events SET title = ?, description = ?, start_time = ?, end_time = ?, - location = ?, html_link = ?, synced_at = CURRENT_TIMESTAMP + location = ?, html_link = ?, all_day = ?, synced_at = CURRENT_TIMESTAMP WHERE id = ? `, [ event.summary || 'Sans titre', @@ -433,14 +434,15 @@ router.post('/sync', authenticateToken, async (req, res) => { endTime, event.location || '', event.htmlLink || '', + isAllDay ? 1 : 0, existing.id ]); } else { // Créer un nouvel événement await runQuery(` INSERT INTO calendar_events - (google_event_id, user_id, title, description, start_time, end_time, location, html_link) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) + (google_event_id, user_id, title, description, start_time, end_time, location, html_link, all_day) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) `, [ event.id, req.user.id, @@ -449,7 +451,8 @@ router.post('/sync', authenticateToken, async (req, res) => { startTime, endTime, event.location || '', - event.htmlLink || '' + event.htmlLink || '', + isAllDay ? 1 : 0 ]); } syncedCount++; @@ -606,6 +609,113 @@ router.post('/events', authenticateToken, async (req, res) => { } }); +/** + * PUT /api/calendar/events/:id + * Mettre à jour un événement dans Google Calendar + */ +router.put('/events/:id', authenticateToken, async (req, res) => { + try { + const { + title, + description, + startDateTime, + endDateTime, + location, + attendees, + reminders, + recurrence, + visibility, + colorId + } = req.body; + + // Récupérer l'événement existant depuis la DB + const dbEvent = await getOne( + 'SELECT google_event_id FROM calendar_events WHERE id = ? AND user_id = ?', + [req.params.id, req.user.id] + ); + + if (!dbEvent) { + return res.status(404).json({ error: 'Événement non trouvé' }); + } + + // Validation + if (!title || !startDateTime || !endDateTime) { + return res.status(400).json({ error: 'Titre, date de début et date de fin requis' }); + } + + const calendar = await getCalendarClient(req.user.id); + + // Préparer l'événement mis à jour + const event = { + summary: title, + location: location || undefined, + description: description || undefined, + start: { + dateTime: startDateTime, + timeZone: 'Europe/Paris', + }, + end: { + dateTime: endDateTime, + timeZone: 'Europe/Paris', + }, + attendees: attendees && attendees.length > 0 ? + attendees.map(email => ({ email })) : undefined, + reminders: reminders ? { + useDefault: false, + overrides: reminders + } : undefined, + recurrence: recurrence || undefined, + visibility: visibility || 'default', + colorId: colorId || undefined + }; + + // Récupérer le Calendar ID depuis les settings + const calendarIdSetting = await getOne("SELECT value FROM settings WHERE key = 'google_calendar_id'"); + const calendarId = calendarIdSetting?.value || 'primary'; + + // Mettre à jour l'événement dans Google Calendar + const response = await calendar.events.update({ + calendarId: calendarId, + eventId: dbEvent.google_event_id, + resource: event, + sendUpdates: attendees && attendees.length > 0 ? 'all' : 'none' + }); + + logger.info(`Événement mis à jour: ${response.data.id} pour l'utilisateur ${req.user.username}`); + + // Mettre à jour dans la base de données locale + const isAllDay = !startDateTime.includes('T') && !endDateTime.includes('T'); + await runQuery(` + UPDATE calendar_events + SET title = ?, description = ?, start_time = ?, end_time = ?, + location = ?, html_link = ?, all_day = ?, synced_at = CURRENT_TIMESTAMP + WHERE id = ? + `, [ + title, + description || null, + startDateTime, + endDateTime, + location || null, + response.data.htmlLink || null, + isAllDay ? 1 : 0, + req.params.id + ]); + + res.json({ + message: 'Événement mis à jour avec succès', + eventId: response.data.id, + htmlLink: response.data.htmlLink + }); + + } catch (error) { + logger.error('Erreur lors de la mise à jour de l\'événement:', error); + res.status(500).json({ + error: 'Erreur lors de la mise à jour de l\'événement', + details: error.message + }); + } +}); + /** * DELETE /api/calendar/events/:id * Supprimer un événement local diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index fee8952..272061e 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -78,6 +78,7 @@ const Index = () => { const [addTagModal, setAddTagModal] = useState(false); const [selectedImage, setSelectedImage] = useState(null); const [addEventModal, setAddEventModal] = useState(false); + const [editEventModal, setEditEventModal] = useState<{open: boolean, event?: CalendarEvent}>({open: false}); // Pagination states const [notesPage, setNotesPage] = useState(0); @@ -856,11 +857,13 @@ const Index = () => { return (
event.html_link && window.open(event.html_link, '_blank')} + className={`p-3 border rounded-lg hover:bg-accent/50 transition-colors group relative ${isSoon ? 'border-red-500 bg-red-50 dark:bg-red-900/20' : ''}`} >
-
+
event.html_link && window.open(event.html_link, '_blank')} + >
{isToday && (
@@ -868,7 +871,8 @@ const Index = () => {

{event.title}

- {isToday ? "Aujourd'hui" : startDate.toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit' })} à {startDate.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })} + {isToday ? "Aujourd'hui" : startDate.toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit' })} + {event.all_day ? ' - Toute la journée' : ` à ${startDate.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })}`}

{event.location && (

@@ -879,7 +883,23 @@ const Index = () => { Dans moins de 30 min )}

- +
+ + event.html_link && window.open(event.html_link, '_blank')} + /> +
); @@ -2286,6 +2306,185 @@ const Index = () => { + {/* Edit Event Modal */} + setEditEventModal({open, event: undefined})}> + + + Modifier le rendez-vous + + Modifier l'événement dans votre Google Calendar + + + +
{ + e.preventDefault(); + if (!editEventModal.event?.id) return; + + const formData = new FormData(e.currentTarget); + + try { + const reminderMinutes = formData.get('reminderMinutes') as string; + const recurrenceValue = formData.get('recurrence') as string; + + const eventData = { + title: formData.get('title') as string, + description: formData.get('description') as string || undefined, + startDateTime: new Date(formData.get('startDateTime') as string).toISOString(), + endDateTime: new Date(formData.get('endDateTime') as string).toISOString(), + location: formData.get('location') as string || undefined, + attendees: (formData.get('attendees') as string || '').split(',').map(e => e.trim()).filter(e => e), + reminders: reminderMinutes && reminderMinutes !== '0' ? [ + { method: 'popup', minutes: parseInt(reminderMinutes) } + ] : undefined, + recurrence: recurrenceValue ? [recurrenceValue] : undefined, + visibility: formData.get('visibility') as string || 'default', + colorId: formData.get('colorId') as string || undefined + }; + + await CalendarService.updateEvent(editEventModal.event.id, eventData); + showSuccess('Événement mis à jour avec succès'); + setEditEventModal({open: false, event: undefined}); + await loadCalendarEvents(); + } catch (error: any) { + showError(error.response?.data?.error || 'Erreur lors de la mise à jour de l\'événement'); + } + }} className="space-y-4"> +
+ + +
+ +
+ +