loko/streetup/contracts/templates/contracts/teams_index.html
2026-07-22 14:48:40 +02:00

812 lines
34 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends "base.html" %}
{% load i18n %}
{% load static %}
{% load roles %}
{% block head %}
<link rel="stylesheet" href="{% static 'common/jquery-ui.min.css' %}">
<style>
#teams-board {
overflow-x: auto;
flex-wrap: nowrap;
}
.team-col {
min-width: 260px;
max-width: 320px;
flex: 0 0 auto;
}
.sortable-members {
min-height: 60px;
padding: 4px;
border-radius: 4px;
transition: background-color 0.2s;
}
.sortable-members.ui-sortable-over {
background-color: #e8f0fe;
}
.unassigned-col .sortable-members.ui-sortable-over {
background-color: #fff3cd;
}
.unassigned-col .team-card .card-header {
background: linear-gradient(135deg, #fff3cd 0%, #ffeaa7 100%);
}
.member-card {
background: #fff;
border: 1px solid #dee2e6;
border-radius: 6px;
padding: 8px 10px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 6px;
transition: box-shadow 0.15s;
}
.member-card:hover {
box-shadow: 0 2px 6px rgba(0,0,0,.12);
}
.member-card .drag-handle {
cursor: grab;
color: #adb5bd;
flex-shrink: 0;
}
.member-card .drag-handle:active {
cursor: grabbing;
}
.member-info {
flex: 1;
overflow: hidden;
}
.member-info strong {
display: block;
font-size: 0.9rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.member-info small {
color: #6c757d;
font-size: 0.78rem;
}
.member-actions {
display: flex;
gap: 4px;
flex-shrink: 0;
}
.ui-sortable-placeholder {
border: 2px dashed #86b7fe;
background: #f0f5ff;
border-radius: 6px;
visibility: visible !important;
height: 44px;
margin-bottom: 6px;
}
.ui-sortable-helper {
box-shadow: 0 4px 12px rgba(0,0,0,.2);
rotate: 2deg;
opacity: 0.9;
}
.team-card {
height: 100%;
}
.team-card .card-header {
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
}
.empty-placeholder {
color: #adb5bd;
font-style: italic;
font-size: 0.85rem;
text-align: center;
padding: 12px 0;
pointer-events: none;
user-select: none;
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid py-3">
<!-- En-tête -->
<div class="d-flex justify-content-between align-items-center mb-4 flex-wrap gap-2">
<h4 class="mb-0">
<i class="bi bi-people-fill me-2 text-primary"></i>{% trans "Gestion des équipes" %}
</h4>
</div>
{% if not teams %}
<div class="alert alert-info">
<i class="bi bi-info-circle me-2"></i>{% trans "Aucune équipe accessible." %}
</div>
{% else %}
<!-- Tableau Kanban des équipes -->
<div class="d-flex gap-3 pb-3" id="teams-board">
<!-- Colonne : membres non assignés -->
<div class="team-col unassigned-col" id="team-col-unassigned">
<div class="card team-card">
<div class="card-header d-flex justify-content-between align-items-start py-2">
<div class="overflow-hidden me-2">
<div class="fw-semibold text-truncate">
<i class="bi bi-person-dash me-1"></i>{% trans "Non assignés" %}
</div>
<small class="text-muted">{% trans "Aucune équipe" %}</small>
</div>
{% if can_edit %}
<button class="btn btn-sm btn-outline-warning border-0"
title="{% trans 'Ajouter un membre non assigné' %}"
onclick="openAddMemberModal(null, '{% trans "Non assignés" %}', {% if user_company %}{{ user_company.id }}{% else %}null{% endif %})">
<i class="bi bi-person-plus"></i>
</button>
{% endif %}
</div>
<div class="card-body p-2">
<ul class="sortable-members list-unstyled mb-0"
data-team-id=""
id="members-unassigned">
{% for member in unassigned_members %}
<li class="member-card mb-2" data-member-id="{{ member.id }}"{% if member.user %} data-user-id="{{ member.user.id }}"{% endif %}>
{% if can_edit %}
<i class="bi bi-grip-vertical drag-handle fs-5"></i>
{% endif %}
<div class="member-info">
<strong>{{ member.name }}</strong>
{% if member.user %}
<small>{{ member.user.get_full_name|default:member.user.username }}</small>
{% endif %}
</div>
{% if can_edit %}
<div class="member-actions">
<button class="btn btn-sm btn-outline-secondary"
title="{% trans 'Assigner à une équipe' %}"
onclick="openMoveMemberModal({{ member.id }}, '{{ member.name|escapejs }}', null)">
<i class="bi bi-arrow-left-right"></i>
</button>
<button class="btn btn-sm btn-outline-danger"
title="{% trans 'Supprimer ce membre' %}"
onclick="deleteMember({{ member.id }}, '{{ member.name|escapejs }}')">
<i class="bi bi-trash"></i>
</button>
</div>
{% endif %}
</li>
{% empty %}
<li class="empty-placeholder" id="empty-unassigned">
<i class="bi bi-inbox d-block mb-1" style="font-size:1.2rem;"></i>
{% trans "Aucun membre" %}
</li>
{% endfor %}
</ul>
</div>
<div class="card-footer text-muted small py-1 px-2">
<span id="count-unassigned">{{ unassigned_members.count }}</span>
{% trans "membre(s)" %}
</div>
</div>
</div>
<!-- Colonnes des équipes -->
{% for team in teams %}
<div class="team-col" id="team-col-{{ team.id }}">
<div class="card team-card">
<div class="card-header d-flex justify-content-between align-items-start py-2">
<div class="overflow-hidden me-2">
<div class="fw-semibold text-truncate">{{ team.name }}</div>
<small class="text-muted">{{ team.company.name }}</small>
</div>
<div class="d-flex flex-shrink-0">
{% if team.active_interventions_count > 0 %}
<button class="btn btn-sm btn-outline-info border-0"
title="{% trans 'Voir les interventions assignées' %} ({{ team.active_interventions_count }})"
onclick="openTeamInterventionsModal({{ team.id }}, '{{ team.name|escapejs }}')">
<i class="bi bi-list-task"></i>
<!-- <span class="badge bg-info text-dark ms-1" style="font-size:0.7rem;">{{ team.active_interventions_count }}</span> -->
</button>
{% endif %}
{% if can_edit %}
<button class="btn btn-sm btn-outline-secondary border-0"
title="{% trans 'Ajouter un membre' %}"
onclick="openAddMemberModal({{ team.id }}, '{{ team.name|escapejs }}', {{ team.company.id }})">
<i class="bi bi-person-plus"></i>
</button>
{% endif %}
</div>
</div>
<div class="card-body p-2">
<ul class="sortable-members list-unstyled mb-0"
data-team-id="{{ team.id }}"
id="members-{{ team.id }}">
{% for member in team.members.all %}
<li class="member-card mb-2" data-member-id="{{ member.id }}"{% if member.user %} data-user-id="{{ member.user.id }}"{% endif %}>
{% if can_edit %}
<i class="bi bi-grip-vertical drag-handle fs-5"></i>
{% endif %}
<div class="member-info">
<strong>{{ member.name }}</strong>
{% if member.user %}
<small>{{ member.user.get_full_name|default:member.user.username }}</small>
{% endif %}
</div>
{% if can_edit %}
<div class="member-actions">
<button class="btn btn-sm btn-outline-secondary"
title="{% trans 'Déplacer vers une autre équipe' %}"
onclick="openMoveMemberModal({{ member.id }}, '{{ member.name|escapejs }}', {{ team.id }})">
<i class="bi bi-arrow-left-right"></i>
</button>
<button class="btn btn-sm btn-outline-danger"
title="{% trans 'Supprimer ce membre' %}"
onclick="deleteMember({{ member.id }}, '{{ member.name|escapejs }}')">
<i class="bi bi-trash"></i>
</button>
</div>
{% endif %}
</li>
{% empty %}
<li class="empty-placeholder" id="empty-{{ team.id }}">
<i class="bi bi-inbox d-block mb-1" style="font-size:1.2rem;"></i>
{% trans "Aucun membre" %}
</li>
{% endfor %}
</ul>
</div>
<div class="card-footer text-muted small py-1 px-2">
<span id="count-{{ team.id }}">{{ team.members.count }}</span>
{% trans "membre(s)" %}
</div>
</div>
</div>
{% endfor %}
</div>
{% endif %}
</div>
<!-- ============================================================
Modales (affichées uniquement si droits d'édition)
============================================================ -->
{% if can_edit %}
<!-- Modale : Ajouter un membre -->
<div class="modal fade" id="addMemberModal" tabindex="-1" aria-labelledby="addMemberModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="addMemberModalLabel">
<i class="bi bi-person-plus me-2"></i><span id="addMemberModalTitle">{% trans "Ajouter un membre" %}</span>
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" id="addMemberTeamId">
<div class="mb-3">
<label class="form-label fw-semibold">{% trans "Nom affiché" %} <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="newMemberName" placeholder="{% trans 'Ex : Jean Dupont' %}">
</div>
<div class="mb-3">
<label class="form-label fw-semibold">{% trans "Compte utilisateur" %} <span class="text-danger">*</span></label>
<select class="form-select" id="newMemberUser">
<option value="">— {% trans "Sélectionner un utilisateur" %} —</option>
{% for u in available_users %}
<option value="{{ u.id }}" data-company-id="{{ u.config.company.id|default:'' }}">
{% if u.get_full_name %}{{ u.get_full_name }} ({{ u.username }}){% else %}{{ u.username }}{% endif %}
</option>
{% endfor %}
</select>
</div>
<div id="addMemberError" class="alert alert-danger d-none"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Annuler" %}</button>
<button type="button" class="btn btn-primary" onclick="createMember()">
<i class="bi bi-person-plus me-1"></i>{% trans "Ajouter" %}
</button>
</div>
</div>
</div>
</div>
<!-- Modale : Éditer un membre (nom + équipe) -->
<div class="modal fade" id="moveMemberModal" tabindex="-1" aria-labelledby="moveMemberModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="moveMemberModalLabel">
<i class="bi bi-pencil-square me-2"></i>{% trans "Modifier le membre" %}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" id="moveMemberId">
<input type="hidden" id="moveOldTeamId">
<div class="mb-3">
<label class="form-label fw-semibold">{% trans "Nom affiché" %} <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="editMemberName">
</div>
<div class="mb-3">
<label class="form-label fw-semibold">{% trans "Équipe" %}</label>
<select class="form-select" id="moveToTeamId">
<option value="">— {% trans "Ne pas modifier l'équipe" %} —</option>
<option value="unassign">&#9747; {% trans "Désassigner (aucune équipe)" %}</option>
<optgroup label="{% trans 'Assigner à une équipe' %}">
{% for team in teams %}
<option value="{{ team.id }}">{{ team.name }} — {{ team.company.name }}</option>
{% endfor %}
</optgroup>
</select>
</div>
<div id="moveMemberError" class="alert alert-danger d-none"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Annuler" %}</button>
<button type="button" class="btn btn-primary" onclick="confirmMoveMember()">
<i class="bi bi-check2 me-1"></i>{% trans "Enregistrer" %}
</button>
</div>
</div>
</div>
</div>
{% endif %}
<!-- Modale : Interventions assignées à une équipe -->
<div class="modal fade" id="teamInterventionsModal" tabindex="-1" aria-labelledby="teamInterventionsModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="teamInterventionsModalLabel">
<i class="bi bi-list-task me-2"></i><span id="teamInterventionsModalTitle">{% trans "Interventions" %}</span>
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-0" id="teamInterventionsModalBody">
<div class="text-center py-5">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">{% trans "Chargement..." %}</span>
</div>
</div>
</div>
<div class="modal-footer text-muted small" id="teamInterventionsModalFooter"></div>
</div>
</div>
</div>
<!-- Modale : Résumé d’intervention -->
<div class="modal fade" id="teamInterventionSummaryModal" tabindex="-1" aria-labelledby="teamInterventionSummaryModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="teamInterventionSummaryModalLabel"
data-base-title="{% trans 'Résumé intervention' %}">
{% trans "Résumé intervention" %}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body" id="teamInterventionSummaryModalBody"></div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" id="teamInterventionSummaryBackBtn">
<i class="bi bi-arrow-left me-1"></i>{% trans "Retour aux interventions" %}
</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Fermer" %}</button>
</div>
</div>
</div>
</div>
{% endblock content %}
{% block script %}
<script src="{% static 'common/jquery-ui.min.js' %}"></script>
<script>
const CSRF_TOKEN = '{{ csrf_token }}';
function postJSON(url, data, onSuccess, onError) {
$.ajax({
url: url,
type: 'POST',
contentType: 'application/json',
headers: { 'X-CSRFToken': CSRF_TOKEN },
data: JSON.stringify(data),
success: function(resp) {
if (resp.success) {
onSuccess(resp);
} else {
onError(resp.error || "{% trans 'Une erreur est survenue.' %}");
}
},
error: function(xhr) {
let msg = "{% trans 'Une erreur est survenue.' %}";
try {
const r = JSON.parse(xhr.responseText);
if (r.error) msg = r.error;
} catch(e) {}
onError(msg);
}
});
}
{% if can_edit %}
// ───────────────────────────────────────────────
// Drag & drop entre équipes (jQuery UI Sortable)
// ───────────────────────────────────────────────
$(function () {
$('.sortable-members').sortable({
connectWith: '.sortable-members',
placeholder: 'ui-sortable-placeholder',
handle: '.drag-handle',
tolerance: 'pointer',
receive: function (event, ui) {
const memberId = ui.item.data('member-id');
const rawTeamId = $(this).data('team-id');
// rawTeamId === '' means unassigned column
const teamIdPayload = (rawTeamId === '' || rawTeamId === null || rawTeamId === undefined) ? null : rawTeamId;
// Get source team column ID
const rawOldTeamId = ui.sender.data('team-id');
const oldTeamIdPayload = (rawOldTeamId === '' || rawOldTeamId === null || rawOldTeamId === undefined) ? null : rawOldTeamId;
// Appel AJAX pour persister le déplacement
postJSON(
`/contracts/teams/members/${memberId}/move/`,
{ team_id: teamIdPayload, old_team_id: oldTeamIdPayload },
function(resp) {
// Mettre à jour les compteurs sans rechargement
updateCounters();
updateEmptyPlaceholders();
},
function(errMsg) {
// En cas d'erreur, annuler le déplacement visuel et recharger
alert(errMsg);
location.reload();
}
);
}
});
$('.sortable-members').disableSelection();
// Auto-fill member name when user is selected
$('#newMemberUser').on('change', function() {
const text = $(this).find('option:selected').text().trim();
if (text && $(this).val()) {
const name = text.split(' (')[0];
$('#newMemberName').val(name);
}
});
});
function updateCounters() {
$('.sortable-members').each(function() {
const rawId = $(this).data('team-id');
const colId = (rawId === '' || rawId === null || rawId === undefined) ? 'unassigned' : rawId;
const count = $(this).find('li.member-card').length;
$(`#count-${colId}`).text(count);
});
}
function updateEmptyPlaceholders() {
$('.sortable-members').each(function() {
const rawId = $(this).data('team-id');
const colId = (rawId === '' || rawId === null || rawId === undefined) ? 'unassigned' : rawId;
const hasMembers = $(this).find('li.member-card').length > 0;
const placeholder = $(`#empty-${colId}`);
if (hasMembers) {
placeholder.hide();
} else {
placeholder.show();
}
});
}
// ───────────────────────────────────────────────
// Éditer un membre via modale (nom + équipe)
// ───────────────────────────────────────────────
function openMoveMemberModal(memberId, memberName, currentTeamId) {
document.getElementById('moveMemberId').value = memberId;
document.getElementById('moveOldTeamId').value = (currentTeamId !== null && currentTeamId !== undefined) ? currentTeamId : '';
document.getElementById('editMemberName').value = memberName;
const sel = document.getElementById('moveToTeamId');
// Réinitialiser sur "ne pas modifier"
sel.value = '';
// Désactiver l'équipe courante + "désassigner" si déjà non assigné
for (const opt of sel.options) {
if (opt.value === 'unassign') {
opt.disabled = (currentTeamId === null || currentTeamId === undefined);
} else {
opt.disabled = (opt.value !== '' && opt.value == currentTeamId);
}
}
document.getElementById('moveMemberError').classList.add('d-none');
new bootstrap.Modal(document.getElementById('moveMemberModal')).show();
}
function confirmMoveMember() {
const memberId = document.getElementById('moveMemberId').value;
const oldTeamId = document.getElementById('moveOldTeamId').value;
const newName = document.getElementById('editMemberName').value.trim();
const selected = document.getElementById('moveToTeamId').value;
if (!newName) {
showError('moveMemberError', "{% trans 'Le nom du membre est requis.' %}");
return;
}
const payload = { name: newName };
payload.old_team_id = oldTeamId !== '' ? parseInt(oldTeamId) : null;
if (selected !== '') {
payload.team_id = selected === 'unassign' ? null : parseInt(selected);
}
bootstrap.Modal.getInstance(document.getElementById('moveMemberModal')).hide();
postJSON(
`/contracts/teams/members/${memberId}/update/`,
payload,
function() { location.reload(); },
function(err) { alert(err); }
);
}
// ───────────────────────────────────────────────
// Ajouter un membre
// ───────────────────────────────────────────────
function openAddMemberModal(teamId, teamName, teamCompanyId) {
document.getElementById('addMemberTeamId').value = teamId !== null ? teamId : '';
const title = teamId !== null
? `{% trans "Ajouter un membre" %} — ${teamName}`
: `{% trans "Ajouter un membre non assigné" %}`;
document.getElementById('addMemberModalTitle').textContent = title;
document.getElementById('newMemberName').value = '';
document.getElementById('newMemberUser').value = '';
// Disable users who are already in this team
const colId = teamId !== null ? teamId : 'unassigned';
const existingUserIds = [];
$(`#members-${colId} li.member-card`).each(function() {
const uid = $(this).data('user-id');
if (uid) {
existingUserIds.push(parseInt(uid));
}
});
const isUserAdmin = {% if is_admin %}true{% else %}false{% endif %};
const userCompanyId = {% if user_company %}{{ user_company.id }}{% else %}null{% endif %};
const sel = document.getElementById('newMemberUser');
for (const opt of sel.options) {
if (opt.value !== '') {
const userId = parseInt(opt.value);
const userCompany = opt.dataset.companyId ? parseInt(opt.dataset.companyId) : null;
const alreadyInTeam = existingUserIds.includes(userId);
let matchesCompany = true;
if (!isUserAdmin && userCompanyId !== null) {
matchesCompany = (userCompany === userCompanyId) ||
(teamCompanyId !== null && teamCompanyId !== undefined && userCompany === parseInt(teamCompanyId));
}
if (alreadyInTeam || !matchesCompany) {
opt.disabled = true;
if (!matchesCompany) {
opt.style.display = 'none';
} else {
opt.style.display = '';
}
} else {
opt.disabled = false;
opt.style.display = '';
}
}
}
document.getElementById('addMemberError').classList.add('d-none');
new bootstrap.Modal(document.getElementById('addMemberModal')).show();
}
function createMember() {
const name = document.getElementById('newMemberName').value.trim();
const userId = document.getElementById('newMemberUser').value;
const teamId = document.getElementById('addMemberTeamId').value;
document.getElementById('addMemberError').classList.add('d-none');
if (!name) { showError('addMemberError', "{% trans 'Le nom du membre est requis.' %}"); return; }
if (!userId) { showError('addMemberError', '{% trans "L\'utilisateur est requis."|escapejs %}'); return; }
postJSON(
'/contracts/teams/members/create/',
{ name: name, user_id: parseInt(userId), team_id: teamId ? parseInt(teamId) : null },
function() {
bootstrap.Modal.getInstance(document.getElementById('addMemberModal')).hide();
location.reload();
},
function(err) { showError('addMemberError', err); }
);
}
// ───────────────────────────────────────────────
// Supprimer un membre
// ───────────────────────────────────────────────
function deleteMember(memberId, memberName) {
if (!confirm(`{% trans "Supprimer le membre" %} « ${memberName} » ? {% trans "Cette action est irréversible." %}`)) return;
postJSON(
`/contracts/teams/members/${memberId}/delete/`,
{},
function() { location.reload(); },
function(err) { alert(err); }
);
}
// ───────────────────────────────────────────────
// Utilitaire
// ───────────────────────────────────────────────
function showError(elementId, message) {
const el = document.getElementById(elementId);
el.textContent = message;
el.classList.remove('d-none');
}
{% endif %}
// ───────────────────────────────────────────────
// Modale interventions de l'équipe
// ───────────────────────────────────────────────
let _currentTeamId = null;
let _currentTeamName = null;
function openTeamInterventionsModal(teamId, teamName) {
_currentTeamId = teamId;
_currentTeamName = teamName;
document.getElementById('teamInterventionsModalTitle').textContent = teamName;
const body = document.getElementById('teamInterventionsModalBody');
const footer = document.getElementById('teamInterventionsModalFooter');
body.innerHTML = `
<div class="text-center py-5">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">{% trans "Chargement..." %}</span>
</div>
</div>`;
footer.textContent = '';
new bootstrap.Modal(document.getElementById('teamInterventionsModal')).show();
fetch(`/contracts/teams/${teamId}/interventions/`, {
headers: { 'X-Requested-With': 'XMLHttpRequest' }
})
.then(r => r.json())
.then(data => renderTeamInterventions(data, body, footer))
.catch(() => {
body.innerHTML = `<div class="alert alert-danger m-3">{% trans "Impossible de charger les interventions." %}</div>`;
});
}
function renderTeamInterventions(data, body, footer) {
const interventions = data.interventions || [];
footer.textContent = `${interventions.length} {% trans "intervention(s)" %}`;
if (interventions.length === 0) {
body.innerHTML = `<div class="text-muted fst-italic p-4 text-center">{% trans "Aucune intervention assignée à cette équipe." %}</div>`;
return;
}
let rows = '';
for (const i of interventions) {
const memberCell = i.assigned_member_name
? `<span class="text-muted small">${escapeHtml(i.assigned_member_name)}</span>`
: `<span class="text-muted small fst-italic">—</span>`;
const summaryBtn = `<button type="button"
class="btn btn-sm btn-outline-secondary"
title="{% trans 'Voir le résumé' %}"
onclick="openTeamInterventionSummary('${escapeHtml(i.summary_url)}', '${escapeHtml(i.code)}')">
<i class="bi bi-eye"></i>
</button>`;
rows += `<tr>
<td class="text-nowrap">${summaryBtn}</td>
<td class="text-nowrap"><a href="${escapeHtml(i.detail_url)}" target="_blank">${escapeHtml(i.code)}</a></td>
<td>${escapeHtml(i.title)}</td>
<td class="text-nowrap">${memberCell}</td>
<td class="text-nowrap"><span class="badge bg-${escapeHtml(i.status_color)}">${escapeHtml(i.status_display)}</span></td>
</tr>`;
}
body.innerHTML = `
<table class="table table-hover table-sm align-middle mb-0">
<thead class="table-light sticky-top">
<tr>
<th></th>
<th>{% trans "Code" %}</th>
<th>{% trans "Titre" %}</th>
<th>{% trans "Membre assigné" %}</th>
<th>{% trans "Statut" %}</th>
</tr>
</thead>
<tbody>${rows}</tbody>
</table>`;
}
function openTeamInterventionSummary(summaryUrl, code) {
// Masquer la modale liste, afficher la modale résumé
const listModalEl = document.getElementById('teamInterventionsModal');
const summaryModalEl = document.getElementById('teamInterventionSummaryModal');
const summaryBody = document.getElementById('teamInterventionSummaryModalBody');
const summaryLabel = document.getElementById('teamInterventionSummaryModalLabel');
bootstrap.Modal.getInstance(listModalEl)?.hide();
summaryLabel.textContent = `${summaryLabel.dataset.baseTitle} — ${code}`;
summaryBody.innerHTML = `
<div class="text-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">{% trans "Chargement..." %}</span>
</div>
</div>`;
new bootstrap.Modal(summaryModalEl).show();
fetch(summaryUrl, { headers: { 'X-Requested-With': 'XMLHttpRequest' } })
.then(r => {
if (!r.ok) throw new Error('forbidden');
return r.text();
})
.then(html => {
summaryBody.innerHTML = html;
initTeamSummaryCarousel(summaryBody);
})
.catch(() => {
summaryBody.innerHTML = `<div class="alert alert-warning m-2">{% trans "Résumé non disponible." %}</div>`;
});
}
function initTeamSummaryCarousel(container) {
const carouselEl = container.querySelector('#summaryDocumentCarousel');
if (!carouselEl) return;
const thumbBtns = Array.from(container.querySelectorAll('.summary-thumbnail-btn'));
const setActive = (idx) => thumbBtns.forEach((b, i) => b.classList.toggle('active', i === idx));
carouselEl.addEventListener('slid.bs.carousel', e => setActive(e.to));
thumbBtns.forEach((btn, idx) => btn.addEventListener('click', () => setActive(idx)));
}
// Bouton "Retour aux interventions" dans la modale résumé
document.addEventListener('DOMContentLoaded', function () {
document.getElementById('teamInterventionSummaryBackBtn').addEventListener('click', function () {
bootstrap.Modal.getInstance(document.getElementById('teamInterventionSummaryModal'))?.hide();
if (_currentTeamId !== null) {
openTeamInterventionsModal(_currentTeamId, _currentTeamName);
}
});
});
function escapeHtml(str) {
if (str == null) return '';
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
</script>
{% endblock script %}