diff --git a/frontend/src/components/ShopFloorMap.vue b/frontend/src/components/ShopFloorMap.vue
index c6918a1..675ce4b 100644
--- a/frontend/src/components/ShopFloorMap.vue
+++ b/frontend/src/components/ShopFloorMap.vue
@@ -68,6 +68,15 @@
{{ assetTypeLabels[assetType] || assetType }}
+
+
+
+ {{ entry.label }}
+
@@ -88,6 +97,7 @@ import L from 'leaflet'
import 'leaflet/dist/leaflet.css'
import { loadMapConfig, blueprintUrlFor, state as mapConfig } from '../composables/mapConfig'
import { assetTypeLabel, assetDetailRoute } from '../utils/assetTypes'
+import api from '../api'
const props = defineProps({
machines: { type: Array, default: () => [] },
@@ -114,6 +124,13 @@ let pickerMarker = null
let markerLayer = null
let canvasRenderer = null
+// ADR-010 map overlays (get_map_overlays): plugin-declared per-asset decoration
+// (ring/badge) drawn on top of markers, plus legend entries. overlayLayers are
+// the extra Leaflet layers, cleared and redrawn with the markers.
+const overlayDecorations = ref([]) // [{ style, byAsset: Map(assetid -> {color,label}) }]
+const overlayLegend = ref([]) // [{ label, color }] distinct entries
+let overlayLayers = []
+
const filters = ref({
machinetype: '',
businessunit: '',
@@ -360,10 +377,77 @@ function getDetailRoute(machine) {
return `${basePath}/${machine.machineid}`
}
+// Fetch the declared map overlays (ADR-010) and each overlay's per-asset
+// decoration data, keyed by assetid for a fast join during rendering.
+async function loadOverlays() {
+ let declared
+ try {
+ const response = await api.get('/pluginui/map-overlays')
+ declared = response.data.data || []
+ } catch (err) {
+ return
+ }
+ const decorations = []
+ const legend = []
+ const seen = new Set()
+ for (const overlay of declared) {
+ let items = []
+ try {
+ const endpoint = overlay.endpoint.replace(/^\/api(?=\/)/, '')
+ const response = await api.get(endpoint)
+ items = response.data.data || []
+ } catch (err) {
+ continue
+ }
+ const byAsset = new Map()
+ for (const item of items) {
+ byAsset.set(item.assetid, { color: item.color, label: item.label })
+ // distinct legend entries (label + color) for overlays that opt in
+ if (overlay.legend) {
+ const key = `${item.label}|${item.color}`
+ if (!seen.has(key)) {
+ seen.add(key)
+ legend.push({ label: item.label, color: item.color })
+ }
+ }
+ }
+ decorations.push({ style: overlay.style || 'badge', byAsset })
+ }
+ overlayDecorations.value = decorations
+ overlayLegend.value = legend
+ if (map) renderMarkers()
+}
+
+// Draw ring/badge decorations for one asset's marker.
+function applyOverlays(item, leafletY, leafletX) {
+ if (item.assetid == null) return
+ overlayDecorations.value.forEach((overlay) => {
+ const dec = overlay.byAsset.get(item.assetid)
+ if (!dec) return
+ if (overlay.style === 'ring') {
+ const ring = L.circleMarker([leafletY, leafletX], {
+ radius: 9, fill: false, color: dec.color, weight: 2,
+ opacity: 0.9, interactive: false, renderer: canvasRenderer,
+ })
+ ring.addTo(map)
+ overlayLayers.push(ring)
+ } else {
+ const badge = L.circleMarker([leafletY + 4, leafletX + 4], {
+ radius: 3.5, fillColor: dec.color, color: '#fff', weight: 1,
+ fillOpacity: 1, interactive: false, renderer: canvasRenderer,
+ })
+ badge.addTo(map)
+ overlayLayers.push(badge)
+ }
+ })
+}
+
function renderMarkers() {
- // Clear existing markers
+ // Clear existing markers + overlay decoration layers
markers.value.forEach(m => m.marker.remove())
markers.value = []
+ overlayLayers.forEach(layer => layer.remove())
+ overlayLayers = []
props.machines.forEach(item => {
if (item.mapx == null || item.mapy == null) return
@@ -498,6 +582,7 @@ function renderMarkers() {
marker.on('click', () => emit('markerClick', item))
marker.addTo(map)
+ applyOverlays(item, leafletY, leafletX)
// Build search data
const searchData = props.assetTypeMode
@@ -565,6 +650,7 @@ onMounted(async () => {
MAP_WIDTH = mapConfig.width
MAP_HEIGHT = mapConfig.height
initMap()
+ loadOverlays()
})
onUnmounted(() => {
@@ -667,6 +753,13 @@ onUnmounted(() => {
box-shadow: 0 1px 4px rgba(0,0,0,0.4);
}
+/* Overlay legend entries: a hollow ring, distinct from the solid type dots. */
+.legend-dot.legend-ring {
+ background: transparent;
+ border-width: 3px;
+ box-shadow: none;
+}
+
.map-container {
flex: 1;
min-height: 600px;
diff --git a/plugins/measuringtools/api/routes.py b/plugins/measuringtools/api/routes.py
index 36ad821..89c6fc6 100644
--- a/plugins/measuringtools/api/routes.py
+++ b/plugins/measuringtools/api/routes.py
@@ -363,13 +363,18 @@ def map_overlay():
"""
today = date.today()
query = db.session.query(MeasuringTool).join(Asset).filter(Asset.isactive == True)
+ # Overlay contract (ADR-010): [{assetid, color, label}] joined by assetid.
+ # Only decorate tools that are actually due/overdue - a marker with no entry
+ # gets no badge.
data = []
for tool in query.all():
status = derive_status(tool.nextcalibrationdate, today)
+ if status not in ('overdue', 'duesoon'):
+ continue
data.append({
'assetid': tool.assetid,
- 'calibrationstatus': status,
- 'statuscolor': STATUS_COLORS.get(status, STATUS_COLORS['unknown']),
+ 'color': STATUS_COLORS.get(status, STATUS_COLORS['unknown']),
+ 'label': 'Overdue' if status == 'overdue' else 'Due soon',
})
return success_response(data)
diff --git a/tests/test_plugins/test_measuringtools.py b/tests/test_plugins/test_measuringtools.py
index 1143a57..cee5794 100644
--- a/tests/test_plugins/test_measuringtools.py
+++ b/tests/test_plugins/test_measuringtools.py
@@ -447,10 +447,11 @@ def test_map_overlay_shape_and_derivation(client, auth_headers):
response = client.get('/api/measuringtools/map-overlay')
assert response.status_code == 200, response.get_json()
rows = response.get_json()['data']
+ # ADR-010 overlay contract: [{assetid, color, label}], only due/overdue tools.
row = next(r for r in rows if r['assetid'] == assetid)
- assert set(row) == {'assetid', 'calibrationstatus', 'statuscolor'}
- assert row['calibrationstatus'] == 'overdue'
- assert row['statuscolor'] == STATUS_COLORS['overdue']
+ assert set(row) == {'assetid', 'color', 'label'}
+ assert row['label'] == 'Overdue'
+ assert row['color'] == STATUS_COLORS['overdue']
def test_map_overlay_excludes_inactive(client, auth_headers):