Upload files to "frontend/templates/partials"

This commit is contained in:
Thierry 2026-04-01 23:26:17 +02:00
parent c9685c4ce2
commit c4b3c41718
4 changed files with 331 additions and 21 deletions

View file

@ -6,22 +6,26 @@
type="button"
class="btn btn-ghost"
hx-get="/viewer/list"
hx-target="#viewer-panel"
hx-trigger="click"
hx-target="#cloud-list-body"
hx-swap="innerHTML"
hx-indicator="#cloud-list-spinner"
>
🔄 Actualiser
</button>
<span id="cloud-list-spinner" class="loading loading-spinner loading-sm htmx-indicator"></span>
</div>
{% if error %}
<!-- ✅ CORRECTION BUG 1 : cible séparée du bouton -->
<div id="cloud-list-body">
{% if error %}
<div class="alert alert-error">
<span>{{ error }}</span>
</div>
{% elif not pointclouds %}
{% elif not pointclouds %}
<p class="text-base-content/40 text-sm text-center py-8">
Aucun nuage disponible sur le serveur.
</p>
{% else %}
{% else %}
<p class="text-xs text-base-content/40 mb-3">{{ pointclouds|length }} nuage(s)</p>
<div class="overflow-x-auto">
<table class="table table-sm">
@ -36,25 +40,42 @@
</thead>
<tbody id="cloud-table-body">
{% for pc in pointclouds %}
<tr>
<td>{{ pc.id }}</td>
<td>{{ pc.size_mb }} MB</td>
<td>{{ pc.file_count }}</td>
<td>{{ pc.created|datetimeformat }}</td>
<td>
<a href="/admin/debug/{{ pc.id }}" class="btn btn-sm btn-ghost">🔍</a>
<a href="/viewer/{{ pc.id }}" class="btn btn-sm" target="_blank">👁️</a>
<button
type="button"
class="btn btn-sm btn-error"
onclick="if(confirm('Supprimer ce nuage ?')) window.location.href='/admin/delete/{{ pc.id }}'"
>🗑️</button>
</td>
</tr>
<tr>
<td class="font-mono text-xs">{{ pc.id }}</td>
<td>{{ pc.size_mb }} MB</td>
<td>{{ pc.file_count }}</td>
<td>{{ pc.created|datetimeformat }}</td>
<td class="flex gap-1 flex-wrap">
<a href="/admin/debug/{{ pc.id }}" class="btn btn-sm btn-ghost">🔍</a>
<a href="/viewer/{{ pc.id }}" class="btn btn-sm" target="_blank">👁️</a>
<!-- ✅ NOUVEAU : bouton Crop qui charge la section crop dans #crop-panel -->
<button
type="button"
class="btn btn-sm btn-warning"
hx-get="/crop?pc_id={{ pc.id }}"
hx-target="#crop-panel"
hx-swap="innerHTML"
onclick="document.getElementById('crop-panel').scrollIntoView({behavior:'smooth'})"
>✂️</button>
<button
type="button"
class="btn btn-sm btn-error"
hx-delete="/admin/delete/{{ pc.id }}"
hx-target="#cloud-list-body"
hx-swap="innerHTML"
hx-confirm="Supprimer le nuage {{ pc.id }} ?"
>🗑️</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
{% endif %}
</div>
</div>
</div>
<!-- ✅ NOUVEAU : section crop injectée dynamiquement ici -->
<div id="crop-panel" class="mt-4"></div>

View file

@ -0,0 +1,61 @@
{% if error %}
<div class="alert alert-error">
<span>{{ error }}</span>
</div>
{% elif not pointclouds %}
<p class="text-base-content/40 text-sm text-center py-8">
Aucun nuage disponible sur le serveur.
</p>
{% else %}
<p class="text-xs text-base-content/40 mb-3">{{ pointclouds|length }} nuage(s)</p>
<div class="overflow-x-auto">
<table class="table table-sm">
<thead>
<tr>
<th>ID</th>
<th>Taille</th>
<th>Fichiers</th>
<th>Créé le</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for pc in pointclouds %}
<tr>
<td class="font-mono text-xs max-w-[120px] truncate" title="{{ pc.id }}">{{ pc.id }}</td>
<td>{{ pc.size_mb }} MB</td>
<td>{{ pc.file_count }}</td>
<td>{{ pc.created|datetimeformat }}</td>
<td>
<div class="flex gap-1 flex-wrap">
<a href="/admin/debug/{{ pc.id }}" class="btn btn-xs btn-ghost" title="Debug">🔍</a>
<a href="/viewer/{{ pc.id }}" class="btn btn-xs" target="_blank" title="Visualiser">👁️</a>
<!-- ✅ Bouton crop : charge crop_section.html dans #crop-panel (hors de ce div) -->
<button
type="button"
class="btn btn-xs btn-warning"
title="Cropper ce nuage"
hx-get="/crop?pc_id={{ pc.id }}"
hx-target="#crop-panel"
hx-swap="innerHTML"
hx-on:htmx:after-swap="document.getElementById('crop-panel').scrollIntoView({behavior:'smooth'})"
>✂️ Crop</button>
<button
type="button"
class="btn btn-xs btn-error"
title="Supprimer"
hx-delete="/admin/delete/{{ pc.id }}"
hx-target="#cloud-list-body"
hx-swap="innerHTML"
hx-confirm="Supprimer le nuage {{ pc.id }} ?"
>🗑️</button>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}

