displays: single display type with IP-driven role (dashboard/lobby/kiosk)

One 'display' image resolves what it shows from its own IP, like the existing
visitor-location BU mapping. Extend DashboardDefault with displayrole
(dashboard|lobby|partskiosk; migration 7d28, businessunitid now nullable since
only the dashboard role needs one) + a role->path map. New unauthenticated
GET /api/dashboarddefaults/display-role returns {role, path, businessunitid}
for the caller IP. Settings UI gains a Display selector, showing the business
unit only for the dashboard role.
This commit is contained in:
cproudlock
2026-07-21 09:49:14 -04:00
parent b05fa33278
commit 60e2947fc7
5 changed files with 218 additions and 21 deletions

View File

@@ -6,9 +6,9 @@
</div>
<p class="setting-description">
Map a kiosk/lobby display's IP address to the business unit it should show.
The shopfloor dashboard auto-selects this business unit by the display's IP
when none is chosen.
Map a display PC's IP address to what it should show: the shopfloor
dashboard (and which business unit), the lobby slideshow, or the 3D parts
kiosk. A single "display" image resolves its role from its own IP.
</p>
<div class="card">
@@ -20,6 +20,7 @@
<thead>
<tr>
<th>IP Address</th>
<th>Display</th>
<th>Business Unit</th>
<th>Description</th>
<th>Actions</th>
@@ -28,6 +29,7 @@
<tbody>
<tr v-for="d in items" :key="d.dashboarddefaultid">
<td class="mono">{{ d.ipaddress }}</td>
<td>{{ roleLabel(d.displayrole) }}</td>
<td>{{ d.businessunit || '-' }}</td>
<td class="cell-truncate" :title="d.description">{{ d.description || '-' }}</td>
<td class="actions">
@@ -36,7 +38,7 @@
</td>
</tr>
<tr v-if="items.length === 0">
<td colspan="4" style="text-align: center; color: var(--text-light);">
<td colspan="5" style="text-align: center; color: var(--text-light);">
No mappings yet
</td>
</tr>
@@ -60,8 +62,15 @@
placeholder="e.g., 10.20.30.40" required />
</div>
<div class="form-group">
<label for="displayrole">Display *</label>
<select id="displayrole" v-model="form.displayrole" class="form-control" required>
<option v-for="r in roleOptions" :key="r.value" :value="r.value">{{ r.label }}</option>
</select>
</div>
<div class="form-group" v-if="form.displayrole === 'dashboard'">
<label for="businessunitid">Business Unit *</label>
<select id="businessunitid" v-model="form.businessunitid" class="form-control" required>
<select id="businessunitid" v-model="form.businessunitid" class="form-control"
:required="form.displayrole === 'dashboard'">
<option value="">Select business unit...</option>
<option v-for="bu in businessUnits" :key="bu.businessunitid" :value="bu.businessunitid">
{{ bu.businessunit }}
@@ -108,6 +117,15 @@ import { useToast } from '../../composables/toast'
import { apiError } from '../../utils/apiError'
const toast = useToast()
const roleOptions = [
{ value: 'dashboard', label: 'Shopfloor Dashboard' },
{ value: 'lobby', label: 'Lobby Slideshow' },
{ value: 'partskiosk', label: '3D Parts Kiosk' },
]
function roleLabel(value) {
return roleOptions.find(r => r.value === value)?.label || value
}
const items = ref([])
const businessUnits = ref([])
const loading = ref(true)
@@ -120,7 +138,7 @@ const error = ref('')
const showDeleteModal = ref(false)
const toDelete = ref(null)
const form = ref({ ipaddress: '', businessunitid: '', description: '' })
const form = ref({ ipaddress: '', displayrole: 'dashboard', businessunitid: '', description: '' })
onMounted(async () => {
try {
@@ -148,9 +166,10 @@ function openModal(item = null) {
editing.value = item
form.value = item ? {
ipaddress: item.ipaddress || '',
displayrole: item.displayrole || 'dashboard',
businessunitid: item.businessunitid || '',
description: item.description || ''
} : { ipaddress: '', businessunitid: '', description: '' }
} : { ipaddress: '', displayrole: 'dashboard', businessunitid: '', description: '' }
error.value = ''
showModal.value = true
}