gmao/app_new/templates/equipments/index.html
root 4f8237aba9
Some checks are pending
CI - Tests et Syntax / lint-and-test (push) Waiting to run
Lier le cycle de vie à la maintenance préventive
2026-08-19 22:49:15 +00:00

279 lines
20 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" %}
{% 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>
<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>
<a href="{{ url_for('wizard.equipment_wizard') }}" class="btn btn-success btn-sm me-2">
<i class="bi bi-magic"></i> <span class="d-none d-sm-inline">Inventaire guidé</span>
</a>
<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>
</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>
{% 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">
{% for zone in building.zones %}<details class="mb-2"><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"><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 text-nowrap"><span class="badge bg-success" title="Disponible">{{ item.available }}</span>{% if item.repair %} <span class="badge bg-warning text-dark" title="En réparation">{{ item.repair }}</span>{% endif %}{% if item.disposal %} <span class="badge bg-danger" title="Rebut proposé ou confirmé">{{ item.disposal }}</span>{% endif %} <span class="badge bg-primary" title="Total">{{ 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 %}
{% else %}
{% 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">
<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é à louverture.</div>
</div>
{% if false and parent.zones_list %}
<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 %}
{% endif %}
{% 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 '#' }}">&laquo;</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 '#' }}">&raquo;</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>
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.reduce((total, item) => total + (item.quantity || 1), 0)})`;
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);
});
}
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');
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;
});
}
});
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 %}