feat: implement automatic duplication from Friday to following Monday in work week view and update associated tests

This commit is contained in:
kdeterme 2026-09-04 11:53:28 +02:00
parent 300feca9f1
commit 0ac214f459
5 changed files with 94 additions and 27 deletions

View file

@ -1645,6 +1645,10 @@ msgstr "Error loading schedule."
msgid "Dupliquer vers le jour suivant (avec absences)" msgid "Dupliquer vers le jour suivant (avec absences)"
msgstr "Duplicate to next day (with absences)" msgstr "Duplicate to next day (with absences)"
#: contracts/templates/contracts/teams_index.html:1568
msgid "Dupliquer vers le lundi suivant (avec absences)"
msgstr "Duplicate to following Monday (with absences)"
#: contracts/templates/contracts/teams_index.html:1558 #: contracts/templates/contracts/teams_index.html:1558
msgid "Options de la journée" msgid "Options de la journée"
msgstr "Day options" msgstr "Day options"

View file

@ -1683,6 +1683,10 @@ msgstr "Erreur lors du chargement de la planification."
msgid "Dupliquer vers le jour suivant (avec absences)" msgid "Dupliquer vers le jour suivant (avec absences)"
msgstr "Dupliquer vers le jour suivant (avec absences)" msgstr "Dupliquer vers le jour suivant (avec absences)"
#: contracts/templates/contracts/teams_index.html:1568
msgid "Dupliquer vers le lundi suivant (avec absences)"
msgstr "Dupliquer vers le lundi suivant (avec absences)"
#: contracts/templates/contracts/teams_index.html:1558 #: contracts/templates/contracts/teams_index.html:1558
msgid "Options de la journée" msgid "Options de la journée"
msgstr "Options de la journée" msgstr "Options de la journée"

View file

@ -1646,6 +1646,10 @@ msgstr "Fout bij het laden van de planning."
msgid "Dupliquer vers le jour suivant (avec absences)" msgid "Dupliquer vers le jour suivant (avec absences)"
msgstr "Kopiëren naar volgende dag (met afwezigheden)" msgstr "Kopiëren naar volgende dag (met afwezigheden)"
#: contracts/templates/contracts/teams_index.html:1568
msgid "Dupliquer vers le lundi suivant (avec absences)"
msgstr "Kopiëren naar volgende maandag (met afwezigheden)"
#: contracts/templates/contracts/teams_index.html:1558 #: contracts/templates/contracts/teams_index.html:1558
msgid "Options de la journée" msgid "Options de la journée"
msgstr "Opties voor de dag" msgstr "Opties voor de dag"

View file

