List pages kept the current page in local state, so clicking into an asset and hitting Back remounted the list at page 1. A shared useListQuery composable now mirrors the page (and search term) into the URL query via router.replace across all 18 list pages, so Back restores the page you were on and lists are deep-linkable. Page 1 with no search stays a bare path; changing a filter resets to page 1; unrelated query keys are preserved. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
174 lines
5.4 KiB
Vue
174 lines
5.4 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>
|
|
</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">
|
|
<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">
|
|
<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
|
|
</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'
|
|
|
|
const tools = ref([])
|
|
const types = ref([])
|
|
const loading = ref(true)
|
|
const { page, search, setPage, setSearch } = useListQuery({ onChange: loadTools })
|
|
const typeid = ref('')
|
|
const calibrationstatus = ref('')
|
|
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
|
|
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 || []
|
|
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>
|