using System.Text.Json; using Omika.App.Local.Models; using Omika.App.Local.Services; using Omika.Core.Contracts.Common; using Omika.Core.Contracts.Planning; namespace Omika.Tests.Mobile.Local; /// /// Rendez-vous de groupe : l'unité affichée est l'AFFECTATION /// (RDV_Collaborateur), jamais le noyau du rendez-vous. Une carte par /// collaborateur et par jour, avec la date, les heures et le collaborateur de la /// ligne réelle ; la fiche conserve l'affectation ouverte et expose, à titre /// informatif, les jours et les collaborateurs du rendez-vous. /// public class AgendaServiceAffectationTests : LocalTestBase { private static readonly DateTime Jour1 = new(2026, 8, 12); private static readonly DateTime Jour2 = new(2026, 8, 13); private static readonly Guid CollaborateurA = Guid.Parse("11111111-1111-1111-1111-111111111111"); private static readonly Guid CollaborateurB = Guid.Parse("22222222-2222-2222-2222-222222222222"); private readonly AgendaService _service; public AgendaServiceAffectationTests() { _service = new AgendaService( RendezVousRepository, ClientRepository, SiteRepository, InterventionRepository, CollaborateurRepository, ApiClient, SessionService); } private static AffectationCollaborateurDto Affectation( Guid affectationId, Guid collaborateurId, string nom, DateTime debut, DateTime fin) => new() { Id = affectationId.ToString("D"), CollaborateurId = collaborateurId.ToString("D"), NomAffiche = nom, PeriodeDebut = debut, PeriodeFin = fin }; /// /// Rendez-vous groupé sur DEUX jours avec DEUX collaborateurs : quatre /// affectations, chacune avec son créneau. /// private async Task<(Guid RendezVousId, Guid AffectationAJour2, Guid AffectationBJour2)> PreparerGroupeAsync() { Guid rendezVousId = Guid.NewGuid(); Guid aJour1 = Guid.NewGuid(); Guid aJour2 = Guid.NewGuid(); Guid bJour1 = Guid.NewGuid(); Guid bJour2 = Guid.NewGuid(); var dto = new RendezVousDetailsDto { Id = rendezVousId.ToString(), Titre = "Chantier groupé", Debut = Jour1.AddHours(8), Fin = Jour2.AddHours(17), Statut = new ReferenceValueDto { Label = "Planifié" }, EstGroupe = true, Collaborateurs = [ Affectation(aJour1, CollaborateurA, "Alice Martin", Jour1.AddHours(8), Jour1.AddHours(12)), Affectation(aJour2, CollaborateurA, "Alice Martin", Jour2.AddHours(8), Jour2.AddHours(12)), Affectation(bJour1, CollaborateurB, "Bruno Petit", Jour1.AddHours(13), Jour1.AddHours(17)), Affectation(bJour2, CollaborateurB, "Bruno Petit", Jour2.AddHours(13), Jour2.AddHours(17)) ] }; await RendezVousRepository.UpsertAsync(new LocalRendezVous { Id = rendezVousId, PayloadJson = JsonSerializer.Serialize(dto), UpdatedAtUtc = DateTimeOffset.UtcNow }); return (rendezVousId, aJour2, bJour2); } [Fact] public async Task Agenda_UneCarteParAffectation_AvecLeBonCollaborateurEtLeBonCreneau() { // Arrange : aucune portée connue → toutes les affectations sont présentées. (Guid rendezVousId, _, _) = await PreparerGroupeAsync(); // Act IReadOnlyList result = await _service.GetRendezVousForRangeAsync(Jour1, Jour2.AddDays(1)); // Assert : une carte par affectation (2 collaborateurs × 2 jours). Assert.Equal(4, result.Count); // La carte d'Alice au 2e jour porte BIEN le créneau du 2e jour (et non // celui du premier rendez-vous du groupe). AgendaItem aliceJour2 = Assert.Single(result, i => i.CollaborateurNom == "Alice Martin" && i.Debut.Date == Jour2); Assert.Equal(Jour2.AddHours(8), aliceJour2.Debut); Assert.Equal(Jour2.AddHours(12), aliceJour2.Fin); AgendaItem brunoJour2 = Assert.Single(result, i => i.CollaborateurNom == "Bruno Petit" && i.Debut.Date == Jour2); Assert.Equal(Jour2.AddHours(13), brunoJour2.Debut); Assert.Equal(Jour2.AddHours(17), brunoJour2.Fin); // Toutes les cartes se rattachent au même rendez-vous (noyau). Assert.All(result, item => Assert.Equal(rendezVousId, item.RendezVousGuid)); } [Fact] public async Task Agenda_PorteeCollaborateur_NePresenteQueSesPropresCreneaux() { // Arrange : agenda personnel d'Alice (bootstrap local). await PreparerGroupeAsync(); SessionService.Bootstrap = new LocalBootstrapState { Collaborateur = new Omika.Core.Contracts.Organisation.CollaborateurDetailsDto { Id = CollaborateurA.ToString("D"), Nom = "Martin", Prenom = "Alice" } }; // Act IReadOnlyList result = await _service.GetRendezVousForRangeAsync(Jour1, Jour2.AddDays(1)); // Assert : uniquement les deux créneaux d'Alice (un par jour). Assert.Equal(2, result.Count); Assert.All(result, item => Assert.Equal("Alice Martin", item.CollaborateurNom)); Assert.Equal([Jour1.AddHours(8), Jour2.AddHours(8)], result.Select(i => i.Debut).OrderBy(d => d).ToList()); } [Fact] public async Task Fiche_AffectationPrecise_GardeLeCreneauEtLeCollaborateurDeLaCarte() { // Arrange : la fiche est ouverte depuis la carte « Bruno – 2e jour ». (Guid rendezVousId, _, Guid affectationBJour2) = await PreparerGroupeAsync(); // Act AgendaItem? item = await _service.GetRendezVousByIdAsync( rendezVousId, default, affectationBJour2.ToString("D")); // Assert : créneau et collaborateur de l'affectation ouverte. Assert.NotNull(item); Assert.Equal(Jour2.AddHours(13), item!.Debut); Assert.Equal(Jour2.AddHours(17), item.Fin); Assert.Equal("Bruno Petit", item.CollaborateurNom); Assert.Equal(affectationBJour2.ToString("D"), item.AffectationId); } [Fact] public async Task Fiche_Groupe_ExposeLesJoursEtTousLesCollaborateurs() { // Arrange (Guid rendezVousId, Guid affectationAJour2, _) = await PreparerGroupeAsync(); // Act AgendaItem? item = await _service.GetRendezVousByIdAsync( rendezVousId, default, affectationAJour2.ToString("D")); // Assert : les deux jours du rendez-vous et les deux collaborateurs sont // exposés pour le bloc informatif de la fiche, avec pour chaque journée le // créneau réellement occupé (du premier démarrage à la dernière fin). Assert.NotNull(item); Assert.Equal([Jour1, Jour2], item!.JoursDuGroupe); Assert.Equal(2, item.JoursAvecHoraires.Count); Assert.Equal(Jour1.AddHours(8), item.JoursAvecHoraires[0].Debut); Assert.Equal(Jour1.AddHours(17), item.JoursAvecHoraires[0].Fin); Assert.Equal(Jour2.AddHours(8), item.JoursAvecHoraires[1].Debut); Assert.Equal(Jour2.AddHours(17), item.JoursAvecHoraires[1].Fin); Assert.Equal(2, item.Collaborateurs.Count); Assert.Contains("Alice Martin", item.Collaborateurs); Assert.Contains("Bruno Petit", item.Collaborateurs); Assert.True(item.AfficheBlocPeriode); // Le collaborateur de l'affectation OUVERTE est identifié : c'est lui qui // sera exclu du bloc (sa propre fiche) et dont les journées sont présentées. Assert.Equal(CollaborateurA, item.CollaborateurOccurrenceGuid); // Journées PAR collaborateur : Alice n'a que son créneau du matin, Bruno // celui de l'après-midi — aucun des deux n'hérite des heures de l'autre. Assert.Equal(2, item.JoursParCollaborateur.Count); JoursParCollaborateur joursAlice = Assert.Single( item.JoursParCollaborateur, j => j.CollaborateurGuid == CollaborateurA); Assert.Equal([Jour1, Jour2], joursAlice.Jours.Select(j => j.Jour).ToList()); Assert.All(joursAlice.Jours, j => { Assert.Equal(j.Jour.AddHours(8), j.Debut); Assert.Equal(j.Jour.AddHours(12), j.Fin); }); JoursParCollaborateur joursBruno = Assert.Single( item.JoursParCollaborateur, j => j.CollaborateurGuid == CollaborateurB); Assert.Equal([Jour1, Jour2], joursBruno.Jours.Select(j => j.Jour).ToList()); Assert.All(joursBruno.Jours, j => { Assert.Equal(j.Jour.AddHours(13), j.Debut); Assert.Equal(j.Jour.AddHours(17), j.Fin); }); } [Fact] public async Task Fiche_RendezVousSimple_NAffichePasLeBlocGroupe() { // Arrange : rendez-vous d'une journée, un seul collaborateur. Guid rendezVousId = Guid.NewGuid(); Guid affectationId = Guid.NewGuid(); var dto = new RendezVousDetailsDto { Id = rendezVousId.ToString(), Titre = "Visite", Debut = Jour1.AddHours(9), Fin = Jour1.AddHours(10), Collaborateurs = [ Affectation(affectationId, CollaborateurA, "Alice Martin", Jour1.AddHours(9), Jour1.AddHours(10)) ] }; await RendezVousRepository.UpsertAsync(new LocalRendezVous { Id = rendezVousId, PayloadJson = JsonSerializer.Serialize(dto), UpdatedAtUtc = DateTimeOffset.UtcNow }); // Act AgendaItem? item = await _service.GetRendezVousByIdAsync(rendezVousId, default, affectationId.ToString("D")); // Assert : bloc informatif masqué (un jour, un collaborateur). Assert.NotNull(item); Assert.False(item!.AfficheBlocPeriode); Assert.Single(item.JoursDuGroupe); } }