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:
@@ -2,47 +2,76 @@
|
||||
<div class="map-page">
|
||||
<div class="page-header">
|
||||
<h2>Shop Floor Map</h2>
|
||||
<router-link v-if="authStore.isAuthenticated" to="/map/editor" class="btn btn-primary">
|
||||
Edit Map
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="loading">Loading...</div>
|
||||
|
||||
<ShopFloorMap
|
||||
v-else
|
||||
:machines="machines"
|
||||
:machinetypes="machinetypes"
|
||||
:businessunits="businessunits"
|
||||
:statuses="statuses"
|
||||
@markerClick="handleMarkerClick"
|
||||
/>
|
||||
<template v-else>
|
||||
<!-- Layer Toggles -->
|
||||
<div class="layer-toggles">
|
||||
<label v-for="t in assetTypes" :key="t.assettypeid" class="layer-toggle">
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="visibleTypes"
|
||||
:value="t.assettype"
|
||||
@change="updateMapLayers"
|
||||
/>
|
||||
<span class="layer-icon">{{ getTypeIcon(t.assettype) }}</span>
|
||||
<span>{{ t.assettype }}</span>
|
||||
<span class="layer-count">({{ getTypeCount(t.assettype) }})</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<ShopFloorMap
|
||||
:machines="filteredAssets"
|
||||
:machinetypes="[]"
|
||||
:businessunits="businessunits"
|
||||
:statuses="statuses"
|
||||
:assetTypeMode="true"
|
||||
:theme="currentTheme"
|
||||
@markerClick="handleMarkerClick"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import ShopFloorMap from '../components/ShopFloorMap.vue'
|
||||
import { machinesApi, machinetypesApi, businessunitsApi, statusesApi } from '../api'
|
||||
import { assetsApi } from '../api'
|
||||
import { currentTheme } from '../stores/theme'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
const loading = ref(true)
|
||||
const machines = ref([])
|
||||
const machinetypes = ref([])
|
||||
const assets = ref([])
|
||||
const assetTypes = ref([])
|
||||
const businessunits = ref([])
|
||||
const statuses = ref([])
|
||||
const visibleTypes = ref([])
|
||||
|
||||
const filteredAssets = computed(() => {
|
||||
if (visibleTypes.value.length === 0) return assets.value
|
||||
return assets.value.filter(a => visibleTypes.value.includes(a.assettype))
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const [machinesRes, typesRes, busRes, statusRes] = await Promise.all([
|
||||
machinesApi.list({ hasmap: true, all: true }),
|
||||
machinetypesApi.list(),
|
||||
businessunitsApi.list(),
|
||||
statusesApi.list()
|
||||
])
|
||||
const response = await assetsApi.getMap()
|
||||
const data = response.data.data || {}
|
||||
|
||||
machines.value = machinesRes.data.data || []
|
||||
machinetypes.value = typesRes.data.data || []
|
||||
businessunits.value = busRes.data.data || []
|
||||
statuses.value = statusRes.data.data || []
|
||||
assets.value = data.assets || []
|
||||
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)
|
||||
} catch (error) {
|
||||
console.error('Failed to load map data:', error)
|
||||
} finally {
|
||||
@@ -50,15 +79,43 @@ onMounted(async () => {
|
||||
}
|
||||
})
|
||||
|
||||
function handleMarkerClick(machine) {
|
||||
const category = machine.category?.toLowerCase() || ''
|
||||
function getTypeIcon(assettype) {
|
||||
const icons = {
|
||||
'equipment': '⚙',
|
||||
'computer': '💻',
|
||||
'printer': '🖨',
|
||||
'network_device': '🌐'
|
||||
}
|
||||
return icons[assettype] || '📦'
|
||||
}
|
||||
|
||||
function getTypeCount(assettype) {
|
||||
return assets.value.filter(a => a.assettype === assettype).length
|
||||
}
|
||||
|
||||
function updateMapLayers() {
|
||||
// Filter is reactive via computed property
|
||||
}
|
||||
|
||||
function handleMarkerClick(asset) {
|
||||
// Route based on asset type
|
||||
const routeMap = {
|
||||
'equipment': '/machines',
|
||||
'pc': '/pcs',
|
||||
'printer': '/printers'
|
||||
'computer': '/pcs',
|
||||
'printer': '/printers',
|
||||
'network_device': '/network'
|
||||
}
|
||||
|
||||
const basePath = routeMap[asset.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}`)
|
||||
}
|
||||
const basePath = routeMap[category] || '/machines'
|
||||
router.push(`${basePath}/${machine.machineid}`)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -73,6 +130,45 @@ function handleMarkerClick(machine) {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.layer-toggles {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
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;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: 6px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.layer-toggle:hover {
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
.layer-toggle input[type="checkbox"] {
|
||||
width: 1.125rem;
|
||||
height: 1.125rem;
|
||||
}
|
||||
|
||||
.layer-icon {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.layer-count {
|
||||
color: var(--text-light);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.map-page :deep(.shopfloor-map) {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user