Files
shopdb-flask/plugins/computers/frontend/views/OperatingSystemsList.vue
cproudlock d8fe0a48b2
Some checks failed
CI / backend (push) Failing after 8s
CI / naming (push) Successful in 1s
CI / frontend (push) Successful in 8s
CI / migrations-mysql (push) Failing after 7s
Stop a stray click outside a modal discarding what was typed
Operators reported losing a part-filled form by clicking slightly outside it.
Every data-entry modal closed on a backdrop click with no warning and no way
back - the worst possible response to a misplaced click, and it happens most to
someone adding their first records at a new site.

Close-on-overlay is removed from 35 modals across 30 files: anything containing
an input, textarea, select or v-model. They still close by Cancel or the X.

Confirmation dialogs keep it, because a delete prompt holds nothing to lose and
dismissing one by clicking away is the behaviour people expect. VendorsList
shows the distinction - its edit form no longer closes that way, its delete
confirmation still does.

The shared Modal component now defaults closeOnOverlay to FALSE. Every current
caller holds a form, a checkout, a stock adjustment or a map position being
picked, and not one passed the prop, so all of them had the same fault. A modal
that genuinely wants dismissing that way opts in explicitly.

Also regroups the operator console menu, which had grown to numbers 1-9 plus
three letters bolted on with no order to them. Actions are now grouped by what
they touch, keyed by their first letter, and the old numbers still work so
nobody who has used it for months is stopped by a rearrangement.

The menu also warns when the server is not fully provisioned and names the key
that fixes it, instead of reporting it as ordinary status lines that read as
normal unless you already knew what to look for. That check is cached for the
session because it shells out to flask twice and the answer does not change
while somebody reads the screen.
2026-08-05 13:42:20 -04:00

229 lines
7.0 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">
<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>