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:
cproudlock
2026-06-26 12:09:05 -04:00
parent a58feebe7a
commit 6fb8adc256
7 changed files with 192 additions and 10 deletions

View File

@@ -202,6 +202,11 @@ export const locationsApi = {
},
delete(id) {
return api.delete(`/locations/${id}`)
},
types: {
list() {
return api.get('/locations/types')
}
}
}

View File

@@ -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()