812 lines
34 KiB
HTML
812 lines
34 KiB
HTML
{% 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">☓ {% 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, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, ''');
|
||
}
|
||
</script>
|
||
{% endblock script %}
|