Add device_type field, mobile-friendly improvements, and device filtering
- Device model: add device_type column (String 50, nullable) - Device add/edit: type select with presets + custom entry - Device detail: show type in info card; new Edit Device form - Device list: Type column + client-side filter bar (type + text search) - Mobile: card-style responsive tables on dashboard and device list, form-grid-2col collapse, larger tap targets, stacked form-actions, column picker viewport fix, filter bar full-width controls - Assign page: larger radio touch targets (min-height 44px) - 3 new acceptance tests for device_type (45 total)
This commit is contained in:
@@ -5,11 +5,27 @@
|
||||
<h1>Devices</h1>
|
||||
|
||||
<div class="card">
|
||||
<div id="device-filter-bar" style="display:flex;gap:0.5rem;flex-wrap:wrap;align-items:center;margin-bottom:0.75rem;">
|
||||
<select id="filter-type" onchange="applyDeviceFilters()"
|
||||
style="padding:0.25rem 0.5rem;font-size:0.85rem;border:1px solid #cbd5e1;border-radius:4px;">
|
||||
<option value="">All Types</option>
|
||||
{% for t in device_types|default([]) %}
|
||||
<option value="{{ t }}">{{ t }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<input type="text" id="filter-device-text" oninput="applyDeviceFilters()" placeholder="Search…"
|
||||
style="padding:0.25rem 0.5rem;font-size:0.85rem;border:1px solid #cbd5e1;border-radius:4px;width:140px;">
|
||||
<button type="button" onclick="resetDeviceFilters()" class="btn btn-sm btn-secondary"
|
||||
id="device-filter-reset" style="display:none;">✕ Reset</button>
|
||||
<span id="device-filter-count" style="font-size:0.8rem;color:#64748b;"></span>
|
||||
</div>
|
||||
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<table class="responsive-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Slots</th>
|
||||
<th>Installed</th>
|
||||
<th>Brands</th>
|
||||
@@ -18,16 +34,17 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for d in devices %}
|
||||
<tr>
|
||||
<td><a href="{{ url_for('device_detail', device_id=d.id) }}"><strong>{{ d.name }}</strong></a></td>
|
||||
<td>{{ d.battery_slots }}</td>
|
||||
<td>
|
||||
<tr data-type="{{ d.device_type or '' }}" data-name="{{ d.name|lower }}">
|
||||
<td data-label="Device"><a href="{{ url_for('device_detail', device_id=d.id) }}"><strong>{{ d.name }}</strong></a></td>
|
||||
<td data-label="Type">{{ d.device_type or '—' }}</td>
|
||||
<td data-label="Slots">{{ d.battery_slots }}</td>
|
||||
<td data-label="Installed">
|
||||
{{ d.installed_count() }} / {{ d.battery_slots }}
|
||||
{% if d.installed_count() >= d.battery_slots %}
|
||||
<span class="badge badge-retired">Full</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<td data-label="Brands">
|
||||
{% set brands = d.installed_brands() %}
|
||||
{% if brands %}
|
||||
{{ brands|join(', ') }}
|
||||
@@ -38,7 +55,7 @@
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td style="white-space:nowrap;">
|
||||
<td data-label="Actions" style="white-space:nowrap;">
|
||||
<a class="btn btn-sm btn-secondary" href="{{ url_for('device_detail', device_id=d.id) }}">View</a>
|
||||
<form class="inline" method="post" action="{{ url_for('device_delete', device_id=d.id) }}"
|
||||
onsubmit="return confirm('Delete {{ d.name }}? All installed batteries will be unassigned.');">
|
||||
@@ -47,7 +64,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="5" class="text-muted" style="text-align:center;padding:1rem;">No devices yet. <a href="{{ url_for('device_add') }}">Add one.</a></td></tr>
|
||||
<tr><td colspan="6" class="text-muted" style="text-align:center;padding:1rem;">No devices yet. <a href="{{ url_for('device_add') }}">Add one.</a></td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -55,4 +72,31 @@
|
||||
</div>
|
||||
|
||||
<a class="btn btn-primary" href="{{ url_for('device_add') }}">+ Add Device</a>
|
||||
|
||||
<script>
|
||||
function applyDeviceFilters() {
|
||||
var typeVal = document.getElementById('filter-type').value.toLowerCase();
|
||||
var textVal = document.getElementById('filter-device-text').value.toLowerCase();
|
||||
var rows = document.querySelectorAll('tbody tr[data-name]');
|
||||
var visible = 0;
|
||||
rows.forEach(function(row) {
|
||||
var rowType = (row.dataset.type || '').toLowerCase();
|
||||
var rowName = (row.dataset.name || '').toLowerCase();
|
||||
var show = (!typeVal || rowType === typeVal) &&
|
||||
(!textVal || rowName.includes(textVal) || rowType.includes(textVal));
|
||||
row.style.display = show ? '' : 'none';
|
||||
if (show) visible++;
|
||||
});
|
||||
var active = typeVal || textVal;
|
||||
document.getElementById('device-filter-reset').style.display = active ? '' : 'none';
|
||||
document.getElementById('device-filter-count').textContent =
|
||||
active ? (visible + ' of ' + rows.length + ' shown') : '';
|
||||
}
|
||||
|
||||
function resetDeviceFilters() {
|
||||
document.getElementById('filter-type').value = '';
|
||||
document.getElementById('filter-device-text').value = '';
|
||||
applyDeviceFilters();
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user