The map was one picture of one floor. A second floor was added, the blueprint changed size, and machines moved, so a position now records WHICH DRAWING its coordinates belong to. Buildings and levels (ADR-017). Each level owns its blueprint per theme and its own native pixel size; assets.mapx/mapy are pixels of assets.levelid, not of the site. A position whose level is unknown renders "level unknown" and is never drawn on the default level, because a marker on the wrong floor plan looks entirely correct while pointing at the wrong place. Repositioning in bulk: filter by unplaced, needs-review or level, search, place, confirm. Landmark recalibration solves the transform PER AXIS from landmark pairs and never from image dimensions - the canvas grew taller without rescaling, so a dimension-derived scale would stretch Y by 1.57 and be wrong everywhere. It defaults to a dry run, reports what would land off the drawing, snapshots before applying, and clears mapverifiedat because a transform is a guess awaiting review. Snapshots restore, including the level and the review state, and a restore snapshots first so an undo is undoable. Search: gaugelabreference was matched only for measuring tools and maintenancereference was matched nowhere at all, for any asset type, while Settings happily offers both identifiers on machines and PCs. A tag an operator is told to record has to be findable or it is a write-only field. USB devices and printed items were unreachable from search entirely - neither is an asset, so the generic asset search could not see them and no searcher existed; they now match on serial, asset tag, label, bin code and gage-lab tag, honouring isactive, with Settings toggles and result labels to match. The retired-application rule was half a rule: GET /api/knowledgebase hid articles whose topic application is retired while global search still returned them and printed the retired application as the subject. A filter is only real if every path that reaches the row applies it. Contract to 0.20.0 (additive): Asset gained levelid and mapverifiedat, Location gained levelid, and resolve_asset_position returns the levelid belonging to whichever source supplied the coordinates. The five plugins that write a map position are re-pinned. The install-list text format gained levelid as a NINTH field, appended, because the shipped Pascal installer reads fields 0-7 by index. That installer still compiles in one drawing's dimensions and bundles one blueprint, so its map is accurate for the default level only; /api/maplevels is deliberately unauthenticated so it can read both at runtime once rebuilt. Recorded in PRINTER-INSTALLER.md section 6 along with the other known gaps. Migration 7d33 converts an existing single-map site into one building and one default level carrying the old map_* settings, then assigns every placed asset and location to it. Nothing moves on screen. Old settings rows are kept so a rollback still finds them. Verified end to end on MySQL 5.6 from a production-shaped database.
643 lines
20 KiB
Vue
643 lines
20 KiB
Vue
<template>
|
|
<div>
|
|
<div class="page-header">
|
|
<h2>{{ isEdit ? 'Edit PC' : 'New PC' }}</h2>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div v-if="loading" class="loading">Loading...</div>
|
|
|
|
<form v-else @submit.prevent="savePC">
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label for="machinenumber">PC Number *</label>
|
|
<input
|
|
id="machinenumber"
|
|
v-model="form.machinenumber"
|
|
type="text"
|
|
class="form-control"
|
|
required
|
|
@input="onPcNumberInput"
|
|
/>
|
|
<small class="form-hint">Defaults to the serial number; editable</small>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="alias">Alias</label>
|
|
<input
|
|
id="alias"
|
|
v-model="form.alias"
|
|
type="text"
|
|
class="form-control"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
<div class="form-group" v-if="isEnabled('fqdn', 'computer')">
|
|
<label for="hostname">Hostname</label>
|
|
<input
|
|
id="hostname"
|
|
v-model="form.hostname"
|
|
type="text"
|
|
class="form-control"
|
|
/>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="serialnumber">Serial Number</label>
|
|
<input
|
|
id="serialnumber"
|
|
v-model="form.serialnumber"
|
|
type="text"
|
|
class="form-control"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-row" v-if="isEnabled('gaugelabreference', 'computer') || isEnabled('maintenancereference', 'computer')">
|
|
<div class="form-group" v-if="isEnabled('gaugelabreference', 'computer')">
|
|
<label for="gaugelabreference">Gauge Lab Reference</label>
|
|
<input
|
|
id="gaugelabreference"
|
|
v-model="form.gaugelabreference"
|
|
type="text"
|
|
class="form-control"
|
|
/>
|
|
</div>
|
|
|
|
<div class="form-group" v-if="isEnabled('maintenancereference', 'computer')">
|
|
<label for="maintenancereference">Maintenance Reference</label>
|
|
<input
|
|
id="maintenancereference"
|
|
v-model="form.maintenancereference"
|
|
type="text"
|
|
class="form-control"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label for="machinetypeid">PC Type *</label>
|
|
<select
|
|
id="machinetypeid"
|
|
v-model="form.machinetypeid"
|
|
class="form-control"
|
|
required
|
|
@change="form.modelnumberid = ''"
|
|
>
|
|
<option value="">Select type...</option>
|
|
<option
|
|
v-for="pt in pcTypes"
|
|
:key="pt.computertypeid"
|
|
:value="pt.computertypeid"
|
|
>
|
|
{{ pt.computertype }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="osid">Operating System</label>
|
|
<select
|
|
id="osid"
|
|
v-model="form.osid"
|
|
class="form-control"
|
|
>
|
|
<option value="">Select OS...</option>
|
|
<option
|
|
v-for="os in operatingsystems"
|
|
:key="os.osid"
|
|
:value="os.osid"
|
|
>
|
|
{{ os.osname }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label for="statusid">Status</label>
|
|
<select
|
|
id="statusid"
|
|
v-model="form.statusid"
|
|
class="form-control"
|
|
>
|
|
<option value="">Select status...</option>
|
|
<option
|
|
v-for="s in statuses"
|
|
:key="s.statusid"
|
|
:value="s.statusid"
|
|
>
|
|
{{ s.status }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="locationid">Location</label>
|
|
<select
|
|
id="locationid"
|
|
v-model="form.locationid"
|
|
class="form-control"
|
|
>
|
|
<option value="">Select location...</option>
|
|
<option
|
|
v-for="l in locations"
|
|
:key="l.locationid"
|
|
:value="l.locationid"
|
|
>
|
|
{{ l.locationname }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label for="vendorid">Vendor</label>
|
|
<select
|
|
id="vendorid"
|
|
v-model="form.vendorid"
|
|
class="form-control"
|
|
@change="form.modelnumberid = ''"
|
|
>
|
|
<option value="">Select vendor...</option>
|
|
<option
|
|
v-for="v in vendors"
|
|
:key="v.vendorid"
|
|
:value="v.vendorid"
|
|
>
|
|
{{ v.vendor }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="modelnumberid">Model</label>
|
|
<select
|
|
id="modelnumberid"
|
|
v-model="form.modelnumberid"
|
|
class="form-control"
|
|
>
|
|
<option value="">Select model...</option>
|
|
<option
|
|
v-for="m in filteredModels"
|
|
:key="m.modelnumberid"
|
|
:value="m.modelnumberid"
|
|
>
|
|
{{ m.modelnumber }}
|
|
</option>
|
|
</select>
|
|
<small v-if="!form.vendorid && !form.machinetypeid" class="form-hint">
|
|
Select vendor or PC type to filter models
|
|
</small>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- PC-specific fields -->
|
|
<h4 style="margin-top: 1.5rem; margin-bottom: 1rem;">Network Settings</h4>
|
|
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label for="ipaddress">IP Address</label>
|
|
<input
|
|
id="ipaddress"
|
|
v-model="form.ipaddress"
|
|
type="text"
|
|
class="form-control"
|
|
placeholder="e.g., 192.168.1.100"
|
|
/>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="loggedinuser">Logged In User</label>
|
|
<input
|
|
id="loggedinuser"
|
|
v-model="form.loggedinuser"
|
|
type="text"
|
|
class="form-control"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>Remote Access Protocols</label>
|
|
<div class="protocol-list">
|
|
<label v-for="p in protocols" :key="p.protocolid" class="protocol-item">
|
|
<input type="checkbox" :checked="isProtocolOn(p.protocolid)" @change="toggleProtocol(p.protocolid, $event.target.checked)" />
|
|
<span>{{ p.name }}</span>
|
|
<input
|
|
v-if="isProtocolOn(p.protocolid)"
|
|
type="number"
|
|
class="port-override"
|
|
:value="protocolPort(p.protocolid)"
|
|
:placeholder="p.defaultport || 'port'"
|
|
min="1"
|
|
max="65535"
|
|
title="Port override (blank = default)"
|
|
@input="setProtocolPort(p.protocolid, $event.target.value)"
|
|
/>
|
|
</label>
|
|
<span v-if="!protocols.length" class="muted">No protocols defined. Add them under Settings > PC Access Protocols.</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="notes">Notes</label>
|
|
<textarea
|
|
id="notes"
|
|
v-model="form.notes"
|
|
class="form-control"
|
|
rows="3"
|
|
></textarea>
|
|
</div>
|
|
|
|
<!-- Map Location Picker -->
|
|
<div class="form-group">
|
|
<label>Map Location</label>
|
|
<div class="map-location-control">
|
|
<div v-if="form.mapx !== null && form.mapy !== null" class="current-position">
|
|
Position: {{ form.mapx }}, {{ form.mapy }}
|
|
<span v-if="form.levelid" class="position-level">on {{ levelName(form.levelid) }}</span>
|
|
<span v-else class="position-level position-level-missing">level not set</span>
|
|
<button type="button" class="btn btn-sm btn-secondary" @click="clearMapPosition">Clear</button>
|
|
</div>
|
|
<button type="button" class="btn btn-secondary" @click="openMapPicker">
|
|
Set Location on Map
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Map Picker Modal -->
|
|
<Modal v-model="showMapPicker" title="Select Location on Map" size="fullscreen">
|
|
<div class="map-modal-content">
|
|
<div v-if="levelOptions().length > 1" class="map-level-picker">
|
|
<label>Level</label>
|
|
<select v-model.number="pickerLevelId" class="form-control">
|
|
<option v-for="option in levelOptions()" :key="option.levelid"
|
|
:value="option.levelid">{{ option.label }}</option>
|
|
</select>
|
|
<span class="input-hint">
|
|
The position is pixels on this drawing, so pick the level first.
|
|
</span>
|
|
</div>
|
|
<ShopFloorMap
|
|
:pickerMode="true"
|
|
:levelid="pickerLevelId"
|
|
:initialPosition="form.mapx !== null ? { left: form.mapx, top: form.mapy } : null"
|
|
:theme="currentTheme"
|
|
@positionPicked="handlePositionPicked"
|
|
/>
|
|
</div>
|
|
<template #footer>
|
|
<button class="btn btn-secondary" @click="showMapPicker = false">Cancel</button>
|
|
<button class="btn btn-primary" @click="confirmMapPosition">Confirm Location</button>
|
|
</template>
|
|
</Modal>
|
|
|
|
<!-- Site-defined custom fields for computers -->
|
|
<CustomFieldsInputs ref="customFieldsRef" :assettypeid="COMPUTER_ASSETTYPEID" :assetid="currentAssetId" />
|
|
|
|
<div v-if="error" class="error-message">{{ error }}</div>
|
|
|
|
<div style="display: flex; gap: 0.5rem; margin-top: 1.5rem;">
|
|
<button type="submit" class="btn btn-primary" :disabled="saving">
|
|
{{ saving ? 'Saving...' : 'Save PC' }}
|
|
</button>
|
|
<router-link to="/pcs" class="btn btn-secondary">Cancel</router-link>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted, computed, watch } from 'vue'
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
import { computersApi, assetsApi, vendorsApi, locationsApi, modelsApi, operatingsystemsApi } from '@/api'
|
|
import ShopFloorMap from '@/components/ShopFloorMap.vue'
|
|
import { loadMapConfig, levelOptions, levelName, state as mapConfig }
|
|
from '@/composables/mapConfig'
|
|
import Modal from '@/components/Modal.vue'
|
|
import CustomFieldsInputs from '@/components/CustomFieldsInputs.vue'
|
|
import { currentTheme } from '@/stores/theme'
|
|
import { useIdentifierFlags } from '@/composables/identifierSettings'
|
|
import { apiError } from '@/utils/apiError'
|
|
|
|
const { isEnabled } = useIdentifierFlags()
|
|
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
|
|
const isEdit = computed(() => !!route.params.id)
|
|
|
|
// Seeded asset-type id for computers (see /api/assets/types). Custom-field
|
|
// values are keyed by the underlying asset id, captured on load / create.
|
|
const COMPUTER_ASSETTYPEID = 2
|
|
const customFieldsRef = ref(null)
|
|
const currentAssetId = ref(null)
|
|
|
|
// PC Number (assetnumber) defaults to the serial number while the user hasn't
|
|
// typed their own. Editable; only auto-fills on a new PC.
|
|
const manualPcNumber = ref(false)
|
|
function onPcNumberInput() {
|
|
manualPcNumber.value = true
|
|
}
|
|
|
|
const loading = ref(true)
|
|
const saving = ref(false)
|
|
const error = ref('')
|
|
const showMapPicker = ref(false)
|
|
// Which drawing the picker shows, and therefore which level the coordinates it
|
|
// returns belong to (ADR-017). Opens on the position's existing level so editing
|
|
// a marker does not silently move it to the default one.
|
|
const pickerLevelId = ref(null)
|
|
const tempMapPosition = ref(null)
|
|
|
|
const form = ref({
|
|
machinenumber: '',
|
|
alias: '',
|
|
hostname: '',
|
|
serialnumber: '',
|
|
gaugelabreference: '',
|
|
maintenancereference: '',
|
|
machinetypeid: '',
|
|
statusid: '',
|
|
vendorid: '',
|
|
modelnumberid: '',
|
|
locationid: '',
|
|
osid: '',
|
|
loggedinuser: '',
|
|
accessmethods: [],
|
|
notes: '',
|
|
mapx: null,
|
|
mapy: null,
|
|
levelid: null,
|
|
ipaddress: ''
|
|
})
|
|
|
|
const pcTypes = ref([])
|
|
const protocols = ref([])
|
|
const statuses = ref([])
|
|
|
|
// Access-method editor helpers (form.accessmethods = [{protocolid, portoverride}])
|
|
function isProtocolOn(protocolid) {
|
|
return form.value.accessmethods.some(a => a.protocolid === protocolid)
|
|
}
|
|
function protocolPort(protocolid) {
|
|
const found = form.value.accessmethods.find(a => a.protocolid === protocolid)
|
|
return found && found.portoverride != null ? found.portoverride : ''
|
|
}
|
|
function toggleProtocol(protocolid, on) {
|
|
if (on) {
|
|
if (!isProtocolOn(protocolid)) {
|
|
form.value.accessmethods.push({ protocolid, portoverride: null })
|
|
}
|
|
} else {
|
|
form.value.accessmethods = form.value.accessmethods.filter(a => a.protocolid !== protocolid)
|
|
}
|
|
}
|
|
function setProtocolPort(protocolid, value) {
|
|
const found = form.value.accessmethods.find(a => a.protocolid === protocolid)
|
|
if (found) {
|
|
const n = parseInt(value, 10)
|
|
found.portoverride = Number.isFinite(n) ? n : null
|
|
}
|
|
}
|
|
const vendors = ref([])
|
|
const models = ref([])
|
|
const locations = ref([])
|
|
const operatingsystems = ref([])
|
|
|
|
// Default PC Number to serial while the user hasn't typed their own (new PC only)
|
|
watch(() => form.value.serialnumber, (serial) => {
|
|
if (!isEdit.value && !manualPcNumber.value && serial) {
|
|
form.value.machinenumber = serial
|
|
}
|
|
})
|
|
|
|
// Filter models by selected vendor and PC type
|
|
const filteredModels = computed(() => {
|
|
// filter by vendor only (PC type now maps to computertypeid, a different id
|
|
// space than a model's machinetypeid)
|
|
if (!form.value.vendorid) return models.value
|
|
return models.value.filter(m => m.vendorid === form.value.vendorid)
|
|
})
|
|
|
|
onMounted(async () => {
|
|
try {
|
|
// Load reference data
|
|
// perpage 100 so dropdowns aren't truncated to the default 20-row page
|
|
const [ptRes, statusRes, vendorRes, allModels, locRes, osRes, protoRes] = await Promise.all([
|
|
computersApi.types.list({ perpage: 100 }),
|
|
assetsApi.statuses.list(),
|
|
vendorsApi.list({ perpage: 100 }),
|
|
modelsApi.listAll(), // backend caps perpage at 100; page through all
|
|
locationsApi.list({ perpage: 100 }),
|
|
operatingsystemsApi.list({ perpage: 100 }),
|
|
computersApi.protocols.list()
|
|
])
|
|
|
|
pcTypes.value = ptRes.data.data || []
|
|
statuses.value = statusRes.data.data || []
|
|
vendors.value = vendorRes.data.data || []
|
|
models.value = allModels
|
|
locations.value = locRes.data.data || []
|
|
operatingsystems.value = osRes.data.data || []
|
|
protocols.value = protoRes.data.data || []
|
|
|
|
// Load PC if editing (asset-based shape: extension under pc.computer)
|
|
if (isEdit.value) {
|
|
const response = await computersApi.get(route.params.id)
|
|
const pc = response.data.data
|
|
const ext = pc.computer || {}
|
|
currentAssetId.value = pc.assetid || null
|
|
|
|
const primaryComm = pc.communications?.find(c => c.isprimary) || pc.communications?.[0]
|
|
|
|
form.value = {
|
|
machinenumber: pc.assetnumber || '',
|
|
alias: pc.name && pc.name.toUpperCase() !== 'NONE' ? pc.name : '',
|
|
hostname: ext.hostname || '',
|
|
serialnumber: pc.serialnumber || '',
|
|
gaugelabreference: pc.gaugelabreference || '',
|
|
maintenancereference: pc.maintenancereference || '',
|
|
machinetypeid: ext.computertypeid || '',
|
|
statusid: pc.statusid || '',
|
|
vendorid: ext.vendorid || '',
|
|
modelnumberid: ext.modelnumberid || '',
|
|
locationid: pc.locationid || '',
|
|
osid: ext.osid || '',
|
|
loggedinuser: ext.loggedinuser || '',
|
|
accessmethods: (pc.accessmethods || []).map(a => ({
|
|
protocolid: a.protocolid,
|
|
portoverride: a.portoverride ?? null
|
|
})),
|
|
notes: pc.notes || '',
|
|
mapx: pc.mapx ?? null,
|
|
mapy: pc.mapy ?? null,
|
|
levelid: pc.levelid ?? null,
|
|
ipaddress: primaryComm?.ipaddress || ''
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error('Error loading data:', err)
|
|
error.value = 'Failed to load data'
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
})
|
|
|
|
function handlePositionPicked(position) {
|
|
tempMapPosition.value = position
|
|
}
|
|
|
|
function openMapPicker() {
|
|
loadMapConfig().then(() => {
|
|
pickerLevelId.value = form.value.levelid || mapConfig.defaultlevelid
|
|
showMapPicker.value = true
|
|
})
|
|
}
|
|
|
|
function confirmMapPosition() {
|
|
if (tempMapPosition.value) {
|
|
form.value.mapx = tempMapPosition.value.left
|
|
form.value.mapy = tempMapPosition.value.top
|
|
// Never one without the other: coordinates saved with no level render as
|
|
// "level unknown", and coordinates saved against the wrong level render
|
|
// convincingly in the wrong place.
|
|
form.value.levelid = pickerLevelId.value
|
|
}
|
|
showMapPicker.value = false
|
|
}
|
|
|
|
function clearMapPosition() {
|
|
form.value.mapx = null
|
|
form.value.mapy = null
|
|
form.value.levelid = null
|
|
tempMapPosition.value = null
|
|
}
|
|
|
|
async function savePC() {
|
|
error.value = ''
|
|
saving.value = true
|
|
|
|
try {
|
|
// One payload for the computers plugin (asset core + computer extension +
|
|
// primary IP). "PC Number" is the business identifier (assetnumber).
|
|
const payload = {
|
|
assetnumber: form.value.machinenumber,
|
|
hostname: form.value.hostname || null,
|
|
serialnumber: form.value.serialnumber || null,
|
|
gaugelabreference: form.value.gaugelabreference || null,
|
|
maintenancereference: form.value.maintenancereference || null,
|
|
computertypeid: form.value.machinetypeid || null,
|
|
statusid: form.value.statusid || null,
|
|
vendorid: form.value.vendorid || null,
|
|
modelnumberid: form.value.modelnumberid || null,
|
|
locationid: form.value.locationid || null,
|
|
osid: form.value.osid || null,
|
|
loggedinuser: form.value.loggedinuser || null,
|
|
accessmethods: form.value.accessmethods,
|
|
notes: form.value.notes || null,
|
|
ipaddress: form.value.ipaddress || null,
|
|
mapx: form.value.mapx,
|
|
mapy: form.value.mapy
|
|
}
|
|
// only set display name when an alias is given, so we don't clobber it
|
|
if (form.value.alias) {
|
|
payload.name = form.value.alias
|
|
}
|
|
|
|
let assetId = currentAssetId.value
|
|
if (isEdit.value) {
|
|
const response = await computersApi.update(route.params.id, payload)
|
|
assetId = assetId || response.data?.data?.assetid || response.data?.data?.asset?.assetid
|
|
} else {
|
|
const response = await computersApi.create(payload)
|
|
assetId = response.data?.data?.assetid || response.data?.data?.asset?.assetid
|
|
}
|
|
|
|
// Persist any custom-field values now that we have an asset id.
|
|
if (assetId && customFieldsRef.value) {
|
|
try {
|
|
await customFieldsRef.value.save(assetId)
|
|
} catch (cfErr) {
|
|
console.error('Error saving custom fields:', cfErr)
|
|
}
|
|
}
|
|
|
|
router.push('/pcs')
|
|
} catch (err) {
|
|
console.error('Error saving PC:', err)
|
|
error.value = apiError(err, 'Failed to save PC')
|
|
} finally {
|
|
saving.value = false
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.protocol-list {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 14px;
|
|
}
|
|
.protocol-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
.protocol-item .port-override {
|
|
width: 78px;
|
|
padding: 4px 6px;
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
}
|
|
.muted {
|
|
color: var(--text-light);
|
|
}
|
|
|
|
.map-location-control {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.current-position {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
padding: 0.5rem 1rem;
|
|
background: var(--bg);
|
|
border: 1px solid var(--border);
|
|
border-radius: 4px;
|
|
font-family: monospace;
|
|
color: var(--text);
|
|
}
|
|
|
|
.map-modal-content {
|
|
height: calc(90vh - 140px);
|
|
}
|
|
|
|
.map-modal-content :deep(.shopfloor-map) {
|
|
height: 100%;
|
|
}
|
|
|
|
.map-modal-content :deep(.map-container) {
|
|
height: calc(100% - 50px);
|
|
}
|
|
|
|
.form-hint {
|
|
display: block;
|
|
margin-top: 0.25rem;
|
|
font-size: 0.8rem;
|
|
color: var(--text-light, #666);
|
|
}
|
|
</style>
|