Warranty polish: hero badges everywhere, search/pagination, re-check, shared apiError
- Shared utils/apiError.js reads the correct nested error message (with fallbacks); swept 37 views/components off the shallow path so real backend messages (e.g. in-use 409s) surface instead of generic "Failed". - useWarrantyBadge composable: warranty hero status/end-date badge now on PC, equipment, printer, and network detail heroes (one shared fetch feeds the badge + the WarrantyPanel). - Warranties list: client-side search (vendor/level/tag/asset) + pagination; truncate long service levels so they stop blowing out the table width. - "Re-check all" button + POST /warranty/sync/dell?all=true to re-pull dated Dell warranties, not just missing ones. - Deprecate the standalone pxe-images/warranty_sync.py in favor of the plugin. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -970,8 +970,8 @@ export const warrantyApi = {
|
|||||||
refresh(id) {
|
refresh(id) {
|
||||||
return api.post(`/warranty/${id}/refresh`)
|
return api.post(`/warranty/${id}/refresh`)
|
||||||
},
|
},
|
||||||
syncDell() {
|
syncDell(all = false) {
|
||||||
return api.post('/warranty/sync/dell')
|
return api.post('/warranty/sync/dell', null, { params: all ? { all: 'true' } : {} })
|
||||||
},
|
},
|
||||||
report() {
|
report() {
|
||||||
return api.get('/warranty/report')
|
return api.get('/warranty/report')
|
||||||
|
|||||||
@@ -178,6 +178,7 @@ 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'
|
||||||
import { useToast } from '../composables/toast'
|
import { useToast } from '../composables/toast'
|
||||||
|
import { apiError } from '../utils/apiError'
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -351,7 +352,7 @@ async function saveRelationship() {
|
|||||||
emit('updated')
|
emit('updated')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to create relationship:', error)
|
console.error('Failed to create relationship:', error)
|
||||||
toast.error('Failed to create relationship: ' + (error.response?.data?.message || error.message))
|
toast.error(apiError(error, 'Failed to create relationship'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
44
frontend/src/composables/warrantyBadge.js
Normal file
44
frontend/src/composables/warrantyBadge.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
// Shared warranty fetch + hero-badge derivation for asset detail pages.
|
||||||
|
// Pass a getter for the asset id; get back the warranties list (to feed a
|
||||||
|
// WarrantyPanel) and a heroWarranty for a compact status badge.
|
||||||
|
import { ref, computed, watch } from 'vue'
|
||||||
|
import { warrantyApi } from '../api'
|
||||||
|
|
||||||
|
const STATUS_RANK = { expired: 0, expiring: 1, active: 2, unknown: 3 }
|
||||||
|
const STATUS_LABELS = {
|
||||||
|
active: 'Under Warranty',
|
||||||
|
expiring: 'Warranty Expiring',
|
||||||
|
expired: 'Warranty Expired',
|
||||||
|
unknown: 'Warranty',
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useWarrantyBadge(getAssetId) {
|
||||||
|
const warranties = ref([])
|
||||||
|
|
||||||
|
// Worst-case warranty drives the badge: expired > expiring > active > unknown.
|
||||||
|
const heroWarranty = computed(() => {
|
||||||
|
if (!warranties.value.length) return null
|
||||||
|
const worst = [...warranties.value]
|
||||||
|
.sort((a, b) => (STATUS_RANK[a.status] ?? 9) - (STATUS_RANK[b.status] ?? 9))[0]
|
||||||
|
return { ...worst, label: STATUS_LABELS[worst.status] || 'Warranty' }
|
||||||
|
})
|
||||||
|
|
||||||
|
function warrantyDate(d) {
|
||||||
|
return d ? new Date(d + 'T00:00:00').toLocaleDateString() : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
const assetid = getAssetId()
|
||||||
|
if (!assetid) { warranties.value = []; return }
|
||||||
|
try {
|
||||||
|
const response = await warrantyApi.forAsset(assetid)
|
||||||
|
warranties.value = response.data.data || []
|
||||||
|
} catch (err) {
|
||||||
|
warranties.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(getAssetId, load, { immediate: true })
|
||||||
|
|
||||||
|
return { warranties, heroWarranty, warrantyDate, reloadWarranties: load }
|
||||||
|
}
|
||||||
9
frontend/src/utils/apiError.js
Normal file
9
frontend/src/utils/apiError.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
// Extract a human-readable message from an API error. The backend nests the
|
||||||
|
// message at data.data.error.message (see error_response); older call sites read
|
||||||
|
// shallower paths, so try them all before falling back.
|
||||||
|
export function apiError(err, fallback = 'Something went wrong') {
|
||||||
|
return err?.response?.data?.data?.error?.message
|
||||||
|
|| err?.response?.data?.error?.message
|
||||||
|
|| err?.response?.data?.message
|
||||||
|
|| fallback
|
||||||
|
}
|
||||||
@@ -1,278 +1,279 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h2>{{ isEdit ? 'Edit Application' : 'New Application' }}</h2>
|
<h2>{{ isEdit ? 'Edit Application' : 'New Application' }}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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="saveApplication">
|
<form v-else @submit.prevent="saveApplication">
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="appname">Application Name *</label>
|
<label for="appname">Application Name *</label>
|
||||||
<input
|
<input
|
||||||
id="appname"
|
id="appname"
|
||||||
v-model="form.appname"
|
v-model="form.appname"
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="supportteamid">Support Team</label>
|
<label for="supportteamid">Support Team</label>
|
||||||
<select
|
<select
|
||||||
id="supportteamid"
|
id="supportteamid"
|
||||||
v-model="form.supportteamid"
|
v-model="form.supportteamid"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
>
|
>
|
||||||
<option value="">Select team...</option>
|
<option value="">Select team...</option>
|
||||||
<option
|
<option
|
||||||
v-for="team in supportTeams"
|
v-for="team in supportTeams"
|
||||||
:key="team.supportteamid"
|
:key="team.supportteamid"
|
||||||
:value="team.supportteamid"
|
:value="team.supportteamid"
|
||||||
>
|
>
|
||||||
{{ team.teamname }}
|
{{ team.teamname }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="appdescription">Description</label>
|
<label for="appdescription">Description</label>
|
||||||
<textarea
|
<textarea
|
||||||
id="appdescription"
|
id="appdescription"
|
||||||
v-model="form.appdescription"
|
v-model="form.appdescription"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
rows="2"
|
rows="2"
|
||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h4 style="margin-top: 1.5rem; margin-bottom: 1rem;">Application Flags</h4>
|
<h4 style="margin-top: 1.5rem; margin-bottom: 1rem;">Application Flags</h4>
|
||||||
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group checkbox-group">
|
<div class="form-group checkbox-group">
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" v-model="form.isinstallable" />
|
<input type="checkbox" v-model="form.isinstallable" />
|
||||||
Installable
|
Installable
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" v-model="form.islicenced" />
|
<input type="checkbox" v-model="form.islicenced" />
|
||||||
Licensed
|
Licensed
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" v-model="form.isprinter" />
|
<input type="checkbox" v-model="form.isprinter" />
|
||||||
Printer App
|
Printer App
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" v-model="form.isrequired" />
|
<input type="checkbox" v-model="form.isrequired" />
|
||||||
Required on all PCs
|
Required on all PCs
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" v-model="form.ishidden" />
|
<input type="checkbox" v-model="form.ishidden" />
|
||||||
Hidden
|
Hidden
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h4 style="margin-top: 1.5rem; margin-bottom: 1rem;">Links & Paths</h4>
|
<h4 style="margin-top: 1.5rem; margin-bottom: 1rem;">Links & Paths</h4>
|
||||||
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="applicationlink">Application Link</label>
|
<label for="applicationlink">Application Link</label>
|
||||||
<input
|
<input
|
||||||
id="applicationlink"
|
id="applicationlink"
|
||||||
v-model="form.applicationlink"
|
v-model="form.applicationlink"
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
placeholder="https://..."
|
placeholder="https://..."
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="documentationpath">Documentation Path</label>
|
<label for="documentationpath">Documentation Path</label>
|
||||||
<input
|
<input
|
||||||
id="documentationpath"
|
id="documentationpath"
|
||||||
v-model="form.documentationpath"
|
v-model="form.documentationpath"
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
placeholder="URL or file path"
|
placeholder="URL or file path"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="installpath">Install Path</label>
|
<label for="installpath">Install Path</label>
|
||||||
<input
|
<input
|
||||||
id="installpath"
|
id="installpath"
|
||||||
v-model="form.installpath"
|
v-model="form.installpath"
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
placeholder="Network path or URL to install files"
|
placeholder="Network path or URL to install files"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="image">Image Filename</label>
|
<label for="image">Image Filename</label>
|
||||||
<input
|
<input
|
||||||
id="image"
|
id="image"
|
||||||
v-model="form.image"
|
v-model="form.image"
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
placeholder="e.g., myapp.png"
|
placeholder="e.g., myapp.png"
|
||||||
/>
|
/>
|
||||||
<small class="form-hint">Image should be placed in /images/applications/</small>
|
<small class="form-hint">Image should be placed in /images/applications/</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h4 style="margin-top: 1.5rem; margin-bottom: 1rem;">Notes</h4>
|
<h4 style="margin-top: 1.5rem; margin-bottom: 1rem;">Notes</h4>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="applicationnotes">Application Notes (HTML supported)</label>
|
<label for="applicationnotes">Application Notes (HTML supported)</label>
|
||||||
<textarea
|
<textarea
|
||||||
id="applicationnotes"
|
id="applicationnotes"
|
||||||
v-model="form.applicationnotes"
|
v-model="form.applicationnotes"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
rows="6"
|
rows="6"
|
||||||
placeholder="Enter notes... HTML tags like <BR>, <a>, <strong> are supported"
|
placeholder="Enter notes... HTML tags like <BR>, <a>, <strong> are supported"
|
||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="error" class="error-message">{{ error }}</div>
|
<div v-if="error" class="error-message">{{ error }}</div>
|
||||||
|
|
||||||
<div style="display: flex; gap: 0.5rem; margin-top: 1.5rem;">
|
<div style="display: flex; gap: 0.5rem; margin-top: 1.5rem;">
|
||||||
<button type="submit" class="btn btn-primary" :disabled="saving">
|
<button type="submit" class="btn btn-primary" :disabled="saving">
|
||||||
{{ saving ? 'Saving...' : 'Save Application' }}
|
{{ saving ? 'Saving...' : 'Save Application' }}
|
||||||
</button>
|
</button>
|
||||||
<router-link to="/applications" class="btn btn-secondary">Cancel</router-link>
|
<router-link to="/applications" class="btn btn-secondary">Cancel</router-link>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, computed } from 'vue'
|
import { ref, onMounted, computed } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { applicationsApi } from '../../api'
|
import { applicationsApi } from '../../api'
|
||||||
|
import { apiError } from '../../utils/apiError'
|
||||||
const route = useRoute()
|
|
||||||
const router = useRouter()
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
const isEdit = computed(() => !!route.params.id)
|
|
||||||
|
const isEdit = computed(() => !!route.params.id)
|
||||||
const loading = ref(true)
|
|
||||||
const saving = ref(false)
|
const loading = ref(true)
|
||||||
const error = ref('')
|
const saving = ref(false)
|
||||||
|
const error = ref('')
|
||||||
const form = ref({
|
|
||||||
appname: '',
|
const form = ref({
|
||||||
appdescription: '',
|
appname: '',
|
||||||
supportteamid: '',
|
appdescription: '',
|
||||||
isinstallable: false,
|
supportteamid: '',
|
||||||
islicenced: false,
|
isinstallable: false,
|
||||||
isprinter: false,
|
islicenced: false,
|
||||||
isrequired: false,
|
isprinter: false,
|
||||||
ishidden: false,
|
isrequired: false,
|
||||||
applicationlink: '',
|
ishidden: false,
|
||||||
documentationpath: '',
|
applicationlink: '',
|
||||||
installpath: '',
|
documentationpath: '',
|
||||||
image: '',
|
installpath: '',
|
||||||
applicationnotes: ''
|
image: '',
|
||||||
})
|
applicationnotes: ''
|
||||||
|
})
|
||||||
const supportTeams = ref([])
|
|
||||||
|
const supportTeams = ref([])
|
||||||
onMounted(async () => {
|
|
||||||
try {
|
onMounted(async () => {
|
||||||
// Load support teams
|
try {
|
||||||
const teamsRes = await applicationsApi.getSupportTeams()
|
// Load support teams
|
||||||
supportTeams.value = teamsRes.data.data || []
|
const teamsRes = await applicationsApi.getSupportTeams()
|
||||||
|
supportTeams.value = teamsRes.data.data || []
|
||||||
// Load application if editing
|
|
||||||
if (isEdit.value) {
|
// Load application if editing
|
||||||
const response = await applicationsApi.get(route.params.id)
|
if (isEdit.value) {
|
||||||
const app = response.data.data
|
const response = await applicationsApi.get(route.params.id)
|
||||||
|
const app = response.data.data
|
||||||
form.value = {
|
|
||||||
appname: app.appname || '',
|
form.value = {
|
||||||
appdescription: app.appdescription || '',
|
appname: app.appname || '',
|
||||||
supportteamid: app.supportteam?.supportteamid || '',
|
appdescription: app.appdescription || '',
|
||||||
isinstallable: app.isinstallable || false,
|
supportteamid: app.supportteam?.supportteamid || '',
|
||||||
islicenced: app.islicenced || false,
|
isinstallable: app.isinstallable || false,
|
||||||
isprinter: app.isprinter || false,
|
islicenced: app.islicenced || false,
|
||||||
isrequired: app.isrequired || false,
|
isprinter: app.isprinter || false,
|
||||||
ishidden: app.ishidden || false,
|
isrequired: app.isrequired || false,
|
||||||
applicationlink: app.applicationlink || '',
|
ishidden: app.ishidden || false,
|
||||||
documentationpath: app.documentationpath || '',
|
applicationlink: app.applicationlink || '',
|
||||||
installpath: app.installpath || '',
|
documentationpath: app.documentationpath || '',
|
||||||
image: app.image || '',
|
installpath: app.installpath || '',
|
||||||
applicationnotes: app.applicationnotes || ''
|
image: app.image || '',
|
||||||
}
|
applicationnotes: app.applicationnotes || ''
|
||||||
}
|
}
|
||||||
} catch (err) {
|
}
|
||||||
console.error('Error loading data:', err)
|
} catch (err) {
|
||||||
error.value = 'Failed to load data'
|
console.error('Error loading data:', err)
|
||||||
} finally {
|
error.value = 'Failed to load data'
|
||||||
loading.value = false
|
} finally {
|
||||||
}
|
loading.value = false
|
||||||
})
|
}
|
||||||
|
})
|
||||||
async function saveApplication() {
|
|
||||||
error.value = ''
|
async function saveApplication() {
|
||||||
saving.value = true
|
error.value = ''
|
||||||
|
saving.value = true
|
||||||
try {
|
|
||||||
const appData = {
|
try {
|
||||||
appname: form.value.appname,
|
const appData = {
|
||||||
appdescription: form.value.appdescription || null,
|
appname: form.value.appname,
|
||||||
supportteamid: form.value.supportteamid || null,
|
appdescription: form.value.appdescription || null,
|
||||||
isinstallable: form.value.isinstallable,
|
supportteamid: form.value.supportteamid || null,
|
||||||
islicenced: form.value.islicenced,
|
isinstallable: form.value.isinstallable,
|
||||||
isprinter: form.value.isprinter,
|
islicenced: form.value.islicenced,
|
||||||
isrequired: form.value.isrequired,
|
isprinter: form.value.isprinter,
|
||||||
ishidden: form.value.ishidden,
|
isrequired: form.value.isrequired,
|
||||||
applicationlink: form.value.applicationlink || null,
|
ishidden: form.value.ishidden,
|
||||||
documentationpath: form.value.documentationpath || null,
|
applicationlink: form.value.applicationlink || null,
|
||||||
installpath: form.value.installpath || null,
|
documentationpath: form.value.documentationpath || null,
|
||||||
image: form.value.image || null,
|
installpath: form.value.installpath || null,
|
||||||
applicationnotes: form.value.applicationnotes || null
|
image: form.value.image || null,
|
||||||
}
|
applicationnotes: form.value.applicationnotes || null
|
||||||
|
}
|
||||||
if (isEdit.value) {
|
|
||||||
await applicationsApi.update(route.params.id, appData)
|
if (isEdit.value) {
|
||||||
} else {
|
await applicationsApi.update(route.params.id, appData)
|
||||||
await applicationsApi.create(appData)
|
} else {
|
||||||
}
|
await applicationsApi.create(appData)
|
||||||
|
}
|
||||||
router.push('/applications')
|
|
||||||
} catch (err) {
|
router.push('/applications')
|
||||||
console.error('Error saving application:', err)
|
} catch (err) {
|
||||||
error.value = err.response?.data?.message || 'Failed to save application'
|
console.error('Error saving application:', err)
|
||||||
} finally {
|
error.value = apiError(err, 'Failed to save application')
|
||||||
saving.value = false
|
} finally {
|
||||||
}
|
saving.value = false
|
||||||
}
|
}
|
||||||
</script>
|
}
|
||||||
|
</script>
|
||||||
<style scoped>
|
|
||||||
.checkbox-group {
|
<style scoped>
|
||||||
display: flex;
|
.checkbox-group {
|
||||||
gap: 1.5rem;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
gap: 1.5rem;
|
||||||
}
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
.checkbox-group label {
|
|
||||||
display: flex;
|
.checkbox-group label {
|
||||||
align-items: center;
|
display: flex;
|
||||||
gap: 0.5rem;
|
align-items: center;
|
||||||
cursor: pointer;
|
gap: 0.5rem;
|
||||||
}
|
cursor: pointer;
|
||||||
|
}
|
||||||
.form-hint {
|
|
||||||
display: block;
|
.form-hint {
|
||||||
margin-top: 0.25rem;
|
display: block;
|
||||||
font-size: 0.8rem;
|
margin-top: 0.25rem;
|
||||||
color: var(--text-light, #666);
|
font-size: 0.8rem;
|
||||||
}
|
color: var(--text-light, #666);
|
||||||
</style>
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,166 +1,167 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h2>{{ isEdit ? 'Edit Article' : 'Add Knowledge Base Article' }}</h2>
|
<h2>{{ isEdit ? 'Edit Article' : 'Add Knowledge Base Article' }}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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="saveArticle">
|
<form v-else @submit.prevent="saveArticle">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="shortdescription">Description *</label>
|
<label for="shortdescription">Description *</label>
|
||||||
<input
|
<input
|
||||||
id="shortdescription"
|
id="shortdescription"
|
||||||
v-model="form.shortdescription"
|
v-model="form.shortdescription"
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
required
|
required
|
||||||
maxlength="500"
|
maxlength="500"
|
||||||
placeholder="Brief description of the article"
|
placeholder="Brief description of the article"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="linkurl">URL *</label>
|
<label for="linkurl">URL *</label>
|
||||||
<input
|
<input
|
||||||
id="linkurl"
|
id="linkurl"
|
||||||
v-model="form.linkurl"
|
v-model="form.linkurl"
|
||||||
type="url"
|
type="url"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
required
|
required
|
||||||
maxlength="2000"
|
maxlength="2000"
|
||||||
placeholder="https://..."
|
placeholder="https://..."
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="keywords">Keywords</label>
|
<label for="keywords">Keywords</label>
|
||||||
<input
|
<input
|
||||||
id="keywords"
|
id="keywords"
|
||||||
v-model="form.keywords"
|
v-model="form.keywords"
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
maxlength="500"
|
maxlength="500"
|
||||||
placeholder="Space-separated keywords"
|
placeholder="Space-separated keywords"
|
||||||
/>
|
/>
|
||||||
<small class="form-hint">Keywords help with search - separate with spaces</small>
|
<small class="form-hint">Keywords help with search - separate with spaces</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="appid">Topic (Application)</label>
|
<label for="appid">Topic (Application)</label>
|
||||||
<select
|
<select
|
||||||
id="appid"
|
id="appid"
|
||||||
v-model="form.appid"
|
v-model="form.appid"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
>
|
>
|
||||||
<option value="">-- Select Topic (Optional) --</option>
|
<option value="">-- Select Topic (Optional) --</option>
|
||||||
<option
|
<option
|
||||||
v-for="app in applications"
|
v-for="app in applications"
|
||||||
:key="app.appid"
|
:key="app.appid"
|
||||||
:value="app.appid"
|
:value="app.appid"
|
||||||
>
|
>
|
||||||
{{ app.appname }}
|
{{ app.appname }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<small class="form-hint">Select the application/topic this article relates to</small>
|
<small class="form-hint">Select the application/topic this article relates to</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="error" class="error-message">{{ error }}</div>
|
<div v-if="error" class="error-message">{{ error }}</div>
|
||||||
|
|
||||||
<div style="display: flex; gap: 0.5rem; margin-top: 1.5rem;">
|
<div style="display: flex; gap: 0.5rem; margin-top: 1.5rem;">
|
||||||
<button type="submit" class="btn btn-primary" :disabled="saving">
|
<button type="submit" class="btn btn-primary" :disabled="saving">
|
||||||
{{ saving ? 'Saving...' : (isEdit ? 'Update Article' : 'Add Article') }}
|
{{ saving ? 'Saving...' : (isEdit ? 'Update Article' : 'Add Article') }}
|
||||||
</button>
|
</button>
|
||||||
<router-link to="/knowledgebase" class="btn btn-secondary">Cancel</router-link>
|
<router-link to="/knowledgebase" class="btn btn-secondary">Cancel</router-link>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, computed } from 'vue'
|
import { ref, onMounted, computed } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { knowledgebaseApi, applicationsApi } from '../../api'
|
import { knowledgebaseApi, applicationsApi } from '../../api'
|
||||||
|
import { apiError } from '../../utils/apiError'
|
||||||
const route = useRoute()
|
|
||||||
const router = useRouter()
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
const isEdit = computed(() => !!route.params.id)
|
|
||||||
|
const isEdit = computed(() => !!route.params.id)
|
||||||
const loading = ref(true)
|
|
||||||
const saving = ref(false)
|
const loading = ref(true)
|
||||||
const error = ref('')
|
const saving = ref(false)
|
||||||
|
const error = ref('')
|
||||||
const form = ref({
|
|
||||||
shortdescription: '',
|
const form = ref({
|
||||||
linkurl: '',
|
shortdescription: '',
|
||||||
keywords: '',
|
linkurl: '',
|
||||||
appid: ''
|
keywords: '',
|
||||||
})
|
appid: ''
|
||||||
|
})
|
||||||
const applications = ref([])
|
|
||||||
|
const applications = ref([])
|
||||||
onMounted(async () => {
|
|
||||||
try {
|
onMounted(async () => {
|
||||||
// Load applications for topic dropdown
|
try {
|
||||||
const appsRes = await applicationsApi.list({ perpage: 1000 })
|
// Load applications for topic dropdown
|
||||||
applications.value = appsRes.data.data || []
|
const appsRes = await applicationsApi.list({ perpage: 1000 })
|
||||||
|
applications.value = appsRes.data.data || []
|
||||||
// Load article if editing
|
|
||||||
if (isEdit.value) {
|
// Load article if editing
|
||||||
const response = await knowledgebaseApi.get(route.params.id)
|
if (isEdit.value) {
|
||||||
const article = response.data.data
|
const response = await knowledgebaseApi.get(route.params.id)
|
||||||
|
const article = response.data.data
|
||||||
form.value = {
|
|
||||||
shortdescription: article.shortdescription || '',
|
form.value = {
|
||||||
linkurl: article.linkurl || '',
|
shortdescription: article.shortdescription || '',
|
||||||
keywords: article.keywords || '',
|
linkurl: article.linkurl || '',
|
||||||
appid: article.application?.appid || ''
|
keywords: article.keywords || '',
|
||||||
}
|
appid: article.application?.appid || ''
|
||||||
}
|
}
|
||||||
} catch (err) {
|
}
|
||||||
console.error('Error loading data:', err)
|
} catch (err) {
|
||||||
error.value = 'Failed to load data'
|
console.error('Error loading data:', err)
|
||||||
} finally {
|
error.value = 'Failed to load data'
|
||||||
loading.value = false
|
} finally {
|
||||||
}
|
loading.value = false
|
||||||
})
|
}
|
||||||
|
})
|
||||||
async function saveArticle() {
|
|
||||||
error.value = ''
|
async function saveArticle() {
|
||||||
saving.value = true
|
error.value = ''
|
||||||
|
saving.value = true
|
||||||
try {
|
|
||||||
const articleData = {
|
try {
|
||||||
shortdescription: form.value.shortdescription,
|
const articleData = {
|
||||||
linkurl: form.value.linkurl,
|
shortdescription: form.value.shortdescription,
|
||||||
keywords: form.value.keywords || null,
|
linkurl: form.value.linkurl,
|
||||||
appid: form.value.appid || null
|
keywords: form.value.keywords || null,
|
||||||
}
|
appid: form.value.appid || null
|
||||||
|
}
|
||||||
if (isEdit.value) {
|
|
||||||
await knowledgebaseApi.update(route.params.id, articleData)
|
if (isEdit.value) {
|
||||||
} else {
|
await knowledgebaseApi.update(route.params.id, articleData)
|
||||||
await knowledgebaseApi.create(articleData)
|
} else {
|
||||||
}
|
await knowledgebaseApi.create(articleData)
|
||||||
|
}
|
||||||
router.push('/knowledgebase')
|
|
||||||
} catch (err) {
|
router.push('/knowledgebase')
|
||||||
console.error('Error saving article:', err)
|
} catch (err) {
|
||||||
error.value = err.response?.data?.message || 'Failed to save article'
|
console.error('Error saving article:', err)
|
||||||
} finally {
|
error.value = apiError(err, 'Failed to save article')
|
||||||
saving.value = false
|
} finally {
|
||||||
}
|
saving.value = false
|
||||||
}
|
}
|
||||||
</script>
|
}
|
||||||
|
</script>
|
||||||
<style scoped>
|
|
||||||
.form-hint {
|
<style scoped>
|
||||||
display: block;
|
.form-hint {
|
||||||
margin-top: 0.25rem;
|
display: block;
|
||||||
font-size: 0.8rem;
|
margin-top: 0.25rem;
|
||||||
color: var(--text-light, #666);
|
font-size: 0.8rem;
|
||||||
}
|
color: var(--text-light, #666);
|
||||||
</style>
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -30,6 +30,10 @@
|
|||||||
<span class="badge badge-lg" :class="getStatusClass(equipment.statusname)">
|
<span class="badge badge-lg" :class="getStatusClass(equipment.statusname)">
|
||||||
{{ equipment.statusname || 'Unknown' }}
|
{{ equipment.statusname || 'Unknown' }}
|
||||||
</span>
|
</span>
|
||||||
|
<span v-if="heroWarranty" class="badge badge-lg" :style="{ background: heroWarranty.statuscolor, color: '#fff' }"
|
||||||
|
:title="heroWarranty.enddate ? `Warranty ends ${warrantyDate(heroWarranty.enddate)}` : 'Warranty'">
|
||||||
|
{{ heroWarranty.label }}<template v-if="heroWarranty.enddate"> - {{ warrantyDate(heroWarranty.enddate) }}</template>
|
||||||
|
</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="equipment.equipment?.equipmenttypename">
|
||||||
@@ -207,7 +211,7 @@
|
|||||||
<CustomFieldsSection :assetid="equipment.assetid" />
|
<CustomFieldsSection :assetid="equipment.assetid" />
|
||||||
|
|
||||||
<!-- Warranty -->
|
<!-- Warranty -->
|
||||||
<WarrantyPanel :assetid="equipment.assetid" />
|
<WarrantyPanel :assetid="equipment.assetid" :items="warranties" />
|
||||||
|
|
||||||
<!-- Notes -->
|
<!-- Notes -->
|
||||||
<div class="section-card" v-if="equipment.notes">
|
<div class="section-card" v-if="equipment.notes">
|
||||||
@@ -237,6 +241,7 @@ import { equipmentApi, 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 { useWarrantyBadge } from '../../composables/warrantyBadge'
|
||||||
import { useIdentifierFlags } from '../../composables/identifierSettings'
|
import { useIdentifierFlags } from '../../composables/identifierSettings'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -244,6 +249,7 @@ const { isEnabled } = useIdentifierFlags()
|
|||||||
|
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const equipment = ref(null)
|
const equipment = ref(null)
|
||||||
|
const { warranties, heroWarranty, warrantyDate } = useWarrantyBadge(() => equipment.value?.assetid)
|
||||||
const relationships = ref({ incoming: [], outgoing: [] })
|
const relationships = ref({ incoming: [], outgoing: [] })
|
||||||
|
|
||||||
const controllingPc = computed(() => {
|
const controllingPc = computed(() => {
|
||||||
|
|||||||
@@ -353,6 +353,7 @@ import Modal from '../../components/Modal.vue'
|
|||||||
import CustomFieldsInputs from '../../components/CustomFieldsInputs.vue'
|
import CustomFieldsInputs from '../../components/CustomFieldsInputs.vue'
|
||||||
import { currentTheme } from '../../stores/theme'
|
import { currentTheme } from '../../stores/theme'
|
||||||
import { useIdentifierFlags } from '../../composables/identifierSettings'
|
import { useIdentifierFlags } from '../../composables/identifierSettings'
|
||||||
|
import { apiError } from '../../utils/apiError'
|
||||||
|
|
||||||
const { isEnabled } = useIdentifierFlags()
|
const { isEnabled } = useIdentifierFlags()
|
||||||
|
|
||||||
@@ -606,7 +607,7 @@ async function saveEquipment() {
|
|||||||
router.push(`/machines/${savedEquipment.equipment?.equipmentid || route.params.id}`)
|
router.push(`/machines/${savedEquipment.equipment?.equipmentid || route.params.id}`)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error saving equipment:', err)
|
console.error('Error saving equipment:', err)
|
||||||
error.value = err.response?.data?.message || 'Failed to save equipment'
|
error.value = apiError(err, 'Failed to save equipment')
|
||||||
} finally {
|
} finally {
|
||||||
saving.value = false
|
saving.value = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,10 @@
|
|||||||
<span v-if="device.networkdevice?.vendorname" class="meta-item">
|
<span v-if="device.networkdevice?.vendorname" class="meta-item">
|
||||||
{{ device.networkdevice.vendorname }}
|
{{ device.networkdevice.vendorname }}
|
||||||
</span>
|
</span>
|
||||||
|
<span v-if="heroWarranty" class="badge" :style="{ background: heroWarranty.statuscolor, color: '#fff' }"
|
||||||
|
:title="heroWarranty.enddate ? `Warranty ends ${warrantyDate(heroWarranty.enddate)}` : 'Warranty'">
|
||||||
|
{{ heroWarranty.label }}<template v-if="heroWarranty.enddate"> - {{ warrantyDate(heroWarranty.enddate) }}</template>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="hero-details">
|
<div class="hero-details">
|
||||||
<div class="detail-item" v-if="device.assetnumber">
|
<div class="detail-item" v-if="device.assetnumber">
|
||||||
@@ -91,7 +95,7 @@
|
|||||||
<CustomFieldsSection :assetid="device.assetid" />
|
<CustomFieldsSection :assetid="device.assetid" />
|
||||||
|
|
||||||
<!-- Warranty -->
|
<!-- Warranty -->
|
||||||
<WarrantyPanel :assetid="device.assetid" />
|
<WarrantyPanel :assetid="device.assetid" :items="warranties" />
|
||||||
|
|
||||||
<!-- Notes -->
|
<!-- Notes -->
|
||||||
<div class="section-card" v-if="device.notes">
|
<div class="section-card" v-if="device.notes">
|
||||||
@@ -198,6 +202,7 @@ import { networkApi } from '../../api'
|
|||||||
import AssetRelationships from '../../components/AssetRelationships.vue'
|
import AssetRelationships from '../../components/AssetRelationships.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 { useWarrantyBadge } from '../../composables/warrantyBadge'
|
||||||
import { useIdentifierFlags } from '../../composables/identifierSettings'
|
import { useIdentifierFlags } from '../../composables/identifierSettings'
|
||||||
import { useToast } from '../../composables/toast'
|
import { useToast } from '../../composables/toast'
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
@@ -210,6 +215,7 @@ const authStore = useAuthStore()
|
|||||||
|
|
||||||
const deviceId = route.params.id
|
const deviceId = route.params.id
|
||||||
const device = ref(null)
|
const device = ref(null)
|
||||||
|
const { warranties, heroWarranty, warrantyDate } = useWarrantyBadge(() => device.value?.assetid)
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -139,6 +139,7 @@
|
|||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { notificationsApi } from '@/api'
|
import { notificationsApi } from '@/api'
|
||||||
import ColorSwatchPicker from '@/components/ColorSwatchPicker.vue'
|
import ColorSwatchPicker from '@/components/ColorSwatchPicker.vue'
|
||||||
|
import { apiError } from '../../utils/apiError'
|
||||||
|
|
||||||
const types = ref([])
|
const types = ref([])
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
@@ -238,7 +239,7 @@ async function save() {
|
|||||||
editing.value = false
|
editing.value = false
|
||||||
await load()
|
await load()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error.value = err.response?.data?.error?.message || err.response?.data?.message || 'Save failed.'
|
error.value = apiError(err, 'Save failed.')
|
||||||
} finally {
|
} finally {
|
||||||
saving.value = false
|
saving.value = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -254,7 +254,8 @@
|
|||||||
import { ref, onMounted, computed } from 'vue'
|
import { ref, onMounted, computed } from 'vue'
|
||||||
import { colorStyle } from "@/utils/colorStyle"
|
import { colorStyle } from "@/utils/colorStyle"
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { computersApi, applicationsApi, assetsApi, warrantyApi } from '../../api'
|
import { computersApi, applicationsApi, assetsApi } from '../../api'
|
||||||
|
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'
|
||||||
@@ -267,21 +268,9 @@ const loading = ref(true)
|
|||||||
const computer = ref(null)
|
const computer = ref(null)
|
||||||
const relationships = ref({ incoming: [], outgoing: [] })
|
const relationships = ref({ incoming: [], outgoing: [] })
|
||||||
const installedApps = ref([])
|
const installedApps = ref([])
|
||||||
const warranties = ref([])
|
|
||||||
|
|
||||||
// Worst-case warranty for the hero badge: expired > expiring > active > unknown.
|
// Warranty fetch + hero badge (shared across asset detail pages).
|
||||||
const heroWarranty = computed(() => {
|
const { warranties, heroWarranty, warrantyDate: formatWarrantyDate } = useWarrantyBadge(() => computer.value?.assetid)
|
||||||
if (!warranties.value.length) return null
|
|
||||||
const rank = { expired: 0, expiring: 1, active: 2, unknown: 3 }
|
|
||||||
const worst = [...warranties.value].sort((a, b) => (rank[a.status] ?? 9) - (rank[b.status] ?? 9))[0]
|
|
||||||
const labels = { active: 'Under Warranty', expiring: 'Warranty Expiring', expired: 'Warranty Expired', unknown: 'Warranty' }
|
|
||||||
return { ...worst, label: labels[worst.status] || 'Warranty' }
|
|
||||||
})
|
|
||||||
|
|
||||||
function formatWarrantyDate(d) {
|
|
||||||
if (!d) return ''
|
|
||||||
return new Date(d + 'T00:00:00').toLocaleDateString()
|
|
||||||
}
|
|
||||||
|
|
||||||
const controlledEquipment = computed(() => {
|
const controlledEquipment = computed(() => {
|
||||||
// For computers, find related equipment in any "Controls" relationship
|
// For computers, find related equipment in any "Controls" relationship
|
||||||
@@ -334,16 +323,7 @@ onMounted(async () => {
|
|||||||
} catch (appError) {
|
} catch (appError) {
|
||||||
console.log('No installed apps data:', appError.message)
|
console.log('No installed apps data:', appError.message)
|
||||||
}
|
}
|
||||||
|
// Warranties load via useWarrantyBadge (watches computer.assetid).
|
||||||
// Load warranties (one fetch; feeds both the hero badge and the panel)
|
|
||||||
if (computer.value?.assetid) {
|
|
||||||
try {
|
|
||||||
const warrantyResponse = await warrantyApi.forAsset(computer.value.assetid)
|
|
||||||
warranties.value = warrantyResponse.data.data || []
|
|
||||||
} catch (warrantyError) {
|
|
||||||
console.log('No warranty data:', warrantyError.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading computer:', error)
|
console.error('Error loading computer:', error)
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -25,6 +25,10 @@
|
|||||||
<span v-if="printer.printer?.iscsf" class="badge badge-lg badge-info">CSF</span>
|
<span v-if="printer.printer?.iscsf" class="badge badge-lg badge-info">CSF</span>
|
||||||
<span v-if="printer.printer?.iscolor" class="badge badge-lg badge-success">Color</span>
|
<span v-if="printer.printer?.iscolor" class="badge badge-lg badge-success">Color</span>
|
||||||
<span v-if="printer.printer?.isnetwork" class="badge badge-lg badge-secondary">Network</span>
|
<span v-if="printer.printer?.isnetwork" class="badge badge-lg badge-secondary">Network</span>
|
||||||
|
<span v-if="heroWarranty" class="badge badge-lg" :style="{ background: heroWarranty.statuscolor, color: '#fff' }"
|
||||||
|
:title="heroWarranty.enddate ? `Warranty ends ${warrantyDate(heroWarranty.enddate)}` : 'Warranty'">
|
||||||
|
{{ heroWarranty.label }}<template v-if="heroWarranty.enddate"> - {{ warrantyDate(heroWarranty.enddate) }}</template>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="hero-details">
|
<div class="hero-details">
|
||||||
<div class="hero-detail" v-if="printer.printer?.vendorname">
|
<div class="hero-detail" v-if="printer.printer?.vendorname">
|
||||||
@@ -121,7 +125,7 @@
|
|||||||
<CustomFieldsSection :assetid="printer.assetid" />
|
<CustomFieldsSection :assetid="printer.assetid" />
|
||||||
|
|
||||||
<!-- Warranty -->
|
<!-- Warranty -->
|
||||||
<WarrantyPanel :assetid="printer.assetid" />
|
<WarrantyPanel :assetid="printer.assetid" :items="warranties" />
|
||||||
|
|
||||||
<!-- Notes -->
|
<!-- Notes -->
|
||||||
<div class="section-card" v-if="printer.notes">
|
<div class="section-card" v-if="printer.notes">
|
||||||
@@ -263,6 +267,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 { useWarrantyBadge } from '../../composables/warrantyBadge'
|
||||||
import { useIdentifierFlags } from '../../composables/identifierSettings'
|
import { useIdentifierFlags } from '../../composables/identifierSettings'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -270,6 +275,7 @@ const { isEnabled } = useIdentifierFlags()
|
|||||||
|
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const printer = ref(null)
|
const printer = ref(null)
|
||||||
|
const { warranties, heroWarranty, warrantyDate } = useWarrantyBadge(() => printer.value?.assetid)
|
||||||
const supplies = ref([])
|
const supplies = ref([])
|
||||||
const drivers = ref([])
|
const drivers = ref([])
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -33,7 +33,7 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="w in buckets[b.key]" :key="w.warrantyid">
|
<tr v-for="w in buckets[b.key]" :key="w.warrantyid">
|
||||||
<td><strong>{{ w.vendor }}</strong></td>
|
<td><strong>{{ w.vendor }}</strong></td>
|
||||||
<td>{{ w.servicelevel || '-' }}</td>
|
<td class="servicelevel-cell" :title="w.servicelevel">{{ w.servicelevel || '-' }}</td>
|
||||||
<td>{{ w.enddate ? formatDate(w.enddate) : '-' }}</td>
|
<td>{{ w.enddate ? formatDate(w.enddate) : '-' }}</td>
|
||||||
<td>
|
<td>
|
||||||
<router-link v-for="a in w.assets" :key="a.assetid" :to="assetLink(a)" class="asset-chip">{{ a.assetnumber }}</router-link>
|
<router-link v-for="a in w.assets" :key="a.assetid" :to="assetLink(a)" class="asset-chip">{{ a.assetnumber }}</router-link>
|
||||||
@@ -100,4 +100,5 @@ onMounted(async () => {
|
|||||||
background: var(--bg); border-radius: 12px; font-size: 0.8rem; text-decoration: none; color: var(--text);
|
background: var(--bg); border-radius: 12px; font-size: 0.8rem; text-decoration: none; color: var(--text);
|
||||||
}
|
}
|
||||||
.muted { color: var(--text-light); }
|
.muted { color: var(--text-light); }
|
||||||
|
.servicelevel-cell { max-width: 320px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -92,6 +92,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { computersApi } from '@/api'
|
import { computersApi } from '@/api'
|
||||||
|
import { apiError } from '../../utils/apiError'
|
||||||
|
|
||||||
const protocols = ref([])
|
const protocols = ref([])
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
@@ -151,7 +152,7 @@ async function save() {
|
|||||||
editing.value = false
|
editing.value = false
|
||||||
await load()
|
await load()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error.value = err.response?.data?.error?.message || err.response?.data?.message || 'Save failed.'
|
error.value = apiError(err, 'Save failed.')
|
||||||
} finally {
|
} finally {
|
||||||
saving.value = false
|
saving.value = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ import { ref, onMounted } from 'vue'
|
|||||||
import { assetsApi } from '../../api'
|
import { assetsApi } from '../../api'
|
||||||
import ColorSwatchPicker from '@/components/ColorSwatchPicker.vue'
|
import ColorSwatchPicker from '@/components/ColorSwatchPicker.vue'
|
||||||
import { colorStyle } from '@/utils/colorStyle'
|
import { colorStyle } from '@/utils/colorStyle'
|
||||||
|
import { apiError } from '../../utils/apiError'
|
||||||
|
|
||||||
const items = ref([])
|
const items = ref([])
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
@@ -106,7 +107,7 @@ async function save() {
|
|||||||
closeModal()
|
closeModal()
|
||||||
loadData()
|
loadData()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error.value = err.response?.data?.error?.message || err.response?.data?.message || 'Failed to save'
|
error.value = apiError(err, 'Failed to save')
|
||||||
} finally {
|
} finally {
|
||||||
saving.value = false
|
saving.value = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ import { ref, onMounted } from 'vue'
|
|||||||
import { businessunitsApi } from '../../api'
|
import { businessunitsApi } 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'
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
const items = ref([])
|
const items = ref([])
|
||||||
@@ -168,7 +169,7 @@ async function save() {
|
|||||||
closeModal()
|
closeModal()
|
||||||
loadData()
|
loadData()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error.value = err.response?.data?.message || 'Failed to save'
|
error.value = apiError(err, 'Failed to save')
|
||||||
} finally {
|
} finally {
|
||||||
saving.value = false
|
saving.value = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,6 +115,7 @@
|
|||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { assetsApi, customFieldsApi } from '../../api'
|
import { assetsApi, customFieldsApi } from '../../api'
|
||||||
import { useToast } from '../../composables/toast'
|
import { useToast } from '../../composables/toast'
|
||||||
|
import { apiError } from '../../utils/apiError'
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
const assetTypes = ref([])
|
const assetTypes = ref([])
|
||||||
@@ -196,7 +197,7 @@ async function save() {
|
|||||||
closeModal()
|
closeModal()
|
||||||
loadFields()
|
loadFields()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error.value = err.response?.data?.error?.message || err.response?.data?.message || 'Failed to save'
|
error.value = apiError(err, 'Failed to save')
|
||||||
} finally {
|
} finally {
|
||||||
saving.value = false
|
saving.value = false
|
||||||
}
|
}
|
||||||
@@ -208,7 +209,7 @@ async function deleteField(f) {
|
|||||||
await customFieldsApi.remove(f.fieldid)
|
await customFieldsApi.remove(f.fieldid)
|
||||||
loadFields()
|
loadFields()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(err.response?.data?.error?.message || err.response?.data?.message || 'Failed to delete')
|
toast.error(apiError(err, 'Failed to delete'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -105,6 +105,7 @@
|
|||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { dashboardDefaultsApi, businessUnitsApi } from '../../api'
|
import { dashboardDefaultsApi, businessUnitsApi } from '../../api'
|
||||||
import { useToast } from '../../composables/toast'
|
import { useToast } from '../../composables/toast'
|
||||||
|
import { apiError } from '../../utils/apiError'
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
const items = ref([])
|
const items = ref([])
|
||||||
@@ -168,7 +169,7 @@ async function save() {
|
|||||||
closeModal()
|
closeModal()
|
||||||
loadData()
|
loadData()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error.value = err.response?.data?.message || 'Failed to save'
|
error.value = apiError(err, 'Failed to save')
|
||||||
} finally {
|
} finally {
|
||||||
saving.value = false
|
saving.value = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ import { equipmentApi } from '../../api'
|
|||||||
import ColorSwatchPicker from '@/components/ColorSwatchPicker.vue'
|
import ColorSwatchPicker from '@/components/ColorSwatchPicker.vue'
|
||||||
import { colorStyle } from '@/utils/colorStyle'
|
import { colorStyle } from '@/utils/colorStyle'
|
||||||
import { useToast } from '../../composables/toast'
|
import { useToast } from '../../composables/toast'
|
||||||
|
import { apiError } from '../../utils/apiError'
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
const items = ref([])
|
const items = ref([])
|
||||||
@@ -126,7 +127,7 @@ async function save() {
|
|||||||
closeModal()
|
closeModal()
|
||||||
loadData()
|
loadData()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error.value = err.response?.data?.error?.message || err.response?.data?.message || 'Failed to save'
|
error.value = apiError(err, 'Failed to save')
|
||||||
} finally {
|
} finally {
|
||||||
saving.value = false
|
saving.value = false
|
||||||
}
|
}
|
||||||
@@ -138,7 +139,7 @@ async function deleteType(t) {
|
|||||||
await equipmentApi.types.remove(t.equipmenttypeid)
|
await equipmentApi.types.remove(t.equipmenttypeid)
|
||||||
loadData()
|
loadData()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(err.response?.data?.error?.message || err.response?.data?.message || 'Failed to delete')
|
toast.error(apiError(err, 'Failed to delete'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ import { locationsApi } from '../../api'
|
|||||||
import ColorSwatchPicker from '@/components/ColorSwatchPicker.vue'
|
import ColorSwatchPicker from '@/components/ColorSwatchPicker.vue'
|
||||||
import { colorStyle } from '@/utils/colorStyle'
|
import { colorStyle } from '@/utils/colorStyle'
|
||||||
import { useToast } from '../../composables/toast'
|
import { useToast } from '../../composables/toast'
|
||||||
|
import { apiError } from '../../utils/apiError'
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
const items = ref([])
|
const items = ref([])
|
||||||
@@ -126,7 +127,7 @@ async function save() {
|
|||||||
closeModal()
|
closeModal()
|
||||||
loadData()
|
loadData()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error.value = err.response?.data?.error?.message || err.response?.data?.message || 'Failed to save'
|
error.value = apiError(err, 'Failed to save')
|
||||||
} finally {
|
} finally {
|
||||||
saving.value = false
|
saving.value = false
|
||||||
}
|
}
|
||||||
@@ -138,7 +139,7 @@ async function deleteType(t) {
|
|||||||
await locationsApi.types.remove(t.locationtypeid)
|
await locationsApi.types.remove(t.locationtypeid)
|
||||||
loadData()
|
loadData()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(err.response?.data?.error?.message || err.response?.data?.message || 'Failed to delete')
|
toast.error(apiError(err, 'Failed to delete'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -212,6 +212,7 @@ import { ref, computed, onMounted } from 'vue'
|
|||||||
import { locationsApi } from '../../api'
|
import { locationsApi } 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'
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
const locations = ref([])
|
const locations = ref([])
|
||||||
@@ -354,7 +355,7 @@ async function saveLocation() {
|
|||||||
loadLocations()
|
loadLocations()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error saving location:', err)
|
console.error('Error saving location:', err)
|
||||||
error.value = err.response?.data?.message || 'Failed to save location'
|
error.value = apiError(err, 'Failed to save location')
|
||||||
} finally {
|
} finally {
|
||||||
saving.value = false
|
saving.value = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -146,6 +146,7 @@ import { ref, onMounted } from 'vue'
|
|||||||
import { machinetypesApi } from '../../api'
|
import { machinetypesApi } 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'
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
const machineTypes = ref([])
|
const machineTypes = ref([])
|
||||||
@@ -239,7 +240,7 @@ async function saveType() {
|
|||||||
loadTypes()
|
loadTypes()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error saving machine type:', err)
|
console.error('Error saving machine type:', err)
|
||||||
error.value = err.response?.data?.message || 'Failed to save machine type'
|
error.value = apiError(err, 'Failed to save machine type')
|
||||||
} finally {
|
} finally {
|
||||||
saving.value = false
|
saving.value = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -147,6 +147,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { printersApi } from '@/api'
|
import { printersApi } from '@/api'
|
||||||
|
import { apiError } from '../../utils/apiError'
|
||||||
|
|
||||||
const models = ref([])
|
const models = ref([])
|
||||||
const loadingModels = ref(true)
|
const loadingModels = ref(true)
|
||||||
@@ -246,7 +247,7 @@ async function save() {
|
|||||||
await selectModel(selectedModel.value)
|
await selectModel(selectedModel.value)
|
||||||
await loadModels()
|
await loadModels()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
formError.value = err.response?.data?.error?.message || 'Save failed.'
|
formError.value = apiError(err, 'Save failed.')
|
||||||
} finally {
|
} finally {
|
||||||
saving.value = false
|
saving.value = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -184,6 +184,7 @@ import { ref, onMounted } from 'vue'
|
|||||||
import { modelsApi, vendorsApi, machinetypesApi } from '../../api'
|
import { modelsApi, vendorsApi, machinetypesApi } 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'
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
const models = ref([])
|
const models = ref([])
|
||||||
@@ -328,7 +329,7 @@ async function saveModel() {
|
|||||||
loadModels()
|
loadModels()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error saving model:', err)
|
console.error('Error saving model:', err)
|
||||||
error.value = err.response?.data?.message || 'Failed to save model'
|
error.value = apiError(err, 'Failed to save model')
|
||||||
} finally {
|
} finally {
|
||||||
saving.value = false
|
saving.value = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ import { networkApi } from '../../api'
|
|||||||
import ColorSwatchPicker from '@/components/ColorSwatchPicker.vue'
|
import ColorSwatchPicker from '@/components/ColorSwatchPicker.vue'
|
||||||
import { colorStyle } from '@/utils/colorStyle'
|
import { colorStyle } from '@/utils/colorStyle'
|
||||||
import { useToast } from '../../composables/toast'
|
import { useToast } from '../../composables/toast'
|
||||||
|
import { apiError } from '../../utils/apiError'
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
const items = ref([])
|
const items = ref([])
|
||||||
@@ -126,7 +127,7 @@ async function save() {
|
|||||||
closeModal()
|
closeModal()
|
||||||
loadData()
|
loadData()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error.value = err.response?.data?.error?.message || err.response?.data?.message || 'Failed to save'
|
error.value = apiError(err, 'Failed to save')
|
||||||
} finally {
|
} finally {
|
||||||
saving.value = false
|
saving.value = false
|
||||||
}
|
}
|
||||||
@@ -138,7 +139,7 @@ async function deleteType(t) {
|
|||||||
await networkApi.types.remove(t.networkdevicetypeid)
|
await networkApi.types.remove(t.networkdevicetypeid)
|
||||||
loadData()
|
loadData()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(err.response?.data?.error?.message || err.response?.data?.message || 'Failed to delete')
|
toast.error(apiError(err, 'Failed to delete'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ import { ref, onMounted } from 'vue'
|
|||||||
import { operatingsystemsApi } from '../../api'
|
import { operatingsystemsApi } 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'
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
const items = ref([])
|
const items = ref([])
|
||||||
@@ -193,7 +194,7 @@ async function save() {
|
|||||||
closeModal()
|
closeModal()
|
||||||
loadData()
|
loadData()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error.value = err.response?.data?.message || 'Failed to save'
|
error.value = apiError(err, 'Failed to save')
|
||||||
} finally {
|
} finally {
|
||||||
saving.value = false
|
saving.value = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ import { computersApi } from '../../api'
|
|||||||
import ColorSwatchPicker from '@/components/ColorSwatchPicker.vue'
|
import ColorSwatchPicker from '@/components/ColorSwatchPicker.vue'
|
||||||
import { colorStyle } from '@/utils/colorStyle'
|
import { colorStyle } from '@/utils/colorStyle'
|
||||||
import { useToast } from '../../composables/toast'
|
import { useToast } from '../../composables/toast'
|
||||||
|
import { apiError } from '../../utils/apiError'
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
const pcTypes = ref([])
|
const pcTypes = ref([])
|
||||||
@@ -132,7 +133,7 @@ async function deleteType(pt) {
|
|||||||
await computersApi.types.remove(pt.computertypeid)
|
await computersApi.types.remove(pt.computertypeid)
|
||||||
loadData()
|
loadData()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(err.response?.data?.error?.message || err.response?.data?.message || 'Failed to delete')
|
toast.error(apiError(err, 'Failed to delete'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,7 +149,7 @@ async function save() {
|
|||||||
closeModal()
|
closeModal()
|
||||||
loadData()
|
loadData()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error.value = err.response?.data?.message || 'Failed to save'
|
error.value = apiError(err, 'Failed to save')
|
||||||
} finally {
|
} finally {
|
||||||
saving.value = false
|
saving.value = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,6 +98,7 @@
|
|||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { printersApi } from '../../api'
|
import { printersApi } from '../../api'
|
||||||
import { useToast } from '../../composables/toast'
|
import { useToast } from '../../composables/toast'
|
||||||
|
import { apiError } from '../../utils/apiError'
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
const items = ref([])
|
const items = ref([])
|
||||||
@@ -161,7 +162,7 @@ async function save() {
|
|||||||
closeModal()
|
closeModal()
|
||||||
loadData()
|
loadData()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error.value = err.response?.data?.error?.message || err.response?.data?.message || 'Failed to save'
|
error.value = apiError(err, 'Failed to save')
|
||||||
} finally {
|
} finally {
|
||||||
saving.value = false
|
saving.value = false
|
||||||
}
|
}
|
||||||
@@ -173,7 +174,7 @@ async function deleteDriver(d) {
|
|||||||
await printersApi.drivers.delete(d.driverid)
|
await printersApi.drivers.delete(d.driverid)
|
||||||
loadData()
|
loadData()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(err.response?.data?.error?.message || err.response?.data?.message || 'Failed to delete')
|
toast.error(apiError(err, 'Failed to delete'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ import { printersApi } from '../../api'
|
|||||||
import ColorSwatchPicker from '@/components/ColorSwatchPicker.vue'
|
import ColorSwatchPicker from '@/components/ColorSwatchPicker.vue'
|
||||||
import { colorStyle } from '@/utils/colorStyle'
|
import { colorStyle } from '@/utils/colorStyle'
|
||||||
import { useToast } from '../../composables/toast'
|
import { useToast } from '../../composables/toast'
|
||||||
|
import { apiError } from '../../utils/apiError'
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
const items = ref([])
|
const items = ref([])
|
||||||
@@ -126,7 +127,7 @@ async function save() {
|
|||||||
closeModal()
|
closeModal()
|
||||||
loadData()
|
loadData()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error.value = err.response?.data?.error?.message || err.response?.data?.message || 'Failed to save'
|
error.value = apiError(err, 'Failed to save')
|
||||||
} finally {
|
} finally {
|
||||||
saving.value = false
|
saving.value = false
|
||||||
}
|
}
|
||||||
@@ -138,7 +139,7 @@ async function deleteType(t) {
|
|||||||
await printersApi.types.remove(t.printertypeid)
|
await printersApi.types.remove(t.printertypeid)
|
||||||
loadData()
|
loadData()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(err.response?.data?.error?.message || err.response?.data?.message || 'Failed to delete')
|
toast.error(apiError(err, 'Failed to delete'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ import { relationshipTypesApi } from '../../api'
|
|||||||
import ColorSwatchPicker from '@/components/ColorSwatchPicker.vue'
|
import ColorSwatchPicker from '@/components/ColorSwatchPicker.vue'
|
||||||
import { colorStyle } from '@/utils/colorStyle'
|
import { colorStyle } from '@/utils/colorStyle'
|
||||||
import { useToast } from '../../composables/toast'
|
import { useToast } from '../../composables/toast'
|
||||||
|
import { apiError } from '../../utils/apiError'
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
const items = ref([])
|
const items = ref([])
|
||||||
@@ -126,7 +127,7 @@ async function save() {
|
|||||||
closeModal()
|
closeModal()
|
||||||
loadData()
|
loadData()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error.value = err.response?.data?.error?.message || err.response?.data?.message || 'Failed to save'
|
error.value = apiError(err, 'Failed to save')
|
||||||
} finally {
|
} finally {
|
||||||
saving.value = false
|
saving.value = false
|
||||||
}
|
}
|
||||||
@@ -138,7 +139,7 @@ async function deleteType(t) {
|
|||||||
await relationshipTypesApi.remove(t.relationshiptypeid)
|
await relationshipTypesApi.remove(t.relationshiptypeid)
|
||||||
loadData()
|
loadData()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(err.response?.data?.error?.message || err.response?.data?.message || 'Failed to delete')
|
toast.error(apiError(err, 'Failed to delete'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { settingsApi } from '@/api'
|
import { settingsApi } from '@/api'
|
||||||
|
import { apiError } from '../../utils/apiError'
|
||||||
|
|
||||||
const items = ref([])
|
const items = ref([])
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
@@ -71,7 +72,7 @@ async function save() {
|
|||||||
}
|
}
|
||||||
saved.value = true
|
saved.value = true
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error.value = err.response?.data?.error?.message || 'Save failed.'
|
error.value = apiError(err, 'Save failed.')
|
||||||
} finally {
|
} finally {
|
||||||
saving.value = false
|
saving.value = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ import { assetsApi } from '../../api'
|
|||||||
import PaginationBar from '../../components/PaginationBar.vue'
|
import PaginationBar from '../../components/PaginationBar.vue'
|
||||||
import ColorSwatchPicker from '@/components/ColorSwatchPicker.vue'
|
import ColorSwatchPicker from '@/components/ColorSwatchPicker.vue'
|
||||||
import { useToast } from '../../composables/toast'
|
import { useToast } from '../../composables/toast'
|
||||||
|
import { apiError } from '../../utils/apiError'
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
const statuses = ref([])
|
const statuses = ref([])
|
||||||
@@ -233,7 +234,7 @@ async function saveStatus() {
|
|||||||
loadStatuses()
|
loadStatuses()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error saving status:', err)
|
console.error('Error saving status:', err)
|
||||||
error.value = err.response?.data?.message || 'Failed to save status'
|
error.value = apiError(err, 'Failed to save status')
|
||||||
} finally {
|
} finally {
|
||||||
saving.value = false
|
saving.value = false
|
||||||
}
|
}
|
||||||
@@ -252,7 +253,7 @@ async function deleteStatus() {
|
|||||||
loadStatuses()
|
loadStatuses()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error deleting status:', err)
|
console.error('Error deleting status:', err)
|
||||||
error.value = err.response?.data?.message || 'Failed to delete status'
|
error.value = apiError(err, 'Failed to delete status')
|
||||||
toast.error(error.value)
|
toast.error(error.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -286,6 +286,7 @@ import { useRoute } from 'vue-router'
|
|||||||
import { networkApi, locationsApi } from '../../api'
|
import { networkApi, locationsApi } 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'
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -472,7 +473,7 @@ async function saveSubnet() {
|
|||||||
loadSubnets()
|
loadSubnets()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error saving subnet:', err)
|
console.error('Error saving subnet:', err)
|
||||||
error.value = err.response?.data?.message || 'Failed to save subnet'
|
error.value = apiError(err, 'Failed to save subnet')
|
||||||
} finally {
|
} finally {
|
||||||
saving.value = false
|
saving.value = false
|
||||||
}
|
}
|
||||||
@@ -491,7 +492,7 @@ async function deleteSubnet() {
|
|||||||
loadSubnets()
|
loadSubnets()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error deleting subnet:', err)
|
console.error('Error deleting subnet:', err)
|
||||||
toast.error(err.response?.data?.message || 'Failed to delete subnet')
|
toast.error(apiError(err, 'Failed to delete subnet'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -657,6 +657,7 @@ import { ref, reactive, onMounted, computed, watch } from 'vue'
|
|||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { settingsApi, computersApi } from '../../api'
|
import { settingsApi, computersApi } from '../../api'
|
||||||
import { setIdentifierFlag } from '../../composables/identifierSettings'
|
import { setIdentifierFlag } from '../../composables/identifierSettings'
|
||||||
|
import { apiError } from '../../utils/apiError'
|
||||||
|
|
||||||
// Section tabs - one section visible at a time to avoid a long scroll. The
|
// Section tabs - one section visible at a time to avoid a long scroll. The
|
||||||
// search box filters tabs (and, while searching, shows every matching section).
|
// search box filters tabs (and, while searching, shows every matching section).
|
||||||
@@ -900,7 +901,7 @@ async function changePcTypeMapping(pxetype, computertype) {
|
|||||||
success.value = 'Setting saved'
|
success.value = 'Setting saved'
|
||||||
setTimeout(() => { success.value = '' }, 2000)
|
setTimeout(() => { success.value = '' }, 2000)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = e.response?.data?.message || 'Failed to save setting'
|
error.value = apiError(e, 'Failed to save setting')
|
||||||
console.error(e)
|
console.error(e)
|
||||||
} finally {
|
} finally {
|
||||||
saving.value = false
|
saving.value = false
|
||||||
@@ -943,7 +944,7 @@ async function toggleIdentifier(name, assettype) {
|
|||||||
success.value = 'Setting saved'
|
success.value = 'Setting saved'
|
||||||
setTimeout(() => { success.value = '' }, 2000)
|
setTimeout(() => { success.value = '' }, 2000)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = e.response?.data?.message || 'Failed to save setting'
|
error.value = apiError(e, 'Failed to save setting')
|
||||||
console.error(e)
|
console.error(e)
|
||||||
} finally {
|
} finally {
|
||||||
saving.value = false
|
saving.value = false
|
||||||
@@ -977,7 +978,7 @@ async function toggleSearchDomain(domainKey) {
|
|||||||
success.value = 'Setting saved'
|
success.value = 'Setting saved'
|
||||||
setTimeout(() => { success.value = '' }, 2000)
|
setTimeout(() => { success.value = '' }, 2000)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = e.response?.data?.message || 'Failed to save setting'
|
error.value = apiError(e, 'Failed to save setting')
|
||||||
console.error(e)
|
console.error(e)
|
||||||
} finally {
|
} finally {
|
||||||
saving.value = false
|
saving.value = false
|
||||||
@@ -996,7 +997,7 @@ async function saveSetting(key, value) {
|
|||||||
success.value = 'Setting saved'
|
success.value = 'Setting saved'
|
||||||
setTimeout(() => { success.value = '' }, 2000)
|
setTimeout(() => { success.value = '' }, 2000)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = e.response?.data?.message || 'Failed to save setting'
|
error.value = apiError(e, 'Failed to save setting')
|
||||||
console.error(e)
|
console.error(e)
|
||||||
} finally {
|
} finally {
|
||||||
saving.value = false
|
saving.value = false
|
||||||
@@ -1013,7 +1014,7 @@ async function testEmail() {
|
|||||||
// await settingsApi.testEmail()
|
// await settingsApi.testEmail()
|
||||||
success.value = 'Test email feature coming soon'
|
success.value = 'Test email feature coming soon'
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = e.response?.data?.message || 'Failed to send test email'
|
error.value = apiError(e, 'Failed to send test email')
|
||||||
} finally {
|
} finally {
|
||||||
testingEmail.value = false
|
testingEmail.value = false
|
||||||
setTimeout(() => { success.value = '' }, 3000)
|
setTimeout(() => { success.value = '' }, 3000)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -188,6 +188,7 @@ import { ref, onMounted } from 'vue'
|
|||||||
import { networkApi } from '../../api'
|
import { networkApi } 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'
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
const vlans = ref([])
|
const vlans = ref([])
|
||||||
@@ -309,7 +310,7 @@ async function saveVLAN() {
|
|||||||
loadVLANs()
|
loadVLANs()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error saving VLAN:', err)
|
console.error('Error saving VLAN:', err)
|
||||||
error.value = err.response?.data?.message || 'Failed to save VLAN'
|
error.value = apiError(err, 'Failed to save VLAN')
|
||||||
} finally {
|
} finally {
|
||||||
saving.value = false
|
saving.value = false
|
||||||
}
|
}
|
||||||
@@ -328,7 +329,7 @@ async function deleteVLAN() {
|
|||||||
loadVLANs()
|
loadVLANs()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error deleting VLAN:', err)
|
console.error('Error deleting VLAN:', err)
|
||||||
toast.error(err.response?.data?.message || 'Failed to delete VLAN')
|
toast.error(apiError(err, 'Failed to delete VLAN'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -169,6 +169,7 @@ import { useRoute } from 'vue-router'
|
|||||||
import { usbApi } from '../../api'
|
import { usbApi } from '../../api'
|
||||||
import Modal from '../../components/Modal.vue'
|
import Modal from '../../components/Modal.vue'
|
||||||
import { useToast } from '../../composables/toast'
|
import { useToast } from '../../composables/toast'
|
||||||
|
import { apiError } from '../../utils/apiError'
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -219,7 +220,7 @@ async function doCheckout() {
|
|||||||
await loadDevice()
|
await loadDevice()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Checkout error:', error)
|
console.error('Checkout error:', error)
|
||||||
toast.error(error.response?.data?.message || 'Checkout failed')
|
toast.error(apiError(error, 'Checkout failed'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,7 +231,7 @@ async function doCheckin() {
|
|||||||
await loadDevice()
|
await loadDevice()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Checkin error:', error)
|
console.error('Checkin error:', error)
|
||||||
toast.error(error.response?.data?.message || 'Check in failed')
|
toast.error(apiError(error, 'Check in failed'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,230 +1,231 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h2>{{ isEdit ? 'Edit USB Device' : 'Add USB Device' }}</h2>
|
<h2>{{ isEdit ? 'Edit USB Device' : 'Add USB Device' }}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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="saveDevice">
|
<form v-else @submit.prevent="saveDevice">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="serialnumber">Serial Number *</label>
|
<label for="serialnumber">Serial Number *</label>
|
||||||
<input
|
<input
|
||||||
id="serialnumber"
|
id="serialnumber"
|
||||||
v-model="form.serialnumber"
|
v-model="form.serialnumber"
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
required
|
required
|
||||||
maxlength="100"
|
maxlength="100"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="displayname">Display Name *</label>
|
<label for="displayname">Display Name *</label>
|
||||||
<input
|
<input
|
||||||
id="displayname"
|
id="displayname"
|
||||||
v-model="form.displayname"
|
v-model="form.displayname"
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
required
|
required
|
||||||
maxlength="100"
|
maxlength="100"
|
||||||
placeholder="e.g., USB Flash Drive #1"
|
placeholder="e.g., USB Flash Drive #1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="label">Label</label>
|
<label for="label">Label</label>
|
||||||
<input
|
<input
|
||||||
id="label"
|
id="label"
|
||||||
v-model="form.label"
|
v-model="form.label"
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
maxlength="100"
|
maxlength="100"
|
||||||
placeholder="Physical label on device"
|
placeholder="Physical label on device"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="usbtypeid">Device Type</label>
|
<label for="usbtypeid">Device Type</label>
|
||||||
<select
|
<select
|
||||||
id="usbtypeid"
|
id="usbtypeid"
|
||||||
v-model="form.usbtypeid"
|
v-model="form.usbtypeid"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
>
|
>
|
||||||
<option value="">-- Select Type --</option>
|
<option value="">-- Select Type --</option>
|
||||||
<option
|
<option
|
||||||
v-for="type in types"
|
v-for="type in types"
|
||||||
:key="type.usbtypeid"
|
:key="type.usbtypeid"
|
||||||
:value="type.usbtypeid"
|
:value="type.usbtypeid"
|
||||||
>
|
>
|
||||||
{{ type.typename }}
|
{{ type.typename }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="capacitygb">Capacity (GB)</label>
|
<label for="capacitygb">Capacity (GB)</label>
|
||||||
<input
|
<input
|
||||||
id="capacitygb"
|
id="capacitygb"
|
||||||
v-model.number="form.capacitygb"
|
v-model.number="form.capacitygb"
|
||||||
type="number"
|
type="number"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
min="0"
|
min="0"
|
||||||
step="1"
|
step="1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="vendorid">Vendor</label>
|
<label for="vendorid">Vendor</label>
|
||||||
<select
|
<select
|
||||||
id="vendorid"
|
id="vendorid"
|
||||||
v-model="form.vendorid"
|
v-model="form.vendorid"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
>
|
>
|
||||||
<option value="">-- Select Vendor --</option>
|
<option value="">-- Select Vendor --</option>
|
||||||
<option
|
<option
|
||||||
v-for="vendor in vendors"
|
v-for="vendor in vendors"
|
||||||
:key="vendor.vendorid"
|
:key="vendor.vendorid"
|
||||||
:value="vendor.vendorid"
|
:value="vendor.vendorid"
|
||||||
>
|
>
|
||||||
{{ vendor.vendorname }}
|
{{ vendor.vendorname }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="notes">Notes</label>
|
<label for="notes">Notes</label>
|
||||||
<textarea
|
<textarea
|
||||||
id="notes"
|
id="notes"
|
||||||
v-model="form.notes"
|
v-model="form.notes"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
rows="3"
|
rows="3"
|
||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="error" class="error-message">{{ error }}</div>
|
<div v-if="error" class="error-message">{{ error }}</div>
|
||||||
|
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button type="submit" class="btn btn-primary" :disabled="saving">
|
<button type="submit" class="btn btn-primary" :disabled="saving">
|
||||||
{{ saving ? 'Saving...' : (isEdit ? 'Update Device' : 'Add Device') }}
|
{{ saving ? 'Saving...' : (isEdit ? 'Update Device' : 'Add Device') }}
|
||||||
</button>
|
</button>
|
||||||
<router-link to="/usb" class="btn btn-secondary">Cancel</router-link>
|
<router-link to="/usb" class="btn btn-secondary">Cancel</router-link>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, computed } from 'vue'
|
import { ref, onMounted, computed } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { usbApi, vendorsApi } from '@/api'
|
import { usbApi, vendorsApi } from '@/api'
|
||||||
|
import { apiError } from '../../utils/apiError'
|
||||||
const route = useRoute()
|
|
||||||
const router = useRouter()
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
const isEdit = computed(() => !!route.params.id)
|
|
||||||
|
const isEdit = computed(() => !!route.params.id)
|
||||||
const loading = ref(true)
|
|
||||||
const saving = ref(false)
|
const loading = ref(true)
|
||||||
const error = ref('')
|
const saving = ref(false)
|
||||||
|
const error = ref('')
|
||||||
const types = ref([])
|
|
||||||
const vendors = ref([])
|
const types = ref([])
|
||||||
|
const vendors = ref([])
|
||||||
const form = ref({
|
|
||||||
serialnumber: '',
|
const form = ref({
|
||||||
displayname: '',
|
serialnumber: '',
|
||||||
label: '',
|
displayname: '',
|
||||||
usbtypeid: '',
|
label: '',
|
||||||
capacitygb: null,
|
usbtypeid: '',
|
||||||
vendorid: '',
|
capacitygb: null,
|
||||||
notes: ''
|
vendorid: '',
|
||||||
})
|
notes: ''
|
||||||
|
})
|
||||||
onMounted(async () => {
|
|
||||||
try {
|
onMounted(async () => {
|
||||||
// Load types and vendors
|
try {
|
||||||
const [typesRes, vendorsRes] = await Promise.all([
|
// Load types and vendors
|
||||||
usbApi.types.list(),
|
const [typesRes, vendorsRes] = await Promise.all([
|
||||||
vendorsApi.list({ perpage: 1000 })
|
usbApi.types.list(),
|
||||||
])
|
vendorsApi.list({ perpage: 1000 })
|
||||||
types.value = typesRes.data.data || []
|
])
|
||||||
vendors.value = vendorsRes.data.data || []
|
types.value = typesRes.data.data || []
|
||||||
|
vendors.value = vendorsRes.data.data || []
|
||||||
// Load device if editing
|
|
||||||
if (isEdit.value) {
|
// Load device if editing
|
||||||
const response = await usbApi.get(route.params.id)
|
if (isEdit.value) {
|
||||||
const device = response.data.data
|
const response = await usbApi.get(route.params.id)
|
||||||
|
const device = response.data.data
|
||||||
form.value = {
|
|
||||||
serialnumber: device.serialnumber || '',
|
form.value = {
|
||||||
displayname: device.displayname || '',
|
serialnumber: device.serialnumber || '',
|
||||||
label: device.label || '',
|
displayname: device.displayname || '',
|
||||||
usbtypeid: device.usbtypeid || '',
|
label: device.label || '',
|
||||||
capacitygb: device.capacitygb || null,
|
usbtypeid: device.usbtypeid || '',
|
||||||
vendorid: device.vendorid || '',
|
capacitygb: device.capacitygb || null,
|
||||||
notes: device.notes || ''
|
vendorid: device.vendorid || '',
|
||||||
}
|
notes: device.notes || ''
|
||||||
}
|
}
|
||||||
} catch (err) {
|
}
|
||||||
console.error('Error loading data:', err)
|
} catch (err) {
|
||||||
error.value = 'Failed to load data'
|
console.error('Error loading data:', err)
|
||||||
} finally {
|
error.value = 'Failed to load data'
|
||||||
loading.value = false
|
} finally {
|
||||||
}
|
loading.value = false
|
||||||
})
|
}
|
||||||
|
})
|
||||||
async function saveDevice() {
|
|
||||||
error.value = ''
|
async function saveDevice() {
|
||||||
saving.value = true
|
error.value = ''
|
||||||
|
saving.value = true
|
||||||
try {
|
|
||||||
const data = {
|
try {
|
||||||
serialnumber: form.value.serialnumber,
|
const data = {
|
||||||
displayname: form.value.displayname,
|
serialnumber: form.value.serialnumber,
|
||||||
label: form.value.label || null,
|
displayname: form.value.displayname,
|
||||||
usbtypeid: form.value.usbtypeid || null,
|
label: form.value.label || null,
|
||||||
capacitygb: form.value.capacitygb || null,
|
usbtypeid: form.value.usbtypeid || null,
|
||||||
vendorid: form.value.vendorid || null,
|
capacitygb: form.value.capacitygb || null,
|
||||||
notes: form.value.notes || null
|
vendorid: form.value.vendorid || null,
|
||||||
}
|
notes: form.value.notes || null
|
||||||
|
}
|
||||||
if (isEdit.value) {
|
|
||||||
await usbApi.update(route.params.id, data)
|
if (isEdit.value) {
|
||||||
} else {
|
await usbApi.update(route.params.id, data)
|
||||||
await usbApi.create(data)
|
} else {
|
||||||
}
|
await usbApi.create(data)
|
||||||
|
}
|
||||||
router.push('/usb')
|
|
||||||
} catch (err) {
|
router.push('/usb')
|
||||||
console.error('Error saving device:', err)
|
} catch (err) {
|
||||||
error.value = err.response?.data?.message || 'Failed to save device'
|
console.error('Error saving device:', err)
|
||||||
} finally {
|
error.value = apiError(err, 'Failed to save device')
|
||||||
saving.value = false
|
} finally {
|
||||||
}
|
saving.value = false
|
||||||
}
|
}
|
||||||
</script>
|
}
|
||||||
|
</script>
|
||||||
<style scoped>
|
|
||||||
.form-row {
|
<style scoped>
|
||||||
display: grid;
|
.form-row {
|
||||||
grid-template-columns: 1fr 1fr;
|
display: grid;
|
||||||
gap: 1rem;
|
grid-template-columns: 1fr 1fr;
|
||||||
}
|
gap: 1rem;
|
||||||
|
}
|
||||||
.form-actions {
|
|
||||||
display: flex;
|
.form-actions {
|
||||||
gap: 0.5rem;
|
display: flex;
|
||||||
margin-top: 1.5rem;
|
gap: 0.5rem;
|
||||||
}
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
@media (max-width: 600px) {
|
|
||||||
.form-row {
|
@media (max-width: 600px) {
|
||||||
grid-template-columns: 1fr;
|
.form-row {
|
||||||
}
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
</style>
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -148,6 +148,7 @@ import Modal from '../../components/Modal.vue'
|
|||||||
import EmployeeSearch from '../../components/EmployeeSearch.vue'
|
import EmployeeSearch from '../../components/EmployeeSearch.vue'
|
||||||
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'
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
const devices = ref([])
|
const devices = ref([])
|
||||||
@@ -243,7 +244,7 @@ async function doCheckout() {
|
|||||||
loadDevices()
|
loadDevices()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Checkout error:', error)
|
console.error('Checkout error:', error)
|
||||||
toast.error(error.response?.data?.message || 'Checkout failed')
|
toast.error(apiError(error, 'Checkout failed'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,7 +255,7 @@ async function doCheckin() {
|
|||||||
loadDevices()
|
loadDevices()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Checkin error:', error)
|
console.error('Checkin error:', error)
|
||||||
toast.error(error.response?.data?.message || 'Check in failed')
|
toast.error(apiError(error, 'Check in failed'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
3
frontend/src/views/vendors/VendorsList.vue
vendored
3
frontend/src/views/vendors/VendorsList.vue
vendored
@@ -187,6 +187,7 @@ import { ref, onMounted } from 'vue'
|
|||||||
import { vendorsApi } from '../../api'
|
import { vendorsApi } 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'
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
const vendors = ref([])
|
const vendors = ref([])
|
||||||
@@ -304,7 +305,7 @@ async function saveVendor() {
|
|||||||
loadVendors()
|
loadVendors()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error saving vendor:', err)
|
console.error('Error saving vendor:', err)
|
||||||
error.value = err.response?.data?.message || 'Failed to save vendor'
|
error.value = apiError(err, 'Failed to save vendor')
|
||||||
} finally {
|
} finally {
|
||||||
saving.value = false
|
saving.value = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,14 +3,17 @@
|
|||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h2>Warranties</h2>
|
<h2>Warranties</h2>
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
<button class="btn btn-secondary" :disabled="syncing" @click="syncDell">
|
<button class="btn btn-secondary" :disabled="syncing" @click="syncDell(false)">
|
||||||
{{ syncing ? 'Syncing Dell...' : 'Sync Dell' }}
|
{{ syncing ? 'Syncing Dell...' : 'Sync Dell' }}
|
||||||
</button>
|
</button>
|
||||||
|
<button class="btn btn-secondary" :disabled="syncing" @click="syncDell(true)"
|
||||||
|
title="Re-check every Dell warranty, including ones already dated">
|
||||||
|
Re-check all
|
||||||
|
</button>
|
||||||
<button class="btn btn-primary" @click="openModal()">+ Add Warranty</button>
|
<button class="btn btn-primary" @click="openModal()">+ Add Warranty</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="filters">
|
<div class="filters">
|
||||||
<label>Status
|
<label>Status
|
||||||
<select v-model="statusFilter" class="form-control" @change="loadData">
|
<select v-model="statusFilter" class="form-control" @change="loadData">
|
||||||
@@ -21,6 +24,8 @@
|
|||||||
<option value="unknown">Unknown</option>
|
<option value="unknown">Unknown</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
<input v-model="search" type="text" class="form-control" placeholder="Search vendor, service level, tag, asset..." />
|
||||||
|
<span class="result-count">{{ filteredItems.length }} of {{ items.length }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
@@ -39,10 +44,10 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="w in items" :key="w.warrantyid">
|
<tr v-for="w in paginatedItems" :key="w.warrantyid">
|
||||||
<td><strong>{{ w.vendor }}</strong><span v-if="w.provider !== 'manual'" class="muted"> ({{ w.provider }})</span></td>
|
<td><strong>{{ w.vendor }}</strong><span v-if="w.provider !== 'manual'" class="muted"> ({{ w.provider }})</span></td>
|
||||||
<td><span class="status-badge" :style="colorStyle(w.statuscolor)">{{ statusLabel(w.status) }}</span></td>
|
<td><span class="status-badge" :style="colorStyle(w.statuscolor)">{{ statusLabel(w.status) }}</span></td>
|
||||||
<td>{{ w.servicelevel || '-' }}</td>
|
<td class="servicelevel-cell" :title="w.servicelevel">{{ w.servicelevel || '-' }}</td>
|
||||||
<td>{{ w.enddate ? formatDate(w.enddate) : '-' }}</td>
|
<td>{{ w.enddate ? formatDate(w.enddate) : '-' }}</td>
|
||||||
<td>
|
<td>
|
||||||
<span v-if="!w.assets.length" class="muted">-</span>
|
<span v-if="!w.assets.length" class="muted">-</span>
|
||||||
@@ -54,12 +59,18 @@
|
|||||||
<button class="btn btn-danger btn-sm" @click="deleteWarranty(w)">Delete</button>
|
<button class="btn btn-danger btn-sm" @click="deleteWarranty(w)">Delete</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-if="items.length === 0">
|
<tr v-if="filteredItems.length === 0">
|
||||||
<td colspan="6" style="text-align: center; color: var(--text-light);">No warranties</td>
|
<td colspan="6" style="text-align: center; color: var(--text-light);">No warranties</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="totalPages > 1" class="pagination">
|
||||||
|
<button class="btn btn-secondary btn-sm" :disabled="page === 1" @click="page--">Prev</button>
|
||||||
|
<span class="page-info">Page {{ page }} of {{ totalPages }}</span>
|
||||||
|
<button class="btn btn-secondary btn-sm" :disabled="page === totalPages" @click="page++">Next</button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -141,11 +152,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, computed, watch, onMounted } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { colorStyle } from '@/utils/colorStyle'
|
import { colorStyle } from '@/utils/colorStyle'
|
||||||
import { warrantyApi, assetsApi } from '../../api'
|
import { warrantyApi, assetsApi } from '../../api'
|
||||||
import { useToast } from '../../composables/toast'
|
import { useToast } from '../../composables/toast'
|
||||||
|
import { apiError } from '../../utils/apiError'
|
||||||
|
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -153,6 +165,28 @@ const route = useRoute()
|
|||||||
const items = ref([])
|
const items = ref([])
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const syncing = ref(false)
|
const syncing = ref(false)
|
||||||
|
const search = ref('')
|
||||||
|
const page = ref(1)
|
||||||
|
const perPage = 25
|
||||||
|
|
||||||
|
// Client-side search across vendor / service level / tag / linked asset numbers.
|
||||||
|
const filteredItems = computed(() => {
|
||||||
|
const term = search.value.trim().toLowerCase()
|
||||||
|
if (!term) return items.value
|
||||||
|
return items.value.filter(w =>
|
||||||
|
(w.vendor || '').toLowerCase().includes(term) ||
|
||||||
|
(w.servicelevel || '').toLowerCase().includes(term) ||
|
||||||
|
(w.servicetag || '').toLowerCase().includes(term) ||
|
||||||
|
(w.assets || []).some(a => (a.assetnumber || '').toLowerCase().includes(term)))
|
||||||
|
})
|
||||||
|
const totalPages = computed(() => Math.max(1, Math.ceil(filteredItems.value.length / perPage)))
|
||||||
|
const paginatedItems = computed(() => filteredItems.value.slice((page.value - 1) * perPage, page.value * perPage))
|
||||||
|
|
||||||
|
// Reset to page 1 whenever the filtered set changes.
|
||||||
|
watch([filteredItems, () => filteredItems.value.length], () => {
|
||||||
|
if (page.value > totalPages.value) page.value = 1
|
||||||
|
})
|
||||||
|
watch(search, () => { page.value = 1 })
|
||||||
const statusFilter = ref('')
|
const statusFilter = ref('')
|
||||||
const showModal = ref(false)
|
const showModal = ref(false)
|
||||||
const editing = ref(null)
|
const editing = ref(null)
|
||||||
@@ -254,14 +288,6 @@ function openModal(item = null) {
|
|||||||
}
|
}
|
||||||
function closeModal() { showModal.value = false; editing.value = null }
|
function closeModal() { showModal.value = false; editing.value = null }
|
||||||
|
|
||||||
// API errors nest the message at data.data.error.message (see error_response).
|
|
||||||
function apiError(err, fallback) {
|
|
||||||
return err.response?.data?.data?.error?.message
|
|
||||||
|| err.response?.data?.error?.message
|
|
||||||
|| err.response?.data?.message
|
|
||||||
|| fallback
|
|
||||||
}
|
|
||||||
|
|
||||||
async function save() {
|
async function save() {
|
||||||
error.value = ''
|
error.value = ''
|
||||||
saving.value = true
|
saving.value = true
|
||||||
@@ -291,11 +317,11 @@ async function deleteWarranty(w) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function syncDell() {
|
async function syncDell(all = false) {
|
||||||
syncing.value = true
|
syncing.value = true
|
||||||
toast.info('Looking up Dell service tags... this can take a moment.')
|
toast.info(all ? 'Re-checking all Dell warranties...' : 'Looking up Dell service tags... this can take a moment.')
|
||||||
try {
|
try {
|
||||||
const response = await warrantyApi.syncDell()
|
const response = await warrantyApi.syncDell(all)
|
||||||
const summary = response.data?.data || {}
|
const summary = response.data?.data || {}
|
||||||
loadData()
|
loadData()
|
||||||
toast.success(`Dell sync: ${summary.created || 0} added, ${summary.updated || 0} updated (${summary.matched || 0} tags matched).`)
|
toast.success(`Dell sync: ${summary.created || 0} added, ${summary.updated || 0} updated (${summary.matched || 0} tags matched).`)
|
||||||
@@ -323,6 +349,12 @@ async function refresh(w) {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.muted { color: var(--text-light); }
|
.muted { color: var(--text-light); }
|
||||||
.status-badge { padding: 0.15rem 0.6rem; border-radius: 12px; font-size: 0.78rem; font-weight: 600; }
|
.status-badge { padding: 0.15rem 0.6rem; border-radius: 12px; font-size: 0.78rem; font-weight: 600; }
|
||||||
|
.filters { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; margin-bottom: 1rem; }
|
||||||
|
.filters .form-control { max-width: 320px; }
|
||||||
|
.result-count { color: var(--text-light); font-size: 0.85rem; }
|
||||||
|
.servicelevel-cell { max-width: 320px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.pagination { display: flex; align-items: center; justify-content: center; gap: 1rem; padding: 0.9rem 0 0.2rem; }
|
||||||
|
.page-info { color: var(--text-light); font-size: 0.85rem; }
|
||||||
.form-row { display: flex; gap: 1rem; }
|
.form-row { display: flex; gap: 1rem; }
|
||||||
.form-row .form-group { flex: 1; }
|
.form-row .form-group { flex: 1; }
|
||||||
.asset-search { position: relative; }
|
.asset-search { position: relative; }
|
||||||
|
|||||||
@@ -225,13 +225,16 @@ def sync_dell():
|
|||||||
and non-Dell serials are filtered out by Dell (they return no coverage).
|
and non-Dell serials are filtered out by Dell (they return no coverage).
|
||||||
"""
|
"""
|
||||||
provider = get_provider('dell')
|
provider = get_provider('dell')
|
||||||
|
# ?all=true re-checks assets that already have a dated warranty too.
|
||||||
|
recheck_all = request.args.get('all', 'false').lower() == 'true'
|
||||||
|
|
||||||
# Assets already covered by a dated warranty - skip these.
|
# Assets already covered by a dated warranty - skipped unless recheck_all.
|
||||||
covered = set()
|
covered = set()
|
||||||
for link in WarrantyAsset.query.all():
|
if not recheck_all:
|
||||||
w = Warranty.query.get(link.warrantyid)
|
for link in WarrantyAsset.query.all():
|
||||||
if w and w.isactive and w.enddate:
|
w = Warranty.query.get(link.warrantyid)
|
||||||
covered.add(link.assetid)
|
if w and w.isactive and w.enddate:
|
||||||
|
covered.add(link.assetid)
|
||||||
|
|
||||||
# Candidate assets: active, have a serial, not already covered.
|
# Candidate assets: active, have a serial, not already covered.
|
||||||
candidates = (Asset.query
|
candidates = (Asset.query
|
||||||
|
|||||||
Reference in New Issue
Block a user