loko/streetup/controls/templates/controls/controls_detail_content.html
2026-07-22 14:48:40 +02:00

833 lines
43 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.

{% load static %}
{% load tz %}
{% load controls_custom_filters %}
{% load i18n %}
{% load short_name %}
<div class="container py-2 position-relative">
<!-- Affichage des messages de succès ou d'échec -->
{% if messages %}
{% for message in messages %}
<div class="alert {% if message.tags == 'success' %}alert-success{% else %}alert-danger{% endif %} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="{% translate 'Fermer' %}"></button>
</div>
{% endfor %}
{% endif %}
<!-- Informations principales -->
<!-- <div class="card shadow-sm"> -->
<div class="card-body position-relative">
<!-- <h5 class="card-title ">{% translate 'Contrôle' %} {{ control.control_code }}</h5> -->
{% if can_change_control == True %}
<a id="control-edit-btn"
href="{% url 'controls:change_control' control.id %}?v={{ request.GET.v }}"
class="btn btn-sm btn-light border position-absolute top-0 end-0 m-2"
title="{% translate 'Modifier le template' %}"
onclick="showControlEditSpinner(event)">
<i id="control-edit-icon" class="bi bi-pencil"></i>
<div id="control-edit-spinner" class="spinner-border spinner-border-sm text-primary d-none" role="status"></div>
</a>
{% endif %}
<p class="mb-1"><strong>{% translate 'Statut' %} :</strong>
<span class="badge bg-info">{{ control.get_status_display }}</span>
</p>
<p class="mb-1"><strong>{% translate 'Type' %} :</strong>
<span class="badge bg-secondary">{{ control.get_control_type_display }}</span>
</p>
<p class="mb-1"><strong>{% translate 'Thématiques' %} :</strong>
{% for thematic in control.thematics.all %}
<span class="badge bg-primary">{{ thematic }}</span>
{% empty %}
<span class="text-muted">{% translate 'Aucune' %}</span>
{% endfor %}
</p>
<p class="mb-1"><strong>{% translate 'Catégories' %} :</strong>
{% if control.categories.count > 0 %}
{% for category in control.categories.all %}
<span class="badge" style="background-color: rgb(0, 140, 255);">{{ category }}</span>
{% empty %}
<span class="text-muted">{% translate 'Aucune' %}</span>
{% endfor %}
{% endif %}
</p>
{% if control.project %}
<p class="mb-1 mt-2"><strong>{% translate 'Projet' %} :</strong>
<a href="{{ control.project.get_absolute_url }}?v={{ request.GET.v }}" >
{{ control.project.name }}
</a>
</p>
{% endif %}
{% if control.phase %}
<p class="mb-1"><strong>{% translate 'Phase' %} :</strong>
{{ control.phase.name }}
</p>
{% endif %}
{% if control.intervention %}
<p class="mb-1"><strong>{% translate 'Intervention' %} :</strong>
{{ control.intervention }}
</p>
{% endif %}
<p class="text-muted small mb-0 mt-2">
{% translate 'Créé le' %} {{ control.created_at|timezone:"Europe/Paris"|date:"d/m/Y H:i" }}
| {% translate 'Mis à jour le' %} {{ control.updated_at|timezone:"Europe/Paris"|date:"d/m/Y H:i" }}
</p>
<p class="text-muted small mb-0">
{% translate 'Auteur' %} : {{ control.control_author|short_name }}
</p>
{% if control.description %}
<p class="mb-1 mt-2"><strong>{% translate 'Description' %} :</strong>
{{ control.description }}
</p>
<p class="mb-1"><strong>{% translate 'Lieu' %} :</strong> {{ control.location }}</p>
{% endif %}
</div>
<!-- </div> -->
<!-- Formulaire en plein écran -->
<div id="remark-form-container">
<button class="close-btn btn btn-primary" onclick="hideRemarkForm()"><i class="bi bi-x-square"></i></button>
<!-- Carte MapLibre -->
<div id="map" class="border-bottom"></div>
<div id="layer-controls">
{% for plan in plans %}
<button class="btn btn-light" onclick="showPlan({{ plan.id }})">{{ plan.name }}</button>
{% endfor %}
{% if plans %}
<button class="btn btn-light" onclick="resetView()">{% translate 'Carte' %}</button>
{% endif %}
</div>
<!-- Formulaire d'ajout de remarque -->
<div class="form-content">
<h2 class="mt-2 text-center" style="color: #007bff;">{% translate 'Ajouter une remarque' %}</h2>
<div class="card shadow p-4">
<form id="remark-form" method="POST" enctype="multipart/form-data" class="needs-validation" novalidate action="{% url 'controls:controls_detail' control_id=control.id %}?v={{ request.GET.v }}">
{% csrf_token %}
<!-- ✅ Management form du formset (obligatoire) -->
{{ qty_formset.management_form }}
<input type="hidden" id="control_id" name="control" value="{{ control.id }}">
<div id="status-badges" class="d-flex justify-content-center gap-2">
<span class="badge bg-secondary status-badge me-1" data-value="information">{% translate 'Info' %}</span>
<span class="badge bg-secondary status-badge me-1" data-value="validated">{% translate 'OK' %}</span>
<span class="badge bg-danger status-badge" data-value="to_be_processed">{% translate 'NOK' %}</span>
<input type="hidden" name="status" id="selectedStatusInput" value="to_be_processed">
</div>
<hr>
<div class="mb-3">
<label for="{{ remark_form.description.id_for_label }}" class="form-label">{% translate 'Description' %}:</label>
{{ remark_form.description }}
<div id="descriptionError" class="invalid-feedback" style="display: none;">
{% translate 'Veuillez remplir la description.' %}
</div>
</div>
<hr>
<div class="mb-3">
<label for="{{ remark_form.images.id_for_label }}" class="form-label">{% translate 'Images' %}:</label>
{{ remark_form.images }}
<div id="imagesError" class="invalid-feedback" style="display: none;">
{% translate 'Veuillez ajouter au moins une image.' %}
</div>
</div>
<div id="preview" class="d-flex flex-wrap gap-2"></div>
<hr>
<div class="mb-3">
<label class="form-label">{% translate 'Tags' %} :</label>
<div id="category-badges">
{% for category in categories %}
<span class="badge bg-secondary category-badge me-1" data-id="{{ category.id }}">
{{ category.get_name }}
</span>
{% endfor %}
</div>
<!-- Champ caché pour stocker les catégories sélectionnées -->
<input type="hidden" name="categories" id="selectedCategoriesInput">
</div>
<!-- ✅ Récapitulatif des quantités (lisible, pas des inputs) -->
<div class="mt-3">
<div class="d-flex justify-content-between align-items-center">
<h6 class="m-0">{% translate 'Quantités liées à la remarque' %}</h6>
<button type="button" class="btn btn-sm btn-outline-primary" id="open-qty-editor">
<i class="bi bi-plus-square"></i> {% translate 'Ajouter des quantités' %}
</button>
</div>
<div id="qty-summary" class="mt-2">
<!-- rempli par JS à partir des lignes du formset -->
<div class="text-muted small" id="qty-summary-empty">
{% translate "Aucune quantité pour l'instant." %}
</div>
<div class="list-group" id="qty-summary-list"></div>
</div>
</div>
<!-- ✅ Les formulaires réels du formset (inputs) sont *cachés* ici mais restent dans le <form> -->
<div id="qty-hidden-forms" class="d-none">
{% for form in qty_formset.forms %}
<div class="qty-form" data-form-index="{{ forloop.counter0 }}">
{{ form.as_p }}
</div>
{% endfor %}
<!-- gabarit pour création dynamique -->
<template id="qty-empty-form-template">
<div class="qty-form" data-form-index="__prefix__">
{{ qty_formset.empty_form.as_p }}
</div>
</template>
</div>
<!-- ✅ Champ caché pour stocker la localisation -->
<input type="hidden" id="latitude" name="lat">
<input type="hidden" id="longitude" name="lon">
<input type="hidden" id="coord_x" name="x">
<input type="hidden" id="coord_y" name="y">
<input type="hidden" name="reference_plan" id="reference_plan" value="map">
<button id="submit-remark-button" type="submit" name="submit_remark" class="btn btn-primary w-100 mt-3">
<span id="submit-remark-button-text">{% translate 'Ajouter' %}</span>
<span id="spinner-remark" class="spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span>
</button>
</form>
</div>
</div>
</div>
<!-- ✅ Bottom sheet plein écran: éditeur des quantités (formset UI) -->
<div id="qty-editor" class="position-fixed top-0 start-0 w-100 h-100 bg-body" style="z-index:1060; display:none;">
<div class="d-flex align-items-center justify-content-between p-3 border-bottom">
<h6 class="m-0">{% translate 'Quantités de la remarque' %}</h6>
<button type="button" class="btn btn-outline-secondary btn-sm" id="close-qty-editor">
<i class="bi bi-x-lg"></i>
</button>
</div>
<!-- ⚠️ Pas un formulaire séparé : ce sont des *clones visibles* des inputs déjà dans qty-hidden-forms -->
<div class="d-flex flex-column h-100">
<div class="flex-grow-1 overflow-auto p-3 content-scroll">
<div id="qty-editor-rows" class="vstack gap-3">
<!-- lignes injectées par JS en clonant les champs du formset -->
</div>
<template id="qty-editor-row-template">
<div class="card shadow-sm p-3" data-form-index="__idx__">
<div class="d-flex justify-content-between align-items-center mb-2">
<strong>{% translate 'Ligne' %} <span class="row-number">__num__</span></strong>
<button type="button" class="btn btn-outline-danger btn-sm editor-remove-row">
<i class="bi bi-trash"></i> {% translate 'Supprimer' %}
</button>
</div>
<!-- Marché -->
<div class="mb-2">
<label class="form-label">{% translate 'Marché (optionnel)' %}</label>
<div class="field-contract"></div>
</div>
<!-- Poste (compat. avec ton autocomplete : label visible + hidden id si tu les as dans le form) -->
<div class="mb-2">
<label class="form-label">{% translate 'Poste (autocomplete si un marché est choisi)' %}</label>
<div class="field-contract_post_label"></div>
<div class="field-contract_post_id"></div>
<div class="list-group mt-1 d-none position-relative" id="qty-post-suggestions-__idx__"></div>
</div>
<!-- Description -->
<div class="mb-2">
<label class="form-label">{% translate 'Description (si pas de marché/poste)' %}</label>
<div class="field-description"></div>
</div>
<!-- q1 q2 q3 -->
<div class="row g-2">
<div class="col-4">
<label class="form-label">{% translate 'Quantité (1)' %}</label>
<div class="field-q1"></div>
</div>
<div class="col-4">
<label class="form-label">{% translate 'Quantité (2)' %}</label>
<div class="field-q2"></div>
</div>
<div class="col-4">
<label class="form-label">{% translate 'Quantité (3)' %}</label>
<div class="field-q3"></div>
</div>
</div>
<div class="row g-2 mt-1">
<div class="col-6">
<label class="form-label">{% translate 'Résultat' %}</label>
<input class="form-control qty-preview" type="text" value="" readonly>
</div>
</div>
<!-- Champ DELETE (si présent dans formset) -->
<div class="field-DELETE d-none"></div>
</div>
</template>
</div>
<!-- Barre d’actions fixe -->
<div class="action-bar fixed-bottom border-top bg-body">
<div class="container-fluid p-3">
<div class="d-grid gap-2">
<button type="button" class="btn btn-outline-primary btn-lg" id="add-qty-row">
<i class="bi bi-plus-square"></i> {% translate 'Ajouter une ligne' %}
</button>
<button type="button" class="btn btn-primary btn-lg" id="apply-qty-and-close">
{% translate 'Terminer et revenir' %}
</button>
</div>
</div>
</div>
</div>
</div>
<hr class="border border-primary">
<!-- Remarks -->
{% if can_change_control == True %}
<button id="add-remark-btn" onclick="showRemarkForm()" class="btn btn-primary w-100 mt-3">
<span id="button-text-pdf">
<span class="bg-white text-primary rounded-circle d-inline-flex align-items-center justify-content-center me-2"
style="width: 24px; height: 24px;">
<i class="bi bi-plus"></i>
</span>
{% translate 'Ajouter une remarque' %}
</span>
</button>
{% endif %}
<div class="card shadow-sm mt-4" id="remarks-card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">{% translate 'Remarques' %}</h5>
</div>
<div class="card-body">
<div id="remarks-list">
{% for remark in remarks %}
<div id="remark-{{ remark.id }}" class="list-group-item position-relative">
<!-- ✅ Bloc des boutons en haut à droite -->
<div id="buttons-{{ remark.id }}" class="d-flex flex-column justify-content-between position-absolute top-0 end-0 p-2 mb-1 h-100">
{% if can_change_control == True %}
<div>
{% if remark.status == "information" %}
<button class="btn btn-sm btn-primary mb-1" onclick="markOk({{ remark.id }})">
<i class="bi bi-hand-thumbs-up"></i>
</button>
<br>
<button class="btn btn-sm btn-primary mb-1" onclick="markNok({{ remark.id }})">
<i class="bi bi-hand-thumbs-down"></i>
</button>
{% elif remark.status == "to_be_processed" or remark.status == "processed" %}
<button class="btn btn-sm btn-primary" onclick="markOk({{ remark.id }})">
<i class="bi bi-hand-thumbs-up"></i>
</button>
{% elif remark.status == "validated" %}
<button class="btn btn-sm btn-primary" onclick="markNok({{ remark.id }})">
<i class="bi bi-hand-thumbs-down"></i>
</button>
{% endif %}
</div>
<div class="d-flex flex-column">
<button class="btn btn-sm btn-secondary stick-to-bottom mb-1"
onclick="window.location.href='{% url 'controls:change_remark' remark.id %}?v={{ request.GET.v }}'" title="{% translate 'Éditer la remarque' %}">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-secondary stick-to-bottom" onclick="archive_remark({{ remark.id }})" title="{% translate 'Archiver la remarque' %}">
<i class="bi bi-archive"></i>
</button>
</div>
{% endif %}
</div>
<!-- ✅ Première Ligne : Numéro, Statut -->
<div class="d-flex justify-content-between align-items-center pe-5">
<!-- Numéro -->
<div class="remark-number">
{{ remark.remark_number }}
</div>
<!-- ✅ Statut -->
<span id="status-{{ remark.id }}" class="badge {{ remark.status|status_badge }}">
<i class="bi {{ remark.status|status_icon }}"></i>
{{ remark.get_status_display }}
</span>
<div style="width: 56px;"></div>
</div>
<!-- ✅ Infos sur l’utilisateur et la date (dernière mise à jour) -->
<p class="small text-muted mb-2">
{% translate 'Mise à jour par' %} <span id="updated-by-{{ remark.id }}">{{ remark.updated_by|short_name }}</span>
{% translate 'le' %} <span id="updated-at-{{ remark.id }}">{{ remark.updated_at|timezone:"Europe/Paris"|date:"d/m/Y H:i" }}</span>
</p>
<!-- ✅ Historique des suivis -->
<div class="remark-history mt-2 mb-3">
<div class="small text-muted mb-2 fw-semibold">{% translate "Suivis de la remarque" %} :</div>
<div class="vstack gap-3 ps-2 border-start border-2 border-primary-subtle ms-1">
{% for entry in remark.history.all %}
<div class="remark-history-entry">
<div class="d-flex align-items-center gap-2 flex-wrap">
<span class="small text-muted fw-semibold">📅 {{ entry.created_at|timezone:"Europe/Paris"|date:"d/m/Y H:i" }}</span>
<span class="badge {{ entry.status|status_badge }}">
<i class="bi {{ entry.status|status_icon }}"></i>
{{ entry.get_status_display }}
</span>
<span class="small text-muted">({% translate 'par' %} {{ entry.created_by|short_name|default:entry.created_by.username }})</span>
</div>
{% if entry.description %}
<div class="mt-1 text-dark" style="white-space: pre-wrap; font-size: 0.95rem;">{{ entry.description }}</div>
{% endif %}
<!-- Images associées à cette étape -->
<div class="remark-images mt-2">
{% for image in entry.images.all %}
<div class="remark-image-wrapper">
{% private_file_url image 'file' as image_file_url %}
{% private_file_url image 'thumbnail' as image_thumbnail_url %}
<a href="{{ image_file_url }}" target="_blank">
<img src="{{ image_thumbnail_url }}" alt="{% translate 'Image de la remarque' %}">
</a>
{% if can_change_control %}
<button type="button"
class="btn btn-sm btn-danger remark-image-delete"
data-image-id="{{ image.id }}"
data-remark-id="{{ remark.id }}"
title="{% translate 'Supprimer' %}">
<i class="bi bi-trash"></i>
</button>
{% endif %}
</div>
{% endfor %}
</div>
</div>
{% empty %}
<!-- Fallback si pas encore d'historique -->
<div class="remark-history-entry">
<div class="d-flex align-items-center gap-2">
<span class="small text-muted fw-semibold">📅 {{ remark.created_at|timezone:"Europe/Paris"|date:"d/m/Y H:i" }}</span>
<span class="badge {{ remark.status|status_badge }}">
<i class="bi {{ remark.status|status_icon }}"></i>
{{ remark.get_status_display }}
</span>
<span class="small text-muted">({% translate 'par' %} {{ remark.updated_by|short_name|default:remark.updated_by.username }})</span>
</div>
{% if remark.description %}
<div class="mt-1 text-dark" style="white-space: pre-wrap; font-size: 0.95rem;">{{ remark.description }}</div>
{% endif %}
<div class="remark-images mt-2">
{% for image in remark.images.all %}
{% if not image.history_entry %}
<div class="remark-image-wrapper">
{% private_file_url image 'file' as image_file_url %}
{% private_file_url image 'thumbnail' as image_thumbnail_url %}
<a href="{{ image_file_url }}" target="_blank">
<img src="{{ image_thumbnail_url }}" alt="{% translate 'Image de la remarque' %}">
</a>
{% if can_change_control %}
<button type="button"
class="btn btn-sm btn-danger remark-image-delete"
data-image-id="{{ image.id }}"
data-remark-id="{{ remark.id }}"
title="{% translate 'Supprimer' %}">
<i class="bi bi-trash"></i>
</button>
{% endif %}
</div>
{% endif %}
{% endfor %}
</div>
</div>
{% endfor %}
</div>
</div>
<!-- ✅ Catégories associées -->
<div class="remark-categories">
{% for tag in remark.remark_tags.all %}
<span class="badge me-1" style="background-color: rgb(169, 179, 191);">
{{ tag.category.get_name }}
</span>
{% endfor %}
</div>
<!-- ✅ Quantités associées -->
{% get_current_language as LANGUAGE_CODE %}
{% with qty_list=remark.quantities.all %}
{% if qty_list %}
<div class="remark-quantities mt-4" style="margin-right: 4rem !important;">
<div class="small text-muted mb-1">{% translate 'Quantités' %}</div>
<div class="table-responsive">
<table class="table table-sm align-middle mb-2">
<thead class="table-light">
<tr>
<th style="min-width: 180px;">{% translate 'Poste / Description' %}</th>
<th class="text-end">{% translate 'Total' %}</th>
</tr>
</thead>
<tbody>
{% for q in qty_list %}
<tr>
<td>
{% if q.contract_post %}
<div class="fw-semibold">
{{ q.contract_post.code }}
{% if q.contract_post.unit %}<span class="text-muted">({{ q.contract_post.unit }})</span>{% endif %}
</div>
<div class="text-muted small">
{% if LANGUAGE_CODE|slice:":2" == "nl" %}
{{ q.contract_post.description_nl|default:q.contract_post.description_fr|truncatechars:120 }}
{% else %}
{{ q.contract_post.description_fr|default:q.contract_post.description_nl|truncatechars:120 }}
{% endif %}
</div>
{% if q.contract %}
<div class="small text-muted">{{ q.contract.contract_number }}</div>
{% endif %}
{% else %}
<div class="fw-semibold">{{ q.description|default:"—" }}</div>
{% endif %}
</td>
<td class="text-end fw-semibold">{{ q.computed_qty|floatformat:2 }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
{% endwith %}
<!-- ✅ Séparateur sauf pour la dernière remarque -->
{% if not forloop.last %}
<div class="shadow border-3 border-bottom mt-3 mb-4"></div>
{% endif %}
</div>
{% empty %}
<p class="text-muted">{% translate 'Aucune remarque enregistrée.' %}</p>
{% endfor %}
</div>
</div>
</div>
<hr class="border border-primary">
<!-- Rapports -->
{% if can_change_control == True %}
<button id="generate-report-btn" data-url="{% url 'controls:export_pdf' control.id %}" class="btn btn-primary w-100 mt-3">
<span id="button-text-pdf"
class="bg-white text-primary rounded-circle d-inline-flex align-items-center justify-content-center me-2"
style="width: 24px; height: 24px;">
<i class="bi bi-plus"></i>
</span>
{% translate 'Produire un rapport PDF' %}
<span id="spinner-pdf" class="spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span>
</button>
{% endif %}
<!-- Message de statut pour le rapport pdf -->
<div id="status-message-pdf" class="mt-3"></div>
<div class="card shadow-sm mt-4">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">{% translate 'Rapports' %}</h5>
</div>
<div class="card-body" id="card-reports">
{% for report in reports %}
<li class="list-group-item d-flex justify-content-between align-items-start">
<div>
<small class="text-muted">
📅 {% translate 'Généré le' %} : {{ report.created_at|timezone:"Europe/Paris"|date:"d/m/Y H:i" }} <br>
🖊 {% translate 'Auteur' %} : {{ report.created_by }}
</small>
</div>
<div>
{% private_file_url report 'document' as report_document_url %}
<a href="{{ report_document_url }}" class="btn btn-sm btn-light mb-1" download>
📥 {% translate 'Télécharger' %}
</a>
<br>
<button id="share-button-{{ report.id }}" class="btn btn-sm btn-light share-button"><i class="bi bi-share-fill text-primary me-1"></i>{% translate 'Partager' %}</button>
</div>
</li>
{% if not forloop.last %}
<div class="shadow border-3 border-bottom mt-3 mb-4"></div>
{% endif %}
{% empty %}
<p class="text-muted">{% translate 'Aucun rapport sauvegardé.' %}</p>
{% endfor %}
</div>
</div>
{% if report_template and can_change_control == True %}
<div class="mt-3 m-2 small">
{% translate 'Template' %}
<button class="btn btn-sm btn-light stick-to-bottom mx-2"
onclick="window.location.href='{% url 'controls:change_template' report_template.id %}?v={{ request.GET.v }}'" title="{% translate 'Éditer le template' %}">
<i class="bi bi-pencil"></i>
</button>
</div>
{% endif %}
<hr class="border border-primary">
<div class="card shadow-sm mt-4" id="archives-card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">{% translate 'Archives' %}</h5>
</div>
<div class="card-body">
<div id="archives-list">
{% for remark in archives %}
<div id="remark-{{ remark.id }}" class="list-group-item position-relative bg-light text-muted">
<!-- ✅ Bloc des boutons en haut à droite -->
<div id="buttons-{{ remark.id }}" class="d-flex flex-column gap-2 position-absolute top-0 end-0 p-2 mb-1">
{% if can_change_control == True %}
<button class="btn btn-sm btn-primary" style="background-color:rgb(120, 163, 215)" onclick="restore_remark({{ remark.id }})">
<i class="bi bi-arrow-90deg-up"></i>
</button>
{% endif %}
</div>
<!-- ✅ Première Ligne : Numéro, Statut -->
<div class="d-flex justify-content-between align-items-center pe-5">
<!-- Numéro -->
<div class="remark-number">
{{ remark.remark_number }}
</div>
<!-- ✅ Statut -->
<span id="status-{{ remark.id }}" class="badge {{ remark.status|status_badge }}">
<i class="bi {{ remark.status|status_icon }}"></i>
{{ remark.get_status_display }}
</span>
<div style="width: 56px;"></div>
</div>
<!-- ✅ Description -->
<h6 class="mt-2 mb-1" style="min-height: 40px;">{{ remark.description }}</h6>
<!-- ✅ Infos sur l’utilisateur et la date -->
<p class="small text-muted">
{% translate 'Mise à jour par' %} <span id="updated-by-{{ remark.id }}">{{ remark.updated_by.username }}</span>
{% translate 'le' %} <span id="updated-at-{{ remark.id }}">{{ remark.updated_at|timezone:"Europe/Paris"|date:"d/m/Y H:i" }}</span>
</p>
<!-- ✅ Images associées -->
<div class="remark-images">
{% for image in remark.images.all %}
<div class="remark-image-wrapper">
{% private_file_url image 'file' as image_file_url %}
{% private_file_url image 'thumbnail' as image_thumbnail_url %}
<a href="{{ image_file_url }}" target="_blank">
<img src="{{ image_thumbnail_url }}" alt="{% translate 'Image de la remarque' %}">
</a>
{% if can_change_control %}
<button type="button"
class="btn btn-sm btn-danger remark-image-delete"
data-image-id="{{ image.id }}"
data-remark-id="{{ remark.id }}"
title="{% translate 'Supprimer' %}">
<i class="bi bi-trash"></i>
</button>
{% endif %}
</div>
{% endfor %}
</div>
<!-- ✅ Catégories associées -->
<div class="remark-categories">
{% for tag in remark.remark_tags.all %}
<span class="badge me-1" style="background-color: rgb(169, 179, 191);">
{{ tag.category.get_name }}
</span>
{% endfor %}
</div>
<!-- ✅ Séparateur sauf pour la dernière remarque -->
{% if not forloop.last %}
<div class="shadow border-3 border-bottom mt-3 mb-4"></div>
{% endif %}
</div>
{% endfor %}
</div>
</div>
</div>
<hr class="border border-primary">
<!-- Bouton pour afficher le formulaire d'upload des plans de référence -->
{% if can_change_control == True %}
<button onclick="showUploadPlanForm()" class="btn btn-light w-100 mt-3"><i class="bi bi-plus-circle px-3 text-primary"></i>{% translate 'Ajouter un plan de référence' %}</button>
{% endif %}
<br><br>
<!-- Formulaire pour uploader les plans de référence -->
<div id="upload-plan-form-container">
<button class="close-btn btn btn-danger" onclick="hideUploadPlanForm()"><i class="bi bi-x-square"></i></button>
<div class="form-content">
<h3 class="text-center">{% translate 'Ajouter un Plan de Référence' %}</h3>
<div class="card shadow p-4 mt-4">
<form id="upload-plan-form" method="POST" enctype="multipart/form-data" action="{% url 'controls:controls_detail' control_id=control.id %}?v={{ request.GET.v }}">
{% csrf_token %}
<div class="mb-3">
<label class="form-label">{% translate 'Nom du Plan' %}</label>
{{ upload_plan_form.name }}
</div>
<div class="mb-3">
<label class="form-label">{% translate 'Sélectionner un Fichier (PDF ou Image)' %}</label>
{{ upload_plan_form.plan }}
</div>
<button type="submit" name="submit_plan" class="btn btn-primary w-100">{% translate 'Uploader' %}</button>
</form>
</div>
</div>
</div>
<div class="modal fade" id="deleteImageModal" tabindex="-1" aria-labelledby="deleteImageModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="deleteImageModalLabel">{% translate 'Confirmer la suppression' %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{% translate 'Fermer' %}"></button>
</div>
<div class="modal-body">
{% translate 'Voulez-vous vraiment supprimer cette image ?' %}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% translate 'Annuler' %}</button>
<button type="button" class="btn btn-danger" id="confirmDeleteImage">{% translate 'Supprimer' %}</button>
</div>
</div>
</div>
</div>
<!-- Badge d'indicateur de synchronisation offline -->
<div id="sync-status" class="position-fixed bottom-0 end-0 m-3" style="z-index: 1000;">
<div class="card shadow-sm">
<div class="card-body p-2 d-flex align-items-center gap-2">
<span id="online-indicator" class="badge bg-success">
<i class="bi bi-wifi"></i> {% translate 'En ligne' %}
</span>
<span id="pending-count" class="badge bg-warning d-none">
<i class="bi bi-cloud-upload"></i> <span id="pending-number">0</span> {% translate 'en attente' %}
</span>
<button id="force-sync-btn" class="btn btn-sm btn-primary d-none" title="{% translate 'Forcer la synchronisation' %}">
<i class="bi bi-arrow-repeat"></i>
</button>
</div>
</div>
</div>
</div>
<script>
// Mettre à jour le badge du nombre de remarques en attente
async function updatePendingBadge() {
if (typeof offlineManager === 'undefined' || !offlineManager) return;
try {
const count = await offlineManager.getPendingCount();
const pendingBadge = document.getElementById('pending-count');
const pendingNumber = document.getElementById('pending-number');
const forceSyncBtn = document.getElementById('force-sync-btn');
if (count > 0) {
pendingBadge.classList.remove('d-none');
pendingNumber.textContent = count;
forceSyncBtn.classList.remove('d-none');
} else {
pendingBadge.classList.add('d-none');
forceSyncBtn.classList.add('d-none');
}
} catch (error) {
console.error('Erreur lors de la mise à jour du badge:', error);
}
}
// Mettre à jour l'indicateur de connexion
function updateOnlineIndicator() {
const indicator = document.getElementById('online-indicator');
if (!indicator) return;
const _om = (typeof offlineManager !== 'undefined') ? offlineManager : null;
const isEffectivelyOnline = _om ? _om.isOnline : navigator.onLine;
if (isEffectivelyOnline) {
indicator.className = 'badge bg-success';
indicator.innerHTML = '<i class="bi bi-wifi"></i> {% translate "En ligne" %}';
} else if (navigator.onLine) {
// Connexion présente mais qualité insuffisante
indicator.className = 'badge bg-warning text-dark';
indicator.innerHTML = '<i class="bi bi-wifi-1"></i> {% translate "Connexion faible" %}';
} else {
indicator.className = 'badge bg-danger';
indicator.innerHTML = '<i class="bi bi-wifi-off"></i> {% translate "Hors ligne" %}';
}
}
// Écouter les changements de connectivité
window.addEventListener('online', () => {
updateOnlineIndicator();
updatePendingBadge();
});
window.addEventListener('offline', () => {
updateOnlineIndicator();
});
// Changements de qualité réseau (Network Information API)
window.addEventListener('effectiveOffline', () => {
updateOnlineIndicator();
});
window.addEventListener('effectiveOnline', () => {
updateOnlineIndicator();
updatePendingBadge();
});
// Initialiser au chargement de la page
document.addEventListener('DOMContentLoaded', () => {
updateOnlineIndicator();
// Attendre que l'offline manager soit prêt
setTimeout(() => {
updatePendingBadge();
}, 500);
});
// Bouton de synchronisation forcée
document.getElementById('force-sync-btn')?.addEventListener('click', async () => {
if (offlineManager) {
await offlineManager.syncPendingData();
await updatePendingBadge();
}
});
</script>