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:
702
frontend/package-lock.json
generated
702
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -13,13 +13,16 @@
|
||||
"@fullcalendar/daygrid": "^6.1.20",
|
||||
"@fullcalendar/vue3": "^6.1.20",
|
||||
"axios": "^1.6.0",
|
||||
"jsbarcode": "^3.12.3",
|
||||
"leaflet": "^1.9.4",
|
||||
"lucide-vue-next": "^0.563.0",
|
||||
"pinia": "^2.1.0",
|
||||
"qrcode": "^1.5.4",
|
||||
"vue": "^3.4.0",
|
||||
"vue-router": "^4.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.0.0",
|
||||
"vite": "^5.0.0"
|
||||
"@vitejs/plugin-vue": "^5.2.4",
|
||||
"vite": "^6.4.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,17 @@
|
||||
--sidebar-bg: #00003d;
|
||||
--sidebar-text: #ffffff;
|
||||
--sidebar-width: 250px;
|
||||
|
||||
/* Hover variants */
|
||||
--secondary-dark: #82503f;
|
||||
--success-dark: #019e4c;
|
||||
--info-dark: #039ce0;
|
||||
--warning-dark: #e67c02;
|
||||
--danger-dark: #e62c51;
|
||||
|
||||
/* Additional semantic colors */
|
||||
--purple: #9c27b0;
|
||||
--bg-popover: #111111;
|
||||
}
|
||||
|
||||
/* Dark Mode - via data-theme attribute or system preference fallback */
|
||||
@@ -146,7 +157,7 @@ h1, h2, h3, h4, h5, h6 {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
text-transform: uppercase;
|
||||
color: #ffffff;
|
||||
color: var(--sidebar-text);
|
||||
margin: 0.5rem 0 0 0;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
@@ -189,6 +200,7 @@ h1, h2, h3, h4, h5, h6 {
|
||||
.sidebar-nav a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.7rem 1.25rem;
|
||||
color: rgba(255,255,255,0.85);
|
||||
text-decoration: none;
|
||||
@@ -196,6 +208,11 @@ h1, h2, h3, h4, h5, h6 {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.sidebar-nav a svg {
|
||||
margin-right: 0.5rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sidebar-nav a:hover,
|
||||
.sidebar-nav a.active {
|
||||
background: rgba(255,255,255,0.1);
|
||||
@@ -431,8 +448,8 @@ tr:hover {
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #82503f;
|
||||
border-color: #82503f;
|
||||
background: var(--secondary-dark);
|
||||
border-color: var(--secondary-dark);
|
||||
color: white;
|
||||
}
|
||||
|
||||
@@ -443,8 +460,8 @@ tr:hover {
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background: #019e4c;
|
||||
border-color: #019e4c;
|
||||
background: var(--success-dark);
|
||||
border-color: var(--success-dark);
|
||||
}
|
||||
|
||||
.btn-info {
|
||||
@@ -454,8 +471,8 @@ tr:hover {
|
||||
}
|
||||
|
||||
.btn-info:hover {
|
||||
background: #039ce0;
|
||||
border-color: #039ce0;
|
||||
background: var(--info-dark);
|
||||
border-color: var(--info-dark);
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
@@ -465,8 +482,8 @@ tr:hover {
|
||||
}
|
||||
|
||||
.btn-warning:hover {
|
||||
background: #e67c02;
|
||||
border-color: #e67c02;
|
||||
background: var(--warning-dark);
|
||||
border-color: var(--warning-dark);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
@@ -476,8 +493,8 @@ tr:hover {
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #e62c51;
|
||||
border-color: #e62c51;
|
||||
background: var(--danger-dark);
|
||||
border-color: var(--danger-dark);
|
||||
}
|
||||
|
||||
.btn-link {
|
||||
@@ -614,7 +631,7 @@ input[type="radio"] {
|
||||
}
|
||||
|
||||
select.form-control option {
|
||||
background: #1a1a2e;
|
||||
background: var(--text);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -690,7 +707,7 @@ input[type="radio"] {
|
||||
|
||||
.error-message {
|
||||
background: rgba(245, 54, 92, 0.2);
|
||||
color: #f5365c;
|
||||
color: var(--danger);
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 0.25rem;
|
||||
margin-bottom: 1rem;
|
||||
@@ -789,11 +806,35 @@ input[type="radio"] {
|
||||
}
|
||||
|
||||
/* Pagination */
|
||||
.pagination-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 1rem;
|
||||
padding-top: 0.75rem;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.pagination-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.perpage-select {
|
||||
padding: 0.375rem 0.5rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.25rem;
|
||||
background: var(--bg-card);
|
||||
color: var(--text);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 0.25rem;
|
||||
margin-top: 1rem;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.pagination button {
|
||||
@@ -806,8 +847,14 @@ input[type="radio"] {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.pagination button:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
.pagination button:hover:not(:disabled):not(.ellipsis) {
|
||||
background: var(--bg);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.pagination button:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.pagination button.active {
|
||||
@@ -816,6 +863,12 @@ input[type="radio"] {
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.pagination button.ellipsis {
|
||||
border: none;
|
||||
cursor: default;
|
||||
padding: 0.5rem 0.25rem;
|
||||
}
|
||||
|
||||
/* Loading */
|
||||
.loading {
|
||||
text-align: center;
|
||||
@@ -1260,7 +1313,7 @@ td.actions {
|
||||
}
|
||||
|
||||
.badge-printer {
|
||||
background: #9c27b0;
|
||||
background: var(--purple);
|
||||
color: white;
|
||||
}
|
||||
|
||||
@@ -1354,7 +1407,7 @@ td.actions {
|
||||
}
|
||||
|
||||
.fc .fc-popover {
|
||||
background: #111111;
|
||||
background: var(--bg-popover);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.25rem;
|
||||
box-shadow: 0 16px 38px -12px rgba(0,0,0,.56), 0 4px 25px 0 rgba(0,0,0,.12), 0 8px 10px -5px rgba(0,0,0,.2);
|
||||
@@ -1413,18 +1466,18 @@ td.actions {
|
||||
LEAFLET DARK THEME OVERRIDES
|
||||
============================================ */
|
||||
.leaflet-container {
|
||||
background: #111111;
|
||||
background: var(--bg-popover);
|
||||
}
|
||||
|
||||
.leaflet-popup-content-wrapper {
|
||||
background: #111111;
|
||||
background: var(--bg-popover);
|
||||
color: var(--text);
|
||||
border-radius: 0.25rem;
|
||||
box-shadow: 0 16px 38px -12px rgba(0,0,0,.56), 0 4px 25px 0 rgba(0,0,0,.12), 0 8px 10px -5px rgba(0,0,0,.2);
|
||||
}
|
||||
|
||||
.leaflet-popup-tip {
|
||||
background: #111111;
|
||||
background: var(--bg-popover);
|
||||
}
|
||||
|
||||
.leaflet-popup-content {
|
||||
|
||||
@@ -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>
|
||||
@@ -1,354 +1,54 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
|
||||
// Views
|
||||
import Login from '../views/Login.vue'
|
||||
import AppLayout from '../views/AppLayout.vue'
|
||||
import Dashboard from '../views/Dashboard.vue'
|
||||
import MachinesList from '../views/machines/MachinesList.vue'
|
||||
import MachineDetail from '../views/machines/MachineDetail.vue'
|
||||
import MachineForm from '../views/machines/MachineForm.vue'
|
||||
import PrintersList from '../views/printers/PrintersList.vue'
|
||||
import PrinterDetail from '../views/printers/PrinterDetail.vue'
|
||||
import PrinterForm from '../views/printers/PrinterForm.vue'
|
||||
import PCsList from '../views/pcs/PCsList.vue'
|
||||
import PCDetail from '../views/pcs/PCDetail.vue'
|
||||
import PCForm from '../views/pcs/PCForm.vue'
|
||||
import VendorsList from '../views/vendors/VendorsList.vue'
|
||||
import ApplicationsList from '../views/applications/ApplicationsList.vue'
|
||||
import ApplicationDetail from '../views/applications/ApplicationDetail.vue'
|
||||
import ApplicationForm from '../views/applications/ApplicationForm.vue'
|
||||
import KnowledgeBaseList from '../views/knowledgebase/KnowledgeBaseList.vue'
|
||||
import KnowledgeBaseDetail from '../views/knowledgebase/KnowledgeBaseDetail.vue'
|
||||
import KnowledgeBaseForm from '../views/knowledgebase/KnowledgeBaseForm.vue'
|
||||
import SearchResults from '../views/SearchResults.vue'
|
||||
import NotificationsList from '../views/notifications/NotificationsList.vue'
|
||||
import NotificationForm from '../views/notifications/NotificationForm.vue'
|
||||
import USBList from '../views/usb/USBList.vue'
|
||||
import USBDetail from '../views/usb/USBDetail.vue'
|
||||
import USBForm from '../views/usb/USBForm.vue'
|
||||
import NetworkDevicesList from '../views/network/NetworkDevicesList.vue'
|
||||
import NetworkDeviceDetail from '../views/network/NetworkDeviceDetail.vue'
|
||||
import NetworkDeviceForm from '../views/network/NetworkDeviceForm.vue'
|
||||
import ReportsIndex from '../views/reports/ReportsIndex.vue'
|
||||
import CalendarView from '../views/CalendarView.vue'
|
||||
import SettingsIndex from '../views/settings/SettingsIndex.vue'
|
||||
import MachineTypesList from '../views/settings/MachineTypesList.vue'
|
||||
import LocationsList from '../views/settings/LocationsList.vue'
|
||||
import StatusesList from '../views/settings/StatusesList.vue'
|
||||
import ModelsList from '../views/settings/ModelsList.vue'
|
||||
import PCTypesList from '../views/settings/PCTypesList.vue'
|
||||
import OperatingSystemsList from '../views/settings/OperatingSystemsList.vue'
|
||||
import BusinessUnitsList from '../views/settings/BusinessUnitsList.vue'
|
||||
import VLANsList from '../views/settings/VLANsList.vue'
|
||||
import SubnetsList from '../views/settings/SubnetsList.vue'
|
||||
import MapView from '../views/MapView.vue'
|
||||
import MapEditor from '../views/MapEditor.vue'
|
||||
import ShopfloorDashboard from '../views/ShopfloorDashboard.vue'
|
||||
import TVDashboard from '../views/TVDashboard.vue'
|
||||
import EmployeeDetail from '../views/employees/EmployeeDetail.vue'
|
||||
|
||||
// Auto-discover all route modules from routes/ directory
|
||||
const routeModules = import.meta.glob('./routes/*.js', { eager: true })
|
||||
const appChildren = Object.values(routeModules).flatMap(m => m.default)
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/login',
|
||||
name: 'login',
|
||||
component: Login,
|
||||
component: () => import('../views/Login.vue'),
|
||||
meta: { guest: true }
|
||||
},
|
||||
// Standalone full-screen dashboards (no sidebar, no auth required)
|
||||
{
|
||||
path: '/shopfloor',
|
||||
name: 'shopfloor',
|
||||
component: ShopfloorDashboard
|
||||
component: () => import('../views/ShopfloorDashboard.vue')
|
||||
},
|
||||
{
|
||||
path: '/tv',
|
||||
name: 'tv',
|
||||
component: TVDashboard
|
||||
component: () => import('../views/TVDashboard.vue')
|
||||
},
|
||||
// Print pages (standalone, no sidebar/header)
|
||||
{
|
||||
path: '/print/equipment-badge/:id',
|
||||
name: 'print-equipment-badge',
|
||||
component: () => import('../views/print/EquipmentBadge.vue')
|
||||
},
|
||||
{
|
||||
path: '/print/printer-qr',
|
||||
name: 'print-printer-qr-batch',
|
||||
component: () => import('../views/print/PrinterQRBatch.vue')
|
||||
},
|
||||
{
|
||||
path: '/print/printer-qr/:id',
|
||||
name: 'print-printer-qr-single',
|
||||
component: () => import('../views/print/PrinterQRSingle.vue')
|
||||
},
|
||||
{
|
||||
path: '/print/usb-labels',
|
||||
name: 'print-usb-labels',
|
||||
component: () => import('../views/print/USBLabelBatch.vue')
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
component: AppLayout,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'dashboard',
|
||||
component: Dashboard
|
||||
},
|
||||
{
|
||||
path: 'search',
|
||||
name: 'search',
|
||||
component: SearchResults
|
||||
},
|
||||
{
|
||||
path: 'machines',
|
||||
name: 'machines',
|
||||
component: MachinesList
|
||||
},
|
||||
{
|
||||
path: 'machines/new',
|
||||
name: 'machine-new',
|
||||
component: MachineForm,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: 'machines/:id',
|
||||
name: 'machine-detail',
|
||||
component: MachineDetail
|
||||
},
|
||||
{
|
||||
path: 'machines/:id/edit',
|
||||
name: 'machine-edit',
|
||||
component: MachineForm,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: 'printers',
|
||||
name: 'printers',
|
||||
component: PrintersList
|
||||
},
|
||||
{
|
||||
path: 'printers/new',
|
||||
name: 'printer-new',
|
||||
component: PrinterForm,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: 'printers/:id',
|
||||
name: 'printer-detail',
|
||||
component: PrinterDetail
|
||||
},
|
||||
{
|
||||
path: 'printers/:id/edit',
|
||||
name: 'printer-edit',
|
||||
component: PrinterForm,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: 'pcs',
|
||||
name: 'pcs',
|
||||
component: PCsList
|
||||
},
|
||||
{
|
||||
path: 'pcs/new',
|
||||
name: 'pc-new',
|
||||
component: PCForm,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: 'pcs/:id',
|
||||
name: 'pc-detail',
|
||||
component: PCDetail
|
||||
},
|
||||
{
|
||||
path: 'pcs/:id/edit',
|
||||
name: 'pc-edit',
|
||||
component: PCForm,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: 'map',
|
||||
name: 'map',
|
||||
component: MapView
|
||||
},
|
||||
{
|
||||
path: 'map/editor',
|
||||
name: 'map-editor',
|
||||
component: MapEditor,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: 'applications',
|
||||
name: 'applications',
|
||||
component: ApplicationsList
|
||||
},
|
||||
{
|
||||
path: 'applications/new',
|
||||
name: 'application-new',
|
||||
component: ApplicationForm,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: 'applications/:id',
|
||||
name: 'application-detail',
|
||||
component: ApplicationDetail
|
||||
},
|
||||
{
|
||||
path: 'applications/:id/edit',
|
||||
name: 'application-edit',
|
||||
component: ApplicationForm,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: 'knowledgebase',
|
||||
name: 'knowledgebase',
|
||||
component: KnowledgeBaseList
|
||||
},
|
||||
{
|
||||
path: 'knowledgebase/new',
|
||||
name: 'knowledgebase-new',
|
||||
component: KnowledgeBaseForm,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: 'knowledgebase/:id',
|
||||
name: 'knowledgebase-detail',
|
||||
component: KnowledgeBaseDetail
|
||||
},
|
||||
{
|
||||
path: 'knowledgebase/:id/edit',
|
||||
name: 'knowledgebase-edit',
|
||||
component: KnowledgeBaseForm,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: 'notifications',
|
||||
name: 'notifications',
|
||||
component: NotificationsList
|
||||
},
|
||||
{
|
||||
path: 'notifications/new',
|
||||
name: 'notification-new',
|
||||
component: NotificationForm,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: 'notifications/:id',
|
||||
name: 'notification-detail',
|
||||
component: NotificationForm
|
||||
},
|
||||
{
|
||||
path: 'notifications/:id/edit',
|
||||
name: 'notification-edit',
|
||||
component: NotificationForm,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: 'usb',
|
||||
name: 'usb',
|
||||
component: USBList
|
||||
},
|
||||
{
|
||||
path: 'usb/new',
|
||||
name: 'usb-new',
|
||||
component: USBForm,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: 'usb/:id',
|
||||
name: 'usb-detail',
|
||||
component: USBDetail
|
||||
},
|
||||
{
|
||||
path: 'usb/:id/edit',
|
||||
name: 'usb-edit',
|
||||
component: USBForm,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: 'network',
|
||||
name: 'network',
|
||||
component: NetworkDevicesList
|
||||
},
|
||||
{
|
||||
path: 'network/new',
|
||||
name: 'network-new',
|
||||
component: NetworkDeviceForm,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: 'network/:id',
|
||||
name: 'network-detail',
|
||||
component: NetworkDeviceDetail
|
||||
},
|
||||
{
|
||||
path: 'network/:id/edit',
|
||||
name: 'network-edit',
|
||||
component: NetworkDeviceForm,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: 'reports',
|
||||
name: 'reports',
|
||||
component: ReportsIndex
|
||||
},
|
||||
{
|
||||
path: 'calendar',
|
||||
name: 'calendar',
|
||||
component: CalendarView
|
||||
},
|
||||
{
|
||||
path: 'settings',
|
||||
name: 'settings',
|
||||
component: SettingsIndex,
|
||||
meta: { requiresAuth: true, requiresAdmin: true }
|
||||
},
|
||||
{
|
||||
path: 'settings/vendors',
|
||||
name: 'vendors',
|
||||
component: VendorsList,
|
||||
meta: { requiresAuth: true, requiresAdmin: true }
|
||||
},
|
||||
{
|
||||
path: 'settings/machinetypes',
|
||||
name: 'machinetypes',
|
||||
component: MachineTypesList,
|
||||
meta: { requiresAuth: true, requiresAdmin: true }
|
||||
},
|
||||
{
|
||||
path: 'settings/locations',
|
||||
name: 'locations',
|
||||
component: LocationsList,
|
||||
meta: { requiresAuth: true, requiresAdmin: true }
|
||||
},
|
||||
{
|
||||
path: 'settings/statuses',
|
||||
name: 'statuses',
|
||||
component: StatusesList,
|
||||
meta: { requiresAuth: true, requiresAdmin: true }
|
||||
},
|
||||
{
|
||||
path: 'settings/models',
|
||||
name: 'models',
|
||||
component: ModelsList,
|
||||
meta: { requiresAuth: true, requiresAdmin: true }
|
||||
},
|
||||
{
|
||||
path: 'settings/pctypes',
|
||||
name: 'pctypes',
|
||||
component: PCTypesList,
|
||||
meta: { requiresAuth: true, requiresAdmin: true }
|
||||
},
|
||||
{
|
||||
path: 'settings/operatingsystems',
|
||||
name: 'operatingsystems',
|
||||
component: OperatingSystemsList,
|
||||
meta: { requiresAuth: true, requiresAdmin: true }
|
||||
},
|
||||
{
|
||||
path: 'settings/businessunits',
|
||||
name: 'businessunits',
|
||||
component: BusinessUnitsList,
|
||||
meta: { requiresAuth: true, requiresAdmin: true }
|
||||
},
|
||||
{
|
||||
path: 'settings/vlans',
|
||||
name: 'vlans',
|
||||
component: VLANsList,
|
||||
meta: { requiresAuth: true, requiresAdmin: true }
|
||||
},
|
||||
{
|
||||
path: 'settings/subnets',
|
||||
name: 'subnets',
|
||||
component: SubnetsList,
|
||||
meta: { requiresAuth: true, requiresAdmin: true }
|
||||
},
|
||||
{
|
||||
path: 'employees/:sso',
|
||||
name: 'employee-detail',
|
||||
component: EmployeeDetail
|
||||
}
|
||||
]
|
||||
children: appChildren
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
27
frontend/src/router/routes/applications.js
Normal file
27
frontend/src/router/routes/applications.js
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Applications routes (core feature)
|
||||
*/
|
||||
export default [
|
||||
{
|
||||
path: 'applications',
|
||||
name: 'applications',
|
||||
component: () => import('../../views/applications/ApplicationsList.vue')
|
||||
},
|
||||
{
|
||||
path: 'applications/new',
|
||||
name: 'application-new',
|
||||
component: () => import('../../views/applications/ApplicationForm.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: 'applications/:id',
|
||||
name: 'application-detail',
|
||||
component: () => import('../../views/applications/ApplicationDetail.vue')
|
||||
},
|
||||
{
|
||||
path: 'applications/:id/edit',
|
||||
name: 'application-edit',
|
||||
component: () => import('../../views/applications/ApplicationForm.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
}
|
||||
]
|
||||
40
frontend/src/router/routes/computers.js
Normal file
40
frontend/src/router/routes/computers.js
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Computers plugin routes
|
||||
*/
|
||||
export default [
|
||||
{
|
||||
path: 'pcs',
|
||||
name: 'pcs',
|
||||
component: () => import('../../views/pcs/PCsList.vue')
|
||||
},
|
||||
{
|
||||
path: 'pcs/new',
|
||||
name: 'pc-new',
|
||||
component: () => import('../../views/pcs/PCForm.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: 'pcs/:id',
|
||||
name: 'pc-detail',
|
||||
component: () => import('../../views/pcs/PCDetail.vue')
|
||||
},
|
||||
{
|
||||
path: 'pcs/:id/edit',
|
||||
name: 'pc-edit',
|
||||
component: () => import('../../views/pcs/PCForm.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
// Computer-specific settings
|
||||
{
|
||||
path: 'settings/pctypes',
|
||||
name: 'pctypes',
|
||||
component: () => import('../../views/settings/PCTypesList.vue'),
|
||||
meta: { requiresAuth: true, requiresAdmin: true }
|
||||
},
|
||||
{
|
||||
path: 'settings/operatingsystems',
|
||||
name: 'operatingsystems',
|
||||
component: () => import('../../views/settings/OperatingSystemsList.vue'),
|
||||
meta: { requiresAuth: true, requiresAdmin: true }
|
||||
}
|
||||
]
|
||||
84
frontend/src/router/routes/core.js
Normal file
84
frontend/src/router/routes/core.js
Normal file
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Core routes - Dashboard, Search, Map, Reports, Settings, Employees
|
||||
*/
|
||||
export default [
|
||||
{
|
||||
path: '',
|
||||
name: 'dashboard',
|
||||
component: () => import('../../views/Dashboard.vue')
|
||||
},
|
||||
{
|
||||
path: 'search',
|
||||
name: 'search',
|
||||
component: () => import('../../views/SearchResults.vue')
|
||||
},
|
||||
{
|
||||
path: 'map',
|
||||
name: 'map',
|
||||
component: () => import('../../views/MapView.vue')
|
||||
},
|
||||
{
|
||||
path: 'map/editor',
|
||||
name: 'map-editor',
|
||||
component: () => import('../../views/MapEditor.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: 'reports',
|
||||
name: 'reports',
|
||||
component: () => import('../../views/reports/ReportsIndex.vue')
|
||||
},
|
||||
{
|
||||
path: 'reports/pc-relationships',
|
||||
name: 'report-pc-relationships',
|
||||
component: () => import('../../views/reports/PCRelationshipsReport.vue')
|
||||
},
|
||||
{
|
||||
path: 'employees/:sso',
|
||||
name: 'employee-detail',
|
||||
component: () => import('../../views/employees/EmployeeDetail.vue')
|
||||
},
|
||||
// Settings
|
||||
{
|
||||
path: 'settings',
|
||||
name: 'settings',
|
||||
component: () => import('../../views/settings/SettingsIndex.vue'),
|
||||
meta: { requiresAuth: true, requiresAdmin: true }
|
||||
},
|
||||
{
|
||||
path: 'settings/vendors',
|
||||
name: 'vendors',
|
||||
component: () => import('../../views/vendors/VendorsList.vue'),
|
||||
meta: { requiresAuth: true, requiresAdmin: true }
|
||||
},
|
||||
{
|
||||
path: 'settings/machinetypes',
|
||||
name: 'machinetypes',
|
||||
component: () => import('../../views/settings/MachineTypesList.vue'),
|
||||
meta: { requiresAuth: true, requiresAdmin: true }
|
||||
},
|
||||
{
|
||||
path: 'settings/locations',
|
||||
name: 'locations',
|
||||
component: () => import('../../views/settings/LocationsList.vue'),
|
||||
meta: { requiresAuth: true, requiresAdmin: true }
|
||||
},
|
||||
{
|
||||
path: 'settings/statuses',
|
||||
name: 'statuses',
|
||||
component: () => import('../../views/settings/StatusesList.vue'),
|
||||
meta: { requiresAuth: true, requiresAdmin: true }
|
||||
},
|
||||
{
|
||||
path: 'settings/models',
|
||||
name: 'models',
|
||||
component: () => import('../../views/settings/ModelsList.vue'),
|
||||
meta: { requiresAuth: true, requiresAdmin: true }
|
||||
},
|
||||
{
|
||||
path: 'settings/businessunits',
|
||||
name: 'businessunits',
|
||||
component: () => import('../../views/settings/BusinessUnitsList.vue'),
|
||||
meta: { requiresAuth: true, requiresAdmin: true }
|
||||
}
|
||||
]
|
||||
27
frontend/src/router/routes/equipment.js
Normal file
27
frontend/src/router/routes/equipment.js
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Equipment plugin routes
|
||||
*/
|
||||
export default [
|
||||
{
|
||||
path: 'machines',
|
||||
name: 'machines',
|
||||
component: () => import('../../views/machines/MachinesList.vue')
|
||||
},
|
||||
{
|
||||
path: 'machines/new',
|
||||
name: 'machine-new',
|
||||
component: () => import('../../views/machines/MachineForm.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: 'machines/:id',
|
||||
name: 'machine-detail',
|
||||
component: () => import('../../views/machines/MachineDetail.vue')
|
||||
},
|
||||
{
|
||||
path: 'machines/:id/edit',
|
||||
name: 'machine-edit',
|
||||
component: () => import('../../views/machines/MachineForm.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
}
|
||||
]
|
||||
27
frontend/src/router/routes/knowledgebase.js
Normal file
27
frontend/src/router/routes/knowledgebase.js
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Knowledge Base routes (core feature)
|
||||
*/
|
||||
export default [
|
||||
{
|
||||
path: 'knowledgebase',
|
||||
name: 'knowledgebase',
|
||||
component: () => import('../../views/knowledgebase/KnowledgeBaseList.vue')
|
||||
},
|
||||
{
|
||||
path: 'knowledgebase/new',
|
||||
name: 'knowledgebase-new',
|
||||
component: () => import('../../views/knowledgebase/KnowledgeBaseForm.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: 'knowledgebase/:id',
|
||||
name: 'knowledgebase-detail',
|
||||
component: () => import('../../views/knowledgebase/KnowledgeBaseDetail.vue')
|
||||
},
|
||||
{
|
||||
path: 'knowledgebase/:id/edit',
|
||||
name: 'knowledgebase-edit',
|
||||
component: () => import('../../views/knowledgebase/KnowledgeBaseForm.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
}
|
||||
]
|
||||
40
frontend/src/router/routes/network.js
Normal file
40
frontend/src/router/routes/network.js
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Network plugin routes
|
||||
*/
|
||||
export default [
|
||||
{
|
||||
path: 'network',
|
||||
name: 'network',
|
||||
component: () => import('../../views/network/NetworkDevicesList.vue')
|
||||
},
|
||||
{
|
||||
path: 'network/new',
|
||||
name: 'network-new',
|
||||
component: () => import('../../views/network/NetworkDeviceForm.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: 'network/:id',
|
||||
name: 'network-detail',
|
||||
component: () => import('../../views/network/NetworkDeviceDetail.vue')
|
||||
},
|
||||
{
|
||||
path: 'network/:id/edit',
|
||||
name: 'network-edit',
|
||||
component: () => import('../../views/network/NetworkDeviceForm.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
// Network-specific settings
|
||||
{
|
||||
path: 'settings/vlans',
|
||||
name: 'vlans',
|
||||
component: () => import('../../views/settings/VLANsList.vue'),
|
||||
meta: { requiresAuth: true, requiresAdmin: true }
|
||||
},
|
||||
{
|
||||
path: 'settings/subnets',
|
||||
name: 'subnets',
|
||||
component: () => import('../../views/settings/SubnetsList.vue'),
|
||||
meta: { requiresAuth: true, requiresAdmin: true }
|
||||
}
|
||||
]
|
||||
32
frontend/src/router/routes/notifications.js
Normal file
32
frontend/src/router/routes/notifications.js
Normal file
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Notifications plugin routes
|
||||
*/
|
||||
export default [
|
||||
{
|
||||
path: 'notifications',
|
||||
name: 'notifications',
|
||||
component: () => import('../../views/notifications/NotificationsList.vue')
|
||||
},
|
||||
{
|
||||
path: 'notifications/new',
|
||||
name: 'notification-new',
|
||||
component: () => import('../../views/notifications/NotificationForm.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: 'notifications/:id',
|
||||
name: 'notification-detail',
|
||||
component: () => import('../../views/notifications/NotificationForm.vue')
|
||||
},
|
||||
{
|
||||
path: 'notifications/:id/edit',
|
||||
name: 'notification-edit',
|
||||
component: () => import('../../views/notifications/NotificationForm.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: 'calendar',
|
||||
name: 'calendar',
|
||||
component: () => import('../../views/CalendarView.vue')
|
||||
}
|
||||
]
|
||||
27
frontend/src/router/routes/printers.js
Normal file
27
frontend/src/router/routes/printers.js
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Printers plugin routes
|
||||
*/
|
||||
export default [
|
||||
{
|
||||
path: 'printers',
|
||||
name: 'printers',
|
||||
component: () => import('../../views/printers/PrintersList.vue')
|
||||
},
|
||||
{
|
||||
path: 'printers/new',
|
||||
name: 'printer-new',
|
||||
component: () => import('../../views/printers/PrinterForm.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: 'printers/:id',
|
||||
name: 'printer-detail',
|
||||
component: () => import('../../views/printers/PrinterDetail.vue')
|
||||
},
|
||||
{
|
||||
path: 'printers/:id/edit',
|
||||
name: 'printer-edit',
|
||||
component: () => import('../../views/printers/PrinterForm.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
}
|
||||
]
|
||||
27
frontend/src/router/routes/usb.js
Normal file
27
frontend/src/router/routes/usb.js
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* USB plugin routes
|
||||
*/
|
||||
export default [
|
||||
{
|
||||
path: 'usb',
|
||||
name: 'usb',
|
||||
component: () => import('../../views/usb/USBList.vue')
|
||||
},
|
||||
{
|
||||
path: 'usb/new',
|
||||
name: 'usb-new',
|
||||
component: () => import('../../views/usb/USBForm.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: 'usb/:id',
|
||||
name: 'usb-detail',
|
||||
component: () => import('../../views/usb/USBDetail.vue')
|
||||
},
|
||||
{
|
||||
path: 'usb/:id/edit',
|
||||
name: 'usb-edit',
|
||||
component: () => import('../../views/usb/USBForm.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
}
|
||||
]
|
||||
@@ -16,22 +16,13 @@
|
||||
</div>
|
||||
|
||||
<nav class="sidebar-nav">
|
||||
<router-link to="/">Dashboard</router-link>
|
||||
<router-link to="/calendar">Calendar</router-link>
|
||||
<router-link to="/map">Map</router-link>
|
||||
|
||||
<div class="nav-section">Assets</div>
|
||||
<router-link to="/machines">Equipment</router-link>
|
||||
<router-link to="/pcs">PCs</router-link>
|
||||
<router-link to="/printers">Printers</router-link>
|
||||
<router-link to="/network">Network Devices</router-link>
|
||||
<router-link to="/usb">USB Devices</router-link>
|
||||
|
||||
<div class="nav-section">Information</div>
|
||||
<router-link to="/applications">Applications</router-link>
|
||||
<router-link to="/knowledgebase">Knowledge Base</router-link>
|
||||
<router-link to="/notifications">Notifications</router-link>
|
||||
<router-link to="/reports">Reports</router-link>
|
||||
<template v-for="item in navItems" :key="item.route">
|
||||
<div v-if="item.section" class="nav-section">{{ item.section }}</div>
|
||||
<router-link :to="item.route">
|
||||
<component v-if="item.iconComponent" :is="item.iconComponent" :size="16" />
|
||||
{{ item.name }}
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<div class="nav-section">Displays</div>
|
||||
<a href="/shopfloor" target="_blank" class="external-link">Shopfloor Dashboard</a>
|
||||
@@ -42,8 +33,8 @@
|
||||
|
||||
<div class="sidebar-footer">
|
||||
<button class="theme-toggle" @click="toggleTheme" :title="currentTheme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'">
|
||||
<span v-if="currentTheme === 'dark'">☀️ Light</span>
|
||||
<span v-else>🌙 Dark</span>
|
||||
<span v-if="currentTheme === 'dark'"><Sun :size="14" /> Light</span>
|
||||
<span v-else><Moon :size="14" /> Dark</span>
|
||||
</button>
|
||||
|
||||
<div class="user-menu">
|
||||
@@ -63,15 +54,92 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import {
|
||||
Sun, Moon, LayoutDashboard, Calendar, Map, Cog, Monitor,
|
||||
Printer, Globe, Usb, AppWindow, BookOpen, BarChart3, Bell
|
||||
} from 'lucide-vue-next'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
import { currentTheme, toggleTheme } from '../stores/theme'
|
||||
import { dashboardApi } from '../api'
|
||||
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const searchQuery = ref('')
|
||||
const navItems = ref([])
|
||||
|
||||
// Map backend icon names to Lucide components
|
||||
const iconMap = {
|
||||
'layout-dashboard': LayoutDashboard,
|
||||
'calendar': Calendar,
|
||||
'map': Map,
|
||||
'cog': Cog,
|
||||
'desktop': Monitor,
|
||||
'printer': Printer,
|
||||
'network-wired': Globe,
|
||||
'usb': Usb,
|
||||
'bell': Bell,
|
||||
'app-window': AppWindow,
|
||||
'book-open': BookOpen,
|
||||
'bar-chart-3': BarChart3,
|
||||
}
|
||||
|
||||
// Default navigation (used as fallback if API fails)
|
||||
const defaultNav = [
|
||||
{ name: 'Dashboard', icon: 'layout-dashboard', route: '/', position: 0 },
|
||||
{ name: 'Notifications', icon: 'bell', route: '/notifications', position: 5 },
|
||||
{ name: 'Calendar', icon: 'calendar', route: '/calendar', position: 6 },
|
||||
{ name: 'Map', icon: 'map', route: '/map', position: 4 },
|
||||
{ name: 'Equipment', icon: 'cog', route: '/machines', position: 10 },
|
||||
{ name: 'PCs', icon: 'desktop', route: '/pcs', position: 15 },
|
||||
{ name: 'Network', icon: 'network-wired', route: '/network', position: 18 },
|
||||
{ name: 'Printers', icon: 'printer', route: '/printers', position: 20 },
|
||||
{ name: 'USB Devices', icon: 'usb', route: '/usb', position: 45 },
|
||||
{ name: 'Applications', icon: 'app-window', route: '/applications', position: 30, section: 'information' },
|
||||
{ name: 'Knowledge Base', icon: 'book-open', route: '/knowledgebase', position: 35 },
|
||||
{ name: 'Reports', icon: 'bar-chart-3', route: '/reports', position: 40 },
|
||||
]
|
||||
|
||||
function buildNavItems(items) {
|
||||
// Sort by position
|
||||
const sorted = [...items].sort((a, b) => (a.position || 99) - (b.position || 99))
|
||||
|
||||
// Assign section headers based on position ranges
|
||||
const result = []
|
||||
let currentSection = null
|
||||
|
||||
for (const item of sorted) {
|
||||
let section = null
|
||||
if (item.position >= 10 && item.position < 30 && currentSection !== 'Assets') {
|
||||
section = 'Assets'
|
||||
currentSection = 'Assets'
|
||||
} else if (item.section === 'information' || (item.position >= 30 && item.position < 50 && currentSection !== 'Information')) {
|
||||
if (currentSection !== 'Information') {
|
||||
section = 'Information'
|
||||
currentSection = 'Information'
|
||||
}
|
||||
}
|
||||
|
||||
result.push({
|
||||
...item,
|
||||
section,
|
||||
iconComponent: iconMap[item.icon] || null
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const response = await dashboardApi.navigation()
|
||||
navItems.value = buildNavItems(response.data.data || [])
|
||||
} catch (err) {
|
||||
// Fall back to default navigation if API unavailable
|
||||
navItems.value = buildNavItems(defaultNav)
|
||||
}
|
||||
})
|
||||
|
||||
function performSearch() {
|
||||
if (searchQuery.value.trim()) {
|
||||
|
||||
@@ -32,13 +32,13 @@
|
||||
<!-- Recognition event with employee highlight -->
|
||||
<div v-if="selectedEvent.extendedProps?.typecolor === 'recognition'" class="recognition-header">
|
||||
<div class="recognition-badge">
|
||||
<span class="recognition-icon">🏆</span>
|
||||
<span class="recognition-icon"><Trophy :size="24" /></span>
|
||||
</div>
|
||||
<div class="recognition-info">
|
||||
<div class="recognition-label">Recognition</div>
|
||||
<h2 class="recognition-title">{{ selectedEvent.extendedProps?.message || selectedEvent.title }}</h2>
|
||||
<div v-if="selectedEvent.extendedProps?.employeename || selectedEvent.extendedProps?.employeesso" class="recognition-employee">
|
||||
<span class="employee-icon">👤</span>
|
||||
<span class="employee-icon"><User :size="16" /></span>
|
||||
<span class="employee-name">{{ selectedEvent.extendedProps.employeename || selectedEvent.extendedProps.employeesso }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -88,6 +88,7 @@
|
||||
import { ref, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import FullCalendar from '@fullcalendar/vue3'
|
||||
import dayGridPlugin from '@fullcalendar/daygrid'
|
||||
import { Trophy, User } from 'lucide-vue-next'
|
||||
import { notificationsApi } from '@/api'
|
||||
|
||||
const events = ref([])
|
||||
|
||||
@@ -40,12 +40,12 @@
|
||||
}"
|
||||
@click="selectAsset(asset)"
|
||||
>
|
||||
<span class="asset-icon">{{ getTypeIcon(asset.assettype) }}</span>
|
||||
<span class="asset-icon"><component :is="getTypeIcon(asset.assettype)" :size="16" /></span>
|
||||
<div class="asset-info">
|
||||
<div class="asset-name">{{ asset.name || asset.assetnumber }}</div>
|
||||
<div class="asset-meta">
|
||||
<span class="badge badge-sm">{{ asset.assettype }}</span>
|
||||
<span v-if="asset.mapleft && asset.maptop" class="placed-indicator" title="Placed on map">📍</span>
|
||||
<span v-if="asset.mapleft && asset.maptop" class="placed-indicator" title="Placed on map"><MapPin :size="12" /></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -105,6 +105,7 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { Cog, Monitor, Printer, Globe, Package, MapPin } from 'lucide-vue-next'
|
||||
import ShopFloorMap from '../components/ShopFloorMap.vue'
|
||||
import { assetsApi } from '../api'
|
||||
import { currentTheme } from '../stores/theme'
|
||||
@@ -152,12 +153,12 @@ async function loadAssets() {
|
||||
|
||||
function getTypeIcon(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 selectAsset(asset) {
|
||||
|
||||
@@ -85,24 +85,22 @@
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="pagination" v-if="totalPages > 1">
|
||||
<button
|
||||
v-for="p in visiblePages"
|
||||
: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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { applicationsApi } from '../../api'
|
||||
import PaginationBar from '../../components/PaginationBar.vue'
|
||||
|
||||
const applications = ref([])
|
||||
const loading = ref(true)
|
||||
@@ -110,20 +108,10 @@ const search = ref('')
|
||||
const filter = ref('installable')
|
||||
const page = ref(1)
|
||||
const totalPages = ref(1)
|
||||
const perPage = 20
|
||||
const perPage = ref(20)
|
||||
|
||||
let searchTimeout = null
|
||||
|
||||
const visiblePages = computed(() => {
|
||||
const pages = []
|
||||
const start = Math.max(1, page.value - 2)
|
||||
const end = Math.min(totalPages.value, page.value + 2)
|
||||
for (let i = start; i <= end; i++) {
|
||||
pages.push(i)
|
||||
}
|
||||
return pages
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
loadApplications()
|
||||
})
|
||||
@@ -133,7 +121,7 @@ async function loadApplications() {
|
||||
try {
|
||||
const params = {
|
||||
page: page.value,
|
||||
per_page: perPage,
|
||||
perpage: perPage.value,
|
||||
search: search.value || undefined
|
||||
}
|
||||
|
||||
@@ -146,7 +134,7 @@ async function loadApplications() {
|
||||
|
||||
const response = await applicationsApi.list(params)
|
||||
applications.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 applications:', error)
|
||||
} finally {
|
||||
@@ -166,6 +154,12 @@ function goToPage(p) {
|
||||
page.value = p
|
||||
loadApplications()
|
||||
}
|
||||
|
||||
function changePerPage(newPerPage) {
|
||||
perPage.value = newPerPage
|
||||
page.value = 1
|
||||
loadApplications()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -105,7 +105,7 @@ const applications = ref([])
|
||||
onMounted(async () => {
|
||||
try {
|
||||
// Load applications for topic dropdown
|
||||
const appsRes = await applicationsApi.list({ per_page: 1000 })
|
||||
const appsRes = await applicationsApi.list({ perpage: 1000 })
|
||||
applications.value = appsRes.data.data || []
|
||||
|
||||
// Load article if editing
|
||||
|
||||
@@ -95,31 +95,29 @@
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="pagination" v-if="totalPages > 1">
|
||||
<button
|
||||
v-for="p in visiblePages"
|
||||
: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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { knowledgebaseApi, applicationsApi } from '../../api'
|
||||
import PaginationBar from '../../components/PaginationBar.vue'
|
||||
|
||||
const loading = ref(true)
|
||||
const articles = ref([])
|
||||
const topics = ref([])
|
||||
const stats = ref(null)
|
||||
const page = ref(1)
|
||||
const perPage = 20
|
||||
const perPage = ref(20)
|
||||
const totalPages = ref(1)
|
||||
const search = ref('')
|
||||
const topicFilter = ref('')
|
||||
@@ -128,16 +126,6 @@ const order = ref('desc')
|
||||
|
||||
let searchTimeout = null
|
||||
|
||||
const visiblePages = computed(() => {
|
||||
const pages = []
|
||||
const start = Math.max(1, page.value - 2)
|
||||
const end = Math.min(totalPages.value, page.value + 2)
|
||||
for (let i = start; i <= end; i++) {
|
||||
pages.push(i)
|
||||
}
|
||||
return pages
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
await Promise.all([
|
||||
loadArticles(),
|
||||
@@ -151,7 +139,7 @@ async function loadArticles() {
|
||||
try {
|
||||
const params = {
|
||||
page: page.value,
|
||||
per_page: perPage,
|
||||
perpage: perPage.value,
|
||||
sort: sort.value,
|
||||
order: order.value
|
||||
}
|
||||
@@ -160,7 +148,7 @@ async function loadArticles() {
|
||||
|
||||
const response = await knowledgebaseApi.list(params)
|
||||
articles.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 articles:', error)
|
||||
} finally {
|
||||
@@ -170,7 +158,7 @@ async function loadArticles() {
|
||||
|
||||
async function loadTopics() {
|
||||
try {
|
||||
const response = await applicationsApi.list({ per_page: 1000 })
|
||||
const response = await applicationsApi.list({ perpage: 1000 })
|
||||
topics.value = response.data.data || []
|
||||
} catch (error) {
|
||||
console.error('Error loading topics:', error)
|
||||
@@ -199,6 +187,12 @@ function goToPage(p) {
|
||||
loadArticles()
|
||||
}
|
||||
|
||||
function changePerPage(newPerPage) {
|
||||
perPage.value = newPerPage
|
||||
page.value = 1
|
||||
loadArticles()
|
||||
}
|
||||
|
||||
function toggleSort(column) {
|
||||
if (sort.value === column) {
|
||||
order.value = order.value === 'desc' ? 'asc' : 'desc'
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
<div class="hero-card">
|
||||
<div class="hero-image">
|
||||
<div class="device-icon">
|
||||
<span class="icon">{{ getDeviceIcon() }}</span>
|
||||
<span class="icon"><component :is="getDeviceIcon()" :size="24" /></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hero-content">
|
||||
<div class="hero-title-row">
|
||||
<h1 class="hero-title">{{ device.network_device?.hostname || device.name || device.assetnumber }}</h1>
|
||||
<h1 class="hero-title">{{ device.networkdevice?.hostname || device.name || device.assetnumber }}</h1>
|
||||
<router-link
|
||||
v-if="authStore.isAuthenticated"
|
||||
:to="`/network/${deviceId}/edit`"
|
||||
@@ -18,14 +18,14 @@
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="hero-meta">
|
||||
<span class="badge" :class="getStatusClass(device.status_name)">
|
||||
{{ device.status_name || 'Unknown' }}
|
||||
<span class="badge" :class="getStatusClass(device.statusname)">
|
||||
{{ device.statusname || 'Unknown' }}
|
||||
</span>
|
||||
<span v-if="device.network_device?.networkdevicetype_name" class="meta-item">
|
||||
{{ device.network_device.networkdevicetype_name }}
|
||||
<span v-if="device.networkdevice?.networkdevicetypename" class="meta-item">
|
||||
{{ device.networkdevice.networkdevicetypename }}
|
||||
</span>
|
||||
<span v-if="device.network_device?.vendor_name" class="meta-item">
|
||||
{{ device.network_device.vendor_name }}
|
||||
<span v-if="device.networkdevice?.vendorname" class="meta-item">
|
||||
{{ device.networkdevice.vendorname }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="hero-details">
|
||||
@@ -37,19 +37,19 @@
|
||||
<span class="label">Serial</span>
|
||||
<span class="value mono">{{ device.serialnumber }}</span>
|
||||
</div>
|
||||
<div class="detail-item" v-if="device.location_name">
|
||||
<div class="detail-item" v-if="device.locationname">
|
||||
<span class="label">Location</span>
|
||||
<span class="value">{{ device.location_name }}</span>
|
||||
<span class="value">{{ device.locationname }}</span>
|
||||
</div>
|
||||
<div class="detail-item" v-if="device.businessunit_name">
|
||||
<div class="detail-item" v-if="device.businessunitname">
|
||||
<span class="label">Business Unit</span>
|
||||
<span class="value">{{ device.businessunit_name }}</span>
|
||||
<span class="value">{{ device.businessunitname }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hero-features" v-if="device.network_device">
|
||||
<span v-if="device.network_device.ispoe" class="feature-badge poe">PoE</span>
|
||||
<span v-if="device.network_device.ismanaged" class="feature-badge managed">Managed</span>
|
||||
<span v-if="device.network_device.portcount" class="feature-badge ports">{{ device.network_device.portcount }} Ports</span>
|
||||
<div class="hero-features" v-if="device.networkdevice">
|
||||
<span v-if="device.networkdevice.ispoe" class="feature-badge poe">PoE</span>
|
||||
<span v-if="device.networkdevice.ismanaged" class="feature-badge managed">Managed</span>
|
||||
<span v-if="device.networkdevice.portcount" class="feature-badge ports">{{ device.networkdevice.portcount }} Ports</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -62,27 +62,27 @@
|
||||
<div class="info-list">
|
||||
<div class="info-row">
|
||||
<span class="info-label">Hostname</span>
|
||||
<span class="info-value mono">{{ device.network_device?.hostname || '-' }}</span>
|
||||
<span class="info-value mono">{{ device.networkdevice?.hostname || '-' }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">Firmware Version</span>
|
||||
<span class="info-value">{{ device.network_device?.firmwareversion || '-' }}</span>
|
||||
<span class="info-value">{{ device.networkdevice?.firmwareversion || '-' }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">Port Count</span>
|
||||
<span class="info-value">{{ device.network_device?.portcount || '-' }}</span>
|
||||
<span class="info-value">{{ device.networkdevice?.portcount || '-' }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">Rack Unit</span>
|
||||
<span class="info-value">{{ device.network_device?.rackunit || '-' }}</span>
|
||||
<span class="info-value">{{ device.networkdevice?.rackunit || '-' }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">PoE Capable</span>
|
||||
<span class="info-value">{{ device.network_device?.ispoe ? 'Yes' : 'No' }}</span>
|
||||
<span class="info-value">{{ device.networkdevice?.ispoe ? 'Yes' : 'No' }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">Managed Device</span>
|
||||
<span class="info-value">{{ device.network_device?.ismanaged ? 'Yes' : 'No' }}</span>
|
||||
<span class="info-value">{{ device.networkdevice?.ismanaged ? 'Yes' : 'No' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -113,17 +113,17 @@
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">Vendor</span>
|
||||
<span class="info-value">{{ device.network_device?.vendor_name || '-' }}</span>
|
||||
<span class="info-value">{{ device.networkdevice?.vendorname || '-' }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">Device Type</span>
|
||||
<span class="info-value">{{ device.network_device?.networkdevicetype_name || '-' }}</span>
|
||||
<span class="info-value">{{ device.networkdevice?.networkdevicetypename || '-' }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">Status</span>
|
||||
<span class="info-value">
|
||||
<span class="badge" :class="getStatusClass(device.status_name)">
|
||||
{{ device.status_name || 'Unknown' }}
|
||||
<span class="badge" :class="getStatusClass(device.statusname)">
|
||||
{{ device.statusname || 'Unknown' }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
@@ -177,6 +177,7 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { Network, Router, Shield, Wifi, Camera, Server, Server as Rack, Globe } from 'lucide-vue-next'
|
||||
import { useAuthStore } from '../../stores/auth'
|
||||
import { networkApi } from '../../api'
|
||||
import AssetRelationships from '../../components/AssetRelationships.vue'
|
||||
@@ -207,15 +208,15 @@ async function loadDevice() {
|
||||
}
|
||||
|
||||
function getDeviceIcon() {
|
||||
const type = device.value?.network_device?.networkdevicetype_name?.toLowerCase() || ''
|
||||
if (type.includes('switch')) return '⏛'
|
||||
if (type.includes('router')) return '⇌'
|
||||
if (type.includes('firewall')) return '🛡'
|
||||
if (type.includes('access point') || type.includes('ap')) return '📶'
|
||||
if (type.includes('camera')) return '📷'
|
||||
if (type.includes('server')) return '🖥'
|
||||
if (type.includes('idf') || type.includes('closet')) return '🗄'
|
||||
return '🌐'
|
||||
const type = device.value?.networkdevice?.networkdevicetypename?.toLowerCase() || ''
|
||||
if (type.includes('switch')) return Network
|
||||
if (type.includes('router')) return Router
|
||||
if (type.includes('firewall')) return Shield
|
||||
if (type.includes('access point') || type.includes('ap')) return Wifi
|
||||
if (type.includes('camera')) return Camera
|
||||
if (type.includes('server')) return Server
|
||||
if (type.includes('idf') || type.includes('closet')) return Rack
|
||||
return Globe
|
||||
}
|
||||
|
||||
function getStatusClass(status) {
|
||||
@@ -240,7 +241,7 @@ function formatDate(dateStr) {
|
||||
}
|
||||
|
||||
async function confirmDelete() {
|
||||
if (confirm(`Are you sure you want to delete ${device.value.network_device?.hostname || device.value.assetnumber}?`)) {
|
||||
if (confirm(`Are you sure you want to delete ${device.value.networkdevice?.hostname || device.value.assetnumber}?`)) {
|
||||
try {
|
||||
await networkApi.delete(deviceId)
|
||||
router.push('/network')
|
||||
|
||||
@@ -280,7 +280,7 @@ onMounted(async () => {
|
||||
|
||||
async function loadDeviceTypes() {
|
||||
try {
|
||||
const response = await networkApi.types.list({ per_page: 100 })
|
||||
const response = await networkApi.types.list({ perpage: 100 })
|
||||
deviceTypes.value = response.data.data || []
|
||||
} catch (err) {
|
||||
console.error('Error loading device types:', err)
|
||||
@@ -289,7 +289,7 @@ async function loadDeviceTypes() {
|
||||
|
||||
async function loadVendors() {
|
||||
try {
|
||||
const response = await vendorsApi.list({ per_page: 100 })
|
||||
const response = await vendorsApi.list({ perpage: 100 })
|
||||
vendors.value = response.data.data || []
|
||||
} catch (err) {
|
||||
console.error('Error loading vendors:', err)
|
||||
@@ -298,7 +298,7 @@ async function loadVendors() {
|
||||
|
||||
async function loadLocations() {
|
||||
try {
|
||||
const response = await locationsApi.list({ per_page: 100 })
|
||||
const response = await locationsApi.list({ perpage: 100 })
|
||||
locations.value = response.data.data || []
|
||||
} catch (err) {
|
||||
console.error('Error loading locations:', err)
|
||||
@@ -307,7 +307,7 @@ async function loadLocations() {
|
||||
|
||||
async function loadStatuses() {
|
||||
try {
|
||||
const response = await statusesApi.list({ per_page: 100 })
|
||||
const response = await statusesApi.list({ perpage: 100 })
|
||||
statuses.value = response.data.data || []
|
||||
} catch (err) {
|
||||
console.error('Error loading statuses:', err)
|
||||
@@ -316,7 +316,7 @@ async function loadStatuses() {
|
||||
|
||||
async function loadBusinessUnits() {
|
||||
try {
|
||||
const response = await businessunitsApi.list({ per_page: 100 })
|
||||
const response = await businessunitsApi.list({ perpage: 100 })
|
||||
businessUnits.value = response.data.data || []
|
||||
} catch (err) {
|
||||
console.error('Error loading business units:', err)
|
||||
@@ -340,15 +340,15 @@ async function loadDevice() {
|
||||
form.value.notes = data.notes || ''
|
||||
|
||||
// Network device specific
|
||||
if (data.network_device) {
|
||||
form.value.hostname = data.network_device.hostname || ''
|
||||
form.value.networkdevicetypeid = data.network_device.networkdevicetypeid || ''
|
||||
form.value.vendorid = data.network_device.vendorid || ''
|
||||
form.value.firmwareversion = data.network_device.firmwareversion || ''
|
||||
form.value.portcount = data.network_device.portcount
|
||||
form.value.rackunit = data.network_device.rackunit || ''
|
||||
form.value.ispoe = data.network_device.ispoe || false
|
||||
form.value.ismanaged = data.network_device.ismanaged || false
|
||||
if (data.networkdevice) {
|
||||
form.value.hostname = data.networkdevice.hostname || ''
|
||||
form.value.networkdevicetypeid = data.networkdevice.networkdevicetypeid || ''
|
||||
form.value.vendorid = data.networkdevice.vendorid || ''
|
||||
form.value.firmwareversion = data.networkdevice.firmwareversion || ''
|
||||
form.value.portcount = data.networkdevice.portcount
|
||||
form.value.rackunit = data.networkdevice.rackunit || ''
|
||||
form.value.ispoe = data.networkdevice.ispoe || false
|
||||
form.value.ismanaged = data.networkdevice.ismanaged || false
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading device:', err)
|
||||
@@ -386,7 +386,7 @@ async function submitForm() {
|
||||
router.push(`/network/${deviceId}`)
|
||||
} else {
|
||||
const response = await networkApi.create(payload)
|
||||
const newId = response.data.data?.network_device?.networkdeviceid
|
||||
const newId = response.data.data?.networkdevice?.networkdeviceid
|
||||
router.push(newId ? `/network/${newId}` : '/network')
|
||||
}
|
||||
} catch (err) {
|
||||
|
||||
@@ -54,37 +54,39 @@
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Hostname</th>
|
||||
<th>Asset #</th>
|
||||
<th>Hostname</th>
|
||||
<th>Serial Number</th>
|
||||
<th>Type</th>
|
||||
<th>Vendor</th>
|
||||
<th>Features</th>
|
||||
<th>Location</th>
|
||||
<th>Status</th>
|
||||
<th>Location</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="device in devices" :key="device.network_device?.networkdeviceid || device.assetid">
|
||||
<td class="mono">{{ device.network_device?.hostname || '-' }}</td>
|
||||
<tr v-for="device in devices" :key="device.networkdevice?.networkdeviceid || device.assetid">
|
||||
<td>{{ device.assetnumber }}</td>
|
||||
<td>{{ device.network_device?.networkdevicetype_name || '-' }}</td>
|
||||
<td>{{ device.network_device?.vendor_name || '-' }}</td>
|
||||
<td class="mono">{{ device.networkdevice?.hostname || '-' }}</td>
|
||||
<td class="mono">{{ device.serialnumber || '-' }}</td>
|
||||
<td>{{ device.networkdevice?.networkdevicetypename || '-' }}</td>
|
||||
<td>{{ device.networkdevice?.vendorname || '-' }}</td>
|
||||
<td class="features">
|
||||
<span v-if="device.network_device?.ispoe" class="feature-tag poe">PoE</span>
|
||||
<span v-if="device.network_device?.ismanaged" class="feature-tag managed">Managed</span>
|
||||
<span v-if="device.network_device?.portcount" class="feature-tag ports">{{ device.network_device.portcount }} ports</span>
|
||||
<span v-if="!device.network_device?.ispoe && !device.network_device?.ismanaged && !device.network_device?.portcount">-</span>
|
||||
<span v-if="device.networkdevice?.ispoe" class="feature-tag poe">PoE</span>
|
||||
<span v-if="device.networkdevice?.ismanaged" class="feature-tag managed">Managed</span>
|
||||
<span v-if="device.networkdevice?.portcount" class="feature-tag ports">{{ device.networkdevice.portcount }} ports</span>
|
||||
<span v-if="!device.networkdevice?.ispoe && !device.networkdevice?.ismanaged && !device.networkdevice?.portcount">-</span>
|
||||
</td>
|
||||
<td>{{ device.location_name || '-' }}</td>
|
||||
<td>
|
||||
<span class="badge" :class="getStatusClass(device.status_name)">
|
||||
{{ device.status_name || 'Unknown' }}
|
||||
<span class="badge" :class="getStatusClass(device.statusname)">
|
||||
{{ device.statusname || 'Unknown' }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ device.locationname || '-' }}</td>
|
||||
<td class="actions">
|
||||
<router-link
|
||||
:to="`/network/${device.network_device?.networkdeviceid}`"
|
||||
:to="`/network/${device.networkdevice?.networkdeviceid}`"
|
||||
class="btn btn-secondary btn-sm"
|
||||
>
|
||||
View
|
||||
@@ -92,7 +94,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="devices.length === 0">
|
||||
<td colspan="8" style="text-align: center; color: var(--text-light);">
|
||||
<td colspan="9" style="text-align: center; color: var(--text-light);">
|
||||
No network devices found
|
||||
</td>
|
||||
</tr>
|
||||
@@ -101,36 +103,22 @@
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="pagination" v-if="totalPages > 1">
|
||||
<button
|
||||
:disabled="page === 1"
|
||||
@click="goToPage(page - 1)"
|
||||
>
|
||||
Prev
|
||||
</button>
|
||||
<button
|
||||
v-for="p in visiblePages"
|
||||
:key="p"
|
||||
:class="{ active: p === page }"
|
||||
@click="goToPage(p)"
|
||||
>
|
||||
{{ p }}
|
||||
</button>
|
||||
<button
|
||||
:disabled="page === totalPages"
|
||||
@click="goToPage(page + 1)"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
<PaginationBar
|
||||
:page="page"
|
||||
:totalPages="totalPages"
|
||||
:perPage="perPage"
|
||||
@update:page="goToPage"
|
||||
@update:perPage="changePerPage"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { networkApi, vendorsApi, locationsApi } from '../../api'
|
||||
import PaginationBar from '../../components/PaginationBar.vue'
|
||||
|
||||
const devices = ref([])
|
||||
const deviceTypes = ref([])
|
||||
@@ -143,20 +131,11 @@ const vendorFilter = ref('')
|
||||
const locationFilter = ref('')
|
||||
const page = ref(1)
|
||||
const totalPages = ref(1)
|
||||
const perPage = ref(25)
|
||||
const totalCount = ref(0)
|
||||
|
||||
let searchTimeout = null
|
||||
|
||||
const visiblePages = computed(() => {
|
||||
const pages = []
|
||||
const start = Math.max(1, page.value - 2)
|
||||
const end = Math.min(totalPages.value, page.value + 2)
|
||||
for (let i = start; i <= end; i++) {
|
||||
pages.push(i)
|
||||
}
|
||||
return pages
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
await Promise.all([
|
||||
loadDeviceTypes(),
|
||||
@@ -168,7 +147,7 @@ onMounted(async () => {
|
||||
|
||||
async function loadDeviceTypes() {
|
||||
try {
|
||||
const response = await networkApi.types.list({ per_page: 100 })
|
||||
const response = await networkApi.types.list({ perpage: 100 })
|
||||
deviceTypes.value = response.data.data || []
|
||||
// Get counts for each type
|
||||
await updateTypeCounts()
|
||||
@@ -181,7 +160,7 @@ async function updateTypeCounts() {
|
||||
// Get summary for type counts
|
||||
try {
|
||||
const response = await networkApi.dashboardSummary()
|
||||
const byType = response.data.data?.by_type || []
|
||||
const byType = response.data.data?.bytype || response.data.data?.by_type || []
|
||||
totalCount.value = response.data.data?.total || 0
|
||||
|
||||
// Map counts to types
|
||||
@@ -196,7 +175,7 @@ async function updateTypeCounts() {
|
||||
|
||||
async function loadVendors() {
|
||||
try {
|
||||
const response = await vendorsApi.list({ per_page: 100 })
|
||||
const response = await vendorsApi.list({ perpage: 100 })
|
||||
vendors.value = response.data.data || []
|
||||
} catch (error) {
|
||||
console.error('Error loading vendors:', error)
|
||||
@@ -205,7 +184,7 @@ async function loadVendors() {
|
||||
|
||||
async function loadLocations() {
|
||||
try {
|
||||
const response = await locationsApi.list({ per_page: 100 })
|
||||
const response = await locationsApi.list({ perpage: 100 })
|
||||
locations.value = response.data.data || []
|
||||
} catch (error) {
|
||||
console.error('Error loading locations:', error)
|
||||
@@ -217,7 +196,7 @@ async function loadDevices() {
|
||||
try {
|
||||
const params = {
|
||||
page: page.value,
|
||||
per_page: 25
|
||||
perpage: perPage.value
|
||||
}
|
||||
if (search.value) params.search = search.value
|
||||
if (selectedType.value) params.type_id = selectedType.value
|
||||
@@ -226,7 +205,7 @@ async function loadDevices() {
|
||||
|
||||
const response = await networkApi.list(params)
|
||||
devices.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 network devices:', error)
|
||||
} finally {
|
||||
@@ -255,6 +234,12 @@ function goToPage(p) {
|
||||
}
|
||||
}
|
||||
|
||||
function changePerPage(newPerPage) {
|
||||
perPage.value = newPerPage
|
||||
page.value = 1
|
||||
loadDevices()
|
||||
}
|
||||
|
||||
function getStatusClass(status) {
|
||||
if (!status) return 'badge-info'
|
||||
const s = status.toLowerCase()
|
||||
|
||||
@@ -287,7 +287,7 @@ onMounted(async () => {
|
||||
const [typesRes, buRes, appsRes] = await Promise.all([
|
||||
notificationsApi.types.list(),
|
||||
businessUnitsApi.list().catch(() => ({ data: { data: [] } })),
|
||||
applicationsApi.list({ per_page: 500 }).catch(() => ({ data: { data: [] } }))
|
||||
applicationsApi.list({ perpage: 500 }).catch(() => ({ data: { data: [] } }))
|
||||
])
|
||||
|
||||
types.value = typesRes.data.data || []
|
||||
|
||||
@@ -73,22 +73,20 @@
|
||||
</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"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { notificationsApi } from '@/api'
|
||||
import PaginationBar from '../../components/PaginationBar.vue'
|
||||
|
||||
const notifications = ref([])
|
||||
const types = ref([])
|
||||
@@ -122,7 +120,7 @@ async function loadNotifications() {
|
||||
try {
|
||||
const params = {
|
||||
page: page.value,
|
||||
per_page: perPage.value
|
||||
perpage: perPage.value
|
||||
}
|
||||
|
||||
if (searchQuery.value) {
|
||||
@@ -161,6 +159,12 @@ function goToPage(p) {
|
||||
loadNotifications()
|
||||
}
|
||||
|
||||
function changePerPage(newPerPage) {
|
||||
perPage.value = newPerPage
|
||||
page.value = 1
|
||||
loadNotifications()
|
||||
}
|
||||
|
||||
function formatDate(dateStr) {
|
||||
if (!dateStr) return ''
|
||||
return new Date(dateStr).toLocaleDateString()
|
||||
|
||||
@@ -20,22 +20,22 @@
|
||||
</div>
|
||||
<div class="hero-meta">
|
||||
<span class="badge badge-lg badge-info">Computer</span>
|
||||
<span class="badge badge-lg" :class="getStatusClass(computer.status_name)">
|
||||
{{ computer.status_name || 'Unknown' }}
|
||||
<span class="badge badge-lg" :class="getStatusClass(computer.statusname)">
|
||||
{{ computer.statusname || 'Unknown' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="hero-details">
|
||||
<div class="hero-detail" v-if="computer.computer?.computertype_name">
|
||||
<div class="hero-detail" v-if="computer.computer?.computertypename">
|
||||
<span class="hero-detail-label">Type</span>
|
||||
<span class="hero-detail-value">{{ computer.computer.computertype_name }}</span>
|
||||
<span class="hero-detail-value">{{ computer.computer.computertypename }}</span>
|
||||
</div>
|
||||
<div class="hero-detail" v-if="computer.computer?.os_name">
|
||||
<div class="hero-detail" v-if="computer.computer?.osname">
|
||||
<span class="hero-detail-label">OS</span>
|
||||
<span class="hero-detail-value">{{ computer.computer.os_name }}</span>
|
||||
<span class="hero-detail-value">{{ computer.computer.osname }}</span>
|
||||
</div>
|
||||
<div class="hero-detail" v-if="computer.location_name">
|
||||
<div class="hero-detail" v-if="computer.locationname">
|
||||
<span class="hero-detail-label">Location</span>
|
||||
<span class="hero-detail-value">{{ computer.location_name }}</span>
|
||||
<span class="hero-detail-value">{{ computer.locationname }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -74,11 +74,11 @@
|
||||
<div class="info-list">
|
||||
<div class="info-row">
|
||||
<span class="info-label">Computer Type</span>
|
||||
<span class="info-value">{{ computer.computer?.computertype_name || '-' }}</span>
|
||||
<span class="info-value">{{ computer.computer?.computertypename || '-' }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">Operating System</span>
|
||||
<span class="info-value">{{ computer.computer?.os_name || '-' }}</span>
|
||||
<span class="info-value">{{ computer.computer?.osname || '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -126,14 +126,14 @@
|
||||
:top="computer.maptop"
|
||||
:machineName="computer.assetnumber"
|
||||
>
|
||||
<span class="location-link">{{ computer.location_name || 'On Map' }}</span>
|
||||
<span class="location-link">{{ computer.locationname || 'On Map' }}</span>
|
||||
</LocationMapTooltip>
|
||||
<span v-else>{{ computer.location_name || '-' }}</span>
|
||||
<span v-else>{{ computer.locationname || '-' }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">Business Unit</span>
|
||||
<span class="info-value">{{ computer.businessunit_name || '-' }}</span>
|
||||
<span class="info-value">{{ computer.businessunitname || '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -145,7 +145,7 @@
|
||||
<router-link
|
||||
v-for="item in controlledEquipment"
|
||||
:key="item.relationshipid"
|
||||
:to="`/machines/${item.plugin_id || item.assetid}`"
|
||||
:to="`/machines/${item.pluginid || item.assetid}`"
|
||||
class="equipment-item"
|
||||
>
|
||||
<div class="equipment-info">
|
||||
@@ -153,7 +153,7 @@
|
||||
<span class="equipment-alias" v-if="item.name">{{ item.name }}</span>
|
||||
</div>
|
||||
<div class="equipment-meta">
|
||||
<span class="category-tag">{{ item.assettype_name }}</span>
|
||||
<span class="category-tag">{{ item.assettypename }}</span>
|
||||
<span class="connection-tag">{{ item.relationshipType }}</span>
|
||||
</div>
|
||||
</router-link>
|
||||
@@ -221,22 +221,22 @@ const controlledEquipment = computed(() => {
|
||||
|
||||
// Check outgoing - computer controls equipment
|
||||
for (const rel of relationships.value.outgoing || []) {
|
||||
if (rel.target_asset?.assettype_name === 'equipment' && rel.relationship_type_name === 'Controls') {
|
||||
if (rel.targetasset?.assettypename === 'equipment' && rel.relationshiptypename === 'Controls') {
|
||||
items.push({
|
||||
...rel.target_asset,
|
||||
...rel.targetasset,
|
||||
relationshipid: rel.relationshipid,
|
||||
relationshipType: rel.relationship_type_name
|
||||
relationshipType: rel.relationshiptypename
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Also check incoming - legacy data may have equipment -> computer Controls relationships
|
||||
for (const rel of relationships.value.incoming || []) {
|
||||
if (rel.source_asset?.assettype_name === 'equipment' && rel.relationship_type_name === 'Controls') {
|
||||
if (rel.sourceasset?.assettypename === 'equipment' && rel.relationshiptypename === 'Controls') {
|
||||
items.push({
|
||||
...rel.source_asset,
|
||||
...rel.sourceasset,
|
||||
relationshipid: rel.relationshipid,
|
||||
relationshipType: rel.relationship_type_name
|
||||
relationshipType: rel.relationshiptypename
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
<th>Type</th>
|
||||
<th>Features</th>
|
||||
<th>Status</th>
|
||||
<th>Location</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -38,17 +39,18 @@
|
||||
<td>{{ item.assetnumber }}</td>
|
||||
<td>{{ item.computer?.hostname || '-' }}</td>
|
||||
<td class="mono">{{ item.serialnumber || '-' }}</td>
|
||||
<td>{{ item.computer?.computertype_name || '-' }}</td>
|
||||
<td>{{ item.computer?.computertypename || '-' }}</td>
|
||||
<td class="features">
|
||||
<span v-if="item.computer?.isvnc" class="feature-tag active">VNC</span>
|
||||
<span v-if="item.computer?.iswinrm" class="feature-tag active">WinRM</span>
|
||||
<span v-if="!item.computer?.isvnc && !item.computer?.iswinrm">-</span>
|
||||
</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.locationname || '-' }}</td>
|
||||
<td class="actions">
|
||||
<router-link
|
||||
:to="`/pcs/${item.computer?.computerid || item.assetid}`"
|
||||
@@ -59,7 +61,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="computers.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 computers found
|
||||
</td>
|
||||
</tr>
|
||||
@@ -68,16 +70,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>
|
||||
</div>
|
||||
@@ -86,12 +85,14 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { computersApi } from '../../api'
|
||||
import PaginationBar from '../../components/PaginationBar.vue'
|
||||
|
||||
const computers = ref([])
|
||||
const loading = ref(true)
|
||||
const search = ref('')
|
||||
const page = ref(1)
|
||||
const totalPages = ref(1)
|
||||
const perPage = ref(20)
|
||||
|
||||
let searchTimeout = null
|
||||
|
||||
@@ -104,13 +105,13 @@ async function loadComputers() {
|
||||
try {
|
||||
const params = {
|
||||
page: page.value,
|
||||
per_page: 20
|
||||
perpage: perPage.value
|
||||
}
|
||||
if (search.value) params.search = search.value
|
||||
|
||||
const response = await computersApi.list(params)
|
||||
computers.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 computers:', error)
|
||||
} finally {
|
||||
@@ -131,6 +132,12 @@ function goToPage(p) {
|
||||
loadComputers()
|
||||
}
|
||||
|
||||
function changePerPage(newPerPage) {
|
||||
perPage.value = newPerPage
|
||||
page.value = 1
|
||||
loadComputers()
|
||||
}
|
||||
|
||||
function getStatusClass(status) {
|
||||
if (!status) return 'badge-info'
|
||||
const s = status.toLowerCase()
|
||||
|
||||
166
frontend/src/views/print/EquipmentBadge.vue
Normal file
166
frontend/src/views/print/EquipmentBadge.vue
Normal file
@@ -0,0 +1,166 @@
|
||||
<template>
|
||||
<div>
|
||||
<button class="print-btn" @click="print" v-if="!loading">Print Badge</button>
|
||||
<div v-if="loading" class="loading-msg">Loading...</div>
|
||||
<div v-else-if="equipment" class="badge-container">
|
||||
<div class="model-name">{{ modelName }}</div>
|
||||
<img
|
||||
v-if="isInspection"
|
||||
class="machine-image"
|
||||
:src="geLogo"
|
||||
alt="GE Logo"
|
||||
/>
|
||||
<img
|
||||
v-else-if="imageUrl"
|
||||
class="machine-image"
|
||||
:src="imageUrl"
|
||||
:alt="modelName"
|
||||
/>
|
||||
<div class="barcode-container">
|
||||
<svg ref="barcodeEl"></svg>
|
||||
<div class="machine-number">*{{ equipment.assetnumber }}*</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="error-msg">Equipment not found</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, nextTick } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { equipmentApi } from '../../api'
|
||||
import JsBarcode from 'jsbarcode'
|
||||
|
||||
const route = useRoute()
|
||||
const loading = ref(true)
|
||||
const equipment = ref(null)
|
||||
const barcodeEl = ref(null)
|
||||
|
||||
const geLogo = '/images/applications/GE-Logo.png'
|
||||
|
||||
const isInspection = computed(() => {
|
||||
if (!equipment.value) return false
|
||||
return equipment.value.assetnumber?.startsWith('06')
|
||||
})
|
||||
|
||||
const modelName = computed(() => {
|
||||
if (!equipment.value) return ''
|
||||
if (isInspection.value) return 'Inspection'
|
||||
return equipment.value.equipment?.modelname || ''
|
||||
})
|
||||
|
||||
const imageUrl = computed(() => {
|
||||
if (!equipment.value) return null
|
||||
const img = equipment.value.equipment?.imageurl
|
||||
if (img) return img.startsWith('/') ? img : `/images/models/machines/${img}`
|
||||
return null
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const response = await equipmentApi.get(route.params.id)
|
||||
equipment.value = response.data.data
|
||||
} catch (error) {
|
||||
console.error('Error loading equipment:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
await nextTick()
|
||||
generateBarcode()
|
||||
}
|
||||
})
|
||||
|
||||
function generateBarcode() {
|
||||
if (!barcodeEl.value || !equipment.value) return
|
||||
try {
|
||||
JsBarcode(barcodeEl.value, equipment.value.assetnumber, {
|
||||
format: 'CODE39',
|
||||
displayValue: false,
|
||||
width: 2,
|
||||
height: 70,
|
||||
margin: 0
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('Barcode generation error:', e)
|
||||
}
|
||||
}
|
||||
|
||||
function print() {
|
||||
window.print()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@page { size: 2.13in 3.38in; margin: 0; }
|
||||
|
||||
body { font-family: Arial, sans-serif; }
|
||||
|
||||
.badge-container {
|
||||
width: 2.13in;
|
||||
height: 3.38in;
|
||||
background: white;
|
||||
margin: 0 auto;
|
||||
border: 1px solid #ccc;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 0.15in;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.model-name {
|
||||
font-size: 12pt;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
margin-bottom: 0.1in;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.machine-image {
|
||||
max-width: 1.8in;
|
||||
max-height: 1.5in;
|
||||
object-fit: contain;
|
||||
margin-bottom: 0.1in;
|
||||
}
|
||||
|
||||
.barcode-container {
|
||||
text-align: center;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.barcode-container svg {
|
||||
width: 1.8in;
|
||||
height: 0.9in;
|
||||
}
|
||||
|
||||
.machine-number {
|
||||
font-size: 14pt;
|
||||
font-weight: bold;
|
||||
font-family: monospace;
|
||||
margin-top: -0.1in;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.print-btn {
|
||||
display: block;
|
||||
margin: 20px auto;
|
||||
padding: 10px 30px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.loading-msg, .error-msg {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
font-size: 1.125rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
@media print {
|
||||
.print-btn { display: none; }
|
||||
.badge-container { border: none; margin: 0; }
|
||||
}
|
||||
</style>
|
||||
291
frontend/src/views/print/PrinterQRBatch.vue
Normal file
291
frontend/src/views/print/PrinterQRBatch.vue
Normal file
@@ -0,0 +1,291 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="no-print">
|
||||
<div class="controls">
|
||||
<h3>Batch Print Printer QR Codes</h3>
|
||||
<p>Select printers to print (6 per page):</p>
|
||||
|
||||
<div v-if="loadingPrinters" class="loading-msg">Loading printers...</div>
|
||||
<div v-else class="printer-grid">
|
||||
<div
|
||||
v-for="printer in printers"
|
||||
:key="printer.assetid"
|
||||
class="printer-item"
|
||||
:class="{ selected: isSelected(printer) }"
|
||||
@click="togglePrinter(printer)"
|
||||
>
|
||||
<input type="checkbox" :checked="isSelected(printer)" @click.stop />
|
||||
<label>
|
||||
<strong>{{ displayName(printer) }}</strong>
|
||||
<div class="model">{{ printer.printer?.modelname || '' }}</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="selected-count">
|
||||
Selected: <span class="count">{{ selectedPrinters.length }}</span> printers
|
||||
(<span class="pages">{{ pageCount }}</span> pages)
|
||||
</div>
|
||||
|
||||
<button class="print-btn" :disabled="selectedPrinters.length === 0" @click="print">Print QR Codes</button>
|
||||
<button class="clear-btn" @click="clearSelection">Clear All</button>
|
||||
<button class="select-all-btn" @click="selectAll">Select All</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sheets-container">
|
||||
<div v-for="(page, pageIdx) in pages" :key="pageIdx" class="print-sheet">
|
||||
<div class="sheet-label">Page {{ pageIdx + 1 }} of {{ pageCount }}</div>
|
||||
<div
|
||||
v-for="pos in 6"
|
||||
:key="pos"
|
||||
class="label"
|
||||
:class="[`pos-${pos}`, page[pos - 1] ? 'filled' : 'empty']"
|
||||
>
|
||||
<template v-if="page[pos - 1]">
|
||||
<div class="model-name">{{ page[pos - 1].printer?.modelname || '' }}</div>
|
||||
<div class="qr-container">
|
||||
<canvas :ref="el => setQrRef(el, pageIdx, pos)"></canvas>
|
||||
</div>
|
||||
<div class="info-section">
|
||||
<div class="csf-name">{{ page[pos - 1].assetnumber }}</div>
|
||||
<div class="info-inner">
|
||||
<div v-if="page[pos - 1].printer?.windowsname" class="info-row">{{ page[pos - 1].printer.windowsname }}</div>
|
||||
<div v-if="getIp(page[pos - 1])" class="info-row">{{ getIp(page[pos - 1]) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<span v-else class="empty-label">Empty</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, watch, nextTick } from 'vue'
|
||||
import { printersApi } from '../../api'
|
||||
import QRCode from 'qrcode'
|
||||
|
||||
const printers = ref([])
|
||||
const selectedPrinters = ref([])
|
||||
const loadingPrinters = ref(true)
|
||||
const qrRefs = ref({})
|
||||
|
||||
const pageCount = computed(() => Math.ceil(selectedPrinters.value.length / 6) || 0)
|
||||
|
||||
const pages = computed(() => {
|
||||
const result = []
|
||||
for (let i = 0; i < selectedPrinters.value.length; i += 6) {
|
||||
const page = []
|
||||
for (let j = 0; j < 6; j++) {
|
||||
page.push(selectedPrinters.value[i + j] || null)
|
||||
}
|
||||
result.push(page)
|
||||
}
|
||||
return result
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const response = await printersApi.list({ perpage: 500 })
|
||||
printers.value = response.data.data || []
|
||||
} catch (error) {
|
||||
console.error('Error loading printers:', error)
|
||||
} finally {
|
||||
loadingPrinters.value = false
|
||||
}
|
||||
})
|
||||
|
||||
watch(selectedPrinters, async () => {
|
||||
await nextTick()
|
||||
generateQRCodes()
|
||||
}, { deep: true })
|
||||
|
||||
function setQrRef(el, pageIdx, pos) {
|
||||
if (el) {
|
||||
qrRefs.value[`${pageIdx}-${pos}`] = el
|
||||
}
|
||||
}
|
||||
|
||||
function generateQRCodes() {
|
||||
pages.value.forEach((page, pageIdx) => {
|
||||
page.forEach((printer, idx) => {
|
||||
if (!printer) return
|
||||
const pos = idx + 1
|
||||
const canvas = qrRefs.value[`${pageIdx}-${pos}`]
|
||||
if (!canvas) return
|
||||
|
||||
const qrUrl = `${window.location.origin}/printers/${printer.printer?.printerid || printer.assetid}`
|
||||
QRCode.toCanvas(canvas, qrUrl, {
|
||||
width: 144,
|
||||
margin: 0,
|
||||
errorCorrectionLevel: 'M'
|
||||
}).catch(err => console.error('QR error:', err))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function displayName(printer) {
|
||||
return printer.assetnumber || printer.name || `Printer-${printer.assetid}`
|
||||
}
|
||||
|
||||
function getIp(printer) {
|
||||
if (!printer.communications?.length) return null
|
||||
const primary = printer.communications.find(c => c.isprimary) || printer.communications[0]
|
||||
return primary?.ipaddress || primary?.address || null
|
||||
}
|
||||
|
||||
function isSelected(printer) {
|
||||
return selectedPrinters.value.some(p => p.assetid === printer.assetid)
|
||||
}
|
||||
|
||||
function togglePrinter(printer) {
|
||||
const idx = selectedPrinters.value.findIndex(p => p.assetid === printer.assetid)
|
||||
if (idx > -1) {
|
||||
selectedPrinters.value.splice(idx, 1)
|
||||
} else {
|
||||
selectedPrinters.value.push(printer)
|
||||
}
|
||||
}
|
||||
|
||||
function clearSelection() {
|
||||
selectedPrinters.value = []
|
||||
}
|
||||
|
||||
function selectAll() {
|
||||
selectedPrinters.value = [...printers.value]
|
||||
}
|
||||
|
||||
function print() {
|
||||
window.print()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@page { size: letter; margin: 0; }
|
||||
|
||||
.no-print { margin-bottom: 20px; padding: 20px; }
|
||||
.controls { background: white; padding: 20px; border-radius: 8px; margin-bottom: 20px; }
|
||||
.controls h3 { margin-top: 0; }
|
||||
|
||||
.print-btn {
|
||||
padding: 10px 30px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.print-btn:disabled { background: #ccc; cursor: not-allowed; }
|
||||
|
||||
.clear-btn {
|
||||
padding: 10px 20px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.select-all-btn {
|
||||
padding: 10px 20px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
background: #28a745;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.printer-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
gap: 10px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #ddd;
|
||||
padding: 10px;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.printer-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.printer-item:hover { background: #f0f0f0; }
|
||||
.printer-item.selected { background: #e7f1ff; border-color: #667eea; }
|
||||
.printer-item input { margin-right: 10px; }
|
||||
.printer-item label { cursor: pointer; flex: 1; }
|
||||
.printer-item .model { font-size: 11px; color: #666; }
|
||||
|
||||
.selected-count { font-weight: bold; margin: 10px 0; }
|
||||
.selected-count .count { color: #667eea; }
|
||||
.selected-count .pages { color: #28a745; }
|
||||
|
||||
.loading-msg { text-align: center; padding: 2rem; color: #666; }
|
||||
|
||||
.sheets-container { display: flex; flex-direction: column; gap: 20px; }
|
||||
|
||||
.print-sheet {
|
||||
width: 8.5in;
|
||||
height: 11in;
|
||||
background: white;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
border: 1px solid #ccc;
|
||||
page-break-after: always;
|
||||
}
|
||||
.print-sheet:last-child { page-break-after: auto; }
|
||||
|
||||
.sheet-label { position: absolute; top: -25px; left: 0; font-size: 12px; color: #666; }
|
||||
|
||||
.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;
|
||||
border: 1px dashed #ccc;
|
||||
}
|
||||
.label.filled { border: 2px solid #667eea; }
|
||||
.label.empty { background: #fafafa; }
|
||||
|
||||
.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; }
|
||||
.empty-label { color: #999; font-size: 14px; }
|
||||
|
||||
@media print {
|
||||
body { padding: 0; margin: 0; background: white; }
|
||||
.no-print { display: none !important; }
|
||||
.sheets-container { gap: 0; }
|
||||
.print-sheet { border: none; margin: 0; width: 8.5in; height: 11in; overflow: hidden; }
|
||||
.sheet-label { display: none; }
|
||||
.label { border: none !important; }
|
||||
.label.empty { visibility: hidden; }
|
||||
}
|
||||
</style>
|
||||
169
frontend/src/views/print/PrinterQRSingle.vue
Normal file
169
frontend/src/views/print/PrinterQRSingle.vue
Normal 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>
|
||||
366
frontend/src/views/print/USBLabelBatch.vue
Normal file
366
frontend/src/views/print/USBLabelBatch.vue
Normal file
@@ -0,0 +1,366 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="no-print">
|
||||
<div class="controls">
|
||||
<h3>Batch Print USB Barcode Labels</h3>
|
||||
<p>Select USB devices to print (72 labels per page - 6 ULINE labels x 12 mini-labels each, cut after printing):</p>
|
||||
|
||||
<div v-if="loadingDevices" class="loading-msg">Loading USB devices...</div>
|
||||
<div v-else-if="devices.length === 0" class="loading-msg">No USB devices found</div>
|
||||
<div v-else class="usb-grid">
|
||||
<div
|
||||
v-for="device in devices"
|
||||
:key="device.machineid"
|
||||
class="usb-item"
|
||||
:class="{ selected: isSelected(device) }"
|
||||
@click="toggleDevice(device)"
|
||||
>
|
||||
<input type="checkbox" :checked="isSelected(device)" @click.stop />
|
||||
<label>
|
||||
<strong><code>{{ device.serialnumber || '-' }}</code></strong>
|
||||
<div class="alias">{{ device.alias || device.machinenumber || '' }}</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="selected-count">
|
||||
Selected: <span class="count">{{ selectedDevices.length }}</span> USB devices
|
||||
(<span class="pages">{{ pageCount }}</span> pages)
|
||||
<label style="margin-left: 20px;">Start at cell:
|
||||
<select v-model="startCell" style="padding: 5px; font-size: 14px;">
|
||||
<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>
|
||||
|
||||
<button class="print-btn" :disabled="selectedDevices.length === 0" @click="print">Print Labels</button>
|
||||
<button class="clear-btn" @click="clearSelection">Clear All</button>
|
||||
<button class="select-all-btn" @click="selectAll">Select All</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sheets-container">
|
||||
<div v-for="(page, pageIdx) in sheetPages" :key="pageIdx" class="print-sheet">
|
||||
<div class="sheet-label">Page {{ pageIdx + 1 }} of {{ pageCount }}</div>
|
||||
<div
|
||||
v-for="cellNum in 6"
|
||||
:key="cellNum"
|
||||
class="label-cell"
|
||||
:class="[`cell-${cellNum}`, page[cellNum - 1].hasContent ? 'has-content' : 'empty']"
|
||||
>
|
||||
<div v-if="page[cellNum - 1].hasContent" class="mini-grid">
|
||||
<div
|
||||
v-for="(miniItem, miniIdx) in page[cellNum - 1].items"
|
||||
:key="miniIdx"
|
||||
class="mini-label"
|
||||
:class="miniItem ? 'filled' : 'empty'"
|
||||
>
|
||||
<template v-if="miniItem">
|
||||
<div class="barcode-container">
|
||||
<svg :ref="el => setBarcodeRef(el, pageIdx, cellNum, miniIdx)"></svg>
|
||||
</div>
|
||||
<div class="serial-text">{{ miniItem.serialnumber }}</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="empty-cell-text">Empty</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, watch, nextTick } from 'vue'
|
||||
import { usbApi } from '../../api'
|
||||
import JsBarcode from 'jsbarcode'
|
||||
|
||||
const MINI_LABELS_PER_CELL = 12
|
||||
const CELLS_PER_PAGE = 6
|
||||
|
||||
const devices = ref([])
|
||||
const selectedDevices = ref([])
|
||||
const loadingDevices = ref(true)
|
||||
const startCell = ref('1')
|
||||
const barcodeRefs = ref({})
|
||||
|
||||
const pageCount = computed(() => {
|
||||
if (selectedDevices.value.length === 0) return 0
|
||||
const skippedCells = parseInt(startCell.value) - 1
|
||||
const numCells = Math.ceil(selectedDevices.value.length / MINI_LABELS_PER_CELL)
|
||||
const totalCellsNeeded = numCells + skippedCells
|
||||
return Math.ceil(totalCellsNeeded / CELLS_PER_PAGE)
|
||||
})
|
||||
|
||||
const sheetPages = computed(() => {
|
||||
const result = []
|
||||
if (selectedDevices.value.length === 0) return result
|
||||
|
||||
const startCellNum = parseInt(startCell.value)
|
||||
let usbIdx = 0
|
||||
|
||||
for (let page = 0; page < pageCount.value; page++) {
|
||||
const cells = []
|
||||
for (let cellNum = 1; cellNum <= CELLS_PER_PAGE; cellNum++) {
|
||||
const skipThisCell = page === 0 && cellNum < startCellNum
|
||||
const hasContent = !skipThisCell && usbIdx < selectedDevices.value.length
|
||||
|
||||
const items = []
|
||||
if (hasContent) {
|
||||
for (let mini = 0; mini < MINI_LABELS_PER_CELL; mini++) {
|
||||
if (usbIdx < selectedDevices.value.length) {
|
||||
items.push(selectedDevices.value[usbIdx])
|
||||
usbIdx++
|
||||
} else {
|
||||
items.push(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cells.push({ hasContent, items })
|
||||
}
|
||||
result.push(cells)
|
||||
}
|
||||
return result
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const response = await usbApi.list({ perpage: 500 })
|
||||
devices.value = response.data.data || []
|
||||
} catch (error) {
|
||||
console.error('Error loading USB devices:', error)
|
||||
} finally {
|
||||
loadingDevices.value = false
|
||||
}
|
||||
})
|
||||
|
||||
watch([selectedDevices, startCell], async () => {
|
||||
await nextTick()
|
||||
generateBarcodes()
|
||||
}, { deep: true })
|
||||
|
||||
function setBarcodeRef(el, pageIdx, cellNum, miniIdx) {
|
||||
if (el) {
|
||||
barcodeRefs.value[`${pageIdx}-${cellNum}-${miniIdx}`] = el
|
||||
}
|
||||
}
|
||||
|
||||
function generateBarcodes() {
|
||||
sheetPages.value.forEach((page, pageIdx) => {
|
||||
page.forEach((cell, cellIdx) => {
|
||||
if (!cell.hasContent) return
|
||||
const cellNum = cellIdx + 1
|
||||
cell.items.forEach((item, miniIdx) => {
|
||||
if (!item) return
|
||||
const el = barcodeRefs.value[`${pageIdx}-${cellNum}-${miniIdx}`]
|
||||
if (!el) return
|
||||
try {
|
||||
JsBarcode(el, item.serialnumber, {
|
||||
format: 'CODE128',
|
||||
width: 1,
|
||||
height: 22,
|
||||
displayValue: false,
|
||||
margin: 0,
|
||||
background: 'transparent'
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('Barcode error:', item.serialnumber, e)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function isSelected(device) {
|
||||
return selectedDevices.value.some(d => d.machineid === device.machineid)
|
||||
}
|
||||
|
||||
function toggleDevice(device) {
|
||||
const idx = selectedDevices.value.findIndex(d => d.machineid === device.machineid)
|
||||
if (idx > -1) {
|
||||
selectedDevices.value.splice(idx, 1)
|
||||
} else {
|
||||
selectedDevices.value.push(device)
|
||||
}
|
||||
}
|
||||
|
||||
function clearSelection() {
|
||||
selectedDevices.value = []
|
||||
}
|
||||
|
||||
function selectAll() {
|
||||
selectedDevices.value = [...devices.value]
|
||||
}
|
||||
|
||||
function print() {
|
||||
window.print()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@page { size: letter; margin: 0; }
|
||||
|
||||
.no-print { margin-bottom: 20px; padding: 20px; }
|
||||
.controls { background: white; padding: 20px; border-radius: 8px; margin-bottom: 20px; }
|
||||
.controls h3 { margin-top: 0; }
|
||||
|
||||
.print-btn {
|
||||
padding: 10px 30px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.print-btn:disabled { background: #ccc; cursor: not-allowed; }
|
||||
|
||||
.clear-btn {
|
||||
padding: 10px 20px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.select-all-btn {
|
||||
padding: 10px 20px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
background: #28a745;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.usb-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 10px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #ddd;
|
||||
padding: 10px;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.usb-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.usb-item:hover { background: #f0f0f0; }
|
||||
.usb-item.selected { background: #e7f1ff; border-color: #667eea; }
|
||||
.usb-item input { margin-right: 10px; }
|
||||
.usb-item label { cursor: pointer; flex: 1; }
|
||||
.usb-item .alias { font-size: 11px; color: #666; }
|
||||
|
||||
.selected-count { font-weight: bold; margin: 10px 0; }
|
||||
.selected-count .count { color: #667eea; }
|
||||
.selected-count .pages { color: #28a745; }
|
||||
|
||||
.loading-msg { text-align: center; padding: 2rem; color: #666; }
|
||||
|
||||
.sheets-container { display: flex; flex-direction: column; gap: 20px; }
|
||||
|
||||
.print-sheet {
|
||||
width: 8.5in;
|
||||
height: 11in;
|
||||
background: white;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
border: 1px solid #ccc;
|
||||
page-break-after: always;
|
||||
}
|
||||
.print-sheet:last-child { page-break-after: auto; }
|
||||
|
||||
.sheet-label { position: absolute; top: -25px; left: 0; font-size: 12px; color: #666; }
|
||||
|
||||
.label-cell {
|
||||
width: 3in;
|
||||
height: 3in;
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
border: 1px dashed #ccc;
|
||||
overflow: hidden;
|
||||
}
|
||||
.label-cell.has-content { border: 1px solid #667eea; }
|
||||
.label-cell.empty { background: #fafafa; }
|
||||
|
||||
.cell-1 { top: 0.875in; left: 1.1875in; }
|
||||
.cell-2 { top: 0.875in; left: 4.3125in; }
|
||||
.cell-3 { top: 4in; left: 1.1875in; }
|
||||
.cell-4 { top: 4in; left: 4.3125in; }
|
||||
.cell-5 { top: 7.125in; left: 1.1875in; }
|
||||
.cell-6 { top: 7.125in; left: 4.3125in; }
|
||||
|
||||
.mini-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1in);
|
||||
grid-template-rows: repeat(4, 0.75in);
|
||||
width: 3in;
|
||||
height: 3in;
|
||||
}
|
||||
|
||||
.mini-label {
|
||||
width: 1in;
|
||||
height: 0.75in;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
padding: 0.02in;
|
||||
border: 1px dotted #ddd;
|
||||
overflow: hidden;
|
||||
}
|
||||
.mini-label.filled { border: 1px solid #999; }
|
||||
.mini-label.empty { background: #f8f8f8; border: 1px dotted #eee; }
|
||||
|
||||
.barcode-container { text-align: center; line-height: 0; }
|
||||
.barcode-container svg { max-width: 0.9in; height: 24px; }
|
||||
|
||||
.serial-text {
|
||||
font-size: 6pt;
|
||||
font-weight: bold;
|
||||
font-family: monospace;
|
||||
text-align: center;
|
||||
margin-top: 1px;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.empty-cell-text {
|
||||
color: #ccc;
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@media print {
|
||||
body { padding: 0; margin: 0; background: white; }
|
||||
.no-print { display: none !important; }
|
||||
.sheets-container { gap: 0; }
|
||||
.print-sheet { border: none; margin: 0; width: 8.5in; height: 11in; overflow: hidden; }
|
||||
.sheet-label { display: none; }
|
||||
.label-cell { border: none !important; }
|
||||
.label-cell.empty { visibility: hidden; }
|
||||
.mini-label { border: 1px dotted #ccc !important; }
|
||||
.mini-label.empty { visibility: hidden; }
|
||||
}
|
||||
</style>
|
||||
@@ -3,6 +3,9 @@
|
||||
<div class="page-header">
|
||||
<h2>Printer Details</h2>
|
||||
<div class="header-actions">
|
||||
<router-link :to="`/print/printer-qr/${$route.params.id}`" class="btn btn-secondary" target="_blank">
|
||||
Print QR
|
||||
</router-link>
|
||||
<router-link :to="`/printers/${$route.params.id}/edit`" class="btn btn-primary">Edit</router-link>
|
||||
<router-link to="/printers" class="btn btn-secondary">Back to List</router-link>
|
||||
</div>
|
||||
@@ -24,13 +27,13 @@
|
||||
<span v-if="printer.printer?.isnetwork" class="badge badge-lg badge-secondary">Network</span>
|
||||
</div>
|
||||
<div class="hero-details">
|
||||
<div class="hero-detail" v-if="printer.printer?.vendor_name">
|
||||
<div class="hero-detail" v-if="printer.printer?.vendorname">
|
||||
<span class="hero-detail-label">Vendor</span>
|
||||
<span class="hero-detail-value">{{ printer.printer.vendor_name }}</span>
|
||||
<span class="hero-detail-value">{{ printer.printer.vendorname }}</span>
|
||||
</div>
|
||||
<div class="hero-detail" v-if="printer.printer?.model_name">
|
||||
<div class="hero-detail" v-if="printer.printer?.modelname">
|
||||
<span class="hero-detail-label">Model</span>
|
||||
<span class="hero-detail-value">{{ printer.printer.model_name }}</span>
|
||||
<span class="hero-detail-value">{{ printer.printer.modelname }}</span>
|
||||
</div>
|
||||
<div class="hero-detail" v-if="printer.serialnumber">
|
||||
<span class="hero-detail-label">Serial Number</span>
|
||||
@@ -133,9 +136,9 @@
|
||||
<span v-else>Not mapped</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="info-row" v-if="printer.businessunit_name">
|
||||
<div class="info-row" v-if="printer.businessunitname">
|
||||
<span class="info-label">Business Unit</span>
|
||||
<span class="info-value">{{ printer.businessunit_name }}</span>
|
||||
<span class="info-value">{{ printer.businessunitname }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
<div>
|
||||
<div class="page-header">
|
||||
<h2>Printers</h2>
|
||||
<router-link to="/printers/new" class="btn btn-primary">Add Printer</router-link>
|
||||
<div class="header-actions">
|
||||
<router-link to="/print/printer-qr" class="btn btn-secondary" target="_blank">Batch Print QR</router-link>
|
||||
<router-link to="/printers/new" class="btn btn-primary">Add Printer</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
@@ -36,11 +39,11 @@
|
||||
<tr v-for="printer in printers" :key="printer.printer?.printerid || printer.assetid">
|
||||
<td>{{ printer.assetnumber }}</td>
|
||||
<td>{{ printer.name || '-' }}</td>
|
||||
<td>{{ printer.businessunit_name || '-' }}</td>
|
||||
<td>{{ printer.printer?.model_name || '-' }}</td>
|
||||
<td>{{ printer.businessunitname || '-' }}</td>
|
||||
<td>{{ printer.printer?.modelname || '-' }}</td>
|
||||
<td>
|
||||
<span class="badge" :class="getStatusClass(printer.status_name)">
|
||||
{{ printer.status_name || 'Active' }}
|
||||
<span class="badge" :class="getStatusClass(printer.statusname)">
|
||||
{{ printer.statusname || 'Active' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="actions">
|
||||
@@ -62,16 +65,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>
|
||||
</div>
|
||||
@@ -80,12 +80,14 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { printersApi } from '../../api'
|
||||
import PaginationBar from '../../components/PaginationBar.vue'
|
||||
|
||||
const printers = ref([])
|
||||
const loading = ref(true)
|
||||
const search = ref('')
|
||||
const page = ref(1)
|
||||
const totalPages = ref(1)
|
||||
const perPage = ref(20)
|
||||
|
||||
let searchTimeout = null
|
||||
|
||||
@@ -98,13 +100,13 @@ async function loadPrinters() {
|
||||
try {
|
||||
const params = {
|
||||
page: page.value,
|
||||
perpage: 20
|
||||
perpage: perPage.value
|
||||
}
|
||||
if (search.value) params.search = search.value
|
||||
|
||||
const response = await printersApi.list(params)
|
||||
printers.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 printers:', error)
|
||||
} finally {
|
||||
@@ -125,6 +127,12 @@ function goToPage(p) {
|
||||
loadPrinters()
|
||||
}
|
||||
|
||||
function changePerPage(newPerPage) {
|
||||
perPage.value = newPerPage
|
||||
page.value = 1
|
||||
loadPrinters()
|
||||
}
|
||||
|
||||
function getStatusClass(status) {
|
||||
if (!status) return 'badge-info'
|
||||
const s = status.toLowerCase()
|
||||
|
||||
144
frontend/src/views/reports/PCRelationshipsReport.vue
Normal file
144
frontend/src/views/reports/PCRelationshipsReport.vue
Normal file
@@ -0,0 +1,144 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="page-header">
|
||||
<h2>PC-Machine Relationships</h2>
|
||||
<div class="header-actions">
|
||||
<button class="btn btn-primary" @click="copyTable">Copy Table</button>
|
||||
<button class="btn btn-secondary" @click="copyCSV">Copy CSV</button>
|
||||
<button class="btn btn-secondary" @click="copyJSON">Copy JSON</button>
|
||||
<router-link to="/reports" class="btn btn-secondary">Back to Reports</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-muted">PCs with relationships to shop floor machines</p>
|
||||
|
||||
<div v-if="copied" class="copy-toast">{{ copiedFormat }} copied to clipboard!</div>
|
||||
|
||||
<div class="card">
|
||||
<div v-if="loading" class="loading">Loading...</div>
|
||||
|
||||
<template v-else>
|
||||
<div class="table-container">
|
||||
<table id="relationshipsTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Machine #</th>
|
||||
<th>Vendor</th>
|
||||
<th>Model</th>
|
||||
<th>PC Hostname</th>
|
||||
<th>PC IP</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(row, idx) in data" :key="idx">
|
||||
<td>{{ row.machine_number }}</td>
|
||||
<td>{{ row.vendor }}</td>
|
||||
<td>{{ row.model }}</td>
|
||||
<td class="mono">{{ row.hostname }}</td>
|
||||
<td class="mono">{{ row.ip }}</td>
|
||||
</tr>
|
||||
<tr v-if="data.length === 0">
|
||||
<td colspan="5" style="text-align: center; color: var(--text-light);">
|
||||
No relationships found
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<p class="record-count">{{ data.length }} records found</p>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { reportsApi } from '../../api'
|
||||
|
||||
const loading = ref(true)
|
||||
const data = ref([])
|
||||
const copied = ref(false)
|
||||
const copiedFormat = ref('')
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const response = await reportsApi.pcRelationships()
|
||||
data.value = response.data.data?.data || []
|
||||
} catch (error) {
|
||||
console.error('Error loading report:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
|
||||
function showCopied(format) {
|
||||
copiedFormat.value = format
|
||||
copied.value = true
|
||||
setTimeout(() => { copied.value = false }, 2000)
|
||||
}
|
||||
|
||||
function copyTable() {
|
||||
const headers = ['Machine #', 'Vendor', 'Model', 'PC Hostname', 'PC IP']
|
||||
let text = headers.join('\t') + '\n'
|
||||
for (const row of data.value) {
|
||||
text += [row.machine_number, row.vendor, row.model, row.hostname, row.ip].join('\t') + '\n'
|
||||
}
|
||||
navigator.clipboard.writeText(text).then(() => showCopied('Table'))
|
||||
}
|
||||
|
||||
function copyCSV() {
|
||||
const headers = ['Machine #', 'Vendor', 'Model', 'PC Hostname', 'PC IP']
|
||||
let csv = headers.map(h => `"${h}"`).join(',') + '\n'
|
||||
for (const row of data.value) {
|
||||
csv += [row.machine_number, row.vendor, row.model, row.hostname, row.ip]
|
||||
.map(v => `"${v}"`)
|
||||
.join(',') + '\n'
|
||||
}
|
||||
navigator.clipboard.writeText(csv).then(() => showCopied('CSV'))
|
||||
}
|
||||
|
||||
function copyJSON() {
|
||||
const jsonData = data.value.map(row => ({
|
||||
Name: row.hostname,
|
||||
IpAddress: row.ip,
|
||||
Group: null
|
||||
}))
|
||||
navigator.clipboard.writeText(JSON.stringify(jsonData, null, 2)).then(() => showCopied('JSON'))
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.text-muted {
|
||||
color: var(--text-light);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.mono {
|
||||
font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
|
||||
}
|
||||
|
||||
.record-count {
|
||||
color: var(--text-light);
|
||||
margin-top: 1rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.copy-toast {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: #28a745;
|
||||
color: white;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
z-index: 9999;
|
||||
animation: fadeIn 0.2s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(-10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
</style>
|
||||
@@ -134,8 +134,11 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { reportsApi } from '@/api'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const reports = ref([])
|
||||
const currentReport = ref(null)
|
||||
const reportData = ref(null)
|
||||
@@ -155,6 +158,12 @@ async function loadReports() {
|
||||
}
|
||||
|
||||
async function runReport(report) {
|
||||
// PC Relationships has its own dedicated page
|
||||
if (report.id === 'pc-relationships') {
|
||||
router.push('/reports/pc-relationships')
|
||||
return
|
||||
}
|
||||
|
||||
currentReport.value = report
|
||||
loading.value = true
|
||||
reportData.value = null
|
||||
|
||||
@@ -38,11 +38,14 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<!-- Pagination -->
|
||||
<PaginationBar
|
||||
:page="page"
|
||||
:totalPages="totalPages"
|
||||
:perPage="perPage"
|
||||
@update:page="goToPage"
|
||||
@update:perPage="changePerPage"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@@ -97,11 +100,13 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { businessunitsApi } from '../../api'
|
||||
import PaginationBar from '../../components/PaginationBar.vue'
|
||||
|
||||
const items = ref([])
|
||||
const loading = ref(true)
|
||||
const page = ref(1)
|
||||
const totalPages = ref(1)
|
||||
const perPage = ref(20)
|
||||
|
||||
const showModal = ref(false)
|
||||
const editing = ref(null)
|
||||
@@ -118,9 +123,9 @@ onMounted(() => loadData())
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await businessunitsApi.list({ page: page.value, perpage: 20 })
|
||||
const response = await businessunitsApi.list({ page: page.value, perpage: perPage.value })
|
||||
items.value = response.data.data || []
|
||||
totalPages.value = response.data.meta?.pages || 1
|
||||
totalPages.value = response.data.meta?.pagination?.totalpages || response.data.meta?.pagination?.total_pages || 1
|
||||
} catch (err) {
|
||||
console.error('Error loading business units:', err)
|
||||
} finally {
|
||||
@@ -130,6 +135,12 @@ async function loadData() {
|
||||
|
||||
function goToPage(p) { page.value = p; loadData() }
|
||||
|
||||
function changePerPage(newPerPage) {
|
||||
perPage.value = newPerPage
|
||||
page.value = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
function openModal(item = null) {
|
||||
editing.value = item
|
||||
form.value = item ? {
|
||||
|
||||
@@ -64,16 +64,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>
|
||||
|
||||
@@ -185,12 +182,14 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { locationsApi } from '../../api'
|
||||
import PaginationBar from '../../components/PaginationBar.vue'
|
||||
|
||||
const locations = ref([])
|
||||
const loading = ref(true)
|
||||
const search = ref('')
|
||||
const page = ref(1)
|
||||
const totalPages = ref(1)
|
||||
const perPage = ref(20)
|
||||
|
||||
const showModal = ref(false)
|
||||
const editingLocation = ref(null)
|
||||
@@ -220,13 +219,13 @@ async function loadLocations() {
|
||||
try {
|
||||
const params = {
|
||||
page: page.value,
|
||||
perpage: 20
|
||||
perpage: perPage.value
|
||||
}
|
||||
if (search.value) params.search = search.value
|
||||
|
||||
const response = await locationsApi.list(params)
|
||||
locations.value = response.data.data || []
|
||||
totalPages.value = response.data.meta?.pages || 1
|
||||
totalPages.value = response.data.meta?.pagination?.totalpages || response.data.meta?.pagination?.total_pages || 1
|
||||
} catch (err) {
|
||||
console.error('Error loading locations:', err)
|
||||
} finally {
|
||||
@@ -247,6 +246,12 @@ function goToPage(p) {
|
||||
loadLocations()
|
||||
}
|
||||
|
||||
function changePerPage(newPerPage) {
|
||||
perPage.value = newPerPage
|
||||
page.value = 1
|
||||
loadLocations()
|
||||
}
|
||||
|
||||
function openModal(loc = null) {
|
||||
editingLocation.value = loc
|
||||
if (loc) {
|
||||
|
||||
@@ -53,16 +53,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>
|
||||
|
||||
@@ -147,11 +144,13 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { machinetypesApi } from '../../api'
|
||||
import PaginationBar from '../../components/PaginationBar.vue'
|
||||
|
||||
const machineTypes = ref([])
|
||||
const loading = ref(true)
|
||||
const page = ref(1)
|
||||
const totalPages = ref(1)
|
||||
const perPage = ref(20)
|
||||
|
||||
const showModal = ref(false)
|
||||
const editingType = ref(null)
|
||||
@@ -176,12 +175,12 @@ async function loadTypes() {
|
||||
try {
|
||||
const params = {
|
||||
page: page.value,
|
||||
perpage: 20
|
||||
perpage: perPage.value
|
||||
}
|
||||
|
||||
const response = await machinetypesApi.list(params)
|
||||
machineTypes.value = response.data.data || []
|
||||
totalPages.value = response.data.meta?.pages || 1
|
||||
totalPages.value = response.data.meta?.pagination?.totalpages || response.data.meta?.pagination?.total_pages || 1
|
||||
} catch (err) {
|
||||
console.error('Error loading machine types:', err)
|
||||
} finally {
|
||||
@@ -194,6 +193,12 @@ function goToPage(p) {
|
||||
loadTypes()
|
||||
}
|
||||
|
||||
function changePerPage(newPerPage) {
|
||||
perPage.value = newPerPage
|
||||
page.value = 1
|
||||
loadTypes()
|
||||
}
|
||||
|
||||
function openModal(mt = null) {
|
||||
editingType.value = mt
|
||||
if (mt) {
|
||||
|
||||
@@ -65,16 +65,14 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<!-- Pagination -->
|
||||
<PaginationBar
|
||||
:page="page"
|
||||
:totalPages="totalPages"
|
||||
:perPage="perPage"
|
||||
@update:page="goToPage"
|
||||
@update:perPage="changePerPage"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@@ -184,6 +182,7 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { modelsApi, vendorsApi, machinetypesApi } from '../../api'
|
||||
import PaginationBar from '../../components/PaginationBar.vue'
|
||||
|
||||
const models = ref([])
|
||||
const vendors = ref([])
|
||||
@@ -193,6 +192,7 @@ const search = ref('')
|
||||
const vendorFilter = ref('')
|
||||
const page = ref(1)
|
||||
const totalPages = ref(1)
|
||||
const perPage = ref(20)
|
||||
|
||||
const showModal = ref(false)
|
||||
const editingModel = ref(null)
|
||||
@@ -225,13 +225,13 @@ onMounted(async () => {
|
||||
async function loadModels() {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = { page: page.value, perpage: 20 }
|
||||
const params = { page: page.value, perpage: perPage.value }
|
||||
if (search.value) params.search = search.value
|
||||
if (vendorFilter.value) params.vendor = vendorFilter.value
|
||||
|
||||
const response = await modelsApi.list(params)
|
||||
models.value = response.data.data || []
|
||||
totalPages.value = response.data.meta?.pages || 1
|
||||
totalPages.value = response.data.meta?.pagination?.totalpages || response.data.meta?.pagination?.total_pages || 1
|
||||
} catch (err) {
|
||||
console.error('Error loading models:', err)
|
||||
} finally {
|
||||
@@ -270,6 +270,12 @@ function goToPage(p) {
|
||||
loadModels()
|
||||
}
|
||||
|
||||
function changePerPage(newPerPage) {
|
||||
perPage.value = newPerPage
|
||||
page.value = 1
|
||||
loadModels()
|
||||
}
|
||||
|
||||
function openModal(m = null) {
|
||||
editingModel.value = m
|
||||
if (m) {
|
||||
|
||||
@@ -45,11 +45,14 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<!-- Pagination -->
|
||||
<PaginationBar
|
||||
:page="page"
|
||||
:totalPages="totalPages"
|
||||
:perPage="perPage"
|
||||
@update:page="goToPage"
|
||||
@update:perPage="changePerPage"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@@ -115,11 +118,13 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { operatingsystemsApi } from '../../api'
|
||||
import PaginationBar from '../../components/PaginationBar.vue'
|
||||
|
||||
const items = ref([])
|
||||
const loading = ref(true)
|
||||
const page = ref(1)
|
||||
const totalPages = ref(1)
|
||||
const perPage = ref(20)
|
||||
|
||||
const showModal = ref(false)
|
||||
const editing = ref(null)
|
||||
@@ -136,9 +141,9 @@ onMounted(() => loadData())
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await operatingsystemsApi.list({ page: page.value, perpage: 20 })
|
||||
const response = await operatingsystemsApi.list({ page: page.value, perpage: perPage.value })
|
||||
items.value = response.data.data || []
|
||||
totalPages.value = response.data.meta?.pages || 1
|
||||
totalPages.value = response.data.meta?.pagination?.totalpages || response.data.meta?.pagination?.total_pages || 1
|
||||
} catch (err) {
|
||||
console.error('Error loading operating systems:', err)
|
||||
} finally {
|
||||
@@ -148,6 +153,12 @@ async function loadData() {
|
||||
|
||||
function goToPage(p) { page.value = p; loadData() }
|
||||
|
||||
function changePerPage(newPerPage) {
|
||||
perPage.value = newPerPage
|
||||
page.value = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
function isPastEol(date) {
|
||||
return new Date(date) < new Date()
|
||||
}
|
||||
|
||||
@@ -36,11 +36,14 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<!-- Pagination -->
|
||||
<PaginationBar
|
||||
:page="page"
|
||||
:totalPages="totalPages"
|
||||
:perPage="perPage"
|
||||
@update:page="goToPage"
|
||||
@update:perPage="changePerPage"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@@ -91,11 +94,13 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { pctypesApi } from '../../api'
|
||||
import PaginationBar from '../../components/PaginationBar.vue'
|
||||
|
||||
const pcTypes = ref([])
|
||||
const loading = ref(true)
|
||||
const page = ref(1)
|
||||
const totalPages = ref(1)
|
||||
const perPage = ref(20)
|
||||
|
||||
const showModal = ref(false)
|
||||
const editing = ref(null)
|
||||
@@ -112,9 +117,9 @@ onMounted(() => loadData())
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await pctypesApi.list({ page: page.value, perpage: 20 })
|
||||
const response = await pctypesApi.list({ page: page.value, perpage: perPage.value })
|
||||
pcTypes.value = response.data.data || []
|
||||
totalPages.value = response.data.meta?.pages || 1
|
||||
totalPages.value = response.data.meta?.pagination?.totalpages || response.data.meta?.pagination?.total_pages || 1
|
||||
} catch (err) {
|
||||
console.error('Error loading PC types:', err)
|
||||
} finally {
|
||||
@@ -124,6 +129,12 @@ async function loadData() {
|
||||
|
||||
function goToPage(p) { page.value = p; loadData() }
|
||||
|
||||
function changePerPage(newPerPage) {
|
||||
perPage.value = newPerPage
|
||||
page.value = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
function openModal(item = null) {
|
||||
editing.value = item
|
||||
form.value = item ? { pctype: item.pctype || '', description: item.description || '' } : { pctype: '', description: '' }
|
||||
|
||||
@@ -4,61 +4,61 @@
|
||||
|
||||
<div class="settings-grid">
|
||||
<router-link to="/settings/vendors" class="settings-card">
|
||||
<div class="card-icon">🏭</div>
|
||||
<div class="card-icon"><Factory :size="28" /></div>
|
||||
<h3>Vendors</h3>
|
||||
<p>Manage equipment vendors and manufacturers</p>
|
||||
</router-link>
|
||||
|
||||
<router-link to="/settings/locations" class="settings-card">
|
||||
<div class="card-icon">📍</div>
|
||||
<div class="card-icon"><MapPin :size="28" /></div>
|
||||
<h3>Locations</h3>
|
||||
<p>Manage physical locations and sites</p>
|
||||
</router-link>
|
||||
|
||||
<router-link to="/settings/statuses" class="settings-card">
|
||||
<div class="card-icon">🏷️</div>
|
||||
<div class="card-icon"><Tag :size="28" /></div>
|
||||
<h3>Statuses</h3>
|
||||
<p>Manage equipment status types</p>
|
||||
</router-link>
|
||||
|
||||
<router-link to="/settings/models" class="settings-card">
|
||||
<div class="card-icon">📦</div>
|
||||
<div class="card-icon"><Package :size="28" /></div>
|
||||
<h3>Models</h3>
|
||||
<p>Manage equipment models by vendor</p>
|
||||
</router-link>
|
||||
|
||||
<router-link to="/settings/machinetypes" class="settings-card">
|
||||
<div class="card-icon">🖥️</div>
|
||||
<div class="card-icon"><Monitor :size="28" /></div>
|
||||
<h3>Machine Types</h3>
|
||||
<p>Manage machine type categories</p>
|
||||
</router-link>
|
||||
|
||||
<router-link to="/settings/pctypes" class="settings-card">
|
||||
<div class="card-icon">💻</div>
|
||||
<div class="card-icon"><Laptop :size="28" /></div>
|
||||
<h3>PC Types</h3>
|
||||
<p>Manage PC form factors</p>
|
||||
</router-link>
|
||||
|
||||
<router-link to="/settings/operatingsystems" class="settings-card">
|
||||
<div class="card-icon">⚙️</div>
|
||||
<div class="card-icon"><Cog :size="28" /></div>
|
||||
<h3>Operating Systems</h3>
|
||||
<p>Manage OS versions and EOL dates</p>
|
||||
</router-link>
|
||||
|
||||
<router-link to="/settings/businessunits" class="settings-card">
|
||||
<div class="card-icon">🏢</div>
|
||||
<div class="card-icon"><Building :size="28" /></div>
|
||||
<h3>Business Units</h3>
|
||||
<p>Manage organizational units</p>
|
||||
</router-link>
|
||||
|
||||
<router-link to="/settings/vlans" class="settings-card">
|
||||
<div class="card-icon">🌐</div>
|
||||
<div class="card-icon"><Globe :size="28" /></div>
|
||||
<h3>VLANs</h3>
|
||||
<p>Manage virtual LANs</p>
|
||||
</router-link>
|
||||
|
||||
<router-link to="/settings/subnets" class="settings-card">
|
||||
<div class="card-icon">🔗</div>
|
||||
<div class="card-icon"><Link :size="28" /></div>
|
||||
<h3>Subnets</h3>
|
||||
<p>Manage IP subnets and DHCP</p>
|
||||
</router-link>
|
||||
@@ -67,6 +67,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Factory, MapPin, Tag, Package, Monitor, Laptop, Cog, Building, Globe, Link } from 'lucide-vue-next'
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -56,16 +56,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>
|
||||
|
||||
@@ -153,11 +150,13 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { statusesApi } from '../../api'
|
||||
import PaginationBar from '../../components/PaginationBar.vue'
|
||||
|
||||
const statuses = ref([])
|
||||
const loading = ref(true)
|
||||
const page = ref(1)
|
||||
const totalPages = ref(1)
|
||||
const perPage = ref(20)
|
||||
|
||||
const showModal = ref(false)
|
||||
const editingStatus = ref(null)
|
||||
@@ -182,12 +181,12 @@ async function loadStatuses() {
|
||||
try {
|
||||
const params = {
|
||||
page: page.value,
|
||||
perpage: 20
|
||||
perpage: perPage.value
|
||||
}
|
||||
|
||||
const response = await statusesApi.list(params)
|
||||
statuses.value = response.data.data || []
|
||||
totalPages.value = response.data.meta?.pages || 1
|
||||
totalPages.value = response.data.meta?.pagination?.totalpages || response.data.meta?.pagination?.total_pages || 1
|
||||
} catch (err) {
|
||||
console.error('Error loading statuses:', err)
|
||||
} finally {
|
||||
@@ -200,6 +199,12 @@ function goToPage(p) {
|
||||
loadStatuses()
|
||||
}
|
||||
|
||||
function changePerPage(newPerPage) {
|
||||
perPage.value = newPerPage
|
||||
page.value = 1
|
||||
loadStatuses()
|
||||
}
|
||||
|
||||
function openModal(s = null) {
|
||||
editingStatus.value = s
|
||||
if (s) {
|
||||
|
||||
@@ -49,8 +49,8 @@
|
||||
<td class="mono">{{ subnet.cidr }}</td>
|
||||
<td>{{ subnet.name }}</td>
|
||||
<td>
|
||||
<span v-if="subnet.vlan_name">
|
||||
VLAN {{ subnet.vlan_number }} - {{ subnet.vlan_name }}
|
||||
<span v-if="subnet.vlanname">
|
||||
VLAN {{ subnet.vlannumber }} - {{ subnet.vlanname }}
|
||||
</span>
|
||||
<span v-else>-</span>
|
||||
</td>
|
||||
@@ -60,7 +60,7 @@
|
||||
{{ subnet.dhcpenabled ? 'Enabled' : 'Disabled' }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ subnet.location_name || '-' }}</td>
|
||||
<td>{{ subnet.locationname || '-' }}</td>
|
||||
<td class="actions">
|
||||
<button
|
||||
class="btn btn-secondary btn-sm"
|
||||
@@ -86,16 +86,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>
|
||||
|
||||
@@ -287,6 +284,7 @@
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { networkApi, locationsApi } from '../../api'
|
||||
import PaginationBar from '../../components/PaginationBar.vue'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
@@ -299,6 +297,7 @@ const vlanFilter = ref('')
|
||||
const typeFilter = ref('')
|
||||
const page = ref(1)
|
||||
const totalPages = ref(1)
|
||||
const perPage = ref(20)
|
||||
|
||||
const showModal = ref(false)
|
||||
const editingSubnet = ref(null)
|
||||
@@ -341,7 +340,7 @@ onMounted(async () => {
|
||||
|
||||
async function loadVLANs() {
|
||||
try {
|
||||
const response = await networkApi.vlans.list({ per_page: 100 })
|
||||
const response = await networkApi.vlans.list({ perpage: 100 })
|
||||
vlans.value = response.data.data || []
|
||||
} catch (err) {
|
||||
console.error('Error loading VLANs:', err)
|
||||
@@ -350,7 +349,7 @@ async function loadVLANs() {
|
||||
|
||||
async function loadLocations() {
|
||||
try {
|
||||
const response = await locationsApi.list({ per_page: 100 })
|
||||
const response = await locationsApi.list({ perpage: 100 })
|
||||
locations.value = response.data.data || []
|
||||
} catch (err) {
|
||||
console.error('Error loading locations:', err)
|
||||
@@ -362,7 +361,7 @@ async function loadSubnets() {
|
||||
try {
|
||||
const params = {
|
||||
page: page.value,
|
||||
per_page: 20
|
||||
perpage: perPage.value
|
||||
}
|
||||
if (search.value) params.search = search.value
|
||||
if (vlanFilter.value) params.vlanid = vlanFilter.value
|
||||
@@ -370,7 +369,7 @@ async function loadSubnets() {
|
||||
|
||||
const response = await networkApi.subnets.list(params)
|
||||
subnets.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 (err) {
|
||||
console.error('Error loading subnets:', err)
|
||||
} finally {
|
||||
@@ -391,6 +390,12 @@ function goToPage(p) {
|
||||
loadSubnets()
|
||||
}
|
||||
|
||||
function changePerPage(newPerPage) {
|
||||
perPage.value = newPerPage
|
||||
page.value = 1
|
||||
loadSubnets()
|
||||
}
|
||||
|
||||
function openModal(subnet = null) {
|
||||
editingSubnet.value = subnet
|
||||
if (subnet) {
|
||||
|
||||
@@ -84,16 +84,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>
|
||||
|
||||
@@ -189,6 +186,7 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { networkApi } from '../../api'
|
||||
import PaginationBar from '../../components/PaginationBar.vue'
|
||||
|
||||
const vlans = ref([])
|
||||
const loading = ref(true)
|
||||
@@ -196,6 +194,7 @@ const search = ref('')
|
||||
const typeFilter = ref('')
|
||||
const page = ref(1)
|
||||
const totalPages = ref(1)
|
||||
const perPage = ref(20)
|
||||
|
||||
const showModal = ref(false)
|
||||
const editingVLAN = ref(null)
|
||||
@@ -223,14 +222,14 @@ async function loadVLANs() {
|
||||
try {
|
||||
const params = {
|
||||
page: page.value,
|
||||
per_page: 20
|
||||
perpage: perPage.value
|
||||
}
|
||||
if (search.value) params.search = search.value
|
||||
if (typeFilter.value) params.type = typeFilter.value
|
||||
|
||||
const response = await networkApi.vlans.list(params)
|
||||
vlans.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 (err) {
|
||||
console.error('Error loading VLANs:', err)
|
||||
} finally {
|
||||
@@ -251,6 +250,12 @@ function goToPage(p) {
|
||||
loadVLANs()
|
||||
}
|
||||
|
||||
function changePerPage(newPerPage) {
|
||||
perPage.value = newPerPage
|
||||
page.value = 1
|
||||
loadVLANs()
|
||||
}
|
||||
|
||||
function getTypeClass(type) {
|
||||
switch (type) {
|
||||
case 'data': return 'badge-info'
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
<h1>{{ device.alias || device.machinenumber }}</h1>
|
||||
</div>
|
||||
<div class="hero-meta">
|
||||
<span class="badge badge-lg" :class="device.is_checked_out ? 'badge-warning' : 'badge-success'">
|
||||
{{ device.is_checked_out ? 'Checked Out' : 'Available' }}
|
||||
<span class="badge badge-lg" :class="device.ischeckedout ? 'badge-warning' : 'badge-success'">
|
||||
{{ device.ischeckedout ? 'Checked Out' : 'Available' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="hero-details">
|
||||
@@ -26,13 +26,13 @@
|
||||
<span class="hero-detail-label">Serial Number</span>
|
||||
<span class="hero-detail-value mono">{{ device.serialnumber }}</span>
|
||||
</div>
|
||||
<div class="hero-detail" v-if="device.vendor_name">
|
||||
<div class="hero-detail" v-if="device.vendorname">
|
||||
<span class="hero-detail-label">Vendor</span>
|
||||
<span class="hero-detail-value">{{ device.vendor_name }}</span>
|
||||
<span class="hero-detail-value">{{ device.vendorname }}</span>
|
||||
</div>
|
||||
<div class="hero-detail" v-if="device.model_name">
|
||||
<div class="hero-detail" v-if="device.modelname">
|
||||
<span class="hero-detail-label">Model</span>
|
||||
<span class="hero-detail-value">{{ device.model_name }}</span>
|
||||
<span class="hero-detail-value">{{ device.modelname }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -41,7 +41,7 @@
|
||||
<!-- Action Buttons -->
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
v-if="!device.is_checked_out"
|
||||
v-if="!device.ischeckedout"
|
||||
class="btn btn-primary btn-lg"
|
||||
@click="openCheckoutModal"
|
||||
>
|
||||
@@ -57,20 +57,20 @@
|
||||
</div>
|
||||
|
||||
<!-- Current Checkout Info -->
|
||||
<div class="section-card" v-if="device.current_checkout">
|
||||
<div class="section-card" v-if="device.currentcheckout">
|
||||
<h3 class="section-title">Current Checkout</h3>
|
||||
<div class="info-list">
|
||||
<div class="info-row">
|
||||
<span class="info-label">Checked Out By</span>
|
||||
<span class="info-value">{{ device.current_checkout.sso }}</span>
|
||||
<span class="info-value">{{ device.currentcheckout.sso }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">Checkout Time</span>
|
||||
<span class="info-value">{{ formatDate(device.current_checkout.checkout_time) }}</span>
|
||||
<span class="info-value">{{ formatDate(device.currentcheckout.checkouttime) }}</span>
|
||||
</div>
|
||||
<div class="info-row" v-if="device.current_checkout.checkout_reason">
|
||||
<div class="info-row" v-if="device.currentcheckout.checkoutreason">
|
||||
<span class="info-label">Reason</span>
|
||||
<span class="info-value">{{ device.current_checkout.checkout_reason }}</span>
|
||||
<span class="info-value">{{ device.currentcheckout.checkoutreason }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -81,7 +81,7 @@
|
||||
<h3>Checkout History</h3>
|
||||
</div>
|
||||
|
||||
<div v-if="!device.checkout_history?.length" class="empty-state">
|
||||
<div v-if="!device.checkouthistory?.length" class="empty-state">
|
||||
No checkout history
|
||||
</div>
|
||||
|
||||
@@ -97,15 +97,15 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="checkout in device.checkout_history" :key="checkout.checkoutid">
|
||||
<tr v-for="checkout in device.checkouthistory" :key="checkout.checkoutid">
|
||||
<td>{{ checkout.sso }}</td>
|
||||
<td>{{ formatDate(checkout.checkout_time) }}</td>
|
||||
<td>{{ checkout.checkin_time ? formatDate(checkout.checkin_time) : 'Still out' }}</td>
|
||||
<td>{{ formatDate(checkout.checkouttime) }}</td>
|
||||
<td>{{ checkout.checkintime ? formatDate(checkout.checkintime) : 'Still out' }}</td>
|
||||
<td>
|
||||
<span v-if="checkout.checkin_time">{{ checkout.was_wiped ? 'Yes' : 'No' }}</span>
|
||||
<span v-if="checkout.checkintime">{{ checkout.waswiped ? 'Yes' : 'No' }}</span>
|
||||
<span v-else>-</span>
|
||||
</td>
|
||||
<td>{{ checkout.checkout_reason || '-' }}</td>
|
||||
<td>{{ checkout.checkoutreason || '-' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -146,7 +146,7 @@
|
||||
<template #body>
|
||||
<div class="form-group">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" v-model="checkinForm.was_wiped" />
|
||||
<input type="checkbox" v-model="checkinForm.waswiped" />
|
||||
Device was wiped
|
||||
</label>
|
||||
</div>
|
||||
@@ -177,7 +177,7 @@ const device = ref(null)
|
||||
const showCheckoutModal = ref(false)
|
||||
const showCheckinModal = ref(false)
|
||||
const checkoutForm = ref({ sso: '', reason: '' })
|
||||
const checkinForm = ref({ was_wiped: false, notes: '' })
|
||||
const checkinForm = ref({ waswiped: false, notes: '' })
|
||||
|
||||
onMounted(async () => {
|
||||
await loadDevice()
|
||||
@@ -201,7 +201,7 @@ function openCheckoutModal() {
|
||||
}
|
||||
|
||||
function openCheckinModal() {
|
||||
checkinForm.value = { was_wiped: false, notes: '' }
|
||||
checkinForm.value = { waswiped: false, notes: '' }
|
||||
showCheckinModal.value = true
|
||||
}
|
||||
|
||||
|
||||
@@ -150,7 +150,7 @@ onMounted(async () => {
|
||||
// Load types and vendors
|
||||
const [typesRes, vendorsRes] = await Promise.all([
|
||||
usbApi.types.list(),
|
||||
vendorsApi.list({ per_page: 1000 })
|
||||
vendorsApi.list({ perpage: 1000 })
|
||||
])
|
||||
types.value = typesRes.data.data || []
|
||||
vendors.value = vendorsRes.data.data || []
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<div>
|
||||
<div class="page-header">
|
||||
<h2>USB Devices</h2>
|
||||
<router-link to="/print/usb-labels" class="btn btn-secondary" target="_blank">Print Labels</router-link>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
@@ -38,24 +39,24 @@
|
||||
<tr v-for="device in devices" :key="device.machineid">
|
||||
<td>
|
||||
<strong>{{ device.alias || device.machinenumber }}</strong>
|
||||
<div v-if="device.model_name" class="text-muted">{{ device.model_name }}</div>
|
||||
<div v-if="device.modelname" class="text-muted">{{ device.modelname }}</div>
|
||||
</td>
|
||||
<td class="mono">{{ device.serialnumber || '-' }}</td>
|
||||
<td>
|
||||
<span class="badge" :class="device.is_checked_out ? 'badge-warning' : 'badge-success'">
|
||||
{{ device.is_checked_out ? 'Checked Out' : 'Available' }}
|
||||
<span class="badge" :class="device.ischeckedout ? 'badge-warning' : 'badge-success'">
|
||||
{{ device.ischeckedout ? 'Checked Out' : 'Available' }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<template v-if="device.current_checkout">
|
||||
{{ device.current_checkout.checkout_name || device.current_checkout.sso }}
|
||||
<div class="text-muted">{{ formatDate(device.current_checkout.checkout_time) }}</div>
|
||||
<template v-if="device.currentcheckout">
|
||||
{{ device.currentcheckout.checkoutname || device.currentcheckout.sso }}
|
||||
<div class="text-muted">{{ formatDate(device.currentcheckout.checkouttime) }}</div>
|
||||
</template>
|
||||
<span v-else>-</span>
|
||||
</td>
|
||||
<td class="actions">
|
||||
<button
|
||||
v-if="!device.is_checked_out"
|
||||
v-if="!device.ischeckedout"
|
||||
class="btn btn-primary btn-sm"
|
||||
@click="openCheckoutModal(device)"
|
||||
>
|
||||
@@ -86,16 +87,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>
|
||||
|
||||
@@ -127,7 +125,7 @@
|
||||
<p>Checking in: <strong>{{ selectedDevice?.alias || selectedDevice?.machinenumber }}</strong></p>
|
||||
<div class="form-group">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" v-model="checkinForm.was_wiped" />
|
||||
<input type="checkbox" v-model="checkinForm.waswiped" />
|
||||
Device was wiped
|
||||
</label>
|
||||
</div>
|
||||
@@ -148,19 +146,21 @@ import { ref, onMounted } from 'vue'
|
||||
import { usbApi } from '../../api'
|
||||
import Modal from '../../components/Modal.vue'
|
||||
import EmployeeSearch from '../../components/EmployeeSearch.vue'
|
||||
import PaginationBar from '../../components/PaginationBar.vue'
|
||||
|
||||
const devices = ref([])
|
||||
const loading = ref(true)
|
||||
const search = ref('')
|
||||
const page = ref(1)
|
||||
const totalPages = ref(1)
|
||||
const perPage = ref(20)
|
||||
const showAvailableOnly = ref(false)
|
||||
|
||||
const showCheckoutModal = ref(false)
|
||||
const showCheckinModal = ref(false)
|
||||
const selectedDevice = ref(null)
|
||||
const checkoutForm = ref({ reason: '' })
|
||||
const checkinForm = ref({ was_wiped: false, notes: '' })
|
||||
const checkinForm = ref({ waswiped: false, notes: '' })
|
||||
const selectedEmployee = ref(null)
|
||||
|
||||
let searchTimeout = null
|
||||
@@ -174,14 +174,14 @@ async function loadDevices() {
|
||||
try {
|
||||
const params = {
|
||||
page: page.value,
|
||||
perpage: 20
|
||||
perpage: perPage.value
|
||||
}
|
||||
if (search.value) params.search = search.value
|
||||
if (showAvailableOnly.value) params.available = 'true'
|
||||
|
||||
const response = await usbApi.list(params)
|
||||
devices.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 USB devices:', error)
|
||||
} finally {
|
||||
@@ -202,6 +202,12 @@ function goToPage(p) {
|
||||
loadDevices()
|
||||
}
|
||||
|
||||
function changePerPage(newPerPage) {
|
||||
perPage.value = newPerPage
|
||||
page.value = 1
|
||||
loadDevices()
|
||||
}
|
||||
|
||||
function openCheckoutModal(device) {
|
||||
selectedDevice.value = device
|
||||
checkoutForm.value = { reason: '' }
|
||||
@@ -211,7 +217,7 @@ function openCheckoutModal(device) {
|
||||
|
||||
function openCheckinModal(device) {
|
||||
selectedDevice.value = device
|
||||
checkinForm.value = { was_wiped: false, notes: '' }
|
||||
checkinForm.value = { waswiped: false, notes: '' }
|
||||
showCheckinModal.value = true
|
||||
}
|
||||
|
||||
|
||||
29
frontend/src/views/vendors/VendorsList.vue
vendored
29
frontend/src/views/vendors/VendorsList.vue
vendored
@@ -62,16 +62,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>
|
||||
|
||||
@@ -188,12 +185,14 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { vendorsApi } from '../../api'
|
||||
import PaginationBar from '../../components/PaginationBar.vue'
|
||||
|
||||
const vendors = ref([])
|
||||
const loading = ref(true)
|
||||
const search = ref('')
|
||||
const page = ref(1)
|
||||
const totalPages = ref(1)
|
||||
const perPage = ref(20)
|
||||
|
||||
const showModal = ref(false)
|
||||
const editingVendor = ref(null)
|
||||
@@ -224,13 +223,13 @@ async function loadVendors() {
|
||||
try {
|
||||
const params = {
|
||||
page: page.value,
|
||||
perpage: 20
|
||||
perpage: perPage.value
|
||||
}
|
||||
if (search.value) params.search = search.value
|
||||
|
||||
const response = await vendorsApi.list(params)
|
||||
vendors.value = response.data.data || []
|
||||
totalPages.value = response.data.meta?.pages || 1
|
||||
totalPages.value = response.data.meta?.pagination?.totalpages || response.data.meta?.pagination?.total_pages || 1
|
||||
} catch (err) {
|
||||
console.error('Error loading vendors:', err)
|
||||
} finally {
|
||||
@@ -251,6 +250,12 @@ function goToPage(p) {
|
||||
loadVendors()
|
||||
}
|
||||
|
||||
function changePerPage(newPerPage) {
|
||||
perPage.value = newPerPage
|
||||
page.value = 1
|
||||
loadVendors()
|
||||
}
|
||||
|
||||
function openModal(vendor = null) {
|
||||
editingVendor.value = vendor
|
||||
if (vendor) {
|
||||
|
||||
Reference in New Issue
Block a user