feat: add rolling horizons and aggregated column support to team planning view modes
This commit is contained in:
parent
096886a194
commit
40ce03d812
3 changed files with 270 additions and 70 deletions
|
|
@ -727,9 +727,8 @@
|
|||
<span class="text-muted small text-nowrap"><i class="bi bi-eye me-1"></i>{% trans "Vue :" %}</span>
|
||||
<div class="btn-group btn-group-sm" role="group" id="planning-viewmode-group">
|
||||
<button type="button" class="btn btn-outline-primary" data-mode="work_week">{% trans "5j (Ouvré)" %}</button>
|
||||
<button type="button" class="btn btn-primary" data-mode="week">{% trans "1 Semaine (7j)" %}</button>
|
||||
<button type="button" class="btn btn-outline-primary" data-mode="two_weeks">{% trans "2 Semaines" %}</button>
|
||||
<button type="button" class="btn btn-outline-primary" data-mode="month">{% trans "1 Mois" %}</button>
|
||||
<button type="button" class="btn btn-primary" data-mode="week">{% trans "Semaine" %}</button>
|
||||
<button type="button" class="btn btn-outline-primary" data-mode="month">{% trans "Mois" %}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -1720,12 +1719,30 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
loadPlanningData();
|
||||
});
|
||||
|
||||
// Changement de mode de vue (5j / 7j / 14j / mois)
|
||||
// Changement de mode de vue (5j / Semaine / Mois)
|
||||
$('#planning-viewmode-group button').on('click', function() {
|
||||
$('#planning-viewmode-group button').removeClass('btn-primary').addClass('btn-outline-primary');
|
||||
$(this).removeClass('btn-outline-primary').addClass('btn-primary');
|
||||
_planningState.viewMode = $(this).data('mode');
|
||||
setStoredSetting('view_mode', _planningState.viewMode);
|
||||
|
||||
if (_planningState.viewMode === 'month') {
|
||||
_planningState.assignScope = 'month';
|
||||
setStoredSetting('assign_scope', 'month');
|
||||
$('#planning-assign-scope-group button').removeClass('btn-primary').addClass('btn-outline-primary');
|
||||
$('#planning-assign-scope-group button[data-scope="month"]').removeClass('btn-outline-primary').addClass('btn-primary');
|
||||
} else if (_planningState.viewMode === 'week') {
|
||||
_planningState.assignScope = 'week';
|
||||
setStoredSetting('assign_scope', 'week');
|
||||
$('#planning-assign-scope-group button').removeClass('btn-primary').addClass('btn-outline-primary');
|
||||
$('#planning-assign-scope-group button[data-scope="week"]').removeClass('btn-outline-primary').addClass('btn-primary');
|
||||
} else if (_planningState.viewMode === 'work_week') {
|
||||
_planningState.assignScope = 'day';
|
||||
setStoredSetting('assign_scope', 'day');
|
||||
$('#planning-assign-scope-group button').removeClass('btn-primary').addClass('btn-outline-primary');
|
||||
$('#planning-assign-scope-group button[data-scope="day"]').removeClass('btn-outline-primary').addClass('btn-primary');
|
||||
}
|
||||
|
||||
loadPlanningData();
|
||||
});
|
||||
|
||||
|
|
@ -1828,11 +1845,16 @@ function navigatePlanningDate(direction) {
|
|||
if (_planningState.viewMode === 'month') {
|
||||
d.setDate(1);
|
||||
d.setMonth(d.getMonth() + direction);
|
||||
} else if (_planningState.viewMode === 'week') {
|
||||
d.setDate(d.getDate() + (direction * 7));
|
||||
} else {
|
||||
const step = _planningState.viewMode === 'two_weeks' ? 14 : 7;
|
||||
d.setDate(d.getDate() + (direction * step));
|
||||
}
|
||||
_planningState.anchorDate = d.toISOString().split('T')[0];
|
||||
const y = d.getFullYear();
|
||||
const m = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const dayNum = String(d.getDate()).padStart(2, '0');
|
||||
_planningState.anchorDate = `${y}-${m}-${dayNum}`;
|
||||
setStoredSetting('anchor_date', _planningState.anchorDate);
|
||||
_planningState.flatpickrInstance.setDate(_planningState.anchorDate);
|
||||
loadPlanningData();
|
||||
|
|
@ -1907,19 +1929,35 @@ function renderPlanningMatrix(data) {
|
|||
if (day.is_past) headerClasses.push('is-past-header');
|
||||
th.className = headerClasses.join(' ');
|
||||
|
||||
const dParts = day.date.split('-');
|
||||
const dateObj = new Date(parseInt(dParts[0], 10), parseInt(dParts[1], 10) - 1, parseInt(dParts[2], 10));
|
||||
const isFriday = dateObj.getDay() === 5;
|
||||
let quickDupTitle = "{% trans 'Dupliquer vers le jour suivant (avec absences)'|escapejs %}";
|
||||
let optionsTitle = "{% trans 'Options de la journée'|escapejs %}";
|
||||
let dupCustomTitle = "{% trans 'Dupliquer cette journée…'|escapejs %}";
|
||||
|
||||
const quickDupTitle = (_planningState.viewMode === 'work_week' && isFriday)
|
||||
? "{% trans 'Dupliquer vers le lundi suivant (avec absences)'|escapejs %}"
|
||||
: "{% trans 'Dupliquer vers le jour suivant (avec absences)'|escapejs %}";
|
||||
const optionsTitle = "{% trans 'Options de la journée'|escapejs %}";
|
||||
const dupCustomTitle = "{% trans 'Dupliquer cette journée…'|escapejs %}";
|
||||
if (_planningState.viewMode === 'week') {
|
||||
quickDupTitle = "{% trans 'Dupliquer vers la semaine suivante (avec absences)'|escapejs %}";
|
||||
optionsTitle = "{% trans 'Options de la semaine'|escapejs %}";
|
||||
dupCustomTitle = "{% trans 'Dupliquer cette semaine…'|escapejs %}";
|
||||
} else if (_planningState.viewMode === 'month') {
|
||||
quickDupTitle = "{% trans 'Dupliquer vers le mois suivant (avec absences)'|escapejs %}";
|
||||
optionsTitle = "{% trans 'Options du mois'|escapejs %}";
|
||||
dupCustomTitle = "{% trans 'Dupliquer ce mois…'|escapejs %}";
|
||||
} else if (_planningState.viewMode === 'work_week') {
|
||||
const dParts = day.date.split('-');
|
||||
const dateObj = new Date(parseInt(dParts[0], 10), parseInt(dParts[1], 10) - 1, parseInt(dParts[2], 10));
|
||||
if (dateObj.getDay() === 5) {
|
||||
quickDupTitle = "{% trans 'Dupliquer vers le lundi suivant (avec absences)'|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>`;
|
||||
let todayBadgeText = "{% trans "Aujourd'hui"|escapejs %}";
|
||||
if (_planningState.viewMode === 'week') {
|
||||
todayBadgeText = "{% trans "En cours"|escapejs %}";
|
||||
} else if (_planningState.viewMode === 'month') {
|
||||
todayBadgeText = "{% trans "En cours"|escapejs %}";
|
||||
}
|
||||
badgeHtml = `<span class="badge bg-primary text-white" style="font-size:0.65rem; padding: 2px 6px;">${todayBadgeText}</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>`;
|
||||
}
|
||||
|
|
@ -2048,6 +2086,11 @@ function createPlanningMemberCard(member, teamId, dateStr, canEdit, isPast) {
|
|||
multiTeamBadge = `<span class="badge bg-warning-subtle text-warning-emphasis border border-warning-subtle px-1 py-0 me-1" style="font-size:0.65rem;" title="${multiTitle}"><i class="bi bi-diagram-3 me-1"></i>{% trans "Multi-équipes"|escapejs %}</span>`;
|
||||
}
|
||||
|
||||
let durationBadge = '';
|
||||
if (member.days_count && member.total_days && member.days_count < member.total_days) {
|
||||
durationBadge = `<span class="badge bg-secondary-subtle text-secondary border px-1 py-0 me-1" style="font-size:0.65rem;" title="${member.days_count} {% trans 'jours sur'|escapejs %} ${member.total_days}">${member.days_count}j</span>`;
|
||||
}
|
||||
|
||||
const titleAbsence = member.is_absent ? "{% trans "Modifier ou annuler l'absence"|escapejs %}" : "{% trans "Marquer absent"|escapejs %}";
|
||||
const titleUnassign = "{% trans 'Désassigner de cette équipe'|escapejs %}";
|
||||
|
||||
|
|
@ -2073,6 +2116,7 @@ function createPlanningMemberCard(member, teamId, dateStr, canEdit, isPast) {
|
|||
<div class="planning-member-info" title="${escapeHtml(member.name)}">
|
||||
<span class="member-name-text fw-semibold">${escapeHtml(member.name)}</span>
|
||||
${multiTeamBadge}
|
||||
${durationBadge}
|
||||
${absenceBadge}
|
||||
</div>
|
||||
${actionsHtml}
|
||||
|
|
@ -2084,6 +2128,7 @@ function createPlanningMemberCard(member, teamId, dateStr, canEdit, isPast) {
|
|||
let _pendingPlanningAssign = null;
|
||||
|
||||
function executePlanningAssign(memberId, targetTeamId, targetDate, action) {
|
||||
const scope = _planningState.assignScope || (_planningState.viewMode === 'month' ? 'month' : (_planningState.viewMode === 'week' ? 'week' : 'day'));
|
||||
postJSON(
|
||||
'/contracts/teams/planning/assign/',
|
||||
{
|
||||
|
|
@ -2091,7 +2136,7 @@ function executePlanningAssign(memberId, targetTeamId, targetDate, action) {
|
|||
team_id: parseInt(targetTeamId),
|
||||
date: targetDate,
|
||||
action: action || 'move',
|
||||
scope: _planningState.assignScope || 'day',
|
||||
scope: scope,
|
||||
},
|
||||
function(resp) {
|
||||
loadPlanningData();
|
||||
|
|
@ -2279,6 +2324,7 @@ function filterSidebarMembers() {
|
|||
}
|
||||
|
||||
function unassignMemberFromDate(memberId, dateStr, teamId) {
|
||||
const scope = _planningState.assignScope || (_planningState.viewMode === 'month' ? 'month' : (_planningState.viewMode === 'week' ? 'week' : 'day'));
|
||||
postJSON(
|
||||
'/contracts/teams/planning/assign/',
|
||||
{
|
||||
|
|
@ -2286,7 +2332,7 @@ function unassignMemberFromDate(memberId, dateStr, teamId) {
|
|||
team_id: null,
|
||||
date: dateStr,
|
||||
unassign_team_id: teamId ? parseInt(teamId) : null,
|
||||
scope: _planningState.assignScope || 'day',
|
||||
scope: scope,
|
||||
},
|
||||
function(resp) {
|
||||
loadPlanningData();
|
||||
|
|
@ -2404,19 +2450,64 @@ function openDuplicateModal() {
|
|||
|
||||
function openDuplicateModalForDay(dateStr) {
|
||||
openDuplicateModal();
|
||||
$('#dupSourceDate').val(dateStr);
|
||||
const targetDateStr = getTargetDuplicateDate(dateStr, _planningState.viewMode);
|
||||
$('#dupTargetDate').val(targetDateStr);
|
||||
if (_planningState.viewMode === 'month') {
|
||||
$('input[name="dupMode"][value="month"]').prop('checked', true).trigger('change');
|
||||
const monthVal = dateStr.slice(0, 7);
|
||||
$('#dupSourceMonth').val(monthVal);
|
||||
const parts = monthVal.split('-');
|
||||
const d = new Date(parseInt(parts[0], 10), parseInt(parts[1], 10) - 1, 1);
|
||||
d.setMonth(d.getMonth() + 1);
|
||||
const nextMonthVal = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`;
|
||||
$('#dupTargetMonth').val(nextMonthVal);
|
||||
} else if (_planningState.viewMode === 'week') {
|
||||
$('input[name="dupMode"][value="week"]').prop('checked', true).trigger('change');
|
||||
$('#dupSourceWeek').val(dateStr);
|
||||
const parts = dateStr.split('-');
|
||||
const d = new Date(parseInt(parts[0], 10), parseInt(parts[1], 10) - 1, parseInt(parts[2], 10));
|
||||
d.setDate(d.getDate() + 7);
|
||||
const nextYear = d.getFullYear();
|
||||
const nextMonth = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const nextDay = String(d.getDate()).padStart(2, '0');
|
||||
$('#dupTargetWeek').val(`${nextYear}-${nextMonth}-${nextDay}`);
|
||||
} else {
|
||||
$('input[name="dupMode"][value="day"]').prop('checked', true).trigger('change');
|
||||
$('#dupSourceDate').val(dateStr);
|
||||
const targetDateStr = getTargetDuplicateDate(dateStr, _planningState.viewMode);
|
||||
$('#dupTargetDate').val(targetDateStr);
|
||||
}
|
||||
}
|
||||
|
||||
function quickDuplicateToNextDay(sourceDateStr) {
|
||||
const targetDateStr = getTargetDuplicateDate(sourceDateStr, _planningState.viewMode);
|
||||
let mode = 'day';
|
||||
let targetDateStr;
|
||||
|
||||
if (_planningState.viewMode === 'week') {
|
||||
mode = 'week';
|
||||
const parts = sourceDateStr.split('-');
|
||||
const d = new Date(parseInt(parts[0], 10), parseInt(parts[1], 10) - 1, parseInt(parts[2], 10));
|
||||
d.setDate(d.getDate() + 7);
|
||||
const nextYear = d.getFullYear();
|
||||
const nextMonth = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const nextDay = String(d.getDate()).padStart(2, '0');
|
||||
targetDateStr = `${nextYear}-${nextMonth}-${nextDay}`;
|
||||
} else if (_planningState.viewMode === 'month') {
|
||||
mode = 'month';
|
||||
const parts = sourceDateStr.split('-');
|
||||
const d = new Date(parseInt(parts[0], 10), parseInt(parts[1], 10) - 1, 1);
|
||||
d.setMonth(d.getMonth() + 1);
|
||||
const nextYear = d.getFullYear();
|
||||
const nextMonth = String(d.getMonth() + 1).padStart(2, '0');
|
||||
targetDateStr = `${nextYear}-${nextMonth}-01`;
|
||||
} else {
|
||||
targetDateStr = getTargetDuplicateDate(sourceDateStr, _planningState.viewMode);
|
||||
}
|
||||
|
||||
if (!targetDateStr) return;
|
||||
|
||||
postJSON(
|
||||
'/contracts/teams/planning/duplicate/',
|
||||
{
|
||||
mode: 'day',
|
||||
mode: mode,
|
||||
source_date: sourceDateStr,
|
||||
target_date: targetDateStr,
|
||||
copy_absences: true,
|
||||
|
|
|
|||
|
|
@ -238,7 +238,8 @@ class TeamPlanningAjaxViewsTests(TestCase):
|
|||
self.assertEqual(response.status_code, 200)
|
||||
data = response.json()
|
||||
self.assertIn('days', data)
|
||||
self.assertEqual(len(data['days']), 7)
|
||||
self.assertEqual(len(data['days']), 5)
|
||||
self.assertEqual(data['days'][0]['col_type'], 'week')
|
||||
self.assertIn('teams', data)
|
||||
self.assertIn('members', data)
|
||||
|
||||
|
|
@ -607,9 +608,19 @@ class TeamPlanningAjaxViewsTests(TestCase):
|
|||
response = self.client.get(url, {'date': '2026-09-15', 'view_mode': 'month'})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = response.json()
|
||||
self.assertEqual(len(data['days']), 30) # Septembre compte 30 jours
|
||||
self.assertEqual(len(data['days']), 6) # Horizon glissant de 6 mois
|
||||
self.assertEqual(data['days'][0]['col_type'], 'month')
|
||||
self.assertEqual(data['days'][0]['date'], '2026-09-01')
|
||||
self.assertEqual(data['days'][-1]['date'], '2026-09-30')
|
||||
self.assertEqual(data['days'][-1]['date'], '2027-02-01')
|
||||
|
||||
def test_planning_data_ajax_work_week_view(self):
|
||||
"""Tester le retour du mode 'work_week' (5 jours ouvrés) dans team_planning_data_ajax."""
|
||||
url = reverse('contracts:team_planning_data_ajax')
|
||||
response = self.client.get(url, {'date': '2026-09-02', 'view_mode': 'work_week'})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = response.json()
|
||||
self.assertEqual(len(data['days']), 5)
|
||||
self.assertEqual(data['days'][0]['col_type'], 'day')
|
||||
|
||||
def test_planning_assign_member_ajax_week_scope(self):
|
||||
"""Tester l'assignation avec portée 'week' via l'API AJAX."""
|
||||
|
|
|
|||
|
|
@ -450,46 +450,98 @@ def team_planning_data_ajax(request):
|
|||
|
||||
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 == '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':
|
||||
_, days_in_month = calendar.monthrange(anchor_date.year, anchor_date.month)
|
||||
num_days = days_in_month
|
||||
# Horizon glissant de 6 mois
|
||||
start_date = date(anchor_date.year, anchor_date.month, 1)
|
||||
end_date = date(anchor_date.year, anchor_date.month, days_in_month)
|
||||
else: # 'week' (default)
|
||||
num_days = 7
|
||||
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=6)
|
||||
|
||||
today = timezone.now().date()
|
||||
|
||||
# Construire la liste des jours
|
||||
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']
|
||||
|
||||
days_list = []
|
||||
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,
|
||||
})
|
||||
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')
|
||||
|
|
@ -525,23 +577,61 @@ def team_planning_data_ajax(request):
|
|||
for t in teams:
|
||||
can_edit_this = user_can_edit_specific_team(request.user, t.id)
|
||||
days_dict = {}
|
||||
for i in range(num_days):
|
||||
d = start_date + timedelta(days=i)
|
||||
d_str = d.isoformat()
|
||||
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
|
||||
|
||||
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,
|
||||
|
|
@ -553,7 +643,15 @@ def team_planning_data_ajax(request):
|
|||
})
|
||||
|
||||
# 7. Structurer les membres pour le volet latéral (Sidebar)
|
||||
absent_member_ids = {mid for (mid, d) in absences.keys() if d == anchor_date}
|
||||
if view_mode == 'week':
|
||||
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}
|
||||
else:
|
||||
absent_member_ids = {mid for (mid, d) in absences.keys() if d == anchor_date}
|
||||
members_data = []
|
||||
for m in all_members:
|
||||
roles = []
|
||||
|
|
|
|||
Loading…
Reference in a new issue