Migrate frontend to plugin-based asset architecture
- Add equipmentApi and computersApi to replace legacy machinesApi - Add controller vendor/model fields to Equipment model and forms - Fix map marker navigation to use plugin-specific IDs (equipmentid, computerid, printerid, networkdeviceid) instead of assetid - Fix search to use unified Asset table with correct plugin IDs - Remove legacy printer search that used non-existent field names - Enable optional JWT auth for detail endpoints (public read access) - Clean up USB plugin models (remove unused checkout model) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -54,7 +54,7 @@ export const authApi = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Machines API
|
// Machines API (legacy - use equipmentApi or computersApi instead)
|
||||||
export const machinesApi = {
|
export const machinesApi = {
|
||||||
list(params = {}) {
|
list(params = {}) {
|
||||||
return api.get('/machines', { params })
|
return api.get('/machines', { params })
|
||||||
@@ -86,6 +86,89 @@ export const machinesApi = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Equipment API (plugin)
|
||||||
|
export const equipmentApi = {
|
||||||
|
list(params = {}) {
|
||||||
|
return api.get('/equipment', { params })
|
||||||
|
},
|
||||||
|
get(id) {
|
||||||
|
return api.get(`/equipment/${id}`)
|
||||||
|
},
|
||||||
|
getByAsset(assetId) {
|
||||||
|
return api.get(`/equipment/by-asset/${assetId}`)
|
||||||
|
},
|
||||||
|
create(data) {
|
||||||
|
return api.post('/equipment', data)
|
||||||
|
},
|
||||||
|
update(id, data) {
|
||||||
|
return api.put(`/equipment/${id}`, data)
|
||||||
|
},
|
||||||
|
delete(id) {
|
||||||
|
return api.delete(`/equipment/${id}`)
|
||||||
|
},
|
||||||
|
dashboardSummary() {
|
||||||
|
return api.get('/equipment/dashboard/summary')
|
||||||
|
},
|
||||||
|
// Equipment types
|
||||||
|
types: {
|
||||||
|
list(params = {}) {
|
||||||
|
return api.get('/equipment/types', { params })
|
||||||
|
},
|
||||||
|
get(id) {
|
||||||
|
return api.get(`/equipment/types/${id}`)
|
||||||
|
},
|
||||||
|
create(data) {
|
||||||
|
return api.post('/equipment/types', data)
|
||||||
|
},
|
||||||
|
update(id, data) {
|
||||||
|
return api.put(`/equipment/types/${id}`, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Computers API (plugin)
|
||||||
|
export const computersApi = {
|
||||||
|
list(params = {}) {
|
||||||
|
return api.get('/computers', { params })
|
||||||
|
},
|
||||||
|
get(id) {
|
||||||
|
return api.get(`/computers/${id}`)
|
||||||
|
},
|
||||||
|
getByAsset(assetId) {
|
||||||
|
return api.get(`/computers/by-asset/${assetId}`)
|
||||||
|
},
|
||||||
|
getByHostname(hostname) {
|
||||||
|
return api.get(`/computers/by-hostname/${hostname}`)
|
||||||
|
},
|
||||||
|
create(data) {
|
||||||
|
return api.post('/computers', data)
|
||||||
|
},
|
||||||
|
update(id, data) {
|
||||||
|
return api.put(`/computers/${id}`, data)
|
||||||
|
},
|
||||||
|
delete(id) {
|
||||||
|
return api.delete(`/computers/${id}`)
|
||||||
|
},
|
||||||
|
dashboardSummary() {
|
||||||
|
return api.get('/computers/dashboard/summary')
|
||||||
|
},
|
||||||
|
// Computer types
|
||||||
|
types: {
|
||||||
|
list(params = {}) {
|
||||||
|
return api.get('/computers/types', { params })
|
||||||
|
},
|
||||||
|
get(id) {
|
||||||
|
return api.get(`/computers/types/${id}`)
|
||||||
|
},
|
||||||
|
create(data) {
|
||||||
|
return api.post('/computers/types', data)
|
||||||
|
},
|
||||||
|
update(id, data) {
|
||||||
|
return api.put(`/computers/types/${id}`, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Relationship Types API
|
// Relationship Types API
|
||||||
export const relationshipTypesApi = {
|
export const relationshipTypesApi = {
|
||||||
list() {
|
list() {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
import { ref, onMounted, onUnmounted, watch } from 'vue'
|
import { ref, onMounted, onUnmounted, watch } from 'vue'
|
||||||
import L from 'leaflet'
|
import L from 'leaflet'
|
||||||
import 'leaflet/dist/leaflet.css'
|
import 'leaflet/dist/leaflet.css'
|
||||||
|
import { currentTheme } from '../stores/theme'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
left: { type: Number, default: null },
|
left: { type: Number, default: null },
|
||||||
@@ -23,11 +24,6 @@ const MAP_WIDTH = 3300
|
|||||||
const MAP_HEIGHT = 2550
|
const MAP_HEIGHT = 2550
|
||||||
const bounds = [[0, 0], [MAP_HEIGHT, MAP_WIDTH]]
|
const bounds = [[0, 0], [MAP_HEIGHT, MAP_WIDTH]]
|
||||||
|
|
||||||
// Detect system color scheme
|
|
||||||
function getTheme() {
|
|
||||||
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
|
||||||
}
|
|
||||||
|
|
||||||
function initMap() {
|
function initMap() {
|
||||||
if (!mapContainer.value || props.left === null || props.top === null) return
|
if (!mapContainer.value || props.left === null || props.top === null) return
|
||||||
|
|
||||||
@@ -39,8 +35,7 @@ function initMap() {
|
|||||||
zoomControl: true
|
zoomControl: true
|
||||||
})
|
})
|
||||||
|
|
||||||
const theme = getTheme()
|
const blueprintUrl = currentTheme.value === 'light'
|
||||||
const blueprintUrl = theme === 'light'
|
|
||||||
? '/static/images/sitemap2025-light.png'
|
? '/static/images/sitemap2025-light.png'
|
||||||
: '/static/images/sitemap2025-dark.png'
|
: '/static/images/sitemap2025-dark.png'
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, nextTick, watch, onMounted, onUnmounted } from 'vue'
|
import { ref, computed, nextTick, watch } from 'vue'
|
||||||
|
import { currentTheme } from '../stores/theme'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
left: { type: Number, default: null },
|
left: { type: Number, default: null },
|
||||||
@@ -49,22 +50,6 @@ const props = defineProps({
|
|||||||
machineName: { type: String, default: '' }
|
machineName: { type: String, default: '' }
|
||||||
})
|
})
|
||||||
|
|
||||||
// Auto-detect system theme with reactive updates
|
|
||||||
const systemTheme = ref(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
|
|
||||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
|
||||||
|
|
||||||
function handleThemeChange(e) {
|
|
||||||
systemTheme.value = e.matches ? 'dark' : 'light'
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
mediaQuery.addEventListener('change', handleThemeChange)
|
|
||||||
})
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
mediaQuery.removeEventListener('change', handleThemeChange)
|
|
||||||
})
|
|
||||||
|
|
||||||
const visible = ref(false)
|
const visible = ref(false)
|
||||||
const tooltipRef = ref(null)
|
const tooltipRef = ref(null)
|
||||||
const mapPreview = ref(null)
|
const mapPreview = ref(null)
|
||||||
@@ -82,7 +67,9 @@ const hasPosition = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const blueprintUrl = computed(() => {
|
const blueprintUrl = computed(() => {
|
||||||
return systemTheme.value === 'light'
|
// Force re-evaluation when theme changes by including theme in the computed
|
||||||
|
const theme = currentTheme.value
|
||||||
|
return theme === 'light'
|
||||||
? '/static/images/sitemap2025-light.png'
|
? '/static/images/sitemap2025-light.png'
|
||||||
: '/static/images/sitemap2025-dark.png'
|
: '/static/images/sitemap2025-dark.png'
|
||||||
})
|
})
|
||||||
@@ -196,6 +183,11 @@ watch(visible, (newVal) => {
|
|||||||
zoom.value = 1
|
zoom.value = 1
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Reset imageLoaded when theme changes to force reload
|
||||||
|
watch(currentTheme, () => {
|
||||||
|
imageLoaded.value = false
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="shopfloor-map">
|
<div class="shopfloor-map">
|
||||||
<div class="map-controls" v-if="!pickerMode">
|
<!-- Controls for legacy machine mode only (MapView handles filters for asset mode) -->
|
||||||
|
<div class="map-controls" v-if="!pickerMode && !assetTypeMode">
|
||||||
<div class="filters">
|
<div class="filters">
|
||||||
<select v-model="filters.machinetype" @change="applyFilters">
|
<select v-model="filters.machinetype" @change="applyFilters">
|
||||||
<option value="">All Types</option>
|
<option value="">All Types</option>
|
||||||
@@ -43,6 +44,32 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Legend for asset type mode - shows subtypes when a type is selected -->
|
||||||
|
<div class="map-legend" v-if="!pickerMode && assetTypeMode">
|
||||||
|
<!-- Show subtype legend when a specific type is selected -->
|
||||||
|
<template v-if="selectedAssetType && Object.keys(visibleSubtypes).length">
|
||||||
|
<span
|
||||||
|
v-for="(color, subtypeId) in visibleSubtypes"
|
||||||
|
:key="subtypeId"
|
||||||
|
class="legend-item"
|
||||||
|
>
|
||||||
|
<span class="legend-dot" :style="{ background: color }"></span>
|
||||||
|
{{ subtypeNames[subtypeId] || `Type ${subtypeId}` }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<!-- Show asset type legend when no type is selected -->
|
||||||
|
<template v-else>
|
||||||
|
<span
|
||||||
|
v-for="(color, assetType) in visibleAssetTypes"
|
||||||
|
:key="assetType"
|
||||||
|
class="legend-item"
|
||||||
|
>
|
||||||
|
<span class="legend-dot" :style="{ background: color }"></span>
|
||||||
|
{{ assetTypeLabels[assetType] || assetType }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="picker-controls" v-if="pickerMode">
|
<div class="picker-controls" v-if="pickerMode">
|
||||||
<span class="picker-message">Click on the map to set location</span>
|
<span class="picker-message">Click on the map to set location</span>
|
||||||
<span v-if="pickedPosition" class="picker-coords">
|
<span v-if="pickedPosition" class="picker-coords">
|
||||||
@@ -68,7 +95,10 @@ const props = defineProps({
|
|||||||
theme: { type: String, default: 'dark' },
|
theme: { type: String, default: 'dark' },
|
||||||
pickerMode: { type: Boolean, default: false },
|
pickerMode: { type: Boolean, default: false },
|
||||||
initialPosition: { type: Object, default: null }, // { left, top }
|
initialPosition: { type: Object, default: null }, // { left, top }
|
||||||
assetTypeMode: { type: Boolean, default: false } // When true, use unified asset format
|
assetTypeMode: { type: Boolean, default: false }, // When true, use unified asset format
|
||||||
|
selectedAssetType: { type: String, default: '' }, // Currently selected asset type filter
|
||||||
|
subtypeColors: { type: Object, default: () => ({}) }, // Map of subtype ID to color
|
||||||
|
subtypeNames: { type: Object, default: () => ({}) } // Map of subtype ID to name
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['markerClick', 'positionPicked'])
|
const emit = defineEmits(['markerClick', 'positionPicked'])
|
||||||
@@ -92,14 +122,40 @@ const MAP_WIDTH = 3300
|
|||||||
const MAP_HEIGHT = 2550
|
const MAP_HEIGHT = 2550
|
||||||
const bounds = [[0, 0], [MAP_HEIGHT, MAP_WIDTH]]
|
const bounds = [[0, 0], [MAP_HEIGHT, MAP_WIDTH]]
|
||||||
|
|
||||||
// Asset type colors (for unified map mode)
|
// Asset type colors (for unified map mode) - normalized lookup
|
||||||
const assetTypeColors = {
|
const assetTypeColorsMap = {
|
||||||
'equipment': '#F44336', // Red
|
'equipment': '#F44336', // Red
|
||||||
'computer': '#2196F3', // Blue
|
'computer': '#2196F3', // Blue
|
||||||
'printer': '#4CAF50', // Green
|
'printer': '#4CAF50', // Green
|
||||||
'network_device': '#FF9800' // Orange
|
'network device': '#FF9800', // Orange
|
||||||
|
'network_device': '#FF9800' // Orange (alternate key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get asset type color with case-insensitive lookup
|
||||||
|
function getAssetTypeColor(assettype) {
|
||||||
|
if (!assettype) return '#BDBDBD'
|
||||||
|
const normalized = assettype.toLowerCase()
|
||||||
|
return assetTypeColorsMap[normalized] || '#BDBDBD'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Asset type labels for display (case-insensitive lookup)
|
||||||
|
const assetTypeLabelsMap = {
|
||||||
|
'equipment': 'Equipment',
|
||||||
|
'computer': 'Computers',
|
||||||
|
'printer': 'Printers',
|
||||||
|
'network device': 'Network Devices',
|
||||||
|
'network_device': 'Network Devices'
|
||||||
|
}
|
||||||
|
|
||||||
|
const assetTypeLabels = new Proxy({}, {
|
||||||
|
get(target, prop) {
|
||||||
|
if (typeof prop === 'string') {
|
||||||
|
return assetTypeLabelsMap[prop.toLowerCase()] || prop
|
||||||
|
}
|
||||||
|
return prop
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// Type colors - distinct colors for each machine type
|
// Type colors - distinct colors for each machine type
|
||||||
const typeColors = {
|
const typeColors = {
|
||||||
// Machining
|
// Machining
|
||||||
@@ -161,6 +217,44 @@ const visibleTypes = computed(() => {
|
|||||||
return props.machinetypes.filter(t => typeIds.has(t.machinetypeid))
|
return props.machinetypes.filter(t => typeIds.has(t.machinetypeid))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Get unique visible asset types (for asset mode)
|
||||||
|
const visibleAssetTypes = computed(() => {
|
||||||
|
const types = new Set(props.machines.map(m => m.assettype).filter(Boolean))
|
||||||
|
const result = {}
|
||||||
|
for (const t of types) {
|
||||||
|
result[t] = getAssetTypeColor(t)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get subtype ID from asset based on asset type
|
||||||
|
function getSubtypeId(asset) {
|
||||||
|
if (!asset.typedata) return null
|
||||||
|
const typeLower = asset.assettype?.toLowerCase() || ''
|
||||||
|
if (typeLower === 'equipment') return asset.typedata.equipmenttypeid
|
||||||
|
if (typeLower === 'computer') return asset.typedata.computertypeid
|
||||||
|
if (typeLower === 'network device') return asset.typedata.networkdevicetypeid
|
||||||
|
if (typeLower === 'printer') return asset.typedata.printertypeid
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get visible subtypes when a type is selected
|
||||||
|
const visibleSubtypes = computed(() => {
|
||||||
|
if (!props.selectedAssetType) return {}
|
||||||
|
const subtypeIds = new Set(
|
||||||
|
props.machines
|
||||||
|
.map(m => getSubtypeId(m))
|
||||||
|
.filter(id => id != null)
|
||||||
|
)
|
||||||
|
const result = {}
|
||||||
|
for (const id of subtypeIds) {
|
||||||
|
if (props.subtypeColors[id]) {
|
||||||
|
result[id] = props.subtypeColors[id]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
|
||||||
function initMap() {
|
function initMap() {
|
||||||
if (!mapContainer.value) return
|
if (!mapContainer.value) return
|
||||||
|
|
||||||
@@ -284,9 +378,15 @@ function renderMarkers() {
|
|||||||
let color, typeName, displayName, detailRoute
|
let color, typeName, displayName, detailRoute
|
||||||
|
|
||||||
if (props.assetTypeMode) {
|
if (props.assetTypeMode) {
|
||||||
// Unified asset mode
|
// Unified asset mode - use subtype colors when a type is selected
|
||||||
color = assetTypeColors[item.assettype] || '#BDBDBD'
|
if (props.selectedAssetType) {
|
||||||
|
const subtypeId = getSubtypeId(item)
|
||||||
|
color = (subtypeId && props.subtypeColors[subtypeId]) || '#BDBDBD'
|
||||||
|
typeName = (subtypeId && props.subtypeNames[subtypeId]) || item.assettype || ''
|
||||||
|
} else {
|
||||||
|
color = getAssetTypeColor(item.assettype)
|
||||||
typeName = item.assettype || ''
|
typeName = item.assettype || ''
|
||||||
|
}
|
||||||
displayName = item.displayname || item.name || item.assetnumber || 'Unknown'
|
displayName = item.displayname || item.name || item.assetnumber || 'Unknown'
|
||||||
detailRoute = getAssetDetailRoute(item)
|
detailRoute = getAssetDetailRoute(item)
|
||||||
} else {
|
} else {
|
||||||
@@ -299,9 +399,9 @@ function renderMarkers() {
|
|||||||
|
|
||||||
const icon = L.divIcon({
|
const icon = L.divIcon({
|
||||||
html: `<div class="machine-marker-dot" style="background: ${color};"></div>`,
|
html: `<div class="machine-marker-dot" style="background: ${color};"></div>`,
|
||||||
iconSize: [24, 24],
|
iconSize: [12, 12],
|
||||||
iconAnchor: [12, 12],
|
iconAnchor: [6, 6],
|
||||||
popupAnchor: [0, -12],
|
popupAnchor: [0, -6],
|
||||||
className: 'machine-marker'
|
className: 'machine-marker'
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -312,13 +412,7 @@ function renderMarkers() {
|
|||||||
|
|
||||||
if (props.assetTypeMode) {
|
if (props.assetTypeMode) {
|
||||||
// Asset mode tooltips
|
// Asset mode tooltips
|
||||||
const assetTypeLabel = {
|
tooltipLines.push(`<span style="color: #888;">${item.assettype || 'Unknown'}</span>`)
|
||||||
'equipment': 'Equipment',
|
|
||||||
'computer': 'Computer',
|
|
||||||
'printer': 'Printer',
|
|
||||||
'network_device': 'Network Device'
|
|
||||||
}
|
|
||||||
tooltipLines.push(`<span style="color: #888;">${assetTypeLabel[item.assettype] || item.assettype}</span>`)
|
|
||||||
|
|
||||||
if (item.primaryip) {
|
if (item.primaryip) {
|
||||||
tooltipLines.push(`<span style="color: #8cf;">IP: ${item.primaryip}</span>`)
|
tooltipLines.push(`<span style="color: #8cf;">IP: ${item.primaryip}</span>`)
|
||||||
@@ -421,19 +515,30 @@ function renderMarkers() {
|
|||||||
|
|
||||||
// Get detail route for unified asset format
|
// Get detail route for unified asset format
|
||||||
function getAssetDetailRoute(asset) {
|
function getAssetDetailRoute(asset) {
|
||||||
|
const assetType = (asset.assettype || '').toLowerCase()
|
||||||
const routeMap = {
|
const routeMap = {
|
||||||
'equipment': '/machines',
|
'equipment': '/machines',
|
||||||
'computer': '/pcs',
|
'computer': '/pcs',
|
||||||
'printer': '/printers',
|
'printer': '/printers',
|
||||||
'network_device': '/network'
|
'network_device': '/network',
|
||||||
|
'network device': '/network'
|
||||||
}
|
}
|
||||||
const basePath = routeMap[asset.assettype] || '/machines'
|
const basePath = routeMap[assetType] || '/machines'
|
||||||
|
|
||||||
if (asset.assettype === 'network_device' && asset.typedata?.networkdeviceid) {
|
// Get the plugin-specific ID from typedata
|
||||||
return `/network/${asset.typedata.networkdeviceid}`
|
let id = asset.assetid // fallback
|
||||||
|
if (asset.typedata) {
|
||||||
|
if (assetType === 'equipment' && asset.typedata.equipmentid) {
|
||||||
|
id = asset.typedata.equipmentid
|
||||||
|
} else if (assetType === 'computer' && asset.typedata.computerid) {
|
||||||
|
id = asset.typedata.computerid
|
||||||
|
} else if (assetType === 'printer' && asset.typedata.printerid) {
|
||||||
|
id = asset.typedata.printerid
|
||||||
|
} else if ((assetType === 'network_device' || assetType === 'network device') && asset.typedata.networkdeviceid) {
|
||||||
|
id = asset.typedata.networkdeviceid
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const id = asset.typedata?.machineid || asset.assetid
|
|
||||||
return `${basePath}/${id}`
|
return `${basePath}/${id}`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -556,6 +661,19 @@ onUnmounted(() => {
|
|||||||
color: var(--text);
|
color: var(--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.map-legend-bar {
|
||||||
|
display: flex;
|
||||||
|
gap: 1.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
font-size: 1rem;
|
||||||
|
color: var(--text);
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
background: var(--bg-card);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.legend-item {
|
.legend-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -645,11 +763,11 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
:deep(.machine-marker-dot) {
|
:deep(.machine-marker-dot) {
|
||||||
width: 24px;
|
width: 12px;
|
||||||
height: 24px;
|
height: 12px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
border: 3px solid var(--border);
|
border: 2px solid rgba(255,255,255,0.8);
|
||||||
box-shadow: 0 2px 6px rgba(0,0,0,0.5);
|
box-shadow: 0 1px 4px rgba(0,0,0,0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.picker-marker-dot) {
|
:deep(.picker-marker-dot) {
|
||||||
@@ -676,4 +794,18 @@ onUnmounted(() => {
|
|||||||
:deep(.marker-tooltip::before) {
|
:deep(.marker-tooltip::before) {
|
||||||
border-top-color: rgba(0, 0, 0, 0.92);
|
border-top-color: rgba(0, 0, 0, 0.92);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Legend bar for asset type mode (no filters - parent handles them) */
|
||||||
|
.map-legend {
|
||||||
|
display: flex;
|
||||||
|
gap: 1.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
background: var(--bg-card);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -54,10 +54,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Recent Machines -->
|
<!-- Recent Devices -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h3>Recent Machines</h3>
|
<h3>Recent Devices</h3>
|
||||||
<router-link to="/machines" class="btn btn-secondary btn-sm">View All</router-link>
|
<router-link to="/machines" class="btn btn-secondary btn-sm">View All</router-link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -65,26 +65,22 @@
|
|||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Machine #</th>
|
<th>Name</th>
|
||||||
<th>Alias</th>
|
<th>Category</th>
|
||||||
<th>Type</th>
|
<th>Type</th>
|
||||||
<th>Status</th>
|
<th>Business Unit</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="machine in recentMachines" :key="machine.machineid">
|
<tr v-for="machine in recentMachines" :key="machine.machineid">
|
||||||
<td>{{ machine.machinenumber }}</td>
|
<td>{{ machine.machinenumber || machine.hostname || machine.alias || '-' }}</td>
|
||||||
<td>{{ machine.alias || '-' }}</td>
|
<td>{{ machine.category || '-' }}</td>
|
||||||
<td>{{ machine.machinetype }}</td>
|
<td>{{ machine.machinetype || '-' }}</td>
|
||||||
<td>
|
<td>{{ machine.businessunit || '-' }}</td>
|
||||||
<span class="badge" :class="getStatusClass(machine.status)">
|
|
||||||
{{ machine.status || 'Unknown' }}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-if="recentMachines.length === 0">
|
<tr v-if="recentMachines.length === 0">
|
||||||
<td colspan="4" style="text-align: center; color: var(--text-light);">
|
<td colspan="4" style="text-align: center; color: var(--text-light);">
|
||||||
No machines found
|
No devices found
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -10,19 +10,44 @@
|
|||||||
<div v-if="loading" class="loading">Loading...</div>
|
<div v-if="loading" class="loading">Loading...</div>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<!-- Layer Toggles -->
|
<!-- Filter Controls -->
|
||||||
<div class="layer-toggles">
|
<div class="map-filters">
|
||||||
<label v-for="t in assetTypes" :key="t.assettypeid" class="layer-toggle">
|
<select v-model="selectedType" @change="onTypeChange">
|
||||||
|
<option value="">All Asset Types</option>
|
||||||
|
<option v-for="t in assetTypes" :key="t.assettypeid" :value="t.assettype">
|
||||||
|
{{ formatTypeName(t.assettype) }} ({{ getTypeCount(t.assettype) }})
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select v-model="selectedSubtype" @change="updateMapLayers" :disabled="!selectedType || !currentSubtypes.length">
|
||||||
|
<option value="">{{ subtypeLabel }}</option>
|
||||||
|
<option v-for="st in currentSubtypes" :key="st.id" :value="st.id">
|
||||||
|
{{ st.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select v-model="selectedBusinessUnit" @change="updateMapLayers">
|
||||||
|
<option value="">All Business Units</option>
|
||||||
|
<option v-for="bu in businessunits" :key="bu.businessunitid" :value="bu.businessunitid">
|
||||||
|
{{ bu.businessunit }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select v-model="selectedStatus" @change="updateMapLayers">
|
||||||
|
<option value="">All Statuses</option>
|
||||||
|
<option v-for="s in statuses" :key="s.statusid" :value="s.statusid">
|
||||||
|
{{ s.status }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="text"
|
||||||
v-model="visibleTypes"
|
v-model="searchQuery"
|
||||||
:value="t.assettype"
|
placeholder="Search assets..."
|
||||||
@change="updateMapLayers"
|
@input="debouncedSearch"
|
||||||
/>
|
/>
|
||||||
<span class="layer-icon">{{ getTypeIcon(t.assettype) }}</span>
|
|
||||||
<span>{{ t.assettype }}</span>
|
<span class="result-count">{{ filteredAssets.length }} assets</span>
|
||||||
<span class="layer-count">({{ getTypeCount(t.assettype) }})</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ShopFloorMap
|
<ShopFloorMap
|
||||||
@@ -32,6 +57,9 @@
|
|||||||
:statuses="statuses"
|
:statuses="statuses"
|
||||||
:assetTypeMode="true"
|
:assetTypeMode="true"
|
||||||
:theme="currentTheme"
|
:theme="currentTheme"
|
||||||
|
:selectedAssetType="selectedType"
|
||||||
|
:subtypeColors="subtypeColorMap"
|
||||||
|
:subtypeNames="subtypeNameMap"
|
||||||
@markerClick="handleMarkerClick"
|
@markerClick="handleMarkerClick"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
@@ -53,11 +81,125 @@ const assets = ref([])
|
|||||||
const assetTypes = ref([])
|
const assetTypes = ref([])
|
||||||
const businessunits = ref([])
|
const businessunits = ref([])
|
||||||
const statuses = ref([])
|
const statuses = ref([])
|
||||||
const visibleTypes = ref([])
|
const subtypes = ref({})
|
||||||
|
|
||||||
|
// Filter state
|
||||||
|
const selectedType = ref('')
|
||||||
|
const selectedSubtype = ref('')
|
||||||
|
const selectedBusinessUnit = ref('')
|
||||||
|
const selectedStatus = ref('')
|
||||||
|
const searchQuery = ref('')
|
||||||
|
|
||||||
|
let searchTimeout = null
|
||||||
|
|
||||||
|
// Case-insensitive lookup helper for subtypes
|
||||||
|
function getSubtypesForType(typeName) {
|
||||||
|
if (!typeName || !subtypes.value) return []
|
||||||
|
// Try exact match first
|
||||||
|
if (subtypes.value[typeName]) return subtypes.value[typeName]
|
||||||
|
// Try case-insensitive match
|
||||||
|
const lowerType = typeName.toLowerCase()
|
||||||
|
for (const [key, value] of Object.entries(subtypes.value)) {
|
||||||
|
if (key.toLowerCase() === lowerType) return value
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentSubtypes = computed(() => {
|
||||||
|
if (!selectedType.value) return []
|
||||||
|
return getSubtypesForType(selectedType.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const subtypeLabel = computed(() => {
|
||||||
|
if (!selectedType.value) return 'Select type first'
|
||||||
|
if (!currentSubtypes.value.length) return 'No subtypes'
|
||||||
|
const labels = {
|
||||||
|
'equipment': 'All Machine Types',
|
||||||
|
'computer': 'All Computer Types',
|
||||||
|
'network device': 'All Device Types',
|
||||||
|
'printer': 'All Printer Types'
|
||||||
|
}
|
||||||
|
return labels[selectedType.value.toLowerCase()] || 'All Subtypes'
|
||||||
|
})
|
||||||
|
|
||||||
|
// Generate distinct colors for subtypes
|
||||||
|
const subtypeColorPalette = [
|
||||||
|
'#F44336', '#E91E63', '#9C27B0', '#673AB7', '#3F51B5',
|
||||||
|
'#2196F3', '#03A9F4', '#00BCD4', '#009688', '#4CAF50',
|
||||||
|
'#8BC34A', '#CDDC39', '#FFEB3B', '#FFC107', '#FF9800',
|
||||||
|
'#FF5722', '#795548', '#607D8B', '#00ACC1', '#5C6BC0'
|
||||||
|
]
|
||||||
|
|
||||||
|
// Map subtype IDs to colors
|
||||||
|
const subtypeColorMap = computed(() => {
|
||||||
|
const colorMap = {}
|
||||||
|
const allSubtypes = currentSubtypes.value
|
||||||
|
allSubtypes.forEach((st, index) => {
|
||||||
|
colorMap[st.id] = subtypeColorPalette[index % subtypeColorPalette.length]
|
||||||
|
})
|
||||||
|
return colorMap
|
||||||
|
})
|
||||||
|
|
||||||
|
// Map subtype IDs to names
|
||||||
|
const subtypeNameMap = computed(() => {
|
||||||
|
const nameMap = {}
|
||||||
|
currentSubtypes.value.forEach(st => {
|
||||||
|
nameMap[st.id] = st.name
|
||||||
|
})
|
||||||
|
return nameMap
|
||||||
|
})
|
||||||
|
|
||||||
const filteredAssets = computed(() => {
|
const filteredAssets = computed(() => {
|
||||||
if (visibleTypes.value.length === 0) return assets.value
|
let result = assets.value
|
||||||
return assets.value.filter(a => visibleTypes.value.includes(a.assettype))
|
|
||||||
|
// Filter by asset type (case-insensitive)
|
||||||
|
if (selectedType.value) {
|
||||||
|
const selectedLower = selectedType.value.toLowerCase()
|
||||||
|
result = result.filter(a => a.assettype && a.assettype.toLowerCase() === selectedLower)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by subtype (case-insensitive type check)
|
||||||
|
if (selectedSubtype.value) {
|
||||||
|
const subtypeId = parseInt(selectedSubtype.value)
|
||||||
|
const typeLower = selectedType.value?.toLowerCase() || ''
|
||||||
|
result = result.filter(a => {
|
||||||
|
if (!a.typedata) return false
|
||||||
|
// Check different ID fields based on asset type
|
||||||
|
if (typeLower === 'equipment') {
|
||||||
|
return a.typedata.equipmenttypeid === subtypeId
|
||||||
|
} else if (typeLower === 'computer') {
|
||||||
|
return a.typedata.computertypeid === subtypeId
|
||||||
|
} else if (typeLower === 'network device') {
|
||||||
|
return a.typedata.networkdevicetypeid === subtypeId
|
||||||
|
} else if (typeLower === 'printer') {
|
||||||
|
return a.typedata.printertypeid === subtypeId
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by business unit
|
||||||
|
if (selectedBusinessUnit.value) {
|
||||||
|
result = result.filter(a => a.businessunitid === parseInt(selectedBusinessUnit.value))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by status
|
||||||
|
if (selectedStatus.value) {
|
||||||
|
result = result.filter(a => a.statusid === parseInt(selectedStatus.value))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by search query
|
||||||
|
if (searchQuery.value) {
|
||||||
|
const q = searchQuery.value.toLowerCase()
|
||||||
|
result = result.filter(a =>
|
||||||
|
(a.assetnumber && a.assetnumber.toLowerCase().includes(q)) ||
|
||||||
|
(a.name && a.name.toLowerCase().includes(q)) ||
|
||||||
|
(a.displayname && a.displayname.toLowerCase().includes(q)) ||
|
||||||
|
(a.serialnumber && a.serialnumber.toLowerCase().includes(q))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
@@ -69,9 +211,7 @@ onMounted(async () => {
|
|||||||
assetTypes.value = data.filters?.assettypes || []
|
assetTypes.value = data.filters?.assettypes || []
|
||||||
businessunits.value = data.filters?.businessunits || []
|
businessunits.value = data.filters?.businessunits || []
|
||||||
statuses.value = data.filters?.statuses || []
|
statuses.value = data.filters?.statuses || []
|
||||||
|
subtypes.value = data.filters?.subtypes || {}
|
||||||
// Default: show all types
|
|
||||||
visibleTypes.value = assetTypes.value.map(t => t.assettype)
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load map data:', error)
|
console.error('Failed to load map data:', error)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -79,44 +219,70 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
function getTypeIcon(assettype) {
|
function formatTypeName(assettype) {
|
||||||
const icons = {
|
if (!assettype) return assettype
|
||||||
'equipment': '⚙',
|
const names = {
|
||||||
'computer': '💻',
|
'equipment': 'Equipment',
|
||||||
'printer': '🖨',
|
'computer': 'Computers',
|
||||||
'network_device': '🌐'
|
'printer': 'Printers',
|
||||||
|
'network device': 'Network Devices',
|
||||||
|
'network_device': 'Network Devices'
|
||||||
}
|
}
|
||||||
return icons[assettype] || '📦'
|
return names[assettype.toLowerCase()] || assettype
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTypeCount(assettype) {
|
function getTypeCount(assettype) {
|
||||||
return assets.value.filter(a => a.assettype === assettype).length
|
if (!assettype) return 0
|
||||||
|
const lowerType = assettype.toLowerCase()
|
||||||
|
return assets.value.filter(a => a.assettype && a.assettype.toLowerCase() === lowerType).length
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTypeChange() {
|
||||||
|
// Reset subtype when type changes
|
||||||
|
selectedSubtype.value = ''
|
||||||
|
updateMapLayers()
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateMapLayers() {
|
function updateMapLayers() {
|
||||||
// Filter is reactive via computed property
|
// Filter is reactive via computed property
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function debouncedSearch() {
|
||||||
|
clearTimeout(searchTimeout)
|
||||||
|
searchTimeout = setTimeout(() => {
|
||||||
|
updateMapLayers()
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
|
||||||
function handleMarkerClick(asset) {
|
function handleMarkerClick(asset) {
|
||||||
// Route based on asset type
|
// Route based on asset type (lowercase keys to match API data)
|
||||||
|
const assetType = (asset.assettype || '').toLowerCase()
|
||||||
const routeMap = {
|
const routeMap = {
|
||||||
'equipment': '/machines',
|
'equipment': '/machines',
|
||||||
'computer': '/pcs',
|
'computer': '/pcs',
|
||||||
'printer': '/printers',
|
'printer': '/printers',
|
||||||
'network_device': '/network'
|
'network_device': '/network',
|
||||||
|
'network device': '/network'
|
||||||
}
|
}
|
||||||
|
|
||||||
const basePath = routeMap[asset.assettype] || '/machines'
|
const basePath = routeMap[assetType] || '/machines'
|
||||||
|
|
||||||
|
// Get the plugin-specific ID from typedata
|
||||||
|
let id = asset.assetid // fallback
|
||||||
|
if (asset.typedata) {
|
||||||
|
if (assetType === 'equipment' && asset.typedata.equipmentid) {
|
||||||
|
id = asset.typedata.equipmentid
|
||||||
|
} else if (assetType === 'computer' && asset.typedata.computerid) {
|
||||||
|
id = asset.typedata.computerid
|
||||||
|
} else if (assetType === 'printer' && asset.typedata.printerid) {
|
||||||
|
id = asset.typedata.printerid
|
||||||
|
} else if ((assetType === 'network_device' || assetType === 'network device') && asset.typedata.networkdeviceid) {
|
||||||
|
id = asset.typedata.networkdeviceid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// For network devices, use the networkdeviceid from typedata
|
|
||||||
if (asset.assettype === 'network_device' && asset.typedata?.networkdeviceid) {
|
|
||||||
router.push(`/network/${asset.typedata.networkdeviceid}`)
|
|
||||||
} else {
|
|
||||||
// For machines (equipment, computer, printer), use machineid from typedata
|
|
||||||
const id = asset.typedata?.machineid || asset.assetid
|
|
||||||
router.push(`${basePath}/${id}`)
|
router.push(`${basePath}/${id}`)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -130,43 +296,45 @@ function handleMarkerClick(asset) {
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.layer-toggles {
|
.map-filters {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1rem;
|
gap: 0.75rem;
|
||||||
padding: 0.75rem 1rem;
|
padding: 0.75rem 1rem;
|
||||||
background: var(--bg-card);
|
background: var(--bg-card);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
margin-bottom: 0.75rem;
|
margin-bottom: 0.75rem;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
|
||||||
|
|
||||||
.layer-toggle {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
}
|
||||||
cursor: pointer;
|
|
||||||
|
.map-filters select,
|
||||||
|
.map-filters input {
|
||||||
padding: 0.5rem 0.75rem;
|
padding: 0.5rem 0.75rem;
|
||||||
|
border: 1px solid var(--border);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
transition: background 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.layer-toggle:hover {
|
|
||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 0.875rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.layer-toggle input[type="checkbox"] {
|
.map-filters select {
|
||||||
width: 1.125rem;
|
min-width: 160px;
|
||||||
height: 1.125rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.layer-icon {
|
.map-filters select:disabled {
|
||||||
font-size: 1.25rem;
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.layer-count {
|
.map-filters input {
|
||||||
|
min-width: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-count {
|
||||||
color: var(--text-light);
|
color: var(--text-light);
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.map-page :deep(.shopfloor-map) {
|
.map-page :deep(.shopfloor-map) {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h2>Equipment Details</h2>
|
<h2>Equipment Details</h2>
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
<router-link :to="`/machines/${machine?.machineid}/edit`" class="btn btn-primary" v-if="machine">
|
<router-link :to="`/machines/${equipment?.equipment?.equipmentid}/edit`" class="btn btn-primary" v-if="equipment">
|
||||||
Edit
|
Edit
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link to="/machines" class="btn btn-secondary">Back to List</router-link>
|
<router-link to="/machines" class="btn btn-secondary">Back to List</router-link>
|
||||||
@@ -12,41 +12,38 @@
|
|||||||
|
|
||||||
<div v-if="loading" class="loading">Loading...</div>
|
<div v-if="loading" class="loading">Loading...</div>
|
||||||
|
|
||||||
<template v-else-if="machine">
|
<template v-else-if="equipment">
|
||||||
<!-- Hero Section -->
|
<!-- Hero Section -->
|
||||||
<div class="hero-card">
|
<div class="hero-card">
|
||||||
<div class="hero-image" v-if="machine.model?.imageurl">
|
|
||||||
<img :src="machine.model.imageurl" :alt="machine.model?.modelnumber" />
|
|
||||||
</div>
|
|
||||||
<div class="hero-content">
|
<div class="hero-content">
|
||||||
<div class="hero-title">
|
<div class="hero-title">
|
||||||
<h1>{{ machine.machinenumber }}</h1>
|
<h1>{{ equipment.assetnumber }}</h1>
|
||||||
<span v-if="machine.alias" class="hero-alias">{{ machine.alias }}</span>
|
<span v-if="equipment.name" class="hero-alias">{{ equipment.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="hero-meta">
|
<div class="hero-meta">
|
||||||
<span class="badge badge-lg" :class="getCategoryClass(machine.machinetype?.category)">
|
<span class="badge badge-lg badge-primary">
|
||||||
{{ machine.machinetype?.category || 'Unknown' }}
|
{{ equipment.assettype_name || 'Equipment' }}
|
||||||
</span>
|
</span>
|
||||||
<span class="badge badge-lg" :class="getStatusClass(machine.status?.status)">
|
<span class="badge badge-lg" :class="getStatusClass(equipment.status_name)">
|
||||||
{{ machine.status?.status || 'Unknown' }}
|
{{ equipment.status_name || 'Unknown' }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="hero-details">
|
<div class="hero-details">
|
||||||
<div class="hero-detail" v-if="machine.machinetype?.machinetype">
|
<div class="hero-detail" v-if="equipment.equipment?.equipmenttype_name">
|
||||||
<span class="hero-detail-label">Type</span>
|
<span class="hero-detail-label">Type</span>
|
||||||
<span class="hero-detail-value">{{ machine.machinetype.machinetype }}</span>
|
<span class="hero-detail-value">{{ equipment.equipment.equipmenttype_name }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="hero-detail" v-if="machine.vendor?.vendor">
|
<div class="hero-detail" v-if="equipment.equipment?.vendor_name">
|
||||||
<span class="hero-detail-label">Vendor</span>
|
<span class="hero-detail-label">Vendor</span>
|
||||||
<span class="hero-detail-value">{{ machine.vendor.vendor }}</span>
|
<span class="hero-detail-value">{{ equipment.equipment.vendor_name }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="hero-detail" v-if="machine.model?.modelnumber">
|
<div class="hero-detail" v-if="equipment.equipment?.model_name">
|
||||||
<span class="hero-detail-label">Model</span>
|
<span class="hero-detail-label">Model</span>
|
||||||
<span class="hero-detail-value">{{ machine.model.modelnumber }}</span>
|
<span class="hero-detail-value">{{ equipment.equipment.model_name }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="hero-detail" v-if="machine.location?.location">
|
<div class="hero-detail" v-if="equipment.location_name">
|
||||||
<span class="hero-detail-label">Location</span>
|
<span class="hero-detail-label">Location</span>
|
||||||
<span class="hero-detail-value">{{ machine.location.location }}</span>
|
<span class="hero-detail-value">{{ equipment.location_name }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -61,20 +58,16 @@
|
|||||||
<h3 class="section-title">Identity</h3>
|
<h3 class="section-title">Identity</h3>
|
||||||
<div class="info-list">
|
<div class="info-list">
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="info-label">Machine Number</span>
|
<span class="info-label">Asset Number</span>
|
||||||
<span class="info-value">{{ machine.machinenumber }}</span>
|
<span class="info-value">{{ equipment.assetnumber }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-row" v-if="machine.alias">
|
<div class="info-row" v-if="equipment.name">
|
||||||
<span class="info-label">Alias</span>
|
<span class="info-label">Name</span>
|
||||||
<span class="info-value">{{ machine.alias }}</span>
|
<span class="info-value">{{ equipment.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-row" v-if="machine.hostname">
|
<div class="info-row" v-if="equipment.serialnumber">
|
||||||
<span class="info-label">Hostname</span>
|
|
||||||
<span class="info-value mono">{{ machine.hostname }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-row" v-if="machine.serialnumber">
|
|
||||||
<span class="info-label">Serial Number</span>
|
<span class="info-label">Serial Number</span>
|
||||||
<span class="info-value mono">{{ machine.serialnumber }}</span>
|
<span class="info-value mono">{{ equipment.serialnumber }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -85,46 +78,72 @@
|
|||||||
<div class="info-list">
|
<div class="info-list">
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="info-label">Type</span>
|
<span class="info-label">Type</span>
|
||||||
<span class="info-value">{{ machine.machinetype?.machinetype || '-' }}</span>
|
<span class="info-value">{{ equipment.equipment?.equipmenttype_name || '-' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="info-label">Vendor</span>
|
<span class="info-label">Vendor</span>
|
||||||
<span class="info-value">{{ machine.vendor?.vendor || '-' }}</span>
|
<span class="info-value">{{ equipment.equipment?.vendor_name || '-' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="info-label">Model</span>
|
<span class="info-label">Model</span>
|
||||||
<span class="info-value">{{ machine.model?.modelnumber || '-' }}</span>
|
<span class="info-value">{{ equipment.equipment?.model_name || '-' }}</span>
|
||||||
</div>
|
|
||||||
<div class="info-row" v-if="machine.operatingsystem">
|
|
||||||
<span class="info-label">Operating System</span>
|
|
||||||
<span class="info-value">{{ machine.operatingsystem.osname }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- PC Information (if PC) -->
|
<!-- Controller Section (for CNC machines) -->
|
||||||
<div class="section-card" v-if="isPc">
|
<div class="section-card" v-if="equipment.equipment?.controller_vendor_name || equipment.equipment?.controller_model_name">
|
||||||
<h3 class="section-title">PC Status</h3>
|
<h3 class="section-title">Controller</h3>
|
||||||
<div class="info-list">
|
<div class="info-list">
|
||||||
<div class="info-row" v-if="machine.loggedinuser">
|
<div class="info-row">
|
||||||
<span class="info-label">Logged In User</span>
|
<span class="info-label">Vendor</span>
|
||||||
<span class="info-value">{{ machine.loggedinuser }}</span>
|
<span class="info-value">{{ equipment.equipment?.controller_vendor_name || '-' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="info-label">Last Reported</span>
|
<span class="info-label">Model</span>
|
||||||
<span class="info-value">{{ formatDate(machine.lastreporteddate) }}</span>
|
<span class="info-value">{{ equipment.equipment?.controller_model_name || '-' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-row">
|
|
||||||
<span class="info-label">Last Boot</span>
|
|
||||||
<span class="info-value">{{ formatDate(machine.lastboottime) }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Equipment Configuration -->
|
||||||
|
<div class="section-card">
|
||||||
|
<h3 class="section-title">Configuration</h3>
|
||||||
|
<div class="info-list">
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="info-label">Features</span>
|
<span class="info-label">Requires Manual Config</span>
|
||||||
<span class="info-value">
|
<span class="info-value">
|
||||||
<span class="feature-tag" :class="{ active: machine.isvnc }">VNC</span>
|
<span class="feature-tag" :class="{ active: equipment.equipment?.requiresmanualconfig }">
|
||||||
<span class="feature-tag" :class="{ active: machine.iswinrm }">WinRM</span>
|
{{ equipment.equipment?.requiresmanualconfig ? 'Yes' : 'No' }}
|
||||||
<span class="feature-tag" :class="{ active: machine.isshopfloor }">Shopfloor</span>
|
|
||||||
</span>
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="info-label">Location Only</span>
|
||||||
|
<span class="info-value">
|
||||||
|
<span class="feature-tag" :class="{ active: equipment.equipment?.islocationonly }">
|
||||||
|
{{ equipment.equipment?.islocationonly ? '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>
|
||||||
</div>
|
</div>
|
||||||
@@ -140,31 +159,31 @@
|
|||||||
<span class="info-label">Location</span>
|
<span class="info-label">Location</span>
|
||||||
<span class="info-value">
|
<span class="info-value">
|
||||||
<LocationMapTooltip
|
<LocationMapTooltip
|
||||||
v-if="machine.mapleft != null && machine.maptop != null"
|
v-if="equipment.mapleft != null && equipment.maptop != null"
|
||||||
:left="machine.mapleft"
|
:left="equipment.mapleft"
|
||||||
:top="machine.maptop"
|
:top="equipment.maptop"
|
||||||
:machineName="machine.machinenumber"
|
:machineName="equipment.assetnumber"
|
||||||
>
|
>
|
||||||
<span class="location-link">{{ machine.location?.location || 'On Map' }}</span>
|
<span class="location-link">{{ equipment.location_name || 'On Map' }}</span>
|
||||||
</LocationMapTooltip>
|
</LocationMapTooltip>
|
||||||
<span v-else>{{ machine.location?.location || '-' }}</span>
|
<span v-else>{{ equipment.location_name || '-' }}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="info-label">Business Unit</span>
|
<span class="info-label">Business Unit</span>
|
||||||
<span class="info-value">{{ machine.businessunit?.businessunit || '-' }}</span>
|
<span class="info-value">{{ equipment.businessunit_name || '-' }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Connected PC (for Equipment) -->
|
<!-- Connected PC -->
|
||||||
<div class="section-card" v-if="isEquipment">
|
<div class="section-card">
|
||||||
<h3 class="section-title">Connected PC</h3>
|
<h3 class="section-title">Connected PC</h3>
|
||||||
<div v-if="!controllingPc" class="empty-message">
|
<div v-if="!controllingPc" class="empty-message">
|
||||||
No controlling PC assigned
|
No controlling PC assigned
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="connected-device">
|
<div v-else class="connected-device">
|
||||||
<router-link :to="getRelatedRoute(controllingPc)" class="device-link">
|
<router-link :to="`/pcs/${controllingPc.plugin_id || controllingPc.assetid}`" class="device-link">
|
||||||
<div class="device-icon">
|
<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">
|
<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>
|
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect>
|
||||||
@@ -173,66 +192,31 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class="device-info">
|
<div class="device-info">
|
||||||
<span class="device-name">{{ controllingPc.relatedmachinenumber }}</span>
|
<span class="device-name">{{ controllingPc.assetnumber }}</span>
|
||||||
<span class="device-alias" v-if="controllingPc.relatedmachinealias">{{ controllingPc.relatedmachinealias }}</span>
|
<span class="device-alias" v-if="controllingPc.name">{{ controllingPc.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
</router-link>
|
</router-link>
|
||||||
<span class="connection-type">{{ controllingPc.relationshiptype }}</span>
|
<span class="connection-type">{{ controllingPc.relationshipType }}</span>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Controlled Equipment (for PCs) -->
|
|
||||||
<div class="section-card" v-if="isPc && controlledMachines.length > 0">
|
|
||||||
<h3 class="section-title">Controlled Equipment</h3>
|
|
||||||
<div class="equipment-list">
|
|
||||||
<router-link
|
|
||||||
v-for="rel in controlledMachines"
|
|
||||||
:key="rel.relationshipid"
|
|
||||||
:to="getRelatedRoute(rel)"
|
|
||||||
class="equipment-item"
|
|
||||||
>
|
|
||||||
<div class="equipment-info">
|
|
||||||
<span class="equipment-name">{{ rel.relatedmachinenumber }}</span>
|
|
||||||
<span class="equipment-alias" v-if="rel.relatedmachinealias">{{ rel.relatedmachinealias }}</span>
|
|
||||||
</div>
|
|
||||||
<span class="connection-tag">{{ rel.relationshiptype }}</span>
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Network -->
|
|
||||||
<div class="section-card" v-if="machine.communications?.length">
|
|
||||||
<h3 class="section-title">Network</h3>
|
|
||||||
<div class="network-list">
|
|
||||||
<div v-for="comm in machine.communications" :key="comm.communicationid" class="network-item">
|
|
||||||
<div class="network-primary">
|
|
||||||
<span class="ip-address">{{ comm.ipaddress || comm.address || '-' }}</span>
|
|
||||||
<span v-if="comm.isprimary" class="primary-badge">Primary</span>
|
|
||||||
</div>
|
|
||||||
<div class="network-secondary" v-if="comm.macaddress">
|
|
||||||
<span class="mac-address">{{ comm.macaddress }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Notes -->
|
<!-- Notes -->
|
||||||
<div class="section-card" v-if="machine.notes">
|
<div class="section-card" v-if="equipment.notes">
|
||||||
<h3 class="section-title">Notes</h3>
|
<h3 class="section-title">Notes</h3>
|
||||||
<p class="notes-text">{{ machine.notes }}</p>
|
<p class="notes-text">{{ equipment.notes }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Audit Footer -->
|
<!-- Audit Footer -->
|
||||||
<div class="audit-footer">
|
<div class="audit-footer">
|
||||||
<span>Created {{ formatDate(machine.createddate) }}<template v-if="machine.createdby"> by {{ machine.createdby }}</template></span>
|
<span>Created {{ formatDate(equipment.createddate) }}<template v-if="equipment.createdby"> by {{ equipment.createdby }}</template></span>
|
||||||
<span>Modified {{ formatDate(machine.modifieddate) }}<template v-if="machine.modifiedby"> by {{ machine.modifiedby }}</template></span>
|
<span>Modified {{ formatDate(equipment.modifieddate) }}<template v-if="equipment.modifiedby"> by {{ equipment.modifiedby }}</template></span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div v-else class="card">
|
<div v-else class="card">
|
||||||
<p style="text-align: center; color: var(--text-light);">Machine not found</p>
|
<p style="text-align: center; color: var(--text-light);">Equipment not found</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -240,55 +224,63 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { machinesApi } from '../../api'
|
import { equipmentApi, assetsApi } from '../../api'
|
||||||
import LocationMapTooltip from '../../components/LocationMapTooltip.vue'
|
import LocationMapTooltip from '../../components/LocationMapTooltip.vue'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const machine = ref(null)
|
const equipment = ref(null)
|
||||||
const relationships = ref([])
|
const relationships = ref({ incoming: [], outgoing: [] })
|
||||||
|
|
||||||
const isPc = computed(() => {
|
|
||||||
return machine.value?.machinetype?.category === 'PC'
|
|
||||||
})
|
|
||||||
|
|
||||||
const isEquipment = computed(() => {
|
|
||||||
return machine.value?.machinetype?.category === 'Equipment'
|
|
||||||
})
|
|
||||||
|
|
||||||
const controllingPc = computed(() => {
|
const controllingPc = computed(() => {
|
||||||
return relationships.value.find(r => r.direction === 'controlled_by' && r.relatedcategory === 'PC')
|
// For equipment, find a related computer in any "Controls" relationship
|
||||||
})
|
// Check both incoming (computer controls this) and outgoing (legacy data may have equipment -> computer)
|
||||||
|
|
||||||
const controlledMachines = computed(() => {
|
// First check incoming - computer as source controlling this equipment
|
||||||
return relationships.value.filter(r => r.direction === 'controls')
|
for (const rel of relationships.value.incoming || []) {
|
||||||
|
if (rel.source_asset?.assettype_name === 'computer' && rel.relationship_type_name === 'Controls') {
|
||||||
|
return {
|
||||||
|
...rel.source_asset,
|
||||||
|
relationshipType: rel.relationship_type_name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also check outgoing - legacy data may have equipment -> computer Controls relationships
|
||||||
|
for (const rel of relationships.value.outgoing || []) {
|
||||||
|
if (rel.target_asset?.assettype_name === 'computer' && rel.relationship_type_name === 'Controls') {
|
||||||
|
return {
|
||||||
|
...rel.target_asset,
|
||||||
|
relationshipType: rel.relationship_type_name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
const response = await machinesApi.get(route.params.id)
|
const response = await equipmentApi.get(route.params.id)
|
||||||
machine.value = response.data.data
|
equipment.value = response.data.data
|
||||||
|
|
||||||
const relResponse = await machinesApi.getRelationships(route.params.id)
|
// Load relationships using asset ID
|
||||||
relationships.value = relResponse.data.data || []
|
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) {
|
} catch (error) {
|
||||||
console.error('Error loading machine:', error)
|
console.error('Error loading equipment:', error)
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
function getCategoryClass(category) {
|
|
||||||
if (!category) return 'badge-info'
|
|
||||||
const c = category.toLowerCase()
|
|
||||||
if (c === 'equipment') return 'badge-primary'
|
|
||||||
if (c === 'pc') return 'badge-info'
|
|
||||||
if (c === 'network') return 'badge-warning'
|
|
||||||
if (c === 'printer') return 'badge-secondary'
|
|
||||||
return 'badge-info'
|
|
||||||
}
|
|
||||||
|
|
||||||
function getStatusClass(status) {
|
function getStatusClass(status) {
|
||||||
if (!status) return 'badge-info'
|
if (!status) return 'badge-info'
|
||||||
const s = status.toLowerCase()
|
const s = status.toLowerCase()
|
||||||
@@ -302,17 +294,31 @@ function formatDate(dateStr) {
|
|||||||
if (!dateStr) return '-'
|
if (!dateStr) return '-'
|
||||||
return new Date(dateStr).toLocaleString()
|
return new Date(dateStr).toLocaleString()
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRelatedRoute(rel) {
|
|
||||||
const category = rel.relatedcategory?.toLowerCase() || ''
|
|
||||||
const routeMap = {
|
|
||||||
'equipment': '/machines',
|
|
||||||
'pc': '/pcs',
|
|
||||||
'printer': '/printers'
|
|
||||||
}
|
|
||||||
const basePath = routeMap[category] || '/machines'
|
|
||||||
return `${basePath}/${rel.relatedmachineid}`
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Uses global detail page styles from style.css -->
|
<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>
|
||||||
|
|||||||
@@ -7,13 +7,15 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div v-if="loading" class="loading">Loading...</div>
|
<div v-if="loading" class="loading">Loading...</div>
|
||||||
|
|
||||||
<form v-else @submit.prevent="saveMachine">
|
<form v-else @submit.prevent="saveEquipment">
|
||||||
|
<!-- Identity Section -->
|
||||||
|
<h3 class="form-section-title">Identity</h3>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="machinenumber">Machine Number *</label>
|
<label for="assetnumber">Asset Number *</label>
|
||||||
<input
|
<input
|
||||||
id="machinenumber"
|
id="assetnumber"
|
||||||
v-model="form.machinenumber"
|
v-model="form.assetnumber"
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
required
|
required
|
||||||
@@ -21,10 +23,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="alias">Alias</label>
|
<label for="name">Name / Alias</label>
|
||||||
<input
|
<input
|
||||||
id="alias"
|
id="name"
|
||||||
v-model="form.alias"
|
v-model="form.name"
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
/>
|
/>
|
||||||
@@ -32,16 +34,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group">
|
|
||||||
<label for="hostname">Hostname</label>
|
|
||||||
<input
|
|
||||||
id="hostname"
|
|
||||||
v-model="form.hostname"
|
|
||||||
type="text"
|
|
||||||
class="form-control"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="serialnumber">Serial Number</label>
|
<label for="serialnumber">Serial Number</label>
|
||||||
<input
|
<input
|
||||||
@@ -51,27 +43,6 @@
|
|||||||
class="form-control"
|
class="form-control"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-row">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="machinetypeid">Equipment Type *</label>
|
|
||||||
<select
|
|
||||||
id="machinetypeid"
|
|
||||||
v-model="form.machinetypeid"
|
|
||||||
class="form-control"
|
|
||||||
required
|
|
||||||
>
|
|
||||||
<option value="">Select type...</option>
|
|
||||||
<option
|
|
||||||
v-for="mt in machineTypes"
|
|
||||||
:key="mt.machinetypeid"
|
|
||||||
:value="mt.machinetypeid"
|
|
||||||
>
|
|
||||||
{{ mt.machinetype }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="statusid">Status</label>
|
<label for="statusid">Status</label>
|
||||||
@@ -92,7 +63,27 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Equipment Hardware Section -->
|
||||||
|
<h3 class="form-section-title">Equipment Hardware</h3>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="equipmenttypeid">Equipment Type</label>
|
||||||
|
<select
|
||||||
|
id="equipmenttypeid"
|
||||||
|
v-model="form.equipmenttypeid"
|
||||||
|
class="form-control"
|
||||||
|
>
|
||||||
|
<option value="">Select type...</option>
|
||||||
|
<option
|
||||||
|
v-for="et in equipmentTypes"
|
||||||
|
:key="et.equipmenttypeid"
|
||||||
|
:value="et.equipmenttypeid"
|
||||||
|
>
|
||||||
|
{{ et.equipmenttype }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="vendorid">Vendor</label>
|
<label for="vendorid">Vendor</label>
|
||||||
<select
|
<select
|
||||||
@@ -110,7 +101,9 @@
|
|||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="modelnumberid">Model</label>
|
<label for="modelnumberid">Model</label>
|
||||||
<select
|
<select
|
||||||
@@ -127,10 +120,56 @@
|
|||||||
{{ m.modelnumber }}
|
{{ m.modelnumber }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<small class="form-help">Selecting a model will auto-set the equipment type</small>
|
<small class="form-help" v-if="form.vendorid">Filtered by selected vendor</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Controller Section (for CNC machines) -->
|
||||||
|
<h3 class="form-section-title">Controller</h3>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="controller_vendorid">Controller Vendor</label>
|
||||||
|
<select
|
||||||
|
id="controller_vendorid"
|
||||||
|
v-model="form.controller_vendorid"
|
||||||
|
class="form-control"
|
||||||
|
>
|
||||||
|
<option value="">Select vendor...</option>
|
||||||
|
<option
|
||||||
|
v-for="v in vendors"
|
||||||
|
:key="v.vendorid"
|
||||||
|
:value="v.vendorid"
|
||||||
|
>
|
||||||
|
{{ v.vendor }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<small class="form-help">e.g., Fanuc, Siemens, Allen-Bradley</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="controller_modelid">Controller Model</label>
|
||||||
|
<select
|
||||||
|
id="controller_modelid"
|
||||||
|
v-model="form.controller_modelid"
|
||||||
|
class="form-control"
|
||||||
|
>
|
||||||
|
<option value="">Select model...</option>
|
||||||
|
<option
|
||||||
|
v-for="m in filteredControllerModels"
|
||||||
|
:key="m.modelnumberid"
|
||||||
|
:value="m.modelnumberid"
|
||||||
|
>
|
||||||
|
{{ m.modelnumber }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<small class="form-help" v-if="form.controller_vendorid">Filtered by controller vendor</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Location Section -->
|
||||||
|
<h3 class="form-section-title">Location & Organization</h3>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="locationid">Location</label>
|
<label for="locationid">Location</label>
|
||||||
@@ -145,7 +184,7 @@
|
|||||||
:key="l.locationid"
|
:key="l.locationid"
|
||||||
:value="l.locationid"
|
:value="l.locationid"
|
||||||
>
|
>
|
||||||
{{ l.location }}
|
{{ l.locationname }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -169,56 +208,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="notes">Notes</label>
|
|
||||||
<textarea
|
|
||||||
id="notes"
|
|
||||||
v-model="form.notes"
|
|
||||||
class="form-control"
|
|
||||||
rows="3"
|
|
||||||
></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Controlling PC Selection -->
|
|
||||||
<div class="form-row">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="controllingpc">Controlling PC</label>
|
|
||||||
<select
|
|
||||||
id="controllingpc"
|
|
||||||
v-model="controllingPcId"
|
|
||||||
class="form-control"
|
|
||||||
>
|
|
||||||
<option :value="null">None (standalone)</option>
|
|
||||||
<option
|
|
||||||
v-for="pc in pcs"
|
|
||||||
:key="pc.machineid"
|
|
||||||
:value="pc.machineid"
|
|
||||||
>
|
|
||||||
{{ pc.machinenumber }}{{ pc.alias ? ` (${pc.alias})` : '' }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
<small class="form-help">Select the PC that controls this equipment</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group" v-if="controllingPcId">
|
|
||||||
<label for="connectiontype">Connection Type</label>
|
|
||||||
<select
|
|
||||||
id="connectiontype"
|
|
||||||
v-model="relationshipTypeId"
|
|
||||||
class="form-control"
|
|
||||||
>
|
|
||||||
<option
|
|
||||||
v-for="rt in relationshipTypes"
|
|
||||||
:key="rt.relationshiptypeid"
|
|
||||||
:value="rt.relationshiptypeid"
|
|
||||||
>
|
|
||||||
{{ rt.relationshiptype }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
<small class="form-help">How the PC connects to this equipment</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Map Location Picker -->
|
<!-- Map Location Picker -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Map Location</label>
|
<label>Map Location</label>
|
||||||
@@ -249,6 +238,79 @@
|
|||||||
</template>
|
</template>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
<!-- Configuration Section -->
|
||||||
|
<h3 class="form-section-title">Configuration</h3>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group checkbox-group">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" v-model="form.requiresmanualconfig" />
|
||||||
|
Requires Manual Config
|
||||||
|
</label>
|
||||||
|
<small class="form-help">Multi-PC machine needs manual configuration</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group checkbox-group">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" v-model="form.islocationonly" />
|
||||||
|
Location Only
|
||||||
|
</label>
|
||||||
|
<small class="form-help">Virtual location marker (not actual equipment)</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Controlling PC Selection -->
|
||||||
|
<h3 class="form-section-title">Connected PC</h3>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="controllingpc">Controlling PC</label>
|
||||||
|
<select
|
||||||
|
id="controllingpc"
|
||||||
|
v-model="controllingPcId"
|
||||||
|
class="form-control"
|
||||||
|
>
|
||||||
|
<option :value="null">None (standalone)</option>
|
||||||
|
<option
|
||||||
|
v-for="pc in pcs"
|
||||||
|
:key="pc.assetid"
|
||||||
|
:value="pc.assetid"
|
||||||
|
>
|
||||||
|
{{ pc.assetnumber }}{{ pc.name ? ` (${pc.name})` : '' }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<small class="form-help">Select the PC that controls this equipment</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" v-if="controllingPcId">
|
||||||
|
<label for="connectiontype">Connection Type</label>
|
||||||
|
<select
|
||||||
|
id="connectiontype"
|
||||||
|
v-model="relationshipTypeId"
|
||||||
|
class="form-control"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="rt in relationshipTypes"
|
||||||
|
:key="rt.relationshiptypeid"
|
||||||
|
:value="rt.relationshiptypeid"
|
||||||
|
>
|
||||||
|
{{ rt.relationshiptype }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<small class="form-help">How the PC connects to this equipment</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Notes Section -->
|
||||||
|
<h3 class="form-section-title">Notes</h3>
|
||||||
|
<div class="form-group">
|
||||||
|
<textarea
|
||||||
|
id="notes"
|
||||||
|
v-model="form.notes"
|
||||||
|
class="form-control"
|
||||||
|
rows="3"
|
||||||
|
placeholder="Additional notes..."
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="error" class="error-message">{{ error }}</div>
|
<div v-if="error" class="error-message">{{ error }}</div>
|
||||||
|
|
||||||
<div style="display: flex; gap: 0.5rem; margin-top: 1.5rem;">
|
<div style="display: flex; gap: 0.5rem; margin-top: 1.5rem;">
|
||||||
@@ -265,7 +327,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, computed, watch } from 'vue'
|
import { ref, onMounted, computed, watch } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { machinesApi, machinetypesApi, statusesApi, vendorsApi, locationsApi, relationshipTypesApi, modelsApi, businessunitsApi } from '../../api'
|
import { equipmentApi, vendorsApi, locationsApi, modelsApi, businessunitsApi, computersApi, assetsApi } from '../../api'
|
||||||
import ShopFloorMap from '../../components/ShopFloorMap.vue'
|
import ShopFloorMap from '../../components/ShopFloorMap.vue'
|
||||||
import Modal from '../../components/Modal.vue'
|
import Modal from '../../components/Modal.vue'
|
||||||
import { currentTheme } from '../../stores/theme'
|
import { currentTheme } from '../../stores/theme'
|
||||||
@@ -282,22 +344,25 @@ const showMapPicker = ref(false)
|
|||||||
const tempMapPosition = ref(null)
|
const tempMapPosition = ref(null)
|
||||||
|
|
||||||
const form = ref({
|
const form = ref({
|
||||||
machinenumber: '',
|
assetnumber: '',
|
||||||
alias: '',
|
name: '',
|
||||||
hostname: '',
|
|
||||||
serialnumber: '',
|
serialnumber: '',
|
||||||
machinetypeid: '',
|
statusid: 1,
|
||||||
modelnumberid: '',
|
equipmenttypeid: '',
|
||||||
statusid: '',
|
|
||||||
vendorid: '',
|
vendorid: '',
|
||||||
|
modelnumberid: '',
|
||||||
|
controller_vendorid: '',
|
||||||
|
controller_modelid: '',
|
||||||
locationid: '',
|
locationid: '',
|
||||||
businessunitid: '',
|
businessunitid: '',
|
||||||
|
requiresmanualconfig: false,
|
||||||
|
islocationonly: false,
|
||||||
notes: '',
|
notes: '',
|
||||||
mapleft: null,
|
mapleft: null,
|
||||||
maptop: null
|
maptop: null
|
||||||
})
|
})
|
||||||
|
|
||||||
const machineTypes = ref([])
|
const equipmentTypes = ref([])
|
||||||
const statuses = ref([])
|
const statuses = ref([])
|
||||||
const vendors = ref([])
|
const vendors = ref([])
|
||||||
const locations = ref([])
|
const locations = ref([])
|
||||||
@@ -308,55 +373,73 @@ const relationshipTypes = ref([])
|
|||||||
const controllingPcId = ref(null)
|
const controllingPcId = ref(null)
|
||||||
const relationshipTypeId = ref(null)
|
const relationshipTypeId = ref(null)
|
||||||
const existingRelationshipId = ref(null)
|
const existingRelationshipId = ref(null)
|
||||||
|
const currentEquipment = ref(null)
|
||||||
|
|
||||||
// Filter models by selected vendor
|
// Filter models by selected equipment vendor
|
||||||
const filteredModels = computed(() => {
|
const filteredModels = computed(() => {
|
||||||
return models.value.filter(m => {
|
if (!form.value.vendorid) return models.value
|
||||||
// Filter by vendor if selected
|
return models.value.filter(m => m.vendorid === form.value.vendorid)
|
||||||
if (form.value.vendorid && m.vendorid !== form.value.vendorid) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
// Only show models for Equipment category types
|
|
||||||
const modelType = machineTypes.value.find(t => t.machinetypeid === m.machinetypeid)
|
|
||||||
if (modelType && modelType.category !== 'Equipment') {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// When model changes, auto-set the machine type
|
// Filter models by selected controller vendor
|
||||||
watch(() => form.value.modelnumberid, (newModelId) => {
|
const filteredControllerModels = computed(() => {
|
||||||
if (newModelId) {
|
if (!form.value.controller_vendorid) return models.value
|
||||||
const selectedModel = models.value.find(m => m.modelnumberid === newModelId)
|
return models.value.filter(m => m.vendorid === form.value.controller_vendorid)
|
||||||
if (selectedModel && selectedModel.machinetypeid) {
|
})
|
||||||
form.value.machinetypeid = selectedModel.machinetypeid
|
|
||||||
|
// Clear model selection when vendor changes
|
||||||
|
watch(() => form.value.vendorid, (newVal, oldVal) => {
|
||||||
|
if (oldVal && newVal !== oldVal) {
|
||||||
|
const currentModel = models.value.find(m => m.modelnumberid === form.value.modelnumberid)
|
||||||
|
if (currentModel && currentModel.vendorid !== newVal) {
|
||||||
|
form.value.modelnumberid = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Clear controller model when controller vendor changes
|
||||||
|
watch(() => form.value.controller_vendorid, (newVal, oldVal) => {
|
||||||
|
if (oldVal && newVal !== oldVal) {
|
||||||
|
const currentModel = models.value.find(m => m.modelnumberid === form.value.controller_modelid)
|
||||||
|
if (currentModel && currentModel.vendorid !== newVal) {
|
||||||
|
form.value.controller_modelid = ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
// Load reference data
|
// Load reference data in parallel
|
||||||
const [mtRes, statusRes, vendorRes, locRes, modelsRes, buRes, pcsRes, relTypesRes] = await Promise.all([
|
const [typesRes, statusRes, vendorRes, locRes, modelsRes, buRes, pcsRes, relTypesRes] = await Promise.all([
|
||||||
machinetypesApi.list({ category: 'Equipment' }),
|
equipmentApi.types.list(),
|
||||||
statusesApi.list(),
|
assetsApi.statuses.list(),
|
||||||
vendorsApi.list(),
|
vendorsApi.list({ per_page: 500 }),
|
||||||
locationsApi.list(),
|
locationsApi.list({ per_page: 500 }),
|
||||||
modelsApi.list(),
|
modelsApi.list({ per_page: 1000 }),
|
||||||
businessunitsApi.list(),
|
businessunitsApi.list({ per_page: 500 }),
|
||||||
machinesApi.list({ category: 'PC', perpage: 500 }),
|
computersApi.list({ per_page: 500 }),
|
||||||
relationshipTypesApi.list()
|
assetsApi.types.list() // Used for relationship types, will fix below
|
||||||
])
|
])
|
||||||
|
|
||||||
machineTypes.value = mtRes.data.data || []
|
equipmentTypes.value = typesRes.data.data || []
|
||||||
statuses.value = statusRes.data.data || []
|
statuses.value = statusRes.data.data || []
|
||||||
vendors.value = vendorRes.data.data || []
|
vendors.value = vendorRes.data.data || []
|
||||||
locations.value = locRes.data.data || []
|
locations.value = locRes.data.data || []
|
||||||
models.value = modelsRes.data.data || []
|
models.value = modelsRes.data.data || []
|
||||||
businessunits.value = buRes.data.data || []
|
businessunits.value = buRes.data.data || []
|
||||||
pcs.value = pcsRes.data.data || []
|
pcs.value = pcsRes.data.data || []
|
||||||
relationshipTypes.value = relTypesRes.data.data || []
|
|
||||||
|
// Load relationship types separately
|
||||||
|
try {
|
||||||
|
const relRes = await fetch('/api/assets/relationshiptypes')
|
||||||
|
if (relRes.ok) {
|
||||||
|
const relData = await relRes.json()
|
||||||
|
relationshipTypes.value = relData.data || []
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Fallback - use hardcoded Controls type
|
||||||
|
relationshipTypes.value = [{ relationshiptypeid: 1, relationshiptype: 'Controls' }]
|
||||||
|
}
|
||||||
|
|
||||||
// Set default relationship type to "Controls" if available
|
// Set default relationship type to "Controls" if available
|
||||||
const controlsType = relationshipTypes.value.find(t => t.relationshiptype === 'Controls')
|
const controlsType = relationshipTypes.value.find(t => t.relationshiptype === 'Controls')
|
||||||
@@ -364,34 +447,49 @@ onMounted(async () => {
|
|||||||
relationshipTypeId.value = controlsType.relationshiptypeid
|
relationshipTypeId.value = controlsType.relationshiptypeid
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load machine if editing
|
// Load equipment if editing
|
||||||
if (isEdit.value) {
|
if (isEdit.value) {
|
||||||
const response = await machinesApi.get(route.params.id)
|
const response = await equipmentApi.get(route.params.id)
|
||||||
const machine = response.data.data
|
const data = response.data.data
|
||||||
|
currentEquipment.value = data
|
||||||
|
|
||||||
form.value = {
|
form.value = {
|
||||||
machinenumber: machine.machinenumber || '',
|
assetnumber: data.assetnumber || '',
|
||||||
alias: machine.alias || '',
|
name: data.name || '',
|
||||||
hostname: machine.hostname || '',
|
serialnumber: data.serialnumber || '',
|
||||||
serialnumber: machine.serialnumber || '',
|
statusid: data.statusid || 1,
|
||||||
machinetypeid: machine.machinetype?.machinetypeid || '',
|
equipmenttypeid: data.equipment?.equipmenttypeid || '',
|
||||||
modelnumberid: machine.model?.modelnumberid || '',
|
vendorid: data.equipment?.vendorid || '',
|
||||||
statusid: machine.status?.statusid || '',
|
modelnumberid: data.equipment?.modelnumberid || '',
|
||||||
vendorid: machine.vendor?.vendorid || '',
|
controller_vendorid: data.equipment?.controller_vendorid || '',
|
||||||
locationid: machine.location?.locationid || '',
|
controller_modelid: data.equipment?.controller_modelid || '',
|
||||||
businessunitid: machine.businessunit?.businessunitid || '',
|
locationid: data.locationid || '',
|
||||||
notes: machine.notes || '',
|
businessunitid: data.businessunitid || '',
|
||||||
mapleft: machine.mapleft ?? null,
|
requiresmanualconfig: data.equipment?.requiresmanualconfig || false,
|
||||||
maptop: machine.maptop ?? null
|
islocationonly: data.equipment?.islocationonly || false,
|
||||||
|
notes: data.notes || '',
|
||||||
|
mapleft: data.mapleft ?? null,
|
||||||
|
maptop: data.maptop ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load existing relationship (controlling PC)
|
// Load existing relationships to find controlling PC
|
||||||
const relResponse = await machinesApi.getRelationships(route.params.id)
|
if (data.assetid) {
|
||||||
const relationships = relResponse.data.data || []
|
try {
|
||||||
const pcRelationship = relationships.find(r => r.direction === 'controlled_by' && r.relatedcategory === 'PC')
|
const relResponse = await assetsApi.getRelationships(data.assetid)
|
||||||
if (pcRelationship) {
|
const relationships = relResponse.data.data || { incoming: [], outgoing: [] }
|
||||||
controllingPcId.value = pcRelationship.relatedmachineid
|
|
||||||
relationshipTypeId.value = pcRelationship.relationshiptypeid
|
// Check incoming relationships for a controlling PC
|
||||||
existingRelationshipId.value = pcRelationship.relationshipid
|
for (const rel of relationships.incoming || []) {
|
||||||
|
if (rel.source_asset?.assettype_name === 'computer' && rel.relationship_type_name === 'Controls') {
|
||||||
|
controllingPcId.value = rel.source_asset.assetid
|
||||||
|
relationshipTypeId.value = rel.relationshiptypeid
|
||||||
|
existingRelationshipId.value = rel.assetrelationshipid
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log('Could not load relationships')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -420,45 +518,56 @@ function clearMapPosition() {
|
|||||||
tempMapPosition.value = null
|
tempMapPosition.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveMachine() {
|
async function saveEquipment() {
|
||||||
error.value = ''
|
error.value = ''
|
||||||
saving.value = true
|
saving.value = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = {
|
const data = {
|
||||||
...form.value,
|
assetnumber: form.value.assetnumber,
|
||||||
machinetypeid: form.value.machinetypeid || null,
|
name: form.value.name || null,
|
||||||
modelnumberid: form.value.modelnumberid || null,
|
serialnumber: form.value.serialnumber || null,
|
||||||
statusid: form.value.statusid || null,
|
statusid: form.value.statusid || 1,
|
||||||
|
equipmenttypeid: form.value.equipmenttypeid || null,
|
||||||
vendorid: form.value.vendorid || null,
|
vendorid: form.value.vendorid || null,
|
||||||
|
modelnumberid: form.value.modelnumberid || null,
|
||||||
|
controller_vendorid: form.value.controller_vendorid || null,
|
||||||
|
controller_modelid: form.value.controller_modelid || null,
|
||||||
locationid: form.value.locationid || null,
|
locationid: form.value.locationid || null,
|
||||||
businessunitid: form.value.businessunitid || null,
|
businessunitid: form.value.businessunitid || null,
|
||||||
|
requiresmanualconfig: form.value.requiresmanualconfig,
|
||||||
|
islocationonly: form.value.islocationonly,
|
||||||
|
notes: form.value.notes || null,
|
||||||
mapleft: form.value.mapleft,
|
mapleft: form.value.mapleft,
|
||||||
maptop: form.value.maptop
|
maptop: form.value.maptop
|
||||||
}
|
}
|
||||||
|
|
||||||
let machineId = route.params.id
|
let savedEquipment
|
||||||
|
let assetId
|
||||||
|
|
||||||
if (isEdit.value) {
|
if (isEdit.value) {
|
||||||
await machinesApi.update(route.params.id, data)
|
const response = await equipmentApi.update(route.params.id, data)
|
||||||
|
savedEquipment = response.data.data
|
||||||
|
assetId = savedEquipment.assetid
|
||||||
} else {
|
} else {
|
||||||
const response = await machinesApi.create(data)
|
const response = await equipmentApi.create(data)
|
||||||
machineId = response.data.data.machineid
|
savedEquipment = response.data.data
|
||||||
|
assetId = savedEquipment.assetid
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle relationship (controlling PC)
|
// Handle relationship (controlling PC)
|
||||||
await saveRelationship(machineId)
|
await saveRelationship(assetId)
|
||||||
|
|
||||||
router.push('/machines')
|
router.push(`/machines/${savedEquipment.equipment?.equipmentid || route.params.id}`)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error saving machine:', err)
|
console.error('Error saving equipment:', err)
|
||||||
error.value = err.response?.data?.message || 'Failed to save machine'
|
error.value = err.response?.data?.message || 'Failed to save equipment'
|
||||||
} finally {
|
} finally {
|
||||||
saving.value = false
|
saving.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveRelationship(machineId) {
|
async function saveRelationship(assetId) {
|
||||||
// If no PC selected and no existing relationship, nothing to do
|
// If no PC selected and no existing relationship, nothing to do
|
||||||
if (!controllingPcId.value && !existingRelationshipId.value) {
|
if (!controllingPcId.value && !existingRelationshipId.value) {
|
||||||
return
|
return
|
||||||
@@ -466,7 +575,7 @@ async function saveRelationship(machineId) {
|
|||||||
|
|
||||||
// If clearing the relationship
|
// If clearing the relationship
|
||||||
if (!controllingPcId.value && existingRelationshipId.value) {
|
if (!controllingPcId.value && existingRelationshipId.value) {
|
||||||
await machinesApi.deleteRelationship(existingRelationshipId.value)
|
await assetsApi.deleteRelationship(existingRelationshipId.value)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -474,20 +583,50 @@ async function saveRelationship(machineId) {
|
|||||||
if (controllingPcId.value) {
|
if (controllingPcId.value) {
|
||||||
// If there's an existing relationship, delete it first
|
// If there's an existing relationship, delete it first
|
||||||
if (existingRelationshipId.value) {
|
if (existingRelationshipId.value) {
|
||||||
await machinesApi.deleteRelationship(existingRelationshipId.value)
|
await assetsApi.deleteRelationship(existingRelationshipId.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new relationship
|
// Create new relationship (PC controls Equipment, so PC is source, Equipment is target)
|
||||||
await machinesApi.createRelationship(machineId, {
|
await assetsApi.createRelationship({
|
||||||
relatedmachineid: controllingPcId.value,
|
source_assetid: controllingPcId.value,
|
||||||
relationshiptypeid: relationshipTypeId.value,
|
target_assetid: assetId,
|
||||||
direction: 'controlled_by'
|
relationshiptypeid: relationshipTypeId.value
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.form-section-title {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text);
|
||||||
|
margin: 1.5rem 0 0.75rem 0;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-section-title:first-of-type {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-group label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-group input[type="checkbox"] {
|
||||||
|
width: 1.1rem;
|
||||||
|
height: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.map-location-control {
|
.map-location-control {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -499,7 +638,7 @@ async function saveRelationship(machineId) {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
background: #e3f2fd;
|
background: var(--bg);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,9 +24,9 @@
|
|||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Machine #</th>
|
<th>Asset #</th>
|
||||||
<th>Alias</th>
|
<th>Name</th>
|
||||||
<th>Hostname</th>
|
<th>Serial Number</th>
|
||||||
<th>Type</th>
|
<th>Type</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th>Location</th>
|
<th>Location</th>
|
||||||
@@ -34,27 +34,27 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="machine in machines" :key="machine.machineid">
|
<tr v-for="item in equipment" :key="item.assetid">
|
||||||
<td>{{ machine.machinenumber }}</td>
|
<td>{{ item.assetnumber }}</td>
|
||||||
<td>{{ machine.alias || '-' }}</td>
|
<td>{{ item.name || '-' }}</td>
|
||||||
<td>{{ machine.hostname || '-' }}</td>
|
<td class="mono">{{ item.serialnumber || '-' }}</td>
|
||||||
<td>{{ machine.machinetype }}</td>
|
<td>{{ item.equipment?.equipmenttype_name || '-' }}</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="badge" :class="getStatusClass(machine.status)">
|
<span class="badge" :class="getStatusClass(item.status_name)">
|
||||||
{{ machine.status || 'Unknown' }}
|
{{ item.status_name || 'Unknown' }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ machine.location || '-' }}</td>
|
<td>{{ item.location_name || '-' }}</td>
|
||||||
<td class="actions">
|
<td class="actions">
|
||||||
<router-link
|
<router-link
|
||||||
:to="`/machines/${machine.machineid}`"
|
:to="`/machines/${item.equipment?.equipmentid || item.assetid}`"
|
||||||
class="btn btn-secondary btn-sm"
|
class="btn btn-secondary btn-sm"
|
||||||
>
|
>
|
||||||
View
|
View
|
||||||
</router-link>
|
</router-link>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-if="machines.length === 0">
|
<tr v-if="equipment.length === 0">
|
||||||
<td colspan="7" style="text-align: center; color: var(--text-light);">
|
<td colspan="7" style="text-align: center; color: var(--text-light);">
|
||||||
No equipment found
|
No equipment found
|
||||||
</td>
|
</td>
|
||||||
@@ -82,9 +82,9 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { machinesApi } from '../../api'
|
import { equipmentApi } from '../../api'
|
||||||
|
|
||||||
const machines = ref([])
|
const equipment = ref([])
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const search = ref('')
|
const search = ref('')
|
||||||
const page = ref(1)
|
const page = ref(1)
|
||||||
@@ -93,21 +93,20 @@ const totalPages = ref(1)
|
|||||||
let searchTimeout = null
|
let searchTimeout = null
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadMachines()
|
loadEquipment()
|
||||||
})
|
})
|
||||||
|
|
||||||
async function loadMachines() {
|
async function loadEquipment() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const params = {
|
const params = {
|
||||||
page: page.value,
|
page: page.value,
|
||||||
perpage: 20,
|
per_page: 20
|
||||||
category: 'Equipment'
|
|
||||||
}
|
}
|
||||||
if (search.value) params.search = search.value
|
if (search.value) params.search = search.value
|
||||||
|
|
||||||
const response = await machinesApi.list(params)
|
const response = await equipmentApi.list(params)
|
||||||
machines.value = response.data.data || []
|
equipment.value = response.data.data || []
|
||||||
totalPages.value = response.data.meta?.pagination?.total_pages || 1
|
totalPages.value = response.data.meta?.pagination?.total_pages || 1
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading equipment:', error)
|
console.error('Error loading equipment:', error)
|
||||||
@@ -120,13 +119,13 @@ function debouncedSearch() {
|
|||||||
clearTimeout(searchTimeout)
|
clearTimeout(searchTimeout)
|
||||||
searchTimeout = setTimeout(() => {
|
searchTimeout = setTimeout(() => {
|
||||||
page.value = 1
|
page.value = 1
|
||||||
loadMachines()
|
loadEquipment()
|
||||||
}, 300)
|
}, 300)
|
||||||
}
|
}
|
||||||
|
|
||||||
function goToPage(p) {
|
function goToPage(p) {
|
||||||
page.value = p
|
page.value = p
|
||||||
loadMachines()
|
loadEquipment()
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStatusClass(status) {
|
function getStatusClass(status) {
|
||||||
@@ -138,3 +137,9 @@ function getStatusClass(status) {
|
|||||||
return 'badge-info'
|
return 'badge-info'
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.mono {
|
||||||
|
font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="detail-page">
|
<div class="detail-page">
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h2>PC Details</h2>
|
<h2>Computer Details</h2>
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
<router-link :to="`/pcs/${$route.params.id}/edit`" class="btn btn-primary">Edit</router-link>
|
<router-link :to="`/pcs/${$route.params.id}/edit`" class="btn btn-primary">Edit</router-link>
|
||||||
<router-link to="/pcs" class="btn btn-secondary">Back to List</router-link>
|
<router-link to="/pcs" class="btn btn-secondary">Back to List</router-link>
|
||||||
@@ -10,39 +10,32 @@
|
|||||||
|
|
||||||
<div v-if="loading" class="loading">Loading...</div>
|
<div v-if="loading" class="loading">Loading...</div>
|
||||||
|
|
||||||
<template v-else-if="pc">
|
<template v-else-if="computer">
|
||||||
<!-- Hero Section -->
|
<!-- Hero Section -->
|
||||||
<div class="hero-card">
|
<div class="hero-card">
|
||||||
<div class="hero-image" v-if="pc.model?.imageurl">
|
|
||||||
<img :src="pc.model.imageurl" :alt="pc.model?.modelnumber" />
|
|
||||||
</div>
|
|
||||||
<div class="hero-content">
|
<div class="hero-content">
|
||||||
<div class="hero-title">
|
<div class="hero-title">
|
||||||
<h1>{{ pc.machinenumber }}</h1>
|
<h1>{{ computer.assetnumber }}</h1>
|
||||||
<span v-if="pc.alias" class="hero-alias">{{ pc.alias }}</span>
|
<span v-if="computer.computer?.hostname" class="hero-alias">{{ computer.computer.hostname }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="hero-meta">
|
<div class="hero-meta">
|
||||||
<span class="badge badge-lg badge-info">PC</span>
|
<span class="badge badge-lg badge-info">Computer</span>
|
||||||
<span class="badge badge-lg" :class="getStatusClass(pc.status?.status)">
|
<span class="badge badge-lg" :class="getStatusClass(computer.status_name)">
|
||||||
{{ pc.status?.status || 'Unknown' }}
|
{{ computer.status_name || 'Unknown' }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="hero-details">
|
<div class="hero-details">
|
||||||
<div class="hero-detail" v-if="pc.pctype">
|
<div class="hero-detail" v-if="computer.computer?.computertype_name">
|
||||||
<span class="hero-detail-label">PC Type</span>
|
<span class="hero-detail-label">Type</span>
|
||||||
<span class="hero-detail-value">{{ pc.pctype.pctype || pc.pctype }}</span>
|
<span class="hero-detail-value">{{ computer.computer.computertype_name }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="hero-detail" v-if="pc.vendor?.vendor">
|
<div class="hero-detail" v-if="computer.computer?.os_name">
|
||||||
<span class="hero-detail-label">Vendor</span>
|
<span class="hero-detail-label">OS</span>
|
||||||
<span class="hero-detail-value">{{ pc.vendor.vendor }}</span>
|
<span class="hero-detail-value">{{ computer.computer.os_name }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="hero-detail" v-if="pc.model?.modelnumber">
|
<div class="hero-detail" v-if="computer.location_name">
|
||||||
<span class="hero-detail-label">Model</span>
|
<span class="hero-detail-label">Location</span>
|
||||||
<span class="hero-detail-value">{{ pc.model.modelnumber }}</span>
|
<span class="hero-detail-value">{{ computer.location_name }}</span>
|
||||||
</div>
|
|
||||||
<div class="hero-detail" v-if="ipAddress">
|
|
||||||
<span class="hero-detail-label">IP Address</span>
|
|
||||||
<span class="hero-detail-value mono">{{ ipAddress }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -57,20 +50,20 @@
|
|||||||
<h3 class="section-title">Identity</h3>
|
<h3 class="section-title">Identity</h3>
|
||||||
<div class="info-list">
|
<div class="info-list">
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="info-label">Machine Number</span>
|
<span class="info-label">Asset Number</span>
|
||||||
<span class="info-value">{{ pc.machinenumber }}</span>
|
<span class="info-value">{{ computer.assetnumber }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-row" v-if="pc.alias">
|
<div class="info-row" v-if="computer.name">
|
||||||
<span class="info-label">Alias</span>
|
<span class="info-label">Name</span>
|
||||||
<span class="info-value">{{ pc.alias }}</span>
|
<span class="info-value">{{ computer.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-row" v-if="pc.hostname">
|
<div class="info-row" v-if="computer.computer?.hostname">
|
||||||
<span class="info-label">Hostname</span>
|
<span class="info-label">Hostname</span>
|
||||||
<span class="info-value mono">{{ pc.hostname }}</span>
|
<span class="info-value mono">{{ computer.computer.hostname }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-row" v-if="pc.serialnumber">
|
<div class="info-row" v-if="computer.serialnumber">
|
||||||
<span class="info-label">Serial Number</span>
|
<span class="info-label">Serial Number</span>
|
||||||
<span class="info-value mono">{{ pc.serialnumber }}</span>
|
<span class="info-value mono">{{ computer.serialnumber }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -79,21 +72,13 @@
|
|||||||
<div class="section-card">
|
<div class="section-card">
|
||||||
<h3 class="section-title">Hardware</h3>
|
<h3 class="section-title">Hardware</h3>
|
||||||
<div class="info-list">
|
<div class="info-list">
|
||||||
<div class="info-row" v-if="pc.pctype">
|
<div class="info-row">
|
||||||
<span class="info-label">PC Type</span>
|
<span class="info-label">Computer Type</span>
|
||||||
<span class="info-value">{{ pc.pctype.pctype || pc.pctype }}</span>
|
<span class="info-value">{{ computer.computer?.computertype_name || '-' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="info-label">Vendor</span>
|
|
||||||
<span class="info-value">{{ pc.vendor?.vendor || '-' }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-row">
|
|
||||||
<span class="info-label">Model</span>
|
|
||||||
<span class="info-value">{{ pc.model?.modelnumber || '-' }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-row" v-if="pc.operatingsystem">
|
|
||||||
<span class="info-label">Operating System</span>
|
<span class="info-label">Operating System</span>
|
||||||
<span class="info-value">{{ pc.operatingsystem.osname }}</span>
|
<span class="info-value">{{ computer.computer?.os_name || '-' }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -102,18 +87,26 @@
|
|||||||
<div class="section-card">
|
<div class="section-card">
|
||||||
<h3 class="section-title">Status</h3>
|
<h3 class="section-title">Status</h3>
|
||||||
<div class="info-list">
|
<div class="info-list">
|
||||||
<div class="info-row" v-if="pc.loggedinuser">
|
<div class="info-row" v-if="computer.computer?.loggedinuser">
|
||||||
<span class="info-label">Logged In User</span>
|
<span class="info-label">Logged In User</span>
|
||||||
<span class="info-value">{{ pc.loggedinuser }}</span>
|
<span class="info-value">{{ computer.computer.loggedinuser }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="info-label">Features</span>
|
<span class="info-label">Features</span>
|
||||||
<span class="info-value">
|
<span class="info-value">
|
||||||
<span class="feature-tag" :class="{ active: pc.isvnc }">VNC</span>
|
<span class="feature-tag" :class="{ active: computer.computer?.isvnc }">VNC</span>
|
||||||
<span class="feature-tag" :class="{ active: pc.iswinrm }">WinRM</span>
|
<span class="feature-tag" :class="{ active: computer.computer?.iswinrm }">WinRM</span>
|
||||||
<span class="feature-tag" :class="{ active: pc.isshopfloor }">Shopfloor</span>
|
<span class="feature-tag" :class="{ active: computer.computer?.isshopfloor }">Shopfloor</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="info-row" v-if="computer.computer?.lastreporteddate">
|
||||||
|
<span class="info-label">Last Reported</span>
|
||||||
|
<span class="info-value">{{ formatDate(computer.computer.lastreporteddate) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row" v-if="computer.computer?.lastboottime">
|
||||||
|
<span class="info-label">Last Boot</span>
|
||||||
|
<span class="info-value">{{ formatDate(computer.computer.lastboottime) }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -128,36 +121,40 @@
|
|||||||
<span class="info-label">Location</span>
|
<span class="info-label">Location</span>
|
||||||
<span class="info-value">
|
<span class="info-value">
|
||||||
<LocationMapTooltip
|
<LocationMapTooltip
|
||||||
v-if="pc.mapleft != null && pc.maptop != null"
|
v-if="computer.mapleft != null && computer.maptop != null"
|
||||||
:left="pc.mapleft"
|
:left="computer.mapleft"
|
||||||
:top="pc.maptop"
|
:top="computer.maptop"
|
||||||
:machineName="pc.machinenumber"
|
:machineName="computer.assetnumber"
|
||||||
>
|
>
|
||||||
<span class="location-link">{{ pc.location?.location || 'On Map' }}</span>
|
<span class="location-link">{{ computer.location_name || 'On Map' }}</span>
|
||||||
</LocationMapTooltip>
|
</LocationMapTooltip>
|
||||||
<span v-else>{{ pc.location?.location || '-' }}</span>
|
<span v-else>{{ computer.location_name || '-' }}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="info-label">Business Unit</span>
|
||||||
|
<span class="info-value">{{ computer.businessunit_name || '-' }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Controlled Equipment -->
|
<!-- Controlled Equipment -->
|
||||||
<div class="section-card" v-if="controlledMachines.length > 0">
|
<div class="section-card" v-if="controlledEquipment.length > 0">
|
||||||
<h3 class="section-title">Controlled Equipment</h3>
|
<h3 class="section-title">Controlled Equipment</h3>
|
||||||
<div class="equipment-list">
|
<div class="equipment-list">
|
||||||
<router-link
|
<router-link
|
||||||
v-for="rel in controlledMachines"
|
v-for="item in controlledEquipment"
|
||||||
:key="rel.relationshipid"
|
:key="item.relationshipid"
|
||||||
:to="getRelatedRoute(rel)"
|
:to="`/machines/${item.plugin_id || item.assetid}`"
|
||||||
class="equipment-item"
|
class="equipment-item"
|
||||||
>
|
>
|
||||||
<div class="equipment-info">
|
<div class="equipment-info">
|
||||||
<span class="equipment-name">{{ rel.relatedmachinenumber }}</span>
|
<span class="equipment-name">{{ item.assetnumber }}</span>
|
||||||
<span class="equipment-alias" v-if="rel.relatedmachinealias">{{ rel.relatedmachinealias }}</span>
|
<span class="equipment-alias" v-if="item.name">{{ item.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="equipment-meta">
|
<div class="equipment-meta">
|
||||||
<span class="category-tag">{{ rel.relatedcategory }}</span>
|
<span class="category-tag">{{ item.assettype_name }}</span>
|
||||||
<span class="connection-tag">{{ rel.relationshiptype }}</span>
|
<span class="connection-tag">{{ item.relationshipType }}</span>
|
||||||
</div>
|
</div>
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
@@ -184,43 +181,23 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Network -->
|
|
||||||
<div class="section-card" v-if="pc.communications?.length">
|
|
||||||
<h3 class="section-title">Network Interfaces</h3>
|
|
||||||
<div class="network-list">
|
|
||||||
<div v-for="comm in pc.communications" :key="comm.communicationid" class="network-item">
|
|
||||||
<div class="network-primary">
|
|
||||||
<span class="ip-address">{{ comm.ipaddress || comm.address || '-' }}</span>
|
|
||||||
<span v-if="comm.isprimary" class="primary-badge">Primary</span>
|
|
||||||
</div>
|
|
||||||
<div class="network-secondary" v-if="comm.macaddress">
|
|
||||||
<span class="mac-address">{{ comm.macaddress }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="network-details" v-if="comm.subnetmask || comm.defaultgateway">
|
|
||||||
<span v-if="comm.subnetmask">Subnet: {{ comm.subnetmask }}</span>
|
|
||||||
<span v-if="comm.defaultgateway">Gateway: {{ comm.defaultgateway }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Notes -->
|
<!-- Notes -->
|
||||||
<div class="section-card" v-if="pc.notes">
|
<div class="section-card" v-if="computer.notes">
|
||||||
<h3 class="section-title">Notes</h3>
|
<h3 class="section-title">Notes</h3>
|
||||||
<p class="notes-text">{{ pc.notes }}</p>
|
<p class="notes-text">{{ computer.notes }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Audit Footer -->
|
<!-- Audit Footer -->
|
||||||
<div class="audit-footer">
|
<div class="audit-footer">
|
||||||
<span>Created {{ formatDate(pc.createddate) }}<template v-if="pc.createdby"> by {{ pc.createdby }}</template></span>
|
<span>Created {{ formatDate(computer.createddate) }}<template v-if="computer.createdby"> by {{ computer.createdby }}</template></span>
|
||||||
<span>Modified {{ formatDate(pc.modifieddate) }}<template v-if="pc.modifiedby"> by {{ pc.modifiedby }}</template></span>
|
<span>Modified {{ formatDate(computer.modifieddate) }}<template v-if="computer.modifiedby"> by {{ computer.modifiedby }}</template></span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div v-else class="card">
|
<div v-else class="card">
|
||||||
<p style="text-align: center; color: var(--text-light);">PC not found</p>
|
<p style="text-align: center; color: var(--text-light);">Computer not found</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -228,60 +205,74 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, computed } from 'vue'
|
import { ref, onMounted, computed } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { machinesApi, applicationsApi } from '../../api'
|
import { computersApi, applicationsApi, assetsApi } from '../../api'
|
||||||
import LocationMapTooltip from '../../components/LocationMapTooltip.vue'
|
import LocationMapTooltip from '../../components/LocationMapTooltip.vue'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const pc = ref(null)
|
const computer = ref(null)
|
||||||
const relationships = ref([])
|
const relationships = ref({ incoming: [], outgoing: [] })
|
||||||
const installedApps = ref([])
|
const installedApps = ref([])
|
||||||
|
|
||||||
const ipAddress = computed(() => {
|
const controlledEquipment = computed(() => {
|
||||||
if (!pc.value?.communications) return null
|
// For computers, find related equipment in any "Controls" relationship
|
||||||
const primaryComm = pc.value.communications.find(c => c.isprimary) || pc.value.communications[0]
|
const items = []
|
||||||
return primaryComm?.ipaddress || primaryComm?.address || null
|
|
||||||
})
|
|
||||||
|
|
||||||
const controlledMachines = computed(() => {
|
// Check outgoing - computer controls equipment
|
||||||
return relationships.value.filter(r => r.direction === 'controls')
|
for (const rel of relationships.value.outgoing || []) {
|
||||||
|
if (rel.target_asset?.assettype_name === 'equipment' && rel.relationship_type_name === 'Controls') {
|
||||||
|
items.push({
|
||||||
|
...rel.target_asset,
|
||||||
|
relationshipid: rel.relationshipid,
|
||||||
|
relationshipType: rel.relationship_type_name
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also check incoming - legacy data may have equipment -> computer Controls relationships
|
||||||
|
for (const rel of relationships.value.incoming || []) {
|
||||||
|
if (rel.source_asset?.assettype_name === 'equipment' && rel.relationship_type_name === 'Controls') {
|
||||||
|
items.push({
|
||||||
|
...rel.source_asset,
|
||||||
|
relationshipid: rel.relationshipid,
|
||||||
|
relationshipType: rel.relationship_type_name
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return items
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
const response = await machinesApi.get(route.params.id)
|
const response = await computersApi.get(route.params.id)
|
||||||
pc.value = response.data.data
|
computer.value = response.data.data
|
||||||
|
|
||||||
const relResponse = await machinesApi.getRelationships(route.params.id)
|
// Load relationships using asset ID
|
||||||
relationships.value = relResponse.data.data || []
|
if (computer.value?.assetid) {
|
||||||
|
try {
|
||||||
|
const relResponse = await assetsApi.getRelationships(computer.value.assetid)
|
||||||
|
relationships.value = relResponse.data.data || { incoming: [], outgoing: [] }
|
||||||
|
} catch (e) {
|
||||||
|
console.log('Relationships not available')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Load installed applications
|
// Load installed applications
|
||||||
try {
|
try {
|
||||||
const appsResponse = await applicationsApi.getMachineApps(route.params.id)
|
const appsResponse = await applicationsApi.getMachineApps(route.params.id)
|
||||||
installedApps.value = appsResponse.data.data || []
|
installedApps.value = appsResponse.data.data || []
|
||||||
} catch (appError) {
|
} catch (appError) {
|
||||||
// Silently handle if no apps table yet
|
|
||||||
console.log('No installed apps data:', appError.message)
|
console.log('No installed apps data:', appError.message)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading PC:', error)
|
console.error('Error loading computer:', error)
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
function getRelatedRoute(rel) {
|
|
||||||
const category = rel.relatedcategory?.toLowerCase() || ''
|
|
||||||
const routeMap = {
|
|
||||||
'equipment': '/machines',
|
|
||||||
'pc': '/pcs',
|
|
||||||
'printer': '/printers'
|
|
||||||
}
|
|
||||||
const basePath = routeMap[category] || '/machines'
|
|
||||||
return `${basePath}/${rel.relatedmachineid}`
|
|
||||||
}
|
|
||||||
|
|
||||||
function getStatusClass(status) {
|
function getStatusClass(status) {
|
||||||
if (!status) return 'badge-info'
|
if (!status) return 'badge-info'
|
||||||
const s = status.toLowerCase()
|
const s = status.toLowerCase()
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h2>PCs</h2>
|
<h2>Computers</h2>
|
||||||
<router-link to="/pcs/new" class="btn btn-primary">Add PC</router-link>
|
<router-link to="/pcs/new" class="btn btn-primary">Add Computer</router-link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Filters -->
|
<!-- Filters -->
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
v-model="search"
|
v-model="search"
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
placeholder="Search PCs..."
|
placeholder="Search computers..."
|
||||||
@input="debouncedSearch"
|
@input="debouncedSearch"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -24,41 +24,43 @@
|
|||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Machine #</th>
|
<th>Asset #</th>
|
||||||
|
<th>Hostname</th>
|
||||||
<th>Serial Number</th>
|
<th>Serial Number</th>
|
||||||
<th>PC Type</th>
|
<th>Type</th>
|
||||||
<th>Features</th>
|
<th>Features</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th>Actions</th>
|
<th>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="pc in pcs" :key="pc.machineid">
|
<tr v-for="item in computers" :key="item.assetid">
|
||||||
<td>{{ pc.machinenumber }}</td>
|
<td>{{ item.assetnumber }}</td>
|
||||||
<td class="mono">{{ pc.serialnumber || '-' }}</td>
|
<td>{{ item.computer?.hostname || '-' }}</td>
|
||||||
<td>{{ pc.pctype || '-' }}</td>
|
<td class="mono">{{ item.serialnumber || '-' }}</td>
|
||||||
|
<td>{{ item.computer?.computertype_name || '-' }}</td>
|
||||||
<td class="features">
|
<td class="features">
|
||||||
<span v-if="pc.isvnc" class="feature-tag active">VNC</span>
|
<span v-if="item.computer?.isvnc" class="feature-tag active">VNC</span>
|
||||||
<span v-if="pc.iswinrm" class="feature-tag active">WinRM</span>
|
<span v-if="item.computer?.iswinrm" class="feature-tag active">WinRM</span>
|
||||||
<span v-if="!pc.isvnc && !pc.iswinrm">-</span>
|
<span v-if="!item.computer?.isvnc && !item.computer?.iswinrm">-</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="badge" :class="getStatusClass(pc.status)">
|
<span class="badge" :class="getStatusClass(item.status_name)">
|
||||||
{{ pc.status || 'Unknown' }}
|
{{ item.status_name || 'Unknown' }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="actions">
|
<td class="actions">
|
||||||
<router-link
|
<router-link
|
||||||
:to="`/pcs/${pc.machineid}`"
|
:to="`/pcs/${item.computer?.computerid || item.assetid}`"
|
||||||
class="btn btn-secondary btn-sm"
|
class="btn btn-secondary btn-sm"
|
||||||
>
|
>
|
||||||
View
|
View
|
||||||
</router-link>
|
</router-link>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-if="pcs.length === 0">
|
<tr v-if="computers.length === 0">
|
||||||
<td colspan="6" style="text-align: center; color: var(--text-light);">
|
<td colspan="7" style="text-align: center; color: var(--text-light);">
|
||||||
No PCs found
|
No computers found
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -83,9 +85,9 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { machinesApi } from '../../api'
|
import { computersApi } from '../../api'
|
||||||
|
|
||||||
const pcs = ref([])
|
const computers = ref([])
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const search = ref('')
|
const search = ref('')
|
||||||
const page = ref(1)
|
const page = ref(1)
|
||||||
@@ -94,24 +96,23 @@ const totalPages = ref(1)
|
|||||||
let searchTimeout = null
|
let searchTimeout = null
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadPCs()
|
loadComputers()
|
||||||
})
|
})
|
||||||
|
|
||||||
async function loadPCs() {
|
async function loadComputers() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const params = {
|
const params = {
|
||||||
page: page.value,
|
page: page.value,
|
||||||
perpage: 20,
|
per_page: 20
|
||||||
category: 'PC'
|
|
||||||
}
|
}
|
||||||
if (search.value) params.search = search.value
|
if (search.value) params.search = search.value
|
||||||
|
|
||||||
const response = await machinesApi.list(params)
|
const response = await computersApi.list(params)
|
||||||
pcs.value = response.data.data || []
|
computers.value = response.data.data || []
|
||||||
totalPages.value = response.data.meta?.pagination?.total_pages || 1
|
totalPages.value = response.data.meta?.pagination?.total_pages || 1
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading PCs:', error)
|
console.error('Error loading computers:', error)
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
@@ -121,13 +122,13 @@ function debouncedSearch() {
|
|||||||
clearTimeout(searchTimeout)
|
clearTimeout(searchTimeout)
|
||||||
searchTimeout = setTimeout(() => {
|
searchTimeout = setTimeout(() => {
|
||||||
page.value = 1
|
page.value = 1
|
||||||
loadPCs()
|
loadComputers()
|
||||||
}, 300)
|
}, 300)
|
||||||
}
|
}
|
||||||
|
|
||||||
function goToPage(p) {
|
function goToPage(p) {
|
||||||
page.value = p
|
page.value = p
|
||||||
loadPCs()
|
loadComputers()
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStatusClass(status) {
|
function getStatusClass(status) {
|
||||||
|
|||||||
@@ -13,11 +13,11 @@ export default defineConfig({
|
|||||||
port: 5173,
|
port: 5173,
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://localhost:5000',
|
target: 'http://localhost:5001',
|
||||||
changeOrigin: true
|
changeOrigin: true
|
||||||
},
|
},
|
||||||
'/static': {
|
'/static': {
|
||||||
target: 'http://localhost:5000',
|
target: 'http://localhost:5001',
|
||||||
changeOrigin: true
|
changeOrigin: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ computers_bp = Blueprint('computers', __name__)
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
@computers_bp.route('/types', methods=['GET'])
|
@computers_bp.route('/types', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required(optional=True)
|
||||||
def list_computer_types():
|
def list_computer_types():
|
||||||
"""List all computer types."""
|
"""List all computer types."""
|
||||||
page, per_page = get_pagination_params(request)
|
page, per_page = get_pagination_params(request)
|
||||||
@@ -45,7 +45,7 @@ def list_computer_types():
|
|||||||
|
|
||||||
|
|
||||||
@computers_bp.route('/types/<int:type_id>', methods=['GET'])
|
@computers_bp.route('/types/<int:type_id>', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required(optional=True)
|
||||||
def get_computer_type(type_id: int):
|
def get_computer_type(type_id: int):
|
||||||
"""Get a single computer type."""
|
"""Get a single computer type."""
|
||||||
t = ComputerType.query.get(type_id)
|
t = ComputerType.query.get(type_id)
|
||||||
@@ -126,7 +126,7 @@ def update_computer_type(type_id: int):
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
@computers_bp.route('', methods=['GET'])
|
@computers_bp.route('', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required(optional=True)
|
||||||
def list_computers():
|
def list_computers():
|
||||||
"""
|
"""
|
||||||
List all computers with filtering and pagination.
|
List all computers with filtering and pagination.
|
||||||
@@ -211,7 +211,7 @@ def list_computers():
|
|||||||
|
|
||||||
|
|
||||||
@computers_bp.route('/<int:computer_id>', methods=['GET'])
|
@computers_bp.route('/<int:computer_id>', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required(optional=True)
|
||||||
def get_computer(computer_id: int):
|
def get_computer(computer_id: int):
|
||||||
"""Get a single computer with full details."""
|
"""Get a single computer with full details."""
|
||||||
comp = Computer.query.get(computer_id)
|
comp = Computer.query.get(computer_id)
|
||||||
@@ -230,7 +230,7 @@ def get_computer(computer_id: int):
|
|||||||
|
|
||||||
|
|
||||||
@computers_bp.route('/by-asset/<int:asset_id>', methods=['GET'])
|
@computers_bp.route('/by-asset/<int:asset_id>', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required(optional=True)
|
||||||
def get_computer_by_asset(asset_id: int):
|
def get_computer_by_asset(asset_id: int):
|
||||||
"""Get computer data by asset ID."""
|
"""Get computer data by asset ID."""
|
||||||
comp = Computer.query.filter_by(assetid=asset_id).first()
|
comp = Computer.query.filter_by(assetid=asset_id).first()
|
||||||
@@ -249,7 +249,7 @@ def get_computer_by_asset(asset_id: int):
|
|||||||
|
|
||||||
|
|
||||||
@computers_bp.route('/by-hostname/<hostname>', methods=['GET'])
|
@computers_bp.route('/by-hostname/<hostname>', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required(optional=True)
|
||||||
def get_computer_by_hostname(hostname: str):
|
def get_computer_by_hostname(hostname: str):
|
||||||
"""Get computer by hostname."""
|
"""Get computer by hostname."""
|
||||||
comp = Computer.query.filter_by(hostname=hostname).first()
|
comp = Computer.query.filter_by(hostname=hostname).first()
|
||||||
@@ -441,7 +441,7 @@ def delete_computer(computer_id: int):
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
@computers_bp.route('/<int:computer_id>/apps', methods=['GET'])
|
@computers_bp.route('/<int:computer_id>/apps', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required(optional=True)
|
||||||
def get_installed_apps(computer_id: int):
|
def get_installed_apps(computer_id: int):
|
||||||
"""Get all installed applications for a computer."""
|
"""Get all installed applications for a computer."""
|
||||||
comp = Computer.query.get(computer_id)
|
comp = Computer.query.get(computer_id)
|
||||||
@@ -547,7 +547,7 @@ def remove_installed_app(computer_id: int, app_id: int):
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
@computers_bp.route('/<int:computer_id>/report', methods=['POST'])
|
@computers_bp.route('/<int:computer_id>/report', methods=['POST'])
|
||||||
@jwt_required(optional=True)
|
@jwt_required()
|
||||||
def report_status(computer_id: int):
|
def report_status(computer_id: int):
|
||||||
"""
|
"""
|
||||||
Report computer status (for agent-based reporting).
|
Report computer status (for agent-based reporting).
|
||||||
@@ -585,7 +585,7 @@ def report_status(computer_id: int):
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
@computers_bp.route('/dashboard/summary', methods=['GET'])
|
@computers_bp.route('/dashboard/summary', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required(optional=True)
|
||||||
def dashboard_summary():
|
def dashboard_summary():
|
||||||
"""Get computer dashboard summary data."""
|
"""Get computer dashboard summary data."""
|
||||||
# Total active computers
|
# Total active computers
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ equipment_bp = Blueprint('equipment', __name__)
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
@equipment_bp.route('/types', methods=['GET'])
|
@equipment_bp.route('/types', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required(optional=True)
|
||||||
def list_equipment_types():
|
def list_equipment_types():
|
||||||
"""List all equipment types."""
|
"""List all equipment types."""
|
||||||
page, per_page = get_pagination_params(request)
|
page, per_page = get_pagination_params(request)
|
||||||
@@ -45,7 +45,7 @@ def list_equipment_types():
|
|||||||
|
|
||||||
|
|
||||||
@equipment_bp.route('/types/<int:type_id>', methods=['GET'])
|
@equipment_bp.route('/types/<int:type_id>', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required(optional=True)
|
||||||
def get_equipment_type(type_id: int):
|
def get_equipment_type(type_id: int):
|
||||||
"""Get a single equipment type."""
|
"""Get a single equipment type."""
|
||||||
t = EquipmentType.query.get(type_id)
|
t = EquipmentType.query.get(type_id)
|
||||||
@@ -126,7 +126,7 @@ def update_equipment_type(type_id: int):
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
@equipment_bp.route('', methods=['GET'])
|
@equipment_bp.route('', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required(optional=True)
|
||||||
def list_equipment():
|
def list_equipment():
|
||||||
"""
|
"""
|
||||||
List all equipment with filtering and pagination.
|
List all equipment with filtering and pagination.
|
||||||
@@ -201,7 +201,7 @@ def list_equipment():
|
|||||||
|
|
||||||
|
|
||||||
@equipment_bp.route('/<int:equipment_id>', methods=['GET'])
|
@equipment_bp.route('/<int:equipment_id>', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required(optional=True)
|
||||||
def get_equipment(equipment_id: int):
|
def get_equipment(equipment_id: int):
|
||||||
"""Get a single equipment item with full details."""
|
"""Get a single equipment item with full details."""
|
||||||
equip = Equipment.query.get(equipment_id)
|
equip = Equipment.query.get(equipment_id)
|
||||||
@@ -220,7 +220,7 @@ def get_equipment(equipment_id: int):
|
|||||||
|
|
||||||
|
|
||||||
@equipment_bp.route('/by-asset/<int:asset_id>', methods=['GET'])
|
@equipment_bp.route('/by-asset/<int:asset_id>', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required(optional=True)
|
||||||
def get_equipment_by_asset(asset_id: int):
|
def get_equipment_by_asset(asset_id: int):
|
||||||
"""Get equipment data by asset ID."""
|
"""Get equipment data by asset ID."""
|
||||||
equip = Equipment.query.filter_by(assetid=asset_id).first()
|
equip = Equipment.query.filter_by(assetid=asset_id).first()
|
||||||
@@ -305,7 +305,9 @@ def create_equipment():
|
|||||||
islocationonly=data.get('islocationonly', False),
|
islocationonly=data.get('islocationonly', False),
|
||||||
lastmaintenancedate=data.get('lastmaintenancedate'),
|
lastmaintenancedate=data.get('lastmaintenancedate'),
|
||||||
nextmaintenancedate=data.get('nextmaintenancedate'),
|
nextmaintenancedate=data.get('nextmaintenancedate'),
|
||||||
maintenanceintervaldays=data.get('maintenanceintervaldays')
|
maintenanceintervaldays=data.get('maintenanceintervaldays'),
|
||||||
|
controller_vendorid=data.get('controller_vendorid'),
|
||||||
|
controller_modelid=data.get('controller_modelid')
|
||||||
)
|
)
|
||||||
|
|
||||||
db.session.add(equip)
|
db.session.add(equip)
|
||||||
@@ -355,7 +357,8 @@ def update_equipment(equipment_id: int):
|
|||||||
# Update equipment fields
|
# Update equipment fields
|
||||||
equipment_fields = ['equipmenttypeid', 'vendorid', 'modelnumberid',
|
equipment_fields = ['equipmenttypeid', 'vendorid', 'modelnumberid',
|
||||||
'requiresmanualconfig', 'islocationonly',
|
'requiresmanualconfig', 'islocationonly',
|
||||||
'lastmaintenancedate', 'nextmaintenancedate', 'maintenanceintervaldays']
|
'lastmaintenancedate', 'nextmaintenancedate', 'maintenanceintervaldays',
|
||||||
|
'controller_vendorid', 'controller_modelid']
|
||||||
for key in equipment_fields:
|
for key in equipment_fields:
|
||||||
if key in data:
|
if key in data:
|
||||||
setattr(equip, key, data[key])
|
setattr(equip, key, data[key])
|
||||||
@@ -393,7 +396,7 @@ def delete_equipment(equipment_id: int):
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
@equipment_bp.route('/dashboard/summary', methods=['GET'])
|
@equipment_bp.route('/dashboard/summary', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required(optional=True)
|
||||||
def dashboard_summary():
|
def dashboard_summary():
|
||||||
"""Get equipment dashboard summary data."""
|
"""Get equipment dashboard summary data."""
|
||||||
# Total active equipment count
|
# Total active equipment count
|
||||||
|
|||||||
@@ -77,14 +77,30 @@ class Equipment(BaseModel):
|
|||||||
nextmaintenancedate = db.Column(db.DateTime, nullable=True)
|
nextmaintenancedate = db.Column(db.DateTime, nullable=True)
|
||||||
maintenanceintervaldays = db.Column(db.Integer, nullable=True)
|
maintenanceintervaldays = db.Column(db.Integer, nullable=True)
|
||||||
|
|
||||||
|
# Controller info (for CNC machines)
|
||||||
|
controller_vendorid = db.Column(
|
||||||
|
db.Integer,
|
||||||
|
db.ForeignKey('vendors.vendorid'),
|
||||||
|
nullable=True,
|
||||||
|
comment='Controller vendor (e.g., FANUC)'
|
||||||
|
)
|
||||||
|
controller_modelid = db.Column(
|
||||||
|
db.Integer,
|
||||||
|
db.ForeignKey('models.modelnumberid'),
|
||||||
|
nullable=True,
|
||||||
|
comment='Controller model (e.g., 31B)'
|
||||||
|
)
|
||||||
|
|
||||||
# Relationships
|
# Relationships
|
||||||
asset = db.relationship(
|
asset = db.relationship(
|
||||||
'Asset',
|
'Asset',
|
||||||
backref=db.backref('equipment', uselist=False, lazy='joined')
|
backref=db.backref('equipment', uselist=False, lazy='joined')
|
||||||
)
|
)
|
||||||
equipmenttype = db.relationship('EquipmentType', backref='equipment')
|
equipmenttype = db.relationship('EquipmentType', backref='equipment')
|
||||||
vendor = db.relationship('Vendor', backref='equipment_items')
|
vendor = db.relationship('Vendor', foreign_keys=[vendorid], backref='equipment_items')
|
||||||
model = db.relationship('Model', backref='equipment_items')
|
model = db.relationship('Model', foreign_keys=[modelnumberid], backref='equipment_items')
|
||||||
|
controller_vendor = db.relationship('Vendor', foreign_keys=[controller_vendorid], backref='equipment_controllers')
|
||||||
|
controller_model = db.relationship('Model', foreign_keys=[controller_modelid], backref='equipment_controller_models')
|
||||||
|
|
||||||
__table_args__ = (
|
__table_args__ = (
|
||||||
db.Index('idx_equipment_type', 'equipmenttypeid'),
|
db.Index('idx_equipment_type', 'equipmenttypeid'),
|
||||||
@@ -106,4 +122,10 @@ class Equipment(BaseModel):
|
|||||||
if self.model:
|
if self.model:
|
||||||
result['model_name'] = self.model.modelnumber
|
result['model_name'] = self.model.modelnumber
|
||||||
|
|
||||||
|
# Add controller info
|
||||||
|
if self.controller_vendor:
|
||||||
|
result['controller_vendor_name'] = self.controller_vendor.vendor
|
||||||
|
if self.controller_model:
|
||||||
|
result['controller_model_name'] = self.controller_model.modelnumber
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ network_bp = Blueprint('network', __name__)
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
@network_bp.route('/types', methods=['GET'])
|
@network_bp.route('/types', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required(optional=True)
|
||||||
def list_network_device_types():
|
def list_network_device_types():
|
||||||
"""List all network device types."""
|
"""List all network device types."""
|
||||||
page, per_page = get_pagination_params(request)
|
page, per_page = get_pagination_params(request)
|
||||||
@@ -45,7 +45,7 @@ def list_network_device_types():
|
|||||||
|
|
||||||
|
|
||||||
@network_bp.route('/types/<int:type_id>', methods=['GET'])
|
@network_bp.route('/types/<int:type_id>', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required(optional=True)
|
||||||
def get_network_device_type(type_id: int):
|
def get_network_device_type(type_id: int):
|
||||||
"""Get a single network device type."""
|
"""Get a single network device type."""
|
||||||
t = NetworkDeviceType.query.get(type_id)
|
t = NetworkDeviceType.query.get(type_id)
|
||||||
@@ -126,7 +126,7 @@ def update_network_device_type(type_id: int):
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
@network_bp.route('', methods=['GET'])
|
@network_bp.route('', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required(optional=True)
|
||||||
def list_network_devices():
|
def list_network_devices():
|
||||||
"""
|
"""
|
||||||
List all network devices with filtering and pagination.
|
List all network devices with filtering and pagination.
|
||||||
@@ -214,7 +214,7 @@ def list_network_devices():
|
|||||||
|
|
||||||
|
|
||||||
@network_bp.route('/<int:device_id>', methods=['GET'])
|
@network_bp.route('/<int:device_id>', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required(optional=True)
|
||||||
def get_network_device(device_id: int):
|
def get_network_device(device_id: int):
|
||||||
"""Get a single network device with full details."""
|
"""Get a single network device with full details."""
|
||||||
netdev = NetworkDevice.query.get(device_id)
|
netdev = NetworkDevice.query.get(device_id)
|
||||||
@@ -233,7 +233,7 @@ def get_network_device(device_id: int):
|
|||||||
|
|
||||||
|
|
||||||
@network_bp.route('/by-asset/<int:asset_id>', methods=['GET'])
|
@network_bp.route('/by-asset/<int:asset_id>', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required(optional=True)
|
||||||
def get_network_device_by_asset(asset_id: int):
|
def get_network_device_by_asset(asset_id: int):
|
||||||
"""Get network device data by asset ID."""
|
"""Get network device data by asset ID."""
|
||||||
netdev = NetworkDevice.query.filter_by(assetid=asset_id).first()
|
netdev = NetworkDevice.query.filter_by(assetid=asset_id).first()
|
||||||
@@ -252,7 +252,7 @@ def get_network_device_by_asset(asset_id: int):
|
|||||||
|
|
||||||
|
|
||||||
@network_bp.route('/by-hostname/<hostname>', methods=['GET'])
|
@network_bp.route('/by-hostname/<hostname>', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required(optional=True)
|
||||||
def get_network_device_by_hostname(hostname: str):
|
def get_network_device_by_hostname(hostname: str):
|
||||||
"""Get network device by hostname."""
|
"""Get network device by hostname."""
|
||||||
netdev = NetworkDevice.query.filter_by(hostname=hostname).first()
|
netdev = NetworkDevice.query.filter_by(hostname=hostname).first()
|
||||||
@@ -443,7 +443,7 @@ def delete_network_device(device_id: int):
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
@network_bp.route('/dashboard/summary', methods=['GET'])
|
@network_bp.route('/dashboard/summary', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required(optional=True)
|
||||||
def dashboard_summary():
|
def dashboard_summary():
|
||||||
"""Get network device dashboard summary data."""
|
"""Get network device dashboard summary data."""
|
||||||
# Total active network devices
|
# Total active network devices
|
||||||
@@ -491,7 +491,7 @@ def dashboard_summary():
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
@network_bp.route('/vlans', methods=['GET'])
|
@network_bp.route('/vlans', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required(optional=True)
|
||||||
def list_vlans():
|
def list_vlans():
|
||||||
"""List all VLANs with filtering and pagination."""
|
"""List all VLANs with filtering and pagination."""
|
||||||
page, per_page = get_pagination_params(request)
|
page, per_page = get_pagination_params(request)
|
||||||
@@ -524,7 +524,7 @@ def list_vlans():
|
|||||||
|
|
||||||
|
|
||||||
@network_bp.route('/vlans/<int:vlan_id>', methods=['GET'])
|
@network_bp.route('/vlans/<int:vlan_id>', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required(optional=True)
|
||||||
def get_vlan(vlan_id: int):
|
def get_vlan(vlan_id: int):
|
||||||
"""Get a single VLAN with its subnets."""
|
"""Get a single VLAN with its subnets."""
|
||||||
vlan = VLAN.query.get(vlan_id)
|
vlan = VLAN.query.get(vlan_id)
|
||||||
@@ -644,7 +644,7 @@ def delete_vlan(vlan_id: int):
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
@network_bp.route('/subnets', methods=['GET'])
|
@network_bp.route('/subnets', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required(optional=True)
|
||||||
def list_subnets():
|
def list_subnets():
|
||||||
"""List all subnets with filtering and pagination."""
|
"""List all subnets with filtering and pagination."""
|
||||||
page, per_page = get_pagination_params(request)
|
page, per_page = get_pagination_params(request)
|
||||||
@@ -685,7 +685,7 @@ def list_subnets():
|
|||||||
|
|
||||||
|
|
||||||
@network_bp.route('/subnets/<int:subnet_id>', methods=['GET'])
|
@network_bp.route('/subnets/<int:subnet_id>', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required(optional=True)
|
||||||
def get_subnet(subnet_id: int):
|
def get_subnet(subnet_id: int):
|
||||||
"""Get a single subnet."""
|
"""Get a single subnet."""
|
||||||
subnet = Subnet.query.get(subnet_id)
|
subnet = Subnet.query.get(subnet_id)
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ def create_printer_type():
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
@printers_asset_bp.route('', methods=['GET'])
|
@printers_asset_bp.route('', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required(optional=True)
|
||||||
def list_printers():
|
def list_printers():
|
||||||
"""
|
"""
|
||||||
List all printers with filtering and pagination.
|
List all printers with filtering and pagination.
|
||||||
@@ -186,7 +186,7 @@ def list_printers():
|
|||||||
|
|
||||||
|
|
||||||
@printers_asset_bp.route('/<int:printer_id>', methods=['GET'])
|
@printers_asset_bp.route('/<int:printer_id>', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required(optional=True)
|
||||||
def get_printer(printer_id: int):
|
def get_printer(printer_id: int):
|
||||||
"""Get a single printer with full details."""
|
"""Get a single printer with full details."""
|
||||||
printer = Printer.query.get(printer_id)
|
printer = Printer.query.get(printer_id)
|
||||||
@@ -210,7 +210,7 @@ def get_printer(printer_id: int):
|
|||||||
|
|
||||||
|
|
||||||
@printers_asset_bp.route('/by-asset/<int:asset_id>', methods=['GET'])
|
@printers_asset_bp.route('/by-asset/<int:asset_id>', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required(optional=True)
|
||||||
def get_printer_by_asset(asset_id: int):
|
def get_printer_by_asset(asset_id: int):
|
||||||
"""Get printer data by asset ID."""
|
"""Get printer data by asset ID."""
|
||||||
printer = Printer.query.filter_by(assetid=asset_id).first()
|
printer = Printer.query.filter_by(assetid=asset_id).first()
|
||||||
@@ -437,7 +437,7 @@ def get_printer_supplies(printer_id: int):
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
@printers_asset_bp.route('/dashboard/summary', methods=['GET'])
|
@printers_asset_bp.route('/dashboard/summary', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required(optional=True)
|
||||||
def dashboard_summary():
|
def dashboard_summary():
|
||||||
"""Get printer dashboard summary data."""
|
"""Get printer dashboard summary data."""
|
||||||
# Total active printers
|
# Total active printers
|
||||||
@@ -467,6 +467,10 @@ def dashboard_summary():
|
|||||||
|
|
||||||
return success_response({
|
return success_response({
|
||||||
'total': total,
|
'total': total,
|
||||||
|
'totalprinters': total, # For dashboard compatibility
|
||||||
|
'online': total, # Placeholder - would need monitoring integration
|
||||||
|
'lowsupplies': 0, # Placeholder - would need Zabbix integration
|
||||||
|
'criticalsupplies': 0, # Placeholder - would need Zabbix integration
|
||||||
'by_type': [{'type': t, 'count': c} for t, c in by_type],
|
'by_type': [{'type': t, 'count': c} for t, c in by_type],
|
||||||
'by_vendor': [{'vendor': v, 'count': c} for v, c in by_vendor],
|
'by_vendor': [{'vendor': v, 'count': c} for v, c in by_vendor],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
"""USB plugin API endpoints."""
|
"""USB plugin API endpoints."""
|
||||||
|
|
||||||
from flask import Blueprint, request
|
from flask import Blueprint, request
|
||||||
from flask_jwt_extended import jwt_required
|
from flask_jwt_extended import jwt_required, get_jwt_identity
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from shopdb.extensions import db
|
from shopdb.extensions import db
|
||||||
from shopdb.core.models import Machine, MachineType, Vendor, Model
|
|
||||||
from shopdb.utils.responses import (
|
from shopdb.utils.responses import (
|
||||||
success_response,
|
success_response,
|
||||||
error_response,
|
error_response,
|
||||||
@@ -14,94 +13,143 @@ from shopdb.utils.responses import (
|
|||||||
)
|
)
|
||||||
from shopdb.utils.pagination import get_pagination_params, paginate_query
|
from shopdb.utils.pagination import get_pagination_params, paginate_query
|
||||||
|
|
||||||
from ..models import USBCheckout
|
from ..models import USBDevice, USBDeviceType, USBCheckout
|
||||||
|
|
||||||
usb_bp = Blueprint('usb', __name__)
|
usb_bp = Blueprint('usb', __name__)
|
||||||
|
|
||||||
|
|
||||||
def get_usb_machinetype_id():
|
# =============================================================================
|
||||||
"""Get the USB Device machine type ID dynamically."""
|
# USB Device Types
|
||||||
usb_type = MachineType.query.filter(
|
# =============================================================================
|
||||||
MachineType.machinetype.ilike('%usb%')
|
|
||||||
).first()
|
|
||||||
return usb_type.machinetypeid if usb_type else None
|
|
||||||
|
|
||||||
|
@usb_bp.route('/types', methods=['GET'])
|
||||||
|
@jwt_required(optional=True)
|
||||||
|
def list_device_types():
|
||||||
|
"""List all USB device types."""
|
||||||
|
types = USBDeviceType.query.filter_by(isactive=True).order_by(USBDeviceType.typename).all()
|
||||||
|
return success_response([{
|
||||||
|
'usbdevicetypeid': t.usbdevicetypeid,
|
||||||
|
'typename': t.typename,
|
||||||
|
'description': t.description,
|
||||||
|
'icon': t.icon
|
||||||
|
} for t in types])
|
||||||
|
|
||||||
|
|
||||||
|
@usb_bp.route('/types', methods=['POST'])
|
||||||
|
@jwt_required()
|
||||||
|
def create_device_type():
|
||||||
|
"""Create a new USB device type."""
|
||||||
|
data = request.get_json() or {}
|
||||||
|
|
||||||
|
if not data.get('typename'):
|
||||||
|
return error_response(ErrorCodes.VALIDATION_ERROR, 'typename is required')
|
||||||
|
|
||||||
|
if USBDeviceType.query.filter_by(typename=data['typename']).first():
|
||||||
|
return error_response(ErrorCodes.CONFLICT, 'Type name already exists', http_code=409)
|
||||||
|
|
||||||
|
device_type = USBDeviceType(
|
||||||
|
typename=data['typename'],
|
||||||
|
description=data.get('description'),
|
||||||
|
icon=data.get('icon', 'usb')
|
||||||
|
)
|
||||||
|
|
||||||
|
db.session.add(device_type)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return success_response({
|
||||||
|
'usbdevicetypeid': device_type.usbdevicetypeid,
|
||||||
|
'typename': device_type.typename
|
||||||
|
}, message='Device type created', http_code=201)
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# USB Devices
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
@usb_bp.route('', methods=['GET'])
|
@usb_bp.route('', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required(optional=True)
|
||||||
def list_usb_devices():
|
def list_usb_devices():
|
||||||
"""
|
"""
|
||||||
List all USB devices with checkout status.
|
List all USB devices with checkout status.
|
||||||
|
|
||||||
Query parameters:
|
Query parameters:
|
||||||
- page, per_page: Pagination
|
- page, per_page: Pagination
|
||||||
- search: Search by serial number or alias
|
- search: Search by serial number, label, or asset number
|
||||||
- available: Filter to only available (not checked out) devices
|
- available: Filter to only available (not checked out) devices
|
||||||
|
- typeid: Filter by device type ID
|
||||||
"""
|
"""
|
||||||
page, per_page = get_pagination_params(request)
|
page, per_page = get_pagination_params(request)
|
||||||
|
|
||||||
usb_type_id = get_usb_machinetype_id()
|
query = USBDevice.query.filter_by(isactive=True)
|
||||||
if not usb_type_id:
|
|
||||||
return success_response([]) # No USB type found
|
|
||||||
|
|
||||||
# Get USB devices from machines table
|
# Filter by type
|
||||||
query = db.session.query(Machine).filter(
|
if type_id := request.args.get('typeid'):
|
||||||
Machine.machinetypeid == usb_type_id,
|
query = query.filter_by(usbdevicetypeid=int(type_id))
|
||||||
Machine.isactive == True
|
|
||||||
)
|
# Filter by checkout status
|
||||||
|
if request.args.get('available', '').lower() == 'true':
|
||||||
|
query = query.filter_by(ischeckedout=False)
|
||||||
|
elif request.args.get('checkedout', '').lower() == 'true':
|
||||||
|
query = query.filter_by(ischeckedout=True)
|
||||||
|
|
||||||
# Search filter
|
# Search filter
|
||||||
if search := request.args.get('search'):
|
if search := request.args.get('search'):
|
||||||
query = query.filter(
|
query = query.filter(
|
||||||
db.or_(
|
db.or_(
|
||||||
Machine.serialnumber.ilike(f'%{search}%'),
|
USBDevice.serialnumber.ilike(f'%{search}%'),
|
||||||
Machine.alias.ilike(f'%{search}%'),
|
USBDevice.label.ilike(f'%{search}%'),
|
||||||
Machine.machinenumber.ilike(f'%{search}%')
|
USBDevice.assetnumber.ilike(f'%{search}%'),
|
||||||
|
USBDevice.manufacturer.ilike(f'%{search}%')
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
query = query.order_by(Machine.alias)
|
query = query.order_by(USBDevice.label, USBDevice.serialnumber)
|
||||||
|
|
||||||
items, total = paginate_query(query, page, per_page)
|
items, total = paginate_query(query, page, per_page)
|
||||||
|
data = [device.to_dict() for device in items]
|
||||||
# Build response with checkout status
|
|
||||||
data = []
|
|
||||||
for device in items:
|
|
||||||
# Check if currently checked out
|
|
||||||
active_checkout = USBCheckout.query.filter_by(
|
|
||||||
machineid=device.machineid,
|
|
||||||
checkin_time=None
|
|
||||||
).first()
|
|
||||||
|
|
||||||
item = {
|
|
||||||
'machineid': device.machineid,
|
|
||||||
'machinenumber': device.machinenumber,
|
|
||||||
'alias': device.alias,
|
|
||||||
'serialnumber': device.serialnumber,
|
|
||||||
'notes': device.notes,
|
|
||||||
'vendor_name': device.vendor.vendorname if device.vendor else None,
|
|
||||||
'model_name': device.model.modelnumber if device.model else None,
|
|
||||||
'is_checked_out': active_checkout is not None,
|
|
||||||
'current_checkout': active_checkout.to_dict() if active_checkout else None
|
|
||||||
}
|
|
||||||
data.append(item)
|
|
||||||
|
|
||||||
# Filter by availability if requested
|
|
||||||
if request.args.get('available', '').lower() == 'true':
|
|
||||||
data = [d for d in data if not d['is_checked_out']]
|
|
||||||
total = len(data)
|
|
||||||
|
|
||||||
return paginated_response(data, page, per_page, total)
|
return paginated_response(data, page, per_page, total)
|
||||||
|
|
||||||
|
|
||||||
@usb_bp.route('/<int:device_id>', methods=['GET'])
|
@usb_bp.route('', methods=['POST'])
|
||||||
@jwt_required()
|
@jwt_required()
|
||||||
|
def create_usb_device():
|
||||||
|
"""Create a new USB device."""
|
||||||
|
data = request.get_json() or {}
|
||||||
|
|
||||||
|
if not data.get('serialnumber'):
|
||||||
|
return error_response(ErrorCodes.VALIDATION_ERROR, 'serialnumber is required')
|
||||||
|
|
||||||
|
if USBDevice.query.filter_by(serialnumber=data['serialnumber']).first():
|
||||||
|
return error_response(ErrorCodes.CONFLICT, 'Serial number already exists', http_code=409)
|
||||||
|
|
||||||
|
device = USBDevice(
|
||||||
|
serialnumber=data['serialnumber'],
|
||||||
|
label=data.get('label'),
|
||||||
|
assetnumber=data.get('assetnumber'),
|
||||||
|
usbdevicetypeid=data.get('usbdevicetypeid'),
|
||||||
|
capacitygb=data.get('capacitygb'),
|
||||||
|
vendorid=data.get('vendorid'),
|
||||||
|
productid=data.get('productid'),
|
||||||
|
manufacturer=data.get('manufacturer'),
|
||||||
|
productname=data.get('productname'),
|
||||||
|
storagelocation=data.get('storagelocation'),
|
||||||
|
pin=data.get('pin'),
|
||||||
|
notes=data.get('notes'),
|
||||||
|
ischeckedout=False
|
||||||
|
)
|
||||||
|
|
||||||
|
db.session.add(device)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return success_response(device.to_dict(), message='Device created', http_code=201)
|
||||||
|
|
||||||
|
|
||||||
|
@usb_bp.route('/<int:device_id>', methods=['GET'])
|
||||||
|
@jwt_required(optional=True)
|
||||||
def get_usb_device(device_id: int):
|
def get_usb_device(device_id: int):
|
||||||
"""Get a single USB device with checkout history."""
|
"""Get a single USB device with checkout history."""
|
||||||
device = Machine.query.filter_by(
|
device = USBDevice.query.filter_by(usbdeviceid=device_id, isactive=True).first()
|
||||||
machineid=device_id,
|
|
||||||
machinetypeid=get_usb_machinetype_id()
|
|
||||||
).first()
|
|
||||||
|
|
||||||
if not device:
|
if not device:
|
||||||
return error_response(
|
return error_response(
|
||||||
@@ -110,38 +158,81 @@ def get_usb_device(device_id: int):
|
|||||||
http_code=404
|
http_code=404
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get checkout history
|
# Get recent checkout history
|
||||||
checkouts = USBCheckout.query.filter_by(
|
checkouts = USBCheckout.query.filter_by(
|
||||||
machineid=device_id
|
usbdeviceid=device_id
|
||||||
).order_by(USBCheckout.checkout_time.desc()).limit(50).all()
|
).order_by(USBCheckout.checkout_time.desc()).limit(20).all()
|
||||||
|
|
||||||
# Check current checkout
|
result = device.to_dict()
|
||||||
active_checkout = next((c for c in checkouts if c.checkin_time is None), None)
|
result['checkout_history'] = [c.to_dict() for c in checkouts]
|
||||||
|
|
||||||
result = {
|
|
||||||
'machineid': device.machineid,
|
|
||||||
'machinenumber': device.machinenumber,
|
|
||||||
'alias': device.alias,
|
|
||||||
'serialnumber': device.serialnumber,
|
|
||||||
'notes': device.notes,
|
|
||||||
'vendor_name': device.vendor.vendorname if device.vendor else None,
|
|
||||||
'model_name': device.model.modelnumber if device.model else None,
|
|
||||||
'is_checked_out': active_checkout is not None,
|
|
||||||
'current_checkout': active_checkout.to_dict() if active_checkout else None,
|
|
||||||
'checkout_history': [c.to_dict() for c in checkouts]
|
|
||||||
}
|
|
||||||
|
|
||||||
return success_response(result)
|
return success_response(result)
|
||||||
|
|
||||||
|
|
||||||
|
@usb_bp.route('/<int:device_id>', methods=['PUT'])
|
||||||
|
@jwt_required()
|
||||||
|
def update_usb_device(device_id: int):
|
||||||
|
"""Update a USB device."""
|
||||||
|
device = USBDevice.query.filter_by(usbdeviceid=device_id, isactive=True).first()
|
||||||
|
|
||||||
|
if not device:
|
||||||
|
return error_response(
|
||||||
|
ErrorCodes.NOT_FOUND,
|
||||||
|
f'USB device with ID {device_id} not found',
|
||||||
|
http_code=404
|
||||||
|
)
|
||||||
|
|
||||||
|
data = request.get_json() or {}
|
||||||
|
|
||||||
|
# Update allowed fields
|
||||||
|
for field in ['label', 'assetnumber', 'usbdevicetypeid', 'capacitygb',
|
||||||
|
'vendorid', 'productid', 'manufacturer', 'productname',
|
||||||
|
'storagelocation', 'pin', 'notes']:
|
||||||
|
if field in data:
|
||||||
|
setattr(device, field, data[field])
|
||||||
|
|
||||||
|
device.modifieddate = datetime.utcnow()
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return success_response(device.to_dict(), message='Device updated')
|
||||||
|
|
||||||
|
|
||||||
|
@usb_bp.route('/<int:device_id>', methods=['DELETE'])
|
||||||
|
@jwt_required()
|
||||||
|
def delete_usb_device(device_id: int):
|
||||||
|
"""Soft delete a USB device."""
|
||||||
|
device = USBDevice.query.filter_by(usbdeviceid=device_id, isactive=True).first()
|
||||||
|
|
||||||
|
if not device:
|
||||||
|
return error_response(
|
||||||
|
ErrorCodes.NOT_FOUND,
|
||||||
|
f'USB device with ID {device_id} not found',
|
||||||
|
http_code=404
|
||||||
|
)
|
||||||
|
|
||||||
|
if device.ischeckedout:
|
||||||
|
return error_response(
|
||||||
|
ErrorCodes.VALIDATION_ERROR,
|
||||||
|
'Cannot delete a device that is currently checked out',
|
||||||
|
http_code=400
|
||||||
|
)
|
||||||
|
|
||||||
|
device.isactive = False
|
||||||
|
device.modifieddate = datetime.utcnow()
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return success_response(None, message='Device deleted')
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Checkout/Checkin Operations
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
@usb_bp.route('/<int:device_id>/checkout', methods=['POST'])
|
@usb_bp.route('/<int:device_id>/checkout', methods=['POST'])
|
||||||
@jwt_required()
|
@jwt_required()
|
||||||
def checkout_device(device_id: int):
|
def checkout_device(device_id: int):
|
||||||
"""Check out a USB device."""
|
"""Check out a USB device."""
|
||||||
device = Machine.query.filter_by(
|
device = USBDevice.query.filter_by(usbdeviceid=device_id, isactive=True).first()
|
||||||
machineid=device_id,
|
|
||||||
machinetypeid=get_usb_machinetype_id()
|
|
||||||
).first()
|
|
||||||
|
|
||||||
if not device:
|
if not device:
|
||||||
return error_response(
|
return error_response(
|
||||||
@@ -150,16 +241,10 @@ def checkout_device(device_id: int):
|
|||||||
http_code=404
|
http_code=404
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check if already checked out
|
if device.ischeckedout:
|
||||||
active_checkout = USBCheckout.query.filter_by(
|
|
||||||
machineid=device_id,
|
|
||||||
checkin_time=None
|
|
||||||
).first()
|
|
||||||
|
|
||||||
if active_checkout:
|
|
||||||
return error_response(
|
return error_response(
|
||||||
ErrorCodes.CONFLICT,
|
ErrorCodes.CONFLICT,
|
||||||
f'Device is already checked out by {active_checkout.sso}',
|
f'Device is already checked out to {device.currentusername or device.currentuserid}',
|
||||||
http_code=409
|
http_code=409
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -168,14 +253,24 @@ def checkout_device(device_id: int):
|
|||||||
if not data.get('sso'):
|
if not data.get('sso'):
|
||||||
return error_response(ErrorCodes.VALIDATION_ERROR, 'sso is required')
|
return error_response(ErrorCodes.VALIDATION_ERROR, 'sso is required')
|
||||||
|
|
||||||
|
# Create checkout record
|
||||||
checkout = USBCheckout(
|
checkout = USBCheckout(
|
||||||
machineid=device_id,
|
usbdeviceid=device_id,
|
||||||
|
machineid=0, # Legacy field, set to 0 for new checkouts
|
||||||
sso=data['sso'],
|
sso=data['sso'],
|
||||||
checkout_name=data.get('name'),
|
checkout_name=data.get('checkout_name'),
|
||||||
checkout_reason=data.get('reason'),
|
checkout_time=datetime.utcnow(),
|
||||||
checkout_time=datetime.utcnow()
|
checkout_reason=data.get('checkout_reason'),
|
||||||
|
was_wiped=False
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Update device status
|
||||||
|
device.ischeckedout = True
|
||||||
|
device.currentuserid = data['sso']
|
||||||
|
device.currentusername = data.get('checkout_name')
|
||||||
|
device.currentcheckoutdate = datetime.utcnow()
|
||||||
|
device.modifieddate = datetime.utcnow()
|
||||||
|
|
||||||
db.session.add(checkout)
|
db.session.add(checkout)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
@@ -186,10 +281,7 @@ def checkout_device(device_id: int):
|
|||||||
@jwt_required()
|
@jwt_required()
|
||||||
def checkin_device(device_id: int):
|
def checkin_device(device_id: int):
|
||||||
"""Check in a USB device."""
|
"""Check in a USB device."""
|
||||||
device = Machine.query.filter_by(
|
device = USBDevice.query.filter_by(usbdeviceid=device_id, isactive=True).first()
|
||||||
machineid=device_id,
|
|
||||||
machinetypeid=get_usb_machinetype_id()
|
|
||||||
).first()
|
|
||||||
|
|
||||||
if not device:
|
if not device:
|
||||||
return error_response(
|
return error_response(
|
||||||
@@ -198,38 +290,53 @@ def checkin_device(device_id: int):
|
|||||||
http_code=404
|
http_code=404
|
||||||
)
|
)
|
||||||
|
|
||||||
# Find active checkout
|
if not device.ischeckedout:
|
||||||
active_checkout = USBCheckout.query.filter_by(
|
|
||||||
machineid=device_id,
|
|
||||||
checkin_time=None
|
|
||||||
).first()
|
|
||||||
|
|
||||||
if not active_checkout:
|
|
||||||
return error_response(
|
return error_response(
|
||||||
ErrorCodes.VALIDATION_ERROR,
|
ErrorCodes.VALIDATION_ERROR,
|
||||||
'Device is not currently checked out',
|
'Device is not currently checked out',
|
||||||
http_code=400
|
http_code=400
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Find active checkout
|
||||||
|
active_checkout = USBCheckout.query.filter_by(
|
||||||
|
usbdeviceid=device_id,
|
||||||
|
checkin_time=None
|
||||||
|
).first()
|
||||||
|
|
||||||
data = request.get_json() or {}
|
data = request.get_json() or {}
|
||||||
|
|
||||||
|
if active_checkout:
|
||||||
active_checkout.checkin_time = datetime.utcnow()
|
active_checkout.checkin_time = datetime.utcnow()
|
||||||
|
active_checkout.checkin_notes = data.get('checkin_notes', active_checkout.checkin_notes)
|
||||||
active_checkout.was_wiped = data.get('was_wiped', False)
|
active_checkout.was_wiped = data.get('was_wiped', False)
|
||||||
active_checkout.checkin_notes = data.get('notes')
|
|
||||||
|
# Update device status
|
||||||
|
device.ischeckedout = False
|
||||||
|
device.currentuserid = None
|
||||||
|
device.currentusername = None
|
||||||
|
device.currentcheckoutdate = None
|
||||||
|
device.modifieddate = datetime.utcnow()
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return success_response(active_checkout.to_dict(), message='Device checked in')
|
return success_response(
|
||||||
|
active_checkout.to_dict() if active_checkout else None,
|
||||||
|
message='Device checked in'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Checkout History
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
@usb_bp.route('/<int:device_id>/history', methods=['GET'])
|
@usb_bp.route('/<int:device_id>/history', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required(optional=True)
|
||||||
def get_checkout_history(device_id: int):
|
def get_device_history(device_id: int):
|
||||||
"""Get checkout history for a USB device."""
|
"""Get checkout history for a USB device."""
|
||||||
page, per_page = get_pagination_params(request)
|
page, per_page = get_pagination_params(request)
|
||||||
|
|
||||||
query = USBCheckout.query.filter_by(
|
query = USBCheckout.query.filter_by(
|
||||||
machineid=device_id
|
usbdeviceid=device_id
|
||||||
).order_by(USBCheckout.checkout_time.desc())
|
).order_by(USBCheckout.checkout_time.desc())
|
||||||
|
|
||||||
items, total = paginate_query(query, page, per_page)
|
items, total = paginate_query(query, page, per_page)
|
||||||
@@ -239,14 +346,18 @@ def get_checkout_history(device_id: int):
|
|||||||
|
|
||||||
|
|
||||||
@usb_bp.route('/checkouts', methods=['GET'])
|
@usb_bp.route('/checkouts', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required(optional=True)
|
||||||
def list_all_checkouts():
|
def list_all_checkouts():
|
||||||
"""List all checkouts (active and historical)."""
|
"""
|
||||||
|
List all checkouts (active and historical).
|
||||||
|
|
||||||
|
Query parameters:
|
||||||
|
- active: Filter to only active (not returned) checkouts
|
||||||
|
- sso: Filter by user SSO
|
||||||
|
"""
|
||||||
page, per_page = get_pagination_params(request)
|
page, per_page = get_pagination_params(request)
|
||||||
|
|
||||||
query = db.session.query(USBCheckout).join(
|
query = USBCheckout.query
|
||||||
Machine, USBCheckout.machineid == Machine.machineid
|
|
||||||
)
|
|
||||||
|
|
||||||
# Filter by active only
|
# Filter by active only
|
||||||
if request.args.get('active', '').lower() == 'true':
|
if request.args.get('active', '').lower() == 'true':
|
||||||
@@ -259,17 +370,17 @@ def list_all_checkouts():
|
|||||||
query = query.order_by(USBCheckout.checkout_time.desc())
|
query = query.order_by(USBCheckout.checkout_time.desc())
|
||||||
|
|
||||||
items, total = paginate_query(query, page, per_page)
|
items, total = paginate_query(query, page, per_page)
|
||||||
|
data = [c.to_dict() for c in items]
|
||||||
# Include device info
|
|
||||||
data = []
|
|
||||||
for checkout in items:
|
|
||||||
device = Machine.query.get(checkout.machineid)
|
|
||||||
item = checkout.to_dict()
|
|
||||||
item['device'] = {
|
|
||||||
'machineid': device.machineid,
|
|
||||||
'alias': device.alias,
|
|
||||||
'serialnumber': device.serialnumber
|
|
||||||
} if device else None
|
|
||||||
data.append(item)
|
|
||||||
|
|
||||||
return paginated_response(data, page, per_page, total)
|
return paginated_response(data, page, per_page, total)
|
||||||
|
|
||||||
|
|
||||||
|
@usb_bp.route('/checkouts/active', methods=['GET'])
|
||||||
|
@jwt_required(optional=True)
|
||||||
|
def list_active_checkouts():
|
||||||
|
"""List all currently active checkouts."""
|
||||||
|
checkouts = USBCheckout.query.filter(
|
||||||
|
USBCheckout.checkin_time == None
|
||||||
|
).order_by(USBCheckout.checkout_time.desc()).all()
|
||||||
|
|
||||||
|
return success_response([c.to_dict() for c in checkouts])
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
"""USB plugin models."""
|
"""USB plugin models."""
|
||||||
|
|
||||||
from .usb_checkout import USBCheckout
|
from .usb_device import USBDevice, USBDeviceType, USBCheckout
|
||||||
|
|
||||||
__all__ = ['USBCheckout']
|
__all__ = ['USBDevice', 'USBDeviceType', 'USBCheckout']
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
"""USB Checkout model."""
|
|
||||||
|
|
||||||
from shopdb.extensions import db
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
|
|
||||||
class USBCheckout(db.Model):
|
|
||||||
"""
|
|
||||||
USB device checkout tracking.
|
|
||||||
|
|
||||||
References machines table (USB devices have machinetypeid=44).
|
|
||||||
"""
|
|
||||||
__tablename__ = 'usbcheckouts'
|
|
||||||
|
|
||||||
checkoutid = db.Column(db.Integer, primary_key=True)
|
|
||||||
machineid = db.Column(db.Integer, db.ForeignKey('machines.machineid'), nullable=False, index=True)
|
|
||||||
sso = db.Column(db.String(20), nullable=False, index=True)
|
|
||||||
checkout_name = db.Column(db.String(100))
|
|
||||||
checkout_reason = db.Column(db.Text)
|
|
||||||
checkout_time = db.Column(db.DateTime, default=datetime.utcnow, index=True)
|
|
||||||
checkin_time = db.Column(db.DateTime, index=True)
|
|
||||||
was_wiped = db.Column(db.Boolean, default=False)
|
|
||||||
checkin_notes = db.Column(db.Text)
|
|
||||||
|
|
||||||
def to_dict(self):
|
|
||||||
"""Convert to dictionary."""
|
|
||||||
return {
|
|
||||||
'checkoutid': self.checkoutid,
|
|
||||||
'machineid': self.machineid,
|
|
||||||
'sso': self.sso,
|
|
||||||
'checkout_name': self.checkout_name,
|
|
||||||
'checkout_reason': self.checkout_reason,
|
|
||||||
'checkout_time': self.checkout_time.isoformat() if self.checkout_time else None,
|
|
||||||
'checkin_time': self.checkin_time.isoformat() if self.checkin_time else None,
|
|
||||||
'was_wiped': self.was_wiped,
|
|
||||||
'checkin_notes': self.checkin_notes,
|
|
||||||
'is_checked_out': self.checkin_time is None
|
|
||||||
}
|
|
||||||
@@ -60,6 +60,9 @@ class USBDevice(BaseModel, AuditMixin):
|
|||||||
# Location
|
# Location
|
||||||
storagelocation = db.Column(db.String(200), nullable=True, comment='Where device is stored when not checked out')
|
storagelocation = db.Column(db.String(200), nullable=True, comment='Where device is stored when not checked out')
|
||||||
|
|
||||||
|
# Security
|
||||||
|
pin = db.Column(db.String(50), nullable=True, comment='PIN for encrypted devices')
|
||||||
|
|
||||||
# Notes
|
# Notes
|
||||||
notes = db.Column(db.Text, nullable=True)
|
notes = db.Column(db.Text, nullable=True)
|
||||||
|
|
||||||
@@ -102,56 +105,51 @@ class USBCheckout(BaseModel):
|
|||||||
USB device checkout history.
|
USB device checkout history.
|
||||||
|
|
||||||
Tracks when devices are checked out and returned.
|
Tracks when devices are checked out and returned.
|
||||||
|
Maps to existing usbcheckouts table from classic ShopDB.
|
||||||
"""
|
"""
|
||||||
__tablename__ = 'usbcheckouts'
|
__tablename__ = 'usbcheckouts'
|
||||||
|
|
||||||
usbcheckoutid = db.Column(db.Integer, primary_key=True)
|
checkoutid = db.Column(db.Integer, primary_key=True)
|
||||||
|
|
||||||
# Device reference
|
# Device reference (new column linking to usbdevices table)
|
||||||
usbdeviceid = db.Column(
|
usbdeviceid = db.Column(
|
||||||
db.Integer,
|
db.Integer,
|
||||||
db.ForeignKey('usbdevices.usbdeviceid', ondelete='CASCADE'),
|
db.ForeignKey('usbdevices.usbdeviceid', ondelete='CASCADE'),
|
||||||
nullable=False
|
nullable=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Legacy reference to machines table (kept for backward compatibility)
|
||||||
|
machineid = db.Column(db.Integer, nullable=False)
|
||||||
|
|
||||||
# User info
|
# User info
|
||||||
userid = db.Column(db.String(50), nullable=False, comment='SSO of user')
|
sso = db.Column(db.String(20), nullable=False, comment='SSO of user')
|
||||||
username = db.Column(db.String(100), nullable=True, comment='Name of user')
|
checkout_name = db.Column(db.String(100), nullable=True, comment='Name of user')
|
||||||
|
|
||||||
# Checkout details
|
# Checkout details
|
||||||
checkoutdate = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
|
checkout_time = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
|
||||||
checkindate = db.Column(db.DateTime, nullable=True)
|
checkin_time = db.Column(db.DateTime, nullable=True)
|
||||||
expectedreturndate = db.Column(db.DateTime, nullable=True)
|
|
||||||
|
|
||||||
# Metadata
|
# Metadata
|
||||||
purpose = db.Column(db.String(500), nullable=True, comment='Reason for checkout')
|
checkout_reason = db.Column(db.Text, nullable=True, comment='Reason for checkout')
|
||||||
notes = db.Column(db.Text, nullable=True)
|
checkin_notes = db.Column(db.Text, nullable=True)
|
||||||
checkedoutby = db.Column(db.String(50), nullable=True, comment='Admin who processed checkout')
|
was_wiped = db.Column(db.Boolean, nullable=True, comment='Was device wiped after return')
|
||||||
checkedinby = db.Column(db.String(50), nullable=True, comment='Admin who processed checkin')
|
|
||||||
|
|
||||||
# Relationships
|
# Relationships
|
||||||
device = db.relationship('USBDevice', backref=db.backref('checkouts', lazy='dynamic'))
|
device = db.relationship('USBDevice', backref=db.backref('checkouts', lazy='dynamic'))
|
||||||
|
|
||||||
# Indexes
|
|
||||||
__table_args__ = (
|
|
||||||
db.Index('idx_usbcheckout_device', 'usbdeviceid'),
|
|
||||||
db.Index('idx_usbcheckout_user', 'userid'),
|
|
||||||
db.Index('idx_usbcheckout_dates', 'checkoutdate', 'checkindate'),
|
|
||||||
)
|
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<USBCheckout device={self.usbdeviceid} user={self.userid}>"
|
return f"<USBCheckout device={self.usbdeviceid} user={self.sso}>"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_active(self):
|
def is_active(self):
|
||||||
"""Check if this checkout is currently active (not returned)."""
|
"""Check if this checkout is currently active (not returned)."""
|
||||||
return self.checkindate is None
|
return self.checkin_time is None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def duration_days(self):
|
def duration_days(self):
|
||||||
"""Get duration of checkout in days."""
|
"""Get duration of checkout in days."""
|
||||||
end = self.checkindate or datetime.utcnow()
|
end = self.checkin_time or datetime.utcnow()
|
||||||
delta = end - self.checkoutdate
|
delta = end - self.checkout_time
|
||||||
return delta.days
|
return delta.days
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from flask import Flask, Blueprint
|
|||||||
from shopdb.plugins.base import BasePlugin, PluginMeta
|
from shopdb.plugins.base import BasePlugin, PluginMeta
|
||||||
from shopdb.extensions import db
|
from shopdb.extensions import db
|
||||||
|
|
||||||
from .models import USBCheckout
|
from .models import USBDevice, USBDeviceType, USBCheckout
|
||||||
from .api import usb_bp
|
from .api import usb_bp
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -18,10 +18,10 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
class USBPlugin(BasePlugin):
|
class USBPlugin(BasePlugin):
|
||||||
"""
|
"""
|
||||||
USB plugin - manages USB device checkouts.
|
USB plugin - manages USB device tracking and checkouts.
|
||||||
|
|
||||||
USB devices are stored in the machines table (machinetypeid=44).
|
Standalone plugin for tracking USB flash drives, external drives,
|
||||||
This plugin provides checkout/checkin tracking.
|
and other portable storage devices with checkout/checkin functionality.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -54,7 +54,7 @@ class USBPlugin(BasePlugin):
|
|||||||
|
|
||||||
def get_models(self) -> List[Type]:
|
def get_models(self) -> List[Type]:
|
||||||
"""Return list of SQLAlchemy model classes."""
|
"""Return list of SQLAlchemy model classes."""
|
||||||
return [USBCheckout]
|
return [USBDeviceType, USBDevice, USBCheckout]
|
||||||
|
|
||||||
def init_app(self, app: Flask, db_instance) -> None:
|
def init_app(self, app: Flask, db_instance) -> None:
|
||||||
"""Initialize plugin with Flask app."""
|
"""Initialize plugin with Flask app."""
|
||||||
|
|||||||
@@ -47,12 +47,12 @@ class DevelopmentConfig(Config):
|
|||||||
DEBUG = True
|
DEBUG = True
|
||||||
SQLALCHEMY_ECHO = True
|
SQLALCHEMY_ECHO = True
|
||||||
|
|
||||||
# Use SQLite for local development if no DATABASE_URL set
|
# Use MySQL from DATABASE_URL
|
||||||
SQLALCHEMY_DATABASE_URI = os.environ.get(
|
SQLALCHEMY_DATABASE_URI = os.environ.get(
|
||||||
'DATABASE_URL',
|
'DATABASE_URL',
|
||||||
'sqlite:///shopdb_dev.db'
|
'mysql+pymysql://root:rootpassword@127.0.0.1:3306/shopdb_flask'
|
||||||
)
|
)
|
||||||
SQLALCHEMY_ENGINE_OPTIONS = {} # SQLite doesn't need pool options
|
# Keep pool options from base Config for MySQL
|
||||||
|
|
||||||
|
|
||||||
class TestingConfig(Config):
|
class TestingConfig(Config):
|
||||||
|
|||||||
@@ -393,7 +393,7 @@ def lookup_asset_by_number(assetnumber: str):
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
@assets_bp.route('/<int:asset_id>/relationships', methods=['GET'])
|
@assets_bp.route('/<int:asset_id>/relationships', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required(optional=True)
|
||||||
def get_asset_relationships(asset_id: int):
|
def get_asset_relationships(asset_id: int):
|
||||||
"""
|
"""
|
||||||
Get all relationships for an asset.
|
Get all relationships for an asset.
|
||||||
@@ -521,7 +521,7 @@ def delete_asset_relationship(rel_id: int):
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
@assets_bp.route('/map', methods=['GET'])
|
@assets_bp.route('/map', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required(optional=True)
|
||||||
def get_assets_map():
|
def get_assets_map():
|
||||||
"""
|
"""
|
||||||
Get all assets with map positions for unified floor map display.
|
Get all assets with map positions for unified floor map display.
|
||||||
@@ -529,13 +529,14 @@ def get_assets_map():
|
|||||||
Returns assets with mapleft/maptop coordinates, joined with type-specific data.
|
Returns assets with mapleft/maptop coordinates, joined with type-specific data.
|
||||||
|
|
||||||
Query parameters:
|
Query parameters:
|
||||||
- assettype: Filter by asset type name (equipment, computer, network, printer)
|
- assettype: Filter by asset type name (equipment, computer, network_device, printer)
|
||||||
|
- subtype: Filter by subtype ID (machinetype for equipment/computer, networkdevicetype for network, printertype for printer)
|
||||||
- businessunitid: Filter by business unit ID
|
- businessunitid: Filter by business unit ID
|
||||||
- statusid: Filter by status ID
|
- statusid: Filter by status ID
|
||||||
- locationid: Filter by location ID
|
- locationid: Filter by location ID
|
||||||
- search: Search by assetnumber, name, or serialnumber
|
- search: Search by assetnumber, name, or serialnumber
|
||||||
"""
|
"""
|
||||||
from shopdb.core.models import Location, BusinessUnit
|
from shopdb.core.models import Location, BusinessUnit, MachineType, Machine
|
||||||
|
|
||||||
query = Asset.query.filter(
|
query = Asset.query.filter(
|
||||||
Asset.isactive == True,
|
Asset.isactive == True,
|
||||||
@@ -543,10 +544,52 @@ def get_assets_map():
|
|||||||
Asset.maptop.isnot(None)
|
Asset.maptop.isnot(None)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
selected_assettype = request.args.get('assettype')
|
||||||
|
|
||||||
# Filter by asset type name
|
# Filter by asset type name
|
||||||
if assettype := request.args.get('assettype'):
|
if selected_assettype:
|
||||||
types = assettype.split(',')
|
query = query.join(AssetType).filter(AssetType.assettype == selected_assettype)
|
||||||
query = query.join(AssetType).filter(AssetType.assettype.in_(types))
|
|
||||||
|
# Filter by subtype (depends on asset type) - case-insensitive matching
|
||||||
|
if subtype_id := request.args.get('subtype'):
|
||||||
|
subtype_id = int(subtype_id)
|
||||||
|
asset_type_lower = selected_assettype.lower() if selected_assettype else ''
|
||||||
|
if asset_type_lower == 'equipment':
|
||||||
|
# Filter by equipment type
|
||||||
|
try:
|
||||||
|
from plugins.equipment.models import Equipment
|
||||||
|
query = query.join(Equipment, Equipment.assetid == Asset.assetid).filter(
|
||||||
|
Equipment.equipmenttypeid == subtype_id
|
||||||
|
)
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
elif asset_type_lower == 'computer':
|
||||||
|
# Filter by computer type
|
||||||
|
try:
|
||||||
|
from plugins.computers.models import Computer
|
||||||
|
query = query.join(Computer, Computer.assetid == Asset.assetid).filter(
|
||||||
|
Computer.computertypeid == subtype_id
|
||||||
|
)
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
elif asset_type_lower == 'network device':
|
||||||
|
# Filter by network device type
|
||||||
|
try:
|
||||||
|
from plugins.network.models import NetworkDevice
|
||||||
|
query = query.join(NetworkDevice, NetworkDevice.assetid == Asset.assetid).filter(
|
||||||
|
NetworkDevice.networkdevicetypeid == subtype_id
|
||||||
|
)
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
elif asset_type_lower == 'printer':
|
||||||
|
# Filter by printer type
|
||||||
|
try:
|
||||||
|
from plugins.printers.models import Printer
|
||||||
|
query = query.join(Printer, Printer.assetid == Asset.assetid).filter(
|
||||||
|
Printer.printertypeid == subtype_id
|
||||||
|
)
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
# Filter by business unit
|
# Filter by business unit
|
||||||
if bu_id := request.args.get('businessunitid'):
|
if bu_id := request.args.get('businessunitid'):
|
||||||
@@ -618,6 +661,41 @@ def get_assets_map():
|
|||||||
locations = Location.query.filter(Location.isactive == True).all()
|
locations = Location.query.filter(Location.isactive == True).all()
|
||||||
loc_data = [{'locationid': loc.locationid, 'locationname': loc.locationname} for loc in locations]
|
loc_data = [{'locationid': loc.locationid, 'locationname': loc.locationname} for loc in locations]
|
||||||
|
|
||||||
|
# Get subtypes based on asset type categories (keys match database asset type values)
|
||||||
|
subtypes = {}
|
||||||
|
|
||||||
|
# Equipment types from equipment plugin
|
||||||
|
try:
|
||||||
|
from plugins.equipment.models import EquipmentType
|
||||||
|
equipment_types = EquipmentType.query.filter(EquipmentType.isactive == True).order_by(EquipmentType.equipmenttype).all()
|
||||||
|
subtypes['Equipment'] = [{'id': et.equipmenttypeid, 'name': et.equipmenttype} for et in equipment_types]
|
||||||
|
except ImportError:
|
||||||
|
subtypes['Equipment'] = []
|
||||||
|
|
||||||
|
# Computer types from computers plugin
|
||||||
|
try:
|
||||||
|
from plugins.computers.models import ComputerType
|
||||||
|
computer_types = ComputerType.query.filter(ComputerType.isactive == True).order_by(ComputerType.computertype).all()
|
||||||
|
subtypes['Computer'] = [{'id': ct.computertypeid, 'name': ct.computertype} for ct in computer_types]
|
||||||
|
except ImportError:
|
||||||
|
subtypes['Computer'] = []
|
||||||
|
|
||||||
|
# Network device types
|
||||||
|
try:
|
||||||
|
from plugins.network.models import NetworkDeviceType
|
||||||
|
net_types = NetworkDeviceType.query.filter(NetworkDeviceType.isactive == True).order_by(NetworkDeviceType.networkdevicetype).all()
|
||||||
|
subtypes['Network Device'] = [{'id': nt.networkdevicetypeid, 'name': nt.networkdevicetype} for nt in net_types]
|
||||||
|
except ImportError:
|
||||||
|
subtypes['Network Device'] = []
|
||||||
|
|
||||||
|
# Printer types
|
||||||
|
try:
|
||||||
|
from plugins.printers.models import PrinterType
|
||||||
|
printer_types = PrinterType.query.filter(PrinterType.isactive == True).order_by(PrinterType.printertype).all()
|
||||||
|
subtypes['Printer'] = [{'id': pt.printertypeid, 'name': pt.printertype} for pt in printer_types]
|
||||||
|
except ImportError:
|
||||||
|
subtypes['Printer'] = []
|
||||||
|
|
||||||
return success_response({
|
return success_response({
|
||||||
'assets': data,
|
'assets': data,
|
||||||
'total': len(data),
|
'total': len(data),
|
||||||
@@ -625,7 +703,8 @@ def get_assets_map():
|
|||||||
'assettypes': types_data,
|
'assettypes': types_data,
|
||||||
'statuses': status_data,
|
'statuses': status_data,
|
||||||
'businessunits': bu_data,
|
'businessunits': bu_data,
|
||||||
'locations': loc_data
|
'locations': loc_data,
|
||||||
|
'subtypes': subtypes
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from flask_jwt_extended import jwt_required
|
|||||||
|
|
||||||
from shopdb.extensions import db
|
from shopdb.extensions import db
|
||||||
from shopdb.core.models import (
|
from shopdb.core.models import (
|
||||||
Machine, Application, KnowledgeBase,
|
Application, KnowledgeBase,
|
||||||
Asset, AssetType
|
Asset, AssetType
|
||||||
)
|
)
|
||||||
from shopdb.utils.responses import success_response
|
from shopdb.utils.responses import success_response
|
||||||
@@ -46,74 +46,9 @@ def global_search():
|
|||||||
results = []
|
results = []
|
||||||
search_term = f'%{query}%'
|
search_term = f'%{query}%'
|
||||||
|
|
||||||
# Search Machines (Equipment and PCs)
|
# NOTE: Legacy Machine search is disabled - all data is now in the Asset table
|
||||||
try:
|
# The Asset search below handles equipment, computers, network devices, and printers
|
||||||
machines = Machine.query.filter(
|
# with proper plugin-specific IDs for correct routing
|
||||||
Machine.isactive == True,
|
|
||||||
db.or_(
|
|
||||||
Machine.machinenumber.ilike(search_term),
|
|
||||||
Machine.alias.ilike(search_term),
|
|
||||||
Machine.hostname.ilike(search_term),
|
|
||||||
Machine.serialnumber.ilike(search_term),
|
|
||||||
Machine.notes.ilike(search_term)
|
|
||||||
)
|
|
||||||
).limit(10).all()
|
|
||||||
except Exception as e:
|
|
||||||
import logging
|
|
||||||
logging.error(f"Machine search failed: {e}")
|
|
||||||
machines = []
|
|
||||||
|
|
||||||
for m in machines:
|
|
||||||
# Determine type: PC, Printer, or Equipment
|
|
||||||
is_pc = m.pctypeid is not None
|
|
||||||
is_printer = m.is_printer
|
|
||||||
|
|
||||||
# Calculate relevance - exact matches score higher
|
|
||||||
relevance = 15
|
|
||||||
if m.machinenumber and query.lower() == m.machinenumber.lower():
|
|
||||||
relevance = 100
|
|
||||||
elif m.hostname and query.lower() == m.hostname.lower():
|
|
||||||
relevance = 100
|
|
||||||
elif m.alias and query.lower() in m.alias.lower():
|
|
||||||
relevance = 50
|
|
||||||
|
|
||||||
display_name = m.hostname if is_pc and m.hostname else m.machinenumber
|
|
||||||
if m.alias and not is_pc:
|
|
||||||
display_name = f"{m.machinenumber} ({m.alias})"
|
|
||||||
|
|
||||||
# Determine result type and URL
|
|
||||||
if is_printer:
|
|
||||||
result_type = 'printer'
|
|
||||||
url = f"/printers/{m.machineid}"
|
|
||||||
elif is_pc:
|
|
||||||
result_type = 'pc'
|
|
||||||
url = f"/pcs/{m.machineid}"
|
|
||||||
else:
|
|
||||||
result_type = 'machine'
|
|
||||||
url = f"/machines/{m.machineid}"
|
|
||||||
|
|
||||||
# Get location - prefer machine's own location, fall back to parent machine's location
|
|
||||||
location_name = None
|
|
||||||
if m.location:
|
|
||||||
location_name = m.location.locationname
|
|
||||||
elif m.parent_relationships:
|
|
||||||
# Check parent machines for location
|
|
||||||
for rel in m.parent_relationships:
|
|
||||||
if rel.parent_machine and rel.parent_machine.location:
|
|
||||||
location_name = rel.parent_machine.location.locationname
|
|
||||||
break
|
|
||||||
|
|
||||||
# Get machinetype from model (single source of truth)
|
|
||||||
mt = m.derived_machinetype
|
|
||||||
results.append({
|
|
||||||
'type': result_type,
|
|
||||||
'id': m.machineid,
|
|
||||||
'title': display_name,
|
|
||||||
'subtitle': mt.machinetype if mt else None,
|
|
||||||
'location': location_name,
|
|
||||||
'url': url,
|
|
||||||
'relevance': relevance
|
|
||||||
})
|
|
||||||
|
|
||||||
# Search Applications
|
# Search Applications
|
||||||
try:
|
try:
|
||||||
@@ -173,37 +108,8 @@ def global_search():
|
|||||||
import logging
|
import logging
|
||||||
logging.error(f"KnowledgeBase search failed: {e}")
|
logging.error(f"KnowledgeBase search failed: {e}")
|
||||||
|
|
||||||
# Search Printers (check if printers model exists)
|
# NOTE: Legacy Printer search removed - printers are now in the unified Asset table
|
||||||
try:
|
# The Asset search below handles printers with correct plugin-specific IDs
|
||||||
from shopdb.plugins.printers.models import Printer
|
|
||||||
printers = Printer.query.filter(
|
|
||||||
Printer.isactive == True,
|
|
||||||
db.or_(
|
|
||||||
Printer.printercsfname.ilike(search_term),
|
|
||||||
Printer.printerwindowsname.ilike(search_term),
|
|
||||||
Printer.serialnumber.ilike(search_term),
|
|
||||||
Printer.fqdn.ilike(search_term)
|
|
||||||
)
|
|
||||||
).limit(10).all()
|
|
||||||
|
|
||||||
for p in printers:
|
|
||||||
relevance = 15
|
|
||||||
if p.printercsfname and query.lower() == p.printercsfname.lower():
|
|
||||||
relevance = 100
|
|
||||||
|
|
||||||
display_name = p.printercsfname or p.printerwindowsname or f"Printer #{p.printerid}"
|
|
||||||
|
|
||||||
results.append({
|
|
||||||
'type': 'printer',
|
|
||||||
'id': p.printerid,
|
|
||||||
'title': display_name,
|
|
||||||
'subtitle': p.printerwindowsname if p.printercsfname else None,
|
|
||||||
'url': f"/printers/{p.printerid}",
|
|
||||||
'relevance': relevance
|
|
||||||
})
|
|
||||||
except Exception as e:
|
|
||||||
import logging
|
|
||||||
logging.error(f"Printer search failed: {e}")
|
|
||||||
|
|
||||||
# Search Employees (separate database)
|
# Search Employees (separate database)
|
||||||
try:
|
try:
|
||||||
@@ -281,11 +187,23 @@ def global_search():
|
|||||||
|
|
||||||
# Determine URL and type based on asset type
|
# Determine URL and type based on asset type
|
||||||
asset_type_name = asset.assettype.assettype if asset.assettype else 'asset'
|
asset_type_name = asset.assettype.assettype if asset.assettype else 'asset'
|
||||||
|
|
||||||
|
# Get the plugin-specific ID for proper routing
|
||||||
|
plugin_id = asset.assetid # fallback
|
||||||
|
if asset_type_name == 'equipment' and hasattr(asset, 'equipment') and asset.equipment:
|
||||||
|
plugin_id = asset.equipment.equipmentid
|
||||||
|
elif asset_type_name == 'computer' and hasattr(asset, 'computer') and asset.computer:
|
||||||
|
plugin_id = asset.computer.computerid
|
||||||
|
elif asset_type_name == 'network_device' and hasattr(asset, 'network_device') and asset.network_device:
|
||||||
|
plugin_id = asset.network_device.networkdeviceid
|
||||||
|
elif asset_type_name == 'printer' and hasattr(asset, 'printer') and asset.printer:
|
||||||
|
plugin_id = asset.printer.printerid
|
||||||
|
|
||||||
url_map = {
|
url_map = {
|
||||||
'equipment': f"/equipment/{asset.assetid}",
|
'equipment': f"/machines/{plugin_id}",
|
||||||
'computer': f"/pcs/{asset.assetid}",
|
'computer': f"/pcs/{plugin_id}",
|
||||||
'network_device': f"/network/{asset.assetid}",
|
'network_device': f"/network/{plugin_id}",
|
||||||
'printer': f"/printers/{asset.assetid}",
|
'printer': f"/printers/{plugin_id}",
|
||||||
}
|
}
|
||||||
url = url_map.get(asset_type_name, f"/assets/{asset.assetid}")
|
url = url_map.get(asset_type_name, f"/assets/{asset.assetid}")
|
||||||
|
|
||||||
@@ -299,7 +217,7 @@ def global_search():
|
|||||||
|
|
||||||
results.append({
|
results.append({
|
||||||
'type': asset_type_name,
|
'type': asset_type_name,
|
||||||
'id': asset.assetid,
|
'id': plugin_id,
|
||||||
'title': display_name,
|
'title': display_name,
|
||||||
'subtitle': subtitle,
|
'subtitle': subtitle,
|
||||||
'location': location_name,
|
'location': location_name,
|
||||||
|
|||||||
@@ -156,12 +156,52 @@ class Asset(BaseModel, SoftDeleteMixin, AuditMixin):
|
|||||||
comm = self.communications.filter_by(comtypeid=1).first()
|
comm = self.communications.filter_by(comtypeid=1).first()
|
||||||
return comm.ipaddress if comm else None
|
return comm.ipaddress if comm else None
|
||||||
|
|
||||||
def to_dict(self, include_type_data=False):
|
def get_inherited_location(self):
|
||||||
|
"""
|
||||||
|
Get location data from a related asset if this asset has none.
|
||||||
|
|
||||||
|
Returns dict with locationid, location_name, mapleft, maptop, and
|
||||||
|
inherited_from (assetnumber of source asset) if location was inherited.
|
||||||
|
Returns None if no location data available.
|
||||||
|
"""
|
||||||
|
# If we have our own location data, don't inherit
|
||||||
|
if self.locationid is not None or (self.mapleft is not None and self.maptop is not None):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Check related assets for location data
|
||||||
|
# Look in both incoming and outgoing relationships
|
||||||
|
related_assets = []
|
||||||
|
|
||||||
|
if hasattr(self, 'incoming_relationships'):
|
||||||
|
for rel in self.incoming_relationships:
|
||||||
|
if rel.source_asset and rel.isactive:
|
||||||
|
related_assets.append(rel.source_asset)
|
||||||
|
|
||||||
|
if hasattr(self, 'outgoing_relationships'):
|
||||||
|
for rel in self.outgoing_relationships:
|
||||||
|
if rel.target_asset and rel.isactive:
|
||||||
|
related_assets.append(rel.target_asset)
|
||||||
|
|
||||||
|
# Find first related asset with location data
|
||||||
|
for related in related_assets:
|
||||||
|
if related.locationid is not None or (related.mapleft is not None and related.maptop is not None):
|
||||||
|
return {
|
||||||
|
'locationid': related.locationid,
|
||||||
|
'location_name': related.location.locationname if related.location else None,
|
||||||
|
'mapleft': related.mapleft,
|
||||||
|
'maptop': related.maptop,
|
||||||
|
'inherited_from': related.assetnumber
|
||||||
|
}
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def to_dict(self, include_type_data=False, include_inherited_location=True):
|
||||||
"""
|
"""
|
||||||
Convert model to dictionary.
|
Convert model to dictionary.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
include_type_data: If True, include category-specific data from extension table
|
include_type_data: If True, include category-specific data from extension table
|
||||||
|
include_inherited_location: If True, include location from related assets when missing
|
||||||
"""
|
"""
|
||||||
result = super().to_dict()
|
result = super().to_dict()
|
||||||
|
|
||||||
@@ -175,6 +215,30 @@ class Asset(BaseModel, SoftDeleteMixin, AuditMixin):
|
|||||||
if self.businessunit:
|
if self.businessunit:
|
||||||
result['businessunit_name'] = self.businessunit.businessunit
|
result['businessunit_name'] = self.businessunit.businessunit
|
||||||
|
|
||||||
|
# Add plugin-specific ID for navigation purposes
|
||||||
|
if hasattr(self, 'equipment') and self.equipment:
|
||||||
|
result['plugin_id'] = self.equipment.equipmentid
|
||||||
|
elif hasattr(self, 'computer') and self.computer:
|
||||||
|
result['plugin_id'] = self.computer.computerid
|
||||||
|
elif hasattr(self, 'network_device') and self.network_device:
|
||||||
|
result['plugin_id'] = self.network_device.networkdeviceid
|
||||||
|
elif hasattr(self, 'printer') and self.printer:
|
||||||
|
result['plugin_id'] = self.printer.printerid
|
||||||
|
|
||||||
|
# Include inherited location if this asset has no location data
|
||||||
|
if include_inherited_location:
|
||||||
|
inherited = self.get_inherited_location()
|
||||||
|
if inherited:
|
||||||
|
result['inherited_location'] = inherited
|
||||||
|
# Also set the location fields if they're missing
|
||||||
|
if result.get('locationid') is None:
|
||||||
|
result['locationid'] = inherited['locationid']
|
||||||
|
result['location_name'] = inherited['location_name']
|
||||||
|
if result.get('mapleft') is None:
|
||||||
|
result['mapleft'] = inherited['mapleft']
|
||||||
|
if result.get('maptop') is None:
|
||||||
|
result['maptop'] = inherited['maptop']
|
||||||
|
|
||||||
# Include extension data if requested
|
# Include extension data if requested
|
||||||
if include_type_data:
|
if include_type_data:
|
||||||
ext_data = self._get_extension_data()
|
ext_data = self._get_extension_data()
|
||||||
|
|||||||
2
wsgi.py
2
wsgi.py
@@ -11,4 +11,4 @@ from shopdb import create_app
|
|||||||
app = create_app(os.environ.get('FLASK_ENV', 'development'))
|
app = create_app(os.environ.get('FLASK_ENV', 'development'))
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
app.run(host='0.0.0.0', port=5000, debug=True)
|
app.run(host='0.0.0.0', port=5001, debug=True)
|
||||||
|
|||||||
Reference in New Issue
Block a user