Add job brackets for grouping multi-part or repeated jobs

- JobBracket model: name, customer, notes
- Migration 0006: job_brackets table + bracket_id FK on print_jobs
- /brackets: list with per-bracket progress bar
- /brackets/{id}: detail with jobs table + Add Job(s) modal (quantity field
  creates N identical jobs in one shot, each with its own UUID and log entry)
- /jobs Add Job modal: optional bracket selector
- Sidebar: Brackets nav item added

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Martin Hohenberg
2026-06-18 22:03:01 +02:00
parent 977e0deb24
commit ccf6a64787
9 changed files with 398 additions and 1 deletions

View File

@@ -0,0 +1,87 @@
{% extends "base.html" %}
{% block title %}Brackets — POPS{% endblock %}
{% block content %}
<div class="d-flex align-items-center justify-content-between mb-4">
<h5 class="text-white mb-0">Job Brackets</h5>
<button class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#addBracketModal">
<i class="bi bi-plus-lg me-1"></i>New Bracket
</button>
</div>
<div class="card">
{% if brackets %}
<table class="table table-dark table-hover mb-0 align-middle">
<thead>
<tr class="text-secondary small">
<th class="fw-normal">Name</th>
<th class="fw-normal">Customer</th>
<th class="fw-normal">Progress</th>
<th class="fw-normal text-end">Created</th>
</tr>
</thead>
<tbody>
{% for b in brackets %}
{% set total = b.jobs | length %}
{% set done = b.jobs | selectattr('status.value', 'equalto', 'done') | list | length %}
{% set pct = (done / total * 100) | int if total else 0 %}
<tr>
<td><a href="/brackets/{{ b.id }}" class="text-white text-decoration-none fw-medium">{{ b.name }}</a></td>
<td class="text-secondary">{{ b.customer or '—' }}</td>
<td style="min-width:200px">
{% if total %}
<div class="d-flex align-items-center gap-2">
<div class="progress flex-grow-1" style="height:6px">
<div class="progress-bar bg-success" style="width:{{ pct }}%"></div>
</div>
<span class="text-secondary small">{{ done }}/{{ total }}</span>
</div>
{% else %}
<span class="text-secondary small">no jobs</span>
{% endif %}
</td>
<td class="text-secondary small text-end">{{ b.created_at.strftime('%d.%m.%Y') }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="card-body text-secondary">No brackets yet.</div>
{% endif %}
</div>
{# ── Create Bracket Modal ── #}
<div class="modal fade" id="addBracketModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content bg-dark border-secondary">
<form method="POST" action="/brackets">
<div class="modal-header border-secondary">
<h6 class="modal-title text-white">New Job Bracket</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>
<label class="form-label text-secondary small">Name <span class="text-danger">*</span></label>
<input type="text" name="name" class="form-control bg-dark border-secondary text-white"
placeholder="e.g. Chair set, 30× phone stand" required autofocus>
</div>
<div>
<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>
<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</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}