837 lines
32 KiB
JavaScript
837 lines
32 KiB
JavaScript
|
||
|
||
const statusTimeline = document.getElementById("statusTimeline");
|
||
|
||
let currentIndex = -1;
|
||
|
||
/* -------- Status color map (réfère à la constante de status_colors.js) -------- */
|
||
const STATUS_COLORS = INTERVENTION_STATUS_COLORS;
|
||
const STATUS_TEXT_DARK = new Set(['to_be_approved', 'to_be_planned', 'assigned', 'on_pause', 'processed', 'to_be_corrected', 'corrected', 'canceled']);
|
||
|
||
STATUS_CHOICES.forEach((status, index) => {
|
||
if (status.key === "to_be_approved") {
|
||
STATUS_CHOICES[index].transition_label = gettext("Envoyer pour approbation");
|
||
}
|
||
if (status.key === "to_be_planned") {
|
||
STATUS_CHOICES[index].transition_label = gettext("Envoyer pour planification");
|
||
}
|
||
if (status.key === "to_be_processed") {
|
||
STATUS_CHOICES[index].transition_label = gettext("Envoyer pour traitement");
|
||
}
|
||
if (status.key === "assigned") {
|
||
STATUS_CHOICES[index].transition_label = gettext("Prendre en charge");
|
||
}
|
||
if (status.key === "in_progress") {
|
||
STATUS_CHOICES[index].transition_label = gettext("Démarrer l'intervention");
|
||
}
|
||
else if (status.key === "on_pause") {
|
||
STATUS_CHOICES[index].transition_label = gettext("Mettre en pause");
|
||
}
|
||
else if (status.key === "finished") {
|
||
STATUS_CHOICES[index].transition_label = gettext("Terminer l'intervention");
|
||
}
|
||
else if (status.key === "processed") {
|
||
STATUS_CHOICES[index].transition_label = gettext("Envoyer pour validation");
|
||
}
|
||
else if (status.key === "to_be_corrected") {
|
||
STATUS_CHOICES[index].transition_label = gettext("Demander correction");
|
||
}
|
||
else if (status.key === "corrected") {
|
||
STATUS_CHOICES[index].transition_label = gettext("Corriger");
|
||
}
|
||
else if (status.key === "validated") {
|
||
STATUS_CHOICES[index].transition_label = gettext("Valider");
|
||
}
|
||
else if (status.key === "invoiced") {
|
||
STATUS_CHOICES[index].transition_label = gettext("Facturer");
|
||
}
|
||
else if (status.key === "closed") {
|
||
STATUS_CHOICES[index].transition_label = gettext("Finaliser");
|
||
}
|
||
else if (status.key === "canceled") {
|
||
STATUS_CHOICES[index].transition_label = gettext("Annuler");
|
||
}
|
||
});
|
||
|
||
|
||
|
||
/* -------- Transition helpers -------- */
|
||
const TRANSITIONS = Array.isArray(allowed_transitions) ? allowed_transitions : [];
|
||
|
||
// Ensemble des "to" autorisés depuis le statut courant (en tenant compte de t.allowed)
|
||
const ALLOWED_TO_SET = new Set(
|
||
TRANSITIONS.filter(t => t.allowed !== false).map(t => t.to)
|
||
);
|
||
|
||
// Vrai si on peut aller vers `statusKey`
|
||
function canGoTo(statusKey) {
|
||
// Filtrage spécial pour 'assigned' (prise en charge) et 'in_progress' (démarrer) :
|
||
// Nécessite un contrat ET un accès au contrat (canTakeOver)
|
||
if (statusKey === 'assigned' || statusKey === 'in_progress') {
|
||
if (typeof canTakeOver === 'undefined' || !canTakeOver) {
|
||
return false;
|
||
}
|
||
}
|
||
return ALLOWED_TO_SET.has(statusKey);
|
||
}
|
||
|
||
// Métadonnées d'une transition vers `statusKey` (label, reason…)
|
||
function transitionMeta(statusKey) {
|
||
return TRANSITIONS.find(t => t.to === statusKey);
|
||
}
|
||
|
||
// Libellé d'action à afficher pour le bouton
|
||
function transitionLabel(statusKey) {
|
||
const t = transitionMeta(statusKey);
|
||
return (t && t.label) || (STATUS_CHOICES?.[statusKey] ?? statusKey);
|
||
}
|
||
|
||
// Raison éventuelle de blocage (quand t.allowed === false ; prêt pour les guards)
|
||
function transitionReason(statusKey) {
|
||
const t = transitionMeta(statusKey);
|
||
return (t && t.allowed === false && t.reason) ? t.reason : "";
|
||
}
|
||
|
||
/* -------- Pause reason modal -------- */
|
||
const pauseReasonModalEl = document.getElementById('pauseReasonModal');
|
||
let pauseReasonModal = null;
|
||
let pauseReasonSelect;
|
||
let pauseReasonOtherGroup;
|
||
let pauseReasonOtherInput;
|
||
let pauseReasonError;
|
||
|
||
if (pauseReasonModalEl) {
|
||
pauseReasonModal = bootstrap.Modal.getOrCreateInstance(pauseReasonModalEl);
|
||
pauseReasonSelect = document.getElementById('pauseReasonSelect');
|
||
pauseReasonOtherGroup = document.getElementById('pauseReasonOtherGroup');
|
||
pauseReasonOtherInput = document.getElementById('pauseReasonOtherInput');
|
||
pauseReasonError = document.getElementById('pauseReasonError');
|
||
|
||
const confirmPauseReasonBtn = document.getElementById('confirmPauseReason');
|
||
|
||
function hidePauseReasonError() {
|
||
if (pauseReasonError) {
|
||
pauseReasonError.classList.add('d-none');
|
||
pauseReasonError.textContent = '';
|
||
}
|
||
}
|
||
|
||
function showPauseReasonError(message) {
|
||
if (!pauseReasonError) {
|
||
return;
|
||
}
|
||
pauseReasonError.textContent = message;
|
||
pauseReasonError.classList.remove('d-none');
|
||
}
|
||
|
||
function resetPauseReasonModal() {
|
||
if (pauseReasonSelect) {
|
||
pauseReasonSelect.value = '';
|
||
}
|
||
if (pauseReasonOtherGroup) {
|
||
pauseReasonOtherGroup.classList.add('d-none');
|
||
}
|
||
if (pauseReasonOtherInput) {
|
||
pauseReasonOtherInput.value = '';
|
||
}
|
||
hidePauseReasonError();
|
||
}
|
||
|
||
if (pauseReasonSelect) {
|
||
pauseReasonSelect.addEventListener('change', () => {
|
||
hidePauseReasonError();
|
||
if (!pauseReasonOtherGroup || !pauseReasonOtherInput) {
|
||
return;
|
||
}
|
||
if (pauseReasonSelect.value === 'other') {
|
||
pauseReasonOtherGroup.classList.remove('d-none');
|
||
} else {
|
||
pauseReasonOtherGroup.classList.add('d-none');
|
||
pauseReasonOtherInput.value = '';
|
||
}
|
||
});
|
||
}
|
||
|
||
if (pauseReasonModalEl) {
|
||
pauseReasonModalEl.addEventListener('hidden.bs.modal', resetPauseReasonModal);
|
||
}
|
||
|
||
if (confirmPauseReasonBtn) {
|
||
confirmPauseReasonBtn.addEventListener('click', () => {
|
||
if (!pauseReasonSelect) {
|
||
return;
|
||
}
|
||
const reason = pauseReasonSelect.value;
|
||
const otherText = pauseReasonOtherInput ? pauseReasonOtherInput.value.trim() : '';
|
||
|
||
if (!reason) {
|
||
showPauseReasonError(gettext('Veuillez sélectionner une raison.'));
|
||
return;
|
||
}
|
||
if (reason === 'other' && !otherText) {
|
||
showPauseReasonError(gettext('Veuillez préciser la raison de la mise en pause.'));
|
||
return;
|
||
}
|
||
|
||
pauseReasonModal.hide();
|
||
hidePauseReasonError();
|
||
updateStatus('on_pause', {
|
||
pause_reason: reason,
|
||
pause_reason_other: otherText,
|
||
});
|
||
});
|
||
}
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
// Trouver l'index du dernier statut visible et le prochain statut possible
|
||
STATUS_CHOICES.forEach((status, index) => {
|
||
if (status.key === currentStatus) {
|
||
currentIndex = index;
|
||
}
|
||
});
|
||
|
||
// Les transitions autorisées sont déterminées entièrement via canGoTo() / ALLOWED_TO_SET,
|
||
// qui provient du backend et tient compte de tous les rôles de l'utilisateur.
|
||
|
||
|
||
|
||
|
||
// Générer la timeline
|
||
// On affiche TOUS les statuts visibles (passés, courant, actions possibles, annulé).
|
||
// Les cercles qui correspondent à une transition autorisée deviennent des <button>
|
||
// colorés avec la couleur du statut cible.
|
||
STATUS_CHOICES.forEach((status, index) => {
|
||
let statusClass = "hide";
|
||
let dashedClass = "";
|
||
let label = status.label;
|
||
const isAllowed = canGoTo(status.key);
|
||
const isActionTarget = isAllowed;
|
||
|
||
if (index === currentIndex) {
|
||
statusClass = "current";
|
||
dashedClass = "next-step-dashed";
|
||
} else if (index < currentIndex) {
|
||
statusClass = "past";
|
||
if (!isActionTarget) {
|
||
label = "";
|
||
}
|
||
} else {
|
||
if (isActionTarget) {
|
||
statusClass = "next";
|
||
dashedClass = "next-step-dashed";
|
||
}
|
||
}
|
||
|
||
if (index === (STATUS_CHOICES.length - 1)) {
|
||
dashedClass = "";
|
||
}
|
||
|
||
// Ne pas rendre 'canceled' dans le flux principal (il a son propre cercle à la fin)
|
||
if (status.key === "canceled" && statusClass === "hide") return;
|
||
|
||
const step = document.createElement("div");
|
||
step.className = `step ${statusClass} ${dashedClass}`;
|
||
|
||
// Cercle futur passif (pas d'action autorisée, pas le statut courant)
|
||
if (statusClass === "hide") {
|
||
step.innerHTML = `
|
||
<div id="status_${status.key}" class="circle" title="${label}">
|
||
${index + 1}
|
||
</div>
|
||
<div class="spacer"></div>
|
||
`;
|
||
statusTimeline.appendChild(step);
|
||
return;
|
||
}
|
||
|
||
// Si l'utilisateur peut envoyer l'intervention vers ce statut → bouton coloré cliquable
|
||
if (isActionTarget && isAllowed) {
|
||
const hexColor = STATUS_COLORS[status.key] || '#757575';
|
||
const textColor = STATUS_TEXT_DARK.has(status.key) ? '#000000' : '#ffffff';
|
||
const isOperator = Array.isArray(userRoles) && userRoles.includes('operator');
|
||
const isRecommended = isOperator && status.key === recommendedNextStatus;
|
||
const recommendedClass = isRecommended ? ' circle-recommended' : '';
|
||
const transLabel = status.transition_label || status.label;
|
||
|
||
step.innerHTML = `
|
||
<button type="button"
|
||
id="status_${status.key}"
|
||
class="circle circle-action${recommendedClass}"
|
||
style="background-color: ${hexColor} !important; color: ${textColor} !important;"
|
||
data-status="${status.key}"
|
||
title="${transLabel}"
|
||
data-bs-toggle="tooltip" data-bs-placement="bottom">
|
||
${index + 1}
|
||
</button>
|
||
<div class="steplabel">${label}</div>
|
||
`;
|
||
} else if (index === currentIndex) {
|
||
// Cercle du statut actuel : coloré + bordure distincte
|
||
const hexColor = STATUS_COLORS[status.key] || '#757575';
|
||
const textColor = STATUS_TEXT_DARK.has(status.key) ? '#000000' : '#ffffff';
|
||
step.innerHTML = `
|
||
<div id="status_${status.key}" class="circle circle-current" style="background-color: ${hexColor} !important; box-shadow: 0 0 0 3px ${hexColor} !important; color: ${textColor} !important;">
|
||
${index + 1}
|
||
</div>
|
||
<div class="steplabel">${label}</div>
|
||
`;
|
||
} else {
|
||
// Cercle passif (passé ou transition non autorisée)
|
||
step.innerHTML = `
|
||
<div id="status_${status.key}" class="circle ${status.key}">
|
||
${index + 1}
|
||
</div>
|
||
<div class="steplabel">${label}</div>
|
||
`;
|
||
}
|
||
|
||
statusTimeline.appendChild(step);
|
||
});
|
||
|
||
|
||
// Initialize Bootstrap tooltips on clickable status circles
|
||
if (statusTimeline) {
|
||
statusTimeline.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(el => {
|
||
new bootstrap.Tooltip(el);
|
||
});
|
||
|
||
// Auto-scroll horizontally to center the current status circle
|
||
const currentCircle = statusTimeline.querySelector('.circle-current');
|
||
if (currentCircle) {
|
||
const step = currentCircle.closest('.step');
|
||
if (step) {
|
||
const scrollLeft = step.offsetLeft - (statusTimeline.clientWidth / 2) + (step.offsetWidth / 2);
|
||
statusTimeline.scrollLeft = Math.max(0, scrollLeft);
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
// $('.circle.action').on('click', function () {
|
||
// const newStatus = $(this).attr('status');
|
||
|
||
// $.ajax({
|
||
// url: `/interventions/${interventionId}/update-status/`,
|
||
// method: 'POST',
|
||
// headers: {'X-CSRFToken': csrftoken},
|
||
// data: {
|
||
// intervention_id: interventionId,
|
||
// new_status: newStatus
|
||
// },
|
||
// success: function (response) {
|
||
// // alert("Statut mis à jour avec succès !");
|
||
// location.reload(); // Recharge la page actuelle
|
||
// },
|
||
// error: function (xhr) {
|
||
// alert("Erreur lors de la mise à jour : " + xhr.responseText);
|
||
// }
|
||
// });
|
||
// });
|
||
|
||
|
||
|
||
let pendingStatus = null;
|
||
let pendingExtraData = null;
|
||
|
||
// Period validation modal
|
||
const periodValidationModalEl = document.getElementById('periodValidationModal');
|
||
let periodValidationModal = null;
|
||
let periodValidationMessageEl = null;
|
||
let confirmPeriodValidationBtn = null;
|
||
|
||
if (periodValidationModalEl) {
|
||
periodValidationModal = bootstrap.Modal.getOrCreateInstance(periodValidationModalEl);
|
||
periodValidationMessageEl = document.getElementById('periodValidationMessage');
|
||
confirmPeriodValidationBtn = document.getElementById('confirmPeriodValidationBtn');
|
||
|
||
if (confirmPeriodValidationBtn) {
|
||
confirmPeriodValidationBtn.addEventListener('click', function () {
|
||
if (!pendingStatus) return;
|
||
const extra = pendingExtraData || {};
|
||
periodValidationModal.hide();
|
||
updateStatus(pendingStatus, extra);
|
||
pendingStatus = null;
|
||
pendingExtraData = null;
|
||
});
|
||
}
|
||
}
|
||
|
||
// Correction reason modal
|
||
const correctionReasonModalEl = document.getElementById('correctionReasonModal');
|
||
let correctionReasonModal = null;
|
||
let reasonForCorrectionInput = null;
|
||
let confirmCorrectionReasonBtn = null;
|
||
|
||
if (correctionReasonModalEl) {
|
||
correctionReasonModal = bootstrap.Modal.getOrCreateInstance(correctionReasonModalEl);
|
||
reasonForCorrectionInput = document.getElementById('reasonForCorrectionInput');
|
||
confirmCorrectionReasonBtn = document.getElementById('confirmCorrectionReason');
|
||
|
||
if (confirmCorrectionReasonBtn) {
|
||
confirmCorrectionReasonBtn.addEventListener('click', function () {
|
||
const reason = reasonForCorrectionInput ? reasonForCorrectionInput.value.trim() : '';
|
||
|
||
if (!reason) {
|
||
const errorEl = document.getElementById('correctionReasonError');
|
||
if (errorEl) {
|
||
errorEl.textContent = gettext('Veuillez indiquer une raison de correction.');
|
||
errorEl.classList.remove('d-none');
|
||
}
|
||
return;
|
||
}
|
||
|
||
correctionReasonModal.hide();
|
||
// Sauvegarder la raison et changer le statut
|
||
saveCorrectionReasonThenUpdateStatus(reason);
|
||
});
|
||
}
|
||
|
||
// Nettoyer la modale quand elle se ferme
|
||
if (correctionReasonModalEl) {
|
||
correctionReasonModalEl.addEventListener('hidden.bs.modal', function () {
|
||
if (reasonForCorrectionInput) {
|
||
reasonForCorrectionInput.value = '';
|
||
}
|
||
const errorEl = document.getElementById('correctionReasonError');
|
||
if (errorEl) {
|
||
errorEl.classList.add('d-none');
|
||
errorEl.textContent = '';
|
||
}
|
||
pendingStatus = null;
|
||
});
|
||
}
|
||
}
|
||
|
||
// Correction details modal
|
||
const correctionDetailsModalEl = document.getElementById('correctionDetailsModal');
|
||
let correctionDetailsModal = null;
|
||
let correctionDetailsInput = null;
|
||
let confirmCorrectionDetailsBtn = null;
|
||
|
||
if (correctionDetailsModalEl) {
|
||
correctionDetailsModal = bootstrap.Modal.getOrCreateInstance(correctionDetailsModalEl);
|
||
correctionDetailsInput = document.getElementById('correctionDetailsInput');
|
||
confirmCorrectionDetailsBtn = document.getElementById('confirmCorrectionDetails');
|
||
|
||
if (confirmCorrectionDetailsBtn) {
|
||
confirmCorrectionDetailsBtn.addEventListener('click', function () {
|
||
const details = correctionDetailsInput ? correctionDetailsInput.value.trim() : '';
|
||
|
||
if (!details) {
|
||
const errorEl = document.getElementById('correctionDetailsError');
|
||
if (errorEl) {
|
||
errorEl.textContent = gettext('Veuillez indiquer les détails de la correction.');
|
||
errorEl.classList.remove('d-none');
|
||
}
|
||
return;
|
||
}
|
||
|
||
correctionDetailsModal.hide();
|
||
// Sauvegarder les détails et changer le statut
|
||
saveCorrectionDetailsThenUpdateStatus(details);
|
||
});
|
||
}
|
||
|
||
// Nettoyer la modale quand elle se ferme
|
||
if (correctionDetailsModalEl) {
|
||
correctionDetailsModalEl.addEventListener('hidden.bs.modal', function () {
|
||
if (correctionDetailsInput) {
|
||
correctionDetailsInput.value = '';
|
||
}
|
||
const errorEl = document.getElementById('correctionDetailsError');
|
||
if (errorEl) {
|
||
errorEl.classList.add('d-none');
|
||
errorEl.textContent = '';
|
||
}
|
||
pendingStatus = null;
|
||
});
|
||
}
|
||
}
|
||
|
||
// Modal d'avertissement : aucune prestation encodée au passage à 'Traité'
|
||
const warnNoPrestationModalEl = document.getElementById('warnNoPrestationModal');
|
||
if (warnNoPrestationModalEl) {
|
||
const confirmWarnNoPrestationBtn = document.getElementById('confirmWarnNoPrestationBtn');
|
||
if (confirmWarnNoPrestationBtn) {
|
||
confirmWarnNoPrestationBtn.addEventListener('click', function () {
|
||
const statusToSet = pendingStatus;
|
||
pendingStatus = null;
|
||
pendingExtraData = null;
|
||
bootstrap.Modal.getInstance(warnNoPrestationModalEl).hide();
|
||
if (statusToSet) {
|
||
updateStatus(statusToSet);
|
||
}
|
||
});
|
||
}
|
||
warnNoPrestationModalEl.addEventListener('hidden.bs.modal', function () {
|
||
pendingStatus = null;
|
||
pendingExtraData = null;
|
||
});
|
||
}
|
||
|
||
$('#statusTimeline').on('click', 'button', function () {
|
||
const newStatus = $(this).data('status');
|
||
|
||
if (newStatus === 'canceled') {
|
||
pendingStatus = newStatus;
|
||
const modal = new bootstrap.Modal(document.getElementById('cancelConfirmModal'));
|
||
modal.show();
|
||
} else if (newStatus === 'on_pause' && pauseReasonModal) {
|
||
pauseReasonModal.show();
|
||
} else if (newStatus === 'to_be_corrected') {
|
||
// Afficher la modale pour demander la raison de correction
|
||
pendingStatus = newStatus;
|
||
const correctionReasonModal = document.getElementById('correctionReasonModal');
|
||
if (correctionReasonModal) {
|
||
const modal = new bootstrap.Modal(correctionReasonModal);
|
||
modal.show();
|
||
} else {
|
||
// Fallback si la modale n'existe pas
|
||
updateStatus(newStatus);
|
||
}
|
||
} else if (newStatus === 'corrected') {
|
||
// Afficher la modale pour les détails de correction
|
||
pendingStatus = newStatus;
|
||
const correctionDetailsModal = document.getElementById('correctionDetailsModal');
|
||
if (correctionDetailsModal) {
|
||
const modal = new bootstrap.Modal(correctionDetailsModal);
|
||
modal.show();
|
||
} else {
|
||
// Fallback si la modale n'existe pas
|
||
updateStatus(newStatus);
|
||
}
|
||
} else if (newStatus === 'validated' && typeof periodValidationEnabled !== 'undefined' && periodValidationEnabled) {
|
||
// Intercepter la validation lorsque la validation par période est active
|
||
pendingStatus = newStatus;
|
||
const isFinal = (typeof periodIsFinal !== 'undefined' && periodIsFinal);
|
||
if (periodValidationModal && periodValidationMessageEl) {
|
||
if (isFinal) {
|
||
periodValidationMessageEl.textContent = gettext("Vous êtes sur le point de valider définitivement l'intervention (dernière période).");
|
||
pendingExtraData = {}; // validation définitive
|
||
} else {
|
||
periodValidationMessageEl.textContent = gettext("Cette action repassera l'intervention au statut 'À planifier' et passera à la période suivante.");
|
||
pendingExtraData = { period_validation_rollover: 1 };
|
||
}
|
||
periodValidationModal.show();
|
||
} else {
|
||
// Fallback simple si la modale n'est pas disponible
|
||
if (isFinal) {
|
||
updateStatus(newStatus);
|
||
} else if (confirm(gettext("Cette action repassera l'intervention à 'À planifier' et passera à la période suivante. Confirmer ?"))) {
|
||
updateStatus(newStatus, { period_validation_rollover: 1 });
|
||
}
|
||
pendingStatus = null;
|
||
pendingExtraData = null;
|
||
}
|
||
} else if (newStatus === 'processed' && typeof contractWarnNoPrestationOnProcessed !== 'undefined' && contractWarnNoPrestationOnProcessed) {
|
||
// Avertissement si aucune prestation n'a été encodée
|
||
let hasPrestations = (typeof hasPrestationsEncoded !== 'undefined') && hasPrestationsEncoded;
|
||
const contractTableBody = document.querySelector('#contractTable tbody');
|
||
if (contractTableBody) {
|
||
hasPrestations = contractTableBody.querySelectorAll('tr[data-contract-post-id]').length > 0;
|
||
}
|
||
if (!hasPrestations) {
|
||
pendingStatus = newStatus;
|
||
pendingExtraData = null;
|
||
const warnModalEl = document.getElementById('warnNoPrestationModal');
|
||
if (warnModalEl) {
|
||
bootstrap.Modal.getOrCreateInstance(warnModalEl).show();
|
||
} else {
|
||
if (confirm(gettext("Aucune prestation n'a été encodée. Voulez-vous quand même passer au statut « Traité » ?"))) {
|
||
updateStatus(newStatus);
|
||
}
|
||
pendingStatus = null;
|
||
}
|
||
} else {
|
||
updateStatus(newStatus);
|
||
}
|
||
} else if (typeof isMobileTimeline !== 'undefined' && isMobileTimeline) {
|
||
// Mobile : modale de confirmation avant changement de statut
|
||
const confirmModalEl = document.getElementById('mobileStatusConfirmModal');
|
||
if (confirmModalEl) {
|
||
const statusObj = STATUS_CHOICES.find(s => s.key === newStatus);
|
||
const transLabel = statusObj && statusObj.transition_label ? statusObj.transition_label : (statusObj ? statusObj.label : newStatus);
|
||
const msgEl = document.getElementById('mobileStatusConfirmMessage');
|
||
if (msgEl) {
|
||
msgEl.textContent = transLabel + ' ?';
|
||
}
|
||
pendingStatus = newStatus;
|
||
pendingExtraData = null;
|
||
const confirmModal = bootstrap.Modal.getOrCreateInstance(confirmModalEl);
|
||
confirmModal.show();
|
||
} else {
|
||
updateStatus(newStatus);
|
||
}
|
||
} else if (newStatus === 'finished' && typeof photosRequired !== 'undefined' && photosRequired) {
|
||
// Vérifier la présence d'au moins une photo (post 'to_be_processed' si date connue)
|
||
const docContainer = document.getElementById('documentsContainer');
|
||
let hasDocuments = false;
|
||
if (docContainer) {
|
||
const cards = Array.from(docContainer.querySelectorAll('[id^="document-card-"], [id^="doc-thumb-"]'));
|
||
const since = (typeof photosRequiredSince !== 'undefined' && photosRequiredSince)
|
||
? new Date(photosRequiredSince).getTime()
|
||
: null;
|
||
if (since) {
|
||
hasDocuments = cards.some(card => {
|
||
const uploadDate = card.dataset.uploadDate;
|
||
return uploadDate && new Date(uploadDate).getTime() >= since;
|
||
});
|
||
} else {
|
||
hasDocuments = cards.length > 0;
|
||
}
|
||
}
|
||
if (!hasDocuments) {
|
||
const msg = (typeof photosRequiredSince !== 'undefined' && photosRequiredSince)
|
||
? gettext('Au moins une photo ajoutée après le passage au statut « À traiter » est obligatoire pour terminer cette intervention.')
|
||
: gettext('Au moins une photo est obligatoire pour terminer cette intervention.');
|
||
let alertContainer = document.getElementById('documentsAlertContainer');
|
||
if (!alertContainer) {
|
||
alertContainer = document.getElementById('statusAlertContainer');
|
||
}
|
||
if (alertContainer) {
|
||
alertContainer.innerHTML = '<div class="alert alert-warning alert-dismissible fade show" role="alert"><i class="bi bi-camera-fill me-2"></i>' +
|
||
msg + '<button type="button" class="btn-close" data-bs-dismiss="alert"></button></div>';
|
||
const docSection = document.getElementById('section-documents') || alertContainer;
|
||
docSection.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||
} else {
|
||
alert(msg);
|
||
}
|
||
return;
|
||
}
|
||
updateStatus(newStatus);
|
||
} else {
|
||
updateStatus(newStatus);
|
||
}
|
||
});
|
||
|
||
// Mobile : confirmer le changement de statut
|
||
$(document).on('click', '#confirmMobileStatusBtn', function () {
|
||
const confirmModalEl = document.getElementById('mobileStatusConfirmModal');
|
||
if (confirmModalEl) {
|
||
const modalInstance = bootstrap.Modal.getInstance(confirmModalEl);
|
||
if (modalInstance) modalInstance.hide();
|
||
}
|
||
if (pendingStatus) {
|
||
const extra = pendingExtraData || {};
|
||
updateStatus(pendingStatus, extra);
|
||
pendingStatus = null;
|
||
pendingExtraData = null;
|
||
}
|
||
});
|
||
|
||
|
||
// Afficher l'astérisque obligatoire sur le commentaire quand le type 'Autre' est sélectionné
|
||
$(document).on('change', '#cancellationTypeSelect', function () {
|
||
const isOther = $(this).val() === 'other';
|
||
const reqMark = document.getElementById('cancellationReasonRequired');
|
||
if (reqMark) reqMark.classList.toggle('d-none', !isOther);
|
||
});
|
||
|
||
$('#confirmCancelBtn').on('click', function () {
|
||
if (pendingStatus) {
|
||
// Récupérer le type et la raison d'annulation
|
||
const cancellationType = $('#cancellationTypeSelect').val();
|
||
const cancellationReason = $('#cancellationReasonInput').val().trim();
|
||
const errorEl = document.getElementById('cancellationReasonError');
|
||
|
||
// Valider que le type est sélectionné
|
||
if (!cancellationType) {
|
||
if (errorEl) {
|
||
errorEl.classList.remove('d-none');
|
||
errorEl.textContent = gettext("Le type d'annulation est obligatoire.");
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Le commentaire est obligatoire uniquement pour le motif 'Autre'
|
||
if (cancellationType === 'other' && !cancellationReason) {
|
||
if (errorEl) {
|
||
errorEl.classList.remove('d-none');
|
||
errorEl.textContent = gettext("Un commentaire est obligatoire pour le motif \"Autre\".");
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Cacher le message d'erreur si présent
|
||
if (errorEl) {
|
||
errorEl.classList.add('d-none');
|
||
errorEl.textContent = '';
|
||
}
|
||
|
||
// Envoyer le statut avec le type et la raison d'annulation
|
||
updateStatus(pendingStatus, { cancellation_type: cancellationType, cancellation_reason: cancellationReason });
|
||
pendingStatus = null;
|
||
|
||
// Fermer la modale
|
||
const modalElement = document.getElementById('cancelConfirmModal');
|
||
const modalInstance = bootstrap.Modal.getInstance(modalElement);
|
||
if (modalInstance) {
|
||
modalInstance.hide();
|
||
}
|
||
|
||
// Réinitialiser les champs après fermeture
|
||
$('#cancellationTypeSelect').val('');
|
||
$('#cancellationReasonInput').val('');
|
||
const reqMark = document.getElementById('cancellationReasonRequired');
|
||
if (reqMark) reqMark.classList.add('d-none');
|
||
}
|
||
});
|
||
|
||
/**
|
||
* Sauvegarde la raison de correction, puis change le statut
|
||
*/
|
||
function saveCorrectionReasonThenUpdateStatus(reason) {
|
||
$.ajax({
|
||
url: `/interventions/${interventionId}/update-correction-reason/`,
|
||
method: 'POST',
|
||
headers: {
|
||
'X-CSRFToken': csrftoken,
|
||
'Content-Type': 'application/json'
|
||
},
|
||
data: JSON.stringify({
|
||
reason_for_correction: reason
|
||
}),
|
||
success: function (response) {
|
||
// Raison sauvegardée, maintenant changer le statut
|
||
updateStatus('to_be_corrected');
|
||
},
|
||
error: function (xhr) {
|
||
const errorEl = document.getElementById('correctionReasonError');
|
||
if (errorEl) {
|
||
try {
|
||
const error = JSON.parse(xhr.responseText);
|
||
errorEl.textContent = error.error || gettext("Erreur lors de la sauvegarde");
|
||
} catch (e) {
|
||
errorEl.textContent = gettext("Erreur lors de la sauvegarde");
|
||
}
|
||
errorEl.classList.remove('d-none');
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Sauvegarde les détails de correction, puis change le statut
|
||
*/
|
||
function saveCorrectionDetailsThenUpdateStatus(details) {
|
||
$.ajax({
|
||
url: `/interventions/${interventionId}/update-correction-details/`,
|
||
method: 'POST',
|
||
headers: {
|
||
'X-CSRFToken': csrftoken,
|
||
'Content-Type': 'application/json'
|
||
},
|
||
data: JSON.stringify({
|
||
correction_details: details
|
||
}),
|
||
success: function (response) {
|
||
// Détails sauvegardés, maintenant changer le statut
|
||
updateStatus('corrected');
|
||
},
|
||
error: function (xhr) {
|
||
const errorEl = document.getElementById('correctionDetailsError');
|
||
if (errorEl) {
|
||
try {
|
||
const error = JSON.parse(xhr.responseText);
|
||
errorEl.textContent = error.error || gettext("Erreur lors de la sauvegarde");
|
||
} catch (e) {
|
||
errorEl.textContent = gettext("Erreur lors de la sauvegarde");
|
||
}
|
||
errorEl.classList.remove('d-none');
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
function updateStatus(newStatus, extraData = {}) {
|
||
const payload = Object.assign({
|
||
intervention_id: interventionId,
|
||
new_status: newStatus,
|
||
}, extraData || {});
|
||
|
||
$.ajax({
|
||
url: `/interventions/${interventionId}/update-status/`,
|
||
method: 'POST',
|
||
headers: {'X-CSRFToken': csrftoken},
|
||
data: payload,
|
||
success: function (response) {
|
||
// Check if email failed but status was updated successfully
|
||
if (response.email_failed && response.email_warning) {
|
||
showStatusAlert(response.email_warning, 'warning');
|
||
}
|
||
|
||
const _isOperator = Array.isArray(userRoles) && userRoles.includes('operator');
|
||
|
||
// Opérateurs : redirection serveur ou page d'accueil pour les brouillons
|
||
if (_isOperator) {
|
||
if (response.redirect_url && response.redirect_url !== '') {
|
||
window.location.href = response.redirect_url;
|
||
return;
|
||
}
|
||
if (['in_preparation', 'to_be_approved', 'to_be_planned'].includes(currentStatus)) {
|
||
window.location.href = `/`;
|
||
return;
|
||
}
|
||
}
|
||
|
||
// Autres rôles (et opérateurs hors brouillon) : rester sur la page
|
||
location.reload();
|
||
},
|
||
error: function (xhr) {
|
||
let message = gettext("Erreur lors de la mise à jour");
|
||
try {
|
||
const errorData = JSON.parse(xhr.responseText);
|
||
if (errorData.message) {
|
||
message = errorData.message;
|
||
}
|
||
} catch (e) {
|
||
// If parsing fails, use the response text directly if available and not HTML
|
||
if (xhr.responseText && !xhr.responseText.trim().startsWith('<')) {
|
||
message = xhr.responseText;
|
||
}
|
||
}
|
||
showStatusAlert(message, 'danger');
|
||
}
|
||
});
|
||
}
|
||
|
||
function showStatusAlert(message, type = 'danger') {
|
||
// Créer un conteneur d'alerte s'il n'existe pas
|
||
let alertContainer = document.getElementById('statusUpdateAlertContainer');
|
||
if (!alertContainer) {
|
||
alertContainer = document.createElement('div');
|
||
alertContainer.id = 'statusUpdateAlertContainer';
|
||
alertContainer.style.position = 'fixed';
|
||
alertContainer.style.top = '20px';
|
||
alertContainer.style.left = '50%';
|
||
alertContainer.style.transform = 'translateX(-50%)';
|
||
alertContainer.style.zIndex = '9999';
|
||
alertContainer.style.minWidth = '300px';
|
||
alertContainer.style.textAlign = 'center';
|
||
document.body.appendChild(alertContainer);
|
||
}
|
||
|
||
// Créer l'élément d'alerte
|
||
const alertEl = document.createElement('div');
|
||
alertEl.className = `alert alert-${type} alert-dismissible fade show`;
|
||
alertEl.role = 'alert';
|
||
alertEl.innerHTML = `
|
||
${message}
|
||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||
`;
|
||
|
||
alertContainer.appendChild(alertEl);
|
||
|
||
// Retirer l'alerte après 5 secondes (si pas fermée manuellement)
|
||
setTimeout(() => {
|
||
if (alertEl.parentNode) {
|
||
alertEl.remove();
|
||
}
|
||
}, 5000);
|
||
}
|