962 lines
36 KiB
Python
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'],
|
|
})
|
|
|