From 71b445797d4326caf3757cdbcde06216a5058b77 Mon Sep 17 00:00:00 2001 From: kdeterme Date: Wed, 2 Sep 2026 11:38:51 +0200 Subject: [PATCH] fix: restrict future planning visibility to explicit assignments and update team index UI styles. --- loko/contracts/services_teams.py | 28 +++++++++++------ .../templates/contracts/teams_index.html | 31 +++++++++++++------ loko/contracts/test_planning.py | 9 ++++-- 3 files changed, 46 insertions(+), 22 deletions(-) diff --git a/loko/contracts/services_teams.py b/loko/contracts/services_teams.py index d65227b..b019ae5 100644 --- a/loko/contracts/services_teams.py +++ b/loko/contracts/services_teams.py @@ -32,12 +32,14 @@ def get_member_team_for_date(member: CompanyMember, target_date: date) -> Option ) if not histories: return None - # Si un historique est ouvert (leave_date IS NULL) ou se termine strictement après target_date, - # on le préfère à un historique se terminant exactement à target_date (transfert en cours de journée) - continuing = [h for h in histories if h.leave_date is None or h.leave_date > target_date] - if continuing: - return continuing[0].team - return histories[0].team + # Parmi les historiques ayant la date de début la plus récente (join_date maximal) + max_join = histories[0].join_date + candidates = [h for h in histories if h.join_date == max_join] + if len(candidates) > 1: + continuing = [h for h in candidates if h.leave_date is None or h.leave_date > target_date] + if continuing: + return continuing[0].team + return candidates[0].team @transaction.atomic @@ -75,7 +77,7 @@ def set_member_team_schedule( break # Si le membre est déjà dans la même équipe à cette date exacte, rien à faire - if current_at_target and current_at_target.team_id == team_id: + if current_at_target and current_at_target.team_id == team_id and current_at_target.join_date == target_date: return current_at_target if not current_at_target and team_id is None: return None @@ -148,8 +150,11 @@ def get_teams_composition_for_period( ) -> Dict[tuple, List[Dict[str, Any]]]: """ Calcule la composition journalière de chaque équipe sur une période donnée [start_date, end_date]. + - Pour aujourd'hui et les jours passés : utilise les affectations actives courantes et historiques. + - Pour les jours futurs : n'affiche que les journées qui ont été explicitement planifiées ou dupliquées (join_date == d). Retourne un dict: (team_id, date) -> [{'id': member_id, 'name': member_name, 'user_id': user_id}] """ + today = timezone.now().date() histories = ( CompanyMemberHistory.objects .filter( @@ -183,8 +188,13 @@ def get_teams_composition_for_period( # Trouver les historiques actifs à la date d active = [] for h in m_data['histories']: - if h['join_date'] <= d and (h['leave_date'] is None or h['leave_date'] >= d): - active.append(h) + if d <= today: + if h['join_date'] <= d and (h['leave_date'] is None or h['leave_date'] >= d): + active.append(h) + else: + # Pour les jours futurs : affiché uniquement si explicitement planifié pour ce jour (join_date == d) + if h['join_date'] == d: + active.append(h) if not active: continue diff --git a/loko/contracts/templates/contracts/teams_index.html b/loko/contracts/templates/contracts/teams_index.html index ba3ee7a..ee67264 100644 --- a/loko/contracts/templates/contracts/teams_index.html +++ b/loko/contracts/templates/contracts/teams_index.html @@ -203,8 +203,8 @@ .planning-table th, .planning-table td { - border-right: 1px solid #e9ecef; - border-bottom: 1px solid #e9ecef; + border-right: 1px solid #cbd5e1; + border-bottom: 1px solid #cbd5e1; vertical-align: top; } @@ -218,6 +218,7 @@ font-weight: 600; min-width: 150px; box-shadow: 0 2px 4px rgba(0,0,0,0.06); + border-bottom: 2px solid #64748b; } .planning-table thead th.sticky-team-col { @@ -227,8 +228,10 @@ min-width: 230px; max-width: 270px; text-align: left; - background: #e9ecef; + background: #e2e8f0; box-shadow: 2px 2px 5px rgba(0,0,0,0.08); + border-right: 2px solid #94a3b8; + border-bottom: 2px solid #64748b; } .planning-table tbody td.sticky-team-col { @@ -238,19 +241,22 @@ background: #ffffff; padding: 10px 12px; box-shadow: 2px 0 4px rgba(0,0,0,0.05); + border-right: 2px solid #94a3b8; } - /* Différenciation des lignes d'équipes */ - .planning-team-row { - border-bottom: 2px solid #ced4da; + /* Différenciation très marquée des lignes d'équipes */ + .planning-table tbody tr.planning-team-row td { + border-bottom: 3px solid #64748b !important; + padding-top: 10px; + padding-bottom: 12px; } - .planning-team-row:nth-child(even) td.sticky-team-col { - background-color: #f8fafd; + .planning-table tbody tr.planning-team-row:nth-child(even) td.sticky-team-col { + background-color: #f1f5f9; } - .planning-team-row:nth-child(even) td:not(.is-today-cell):not(.is-past-cell):not(.is-weekend-cell) { - background-color: #fafcff; + .planning-table tbody tr.planning-team-row:nth-child(even) td.planning-drop-cell:not(.is-today-cell):not(.is-past-cell):not(.is-weekend-cell) { + background-color: #f8fafc; } /* En-têtes et cellules de jours */ @@ -1542,6 +1548,11 @@ function createPlanningMemberCard(member, teamId, dateStr, canEdit, isPast) { card.dataset.date = dateStr; card.dataset.memberName = member.name; + const teamColor = getTeamColor(teamId); + if (!member.is_absent && !isPast) { + card.style.borderLeftColor = teamColor; + } + let absenceBadge = ''; if (member.is_absent) { const reasonDisplay = (member.absence && member.absence.reason_display) ? member.absence.reason_display : "{% trans 'Absent'|escapejs %}"; diff --git a/loko/contracts/test_planning.py b/loko/contracts/test_planning.py index 17e0714..fe73397 100644 --- a/loko/contracts/test_planning.py +++ b/loko/contracts/test_planning.py @@ -203,12 +203,12 @@ class TeamPlanningAjaxViewsTests(TestCase): self.assertEqual(get_member_team_for_date(self.member_alice, today), self.team_a) def test_move_member_takes_effect_on_day_j(self): - """Vérifie que déplacer un membre le jour J s'applique immédiatement le jour J dans les compositions.""" + """Vérifie que déplacer un membre le jour J s'applique immédiatement le jour J dans les compositions, sans pré-remplir les jours futurs non planifiés.""" today = timezone.now().date() day_j = today + timedelta(days=2) day_next = day_j + timedelta(days=1) - # 1. Alice est dans Team A depuis day_j + # 1. Alice est dans Team A pour day_j set_member_team_schedule(self.member_alice, self.team_a.id, day_j) comp = get_teams_composition_for_period({self.team_a.id, self.team_b.id}, day_j, day_next) team_a_day_j_members = [m['id'] for m in comp.get((self.team_a.id, day_j), [])] @@ -225,7 +225,10 @@ class TeamPlanningAjaxViewsTests(TestCase): self.assertNotIn(self.member_alice.id, team_a_day_j_after) self.assertIn(self.member_alice.id, team_b_day_j_after) - self.assertIn(self.member_alice.id, team_b_day_next_after) + # Le jour suivant n'ayant pas été planifié, il ne doit pas être pré-rempli dans la matrice de planification + self.assertNotIn(self.member_alice.id, team_b_day_next_after) + # Mais get_member_team_for_date assure la persistance théorique pour les interventions + self.assertEqual(get_member_team_for_date(self.member_alice, day_next), self.team_b) def test_planning_past_date_is_forbidden(self): """Vérifie que les endpoints AJAX rejettent les modifications sur des dates passées."""