The retire dialog says "History and relationships are kept, and it can be
restored." The API always kept that promise - ?active=false lists retired rows,
PUT {"isactive": true} restores one - and no screen in the product could reach
either. A retired asset simply vanished: the lists filtered it out, global
search missed it, and its detail page still rendered Edit and Retire with
nothing anywhere saying it was retired.
Five list pages gained a "Show retired" toggle that passes active=false, and a
retired row now carries a badge. Five detail pages gained a banner saying the
record is retired and what that means, a Restore button, and the suppression of
Edit and Retire while it is retired - offering Retire on an already-retired
record was the part that made the state invisible.
Verified end to end in a browser: retire a machine, the detail page says so and
offers Restore, the list hides it until the toggle is ticked and badges it when
shown, and Restore puts it back (isactive true again on the API).
One thing this does NOT do: global search still does not reach a retired asset.
That is a separate surface with its own filter, and worth deciding on
deliberately rather than sliding in here - a search that returns retired
records by default would change every result page.
Scope: product
196 lines
6.5 KiB
Vue
196 lines
6.5 KiB
Vue
<template>
|
|
<div>
|
|
<div class="page-header">
|
|
<h2>Measuring Tools</h2>
|
|
<router-link to="/print/asset-label-batch/measuring_tool" class="btn btn-secondary" target="_blank">Print Labels</router-link>
|
|
<router-link to="/measuringtools/new" class="btn btn-primary">Add Measuring Tool</router-link>
|
|
</div>
|
|
|
|
<!-- Filters -->
|
|
<div class="filters">
|
|
<input
|
|
v-model="search"
|
|
type="text"
|
|
class="form-control"
|
|
placeholder="Search measuring tools..."
|
|
@input="debouncedSearch"
|
|
/>
|
|
<select v-model="typeid" class="form-control" @change="reload">
|
|
<option value="">All types</option>
|
|
<option v-for="t in types" :key="t.measuringtooltypeid" :value="t.measuringtooltypeid">
|
|
{{ t.name }}
|
|
</option>
|
|
</select>
|
|
<select v-model="calibrationstatus" class="form-control" @change="reload">
|
|
<option value="">All calibration statuses</option>
|
|
<option value="overdue">Overdue</option>
|
|
<option value="duesoon">Due soon</option>
|
|
<option value="current">Current</option>
|
|
<option value="unknown">Unknown</option>
|
|
</select>
|
|
<!-- Retire is a soft delete and the confirm dialog promises the record
|
|
"can be restored". The API kept that promise (?active=false lists
|
|
them, PUT isactive restores); nothing in the UI could reach it. -->
|
|
<label class="checkbox-label">
|
|
<input type="checkbox" v-model="showRetired" @change="loadTools" />
|
|
Show retired
|
|
</label>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div v-if="loading" class="loading">Loading...</div>
|
|
|
|
<template v-else>
|
|
<div class="table-container">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Asset #</th>
|
|
<th>Name</th>
|
|
<th>Serial Number</th>
|
|
<th>Type</th>
|
|
<th>Next Calibration</th>
|
|
<th>Calibration</th>
|
|
<th>Location</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="item in tools" :key="item.assetid" class="clickable-row" @click="$router.push(`/measuringtools/${item.measuringtool?.measuringtoolid || item.assetid}`)">
|
|
<td>{{ item.assetnumber }}</td>
|
|
<td>{{ item.name || '-' }}</td>
|
|
<td class="mono">{{ item.serialnumber || '-' }}</td>
|
|
<td>{{ item.measuringtool?.measuringtooltypename || '-' }}</td>
|
|
<td>{{ item.measuringtool?.nextcalibrationdate ? formatDate(item.measuringtool.nextcalibrationdate) : '-' }}</td>
|
|
<td>
|
|
<span class="badge" :style="colorStyle(item.measuringtool?.calibrationstatuscolor)">
|
|
{{ statusLabel(item.measuringtool?.calibrationstatus) }}
|
|
</span>
|
|
</td>
|
|
<td>{{ item.locationname || '-' }}</td>
|
|
<td class="actions" @click.stop>
|
|
<router-link
|
|
:to="`/measuringtools/${item.measuringtool?.measuringtoolid || item.assetid}`"
|
|
class="btn btn-secondary btn-sm"
|
|
>
|
|
View
|
|
</router-link>
|
|
</td>
|
|
</tr>
|
|
<tr v-if="tools.length === 0">
|
|
<td colspan="8" style="text-align: center; color: var(--text-light);">
|
|
No measuring tools found
|
|
<SearchSuggestion :suggestion="suggestion" @pick="applySuggestion" />
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<PaginationBar
|
|
:page="page"
|
|
:totalPages="totalPages"
|
|
:perPage="perPage"
|
|
@update:page="goToPage"
|
|
@update:perPage="changePerPage"
|
|
/>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted } from 'vue'
|
|
import { colorStyle } from '@/utils/colorStyle'
|
|
import { measuringtoolsApi } from '@/api'
|
|
import PaginationBar from '@/components/PaginationBar.vue'
|
|
import { useListQuery } from '@/composables/listQuery'
|
|
import SearchSuggestion from '@/components/SearchSuggestion.vue'
|
|
|
|
const tools = ref([])
|
|
const types = ref([])
|
|
const loading = ref(true)
|
|
const { page, search, setPage, setSearch, suggestion, reportCount } = useListQuery({ onChange: loadTools })
|
|
|
|
// Take the offered spelling. setSearch only syncs the URL and the watcher
|
|
// that reloads is suppressed because search.value already holds the new
|
|
// term, so load directly.
|
|
function applySuggestion(word) {
|
|
search.value = word
|
|
setSearch(word)
|
|
loadTools()
|
|
}
|
|
const typeid = ref('')
|
|
const calibrationstatus = ref('')
|
|
const showRetired = ref(false)
|
|
const totalPages = ref(1)
|
|
const perPage = ref(20)
|
|
|
|
let searchTimeout = null
|
|
|
|
const STATUS_LABELS = { overdue: 'Overdue', duesoon: 'Due Soon', current: 'Current', unknown: 'Unknown' }
|
|
function statusLabel(status) { return STATUS_LABELS[status] || 'Unknown' }
|
|
function formatDate(d) { return new Date(d + 'T00:00:00').toLocaleDateString() }
|
|
|
|
onMounted(async () => {
|
|
try {
|
|
const typesResponse = await measuringtoolsApi.types.list({ perpage: 200 })
|
|
types.value = typesResponse.data.data || []
|
|
} catch (err) {
|
|
console.error('Error loading types:', err)
|
|
}
|
|
loadTools()
|
|
})
|
|
|
|
async function loadTools() {
|
|
loading.value = true
|
|
try {
|
|
const params = { page: page.value, perpage: perPage.value }
|
|
if (search.value) params.search = search.value
|
|
// active=false means "include retired", matching the API.
|
|
if (showRetired.value) params.active = 'false'
|
|
if (typeid.value) params.typeid = typeid.value
|
|
if (calibrationstatus.value) params.calibrationstatus = calibrationstatus.value
|
|
|
|
const response = await measuringtoolsApi.list(params)
|
|
tools.value = response.data.data || []
|
|
reportCount(tools.value.length)
|
|
totalPages.value = response.data.meta?.pagination?.totalpages || response.data.meta?.pagination?.total_pages || 1
|
|
} catch (err) {
|
|
console.error('Error loading measuring tools:', err)
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
function reload() {
|
|
setPage(1)
|
|
loadTools()
|
|
}
|
|
|
|
function debouncedSearch() {
|
|
clearTimeout(searchTimeout)
|
|
searchTimeout = setTimeout(() => {
|
|
setSearch(search.value)
|
|
loadTools()
|
|
}, 300)
|
|
}
|
|
|
|
function goToPage(p) {
|
|
setPage(p)
|
|
loadTools()
|
|
}
|
|
|
|
function changePerPage(newPerPage) {
|
|
perPage.value = newPerPage
|
|
setPage(1)
|
|
loadTools()
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.mono {
|
|
font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
|
|
}
|
|
</style>
|