loko/loko/contracts/views_teams.py

962 lines
36 KiB
Python

import calendar
import json
from datetime import date, timedelta, datetime
from django.contrib.auth import get_user_model
from django.contrib.auth.decorators import login_required
from django.core.exceptions import PermissionDenied
from django.http import JsonResponse
from django.shortcuts import get_object_or_404, render
from django.db.models import Count, Q
from django.utils import timezone
from django.utils.dateparse import parse_date
from django.utils.translation import gettext as _
from django.views.decorators.http import require_GET, require_POST
from contracts.models import CompanyMember, CompanyTeam, CompanyMemberAbsence
from contracts.permissions import (
get_viewable_teams_for_user,
user_can_edit_specific_team,
user_can_edit_teams,
user_can_view_teams,
)
from contracts.services_teams import (
set_member_team_schedule,
set_member_team_period_schedule,
get_member_team_for_date,
get_teams_composition_for_period,
get_member_absences_for_period,
toggle_member_absence,
duplicate_day_schedule,
duplicate_week_schedule,
duplicate_month_schedule,
sync_member_current_teams,
sync_all_members_current_teams,
)
User = get_user_model()
def _get_user_company(user):
"""Retourne la Company associée à l'utilisateur, ou None."""
try:
return user.config.company
except AttributeError:
return None
def _is_admin(user):
try:
return user.config.roles.filter(name="admin").exists()
except AttributeError:
return False
@login_required
def teams_index(request):
"""Vue principale de gestion des équipes."""
if not user_can_view_teams(request.user):
raise PermissionDenied("Vous n'avez pas accès à la gestion des équipes.")
# Synchroniser les équipes courantes pour aujourd'hui selon l'historique planifié
sync_all_members_current_teams()
teams = (
get_viewable_teams_for_user(request.user)
.select_related("company")
.prefetch_related("members__user")
.annotate(
active_interventions_count=Count(
'assigned_interventions',
filter=Q(assigned_interventions__status_order__lte=51)
)
)
.order_by("company__name", "name")
)
can_edit = user_can_edit_teams(request.user)
user_company = _get_user_company(request.user)
is_admin_user = _is_admin(request.user)
# Les non-admins ne voient que les utilisateurs de leur propre société et des sociétés des équipes qu'ils ont le droit d'éditer
if is_admin_user or user_company is None:
available_users = User.objects.all().select_related("config__company").order_by("first_name", "last_name", "username")
else:
from contracts.models import UserCompanyTeamAccess
editable_team_companies = CompanyTeam.objects.filter(
user_accesses__user_config=request.user.config,
user_accesses__can_edit=True
).values_list('company_id', flat=True)
allowed_company_ids = set(editable_team_companies)
allowed_company_ids.add(user_company.id)
available_users = User.objects.filter(
config__company_id__in=allowed_company_ids
).select_related("config__company").order_by("first_name", "last_name", "username")
# Membres non assignés à une équipe
unassigned_qs = CompanyMember.objects.filter(teams__isnull=True).select_related("user")
if not is_admin_user and user_company is not None:
unassigned_qs = unassigned_qs.filter(user__config__company=user_company)
unassigned_members = unassigned_qs.order_by("name")
user_config = getattr(request.user, 'config', None)
user_roles = set(user_config.roles.values_list('name', flat=True)) if user_config else set()
can_view_member_history = bool(user_roles & {'admin', 'manager', 'external_manager'}) or request.user.is_superuser
return render(
request,
"contracts/teams_index.html",
{
"teams": teams,
"can_edit": can_edit,
"can_view_member_history": can_view_member_history,
"available_users": available_users,
"unassigned_members": unassigned_members,
"user_company": user_company,
"is_admin": is_admin_user,
},
)
@login_required
@require_POST
def teams_move_member(request, member_id):
"""Déplacer un membre vers une autre équipe (AJAX)."""
try:
data = json.loads(request.body)
except (json.JSONDecodeError, ValueError):
return JsonResponse({"error": "JSON invalide"}, status=400)
# team_id peut être None / absent pour désassigner
raw_team_id = data.get("team_id")
new_team_id = int(raw_team_id) if raw_team_id is not None else None
# old_team_id represents the team column they are dragged out of
old_team_id = data.get("old_team_id")
old_team_id = int(old_team_id) if old_team_id is not None else None
member = get_object_or_404(CompanyMember, id=member_id)
viewable = get_viewable_teams_for_user(request.user)
# Vérifier l'accès à l'équipe source (si le membre était dans une équipe)
if old_team_id is not None:
if not viewable.filter(id=old_team_id).exists():
raise PermissionDenied("Accès refusé.")
if not user_can_edit_specific_team(request.user, old_team_id):
raise PermissionDenied("Droits d'édition insuffisants.")
else:
if not user_can_edit_teams(request.user):
raise PermissionDenied("Droits d'édition insuffisants.")
if new_team_id is not None:
new_team = get_object_or_404(CompanyTeam, id=new_team_id)
if not viewable.filter(id=new_team_id).exists():
raise PermissionDenied("Accès refusé.")
if not user_can_edit_specific_team(request.user, new_team_id):
raise PermissionDenied("Droits d'édition insuffisants.")
else:
new_team = None
old_team = None
if old_team_id is not None:
old_team = get_object_or_404(CompanyTeam, id=old_team_id)
action = data.get("action", "move")
today = timezone.now().date()
if new_team_id is None and old_team_id is not None:
set_member_team_schedule(
member=member,
team_id=None,
target_date=today,
changed_by=request.user,
unassign_team_id=old_team_id,
)
else:
set_member_team_schedule(
member=member,
team_id=new_team_id,
target_date=today,
changed_by=request.user,
action=action,
)
member.save()
return JsonResponse(
{
"success": True,
"member_id": member.id,
"member_name": member.name,
"old_team_name": old_team.name if old_team else None,
"new_team_name": new_team.name if new_team else None,
}
)
@login_required
@require_POST
def teams_create_member(request):
"""Créer un nouveau membre, optionnellement dans une équipe (AJAX)."""
try:
data = json.loads(request.body)
except (json.JSONDecodeError, ValueError):
return JsonResponse({"error": "JSON invalide"}, status=400)
name = data.get("name", "").strip()
user_id = data.get("user_id")
raw_team_id = data.get("team_id")
team_id = int(raw_team_id) if raw_team_id is not None else None
if not name:
return JsonResponse({"error": "Le nom du membre est requis."}, status=400)
if not user_id:
return JsonResponse({"error": "L'utilisateur est requis."}, status=400)
if team_id is not None:
if not user_can_edit_specific_team(request.user, team_id):
raise PermissionDenied("Droits d'édition insuffisants.")
viewable = get_viewable_teams_for_user(request.user)
if not viewable.filter(id=team_id).exists():
raise PermissionDenied("Accès refusé.")
team = get_object_or_404(CompanyTeam, id=team_id)
else:
if not user_can_edit_teams(request.user):
raise PermissionDenied("Droits d'édition insuffisants.")
team = None
user_obj = get_object_or_404(User, id=user_id)
# Les non-admins ne peuvent ajouter que des utilisateurs de leur société, ou de la société de l'équipe concernée
user_company = _get_user_company(request.user)
if not _is_admin(request.user) and user_company is not None:
target_company = _get_user_company(user_obj)
allowed_companies = {user_company}
if team is not None:
allowed_companies.add(team.company)
if target_company not in allowed_companies:
return JsonResponse(
{"error": "Vous ne pouvez ajouter que des utilisateurs de votre société ou de la société de l'équipe."},
status=403,
)
# Check if there is already a CompanyMember for this user
member = CompanyMember.objects.filter(user=user_obj).first()
if member:
if team:
if member.teams.filter(id=team.id).exists():
return JsonResponse(
{"error": "Cet utilisateur fait déjà partie de cette équipe."},
status=400,
)
else:
if not member.teams.exists():
return JsonResponse(
{"error": "Cet utilisateur est déjà dans les membres non assignés."},
status=400,
)
if name:
member.name = name
member.save()
else:
member = CompanyMember.objects.create(name=name, user=user_obj)
if team:
member.teams.add(team)
return JsonResponse(
{
"success": True,
"member": {
"id": member.id,
"name": member.name,
"team_id": team.id if team else None,
"team_name": team.name if team else None,
"user_display": user_obj.get_full_name() or user_obj.username,
},
}
)
@login_required
@require_POST
def teams_delete_member(request, member_id):
"""Supprimer un membre (AJAX)."""
member = get_object_or_404(CompanyMember, id=member_id)
member_team_ids = list(member.teams.values_list('id', flat=True))
if member_team_ids:
viewable = get_viewable_teams_for_user(request.user)
for t_id in member_team_ids:
if not viewable.filter(id=t_id).exists():
raise PermissionDenied("Accès refusé.")
if not user_can_edit_specific_team(request.user, t_id):
raise PermissionDenied("Droits d'édition insuffisants.")
else:
# Membre non assigné : droits d'édition généraux suffisent
if not user_can_edit_teams(request.user):
raise PermissionDenied("Droits d'édition insuffisants.")
member_name = member.name
member.delete()
return JsonResponse({"success": True, "member_name": member_name})
@login_required
@require_POST
def teams_update_member(request, member_id):
"""Modifier le nom et/ou l'équipe d'un membre (AJAX)."""
member = get_object_or_404(CompanyMember, id=member_id)
viewable = get_viewable_teams_for_user(request.user)
try:
data = json.loads(request.body)
except (json.JSONDecodeError, ValueError):
return JsonResponse({"error": "JSON invalide"}, status=400)
# old_team_id represents the team column they clicked edit on (or None if unassigned)
old_team_id = data.get("old_team_id")
old_team_id = int(old_team_id) if old_team_id is not None else None
# Vérifier l'accès à l'équipe source
if old_team_id is not None:
if not viewable.filter(id=old_team_id).exists():
raise PermissionDenied("Accès refusé.")
if not user_can_edit_specific_team(request.user, old_team_id):
raise PermissionDenied("Droits d'édition insuffisants.")
else:
if not user_can_edit_teams(request.user):
raise PermissionDenied("Droits d'édition insuffisants.")
new_name = data.get("name", "").strip()
if not new_name:
return JsonResponse({"error": "Le nom du membre est requis."}, status=400)
raw_team_id = data.get("team_id") # peut être absent (non modifié) ou None (désassigner)
team_changed = "team_id" in data
new_team_id = None
new_team = None
if team_changed:
new_team_id = int(raw_team_id) if raw_team_id is not None else None
if new_team_id is not None:
new_team = get_object_or_404(CompanyTeam, id=new_team_id)
if not viewable.filter(id=new_team_id).exists():
raise PermissionDenied("Accès refusé.")
if not user_can_edit_specific_team(request.user, new_team_id):
raise PermissionDenied("Droits d'édition insuffisants.")
else:
new_team = None
# Effectuer le transfert via le service de planification (date d'aujourd'hui)
today = timezone.now().date()
action = data.get("action", "move")
if new_team_id is None and old_team_id is not None:
set_member_team_schedule(
member,
None,
today,
changed_by=request.user,
unassign_team_id=old_team_id,
)
else:
set_member_team_schedule(
member,
new_team_id,
today,
changed_by=request.user,
action=action,
)
member.name = new_name
member.save()
return JsonResponse({
"success": True,
"member_id": member.id,
"member_name": member.name,
"team_id": new_team_id if team_changed else old_team_id,
"team_name": new_team.name if team_changed and new_team else None,
})
@login_required
@require_GET
def team_interventions_json(request, team_id):
"""Retourne les interventions assignées à une équipe (JSON, pour la modale)."""
from django.urls import reverse
from interventions.models import Intervention
team = get_object_or_404(CompanyTeam, id=team_id)
viewable = get_viewable_teams_for_user(request.user)
if not viewable.filter(id=team_id).exists():
raise PermissionDenied("Accès refusé.")
interventions = (
Intervention.objects
.filter(assigned_team=team, status_order__lte=51)
.select_related('assigned_member')
.order_by('-creation_time')
[:500]
)
data = []
for i in interventions:
data.append({
'id': i.id,
'code': i.code,
'title': i.title,
'status': i.status,
'status_display': i.get_status_display(),
'status_color': i.get_status_color(),
'assigned_member_name': i.assigned_member.name if i.assigned_member else None,
'summary_url': reverse('interventions:intervention_summary_modal', args=[i.id]),
'detail_url': reverse('interventions:intervention_detail', args=[i.id]),
})
return JsonResponse({'interventions': data, 'team_name': team.name})
# ---------------------------------------------------------------------------
# Vues AJAX de planification avancée des équipes (Onglet Calendrier)
# ---------------------------------------------------------------------------
@login_required
@require_GET
def team_planning_data_ajax(request):
"""
Retourne la matrice complète de planification des équipes sur une période.
Paramètres GET :
- date : date d'ancrage (YYYY-MM-DD), par défaut aujourd'hui
- view_mode : 'work_week' (5j), 'days_7' (7j), 'week' (5 semaines), 'month' (6 mois), 'two_weeks' (14j)
- company_id : filtrer sur une société spécifique (optionnel)
"""
if not user_can_view_teams(request.user):
raise PermissionDenied("Vous n'avez pas accès à la gestion des équipes.")
# 1. Déterminer la période
date_str = request.GET.get('date')
anchor_date = parse_date(date_str) if date_str else timezone.now().date()
if not anchor_date:
anchor_date = timezone.now().date()
view_mode = request.GET.get('view_mode', 'week')
monday = anchor_date - timedelta(days=anchor_date.weekday())
today = timezone.now().date()
DAYS_FR_SHORT = ['Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam', 'Dim']
DAYS_FR_FULL = ['Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi', 'Samedi', 'Dimanche']
MONTHS_FR = ['', 'Janv', 'Févr', 'Mars', 'Avr', 'Mai', 'Juin', 'Juil', 'Août', 'Sept', 'Oct', 'Nov', 'Déc']
MONTHS_FR_FULL = ['', 'Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre']
days_list = []
if view_mode == 'work_week':
num_days = 5
start_date = monday
end_date = monday + timedelta(days=4)
for i in range(num_days):
d = start_date + timedelta(days=i)
w_idx = d.weekday()
days_list.append({
'date': d.isoformat(),
'day_name': DAYS_FR_FULL[w_idx],
'day_short': DAYS_FR_SHORT[w_idx],
'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': False,
'col_type': 'day',
})
elif view_mode in ('days_7', '7_days'):
num_days = 7
start_date = monday
end_date = monday + timedelta(days=6)
for i in range(num_days):
d = start_date + timedelta(days=i)
w_idx = d.weekday()
days_list.append({
'date': d.isoformat(),
'day_name': DAYS_FR_FULL[w_idx],
'day_short': DAYS_FR_SHORT[w_idx],
'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,
'col_type': 'day',
})
elif view_mode == 'two_weeks':
num_days = 14
start_date = monday
end_date = monday + timedelta(days=13)
for i in range(num_days):
d = start_date + timedelta(days=i)
w_idx = d.weekday()
days_list.append({
'date': d.isoformat(),
'day_name': DAYS_FR_FULL[w_idx],
'day_short': DAYS_FR_SHORT[w_idx],
'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,
'col_type': 'day',
})
elif view_mode == 'month':
# Horizon glissant de 6 mois
start_date = date(anchor_date.year, anchor_date.month, 1)
end_date = start_date
for i in range(6):
m_year = anchor_date.year + (anchor_date.month - 1 + i) // 12
m_month = (anchor_date.month - 1 + i) % 12 + 1
_, days_in_m = calendar.monthrange(m_year, m_month)
m_start = date(m_year, m_month, 1)
m_end = date(m_year, m_month, days_in_m)
if i == 5:
end_date = m_end
is_current_month = (m_start <= today <= m_end)
is_past_month = (m_end < today)
days_list.append({
'date': m_start.isoformat(),
'end_date': m_end.isoformat(),
'day_name': f"{MONTHS_FR_FULL[m_month]} {m_year}",
'day_short': MONTHS_FR[m_month],
'day_num': str(m_year),
'formatted': f"{MONTHS_FR_FULL[m_month]} {m_year}",
'is_today': is_current_month,
'is_past': is_past_month,
'is_weekend': False,
'col_type': 'month',
})
else: # 'week' (défaut) : Horizon glissant de 5 semaines
start_date = monday
end_date = monday + timedelta(days=5 * 7 - 1)
for i in range(5):
w_start = monday + timedelta(weeks=i)
w_end = w_start + timedelta(days=6)
iso_year, iso_week, _ = w_start.isocalendar()
is_current_week = (w_start <= today <= w_end)
is_past_week = (w_end < today)
days_list.append({
'date': w_start.isoformat(),
'end_date': w_end.isoformat(),
'day_name': f"Semaine {iso_week}",
'day_short': f"S{iso_week}",
'day_num': f"{w_start.day:02d}/{w_start.month:02d} - {w_end.day:02d}/{w_end.month:02d}",
'formatted': f"Semaine {iso_week} ({w_start.day:02d} {MONTHS_FR[w_start.month]} - {w_end.day:02d} {MONTHS_FR[w_end.month]})",
'is_today': is_current_week,
'is_past': is_past_week,
'is_weekend': False,
'col_type': 'week',
})
# 2. Récupérer les équipes autorisées
teams_qs = get_viewable_teams_for_user(request.user).select_related('company')
raw_company_id = request.GET.get('company_id')
if raw_company_id:
try:
teams_qs = teams_qs.filter(company_id=int(raw_company_id))
except (ValueError, TypeError):
pass
teams = list(teams_qs.order_by('company__name', 'name'))
team_ids = {t.id for t in teams}
# 3. Récupérer la composition des équipes sur la période
compositions = get_teams_composition_for_period(team_ids, start_date, end_date)
# 4. Récupérer tous les membres disponibles pour la sidebar
is_admin_user = _is_admin(request.user)
user_company = _get_user_company(request.user)
all_members_qs = CompanyMember.objects.select_related('user', 'user__config').order_by('name')
if not is_admin_user and user_company is not None:
all_members_qs = all_members_qs.filter(user__config__company=user_company)
all_members = list(all_members_qs)
all_member_ids = {m.id for m in all_members}
# 5. Récupérer les absences
absences = get_member_absences_for_period(all_member_ids, start_date, end_date)
# 6. Structurer les données des équipes
teams_data = []
for t in teams:
can_edit_this = user_can_edit_specific_team(request.user, t.id)
days_dict = {}
if view_mode in ('week', 'month'):
for col in days_list:
col_start = parse_date(col['date'])
col_end = parse_date(col['end_date'])
num_col_days = (col_end - col_start).days + 1
members_map = {}
for day_idx in range(num_col_days):
d = col_start + timedelta(days=day_idx)
for m in compositions.get((t.id, d), []):
m_id = m['id']
if m_id not in members_map:
members_map[m_id] = {
'id': m_id,
'name': m['name'],
'user_id': m.get('user_id'),
'is_absent': False,
'absence': None,
'is_multi_team': m.get('is_multi_team', False),
'other_teams': [],
'days_count': 0,
'total_days': num_col_days,
}
members_map[m_id]['days_count'] += 1
if m.get('is_multi_team'):
members_map[m_id]['is_multi_team'] = True
for ot in m.get('other_teams', []):
if not any(x['id'] == ot['id'] for x in members_map[m_id]['other_teams']):
members_map[m_id]['other_teams'].append(ot)
abs_info = absences.get((m_id, d))
if abs_info is not None:
members_map[m_id]['is_absent'] = True
members_map[m_id]['absence'] = abs_info
days_dict[col['date']] = sorted(members_map.values(), key=lambda x: x['name'].lower())
else:
for day_info in days_list:
d = parse_date(day_info['date'])
d_str = day_info['date']
members_on_day = []
for m in compositions.get((t.id, d), []):
m_id = m['id']
abs_info = absences.get((m_id, d))
members_on_day.append({
'id': m_id,
'name': m['name'],
'user_id': m.get('user_id'),
'is_absent': abs_info is not None,
'absence': abs_info,
'is_multi_team': m.get('is_multi_team', False),
'other_teams': m.get('other_teams', []),
})
days_dict[d_str] = members_on_day
teams_data.append({
'id': t.id,
'name': t.name,
'company_id': t.company_id,
'company_name': t.company.name if t.company else '',
'can_edit': can_edit_this,
'days': days_dict,
})
# 7. Structurer les membres pour le volet latéral (Sidebar)
if view_mode in ('week', 'days_7', '7_days'):
absent_member_ids = {mid for (mid, d) in absences.keys() if monday <= d <= monday + timedelta(days=6)}
elif view_mode == 'month':
_, d_in_m = calendar.monthrange(anchor_date.year, anchor_date.month)
m_s = date(anchor_date.year, anchor_date.month, 1)
m_e = date(anchor_date.year, anchor_date.month, d_in_m)
absent_member_ids = {mid for (mid, d) in absences.keys() if m_s <= d <= m_e}
elif view_mode == 'work_week':
absent_member_ids = {mid for (mid, d) in absences.keys() if monday <= d <= monday + timedelta(days=4)}
else:
absent_member_ids = {mid for (mid, d) in absences.keys() if d == anchor_date}
members_data = []
for m in all_members:
roles = []
if m.user and hasattr(m.user, 'config'):
roles = list(m.user.config.roles.values_list('name', flat=True))
members_data.append({
'id': m.id,
'name': m.name,
'user_display': m.user.get_full_name() or m.user.username if m.user else '',
'roles': roles,
'company_ids': list(m.teams.values_list('company_id', flat=True)),
'is_absent_on_anchor': m.id in absent_member_ids,
})
# 8. Récupérer les sociétés pour le filtre de la toolbar
companies_dict = {}
for t in teams:
if t.company_id and t.company:
companies_dict[t.company_id] = t.company.name
companies_list = [{'id': cid, 'name': cname} for cid, cname in sorted(companies_dict.items(), key=lambda x: x[1])]
user_config = getattr(request.user, 'config', None)
user_roles = set(user_config.roles.values_list('name', flat=True)) if user_config else set()
can_view_member_history = bool(user_roles & {'admin', 'manager', 'external_manager'}) or request.user.is_superuser
return JsonResponse({
'days': days_list,
'teams': teams_data,
'members': members_data,
'companies': companies_list,
'start_date': start_date.isoformat(),
'end_date': end_date.isoformat(),
'anchor_date': anchor_date.isoformat(),
'view_mode': view_mode,
'can_edit': user_can_edit_teams(request.user),
'can_view_member_history': can_view_member_history,
})
@login_required
@require_POST
def team_planning_assign_member_ajax(request):
"""
Assigne ou déplace un membre vers une équipe à une date donnée (AJAX).
Payload JSON : { member_id, team_id, date } (team_id peut être null pour désassigner)
"""
try:
data = json.loads(request.body)
except (json.JSONDecodeError, ValueError):
return JsonResponse({"error": "JSON invalide"}, status=400)
member_id = data.get("member_id")
raw_team_id = data.get("team_id")
date_str = data.get("date")
scope = data.get("scope", "day") # 'day', 'week', 'work_week', 'month'
if not member_id or not date_str:
return JsonResponse({"error": "Paramètres member_id et date requis."}, status=400)
target_date = parse_date(date_str)
if not target_date:
return JsonResponse({"error": "Date invalide."}, status=400)
today = timezone.now().date()
if scope == "day" and 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)
team_id = int(raw_team_id) if raw_team_id is not None else None
if team_id is not None:
if not viewable.filter(id=team_id).exists():
raise PermissionDenied("Accès refusé.")
if not user_can_edit_specific_team(request.user, team_id):
raise PermissionDenied("Droits d'édition insuffisants.")
else:
if not user_can_edit_teams(request.user):
raise PermissionDenied("Droits d'édition insuffisants.")
action = data.get("action", "move")
raw_unassign_team_id = data.get("unassign_team_id")
unassign_team_id = int(raw_unassign_team_id) if raw_unassign_team_id is not None else None
# Effectuer la planification selon la portée choisie
if scope in ("week", "work_week"):
monday = target_date - timedelta(days=target_date.weekday())
week_end = monday + timedelta(days=4 if scope == "work_week" else 6)
if week_end < today:
return JsonResponse({"error": _("Impossible de modifier une période passée.")}, status=400)
updated_count = set_member_team_period_schedule(
member=member,
team_id=team_id,
start_date=monday,
end_date=week_end,
action=action,
unassign_team_id=unassign_team_id,
changed_by=request.user,
weekdays_only=(scope == "work_week"),
)
elif scope == "month":
_, days_in_month = calendar.monthrange(target_date.year, target_date.month)
month_start = date(target_date.year, target_date.month, 1)
month_end = date(target_date.year, target_date.month, days_in_month)
if month_end < today:
return JsonResponse({"error": _("Impossible de modifier une période passée.")}, status=400)
updated_count = set_member_team_period_schedule(
member=member,
team_id=team_id,
start_date=month_start,
end_date=month_end,
action=action,
unassign_team_id=unassign_team_id,
changed_by=request.user,
)
else:
set_member_team_schedule(
member=member,
team_id=team_id,
target_date=target_date,
changed_by=request.user,
sync_current=True,
action=action,
unassign_team_id=unassign_team_id,
)
updated_count = 1
return JsonResponse({
"success": True,
"member_id": member.id,
"member_name": member.name,
"team_id": team_id,
"date": target_date.isoformat(),
"scope": scope,
"updated_count": updated_count,
})
@login_required
@require_POST
def team_planning_toggle_absence_ajax(request):
"""
Active ou désactive l'absence d'un membre pour une date donnée (AJAX).
Payload JSON : { member_id, date, is_absent, reason, comment }
"""
if not user_can_edit_teams(request.user):
raise PermissionDenied("Droits d'édition insuffisants.")
try:
data = json.loads(request.body)
except (json.JSONDecodeError, ValueError):
return JsonResponse({"error": "JSON invalide"}, status=400)
member_id = data.get("member_id")
date_str = data.get("date")
is_absent = bool(data.get("is_absent", True))
reason = data.get("reason", "conge")
comment = data.get("comment", "")
if not member_id or not date_str:
return JsonResponse({"error": "Paramètres member_id et date requis."}, status=400)
target_date = parse_date(date_str)
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(
member=member,
target_date=target_date,
is_absent=is_absent,
reason=reason,
comment=comment,
created_by=request.user,
)
return JsonResponse({
"success": True,
"member_id": member.id,
"date": target_date.isoformat(),
"is_absent": is_absent,
"reason": absence_obj.reason if absence_obj else None,
"reason_display": absence_obj.get_reason_display() if absence_obj else None,
"comment": absence_obj.comment if absence_obj else "",
})
@login_required
@require_POST
def team_planning_duplicate_ajax(request):
"""
Duplique la configuration d'une journée ou d'une semaine vers une autre date cible (AJAX).
Payload JSON : { mode: 'day'|'week', source_date, target_date, copy_absences }
"""
if not user_can_edit_teams(request.user):
raise PermissionDenied("Droits d'édition insuffisants.")
try:
data = json.loads(request.body)
except (json.JSONDecodeError, ValueError):
return JsonResponse({"error": "JSON invalide"}, status=400)
mode = data.get("mode", "day")
source_date_str = data.get("source_date")
target_date_str = data.get("target_date")
copy_absences = bool(data.get("copy_absences", False))
if not source_date_str or not target_date_str:
return JsonResponse({"error": "Dates source et cible requises."}, status=400)
# Si mode == 'month' et dates au format 'YYYY-MM', normaliser avec le 1er du mois
if mode == "month":
if len(source_date_str) == 7:
source_date_str = f"{source_date_str}-01"
if len(target_date_str) == 7:
target_date_str = f"{target_date_str}-01"
source_date = parse_date(source_date_str)
target_date = parse_date(target_date_str)
if not source_date or not target_date:
return JsonResponse({"error": "Format de date invalide."}, status=400)
today = timezone.now().date()
if mode == "month":
if (target_date.year, target_date.month) < (today.year, today.month):
return JsonResponse({"error": _("Le mois cible ne peut pas être dans le passé.")}, status=400)
elif 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):
from contracts.models import UserCompanyTeamAccess
editable_ids = set(
UserCompanyTeamAccess.objects.filter(
user_config__user=request.user,
can_edit=True
).values_list('team_id', flat=True)
)
allowed_team_ids = set(viewable.filter(id__in=editable_ids).values_list('id', flat=True))
else:
allowed_team_ids = set(viewable.values_list('id', flat=True))
if mode == "month":
res = duplicate_month_schedule(
source_year=source_date.year,
source_month=source_date.month,
target_year=target_date.year,
target_month=target_date.month,
allowed_team_ids=allowed_team_ids,
copy_absences=copy_absences,
changed_by=request.user,
)
elif mode == "week":
# S'assurer de pointer sur les lundis
source_monday = source_date - timedelta(days=source_date.weekday())
target_monday = target_date - timedelta(days=target_date.weekday())
res = duplicate_week_schedule(
source_monday=source_monday,
target_monday=target_monday,
allowed_team_ids=allowed_team_ids,
copy_absences=copy_absences,
changed_by=request.user,
)
else:
res = duplicate_day_schedule(
source_date=source_date,
target_date=target_date,
allowed_team_ids=allowed_team_ids,
copy_absences=copy_absences,
changed_by=request.user,
)
return JsonResponse({
"success": True,
"mode": mode,
"assigned_count": res['assigned_count'],
"absences_count": res['absences_count'],
})