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 c7b611c..f14814d 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; @@ -78,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); @@ -109,6 +111,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) { @@ -698,17 +713,22 @@ const Index = () => { Rendez-vous - +
+ + +
@@ -728,7 +748,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 +1196,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 +1277,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) => ( +
+ ))} +
+
+ )}
@@ -1903,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"> +
+ + +
+ +
+ +