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:
|
if not histories:
|
||||||
return None
|
return None
|
||||||
# Si un historique est ouvert (leave_date IS NULL) ou se termine strictement après target_date,
|
# Parmi les historiques ayant la date de début la plus récente (join_date maximal)
|
||||||
# on le préfère à un historique se terminant exactement à target_date (transfert en cours de journée)
|
max_join = histories[0].join_date
|
||||||
continuing = [h for h in histories if h.leave_date is None or h.leave_date > target_date]
|
candidates = [h for h in histories if h.join_date == max_join]
|
||||||
if continuing:
|
if len(candidates) > 1:
|
||||||
return continuing[0].team
|
continuing = [h for h in candidates if h.leave_date is None or h.leave_date > target_date]
|
||||||
return histories[0].team
|
if continuing:
|
||||||
|
return continuing[0].team
|
||||||
|
return candidates[0].team
|
||||||
|
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
|
|
@ -75,7 +77,7 @@ def set_member_team_schedule(
|
||||||
break
|
break
|
||||||
|
|
||||||
# Si le membre est déjà dans la même équipe à cette date exacte, rien à faire
|
# 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
|
return current_at_target
|
||||||
if not current_at_target and team_id is None:
|
if not current_at_target and team_id is None:
|
||||||
return None
|
return None
|
||||||
|
|
@ -148,8 +150,11 @@ def get_teams_composition_for_period(
|
||||||
) -> Dict[tuple, List[Dict[str, Any]]]:
|
) -> Dict[tuple, List[Dict[str, Any]]]:
|
||||||
"""
|
"""
|
||||||
Calcule la composition journalière de chaque équipe sur une période donnée [start_date, end_date].
|
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}]
|
Retourne un dict: (team_id, date) -> [{'id': member_id, 'name': member_name, 'user_id': user_id}]
|
||||||
"""
|
"""
|
||||||
|
today = timezone.now().date()
|
||||||
histories = (
|
histories = (
|
||||||
CompanyMemberHistory.objects
|
CompanyMemberHistory.objects
|
||||||
.filter(
|
.filter(
|
||||||
|
|
@ -183,8 +188,13 @@ def get_teams_composition_for_period(
|
||||||
# Trouver les historiques actifs à la date d
|
# Trouver les historiques actifs à la date d
|
||||||
active = []
|
active = []
|
||||||
for h in m_data['histories']:
|
for h in m_data['histories']:
|
||||||
if h['join_date'] <= d and (h['leave_date'] is None or h['leave_date'] >= d):
|
if d <= today:
|
||||||
active.append(h)
|
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:
|
if not active:
|
||||||
continue
|
continue
|
||||||
|
|
|
||||||
|
|
@ -203,8 +203,8 @@
|
||||||
|
|
||||||
.planning-table th,
|
.planning-table th,
|
||||||
.planning-table td {
|
.planning-table td {
|
||||||
border-right: 1px solid #e9ecef;
|
border-right: 1px solid #cbd5e1;
|
||||||
border-bottom: 1px solid #e9ecef;
|
border-bottom: 1px solid #cbd5e1;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -218,6 +218,7 @@
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
min-width: 150px;
|
min-width: 150px;
|
||||||
box-shadow: 0 2px 4px rgba(0,0,0,0.06);
|
box-shadow: 0 2px 4px rgba(0,0,0,0.06);
|
||||||
|
border-bottom: 2px solid #64748b;
|
||||||
}
|
}
|
||||||
|
|
||||||
.planning-table thead th.sticky-team-col {
|
.planning-table thead th.sticky-team-col {
|
||||||
|
|
@ -227,8 +228,10 @@
|
||||||
min-width: 230px;
|
min-width: 230px;
|
||||||
max-width: 270px;
|
max-width: 270px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
background: #e9ecef;
|
background: #e2e8f0;
|
||||||
box-shadow: 2px 2px 5px rgba(0,0,0,0.08);
|
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 {
|
.planning-table tbody td.sticky-team-col {
|
||||||
|
|
@ -238,19 +241,22 @@
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
box-shadow: 2px 0 4px rgba(0,0,0,0.05);
|
box-shadow: 2px 0 4px rgba(0,0,0,0.05);
|
||||||
|
border-right: 2px solid #94a3b8;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Différenciation des lignes d'équipes */
|
/* Différenciation très marquée des lignes d'équipes */
|
||||||
.planning-team-row {
|
.planning-table tbody tr.planning-team-row td {
|
||||||
border-bottom: 2px solid #ced4da;
|
border-bottom: 3px solid #64748b !important;
|
||||||
|
padding-top: 10px;
|
||||||
|
padding-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.planning-team-row:nth-child(even) td.sticky-team-col {
|
.planning-table tbody tr.planning-team-row:nth-child(even) td.sticky-team-col {
|
||||||
background-color: #f8fafd;
|
background-color: #f1f5f9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.planning-team-row:nth-child(even) td:not(.is-today-cell):not(.is-past-cell):not(.is-weekend-cell) {
|
.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: #fafcff;
|
background-color: #f8fafc;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* En-têtes et cellules de jours */
|
/* En-têtes et cellules de jours */
|
||||||
|
|
@ -1542,6 +1548,11 @@ function createPlanningMemberCard(member, teamId, dateStr, canEdit, isPast) {
|
||||||
card.dataset.date = dateStr;
|
card.dataset.date = dateStr;
|
||||||
card.dataset.memberName = member.name;
|
card.dataset.memberName = member.name;
|
||||||
|
|
||||||
|
const teamColor = getTeamColor(teamId);
|
||||||
|
if (!member.is_absent && !isPast) {
|
||||||
|
card.style.borderLeftColor = teamColor;
|
||||||
|
}
|
||||||
|
|
||||||
let absenceBadge = '';
|
let absenceBadge = '';
|
||||||
if (member.is_absent) {
|
if (member.is_absent) {
|
||||||
const reasonDisplay = (member.absence && member.absence.reason_display) ? member.absence.reason_display : "{% trans 'Absent'|escapejs %}";
|
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)
|
self.assertEqual(get_member_team_for_date(self.member_alice, today), self.team_a)
|
||||||
|
|
||||||
def test_move_member_takes_effect_on_day_j(self):
|
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()
|
today = timezone.now().date()
|
||||||
day_j = today + timedelta(days=2)
|
day_j = today + timedelta(days=2)
|
||||||
day_next = day_j + timedelta(days=1)
|
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)
|
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)
|
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), [])]
|
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.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_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):
|
def test_planning_past_date_is_forbidden(self):
|
||||||
"""Vérifie que les endpoints AJAX rejettent les modifications sur des dates passées."""
|
"""Vérifie que les endpoints AJAX rejettent les modifications sur des dates passées."""
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue