Let the warranty search find a machine number
Reported: the warranty page cannot be searched by machine number. It was there to be found. _related_machine puts the machine's number in the payload, and it does so for a stated reason - a shopfloor PC is bought, warranted and replaced as a PC, but it is FOUND by the machine it drives, and nobody walks the floor looking for an asset number. The table renders it as a chip. The filter looked at vendor, service level, service tag and asset number, and never at the machine. So the one identifier the feature was designed around was the one thing the search box could not match. This is how a field list goes wrong: a column was added to the table and not to the filter, and nothing connects the two. Moved the field list into warrantySearch.js so a test can see it, following controllingPc.js and labelPages.js. Machine name is searched as well - somebody who knows the bay by "the Makino" should not have to look up its number to find its warranty. The specs also pin the case that made the old inline version fragile: a covered asset whose machine is null, which is every printer and any PC nobody has linked yet, must not throw. And a missing field must not match: String(undefined) is 'undefined', so a naive coercion makes a search for "und" hit every row with an empty vendor. Not changed, worth knowing: the covered asset's NAME is still not searched, only its number. That looks like the same omission, but it is a wider change than the report - the placeholder has always said "asset" without saying which half - so it is left for a deliberate decision rather than folded in here.
This commit is contained in:
@@ -105,6 +105,13 @@ ADR-007 and ADR-002.
|
||||
the wrong code. The page index now travels with the page.
|
||||
- The back label's dotted preview outline outspecified the print reset and drew
|
||||
a dashed rule down the edge of every second card.
|
||||
- **The warranty list could not be searched by machine number.** The number was
|
||||
in the payload and rendered in the table as a chip, but the search box looked
|
||||
only at vendor, service level, service tag and asset number. That made the
|
||||
identifier the feature was built around - a shopfloor PC is warranted as a PC
|
||||
but found by the machine it drives - the one thing the box could not find.
|
||||
Machine number and machine name are now searched too, and the placeholder
|
||||
names them.
|
||||
- **`flask plugin upgrade-all` failed a plugin whose tables already existed**
|
||||
("table securesendsecrets already exists" on the tools plugin). Every
|
||||
post-cutover baseline emitted a plain `op.create_table`, which is correct only
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<option value="unknown">Unknown</option>
|
||||
</select>
|
||||
</label>
|
||||
<input v-model="search" type="text" class="form-control" placeholder="Search vendor, service level, tag, asset..." />
|
||||
<input v-model="search" type="text" class="form-control" placeholder="Search vendor, service level, tag, asset number, machine..." />
|
||||
<span class="result-count">{{ filteredItems.length }} of {{ items.length }}</span>
|
||||
</div>
|
||||
|
||||
@@ -186,6 +186,7 @@ import api, { warrantyApi, assetsApi, vendorsApi } from '@/api'
|
||||
import { useToast } from '@/composables/toast'
|
||||
import { apiError } from '@/utils/apiError'
|
||||
import MachineMapChip from '../components/MachineMapChip.vue'
|
||||
import { matchesWarrantySearch } from './warrantySearch'
|
||||
|
||||
const toast = useToast()
|
||||
const route = useRoute()
|
||||
@@ -197,16 +198,11 @@ 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)))
|
||||
})
|
||||
// Client-side search. The field list lives in warrantySearch.js, where it is
|
||||
// covered by tests - the machine number was rendered in this table for a long
|
||||
// time before anything let you search for it.
|
||||
const filteredItems = computed(() =>
|
||||
items.value.filter(w => matchesWarrantySearch(w, search.value)))
|
||||
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))
|
||||
|
||||
|
||||
44
plugins/warranty/frontend/views/warrantySearch.js
Normal file
44
plugins/warranty/frontend/views/warrantySearch.js
Normal file
@@ -0,0 +1,44 @@
|
||||
// What the warranty list's search box matches against.
|
||||
//
|
||||
// Pulled out of WarrantiesList.vue so the field list is somewhere a test can
|
||||
// see it. It went wrong the way a field list usually does: silently, by a
|
||||
// column being added to the table and not to the filter.
|
||||
//
|
||||
// THE MACHINE NUMBER IS THE POINT. _related_machine in the warranty API exists
|
||||
// precisely because "a shopfloor PC is bought, warranted and replaced as a PC,
|
||||
// but it is FOUND by the machine it drives - nobody walks the floor looking for
|
||||
// an asset number". The payload carries it, the table renders it in a chip, and
|
||||
// the filter did not look at it - so the one identifier the feature was built
|
||||
// around was the one thing the search box could not find.
|
||||
|
||||
/**
|
||||
* Does this warranty match the search term?
|
||||
*
|
||||
* @param {object} warranty - a row from GET /api/warranties
|
||||
* @param {string} term - the raw search box contents
|
||||
* @returns {boolean} true when the term is empty or any searched field contains it
|
||||
*/
|
||||
export function matchesWarrantySearch(warranty, term) {
|
||||
const needle = (term || '').trim().toLowerCase()
|
||||
if (!needle) return true
|
||||
if (!warranty) return false
|
||||
|
||||
const has = (value) => String(value ?? '').toLowerCase().includes(needle)
|
||||
|
||||
if (has(warranty.vendor) || has(warranty.servicelevel) || has(warranty.servicetag)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return (warranty.assets || []).some((asset) => {
|
||||
if (has(asset?.assetnumber)) return true
|
||||
// The related machine is optional: a warranty on something that drives no
|
||||
// machine has none, and a machine that was never linked leaves it null.
|
||||
const machine = asset?.machine
|
||||
return !!machine && (has(machine.machinenumber) || has(machine.name))
|
||||
})
|
||||
}
|
||||
|
||||
/** The fields above, in the order the placeholder should name them. */
|
||||
export const WARRANTY_SEARCH_FIELDS = [
|
||||
'vendor', 'service level', 'tag', 'asset number', 'machine',
|
||||
]
|
||||
79
plugins/warranty/frontend/views/warrantySearch.spec.js
Normal file
79
plugins/warranty/frontend/views/warrantySearch.spec.js
Normal file
@@ -0,0 +1,79 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
|
||||
import { matchesWarrantySearch } from './warrantySearch'
|
||||
|
||||
// Reported: the warranty page could not be searched by machine number.
|
||||
//
|
||||
// The number was in the payload (_related_machine puts it there) and rendered
|
||||
// in the table as a chip, but the filter looked only at vendor, service level,
|
||||
// service tag and asset number. So the identifier the feature was built around
|
||||
// - a PC is warranted as a PC but FOUND by the machine it drives - was the one
|
||||
// thing the box could not find.
|
||||
|
||||
function warranty(overrides = {}) {
|
||||
return {
|
||||
vendor: 'Dell',
|
||||
servicelevel: 'ProSupport',
|
||||
servicetag: 'ABC1234',
|
||||
assets: [{
|
||||
assetnumber: 'PC-00912',
|
||||
name: 'Bay 3105 workstation',
|
||||
machine: { machinenumber: '3016', name: 'Makino a61nx' },
|
||||
}],
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
describe('matchesWarrantySearch', () => {
|
||||
it('matches on the machine number', () => {
|
||||
// The reported case.
|
||||
expect(matchesWarrantySearch(warranty(), '3016')).toBe(true)
|
||||
})
|
||||
|
||||
it('matches on the machine name', () => {
|
||||
expect(matchesWarrantySearch(warranty(), 'makino')).toBe(true)
|
||||
})
|
||||
|
||||
it('still matches the fields it always did', () => {
|
||||
expect(matchesWarrantySearch(warranty(), 'dell')).toBe(true)
|
||||
expect(matchesWarrantySearch(warranty(), 'prosupport')).toBe(true)
|
||||
expect(matchesWarrantySearch(warranty(), 'abc1234')).toBe(true)
|
||||
expect(matchesWarrantySearch(warranty(), 'pc-00912')).toBe(true)
|
||||
})
|
||||
|
||||
it('is case insensitive and ignores surrounding space', () => {
|
||||
expect(matchesWarrantySearch(warranty(), ' MAKINO ')).toBe(true)
|
||||
})
|
||||
|
||||
it('does not match an unrelated term', () => {
|
||||
expect(matchesWarrantySearch(warranty(), '9999')).toBe(false)
|
||||
})
|
||||
|
||||
it('keeps every row when the term is empty', () => {
|
||||
expect(matchesWarrantySearch(warranty(), '')).toBe(true)
|
||||
expect(matchesWarrantySearch(warranty(), ' ')).toBe(true)
|
||||
expect(matchesWarrantySearch(warranty(), undefined)).toBe(true)
|
||||
})
|
||||
|
||||
it('handles a warranty whose asset drives no machine', () => {
|
||||
// machine is null whenever the covered asset has no controls link - a
|
||||
// printer, or a PC nobody linked yet. It must not throw.
|
||||
const row = warranty({ assets: [{ assetnumber: 'PR-42', machine: null }] })
|
||||
expect(matchesWarrantySearch(row, 'PR-42')).toBe(true)
|
||||
expect(matchesWarrantySearch(row, '3016')).toBe(false)
|
||||
})
|
||||
|
||||
it('handles a warranty with no linked assets at all', () => {
|
||||
const row = warranty({ assets: [] })
|
||||
expect(matchesWarrantySearch(row, 'dell')).toBe(true)
|
||||
expect(matchesWarrantySearch(row, '3016')).toBe(false)
|
||||
})
|
||||
|
||||
it('tolerates missing fields rather than matching everything', () => {
|
||||
// String(undefined) is 'undefined', which would make a search for 'und'
|
||||
// match every row with an empty field.
|
||||
const row = { vendor: null, servicelevel: undefined, assets: null }
|
||||
expect(matchesWarrantySearch(row, 'und')).toBe(false)
|
||||
expect(matchesWarrantySearch(row, 'null')).toBe(false)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user