Follow-up to the application-picker fix. Three of these were already wrong on data that exists today, not merely latent. Batch label printing is the worst of them: AssetLabelBatch asked for 500 machines or PCs, got 100, and printed a sheet that looked complete. With 262 machines and 290 PCs in the catalogue that is a physically short run with no error anywhere - the operator finds out at the label printer, or later at the bay with no label on it. PrinterQRBatch, USBLabelBatch and PrintedPartsLabels had the same shape and are fixed alongside it, before their tables cross 100 too. MachineForm's "controls" PC dropdown offered the first 100 of 290, so a machine could not be linked to a PC sorting late in the list. NetworkDeviceForm had it for models, which are already past 100 - and the same file already called modelsApi.listAll() correctly two lines away. Adds listAll() to the machines, computers, printers, network, measuring-tools, USB and printed-parts APIs, all delegating to fetchAllPages(). Still outstanding: callers of vendors, locations, business units and the type catalogues that ask for more than 100. Those tables are all well under the cap today, so they are correct for now and wrong the day they are not.
740 lines
24 KiB
Vue
740 lines
24 KiB
Vue
<template>
|
|
<div>
|
|
<div class="page-header">
|
|
<h2>{{ isEdit ? 'Edit Machine' : 'New Machine' }}</h2>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div v-if="loading" class="loading">Loading...</div>
|
|
|
|
<form v-else @submit.prevent="saveMachine">
|
|
<!-- Identity Section -->
|
|
<h3 class="form-section-title">Identity</h3>
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label for="assetnumber">Asset Number *</label>
|
|
<input
|
|
id="assetnumber"
|
|
v-model="form.assetnumber"
|
|
type="text"
|
|
class="form-control"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="name">Name / Alias</label>
|
|
<input
|
|
id="name"
|
|
v-model="form.name"
|
|
type="text"
|
|
class="form-control"
|
|
/>
|
|
<small class="form-help">Layperson-friendly label</small>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-row" v-if="isEnabled('gaugelabreference', 'machine') || isEnabled('maintenancereference', 'machine')">
|
|
<div class="form-group" v-if="isEnabled('gaugelabreference', 'machine')">
|
|
<label for="gaugelabreference">Gauge Lab Reference</label>
|
|
<input
|
|
id="gaugelabreference"
|
|
v-model="form.gaugelabreference"
|
|
type="text"
|
|
class="form-control"
|
|
/>
|
|
<small class="form-help">Authoritative gauge lab asset reference (if tracked)</small>
|
|
</div>
|
|
|
|
<div class="form-group" v-if="isEnabled('maintenancereference', 'machine')">
|
|
<label for="maintenancereference">Maintenance Reference</label>
|
|
<input
|
|
id="maintenancereference"
|
|
v-model="form.maintenancereference"
|
|
type="text"
|
|
class="form-control"
|
|
/>
|
|
<small class="form-help">Maintenance system asset reference (if tracked)</small>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label for="serialnumber">Serial Number</label>
|
|
<input
|
|
id="serialnumber"
|
|
v-model="form.serialnumber"
|
|
type="text"
|
|
class="form-control"
|
|
/>
|
|
</div>
|
|
|
|
<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>
|
|
|
|
<!-- Machine Hardware Section -->
|
|
<h3 class="form-section-title">Machine Hardware</h3>
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label for="machinetypeid">Machine Type</label>
|
|
<select
|
|
id="machinetypeid"
|
|
v-model="form.machinetypeid"
|
|
class="form-control"
|
|
>
|
|
<option value="">Select type...</option>
|
|
<option
|
|
v-for="mt in machineTypes"
|
|
:key="mt.machinetypeid"
|
|
:value="mt.machinetypeid"
|
|
>
|
|
{{ mt.machinetype }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="vendorid">Vendor</label>
|
|
<select
|
|
id="vendorid"
|
|
v-model="form.vendorid"
|
|
class="form-control"
|
|
>
|
|
<option value="">Select vendor...</option>
|
|
<option
|
|
v-for="v in vendors"
|
|
:key="v.vendorid"
|
|
:value="v.vendorid"
|
|
>
|
|
{{ v.vendor }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
<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 class="form-help" v-if="form.vendorid">Filtered by selected vendor</small>
|
|
</div>
|
|
|
|
<div class="form-group"></div>
|
|
</div>
|
|
|
|
<!-- Controller Section (for CNC machines) -->
|
|
<h3 class="form-section-title">Controller</h3>
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label for="controllervendorid">Controller Vendor</label>
|
|
<select
|
|
id="controllervendorid"
|
|
v-model="form.controllervendorid"
|
|
class="form-control"
|
|
>
|
|
<option value="">Select vendor...</option>
|
|
<option
|
|
v-for="v in vendors"
|
|
:key="v.vendorid"
|
|
:value="v.vendorid"
|
|
>
|
|
{{ v.vendor }}
|
|
</option>
|
|
</select>
|
|
<small class="form-help">e.g., Fanuc, Siemens, Allen-Bradley</small>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="controllermodelid">Controller Model</label>
|
|
<select
|
|
id="controllermodelid"
|
|
v-model="form.controllermodelid"
|
|
class="form-control"
|
|
>
|
|
<option value="">Select model...</option>
|
|
<option
|
|
v-for="m in filteredControllerModels"
|
|
:key="m.modelnumberid"
|
|
:value="m.modelnumberid"
|
|
>
|
|
{{ m.modelnumber }}
|
|
</option>
|
|
</select>
|
|
<small class="form-help" v-if="form.controllervendorid">Filtered by controller vendor</small>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Location Section -->
|
|
<h3 class="form-section-title">Location & Organization</h3>
|
|
<div class="form-row">
|
|
<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 class="form-group">
|
|
<label for="businessunitid">Business Unit</label>
|
|
<select
|
|
id="businessunitid"
|
|
v-model="form.businessunitid"
|
|
class="form-control"
|
|
>
|
|
<option value="">Select business unit...</option>
|
|
<option
|
|
v-for="bu in businessunits"
|
|
:key="bu.businessunitid"
|
|
:value="bu.businessunitid"
|
|
>
|
|
{{ bu.businessunit }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
</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>
|
|
|
|
<!-- Configuration Section -->
|
|
<h3 class="form-section-title">Configuration</h3>
|
|
<div class="form-row">
|
|
<div class="form-group checkbox-group">
|
|
<label>
|
|
<input type="checkbox" v-model="form.requiresmanualconfig" />
|
|
Requires Manual Config
|
|
</label>
|
|
<small class="form-help">Machine a tech must configure by hand (e.g. driven by multiple PCs)</small>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Controlling PC Selection -->
|
|
<h3 class="form-section-title">Connected PC</h3>
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label for="controllingpc">Controlling PC</label>
|
|
<select
|
|
id="controllingpc"
|
|
v-model="controllingPcId"
|
|
class="form-control"
|
|
>
|
|
<option :value="null">None (standalone)</option>
|
|
<option
|
|
v-for="pc in pcs"
|
|
:key="pc.assetid"
|
|
:value="pc.assetid"
|
|
>
|
|
{{ pc.assetnumber }}{{ pc.name ? ` (${pc.name})` : '' }}
|
|
</option>
|
|
</select>
|
|
<small class="form-help">Select the PC that controls this machine</small>
|
|
</div>
|
|
|
|
<div class="form-group" v-if="controllingPcId">
|
|
<label for="connectiontype">Connection Type</label>
|
|
<select
|
|
id="connectiontype"
|
|
v-model="relationshipTypeId"
|
|
class="form-control"
|
|
>
|
|
<option
|
|
v-for="rt in relationshipTypes"
|
|
:key="rt.relationshiptypeid"
|
|
:value="rt.relationshiptypeid"
|
|
>
|
|
{{ rt.relationshiptype }}
|
|
</option>
|
|
</select>
|
|
<small class="form-help">How the PC connects to this machine</small>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Notes Section -->
|
|
<h3 class="form-section-title">Notes</h3>
|
|
<div class="form-group">
|
|
<textarea
|
|
id="notes"
|
|
v-model="form.notes"
|
|
class="form-control"
|
|
rows="3"
|
|
placeholder="Additional notes..."
|
|
></textarea>
|
|
</div>
|
|
|
|
<!-- Site-defined custom fields for machines -->
|
|
<CustomFieldsInputs ref="customFieldsRef" :assettypeid="MACHINE_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 Machine' }}
|
|
</button>
|
|
<router-link to="/machines" 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 { machinesApi, vendorsApi, locationsApi, modelsApi, businessunitsApi, computersApi, assetsApi, relationshipTypesApi } 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 machines (see /api/assets/types).
|
|
const MACHINE_ASSETTYPEID = 1
|
|
const customFieldsRef = ref(null)
|
|
const currentAssetId = ref(null)
|
|
|
|
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({
|
|
assetnumber: '',
|
|
name: '',
|
|
gaugelabreference: '',
|
|
maintenancereference: '',
|
|
serialnumber: '',
|
|
statusid: 1,
|
|
machinetypeid: '',
|
|
vendorid: '',
|
|
modelnumberid: '',
|
|
controllervendorid: '',
|
|
controllermodelid: '',
|
|
locationid: '',
|
|
businessunitid: '',
|
|
requiresmanualconfig: false,
|
|
islocationonly: false,
|
|
notes: '',
|
|
mapx: null,
|
|
mapy: null,
|
|
levelid: null
|
|
})
|
|
|
|
const machineTypes = ref([])
|
|
const statuses = ref([])
|
|
const vendors = ref([])
|
|
const locations = ref([])
|
|
const models = ref([])
|
|
const businessunits = ref([])
|
|
const pcs = ref([])
|
|
const relationshipTypes = ref([])
|
|
const controllingPcId = ref(null)
|
|
const relationshipTypeId = ref(null)
|
|
const existingRelationshipId = ref(null)
|
|
const currentMachine = ref(null)
|
|
|
|
// Filter models by selected machine vendor
|
|
const filteredModels = computed(() => {
|
|
if (!form.value.vendorid) return models.value
|
|
return models.value.filter(m => m.vendorid === form.value.vendorid)
|
|
})
|
|
|
|
// Filter models by selected controller vendor
|
|
const filteredControllerModels = computed(() => {
|
|
if (!form.value.controllervendorid) return models.value
|
|
return models.value.filter(m => m.vendorid === form.value.controllervendorid)
|
|
})
|
|
|
|
// Clear model selection when vendor changes
|
|
watch(() => form.value.vendorid, (newVal, oldVal) => {
|
|
if (oldVal && newVal !== oldVal) {
|
|
const currentModel = models.value.find(m => m.modelnumberid === form.value.modelnumberid)
|
|
if (currentModel && currentModel.vendorid !== newVal) {
|
|
form.value.modelnumberid = ''
|
|
}
|
|
}
|
|
})
|
|
|
|
// Clear controller model when controller vendor changes
|
|
watch(() => form.value.controllervendorid, (newVal, oldVal) => {
|
|
if (oldVal && newVal !== oldVal) {
|
|
const currentModel = models.value.find(m => m.modelnumberid === form.value.controllermodelid)
|
|
if (currentModel && currentModel.vendorid !== newVal) {
|
|
form.value.controllermodelid = ''
|
|
}
|
|
}
|
|
})
|
|
|
|
onMounted(async () => {
|
|
try {
|
|
// Load reference data in parallel
|
|
const [typesRes, statusRes, vendorRes, locRes, allModels, buRes, pcsRes, relTypesRes] = await Promise.all([
|
|
machinesApi.types.list(),
|
|
assetsApi.statuses.list(),
|
|
vendorsApi.list({ perpage: 500 }),
|
|
locationsApi.list({ perpage: 500 }),
|
|
modelsApi.listAll(), // backend caps perpage at 100; page through all
|
|
businessunitsApi.list({ perpage: 500 }),
|
|
computersApi.listAll(), // backend caps perpage at 100; page through all
|
|
assetsApi.types.list() // Used for relationship types, will fix below
|
|
])
|
|
|
|
machineTypes.value = typesRes.data.data || []
|
|
statuses.value = statusRes.data.data || []
|
|
vendors.value = vendorRes.data.data || []
|
|
locations.value = locRes.data.data || []
|
|
models.value = allModels
|
|
businessunits.value = buRes.data.data || []
|
|
pcs.value = pcsRes.data.data || []
|
|
|
|
// Load relationship types separately
|
|
try {
|
|
const relRes = await relationshipTypesApi.list()
|
|
relationshipTypes.value = relRes.data.data || []
|
|
} catch (e) {
|
|
// Fallback - use hardcoded Controls type
|
|
relationshipTypes.value = [{ relationshiptypeid: 1, relationshiptype: 'Controls' }]
|
|
}
|
|
|
|
// Set default relationship type to "Controls" if available (name is data, compare folded)
|
|
const controlsType = relationshipTypes.value.find(t => (t.relationshiptype || '').toLowerCase() === 'controls')
|
|
if (controlsType) {
|
|
relationshipTypeId.value = controlsType.relationshiptypeid
|
|
}
|
|
|
|
// Load machine if editing
|
|
if (isEdit.value) {
|
|
const response = await machinesApi.get(route.params.id)
|
|
const data = response.data.data
|
|
currentAssetId.value = data.assetid || null
|
|
currentMachine.value = data
|
|
|
|
form.value = {
|
|
assetnumber: data.assetnumber || '',
|
|
name: data.name || '',
|
|
gaugelabreference: data.gaugelabreference || '',
|
|
maintenancereference: data.maintenancereference || '',
|
|
serialnumber: data.serialnumber || '',
|
|
statusid: data.statusid || 1,
|
|
machinetypeid: data.machine?.machinetypeid || '',
|
|
vendorid: data.machine?.vendorid || '',
|
|
modelnumberid: data.machine?.modelnumberid || '',
|
|
controllervendorid: data.machine?.controllervendorid || '',
|
|
controllermodelid: data.machine?.controllermodelid || '',
|
|
locationid: data.locationid || '',
|
|
businessunitid: data.businessunitid || '',
|
|
requiresmanualconfig: data.machine?.requiresmanualconfig || false,
|
|
islocationonly: data.machine?.islocationonly || false,
|
|
notes: data.notes || '',
|
|
mapx: data.mapx ?? null,
|
|
mapy: data.mapy ?? null,
|
|
levelid: data.levelid ?? null,
|
|
}
|
|
|
|
// Load existing relationships to find controlling PC
|
|
if (data.assetid) {
|
|
try {
|
|
const relResponse = await assetsApi.getRelationships(data.assetid)
|
|
const relationships = relResponse.data.data || { incoming: [], outgoing: [] }
|
|
|
|
// Check incoming relationships for a controlling PC
|
|
for (const rel of relationships.incoming || []) {
|
|
if (rel.sourceasset?.assettypename === 'computer' && (rel.relationshiptypename || '').toLowerCase() === 'controls') {
|
|
controllingPcId.value = rel.sourceasset.assetid
|
|
relationshipTypeId.value = rel.relationshiptypeid
|
|
existingRelationshipId.value = rel.assetrelationshipid
|
|
break
|
|
}
|
|
}
|
|
} catch (e) {
|
|
}
|
|
}
|
|
}
|
|
} 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 saveMachine() {
|
|
error.value = ''
|
|
saving.value = true
|
|
|
|
try {
|
|
const data = {
|
|
assetnumber: form.value.assetnumber,
|
|
name: form.value.name || null,
|
|
gaugelabreference: form.value.gaugelabreference || null,
|
|
maintenancereference: form.value.maintenancereference || null,
|
|
serialnumber: form.value.serialnumber || null,
|
|
statusid: form.value.statusid || 1,
|
|
machinetypeid: form.value.machinetypeid || null,
|
|
vendorid: form.value.vendorid || null,
|
|
modelnumberid: form.value.modelnumberid || null,
|
|
controllervendorid: form.value.controllervendorid || null,
|
|
controllermodelid: form.value.controllermodelid || null,
|
|
locationid: form.value.locationid || null,
|
|
businessunitid: form.value.businessunitid || null,
|
|
requiresmanualconfig: form.value.requiresmanualconfig,
|
|
islocationonly: form.value.islocationonly,
|
|
notes: form.value.notes || null,
|
|
mapx: form.value.mapx,
|
|
mapy: form.value.mapy
|
|
}
|
|
|
|
let savedMachine
|
|
let assetId
|
|
|
|
if (isEdit.value) {
|
|
const response = await machinesApi.update(route.params.id, data)
|
|
savedMachine = response.data.data
|
|
assetId = savedMachine.assetid
|
|
} else {
|
|
const response = await machinesApi.create(data)
|
|
savedMachine = response.data.data
|
|
assetId = savedMachine.assetid
|
|
}
|
|
|
|
// Handle relationship (controlling PC)
|
|
await saveRelationship(assetId)
|
|
|
|
// Persist custom-field values against the asset id.
|
|
if (assetId && customFieldsRef.value) {
|
|
try {
|
|
await customFieldsRef.value.save(assetId)
|
|
} catch (cfErr) {
|
|
console.error('Error saving custom fields:', cfErr)
|
|
}
|
|
}
|
|
|
|
router.push(`/machines/${savedMachine.machine?.machineid || route.params.id}`)
|
|
} catch (err) {
|
|
console.error('Error saving machine:', err)
|
|
error.value = apiError(err, 'Failed to save machine')
|
|
} finally {
|
|
saving.value = false
|
|
}
|
|
}
|
|
|
|
async function saveRelationship(assetId) {
|
|
// If no PC selected and no existing relationship, nothing to do
|
|
if (!controllingPcId.value && !existingRelationshipId.value) {
|
|
return
|
|
}
|
|
|
|
// If clearing the relationship
|
|
if (!controllingPcId.value && existingRelationshipId.value) {
|
|
await assetsApi.deleteRelationship(existingRelationshipId.value)
|
|
return
|
|
}
|
|
|
|
// If PC is selected
|
|
if (controllingPcId.value) {
|
|
// If there's an existing relationship, delete it first
|
|
if (existingRelationshipId.value) {
|
|
await assetsApi.deleteRelationship(existingRelationshipId.value)
|
|
}
|
|
|
|
// Create new relationship (PC controls Machine, so PC is source, Machine is target)
|
|
await assetsApi.createRelationship({
|
|
sourceassetid: controllingPcId.value,
|
|
targetassetid: assetId,
|
|
relationshiptypeid: relationshipTypeId.value
|
|
})
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.form-section-title {
|
|
font-size: 1rem;
|
|
font-weight: 600;
|
|
color: var(--text);
|
|
margin: 1.5rem 0 0.75rem 0;
|
|
padding-bottom: 0.5rem;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
|
|
.form-section-title:first-of-type {
|
|
margin-top: 0;
|
|
}
|
|
|
|
.checkbox-group {
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.checkbox-group label {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.checkbox-group input[type="checkbox"] {
|
|
width: 1.1rem;
|
|
height: 1.1rem;
|
|
}
|
|
|
|
.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-radius: 4px;
|
|
font-family: monospace;
|
|
}
|
|
|
|
.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-help {
|
|
display: block;
|
|
margin-top: 0.25rem;
|
|
font-size: 0.8rem;
|
|
color: var(--text-light);
|
|
}
|
|
</style>
|