From c103a517c8169e2ce546c302f30138fe089ca6b6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 16 Nov 2025 11:06:21 +0000 Subject: [PATCH 1/2] =?UTF-8?q?Fix:=20Correction=20compl=C3=A8te=20du=20d?= =?UTF-8?q?=C3=A9calage=20horaire=20d'1=20heure=20dans=20le=20calendrier?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problèmes corrigés: - Ajout de fonctions utilitaires toLocalDateTimeString() et toParisISO() pour gérer correctement les conversions de timezone Europe/Paris - Correction des valeurs par défaut des champs datetime-local lors de la création d'événements (utilisaient toISOString().slice() incorrectement) - Correction des valeurs par défaut lors de l'édition d'événements - Correction de la soumission des formulaires (création et édition) pour envoyer les dates ISO correctes avec timezone Europe/Paris Les dates sont désormais: 1. Affichées correctement en heure Europe/Paris dans les formulaires 2. Converties correctement lors de l'envoi au backend 3. Stockées et récupérées sans décalage horaire Fichiers modifiés: - src/pages/Index.tsx: Ajout des fonctions utilitaires et correction de toutes les conversions de dates pour les événements du calendrier --- src/pages/Index.tsx | 86 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 78 insertions(+), 8 deletions(-) diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index dd7c7ba..0621f4d 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -32,6 +32,76 @@ import { ConfirmModal } from "@/components/modals/ConfirmModal"; import { InputModal } from "@/components/modals/InputModal"; import { AddUserModal } from "@/components/modals/AddUserModal"; +// ===== FONCTIONS UTILITAIRES TIMEZONE EUROPE/PARIS ===== + +/** + * Convertit une date ISO/UTC en format datetime-local pour Europe/Paris + * @param date - Date ISO string ou Date object + * @returns String au format "YYYY-MM-DDTHH:mm" en heure Europe/Paris + */ +function toLocalDateTimeString(date: string | Date): string { + const d = typeof date === 'string' ? new Date(date) : date; + + // Formater en heure Europe/Paris + const formatter = new Intl.DateTimeFormat('fr-FR', { + timeZone: 'Europe/Paris', + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + hour12: false + }); + + const parts = formatter.formatToParts(d); + const year = parts.find(p => p.type === 'year')?.value; + const month = parts.find(p => p.type === 'month')?.value; + const day = parts.find(p => p.type === 'day')?.value; + const hour = parts.find(p => p.type === 'hour')?.value; + const minute = parts.find(p => p.type === 'minute')?.value; + + return `${year}-${month}-${day}T${hour}:${minute}`; +} + +/** + * Convertit une valeur datetime-local en ISO string pour Europe/Paris + * @param localDateTimeString - String au format "YYYY-MM-DDTHH:mm" (sans timezone) + * @returns ISO string représentant cette heure en Europe/Paris + */ +function toParisISO(localDateTimeString: string): string { + // Le champ datetime-local renvoie une string locale (ex: "2024-11-16T14:30") + // On doit l'interpréter comme étant en Europe/Paris, puis convertir en ISO + + // Parser les composants de la date + const [datePart, timePart] = localDateTimeString.split('T'); + const [year, month, day] = datePart.split('-').map(Number); + const [hour, minute] = timePart.split(':').map(Number); + + // Créer une date "fictive" en UTC avec ces valeurs + const utcDate = new Date(Date.UTC(year, month - 1, day, hour, minute, 0)); + + // Formater cette date en tant que date Paris + const parisTime = utcDate.toLocaleString('en-US', { + timeZone: 'Europe/Paris', + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false + }); + + // Calculer la différence entre l'heure voulue et l'heure obtenue + const parisDate = new Date(parisTime); + const diff = utcDate.getTime() - parisDate.getTime(); + + // Créer la date finale en ajustant + const finalDate = new Date(utcDate.getTime() - diff); + + return finalDate.toISOString(); +} + interface UserType { id: number; username: string; @@ -2061,8 +2131,8 @@ const Index = () => { 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(), + startDateTime: toParisISO(formData.get('startDateTime') as string), + endDateTime: toParisISO(formData.get('endDateTime') as string), 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' ? [ @@ -2105,7 +2175,7 @@ const Index = () => { name="startDateTime" type="datetime-local" required - defaultValue={new Date(Date.now() + 60*60*1000).toISOString().slice(0, 16)} + defaultValue={toLocalDateTimeString(new Date(Date.now() + 60*60*1000))} />
@@ -2115,7 +2185,7 @@ const Index = () => { name="endDateTime" type="datetime-local" required - defaultValue={new Date(Date.now() + 2*60*60*1000).toISOString().slice(0, 16)} + defaultValue={toLocalDateTimeString(new Date(Date.now() + 2*60*60*1000))} />
@@ -2239,8 +2309,8 @@ const Index = () => { 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(), + startDateTime: toParisISO(formData.get('startDateTime') as string), + endDateTime: toParisISO(formData.get('endDateTime') as string), 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' ? [ @@ -2282,7 +2352,7 @@ const Index = () => { name="startDateTime" type="datetime-local" required - defaultValue={editEventModal.event ? new Date(editEventModal.event.start_time).toISOString().slice(0, 16) : ''} + defaultValue={editEventModal.event ? toLocalDateTimeString(editEventModal.event.start_time) : ''} />
@@ -2292,7 +2362,7 @@ const Index = () => { name="endDateTime" type="datetime-local" required - defaultValue={editEventModal.event ? new Date(editEventModal.event.end_time).toISOString().slice(0, 16) : ''} + defaultValue={editEventModal.event ? toLocalDateTimeString(editEventModal.event.end_time) : ''} />
From b123193c9a910f0f83a9d51a5fdc9ca958b765a4 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 16 Nov 2025 11:27:29 +0000 Subject: [PATCH 2/2] =?UTF-8?q?Fix:=20Correction=20d=C3=A9finitive=20du=20?= =?UTF-8?q?d=C3=A9calage=20horaire=20dans=20le=20calendrier?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PROBLÈME IDENTIFIÉ: Le backend envoyait des dates ISO UTC à Google Calendar API avec timeZone: 'Europe/Paris', ce qui causait une double interprétation. Google interprétait l'heure UTC comme étant l'heure locale de Paris, créant ainsi un décalage d'1 heure. CORRECTIONS APPORTÉES: 1. Frontend (src/pages/Index.tsx): - Réécriture de toParisISO() pour convertir correctement les dates datetime-local en ISO UTC avec gestion automatique de l'heure d'été/hiver - Calcul dynamique de l'offset Europe/Paris (+01:00 hiver, +02:00 été) - Tests de validation confirmant le bon fonctionnement (round-trip OK) 2. Backend (routes/calendar.routes.js): - Retrait de timeZone: 'Europe/Paris' lors de la création d'événements - Retrait de timeZone: 'Europe/Paris' lors de la mise à jour d'événements - Google Calendar API interprète maintenant correctement les ISO UTC 3. Scripts de test ajoutés: - scripts/test-timezone-functions.js: Validation des conversions - scripts/diagnose-timezone.js: Diagnostic du flux complet RÉSULTAT: Les heures affichées dans NoteFlow sont maintenant identiques à celles de Google Calendar, sans aucun décalage horaire. Tests effectués: ✅ Conversion datetime-local → ISO UTC (hiver): 14:30 → 13:30 UTC ✅ Conversion datetime-local → ISO UTC (été): 14:30 → 12:30 UTC ✅ Round-trip: conservation de la valeur d'origine ✅ Affichage correct en Europe/Paris depuis ISO UTC ✅ Compilation réussie --- routes/calendar.routes.js | 9 +- scripts/diagnose-timezone.js | 149 +++++++++++++++++++++++++++++ scripts/test-timezone-functions.js | 137 ++++++++++++++++++++++++++ src/pages/Index.tsx | 36 ++++--- 4 files changed, 308 insertions(+), 23 deletions(-) create mode 100644 scripts/diagnose-timezone.js create mode 100644 scripts/test-timezone-functions.js diff --git a/routes/calendar.routes.js b/routes/calendar.routes.js index 6251028..7b51c67 100644 --- a/routes/calendar.routes.js +++ b/routes/calendar.routes.js @@ -528,11 +528,12 @@ router.post('/events', authenticateToken, async (req, res) => { description: description || undefined, start: { dateTime: startDateTime, - timeZone: 'Europe/Paris', + // Note: startDateTime est déjà en ISO UTC (ex: "2024-11-16T13:30:00.000Z") + // Google Calendar l'interprétera correctement sans spécifier timeZone }, end: { dateTime: endDateTime, - timeZone: 'Europe/Paris', + // Note: endDateTime est déjà en ISO UTC }, attendees: attendees && attendees.length > 0 ? attendees.map(email => ({ email })) : undefined, @@ -648,11 +649,11 @@ router.put('/events/:id', authenticateToken, async (req, res) => { description: description || undefined, start: { dateTime: startDateTime, - timeZone: 'Europe/Paris', + // Note: startDateTime est déjà en ISO UTC }, end: { dateTime: endDateTime, - timeZone: 'Europe/Paris', + // Note: endDateTime est déjà en ISO UTC }, attendees: attendees && attendees.length > 0 ? attendees.map(email => ({ email })) : undefined, diff --git a/scripts/diagnose-timezone.js b/scripts/diagnose-timezone.js new file mode 100644 index 0000000..f775f69 --- /dev/null +++ b/scripts/diagnose-timezone.js @@ -0,0 +1,149 @@ +// Script de diagnostic des problèmes de timezone +const { Pool } = require('pg'); + +const DATABASE_URL = process.env.DATABASE_URL || + `postgresql://${process.env.PGUSER || 'noteflow'}:${process.env.PGPASSWORD || 'noteflow_secure_password_change_me'}@${process.env.PGHOST || 'localhost'}:${process.env.PGPORT || '5499'}/${process.env.PGDATABASE || 'noteflow'}`; + +const pool = new Pool({ + connectionString: DATABASE_URL +}); + +async function diagnose() { + console.log('═══════════════════════════════════════════════════════════'); + console.log('📊 DIAGNOSTIC TIMEZONE CALENDRIER'); + console.log('═══════════════════════════════════════════════════════════\n'); + + try { + // 1. Vérifier le timezone de PostgreSQL + const tzResult = await pool.query('SHOW timezone'); + console.log('1️⃣ TIMEZONE POSTGRESQL:'); + console.log(' Timezone serveur:', tzResult.rows[0].TimeZone); + console.log(''); + + // 2. Récupérer un événement de la base + const eventResult = await pool.query(` + SELECT + id, + title, + start_time, + end_time, + pg_typeof(start_time) as type_start + FROM calendar_events + ORDER BY start_time DESC + LIMIT 1 + `); + + if (eventResult.rows.length === 0) { + console.log('⚠️ Aucun événement trouvé dans la base de données'); + process.exit(0); + } + + const event = eventResult.rows[0]; + console.log('2️⃣ ÉVÉNEMENT RÉCUPÉRÉ DE LA BASE:'); + console.log(' Titre:', event.title); + console.log(' Type de start_time:', event.type_start); + console.log(' start_time (brut):', event.start_time); + console.log(' Type JavaScript:', typeof event.start_time); + console.log(''); + + // 3. Tester différentes représentations de la date + console.log('3️⃣ CONVERSIONS DE LA DATE:'); + + const startTime = event.start_time; + + // Si c'est un objet Date + if (startTime instanceof Date) { + console.log(' ❌ start_time est un OBJET DATE (le parser est actif)'); + console.log(' toISOString():', startTime.toISOString()); + console.log(' toLocaleString("fr-FR", {timeZone: "Europe/Paris"}):', + startTime.toLocaleString('fr-FR', {timeZone: 'Europe/Paris'})); + } + // Si c'est une string + else if (typeof startTime === 'string') { + console.log(' ✅ start_time est une STRING (parser désactivé)'); + console.log(' String brute:', startTime); + + const dateObj = new Date(startTime); + console.log(' new Date(startTime):', dateObj); + console.log(' toISOString():', dateObj.toISOString()); + console.log(' toLocaleString("fr-FR", {timeZone: "Europe/Paris"}):', + dateObj.toLocaleString('fr-FR', {timeZone: 'Europe/Paris'})); + console.log(' toLocaleTimeString("fr-FR", {hour: "2-digit", minute: "2-digit", timeZone: "Europe/Paris"}):', + dateObj.toLocaleTimeString('fr-FR', {hour: '2-digit', minute: '2-digit', timeZone: 'Europe/Paris'})); + } + console.log(''); + + // 4. Tester les formats de stockage PostgreSQL + console.log('4️⃣ FORMATS POSTGRESQL:'); + const formatResult = await pool.query(` + SELECT + start_time, + start_time::text as text_format, + to_char(start_time, 'YYYY-MM-DD HH24:MI:SS TZ') as custom_format, + start_time AT TIME ZONE 'Europe/Paris' as paris_time, + start_time AT TIME ZONE 'UTC' as utc_time + FROM calendar_events + WHERE id = $1 + `, [event.id]); + + const formats = formatResult.rows[0]; + console.log(' start_time:', formats.start_time); + console.log(' start_time::text:', formats.text_format); + console.log(' to_char (custom):', formats.custom_format); + console.log(' AT TIME ZONE Europe/Paris:', formats.paris_time); + console.log(' AT TIME ZONE UTC:', formats.utc_time); + console.log(''); + + // 5. Simuler ce que reçoit le frontend + console.log('5️⃣ SIMULATION FRONTEND:'); + const apiResult = await pool.query(` + SELECT id, title, start_time, end_time + FROM calendar_events + WHERE id = $1 + `, [event.id]); + + console.log(' Ce que l\'API renvoie:', JSON.stringify(apiResult.rows[0], null, 2)); + console.log(''); + + // 6. Tester la fonction toParisISO (simulation) + console.log('6️⃣ TEST FONCTION toParisISO:'); + console.log(' Input exemple: "2024-11-16T14:30"'); + + const testInput = "2024-11-16T14:30"; + const [datePart, timePart] = testInput.split('T'); + const [year, month, day] = datePart.split('-').map(Number); + const [hour, minute] = timePart.split(':').map(Number); + + const utcDate = new Date(Date.UTC(year, month - 1, day, hour, minute, 0)); + console.log(' Date.UTC créée:', utcDate.toISOString()); + + const parisTime = utcDate.toLocaleString('en-US', { + timeZone: 'Europe/Paris', + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false + }); + console.log(' toLocaleString avec Europe/Paris:', parisTime); + + const parisDate = new Date(parisTime); + const diff = utcDate.getTime() - parisDate.getTime(); + const finalDate = new Date(utcDate.getTime() - diff); + console.log(' Date finale (toParisISO):', finalDate.toISOString()); + console.log(''); + + console.log('═══════════════════════════════════════════════════════════'); + console.log('✅ Diagnostic terminé'); + console.log('═══════════════════════════════════════════════════════════'); + + } catch (error) { + console.error('❌ Erreur:', error); + } finally { + await pool.end(); + } +} + +diagnose(); diff --git a/scripts/test-timezone-functions.js b/scripts/test-timezone-functions.js new file mode 100644 index 0000000..cc5ef6b --- /dev/null +++ b/scripts/test-timezone-functions.js @@ -0,0 +1,137 @@ +// Test des fonctions de conversion timezone +console.log('═══════════════════════════════════════════════════════════'); +console.log('🧪 TEST DES FONCTIONS DE TIMEZONE'); +console.log('═══════════════════════════════════════════════════════════\n'); + +// Fonction toParisISO +function toParisISO(localDateTimeString) { + // Input: "2024-11-16T14:30" signifie 14:30 à Paris + // Output: ISO UTC correspondant (ex: "2024-11-16T13:30:00.000Z" en hiver) + + const [datePart, timePart] = localDateTimeString.split('T'); + const [year, month, day] = datePart.split('-').map(Number); + const [hour, minute] = timePart.split(':').map(Number); + + // Déterminer l'offset de Europe/Paris pour cette date spécifique + // (pour gérer automatiquement l'heure d'été/hiver) + + // Créer une date test en UTC à midi + const testDate = new Date(Date.UTC(year, month - 1, day, 12, 0, 0)); + + // Formater cette date en Europe/Paris + const parisHour = parseInt(testDate.toLocaleString('en-US', { + timeZone: 'Europe/Paris', + hour: '2-digit', + hour12: false + })); + + // Calculer l'offset (normalement +1 en hiver, +2 en été) + const offset = parisHour - 12; + const offsetString = offset === 1 ? '+01:00' : '+02:00'; + + // Construire l'ISO string avec le timezone de Paris + const isoWithTZ = `${datePart}T${timePart.padEnd(5, '0')}:00${offsetString}`; + + // Créer la date (JavaScript va automatiquement convertir en UTC) + return new Date(isoWithTZ).toISOString(); +} + +// Fonction toLocalDateTimeString +function toLocalDateTimeString(date) { + const d = typeof date === 'string' ? new Date(date) : date; + + // Formater en heure Europe/Paris + const formatter = new Intl.DateTimeFormat('fr-FR', { + timeZone: 'Europe/Paris', + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + hour12: false + }); + + const parts = formatter.formatToParts(d); + const year = parts.find(p => p.type === 'year')?.value; + const month = parts.find(p => p.type === 'month')?.value; + const day = parts.find(p => p.type === 'day')?.value; + const hour = parts.find(p => p.type === 'hour')?.value; + const minute = parts.find(p => p.type === 'minute')?.value; + + return `${year}-${month}-${day}T${hour}:${minute}`; +} + +// Test 1: Date en hiver (novembre - UTC+1) +console.log('Test 1: Date en hiver (16 novembre 2024, 14:30 à Paris)'); +const winterInput = '2024-11-16T14:30'; +const winterISO = toParisISO(winterInput); +console.log(' Input (datetime-local):', winterInput); +console.log(' Output (ISO UTC):', winterISO); + +const winterDate = new Date(winterISO); +const winterParis = winterDate.toLocaleString('fr-FR', { + timeZone: 'Europe/Paris', + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit' +}); +console.log(' Vérification (affichage Paris):', winterParis); +console.log(' ✓ Devrait être: 16/11/2024, 14:30:00'); +console.log(''); + +// Test 2: Date en été (juillet - UTC+2) +console.log('Test 2: Date en été (15 juillet 2024, 14:30 à Paris)'); +const summerInput = '2024-07-15T14:30'; +const summerISO = toParisISO(summerInput); +console.log(' Input (datetime-local):', summerInput); +console.log(' Output (ISO UTC):', summerISO); + +const summerDate = new Date(summerISO); +const summerParis = summerDate.toLocaleString('fr-FR', { + timeZone: 'Europe/Paris', + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit' +}); +console.log(' Vérification (affichage Paris):', summerParis); +console.log(' ✓ Devrait être: 15/07/2024, 14:30:00'); +console.log(''); + +// Test 3: Round-trip (toParisISO puis toLocalDateTimeString) +console.log('Test 3: Round-trip (conversion aller-retour)'); +const originalInput = '2024-11-16T14:30'; +console.log(' Original:', originalInput); + +const iso = toParisISO(originalInput); +console.log(' → ISO:', iso); + +const backToLocal = toLocalDateTimeString(iso); +console.log(' → Back to local:', backToLocal); + +if (originalInput === backToLocal) { + console.log(' ✅ SUCCÈS: Round-trip conserve la valeur'); +} else { + console.log(' ❌ ÉCHEC: Round-trip ne conserve pas la valeur'); + console.log(' Attendu:', originalInput); + console.log(' Obtenu:', backToLocal); +} +console.log(''); + +// Test 4: toLocalDateTimeString avec une date ISO UTC +console.log('Test 4: toLocalDateTimeString avec ISO UTC'); +const utcISO = '2024-11-16T13:30:00.000Z'; // 13:30 UTC = 14:30 Paris (hiver) +const localResult = toLocalDateTimeString(utcISO); +console.log(' Input (ISO UTC):', utcISO); +console.log(' Output (datetime-local):', localResult); +console.log(' ✓ Devrait être: 2024-11-16T14:30'); +console.log(''); + +console.log('═══════════════════════════════════════════════════════════'); +console.log('✅ Tests terminés'); +console.log('═══════════════════════════════════════════════════════════'); diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index 0621f4d..43a55fb 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -69,37 +69,35 @@ function toLocalDateTimeString(date: string | Date): string { * @returns ISO string représentant cette heure en Europe/Paris */ function toParisISO(localDateTimeString: string): string { - // Le champ datetime-local renvoie une string locale (ex: "2024-11-16T14:30") - // On doit l'interpréter comme étant en Europe/Paris, puis convertir en ISO + // Input: "2024-11-16T14:30" signifie 14:30 à Paris + // Output: ISO UTC correspondant (ex: "2024-11-16T13:30:00.000Z" en hiver) - // Parser les composants de la date const [datePart, timePart] = localDateTimeString.split('T'); const [year, month, day] = datePart.split('-').map(Number); const [hour, minute] = timePart.split(':').map(Number); - // Créer une date "fictive" en UTC avec ces valeurs - const utcDate = new Date(Date.UTC(year, month - 1, day, hour, minute, 0)); + // Déterminer l'offset de Europe/Paris pour cette date spécifique + // (pour gérer automatiquement l'heure d'été/hiver) - // Formater cette date en tant que date Paris - const parisTime = utcDate.toLocaleString('en-US', { + // Créer une date test en UTC à midi + const testDate = new Date(Date.UTC(year, month - 1, day, 12, 0, 0)); + + // Formater cette date en Europe/Paris + const parisHour = parseInt(testDate.toLocaleString('en-US', { timeZone: 'Europe/Paris', - year: 'numeric', - month: '2-digit', - day: '2-digit', hour: '2-digit', - minute: '2-digit', - second: '2-digit', hour12: false - }); + })); - // Calculer la différence entre l'heure voulue et l'heure obtenue - const parisDate = new Date(parisTime); - const diff = utcDate.getTime() - parisDate.getTime(); + // Calculer l'offset (normalement +1 en hiver, +2 en été) + const offset = parisHour - 12; + const offsetString = offset === 1 ? '+01:00' : '+02:00'; - // Créer la date finale en ajustant - const finalDate = new Date(utcDate.getTime() - diff); + // Construire l'ISO string avec le timezone de Paris + const isoWithTZ = `${datePart}T${timePart.padEnd(5, '0')}:00${offsetString}`; - return finalDate.toISOString(); + // Créer la date (JavaScript va automatiquement convertir en UTC) + return new Date(isoWithTZ).toISOString(); } interface UserType {