feat: add team synchronization service, management command, and improve calendar header UI
This commit is contained in:
parent
da7de1ac7d
commit
fad90d91c9
4 changed files with 68 additions and 9 deletions
30
loko/contracts/management/commands/sync_teams_schedule.py
Normal file
30
loko/contracts/management/commands/sync_teams_schedule.py
Normal 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)."))
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue