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)"
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"

View file

@ -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"

View file

@ -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"

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() {
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/',

View file

@ -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 = {