Files
shopdb-flask/frontend/src/views/settings/StatusesList.vue
cproudlock f6dcaef4c0
All checks were successful
CI / backend (push) Successful in 1m25s
CI / naming (push) Successful in 1s
CI / frontend (push) Successful in 7s
Persist list pagination and search in the URL
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>
2026-07-12 14:19:50 -04:00

320 lines
8.3 KiB
Vue

<template>
<div>
<div class="page-header">
<h2>Asset Statuses</h2>
<button class="btn btn-primary" @click="openModal()">+ Add Status</button>
</div>
<div class="card">
<div v-if="loading" class="loading">Loading...</div>
<template v-else>
<div class="table-container">
<table>
<thead>
<tr>
<th>Status</th>
<th>Color</th>
<th>Description</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="s in statuses" :key="s.statusid">
<td>
<span class="status-badge" :style="getStatusStyle(s.color)">
{{ s.status }}
</span>
</td>
<td>
<span class="color-preview" :style="{ backgroundColor: s.color || '#6c757d' }"></span>
{{ s.color || 'default' }}
</td>
<td class="cell-truncate" :title="s.description">{{ s.description || '-' }}</td>
<td class="actions">
<button
class="btn btn-secondary btn-sm"
@click="openModal(s)"
>
Edit
</button>
<button
class="btn btn-danger btn-sm"
@click="confirmDelete(s)"
>
Delete
</button>
</td>
</tr>
<tr v-if="statuses.length === 0">
<td colspan="4" style="text-align: center; color: var(--text-light);">
No statuses found
</td>
</tr>
</tbody>
</table>
</div>
<!-- Pagination -->
<PaginationBar
:page="page"
:totalPages="totalPages"
:perPage="perPage"
@update:page="goToPage"
@update:perPage="changePerPage"
/>
</template>
</div>
<!-- Add/Edit Modal -->
<div v-if="showModal" class="modal-overlay" @click.self="closeModal">
<div class="modal">
<div class="modal-header">
<h3>{{ editingStatus ? 'Edit Status' : 'Add Status' }}</h3>
</div>
<form @submit.prevent="saveStatus">
<div class="modal-body">
<div class="form-group">
<label for="status">Status Name *</label>
<input
id="status"
v-model="form.status"
type="text"
class="form-control"
required
/>
</div>
<div class="form-group">
<label>Color</label>
<ColorSwatchPicker v-model="form.color" />
<small class="form-hint">Used in UI to visually distinguish statuses</small>
</div>
<div class="form-group">
<label for="description">Description</label>
<textarea
id="description"
v-model="form.description"
class="form-control"
rows="3"
></textarea>
</div>
<div v-if="error" class="error-message">{{ error }}</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" @click="closeModal">Cancel</button>
<button type="submit" class="btn btn-primary" :disabled="saving">
{{ saving ? 'Saving...' : 'Save' }}
</button>
</div>
</form>
</div>
</div>
<!-- Delete Confirmation Modal -->
<div v-if="showDeleteModal" class="modal-overlay" @click.self="showDeleteModal = false">
<div class="modal">
<div class="modal-header">
<h3>Delete Status</h3>
</div>
<div class="modal-body">
<p>Are you sure you want to delete <strong>{{ statusToDelete?.status }}</strong>?</p>
<p style="color: var(--text-light); font-size: 0.875rem;">
Cannot delete if assets are using this status.
</p>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" @click="showDeleteModal = false">Cancel</button>
<button class="btn btn-danger" @click="deleteStatus">Delete</button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { assetsApi } from '../../api'
import PaginationBar from '../../components/PaginationBar.vue'
import ColorSwatchPicker from '@/components/ColorSwatchPicker.vue'
import { useToast } from '../../composables/toast'
import { apiError } from '../../utils/apiError'
import { useListQuery } from '@/composables/listQuery'
const toast = useToast()
const statuses = ref([])
const loading = ref(true)
const { page, setPage } = useListQuery({ onChange: loadStatuses })
const totalPages = ref(1)
const perPage = ref(20)
const showModal = ref(false)
const editingStatus = ref(null)
const saving = ref(false)
const error = ref('')
const showDeleteModal = ref(false)
const statusToDelete = ref(null)
const form = ref({
status: '',
color: '#6c757d',
description: ''
})
onMounted(() => {
loadStatuses()
})
async function loadStatuses() {
loading.value = true
try {
const params = {
page: page.value,
perpage: perPage.value
}
const response = await assetsApi.statuses.list(params)
statuses.value = response.data.data || []
totalPages.value = response.data.meta?.pagination?.totalpages || response.data.meta?.pagination?.total_pages || 1
} catch (err) {
console.error('Error loading statuses:', err)
} finally {
loading.value = false
}
}
function goToPage(p) {
setPage(p)
loadStatuses()
}
function changePerPage(newPerPage) {
perPage.value = newPerPage
setPage(1)
loadStatuses()
}
function openModal(s = null) {
editingStatus.value = s
if (s) {
form.value = {
status: s.status || '',
color: s.color || '#6c757d',
description: s.description || ''
}
} else {
form.value = {
status: '',
color: '#6c757d',
description: ''
}
}
error.value = ''
showModal.value = true
}
function closeModal() {
showModal.value = false
editingStatus.value = null
}
async function saveStatus() {
error.value = ''
saving.value = true
try {
if (editingStatus.value) {
await assetsApi.statuses.update(editingStatus.value.statusid, form.value)
} else {
await assetsApi.statuses.create(form.value)
}
closeModal()
loadStatuses()
} catch (err) {
console.error('Error saving status:', err)
error.value = apiError(err, 'Failed to save status')
} finally {
saving.value = false
}
}
function confirmDelete(s) {
statusToDelete.value = s
showDeleteModal.value = true
}
async function deleteStatus() {
try {
await assetsApi.statuses.delete(statusToDelete.value.statusid)
showDeleteModal.value = false
statusToDelete.value = null
loadStatuses()
} catch (err) {
console.error('Error deleting status:', err)
error.value = apiError(err, 'Failed to delete status')
toast.error(error.value)
}
}
function getStatusStyle(color) {
const bgColor = color || '#6c757d'
return {
backgroundColor: bgColor,
color: isLightColor(bgColor) ? '#000' : '#fff'
}
}
function isLightColor(color) {
if (!color) return false
const hex = color.replace('#', '')
const r = parseInt(hex.substr(0, 2), 16)
const g = parseInt(hex.substr(2, 2), 16)
const b = parseInt(hex.substr(4, 2), 16)
const brightness = (r * 299 + g * 587 + b * 114) / 1000
return brightness > 128
}
</script>
<style scoped>
.status-badge {
padding: 0.25rem 0.75rem;
border-radius: 4px;
font-size: 0.875rem;
font-weight: 500;
}
.color-preview {
display: inline-block;
width: 16px;
height: 16px;
border-radius: 3px;
margin-right: 0.5rem;
vertical-align: middle;
border: 1px solid var(--border-color);
}
.color-input-row {
display: flex;
gap: 0.5rem;
align-items: center;
}
.color-picker {
width: 50px;
height: 38px;
padding: 2px;
border: 1px solid var(--border-color);
border-radius: 4px;
cursor: pointer;
}
.form-hint {
color: var(--text-light);
font-size: 0.75rem;
margin-top: 0.25rem;
}
</style>