251 lines
11 KiB
HTML
251 lines
11 KiB
HTML
{% extends "base.html" %}
|
||
{% block title %}Jobs — POPS{% endblock %}
|
||
|
||
{% block content %}
|
||
<div class="d-flex align-items-center justify-content-between mb-4">
|
||
<h5 class="text-white mb-0">Print Jobs</h5>
|
||
<button class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#addJobModal">
|
||
<i class="bi bi-plus-lg me-1"></i>Add Job
|
||
</button>
|
||
</div>
|
||
|
||
{# ── Search bar ── #}
|
||
<form method="GET" action="/jobs" class="mb-3">
|
||
<div class="input-group input-group-sm" style="max-width:360px">
|
||
<span class="input-group-text bg-dark border-secondary text-secondary"><i class="bi bi-search"></i></span>
|
||
<input type="text" name="q" value="{{ q }}"
|
||
class="form-control bg-dark border-secondary text-white"
|
||
placeholder="Search by customer or filename…">
|
||
{% if q %}
|
||
<a href="/jobs" class="btn btn-outline-secondary">×</a>
|
||
{% endif %}
|
||
</div>
|
||
</form>
|
||
{% if active_bracket %}
|
||
<div class="d-flex align-items-center gap-2 mb-3">
|
||
<span class="badge bg-secondary fs-6 fw-normal px-3 py-2">
|
||
<i class="bi bi-collection me-1"></i>{{ active_bracket.name }}
|
||
</span>
|
||
{% if bracket_gross_total is not none %}
|
||
<span class="badge bg-dark border border-secondary fs-6 fw-normal px-3 py-2 text-white">
|
||
<i class="bi bi-currency-euro me-1 text-secondary"></i>{{ "%.2f"|format(bracket_gross_total) }} <span class="text-secondary fw-normal" style="font-size:.75em">gross total</span>
|
||
</span>
|
||
{% endif %}
|
||
<a href="/jobs{% if q %}?q={{ q }}{% endif %}" class="text-secondary text-decoration-none small">× clear filter</a>
|
||
</div>
|
||
{% elif q %}
|
||
<p class="text-secondary small mb-3">Results for <strong class="text-white">{{ q }}</strong></p>
|
||
{% endif %}
|
||
|
||
<div class="card">
|
||
{% if jobs %}
|
||
<table class="table table-dark table-hover mb-0 align-middle" style="font-size:.875rem">
|
||
<thead>
|
||
<tr class="text-secondary small">
|
||
<th class="fw-normal"></th>
|
||
<th class="fw-normal">UUID</th>
|
||
<th class="fw-normal">Customer</th>
|
||
<th class="fw-normal">File</th>
|
||
<th class="fw-normal">Printer</th>
|
||
<th class="fw-normal">Material</th>
|
||
<th class="fw-normal">Status</th>
|
||
<th class="fw-normal text-end">Created</th>
|
||
<th class="fw-normal"></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for j in jobs %}
|
||
<tr>
|
||
<td style="width:24px">
|
||
{% if j.priority %}<i class="bi bi-exclamation-circle-fill text-warning" title="Priority"></i>{% endif %}
|
||
</td>
|
||
<td class="font-monospace text-secondary" style="font-size:.75rem">
|
||
{{ j.job_uuid[:8] if j.job_uuid else '—' }}
|
||
</td>
|
||
<td class="text-white">{{ j.customer or '—' }}</td>
|
||
<td><a href="/jobs/{{ j.id }}" class="text-secondary text-decoration-none">{{ j.file_name or j.name or '—' }}</a></td>
|
||
<td class="text-secondary">{{ j.printer.name if j.printer else '—' }}</td>
|
||
<td class="text-secondary">
|
||
{% if j.job_material %}
|
||
{% set count = spool_map.get((j.job_material, j.job_color or ''), 0) %}
|
||
{% if count >= 2 %}
|
||
<span class="text-success me-1" title="{{ count }} rolls available">●</span>
|
||
{% elif count == 1 %}
|
||
<span class="text-warning me-1" title="1 roll available">●</span>
|
||
{% else %}
|
||
<span class="text-danger me-1" title="No rolls in stock">●</span>
|
||
{% endif %}
|
||
{{ j.job_material }}{% if j.job_color %} · {{ j.job_color }}{% endif %}
|
||
{% else %}—{% endif %}
|
||
</td>
|
||
<td>
|
||
{% set s = j.status.value %}
|
||
{% if s == 'printing' %}<span class="badge bg-warning text-dark">printing</span>
|
||
{% elif s == 'done' %}<span class="badge bg-success">done</span>
|
||
{% elif s == 'failed' %}<span class="badge bg-danger">failed</span>
|
||
{% elif s == 'cancelled' %}<span class="badge bg-secondary">cancelled</span>
|
||
{% else %}<span class="badge border border-warning text-warning" style="background:#000">queued</span>
|
||
{% endif %}
|
||
</td>
|
||
<td class="text-secondary text-end">{{ j.created_at.strftime('%d.%m.%Y %H:%M') }}</td>
|
||
<td class="text-end" style="width:32px">
|
||
<form method="POST" action="/jobs/{{ j.id }}/duplicate" class="d-inline">
|
||
<button type="submit" class="btn btn-sm btn-link text-secondary p-0" title="Duplicate job">
|
||
<i class="bi bi-files"></i>
|
||
</button>
|
||
</form>
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
{% else %}
|
||
<div class="card-body text-secondary">
|
||
{% if q %}No jobs match "{{ q }}".{% else %}No jobs yet.{% endif %}
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
{% if hidden_count %}
|
||
{% set sep = '&' if (q or bracket_id) else '?' %}
|
||
{% set show_all_url = '/jobs?show_all=1' ~ ('&bracket_id=' ~ bracket_id if bracket_id else '') ~ ('&q=' ~ q if q else '') %}
|
||
<p class="text-secondary small mt-3 mb-0">
|
||
<i class="bi bi-archive me-1"></i>
|
||
{{ hidden_count }} finished job{{ 's' if hidden_count != 1 else '' }} older than 24 h hidden.
|
||
<a href="{{ show_all_url }}" class="text-secondary">Show all</a>
|
||
</p>
|
||
{% endif %}
|
||
|
||
{# ── Add Job Modal ── #}
|
||
<div class="modal fade" id="addJobModal" tabindex="-1">
|
||
<div class="modal-dialog modal-lg">
|
||
<div class="modal-content bg-dark border-secondary">
|
||
<form method="POST" action="/jobs" enctype="multipart/form-data">
|
||
<div class="modal-header border-secondary">
|
||
<h6 class="modal-title text-white">Add Print Job</h6>
|
||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||
</div>
|
||
<div class="modal-body d-flex flex-column gap-3">
|
||
|
||
<div class="row g-3">
|
||
<div class="col-md-6">
|
||
<label class="form-label text-secondary small">Printer <span class="text-secondary fw-normal">(assigned when starting)</span></label>
|
||
<select name="printer_id" class="form-select bg-dark border-secondary text-white">
|
||
<option value="">— unassigned —</option>
|
||
{% for p in printers %}
|
||
<option value="{{ p.id }}">{{ p.name }}{% if p.location %} ({{ p.location }}){% endif %}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
<div class="col-md-6">
|
||
<label class="form-label text-secondary small">Customer</label>
|
||
<input type="text" name="customer" class="form-control bg-dark border-secondary text-white"
|
||
placeholder="Customer name or reference">
|
||
</div>
|
||
</div>
|
||
|
||
{# ── STL file ── #}
|
||
<div>
|
||
<label class="form-label text-secondary small">STL / File</label>
|
||
<ul class="nav nav-pills mb-2" id="stlTabs" role="tablist">
|
||
<li class="nav-item">
|
||
<button class="nav-link active py-1 px-3" style="font-size:.8rem"
|
||
data-bs-toggle="pill" data-bs-target="#uploadPane" type="button">
|
||
<i class="bi bi-upload me-1"></i>Upload
|
||
</button>
|
||
</li>
|
||
<li class="nav-item">
|
||
<button class="nav-link py-1 px-3" style="font-size:.8rem"
|
||
data-bs-toggle="pill" data-bs-target="#libraryPane" type="button">
|
||
<i class="bi bi-folder2-open me-1"></i>Library
|
||
<span class="badge bg-secondary ms-1">{{ stl_files | length }}</span>
|
||
</button>
|
||
</li>
|
||
</ul>
|
||
<div class="tab-content">
|
||
<div class="tab-pane fade show active" id="uploadPane">
|
||
<input type="file" name="stl_file"
|
||
class="form-control bg-dark border-secondary text-white"
|
||
accept=".stl,.3mf,.gcode">
|
||
</div>
|
||
<div class="tab-pane fade" id="libraryPane">
|
||
{% if stl_files %}
|
||
<select name="stl_select" class="form-select bg-dark border-secondary text-white">
|
||
<option value="">— select file —</option>
|
||
{% for f in stl_files %}
|
||
{% set c = stl_cache.get(f) %}
|
||
<option value="{{ f }}">{{ f }}{% if c %} — {{ c.cube_x|round(1) }}×{{ c.cube_y|round(1) }}×{{ c.cube_z|round(1) }} mm · {{ c.volume_ccm }} cm³{% endif %}</option>
|
||
{% endfor %}
|
||
</select>
|
||
{% else %}
|
||
<p class="text-secondary small mb-0">No files in library yet. Upload one first.</p>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="row g-3">
|
||
<div class="col-md-6">
|
||
<label class="form-label text-secondary small">Material</label>
|
||
<select name="job_material" class="form-select bg-dark border-secondary text-white">
|
||
<option value="">— none —</option>
|
||
{% for m in materials %}<option>{{ m }}</option>{% endfor %}
|
||
</select>
|
||
</div>
|
||
<div class="col-md-6">
|
||
<label class="form-label text-secondary small">Color</label>
|
||
<select name="job_color" class="form-select bg-dark border-secondary text-white">
|
||
<option value="">— none —</option>
|
||
{% for c in colors %}<option>{{ c }}</option>{% endfor %}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-check form-switch mb-1">
|
||
<input class="form-check-input" type="checkbox" name="priority" id="priorityCheck" value="1">
|
||
<label class="form-check-label text-secondary small" for="priorityCheck">
|
||
<i class="bi bi-exclamation-circle text-warning me-1"></i>Priority
|
||
</label>
|
||
</div>
|
||
|
||
<div class="row g-3">
|
||
<div class="col-md-4">
|
||
<label class="form-label text-secondary small">
|
||
Amount
|
||
<span class="text-secondary fw-normal">(≥2 creates a bracket)</span>
|
||
</label>
|
||
<input type="number" name="amount"
|
||
class="form-control bg-dark border-secondary text-white"
|
||
value="1" min="1" max="500">
|
||
</div>
|
||
<div class="col-md-8">
|
||
<label class="form-label text-secondary small">
|
||
Bracket
|
||
<span class="text-secondary fw-normal">(auto-created when amount > 1)</span>
|
||
</label>
|
||
<select name="bracket_id" class="form-select bg-dark border-secondary text-white">
|
||
<option value="">— none / auto —</option>
|
||
{% for b in brackets %}
|
||
<option value="{{ b.id }}">{{ b.name }}{% if b.customer %} · {{ b.customer }}{% endif %}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<label class="form-label text-secondary small">Notes</label>
|
||
<textarea name="notes" class="form-control bg-dark border-secondary text-white"
|
||
rows="2" placeholder="Optional notes"></textarea>
|
||
</div>
|
||
|
||
</div>
|
||
<div class="modal-footer border-secondary">
|
||
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Cancel</button>
|
||
<button type="submit" class="btn btn-primary btn-sm">Create Job</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endblock %}
|