diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js
index bc6670d..55b89fd 100644
--- a/frontend/src/api/index.js
+++ b/frontend/src/api/index.js
@@ -54,7 +54,7 @@ export const authApi = {
}
}
-// Machines API
+// Machines API (legacy - use equipmentApi or computersApi instead)
export const machinesApi = {
list(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
export const relationshipTypesApi = {
list() {
diff --git a/frontend/src/components/EmbeddedLocationMap.vue b/frontend/src/components/EmbeddedLocationMap.vue
index c9415f0..b6903ff 100644
--- a/frontend/src/components/EmbeddedLocationMap.vue
+++ b/frontend/src/components/EmbeddedLocationMap.vue
@@ -6,6 +6,7 @@
import { ref, onMounted, onUnmounted, watch } from 'vue'
import L from 'leaflet'
import 'leaflet/dist/leaflet.css'
+import { currentTheme } from '../stores/theme'
const props = defineProps({
left: { type: Number, default: null },
@@ -23,11 +24,6 @@ const MAP_WIDTH = 3300
const MAP_HEIGHT = 2550
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() {
if (!mapContainer.value || props.left === null || props.top === null) return
@@ -39,8 +35,7 @@ function initMap() {
zoomControl: true
})
- const theme = getTheme()
- const blueprintUrl = theme === 'light'
+ const blueprintUrl = currentTheme.value === 'light'
? '/static/images/sitemap2025-light.png'
: '/static/images/sitemap2025-dark.png'
diff --git a/frontend/src/components/LocationMapTooltip.vue b/frontend/src/components/LocationMapTooltip.vue
index 43e4395..10e6a93 100644
--- a/frontend/src/components/LocationMapTooltip.vue
+++ b/frontend/src/components/LocationMapTooltip.vue
@@ -41,7 +41,8 @@
diff --git a/frontend/src/views/Dashboard.vue b/frontend/src/views/Dashboard.vue
index 7ed9064..ae70c06 100644
--- a/frontend/src/views/Dashboard.vue
+++ b/frontend/src/views/Dashboard.vue
@@ -54,10 +54,10 @@
-
+
@@ -65,26 +65,22 @@
- | Machine # |
- Alias |
+ Name |
+ Category |
Type |
- Status |
+ Business Unit |
- | {{ machine.machinenumber }} |
- {{ machine.alias || '-' }} |
- {{ machine.machinetype }} |
-
-
- {{ machine.status || 'Unknown' }}
-
- |
+ {{ machine.machinenumber || machine.hostname || machine.alias || '-' }} |
+ {{ machine.category || '-' }} |
+ {{ machine.machinetype || '-' }} |
+ {{ machine.businessunit || '-' }} |
|
- No machines found
+ No devices found
|
diff --git a/frontend/src/views/MapView.vue b/frontend/src/views/MapView.vue
index 428cf01..b69ef1e 100644
--- a/frontend/src/views/MapView.vue
+++ b/frontend/src/views/MapView.vue
@@ -10,19 +10,44 @@
Loading...
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ filteredAssets.length }} assets
@@ -53,11 +81,125 @@ const assets = ref([])
const assetTypes = ref([])
const businessunits = 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(() => {
- if (visibleTypes.value.length === 0) return assets.value
- return assets.value.filter(a => visibleTypes.value.includes(a.assettype))
+ let result = assets.value
+
+ // 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 () => {
@@ -69,9 +211,7 @@ onMounted(async () => {
assetTypes.value = data.filters?.assettypes || []
businessunits.value = data.filters?.businessunits || []
statuses.value = data.filters?.statuses || []
-
- // Default: show all types
- visibleTypes.value = assetTypes.value.map(t => t.assettype)
+ subtypes.value = data.filters?.subtypes || {}
} catch (error) {
console.error('Failed to load map data:', error)
} finally {
@@ -79,43 +219,69 @@ onMounted(async () => {
}
})
-function getTypeIcon(assettype) {
- const icons = {
- 'equipment': '⚙',
- 'computer': '💻',
- 'printer': '🖨',
- 'network_device': '🌐'
+function formatTypeName(assettype) {
+ if (!assettype) return assettype
+ const names = {
+ 'equipment': 'Equipment',
+ 'computer': 'Computers',
+ 'printer': 'Printers',
+ 'network device': 'Network Devices',
+ 'network_device': 'Network Devices'
}
- return icons[assettype] || '📦'
+ return names[assettype.toLowerCase()] || 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() {
// Filter is reactive via computed property
}
+function debouncedSearch() {
+ clearTimeout(searchTimeout)
+ searchTimeout = setTimeout(() => {
+ updateMapLayers()
+ }, 300)
+}
+
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 = {
'equipment': '/machines',
'computer': '/pcs',
'printer': '/printers',
- 'network_device': '/network'
+ 'network_device': '/network',
+ 'network device': '/network'
}
- const basePath = routeMap[asset.assettype] || '/machines'
+ const basePath = routeMap[assetType] || '/machines'
- // 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}`)
+ // 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
+ }
}
+
+ router.push(`${basePath}/${id}`)
}
@@ -130,43 +296,45 @@ function handleMarkerClick(asset) {
flex-shrink: 0;
}
-.layer-toggles {
+.map-filters {
display: flex;
- gap: 1rem;
+ gap: 0.75rem;
padding: 0.75rem 1rem;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 8px;
margin-bottom: 0.75rem;
flex-wrap: wrap;
-}
-
-.layer-toggle {
- display: flex;
align-items: center;
- gap: 0.5rem;
- cursor: pointer;
+}
+
+.map-filters select,
+.map-filters input {
padding: 0.5rem 0.75rem;
+ border: 1px solid var(--border);
border-radius: 6px;
- transition: background 0.2s;
-}
-
-.layer-toggle:hover {
background: var(--bg);
+ color: var(--text);
+ font-size: 0.875rem;
}
-.layer-toggle input[type="checkbox"] {
- width: 1.125rem;
- height: 1.125rem;
+.map-filters select {
+ min-width: 160px;
}
-.layer-icon {
- font-size: 1.25rem;
+.map-filters select:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
}
-.layer-count {
+.map-filters input {
+ min-width: 180px;
+}
+
+.result-count {
color: var(--text-light);
font-size: 0.875rem;
+ margin-left: auto;
}
.map-page :deep(.shopfloor-map) {
diff --git a/frontend/src/views/machines/MachineDetail.vue b/frontend/src/views/machines/MachineDetail.vue
index 5da08bb..ed33603 100644
--- a/frontend/src/views/machines/MachineDetail.vue
+++ b/frontend/src/views/machines/MachineDetail.vue
@@ -3,7 +3,7 @@