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:
@@ -169,6 +169,7 @@ import { useRoute } from 'vue-router'
|
||||
import { usbApi } from '../../api'
|
||||
import Modal from '../../components/Modal.vue'
|
||||
import { useToast } from '../../composables/toast'
|
||||
import { apiError } from '../../utils/apiError'
|
||||
const toast = useToast()
|
||||
|
||||
const route = useRoute()
|
||||
@@ -219,7 +220,7 @@ async function doCheckout() {
|
||||
await loadDevice()
|
||||
} catch (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()
|
||||
} catch (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>
|
||||
<div>
|
||||
<div class="page-header">
|
||||
<h2>{{ isEdit ? 'Edit USB Device' : 'Add USB Device' }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div v-if="loading" class="loading">Loading...</div>
|
||||
|
||||
<form v-else @submit.prevent="saveDevice">
|
||||
<div class="form-group">
|
||||
<label for="serialnumber">Serial Number *</label>
|
||||
<input
|
||||
id="serialnumber"
|
||||
v-model="form.serialnumber"
|
||||
type="text"
|
||||
class="form-control"
|
||||
required
|
||||
maxlength="100"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="displayname">Display Name *</label>
|
||||
<input
|
||||
id="displayname"
|
||||
v-model="form.displayname"
|
||||
type="text"
|
||||
class="form-control"
|
||||
required
|
||||
maxlength="100"
|
||||
placeholder="e.g., USB Flash Drive #1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="label">Label</label>
|
||||
<input
|
||||
id="label"
|
||||
v-model="form.label"
|
||||
type="text"
|
||||
class="form-control"
|
||||
maxlength="100"
|
||||
placeholder="Physical label on device"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="usbtypeid">Device Type</label>
|
||||
<select
|
||||
id="usbtypeid"
|
||||
v-model="form.usbtypeid"
|
||||
class="form-control"
|
||||
>
|
||||
<option value="">-- Select Type --</option>
|
||||
<option
|
||||
v-for="type in types"
|
||||
:key="type.usbtypeid"
|
||||
:value="type.usbtypeid"
|
||||
>
|
||||
{{ type.typename }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="capacitygb">Capacity (GB)</label>
|
||||
<input
|
||||
id="capacitygb"
|
||||
v-model.number="form.capacitygb"
|
||||
type="number"
|
||||
class="form-control"
|
||||
min="0"
|
||||
step="1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="vendorid">Vendor</label>
|
||||
<select
|
||||
id="vendorid"
|
||||
v-model="form.vendorid"
|
||||
class="form-control"
|
||||
>
|
||||
<option value="">-- Select Vendor --</option>
|
||||
<option
|
||||
v-for="vendor in vendors"
|
||||
:key="vendor.vendorid"
|
||||
:value="vendor.vendorid"
|
||||
>
|
||||
{{ vendor.vendorname }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="notes">Notes</label>
|
||||
<textarea
|
||||
id="notes"
|
||||
v-model="form.notes"
|
||||
class="form-control"
|
||||
rows="3"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="error-message">{{ error }}</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary" :disabled="saving">
|
||||
{{ saving ? 'Saving...' : (isEdit ? 'Update Device' : 'Add Device') }}
|
||||
</button>
|
||||
<router-link to="/usb" class="btn btn-secondary">Cancel</router-link>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { usbApi, vendorsApi } from '@/api'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const isEdit = computed(() => !!route.params.id)
|
||||
|
||||
const loading = ref(true)
|
||||
const saving = ref(false)
|
||||
const error = ref('')
|
||||
|
||||
const types = ref([])
|
||||
const vendors = ref([])
|
||||
|
||||
const form = ref({
|
||||
serialnumber: '',
|
||||
displayname: '',
|
||||
label: '',
|
||||
usbtypeid: '',
|
||||
capacitygb: null,
|
||||
vendorid: '',
|
||||
notes: ''
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
// Load types and vendors
|
||||
const [typesRes, vendorsRes] = await Promise.all([
|
||||
usbApi.types.list(),
|
||||
vendorsApi.list({ perpage: 1000 })
|
||||
])
|
||||
types.value = typesRes.data.data || []
|
||||
vendors.value = vendorsRes.data.data || []
|
||||
|
||||
// Load device if editing
|
||||
if (isEdit.value) {
|
||||
const response = await usbApi.get(route.params.id)
|
||||
const device = response.data.data
|
||||
|
||||
form.value = {
|
||||
serialnumber: device.serialnumber || '',
|
||||
displayname: device.displayname || '',
|
||||
label: device.label || '',
|
||||
usbtypeid: device.usbtypeid || '',
|
||||
capacitygb: device.capacitygb || null,
|
||||
vendorid: device.vendorid || '',
|
||||
notes: device.notes || ''
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading data:', err)
|
||||
error.value = 'Failed to load data'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
|
||||
async function saveDevice() {
|
||||
error.value = ''
|
||||
saving.value = true
|
||||
|
||||
try {
|
||||
const data = {
|
||||
serialnumber: form.value.serialnumber,
|
||||
displayname: form.value.displayname,
|
||||
label: form.value.label || null,
|
||||
usbtypeid: form.value.usbtypeid || null,
|
||||
capacitygb: form.value.capacitygb || null,
|
||||
vendorid: form.value.vendorid || null,
|
||||
notes: form.value.notes || null
|
||||
}
|
||||
|
||||
if (isEdit.value) {
|
||||
await usbApi.update(route.params.id, data)
|
||||
} else {
|
||||
await usbApi.create(data)
|
||||
}
|
||||
|
||||
router.push('/usb')
|
||||
} catch (err) {
|
||||
console.error('Error saving device:', err)
|
||||
error.value = err.response?.data?.message || 'Failed to save device'
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<div>
|
||||
<div class="page-header">
|
||||
<h2>{{ isEdit ? 'Edit USB Device' : 'Add USB Device' }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div v-if="loading" class="loading">Loading...</div>
|
||||
|
||||
<form v-else @submit.prevent="saveDevice">
|
||||
<div class="form-group">
|
||||
<label for="serialnumber">Serial Number *</label>
|
||||
<input
|
||||
id="serialnumber"
|
||||
v-model="form.serialnumber"
|
||||
type="text"
|
||||
class="form-control"
|
||||
required
|
||||
maxlength="100"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="displayname">Display Name *</label>
|
||||
<input
|
||||
id="displayname"
|
||||
v-model="form.displayname"
|
||||
type="text"
|
||||
class="form-control"
|
||||
required
|
||||
maxlength="100"
|
||||
placeholder="e.g., USB Flash Drive #1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="label">Label</label>
|
||||
<input
|
||||
id="label"
|
||||
v-model="form.label"
|
||||
type="text"
|
||||
class="form-control"
|
||||
maxlength="100"
|
||||
placeholder="Physical label on device"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="usbtypeid">Device Type</label>
|
||||
<select
|
||||
id="usbtypeid"
|
||||
v-model="form.usbtypeid"
|
||||
class="form-control"
|
||||
>
|
||||
<option value="">-- Select Type --</option>
|
||||
<option
|
||||
v-for="type in types"
|
||||
:key="type.usbtypeid"
|
||||
:value="type.usbtypeid"
|
||||
>
|
||||
{{ type.typename }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="capacitygb">Capacity (GB)</label>
|
||||
<input
|
||||
id="capacitygb"
|
||||
v-model.number="form.capacitygb"
|
||||
type="number"
|
||||
class="form-control"
|
||||
min="0"
|
||||
step="1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="vendorid">Vendor</label>
|
||||
<select
|
||||
id="vendorid"
|
||||
v-model="form.vendorid"
|
||||
class="form-control"
|
||||
>
|
||||
<option value="">-- Select Vendor --</option>
|
||||
<option
|
||||
v-for="vendor in vendors"
|
||||
:key="vendor.vendorid"
|
||||
:value="vendor.vendorid"
|
||||
>
|
||||
{{ vendor.vendorname }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="notes">Notes</label>
|
||||
<textarea
|
||||
id="notes"
|
||||
v-model="form.notes"
|
||||
class="form-control"
|
||||
rows="3"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="error-message">{{ error }}</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary" :disabled="saving">
|
||||
{{ saving ? 'Saving...' : (isEdit ? 'Update Device' : 'Add Device') }}
|
||||
</button>
|
||||
<router-link to="/usb" class="btn btn-secondary">Cancel</router-link>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { usbApi, vendorsApi } from '@/api'
|
||||
import { apiError } from '../../utils/apiError'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const isEdit = computed(() => !!route.params.id)
|
||||
|
||||
const loading = ref(true)
|
||||
const saving = ref(false)
|
||||
const error = ref('')
|
||||
|
||||
const types = ref([])
|
||||
const vendors = ref([])
|
||||
|
||||
const form = ref({
|
||||
serialnumber: '',
|
||||
displayname: '',
|
||||
label: '',
|
||||
usbtypeid: '',
|
||||
capacitygb: null,
|
||||
vendorid: '',
|
||||
notes: ''
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
// Load types and vendors
|
||||
const [typesRes, vendorsRes] = await Promise.all([
|
||||
usbApi.types.list(),
|
||||
vendorsApi.list({ perpage: 1000 })
|
||||
])
|
||||
types.value = typesRes.data.data || []
|
||||
vendors.value = vendorsRes.data.data || []
|
||||
|
||||
// Load device if editing
|
||||
if (isEdit.value) {
|
||||
const response = await usbApi.get(route.params.id)
|
||||
const device = response.data.data
|
||||
|
||||
form.value = {
|
||||
serialnumber: device.serialnumber || '',
|
||||
displayname: device.displayname || '',
|
||||
label: device.label || '',
|
||||
usbtypeid: device.usbtypeid || '',
|
||||
capacitygb: device.capacitygb || null,
|
||||
vendorid: device.vendorid || '',
|
||||
notes: device.notes || ''
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading data:', err)
|
||||
error.value = 'Failed to load data'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
|
||||
async function saveDevice() {
|
||||
error.value = ''
|
||||
saving.value = true
|
||||
|
||||
try {
|
||||
const data = {
|
||||
serialnumber: form.value.serialnumber,
|
||||
displayname: form.value.displayname,
|
||||
label: form.value.label || null,
|
||||
usbtypeid: form.value.usbtypeid || null,
|
||||
capacitygb: form.value.capacitygb || null,
|
||||
vendorid: form.value.vendorid || null,
|
||||
notes: form.value.notes || null
|
||||
}
|
||||
|
||||
if (isEdit.value) {
|
||||
await usbApi.update(route.params.id, data)
|
||||
} else {
|
||||
await usbApi.create(data)
|
||||
}
|
||||
|
||||
router.push('/usb')
|
||||
} catch (err) {
|
||||
console.error('Error saving device:', err)
|
||||
error.value = apiError(err, 'Failed to save device')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -148,6 +148,7 @@ import Modal from '../../components/Modal.vue'
|
||||
import EmployeeSearch from '../../components/EmployeeSearch.vue'
|
||||
import PaginationBar from '../../components/PaginationBar.vue'
|
||||
import { useToast } from '../../composables/toast'
|
||||
import { apiError } from '../../utils/apiError'
|
||||
const toast = useToast()
|
||||
|
||||
const devices = ref([])
|
||||
@@ -243,7 +244,7 @@ async function doCheckout() {
|
||||
loadDevices()
|
||||
} catch (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()
|
||||
} catch (error) {
|
||||
console.error('Checkin error:', error)
|
||||
toast.error(error.response?.data?.message || 'Check in failed')
|
||||
toast.error(apiError(error, 'Check in failed'))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user