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:
108
CLAUDE.md
Normal file
108
CLAUDE.md
Normal file
@@ -0,0 +1,108 @@
|
||||
# ShopDB Flask Project
|
||||
|
||||
## Database Configuration
|
||||
|
||||
- **Development database:** `shopdb_flask` (new schema with asset abstraction)
|
||||
- **Legacy database:** `shopdb` (Classic ASP schema - used for data migration reference only)
|
||||
- **Connection:** Configured in `.env` file
|
||||
|
||||
## Current Tasks
|
||||
|
||||
### Data Migration from Legacy Database
|
||||
|
||||
Migrate data from `shopdb` to `shopdb_flask` using the new asset-based schema.
|
||||
|
||||
**Status:** Pending
|
||||
|
||||
**Migration guide:** `migrations/DATA_MIGRATION_GUIDE.md`
|
||||
|
||||
**Tables to migrate:**
|
||||
- [ ] Reference data (vendors, models, locations, business units, subnets)
|
||||
- [ ] Equipment (machines with category='Equipment')
|
||||
- [ ] Computers/PCs (machines with pctypeid IS NOT NULL)
|
||||
- [ ] Network devices (machines with category='Network')
|
||||
- [ ] Printers (separate printers table)
|
||||
- [ ] Communications/IP addresses
|
||||
- [ ] Notifications
|
||||
- [ ] Machine relationships
|
||||
|
||||
### Plugin Architecture Verification
|
||||
|
||||
Verify all plugins follow plug-and-play architecture - can be added/removed without impacting core site or other plugins.
|
||||
|
||||
**Plugins to verify:**
|
||||
- [ ] Equipment (`plugins/equipment/`) - frontend: `views/machines/`
|
||||
- [ ] Computers (`plugins/computers/`) - frontend: `views/pcs/`
|
||||
- [ ] Printers (`plugins/printers/`) - frontend: `views/printers/`
|
||||
- [ ] Network (`plugins/network/`) - frontend: `views/network/`
|
||||
- [ ] USB (`plugins/usb/`) - frontend: `views/usb/`
|
||||
- [ ] Notifications (`plugins/notifications/`) - frontend: `views/notifications/`
|
||||
|
||||
**Architecture checks:**
|
||||
- [ ] Core API uses try/except ImportError for plugin imports (see `shopdb/core/api/assets.py` for pattern)
|
||||
- [ ] Frontend router can dynamically add/remove routes per plugin
|
||||
- [ ] Navigation menu reads from plugin `get_navigation_items()`
|
||||
- [ ] Document plugin enable/disable mechanism in app factory
|
||||
|
||||
**Each plugin must have:**
|
||||
- `models/__init__.py` - exports all models
|
||||
- `api/routes.py` - Flask Blueprint with endpoints
|
||||
- `plugin.py` - implements BasePlugin class
|
||||
- `manifest.json` - plugin metadata (name, version, dependencies)
|
||||
- No direct imports from core code (use optional imports)
|
||||
- Modular frontend components
|
||||
|
||||
## Plugin Structure
|
||||
|
||||
```
|
||||
plugins/
|
||||
{plugin_name}/
|
||||
__init__.py
|
||||
plugin.py # BasePlugin implementation
|
||||
manifest.json # Plugin metadata
|
||||
models/
|
||||
__init__.py # Export all models
|
||||
{model}.py # SQLAlchemy models
|
||||
api/
|
||||
__init__.py
|
||||
routes.py # Flask Blueprint
|
||||
```
|
||||
|
||||
## Key Files
|
||||
|
||||
- `shopdb/plugins/base.py` - BasePlugin class and PluginMeta
|
||||
- `shopdb/core/api/assets.py` - Example of optional plugin imports with try/except
|
||||
- `frontend/src/router/index.js` - Frontend routing
|
||||
- `frontend/src/components/AppSidebar.vue` - Navigation menu
|
||||
|
||||
## Migration Notes
|
||||
|
||||
See `migrations/` folder for:
|
||||
- `DATA_MIGRATION_GUIDE.md` - Complete guide for migrating from legacy shopdb to new schema
|
||||
- `MIGRATE_USB_DEVICES_FROM_EQUIPMENT.md` - USB device migration from equipment table
|
||||
- `FIX_LOCATIONONLY_EQUIPMENT_TYPES.md` - Fix for LocationOnly equipment types
|
||||
- `PRODUCTION_MIGRATION_GUIDE.md` - Production data import methods
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Start dev environment
|
||||
~/start-dev-env.sh
|
||||
|
||||
# Create/update database tables
|
||||
cd /home/camp/projects/shopdb-flask
|
||||
source venv/bin/activate
|
||||
flask db-utils create-all
|
||||
|
||||
# Seed reference data
|
||||
flask seed reference-data
|
||||
|
||||
# Restart services after changes
|
||||
pm2 restart shopdb-flask-api shopdb-flask-ui
|
||||
```
|
||||
|
||||
## Service URLs
|
||||
|
||||
- **Flask API:** http://localhost:5001
|
||||
- **Flask UI:** http://localhost:5173
|
||||
- **Legacy ASP:** http://192.168.122.151:8080
|
||||
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) {
|
||||
|
||||
321
migrations/DATA_MIGRATION_GUIDE.md
Normal file
321
migrations/DATA_MIGRATION_GUIDE.md
Normal file
@@ -0,0 +1,321 @@
|
||||
# ShopDB Flask Data Migration Guide
|
||||
|
||||
## Overview
|
||||
|
||||
This document describes how to migrate data from the legacy `shopdb` database to the new `shopdb_flask` database schema.
|
||||
|
||||
## Database Configuration
|
||||
|
||||
**Development:**
|
||||
- Legacy database: `shopdb` (Classic ASP/VBScript schema)
|
||||
- New database: `shopdb_flask` (Flask/SQLAlchemy schema)
|
||||
- Connection: `mysql+pymysql://root:rootpassword@127.0.0.1:3306/shopdb_flask`
|
||||
|
||||
**Production:**
|
||||
- Follow the same migration steps on production MySQL server
|
||||
- Update connection string in `.env` accordingly
|
||||
|
||||
## Schema Differences
|
||||
|
||||
### Legacy Schema (shopdb)
|
||||
- `machines` table holds ALL assets (equipment, PCs, network devices)
|
||||
- `printers` table is separate
|
||||
- No unified asset abstraction
|
||||
|
||||
### New Schema (shopdb_flask)
|
||||
- `assets` table: Core asset data (shared fields)
|
||||
- `assettypes` table: Asset category registry
|
||||
- Plugin extension tables:
|
||||
- `equipment` - Manufacturing equipment details
|
||||
- `computers` - PC-specific fields
|
||||
- `networkdevices` - Network device details
|
||||
- `printers` - Printer-specific fields
|
||||
- Each extension links to `assets` via `assetid`
|
||||
|
||||
## Migration Steps
|
||||
|
||||
### Step 1: Seed Reference Data
|
||||
|
||||
```bash
|
||||
cd /home/camp/projects/shopdb-flask
|
||||
source venv/bin/activate
|
||||
flask seed reference-data
|
||||
```
|
||||
|
||||
This creates:
|
||||
- Asset types (equipment, computer, network_device, printer)
|
||||
- Asset statuses (In Use, Spare, Retired, etc.)
|
||||
- Machine types, operating systems, relationship types
|
||||
|
||||
### Step 2: Migrate Asset Types
|
||||
|
||||
```sql
|
||||
-- Insert asset types if not exists
|
||||
INSERT INTO assettypes (assettype, pluginname, tablename, description) VALUES
|
||||
('equipment', 'equipment', 'equipment', 'Manufacturing equipment'),
|
||||
('computer', 'computers', 'computers', 'PCs and workstations'),
|
||||
('network_device', 'network', 'networkdevices', 'Network infrastructure'),
|
||||
('printer', 'printers', 'printers', 'Printers and MFPs')
|
||||
ON DUPLICATE KEY UPDATE assettype=assettype;
|
||||
```
|
||||
|
||||
### Step 3: Migrate Equipment
|
||||
|
||||
```sql
|
||||
-- Migrate equipment from legacy machines table
|
||||
INSERT INTO assets (assetid, assetnumber, name, serialnumber, assettypeid, statusid,
|
||||
locationid, businessunitid, mapleft, maptop, notes,
|
||||
createddate, modifieddate, isactive)
|
||||
SELECT
|
||||
m.machineid,
|
||||
m.machinenumber,
|
||||
m.alias,
|
||||
m.serialnumber,
|
||||
(SELECT assettypeid FROM assettypes WHERE assettype = 'equipment'),
|
||||
m.statusid,
|
||||
m.locationid,
|
||||
m.businessunitid,
|
||||
m.mapleft,
|
||||
m.maptop,
|
||||
m.notes,
|
||||
m.createddate,
|
||||
m.modifieddate,
|
||||
m.isactive
|
||||
FROM shopdb.machines m
|
||||
JOIN shopdb.machinetypes mt ON m.machinetypeid = mt.machinetypeid
|
||||
WHERE mt.category = 'Equipment'
|
||||
AND m.pctypeid IS NULL;
|
||||
|
||||
-- Insert equipment extension data
|
||||
INSERT INTO equipment (assetid, equipmenttypeid, vendorid, modelnumberid,
|
||||
controllertypeid, controllervendorid, controllermodelid)
|
||||
SELECT
|
||||
m.machineid,
|
||||
m.machinetypeid,
|
||||
m.vendorid,
|
||||
m.modelnumberid,
|
||||
m.controllertypeid,
|
||||
m.controllervendorid,
|
||||
m.controllermodelid
|
||||
FROM shopdb.machines m
|
||||
JOIN shopdb.machinetypes mt ON m.machinetypeid = mt.machinetypeid
|
||||
WHERE mt.category = 'Equipment'
|
||||
AND m.pctypeid IS NULL;
|
||||
```
|
||||
|
||||
### Step 4: Migrate PCs/Computers
|
||||
|
||||
```sql
|
||||
-- Migrate PCs to assets table
|
||||
INSERT INTO assets (assetid, assetnumber, name, serialnumber, assettypeid, statusid,
|
||||
locationid, businessunitid, mapleft, maptop, notes,
|
||||
createddate, modifieddate, isactive)
|
||||
SELECT
|
||||
m.machineid,
|
||||
m.machinenumber,
|
||||
m.alias,
|
||||
m.serialnumber,
|
||||
(SELECT assettypeid FROM assettypes WHERE assettype = 'computer'),
|
||||
m.statusid,
|
||||
m.locationid,
|
||||
m.businessunitid,
|
||||
m.mapleft,
|
||||
m.maptop,
|
||||
m.notes,
|
||||
m.createddate,
|
||||
m.modifieddate,
|
||||
m.isactive
|
||||
FROM shopdb.machines m
|
||||
WHERE m.pctypeid IS NOT NULL;
|
||||
|
||||
-- Insert computer extension data
|
||||
INSERT INTO computers (assetid, computertypeid, hostname, osid,
|
||||
loggedinuser, lastreporteddate, lastboottime,
|
||||
isvnc, iswinrm, isshopfloor)
|
||||
SELECT
|
||||
m.machineid,
|
||||
m.pctypeid,
|
||||
m.hostname,
|
||||
m.osid,
|
||||
m.loggedinuser,
|
||||
m.lastreporteddate,
|
||||
m.lastboottime,
|
||||
m.isvnc,
|
||||
m.iswinrm,
|
||||
m.isshopfloor
|
||||
FROM shopdb.machines m
|
||||
WHERE m.pctypeid IS NOT NULL;
|
||||
```
|
||||
|
||||
### Step 5: Migrate Network Devices
|
||||
|
||||
```sql
|
||||
-- Migrate network devices to assets
|
||||
INSERT INTO assets (assetid, assetnumber, name, serialnumber, assettypeid, statusid,
|
||||
locationid, businessunitid, mapleft, maptop, notes,
|
||||
createddate, modifieddate, isactive)
|
||||
SELECT
|
||||
m.machineid,
|
||||
m.machinenumber,
|
||||
m.alias,
|
||||
m.serialnumber,
|
||||
(SELECT assettypeid FROM assettypes WHERE assettype = 'network_device'),
|
||||
m.statusid,
|
||||
m.locationid,
|
||||
m.businessunitid,
|
||||
m.mapleft,
|
||||
m.maptop,
|
||||
m.notes,
|
||||
m.createddate,
|
||||
m.modifieddate,
|
||||
m.isactive
|
||||
FROM shopdb.machines m
|
||||
JOIN shopdb.machinetypes mt ON m.machinetypeid = mt.machinetypeid
|
||||
WHERE mt.category = 'Network';
|
||||
|
||||
-- Insert network device extension data
|
||||
INSERT INTO networkdevices (assetid, networkdevicetypeid, hostname, vendorid, modelnumberid)
|
||||
SELECT
|
||||
m.machineid,
|
||||
m.machinetypeid,
|
||||
m.hostname,
|
||||
m.vendorid,
|
||||
m.modelnumberid
|
||||
FROM shopdb.machines m
|
||||
JOIN shopdb.machinetypes mt ON m.machinetypeid = mt.machinetypeid
|
||||
WHERE mt.category = 'Network';
|
||||
```
|
||||
|
||||
### Step 6: Migrate Printers
|
||||
|
||||
```sql
|
||||
-- Migrate printers to assets (printers are in separate table in legacy)
|
||||
INSERT INTO assets (assetnumber, name, serialnumber, assettypeid, statusid,
|
||||
locationid, businessunitid, notes, createddate, modifieddate, isactive)
|
||||
SELECT
|
||||
p.hostname,
|
||||
p.windowsname,
|
||||
NULL,
|
||||
(SELECT assettypeid FROM assettypes WHERE assettype = 'printer'),
|
||||
1, -- Default status
|
||||
p.locationid,
|
||||
p.businessunitid,
|
||||
NULL,
|
||||
p.createddate,
|
||||
p.modifieddate,
|
||||
p.isactive
|
||||
FROM shopdb.printers p;
|
||||
|
||||
-- Insert printer extension data (need to get the new assetid)
|
||||
INSERT INTO printers (assetid, printertypeid, vendorid, modelnumberid, hostname,
|
||||
windowsname, sharename, iscsf, installpath, pin,
|
||||
iscolor, isduplex, isnetwork)
|
||||
SELECT
|
||||
a.assetid,
|
||||
p.printertypeid,
|
||||
p.vendorid,
|
||||
p.modelnumberid,
|
||||
p.hostname,
|
||||
p.windowsname,
|
||||
p.sharename,
|
||||
p.iscsf,
|
||||
p.installpath,
|
||||
p.pin,
|
||||
p.iscolor,
|
||||
p.isduplex,
|
||||
p.isnetwork
|
||||
FROM shopdb.printers p
|
||||
JOIN assets a ON a.assetnumber = p.hostname
|
||||
WHERE a.assettypeid = (SELECT assettypeid FROM assettypes WHERE assettype = 'printer');
|
||||
```
|
||||
|
||||
### Step 7: Migrate Communications (IP Addresses)
|
||||
|
||||
```sql
|
||||
-- Migrate communications/IP addresses
|
||||
INSERT INTO communications (machineid, assetid, comtypeid, address,
|
||||
subnetid, isprimary, createddate, modifieddate, isactive)
|
||||
SELECT
|
||||
c.machineid,
|
||||
c.machineid, -- assetid = machineid for migrated assets
|
||||
c.comtypeid,
|
||||
c.address,
|
||||
c.subnetid,
|
||||
c.isprimary,
|
||||
c.createddate,
|
||||
c.modifieddate,
|
||||
c.isactive
|
||||
FROM shopdb.communications c;
|
||||
```
|
||||
|
||||
### Step 8: Migrate Notifications
|
||||
|
||||
```sql
|
||||
INSERT INTO notifications (notificationid, notificationtypeid, businessunitid, appid,
|
||||
notification, starttime, endtime, ticketnumber, link,
|
||||
isactive, isshopfloor, employeesso, employeename)
|
||||
SELECT * FROM shopdb.notifications;
|
||||
```
|
||||
|
||||
### Step 9: Migrate Supporting Tables
|
||||
|
||||
```sql
|
||||
-- Vendors
|
||||
INSERT INTO vendors SELECT * FROM shopdb.vendors
|
||||
ON DUPLICATE KEY UPDATE vendor=VALUES(vendor);
|
||||
|
||||
-- Models
|
||||
INSERT INTO models SELECT * FROM shopdb.models
|
||||
ON DUPLICATE KEY UPDATE modelnumber=VALUES(modelnumber);
|
||||
|
||||
-- Locations
|
||||
INSERT INTO locations SELECT * FROM shopdb.locations
|
||||
ON DUPLICATE KEY UPDATE locationname=VALUES(locationname);
|
||||
|
||||
-- Business Units
|
||||
INSERT INTO businessunits SELECT * FROM shopdb.businessunits
|
||||
ON DUPLICATE KEY UPDATE businessunit=VALUES(businessunit);
|
||||
|
||||
-- Subnets
|
||||
INSERT INTO subnets SELECT * FROM shopdb.subnets
|
||||
ON DUPLICATE KEY UPDATE subnet=VALUES(subnet);
|
||||
```
|
||||
|
||||
## Verification Queries
|
||||
|
||||
```sql
|
||||
-- Check migration counts
|
||||
SELECT 'Legacy machines' as source, COUNT(*) as cnt FROM shopdb.machines
|
||||
UNION ALL SELECT 'New assets', COUNT(*) FROM shopdb_flask.assets
|
||||
UNION ALL SELECT 'Equipment', COUNT(*) FROM shopdb_flask.equipment
|
||||
UNION ALL SELECT 'Computers', COUNT(*) FROM shopdb_flask.computers
|
||||
UNION ALL SELECT 'Network devices', COUNT(*) FROM shopdb_flask.networkdevices
|
||||
UNION ALL SELECT 'Printers', COUNT(*) FROM shopdb_flask.printers;
|
||||
|
||||
-- Verify asset type distribution
|
||||
SELECT at.assettype, COUNT(a.assetid) as count
|
||||
FROM shopdb_flask.assets a
|
||||
JOIN shopdb_flask.assettypes at ON a.assettypeid = at.assettypeid
|
||||
GROUP BY at.assettype;
|
||||
```
|
||||
|
||||
## Production Deployment Notes
|
||||
|
||||
1. **Backup production database first**
|
||||
2. Create `shopdb_flask` database on production
|
||||
3. Run `flask db-utils create-all` to create schema
|
||||
4. Execute migration SQL scripts in order
|
||||
5. Verify data counts match
|
||||
6. Update Flask `.env` to point to production database
|
||||
7. Restart Flask services
|
||||
|
||||
## Rollback
|
||||
|
||||
If migration fails:
|
||||
1. Drop all tables in `shopdb_flask`: `flask db-utils drop-all`
|
||||
2. Recreate schema: `flask db-utils create-all`
|
||||
3. Investigate and fix migration scripts
|
||||
4. Re-run migration
|
||||
|
||||
---
|
||||
Last updated: 2026-01-28
|
||||
89
migrations/FIX_LOCATIONONLY_EQUIPMENT_TYPES.md
Normal file
89
migrations/FIX_LOCATIONONLY_EQUIPMENT_TYPES.md
Normal file
@@ -0,0 +1,89 @@
|
||||
# Fix LocationOnly Equipment Types Migration
|
||||
|
||||
## Issue
|
||||
During the original migration from the VBScript ShopDB site, 110 machines were imported with `LocationOnly` type even though they have models assigned that specify the correct equipment type.
|
||||
|
||||
## Root Cause
|
||||
The migration copied the `machinetypeid` directly from the `machines` table, but many machines had `machinetypeid=1` (LocationOnly) even when they had a model with a proper type.
|
||||
|
||||
## Affected Records
|
||||
- **110 machines** in the `machines` table
|
||||
- **110 equipment** records in the `equipment` table
|
||||
|
||||
## Example
|
||||
| Machine | Current Type | Should Be | Model |
|
||||
|---------|-------------|-----------|-------|
|
||||
| 7502 | LocationOnly | Horizontal Machining Center | a81nx |
|
||||
| 3108 | LocationOnly | Vertical Lathe | VTM-100 |
|
||||
| 4001 | LocationOnly | 5-axis Mill | VP9000 |
|
||||
|
||||
## Fix SQL
|
||||
|
||||
### Step 1: Preview affected records
|
||||
```sql
|
||||
-- Preview what will be changed
|
||||
SELECT
|
||||
m.machineid,
|
||||
m.machinenumber,
|
||||
mt_current.machinetype as current_type,
|
||||
mt_model.machinetype as correct_type,
|
||||
mo.modelnumber
|
||||
FROM machines m
|
||||
JOIN models mo ON mo.modelnumberid = m.modelnumberid
|
||||
JOIN machinetypes mt_current ON mt_current.machinetypeid = m.machinetypeid
|
||||
JOIN machinetypes mt_model ON mt_model.machinetypeid = mo.machinetypeid
|
||||
WHERE m.machinetypeid = 1 -- Currently LocationOnly
|
||||
AND mo.machinetypeid != 1; -- Model has a real type
|
||||
```
|
||||
|
||||
### Step 2: Fix the machines table
|
||||
```sql
|
||||
-- Update machines table to use the model's machine type
|
||||
UPDATE machines m
|
||||
JOIN models mo ON mo.modelnumberid = m.modelnumberid
|
||||
SET m.machinetypeid = mo.machinetypeid
|
||||
WHERE m.machinetypeid = 1 -- Currently LocationOnly
|
||||
AND mo.machinetypeid != 1; -- Model has a real type
|
||||
```
|
||||
|
||||
### Step 3: Fix the equipment table
|
||||
```sql
|
||||
-- Update equipment table to match
|
||||
-- Note: equipmenttypeid values match machinetypeid values
|
||||
UPDATE equipment e
|
||||
JOIN assets a ON a.assetid = e.assetid
|
||||
JOIN machines m ON m.machinenumber = SUBSTRING_INDEX(a.assetnumber, '-', 1)
|
||||
SET e.equipmenttypeid = m.machinetypeid
|
||||
WHERE e.equipmenttypeid = 1 -- Currently LocationOnly
|
||||
AND m.machinetypeid != 1; -- Machine now has correct type
|
||||
```
|
||||
|
||||
### Alternative Step 3 (if asset numbers don't match)
|
||||
```sql
|
||||
-- Update equipment based on their own model reference
|
||||
UPDATE equipment e
|
||||
JOIN models mo ON mo.modelnumberid = e.modelnumberid
|
||||
SET e.equipmenttypeid = mo.machinetypeid
|
||||
WHERE e.equipmenttypeid = 1 -- Currently LocationOnly
|
||||
AND mo.machinetypeid != 1 -- Model has a real type
|
||||
AND e.modelnumberid IS NOT NULL;
|
||||
```
|
||||
|
||||
## Verification
|
||||
```sql
|
||||
-- Count remaining LocationOnly records that should have been fixed
|
||||
SELECT COUNT(*) as remaining_fixable
|
||||
FROM machines m
|
||||
JOIN models mo ON mo.modelnumberid = m.modelnumberid
|
||||
WHERE m.machinetypeid = 1
|
||||
AND mo.machinetypeid != 1;
|
||||
-- Should return 0 after fix
|
||||
```
|
||||
|
||||
## Notes for Future Migrations
|
||||
1. When importing equipment, always check if the model has a type and use that as the default
|
||||
2. Only use "LocationOnly" for genuine location markers (rooms, offices) without models
|
||||
3. Validate after import: any equipment with a model should NOT be LocationOnly
|
||||
|
||||
## Date Created
|
||||
2026-01-27
|
||||
92
migrations/MIGRATE_USB_DEVICES_FROM_EQUIPMENT.md
Normal file
92
migrations/MIGRATE_USB_DEVICES_FROM_EQUIPMENT.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# Migrate USB Devices from Equipment Table
|
||||
|
||||
## Issue
|
||||
6 USB devices were incorrectly stored in the `equipment` table instead of the proper `usbdevices` table.
|
||||
|
||||
## Status
|
||||
- [x] `pin` field added to `usbdevices` table for encrypted devices
|
||||
- [x] `pin` field added to USBDevice model
|
||||
- [x] USB Device equipment type deactivated (equipmenttypeid=44)
|
||||
- [ ] Migrate 6 USB devices to usbdevices table
|
||||
|
||||
## Affected Records
|
||||
| Asset Number | Name | Serial Number |
|
||||
|--------------|------|---------------|
|
||||
| 82841957-5891 | Green Kingston 64GB | 82841957 |
|
||||
| 48854302-5892 | Blue Kingston 64GB | 48854302 |
|
||||
| 75953637-5893 | Blue Kingston 64GB USB 3.0 | 75953637 |
|
||||
| 41299370-5904 | Lenovo Portable DVD | 41299370 |
|
||||
| 15492331-5905 | TEAC Portable Floppy Drive | 15492331 |
|
||||
| 25777358-5906 | Netgear WiFi Adapter | 25777358 |
|
||||
|
||||
## Migration SQL
|
||||
|
||||
### Step 1: Insert into usbdevices
|
||||
```sql
|
||||
INSERT INTO usbdevices (
|
||||
serialnumber,
|
||||
label,
|
||||
assetnumber,
|
||||
usbdevicetypeid,
|
||||
storagelocation,
|
||||
notes,
|
||||
createddate,
|
||||
modifieddate,
|
||||
isactive
|
||||
)
|
||||
SELECT
|
||||
a.serialnumber,
|
||||
a.name,
|
||||
a.assetnumber,
|
||||
NULL, -- Assign proper type later
|
||||
NULL, -- Storage location TBD
|
||||
a.notes,
|
||||
a.createddate,
|
||||
a.modifieddate,
|
||||
a.isactive
|
||||
FROM assets a
|
||||
JOIN equipment e ON e.assetid = a.assetid
|
||||
WHERE e.equipmenttypeid = 44; -- USB Device type
|
||||
```
|
||||
|
||||
### Step 2: Delete from equipment table
|
||||
```sql
|
||||
DELETE e FROM equipment e
|
||||
JOIN assets a ON a.assetid = e.assetid
|
||||
WHERE e.equipmenttypeid = 44;
|
||||
```
|
||||
|
||||
### Step 3: Delete from assets table
|
||||
```sql
|
||||
DELETE a FROM assets a
|
||||
JOIN equipment e ON e.assetid = a.assetid
|
||||
WHERE e.equipmenttypeid = 44;
|
||||
|
||||
-- Note: Run step 2 first since it references assets
|
||||
```
|
||||
|
||||
### Alternative: Manual Migration
|
||||
Since there are only 6 devices, you may prefer to:
|
||||
1. Manually create them in the USB Devices section
|
||||
2. Delete the equipment/asset records
|
||||
|
||||
## USB Device Types to Create
|
||||
```sql
|
||||
-- Check if types exist, create if needed
|
||||
INSERT IGNORE INTO usbdevicetypes (typename, description, icon) VALUES
|
||||
('Flash Drive', 'USB flash drive / thumb drive', 'usb'),
|
||||
('External HDD', 'External hard disk drive', 'harddisk'),
|
||||
('External SSD', 'External solid state drive', 'harddisk'),
|
||||
('Card Reader', 'SD/CF card reader', 'sdcard'),
|
||||
('Optical Drive', 'External CD/DVD/Blu-ray drive', 'disc'),
|
||||
('WiFi Adapter', 'USB wireless network adapter', 'wifi'),
|
||||
('Other', 'Other USB device', 'usb');
|
||||
```
|
||||
|
||||
## Notes
|
||||
- USB Device equipment type has been deactivated (isactive=0)
|
||||
- New USB devices should be created in the USB Devices section
|
||||
- USB devices do NOT need map positions (no mapleft/maptop)
|
||||
|
||||
## Date Created
|
||||
2026-01-27
|
||||
165
migrations/PRODUCTION_MIGRATION_GUIDE.md
Normal file
165
migrations/PRODUCTION_MIGRATION_GUIDE.md
Normal file
@@ -0,0 +1,165 @@
|
||||
# Production Migration Guide
|
||||
|
||||
## Overview
|
||||
This guide documents the process for migrating data from the legacy VBScript ShopDB site to the new Flask-based ShopDB.
|
||||
|
||||
## Database Architecture
|
||||
|
||||
### Legacy System (VBScript)
|
||||
- Single `machines` table containing all equipment, PCs, printers
|
||||
- `machinetypes` table for classification
|
||||
- `models` table with `machinetypeid` reference
|
||||
|
||||
### New System (Flask)
|
||||
- Core `assets` table (unified asset registry)
|
||||
- Plugin-specific extension tables:
|
||||
- `equipment` (equipmenttypeid → equipmenttypes)
|
||||
- `computers` (computertypeid → computertypes)
|
||||
- `printers` (printertypeid → printertypes)
|
||||
- `network_device` (networkdevicetypeid → networkdevicetypes)
|
||||
|
||||
## Key Mappings
|
||||
|
||||
### Asset Type Mapping
|
||||
| Legacy Category | New Asset Type | Extension Table |
|
||||
|-----------------|---------------|-----------------|
|
||||
| Equipment machines | Equipment | equipment |
|
||||
| PC/Computer | Computer | computers |
|
||||
| Printer | Printer | printers |
|
||||
| Network (IDF, Switch, AP) | Network Device | network_device |
|
||||
|
||||
### Type ID Alignment
|
||||
The `equipmenttypes` table IDs match `machinetypes` IDs for easy migration:
|
||||
- equipmenttypeid = machinetypeid (where applicable)
|
||||
|
||||
## Migration Steps
|
||||
|
||||
### Step 1: Export from Legacy Database
|
||||
```sql
|
||||
-- Export machines with all related data
|
||||
SELECT
|
||||
m.*,
|
||||
mt.machinetype,
|
||||
mo.modelnumber,
|
||||
mo.machinetypeid as model_typeid,
|
||||
v.vendor,
|
||||
bu.businessunit,
|
||||
s.status
|
||||
FROM machines m
|
||||
LEFT JOIN machinetypes mt ON mt.machinetypeid = m.machinetypeid
|
||||
LEFT JOIN models mo ON mo.modelnumberid = m.modelnumberid
|
||||
LEFT JOIN vendors v ON v.vendorid = m.vendorid
|
||||
LEFT JOIN businessunits bu ON bu.businessunitid = m.businessunitid
|
||||
LEFT JOIN statuses s ON s.statusid = m.statusid;
|
||||
```
|
||||
|
||||
### Step 2: Create Assets
|
||||
For each machine, create an asset record:
|
||||
```sql
|
||||
INSERT INTO assets (assetnumber, name, assettypeid, statusid, locationid, businessunitid, mapleft, maptop)
|
||||
SELECT
|
||||
CONCAT(machinenumber, '-', machineid), -- Unique asset number
|
||||
alias,
|
||||
CASE
|
||||
WHEN category = 'PC' THEN 2 -- Computer
|
||||
WHEN category = 'Printer' THEN 4 -- Printer
|
||||
ELSE 1 -- Equipment
|
||||
END,
|
||||
statusid,
|
||||
locationid,
|
||||
businessunitid,
|
||||
mapleft,
|
||||
maptop
|
||||
FROM machines;
|
||||
```
|
||||
|
||||
### Step 3: Create Extension Records
|
||||
```sql
|
||||
-- For Equipment
|
||||
INSERT INTO equipment (assetid, equipmenttypeid, vendorid, modelnumberid)
|
||||
SELECT
|
||||
a.assetid,
|
||||
COALESCE(mo.machinetypeid, m.machinetypeid), -- Use model's type if available!
|
||||
m.vendorid,
|
||||
m.modelnumberid
|
||||
FROM machines m
|
||||
JOIN assets a ON a.assetnumber = CONCAT(m.machinenumber, '-', m.machineid)
|
||||
LEFT JOIN models mo ON mo.modelnumberid = m.modelnumberid
|
||||
WHERE m.category = 'Equipment' OR m.category IS NULL;
|
||||
```
|
||||
|
||||
### Step 4: Post-Migration Fixes
|
||||
|
||||
#### Fix LocationOnly Equipment Types
|
||||
Equipment imported with LocationOnly type should inherit type from their model:
|
||||
```sql
|
||||
-- Fix equipment that has a model with a proper type
|
||||
UPDATE equipment e
|
||||
JOIN assets a ON a.assetid = e.assetid
|
||||
JOIN machines m ON m.machinenumber = SUBSTRING_INDEX(a.assetnumber, '-', 1)
|
||||
JOIN models mo ON mo.modelnumberid = m.modelnumberid
|
||||
SET e.equipmenttypeid = mo.machinetypeid
|
||||
WHERE e.equipmenttypeid = 1 -- LocationOnly
|
||||
AND mo.machinetypeid != 1; -- Model has real type
|
||||
```
|
||||
|
||||
## Validation Queries
|
||||
|
||||
### Check for Orphaned Assets
|
||||
```sql
|
||||
SELECT a.* FROM assets a
|
||||
LEFT JOIN equipment e ON e.assetid = a.assetid
|
||||
LEFT JOIN computers c ON c.assetid = a.assetid
|
||||
LEFT JOIN printers p ON p.assetid = a.assetid
|
||||
WHERE a.assettypeid = 1 -- Equipment type
|
||||
AND e.assetid IS NULL;
|
||||
```
|
||||
|
||||
### Check LocationOnly with Models
|
||||
```sql
|
||||
-- Should return 0 after migration fix
|
||||
SELECT COUNT(*)
|
||||
FROM equipment e
|
||||
JOIN assets a ON a.assetid = e.assetid
|
||||
JOIN machines m ON m.machinenumber = SUBSTRING_INDEX(a.assetnumber, '-', 1)
|
||||
JOIN models mo ON mo.modelnumberid = m.modelnumberid
|
||||
WHERE e.equipmenttypeid = 1
|
||||
AND mo.machinetypeid != 1;
|
||||
```
|
||||
|
||||
### Verify Type Distribution
|
||||
```sql
|
||||
SELECT et.equipmenttype, COUNT(*) as count
|
||||
FROM equipment e
|
||||
JOIN equipmenttypes et ON et.equipmenttypeid = e.equipmenttypeid
|
||||
GROUP BY et.equipmenttype
|
||||
ORDER BY count DESC;
|
||||
```
|
||||
|
||||
## Common Issues
|
||||
|
||||
### Issue: Equipment marked as LocationOnly but has model
|
||||
**Cause**: Migration copied machinetypeid from machines table instead of using model's type
|
||||
**Fix**: See FIX_LOCATIONONLY_EQUIPMENT_TYPES.md
|
||||
|
||||
### Issue: Missing model relationship
|
||||
**Cause**: Equipment table modelnumberid not populated during migration
|
||||
**Fix**: Link through machines table using asset number pattern
|
||||
|
||||
### Issue: Duplicate asset numbers
|
||||
**Cause**: Asset number generation didn't account for existing duplicates
|
||||
**Fix**: Use unique suffix or check before insert
|
||||
|
||||
## Scripts Location
|
||||
- `/migrations/FIX_LOCATIONONLY_EQUIPMENT_TYPES.md` - Fix for LocationOnly type issue
|
||||
- `/scripts/import_from_mysql.py` - Original import script (may need updates)
|
||||
- `/scripts/migration/` - Migration utilities
|
||||
|
||||
## Notes
|
||||
- Always backup before running migration fixes
|
||||
- Test on staging/dev before production
|
||||
- Verify counts before and after each fix
|
||||
- Keep legacy `machines` table for reference during transition
|
||||
|
||||
## Date Created
|
||||
2026-01-27
|
||||
23
migrations/rename_underscore_columns.sql
Normal file
23
migrations/rename_underscore_columns.sql
Normal file
@@ -0,0 +1,23 @@
|
||||
-- Rename database columns to remove underscores per naming convention
|
||||
-- Date: 2026-02-03
|
||||
-- Affects: assettypes, assetrelationships, equipment, usbcheckouts
|
||||
|
||||
-- 1. assettypes table
|
||||
ALTER TABLE assettypes CHANGE COLUMN plugin_name pluginname VARCHAR(100) NULL COMMENT 'Plugin that owns this type';
|
||||
ALTER TABLE assettypes CHANGE COLUMN table_name tablename VARCHAR(100) NULL COMMENT 'Extension table name for this type';
|
||||
|
||||
-- 2. assetrelationships table
|
||||
ALTER TABLE assetrelationships CHANGE COLUMN source_assetid sourceassetid INT NOT NULL;
|
||||
ALTER TABLE assetrelationships CHANGE COLUMN target_assetid targetassetid INT NOT NULL;
|
||||
|
||||
-- 3. equipment table
|
||||
ALTER TABLE equipment CHANGE COLUMN controller_vendorid controllervendorid INT NULL COMMENT 'Controller vendor (e.g., FANUC)';
|
||||
ALTER TABLE equipment CHANGE COLUMN controller_modelid controllermodelid INT NULL COMMENT 'Controller model (e.g., 31B)';
|
||||
|
||||
-- 4. usbcheckouts table
|
||||
ALTER TABLE usbcheckouts CHANGE COLUMN checkout_name checkoutname VARCHAR(100) NULL COMMENT 'Name of user';
|
||||
ALTER TABLE usbcheckouts CHANGE COLUMN checkout_time checkouttime DATETIME NOT NULL;
|
||||
ALTER TABLE usbcheckouts CHANGE COLUMN checkin_time checkintime DATETIME NULL;
|
||||
ALTER TABLE usbcheckouts CHANGE COLUMN checkout_reason checkoutreason TEXT NULL COMMENT 'Reason for checkout';
|
||||
ALTER TABLE usbcheckouts CHANGE COLUMN checkin_notes checkinnotes TEXT NULL;
|
||||
ALTER TABLE usbcheckouts CHANGE COLUMN was_wiped waswiped TINYINT(1) NULL COMMENT 'Was device wiped after return';
|
||||
@@ -621,8 +621,8 @@ def dashboard_summary():
|
||||
|
||||
return success_response({
|
||||
'total': total,
|
||||
'by_type': [{'type': t, 'count': c} for t, c in by_type],
|
||||
'by_os': [{'os': o, 'count': c} for o, c in by_os],
|
||||
'bytype': [{'type': t, 'count': c} for t, c in by_type],
|
||||
'byos': [{'os': o, 'count': c} for o, c in by_os],
|
||||
'shopfloor': shopfloor_count,
|
||||
'non_shopfloor': total - shopfloor_count
|
||||
'nonshopfloor': total - shopfloor_count
|
||||
})
|
||||
|
||||
@@ -117,9 +117,9 @@ class Computer(BaseModel):
|
||||
|
||||
# Add related object names
|
||||
if self.computertype:
|
||||
result['computertype_name'] = self.computertype.computertype
|
||||
result['computertypename'] = self.computertype.computertype
|
||||
if self.operatingsystem:
|
||||
result['os_name'] = self.operatingsystem.osname
|
||||
result['osname'] = self.operatingsystem.osname
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@@ -78,8 +78,8 @@ class ComputersPlugin(BasePlugin):
|
||||
if not existing:
|
||||
at = AssetType(
|
||||
assettype='computer',
|
||||
plugin_name='computers',
|
||||
table_name='computers',
|
||||
pluginname='computers',
|
||||
tablename='computers',
|
||||
description='PCs, servers, and workstations',
|
||||
icon='desktop'
|
||||
)
|
||||
@@ -201,9 +201,9 @@ class ComputersPlugin(BasePlugin):
|
||||
"""Return navigation menu items."""
|
||||
return [
|
||||
{
|
||||
'name': 'Computers',
|
||||
'name': 'PCs',
|
||||
'icon': 'desktop',
|
||||
'route': '/computers',
|
||||
'route': '/pcs',
|
||||
'position': 15,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -306,8 +306,8 @@ def create_equipment():
|
||||
lastmaintenancedate=data.get('lastmaintenancedate'),
|
||||
nextmaintenancedate=data.get('nextmaintenancedate'),
|
||||
maintenanceintervaldays=data.get('maintenanceintervaldays'),
|
||||
controller_vendorid=data.get('controller_vendorid'),
|
||||
controller_modelid=data.get('controller_modelid')
|
||||
controllervendorid=data.get('controllervendorid'),
|
||||
controllermodelid=data.get('controllermodelid')
|
||||
)
|
||||
|
||||
db.session.add(equip)
|
||||
@@ -358,7 +358,7 @@ def update_equipment(equipment_id: int):
|
||||
equipment_fields = ['equipmenttypeid', 'vendorid', 'modelnumberid',
|
||||
'requiresmanualconfig', 'islocationonly',
|
||||
'lastmaintenancedate', 'nextmaintenancedate', 'maintenanceintervaldays',
|
||||
'controller_vendorid', 'controller_modelid']
|
||||
'controllervendorid', 'controllermodelid']
|
||||
for key in equipment_fields:
|
||||
if key in data:
|
||||
setattr(equip, key, data[key])
|
||||
@@ -427,6 +427,6 @@ def dashboard_summary():
|
||||
|
||||
return success_response({
|
||||
'total': total,
|
||||
'by_type': [{'type': t, 'count': c} for t, c in by_type],
|
||||
'by_status': [{'status': s, 'count': c} for s, c in by_status]
|
||||
'bytype': [{'type': t, 'count': c} for t, c in by_type],
|
||||
'bystatus': [{'status': s, 'count': c} for s, c in by_status]
|
||||
})
|
||||
|
||||
@@ -78,13 +78,13 @@ class Equipment(BaseModel):
|
||||
maintenanceintervaldays = db.Column(db.Integer, nullable=True)
|
||||
|
||||
# Controller info (for CNC machines)
|
||||
controller_vendorid = db.Column(
|
||||
controllervendorid = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey('vendors.vendorid'),
|
||||
nullable=True,
|
||||
comment='Controller vendor (e.g., FANUC)'
|
||||
)
|
||||
controller_modelid = db.Column(
|
||||
controllermodelid = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey('models.modelnumberid'),
|
||||
nullable=True,
|
||||
@@ -99,8 +99,8 @@ class Equipment(BaseModel):
|
||||
equipmenttype = db.relationship('EquipmentType', backref='equipment')
|
||||
vendor = db.relationship('Vendor', foreign_keys=[vendorid], backref='equipment_items')
|
||||
model = db.relationship('Model', foreign_keys=[modelnumberid], backref='equipment_items')
|
||||
controller_vendor = db.relationship('Vendor', foreign_keys=[controller_vendorid], backref='equipment_controllers')
|
||||
controller_model = db.relationship('Model', foreign_keys=[controller_modelid], backref='equipment_controller_models')
|
||||
controllervendor = db.relationship('Vendor', foreign_keys=[controllervendorid], backref='equipment_controllers')
|
||||
controllermodel = db.relationship('Model', foreign_keys=[controllermodelid], backref='equipment_controller_models')
|
||||
|
||||
__table_args__ = (
|
||||
db.Index('idx_equipment_type', 'equipmenttypeid'),
|
||||
@@ -116,16 +116,18 @@ class Equipment(BaseModel):
|
||||
|
||||
# Add related object names
|
||||
if self.equipmenttype:
|
||||
result['equipmenttype_name'] = self.equipmenttype.equipmenttype
|
||||
result['equipmenttypename'] = self.equipmenttype.equipmenttype
|
||||
if self.vendor:
|
||||
result['vendor_name'] = self.vendor.vendor
|
||||
result['vendorname'] = self.vendor.vendor
|
||||
if self.model:
|
||||
result['model_name'] = self.model.modelnumber
|
||||
result['modelname'] = self.model.modelnumber
|
||||
if self.model.imageurl:
|
||||
result['imageurl'] = self.model.imageurl
|
||||
|
||||
# Add controller info
|
||||
if self.controller_vendor:
|
||||
result['controller_vendor_name'] = self.controller_vendor.vendor
|
||||
if self.controller_model:
|
||||
result['controller_model_name'] = self.controller_model.modelnumber
|
||||
if self.controllervendor:
|
||||
result['controllervendorname'] = self.controllervendor.vendor
|
||||
if self.controllermodel:
|
||||
result['controllermodelname'] = self.controllermodel.modelnumber
|
||||
|
||||
return result
|
||||
|
||||
@@ -79,8 +79,8 @@ class EquipmentPlugin(BasePlugin):
|
||||
if not existing:
|
||||
at = AssetType(
|
||||
assettype='equipment',
|
||||
plugin_name='equipment',
|
||||
table_name='equipment',
|
||||
pluginname='equipment',
|
||||
tablename='equipment',
|
||||
description='Manufacturing equipment (CNCs, CMMs, lathes, etc.)',
|
||||
icon='cog'
|
||||
)
|
||||
@@ -214,7 +214,7 @@ class EquipmentPlugin(BasePlugin):
|
||||
{
|
||||
'name': 'Equipment',
|
||||
'icon': 'cog',
|
||||
'route': '/equipment',
|
||||
'route': '/machines',
|
||||
'position': 10,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -227,7 +227,7 @@ def get_network_device(device_id: int):
|
||||
)
|
||||
|
||||
result = netdev.asset.to_dict() if netdev.asset else {}
|
||||
result['network_device'] = netdev.to_dict()
|
||||
result['networkdevice'] = netdev.to_dict()
|
||||
|
||||
return success_response(result)
|
||||
|
||||
@@ -246,7 +246,7 @@ def get_network_device_by_asset(asset_id: int):
|
||||
)
|
||||
|
||||
result = netdev.asset.to_dict() if netdev.asset else {}
|
||||
result['network_device'] = netdev.to_dict()
|
||||
result['networkdevice'] = netdev.to_dict()
|
||||
|
||||
return success_response(result)
|
||||
|
||||
@@ -265,7 +265,7 @@ def get_network_device_by_hostname(hostname: str):
|
||||
)
|
||||
|
||||
result = netdev.asset.to_dict() if netdev.asset else {}
|
||||
result['network_device'] = netdev.to_dict()
|
||||
result['networkdevice'] = netdev.to_dict()
|
||||
|
||||
return success_response(result)
|
||||
|
||||
@@ -353,7 +353,7 @@ def create_network_device():
|
||||
db.session.commit()
|
||||
|
||||
result = asset.to_dict()
|
||||
result['network_device'] = netdev.to_dict()
|
||||
result['networkdevice'] = netdev.to_dict()
|
||||
|
||||
return success_response(result, message='Network device created', http_code=201)
|
||||
|
||||
@@ -413,7 +413,7 @@ def update_network_device(device_id: int):
|
||||
db.session.commit()
|
||||
|
||||
result = asset.to_dict()
|
||||
result['network_device'] = netdev.to_dict()
|
||||
result['networkdevice'] = netdev.to_dict()
|
||||
|
||||
return success_response(result, message='Network device updated')
|
||||
|
||||
@@ -479,10 +479,10 @@ def dashboard_summary():
|
||||
|
||||
return success_response({
|
||||
'total': total,
|
||||
'by_type': [{'type': t, 'count': c} for t, c in by_type],
|
||||
'by_vendor': [{'vendor': v, 'count': c} for v, c in by_vendor],
|
||||
'bytype': [{'type': t, 'count': c} for t, c in by_type],
|
||||
'byvendor': [{'vendor': v, 'count': c} for v, c in by_vendor],
|
||||
'poe': poe_count,
|
||||
'non_poe': total - poe_count
|
||||
'nonpoe': total - poe_count
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -114,8 +114,8 @@ class NetworkDevice(BaseModel):
|
||||
|
||||
# Add related object names
|
||||
if self.networkdevicetype:
|
||||
result['networkdevicetype_name'] = self.networkdevicetype.networkdevicetype
|
||||
result['networkdevicetypename'] = self.networkdevicetype.networkdevicetype
|
||||
if self.vendor:
|
||||
result['vendor_name'] = self.vendor.vendor
|
||||
result['vendorname'] = self.vendor.vendor
|
||||
|
||||
return result
|
||||
|
||||
@@ -78,8 +78,8 @@ class NetworkPlugin(BasePlugin):
|
||||
if not existing:
|
||||
at = AssetType(
|
||||
assettype='network_device',
|
||||
plugin_name='network',
|
||||
table_name='networkdevices',
|
||||
pluginname='network',
|
||||
tablename='networkdevices',
|
||||
description='Network infrastructure devices (switches, APs, cameras, etc.)',
|
||||
icon='network-wired'
|
||||
)
|
||||
|
||||
@@ -374,7 +374,7 @@ def dashboard_summary():
|
||||
|
||||
return success_response({
|
||||
'active': total_active,
|
||||
'by_type': [{'type': t, 'color': c, 'count': n} for t, c, n in by_type]
|
||||
'bytype': [{'type': t, 'color': c, 'count': n} for t, c, n in by_type]
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ printers_asset_bp = Blueprint('printers_asset', __name__)
|
||||
# =============================================================================
|
||||
|
||||
@printers_asset_bp.route('/types', methods=['GET'])
|
||||
@jwt_required()
|
||||
@jwt_required(optional=True)
|
||||
def list_printer_types():
|
||||
"""List all printer types."""
|
||||
page, per_page = get_pagination_params(request)
|
||||
@@ -46,7 +46,7 @@ def list_printer_types():
|
||||
|
||||
|
||||
@printers_asset_bp.route('/types/<int:type_id>', methods=['GET'])
|
||||
@jwt_required()
|
||||
@jwt_required(optional=True)
|
||||
def get_printer_type(type_id: int):
|
||||
"""Get a single printer type."""
|
||||
t = PrinterType.query.get(type_id)
|
||||
@@ -471,6 +471,6 @@ def dashboard_summary():
|
||||
'online': total, # Placeholder - would need monitoring integration
|
||||
'lowsupplies': 0, # Placeholder - would need Zabbix integration
|
||||
'criticalsupplies': 0, # Placeholder - would need Zabbix integration
|
||||
'by_type': [{'type': t, 'count': c} for t, c in by_type],
|
||||
'by_vendor': [{'vendor': v, 'count': c} for v, c in by_vendor],
|
||||
'bytype': [{'type': t, 'count': c} for t, c in by_type],
|
||||
'byvendor': [{'vendor': v, 'count': c} for v, c in by_vendor],
|
||||
})
|
||||
|
||||
@@ -113,10 +113,10 @@ class Printer(BaseModel):
|
||||
|
||||
# Add related object names
|
||||
if self.printertype:
|
||||
result['printertype_name'] = self.printertype.printertype
|
||||
result['printertypename'] = self.printertype.printertype
|
||||
if self.vendor:
|
||||
result['vendor_name'] = self.vendor.vendor
|
||||
result['vendorname'] = self.vendor.vendor
|
||||
if self.model:
|
||||
result['model_name'] = self.model.modelnumber
|
||||
result['modelname'] = self.model.modelnumber
|
||||
|
||||
return result
|
||||
|
||||
@@ -116,8 +116,8 @@ class PrintersPlugin(BasePlugin):
|
||||
if not existing:
|
||||
at = AssetType(
|
||||
assettype='printer',
|
||||
plugin_name='printers',
|
||||
table_name='printers',
|
||||
pluginname='printers',
|
||||
tablename='printers',
|
||||
description='Printers (laser, inkjet, label, MFP, plotter)',
|
||||
icon='printer'
|
||||
)
|
||||
|
||||
@@ -161,10 +161,10 @@ def get_usb_device(device_id: int):
|
||||
# Get recent checkout history
|
||||
checkouts = USBCheckout.query.filter_by(
|
||||
usbdeviceid=device_id
|
||||
).order_by(USBCheckout.checkout_time.desc()).limit(20).all()
|
||||
).order_by(USBCheckout.checkouttime.desc()).limit(20).all()
|
||||
|
||||
result = device.to_dict()
|
||||
result['checkout_history'] = [c.to_dict() for c in checkouts]
|
||||
result['checkouthistory'] = [c.to_dict() for c in checkouts]
|
||||
|
||||
return success_response(result)
|
||||
|
||||
@@ -258,16 +258,16 @@ def checkout_device(device_id: int):
|
||||
usbdeviceid=device_id,
|
||||
machineid=0, # Legacy field, set to 0 for new checkouts
|
||||
sso=data['sso'],
|
||||
checkout_name=data.get('checkout_name'),
|
||||
checkout_time=datetime.utcnow(),
|
||||
checkout_reason=data.get('checkout_reason'),
|
||||
was_wiped=False
|
||||
checkoutname=data.get('checkoutname'),
|
||||
checkouttime=datetime.utcnow(),
|
||||
checkoutreason=data.get('checkoutreason'),
|
||||
waswiped=False
|
||||
)
|
||||
|
||||
# Update device status
|
||||
device.ischeckedout = True
|
||||
device.currentuserid = data['sso']
|
||||
device.currentusername = data.get('checkout_name')
|
||||
device.currentusername = data.get('checkoutname')
|
||||
device.currentcheckoutdate = datetime.utcnow()
|
||||
device.modifieddate = datetime.utcnow()
|
||||
|
||||
@@ -300,15 +300,15 @@ def checkin_device(device_id: int):
|
||||
# Find active checkout
|
||||
active_checkout = USBCheckout.query.filter_by(
|
||||
usbdeviceid=device_id,
|
||||
checkin_time=None
|
||||
checkintime=None
|
||||
).first()
|
||||
|
||||
data = request.get_json() or {}
|
||||
|
||||
if active_checkout:
|
||||
active_checkout.checkin_time = datetime.utcnow()
|
||||
active_checkout.checkin_notes = data.get('checkin_notes', active_checkout.checkin_notes)
|
||||
active_checkout.was_wiped = data.get('was_wiped', False)
|
||||
active_checkout.checkintime = datetime.utcnow()
|
||||
active_checkout.checkinnotes = data.get('checkinnotes', active_checkout.checkinnotes)
|
||||
active_checkout.waswiped = data.get('waswiped', False)
|
||||
|
||||
# Update device status
|
||||
device.ischeckedout = False
|
||||
@@ -337,7 +337,7 @@ def get_device_history(device_id: int):
|
||||
|
||||
query = USBCheckout.query.filter_by(
|
||||
usbdeviceid=device_id
|
||||
).order_by(USBCheckout.checkout_time.desc())
|
||||
).order_by(USBCheckout.checkouttime.desc())
|
||||
|
||||
items, total = paginate_query(query, page, per_page)
|
||||
data = [c.to_dict() for c in items]
|
||||
@@ -361,13 +361,13 @@ def list_all_checkouts():
|
||||
|
||||
# Filter by active only
|
||||
if request.args.get('active', '').lower() == 'true':
|
||||
query = query.filter(USBCheckout.checkin_time == None)
|
||||
query = query.filter(USBCheckout.checkintime == None)
|
||||
|
||||
# Filter by user
|
||||
if sso := request.args.get('sso'):
|
||||
query = query.filter(USBCheckout.sso == sso)
|
||||
|
||||
query = query.order_by(USBCheckout.checkout_time.desc())
|
||||
query = query.order_by(USBCheckout.checkouttime.desc())
|
||||
|
||||
items, total = paginate_query(query, page, per_page)
|
||||
data = [c.to_dict() for c in items]
|
||||
@@ -380,7 +380,7 @@ def list_all_checkouts():
|
||||
def list_active_checkouts():
|
||||
"""List all currently active checkouts."""
|
||||
checkouts = USBCheckout.query.filter(
|
||||
USBCheckout.checkin_time == None
|
||||
).order_by(USBCheckout.checkout_time.desc()).all()
|
||||
USBCheckout.checkintime == None
|
||||
).order_by(USBCheckout.checkouttime.desc()).all()
|
||||
|
||||
return success_response([c.to_dict() for c in checkouts])
|
||||
|
||||
@@ -123,16 +123,16 @@ class USBCheckout(BaseModel):
|
||||
|
||||
# User info
|
||||
sso = db.Column(db.String(20), nullable=False, comment='SSO of user')
|
||||
checkout_name = db.Column(db.String(100), nullable=True, comment='Name of user')
|
||||
checkoutname = db.Column(db.String(100), nullable=True, comment='Name of user')
|
||||
|
||||
# Checkout details
|
||||
checkout_time = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
|
||||
checkin_time = db.Column(db.DateTime, nullable=True)
|
||||
checkouttime = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
|
||||
checkintime = db.Column(db.DateTime, nullable=True)
|
||||
|
||||
# Metadata
|
||||
checkout_reason = db.Column(db.Text, nullable=True, comment='Reason for checkout')
|
||||
checkin_notes = db.Column(db.Text, nullable=True)
|
||||
was_wiped = db.Column(db.Boolean, nullable=True, comment='Was device wiped after return')
|
||||
checkoutreason = db.Column(db.Text, nullable=True, comment='Reason for checkout')
|
||||
checkinnotes = db.Column(db.Text, nullable=True)
|
||||
waswiped = db.Column(db.Boolean, nullable=True, comment='Was device wiped after return')
|
||||
|
||||
# Relationships
|
||||
device = db.relationship('USBDevice', backref=db.backref('checkouts', lazy='dynamic'))
|
||||
@@ -143,13 +143,13 @@ class USBCheckout(BaseModel):
|
||||
@property
|
||||
def is_active(self):
|
||||
"""Check if this checkout is currently active (not returned)."""
|
||||
return self.checkin_time is None
|
||||
return self.checkintime is None
|
||||
|
||||
@property
|
||||
def duration_days(self):
|
||||
"""Get duration of checkout in days."""
|
||||
end = self.checkin_time or datetime.utcnow()
|
||||
delta = end - self.checkout_time
|
||||
end = self.checkintime or datetime.utcnow()
|
||||
delta = end - self.checkouttime
|
||||
return delta.days
|
||||
|
||||
def to_dict(self):
|
||||
|
||||
@@ -22,7 +22,7 @@ assets_bp = Blueprint('assets', __name__)
|
||||
# =============================================================================
|
||||
|
||||
@assets_bp.route('/types', methods=['GET'])
|
||||
@jwt_required()
|
||||
@jwt_required(optional=True)
|
||||
def list_asset_types():
|
||||
"""List all asset types."""
|
||||
page, per_page = get_pagination_params(request)
|
||||
@@ -41,7 +41,7 @@ def list_asset_types():
|
||||
|
||||
|
||||
@assets_bp.route('/types/<int:type_id>', methods=['GET'])
|
||||
@jwt_required()
|
||||
@jwt_required(optional=True)
|
||||
def get_asset_type(type_id: int):
|
||||
"""Get a single asset type."""
|
||||
t = AssetType.query.get(type_id)
|
||||
@@ -91,7 +91,7 @@ def create_asset_type():
|
||||
# =============================================================================
|
||||
|
||||
@assets_bp.route('/statuses', methods=['GET'])
|
||||
@jwt_required()
|
||||
@jwt_required(optional=True)
|
||||
def list_asset_statuses():
|
||||
"""List all asset statuses."""
|
||||
page, per_page = get_pagination_params(request)
|
||||
@@ -110,7 +110,7 @@ def list_asset_statuses():
|
||||
|
||||
|
||||
@assets_bp.route('/statuses/<int:status_id>', methods=['GET'])
|
||||
@jwt_required()
|
||||
@jwt_required(optional=True)
|
||||
def get_asset_status(status_id: int):
|
||||
"""Get a single asset status."""
|
||||
s = AssetStatus.query.get(status_id)
|
||||
@@ -158,7 +158,7 @@ def create_asset_status():
|
||||
# =============================================================================
|
||||
|
||||
@assets_bp.route('', methods=['GET'])
|
||||
@jwt_required()
|
||||
@jwt_required(optional=True)
|
||||
def list_assets():
|
||||
"""
|
||||
List all assets with filtering and pagination.
|
||||
@@ -240,7 +240,7 @@ def list_assets():
|
||||
|
||||
|
||||
@assets_bp.route('/<int:asset_id>', methods=['GET'])
|
||||
@jwt_required()
|
||||
@jwt_required(optional=True)
|
||||
def get_asset(asset_id: int):
|
||||
"""
|
||||
Get a single asset with full details.
|
||||
@@ -370,7 +370,7 @@ def delete_asset(asset_id: int):
|
||||
|
||||
|
||||
@assets_bp.route('/lookup/<assetnumber>', methods=['GET'])
|
||||
@jwt_required()
|
||||
@jwt_required(optional=True)
|
||||
def lookup_asset_by_number(assetnumber: str):
|
||||
"""
|
||||
Look up an asset by its asset number.
|
||||
@@ -798,7 +798,7 @@ def get_assets_map():
|
||||
|
||||
|
||||
@assets_bp.route('/<int:asset_id>/communications', methods=['GET'])
|
||||
@jwt_required()
|
||||
@jwt_required(optional=True)
|
||||
def get_asset_communications(asset_id: int):
|
||||
"""Get all communications for an asset."""
|
||||
from shopdb.core.models import Communication
|
||||
|
||||
@@ -276,7 +276,7 @@ def pc_heartbeat():
|
||||
|
||||
return success_response({
|
||||
'updated': updated,
|
||||
'not_found': not_found,
|
||||
'notfound': not_found,
|
||||
'timestamp': datetime.utcnow().isoformat()
|
||||
}, message=f'{updated} PC(s) heartbeat recorded')
|
||||
|
||||
@@ -351,7 +351,7 @@ def bulk_update():
|
||||
|
||||
return success_response({
|
||||
'updated': updated,
|
||||
'not_found': not_found,
|
||||
'notfound': not_found,
|
||||
'errors': errors,
|
||||
'timestamp': datetime.utcnow().isoformat()
|
||||
}, message=f'{updated} PC(s) updated')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Dashboard API endpoints."""
|
||||
|
||||
from flask import Blueprint
|
||||
from flask import Blueprint, current_app
|
||||
from flask_jwt_extended import jwt_required
|
||||
|
||||
from shopdb.extensions import db
|
||||
@@ -60,10 +60,10 @@ def get_dashboard():
|
||||
'counts': {
|
||||
'equipment': equipment_count,
|
||||
'pcs': pc_count,
|
||||
'network_devices': network_count,
|
||||
'networkdevices': network_count,
|
||||
'total': equipment_count + pc_count + network_count
|
||||
},
|
||||
'by_status': status_dict,
|
||||
'bystatus': status_dict,
|
||||
'recent': [
|
||||
{
|
||||
'machineid': m.machineid,
|
||||
@@ -93,13 +93,51 @@ def get_stats():
|
||||
).all()
|
||||
|
||||
return success_response({
|
||||
'by_type': [
|
||||
'bytype': [
|
||||
{'type': t, 'category': c, 'count': count}
|
||||
for t, c, count in type_counts
|
||||
]
|
||||
})
|
||||
|
||||
|
||||
@dashboard_bp.route('/navigation', methods=['GET'])
|
||||
def get_navigation():
|
||||
"""Get navigation items from all loaded plugins."""
|
||||
pm = current_app.extensions.get('plugin_manager')
|
||||
if not pm:
|
||||
return success_response([])
|
||||
|
||||
all_items = []
|
||||
|
||||
# Core navigation items (always present)
|
||||
all_items.extend([
|
||||
{'name': 'Dashboard', 'icon': 'layout-dashboard', 'route': '/', 'position': 0},
|
||||
{'name': 'Map', 'icon': 'map', 'route': '/map', 'position': 4},
|
||||
])
|
||||
|
||||
# Collect navigation items from all plugins
|
||||
for name, plugin in pm.get_all_plugins().items():
|
||||
try:
|
||||
items = plugin.get_navigation_items()
|
||||
for item in items:
|
||||
item['plugin'] = name
|
||||
all_items.extend(items)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Add core information section items
|
||||
all_items.extend([
|
||||
{'name': 'Applications', 'icon': 'app-window', 'route': '/applications', 'position': 30, 'section': 'information'},
|
||||
{'name': 'Knowledge Base', 'icon': 'book-open', 'route': '/knowledgebase', 'position': 35, 'section': 'information'},
|
||||
{'name': 'Reports', 'icon': 'bar-chart-3', 'route': '/reports', 'position': 40, 'section': 'information'},
|
||||
])
|
||||
|
||||
# Sort by position
|
||||
all_items.sort(key=lambda x: x.get('position', 99))
|
||||
|
||||
return success_response(all_items)
|
||||
|
||||
|
||||
@dashboard_bp.route('/health', methods=['GET'])
|
||||
def health_check():
|
||||
"""Health check endpoint (no auth required)."""
|
||||
|
||||
@@ -17,7 +17,7 @@ locations_bp = Blueprint('locations', __name__)
|
||||
|
||||
|
||||
@locations_bp.route('', methods=['GET'])
|
||||
@jwt_required()
|
||||
@jwt_required(optional=True)
|
||||
def list_locations():
|
||||
"""List all locations."""
|
||||
page, per_page = get_pagination_params(request)
|
||||
@@ -44,7 +44,7 @@ def list_locations():
|
||||
|
||||
|
||||
@locations_bp.route('/<int:location_id>', methods=['GET'])
|
||||
@jwt_required()
|
||||
@jwt_required(optional=True)
|
||||
def get_location(location_id: int):
|
||||
"""Get a single location."""
|
||||
loc = Location.query.get(location_id)
|
||||
|
||||
@@ -337,7 +337,7 @@ def delete_machine(machine_id: int):
|
||||
|
||||
|
||||
@machines_bp.route('/<int:machine_id>/communications', methods=['GET'])
|
||||
@jwt_required()
|
||||
@jwt_required(optional=True)
|
||||
@add_deprecation_headers
|
||||
def get_machine_communications(machine_id: int):
|
||||
"""Get all communications for a machine."""
|
||||
|
||||
@@ -51,7 +51,7 @@ def list_models():
|
||||
|
||||
|
||||
@models_bp.route('/<int:model_id>', methods=['GET'])
|
||||
@jwt_required()
|
||||
@jwt_required(optional=True)
|
||||
def get_model(model_id: int):
|
||||
"""Get a single model."""
|
||||
m = Model.query.get(model_id)
|
||||
|
||||
@@ -17,7 +17,7 @@ operatingsystems_bp = Blueprint('operatingsystems', __name__)
|
||||
|
||||
|
||||
@operatingsystems_bp.route('', methods=['GET'])
|
||||
@jwt_required()
|
||||
@jwt_required(optional=True)
|
||||
def list_operatingsystems():
|
||||
"""List all operating systems."""
|
||||
page, per_page = get_pagination_params(request)
|
||||
@@ -39,7 +39,7 @@ def list_operatingsystems():
|
||||
|
||||
|
||||
@operatingsystems_bp.route('/<int:os_id>', methods=['GET'])
|
||||
@jwt_required()
|
||||
@jwt_required(optional=True)
|
||||
def get_operatingsystem(os_id: int):
|
||||
"""Get a single operating system."""
|
||||
os = OperatingSystem.query.get(os_id)
|
||||
|
||||
@@ -17,7 +17,7 @@ pctypes_bp = Blueprint('pctypes', __name__)
|
||||
|
||||
|
||||
@pctypes_bp.route('', methods=['GET'])
|
||||
@jwt_required()
|
||||
@jwt_required(optional=True)
|
||||
def list_pctypes():
|
||||
"""List all PC types."""
|
||||
page, per_page = get_pagination_params(request)
|
||||
@@ -39,7 +39,7 @@ def list_pctypes():
|
||||
|
||||
|
||||
@pctypes_bp.route('/<int:type_id>', methods=['GET'])
|
||||
@jwt_required()
|
||||
@jwt_required(optional=True)
|
||||
def get_pctype(type_id: int):
|
||||
"""Get a single PC type."""
|
||||
pt = PCType.query.get(type_id)
|
||||
|
||||
@@ -31,7 +31,7 @@ def generate_csv(data: list, columns: list) -> str:
|
||||
# =============================================================================
|
||||
|
||||
@reports_bp.route('/equipment-by-type', methods=['GET'])
|
||||
@jwt_required()
|
||||
@jwt_required(optional=True)
|
||||
def equipment_by_type():
|
||||
"""
|
||||
Report: Equipment count grouped by equipment type.
|
||||
@@ -95,7 +95,7 @@ def equipment_by_type():
|
||||
# =============================================================================
|
||||
|
||||
@reports_bp.route('/assets-by-status', methods=['GET'])
|
||||
@jwt_required()
|
||||
@jwt_required(optional=True)
|
||||
def assets_by_status():
|
||||
"""
|
||||
Report: Asset count grouped by status.
|
||||
@@ -154,7 +154,7 @@ def assets_by_status():
|
||||
# =============================================================================
|
||||
|
||||
@reports_bp.route('/kb-popularity', methods=['GET'])
|
||||
@jwt_required()
|
||||
@jwt_required(optional=True)
|
||||
def kb_popularity():
|
||||
"""
|
||||
Report: Most clicked knowledge base articles.
|
||||
@@ -202,7 +202,7 @@ def kb_popularity():
|
||||
# =============================================================================
|
||||
|
||||
@reports_bp.route('/warranty-status', methods=['GET'])
|
||||
@jwt_required()
|
||||
@jwt_required(optional=True)
|
||||
def warranty_status():
|
||||
"""
|
||||
Report: Assets by warranty expiration status.
|
||||
@@ -305,7 +305,7 @@ def warranty_status():
|
||||
# =============================================================================
|
||||
|
||||
@reports_bp.route('/software-compliance', methods=['GET'])
|
||||
@jwt_required()
|
||||
@jwt_required(optional=True)
|
||||
def software_compliance():
|
||||
"""
|
||||
Report: Required applications vs installed (per PC).
|
||||
@@ -409,7 +409,7 @@ def software_compliance():
|
||||
# =============================================================================
|
||||
|
||||
@reports_bp.route('/asset-inventory', methods=['GET'])
|
||||
@jwt_required()
|
||||
@jwt_required(optional=True)
|
||||
def asset_inventory():
|
||||
"""
|
||||
Report: Complete asset inventory summary.
|
||||
@@ -518,8 +518,68 @@ def asset_inventory():
|
||||
# Available Reports List
|
||||
# =============================================================================
|
||||
|
||||
# =============================================================================
|
||||
# Report: PC-Machine Relationships
|
||||
# =============================================================================
|
||||
|
||||
@reports_bp.route('/pc-relationships', methods=['GET'])
|
||||
@jwt_required(optional=True)
|
||||
def pc_relationships():
|
||||
"""
|
||||
Report: PCs with relationships to shop floor machines.
|
||||
|
||||
Returns machine number, vendor, model, PC hostname, and PC IP address
|
||||
for all active PC-equipment relationships.
|
||||
|
||||
Query parameters:
|
||||
- format: 'json' (default) or 'csv'
|
||||
"""
|
||||
sql = db.text("""
|
||||
SELECT
|
||||
eq.machinenumber AS machine_number,
|
||||
v.vendor AS vendor,
|
||||
mo.modelnumber AS model,
|
||||
pc.machinenumber AS hostname,
|
||||
c.ipaddress AS ip
|
||||
FROM machinerelationships mr
|
||||
JOIN machines eq ON mr.parentmachineid = eq.machineid
|
||||
JOIN machines pc ON mr.childmachineid = pc.machineid
|
||||
LEFT JOIN communications c ON pc.machineid = c.machineid AND c.isprimary = 1 AND c.comtypeid = 1
|
||||
LEFT JOIN models mo ON eq.modelnumberid = mo.modelnumberid
|
||||
LEFT JOIN vendors v ON mo.vendorid = v.vendorid
|
||||
WHERE mr.isactive = 1
|
||||
AND pc.pctypeid IS NOT NULL
|
||||
AND eq.machinenumber IS NOT NULL AND eq.machinenumber != ''
|
||||
ORDER BY eq.machinenumber
|
||||
""")
|
||||
|
||||
results = db.session.execute(sql).fetchall()
|
||||
data = [{
|
||||
'machine_number': r.machine_number,
|
||||
'vendor': r.vendor or '',
|
||||
'model': r.model or '',
|
||||
'hostname': r.hostname or '',
|
||||
'ip': r.ip or ''
|
||||
} for r in results]
|
||||
|
||||
if request.args.get('format') == 'csv':
|
||||
csv_data = generate_csv(data, ['machine_number', 'vendor', 'model', 'hostname', 'ip'])
|
||||
return Response(
|
||||
csv_data,
|
||||
mimetype='text/csv',
|
||||
headers={'Content-Disposition': 'attachment; filename=pc_relationships.csv'}
|
||||
)
|
||||
|
||||
return success_response({
|
||||
'report': 'pc_relationships',
|
||||
'generated': datetime.utcnow().isoformat(),
|
||||
'data': data,
|
||||
'total': len(data)
|
||||
})
|
||||
|
||||
|
||||
@reports_bp.route('', methods=['GET'])
|
||||
@jwt_required()
|
||||
@jwt_required(optional=True)
|
||||
def list_reports():
|
||||
"""List all available reports."""
|
||||
reports = [
|
||||
@@ -564,6 +624,13 @@ def list_reports():
|
||||
'description': 'Complete asset inventory breakdown',
|
||||
'endpoint': '/api/reports/asset-inventory',
|
||||
'category': 'inventory'
|
||||
},
|
||||
{
|
||||
'id': 'pc-relationships',
|
||||
'name': 'PC-Machine Relationships',
|
||||
'description': 'PCs with relationships to shop floor machines',
|
||||
'endpoint': '/api/reports/pc-relationships',
|
||||
'category': 'inventory'
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ vendors_bp = Blueprint('vendors', __name__)
|
||||
|
||||
|
||||
@vendors_bp.route('', methods=['GET'])
|
||||
@jwt_required()
|
||||
@jwt_required(optional=True)
|
||||
def list_vendors():
|
||||
"""List all vendors."""
|
||||
page, per_page = get_pagination_params(request)
|
||||
@@ -39,7 +39,7 @@ def list_vendors():
|
||||
|
||||
|
||||
@vendors_bp.route('/<int:vendor_id>', methods=['GET'])
|
||||
@jwt_required()
|
||||
@jwt_required(optional=True)
|
||||
def get_vendor(vendor_id: int):
|
||||
"""Get a single vendor."""
|
||||
v = Vendor.query.get(vendor_id)
|
||||
|
||||
@@ -20,12 +20,12 @@ class AssetType(BaseModel):
|
||||
nullable=False,
|
||||
comment='Category name: equipment, computer, network_device, printer'
|
||||
)
|
||||
plugin_name = db.Column(
|
||||
pluginname = db.Column(
|
||||
db.String(100),
|
||||
nullable=True,
|
||||
comment='Plugin that owns this type'
|
||||
)
|
||||
table_name = db.Column(
|
||||
tablename = db.Column(
|
||||
db.String(100),
|
||||
nullable=True,
|
||||
comment='Extension table name for this type'
|
||||
@@ -174,23 +174,23 @@ class Asset(BaseModel, SoftDeleteMixin, AuditMixin):
|
||||
|
||||
if hasattr(self, 'incoming_relationships'):
|
||||
for rel in self.incoming_relationships:
|
||||
if rel.source_asset and rel.isactive:
|
||||
related_assets.append(rel.source_asset)
|
||||
if rel.sourceasset and rel.isactive:
|
||||
related_assets.append(rel.sourceasset)
|
||||
|
||||
if hasattr(self, 'outgoing_relationships'):
|
||||
for rel in self.outgoing_relationships:
|
||||
if rel.target_asset and rel.isactive:
|
||||
related_assets.append(rel.target_asset)
|
||||
if rel.targetasset and rel.isactive:
|
||||
related_assets.append(rel.targetasset)
|
||||
|
||||
# Find first related asset with location data
|
||||
for related in related_assets:
|
||||
if related.locationid is not None or (related.mapleft is not None and related.maptop is not None):
|
||||
return {
|
||||
'locationid': related.locationid,
|
||||
'location_name': related.location.locationname if related.location else None,
|
||||
'locationname': related.location.locationname if related.location else None,
|
||||
'mapleft': related.mapleft,
|
||||
'maptop': related.maptop,
|
||||
'inherited_from': related.assetnumber
|
||||
'inheritedfrom': related.assetnumber
|
||||
}
|
||||
|
||||
return None
|
||||
@@ -207,33 +207,33 @@ class Asset(BaseModel, SoftDeleteMixin, AuditMixin):
|
||||
|
||||
# Add related object names for convenience
|
||||
if self.assettype:
|
||||
result['assettype_name'] = self.assettype.assettype
|
||||
result['assettypename'] = self.assettype.assettype
|
||||
if self.status:
|
||||
result['status_name'] = self.status.status
|
||||
result['statusname'] = self.status.status
|
||||
if self.location:
|
||||
result['location_name'] = self.location.locationname
|
||||
result['locationname'] = self.location.locationname
|
||||
if self.businessunit:
|
||||
result['businessunit_name'] = self.businessunit.businessunit
|
||||
result['businessunitname'] = self.businessunit.businessunit
|
||||
|
||||
# Add plugin-specific ID for navigation purposes
|
||||
if hasattr(self, 'equipment') and self.equipment:
|
||||
result['plugin_id'] = self.equipment.equipmentid
|
||||
result['pluginid'] = self.equipment.equipmentid
|
||||
elif hasattr(self, 'computer') and self.computer:
|
||||
result['plugin_id'] = self.computer.computerid
|
||||
result['pluginid'] = self.computer.computerid
|
||||
elif hasattr(self, 'network_device') and self.network_device:
|
||||
result['plugin_id'] = self.network_device.networkdeviceid
|
||||
result['pluginid'] = self.network_device.networkdeviceid
|
||||
elif hasattr(self, 'printer') and self.printer:
|
||||
result['plugin_id'] = self.printer.printerid
|
||||
result['pluginid'] = self.printer.printerid
|
||||
|
||||
# Include inherited location if this asset has no location data
|
||||
if include_inherited_location:
|
||||
inherited = self.get_inherited_location()
|
||||
if inherited:
|
||||
result['inherited_location'] = inherited
|
||||
result['inheritedlocation'] = inherited
|
||||
# Also set the location fields if they're missing
|
||||
if result.get('locationid') is None:
|
||||
result['locationid'] = inherited['locationid']
|
||||
result['location_name'] = inherited['location_name']
|
||||
result['locationname'] = inherited['locationname']
|
||||
if result.get('mapleft') is None:
|
||||
result['mapleft'] = inherited['mapleft']
|
||||
if result.get('maptop') is None:
|
||||
@@ -243,7 +243,7 @@ class Asset(BaseModel, SoftDeleteMixin, AuditMixin):
|
||||
if include_type_data:
|
||||
ext_data = self._get_extension_data()
|
||||
if ext_data:
|
||||
result['type_data'] = ext_data
|
||||
result['typedata'] = ext_data
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@@ -34,12 +34,12 @@ class AssetRelationship(BaseModel):
|
||||
|
||||
relationshipid = db.Column(db.Integer, primary_key=True)
|
||||
|
||||
source_assetid = db.Column(
|
||||
sourceassetid = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey('assets.assetid'),
|
||||
nullable=False
|
||||
)
|
||||
target_assetid = db.Column(
|
||||
targetassetid = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey('assets.assetid'),
|
||||
nullable=False
|
||||
@@ -53,31 +53,31 @@ class AssetRelationship(BaseModel):
|
||||
notes = db.Column(db.Text)
|
||||
|
||||
# Relationships
|
||||
source_asset = db.relationship(
|
||||
sourceasset = db.relationship(
|
||||
'Asset',
|
||||
foreign_keys=[source_assetid],
|
||||
foreign_keys=[sourceassetid],
|
||||
backref='outgoing_relationships'
|
||||
)
|
||||
target_asset = db.relationship(
|
||||
targetasset = db.relationship(
|
||||
'Asset',
|
||||
foreign_keys=[target_assetid],
|
||||
foreign_keys=[targetassetid],
|
||||
backref='incoming_relationships'
|
||||
)
|
||||
relationship_type = db.relationship('RelationshipType', backref='asset_relationships')
|
||||
relationshiptype = db.relationship('RelationshipType', backref='asset_relationships')
|
||||
|
||||
__table_args__ = (
|
||||
db.UniqueConstraint(
|
||||
'source_assetid',
|
||||
'target_assetid',
|
||||
'sourceassetid',
|
||||
'targetassetid',
|
||||
'relationshiptypeid',
|
||||
name='uq_asset_relationship'
|
||||
),
|
||||
db.Index('idx_asset_rel_source', 'source_assetid'),
|
||||
db.Index('idx_asset_rel_target', 'target_assetid'),
|
||||
db.Index('idx_asset_rel_source', 'sourceassetid'),
|
||||
db.Index('idx_asset_rel_target', 'targetassetid'),
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<AssetRelationship {self.source_assetid} -> {self.target_assetid}>"
|
||||
return f"<AssetRelationship {self.sourceassetid} -> {self.targetassetid}>"
|
||||
|
||||
|
||||
class MachineRelationship(BaseModel):
|
||||
|
||||
@@ -109,7 +109,7 @@ class PluginManager:
|
||||
'dependencies': meta.dependencies,
|
||||
'installed': state is not None,
|
||||
'enabled': state.enabled if state else False,
|
||||
'installed_at': state.installed_at if state else None
|
||||
'installedat': state.installed_at if state else None
|
||||
})
|
||||
except Exception as e:
|
||||
logger.warning(f"Error inspecting plugin {name}: {e}")
|
||||
|
||||
@@ -23,7 +23,7 @@ def get_pagination_params(req=None) -> Tuple[int, int]:
|
||||
page = 1
|
||||
|
||||
try:
|
||||
per_page = int(req.args.get('per_page', default_size))
|
||||
per_page = int(req.args.get('perpage', req.args.get('per_page', default_size)))
|
||||
per_page = max(1, min(per_page, max_size))
|
||||
except (TypeError, ValueError):
|
||||
per_page = default_size
|
||||
|
||||
@@ -44,7 +44,7 @@ def api_response(
|
||||
'status': status,
|
||||
'meta': {
|
||||
'timestamp': datetime.utcnow().isoformat() + 'Z',
|
||||
'request_id': str(uuid.uuid4())[:8],
|
||||
'requestid': str(uuid.uuid4())[:8],
|
||||
**(meta or {})
|
||||
}
|
||||
}
|
||||
@@ -161,11 +161,11 @@ def paginated_response(
|
||||
meta={
|
||||
'pagination': {
|
||||
'page': page,
|
||||
'per_page': per_page,
|
||||
'perpage': per_page,
|
||||
'total': total,
|
||||
'total_pages': total_pages,
|
||||
'has_next': page < total_pages,
|
||||
'has_prev': page > 1
|
||||
'totalpages': total_pages,
|
||||
'hasnext': page < total_pages,
|
||||
'hasprev': page > 1
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user