397 lines
24 KiB
HTML
397 lines
24 KiB
HTML
{% extends base_template|default:"base.html" %}
|
|
{% load i18n %}
|
|
|
|
{% block title %}{% translate "Inspections d'assets" %}{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container-fluid py-3">
|
|
<div class="d-flex align-items-center justify-content-between mb-3 flex-wrap gap-2">
|
|
<div>
|
|
<h3 class="mb-0 fw-bold">
|
|
<i class="bi bi-search text-primary me-2"></i>{% translate "Inspections d'assets" %}
|
|
</h3>
|
|
<p class="text-muted mb-0 small">{% translate "Suivi et historique des contrôles et vérifications sur le terrain" %}</p>
|
|
</div>
|
|
{% if can_inspect %}
|
|
<div>
|
|
<a href="{% if is_mobile %}{% url 'mobile:quick_inspection_map_mobile' %}{% else %}{% url 'assets:quick_inspection_map' %}{% endif %}" class="btn btn-primary shadow-sm">
|
|
<i class="bi bi-geo-alt-fill me-1"></i>{% translate "Nouvelle inspection (Carte GPS)" %}
|
|
</a>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Navigation par Onglets -->
|
|
<ul class="nav nav-tabs mb-3">
|
|
<li class="nav-item">
|
|
<a class="nav-link {% if filter_type == 'my' %}active fw-bold{% endif %}" href="?tab=my">
|
|
<i class="bi bi-person-check me-1"></i>{% translate "Mes inspections" %}
|
|
</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link {% if filter_type == 'all' %}active fw-bold{% endif %}" href="?tab=all">
|
|
<i class="bi bi-collection me-1"></i>{% translate "Toutes les inspections" %}
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
|
|
<!-- Barre de filtres -->
|
|
<div class="card mb-3 shadow-sm border-0 bg-light">
|
|
<div class="card-body p-3">
|
|
<form method="get" class="row g-2 align-items-center">
|
|
<input type="hidden" name="tab" value="{{ filter_type }}">
|
|
|
|
<div class="col-12 col-md-3">
|
|
<div class="input-group input-group-sm">
|
|
<span class="input-group-text bg-white"><i class="bi bi-search"></i></span>
|
|
<input type="text" name="q" value="{{ search_q }}" class="form-control" placeholder="{% translate 'Rechercher par note, asset, ...' %}">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-12 col-sm-6 col-md-2">
|
|
<select name="thematic" class="form-select form-select-sm" onchange="this.form.submit()">
|
|
<option value="">{% translate "-- Thématiques --" %}</option>
|
|
{% for t in thematics %}
|
|
<option value="{{ t.code }}" {% if selected_thematic == t.code %}selected{% endif %}>{{ t.name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
|
|
<div class="col-12 col-sm-6 col-md-2">
|
|
<select name="nature" class="form-select form-select-sm" onchange="this.form.submit()">
|
|
<option value="">{% translate "-- Natures --" %}</option>
|
|
{% for val, label in nature_choices %}
|
|
<option value="{{ val }}" {% if selected_nature == val %}selected{% endif %}>{{ label }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
|
|
<div class="col-12 col-sm-6 col-md-3">
|
|
<select name="status" class="form-select form-select-sm" onchange="this.form.submit()">
|
|
<option value="">{% translate "-- Tous les résultats --" %}</option>
|
|
{% for val, label in result_choices %}
|
|
<option value="{{ val }}" {% if selected_status == val %}selected{% endif %}>{{ label }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
|
|
<div class="col-12 col-sm-6 col-md-2 d-flex gap-2">
|
|
<button type="submit" class="btn btn-sm btn-secondary flex-grow-1">{% translate "Filtrer" %}</button>
|
|
<a href="?tab={{ filter_type }}" class="btn btn-sm btn-outline-secondary" title="{% translate 'Réinitialiser' %}"><i class="bi bi-x-circle"></i></a>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Liste des inspections sous forme de Cards -->
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<span class="fw-semibold text-muted small">
|
|
{% blocktranslate count counter=total_count %}{{ counter }} inspection trouvée{% plural %}{{ counter }} inspections trouvées{% endblocktranslate %}
|
|
<span class="fst-italic text-secondary">({% translate "limité aux 100 plus récentes" %})</span>
|
|
</span>
|
|
</div>
|
|
|
|
{% if inspections %}
|
|
<div class="row g-3 mb-4">
|
|
{% for insp in inspections %}
|
|
<div class="col-12">
|
|
<div class="card shadow-sm border-0 h-100 inspection-card cursor-pointer"
|
|
style="cursor: pointer; transition: transform 0.15s ease-in-out, box-shadow 0.15s ease-in-out;"
|
|
data-bs-toggle="modal"
|
|
data-bs-target="#inspectionDetailModal{{ insp.id }}">
|
|
<div class="card-body p-3 d-flex flex-column justify-content-between">
|
|
<div>
|
|
<!-- Entête card : Thématique/Type + Badge résultat -->
|
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
|
<span class="badge bg-light text-dark border d-inline-flex align-items-center me-2">
|
|
<i class="bi {{ insp.thematic_icon }} me-2 text-primary"></i>
|
|
{{ insp.asset_content_type.name }}
|
|
</span>
|
|
<span class="badge {{ insp.status_badge_class }}">
|
|
{{ insp.get_result_status_display }}
|
|
</span>
|
|
</div>
|
|
|
|
<!-- Nom de la cible tronqué à 100 caractères -->
|
|
<h6 class="fw-bold text-dark mb-1 text-break">
|
|
{{ insp.target_display|truncatechars:100 }}
|
|
</h6>
|
|
|
|
<!-- Résumé des constats si présent -->
|
|
{% if insp.details_summary and insp.details_summary != "-" %}
|
|
<p class="text-muted small mb-2 text-truncate mt-1">
|
|
<i class="bi bi-ui-checks me-1 text-secondary"></i>{{ insp.details_summary }}
|
|
</p>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Ligne de pied card : Date & Inspecteur -->
|
|
<div class="d-flex justify-content-between align-items-center pt-2 border-top mt-2 text-muted extra-small">
|
|
<span><i class="bi bi-clock me-1"></i>{{ insp.inspection_date|date:"d/m/Y H:i" }}</span>
|
|
<div class="d-flex align-items-center gap-2">
|
|
{% if insp.documents.all %}
|
|
<span class="badge bg-light text-secondary border" title="{% translate 'Photos / Documents joints' %}">
|
|
<i class="bi bi-paperclip me-1"></i>{{ insp.documents.count }}
|
|
</span>
|
|
{% endif %}
|
|
{% if insp.inspector %}
|
|
<span><i class="bi bi-person me-1"></i>{{ insp.inspector.get_full_name|default:insp.inspector.username }}</span>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal de détail de l'inspection -->
|
|
<div class="modal fade" id="inspectionDetailModal{{ insp.id }}" tabindex="-1" aria-labelledby="inspectionDetailModalLabel{{ insp.id }}" aria-hidden="true">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content border-0 shadow">
|
|
<div class="modal-header bg-light">
|
|
<h5 class="modal-title fw-bold" id="inspectionDetailModalLabel{{ insp.id }}">
|
|
<i class="bi {{ insp.thematic_icon }} me-2 text-primary"></i>{% translate "Détail de l'inspection" %}
|
|
</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body p-3">
|
|
<!-- Lien vers la fiche de l'asset -->
|
|
{% if insp.asset %}
|
|
<div class="mb-3">
|
|
<a href="{% if is_mobile %}{{ insp.mobile_asset_url }}{% else %}{{ insp.asset.get_absolute_url }}{% endif %}" class="btn btn-primary w-100 fw-semibold">
|
|
<i class="bi bi-box-arrow-up-right me-2"></i>{% translate "Consulter la fiche de l'asset" %}
|
|
</a>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<ul class="list-group list-group-flush mb-3">
|
|
<li class="list-group-item d-flex justify-content-between align-items-center px-0">
|
|
<span class="text-muted"><i class="bi bi-geo-alt me-1"></i>{% translate "Cible" %}</span>
|
|
<span class="fw-bold text-end ms-2">{{ insp.target_display }}</span>
|
|
</li>
|
|
<li class="list-group-item d-flex justify-content-between align-items-center px-0">
|
|
<span class="text-muted"><i class="bi bi-layers me-1"></i>{% translate "Type d'asset" %}</span>
|
|
<span class="badge bg-secondary">{{ insp.asset_content_type.name }}</span>
|
|
</li>
|
|
<li class="list-group-item d-flex justify-content-between align-items-center px-0">
|
|
<span class="text-muted"><i class="bi bi-calendar-event me-1"></i>{% translate "Date & Heure" %}</span>
|
|
<span class="fw-semibold">{{ insp.inspection_date|date:"d/m/Y H:i" }}</span>
|
|
</li>
|
|
<li class="list-group-item d-flex justify-content-between align-items-center px-0">
|
|
<span class="text-muted"><i class="bi bi-shield-check me-1"></i>{% translate "Résultat du contrôle" %}</span>
|
|
<span class="badge {{ insp.status_badge_class }}">{{ insp.get_result_status_display }}</span>
|
|
</li>
|
|
<li class="list-group-item d-flex justify-content-between align-items-center px-0">
|
|
<span class="text-muted"><i class="bi bi-eye me-1"></i>{% translate "Nature" %}</span>
|
|
<span class="badge bg-light text-dark border">{{ insp.get_nature_display }}</span>
|
|
</li>
|
|
<li class="list-group-item d-flex justify-content-between align-items-center px-0">
|
|
<span class="text-muted"><i class="bi bi-person me-1"></i>{% translate "Inspecteur" %}</span>
|
|
<span class="fw-semibold">{{ insp.inspector.get_full_name|default:insp.inspector.username|default:"-" }}</span>
|
|
</li>
|
|
|
|
{% with leaf=insp.detailed_inspection %}
|
|
{% if leaf.verified_standard_stand_count is not None %}
|
|
<li class="list-group-item d-flex justify-content-between align-items-center px-0">
|
|
<span class="text-muted"><i class="bi bi-bicycle me-1 text-primary"></i>{% translate "Arceaux classiques constatés" %}</span>
|
|
<span class="badge bg-primary rounded-pill fs-6">{{ leaf.verified_standard_stand_count }}</span>
|
|
</li>
|
|
{% endif %}
|
|
{% if leaf.verified_cargo_stand_count is not None %}
|
|
<li class="list-group-item d-flex justify-content-between align-items-center px-0">
|
|
<span class="text-muted"><i class="bi bi-box-seam me-1 text-info"></i>{% translate "Arceaux cargo constatés" %}</span>
|
|
<span class="badge bg-info text-dark rounded-pill fs-6">{{ leaf.verified_cargo_stand_count }}</span>
|
|
</li>
|
|
{% endif %}
|
|
{% if leaf.verified_marking_quality %}
|
|
<li class="list-group-item d-flex justify-content-between align-items-center px-0">
|
|
<span class="text-muted"><i class="bi bi-paint-bucket me-1"></i>{% translate "Qualité du marquage" %}</span>
|
|
<span class="fw-semibold">{{ leaf.get_verified_marking_quality_display|default:leaf.verified_marking_quality }}</span>
|
|
</li>
|
|
{% endif %}
|
|
{% endwith %}
|
|
</ul>
|
|
|
|
{% if insp.notes %}
|
|
<div class="card bg-light border-0 p-3 mb-3">
|
|
<h6 class="fw-bold text-secondary mb-2"><i class="bi bi-journal-text me-2"></i>{% translate "Remarques" %}</h6>
|
|
<div class="text-dark small">{{ insp.notes }}</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if insp.verified_obsolescence %}
|
|
<div class="card bg-warning-subtle border-0 p-3 mb-3">
|
|
<h6 class="fw-bold text-warning-emphasis mb-1"><i class="bi bi-exclamation-triangle me-2"></i>{% translate "Obsolescence" %}</h6>
|
|
<div class="text-warning-emphasis small">{{ insp.verified_obsolescence }}</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if insp.documents.exists %}
|
|
<div class="card bg-light border-0 p-3 mb-3">
|
|
<h6 class="fw-bold text-secondary mb-2">
|
|
<i class="bi bi-images me-2 text-primary"></i>{% translate "Photos & Documents joints" %} ({{ insp.documents.count }})
|
|
</h6>
|
|
|
|
{% with docs=insp.documents.all %}
|
|
<!-- Carrousel Photos Bootstrap -->
|
|
<div id="carouselInsp{{ insp.id }}" class="carousel slide rounded overflow-hidden shadow-sm bg-black mb-3" data-bs-ride="false">
|
|
<div class="carousel-indicators">
|
|
{% for doc in docs %}
|
|
{% if doc.is_image %}
|
|
<button type="button" data-bs-target="#carouselInsp{{ insp.id }}" data-bs-slide-to="{{ forloop.counter0 }}" {% if forloop.first %}class="active" aria-current="true"{% endif %} aria-label="Slide {{ forloop.counter }}"></button>
|
|
{% endif %}
|
|
{% endfor %}
|
|
</div>
|
|
<div class="carousel-inner" style="max-height: 300px; min-height: 180px;">
|
|
{% for doc in docs %}
|
|
{% if doc.is_image %}
|
|
<div class="carousel-item {% if forloop.first %}active{% endif %} h-100">
|
|
<div class="d-flex align-items-center justify-content-center bg-black position-relative" style="height: 260px;">
|
|
<img src="{{ doc.file_url }}" class="d-block img-fluid mh-100 mw-100 list-insp-carousel-trigger" style="object-fit: contain; max-height: 260px; cursor: pointer;" data-insp-id="{{ insp.id }}" data-img-index="{{ forloop.counter0 }}" alt="{{ doc.filename }}">
|
|
<button type="button" class="btn btn-sm btn-dark bg-opacity-75 text-white rounded-circle p-1.5 shadow list-insp-carousel-trigger position-absolute bottom-0 end-0 m-2" style="z-index: 10; cursor: pointer;" data-insp-id="{{ insp.id }}" data-img-index="{{ forloop.counter0 }}" title="Plein écran">
|
|
<i class="bi bi-arrows-fullscreen"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
{% endfor %}
|
|
</div>
|
|
<button class="carousel-control-prev" type="button" data-bs-target="#carouselInsp{{ insp.id }}" data-bs-slide="prev">
|
|
<span class="carousel-control-prev-icon p-2 rounded-circle bg-dark bg-opacity-50" aria-hidden="true"></span>
|
|
<span class="visually-hidden">Précédent</span>
|
|
</button>
|
|
<button class="carousel-control-next" type="button" data-bs-target="#carouselInsp{{ insp.id }}" data-bs-slide="next">
|
|
<span class="carousel-control-next-icon p-2 rounded-circle bg-dark bg-opacity-50" aria-hidden="true"></span>
|
|
<span class="visually-hidden">Suivant</span>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Documents non-images -->
|
|
<div class="d-flex flex-wrap gap-2">
|
|
{% for doc in docs %}
|
|
{% if not doc.is_image %}
|
|
<a href="{{ doc.file_url }}" target="_blank" class="btn btn-sm btn-outline-secondary d-inline-flex align-items-center gap-1">
|
|
<i class="bi bi-file-earmark-arrow-down text-primary"></i>
|
|
<span class="text-truncate" style="max-width: 160px;">{{ doc.filename }}</span>
|
|
</a>
|
|
{% endif %}
|
|
{% endfor %}
|
|
</div>
|
|
{% endwith %}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
<div class="modal-footer bg-light py-2">
|
|
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">{% translate "Fermer" %}</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% else %}
|
|
<div class="card shadow-sm border-0 p-5 text-center text-muted">
|
|
<i class="bi bi-inbox fs-1 d-block mb-3 text-secondary"></i>
|
|
<h5>{% translate "Aucune inspection trouvée" %}</h5>
|
|
<p class="small text-muted mb-0">{% translate "Aucune inspection ne correspond à vos critères de recherche." %}</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Overlay Carrousel Plein Écran pour Photos (avec bouton fermer) -->
|
|
<div id="fullscreenPhotoOverlay" class="d-none position-fixed top-0 start-0 w-100 h-100 bg-black" style="z-index: 999999;">
|
|
<!-- Bouton Fermer -->
|
|
<button type="button" class="btn-close btn-close-white position-absolute top-0 end-0 m-3 fs-4 shadow p-2" id="btn-close-photo-overlay" style="z-index: 1000005; cursor: pointer;" aria-label="Fermer"></button>
|
|
|
|
<!-- Conteneur Carrousel -->
|
|
<div id="fullscreenPhotoCarousel" class="carousel slide w-100 h-100" data-bs-ride="false">
|
|
<div class="carousel-indicators mb-3" id="fullscreen-carousel-indicators" style="z-index: 1000001;"></div>
|
|
<div class="carousel-inner w-100 h-100" id="fullscreen-carousel-inner"></div>
|
|
<button class="carousel-control-prev" type="button" data-bs-target="#fullscreenPhotoCarousel" data-bs-slide="prev" style="z-index: 1000000;">
|
|
<span class="carousel-control-prev-icon p-3 rounded-circle bg-dark bg-opacity-75" aria-hidden="true"></span>
|
|
<span class="visually-hidden">Précédent</span>
|
|
</button>
|
|
<button class="carousel-control-next" type="button" data-bs-target="#fullscreenPhotoCarousel" data-bs-slide="next" style="z-index: 1000000;">
|
|
<span class="carousel-control-next-icon p-3 rounded-circle bg-dark bg-opacity-75" aria-hidden="true"></span>
|
|
<span class="visually-hidden">Suivant</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const listTriggers = document.querySelectorAll('.list-insp-carousel-trigger');
|
|
listTriggers.forEach(trig => {
|
|
trig.addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
const inspId = this.getAttribute('data-insp-id');
|
|
const startIndex = parseInt(this.getAttribute('data-img-index'), 10) || 0;
|
|
const carouselEl = document.getElementById('carouselInsp' + inspId);
|
|
if (!carouselEl) return;
|
|
|
|
const imgs = carouselEl.querySelectorAll('img.list-insp-carousel-trigger');
|
|
const imagesData = [];
|
|
imgs.forEach(img => {
|
|
imagesData.push({
|
|
url: img.getAttribute('src'),
|
|
filename: img.getAttribute('alt') || 'Photo'
|
|
});
|
|
});
|
|
|
|
const overlay = document.getElementById('fullscreenPhotoOverlay');
|
|
const fsInner = document.getElementById('fullscreen-carousel-inner');
|
|
const fsIndicators = document.getElementById('fullscreen-carousel-indicators');
|
|
if (!overlay || !fsInner) return;
|
|
|
|
fsInner.innerHTML = '';
|
|
if (fsIndicators) fsIndicators.innerHTML = '';
|
|
|
|
imagesData.forEach((doc, idx) => {
|
|
const activeClass = idx === startIndex ? 'active' : '';
|
|
const item = document.createElement('div');
|
|
item.className = `carousel-item ${activeClass} w-100 h-100`;
|
|
item.innerHTML = `
|
|
<div class="w-100 h-100 d-flex flex-column align-items-center justify-content-center p-3">
|
|
<img src="${doc.url}" class="img-fluid mh-100 mw-100 shadow-lg" style="object-fit: contain; max-height: 85vh;" alt="${doc.filename}">
|
|
<div class="text-white small mt-2 bg-dark bg-opacity-75 px-3 py-1 rounded-pill">${doc.filename}</div>
|
|
</div>
|
|
`;
|
|
fsInner.appendChild(item);
|
|
|
|
if (fsIndicators && imagesData.length > 1) {
|
|
const ind = document.createElement('button');
|
|
ind.type = 'button';
|
|
ind.setAttribute('data-bs-target', '#fullscreenPhotoCarousel');
|
|
ind.setAttribute('data-bs-slide-to', idx.toString());
|
|
if (idx === startIndex) {
|
|
ind.className = 'active';
|
|
ind.setAttribute('aria-current', 'true');
|
|
}
|
|
ind.setAttribute('aria-label', `Photo ${idx + 1}`);
|
|
fsIndicators.appendChild(ind);
|
|
}
|
|
});
|
|
|
|
overlay.classList.remove('d-none');
|
|
|
|
const btnClose = document.getElementById('btn-close-photo-overlay');
|
|
if (btnClose) {
|
|
const closeHandler = function(e) {
|
|
if (e) e.preventDefault();
|
|
overlay.classList.add('d-none');
|
|
btnClose.removeEventListener('click', closeHandler);
|
|
};
|
|
btnClose.addEventListener('click', closeHandler);
|
|
}
|
|
|
|
const escHandler = function(e) {
|
|
if (e.key === 'Escape') {
|
|
overlay.classList.add('d-none');
|
|
document.removeEventListener('keydown', escHandler);
|
|
}
|
|
};
|
|
document.addEventListener('keydown', escHandler);
|
|
});
|
|
});
|
|
});
|
|
</script>
|
|
{% endblock %}
|