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>
|
<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">
|
<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-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-primary" data-mode="week">{% trans "Semaine" %}</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 "Mois" %}</button>
|
||||||
<button type="button" class="btn btn-outline-primary" data-mode="month">{% trans "1 Mois" %}</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -1720,12 +1719,30 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
loadPlanningData();
|
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').on('click', function() {
|
||||||
$('#planning-viewmode-group button').removeClass('btn-primary').addClass('btn-outline-primary');
|
$('#planning-viewmode-group button').removeClass('btn-primary').addClass('btn-outline-primary');
|
||||||
$(this).removeClass('btn-outline-primary').addClass('btn-primary');
|
$(this).removeClass('btn-outline-primary').addClass('btn-primary');
|
||||||
_planningState.viewMode = $(this).data('mode');
|
_planningState.viewMode = $(this).data('mode');
|
||||||
setStoredSetting('view_mode', _planningState.viewMode);
|
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();
|
loadPlanningData();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1828,11 +1845,16 @@ function navigatePlanningDate(direction) {
|
||||||
if (_planningState.viewMode === 'month') {
|
if (_planningState.viewMode === 'month') {
|
||||||
d.setDate(1);
|
d.setDate(1);
|
||||||
d.setMonth(d.getMonth() + direction);
|
d.setMonth(d.getMonth() + direction);
|
||||||
|
} else if (_planningState.viewMode === 'week') {
|
||||||
|
d.setDate(d.getDate() + (direction * 7));
|
||||||
} else {
|
} else {
|
||||||
const step = _planningState.viewMode === 'two_weeks' ? 14 : 7;
|
const step = _planningState.viewMode === 'two_weeks' ? 14 : 7;
|
||||||
d.setDate(d.getDate() + (direction * step));
|
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);
|
setStoredSetting('anchor_date', _planningState.anchorDate);
|
||||||
_planningState.flatpickrInstance.setDate(_planningState.anchorDate);
|
_planningState.flatpickrInstance.setDate(_planningState.anchorDate);
|
||||||
loadPlanningData();
|
loadPlanningData();
|
||||||
|
|
@ -1907,19 +1929,35 @@ function renderPlanningMatrix(data) {
|
||||||
if (day.is_past) headerClasses.push('is-past-header');
|
if (day.is_past) headerClasses.push('is-past-header');
|
||||||
th.className = headerClasses.join(' ');
|
th.className = headerClasses.join(' ');
|
||||||
|
|
||||||
|
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 %}";
|
||||||
|
|
||||||
|
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 dParts = day.date.split('-');
|
||||||
const dateObj = new Date(parseInt(dParts[0], 10), parseInt(dParts[1], 10) - 1, parseInt(dParts[2], 10));
|
const dateObj = new Date(parseInt(dParts[0], 10), parseInt(dParts[1], 10) - 1, parseInt(dParts[2], 10));
|
||||||
const isFriday = dateObj.getDay() === 5;
|
if (dateObj.getDay() === 5) {
|
||||||
|
quickDupTitle = "{% trans 'Dupliquer vers le lundi suivant (avec absences)'|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 %}";
|
|
||||||
|
|
||||||
let badgeHtml = '';
|
let badgeHtml = '';
|
||||||
if (day.is_today) {
|
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) {
|
} 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>`;
|
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>`;
|
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 titleAbsence = member.is_absent ? "{% trans "Modifier ou annuler l'absence"|escapejs %}" : "{% trans "Marquer absent"|escapejs %}";
|
||||||
const titleUnassign = "{% trans 'Désassigner de cette équipe'|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)}">
|
<div class="planning-member-info" title="${escapeHtml(member.name)}">
|
||||||
<span class="member-name-text fw-semibold">${escapeHtml(member.name)}</span>
|
<span class="member-name-text fw-semibold">${escapeHtml(member.name)}</span>
|
||||||
${multiTeamBadge}
|
${multiTeamBadge}
|
||||||
|
${durationBadge}
|
||||||
${absenceBadge}
|
${absenceBadge}
|
||||||
</div>
|
</div>
|
||||||
${actionsHtml}
|
${actionsHtml}
|
||||||
|
|
@ -2084,6 +2128,7 @@ function createPlanningMemberCard(member, teamId, dateStr, canEdit, isPast) {
|
||||||
let _pendingPlanningAssign = null;
|
let _pendingPlanningAssign = null;
|
||||||
|
|
||||||
function executePlanningAssign(memberId, targetTeamId, targetDate, action) {
|
function executePlanningAssign(memberId, targetTeamId, targetDate, action) {
|
||||||
|
const scope = _planningState.assignScope || (_planningState.viewMode === 'month' ? 'month' : (_planningState.viewMode === 'week' ? 'week' : 'day'));
|
||||||
postJSON(
|
postJSON(
|
||||||
'/contracts/teams/planning/assign/',
|
'/contracts/teams/planning/assign/',
|
||||||
{
|
{
|
||||||
|
|
@ -2091,7 +2136,7 @@ function executePlanningAssign(memberId, targetTeamId, targetDate, action) {
|
||||||
team_id: parseInt(targetTeamId),
|
team_id: parseInt(targetTeamId),
|
||||||
date: targetDate,
|
date: targetDate,
|
||||||
action: action || 'move',
|
action: action || 'move',
|
||||||
scope: _planningState.assignScope || 'day',
|
scope: scope,
|
||||||
},
|
},
|
||||||
function(resp) {
|
function(resp) {
|
||||||
loadPlanningData();
|
loadPlanningData();
|
||||||
|
|
@ -2279,6 +2324,7 @@ function filterSidebarMembers() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function unassignMemberFromDate(memberId, dateStr, teamId) {
|
function unassignMemberFromDate(memberId, dateStr, teamId) {
|
||||||
|
const scope = _planningState.assignScope || (_planningState.viewMode === 'month' ? 'month' : (_planningState.viewMode === 'week' ? 'week' : 'day'));
|
||||||
postJSON(
|
postJSON(
|
||||||
'/contracts/teams/planning/assign/',
|
'/contracts/teams/planning/assign/',
|
||||||
{
|
{
|
||||||
|
|
@ -2286,7 +2332,7 @@ function unassignMemberFromDate(memberId, dateStr, teamId) {
|
||||||
team_id: null,
|
team_id: null,
|
||||||
date: dateStr,
|
date: dateStr,
|
||||||
unassign_team_id: teamId ? parseInt(teamId) : null,
|
unassign_team_id: teamId ? parseInt(teamId) : null,
|
||||||
scope: _planningState.assignScope || 'day',
|
scope: scope,
|
||||||
},
|
},
|
||||||
function(resp) {
|
function(resp) {
|
||||||
loadPlanningData();
|
loadPlanningData();
|
||||||
|
|
@ -2404,19 +2450,64 @@ function openDuplicateModal() {
|
||||||
|
|
||||||
function openDuplicateModalForDay(dateStr) {
|
function openDuplicateModalForDay(dateStr) {
|
||||||
openDuplicateModal();
|
openDuplicateModal();
|
||||||
|
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);
|
$('#dupSourceDate').val(dateStr);
|
||||||
const targetDateStr = getTargetDuplicateDate(dateStr, _planningState.viewMode);
|
const targetDateStr = getTargetDuplicateDate(dateStr, _planningState.viewMode);
|
||||||
$('#dupTargetDate').val(targetDateStr);
|
$('#dupTargetDate').val(targetDateStr);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function quickDuplicateToNextDay(sourceDateStr) {
|
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;
|
if (!targetDateStr) return;
|
||||||
|
|
||||||
postJSON(
|
postJSON(
|
||||||
'/contracts/teams/planning/duplicate/',
|
'/contracts/teams/planning/duplicate/',
|
||||||
{
|
{
|
||||||
mode: 'day',
|
mode: mode,
|
||||||
source_date: sourceDateStr,
|
source_date: sourceDateStr,
|
||||||
target_date: targetDateStr,
|
target_date: targetDateStr,
|
||||||
copy_absences: true,
|
copy_absences: true,
|
||||||
|
|
|
||||||
|
|
@ -238,7 +238,8 @@ class TeamPlanningAjaxViewsTests(TestCase):
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
data = response.json()
|
data = response.json()
|
||||||
self.assertIn('days', data)
|
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('teams', data)
|
||||||
self.assertIn('members', data)
|
self.assertIn('members', data)
|
||||||
|
|
||||||
|
|
@ -607,9 +608,19 @@ class TeamPlanningAjaxViewsTests(TestCase):
|
||||||
response = self.client.get(url, {'date': '2026-09-15', 'view_mode': 'month'})
|
response = self.client.get(url, {'date': '2026-09-15', 'view_mode': 'month'})
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
data = response.json()
|
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'][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):
|
def test_planning_assign_member_ajax_week_scope(self):
|
||||||
"""Tester l'assignation avec portée 'week' via l'API AJAX."""
|
"""Tester l'assignation avec portée 'week' via l'API AJAX."""
|
||||||
|
|
|
||||||
|
|
@ -450,33 +450,37 @@ def team_planning_data_ajax(request):
|
||||||
|
|
||||||
view_mode = request.GET.get('view_mode', 'week')
|
view_mode = request.GET.get('view_mode', 'week')
|
||||||
monday = anchor_date - timedelta(days=anchor_date.weekday())
|
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':
|
if view_mode == 'work_week':
|
||||||
num_days = 5
|
num_days = 5
|
||||||
start_date = monday
|
start_date = monday
|
||||||
end_date = monday + timedelta(days=4)
|
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':
|
elif view_mode == 'two_weeks':
|
||||||
num_days = 14
|
num_days = 14
|
||||||
start_date = monday
|
start_date = monday
|
||||||
end_date = monday + timedelta(days=13)
|
end_date = monday + timedelta(days=13)
|
||||||
elif view_mode == 'month':
|
|
||||||
_, days_in_month = calendar.monthrange(anchor_date.year, anchor_date.month)
|
|
||||||
num_days = days_in_month
|
|
||||||
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
|
|
||||||
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):
|
for i in range(num_days):
|
||||||
d = start_date + timedelta(days=i)
|
d = start_date + timedelta(days=i)
|
||||||
w_idx = d.weekday()
|
w_idx = d.weekday()
|
||||||
|
|
@ -489,6 +493,54 @@ def team_planning_data_ajax(request):
|
||||||
'is_today': d == today,
|
'is_today': d == today,
|
||||||
'is_past': d < today,
|
'is_past': d < today,
|
||||||
'is_weekend': w_idx >= 5,
|
'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
|
# 2. Récupérer les équipes autorisées
|
||||||
|
|
@ -525,9 +577,47 @@ def team_planning_data_ajax(request):
|
||||||
for t in teams:
|
for t in teams:
|
||||||
can_edit_this = user_can_edit_specific_team(request.user, t.id)
|
can_edit_this = user_can_edit_specific_team(request.user, t.id)
|
||||||
days_dict = {}
|
days_dict = {}
|
||||||
for i in range(num_days):
|
|
||||||
d = start_date + timedelta(days=i)
|
if view_mode in ('week', 'month'):
|
||||||
d_str = d.isoformat()
|
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 = []
|
members_on_day = []
|
||||||
for m in compositions.get((t.id, d), []):
|
for m in compositions.get((t.id, d), []):
|
||||||
m_id = m['id']
|
m_id = m['id']
|
||||||
|
|
@ -553,6 +643,14 @@ def team_planning_data_ajax(request):
|
||||||
})
|
})
|
||||||
|
|
||||||
# 7. Structurer les membres pour le volet latéral (Sidebar)
|
# 7. Structurer les membres pour le volet latéral (Sidebar)
|
||||||
|
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}
|
absent_member_ids = {mid for (mid, d) in absences.keys() if d == anchor_date}
|
||||||
members_data = []
|
members_data = []
|
||||||
for m in all_members:
|
for m in all_members:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue