+ ${actionsHtml}
`;
return card;
@@ -1500,8 +1583,8 @@ function createPlanningMemberCard(member, teamId, dateStr, canEdit) {
function initPlanningDragAndDrop() {
if (!CAN_EDIT_TEAMS) return;
- // Cartes de membres dans la grille draggables
- $('.planning-member-card').draggable({
+ // Cartes de membres dans la grille draggables (uniquement sur les jours présents ou futurs)
+ $('.planning-member-card:not(.is-past-member)').draggable({
revert: 'invalid',
cursor: 'grabbing',
helper: 'clone',
@@ -1524,9 +1607,9 @@ function initPlanningDragAndDrop() {
}
});
- // Cellules de dépôt
- $('.planning-drop-cell').droppable({
- accept: '.planning-member-card, .sidebar-member-item',
+ // Cellules de dépôt (uniquement sur les jours présents ou futurs)
+ $('.planning-drop-cell:not(.is-past-cell)').droppable({
+ accept: '.planning-member-card:not(.is-past-member), .sidebar-member-item',
hoverClass: 'drag-over',
tolerance: 'pointer',
drop: function(event, ui) {
diff --git a/loko/contracts/test_planning.py b/loko/contracts/test_planning.py
index ad0cb56..17e0714 100644
--- a/loko/contracts/test_planning.py
+++ b/loko/contracts/test_planning.py
@@ -202,3 +202,62 @@ class TeamPlanningAjaxViewsTests(TestCase):
today = timezone.now().date()
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."""
+ 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
+ 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), [])]
+ self.assertIn(self.member_alice.id, team_a_day_j_members)
+
+ # 2. On déplace Alice vers Team B pour le jour J
+ set_member_team_schedule(self.member_alice, self.team_b.id, day_j)
+
+ # 3. Vérifier que sur le jour J, Alice est bien dans Team B (et plus dans Team A)
+ comp_after = get_teams_composition_for_period({self.team_a.id, self.team_b.id}, day_j, day_next)
+ team_a_day_j_after = [m['id'] for m in comp_after.get((self.team_a.id, day_j), [])]
+ team_b_day_j_after = [m['id'] for m in comp_after.get((self.team_b.id, day_j), [])]
+ team_b_day_next_after = [m['id'] for m in comp_after.get((self.team_b.id, day_next), [])]
+
+ 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)
+
+ def test_planning_past_date_is_forbidden(self):
+ """Vérifie que les endpoints AJAX rejettent les modifications sur des dates passées."""
+ past_date = (timezone.now().date() - timedelta(days=2)).isoformat()
+
+ # 1. Assign
+ url = reverse('contracts:team_planning_assign_member_ajax')
+ resp = self.client.post(url, data=json.dumps({
+ 'member_id': self.member_alice.id,
+ 'team_id': self.team_a.id,
+ 'date': past_date,
+ }), content_type='application/json')
+ self.assertEqual(resp.status_code, 400)
+ self.assertIn('passé', resp.json().get('error', '').lower())
+
+ # 2. Absence
+ url_abs = reverse('contracts:team_planning_toggle_absence_ajax')
+ resp_abs = self.client.post(url_abs, data=json.dumps({
+ 'member_id': self.member_alice.id,
+ 'date': past_date,
+ 'is_absent': True,
+ }), content_type='application/json')
+ self.assertEqual(resp_abs.status_code, 400)
+ self.assertIn('passé', resp_abs.json().get('error', '').lower())
+
+ # 3. Duplicate target in past
+ url_dup = reverse('contracts:team_planning_duplicate_ajax')
+ resp_dup = self.client.post(url_dup, data=json.dumps({
+ 'mode': 'day',
+ 'source_date': timezone.now().date().isoformat(),
+ 'target_date': past_date,
+ }), content_type='application/json')
+ self.assertEqual(resp_dup.status_code, 400)
+ self.assertIn('passé', resp_dup.json().get('error', '').lower())
+
diff --git a/loko/contracts/views_teams.py b/loko/contracts/views_teams.py
index be20ded..beb19f6 100644
--- a/loko/contracts/views_teams.py
+++ b/loko/contracts/views_teams.py
@@ -438,6 +438,7 @@ def team_planning_data_ajax(request):
'day_num': f"{d.day:02d}/{d.month:02d}",
'formatted': f"{DAYS_FR_SHORT[w_idx]} {d.day:02d} {MONTHS_FR[d.month]}",
'is_today': d == today,
+ 'is_past': d < today,
'is_weekend': w_idx >= 5,
})
@@ -559,6 +560,10 @@ def team_planning_assign_member_ajax(request):
if not target_date:
return JsonResponse({"error": "Date invalide."}, status=400)
+ today = timezone.now().date()
+ if target_date < today:
+ return JsonResponse({"error": "Impossible de modifier la planification d'un jour passé."}, status=400)
+
member = get_object_or_404(CompanyMember, id=member_id)
viewable = get_viewable_teams_for_user(request.user)
@@ -618,6 +623,10 @@ def team_planning_toggle_absence_ajax(request):
if not target_date:
return JsonResponse({"error": "Date invalide."}, status=400)
+ today = timezone.now().date()
+ if target_date < today:
+ return JsonResponse({"error": "Impossible de modifier les absences pour un jour passé."}, status=400)
+
member = get_object_or_404(CompanyMember, id=member_id)
absence_obj = toggle_member_absence(
@@ -669,6 +678,10 @@ def team_planning_duplicate_ajax(request):
if not source_date or not target_date:
return JsonResponse({"error": "Format de date invalide."}, status=400)
+ today = timezone.now().date()
+ if target_date < today:
+ return JsonResponse({"error": "La date cible ne peut pas être dans le passé."}, status=400)
+
# Récupérer les équipes que l'utilisateur a le droit d'éditer
viewable = get_viewable_teams_for_user(request.user)
if not _is_admin(request.user):