diff --git a/loko/interventions/static/interventions/calendar/interventions_calendar.css b/loko/interventions/static/interventions/calendar/interventions_calendar.css
index e273468..4ca66c8 100644
--- a/loko/interventions/static/interventions/calendar/interventions_calendar.css
+++ b/loko/interventions/static/interventions/calendar/interventions_calendar.css
@@ -938,19 +938,86 @@
line-height: 1.35;
border-left: 3px solid #cbd5e1;
box-shadow: 0 1px 2px rgba(0,0,0,0.03);
- cursor: pointer;
transition: background-color 0.15s ease, box-shadow 0.15s ease;
}
+.team-members-list.clickable-team-members {
+ cursor: pointer;
+}
+
+.team-members-list:hover {
+ background: #f1f5f9;
+ box-shadow: 0 2px 5px rgba(0,0,0,0.08);
+}
+
.team-members-list strong,
.team-members-list .member-name {
font-weight: 700;
color: #0f172a;
}
-.team-members-list:hover {
- background: #f1f5f9;
- box-shadow: 0 2px 5px rgba(0,0,0,0.08);
+/* Member chips in Teams Daily View */
+.team-daily-members-chips {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 4px;
+ margin-top: 4px;
+}
+
+.team-daily-member-chip {
+ display: inline-flex;
+ align-items: center;
+ background-color: #ffffff;
+ border: 1px solid #cbd5e1;
+ border-radius: 4px;
+ padding: 2px 6px;
+ font-size: 0.76rem;
+ font-weight: 600;
+ color: #1e293b;
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
+ transition: all 0.15s ease;
+ user-select: none;
+ -webkit-user-select: none;
+ max-width: 100%;
+}
+
+.team-daily-member-chip.draggable {
+ cursor: grab;
+}
+
+.team-daily-member-chip.draggable:hover {
+ background-color: #f8fafc;
+ border-color: #0d6efd;
+ box-shadow: 0 2px 6px rgba(13, 110, 253, 0.2);
+ transform: translateY(-1px);
+}
+
+.team-daily-member-chip.draggable:active {
+ cursor: grabbing;
+}
+
+.team-daily-member-chip.is-dragging {
+ opacity: 0.45;
+ border: 1px dashed #0d6efd;
+ background-color: #e7f1ff;
+ box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15);
+}
+
+.team-daily-member-chip.is-absent {
+ background-color: #fff5f5;
+ border-color: #f8d7da;
+}
+
+.team-daily-member-chip.is-absent .member-name-text {
+ text-decoration: line-through;
+ color: #dc3545;
+ opacity: 0.8;
+}
+
+.team-daily-member-chip .member-name-text {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
}
/* Stack of interventions */
diff --git a/loko/interventions/static/interventions/calendar/interventions_calendar.js b/loko/interventions/static/interventions/calendar/interventions_calendar.js
index d9dfcb1..a970e18 100644
--- a/loko/interventions/static/interventions/calendar/interventions_calendar.js
+++ b/loko/interventions/static/interventions/calendar/interventions_calendar.js
@@ -294,6 +294,10 @@ document.addEventListener('DOMContentLoaded', function () {
let currentModalMemberId = null;
let draggedBacklogItem = null;
let draggedBacklogGroup = null;
+ let _draggedTeamMember = null;
+ let _memberDragJustEnded = false;
+ let _pendingMemberDailyAssign = null;
+ let _lastTeamsDailyData = null;
const csrfToken = getCookie('csrftoken');
let suppressDurationInputEvent = false;
let suppressEndInputEvent = false;
@@ -451,6 +455,141 @@ document.addEventListener('DOMContentLoaded', function () {
return TEAM_PALETTE[Math.abs(teamId || 0) % TEAM_PALETTE.length];
}
+ function buildTeamDailyMembersHtml(members, teamId, dateStr, teamName, canEditThisTeam, isPastDay) {
+ const teamColor = typeof getTeamColor === 'function' ? getTeamColor(teamId) : '#0d6efd';
+ const isMemberHistoryAllowed = typeof canViewMemberHistory !== 'undefined' ? Boolean(canViewMemberHistory) : false;
+ const titleTooltip = canEditThisTeam && !isPastDay ? gettext('Modifier la composition de l’équipe pour ce jour') : gettext('Composition de l’équipe');
+ const membersList = (members && Array.isArray(members)) ? members : [];
+
+ if (membersList.length > 0) {
+ const chipsHtml = membersList.map(m => {
+ const name = typeof m === 'object' && m ? m.name : m;
+ const id = typeof m === 'object' && m ? m.id : null;
+ const isAbsent = typeof m === 'object' && m ? Boolean(m.is_absent) : false;
+ const absenceReason = typeof m === 'object' && m ? (m.absence_reason || '') : '';
+ const isMultiTeam = typeof m === 'object' && m ? Boolean(m.is_multi_team) : false;
+ const otherTeams = typeof m === 'object' && m && m.other_teams ? m.other_teams : [];
+
+ const canDrag = Boolean(canEditThisTeam && !isPastDay && id);
+ let chipTitle = escapeHtml(name);
+ if (canDrag) {
+ chipTitle += ` — ${gettext('Glisser pour réassigner. Cliquer pour gérer l’équipe.')}`;
+ }
+
+ let multiTeamHtml = '';
+ if (isMultiTeam) {
+ const otherNames = otherTeams.map(t => t.name).join(', ');
+ const tooltip = otherNames ? `${gettext('Planifié aussi dans')} : ${otherNames}` : gettext('Planifié dans plusieurs équipes ce jour');
+ multiTeamHtml = ` `;
+ }
+
+ let absentBadgeHtml = '';
+ if (isAbsent) {
+ absentBadgeHtml = ` ${escapeHtml(absenceReason || gettext('Absent'))}`;
+ }
+
+ let historyLinkHtml = '';
+ if (isMemberHistoryAllowed && id) {
+ historyLinkHtml = ` `;
+ }
+
+ return `
+ ${escapeHtml(name)}${multiTeamHtml}${absentBadgeHtml}${historyLinkHtml}
+
`;
+ }).join('');
+
+ return `
+
+
+
+ ${gettext('Équipe')} (${membersList.length})
+
+ ${canEditThisTeam && !isPastDay ? `
+
+ ` : ''}
+
+
+ ${chipsHtml}
+
+
`;
+ } else if (teamId !== null && teamId !== '' && typeof teamId !== 'undefined') {
+ return `
+ (${gettext('Aucun membre')})
+ ${canEditThisTeam && !isPastDay ? `` : ''}
+
`;
+ }
+ return '';
+ }
+
+ function initTeamDailyMemberChips(container) {
+ const root = container || document.getElementById('teams-daily-table-body');
+ if (!root) return;
+
+ root.querySelectorAll('.team-daily-member-chip[draggable="true"]').forEach(chip => {
+ chip.addEventListener('dragstart', (e) => {
+ const memberId = parseInt(chip.getAttribute('data-member-id'));
+ const memberName = chip.getAttribute('data-member-name') || '';
+ const sourceTeamId = chip.getAttribute('data-team-id') ? parseInt(chip.getAttribute('data-team-id')) : null;
+ const sourceDate = chip.getAttribute('data-date');
+
+ _draggedTeamMember = {
+ id: memberId,
+ name: memberName,
+ sourceTeamId: sourceTeamId,
+ sourceDate: sourceDate
+ };
+
+ chip.classList.add('is-dragging');
+ e.dataTransfer.effectAllowed = 'move';
+ e.dataTransfer.setData('text/plain', String(memberId));
+ });
+
+ chip.addEventListener('dragend', () => {
+ chip.classList.remove('is-dragging');
+ _draggedTeamMember = null;
+ _memberDragJustEnded = true;
+ setTimeout(() => {
+ _memberDragJustEnded = false;
+ }, 200);
+ });
+ });
+ }
+
+ function showTeamDailyMultiTeamConfirmModal(member, targetTeamId, targetDate, otherAssignedTeams) {
+ const modalEl = document.getElementById('teamDailyMultiTeamConfirmModal');
+ if (!modalEl) {
+ if (confirm(gettext("Ce membre est déjà affecté à une autre équipe ce jour-là. Voulez-vous le déplacer vers cette équipe ?"))) {
+ assignMemberToDailyTeam(member.id, targetTeamId, targetDate, 'move');
+ }
+ return;
+ }
+
+ const otherNames = otherAssignedTeams.map(t => t.name).join(', ');
+ const targetTeamObj = (_lastTeamsDailyData?.teams || []).find(t => t.id === targetTeamId);
+ const targetName = targetTeamObj ? targetTeamObj.name : `Équipe #${targetTeamId}`;
+
+ const memberNameEl = document.getElementById('team-daily-multi-member-name');
+ const dateEl = document.getElementById('team-daily-multi-date');
+ const existingTeamsEl = document.getElementById('team-daily-multi-existing-teams');
+ const targetTeamEl = document.getElementById('team-daily-multi-target-team');
+
+ if (memberNameEl) memberNameEl.textContent = member.name;
+ if (dateEl) dateEl.textContent = targetDate;
+ if (existingTeamsEl) existingTeamsEl.textContent = otherNames;
+ if (targetTeamEl) targetTeamEl.textContent = targetName;
+
+ _pendingMemberDailyAssign = {
+ memberId: member.id,
+ targetTeamId: targetTeamId,
+ targetDate: targetDate
+ };
+
+ const modal = bootstrap.Modal.getOrCreateInstance(modalEl);
+ modal.show();
+ }
+
function renderTeamsDailyGrid(data) {
const todayStr = formatDateISO(new Date());
const headerRow = document.getElementById('teams-daily-table-header');
@@ -459,6 +598,7 @@ document.addEventListener('DOMContentLoaded', function () {
const datepicker = document.getElementById('teams-daily-datepicker');
if (!headerRow || !tbody || !data) return;
+ _lastTeamsDailyData = data;
const startDate = new Date(data.start_date);
teamsDailyActiveDate = startDate;
@@ -550,40 +690,7 @@ document.addEventListener('DOMContentLoaded', function () {
const dayData = team.days[day.date] || { members: [], interventions: [] };
- let membersHTML = '';
- const isPastDay = day.date < todayStr;
- const isMemberHistoryAllowed = typeof canViewMemberHistory !== 'undefined' ? Boolean(canViewMemberHistory) : false;
- const canEditThisTeam = (typeof canEditTeams !== 'undefined' && Boolean(canEditTeams)) && (team.can_edit !== false);
- const titleTooltip = canEditThisTeam && !isPastDay ? gettext('Modifier la composition de l’équipe pour ce jour') : gettext('Composition de l’équipe');
-
- if (dayData.members && dayData.members.length > 0) {
- const formattedMembers = dayData.members.map(m => {
- const name = typeof m === 'object' && m ? m.name : m;
- const id = typeof m === 'object' && m ? m.id : null;
- const isAbsent = typeof m === 'object' && m ? Boolean(m.is_absent) : false;
- const absenceReason = typeof m === 'object' && m ? (m.absence_reason || '') : '';
- const isMultiTeam = typeof m === 'object' && m ? Boolean(m.is_multi_team) : false;
- const otherTeams = typeof m === 'object' && m && m.other_teams ? m.other_teams : [];
-
- let memberNameHtml = `${escapeHtml(name)}`;
- if (isMultiTeam) {
- const otherNames = otherTeams.map(t => t.name).join(', ');
- const tooltip = otherNames ? `${gettext('Planifié aussi dans')} : ${otherNames}` : gettext('Planifié dans plusieurs équipes ce jour');
- memberNameHtml += ` `;
- }
- if (isAbsent) {
- return `${memberNameHtml} ${escapeHtml(absenceReason || gettext('Absent'))}`;
- }
- return memberNameHtml;
- }).join(', ');
- membersHTML = `
- ${formattedMembers}
-
`;
- } else if (team.id !== null) {
- membersHTML = `
- (${gettext('Aucun membre')})
-
`;
- }
+ const membersHTML = buildTeamDailyMembersHtml(dayData.members, team.id, day.date, team.name, canEditThisTeam, isPastDay);
let itvsHTML = '';
if (dayData.interventions && dayData.interventions.length > 0) {
@@ -696,7 +803,7 @@ document.addEventListener('DOMContentLoaded', function () {
if (isPastCell) return;
cell.addEventListener('dragover', (e) => {
- if (draggedBacklogItem || draggedBacklogGroup) {
+ if (draggedBacklogItem || draggedBacklogGroup || _draggedTeamMember) {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
cell.classList.add('teams-daily-grid-cell--highlight');
@@ -709,8 +816,55 @@ document.addEventListener('DOMContentLoaded', function () {
cell.addEventListener('drop', (e) => {
cell.classList.remove('teams-daily-grid-cell--highlight');
- if (!draggedBacklogItem && !draggedBacklogGroup) return;
+ if (!draggedBacklogItem && !draggedBacklogGroup && !_draggedTeamMember) return;
e.preventDefault();
+
+ if (_draggedTeamMember) {
+ const member = _draggedTeamMember;
+ _draggedTeamMember = null;
+
+ const targetDate = dateStr;
+ const targetTeamId = teamId ? parseInt(teamId) : null;
+
+ if (!member.id || !targetDate || !targetTeamId) return;
+
+ // Si même équipe et même date, rien à faire
+ if (member.sourceTeamId === targetTeamId && member.sourceDate === targetDate) {
+ return;
+ }
+
+ // Vérifier si le membre est déjà dans l'équipe cible ce jour-là
+ let alreadyInTargetTeam = false;
+ const otherAssignedTeams = [];
+ if (_lastTeamsDailyData && _lastTeamsDailyData.teams) {
+ _lastTeamsDailyData.teams.forEach(t => {
+ if (t.days && t.days[targetDate] && t.days[targetDate].members) {
+ const found = t.days[targetDate].members.some(m => {
+ const mId = typeof m === 'object' && m ? m.id : null;
+ return mId === member.id;
+ });
+ if (found) {
+ if (t.id === targetTeamId) {
+ alreadyInTargetTeam = true;
+ } else {
+ otherAssignedTeams.push(t);
+ }
+ }
+ }
+ });
+ }
+
+ if (alreadyInTargetTeam) {
+ return;
+ }
+
+ if (otherAssignedTeams.length > 0) {
+ showTeamDailyMultiTeamConfirmModal(member, targetTeamId, targetDate, otherAssignedTeams);
+ } else {
+ assignMemberToDailyTeam(member.id, targetTeamId, targetDate, 'move');
+ }
+ return;
+ }
const start = new Date(dateStr + 'T08:00:00');
@@ -741,6 +895,8 @@ document.addEventListener('DOMContentLoaded', function () {
});
});
+ initTeamDailyMemberChips(tbody);
+
setTimeout(scrollToTodayColumn, 100);
}
@@ -5412,55 +5568,27 @@ function updateTeamsDailyGridCell(teamId, dateStr, currentMembers, teamObj) {
const cell = document.querySelector(`.teams-daily-grid-cell[data-team-id="${teamId}"][data-date="${dateStr}"]`);
if (!cell) return;
- let membersContainer = cell.querySelector('.team-members-list');
- const teamColor = typeof getTeamColor === 'function' ? getTeamColor(teamId) : (window.getTeamColor ? window.getTeamColor(teamId) : '#0d6efd');
const canEditThisTeam = (typeof canEditTeams !== 'undefined' && Boolean(canEditTeams)) && (!teamObj || teamObj.can_edit !== false);
const todayStr = new Date().toISOString().split('T')[0];
const isPastDay = dateStr < todayStr;
- const titleTooltip = canEditThisTeam && !isPastDay ? gettext('Modifier la composition de l’équipe pour ce jour') : gettext('Composition de l’équipe');
const teamName = teamObj && teamObj.name ? teamObj.name : '';
- if (!membersContainer) {
- membersContainer = document.createElement('div');
- cell.prepend(membersContainer);
+ const membersContainer = cell.querySelector('.team-members-list');
+ const newHtml = buildTeamDailyMembersHtml(currentMembers, teamId, dateStr, teamName, canEditThisTeam, isPastDay);
+
+ if (membersContainer) {
+ membersContainer.outerHTML = newHtml;
+ } else if (newHtml) {
+ cell.insertAdjacentHTML('afterbegin', newHtml);
}
+ initTeamDailyMemberChips(cell);
- membersContainer.setAttribute('data-team-id', teamId);
- membersContainer.setAttribute('data-date', dateStr);
- if (teamName) {
- membersContainer.setAttribute('data-team-name', teamName);
- }
- membersContainer.setAttribute('title', titleTooltip);
-
- if (currentMembers && currentMembers.length > 0) {
- const formattedMembers = currentMembers.map(m => {
- const name = typeof m === 'object' && m ? m.name : m;
- const isAbsent = typeof m === 'object' && m ? Boolean(m.is_absent) : false;
- const absenceReason = typeof m === 'object' && m ? (m.absence_reason || '') : '';
- const isMultiTeam = typeof m === 'object' && m ? Boolean(m.is_multi_team) : false;
- const otherTeams = typeof m === 'object' && m && m.other_teams ? m.other_teams : [];
-
- let memberNameHtml = `${escapeHtml(name)}`;
- if (isMultiTeam) {
- const otherNames = otherTeams.map(t => t.name).join(', ');
- const tooltip = otherNames ? `${gettext('Planifié aussi dans')} : ${otherNames}` : gettext('Planifié dans plusieurs équipes ce jour');
- memberNameHtml += ` `;
- }
- if (isAbsent) {
- return `${memberNameHtml} ${escapeHtml(absenceReason || gettext('Absent'))}`;
- }
- return memberNameHtml;
- }).join(', ');
-
- membersContainer.className = 'team-members-list clickable-team-members small mb-2';
- membersContainer.style.borderLeft = `3px solid ${teamColor}`;
- membersContainer.style.cursor = 'pointer';
- membersContainer.innerHTML = `${formattedMembers}`;
- } else {
- membersContainer.className = 'team-members-list clickable-team-members text-muted small fst-italic mb-2 text-opacity-50';
- membersContainer.style.borderLeft = '3px solid #cbd5e1';
- membersContainer.style.cursor = 'pointer';
- membersContainer.innerHTML = `(${gettext('Aucun membre')})`;
+ // Mettre à jour les données en cache
+ if (_lastTeamsDailyData && _lastTeamsDailyData.teams) {
+ const teamInCache = _lastTeamsDailyData.teams.find(t => t.id === parseInt(teamId));
+ if (teamInCache && teamInCache.days && teamInCache.days[dateStr]) {
+ teamInCache.days[dateStr].members = currentMembers || [];
+ }
}
}
@@ -5670,6 +5798,34 @@ function assignMemberToDailyTeam(memberId, teamId, dateStr, action) {
}
document.addEventListener('click', function(e) {
+ const btnMultiMove = e.target.closest('#btn-team-daily-multi-move');
+ if (btnMultiMove && _pendingMemberDailyAssign) {
+ e.preventDefault();
+ const { memberId, targetTeamId, targetDate } = _pendingMemberDailyAssign;
+ _pendingMemberDailyAssign = null;
+ const modalEl = document.getElementById('teamDailyMultiTeamConfirmModal');
+ if (modalEl) {
+ const modal = bootstrap.Modal.getInstance(modalEl);
+ if (modal) modal.hide();
+ }
+ assignMemberToDailyTeam(memberId, targetTeamId, targetDate, 'move');
+ return;
+ }
+
+ const btnMultiAdd = e.target.closest('#btn-team-daily-multi-add');
+ if (btnMultiAdd && _pendingMemberDailyAssign) {
+ e.preventDefault();
+ const { memberId, targetTeamId, targetDate } = _pendingMemberDailyAssign;
+ _pendingMemberDailyAssign = null;
+ const modalEl = document.getElementById('teamDailyMultiTeamConfirmModal');
+ if (modalEl) {
+ const modal = bootstrap.Modal.getInstance(modalEl);
+ if (modal) modal.hide();
+ }
+ assignMemberToDailyTeam(memberId, targetTeamId, targetDate, 'add');
+ return;
+ }
+
const btnAdd = e.target.closest('#btn-team-daily-add-member');
if (btnAdd) {
e.preventDefault();
@@ -5696,6 +5852,19 @@ document.addEventListener('click', function(e) {
const teamMembersList = e.target.closest('.clickable-team-members, .team-members-list');
if (teamMembersList) {
+ if (_memberDragJustEnded) return;
+
+ const historyIcon = e.target.closest('.member-history-icon');
+ if (historyIcon) {
+ e.preventDefault();
+ e.stopPropagation();
+ const mId = historyIcon.getAttribute('data-member-id');
+ if (mId) {
+ openMemberHistoryModal(mId);
+ }
+ return;
+ }
+
if (e.target.closest('.member-history-link, .btn-team-daily-remove-member, #btn-team-daily-add-member')) return;
const teamId = teamMembersList.getAttribute('data-team-id');
@@ -5709,6 +5878,13 @@ document.addEventListener('click', function(e) {
}
});
+const _multiTeamModalEl = document.getElementById('teamDailyMultiTeamConfirmModal');
+if (_multiTeamModalEl) {
+ _multiTeamModalEl.addEventListener('hidden.bs.modal', function () {
+ _pendingMemberDailyAssign = null;
+ });
+}
+
const _memberHistoryModalEl = document.getElementById('memberHistoryModal');
if (_memberHistoryModalEl) {
function ensureHistoryModalOnTop() {
diff --git a/loko/interventions/templates/interventions/interventions_calendar.html b/loko/interventions/templates/interventions/interventions_calendar.html
index 1457739..489ad1c 100644
--- a/loko/interventions/templates/interventions/interventions_calendar.html
+++ b/loko/interventions/templates/interventions/interventions_calendar.html
@@ -745,6 +745,44 @@
+
+
+
+
+
+
+
+ {% translate "est déjà planifié(e) le" %} {% translate "dans l'équipe" %} :
+ .
+
+
+ {% translate "Vous tentez de l'assigner à l'équipe" %} : .
+ {% translate "Souhaitez-vous déplacer ce membre ou l'affecter aux deux équipes ?" %}
+
+
+
+
+
+
+