771 lines
No EOL
42 KiB
HTML
771 lines
No EOL
42 KiB
HTML
{% extends "documents/base_documents.html" %}
|
||
{% load i18n documents_tags static %}
|
||
{% load short_name %}
|
||
|
||
{% block head %}
|
||
{{ block.super }}
|
||
<link rel="stylesheet" href="{% static 'common/jquery-ui.min.css' %}" />
|
||
<style>
|
||
.ui-autocomplete {
|
||
z-index: 2000 !important;
|
||
max-height: 250px;
|
||
overflow-y: auto;
|
||
overflow-x: hidden;
|
||
}
|
||
</style>
|
||
{% endblock head %}
|
||
|
||
{% block content %}
|
||
<div class="container-fluid py-4">
|
||
<div class="d-flex justify-content-between align-items-start flex-wrap gap-2 mb-3">
|
||
<div>
|
||
<h2 class="mb-1">{{ document.title }}</h2>
|
||
<p class="text-muted mb-0">{{ document.description|default:_("No description provided") }}</p>
|
||
</div>
|
||
<div class="btn-group">
|
||
{% if document.georeference_coords %}
|
||
<a class="btn btn-outline-success" href="{% url 'documents:map_view' document.pk %}" target="_blank">
|
||
<i class="bi bi-map"></i> {% translate "Afficher sur la carte" %}
|
||
</a>
|
||
{% endif %}
|
||
{% if is_ifc or document.get_file_extension.lower == '.ifc' %}
|
||
<a class="btn btn-outline-primary" href="{% url 'bim:viewer' document.uuid %}" target="_blank">
|
||
<i class="bi bi-box-seam"></i> {% translate "Visualiser la maquette 3D (BIM)" %}
|
||
</a>
|
||
{% endif %}
|
||
{% if is_dxf and intersection %}
|
||
<a class="btn btn-outline-info" href="{% url 'assets:import_dxf' intersection.id %}?dxf_file={{ document_filename }}">
|
||
<i class="bi bi-file-earmark-code"></i> {% translate "Importer les couches vectorielles" %}
|
||
</a>
|
||
{% endif %}
|
||
{% if can_edit_document and document.latest_version %}
|
||
<button type="button" class="btn btn-outline-primary" data-bs-toggle="modal" data-bs-target="#geolocModeModal">
|
||
<i class="bi bi-geo-alt"></i> {% translate "Géolocaliser" %}
|
||
</button>
|
||
<a class="btn btn-outline-danger" href="{% url 'documents:delete' document.pk %}?next={% url 'documents:list' %}"><i class="bi bi-trash"></i> {% translate "Delete" %}</a>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
<nav aria-label="{% translate 'Breadcrumb' %}" class="mb-4">
|
||
<ol class="breadcrumb mb-0">
|
||
<li class="breadcrumb-item">
|
||
<a href="{% url 'documents:list' %}">{% translate "Document Management" %}</a>
|
||
</li>
|
||
{% for folder in breadcrumbs %}
|
||
<li class="breadcrumb-item {% if forloop.last %}active{% endif %}">
|
||
<a href="{% url 'documents:list' %}?folder={{ folder.slug }}">{{ folder.name }}</a>
|
||
</li>
|
||
{% endfor %}
|
||
<li class="breadcrumb-item active" aria-current="page">{{ document.title }}</li>
|
||
</ol>
|
||
</nav>
|
||
|
||
{# ── Row 1 : Version actuelle | Dossiers | Tags ── #}
|
||
<div class="row g-4 mb-4">
|
||
<div class="col-lg-4">
|
||
{% if document.latest_version %}
|
||
{% private_file_url document.latest_version 'file' as latest_version_url %}
|
||
<div class="card h-100">
|
||
<div class="card-body">
|
||
<h5 class="card-title">{% translate "Current version" %}</h5>
|
||
<p class="card-text mb-2">
|
||
<strong>v{{ document.latest_version.version_number }}</strong>
|
||
– {{ document.latest_version.upload_date|date:"SHORT_DATETIME_FORMAT" }}
|
||
{% if document.latest_version.uploaded_by %}
|
||
– {{ document.latest_version.uploaded_by|short_name }}
|
||
{% endif %}
|
||
</p>
|
||
{% if latest_version_url %}
|
||
<div class="d-flex flex-wrap gap-2">
|
||
<a class="btn btn-sm btn-outline-primary" href="{{ latest_version_url }}" target="_blank"><i class="bi bi-eye"></i> {% translate "Open" %}</a>
|
||
<a class="btn btn-sm btn-outline-secondary" href="{{ latest_version_url }}" download><i class="bi bi-download"></i> {% translate "Download" %}</a>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<div class="col-lg-4">
|
||
<div class="card h-100">
|
||
<div class="card-header d-flex justify-content-between align-items-center">
|
||
<h5 class="mb-0">{% translate "Folders" %}</h5>
|
||
{% if can_edit_document %}
|
||
<button class="btn btn-sm btn-outline-primary" type="button" data-bs-toggle="collapse" data-bs-target="#add-folder-form" aria-expanded="false">
|
||
<i class="bi bi-folder-plus"></i>
|
||
</button>
|
||
{% endif %}
|
||
</div>
|
||
{% if can_edit_document %}
|
||
<div class="collapse" id="add-folder-form">
|
||
<div class="card-body border-bottom">
|
||
<form method="post" action="{% url 'documents:add_folder' document.pk %}">
|
||
{% csrf_token %}
|
||
<div class="input-group input-group-sm">
|
||
<select name="folder_id" class="form-select" required>
|
||
<option value="">-- {% translate "Select a folder" %} --</option>
|
||
{% for f in available_folders %}
|
||
<option value="{{ f.pk }}">{{ f.name }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
<button type="submit" class="btn btn-primary">{% translate "Add" %}</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
<div class="card-body">
|
||
{% for folder in document.folders.all %}
|
||
<div class="d-flex justify-content-between align-items-center mb-2 gap-2">
|
||
<a href="{% url 'documents:list' %}?folder={{ folder.slug }}" class="badge bg-light text-dark text-decoration-none">
|
||
<i class="bi bi-folder"></i> {{ folder.name }}
|
||
</a>
|
||
{% with parents=folder.parent_folders.all %}
|
||
{% if parents %}
|
||
<span class="small text-muted flex-grow-1">{% translate "Parents" %}: {{ parents|join:', ' }}</span>
|
||
{% endif %}
|
||
{% endwith %}
|
||
{% if can_edit_document %}
|
||
<form method="post" action="{% url 'documents:remove_folder' document.pk folder.pk %}" class="ms-auto">
|
||
{% csrf_token %}
|
||
<button class="btn btn-sm btn-outline-danger" type="submit"><i class="bi bi-x"></i></button>
|
||
</form>
|
||
{% endif %}
|
||
</div>
|
||
{% empty %}
|
||
<p class="text-muted mb-0">{% translate "No folder associated" %}</p>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="col-lg-4">
|
||
<div class="card h-100">
|
||
<div class="card-header d-flex justify-content-between align-items-center">
|
||
<h5 class="mb-0">{% translate "Tags" %}</h5>
|
||
{% if can_edit_document %}
|
||
<button class="btn btn-sm btn-outline-primary" type="button" data-bs-toggle="collapse" data-bs-target="#add-tag-form" aria-expanded="false">
|
||
<i class="bi bi-tag"></i>
|
||
</button>
|
||
{% endif %}
|
||
</div>
|
||
{% if can_edit_document %}
|
||
<div class="collapse" id="add-tag-form">
|
||
<div class="card-body border-bottom">
|
||
<form method="post" action="{% url 'documents:add_tag' document.pk %}">
|
||
{% csrf_token %}
|
||
<div class="input-group input-group-sm">
|
||
<select name="tag_id" class="form-select" required>
|
||
<option value="">-- {% translate "Select a tag" %} --</option>
|
||
{% for t in available_tags %}
|
||
<option value="{{ t.pk }}">{{ t.name }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
<button type="submit" class="btn btn-primary">{% translate "Add" %}</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
<div class="card-body">
|
||
{% for tag in document.tags.all %}
|
||
<div class="d-inline-flex align-items-center gap-1 me-1 mb-1">
|
||
<a href="{% url 'documents:list' %}?tag={{ tag.slug }}" class="badge bg-secondary text-decoration-none">{{ tag.name }}</a>
|
||
{% if can_edit_document %}
|
||
<form method="post" action="{% url 'documents:remove_tag' document.pk tag.pk %}" class="d-inline">
|
||
{% csrf_token %}
|
||
<button class="btn btn-sm btn-link text-danger p-0 lh-1" type="submit"><i class="bi bi-x"></i></button>
|
||
</form>
|
||
{% endif %}
|
||
</div>
|
||
{% empty %}
|
||
<p class="text-muted mb-0">{% translate "No tag associated" %}</p>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{# ── Row 2 : Historique des versions | Prévisualisation ── #}
|
||
<div class="row g-4">
|
||
<div class="col-lg-4">
|
||
<div class="card" id="upload-version">
|
||
<div class="card-header d-flex justify-content-between align-items-center">
|
||
<h5 class="mb-0">{% translate "Version history" %}</h5>
|
||
</div>
|
||
<div class="card-body border-bottom">
|
||
<form method="post" enctype="multipart/form-data" action="{% url 'documents:add_version' document.pk %}">
|
||
{% csrf_token %}
|
||
<div class="mb-2">
|
||
<label class="form-label small fw-semibold mb-1">{% translate "File" %}</label>
|
||
{{ version_form.file }}
|
||
<div class="form-text text-muted mt-1 version-filename-display d-none"></div>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label class="form-label small fw-semibold mb-1">{% translate "Comment" %}</label>
|
||
{{ version_form.comment }}
|
||
</div>
|
||
<button class="btn btn-primary btn-sm w-100" type="submit"><i class="bi bi-upload me-1"></i>{% translate "Upload new version" %}</button>
|
||
{% if version_form.errors %}
|
||
<div class="text-danger small mt-2">
|
||
{{ version_form.errors }}
|
||
</div>
|
||
{% endif %}
|
||
</form>
|
||
</div>
|
||
<ul class="list-group list-group-flush">
|
||
{% for version in document.versions.all %}
|
||
{% private_file_url version 'file' as version_file_url %}
|
||
<li class="list-group-item">
|
||
<div class="d-flex justify-content-between align-items-start">
|
||
<div>
|
||
<h6 class="mb-1">
|
||
{% if version_file_url %}
|
||
<a href="#version-preview-card" class="version-preview-link text-decoration-none"
|
||
data-url="{{ version_file_url }}"
|
||
data-filename="{{ version.file.name }}"
|
||
data-preview-api-url="{% url 'documents:version_preview_data' document.pk version.pk %}">
|
||
<i class="bi bi-eye me-1 small opacity-50"></i>v{{ version.version_number }}
|
||
</a>
|
||
{% else %}
|
||
v{{ version.version_number }}
|
||
{% endif %}
|
||
</h6>
|
||
<small class="text-muted">
|
||
{{ version.upload_date|date:"SHORT_DATETIME_FORMAT" }} {% if version.uploaded_by %} {{ version.uploaded_by|short_name }}{% endif %}
|
||
</small>
|
||
{% if version.comment %}<p class="mb-0">{{ version.comment }}</p>{% endif %}
|
||
</div>
|
||
<div class="d-flex align-items-center gap-1">
|
||
{% if version_file_url %}
|
||
<div class="btn-group btn-group-sm">
|
||
<a class="btn btn-outline-primary" href="{{ version_file_url }}" target="_blank">{% translate "Open" %}</a>
|
||
<a class="btn btn-outline-secondary" href="{{ version_file_url }}" download>{% translate "Download" %}</a>
|
||
</div>
|
||
{% endif %}
|
||
{% if can_edit_document %}
|
||
<form method="post" action="{% url 'documents:delete_version' document.pk version.pk %}" onsubmit="return confirm('{% translate "Are you sure you want to delete this version?" %}');">
|
||
{% csrf_token %}
|
||
<button type="submit" class="btn btn-sm btn-outline-danger" title="{% translate "Delete version" %}">
|
||
<i class="bi bi-trash"></i>
|
||
</button>
|
||
</form>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
{% with validations=version.validations.all %}
|
||
{% if validations %}
|
||
<div class="mt-3">
|
||
<h6 class="fw-semibold">{% translate "Validations" %}</h6>
|
||
{% for validation in validations %}
|
||
{% with form=validation_forms|get_item:validation.id %}
|
||
<form method="post" action="{% url 'documents:validate' document.pk validation.pk %}" class="row g-2 align-items-center mb-2">
|
||
{% csrf_token %}
|
||
<div class="col-6">
|
||
<span class="badge bg-light text-dark w-100 text-wrap">{{ validation.role.name }}</span>
|
||
</div>
|
||
<div class="col-6 col-md-3">{{ validation.get_status_display }}</div>
|
||
<div class="col-6 col-md-3">{{ form.status }}</div>
|
||
<div class="col-12 col-md-6">{{ form.comment }}</div>
|
||
<div class="col-12 col-md-3 d-grid"><button class="btn btn-sm btn-outline-primary" type="submit">{% translate "Update" %}</button></div>
|
||
</form>
|
||
{% endwith %}
|
||
{% endfor %}
|
||
</div>
|
||
{% endif %}
|
||
{% endwith %}
|
||
</li>
|
||
{% empty %}
|
||
<li class="list-group-item text-center text-muted py-3">{% translate "No versions yet." %}</li>
|
||
{% endfor %}
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="col-lg-8">
|
||
<div class="card" id="version-preview-card">
|
||
<div class="card-header d-flex justify-content-between align-items-center">
|
||
<h5 class="mb-0">{% translate "Document preview" %} <small class="text-muted fw-normal fs-6" id="preview-version-label"></small></h5>
|
||
{% if document.latest_version %}
|
||
{% private_file_url document.latest_version 'file' as latest_version_url %}
|
||
{% if latest_version_url %}
|
||
<div class="btn-group btn-group-sm" id="preview-action-btns">
|
||
<a class="btn btn-outline-primary" href="{{ latest_version_url }}" target="_blank" id="preview-open-btn"><i class="bi bi-eye"></i> {% translate "Open" %}</a>
|
||
<a class="btn btn-outline-secondary" href="{{ latest_version_url }}" download id="preview-download-btn"><i class="bi bi-download"></i> {% translate "Download" %}</a>
|
||
</div>
|
||
{% endif %}
|
||
{% endif %}
|
||
</div>
|
||
<div class="card-body" id="version-preview-container">
|
||
{% if preview_mime_type %}
|
||
{% private_file_url document.latest_version 'file' as preview_url %}
|
||
{% if is_preview_pdf %}
|
||
<div class="ratio ratio-4x3 border rounded overflow-hidden">
|
||
<iframe src="{{ preview_url }}#toolbar=0" title="{% translate 'Document preview' %}" frameborder="0"></iframe>
|
||
</div>
|
||
{% elif is_preview_image %}
|
||
<img src="{{ preview_url }}" alt="{% translate 'Document preview' %}" class="img-fluid rounded border">
|
||
{% endif %}
|
||
{% elif is_preview_text %}
|
||
<pre class="bg-light border rounded p-3 mb-0" style="max-height:500px;overflow:auto;font-size:.85rem;white-space:pre-wrap;word-break:break-all;">{{ preview_text }}</pre>
|
||
{% if preview_truncated %}
|
||
<p class="text-muted small mt-2 mb-0">{% translate "File truncated to the first 50 000 characters." %}</p>
|
||
{% endif %}
|
||
{% elif is_preview_tabular %}
|
||
{% if preview_rows %}
|
||
<div class="table-responsive" style="max-height:500px;">
|
||
<table class="table table-sm table-bordered table-hover mb-0 align-top">
|
||
<thead class="table-light sticky-top">
|
||
<tr>{% for cell in preview_rows.0 %}<th scope="col">{{ cell }}</th>{% endfor %}</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for row in preview_rows|slice:"1:" %}
|
||
<tr>{% for cell in row %}<td>{{ cell }}</td>{% endfor %}</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
{% if preview_truncated %}
|
||
<p class="text-muted small mt-2 mb-0">{% translate "Only the first 200 rows are shown." %}</p>
|
||
{% endif %}
|
||
{% else %}
|
||
<p class="text-muted mb-0">{% translate "Empty file." %}</p>
|
||
{% endif %}
|
||
{% elif document.latest_version %}
|
||
<div class="text-center text-muted py-5">
|
||
<i class="bi bi-file-earmark-text display-6"></i>
|
||
<p class="mt-2 mb-0">{% translate "Preview is not available for this file type." %}</p>
|
||
</div>
|
||
{% else %}
|
||
<div class="text-center text-muted py-5">
|
||
<i class="bi bi-hourglass display-6"></i>
|
||
<p class="mt-2 mb-0">{% translate "No version available yet." %}</p>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="container-fluid pb-4">
|
||
<div class="row g-4">
|
||
<div class="col-lg-4">
|
||
<div class="card mb-0">
|
||
<div class="card-header"><h5 class="mb-0">{% translate "Linked items" %}</h5></div>
|
||
<div class="card-body">
|
||
<form method="post" action="{% url 'documents:attach' document.pk %}" class="mb-3">
|
||
{% csrf_token %}
|
||
{% if attachment_form.non_field_errors %}
|
||
<div class="alert alert-danger">{{ attachment_form.non_field_errors }}</div>
|
||
{% endif %}
|
||
<div class="mb-2">
|
||
{{ attachment_form.content_type }}
|
||
{% if attachment_form.content_type.errors %}
|
||
<div class="text-danger small">{{ attachment_form.content_type.errors }}</div>
|
||
{% endif %}
|
||
</div>
|
||
<div class="mb-2">
|
||
<input type="text" id="object_search_input" class="form-control" placeholder="{% translate "Search by code or name..." %}" disabled>
|
||
</div>
|
||
{{ attachment_form.object_id }}
|
||
{% if attachment_form.object_id.errors %}
|
||
<div class="mb-2 text-danger small">
|
||
{{ attachment_form.object_id.errors }}
|
||
</div>
|
||
{% endif %}
|
||
<div class="mb-2">
|
||
{{ attachment_form.note }}
|
||
{% if attachment_form.note.errors %}
|
||
<div class="text-danger small">{{ attachment_form.note.errors }}</div>
|
||
{% endif %}
|
||
</div>
|
||
<button class="btn btn-outline-primary w-100" type="submit">{% translate "Link" %}</button>
|
||
</form>
|
||
<ul class="list-group list-group-flush">
|
||
{% for attachment in document.attachments.all %}
|
||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||
{% if attachment.content_object.get_absolute_url %}
|
||
<a href="{{ attachment.content_object.get_absolute_url }}">{{ attachment.content_object }}</a>
|
||
{% else %}
|
||
<span>{{ attachment.content_object }}</span>
|
||
{% endif %}
|
||
<form method="post" action="{% url 'documents:detach' document.pk attachment.pk %}">
|
||
{% csrf_token %}
|
||
<button class="btn btn-sm btn-outline-danger" type="submit"><i class="bi bi-x"></i></button>
|
||
</form>
|
||
</li>
|
||
{% empty %}
|
||
<li class="list-group-item text-muted text-center">{% translate "No link yet" %}</li>
|
||
{% endfor %}
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="col-lg-4">
|
||
<div class="card mb-0">
|
||
<div class="card-header"><h5 class="mb-0">{% translate "Shares" %}</h5></div>
|
||
<div class="card-body">
|
||
<form method="post" action="{% url 'documents:share' document.pk %}" class="mb-3">
|
||
{% csrf_token %}
|
||
{% if share_form.non_field_errors %}
|
||
<div class="alert alert-danger">{{ share_form.non_field_errors }}</div>
|
||
{% endif %}
|
||
<div class="mb-2">
|
||
{{ share_form.user }}
|
||
{% if share_form.user.errors %}
|
||
<div class="text-danger small">{{ share_form.user.errors }}</div>
|
||
{% endif %}
|
||
</div>
|
||
<div class="mb-2">
|
||
{{ share_form.permission }}
|
||
{% if share_form.permission.errors %}
|
||
<div class="text-danger small">{{ share_form.permission.errors }}</div>
|
||
{% endif %}
|
||
</div>
|
||
<button type="submit" class="btn btn-primary w-100">{% translate "Share" %}</button>
|
||
</form>
|
||
<ul class="list-group list-group-flush">
|
||
{% for share in document.shares.all %}
|
||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||
<span>{{ share.user.get_full_name|default:share.user.username }} – {{ share.get_permission_display }}</span>
|
||
<form method="post" action="{% url 'documents:delete_share' document.pk share.pk %}">
|
||
{% csrf_token %}
|
||
<button class="btn btn-sm btn-outline-danger" type="submit"><i class="bi bi-x"></i></button>
|
||
</form>
|
||
</li>
|
||
{% empty %}
|
||
<li class="list-group-item text-muted text-center">{% translate "No share yet" %}</li>
|
||
{% endfor %}
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="col-lg-4">
|
||
<div class="card mb-0">
|
||
<div class="card-header"><h5 class="mb-0">{% translate "Shareable links" %}</h5></div>
|
||
<div class="card-body">
|
||
<form method="post" action="{% url 'documents:access_link' document.pk %}" class="mb-3">
|
||
{% csrf_token %}
|
||
{% if access_link_form.non_field_errors %}
|
||
<div class="alert alert-danger">{{ access_link_form.non_field_errors }}</div>
|
||
{% endif %}
|
||
<div class="mb-2">
|
||
{{ access_link_form.permission }}
|
||
{% if access_link_form.permission.errors %}
|
||
<div class="text-danger small">{{ access_link_form.permission.errors }}</div>
|
||
{% endif %}
|
||
</div>
|
||
<div class="mb-2">
|
||
{{ access_link_form.expires_at }}
|
||
{% if access_link_form.expires_at.errors %}
|
||
<div class="text-danger small">{{ access_link_form.expires_at.errors }}</div>
|
||
{% endif %}
|
||
</div>
|
||
<button class="btn btn-outline-primary w-100" type="submit">{% translate "Create link" %}</button>
|
||
</form>
|
||
<ul class="list-group list-group-flush">
|
||
{% for link in document.access_links.all %}
|
||
<li class="list-group-item">
|
||
<div class="d-flex justify-content-between align-items-center">
|
||
<div>
|
||
<a href="{% url 'documents:public_view' link.token %}" target="_blank">{{ link.token }}</a>
|
||
<div class="small text-muted">{{ link.get_permission_display }}{% if link.expires_at %} – {{ link.expires_at|date:"SHORT_DATETIME_FORMAT" }}{% endif %}</div>
|
||
</div>
|
||
<form method="post" action="{% url 'documents:revoke_link' document.pk link.pk %}">
|
||
{% csrf_token %}
|
||
<button class="btn btn-sm btn-outline-danger" type="submit"><i class="bi bi-x"></i></button>
|
||
</form>
|
||
</div>
|
||
</li>
|
||
{% empty %}
|
||
<li class="list-group-item text-muted text-center">{% translate "No shareable link" %}</li>
|
||
{% endfor %}
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
document.addEventListener('DOMContentLoaded', function () {
|
||
// --- File input filename display ---
|
||
const fileInput = document.querySelector('#upload-version input[type="file"]');
|
||
const display = document.querySelector('#upload-version .version-filename-display');
|
||
if (fileInput && display) {
|
||
fileInput.addEventListener('change', function () {
|
||
if (this.files && this.files.length > 0) {
|
||
display.textContent = '\uD83D\uDCC4 ' + this.files[0].name;
|
||
display.classList.remove('d-none');
|
||
} else {
|
||
display.textContent = '';
|
||
display.classList.add('d-none');
|
||
}
|
||
});
|
||
}
|
||
|
||
// --- Version preview switching ---
|
||
const previewContainer = document.getElementById('version-preview-container');
|
||
const previewLabel = document.getElementById('preview-version-label');
|
||
const previewOpenBtn = document.getElementById('preview-open-btn');
|
||
const previewDownloadBtn = document.getElementById('preview-download-btn');
|
||
|
||
// --- Preview helpers ---
|
||
const loadingHtml = `<div class="text-center text-muted py-5"><div class="spinner-border spinner-border-sm me-2" role="status"></div>{% trans "Loading…" %}</div>`;
|
||
const unsupportedHtml = `<div class="text-center text-muted py-5"><i class="bi bi-file-earmark-text display-6"></i><p class="mt-2 mb-0">{% trans "Preview is not available for this file type." %}</p></div>`;
|
||
|
||
function escHtml(s) {
|
||
return String(s)
|
||
.replace(/&/g, '&').replace(/</g, '<')
|
||
.replace(/>/g, '>').replace(/"/g, '"');
|
||
}
|
||
|
||
function buildTable(rows, truncated) {
|
||
if (!rows || !rows.length) return `<p class="text-muted">{% trans "Empty file." %}</p>`;
|
||
let html = '<div class="table-responsive" style="max-height:500px;"><table class="table table-sm table-bordered table-hover mb-0 align-top"><thead class="table-light sticky-top"><tr>';
|
||
for (const cell of rows[0]) html += `<th>${escHtml(cell)}</th>`;
|
||
html += '</tr></thead><tbody>';
|
||
for (let i = 1; i < rows.length; i++) {
|
||
html += '<tr>';
|
||
for (const cell of rows[i]) html += `<td>${escHtml(cell)}</td>`;
|
||
html += '</tr>';
|
||
}
|
||
html += '</tbody></table></div>';
|
||
if (truncated) html += `<p class="text-muted small mt-2 mb-0">{% trans "Only the first 200 rows are shown." %}</p>`;
|
||
return html;
|
||
}
|
||
|
||
function parseCSV(text) {
|
||
const lines = text.split(/\r?\n/).filter(l => l.trim());
|
||
if (!lines.length) return [];
|
||
// Sniff delimiter
|
||
const delim = (lines[0].split(';').length > lines[0].split(',').length) ? ';' : ',';
|
||
return lines.slice(0, 200).map(line => {
|
||
const row = [];
|
||
let inQuote = false, field = '';
|
||
for (let i = 0; i < line.length; i++) {
|
||
const ch = line[i];
|
||
if (ch === '"') {
|
||
if (inQuote && line[i + 1] === '"') { field += '"'; i++; }
|
||
else inQuote = !inQuote;
|
||
} else if (ch === delim && !inQuote) {
|
||
row.push(field); field = '';
|
||
} else {
|
||
field += ch;
|
||
}
|
||
}
|
||
row.push(field);
|
||
return row;
|
||
});
|
||
}
|
||
|
||
function updatePreviewControls(url, versionLabel) {
|
||
if (previewLabel) previewLabel.textContent = versionLabel ? '\u2014 ' + versionLabel : '';
|
||
if (previewOpenBtn) { previewOpenBtn.href = url; previewOpenBtn.closest('.btn-group')?.classList.remove('d-none'); }
|
||
if (previewDownloadBtn) { previewDownloadBtn.href = url; }
|
||
}
|
||
|
||
function renderPreview(url, filename, versionLabel, previewApiUrl) {
|
||
if (!previewContainer) return;
|
||
const name = (filename || '').toLowerCase();
|
||
updatePreviewControls(url, versionLabel);
|
||
|
||
if (name.endsWith('.pdf')) {
|
||
previewContainer.innerHTML = `<div class="ratio ratio-4x3 border rounded overflow-hidden"><iframe src="${url}#toolbar=0" frameborder="0"></iframe></div>`;
|
||
} else if (/\.(jpe?g|png|gif|webp|svg|bmp)$/.test(name)) {
|
||
previewContainer.innerHTML = `<img src="${url}" class="img-fluid rounded border" alt="">`;
|
||
} else if (/\.(txt|log)$/.test(name)) {
|
||
previewContainer.innerHTML = loadingHtml;
|
||
fetch(url)
|
||
.then(r => r.text())
|
||
.then(text => {
|
||
const truncated = text.length >= 50000;
|
||
const pre = document.createElement('pre');
|
||
pre.className = 'bg-light border rounded p-3 mb-0';
|
||
pre.style.cssText = 'max-height:500px;overflow:auto;font-size:.85rem;white-space:pre-wrap;word-break:break-all;';
|
||
pre.textContent = text.substring(0, 50000);
|
||
previewContainer.innerHTML = '';
|
||
previewContainer.appendChild(pre);
|
||
if (truncated) {
|
||
const note = document.createElement('p');
|
||
note.className = 'text-muted small mt-2 mb-0';
|
||
note.textContent = `{% trans "File truncated to the first 50 000 characters." %}`;
|
||
previewContainer.appendChild(note);
|
||
}
|
||
})
|
||
.catch(() => { previewContainer.innerHTML = unsupportedHtml; });
|
||
} else if (/\.csv$/.test(name)) {
|
||
previewContainer.innerHTML = loadingHtml;
|
||
fetch(url)
|
||
.then(r => r.text())
|
||
.then(text => {
|
||
const rows = parseCSV(text);
|
||
const truncated = text.split(/\r?\n/).filter(l => l.trim()).length > 200;
|
||
previewContainer.innerHTML = buildTable(rows, truncated);
|
||
})
|
||
.catch(() => { previewContainer.innerHTML = unsupportedHtml; });
|
||
} else if (/\.xlsx?$/.test(name) && previewApiUrl) {
|
||
previewContainer.innerHTML = loadingHtml;
|
||
fetch(previewApiUrl, {headers: {'X-Requested-With': 'XMLHttpRequest'}})
|
||
.then(r => r.json())
|
||
.then(data => {
|
||
if (data.type === 'tabular') {
|
||
previewContainer.innerHTML = buildTable(data.rows, data.truncated);
|
||
} else {
|
||
previewContainer.innerHTML = unsupportedHtml;
|
||
}
|
||
})
|
||
.catch(() => { previewContainer.innerHTML = unsupportedHtml; });
|
||
} else {
|
||
previewContainer.innerHTML = unsupportedHtml;
|
||
}
|
||
}
|
||
|
||
document.querySelectorAll('.version-preview-link').forEach(function (link) {
|
||
link.addEventListener('click', function (e) {
|
||
e.preventDefault();
|
||
const url = this.dataset.url;
|
||
const filename = this.dataset.filename;
|
||
const previewApiUrl = this.dataset.previewApiUrl || null;
|
||
const vLabel = this.textContent.trim();
|
||
renderPreview(url, filename, vLabel, previewApiUrl);
|
||
// Highlight active
|
||
document.querySelectorAll('.version-preview-link').forEach(l => l.classList.remove('fw-bold', 'text-primary'));
|
||
this.classList.add('fw-bold', 'text-primary');
|
||
// Smooth scroll to preview
|
||
document.getElementById('version-preview-card')?.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||
});
|
||
});
|
||
|
||
});
|
||
</script>
|
||
|
||
<!-- Modal de choix du mode de géolocalisation -->
|
||
<div class="modal fade" id="geolocModeModal" tabindex="-1" aria-labelledby="geolocModeModalLabel" aria-hidden="true">
|
||
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title" id="geolocModeModalLabel"><i class="bi bi-geo-alt-fill text-primary"></i> {% translate "Géolocaliser le plan" %}</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||
</div>
|
||
<div class="modal-body py-4">
|
||
<p class="text-muted text-center mb-4">Choisissez la méthode que vous souhaitez utiliser pour positionner ce plan sur la carte.</p>
|
||
<div class="row g-4">
|
||
<!-- Option 1: Superposition -->
|
||
<div class="col-md-6">
|
||
<div class="card h-100 border-2 border-hover-primary text-center p-3 shadow-sm" style="cursor: pointer;" onclick="location.href='{% url 'documents:georeference' document.pk %}?mode=overlay'">
|
||
<div class="card-body">
|
||
<div class="mb-3 text-primary" style="font-size: 3rem;">
|
||
<i class="bi bi-layers-half"></i>
|
||
</div>
|
||
<h5 class="card-title fw-bold">1. Superposition (Overlay)</h5>
|
||
<p class="card-text text-muted small mt-2">
|
||
Déplacez, pivotez et étirez le plan directement sur la carte de manière interactive à l'aide de poignées. Idéal pour un positionnement rapide.
|
||
</p>
|
||
</div>
|
||
<div class="card-footer bg-transparent border-0">
|
||
<a href="{% url 'documents:georeference' document.pk %}?mode=overlay" class="btn btn-primary btn-sm">Choisir</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- Option 2: Points d'ancrage -->
|
||
<div class="col-md-6">
|
||
<div class="card h-100 border-2 border-hover-primary text-center p-3 shadow-sm" style="cursor: pointer;" onclick="location.href='{% url 'documents:georeference' document.pk %}?mode=anchor'">
|
||
<div class="card-body">
|
||
<div class="mb-3 text-primary" style="font-size: 3rem;">
|
||
<i class="bi bi-pin-map"></i>
|
||
</div>
|
||
<h5 class="card-title fw-bold">2. Points d'ancrage (Anchors)</h5>
|
||
<p class="card-text text-muted small mt-2">
|
||
Associez au moins 3 points précis du plan avec des points réels correspondants sur la carte. Recommandé pour une précision optimale.
|
||
</p>
|
||
</div>
|
||
<div class="card-footer bg-transparent border-0">
|
||
<a href="{% url 'documents:georeference' document.pk %}?mode=anchor" class="btn btn-primary btn-sm">Choisir</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<style>
|
||
.border-hover-primary:hover {
|
||
border-color: #0d6efd !important;
|
||
transition: border-color 0.2s ease-in-out;
|
||
}
|
||
</style>
|
||
{% endblock content %}
|
||
|
||
{% block script %}
|
||
{{ block.super }}
|
||
<script src="{% static 'common/jquery-ui.min.js' %}"></script>
|
||
<script>
|
||
$(document).ready(function() {
|
||
const $contentType = $('#id_content_type');
|
||
const $objectId = $('#id_object_id');
|
||
const $searchInput = $('#object_search_input');
|
||
|
||
$searchInput.autocomplete({
|
||
source: function(request, response) {
|
||
const ctId = $contentType.val();
|
||
if (!ctId) {
|
||
response([]);
|
||
return;
|
||
}
|
||
$.ajax({
|
||
url: "{% url 'documents:search_objects' document.pk %}",
|
||
dataType: "json",
|
||
data: {
|
||
content_type: ctId,
|
||
q: request.term
|
||
},
|
||
success: function(data) {
|
||
response(data.results.map(item => ({
|
||
label: item.text,
|
||
value: item.text,
|
||
id: item.id
|
||
})));
|
||
}
|
||
});
|
||
},
|
||
minLength: 1,
|
||
select: function(event, ui) {
|
||
$objectId.val(ui.item.id);
|
||
$searchInput.data('selected-text', ui.item.value);
|
||
}
|
||
});
|
||
|
||
$contentType.on('change', function() {
|
||
$searchInput.val('');
|
||
$objectId.val('');
|
||
$searchInput.data('selected-text', '');
|
||
if ($(this).val()) {
|
||
$searchInput.prop('disabled', false);
|
||
} else {
|
||
$searchInput.prop('disabled', true);
|
||
}
|
||
});
|
||
|
||
$searchInput.on('input', function() {
|
||
if ($(this).val() !== $(this).data('selected-text')) {
|
||
$objectId.val('');
|
||
$(this).data('selected-text', '');
|
||
}
|
||
});
|
||
|
||
if ($contentType.val()) {
|
||
$searchInput.prop('disabled', false);
|
||
} else {
|
||
$searchInput.prop('disabled', true);
|
||
}
|
||
});
|
||
</script>
|
||
{% endblock script %} |