Fix the levels viewer: markers were drawn on whichever plan was showing
This commit is contained in:
@@ -492,7 +492,7 @@ function cancelEdit() {
|
||||
.map-editor {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - 40px);
|
||||
height: calc(100vh - var(--main-pad-top) - var(--main-pad-bottom));
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
|
||||
@@ -12,6 +12,17 @@
|
||||
<template v-else>
|
||||
<!-- Filter Controls -->
|
||||
<div class="map-filters">
|
||||
<select
|
||||
v-if="levelChoices.length > 1"
|
||||
v-model.number="shownLevelId"
|
||||
@change="onLevelChange"
|
||||
title="Which floor plan to show"
|
||||
>
|
||||
<option v-for="level in levelChoices" :key="level.levelid" :value="level.levelid">
|
||||
{{ level.label }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<select v-model="selectedType" @change="onTypeChange">
|
||||
<option value="">All Asset Types</option>
|
||||
<option v-for="t in assetTypes" :key="t.assettypeid" :value="t.assettype">
|
||||
@@ -60,6 +71,7 @@
|
||||
</div>
|
||||
|
||||
<ShopFloorMap
|
||||
:levelid="shownLevelId"
|
||||
:machines="filteredAssets"
|
||||
:machinetypes="[]"
|
||||
:businessunits="businessunits"
|
||||
@@ -82,7 +94,8 @@ import ShopFloorMap from '../components/ShopFloorMap.vue'
|
||||
import { assetsApi } from '../api'
|
||||
import { currentTheme } from '../stores/theme'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
import { loadMapConfig, blueprintUrlFor, dimensionsFor, levelName, state as mapConfig } from '@/composables/mapConfig'
|
||||
import { loadMapConfig, blueprintUrlFor, dimensionsFor, levelName, levelOptions,
|
||||
setCurrentLevel, state as mapConfig } from '@/composables/mapConfig'
|
||||
import { exportMapPdf } from '../utils/mapPdf'
|
||||
import { assetTypeLabel, assetDetailRoute } from '../utils/assetTypes'
|
||||
import { getSubtypeId } from '../utils/mapColors'
|
||||
@@ -104,6 +117,41 @@ const selectedSubtype = ref('')
|
||||
const selectedBusinessUnit = ref('')
|
||||
const selectedStatus = ref('')
|
||||
const searchQuery = ref('')
|
||||
|
||||
// Which floor plan is on screen. Every marker coordinate is pixels of ONE level
|
||||
// (ADR-017), so this decides both the drawing and which markers belong on it.
|
||||
const shownLevelId = ref(null)
|
||||
|
||||
const levelChoices = computed(() => levelOptions())
|
||||
|
||||
function onLevelChange() {
|
||||
// Keep the shared composable in step, so a map-position picker opened later
|
||||
// starts on the level being looked at rather than the site default.
|
||||
setCurrentLevel(shownLevelId.value)
|
||||
}
|
||||
|
||||
// A search whose only matches are on another floor would otherwise show an empty
|
||||
// map: the assets matched, the filter counted them, and nothing was drawn.
|
||||
// Follow the results to the level that actually holds them - the level with the
|
||||
// most matches, so a search matching several floors lands on the best one.
|
||||
function followSearchAcrossLevels() {
|
||||
if (!searchQuery.value.trim()) return
|
||||
const matches = filteredAssets.value.filter(a => a.mapx != null && a.mapy != null)
|
||||
if (!matches.length) return
|
||||
if (matches.some(a => (a.levelid ?? null) === shownLevelId.value)) return
|
||||
|
||||
const tally = new Map()
|
||||
matches.forEach(asset => {
|
||||
const levelid = asset.levelid ?? null
|
||||
if (levelid === null) return
|
||||
tally.set(levelid, (tally.get(levelid) || 0) + 1)
|
||||
})
|
||||
if (!tally.size) return
|
||||
|
||||
const best = [...tally.entries()].sort((a, b) => b[1] - a[1])[0][0]
|
||||
shownLevelId.value = best
|
||||
setCurrentLevel(best)
|
||||
}
|
||||
const exporting = ref(false)
|
||||
|
||||
let searchTimeout = null
|
||||
@@ -245,12 +293,12 @@ async function exportPdf() {
|
||||
// blueprintUrlFor applies withBase - the raw setting value is a
|
||||
// root-relative /api path, which 404s under a subpath mount like /ops.
|
||||
// The PDF always uses the light blueprint: it prints on white paper.
|
||||
blueprintUrl: blueprintUrlFor('light', mapConfig.currentlevelid),
|
||||
mapWidth: dimensionsFor(mapConfig.currentlevelid).width,
|
||||
mapHeight: dimensionsFor(mapConfig.currentlevelid).height,
|
||||
blueprintUrl: blueprintUrlFor('light', shownLevelId.value),
|
||||
mapWidth: dimensionsFor(shownLevelId.value).width,
|
||||
mapHeight: dimensionsFor(shownLevelId.value).height,
|
||||
// Named on the sheet, because a floor plan with no level on it is not
|
||||
// identifiable once it is printed and carried to the floor.
|
||||
levelname: levelName(mapConfig.currentlevelid),
|
||||
levelname: levelName(shownLevelId.value),
|
||||
selectedType: selectedType.value,
|
||||
subtypeColors: subtypeColorMap.value,
|
||||
subtypeNames: subtypeNameMap.value,
|
||||
@@ -265,7 +313,8 @@ async function exportPdf() {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
loadMapConfig()
|
||||
await loadMapConfig()
|
||||
shownLevelId.value = mapConfig.currentlevelid ?? mapConfig.defaultlevelid ?? null
|
||||
try {
|
||||
const response = await assetsApi.getMap()
|
||||
const data = response.data.data || {}
|
||||
@@ -305,6 +354,7 @@ function updateMapLayers() {
|
||||
function debouncedSearch() {
|
||||
clearTimeout(searchTimeout)
|
||||
searchTimeout = setTimeout(() => {
|
||||
followSearchAcrossLevels()
|
||||
updateMapLayers()
|
||||
}, 300)
|
||||
}
|
||||
@@ -318,7 +368,7 @@ function handleMarkerClick(asset) {
|
||||
.map-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - 2rem);
|
||||
height: calc(100vh - var(--main-pad-top) - var(--main-pad-bottom));
|
||||
}
|
||||
|
||||
.map-page .page-header {
|
||||
|
||||
@@ -213,9 +213,18 @@ async function search(q) {
|
||||
}
|
||||
|
||||
function performSearch() {
|
||||
if (searchInput.value.trim()) {
|
||||
router.push({ path: '/search', query: { q: searchInput.value.trim() } })
|
||||
const term = searchInput.value.trim()
|
||||
if (!term) return
|
||||
// Searching a term that is ALREADY in the URL is a duplicate navigation: the
|
||||
// router aborts it, so the route watcher never fires and the button does
|
||||
// nothing at all. Re-run the query directly in that case. The sidebar box
|
||||
// never hits this, because it is always navigating from somewhere else -
|
||||
// which is why the two search boxes appeared to behave differently.
|
||||
if (term === route.query.q) {
|
||||
search(term)
|
||||
return
|
||||
}
|
||||
router.push({ path: '/search', query: { q: term } })
|
||||
}
|
||||
|
||||
async function openKBArticle(result) {
|
||||
|
||||
@@ -62,18 +62,36 @@
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<span class="mono">{{ level.mapwidth }} x {{ level.mapheight }}</span>
|
||||
<div class="size-fields">
|
||||
<input
|
||||
v-model.number="level.mapwidth"
|
||||
type="number"
|
||||
min="1"
|
||||
class="form-control size-input"
|
||||
aria-label="Blueprint width in pixels"
|
||||
@blur="saveSize(level)"
|
||||
:disabled="saving"
|
||||
/>
|
||||
<span class="size-x">x</span>
|
||||
<input
|
||||
v-model.number="level.mapheight"
|
||||
type="number"
|
||||
min="1"
|
||||
class="form-control size-input"
|
||||
aria-label="Blueprint height in pixels"
|
||||
@blur="saveSize(level)"
|
||||
:disabled="saving"
|
||||
/>
|
||||
</div>
|
||||
<small v-if="level.assetcount" class="input-hint">
|
||||
fixed while {{ level.assetcount }} marker(s) are placed
|
||||
Taken from the image while a level is empty. Changing it now
|
||||
re-scales where all {{ level.assetcount }} marker(s) sit relative
|
||||
to the drawing - set the new size, then Recalibrate from
|
||||
landmarks in the map editor.
|
||||
</small>
|
||||
<small v-else class="input-hint">
|
||||
Set from the blueprint you upload.
|
||||
</small>
|
||||
<input
|
||||
v-else
|
||||
v-model.number="level.mapwidth"
|
||||
type="number"
|
||||
class="form-control size-input"
|
||||
@blur="saveLevel(level, { mapwidth: level.mapwidth, mapheight: level.mapheight })"
|
||||
:disabled="saving"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<span :class="{ 'muted': !level.assetcount }">{{ level.assetcount }}</span>
|
||||
@@ -311,6 +329,29 @@ async function saveLevel(level, payload) {
|
||||
}
|
||||
}
|
||||
|
||||
async function saveSize(level) {
|
||||
if (!(level.mapwidth > 0) || !(level.mapheight > 0)) {
|
||||
error.value = 'Width and height must both be positive.'
|
||||
await load()
|
||||
return
|
||||
}
|
||||
// A level with markers keeps its coordinates when the size changes, so every
|
||||
// marker moves relative to the drawing. That is sometimes exactly right (a
|
||||
// re-export of the same plan at a new resolution) and sometimes the start of
|
||||
// a recalibration, but it is never something to do by accident.
|
||||
if (level.assetcount) {
|
||||
const ok = window.confirm(
|
||||
`${level.levelname} has ${level.assetcount} marker(s) placed against ` +
|
||||
`${level.mapwidth} x ${level.mapheight}. Changing the size moves all of ` +
|
||||
`them relative to the drawing. Recalibrate from landmarks afterwards. Continue?`)
|
||||
if (!ok) {
|
||||
await load()
|
||||
return
|
||||
}
|
||||
}
|
||||
await saveLevel(level, { mapwidth: level.mapwidth, mapheight: level.mapheight })
|
||||
}
|
||||
|
||||
async function upload(level, theme, event) {
|
||||
const file = event.target.files?.[0]
|
||||
if (!file) return
|
||||
@@ -398,6 +439,8 @@ async function remove(level) {
|
||||
.mono { font-family: monospace; }
|
||||
.order-input { width: 4.5rem; }
|
||||
.size-input { width: 6rem; }
|
||||
.size-fields { display: flex; align-items: center; gap: 0.35rem; }
|
||||
.size-x { color: var(--text-light); }
|
||||
.file-input { display: block; margin-top: 0.25rem; font-size: 0.75rem; }
|
||||
|
||||
.map-thumb {
|
||||
|
||||
Reference in New Issue
Block a user