Files
shopdb-flask/frontend/src/views/network/NetworkDeviceDetail.vue
cproudlock 9efdb5f52d Add print badges, pagination, route splitting, JWT auth fixes, and list page alignment
- Fix equipment badge barcode not rendering (loading race condition)
- Fix printer QR code not rendering on initial load (same race condition)
- Add model image to equipment badge via imageurl from Model table
- Fix white-on-white machine number text on badge, tighten barcode spacing
- Add PaginationBar component used across all list pages
- Split monolithic router into per-plugin route modules
- Fix 25 GET API endpoints returning 401 (jwt_required -> optional=True)
- Align list page columns across Equipment, PCs, and Network pages
- Add print views: EquipmentBadge, PrinterQRSingle, PrinterQRBatch, USBLabelBatch
- Add PC Relationships report, migration docs, and CLAUDE.md project guide
- Various plugin model, API, and frontend refinements

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 07:32:44 -05:00

350 lines
11 KiB
Vue

<template>
<div class="detail-page" v-if="device">
<div class="hero-card">
<div class="hero-image">
<div class="device-icon">
<span class="icon"><component :is="getDeviceIcon()" :size="24" /></span>
</div>
</div>
<div class="hero-content">
<div class="hero-title-row">
<h1 class="hero-title">{{ device.networkdevice?.hostname || device.name || device.assetnumber }}</h1>
<router-link
v-if="authStore.isAuthenticated"
:to="`/network/${deviceId}/edit`"
class="btn btn-secondary"
>
Edit
</router-link>
</div>
<div class="hero-meta">
<span class="badge" :class="getStatusClass(device.statusname)">
{{ device.statusname || 'Unknown' }}
</span>
<span v-if="device.networkdevice?.networkdevicetypename" class="meta-item">
{{ device.networkdevice.networkdevicetypename }}
</span>
<span v-if="device.networkdevice?.vendorname" class="meta-item">
{{ device.networkdevice.vendorname }}
</span>
</div>
<div class="hero-details">
<div class="detail-item" v-if="device.assetnumber">
<span class="label">Asset #</span>
<span class="value">{{ device.assetnumber }}</span>
</div>
<div class="detail-item" v-if="device.serialnumber">
<span class="label">Serial</span>
<span class="value mono">{{ device.serialnumber }}</span>
</div>
<div class="detail-item" v-if="device.locationname">
<span class="label">Location</span>
<span class="value">{{ device.locationname }}</span>
</div>
<div class="detail-item" v-if="device.businessunitname">
<span class="label">Business Unit</span>
<span class="value">{{ device.businessunitname }}</span>
</div>
</div>
<div class="hero-features" v-if="device.networkdevice">
<span v-if="device.networkdevice.ispoe" class="feature-badge poe">PoE</span>
<span v-if="device.networkdevice.ismanaged" class="feature-badge managed">Managed</span>
<span v-if="device.networkdevice.portcount" class="feature-badge ports">{{ device.networkdevice.portcount }} Ports</span>
</div>
</div>
</div>
<div class="content-grid">
<div class="content-column">
<!-- Network Info -->
<div class="section-card">
<h3 class="section-title">Network Information</h3>
<div class="info-list">
<div class="info-row">
<span class="info-label">Hostname</span>
<span class="info-value mono">{{ device.networkdevice?.hostname || '-' }}</span>
</div>
<div class="info-row">
<span class="info-label">Firmware Version</span>
<span class="info-value">{{ device.networkdevice?.firmwareversion || '-' }}</span>
</div>
<div class="info-row">
<span class="info-label">Port Count</span>
<span class="info-value">{{ device.networkdevice?.portcount || '-' }}</span>
</div>
<div class="info-row">
<span class="info-label">Rack Unit</span>
<span class="info-value">{{ device.networkdevice?.rackunit || '-' }}</span>
</div>
<div class="info-row">
<span class="info-label">PoE Capable</span>
<span class="info-value">{{ device.networkdevice?.ispoe ? 'Yes' : 'No' }}</span>
</div>
<div class="info-row">
<span class="info-label">Managed Device</span>
<span class="info-value">{{ device.networkdevice?.ismanaged ? 'Yes' : 'No' }}</span>
</div>
</div>
</div>
<!-- Notes -->
<div class="section-card" v-if="device.notes">
<h3 class="section-title">Notes</h3>
<div class="notes-content">{{ device.notes }}</div>
</div>
</div>
<div class="content-column">
<!-- Asset Info -->
<div class="section-card">
<h3 class="section-title">Asset Information</h3>
<div class="info-list">
<div class="info-row">
<span class="info-label">Asset Number</span>
<span class="info-value">{{ device.assetnumber }}</span>
</div>
<div class="info-row">
<span class="info-label">Name</span>
<span class="info-value">{{ device.name || '-' }}</span>
</div>
<div class="info-row">
<span class="info-label">Serial Number</span>
<span class="info-value mono">{{ device.serialnumber || '-' }}</span>
</div>
<div class="info-row">
<span class="info-label">Vendor</span>
<span class="info-value">{{ device.networkdevice?.vendorname || '-' }}</span>
</div>
<div class="info-row">
<span class="info-label">Device Type</span>
<span class="info-value">{{ device.networkdevice?.networkdevicetypename || '-' }}</span>
</div>
<div class="info-row">
<span class="info-label">Status</span>
<span class="info-value">
<span class="badge" :class="getStatusClass(device.statusname)">
{{ device.statusname || 'Unknown' }}
</span>
</span>
</div>
</div>
</div>
<!-- Relationships -->
<AssetRelationships
v-if="device.assetid"
:assetId="device.assetid"
/>
<!-- Audit Info -->
<div class="section-card audit-card">
<h3 class="section-title">Record Info</h3>
<div class="info-list">
<div class="info-row" v-if="device.datecreated">
<span class="info-label">Created</span>
<span class="info-value">{{ formatDate(device.datecreated) }}</span>
</div>
<div class="info-row" v-if="device.datemodified">
<span class="info-label">Last Modified</span>
<span class="info-value">{{ formatDate(device.datemodified) }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- Actions -->
<div class="action-bar" v-if="authStore.isAuthenticated">
<router-link :to="`/network/${deviceId}/edit`" class="btn btn-primary">
Edit Device
</router-link>
<button @click="confirmDelete" class="btn btn-danger">
Delete Device
</button>
</div>
</div>
<div v-else-if="loading" class="loading-container">
<div class="loading">Loading device...</div>
</div>
<div v-else class="error-container">
<p>Device not found</p>
<router-link to="/network" class="btn btn-secondary">Back to Network Devices</router-link>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { Network, Router, Shield, Wifi, Camera, Server, Server as Rack, Globe } from 'lucide-vue-next'
import { useAuthStore } from '../../stores/auth'
import { networkApi } from '../../api'
import AssetRelationships from '../../components/AssetRelationships.vue'
const route = useRoute()
const router = useRouter()
const authStore = useAuthStore()
const deviceId = route.params.id
const device = ref(null)
const loading = ref(true)
onMounted(async () => {
await loadDevice()
})
async function loadDevice() {
loading.value = true
try {
const response = await networkApi.get(deviceId)
device.value = response.data.data
} catch (error) {
console.error('Error loading device:', error)
device.value = null
} finally {
loading.value = false
}
}
function getDeviceIcon() {
const type = device.value?.networkdevice?.networkdevicetypename?.toLowerCase() || ''
if (type.includes('switch')) return Network
if (type.includes('router')) return Router
if (type.includes('firewall')) return Shield
if (type.includes('access point') || type.includes('ap')) return Wifi
if (type.includes('camera')) return Camera
if (type.includes('server')) return Server
if (type.includes('idf') || type.includes('closet')) return Rack
return Globe
}
function getStatusClass(status) {
if (!status) return 'badge-info'
const s = status.toLowerCase()
if (s === 'in use' || s === 'active') return 'badge-success'
if (s === 'in repair' || s === 'maintenance') return 'badge-warning'
if (s === 'retired' || s === 'decommissioned') return 'badge-danger'
return 'badge-info'
}
function formatDate(dateStr) {
if (!dateStr) return '-'
const date = new Date(dateStr)
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
})
}
async function confirmDelete() {
if (confirm(`Are you sure you want to delete ${device.value.networkdevice?.hostname || device.value.assetnumber}?`)) {
try {
await networkApi.delete(deviceId)
router.push('/network')
} catch (error) {
console.error('Error deleting device:', error)
alert('Failed to delete device')
}
}
}
</script>
<style scoped>
.hero-title-row {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 1rem;
margin-bottom: 0.5rem;
}
.hero-title {
margin: 0;
}
.device-icon {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
border-radius: 8px;
}
.device-icon .icon {
font-size: 4rem;
}
.hero-features {
display: flex;
gap: 0.5rem;
margin-top: 1rem;
flex-wrap: wrap;
}
.feature-badge {
display: inline-block;
padding: 0.35rem 0.75rem;
font-size: 0.875rem;
border-radius: 5px;
font-weight: 500;
}
.feature-badge.poe {
background: #d4edda;
color: #155724;
}
.feature-badge.managed {
background: #e3f2fd;
color: #1565c0;
}
.feature-badge.ports {
background: var(--bg);
color: var(--text-light);
}
.mono {
font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
}
.notes-content {
white-space: pre-wrap;
color: var(--text);
line-height: 1.6;
}
.action-bar {
display: flex;
gap: 1rem;
margin-top: 2rem;
padding-top: 1.5rem;
border-top: 1px solid var(--border);
}
.loading-container,
.error-container {
text-align: center;
padding: 3rem;
color: var(--text-light);
}
@media (prefers-color-scheme: dark) {
.feature-badge.poe {
background: #1e3a29;
color: #4ade80;
}
.feature-badge.managed {
background: #1e3a5f;
color: #60a5fa;
}
}
</style>