Add print badges, pagination, route splitting, JWT auth fixes, and list page alignment

- Fix equipment badge barcode not rendering (loading race condition)
- Fix printer QR code not rendering on initial load (same race condition)
- Add model image to equipment badge via imageurl from Model table
- Fix white-on-white machine number text on badge, tighten barcode spacing
- Add PaginationBar component used across all list pages
- Split monolithic router into per-plugin route modules
- Fix 25 GET API endpoints returning 401 (jwt_required -> optional=True)
- Align list page columns across Equipment, PCs, and Network pages
- Add print views: EquipmentBadge, PrinterQRSingle, PrinterQRBatch, USBLabelBatch
- Add PC Relationships report, migration docs, and CLAUDE.md project guide
- Various plugin model, API, and frontend refinements

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-02-04 07:32:44 -05:00
parent fe7a00b260
commit 83caaa7b7c
89 changed files with 3951 additions and 1138 deletions

View File

@@ -3,6 +3,9 @@
<div class="page-header">
<h2>Equipment Details</h2>
<div class="header-actions">
<router-link :to="`/print/equipment-badge/${equipment?.equipment?.equipmentid}`" class="btn btn-secondary" v-if="equipment" target="_blank">
Print Badge
</router-link>
<router-link :to="`/machines/${equipment?.equipment?.equipmentid}/edit`" class="btn btn-primary" v-if="equipment">
Edit
</router-link>
@@ -22,28 +25,28 @@
</div>
<div class="hero-meta">
<span class="badge badge-lg badge-primary">
{{ equipment.assettype_name || 'Equipment' }}
{{ equipment.assettypename || 'Equipment' }}
</span>
<span class="badge badge-lg" :class="getStatusClass(equipment.status_name)">
{{ equipment.status_name || 'Unknown' }}
<span class="badge badge-lg" :class="getStatusClass(equipment.statusname)">
{{ equipment.statusname || 'Unknown' }}
</span>
</div>
<div class="hero-details">
<div class="hero-detail" v-if="equipment.equipment?.equipmenttype_name">
<div class="hero-detail" v-if="equipment.equipment?.equipmenttypename">
<span class="hero-detail-label">Type</span>
<span class="hero-detail-value">{{ equipment.equipment.equipmenttype_name }}</span>
<span class="hero-detail-value">{{ equipment.equipment.equipmenttypename }}</span>
</div>
<div class="hero-detail" v-if="equipment.equipment?.vendor_name">
<div class="hero-detail" v-if="equipment.equipment?.vendorname">
<span class="hero-detail-label">Vendor</span>
<span class="hero-detail-value">{{ equipment.equipment.vendor_name }}</span>
<span class="hero-detail-value">{{ equipment.equipment.vendorname }}</span>
</div>
<div class="hero-detail" v-if="equipment.equipment?.model_name">
<div class="hero-detail" v-if="equipment.equipment?.modelname">
<span class="hero-detail-label">Model</span>
<span class="hero-detail-value">{{ equipment.equipment.model_name }}</span>
<span class="hero-detail-value">{{ equipment.equipment.modelname }}</span>
</div>
<div class="hero-detail" v-if="equipment.location_name">
<div class="hero-detail" v-if="equipment.locationname">
<span class="hero-detail-label">Location</span>
<span class="hero-detail-value">{{ equipment.location_name }}</span>
<span class="hero-detail-value">{{ equipment.locationname }}</span>
</div>
</div>
</div>
@@ -78,30 +81,30 @@
<div class="info-list">
<div class="info-row">
<span class="info-label">Type</span>
<span class="info-value">{{ equipment.equipment?.equipmenttype_name || '-' }}</span>
<span class="info-value">{{ equipment.equipment?.equipmenttypename || '-' }}</span>
</div>
<div class="info-row">
<span class="info-label">Vendor</span>
<span class="info-value">{{ equipment.equipment?.vendor_name || '-' }}</span>
<span class="info-value">{{ equipment.equipment?.vendorname || '-' }}</span>
</div>
<div class="info-row">
<span class="info-label">Model</span>
<span class="info-value">{{ equipment.equipment?.model_name || '-' }}</span>
<span class="info-value">{{ equipment.equipment?.modelname || '-' }}</span>
</div>
</div>
</div>
<!-- Controller Section (for CNC machines) -->
<div class="section-card" v-if="equipment.equipment?.controller_vendor_name || equipment.equipment?.controller_model_name">
<div class="section-card" v-if="equipment.equipment?.controllervendorname || equipment.equipment?.controllermodelname">
<h3 class="section-title">Controller</h3>
<div class="info-list">
<div class="info-row">
<span class="info-label">Vendor</span>
<span class="info-value">{{ equipment.equipment?.controller_vendor_name || '-' }}</span>
<span class="info-value">{{ equipment.equipment?.controllervendorname || '-' }}</span>
</div>
<div class="info-row">
<span class="info-label">Model</span>
<span class="info-value">{{ equipment.equipment?.controller_model_name || '-' }}</span>
<span class="info-value">{{ equipment.equipment?.controllermodelname || '-' }}</span>
</div>
</div>
</div>
@@ -164,14 +167,14 @@
:top="equipment.maptop"
:machineName="equipment.assetnumber"
>
<span class="location-link">{{ equipment.location_name || 'On Map' }}</span>
<span class="location-link">{{ equipment.locationname || 'On Map' }}</span>
</LocationMapTooltip>
<span v-else>{{ equipment.location_name || '-' }}</span>
<span v-else>{{ equipment.locationname || '-' }}</span>
</span>
</div>
<div class="info-row">
<span class="info-label">Business Unit</span>
<span class="info-value">{{ equipment.businessunit_name || '-' }}</span>
<span class="info-value">{{ equipment.businessunitname || '-' }}</span>
</div>
</div>
</div>
@@ -183,7 +186,7 @@
No controlling PC assigned
</div>
<div v-else class="connected-device">
<router-link :to="`/pcs/${controllingPc.plugin_id || controllingPc.assetid}`" class="device-link">
<router-link :to="`/pcs/${controllingPc.pluginid || controllingPc.assetid}`" class="device-link">
<div class="device-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect>
@@ -239,20 +242,20 @@ const controllingPc = computed(() => {
// First check incoming - computer as source controlling this equipment
for (const rel of relationships.value.incoming || []) {
if (rel.source_asset?.assettype_name === 'computer' && rel.relationship_type_name === 'Controls') {
if (rel.sourceasset?.assettypename === 'computer' && rel.relationshiptypename === 'Controls') {
return {
...rel.source_asset,
relationshipType: rel.relationship_type_name
...rel.sourceasset,
relationshipType: rel.relationshiptypename
}
}
}
// Also check outgoing - legacy data may have equipment -> computer Controls relationships
for (const rel of relationships.value.outgoing || []) {
if (rel.target_asset?.assettype_name === 'computer' && rel.relationship_type_name === 'Controls') {
if (rel.targetasset?.assettypename === 'computer' && rel.relationshiptypename === 'Controls') {
return {
...rel.target_asset,
relationshipType: rel.relationship_type_name
...rel.targetasset,
relationshipType: rel.relationshiptypename
}
}
}

View File

@@ -130,10 +130,10 @@
<h3 class="form-section-title">Controller</h3>
<div class="form-row">
<div class="form-group">
<label for="controller_vendorid">Controller Vendor</label>
<label for="controllervendorid">Controller Vendor</label>
<select
id="controller_vendorid"
v-model="form.controller_vendorid"
id="controllervendorid"
v-model="form.controllervendorid"
class="form-control"
>
<option value="">Select vendor...</option>
@@ -149,10 +149,10 @@
</div>
<div class="form-group">
<label for="controller_modelid">Controller Model</label>
<label for="controllermodelid">Controller Model</label>
<select
id="controller_modelid"
v-model="form.controller_modelid"
id="controllermodelid"
v-model="form.controllermodelid"
class="form-control"
>
<option value="">Select model...</option>
@@ -164,7 +164,7 @@
{{ m.modelnumber }}
</option>
</select>
<small class="form-help" v-if="form.controller_vendorid">Filtered by controller vendor</small>
<small class="form-help" v-if="form.controllervendorid">Filtered by controller vendor</small>
</div>
</div>
@@ -351,8 +351,8 @@ const form = ref({
equipmenttypeid: '',
vendorid: '',
modelnumberid: '',
controller_vendorid: '',
controller_modelid: '',
controllervendorid: '',
controllermodelid: '',
locationid: '',
businessunitid: '',
requiresmanualconfig: false,
@@ -383,8 +383,8 @@ const filteredModels = computed(() => {
// Filter models by selected controller vendor
const filteredControllerModels = computed(() => {
if (!form.value.controller_vendorid) return models.value
return models.value.filter(m => m.vendorid === form.value.controller_vendorid)
if (!form.value.controllervendorid) return models.value
return models.value.filter(m => m.vendorid === form.value.controllervendorid)
})
// Clear model selection when vendor changes
@@ -398,11 +398,11 @@ watch(() => form.value.vendorid, (newVal, oldVal) => {
})
// Clear controller model when controller vendor changes
watch(() => form.value.controller_vendorid, (newVal, oldVal) => {
watch(() => form.value.controllervendorid, (newVal, oldVal) => {
if (oldVal && newVal !== oldVal) {
const currentModel = models.value.find(m => m.modelnumberid === form.value.controller_modelid)
const currentModel = models.value.find(m => m.modelnumberid === form.value.controllermodelid)
if (currentModel && currentModel.vendorid !== newVal) {
form.value.controller_modelid = ''
form.value.controllermodelid = ''
}
}
})
@@ -413,11 +413,11 @@ onMounted(async () => {
const [typesRes, statusRes, vendorRes, locRes, modelsRes, buRes, pcsRes, relTypesRes] = await Promise.all([
equipmentApi.types.list(),
assetsApi.statuses.list(),
vendorsApi.list({ per_page: 500 }),
locationsApi.list({ per_page: 500 }),
modelsApi.list({ per_page: 1000 }),
businessunitsApi.list({ per_page: 500 }),
computersApi.list({ per_page: 500 }),
vendorsApi.list({ perpage: 500 }),
locationsApi.list({ perpage: 500 }),
modelsApi.list({ perpage: 1000 }),
businessunitsApi.list({ perpage: 500 }),
computersApi.list({ perpage: 500 }),
assetsApi.types.list() // Used for relationship types, will fix below
])
@@ -461,8 +461,8 @@ onMounted(async () => {
equipmenttypeid: data.equipment?.equipmenttypeid || '',
vendorid: data.equipment?.vendorid || '',
modelnumberid: data.equipment?.modelnumberid || '',
controller_vendorid: data.equipment?.controller_vendorid || '',
controller_modelid: data.equipment?.controller_modelid || '',
controllervendorid: data.equipment?.controllervendorid || '',
controllermodelid: data.equipment?.controllermodelid || '',
locationid: data.locationid || '',
businessunitid: data.businessunitid || '',
requiresmanualconfig: data.equipment?.requiresmanualconfig || false,
@@ -480,8 +480,8 @@ onMounted(async () => {
// Check incoming relationships for a controlling PC
for (const rel of relationships.incoming || []) {
if (rel.source_asset?.assettype_name === 'computer' && rel.relationship_type_name === 'Controls') {
controllingPcId.value = rel.source_asset.assetid
if (rel.sourceasset?.assettypename === 'computer' && rel.relationshiptypename === 'Controls') {
controllingPcId.value = rel.sourceasset.assetid
relationshipTypeId.value = rel.relationshiptypeid
existingRelationshipId.value = rel.assetrelationshipid
break
@@ -531,8 +531,8 @@ async function saveEquipment() {
equipmenttypeid: form.value.equipmenttypeid || null,
vendorid: form.value.vendorid || null,
modelnumberid: form.value.modelnumberid || null,
controller_vendorid: form.value.controller_vendorid || null,
controller_modelid: form.value.controller_modelid || 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,
@@ -588,8 +588,8 @@ async function saveRelationship(assetId) {
// Create new relationship (PC controls Equipment, so PC is source, Equipment is target)
await assetsApi.createRelationship({
source_assetid: controllingPcId.value,
target_assetid: assetId,
sourceassetid: controllingPcId.value,
targetassetid: assetId,
relationshiptypeid: relationshipTypeId.value
})
}

View File

@@ -28,6 +28,7 @@
<th>Name</th>
<th>Serial Number</th>
<th>Type</th>
<th>Vendor</th>
<th>Status</th>
<th>Location</th>
<th>Actions</th>
@@ -38,13 +39,14 @@
<td>{{ item.assetnumber }}</td>
<td>{{ item.name || '-' }}</td>
<td class="mono">{{ item.serialnumber || '-' }}</td>
<td>{{ item.equipment?.equipmenttype_name || '-' }}</td>
<td>{{ item.equipment?.equipmenttypename || '-' }}</td>
<td>{{ item.equipment?.vendorname || '-' }}</td>
<td>
<span class="badge" :class="getStatusClass(item.status_name)">
{{ item.status_name || 'Unknown' }}
<span class="badge" :class="getStatusClass(item.statusname)">
{{ item.statusname || 'Unknown' }}
</span>
</td>
<td>{{ item.location_name || '-' }}</td>
<td>{{ item.locationname || '-' }}</td>
<td class="actions">
<router-link
:to="`/machines/${item.equipment?.equipmentid || item.assetid}`"
@@ -55,7 +57,7 @@
</td>
</tr>
<tr v-if="equipment.length === 0">
<td colspan="7" style="text-align: center; color: var(--text-light);">
<td colspan="8" style="text-align: center; color: var(--text-light);">
No equipment found
</td>
</tr>
@@ -64,16 +66,13 @@
</div>
<!-- Pagination -->
<div class="pagination" v-if="totalPages > 1">
<button
v-for="p in totalPages"
:key="p"
:class="{ active: p === page }"
@click="goToPage(p)"
>
{{ p }}
</button>
</div>
<PaginationBar
:page="page"
:totalPages="totalPages"
:perPage="perPage"
@update:page="goToPage"
@update:perPage="changePerPage"
/>
</template>
</div>
@@ -83,12 +82,14 @@
<script setup>
import { ref, onMounted } from 'vue'
import { equipmentApi } from '../../api'
import PaginationBar from '../../components/PaginationBar.vue'
const equipment = ref([])
const loading = ref(true)
const search = ref('')
const page = ref(1)
const totalPages = ref(1)
const perPage = ref(20)
let searchTimeout = null
@@ -101,13 +102,13 @@ async function loadEquipment() {
try {
const params = {
page: page.value,
per_page: 20
perpage: perPage.value
}
if (search.value) params.search = search.value
const response = await equipmentApi.list(params)
equipment.value = response.data.data || []
totalPages.value = response.data.meta?.pagination?.total_pages || 1
totalPages.value = response.data.meta?.pagination?.totalpages || response.data.meta?.pagination?.total_pages || 1
} catch (error) {
console.error('Error loading equipment:', error)
} finally {
@@ -128,6 +129,12 @@ function goToPage(p) {
loadEquipment()
}
function changePerPage(newPerPage) {
perPage.value = newPerPage
page.value = 1
loadEquipment()
}
function getStatusClass(status) {
if (!status) return 'badge-info'
const s = status.toLowerCase()