1222 lines
61 KiB
HTML
1222 lines
61 KiB
HTML
{% extends 'mobile/mobile_base.html' %}
|
||
|
||
{% load static %}
|
||
{% load tz %}
|
||
{% load controls_custom_filters %}
|
||
{% load i18n %}
|
||
|
||
{% block extra_head %}
|
||
<!-- MapLibre GL 5.7.0 (local) -->
|
||
<link href="{% static 'common/maplibre-gl.css' %}" rel="stylesheet" />
|
||
<link rel="stylesheet" href="{% static 'controls/controls_detail.css' %}">
|
||
<style>
|
||
.preview-image {
|
||
width: 80px;
|
||
height: 80px;
|
||
object-fit: cover;
|
||
border-radius: 6px;
|
||
border: 1px solid #dee2e6;
|
||
}
|
||
|
||
.preview-wrapper {
|
||
position: relative;
|
||
display: inline-block;
|
||
}
|
||
|
||
.remove-btn {
|
||
position: absolute;
|
||
top: -6px;
|
||
right: -6px;
|
||
background: rgba(255, 0, 0, 0.7);
|
||
border: none;
|
||
color: white;
|
||
border-radius: 50%;
|
||
width: 22px;
|
||
height: 22px;
|
||
font-size: 14px;
|
||
line-height: 18px;
|
||
text-align: center;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.camera-flash-active {
|
||
opacity: 1 !important;
|
||
}
|
||
</style>
|
||
{% endblock extra_head %}
|
||
|
||
|
||
{% block upleft %}
|
||
<a href="javascript:history.back()" class="nav-link navbar-upleft" style="color: white;">
|
||
<i class="bi bi-arrow-left fs-3" title="Retour"></i>
|
||
</a>
|
||
{% endblock upleft %}
|
||
|
||
{% block title %}
|
||
{{ control.control_code }}
|
||
{% endblock title %}
|
||
|
||
|
||
{% block content %}
|
||
|
||
<div class="container py-2">
|
||
<!-- 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-body position-relative">
|
||
<a id="control-edit-btn"
|
||
href="{% url 'controls:change_control' control.id %}?v=mobile"
|
||
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>
|
||
|
||
<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>
|
||
|
||
<br>
|
||
|
||
{% if control.project %}
|
||
<p class="mb-1"><strong>{% translate 'Projet' %} :</strong>
|
||
<a href="{{ control.project.get_absolute_url }}?v=mobile">
|
||
{{ 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="mb-1"><strong>{% translate 'Lieu' %} :</strong> {{ control.location }}</p>
|
||
|
||
<p class="text-muted small mb-0">
|
||
{% 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 }}
|
||
</p>
|
||
|
||
{% if control.description %}
|
||
<p class="mb-1"><strong>{% translate 'Description' %} :</strong>
|
||
{{ control.description }}
|
||
</p>
|
||
{% endif %}
|
||
</div>
|
||
|
||
|
||
|
||
<!-- Formulaire en plein écran -->
|
||
<div id="remark-form-container">
|
||
<button class="close-btn btn btn-primary" onclick="hideRemarkForm()" title="{% translate 'Fermer' %}"><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 d-flex flex-column" novalidate data-no-overlay>
|
||
{% 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 class="form-label">{% translate 'Images' %}:</label>
|
||
<div class="d-grid gap-2">
|
||
<button type="button" id="open-camera-modal-btn" class="btn btn-outline-primary">
|
||
<i class="bi bi-camera-fill me-2"></i>{% translate "Prendre des photos (mode continu)" %}
|
||
</button>
|
||
<button type="button" id="select-files-btn" class="btn btn-outline-primary">
|
||
<i class="bi bi-folder2-open me-2"></i>{% translate "Photos, vidéos, fichiers" %}
|
||
</button>
|
||
</div>
|
||
<input type="file" id="id_images" name="images" multiple class="d-none">
|
||
<div id="photo-inputs" class="d-none"></div>
|
||
<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>
|
||
<input type="hidden" name="categories" id="selectedCategoriesInput">
|
||
</div>
|
||
|
||
<!-- ✅ Coordonnées / plan -->
|
||
<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">
|
||
|
||
<!-- ✅ 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>
|
||
|
||
<!-- Footer actions du formulaire remarque -->
|
||
<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 -->
|
||
<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>
|
||
|
||
<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">
|
||
<!-- Remarques en attente (mode offline) -->
|
||
<div id="pending-remarks-list"></div>
|
||
|
||
<!-- Remarques existantes -->
|
||
<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">
|
||
<div>
|
||
{% if remark.status == "information" %}
|
||
<button class="btn btn-sm btn-primary mb-1" onclick="markOk({{ remark.id }})" title="{% translate 'Valider' %}">
|
||
<i class="bi bi-hand-thumbs-up"></i>
|
||
</button>
|
||
<br>
|
||
<button class="btn btn-sm btn-primary mb-1" onclick="markNok({{ remark.id }})" title="{% translate 'Marquer NOK' %}">
|
||
<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 }})" title="{% translate 'Valider' %}">
|
||
<i class="bi bi-hand-thumbs-up"></i>
|
||
</button>
|
||
{% elif remark.status == "validated" %}
|
||
<button class="btn btn-sm btn-primary" onclick="markNok({{ remark.id }})" title="{% translate 'Marquer NOK' %}">
|
||
<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 'mobile:change_remark' remark.id %}'" 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>
|
||
</div>
|
||
|
||
<!-- ✅ Première Ligne : Numéro, Statut -->
|
||
<div class="d-flex justify-content-between align-items-center pe-5">
|
||
<div class="d-flex align-items-center gap-2">
|
||
<div class="remark-number">
|
||
{{ remark.remark_number }}
|
||
</div>
|
||
{% if remark.images.exists %}
|
||
<button type="button" class="btn btn-sm btn-outline-primary rounded-pill px-2 py-0" onclick="openRemarkPhotoCarouselByRemarkId({{ remark.id }})" title="{% translate 'Visualiser les photos dans le carrousel' %}">
|
||
<i class="bi bi-images me-1"></i>{{ remark.images.count }}
|
||
</button>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<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">
|
||
{% load short_name %}
|
||
{% translate 'Mise à jour par' %} <span id="updated-by-{{ remark.id }}">{{ remark.updated_by|short_name|default: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>
|
||
|
||
<!-- ✅ 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_thumb_url %}
|
||
<img src="{{ image_thumb_url }}"
|
||
alt="{% translate 'Image de la remarque' %}"
|
||
class="remark-photo-thumb shadow-sm"
|
||
style="cursor: pointer;"
|
||
data-remark-id="{{ remark.id }}"
|
||
data-full-url="{{ image_file_url }}"
|
||
data-thumb-url="{{ image_thumb_url }}"
|
||
data-image-id="{{ image.id }}"
|
||
data-caption="{% translate 'Remarque' %} #{{ remark.remark_number }}{% if entry.created_at %} — {{ entry.created_at|timezone:'Europe/Paris'|date:'d/m/Y H:i' }}{% endif %}"
|
||
onclick="openRemarkPhotoCarousel(this)">
|
||
{% 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_thumb_url %}
|
||
<img src="{{ image_thumb_url }}"
|
||
alt="{% translate 'Image de la remarque' %}"
|
||
class="remark-photo-thumb shadow-sm"
|
||
style="cursor: pointer;"
|
||
data-remark-id="{{ remark.id }}"
|
||
data-full-url="{{ image_file_url }}"
|
||
data-thumb-url="{{ image_thumb_url }}"
|
||
data-image-id="{{ image.id }}"
|
||
data-caption="{% translate 'Remarque' %} #{{ remark.remark_number }}"
|
||
onclick="openRemarkPhotoCarousel(this)">
|
||
{% 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 %}
|
||
|
||
{% 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 -->
|
||
<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>
|
||
|
||
<!-- 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 %}
|
||
<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=mobile'" 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">
|
||
<button class="btn btn-sm btn-primary" style="background-color:rgb(120, 163, 215)" onclick="restore_remark({{ remark.id }})" title="{% translate 'Restaurer la remarque' %}">
|
||
<i class="bi bi-arrow-90deg-up"></i>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- ✅ Première Ligne : Numéro, Statut -->
|
||
<div class="d-flex justify-content-between align-items-center pe-5">
|
||
<div class="remark-number">
|
||
{{ remark.remark_number }}
|
||
</div>
|
||
|
||
<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 %}
|
||
{% private_file_url image 'file' as image_file_url %}
|
||
{% private_file_url image 'thumbnail' as image_thumb_url %}
|
||
<div class="remark-image-wrapper">
|
||
<img src="{{ image_thumb_url }}"
|
||
alt="{% translate 'Image de la remarque' %}"
|
||
class="remark-photo-thumb shadow-sm"
|
||
style="cursor: pointer;"
|
||
data-remark-id="{{ remark.id }}"
|
||
data-full-url="{{ image_file_url }}"
|
||
data-thumb-url="{{ image_thumb_url }}"
|
||
data-image-id="{{ image.id }}"
|
||
data-caption="{% translate 'Remarque' %} #{{ remark.remark_number }} ({% translate 'Archivée' %})"
|
||
onclick="openRemarkPhotoCarousel(this)">
|
||
{% 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>
|
||
|
||
{% 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 -->
|
||
<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>
|
||
|
||
<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()" title="{% translate 'Fermer' %}"><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">
|
||
{% 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>
|
||
|
||
<!-- Modal caméra Canvas (mode continu) -->
|
||
<div class="modal fade" id="cameraModal" tabindex="-1" aria-labelledby="cameraModalLabel" aria-hidden="true">
|
||
<div class="modal-dialog modal-fullscreen-sm-down d-flex m-0 h-100 mx-auto">
|
||
<div class="modal-content bg-dark text-white d-flex flex-column">
|
||
<div class="modal-header border-0 flex-shrink-0 py-2">
|
||
<h5 class="modal-title" id="cameraModalLabel">
|
||
<i class="bi bi-camera-video"></i> {% translate "Caméra" %}
|
||
</h5>
|
||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="{% translate 'Fermer' %}" id="cameraModalCloseBtn"></button>
|
||
</div>
|
||
<div class="modal-body d-flex flex-column p-2 flex-grow-1" style="overflow: hidden; min-height: 0;">
|
||
<div class="flex-grow-1 d-flex align-items-center justify-content-center" style="min-height: 0; overflow: hidden;">
|
||
<div class="position-relative w-100 h-100 d-flex align-items-center justify-content-center" style="border-radius: 0.5rem; overflow: hidden;">
|
||
<video id="cameraPreview" playsinline autoplay style="max-width: 100%; max-height: 100%; object-fit: contain;"></video>
|
||
<div id="cameraFlashOverlay" style="position: absolute; inset: 0; background: rgba(255, 255, 255, 0.9); opacity: 0; pointer-events: none; transition: opacity 120ms ease-out; z-index: 2;"></div>
|
||
<canvas id="cameraCanvas" class="d-none"></canvas>
|
||
</div>
|
||
</div>
|
||
<div id="cameraStatus" class="small text-center text-muted my-2 flex-shrink-0"></div>
|
||
<div class="d-flex justify-content-between align-items-center gap-2 mb-2 flex-shrink-0">
|
||
<button type="button" class="btn btn-outline-light flex-fill" id="cameraStopBtn">
|
||
<i class="bi bi-x-circle"></i> {% translate "Terminer" %}
|
||
</button>
|
||
<button type="button" class="btn btn-primary flex-fill" id="cameraCaptureBtn">
|
||
<i class="bi bi-camera-fill"></i> {% translate "Capturer" %}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Badge d'indicateur de synchronisation offline -->
|
||
<div id="sync-status" class="position-fixed end-0 m-3 d-none" style="bottom: 40px; 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-danger">
|
||
<i class="bi bi-wifi-off"></i> {% translate 'Hors 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 (!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');
|
||
const syncStatus = document.getElementById('sync-status');
|
||
|
||
if (count > 0) {
|
||
pendingBadge.classList.remove('d-none');
|
||
pendingNumber.textContent = count;
|
||
forceSyncBtn.classList.remove('d-none');
|
||
// Afficher le badge s'il y a des remarques en attente
|
||
syncStatus.classList.remove('d-none');
|
||
} else {
|
||
pendingBadge.classList.add('d-none');
|
||
forceSyncBtn.classList.add('d-none');
|
||
// Masquer le badge s'il n'y a plus de remarques et qu'on est en ligne
|
||
if (navigator.onLine) {
|
||
syncStatus.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');
|
||
const syncStatus = document.getElementById('sync-status');
|
||
if (!indicator || !syncStatus) 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" %}';
|
||
// Masquer le badge quand on revient en ligne (sauf si synchro en cours)
|
||
setTimeout(async () => {
|
||
if (offlineManager) {
|
||
const count = await offlineManager.getPendingCount();
|
||
if (count === 0) {
|
||
syncStatus.classList.add('d-none');
|
||
}
|
||
}
|
||
}, 2000);
|
||
} 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" %}';
|
||
syncStatus.classList.remove('d-none');
|
||
} else {
|
||
indicator.className = 'badge bg-danger';
|
||
indicator.innerHTML = '<i class="bi bi-wifi-off"></i> {% translate "Hors ligne" %}';
|
||
syncStatus.classList.remove('d-none');
|
||
}
|
||
}
|
||
|
||
// É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();
|
||
updateExistingRemarksButtons();
|
||
});
|
||
|
||
window.addEventListener('effectiveOnline', () => {
|
||
updateOnlineIndicator();
|
||
updatePendingBadge();
|
||
updateExistingRemarksButtons();
|
||
});
|
||
|
||
// Initialiser au chargement de la page
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
// Mémoriser ce contrôle comme dernier visité (utilisé par la page offline)
|
||
try {
|
||
localStorage.setItem('streetup.lastControlUrl', window.location.href);
|
||
} catch (e) { /* localStorage non disponible */ }
|
||
|
||
// Afficher le badge si hors ligne ou connexion trop faible
|
||
const syncStatus = document.getElementById('sync-status');
|
||
const isEffectivelyOnline = (typeof offlineManager !== 'undefined' && offlineManager) ? offlineManager.isOnline : navigator.onLine;
|
||
if (!isEffectivelyOnline) {
|
||
syncStatus.classList.remove('d-none');
|
||
}
|
||
updateOnlineIndicator();
|
||
|
||
// Attendre que l'offline manager soit prêt
|
||
setTimeout(() => {
|
||
updatePendingBadge();
|
||
displayPendingRemarks();
|
||
updateExistingRemarksButtons();
|
||
}, 500);
|
||
});
|
||
|
||
// Afficher les remarques en attente
|
||
async function displayPendingRemarks() {
|
||
if (!offlineManager || !offlineManagerReady) return;
|
||
|
||
const controlId = document.getElementById('control_id').value;
|
||
const pendingRemarks = await offlineManager.getPendingRemarks(controlId);
|
||
const container = document.getElementById('pending-remarks-list');
|
||
|
||
if (!container) return;
|
||
|
||
container.innerHTML = '';
|
||
|
||
if (pendingRemarks.length === 0) {
|
||
return;
|
||
}
|
||
|
||
// Afficher chaque remarque en attente
|
||
pendingRemarks.forEach((remark, index) => {
|
||
const remarkDiv = document.createElement('div');
|
||
remarkDiv.className = 'list-group-item position-relative border-warning bg-warning-subtle';
|
||
remarkDiv.id = `pending-remark-${remark.id}`;
|
||
|
||
const statusBadgeClass = remark.status === 'validated' ? 'bg-success' :
|
||
remark.status === 'information' ? 'bg-secondary' : 'bg-danger';
|
||
const statusText = remark.status === 'validated' ? '{% translate "OK" %}' :
|
||
remark.status === 'information' ? '{% translate "Info" %}' : '{% translate "NOK" %}';
|
||
|
||
remarkDiv.innerHTML = `
|
||
<!-- Badge "En attente de synchronisation" -->
|
||
<div class="badge bg-warning text-dark mb-2">
|
||
<i class="bi bi-cloud-upload"></i> {% translate 'En attente de synchronisation' %}
|
||
</div>
|
||
|
||
<!-- Boutons d'action -->
|
||
<div class="d-flex flex-column position-absolute top-0 end-0 p-2 mb-1 gap-2">
|
||
<button class="btn btn-sm btn-outline-primary" onclick="editPendingRemark(${remark.id})" title="{% translate 'Modifier' %}">
|
||
<i class="bi bi-pencil"></i>
|
||
</button>
|
||
<button class="btn btn-sm btn-outline-danger" onclick="deletePendingRemark(${remark.id})" title="{% translate 'Supprimer' %}">
|
||
<i class="bi bi-trash"></i>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- En-tête -->
|
||
<div class="d-flex justify-content-between align-items-center pe-5">
|
||
<div class="remark-number">
|
||
<strong>#${pendingRemarks.length - index}</strong>
|
||
</div>
|
||
<span class="badge ${statusBadgeClass}">${statusText}</span>
|
||
<div style="width: 56px;"></div>
|
||
</div>
|
||
|
||
<!-- Description -->
|
||
<h6 class="mt-2 mb-1" style="min-height: 50px; max-width: 90%;">${remark.description || ''}</h6>
|
||
|
||
<!-- Infos -->
|
||
<p class="small text-muted">
|
||
{% translate 'Créé localement le' %} ${new Date(remark.timestamp).toLocaleString('fr-FR')}
|
||
</p>
|
||
|
||
<!-- Images -->
|
||
${remark.photos && remark.photos.length > 0 ? `
|
||
<div class="remark-images mt-2">
|
||
<div class="d-flex flex-wrap gap-2">
|
||
${remark.photos.map(photo => `
|
||
<div class="position-relative">
|
||
<img src="${photo.url}" class="img-thumbnail" style="width: 80px; height: 80px; object-fit: cover;" onclick="showImageModal('${photo.url}')">
|
||
<button class="btn btn-sm btn-danger position-absolute top-0 end-0" style="padding: 0.1rem 0.3rem;" onclick="deletePendingPhoto(${remark.id}, ${photo.id})" title="{% translate 'Supprimer' %}">
|
||
<i class="bi bi-x"></i>
|
||
</button>
|
||
</div>
|
||
`).join('')}
|
||
</div>
|
||
</div>
|
||
` : ''}
|
||
|
||
<div class="shadow border-3 border-bottom mt-3 mb-4"></div>
|
||
`;
|
||
|
||
container.appendChild(remarkDiv);
|
||
});
|
||
}
|
||
|
||
// Éditer une remarque en attente
|
||
async function editPendingRemark(remarkId) {
|
||
if (!offlineManager) return;
|
||
|
||
const controlId = document.getElementById('control_id').value;
|
||
const pendingRemarks = await offlineManager.getPendingRemarks(controlId);
|
||
const remark = pendingRemarks.find(r => r.id === remarkId);
|
||
|
||
if (!remark) return;
|
||
|
||
// Stocker l'ID de la remarque en édition AVANT d'afficher le formulaire
|
||
const form = document.getElementById('remark-form');
|
||
form.dataset.editingRemarkId = remarkId;
|
||
|
||
// Afficher le formulaire en mode édition
|
||
showRemarkForm();
|
||
|
||
// Pré-remplir le formulaire APRÈS l'avoir affiché
|
||
setTimeout(() => {
|
||
document.getElementById('id_description').value = remark.description || '';
|
||
document.getElementById('selectedStatusInput').value = remark.status;
|
||
|
||
// Mettre à jour les badges de statut
|
||
document.querySelectorAll('.status-badge').forEach(badge => {
|
||
if (badge.dataset.value === remark.status) {
|
||
badge.classList.remove('bg-secondary');
|
||
badge.classList.add('bg-primary');
|
||
} else {
|
||
badge.classList.remove('bg-primary');
|
||
badge.classList.add('bg-secondary');
|
||
}
|
||
});
|
||
|
||
// Changer le texte du bouton
|
||
document.getElementById('submit-remark-button-text').textContent = '{% translate "Mettre à jour" %}';
|
||
}, 100);
|
||
}
|
||
|
||
// Supprimer une remarque en attente
|
||
async function deletePendingRemark(remarkId) {
|
||
if (!offlineManager) return;
|
||
|
||
if (!confirm('{% translate "Voulez-vous vraiment supprimer cette remarque ?" %}')) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
await offlineManager.deleteRemarkOffline(remarkId);
|
||
await displayPendingRemarks();
|
||
await updatePendingBadge();
|
||
} catch (error) {
|
||
console.error('Erreur lors de la suppression:', error);
|
||
alert('{% translate "Erreur lors de la suppression de la remarque" %}');
|
||
}
|
||
}
|
||
|
||
// Supprimer une photo d'une remarque en attente
|
||
async function deletePendingPhoto(remarkId, photoId) {
|
||
if (!offlineManager) return;
|
||
|
||
if (!confirm('{% translate "Voulez-vous vraiment supprimer cette image ?" %}')) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
await offlineManager.deletePhotoOffline(remarkId, photoId);
|
||
await displayPendingRemarks();
|
||
} catch (error) {
|
||
console.error('Erreur lors de la suppression:', error);
|
||
alert("{% translate "Erreur lors de la suppression de l\'image" %}");
|
||
}
|
||
}
|
||
|
||
// Mettre à jour les boutons des remarques existantes selon le mode online/offline
|
||
function updateExistingRemarksButtons() {
|
||
const isOnline = (typeof offlineManager !== 'undefined' && offlineManager) ? offlineManager.isOnline : navigator.onLine;
|
||
|
||
// Sélectionner tous les boutons d'action des remarques existantes (pas les pending)
|
||
document.querySelectorAll('#remarks-list [id^="buttons-"]').forEach(buttonContainer => {
|
||
const buttons = buttonContainer.querySelectorAll('button, a');
|
||
buttons.forEach(button => {
|
||
if (!isOnline) {
|
||
button.disabled = true;
|
||
button.classList.add('opacity-50');
|
||
button.style.pointerEvents = 'none';
|
||
button.title = '{% translate "Action non disponible hors ligne" %}';
|
||
} else {
|
||
button.disabled = false;
|
||
button.classList.remove('opacity-50');
|
||
button.style.pointerEvents = '';
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// Écouter les changements de connectivité pour mettre à jour les boutons
|
||
window.addEventListener('online', () => {
|
||
updateExistingRemarksButtons();
|
||
});
|
||
|
||
window.addEventListener('offline', () => {
|
||
updateExistingRemarksButtons();
|
||
});
|
||
|
||
// Bouton de synchronisation forcée
|
||
document.getElementById('force-sync-btn')?.addEventListener('click', async () => {
|
||
if (offlineManager) {
|
||
await offlineManager.syncPendingData();
|
||
await updatePendingBadge();
|
||
await displayPendingRemarks();
|
||
}
|
||
});
|
||
<!-- Modal Carrousel pour les photos d'une remarque -->
|
||
<div class="modal fade" id="remarkPhotoCarouselModal" tabindex="-1" aria-labelledby="remarkPhotoCarouselModalLabel" aria-hidden="true">
|
||
<div class="modal-dialog modal-xl modal-dialog-centered">
|
||
<div class="modal-content bg-dark text-white shadow-lg border-0">
|
||
<div class="modal-header border-bottom border-secondary py-2 px-3">
|
||
<div class="d-flex align-items-center gap-2">
|
||
<i class="bi bi-images text-primary fs-5"></i>
|
||
<h5 class="modal-title fs-6 mb-0 text-white" id="remarkPhotoCarouselModalLabel">{% translate "Photos de la remarque" %}</h5>
|
||
<span class="badge bg-primary rounded-pill px-2 py-1 fs-7" id="remark-carousel-counter">1 / 1</span>
|
||
</div>
|
||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="{% translate 'Fermer' %}"></button>
|
||
</div>
|
||
<div class="modal-body p-0 position-relative bg-black d-flex align-items-center justify-content-center overflow-hidden" style="min-height: 300px; max-height: 80vh;">
|
||
<div id="remarkPhotoCarousel" class="carousel slide w-100 h-100 d-flex flex-column justify-content-center" data-bs-ride="false">
|
||
<div class="carousel-indicators mb-2" id="remark-carousel-indicators"></div>
|
||
<div class="carousel-inner h-100" id="remark-carousel-inner"></div>
|
||
<button class="carousel-control-prev" type="button" data-bs-target="#remarkPhotoCarousel" data-bs-slide="prev" style="z-index: 10;">
|
||
<span class="carousel-control-prev-icon p-3 rounded-circle bg-dark bg-opacity-75" aria-hidden="true"></span>
|
||
<span class="visually-hidden">{% translate "Précédent" %}</span>
|
||
</button>
|
||
<button class="carousel-control-next" type="button" data-bs-target="#remarkPhotoCarousel" data-bs-slide="next" style="z-index: 10;">
|
||
<span class="carousel-control-next-icon p-3 rounded-circle bg-dark bg-opacity-75" aria-hidden="true"></span>
|
||
<span class="visually-hidden">{% translate "Suivant" %}</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer border-top border-secondary py-2 px-3 d-flex justify-content-between align-items-center bg-dark">
|
||
<div class="small text-muted text-truncate me-2" id="remark-carousel-caption"></div>
|
||
<div class="d-flex align-items-center gap-2">
|
||
<a id="remark-carousel-hd-btn" href="#" target="_blank" class="btn btn-sm btn-outline-light d-inline-flex align-items-center gap-1" title="{% translate 'Ouvrir en plein écran HD' %}">
|
||
<i class="bi bi-box-arrow-up-right"></i> <span class="d-none d-sm-inline">{% translate "Plein écran" %}</span>
|
||
</a>
|
||
{% if can_change_control %}
|
||
<button type="button" id="remark-carousel-delete-btn" class="btn btn-sm btn-outline-danger d-inline-flex align-items-center gap-1" title="{% translate 'Supprimer cette photo' %}">
|
||
<i class="bi bi-trash"></i> <span class="d-none d-sm-inline">{% translate "Supprimer" %}</span>
|
||
</button>
|
||
{% endif %}
|
||
<button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">{% translate "Fermer" %}</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</script>
|
||
|
||
{% endblock content %}
|
||
|
||
|
||
{% block extra_scripts %}
|
||
|
||
<!-- Convertir plans_data en JSON et le rendre accessible en JavaScript -->
|
||
{{ plans_data|json_script:"plans-data" }}
|
||
{{ remarks_data|json_script:"remarks-data" }}
|
||
|
||
<script>
|
||
const plansData = JSON.parse(document.getElementById('plans-data').textContent);
|
||
var remarksData = JSON.parse(document.getElementById('remarks-data').textContent);
|
||
const autocomplete_contract_posts_url = "{% url 'contracts:autocomplete-posts' %}";
|
||
</script>
|
||
|
||
|
||
<!-- jQuery 3.7.1 (local) -->
|
||
<script src="{% static 'common/jquery.min.js' %}"></script>
|
||
<script src="{% static 'common/maplibre-gl.js' %}"></script>
|
||
<script src="{% static 'controls/offline-manager.js' %}"></script>
|
||
<script src="{% static 'controls/controls_detail.js' %}"></script>
|
||
<script src="{% static 'common/clearable_inputs.js' %}"></script>
|
||
|
||
{% endblock extra_scripts %}
|