Reskin label list and add tape printing entry points

This commit is contained in:
jessikitty committed 2026-10-12 21:31:49 +11:00
1 parent 6433f1b172
commit 70443028e3
1 file changed
+129 -140
+129 -140
View File
@@ -1,64 +1,55 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}My Labels - Admin{% endblock %}
{% block title %}Parts Labels - Admin{% endblock %} {% block extra_css %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/labels-bricklink.css') }}">
{% endblock %}
{% block content %} {% block content %}
<div class="row mb-4"> <div class="bl">
<div class="col"> {% set section = 'labels' %}
<h1><i class="bi bi-tags"></i> Parts Drawer Labels</h1> {% set bar_sub = total_labels ~ ' label' ~ ('' if total_labels == 1 else 's') ~ ' defined' %}
<p class="text-muted mb-0">{{ total_labels }} label{{ '' if total_labels == 1 else 's' }} defined</p> {% set crumbs = [{'name': 'My Labels'}] + ([{'name': active_category.full_name}] if active_category else []) %}
</div> {% include 'admin/labels/_nav.html' %}
<div class="col-auto">
<a href="{{ url_for('labels.new_label', category=active_category.id if active_category else None) }}" class="btn btn-danger me-2">
<i class="bi bi-plus-circle"></i> New Label
</a>
<a href="{{ url_for('labels.categories') }}" class="btn btn-outline-secondary me-2">
<i class="bi bi-diagram-3"></i> Categories
</a>
<a href="{{ url_for('labels.settings') }}" class="btn btn-outline-secondary me-2">
<i class="bi bi-sliders"></i> Settings
</a>
<a href="{{ url_for('admin.dashboard') }}" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left"></i> Back
</a>
</div>
</div>
{% if not rebrickable_ready %} {% if not rebrickable_ready %}
<div class="alert alert-info"> <div class="bl-panel" style="border-top:0;border-radius:0 0 3px 3px;">
<i class="bi bi-info-circle"></i> <div class="bl-panel-body">
No Rebrickable API key yet, so part lookup is off. You can still add labels by hand and upload your own images. <span class="bl-tag warn">No API key</span>
<a href="{{ url_for('labels.settings') }}" class="alert-link">Add a key</a> &mdash; it's free. Part lookup and photos are off. You can still add labels by hand and upload your own
images, and the <a href="{{ url_for('catalog.index') }}">part catalogue</a> works
without a key. <a href="{{ url_for('labels.settings') }}">Add a key</a> &mdash; it's free.
</div>
</div> </div>
{% endif %} {% endif %}
<div class="row"> <div class="row g-2">
<!-- Category tree --> <!-- Drawer category tree -->
<div class="col-lg-3 mb-4"> <div class="col-lg-3">
<div class="card"> <div class="bl-panel">
<div class="card-header d-flex justify-content-between align-items-center"> <div class="bl-panel-head">
<strong><i class="bi bi-diagram-3"></i> Categories</strong> <span>My Categories</span>
<a href="{{ url_for('labels.index') }}" class="small text-decoration-none">All</a> <a href="{{ url_for('labels.index') }}" class="bl-btn bl-btn-sm">All</a>
</div> </div>
<div class="list-group list-group-flush" style="max-height: 70vh; overflow-y: auto;"> <div class="bl-panel-body tight bl-tree" style="max-height:66vh;overflow-y:auto;">
{% for root in roots %} {% for root in roots %}
<a href="{{ url_for('labels.index', category=root.id) }}" <a href="{{ url_for('labels.index', category=root.id) }}"
class="list-group-item list-group-item-action d-flex justify-content-between align-items-center {% if active_category and active_category.id == root.id %}active{% endif %}"> class="{% if active_category and active_category.id == root.id %}active{% endif %}">
<span> <span class="text-truncate">
<span class="badge rounded-pill me-1" style="background: {{ root.accent_colour() }};">&nbsp;</span> <span class="bl-swatch" style="background:{{ root.accent_colour() }};"></span>
{{ root.display_code }} {{ root.name }} <strong>{{ root.display_code }}</strong> {{ root.name }}
</span> </span>
<span class="badge bg-secondary rounded-pill">{{ root.label_count() }}</span> <span class="bl-count">{{ root.label_count() }}</span>
</a> </a>
{% for child in root.children %} {% for child in root.children %}
<a href="{{ url_for('labels.index', category=child.id) }}" <a href="{{ url_for('labels.index', category=child.id) }}"
class="list-group-item list-group-item-action ps-4 small d-flex justify-content-between align-items-center {% if active_category and active_category.id == child.id %}active{% endif %}"> class="child {% if active_category and active_category.id == child.id %}active{% endif %}">
<span class="text-truncate">&#8627; {{ child.name }}</span> <span class="text-truncate">{{ child.name }}</span>
<span class="badge bg-light text-dark rounded-pill">{{ child.label_count() }}</span> <span class="bl-count">{{ child.label_count() }}</span>
</a> </a>
{% endfor %} {% endfor %}
{% else %} {% else %}
<div class="list-group-item text-muted small">No categories yet.</div> <div class="bl-empty">No categories yet.</div>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
@@ -66,29 +57,31 @@
<!-- Labels --> <!-- Labels -->
<div class="col-lg-9"> <div class="col-lg-9">
<div class="card mb-3"> <div class="bl-panel">
<div class="card-body"> <div class="bl-panel-body">
<form method="GET" class="row g-2 align-items-end"> <form method="GET" class="bl-filters">
{% if active_category %}<input type="hidden" name="category" value="{{ active_category.id }}">{% endif %} {% if active_category %}<input type="hidden" name="category" value="{{ active_category.id }}">{% endif %}
<div class="col-md-8"> <div style="flex:2 1 280px;">
<label class="form-label small mb-1">Search labels</label> <label class="fld">Search my labels</label>
<input type="search" name="q" value="{{ search }}" class="form-control" <input type="search" name="q" value="{{ search }}" class="w-100"
placeholder="Part number, name, dimensions or drawer..."> placeholder="Part number, name, dimensions or drawer&hellip;">
</div>
<div class="col-md-4">
<button class="btn btn-outline-danger w-100"><i class="bi bi-search"></i> Search</button>
</div> </div>
<button class="bl-btn bl-btn-primary"><i class="bi bi-search"></i> Search</button>
<a href="{{ url_for('catalog.index') }}" class="bl-btn">
<i class="bi bi-box-seam"></i> Add from catalogue
</a>
</form> </form>
</div> </div>
</div> </div>
{% if rebrickable_ready %} {% if rebrickable_ready %}
<div class="card mb-3"> <div class="bl-panel">
<div class="card-body"> <div class="bl-panel-head"><span>Quick lookup on Rebrickable</span></div>
<label class="form-label small mb-1"><i class="bi bi-cloud-arrow-down"></i> Look up a part on Rebrickable</label> <div class="bl-panel-body">
<div class="input-group"> <div class="bl-filters">
<input type="text" id="lookupQuery" class="form-control" placeholder="e.g. 3001 or &quot;brick 2 x 4&quot;"> <input type="text" id="lookupQuery" style="flex:2 1 280px;"
<button class="btn btn-outline-secondary" type="button" id="lookupBtn">Search</button> placeholder="3001 or &quot;brick 2 x 4&quot;">
<button class="bl-btn" type="button" id="lookupBtn">Search</button>
</div> </div>
<div id="lookupResults" class="row g-2 mt-2"></div> <div id="lookupResults" class="row g-2 mt-2"></div>
</div> </div>
@@ -96,107 +89,101 @@
{% endif %} {% endif %}
<form method="POST" action="{{ url_for('labels.print_sheet') }}" id="printForm"> <form method="POST" action="{{ url_for('labels.print_sheet') }}" id="printForm">
<div class="card"> <div class="bl-panel">
<div class="card-header d-flex flex-wrap gap-2 justify-content-between align-items-center"> <div class="bl-panel-head">
<div> <span>
<input type="checkbox" class="form-check-input me-1" id="checkAll"> <input type="checkbox" id="checkAll"> Select all on page
<label for="checkAll" class="small">Select all on this page</label> </span>
</div> <span class="d-flex gap-1 align-items-center flex-wrap">
<div class="d-flex gap-2 align-items-center"> <select name="preset" style="max-width:240px;">
<select name="preset" class="form-select form-select-sm" style="min-width: 260px;">
{% for key, preset in presets.items() %} {% for key, preset in presets.items() %}
<option value="{{ key }}" {% if key == default_preset %}selected{% endif %}>{{ preset.name }}</option> <option value="{{ key }}" {% if key == default_preset %}selected{% endif %}>{{ preset.name }}</option>
{% endfor %} {% endfor %}
</select> </select>
<button class="btn btn-sm btn-danger" type="submit"> <button class="bl-btn bl-btn-primary" type="submit">
<i class="bi bi-printer"></i> Print selected <i class="bi bi-printer"></i> Sheet
</button> </button>
{% if active_category %} <button class="bl-btn bl-btn-go" type="submit"
<a href="{{ url_for('labels.print_sheet', category=active_category.id, preset=default_preset) }}" formaction="{{ url_for('tape.index') }}" formmethod="POST">
class="btn btn-sm btn-outline-danger">Print whole category</a> <i class="bi bi-upc"></i> Tape
{% endif %} </button>
</div> </span>
</div> </div>
<div class="bl-panel-body tight">
<div class="table-responsive"> <table class="bl-table">
<table class="table table-hover align-middle mb-0"> <thead>
<thead class="table-light">
<tr> <tr>
<th style="width: 38px;"></th> <th style="width:26px;"></th>
<th style="width: 60px;">Image</th> <th style="width:46px;">Img</th>
<th>Part</th> <th style="width:110px;">Part no.</th>
<th>Category</th> <th>Name</th>
<th>Drawer</th> <th style="width:170px;">Category</th>
<th style="width: 110px;"></th> <th style="width:150px;">Drawer</th>
<th style="width:80px;"></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for label in labels %} {% for label in labels %}
<tr> <tr>
<td><input type="checkbox" class="form-check-input label-check" name="ids" value="{{ label.id }}"></td> <td><input type="checkbox" class="label-check" name="ids" value="{{ label.id }}"></td>
<td> <td>
{% if label.get_image() %} {% if label.get_image() %}
<img src="{{ label.get_image() }}" alt="" style="width: 44px; height: 44px; object-fit: contain;"> <img class="thumb" src="{{ label.get_image() }}" alt="" loading="lazy">
{% else %}
<span class="text-muted"><i class="bi bi-image"></i></span>
{% endif %} {% endif %}
</td> </td>
<td class="num">{{ label.part_numbers_text }}</td>
<td>
<span class="bl-name">{{ label.name }}</span>
{% if label.dimensions %}<span class="bl-dim">{{ label.dimensions }}</span>{% endif %}
{% if label.colour_name %}<span class="bl-tag">{{ label.colour_name }}</span>{% endif %}
</td>
<td> <td>
<strong>{{ label.display_title }}</strong>
{% if label.part_numbers_text %}
<div class="small text-muted">{{ label.part_numbers_text }}</div>
{% endif %}
{% if label.colour_name %}
<span class="badge bg-light text-dark">{{ label.colour_name }}</span>
{% endif %}
</td>
<td class="small">
{% if label.category %} {% if label.category %}
<span class="badge rounded-pill" style="background: {{ label.category.accent_colour() }};">&nbsp;</span> <span class="bl-swatch" style="background:{{ label.category.accent_colour() }};"></span>
{{ label.category.full_name }} {{ label.category.full_name }}
{% else %} {% else %}<span class="bl-dim">Uncategorised</span>{% endif %}
<span class="text-muted">Uncategorised</span>
{% endif %}
</td> </td>
<td class="small">{{ label.location or '-' }}</td> <td>{{ label.location or '' }}</td>
<td class="text-end"> <td class="text-end">
<a href="{{ url_for('labels.edit_label', label_id=label.id) }}" class="btn btn-sm btn-outline-secondary"> <a href="{{ url_for('labels.edit_label', label_id=label.id) }}"
<i class="bi bi-pencil"></i> class="bl-btn bl-btn-sm"><i class="bi bi-pencil"></i></a>
</a> <a href="{{ url_for('tape.index', ids=label.id) }}"
<a href="{{ url_for('labels.print_sheet', ids=label.id, preset=default_preset) }}" class="btn btn-sm btn-outline-danger"> class="bl-btn bl-btn-sm" title="Print to tape"><i class="bi bi-upc"></i></a>
<i class="bi bi-printer"></i>
</a>
</td> </td>
</tr> </tr>
{% else %} {% else %}
<tr><td colspan="6" class="text-center text-muted py-4"> <tr><td colspan="7" class="bl-empty">
No labels here yet. No labels here yet.
<a href="{{ url_for('labels.new_label', category=active_category.id if active_category else None) }}">Add the first one</a>. <a href="{{ url_for('catalog.index') }}">Pick some from the catalogue</a>
or <a href="{{ url_for('labels.new_label', category=active_category.id if active_category else None) }}">add one by hand</a>.
</td></tr> </td></tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>
{% if pagination.pages > 1 %}
<div class="bl-pager">
<a class="bl-btn bl-btn-sm" href="{{ url_for('labels.index', page=pagination.prev_num, category=active_category.id if active_category else None, q=search) }}">&laquo; Prev</a>
<span>Page {{ pagination.page }} of {{ pagination.pages }}</span>
<a class="bl-btn bl-btn-sm" href="{{ url_for('labels.index', page=pagination.next_num, category=active_category.id if active_category else None, q=search) }}">Next &raquo;</a>
</div>
{% endif %}
</div> </div>
</form> </form>
{% if pagination.pages > 1 %} {% if active_category %}
<nav class="mt-3"> <div class="d-flex gap-1 justify-content-end">
<ul class="pagination justify-content-center"> <a href="{{ url_for('labels.print_sheet', category=active_category.id, preset=default_preset) }}"
<li class="page-item {% if not pagination.has_prev %}disabled{% endif %}"> class="bl-btn">Print whole category to sheet</a>
<a class="page-link" href="{{ url_for('labels.index', page=pagination.prev_num, category=active_category.id if active_category else None, q=search) }}">Previous</a> <a href="{{ url_for('tape.index', category=active_category.id) }}"
</li> class="bl-btn">&hellip;to tape</a>
<li class="page-item disabled"><span class="page-link">{{ pagination.page }} / {{ pagination.pages }}</span></li> </div>
<li class="page-item {% if not pagination.has_next %}disabled{% endif %}">
<a class="page-link" href="{{ url_for('labels.index', page=pagination.next_num, category=active_category.id if active_category else None, q=search) }}">Next</a>
</li>
</ul>
</nav>
{% endif %} {% endif %}
</div> </div>
</div> </div>
</div>
<!-- Hidden form used to import a Rebrickable result -->
<form method="POST" action="{{ url_for('labels.import_part') }}" id="importForm"> <form method="POST" action="{{ url_for('labels.import_part') }}" id="importForm">
<input type="hidden" name="part_number" id="importPartNumber"> <input type="hidden" name="part_number" id="importPartNumber">
<input type="hidden" name="category_id" value="{{ active_category.id if active_category else '' }}"> <input type="hidden" name="category_id" value="{{ active_category.id if active_category else '' }}">
@@ -206,7 +193,7 @@
{% block extra_js %} {% block extra_js %}
<script> <script>
document.getElementById('checkAll')?.addEventListener('change', function () { document.getElementById('checkAll')?.addEventListener('change', function () {
document.querySelectorAll('.label-check').forEach(box => { box.checked = this.checked; }); document.querySelectorAll('.label-check').forEach(b => { b.checked = this.checked; });
}); });
const lookupBtn = document.getElementById('lookupBtn'); const lookupBtn = document.getElementById('lookupBtn');
@@ -215,37 +202,39 @@ if (lookupBtn) {
const query = document.getElementById('lookupQuery').value.trim(); const query = document.getElementById('lookupQuery').value.trim();
const target = document.getElementById('lookupResults'); const target = document.getElementById('lookupResults');
if (!query) { target.innerHTML = ''; return; } if (!query) { target.innerHTML = ''; return; }
target.innerHTML = '<div class="col-12 text-muted small">Searching Rebrickable...</div>'; target.innerHTML = '<div class="col-12 bl-dim">Searching…</div>';
try { try {
const response = await fetch('{{ url_for("labels.lookup") }}?q=' + encodeURIComponent(query)); const r = await fetch('{{ url_for("labels.lookup") }}?q=' + encodeURIComponent(query));
const data = await response.json(); const data = await r.json();
if (!data.ok) { if (!data.ok) {
target.innerHTML = '<div class="col-12 text-danger small">' + (data.reason || 'Lookup failed.') + '</div>'; target.innerHTML = '<div class="col-12"><span class="bl-tag err">Error</span> ' +
(data.reason || 'Lookup failed.') + '</div>';
return; return;
} }
if (!data.results.length) { if (!data.results.length) {
target.innerHTML = '<div class="col-12 text-muted small">Nothing found.</div>'; target.innerHTML = '<div class="col-12 bl-dim">Nothing found.</div>';
return; return;
} }
target.innerHTML = data.results.map(part => ` target.innerHTML = data.results.map(p => `
<div class="col-6 col-md-4 col-xl-3"> <div class="col-6 col-md-4 col-xl-3">
<div class="card h-100"> <div class="bl-panel mb-0" style="height:100%;">
<div class="card-body p-2 text-center"> <div class="bl-panel-body text-center" style="padding:6px;">
${part.image_url ? `<img src="${part.image_url}" alt="" style="height:60px;object-fit:contain;">` : ''} ${p.image_url ? `<img src="${p.image_url}" alt="" style="height:52px;object-fit:contain;">` : ''}
<div class="small fw-semibold text-truncate" title="${part.name}">${part.name}</div> <div class="bl-name text-truncate" title="${p.name}">${p.name}</div>
<div class="small text-muted">${part.part_number}</div> <div class="num">${p.part_number}</div>
<button type="button" class="btn btn-sm btn-outline-danger mt-1 import-part" data-part="${part.part_number}">Add label</button> <button type="button" class="bl-btn bl-btn-sm bl-btn-go mt-1 import-part"
data-part="${p.part_number}">Add label</button>
</div> </div>
</div> </div>
</div>`).join(''); </div>`).join('');
target.querySelectorAll('.import-part').forEach(button => { target.querySelectorAll('.import-part').forEach(b => {
button.addEventListener('click', () => { b.addEventListener('click', () => {
document.getElementById('importPartNumber').value = button.dataset.part; document.getElementById('importPartNumber').value = b.dataset.part;
document.getElementById('importForm').submit(); document.getElementById('importForm').submit();
}); });
}); });
} catch (err) { } catch (e) {
target.innerHTML = '<div class="col-12 text-danger small">Lookup failed.</div>'; target.innerHTML = '<div class="col-12"><span class="bl-tag err">Failed</span></div>';
} }
}; };
lookupBtn.addEventListener('click', runLookup); lookupBtn.addEventListener('click', runLookup);