From 1ae40ad84cb3bea911df890d9932610f77b36ae7 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Nov 2025 14:53:10 +0000 Subject: [PATCH 1/2] =?UTF-8?q?Feature:=20Am=C3=A9liorations=20UI=20des=20?= =?UTF-8?q?boxes=20Todo,=20RSS=20et=20Calendrier?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Modifications : 1. Box Flux RSS : - Correction du bug d'affichage de l'entête (boutons plus compacts) - Affichage de seulement 3 articles à la fois - Rotation automatique toutes les 20 secondes parmi les 15 derniers articles - Indicateurs de pagination (dots) pour visualiser la rotation - Taille de texte réduite pour plus de lisibilité 2. Box Todo List : - Interface plus compacte pour afficher plus de tâches - Padding réduit (p-2 au lieu de p-3) - Taille de texte réduite (text-sm) - Espacement vertical réduit (space-y-1.5 au lieu de space-y-3) - Hauteur maximale augmentée de 400px à 350px - Boutons et icônes plus petits 3. Box Calendrier : - Ajout d'une icône bleue animée (pulse) pour les RDV du jour - Meilleure identification visuelle des événements d'aujourd'hui --- src/pages/Index.tsx | 120 +++++++++++++++++++++++++++----------------- 1 file changed, 75 insertions(+), 45 deletions(-) diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index c7b611c..44bc1cd 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -56,6 +56,7 @@ const Index = () => { const [loadingModels, setLoadingModels] = useState(false); const [currentDateTime, setCurrentDateTime] = useState(new Date()); const [upcomingAlert, setUpcomingAlert] = useState(null); + const [rssDisplayOffset, setRssDisplayOffset] = useState(0); const [calendarAuthStatus, setCalendarAuthStatus] = useState<{ isAuthenticated: boolean; isExpired: boolean; @@ -109,6 +110,19 @@ const Index = () => { return () => clearInterval(timer); }, [calendarEvents]); + // RSS articles rotation every 20 seconds + useEffect(() => { + if (rssArticles.length > 3) { + const timer = setInterval(() => { + setRssDisplayOffset(prev => { + const maxOffset = Math.max(0, rssArticles.length - 3); + return (prev + 3) % (maxOffset + 3); + }); + }, 20000); // Rotate every 20 seconds + return () => clearInterval(timer); + } + }, [rssArticles.length]); + useEffect(() => { const userFromAuth = AuthService.getUser(); if (userFromAuth) { @@ -728,7 +742,12 @@ const Index = () => { >
-

{event.title}

+
+ {isToday && ( +
+ )} +

{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' })}

@@ -1171,79 +1190,78 @@ const Index = () => {
{/* Todos Box */} - +
- + Tâches -
- - Actives ({todos.filter(t => !t.completed).length}) - Complétées ({todos.filter(t => t.completed).length}) + + Actives ({todos.filter(t => !t.completed).length}) + Complétées ({todos.filter(t => t.completed).length}) - -
+ +
{todos.filter(t => !t.completed).length > 0 ? ( todos.filter(t => !t.completed).map(todo => ( -
+
todo.id && handleToggleTodo(todo.id)} - className="h-5 w-5" + className="h-4 w-4" /> - + {todo.text}
)) ) : ( -

Aucune tâche active

+

Aucune tâche active

)}
- -
+ +
{todos.filter(t => t.completed).length > 0 ? ( todos.filter(t => t.completed).map(todo => ( -
+
todo.id && handleToggleTodo(todo.id)} - className="h-5 w-5" + className="h-4 w-4" /> - + {todo.text}
)) ) : ( -

Aucune tâche complétée

+

Aucune tâche complétée

)}
@@ -1253,59 +1271,71 @@ const Index = () => { {/* RSS Box */} - -
- - + +
+ + Flux RSS -
- -
-
+
{rssArticles.length > 0 ? ( - rssArticles.map(article => ( + rssArticles.slice(rssDisplayOffset, rssDisplayOffset + 3).map(article => (
article.link && window.open(article.link, '_blank')} > -
+
-

{article.title}

+

{article.title}

{article.description && ( -

+

{article.description.replace(/<[^>]*>/g, '')}

)}
- +
)) ) : ( -

+

Aucun article

)} + {rssArticles.length > 3 && ( +
+
+ {Array.from({ length: Math.ceil(rssArticles.length / 3) }).map((_, i) => ( +
+ ))} +
+
+ )}
From 822e0761a7a017eda0ee3724e636d6b5ab103c94 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Nov 2025 14:57:46 +0000 Subject: [PATCH 2/2] 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"> +
+ + +
+ +
+ +