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:
cproudlock
2026-01-13 16:07:34 -05:00
commit 1196de6e88
188 changed files with 19921 additions and 0 deletions

View 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>

View 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>

View 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 -->

View 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>

View 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>

View 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>

View 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>

View 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>