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:
@@ -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 & 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 %}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user