View file

@ -0,0 +1,157 @@
<!-- crop_section.html — injecté dans #crop-panel (hors de viewer-panel) -->
<div class="card bg-base-100 shadow border border-warning/30" x-data="cropForm()">
<div class="card-body">
<!-- En-tête -->
<div class="flex items-center justify-between mb-2">
<h2 class="card-title text-base">
✂️ Crop du nuage
<span class="badge badge-warning font-mono text-xs">{{ pc_id }}</span>
</h2>
<button
type="button"
class="btn btn-ghost btn-sm"
onclick="document.getElementById('crop-panel').innerHTML = ''"
>✕ Fermer</button>
</div>
<p class="text-sm text-base-content/60 mb-4">
Renseignez les coordonnées de la boîte 3D à découper. Les valeurs sont dans
le système de coordonnées du nuage de points (en mètres ou en unités du fichier source).
</p>
<!-- Schéma visuel de la box -->
<div class="bg-base-200 rounded-lg p-4 mb-4 flex items-center justify-center gap-8">
<div class="text-center">
<div class="text-xs text-base-content/50 mb-1">Vue de dessus (XY)</div>
<svg width="110" height="75" viewBox="0 0 110 75">
<ellipse cx="55" cy="37" rx="50" ry="30" fill="none" stroke="currentColor" stroke-opacity="0.15" stroke-width="1" stroke-dasharray="3 2"/>
<rect x="18" y="14" width="74" height="46" rx="2" fill="none" stroke="#f59e0b" stroke-width="1.5" stroke-dasharray="4 2"/>
<text x="100" y="40" font-size="9" fill="currentColor" opacity="0.4">X</text>
<text x="52" y="9" font-size="9" fill="currentColor" opacity="0.4">Y</text>
</svg>
</div>
<div class="text-center">
<div class="text-xs text-base-content/50 mb-1">Vue de côté (XZ)</div>
<svg width="110" height="75" viewBox="0 0 110 75">
<ellipse cx="55" cy="37" rx="50" ry="25" fill="none" stroke="currentColor" stroke-opacity="0.15" stroke-width="1" stroke-dasharray="3 2"/>
<rect x="18" y="16" width="74" height="42" rx="2" fill="none" stroke="#f59e0b" stroke-width="1.5" stroke-dasharray="4 2"/>
<text x="100" y="40" font-size="9" fill="currentColor" opacity="0.4">X</text>
<text x="52" y="9" font-size="9" fill="currentColor" opacity="0.4">Z</text>
</svg>
</div>
</div>
<!-- Formulaire -->
<form
hx-post="/admin/crop/{{ pc_id }}"
hx-target="#crop-result"
hx-swap="innerHTML"
hx-indicator="#crop-spinner"
>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-4">
<!-- X -->
<div>
<div class="text-xs font-semibold text-base-content/50 uppercase tracking-wider mb-2">Axe X</div>
<div class="flex flex-col gap-2">
<div>
<label class="label py-0 pb-1"><span class="label-text text-xs">Min X</span></label>
<input type="number" step="any" name="minX" x-model="box.minX"
class="input input-bordered input-sm w-full font-mono" placeholder="ex: 100.0" required/>
</div>
<div>
<label class="label py-0 pb-1"><span class="label-text text-xs">Max X</span></label>
<input type="number" step="any" name="maxX" x-model="box.maxX"
class="input input-bordered input-sm w-full font-mono" placeholder="ex: 500.0" required/>
</div>
</div>
</div>
<!-- Y -->
<div>
<div class="text-xs font-semibold text-base-content/50 uppercase tracking-wider mb-2">Axe Y</div>
<div class="flex flex-col gap-2">
<div>
<label class="label py-0 pb-1"><span class="label-text text-xs">Min Y</span></label>
<input type="number" step="any" name="minY" x-model="box.minY"
class="input input-bordered input-sm w-full font-mono" placeholder="ex: 200.0" required/>
</div>
<div>
<label class="label py-0 pb-1"><span class="label-text text-xs">Max Y</span></label>
<input type="number" step="any" name="maxY" x-model="box.maxY"
class="input input-bordered input-sm w-full font-mono" placeholder="ex: 800.0" required/>
</div>
</div>
</div>
<!-- Z -->
<div>
<div class="text-xs font-semibold text-base-content/50 uppercase tracking-wider mb-2">Axe Z (altitude)</div>
<div class="flex flex-col gap-2">
<div>
<label class="label py-0 pb-1"><span class="label-text text-xs">Min Z</span></label>
<input type="number" step="any" name="minZ" x-model="box.minZ"
class="input input-bordered input-sm w-full font-mono" placeholder="ex: 10.0" required/>
</div>
<div>
<label class="label py-0 pb-1"><span class="label-text text-xs">Max Z</span></label>
<input type="number" step="any" name="maxZ" x-model="box.maxZ"
class="input input-bordered input-sm w-full font-mono" placeholder="ex: 50.0" required/>
</div>
</div>
</div>
</div>
<!-- Résumé dimensions en temps réel -->
<div class="bg-base-200 rounded p-3 mb-4 text-xs font-mono" x-show="isValid()">
<span class="text-base-content/60 mr-2">Dimensions :</span>
<span class="text-warning">ΔX=<span x-text="delta('X')"></span></span>
<span class="mx-2 text-base-content/30">|</span>
<span class="text-warning">ΔY=<span x-text="delta('Y')"></span></span>
<span class="mx-2 text-base-content/30">|</span>
<span class="text-warning">ΔZ=<span x-text="delta('Z')"></span></span>
</div>
<div class="flex items-center gap-3">
<button
type="submit"
class="btn btn-warning"
:disabled="!isValid()"
>
✂️ Lancer le crop
</button>
<div id="crop-spinner" class="loading loading-spinner loading-sm htmx-indicator text-warning"></div>
<span class="text-xs text-base-content/50">Traitement PDAL — peut prendre plusieurs minutes</span>
</div>
</form>
<div id="crop-result" class="mt-4"></div>
</div>
</div>
<script>
function cropForm() {
return {
box: { minX: '', minY: '', minZ: '', maxX: '', maxY: '', maxZ: '' },
isValid() {
const keys = ['minX', 'minY', 'minZ', 'maxX', 'maxY', 'maxZ'];
if (keys.some(k => this.box[k] === '' || isNaN(parseFloat(this.box[k])))) return false;
return (
parseFloat(this.box.minX) < parseFloat(this.box.maxX) &&
parseFloat(this.box.minY) < parseFloat(this.box.maxY) &&
parseFloat(this.box.minZ) < parseFloat(this.box.maxZ)
);
},
delta(axis) {
const min = parseFloat(this.box['min' + axis]);
const max = parseFloat(this.box['max' + axis]);
if (isNaN(min) || isNaN(max) || min >= max) return '—';
return (max - min).toFixed(3);
}
}
}
</script>

