sync_agenda_rdv_ressources_v2.md 15809 octets

Sync Agenda v2 — Plan d’implémentation (serveur puis mobile)

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans. Steps use checkbox (- [ ]) syntax. TDD. Ne pas committer sauf demande explicite de l’utilisateur.

Goal : Exposer RDV + réservations/indispos en API/sync Projectiaon, puis pont Card2vcf Room ↔ Agenda Android multi-calendriers (Mes RDV + ressources sélectionnées), sans écran agenda in-app.

Architecture : Étendre sync v1 (tombstones, status/pull, SyncOp, LWW). Serveur d’abord. Mobile : Paramètres crée/lie des calendriers système ; event ressource = réservation Active ; 409 → refresh + dialog annulation.

Tech Stack : Rust/Axum (Projectiaon) ; Kotlin/Room/CalendarContract/OkHttp (Card2vcf).

Spec : docs/superpowers/specs/2026-07-22-sync-agenda-rdv-ressources-v2-design.md

Hors scope : UI demande/réclamation mobile ; SyncAdapter ; calendriers cloud GMS ; écran agenda Card2vcf.


Fichiers (carte)

Serveur (Projectiaon)

FichierRôle
src/modeles.rsmis_a_jour_le sur RendezVous, Reservation, Indisponibilite
src/handlers/rdv.rs + chemins UIPoser mis_a_jour_le à chaque mutation HTML
src/services/ressources.rsIdem résa/indispo ; mapper conflit → signal API 409
src/handlers/api.rsRoutes /api/rdv, /api/ressources*, OpenAPI
src/services/sync.rsDelta rdv/reservations/indisponibilites + tombstones + query ressources
src/services/sync.rs TypeTombstoneVariantes Rdv, Reservation, Indisponibilite
tests/api_agenda_http.rsTests HTTP Bearer CRUD + sync + 409
docs/guide-administrateur.mdDoc endpoints agenda mobile

Mobile (EBII_mobileVCF)

