feat: add team synchronization service, management command, and improve calendar header UI

This commit is contained in:
kdeterme 2026-09-02 12:20:42 +02:00
parent da7de1ac7d
commit fad90d91c9
4 changed files with 68 additions and 9 deletions

View file

@ -0,0 +1,30 @@
from datetime import datetime
from django.core.management.base import BaseCommand
from django.utils import timezone
from contracts.services_teams import sync_all_members_current_teams
class Command(BaseCommand):
help = "Synchronise les équipes actuelles (member.teams) avec l'historique planifié pour aujourd'hui (ou une date donnée)."
def add_arguments(self, parser):
parser.add_argument(
'--date',
type=str,
help="Date de synchronisation au format YYYY-MM-DD (défaut: aujourd'hui)",
)
def handle(self, *args, **options):
date_str = options.get('date')
if date_str:
try:
target_date = datetime.strptime(date_str, '%Y-%m-%d').date()
except ValueError:
self.stderr.write(self.style.ERROR(f"Format de date invalide : {date_str} (attendu YYYY-MM-DD)"))
return
else:
target_date = timezone.now().date()
self.stdout.write(f"Synchronisation des équipes pour la date : {target_date}...")
count = sync_all_members_current_teams(target_date)
self.stdout.write(self.style.SUCCESS(f"Synchronisation terminée avec succès pour {count} membre(s)."))

View file

@ -134,6 +134,23 @@ def sync_member_current_teams(member: CompanyMember, for_date: Optional[date] =
member.teams.set(list(target_team_ids)) member.teams.set(list(target_team_ids))
def sync_all_members_current_teams(for_date: Optional[date] = None) -> int:
"""
Synchronise le champ ManyToMany member.teams pour tous les membres
selon l'équipe active dans CompanyMemberHistory à la date donnée (aujourd'hui par défaut).
Retourne le nombre de membres synchronisés.
"""
if for_date is None:
for_date = timezone.now().date()
members = CompanyMember.objects.all().prefetch_related('teams')
count = 0
for m in members:
sync_member_current_teams(m, for_date)
count += 1
return count
def get_teams_composition_for_period( def get_teams_composition_for_period(
team_ids: Set[int], team_ids: Set[int],
start_date: date, start_date: date,

View file

@ -282,10 +282,10 @@
/* Bouton de duplication rapide au bas à droite des en-têtes de jour */ /* Bouton de duplication rapide au bas à droite des en-têtes de jour */
.btn-quick-duplicate { .btn-quick-duplicate {
position: absolute; position: absolute;
bottom: 4px; bottom: 2px;
right: 4px; right: 2px;
width: 24px; width: 22px;
height: 24px; height: 22px;
padding: 0; padding: 0;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
@ -1558,8 +1558,15 @@ function renderPlanningMatrix(data) {
const optionsTitle = "{% trans 'Options de la journée'|escapejs %}"; const optionsTitle = "{% trans 'Options de la journée'|escapejs %}";
const dupCustomTitle = "{% trans 'Dupliquer cette journée…'|escapejs %}"; const dupCustomTitle = "{% trans 'Dupliquer cette journée…'|escapejs %}";
let badgeHtml = '';
if (day.is_today) {
badgeHtml = `<span class="badge bg-primary text-white" style="font-size:0.65rem; padding: 2px 6px;">{% trans "Aujourd'hui"|escapejs %}</span>`;
} else if (day.is_past) {
badgeHtml = `<span class="badge bg-light text-muted border" style="font-size:0.62rem; padding: 2px 5px;"><i class="bi bi-clock-history me-1"></i>{% trans "Passé"|escapejs %}</span>`;
}
th.innerHTML = ` th.innerHTML = `
<div class="d-flex flex-column align-items-center justify-content-center position-relative py-1" style="min-height: 56px;"> <div class="d-flex flex-column align-items-center justify-content-between position-relative py-1" style="min-height: 64px; height: 100%;">
${CAN_EDIT_TEAMS ? ` ${CAN_EDIT_TEAMS ? `
<div class="dropdown position-absolute" style="top: 0px; right: 0px;"> <div class="dropdown position-absolute" style="top: 0px; right: 0px;">
<button class="btn btn-sm btn-link text-muted p-0 dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" title="${optionsTitle}"></button> <button class="btn btn-sm btn-link text-muted p-0 dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" title="${optionsTitle}"></button>
@ -1572,10 +1579,11 @@ function renderPlanningMatrix(data) {
</ul> </ul>
</div>` : ''} </div>` : ''}
<div class="small text-uppercase ${day.is_past ? 'text-muted opacity-75' : 'text-muted'}" style="font-size:0.75rem; letter-spacing: 0.5px;">${escapeHtml(day.day_short)}</div> <div class="small text-uppercase ${day.is_past ? 'text-muted opacity-75' : 'text-muted'}" style="font-size:0.75rem; letter-spacing: 0.5px; line-height: 1.2;">${escapeHtml(day.day_short)}</div>
<div class="fw-bold fs-6 ${day.is_past ? 'text-muted' : ''}">${escapeHtml(day.day_num)}</div> <div class="fw-bold fs-6 ${day.is_past ? 'text-muted' : ''}" style="line-height: 1.2;">${escapeHtml(day.day_num)}</div>
${day.is_today ? `<span class="badge bg-primary text-white mt-1" style="font-size:0.65rem;">{% trans "Aujourd'hui"|escapejs %}</span>` : ''} <div class="header-badge-slot d-flex align-items-center justify-content-center" style="min-height: 20px; width: 100%;">
${day.is_past ? `<span class="badge bg-light text-muted border mt-1" style="font-size:0.62rem;"><i class="bi bi-clock-history me-1"></i>{% trans "Passé"|escapejs %}</span>` : ''} ${badgeHtml}
</div>
${CAN_EDIT_TEAMS ? ` ${CAN_EDIT_TEAMS ? `
<button type="button" <button type="button"

View file

@ -28,6 +28,7 @@ from contracts.services_teams import (
duplicate_day_schedule, duplicate_day_schedule,
duplicate_week_schedule, duplicate_week_schedule,
sync_member_current_teams, sync_member_current_teams,
sync_all_members_current_teams,
) )
User = get_user_model() User = get_user_model()
@ -54,6 +55,9 @@ def teams_index(request):
if not user_can_view_teams(request.user): if not user_can_view_teams(request.user):
raise PermissionDenied("Vous n'avez pas accès à la gestion des équipes.") 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 = ( teams = (
get_viewable_teams_for_user(request.user) get_viewable_teams_for_user(request.user)
.select_related("company") .select_related("company")