Flask + Vue rewrite of the classic-ASP shopdb, built as a plugin framework so sister GE Aerospace sites can adopt it. Product version 0.7.0-pre-0.8.0, plugin contract 0.11.0, 966 backend tests. Key entry points: - docs/INSTALL-WINDOWS-IIS.md - tested Windows + IIS install runbook (own-site and subpath deployment methods) - docs/PILOT-DEPLOY.md - production pilot: stand up + import + verify - docs/IMPORT-API.md - schema-agnostic legacy data import contract - scripts/site_imports/wjf/ - West Jefferson reference import loader - docs/adr/ - twelve accepted architecture decision records
191 lines
6.1 KiB
Vue
191 lines
6.1 KiB
Vue
<template>
|
|
<div>
|
|
<div class="page-header">
|
|
<h2>Dashboard Defaults</h2>
|
|
<button class="btn btn-primary" @click="openModal()">+ Add Mapping</button>
|
|
</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.
|
|
</p>
|
|
|
|
<div class="card">
|
|
<div v-if="loading" class="loading">Loading...</div>
|
|
|
|
<template v-else>
|
|
<div class="table-container">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>IP Address</th>
|
|
<th>Business Unit</th>
|
|
<th>Description</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="d in items" :key="d.dashboarddefaultid">
|
|
<td class="mono">{{ d.ipaddress }}</td>
|
|
<td>{{ d.businessunit || '-' }}</td>
|
|
<td class="cell-truncate" :title="d.description">{{ d.description || '-' }}</td>
|
|
<td class="actions">
|
|
<button class="btn btn-secondary btn-sm" @click="openModal(d)">Edit</button>
|
|
<button class="btn btn-danger btn-sm" @click="confirmDelete(d)">Delete</button>
|
|
</td>
|
|
</tr>
|
|
<tr v-if="items.length === 0">
|
|
<td colspan="4" style="text-align: center; color: var(--text-light);">
|
|
No mappings yet
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
<!-- Add/Edit Modal -->
|
|
<div v-if="showModal" class="modal-overlay" @click.self="closeModal">
|
|
<div class="modal">
|
|
<div class="modal-header">
|
|
<h3>{{ editing ? 'Edit Mapping' : 'Add Mapping' }}</h3>
|
|
</div>
|
|
<form @submit.prevent="save">
|
|
<div class="modal-body">
|
|
<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., 10.20.30.40" required />
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="businessunitid">Business Unit *</label>
|
|
<select id="businessunitid" v-model="form.businessunitid" class="form-control" required>
|
|
<option value="">Select business unit...</option>
|
|
<option v-for="bu in businessUnits" :key="bu.businessunitid" :value="bu.businessunitid">
|
|
{{ bu.businessunit }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="description">Description</label>
|
|
<input id="description" v-model="form.description" type="text" class="form-control"
|
|
placeholder="e.g., Materials lobby kiosk" />
|
|
</div>
|
|
<div v-if="error" class="error-message">{{ error }}</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" @click="closeModal">Cancel</button>
|
|
<button type="submit" class="btn btn-primary" :disabled="saving">
|
|
{{ saving ? 'Saving...' : 'Save' }}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Delete Modal -->
|
|
<div v-if="showDeleteModal" class="modal-overlay" @click.self="showDeleteModal = false">
|
|
<div class="modal">
|
|
<div class="modal-header"><h3>Delete Mapping</h3></div>
|
|
<div class="modal-body">
|
|
<p>Delete the mapping for <strong>{{ toDelete?.ipaddress }}</strong>?</p>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button class="btn btn-secondary" @click="showDeleteModal = false">Cancel</button>
|
|
<button class="btn btn-danger" @click="deleteItem">Delete</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted } from 'vue'
|
|
import { dashboardDefaultsApi, businessUnitsApi } from '../../api'
|
|
import { useToast } from '../../composables/toast'
|
|
import { apiError } from '../../utils/apiError'
|
|
const toast = useToast()
|
|
|
|
const items = ref([])
|
|
const businessUnits = ref([])
|
|
const loading = ref(true)
|
|
|
|
const showModal = ref(false)
|
|
const editing = ref(null)
|
|
const saving = ref(false)
|
|
const error = ref('')
|
|
|
|
const showDeleteModal = ref(false)
|
|
const toDelete = ref(null)
|
|
|
|
const form = ref({ ipaddress: '', businessunitid: '', description: '' })
|
|
|
|
onMounted(async () => {
|
|
try {
|
|
const response = await businessUnitsApi.list({ perpage: 100 })
|
|
businessUnits.value = response.data.data || []
|
|
} catch (err) {
|
|
console.error('Error loading business units:', err)
|
|
}
|
|
await loadData()
|
|
})
|
|
|
|
async function loadData() {
|
|
loading.value = true
|
|
try {
|
|
const response = await dashboardDefaultsApi.list()
|
|
items.value = response.data.data || []
|
|
} catch (err) {
|
|
console.error('Error loading dashboard defaults:', err)
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
function openModal(item = null) {
|
|
editing.value = item
|
|
form.value = item ? {
|
|
ipaddress: item.ipaddress || '',
|
|
businessunitid: item.businessunitid || '',
|
|
description: item.description || ''
|
|
} : { ipaddress: '', businessunitid: '', description: '' }
|
|
error.value = ''
|
|
showModal.value = true
|
|
}
|
|
|
|
function closeModal() { showModal.value = false; editing.value = null }
|
|
|
|
async function save() {
|
|
error.value = ''
|
|
saving.value = true
|
|
try {
|
|
if (editing.value) {
|
|
await dashboardDefaultsApi.update(editing.value.dashboarddefaultid, form.value)
|
|
} else {
|
|
await dashboardDefaultsApi.create(form.value)
|
|
}
|
|
closeModal()
|
|
loadData()
|
|
} catch (err) {
|
|
error.value = apiError(err, 'Failed to save')
|
|
} finally {
|
|
saving.value = false
|
|
}
|
|
}
|
|
|
|
function confirmDelete(item) { toDelete.value = item; showDeleteModal.value = true }
|
|
|
|
async function deleteItem() {
|
|
try {
|
|
await dashboardDefaultsApi.delete(toDelete.value.dashboarddefaultid)
|
|
showDeleteModal.value = false
|
|
toDelete.value = null
|
|
loadData()
|
|
} catch (err) {
|
|
toast.error('Failed to delete')
|
|
}
|
|
}
|
|
</script>
|