feat: implement automatic duplication from Friday to following Monday in work week view and update associated tests
This commit is contained in:
parent
300feca9f1
commit
0ac214f459
5 changed files with 94 additions and 27 deletions
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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/',
|
||||||
|
|
|
||||||
|
|
@ -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 = {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue