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/daygrid": "^6.1.20",
|
||||||
"@fullcalendar/vue3": "^6.1.20",
|
"@fullcalendar/vue3": "^6.1.20",
|
||||||
"axios": "^1.6.0",
|
"axios": "^1.6.0",
|
||||||
|
"jsbarcode": "^3.12.3",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
|
"lucide-vue-next": "^0.563.0",
|
||||||
"pinia": "^2.1.0",
|
"pinia": "^2.1.0",
|
||||||
|
"qrcode": "^1.5.4",
|
||||||
"vue": "^3.4.0",
|
"vue": "^3.4.0",
|
||||||
"vue-router": "^4.2.0"
|
"vue-router": "^4.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^5.0.0",
|
"@vitejs/plugin-vue": "^5.2.4",
|
||||||
"vite": "^5.0.0"
|
"vite": "^6.4.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,17 @@
|
|||||||
--sidebar-bg: #00003d;
|
--sidebar-bg: #00003d;
|
||||||
--sidebar-text: #ffffff;
|
--sidebar-text: #ffffff;
|
||||||
--sidebar-width: 250px;
|
--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 */
|
/* Dark Mode - via data-theme attribute or system preference fallback */
|
||||||
@@ -146,7 +157,7 @@ h1, h2, h3, h4, h5, h6 {
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
color: #ffffff;
|
color: var(--sidebar-text);
|
||||||
margin: 0.5rem 0 0 0;
|
margin: 0.5rem 0 0 0;
|
||||||
letter-spacing: 1px;
|
letter-spacing: 1px;
|
||||||
}
|
}
|
||||||
@@ -189,6 +200,7 @@ h1, h2, h3, h4, h5, h6 {
|
|||||||
.sidebar-nav a {
|
.sidebar-nav a {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
padding: 0.7rem 1.25rem;
|
padding: 0.7rem 1.25rem;
|
||||||
color: rgba(255,255,255,0.85);
|
color: rgba(255,255,255,0.85);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
@@ -196,6 +208,11 @@ h1, h2, h3, h4, h5, h6 {
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sidebar-nav a svg {
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.sidebar-nav a:hover,
|
.sidebar-nav a:hover,
|
||||||
.sidebar-nav a.active {
|
.sidebar-nav a.active {
|
||||||
background: rgba(255,255,255,0.1);
|
background: rgba(255,255,255,0.1);
|
||||||
@@ -431,8 +448,8 @@ tr:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btn-secondary:hover {
|
.btn-secondary:hover {
|
||||||
background: #82503f;
|
background: var(--secondary-dark);
|
||||||
border-color: #82503f;
|
border-color: var(--secondary-dark);
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -443,8 +460,8 @@ tr:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btn-success:hover {
|
.btn-success:hover {
|
||||||
background: #019e4c;
|
background: var(--success-dark);
|
||||||
border-color: #019e4c;
|
border-color: var(--success-dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-info {
|
.btn-info {
|
||||||
@@ -454,8 +471,8 @@ tr:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btn-info:hover {
|
.btn-info:hover {
|
||||||
background: #039ce0;
|
background: var(--info-dark);
|
||||||
border-color: #039ce0;
|
border-color: var(--info-dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-warning {
|
.btn-warning {
|
||||||
@@ -465,8 +482,8 @@ tr:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btn-warning:hover {
|
.btn-warning:hover {
|
||||||
background: #e67c02;
|
background: var(--warning-dark);
|
||||||
border-color: #e67c02;
|
border-color: var(--warning-dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-danger {
|
.btn-danger {
|
||||||
@@ -476,8 +493,8 @@ tr:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btn-danger:hover {
|
.btn-danger:hover {
|
||||||
background: #e62c51;
|
background: var(--danger-dark);
|
||||||
border-color: #e62c51;
|
border-color: var(--danger-dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-link {
|
.btn-link {
|
||||||
@@ -614,7 +631,7 @@ input[type="radio"] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
select.form-control option {
|
select.form-control option {
|
||||||
background: #1a1a2e;
|
background: var(--text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -690,7 +707,7 @@ input[type="radio"] {
|
|||||||
|
|
||||||
.error-message {
|
.error-message {
|
||||||
background: rgba(245, 54, 92, 0.2);
|
background: rgba(245, 54, 92, 0.2);
|
||||||
color: #f5365c;
|
color: var(--danger);
|
||||||
padding: 0.75rem 1rem;
|
padding: 0.75rem 1rem;
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
@@ -789,11 +806,35 @@ input[type="radio"] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Pagination */
|
/* 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 {
|
.pagination {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
margin-top: 1rem;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination button {
|
.pagination button {
|
||||||
@@ -806,8 +847,14 @@ input[type="radio"] {
|
|||||||
color: var(--text);
|
color: var(--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination button:hover {
|
.pagination button:hover:not(:disabled):not(.ellipsis) {
|
||||||
background: rgba(255, 255, 255, 0.1);
|
background: var(--bg);
|
||||||
|
border-color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination button:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination button.active {
|
.pagination button.active {
|
||||||
@@ -816,6 +863,12 @@ input[type="radio"] {
|
|||||||
border-color: var(--primary);
|
border-color: var(--primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pagination button.ellipsis {
|
||||||
|
border: none;
|
||||||
|
cursor: default;
|
||||||
|
padding: 0.5rem 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
/* Loading */
|
/* Loading */
|
||||||
.loading {
|
.loading {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -1260,7 +1313,7 @@ td.actions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.badge-printer {
|
.badge-printer {
|
||||||
background: #9c27b0;
|
background: var(--purple);
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1354,7 +1407,7 @@ td.actions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.fc .fc-popover {
|
.fc .fc-popover {
|
||||||
background: #111111;
|
background: var(--bg-popover);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 0.25rem;
|
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);
|
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 DARK THEME OVERRIDES
|
||||||
============================================ */
|
============================================ */
|
||||||
.leaflet-container {
|
.leaflet-container {
|
||||||
background: #111111;
|
background: var(--bg-popover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.leaflet-popup-content-wrapper {
|
.leaflet-popup-content-wrapper {
|
||||||
background: #111111;
|
background: var(--bg-popover);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
border-radius: 0.25rem;
|
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);
|
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 {
|
.leaflet-popup-tip {
|
||||||
background: #111111;
|
background: var(--bg-popover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.leaflet-popup-content {
|
.leaflet-popup-content {
|
||||||
|
|||||||
@@ -27,14 +27,14 @@
|
|||||||
:key="rel.relationshipid"
|
:key="rel.relationshipid"
|
||||||
class="relationship-item"
|
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">
|
<div class="rel-content">
|
||||||
<router-link :to="getAssetRoute(rel.target_asset)" class="rel-name">
|
<router-link :to="getAssetRoute(rel.targetasset)" class="rel-name">
|
||||||
{{ rel.target_asset?.name || rel.target_asset?.assetnumber || 'Unknown' }}
|
{{ rel.targetasset?.name || rel.targetasset?.assetnumber || 'Unknown' }}
|
||||||
</router-link>
|
</router-link>
|
||||||
<div class="rel-meta">
|
<div class="rel-meta">
|
||||||
<span class="badge badge-outline">{{ rel.relationship_type_name }}</span>
|
<span class="badge badge-outline">{{ rel.relationshiptypename }}</span>
|
||||||
<span class="rel-type-badge">{{ rel.target_asset?.assettype }}</span>
|
<span class="rel-type-badge">{{ rel.targetasset?.assettype }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="rel.notes" class="rel-notes">{{ rel.notes }}</div>
|
<div v-if="rel.notes" class="rel-notes">{{ rel.notes }}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -59,14 +59,14 @@
|
|||||||
:key="rel.relationshipid"
|
:key="rel.relationshipid"
|
||||||
class="relationship-item"
|
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">
|
<div class="rel-content">
|
||||||
<router-link :to="getAssetRoute(rel.source_asset)" class="rel-name">
|
<router-link :to="getAssetRoute(rel.sourceasset)" class="rel-name">
|
||||||
{{ rel.source_asset?.name || rel.source_asset?.assetnumber || 'Unknown' }}
|
{{ rel.sourceasset?.name || rel.sourceasset?.assetnumber || 'Unknown' }}
|
||||||
</router-link>
|
</router-link>
|
||||||
<div class="rel-meta">
|
<div class="rel-meta">
|
||||||
<span class="badge badge-outline">{{ rel.relationship_type_name }}</span>
|
<span class="badge badge-outline">{{ rel.relationshiptypename }}</span>
|
||||||
<span class="rel-type-badge">{{ rel.source_asset?.assettype }}</span>
|
<span class="rel-type-badge">{{ rel.sourceasset?.assettype }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="rel.notes" class="rel-notes">{{ rel.notes }}</div>
|
<div v-if="rel.notes" class="rel-notes">{{ rel.notes }}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -173,6 +173,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted, watch } from 'vue'
|
import { ref, computed, onMounted, watch } from 'vue'
|
||||||
|
import { Cog, Monitor, Printer, Globe, Package } from 'lucide-vue-next'
|
||||||
import { assetsApi, relationshipTypesApi } from '../api'
|
import { assetsApi, relationshipTypesApi } from '../api'
|
||||||
import { useAuthStore } from '../stores/auth'
|
import { useAuthStore } from '../stores/auth'
|
||||||
|
|
||||||
@@ -296,7 +297,7 @@ function searchAssets() {
|
|||||||
|
|
||||||
searchTimeout = setTimeout(async () => {
|
searchTimeout = setTimeout(async () => {
|
||||||
try {
|
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
|
// Filter out the current asset
|
||||||
assetSearchResults.value = (response.data.data || []).filter(
|
assetSearchResults.value = (response.data.data || []).filter(
|
||||||
a => a.assetid !== resolvedAssetId.value
|
a => a.assetid !== resolvedAssetId.value
|
||||||
@@ -329,11 +330,11 @@ async function saveRelationship() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (newRel.value.direction === 'outgoing') {
|
if (newRel.value.direction === 'outgoing') {
|
||||||
data.source_assetid = resolvedAssetId.value
|
data.sourceassetid = resolvedAssetId.value
|
||||||
data.target_assetid = newRel.value.targetAssetId
|
data.targetassetid = newRel.value.targetAssetId
|
||||||
} else {
|
} else {
|
||||||
data.source_assetid = newRel.value.targetAssetId
|
data.sourceassetid = newRel.value.targetAssetId
|
||||||
data.target_assetid = resolvedAssetId.value
|
data.targetassetid = resolvedAssetId.value
|
||||||
}
|
}
|
||||||
|
|
||||||
await assetsApi.createRelationship(data)
|
await assetsApi.createRelationship(data)
|
||||||
@@ -374,12 +375,12 @@ function closeModal() {
|
|||||||
|
|
||||||
function getAssetIcon(assettype) {
|
function getAssetIcon(assettype) {
|
||||||
const icons = {
|
const icons = {
|
||||||
'equipment': '⚙',
|
'equipment': Cog,
|
||||||
'computer': '💻',
|
'computer': Monitor,
|
||||||
'printer': '🖨',
|
'printer': Printer,
|
||||||
'network_device': '🌐'
|
'network_device': Globe
|
||||||
}
|
}
|
||||||
return icons[assettype] || '📦'
|
return icons[assettype] || Package
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAssetRoute(asset) {
|
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 { createRouter, createWebHistory } from 'vue-router'
|
||||||
import { useAuthStore } from '../stores/auth'
|
import { useAuthStore } from '../stores/auth'
|
||||||
|
|
||||||
// Views
|
|
||||||
import Login from '../views/Login.vue'
|
|
||||||
import AppLayout from '../views/AppLayout.vue'
|
import AppLayout from '../views/AppLayout.vue'
|
||||||
import Dashboard from '../views/Dashboard.vue'
|
|
||||||
import MachinesList from '../views/machines/MachinesList.vue'
|
// Auto-discover all route modules from routes/ directory
|
||||||
import MachineDetail from '../views/machines/MachineDetail.vue'
|
const routeModules = import.meta.glob('./routes/*.js', { eager: true })
|
||||||
import MachineForm from '../views/machines/MachineForm.vue'
|
const appChildren = Object.values(routeModules).flatMap(m => m.default)
|
||||||
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'
|
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
path: '/login',
|
path: '/login',
|
||||||
name: 'login',
|
name: 'login',
|
||||||
component: Login,
|
component: () => import('../views/Login.vue'),
|
||||||
meta: { guest: true }
|
meta: { guest: true }
|
||||||
},
|
},
|
||||||
// Standalone full-screen dashboards (no sidebar, no auth required)
|
// Standalone full-screen dashboards (no sidebar, no auth required)
|
||||||
{
|
{
|
||||||
path: '/shopfloor',
|
path: '/shopfloor',
|
||||||
name: 'shopfloor',
|
name: 'shopfloor',
|
||||||
component: ShopfloorDashboard
|
component: () => import('../views/ShopfloorDashboard.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/tv',
|
path: '/tv',
|
||||||
name: '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: '/',
|
path: '/',
|
||||||
component: AppLayout,
|
component: AppLayout,
|
||||||
children: [
|
children: appChildren
|
||||||
{
|
|
||||||
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
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
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>
|
</div>
|
||||||
|
|
||||||
<nav class="sidebar-nav">
|
<nav class="sidebar-nav">
|
||||||
<router-link to="/">Dashboard</router-link>
|
<template v-for="item in navItems" :key="item.route">
|
||||||
<router-link to="/calendar">Calendar</router-link>
|
<div v-if="item.section" class="nav-section">{{ item.section }}</div>
|
||||||
<router-link to="/map">Map</router-link>
|
<router-link :to="item.route">
|
||||||
|
<component v-if="item.iconComponent" :is="item.iconComponent" :size="16" />
|
||||||
<div class="nav-section">Assets</div>
|
{{ item.name }}
|
||||||
<router-link to="/machines">Equipment</router-link>
|
</router-link>
|
||||||
<router-link to="/pcs">PCs</router-link>
|
</template>
|
||||||
<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>
|
|
||||||
|
|
||||||
<div class="nav-section">Displays</div>
|
<div class="nav-section">Displays</div>
|
||||||
<a href="/shopfloor" target="_blank" class="external-link">Shopfloor Dashboard</a>
|
<a href="/shopfloor" target="_blank" class="external-link">Shopfloor Dashboard</a>
|
||||||
@@ -42,8 +33,8 @@
|
|||||||
|
|
||||||
<div class="sidebar-footer">
|
<div class="sidebar-footer">
|
||||||
<button class="theme-toggle" @click="toggleTheme" :title="currentTheme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'">
|
<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-if="currentTheme === 'dark'"><Sun :size="14" /> Light</span>
|
||||||
<span v-else>🌙 Dark</span>
|
<span v-else><Moon :size="14" /> Dark</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="user-menu">
|
<div class="user-menu">
|
||||||
@@ -63,15 +54,92 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
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 { useAuthStore } from '../stores/auth'
|
||||||
import { currentTheme, toggleTheme } from '../stores/theme'
|
import { currentTheme, toggleTheme } from '../stores/theme'
|
||||||
|
import { dashboardApi } from '../api'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
const searchQuery = ref('')
|
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() {
|
function performSearch() {
|
||||||
if (searchQuery.value.trim()) {
|
if (searchQuery.value.trim()) {
|
||||||
|
|||||||
@@ -32,13 +32,13 @@
|
|||||||
<!-- Recognition event with employee highlight -->
|
<!-- Recognition event with employee highlight -->
|
||||||
<div v-if="selectedEvent.extendedProps?.typecolor === 'recognition'" class="recognition-header">
|
<div v-if="selectedEvent.extendedProps?.typecolor === 'recognition'" class="recognition-header">
|
||||||
<div class="recognition-badge">
|
<div class="recognition-badge">
|
||||||
<span class="recognition-icon">🏆</span>
|
<span class="recognition-icon"><Trophy :size="24" /></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="recognition-info">
|
<div class="recognition-info">
|
||||||
<div class="recognition-label">Recognition</div>
|
<div class="recognition-label">Recognition</div>
|
||||||
<h2 class="recognition-title">{{ selectedEvent.extendedProps?.message || selectedEvent.title }}</h2>
|
<h2 class="recognition-title">{{ selectedEvent.extendedProps?.message || selectedEvent.title }}</h2>
|
||||||
<div v-if="selectedEvent.extendedProps?.employeename || selectedEvent.extendedProps?.employeesso" class="recognition-employee">
|
<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>
|
<span class="employee-name">{{ selectedEvent.extendedProps.employeename || selectedEvent.extendedProps.employeesso }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -88,6 +88,7 @@
|
|||||||
import { ref, onMounted, onUnmounted, nextTick } from 'vue'
|
import { ref, onMounted, onUnmounted, nextTick } from 'vue'
|
||||||
import FullCalendar from '@fullcalendar/vue3'
|
import FullCalendar from '@fullcalendar/vue3'
|
||||||
import dayGridPlugin from '@fullcalendar/daygrid'
|
import dayGridPlugin from '@fullcalendar/daygrid'
|
||||||
|
import { Trophy, User } from 'lucide-vue-next'
|
||||||
import { notificationsApi } from '@/api'
|
import { notificationsApi } from '@/api'
|
||||||
|
|
||||||
const events = ref([])
|
const events = ref([])
|
||||||
|
|||||||
@@ -40,12 +40,12 @@
|
|||||||
}"
|
}"
|
||||||
@click="selectAsset(asset)"
|
@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-info">
|
||||||
<div class="asset-name">{{ asset.name || asset.assetnumber }}</div>
|
<div class="asset-name">{{ asset.name || asset.assetnumber }}</div>
|
||||||
<div class="asset-meta">
|
<div class="asset-meta">
|
||||||
<span class="badge badge-sm">{{ asset.assettype }}</span>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -105,6 +105,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
import { Cog, Monitor, Printer, Globe, Package, MapPin } from 'lucide-vue-next'
|
||||||
import ShopFloorMap from '../components/ShopFloorMap.vue'
|
import ShopFloorMap from '../components/ShopFloorMap.vue'
|
||||||
import { assetsApi } from '../api'
|
import { assetsApi } from '../api'
|
||||||
import { currentTheme } from '../stores/theme'
|
import { currentTheme } from '../stores/theme'
|
||||||
@@ -152,12 +153,12 @@ async function loadAssets() {
|
|||||||
|
|
||||||
function getTypeIcon(assettype) {
|
function getTypeIcon(assettype) {
|
||||||
const icons = {
|
const icons = {
|
||||||
'equipment': '⚙',
|
'equipment': Cog,
|
||||||
'computer': '💻',
|
'computer': Monitor,
|
||||||
'printer': '🖨',
|
'printer': Printer,
|
||||||
'network_device': '🌐'
|
'network_device': Globe
|
||||||
}
|
}
|
||||||
return icons[assettype] || '📦'
|
return icons[assettype] || Package
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectAsset(asset) {
|
function selectAsset(asset) {
|
||||||
|
|||||||
@@ -85,24 +85,22 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Pagination -->
|
<!-- Pagination -->
|
||||||
<div class="pagination" v-if="totalPages > 1">
|
<PaginationBar
|
||||||
<button
|
:page="page"
|
||||||
v-for="p in visiblePages"
|
:totalPages="totalPages"
|
||||||
:key="p"
|
:perPage="perPage"
|
||||||
:class="{ active: p === page }"
|
@update:page="goToPage"
|
||||||
@click="goToPage(p)"
|
@update:perPage="changePerPage"
|
||||||
>
|
/>
|
||||||
{{ p }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { applicationsApi } from '../../api'
|
import { applicationsApi } from '../../api'
|
||||||
|
import PaginationBar from '../../components/PaginationBar.vue'
|
||||||
|
|
||||||
const applications = ref([])
|
const applications = ref([])
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
@@ -110,20 +108,10 @@ const search = ref('')
|
|||||||
const filter = ref('installable')
|
const filter = ref('installable')
|
||||||
const page = ref(1)
|
const page = ref(1)
|
||||||
const totalPages = ref(1)
|
const totalPages = ref(1)
|
||||||
const perPage = 20
|
const perPage = ref(20)
|
||||||
|
|
||||||
let searchTimeout = null
|
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(() => {
|
onMounted(() => {
|
||||||
loadApplications()
|
loadApplications()
|
||||||
})
|
})
|
||||||
@@ -133,7 +121,7 @@ async function loadApplications() {
|
|||||||
try {
|
try {
|
||||||
const params = {
|
const params = {
|
||||||
page: page.value,
|
page: page.value,
|
||||||
per_page: perPage,
|
perpage: perPage.value,
|
||||||
search: search.value || undefined
|
search: search.value || undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,7 +134,7 @@ async function loadApplications() {
|
|||||||
|
|
||||||
const response = await applicationsApi.list(params)
|
const response = await applicationsApi.list(params)
|
||||||
applications.value = response.data.data || []
|
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) {
|
} catch (error) {
|
||||||
console.error('Error loading applications:', error)
|
console.error('Error loading applications:', error)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -166,6 +154,12 @@ function goToPage(p) {
|
|||||||
page.value = p
|
page.value = p
|
||||||
loadApplications()
|
loadApplications()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function changePerPage(newPerPage) {
|
||||||
|
perPage.value = newPerPage
|
||||||
|
page.value = 1
|
||||||
|
loadApplications()
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ const applications = ref([])
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
// Load applications for topic dropdown
|
// 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 || []
|
applications.value = appsRes.data.data || []
|
||||||
|
|
||||||
// Load article if editing
|
// Load article if editing
|
||||||
|
|||||||
@@ -95,31 +95,29 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Pagination -->
|
<!-- Pagination -->
|
||||||
<div class="pagination" v-if="totalPages > 1">
|
<PaginationBar
|
||||||
<button
|
:page="page"
|
||||||
v-for="p in visiblePages"
|
:totalPages="totalPages"
|
||||||
:key="p"
|
:perPage="perPage"
|
||||||
:class="{ active: p === page }"
|
@update:page="goToPage"
|
||||||
@click="goToPage(p)"
|
@update:perPage="changePerPage"
|
||||||
>
|
/>
|
||||||
{{ p }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, computed } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { knowledgebaseApi, applicationsApi } from '../../api'
|
import { knowledgebaseApi, applicationsApi } from '../../api'
|
||||||
|
import PaginationBar from '../../components/PaginationBar.vue'
|
||||||
|
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const articles = ref([])
|
const articles = ref([])
|
||||||
const topics = ref([])
|
const topics = ref([])
|
||||||
const stats = ref(null)
|
const stats = ref(null)
|
||||||
const page = ref(1)
|
const page = ref(1)
|
||||||
const perPage = 20
|
const perPage = ref(20)
|
||||||
const totalPages = ref(1)
|
const totalPages = ref(1)
|
||||||
const search = ref('')
|
const search = ref('')
|
||||||
const topicFilter = ref('')
|
const topicFilter = ref('')
|
||||||
@@ -128,16 +126,6 @@ const order = ref('desc')
|
|||||||
|
|
||||||
let searchTimeout = null
|
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 () => {
|
onMounted(async () => {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
loadArticles(),
|
loadArticles(),
|
||||||
@@ -151,7 +139,7 @@ async function loadArticles() {
|
|||||||
try {
|
try {
|
||||||
const params = {
|
const params = {
|
||||||
page: page.value,
|
page: page.value,
|
||||||
per_page: perPage,
|
perpage: perPage.value,
|
||||||
sort: sort.value,
|
sort: sort.value,
|
||||||
order: order.value
|
order: order.value
|
||||||
}
|
}
|
||||||
@@ -160,7 +148,7 @@ async function loadArticles() {
|
|||||||
|
|
||||||
const response = await knowledgebaseApi.list(params)
|
const response = await knowledgebaseApi.list(params)
|
||||||
articles.value = response.data.data || []
|
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) {
|
} catch (error) {
|
||||||
console.error('Error loading articles:', error)
|
console.error('Error loading articles:', error)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -170,7 +158,7 @@ async function loadArticles() {
|
|||||||
|
|
||||||
async function loadTopics() {
|
async function loadTopics() {
|
||||||
try {
|
try {
|
||||||
const response = await applicationsApi.list({ per_page: 1000 })
|
const response = await applicationsApi.list({ perpage: 1000 })
|
||||||
topics.value = response.data.data || []
|
topics.value = response.data.data || []
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading topics:', error)
|
console.error('Error loading topics:', error)
|
||||||
@@ -199,6 +187,12 @@ function goToPage(p) {
|
|||||||
loadArticles()
|
loadArticles()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function changePerPage(newPerPage) {
|
||||||
|
perPage.value = newPerPage
|
||||||
|
page.value = 1
|
||||||
|
loadArticles()
|
||||||
|
}
|
||||||
|
|
||||||
function toggleSort(column) {
|
function toggleSort(column) {
|
||||||
if (sort.value === column) {
|
if (sort.value === column) {
|
||||||
order.value = order.value === 'desc' ? 'asc' : 'desc'
|
order.value = order.value === 'desc' ? 'asc' : 'desc'
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h2>Equipment Details</h2>
|
<h2>Equipment Details</h2>
|
||||||
<div class="header-actions">
|
<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">
|
<router-link :to="`/machines/${equipment?.equipment?.equipmentid}/edit`" class="btn btn-primary" v-if="equipment">
|
||||||
Edit
|
Edit
|
||||||
</router-link>
|
</router-link>
|
||||||
@@ -22,28 +25,28 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="hero-meta">
|
<div class="hero-meta">
|
||||||
<span class="badge badge-lg badge-primary">
|
<span class="badge badge-lg badge-primary">
|
||||||
{{ equipment.assettype_name || 'Equipment' }}
|
{{ equipment.assettypename || 'Equipment' }}
|
||||||
</span>
|
</span>
|
||||||
<span class="badge badge-lg" :class="getStatusClass(equipment.status_name)">
|
<span class="badge badge-lg" :class="getStatusClass(equipment.statusname)">
|
||||||
{{ equipment.status_name || 'Unknown' }}
|
{{ equipment.statusname || 'Unknown' }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="hero-details">
|
<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-label">Type</span>
|
||||||
<span class="hero-detail-value">{{ equipment.equipment.equipmenttype_name }}</span>
|
<span class="hero-detail-value">{{ equipment.equipment.equipmenttypename }}</span>
|
||||||
</div>
|
</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-label">Vendor</span>
|
||||||
<span class="hero-detail-value">{{ equipment.equipment.vendor_name }}</span>
|
<span class="hero-detail-value">{{ equipment.equipment.vendorname }}</span>
|
||||||
</div>
|
</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-label">Model</span>
|
||||||
<span class="hero-detail-value">{{ equipment.equipment.model_name }}</span>
|
<span class="hero-detail-value">{{ equipment.equipment.modelname }}</span>
|
||||||
</div>
|
</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-label">Location</span>
|
||||||
<span class="hero-detail-value">{{ equipment.location_name }}</span>
|
<span class="hero-detail-value">{{ equipment.locationname }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -78,30 +81,30 @@
|
|||||||
<div class="info-list">
|
<div class="info-list">
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="info-label">Type</span>
|
<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>
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="info-label">Vendor</span>
|
<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>
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="info-label">Model</span>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Controller Section (for CNC machines) -->
|
<!-- 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>
|
<h3 class="section-title">Controller</h3>
|
||||||
<div class="info-list">
|
<div class="info-list">
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="info-label">Vendor</span>
|
<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>
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="info-label">Model</span>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -164,14 +167,14 @@
|
|||||||
:top="equipment.maptop"
|
:top="equipment.maptop"
|
||||||
:machineName="equipment.assetnumber"
|
:machineName="equipment.assetnumber"
|
||||||
>
|
>
|
||||||
<span class="location-link">{{ equipment.location_name || 'On Map' }}</span>
|
<span class="location-link">{{ equipment.locationname || 'On Map' }}</span>
|
||||||
</LocationMapTooltip>
|
</LocationMapTooltip>
|
||||||
<span v-else>{{ equipment.location_name || '-' }}</span>
|
<span v-else>{{ equipment.locationname || '-' }}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="info-label">Business Unit</span>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -183,7 +186,7 @@
|
|||||||
No controlling PC assigned
|
No controlling PC assigned
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="connected-device">
|
<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">
|
<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">
|
<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>
|
<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
|
// First check incoming - computer as source controlling this equipment
|
||||||
for (const rel of relationships.value.incoming || []) {
|
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 {
|
return {
|
||||||
...rel.source_asset,
|
...rel.sourceasset,
|
||||||
relationshipType: rel.relationship_type_name
|
relationshipType: rel.relationshiptypename
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also check outgoing - legacy data may have equipment -> computer Controls relationships
|
// Also check outgoing - legacy data may have equipment -> computer Controls relationships
|
||||||
for (const rel of relationships.value.outgoing || []) {
|
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 {
|
return {
|
||||||
...rel.target_asset,
|
...rel.targetasset,
|
||||||
relationshipType: rel.relationship_type_name
|
relationshipType: rel.relationshiptypename
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -130,10 +130,10 @@
|
|||||||
<h3 class="form-section-title">Controller</h3>
|
<h3 class="form-section-title">Controller</h3>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="controller_vendorid">Controller Vendor</label>
|
<label for="controllervendorid">Controller Vendor</label>
|
||||||
<select
|
<select
|
||||||
id="controller_vendorid"
|
id="controllervendorid"
|
||||||
v-model="form.controller_vendorid"
|
v-model="form.controllervendorid"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
>
|
>
|
||||||
<option value="">Select vendor...</option>
|
<option value="">Select vendor...</option>
|
||||||
@@ -149,10 +149,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="controller_modelid">Controller Model</label>
|
<label for="controllermodelid">Controller Model</label>
|
||||||
<select
|
<select
|
||||||
id="controller_modelid"
|
id="controllermodelid"
|
||||||
v-model="form.controller_modelid"
|
v-model="form.controllermodelid"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
>
|
>
|
||||||
<option value="">Select model...</option>
|
<option value="">Select model...</option>
|
||||||
@@ -164,7 +164,7 @@
|
|||||||
{{ m.modelnumber }}
|
{{ m.modelnumber }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -351,8 +351,8 @@ const form = ref({
|
|||||||
equipmenttypeid: '',
|
equipmenttypeid: '',
|
||||||
vendorid: '',
|
vendorid: '',
|
||||||
modelnumberid: '',
|
modelnumberid: '',
|
||||||
controller_vendorid: '',
|
controllervendorid: '',
|
||||||
controller_modelid: '',
|
controllermodelid: '',
|
||||||
locationid: '',
|
locationid: '',
|
||||||
businessunitid: '',
|
businessunitid: '',
|
||||||
requiresmanualconfig: false,
|
requiresmanualconfig: false,
|
||||||
@@ -383,8 +383,8 @@ const filteredModels = computed(() => {
|
|||||||
|
|
||||||
// Filter models by selected controller vendor
|
// Filter models by selected controller vendor
|
||||||
const filteredControllerModels = computed(() => {
|
const filteredControllerModels = computed(() => {
|
||||||
if (!form.value.controller_vendorid) return models.value
|
if (!form.value.controllervendorid) return models.value
|
||||||
return models.value.filter(m => m.vendorid === form.value.controller_vendorid)
|
return models.value.filter(m => m.vendorid === form.value.controllervendorid)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Clear model selection when vendor changes
|
// Clear model selection when vendor changes
|
||||||
@@ -398,11 +398,11 @@ watch(() => form.value.vendorid, (newVal, oldVal) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Clear controller model when controller vendor changes
|
// Clear controller model when controller vendor changes
|
||||||
watch(() => form.value.controller_vendorid, (newVal, oldVal) => {
|
watch(() => form.value.controllervendorid, (newVal, oldVal) => {
|
||||||
if (oldVal && 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) {
|
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([
|
const [typesRes, statusRes, vendorRes, locRes, modelsRes, buRes, pcsRes, relTypesRes] = await Promise.all([
|
||||||
equipmentApi.types.list(),
|
equipmentApi.types.list(),
|
||||||
assetsApi.statuses.list(),
|
assetsApi.statuses.list(),
|
||||||
vendorsApi.list({ per_page: 500 }),
|
vendorsApi.list({ perpage: 500 }),
|
||||||
locationsApi.list({ per_page: 500 }),
|
locationsApi.list({ perpage: 500 }),
|
||||||
modelsApi.list({ per_page: 1000 }),
|
modelsApi.list({ perpage: 1000 }),
|
||||||
businessunitsApi.list({ per_page: 500 }),
|
businessunitsApi.list({ perpage: 500 }),
|
||||||
computersApi.list({ per_page: 500 }),
|
computersApi.list({ perpage: 500 }),
|
||||||
assetsApi.types.list() // Used for relationship types, will fix below
|
assetsApi.types.list() // Used for relationship types, will fix below
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -461,8 +461,8 @@ onMounted(async () => {
|
|||||||
equipmenttypeid: data.equipment?.equipmenttypeid || '',
|
equipmenttypeid: data.equipment?.equipmenttypeid || '',
|
||||||
vendorid: data.equipment?.vendorid || '',
|
vendorid: data.equipment?.vendorid || '',
|
||||||
modelnumberid: data.equipment?.modelnumberid || '',
|
modelnumberid: data.equipment?.modelnumberid || '',
|
||||||
controller_vendorid: data.equipment?.controller_vendorid || '',
|
controllervendorid: data.equipment?.controllervendorid || '',
|
||||||
controller_modelid: data.equipment?.controller_modelid || '',
|
controllermodelid: data.equipment?.controllermodelid || '',
|
||||||
locationid: data.locationid || '',
|
locationid: data.locationid || '',
|
||||||
businessunitid: data.businessunitid || '',
|
businessunitid: data.businessunitid || '',
|
||||||
requiresmanualconfig: data.equipment?.requiresmanualconfig || false,
|
requiresmanualconfig: data.equipment?.requiresmanualconfig || false,
|
||||||
@@ -480,8 +480,8 @@ onMounted(async () => {
|
|||||||
|
|
||||||
// Check incoming relationships for a controlling PC
|
// Check incoming relationships for a controlling PC
|
||||||
for (const rel of relationships.incoming || []) {
|
for (const rel of relationships.incoming || []) {
|
||||||
if (rel.source_asset?.assettype_name === 'computer' && rel.relationship_type_name === 'Controls') {
|
if (rel.sourceasset?.assettypename === 'computer' && rel.relationshiptypename === 'Controls') {
|
||||||
controllingPcId.value = rel.source_asset.assetid
|
controllingPcId.value = rel.sourceasset.assetid
|
||||||
relationshipTypeId.value = rel.relationshiptypeid
|
relationshipTypeId.value = rel.relationshiptypeid
|
||||||
existingRelationshipId.value = rel.assetrelationshipid
|
existingRelationshipId.value = rel.assetrelationshipid
|
||||||
break
|
break
|
||||||
@@ -531,8 +531,8 @@ async function saveEquipment() {
|
|||||||
equipmenttypeid: form.value.equipmenttypeid || null,
|
equipmenttypeid: form.value.equipmenttypeid || null,
|
||||||
vendorid: form.value.vendorid || null,
|
vendorid: form.value.vendorid || null,
|
||||||
modelnumberid: form.value.modelnumberid || null,
|
modelnumberid: form.value.modelnumberid || null,
|
||||||
controller_vendorid: form.value.controller_vendorid || null,
|
controllervendorid: form.value.controllervendorid || null,
|
||||||
controller_modelid: form.value.controller_modelid || null,
|
controllermodelid: form.value.controllermodelid || null,
|
||||||
locationid: form.value.locationid || null,
|
locationid: form.value.locationid || null,
|
||||||
businessunitid: form.value.businessunitid || null,
|
businessunitid: form.value.businessunitid || null,
|
||||||
requiresmanualconfig: form.value.requiresmanualconfig,
|
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)
|
// Create new relationship (PC controls Equipment, so PC is source, Equipment is target)
|
||||||
await assetsApi.createRelationship({
|
await assetsApi.createRelationship({
|
||||||
source_assetid: controllingPcId.value,
|
sourceassetid: controllingPcId.value,
|
||||||
target_assetid: assetId,
|
targetassetid: assetId,
|
||||||
relationshiptypeid: relationshipTypeId.value
|
relationshiptypeid: relationshipTypeId.value
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>Serial Number</th>
|
<th>Serial Number</th>
|
||||||
<th>Type</th>
|
<th>Type</th>
|
||||||
|
<th>Vendor</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th>Location</th>
|
<th>Location</th>
|
||||||
<th>Actions</th>
|
<th>Actions</th>
|
||||||
@@ -38,13 +39,14 @@
|
|||||||
<td>{{ item.assetnumber }}</td>
|
<td>{{ item.assetnumber }}</td>
|
||||||
<td>{{ item.name || '-' }}</td>
|
<td>{{ item.name || '-' }}</td>
|
||||||
<td class="mono">{{ item.serialnumber || '-' }}</td>
|
<td class="mono">{{ item.serialnumber || '-' }}</td>
|
||||||
<td>{{ item.equipment?.equipmenttype_name || '-' }}</td>
|
<td>{{ item.equipment?.equipmenttypename || '-' }}</td>
|
||||||
|
<td>{{ item.equipment?.vendorname || '-' }}</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="badge" :class="getStatusClass(item.status_name)">
|
<span class="badge" :class="getStatusClass(item.statusname)">
|
||||||
{{ item.status_name || 'Unknown' }}
|
{{ item.statusname || 'Unknown' }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ item.location_name || '-' }}</td>
|
<td>{{ item.locationname || '-' }}</td>
|
||||||
<td class="actions">
|
<td class="actions">
|
||||||
<router-link
|
<router-link
|
||||||
:to="`/machines/${item.equipment?.equipmentid || item.assetid}`"
|
:to="`/machines/${item.equipment?.equipmentid || item.assetid}`"
|
||||||
@@ -55,7 +57,7 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-if="equipment.length === 0">
|
<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
|
No equipment found
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -64,16 +66,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Pagination -->
|
<!-- Pagination -->
|
||||||
<div class="pagination" v-if="totalPages > 1">
|
<PaginationBar
|
||||||
<button
|
:page="page"
|
||||||
v-for="p in totalPages"
|
:totalPages="totalPages"
|
||||||
:key="p"
|
:perPage="perPage"
|
||||||
:class="{ active: p === page }"
|
@update:page="goToPage"
|
||||||
@click="goToPage(p)"
|
@update:perPage="changePerPage"
|
||||||
>
|
/>
|
||||||
{{ p }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -83,12 +82,14 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { equipmentApi } from '../../api'
|
import { equipmentApi } from '../../api'
|
||||||
|
import PaginationBar from '../../components/PaginationBar.vue'
|
||||||
|
|
||||||
const equipment = ref([])
|
const equipment = ref([])
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const search = ref('')
|
const search = ref('')
|
||||||
const page = ref(1)
|
const page = ref(1)
|
||||||
const totalPages = ref(1)
|
const totalPages = ref(1)
|
||||||
|
const perPage = ref(20)
|
||||||
|
|
||||||
let searchTimeout = null
|
let searchTimeout = null
|
||||||
|
|
||||||
@@ -101,13 +102,13 @@ async function loadEquipment() {
|
|||||||
try {
|
try {
|
||||||
const params = {
|
const params = {
|
||||||
page: page.value,
|
page: page.value,
|
||||||
per_page: 20
|
perpage: perPage.value
|
||||||
}
|
}
|
||||||
if (search.value) params.search = search.value
|
if (search.value) params.search = search.value
|
||||||
|
|
||||||
const response = await equipmentApi.list(params)
|
const response = await equipmentApi.list(params)
|
||||||
equipment.value = response.data.data || []
|
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) {
|
} catch (error) {
|
||||||
console.error('Error loading equipment:', error)
|
console.error('Error loading equipment:', error)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -128,6 +129,12 @@ function goToPage(p) {
|
|||||||
loadEquipment()
|
loadEquipment()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function changePerPage(newPerPage) {
|
||||||
|
perPage.value = newPerPage
|
||||||
|
page.value = 1
|
||||||
|
loadEquipment()
|
||||||
|
}
|
||||||
|
|
||||||
function getStatusClass(status) {
|
function getStatusClass(status) {
|
||||||
if (!status) return 'badge-info'
|
if (!status) return 'badge-info'
|
||||||
const s = status.toLowerCase()
|
const s = status.toLowerCase()
|
||||||
|
|||||||
@@ -3,12 +3,12 @@
|
|||||||
<div class="hero-card">
|
<div class="hero-card">
|
||||||
<div class="hero-image">
|
<div class="hero-image">
|
||||||
<div class="device-icon">
|
<div class="device-icon">
|
||||||
<span class="icon">{{ getDeviceIcon() }}</span>
|
<span class="icon"><component :is="getDeviceIcon()" :size="24" /></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="hero-content">
|
<div class="hero-content">
|
||||||
<div class="hero-title-row">
|
<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
|
<router-link
|
||||||
v-if="authStore.isAuthenticated"
|
v-if="authStore.isAuthenticated"
|
||||||
:to="`/network/${deviceId}/edit`"
|
:to="`/network/${deviceId}/edit`"
|
||||||
@@ -18,14 +18,14 @@
|
|||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
<div class="hero-meta">
|
<div class="hero-meta">
|
||||||
<span class="badge" :class="getStatusClass(device.status_name)">
|
<span class="badge" :class="getStatusClass(device.statusname)">
|
||||||
{{ device.status_name || 'Unknown' }}
|
{{ device.statusname || 'Unknown' }}
|
||||||
</span>
|
</span>
|
||||||
<span v-if="device.network_device?.networkdevicetype_name" class="meta-item">
|
<span v-if="device.networkdevice?.networkdevicetypename" class="meta-item">
|
||||||
{{ device.network_device.networkdevicetype_name }}
|
{{ device.networkdevice.networkdevicetypename }}
|
||||||
</span>
|
</span>
|
||||||
<span v-if="device.network_device?.vendor_name" class="meta-item">
|
<span v-if="device.networkdevice?.vendorname" class="meta-item">
|
||||||
{{ device.network_device.vendor_name }}
|
{{ device.networkdevice.vendorname }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="hero-details">
|
<div class="hero-details">
|
||||||
@@ -37,19 +37,19 @@
|
|||||||
<span class="label">Serial</span>
|
<span class="label">Serial</span>
|
||||||
<span class="value mono">{{ device.serialnumber }}</span>
|
<span class="value mono">{{ device.serialnumber }}</span>
|
||||||
</div>
|
</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="label">Location</span>
|
||||||
<span class="value">{{ device.location_name }}</span>
|
<span class="value">{{ device.locationname }}</span>
|
||||||
</div>
|
</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="label">Business Unit</span>
|
||||||
<span class="value">{{ device.businessunit_name }}</span>
|
<span class="value">{{ device.businessunitname }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="hero-features" v-if="device.network_device">
|
<div class="hero-features" v-if="device.networkdevice">
|
||||||
<span v-if="device.network_device.ispoe" class="feature-badge poe">PoE</span>
|
<span v-if="device.networkdevice.ispoe" class="feature-badge poe">PoE</span>
|
||||||
<span v-if="device.network_device.ismanaged" class="feature-badge managed">Managed</span>
|
<span v-if="device.networkdevice.ismanaged" class="feature-badge managed">Managed</span>
|
||||||
<span v-if="device.network_device.portcount" class="feature-badge ports">{{ device.network_device.portcount }} Ports</span>
|
<span v-if="device.networkdevice.portcount" class="feature-badge ports">{{ device.networkdevice.portcount }} Ports</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -62,27 +62,27 @@
|
|||||||
<div class="info-list">
|
<div class="info-list">
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="info-label">Hostname</span>
|
<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>
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="info-label">Firmware Version</span>
|
<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>
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="info-label">Port Count</span>
|
<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>
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="info-label">Rack Unit</span>
|
<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>
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="info-label">PoE Capable</span>
|
<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>
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="info-label">Managed Device</span>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -113,17 +113,17 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="info-label">Vendor</span>
|
<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>
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="info-label">Device Type</span>
|
<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>
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="info-label">Status</span>
|
<span class="info-label">Status</span>
|
||||||
<span class="info-value">
|
<span class="info-value">
|
||||||
<span class="badge" :class="getStatusClass(device.status_name)">
|
<span class="badge" :class="getStatusClass(device.statusname)">
|
||||||
{{ device.status_name || 'Unknown' }}
|
{{ device.statusname || 'Unknown' }}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -177,6 +177,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
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 { useAuthStore } from '../../stores/auth'
|
||||||
import { networkApi } from '../../api'
|
import { networkApi } from '../../api'
|
||||||
import AssetRelationships from '../../components/AssetRelationships.vue'
|
import AssetRelationships from '../../components/AssetRelationships.vue'
|
||||||
@@ -207,15 +208,15 @@ async function loadDevice() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getDeviceIcon() {
|
function getDeviceIcon() {
|
||||||
const type = device.value?.network_device?.networkdevicetype_name?.toLowerCase() || ''
|
const type = device.value?.networkdevice?.networkdevicetypename?.toLowerCase() || ''
|
||||||
if (type.includes('switch')) return '⏛'
|
if (type.includes('switch')) return Network
|
||||||
if (type.includes('router')) return '⇌'
|
if (type.includes('router')) return Router
|
||||||
if (type.includes('firewall')) return '🛡'
|
if (type.includes('firewall')) return Shield
|
||||||
if (type.includes('access point') || type.includes('ap')) return '📶'
|
if (type.includes('access point') || type.includes('ap')) return Wifi
|
||||||
if (type.includes('camera')) return '📷'
|
if (type.includes('camera')) return Camera
|
||||||
if (type.includes('server')) return '🖥'
|
if (type.includes('server')) return Server
|
||||||
if (type.includes('idf') || type.includes('closet')) return '🗄'
|
if (type.includes('idf') || type.includes('closet')) return Rack
|
||||||
return '🌐'
|
return Globe
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStatusClass(status) {
|
function getStatusClass(status) {
|
||||||
@@ -240,7 +241,7 @@ function formatDate(dateStr) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function confirmDelete() {
|
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 {
|
try {
|
||||||
await networkApi.delete(deviceId)
|
await networkApi.delete(deviceId)
|
||||||
router.push('/network')
|
router.push('/network')
|
||||||
|
|||||||
@@ -280,7 +280,7 @@ onMounted(async () => {
|
|||||||
|
|
||||||
async function loadDeviceTypes() {
|
async function loadDeviceTypes() {
|
||||||
try {
|
try {
|
||||||
const response = await networkApi.types.list({ per_page: 100 })
|
const response = await networkApi.types.list({ perpage: 100 })
|
||||||
deviceTypes.value = response.data.data || []
|
deviceTypes.value = response.data.data || []
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error loading device types:', err)
|
console.error('Error loading device types:', err)
|
||||||
@@ -289,7 +289,7 @@ async function loadDeviceTypes() {
|
|||||||
|
|
||||||
async function loadVendors() {
|
async function loadVendors() {
|
||||||
try {
|
try {
|
||||||
const response = await vendorsApi.list({ per_page: 100 })
|
const response = await vendorsApi.list({ perpage: 100 })
|
||||||
vendors.value = response.data.data || []
|
vendors.value = response.data.data || []
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error loading vendors:', err)
|
console.error('Error loading vendors:', err)
|
||||||
@@ -298,7 +298,7 @@ async function loadVendors() {
|
|||||||
|
|
||||||
async function loadLocations() {
|
async function loadLocations() {
|
||||||
try {
|
try {
|
||||||
const response = await locationsApi.list({ per_page: 100 })
|
const response = await locationsApi.list({ perpage: 100 })
|
||||||
locations.value = response.data.data || []
|
locations.value = response.data.data || []
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error loading locations:', err)
|
console.error('Error loading locations:', err)
|
||||||
@@ -307,7 +307,7 @@ async function loadLocations() {
|
|||||||
|
|
||||||
async function loadStatuses() {
|
async function loadStatuses() {
|
||||||
try {
|
try {
|
||||||
const response = await statusesApi.list({ per_page: 100 })
|
const response = await statusesApi.list({ perpage: 100 })
|
||||||
statuses.value = response.data.data || []
|
statuses.value = response.data.data || []
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error loading statuses:', err)
|
console.error('Error loading statuses:', err)
|
||||||
@@ -316,7 +316,7 @@ async function loadStatuses() {
|
|||||||
|
|
||||||
async function loadBusinessUnits() {
|
async function loadBusinessUnits() {
|
||||||
try {
|
try {
|
||||||
const response = await businessunitsApi.list({ per_page: 100 })
|
const response = await businessunitsApi.list({ perpage: 100 })
|
||||||
businessUnits.value = response.data.data || []
|
businessUnits.value = response.data.data || []
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error loading business units:', err)
|
console.error('Error loading business units:', err)
|
||||||
@@ -340,15 +340,15 @@ async function loadDevice() {
|
|||||||
form.value.notes = data.notes || ''
|
form.value.notes = data.notes || ''
|
||||||
|
|
||||||
// Network device specific
|
// Network device specific
|
||||||
if (data.network_device) {
|
if (data.networkdevice) {
|
||||||
form.value.hostname = data.network_device.hostname || ''
|
form.value.hostname = data.networkdevice.hostname || ''
|
||||||
form.value.networkdevicetypeid = data.network_device.networkdevicetypeid || ''
|
form.value.networkdevicetypeid = data.networkdevice.networkdevicetypeid || ''
|
||||||
form.value.vendorid = data.network_device.vendorid || ''
|
form.value.vendorid = data.networkdevice.vendorid || ''
|
||||||
form.value.firmwareversion = data.network_device.firmwareversion || ''
|
form.value.firmwareversion = data.networkdevice.firmwareversion || ''
|
||||||
form.value.portcount = data.network_device.portcount
|
form.value.portcount = data.networkdevice.portcount
|
||||||
form.value.rackunit = data.network_device.rackunit || ''
|
form.value.rackunit = data.networkdevice.rackunit || ''
|
||||||
form.value.ispoe = data.network_device.ispoe || false
|
form.value.ispoe = data.networkdevice.ispoe || false
|
||||||
form.value.ismanaged = data.network_device.ismanaged || false
|
form.value.ismanaged = data.networkdevice.ismanaged || false
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error loading device:', err)
|
console.error('Error loading device:', err)
|
||||||
@@ -386,7 +386,7 @@ async function submitForm() {
|
|||||||
router.push(`/network/${deviceId}`)
|
router.push(`/network/${deviceId}`)
|
||||||
} else {
|
} else {
|
||||||
const response = await networkApi.create(payload)
|
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')
|
router.push(newId ? `/network/${newId}` : '/network')
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -54,37 +54,39 @@
|
|||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Hostname</th>
|
|
||||||
<th>Asset #</th>
|
<th>Asset #</th>
|
||||||
|
<th>Hostname</th>
|
||||||
|
<th>Serial Number</th>
|
||||||
<th>Type</th>
|
<th>Type</th>
|
||||||
<th>Vendor</th>
|
<th>Vendor</th>
|
||||||
<th>Features</th>
|
<th>Features</th>
|
||||||
<th>Location</th>
|
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
|
<th>Location</th>
|
||||||
<th>Actions</th>
|
<th>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="device in devices" :key="device.network_device?.networkdeviceid || device.assetid">
|
<tr v-for="device in devices" :key="device.networkdevice?.networkdeviceid || device.assetid">
|
||||||
<td class="mono">{{ device.network_device?.hostname || '-' }}</td>
|
|
||||||
<td>{{ device.assetnumber }}</td>
|
<td>{{ device.assetnumber }}</td>
|
||||||
<td>{{ device.network_device?.networkdevicetype_name || '-' }}</td>
|
<td class="mono">{{ device.networkdevice?.hostname || '-' }}</td>
|
||||||
<td>{{ device.network_device?.vendor_name || '-' }}</td>
|
<td class="mono">{{ device.serialnumber || '-' }}</td>
|
||||||
|
<td>{{ device.networkdevice?.networkdevicetypename || '-' }}</td>
|
||||||
|
<td>{{ device.networkdevice?.vendorname || '-' }}</td>
|
||||||
<td class="features">
|
<td class="features">
|
||||||
<span v-if="device.network_device?.ispoe" class="feature-tag poe">PoE</span>
|
<span v-if="device.networkdevice?.ispoe" class="feature-tag poe">PoE</span>
|
||||||
<span v-if="device.network_device?.ismanaged" class="feature-tag managed">Managed</span>
|
<span v-if="device.networkdevice?.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.networkdevice?.portcount" class="feature-tag ports">{{ device.networkdevice.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 && !device.networkdevice?.ismanaged && !device.networkdevice?.portcount">-</span>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ device.location_name || '-' }}</td>
|
|
||||||
<td>
|
<td>
|
||||||
<span class="badge" :class="getStatusClass(device.status_name)">
|
<span class="badge" :class="getStatusClass(device.statusname)">
|
||||||
{{ device.status_name || 'Unknown' }}
|
{{ device.statusname || 'Unknown' }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
<td>{{ device.locationname || '-' }}</td>
|
||||||
<td class="actions">
|
<td class="actions">
|
||||||
<router-link
|
<router-link
|
||||||
:to="`/network/${device.network_device?.networkdeviceid}`"
|
:to="`/network/${device.networkdevice?.networkdeviceid}`"
|
||||||
class="btn btn-secondary btn-sm"
|
class="btn btn-secondary btn-sm"
|
||||||
>
|
>
|
||||||
View
|
View
|
||||||
@@ -92,7 +94,7 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-if="devices.length === 0">
|
<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
|
No network devices found
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -101,36 +103,22 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Pagination -->
|
<!-- Pagination -->
|
||||||
<div class="pagination" v-if="totalPages > 1">
|
<PaginationBar
|
||||||
<button
|
:page="page"
|
||||||
:disabled="page === 1"
|
:totalPages="totalPages"
|
||||||
@click="goToPage(page - 1)"
|
:perPage="perPage"
|
||||||
>
|
@update:page="goToPage"
|
||||||
Prev
|
@update:perPage="changePerPage"
|
||||||
</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>
|
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { networkApi, vendorsApi, locationsApi } from '../../api'
|
import { networkApi, vendorsApi, locationsApi } from '../../api'
|
||||||
|
import PaginationBar from '../../components/PaginationBar.vue'
|
||||||
|
|
||||||
const devices = ref([])
|
const devices = ref([])
|
||||||
const deviceTypes = ref([])
|
const deviceTypes = ref([])
|
||||||
@@ -143,20 +131,11 @@ const vendorFilter = ref('')
|
|||||||
const locationFilter = ref('')
|
const locationFilter = ref('')
|
||||||
const page = ref(1)
|
const page = ref(1)
|
||||||
const totalPages = ref(1)
|
const totalPages = ref(1)
|
||||||
|
const perPage = ref(25)
|
||||||
const totalCount = ref(0)
|
const totalCount = ref(0)
|
||||||
|
|
||||||
let searchTimeout = null
|
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 () => {
|
onMounted(async () => {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
loadDeviceTypes(),
|
loadDeviceTypes(),
|
||||||
@@ -168,7 +147,7 @@ onMounted(async () => {
|
|||||||
|
|
||||||
async function loadDeviceTypes() {
|
async function loadDeviceTypes() {
|
||||||
try {
|
try {
|
||||||
const response = await networkApi.types.list({ per_page: 100 })
|
const response = await networkApi.types.list({ perpage: 100 })
|
||||||
deviceTypes.value = response.data.data || []
|
deviceTypes.value = response.data.data || []
|
||||||
// Get counts for each type
|
// Get counts for each type
|
||||||
await updateTypeCounts()
|
await updateTypeCounts()
|
||||||
@@ -181,7 +160,7 @@ async function updateTypeCounts() {
|
|||||||
// Get summary for type counts
|
// Get summary for type counts
|
||||||
try {
|
try {
|
||||||
const response = await networkApi.dashboardSummary()
|
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
|
totalCount.value = response.data.data?.total || 0
|
||||||
|
|
||||||
// Map counts to types
|
// Map counts to types
|
||||||
@@ -196,7 +175,7 @@ async function updateTypeCounts() {
|
|||||||
|
|
||||||
async function loadVendors() {
|
async function loadVendors() {
|
||||||
try {
|
try {
|
||||||
const response = await vendorsApi.list({ per_page: 100 })
|
const response = await vendorsApi.list({ perpage: 100 })
|
||||||
vendors.value = response.data.data || []
|
vendors.value = response.data.data || []
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading vendors:', error)
|
console.error('Error loading vendors:', error)
|
||||||
@@ -205,7 +184,7 @@ async function loadVendors() {
|
|||||||
|
|
||||||
async function loadLocations() {
|
async function loadLocations() {
|
||||||
try {
|
try {
|
||||||
const response = await locationsApi.list({ per_page: 100 })
|
const response = await locationsApi.list({ perpage: 100 })
|
||||||
locations.value = response.data.data || []
|
locations.value = response.data.data || []
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading locations:', error)
|
console.error('Error loading locations:', error)
|
||||||
@@ -217,7 +196,7 @@ async function loadDevices() {
|
|||||||
try {
|
try {
|
||||||
const params = {
|
const params = {
|
||||||
page: page.value,
|
page: page.value,
|
||||||
per_page: 25
|
perpage: perPage.value
|
||||||
}
|
}
|
||||||
if (search.value) params.search = search.value
|
if (search.value) params.search = search.value
|
||||||
if (selectedType.value) params.type_id = selectedType.value
|
if (selectedType.value) params.type_id = selectedType.value
|
||||||
@@ -226,7 +205,7 @@ async function loadDevices() {
|
|||||||
|
|
||||||
const response = await networkApi.list(params)
|
const response = await networkApi.list(params)
|
||||||
devices.value = response.data.data || []
|
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) {
|
} catch (error) {
|
||||||
console.error('Error loading network devices:', error)
|
console.error('Error loading network devices:', error)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -255,6 +234,12 @@ function goToPage(p) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function changePerPage(newPerPage) {
|
||||||
|
perPage.value = newPerPage
|
||||||
|
page.value = 1
|
||||||
|
loadDevices()
|
||||||
|
}
|
||||||
|
|
||||||
function getStatusClass(status) {
|
function getStatusClass(status) {
|
||||||
if (!status) return 'badge-info'
|
if (!status) return 'badge-info'
|
||||||
const s = status.toLowerCase()
|
const s = status.toLowerCase()
|
||||||
|
|||||||
@@ -287,7 +287,7 @@ onMounted(async () => {
|
|||||||
const [typesRes, buRes, appsRes] = await Promise.all([
|
const [typesRes, buRes, appsRes] = await Promise.all([
|
||||||
notificationsApi.types.list(),
|
notificationsApi.types.list(),
|
||||||
businessUnitsApi.list().catch(() => ({ data: { data: [] } })),
|
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 || []
|
types.value = typesRes.data.data || []
|
||||||
|
|||||||
@@ -73,22 +73,20 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Pagination -->
|
<!-- Pagination -->
|
||||||
<div class="pagination" v-if="totalPages > 1">
|
<PaginationBar
|
||||||
<button
|
:page="page"
|
||||||
v-for="p in totalPages"
|
:totalPages="totalPages"
|
||||||
:key="p"
|
:perPage="perPage"
|
||||||
:class="{ active: p === page }"
|
@update:page="goToPage"
|
||||||
@click="goToPage(p)"
|
@update:perPage="changePerPage"
|
||||||
>
|
/>
|
||||||
{{ p }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { notificationsApi } from '@/api'
|
import { notificationsApi } from '@/api'
|
||||||
|
import PaginationBar from '../../components/PaginationBar.vue'
|
||||||
|
|
||||||
const notifications = ref([])
|
const notifications = ref([])
|
||||||
const types = ref([])
|
const types = ref([])
|
||||||
@@ -122,7 +120,7 @@ async function loadNotifications() {
|
|||||||
try {
|
try {
|
||||||
const params = {
|
const params = {
|
||||||
page: page.value,
|
page: page.value,
|
||||||
per_page: perPage.value
|
perpage: perPage.value
|
||||||
}
|
}
|
||||||
|
|
||||||
if (searchQuery.value) {
|
if (searchQuery.value) {
|
||||||
@@ -161,6 +159,12 @@ function goToPage(p) {
|
|||||||
loadNotifications()
|
loadNotifications()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function changePerPage(newPerPage) {
|
||||||
|
perPage.value = newPerPage
|
||||||
|
page.value = 1
|
||||||
|
loadNotifications()
|
||||||
|
}
|
||||||
|
|
||||||
function formatDate(dateStr) {
|
function formatDate(dateStr) {
|
||||||
if (!dateStr) return ''
|
if (!dateStr) return ''
|
||||||
return new Date(dateStr).toLocaleDateString()
|
return new Date(dateStr).toLocaleDateString()
|
||||||
|
|||||||
@@ -20,22 +20,22 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="hero-meta">
|
<div class="hero-meta">
|
||||||
<span class="badge badge-lg badge-info">Computer</span>
|
<span class="badge badge-lg badge-info">Computer</span>
|
||||||
<span class="badge badge-lg" :class="getStatusClass(computer.status_name)">
|
<span class="badge badge-lg" :class="getStatusClass(computer.statusname)">
|
||||||
{{ computer.status_name || 'Unknown' }}
|
{{ computer.statusname || 'Unknown' }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="hero-details">
|
<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-label">Type</span>
|
||||||
<span class="hero-detail-value">{{ computer.computer.computertype_name }}</span>
|
<span class="hero-detail-value">{{ computer.computer.computertypename }}</span>
|
||||||
</div>
|
</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-label">OS</span>
|
||||||
<span class="hero-detail-value">{{ computer.computer.os_name }}</span>
|
<span class="hero-detail-value">{{ computer.computer.osname }}</span>
|
||||||
</div>
|
</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-label">Location</span>
|
||||||
<span class="hero-detail-value">{{ computer.location_name }}</span>
|
<span class="hero-detail-value">{{ computer.locationname }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -74,11 +74,11 @@
|
|||||||
<div class="info-list">
|
<div class="info-list">
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="info-label">Computer Type</span>
|
<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>
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="info-label">Operating System</span>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -126,14 +126,14 @@
|
|||||||
:top="computer.maptop"
|
:top="computer.maptop"
|
||||||
:machineName="computer.assetnumber"
|
:machineName="computer.assetnumber"
|
||||||
>
|
>
|
||||||
<span class="location-link">{{ computer.location_name || 'On Map' }}</span>
|
<span class="location-link">{{ computer.locationname || 'On Map' }}</span>
|
||||||
</LocationMapTooltip>
|
</LocationMapTooltip>
|
||||||
<span v-else>{{ computer.location_name || '-' }}</span>
|
<span v-else>{{ computer.locationname || '-' }}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="info-label">Business Unit</span>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -145,7 +145,7 @@
|
|||||||
<router-link
|
<router-link
|
||||||
v-for="item in controlledEquipment"
|
v-for="item in controlledEquipment"
|
||||||
:key="item.relationshipid"
|
:key="item.relationshipid"
|
||||||
:to="`/machines/${item.plugin_id || item.assetid}`"
|
:to="`/machines/${item.pluginid || item.assetid}`"
|
||||||
class="equipment-item"
|
class="equipment-item"
|
||||||
>
|
>
|
||||||
<div class="equipment-info">
|
<div class="equipment-info">
|
||||||
@@ -153,7 +153,7 @@
|
|||||||
<span class="equipment-alias" v-if="item.name">{{ item.name }}</span>
|
<span class="equipment-alias" v-if="item.name">{{ item.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="equipment-meta">
|
<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>
|
<span class="connection-tag">{{ item.relationshipType }}</span>
|
||||||
</div>
|
</div>
|
||||||
</router-link>
|
</router-link>
|
||||||
@@ -221,22 +221,22 @@ const controlledEquipment = computed(() => {
|
|||||||
|
|
||||||
// Check outgoing - computer controls equipment
|
// Check outgoing - computer controls equipment
|
||||||
for (const rel of relationships.value.outgoing || []) {
|
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({
|
items.push({
|
||||||
...rel.target_asset,
|
...rel.targetasset,
|
||||||
relationshipid: rel.relationshipid,
|
relationshipid: rel.relationshipid,
|
||||||
relationshipType: rel.relationship_type_name
|
relationshipType: rel.relationshiptypename
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also check incoming - legacy data may have equipment -> computer Controls relationships
|
// Also check incoming - legacy data may have equipment -> computer Controls relationships
|
||||||
for (const rel of relationships.value.incoming || []) {
|
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({
|
items.push({
|
||||||
...rel.source_asset,
|
...rel.sourceasset,
|
||||||
relationshipid: rel.relationshipid,
|
relationshipid: rel.relationshipid,
|
||||||
relationshipType: rel.relationship_type_name
|
relationshipType: rel.relationshiptypename
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
<th>Type</th>
|
<th>Type</th>
|
||||||
<th>Features</th>
|
<th>Features</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
|
<th>Location</th>
|
||||||
<th>Actions</th>
|
<th>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -38,17 +39,18 @@
|
|||||||
<td>{{ item.assetnumber }}</td>
|
<td>{{ item.assetnumber }}</td>
|
||||||
<td>{{ item.computer?.hostname || '-' }}</td>
|
<td>{{ item.computer?.hostname || '-' }}</td>
|
||||||
<td class="mono">{{ item.serialnumber || '-' }}</td>
|
<td class="mono">{{ item.serialnumber || '-' }}</td>
|
||||||
<td>{{ item.computer?.computertype_name || '-' }}</td>
|
<td>{{ item.computer?.computertypename || '-' }}</td>
|
||||||
<td class="features">
|
<td class="features">
|
||||||
<span v-if="item.computer?.isvnc" class="feature-tag active">VNC</span>
|
<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?.iswinrm" class="feature-tag active">WinRM</span>
|
||||||
<span v-if="!item.computer?.isvnc && !item.computer?.iswinrm">-</span>
|
<span v-if="!item.computer?.isvnc && !item.computer?.iswinrm">-</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="badge" :class="getStatusClass(item.status_name)">
|
<span class="badge" :class="getStatusClass(item.statusname)">
|
||||||
{{ item.status_name || 'Unknown' }}
|
{{ item.statusname || 'Unknown' }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
<td>{{ item.locationname || '-' }}</td>
|
||||||
<td class="actions">
|
<td class="actions">
|
||||||
<router-link
|
<router-link
|
||||||
:to="`/pcs/${item.computer?.computerid || item.assetid}`"
|
:to="`/pcs/${item.computer?.computerid || item.assetid}`"
|
||||||
@@ -59,7 +61,7 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-if="computers.length === 0">
|
<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
|
No computers found
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -68,16 +70,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Pagination -->
|
<!-- Pagination -->
|
||||||
<div class="pagination" v-if="totalPages > 1">
|
<PaginationBar
|
||||||
<button
|
:page="page"
|
||||||
v-for="p in totalPages"
|
:totalPages="totalPages"
|
||||||
:key="p"
|
:perPage="perPage"
|
||||||
:class="{ active: p === page }"
|
@update:page="goToPage"
|
||||||
@click="goToPage(p)"
|
@update:perPage="changePerPage"
|
||||||
>
|
/>
|
||||||
{{ p }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -86,12 +85,14 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { computersApi } from '../../api'
|
import { computersApi } from '../../api'
|
||||||
|
import PaginationBar from '../../components/PaginationBar.vue'
|
||||||
|
|
||||||
const computers = ref([])
|
const computers = ref([])
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const search = ref('')
|
const search = ref('')
|
||||||
const page = ref(1)
|
const page = ref(1)
|
||||||
const totalPages = ref(1)
|
const totalPages = ref(1)
|
||||||
|
const perPage = ref(20)
|
||||||
|
|
||||||
let searchTimeout = null
|
let searchTimeout = null
|
||||||
|
|
||||||
@@ -104,13 +105,13 @@ async function loadComputers() {
|
|||||||
try {
|
try {
|
||||||
const params = {
|
const params = {
|
||||||
page: page.value,
|
page: page.value,
|
||||||
per_page: 20
|
perpage: perPage.value
|
||||||
}
|
}
|
||||||
if (search.value) params.search = search.value
|
if (search.value) params.search = search.value
|
||||||
|
|
||||||
const response = await computersApi.list(params)
|
const response = await computersApi.list(params)
|
||||||
computers.value = response.data.data || []
|
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) {
|
} catch (error) {
|
||||||
console.error('Error loading computers:', error)
|
console.error('Error loading computers:', error)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -131,6 +132,12 @@ function goToPage(p) {
|
|||||||
loadComputers()
|
loadComputers()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function changePerPage(newPerPage) {
|
||||||
|
perPage.value = newPerPage
|
||||||
|
page.value = 1
|
||||||
|
loadComputers()
|
||||||
|
}
|
||||||
|
|
||||||
function getStatusClass(status) {
|
function getStatusClass(status) {
|
||||||
if (!status) return 'badge-info'
|
if (!status) return 'badge-info'
|
||||||
const s = status.toLowerCase()
|
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">
|
<div class="page-header">
|
||||||
<h2>Printer Details</h2>
|
<h2>Printer Details</h2>
|
||||||
<div class="header-actions">
|
<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/${$route.params.id}/edit`" class="btn btn-primary">Edit</router-link>
|
||||||
<router-link to="/printers" class="btn btn-secondary">Back to List</router-link>
|
<router-link to="/printers" class="btn btn-secondary">Back to List</router-link>
|
||||||
</div>
|
</div>
|
||||||
@@ -24,13 +27,13 @@
|
|||||||
<span v-if="printer.printer?.isnetwork" class="badge badge-lg badge-secondary">Network</span>
|
<span v-if="printer.printer?.isnetwork" class="badge badge-lg badge-secondary">Network</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="hero-details">
|
<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-label">Vendor</span>
|
||||||
<span class="hero-detail-value">{{ printer.printer.vendor_name }}</span>
|
<span class="hero-detail-value">{{ printer.printer.vendorname }}</span>
|
||||||
</div>
|
</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-label">Model</span>
|
||||||
<span class="hero-detail-value">{{ printer.printer.model_name }}</span>
|
<span class="hero-detail-value">{{ printer.printer.modelname }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="hero-detail" v-if="printer.serialnumber">
|
<div class="hero-detail" v-if="printer.serialnumber">
|
||||||
<span class="hero-detail-label">Serial Number</span>
|
<span class="hero-detail-label">Serial Number</span>
|
||||||
@@ -133,9 +136,9 @@
|
|||||||
<span v-else>Not mapped</span>
|
<span v-else>Not mapped</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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-label">Business Unit</span>
|
||||||
<span class="info-value">{{ printer.businessunit_name }}</span>
|
<span class="info-value">{{ printer.businessunitname }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,7 +2,10 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h2>Printers</h2>
|
<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>
|
</div>
|
||||||
|
|
||||||
<!-- Filters -->
|
<!-- Filters -->
|
||||||
@@ -36,11 +39,11 @@
|
|||||||
<tr v-for="printer in printers" :key="printer.printer?.printerid || printer.assetid">
|
<tr v-for="printer in printers" :key="printer.printer?.printerid || printer.assetid">
|
||||||
<td>{{ printer.assetnumber }}</td>
|
<td>{{ printer.assetnumber }}</td>
|
||||||
<td>{{ printer.name || '-' }}</td>
|
<td>{{ printer.name || '-' }}</td>
|
||||||
<td>{{ printer.businessunit_name || '-' }}</td>
|
<td>{{ printer.businessunitname || '-' }}</td>
|
||||||
<td>{{ printer.printer?.model_name || '-' }}</td>
|
<td>{{ printer.printer?.modelname || '-' }}</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="badge" :class="getStatusClass(printer.status_name)">
|
<span class="badge" :class="getStatusClass(printer.statusname)">
|
||||||
{{ printer.status_name || 'Active' }}
|
{{ printer.statusname || 'Active' }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="actions">
|
<td class="actions">
|
||||||
@@ -62,16 +65,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Pagination -->
|
<!-- Pagination -->
|
||||||
<div class="pagination" v-if="totalPages > 1">
|
<PaginationBar
|
||||||
<button
|
:page="page"
|
||||||
v-for="p in totalPages"
|
:totalPages="totalPages"
|
||||||
:key="p"
|
:perPage="perPage"
|
||||||
:class="{ active: p === page }"
|
@update:page="goToPage"
|
||||||
@click="goToPage(p)"
|
@update:perPage="changePerPage"
|
||||||
>
|
/>
|
||||||
{{ p }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -80,12 +80,14 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { printersApi } from '../../api'
|
import { printersApi } from '../../api'
|
||||||
|
import PaginationBar from '../../components/PaginationBar.vue'
|
||||||
|
|
||||||
const printers = ref([])
|
const printers = ref([])
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const search = ref('')
|
const search = ref('')
|
||||||
const page = ref(1)
|
const page = ref(1)
|
||||||
const totalPages = ref(1)
|
const totalPages = ref(1)
|
||||||
|
const perPage = ref(20)
|
||||||
|
|
||||||
let searchTimeout = null
|
let searchTimeout = null
|
||||||
|
|
||||||
@@ -98,13 +100,13 @@ async function loadPrinters() {
|
|||||||
try {
|
try {
|
||||||
const params = {
|
const params = {
|
||||||
page: page.value,
|
page: page.value,
|
||||||
perpage: 20
|
perpage: perPage.value
|
||||||
}
|
}
|
||||||
if (search.value) params.search = search.value
|
if (search.value) params.search = search.value
|
||||||
|
|
||||||
const response = await printersApi.list(params)
|
const response = await printersApi.list(params)
|
||||||
printers.value = response.data.data || []
|
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) {
|
} catch (error) {
|
||||||
console.error('Error loading printers:', error)
|
console.error('Error loading printers:', error)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -125,6 +127,12 @@ function goToPage(p) {
|
|||||||
loadPrinters()
|
loadPrinters()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function changePerPage(newPerPage) {
|
||||||
|
perPage.value = newPerPage
|
||||||
|
page.value = 1
|
||||||
|
loadPrinters()
|
||||||
|
}
|
||||||
|
|
||||||
function getStatusClass(status) {
|
function getStatusClass(status) {
|
||||||
if (!status) return 'badge-info'
|
if (!status) return 'badge-info'
|
||||||
const s = status.toLowerCase()
|
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>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
import { reportsApi } from '@/api'
|
import { reportsApi } from '@/api'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
const reports = ref([])
|
const reports = ref([])
|
||||||
const currentReport = ref(null)
|
const currentReport = ref(null)
|
||||||
const reportData = ref(null)
|
const reportData = ref(null)
|
||||||
@@ -155,6 +158,12 @@ async function loadReports() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function runReport(report) {
|
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
|
currentReport.value = report
|
||||||
loading.value = true
|
loading.value = true
|
||||||
reportData.value = null
|
reportData.value = null
|
||||||
|
|||||||
@@ -38,11 +38,14 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pagination" v-if="totalPages > 1">
|
<!-- Pagination -->
|
||||||
<button v-for="p in totalPages" :key="p" :class="{ active: p === page }" @click="goToPage(p)">
|
<PaginationBar
|
||||||
{{ p }}
|
:page="page"
|
||||||
</button>
|
:totalPages="totalPages"
|
||||||
</div>
|
:perPage="perPage"
|
||||||
|
@update:page="goToPage"
|
||||||
|
@update:perPage="changePerPage"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -97,11 +100,13 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { businessunitsApi } from '../../api'
|
import { businessunitsApi } from '../../api'
|
||||||
|
import PaginationBar from '../../components/PaginationBar.vue'
|
||||||
|
|
||||||
const items = ref([])
|
const items = ref([])
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const page = ref(1)
|
const page = ref(1)
|
||||||
const totalPages = ref(1)
|
const totalPages = ref(1)
|
||||||
|
const perPage = ref(20)
|
||||||
|
|
||||||
const showModal = ref(false)
|
const showModal = ref(false)
|
||||||
const editing = ref(null)
|
const editing = ref(null)
|
||||||
@@ -118,9 +123,9 @@ onMounted(() => loadData())
|
|||||||
async function loadData() {
|
async function loadData() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
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 || []
|
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) {
|
} catch (err) {
|
||||||
console.error('Error loading business units:', err)
|
console.error('Error loading business units:', err)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -130,6 +135,12 @@ async function loadData() {
|
|||||||
|
|
||||||
function goToPage(p) { page.value = p; loadData() }
|
function goToPage(p) { page.value = p; loadData() }
|
||||||
|
|
||||||
|
function changePerPage(newPerPage) {
|
||||||
|
perPage.value = newPerPage
|
||||||
|
page.value = 1
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
|
||||||
function openModal(item = null) {
|
function openModal(item = null) {
|
||||||
editing.value = item
|
editing.value = item
|
||||||
form.value = item ? {
|
form.value = item ? {
|
||||||
|
|||||||
@@ -64,16 +64,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Pagination -->
|
<!-- Pagination -->
|
||||||
<div class="pagination" v-if="totalPages > 1">
|
<PaginationBar
|
||||||
<button
|
:page="page"
|
||||||
v-for="p in totalPages"
|
:totalPages="totalPages"
|
||||||
:key="p"
|
:perPage="perPage"
|
||||||
:class="{ active: p === page }"
|
@update:page="goToPage"
|
||||||
@click="goToPage(p)"
|
@update:perPage="changePerPage"
|
||||||
>
|
/>
|
||||||
{{ p }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -185,12 +182,14 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { locationsApi } from '../../api'
|
import { locationsApi } from '../../api'
|
||||||
|
import PaginationBar from '../../components/PaginationBar.vue'
|
||||||
|
|
||||||
const locations = ref([])
|
const locations = ref([])
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const search = ref('')
|
const search = ref('')
|
||||||
const page = ref(1)
|
const page = ref(1)
|
||||||
const totalPages = ref(1)
|
const totalPages = ref(1)
|
||||||
|
const perPage = ref(20)
|
||||||
|
|
||||||
const showModal = ref(false)
|
const showModal = ref(false)
|
||||||
const editingLocation = ref(null)
|
const editingLocation = ref(null)
|
||||||
@@ -220,13 +219,13 @@ async function loadLocations() {
|
|||||||
try {
|
try {
|
||||||
const params = {
|
const params = {
|
||||||
page: page.value,
|
page: page.value,
|
||||||
perpage: 20
|
perpage: perPage.value
|
||||||
}
|
}
|
||||||
if (search.value) params.search = search.value
|
if (search.value) params.search = search.value
|
||||||
|
|
||||||
const response = await locationsApi.list(params)
|
const response = await locationsApi.list(params)
|
||||||
locations.value = response.data.data || []
|
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) {
|
} catch (err) {
|
||||||
console.error('Error loading locations:', err)
|
console.error('Error loading locations:', err)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -247,6 +246,12 @@ function goToPage(p) {
|
|||||||
loadLocations()
|
loadLocations()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function changePerPage(newPerPage) {
|
||||||
|
perPage.value = newPerPage
|
||||||
|
page.value = 1
|
||||||
|
loadLocations()
|
||||||
|
}
|
||||||
|
|
||||||
function openModal(loc = null) {
|
function openModal(loc = null) {
|
||||||
editingLocation.value = loc
|
editingLocation.value = loc
|
||||||
if (loc) {
|
if (loc) {
|
||||||
|
|||||||
@@ -53,16 +53,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Pagination -->
|
<!-- Pagination -->
|
||||||
<div class="pagination" v-if="totalPages > 1">
|
<PaginationBar
|
||||||
<button
|
:page="page"
|
||||||
v-for="p in totalPages"
|
:totalPages="totalPages"
|
||||||
:key="p"
|
:perPage="perPage"
|
||||||
:class="{ active: p === page }"
|
@update:page="goToPage"
|
||||||
@click="goToPage(p)"
|
@update:perPage="changePerPage"
|
||||||
>
|
/>
|
||||||
{{ p }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -147,11 +144,13 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { machinetypesApi } from '../../api'
|
import { machinetypesApi } from '../../api'
|
||||||
|
import PaginationBar from '../../components/PaginationBar.vue'
|
||||||
|
|
||||||
const machineTypes = ref([])
|
const machineTypes = ref([])
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const page = ref(1)
|
const page = ref(1)
|
||||||
const totalPages = ref(1)
|
const totalPages = ref(1)
|
||||||
|
const perPage = ref(20)
|
||||||
|
|
||||||
const showModal = ref(false)
|
const showModal = ref(false)
|
||||||
const editingType = ref(null)
|
const editingType = ref(null)
|
||||||
@@ -176,12 +175,12 @@ async function loadTypes() {
|
|||||||
try {
|
try {
|
||||||
const params = {
|
const params = {
|
||||||
page: page.value,
|
page: page.value,
|
||||||
perpage: 20
|
perpage: perPage.value
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await machinetypesApi.list(params)
|
const response = await machinetypesApi.list(params)
|
||||||
machineTypes.value = response.data.data || []
|
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) {
|
} catch (err) {
|
||||||
console.error('Error loading machine types:', err)
|
console.error('Error loading machine types:', err)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -194,6 +193,12 @@ function goToPage(p) {
|
|||||||
loadTypes()
|
loadTypes()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function changePerPage(newPerPage) {
|
||||||
|
perPage.value = newPerPage
|
||||||
|
page.value = 1
|
||||||
|
loadTypes()
|
||||||
|
}
|
||||||
|
|
||||||
function openModal(mt = null) {
|
function openModal(mt = null) {
|
||||||
editingType.value = mt
|
editingType.value = mt
|
||||||
if (mt) {
|
if (mt) {
|
||||||
|
|||||||
@@ -65,16 +65,14 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pagination" v-if="totalPages > 1">
|
<!-- Pagination -->
|
||||||
<button
|
<PaginationBar
|
||||||
v-for="p in totalPages"
|
:page="page"
|
||||||
:key="p"
|
:totalPages="totalPages"
|
||||||
:class="{ active: p === page }"
|
:perPage="perPage"
|
||||||
@click="goToPage(p)"
|
@update:page="goToPage"
|
||||||
>
|
@update:perPage="changePerPage"
|
||||||
{{ p }}
|
/>
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -184,6 +182,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { modelsApi, vendorsApi, machinetypesApi } from '../../api'
|
import { modelsApi, vendorsApi, machinetypesApi } from '../../api'
|
||||||
|
import PaginationBar from '../../components/PaginationBar.vue'
|
||||||
|
|
||||||
const models = ref([])
|
const models = ref([])
|
||||||
const vendors = ref([])
|
const vendors = ref([])
|
||||||
@@ -193,6 +192,7 @@ const search = ref('')
|
|||||||
const vendorFilter = ref('')
|
const vendorFilter = ref('')
|
||||||
const page = ref(1)
|
const page = ref(1)
|
||||||
const totalPages = ref(1)
|
const totalPages = ref(1)
|
||||||
|
const perPage = ref(20)
|
||||||
|
|
||||||
const showModal = ref(false)
|
const showModal = ref(false)
|
||||||
const editingModel = ref(null)
|
const editingModel = ref(null)
|
||||||
@@ -225,13 +225,13 @@ onMounted(async () => {
|
|||||||
async function loadModels() {
|
async function loadModels() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const params = { page: page.value, perpage: 20 }
|
const params = { page: page.value, perpage: perPage.value }
|
||||||
if (search.value) params.search = search.value
|
if (search.value) params.search = search.value
|
||||||
if (vendorFilter.value) params.vendor = vendorFilter.value
|
if (vendorFilter.value) params.vendor = vendorFilter.value
|
||||||
|
|
||||||
const response = await modelsApi.list(params)
|
const response = await modelsApi.list(params)
|
||||||
models.value = response.data.data || []
|
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) {
|
} catch (err) {
|
||||||
console.error('Error loading models:', err)
|
console.error('Error loading models:', err)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -270,6 +270,12 @@ function goToPage(p) {
|
|||||||
loadModels()
|
loadModels()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function changePerPage(newPerPage) {
|
||||||
|
perPage.value = newPerPage
|
||||||
|
page.value = 1
|
||||||
|
loadModels()
|
||||||
|
}
|
||||||
|
|
||||||
function openModal(m = null) {
|
function openModal(m = null) {
|
||||||
editingModel.value = m
|
editingModel.value = m
|
||||||
if (m) {
|
if (m) {
|
||||||
|
|||||||
@@ -45,11 +45,14 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pagination" v-if="totalPages > 1">
|
<!-- Pagination -->
|
||||||
<button v-for="p in totalPages" :key="p" :class="{ active: p === page }" @click="goToPage(p)">
|
<PaginationBar
|
||||||
{{ p }}
|
:page="page"
|
||||||
</button>
|
:totalPages="totalPages"
|
||||||
</div>
|
:perPage="perPage"
|
||||||
|
@update:page="goToPage"
|
||||||
|
@update:perPage="changePerPage"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -115,11 +118,13 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { operatingsystemsApi } from '../../api'
|
import { operatingsystemsApi } from '../../api'
|
||||||
|
import PaginationBar from '../../components/PaginationBar.vue'
|
||||||
|
|
||||||
const items = ref([])
|
const items = ref([])
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const page = ref(1)
|
const page = ref(1)
|
||||||
const totalPages = ref(1)
|
const totalPages = ref(1)
|
||||||
|
const perPage = ref(20)
|
||||||
|
|
||||||
const showModal = ref(false)
|
const showModal = ref(false)
|
||||||
const editing = ref(null)
|
const editing = ref(null)
|
||||||
@@ -136,9 +141,9 @@ onMounted(() => loadData())
|
|||||||
async function loadData() {
|
async function loadData() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
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 || []
|
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) {
|
} catch (err) {
|
||||||
console.error('Error loading operating systems:', err)
|
console.error('Error loading operating systems:', err)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -148,6 +153,12 @@ async function loadData() {
|
|||||||
|
|
||||||
function goToPage(p) { page.value = p; loadData() }
|
function goToPage(p) { page.value = p; loadData() }
|
||||||
|
|
||||||
|
function changePerPage(newPerPage) {
|
||||||
|
perPage.value = newPerPage
|
||||||
|
page.value = 1
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
|
||||||
function isPastEol(date) {
|
function isPastEol(date) {
|
||||||
return new Date(date) < new Date()
|
return new Date(date) < new Date()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,11 +36,14 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pagination" v-if="totalPages > 1">
|
<!-- Pagination -->
|
||||||
<button v-for="p in totalPages" :key="p" :class="{ active: p === page }" @click="goToPage(p)">
|
<PaginationBar
|
||||||
{{ p }}
|
:page="page"
|
||||||
</button>
|
:totalPages="totalPages"
|
||||||
</div>
|
:perPage="perPage"
|
||||||
|
@update:page="goToPage"
|
||||||
|
@update:perPage="changePerPage"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -91,11 +94,13 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { pctypesApi } from '../../api'
|
import { pctypesApi } from '../../api'
|
||||||
|
import PaginationBar from '../../components/PaginationBar.vue'
|
||||||
|
|
||||||
const pcTypes = ref([])
|
const pcTypes = ref([])
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const page = ref(1)
|
const page = ref(1)
|
||||||
const totalPages = ref(1)
|
const totalPages = ref(1)
|
||||||
|
const perPage = ref(20)
|
||||||
|
|
||||||
const showModal = ref(false)
|
const showModal = ref(false)
|
||||||
const editing = ref(null)
|
const editing = ref(null)
|
||||||
@@ -112,9 +117,9 @@ onMounted(() => loadData())
|
|||||||
async function loadData() {
|
async function loadData() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
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 || []
|
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) {
|
} catch (err) {
|
||||||
console.error('Error loading PC types:', err)
|
console.error('Error loading PC types:', err)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -124,6 +129,12 @@ async function loadData() {
|
|||||||
|
|
||||||
function goToPage(p) { page.value = p; loadData() }
|
function goToPage(p) { page.value = p; loadData() }
|
||||||
|
|
||||||
|
function changePerPage(newPerPage) {
|
||||||
|
perPage.value = newPerPage
|
||||||
|
page.value = 1
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
|
||||||
function openModal(item = null) {
|
function openModal(item = null) {
|
||||||
editing.value = item
|
editing.value = item
|
||||||
form.value = item ? { pctype: item.pctype || '', description: item.description || '' } : { pctype: '', description: '' }
|
form.value = item ? { pctype: item.pctype || '', description: item.description || '' } : { pctype: '', description: '' }
|
||||||
|
|||||||
@@ -4,61 +4,61 @@
|
|||||||
|
|
||||||
<div class="settings-grid">
|
<div class="settings-grid">
|
||||||
<router-link to="/settings/vendors" class="settings-card">
|
<router-link to="/settings/vendors" class="settings-card">
|
||||||
<div class="card-icon">🏭</div>
|
<div class="card-icon"><Factory :size="28" /></div>
|
||||||
<h3>Vendors</h3>
|
<h3>Vendors</h3>
|
||||||
<p>Manage equipment vendors and manufacturers</p>
|
<p>Manage equipment vendors and manufacturers</p>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<router-link to="/settings/locations" class="settings-card">
|
<router-link to="/settings/locations" class="settings-card">
|
||||||
<div class="card-icon">📍</div>
|
<div class="card-icon"><MapPin :size="28" /></div>
|
||||||
<h3>Locations</h3>
|
<h3>Locations</h3>
|
||||||
<p>Manage physical locations and sites</p>
|
<p>Manage physical locations and sites</p>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<router-link to="/settings/statuses" class="settings-card">
|
<router-link to="/settings/statuses" class="settings-card">
|
||||||
<div class="card-icon">🏷️</div>
|
<div class="card-icon"><Tag :size="28" /></div>
|
||||||
<h3>Statuses</h3>
|
<h3>Statuses</h3>
|
||||||
<p>Manage equipment status types</p>
|
<p>Manage equipment status types</p>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<router-link to="/settings/models" class="settings-card">
|
<router-link to="/settings/models" class="settings-card">
|
||||||
<div class="card-icon">📦</div>
|
<div class="card-icon"><Package :size="28" /></div>
|
||||||
<h3>Models</h3>
|
<h3>Models</h3>
|
||||||
<p>Manage equipment models by vendor</p>
|
<p>Manage equipment models by vendor</p>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<router-link to="/settings/machinetypes" class="settings-card">
|
<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>
|
<h3>Machine Types</h3>
|
||||||
<p>Manage machine type categories</p>
|
<p>Manage machine type categories</p>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<router-link to="/settings/pctypes" class="settings-card">
|
<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>
|
<h3>PC Types</h3>
|
||||||
<p>Manage PC form factors</p>
|
<p>Manage PC form factors</p>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<router-link to="/settings/operatingsystems" class="settings-card">
|
<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>
|
<h3>Operating Systems</h3>
|
||||||
<p>Manage OS versions and EOL dates</p>
|
<p>Manage OS versions and EOL dates</p>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<router-link to="/settings/businessunits" class="settings-card">
|
<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>
|
<h3>Business Units</h3>
|
||||||
<p>Manage organizational units</p>
|
<p>Manage organizational units</p>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<router-link to="/settings/vlans" class="settings-card">
|
<router-link to="/settings/vlans" class="settings-card">
|
||||||
<div class="card-icon">🌐</div>
|
<div class="card-icon"><Globe :size="28" /></div>
|
||||||
<h3>VLANs</h3>
|
<h3>VLANs</h3>
|
||||||
<p>Manage virtual LANs</p>
|
<p>Manage virtual LANs</p>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<router-link to="/settings/subnets" class="settings-card">
|
<router-link to="/settings/subnets" class="settings-card">
|
||||||
<div class="card-icon">🔗</div>
|
<div class="card-icon"><Link :size="28" /></div>
|
||||||
<h3>Subnets</h3>
|
<h3>Subnets</h3>
|
||||||
<p>Manage IP subnets and DHCP</p>
|
<p>Manage IP subnets and DHCP</p>
|
||||||
</router-link>
|
</router-link>
|
||||||
@@ -67,6 +67,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { Factory, MapPin, Tag, Package, Monitor, Laptop, Cog, Building, Globe, Link } from 'lucide-vue-next'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -56,16 +56,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Pagination -->
|
<!-- Pagination -->
|
||||||
<div class="pagination" v-if="totalPages > 1">
|
<PaginationBar
|
||||||
<button
|
:page="page"
|
||||||
v-for="p in totalPages"
|
:totalPages="totalPages"
|
||||||
:key="p"
|
:perPage="perPage"
|
||||||
:class="{ active: p === page }"
|
@update:page="goToPage"
|
||||||
@click="goToPage(p)"
|
@update:perPage="changePerPage"
|
||||||
>
|
/>
|
||||||
{{ p }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -153,11 +150,13 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { statusesApi } from '../../api'
|
import { statusesApi } from '../../api'
|
||||||
|
import PaginationBar from '../../components/PaginationBar.vue'
|
||||||
|
|
||||||
const statuses = ref([])
|
const statuses = ref([])
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const page = ref(1)
|
const page = ref(1)
|
||||||
const totalPages = ref(1)
|
const totalPages = ref(1)
|
||||||
|
const perPage = ref(20)
|
||||||
|
|
||||||
const showModal = ref(false)
|
const showModal = ref(false)
|
||||||
const editingStatus = ref(null)
|
const editingStatus = ref(null)
|
||||||
@@ -182,12 +181,12 @@ async function loadStatuses() {
|
|||||||
try {
|
try {
|
||||||
const params = {
|
const params = {
|
||||||
page: page.value,
|
page: page.value,
|
||||||
perpage: 20
|
perpage: perPage.value
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await statusesApi.list(params)
|
const response = await statusesApi.list(params)
|
||||||
statuses.value = response.data.data || []
|
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) {
|
} catch (err) {
|
||||||
console.error('Error loading statuses:', err)
|
console.error('Error loading statuses:', err)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -200,6 +199,12 @@ function goToPage(p) {
|
|||||||
loadStatuses()
|
loadStatuses()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function changePerPage(newPerPage) {
|
||||||
|
perPage.value = newPerPage
|
||||||
|
page.value = 1
|
||||||
|
loadStatuses()
|
||||||
|
}
|
||||||
|
|
||||||
function openModal(s = null) {
|
function openModal(s = null) {
|
||||||
editingStatus.value = s
|
editingStatus.value = s
|
||||||
if (s) {
|
if (s) {
|
||||||
|
|||||||
@@ -49,8 +49,8 @@
|
|||||||
<td class="mono">{{ subnet.cidr }}</td>
|
<td class="mono">{{ subnet.cidr }}</td>
|
||||||
<td>{{ subnet.name }}</td>
|
<td>{{ subnet.name }}</td>
|
||||||
<td>
|
<td>
|
||||||
<span v-if="subnet.vlan_name">
|
<span v-if="subnet.vlanname">
|
||||||
VLAN {{ subnet.vlan_number }} - {{ subnet.vlan_name }}
|
VLAN {{ subnet.vlannumber }} - {{ subnet.vlanname }}
|
||||||
</span>
|
</span>
|
||||||
<span v-else>-</span>
|
<span v-else>-</span>
|
||||||
</td>
|
</td>
|
||||||
@@ -60,7 +60,7 @@
|
|||||||
{{ subnet.dhcpenabled ? 'Enabled' : 'Disabled' }}
|
{{ subnet.dhcpenabled ? 'Enabled' : 'Disabled' }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ subnet.location_name || '-' }}</td>
|
<td>{{ subnet.locationname || '-' }}</td>
|
||||||
<td class="actions">
|
<td class="actions">
|
||||||
<button
|
<button
|
||||||
class="btn btn-secondary btn-sm"
|
class="btn btn-secondary btn-sm"
|
||||||
@@ -86,16 +86,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Pagination -->
|
<!-- Pagination -->
|
||||||
<div class="pagination" v-if="totalPages > 1">
|
<PaginationBar
|
||||||
<button
|
:page="page"
|
||||||
v-for="p in totalPages"
|
:totalPages="totalPages"
|
||||||
:key="p"
|
:perPage="perPage"
|
||||||
:class="{ active: p === page }"
|
@update:page="goToPage"
|
||||||
@click="goToPage(p)"
|
@update:perPage="changePerPage"
|
||||||
>
|
/>
|
||||||
{{ p }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -287,6 +284,7 @@
|
|||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { networkApi, locationsApi } from '../../api'
|
import { networkApi, locationsApi } from '../../api'
|
||||||
|
import PaginationBar from '../../components/PaginationBar.vue'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
@@ -299,6 +297,7 @@ const vlanFilter = ref('')
|
|||||||
const typeFilter = ref('')
|
const typeFilter = ref('')
|
||||||
const page = ref(1)
|
const page = ref(1)
|
||||||
const totalPages = ref(1)
|
const totalPages = ref(1)
|
||||||
|
const perPage = ref(20)
|
||||||
|
|
||||||
const showModal = ref(false)
|
const showModal = ref(false)
|
||||||
const editingSubnet = ref(null)
|
const editingSubnet = ref(null)
|
||||||
@@ -341,7 +340,7 @@ onMounted(async () => {
|
|||||||
|
|
||||||
async function loadVLANs() {
|
async function loadVLANs() {
|
||||||
try {
|
try {
|
||||||
const response = await networkApi.vlans.list({ per_page: 100 })
|
const response = await networkApi.vlans.list({ perpage: 100 })
|
||||||
vlans.value = response.data.data || []
|
vlans.value = response.data.data || []
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error loading VLANs:', err)
|
console.error('Error loading VLANs:', err)
|
||||||
@@ -350,7 +349,7 @@ async function loadVLANs() {
|
|||||||
|
|
||||||
async function loadLocations() {
|
async function loadLocations() {
|
||||||
try {
|
try {
|
||||||
const response = await locationsApi.list({ per_page: 100 })
|
const response = await locationsApi.list({ perpage: 100 })
|
||||||
locations.value = response.data.data || []
|
locations.value = response.data.data || []
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error loading locations:', err)
|
console.error('Error loading locations:', err)
|
||||||
@@ -362,7 +361,7 @@ async function loadSubnets() {
|
|||||||
try {
|
try {
|
||||||
const params = {
|
const params = {
|
||||||
page: page.value,
|
page: page.value,
|
||||||
per_page: 20
|
perpage: perPage.value
|
||||||
}
|
}
|
||||||
if (search.value) params.search = search.value
|
if (search.value) params.search = search.value
|
||||||
if (vlanFilter.value) params.vlanid = vlanFilter.value
|
if (vlanFilter.value) params.vlanid = vlanFilter.value
|
||||||
@@ -370,7 +369,7 @@ async function loadSubnets() {
|
|||||||
|
|
||||||
const response = await networkApi.subnets.list(params)
|
const response = await networkApi.subnets.list(params)
|
||||||
subnets.value = response.data.data || []
|
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) {
|
} catch (err) {
|
||||||
console.error('Error loading subnets:', err)
|
console.error('Error loading subnets:', err)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -391,6 +390,12 @@ function goToPage(p) {
|
|||||||
loadSubnets()
|
loadSubnets()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function changePerPage(newPerPage) {
|
||||||
|
perPage.value = newPerPage
|
||||||
|
page.value = 1
|
||||||
|
loadSubnets()
|
||||||
|
}
|
||||||
|
|
||||||
function openModal(subnet = null) {
|
function openModal(subnet = null) {
|
||||||
editingSubnet.value = subnet
|
editingSubnet.value = subnet
|
||||||
if (subnet) {
|
if (subnet) {
|
||||||
|
|||||||
@@ -84,16 +84,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Pagination -->
|
<!-- Pagination -->
|
||||||
<div class="pagination" v-if="totalPages > 1">
|
<PaginationBar
|
||||||
<button
|
:page="page"
|
||||||
v-for="p in totalPages"
|
:totalPages="totalPages"
|
||||||
:key="p"
|
:perPage="perPage"
|
||||||
:class="{ active: p === page }"
|
@update:page="goToPage"
|
||||||
@click="goToPage(p)"
|
@update:perPage="changePerPage"
|
||||||
>
|
/>
|
||||||
{{ p }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -189,6 +186,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { networkApi } from '../../api'
|
import { networkApi } from '../../api'
|
||||||
|
import PaginationBar from '../../components/PaginationBar.vue'
|
||||||
|
|
||||||
const vlans = ref([])
|
const vlans = ref([])
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
@@ -196,6 +194,7 @@ const search = ref('')
|
|||||||
const typeFilter = ref('')
|
const typeFilter = ref('')
|
||||||
const page = ref(1)
|
const page = ref(1)
|
||||||
const totalPages = ref(1)
|
const totalPages = ref(1)
|
||||||
|
const perPage = ref(20)
|
||||||
|
|
||||||
const showModal = ref(false)
|
const showModal = ref(false)
|
||||||
const editingVLAN = ref(null)
|
const editingVLAN = ref(null)
|
||||||
@@ -223,14 +222,14 @@ async function loadVLANs() {
|
|||||||
try {
|
try {
|
||||||
const params = {
|
const params = {
|
||||||
page: page.value,
|
page: page.value,
|
||||||
per_page: 20
|
perpage: perPage.value
|
||||||
}
|
}
|
||||||
if (search.value) params.search = search.value
|
if (search.value) params.search = search.value
|
||||||
if (typeFilter.value) params.type = typeFilter.value
|
if (typeFilter.value) params.type = typeFilter.value
|
||||||
|
|
||||||
const response = await networkApi.vlans.list(params)
|
const response = await networkApi.vlans.list(params)
|
||||||
vlans.value = response.data.data || []
|
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) {
|
} catch (err) {
|
||||||
console.error('Error loading VLANs:', err)
|
console.error('Error loading VLANs:', err)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -251,6 +250,12 @@ function goToPage(p) {
|
|||||||
loadVLANs()
|
loadVLANs()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function changePerPage(newPerPage) {
|
||||||
|
perPage.value = newPerPage
|
||||||
|
page.value = 1
|
||||||
|
loadVLANs()
|
||||||
|
}
|
||||||
|
|
||||||
function getTypeClass(type) {
|
function getTypeClass(type) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'data': return 'badge-info'
|
case 'data': return 'badge-info'
|
||||||
|
|||||||
@@ -17,8 +17,8 @@
|
|||||||
<h1>{{ device.alias || device.machinenumber }}</h1>
|
<h1>{{ device.alias || device.machinenumber }}</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="hero-meta">
|
<div class="hero-meta">
|
||||||
<span class="badge badge-lg" :class="device.is_checked_out ? 'badge-warning' : 'badge-success'">
|
<span class="badge badge-lg" :class="device.ischeckedout ? 'badge-warning' : 'badge-success'">
|
||||||
{{ device.is_checked_out ? 'Checked Out' : 'Available' }}
|
{{ device.ischeckedout ? 'Checked Out' : 'Available' }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="hero-details">
|
<div class="hero-details">
|
||||||
@@ -26,13 +26,13 @@
|
|||||||
<span class="hero-detail-label">Serial Number</span>
|
<span class="hero-detail-label">Serial Number</span>
|
||||||
<span class="hero-detail-value mono">{{ device.serialnumber }}</span>
|
<span class="hero-detail-value mono">{{ device.serialnumber }}</span>
|
||||||
</div>
|
</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-label">Vendor</span>
|
||||||
<span class="hero-detail-value">{{ device.vendor_name }}</span>
|
<span class="hero-detail-value">{{ device.vendorname }}</span>
|
||||||
</div>
|
</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-label">Model</span>
|
||||||
<span class="hero-detail-value">{{ device.model_name }}</span>
|
<span class="hero-detail-value">{{ device.modelname }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
<!-- Action Buttons -->
|
<!-- Action Buttons -->
|
||||||
<div class="action-buttons">
|
<div class="action-buttons">
|
||||||
<button
|
<button
|
||||||
v-if="!device.is_checked_out"
|
v-if="!device.ischeckedout"
|
||||||
class="btn btn-primary btn-lg"
|
class="btn btn-primary btn-lg"
|
||||||
@click="openCheckoutModal"
|
@click="openCheckoutModal"
|
||||||
>
|
>
|
||||||
@@ -57,20 +57,20 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Current Checkout Info -->
|
<!-- 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>
|
<h3 class="section-title">Current Checkout</h3>
|
||||||
<div class="info-list">
|
<div class="info-list">
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="info-label">Checked Out By</span>
|
<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>
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="info-label">Checkout Time</span>
|
<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>
|
||||||
<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-label">Reason</span>
|
||||||
<span class="info-value">{{ device.current_checkout.checkout_reason }}</span>
|
<span class="info-value">{{ device.currentcheckout.checkoutreason }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
<h3>Checkout History</h3>
|
<h3>Checkout History</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="!device.checkout_history?.length" class="empty-state">
|
<div v-if="!device.checkouthistory?.length" class="empty-state">
|
||||||
No checkout history
|
No checkout history
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -97,15 +97,15 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<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>{{ checkout.sso }}</td>
|
||||||
<td>{{ formatDate(checkout.checkout_time) }}</td>
|
<td>{{ formatDate(checkout.checkouttime) }}</td>
|
||||||
<td>{{ checkout.checkin_time ? formatDate(checkout.checkin_time) : 'Still out' }}</td>
|
<td>{{ checkout.checkintime ? formatDate(checkout.checkintime) : 'Still out' }}</td>
|
||||||
<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>
|
<span v-else>-</span>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ checkout.checkout_reason || '-' }}</td>
|
<td>{{ checkout.checkoutreason || '-' }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -146,7 +146,7 @@
|
|||||||
<template #body>
|
<template #body>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="checkbox-label">
|
<label class="checkbox-label">
|
||||||
<input type="checkbox" v-model="checkinForm.was_wiped" />
|
<input type="checkbox" v-model="checkinForm.waswiped" />
|
||||||
Device was wiped
|
Device was wiped
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -177,7 +177,7 @@ const device = ref(null)
|
|||||||
const showCheckoutModal = ref(false)
|
const showCheckoutModal = ref(false)
|
||||||
const showCheckinModal = ref(false)
|
const showCheckinModal = ref(false)
|
||||||
const checkoutForm = ref({ sso: '', reason: '' })
|
const checkoutForm = ref({ sso: '', reason: '' })
|
||||||
const checkinForm = ref({ was_wiped: false, notes: '' })
|
const checkinForm = ref({ waswiped: false, notes: '' })
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await loadDevice()
|
await loadDevice()
|
||||||
@@ -201,7 +201,7 @@ function openCheckoutModal() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openCheckinModal() {
|
function openCheckinModal() {
|
||||||
checkinForm.value = { was_wiped: false, notes: '' }
|
checkinForm.value = { waswiped: false, notes: '' }
|
||||||
showCheckinModal.value = true
|
showCheckinModal.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ onMounted(async () => {
|
|||||||
// Load types and vendors
|
// Load types and vendors
|
||||||
const [typesRes, vendorsRes] = await Promise.all([
|
const [typesRes, vendorsRes] = await Promise.all([
|
||||||
usbApi.types.list(),
|
usbApi.types.list(),
|
||||||
vendorsApi.list({ per_page: 1000 })
|
vendorsApi.list({ perpage: 1000 })
|
||||||
])
|
])
|
||||||
types.value = typesRes.data.data || []
|
types.value = typesRes.data.data || []
|
||||||
vendors.value = vendorsRes.data.data || []
|
vendors.value = vendorsRes.data.data || []
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h2>USB Devices</h2>
|
<h2>USB Devices</h2>
|
||||||
|
<router-link to="/print/usb-labels" class="btn btn-secondary" target="_blank">Print Labels</router-link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Filters -->
|
<!-- Filters -->
|
||||||
@@ -38,24 +39,24 @@
|
|||||||
<tr v-for="device in devices" :key="device.machineid">
|
<tr v-for="device in devices" :key="device.machineid">
|
||||||
<td>
|
<td>
|
||||||
<strong>{{ device.alias || device.machinenumber }}</strong>
|
<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>
|
||||||
<td class="mono">{{ device.serialnumber || '-' }}</td>
|
<td class="mono">{{ device.serialnumber || '-' }}</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="badge" :class="device.is_checked_out ? 'badge-warning' : 'badge-success'">
|
<span class="badge" :class="device.ischeckedout ? 'badge-warning' : 'badge-success'">
|
||||||
{{ device.is_checked_out ? 'Checked Out' : 'Available' }}
|
{{ device.ischeckedout ? 'Checked Out' : 'Available' }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<template v-if="device.current_checkout">
|
<template v-if="device.currentcheckout">
|
||||||
{{ device.current_checkout.checkout_name || device.current_checkout.sso }}
|
{{ device.currentcheckout.checkoutname || device.currentcheckout.sso }}
|
||||||
<div class="text-muted">{{ formatDate(device.current_checkout.checkout_time) }}</div>
|
<div class="text-muted">{{ formatDate(device.currentcheckout.checkouttime) }}</div>
|
||||||
</template>
|
</template>
|
||||||
<span v-else>-</span>
|
<span v-else>-</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="actions">
|
<td class="actions">
|
||||||
<button
|
<button
|
||||||
v-if="!device.is_checked_out"
|
v-if="!device.ischeckedout"
|
||||||
class="btn btn-primary btn-sm"
|
class="btn btn-primary btn-sm"
|
||||||
@click="openCheckoutModal(device)"
|
@click="openCheckoutModal(device)"
|
||||||
>
|
>
|
||||||
@@ -86,16 +87,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Pagination -->
|
<!-- Pagination -->
|
||||||
<div class="pagination" v-if="totalPages > 1">
|
<PaginationBar
|
||||||
<button
|
:page="page"
|
||||||
v-for="p in totalPages"
|
:totalPages="totalPages"
|
||||||
:key="p"
|
:perPage="perPage"
|
||||||
:class="{ active: p === page }"
|
@update:page="goToPage"
|
||||||
@click="goToPage(p)"
|
@update:perPage="changePerPage"
|
||||||
>
|
/>
|
||||||
{{ p }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -127,7 +125,7 @@
|
|||||||
<p>Checking in: <strong>{{ selectedDevice?.alias || selectedDevice?.machinenumber }}</strong></p>
|
<p>Checking in: <strong>{{ selectedDevice?.alias || selectedDevice?.machinenumber }}</strong></p>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="checkbox-label">
|
<label class="checkbox-label">
|
||||||
<input type="checkbox" v-model="checkinForm.was_wiped" />
|
<input type="checkbox" v-model="checkinForm.waswiped" />
|
||||||
Device was wiped
|
Device was wiped
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -148,19 +146,21 @@ import { ref, onMounted } from 'vue'
|
|||||||
import { usbApi } from '../../api'
|
import { usbApi } from '../../api'
|
||||||
import Modal from '../../components/Modal.vue'
|
import Modal from '../../components/Modal.vue'
|
||||||
import EmployeeSearch from '../../components/EmployeeSearch.vue'
|
import EmployeeSearch from '../../components/EmployeeSearch.vue'
|
||||||
|
import PaginationBar from '../../components/PaginationBar.vue'
|
||||||
|
|
||||||
const devices = ref([])
|
const devices = ref([])
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const search = ref('')
|
const search = ref('')
|
||||||
const page = ref(1)
|
const page = ref(1)
|
||||||
const totalPages = ref(1)
|
const totalPages = ref(1)
|
||||||
|
const perPage = ref(20)
|
||||||
const showAvailableOnly = ref(false)
|
const showAvailableOnly = ref(false)
|
||||||
|
|
||||||
const showCheckoutModal = ref(false)
|
const showCheckoutModal = ref(false)
|
||||||
const showCheckinModal = ref(false)
|
const showCheckinModal = ref(false)
|
||||||
const selectedDevice = ref(null)
|
const selectedDevice = ref(null)
|
||||||
const checkoutForm = ref({ reason: '' })
|
const checkoutForm = ref({ reason: '' })
|
||||||
const checkinForm = ref({ was_wiped: false, notes: '' })
|
const checkinForm = ref({ waswiped: false, notes: '' })
|
||||||
const selectedEmployee = ref(null)
|
const selectedEmployee = ref(null)
|
||||||
|
|
||||||
let searchTimeout = null
|
let searchTimeout = null
|
||||||
@@ -174,14 +174,14 @@ async function loadDevices() {
|
|||||||
try {
|
try {
|
||||||
const params = {
|
const params = {
|
||||||
page: page.value,
|
page: page.value,
|
||||||
perpage: 20
|
perpage: perPage.value
|
||||||
}
|
}
|
||||||
if (search.value) params.search = search.value
|
if (search.value) params.search = search.value
|
||||||
if (showAvailableOnly.value) params.available = 'true'
|
if (showAvailableOnly.value) params.available = 'true'
|
||||||
|
|
||||||
const response = await usbApi.list(params)
|
const response = await usbApi.list(params)
|
||||||
devices.value = response.data.data || []
|
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) {
|
} catch (error) {
|
||||||
console.error('Error loading USB devices:', error)
|
console.error('Error loading USB devices:', error)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -202,6 +202,12 @@ function goToPage(p) {
|
|||||||
loadDevices()
|
loadDevices()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function changePerPage(newPerPage) {
|
||||||
|
perPage.value = newPerPage
|
||||||
|
page.value = 1
|
||||||
|
loadDevices()
|
||||||
|
}
|
||||||
|
|
||||||
function openCheckoutModal(device) {
|
function openCheckoutModal(device) {
|
||||||
selectedDevice.value = device
|
selectedDevice.value = device
|
||||||
checkoutForm.value = { reason: '' }
|
checkoutForm.value = { reason: '' }
|
||||||
@@ -211,7 +217,7 @@ function openCheckoutModal(device) {
|
|||||||
|
|
||||||
function openCheckinModal(device) {
|
function openCheckinModal(device) {
|
||||||
selectedDevice.value = device
|
selectedDevice.value = device
|
||||||
checkinForm.value = { was_wiped: false, notes: '' }
|
checkinForm.value = { waswiped: false, notes: '' }
|
||||||
showCheckinModal.value = true
|
showCheckinModal.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
29
frontend/src/views/vendors/VendorsList.vue
vendored
29
frontend/src/views/vendors/VendorsList.vue
vendored
@@ -62,16 +62,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Pagination -->
|
<!-- Pagination -->
|
||||||
<div class="pagination" v-if="totalPages > 1">
|
<PaginationBar
|
||||||
<button
|
:page="page"
|
||||||
v-for="p in totalPages"
|
:totalPages="totalPages"
|
||||||
:key="p"
|
:perPage="perPage"
|
||||||
:class="{ active: p === page }"
|
@update:page="goToPage"
|
||||||
@click="goToPage(p)"
|
@update:perPage="changePerPage"
|
||||||
>
|
/>
|
||||||
{{ p }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -188,12 +185,14 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { vendorsApi } from '../../api'
|
import { vendorsApi } from '../../api'
|
||||||
|
import PaginationBar from '../../components/PaginationBar.vue'
|
||||||
|
|
||||||
const vendors = ref([])
|
const vendors = ref([])
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const search = ref('')
|
const search = ref('')
|
||||||
const page = ref(1)
|
const page = ref(1)
|
||||||
const totalPages = ref(1)
|
const totalPages = ref(1)
|
||||||
|
const perPage = ref(20)
|
||||||
|
|
||||||
const showModal = ref(false)
|
const showModal = ref(false)
|
||||||
const editingVendor = ref(null)
|
const editingVendor = ref(null)
|
||||||
@@ -224,13 +223,13 @@ async function loadVendors() {
|
|||||||
try {
|
try {
|
||||||
const params = {
|
const params = {
|
||||||
page: page.value,
|
page: page.value,
|
||||||
perpage: 20
|
perpage: perPage.value
|
||||||
}
|
}
|
||||||
if (search.value) params.search = search.value
|
if (search.value) params.search = search.value
|
||||||
|
|
||||||
const response = await vendorsApi.list(params)
|
const response = await vendorsApi.list(params)
|
||||||
vendors.value = response.data.data || []
|
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) {
|
} catch (err) {
|
||||||
console.error('Error loading vendors:', err)
|
console.error('Error loading vendors:', err)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -251,6 +250,12 @@ function goToPage(p) {
|
|||||||
loadVendors()
|
loadVendors()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function changePerPage(newPerPage) {
|
||||||
|
perPage.value = newPerPage
|
||||||
|
page.value = 1
|
||||||
|
loadVendors()
|
||||||
|
}
|
||||||
|
|
||||||
function openModal(vendor = null) {
|
function openModal(vendor = null) {
|
||||||
editingVendor.value = vendor
|
editingVendor.value = vendor
|
||||||
if (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({
|
return success_response({
|
||||||
'total': total,
|
'total': total,
|
||||||
'by_type': [{'type': t, 'count': c} for t, c in by_type],
|
'bytype': [{'type': t, 'count': c} for t, c in by_type],
|
||||||
'by_os': [{'os': o, 'count': c} for o, c in by_os],
|
'byos': [{'os': o, 'count': c} for o, c in by_os],
|
||||||
'shopfloor': shopfloor_count,
|
'shopfloor': shopfloor_count,
|
||||||
'non_shopfloor': total - shopfloor_count
|
'nonshopfloor': total - shopfloor_count
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -117,9 +117,9 @@ class Computer(BaseModel):
|
|||||||
|
|
||||||
# Add related object names
|
# Add related object names
|
||||||
if self.computertype:
|
if self.computertype:
|
||||||
result['computertype_name'] = self.computertype.computertype
|
result['computertypename'] = self.computertype.computertype
|
||||||
if self.operatingsystem:
|
if self.operatingsystem:
|
||||||
result['os_name'] = self.operatingsystem.osname
|
result['osname'] = self.operatingsystem.osname
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|||||||
@@ -78,8 +78,8 @@ class ComputersPlugin(BasePlugin):
|
|||||||
if not existing:
|
if not existing:
|
||||||
at = AssetType(
|
at = AssetType(
|
||||||
assettype='computer',
|
assettype='computer',
|
||||||
plugin_name='computers',
|
pluginname='computers',
|
||||||
table_name='computers',
|
tablename='computers',
|
||||||
description='PCs, servers, and workstations',
|
description='PCs, servers, and workstations',
|
||||||
icon='desktop'
|
icon='desktop'
|
||||||
)
|
)
|
||||||
@@ -201,9 +201,9 @@ class ComputersPlugin(BasePlugin):
|
|||||||
"""Return navigation menu items."""
|
"""Return navigation menu items."""
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
'name': 'Computers',
|
'name': 'PCs',
|
||||||
'icon': 'desktop',
|
'icon': 'desktop',
|
||||||
'route': '/computers',
|
'route': '/pcs',
|
||||||
'position': 15,
|
'position': 15,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -306,8 +306,8 @@ def create_equipment():
|
|||||||
lastmaintenancedate=data.get('lastmaintenancedate'),
|
lastmaintenancedate=data.get('lastmaintenancedate'),
|
||||||
nextmaintenancedate=data.get('nextmaintenancedate'),
|
nextmaintenancedate=data.get('nextmaintenancedate'),
|
||||||
maintenanceintervaldays=data.get('maintenanceintervaldays'),
|
maintenanceintervaldays=data.get('maintenanceintervaldays'),
|
||||||
controller_vendorid=data.get('controller_vendorid'),
|
controllervendorid=data.get('controllervendorid'),
|
||||||
controller_modelid=data.get('controller_modelid')
|
controllermodelid=data.get('controllermodelid')
|
||||||
)
|
)
|
||||||
|
|
||||||
db.session.add(equip)
|
db.session.add(equip)
|
||||||
@@ -358,7 +358,7 @@ def update_equipment(equipment_id: int):
|
|||||||
equipment_fields = ['equipmenttypeid', 'vendorid', 'modelnumberid',
|
equipment_fields = ['equipmenttypeid', 'vendorid', 'modelnumberid',
|
||||||
'requiresmanualconfig', 'islocationonly',
|
'requiresmanualconfig', 'islocationonly',
|
||||||
'lastmaintenancedate', 'nextmaintenancedate', 'maintenanceintervaldays',
|
'lastmaintenancedate', 'nextmaintenancedate', 'maintenanceintervaldays',
|
||||||
'controller_vendorid', 'controller_modelid']
|
'controllervendorid', 'controllermodelid']
|
||||||
for key in equipment_fields:
|
for key in equipment_fields:
|
||||||
if key in data:
|
if key in data:
|
||||||
setattr(equip, key, data[key])
|
setattr(equip, key, data[key])
|
||||||
@@ -427,6 +427,6 @@ def dashboard_summary():
|
|||||||
|
|
||||||
return success_response({
|
return success_response({
|
||||||
'total': total,
|
'total': total,
|
||||||
'by_type': [{'type': t, 'count': c} for t, c in by_type],
|
'bytype': [{'type': t, 'count': c} for t, c in by_type],
|
||||||
'by_status': [{'status': s, 'count': c} for s, c in by_status]
|
'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)
|
maintenanceintervaldays = db.Column(db.Integer, nullable=True)
|
||||||
|
|
||||||
# Controller info (for CNC machines)
|
# Controller info (for CNC machines)
|
||||||
controller_vendorid = db.Column(
|
controllervendorid = db.Column(
|
||||||
db.Integer,
|
db.Integer,
|
||||||
db.ForeignKey('vendors.vendorid'),
|
db.ForeignKey('vendors.vendorid'),
|
||||||
nullable=True,
|
nullable=True,
|
||||||
comment='Controller vendor (e.g., FANUC)'
|
comment='Controller vendor (e.g., FANUC)'
|
||||||
)
|
)
|
||||||
controller_modelid = db.Column(
|
controllermodelid = db.Column(
|
||||||
db.Integer,
|
db.Integer,
|
||||||
db.ForeignKey('models.modelnumberid'),
|
db.ForeignKey('models.modelnumberid'),
|
||||||
nullable=True,
|
nullable=True,
|
||||||
@@ -99,8 +99,8 @@ class Equipment(BaseModel):
|
|||||||
equipmenttype = db.relationship('EquipmentType', backref='equipment')
|
equipmenttype = db.relationship('EquipmentType', backref='equipment')
|
||||||
vendor = db.relationship('Vendor', foreign_keys=[vendorid], backref='equipment_items')
|
vendor = db.relationship('Vendor', foreign_keys=[vendorid], backref='equipment_items')
|
||||||
model = db.relationship('Model', foreign_keys=[modelnumberid], 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')
|
controllervendor = db.relationship('Vendor', foreign_keys=[controllervendorid], backref='equipment_controllers')
|
||||||
controller_model = db.relationship('Model', foreign_keys=[controller_modelid], backref='equipment_controller_models')
|
controllermodel = db.relationship('Model', foreign_keys=[controllermodelid], backref='equipment_controller_models')
|
||||||
|
|
||||||
__table_args__ = (
|
__table_args__ = (
|
||||||
db.Index('idx_equipment_type', 'equipmenttypeid'),
|
db.Index('idx_equipment_type', 'equipmenttypeid'),
|
||||||
@@ -116,16 +116,18 @@ class Equipment(BaseModel):
|
|||||||
|
|
||||||
# Add related object names
|
# Add related object names
|
||||||
if self.equipmenttype:
|
if self.equipmenttype:
|
||||||
result['equipmenttype_name'] = self.equipmenttype.equipmenttype
|
result['equipmenttypename'] = self.equipmenttype.equipmenttype
|
||||||
if self.vendor:
|
if self.vendor:
|
||||||
result['vendor_name'] = self.vendor.vendor
|
result['vendorname'] = self.vendor.vendor
|
||||||
if self.model:
|
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
|
# Add controller info
|
||||||
if self.controller_vendor:
|
if self.controllervendor:
|
||||||
result['controller_vendor_name'] = self.controller_vendor.vendor
|
result['controllervendorname'] = self.controllervendor.vendor
|
||||||
if self.controller_model:
|
if self.controllermodel:
|
||||||
result['controller_model_name'] = self.controller_model.modelnumber
|
result['controllermodelname'] = self.controllermodel.modelnumber
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -79,8 +79,8 @@ class EquipmentPlugin(BasePlugin):
|
|||||||
if not existing:
|
if not existing:
|
||||||
at = AssetType(
|
at = AssetType(
|
||||||
assettype='equipment',
|
assettype='equipment',
|
||||||
plugin_name='equipment',
|
pluginname='equipment',
|
||||||
table_name='equipment',
|
tablename='equipment',
|
||||||
description='Manufacturing equipment (CNCs, CMMs, lathes, etc.)',
|
description='Manufacturing equipment (CNCs, CMMs, lathes, etc.)',
|
||||||
icon='cog'
|
icon='cog'
|
||||||
)
|
)
|
||||||
@@ -214,7 +214,7 @@ class EquipmentPlugin(BasePlugin):
|
|||||||
{
|
{
|
||||||
'name': 'Equipment',
|
'name': 'Equipment',
|
||||||
'icon': 'cog',
|
'icon': 'cog',
|
||||||
'route': '/equipment',
|
'route': '/machines',
|
||||||
'position': 10,
|
'position': 10,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -227,7 +227,7 @@ def get_network_device(device_id: int):
|
|||||||
)
|
)
|
||||||
|
|
||||||
result = netdev.asset.to_dict() if netdev.asset else {}
|
result = netdev.asset.to_dict() if netdev.asset else {}
|
||||||
result['network_device'] = netdev.to_dict()
|
result['networkdevice'] = netdev.to_dict()
|
||||||
|
|
||||||
return success_response(result)
|
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 = netdev.asset.to_dict() if netdev.asset else {}
|
||||||
result['network_device'] = netdev.to_dict()
|
result['networkdevice'] = netdev.to_dict()
|
||||||
|
|
||||||
return success_response(result)
|
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 = netdev.asset.to_dict() if netdev.asset else {}
|
||||||
result['network_device'] = netdev.to_dict()
|
result['networkdevice'] = netdev.to_dict()
|
||||||
|
|
||||||
return success_response(result)
|
return success_response(result)
|
||||||
|
|
||||||
@@ -353,7 +353,7 @@ def create_network_device():
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
result = asset.to_dict()
|
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)
|
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()
|
db.session.commit()
|
||||||
|
|
||||||
result = asset.to_dict()
|
result = asset.to_dict()
|
||||||
result['network_device'] = netdev.to_dict()
|
result['networkdevice'] = netdev.to_dict()
|
||||||
|
|
||||||
return success_response(result, message='Network device updated')
|
return success_response(result, message='Network device updated')
|
||||||
|
|
||||||
@@ -479,10 +479,10 @@ def dashboard_summary():
|
|||||||
|
|
||||||
return success_response({
|
return success_response({
|
||||||
'total': total,
|
'total': total,
|
||||||
'by_type': [{'type': t, 'count': c} for t, c in by_type],
|
'bytype': [{'type': t, 'count': c} for t, c in by_type],
|
||||||
'by_vendor': [{'vendor': v, 'count': c} for v, c in by_vendor],
|
'byvendor': [{'vendor': v, 'count': c} for v, c in by_vendor],
|
||||||
'poe': poe_count,
|
'poe': poe_count,
|
||||||
'non_poe': total - poe_count
|
'nonpoe': total - poe_count
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -114,8 +114,8 @@ class NetworkDevice(BaseModel):
|
|||||||
|
|
||||||
# Add related object names
|
# Add related object names
|
||||||
if self.networkdevicetype:
|
if self.networkdevicetype:
|
||||||
result['networkdevicetype_name'] = self.networkdevicetype.networkdevicetype
|
result['networkdevicetypename'] = self.networkdevicetype.networkdevicetype
|
||||||
if self.vendor:
|
if self.vendor:
|
||||||
result['vendor_name'] = self.vendor.vendor
|
result['vendorname'] = self.vendor.vendor
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -78,8 +78,8 @@ class NetworkPlugin(BasePlugin):
|
|||||||
if not existing:
|
if not existing:
|
||||||
at = AssetType(
|
at = AssetType(
|
||||||
assettype='network_device',
|
assettype='network_device',
|
||||||
plugin_name='network',
|
pluginname='network',
|
||||||
table_name='networkdevices',
|
tablename='networkdevices',
|
||||||
description='Network infrastructure devices (switches, APs, cameras, etc.)',
|
description='Network infrastructure devices (switches, APs, cameras, etc.)',
|
||||||
icon='network-wired'
|
icon='network-wired'
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -374,7 +374,7 @@ def dashboard_summary():
|
|||||||
|
|
||||||
return success_response({
|
return success_response({
|
||||||
'active': total_active,
|
'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'])
|
@printers_asset_bp.route('/types', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required(optional=True)
|
||||||
def list_printer_types():
|
def list_printer_types():
|
||||||
"""List all printer types."""
|
"""List all printer types."""
|
||||||
page, per_page = get_pagination_params(request)
|
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'])
|
@printers_asset_bp.route('/types/<int:type_id>', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required(optional=True)
|
||||||
def get_printer_type(type_id: int):
|
def get_printer_type(type_id: int):
|
||||||
"""Get a single printer type."""
|
"""Get a single printer type."""
|
||||||
t = PrinterType.query.get(type_id)
|
t = PrinterType.query.get(type_id)
|
||||||
@@ -471,6 +471,6 @@ def dashboard_summary():
|
|||||||
'online': total, # Placeholder - would need monitoring integration
|
'online': total, # Placeholder - would need monitoring integration
|
||||||
'lowsupplies': 0, # Placeholder - would need Zabbix integration
|
'lowsupplies': 0, # Placeholder - would need Zabbix integration
|
||||||
'criticalsupplies': 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],
|
'bytype': [{'type': t, 'count': c} for t, c in by_type],
|
||||||
'by_vendor': [{'vendor': v, 'count': c} for v, c in by_vendor],
|
'byvendor': [{'vendor': v, 'count': c} for v, c in by_vendor],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -113,10 +113,10 @@ class Printer(BaseModel):
|
|||||||
|
|
||||||
# Add related object names
|
# Add related object names
|
||||||
if self.printertype:
|
if self.printertype:
|
||||||
result['printertype_name'] = self.printertype.printertype
|
result['printertypename'] = self.printertype.printertype
|
||||||
if self.vendor:
|
if self.vendor:
|
||||||
result['vendor_name'] = self.vendor.vendor
|
result['vendorname'] = self.vendor.vendor
|
||||||
if self.model:
|
if self.model:
|
||||||
result['model_name'] = self.model.modelnumber
|
result['modelname'] = self.model.modelnumber
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -116,8 +116,8 @@ class PrintersPlugin(BasePlugin):
|
|||||||
if not existing:
|
if not existing:
|
||||||
at = AssetType(
|
at = AssetType(
|
||||||
assettype='printer',
|
assettype='printer',
|
||||||
plugin_name='printers',
|
pluginname='printers',
|
||||||
table_name='printers',
|
tablename='printers',
|
||||||
description='Printers (laser, inkjet, label, MFP, plotter)',
|
description='Printers (laser, inkjet, label, MFP, plotter)',
|
||||||
icon='printer'
|
icon='printer'
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -161,10 +161,10 @@ def get_usb_device(device_id: int):
|
|||||||
# Get recent checkout history
|
# Get recent checkout history
|
||||||
checkouts = USBCheckout.query.filter_by(
|
checkouts = USBCheckout.query.filter_by(
|
||||||
usbdeviceid=device_id
|
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 = 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)
|
return success_response(result)
|
||||||
|
|
||||||
@@ -258,16 +258,16 @@ def checkout_device(device_id: int):
|
|||||||
usbdeviceid=device_id,
|
usbdeviceid=device_id,
|
||||||
machineid=0, # Legacy field, set to 0 for new checkouts
|
machineid=0, # Legacy field, set to 0 for new checkouts
|
||||||
sso=data['sso'],
|
sso=data['sso'],
|
||||||
checkout_name=data.get('checkout_name'),
|
checkoutname=data.get('checkoutname'),
|
||||||
checkout_time=datetime.utcnow(),
|
checkouttime=datetime.utcnow(),
|
||||||
checkout_reason=data.get('checkout_reason'),
|
checkoutreason=data.get('checkoutreason'),
|
||||||
was_wiped=False
|
waswiped=False
|
||||||
)
|
)
|
||||||
|
|
||||||
# Update device status
|
# Update device status
|
||||||
device.ischeckedout = True
|
device.ischeckedout = True
|
||||||
device.currentuserid = data['sso']
|
device.currentuserid = data['sso']
|
||||||
device.currentusername = data.get('checkout_name')
|
device.currentusername = data.get('checkoutname')
|
||||||
device.currentcheckoutdate = datetime.utcnow()
|
device.currentcheckoutdate = datetime.utcnow()
|
||||||
device.modifieddate = datetime.utcnow()
|
device.modifieddate = datetime.utcnow()
|
||||||
|
|
||||||
@@ -300,15 +300,15 @@ def checkin_device(device_id: int):
|
|||||||
# Find active checkout
|
# Find active checkout
|
||||||
active_checkout = USBCheckout.query.filter_by(
|
active_checkout = USBCheckout.query.filter_by(
|
||||||
usbdeviceid=device_id,
|
usbdeviceid=device_id,
|
||||||
checkin_time=None
|
checkintime=None
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
data = request.get_json() or {}
|
data = request.get_json() or {}
|
||||||
|
|
||||||
if active_checkout:
|
if active_checkout:
|
||||||
active_checkout.checkin_time = datetime.utcnow()
|
active_checkout.checkintime = datetime.utcnow()
|
||||||
active_checkout.checkin_notes = data.get('checkin_notes', active_checkout.checkin_notes)
|
active_checkout.checkinnotes = data.get('checkinnotes', active_checkout.checkinnotes)
|
||||||
active_checkout.was_wiped = data.get('was_wiped', False)
|
active_checkout.waswiped = data.get('waswiped', False)
|
||||||
|
|
||||||
# Update device status
|
# Update device status
|
||||||
device.ischeckedout = False
|
device.ischeckedout = False
|
||||||
@@ -337,7 +337,7 @@ def get_device_history(device_id: int):
|
|||||||
|
|
||||||
query = USBCheckout.query.filter_by(
|
query = USBCheckout.query.filter_by(
|
||||||
usbdeviceid=device_id
|
usbdeviceid=device_id
|
||||||
).order_by(USBCheckout.checkout_time.desc())
|
).order_by(USBCheckout.checkouttime.desc())
|
||||||
|
|
||||||
items, total = paginate_query(query, page, per_page)
|
items, total = paginate_query(query, page, per_page)
|
||||||
data = [c.to_dict() for c in items]
|
data = [c.to_dict() for c in items]
|
||||||
@@ -361,13 +361,13 @@ def list_all_checkouts():
|
|||||||
|
|
||||||
# Filter by active only
|
# Filter by active only
|
||||||
if request.args.get('active', '').lower() == 'true':
|
if request.args.get('active', '').lower() == 'true':
|
||||||
query = query.filter(USBCheckout.checkin_time == None)
|
query = query.filter(USBCheckout.checkintime == None)
|
||||||
|
|
||||||
# Filter by user
|
# Filter by user
|
||||||
if sso := request.args.get('sso'):
|
if sso := request.args.get('sso'):
|
||||||
query = query.filter(USBCheckout.sso == 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)
|
items, total = paginate_query(query, page, per_page)
|
||||||
data = [c.to_dict() for c in items]
|
data = [c.to_dict() for c in items]
|
||||||
@@ -380,7 +380,7 @@ def list_all_checkouts():
|
|||||||
def list_active_checkouts():
|
def list_active_checkouts():
|
||||||
"""List all currently active checkouts."""
|
"""List all currently active checkouts."""
|
||||||
checkouts = USBCheckout.query.filter(
|
checkouts = USBCheckout.query.filter(
|
||||||
USBCheckout.checkin_time == None
|
USBCheckout.checkintime == None
|
||||||
).order_by(USBCheckout.checkout_time.desc()).all()
|
).order_by(USBCheckout.checkouttime.desc()).all()
|
||||||
|
|
||||||
return success_response([c.to_dict() for c in checkouts])
|
return success_response([c.to_dict() for c in checkouts])
|
||||||
|
|||||||
@@ -123,16 +123,16 @@ class USBCheckout(BaseModel):
|
|||||||
|
|
||||||
# User info
|
# User info
|
||||||
sso = db.Column(db.String(20), nullable=False, comment='SSO of user')
|
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 details
|
||||||
checkout_time = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
|
checkouttime = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
|
||||||
checkin_time = db.Column(db.DateTime, nullable=True)
|
checkintime = db.Column(db.DateTime, nullable=True)
|
||||||
|
|
||||||
# Metadata
|
# Metadata
|
||||||
checkout_reason = db.Column(db.Text, nullable=True, comment='Reason for checkout')
|
checkoutreason = db.Column(db.Text, nullable=True, comment='Reason for checkout')
|
||||||
checkin_notes = db.Column(db.Text, nullable=True)
|
checkinnotes = db.Column(db.Text, nullable=True)
|
||||||
was_wiped = db.Column(db.Boolean, nullable=True, comment='Was device wiped after return')
|
waswiped = db.Column(db.Boolean, nullable=True, comment='Was device wiped after return')
|
||||||
|
|
||||||
# Relationships
|
# Relationships
|
||||||
device = db.relationship('USBDevice', backref=db.backref('checkouts', lazy='dynamic'))
|
device = db.relationship('USBDevice', backref=db.backref('checkouts', lazy='dynamic'))
|
||||||
@@ -143,13 +143,13 @@ class USBCheckout(BaseModel):
|
|||||||
@property
|
@property
|
||||||
def is_active(self):
|
def is_active(self):
|
||||||
"""Check if this checkout is currently active (not returned)."""
|
"""Check if this checkout is currently active (not returned)."""
|
||||||
return self.checkin_time is None
|
return self.checkintime is None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def duration_days(self):
|
def duration_days(self):
|
||||||
"""Get duration of checkout in days."""
|
"""Get duration of checkout in days."""
|
||||||
end = self.checkin_time or datetime.utcnow()
|
end = self.checkintime or datetime.utcnow()
|
||||||
delta = end - self.checkout_time
|
delta = end - self.checkouttime
|
||||||
return delta.days
|
return delta.days
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ assets_bp = Blueprint('assets', __name__)
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
@assets_bp.route('/types', methods=['GET'])
|
@assets_bp.route('/types', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required(optional=True)
|
||||||
def list_asset_types():
|
def list_asset_types():
|
||||||
"""List all asset types."""
|
"""List all asset types."""
|
||||||
page, per_page = get_pagination_params(request)
|
page, per_page = get_pagination_params(request)
|
||||||
@@ -41,7 +41,7 @@ def list_asset_types():
|
|||||||
|
|
||||||
|
|
||||||
@assets_bp.route('/types/<int:type_id>', methods=['GET'])
|
@assets_bp.route('/types/<int:type_id>', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required(optional=True)
|
||||||
def get_asset_type(type_id: int):
|
def get_asset_type(type_id: int):
|
||||||
"""Get a single asset type."""
|
"""Get a single asset type."""
|
||||||
t = AssetType.query.get(type_id)
|
t = AssetType.query.get(type_id)
|
||||||
@@ -91,7 +91,7 @@ def create_asset_type():
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
@assets_bp.route('/statuses', methods=['GET'])
|
@assets_bp.route('/statuses', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required(optional=True)
|
||||||
def list_asset_statuses():
|
def list_asset_statuses():
|
||||||
"""List all asset statuses."""
|
"""List all asset statuses."""
|
||||||
page, per_page = get_pagination_params(request)
|
page, per_page = get_pagination_params(request)
|
||||||
@@ -110,7 +110,7 @@ def list_asset_statuses():
|
|||||||
|
|
||||||
|
|
||||||
@assets_bp.route('/statuses/<int:status_id>', methods=['GET'])
|
@assets_bp.route('/statuses/<int:status_id>', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required(optional=True)
|
||||||
def get_asset_status(status_id: int):
|
def get_asset_status(status_id: int):
|
||||||
"""Get a single asset status."""
|
"""Get a single asset status."""
|
||||||
s = AssetStatus.query.get(status_id)
|
s = AssetStatus.query.get(status_id)
|
||||||
@@ -158,7 +158,7 @@ def create_asset_status():
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
@assets_bp.route('', methods=['GET'])
|
@assets_bp.route('', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required(optional=True)
|
||||||
def list_assets():
|
def list_assets():
|
||||||
"""
|
"""
|
||||||
List all assets with filtering and pagination.
|
List all assets with filtering and pagination.
|
||||||
@@ -240,7 +240,7 @@ def list_assets():
|
|||||||
|
|
||||||
|
|
||||||
@assets_bp.route('/<int:asset_id>', methods=['GET'])
|
@assets_bp.route('/<int:asset_id>', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required(optional=True)
|
||||||
def get_asset(asset_id: int):
|
def get_asset(asset_id: int):
|
||||||
"""
|
"""
|
||||||
Get a single asset with full details.
|
Get a single asset with full details.
|
||||||
@@ -370,7 +370,7 @@ def delete_asset(asset_id: int):
|
|||||||
|
|
||||||
|
|
||||||
@assets_bp.route('/lookup/<assetnumber>', methods=['GET'])
|
@assets_bp.route('/lookup/<assetnumber>', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required(optional=True)
|
||||||
def lookup_asset_by_number(assetnumber: str):
|
def lookup_asset_by_number(assetnumber: str):
|
||||||
"""
|
"""
|
||||||
Look up an asset by its asset number.
|
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'])
|
@assets_bp.route('/<int:asset_id>/communications', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required(optional=True)
|
||||||
def get_asset_communications(asset_id: int):
|
def get_asset_communications(asset_id: int):
|
||||||
"""Get all communications for an asset."""
|
"""Get all communications for an asset."""
|
||||||
from shopdb.core.models import Communication
|
from shopdb.core.models import Communication
|
||||||
|
|||||||
@@ -276,7 +276,7 @@ def pc_heartbeat():
|
|||||||
|
|
||||||
return success_response({
|
return success_response({
|
||||||
'updated': updated,
|
'updated': updated,
|
||||||
'not_found': not_found,
|
'notfound': not_found,
|
||||||
'timestamp': datetime.utcnow().isoformat()
|
'timestamp': datetime.utcnow().isoformat()
|
||||||
}, message=f'{updated} PC(s) heartbeat recorded')
|
}, message=f'{updated} PC(s) heartbeat recorded')
|
||||||
|
|
||||||
@@ -351,7 +351,7 @@ def bulk_update():
|
|||||||
|
|
||||||
return success_response({
|
return success_response({
|
||||||
'updated': updated,
|
'updated': updated,
|
||||||
'not_found': not_found,
|
'notfound': not_found,
|
||||||
'errors': errors,
|
'errors': errors,
|
||||||
'timestamp': datetime.utcnow().isoformat()
|
'timestamp': datetime.utcnow().isoformat()
|
||||||
}, message=f'{updated} PC(s) updated')
|
}, message=f'{updated} PC(s) updated')
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""Dashboard API endpoints."""
|
"""Dashboard API endpoints."""
|
||||||
|
|
||||||
from flask import Blueprint
|
from flask import Blueprint, current_app
|
||||||
from flask_jwt_extended import jwt_required
|
from flask_jwt_extended import jwt_required
|
||||||
|
|
||||||
from shopdb.extensions import db
|
from shopdb.extensions import db
|
||||||
@@ -60,10 +60,10 @@ def get_dashboard():
|
|||||||
'counts': {
|
'counts': {
|
||||||
'equipment': equipment_count,
|
'equipment': equipment_count,
|
||||||
'pcs': pc_count,
|
'pcs': pc_count,
|
||||||
'network_devices': network_count,
|
'networkdevices': network_count,
|
||||||
'total': equipment_count + pc_count + network_count
|
'total': equipment_count + pc_count + network_count
|
||||||
},
|
},
|
||||||
'by_status': status_dict,
|
'bystatus': status_dict,
|
||||||
'recent': [
|
'recent': [
|
||||||
{
|
{
|
||||||
'machineid': m.machineid,
|
'machineid': m.machineid,
|
||||||
@@ -93,13 +93,51 @@ def get_stats():
|
|||||||
).all()
|
).all()
|
||||||
|
|
||||||
return success_response({
|
return success_response({
|
||||||
'by_type': [
|
'bytype': [
|
||||||
{'type': t, 'category': c, 'count': count}
|
{'type': t, 'category': c, 'count': count}
|
||||||
for t, c, count in type_counts
|
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'])
|
@dashboard_bp.route('/health', methods=['GET'])
|
||||||
def health_check():
|
def health_check():
|
||||||
"""Health check endpoint (no auth required)."""
|
"""Health check endpoint (no auth required)."""
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ locations_bp = Blueprint('locations', __name__)
|
|||||||
|
|
||||||
|
|
||||||
@locations_bp.route('', methods=['GET'])
|
@locations_bp.route('', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required(optional=True)
|
||||||
def list_locations():
|
def list_locations():
|
||||||
"""List all locations."""
|
"""List all locations."""
|
||||||
page, per_page = get_pagination_params(request)
|
page, per_page = get_pagination_params(request)
|
||||||
@@ -44,7 +44,7 @@ def list_locations():
|
|||||||
|
|
||||||
|
|
||||||
@locations_bp.route('/<int:location_id>', methods=['GET'])
|
@locations_bp.route('/<int:location_id>', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required(optional=True)
|
||||||
def get_location(location_id: int):
|
def get_location(location_id: int):
|
||||||
"""Get a single location."""
|
"""Get a single location."""
|
||||||
loc = Location.query.get(location_id)
|
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'])
|
@machines_bp.route('/<int:machine_id>/communications', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required(optional=True)
|
||||||
@add_deprecation_headers
|
@add_deprecation_headers
|
||||||
def get_machine_communications(machine_id: int):
|
def get_machine_communications(machine_id: int):
|
||||||
"""Get all communications for a machine."""
|
"""Get all communications for a machine."""
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ def list_models():
|
|||||||
|
|
||||||
|
|
||||||
@models_bp.route('/<int:model_id>', methods=['GET'])
|
@models_bp.route('/<int:model_id>', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required(optional=True)
|
||||||
def get_model(model_id: int):
|
def get_model(model_id: int):
|
||||||
"""Get a single model."""
|
"""Get a single model."""
|
||||||
m = Model.query.get(model_id)
|
m = Model.query.get(model_id)
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ operatingsystems_bp = Blueprint('operatingsystems', __name__)
|
|||||||
|
|
||||||
|
|
||||||
@operatingsystems_bp.route('', methods=['GET'])
|
@operatingsystems_bp.route('', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required(optional=True)
|
||||||
def list_operatingsystems():
|
def list_operatingsystems():
|
||||||
"""List all operating systems."""
|
"""List all operating systems."""
|
||||||
page, per_page = get_pagination_params(request)
|
page, per_page = get_pagination_params(request)
|
||||||
@@ -39,7 +39,7 @@ def list_operatingsystems():
|
|||||||
|
|
||||||
|
|
||||||
@operatingsystems_bp.route('/<int:os_id>', methods=['GET'])
|
@operatingsystems_bp.route('/<int:os_id>', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required(optional=True)
|
||||||
def get_operatingsystem(os_id: int):
|
def get_operatingsystem(os_id: int):
|
||||||
"""Get a single operating system."""
|
"""Get a single operating system."""
|
||||||
os = OperatingSystem.query.get(os_id)
|
os = OperatingSystem.query.get(os_id)
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ pctypes_bp = Blueprint('pctypes', __name__)
|
|||||||
|
|
||||||
|
|
||||||
@pctypes_bp.route('', methods=['GET'])
|
@pctypes_bp.route('', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required(optional=True)
|
||||||
def list_pctypes():
|
def list_pctypes():
|
||||||
"""List all PC types."""
|
"""List all PC types."""
|
||||||
page, per_page = get_pagination_params(request)
|
page, per_page = get_pagination_params(request)
|
||||||
@@ -39,7 +39,7 @@ def list_pctypes():
|
|||||||
|
|
||||||
|
|
||||||
@pctypes_bp.route('/<int:type_id>', methods=['GET'])
|
@pctypes_bp.route('/<int:type_id>', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required(optional=True)
|
||||||
def get_pctype(type_id: int):
|
def get_pctype(type_id: int):
|
||||||
"""Get a single PC type."""
|
"""Get a single PC type."""
|
||||||
pt = PCType.query.get(type_id)
|
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'])
|
@reports_bp.route('/equipment-by-type', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required(optional=True)
|
||||||
def equipment_by_type():
|
def equipment_by_type():
|
||||||
"""
|
"""
|
||||||
Report: Equipment count grouped by equipment type.
|
Report: Equipment count grouped by equipment type.
|
||||||
@@ -95,7 +95,7 @@ def equipment_by_type():
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
@reports_bp.route('/assets-by-status', methods=['GET'])
|
@reports_bp.route('/assets-by-status', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required(optional=True)
|
||||||
def assets_by_status():
|
def assets_by_status():
|
||||||
"""
|
"""
|
||||||
Report: Asset count grouped by status.
|
Report: Asset count grouped by status.
|
||||||
@@ -154,7 +154,7 @@ def assets_by_status():
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
@reports_bp.route('/kb-popularity', methods=['GET'])
|
@reports_bp.route('/kb-popularity', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required(optional=True)
|
||||||
def kb_popularity():
|
def kb_popularity():
|
||||||
"""
|
"""
|
||||||
Report: Most clicked knowledge base articles.
|
Report: Most clicked knowledge base articles.
|
||||||
@@ -202,7 +202,7 @@ def kb_popularity():
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
@reports_bp.route('/warranty-status', methods=['GET'])
|
@reports_bp.route('/warranty-status', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required(optional=True)
|
||||||
def warranty_status():
|
def warranty_status():
|
||||||
"""
|
"""
|
||||||
Report: Assets by warranty expiration status.
|
Report: Assets by warranty expiration status.
|
||||||
@@ -305,7 +305,7 @@ def warranty_status():
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
@reports_bp.route('/software-compliance', methods=['GET'])
|
@reports_bp.route('/software-compliance', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required(optional=True)
|
||||||
def software_compliance():
|
def software_compliance():
|
||||||
"""
|
"""
|
||||||
Report: Required applications vs installed (per PC).
|
Report: Required applications vs installed (per PC).
|
||||||
@@ -409,7 +409,7 @@ def software_compliance():
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
@reports_bp.route('/asset-inventory', methods=['GET'])
|
@reports_bp.route('/asset-inventory', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required(optional=True)
|
||||||
def asset_inventory():
|
def asset_inventory():
|
||||||
"""
|
"""
|
||||||
Report: Complete asset inventory summary.
|
Report: Complete asset inventory summary.
|
||||||
@@ -518,8 +518,68 @@ def asset_inventory():
|
|||||||
# Available Reports List
|
# 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'])
|
@reports_bp.route('', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required(optional=True)
|
||||||
def list_reports():
|
def list_reports():
|
||||||
"""List all available reports."""
|
"""List all available reports."""
|
||||||
reports = [
|
reports = [
|
||||||
@@ -564,6 +624,13 @@ def list_reports():
|
|||||||
'description': 'Complete asset inventory breakdown',
|
'description': 'Complete asset inventory breakdown',
|
||||||
'endpoint': '/api/reports/asset-inventory',
|
'endpoint': '/api/reports/asset-inventory',
|
||||||
'category': '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'])
|
@vendors_bp.route('', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required(optional=True)
|
||||||
def list_vendors():
|
def list_vendors():
|
||||||
"""List all vendors."""
|
"""List all vendors."""
|
||||||
page, per_page = get_pagination_params(request)
|
page, per_page = get_pagination_params(request)
|
||||||
@@ -39,7 +39,7 @@ def list_vendors():
|
|||||||
|
|
||||||
|
|
||||||
@vendors_bp.route('/<int:vendor_id>', methods=['GET'])
|
@vendors_bp.route('/<int:vendor_id>', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required(optional=True)
|
||||||
def get_vendor(vendor_id: int):
|
def get_vendor(vendor_id: int):
|
||||||
"""Get a single vendor."""
|
"""Get a single vendor."""
|
||||||
v = Vendor.query.get(vendor_id)
|
v = Vendor.query.get(vendor_id)
|
||||||
|
|||||||
@@ -20,12 +20,12 @@ class AssetType(BaseModel):
|
|||||||
nullable=False,
|
nullable=False,
|
||||||
comment='Category name: equipment, computer, network_device, printer'
|
comment='Category name: equipment, computer, network_device, printer'
|
||||||
)
|
)
|
||||||
plugin_name = db.Column(
|
pluginname = db.Column(
|
||||||
db.String(100),
|
db.String(100),
|
||||||
nullable=True,
|
nullable=True,
|
||||||
comment='Plugin that owns this type'
|
comment='Plugin that owns this type'
|
||||||
)
|
)
|
||||||
table_name = db.Column(
|
tablename = db.Column(
|
||||||
db.String(100),
|
db.String(100),
|
||||||
nullable=True,
|
nullable=True,
|
||||||
comment='Extension table name for this type'
|
comment='Extension table name for this type'
|
||||||
@@ -174,23 +174,23 @@ class Asset(BaseModel, SoftDeleteMixin, AuditMixin):
|
|||||||
|
|
||||||
if hasattr(self, 'incoming_relationships'):
|
if hasattr(self, 'incoming_relationships'):
|
||||||
for rel in self.incoming_relationships:
|
for rel in self.incoming_relationships:
|
||||||
if rel.source_asset and rel.isactive:
|
if rel.sourceasset and rel.isactive:
|
||||||
related_assets.append(rel.source_asset)
|
related_assets.append(rel.sourceasset)
|
||||||
|
|
||||||
if hasattr(self, 'outgoing_relationships'):
|
if hasattr(self, 'outgoing_relationships'):
|
||||||
for rel in self.outgoing_relationships:
|
for rel in self.outgoing_relationships:
|
||||||
if rel.target_asset and rel.isactive:
|
if rel.targetasset and rel.isactive:
|
||||||
related_assets.append(rel.target_asset)
|
related_assets.append(rel.targetasset)
|
||||||
|
|
||||||
# Find first related asset with location data
|
# Find first related asset with location data
|
||||||
for related in related_assets:
|
for related in related_assets:
|
||||||
if related.locationid is not None or (related.mapleft is not None and related.maptop is not None):
|
if related.locationid is not None or (related.mapleft is not None and related.maptop is not None):
|
||||||
return {
|
return {
|
||||||
'locationid': related.locationid,
|
'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,
|
'mapleft': related.mapleft,
|
||||||
'maptop': related.maptop,
|
'maptop': related.maptop,
|
||||||
'inherited_from': related.assetnumber
|
'inheritedfrom': related.assetnumber
|
||||||
}
|
}
|
||||||
|
|
||||||
return None
|
return None
|
||||||
@@ -207,33 +207,33 @@ class Asset(BaseModel, SoftDeleteMixin, AuditMixin):
|
|||||||
|
|
||||||
# Add related object names for convenience
|
# Add related object names for convenience
|
||||||
if self.assettype:
|
if self.assettype:
|
||||||
result['assettype_name'] = self.assettype.assettype
|
result['assettypename'] = self.assettype.assettype
|
||||||
if self.status:
|
if self.status:
|
||||||
result['status_name'] = self.status.status
|
result['statusname'] = self.status.status
|
||||||
if self.location:
|
if self.location:
|
||||||
result['location_name'] = self.location.locationname
|
result['locationname'] = self.location.locationname
|
||||||
if self.businessunit:
|
if self.businessunit:
|
||||||
result['businessunit_name'] = self.businessunit.businessunit
|
result['businessunitname'] = self.businessunit.businessunit
|
||||||
|
|
||||||
# Add plugin-specific ID for navigation purposes
|
# Add plugin-specific ID for navigation purposes
|
||||||
if hasattr(self, 'equipment') and self.equipment:
|
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:
|
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:
|
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:
|
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
|
# Include inherited location if this asset has no location data
|
||||||
if include_inherited_location:
|
if include_inherited_location:
|
||||||
inherited = self.get_inherited_location()
|
inherited = self.get_inherited_location()
|
||||||
if inherited:
|
if inherited:
|
||||||
result['inherited_location'] = inherited
|
result['inheritedlocation'] = inherited
|
||||||
# Also set the location fields if they're missing
|
# Also set the location fields if they're missing
|
||||||
if result.get('locationid') is None:
|
if result.get('locationid') is None:
|
||||||
result['locationid'] = inherited['locationid']
|
result['locationid'] = inherited['locationid']
|
||||||
result['location_name'] = inherited['location_name']
|
result['locationname'] = inherited['locationname']
|
||||||
if result.get('mapleft') is None:
|
if result.get('mapleft') is None:
|
||||||
result['mapleft'] = inherited['mapleft']
|
result['mapleft'] = inherited['mapleft']
|
||||||
if result.get('maptop') is None:
|
if result.get('maptop') is None:
|
||||||
@@ -243,7 +243,7 @@ class Asset(BaseModel, SoftDeleteMixin, AuditMixin):
|
|||||||
if include_type_data:
|
if include_type_data:
|
||||||
ext_data = self._get_extension_data()
|
ext_data = self._get_extension_data()
|
||||||
if ext_data:
|
if ext_data:
|
||||||
result['type_data'] = ext_data
|
result['typedata'] = ext_data
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|||||||
@@ -34,12 +34,12 @@ class AssetRelationship(BaseModel):
|
|||||||
|
|
||||||
relationshipid = db.Column(db.Integer, primary_key=True)
|
relationshipid = db.Column(db.Integer, primary_key=True)
|
||||||
|
|
||||||
source_assetid = db.Column(
|
sourceassetid = db.Column(
|
||||||
db.Integer,
|
db.Integer,
|
||||||
db.ForeignKey('assets.assetid'),
|
db.ForeignKey('assets.assetid'),
|
||||||
nullable=False
|
nullable=False
|
||||||
)
|
)
|
||||||
target_assetid = db.Column(
|
targetassetid = db.Column(
|
||||||
db.Integer,
|
db.Integer,
|
||||||
db.ForeignKey('assets.assetid'),
|
db.ForeignKey('assets.assetid'),
|
||||||
nullable=False
|
nullable=False
|
||||||
@@ -53,31 +53,31 @@ class AssetRelationship(BaseModel):
|
|||||||
notes = db.Column(db.Text)
|
notes = db.Column(db.Text)
|
||||||
|
|
||||||
# Relationships
|
# Relationships
|
||||||
source_asset = db.relationship(
|
sourceasset = db.relationship(
|
||||||
'Asset',
|
'Asset',
|
||||||
foreign_keys=[source_assetid],
|
foreign_keys=[sourceassetid],
|
||||||
backref='outgoing_relationships'
|
backref='outgoing_relationships'
|
||||||
)
|
)
|
||||||
target_asset = db.relationship(
|
targetasset = db.relationship(
|
||||||
'Asset',
|
'Asset',
|
||||||
foreign_keys=[target_assetid],
|
foreign_keys=[targetassetid],
|
||||||
backref='incoming_relationships'
|
backref='incoming_relationships'
|
||||||
)
|
)
|
||||||
relationship_type = db.relationship('RelationshipType', backref='asset_relationships')
|
relationshiptype = db.relationship('RelationshipType', backref='asset_relationships')
|
||||||
|
|
||||||
__table_args__ = (
|
__table_args__ = (
|
||||||
db.UniqueConstraint(
|
db.UniqueConstraint(
|
||||||
'source_assetid',
|
'sourceassetid',
|
||||||
'target_assetid',
|
'targetassetid',
|
||||||
'relationshiptypeid',
|
'relationshiptypeid',
|
||||||
name='uq_asset_relationship'
|
name='uq_asset_relationship'
|
||||||
),
|
),
|
||||||
db.Index('idx_asset_rel_source', 'source_assetid'),
|
db.Index('idx_asset_rel_source', 'sourceassetid'),
|
||||||
db.Index('idx_asset_rel_target', 'target_assetid'),
|
db.Index('idx_asset_rel_target', 'targetassetid'),
|
||||||
)
|
)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<AssetRelationship {self.source_assetid} -> {self.target_assetid}>"
|
return f"<AssetRelationship {self.sourceassetid} -> {self.targetassetid}>"
|
||||||
|
|
||||||
|
|
||||||
class MachineRelationship(BaseModel):
|
class MachineRelationship(BaseModel):
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ class PluginManager:
|
|||||||
'dependencies': meta.dependencies,
|
'dependencies': meta.dependencies,
|
||||||
'installed': state is not None,
|
'installed': state is not None,
|
||||||
'enabled': state.enabled if state else False,
|
'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:
|
except Exception as e:
|
||||||
logger.warning(f"Error inspecting plugin {name}: {e}")
|
logger.warning(f"Error inspecting plugin {name}: {e}")
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ def get_pagination_params(req=None) -> Tuple[int, int]:
|
|||||||
page = 1
|
page = 1
|
||||||
|
|
||||||
try:
|
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))
|
per_page = max(1, min(per_page, max_size))
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
per_page = default_size
|
per_page = default_size
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ def api_response(
|
|||||||
'status': status,
|
'status': status,
|
||||||
'meta': {
|
'meta': {
|
||||||
'timestamp': datetime.utcnow().isoformat() + 'Z',
|
'timestamp': datetime.utcnow().isoformat() + 'Z',
|
||||||
'request_id': str(uuid.uuid4())[:8],
|
'requestid': str(uuid.uuid4())[:8],
|
||||||
**(meta or {})
|
**(meta or {})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -161,11 +161,11 @@ def paginated_response(
|
|||||||
meta={
|
meta={
|
||||||
'pagination': {
|
'pagination': {
|
||||||
'page': page,
|
'page': page,
|
||||||
'per_page': per_page,
|
'perpage': per_page,
|
||||||
'total': total,
|
'total': total,
|
||||||
'total_pages': total_pages,
|
'totalpages': total_pages,
|
||||||
'has_next': page < total_pages,
|
'hasnext': page < total_pages,
|
||||||
'has_prev': page > 1
|
'hasprev': page > 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user