Add USB, Notifications, Network plugins and reusable EmployeeSearch component
New Plugins: - USB plugin: Device checkout/checkin with employee lookup, checkout history - Notifications plugin: Announcements with types, scheduling, shopfloor display - Network plugin: Network device management with subnets and VLANs - Equipment and Computers plugins: Asset type separation Frontend: - EmployeeSearch component: Reusable employee lookup with autocomplete - USB views: List, detail, checkout/checkin modals - Notifications views: List, form with recognition mode - Network views: Device list, detail, form - Calendar view with FullCalendar integration - Shopfloor and TV dashboard views - Reports index page - Map editor for asset positioning - Light/dark mode fixes for map tooltips Backend: - Employee search API with external lookup service - Collector API for PowerShell data collection - Reports API endpoints - Slides API for TV dashboard - Fixed AppVersion model (removed BaseModel inheritance) - Added checkout_name column to usbcheckouts table Styling: - Unified detail page styles - Improved pagination (page numbers instead of prev/next) - Dark/light mode theme improvements Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
269
frontend/src/views/reports/ReportsIndex.vue
Normal file
269
frontend/src/views/reports/ReportsIndex.vue
Normal file
@@ -0,0 +1,269 @@
|
||||
<template>
|
||||
<div class="page-header">
|
||||
<h1>Reports</h1>
|
||||
</div>
|
||||
|
||||
<div class="reports-grid">
|
||||
<div v-for="report in reports" :key="report.id" class="report-card card" @click="runReport(report)">
|
||||
<h3>{{ report.name }}</h3>
|
||||
<p>{{ report.description }}</p>
|
||||
<span class="badge">{{ report.category }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Report Results -->
|
||||
<div v-if="currentReport" class="report-results card">
|
||||
<div class="report-header">
|
||||
<h2>{{ currentReport.name }}</h2>
|
||||
<div class="report-actions">
|
||||
<button class="btn btn-secondary" @click="exportCSV">Export CSV</button>
|
||||
<button class="btn btn-secondary" @click="clearReport">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="loading">Loading report...</div>
|
||||
<div v-else-if="reportData">
|
||||
<!-- Equipment by Type -->
|
||||
<div v-if="currentReport.id === 'equipment-by-type'" class="report-content">
|
||||
<p class="report-summary">Total: {{ reportData.total }}</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Equipment Type</th>
|
||||
<th>Description</th>
|
||||
<th>Count</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="item in reportData.data" :key="item.equipmenttype">
|
||||
<td>{{ item.equipmenttype }}</td>
|
||||
<td>{{ item.description }}</td>
|
||||
<td>{{ item.count }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Assets by Status -->
|
||||
<div v-else-if="currentReport.id === 'assets-by-status'" class="report-content">
|
||||
<p class="report-summary">Total: {{ reportData.total }}</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Status</th>
|
||||
<th>Count</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="item in reportData.data" :key="item.status">
|
||||
<td>
|
||||
<span class="badge" :style="{ backgroundColor: item.color }">{{ item.status }}</span>
|
||||
</td>
|
||||
<td>{{ item.count }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- KB Popularity -->
|
||||
<div v-else-if="currentReport.id === 'kb-popularity'" class="report-content">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Article</th>
|
||||
<th>Application</th>
|
||||
<th>Clicks</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="item in reportData.data" :key="item.linkid">
|
||||
<td>
|
||||
<a v-if="item.linkurl" :href="item.linkurl" target="_blank">{{ item.shortdescription }}</a>
|
||||
<span v-else>{{ item.shortdescription }}</span>
|
||||
</td>
|
||||
<td>{{ item.application || '-' }}</td>
|
||||
<td>{{ item.clicks }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Asset Inventory -->
|
||||
<div v-else-if="currentReport.id === 'asset-inventory'" class="report-content">
|
||||
<p class="report-summary">Total Assets: {{ reportData.total }}</p>
|
||||
|
||||
<h3>By Type</h3>
|
||||
<table>
|
||||
<thead><tr><th>Type</th><th>Count</th></tr></thead>
|
||||
<tbody>
|
||||
<tr v-for="item in reportData.data.bytype" :key="item.type">
|
||||
<td>{{ item.type }}</td><td>{{ item.count }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>By Status</h3>
|
||||
<table>
|
||||
<thead><tr><th>Status</th><th>Count</th></tr></thead>
|
||||
<tbody>
|
||||
<tr v-for="item in reportData.data.bystatus" :key="item.status">
|
||||
<td><span class="badge" :style="{ backgroundColor: item.color }">{{ item.status }}</span></td>
|
||||
<td>{{ item.count }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>By Location</h3>
|
||||
<table>
|
||||
<thead><tr><th>Location</th><th>Count</th></tr></thead>
|
||||
<tbody>
|
||||
<tr v-for="item in reportData.data.bylocation" :key="item.location">
|
||||
<td>{{ item.location }}</td><td>{{ item.count }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Generic fallback -->
|
||||
<div v-else class="report-content">
|
||||
<pre>{{ JSON.stringify(reportData, null, 2) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { reportsApi } from '@/api'
|
||||
|
||||
const reports = ref([])
|
||||
const currentReport = ref(null)
|
||||
const reportData = ref(null)
|
||||
const loading = ref(false)
|
||||
|
||||
onMounted(async () => {
|
||||
await loadReports()
|
||||
})
|
||||
|
||||
async function loadReports() {
|
||||
try {
|
||||
const response = await reportsApi.list()
|
||||
reports.value = response.data.data.reports
|
||||
} catch (error) {
|
||||
console.error('Error loading reports:', error)
|
||||
}
|
||||
}
|
||||
|
||||
async function runReport(report) {
|
||||
currentReport.value = report
|
||||
loading.value = true
|
||||
reportData.value = null
|
||||
|
||||
try {
|
||||
let response
|
||||
switch (report.id) {
|
||||
case 'equipment-by-type':
|
||||
response = await reportsApi.equipmentByType()
|
||||
break
|
||||
case 'assets-by-status':
|
||||
response = await reportsApi.assetsByStatus()
|
||||
break
|
||||
case 'kb-popularity':
|
||||
response = await reportsApi.kbPopularity()
|
||||
break
|
||||
case 'warranty-status':
|
||||
response = await reportsApi.warrantyStatus()
|
||||
break
|
||||
case 'software-compliance':
|
||||
response = await reportsApi.softwareCompliance()
|
||||
break
|
||||
case 'asset-inventory':
|
||||
response = await reportsApi.assetInventory()
|
||||
break
|
||||
default:
|
||||
console.error('Unknown report:', report.id)
|
||||
return
|
||||
}
|
||||
reportData.value = response.data.data
|
||||
} catch (error) {
|
||||
console.error('Error running report:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function exportCSV() {
|
||||
if (!currentReport.value) return
|
||||
window.open(`/api/reports/${currentReport.value.id}?format=csv`, '_blank')
|
||||
}
|
||||
|
||||
function clearReport() {
|
||||
currentReport.value = null
|
||||
reportData.value = null
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.reports-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.report-card {
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.report-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.report-card h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.report-card p {
|
||||
color: var(--text-light);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.report-results {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.report-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.report-header h2 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.report-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.report-summary {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.report-content h3 {
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.report-content table {
|
||||
width: 100%;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user