Feature work from the 2026-07 session: Settings IA - Replace the flat 27-card settings hub with a persistent two-pane shell (SettingsLayout.vue): grouped, searchable left rail + content pane. - Nest all settings/* routes under the shell via router post-processing; shared nav catalog in settingsNav.js. Group by asset class (PCs, Printers, Equipment, Network) so per-type settings stop scattering. Custom fields (core) - customfields + customfieldvalues tables (migration 7d14), CRUD API at /api/customfields, per-asset value get/save. - Settings management page + reusable CustomFieldsSection (detail) and CustomFieldsInputs (form) wired into all four asset types. Warranty (new plugin) - plugins/warranty: warranties + warrantyassets (migration 7d15), derived coverage status, provider abstraction (manual now; Dell/Lenovo/HP stubs). - API CRUD + per-asset panel + report buckets; WarrantyPanel on all four detail pages; Warranties management page; Warranty report + Reports card. - Seed warranty.* permissions. Printer drivers - printerdrivers table (migration 7d13) linked to printer models; drivers now surface on the matching printer's detail page. Other - PCDetail rebalanced (Network + Status + Warranty + custom fields on the right). - Rename PCs list "Features" column to "Remote Access"; fix badge hover underline. - Drop equipment islocationonly field. - Centralize asset-type label/route maps into utils/assetTypes.js. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
338 lines
14 KiB
Vue
338 lines
14 KiB
Vue
<template>
|
|
<div class="detail-page">
|
|
<div class="page-header">
|
|
<h2>Equipment Details</h2>
|
|
<div class="header-actions">
|
|
<router-link :to="`/print/equipment-badge/${equipment?.equipment?.equipmentid}`" class="btn btn-secondary" v-if="equipment" target="_blank">
|
|
Print Badge
|
|
</router-link>
|
|
<router-link :to="`/machines/${equipment?.equipment?.equipmentid}/edit`" class="btn btn-primary" v-if="equipment">
|
|
Edit
|
|
</router-link>
|
|
<router-link to="/machines" class="btn btn-secondary">Back to List</router-link>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="loading" class="loading">Loading...</div>
|
|
|
|
<template v-else-if="equipment">
|
|
<!-- Hero Section -->
|
|
<div class="hero-card">
|
|
<div class="hero-content">
|
|
<div class="hero-title">
|
|
<h1>{{ equipment.assetnumber }}</h1>
|
|
<span v-if="equipment.name" class="hero-alias">{{ equipment.name }}</span>
|
|
</div>
|
|
<div class="hero-meta">
|
|
<span class="badge badge-lg badge-primary">
|
|
{{ equipment.assettypename || 'Equipment' }}
|
|
</span>
|
|
<span class="badge badge-lg" :class="getStatusClass(equipment.statusname)">
|
|
{{ equipment.statusname || 'Unknown' }}
|
|
</span>
|
|
</div>
|
|
<div class="hero-details">
|
|
<div class="hero-detail" v-if="equipment.equipment?.equipmenttypename">
|
|
<span class="hero-detail-label">Type</span>
|
|
<span class="hero-detail-value">{{ equipment.equipment.equipmenttypename }}</span>
|
|
</div>
|
|
<div class="hero-detail" v-if="equipment.equipment?.vendorname">
|
|
<span class="hero-detail-label">Vendor</span>
|
|
<span class="hero-detail-value">{{ equipment.equipment.vendorname }}</span>
|
|
</div>
|
|
<div class="hero-detail" v-if="equipment.equipment?.modelname">
|
|
<span class="hero-detail-label">Model</span>
|
|
<span class="hero-detail-value">{{ equipment.equipment.modelname }}</span>
|
|
</div>
|
|
<div class="hero-detail" v-if="equipment.locationname">
|
|
<span class="hero-detail-label">Location</span>
|
|
<span class="hero-detail-value">{{ equipment.locationname }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Main Content Grid -->
|
|
<div class="content-grid">
|
|
<!-- Left Column -->
|
|
<div class="content-column">
|
|
<!-- Identity Section -->
|
|
<div class="section-card">
|
|
<h3 class="section-title">Identity</h3>
|
|
<div class="info-list">
|
|
<div class="info-row">
|
|
<span class="info-label">Asset Number</span>
|
|
<span class="info-value">{{ equipment.assetnumber }}</span>
|
|
</div>
|
|
<div class="info-row" v-if="equipment.name">
|
|
<span class="info-label">Name</span>
|
|
<span class="info-value">{{ equipment.name }}</span>
|
|
</div>
|
|
<div class="info-row" v-if="isEnabled('gaugelabreference', 'equipment') && equipment.gaugelabreference">
|
|
<span class="info-label">Gauge Lab Reference</span>
|
|
<span class="info-value mono">{{ equipment.gaugelabreference }}</span>
|
|
</div>
|
|
<div class="info-row" v-if="isEnabled('maintenancereference', 'equipment') && equipment.maintenancereference">
|
|
<span class="info-label">Maintenance Reference</span>
|
|
<span class="info-value mono">{{ equipment.maintenancereference }}</span>
|
|
</div>
|
|
<div class="info-row" v-if="equipment.serialnumber">
|
|
<span class="info-label">Serial Number</span>
|
|
<span class="info-value mono">{{ equipment.serialnumber }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Hardware Section -->
|
|
<div class="section-card">
|
|
<h3 class="section-title">Hardware</h3>
|
|
<div class="info-list">
|
|
<div class="info-row">
|
|
<span class="info-label">Type</span>
|
|
<span class="info-value">{{ equipment.equipment?.equipmenttypename || '-' }}</span>
|
|
</div>
|
|
<div class="info-row">
|
|
<span class="info-label">Vendor</span>
|
|
<span class="info-value">{{ equipment.equipment?.vendorname || '-' }}</span>
|
|
</div>
|
|
<div class="info-row">
|
|
<span class="info-label">Model</span>
|
|
<span class="info-value">{{ equipment.equipment?.modelname || '-' }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Controller Section (for CNC machines) -->
|
|
<div class="section-card" v-if="equipment.equipment?.controllervendorname || equipment.equipment?.controllermodelname">
|
|
<h3 class="section-title">Controller</h3>
|
|
<div class="info-list">
|
|
<div class="info-row">
|
|
<span class="info-label">Vendor</span>
|
|
<span class="info-value">{{ equipment.equipment?.controllervendorname || '-' }}</span>
|
|
</div>
|
|
<div class="info-row">
|
|
<span class="info-label">Model</span>
|
|
<span class="info-value">{{ equipment.equipment?.controllermodelname || '-' }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Equipment Configuration -->
|
|
<div class="section-card">
|
|
<h3 class="section-title">Configuration</h3>
|
|
<div class="info-list">
|
|
<div class="info-row">
|
|
<span class="info-label">Requires Manual Config</span>
|
|
<span class="info-value">
|
|
<span class="feature-tag" :class="{ active: equipment.equipment?.requiresmanualconfig }">
|
|
{{ equipment.equipment?.requiresmanualconfig ? 'Yes' : 'No' }}
|
|
</span>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Maintenance Section -->
|
|
<div class="section-card" v-if="equipment.equipment?.lastmaintenancedate || equipment.equipment?.nextmaintenancedate">
|
|
<h3 class="section-title">Maintenance</h3>
|
|
<div class="info-list">
|
|
<div class="info-row" v-if="equipment.equipment?.lastmaintenancedate">
|
|
<span class="info-label">Last Maintenance</span>
|
|
<span class="info-value">{{ formatDate(equipment.equipment.lastmaintenancedate) }}</span>
|
|
</div>
|
|
<div class="info-row" v-if="equipment.equipment?.nextmaintenancedate">
|
|
<span class="info-label">Next Maintenance</span>
|
|
<span class="info-value">{{ formatDate(equipment.equipment.nextmaintenancedate) }}</span>
|
|
</div>
|
|
<div class="info-row" v-if="equipment.equipment?.maintenanceintervaldays">
|
|
<span class="info-label">Interval</span>
|
|
<span class="info-value">{{ equipment.equipment.maintenanceintervaldays }} days</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Right Column -->
|
|
<div class="content-column">
|
|
<!-- Location & Organization -->
|
|
<div class="section-card">
|
|
<h3 class="section-title">Location & Organization</h3>
|
|
<div class="info-list">
|
|
<div class="info-row">
|
|
<span class="info-label">Location</span>
|
|
<span class="info-value">
|
|
<LocationMapTooltip
|
|
v-if="equipment.mapx != null && equipment.mapy != null"
|
|
:left="equipment.mapx"
|
|
:top="equipment.mapy"
|
|
:machineName="equipment.assetnumber"
|
|
>
|
|
<span class="location-link">{{ equipment.locationname || 'On Map' }}</span>
|
|
</LocationMapTooltip>
|
|
<span v-else>{{ equipment.locationname || '-' }}</span>
|
|
</span>
|
|
</div>
|
|
<div class="info-row">
|
|
<span class="info-label">Business Unit</span>
|
|
<span class="info-value">{{ equipment.businessunitname || '-' }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Connected PC -->
|
|
<div class="section-card">
|
|
<h3 class="section-title">Connected PC</h3>
|
|
<div v-if="!controllingPc" class="empty-message">
|
|
No controlling PC assigned
|
|
</div>
|
|
<div v-else class="connected-device">
|
|
<router-link :to="`/pcs/${controllingPc.pluginid || controllingPc.assetid}`" class="device-link">
|
|
<div class="device-icon">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect>
|
|
<line x1="8" y1="21" x2="16" y2="21"></line>
|
|
<line x1="12" y1="17" x2="12" y2="21"></line>
|
|
</svg>
|
|
</div>
|
|
<div class="device-info">
|
|
<span class="device-name">{{ controllingPc.assetnumber }}</span>
|
|
<span class="device-alias" v-if="controllingPc.name">{{ controllingPc.name }}</span>
|
|
</div>
|
|
</router-link>
|
|
<span class="connection-type">{{ controllingPc.relationshipType }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Custom Fields -->
|
|
<CustomFieldsSection :assetid="equipment.assetid" />
|
|
|
|
<!-- Warranty -->
|
|
<WarrantyPanel :assetid="equipment.assetid" />
|
|
|
|
<!-- Notes -->
|
|
<div class="section-card" v-if="equipment.notes">
|
|
<h3 class="section-title">Notes</h3>
|
|
<p class="notes-text">{{ equipment.notes }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Audit Footer -->
|
|
<div class="audit-footer">
|
|
<span>Created {{ formatDate(equipment.createddate) }}<template v-if="equipment.createdby"> by {{ equipment.createdby }}</template></span>
|
|
<span>Modified {{ formatDate(equipment.modifieddate) }}<template v-if="equipment.modifiedby"> by {{ equipment.modifiedby }}</template></span>
|
|
</div>
|
|
</template>
|
|
|
|
<div v-else class="card">
|
|
<p style="text-align: center; color: var(--text-light);">Equipment not found</p>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, onMounted } from 'vue'
|
|
import { useRoute } from 'vue-router'
|
|
import { equipmentApi, assetsApi } from '../../api'
|
|
import LocationMapTooltip from '../../components/LocationMapTooltip.vue'
|
|
import CustomFieldsSection from '../../components/CustomFieldsSection.vue'
|
|
import WarrantyPanel from '../../components/WarrantyPanel.vue'
|
|
import { useIdentifierFlags } from '../../composables/identifierSettings'
|
|
|
|
const route = useRoute()
|
|
const { isEnabled } = useIdentifierFlags()
|
|
|
|
const loading = ref(true)
|
|
const equipment = ref(null)
|
|
const relationships = ref({ incoming: [], outgoing: [] })
|
|
|
|
const controllingPc = computed(() => {
|
|
// For equipment, find a related computer in any "Controls" relationship
|
|
// Check both incoming (computer controls this) and outgoing (legacy data may have equipment -> computer)
|
|
|
|
// First check incoming - computer as source controlling this equipment
|
|
for (const rel of relationships.value.incoming || []) {
|
|
if (rel.sourceasset?.assettypename === 'computer' && rel.relationshiptypename === 'Controls') {
|
|
return {
|
|
...rel.sourceasset,
|
|
relationshipType: rel.relationshiptypename
|
|
}
|
|
}
|
|
}
|
|
|
|
// Also check outgoing - legacy data may have equipment -> computer Controls relationships
|
|
for (const rel of relationships.value.outgoing || []) {
|
|
if (rel.targetasset?.assettypename === 'computer' && rel.relationshiptypename === 'Controls') {
|
|
return {
|
|
...rel.targetasset,
|
|
relationshipType: rel.relationshiptypename
|
|
}
|
|
}
|
|
}
|
|
|
|
return null
|
|
})
|
|
|
|
onMounted(async () => {
|
|
try {
|
|
const response = await equipmentApi.get(route.params.id)
|
|
equipment.value = response.data.data
|
|
|
|
// Load relationships using asset ID
|
|
if (equipment.value?.assetid) {
|
|
try {
|
|
const relResponse = await assetsApi.getRelationships(equipment.value.assetid)
|
|
relationships.value = relResponse.data.data || { incoming: [], outgoing: [] }
|
|
} catch (e) {
|
|
console.log('Relationships not available')
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading equipment:', error)
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
})
|
|
|
|
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 === 'spare') return 'badge-warning'
|
|
if (s === 'retired' || s === 'disposed') return 'badge-danger'
|
|
return 'badge-info'
|
|
}
|
|
|
|
function formatDate(dateStr) {
|
|
if (!dateStr) return '-'
|
|
return new Date(dateStr).toLocaleString()
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.mono {
|
|
font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
|
|
}
|
|
|
|
.feature-tag {
|
|
display: inline-block;
|
|
padding: 0.3rem 0.625rem;
|
|
font-size: 0.875rem;
|
|
border-radius: 5px;
|
|
background: var(--bg);
|
|
color: var(--text-light);
|
|
}
|
|
|
|
.feature-tag.active {
|
|
background: #e3f2fd;
|
|
color: #1976d2;
|
|
}
|
|
|
|
@media (prefers-color-scheme: dark) {
|
|
.feature-tag.active {
|
|
background: #1e3a5f;
|
|
color: #60a5fa;
|
|
}
|
|
}
|
|
</style>
|