loko/streetup/documents/templates/documents/document_detail.html

771 lines
No EOL
42 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% 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, '&amp;').replace(/</g, '&lt;')
.replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
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 %}