diff --git a/CALENDAR_TIMEZONE_FIX.md b/CALENDAR_TIMEZONE_FIX.md new file mode 100644 index 0000000..56a5c9e --- /dev/null +++ b/CALENDAR_TIMEZONE_FIX.md @@ -0,0 +1,157 @@ +# Fix Décalage Horaire Calendrier + +## Problème + +Les événements du calendrier Google s'affichent avec 1 heure de décalage dans NoteFlow. + +## Cause Racine + +Le driver PostgreSQL (`pg`) convertit automatiquement les colonnes `TIMESTAMPTZ` en objets `Date` JavaScript, causant une double conversion de timezone: +1. Google Calendar → PostgreSQL (stockage en UTC) +2. PostgreSQL → JavaScript Date (conversion automatique en timezone locale) +3. Frontend → Affichage (nouvelle conversion) + +## Solution Appliquée + +### 1. Désactivation du parsing automatique (config/database.js) + +```javascript +const { Pool, types } = require('pg'); + +// IMPORTANT: Désactiver le parsing automatique des TIMESTAMPTZ +types.setTypeParser(1184, function(stringValue) { + return stringValue; // Renvoie la string ISO au lieu d'un objet Date +}); + +const pool = new Pool({...}); +``` + +### 2. Frontend avec timezone explicite (src/pages/Index.tsx) + +```javascript +startDate.toLocaleTimeString('fr-FR', { + hour: '2-digit', + minute: '2-digit', + timeZone: 'Europe/Paris' +}) +``` + +## Procédure de Fix + +### Étape 1: Diagnostic + +Vérifier si le fix est actif: + +```bash +node scripts/check-calendar-timezone.js +``` + +Ce script va vérifier: +- ✓ Si `start_time` est retourné comme `string` (bon) +- ✗ Si `start_time` est un `Date` object (mauvais - rebuild requis) + +### Étape 2: Rebuild du Container (SI NÉCESSAIRE) + +Si le diagnostic montre que les dates sont des `Date` objects: + +```bash +# 1. Arrêter le container +docker-compose down + +# 2. Rebuild SANS cache (important!) +docker-compose build --no-cache notes-app + +# 3. Redémarrer +docker-compose up -d + +# 4. Vérifier les logs +docker-compose logs -f notes-app +``` + +### Étape 3: Resynchronisation + +1. Ouvrir l'interface web NoteFlow +2. Aller dans Admin → Calendrier +3. Cliquer sur "Synchroniser avec Google Calendar" +4. Vérifier que les heures affichées sont correctes + +### Étape 4: Si le problème persiste + +Supprimer les événements et re-synchroniser: + +```bash +# Se connecter à PostgreSQL +docker exec -it noteflow-postgres psql -U noteflow -d noteflow -p 5499 + +# Supprimer tous les événements +DELETE FROM calendar_events; + +# Quitter +\q +``` + +Puis re-synchroniser depuis l'interface web. + +## Vérification Finale + +Comparer un événement dans Google Calendar et NoteFlow: + +**Google Calendar:** +- Rendez-vous: 14:00 + +**NoteFlow (devrait afficher):** +- Rendez-vous: 14:00 ✓ + +**Si NoteFlow affiche:** +- Rendez-vous: 15:00 ✗ → Reprendre la procédure depuis l'étape 2 + +## Notes Techniques + +### Types PostgreSQL + +- `TIMESTAMP` - Sans timezone, stocke la valeur brute +- `TIMESTAMPTZ` - Avec timezone, stocke en UTC + timezone + +### Type Parser ID + +- `1184` = TIMESTAMPTZ (timestamp with time zone) +- `1114` = TIMESTAMP (timestamp without time zone) + +### Timezone PostgreSQL + +Par défaut, PostgreSQL utilise UTC: + +```sql +SHOW timezone; +-- Résultat: UTC +``` + +### Timezone Node.js/Container + +```bash +echo $TZ +# Si vide, Node.js utilise la timezone système (généralement UTC dans Docker) +``` + +### Format des Dates Google Calendar + +Google Calendar API retourne: +- `event.start.dateTime`: `"2025-11-15T14:00:00+01:00"` (événement avec heure) +- `event.start.date`: `"2025-11-15"` (événement toute la journée) + +Ces valeurs sont stockées telles quelles dans PostgreSQL TIMESTAMPTZ. + +## Historique des Tentatives + +1. ✗ Changed TIMESTAMP → TIMESTAMPTZ +2. ✗ Migration with 'AT TIME ZONE UTC' +3. ✗ Migration with 'AT TIME ZONE Europe/Paris' +4. ✗ Added timeZone: 'Europe/Paris' to frontend +5. ✓ **types.setTypeParser(1184)** - Solution finale + +## Contact + +Si le problème persiste après toutes ces étapes, vérifier: +1. Version de `pg` dans package.json +2. Logs backend pendant la récupération des événements +3. Network tab du navigateur pour voir les données brutes retournées par l'API diff --git a/scripts/check-calendar-timezone.js b/scripts/check-calendar-timezone.js new file mode 100755 index 0000000..c58df2a --- /dev/null +++ b/scripts/check-calendar-timezone.js @@ -0,0 +1,111 @@ +#!/usr/bin/env node + +/** + * Script de diagnostic pour vérifier le parsing des TIMESTAMPTZ + * et le décalage horaire dans le calendrier + */ + +const { getAll, getOne } = require('../config/database'); +const logger = require('../config/logger'); + +async function checkTimezone() { + try { + console.log('\n=== DIAGNOSTIC TIMEZONE CALENDRIER ===\n'); + + // 1. Vérifier la timezone de PostgreSQL + const pgTimezone = await getOne('SHOW timezone'); + console.log('1. Timezone PostgreSQL:', pgTimezone.timezone); + + // 2. Vérifier la timezone système Node.js + const nodeTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone; + console.log('2. Timezone Node.js:', nodeTimezone); + console.log('3. TZ env var:', process.env.TZ || 'non défini'); + + // 3. Récupérer un événement de test + const event = await getOne(` + SELECT + id, + title, + start_time, + end_time, + all_day, + pg_typeof(start_time) as type_start + FROM calendar_events + ORDER BY start_time DESC + LIMIT 1 + `); + + if (!event) { + console.log('\n❌ Aucun événement trouvé dans la base de données'); + console.log(' Veuillez synchroniser avec Google Calendar d\'abord\n'); + return; + } + + console.log('\n4. Événement récupéré:'); + console.log(' - ID:', event.id); + console.log(' - Titre:', event.title); + console.log(' - Type SQL de start_time:', event.type_start); + console.log(' - start_time (brut):', event.start_time); + console.log(' - Type JS de start_time:', typeof event.start_time); + console.log(' - end_time (brut):', event.end_time); + console.log(' - Type JS de end_time:', typeof event.end_time); + console.log(' - all_day:', event.all_day); + + // 4. Tester la conversion en Date + if (typeof event.start_time === 'string') { + const dateObj = new Date(event.start_time); + console.log('\n5. Conversion en Date object:'); + console.log(' - new Date(start_time):', dateObj); + console.log(' - toISOString():', dateObj.toISOString()); + console.log(' - toLocaleString("fr-FR"):', dateObj.toLocaleString('fr-FR')); + console.log(' - toLocaleString("fr-FR", {timeZone: "Europe/Paris"}):', + dateObj.toLocaleString('fr-FR', { timeZone: 'Europe/Paris' })); + console.log(' - Offset timezone (minutes):', dateObj.getTimezoneOffset()); + } else { + console.log('\n⚠️ WARNING: start_time n\'est PAS une string!'); + console.log(' Le types.setTypeParser(1184) ne fonctionne pas correctement'); + console.log(' Le container doit être rebuild avec --no-cache'); + } + + // 5. Récupérer la valeur brute SQL + const rawEvent = await getOne(` + SELECT + start_time::text as start_text, + start_time AT TIME ZONE 'UTC' as start_utc, + start_time AT TIME ZONE 'Europe/Paris' as start_paris + FROM calendar_events + WHERE id = $1 + `, [event.id]); + + console.log('\n6. Valeurs SQL brutes:'); + console.log(' - start_time::text:', rawEvent.start_text); + console.log(' - start_time AT TIME ZONE \'UTC\':', rawEvent.start_utc); + console.log(' - start_time AT TIME ZONE \'Europe/Paris\':', rawEvent.start_paris); + + // 6. Compter les événements + const count = await getOne('SELECT COUNT(*) as count FROM calendar_events'); + console.log('\n7. Nombre total d\'événements:', count.count); + + console.log('\n=== FIN DIAGNOSTIC ===\n'); + + // Recommandations + if (typeof event.start_time !== 'string') { + console.log('⚠️ ACTION REQUISE:'); + console.log(' 1. Rebuild le container avec --no-cache:'); + console.log(' docker-compose build --no-cache notes-app'); + console.log(' 2. Restart le container:'); + console.log(' docker-compose restart notes-app'); + console.log(' 3. Resynchroniser le calendrier depuis l\'interface web\n'); + } else { + console.log('✓ types.setTypeParser fonctionne correctement'); + console.log(' Les dates sont retournées comme strings\n'); + } + + process.exit(0); + } catch (error) { + console.error('\n❌ Erreur:', error); + process.exit(1); + } +} + +checkTimezone();