2026-08-14 18:02:25 +02:00
{% extends "base.html" %}
{% block title %}Équipements — GMAO Collège{% endblock %}
{% block content %}
< div class = "d-flex justify-content-between align-items-center mb-3" >
< h2 class = "mb-0 h5" > < i class = "bi bi-pc-display" > < / i > Équipements par zone< / h2 >
< div >
2026-08-17 01:58:41 +02:00
< div class = "btn-group btn-group-sm me-2" >
< a class = "btn {{ 'btn-primary' if view == 'zones' else 'btn-outline-primary' }}" href = "{{ url_for('equipments.index', view='zones') }}" > < i class = "bi bi-signpost-split" > < / i > Zones / salles< / a >
< a class = "btn {{ 'btn-primary' if view != 'zones' else 'btn-outline-primary' }}" href = "{{ url_for('equipments.index', view='families') }}" > < i class = "bi bi-diagram-3" > < / i > Familles< / a >
< / div >
2026-08-14 18:02:25 +02:00
< a href = "{{ url_for('wizard.equipment_wizard') }}" class = "btn btn-success btn-sm me-2" >
2026-08-14 23:24:42 +02:00
< i class = "bi bi-magic" > < / i > < span class = "d-none d-sm-inline" > Inventaire guidé< / span >
2026-08-14 18:02:25 +02:00
< / a >
2026-08-14 23:24:42 +02:00
< a href = "{{ url_for('equipments.create', advanced=1) }}" class = "btn btn-outline-primary btn-sm" >
< i class = "bi bi-plus-lg" > < / i > < span class = "d-none d-sm-inline" > Fiche unitaire avancée< / span > < span class = "d-sm-none" > +< / span >
2026-08-14 18:02:25 +02:00
< / a >
< / div >
< / div >
<!-- Filtres -->
< div class = "card border-0 shadow-sm mb-3" >
< div class = "card-body py-2" >
< form method = "GET" class = "row g-2 g-md-3 align-items-end" >
< div class = "col-6 col-md-3" >
< input type = "text" name = "search" class = "form-control form-control-sm" placeholder = "Rechercher..." value = "{{ filters.search or '' }}" >
< / div >
< div class = "col-6 col-md-2" >
< select name = "category" class = "form-select form-select-sm" >
< option value = "" > — Catégorie —< / option >
{% for cat in categories %}
< option value = "{{ cat.id }}" { % if filters . category = = cat . id % } selected { % endif % } > {{ cat.name }}< / option >
{% endfor %}
< / select >
< / div >
< div class = "col-6 col-md-2" >
< select name = "building" class = "form-select form-select-sm" >
< option value = "" > — Bâtiment —< / option >
{% for b in buildings %}
< option value = "{{ b.id }}" { % if filters . building = = b . id % } selected { % endif % } > {{ b.name }}< / option >
{% endfor %}
< / select >
< / div >
< div class = "col-6 col-md-2" >
< select name = "status" class = "form-select form-select-sm" >
< option value = "" > — Statut —< / option >
< option value = "en_service" { % if filters . status = = ' en_service ' % } selected { % endif % } > En service< / option >
< option value = "hors_service" { % if filters . status = = ' hors_service ' % } selected { % endif % } > Hors service< / option >
< option value = "en_reparation" { % if filters . status = = ' en_reparation ' % } selected { % endif % } > En réparation< / option >
< option value = "remplace" { % if filters . status = = ' remplace ' % } selected { % endif % } > Remplacé< / option >
< / select >
< / div >
< div class = "col-12 col-md-2" >
< button type = "submit" class = "btn btn-sm btn-outline-primary" > < i class = "bi bi-funnel" > < / i > Filtrer< / button >
< a href = "{{ url_for('equipments.index') }}" class = "btn btn-sm btn-outline-secondary" > < i class = "bi bi-x-circle" > < / i > < / a >
< / div >
< / form >
< / div >
< / div >
2026-08-17 01:58:41 +02:00
{% if view == 'zones' %}
< div class = "alert alert-info py-2" > < i class = "bi bi-info-circle" > < / i > Quantités regroupées par zone, salle et type d’ équipement.< / div >
{% for building in zone_tree %}< div class = "card mb-3 border-0 shadow-sm" > < div class = "card-header fw-semibold" > < i class = "bi bi-building" > < / i > {{ building.name }}< / div > < div class = "card-body" >
2026-08-17 02:16:06 +02:00
{% for zone in building.zones %}< details class = "mb-2" open > < summary class = "fw-semibold" > < i class = "bi bi-signpost-split" > < / i > {{ zone.name }} < span class = "badge bg-light text-dark" > {{ zone.rooms|length }} salle(s)< / span > < / summary > {% for room in zone.rooms %}< details class = "border rounded p-2 mb-2 ms-3" open > < summary > < a href = "{{ url_for('rooms.detail', id=room.id) }}" class = "text-decoration-none" > < i class = "bi bi-door-open" > < / i > {{ room.name }}< / a > < span class = "badge bg-primary" > {{ room.types|sum(attribute='quantity') }}< / span > < / summary > < table class = "table table-sm mb-0 mt-1" > < tbody > {% for item in room.types %}< tr > < td > < a href = "{{ url_for('equipments.detail', id=item.ids[0]) }}" class = "text-decoration-none" > {{ item.name }}< / a > < / td > < td class = "text-muted" > {{ item.category }}< / td > < td class = "text-end" > < span class = "badge bg-primary" > {{ item.quantity }}< / span > < / td > < / tr > {% endfor %}< / tbody > < / table > < / details > {% endfor %}< / details > {% endfor %}< / div > < / div > {% else %}< div class = "text-muted text-center py-4" > Aucun équipement localisé.< / div > {% endfor %}
2026-08-17 01:58:41 +02:00
{% else %}
2026-08-14 18:02:25 +02:00
{% if parents %}
< div class = "accordion" id = "parentsAccordion" >
{% for parent in parents %}
< div class = "card mb-2 border-0 shadow-sm" >
< div class = "card-header bg-light d-flex justify-content-between align-items-center py-2" id = "heading-parent-{{ parent.id }}" >
< button class = "btn btn-link text-decoration-none text-dark fw-bold d-flex align-items-center" type = "button" data-bs-toggle = "collapse" data-bs-target = "#collapse-parent-{{ parent.id }}" aria-expanded = "false" >
< i class = "bi bi-chevron-right me-2 parent-chevron" > < / i >
< i class = "bi bi-collection me-2 text-info" > < / i >
{{ parent.name }}
< span class = "badge bg-info ms-2" > {{ parent.individual_count }} individu{{ 'els' if parent.individual_count > 1 else 'el' }}< / span >
{% if parent.category %}
< span class = "badge bg-secondary ms-1" > {{ parent.category.name }}< / span >
{% endif %}
< / button >
< div class = "d-flex gap-1" >
< a href = "{{ url_for('equipments.detail', id=parent.id) }}" class = "btn btn-sm btn-outline-info" title = "Voir" > < i class = "bi bi-eye" > < / i > < / a >
< a href = "{{ url_for('equipments.edit', id=parent.id) }}" class = "btn btn-sm btn-outline-primary" title = "Modifier" > < i class = "bi bi-pencil" > < / i > < / a >
< form action = "{{ url_for('equipments.delete', id=parent.id) }}" method = "POST" class = "d-inline" onsubmit = "return confirm('Supprimer le groupe \" { { parent . name } } \ " et ses { { parent . individual_count } } individuels ? ' ) " >
< input type = "hidden" name = "csrf_token" value = "{{ csrf_token() }}" >
< button type = "submit" class = "btn btn-sm btn-outline-danger" title = "Supprimer le groupe et ses individuels" > < i class = "bi bi-trash" > < / i > < / button >
< / form >
< / div >
< / div >
< div id = "collapse-parent-{{ parent.id }}" class = "collapse" aria-labelledby = "heading-parent-{{ parent.id }}" data-bs-parent = "#parentsAccordion" >
< div class = "card-body p-0" >
2026-08-15 01:12:51 +02:00
< div class = "equipment-hierarchy p-3" data-url = "{{ url_for('equipments.hierarchy', id=parent.id) }}" >
< div class = "text-muted" > < span class = "spinner-border spinner-border-sm me-2" > < / span > Le contenu sera chargé à l’ ouverture.< / div >
< / div >
{% if false and parent.zones_list %}
2026-08-14 18:02:25 +02:00
< div class = "accordion" id = "zonesAccordion-{{ parent.id }}" >
{% for zone in parent.zones_list %}
< div class = "card border-0" >
< div class = "card-header py-2 d-flex justify-content-between align-items-center bg-white" id = "heading-zone-{{ parent.id }}-{{ zone.zone.id if zone.zone else 'none' }}" >
< button class = "btn btn-link text-decoration-none text-dark ps-0 d-flex align-items-center" type = "button" data-bs-toggle = "collapse" data-bs-target = "#collapse-zone-{{ parent.id }}-{{ zone.zone.id if zone.zone else 'none' }}" aria-expanded = "false" >
< i class = "bi bi-chevron-right me-2 zone-chevron" > < / i >
< i class = "bi bi-geo-alt me-1 text-secondary" > < / i >
{{ zone.name }}
< span class = "badge bg-secondary ms-2" > {{ zone.rooms_list|length }} salle{{ 's' if zone.rooms_list|length > 1 else '' }}< / span >
< / button >
< / div >
< div id = "collapse-zone-{{ parent.id }}-{{ zone.zone.id if zone.zone else 'none' }}" class = "collapse" aria-labelledby = "heading-zone-{{ parent.id }}-{{ zone.zone.id if zone.zone else 'none' }}" data-bs-parent = "#zonesAccordion-{{ parent.id }}" >
< div class = "card-body p-0" >
< div class = "accordion" id = "roomsAccordion-{{ parent.id }}-{{ zone.zone.id if zone.zone else 'none' }}" >
{% for room_data in zone.rooms_list %}
< div class = "card border-0" >
< div class = "card-header py-2 d-flex justify-content-between align-items-center" style = "background:#fafafa;" id = "heading-room-{{ parent.id }}-{{ room_data.room.id }}" >
< button class = "btn btn-link text-decoration-none text-dark ps-4 d-flex align-items-center" type = "button" data-bs-toggle = "collapse" data-bs-target = "#collapse-room-{{ parent.id }}-{{ room_data.room.id }}" aria-expanded = "false" >
< i class = "bi bi-chevron-right me-2 room-chevron" > < / i >
< i class = "bi bi-door-open me-1 text-muted" > < / i >
{{ room_data.name }}
< span class = "badge bg-light text-dark border ms-2" > {{ room_data.individuals|length }}< / span >
< / button >
< / div >
< div id = "collapse-room-{{ parent.id }}-{{ room_data.room.id }}" class = "collapse" aria-labelledby = "heading-room-{{ parent.id }}-{{ room_data.room.id }}" data-bs-parent = "#roomsAccordion-{{ parent.id }}-{{ zone.zone.id if zone.zone else 'none' }}" >
< div class = "card-body p-0" >
< div class = "table-responsive" >
< table class = "table table-hover table-sm mb-0" >
< thead class = "table-light" >
< tr >
< th > Code< / th >
< th > Nom / N°< / th >
< th > Statut< / th >
< th > Curatif< / th >
< th style = "width:110px;" > Actions< / th >
< / tr >
< / thead >
< tbody >
{% for indiv in room_data.individuals %}
< tr { % if indiv . needs_deep_work % } class = "table-danger" { % endif % } >
< td > < code > {{ indiv.code or '—' }}< / code > < / td >
< td >
< i class = "bi bi-cube text-muted me-1" > < / i >
< a href = "{{ url_for('equipments.detail', id=indiv.id) }}" > {{ indiv.name }}< / a >
{% if indiv.individual_number %}
< span class = "badge bg-light text-dark ms-1" > #{{ indiv.individual_number }}< / span >
{% endif %}
< / td >
< td > < span class = "badge {{ indiv.status|status_badge }}" > {{ indiv.status|format_status }}< / span > < / td >
< td >
{% if indiv.curative_count > 0 %}
< span class = "badge {% if indiv.needs_deep_work %}bg-danger{% else %}bg-warning text-dark{% endif %}" > {{ indiv.curative_count }}< / span >
{% else %}
< span class = "text-muted" > 0< / span >
{% endif %}
< / td >
< td >
< a href = "{{ url_for('equipments.detail', id=indiv.id) }}" class = "btn btn-sm btn-outline-info" title = "Voir" > < i class = "bi bi-eye" > < / i > < / a >
< a href = "{{ url_for('equipments.edit', id=indiv.id) }}" class = "btn btn-sm btn-outline-primary" title = "Modifier" > < i class = "bi bi-pencil" > < / i > < / a >
< form action = "{{ url_for('equipments.delete', id=indiv.id) }}" method = "POST" class = "d-inline" onsubmit = "return confirm('Supprimer l\'élément \" { { indiv . name } } \ " ? ' ) " >
< input type = "hidden" name = "csrf_token" value = "{{ csrf_token() }}" >
< button type = "submit" class = "btn btn-sm btn-outline-danger" title = "Supprimer" > < i class = "bi bi-trash" > < / i > < / button >
< / form >
< / td >
< / tr >
{% endfor %}
< / tbody >
< / table >
< / div >
< / div >
< / div >
< / div >
{% endfor %}
< / div >
< / div >
< / div >
< / div >
{% endfor %}
< / div >
{% else %}
< p class = "text-muted p-3 mb-0" > < i class = "bi bi-info-circle" > < / i > Aucun individuel dans ce groupe.< / p >
{% endif %}
< / div >
< / div >
< / div >
{% endfor %}
< / div >
{% else %}
< div class = "text-center py-5 text-muted" >
< i class = "bi bi-inbox" style = "font-size:3rem;" > < / i >
< p class = "mt-2" > Aucun équipement< / p >
< / div >
{% endif %}
2026-08-17 01:58:41 +02:00
{% endif %}
2026-08-14 18:02:25 +02:00
{% if pagination and pagination.pages > 1 %}
< nav aria-label = "Pagination équipements" class = "mt-3" >
< ul class = "pagination pagination-sm justify-content-center" >
< li class = "page-item {% if not pagination.has_prev %}disabled{% endif %}" >
< a class = "page-link" href = "{{ url_for('equipments.index', page=pagination.prev_num, **filters) if pagination.has_prev else '#' }}" > « < / a >
< / li >
{% for p in pagination.iter_pages(left_edge=1, right_edge=1, left_current=2, right_current=2) %}
{% if p %}
< li class = "page-item {% if p == pagination.page %}active{% endif %}" >
< a class = "page-link" href = "{{ url_for('equipments.index', page=p, **filters) }}" > {{ p }}< / a >
< / li >
{% else %}
< li class = "page-item disabled" > < span class = "page-link" > …< / span > < / li >
{% endif %}
{% endfor %}
< li class = "page-item {% if not pagination.has_next %}disabled{% endif %}" >
< a class = "page-link" href = "{{ url_for('equipments.index', page=pagination.next_num, **filters) if pagination.has_next else '#' }}" > » < / a >
< / li >
< / ul >
< / nav >
< p class = "text-center text-muted small" >
Page {{ pagination.page }} / {{ pagination.pages }} — {{ pagination.total }} groupe(s) au total
< / p >
{% endif %}
< script >
2026-08-15 01:12:51 +02:00
function renderHierarchy(target, zones) {
target.replaceChildren();
if (!zones.length) {
target.innerHTML = '< p class = "text-muted mb-0" > Aucun équipement localisé dans cette famille.< / p > ';
return;
}
zones.forEach(zone => {
const section = document.createElement('section');
section.className = 'mb-3';
const heading = document.createElement('h6');
heading.className = 'border-bottom pb-2';
heading.textContent = `${zone.name} — ${zone.count} équipement(s)`;
section.appendChild(heading);
zone.rooms.forEach(room => {
const details = document.createElement('details');
details.className = 'ms-2 mb-2';
const summary = document.createElement('summary');
summary.className = 'fw-semibold';
summary.textContent = `${room.name} (${room.items.length})`;
details.appendChild(summary);
const list = document.createElement('div');
list.className = 'list-group list-group-flush ms-3 mt-1';
room.items.forEach(item => {
const link = document.createElement('a');
link.className = 'list-group-item list-group-item-action py-1 d-flex justify-content-between';
link.href = item.url;
const label = document.createElement('span');
label.textContent = item.code ? `${item.name} — ${item.code}` : item.name;
const badge = document.createElement('span');
badge.className = `badge ${item.status_class}`;
badge.textContent = item.status_label;
link.append(label, badge);
list.appendChild(link);
});
details.appendChild(list);
section.appendChild(details);
});
target.appendChild(section);
});
}
2026-08-14 18:02:25 +02:00
document.querySelectorAll('.collapse').forEach(collapse => {
collapse.addEventListener('shown.bs.collapse', function() {
const chevron = this.closest('.card').querySelector('.parent-chevron, .zone-chevron, .room-chevron');
if (chevron) chevron.classList.replace('bi-chevron-right', 'bi-chevron-down');
2026-08-15 01:12:51 +02:00
const target = this.querySelector('.equipment-hierarchy[data-url]');
if (target & & !target.dataset.loaded) {
target.dataset.loaded = '1';
fetch(target.dataset.url).then(response => {
if (!response.ok) throw new Error('Chargement impossible');
return response.json();
}).then(data => renderHierarchy(target, data.zones)).catch(() => {
target.innerHTML = '< div class = "alert alert-danger mb-0" > Impossible de charger cette famille.< / div > ';
delete target.dataset.loaded;
});
}
2026-08-14 18:02:25 +02:00
});
collapse.addEventListener('hidden.bs.collapse', function() {
const chevron = this.closest('.card').querySelector('.parent-chevron, .zone-chevron, .room-chevron');
if (chevron) chevron.classList.replace('bi-chevron-down', 'bi-chevron-right');
});
});
< / script >
{% endblock %}