diff --git a/loko/contracts/locale/en/LC_MESSAGES/django.po b/loko/contracts/locale/en/LC_MESSAGES/django.po index eda1a84..669c35a 100644 --- a/loko/contracts/locale/en/LC_MESSAGES/django.po +++ b/loko/contracts/locale/en/LC_MESSAGES/django.po @@ -1645,6 +1645,10 @@ msgstr "Error loading schedule." msgid "Dupliquer vers le jour suivant (avec 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 msgid "Options de la journée" msgstr "Day options" diff --git a/loko/contracts/locale/fr/LC_MESSAGES/django.po b/loko/contracts/locale/fr/LC_MESSAGES/django.po index 38ea8a6..ac2b647 100644 --- a/loko/contracts/locale/fr/LC_MESSAGES/django.po +++ b/loko/contracts/locale/fr/LC_MESSAGES/django.po @@ -1683,6 +1683,10 @@ msgstr "Erreur lors du chargement de la planification." msgid "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 msgid "Options de la journée" msgstr "Options de la journée" diff --git a/loko/contracts/locale/nl/LC_MESSAGES/django.po b/loko/contracts/locale/nl/LC_MESSAGES/django.po index 55901a8..24a6569 100644 --- a/loko/contracts/locale/nl/LC_MESSAGES/django.po +++ b/loko/contracts/locale/nl/LC_MESSAGES/django.po @@ -1646,6 +1646,10 @@ msgstr "Fout bij het laden van de planning." msgid "Dupliquer vers le jour suivant (avec absences)" 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 msgid "Options de la journée" msgstr "Opties voor de dag" diff --git a/loko/contracts/templates/contracts/teams_index.html b/loko/contracts/templates/contracts/teams_index.html index 74d6164..c93478b 100644 --- a/loko/contracts/templates/contracts/teams_index.html +++ b/loko/contracts/templates/contracts/teams_index.html @@ -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() { openDuplicateModal(); }); @@ -1554,7 +1561,13 @@ function renderPlanningMatrix(data) { if (day.is_past) headerClasses.push('is-past-header'); 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 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() { 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); - d.setDate(d.getDate() + 1); - $('#dupTargetDate').val(d.toISOString().split('T')[0]); + const targetDateStr = getTargetDuplicateDate(srcDateStr, _planningState.viewMode); + $('#dupTargetDate').val(targetDateStr); - $('#dupSourceWeek').val(_planningState.anchorDate || todayStr); - const dWeek = new Date(_planningState.anchorDate || todayStr); + $('#dupSourceWeek').val(srcDateStr); + 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); - $('#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'); new bootstrap.Modal(document.getElementById('duplicatePlanningModal')).show(); @@ -1930,23 +1973,13 @@ function openDuplicateModal() { function openDuplicateModalForDay(dateStr) { openDuplicateModal(); $('#dupSourceDate').val(dateStr); - const d = new Date(dateStr); - d.setDate(d.getDate() + 1); - $('#dupTargetDate').val(d.toISOString().split('T')[0]); + const targetDateStr = getTargetDuplicateDate(dateStr, _planningState.viewMode); + $('#dupTargetDate').val(targetDateStr); } function quickDuplicateToNextDay(sourceDateStr) { - const parts = sourceDateStr.split('-'); - 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); - 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}`; + const targetDateStr = getTargetDuplicateDate(sourceDateStr, _planningState.viewMode); + if (!targetDateStr) return; postJSON( '/contracts/teams/planning/duplicate/', diff --git a/loko/contracts/test_planning.py b/loko/contracts/test_planning.py index c09a1f8..9ed0a8d 100644 --- a/loko/contracts/test_planning.py +++ b/loko/contracts/test_planning.py @@ -146,24 +146,26 @@ class TeamPlanningAjaxViewsTests(TestCase): self.assertIn('members', data) def test_planning_assign_member_ajax(self): + target_date = timezone.now().date() + timedelta(days=2) url = reverse('contracts:team_planning_assign_member_ajax') payload = { 'member_id': self.member_alice.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') self.assertEqual(response.status_code, 200) self.assertTrue(response.json()['success']) # 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): + target_date = timezone.now().date() + timedelta(days=2) url = reverse('contracts:team_planning_toggle_absence_ajax') payload = { 'member_id': self.member_alice.id, - 'date': '2026-09-04', + 'date': target_date.isoformat(), 'is_absent': True, 'reason': 'maladie', 'comment': 'Rhume', @@ -171,7 +173,7 @@ class TeamPlanningAjaxViewsTests(TestCase): response = self.client.post(url, data=json.dumps(payload), content_type='application/json') self.assertEqual(response.status_code, 200) 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): # Assigner Alice le 07/09 @@ -189,6 +191,26 @@ class TeamPlanningAjaxViewsTests(TestCase): self.assertTrue(response.json()['success']) 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): url = reverse('contracts:teams_move_member', args=[self.member_alice.id]) payload = {