Files
shopdb-flask/frontend/src/views/settings/OperatingSystemsList.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

229 lines
7.1 KiB
Vue

<template>
<div>
<div class="page-header">
<h2>Operating Systems</h2>
<button class="btn btn-primary" @click="openModal()">+ Add OS</button>
</div>
<div class="card">
<div v-if="loading" class="loading">Loading...</div>
<template v-else>
<div class="table-container">
<table>
<thead>
<tr>
<th>OS Name</th>
<th>Version</th>
<th>Architecture</th>
<th>End of Life</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="os in items" :key="os.osid">
<td>{{ os.osname }}</td>
<td>{{ os.osversion || '-' }}</td>
<td>{{ os.architecture || '-' }}</td>
<td>
<span v-if="os.endoflife" :class="{ 'text-danger': isPastEol(os.endoflife) }">
{{ os.endoflife }}
</span>
<span v-else>-</span>
</td>
<td class="actions">
<button class="btn btn-secondary btn-sm" @click="openModal(os)">Edit</button>
<button class="btn btn-danger btn-sm" @click="confirmDelete(os)">Delete</button>
</td>
</tr>
<tr v-if="items.length === 0">
<td colspan="5" style="text-align: center; color: var(--text-light);">
No operating systems 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>{{ editing ? 'Edit Operating System' : 'Add Operating System' }}</h3>
</div>
<form @submit.prevent="save">
<div class="modal-body">
<div class="form-group">
<label for="osname">OS Name *</label>
<input id="osname" v-model="form.osname" type="text" class="form-control" required placeholder="Windows 11" />
</div>
<div class="form-row">
<div class="form-group">
<label for="osversion">Version</label>
<input id="osversion" v-model="form.osversion" type="text" class="form-control" placeholder="23H2" />
</div>
<div class="form-group">
<label for="architecture">Architecture</label>
<select id="architecture" v-model="form.architecture" class="form-control">
<option value="">Select...</option>
<option value="x64">x64</option>
<option value="x86">x86</option>
<option value="ARM64">ARM64</option>
</select>
</div>
</div>
<div class="form-group">
<label for="endoflife">End of Life Date</label>
<input id="endoflife" v-model="form.endoflife" type="date" class="form-control" />
</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 Modal -->
<div v-if="showDeleteModal" class="modal-overlay" @click.self="showDeleteModal = false">
<div class="modal">
<div class="modal-header"><h3>Delete Operating System</h3></div>
<div class="modal-body">
<p>Are you sure you want to delete <strong>{{ toDelete?.osname }}</strong>?</p>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" @click="showDeleteModal = false">Cancel</button>
<button class="btn btn-danger" @click="deleteItem">Delete</button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { operatingsystemsApi } from '../../api'
import PaginationBar from '../../components/PaginationBar.vue'
import { useToast } from '../../composables/toast'
import { apiError } from '../../utils/apiError'
import { useListQuery } from '@/composables/listQuery'
const toast = useToast()
const items = ref([])
const loading = ref(true)
const { page, setPage } = useListQuery({ onChange: loadData })
const totalPages = ref(1)
const perPage = ref(20)
const showModal = ref(false)
const editing = ref(null)
const saving = ref(false)
const error = ref('')
const showDeleteModal = ref(false)
const toDelete = ref(null)
const form = ref({ osname: '', osversion: '', architecture: '', endoflife: '' })
onMounted(() => loadData())
async function loadData() {
loading.value = true
try {
const response = await operatingsystemsApi.list({ page: page.value, perpage: perPage.value })
items.value = response.data.data || []
totalPages.value = response.data.meta?.pagination?.totalpages || response.data.meta?.pagination?.total_pages || 1
} catch (err) {
console.error('Error loading operating systems:', err)
} finally {
loading.value = false
}
}
function goToPage(p) { setPage(p); loadData() }
function changePerPage(newPerPage) {
perPage.value = newPerPage
setPage(1)
loadData()
}
function isPastEol(date) {
return new Date(date) < new Date()
}
function openModal(item = null) {
editing.value = item
form.value = item ? {
osname: item.osname || '',
osversion: item.osversion || '',
architecture: item.architecture || '',
endoflife: item.endoflife || ''
} : { osname: '', osversion: '', architecture: '', endoflife: '' }
error.value = ''
showModal.value = true
}
function closeModal() { showModal.value = false; editing.value = null }
async function save() {
error.value = ''
saving.value = true
try {
const data = { ...form.value }
if (!data.endoflife) data.endoflife = null
if (editing.value) {
await operatingsystemsApi.update(editing.value.osid, data)
} else {
await operatingsystemsApi.create(data)
}
closeModal()
loadData()
} catch (err) {
error.value = apiError(err, 'Failed to save')
} finally {
saving.value = false
}
}
function confirmDelete(item) { toDelete.value = item; showDeleteModal.value = true }
async function deleteItem() {
try {
await operatingsystemsApi.delete(toDelete.value.osid)
showDeleteModal.value = false
toDelete.value = null
loadData()
} catch (err) {
toast.error('Failed to delete')
}
}
</script>
<style scoped>
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
.text-danger {
color: var(--danger);
font-weight: 500;
}
</style>