206 lines
9.7 KiB
HTML
206 lines
9.7 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}Devices — Battery Tracker{% endblock %}
|
|
|
|
{% block content %}
|
|
<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>
|
|
<select id="filter-location" onchange="applyDeviceFilters()"
|
|
style="padding:0.25rem 0.5rem;font-size:0.85rem;border:1px solid #cbd5e1;border-radius:4px;">
|
|
<option value="">All Locations</option>
|
|
{% for loc in device_locations|default([]) %}
|
|
<option value="{{ loc }}">{{ loc }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
<select id="filter-battery-size" onchange="applyDeviceFilters()"
|
|
style="padding:0.25rem 0.5rem;font-size:0.85rem;border:1px solid #cbd5e1;border-radius:4px;">
|
|
<option value="">Any Size</option>
|
|
{% for sz in device_battery_sizes|default([]) %}
|
|
<option value="{{ sz }}">{{ sz }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
<select id="filter-fill" onchange="applyDeviceFilters()"
|
|
style="padding:0.25rem 0.5rem;font-size:0.85rem;border:1px solid #cbd5e1;border-radius:4px;">
|
|
<option value="">Any Fill</option>
|
|
<option value="empty">Empty</option>
|
|
<option value="partial">Partial</option>
|
|
<option value="full">Full</option>
|
|
</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 class="responsive-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Name</th>
|
|
<th>Type</th>
|
|
<th>Size</th>
|
|
<th>Location</th>
|
|
<th>Slots</th>
|
|
<th>Installed</th>
|
|
<th>Brands</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for d in devices %}
|
|
{% set installed = d.effective_installed_count() %}
|
|
{% set total_slots = d.effective_slots() %}
|
|
{% if installed == 0 or total_slots == 0 %}{% set fill_state = 'empty' %}
|
|
{% elif installed >= total_slots %}{% set fill_state = 'full' %}
|
|
{% else %}{% set fill_state = 'partial' %}{% endif %}
|
|
<tr data-type="{{ d.device_type or '' }}"
|
|
data-battery-size="{{ d.battery_size or '' }}"
|
|
data-location="{{ d.location or '' }}"
|
|
data-fill="{{ fill_state }}"
|
|
data-name="{{ d.name|lower }}">
|
|
<td data-label="Device">
|
|
<a href="{{ url_for('device_detail', device_id=d.id) }}"><strong>{{ d.name }}</strong></a>
|
|
{% if d.has_children() %}
|
|
<br><small class="text-muted">{{ d.children|length }} sub-component{{ 's' if d.children|length != 1 }}</small>
|
|
{% endif %}
|
|
</td>
|
|
<td data-label="Type">{{ d.device_type or '—' }}</td>
|
|
<td data-label="Size">{{ d.battery_size or '—' }}</td>
|
|
<td data-label="Location">{{ d.location or '—' }}</td>
|
|
<td data-label="Slots">{{ total_slots }}</td>
|
|
<td data-label="Installed">
|
|
{{ installed }} / {{ total_slots }}
|
|
{% if total_slots > 0 and installed >= total_slots %}
|
|
<span class="badge badge-retired">Full</span>
|
|
{% endif %}
|
|
</td>
|
|
<td data-label="Brands">
|
|
{% set brands = d.installed_brands() %}
|
|
{% if brands %}
|
|
{{ brands|join(', ') }}
|
|
{% if d.has_mixed_brands() %}
|
|
<span class="badge badge-warning">⚠ mixed</span>
|
|
{% endif %}
|
|
{% else %}
|
|
<span class="text-muted">—</span>
|
|
{% endif %}
|
|
</td>
|
|
<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>
|
|
{% if installed > 0 %}
|
|
<form class="inline" method="post" action="{{ url_for('device_unassign_all', device_id=d.id) }}"
|
|
data-confirm="Unassign all batteries from {{ d.name }}?"
|
|
data-confirm-ok="Unassign" data-confirm-class="btn-warning">
|
|
<button class="btn btn-sm btn-warning" type="submit">Unassign All</button>
|
|
</form>
|
|
{% endif %}
|
|
<form class="inline" method="post" action="{{ url_for('device_delete', device_id=d.id) }}"
|
|
data-confirm="Delete {{ d.name }}? All installed batteries will be unassigned."
|
|
data-confirm-ok="Delete" data-confirm-class="btn-danger">
|
|
<button class="btn btn-sm btn-danger" type="submit">Delete</button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
{% else %}
|
|
<tr><td colspan="8" 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>
|
|
</div>
|
|
<div id="dev-pagination-bar" style="display:flex;align-items:center;gap:0.75rem;margin-top:0.75rem;flex-wrap:wrap;">
|
|
<button id="dev-prev-page" class="btn btn-sm btn-secondary" type="button"
|
|
onclick="devCurrentPage--; applyDevicePagination();" disabled>← Prev</button>
|
|
<span id="dev-page-info" style="color:var(--text-muted);font-size:0.9rem;"></span>
|
|
<button id="dev-next-page" class="btn btn-sm btn-secondary" type="button"
|
|
onclick="devCurrentPage++; applyDevicePagination();" disabled>Next →</button>
|
|
<select id="dev-page-size-select" onchange="DEV_PAGE_SIZE=+this.value||Infinity;devCurrentPage=1;applyDevicePagination();"
|
|
style="padding:0.2rem 0.4rem;font-size:0.85rem;border:1px solid #cbd5e1;border-radius:4px;">
|
|
<option value="10">10 / page</option>
|
|
<option value="25" selected>25 / page</option>
|
|
<option value="50">50 / page</option>
|
|
<option value="100">100 / page</option>
|
|
<option value="">All</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<a class="btn btn-primary" href="{{ url_for('device_add') }}">+ Add Device</a>
|
|
|
|
<script>
|
|
var DEV_PAGE_SIZE = 25;
|
|
var devCurrentPage = 1;
|
|
|
|
function applyDeviceFilters() {
|
|
var typeVal = document.getElementById('filter-type').value;
|
|
var batterySizeVal = document.getElementById('filter-battery-size').value;
|
|
var locationVal = document.getElementById('filter-location').value;
|
|
var fillVal = document.getElementById('filter-fill').value;
|
|
var textVal = document.getElementById('filter-device-text').value.toLowerCase();
|
|
var rows = document.querySelectorAll('tbody tr[data-name]');
|
|
var filtered = 0;
|
|
rows.forEach(function(row) {
|
|
var rowType = row.dataset.type || '';
|
|
var rowBatterySize = row.dataset.batterySize || '';
|
|
var rowLocation = row.dataset.location || '';
|
|
var rowFill = row.dataset.fill || '';
|
|
var rowName = row.dataset.name || '';
|
|
var show = (!typeVal || rowType === typeVal) &&
|
|
(!batterySizeVal || rowBatterySize === batterySizeVal) &&
|
|
(!locationVal || rowLocation === locationVal) &&
|
|
(!fillVal || rowFill === fillVal) &&
|
|
(!textVal || rowName.includes(textVal) ||
|
|
rowType.toLowerCase().includes(textVal) ||
|
|
rowBatterySize.toLowerCase().includes(textVal) ||
|
|
rowLocation.toLowerCase().includes(textVal));
|
|
row.dataset.filteredOut = show ? '' : '1';
|
|
if (show) filtered++;
|
|
});
|
|
var active = typeVal || batterySizeVal || locationVal || fillVal || textVal;
|
|
document.getElementById('device-filter-reset').style.display = active ? '' : 'none';
|
|
document.getElementById('device-filter-count').textContent =
|
|
active ? (filtered + ' of ' + rows.length + ' shown') : '';
|
|
devCurrentPage = 1;
|
|
applyDevicePagination();
|
|
}
|
|
|
|
function applyDevicePagination() {
|
|
var allRows = Array.from(document.querySelectorAll('tbody tr[data-name]'));
|
|
var visible = allRows.filter(function(r) { return !r.dataset.filteredOut; });
|
|
var totalPages = Math.max(1, Math.ceil(visible.length / DEV_PAGE_SIZE));
|
|
if (devCurrentPage > totalPages) devCurrentPage = totalPages;
|
|
var start = (devCurrentPage - 1) * DEV_PAGE_SIZE;
|
|
|
|
allRows.forEach(function(r) { r.style.display = 'none'; });
|
|
visible.slice(start, start + DEV_PAGE_SIZE).forEach(function(r) { r.style.display = ''; });
|
|
|
|
var info = document.getElementById('dev-page-info');
|
|
var prev = document.getElementById('dev-prev-page');
|
|
var next = document.getElementById('dev-next-page');
|
|
if (info) info.textContent = visible.length > DEV_PAGE_SIZE
|
|
? 'Page ' + devCurrentPage + ' of ' + totalPages : '';
|
|
if (prev) prev.disabled = devCurrentPage <= 1;
|
|
if (next) next.disabled = devCurrentPage >= totalPages;
|
|
}
|
|
|
|
function resetDeviceFilters() {
|
|
document.getElementById('filter-type').value = '';
|
|
document.getElementById('filter-battery-size').value = '';
|
|
document.getElementById('filter-location').value = '';
|
|
document.getElementById('filter-fill').value = '';
|
|
document.getElementById('filter-device-text').value = '';
|
|
applyDeviceFilters();
|
|
}
|
|
|
|
applyDeviceFilters();
|
|
</script>
|
|
{% endblock %}
|