Add locationtypes + location tree (ADR-001)
- New locationtypes lookup (LocationType model) seeded with section, cell, subcell, operation, meetingroom, lab, office, storage, hallway, networkcloset, building. - locations gains locationtypeid + parentlocationid (self-FK) for the site location tree. Migration 7c03; cli reference-data seeds the types. - GET /api/locations/types; locations CRUD accepts type + parent; list/detail return locationtypename + parentlocationname. - Locations settings page: Type column + Type/Parent selectors in the modal. Realizes the operation-as-Location model (operations are locations with locationtypeid='operation'). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -202,6 +202,11 @@ export const locationsApi = {
|
||||
},
|
||||
delete(id) {
|
||||
return api.delete(`/locations/${id}`)
|
||||
},
|
||||
types: {
|
||||
list() {
|
||||
return api.get('/locations/types')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Location Name</th>
|
||||
<th>Type</th>
|
||||
<th>Building</th>
|
||||
<th>Floor</th>
|
||||
<th>Room</th>
|
||||
@@ -35,6 +36,7 @@
|
||||
<tbody>
|
||||
<tr v-for="loc in locations" :key="loc.locationid">
|
||||
<td>{{ loc.locationname }}</td>
|
||||
<td>{{ loc.locationtypename || '-' }}</td>
|
||||
<td>{{ loc.building || '-' }}</td>
|
||||
<td>{{ loc.floor || '-' }}</td>
|
||||
<td>{{ loc.room || '-' }}</td>
|
||||
@@ -55,7 +57,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="locations.length === 0">
|
||||
<td colspan="6" style="text-align: center; color: var(--text-light);">
|
||||
<td colspan="7" style="text-align: center; color: var(--text-light);">
|
||||
No locations found
|
||||
</td>
|
||||
</tr>
|
||||
@@ -125,6 +127,32 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="locationtypeid">Type</label>
|
||||
<select id="locationtypeid" v-model="form.locationtypeid" class="form-control">
|
||||
<option value="">Select type...</option>
|
||||
<option v-for="t in locationTypes" :key="t.locationtypeid" :value="t.locationtypeid">
|
||||
{{ t.locationtype }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="parentlocationid">Parent Location</label>
|
||||
<select id="parentlocationid" v-model="form.parentlocationid" class="form-control">
|
||||
<option value="">None</option>
|
||||
<option
|
||||
v-for="l in parentOptions"
|
||||
:key="l.locationid"
|
||||
:value="l.locationid"
|
||||
>
|
||||
{{ l.locationname }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="description">Description</label>
|
||||
<textarea
|
||||
@@ -180,11 +208,13 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { locationsApi } from '../../api'
|
||||
import PaginationBar from '../../components/PaginationBar.vue'
|
||||
|
||||
const locations = ref([])
|
||||
const locationTypes = ref([])
|
||||
const allLocations = ref([])
|
||||
const loading = ref(true)
|
||||
const search = ref('')
|
||||
const page = ref(1)
|
||||
@@ -205,12 +235,29 @@ const form = ref({
|
||||
floor: '',
|
||||
room: '',
|
||||
description: '',
|
||||
locationtypeid: '',
|
||||
parentlocationid: '',
|
||||
mapimage: ''
|
||||
})
|
||||
|
||||
// parent options = all locations except the one being edited (no self-parent)
|
||||
const parentOptions = computed(() =>
|
||||
allLocations.value.filter(l => l.locationid !== editingLocation.value?.locationid)
|
||||
)
|
||||
|
||||
let searchTimeout = null
|
||||
|
||||
onMounted(() => {
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const [typesRes, allRes] = await Promise.all([
|
||||
locationsApi.types.list(),
|
||||
locationsApi.list({ perpage: 100 })
|
||||
])
|
||||
locationTypes.value = typesRes.data.data || []
|
||||
allLocations.value = allRes.data.data || []
|
||||
} catch (err) {
|
||||
console.error('Error loading location types:', err)
|
||||
}
|
||||
loadLocations()
|
||||
})
|
||||
|
||||
@@ -261,6 +308,8 @@ function openModal(loc = null) {
|
||||
floor: loc.floor || '',
|
||||
room: loc.room || '',
|
||||
description: loc.description || '',
|
||||
locationtypeid: loc.locationtypeid || '',
|
||||
parentlocationid: loc.parentlocationid || '',
|
||||
mapimage: loc.mapimage || ''
|
||||
}
|
||||
} else {
|
||||
@@ -270,6 +319,8 @@ function openModal(loc = null) {
|
||||
floor: '',
|
||||
room: '',
|
||||
description: '',
|
||||
locationtypeid: '',
|
||||
parentlocationid: '',
|
||||
mapimage: ''
|
||||
}
|
||||
}
|
||||
@@ -287,10 +338,15 @@ async function saveLocation() {
|
||||
saving.value = true
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
...form.value,
|
||||
locationtypeid: form.value.locationtypeid || null,
|
||||
parentlocationid: form.value.parentlocationid || null
|
||||
}
|
||||
if (editingLocation.value) {
|
||||
await locationsApi.update(editingLocation.value.locationid, form.value)
|
||||
await locationsApi.update(editingLocation.value.locationid, payload)
|
||||
} else {
|
||||
await locationsApi.create(form.value)
|
||||
await locationsApi.create(payload)
|
||||
}
|
||||
closeModal()
|
||||
loadLocations()
|
||||
|
||||
Reference in New Issue
Block a user