Add USB, Notifications, Network plugins and reusable EmployeeSearch component
New Plugins: - USB plugin: Device checkout/checkin with employee lookup, checkout history - Notifications plugin: Announcements with types, scheduling, shopfloor display - Network plugin: Network device management with subnets and VLANs - Equipment and Computers plugins: Asset type separation Frontend: - EmployeeSearch component: Reusable employee lookup with autocomplete - USB views: List, detail, checkout/checkin modals - Notifications views: List, form with recognition mode - Network views: Device list, detail, form - Calendar view with FullCalendar integration - Shopfloor and TV dashboard views - Reports index page - Map editor for asset positioning - Light/dark mode fixes for map tooltips Backend: - Employee search API with external lookup service - Collector API for PowerShell data collection - Reports API endpoints - Slides API for TV dashboard - Fixed AppVersion model (removed BaseModel inheritance) - Added checkout_name column to usbcheckouts table Styling: - Unified detail page styles - Improved pagination (page numbers instead of prev/next) - Dark/light mode theme improvements Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -67,7 +67,8 @@ const props = defineProps({
|
||||
statuses: { type: Array, default: () => [] },
|
||||
theme: { type: String, default: 'dark' },
|
||||
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
|
||||
})
|
||||
|
||||
const emit = defineEmits(['markerClick', 'positionPicked'])
|
||||
@@ -91,6 +92,14 @@ const MAP_WIDTH = 3300
|
||||
const MAP_HEIGHT = 2550
|
||||
const bounds = [[0, 0], [MAP_HEIGHT, MAP_WIDTH]]
|
||||
|
||||
// Asset type colors (for unified map mode)
|
||||
const assetTypeColors = {
|
||||
'equipment': '#F44336', // Red
|
||||
'computer': '#2196F3', // Blue
|
||||
'printer': '#4CAF50', // Green
|
||||
'network_device': '#FF9800' // Orange
|
||||
}
|
||||
|
||||
// Type colors - distinct colors for each machine type
|
||||
const typeColors = {
|
||||
// Machining
|
||||
@@ -158,7 +167,8 @@ function initMap() {
|
||||
map = L.map(mapContainer.value, {
|
||||
crs: L.CRS.Simple,
|
||||
minZoom: -4,
|
||||
maxZoom: 2
|
||||
maxZoom: 2,
|
||||
attributionControl: false
|
||||
})
|
||||
|
||||
const blueprintUrl = props.theme === 'light'
|
||||
@@ -168,8 +178,8 @@ function initMap() {
|
||||
imageOverlay = L.imageOverlay(blueprintUrl, bounds)
|
||||
imageOverlay.addTo(map)
|
||||
|
||||
// Set initial view
|
||||
const initialZoom = -1
|
||||
// Set initial view - zoom out to show full floor plan
|
||||
const initialZoom = -2
|
||||
map.setView([MAP_HEIGHT / 2, MAP_WIDTH / 2], initialZoom)
|
||||
map.setMaxBounds(bounds)
|
||||
|
||||
@@ -263,15 +273,29 @@ function renderMarkers() {
|
||||
markers.value.forEach(m => m.marker.remove())
|
||||
markers.value = []
|
||||
|
||||
props.machines.forEach(machine => {
|
||||
if (machine.mapleft == null || machine.maptop == null) return
|
||||
props.machines.forEach(item => {
|
||||
if (item.mapleft == null || item.maptop == null) return
|
||||
|
||||
// Transform coordinates (database Y is top-down, Leaflet is bottom-up)
|
||||
const leafletY = MAP_HEIGHT - machine.maptop
|
||||
const leafletX = machine.mapleft
|
||||
const leafletY = MAP_HEIGHT - item.maptop
|
||||
const leafletX = item.mapleft
|
||||
|
||||
const typeName = machine.machinetype || ''
|
||||
const color = getTypeColor(typeName)
|
||||
// Determine color based on mode
|
||||
let color, typeName, displayName, detailRoute
|
||||
|
||||
if (props.assetTypeMode) {
|
||||
// Unified asset mode
|
||||
color = assetTypeColors[item.assettype] || '#BDBDBD'
|
||||
typeName = item.assettype || ''
|
||||
displayName = item.displayname || item.name || item.assetnumber || 'Unknown'
|
||||
detailRoute = getAssetDetailRoute(item)
|
||||
} else {
|
||||
// Legacy machine mode
|
||||
typeName = item.machinetype || ''
|
||||
color = getTypeColor(typeName)
|
||||
displayName = item.alias || item.machinenumber || 'Unknown'
|
||||
detailRoute = getDetailRoute(item)
|
||||
}
|
||||
|
||||
const icon = L.divIcon({
|
||||
html: `<div class="machine-marker-dot" style="background: ${color};"></div>`,
|
||||
@@ -283,45 +307,56 @@ function renderMarkers() {
|
||||
|
||||
const marker = L.marker([leafletY, leafletX], { icon })
|
||||
|
||||
// Display name for tooltips and popups
|
||||
const displayName = machine.alias || machine.machinenumber || 'Unknown'
|
||||
const detailRoute = getDetailRoute(machine)
|
||||
const category = machine.category?.toLowerCase() || ''
|
||||
|
||||
// Build tooltip content based on category
|
||||
// Build tooltip content
|
||||
let tooltipLines = [`<strong>${displayName}</strong>`]
|
||||
|
||||
// Don't show "LocationOnly" as machine type
|
||||
if (typeName && typeName.toLowerCase() !== 'locationonly') {
|
||||
tooltipLines.push(`<span style="color: #888;">${typeName}</span>`)
|
||||
}
|
||||
|
||||
// Add vendor and model if available
|
||||
if (machine.vendor) {
|
||||
tooltipLines.push(`<span style="color: #aaa;">${machine.vendor}${machine.model ? ' ' + machine.model : ''}</span>`)
|
||||
} else if (machine.model) {
|
||||
tooltipLines.push(`<span style="color: #aaa;">${machine.model}</span>`)
|
||||
}
|
||||
|
||||
// Category-specific info
|
||||
if (category === 'printer') {
|
||||
// Printers: show IP and hostname
|
||||
if (machine.ipaddress) {
|
||||
tooltipLines.push(`<span style="color: #8cf;">IP: ${machine.ipaddress}</span>`)
|
||||
if (props.assetTypeMode) {
|
||||
// Asset mode tooltips
|
||||
const assetTypeLabel = {
|
||||
'equipment': 'Equipment',
|
||||
'computer': 'Computer',
|
||||
'printer': 'Printer',
|
||||
'network_device': 'Network Device'
|
||||
}
|
||||
if (machine.hostname) {
|
||||
tooltipLines.push(`<span style="color: #8cf;">${machine.hostname}</span>`)
|
||||
tooltipLines.push(`<span style="color: #888;">${assetTypeLabel[item.assettype] || item.assettype}</span>`)
|
||||
|
||||
if (item.primaryip) {
|
||||
tooltipLines.push(`<span style="color: #8cf;">IP: ${item.primaryip}</span>`)
|
||||
}
|
||||
if (item.typedata?.hostname) {
|
||||
tooltipLines.push(`<span style="color: #8cf;">${item.typedata.hostname}</span>`)
|
||||
}
|
||||
} else {
|
||||
// Equipment/PC: show connected PC if available
|
||||
if (machine.connected_pc) {
|
||||
tooltipLines.push(`<span style="color: #fc8;">PC: ${machine.connected_pc}</span>`)
|
||||
// Legacy machine mode tooltips
|
||||
const category = item.category?.toLowerCase() || ''
|
||||
|
||||
if (typeName && typeName.toLowerCase() !== 'locationonly') {
|
||||
tooltipLines.push(`<span style="color: #888;">${typeName}</span>`)
|
||||
}
|
||||
|
||||
if (item.vendor) {
|
||||
tooltipLines.push(`<span style="color: #aaa;">${item.vendor}${item.model ? ' ' + item.model : ''}</span>`)
|
||||
} else if (item.model) {
|
||||
tooltipLines.push(`<span style="color: #aaa;">${item.model}</span>`)
|
||||
}
|
||||
|
||||
if (category === 'printer') {
|
||||
if (item.ipaddress) {
|
||||
tooltipLines.push(`<span style="color: #8cf;">IP: ${item.ipaddress}</span>`)
|
||||
}
|
||||
if (item.hostname) {
|
||||
tooltipLines.push(`<span style="color: #8cf;">${item.hostname}</span>`)
|
||||
}
|
||||
} else {
|
||||
if (item.connected_pc) {
|
||||
tooltipLines.push(`<span style="color: #fc8;">PC: ${item.connected_pc}</span>`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Business unit
|
||||
if (machine.businessunit) {
|
||||
tooltipLines.push(`<span style="color: #ccc;">${machine.businessunit}</span>`)
|
||||
if (item.businessunit) {
|
||||
tooltipLines.push(`<span style="color: #ccc;">${item.businessunit}</span>`)
|
||||
}
|
||||
|
||||
const tooltipContent = tooltipLines.join('<br>')
|
||||
@@ -332,36 +367,76 @@ function renderMarkers() {
|
||||
})
|
||||
|
||||
// Click popup (detailed info)
|
||||
const popupContent = `
|
||||
<div class="marker-popup">
|
||||
<strong>${displayName}</strong>
|
||||
<div class="popup-details">
|
||||
<div><span class="label">Number:</span> ${machine.machinenumber || '-'}</div>
|
||||
<div><span class="label">Type:</span> ${typeName || '-'}</div>
|
||||
<div><span class="label">Category:</span> ${machine.category || '-'}</div>
|
||||
<div><span class="label">Status:</span> ${machine.status || '-'}</div>
|
||||
<div><span class="label">Vendor:</span> ${machine.vendor || '-'}</div>
|
||||
<div><span class="label">Model:</span> ${machine.model || '-'}</div>
|
||||
let popupContent
|
||||
if (props.assetTypeMode) {
|
||||
popupContent = `
|
||||
<div class="marker-popup">
|
||||
<strong>${displayName}</strong>
|
||||
<div class="popup-details">
|
||||
<div><span class="label">Asset #:</span> ${item.assetnumber || '-'}</div>
|
||||
<div><span class="label">Type:</span> ${item.assettype || '-'}</div>
|
||||
<div><span class="label">Status:</span> ${item.status || '-'}</div>
|
||||
<div><span class="label">Location:</span> ${item.location || '-'}</div>
|
||||
${item.primaryip ? `<div><span class="label">IP:</span> ${item.primaryip}</div>` : ''}
|
||||
</div>
|
||||
<a href="${detailRoute}" class="popup-link">View Details</a>
|
||||
</div>
|
||||
<a href="${detailRoute}" class="popup-link">View Details</a>
|
||||
</div>
|
||||
`
|
||||
`
|
||||
} else {
|
||||
popupContent = `
|
||||
<div class="marker-popup">
|
||||
<strong>${displayName}</strong>
|
||||
<div class="popup-details">
|
||||
<div><span class="label">Number:</span> ${item.machinenumber || '-'}</div>
|
||||
<div><span class="label">Type:</span> ${typeName || '-'}</div>
|
||||
<div><span class="label">Category:</span> ${item.category || '-'}</div>
|
||||
<div><span class="label">Status:</span> ${item.status || '-'}</div>
|
||||
<div><span class="label">Vendor:</span> ${item.vendor || '-'}</div>
|
||||
<div><span class="label">Model:</span> ${item.model || '-'}</div>
|
||||
</div>
|
||||
<a href="${detailRoute}" class="popup-link">View Details</a>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
marker.bindPopup(popupContent)
|
||||
marker.on('click', () => emit('markerClick', machine))
|
||||
marker.on('click', () => emit('markerClick', item))
|
||||
|
||||
marker.addTo(map)
|
||||
|
||||
// Build search data
|
||||
const searchData = props.assetTypeMode
|
||||
? `${item.assetnumber} ${item.name} ${item.displayname} ${item.assettype} ${item.status} ${item.businessunit} ${item.typedata?.hostname || ''} ${item.primaryip || ''}`.toLowerCase()
|
||||
: `${item.machinenumber} ${item.alias} ${typeName} ${item.vendor} ${item.model} ${item.serialnumber} ${item.businessunit}`.toLowerCase()
|
||||
|
||||
markers.value.push({
|
||||
marker,
|
||||
machine,
|
||||
searchData: `${machine.machinenumber} ${machine.alias} ${typeName} ${machine.vendor} ${machine.model} ${machine.serialnumber} ${machine.businessunit}`.toLowerCase()
|
||||
machine: item,
|
||||
searchData
|
||||
})
|
||||
})
|
||||
|
||||
applyFilters()
|
||||
}
|
||||
|
||||
// Get detail route for unified asset format
|
||||
function getAssetDetailRoute(asset) {
|
||||
const routeMap = {
|
||||
'equipment': '/machines',
|
||||
'computer': '/pcs',
|
||||
'printer': '/printers',
|
||||
'network_device': '/network'
|
||||
}
|
||||
const basePath = routeMap[asset.assettype] || '/machines'
|
||||
|
||||
if (asset.assettype === 'network_device' && asset.typedata?.networkdeviceid) {
|
||||
return `/network/${asset.typedata.networkdeviceid}`
|
||||
}
|
||||
|
||||
const id = asset.typedata?.machineid || asset.assetid
|
||||
return `${basePath}/${id}`
|
||||
}
|
||||
|
||||
function applyFilters() {
|
||||
const searchTerm = filters.value.search.toLowerCase()
|
||||
|
||||
@@ -445,15 +520,30 @@ onUnmounted(() => {
|
||||
padding: 0.625rem 1rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
font-size: 1.125rem;
|
||||
font-size: 1rem;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.filters select {
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23999' d='M6 8L1 3h10z'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 0.75rem center;
|
||||
padding-right: 2.25rem;
|
||||
min-width: 160px;
|
||||
}
|
||||
|
||||
.filters input {
|
||||
width: 280px;
|
||||
}
|
||||
|
||||
.filters input::placeholder {
|
||||
color: var(--text-light);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.filters input::placeholder {
|
||||
color: var(--text-light);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user