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 @@ - +
-

Recent Machines

+

Recent Devices

View All
@@ -65,26 +65,22 @@ - - + + - + - - - - + + + + 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...
@@ -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 @@
Machine #AliasNameCategory TypeStatusBusiness 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