@ -1470,6 +1470,13 @@ document.addEventListener('DOMContentLoaded', function() {
} }
}); });
$('#dupSourceDate').on('change', function() {
const targetDateStr = getTargetDuplicateDate($(this).val(), _planningState.viewMode);
if (targetDateStr) {
$('#dupTargetDate').val(targetDateStr);
}
});
$('#btn-open-duplicate-modal').on('click', function() { $('#btn-open-duplicate-modal').on('click', function() {
openDuplicateModal(); openDuplicateModal();
}); });
@ -1554,7 +1561,13 @@ 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(' ');
const quickDupTitle = "{% trans 'Dupliquer vers le jour suivant (avec absences)'|escapejs %}"; 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;
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 optionsTitle = "{% trans 'Options de la journée'|escapejs %}";
const dupCustomTitle = "{% trans 'Dupliquer cette journée…'|escapejs %}"; const dupCustomTitle = "{% trans 'Dupliquer cette journée…'|escapejs %}";
@ -1908,20 +1921,50 @@ function submitToggleAbsence(isAbsent) {
} }
// ─────────────────────────────────────────────── // ───────────────────────────────────────────────
// Modale Duplication // Modale Duplication & Duplication rapide
// ─────────────────────────────────────────────── // ───────────────────────────────────────────────
function getTargetDuplicateDate(sourceDateStr, viewMode) {
if (!sourceDateStr) return '';
const parts = sourceDateStr.split('-');
if (parts.length !== 3) return '';
const srcYear = parseInt(parts[0], 10);
const srcMonth = parseInt(parts[1], 10) - 1;
const srcDay = parseInt(parts[2], 10);
const d = new Date(srcYear, srcMonth, srcDay);
let daysToAdd = 1;
if (viewMode === 'work_week') {
const dayOfWeek = d.getDay(); // 0 = Dim, 1 = Lun, ..., 5 = Ven, 6 = Sam
if (dayOfWeek === 5) {
daysToAdd = 3; // Vendredi -> Lundi
} else if (dayOfWeek === 6) {
daysToAdd = 2; // Samedi -> Lundi
}
}
d.setDate(d.getDate() + daysToAdd);
const nextYear = d.getFullYear();
const nextMonth = String(d.getMonth() + 1).padStart(2, '0');
const nextDay = String(d.getDate()).padStart(2, '0');
return `${nextYear}-${nextMonth}-${nextDay}`;
}
function openDuplicateModal() { function openDuplicateModal() {
const todayStr = new Date().toISOString().split('T')[0]; const todayStr = new Date().toISOString().split('T')[0];
$('#dupSourceDate').val(_planningState.anchorDate || todayStr); const srcDateStr = _planningState.anchorDate || todayStr;
$('#dupSourceDate').val(srcDateStr);
const d = new Date(_planningState.anchorDate || todayStr); const targetDateStr = getTargetDuplicateDate(srcDateStr, _planningState.viewMode);
d.setDate(d.getDate() + 1); $('#dupTargetDate').val(targetDateStr);
$('#dupTargetDate').val(d.toISOString().split('T')[0]);
$('#dupSourceWeek').val(_planningState.anchorDate || todayStr); $('#dupSourceWeek').val(srcDateStr);
const dWeek = new Date(_planningState.anchorDate || todayStr); const parts = srcDateStr.split('-');
const dWeek = new Date(parseInt(parts[0], 10), parseInt(parts[1], 10) - 1, parseInt(parts[2], 10));
dWeek.setDate(dWeek.getDate() + 7); dWeek.setDate(dWeek.getDate() + 7);
$('#dupTargetWeek').val(dWeek.toISOString().split('T')[0]); const nextWeekYear = dWeek.getFullYear();
const nextWeekMonth = String(dWeek.getMonth() + 1).padStart(2, '0');
const nextWeekDay = String(dWeek.getDate()).padStart(2, '0');
$('#dupTargetWeek').val(`${nextWeekYear}-${nextWeekMonth}-${nextWeekDay}`);
$('#dupModalAlert').addClass('d-none'); $('#dupModalAlert').addClass('d-none');
new bootstrap.Modal(document.getElementById('duplicatePlanningModal')).show(); new bootstrap.Modal(document.getElementById('duplicatePlanningModal')).show();
@ -1930,23 +1973,13 @@ function openDuplicateModal() {
function openDuplicateModalForDay(dateStr) { function openDuplicateModalForDay(dateStr) {
openDuplicateModal(); openDuplicateModal();
$('#dupSourceDate').val(dateStr); $('#dupSourceDate').val(dateStr);
const d = new Date(dateStr); const targetDateStr = getTargetDuplicateDate(dateStr, _planningState.viewMode);
d.setDate(d.getDate() + 1); $('#dupTargetDate').val(targetDateStr);
$('#dupTargetDate').val(d.toISOString().split('T')[0]);
} }
function quickDuplicateToNextDay(sourceDateStr) { function quickDuplicateToNextDay(sourceDateStr) {
const parts = sourceDateStr.split('-'); const targetDateStr = getTargetDuplicateDate(sourceDateStr, _planningState.viewMode);
const srcYear = parseInt(parts[0], 10); if (!targetDateStr) return;
const srcMonth = parseInt(parts[1], 10) - 1;
const srcDay = parseInt(parts[2], 10);
const d = new Date(srcYear, srcMonth, srcDay);
d.setDate(d.getDate() + 1);
const nextYear = d.getFullYear();
const nextMonth = String(d.getMonth() + 1).padStart(2, '0');
const nextDay = String(d.getDate()).padStart(2, '0');
const targetDateStr = `${nextYear}-${nextMonth}-${nextDay}`;
postJSON( postJSON(
'/contracts/teams/planning/duplicate/', '/contracts/teams/planning/duplicate/',

View file

@ -146,24 +146,26 @@ class TeamPlanningAjaxViewsTests(TestCase):
self.assertIn('members', data) self.assertIn('members', data)
def test_planning_assign_member_ajax(self): def test_planning_assign_member_ajax(self):
target_date = timezone.now().date() + timedelta(days=2)
url = reverse('contracts:team_planning_assign_member_ajax') url = reverse('contracts:team_planning_assign_member_ajax')
payload = { payload = {
'member_id': self.member_alice.id, 'member_id': self.member_alice.id,
'team_id': self.team_b.id, 'team_id': self.team_b.id,
'date': '2026-09-03', 'date': target_date.isoformat(),
} }
response = self.client.post(url, data=json.dumps(payload), content_type='application/json') response = self.client.post(url, data=json.dumps(payload), content_type='application/json')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertTrue(response.json()['success']) self.assertTrue(response.json()['success'])
# Vérifier l'affectation # Vérifier l'affectation
self.assertEqual(get_member_team_for_date(self.member_alice, date(2026, 9, 3)), self.team_b) self.assertEqual(get_member_team_for_date(self.member_alice, target_date), self.team_b)
def test_planning_toggle_absence_ajax(self): def test_planning_toggle_absence_ajax(self):
target_date = timezone.now().date() + timedelta(days=2)
url = reverse('contracts:team_planning_toggle_absence_ajax') url = reverse('contracts:team_planning_toggle_absence_ajax')
payload = { payload = {
'member_id': self.member_alice.id, 'member_id': self.member_alice.id,
'date': '2026-09-04', 'date': target_date.isoformat(),
'is_absent': True, 'is_absent': True,
'reason': 'maladie', 'reason': 'maladie',
'comment': 'Rhume', 'comment': 'Rhume',
@ -171,7 +173,7 @@ class TeamPlanningAjaxViewsTests(TestCase):
response = self.client.post(url, data=json.dumps(payload), content_type='application/json') response = self.client.post(url, data=json.dumps(payload), content_type='application/json')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertTrue(response.json()['is_absent']) self.assertTrue(response.json()['is_absent'])
self.assertEqual(CompanyMemberAbsence.objects.filter(member=self.member_alice, date='2026-09-04').count(), 1) self.assertEqual(CompanyMemberAbsence.objects.filter(member=self.member_alice, date=target_date).count(), 1)
def test_planning_duplicate_ajax(self): def test_planning_duplicate_ajax(self):
# Assigner Alice le 07/09 # Assigner Alice le 07/09
@ -189,6 +191,26 @@ class TeamPlanningAjaxViewsTests(TestCase):
self.assertTrue(response.json()['success']) self.assertTrue(response.json()['success'])
self.assertEqual(get_member_team_for_date(self.member_alice, date(2026, 9, 8)), self.team_a) self.assertEqual(get_member_team_for_date(self.member_alice, date(2026, 9, 8)), self.team_a)
def test_planning_duplicate_friday_to_monday_ajax(self):
"""Vérifie la duplication d'un vendredi vers le lundi suivant via l'endpoint AJAX."""
friday = date(2026, 9, 11)
monday = date(2026, 9, 14)
set_member_team_schedule(self.member_alice, self.team_b.id, friday)
toggle_member_absence(self.member_alice, friday, is_absent=True, reason='conge', comment='Weekend prolonge')
url = reverse('contracts:team_planning_duplicate_ajax')
payload = {
'mode': 'day',
'source_date': friday.isoformat(),
'target_date': monday.isoformat(),
'copy_absences': True,
}
response = self.client.post(url, data=json.dumps(payload), content_type='application/json')
self.assertEqual(response.status_code, 200)
self.assertTrue(response.json()['success'])
self.assertEqual(get_member_team_for_date(self.member_alice, monday), self.team_b)
self.assertTrue(CompanyMemberAbsence.objects.filter(member=self.member_alice, date=monday).exists())
def test_teams_move_member_syncs_history(self): def test_teams_move_member_syncs_history(self):
url = reverse('contracts:teams_move_member', args=[self.member_alice.id]) url = reverse('contracts:teams_move_member', args=[self.member_alice.id])
payload = { payload = {