mirror of
https://github.com/R0m1k3/noteflow.git
synced 2026-10-11 17:29:37 +02:00
Merge pull request #98 from R0m1k3/claude/fix-time-offset-bug-01FzrcjKh9o5emsU6hRMMLRL
Fix: Correction complète du décalage horaire d'1 heure dans le calend…
This commit is contained in:
4 files changed
+367
-12
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
@@ -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('═══════════════════════════════════════════════════════════');
|
||||
+76
-8
@@ -32,6 +32,74 @@ 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 {
|
||||
// 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();
|
||||
}
|
||||
|
||||
interface UserType {
|
||||
id: number;
|
||||
username: string;
|
||||
@@ -2061,8 +2129,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 +2173,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))}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
@@ -2115,7 +2183,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))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2239,8 +2307,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 +2350,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) : ''}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
@@ -2292,7 +2360,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) : ''}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user