Add custom fields + warranty plugin, rework settings into two-pane shell
Feature work from the 2026-07 session: Settings IA - Replace the flat 27-card settings hub with a persistent two-pane shell (SettingsLayout.vue): grouped, searchable left rail + content pane. - Nest all settings/* routes under the shell via router post-processing; shared nav catalog in settingsNav.js. Group by asset class (PCs, Printers, Equipment, Network) so per-type settings stop scattering. Custom fields (core) - customfields + customfieldvalues tables (migration 7d14), CRUD API at /api/customfields, per-asset value get/save. - Settings management page + reusable CustomFieldsSection (detail) and CustomFieldsInputs (form) wired into all four asset types. Warranty (new plugin) - plugins/warranty: warranties + warrantyassets (migration 7d15), derived coverage status, provider abstraction (manual now; Dell/Lenovo/HP stubs). - API CRUD + per-asset panel + report buckets; WarrantyPanel on all four detail pages; Warranties management page; Warranty report + Reports card. - Seed warranty.* permissions. Printer drivers - printerdrivers table (migration 7d13) linked to printer models; drivers now surface on the matching printer's detail page. Other - PCDetail rebalanced (Network + Status + Warranty + custom fields on the right). - Rename PCs list "Features" column to "Remote Access"; fix badge hover underline. - Drop equipment islocationonly field. - Centralize asset-type label/route maps into utils/assetTypes.js. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -86,6 +86,8 @@
|
||||
import { ref, onMounted, onUnmounted, computed, watch } from 'vue'
|
||||
import L from 'leaflet'
|
||||
import 'leaflet/dist/leaflet.css'
|
||||
import { loadMapConfig, blueprintUrlFor, state as mapConfig } from '../composables/mapConfig'
|
||||
import { assetTypeLabel, assetDetailRoute } from '../utils/assetTypes'
|
||||
|
||||
const props = defineProps({
|
||||
machines: { type: Array, default: () => [] },
|
||||
@@ -119,10 +121,10 @@ const filters = ref({
|
||||
search: ''
|
||||
})
|
||||
|
||||
// Map dimensions (matching old system)
|
||||
const MAP_WIDTH = 3300
|
||||
const MAP_HEIGHT = 2550
|
||||
const bounds = [[0, 0], [MAP_HEIGHT, MAP_WIDTH]]
|
||||
// Map dimensions - facility blueprint size, loaded from settings before
|
||||
// initMap runs (mutable so the loaded values replace the fallback defaults).
|
||||
let MAP_WIDTH = mapConfig.width
|
||||
let MAP_HEIGHT = mapConfig.height
|
||||
|
||||
// Asset type colors (for unified map mode) - normalized lookup
|
||||
const assetTypeColorsMap = {
|
||||
@@ -140,21 +142,10 @@ function getAssetTypeColor(assettype) {
|
||||
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'
|
||||
}
|
||||
|
||||
// Asset-type display labels come from the shared util (single source of truth).
|
||||
const assetTypeLabels = new Proxy({}, {
|
||||
get(target, prop) {
|
||||
if (typeof prop === 'string') {
|
||||
return assetTypeLabelsMap[prop.toLowerCase()] || prop
|
||||
}
|
||||
return prop
|
||||
return typeof prop === 'string' ? assetTypeLabel(prop) : prop
|
||||
}
|
||||
})
|
||||
|
||||
@@ -232,7 +223,8 @@ const visibleAssetTypes = computed(() => {
|
||||
// Get subtype ID from asset based on asset type
|
||||
function getSubtypeId(asset) {
|
||||
if (!asset.typedata) return null
|
||||
const typeLower = asset.assettype?.toLowerCase() || ''
|
||||
// Normalize network_device -> network device so the subtype id resolves.
|
||||
const typeLower = (asset.assettype || '').toLowerCase().replace(/_/g, ' ')
|
||||
if (typeLower === 'equipment') return asset.typedata.equipmenttypeid
|
||||
if (typeLower === 'computer') return asset.typedata.computertypeid
|
||||
if (typeLower === 'network device') return asset.typedata.networkdevicetypeid
|
||||
@@ -271,11 +263,8 @@ function initMap() {
|
||||
renderer: canvasRenderer
|
||||
})
|
||||
|
||||
const blueprintUrl = props.theme === 'light'
|
||||
? '/static/images/sitemap2025-light.png'
|
||||
: '/static/images/sitemap2025-dark.png'
|
||||
|
||||
imageOverlay = L.imageOverlay(blueprintUrl, bounds)
|
||||
const bounds = [[0, 0], [MAP_HEIGHT, MAP_WIDTH]]
|
||||
imageOverlay = L.imageOverlay(blueprintUrlFor(props.theme), bounds)
|
||||
imageOverlay.addTo(map)
|
||||
|
||||
// Set initial view - zoom out to show full floor plan
|
||||
@@ -390,7 +379,8 @@ function renderMarkers() {
|
||||
color = (subtypeId && props.subtypeColors[subtypeId]) || '#BDBDBD'
|
||||
typeName = (subtypeId && props.subtypeNames[subtypeId]) || item.assettype || ''
|
||||
} else {
|
||||
color = getAssetTypeColor(item.assettype)
|
||||
// Prefer the stored AssetType.color; fall back to the built-in map.
|
||||
color = item.assettypecolor || getAssetTypeColor(item.assettype)
|
||||
typeName = item.assettype || ''
|
||||
}
|
||||
displayName = item.displayname || item.name || item.assetnumber || 'Unknown'
|
||||
@@ -517,33 +507,9 @@ function renderMarkers() {
|
||||
applyFilters()
|
||||
}
|
||||
|
||||
// Get detail route for unified asset format
|
||||
// Get detail route for unified asset format (shared util = single source).
|
||||
function getAssetDetailRoute(asset) {
|
||||
const assetType = (asset.assettype || '').toLowerCase()
|
||||
const routeMap = {
|
||||
'equipment': '/machines',
|
||||
'computer': '/pcs',
|
||||
'printer': '/printers',
|
||||
'network_device': '/network',
|
||||
'network device': '/network'
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
return `${basePath}/${id}`
|
||||
return assetDetailRoute(asset)
|
||||
}
|
||||
|
||||
function applyFilters() {
|
||||
@@ -581,14 +547,16 @@ watch(() => props.machines, (newVal, oldVal) => {
|
||||
|
||||
watch(() => props.theme, (newTheme) => {
|
||||
if (imageOverlay && map) {
|
||||
const blueprintUrl = newTheme === 'light'
|
||||
? '/static/images/sitemap2025-light.png'
|
||||
: '/static/images/sitemap2025-dark.png'
|
||||
imageOverlay.setUrl(blueprintUrl)
|
||||
imageOverlay.setUrl(blueprintUrlFor(newTheme))
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
onMounted(async () => {
|
||||
// Load this facility's blueprint + dimensions before building the map so
|
||||
// bounds and coordinate math use the right size. Falls back to defaults.
|
||||
await loadMapConfig()
|
||||
MAP_WIDTH = mapConfig.width
|
||||
MAP_HEIGHT = mapConfig.height
|
||||
initMap()
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user