refactor: update intervention calendar to support priority-based backlog sorting and extend pre-processing status visibility
This commit is contained in:
parent
718403c295
commit
093457ce8d
7 changed files with 500 additions and 34 deletions
|
|
@ -591,10 +591,12 @@ class Intervention(models.Model):
|
||||||
'status': self.status,
|
'status': self.status,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
# Nettoyage des assignations futures obsolètes uniquement
|
||||||
# Nettoyage des assignations futures obsolètes uniquement
|
qs_to_delete = InterventionAssignment.objects.filter(intervention=self).exclude(date__in=active_dates)
|
||||||
qs_to_delete = InterventionAssignment.objects.filter(intervention=self).exclude(date__in=active_dates)
|
qs_to_delete.filter(date__gte=today).delete()
|
||||||
qs_to_delete.filter(date__gte=today).delete()
|
else:
|
||||||
|
# Si l'intervention n'a plus d'équipe assignée, supprimer toutes les assignations pour aujourd'hui et le futur
|
||||||
|
InterventionAssignment.objects.filter(intervention=self, date__gte=today).delete()
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.code} - {self.title}"
|
return f"{self.code} - {self.title}"
|
||||||
|
|
|
||||||
|
|
@ -502,10 +502,19 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
|
||||||
let membersHTML = '';
|
let membersHTML = '';
|
||||||
const isFutureDay = day.date > todayStr;
|
const isFutureDay = day.date > todayStr;
|
||||||
|
const isMemberHistoryAllowed = typeof canViewMemberHistory !== 'undefined' ? Boolean(canViewMemberHistory) : false;
|
||||||
if (!isFutureDay) {
|
if (!isFutureDay) {
|
||||||
if (dayData.members && dayData.members.length > 0) {
|
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;
|
||||||
|
if (isMemberHistoryAllowed && id) {
|
||||||
|
return `<a href="#" class="member-history-link text-decoration-none text-muted fw-semibold" data-member-id="${id}">${escapeHtml(name)}</a>`;
|
||||||
|
}
|
||||||
|
return escapeHtml(name);
|
||||||
|
}).join(', ');
|
||||||
membersHTML = `<div class="team-members-list text-muted small fst-italic mb-2" title="${gettext('Composition de l’équipe')}">
|
membersHTML = `<div class="team-members-list text-muted small fst-italic mb-2" title="${gettext('Composition de l’équipe')}">
|
||||||
<i class="bi bi-people-fill me-1"></i>${dayData.members.map(m => escapeHtml(m)).join(', ')}
|
<i class="bi bi-people-fill me-1"></i>${formattedMembers}
|
||||||
</div>`;
|
</div>`;
|
||||||
} else if (team.id !== null) {
|
} else if (team.id !== null) {
|
||||||
membersHTML = `<div class="team-members-list text-muted small fst-italic mb-2 text-opacity-50">
|
membersHTML = `<div class="team-members-list text-muted small fst-italic mb-2 text-opacity-50">
|
||||||
|
|
@ -541,7 +550,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||||
</div>
|
</div>
|
||||||
${itv.member_name ? `
|
${itv.member_name ? `
|
||||||
<div class="teams-daily-itv-member text-muted small mt-1">
|
<div class="teams-daily-itv-member text-muted small mt-1">
|
||||||
<i class="bi bi-person me-1"></i>${escapeHtml(itv.member_name)}
|
<i class="bi bi-person me-1"></i>${(isMemberHistoryAllowed && itv.member_id) ? `<a href="#" class="member-history-link text-decoration-none text-muted fw-medium" data-member-id="${itv.member_id}">${escapeHtml(itv.member_name)}</a>` : escapeHtml(itv.member_name)}
|
||||||
</div>
|
</div>
|
||||||
` : ''}
|
` : ''}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -574,11 +583,13 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||||
card.addEventListener('dragstart', (e) => {
|
card.addEventListener('dragstart', (e) => {
|
||||||
const itvId = card.getAttribute('data-intervention-id');
|
const itvId = card.getAttribute('data-intervention-id');
|
||||||
let itvInfo = null;
|
let itvInfo = null;
|
||||||
|
let cardDayKey = null;
|
||||||
for (const team of data.teams) {
|
for (const team of data.teams) {
|
||||||
for (const dayKey in team.days) {
|
for (const dayKey in team.days) {
|
||||||
const found = team.days[dayKey].interventions?.find(it => String(it.id) === String(itvId));
|
const found = team.days[dayKey].interventions?.find(it => String(it.id) === String(itvId));
|
||||||
if (found) {
|
if (found) {
|
||||||
itvInfo = found;
|
itvInfo = found;
|
||||||
|
cardDayKey = dayKey;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -599,7 +610,8 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||||
title: itvInfo.title,
|
title: itvInfo.title,
|
||||||
status_code: itvInfo.status,
|
status_code: itvInfo.status,
|
||||||
status: itvInfo.status_display,
|
status: itvInfo.status_display,
|
||||||
duration_ms: itvInfo.duration_ms || 4 * 60 * 60 * 1000
|
duration_ms: itvInfo.duration_ms || 4 * 60 * 60 * 1000,
|
||||||
|
start: itvInfo.start || (cardDayKey ? `${cardDayKey}T08:00:00` : null)
|
||||||
};
|
};
|
||||||
|
|
||||||
card.classList.add('is-dragging');
|
card.classList.add('is-dragging');
|
||||||
|
|
@ -3432,6 +3444,18 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||||
function handleMembersClick(event) {
|
function handleMembersClick(event) {
|
||||||
const coords = getMembersCanvasCoordinates(event);
|
const coords = getMembersCanvasCoordinates(event);
|
||||||
if (!coords) return;
|
if (!coords) return;
|
||||||
|
|
||||||
|
if (coords.x < MEMBERS_LABEL_WIDTH) {
|
||||||
|
const isMemberHistoryAllowed = typeof canViewMemberHistory !== 'undefined' ? Boolean(canViewMemberHistory) : false;
|
||||||
|
if (isMemberHistoryAllowed) {
|
||||||
|
const row = getMemberRowAtY(coords.y);
|
||||||
|
if (row && row.type === 'member' && row.memberId) {
|
||||||
|
openMemberHistoryModal(row.memberId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const hit = membersState.hitRegions.find(region =>
|
const hit = membersState.hitRegions.find(region =>
|
||||||
coords.x >= region.x &&
|
coords.x >= region.x &&
|
||||||
coords.x <= region.x + region.width &&
|
coords.x <= region.x + region.width &&
|
||||||
|
|
@ -3662,6 +3686,14 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (coords.x < MEMBERS_LABEL_WIDTH) {
|
if (coords.x < MEMBERS_LABEL_WIDTH) {
|
||||||
|
const isMemberHistoryAllowed = typeof canViewMemberHistory !== 'undefined' ? Boolean(canViewMemberHistory) : false;
|
||||||
|
if (isMemberHistoryAllowed) {
|
||||||
|
const row = getMemberRowAtY(coords.y);
|
||||||
|
if (row && row.type === 'member' && row.memberId) {
|
||||||
|
dom.bodyEl.classList.add('is-hovering-item');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
dom.bodyEl.classList.remove('is-hovering-item');
|
dom.bodyEl.classList.remove('is-hovering-item');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -3837,7 +3869,12 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||||
};
|
};
|
||||||
|
|
||||||
groupsOrder.forEach(key => {
|
groupsOrder.forEach(key => {
|
||||||
const items = backlogGroups[key] || [];
|
const items = (backlogGroups[key] || []).slice().sort((a, b) => {
|
||||||
|
const pA = String(a.priority_code || '9');
|
||||||
|
const pB = String(b.priority_code || '9');
|
||||||
|
if (pA !== pB) return pA.localeCompare(pB);
|
||||||
|
return (b.id || 0) - (a.id || 0);
|
||||||
|
});
|
||||||
container.appendChild(createBacklogCard(labels[key], items, key));
|
container.appendChild(createBacklogCard(labels[key], items, key));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -4266,6 +4303,20 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||||
modalInterventionCode.textContent = record.code || record.extendedProps?.code || `#${currentModalInterventionId}`;
|
modalInterventionCode.textContent = record.code || record.extendedProps?.code || `#${currentModalInterventionId}`;
|
||||||
modalInterventionTitle.textContent = record.title || '';
|
modalInterventionTitle.textContent = record.title || '';
|
||||||
|
|
||||||
|
const priority = record.priority || record.extendedProps?.priority || '';
|
||||||
|
const priorityCode = record.priority_code || record.extendedProps?.priority_code || '';
|
||||||
|
const modalInterventionPriority = document.getElementById('modal-intervention-priority');
|
||||||
|
if (modalInterventionPriority) {
|
||||||
|
modalInterventionPriority.textContent = priority || gettext('Normal');
|
||||||
|
const priorityClasses = {
|
||||||
|
'1': 'badge text-bg-danger',
|
||||||
|
'2': 'badge text-bg-warning text-dark',
|
||||||
|
'3': 'badge text-bg-info text-dark',
|
||||||
|
'4': 'badge text-bg-secondary'
|
||||||
|
};
|
||||||
|
modalInterventionPriority.className = priorityClasses[priorityCode] || 'badge text-bg-light border text-dark';
|
||||||
|
}
|
||||||
|
|
||||||
const status = record.status || record.extendedProps?.status || '';
|
const status = record.status || record.extendedProps?.status || '';
|
||||||
const statusCode = record.status_code || record.extendedProps?.status_code || '';
|
const statusCode = record.status_code || record.extendedProps?.status_code || '';
|
||||||
const color = STATUS_COLORS[statusCode] || DEFAULT_STATUS_COLOR;
|
const color = STATUS_COLORS[statusCode] || DEFAULT_STATUS_COLOR;
|
||||||
|
|
@ -4298,6 +4349,18 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||||
modalInterventionLink.href = link;
|
modalInterventionLink.href = link;
|
||||||
modalInterventionLink.classList.toggle('disabled', !link);
|
modalInterventionLink.classList.toggle('disabled', !link);
|
||||||
|
|
||||||
|
const modalMemberBtn = document.getElementById('btn-modal-member-history');
|
||||||
|
if (modalMemberBtn) {
|
||||||
|
const isMemberHistoryAllowed = typeof canViewMemberHistory !== 'undefined' ? Boolean(canViewMemberHistory) : false;
|
||||||
|
if (isMemberHistoryAllowed && currentModalMemberId) {
|
||||||
|
modalMemberBtn.classList.remove('d-none');
|
||||||
|
modalMemberBtn.setAttribute('data-member-id', currentModalMemberId);
|
||||||
|
} else {
|
||||||
|
modalMemberBtn.classList.add('d-none');
|
||||||
|
modalMemberBtn.removeAttribute('data-member-id');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
clearPlanningFeedback();
|
clearPlanningFeedback();
|
||||||
planningModal.show();
|
planningModal.show();
|
||||||
}
|
}
|
||||||
|
|
@ -4672,3 +4735,92 @@ function snapToHour(timestampMs) {
|
||||||
}
|
}
|
||||||
return Math.round(timestampMs / HOUR_MS) * HOUR_MS;
|
return Math.round(timestampMs / HOUR_MS) * HOUR_MS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openMemberHistoryModal(memberId) {
|
||||||
|
const isMemberHistoryAllowed = typeof canViewMemberHistory !== 'undefined' ? Boolean(canViewMemberHistory) : false;
|
||||||
|
if (!memberId || !isMemberHistoryAllowed) return;
|
||||||
|
const modalEl = document.getElementById('memberHistoryModal');
|
||||||
|
if (!modalEl || typeof bootstrap === 'undefined') return;
|
||||||
|
|
||||||
|
const modal = bootstrap.Modal.getInstance(modalEl) || new bootstrap.Modal(modalEl);
|
||||||
|
const spinner = document.getElementById('member-history-spinner');
|
||||||
|
const content = document.getElementById('member-history-content');
|
||||||
|
const titleName = document.getElementById('member-history-title-name');
|
||||||
|
const cardName = document.getElementById('member-history-name-card');
|
||||||
|
const cardCompany = document.getElementById('member-history-company-card');
|
||||||
|
const teamsTbody = document.getElementById('member-history-teams-tbody');
|
||||||
|
const itvsTbody = document.getElementById('member-history-interventions-tbody');
|
||||||
|
|
||||||
|
if (spinner) spinner.classList.remove('d-none');
|
||||||
|
if (content) content.classList.add('d-none');
|
||||||
|
|
||||||
|
modal.show();
|
||||||
|
|
||||||
|
fetch(`/interventions/calendar/member/${memberId}/history/`, {
|
||||||
|
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
if (!res.ok) throw new Error('Network error');
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
if (titleName) titleName.textContent = `${gettext('Historique de')} ${data.member.name}`;
|
||||||
|
if (cardName) cardName.textContent = data.member.name;
|
||||||
|
if (cardCompany) cardCompany.textContent = data.member.company || '—';
|
||||||
|
|
||||||
|
if (teamsTbody) {
|
||||||
|
teamsTbody.innerHTML = (data.team_history && data.team_history.length > 0)
|
||||||
|
? data.team_history.map(h => `
|
||||||
|
<tr>
|
||||||
|
<td><span class="fw-semibold">${escapeHtml(h.team_name)}</span></td>
|
||||||
|
<td>${escapeHtml(h.join_date || '—')}</td>
|
||||||
|
<td>${h.is_active ? `<span class="badge text-bg-success">${gettext('En poste')}</span>` : escapeHtml(h.leave_date || '—')}</td>
|
||||||
|
</tr>
|
||||||
|
`).join('')
|
||||||
|
: `<tr><td colspan="3" class="text-center text-muted py-3">${gettext('Aucun historique d\'équipe')}</td></tr>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (itvsTbody) {
|
||||||
|
if (data.interventions && data.interventions.length > 0) {
|
||||||
|
let lastDate = null;
|
||||||
|
itvsTbody.innerHTML = data.interventions.map((itv, idx) => {
|
||||||
|
const isNewDate = itv.date !== lastDate;
|
||||||
|
lastDate = itv.date;
|
||||||
|
const borderStyle = (isNewDate && idx > 0) ? 'border-top: 2px solid #cbd5e1 !important;' : '';
|
||||||
|
return `
|
||||||
|
<tr style="${borderStyle}">
|
||||||
|
<td class="text-nowrap fw-semibold">${escapeHtml(itv.date)}</td>
|
||||||
|
<td><a href="${itv.detail_url}" target="_blank" class="fw-bold text-primary text-decoration-none">${escapeHtml(itv.code)}</a></td>
|
||||||
|
<td class="text-truncate" style="max-width: 180px;" title="${escapeHtml(itv.title)}">${escapeHtml(itv.title)}</td>
|
||||||
|
<td>${escapeHtml(itv.team_name)}</td>
|
||||||
|
<td><span class="badge rounded-pill" style="background-color: ${STATUS_COLORS[itv.status_code] || DEFAULT_STATUS_COLOR}; color: ${idealTextColor(STATUS_COLORS[itv.status_code] || DEFAULT_STATUS_COLOR)};">${escapeHtml(itv.status_display)}</span></td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
} else {
|
||||||
|
itvsTbody.innerHTML = `<tr><td colspan="5" class="text-center text-muted py-3">${gettext('Aucune intervention enregistrée')}</td></tr>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (spinner) spinner.classList.add('d-none');
|
||||||
|
if (content) content.classList.remove('d-none');
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
if (teamsTbody) teamsTbody.innerHTML = `<tr><td colspan="3" class="text-danger py-3 text-center">${gettext('Erreur de chargement')}</td></tr>`;
|
||||||
|
if (spinner) spinner.classList.add('d-none');
|
||||||
|
if (content) content.classList.remove('d-none');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('click', function(e) {
|
||||||
|
const btn = e.target.closest('.member-history-link, #btn-modal-member-history');
|
||||||
|
if (btn) {
|
||||||
|
const memberId = btn.getAttribute('data-member-id');
|
||||||
|
if (memberId) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
openMemberHistoryModal(memberId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -554,13 +554,18 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<p class="mb-1">
|
<div class="d-flex justify-content-between align-items-center mb-1">
|
||||||
<strong>{% translate "Code" %} :</strong> <span id="modal-intervention-code" class="text-primary"></span>
|
<div>
|
||||||
</p>
|
<strong>{% translate "Code" %} :</strong> <span id="modal-intervention-code" class="text-primary fw-bold"></span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span id="modal-intervention-priority" class="badge text-bg-light border"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<p class="mb-1">
|
<p class="mb-1">
|
||||||
<strong>{% translate "Titre" %} :</strong> <span id="modal-intervention-title"></span>
|
<strong>{% translate "Titre" %} :</strong> <span id="modal-intervention-title"></span>
|
||||||
</p>
|
</p>
|
||||||
<p class="mb-0">
|
<p class="mb-1">
|
||||||
<strong>{% translate "Statut" %} :</strong> <span id="modal-intervention-status" class="badge rounded-pill"></span>
|
<strong>{% translate "Statut" %} :</strong> <span id="modal-intervention-status" class="badge rounded-pill"></span>
|
||||||
</p>
|
</p>
|
||||||
<p class="mb-0">
|
<p class="mb-0">
|
||||||
|
|
@ -623,7 +628,14 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-md-6">
|
<div class="col-12 col-md-6">
|
||||||
<label for="planning-member-input" class="form-label">{% translate "Membre assigné" %}</label>
|
<label for="planning-member-input" class="form-label">{% translate "Membre assigné" %}</label>
|
||||||
<input type="text" class="form-control" id="planning-member-input" readonly disabled>
|
<div class="input-group">
|
||||||
|
<input type="text" class="form-control" id="planning-member-input" readonly disabled>
|
||||||
|
{% if can_view_member_history %}
|
||||||
|
<button type="button" class="btn btn-outline-secondary text-muted d-none" id="btn-modal-member-history" title="{% translate 'Voir l’historique du membre' %}">
|
||||||
|
<i class="bi bi-clock-history"></i>
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 d-none" id="planning-feedback"></div>
|
<div class="col-12 d-none" id="planning-feedback"></div>
|
||||||
|
|
@ -638,6 +650,101 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal Bootstrap pour l'historique d'un membre -->
|
||||||
|
<div class="modal fade" id="memberHistoryModal" tabindex="-1" aria-labelledby="memberHistoryModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered modal-xl">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title d-flex align-items-center gap-2" id="memberHistoryModalLabel">
|
||||||
|
<i class="bi bi-person-badge text-primary fs-4"></i>
|
||||||
|
<span id="member-history-title-name">{% translate "Historique du membre" %}</span>
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Fermer"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div id="member-history-spinner" class="text-center py-5">
|
||||||
|
<div class="spinner-border text-primary" role="status">
|
||||||
|
<span class="visually-hidden">{% translate "Chargement..." %}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="member-history-content" class="d-none">
|
||||||
|
<!-- Header info card -->
|
||||||
|
<div class="row g-3 mb-4">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="p-3 bg-light rounded border">
|
||||||
|
<div class="small text-muted mb-1">{% translate "Technicien / Membre" %}</div>
|
||||||
|
<h5 class="mb-0 fw-bold text-primary" id="member-history-name-card"></h5>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="p-3 bg-light rounded border">
|
||||||
|
<div class="small text-muted mb-1">{% translate "Entreprise" %}</div>
|
||||||
|
<h5 class="mb-0 fw-bold" id="member-history-company-card"></h5>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tables for Teams History and Interventions History -->
|
||||||
|
<div class="row g-4">
|
||||||
|
<!-- Team membership history -->
|
||||||
|
<div class="col-md-5">
|
||||||
|
<div class="card h-100 shadow-sm border">
|
||||||
|
<div class="card-header bg-white fw-bold d-flex align-items-center gap-2">
|
||||||
|
<i class="bi bi-people-fill text-primary"></i>
|
||||||
|
{% translate "Historique des équipes" %}
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover align-middle mb-0">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>{% translate "Équipe" %}</th>
|
||||||
|
<th>{% translate "Entrée" %}</th>
|
||||||
|
<th>{% translate "Sortie" %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="member-history-teams-tbody"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Interventions assignments history -->
|
||||||
|
<div class="col-md-7">
|
||||||
|
<div class="card h-100 shadow-sm border">
|
||||||
|
<div class="card-header bg-white fw-bold d-flex align-items-center gap-2">
|
||||||
|
<i class="bi bi-tools text-primary"></i>
|
||||||
|
{% translate "Dernières interventions réalisées / assignées" %}
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<div class="table-responsive" style="max-height: 400px; overflow-y: auto;">
|
||||||
|
<table class="table table-hover align-middle mb-0">
|
||||||
|
<thead class="table-light sticky-top">
|
||||||
|
<tr>
|
||||||
|
<th>{% translate "Date" %}</th>
|
||||||
|
<th>{% translate "Code" %}</th>
|
||||||
|
<th>{% translate "Titre" %}</th>
|
||||||
|
<th>{% translate "Équipe" %}</th>
|
||||||
|
<th>{% translate "Statut" %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="member-history-interventions-tbody"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% translate "Fermer" %}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% endblock content%}
|
{% endblock content%}
|
||||||
|
|
||||||
{% block script %}
|
{% block script %}
|
||||||
|
|
@ -647,6 +754,7 @@
|
||||||
const interventionScheduleUpdateUrl = "{% url 'interventions:interventions_schedule_update' %}";
|
const interventionScheduleUpdateUrl = "{% url 'interventions:interventions_schedule_update' %}";
|
||||||
const calendarGeojsonBaseUrl = "{% url 'interventions:interventions_geojson' %}";
|
const calendarGeojsonBaseUrl = "{% url 'interventions:interventions_geojson' %}";
|
||||||
const saveCalendarPreferencesUrl = "{% url 'common:save_calendar_preferences' %}";
|
const saveCalendarPreferencesUrl = "{% url 'common:save_calendar_preferences' %}";
|
||||||
|
const canViewMemberHistory = {{ can_view_member_history|yesno:"true,false" }};
|
||||||
</script>
|
</script>
|
||||||
<script src="{% url 'javascript-catalog' %}"></script>
|
<script src="{% url 'javascript-catalog' %}"></script>
|
||||||
<script src="{% static 'interventions/status_colors.js' %}"></script>
|
<script src="{% static 'interventions/status_colors.js' %}"></script>
|
||||||
|
|
|
||||||
|
|
@ -174,6 +174,122 @@ class CalendarEventsTests(TestCase):
|
||||||
backlog_ids = [item['id'] for item in backlog_corrective]
|
backlog_ids = [item['id'] for item in backlog_corrective]
|
||||||
self.assertIn(past_unfinished_itv.id, backlog_ids)
|
self.assertIn(past_unfinished_itv.id, backlog_ids)
|
||||||
|
|
||||||
|
def test_unassign_intervention_moves_it_to_backlog(self):
|
||||||
|
from interventions.models import InterventionAssignment
|
||||||
|
now = timezone.now()
|
||||||
|
itv = Intervention.objects.create(
|
||||||
|
title='Intervention Today Team A',
|
||||||
|
status='to_be_processed',
|
||||||
|
maintain_type='corrective',
|
||||||
|
assigned_team=self.team_a,
|
||||||
|
planned_begin_time=now,
|
||||||
|
planned_end_time=now + timedelta(hours=2),
|
||||||
|
)
|
||||||
|
self.assertEqual(InterventionAssignment.objects.filter(intervention=itv, date=now.date()).count(), 1)
|
||||||
|
|
||||||
|
update_url = reverse('interventions:interventions_schedule_update')
|
||||||
|
payload = {
|
||||||
|
'intervention_id': itv.id,
|
||||||
|
'planned_begin_time': now.isoformat(),
|
||||||
|
'planned_end_time': (now + timedelta(hours=2)).isoformat(),
|
||||||
|
'assigned_member_id': None,
|
||||||
|
'assigned_team_id': None,
|
||||||
|
}
|
||||||
|
res = self.client.post(update_url, data=json.dumps(payload), content_type='application/json')
|
||||||
|
self.assertEqual(res.status_code, 200)
|
||||||
|
|
||||||
|
itv.refresh_from_db()
|
||||||
|
self.assertIsNone(itv.assigned_team)
|
||||||
|
self.assertIsNone(itv.assigned_member)
|
||||||
|
self.assertEqual(itv.status, 'to_be_planned')
|
||||||
|
self.assertEqual(InterventionAssignment.objects.filter(intervention=itv, date=now.date()).count(), 0)
|
||||||
|
|
||||||
|
url = reverse('interventions:interventions_events') + '?view_mode=teams_daily'
|
||||||
|
response = self.client.get(url)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
backlog_corrective = data.get('backlog_groups', {}).get('corrective', [])
|
||||||
|
self.assertIn(itv.id, [item['id'] for item in backlog_corrective])
|
||||||
|
|
||||||
|
for team in data.get('teams', []):
|
||||||
|
for day_key, day_data in team.get('days', {}).items():
|
||||||
|
itv_ids_in_day = [item['id'] for item in day_data.get('interventions', [])]
|
||||||
|
self.assertNotIn(itv.id, itv_ids_in_day)
|
||||||
|
|
||||||
|
def test_in_preparation_interventions_only_in_backlog(self):
|
||||||
|
now = timezone.now()
|
||||||
|
itv = Intervention.objects.create(
|
||||||
|
title='In Preparation With Schedule',
|
||||||
|
status='in_preparation',
|
||||||
|
maintain_type='corrective',
|
||||||
|
planned_begin_time=now,
|
||||||
|
planned_end_time=now + timedelta(hours=2),
|
||||||
|
)
|
||||||
|
|
||||||
|
url = reverse('interventions:interventions_events') + '?view_mode=teams_daily'
|
||||||
|
response = self.client.get(url)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
backlog_corrective = data.get('backlog_groups', {}).get('corrective', [])
|
||||||
|
self.assertIn(itv.id, [item['id'] for item in backlog_corrective])
|
||||||
|
|
||||||
|
for team in data.get('teams', []):
|
||||||
|
for day_key, day_data in team.get('days', {}).items():
|
||||||
|
itv_ids_in_day = [item['id'] for item in day_data.get('interventions', [])]
|
||||||
|
self.assertNotIn(itv.id, itv_ids_in_day)
|
||||||
|
|
||||||
|
def test_backlog_sorting_order(self):
|
||||||
|
itv_low_old = Intervention.objects.create(
|
||||||
|
title='Low Priority Old',
|
||||||
|
status='to_be_planned',
|
||||||
|
maintain_type='corrective',
|
||||||
|
priority='4',
|
||||||
|
)
|
||||||
|
itv_urgent_old = Intervention.objects.create(
|
||||||
|
title='Urgent Priority Old',
|
||||||
|
status='to_be_planned',
|
||||||
|
maintain_type='corrective',
|
||||||
|
priority='1',
|
||||||
|
)
|
||||||
|
itv_urgent_new = Intervention.objects.create(
|
||||||
|
title='Urgent Priority New',
|
||||||
|
status='to_be_planned',
|
||||||
|
maintain_type='corrective',
|
||||||
|
priority='1',
|
||||||
|
)
|
||||||
|
|
||||||
|
url = reverse('interventions:interventions_events') + '?view_mode=teams_daily'
|
||||||
|
response = self.client.get(url)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
backlog_corrective = data.get('backlog_groups', {}).get('corrective', [])
|
||||||
|
ids = [item['id'] for item in backlog_corrective if item['id'] in {itv_low_old.id, itv_urgent_old.id, itv_urgent_new.id}]
|
||||||
|
|
||||||
|
self.assertEqual(ids, [itv_urgent_new.id, itv_urgent_old.id, itv_low_old.id])
|
||||||
|
|
||||||
|
def test_member_history_ajax_permissions_and_data(self):
|
||||||
|
from contracts.models import CompanyMember, CompanyMemberHistory
|
||||||
|
member = CompanyMember.objects.create(name="Jean Dupont", user=self.user)
|
||||||
|
member.teams.add(self.team_a)
|
||||||
|
CompanyMemberHistory.objects.create(
|
||||||
|
member=member,
|
||||||
|
team=self.team_a,
|
||||||
|
join_date=timezone.now().date() - timedelta(days=10)
|
||||||
|
)
|
||||||
|
url = reverse('interventions:member_history_ajax', args=[member.id])
|
||||||
|
response = self.client.get(url)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
self.assertIn('member', data)
|
||||||
|
self.assertEqual(data['member']['name'], "Jean Dupont")
|
||||||
|
self.assertIn('team_history', data)
|
||||||
|
self.assertTrue(len(data['team_history']) >= 1)
|
||||||
|
self.assertEqual(data['team_history'][0]['team_name'], self.team_a.name)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -936,13 +936,15 @@ class InterventionAssignmentTests(TestCase):
|
||||||
|
|
||||||
# On 2026-06-09, team_a should have John Doe in members, and itv in interventions
|
# On 2026-06-09, team_a should have John Doe in members, and itv in interventions
|
||||||
day_06_09 = team_a_entry['days']['2026-06-09']
|
day_06_09 = team_a_entry['days']['2026-06-09']
|
||||||
self.assertIn('John Doe', day_06_09['members'])
|
day_06_09_names = [m['name'] if isinstance(m, dict) else m for m in day_06_09['members']]
|
||||||
|
self.assertIn('John Doe', day_06_09_names)
|
||||||
self.assertEqual(len(day_06_09['interventions']), 1)
|
self.assertEqual(len(day_06_09['interventions']), 1)
|
||||||
self.assertEqual(day_06_09['interventions'][0]['id'], itv.id)
|
self.assertEqual(day_06_09['interventions'][0]['id'], itv.id)
|
||||||
|
|
||||||
# On 2026-06-11, team_a should NOT have John Doe (he left on 2026-06-10)
|
# On 2026-06-11, team_a should NOT have John Doe (he left on 2026-06-10)
|
||||||
day_06_11 = team_a_entry['days']['2026-06-11']
|
day_06_11 = team_a_entry['days']['2026-06-11']
|
||||||
self.assertNotIn('John Doe', day_06_11['members'])
|
day_06_11_names = [m['name'] if isinstance(m, dict) else m for m in day_06_11['members']]
|
||||||
|
self.assertNotIn('John Doe', day_06_11_names)
|
||||||
|
|
||||||
# Check "Sans équipe" row
|
# Check "Sans équipe" row
|
||||||
unassigned_entry = next((t for t in teams if t['id'] is None), None)
|
unassigned_entry = next((t for t in teams if t['id'] is None), None)
|
||||||
|
|
@ -989,7 +991,8 @@ class InterventionAssignmentTests(TestCase):
|
||||||
|
|
||||||
# On 2026-06-09, members list should have ONLY ONE entry for 'John Doe'
|
# On 2026-06-09, members list should have ONLY ONE entry for 'John Doe'
|
||||||
day_06_09 = team_a_entry['days']['2026-06-09']
|
day_06_09 = team_a_entry['days']['2026-06-09']
|
||||||
self.assertEqual(day_06_09['members'], ['John Doe'])
|
day_06_09_names = [m['name'] if isinstance(m, dict) else m for m in day_06_09['members']]
|
||||||
|
self.assertEqual(day_06_09_names, ['John Doe'])
|
||||||
|
|
||||||
@mock.patch('django.utils.timezone.now')
|
@mock.patch('django.utils.timezone.now')
|
||||||
def test_teams_daily_ajax_view_member_transfer_resolution(self, mock_now):
|
def test_teams_daily_ajax_view_member_transfer_resolution(self, mock_now):
|
||||||
|
|
@ -1031,14 +1034,14 @@ class InterventionAssignmentTests(TestCase):
|
||||||
# On 2026-06-11, John Doe should still be in team_a and NOT in team_b
|
# On 2026-06-11, John Doe should still be in team_a and NOT in team_b
|
||||||
day_06_11_a = team_a_entry['days']['2026-06-11']
|
day_06_11_a = team_a_entry['days']['2026-06-11']
|
||||||
day_06_11_b = team_b_entry['days']['2026-06-11']
|
day_06_11_b = team_b_entry['days']['2026-06-11']
|
||||||
self.assertIn('John Doe', day_06_11_a['members'])
|
self.assertIn('John Doe', [m['name'] if isinstance(m, dict) else m for m in day_06_11_a['members']])
|
||||||
self.assertNotIn('John Doe', day_06_11_b['members'])
|
self.assertNotIn('John Doe', [m['name'] if isinstance(m, dict) else m for m in day_06_11_b['members']])
|
||||||
|
|
||||||
# On 2026-06-12 (transfer day), John Doe should NOT be in team_a and should be in team_b
|
# On 2026-06-12 (transfer day), John Doe should NOT be in team_a and should be in team_b
|
||||||
day_06_12_a = team_a_entry['days']['2026-06-12']
|
day_06_12_a = team_a_entry['days']['2026-06-12']
|
||||||
day_06_12_b = team_b_entry['days']['2026-06-12']
|
day_06_12_b = team_b_entry['days']['2026-06-12']
|
||||||
self.assertNotIn('John Doe', day_06_12_a['members'])
|
self.assertNotIn('John Doe', [m['name'] if isinstance(m, dict) else m for m in day_06_12_a['members']])
|
||||||
self.assertIn('John Doe', day_06_12_b['members'])
|
self.assertIn('John Doe', [m['name'] if isinstance(m, dict) else m for m in day_06_12_b['members']])
|
||||||
|
|
||||||
@mock.patch('django.utils.timezone.now')
|
@mock.patch('django.utils.timezone.now')
|
||||||
def test_teams_daily_ajax_view_member_concurrent_resolution(self, mock_now):
|
def test_teams_daily_ajax_view_member_concurrent_resolution(self, mock_now):
|
||||||
|
|
@ -1080,8 +1083,8 @@ class InterventionAssignmentTests(TestCase):
|
||||||
# On 2026-06-11, John Doe should be in both team_a and team_b
|
# On 2026-06-11, John Doe should be in both team_a and team_b
|
||||||
day_06_11_a = team_a_entry['days']['2026-06-11']
|
day_06_11_a = team_a_entry['days']['2026-06-11']
|
||||||
day_06_11_b = team_b_entry['days']['2026-06-11']
|
day_06_11_b = team_b_entry['days']['2026-06-11']
|
||||||
self.assertIn('John Doe', day_06_11_a['members'])
|
self.assertIn('John Doe', [m['name'] if isinstance(m, dict) else m for m in day_06_11_a['members']])
|
||||||
self.assertIn('John Doe', day_06_11_b['members'])
|
self.assertIn('John Doe', [m['name'] if isinstance(m, dict) else m for m in day_06_11_b['members']])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,7 @@ urlpatterns = [
|
||||||
path('events/', views.interventions_events, name='interventions_events'),
|
path('events/', views.interventions_events, name='interventions_events'),
|
||||||
path('schedule/update/', views.interventions_schedule_update, name='interventions_schedule_update'),
|
path('schedule/update/', views.interventions_schedule_update, name='interventions_schedule_update'),
|
||||||
path('calendar/', views.interventions_calendar, name='interventions_calendar'),
|
path('calendar/', views.interventions_calendar, name='interventions_calendar'),
|
||||||
|
path('calendar/member/<int:member_id>/history/', views.member_history_ajax, name='member_history_ajax'),
|
||||||
path('add/choose-thematic/', views.choose_thematic_before_add, name='intervention_add_choose_thematic'),
|
path('add/choose-thematic/', views.choose_thematic_before_add, name='intervention_add_choose_thematic'),
|
||||||
path('<str:thematic_code>/add/', views.interventions_add, name='intervention_add'),
|
path('<str:thematic_code>/add/', views.interventions_add, name='intervention_add'),
|
||||||
path('<str:thematic_code>/add-mobile/', views.interventions_add, name='intervention_add_mobile_form'),
|
path('<str:thematic_code>/add-mobile/', views.interventions_add, name='intervention_add_mobile_form'),
|
||||||
|
|
|
||||||
|
|
@ -268,6 +268,7 @@ def interventions_calendar(request):
|
||||||
'selected_view_mode': selected_view_mode,
|
'selected_view_mode': selected_view_mode,
|
||||||
'selected_search': selected_search,
|
'selected_search': selected_search,
|
||||||
'can_use_member_view': can_use_member_view,
|
'can_use_member_view': can_use_member_view,
|
||||||
|
'can_view_member_history': bool(role_names & {'admin', 'manager', 'external_manager'}),
|
||||||
'can_move_interventions': can_move_interventions,
|
'can_move_interventions': can_move_interventions,
|
||||||
'is_manager': is_manager,
|
'is_manager': is_manager,
|
||||||
'asset_category_choices': asset_category_choices,
|
'asset_category_choices': asset_category_choices,
|
||||||
|
|
@ -487,6 +488,7 @@ def interventions_events(request):
|
||||||
'status_code': itv.status, # utile pour la couleur dans le Gantt
|
'status_code': itv.status, # utile pour la couleur dans le Gantt
|
||||||
'prep_state': itv.prep_state,
|
'prep_state': itv.prep_state,
|
||||||
'priority': itv.get_priority_display(),
|
'priority': itv.get_priority_display(),
|
||||||
|
'priority_code': itv.priority,
|
||||||
'type': itv.get_type_display(),
|
'type': itv.get_type_display(),
|
||||||
'planned_begin': itv.planned_begin_time.isoformat() if itv.planned_begin_time else None,
|
'planned_begin': itv.planned_begin_time.isoformat() if itv.planned_begin_time else None,
|
||||||
'planned_end': itv.planned_end_time.isoformat() if itv.planned_end_time else None,
|
'planned_end': itv.planned_end_time.isoformat() if itv.planned_end_time else None,
|
||||||
|
|
@ -550,7 +552,8 @@ def _interventions_events_by_members(
|
||||||
planned_begin = getattr(itv, 'planned_begin_time', None)
|
planned_begin = getattr(itv, 'planned_begin_time', None)
|
||||||
return bool(planned_begin and planned_begin > timezone.now())
|
return bool(planned_begin and planned_begin > timezone.now())
|
||||||
|
|
||||||
backlog_allowed_statuses = {'to_be_planned', 'to_be_processed'}
|
backlog_allowed_statuses = {'in_preparation', 'to_be_approved', 'to_be_planned', 'to_be_processed'}
|
||||||
|
PRE_PROCESSING_STATUSES = {'in_preparation', 'to_be_approved', 'to_be_planned'}
|
||||||
backlog_allowed_types = {'corrective', 'preventive', 'ameliorative'}
|
backlog_allowed_types = {'corrective', 'preventive', 'ameliorative'}
|
||||||
maintain_type_labels = dict(MAINTAIN_CHOICES)
|
maintain_type_labels = dict(MAINTAIN_CHOICES)
|
||||||
|
|
||||||
|
|
@ -588,6 +591,7 @@ def _interventions_events_by_members(
|
||||||
'status_code': itv.status,
|
'status_code': itv.status,
|
||||||
'prep_state': prep_state,
|
'prep_state': prep_state,
|
||||||
'priority': itv.get_priority_display(),
|
'priority': itv.get_priority_display(),
|
||||||
|
'priority_code': itv.priority or '3',
|
||||||
'type': itv.get_type_display(),
|
'type': itv.get_type_display(),
|
||||||
'maintain_type': itv.maintain_type,
|
'maintain_type': itv.maintain_type,
|
||||||
'maintain_type_label': maintain_type_labels.get(itv.maintain_type, itv.maintain_type),
|
'maintain_type_label': maintain_type_labels.get(itv.maintain_type, itv.maintain_type),
|
||||||
|
|
@ -654,9 +658,9 @@ def _interventions_events_by_members(
|
||||||
member = itv.assigned_member
|
member = itv.assigned_member
|
||||||
team = itv.assigned_team
|
team = itv.assigned_team
|
||||||
|
|
||||||
# Interventions with status "to_be_planned" always go to the backlog (right column)
|
# Interventions with status lower than "to_be_processed" always go to the backlog (right column)
|
||||||
# regardless of assignment or scheduling
|
# regardless of assignment or scheduling
|
||||||
if itv.status == 'to_be_planned':
|
if itv.status in PRE_PROCESSING_STATUSES:
|
||||||
unassigned.append(itv)
|
unassigned.append(itv)
|
||||||
if not scheduled:
|
if not scheduled:
|
||||||
unscheduled_ids.add(itv.id)
|
unscheduled_ids.add(itv.id)
|
||||||
|
|
@ -757,7 +761,7 @@ def _interventions_events_by_members(
|
||||||
and (
|
and (
|
||||||
(
|
(
|
||||||
(itv.status in backlog_allowed_statuses)
|
(itv.status in backlog_allowed_statuses)
|
||||||
and (not assigned or not scheduled or itv.status == 'to_be_planned')
|
and (not assigned or not scheduled or itv.status in PRE_PROCESSING_STATUSES)
|
||||||
)
|
)
|
||||||
or is_past_unfinished
|
or is_past_unfinished
|
||||||
)
|
)
|
||||||
|
|
@ -766,6 +770,9 @@ def _interventions_events_by_members(
|
||||||
if include_in_backlog:
|
if include_in_backlog:
|
||||||
backlog_groups[itv.maintain_type].append(serialize_intervention(itv))
|
backlog_groups[itv.maintain_type].append(serialize_intervention(itv))
|
||||||
|
|
||||||
|
for group_key in backlog_groups:
|
||||||
|
backlog_groups[group_key].sort(key=lambda x: (x.get('priority_code') or '9', -x.get('id', 0)))
|
||||||
|
|
||||||
return JsonResponse({
|
return JsonResponse({
|
||||||
'view_mode': 'members',
|
'view_mode': 'members',
|
||||||
'teams': teams,
|
'teams': teams,
|
||||||
|
|
@ -879,12 +886,12 @@ def _interventions_events_by_teams_daily(
|
||||||
|
|
||||||
# Add to team_compositions_sets for the remaining team(s)
|
# Add to team_compositions_sets for the remaining team(s)
|
||||||
for h in active:
|
for h in active:
|
||||||
team_compositions_sets[(h['team_id'], d)].add(member_name)
|
team_compositions_sets[(h['team_id'], d)].add((m_id, member_name))
|
||||||
|
|
||||||
# Convert sets to sorted lists for consistency and default to empty list on missing access
|
# Convert sets to sorted lists of dicts for consistency and default to empty list on missing access
|
||||||
team_compositions = defaultdict(list)
|
team_compositions = defaultdict(list)
|
||||||
for key, names_set in team_compositions_sets.items():
|
for key, members_set in team_compositions_sets.items():
|
||||||
team_compositions[key] = sorted(list(names_set))
|
team_compositions[key] = [{'id': mid, 'name': mname} for mid, mname in sorted(list(members_set), key=lambda x: x[1])]
|
||||||
|
|
||||||
# 6. Retrieve assignments for the 7 days
|
# 6. Retrieve assignments for the 7 days
|
||||||
# Filter assignments by the interventions list (which already has user view permissions and search filters applied!)
|
# Filter assignments by the interventions list (which already has user view permissions and search filters applied!)
|
||||||
|
|
@ -922,7 +929,8 @@ def _interventions_events_by_teams_daily(
|
||||||
# Unassigned interventions are those scheduled in this period but having no assignment in InterventionAssignment
|
# Unassigned interventions are those scheduled in this period but having no assignment in InterventionAssignment
|
||||||
# OR assigned to a team that is not allowed (t_id is None in our loop above)
|
# OR assigned to a team that is not allowed (t_id is None in our loop above)
|
||||||
# Let's find interventions scheduled in this week that are in our viewable interventions list:
|
# Let's find interventions scheduled in this week that are in our viewable interventions list:
|
||||||
scheduled_itvs = interventions.filter(
|
PRE_PROCESSING_STATUSES = {'in_preparation', 'to_be_approved', 'to_be_planned'}
|
||||||
|
scheduled_itvs = interventions.exclude(status__in=PRE_PROCESSING_STATUSES).filter(
|
||||||
Q(effective_begin__lte=end_date) &
|
Q(effective_begin__lte=end_date) &
|
||||||
(Q(effective_end__gte=start_date) | Q(effective_end__isnull=True))
|
(Q(effective_end__gte=start_date) | Q(effective_end__isnull=True))
|
||||||
)
|
)
|
||||||
|
|
@ -994,7 +1002,7 @@ def _interventions_events_by_teams_daily(
|
||||||
})
|
})
|
||||||
|
|
||||||
# Compute backlog groups for the assignment sidebar
|
# Compute backlog groups for the assignment sidebar
|
||||||
backlog_allowed_statuses = {'to_be_planned', 'to_be_processed'}
|
backlog_allowed_statuses = {'in_preparation', 'to_be_approved', 'to_be_planned', 'to_be_processed'}
|
||||||
backlog_allowed_types = {'corrective', 'preventive', 'ameliorative'}
|
backlog_allowed_types = {'corrective', 'preventive', 'ameliorative'}
|
||||||
maintain_type_labels = dict(MAINTAIN_CHOICES)
|
maintain_type_labels = dict(MAINTAIN_CHOICES)
|
||||||
contract_filter = None if allowed_contract_ids is None else set(allowed_contract_ids)
|
contract_filter = None if allowed_contract_ids is None else set(allowed_contract_ids)
|
||||||
|
|
@ -1033,6 +1041,7 @@ def _interventions_events_by_teams_daily(
|
||||||
'status_code': itv.status,
|
'status_code': itv.status,
|
||||||
'prep_state': prep_state,
|
'prep_state': prep_state,
|
||||||
'priority': itv.get_priority_display(),
|
'priority': itv.get_priority_display(),
|
||||||
|
'priority_code': itv.priority or '3',
|
||||||
'type': itv.get_type_display(),
|
'type': itv.get_type_display(),
|
||||||
'maintain_type': itv.maintain_type,
|
'maintain_type': itv.maintain_type,
|
||||||
'maintain_type_label': maintain_type_labels.get(itv.maintain_type, itv.maintain_type),
|
'maintain_type_label': maintain_type_labels.get(itv.maintain_type, itv.maintain_type),
|
||||||
|
|
@ -1069,7 +1078,7 @@ def _interventions_events_by_teams_daily(
|
||||||
and (
|
and (
|
||||||
(
|
(
|
||||||
(itv.status in backlog_allowed_statuses)
|
(itv.status in backlog_allowed_statuses)
|
||||||
and (not assigned or not scheduled or itv.status == 'to_be_planned')
|
and (not assigned or not scheduled or itv.status in PRE_PROCESSING_STATUSES)
|
||||||
)
|
)
|
||||||
or is_past_unfinished
|
or is_past_unfinished
|
||||||
)
|
)
|
||||||
|
|
@ -1078,6 +1087,9 @@ def _interventions_events_by_teams_daily(
|
||||||
if include_in_backlog:
|
if include_in_backlog:
|
||||||
backlog_groups[itv.maintain_type].append(serialize_intervention(itv))
|
backlog_groups[itv.maintain_type].append(serialize_intervention(itv))
|
||||||
|
|
||||||
|
for group_key in backlog_groups:
|
||||||
|
backlog_groups[group_key].sort(key=lambda x: (x.get('priority_code') or '9', -x.get('id', 0)))
|
||||||
|
|
||||||
return JsonResponse({
|
return JsonResponse({
|
||||||
'view_mode': 'teams_daily',
|
'view_mode': 'teams_daily',
|
||||||
'start_date': start_date.isoformat(),
|
'start_date': start_date.isoformat(),
|
||||||
|
|
@ -1250,7 +1262,7 @@ def interventions_schedule_update(request):
|
||||||
if prev_status == 'to_be_planned' and is_now_assigned:
|
if prev_status == 'to_be_planned' and is_now_assigned:
|
||||||
new_status = 'to_be_processed'
|
new_status = 'to_be_processed'
|
||||||
# If intervention is being unassigned (back to backlog) -> change to "to_be_planned"
|
# If intervention is being unassigned (back to backlog) -> change to "to_be_planned"
|
||||||
elif was_assigned and not is_now_assigned and prev_status == 'to_be_processed':
|
elif was_assigned and not is_now_assigned and prev_status in ('to_be_processed', 'assigned'):
|
||||||
new_status = 'to_be_planned'
|
new_status = 'to_be_planned'
|
||||||
|
|
||||||
if new_status != prev_status:
|
if new_status != prev_status:
|
||||||
|
|
@ -1302,3 +1314,75 @@ def intervention_planning_data_ajax(request, intervention_id):
|
||||||
'team_name': intervention.assigned_team.name if intervention.assigned_team else None,
|
'team_name': intervention.assigned_team.name if intervention.assigned_team else None,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def member_history_ajax(request, member_id):
|
||||||
|
if not request.user.is_authenticated:
|
||||||
|
return JsonResponse({'error': _("Authentification requise.")}, status=403)
|
||||||
|
|
||||||
|
user_config = (
|
||||||
|
UserConfig.objects
|
||||||
|
.select_related('user')
|
||||||
|
.prefetch_related('roles')
|
||||||
|
.filter(user=request.user)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if not user_config:
|
||||||
|
return JsonResponse({'error': _("Accès non autorisé.")}, status=403)
|
||||||
|
|
||||||
|
role_names = _get_role_names(user_config)
|
||||||
|
can_view = bool(role_names & {'admin', 'manager', 'external_manager'})
|
||||||
|
if not can_view:
|
||||||
|
return JsonResponse({'error': _("Permission refusée.")}, status=403)
|
||||||
|
|
||||||
|
member = get_object_or_404(CompanyMember.objects.prefetch_related('teams__company'), id=member_id)
|
||||||
|
company_names = set(t.company.name for t in member.teams.all() if getattr(t, 'company', None))
|
||||||
|
company_str = ", ".join(sorted(company_names)) if company_names else ''
|
||||||
|
|
||||||
|
team_history_qs = (
|
||||||
|
CompanyMemberHistory.objects
|
||||||
|
.filter(member=member)
|
||||||
|
.select_related('team')
|
||||||
|
.order_by('-join_date')
|
||||||
|
)
|
||||||
|
team_history = [
|
||||||
|
{
|
||||||
|
'team_name': h.team.name if h.team else '—',
|
||||||
|
'join_date': h.join_date.strftime('%d/%m/%Y') if h.join_date else None,
|
||||||
|
'leave_date': h.leave_date.strftime('%d/%m/%Y') if h.leave_date else None,
|
||||||
|
'is_active': h.leave_date is None,
|
||||||
|
}
|
||||||
|
for h in team_history_qs
|
||||||
|
]
|
||||||
|
|
||||||
|
assignments_qs = (
|
||||||
|
InterventionAssignment.objects
|
||||||
|
.filter(member=member)
|
||||||
|
.select_related('intervention', 'team')
|
||||||
|
.order_by('-date', '-intervention__code', '-intervention__id')[:100]
|
||||||
|
)
|
||||||
|
interventions_list = [
|
||||||
|
{
|
||||||
|
'id': a.intervention.id,
|
||||||
|
'code': a.intervention.code,
|
||||||
|
'title': a.intervention.title,
|
||||||
|
'date': a.date.strftime('%d/%m/%Y') if a.date else '',
|
||||||
|
'raw_date': a.date.isoformat() if a.date else '',
|
||||||
|
'team_name': a.team.name if a.team else '—',
|
||||||
|
'status_code': a.status or a.intervention.status,
|
||||||
|
'status_display': dict(STATUS_CHOICES).get(a.status or a.intervention.status, a.status or a.intervention.status),
|
||||||
|
'detail_url': reverse('interventions:intervention_detail', args=[a.intervention.id]),
|
||||||
|
}
|
||||||
|
for a in assignments_qs
|
||||||
|
]
|
||||||
|
interventions_list.sort(key=lambda x: (x['raw_date'], x['code'] or '', x['id']), reverse=True)
|
||||||
|
|
||||||
|
return JsonResponse({
|
||||||
|
'member': {
|
||||||
|
'id': member.id,
|
||||||
|
'name': member.name,
|
||||||
|
'company': company_str,
|
||||||
|
},
|
||||||
|
'team_history': team_history,
|
||||||
|
'interventions': interventions_list,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue