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:
cproudlock
2026-07-09 17:23:21 -04:00
parent 640b8de1b2
commit ec2de635ed
46 changed files with 4091 additions and 3968 deletions

View File

@@ -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')

View File

@@ -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'))
} }
} }

View 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 }
}

View 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
}

View File

@@ -155,6 +155,7 @@
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 route = useRoute()
const router = useRouter() const router = useRouter()
@@ -248,7 +249,7 @@ async function saveApplication() {
router.push('/applications') router.push('/applications')
} catch (err) { } catch (err) {
console.error('Error saving application:', err) console.error('Error saving application:', err)
error.value = err.response?.data?.message || 'Failed to save application' error.value = apiError(err, 'Failed to save application')
} finally { } finally {
saving.value = false saving.value = false
} }

View File

@@ -83,6 +83,7 @@
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 route = useRoute()
const router = useRouter() const router = useRouter()
@@ -149,7 +150,7 @@ async function saveArticle() {
router.push('/knowledgebase') router.push('/knowledgebase')
} catch (err) { } catch (err) {
console.error('Error saving article:', err) console.error('Error saving article:', err)
error.value = err.response?.data?.message || 'Failed to save article' error.value = apiError(err, 'Failed to save article')
} finally { } finally {
saving.value = false saving.value = false
} }

View File

@@ -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(() => {

View File

@@ -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
} }

View File

@@ -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 () => {

View File

@@ -257,6 +257,7 @@ import {
} from '../../api' } from '../../api'
import CustomFieldsInputs from '../../components/CustomFieldsInputs.vue' import CustomFieldsInputs from '../../components/CustomFieldsInputs.vue'
import { useIdentifierFlags } from '../../composables/identifierSettings' import { useIdentifierFlags } from '../../composables/identifierSettings'
import { apiError } from '../../utils/apiError'
const { isEnabled } = useIdentifierFlags() const { isEnabled } = useIdentifierFlags()
@@ -445,7 +446,7 @@ async function submitForm() {
router.push(redirectId ? `/network/${redirectId}` : '/network') router.push(redirectId ? `/network/${redirectId}` : '/network')
} catch (err) { } catch (err) {
console.error('Error saving device:', err) console.error('Error saving device:', err)
error.value = err.response?.data?.message || 'Failed to save device' error.value = apiError(err, 'Failed to save device')
} finally { } finally {
saving.value = false saving.value = false
} }

View File

@@ -242,6 +242,7 @@
import { ref, onMounted, computed, watch } from 'vue' import { ref, onMounted, computed, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { notificationsApi, applicationsApi, businessUnitsApi, employeesApi } from '@/api' import { notificationsApi, applicationsApi, businessUnitsApi, employeesApi } from '@/api'
import { apiError } from '../../utils/apiError'
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
@@ -499,7 +500,7 @@ async function saveNotification() {
router.push('/notifications') router.push('/notifications')
} catch (err) { } catch (err) {
console.error('Error saving notification:', err) console.error('Error saving notification:', err)
error.value = err.response?.data?.message || 'Failed to save notification' error.value = apiError(err, 'Failed to save notification')
} finally { } finally {
saving.value = false saving.value = false
} }

View File

@@ -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
} }

View File

@@ -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 {

View File

@@ -310,6 +310,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()
@@ -539,7 +540,7 @@ async function savePC() {
router.push('/pcs') router.push('/pcs')
} catch (err) { } catch (err) {
console.error('Error saving PC:', err) console.error('Error saving PC:', err)
error.value = err.response?.data?.message || 'Failed to save PC' error.value = apiError(err, 'Failed to save PC')
} finally { } finally {
saving.value = false saving.value = false
} }

View File

@@ -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([])

View File

@@ -298,6 +298,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()
@@ -596,7 +597,7 @@ async function savePrinter() {
router.push('/printers') router.push('/printers')
} catch (err) { } catch (err) {
console.error('Error saving printer:', err) console.error('Error saving printer:', err)
error.value = err.response?.data?.message || 'Failed to save printer' error.value = apiError(err, 'Failed to save printer')
} finally { } finally {
saving.value = false saving.value = false
} }

View File

@@ -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>

View File

@@ -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
} }

View File

@@ -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
} }

View File

@@ -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
} }

View File

@@ -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>

View File

@@ -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
} }

View File

@@ -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>

View File

@@ -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>

View File

@@ -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
} }

View File

@@ -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
} }

View File

@@ -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
} }

View File

@@ -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
} }

View File

@@ -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>

View File

@@ -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
} }

View File

@@ -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
} }

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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
} }

View File

@@ -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)
} }
} }

View File

@@ -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>

View File

@@ -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)

View File

@@ -288,6 +288,7 @@
import { ref, reactive, onMounted } from 'vue' import { ref, reactive, onMounted } from 'vue'
import { usersApi } from '../../api' import { usersApi } from '../../api'
import { useAuthStore } from '../../stores/auth' import { useAuthStore } from '../../stores/auth'
import { apiError } from '../../utils/apiError'
const authStore = useAuthStore() const authStore = useAuthStore()
const currentUserId = authStore.user?.userid const currentUserId = authStore.user?.userid
@@ -428,7 +429,7 @@ async function saveUser() {
closeUserModal() closeUserModal()
await loadData() await loadData()
} catch (e) { } catch (e) {
error.value = e.response?.data?.message || 'Failed to save user' error.value = apiError(e, 'Failed to save user')
setTimeout(() => error.value = '', 3000) setTimeout(() => error.value = '', 3000)
} finally { } finally {
saving.value = false saving.value = false
@@ -446,7 +447,7 @@ async function deleteUser() {
deletingUser.value = null deletingUser.value = null
await loadData() await loadData()
} catch (e) { } catch (e) {
error.value = e.response?.data?.message || 'Failed to delete user' error.value = apiError(e, 'Failed to delete user')
setTimeout(() => error.value = '', 3000) setTimeout(() => error.value = '', 3000)
} finally { } finally {
saving.value = false saving.value = false
@@ -487,7 +488,7 @@ async function saveRole() {
closeRoleModal() closeRoleModal()
await loadData() await loadData()
} catch (e) { } catch (e) {
error.value = e.response?.data?.message || 'Failed to save role' error.value = apiError(e, 'Failed to save role')
setTimeout(() => error.value = '', 3000) setTimeout(() => error.value = '', 3000)
} finally { } finally {
saving.value = false saving.value = false
@@ -505,7 +506,7 @@ async function deleteRole(role) {
await usersApi.roles.delete(role.roleid) await usersApi.roles.delete(role.roleid)
await loadData() await loadData()
} catch (e) { } catch (e) {
error.value = e.response?.data?.message || 'Failed to delete role' error.value = apiError(e, 'Failed to delete role')
setTimeout(() => error.value = '', 3000) setTimeout(() => error.value = '', 3000)
} }
} }

View File

@@ -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>

View File

@@ -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'))
} }
} }

View File

@@ -122,6 +122,7 @@
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 route = useRoute()
const router = useRouter() const router = useRouter()
@@ -202,7 +203,7 @@ async function saveDevice() {
router.push('/usb') router.push('/usb')
} catch (err) { } catch (err) {
console.error('Error saving device:', err) console.error('Error saving device:', err)
error.value = err.response?.data?.message || 'Failed to save device' error.value = apiError(err, 'Failed to save device')
} finally { } finally {
saving.value = false saving.value = false
} }

View File

@@ -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'))
} }
} }

View File

@@ -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
} }

View File

@@ -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; }

View File

@@ -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