Files

301 lines
12 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends 'base.html' %}
{% block title %}Assets IT Asset Management{% endblock %}
{% block breadcrumb %}
<li class="breadcrumb-item"><a href="{{ url_for('dashboard.index') }}">Home</a></li>
<li class="breadcrumb-item active">Assets</li>
{% endblock %}
{% block content %}
<div class="page-header d-flex align-items-center justify-content-between mb-4">
<h1><i class="bi bi-laptop me-2"></i>Assets</h1>
<a href="{{ url_for('assets.create') }}" class="btn btn-primary btn-sm">
<i class="bi bi-plus-circle me-1"></i>Add Asset
</a>
</div>
<!-- Dell Service Tag Quick Import -->
<div class="card border-0 shadow-sm mb-4" id="dellLookupCard">
<div class="card-body py-3">
<div class="d-flex align-items-center gap-2 flex-wrap">
<span class="fw-semibold text-nowrap">
<i class="bi bi-search me-1 text-primary"></i>Dell Quick Import
</span>
<span class="text-muted small text-nowrap">Enter a service tag to open the asset form pre-filled:</span>
<div class="input-group input-group-sm" style="max-width:220px;">
<input type="text" id="dellTagInput" class="form-control text-uppercase"
placeholder="e.g. ABC1234" maxlength="20"
style="text-transform:uppercase; letter-spacing:.05em;">
<button class="btn btn-outline-primary" id="dellLookupBtn" type="button">
<i class="bi bi-cloud-download me-1"></i>Lookup
</button>
</div>
<div id="dellLookupSpinner" class="spinner-border spinner-border-sm text-primary d-none" role="status">
<span class="visually-hidden">Loading…</span>
</div>
<span class="text-muted small ms-auto">
<i class="bi bi-info-circle me-1"></i>
Full auto-fill available with a
<a href="https://tdm.dell.com" target="_blank" class="text-decoration-none">free Dell TechDirect API key</a>
</span>
</div>
<!-- Result preview (hidden until data arrives) -->
<div id="dellLookupResult" class="mt-3 d-none">
<div class="alert mb-2 py-2 d-flex align-items-start gap-3" id="dellResultBody">
<i class="bi bi-pc-display-horizontal fs-4 flex-shrink-0 mt-1" id="dellResultIcon"></i>
<div class="flex-grow-1">
<div class="fw-semibold mb-1" id="dellResultTitle"></div>
<div class="row row-cols-2 row-cols-md-4 g-1 small" id="dellResultMeta"></div>
</div>
<div class="d-flex flex-column gap-1 flex-shrink-0">
<a id="dellCreateBtn" href="#" class="btn btn-sm btn-primary">
<i class="bi bi-plus-circle me-1"></i>Create Asset
</a>
<a id="dellSupportLink" href="#" target="_blank" class="btn btn-sm btn-outline-secondary d-none">
<i class="bi bi-box-arrow-up-right me-1"></i>View on Dell
</a>
</div>
</div>
</div>
<!-- Error area -->
<div id="dellLookupError" class="alert alert-warning mt-2 py-2 d-none small mb-0"></div>
</div>
</div>
<!-- Filters -->
<form method="GET" class="row g-2 mb-3">
<div class="col-md-4">
<div class="input-group input-group-sm">
<span class="input-group-text"><i class="bi bi-search"></i></span>
<input type="text" name="q" class="form-control" placeholder="Search SN, service tag, brand, model…" value="{{ q }}">
</div>
</div>
<div class="col-md-2">
<select name="status" class="form-select form-select-sm">
<option value="">All statuses</option>
{% for val, label in asset_statuses %}
<option value="{{ val }}" {% if status_filter == val %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-2">
<select name="asset_type" class="form-select form-select-sm">
<option value="">All types</option>
{% for t in asset_types %}
<option value="{{ t }}" {% if type_filter == t %}selected{% endif %}>{{ t }}</option>
{% endfor %}
</select>
</div>
<div class="col-auto">
<button type="submit" class="btn btn-sm btn-primary">Filter</button>
<a href="{{ url_for('assets.index') }}" class="btn btn-sm btn-outline-secondary">Clear</a>
</div>
</form>
<div class="card border-0 shadow-sm">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>Type</th>
<th>Brand / Model</th>
<th>Serial Number</th>
<th>Service Tag</th>
<th>Status</th>
<th>Assigned To</th>
<th>Warranty</th>
<th></th>
</tr>
</thead>
<tbody>
{% for a in pagination.items %}
<tr>
<td><span class="badge bg-secondary">{{ a.asset_type }}</span></td>
<td>{{ a.brand or '' }} {{ a.model or '' }}</td>
<td><code>{{ a.serial_number }}</code></td>
<td><code>{{ a.service_tag or '—' }}</code></td>
<td>
<span class="badge badge-{{ a.status }}">{{ a.status | title }}</span>
</td>
<td>
{% if a.current_user %}
<a href="{{ url_for('users.detail', user_id=a.current_user.id) }}">
{{ a.current_user.display_name }}
</a>
{% else %}
<span class="text-muted"></span>
{% endif %}
</td>
<td>
{% if a.warranty_expiry %}
<span class="{% if a.warranty_expiry < today %}text-danger{% else %}text-success{% endif %}">
{{ a.warranty_expiry.strftime('%d/%m/%Y') }}
</span>
{% else %}—{% endif %}
</td>
<td>
<a href="{{ url_for('assets.detail', asset_id=a.id) }}"
class="btn btn-sm btn-outline-secondary py-0 px-2">
<i class="bi bi-eye"></i>
</a>
</td>
</tr>
{% else %}
<tr><td colspan="8" class="text-center text-muted py-4">No assets found.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
{% if pagination.pages > 1 %}
<div class="card-footer bg-white d-flex justify-content-between align-items-center py-2">
<small class="text-muted">Showing {{ pagination.first }}{{ pagination.last }} of {{ pagination.total }}</small>
<nav>
<ul class="pagination pagination-sm mb-0">
{% if pagination.has_prev %}
<li class="page-item">
<a class="page-link" href="{{ url_for('assets.index', page=pagination.prev_num, q=q, status=status_filter, asset_type=type_filter) }}"></a>
</li>
{% endif %}
{% for p in pagination.iter_pages(left_edge=1, right_edge=1, left_current=2, right_current=2) %}
{% if p %}
<li class="page-item {% if p == pagination.page %}active{% endif %}">
<a class="page-link" href="{{ url_for('assets.index', page=p, q=q, status=status_filter, asset_type=type_filter) }}">{{ p }}</a>
</li>
{% else %}
<li class="page-item disabled"><span class="page-link"></span></li>
{% endif %}
{% endfor %}
{% if pagination.has_next %}
<li class="page-item">
<a class="page-link" href="{{ url_for('assets.index', page=pagination.next_num, q=q, status=status_filter, asset_type=type_filter) }}"></a>
</li>
{% endif %}
</ul>
</nav>
</div>
{% endif %}
</div>
{% endblock %}
{% block extra_head %}
<script>
// make today available for warranty colour
const today = new Date().toISOString().slice(0, 10);
</script>
{% endblock %}
{% block extra_js %}
<script>
(function () {
const input = document.getElementById('dellTagInput');
const btn = document.getElementById('dellLookupBtn');
const spinner = document.getElementById('dellLookupSpinner');
const result = document.getElementById('dellLookupResult');
const errBox = document.getElementById('dellLookupError');
const title = document.getElementById('dellResultTitle');
const meta = document.getElementById('dellResultMeta');
const createBtn = document.getElementById('dellCreateBtn');
function setLoading(on) {
btn.disabled = on;
spinner.classList.toggle('d-none', !on);
}
function showError(msg) {
errBox.textContent = msg;
errBox.classList.remove('d-none');
result.classList.add('d-none');
}
function doLookup() {
const tag = input.value.trim().toUpperCase();
if (!tag) { input.focus(); return; }
result.classList.add('d-none');
errBox.classList.add('d-none');
setLoading(true);
fetch(`{{ url_for('assets.dell_lookup') }}?tag=${encodeURIComponent(tag)}`)
.then(r => r.json().then(d => ({ ok: r.ok, status: r.status, data: d })))
.then(({ ok, status, data }) => {
setLoading(false);
if (!ok) {
if (status === 409 && data.existing_id) {
errBox.innerHTML = `${data.error} — <a href="/assets/${data.existing_id}">View asset</a>`;
errBox.classList.remove('d-none');
} else {
showError(data.error || 'Lookup failed.');
}
return;
}
// Build preview
const isPartial = data.source === 'partial';
const resultBody = document.getElementById('dellResultBody');
const resultIcon = document.getElementById('dellResultIcon');
const supportLink = document.getElementById('dellSupportLink');
resultBody.className = `alert mb-2 py-2 d-flex align-items-start gap-3 ${isPartial ? 'alert-warning' : 'alert-info'}`;
resultIcon.className = `bi bi-pc-display-horizontal fs-4 flex-shrink-0 mt-1 ${isPartial ? 'text-warning' : 'text-primary'}`;
if (isPartial) {
// Auto-open Dell's warranty page in a new tab so the user can read model + warranty
if (data.support_url) window.open(data.support_url, '_blank', 'noopener');
title.innerHTML = `Dell service tag <strong>${data.service_tag}</strong> &mdash; Dell&rsquo;s page opened in a new tab. Copy model &amp; warranty date into the form below.`;
meta.innerHTML = `
<div><span class="fw-medium">Brand:</span> Dell</div>
<div><span class="fw-medium">OS:</span> ${data.operating_system}</div>
<div><span class="fw-medium">Model:</span> <em class="text-muted">fill from Dell tab &rarr;</em></div>
<div><span class="fw-medium">Warranty:</span> <em class="text-muted">fill from Dell tab &rarr;</em></div>`;
supportLink.href = data.support_url;
supportLink.classList.remove('d-none');
createBtn.textContent = '';
createBtn.innerHTML = '<i class="bi bi-plus-circle me-1"></i>Open Form';
} else {
title.textContent = `Dell ${data.model || data.service_tag}`;
supportLink.href = data.support_url || '#';
supportLink.classList.remove('d-none');
const fields = [
['Type', data.asset_type],
['Service Tag', data.service_tag],
['Serial', data.serial_number || '—'],
['Warranty', data.warranty_expiry || '—'],
['Purchased', data.purchase_date || '—'],
['OS', data.operating_system],
];
meta.innerHTML = fields
.map(([k, v]) => `<div><span class="fw-medium">${k}:</span> ${v || '—'}</div>`)
.join('');
}
// Build "Create Asset" URL with pre-filled params
const params = new URLSearchParams({
service_tag: data.service_tag || '',
serial_number: data.serial_number || '',
brand: data.brand || 'Dell',
model: data.model || '',
asset_type: data.asset_type || '',
operating_system: data.operating_system || '',
warranty_expiry: data.warranty_expiry || '',
purchase_date: data.purchase_date || '',
});
createBtn.href = `{{ url_for('assets.create') }}?${params.toString()}`;
result.classList.remove('d-none');
})
.catch(() => {
setLoading(false);
showError('Network error could not reach the server.');
});
}
btn.addEventListener('click', doLookup);
input.addEventListener('keydown', e => { if (e.key === 'Enter') doLookup(); });
input.addEventListener('input', () => {
input.value = input.value.toUpperCase();
});
})();
</script>
{% endblock %}