215 lines
15 KiB
HTML
215 lines
15 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>
|
||
|
|
<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">Création par lot</span>
|
||
|
|
</a>
|
||
|
|
<a href="{{ url_for('equipments.create') }}" class="btn btn-primary btn-sm">
|
||
|
|
<i class="bi bi-plus-lg"></i> <span class="d-none d-sm-inline">Nouvel</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 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">
|
||
|
|
{% if 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 %}
|
||
|
|
|
||
|
|
{% 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>
|
||
|
|
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');
|
||
|
|
});
|
||
|
|
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 %}
|