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:
cproudlock
2026-01-21 16:37:49 -05:00
parent 02d83335ee
commit 9c220a4194
110 changed files with 17693 additions and 600 deletions

View File

@@ -0,0 +1,145 @@
<template>
<div class="embedded-map" ref="mapContainer"></div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, watch } from 'vue'
import L from 'leaflet'
import 'leaflet/dist/leaflet.css'
const props = defineProps({
left: { type: Number, default: null },
top: { type: Number, default: null },
markerColor: { type: String, default: '#ff0000' },
markerLabel: { type: String, default: '' }
})
const mapContainer = ref(null)
let map = null
let marker = null
// Map dimensions
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
map = L.map(mapContainer.value, {
crs: L.CRS.Simple,
minZoom: -3,
maxZoom: 2,
attributionControl: false,
zoomControl: true
})
const theme = getTheme()
const blueprintUrl = theme === 'light'
? '/static/images/sitemap2025-light.png'
: '/static/images/sitemap2025-dark.png'
L.imageOverlay(blueprintUrl, bounds).addTo(map)
// Convert database coordinates to Leaflet (y is inverted)
const leafletY = MAP_HEIGHT - props.top
const leafletX = props.left
// Create marker
const icon = L.divIcon({
html: `<div class="location-marker-dot" style="background: ${props.markerColor};"></div>`,
iconSize: [20, 20],
iconAnchor: [10, 10],
className: 'location-marker'
})
marker = L.marker([leafletY, leafletX], { icon })
if (props.markerLabel) {
marker.bindTooltip(props.markerLabel, {
permanent: true,
direction: 'top',
offset: [0, -10],
className: 'location-label'
})
}
marker.addTo(map)
// Center on marker with appropriate zoom
map.setView([leafletY, leafletX], -1)
map.setMaxBounds(bounds)
}
onMounted(() => {
initMap()
})
onUnmounted(() => {
if (map) {
map.remove()
map = null
}
})
watch([() => props.left, () => props.top], () => {
if (map) {
map.remove()
map = null
}
initMap()
})
</script>
<style scoped>
.embedded-map {
width: 100%;
height: 300px;
border-radius: 8px;
overflow: hidden;
background: var(--bg);
}
:deep(.location-marker) {
background: transparent !important;
border: none !important;
}
:deep(.location-marker-dot) {
width: 20px;
height: 20px;
border-radius: 50%;
border: 3px solid #fff;
box-shadow: 0 0 0 2px rgba(0,0,0,0.3), 0 2px 8px rgba(0,0,0,0.4);
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% {
box-shadow: 0 0 0 2px rgba(0,0,0,0.3), 0 2px 8px rgba(0,0,0,0.4);
}
50% {
box-shadow: 0 0 0 6px rgba(255,0,0,0.2), 0 2px 8px rgba(0,0,0,0.4);
}
}
:deep(.location-label) {
background: rgba(0, 0, 0, 0.85);
color: #fff;
border: none;
border-radius: 4px;
padding: 4px 8px;
font-size: 0.875rem;
font-weight: 500;
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
}
:deep(.location-label::before) {
border-top-color: rgba(0, 0, 0, 0.85);
}
</style>