279 lines
20 KiB
HTML
279 lines
20 KiB
HTML
{% 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é à l’ouverture.</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 '#' }}">«</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>
|
||
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 %}
|