Rename the equipment domain to machines; retype the models catalog (ADR-011)
The equipment plugin is now the machines plugin, ending the UI-vs-code vocabulary split while the contract is pre-1.0 and nothing external depends on the old names. - plugins/equipment -> plugins/machines: manifest, class, /api/machines, machines.* permissions, registry key (with an auto-migrating load shim for existing installs). - Tables: equipment -> machines (equipmentid -> machineid) and equipmenttypes -> machinetypes, renamed in the plugin's own migration chain (machines0002rename), idempotent for both upgrading and fresh installs. - The legacy core machinetypes lookup actually types the vendor MODELS catalog, so it is renamed losslessly to modeltypes (models.modeltypeid, /api/modeltypes, Model Types settings page) rather than collapsed, freeing the machinetypes name. Core migration 7d17_machines_rename also flips data in place: assettypes row equipment -> machine, auditlog entitytype, identifier_/search_ settings keys, permission rows, and renames alembic_version_equipment. - Frontend: machinesApi/modeltypesApi, item.machine response shape, assettype value compares 'equipment' -> 'machine' (map, search, custom fields, relationships), routes machines.js with plugin gating retagged, /print/machine-badge, Machine Types (subtypes) and Model Types (catalog) settings pages, machines-by-type report id. - Docs swept; ADRs left as history per the authoring rule. Upgrade: flask db upgrade then flask plugin upgrade-all. Verified: dev DB flipped live (262 machines, 35 modeltypes, 95 models retyped, zero equipment tables remain); fresh scratch-MySQL install produces the new names; 341 tests green; naming/style green; frontend builds; live E2E on machines list/detail, PC relationships, map, reports, and both settings pages. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
16
CHANGELOG.md
16
CHANGELOG.md
@@ -41,6 +41,22 @@ ADR-007 and ADR-002.
|
|||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
- Equipment -> machines rename (backend). The equipment plugin is now the
|
||||||
|
machines plugin: `/api/equipment` -> `/api/machines`, tables
|
||||||
|
`equipment`/`equipmenttypes` -> `machines`/`machinetypes` (columns
|
||||||
|
`equipmentid` -> `machineid`, `equipmenttypeid` -> `machinetypeid`,
|
||||||
|
`equipmenttype` -> `machinetype`), permissions `equipment.*` ->
|
||||||
|
`machines.*`, assettype value `equipment` -> `machine`. The legacy core
|
||||||
|
`machinetypes` lookup (it types the vendor MODELS catalog, not machine
|
||||||
|
instances) is renamed to `modeltypes` (`/api/machinetypes` ->
|
||||||
|
`/api/modeltypes`, `models.machinetypeid` -> `models.modeltypeid`) to
|
||||||
|
free the name. Data flips migrate assettypes, auditlog entitytype,
|
||||||
|
settings keys (`identifier_*_equipment_enabled` ->
|
||||||
|
`identifier_*_machine_enabled`, `search_equipment_enabled` ->
|
||||||
|
`search_machine_enabled`), and permission rows in place; plugins.json
|
||||||
|
registry entries carry over automatically. Upgrade: run
|
||||||
|
`flask db upgrade` then `flask plugin upgrade-all`.
|
||||||
|
|
||||||
- Inter (variable) replaces Roboto, bundled locally - no Google Fonts
|
- Inter (variable) replaces Roboto, bundled locally - no Google Fonts
|
||||||
fetch, so air-gapped installs render correctly. Tables use tabular
|
fetch, so air-gapped installs render correctly. Tables use tabular
|
||||||
numerals.
|
numerals.
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ Refactor phases 0-5 landed; phase 6 (multi-site distribution readiness) in progr
|
|||||||
|
|
||||||
- 340 tests passing, naming/style check green, Gitea Actions CI (backend + naming + frontend build)
|
- 340 tests passing, naming/style check green, Gitea Actions CI (backend + naming + frontend build)
|
||||||
- `__contract_version__` at 0.6.0 (product `__version__` 0.5.0 - distinct series, ADR-007)
|
- `__contract_version__` at 0.6.0 (product `__version__` 0.5.0 - distinct series, ADR-007)
|
||||||
- 11 bundled plugins all satisfy contract: computers, employees, equipment, knowledgebase, measuringtools, network, notifications, printers, slides, usb, warranty
|
- 11 bundled plugins all satisfy contract: computers, employees, knowledgebase, machines, measuringtools, network, notifications, printers, slides, usb, warranty
|
||||||
- Single core Alembic chain: baseline `68b3947ae14f` -> head `7d16_directoryemployees` (23 migrations). A fresh site runs `flask db upgrade` from empty; it is reproducible and idempotent.
|
- Single core Alembic chain: baseline `68b3947ae14f` -> head `7d16_directoryemployees` (23 migrations). A fresh site runs `flask db upgrade` from empty; it is reproducible and idempotent.
|
||||||
- Pre-1.0 framework; sister sites should pin tight `core_version` ranges until contract reaches 1.0
|
- Pre-1.0 framework; sister sites should pin tight `core_version` ranges until contract reaches 1.0
|
||||||
|
|
||||||
|
|||||||
@@ -198,7 +198,7 @@ Query parameters for list endpoints:
|
|||||||
- `sort` - Sort field
|
- `sort` - Sort field
|
||||||
- `order` - Sort direction (asc/desc)
|
- `order` - Sort direction (asc/desc)
|
||||||
- `search` - Search term
|
- `search` - Search term
|
||||||
- `type` - Filter by machine type (pc, printer, equipment)
|
- `type` - Filter by asset type (computer, printer, machine, network_device)
|
||||||
|
|
||||||
## Plugin System
|
## Plugin System
|
||||||
|
|
||||||
@@ -208,7 +208,7 @@ The image bundles ten plugins; only the ones a site installs are loaded:
|
|||||||
|
|
||||||
- **computers** - Shopfloor PCs and workstations
|
- **computers** - Shopfloor PCs and workstations
|
||||||
- **employees** - Employee directory
|
- **employees** - Employee directory
|
||||||
- **equipment** - CNC, CMM, and inspection equipment
|
- **machine** - CNC, CMM, and other shop-floor machines
|
||||||
- **knowledgebase** - Documentation and troubleshooting guides
|
- **knowledgebase** - Documentation and troubleshooting guides
|
||||||
- **network** - Network devices
|
- **network** - Network devices
|
||||||
- **notifications** - Shopfloor notifications and recognition feed
|
- **notifications** - Shopfloor notifications and recognition feed
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ them under `instance/branding/`.
|
|||||||
|-----|---------|-------|
|
|-----|---------|-------|
|
||||||
| `site_logo` | `/ge-aerospace-logo.svg` | Header and login-page logo. |
|
| `site_logo` | `/ge-aerospace-logo.svg` | Header and login-page logo. |
|
||||||
| `qr_logo` | `/ge-monogram.svg` | Logo composited in printer QR labels. Blank = no overlay. |
|
| `qr_logo` | `/ge-monogram.svg` | Logo composited in printer QR labels. Blank = no overlay. |
|
||||||
| `badge_logo` | `/ge-aerospace-logo.svg` | Logo on the equipment badge print page. |
|
| `badge_logo` | `/ge-aerospace-logo.svg` | Logo on the machine badge print page. |
|
||||||
| `site_favicon` | (empty) | Browser tab favicon. Blank = shipped `/favicon.svg`. |
|
| `site_favicon` | (empty) | Browser tab favicon. Blank = shipped `/favicon.svg`. |
|
||||||
| `brand_primary_color` | (empty) | Primary brand color as a CSS color value (maps to `--primary`). Blank = built-in theme color. |
|
| `brand_primary_color` | (empty) | Primary brand color as a CSS color value (maps to `--primary`). Blank = built-in theme color. |
|
||||||
| `brand_primary_dark_color` | (empty) | Primary hover/active color (maps to `--primary-dark`). Blank = auto-derived by darkening the primary color ~15%. |
|
| `brand_primary_dark_color` | (empty) | Primary hover/active color (maps to `--primary-dark`). Blank = auto-derived by darkening the primary color ~15%. |
|
||||||
|
|||||||
@@ -104,12 +104,12 @@ docker compose exec api flask seed reference-data
|
|||||||
|
|
||||||
## Step 5: Pick plugins to enable
|
## Step 5: Pick plugins to enable
|
||||||
|
|
||||||
The image bundles ten plugins (computers, employees, equipment, knowledgebase, network, notifications, printers, slides, usb, warranty). Only enabled plugins are loaded.
|
The image bundles eleven plugins (computers, employees, knowledgebase, machines, measuringtools, network, notifications, printers, slides, usb, warranty). Only enabled plugins are loaded.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose exec api flask plugin list
|
docker compose exec api flask plugin list
|
||||||
docker compose exec api flask plugin install computers
|
docker compose exec api flask plugin install computers
|
||||||
docker compose exec api flask plugin install equipment
|
docker compose exec api flask plugin install machines
|
||||||
# ... repeat for each plugin the site tracks
|
# ... repeat for each plugin the site tracks
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ def meta(self) -> PluginMeta:
|
|||||||
The plugin owns two tables (`plugins/measuringtools/models/measuringtool.py`):
|
The plugin owns two tables (`plugins/measuringtools/models/measuringtool.py`):
|
||||||
|
|
||||||
- `measuringtooltypes` - the site-managed lookup (Caliper, Micrometer, ...) with a
|
- `measuringtooltypes` - the site-managed lookup (Caliper, Micrometer, ...) with a
|
||||||
display color, the same shape as `equipmenttypes` / `computertypes`.
|
display color, the same shape as `machinetypes` / `computertypes`.
|
||||||
- `measuringtools` - the one-to-one extension of a core `Asset`, carrying only the
|
- `measuringtools` - the one-to-one extension of a core `Asset`, carrying only the
|
||||||
metrology domain fields.
|
metrology domain fields.
|
||||||
|
|
||||||
@@ -523,7 +523,7 @@ form is `requiresAuth`; the settings subtype page is `requiresAuth + requiresAdm
|
|||||||
**API client, addition only.** `frontend/src/api/index.js` gets a
|
**API client, addition only.** `frontend/src/api/index.js` gets a
|
||||||
`measuringtoolsApi` object appended after `warrantyApi` (`list`, `get`, `create`,
|
`measuringtoolsApi` object appended after `warrantyApi` (`list`, `get`, `create`,
|
||||||
`update`, `remove`, `calibrationReport`, and a nested `types` CRUD). Do not
|
`update`, `remove`, `calibrationReport`, and a nested `types` CRUD). Do not
|
||||||
reorganize the file; just add the block, mirroring `equipmentApi`.
|
reorganize the file; just add the block, mirroring `machinesApi`.
|
||||||
|
|
||||||
**Views mirror the master templates.** The frontend has master templates the
|
**Views mirror the master templates.** The frontend has master templates the
|
||||||
frontend CLAUDE.md points to (`PrintersList.vue` for lists, `PrinterDetail.vue`
|
frontend CLAUDE.md points to (`PrintersList.vue` for lists, `PrinterDetail.vue`
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ These plugins are in `plugins/` in this repo. Enable per site with `flask plugin
|
|||||||
|
|
||||||
| Plugin | Tracks | Notes |
|
| Plugin | Tracks | Notes |
|
||||||
|--------|--------|-------|
|
|--------|--------|-------|
|
||||||
| `equipment` | Manufacturing machinery: 5-axis mills, lathes, broachers, heat treatment ovens | Manually entered. See [ADR-005](adr/ADR-005-equipment-vs-measuringtools.md). Subtype tables for FOCAS / CLM / MTConnect controller protocols (planned). |
|
| `machines` | Manufacturing machinery: 5-axis mills, lathes, broachers, heat treatment ovens | Manually entered. See [ADR-005](adr/ADR-005-equipment-vs-measuringtools.md). Subtype tables for FOCAS / CLM / MTConnect controller protocols (planned). |
|
||||||
| `computers` | Shop-floor PCs and engineering workstations | Fed by the PXE pipeline collector per [ADR-006](adr/ADR-006-collector-contract.md). |
|
| `computers` | Shop-floor PCs and engineering workstations | Fed by the PXE pipeline collector per [ADR-006](adr/ADR-006-collector-contract.md). |
|
||||||
| `printers` | Network and shop-floor printers | Optional Zabbix integration for supply tracking. Legacy `PrinterData` retiring per ADR-001. |
|
| `printers` | Network and shop-floor printers | Optional Zabbix integration for supply tracking. Legacy `PrinterData` retiring per ADR-001. |
|
||||||
| `network` | Switches, routers, access points, IDFs as locations | Asset-only; cleanest of the bundled set. |
|
| `network` | Switches, routers, access points, IDFs as locations | Asset-only; cleanest of the bundled set. |
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ shopdb-flask is at `__contract_version__ = '0.5.0'` (pre-1.0). This document cap
|
|||||||
- Frontend scaffolding skill (the backend has `flask plugin new`; the frontend stub is currently manual copy-paste).
|
- Frontend scaffolding skill (the backend has `flask plugin new`; the frontend stub is currently manual copy-paste).
|
||||||
- Marketplace listing site (PLUGINS.md is a one-pager; a proper listing with links to sister-site plugins becomes useful when there are more than three external plugins).
|
- Marketplace listing site (PLUGINS.md is a one-pager; a proper listing with links to sister-site plugins becomes useful when there are more than three external plugins).
|
||||||
- Plugin contract surface diff tooling. Today version bumps are manual judgment; a CI check that diffs the contract surface against the previous tag would catch missed bumps. See ADR-002.
|
- Plugin contract surface diff tooling. Today version bumps are manual judgment; a CI check that diffs the contract surface against the previous tag would catch missed bumps. See ADR-002.
|
||||||
- Calibration cycles, maintenance windows, downtime tracking (domain extensions; would likely live in the `equipment` and `measuringtools` plugins).
|
- Calibration cycles, maintenance windows, downtime tracking (domain extensions; would likely live in the `machines` and `measuringtools` plugins).
|
||||||
|
|
||||||
### Deferred (out of scope for 1.0)
|
### Deferred (out of scope for 1.0)
|
||||||
|
|
||||||
|
|||||||
@@ -58,45 +58,45 @@ export const authApi = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Equipment API (plugin)
|
// Machines API (plugin)
|
||||||
export const equipmentApi = {
|
export const machinesApi = {
|
||||||
list(params = {}) {
|
list(params = {}) {
|
||||||
return api.get('/equipment', { params })
|
return api.get('/machines', { params })
|
||||||
},
|
},
|
||||||
get(id) {
|
get(id) {
|
||||||
return api.get(`/equipment/${id}`)
|
return api.get(`/machines/${id}`)
|
||||||
},
|
},
|
||||||
getByAsset(assetId) {
|
getByAsset(assetId) {
|
||||||
return api.get(`/equipment/by-asset/${assetId}`)
|
return api.get(`/machines/by-asset/${assetId}`)
|
||||||
},
|
},
|
||||||
create(data) {
|
create(data) {
|
||||||
return api.post('/equipment', data)
|
return api.post('/machines', data)
|
||||||
},
|
},
|
||||||
update(id, data) {
|
update(id, data) {
|
||||||
return api.put(`/equipment/${id}`, data)
|
return api.put(`/machines/${id}`, data)
|
||||||
},
|
},
|
||||||
delete(id) {
|
delete(id) {
|
||||||
return api.delete(`/equipment/${id}`)
|
return api.delete(`/machines/${id}`)
|
||||||
},
|
},
|
||||||
dashboardSummary() {
|
dashboardSummary() {
|
||||||
return api.get('/equipment/dashboard/summary')
|
return api.get('/machines/dashboard/summary')
|
||||||
},
|
},
|
||||||
// Equipment types
|
// Machine types
|
||||||
types: {
|
types: {
|
||||||
list(params = {}) {
|
list(params = {}) {
|
||||||
return api.get('/equipment/types', { params })
|
return api.get('/machines/types', { params })
|
||||||
},
|
},
|
||||||
get(id) {
|
get(id) {
|
||||||
return api.get(`/equipment/types/${id}`)
|
return api.get(`/machines/types/${id}`)
|
||||||
},
|
},
|
||||||
create(data) {
|
create(data) {
|
||||||
return api.post('/equipment/types', data)
|
return api.post('/machines/types', data)
|
||||||
},
|
},
|
||||||
update(id, data) {
|
update(id, data) {
|
||||||
return api.put(`/equipment/types/${id}`, data)
|
return api.put(`/machines/types/${id}`, data)
|
||||||
},
|
},
|
||||||
remove(id) {
|
remove(id) {
|
||||||
return api.delete(`/equipment/types/${id}`)
|
return api.delete(`/machines/types/${id}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -178,19 +178,19 @@ export const relationshipTypesApi = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Machine Types API
|
// Model Types API (the vendor models catalog: modeltypeid)
|
||||||
export const machinetypesApi = {
|
export const modeltypesApi = {
|
||||||
list(params = {}) {
|
list(params = {}) {
|
||||||
return api.get('/machinetypes', { params })
|
return api.get('/modeltypes', { params })
|
||||||
},
|
},
|
||||||
create(data) {
|
create(data) {
|
||||||
return api.post('/machinetypes', data)
|
return api.post('/modeltypes', data)
|
||||||
},
|
},
|
||||||
update(id, data) {
|
update(id, data) {
|
||||||
return api.put(`/machinetypes/${id}`, data)
|
return api.put(`/modeltypes/${id}`, data)
|
||||||
},
|
},
|
||||||
delete(id) {
|
delete(id) {
|
||||||
return api.delete(`/machinetypes/${id}`)
|
return api.delete(`/modeltypes/${id}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -667,8 +667,8 @@ export const reportsApi = {
|
|||||||
list() {
|
list() {
|
||||||
return api.get('/reports')
|
return api.get('/reports')
|
||||||
},
|
},
|
||||||
equipmentByType(params = {}) {
|
machinesByType(params = {}) {
|
||||||
return api.get('/reports/equipment-by-type', { params })
|
return api.get('/reports/machines-by-type', { params })
|
||||||
},
|
},
|
||||||
assetsByStatus(params = {}) {
|
assetsByStatus(params = {}) {
|
||||||
return api.get('/reports/assets-by-status', { params })
|
return api.get('/reports/assets-by-status', { params })
|
||||||
|
|||||||
@@ -976,7 +976,7 @@ td.actions {
|
|||||||
width: 180px;
|
width: 180px;
|
||||||
height: 180px;
|
height: 180px;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
background: rgba(0, 0, 0, 0.3);
|
background: var(--bg);
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
@@ -1208,15 +1208,16 @@ td.actions {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
background: rgba(0, 0, 0, 0.3);
|
background: var(--bg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
transition: background 0.15s;
|
transition: border-color 0.15s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.equipment-item:hover {
|
.equipment-item:hover {
|
||||||
background: rgba(255, 255, 255, 0.1);
|
border-color: var(--primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.equipment-info {
|
.equipment-info {
|
||||||
@@ -1251,7 +1252,8 @@ td.actions {
|
|||||||
|
|
||||||
.network-item {
|
.network-item {
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
background: rgba(0, 0, 0, 0.3);
|
background: var(--bg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,14 +27,14 @@
|
|||||||
:key="rel.relationshipid"
|
:key="rel.relationshipid"
|
||||||
class="relationship-item"
|
class="relationship-item"
|
||||||
>
|
>
|
||||||
<div class="rel-icon"><component :is="getAssetIcon(rel.targetasset?.assettype)" :size="16" /></div>
|
<div class="rel-icon"><component :is="getAssetIcon(rel.targetasset?.assettypename || rel.targetasset?.assettype)" :size="16" /></div>
|
||||||
<div class="rel-content">
|
<div class="rel-content">
|
||||||
<router-link :to="getAssetRoute(rel.targetasset)" class="rel-name">
|
<router-link :to="getAssetRoute(rel.targetasset)" class="rel-name">
|
||||||
{{ rel.targetasset?.name || rel.targetasset?.assetnumber || 'Unknown' }}
|
{{ rel.targetasset?.name || rel.targetasset?.assetnumber || 'Unknown' }}
|
||||||
</router-link>
|
</router-link>
|
||||||
<div class="rel-meta">
|
<div class="rel-meta">
|
||||||
<span class="badge" :style="colorStyle(colorForType(rel.relationshiptypename))">{{ rel.relationshiptypename }}</span>
|
<span class="badge" :style="colorStyle(colorForType(rel.relationshiptypename))">{{ rel.relationshiptypename }}</span>
|
||||||
<span class="rel-type-badge">{{ rel.targetasset?.assettype }}</span>
|
<span class="rel-type-badge">{{ rel.targetasset?.assettypename || 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"><component :is="getAssetIcon(rel.sourceasset?.assettype)" :size="16" /></div>
|
<div class="rel-icon"><component :is="getAssetIcon(rel.sourceasset?.assettypename || rel.sourceasset?.assettype)" :size="16" /></div>
|
||||||
<div class="rel-content">
|
<div class="rel-content">
|
||||||
<router-link :to="getAssetRoute(rel.sourceasset)" class="rel-name">
|
<router-link :to="getAssetRoute(rel.sourceasset)" class="rel-name">
|
||||||
{{ rel.sourceasset?.name || rel.sourceasset?.assetnumber || 'Unknown' }}
|
{{ rel.sourceasset?.name || rel.sourceasset?.assetnumber || 'Unknown' }}
|
||||||
</router-link>
|
</router-link>
|
||||||
<div class="rel-meta">
|
<div class="rel-meta">
|
||||||
<span class="badge" :style="colorStyle(colorForType(rel.relationshiptypename))">{{ rel.relationshiptypename }}</span>
|
<span class="badge" :style="colorStyle(colorForType(rel.relationshiptypename))">{{ rel.relationshiptypename }}</span>
|
||||||
<span class="rel-type-badge">{{ rel.sourceasset?.assettype }}</span>
|
<span class="rel-type-badge">{{ rel.sourceasset?.assettypename || 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,7 +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 { Cog, Monitor, Printer, Globe, Package, Ruler } from 'lucide-vue-next'
|
||||||
import { assetsApi, relationshipTypesApi } from '../api'
|
import { assetsApi, relationshipTypesApi } from '../api'
|
||||||
import { colorStyle } from '@/utils/colorStyle'
|
import { colorStyle } from '@/utils/colorStyle'
|
||||||
import { useAuthStore } from '../stores/auth'
|
import { useAuthStore } from '../stores/auth'
|
||||||
@@ -384,10 +384,11 @@ function closeModal() {
|
|||||||
|
|
||||||
function getAssetIcon(assettype) {
|
function getAssetIcon(assettype) {
|
||||||
const icons = {
|
const icons = {
|
||||||
'equipment': Cog,
|
'machine': Cog,
|
||||||
'computer': Monitor,
|
'computer': Monitor,
|
||||||
'printer': Printer,
|
'printer': Printer,
|
||||||
'network_device': Globe
|
'network_device': Globe,
|
||||||
|
'measuring_tool': Ruler
|
||||||
}
|
}
|
||||||
return icons[assettype] || Package
|
return icons[assettype] || Package
|
||||||
}
|
}
|
||||||
@@ -396,21 +397,22 @@ function getAssetRoute(asset) {
|
|||||||
if (!asset) return '#'
|
if (!asset) return '#'
|
||||||
|
|
||||||
const routeMap = {
|
const routeMap = {
|
||||||
'equipment': '/machines',
|
'machine': '/machines',
|
||||||
'computer': '/pcs',
|
'computer': '/pcs',
|
||||||
'printer': '/printers',
|
'printer': '/printers',
|
||||||
'network_device': '/network'
|
'network_device': '/network',
|
||||||
|
'measuring_tool': '/measuringtools'
|
||||||
}
|
}
|
||||||
|
|
||||||
const basePath = routeMap[asset.assettype] || '/assets'
|
// serializer sends assettypename; old shape used assettype
|
||||||
|
const typename = asset.assettypename || asset.assettype
|
||||||
|
const basePath = routeMap[typename] || '/assets'
|
||||||
|
|
||||||
// Use typedata ID if available
|
// pluginid is the extension-table id the detail routes key on
|
||||||
if (asset.assettype === 'network_device' && asset.typedata?.networkdeviceid) {
|
const id = asset.pluginid
|
||||||
return `/network/${asset.typedata.networkdeviceid}`
|
|| asset.typedata?.networkdeviceid
|
||||||
}
|
|| asset.typedata?.machineid
|
||||||
|
|| asset.assetid
|
||||||
// For equipment/computer/printer, use machineid from typedata or assetid
|
|
||||||
const id = asset.typedata?.machineid || asset.assetid
|
|
||||||
return `${basePath}/${id}`
|
return `${basePath}/${id}`
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ let MAP_HEIGHT = mapConfig.height
|
|||||||
|
|
||||||
// Asset type colors (for unified map mode) - normalized lookup
|
// Asset type colors (for unified map mode) - normalized lookup
|
||||||
const assetTypeColorsMap = {
|
const assetTypeColorsMap = {
|
||||||
'equipment': '#F44336', // Red
|
'machine': '#F44336', // Red
|
||||||
'computer': '#2196F3', // Blue
|
'computer': '#2196F3', // Blue
|
||||||
'printer': '#4CAF50', // Green
|
'printer': '#4CAF50', // Green
|
||||||
'network device': '#FF9800', // Orange
|
'network device': '#FF9800', // Orange
|
||||||
@@ -225,7 +225,7 @@ function getSubtypeId(asset) {
|
|||||||
if (!asset.typedata) return null
|
if (!asset.typedata) return null
|
||||||
// Normalize network_device -> network device so the subtype id resolves.
|
// Normalize network_device -> network device so the subtype id resolves.
|
||||||
const typeLower = (asset.assettype || '').toLowerCase().replace(/_/g, ' ')
|
const typeLower = (asset.assettype || '').toLowerCase().replace(/_/g, ' ')
|
||||||
if (typeLower === 'equipment') return asset.typedata.equipmenttypeid
|
if (typeLower === 'machine') return asset.typedata.machinetypeid
|
||||||
if (typeLower === 'computer') return asset.typedata.computertypeid
|
if (typeLower === 'computer') return asset.typedata.computertypeid
|
||||||
if (typeLower === 'network device') return asset.typedata.networkdevicetypeid
|
if (typeLower === 'network device') return asset.typedata.networkdevicetypeid
|
||||||
if (typeLower === 'printer') return asset.typedata.printertypeid
|
if (typeLower === 'printer') return asset.typedata.printertypeid
|
||||||
@@ -346,7 +346,7 @@ function clearPosition() {
|
|||||||
function getDetailRoute(machine) {
|
function getDetailRoute(machine) {
|
||||||
const category = machine.category?.toLowerCase() || ''
|
const category = machine.category?.toLowerCase() || ''
|
||||||
const routeMap = {
|
const routeMap = {
|
||||||
'equipment': '/machines',
|
'machine': '/machines',
|
||||||
'pc': '/pcs',
|
'pc': '/pcs',
|
||||||
'printer': '/printers',
|
'printer': '/printers',
|
||||||
// Future addon routes can be added here:
|
// Future addon routes can be added here:
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ const state = reactive({
|
|||||||
|
|
||||||
let inflight = null
|
let inflight = null
|
||||||
|
|
||||||
const KEY_RE = /^identifier_(.+?)(?:_(equipment|computer|printer|network_device))?_enabled$/
|
const KEY_RE = /^identifier_(.+?)(?:_(machine|computer|printer|network_device))?_enabled$/
|
||||||
|
|
||||||
function applySetting(key, value) {
|
function applySetting(key, value) {
|
||||||
const match = KEY_RE.exec(key)
|
const match = KEY_RE.exec(key)
|
||||||
|
|||||||
@@ -66,9 +66,9 @@ const routes = [
|
|||||||
},
|
},
|
||||||
// Print pages (standalone, no sidebar/header)
|
// Print pages (standalone, no sidebar/header)
|
||||||
{
|
{
|
||||||
path: '/print/equipment-badge/:id',
|
path: '/print/machine-badge/:id',
|
||||||
name: 'print-equipment-badge',
|
name: 'print-machine-badge',
|
||||||
component: () => import('../views/print/EquipmentBadge.vue')
|
component: () => import('../views/print/MachineBadge.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/print/printer-qr',
|
path: '/print/printer-qr',
|
||||||
|
|||||||
@@ -60,9 +60,9 @@ export default [
|
|||||||
meta: { requiresAuth: true, requiresAdmin: true }
|
meta: { requiresAuth: true, requiresAdmin: true }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'settings/machinetypes',
|
path: 'settings/modeltypes',
|
||||||
name: 'machinetypes',
|
name: 'modeltypes',
|
||||||
component: () => import('../../views/settings/MachineTypesList.vue'),
|
component: () => import('../../views/settings/ModelTypesList.vue'),
|
||||||
meta: { requiresAuth: true, requiresAdmin: true }
|
meta: { requiresAuth: true, requiresAdmin: true }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -108,9 +108,9 @@ export default [
|
|||||||
meta: { requiresAuth: true, requiresAdmin: true, plugin: 'slides' }
|
meta: { requiresAuth: true, requiresAdmin: true, plugin: 'slides' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'settings/equipmenttypes',
|
path: 'settings/machinetypes',
|
||||||
name: 'equipment-types',
|
name: 'machinetypes',
|
||||||
component: () => import('../../views/settings/EquipmentTypesList.vue'),
|
component: () => import('../../views/settings/MachineTypesList.vue'),
|
||||||
meta: { requiresAuth: true, requiresAdmin: true }
|
meta: { requiresAuth: true, requiresAdmin: true }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,29 +1,29 @@
|
|||||||
/**
|
/**
|
||||||
* Equipment plugin routes
|
* Machines plugin routes
|
||||||
*/
|
*/
|
||||||
export default [
|
export default [
|
||||||
{
|
{
|
||||||
path: 'machines',
|
path: 'machines',
|
||||||
name: 'machines',
|
name: 'machines',
|
||||||
component: () => import('../../views/machines/MachinesList.vue'),
|
component: () => import('../../views/machines/MachinesList.vue'),
|
||||||
meta: { plugin: 'equipment' }
|
meta: { plugin: 'machines' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'machines/new',
|
path: 'machines/new',
|
||||||
name: 'machine-new',
|
name: 'machine-new',
|
||||||
component: () => import('../../views/machines/MachineForm.vue'),
|
component: () => import('../../views/machines/MachineForm.vue'),
|
||||||
meta: { requiresAuth: true, plugin: 'equipment' }
|
meta: { requiresAuth: true, plugin: 'machines' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'machines/:id',
|
path: 'machines/:id',
|
||||||
name: 'machine-detail',
|
name: 'machine-detail',
|
||||||
component: () => import('../../views/machines/MachineDetail.vue'),
|
component: () => import('../../views/machines/MachineDetail.vue'),
|
||||||
meta: { plugin: 'equipment' }
|
meta: { plugin: 'machines' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'machines/:id/edit',
|
path: 'machines/:id/edit',
|
||||||
name: 'machine-edit',
|
name: 'machine-edit',
|
||||||
component: () => import('../../views/machines/MachineForm.vue'),
|
component: () => import('../../views/machines/MachineForm.vue'),
|
||||||
meta: { requiresAuth: true, plugin: 'equipment' }
|
meta: { requiresAuth: true, plugin: 'machines' }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
// because the API sends the underscore form and some map data the spaced form.
|
// because the API sends the underscore form and some map data the spaced form.
|
||||||
|
|
||||||
const ASSET_TYPE_LABELS = {
|
const ASSET_TYPE_LABELS = {
|
||||||
'equipment': 'Equipment',
|
'machine': 'Machines',
|
||||||
'computer': 'Computers',
|
'computer': 'Computers',
|
||||||
'printer': 'Printers',
|
'printer': 'Printers',
|
||||||
'network_device': 'Network Devices',
|
'network_device': 'Network Devices',
|
||||||
@@ -12,7 +12,7 @@ const ASSET_TYPE_LABELS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ASSET_TYPE_ROUTES = {
|
const ASSET_TYPE_ROUTES = {
|
||||||
'equipment': '/machines',
|
'machine': '/machines',
|
||||||
'computer': '/pcs',
|
'computer': '/pcs',
|
||||||
'printer': '/printers',
|
'printer': '/printers',
|
||||||
'network_device': '/network',
|
'network_device': '/network',
|
||||||
@@ -21,7 +21,7 @@ const ASSET_TYPE_ROUTES = {
|
|||||||
|
|
||||||
// Plugin-specific id field inside asset.typedata for each asset type.
|
// Plugin-specific id field inside asset.typedata for each asset type.
|
||||||
const ASSET_TYPE_ID_KEYS = {
|
const ASSET_TYPE_ID_KEYS = {
|
||||||
'equipment': 'equipmentid',
|
'machine': 'machineid',
|
||||||
'computer': 'computerid',
|
'computer': 'computerid',
|
||||||
'printer': 'printerid',
|
'printer': 'printerid',
|
||||||
'network_device': 'networkdeviceid',
|
'network_device': 'networkdeviceid',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
// ShopFloorMap.vue - keep the two in sync if the palette changes.
|
// ShopFloorMap.vue - keep the two in sync if the palette changes.
|
||||||
|
|
||||||
export const assetTypeColorsMap = {
|
export const assetTypeColorsMap = {
|
||||||
equipment: '#F44336', // Red
|
machine: '#F44336', // Red
|
||||||
computer: '#2196F3', // Blue
|
computer: '#2196F3', // Blue
|
||||||
printer: '#4CAF50', // Green
|
printer: '#4CAF50', // Green
|
||||||
'network device': '#FF9800', // Orange
|
'network device': '#FF9800', // Orange
|
||||||
@@ -15,7 +15,7 @@ const DEFAULT_COLOR = '#BDBDBD'
|
|||||||
// Canonical form for comparing asset-type strings. The API sends the machine
|
// Canonical form for comparing asset-type strings. The API sends the machine
|
||||||
// type as 'network_device' but subtype keys and labels use 'network device',
|
// type as 'network_device' but subtype keys and labels use 'network device',
|
||||||
// so normalize underscores to spaces before any comparison. Without this,
|
// so normalize underscores to spaces before any comparison. Without this,
|
||||||
// network-device subtypes silently fail to match (equipment/computer/printer
|
// network-device subtypes silently fail to match (machine/computer/printer
|
||||||
// are single words and were unaffected, which is why only network broke).
|
// are single words and were unaffected, which is why only network broke).
|
||||||
export function normalizeAssetType(assettype) {
|
export function normalizeAssetType(assettype) {
|
||||||
return (assettype || '').toLowerCase().replace(/_/g, ' ')
|
return (assettype || '').toLowerCase().replace(/_/g, ' ')
|
||||||
@@ -33,7 +33,7 @@ export function getAssetTypeColor(assettype) {
|
|||||||
export function getSubtypeId(asset) {
|
export function getSubtypeId(asset) {
|
||||||
if (!asset || !asset.typedata) return null
|
if (!asset || !asset.typedata) return null
|
||||||
const typeLower = normalizeAssetType(asset.assettype)
|
const typeLower = normalizeAssetType(asset.assettype)
|
||||||
if (typeLower === 'equipment') return asset.typedata.equipmenttypeid
|
if (typeLower === 'machine') return asset.typedata.machinetypeid
|
||||||
if (typeLower === 'computer') return asset.typedata.computertypeid
|
if (typeLower === 'computer') return asset.typedata.computertypeid
|
||||||
if (typeLower === 'network device') return asset.typedata.networkdevicetypeid
|
if (typeLower === 'network device') return asset.typedata.networkdevicetypeid
|
||||||
if (typeLower === 'printer') return asset.typedata.printertypeid
|
if (typeLower === 'printer') return asset.typedata.printertypeid
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export async function getQrLogo() {
|
|||||||
return (value === undefined || value === null) ? '/ge-monogram.svg' : value
|
return (value === undefined || value === null) ? '/ge-monogram.svg' : value
|
||||||
}
|
}
|
||||||
|
|
||||||
// Logo printed on equipment inspection badges.
|
// Logo printed on machine inspection badges.
|
||||||
export async function getBadgeLogo() {
|
export async function getBadgeLogo() {
|
||||||
return getSetting('badge_logo', '/ge-aerospace-logo.svg')
|
return getSetting('badge_logo', '/ge-aerospace-logo.svg')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="stat-card success">
|
<div class="stat-card success">
|
||||||
<div class="label">Active</div>
|
<div class="label">Active</div>
|
||||||
<div class="value">{{ stats.activemachines || 0 }}</div>
|
<div class="value">{{ stats.activeassets || 0 }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card warning">
|
<div class="stat-card warning">
|
||||||
<div class="label">In Repair</div>
|
<div class="label">In Repair</div>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
<h3>Assets</h3>
|
<h3>Assets</h3>
|
||||||
<select v-model="filterType" class="form-control">
|
<select v-model="filterType" class="form-control">
|
||||||
<option value="">All Types</option>
|
<option value="">All Types</option>
|
||||||
<option value="equipment">Machines</option>
|
<option value="machine">Machines</option>
|
||||||
<option value="computer">Computers</option>
|
<option value="computer">Computers</option>
|
||||||
<option value="printer">Printers</option>
|
<option value="printer">Printers</option>
|
||||||
<option value="network_device">Network Devices</option>
|
<option value="network_device">Network Devices</option>
|
||||||
@@ -155,7 +155,7 @@ async function loadAssets() {
|
|||||||
|
|
||||||
function getTypeIcon(assettype) {
|
function getTypeIcon(assettype) {
|
||||||
const icons = {
|
const icons = {
|
||||||
'equipment': Cog,
|
'machine': Cog,
|
||||||
'computer': Monitor,
|
'computer': Monitor,
|
||||||
'printer': Printer,
|
'printer': Printer,
|
||||||
'network_device': Globe
|
'network_device': Globe
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ const subtypeLabel = computed(() => {
|
|||||||
if (!selectedType.value) return 'Select type first'
|
if (!selectedType.value) return 'Select type first'
|
||||||
if (!currentSubtypes.value.length) return 'No subtypes'
|
if (!currentSubtypes.value.length) return 'No subtypes'
|
||||||
const labels = {
|
const labels = {
|
||||||
'equipment': 'All Machine Types',
|
'machine': 'All Machine Types',
|
||||||
'computer': 'All Computer Types',
|
'computer': 'All Computer Types',
|
||||||
'network device': 'All Device Types',
|
'network device': 'All Device Types',
|
||||||
'printer': 'All Printer Types'
|
'printer': 'All Printer Types'
|
||||||
@@ -182,8 +182,8 @@ const filteredAssets = computed(() => {
|
|||||||
result = result.filter(a => {
|
result = result.filter(a => {
|
||||||
if (!a.typedata) return false
|
if (!a.typedata) return false
|
||||||
// Check different ID fields based on asset type
|
// Check different ID fields based on asset type
|
||||||
if (typeLower === 'equipment') {
|
if (typeLower === 'machine') {
|
||||||
return a.typedata.equipmenttypeid === subtypeId
|
return a.typedata.machinetypeid === subtypeId
|
||||||
} else if (typeLower === 'computer') {
|
} else if (typeLower === 'computer') {
|
||||||
return a.typedata.computertypeid === subtypeId
|
return a.typedata.computertypeid === subtypeId
|
||||||
} else if (typeLower === 'network device') {
|
} else if (typeLower === 'network device') {
|
||||||
|
|||||||
@@ -110,7 +110,6 @@ const copiedId = ref(null)
|
|||||||
|
|
||||||
const typeLabels = {
|
const typeLabels = {
|
||||||
machine: 'Machine',
|
machine: 'Machine',
|
||||||
equipment: 'Machine',
|
|
||||||
pc: 'PC',
|
pc: 'PC',
|
||||||
computer: 'PC',
|
computer: 'PC',
|
||||||
application: 'App',
|
application: 'App',
|
||||||
@@ -124,7 +123,7 @@ const typeLabels = {
|
|||||||
|
|
||||||
const filterTypeMap = {
|
const filterTypeMap = {
|
||||||
all: null,
|
all: null,
|
||||||
equipment: ['equipment'],
|
machines: ['machine'],
|
||||||
computers: ['computer'],
|
computers: ['computer'],
|
||||||
printers: ['printer'],
|
printers: ['printer'],
|
||||||
network: ['network_device', 'subnet'],
|
network: ['network_device', 'subnet'],
|
||||||
@@ -136,7 +135,7 @@ const filterTypeMap = {
|
|||||||
|
|
||||||
const filterList = [
|
const filterList = [
|
||||||
{ key: 'all', label: 'All' },
|
{ key: 'all', label: 'All' },
|
||||||
{ key: 'equipment', label: 'Machines' },
|
{ key: 'machines', label: 'Machines' },
|
||||||
{ key: 'computers', label: 'PCs' },
|
{ key: 'computers', label: 'PCs' },
|
||||||
{ key: 'printers', label: 'Printers' },
|
{ key: 'printers', label: 'Printers' },
|
||||||
{ key: 'network', label: 'Network' },
|
{ key: 'network', label: 'Network' },
|
||||||
@@ -383,8 +382,7 @@ watch(results, () => {
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.result-type.machine,
|
.result-type.machine {
|
||||||
.result-type.equipment {
|
|
||||||
background: #e3f2fd;
|
background: #e3f2fd;
|
||||||
color: #1565c0;
|
color: #1565c0;
|
||||||
}
|
}
|
||||||
@@ -484,8 +482,7 @@ watch(results, () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
.result-type.machine,
|
.result-type.machine {
|
||||||
.result-type.equipment {
|
|
||||||
background: rgba(21, 101, 192, 0.2);
|
background: rgba(21, 101, 192, 0.2);
|
||||||
color: #64b5f6;
|
color: #64b5f6;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,10 @@
|
|||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h2>Machine Details</h2>
|
<h2>Machine 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">
|
<router-link :to="`/print/machine-badge/${machine?.machine?.machineid}`" class="btn btn-secondary" v-if="machine" target="_blank">
|
||||||
Print Badge
|
Print Badge
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link :to="`/machines/${equipment?.equipment?.equipmentid}/edit`" class="btn btn-primary" v-if="equipment">
|
<router-link :to="`/machines/${machine?.machine?.machineid}/edit`" class="btn btn-primary" v-if="machine">
|
||||||
Edit
|
Edit
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link to="/machines" class="btn btn-secondary">Back to List</router-link>
|
<router-link to="/machines" class="btn btn-secondary">Back to List</router-link>
|
||||||
@@ -15,20 +15,20 @@
|
|||||||
|
|
||||||
<div v-if="loading" class="loading">Loading...</div>
|
<div v-if="loading" class="loading">Loading...</div>
|
||||||
|
|
||||||
<template v-else-if="equipment">
|
<template v-else-if="machine">
|
||||||
<!-- Hero Section -->
|
<!-- Hero Section -->
|
||||||
<div class="hero-card">
|
<div class="hero-card">
|
||||||
<div class="hero-content">
|
<div class="hero-content">
|
||||||
<div class="hero-title">
|
<div class="hero-title">
|
||||||
<h1>{{ equipment.assetnumber }}</h1>
|
<h1>{{ machine.assetnumber }}</h1>
|
||||||
<span v-if="equipment.name" class="hero-alias">{{ equipment.name }}</span>
|
<span v-if="machine.name" class="hero-alias">{{ machine.name }}</span>
|
||||||
</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.assettypename || 'Machine' }}
|
{{ machine.assettypename || 'Machine' }}
|
||||||
</span>
|
</span>
|
||||||
<span class="badge badge-lg" :class="getStatusClass(equipment.statusname)">
|
<span class="badge badge-lg" :class="getStatusClass(machine.statusname)">
|
||||||
{{ equipment.statusname || 'Unknown' }}
|
{{ machine.statusname || 'Unknown' }}
|
||||||
</span>
|
</span>
|
||||||
<span v-if="heroWarranty" class="badge badge-lg" :style="{ background: heroWarranty.statuscolor, color: '#fff' }"
|
<span v-if="heroWarranty" class="badge badge-lg" :style="{ background: heroWarranty.statuscolor, color: '#fff' }"
|
||||||
:title="heroWarranty.enddate ? `Warranty ends ${warrantyDate(heroWarranty.enddate)}` : 'Warranty'">
|
:title="heroWarranty.enddate ? `Warranty ends ${warrantyDate(heroWarranty.enddate)}` : 'Warranty'">
|
||||||
@@ -36,21 +36,21 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="hero-details">
|
<div class="hero-details">
|
||||||
<div class="hero-detail" v-if="equipment.equipment?.equipmenttypename">
|
<div class="hero-detail" v-if="machine.machine?.machinetypename">
|
||||||
<span class="hero-detail-label">Type</span>
|
<span class="hero-detail-label">Type</span>
|
||||||
<span class="hero-detail-value">{{ equipment.equipment.equipmenttypename }}</span>
|
<span class="hero-detail-value">{{ machine.machine.machinetypename }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="hero-detail" v-if="equipment.equipment?.vendorname">
|
<div class="hero-detail" v-if="machine.machine?.vendorname">
|
||||||
<span class="hero-detail-label">Vendor</span>
|
<span class="hero-detail-label">Vendor</span>
|
||||||
<span class="hero-detail-value">{{ equipment.equipment.vendorname }}</span>
|
<span class="hero-detail-value">{{ machine.machine.vendorname }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="hero-detail" v-if="equipment.equipment?.modelname">
|
<div class="hero-detail" v-if="machine.machine?.modelname">
|
||||||
<span class="hero-detail-label">Model</span>
|
<span class="hero-detail-label">Model</span>
|
||||||
<span class="hero-detail-value">{{ equipment.equipment.modelname }}</span>
|
<span class="hero-detail-value">{{ machine.machine.modelname }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="hero-detail" v-if="equipment.locationname">
|
<div class="hero-detail" v-if="machine.locationname">
|
||||||
<span class="hero-detail-label">Location</span>
|
<span class="hero-detail-label">Location</span>
|
||||||
<span class="hero-detail-value">{{ equipment.locationname }}</span>
|
<span class="hero-detail-value">{{ machine.locationname }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -66,23 +66,23 @@
|
|||||||
<div class="info-list">
|
<div class="info-list">
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="info-label">Asset Number</span>
|
<span class="info-label">Asset Number</span>
|
||||||
<span class="info-value">{{ equipment.assetnumber }}</span>
|
<span class="info-value">{{ machine.assetnumber }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-row" v-if="equipment.name">
|
<div class="info-row" v-if="machine.name">
|
||||||
<span class="info-label">Name</span>
|
<span class="info-label">Name</span>
|
||||||
<span class="info-value">{{ equipment.name }}</span>
|
<span class="info-value">{{ machine.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-row" v-if="isEnabled('gaugelabreference', 'equipment') && equipment.gaugelabreference">
|
<div class="info-row" v-if="isEnabled('gaugelabreference', 'machine') && machine.gaugelabreference">
|
||||||
<span class="info-label">Gauge Lab Reference</span>
|
<span class="info-label">Gauge Lab Reference</span>
|
||||||
<span class="info-value mono">{{ equipment.gaugelabreference }}</span>
|
<span class="info-value mono">{{ machine.gaugelabreference }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-row" v-if="isEnabled('maintenancereference', 'equipment') && equipment.maintenancereference">
|
<div class="info-row" v-if="isEnabled('maintenancereference', 'machine') && machine.maintenancereference">
|
||||||
<span class="info-label">Maintenance Reference</span>
|
<span class="info-label">Maintenance Reference</span>
|
||||||
<span class="info-value mono">{{ equipment.maintenancereference }}</span>
|
<span class="info-value mono">{{ machine.maintenancereference }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-row" v-if="equipment.serialnumber">
|
<div class="info-row" v-if="machine.serialnumber">
|
||||||
<span class="info-label">Serial Number</span>
|
<span class="info-label">Serial Number</span>
|
||||||
<span class="info-value mono">{{ equipment.serialnumber }}</span>
|
<span class="info-value mono">{{ machine.serialnumber }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -93,43 +93,43 @@
|
|||||||
<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?.equipmenttypename || '-' }}</span>
|
<span class="info-value">{{ machine.machine?.machinetypename || '-' }}</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?.vendorname || '-' }}</span>
|
<span class="info-value">{{ machine.machine?.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?.modelname || '-' }}</span>
|
<span class="info-value">{{ machine.machine?.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?.controllervendorname || equipment.equipment?.controllermodelname">
|
<div class="section-card" v-if="machine.machine?.controllervendorname || machine.machine?.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?.controllervendorname || '-' }}</span>
|
<span class="info-value">{{ machine.machine?.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?.controllermodelname || '-' }}</span>
|
<span class="info-value">{{ machine.machine?.controllermodelname || '-' }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Equipment Configuration -->
|
<!-- Machine Configuration -->
|
||||||
<div class="section-card">
|
<div class="section-card">
|
||||||
<h3 class="section-title">Configuration</h3>
|
<h3 class="section-title">Configuration</h3>
|
||||||
<div class="info-list">
|
<div class="info-list">
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="info-label">Requires Manual Config</span>
|
<span class="info-label">Requires Manual Config</span>
|
||||||
<span class="info-value">
|
<span class="info-value">
|
||||||
<span class="feature-tag" :class="{ active: equipment.equipment?.requiresmanualconfig }">
|
<span class="feature-tag" :class="{ active: machine.machine?.requiresmanualconfig }">
|
||||||
{{ equipment.equipment?.requiresmanualconfig ? 'Yes' : 'No' }}
|
{{ machine.machine?.requiresmanualconfig ? 'Yes' : 'No' }}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -137,20 +137,20 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Maintenance Section -->
|
<!-- Maintenance Section -->
|
||||||
<div class="section-card" v-if="equipment.equipment?.lastmaintenancedate || equipment.equipment?.nextmaintenancedate">
|
<div class="section-card" v-if="machine.machine?.lastmaintenancedate || machine.machine?.nextmaintenancedate">
|
||||||
<h3 class="section-title">Maintenance</h3>
|
<h3 class="section-title">Maintenance</h3>
|
||||||
<div class="info-list">
|
<div class="info-list">
|
||||||
<div class="info-row" v-if="equipment.equipment?.lastmaintenancedate">
|
<div class="info-row" v-if="machine.machine?.lastmaintenancedate">
|
||||||
<span class="info-label">Last Maintenance</span>
|
<span class="info-label">Last Maintenance</span>
|
||||||
<span class="info-value">{{ formatDate(equipment.equipment.lastmaintenancedate) }}</span>
|
<span class="info-value">{{ formatDate(machine.machine.lastmaintenancedate) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-row" v-if="equipment.equipment?.nextmaintenancedate">
|
<div class="info-row" v-if="machine.machine?.nextmaintenancedate">
|
||||||
<span class="info-label">Next Maintenance</span>
|
<span class="info-label">Next Maintenance</span>
|
||||||
<span class="info-value">{{ formatDate(equipment.equipment.nextmaintenancedate) }}</span>
|
<span class="info-value">{{ formatDate(machine.machine.nextmaintenancedate) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-row" v-if="equipment.equipment?.maintenanceintervaldays">
|
<div class="info-row" v-if="machine.machine?.maintenanceintervaldays">
|
||||||
<span class="info-label">Interval</span>
|
<span class="info-label">Interval</span>
|
||||||
<span class="info-value">{{ equipment.equipment.maintenanceintervaldays }} days</span>
|
<span class="info-value">{{ machine.machine.maintenanceintervaldays }} days</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -166,19 +166,19 @@
|
|||||||
<span class="info-label">Location</span>
|
<span class="info-label">Location</span>
|
||||||
<span class="info-value">
|
<span class="info-value">
|
||||||
<LocationMapTooltip
|
<LocationMapTooltip
|
||||||
v-if="equipment.mapx != null && equipment.mapy != null"
|
v-if="machine.mapx != null && machine.mapy != null"
|
||||||
:left="equipment.mapx"
|
:left="machine.mapx"
|
||||||
:top="equipment.mapy"
|
:top="machine.mapy"
|
||||||
:machineName="equipment.assetnumber"
|
:machineName="machine.assetnumber"
|
||||||
>
|
>
|
||||||
<span class="location-link">{{ equipment.locationname || 'On Map' }}</span>
|
<span class="location-link">{{ machine.locationname || 'On Map' }}</span>
|
||||||
</LocationMapTooltip>
|
</LocationMapTooltip>
|
||||||
<span v-else>{{ equipment.locationname || '-' }}</span>
|
<span v-else>{{ machine.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.businessunitname || '-' }}</span>
|
<span class="info-value">{{ machine.businessunitname || '-' }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -208,23 +208,26 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Custom Fields -->
|
<!-- Custom Fields -->
|
||||||
<CustomFieldsSection :assetid="equipment.assetid" />
|
<CustomFieldsSection :assetid="machine.assetid" />
|
||||||
|
|
||||||
<!-- Warranty -->
|
<!-- Warranty -->
|
||||||
<WarrantyPanel :assetid="equipment.assetid" :items="warranties" />
|
<WarrantyPanel :assetid="machine.assetid" :items="warranties" />
|
||||||
|
|
||||||
|
<!-- All relationships (dualpath, controls, ...) -->
|
||||||
|
<AssetRelationships v-if="machine.assetid" :assetId="machine.assetid" />
|
||||||
|
|
||||||
<!-- Notes -->
|
<!-- Notes -->
|
||||||
<div class="section-card" v-if="equipment.notes">
|
<div class="section-card" v-if="machine.notes">
|
||||||
<h3 class="section-title">Notes</h3>
|
<h3 class="section-title">Notes</h3>
|
||||||
<p class="notes-text">{{ equipment.notes }}</p>
|
<p class="notes-text">{{ machine.notes }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Audit Footer -->
|
<!-- Audit Footer -->
|
||||||
<div class="audit-footer">
|
<div class="audit-footer">
|
||||||
<span>Created {{ formatDate(equipment.createddate) }}<template v-if="equipment.createdby"> by {{ equipment.createdby }}</template></span>
|
<span>Created {{ formatDate(machine.createddate) }}<template v-if="machine.createdby"> by {{ machine.createdby }}</template></span>
|
||||||
<span>Modified {{ formatDate(equipment.modifieddate) }}<template v-if="equipment.modifiedby"> by {{ equipment.modifiedby }}</template></span>
|
<span>Modified {{ formatDate(machine.modifieddate) }}<template v-if="machine.modifiedby"> by {{ machine.modifiedby }}</template></span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -237,10 +240,11 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { equipmentApi, assetsApi } from '../../api'
|
import { machinesApi, assetsApi } from '../../api'
|
||||||
import LocationMapTooltip from '../../components/LocationMapTooltip.vue'
|
import LocationMapTooltip from '../../components/LocationMapTooltip.vue'
|
||||||
import CustomFieldsSection from '../../components/CustomFieldsSection.vue'
|
import CustomFieldsSection from '../../components/CustomFieldsSection.vue'
|
||||||
import WarrantyPanel from '../../components/WarrantyPanel.vue'
|
import WarrantyPanel from '../../components/WarrantyPanel.vue'
|
||||||
|
import AssetRelationships from '../../components/AssetRelationships.vue'
|
||||||
import { useWarrantyBadge } from '../../composables/warrantyBadge'
|
import { useWarrantyBadge } from '../../composables/warrantyBadge'
|
||||||
import { useIdentifierFlags } from '../../composables/identifierSettings'
|
import { useIdentifierFlags } from '../../composables/identifierSettings'
|
||||||
|
|
||||||
@@ -248,8 +252,8 @@ const route = useRoute()
|
|||||||
const { isEnabled } = useIdentifierFlags()
|
const { isEnabled } = useIdentifierFlags()
|
||||||
|
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const equipment = ref(null)
|
const machine = ref(null)
|
||||||
const { warranties, heroWarranty, warrantyDate } = useWarrantyBadge(() => equipment.value?.assetid)
|
const { warranties, heroWarranty, warrantyDate } = useWarrantyBadge(() => machine.value?.assetid)
|
||||||
const relationships = ref({ incoming: [], outgoing: [] })
|
const relationships = ref({ incoming: [], outgoing: [] })
|
||||||
|
|
||||||
// type name is data, sites may seed 'controls' or 'Controls' - compare folded
|
// type name is data, sites may seed 'controls' or 'Controls' - compare folded
|
||||||
@@ -258,10 +262,10 @@ function isControls(rel) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const controllingPc = computed(() => {
|
const controllingPc = computed(() => {
|
||||||
// For equipment, find a related computer in any "Controls" relationship
|
// For a machine, find a related computer in any "Controls" relationship
|
||||||
// Check both incoming (computer controls this) and outgoing (legacy data may have equipment -> computer)
|
// Check both incoming (computer controls this) and outgoing (legacy data may have machine -> computer)
|
||||||
|
|
||||||
// First check incoming - computer as source controlling this equipment
|
// First check incoming - computer as source controlling this machine
|
||||||
for (const rel of relationships.value.incoming || []) {
|
for (const rel of relationships.value.incoming || []) {
|
||||||
if (rel.sourceasset?.assettypename === 'computer' && isControls(rel)) {
|
if (rel.sourceasset?.assettypename === 'computer' && isControls(rel)) {
|
||||||
return {
|
return {
|
||||||
@@ -271,7 +275,7 @@ const controllingPc = computed(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also check outgoing - legacy data may have equipment -> computer Controls relationships
|
// Also check outgoing - legacy data may have machine -> computer Controls relationships
|
||||||
for (const rel of relationships.value.outgoing || []) {
|
for (const rel of relationships.value.outgoing || []) {
|
||||||
if (rel.targetasset?.assettypename === 'computer' && isControls(rel)) {
|
if (rel.targetasset?.assettypename === 'computer' && isControls(rel)) {
|
||||||
return {
|
return {
|
||||||
@@ -286,20 +290,20 @@ const controllingPc = computed(() => {
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
const response = await equipmentApi.get(route.params.id)
|
const response = await machinesApi.get(route.params.id)
|
||||||
equipment.value = response.data.data
|
machine.value = response.data.data
|
||||||
|
|
||||||
// Load relationships using asset ID
|
// Load relationships using asset ID
|
||||||
if (equipment.value?.assetid) {
|
if (machine.value?.assetid) {
|
||||||
try {
|
try {
|
||||||
const relResponse = await assetsApi.getRelationships(equipment.value.assetid)
|
const relResponse = await assetsApi.getRelationships(machine.value.assetid)
|
||||||
relationships.value = relResponse.data.data || { incoming: [], outgoing: [] }
|
relationships.value = relResponse.data.data || { incoming: [], outgoing: [] }
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('Relationships not available')
|
console.log('Relationships not available')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading equipment:', error)
|
console.error('Error loading machine:', error)
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div v-if="loading" class="loading">Loading...</div>
|
<div v-if="loading" class="loading">Loading...</div>
|
||||||
|
|
||||||
<form v-else @submit.prevent="saveEquipment">
|
<form v-else @submit.prevent="saveMachine">
|
||||||
<!-- Identity Section -->
|
<!-- Identity Section -->
|
||||||
<h3 class="form-section-title">Identity</h3>
|
<h3 class="form-section-title">Identity</h3>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
@@ -34,8 +34,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-row" v-if="isEnabled('gaugelabreference', 'equipment') || isEnabled('maintenancereference', 'equipment')">
|
<div class="form-row" v-if="isEnabled('gaugelabreference', 'machine') || isEnabled('maintenancereference', 'machine')">
|
||||||
<div class="form-group" v-if="isEnabled('gaugelabreference', 'equipment')">
|
<div class="form-group" v-if="isEnabled('gaugelabreference', 'machine')">
|
||||||
<label for="gaugelabreference">Gauge Lab Reference</label>
|
<label for="gaugelabreference">Gauge Lab Reference</label>
|
||||||
<input
|
<input
|
||||||
id="gaugelabreference"
|
id="gaugelabreference"
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
<small class="form-help">Authoritative gauge lab asset reference (if tracked)</small>
|
<small class="form-help">Authoritative gauge lab asset reference (if tracked)</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group" v-if="isEnabled('maintenancereference', 'equipment')">
|
<div class="form-group" v-if="isEnabled('maintenancereference', 'machine')">
|
||||||
<label for="maintenancereference">Maintenance Reference</label>
|
<label for="maintenancereference">Maintenance Reference</label>
|
||||||
<input
|
<input
|
||||||
id="maintenancereference"
|
id="maintenancereference"
|
||||||
@@ -88,23 +88,23 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Equipment Hardware Section -->
|
<!-- Machine Hardware Section -->
|
||||||
<h3 class="form-section-title">Machine Hardware</h3>
|
<h3 class="form-section-title">Machine Hardware</h3>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="equipmenttypeid">Equipment Type</label>
|
<label for="machinetypeid">Machine Type</label>
|
||||||
<select
|
<select
|
||||||
id="equipmenttypeid"
|
id="machinetypeid"
|
||||||
v-model="form.equipmenttypeid"
|
v-model="form.machinetypeid"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
>
|
>
|
||||||
<option value="">Select type...</option>
|
<option value="">Select type...</option>
|
||||||
<option
|
<option
|
||||||
v-for="et in equipmentTypes"
|
v-for="mt in machineTypes"
|
||||||
:key="et.equipmenttypeid"
|
:key="mt.machinetypeid"
|
||||||
:value="et.equipmenttypeid"
|
:value="mt.machinetypeid"
|
||||||
>
|
>
|
||||||
{{ et.equipmenttype }}
|
{{ mt.machinetype }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -294,7 +294,7 @@
|
|||||||
{{ pc.assetnumber }}{{ pc.name ? ` (${pc.name})` : '' }}
|
{{ pc.assetnumber }}{{ pc.name ? ` (${pc.name})` : '' }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<small class="form-help">Select the PC that controls this equipment</small>
|
<small class="form-help">Select the PC that controls this machine</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group" v-if="controllingPcId">
|
<div class="form-group" v-if="controllingPcId">
|
||||||
@@ -312,7 +312,7 @@
|
|||||||
{{ rt.relationshiptype }}
|
{{ rt.relationshiptype }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<small class="form-help">How the PC connects to this equipment</small>
|
<small class="form-help">How the PC connects to this machine</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -328,8 +328,8 @@
|
|||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Site-defined custom fields for equipment -->
|
<!-- Site-defined custom fields for machines -->
|
||||||
<CustomFieldsInputs ref="customFieldsRef" :assettypeid="EQUIPMENT_ASSETTYPEID" :assetid="currentAssetId" />
|
<CustomFieldsInputs ref="customFieldsRef" :assettypeid="MACHINE_ASSETTYPEID" :assetid="currentAssetId" />
|
||||||
|
|
||||||
<div v-if="error" class="error-message">{{ error }}</div>
|
<div v-if="error" class="error-message">{{ error }}</div>
|
||||||
|
|
||||||
@@ -347,7 +347,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, computed, watch } from 'vue'
|
import { ref, onMounted, computed, watch } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { equipmentApi, vendorsApi, locationsApi, modelsApi, businessunitsApi, computersApi, assetsApi } from '../../api'
|
import { machinesApi, vendorsApi, locationsApi, modelsApi, businessunitsApi, computersApi, assetsApi } from '../../api'
|
||||||
import ShopFloorMap from '../../components/ShopFloorMap.vue'
|
import ShopFloorMap from '../../components/ShopFloorMap.vue'
|
||||||
import Modal from '../../components/Modal.vue'
|
import Modal from '../../components/Modal.vue'
|
||||||
import CustomFieldsInputs from '../../components/CustomFieldsInputs.vue'
|
import CustomFieldsInputs from '../../components/CustomFieldsInputs.vue'
|
||||||
@@ -362,8 +362,8 @@ const router = useRouter()
|
|||||||
|
|
||||||
const isEdit = computed(() => !!route.params.id)
|
const isEdit = computed(() => !!route.params.id)
|
||||||
|
|
||||||
// Seeded asset-type id for equipment (see /api/assets/types).
|
// Seeded asset-type id for machines (see /api/assets/types).
|
||||||
const EQUIPMENT_ASSETTYPEID = 1
|
const MACHINE_ASSETTYPEID = 1
|
||||||
const customFieldsRef = ref(null)
|
const customFieldsRef = ref(null)
|
||||||
const currentAssetId = ref(null)
|
const currentAssetId = ref(null)
|
||||||
|
|
||||||
@@ -380,7 +380,7 @@ const form = ref({
|
|||||||
maintenancereference: '',
|
maintenancereference: '',
|
||||||
serialnumber: '',
|
serialnumber: '',
|
||||||
statusid: 1,
|
statusid: 1,
|
||||||
equipmenttypeid: '',
|
machinetypeid: '',
|
||||||
vendorid: '',
|
vendorid: '',
|
||||||
modelnumberid: '',
|
modelnumberid: '',
|
||||||
controllervendorid: '',
|
controllervendorid: '',
|
||||||
@@ -394,7 +394,7 @@ const form = ref({
|
|||||||
mapy: null
|
mapy: null
|
||||||
})
|
})
|
||||||
|
|
||||||
const equipmentTypes = ref([])
|
const machineTypes = ref([])
|
||||||
const statuses = ref([])
|
const statuses = ref([])
|
||||||
const vendors = ref([])
|
const vendors = ref([])
|
||||||
const locations = ref([])
|
const locations = ref([])
|
||||||
@@ -405,9 +405,9 @@ const relationshipTypes = ref([])
|
|||||||
const controllingPcId = ref(null)
|
const controllingPcId = ref(null)
|
||||||
const relationshipTypeId = ref(null)
|
const relationshipTypeId = ref(null)
|
||||||
const existingRelationshipId = ref(null)
|
const existingRelationshipId = ref(null)
|
||||||
const currentEquipment = ref(null)
|
const currentMachine = ref(null)
|
||||||
|
|
||||||
// Filter models by selected equipment vendor
|
// Filter models by selected machine vendor
|
||||||
const filteredModels = computed(() => {
|
const filteredModels = computed(() => {
|
||||||
if (!form.value.vendorid) return models.value
|
if (!form.value.vendorid) return models.value
|
||||||
return models.value.filter(m => m.vendorid === form.value.vendorid)
|
return models.value.filter(m => m.vendorid === form.value.vendorid)
|
||||||
@@ -443,7 +443,7 @@ onMounted(async () => {
|
|||||||
try {
|
try {
|
||||||
// Load reference data in parallel
|
// Load reference data in parallel
|
||||||
const [typesRes, statusRes, vendorRes, locRes, allModels, buRes, pcsRes, relTypesRes] = await Promise.all([
|
const [typesRes, statusRes, vendorRes, locRes, allModels, buRes, pcsRes, relTypesRes] = await Promise.all([
|
||||||
equipmentApi.types.list(),
|
machinesApi.types.list(),
|
||||||
assetsApi.statuses.list(),
|
assetsApi.statuses.list(),
|
||||||
vendorsApi.list({ perpage: 500 }),
|
vendorsApi.list({ perpage: 500 }),
|
||||||
locationsApi.list({ perpage: 500 }),
|
locationsApi.list({ perpage: 500 }),
|
||||||
@@ -453,7 +453,7 @@ onMounted(async () => {
|
|||||||
assetsApi.types.list() // Used for relationship types, will fix below
|
assetsApi.types.list() // Used for relationship types, will fix below
|
||||||
])
|
])
|
||||||
|
|
||||||
equipmentTypes.value = typesRes.data.data || []
|
machineTypes.value = typesRes.data.data || []
|
||||||
statuses.value = statusRes.data.data || []
|
statuses.value = statusRes.data.data || []
|
||||||
vendors.value = vendorRes.data.data || []
|
vendors.value = vendorRes.data.data || []
|
||||||
locations.value = locRes.data.data || []
|
locations.value = locRes.data.data || []
|
||||||
@@ -479,12 +479,12 @@ onMounted(async () => {
|
|||||||
relationshipTypeId.value = controlsType.relationshiptypeid
|
relationshipTypeId.value = controlsType.relationshiptypeid
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load equipment if editing
|
// Load machine if editing
|
||||||
if (isEdit.value) {
|
if (isEdit.value) {
|
||||||
const response = await equipmentApi.get(route.params.id)
|
const response = await machinesApi.get(route.params.id)
|
||||||
const data = response.data.data
|
const data = response.data.data
|
||||||
currentAssetId.value = data.assetid || null
|
currentAssetId.value = data.assetid || null
|
||||||
currentEquipment.value = data
|
currentMachine.value = data
|
||||||
|
|
||||||
form.value = {
|
form.value = {
|
||||||
assetnumber: data.assetnumber || '',
|
assetnumber: data.assetnumber || '',
|
||||||
@@ -493,15 +493,15 @@ onMounted(async () => {
|
|||||||
maintenancereference: data.maintenancereference || '',
|
maintenancereference: data.maintenancereference || '',
|
||||||
serialnumber: data.serialnumber || '',
|
serialnumber: data.serialnumber || '',
|
||||||
statusid: data.statusid || 1,
|
statusid: data.statusid || 1,
|
||||||
equipmenttypeid: data.equipment?.equipmenttypeid || '',
|
machinetypeid: data.machine?.machinetypeid || '',
|
||||||
vendorid: data.equipment?.vendorid || '',
|
vendorid: data.machine?.vendorid || '',
|
||||||
modelnumberid: data.equipment?.modelnumberid || '',
|
modelnumberid: data.machine?.modelnumberid || '',
|
||||||
controllervendorid: data.equipment?.controllervendorid || '',
|
controllervendorid: data.machine?.controllervendorid || '',
|
||||||
controllermodelid: data.equipment?.controllermodelid || '',
|
controllermodelid: data.machine?.controllermodelid || '',
|
||||||
locationid: data.locationid || '',
|
locationid: data.locationid || '',
|
||||||
businessunitid: data.businessunitid || '',
|
businessunitid: data.businessunitid || '',
|
||||||
requiresmanualconfig: data.equipment?.requiresmanualconfig || false,
|
requiresmanualconfig: data.machine?.requiresmanualconfig || false,
|
||||||
islocationonly: data.equipment?.islocationonly || false,
|
islocationonly: data.machine?.islocationonly || false,
|
||||||
notes: data.notes || '',
|
notes: data.notes || '',
|
||||||
mapx: data.mapx ?? null,
|
mapx: data.mapx ?? null,
|
||||||
mapy: data.mapy ?? null
|
mapy: data.mapy ?? null
|
||||||
@@ -553,7 +553,7 @@ function clearMapPosition() {
|
|||||||
tempMapPosition.value = null
|
tempMapPosition.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveEquipment() {
|
async function saveMachine() {
|
||||||
error.value = ''
|
error.value = ''
|
||||||
saving.value = true
|
saving.value = true
|
||||||
|
|
||||||
@@ -565,7 +565,7 @@ async function saveEquipment() {
|
|||||||
maintenancereference: form.value.maintenancereference || null,
|
maintenancereference: form.value.maintenancereference || null,
|
||||||
serialnumber: form.value.serialnumber || null,
|
serialnumber: form.value.serialnumber || null,
|
||||||
statusid: form.value.statusid || 1,
|
statusid: form.value.statusid || 1,
|
||||||
equipmenttypeid: form.value.equipmenttypeid || null,
|
machinetypeid: form.value.machinetypeid || null,
|
||||||
vendorid: form.value.vendorid || null,
|
vendorid: form.value.vendorid || null,
|
||||||
modelnumberid: form.value.modelnumberid || null,
|
modelnumberid: form.value.modelnumberid || null,
|
||||||
controllervendorid: form.value.controllervendorid || null,
|
controllervendorid: form.value.controllervendorid || null,
|
||||||
@@ -579,17 +579,17 @@ async function saveEquipment() {
|
|||||||
mapy: form.value.mapy
|
mapy: form.value.mapy
|
||||||
}
|
}
|
||||||
|
|
||||||
let savedEquipment
|
let savedMachine
|
||||||
let assetId
|
let assetId
|
||||||
|
|
||||||
if (isEdit.value) {
|
if (isEdit.value) {
|
||||||
const response = await equipmentApi.update(route.params.id, data)
|
const response = await machinesApi.update(route.params.id, data)
|
||||||
savedEquipment = response.data.data
|
savedMachine = response.data.data
|
||||||
assetId = savedEquipment.assetid
|
assetId = savedMachine.assetid
|
||||||
} else {
|
} else {
|
||||||
const response = await equipmentApi.create(data)
|
const response = await machinesApi.create(data)
|
||||||
savedEquipment = response.data.data
|
savedMachine = response.data.data
|
||||||
assetId = savedEquipment.assetid
|
assetId = savedMachine.assetid
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle relationship (controlling PC)
|
// Handle relationship (controlling PC)
|
||||||
@@ -604,10 +604,10 @@ async function saveEquipment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
router.push(`/machines/${savedEquipment.equipment?.equipmentid || route.params.id}`)
|
router.push(`/machines/${savedMachine.machine?.machineid || route.params.id}`)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error saving equipment:', err)
|
console.error('Error saving machine:', err)
|
||||||
error.value = apiError(err, 'Failed to save equipment')
|
error.value = apiError(err, 'Failed to save machine')
|
||||||
} finally {
|
} finally {
|
||||||
saving.value = false
|
saving.value = false
|
||||||
}
|
}
|
||||||
@@ -632,7 +632,7 @@ async function saveRelationship(assetId) {
|
|||||||
await assetsApi.deleteRelationship(existingRelationshipId.value)
|
await assetsApi.deleteRelationship(existingRelationshipId.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new relationship (PC controls Equipment, so PC is source, Equipment is target)
|
// Create new relationship (PC controls Machine, so PC is source, Machine is target)
|
||||||
await assetsApi.createRelationship({
|
await assetsApi.createRelationship({
|
||||||
sourceassetid: controllingPcId.value,
|
sourceassetid: controllingPcId.value,
|
||||||
targetassetid: assetId,
|
targetassetid: assetId,
|
||||||
|
|||||||
@@ -35,12 +35,12 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="item in equipment" :key="item.assetid">
|
<tr v-for="item in machines" :key="item.assetid">
|
||||||
<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?.equipmenttypename || '-' }}</td>
|
<td>{{ item.machine?.machinetypename || '-' }}</td>
|
||||||
<td>{{ item.equipment?.vendorname || '-' }}</td>
|
<td>{{ item.machine?.vendorname || '-' }}</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="badge" :style="colorStyle(item.statuscolor)">
|
<span class="badge" :style="colorStyle(item.statuscolor)">
|
||||||
{{ item.statusname || 'Unknown' }}
|
{{ item.statusname || 'Unknown' }}
|
||||||
@@ -49,16 +49,16 @@
|
|||||||
<td>{{ item.locationname || '-' }}</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.machine?.machineid || item.assetid}`"
|
||||||
class="btn btn-secondary btn-sm"
|
class="btn btn-secondary btn-sm"
|
||||||
>
|
>
|
||||||
View
|
View
|
||||||
</router-link>
|
</router-link>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-if="equipment.length === 0">
|
<tr v-if="machines.length === 0">
|
||||||
<td colspan="8" style="text-align: center; color: var(--text-light);">
|
<td colspan="8" style="text-align: center; color: var(--text-light);">
|
||||||
No equipment found
|
No machines found
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -82,10 +82,10 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { colorStyle } from "@/utils/colorStyle"
|
import { colorStyle } from "@/utils/colorStyle"
|
||||||
import { equipmentApi } from '../../api'
|
import { machinesApi } from '../../api'
|
||||||
import PaginationBar from '../../components/PaginationBar.vue'
|
import PaginationBar from '../../components/PaginationBar.vue'
|
||||||
|
|
||||||
const equipment = ref([])
|
const machines = ref([])
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const search = ref('')
|
const search = ref('')
|
||||||
const page = ref(1)
|
const page = ref(1)
|
||||||
@@ -95,10 +95,10 @@ const perPage = ref(20)
|
|||||||
let searchTimeout = null
|
let searchTimeout = null
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadEquipment()
|
loadMachines()
|
||||||
})
|
})
|
||||||
|
|
||||||
async function loadEquipment() {
|
async function loadMachines() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const params = {
|
const params = {
|
||||||
@@ -107,11 +107,11 @@ async function loadEquipment() {
|
|||||||
}
|
}
|
||||||
if (search.value) params.search = search.value
|
if (search.value) params.search = search.value
|
||||||
|
|
||||||
const response = await equipmentApi.list(params)
|
const response = await machinesApi.list(params)
|
||||||
equipment.value = response.data.data || []
|
machines.value = response.data.data || []
|
||||||
totalPages.value = response.data.meta?.pagination?.totalpages || 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 machines:', error)
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
@@ -121,19 +121,19 @@ function debouncedSearch() {
|
|||||||
clearTimeout(searchTimeout)
|
clearTimeout(searchTimeout)
|
||||||
searchTimeout = setTimeout(() => {
|
searchTimeout = setTimeout(() => {
|
||||||
page.value = 1
|
page.value = 1
|
||||||
loadEquipment()
|
loadMachines()
|
||||||
}, 300)
|
}, 300)
|
||||||
}
|
}
|
||||||
|
|
||||||
function goToPage(p) {
|
function goToPage(p) {
|
||||||
page.value = p
|
page.value = p
|
||||||
loadEquipment()
|
loadMachines()
|
||||||
}
|
}
|
||||||
|
|
||||||
function changePerPage(newPerPage) {
|
function changePerPage(newPerPage) {
|
||||||
perPage.value = newPerPage
|
perPage.value = newPerPage
|
||||||
page.value = 1
|
page.value = 1
|
||||||
loadEquipment()
|
loadMachines()
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -131,6 +131,9 @@
|
|||||||
<!-- Warranty -->
|
<!-- Warranty -->
|
||||||
<WarrantyPanel :assetid="tool.assetid" :items="warranties" />
|
<WarrantyPanel :assetid="tool.assetid" :items="warranties" />
|
||||||
|
|
||||||
|
<!-- All relationships (partof, connectedto, ...) -->
|
||||||
|
<AssetRelationships v-if="tool.assetid" :assetId="tool.assetid" />
|
||||||
|
|
||||||
<!-- Notes -->
|
<!-- Notes -->
|
||||||
<div class="section-card" v-if="tool.notes">
|
<div class="section-card" v-if="tool.notes">
|
||||||
<h3 class="section-title">Notes</h3>
|
<h3 class="section-title">Notes</h3>
|
||||||
@@ -159,6 +162,7 @@ import { colorStyle } from '@/utils/colorStyle'
|
|||||||
import { measuringtoolsApi } from '../../api'
|
import { measuringtoolsApi } from '../../api'
|
||||||
import CustomFieldsSection from '../../components/CustomFieldsSection.vue'
|
import CustomFieldsSection from '../../components/CustomFieldsSection.vue'
|
||||||
import WarrantyPanel from '../../components/WarrantyPanel.vue'
|
import WarrantyPanel from '../../components/WarrantyPanel.vue'
|
||||||
|
import AssetRelationships from '../../components/AssetRelationships.vue'
|
||||||
import { useWarrantyBadge } from '../../composables/warrantyBadge'
|
import { useWarrantyBadge } from '../../composables/warrantyBadge'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|||||||
@@ -180,9 +180,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Controlled Equipment -->
|
<!-- Controlled Machines -->
|
||||||
<div class="section-card" v-if="controlledEquipment.length > 0">
|
<div class="section-card" v-if="controlledEquipment.length > 0">
|
||||||
<h3 class="section-title">Controlled Equipment</h3>
|
<h3 class="section-title">Controlled Machines</h3>
|
||||||
<div class="equipment-list">
|
<div class="equipment-list">
|
||||||
<router-link
|
<router-link
|
||||||
v-for="item in controlledEquipment"
|
v-for="item in controlledEquipment"
|
||||||
@@ -229,6 +229,9 @@
|
|||||||
<!-- Warranty -->
|
<!-- Warranty -->
|
||||||
<WarrantyPanel :assetid="computer.assetid" :items="warranties" />
|
<WarrantyPanel :assetid="computer.assetid" :items="warranties" />
|
||||||
|
|
||||||
|
<!-- All relationships (controls, defaultprinter, ...) -->
|
||||||
|
<AssetRelationships v-if="computer.assetid" :assetId="computer.assetid" />
|
||||||
|
|
||||||
<!-- Notes -->
|
<!-- Notes -->
|
||||||
<div class="section-card" v-if="computer.notes">
|
<div class="section-card" v-if="computer.notes">
|
||||||
<h3 class="section-title">Notes</h3>
|
<h3 class="section-title">Notes</h3>
|
||||||
@@ -259,6 +262,7 @@ import { useWarrantyBadge } from '../../composables/warrantyBadge'
|
|||||||
import LocationMapTooltip from '../../components/LocationMapTooltip.vue'
|
import LocationMapTooltip from '../../components/LocationMapTooltip.vue'
|
||||||
import CustomFieldsSection from '../../components/CustomFieldsSection.vue'
|
import CustomFieldsSection from '../../components/CustomFieldsSection.vue'
|
||||||
import WarrantyPanel from '../../components/WarrantyPanel.vue'
|
import WarrantyPanel from '../../components/WarrantyPanel.vue'
|
||||||
|
import AssetRelationships from '../../components/AssetRelationships.vue'
|
||||||
import { useIdentifierFlags } from '../../composables/identifierSettings'
|
import { useIdentifierFlags } from '../../composables/identifierSettings'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -283,7 +287,7 @@ 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.targetasset?.assettypename === 'equipment' && isControls(rel)) {
|
if (rel.targetasset?.assettypename === 'machine' && isControls(rel)) {
|
||||||
items.push({
|
items.push({
|
||||||
...rel.targetasset,
|
...rel.targetasset,
|
||||||
relationshipid: rel.relationshipid,
|
relationshipid: rel.relationshipid,
|
||||||
@@ -294,7 +298,7 @@ const controlledEquipment = computed(() => {
|
|||||||
|
|
||||||
// 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.sourceasset?.assettypename === 'equipment' && isControls(rel)) {
|
if (rel.sourceasset?.assettypename === 'machine' && isControls(rel)) {
|
||||||
items.push({
|
items.push({
|
||||||
...rel.sourceasset,
|
...rel.sourceasset,
|
||||||
relationshipid: rel.relationshipid,
|
relationshipid: rel.relationshipid,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<button class="print-btn" @click="print" v-if="!loading">Print Badge</button>
|
<button class="print-btn" @click="print" v-if="!loading">Print Badge</button>
|
||||||
<div v-if="loading" class="loading-msg">Loading...</div>
|
<div v-if="loading" class="loading-msg">Loading...</div>
|
||||||
<div v-else-if="equipment" class="badge-container">
|
<div v-else-if="machine" class="badge-container">
|
||||||
<div class="model-name">{{ modelName }}</div>
|
<div class="model-name">{{ modelName }}</div>
|
||||||
<img
|
<img
|
||||||
v-if="isInspection"
|
v-if="isInspection"
|
||||||
@@ -18,41 +18,41 @@
|
|||||||
/>
|
/>
|
||||||
<div class="barcode-container">
|
<div class="barcode-container">
|
||||||
<svg ref="barcodeEl"></svg>
|
<svg ref="barcodeEl"></svg>
|
||||||
<div class="machine-number">*{{ equipment.assetnumber }}*</div>
|
<div class="machine-number">*{{ machine.assetnumber }}*</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="error-msg">Equipment not found</div>
|
<div v-else class="error-msg">Machine not found</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted, nextTick } from 'vue'
|
import { ref, computed, onMounted, nextTick } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { equipmentApi } from '../../api'
|
import { machinesApi } from '../../api'
|
||||||
import { getBadgeLogo } from '@/utils/siteSettings'
|
import { getBadgeLogo } from '@/utils/siteSettings'
|
||||||
import JsBarcode from 'jsbarcode'
|
import JsBarcode from 'jsbarcode'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const equipment = ref(null)
|
const machine = ref(null)
|
||||||
const barcodeEl = ref(null)
|
const barcodeEl = ref(null)
|
||||||
|
|
||||||
const geLogo = ref('/ge-aerospace-logo.svg')
|
const geLogo = ref('/ge-aerospace-logo.svg')
|
||||||
|
|
||||||
const isInspection = computed(() => {
|
const isInspection = computed(() => {
|
||||||
if (!equipment.value) return false
|
if (!machine.value) return false
|
||||||
return equipment.value.assetnumber?.startsWith('06')
|
return machine.value.assetnumber?.startsWith('06')
|
||||||
})
|
})
|
||||||
|
|
||||||
const modelName = computed(() => {
|
const modelName = computed(() => {
|
||||||
if (!equipment.value) return ''
|
if (!machine.value) return ''
|
||||||
if (isInspection.value) return 'Inspection'
|
if (isInspection.value) return 'Inspection'
|
||||||
return equipment.value.equipment?.modelname || ''
|
return machine.value.machine?.modelname || ''
|
||||||
})
|
})
|
||||||
|
|
||||||
const imageUrl = computed(() => {
|
const imageUrl = computed(() => {
|
||||||
if (!equipment.value) return null
|
if (!machine.value) return null
|
||||||
const img = equipment.value.equipment?.imageurl
|
const img = machine.value.machine?.imageurl
|
||||||
if (img) return img.startsWith('/') ? img : `/images/models/machines/${img}`
|
if (img) return img.startsWith('/') ? img : `/images/models/machines/${img}`
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
@@ -60,10 +60,10 @@ const imageUrl = computed(() => {
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
getBadgeLogo().then(logo => { geLogo.value = logo })
|
getBadgeLogo().then(logo => { geLogo.value = logo })
|
||||||
try {
|
try {
|
||||||
const response = await equipmentApi.get(route.params.id)
|
const response = await machinesApi.get(route.params.id)
|
||||||
equipment.value = response.data.data
|
machine.value = response.data.data
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading equipment:', error)
|
console.error('Error loading machine:', error)
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
await nextTick()
|
await nextTick()
|
||||||
@@ -72,9 +72,9 @@ onMounted(async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
function generateBarcode() {
|
function generateBarcode() {
|
||||||
if (!barcodeEl.value || !equipment.value) return
|
if (!barcodeEl.value || !machine.value) return
|
||||||
try {
|
try {
|
||||||
JsBarcode(barcodeEl.value, equipment.value.assetnumber, {
|
JsBarcode(barcodeEl.value, machine.value.assetnumber, {
|
||||||
format: 'CODE39',
|
format: 'CODE39',
|
||||||
displayValue: false,
|
displayValue: false,
|
||||||
width: 2,
|
width: 2,
|
||||||
@@ -127,6 +127,9 @@
|
|||||||
<!-- Warranty -->
|
<!-- Warranty -->
|
||||||
<WarrantyPanel :assetid="printer.assetid" :items="warranties" />
|
<WarrantyPanel :assetid="printer.assetid" :items="warranties" />
|
||||||
|
|
||||||
|
<!-- All relationships (defaultprinter, connectedto, ...) -->
|
||||||
|
<AssetRelationships v-if="printer.assetid" :assetId="printer.assetid" />
|
||||||
|
|
||||||
<!-- Notes -->
|
<!-- Notes -->
|
||||||
<div class="section-card" v-if="printer.notes">
|
<div class="section-card" v-if="printer.notes">
|
||||||
<h3 class="section-title">Notes</h3>
|
<h3 class="section-title">Notes</h3>
|
||||||
@@ -267,6 +270,7 @@ import { printersApi } from '../../api'
|
|||||||
import LocationMapTooltip from '../../components/LocationMapTooltip.vue'
|
import LocationMapTooltip from '../../components/LocationMapTooltip.vue'
|
||||||
import CustomFieldsSection from '../../components/CustomFieldsSection.vue'
|
import CustomFieldsSection from '../../components/CustomFieldsSection.vue'
|
||||||
import WarrantyPanel from '../../components/WarrantyPanel.vue'
|
import WarrantyPanel from '../../components/WarrantyPanel.vue'
|
||||||
|
import AssetRelationships from '../../components/AssetRelationships.vue'
|
||||||
import { useWarrantyBadge } from '../../composables/warrantyBadge'
|
import { useWarrantyBadge } from '../../composables/warrantyBadge'
|
||||||
import { useIdentifierFlags } from '../../composables/identifierSettings'
|
import { useIdentifierFlags } from '../../composables/identifierSettings'
|
||||||
|
|
||||||
|
|||||||
@@ -107,8 +107,8 @@
|
|||||||
|
|
||||||
<div v-if="loading" class="loading">Loading report...</div>
|
<div v-if="loading" class="loading">Loading report...</div>
|
||||||
<div v-else-if="reportData">
|
<div v-else-if="reportData">
|
||||||
<!-- Equipment by Type -->
|
<!-- Machines by Type -->
|
||||||
<div v-if="currentReport.id === 'equipment-by-type'" class="report-content">
|
<div v-if="currentReport.id === 'machines-by-type'" class="report-content">
|
||||||
<p class="report-summary">Total: {{ reportData.total }}</p>
|
<p class="report-summary">Total: {{ reportData.total }}</p>
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
@@ -119,8 +119,8 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="item in reportData.data" :key="item.equipmenttype">
|
<tr v-for="item in reportData.data" :key="item.machinetype">
|
||||||
<td>{{ item.equipmenttype }}</td>
|
<td>{{ item.machinetype }}</td>
|
||||||
<td>{{ item.description }}</td>
|
<td>{{ item.description }}</td>
|
||||||
<td>{{ item.count }}</td>
|
<td>{{ item.count }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -235,7 +235,7 @@ const categoryOrder = ['inventory', 'compliance', 'usage']
|
|||||||
|
|
||||||
// server-side filters each inline report accepts (query params on its endpoint)
|
// server-side filters each inline report accepts (query params on its endpoint)
|
||||||
const reportFilterFields = {
|
const reportFilterFields = {
|
||||||
'equipment-by-type': ['businessunit'],
|
'machines-by-type': ['businessunit'],
|
||||||
'assets-by-status': ['assettype', 'businessunit'],
|
'assets-by-status': ['assettype', 'businessunit'],
|
||||||
'asset-inventory': ['businessunit', 'location'],
|
'asset-inventory': ['businessunit', 'location'],
|
||||||
'kb-popularity': ['limit'],
|
'kb-popularity': ['limit'],
|
||||||
@@ -388,8 +388,8 @@ async function runReport(report) {
|
|||||||
try {
|
try {
|
||||||
let response
|
let response
|
||||||
switch (report.id) {
|
switch (report.id) {
|
||||||
case 'equipment-by-type':
|
case 'machines-by-type':
|
||||||
response = await reportsApi.equipmentByType(params)
|
response = await reportsApi.machinesByType(params)
|
||||||
break
|
break
|
||||||
case 'assets-by-status':
|
case 'assets-by-status':
|
||||||
response = await reportsApi.assetsByStatus(params)
|
response = await reportsApi.assetsByStatus(params)
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ const bucketOrder = [
|
|||||||
function formatDate(d) { return new Date(d + 'T00:00:00').toLocaleDateString() }
|
function formatDate(d) { return new Date(d + 'T00:00:00').toLocaleDateString() }
|
||||||
function cardStyle(color) { return { borderTop: `3px solid ${color}` } }
|
function cardStyle(color) { return { borderTop: `3px solid ${color}` } }
|
||||||
function assetLink(a) {
|
function assetLink(a) {
|
||||||
const map = { computer: '/pcs/', printer: '/printers/', network_device: '/network/', equipment: '/machines/' }
|
const map = { computer: '/pcs/', printer: '/printers/', network_device: '/network/', machine: '/machines/' }
|
||||||
return (map[a.assettypename] || '/assets/') + a.assetid
|
return (map[a.assettypename] || '/assets/') + a.assetid
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ const filterAction = ref('')
|
|||||||
const filterEntity = ref('')
|
const filterEntity = ref('')
|
||||||
const selectedLog = ref(null)
|
const selectedLog = ref(null)
|
||||||
|
|
||||||
const entityTypes = ['Asset', 'Printer', 'Computer', 'Equipment', 'Network', 'Setting', 'User', 'Application', 'KnowledgeBase']
|
const entityTypes = ['Asset', 'Printer', 'Computer', 'Machine', 'Network', 'Setting', 'User', 'Application', 'KnowledgeBase']
|
||||||
|
|
||||||
const totalPages = computed(() => Math.ceil(total.value / perpage))
|
const totalPages = computed(() => Math.ceil(total.value / perpage))
|
||||||
|
|
||||||
@@ -183,7 +183,7 @@ function getEntityLink(log) {
|
|||||||
switch (type) {
|
switch (type) {
|
||||||
case 'printer': return `/printers/${log.entityid}`
|
case 'printer': return `/printers/${log.entityid}`
|
||||||
case 'computer': return `/pcs/${log.entityid}`
|
case 'computer': return `/pcs/${log.entityid}`
|
||||||
case 'equipment': return `/equipment/${log.entityid}`
|
case 'machine': return `/machines/${log.entityid}`
|
||||||
case 'asset': return `/assets/${log.entityid}`
|
case 'asset': return `/assets/${log.entityid}`
|
||||||
case 'application': return `/applications/${log.entityid}`
|
case 'application': return `/applications/${log.entityid}`
|
||||||
case 'knowledgebase': return `/kb/${log.entityid}`
|
case 'knowledgebase': return `/kb/${log.entityid}`
|
||||||
|
|||||||
@@ -1,145 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<div class="page-header">
|
|
||||||
<h2>Equipment Types</h2>
|
|
||||||
<label class="show-inactive"><input type="checkbox" v-model="showInactive" /> Show inactive</label>
|
|
||||||
<button class="btn btn-primary" @click="openModal()">+ Add Equipment Type</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<div v-if="loading" class="loading">Loading...</div>
|
|
||||||
<template v-else>
|
|
||||||
<div class="table-container">
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Equipment Type</th>
|
|
||||||
<th>Description</th>
|
|
||||||
<th>Color</th>
|
|
||||||
<th>Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr v-for="t in visibleItems" :key="t.equipmenttypeid">
|
|
||||||
<td>{{ t.equipmenttype }}</td>
|
|
||||||
<td class="cell-truncate" :title="t.description">{{ t.description || '-' }}</td>
|
|
||||||
<td><span class="badge" :style="colorStyle(t.color)">{{ t.color || 'auto' }}</span></td>
|
|
||||||
<td class="actions">
|
|
||||||
<span v-if="t.isactive === false" class="badge badge-secondary" style="margin-right:6px;">inactive</span>
|
|
||||||
<button class="btn btn-secondary btn-sm" @click="openModal(t)">Edit</button>
|
|
||||||
<button class="btn btn-danger btn-sm" @click="deleteType(t)">Delete</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr v-if="visibleItems.length === 0">
|
|
||||||
<td colspan="4" style="text-align: center; color: var(--text-light);">No equipment types found</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="showModal" class="modal-overlay" @click.self="closeModal">
|
|
||||||
<div class="modal">
|
|
||||||
<div class="modal-header"><h3>{{ editing ? 'Edit' : 'Add' }} Equipment Type</h3></div>
|
|
||||||
<form @submit.prevent="save">
|
|
||||||
<div class="modal-body">
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Equipment Type *</label>
|
|
||||||
<input v-model="form.equipmenttype" type="text" class="form-control" required />
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Description</label>
|
|
||||||
<textarea v-model="form.description" class="form-control" rows="3"></textarea>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Color <span class="hint">(map markers; blank = auto)</span></label>
|
|
||||||
<ColorSwatchPicker v-model="form.color" />
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="checkbox-label"><input type="checkbox" v-model="form.isactive" /> Active</label>
|
|
||||||
</div>
|
|
||||||
<div v-if="error" class="error-message">{{ error }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-secondary" @click="closeModal">Cancel</button>
|
|
||||||
<button type="submit" class="btn btn-primary" :disabled="saving">{{ saving ? 'Saving...' : 'Save' }}</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, computed, onMounted } from 'vue'
|
|
||||||
import { equipmentApi } from '../../api'
|
|
||||||
import ColorSwatchPicker from '@/components/ColorSwatchPicker.vue'
|
|
||||||
import { colorStyle } from '@/utils/colorStyle'
|
|
||||||
import { useToast } from '../../composables/toast'
|
|
||||||
import { apiError } from '../../utils/apiError'
|
|
||||||
const toast = useToast()
|
|
||||||
|
|
||||||
const items = ref([])
|
|
||||||
const showInactive = ref(false)
|
|
||||||
const visibleItems = computed(() => showInactive.value ? items.value : items.value.filter(x => x.isactive !== false))
|
|
||||||
const loading = ref(true)
|
|
||||||
const showModal = ref(false)
|
|
||||||
const editing = ref(null)
|
|
||||||
const saving = ref(false)
|
|
||||||
const error = ref('')
|
|
||||||
const form = ref({ equipmenttype: '', description: '', color: '', isactive: true })
|
|
||||||
|
|
||||||
onMounted(loadData)
|
|
||||||
|
|
||||||
async function loadData() {
|
|
||||||
loading.value = true
|
|
||||||
try {
|
|
||||||
const response = await equipmentApi.types.list({ perpage: 200, active: false })
|
|
||||||
items.value = response.data.data || []
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error loading equipment types:', err)
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function openModal(item = null) {
|
|
||||||
editing.value = item
|
|
||||||
form.value = item
|
|
||||||
? { equipmenttype: item.equipmenttype || '', description: item.description || '', color: item.color || '', isactive: item.isactive !== false }
|
|
||||||
: { equipmenttype: '', description: '', color: '', isactive: true }
|
|
||||||
error.value = ''
|
|
||||||
showModal.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeModal() { showModal.value = false; editing.value = null }
|
|
||||||
|
|
||||||
async function save() {
|
|
||||||
error.value = ''
|
|
||||||
saving.value = true
|
|
||||||
try {
|
|
||||||
if (editing.value) {
|
|
||||||
await equipmentApi.types.update(editing.value.equipmenttypeid, form.value)
|
|
||||||
} else {
|
|
||||||
await equipmentApi.types.create(form.value)
|
|
||||||
}
|
|
||||||
closeModal()
|
|
||||||
loadData()
|
|
||||||
} catch (err) {
|
|
||||||
error.value = apiError(err, 'Failed to save')
|
|
||||||
} finally {
|
|
||||||
saving.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteType(t) {
|
|
||||||
if (!confirm(`Delete equipment type "${t.equipmenttype}"?`)) return
|
|
||||||
try {
|
|
||||||
await equipmentApi.types.remove(t.equipmenttypeid)
|
|
||||||
loadData()
|
|
||||||
} catch (err) {
|
|
||||||
toast.error(apiError(err, 'Failed to delete'))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -2,188 +2,101 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h2>Machine Types</h2>
|
<h2>Machine Types</h2>
|
||||||
<button class="btn btn-primary" @click="openModal()">+ Add Type</button>
|
<label class="show-inactive"><input type="checkbox" v-model="showInactive" /> Show inactive</label>
|
||||||
|
<button class="btn btn-primary" @click="openModal()">+ Add Machine Type</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div v-if="loading" class="loading">Loading...</div>
|
<div v-if="loading" class="loading">Loading...</div>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="table-container">
|
<div class="table-container">
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Machine Type</th>
|
<th>Machine Type</th>
|
||||||
<th>Category</th>
|
|
||||||
<th>Description</th>
|
<th>Description</th>
|
||||||
|
<th>Color</th>
|
||||||
<th>Actions</th>
|
<th>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="mt in machineTypes" :key="mt.machinetypeid">
|
<tr v-for="t in visibleItems" :key="t.machinetypeid">
|
||||||
<td>{{ mt.machinetype }}</td>
|
<td>{{ t.machinetype }}</td>
|
||||||
<td>
|
<td class="cell-truncate" :title="t.description">{{ t.description || '-' }}</td>
|
||||||
<span class="badge" :class="getCategoryClass(mt.category)">
|
<td><span class="badge" :style="colorStyle(t.color)">{{ t.color || 'auto' }}</span></td>
|
||||||
{{ mt.category }}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td class="cell-truncate" :title="mt.description">{{ mt.description || '-' }}</td>
|
|
||||||
<td class="actions">
|
<td class="actions">
|
||||||
<button
|
<span v-if="t.isactive === false" class="badge badge-secondary" style="margin-right:6px;">inactive</span>
|
||||||
class="btn btn-secondary btn-sm"
|
<button class="btn btn-secondary btn-sm" @click="openModal(t)">Edit</button>
|
||||||
@click="openModal(mt)"
|
<button class="btn btn-danger btn-sm" @click="deleteType(t)">Delete</button>
|
||||||
>
|
|
||||||
Edit
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="btn btn-danger btn-sm"
|
|
||||||
@click="confirmDelete(mt)"
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</button>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-if="machineTypes.length === 0">
|
<tr v-if="visibleItems.length === 0">
|
||||||
<td colspan="4" style="text-align: center; color: var(--text-light);">
|
<td colspan="4" style="text-align: center; color: var(--text-light);">No machine types found</td>
|
||||||
No machine types found
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Pagination -->
|
|
||||||
<PaginationBar
|
|
||||||
:page="page"
|
|
||||||
:totalPages="totalPages"
|
|
||||||
:perPage="perPage"
|
|
||||||
@update:page="goToPage"
|
|
||||||
@update:perPage="changePerPage"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Add/Edit Modal -->
|
|
||||||
<div v-if="showModal" class="modal-overlay" @click.self="closeModal">
|
<div v-if="showModal" class="modal-overlay" @click.self="closeModal">
|
||||||
<div class="modal">
|
<div class="modal">
|
||||||
<div class="modal-header">
|
<div class="modal-header"><h3>{{ editing ? 'Edit' : 'Add' }} Machine Type</h3></div>
|
||||||
<h3>{{ editingType ? 'Edit Machine Type' : 'Add Machine Type' }}</h3>
|
<form @submit.prevent="save">
|
||||||
</div>
|
|
||||||
<form @submit.prevent="saveType">
|
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="machinetype">Type Name *</label>
|
<label>Machine Type *</label>
|
||||||
<input
|
<input v-model="form.machinetype" type="text" class="form-control" required />
|
||||||
id="machinetype"
|
|
||||||
v-model="form.machinetype"
|
|
||||||
type="text"
|
|
||||||
class="form-control"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="category">Category *</label>
|
<label>Description</label>
|
||||||
<select
|
<textarea v-model="form.description" class="form-control" rows="3"></textarea>
|
||||||
id="category"
|
|
||||||
v-model="form.category"
|
|
||||||
class="form-control"
|
|
||||||
required
|
|
||||||
>
|
|
||||||
<option value="">Select category...</option>
|
|
||||||
<option value="Equipment">Equipment</option>
|
|
||||||
<option value="PC">PC</option>
|
|
||||||
<option value="Network">Network</option>
|
|
||||||
<option value="Printer">Printer</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="description">Description</label>
|
<label>Color <span class="hint">(map markers; blank = auto)</span></label>
|
||||||
<textarea
|
<ColorSwatchPicker v-model="form.color" />
|
||||||
id="description"
|
</div>
|
||||||
v-model="form.description"
|
<div class="form-group">
|
||||||
class="form-control"
|
<label class="checkbox-label"><input type="checkbox" v-model="form.isactive" /> Active</label>
|
||||||
rows="3"
|
|
||||||
></textarea>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="error" class="error-message">{{ error }}</div>
|
<div v-if="error" class="error-message">{{ error }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-secondary" @click="closeModal">Cancel</button>
|
<button type="button" class="btn btn-secondary" @click="closeModal">Cancel</button>
|
||||||
<button type="submit" class="btn btn-primary" :disabled="saving">
|
<button type="submit" class="btn btn-primary" :disabled="saving">{{ saving ? 'Saving...' : 'Save' }}</button>
|
||||||
{{ saving ? 'Saving...' : 'Save' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Delete Confirmation Modal -->
|
|
||||||
<div v-if="showDeleteModal" class="modal-overlay" @click.self="showDeleteModal = false">
|
|
||||||
<div class="modal">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h3>Delete Machine Type</h3>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<p>Are you sure you want to delete <strong>{{ typeToDelete?.machinetype }}</strong>?</p>
|
|
||||||
<p style="color: var(--text-light); font-size: 0.875rem;">
|
|
||||||
This may affect machines using this type.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button class="btn btn-secondary" @click="showDeleteModal = false">Cancel</button>
|
|
||||||
<button class="btn btn-danger" @click="deleteType">Delete</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { machinetypesApi } from '../../api'
|
import { machinesApi } from '../../api'
|
||||||
import PaginationBar from '../../components/PaginationBar.vue'
|
import ColorSwatchPicker from '@/components/ColorSwatchPicker.vue'
|
||||||
|
import { colorStyle } from '@/utils/colorStyle'
|
||||||
import { useToast } from '../../composables/toast'
|
import { useToast } from '../../composables/toast'
|
||||||
import { apiError } from '../../utils/apiError'
|
import { apiError } from '../../utils/apiError'
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
const machineTypes = ref([])
|
const items = ref([])
|
||||||
|
const showInactive = ref(false)
|
||||||
|
const visibleItems = computed(() => showInactive.value ? items.value : items.value.filter(x => x.isactive !== false))
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const page = ref(1)
|
|
||||||
const totalPages = ref(1)
|
|
||||||
const perPage = ref(20)
|
|
||||||
|
|
||||||
const showModal = ref(false)
|
const showModal = ref(false)
|
||||||
const editingType = ref(null)
|
const editing = ref(null)
|
||||||
const saving = ref(false)
|
const saving = ref(false)
|
||||||
const error = ref('')
|
const error = ref('')
|
||||||
|
const form = ref({ machinetype: '', description: '', color: '', isactive: true })
|
||||||
|
|
||||||
const showDeleteModal = ref(false)
|
onMounted(loadData)
|
||||||
const typeToDelete = ref(null)
|
|
||||||
|
|
||||||
const form = ref({
|
async function loadData() {
|
||||||
machinetype: '',
|
|
||||||
category: '',
|
|
||||||
description: ''
|
|
||||||
})
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
loadTypes()
|
|
||||||
})
|
|
||||||
|
|
||||||
async function loadTypes() {
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const params = {
|
const response = await machinesApi.types.list({ perpage: 200, active: false })
|
||||||
page: page.value,
|
items.value = response.data.data || []
|
||||||
perpage: perPage.value
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await machinetypesApi.list(params)
|
|
||||||
machineTypes.value = response.data.data || []
|
|
||||||
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 {
|
||||||
@@ -191,87 +104,42 @@ async function loadTypes() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function goToPage(p) {
|
function openModal(item = null) {
|
||||||
page.value = p
|
editing.value = item
|
||||||
loadTypes()
|
form.value = item
|
||||||
}
|
? { machinetype: item.machinetype || '', description: item.description || '', color: item.color || '', isactive: item.isactive !== false }
|
||||||
|
: { machinetype: '', description: '', color: '', isactive: true }
|
||||||
function changePerPage(newPerPage) {
|
|
||||||
perPage.value = newPerPage
|
|
||||||
page.value = 1
|
|
||||||
loadTypes()
|
|
||||||
}
|
|
||||||
|
|
||||||
function openModal(mt = null) {
|
|
||||||
editingType.value = mt
|
|
||||||
if (mt) {
|
|
||||||
form.value = {
|
|
||||||
machinetype: mt.machinetype || '',
|
|
||||||
category: mt.category || '',
|
|
||||||
description: mt.description || ''
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
form.value = {
|
|
||||||
machinetype: '',
|
|
||||||
category: '',
|
|
||||||
description: ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
error.value = ''
|
error.value = ''
|
||||||
showModal.value = true
|
showModal.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeModal() {
|
function closeModal() { showModal.value = false; editing.value = null }
|
||||||
showModal.value = false
|
|
||||||
editingType.value = null
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveType() {
|
async function save() {
|
||||||
error.value = ''
|
error.value = ''
|
||||||
saving.value = true
|
saving.value = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (editingType.value) {
|
if (editing.value) {
|
||||||
await machinetypesApi.update(editingType.value.machinetypeid, form.value)
|
await machinesApi.types.update(editing.value.machinetypeid, form.value)
|
||||||
} else {
|
} else {
|
||||||
await machinetypesApi.create(form.value)
|
await machinesApi.types.create(form.value)
|
||||||
}
|
}
|
||||||
closeModal()
|
closeModal()
|
||||||
loadTypes()
|
loadData()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error saving machine type:', err)
|
error.value = apiError(err, 'Failed to save')
|
||||||
error.value = apiError(err, 'Failed to save machine type')
|
|
||||||
} finally {
|
} finally {
|
||||||
saving.value = false
|
saving.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function confirmDelete(mt) {
|
async function deleteType(t) {
|
||||||
typeToDelete.value = mt
|
if (!confirm(`Delete machine type "${t.machinetype}"?`)) return
|
||||||
showDeleteModal.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteType() {
|
|
||||||
try {
|
try {
|
||||||
await machinetypesApi.delete(typeToDelete.value.machinetypeid)
|
await machinesApi.types.remove(t.machinetypeid)
|
||||||
showDeleteModal.value = false
|
loadData()
|
||||||
typeToDelete.value = null
|
|
||||||
loadTypes()
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error deleting machine type:', err)
|
toast.error(apiError(err, 'Failed to delete'))
|
||||||
toast.error('Failed to delete machine type')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCategoryClass(category) {
|
|
||||||
if (!category) return 'badge-info'
|
|
||||||
const c = category.toLowerCase()
|
|
||||||
if (c === 'equipment') return 'badge-info'
|
|
||||||
if (c === 'pc') return 'badge-success'
|
|
||||||
if (c === 'network') return 'badge-warning'
|
|
||||||
if (c === 'printer') return 'badge-primary'
|
|
||||||
return 'badge-info'
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Uses global styles from style.css -->
|
|
||||||
|
|||||||
277
frontend/src/views/settings/ModelTypesList.vue
Normal file
277
frontend/src/views/settings/ModelTypesList.vue
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="page-header">
|
||||||
|
<h2>Model Types</h2>
|
||||||
|
<button class="btn btn-primary" @click="openModal()">+ Add Type</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div v-if="loading" class="loading">Loading...</div>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<div class="table-container">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Model Type</th>
|
||||||
|
<th>Category</th>
|
||||||
|
<th>Description</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="mt in modelTypes" :key="mt.modeltypeid">
|
||||||
|
<td>{{ mt.modeltype }}</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge" :class="getCategoryClass(mt.category)">
|
||||||
|
{{ mt.category }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="cell-truncate" :title="mt.description">{{ mt.description || '-' }}</td>
|
||||||
|
<td class="actions">
|
||||||
|
<button
|
||||||
|
class="btn btn-secondary btn-sm"
|
||||||
|
@click="openModal(mt)"
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-danger btn-sm"
|
||||||
|
@click="confirmDelete(mt)"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="modelTypes.length === 0">
|
||||||
|
<td colspan="4" style="text-align: center; color: var(--text-light);">
|
||||||
|
No model types found
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pagination -->
|
||||||
|
<PaginationBar
|
||||||
|
:page="page"
|
||||||
|
:totalPages="totalPages"
|
||||||
|
:perPage="perPage"
|
||||||
|
@update:page="goToPage"
|
||||||
|
@update:perPage="changePerPage"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Add/Edit Modal -->
|
||||||
|
<div v-if="showModal" class="modal-overlay" @click.self="closeModal">
|
||||||
|
<div class="modal">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3>{{ editingType ? 'Edit Model Type' : 'Add Model Type' }}</h3>
|
||||||
|
</div>
|
||||||
|
<form @submit.prevent="saveType">
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="modeltype">Type Name *</label>
|
||||||
|
<input
|
||||||
|
id="modeltype"
|
||||||
|
v-model="form.modeltype"
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="category">Category *</label>
|
||||||
|
<select
|
||||||
|
id="category"
|
||||||
|
v-model="form.category"
|
||||||
|
class="form-control"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option value="">Select category...</option>
|
||||||
|
<option value="Equipment">Equipment</option>
|
||||||
|
<option value="PC">PC</option>
|
||||||
|
<option value="Network">Network</option>
|
||||||
|
<option value="Printer">Printer</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="description">Description</label>
|
||||||
|
<textarea
|
||||||
|
id="description"
|
||||||
|
v-model="form.description"
|
||||||
|
class="form-control"
|
||||||
|
rows="3"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="error" class="error-message">{{ error }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" @click="closeModal">Cancel</button>
|
||||||
|
<button type="submit" class="btn btn-primary" :disabled="saving">
|
||||||
|
{{ saving ? 'Saving...' : 'Save' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Delete Confirmation Modal -->
|
||||||
|
<div v-if="showDeleteModal" class="modal-overlay" @click.self="showDeleteModal = false">
|
||||||
|
<div class="modal">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3>Delete Model Type</h3>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>Are you sure you want to delete <strong>{{ typeToDelete?.modeltype }}</strong>?</p>
|
||||||
|
<p style="color: var(--text-light); font-size: 0.875rem;">
|
||||||
|
This may affect models using this type.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-secondary" @click="showDeleteModal = false">Cancel</button>
|
||||||
|
<button class="btn btn-danger" @click="deleteType">Delete</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { modeltypesApi } from '../../api'
|
||||||
|
import PaginationBar from '../../components/PaginationBar.vue'
|
||||||
|
import { useToast } from '../../composables/toast'
|
||||||
|
import { apiError } from '../../utils/apiError'
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
|
const modelTypes = ref([])
|
||||||
|
const loading = ref(true)
|
||||||
|
const page = ref(1)
|
||||||
|
const totalPages = ref(1)
|
||||||
|
const perPage = ref(20)
|
||||||
|
|
||||||
|
const showModal = ref(false)
|
||||||
|
const editingType = ref(null)
|
||||||
|
const saving = ref(false)
|
||||||
|
const error = ref('')
|
||||||
|
|
||||||
|
const showDeleteModal = ref(false)
|
||||||
|
const typeToDelete = ref(null)
|
||||||
|
|
||||||
|
const form = ref({
|
||||||
|
modeltype: '',
|
||||||
|
category: '',
|
||||||
|
description: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadTypes()
|
||||||
|
})
|
||||||
|
|
||||||
|
async function loadTypes() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
page: page.value,
|
||||||
|
perpage: perPage.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await modeltypesApi.list(params)
|
||||||
|
modelTypes.value = response.data.data || []
|
||||||
|
totalPages.value = response.data.meta?.pagination?.totalpages || response.data.meta?.pagination?.total_pages || 1
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error loading model types:', err)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function goToPage(p) {
|
||||||
|
page.value = p
|
||||||
|
loadTypes()
|
||||||
|
}
|
||||||
|
|
||||||
|
function changePerPage(newPerPage) {
|
||||||
|
perPage.value = newPerPage
|
||||||
|
page.value = 1
|
||||||
|
loadTypes()
|
||||||
|
}
|
||||||
|
|
||||||
|
function openModal(mt = null) {
|
||||||
|
editingType.value = mt
|
||||||
|
if (mt) {
|
||||||
|
form.value = {
|
||||||
|
modeltype: mt.modeltype || '',
|
||||||
|
category: mt.category || '',
|
||||||
|
description: mt.description || ''
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
form.value = {
|
||||||
|
modeltype: '',
|
||||||
|
category: '',
|
||||||
|
description: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
error.value = ''
|
||||||
|
showModal.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeModal() {
|
||||||
|
showModal.value = false
|
||||||
|
editingType.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveType() {
|
||||||
|
error.value = ''
|
||||||
|
saving.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (editingType.value) {
|
||||||
|
await modeltypesApi.update(editingType.value.modeltypeid, form.value)
|
||||||
|
} else {
|
||||||
|
await modeltypesApi.create(form.value)
|
||||||
|
}
|
||||||
|
closeModal()
|
||||||
|
loadTypes()
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error saving model type:', err)
|
||||||
|
error.value = apiError(err, 'Failed to save model type')
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmDelete(mt) {
|
||||||
|
typeToDelete.value = mt
|
||||||
|
showDeleteModal.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteType() {
|
||||||
|
try {
|
||||||
|
await modeltypesApi.delete(typeToDelete.value.modeltypeid)
|
||||||
|
showDeleteModal.value = false
|
||||||
|
typeToDelete.value = null
|
||||||
|
loadTypes()
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error deleting model type:', err)
|
||||||
|
toast.error('Failed to delete model type')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCategoryClass(category) {
|
||||||
|
if (!category) return 'badge-info'
|
||||||
|
const c = category.toLowerCase()
|
||||||
|
if (c === 'equipment') return 'badge-info'
|
||||||
|
if (c === 'pc') return 'badge-success'
|
||||||
|
if (c === 'network') return 'badge-warning'
|
||||||
|
if (c === 'printer') return 'badge-primary'
|
||||||
|
return 'badge-info'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Uses global styles from style.css -->
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
<small v-if="m.description" class="text-muted">{{ m.description }}</small>
|
<small v-if="m.description" class="text-muted">{{ m.description }}</small>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ m.vendor || '-' }}</td>
|
<td>{{ m.vendor || '-' }}</td>
|
||||||
<td>{{ m.machinetype || '-' }}</td>
|
<td>{{ m.modeltype || '-' }}</td>
|
||||||
<td>
|
<td>
|
||||||
<a v-if="m.documentationurl" :href="m.documentationurl" target="_blank" class="btn btn-sm btn-link">
|
<a v-if="m.documentationurl" :href="m.documentationurl" target="_blank" class="btn btn-sm btn-link">
|
||||||
View Docs
|
View Docs
|
||||||
@@ -108,11 +108,11 @@
|
|||||||
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="machinetypeid">Machine Type</label>
|
<label for="modeltypeid">Model Type</label>
|
||||||
<select id="machinetypeid" v-model="form.machinetypeid" class="form-control">
|
<select id="modeltypeid" v-model="form.modeltypeid" class="form-control">
|
||||||
<option value="">Select type...</option>
|
<option value="">Select type...</option>
|
||||||
<option v-for="mt in machineTypes" :key="mt.machinetypeid" :value="mt.machinetypeid">
|
<option v-for="mt in modelTypes" :key="mt.modeltypeid" :value="mt.modeltypeid">
|
||||||
{{ mt.machinetype }}
|
{{ mt.modeltype }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -181,7 +181,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { modelsApi, vendorsApi, machinetypesApi } from '../../api'
|
import { modelsApi, vendorsApi, modeltypesApi } from '../../api'
|
||||||
import PaginationBar from '../../components/PaginationBar.vue'
|
import PaginationBar from '../../components/PaginationBar.vue'
|
||||||
import { useToast } from '../../composables/toast'
|
import { useToast } from '../../composables/toast'
|
||||||
import { apiError } from '../../utils/apiError'
|
import { apiError } from '../../utils/apiError'
|
||||||
@@ -189,7 +189,7 @@ const toast = useToast()
|
|||||||
|
|
||||||
const models = ref([])
|
const models = ref([])
|
||||||
const vendors = ref([])
|
const vendors = ref([])
|
||||||
const machineTypes = ref([])
|
const modelTypes = ref([])
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const search = ref('')
|
const search = ref('')
|
||||||
const vendorFilter = ref('')
|
const vendorFilter = ref('')
|
||||||
@@ -208,7 +208,7 @@ const modelToDelete = ref(null)
|
|||||||
const form = ref({
|
const form = ref({
|
||||||
modelnumber: '',
|
modelnumber: '',
|
||||||
vendorid: '',
|
vendorid: '',
|
||||||
machinetypeid: '',
|
modeltypeid: '',
|
||||||
description: '',
|
description: '',
|
||||||
documentationurl: '',
|
documentationurl: '',
|
||||||
imageurl: '',
|
imageurl: '',
|
||||||
@@ -221,7 +221,7 @@ onMounted(async () => {
|
|||||||
await Promise.all([
|
await Promise.all([
|
||||||
loadModels(),
|
loadModels(),
|
||||||
loadVendors(),
|
loadVendors(),
|
||||||
loadMachineTypes()
|
loadModelTypes()
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -251,12 +251,12 @@ async function loadVendors() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadMachineTypes() {
|
async function loadModelTypes() {
|
||||||
try {
|
try {
|
||||||
const response = await machinetypesApi.list({ perpage: 100 })
|
const response = await modeltypesApi.list({ perpage: 100 })
|
||||||
machineTypes.value = response.data.data || []
|
modelTypes.value = response.data.data || []
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error loading machine types:', err)
|
console.error('Error loading model types:', err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,7 +285,7 @@ function openModal(m = null) {
|
|||||||
form.value = {
|
form.value = {
|
||||||
modelnumber: m.modelnumber || '',
|
modelnumber: m.modelnumber || '',
|
||||||
vendorid: m.vendorid || '',
|
vendorid: m.vendorid || '',
|
||||||
machinetypeid: m.machinetypeid || '',
|
modeltypeid: m.modeltypeid || '',
|
||||||
description: m.description || '',
|
description: m.description || '',
|
||||||
documentationurl: m.documentationurl || '',
|
documentationurl: m.documentationurl || '',
|
||||||
imageurl: m.imageurl || '',
|
imageurl: m.imageurl || '',
|
||||||
@@ -295,7 +295,7 @@ function openModal(m = null) {
|
|||||||
form.value = {
|
form.value = {
|
||||||
modelnumber: '',
|
modelnumber: '',
|
||||||
vendorid: '',
|
vendorid: '',
|
||||||
machinetypeid: '',
|
modeltypeid: '',
|
||||||
description: '',
|
description: '',
|
||||||
documentationurl: '',
|
documentationurl: '',
|
||||||
imageurl: '',
|
imageurl: '',
|
||||||
@@ -318,7 +318,7 @@ async function saveModel() {
|
|||||||
try {
|
try {
|
||||||
const data = { ...form.value }
|
const data = { ...form.value }
|
||||||
if (!data.vendorid) data.vendorid = null
|
if (!data.vendorid) data.vendorid = null
|
||||||
if (!data.machinetypeid) data.machinetypeid = null
|
if (!data.modeltypeid) data.modeltypeid = null
|
||||||
|
|
||||||
if (editingModel.value) {
|
if (editingModel.value) {
|
||||||
await modelsApi.update(editingModel.value.modelnumberid, data)
|
await modelsApi.update(editingModel.value.modelnumberid, data)
|
||||||
|
|||||||
@@ -1037,9 +1037,9 @@ const brandingLogos = [
|
|||||||
{ kind: 'qr', key: 'qr_logo', label: 'QR overlay logo', accept: 'image/*',
|
{ kind: 'qr', key: 'qr_logo', label: 'QR overlay logo', accept: 'image/*',
|
||||||
placeholder: '/ge-monogram.svg',
|
placeholder: '/ge-monogram.svg',
|
||||||
hint: 'Logo overlaid on printed QR codes. Leave blank for no overlay.' },
|
hint: 'Logo overlaid on printed QR codes. Leave blank for no overlay.' },
|
||||||
{ kind: 'badge', key: 'badge_logo', label: 'Equipment badge logo', accept: 'image/*',
|
{ kind: 'badge', key: 'badge_logo', label: 'Machine badge logo', accept: 'image/*',
|
||||||
placeholder: '/ge-aerospace-logo.svg',
|
placeholder: '/ge-aerospace-logo.svg',
|
||||||
hint: 'Logo printed on equipment badges. Upload an image or type a path/URL.' },
|
hint: 'Logo printed on machine badges. Upload an image or type a path/URL.' },
|
||||||
{ kind: 'favicon', key: 'site_favicon', label: 'Favicon', accept: 'image/*,.ico',
|
{ kind: 'favicon', key: 'site_favicon', label: 'Favicon', accept: 'image/*,.ico',
|
||||||
placeholder: '(blank = shipped /favicon.svg)',
|
placeholder: '(blank = shipped /favicon.svg)',
|
||||||
hint: 'Browser-tab icon. Leave blank to use the shipped favicon.' },
|
hint: 'Browser-tab icon. Leave blank to use the shipped favicon.' },
|
||||||
@@ -1053,7 +1053,7 @@ const identifierRows = [
|
|||||||
{ name: 'fqdn', label: 'FQDN / Hostname' }
|
{ name: 'fqdn', label: 'FQDN / Hostname' }
|
||||||
]
|
]
|
||||||
const assetTypeCols = [
|
const assetTypeCols = [
|
||||||
{ key: 'equipment', label: 'Equipment' },
|
{ key: 'machine', label: 'Machine' },
|
||||||
{ key: 'computer', label: 'PC' },
|
{ key: 'computer', label: 'PC' },
|
||||||
{ key: 'printer', label: 'Printer' },
|
{ key: 'printer', label: 'Printer' },
|
||||||
{ key: 'network_device', label: 'Network' }
|
{ key: 'network_device', label: 'Network' }
|
||||||
@@ -1075,7 +1075,7 @@ const searchDomains = [
|
|||||||
{ key: 'application', label: 'Applications' },
|
{ key: 'application', label: 'Applications' },
|
||||||
{ key: 'knowledgebase', label: 'Knowledge Base' },
|
{ key: 'knowledgebase', label: 'Knowledge Base' },
|
||||||
{ key: 'employee', label: 'Employees' },
|
{ key: 'employee', label: 'Employees' },
|
||||||
{ key: 'equipment', label: 'Equipment' },
|
{ key: 'machine', label: 'Machines' },
|
||||||
{ key: 'computer', label: 'PCs' },
|
{ key: 'computer', label: 'PCs' },
|
||||||
{ key: 'printer', label: 'Printers' },
|
{ key: 'printer', label: 'Printers' },
|
||||||
{ key: 'network_device', label: 'Network Devices' },
|
{ key: 'network_device', label: 'Network Devices' },
|
||||||
@@ -1170,7 +1170,7 @@ async function loadSettings() {
|
|||||||
for (const setting of data.data) {
|
for (const setting of data.data) {
|
||||||
if (setting.key in settings) {
|
if (setting.key in settings) {
|
||||||
settings[setting.key] = setting.value
|
settings[setting.key] = setting.value
|
||||||
} else if (/^identifier_.+_(equipment|computer|printer|network_device)_enabled$/.test(setting.key)) {
|
} else if (/^identifier_.+_(machine|computer|printer|network_device)_enabled$/.test(setting.key)) {
|
||||||
identifierMatrix[setting.key] = setting.value !== false
|
identifierMatrix[setting.key] = setting.value !== false
|
||||||
} else if (/^search_.+_enabled$/.test(setting.key)) {
|
} else if (/^search_.+_enabled$/.test(setting.key)) {
|
||||||
searchMatrix[setting.key] = setting.value !== false
|
searchMatrix[setting.key] = setting.value !== false
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export const settingsGroups = [
|
|||||||
cards: [
|
cards: [
|
||||||
{ to: '/settings/vendors', icon: Factory, title: 'Vendors', description: 'Manage equipment vendors and manufacturers' },
|
{ to: '/settings/vendors', icon: Factory, title: 'Vendors', description: 'Manage equipment vendors and manufacturers' },
|
||||||
{ to: '/settings/models', icon: Package, title: 'Models', description: 'Manage equipment models by vendor' },
|
{ to: '/settings/models', icon: Package, title: 'Models', description: 'Manage equipment models by vendor' },
|
||||||
|
{ to: '/settings/modeltypes', icon: Monitor, title: 'Model Types', description: 'Manage model type categories' },
|
||||||
{ to: '/settings/statuses', icon: Tag, title: 'Statuses', description: 'Manage asset status types' },
|
{ to: '/settings/statuses', icon: Tag, title: 'Statuses', description: 'Manage asset status types' },
|
||||||
{ to: '/settings/relationshiptypes', icon: Link, title: 'Relationship Types', description: 'Manage asset relationship types (Controls, Contains...) + colors' },
|
{ to: '/settings/relationshiptypes', icon: Link, title: 'Relationship Types', description: 'Manage asset relationship types (Controls, Contains...) + colors' },
|
||||||
{ to: '/settings/assettypes', icon: Palette, title: 'Asset Type Colors', description: 'Map colors for the top-level asset categories' },
|
{ to: '/settings/assettypes', icon: Palette, title: 'Asset Type Colors', description: 'Map colors for the top-level asset categories' },
|
||||||
@@ -42,8 +43,7 @@ export const settingsGroups = [
|
|||||||
{
|
{
|
||||||
title: 'Machines',
|
title: 'Machines',
|
||||||
cards: [
|
cards: [
|
||||||
{ to: '/settings/equipmenttypes', icon: Wrench, title: 'Equipment Types', description: 'Manage machine subtypes + map colors' },
|
{ to: '/settings/machinetypes', icon: Wrench, title: 'Machine Types', description: 'Manage machine subtypes + map colors' },
|
||||||
{ to: '/settings/machinetypes', icon: Monitor, title: 'Machine Types', description: 'Manage machine type categories' },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -208,7 +208,7 @@ function formatDate(d) { return new Date(d + 'T00:00:00').toLocaleDateString() }
|
|||||||
|
|
||||||
// Route to the right detail page by asset type.
|
// Route to the right detail page by asset type.
|
||||||
function assetLink(a) {
|
function assetLink(a) {
|
||||||
const map = { computer: '/pcs/', printer: '/printers/', network_device: '/network/', equipment: '/machines/' }
|
const map = { computer: '/pcs/', printer: '/printers/', network_device: '/network/', machine: '/machines/' }
|
||||||
const base = map[a.assettypename] || '/assets/'
|
const base = map[a.assettypename] || '/assets/'
|
||||||
return base + a.assetid
|
return base + a.assetid
|
||||||
}
|
}
|
||||||
|
|||||||
205
migrations/versions/7d17_machines_rename.py
Normal file
205
migrations/versions/7d17_machines_rename.py
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
"""Equipment -> machines rename, core half
|
||||||
|
|
||||||
|
Part 1 of the approved equipment -> machines rename (the plugin half lives in
|
||||||
|
plugins/machines/migrations/versions/0002_rename_from_equipment.py):
|
||||||
|
|
||||||
|
1. machinetypes -> modeltypes (+ machinetypeid -> modeltypeid,
|
||||||
|
machinetype -> modeltype, models.machinetypeid -> models.modeltypeid).
|
||||||
|
The table types the vendor MODELS catalog, not machine instances, so
|
||||||
|
the new name is role-accurate and frees "machinetypes" for the plugin.
|
||||||
|
2. Data flips: assettypes 'equipment' -> 'machine' (incl pluginname and
|
||||||
|
tablename), auditlogs.entitytype 'Equipment' -> 'Machine', settings keys
|
||||||
|
identifier_*_equipment_enabled -> identifier_*_machine_enabled,
|
||||||
|
search_equipment_enabled -> search_machine_enabled, and permission
|
||||||
|
rows equipment.* -> machines.* (role links survive).
|
||||||
|
3. alembic_version_equipment -> alembic_version_machines (if present) so
|
||||||
|
the renamed plugin's migration chain resumes seamlessly.
|
||||||
|
|
||||||
|
Idempotent via inspector guards, following the 7c01 pattern. MySQL is the
|
||||||
|
only dialect the core chain runs against in practice.
|
||||||
|
|
||||||
|
Revision ID: 7d17_machines_rename
|
||||||
|
Revises: 7d16_directoryemployees
|
||||||
|
Create Date: 2026-07-11
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
revision = '7d17_machines_rename'
|
||||||
|
down_revision = '7d16_directoryemployees'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def _fk_names(insp, table, referred_table):
|
||||||
|
# find FK constraint names on table pointing at referred_table
|
||||||
|
return [fk['name'] for fk in insp.get_foreign_keys(table)
|
||||||
|
if fk.get('referred_table') == referred_table and fk.get('name')]
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
bind = op.get_bind()
|
||||||
|
insp = sa.inspect(bind)
|
||||||
|
tables = set(insp.get_table_names())
|
||||||
|
|
||||||
|
# -- 1. machinetypes -> modeltypes ------------------------------------
|
||||||
|
if 'machinetypes' in tables and 'modeltypes' not in tables:
|
||||||
|
bind.exec_driver_sql("SET FOREIGN_KEY_CHECKS=0")
|
||||||
|
|
||||||
|
# drop the models -> machinetypes FK and its index before renaming
|
||||||
|
for name in _fk_names(insp, 'models', 'machinetypes'):
|
||||||
|
bind.exec_driver_sql(f"ALTER TABLE models DROP FOREIGN KEY {name}")
|
||||||
|
model_indexes = {ix['name'] for ix in insp.get_indexes('models')}
|
||||||
|
if 'machinetypeid' in model_indexes:
|
||||||
|
bind.exec_driver_sql("ALTER TABLE models DROP KEY machinetypeid")
|
||||||
|
|
||||||
|
bind.exec_driver_sql("RENAME TABLE machinetypes TO modeltypes")
|
||||||
|
bind.exec_driver_sql(
|
||||||
|
"ALTER TABLE modeltypes "
|
||||||
|
"CHANGE machinetypeid modeltypeid INT NOT NULL AUTO_INCREMENT, "
|
||||||
|
"CHANGE machinetype modeltype VARCHAR(100) NOT NULL, "
|
||||||
|
"DROP KEY machinetype, "
|
||||||
|
"ADD UNIQUE KEY modeltype (modeltype)"
|
||||||
|
)
|
||||||
|
bind.exec_driver_sql(
|
||||||
|
"ALTER TABLE models "
|
||||||
|
"CHANGE machinetypeid modeltypeid INT NULL, "
|
||||||
|
"ADD KEY modeltypeid (modeltypeid), "
|
||||||
|
"ADD CONSTRAINT fk_models_modeltypeid "
|
||||||
|
"FOREIGN KEY (modeltypeid) REFERENCES modeltypes (modeltypeid)"
|
||||||
|
)
|
||||||
|
bind.exec_driver_sql("SET FOREIGN_KEY_CHECKS=1")
|
||||||
|
|
||||||
|
# -- 2. data flips ------------------------------------------------------
|
||||||
|
if 'assettypes' in tables:
|
||||||
|
bind.exec_driver_sql(
|
||||||
|
"UPDATE assettypes SET assettype='machine', pluginname='machines', "
|
||||||
|
"tablename='machines' WHERE assettype='equipment'"
|
||||||
|
)
|
||||||
|
if 'auditlogs' in tables:
|
||||||
|
bind.exec_driver_sql(
|
||||||
|
"UPDATE auditlogs SET entitytype='Machine' "
|
||||||
|
"WHERE entitytype='Equipment'"
|
||||||
|
)
|
||||||
|
if 'settings' in tables:
|
||||||
|
_rename_setting_keys(bind, old_marker='_equipment_',
|
||||||
|
new_marker='_machine_')
|
||||||
|
if 'permissions' in tables:
|
||||||
|
_rename_permissions(bind, old_prefix='equipment.',
|
||||||
|
new_prefix='machines.',
|
||||||
|
old_category='equipment', new_category='machines')
|
||||||
|
|
||||||
|
# -- 3. plugin version table --------------------------------------------
|
||||||
|
if ('alembic_version_equipment' in tables
|
||||||
|
and 'alembic_version_machines' not in tables):
|
||||||
|
bind.exec_driver_sql(
|
||||||
|
"RENAME TABLE alembic_version_equipment TO alembic_version_machines"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _rename_setting_keys(bind, old_marker, new_marker):
|
||||||
|
"""Rename settings keys embedding the assettype value, collision-safe.
|
||||||
|
|
||||||
|
Covers identifier_<name>_equipment_enabled and search_equipment_enabled.
|
||||||
|
If a row with the new key already exists (fresh seed ran on new code),
|
||||||
|
the old row is dropped instead of renamed.
|
||||||
|
"""
|
||||||
|
# broad fetch; the marker replace below filters non-matches
|
||||||
|
rows = bind.exec_driver_sql(
|
||||||
|
"SELECT settingid, `key` FROM settings"
|
||||||
|
).fetchall()
|
||||||
|
for settingid, key in rows:
|
||||||
|
newkey = key.replace(old_marker, new_marker)
|
||||||
|
if newkey == key:
|
||||||
|
continue
|
||||||
|
exists = bind.exec_driver_sql(
|
||||||
|
"SELECT 1 FROM settings WHERE `key` = %s", (newkey,)
|
||||||
|
).fetchone()
|
||||||
|
if exists:
|
||||||
|
bind.exec_driver_sql(
|
||||||
|
"DELETE FROM settings WHERE settingid = %s", (settingid,))
|
||||||
|
else:
|
||||||
|
bind.exec_driver_sql(
|
||||||
|
"UPDATE settings SET `key` = %s WHERE settingid = %s",
|
||||||
|
(newkey, settingid))
|
||||||
|
|
||||||
|
|
||||||
|
def _rename_permissions(bind, old_prefix, new_prefix, old_category,
|
||||||
|
new_category):
|
||||||
|
"""Rename permission rows in place so role links survive, collision-safe."""
|
||||||
|
rows = bind.exec_driver_sql(
|
||||||
|
"SELECT permissionid, name FROM permissions"
|
||||||
|
).fetchall()
|
||||||
|
for permissionid, name in rows:
|
||||||
|
if not name.startswith(old_prefix):
|
||||||
|
continue
|
||||||
|
newname = new_prefix + name[len(old_prefix):]
|
||||||
|
exists = bind.exec_driver_sql(
|
||||||
|
"SELECT 1 FROM permissions WHERE name = %s", (newname,)
|
||||||
|
).fetchone()
|
||||||
|
if exists:
|
||||||
|
bind.exec_driver_sql(
|
||||||
|
"DELETE FROM permissions WHERE permissionid = %s",
|
||||||
|
(permissionid,))
|
||||||
|
else:
|
||||||
|
bind.exec_driver_sql(
|
||||||
|
"UPDATE permissions SET name = %s, category = %s "
|
||||||
|
"WHERE permissionid = %s",
|
||||||
|
(newname, new_category, permissionid))
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
bind = op.get_bind()
|
||||||
|
insp = sa.inspect(bind)
|
||||||
|
tables = set(insp.get_table_names())
|
||||||
|
|
||||||
|
if ('alembic_version_machines' in tables
|
||||||
|
and 'alembic_version_equipment' not in tables):
|
||||||
|
bind.exec_driver_sql(
|
||||||
|
"RENAME TABLE alembic_version_machines TO alembic_version_equipment"
|
||||||
|
)
|
||||||
|
|
||||||
|
if 'settings' in tables:
|
||||||
|
_rename_setting_keys(bind, old_marker='_machine_',
|
||||||
|
new_marker='_equipment_')
|
||||||
|
if 'permissions' in tables:
|
||||||
|
_rename_permissions(bind, old_prefix='machines.',
|
||||||
|
new_prefix='equipment.',
|
||||||
|
old_category='machines', new_category='equipment')
|
||||||
|
if 'auditlogs' in tables:
|
||||||
|
bind.exec_driver_sql(
|
||||||
|
"UPDATE auditlogs SET entitytype='Equipment' "
|
||||||
|
"WHERE entitytype='Machine'"
|
||||||
|
)
|
||||||
|
if 'assettypes' in tables:
|
||||||
|
bind.exec_driver_sql(
|
||||||
|
"UPDATE assettypes SET assettype='equipment', "
|
||||||
|
"pluginname='equipment', tablename='equipment' "
|
||||||
|
"WHERE assettype='machine'"
|
||||||
|
)
|
||||||
|
|
||||||
|
if 'modeltypes' in tables and 'machinetypes' not in tables:
|
||||||
|
bind.exec_driver_sql("SET FOREIGN_KEY_CHECKS=0")
|
||||||
|
for name in _fk_names(insp, 'models', 'modeltypes'):
|
||||||
|
bind.exec_driver_sql(f"ALTER TABLE models DROP FOREIGN KEY {name}")
|
||||||
|
model_indexes = {ix['name'] for ix in insp.get_indexes('models')}
|
||||||
|
if 'modeltypeid' in model_indexes:
|
||||||
|
bind.exec_driver_sql("ALTER TABLE models DROP KEY modeltypeid")
|
||||||
|
bind.exec_driver_sql("RENAME TABLE modeltypes TO machinetypes")
|
||||||
|
bind.exec_driver_sql(
|
||||||
|
"ALTER TABLE machinetypes "
|
||||||
|
"CHANGE modeltypeid machinetypeid INT NOT NULL AUTO_INCREMENT, "
|
||||||
|
"CHANGE modeltype machinetype VARCHAR(100) NOT NULL, "
|
||||||
|
"DROP KEY modeltype, "
|
||||||
|
"ADD UNIQUE KEY machinetype (machinetype)"
|
||||||
|
)
|
||||||
|
bind.exec_driver_sql(
|
||||||
|
"ALTER TABLE models "
|
||||||
|
"CHANGE modeltypeid machinetypeid INT NULL, "
|
||||||
|
"ADD KEY machinetypeid (machinetypeid), "
|
||||||
|
"ADD CONSTRAINT models_ibfk_1 "
|
||||||
|
"FOREIGN KEY (machinetypeid) REFERENCES machinetypes (machinetypeid)"
|
||||||
|
)
|
||||||
|
bind.exec_driver_sql("SET FOREIGN_KEY_CHECKS=1")
|
||||||
@@ -62,7 +62,7 @@ class Computer(BaseModel):
|
|||||||
nullable=True
|
nullable=True
|
||||||
)
|
)
|
||||||
|
|
||||||
# Hardware make/model (PCs carry vendor + model like equipment)
|
# Hardware make/model (PCs carry vendor + model like machines)
|
||||||
vendorid = db.Column(
|
vendorid = db.Column(
|
||||||
db.Integer,
|
db.Integer,
|
||||||
db.ForeignKey('vendors.vendorid'),
|
db.ForeignKey('vendors.vendorid'),
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
"""Equipment plugin for ShopDB."""
|
|
||||||
|
|
||||||
from .plugin import EquipmentPlugin
|
|
||||||
|
|
||||||
__all__ = ['EquipmentPlugin']
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
"""Equipment plugin API."""
|
|
||||||
|
|
||||||
from .routes import equipment_bp
|
|
||||||
|
|
||||||
__all__ = ['equipment_bp']
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
"""Equipment plugin models."""
|
|
||||||
|
|
||||||
from .equipment import Equipment, EquipmentType
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
'Equipment',
|
|
||||||
'EquipmentType',
|
|
||||||
]
|
|
||||||
5
plugins/machines/__init__.py
Normal file
5
plugins/machines/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
"""Machines plugin for ShopDB."""
|
||||||
|
|
||||||
|
from .plugin import MachinesPlugin
|
||||||
|
|
||||||
|
__all__ = ['MachinesPlugin']
|
||||||
5
plugins/machines/api/__init__.py
Normal file
5
plugins/machines/api/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
"""Machines plugin API."""
|
||||||
|
|
||||||
|
from .routes import machines_bp
|
||||||
|
|
||||||
|
__all__ = ['machines_bp']
|
||||||
@@ -1,36 +1,36 @@
|
|||||||
"""Equipment plugin API endpoints."""
|
"""Machines plugin API endpoints."""
|
||||||
|
|
||||||
from flask import Blueprint, request
|
from flask import Blueprint, request
|
||||||
from flask_jwt_extended import jwt_required
|
from flask_jwt_extended import jwt_required
|
||||||
|
|
||||||
from shopdb.api import db, Asset, AssetType, Vendor, Model, AuditLog, success_response, error_response, paginated_response, ErrorCodes, get_pagination_params, paginate_query
|
from shopdb.api import db, Asset, AssetType, Vendor, Model, AuditLog, success_response, error_response, paginated_response, ErrorCodes, get_pagination_params, paginate_query
|
||||||
|
|
||||||
from ..models import Equipment, EquipmentType
|
from ..models import Machine, MachineType
|
||||||
|
|
||||||
from shopdb.api import require_permission, require_role
|
from shopdb.api import require_permission, require_role
|
||||||
|
|
||||||
equipment_bp = Blueprint('equipment', __name__)
|
machines_bp = Blueprint('machines', __name__)
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Equipment Types
|
# Machine Types
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
@equipment_bp.route('/types', methods=['GET'])
|
@machines_bp.route('/types', methods=['GET'])
|
||||||
@jwt_required(optional=True)
|
@jwt_required(optional=True)
|
||||||
def list_equipment_types():
|
def list_machine_types():
|
||||||
"""List all equipment types."""
|
"""List all machine types."""
|
||||||
page, per_page = get_pagination_params(request)
|
page, per_page = get_pagination_params(request)
|
||||||
|
|
||||||
query = EquipmentType.query
|
query = MachineType.query
|
||||||
|
|
||||||
if request.args.get('active', 'true').lower() != 'false':
|
if request.args.get('active', 'true').lower() != 'false':
|
||||||
query = query.filter(EquipmentType.isactive == True)
|
query = query.filter(MachineType.isactive == True)
|
||||||
|
|
||||||
if search := request.args.get('search'):
|
if search := request.args.get('search'):
|
||||||
query = query.filter(EquipmentType.equipmenttype.ilike(f'%{search}%'))
|
query = query.filter(MachineType.machinetype.ilike(f'%{search}%'))
|
||||||
|
|
||||||
query = query.order_by(EquipmentType.equipmenttype)
|
query = query.order_by(MachineType.machinetype)
|
||||||
|
|
||||||
items, total = paginate_query(query, page, per_page)
|
items, total = paginate_query(query, page, per_page)
|
||||||
data = [t.to_dict() for t in items]
|
data = [t.to_dict() for t in items]
|
||||||
@@ -38,33 +38,33 @@ def list_equipment_types():
|
|||||||
return paginated_response(data, page, per_page, total)
|
return paginated_response(data, page, per_page, total)
|
||||||
|
|
||||||
|
|
||||||
@equipment_bp.route('/types/<int:type_id>', methods=['GET'])
|
@machines_bp.route('/types/<int:type_id>', methods=['GET'])
|
||||||
@jwt_required(optional=True)
|
@jwt_required(optional=True)
|
||||||
def get_equipment_type(type_id: int):
|
def get_machine_type(type_id: int):
|
||||||
"""Get a single equipment type."""
|
"""Get a single machine type."""
|
||||||
t = db.session.get(EquipmentType, type_id)
|
t = db.session.get(MachineType, type_id)
|
||||||
|
|
||||||
if not t:
|
if not t:
|
||||||
return error_response(
|
return error_response(
|
||||||
ErrorCodes.NOT_FOUND,
|
ErrorCodes.NOT_FOUND,
|
||||||
f'Equipment type with ID {type_id} not found',
|
f'Machine type with ID {type_id} not found',
|
||||||
http_code=404
|
http_code=404
|
||||||
)
|
)
|
||||||
|
|
||||||
return success_response(t.to_dict())
|
return success_response(t.to_dict())
|
||||||
|
|
||||||
|
|
||||||
@equipment_bp.route('/types', methods=['POST'])
|
@machines_bp.route('/types', methods=['POST'])
|
||||||
@jwt_required()
|
@jwt_required()
|
||||||
@require_permission('equipment.create')
|
@require_permission('machines.create')
|
||||||
def create_equipment_type():
|
def create_machine_type():
|
||||||
"""Create a new equipment type."""
|
"""Create a new machine type."""
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
|
|
||||||
if not data or not data.get('equipmenttype'):
|
if not data or not data.get('machinetype'):
|
||||||
return error_response(ErrorCodes.VALIDATION_ERROR, 'equipmenttype is required')
|
return error_response(ErrorCodes.VALIDATION_ERROR, 'machinetype is required')
|
||||||
|
|
||||||
existing = EquipmentType.query.filter_by(equipmenttype=data['equipmenttype']).first()
|
existing = MachineType.query.filter_by(machinetype=data['machinetype']).first()
|
||||||
if existing:
|
if existing:
|
||||||
if not existing.isactive:
|
if not existing.isactive:
|
||||||
existing.isactive = True
|
existing.isactive = True
|
||||||
@@ -75,12 +75,12 @@ def create_equipment_type():
|
|||||||
return success_response(existing.to_dict(), message='Reactivated existing type')
|
return success_response(existing.to_dict(), message='Reactivated existing type')
|
||||||
return error_response(
|
return error_response(
|
||||||
ErrorCodes.CONFLICT,
|
ErrorCodes.CONFLICT,
|
||||||
f"Equipment type '{data['equipmenttype']}' already exists",
|
f"Machine type '{data['machinetype']}' already exists",
|
||||||
http_code=409
|
http_code=409
|
||||||
)
|
)
|
||||||
|
|
||||||
t = EquipmentType(
|
t = MachineType(
|
||||||
equipmenttype=data['equipmenttype'],
|
machinetype=data['machinetype'],
|
||||||
description=data.get('description'),
|
description=data.get('description'),
|
||||||
icon=data.get('icon'), color=data.get('color')
|
icon=data.get('icon'), color=data.get('color')
|
||||||
)
|
)
|
||||||
@@ -88,20 +88,20 @@ def create_equipment_type():
|
|||||||
db.session.add(t)
|
db.session.add(t)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return success_response(t.to_dict(), message='Equipment type created', http_code=201)
|
return success_response(t.to_dict(), message='Machine type created', http_code=201)
|
||||||
|
|
||||||
|
|
||||||
@equipment_bp.route('/types/<int:type_id>', methods=['PUT'])
|
@machines_bp.route('/types/<int:type_id>', methods=['PUT'])
|
||||||
@jwt_required()
|
@jwt_required()
|
||||||
@require_permission('equipment.edit')
|
@require_permission('machines.edit')
|
||||||
def update_equipment_type(type_id: int):
|
def update_machine_type(type_id: int):
|
||||||
"""Update an equipment type."""
|
"""Update a machine type."""
|
||||||
t = db.session.get(EquipmentType, type_id)
|
t = db.session.get(MachineType, type_id)
|
||||||
|
|
||||||
if not t:
|
if not t:
|
||||||
return error_response(
|
return error_response(
|
||||||
ErrorCodes.NOT_FOUND,
|
ErrorCodes.NOT_FOUND,
|
||||||
f'Equipment type with ID {type_id} not found',
|
f'Machine type with ID {type_id} not found',
|
||||||
http_code=404
|
http_code=404
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -109,62 +109,62 @@ def update_equipment_type(type_id: int):
|
|||||||
if not data:
|
if not data:
|
||||||
return error_response(ErrorCodes.VALIDATION_ERROR, 'No data provided')
|
return error_response(ErrorCodes.VALIDATION_ERROR, 'No data provided')
|
||||||
|
|
||||||
if 'equipmenttype' in data and data['equipmenttype'] != t.equipmenttype:
|
if 'machinetype' in data and data['machinetype'] != t.machinetype:
|
||||||
if EquipmentType.query.filter_by(equipmenttype=data['equipmenttype']).first():
|
if MachineType.query.filter_by(machinetype=data['machinetype']).first():
|
||||||
return error_response(
|
return error_response(
|
||||||
ErrorCodes.CONFLICT,
|
ErrorCodes.CONFLICT,
|
||||||
f"Equipment type '{data['equipmenttype']}' already exists",
|
f"Machine type '{data['machinetype']}' already exists",
|
||||||
http_code=409
|
http_code=409
|
||||||
)
|
)
|
||||||
|
|
||||||
for key in ['equipmenttype', 'description', 'icon', 'color', 'isactive']:
|
for key in ['machinetype', 'description', 'icon', 'color', 'isactive']:
|
||||||
if key in data:
|
if key in data:
|
||||||
setattr(t, key, data[key])
|
setattr(t, key, data[key])
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return success_response(t.to_dict(), message='Equipment type updated')
|
return success_response(t.to_dict(), message='Machine type updated')
|
||||||
|
|
||||||
|
|
||||||
@equipment_bp.route('/types/<int:type_id>', methods=['DELETE'])
|
@machines_bp.route('/types/<int:type_id>', methods=['DELETE'])
|
||||||
@jwt_required()
|
@jwt_required()
|
||||||
@require_permission('equipment.delete')
|
@require_permission('machines.delete')
|
||||||
def delete_equipment_type(type_id: int):
|
def delete_machine_type(type_id: int):
|
||||||
"""Delete an equipment type. Refused if any asset still uses it."""
|
"""Delete a machine type. Refused if any asset still uses it."""
|
||||||
t = db.session.get(EquipmentType, type_id)
|
t = db.session.get(MachineType, type_id)
|
||||||
if not t:
|
if not t:
|
||||||
return error_response(ErrorCodes.NOT_FOUND, 'Equipment type not found', http_code=404)
|
return error_response(ErrorCodes.NOT_FOUND, 'Machine type not found', http_code=404)
|
||||||
inuse = Equipment.query.filter_by(equipmenttypeid=type_id).count()
|
inuse = Machine.query.filter_by(machinetypeid=type_id).count()
|
||||||
if inuse:
|
if inuse:
|
||||||
return error_response(ErrorCodes.CONFLICT,
|
return error_response(ErrorCodes.CONFLICT,
|
||||||
f"Cannot delete: {inuse} asset(s) still use this type", http_code=409)
|
f"Cannot delete: {inuse} asset(s) still use this type", http_code=409)
|
||||||
db.session.delete(t)
|
db.session.delete(t)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return success_response(message='Equipment type deleted')
|
return success_response(message='Machine type deleted')
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Equipment CRUD
|
# Machine CRUD
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
@equipment_bp.route('', methods=['GET'])
|
@machines_bp.route('', methods=['GET'])
|
||||||
@jwt_required(optional=True)
|
@jwt_required(optional=True)
|
||||||
def list_equipment():
|
def list_machines():
|
||||||
"""
|
"""
|
||||||
List all equipment with filtering and pagination.
|
List all machines with filtering and pagination.
|
||||||
|
|
||||||
Query parameters:
|
Query parameters:
|
||||||
- page, per_page: Pagination
|
- page, per_page: Pagination
|
||||||
- active: Filter by active status
|
- active: Filter by active status
|
||||||
- search: Search by asset number or name
|
- search: Search by asset number or name
|
||||||
- type_id: Filter by equipment type ID
|
- type_id: Filter by machine type ID
|
||||||
- vendor_id: Filter by vendor ID
|
- vendor_id: Filter by vendor ID
|
||||||
- location_id: Filter by location ID
|
- location_id: Filter by location ID
|
||||||
- businessunit_id: Filter by business unit ID
|
- businessunit_id: Filter by business unit ID
|
||||||
"""
|
"""
|
||||||
page, per_page = get_pagination_params(request)
|
page, per_page = get_pagination_params(request)
|
||||||
|
|
||||||
# Join Equipment with Asset
|
# Join Machine with Asset
|
||||||
query = db.session.query(Equipment).join(Asset)
|
query = db.session.query(Machine).join(Asset)
|
||||||
|
|
||||||
# Active filter
|
# Active filter
|
||||||
if request.args.get('active', 'true').lower() != 'false':
|
if request.args.get('active', 'true').lower() != 'false':
|
||||||
@@ -180,13 +180,13 @@ def list_equipment():
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Equipment type filter
|
# Machine type filter
|
||||||
if type_id := request.args.get('typeid', request.args.get('type_id')):
|
if type_id := request.args.get('typeid', request.args.get('type_id')):
|
||||||
query = query.filter(Equipment.equipmenttypeid == int(type_id))
|
query = query.filter(Machine.machinetypeid == int(type_id))
|
||||||
|
|
||||||
# Vendor filter
|
# Vendor filter
|
||||||
if vendor_id := request.args.get('vendorid', request.args.get('vendor_id')):
|
if vendor_id := request.args.get('vendorid', request.args.get('vendor_id')):
|
||||||
query = query.filter(Equipment.vendorid == int(vendor_id))
|
query = query.filter(Machine.vendorid == int(vendor_id))
|
||||||
|
|
||||||
# Location filter
|
# Location filter
|
||||||
if location_id := request.args.get('locationid', request.args.get('location_id')):
|
if location_id := request.args.get('locationid', request.args.get('location_id')):
|
||||||
@@ -211,67 +211,67 @@ def list_equipment():
|
|||||||
|
|
||||||
items, total = paginate_query(query, page, per_page)
|
items, total = paginate_query(query, page, per_page)
|
||||||
|
|
||||||
# Build response with both asset and equipment data
|
# Build response with both asset and machine data
|
||||||
data = []
|
data = []
|
||||||
for equip in items:
|
for mach in items:
|
||||||
item = equip.asset.to_dict() if equip.asset else {}
|
item = mach.asset.to_dict() if mach.asset else {}
|
||||||
item['equipment'] = equip.to_dict()
|
item['machine'] = mach.to_dict()
|
||||||
data.append(item)
|
data.append(item)
|
||||||
|
|
||||||
return paginated_response(data, page, per_page, total)
|
return paginated_response(data, page, per_page, total)
|
||||||
|
|
||||||
|
|
||||||
@equipment_bp.route('/<int:equipment_id>', methods=['GET'])
|
@machines_bp.route('/<int:machine_id>', methods=['GET'])
|
||||||
@jwt_required(optional=True)
|
@jwt_required(optional=True)
|
||||||
def get_equipment(equipment_id: int):
|
def get_machine(machine_id: int):
|
||||||
"""Get a single equipment item with full details."""
|
"""Get a single machine item with full details."""
|
||||||
equip = db.session.get(Equipment, equipment_id)
|
mach = db.session.get(Machine, machine_id)
|
||||||
|
|
||||||
if not equip:
|
if not mach:
|
||||||
return error_response(
|
return error_response(
|
||||||
ErrorCodes.NOT_FOUND,
|
ErrorCodes.NOT_FOUND,
|
||||||
f'Equipment with ID {equipment_id} not found',
|
f'Machine with ID {machine_id} not found',
|
||||||
http_code=404
|
http_code=404
|
||||||
)
|
)
|
||||||
|
|
||||||
result = equip.asset.to_dict() if equip.asset else {}
|
result = mach.asset.to_dict() if mach.asset else {}
|
||||||
result['equipment'] = equip.to_dict()
|
result['machine'] = mach.to_dict()
|
||||||
|
|
||||||
return success_response(result)
|
return success_response(result)
|
||||||
|
|
||||||
|
|
||||||
@equipment_bp.route('/by-asset/<int:asset_id>', methods=['GET'])
|
@machines_bp.route('/by-asset/<int:asset_id>', methods=['GET'])
|
||||||
@jwt_required(optional=True)
|
@jwt_required(optional=True)
|
||||||
def get_equipment_by_asset(asset_id: int):
|
def get_machine_by_asset(asset_id: int):
|
||||||
"""Get equipment data by asset ID."""
|
"""Get machine data by asset ID."""
|
||||||
equip = Equipment.query.filter_by(assetid=asset_id).first()
|
mach = Machine.query.filter_by(assetid=asset_id).first()
|
||||||
|
|
||||||
if not equip:
|
if not mach:
|
||||||
return error_response(
|
return error_response(
|
||||||
ErrorCodes.NOT_FOUND,
|
ErrorCodes.NOT_FOUND,
|
||||||
f'Equipment for asset {asset_id} not found',
|
f'Machine for asset {asset_id} not found',
|
||||||
http_code=404
|
http_code=404
|
||||||
)
|
)
|
||||||
|
|
||||||
result = equip.asset.to_dict() if equip.asset else {}
|
result = mach.asset.to_dict() if mach.asset else {}
|
||||||
result['equipment'] = equip.to_dict()
|
result['machine'] = mach.to_dict()
|
||||||
|
|
||||||
return success_response(result)
|
return success_response(result)
|
||||||
|
|
||||||
|
|
||||||
@equipment_bp.route('', methods=['POST'])
|
@machines_bp.route('', methods=['POST'])
|
||||||
@jwt_required()
|
@jwt_required()
|
||||||
@require_permission('equipment.create')
|
@require_permission('machines.create')
|
||||||
def create_equipment():
|
def create_machine():
|
||||||
"""
|
"""
|
||||||
Create new equipment (creates both Asset and Equipment records).
|
Create new machine (creates both Asset and Machine records).
|
||||||
|
|
||||||
Required fields:
|
Required fields:
|
||||||
- assetnumber: Business identifier
|
- assetnumber: Business identifier
|
||||||
|
|
||||||
Optional fields:
|
Optional fields:
|
||||||
- name, serialnumber, statusid, locationid, businessunitid
|
- name, serialnumber, statusid, locationid, businessunitid
|
||||||
- equipmenttypeid, vendorid, modelnumberid
|
- machinetypeid, vendorid, modelnumberid
|
||||||
- requiresmanualconfig, islocationonly
|
- requiresmanualconfig, islocationonly
|
||||||
- mapx, mapy, notes
|
- mapx, mapy, notes
|
||||||
"""
|
"""
|
||||||
@@ -291,12 +291,12 @@ def create_equipment():
|
|||||||
http_code=409
|
http_code=409
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get equipment asset type
|
# Get machine asset type
|
||||||
equipment_type = AssetType.query.filter_by(assettype='equipment').first()
|
machine_type = AssetType.query.filter_by(assettype='machine').first()
|
||||||
if not equipment_type:
|
if not machine_type:
|
||||||
return error_response(
|
return error_response(
|
||||||
ErrorCodes.INTERNAL_ERROR,
|
ErrorCodes.INTERNAL_ERROR,
|
||||||
'Equipment asset type not found. Plugin may not be properly installed.',
|
'Machine asset type not found. Plugin may not be properly installed.',
|
||||||
http_code=500
|
http_code=500
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -307,7 +307,7 @@ def create_equipment():
|
|||||||
gaugelabreference=data.get('gaugelabreference'),
|
gaugelabreference=data.get('gaugelabreference'),
|
||||||
maintenancereference=data.get('maintenancereference'),
|
maintenancereference=data.get('maintenancereference'),
|
||||||
serialnumber=data.get('serialnumber'),
|
serialnumber=data.get('serialnumber'),
|
||||||
assettypeid=equipment_type.assettypeid,
|
assettypeid=machine_type.assettypeid,
|
||||||
statusid=data.get('statusid', 1),
|
statusid=data.get('statusid', 1),
|
||||||
locationid=data.get('locationid'),
|
locationid=data.get('locationid'),
|
||||||
businessunitid=data.get('businessunitid'),
|
businessunitid=data.get('businessunitid'),
|
||||||
@@ -319,10 +319,10 @@ def create_equipment():
|
|||||||
db.session.add(asset)
|
db.session.add(asset)
|
||||||
db.session.flush() # Get the assetid
|
db.session.flush() # Get the assetid
|
||||||
|
|
||||||
# Create the equipment extension
|
# Create the machine extension
|
||||||
equip = Equipment(
|
mach = Machine(
|
||||||
assetid=asset.assetid,
|
assetid=asset.assetid,
|
||||||
equipmenttypeid=data.get('equipmenttypeid'),
|
machinetypeid=data.get('machinetypeid'),
|
||||||
vendorid=data.get('vendorid'),
|
vendorid=data.get('vendorid'),
|
||||||
modelnumberid=data.get('modelnumberid'),
|
modelnumberid=data.get('modelnumberid'),
|
||||||
requiresmanualconfig=data.get('requiresmanualconfig', False),
|
requiresmanualconfig=data.get('requiresmanualconfig', False),
|
||||||
@@ -334,32 +334,32 @@ def create_equipment():
|
|||||||
controllermodelid=data.get('controllermodelid')
|
controllermodelid=data.get('controllermodelid')
|
||||||
)
|
)
|
||||||
|
|
||||||
db.session.add(equip)
|
db.session.add(mach)
|
||||||
db.session.flush()
|
db.session.flush()
|
||||||
|
|
||||||
# Audit log
|
# Audit log
|
||||||
AuditLog.log('created', 'Equipment', entityid=equip.equipmentid,
|
AuditLog.log('created', 'Machine', entityid=mach.machineid,
|
||||||
entityname=data['assetnumber'])
|
entityname=data['assetnumber'])
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
result = asset.to_dict()
|
result = asset.to_dict()
|
||||||
result['equipment'] = equip.to_dict()
|
result['machine'] = mach.to_dict()
|
||||||
|
|
||||||
return success_response(result, message='Equipment created', http_code=201)
|
return success_response(result, message='Machine created', http_code=201)
|
||||||
|
|
||||||
|
|
||||||
@equipment_bp.route('/<int:equipment_id>', methods=['PUT'])
|
@machines_bp.route('/<int:machine_id>', methods=['PUT'])
|
||||||
@jwt_required()
|
@jwt_required()
|
||||||
@require_permission('equipment.edit')
|
@require_permission('machines.edit')
|
||||||
def update_equipment(equipment_id: int):
|
def update_machine(machine_id: int):
|
||||||
"""Update equipment (both Asset and Equipment records)."""
|
"""Update machine (both Asset and Machine records)."""
|
||||||
equip = db.session.get(Equipment, equipment_id)
|
mach = db.session.get(Machine, machine_id)
|
||||||
|
|
||||||
if not equip:
|
if not mach:
|
||||||
return error_response(
|
return error_response(
|
||||||
ErrorCodes.NOT_FOUND,
|
ErrorCodes.NOT_FOUND,
|
||||||
f'Equipment with ID {equipment_id} not found',
|
f'Machine with ID {machine_id} not found',
|
||||||
http_code=404
|
http_code=404
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -367,7 +367,7 @@ def update_equipment(equipment_id: int):
|
|||||||
if not data:
|
if not data:
|
||||||
return error_response(ErrorCodes.VALIDATION_ERROR, 'No data provided')
|
return error_response(ErrorCodes.VALIDATION_ERROR, 'No data provided')
|
||||||
|
|
||||||
asset = equip.asset
|
asset = mach.asset
|
||||||
|
|
||||||
# Check for conflicting assetnumber
|
# Check for conflicting assetnumber
|
||||||
if 'assetnumber' in data and data['assetnumber'] != asset.assetnumber:
|
if 'assetnumber' in data and data['assetnumber'] != asset.assetnumber:
|
||||||
@@ -394,87 +394,87 @@ def update_equipment(equipment_id: int):
|
|||||||
changes[key] = {'old': old_val, 'new': new_val}
|
changes[key] = {'old': old_val, 'new': new_val}
|
||||||
setattr(asset, key, data[key])
|
setattr(asset, key, data[key])
|
||||||
|
|
||||||
# Update equipment fields
|
# Update machine fields
|
||||||
equipment_fields = ['equipmenttypeid', 'vendorid', 'modelnumberid',
|
machine_fields = ['machinetypeid', 'vendorid', 'modelnumberid',
|
||||||
'requiresmanualconfig', 'islocationonly',
|
'requiresmanualconfig', 'islocationonly',
|
||||||
'lastmaintenancedate', 'nextmaintenancedate', 'maintenanceintervaldays',
|
'lastmaintenancedate', 'nextmaintenancedate', 'maintenanceintervaldays',
|
||||||
'controllervendorid', 'controllermodelid']
|
'controllervendorid', 'controllermodelid']
|
||||||
for key in equipment_fields:
|
for key in machine_fields:
|
||||||
if key in data:
|
if key in data:
|
||||||
old_val = getattr(equip, key)
|
old_val = getattr(mach, key)
|
||||||
new_val = data[key]
|
new_val = data[key]
|
||||||
if old_val != new_val:
|
if old_val != new_val:
|
||||||
changes[key] = {'old': old_val, 'new': new_val}
|
changes[key] = {'old': old_val, 'new': new_val}
|
||||||
setattr(equip, key, data[key])
|
setattr(mach, key, data[key])
|
||||||
|
|
||||||
# Audit log if there were changes
|
# Audit log if there were changes
|
||||||
if changes:
|
if changes:
|
||||||
AuditLog.log('updated', 'Equipment', entityid=equip.equipmentid,
|
AuditLog.log('updated', 'Machine', entityid=mach.machineid,
|
||||||
entityname=asset.assetnumber, changes=changes)
|
entityname=asset.assetnumber, changes=changes)
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
result = asset.to_dict()
|
result = asset.to_dict()
|
||||||
result['equipment'] = equip.to_dict()
|
result['machine'] = mach.to_dict()
|
||||||
|
|
||||||
return success_response(result, message='Equipment updated')
|
return success_response(result, message='Machine updated')
|
||||||
|
|
||||||
|
|
||||||
@equipment_bp.route('/<int:equipment_id>', methods=['DELETE'])
|
@machines_bp.route('/<int:machine_id>', methods=['DELETE'])
|
||||||
@jwt_required()
|
@jwt_required()
|
||||||
@require_permission('equipment.delete')
|
@require_permission('machines.delete')
|
||||||
def delete_equipment(equipment_id: int):
|
def delete_machine(machine_id: int):
|
||||||
"""Delete (soft delete) equipment."""
|
"""Delete (soft delete) machine."""
|
||||||
equip = db.session.get(Equipment, equipment_id)
|
mach = db.session.get(Machine, machine_id)
|
||||||
|
|
||||||
if not equip:
|
if not mach:
|
||||||
return error_response(
|
return error_response(
|
||||||
ErrorCodes.NOT_FOUND,
|
ErrorCodes.NOT_FOUND,
|
||||||
f'Equipment with ID {equipment_id} not found',
|
f'Machine with ID {machine_id} not found',
|
||||||
http_code=404
|
http_code=404
|
||||||
)
|
)
|
||||||
|
|
||||||
# Soft delete the asset (equipment extension will stay linked)
|
# Soft delete the asset (machine extension will stay linked)
|
||||||
equip.asset.isactive = False
|
mach.asset.isactive = False
|
||||||
|
|
||||||
# Audit log
|
# Audit log
|
||||||
AuditLog.log('deleted', 'Equipment', entityid=equip.equipmentid,
|
AuditLog.log('deleted', 'Machine', entityid=mach.machineid,
|
||||||
entityname=equip.asset.assetnumber)
|
entityname=mach.asset.assetnumber)
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return success_response(message='Equipment deleted')
|
return success_response(message='Machine deleted')
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Dashboard
|
# Dashboard
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
@equipment_bp.route('/dashboard/summary', methods=['GET'])
|
@machines_bp.route('/dashboard/summary', methods=['GET'])
|
||||||
@jwt_required(optional=True)
|
@jwt_required(optional=True)
|
||||||
def dashboard_summary():
|
def dashboard_summary():
|
||||||
"""Get equipment dashboard summary data."""
|
"""Get machine dashboard summary data."""
|
||||||
# Total active equipment count
|
# Total active machine count
|
||||||
total = db.session.query(Equipment).join(Asset).filter(
|
total = db.session.query(Machine).join(Asset).filter(
|
||||||
Asset.isactive == True
|
Asset.isactive == True
|
||||||
).count()
|
).count()
|
||||||
|
|
||||||
# Count by equipment type
|
# Count by machine type
|
||||||
by_type = db.session.query(
|
by_type = db.session.query(
|
||||||
EquipmentType.equipmenttype,
|
MachineType.machinetype,
|
||||||
db.func.count(Equipment.equipmentid)
|
db.func.count(Machine.machineid)
|
||||||
).join(Equipment, Equipment.equipmenttypeid == EquipmentType.equipmenttypeid
|
).join(Machine, Machine.machinetypeid == MachineType.machinetypeid
|
||||||
).join(Asset, Asset.assetid == Equipment.assetid
|
).join(Asset, Asset.assetid == Machine.assetid
|
||||||
).filter(Asset.isactive == True
|
).filter(Asset.isactive == True
|
||||||
).group_by(EquipmentType.equipmenttype
|
).group_by(MachineType.machinetype
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
# Count by status
|
# Count by status
|
||||||
from shopdb.api import AssetStatus
|
from shopdb.api import AssetStatus
|
||||||
by_status = db.session.query(
|
by_status = db.session.query(
|
||||||
AssetStatus.status,
|
AssetStatus.status,
|
||||||
db.func.count(Equipment.equipmentid)
|
db.func.count(Machine.machineid)
|
||||||
).join(Asset, Asset.assetid == Equipment.assetid
|
).join(Asset, Asset.assetid == Machine.assetid
|
||||||
).join(AssetStatus, AssetStatus.statusid == Asset.statusid
|
).join(AssetStatus, AssetStatus.statusid == Asset.statusid
|
||||||
).filter(Asset.isactive == True
|
).filter(Asset.isactive == True
|
||||||
).group_by(AssetStatus.status
|
).group_by(AssetStatus.status
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
{
|
{
|
||||||
"name": "equipment",
|
"name": "machines",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "Equipment management plugin for CNCs, CMMs, lathes, grinders, and other manufacturing equipment",
|
"description": "Machine management plugin for CNCs, CMMs, lathes, grinders, and other manufacturing machines",
|
||||||
"author": "ShopDB Team",
|
"author": "ShopDB Team",
|
||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
"core_version": ">=0.1.0,<1.0.0",
|
"core_version": ">=0.1.0,<1.0.0",
|
||||||
"api_prefix": "/api/equipment",
|
"api_prefix": "/api/machines",
|
||||||
"provides": {
|
"provides": {
|
||||||
"asset_type": "equipment",
|
"asset_type": "machine",
|
||||||
"features": [
|
"features": [
|
||||||
"equipment_tracking",
|
"machine_tracking",
|
||||||
"maintenance_scheduling",
|
"maintenance_scheduling",
|
||||||
"vendor_management",
|
"vendor_management",
|
||||||
"model_catalog"
|
"model_catalog"
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
"""Alembic environment for the equipment plugin migration chain.
|
"""Alembic environment for the machines plugin migration chain.
|
||||||
|
|
||||||
Delegates to the shared runner in shopdb.plugins.alembic_template, which
|
Delegates to the shared runner in shopdb.plugins.alembic_template, which
|
||||||
filters the metadata to this plugin's tables and drives Alembic against the
|
filters the metadata to this plugin's tables and drives Alembic against the
|
||||||
per-plugin version table alembic_version_equipment. See ADR-008 for the
|
per-plugin version table alembic_version_machines. See ADR-008 for the
|
||||||
ownership cutover between the core chain and per-plugin chains.
|
ownership cutover between the core chain and per-plugin chains.
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
|
|
||||||
os.environ['PLUGIN_NAME'] = 'equipment'
|
os.environ['PLUGIN_NAME'] = 'machines'
|
||||||
|
|
||||||
from shopdb.plugins.alembic_template import run_migrations # noqa: E402
|
from shopdb.plugins.alembic_template import run_migrations # noqa: E402
|
||||||
|
|
||||||
@@ -1,11 +1,15 @@
|
|||||||
"""equipment plugin anchor (ownership cutover).
|
"""machines plugin anchor (ownership cutover; authored as equipment).
|
||||||
|
|
||||||
Stamp-only no-op. The core Alembic chain (baseline .. 7d16_directoryemployees)
|
Stamp-only no-op. The core Alembic chain (baseline .. 7d16_directoryemployees)
|
||||||
already created every table this plugin owns at the cutover point, so there is
|
already created every table this plugin owns at the cutover point, so there is
|
||||||
nothing to build here. This revision gives the plugin's own chain a base that
|
nothing to build here. This revision gives the plugin's own chain a base that
|
||||||
`flask plugin upgrade-all` can stamp into alembic_version_equipment. From this
|
`flask plugin upgrade-all` can stamp into alembic_version_machines. From this
|
||||||
anchor forward, new equipment schema changes land as 000N revisions in this
|
anchor forward, new machines schema changes land as 000N revisions in this
|
||||||
directory, never in the core chain. See ADR-008.
|
directory, never in the core chain. See ADR-008.
|
||||||
|
|
||||||
|
The revision id keeps its original 'equipment0001anchor' string: ids are
|
||||||
|
arbitrary and existing installs already carry it in their version table
|
||||||
|
(which the core chain renames to alembic_version_machines).
|
||||||
"""
|
"""
|
||||||
from alembic import op # noqa: F401
|
from alembic import op # noqa: F401
|
||||||
import sqlalchemy as sa # noqa: F401
|
import sqlalchemy as sa # noqa: F401
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
"""Rename equipment tables to machines
|
||||||
|
|
||||||
|
Plugin half of the approved equipment -> machines rename (the core half is
|
||||||
|
migrations/versions/7d17_machines_rename.py):
|
||||||
|
|
||||||
|
equipment -> machines (equipmentid -> machineid,
|
||||||
|
equipmenttypeid -> machinetypeid)
|
||||||
|
equipmenttypes -> machinetypes (equipmenttypeid -> machinetypeid,
|
||||||
|
equipmenttype -> machinetype)
|
||||||
|
|
||||||
|
Runs on BOTH upgraded installs and fresh installs: the core baseline creates
|
||||||
|
these tables under their old names (frozen DDL), so this revision always does
|
||||||
|
the physical rename. Idempotent: skips when the tables already carry the new
|
||||||
|
names (e.g. test DBs built by db.create_all() from current models). Refuses
|
||||||
|
to run while the core machinetypes table still exists - `flask db upgrade`
|
||||||
|
(which renames it to modeltypes) must land first.
|
||||||
|
|
||||||
|
Revision ID: machines0002rename
|
||||||
|
Revises: equipment0001anchor
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
revision = 'machines0002rename'
|
||||||
|
down_revision = 'equipment0001anchor'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def _fk_names(insp, table, referred_table):
|
||||||
|
# find FK constraint names on table pointing at referred_table
|
||||||
|
return [fk['name'] for fk in insp.get_foreign_keys(table)
|
||||||
|
if fk.get('referred_table') == referred_table and fk.get('name')]
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
bind = op.get_bind()
|
||||||
|
insp = sa.inspect(bind)
|
||||||
|
tables = set(insp.get_table_names())
|
||||||
|
|
||||||
|
if 'equipment' not in tables and 'equipmenttypes' not in tables:
|
||||||
|
# already renamed, or fresh schema built straight from current models
|
||||||
|
return
|
||||||
|
|
||||||
|
if 'machinetypes' in tables:
|
||||||
|
# core chain still owns the machinetypes name (not yet renamed to
|
||||||
|
# modeltypes) - renaming equipmenttypes onto it would collide
|
||||||
|
raise RuntimeError(
|
||||||
|
"Core migration 7d17_machines_rename has not run: the legacy "
|
||||||
|
"machinetypes table still exists. Run `flask db upgrade` before "
|
||||||
|
"`flask plugin upgrade-all`."
|
||||||
|
)
|
||||||
|
|
||||||
|
if bind.dialect.name == 'mysql':
|
||||||
|
_upgrade_mysql(bind, insp)
|
||||||
|
else:
|
||||||
|
_upgrade_generic(insp)
|
||||||
|
|
||||||
|
|
||||||
|
def _upgrade_mysql(bind, insp):
|
||||||
|
bind.exec_driver_sql("SET FOREIGN_KEY_CHECKS=0")
|
||||||
|
|
||||||
|
# snapshot index names before the renames invalidate the table name
|
||||||
|
machine_indexes = {ix['name'] for ix in insp.get_indexes('equipment')}
|
||||||
|
|
||||||
|
# drop the equipment -> equipmenttypes FK before renaming the parent cols
|
||||||
|
for name in _fk_names(insp, 'equipment', 'equipmenttypes'):
|
||||||
|
bind.exec_driver_sql(f"ALTER TABLE equipment DROP FOREIGN KEY {name}")
|
||||||
|
|
||||||
|
bind.exec_driver_sql("RENAME TABLE equipmenttypes TO machinetypes")
|
||||||
|
bind.exec_driver_sql("RENAME TABLE equipment TO machines")
|
||||||
|
|
||||||
|
bind.exec_driver_sql(
|
||||||
|
"ALTER TABLE machinetypes "
|
||||||
|
"CHANGE equipmenttypeid machinetypeid INT NOT NULL AUTO_INCREMENT, "
|
||||||
|
"CHANGE equipmenttype machinetype VARCHAR(100) NOT NULL, "
|
||||||
|
"DROP KEY equipmenttype, "
|
||||||
|
"ADD UNIQUE KEY machinetype (machinetype)"
|
||||||
|
)
|
||||||
|
|
||||||
|
parts = [
|
||||||
|
"CHANGE equipmentid machineid INT NOT NULL AUTO_INCREMENT",
|
||||||
|
"CHANGE equipmenttypeid machinetypeid INT NULL",
|
||||||
|
]
|
||||||
|
# rename the model-declared index names to match the Machine model
|
||||||
|
if 'idx_equipment_type' in machine_indexes:
|
||||||
|
parts += ["DROP KEY idx_equipment_type",
|
||||||
|
"ADD KEY idx_machine_type (machinetypeid)"]
|
||||||
|
if 'idx_equipment_vendor' in machine_indexes:
|
||||||
|
parts += ["DROP KEY idx_equipment_vendor",
|
||||||
|
"ADD KEY idx_machine_vendor (vendorid)"]
|
||||||
|
if 'ix_equipment_assetid' in machine_indexes:
|
||||||
|
parts += ["DROP KEY ix_equipment_assetid",
|
||||||
|
"ADD UNIQUE KEY ix_machines_assetid (assetid)"]
|
||||||
|
parts += [
|
||||||
|
"ADD CONSTRAINT fk_machines_machinetypeid "
|
||||||
|
"FOREIGN KEY (machinetypeid) REFERENCES machinetypes (machinetypeid)"
|
||||||
|
]
|
||||||
|
bind.exec_driver_sql("ALTER TABLE machines " + ", ".join(parts))
|
||||||
|
|
||||||
|
bind.exec_driver_sql("SET FOREIGN_KEY_CHECKS=1")
|
||||||
|
|
||||||
|
|
||||||
|
def _upgrade_generic(insp):
|
||||||
|
# SQLite (and anything else): batch rename via ALTER ... RENAME
|
||||||
|
op.rename_table('equipmenttypes', 'machinetypes')
|
||||||
|
op.rename_table('equipment', 'machines')
|
||||||
|
with op.batch_alter_table('machinetypes') as batch_op:
|
||||||
|
batch_op.alter_column('equipmenttypeid', new_column_name='machinetypeid',
|
||||||
|
existing_type=sa.Integer(), existing_nullable=False)
|
||||||
|
batch_op.alter_column('equipmenttype', new_column_name='machinetype',
|
||||||
|
existing_type=sa.String(100),
|
||||||
|
existing_nullable=False)
|
||||||
|
with op.batch_alter_table('machines') as batch_op:
|
||||||
|
batch_op.alter_column('equipmentid', new_column_name='machineid',
|
||||||
|
existing_type=sa.Integer(), existing_nullable=False)
|
||||||
|
batch_op.alter_column('equipmenttypeid', new_column_name='machinetypeid',
|
||||||
|
existing_type=sa.Integer(), existing_nullable=True)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
bind = op.get_bind()
|
||||||
|
insp = sa.inspect(bind)
|
||||||
|
tables = set(insp.get_table_names())
|
||||||
|
|
||||||
|
if 'machines' not in tables and 'machinetypes' not in tables:
|
||||||
|
return
|
||||||
|
|
||||||
|
if bind.dialect.name != 'mysql':
|
||||||
|
raise NotImplementedError(
|
||||||
|
"downgrade implemented for MySQL only (dev/prod dialect)")
|
||||||
|
|
||||||
|
bind.exec_driver_sql("SET FOREIGN_KEY_CHECKS=0")
|
||||||
|
machine_indexes = {ix['name'] for ix in insp.get_indexes('machines')}
|
||||||
|
for name in _fk_names(insp, 'machines', 'machinetypes'):
|
||||||
|
bind.exec_driver_sql(f"ALTER TABLE machines DROP FOREIGN KEY {name}")
|
||||||
|
|
||||||
|
bind.exec_driver_sql("RENAME TABLE machinetypes TO equipmenttypes")
|
||||||
|
bind.exec_driver_sql("RENAME TABLE machines TO equipment")
|
||||||
|
|
||||||
|
bind.exec_driver_sql(
|
||||||
|
"ALTER TABLE equipmenttypes "
|
||||||
|
"CHANGE machinetypeid equipmenttypeid INT NOT NULL AUTO_INCREMENT, "
|
||||||
|
"CHANGE machinetype equipmenttype VARCHAR(100) NOT NULL, "
|
||||||
|
"DROP KEY machinetype, "
|
||||||
|
"ADD UNIQUE KEY equipmenttype (equipmenttype)"
|
||||||
|
)
|
||||||
|
|
||||||
|
parts = [
|
||||||
|
"CHANGE machineid equipmentid INT NOT NULL AUTO_INCREMENT",
|
||||||
|
"CHANGE machinetypeid equipmenttypeid INT NULL",
|
||||||
|
]
|
||||||
|
if 'idx_machine_type' in machine_indexes:
|
||||||
|
parts += ["DROP KEY idx_machine_type",
|
||||||
|
"ADD KEY idx_equipment_type (equipmenttypeid)"]
|
||||||
|
if 'idx_machine_vendor' in machine_indexes:
|
||||||
|
parts += ["DROP KEY idx_machine_vendor",
|
||||||
|
"ADD KEY idx_equipment_vendor (vendorid)"]
|
||||||
|
if 'ix_machines_assetid' in machine_indexes:
|
||||||
|
parts += ["DROP KEY ix_machines_assetid",
|
||||||
|
"ADD UNIQUE KEY ix_equipment_assetid (assetid)"]
|
||||||
|
parts += [
|
||||||
|
"ADD CONSTRAINT equipment_ibfk_2 "
|
||||||
|
"FOREIGN KEY (equipmenttypeid) REFERENCES equipmenttypes (equipmenttypeid)"
|
||||||
|
]
|
||||||
|
bind.exec_driver_sql("ALTER TABLE equipment " + ", ".join(parts))
|
||||||
|
bind.exec_driver_sql("SET FOREIGN_KEY_CHECKS=1")
|
||||||
8
plugins/machines/models/__init__.py
Normal file
8
plugins/machines/models/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
"""Machines plugin models."""
|
||||||
|
|
||||||
|
from .machine import Machine, MachineType
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'Machine',
|
||||||
|
'MachineType',
|
||||||
|
]
|
||||||
@@ -1,36 +1,36 @@
|
|||||||
"""Equipment plugin models."""
|
"""Machines plugin models."""
|
||||||
|
|
||||||
from shopdb.api import db, BaseModel
|
from shopdb.api import db, BaseModel
|
||||||
|
|
||||||
|
|
||||||
class EquipmentType(BaseModel):
|
class MachineType(BaseModel):
|
||||||
"""
|
"""
|
||||||
Equipment type classification.
|
Machine type classification.
|
||||||
|
|
||||||
Examples: CNC, CMM, Lathe, Grinder, EDM, Part Marker, etc.
|
Examples: CNC, CMM, Lathe, Grinder, EDM, Part Marker, etc.
|
||||||
"""
|
"""
|
||||||
__tablename__ = 'equipmenttypes'
|
__tablename__ = 'machinetypes'
|
||||||
|
|
||||||
equipmenttypeid = db.Column(db.Integer, primary_key=True)
|
machinetypeid = db.Column(db.Integer, primary_key=True)
|
||||||
equipmenttype = db.Column(db.String(100), unique=True, nullable=False)
|
machinetype = db.Column(db.String(100), unique=True, nullable=False)
|
||||||
description = db.Column(db.Text)
|
description = db.Column(db.Text)
|
||||||
icon = db.Column(db.String(50), comment='Icon name for UI')
|
icon = db.Column(db.String(50), comment='Icon name for UI')
|
||||||
color = db.Column(db.String(20), comment='CSS color for UI/map markers')
|
color = db.Column(db.String(20), comment='CSS color for UI/map markers')
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<EquipmentType {self.equipmenttype}>"
|
return f"<MachineType {self.machinetype}>"
|
||||||
|
|
||||||
|
|
||||||
class Equipment(BaseModel):
|
class Machine(BaseModel):
|
||||||
"""
|
"""
|
||||||
Equipment-specific extension data.
|
Machine-specific extension data.
|
||||||
|
|
||||||
Links to core Asset table via assetid.
|
Links to core Asset table via assetid.
|
||||||
Stores equipment-specific fields like type, model, vendor, etc.
|
Stores machine-specific fields like type, model, vendor, etc.
|
||||||
"""
|
"""
|
||||||
__tablename__ = 'equipment'
|
__tablename__ = 'machines'
|
||||||
|
|
||||||
equipmentid = db.Column(db.Integer, primary_key=True)
|
machineid = db.Column(db.Integer, primary_key=True)
|
||||||
|
|
||||||
# Link to core asset
|
# Link to core asset
|
||||||
assetid = db.Column(
|
assetid = db.Column(
|
||||||
@@ -41,10 +41,10 @@ class Equipment(BaseModel):
|
|||||||
index=True
|
index=True
|
||||||
)
|
)
|
||||||
|
|
||||||
# Equipment classification
|
# Machine classification
|
||||||
equipmenttypeid = db.Column(
|
machinetypeid = db.Column(
|
||||||
db.Integer,
|
db.Integer,
|
||||||
db.ForeignKey('equipmenttypes.equipmenttypeid'),
|
db.ForeignKey('machinetypes.machinetypeid'),
|
||||||
nullable=True
|
nullable=True
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ class Equipment(BaseModel):
|
|||||||
nullable=True
|
nullable=True
|
||||||
)
|
)
|
||||||
|
|
||||||
# Equipment-specific fields
|
# Machine-specific fields
|
||||||
requiresmanualconfig = db.Column(
|
requiresmanualconfig = db.Column(
|
||||||
db.Boolean,
|
db.Boolean,
|
||||||
default=False,
|
default=False,
|
||||||
@@ -69,7 +69,7 @@ class Equipment(BaseModel):
|
|||||||
islocationonly = db.Column(
|
islocationonly = db.Column(
|
||||||
db.Boolean,
|
db.Boolean,
|
||||||
default=False,
|
default=False,
|
||||||
comment='Virtual location marker (not actual equipment)'
|
comment='Virtual location marker (not actual machine)'
|
||||||
)
|
)
|
||||||
|
|
||||||
# Maintenance tracking
|
# Maintenance tracking
|
||||||
@@ -94,29 +94,29 @@ class Equipment(BaseModel):
|
|||||||
# Relationships
|
# Relationships
|
||||||
asset = db.relationship(
|
asset = db.relationship(
|
||||||
'Asset',
|
'Asset',
|
||||||
backref=db.backref('equipment', uselist=False, lazy='joined')
|
backref=db.backref('machine', uselist=False, lazy='joined')
|
||||||
)
|
)
|
||||||
equipmenttype = db.relationship('EquipmentType', backref='equipment')
|
machinetype = db.relationship('MachineType', backref='machines')
|
||||||
vendor = db.relationship('Vendor', foreign_keys=[vendorid], backref='equipment_items')
|
vendor = db.relationship('Vendor', foreign_keys=[vendorid], backref='machine_items')
|
||||||
model = db.relationship('Model', foreign_keys=[modelnumberid], backref='equipment_items')
|
model = db.relationship('Model', foreign_keys=[modelnumberid], backref='machine_items')
|
||||||
controllervendor = db.relationship('Vendor', foreign_keys=[controllervendorid], backref='equipment_controllers')
|
controllervendor = db.relationship('Vendor', foreign_keys=[controllervendorid], backref='machine_controllers')
|
||||||
controllermodel = db.relationship('Model', foreign_keys=[controllermodelid], backref='equipment_controller_models')
|
controllermodel = db.relationship('Model', foreign_keys=[controllermodelid], backref='machine_controller_models')
|
||||||
|
|
||||||
__table_args__ = (
|
__table_args__ = (
|
||||||
db.Index('idx_equipment_type', 'equipmenttypeid'),
|
db.Index('idx_machine_type', 'machinetypeid'),
|
||||||
db.Index('idx_equipment_vendor', 'vendorid'),
|
db.Index('idx_machine_vendor', 'vendorid'),
|
||||||
)
|
)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<Equipment {self.assetid}>"
|
return f"<Machine {self.assetid}>"
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
"""Convert to dictionary with related names."""
|
"""Convert to dictionary with related names."""
|
||||||
result = super().to_dict()
|
result = super().to_dict()
|
||||||
|
|
||||||
# Add related object names
|
# Add related object names
|
||||||
if self.equipmenttype:
|
if self.machinetype:
|
||||||
result['equipmenttypename'] = self.equipmenttype.equipmenttype
|
result['machinetypename'] = self.machinetype.machinetype
|
||||||
if self.vendor:
|
if self.vendor:
|
||||||
result['vendorname'] = self.vendor.vendor
|
result['vendorname'] = self.vendor.vendor
|
||||||
if self.model:
|
if self.model:
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Equipment plugin main class."""
|
"""Machines plugin main class."""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
@@ -11,18 +11,18 @@ import click
|
|||||||
from shopdb.plugins.base import BasePlugin, PluginMeta
|
from shopdb.plugins.base import BasePlugin, PluginMeta
|
||||||
from shopdb.api import db, AssetType, AssetStatus
|
from shopdb.api import db, AssetType, AssetStatus
|
||||||
|
|
||||||
from .models import Equipment, EquipmentType
|
from .models import Machine, MachineType
|
||||||
from .api import equipment_bp
|
from .api import machines_bp
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class EquipmentPlugin(BasePlugin):
|
class MachinesPlugin(BasePlugin):
|
||||||
"""
|
"""
|
||||||
Equipment plugin - manages manufacturing equipment assets.
|
Machines plugin - manages manufacturing machine assets.
|
||||||
|
|
||||||
Equipment includes CNCs, CMMs, lathes, grinders, EDMs, part markers, etc.
|
Machines include CNCs, CMMs, lathes, grinders, EDMs, part markers, etc.
|
||||||
Uses the new Asset architecture with Equipment extension table.
|
Uses the new Asset architecture with Machine extension table.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -40,51 +40,51 @@ class EquipmentPlugin(BasePlugin):
|
|||||||
def meta(self) -> PluginMeta:
|
def meta(self) -> PluginMeta:
|
||||||
"""Return plugin metadata."""
|
"""Return plugin metadata."""
|
||||||
return PluginMeta(
|
return PluginMeta(
|
||||||
name=self._manifest.get('name', 'equipment'),
|
name=self._manifest.get('name', 'machines'),
|
||||||
version=self._manifest.get('version', '1.0.0'),
|
version=self._manifest.get('version', '1.0.0'),
|
||||||
description=self._manifest.get(
|
description=self._manifest.get(
|
||||||
'description',
|
'description',
|
||||||
'Equipment management for manufacturing assets'
|
'Machine management for manufacturing assets'
|
||||||
),
|
),
|
||||||
author=self._manifest.get('author', 'ShopDB Team'),
|
author=self._manifest.get('author', 'ShopDB Team'),
|
||||||
dependencies=self._manifest.get('dependencies', []),
|
dependencies=self._manifest.get('dependencies', []),
|
||||||
core_version=self._manifest.get('core_version', '>=1.0.0'),
|
core_version=self._manifest.get('core_version', '>=1.0.0'),
|
||||||
api_prefix=self._manifest.get('api_prefix', '/api/equipment'),
|
api_prefix=self._manifest.get('api_prefix', '/api/machines'),
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_blueprint(self) -> Optional[Blueprint]:
|
def get_blueprint(self) -> Optional[Blueprint]:
|
||||||
"""Return Flask Blueprint with API routes."""
|
"""Return Flask Blueprint with API routes."""
|
||||||
return equipment_bp
|
return machines_bp
|
||||||
|
|
||||||
def get_models(self) -> List[Type]:
|
def get_models(self) -> List[Type]:
|
||||||
"""Return list of SQLAlchemy model classes."""
|
"""Return list of SQLAlchemy model classes."""
|
||||||
return [Equipment, EquipmentType]
|
return [Machine, MachineType]
|
||||||
|
|
||||||
def init_app(self, app: Flask, db_instance) -> None:
|
def init_app(self, app: Flask, db_instance) -> None:
|
||||||
"""Initialize plugin with Flask app."""
|
"""Initialize plugin with Flask app."""
|
||||||
logger.info(f"Equipment plugin initialized (v{self.meta.version})")
|
logger.info(f"Machines plugin initialized (v{self.meta.version})")
|
||||||
|
|
||||||
def on_install(self, app: Flask) -> None:
|
def on_install(self, app: Flask) -> None:
|
||||||
"""Called when plugin is installed."""
|
"""Called when plugin is installed."""
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
self._ensure_asset_type()
|
self._ensure_asset_type()
|
||||||
self._ensure_asset_statuses()
|
self._ensure_asset_statuses()
|
||||||
self._ensure_equipment_types()
|
self._ensure_machine_types()
|
||||||
logger.info("Equipment plugin installed")
|
logger.info("Machines plugin installed")
|
||||||
|
|
||||||
def _ensure_asset_type(self) -> None:
|
def _ensure_asset_type(self) -> None:
|
||||||
"""Ensure equipment asset type exists."""
|
"""Ensure machine asset type exists."""
|
||||||
existing = AssetType.query.filter_by(assettype='equipment').first()
|
existing = AssetType.query.filter_by(assettype='machine').first()
|
||||||
if not existing:
|
if not existing:
|
||||||
at = AssetType(
|
at = AssetType(
|
||||||
assettype='equipment',
|
assettype='machine',
|
||||||
pluginname='equipment',
|
pluginname='machines',
|
||||||
tablename='equipment',
|
tablename='machines',
|
||||||
description='Manufacturing equipment (CNCs, CMMs, lathes, etc.)',
|
description='Manufacturing machines (CNCs, CMMs, lathes, etc.)',
|
||||||
icon='cog'
|
icon='cog'
|
||||||
)
|
)
|
||||||
db.session.add(at)
|
db.session.add(at)
|
||||||
logger.debug("Created asset type: equipment")
|
logger.debug("Created asset type: machine")
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
def _ensure_asset_statuses(self) -> None:
|
def _ensure_asset_statuses(self) -> None:
|
||||||
@@ -110,82 +110,82 @@ class EquipmentPlugin(BasePlugin):
|
|||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
def _ensure_equipment_types(self) -> None:
|
def _ensure_machine_types(self) -> None:
|
||||||
"""Ensure basic equipment types exist."""
|
"""Ensure basic machine types exist."""
|
||||||
equipment_types = [
|
machine_types = [
|
||||||
('CNC', 'Computer Numerical Control machine', 'cnc'),
|
('CNC', 'Computer Numerical Control machine', 'cnc'),
|
||||||
('CMM', 'Coordinate Measuring Machine', 'cmm'),
|
('CMM', 'Coordinate Measuring Machine', 'cmm'),
|
||||||
('Lathe', 'Lathe machine', 'lathe'),
|
('Lathe', 'Lathe machine', 'lathe'),
|
||||||
('Grinder', 'Grinding machine', 'grinder'),
|
('Grinder', 'Grinding machine', 'grinder'),
|
||||||
('EDM', 'Electrical Discharge Machine', 'edm'),
|
('EDM', 'Electrical Discharge Machine', 'edm'),
|
||||||
('Part Marker', 'Part marking/engraving equipment', 'marker'),
|
('Part Marker', 'Part marking/engraving machine', 'marker'),
|
||||||
('Mill', 'Milling machine', 'mill'),
|
('Mill', 'Milling machine', 'mill'),
|
||||||
('Press', 'Press machine', 'press'),
|
('Press', 'Press machine', 'press'),
|
||||||
('Robot', 'Industrial robot', 'robot'),
|
('Robot', 'Industrial robot', 'robot'),
|
||||||
('Other', 'Other equipment type', 'cog'),
|
('Other', 'Other machine type', 'cog'),
|
||||||
]
|
]
|
||||||
|
|
||||||
for name, description, icon in equipment_types:
|
for name, description, icon in machine_types:
|
||||||
existing = EquipmentType.query.filter_by(equipmenttype=name).first()
|
existing = MachineType.query.filter_by(machinetype=name).first()
|
||||||
if not existing:
|
if not existing:
|
||||||
et = EquipmentType(
|
mt = MachineType(
|
||||||
equipmenttype=name,
|
machinetype=name,
|
||||||
description=description,
|
description=description,
|
||||||
icon=icon
|
icon=icon
|
||||||
)
|
)
|
||||||
db.session.add(et)
|
db.session.add(mt)
|
||||||
logger.debug(f"Created equipment type: {name}")
|
logger.debug(f"Created machine type: {name}")
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
def on_uninstall(self, app: Flask) -> None:
|
def on_uninstall(self, app: Flask) -> None:
|
||||||
"""Called when plugin is uninstalled."""
|
"""Called when plugin is uninstalled."""
|
||||||
logger.info("Equipment plugin uninstalled")
|
logger.info("Machines plugin uninstalled")
|
||||||
|
|
||||||
def get_cli_commands(self) -> List:
|
def get_cli_commands(self) -> List:
|
||||||
"""Return CLI commands for this plugin."""
|
"""Return CLI commands for this plugin."""
|
||||||
|
|
||||||
@click.group('equipment')
|
@click.group('machines')
|
||||||
def equipmentcli():
|
def machinescli():
|
||||||
"""Equipment plugin commands."""
|
"""Machines plugin commands."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@equipmentcli.command('list-types')
|
@machinescli.command('list-types')
|
||||||
def list_types():
|
def list_types():
|
||||||
"""List all equipment types."""
|
"""List all machine types."""
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
|
|
||||||
with current_app.app_context():
|
with current_app.app_context():
|
||||||
types = EquipmentType.query.filter_by(isactive=True).all()
|
types = MachineType.query.filter_by(isactive=True).all()
|
||||||
if not types:
|
if not types:
|
||||||
click.echo('No equipment types found.')
|
click.echo('No machine types found.')
|
||||||
return
|
return
|
||||||
|
|
||||||
click.echo('Equipment Types:')
|
click.echo('Machine Types:')
|
||||||
for t in types:
|
for t in types:
|
||||||
click.echo(f" [{t.equipmenttypeid}] {t.equipmenttype}")
|
click.echo(f" [{t.machinetypeid}] {t.machinetype}")
|
||||||
|
|
||||||
@equipmentcli.command('stats')
|
@machinescli.command('stats')
|
||||||
def stats():
|
def stats():
|
||||||
"""Show equipment statistics."""
|
"""Show machine statistics."""
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
from shopdb.api import Asset
|
from shopdb.api import Asset
|
||||||
|
|
||||||
with current_app.app_context():
|
with current_app.app_context():
|
||||||
total = db.session.query(Equipment).join(Asset).filter(
|
total = db.session.query(Machine).join(Asset).filter(
|
||||||
Asset.isactive == True
|
Asset.isactive == True
|
||||||
).count()
|
).count()
|
||||||
|
|
||||||
click.echo(f"Total active equipment: {total}")
|
click.echo(f"Total active machines: {total}")
|
||||||
|
|
||||||
# By type
|
# By type
|
||||||
by_type = db.session.query(
|
by_type = db.session.query(
|
||||||
EquipmentType.equipmenttype,
|
MachineType.machinetype,
|
||||||
db.func.count(Equipment.equipmentid)
|
db.func.count(Machine.machineid)
|
||||||
).join(Equipment, Equipment.equipmenttypeid == EquipmentType.equipmenttypeid
|
).join(Machine, Machine.machinetypeid == MachineType.machinetypeid
|
||||||
).join(Asset, Asset.assetid == Equipment.assetid
|
).join(Asset, Asset.assetid == Machine.assetid
|
||||||
).filter(Asset.isactive == True
|
).filter(Asset.isactive == True
|
||||||
).group_by(EquipmentType.equipmenttype
|
).group_by(MachineType.machinetype
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
if by_type:
|
if by_type:
|
||||||
@@ -193,15 +193,15 @@ class EquipmentPlugin(BasePlugin):
|
|||||||
for t, c in by_type:
|
for t, c in by_type:
|
||||||
click.echo(f" {t}: {c}")
|
click.echo(f" {t}: {c}")
|
||||||
|
|
||||||
return [equipmentcli]
|
return [machinescli]
|
||||||
|
|
||||||
def get_dashboard_widgets(self) -> List[Dict]:
|
def get_dashboard_widgets(self) -> List[Dict]:
|
||||||
"""Return dashboard widget definitions."""
|
"""Return dashboard widget definitions."""
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
'name': 'Machine Status',
|
'name': 'Machine Status',
|
||||||
'component': 'EquipmentStatusWidget',
|
'component': 'MachineStatusWidget',
|
||||||
'endpoint': '/api/equipment/dashboard/summary',
|
'endpoint': '/api/machines/dashboard/summary',
|
||||||
'size': 'medium',
|
'size': 'medium',
|
||||||
'position': 5,
|
'position': 5,
|
||||||
},
|
},
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
Measuring tools are gage-lab instruments (calipers, micrometers, thread gages,
|
Measuring tools are gage-lab instruments (calipers, micrometers, thread gages,
|
||||||
bore gages, height gages, Genspect heads, ...) that measure parts, as opposed
|
bore gages, height gages, Genspect heads, ...) that measure parts, as opposed
|
||||||
to equipment that makes parts (see ADR-005). Each tool is a core Asset plus a
|
to machines that make parts (see ADR-005). Each tool is a core Asset plus a
|
||||||
one-to-one measuringtools extension row.
|
one-to-one measuringtools extension row.
|
||||||
|
|
||||||
The lifecycle a measuring tool cares about is CALIBRATION, not maintenance:
|
The lifecycle a measuring tool cares about is CALIBRATION, not maintenance:
|
||||||
@@ -50,7 +50,7 @@ class MeasuringToolType(BaseModel):
|
|||||||
"""Measuring-tool classification (Caliper, Micrometer, Thread Gage, ...).
|
"""Measuring-tool classification (Caliper, Micrometer, Thread Gage, ...).
|
||||||
|
|
||||||
Site-managed lookup with a display color for badges and map markers,
|
Site-managed lookup with a display color for badges and map markers,
|
||||||
the same shape as the equipment/computer/printer type tables.
|
the same shape as the machine/computer/printer type tables.
|
||||||
"""
|
"""
|
||||||
__tablename__ = 'measuringtooltypes'
|
__tablename__ = 'measuringtooltypes'
|
||||||
|
|
||||||
|
|||||||
@@ -317,7 +317,7 @@ def seedsupplies():
|
|||||||
targets = _matching_models(family['matchkeys'], vendor.vendorid)
|
targets = _matching_models(family['matchkeys'], vendor.vendorid)
|
||||||
|
|
||||||
if not targets:
|
if not targets:
|
||||||
# machinetypeid is a legacy Model column (nullable); printers are
|
# modeltypeid is a legacy Model column (nullable); printers are
|
||||||
# asset-based now and carry their type via PrinterType, not here.
|
# asset-based now and carry their type via PrinterType, not here.
|
||||||
model = Model(
|
model = Model(
|
||||||
modelnumber=family['canonical'],
|
modelnumber=family['canonical'],
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
Asset-general plugin: owns its warranties + warrantyassets tables, an API
|
Asset-general plugin: owns its warranties + warrantyassets tables, an API
|
||||||
surface, and a sidebar entry. Not tied to any one asset type - a warranty can
|
surface, and a sidebar entry. Not tied to any one asset type - a warranty can
|
||||||
cover a PC, printer, network device, or equipment.
|
cover a PC, printer, network device, or machine.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ def create_app(config_name: str = None) -> Flask:
|
|||||||
CORE_BLUEPRINT_NAMES = (
|
CORE_BLUEPRINT_NAMES = (
|
||||||
'auth',
|
'auth',
|
||||||
'assets',
|
'assets',
|
||||||
'machinetypes',
|
'modeltypes',
|
||||||
'plugins',
|
'plugins',
|
||||||
'vendors',
|
'vendors',
|
||||||
'models',
|
'models',
|
||||||
|
|||||||
@@ -125,31 +125,31 @@ def seed_cli():
|
|||||||
@seed_cli.command('reference-data')
|
@seed_cli.command('reference-data')
|
||||||
@with_appcontext
|
@with_appcontext
|
||||||
def seed_reference_data():
|
def seed_reference_data():
|
||||||
"""Seed reference data (machine types, statuses, etc.)."""
|
"""Seed reference data (model types, statuses, etc.)."""
|
||||||
from shopdb.extensions import db
|
from shopdb.extensions import db
|
||||||
from shopdb.core.models import MachineType, OperatingSystem, AssetStatus, LocationType
|
from shopdb.core.models import ModelType, OperatingSystem, AssetStatus, LocationType
|
||||||
from shopdb.core.models.relationship import RelationshipType
|
from shopdb.core.models.relationship import RelationshipType
|
||||||
|
|
||||||
# Machine types
|
# Model types (type the vendor models catalog)
|
||||||
machine_types = [
|
model_types = [
|
||||||
{'machinetype': 'CNC Mill', 'category': 'Equipment', 'description': 'CNC Milling Machine'},
|
{'modeltype': 'CNC Mill', 'category': 'Equipment', 'description': 'CNC Milling Machine'},
|
||||||
{'machinetype': 'CNC Lathe', 'category': 'Equipment', 'description': 'CNC Lathe'},
|
{'modeltype': 'CNC Lathe', 'category': 'Equipment', 'description': 'CNC Lathe'},
|
||||||
{'machinetype': 'CMM', 'category': 'Equipment', 'description': 'Coordinate Measuring Machine'},
|
{'modeltype': 'CMM', 'category': 'Equipment', 'description': 'Coordinate Measuring Machine'},
|
||||||
{'machinetype': 'EDM', 'category': 'Equipment', 'description': 'Electrical Discharge Machine'},
|
{'modeltype': 'EDM', 'category': 'Equipment', 'description': 'Electrical Discharge Machine'},
|
||||||
{'machinetype': 'Grinder', 'category': 'Equipment', 'description': 'Grinding Machine'},
|
{'modeltype': 'Grinder', 'category': 'Equipment', 'description': 'Grinding Machine'},
|
||||||
{'machinetype': 'Inspection Station', 'category': 'Equipment', 'description': 'Inspection Station'},
|
{'modeltype': 'Inspection Station', 'category': 'Equipment', 'description': 'Inspection Station'},
|
||||||
{'machinetype': 'Desktop PC', 'category': 'PC', 'description': 'Desktop Computer'},
|
{'modeltype': 'Desktop PC', 'category': 'PC', 'description': 'Desktop Computer'},
|
||||||
{'machinetype': 'Laptop', 'category': 'PC', 'description': 'Laptop Computer'},
|
{'modeltype': 'Laptop', 'category': 'PC', 'description': 'Laptop Computer'},
|
||||||
{'machinetype': 'Shopfloor PC', 'category': 'PC', 'description': 'Shopfloor Computer'},
|
{'modeltype': 'Shopfloor PC', 'category': 'PC', 'description': 'Shopfloor Computer'},
|
||||||
{'machinetype': 'Server', 'category': 'Network', 'description': 'Server'},
|
{'modeltype': 'Server', 'category': 'Network', 'description': 'Server'},
|
||||||
{'machinetype': 'Switch', 'category': 'Network', 'description': 'Network Switch'},
|
{'modeltype': 'Switch', 'category': 'Network', 'description': 'Network Switch'},
|
||||||
{'machinetype': 'Access Point', 'category': 'Network', 'description': 'Wireless Access Point'},
|
{'modeltype': 'Access Point', 'category': 'Network', 'description': 'Wireless Access Point'},
|
||||||
]
|
]
|
||||||
|
|
||||||
for mt_data in machine_types:
|
for mt_data in model_types:
|
||||||
existing = MachineType.query.filter_by(machinetype=mt_data['machinetype']).first()
|
existing = ModelType.query.filter_by(modeltype=mt_data['modeltype']).first()
|
||||||
if not existing:
|
if not existing:
|
||||||
mt = MachineType(**mt_data)
|
mt = ModelType(**mt_data)
|
||||||
db.session.add(mt)
|
db.session.add(mt)
|
||||||
|
|
||||||
# Asset statuses (canonical set - the asset model is the contract)
|
# Asset statuses (canonical set - the asset model is the contract)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from .auth import auth_bp
|
from .auth import auth_bp
|
||||||
from .assets import assets_bp
|
from .assets import assets_bp
|
||||||
from .machinetypes import machinetypes_bp
|
from .modeltypes import modeltypes_bp
|
||||||
from .plugins import plugins_bp
|
from .plugins import plugins_bp
|
||||||
from .vendors import vendors_bp
|
from .vendors import vendors_bp
|
||||||
from .models import models_bp
|
from .models import models_bp
|
||||||
@@ -24,7 +24,7 @@ from .setup import setup_bp
|
|||||||
__all__ = [
|
__all__ = [
|
||||||
'auth_bp',
|
'auth_bp',
|
||||||
'assets_bp',
|
'assets_bp',
|
||||||
'machinetypes_bp',
|
'modeltypes_bp',
|
||||||
'plugins_bp',
|
'plugins_bp',
|
||||||
'vendors_bp',
|
'vendors_bp',
|
||||||
'models_bp',
|
'models_bp',
|
||||||
|
|||||||
@@ -332,7 +332,7 @@ def list_assets():
|
|||||||
- per_page: Items per page (default: 20, max: 100)
|
- per_page: Items per page (default: 20, max: 100)
|
||||||
- active: Filter by active status (default: true)
|
- active: Filter by active status (default: true)
|
||||||
- search: Search by assetnumber or name
|
- search: Search by assetnumber or name
|
||||||
- type: Filter by asset type name (e.g., 'equipment', 'computer')
|
- type: Filter by asset type name (e.g., 'machine', 'computer')
|
||||||
- type_id: Filter by asset type ID
|
- type_id: Filter by asset type ID
|
||||||
- status_id: Filter by status ID
|
- status_id: Filter by status ID
|
||||||
- location_id: Filter by location ID
|
- location_id: Filter by location ID
|
||||||
@@ -699,8 +699,8 @@ def get_assets_map():
|
|||||||
Returns assets with mapx/mapy coordinates, joined with type-specific data.
|
Returns assets with mapx/mapy coordinates, joined with type-specific data.
|
||||||
|
|
||||||
Query parameters:
|
Query parameters:
|
||||||
- assettype: Filter by asset type name (equipment, computer, network_device, printer)
|
- assettype: Filter by asset type name (machine, computer, network_device, printer)
|
||||||
- subtype: Filter by subtype ID (machinetype for equipment/computer, networkdevicetype for network, printertype for printer)
|
- subtype: Filter by subtype ID (machinetype for machines, computertype for PCs, networkdevicetype for network, printertype for printer)
|
||||||
- businessunitid: Filter by business unit ID
|
- businessunitid: Filter by business unit ID
|
||||||
- statusid: Filter by status ID
|
- statusid: Filter by status ID
|
||||||
- locationid: Filter by location ID
|
- locationid: Filter by location ID
|
||||||
@@ -720,26 +720,26 @@ def get_assets_map():
|
|||||||
|
|
||||||
# Eager-load plugin extension tables AND their relationships
|
# Eager-load plugin extension tables AND their relationships
|
||||||
try:
|
try:
|
||||||
from plugins.equipment.models import Equipment
|
from plugins.machines.models import Machine
|
||||||
eager_options.append(
|
eager_options.append(
|
||||||
subqueryload(Asset.equipment)
|
subqueryload(Asset.machine)
|
||||||
.joinedload(Equipment.equipmenttype)
|
.joinedload(Machine.machinetype)
|
||||||
)
|
)
|
||||||
eager_options.append(
|
eager_options.append(
|
||||||
subqueryload(Asset.equipment)
|
subqueryload(Asset.machine)
|
||||||
.joinedload(Equipment.vendor)
|
.joinedload(Machine.vendor)
|
||||||
)
|
)
|
||||||
eager_options.append(
|
eager_options.append(
|
||||||
subqueryload(Asset.equipment)
|
subqueryload(Asset.machine)
|
||||||
.joinedload(Equipment.model)
|
.joinedload(Machine.model)
|
||||||
)
|
)
|
||||||
eager_options.append(
|
eager_options.append(
|
||||||
subqueryload(Asset.equipment)
|
subqueryload(Asset.machine)
|
||||||
.joinedload(Equipment.controllervendor)
|
.joinedload(Machine.controllervendor)
|
||||||
)
|
)
|
||||||
eager_options.append(
|
eager_options.append(
|
||||||
subqueryload(Asset.equipment)
|
subqueryload(Asset.machine)
|
||||||
.joinedload(Equipment.controllermodel)
|
.joinedload(Machine.controllermodel)
|
||||||
)
|
)
|
||||||
except (ImportError, AttributeError):
|
except (ImportError, AttributeError):
|
||||||
pass
|
pass
|
||||||
@@ -800,11 +800,11 @@ def get_assets_map():
|
|||||||
if subtype_id := request.args.get('subtype'):
|
if subtype_id := request.args.get('subtype'):
|
||||||
subtype_id = int(subtype_id)
|
subtype_id = int(subtype_id)
|
||||||
asset_type_lower = selected_assettype.lower() if selected_assettype else ''
|
asset_type_lower = selected_assettype.lower() if selected_assettype else ''
|
||||||
if asset_type_lower == 'equipment':
|
if asset_type_lower == 'machine':
|
||||||
try:
|
try:
|
||||||
from plugins.equipment.models import Equipment
|
from plugins.machines.models import Machine
|
||||||
query = query.join(Equipment, Equipment.assetid == Asset.assetid).filter(
|
query = query.join(Machine, Machine.assetid == Asset.assetid).filter(
|
||||||
Equipment.equipmenttypeid == subtype_id
|
Machine.machinetypeid == subtype_id
|
||||||
)
|
)
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
@@ -926,11 +926,11 @@ def get_assets_map():
|
|||||||
subtypes = {}
|
subtypes = {}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from plugins.equipment.models import EquipmentType
|
from plugins.machines.models import MachineType
|
||||||
equipment_types = EquipmentType.query.filter(EquipmentType.isactive == True).order_by(EquipmentType.equipmenttype).all()
|
machine_types = MachineType.query.filter(MachineType.isactive == True).order_by(MachineType.machinetype).all()
|
||||||
subtypes['Equipment'] = [{'id': et.equipmenttypeid, 'name': et.equipmenttype, 'color': et.color} for et in equipment_types]
|
subtypes['Machine'] = [{'id': mt.machinetypeid, 'name': mt.machinetype, 'color': mt.color} for mt in machine_types]
|
||||||
except ImportError:
|
except ImportError:
|
||||||
subtypes['Equipment'] = []
|
subtypes['Machine'] = []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from plugins.computers.models import ComputerType
|
from plugins.computers.models import ComputerType
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ dashboard_bp = Blueprint('dashboard', __name__)
|
|||||||
|
|
||||||
# Map asset type name -> dashboard category label
|
# Map asset type name -> dashboard category label
|
||||||
_TYPE_CATEGORY = {
|
_TYPE_CATEGORY = {
|
||||||
'equipment': 'Equipment',
|
'machine': 'Machine',
|
||||||
'computer': 'PC',
|
'computer': 'PC',
|
||||||
'printer': 'Printer',
|
'printer': 'Printer',
|
||||||
'network_device': 'Network',
|
'network_device': 'Network',
|
||||||
@@ -30,11 +30,11 @@ def _count_by_type(assettype):
|
|||||||
@jwt_required(optional=True)
|
@jwt_required(optional=True)
|
||||||
def get_dashboard():
|
def get_dashboard():
|
||||||
"""Get dashboard summary data (asset-based)."""
|
"""Get dashboard summary data (asset-based)."""
|
||||||
equipment_count = _count_by_type('equipment')
|
machine_count = _count_by_type('machine')
|
||||||
pc_count = _count_by_type('computer')
|
pc_count = _count_by_type('computer')
|
||||||
network_count = _count_by_type('network_device')
|
network_count = _count_by_type('network_device')
|
||||||
printer_count = _count_by_type('printer')
|
printer_count = _count_by_type('printer')
|
||||||
total = equipment_count + pc_count + network_count + printer_count
|
total = machine_count + pc_count + network_count + printer_count
|
||||||
|
|
||||||
# Count by status
|
# Count by status
|
||||||
status_counts = db.session.query(
|
status_counts = db.session.query(
|
||||||
@@ -52,17 +52,18 @@ def get_dashboard():
|
|||||||
).limit(10).all()
|
).limit(10).all()
|
||||||
|
|
||||||
return success_response({
|
return success_response({
|
||||||
# Fields expected by frontend
|
# Fields expected by frontend (totalmachines now means machines,
|
||||||
'totalmachines': total,
|
# totalassets is the grand total - renamed with the machines plugin)
|
||||||
'totalequipment': equipment_count,
|
'totalassets': total,
|
||||||
|
'totalmachines': machine_count,
|
||||||
'totalpc': pc_count,
|
'totalpc': pc_count,
|
||||||
'totalnetwork': network_count,
|
'totalnetwork': network_count,
|
||||||
'totalprinter': printer_count,
|
'totalprinter': printer_count,
|
||||||
'activemachines': status_dict.get('In Use', 0),
|
'activeassets': status_dict.get('In Use', 0),
|
||||||
'inrepair': status_dict.get('In Repair', 0),
|
'inrepair': status_dict.get('In Repair', 0),
|
||||||
# Structured data
|
# Structured data
|
||||||
'counts': {
|
'counts': {
|
||||||
'equipment': equipment_count,
|
'machines': machine_count,
|
||||||
'pcs': pc_count,
|
'pcs': pc_count,
|
||||||
'networkdevices': network_count,
|
'networkdevices': network_count,
|
||||||
'printers': printer_count,
|
'printers': printer_count,
|
||||||
|
|||||||
@@ -1,153 +0,0 @@
|
|||||||
"""Machine Types API endpoints - Full CRUD."""
|
|
||||||
|
|
||||||
from flask import Blueprint, request
|
|
||||||
from flask_jwt_extended import jwt_required, current_user
|
|
||||||
|
|
||||||
from shopdb.extensions import db
|
|
||||||
from shopdb.core.models import MachineType
|
|
||||||
from shopdb.utils.responses import (
|
|
||||||
success_response,
|
|
||||||
error_response,
|
|
||||||
paginated_response,
|
|
||||||
ErrorCodes
|
|
||||||
)
|
|
||||||
from shopdb.utils.pagination import get_pagination_params, paginate_query
|
|
||||||
|
|
||||||
from shopdb.utils.authz import require_permission, require_role
|
|
||||||
|
|
||||||
machinetypes_bp = Blueprint('machinetypes', __name__)
|
|
||||||
|
|
||||||
|
|
||||||
@machinetypes_bp.route('', methods=['GET'])
|
|
||||||
@jwt_required(optional=True)
|
|
||||||
def list_machinetypes():
|
|
||||||
"""List all machine types with optional filtering."""
|
|
||||||
page, per_page = get_pagination_params(request)
|
|
||||||
|
|
||||||
query = MachineType.query
|
|
||||||
|
|
||||||
if request.args.get('active', 'true').lower() != 'false':
|
|
||||||
query = query.filter(MachineType.isactive == True)
|
|
||||||
|
|
||||||
if category := request.args.get('category'):
|
|
||||||
query = query.filter(MachineType.category == category)
|
|
||||||
|
|
||||||
if search := request.args.get('search'):
|
|
||||||
query = query.filter(MachineType.machinetype.ilike(f'%{search}%'))
|
|
||||||
|
|
||||||
query = query.order_by(MachineType.machinetype)
|
|
||||||
|
|
||||||
items, total = paginate_query(query, page, per_page)
|
|
||||||
data = [mt.to_dict() for mt in items]
|
|
||||||
|
|
||||||
return paginated_response(data, page, per_page, total)
|
|
||||||
|
|
||||||
|
|
||||||
@machinetypes_bp.route('/<int:type_id>', methods=['GET'])
|
|
||||||
@jwt_required(optional=True)
|
|
||||||
def get_machinetype(type_id: int):
|
|
||||||
"""Get a single machine type."""
|
|
||||||
mt = db.session.get(MachineType, type_id)
|
|
||||||
|
|
||||||
if not mt:
|
|
||||||
return error_response(
|
|
||||||
ErrorCodes.NOT_FOUND,
|
|
||||||
f'Machine type with ID {type_id} not found',
|
|
||||||
http_code=404
|
|
||||||
)
|
|
||||||
|
|
||||||
return success_response(mt.to_dict())
|
|
||||||
|
|
||||||
|
|
||||||
@machinetypes_bp.route('', methods=['POST'])
|
|
||||||
@jwt_required()
|
|
||||||
@require_role('admin')
|
|
||||||
def create_machinetype():
|
|
||||||
"""Create a new machine type."""
|
|
||||||
data = request.get_json()
|
|
||||||
|
|
||||||
if not data or not data.get('machinetype'):
|
|
||||||
return error_response(ErrorCodes.VALIDATION_ERROR, 'machinetype is required')
|
|
||||||
|
|
||||||
if MachineType.query.filter_by(machinetype=data['machinetype']).first():
|
|
||||||
return error_response(
|
|
||||||
ErrorCodes.CONFLICT,
|
|
||||||
f"Machine type '{data['machinetype']}' already exists",
|
|
||||||
http_code=409
|
|
||||||
)
|
|
||||||
|
|
||||||
mt = MachineType(
|
|
||||||
machinetype=data['machinetype'],
|
|
||||||
category=data.get('category', 'Equipment'),
|
|
||||||
description=data.get('description'),
|
|
||||||
icon=data.get('icon')
|
|
||||||
)
|
|
||||||
|
|
||||||
db.session.add(mt)
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
return success_response(mt.to_dict(), message='Machine type created', http_code=201)
|
|
||||||
|
|
||||||
|
|
||||||
@machinetypes_bp.route('/<int:type_id>', methods=['PUT'])
|
|
||||||
@jwt_required()
|
|
||||||
@require_role('admin')
|
|
||||||
def update_machinetype(type_id: int):
|
|
||||||
"""Update a machine type."""
|
|
||||||
mt = db.session.get(MachineType, type_id)
|
|
||||||
|
|
||||||
if not mt:
|
|
||||||
return error_response(
|
|
||||||
ErrorCodes.NOT_FOUND,
|
|
||||||
f'Machine type with ID {type_id} not found',
|
|
||||||
http_code=404
|
|
||||||
)
|
|
||||||
|
|
||||||
data = request.get_json()
|
|
||||||
if not data:
|
|
||||||
return error_response(ErrorCodes.VALIDATION_ERROR, 'No data provided')
|
|
||||||
|
|
||||||
# Check duplicate name
|
|
||||||
if 'machinetype' in data and data['machinetype'] != mt.machinetype:
|
|
||||||
if MachineType.query.filter_by(machinetype=data['machinetype']).first():
|
|
||||||
return error_response(
|
|
||||||
ErrorCodes.CONFLICT,
|
|
||||||
f"Machine type '{data['machinetype']}' already exists",
|
|
||||||
http_code=409
|
|
||||||
)
|
|
||||||
|
|
||||||
for key in ['machinetype', 'category', 'description', 'icon', 'isactive']:
|
|
||||||
if key in data:
|
|
||||||
setattr(mt, key, data[key])
|
|
||||||
|
|
||||||
db.session.commit()
|
|
||||||
return success_response(mt.to_dict(), message='Machine type updated')
|
|
||||||
|
|
||||||
|
|
||||||
@machinetypes_bp.route('/<int:type_id>', methods=['DELETE'])
|
|
||||||
@jwt_required()
|
|
||||||
@require_role('admin')
|
|
||||||
def delete_machinetype(type_id: int):
|
|
||||||
"""Delete (deactivate) a machine type."""
|
|
||||||
mt = db.session.get(MachineType, type_id)
|
|
||||||
|
|
||||||
if not mt:
|
|
||||||
return error_response(
|
|
||||||
ErrorCodes.NOT_FOUND,
|
|
||||||
f'Machine type with ID {type_id} not found',
|
|
||||||
http_code=404
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check if any model uses this type
|
|
||||||
from shopdb.core.models import Model
|
|
||||||
if Model.query.filter_by(machinetypeid=type_id).first():
|
|
||||||
return error_response(
|
|
||||||
ErrorCodes.CONFLICT,
|
|
||||||
'Cannot delete machine type: models are using it',
|
|
||||||
http_code=409
|
|
||||||
)
|
|
||||||
|
|
||||||
mt.isactive = False
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
return success_response(message='Machine type deleted')
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Models (equipment models) API endpoints - Full CRUD."""
|
"""Models (vendor model catalog) API endpoints - Full CRUD."""
|
||||||
|
|
||||||
from flask import Blueprint, request
|
from flask import Blueprint, request
|
||||||
from flask_jwt_extended import jwt_required
|
from flask_jwt_extended import jwt_required
|
||||||
@@ -21,7 +21,7 @@ models_bp = Blueprint('models', __name__)
|
|||||||
@models_bp.route('', methods=['GET'])
|
@models_bp.route('', methods=['GET'])
|
||||||
@jwt_required(optional=True)
|
@jwt_required(optional=True)
|
||||||
def list_models():
|
def list_models():
|
||||||
"""List all equipment models."""
|
"""List all vendor catalog models."""
|
||||||
page, per_page = get_pagination_params(request)
|
page, per_page = get_pagination_params(request)
|
||||||
|
|
||||||
query = Model.query
|
query = Model.query
|
||||||
@@ -32,8 +32,8 @@ def list_models():
|
|||||||
if vendor_id := request.args.get('vendor', type=int):
|
if vendor_id := request.args.get('vendor', type=int):
|
||||||
query = query.filter(Model.vendorid == vendor_id)
|
query = query.filter(Model.vendorid == vendor_id)
|
||||||
|
|
||||||
if machinetype_id := request.args.get('machinetype', type=int):
|
if modeltype_id := request.args.get('modeltype', type=int):
|
||||||
query = query.filter(Model.machinetypeid == machinetype_id)
|
query = query.filter(Model.modeltypeid == modeltype_id)
|
||||||
|
|
||||||
if search := request.args.get('search'):
|
if search := request.args.get('search'):
|
||||||
query = query.filter(Model.modelnumber.ilike(f'%{search}%'))
|
query = query.filter(Model.modelnumber.ilike(f'%{search}%'))
|
||||||
@@ -46,7 +46,7 @@ def list_models():
|
|||||||
for m in items:
|
for m in items:
|
||||||
d = m.to_dict()
|
d = m.to_dict()
|
||||||
d['vendor'] = m.vendor.vendor if m.vendor else None
|
d['vendor'] = m.vendor.vendor if m.vendor else None
|
||||||
d['machinetype'] = m.machinetype.machinetype if m.machinetype else None
|
d['modeltype'] = m.modeltype.modeltype if m.modeltype else None
|
||||||
data.append(d)
|
data.append(d)
|
||||||
|
|
||||||
return paginated_response(data, page, per_page, total)
|
return paginated_response(data, page, per_page, total)
|
||||||
@@ -67,7 +67,7 @@ def get_model(model_id: int):
|
|||||||
|
|
||||||
data = m.to_dict()
|
data = m.to_dict()
|
||||||
data['vendor'] = m.vendor.to_dict() if m.vendor else None
|
data['vendor'] = m.vendor.to_dict() if m.vendor else None
|
||||||
data['machinetype'] = m.machinetype.to_dict() if m.machinetype else None
|
data['modeltype'] = m.modeltype.to_dict() if m.modeltype else None
|
||||||
|
|
||||||
return success_response(data)
|
return success_response(data)
|
||||||
|
|
||||||
@@ -97,7 +97,7 @@ def create_model():
|
|||||||
m = Model(
|
m = Model(
|
||||||
modelnumber=data['modelnumber'],
|
modelnumber=data['modelnumber'],
|
||||||
vendorid=data.get('vendorid'),
|
vendorid=data.get('vendorid'),
|
||||||
machinetypeid=data.get('machinetypeid'),
|
modeltypeid=data.get('modeltypeid'),
|
||||||
description=data.get('description'),
|
description=data.get('description'),
|
||||||
imageurl=data.get('imageurl'),
|
imageurl=data.get('imageurl'),
|
||||||
documentationurl=data.get('documentationurl'),
|
documentationurl=data.get('documentationurl'),
|
||||||
@@ -128,7 +128,7 @@ def update_model(model_id: int):
|
|||||||
if not data:
|
if not data:
|
||||||
return error_response(ErrorCodes.VALIDATION_ERROR, 'No data provided')
|
return error_response(ErrorCodes.VALIDATION_ERROR, 'No data provided')
|
||||||
|
|
||||||
for key in ['modelnumber', 'vendorid', 'machinetypeid', 'description', 'imageurl', 'documentationurl', 'notes', 'isactive']:
|
for key in ['modelnumber', 'vendorid', 'modeltypeid', 'description', 'imageurl', 'documentationurl', 'notes', 'isactive']:
|
||||||
if key in data:
|
if key in data:
|
||||||
setattr(m, key, data[key])
|
setattr(m, key, data[key])
|
||||||
|
|
||||||
|
|||||||
157
shopdb/core/api/modeltypes.py
Normal file
157
shopdb/core/api/modeltypes.py
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
"""Model Types API endpoints - Full CRUD.
|
||||||
|
|
||||||
|
Types the vendor MODELS catalog (models.modeltypeid). Renamed from
|
||||||
|
machinetypes; the machines plugin now owns the "machinetypes" name.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from flask import Blueprint, request
|
||||||
|
from flask_jwt_extended import jwt_required, current_user
|
||||||
|
|
||||||
|
from shopdb.extensions import db
|
||||||
|
from shopdb.core.models import ModelType
|
||||||
|
from shopdb.utils.responses import (
|
||||||
|
success_response,
|
||||||
|
error_response,
|
||||||
|
paginated_response,
|
||||||
|
ErrorCodes
|
||||||
|
)
|
||||||
|
from shopdb.utils.pagination import get_pagination_params, paginate_query
|
||||||
|
|
||||||
|
from shopdb.utils.authz import require_permission, require_role
|
||||||
|
|
||||||
|
modeltypes_bp = Blueprint('modeltypes', __name__)
|
||||||
|
|
||||||
|
|
||||||
|
@modeltypes_bp.route('', methods=['GET'])
|
||||||
|
@jwt_required(optional=True)
|
||||||
|
def list_modeltypes():
|
||||||
|
"""List all model types with optional filtering."""
|
||||||
|
page, per_page = get_pagination_params(request)
|
||||||
|
|
||||||
|
query = ModelType.query
|
||||||
|
|
||||||
|
if request.args.get('active', 'true').lower() != 'false':
|
||||||
|
query = query.filter(ModelType.isactive == True)
|
||||||
|
|
||||||
|
if category := request.args.get('category'):
|
||||||
|
query = query.filter(ModelType.category == category)
|
||||||
|
|
||||||
|
if search := request.args.get('search'):
|
||||||
|
query = query.filter(ModelType.modeltype.ilike(f'%{search}%'))
|
||||||
|
|
||||||
|
query = query.order_by(ModelType.modeltype)
|
||||||
|
|
||||||
|
items, total = paginate_query(query, page, per_page)
|
||||||
|
data = [mt.to_dict() for mt in items]
|
||||||
|
|
||||||
|
return paginated_response(data, page, per_page, total)
|
||||||
|
|
||||||
|
|
||||||
|
@modeltypes_bp.route('/<int:type_id>', methods=['GET'])
|
||||||
|
@jwt_required(optional=True)
|
||||||
|
def get_modeltype(type_id: int):
|
||||||
|
"""Get a single model type."""
|
||||||
|
mt = db.session.get(ModelType, type_id)
|
||||||
|
|
||||||
|
if not mt:
|
||||||
|
return error_response(
|
||||||
|
ErrorCodes.NOT_FOUND,
|
||||||
|
f'Model type with ID {type_id} not found',
|
||||||
|
http_code=404
|
||||||
|
)
|
||||||
|
|
||||||
|
return success_response(mt.to_dict())
|
||||||
|
|
||||||
|
|
||||||
|
@modeltypes_bp.route('', methods=['POST'])
|
||||||
|
@jwt_required()
|
||||||
|
@require_role('admin')
|
||||||
|
def create_modeltype():
|
||||||
|
"""Create a new model type."""
|
||||||
|
data = request.get_json()
|
||||||
|
|
||||||
|
if not data or not data.get('modeltype'):
|
||||||
|
return error_response(ErrorCodes.VALIDATION_ERROR, 'modeltype is required')
|
||||||
|
|
||||||
|
if ModelType.query.filter_by(modeltype=data['modeltype']).first():
|
||||||
|
return error_response(
|
||||||
|
ErrorCodes.CONFLICT,
|
||||||
|
f"Model type '{data['modeltype']}' already exists",
|
||||||
|
http_code=409
|
||||||
|
)
|
||||||
|
|
||||||
|
mt = ModelType(
|
||||||
|
modeltype=data['modeltype'],
|
||||||
|
category=data.get('category', 'Equipment'),
|
||||||
|
description=data.get('description'),
|
||||||
|
icon=data.get('icon')
|
||||||
|
)
|
||||||
|
|
||||||
|
db.session.add(mt)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return success_response(mt.to_dict(), message='Model type created', http_code=201)
|
||||||
|
|
||||||
|
|
||||||
|
@modeltypes_bp.route('/<int:type_id>', methods=['PUT'])
|
||||||
|
@jwt_required()
|
||||||
|
@require_role('admin')
|
||||||
|
def update_modeltype(type_id: int):
|
||||||
|
"""Update a model type."""
|
||||||
|
mt = db.session.get(ModelType, type_id)
|
||||||
|
|
||||||
|
if not mt:
|
||||||
|
return error_response(
|
||||||
|
ErrorCodes.NOT_FOUND,
|
||||||
|
f'Model type with ID {type_id} not found',
|
||||||
|
http_code=404
|
||||||
|
)
|
||||||
|
|
||||||
|
data = request.get_json()
|
||||||
|
if not data:
|
||||||
|
return error_response(ErrorCodes.VALIDATION_ERROR, 'No data provided')
|
||||||
|
|
||||||
|
# Check duplicate name
|
||||||
|
if 'modeltype' in data and data['modeltype'] != mt.modeltype:
|
||||||
|
if ModelType.query.filter_by(modeltype=data['modeltype']).first():
|
||||||
|
return error_response(
|
||||||
|
ErrorCodes.CONFLICT,
|
||||||
|
f"Model type '{data['modeltype']}' already exists",
|
||||||
|
http_code=409
|
||||||
|
)
|
||||||
|
|
||||||
|
for key in ['modeltype', 'category', 'description', 'icon', 'isactive']:
|
||||||
|
if key in data:
|
||||||
|
setattr(mt, key, data[key])
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
return success_response(mt.to_dict(), message='Model type updated')
|
||||||
|
|
||||||
|
|
||||||
|
@modeltypes_bp.route('/<int:type_id>', methods=['DELETE'])
|
||||||
|
@jwt_required()
|
||||||
|
@require_role('admin')
|
||||||
|
def delete_modeltype(type_id: int):
|
||||||
|
"""Delete (deactivate) a model type."""
|
||||||
|
mt = db.session.get(ModelType, type_id)
|
||||||
|
|
||||||
|
if not mt:
|
||||||
|
return error_response(
|
||||||
|
ErrorCodes.NOT_FOUND,
|
||||||
|
f'Model type with ID {type_id} not found',
|
||||||
|
http_code=404
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if any model uses this type
|
||||||
|
from shopdb.core.models import Model
|
||||||
|
if Model.query.filter_by(modeltypeid=type_id).first():
|
||||||
|
return error_response(
|
||||||
|
ErrorCodes.CONFLICT,
|
||||||
|
'Cannot delete model type: models are using it',
|
||||||
|
http_code=409
|
||||||
|
)
|
||||||
|
|
||||||
|
mt.isactive = False
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return success_response(message='Model type deleted')
|
||||||
@@ -27,37 +27,37 @@ def generate_csv(data: list, columns: list) -> str:
|
|||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Report: Equipment by Type
|
# Report: Machines by Type
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
@reports_bp.route('/equipment-by-type', methods=['GET'])
|
@reports_bp.route('/machines-by-type', methods=['GET'])
|
||||||
@jwt_required(optional=True)
|
@jwt_required(optional=True)
|
||||||
def equipment_by_type():
|
def machines_by_type():
|
||||||
"""
|
"""
|
||||||
Report: Equipment count grouped by equipment type.
|
Report: Machine count grouped by machine type.
|
||||||
|
|
||||||
Query parameters:
|
Query parameters:
|
||||||
- businessunitid: Filter by business unit
|
- businessunitid: Filter by business unit
|
||||||
- format: 'json' (default) or 'csv'
|
- format: 'json' (default) or 'csv'
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
from plugins.equipment.models import Equipment, EquipmentType
|
from plugins.machines.models import Machine, MachineType
|
||||||
except ImportError:
|
except ImportError:
|
||||||
return error_response(
|
return error_response(
|
||||||
ErrorCodes.NOT_FOUND,
|
ErrorCodes.NOT_FOUND,
|
||||||
'Equipment plugin not installed',
|
'Machines plugin not installed',
|
||||||
http_code=404
|
http_code=404
|
||||||
)
|
)
|
||||||
|
|
||||||
query = db.session.query(
|
query = db.session.query(
|
||||||
EquipmentType.equipmenttype,
|
MachineType.machinetype,
|
||||||
EquipmentType.description,
|
MachineType.description,
|
||||||
db.func.count(Equipment.equipmentid).label('count')
|
db.func.count(Machine.machineid).label('count')
|
||||||
).outerjoin(Equipment, Equipment.equipmenttypeid == EquipmentType.equipmenttypeid
|
).outerjoin(Machine, Machine.machinetypeid == MachineType.machinetypeid
|
||||||
).outerjoin(Asset, Asset.assetid == Equipment.assetid
|
).outerjoin(Asset, Asset.assetid == Machine.assetid
|
||||||
).filter(
|
).filter(
|
||||||
db.or_(Asset.isactive == True, Asset.assetid.is_(None)),
|
db.or_(Asset.isactive == True, Asset.assetid.is_(None)),
|
||||||
EquipmentType.isactive == True
|
MachineType.isactive == True
|
||||||
)
|
)
|
||||||
|
|
||||||
# Business unit filter
|
# Business unit filter
|
||||||
@@ -65,25 +65,25 @@ def equipment_by_type():
|
|||||||
query = query.filter(Asset.businessunitid == int(bu_id))
|
query = query.filter(Asset.businessunitid == int(bu_id))
|
||||||
|
|
||||||
query = query.group_by(
|
query = query.group_by(
|
||||||
EquipmentType.equipmenttypeid,
|
MachineType.machinetypeid,
|
||||||
EquipmentType.equipmenttype,
|
MachineType.machinetype,
|
||||||
EquipmentType.description
|
MachineType.description
|
||||||
).order_by(EquipmentType.equipmenttype)
|
).order_by(MachineType.machinetype)
|
||||||
|
|
||||||
results = query.all()
|
results = query.all()
|
||||||
data = [{'equipmenttype': r.equipmenttype, 'description': r.description or '', 'count': r.count} for r in results]
|
data = [{'machinetype': r.machinetype, 'description': r.description or '', 'count': r.count} for r in results]
|
||||||
total = sum(r['count'] for r in data)
|
total = sum(r['count'] for r in data)
|
||||||
|
|
||||||
if request.args.get('format') == 'csv':
|
if request.args.get('format') == 'csv':
|
||||||
csv_data = generate_csv(data, ['equipmenttype', 'description', 'count'])
|
csv_data = generate_csv(data, ['machinetype', 'description', 'count'])
|
||||||
return Response(
|
return Response(
|
||||||
csv_data,
|
csv_data,
|
||||||
mimetype='text/csv',
|
mimetype='text/csv',
|
||||||
headers={'Content-Disposition': 'attachment; filename=equipment_by_type.csv'}
|
headers={'Content-Disposition': 'attachment; filename=machines_by_type.csv'}
|
||||||
)
|
)
|
||||||
|
|
||||||
return success_response({
|
return success_response({
|
||||||
'report': 'equipment_by_type',
|
'report': 'machines_by_type',
|
||||||
'generated': datetime.now(timezone.utc).replace(tzinfo=None).isoformat(),
|
'generated': datetime.now(timezone.utc).replace(tzinfo=None).isoformat(),
|
||||||
'data': data,
|
'data': data,
|
||||||
'total': total
|
'total': total
|
||||||
@@ -450,12 +450,12 @@ def pc_relationships():
|
|||||||
Report: PCs with relationships to shop floor machines.
|
Report: PCs with relationships to shop floor machines.
|
||||||
|
|
||||||
Returns machine number, vendor, model, PC hostname, and PC IP address
|
Returns machine number, vendor, model, PC hostname, and PC IP address
|
||||||
for all active PC-equipment relationships.
|
for all active PC-machine relationships.
|
||||||
|
|
||||||
Query parameters:
|
Query parameters:
|
||||||
- format: 'json' (default) or 'csv'
|
- format: 'json' (default) or 'csv'
|
||||||
"""
|
"""
|
||||||
# Asset relationships where a computer (source) relates to an equipment
|
# Asset relationships where a computer (source) relates to a machine
|
||||||
# (target) - the asset-model equivalent of the legacy PC->machine links.
|
# (target) - the asset-model equivalent of the legacy PC->machine links.
|
||||||
sql = db.text("""
|
sql = db.text("""
|
||||||
SELECT
|
SELECT
|
||||||
@@ -468,7 +468,7 @@ def pc_relationships():
|
|||||||
JOIN assets pc ON ar.sourceassetid = pc.assetid
|
JOIN assets pc ON ar.sourceassetid = pc.assetid
|
||||||
JOIN computers cpc ON cpc.assetid = pc.assetid
|
JOIN computers cpc ON cpc.assetid = pc.assetid
|
||||||
JOIN assets eq ON ar.targetassetid = eq.assetid
|
JOIN assets eq ON ar.targetassetid = eq.assetid
|
||||||
JOIN equipment eqx ON eqx.assetid = eq.assetid
|
JOIN machines eqx ON eqx.assetid = eq.assetid
|
||||||
LEFT JOIN communications c ON c.assetid = pc.assetid AND c.isprimary = 1 AND c.comtypeid = 1
|
LEFT JOIN communications c ON c.assetid = pc.assetid AND c.isprimary = 1 AND c.comtypeid = 1
|
||||||
LEFT JOIN models mo ON eqx.modelnumberid = mo.modelnumberid
|
LEFT JOIN models mo ON eqx.modelnumberid = mo.modelnumberid
|
||||||
LEFT JOIN vendors v ON mo.vendorid = v.vendorid
|
LEFT JOIN vendors v ON mo.vendorid = v.vendorid
|
||||||
@@ -508,10 +508,10 @@ def list_reports():
|
|||||||
"""List all available reports."""
|
"""List all available reports."""
|
||||||
reports = [
|
reports = [
|
||||||
{
|
{
|
||||||
'id': 'equipment-by-type',
|
'id': 'machines-by-type',
|
||||||
'name': 'Machines by Type',
|
'name': 'Machines by Type',
|
||||||
'description': 'Machine count grouped by machine type',
|
'description': 'Machine count grouped by machine type',
|
||||||
'endpoint': '/api/reports/equipment-by-type',
|
'endpoint': '/api/reports/machines-by-type',
|
||||||
'category': 'inventory'
|
'category': 'inventory'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -110,8 +110,8 @@ def _get_asset_result(asset, query, relevance=None):
|
|||||||
asset_type_name = asset.assettype.assettype if asset.assettype else 'asset'
|
asset_type_name = asset.assettype.assettype if asset.assettype else 'asset'
|
||||||
|
|
||||||
plugin_id = asset.assetid
|
plugin_id = asset.assetid
|
||||||
if asset_type_name == 'equipment' and hasattr(asset, 'equipment') and asset.equipment:
|
if asset_type_name == 'machine' and hasattr(asset, 'machine') and asset.machine:
|
||||||
plugin_id = asset.equipment.equipmentid
|
plugin_id = asset.machine.machineid
|
||||||
elif asset_type_name == 'computer' and hasattr(asset, 'computer') and asset.computer:
|
elif asset_type_name == 'computer' and hasattr(asset, 'computer') and asset.computer:
|
||||||
plugin_id = asset.computer.computerid
|
plugin_id = asset.computer.computerid
|
||||||
elif asset_type_name == 'network_device' and hasattr(asset, 'network_device') and asset.network_device:
|
elif asset_type_name == 'network_device' and hasattr(asset, 'network_device') and asset.network_device:
|
||||||
@@ -120,7 +120,7 @@ def _get_asset_result(asset, query, relevance=None):
|
|||||||
plugin_id = asset.printer.printerid
|
plugin_id = asset.printer.printerid
|
||||||
|
|
||||||
url_map = {
|
url_map = {
|
||||||
'equipment': f"/machines/{plugin_id}",
|
'machine': f"/machines/{plugin_id}",
|
||||||
'computer': f"/pcs/{plugin_id}",
|
'computer': f"/pcs/{plugin_id}",
|
||||||
'network_device': f"/network/{plugin_id}",
|
'network_device': f"/network/{plugin_id}",
|
||||||
'printer': f"/printers/{plugin_id}",
|
'printer': f"/printers/{plugin_id}",
|
||||||
@@ -478,21 +478,21 @@ def _search_notifications(query, search_term):
|
|||||||
|
|
||||||
|
|
||||||
def _search_vendor_model_type(query, search_term):
|
def _search_vendor_model_type(query, search_term):
|
||||||
"""Search assets by vendor name, model name, or equipment/device type name."""
|
"""Search assets by vendor name, model name, or machine/device type name."""
|
||||||
results = []
|
results = []
|
||||||
|
|
||||||
# Equipment: vendor, model, equipmenttype
|
# Machines: vendor, model, machinetype
|
||||||
try:
|
try:
|
||||||
_require_enabled('equipment')
|
_require_enabled('machines')
|
||||||
from plugins.equipment.models import Equipment, EquipmentType
|
from plugins.machines.models import Machine, MachineType
|
||||||
equipment_assets = db.session.query(Asset).join(
|
machine_assets = db.session.query(Asset).join(
|
||||||
Equipment, Equipment.assetid == Asset.assetid
|
Machine, Machine.assetid == Asset.assetid
|
||||||
).outerjoin(
|
).outerjoin(
|
||||||
Vendor, Equipment.vendorid == Vendor.vendorid
|
Vendor, Machine.vendorid == Vendor.vendorid
|
||||||
).outerjoin(
|
).outerjoin(
|
||||||
Model, Equipment.modelnumberid == Model.modelnumberid
|
Model, Machine.modelnumberid == Model.modelnumberid
|
||||||
).outerjoin(
|
).outerjoin(
|
||||||
EquipmentType, Equipment.equipmenttypeid == EquipmentType.equipmenttypeid
|
MachineType, Machine.machinetypeid == MachineType.machinetypeid
|
||||||
).options(
|
).options(
|
||||||
joinedload(Asset.assettype),
|
joinedload(Asset.assettype),
|
||||||
joinedload(Asset.location),
|
joinedload(Asset.location),
|
||||||
@@ -501,16 +501,16 @@ def _search_vendor_model_type(query, search_term):
|
|||||||
db.or_(
|
db.or_(
|
||||||
Vendor.vendor.ilike(search_term),
|
Vendor.vendor.ilike(search_term),
|
||||||
Model.modelnumber.ilike(search_term),
|
Model.modelnumber.ilike(search_term),
|
||||||
EquipmentType.equipmenttype.ilike(search_term)
|
MachineType.machinetype.ilike(search_term)
|
||||||
)
|
)
|
||||||
).limit(10).all()
|
).limit(10).all()
|
||||||
|
|
||||||
for asset in equipment_assets:
|
for asset in machine_assets:
|
||||||
results.append(_get_asset_result(asset, query, 30))
|
results.append(_get_asset_result(asset, query, 30))
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Equipment vendor/model/type search failed: {e}")
|
logger.error(f"Machine vendor/model/type search failed: {e}")
|
||||||
|
|
||||||
# Printers: vendor, model, printertype
|
# Printers: vendor, model, printertype
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ IDENTIFIER_LABELS = {
|
|||||||
'maintenancereference': 'Maintenance Reference',
|
'maintenancereference': 'Maintenance Reference',
|
||||||
'fqdn': 'FQDN / hostname',
|
'fqdn': 'FQDN / hostname',
|
||||||
}
|
}
|
||||||
IDENTIFIER_ASSETTYPES = ['equipment', 'computer', 'printer', 'network_device']
|
IDENTIFIER_ASSETTYPES = ['machine', 'computer', 'printer', 'network_device']
|
||||||
|
|
||||||
# Global-search result types that can be toggled on/off independently of whether
|
# Global-search result types that can be toggled on/off independently of whether
|
||||||
# the owning plugin is enabled. Keys match the `type` field on search results;
|
# the owning plugin is enabled. Keys match the `type` field on search results;
|
||||||
@@ -63,7 +63,7 @@ SEARCH_DOMAINS = {
|
|||||||
'application': 'Applications',
|
'application': 'Applications',
|
||||||
'knowledgebase': 'Knowledge Base',
|
'knowledgebase': 'Knowledge Base',
|
||||||
'employee': 'Employees',
|
'employee': 'Employees',
|
||||||
'equipment': 'Equipment',
|
'machine': 'Machines',
|
||||||
'computer': 'PCs',
|
'computer': 'PCs',
|
||||||
'printer': 'Printers',
|
'printer': 'Printers',
|
||||||
'network_device': 'Network Devices',
|
'network_device': 'Network Devices',
|
||||||
@@ -450,7 +450,7 @@ def build_default_settings():
|
|||||||
'value': '/ge-aerospace-logo.svg',
|
'value': '/ge-aerospace-logo.svg',
|
||||||
'valuetype': 'string',
|
'valuetype': 'string',
|
||||||
'category': 'branding',
|
'category': 'branding',
|
||||||
'description': 'Logo shown on the equipment badge print page'
|
'description': 'Logo shown on the machine badge print page'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'key': 'site_favicon',
|
'key': 'site_favicon',
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from .base import BaseModel, SoftDeleteMixin, AuditMixin
|
from .base import BaseModel, SoftDeleteMixin, AuditMixin
|
||||||
from .asset import Asset, AssetType, AssetStatus
|
from .asset import Asset, AssetType, AssetStatus
|
||||||
from .machine import MachineType
|
from .modeltype import ModelType
|
||||||
from .vendor import Vendor
|
from .vendor import Vendor
|
||||||
from .model import Model
|
from .model import Model
|
||||||
from .businessunit import BusinessUnit
|
from .businessunit import BusinessUnit
|
||||||
@@ -26,8 +26,8 @@ __all__ = [
|
|||||||
'Asset',
|
'Asset',
|
||||||
'AssetType',
|
'AssetType',
|
||||||
'AssetStatus',
|
'AssetStatus',
|
||||||
# Legacy machine type lookup (still referenced by models.machinetypeid)
|
# Model-type lookup (referenced by models.modeltypeid)
|
||||||
'MachineType',
|
'ModelType',
|
||||||
# Reference
|
# Reference
|
||||||
'Vendor',
|
'Vendor',
|
||||||
'Model',
|
'Model',
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ class AssetType(BaseModel):
|
|||||||
Registry of asset categories.
|
Registry of asset categories.
|
||||||
|
|
||||||
Each type maps to a plugin-owned extension table.
|
Each type maps to a plugin-owned extension table.
|
||||||
Examples: equipment, computer, network_device, printer
|
Examples: machine, computer, network_device, printer
|
||||||
"""
|
"""
|
||||||
__tablename__ = 'assettypes'
|
__tablename__ = 'assettypes'
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ class AssetType(BaseModel):
|
|||||||
db.String(50),
|
db.String(50),
|
||||||
unique=True,
|
unique=True,
|
||||||
nullable=False,
|
nullable=False,
|
||||||
comment='Category name: equipment, computer, network_device, printer'
|
comment='Category name: machine, computer, network_device, printer'
|
||||||
)
|
)
|
||||||
pluginname = db.Column(
|
pluginname = db.Column(
|
||||||
db.String(100),
|
db.String(100),
|
||||||
@@ -56,7 +56,7 @@ class Asset(BaseModel, SoftDeleteMixin, AuditMixin):
|
|||||||
Core asset model - minimal shared fields.
|
Core asset model - minimal shared fields.
|
||||||
|
|
||||||
Category-specific data lives in plugin extension tables
|
Category-specific data lives in plugin extension tables
|
||||||
(equipment, computers, network_devices, printers).
|
(machines, computers, network_devices, printers).
|
||||||
The assetid matches original machineid for migration compatibility.
|
The assetid matches original machineid for migration compatibility.
|
||||||
"""
|
"""
|
||||||
__tablename__ = 'assets'
|
__tablename__ = 'assets'
|
||||||
@@ -79,7 +79,7 @@ class Asset(BaseModel, SoftDeleteMixin, AuditMixin):
|
|||||||
db.String(50),
|
db.String(50),
|
||||||
index=True,
|
index=True,
|
||||||
comment='Gauge lab asset reference (authoritative tag the gauge lab '
|
comment='Gauge lab asset reference (authoritative tag the gauge lab '
|
||||||
'assigns to equipment); distinct from assetnumber'
|
'assigns to machines); distinct from assetnumber'
|
||||||
)
|
)
|
||||||
maintenancereference = db.Column(
|
maintenancereference = db.Column(
|
||||||
db.String(50),
|
db.String(50),
|
||||||
@@ -226,8 +226,8 @@ class Asset(BaseModel, SoftDeleteMixin, AuditMixin):
|
|||||||
result['businessunitname'] = 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, 'machine') and self.machine:
|
||||||
result['pluginid'] = self.equipment.equipmentid
|
result['pluginid'] = self.machine.machineid
|
||||||
elif hasattr(self, 'computer') and self.computer:
|
elif hasattr(self, 'computer') and self.computer:
|
||||||
result['pluginid'] = 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:
|
||||||
@@ -259,9 +259,9 @@ class Asset(BaseModel, SoftDeleteMixin, AuditMixin):
|
|||||||
|
|
||||||
def _get_extension_data(self):
|
def _get_extension_data(self):
|
||||||
"""Get category-specific data from extension table."""
|
"""Get category-specific data from extension table."""
|
||||||
# Check for equipment extension
|
# Check for machine extension
|
||||||
if hasattr(self, 'equipment') and self.equipment:
|
if hasattr(self, 'machine') and self.machine:
|
||||||
return self.equipment.to_dict()
|
return self.machine.to_dict()
|
||||||
# Check for computer extension
|
# Check for computer extension
|
||||||
if hasattr(self, 'computer') and self.computer:
|
if hasattr(self, 'computer') and self.computer:
|
||||||
return self.computer.to_dict()
|
return self.computer.to_dict()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""Custom fields: site-defined extra attributes per asset type.
|
"""Custom fields: site-defined extra attributes per asset type.
|
||||||
|
|
||||||
A CustomField is a definition scoped to one asset type (equipment, computer,
|
A CustomField is a definition scoped to one asset type (machine, computer,
|
||||||
printer, network_device). A CustomFieldValue holds one asset's value for one
|
printer, network_device). A CustomFieldValue holds one asset's value for one
|
||||||
field. This is the generic form of the built-in identifier columns - sites add
|
field. This is the generic form of the built-in identifier columns - sites add
|
||||||
their own attributes without a schema change.
|
their own attributes without a schema change.
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
"""Legacy machine type lookup.
|
|
||||||
|
|
||||||
The Machine instance model and its PC/status lookups were retired (ADR-001);
|
|
||||||
assets are the platform contract. MachineType is kept only because the shared
|
|
||||||
`models` table still references it via models.machinetypeid.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from shopdb.extensions import db
|
|
||||||
from .base import BaseModel
|
|
||||||
|
|
||||||
|
|
||||||
class MachineType(BaseModel):
|
|
||||||
"""
|
|
||||||
Machine type classification.
|
|
||||||
Categories: Equipment, PC, Network, Printer
|
|
||||||
"""
|
|
||||||
__tablename__ = 'machinetypes'
|
|
||||||
|
|
||||||
machinetypeid = db.Column(db.Integer, primary_key=True)
|
|
||||||
machinetype = db.Column(db.String(100), unique=True, nullable=False)
|
|
||||||
category = db.Column(
|
|
||||||
db.String(50),
|
|
||||||
nullable=False,
|
|
||||||
default='Equipment',
|
|
||||||
comment='Equipment, PC, Network, or Printer'
|
|
||||||
)
|
|
||||||
description = db.Column(db.Text)
|
|
||||||
icon = db.Column(db.String(50), comment='Icon name for UI')
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"<MachineType {self.machinetype}>"
|
|
||||||
@@ -1,20 +1,20 @@
|
|||||||
"""Model (equipment model number) model."""
|
"""Model (vendor catalog model number) model."""
|
||||||
|
|
||||||
from shopdb.extensions import db
|
from shopdb.extensions import db
|
||||||
from .base import BaseModel
|
from .base import BaseModel
|
||||||
|
|
||||||
|
|
||||||
class Model(BaseModel):
|
class Model(BaseModel):
|
||||||
"""Equipment/device model information."""
|
"""Vendor catalog model information (machines, PCs, printers, network)."""
|
||||||
__tablename__ = 'models'
|
__tablename__ = 'models'
|
||||||
|
|
||||||
modelnumberid = db.Column(db.Integer, primary_key=True)
|
modelnumberid = db.Column(db.Integer, primary_key=True)
|
||||||
modelnumber = db.Column(db.String(100), nullable=False)
|
modelnumber = db.Column(db.String(100), nullable=False)
|
||||||
|
|
||||||
# Link to machine type (what kind of equipment this model is for)
|
# Link to model type (what kind of thing this catalog model is for)
|
||||||
machinetypeid = db.Column(
|
modeltypeid = db.Column(
|
||||||
db.Integer,
|
db.Integer,
|
||||||
db.ForeignKey('machinetypes.machinetypeid'),
|
db.ForeignKey('modeltypes.modeltypeid'),
|
||||||
nullable=True
|
nullable=True
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ class Model(BaseModel):
|
|||||||
notes = db.Column(db.Text)
|
notes = db.Column(db.Text)
|
||||||
|
|
||||||
# Relationships
|
# Relationships
|
||||||
machinetype = db.relationship('MachineType', backref='models')
|
modeltype = db.relationship('ModelType', backref='models')
|
||||||
vendor = db.relationship('Vendor', backref='models')
|
vendor = db.relationship('Vendor', backref='models')
|
||||||
|
|
||||||
# Unique constraint on modelnumber + vendor
|
# Unique constraint on modelnumber + vendor
|
||||||
|
|||||||
33
shopdb/core/models/modeltype.py
Normal file
33
shopdb/core/models/modeltype.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
"""Model-type lookup (types the vendor MODELS catalog).
|
||||||
|
|
||||||
|
The Machine instance model and its PC/status lookups were retired (ADR-001);
|
||||||
|
assets are the platform contract. ModelType is kept because the shared `models`
|
||||||
|
table references it via models.modeltypeid: it types vendor models (Lathe,
|
||||||
|
Switch, Laser Printer), not asset instances. Renamed from MachineType to be
|
||||||
|
role-accurate and to free the "machinetype" name for the machines plugin.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from shopdb.extensions import db
|
||||||
|
from .base import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class ModelType(BaseModel):
|
||||||
|
"""
|
||||||
|
Model-type classification (what kind of thing a catalog model is for).
|
||||||
|
Categories: Equipment, PC, Network, Printer
|
||||||
|
"""
|
||||||
|
__tablename__ = 'modeltypes'
|
||||||
|
|
||||||
|
modeltypeid = db.Column(db.Integer, primary_key=True)
|
||||||
|
modeltype = db.Column(db.String(100), unique=True, nullable=False)
|
||||||
|
category = db.Column(
|
||||||
|
db.String(50),
|
||||||
|
nullable=False,
|
||||||
|
default='Equipment',
|
||||||
|
comment='Equipment, PC, Network, or Printer'
|
||||||
|
)
|
||||||
|
description = db.Column(db.Text)
|
||||||
|
icon = db.Column(db.String(50), comment='Icon name for UI')
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<ModelType {self.modeltype}>"
|
||||||
@@ -50,9 +50,9 @@ class AssetRelationship(BaseModel):
|
|||||||
Relationships between assets.
|
Relationships between assets.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
- Computer controls Equipment
|
- Computer controls Machine
|
||||||
- Two machines are dualpath partners
|
- Two machines are dualpath partners
|
||||||
- Network device connects to equipment
|
- Network device connects to machine
|
||||||
"""
|
"""
|
||||||
__tablename__ = 'assetrelationships'
|
__tablename__ = 'assetrelationships'
|
||||||
|
|
||||||
|
|||||||
@@ -40,11 +40,11 @@ class Permission(db.Model):
|
|||||||
('assets.create', 'Create assets', 'assets'),
|
('assets.create', 'Create assets', 'assets'),
|
||||||
('assets.edit', 'Edit assets', 'assets'),
|
('assets.edit', 'Edit assets', 'assets'),
|
||||||
('assets.delete', 'Delete assets', 'assets'),
|
('assets.delete', 'Delete assets', 'assets'),
|
||||||
# Equipment
|
# Machines
|
||||||
('equipment.view', 'View equipment', 'equipment'),
|
('machines.view', 'View machines', 'machines'),
|
||||||
('equipment.create', 'Create equipment', 'equipment'),
|
('machines.create', 'Create machines', 'machines'),
|
||||||
('equipment.edit', 'Edit equipment', 'equipment'),
|
('machines.edit', 'Edit machines', 'machines'),
|
||||||
('equipment.delete', 'Delete equipment', 'equipment'),
|
('machines.delete', 'Delete machines', 'machines'),
|
||||||
# Computers
|
# Computers
|
||||||
('computers.view', 'View computers', 'computers'),
|
('computers.view', 'View computers', 'computers'),
|
||||||
('computers.create', 'Create computers', 'computers'),
|
('computers.create', 'Create computers', 'computers'),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""Shared Alembic env.py logic for bundled plugins.
|
"""Shared Alembic env.py logic for bundled plugins.
|
||||||
|
|
||||||
Every bundled plugin that owns tables (computers, employees, equipment,
|
Every bundled plugin that owns tables (computers, employees, knowledgebase,
|
||||||
knowledgebase, network, notifications, printers, slides, usb, warranty) has a
|
machines, network, notifications, printers, slides, usb, warranty) has a
|
||||||
`migrations/env.py` that does the minimum:
|
`migrations/env.py` that does the minimum:
|
||||||
|
|
||||||
import os
|
import os
|
||||||
@@ -42,8 +42,8 @@ PLUGIN_TABLE_OWNERS: dict[str, Iterable[str]] = {
|
|||||||
'computers': ('computertypes', 'computers', 'computerinstalledapps',
|
'computers': ('computertypes', 'computers', 'computerinstalledapps',
|
||||||
'accessprotocols', 'computeraccess'),
|
'accessprotocols', 'computeraccess'),
|
||||||
'employees': ('directoryemployees',),
|
'employees': ('directoryemployees',),
|
||||||
'equipment': ('equipmenttypes', 'equipment'),
|
|
||||||
'knowledgebase': ('knowledgebase',),
|
'knowledgebase': ('knowledgebase',),
|
||||||
|
'machines': ('machinetypes', 'machines'),
|
||||||
'measuringtools': ('measuringtooltypes', 'measuringtools'),
|
'measuringtools': ('measuringtooltypes', 'measuringtools'),
|
||||||
'network': ('networkdevicetypes', 'networkdevices', 'vlans', 'subnets'),
|
'network': ('networkdevicetypes', 'networkdevices', 'vlans', 'subnets'),
|
||||||
'notifications': ('notificationtypes', 'notifications'),
|
'notifications': ('notificationtypes', 'notifications'),
|
||||||
|
|||||||
@@ -41,6 +41,24 @@ class PluginRegistry:
|
|||||||
except (json.JSONDecodeError, TypeError):
|
except (json.JSONDecodeError, TypeError):
|
||||||
# Corrupted file, start fresh
|
# Corrupted file, start fresh
|
||||||
self._plugins = {}
|
self._plugins = {}
|
||||||
|
self._migrate_renamed_plugins()
|
||||||
|
|
||||||
|
def _migrate_renamed_plugins(self) -> None:
|
||||||
|
"""Upgrade path for the equipment -> machines plugin rename.
|
||||||
|
|
||||||
|
Existing installs carry an 'equipment' entry in plugins.json. When the
|
||||||
|
renamed plugins/machines directory exists and no 'machines' entry does,
|
||||||
|
carry the state over under the new name and persist.
|
||||||
|
"""
|
||||||
|
if 'equipment' not in self._plugins or 'machines' in self._plugins:
|
||||||
|
return
|
||||||
|
machines_dir = Path(__file__).resolve().parents[2] / 'plugins' / 'machines'
|
||||||
|
if not machines_dir.exists():
|
||||||
|
return
|
||||||
|
state = self._plugins.pop('equipment')
|
||||||
|
state.name = 'machines'
|
||||||
|
self._plugins['machines'] = state
|
||||||
|
self._save()
|
||||||
|
|
||||||
def _save(self) -> None:
|
def _save(self) -> None:
|
||||||
"""Save registry to file."""
|
"""Save registry to file."""
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ def _report_ids(client, headers):
|
|||||||
def test_reports_include_core_reports(client, auth_headers):
|
def test_reports_include_core_reports(client, auth_headers):
|
||||||
"""The static core reports are always listed."""
|
"""The static core reports are always listed."""
|
||||||
_, ids = _report_ids(client, auth_headers)
|
_, ids = _report_ids(client, auth_headers)
|
||||||
assert 'equipment-by-type' in ids
|
assert 'machines-by-type' in ids
|
||||||
assert 'pc-relationships' in ids
|
assert 'pc-relationships' in ids
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ from shopdb.plugins import plugin_manager
|
|||||||
from shopdb.plugins.base import BasePlugin, PluginMeta
|
from shopdb.plugins.base import BasePlugin, PluginMeta
|
||||||
|
|
||||||
|
|
||||||
BUNDLED_PLUGINS = ('computers', 'employees', 'equipment', 'knowledgebase', 'network', 'notifications', 'printers', 'slides', 'usb')
|
BUNDLED_PLUGINS = ('computers', 'employees', 'knowledgebase', 'machines', 'network', 'notifications', 'printers', 'slides', 'usb')
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ TABLE_OWNING_PLUGINS = tuple(sorted(PLUGIN_TABLE_OWNERS))
|
|||||||
# no-op assertion must not apply to them. This is a frozen list on purpose - a
|
# no-op assertion must not apply to them. This is a frozen list on purpose - a
|
||||||
# newly discovered plugin does not silently get treated as a cutover no-op.
|
# newly discovered plugin does not silently get treated as a cutover no-op.
|
||||||
CUTOVER_PLUGINS = (
|
CUTOVER_PLUGINS = (
|
||||||
'computers', 'employees', 'equipment', 'knowledgebase', 'network',
|
'computers', 'employees', 'knowledgebase', 'machines', 'network',
|
||||||
'notifications', 'printers', 'slides', 'usb', 'warranty',
|
'notifications', 'printers', 'slides', 'usb', 'warranty',
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -46,6 +46,9 @@ CUTOVER_PLUGINS = (
|
|||||||
# stamp '<plugin>0001anchor'; measuringtools stamps its real baseline id.
|
# stamp '<plugin>0001anchor'; measuringtools stamps its real baseline id.
|
||||||
EXPECTED_HEAD_REVISION = {plugin: f'{plugin}0001anchor' for plugin in CUTOVER_PLUGINS}
|
EXPECTED_HEAD_REVISION = {plugin: f'{plugin}0001anchor' for plugin in CUTOVER_PLUGINS}
|
||||||
EXPECTED_HEAD_REVISION['measuringtools'] = 'measuringtools0001baseline'
|
EXPECTED_HEAD_REVISION['measuringtools'] = 'measuringtools0001baseline'
|
||||||
|
# machines (renamed from equipment) keeps its original anchor id and adds the
|
||||||
|
# rename revision on top, so its head is not the f-string default.
|
||||||
|
EXPECTED_HEAD_REVISION['machines'] = 'machines0002rename'
|
||||||
|
|
||||||
# Plugins built after the cutover: their 0001 baseline really creates tables the
|
# Plugins built after the cutover: their 0001 baseline really creates tables the
|
||||||
# core chain never owned.
|
# core chain never owned.
|
||||||
|
|||||||
@@ -95,8 +95,8 @@ def test_plugin_loader_discovers_bundled_plugins(app):
|
|||||||
expected_plugins = {
|
expected_plugins = {
|
||||||
'computers',
|
'computers',
|
||||||
'employees',
|
'employees',
|
||||||
'equipment',
|
|
||||||
'knowledgebase',
|
'knowledgebase',
|
||||||
|
'machines',
|
||||||
'network',
|
'network',
|
||||||
'notifications',
|
'notifications',
|
||||||
'printers',
|
'printers',
|
||||||
|
|||||||
Reference in New Issue
Block a user