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:
@@ -27,14 +27,14 @@
|
||||
:key="rel.relationshipid"
|
||||
class="relationship-item"
|
||||
>
|
||||
<div class="rel-icon">{{ getAssetIcon(rel.target_asset?.assettype) }}</div>
|
||||
<div class="rel-icon"><component :is="getAssetIcon(rel.targetasset?.assettype)" :size="16" /></div>
|
||||
<div class="rel-content">
|
||||
<router-link :to="getAssetRoute(rel.target_asset)" class="rel-name">
|
||||
{{ rel.target_asset?.name || rel.target_asset?.assetnumber || 'Unknown' }}
|
||||
<router-link :to="getAssetRoute(rel.targetasset)" class="rel-name">
|
||||
{{ rel.targetasset?.name || rel.targetasset?.assetnumber || 'Unknown' }}
|
||||
</router-link>
|
||||
<div class="rel-meta">
|
||||
<span class="badge badge-outline">{{ rel.relationship_type_name }}</span>
|
||||
<span class="rel-type-badge">{{ rel.target_asset?.assettype }}</span>
|
||||
<span class="badge badge-outline">{{ rel.relationshiptypename }}</span>
|
||||
<span class="rel-type-badge">{{ rel.targetasset?.assettype }}</span>
|
||||
</div>
|
||||
<div v-if="rel.notes" class="rel-notes">{{ rel.notes }}</div>
|
||||
</div>
|
||||
@@ -59,14 +59,14 @@
|
||||
:key="rel.relationshipid"
|
||||
class="relationship-item"
|
||||
>
|
||||
<div class="rel-icon">{{ getAssetIcon(rel.source_asset?.assettype) }}</div>
|
||||
<div class="rel-icon"><component :is="getAssetIcon(rel.sourceasset?.assettype)" :size="16" /></div>
|
||||
<div class="rel-content">
|
||||
<router-link :to="getAssetRoute(rel.source_asset)" class="rel-name">
|
||||
{{ rel.source_asset?.name || rel.source_asset?.assetnumber || 'Unknown' }}
|
||||
<router-link :to="getAssetRoute(rel.sourceasset)" class="rel-name">
|
||||
{{ rel.sourceasset?.name || rel.sourceasset?.assetnumber || 'Unknown' }}
|
||||
</router-link>
|
||||
<div class="rel-meta">
|
||||
<span class="badge badge-outline">{{ rel.relationship_type_name }}</span>
|
||||
<span class="rel-type-badge">{{ rel.source_asset?.assettype }}</span>
|
||||
<span class="badge badge-outline">{{ rel.relationshiptypename }}</span>
|
||||
<span class="rel-type-badge">{{ rel.sourceasset?.assettype }}</span>
|
||||
</div>
|
||||
<div v-if="rel.notes" class="rel-notes">{{ rel.notes }}</div>
|
||||
</div>
|
||||
@@ -173,6 +173,7 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { Cog, Monitor, Printer, Globe, Package } from 'lucide-vue-next'
|
||||
import { assetsApi, relationshipTypesApi } from '../api'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
|
||||
@@ -296,7 +297,7 @@ function searchAssets() {
|
||||
|
||||
searchTimeout = setTimeout(async () => {
|
||||
try {
|
||||
const response = await assetsApi.search(assetSearchQuery.value, { per_page: 10 })
|
||||
const response = await assetsApi.search(assetSearchQuery.value, { perpage: 10 })
|
||||
// Filter out the current asset
|
||||
assetSearchResults.value = (response.data.data || []).filter(
|
||||
a => a.assetid !== resolvedAssetId.value
|
||||
@@ -329,11 +330,11 @@ async function saveRelationship() {
|
||||
}
|
||||
|
||||
if (newRel.value.direction === 'outgoing') {
|
||||
data.source_assetid = resolvedAssetId.value
|
||||
data.target_assetid = newRel.value.targetAssetId
|
||||
data.sourceassetid = resolvedAssetId.value
|
||||
data.targetassetid = newRel.value.targetAssetId
|
||||
} else {
|
||||
data.source_assetid = newRel.value.targetAssetId
|
||||
data.target_assetid = resolvedAssetId.value
|
||||
data.sourceassetid = newRel.value.targetAssetId
|
||||
data.targetassetid = resolvedAssetId.value
|
||||
}
|
||||
|
||||
await assetsApi.createRelationship(data)
|
||||
@@ -374,12 +375,12 @@ function closeModal() {
|
||||
|
||||
function getAssetIcon(assettype) {
|
||||
const icons = {
|
||||
'equipment': '⚙',
|
||||
'computer': '💻',
|
||||
'printer': '🖨',
|
||||
'network_device': '🌐'
|
||||
'equipment': Cog,
|
||||
'computer': Monitor,
|
||||
'printer': Printer,
|
||||
'network_device': Globe
|
||||
}
|
||||
return icons[assettype] || '📦'
|
||||
return icons[assettype] || Package
|
||||
}
|
||||
|
||||
function getAssetRoute(asset) {
|
||||
|
||||
83
frontend/src/components/PaginationBar.vue
Normal file
83
frontend/src/components/PaginationBar.vue
Normal file
@@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<div class="pagination-bar" v-if="totalPages > 1 || showPerPage">
|
||||
<div class="pagination-info">
|
||||
<select
|
||||
v-if="showPerPage"
|
||||
:value="perPage"
|
||||
class="perpage-select"
|
||||
@change="$emit('update:perPage', Number($event.target.value))"
|
||||
>
|
||||
<option v-for="opt in perPageOptions" :key="opt" :value="opt">
|
||||
{{ opt }} per page
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="pagination" v-if="totalPages > 1">
|
||||
<button :disabled="page === 1" @click="$emit('update:page', page - 1)">
|
||||
Prev
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-for="p in visiblePages"
|
||||
:key="p"
|
||||
:class="{ active: p === page, ellipsis: p === '...' }"
|
||||
:disabled="p === '...'"
|
||||
@click="p !== '...' && $emit('update:page', p)"
|
||||
>
|
||||
{{ p }}
|
||||
</button>
|
||||
|
||||
<button :disabled="page === totalPages" @click="$emit('update:page', page + 1)">
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
page: { type: Number, required: true },
|
||||
totalPages: { type: Number, required: true },
|
||||
perPage: { type: Number, default: 20 },
|
||||
showPerPage: { type: Boolean, default: true }
|
||||
})
|
||||
|
||||
defineEmits(['update:page', 'update:perPage'])
|
||||
|
||||
const perPageOptions = [10, 20, 50, 100]
|
||||
|
||||
const visiblePages = computed(() => {
|
||||
const total = props.totalPages
|
||||
const current = props.page
|
||||
const pages = []
|
||||
|
||||
if (total <= 7) {
|
||||
for (let i = 1; i <= total; i++) pages.push(i)
|
||||
return pages
|
||||
}
|
||||
|
||||
pages.push(1)
|
||||
|
||||
if (current > 3) {
|
||||
pages.push('...')
|
||||
}
|
||||
|
||||
const start = Math.max(2, current - 1)
|
||||
const end = Math.min(total - 1, current + 1)
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
pages.push(i)
|
||||
}
|
||||
|
||||
if (current < total - 2) {
|
||||
pages.push('...')
|
||||
}
|
||||
|
||||
pages.push(total)
|
||||
|
||||
return pages
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user