FichierRôle
AndroidManifest.xmlREAD_CALENDAR, WRITE_CALENDAR
sync/CalendarBindingsStore.ktPrefs calendriers liés
sync/CalendarBridge.ktCRUD events / calendriers locaux
data/RdvEntity.kt + DaoRoom RDV
data/ReservationEntity.kt + DaoRoom résas
data/IndisponibiliteEntity.kt + DaoRoom indispos lecture
CrmDatabase.ktversion ↑ (3), entities
sync/SyncModels.kt + AilianceApi*DTOs + méthodes API
sync/SyncEngine.ktAgenda↔Room↔API, conflit 409
ui/settings/*Section calendriers
ui/sync/SyncBanner.kt + ViewModelsBandeau « local en avance » + dialog conflit
Tests unitairesBridge mapping, SyncEngine conflit, bindings

Partie A — Serveur

Task S1 — mis_a_jour_le sur RDV / Reservation / Indisponibilite

Files :

  • Modify: Projectiaon/src/modeles.rs (RendezVous, Reservation, Indisponibilite)

  • Modify: handlers/services qui écrivent ces entités (handlers/rdv.rs, services/rdv.rs, services/ressources.rs, handlers/ressources.rs, handlers/planning.rs si besoin)

  • Test: étendre tests unitaires existants ressources/rdv ou nouveau test modèle

  • Step 1 : Ajouter aux 3 structs :

#[serde(default)]
pub mis_a_jour_le: Option<DateTime<Utc>>,
  • Step 2 : Helper fn toucher_horodatage(m: &mut Option<DateTime<Utc>>) ou poser Some(Utc::now()) à chaque ecrire_* create/update (et avant delete pour tombstone timestamp = now).
  • Step 3 : Migration serde : fichiers JSON existants sans champ → None OK (#[serde(default)]).
  • Verify : cargo test -p ailiance-brain --lib (filtres rdv/ressources si présents) vert.

Task S2 — Tombstones + helpers list-since

Files :

  • Modify: src/services/sync.rs (TypeTombstone, CompteursSync, PullSync, status, pull)
  • Modify: tous les supprimer_rendezvous / supprimer_reservation / supprimer_indisponibilite chemins API+UI critiques → enregistrer_tombstone avant hard delete

Schéma type JSON inchangé :

{ "type": "rdv|reservation|indisponibilite", "id": "", "projet_id": null, "supprime_le": "" }

Pour résa/indispo, stocker aussi la cible dans un champ optionnel si utile au client :

{ "type": "reservation", "id": "", "cible_type": "salle", "cible_id": "abc", "supprime_le": "" }

(cible_type/cible_id : #[serde(default)] pour ne pas casser tombstones v1.)

  • Étendre Tombstone + TypeTombstone.
  • Brancher deletes RDV/résa/indispo (au minimum chemins qui seront exposés API ; idéalement UI aussi).
  • Verify : test unitaire tombstones_depuis filtre les nouveaux types.

Task S3 — API CRUD /api/rdv

Files :

  • Modify: src/handlers/api.rs
  • Test: tests/api_agenda_http.rs (créer)

Contrat :

GET    /api/rdv                 → [RendezVous]  # option: ?mine=1 défaut pour mobile = filtre utilisateur==auteur clé
POST   /api/rdv                 → 201 RendezVous
GET    /api/rdv/:id
PUT    /api/rdv/:id
DELETE /api/rdv/:id             → 204 + tombstone

Body create/update (snake_case) :

{
  "titre": "",
  "description": "",
  "lieu": "",
  "debut": "ISO-8601",
  "fin": "ISO-8601",
  "contact_ids": [],
  "projet_id": null
}

Règles :

  • utilisateur / cree_par = user Bearer (ignorer spoof).

  • peut_modifier pour PUT/DELETE.

  • Valider via RendezVous::valider.

  • Poser mis_a_jour_le.

  • RED : test HTTP create → get → auteur = user clé ; mauvaise clé 401 ; autre user ne peut pas DELETE.

  • GREEN : handlers + routes dans le routeur Bearer (pas /auth/cle).

  • OpenAPI : documenter les 5 routes.

  • Verify : cargo test -p ailiance-brain --test api_agenda_http -- --nocapture


Task S4 — API catalogue ressources + CRUD réservations + GET indispos

Files : api.rs, éventuellement petit module handlers/api_ressources.rs si api.rs trop gros
Test : api_agenda_http.rs

Catalogue

GET /api/ressources

Réponse :

{
  "salles": [ { "id", "nom", "actif",  } ],
  "materiels": [  ],
  "vehicules": [  ]
}

Uniquement ressources actif == true pour le mobile (ou toutes + flag actif — préférer toutes avec actif pour que Paramètres puisse griser).

Réservations

GET  /api/ressources/:genre/:id/reservations
POST /api/ressources/:genre/:id/reservations
PUT  /api/ressources/:genre/:id/reservations/:rid
DELETE /api/ressources/:genre/:id/reservations/:rid

genresalle|materiel|vehicule (pas utilisateur en v2 mobile).

Body create :

{
  "debut": "",
  "fin": "",
  "motif": "",
  "projet_id": null,
  "rdv_id": null
}
  • reserve_par = Bearer user ; statut = Active forcé.
  • Appeler ressources::creer_reservation / modifier_reservation.
  • Sur ErreurReservation::ConflitHTTP 409 + JSON { "message": "…", "reclamable": bool } (ne pas créer EnAttente depuis l’API mobile).

Indispos (lecture)

GET /api/ressources/:genre/:id/indisponibilites
  • Tests : create résa OK ; second chevauchant → 409 ; delete → 204.
  • OpenAPI.
  • Verify : mêmes tests api_agenda_http.

Task S5 — Étendre /api/sync/status et /api/sync/pull

Files : services/sync.rs, handlers/api.rs

Query additionnelle (optionnelle) :

GET /api/sync/pull?since=…&ressources=salle:abc,vehicule:xyz
GET /api/sync/status?since=…&ressources=…

Parser ressourcesVec<CibleRessource> ; vide = ne pas renvoyer résas/indispos (client n’a lié aucun calendrier ressource) ; RDV toujours filtrés utilisateur == Bearer.

Étendre CompteursSync / JSON changes :

"changes": {
  "contacts": 0, "entreprises": 0, "projets": 0, "taches": 0, "interactions": 0,
  "rdv": 1, "reservations": 2, "indisponibilites": 1, "tombstones": 0
}

PullSync :

{
  "server_time": "",
  "contacts": [], "entreprises": [], "projets": [], "taches": [], "interactions": [],
  "rdv": [ /* RendezVous */ ],
  "reservations": [ /* Reservation */ ],
  "indisponibilites": [ /* Indisponibilite */ ],
  "tombstones": [],
  "workflows": []
}

