fix: restrict future planning visibility to explicit assignments and update team index UI styles.
This commit is contained in:
parent
11c81c8e00
commit
71b445797d
3 changed files with 46 additions and 22 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 %}";
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
Loading…
Reference in a new issue