View file

@ -0,0 +1,71 @@
<!-- upload_form.html — partial HTMX uniquement (sans navbar ni layout) -->
<!-- Injecté dans #main-content lors du clic sur le tab Upload -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- Colonne gauche : upload -->
<div class="lg:col-span-1 flex flex-col gap-4">
<!-- Configuration Backend -->
<div id="backend-config-panel" hx-get="/admin/backend-config" hx-target="this" hx-swap="innerHTML" hx-trigger="load">
{% include "partials/backend_config.html" %}
</div>
<div class="card bg-base-100 shadow">
<div class="card-body">
<h2 class="card-title text-base mb-2">📤 Upload</h2>
<p class="text-sm text-base-content/60 mb-3">
Formats acceptés : LAS, LAZ, PLY, XYZ, PTS
</p>
<form
hx-post="/upload"
hx-target="#upload-result"
hx-swap="innerHTML"
hx-encoding="multipart/form-data"
hx-indicator="#upload-spinner"
>
<input
type="file"
name="file"
accept=".las,.laz,.ply,.xyz,.pts"
class="file-input file-input-bordered w-full mb-4"
required
>
<div class="flex items-center gap-3">
<button
type="submit"
class="btn"
>
📤 Uploader & convertir
</button>
<div id="upload-spinner" class="loading loading-spinner loading-sm htmx-indicator"></div>
</div>
</form>
</div>
</div>
<div id="upload-result"></div>
</div>
<!-- Colonne droite : viewer -->
<div class="lg:col-span-2">
<div
id="viewer-container"
class="card bg-base-100 shadow min-h-[600px] flex items-center justify-center"
>
<p class="text-base-content/40 text-sm">
Uploadez un fichier pour lancer la visualisation
</p>
</div>
<div
id="viewer-panel"
class="card bg-base-100 shadow mt-4"
hx-get="/viewer/list"
hx-target="#cloud-list-body"
hx-swap="innerHTML"
hx-trigger="load"
>
{% include "partials/cloud_list.html" %}
</div>
</div>
</div>