Filtre since : mis_a_jour_le.unwrap_or(cree_le) > since (RDV/résa/indispo).

Compat clients v1 : champs JSON nouveaux avec défaut [] / 0 — anciens parseurs Kotlin devront être mis à jour (Task M*). Serde côté serveur : toujours émettre les clés.

  • Tests : create RDV → status.rdv >= 1 pour owner ; autre user → 0 ; résa sur salle filtrée par query ressources.
  • Verify : cargo test -p ailiance-brain --test api_agenda_http --test api_sync_http

Task S6 — Doc admin serveur

Files : Projectiaon/docs/guide-administrateur.md, OpenAPI déjà S3–S5

  • Documenter /api/rdv, /api/ressources, sync rdv/reservations/indisponibilites, query ressources=, code 409.
  • Lien vers la spec Card2vcf v2.
  • Verify : relecture manuelle ; cargo test --all-targets vert sur Projectiaon.

Partie B — Mobile

Task M1 — Permissions + CalendarBindingsStore

Files :

  • Modify: android/app/src/main/AndroidManifest.xml
  • Create: …/sync/CalendarBindingsStore.kt
  • Test: …/sync/CalendarBindingsStoreTest.kt
@Serializable
data class CalendarBinding(
  val kind: String, // "rdv" | "salle" | "materiel" | "vehicule"
  val serverResourceId: String? = null,
  val displayName: String,
  val androidCalendarId: Long? = null,
)
  • Permissions READ_CALENDAR / WRITE_CALENDAR.
  • Store JSON dans prefs (EncryptedSharedPreferences ou prefs dédiées sync — réutiliser pattern credentials).
  • list(), upsert(binding), remove(kind, serverResourceId?).
  • Verify : unit test round-trip.

Task M2 — CalendarBridge (TDD sur mapping pur + smoke)

Files :

  • Create: …/sync/CalendarBridge.kt
  • Create: …/sync/CalendarEventSnapshot.kt (data class titre, debutMs, finMs, eventId, serverIdExtra?)
  • Test: mapping helpers sans ContentResolver si possible ; Robolectric limité pour insert calendar

API :

class CalendarBridge(private val resolver: ContentResolver) {
  fun ensureLocalCalendar(displayName: String): Long
  fun listEvents(calendarId: Long): List<CalendarEventSnapshot>
  fun upsertEvent(calendarId: Long, snap: CalendarEventSnapshot): Long
  fun deleteEvent(eventId: Long)
}
  • Calendriers locaux (ACCOUNT_TYPE local / Calendars.ACCOUNT_NAME = "card2vcf") pour hors-GMS.

  • Stocker serverId dans Events.DESCRIPTION préfixe stable card2vcf:serverId=…\n ou extended properties si fiable ; documenter le choix dans le code.

  • Titre indispo : préfixe [Indispo] .

  • Verify : tests mapping parse/format serverId ; assembleDebug OK.


Task M3 — Room v3 : RDV / Reservation / Indisponibilite

Files : entities + DAOs ; CrmDatabase version 3 + fallbackToDestructiveMigration (doc README)

Champs alignés spec §4.2 (dirtyLocal, conflictPending, calendarEventId, …).

  • DAOs : upsert, getByServerId, listDirty, deleteByServerId, listByCible.
  • Test smoke Robolectric AgendaSchemaDaoTest.
  • Verify : ./gradlew :app:testDebugUnitTest --tests '*AgendaSchema*' + tests data existants.

Task M4 — DTOs + AilianceApi méthodes agenda

Files : SyncModels.kt, AilianceApi.kt, AilianceApiClient.kt
Test : MockWebServer

  • DTOs RendezVousDto, ReservationDto, IndisponibiliteDto, RessourcesCatalogueDto.

  • Étendre SyncStatusResponse.changes + SyncPullResponse avec rdv, reservations, indisponibilites (défaut empty).

  • syncStatus(since, ressourcesQuery: String?), syncPull(since, ressourcesQuery: String?).

  • CRUD rdv + résa ; GET /api/ressources.

  • Parser 409 → ApiResult.Err(409, message).

  • Verify : tests client verts ; régessions AilianceApiClientTest.


