Files
photoBooth/frontend/templates/admin/print.html
T
admin 209f81aa3d
🚀 Deploy — JH Photomaton / 🔍 Vérification (push) Has been cancelled
🚀 Deploy — JH Photomaton / 🍓 Deploy sur le Pi (push) Has been cancelled
first commit
2026-07-16 00:55:29 +02:00

191 lines
7.7 KiB
HTML

{% extends "base.html" %}
{% block title %}Impression — JH Photomaton{% endblock %}
{% block nav_links %}
<a href="/admin">Dashboard</a>
<a href="/admin/gallery">Galerie admin</a>
<a href="/admin/print" class="active">Impression</a>
<a href="/admin/actions">Actions</a>
<a href="/gallery">Galerie publique</a>
<a href="/admin/logout" style="margin-left:auto;color:var(--text-dim)">Déconnexion</a>
{% endblock %}
{% block content %}
<div class="container">
<h1 class="page-title">Gestion de l'impression</h1>
<!-- Imprimantes -->
<div class="section">
<div class="card-title">Imprimantes CUPS</div>
<div class="card">
<table class="table" id="printers-table">
<thead><tr><th>Imprimante</th><th>Statut</th><th>Jobs en attente</th><th>Actions</th></tr></thead>
<tbody>
{% for p in printers %}
<tr>
<td class="font-bold">{{ p.label }}</td>
<td>
<span class="badge
{% if p.state == 'idle' %}badge-success
{% elif p.state == 'printing' %}badge-info
{% elif p.state == 'disabled' %}badge-warning
{% else %}badge-error{% endif %}
">
{% if p.state == 'idle' %}Disponible
{% elif p.state == 'printing' %}Impression
{% elif p.state == 'disabled' %}Désactivée
{% elif p.state == 'offline' %}Hors ligne
{% else %}{{ p.state }}{% endif %}
</span>
</td>
<td>{{ p.jobs }}</td>
<td>
<button class="btn btn-danger btn-sm" onclick="cancelCupsJobs('{{ p.name }}')">✕ Vider file</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- Mode impression -->
<div class="section">
<div class="card-title">Mode d'impression</div>
<div class="card">
<div class="flex gap-1 mb-1">
<label class="flex items-center gap-1" style="cursor:pointer">
<input type="radio" name="print-mode" value="direct" {% if config.print.mode=='direct' %}checked{% endif %}> Direct (impression automatique)
</label>
<label class="flex items-center gap-1" style="cursor:pointer">
<input type="radio" name="print-mode" value="validation" {% if config.print.mode=='validation' %}checked{% endif %}> Validation (admin confirme)
</label>
<label class="flex items-center gap-1" style="cursor:pointer">
<input type="radio" name="print-mode" value="gallery" {% if config.print.mode=='gallery' %}checked{% endif %}> Galerie (file uniquement)
</label>
</div>
<p class="text-xs text-muted">
<b>Direct</b> : impression lancée immédiatement sans confirmation. <b>Validation</b> : l'admin valide chaque impression. <b>Galerie</b> : les demandes s'accumulent, impression via la galerie admin.
</p>
</div>
</div>
<!-- File d'attente -->
<div class="section">
<div class="flex items-center justify-between mb-1">
<div class="card-title" style="margin:0">File d'attente (<span id="pending-count">{{ queue|length }}</span>)</div>
<button class="btn btn-ghost btn-sm" onclick="refreshQueue()">↻ Actualiser</button>
</div>
<div class="card">
<div id="queue-container">
{% if not queue %}
<div class="empty-state"><div class="icon"></div>Aucune impression en attente</div>
{% else %}
<table class="table" id="queue-table">
<thead><tr><th>Aperçu</th><th>Fichier</th><th>Copies</th><th>Statut</th><th>Demandé</th><th>Actions</th></tr></thead>
<tbody id="queue-tbody">
{% for q in queue %}
<tr id="row-{{ q.id }}">
<td><img src="{{ q.thumb_url }}" class="print-thumb" onerror="this.style.opacity=0"></td>
<td class="text-sm">{{ q.filename.split('/')[-1] }}</td>
<td>
<input type="number" value="{{ q.copies }}" min="1" max="3" id="copies-{{ q.id }}" style="width:50px;background:var(--surface2);border:1px solid var(--border);color:var(--text);border-radius:4px;padding:2px 6px">
</td>
<td>
<span class="badge
{% if q.status == 'pending' %}badge-warning
{% elif q.status == 'done' %}badge-success
{% elif q.status == 'printing' %}badge-info
{% else %}badge-error{% endif %}
">{{ q.status }}</span>
</td>
<td class="text-xs text-muted">{{ q.requested_at|int }}</td>
<td>
{% if q.status == 'pending' %}
<button class="btn btn-success btn-sm" onclick="executePrint('{{ q.id }}')">🖨 Imprimer</button>
<button class="btn btn-danger btn-sm" onclick="cancelPrint('{{ q.id }}')"></button>
{% elif q.status == 'done' %}
<span class="text-sm text-muted">{{ q.printer or '—' }}</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
// ── Mode impression ────────────────────────────────────────────────────────────
document.querySelectorAll('input[name="print-mode"]').forEach(r => {
r.addEventListener('change', async (e) => {
try {
await api('POST', `/api/print/mode?mode=${e.target.value}`);
showToast(`Mode: ${e.target.value}`, 'success');
} catch(e) { showToast('Erreur', 'error'); }
});
});
// ── File d'attente ─────────────────────────────────────────────────────────────
async function refreshQueue() {
try {
const data = await api('GET', '/api/print/queue');
const queue = data.queue || [];
const pending = queue.filter(q => q.status === 'pending');
document.getElementById('pending-count').textContent = pending.length;
const tbody = document.getElementById('queue-tbody');
if (!tbody) return;
// Mise à jour des statuts existants
queue.forEach(q => {
const row = document.getElementById('row-' + q.id);
if (row) {
const badge = row.querySelector('.badge');
if (badge) badge.textContent = q.status;
}
});
} catch(e) {}
}
async function executePrint(id) {
const copiesEl = document.getElementById('copies-' + id);
const copies = copiesEl ? parseInt(copiesEl.value) : 1;
try {
const r = await api('POST', `/api/print/execute/${id}?copies=${copies}`);
showToast(r.success ? '✅ Impression lancée sur ' + r.printer : '❌ ' + r.error, r.success ? 'success' : 'error');
setTimeout(refreshQueue, 1000);
} catch(e) { showToast('Erreur impression', 'error'); }
}
async function cancelPrint(id) {
try {
await api('POST', `/api/print/cancel/${id}`);
showToast('Annulé', 'info');
const row = document.getElementById('row-' + id);
if (row) row.remove();
} catch(e) { showToast('Erreur', 'error'); }
}
async function cancelCupsJobs(printer) {
if (!confirm(`Vider la file de ${printer} ?`)) return;
try {
const r = await api('POST', `/api/print/cups/cancel/${printer}`);
showToast(r.ok ? 'File vidée' : 'Erreur', r.ok ? 'success' : 'error');
refreshQueue();
} catch(e) { showToast('Erreur', 'error'); }
}
function onWsMessage(msg) {
if (['print_request','print_result','print_cancelled'].includes(msg.type)) refreshQueue();
}
setInterval(refreshQueue, 10000);
</script>
{% endblock %}