IT Assets: bulk-entry mode for Add Asset form

- Green info banner explains that hardware fields persist between saves
- Identifiers section badge shows 'These fields clear after each save'
- MAC Address gets same warning hint
- Submit button relabelled 'Save & Add Next' in add mode
- 'Reset All' button clears localStorage and the entire form
- localStorage saves all shared fields on submit; restores on page load
  (only Service Tag, Asset Tag and MAC Address are cleared each time)
- Cursor auto-focuses Service Tag field for fast keyboard entry
- Dell pre-fill still takes priority over stored state
This commit is contained in:
ske087
2026-07-08 23:06:33 +03:00
parent 55a088eff4
commit 1f2fedba95
@@ -13,36 +13,60 @@
<div class="card border-0 shadow-sm" style="max-width:800px;"> <div class="card border-0 shadow-sm" style="max-width:800px;">
<div class="card-body"> <div class="card-body">
{% if not asset and prefill and prefill.service_tag %} {% if not asset and prefill and prefill.service_tag %}
<div class="alert alert-info py-2 mb-3 small"> <div class="alert alert-info py-2 mb-3 small">
<i class="bi bi-cloud-check me-1"></i> <i class="bi bi-cloud-check me-1"></i>
Pre-filled from Dell service tag <strong>{{ prefill.service_tag }}</strong>. Review the details below before saving. Pre-filled from Dell service tag <strong>{{ prefill.service_tag }}</strong>. Review the details below before saving.
</div> </div>
{% endif %} {% endif %}
<form method="POST" action="{{ url_for('assets.edit', asset_id=asset.id) if asset else url_for('assets.create') }}">
<h6 class="text-uppercase text-muted mb-3 small">Identifiers</h6> {# ── Bulk-entry hint (Add mode only) ── #}
{% if not asset %}
<div class="alert alert-success py-2 mb-3 small d-flex align-items-start gap-2" id="bulkHint">
<i class="bi bi-info-circle-fill mt-1 flex-shrink-0"></i>
<span>
<strong>Bulk-entry mode:</strong> after saving, all hardware details are kept so you can
add the next asset from the same batch by only changing the
<strong>Service Tag</strong> and <strong>Asset Tag</strong>.
Use <strong>Reset All</strong> to start a completely new entry.
</span>
</div>
{% endif %}
<form id="assetForm" method="POST" action="{{ url_for('assets.edit', asset_id=asset.id) if asset else url_for('assets.create') }}">
{# ── Identifiers ─────────────────────────────────────────────────── #}
<div class="d-flex align-items-center justify-content-between mb-2">
<h6 class="text-uppercase text-muted mb-0 small">Identifiers</h6>
{% if not asset %}
<span class="badge bg-warning text-dark small">
<i class="bi bi-arrow-counterclockwise me-1"></i>These fields clear after each save
</span>
{% endif %}
</div>
<div class="row g-3 mb-3"> <div class="row g-3 mb-3">
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label">Service Tag / Serial Number <span class="text-danger">*</span></label> <label class="form-label">Service Tag / Serial Number <span class="text-danger">*</span></label>
<input type="text" name="serial_number" class="form-control" <input type="text" name="serial_number" id="f_serial_number" class="form-control"
value="{{ asset.service_tag or asset.serial_number if asset else (prefill.service_tag or prefill.serial_number if prefill else '') }}" value="{{ asset.service_tag or asset.serial_number if asset else (prefill.service_tag or prefill.serial_number if prefill else '') }}"
placeholder="e.g. ABC1234" required> placeholder="e.g. ABC1234" required autocomplete="off">
<div class="form-text">Used as both the service tag and serial number.</div> <div class="form-text">Used as both the service tag and serial number.</div>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label">Asset Tag</label> <label class="form-label">Asset Tag</label>
<input type="text" name="asset_tag" class="form-control" <input type="text" name="asset_tag" id="f_asset_tag" class="form-control"
value="{{ asset.asset_tag or '' if asset else '' }}" value="{{ asset.asset_tag or '' if asset else '' }}"
placeholder="Internal barcode/tag"> placeholder="Internal barcode/tag" autocomplete="off">
</div> </div>
</div> </div>
{# ── Classification ──────────────────────────────────────────────── #}
<h6 class="text-uppercase text-muted mb-3 small">Classification</h6> <h6 class="text-uppercase text-muted mb-3 small">Classification</h6>
<div class="row g-3 mb-3"> <div class="row g-3 mb-3">
<div class="col-md-4"> <div class="col-md-4">
<label class="form-label">Asset Type <span class="text-danger">*</span></label> <label class="form-label">Asset Type <span class="text-danger">*</span></label>
<select name="asset_type" class="form-select" required> <select name="asset_type" id="f_asset_type" class="form-select" required>
{% for t in asset_types %} {% for t in asset_types %}
<option value="{{ t }}" <option value="{{ t }}"
{% if asset and asset.asset_type == t %}selected {% if asset and asset.asset_type == t %}selected
@@ -54,74 +78,80 @@
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<label class="form-label">Brand</label> <label class="form-label">Brand</label>
<input type="text" name="brand" class="form-control" <input type="text" name="brand" id="f_brand" class="form-control"
value="{{ asset.brand or '' if asset else (prefill.brand if prefill else '') }}" value="{{ asset.brand or '' if asset else (prefill.brand if prefill else '') }}"
placeholder="e.g. Dell, Lenovo, HP"> placeholder="e.g. Dell, Lenovo, HP">
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<label class="form-label">Model</label> <label class="form-label">Model</label>
<input type="text" name="model" class="form-control" <input type="text" name="model" id="f_model" class="form-control"
value="{{ asset.model or '' if asset else (prefill.model if prefill else '') }}" value="{{ asset.model or '' if asset else (prefill.model if prefill else '') }}"
placeholder="e.g. Latitude 5540"> placeholder="e.g. Latitude 5540">
</div> </div>
</div> </div>
{# ── Technical Specs ─────────────────────────────────────────────── #}
<h6 class="text-uppercase text-muted mb-3 small">Technical Specs</h6> <h6 class="text-uppercase text-muted mb-3 small">Technical Specs</h6>
<div class="row g-3 mb-3"> <div class="row g-3 mb-3">
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label">Processor</label> <label class="form-label">Processor</label>
<input type="text" name="processor" class="form-control" <input type="text" name="processor" id="f_processor" class="form-control"
value="{{ asset.processor or '' if asset else '' }}" value="{{ asset.processor or '' if asset else '' }}"
placeholder="e.g. Intel Core i5-1345U"> placeholder="e.g. Intel Core i5-1345U">
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<label class="form-label">RAM (GB)</label> <label class="form-label">RAM (GB)</label>
<input type="number" name="ram_gb" class="form-control" min="0" <input type="number" name="ram_gb" id="f_ram_gb" class="form-control" min="0"
value="{{ asset.ram_gb or '' if asset else '' }}"> value="{{ asset.ram_gb or '' if asset else '' }}">
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<label class="form-label">Storage (GB)</label> <label class="form-label">Storage (GB)</label>
<input type="number" name="storage_gb" class="form-control" min="0" <input type="number" name="storage_gb" id="f_storage_gb" class="form-control" min="0"
value="{{ asset.storage_gb or '' if asset else '' }}"> value="{{ asset.storage_gb or '' if asset else '' }}">
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<label class="form-label">Operating System</label> <label class="form-label">Operating System</label>
<input type="text" name="operating_system" class="form-control" <input type="text" name="operating_system" id="f_operating_system" class="form-control"
value="{{ asset.operating_system or '' if asset else (prefill.operating_system if prefill else 'Windows 11 Pro') }}" value="{{ asset.operating_system or '' if asset else (prefill.operating_system if prefill else 'Windows 11 Pro') }}"
placeholder="e.g. Windows 11 Pro"> placeholder="e.g. Windows 11 Pro">
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<label class="form-label">MAC Address</label> <label class="form-label">MAC Address</label>
<input type="text" name="mac_address" class="form-control" <input type="text" name="mac_address" id="f_mac_address" class="form-control"
value="{{ asset.mac_address or '' if asset else '' }}"> value="{{ asset.mac_address or '' if asset else '' }}"
placeholder="unique per device" autocomplete="off">
{% if not asset %}
<div class="form-text text-warning"><i class="bi bi-arrow-counterclockwise me-1"></i>Clears after each save</div>
{% endif %}
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<label class="form-label">Location</label> <label class="form-label">Location</label>
<input type="text" name="location" class="form-control" <input type="text" name="location" id="f_location" class="form-control"
value="{{ asset.location or '' if asset else '' }}"> value="{{ asset.location or '' if asset else '' }}">
</div> </div>
</div> </div>
{# ── Procurement ─────────────────────────────────────────────────── #}
<h6 class="text-uppercase text-muted mb-3 small">Procurement</h6> <h6 class="text-uppercase text-muted mb-3 small">Procurement</h6>
<div class="row g-3 mb-3"> <div class="row g-3 mb-3">
<div class="col-md-3"> <div class="col-md-3">
<label class="form-label">Purchase Date</label> <label class="form-label">Purchase Date</label>
<input type="date" name="purchase_date" class="form-control" <input type="date" name="purchase_date" id="f_purchase_date" class="form-control"
value="{{ asset.purchase_date.isoformat() if asset and asset.purchase_date else (prefill.purchase_date if prefill else '') }}"> value="{{ asset.purchase_date.isoformat() if asset and asset.purchase_date else (prefill.purchase_date if prefill else '') }}">
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<label class="form-label">Warranty Expiry</label> <label class="form-label">Warranty Expiry</label>
<input type="date" name="warranty_expiry" class="form-control" <input type="date" name="warranty_expiry" id="f_warranty_expiry" class="form-control"
value="{{ asset.warranty_expiry.isoformat() if asset and asset.warranty_expiry else (prefill.warranty_expiry if prefill else '') }}"> value="{{ asset.warranty_expiry.isoformat() if asset and asset.warranty_expiry else (prefill.warranty_expiry if prefill else '') }}">
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<label class="form-label">Purchase Price</label> <label class="form-label">Purchase Price</label>
<input type="number" name="purchase_price" class="form-control" step="0.01" min="0" <input type="number" name="purchase_price" id="f_purchase_price" class="form-control" step="0.01" min="0"
value="{{ asset.purchase_price or '' if asset else '' }}"> value="{{ asset.purchase_price or '' if asset else '' }}">
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<label class="form-label">Status</label> <label class="form-label">Status</label>
<select name="status" class="form-select"> <select name="status" id="f_status" class="form-select">
{% for val, label in asset_statuses %} {% for val, label in asset_statuses %}
<option value="{{ val }}" {% if asset and asset.status == val %}selected{% elif not asset and val == 'available' %}selected{% endif %}>{{ label }}</option> <option value="{{ val }}" {% if asset and asset.status == val %}selected{% elif not asset and val == 'available' %}selected{% endif %}>{{ label }}</option>
{% endfor %} {% endfor %}
@@ -129,28 +159,120 @@
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<label class="form-label">Supplier</label> <label class="form-label">Supplier</label>
<input type="text" name="supplier" class="form-control" <input type="text" name="supplier" id="f_supplier" class="form-control"
value="{{ asset.supplier or '' if asset else '' }}"> value="{{ asset.supplier or '' if asset else '' }}">
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<label class="form-label">PO Number</label> <label class="form-label">PO Number</label>
<input type="text" name="po_number" class="form-control" <input type="text" name="po_number" id="f_po_number" class="form-control"
value="{{ asset.po_number or '' if asset else '' }}"> value="{{ asset.po_number or '' if asset else '' }}">
</div> </div>
<div class="col-12"> <div class="col-12">
<label class="form-label">Notes</label> <label class="form-label">Notes</label>
<textarea name="notes" class="form-control" rows="2">{{ asset.notes or '' if asset else '' }}</textarea> <textarea name="notes" id="f_notes" class="form-control" rows="2">{{ asset.notes or '' if asset else '' }}</textarea>
</div> </div>
</div> </div>
<div class="d-flex gap-2"> <div class="d-flex gap-2 flex-wrap">
<button type="submit" class="btn btn-primary"> <button type="submit" class="btn btn-primary">
<i class="bi bi-check-lg me-1"></i>{{ 'Save Changes' if asset else 'Create Asset' }} <i class="bi bi-check-lg me-1"></i>{{ 'Save Changes' if asset else 'Save &amp; Add Next' }}
</button> </button>
{% if not asset %}
<button type="button" id="btnReset" class="btn btn-outline-secondary">
<i class="bi bi-arrow-counterclockwise me-1"></i>Reset All
</button>
{% endif %}
<a href="{{ url_for('assets.detail', asset_id=asset.id) if asset else url_for('assets.index') }}" <a href="{{ url_for('assets.detail', asset_id=asset.id) if asset else url_for('assets.index') }}"
class="btn btn-outline-secondary">Cancel</a> class="btn btn-outline-secondary ms-auto">Cancel</a>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
{% block extra_js %}
{% if not asset %}
<script>
/* ── Bulk-entry persistence via localStorage ─────────────────────────────── */
const STORE_KEY = 'itassets_bulk_prefill';
// Fields that persist between entries (shared hardware specs)
const PERSISTENT_FIELDS = [
'f_asset_type', 'f_brand', 'f_model', 'f_processor',
'f_ram_gb', 'f_storage_gb', 'f_operating_system',
'f_location', 'f_purchase_date', 'f_warranty_expiry',
'f_purchase_price', 'f_status', 'f_supplier', 'f_po_number', 'f_notes'
];
// Fields unique per device — always cleared after save
const UNIQUE_FIELDS = ['f_serial_number', 'f_asset_tag', 'f_mac_address'];
function saveState() {
const state = {};
PERSISTENT_FIELDS.forEach(id => {
const el = document.getElementById(id);
if (el) state[id] = el.value;
});
localStorage.setItem(STORE_KEY, JSON.stringify(state));
}
function restoreState() {
const raw = localStorage.getItem(STORE_KEY);
if (!raw) return;
try {
const state = JSON.parse(raw);
PERSISTENT_FIELDS.forEach(id => {
const el = document.getElementById(id);
if (el && state[id] !== undefined && state[id] !== '') {
el.value = state[id];
}
});
} catch (_) {}
}
function clearState() {
localStorage.removeItem(STORE_KEY);
PERSISTENT_FIELDS.concat(UNIQUE_FIELDS).forEach(id => {
const el = document.getElementById(id);
if (!el) return;
if (el.tagName === 'SELECT') {
el.selectedIndex = 0;
} else if (el.tagName === 'TEXTAREA') {
el.value = '';
} else {
el.value = '';
}
});
// Restore default select values
const typeEl = document.getElementById('f_asset_type');
if (typeEl) typeEl.value = 'Laptop';
const statusEl = document.getElementById('f_status');
if (statusEl) statusEl.value = 'available';
document.getElementById('f_serial_number').focus();
}
// Restore on load (only if no prefill from Dell lookup is already in the form)
const hasDellPrefill = {{ 'true' if prefill and prefill.service_tag else 'false' }};
if (!hasDellPrefill) {
restoreState();
}
// Save before submit (so next page load gets the persisted values)
document.getElementById('assetForm').addEventListener('submit', saveState);
// Reset All button
const btnReset = document.getElementById('btnReset');
if (btnReset) {
btnReset.addEventListener('click', () => {
if (confirm('Clear all fields and start a fresh entry?')) {
clearState();
}
});
}
// Focus the service tag field immediately for fast entry
document.getElementById('f_serial_number').focus();
</script>
{% endif %}
{% endblock %}