fix: restrict future planning visibility to explicit assignments and update team index UI styles.

This commit is contained in:
kdeterme 2026-09-02 11:38:51 +02:00
parent 11c81c8e00
commit 71b445797d
3 changed files with 46 additions and 22 deletions

View file

@ -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

View file

@ -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 %}";

View file

@ -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."""