Task M5 — SyncEngine : RDV LWW + résas + Agenda↔Room

Files : SyncEngine.kt (+ collaborator AgendaSyncCoordinator si fichier trop gros)

Étendre syncNow(bindings: List<CalendarBinding>, bridge: CalendarBridge) :

  1. agendaToRoom(bindings) → dirty → SyncOp rdv/reservation
  2. pushOps() — sur résa create/update si Err.code == 409 : set conflictPending, ne pas drop SyncOp tout de suite ; retourner SyncResult(conflicts = […])
  3. pull avec query ressources= dérivée des bindings
  4. apply LWW RDV ; résas Active ; indispos append/replace by serverId
  5. roomToAgenda(bindings)
  6. watermark

localAheadCount() : dirty Room + SyncOp pending rdv/reservation (+ events orphelins).

  • Tests Fake API + in-memory Room : LWW RDV ; 409 set conflictPending ; cancel path helper abandonLocalReservation(localId).
  • Verify : SyncEngineTest + nouveaux tests.

Task M6 — UI Paramètres calendriers

Files : SettingsScreen.kt, SettingsViewModel.kt, strings FR/EN

  • Demande runtime permissions calendrier.

  • Toggle Mes RDV → ensureLocalCalendar("Card2vcf — Mes RDV") + binding.

  • Fetch catalogue ressources si credentials → toggles par ressource active.

  • Confirmation avant remove + delete calendar optionnel.

  • Verify : assembleDebug ; test ViewModel permissions/bindings mock si possible.


Task M7 — Bandeaux + dialog conflit

Files : SyncBanner.kt, SyncChromeViewModel.kt, strings

  • Bandeau A : changements serveur (pendingRemoteChanges) — existant.

  • Bandeau B : localCalendarAhead > 0 → « Calendrier local en avance sur le serveur ».

  • Après syncNow, si conflicts non vide → dialog « Annuler ma réservation ? » (une par une ou première).

    • Oui → abandonLocalReservation + delete event Agenda.
    • Non → dismiss ; SyncOp peut rester / flag conflictPending (manager serveur).
  • Verify : test ViewModel conflit ; compile.


Task M8 — Doc README Card2vcf

Files : EBII_mobileVCF/README.md

  • Section Agenda / calendriers : permissions, Paramètres, pas d’écran agenda, conflit 409, hors-GMS calendriers locaux.

  • Lien spec + ce plan.

  • Nuancer destructive migration Room v3.

  • Verify : relecture ; ./gradlew :app:testDebugUnitTest assembleDebug.


Ordre / commits suggérés (si demandé)

  1. feat(api): mis_a_jour_le rdv reservation indispo
  2. feat(sync): tombstones agenda types
  3. feat(api): CRUD /api/rdv
  4. feat(api): ressources catalog + reservations 409
  5. feat(api): sync pull rdv reservations indispos
  6. docs(admin): agenda mobile API
  7. feat(android): calendar bindings + permissions
  8. feat(android): CalendarBridge local calendars
  9. feat(android): Room v3 agenda entities
  10. feat(android): API DTOs agenda
  11. feat(android): SyncEngine agenda bridge
  12. feat(ui): settings calendars + conflict dialog
  13. docs: README agenda sync

Critère de done v2

  • Curl Bearer : CRUD RDV + résa ; 409 chevauchement ; sync pull filtre ressources= + mes RDV.
  • App : lier Mes RDV + une salle → events Agenda ↔ serveur après Sync.
  • Conflit : deux events visibles + dialog annulation.
  • Bandeau local en avance.
  • cargo test --all-targets (Projectiaon) et ./gradlew :app:testDebugUnitTest assembleDebug verts.
  • Pas d’écran agenda dans la nav.

Coverage spec (self-check)

Spec §Tasks
RDV LWW + mes RDVS1, S3, S5, M3–M5
Multi-calendriers ParamètresM1, M6
Event ressource → résa ActiveS4, M5
Indispos lectureS4, S5, M3, M5
409 → refresh + annulerS4, M5, M7
Bandeau local en avanceM5, M7
Pas UI agenda / pas demandes mobileM6–M8 (hors scope respecté)
DocS6, M8