From 822e0761a7a017eda0ee3724e636d6b5ab103c94 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Nov 2025 14:57:46 +0000 Subject: [PATCH] Feature: Ajout de rendez-vous Google Calendar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ajout d'un modal complet pour créer des événements directement dans Google Calendar avec toutes les fonctionnalités : Backend (routes/calendar.routes.js) : - Changement du scope de 'calendar.readonly' à 'calendar' pour permettre l'écriture - Nouvelle route POST /api/calendar/events pour créer des événements - Support complet des fonctionnalités Google Calendar : * Titre, description, dates de début/fin * Lieu * Participants (attendees) avec notifications automatiques * Rappels personnalisables (popup) * Récurrence (quotidienne, hebdomadaire, mensuelle, annuelle) * Visibilité (public, privé, par défaut) * Couleurs d'événement (11 couleurs disponibles) * Fuseau horaire Europe/Paris - Synchronisation automatique après création pour mise à jour locale Frontend : - Service CalendarService.ts : nouvelle méthode createEvent() - Bouton "Ajouter" dans la box Rendez-vous - Modal complet avec formulaire de création d'événement - Tous les champs Google Calendar disponibles : * Titre et description * Date/heure de début et fin (datetime-local) * Lieu * Participants (emails séparés par virgules) * Rappels (10min, 30min, 1h, 1 jour) * Récurrence (quotidienne, hebdomadaire, mensuelle, annuelle) * Visibilité (public/privé) * Couleur (11 options) - Validation des champs obligatoires - Messages de succès/erreur - Rechargement automatique de la liste après création --- routes/calendar.routes.js | 114 +++++++++++++++++- src/pages/Index.tsx | 200 ++++++++++++++++++++++++++++++-- src/services/CalendarService.ts | 19 +++ 3 files changed, 320 insertions(+), 13 deletions(-) diff --git a/routes/calendar.routes.js b/routes/calendar.routes.js index 34c552d..5c592f9 100644 --- a/routes/calendar.routes.js +++ b/routes/calendar.routes.js @@ -6,8 +6,8 @@ const { getAll, getOne, runQuery } = require('../config/database'); const { authenticateToken, requireAdmin } = require('../middleware/auth'); const logger = require('../config/logger'); -// Scopes Google Calendar requis -const SCOPES = ['https://www.googleapis.com/auth/calendar.readonly']; +// Scopes Google Calendar requis (lecture ET écriture) +const SCOPES = ['https://www.googleapis.com/auth/calendar']; /** * Types d'authentification supportés @@ -496,6 +496,116 @@ router.post('/disconnect', authenticateToken, async (req, res) => { } }); +/** + * POST /api/calendar/events + * Créer un événement dans Google Calendar + */ +router.post('/events', authenticateToken, async (req, res) => { + try { + const { + title, + description, + startDateTime, + endDateTime, + location, + attendees, + reminders, + recurrence, + visibility, + colorId + } = req.body; + + // 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 + 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'; + + // Créer l'événement dans Google Calendar + const response = await calendar.events.insert({ + calendarId: calendarId, + resource: event, + sendUpdates: attendees && attendees.length > 0 ? 'all' : 'none' + }); + + logger.info(`Événement créé: ${response.data.id} pour l'utilisateur ${req.user.username}`); + + // Synchroniser après création pour mettre à jour la base locale + try { + const events = await calendar.events.list({ + calendarId: calendarId, + timeMin: new Date().toISOString(), + maxResults: 10, + singleEvents: true, + orderBy: 'startTime' + }); + + if (events.data.items && events.data.items.length > 0) { + for (const item of events.data.items) { + await runQuery(` + INSERT OR REPLACE INTO calendar_events + (google_event_id, user_id, title, description, start_time, end_time, location, html_link, synced_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, datetime('now')) + `, [ + item.id, + req.user.id, + item.summary || 'Sans titre', + item.description || null, + item.start.dateTime || item.start.date, + item.end.dateTime || item.end.date, + item.location || null, + item.htmlLink || null + ]); + } + } + } catch (syncError) { + logger.warn('Erreur lors de la synchronisation post-création:', syncError); + } + + res.json({ + message: 'Événement créé avec succès', + eventId: response.data.id, + htmlLink: response.data.htmlLink + }); + + } catch (error) { + logger.error('Erreur lors de la création de l\'événement:', error); + res.status(500).json({ + error: 'Erreur lors de la création 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 44bc1cd..f14814d 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -79,6 +79,7 @@ const Index = () => { const [addNoteTodoModal, setAddNoteTodoModal] = useState(false); const [addTagModal, setAddTagModal] = useState(false); const [selectedImage, setSelectedImage] = useState(null); + const [addEventModal, setAddEventModal] = useState(false); // Debounce timer for auto-save const saveTimerRef = useRef(null); @@ -712,17 +713,22 @@ const Index = () => { Rendez-vous - +
+ + +
@@ -1933,6 +1939,178 @@ const Index = () => { confirmText="Ajouter" /> + {/* Add Event Modal */} + + + + Ajouter un rendez-vous + + Créer un événement dans votre Google Calendar + + + +
{ + e.preventDefault(); + const formData = new FormData(e.currentTarget); + + try { + 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: formData.get('reminderMinutes') ? [ + { method: 'popup', minutes: parseInt(formData.get('reminderMinutes') as string) } + ] : undefined, + recurrence: formData.get('recurrence') ? [formData.get('recurrence') as string] : undefined, + visibility: formData.get('visibility') as string || 'default', + colorId: formData.get('colorId') as string || undefined + }; + + await CalendarService.createEvent(eventData); + showSuccess('Événement créé avec succès'); + setAddEventModal(false); + await loadCalendarEvents(); + } catch (error: any) { + showError(error.response?.data?.error || 'Erreur lors de la création de l\'événement'); + } + }} className="space-y-4"> +
+ + +
+ +
+ +