Architecture de Synchronisation Calendrier
Ce document décrit l'architecture complète de la synchronisation bidirectionnelle entre le CRM et Google Calendar, ainsi que la gestion de disponibilité.
Vue d'ensemble
Le système de synchronisation calendrier permet :
- Synchronisation unidirectionnelle : Les sessions créées/modifiées dans le CRM créent/mettent à jour automatiquement les événements Google Calendar
- Synchronisation bidirectionnelle : Les modifications dans Google Calendar sont synchronisées vers les sessions CRM
- Gestion de disponibilité : Système de créneaux disponibles/bloqués pour gérer les horaires
- Résolution de conflits : Détection et gestion des modifications simultanées
Architecture des données
Flux de synchronisation CRM → Google Calendar
graph TD
A[Utilisateur crée/modifie Session] --> B[SessionsService.create/update]
B --> C{Session a dates?}
C -->|Oui| D[GoogleCalendarService.createSessionEvent/updateSessionEvent]
C -->|Non| E[Fin]
D --> F[buildSessionEvent avec extendedProperties]
F --> G[extendedProperties.private.sessionId = sessionId]
F --> H[extendedProperties.private.syncSource = 'crm']
F --> I[extendedProperties.private.sessionUpdatedAt = session.updated_at]
G --> J[Google Calendar API: events.insert/update]
H --> J
I --> J
J --> K[Événement créé/mis à jour dans Google Calendar]
Flux de synchronisation Google Calendar → CRM
graph TD
A[GoogleCalendarSyncScheduler - Toutes les 5 min] --> B[getUsersWithCalendar]
B --> C[Pour chaque utilisateur]
C --> D[getEventsWithSessionIds -7j à +30j]
D --> E[Pour chaque événement avec sessionId]
E --> F[findById - Récupérer session]
F --> G{Session existe?}
G -->|Non| H[Skip]
G -->|Oui| I[Extraire données événement]
I --> J[Vérifier conflit: syncSource === 'crm'?]
J -->|Oui| K{session.updated_at > event.sessionUpdatedAt?}
K -->|Oui| L[Skip - Session plus récente]
K -->|Non| M[Comparer données]
J -->|Non| M[Comparer données]
M --> N{Différences détectées?}
N -->|Oui| O[updateFromCalendar - Sans callback Google Calendar]
N -->|Non| P[Skip]
O --> Q[Session mise à jour]
Gestion de disponibilité
graph TD
A[Utilisateur crée créneau disponibilité] --> B[AvailabilitySlotDialog]
B --> C[useCreateAvailabilitySlot]
C --> D[API POST /availability/slots]
D --> E[AvailabilityService.create]
E --> F[Insert dans availability_slots]
F --> G[useAvailabilitySlots - React Query]
G --> H[Affichage dans CalendarPage]
H --> I[DayView/WeekView/MonthView]
I --> J[AvailabilitySlotDisplay - Vert AVAILABLE / Rouge BLOCKED]
K[Utilisateur crée Session] --> L[SessionFormPage]
L --> M[checkAvailability - Avant création]
M --> N{Slot disponible?}
N -->|Non| O[Erreur: Créneau bloqué]
N -->|Oui| P[Création session]
Résolution de conflits
graph TD
A[Événement Google Calendar modifié] --> B[Scheduler détecte changement]
B --> C[Vérifier extendedProperties.private.syncSource]
C -->|'crm'| D[Événement créé depuis CRM]
C -->|'calendar' ou absent| E[Événement modifié dans Google Calendar]
D --> F[Comparer sessionUpdatedAt avec session.updated_at]
F --> G{session.updated_at > sessionUpdatedAt?}
G -->|Oui| H[CONFLIT: Session plus récente]
G -->|Non| I[Synchroniser depuis Calendar]
H --> J[Skip sync - Éviter écrasement]
E --> I
I --> K[updateFromCalendar - Met à jour session]
K --> L[Pas de callback Google Calendar - Évite boucle]
Synchronisation des événements récurrents
Le système gère également la synchronisation bidirectionnelle pour les événements récurrents :
Occurrences modifiées (exceptions)
graph TD
A[Scheduler détecte exception] --> B{recurringEventId && originalStartTime?}
B -->|Oui| C[getRecurringParentEvent]
C --> D[Extraire parentSessionId]
D --> E[findOccurrenceByParentAndDate]
E --> F{Occurrence existe?}
F -->|Non| G[Créer occurrence depuis exception]
F -->|Oui| H[Vérifier conflit]
H --> I{occurrence.updated_at > event.sessionUpdatedAt?}
I -->|Oui| J[Skip - Occurrence plus récente]
I -->|Non| K[Synchroniser depuis exception]
K --> L[updateFromCalendar - Met à jour occurrence]
G --> M[Occurrence créée/mise à jour]
Occurrences supprimées (EXDATE)
graph TD
A[Scheduler détecte événement récurrent parent] --> B[getRecurringEventInstances]
B --> C[Obtenir toutes les instances Google Calendar]
C --> D[Obtenir toutes les occurrences CRM]
D --> E[Comparer les deux listes]
E --> F{Occurrence CRM n'existe pas dans Calendar?}
F -->|Oui| G[Supprimer occurrence CRM]
F -->|Non| H[Conserver occurrence]
G --> I[Occurrence supprimée - Synchronisée]
Fonctionnalités :
- ✅ Détection automatique des exceptions (occurrences modifiées individuellement)
- ✅ Création automatique d'occurrences manquantes si modifiées dans Google Calendar
- ✅ Synchronisation des modifications (titre, dates, lieu) vers les occurrences CRM
- ✅ Détection et suppression des occurrences supprimées dans Google Calendar (EXDATE)
- ✅ Résolution de conflits pour éviter d'écraser les modifications récentes du CRM
Structure des données
ExtendedProperties dans Google Calendar
Les événements Google Calendar liés à des sessions contiennent dans extendedProperties.private :
{
sessionId: string; // ID de la session CRM
syncSource: "crm" | "calendar"; // Source de la dernière modification
sessionUpdatedAt: string; // ISO timestamp de la dernière mise à jour session
}
Table availability_slots
CREATE TABLE availability_slots (
id UUID PRIMARY KEY,
owner_id UUID NOT NULL REFERENCES users(id),
start_date TIMESTAMPTZ NOT NULL,
end_date TIMESTAMPTZ NOT NULL,
type TEXT NOT NULL CHECK (type IN ('AVAILABLE', 'BLOCKED')),
title TEXT,
description TEXT,
is_recurring BOOLEAN DEFAULT false,
recurrence_pattern TEXT, -- JSON pattern
recurrence_end_date TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
Composants principaux
Backend
-
GoogleCalendarService (
backend/src/google-calendar/google-calendar.service.ts)- Gestion de l'authentification OAuth2
- Création/mise à jour/suppression d'événements
- Mapping sessions ↔ événements via
extendedProperties - Méthodes utilitaires pour récupérer les événements
-
GoogleCalendarSyncScheduler (
backend/src/google-calendar/google-calendar-sync.scheduler.ts)- Scheduler exécuté toutes les 5 minutes
- Synchronisation Google Calendar → CRM
- Détection et résolution de conflits
-
SessionsService (
backend/src/sessions/sessions.service.ts)updateFromCalendar(): Met à jour une session depuis Google Calendar sans déclencher de callbackcreate()/update(): Déclenchent la création/mise à jour d'événements Google Calendar
-
AvailabilityService (
backend/src/availability/availability.service.ts)- CRUD des créneaux de disponibilité
- Vérification de disponibilité (
isSlotAvailable())
Frontend
-
CalendarPage (
frontend/src/pages/calendar/CalendarPage/CalendarPage.tsx)- Page principale du calendrier
- Intègre sessions, événements Google Calendar, et créneaux de disponibilité
- Gestion du drag & drop avec optimistic updates
-
AvailabilitySlotDialog (
frontend/src/pages/calendar/CalendarPage/components/AvailabilitySlotDialog.tsx)- Dialogue pour créer/modifier/supprimer des créneaux
- Support des créneaux récurrents
-
useCalendarDragAndDrop (
frontend/src/pages/calendar/CalendarPage/hooks/useCalendarDragAndDrop.ts)- Gestion du drag & drop avec optimistic updates
- Synchronisation immédiate de l'UI avant réponse API
-
useAvailability (
frontend/src/client/availability/useAvailability.ts)- Hooks React Query pour gérer les créneaux
- Optimistic updates pour une expérience fluide
Flux complet de synchronisation
sequenceDiagram
participant U as Utilisateur
participant C as CalendarPage
participant S as SessionsService
participant G as GoogleCalendarService
participant GC as Google Calendar API
participant SCH as SyncScheduler
Note over U,GC: Création/Mise à jour Session → Google Calendar
U->>C: Crée/modifie session
C->>S: create/update
S->>G: createSessionEvent/updateSessionEvent
G->>GC: events.insert/update (avec extendedProperties)
GC-->>G: Événement créé/mis à jour
G-->>S: Success
S-->>C: Session créée/mise à jour
Note over SCH,GC: Synchronisation Google Calendar → CRM (Toutes les 5 min)
SCH->>G: getUsersWithCalendar
G-->>SCH: Liste utilisateurs
SCH->>G: getEventsWithSessionIds (pour chaque user)
G->>GC: events.list (avec privateExtendedProperty)
GC-->>G: Liste événements
G-->>SCH: Événements avec sessionId
loop Pour chaque événement
SCH->>S: findById (session)
S-->>SCH: Session
SCH->>SCH: Vérifier conflit
alt Pas de conflit
SCH->>SCH: Comparer données
alt Différences détectées
SCH->>S: updateFromCalendar (sans callback)
S->>S: Mettre à jour session
S-->>SCH: Success
end
else Conflit détecté
SCH->>SCH: Skip sync (session plus récente)
end
end
Optimistic Updates
Le système utilise des optimistic updates pour une expérience utilisateur fluide :
graph TD
A[Action utilisateur] --> B[Optimistic update UI]
B --> C[Appel API]
C --> D{Succès?}
D -->|Oui| E[Update cache avec réponse serveur]
D -->|Non| F[Rollback vers état précédent]
E --> G[UI synchronisée]
F --> H[UI restaurée + Erreur affichée]
Implémentation
-
Sessions :
useCalendarDragAndDrop.tsonMutate: Met à jour le cache immédiatementonError: Restaure l'état précédentonSuccess: Met à jour avec la réponse serveur
-
Google Calendar Events :
useCalendarDragAndDrop.ts- Même pattern avec gestion des champs
dateTimeetdate
- Même pattern avec gestion des champs
-
Theme Toggle :
ThemeContext.tsxoptimisticThemestate pour feedback immédiat- Synchronisation avec settings après réponse serveur
-
Availability Slots :
useAvailability.ts- Optimistic updates pour create/update/delete
Gestion des erreurs
Erreurs de synchronisation
- Erreurs OAuth : Token expiré → Redirection vers reconnexion
- Erreurs API : Loggées mais n'arrêtent pas le scheduler
- Conflits : Détectés et loggés, sync ignorée si session plus récente
Erreurs de disponibilité
- Créneau bloqué : Validation avant création de session
- Erreurs API : Affichées via toast notifications
Monitoring et logs
Logs backend
- Scope
GOOGLE_CALENDAR: Opérations Google Calendar - Scope
GOOGLE_CALENDAR_SYNC: Synchronisation bidirectionnelle - Scope
DB: Opérations base de données
Métriques
- Nombre de sessions synchronisées par cycle
- Nombre de conflits détectés
- Erreurs de synchronisation
Configuration
Variables d'environnement
GOOGLE_CLIENT_ID: Client ID OAuth2GOOGLE_CLIENT_SECRET: Client Secret OAuth2GOOGLE_REDIRECT_URI: URI de redirection OAuth
Scheduler
- Fréquence : Toutes les 5 minutes (
CronExpression.EVERY_5_MINUTES) - Période de sync : -7 jours à +30 jours
- Seuil de différence : 1 minute pour les dates
Améliorations futures
- Webhooks Google Calendar : Remplacer le polling par des webhooks en temps réel
- Synchronisation Outlook : Ajouter le support d'autres calendriers
- Résolution de conflits avancée : Interface pour résoudre manuellement les conflits
- Notifications : Notifier l'utilisateur des synchronisations et conflits
- Historique de sync : Traçabilité des synchronisations