Simplify battery management: bulk add, device-level auto-install, mass operations
- Replace single-battery add form with bulk add (brand + count, auto-generated labels) - Add device-level install form: specify brand+qty pairs, system autoselects available batteries - Add bulk actions on dashboard: retire, delete, unassign, change brand (checkbox multi-select) - Keep per-battery assign route for special cases (e.g. known low-capacity battery) - Remove unique constraint on Battery.label (labels are now auto-generated) - Add *.snapshot to .gitignore for DB snapshot files - Rewrite tests: 35 passing (11 new tests for bulk-add, device-install, bulk-actions) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,17 +1,11 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Add Battery — Battery Tracker{% endblock %}
|
||||
{% block title %}Add Batteries — Battery Tracker{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Add Battery</h1>
|
||||
<h1>Add Batteries</h1>
|
||||
|
||||
<div class="card">
|
||||
<form method="post" action="{{ url_for('battery_add') }}">
|
||||
<div class="form-group">
|
||||
<label for="label">Label <span class="text-danger">*</span></label>
|
||||
<input type="text" id="label" name="label" value="{{ form_label|default('') }}"
|
||||
placeholder="e.g. ENL-17" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="brand">Brand <span class="text-danger">*</span></label>
|
||||
<input type="text" id="brand" name="brand" value="{{ form_brand|default('') }}"
|
||||
@@ -19,21 +13,18 @@
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="status">Initial Status</label>
|
||||
<select id="status" name="status">
|
||||
<option value="available" {% if form_status|default('available') == 'available' %}selected{% endif %}>Available</option>
|
||||
<option value="installed" {% if form_status|default('') == 'installed' %}selected{% endif %}>Installed</option>
|
||||
<option value="retired" {% if form_status|default('') == 'retired' %}selected{% endif %}>Retired</option>
|
||||
</select>
|
||||
<label for="count">Quantity</label>
|
||||
<input type="number" id="count" name="count" value="{{ form_count|default(1) }}" min="1" max="50">
|
||||
<small class="text-muted">Labels are auto-generated (e.g. Eneloop 001, Eneloop 002)</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="notes">Notes</label>
|
||||
<textarea id="notes" name="notes" placeholder="Optional notes…">{{ form_notes|default('') }}</textarea>
|
||||
<textarea id="notes" name="notes" placeholder="Optional notes applied to all batteries…">{{ form_notes|default('') }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button class="btn btn-primary" type="submit">Add Battery</button>
|
||||
<button class="btn btn-primary" type="submit">Add Batteries</button>
|
||||
<a class="btn btn-secondary" href="{{ url_for('dashboard') }}">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
+92
-51
@@ -29,60 +29,101 @@
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Label</th>
|
||||
<th>Brand</th>
|
||||
<th>Status</th>
|
||||
<th>Assigned To</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for b in batteries %}
|
||||
<tr>
|
||||
<td><a href="{{ url_for('battery_detail', battery_id=b.id) }}"><strong>{{ b.label }}</strong></a></td>
|
||||
<td>{{ b.brand }}</td>
|
||||
<td>
|
||||
<span class="badge badge-{{ b.status }}">{{ b.status|capitalize }}</span>
|
||||
</td>
|
||||
<td>
|
||||
{% if b.device %}
|
||||
<a href="{{ url_for('device_detail', device_id=b.device.id) }}">{{ b.device.name }}</a>
|
||||
{% if b.device.has_mixed_brands() %}
|
||||
<span class="badge badge-warning" title="Mixed brands in this device">⚠ mixed</span>
|
||||
<form method="post" action="{{ url_for('battery_bulk_action') }}" id="bulk-form">
|
||||
|
||||
<div id="bulk-toolbar" style="display:none;margin-bottom:0.75rem;padding:0.6rem 0.75rem;background:#f1f5f9;border-radius:6px;display:none;align-items:center;gap:0.5rem;flex-wrap:wrap;">
|
||||
<span id="selected-count" style="font-size:0.85rem;color:#64748b;margin-right:0.25rem;"></span>
|
||||
<button class="btn btn-sm btn-warning" name="action" value="unassign" type="submit">Unassign</button>
|
||||
<button class="btn btn-sm btn-secondary" name="action" value="retire" type="submit">Retire</button>
|
||||
<button class="btn btn-sm btn-danger" name="action" value="delete" type="submit"
|
||||
onclick="return confirm('Permanently delete selected batteries?')">Delete</button>
|
||||
<span style="display:flex;gap:0.35rem;align-items:center;">
|
||||
<input type="text" name="new_brand" id="new-brand-input" placeholder="New brand name"
|
||||
style="padding:0.25rem 0.5rem;font-size:0.85rem;border:1px solid #cbd5e1;border-radius:4px;width:160px;">
|
||||
<button class="btn btn-sm btn-primary" name="action" value="set_brand" type="submit">Change Brand</button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:1.5rem;"><input type="checkbox" id="select-all" title="Select all"></th>
|
||||
<th>Label</th>
|
||||
<th>Brand</th>
|
||||
<th>Status</th>
|
||||
<th>Assigned To</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for b in batteries %}
|
||||
<tr>
|
||||
<td><input type="checkbox" name="battery_ids" value="{{ b.id }}" class="row-cb"></td>
|
||||
<td><a href="{{ url_for('battery_detail', battery_id=b.id) }}"><strong>{{ b.label }}</strong></a></td>
|
||||
<td>{{ b.brand }}</td>
|
||||
<td>
|
||||
<span class="badge badge-{{ b.status }}">{{ b.status|capitalize }}</span>
|
||||
</td>
|
||||
<td>
|
||||
{% if b.device %}
|
||||
<a href="{{ url_for('device_detail', device_id=b.device.id) }}">{{ b.device.name }}</a>
|
||||
{% if b.device.has_mixed_brands() %}
|
||||
<span class="badge badge-warning" title="Mixed brands in this device">⚠ mixed</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td style="white-space:nowrap;">
|
||||
<a class="btn btn-sm btn-secondary" href="{{ url_for('battery_detail', battery_id=b.id) }}">View</a>
|
||||
</td>
|
||||
<td style="white-space:nowrap;">
|
||||
<a class="btn btn-sm btn-secondary" href="{{ url_for('battery_detail', battery_id=b.id) }}">View</a>
|
||||
|
||||
{% if b.is_available() %}
|
||||
<a class="btn btn-sm btn-primary" href="{{ url_for('battery_assign', battery_id=b.id) }}">Assign</a>
|
||||
{% endif %}
|
||||
{% if b.is_available() %}
|
||||
<a class="btn btn-sm btn-primary" href="{{ url_for('battery_assign', battery_id=b.id) }}">Assign</a>
|
||||
{% endif %}
|
||||
|
||||
{% if b.is_installed() %}
|
||||
<form class="inline" method="post" action="{{ url_for('battery_unassign', battery_id=b.id) }}">
|
||||
<button class="btn btn-sm btn-warning" type="submit">Unassign</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if b.is_installed() %}
|
||||
<button class="btn btn-sm btn-warning" type="submit"
|
||||
formaction="{{ url_for('battery_unassign', battery_id=b.id) }}">Unassign</button>
|
||||
{% endif %}
|
||||
|
||||
{% if not b.is_retired() %}
|
||||
<form class="inline" method="post" action="{{ url_for('battery_retire', battery_id=b.id) }}">
|
||||
<button class="btn btn-sm btn-secondary" type="submit">Retire</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="5" class="text-muted" style="text-align:center;padding:1rem;">No batteries found. <a href="{{ url_for('battery_add') }}">Add one.</a></td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% if not b.is_retired() %}
|
||||
<button class="btn btn-sm btn-secondary" type="submit"
|
||||
formaction="{{ url_for('battery_retire', battery_id=b.id) }}">Retire</button>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="6" class="text-muted" style="text-align:center;padding:1rem;">No batteries found. <a href="{{ url_for('battery_add') }}">Add some.</a></td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var cbs = document.querySelectorAll('.row-cb');
|
||||
var selectAll = document.getElementById('select-all');
|
||||
var toolbar = document.getElementById('bulk-toolbar');
|
||||
var countEl = document.getElementById('selected-count');
|
||||
|
||||
function updateToolbar() {
|
||||
var checked = document.querySelectorAll('.row-cb:checked');
|
||||
var n = checked.length;
|
||||
toolbar.style.display = n > 0 ? 'flex' : 'none';
|
||||
countEl.textContent = n + ' selected';
|
||||
selectAll.indeterminate = n > 0 && n < cbs.length;
|
||||
selectAll.checked = cbs.length > 0 && n === cbs.length;
|
||||
}
|
||||
|
||||
cbs.forEach(function (cb) { cb.addEventListener('change', updateToolbar); });
|
||||
selectAll.addEventListener('change', function () {
|
||||
cbs.forEach(function (cb) { cb.checked = selectAll.checked; });
|
||||
updateToolbar();
|
||||
});
|
||||
}());
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -25,6 +25,24 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Install Batteries</h2>
|
||||
{% set free_slots = device.battery_slots - device.installed_count() %}
|
||||
<p class="text-muted" style="margin-bottom:0.75rem;">{{ free_slots }} slot(s) free</p>
|
||||
<form method="post" action="{{ url_for('device_install', device_id=device.id) }}">
|
||||
<div style="display:grid;grid-template-columns:1fr auto;gap:0.4rem;max-width:360px;align-items:center;margin-bottom:0.75rem;">
|
||||
<span style="font-weight:600;font-size:0.85rem;color:#64748b;">Brand</span>
|
||||
<span style="font-weight:600;font-size:0.85rem;color:#64748b;">Qty</span>
|
||||
{% for _ in range(4) %}
|
||||
<input type="text" name="brand[]" placeholder="e.g. Eneloop" style="padding:0.3rem 0.5rem;">
|
||||
<input type="number" name="qty[]" value="0" min="0"
|
||||
style="padding:0.3rem 0.5rem;width:4rem;text-align:center;">
|
||||
{% endfor %}
|
||||
</div>
|
||||
<button class="btn btn-primary" type="submit">Install</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Installed Batteries</h2>
|
||||
{% set installed = device.batteries | selectattr('status', 'eq', 'installed') | list %}
|
||||
|
||||
Reference in New Issue
Block a user