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

@@ -0,0 +1,169 @@
<template>
<div>
<div class="no-print">
<button class="print-btn" :disabled="!printer" @click="print">Print QR Code</button>
<label>Position:
<select class="position-select" v-model="position">
<option value="1">1 - Top Left</option>
<option value="2">2 - Top Right</option>
<option value="3">3 - Middle Left</option>
<option value="4">4 - Middle Right</option>
<option value="5">5 - Bottom Left</option>
<option value="6">6 - Bottom Right</option>
</select>
</label>
</div>
<div v-if="loading" class="loading-msg">Loading...</div>
<div v-else-if="printer" class="print-sheet">
<div
v-for="pos in 6"
:key="pos"
class="label"
:class="[`pos-${pos}`, pos === parseInt(position) ? 'active' : 'inactive']"
>
<template v-if="pos === parseInt(position)">
<div class="model-name">{{ printer.printer?.modelname || '' }}</div>
<div class="qr-container">
<canvas ref="qrCanvas"></canvas>
</div>
<div class="info-section">
<div class="csf-name">{{ printer.assetnumber }}</div>
<div class="info-inner">
<div v-if="printer.printer?.windowsname" class="info-row">{{ printer.printer.windowsname }}</div>
<div v-if="ipAddress" class="info-row">{{ ipAddress }}</div>
</div>
</div>
</template>
</div>
</div>
<div v-else class="error-msg">Printer not found</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, watch, nextTick } from 'vue'
import { useRoute } from 'vue-router'
import { printersApi } from '../../api'
import QRCode from 'qrcode'
const route = useRoute()
const loading = ref(true)
const printer = ref(null)
const position = ref('1')
const qrCanvas = ref(null)
const ipAddress = computed(() => {
if (!printer.value?.communications?.length) return null
const primary = printer.value.communications.find(c => c.isprimary) || printer.value.communications[0]
return primary?.ipaddress || primary?.address || null
})
onMounted(async () => {
try {
const response = await printersApi.get(route.params.id)
printer.value = response.data.data
} catch (error) {
console.error('Error loading printer:', error)
} finally {
loading.value = false
await nextTick()
generateQR()
}
})
watch(position, async () => {
await nextTick()
generateQR()
})
function generateQR() {
const canvas = Array.isArray(qrCanvas.value) ? qrCanvas.value[0] : qrCanvas.value
if (!canvas || !printer.value) return
const qrUrl = `${window.location.origin}/printers/${printer.value.printer?.printerid || printer.value.assetid}`
QRCode.toCanvas(canvas, qrUrl, {
width: 144,
margin: 0,
errorCorrectionLevel: 'M'
}).catch(err => console.error('QR error:', err))
}
function print() {
window.print()
}
</script>
<style scoped>
@page { size: letter; margin: 0; }
.no-print { margin-bottom: 20px; text-align: center; padding: 20px; }
.print-btn {
padding: 10px 30px;
font-size: 16px;
cursor: pointer;
background: #667eea;
color: white;
border: none;
border-radius: 5px;
margin: 5px;
}
.print-btn:disabled { background: #ccc; cursor: not-allowed; }
.position-select { padding: 8px; font-size: 14px; margin-left: 10px; }
.loading-msg, .error-msg {
text-align: center;
padding: 2rem;
font-size: 1.125rem;
color: #666;
}
.print-sheet {
width: 8.5in;
height: 11in;
background: white;
margin: 0 auto;
position: relative;
border: 1px solid #ccc;
}
.label {
width: 3in;
height: 3in;
position: absolute;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 0.1in;
box-sizing: border-box;
}
.label.inactive { border: 1px dashed #ccc; }
.label.active { border: 2px solid #667eea; }
.pos-1 { top: 0.875in; left: 1.1875in; }
.pos-2 { top: 0.875in; left: 4.3125in; }
.pos-3 { top: 4in; left: 1.1875in; }
.pos-4 { top: 4in; left: 4.3125in; }
.pos-5 { top: 7.125in; left: 1.1875in; }
.pos-6 { top: 7.125in; left: 4.3125in; }
.model-name { font-size: 11pt; font-weight: bold; text-align: center; margin-bottom: 0.1in; color: #000; }
.qr-container { text-align: center; }
.info-section { margin-top: 0.1in; display: flex; flex-direction: column; align-items: center; }
.info-inner { text-align: left; }
.info-row { font-size: 9pt; color: #333; margin: 1px 0; white-space: nowrap; }
.csf-name { font-size: 12pt; font-weight: bold; font-family: monospace; text-align: center; margin-bottom: 2px; }
@media print {
body { padding: 0; margin: 0; background: white; }
.no-print { display: none !important; }
.print-sheet { border: none; margin: 0; width: 8.5in; height: 11in; overflow: hidden; }
.label { border: none !important; }
.label.inactive { visibility: hidden; }
}
</style>