Initial commit: Shop Database Flask Application
Flask backend with Vue 3 frontend for shop floor machine management. Includes database schema export for MySQL shopdb_flask database. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
176
frontend/src/views/settings/BusinessUnitsList.vue
Normal file
176
frontend/src/views/settings/BusinessUnitsList.vue
Normal file
@@ -0,0 +1,176 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="page-header">
|
||||
<h2>Business Units</h2>
|
||||
<button class="btn btn-primary" @click="openModal()">+ Add Business Unit</button>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div v-if="loading" class="loading">Loading...</div>
|
||||
|
||||
<template v-else>
|
||||
<div class="table-container">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Business Unit</th>
|
||||
<th>Code</th>
|
||||
<th>Description</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="bu in items" :key="bu.businessunitid">
|
||||
<td>{{ bu.businessunit }}</td>
|
||||
<td>{{ bu.code || '-' }}</td>
|
||||
<td>{{ bu.description || '-' }}</td>
|
||||
<td class="actions">
|
||||
<button class="btn btn-secondary btn-sm" @click="openModal(bu)">Edit</button>
|
||||
<button class="btn btn-danger btn-sm" @click="confirmDelete(bu)">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="items.length === 0">
|
||||
<td colspan="4" style="text-align: center; color: var(--text-light);">
|
||||
No business units found
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="pagination" v-if="totalPages > 1">
|
||||
<button v-for="p in totalPages" :key="p" :class="{ active: p === page }" @click="goToPage(p)">
|
||||
{{ p }}
|
||||
</button>
|
||||
</div>
|
||||
</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 Business Unit' : 'Add Business Unit' }}</h3>
|
||||
</div>
|
||||
<form @submit.prevent="save">
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label for="businessunit">Business Unit Name *</label>
|
||||
<input id="businessunit" v-model="form.businessunit" type="text" class="form-control" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="code">Code</label>
|
||||
<input id="code" v-model="form.code" type="text" class="form-control" placeholder="e.g., ENGR, MFG" />
|
||||
</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 Modal -->
|
||||
<div v-if="showDeleteModal" class="modal-overlay" @click.self="showDeleteModal = false">
|
||||
<div class="modal">
|
||||
<div class="modal-header"><h3>Delete Business Unit</h3></div>
|
||||
<div class="modal-body">
|
||||
<p>Are you sure you want to delete <strong>{{ toDelete?.businessunit }}</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 { businessunitsApi } from '../../api'
|
||||
|
||||
const items = ref([])
|
||||
const loading = ref(true)
|
||||
const page = ref(1)
|
||||
const totalPages = ref(1)
|
||||
|
||||
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({ businessunit: '', code: '', description: '' })
|
||||
|
||||
onMounted(() => loadData())
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await businessunitsApi.list({ page: page.value, perpage: 20 })
|
||||
items.value = response.data.data || []
|
||||
totalPages.value = response.data.meta?.pages || 1
|
||||
} catch (err) {
|
||||
console.error('Error loading business units:', err)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function goToPage(p) { page.value = p; loadData() }
|
||||
|
||||
function openModal(item = null) {
|
||||
editing.value = item
|
||||
form.value = item ? {
|
||||
businessunit: item.businessunit || '',
|
||||
code: item.code || '',
|
||||
description: item.description || ''
|
||||
} : { businessunit: '', code: '', description: '' }
|
||||
error.value = ''
|
||||
showModal.value = true
|
||||
}
|
||||
|
||||
function closeModal() { showModal.value = false; editing.value = null }
|
||||
|
||||
async function save() {
|
||||
error.value = ''
|
||||
saving.value = true
|
||||
try {
|
||||
if (editing.value) {
|
||||
await businessunitsApi.update(editing.value.businessunitid, form.value)
|
||||
} else {
|
||||
await businessunitsApi.create(form.value)
|
||||
}
|
||||
closeModal()
|
||||
loadData()
|
||||
} catch (err) {
|
||||
error.value = err.response?.data?.message || 'Failed to save'
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function confirmDelete(item) { toDelete.value = item; showDeleteModal.value = true }
|
||||
|
||||
async function deleteItem() {
|
||||
try {
|
||||
await businessunitsApi.delete(toDelete.value.businessunitid)
|
||||
showDeleteModal.value = false
|
||||
toDelete.value = null
|
||||
loadData()
|
||||
} catch (err) {
|
||||
alert('Failed to delete')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
324
frontend/src/views/settings/LocationsList.vue
Normal file
324
frontend/src/views/settings/LocationsList.vue
Normal file
@@ -0,0 +1,324 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="page-header">
|
||||
<h2>Locations</h2>
|
||||
<button class="btn btn-primary" @click="openModal()">+ Add Location</button>
|
||||
</div>
|
||||
|
||||
<!-- Search -->
|
||||
<div class="filters">
|
||||
<input
|
||||
v-model="search"
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="Search locations..."
|
||||
@input="debouncedSearch"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div v-if="loading" class="loading">Loading...</div>
|
||||
|
||||
<template v-else>
|
||||
<div class="table-container">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Location Name</th>
|
||||
<th>Building</th>
|
||||
<th>Floor</th>
|
||||
<th>Room</th>
|
||||
<th>Description</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="loc in locations" :key="loc.locationid">
|
||||
<td>{{ loc.locationname }}</td>
|
||||
<td>{{ loc.building || '-' }}</td>
|
||||
<td>{{ loc.floor || '-' }}</td>
|
||||
<td>{{ loc.room || '-' }}</td>
|
||||
<td>{{ loc.description || '-' }}</td>
|
||||
<td class="actions">
|
||||
<button
|
||||
class="btn btn-secondary btn-sm"
|
||||
@click="openModal(loc)"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-danger btn-sm"
|
||||
@click="confirmDelete(loc)"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="locations.length === 0">
|
||||
<td colspan="6" style="text-align: center; color: var(--text-light);">
|
||||
No locations found
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="pagination" v-if="totalPages > 1">
|
||||
<button
|
||||
v-for="p in totalPages"
|
||||
:key="p"
|
||||
:class="{ active: p === page }"
|
||||
@click="goToPage(p)"
|
||||
>
|
||||
{{ p }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Add/Edit Modal -->
|
||||
<div v-if="showModal" class="modal-overlay" @click.self="closeModal">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<h3>{{ editingLocation ? 'Edit Location' : 'Add Location' }}</h3>
|
||||
</div>
|
||||
<form @submit.prevent="saveLocation">
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label for="locationname">Location Name *</label>
|
||||
<input
|
||||
id="locationname"
|
||||
v-model="form.locationname"
|
||||
type="text"
|
||||
class="form-control"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="building">Building</label>
|
||||
<input
|
||||
id="building"
|
||||
v-model="form.building"
|
||||
type="text"
|
||||
class="form-control"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="floor">Floor</label>
|
||||
<input
|
||||
id="floor"
|
||||
v-model="form.floor"
|
||||
type="text"
|
||||
class="form-control"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="room">Room</label>
|
||||
<input
|
||||
id="room"
|
||||
v-model="form.room"
|
||||
type="text"
|
||||
class="form-control"
|
||||
/>
|
||||
</div>
|
||||
</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 class="form-group">
|
||||
<label for="mapimage">Map Image URL</label>
|
||||
<input
|
||||
id="mapimage"
|
||||
v-model="form.mapimage"
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="Optional floor plan image URL"
|
||||
/>
|
||||
</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 Location</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Are you sure you want to delete <strong>{{ locationToDelete?.locationname }}</strong>?</p>
|
||||
<p style="color: var(--text-light); font-size: 0.875rem;">
|
||||
This may affect machines assigned to this location.
|
||||
</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" @click="showDeleteModal = false">Cancel</button>
|
||||
<button class="btn btn-danger" @click="deleteLocation">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { locationsApi } from '../../api'
|
||||
|
||||
const locations = ref([])
|
||||
const loading = ref(true)
|
||||
const search = ref('')
|
||||
const page = ref(1)
|
||||
const totalPages = ref(1)
|
||||
|
||||
const showModal = ref(false)
|
||||
const editingLocation = ref(null)
|
||||
const saving = ref(false)
|
||||
const error = ref('')
|
||||
|
||||
const showDeleteModal = ref(false)
|
||||
const locationToDelete = ref(null)
|
||||
|
||||
const form = ref({
|
||||
locationname: '',
|
||||
building: '',
|
||||
floor: '',
|
||||
room: '',
|
||||
description: '',
|
||||
mapimage: ''
|
||||
})
|
||||
|
||||
let searchTimeout = null
|
||||
|
||||
onMounted(() => {
|
||||
loadLocations()
|
||||
})
|
||||
|
||||
async function loadLocations() {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
page: page.value,
|
||||
perpage: 20
|
||||
}
|
||||
if (search.value) params.search = search.value
|
||||
|
||||
const response = await locationsApi.list(params)
|
||||
locations.value = response.data.data || []
|
||||
totalPages.value = response.data.meta?.pages || 1
|
||||
} catch (err) {
|
||||
console.error('Error loading locations:', err)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function debouncedSearch() {
|
||||
clearTimeout(searchTimeout)
|
||||
searchTimeout = setTimeout(() => {
|
||||
page.value = 1
|
||||
loadLocations()
|
||||
}, 300)
|
||||
}
|
||||
|
||||
function goToPage(p) {
|
||||
page.value = p
|
||||
loadLocations()
|
||||
}
|
||||
|
||||
function openModal(loc = null) {
|
||||
editingLocation.value = loc
|
||||
if (loc) {
|
||||
form.value = {
|
||||
locationname: loc.locationname || '',
|
||||
building: loc.building || '',
|
||||
floor: loc.floor || '',
|
||||
room: loc.room || '',
|
||||
description: loc.description || '',
|
||||
mapimage: loc.mapimage || ''
|
||||
}
|
||||
} else {
|
||||
form.value = {
|
||||
locationname: '',
|
||||
building: '',
|
||||
floor: '',
|
||||
room: '',
|
||||
description: '',
|
||||
mapimage: ''
|
||||
}
|
||||
}
|
||||
error.value = ''
|
||||
showModal.value = true
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
showModal.value = false
|
||||
editingLocation.value = null
|
||||
}
|
||||
|
||||
async function saveLocation() {
|
||||
error.value = ''
|
||||
saving.value = true
|
||||
|
||||
try {
|
||||
if (editingLocation.value) {
|
||||
await locationsApi.update(editingLocation.value.locationid, form.value)
|
||||
} else {
|
||||
await locationsApi.create(form.value)
|
||||
}
|
||||
closeModal()
|
||||
loadLocations()
|
||||
} catch (err) {
|
||||
console.error('Error saving location:', err)
|
||||
error.value = err.response?.data?.message || 'Failed to save location'
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function confirmDelete(loc) {
|
||||
locationToDelete.value = loc
|
||||
showDeleteModal.value = true
|
||||
}
|
||||
|
||||
async function deleteLocation() {
|
||||
try {
|
||||
await locationsApi.delete(locationToDelete.value.locationid)
|
||||
showDeleteModal.value = false
|
||||
locationToDelete.value = null
|
||||
loadLocations()
|
||||
} catch (err) {
|
||||
console.error('Error deleting location:', err)
|
||||
alert('Failed to delete location')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 1rem;
|
||||
}
|
||||
</style>
|
||||
269
frontend/src/views/settings/MachineTypesList.vue
Normal file
269
frontend/src/views/settings/MachineTypesList.vue
Normal file
@@ -0,0 +1,269 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="page-header">
|
||||
<h2>Machine Types</h2>
|
||||
<button class="btn btn-primary" @click="openModal()">+ Add Type</button>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div v-if="loading" class="loading">Loading...</div>
|
||||
|
||||
<template v-else>
|
||||
<div class="table-container">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Machine Type</th>
|
||||
<th>Category</th>
|
||||
<th>Description</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="mt in machineTypes" :key="mt.machinetypeid">
|
||||
<td>{{ mt.machinetype }}</td>
|
||||
<td>
|
||||
<span class="badge" :class="getCategoryClass(mt.category)">
|
||||
{{ mt.category }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ mt.description || '-' }}</td>
|
||||
<td class="actions">
|
||||
<button
|
||||
class="btn btn-secondary btn-sm"
|
||||
@click="openModal(mt)"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-danger btn-sm"
|
||||
@click="confirmDelete(mt)"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="machineTypes.length === 0">
|
||||
<td colspan="4" style="text-align: center; color: var(--text-light);">
|
||||
No machine types found
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="pagination" v-if="totalPages > 1">
|
||||
<button
|
||||
v-for="p in totalPages"
|
||||
:key="p"
|
||||
:class="{ active: p === page }"
|
||||
@click="goToPage(p)"
|
||||
>
|
||||
{{ p }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Add/Edit Modal -->
|
||||
<div v-if="showModal" class="modal-overlay" @click.self="closeModal">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<h3>{{ editingType ? 'Edit Machine Type' : 'Add Machine Type' }}</h3>
|
||||
</div>
|
||||
<form @submit.prevent="saveType">
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label for="machinetype">Type Name *</label>
|
||||
<input
|
||||
id="machinetype"
|
||||
v-model="form.machinetype"
|
||||
type="text"
|
||||
class="form-control"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="category">Category *</label>
|
||||
<select
|
||||
id="category"
|
||||
v-model="form.category"
|
||||
class="form-control"
|
||||
required
|
||||
>
|
||||
<option value="">Select category...</option>
|
||||
<option value="Equipment">Equipment</option>
|
||||
<option value="PC">PC</option>
|
||||
<option value="Network">Network</option>
|
||||
<option value="Printer">Printer</option>
|
||||
</select>
|
||||
</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 Machine Type</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Are you sure you want to delete <strong>{{ typeToDelete?.machinetype }}</strong>?</p>
|
||||
<p style="color: var(--text-light); font-size: 0.875rem;">
|
||||
This may affect machines using this type.
|
||||
</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" @click="showDeleteModal = false">Cancel</button>
|
||||
<button class="btn btn-danger" @click="deleteType">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { machinetypesApi } from '../../api'
|
||||
|
||||
const machineTypes = ref([])
|
||||
const loading = ref(true)
|
||||
const page = ref(1)
|
||||
const totalPages = ref(1)
|
||||
|
||||
const showModal = ref(false)
|
||||
const editingType = ref(null)
|
||||
const saving = ref(false)
|
||||
const error = ref('')
|
||||
|
||||
const showDeleteModal = ref(false)
|
||||
const typeToDelete = ref(null)
|
||||
|
||||
const form = ref({
|
||||
machinetype: '',
|
||||
category: '',
|
||||
description: ''
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
loadTypes()
|
||||
})
|
||||
|
||||
async function loadTypes() {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
page: page.value,
|
||||
perpage: 20
|
||||
}
|
||||
|
||||
const response = await machinetypesApi.list(params)
|
||||
machineTypes.value = response.data.data || []
|
||||
totalPages.value = response.data.meta?.pages || 1
|
||||
} catch (err) {
|
||||
console.error('Error loading machine types:', err)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function goToPage(p) {
|
||||
page.value = p
|
||||
loadTypes()
|
||||
}
|
||||
|
||||
function openModal(mt = null) {
|
||||
editingType.value = mt
|
||||
if (mt) {
|
||||
form.value = {
|
||||
machinetype: mt.machinetype || '',
|
||||
category: mt.category || '',
|
||||
description: mt.description || ''
|
||||
}
|
||||
} else {
|
||||
form.value = {
|
||||
machinetype: '',
|
||||
category: '',
|
||||
description: ''
|
||||
}
|
||||
}
|
||||
error.value = ''
|
||||
showModal.value = true
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
showModal.value = false
|
||||
editingType.value = null
|
||||
}
|
||||
|
||||
async function saveType() {
|
||||
error.value = ''
|
||||
saving.value = true
|
||||
|
||||
try {
|
||||
if (editingType.value) {
|
||||
await machinetypesApi.update(editingType.value.machinetypeid, form.value)
|
||||
} else {
|
||||
await machinetypesApi.create(form.value)
|
||||
}
|
||||
closeModal()
|
||||
loadTypes()
|
||||
} catch (err) {
|
||||
console.error('Error saving machine type:', err)
|
||||
error.value = err.response?.data?.message || 'Failed to save machine type'
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function confirmDelete(mt) {
|
||||
typeToDelete.value = mt
|
||||
showDeleteModal.value = true
|
||||
}
|
||||
|
||||
async function deleteType() {
|
||||
try {
|
||||
await machinetypesApi.delete(typeToDelete.value.machinetypeid)
|
||||
showDeleteModal.value = false
|
||||
typeToDelete.value = null
|
||||
loadTypes()
|
||||
} catch (err) {
|
||||
console.error('Error deleting machine type:', err)
|
||||
alert('Failed to delete machine type')
|
||||
}
|
||||
}
|
||||
|
||||
function getCategoryClass(category) {
|
||||
if (!category) return 'badge-info'
|
||||
const c = category.toLowerCase()
|
||||
if (c === 'equipment') return 'badge-info'
|
||||
if (c === 'pc') return 'badge-success'
|
||||
if (c === 'network') return 'badge-warning'
|
||||
if (c === 'printer') return 'badge-primary'
|
||||
return 'badge-info'
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Uses global styles from style.css -->
|
||||
362
frontend/src/views/settings/ModelsList.vue
Normal file
362
frontend/src/views/settings/ModelsList.vue
Normal file
@@ -0,0 +1,362 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="page-header">
|
||||
<h2>Models</h2>
|
||||
<button class="btn btn-primary" @click="openModal()">+ Add Model</button>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="filters">
|
||||
<input
|
||||
v-model="search"
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="Search models..."
|
||||
@input="debouncedSearch"
|
||||
/>
|
||||
<select v-model="vendorFilter" class="form-control" @change="loadModels">
|
||||
<option value="">All Vendors</option>
|
||||
<option v-for="v in vendors" :key="v.vendorid" :value="v.vendorid">
|
||||
{{ v.vendor }}
|
||||
</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>Model</th>
|
||||
<th>Vendor</th>
|
||||
<th>Type</th>
|
||||
<th>Documentation</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="m in models" :key="m.modelnumberid">
|
||||
<td>
|
||||
<div>{{ m.modelnumber }}</div>
|
||||
<small v-if="m.description" class="text-muted">{{ m.description }}</small>
|
||||
</td>
|
||||
<td>{{ m.vendor || '-' }}</td>
|
||||
<td>{{ m.machinetype || '-' }}</td>
|
||||
<td>
|
||||
<a v-if="m.documentationurl" :href="m.documentationurl" target="_blank" class="btn btn-sm btn-link">
|
||||
View Docs
|
||||
</a>
|
||||
<span v-else>-</span>
|
||||
</td>
|
||||
<td class="actions">
|
||||
<button class="btn btn-secondary btn-sm" @click="openModal(m)">Edit</button>
|
||||
<button class="btn btn-danger btn-sm" @click="confirmDelete(m)">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="models.length === 0">
|
||||
<td colspan="5" style="text-align: center; color: var(--text-light);">
|
||||
No models found
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="pagination" v-if="totalPages > 1">
|
||||
<button
|
||||
v-for="p in totalPages"
|
||||
:key="p"
|
||||
:class="{ active: p === page }"
|
||||
@click="goToPage(p)"
|
||||
>
|
||||
{{ p }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Add/Edit Modal -->
|
||||
<div v-if="showModal" class="modal-overlay" @click.self="closeModal">
|
||||
<div class="modal modal-lg">
|
||||
<div class="modal-header">
|
||||
<h3>{{ editingModel ? 'Edit Model' : 'Add Model' }}</h3>
|
||||
</div>
|
||||
<form @submit.prevent="saveModel">
|
||||
<div class="modal-body">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="modelnumber">Model Number *</label>
|
||||
<input
|
||||
id="modelnumber"
|
||||
v-model="form.modelnumber"
|
||||
type="text"
|
||||
class="form-control"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="vendorid">Vendor *</label>
|
||||
<select id="vendorid" v-model="form.vendorid" class="form-control" required>
|
||||
<option value="">Select vendor...</option>
|
||||
<option v-for="v in vendors" :key="v.vendorid" :value="v.vendorid">
|
||||
{{ v.vendor }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="machinetypeid">Machine Type</label>
|
||||
<select id="machinetypeid" v-model="form.machinetypeid" class="form-control">
|
||||
<option value="">Select type...</option>
|
||||
<option v-for="mt in machineTypes" :key="mt.machinetypeid" :value="mt.machinetypeid">
|
||||
{{ mt.machinetype }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="description">Description</label>
|
||||
<input id="description" v-model="form.description" type="text" class="form-control" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="documentationurl">Documentation URL</label>
|
||||
<input
|
||||
id="documentationurl"
|
||||
v-model="form.documentationurl"
|
||||
type="url"
|
||||
class="form-control"
|
||||
placeholder="https://..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="imageurl">Image URL</label>
|
||||
<input
|
||||
id="imageurl"
|
||||
v-model="form.imageurl"
|
||||
type="url"
|
||||
class="form-control"
|
||||
placeholder="https://..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="notes">Notes</label>
|
||||
<textarea id="notes" v-model="form.notes" class="form-control" rows="2"></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 Model</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Are you sure you want to delete <strong>{{ modelToDelete?.modelnumber }}</strong>?</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" @click="showDeleteModal = false">Cancel</button>
|
||||
<button class="btn btn-danger" @click="deleteModel">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { modelsApi, vendorsApi, machinetypesApi } from '../../api'
|
||||
|
||||
const models = ref([])
|
||||
const vendors = ref([])
|
||||
const machineTypes = ref([])
|
||||
const loading = ref(true)
|
||||
const search = ref('')
|
||||
const vendorFilter = ref('')
|
||||
const page = ref(1)
|
||||
const totalPages = ref(1)
|
||||
|
||||
const showModal = ref(false)
|
||||
const editingModel = ref(null)
|
||||
const saving = ref(false)
|
||||
const error = ref('')
|
||||
|
||||
const showDeleteModal = ref(false)
|
||||
const modelToDelete = ref(null)
|
||||
|
||||
const form = ref({
|
||||
modelnumber: '',
|
||||
vendorid: '',
|
||||
machinetypeid: '',
|
||||
description: '',
|
||||
documentationurl: '',
|
||||
imageurl: '',
|
||||
notes: ''
|
||||
})
|
||||
|
||||
let searchTimeout = null
|
||||
|
||||
onMounted(async () => {
|
||||
await Promise.all([
|
||||
loadModels(),
|
||||
loadVendors(),
|
||||
loadMachineTypes()
|
||||
])
|
||||
})
|
||||
|
||||
async function loadModels() {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = { page: page.value, perpage: 20 }
|
||||
if (search.value) params.search = search.value
|
||||
if (vendorFilter.value) params.vendor = vendorFilter.value
|
||||
|
||||
const response = await modelsApi.list(params)
|
||||
models.value = response.data.data || []
|
||||
totalPages.value = response.data.meta?.pages || 1
|
||||
} catch (err) {
|
||||
console.error('Error loading models:', err)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadVendors() {
|
||||
try {
|
||||
const response = await vendorsApi.list({ perpage: 100 })
|
||||
vendors.value = response.data.data || []
|
||||
} catch (err) {
|
||||
console.error('Error loading vendors:', err)
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMachineTypes() {
|
||||
try {
|
||||
const response = await machinetypesApi.list({ perpage: 100 })
|
||||
machineTypes.value = response.data.data || []
|
||||
} catch (err) {
|
||||
console.error('Error loading machine types:', err)
|
||||
}
|
||||
}
|
||||
|
||||
function debouncedSearch() {
|
||||
clearTimeout(searchTimeout)
|
||||
searchTimeout = setTimeout(() => {
|
||||
page.value = 1
|
||||
loadModels()
|
||||
}, 300)
|
||||
}
|
||||
|
||||
function goToPage(p) {
|
||||
page.value = p
|
||||
loadModels()
|
||||
}
|
||||
|
||||
function openModal(m = null) {
|
||||
editingModel.value = m
|
||||
if (m) {
|
||||
form.value = {
|
||||
modelnumber: m.modelnumber || '',
|
||||
vendorid: m.vendorid || '',
|
||||
machinetypeid: m.machinetypeid || '',
|
||||
description: m.description || '',
|
||||
documentationurl: m.documentationurl || '',
|
||||
imageurl: m.imageurl || '',
|
||||
notes: m.notes || ''
|
||||
}
|
||||
} else {
|
||||
form.value = {
|
||||
modelnumber: '',
|
||||
vendorid: '',
|
||||
machinetypeid: '',
|
||||
description: '',
|
||||
documentationurl: '',
|
||||
imageurl: '',
|
||||
notes: ''
|
||||
}
|
||||
}
|
||||
error.value = ''
|
||||
showModal.value = true
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
showModal.value = false
|
||||
editingModel.value = null
|
||||
}
|
||||
|
||||
async function saveModel() {
|
||||
error.value = ''
|
||||
saving.value = true
|
||||
|
||||
try {
|
||||
const data = { ...form.value }
|
||||
if (!data.vendorid) data.vendorid = null
|
||||
if (!data.machinetypeid) data.machinetypeid = null
|
||||
|
||||
if (editingModel.value) {
|
||||
await modelsApi.update(editingModel.value.modelnumberid, data)
|
||||
} else {
|
||||
await modelsApi.create(data)
|
||||
}
|
||||
closeModal()
|
||||
loadModels()
|
||||
} catch (err) {
|
||||
console.error('Error saving model:', err)
|
||||
error.value = err.response?.data?.message || 'Failed to save model'
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function confirmDelete(m) {
|
||||
modelToDelete.value = m
|
||||
showDeleteModal.value = true
|
||||
}
|
||||
|
||||
async function deleteModel() {
|
||||
try {
|
||||
await modelsApi.delete(modelToDelete.value.modelnumberid)
|
||||
showDeleteModal.value = false
|
||||
modelToDelete.value = null
|
||||
loadModels()
|
||||
} catch (err) {
|
||||
console.error('Error deleting model:', err)
|
||||
alert('Failed to delete model')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.modal-lg {
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: var(--text-light);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
</style>
|
||||
213
frontend/src/views/settings/OperatingSystemsList.vue
Normal file
213
frontend/src/views/settings/OperatingSystemsList.vue
Normal file
@@ -0,0 +1,213 @@
|
||||
<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>
|
||||
|
||||
<div class="pagination" v-if="totalPages > 1">
|
||||
<button v-for="p in totalPages" :key="p" :class="{ active: p === page }" @click="goToPage(p)">
|
||||
{{ p }}
|
||||
</button>
|
||||
</div>
|
||||
</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'
|
||||
|
||||
const items = ref([])
|
||||
const loading = ref(true)
|
||||
const page = ref(1)
|
||||
const totalPages = ref(1)
|
||||
|
||||
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: 20 })
|
||||
items.value = response.data.data || []
|
||||
totalPages.value = response.data.meta?.pages || 1
|
||||
} catch (err) {
|
||||
console.error('Error loading operating systems:', err)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function goToPage(p) { page.value = p; 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 = err.response?.data?.message || '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) {
|
||||
alert('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>
|
||||
166
frontend/src/views/settings/PCTypesList.vue
Normal file
166
frontend/src/views/settings/PCTypesList.vue
Normal file
@@ -0,0 +1,166 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="page-header">
|
||||
<h2>PC Types</h2>
|
||||
<button class="btn btn-primary" @click="openModal()">+ Add PC Type</button>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div v-if="loading" class="loading">Loading...</div>
|
||||
|
||||
<template v-else>
|
||||
<div class="table-container">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>PC Type</th>
|
||||
<th>Description</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="pt in pcTypes" :key="pt.pctypeid">
|
||||
<td>{{ pt.pctype }}</td>
|
||||
<td>{{ pt.description || '-' }}</td>
|
||||
<td class="actions">
|
||||
<button class="btn btn-secondary btn-sm" @click="openModal(pt)">Edit</button>
|
||||
<button class="btn btn-danger btn-sm" @click="confirmDelete(pt)">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="pcTypes.length === 0">
|
||||
<td colspan="3" style="text-align: center; color: var(--text-light);">
|
||||
No PC types found
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="pagination" v-if="totalPages > 1">
|
||||
<button v-for="p in totalPages" :key="p" :class="{ active: p === page }" @click="goToPage(p)">
|
||||
{{ p }}
|
||||
</button>
|
||||
</div>
|
||||
</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 PC Type' : 'Add PC Type' }}</h3>
|
||||
</div>
|
||||
<form @submit.prevent="save">
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label for="pctype">PC Type *</label>
|
||||
<input id="pctype" v-model="form.pctype" type="text" class="form-control" required />
|
||||
</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 Modal -->
|
||||
<div v-if="showDeleteModal" class="modal-overlay" @click.self="showDeleteModal = false">
|
||||
<div class="modal">
|
||||
<div class="modal-header"><h3>Delete PC Type</h3></div>
|
||||
<div class="modal-body">
|
||||
<p>Are you sure you want to delete <strong>{{ toDelete?.pctype }}</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 { pctypesApi } from '../../api'
|
||||
|
||||
const pcTypes = ref([])
|
||||
const loading = ref(true)
|
||||
const page = ref(1)
|
||||
const totalPages = ref(1)
|
||||
|
||||
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({ pctype: '', description: '' })
|
||||
|
||||
onMounted(() => loadData())
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await pctypesApi.list({ page: page.value, perpage: 20 })
|
||||
pcTypes.value = response.data.data || []
|
||||
totalPages.value = response.data.meta?.pages || 1
|
||||
} catch (err) {
|
||||
console.error('Error loading PC types:', err)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function goToPage(p) { page.value = p; loadData() }
|
||||
|
||||
function openModal(item = null) {
|
||||
editing.value = item
|
||||
form.value = item ? { pctype: item.pctype || '', description: item.description || '' } : { pctype: '', description: '' }
|
||||
error.value = ''
|
||||
showModal.value = true
|
||||
}
|
||||
|
||||
function closeModal() { showModal.value = false; editing.value = null }
|
||||
|
||||
async function save() {
|
||||
error.value = ''
|
||||
saving.value = true
|
||||
try {
|
||||
if (editing.value) {
|
||||
await pctypesApi.update(editing.value.pctypeid, form.value)
|
||||
} else {
|
||||
await pctypesApi.create(form.value)
|
||||
}
|
||||
closeModal()
|
||||
loadData()
|
||||
} catch (err) {
|
||||
error.value = err.response?.data?.message || 'Failed to save'
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function confirmDelete(item) { toDelete.value = item; showDeleteModal.value = true }
|
||||
|
||||
async function deleteItem() {
|
||||
try {
|
||||
await pctypesApi.delete(toDelete.value.pctypeid)
|
||||
showDeleteModal.value = false
|
||||
toDelete.value = null
|
||||
loadData()
|
||||
} catch (err) {
|
||||
alert('Failed to delete')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
102
frontend/src/views/settings/SettingsIndex.vue
Normal file
102
frontend/src/views/settings/SettingsIndex.vue
Normal file
@@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<div class="settings-page">
|
||||
<h1>Settings</h1>
|
||||
|
||||
<div class="settings-grid">
|
||||
<router-link to="/settings/vendors" class="settings-card">
|
||||
<div class="card-icon">🏭</div>
|
||||
<h3>Vendors</h3>
|
||||
<p>Manage equipment vendors and manufacturers</p>
|
||||
</router-link>
|
||||
|
||||
<router-link to="/settings/locations" class="settings-card">
|
||||
<div class="card-icon">📍</div>
|
||||
<h3>Locations</h3>
|
||||
<p>Manage physical locations and sites</p>
|
||||
</router-link>
|
||||
|
||||
<router-link to="/settings/statuses" class="settings-card">
|
||||
<div class="card-icon">🏷️</div>
|
||||
<h3>Statuses</h3>
|
||||
<p>Manage equipment status types</p>
|
||||
</router-link>
|
||||
|
||||
<router-link to="/settings/models" class="settings-card">
|
||||
<div class="card-icon">📦</div>
|
||||
<h3>Models</h3>
|
||||
<p>Manage equipment models by vendor</p>
|
||||
</router-link>
|
||||
|
||||
<router-link to="/settings/machinetypes" class="settings-card">
|
||||
<div class="card-icon">🖥️</div>
|
||||
<h3>Machine Types</h3>
|
||||
<p>Manage machine type categories</p>
|
||||
</router-link>
|
||||
|
||||
<router-link to="/settings/pctypes" class="settings-card">
|
||||
<div class="card-icon">💻</div>
|
||||
<h3>PC Types</h3>
|
||||
<p>Manage PC form factors</p>
|
||||
</router-link>
|
||||
|
||||
<router-link to="/settings/operatingsystems" class="settings-card">
|
||||
<div class="card-icon">⚙️</div>
|
||||
<h3>Operating Systems</h3>
|
||||
<p>Manage OS versions and EOL dates</p>
|
||||
</router-link>
|
||||
|
||||
<router-link to="/settings/businessunits" class="settings-card">
|
||||
<div class="card-icon">🏢</div>
|
||||
<h3>Business Units</h3>
|
||||
<p>Manage organizational units</p>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.settings-page h1 {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.settings-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.settings-card {
|
||||
display: block;
|
||||
padding: 1.5rem;
|
||||
background: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
transition: box-shadow 0.2s, border-color 0.2s;
|
||||
}
|
||||
|
||||
.settings-card:hover {
|
||||
border-color: #1976d2;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.settings-card h3 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.settings-card p {
|
||||
margin: 0;
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
</style>
|
||||
322
frontend/src/views/settings/StatusesList.vue
Normal file
322
frontend/src/views/settings/StatusesList.vue
Normal file
@@ -0,0 +1,322 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="page-header">
|
||||
<h2>Machine 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>{{ 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 -->
|
||||
<div class="pagination" v-if="totalPages > 1">
|
||||
<button
|
||||
v-for="p in totalPages"
|
||||
:key="p"
|
||||
:class="{ active: p === page }"
|
||||
@click="goToPage(p)"
|
||||
>
|
||||
{{ p }}
|
||||
</button>
|
||||
</div>
|
||||
</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 for="color">Color</label>
|
||||
<div class="color-input-row">
|
||||
<input
|
||||
id="color"
|
||||
v-model="form.color"
|
||||
type="color"
|
||||
class="color-picker"
|
||||
/>
|
||||
<input
|
||||
v-model="form.color"
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="#000000"
|
||||
/>
|
||||
</div>
|
||||
<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 machines 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 { statusesApi } from '../../api'
|
||||
|
||||
const statuses = ref([])
|
||||
const loading = ref(true)
|
||||
const page = ref(1)
|
||||
const totalPages = ref(1)
|
||||
|
||||
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: 20
|
||||
}
|
||||
|
||||
const response = await statusesApi.list(params)
|
||||
statuses.value = response.data.data || []
|
||||
totalPages.value = response.data.meta?.pages || 1
|
||||
} catch (err) {
|
||||
console.error('Error loading statuses:', err)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function goToPage(p) {
|
||||
page.value = p
|
||||
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 statusesApi.update(editingStatus.value.statusid, form.value)
|
||||
} else {
|
||||
await statusesApi.create(form.value)
|
||||
}
|
||||
closeModal()
|
||||
loadStatuses()
|
||||
} catch (err) {
|
||||
console.error('Error saving status:', err)
|
||||
error.value = err.response?.data?.message || 'Failed to save status'
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function confirmDelete(s) {
|
||||
statusToDelete.value = s
|
||||
showDeleteModal.value = true
|
||||
}
|
||||
|
||||
async function deleteStatus() {
|
||||
try {
|
||||
await statusesApi.delete(statusToDelete.value.statusid)
|
||||
showDeleteModal.value = false
|
||||
statusToDelete.value = null
|
||||
loadStatuses()
|
||||
} catch (err) {
|
||||
console.error('Error deleting status:', err)
|
||||
error.value = err.response?.data?.message || 'Failed to delete status'
|
||||
alert(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>
|
||||
Reference in New Issue
Block a user