feat: viewer local upload, Bambu collapse, planning done/fail, stats waste, portal link in jobs, sidebar collapsible, settings layout fix
Deploy via Portainer / deploy (push) Successful in 0s

This commit is contained in:
Jo
2026-07-07 09:16:24 +02:00
parent 7b65c4b2e2
commit b22e5e0fb4
9 changed files with 502 additions and 78 deletions
+72 -6
View File
@@ -148,9 +148,20 @@
<div class="modal-body">
<input type="hidden" id="slotModalId">
<p id="slotModalInfo" class="text-muted small"></p>
<!-- Quick action buttons -->
<div class="d-flex gap-2 mb-3">
<button class="btn btn-sm btn-success flex-fill" onclick="quickAction('done')">
<i class="bi bi-check-circle me-1"></i>Terminé
</button>
<button class="btn btn-sm btn-danger flex-fill" onclick="quickAction('fail')">
<i class="bi bi-exclamation-triangle me-1"></i>Échec
</button>
</div>
<div class="mb-3">
<label class="form-label fw-semibold">Statut</label>
<select id="slotModalStatus" class="form-select">
<select id="slotModalStatus" class="form-select" onchange="onStatusChange()">
<option value="planned">Planifié</option>
<option value="printing">En cours</option>
<option value="done">Terminé ✓</option>
@@ -159,9 +170,19 @@
</select>
</div>
<div class="mb-3" id="slotModalNoteGroup">
<label class="form-label">Note (optionnel)</label>
<label class="form-label">Note d'échec (optionnel)</label>
<input type="text" id="slotModalNote" class="form-control" placeholder="Ex: couche 45 — sous-extrusion">
</div>
<div class="mb-3" id="slotModalWastedGroup" style="display:none">
<label class="form-label">Filament gaspillé (g) <small class="text-muted fw-normal">optionnel</small></label>
<input type="number" id="slotModalWasted" class="form-control" step="0.1" min="0"
placeholder="Poids plateau si vide">
</div>
<div class="mb-3" id="slotModalActualGroup" style="display:none">
<label class="form-label">Poids réel consommé (g) <small class="text-muted fw-normal">optionnel</small></label>
<input type="number" id="slotModalActual" class="form-control" step="0.1" min="0"
placeholder="Pour déduire du stock">
</div>
</div>
<div class="modal-footer d-flex justify-content-between">
<button class="btn btn-sm btn-outline-danger" onclick="deleteSlot()">
@@ -343,6 +364,13 @@ async function saveSlot() {
}
let currentSlot = null;
function onStatusChange() {
const status = document.getElementById('slotModalStatus').value;
document.getElementById('slotModalWastedGroup').style.display = status === 'failed' ? '' : 'none';
document.getElementById('slotModalActualGroup').style.display = status === 'done' ? '' : 'none';
}
function openSlotModal(slot) {
currentSlot = slot;
document.getElementById('slotModalId').value = slot.id;
@@ -350,17 +378,55 @@ function openSlotModal(slot) {
`${slot.job_name} · ${slot.printer_name} · ${formatDt(slot.planned_start)}`;
document.getElementById('slotModalStatus').value = slot.status || 'planned';
document.getElementById('slotModalNote').value = slot.failure_note || '';
document.getElementById('slotModalWasted').value = '';
document.getElementById('slotModalActual').value = '';
onStatusChange();
new bootstrap.Modal(document.getElementById('slotModal')).show();
}
async function quickAction(type) {
// Submit immediately via the dedicated fail/done endpoints
const id = document.getElementById('slotModalId').value;
const note = document.getElementById('slotModalNote').value;
const wasted_g = document.getElementById('slotModalWasted').value || '';
const actual_g = document.getElementById('slotModalActual').value || '';
if (type === 'done') {
const fd = new FormData();
fd.append('actual_g', actual_g);
await fetch(`/api/slots/${id}/done`, {method:'POST', body:fd});
} else {
const fd = new FormData();
fd.append('failure_note', note);
fd.append('wasted_g', wasted_g);
await fetch(`/api/slots/${id}/fail`, {method:'POST', body:fd});
}
bootstrap.Modal.getInstance(document.getElementById('slotModal')).hide();
location.reload();
}
async function updateSlotStatus() {
const id = document.getElementById('slotModalId').value;
const status = document.getElementById('slotModalStatus').value;
const note = document.getElementById('slotModalNote').value;
await fetch(`/api/slots/${id}/status`, {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({status, note})
});
const wasted = document.getElementById('slotModalWasted').value;
const actual = document.getElementById('slotModalActual').value;
if (status === 'done') {
const fd = new FormData();
fd.append('actual_g', actual);
await fetch(`/api/slots/${id}/done`, {method:'POST', body:fd});
} else if (status === 'failed') {
const fd = new FormData();
fd.append('failure_note', note);
fd.append('wasted_g', wasted);
await fetch(`/api/slots/${id}/fail`, {method:'POST', body:fd});
} else {
await fetch(`/api/slots/${id}/status`, {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({status, note})
});
}
bootstrap.Modal.getInstance(document.getElementById('slotModal')).hide();
location.reload();
}