191 lines
7.7 KiB
HTML
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 %}
|