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:
@@ -3,14 +3,17 @@
|
||||
<div class="page-header">
|
||||
<h2>Warranties</h2>
|
||||
<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' }}
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="filters">
|
||||
<label>Status
|
||||
<select v-model="statusFilter" class="form-control" @change="loadData">
|
||||
@@ -21,6 +24,8 @@
|
||||
<option value="unknown">Unknown</option>
|
||||
</select>
|
||||
</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 class="card">
|
||||
@@ -39,10 +44,10 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<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><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>
|
||||
<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>
|
||||
</td>
|
||||
</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>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
@@ -141,11 +152,12 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, computed, watch, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { colorStyle } from '@/utils/colorStyle'
|
||||
import { warrantyApi, assetsApi } from '../../api'
|
||||
import { useToast } from '../../composables/toast'
|
||||
import { apiError } from '../../utils/apiError'
|
||||
|
||||
const toast = useToast()
|
||||
const route = useRoute()
|
||||
@@ -153,6 +165,28 @@ const route = useRoute()
|
||||
const items = ref([])
|
||||
const loading = ref(true)
|
||||
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 showModal = ref(false)
|
||||
const editing = ref(null)
|
||||
@@ -254,14 +288,6 @@ function openModal(item = 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() {
|
||||
error.value = ''
|
||||
saving.value = true
|
||||
@@ -291,11 +317,11 @@ async function deleteWarranty(w) {
|
||||
}
|
||||
}
|
||||
|
||||
async function syncDell() {
|
||||
async function syncDell(all = false) {
|
||||
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 {
|
||||
const response = await warrantyApi.syncDell()
|
||||
const response = await warrantyApi.syncDell(all)
|
||||
const summary = response.data?.data || {}
|
||||
loadData()
|
||||
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>
|
||||
.muted { color: var(--text-light); }
|
||||
.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 .form-group { flex: 1; }
|
||||
.asset-search { position: relative; }
|
||||
|
||||
Reference in New Issue
Block a user