Pick a network device's map position, and stop hardcoding one site's label prefix
Two things a second site ran into. The network device form asked for the map position as two raw numbers, so placing a device meant reading coordinates off another screen and typing them in. Machines, PCs and printers have had a "Set Location on Map" picker all along, and the network API already accepted mapx and mapy - only the form was missing. Same picker, same modal. The 3D parts kiosk hardcoded 'WJ' as the prefix shown before the number box, with a comment inviting whoever needed something else to edit the source. That is West Jefferson's gage-lab tag format and nobody else's, so another site's operators were told to expect letters that are not on their labels. It is now printedparts_label_prefix, set in Settings, defaulting to EMPTY - a site that has not set one sees no prefix rather than inheriting another site's convention. West Jefferson sets it to WJ once. The kiosk hides the prefix entirely when unset and falls back to no prefix if the setting cannot be read, because a cosmetic hint must never stop a kiosk working. Not to be confused with printedparts_code_prefix, which mints item codes like 3DP0042 and was already configurable. That is the code we generate; this is the tag already printed on the label.
This commit is contained in:
@@ -205,30 +205,41 @@
|
|||||||
<!-- Map Position (optional) -->
|
<!-- Map Position (optional) -->
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>Map Position (Optional)</legend>
|
<legend>Map Position (Optional)</legend>
|
||||||
<div class="form-row">
|
<!--
|
||||||
<div class="form-group">
|
Picked on the floor plan, the same way machines, PCs and printers do
|
||||||
<label for="mapx">Map X Position</label>
|
it. This form used to ask for the X and Y as raw numbers, which meant
|
||||||
<input
|
reading coordinates off another screen and typing them in.
|
||||||
id="mapx"
|
-->
|
||||||
v-model.number="form.mapx"
|
<div class="form-group">
|
||||||
type="number"
|
<label>Map Location</label>
|
||||||
class="form-control"
|
<div class="map-location-control">
|
||||||
min="0"
|
<div v-if="form.mapx !== null && form.mapy !== null" class="current-position">
|
||||||
/>
|
Position: {{ form.mapx }}, {{ form.mapy }}
|
||||||
</div>
|
<button type="button" class="btn btn-sm btn-secondary" @click="clearMapPosition">Clear</button>
|
||||||
<div class="form-group">
|
</div>
|
||||||
<label for="mapy">Map Y Position</label>
|
<button type="button" class="btn btn-secondary" @click="showMapPicker = true">
|
||||||
<input
|
Set Location on Map
|
||||||
id="mapy"
|
</button>
|
||||||
v-model.number="form.mapy"
|
|
||||||
type="number"
|
|
||||||
class="form-control"
|
|
||||||
min="0"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
|
<!-- Map Picker Modal -->
|
||||||
|
<Modal v-model="showMapPicker" title="Select Location on Map" size="fullscreen">
|
||||||
|
<div class="map-modal-content">
|
||||||
|
<ShopFloorMap
|
||||||
|
:pickerMode="true"
|
||||||
|
: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>
|
||||||
|
|
||||||
<!-- Notes -->
|
<!-- Notes -->
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>Notes</legend>
|
<legend>Notes</legend>
|
||||||
@@ -274,11 +285,16 @@ import {
|
|||||||
businessunitsApi
|
businessunitsApi
|
||||||
} from '@/api'
|
} from '@/api'
|
||||||
import CustomFieldsInputs from '@/components/CustomFieldsInputs.vue'
|
import CustomFieldsInputs from '@/components/CustomFieldsInputs.vue'
|
||||||
|
import ShopFloorMap from '@/components/ShopFloorMap.vue'
|
||||||
|
import Modal from '@/components/Modal.vue'
|
||||||
|
import { currentTheme } from '@/stores/theme'
|
||||||
import { useIdentifierFlags } from '@/composables/identifierSettings'
|
import { useIdentifierFlags } from '@/composables/identifierSettings'
|
||||||
import { apiError } from '@/utils/apiError'
|
import { apiError } from '@/utils/apiError'
|
||||||
|
|
||||||
const { isEnabled } = useIdentifierFlags()
|
const { isEnabled } = useIdentifierFlags()
|
||||||
|
|
||||||
|
const showMapPicker = ref(false)
|
||||||
|
const tempMapPosition = ref(null)
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
@@ -483,6 +499,24 @@ function cancel() {
|
|||||||
router.push('/network')
|
router.push('/network')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
function handlePositionPicked(position) {
|
||||||
|
tempMapPosition.value = position
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmMapPosition() {
|
||||||
|
if (tempMapPosition.value) {
|
||||||
|
form.value.mapx = tempMapPosition.value.left
|
||||||
|
form.value.mapy = tempMapPosition.value.top
|
||||||
|
}
|
||||||
|
showMapPicker.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearMapPosition() {
|
||||||
|
form.value.mapx = null
|
||||||
|
form.value.mapy = null
|
||||||
|
tempMapPosition.value = null
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
<!-- The tag prefix is shown as a fixed addon so operators type only
|
<!-- The tag prefix is shown as a fixed addon so operators type only
|
||||||
the number part off the label. -->
|
the number part off the label. -->
|
||||||
<div class="entry-input-group">
|
<div class="entry-input-group">
|
||||||
<span class="entry-prefix">{{ LABEL_PREFIX }}</span>
|
<span v-if="LABEL_PREFIX" class="entry-prefix">{{ LABEL_PREFIX }}</span>
|
||||||
<input ref="entryInput" v-model="manualCode" class="entry-input has-prefix"
|
<input ref="entryInput" v-model="manualCode" class="entry-input has-prefix"
|
||||||
inputmode="none" autocomplete="off" placeholder="number"
|
inputmode="none" autocomplete="off" placeholder="number"
|
||||||
@keydown.enter.prevent="lookupItem(manualCode)" />
|
@keydown.enter.prevent="lookupItem(manualCode)" />
|
||||||
@@ -92,13 +92,16 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue'
|
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue'
|
||||||
import { printedpartsApi } from '@/api'
|
import { printedpartsApi, settingsApi } from '@/api'
|
||||||
import { withBase } from '@/utils/basePath'
|
import { withBase } from '@/utils/basePath'
|
||||||
import TouchKeypad from '@/components/TouchKeypad.vue'
|
import TouchKeypad from '@/components/TouchKeypad.vue'
|
||||||
|
|
||||||
// Fixed tag prefix shown before the label number at the kiosk. Change here if
|
// Leading text on the physical labels, shown before the number box so operators
|
||||||
// the gage lab tags use a different leading string.
|
// type only the digits. It was hardcoded to 'WJ', which is West Jefferson's tag
|
||||||
const LABEL_PREFIX = 'WJ'
|
// format and nobody else's - on another site's kiosk it told operators to expect
|
||||||
|
// letters that are not on their labels. Display only: the lookup sends whatever
|
||||||
|
// was typed, unprefixed.
|
||||||
|
const LABEL_PREFIX = ref('')
|
||||||
|
|
||||||
const step = ref('item')
|
const step = ref('item')
|
||||||
const item = ref(null)
|
const item = ref(null)
|
||||||
@@ -113,7 +116,20 @@ const scannedRevision = ref(null)
|
|||||||
const entryInput = ref(null)
|
const entryInput = ref(null)
|
||||||
let resetTimer = null
|
let resetTimer = null
|
||||||
|
|
||||||
onMounted(focusEntry)
|
onMounted(async () => {
|
||||||
|
// A kiosk left running for weeks reads this once at load, which is fine: the
|
||||||
|
// label format is a property of the site's physical tags, not something that
|
||||||
|
// changes during a shift.
|
||||||
|
try {
|
||||||
|
const response = await settingsApi.get('printedparts_label_prefix')
|
||||||
|
LABEL_PREFIX.value = (response.data?.data?.value || '').trim()
|
||||||
|
} catch (err) {
|
||||||
|
// Never block the kiosk over a cosmetic hint. An unreachable or unseeded
|
||||||
|
// setting simply means no prefix is shown.
|
||||||
|
LABEL_PREFIX.value = ''
|
||||||
|
}
|
||||||
|
focusEntry()
|
||||||
|
})
|
||||||
onBeforeUnmount(() => clearTimeout(resetTimer))
|
onBeforeUnmount(() => clearTimeout(resetTimer))
|
||||||
|
|
||||||
function focusEntry(event) {
|
function focusEntry(event) {
|
||||||
|
|||||||
@@ -18,6 +18,17 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Kiosk label prefix</label>
|
||||||
|
<input v-model="values.printedparts_label_prefix" type="text"
|
||||||
|
class="form-control" maxlength="8" placeholder="none" />
|
||||||
|
<p class="field-hint">
|
||||||
|
Leading text on this site's physical labels, shown at the kiosk before the
|
||||||
|
number box so operators type only the digits. Leave it empty to show no
|
||||||
|
prefix. This is about the printed tags, not the item codes above.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Default low-stock threshold</label>
|
<label>Default low-stock threshold</label>
|
||||||
<input v-model.number="values.printedparts_default_threshold"
|
<input v-model.number="values.printedparts_default_threshold"
|
||||||
@@ -103,6 +114,7 @@ import { settingsApi, usersApi, supportteamsApi } from '@/api'
|
|||||||
|
|
||||||
const KEYS = [
|
const KEYS = [
|
||||||
'printedparts_code_prefix',
|
'printedparts_code_prefix',
|
||||||
|
'printedparts_label_prefix',
|
||||||
'printedparts_default_threshold',
|
'printedparts_default_threshold',
|
||||||
'printedparts_unknown_badge',
|
'printedparts_unknown_badge',
|
||||||
'printedparts_alert_email',
|
'printedparts_alert_email',
|
||||||
@@ -113,6 +125,7 @@ const KEYS = [
|
|||||||
|
|
||||||
const values = ref({
|
const values = ref({
|
||||||
printedparts_code_prefix: '3DP',
|
printedparts_code_prefix: '3DP',
|
||||||
|
printedparts_label_prefix: '',
|
||||||
printedparts_default_threshold: 5,
|
printedparts_default_threshold: 5,
|
||||||
printedparts_unknown_badge: 'deny',
|
printedparts_unknown_badge: 'deny',
|
||||||
printedparts_alert_email: '',
|
printedparts_alert_email: '',
|
||||||
|
|||||||
@@ -126,6 +126,10 @@ class PrintedpartsPlugin(BasePlugin):
|
|||||||
defaults = [
|
defaults = [
|
||||||
('printedparts_code_prefix', '3DP', 'string',
|
('printedparts_code_prefix', '3DP', 'string',
|
||||||
'Prefix for generated item codes'),
|
'Prefix for generated item codes'),
|
||||||
|
('printedparts_label_prefix', '', 'string',
|
||||||
|
'Leading text on the physical gage-lab labels, shown at the kiosk '
|
||||||
|
'before the number box so operators type only the digits. Empty '
|
||||||
|
'shows no prefix. Site-specific: West Jefferson labels read WJ'),
|
||||||
('printedparts_default_threshold', '5', 'integer',
|
('printedparts_default_threshold', '5', 'integer',
|
||||||
'Default low-stock threshold for new items'),
|
'Default low-stock threshold for new items'),
|
||||||
('printedparts_unknown_badge', 'deny', 'string',
|
('printedparts_unknown_badge', 'deny', 'string',
|
||||||
|
|||||||
Reference in New Issue
Block a user