833 lines
43 KiB
HTML
833 lines
43 KiB
HTML
{% 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>
|