221 lines
11 KiB
Vue
221 lines
11 KiB
Vue
<template>
|
|
<BaseLayout>
|
|
<main class="content">
|
|
<div class="container-fluid p-0">
|
|
<h1 class="h3 mb-3">Storage Locations</h1>
|
|
<div class="row mb-3">
|
|
<div class="col-12 col-md-4">
|
|
<label for="ownerSelect" class="form-label">Viewing storage locations for</label>
|
|
<select id="ownerSelect" class="form-select" v-model="selectedOwner">
|
|
<option :value="user">{{ user }} (you)</option>
|
|
<option v-for="group in ownerGroups" :value="group.handle" :key="group.handle">
|
|
{{ group.handle }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="row">
|
|
<div class="col-12 col-xl-12">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5 class="card-title">{{ selectedOwner }}'s Storage Locations</h5>
|
|
<button v-if="layout === 'grid'" @click="layout = 'table'" class="btn">
|
|
<b-icon-list></b-icon-list>
|
|
</button>
|
|
<button v-else @click="layout = 'grid'" class="btn">
|
|
<b-icon-grid></b-icon-grid>
|
|
</button>
|
|
</div>
|
|
<table class="table table-striped" v-if="layout === 'table'">
|
|
<thead>
|
|
<tr>
|
|
<th style="width:40%;">Name</th>
|
|
<th style="width:25%">Path</th>
|
|
<th class="d-none d-md-table-cell" style="width:25%">Category</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="location in locations" :key="location.id">
|
|
<td>
|
|
<router-link :to="locationRoute(location)">{{ location.name }}</router-link>
|
|
</td>
|
|
<td class="d-none d-md-table-cell">
|
|
<span class="text-muted">{{ location.path }}</span>
|
|
</td>
|
|
<td class="d-none d-md-table-cell">
|
|
<span class="badge bg-info text-white" v-if="location.category">{{ location.category }}</span>
|
|
<span class="text-muted" v-else>-</span>
|
|
</td>
|
|
<td class="table-action">
|
|
<router-link :to="`${locationRoute(location)}/edit`">
|
|
<b-icon-pencil-square></b-icon-pencil-square>
|
|
</router-link>
|
|
<a :href="`${locationRoute(location)}/delete`" @click.prevent="tryDeleteLocation(location)">
|
|
<b-icon-trash></b-icon-trash>
|
|
</a>
|
|
<router-link v-if="shortIdLink(location)" :to="shortIdLink(location)">
|
|
<b-icon-link></b-icon-link>
|
|
</router-link>
|
|
<router-link v-if="printLinkFor(location)" :to="printLinkFor(location)">
|
|
<b-icon-qr-code></b-icon-qr-code>
|
|
</router-link>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
<div class="card-body" v-else>
|
|
<div class="row">
|
|
<div class="col-12 col-md-4 col-lg-3 col-xl-2" v-for="location in locations"
|
|
:key="location.id">
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<h5 class="card-title mb-0">
|
|
<router-link :to="locationRoute(location)">
|
|
{{ location.name }}
|
|
</router-link>
|
|
</h5>
|
|
<div class="card-text text-black-50">
|
|
<small class="text-muted d-block">{{ location.path }}</small>
|
|
<span class="badge bg-info text-white" v-if="location.category">{{ location.category }}</span>
|
|
</div>
|
|
<div class="card-text" v-if="location.description">
|
|
<small>{{ location.description }}</small>
|
|
</div>
|
|
<div class="btn-group mt-2">
|
|
<button class="btn btn-danger btn-sm"
|
|
@click="tryDeleteLocation(location)">Delete
|
|
</button>
|
|
<router-link :to="`${locationRoute(location)}/edit`"
|
|
class="btn btn-primary btn-sm">Edit
|
|
</router-link>
|
|
<router-link v-if="shortIdLink(location)" :to="shortIdLink(location)"
|
|
class="btn btn-secondary btn-sm">
|
|
<b-icon-link></b-icon-link>
|
|
</router-link>
|
|
<router-link v-if="printLinkFor(location)" :to="printLinkFor(location)"
|
|
class="btn btn-secondary btn-sm">
|
|
<b-icon-qr-code></b-icon-qr-code>
|
|
</router-link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
<div class="card">
|
|
<button class="btn" @click="fetchLocationsForOwner">Refresh</button>
|
|
<router-link :to="addLocationRoute" class="btn btn-primary">Add</router-link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</BaseLayout>
|
|
</template>
|
|
|
|
<script>
|
|
import {mapActions, mapGetters, mapState} from "vuex";
|
|
import * as BIcons from "bootstrap-icons-vue";
|
|
import BaseLayout from "@/components/BaseLayout.vue";
|
|
import {shortenedRoute, encodeHandleForUrl} from "@/router";
|
|
|
|
export default {
|
|
name: "StorageLocation",
|
|
data() {
|
|
return {
|
|
layout: "grid",
|
|
selectedOwner: null,
|
|
}
|
|
},
|
|
components: {
|
|
BaseLayout,
|
|
...BIcons
|
|
},
|
|
computed: {
|
|
...mapGetters(["identityIdByHandle", "groupIdByHandle", "groupStorageLocations"]),
|
|
...mapState(["user", "storage_locations", "groups", "groupMemberships"]),
|
|
// Groups hosted here plus groups only known via a GroupMembership pointer - see
|
|
// Groups.vue's allGroups and Inventory.vue's ownerGroups for the same merge/dedupe.
|
|
ownerGroups() {
|
|
const hostedHandles = new Set(this.groups.map(group => group.handle))
|
|
const foreign = this.groupMemberships.filter(m => !hostedHandles.has(m.handle))
|
|
return [...this.groups, ...foreign].sort((a, b) => a.handle.localeCompare(b.handle))
|
|
},
|
|
locations() {
|
|
return this.selectedOwner === this.user ? this.storage_locations : this.groupStorageLocations(this.selectedOwner)
|
|
},
|
|
addLocationRoute() {
|
|
return this.selectedOwner === this.user
|
|
? '/storage-locations/new'
|
|
: `/storage-locations/new?group=${encodeHandleForUrl(this.selectedOwner)}`
|
|
}
|
|
},
|
|
methods: {
|
|
...mapActions(["fetchStorageLocations", "fetchGroupStorageLocations", "deleteStorageLocation",
|
|
"fetchIdMap", "fetchGroups", "fetchGroupMemberships"]),
|
|
fetchLocationsForOwner() {
|
|
return this.selectedOwner === this.user
|
|
? this.fetchStorageLocations()
|
|
: this.fetchGroupStorageLocations({groupHandle: this.selectedOwner})
|
|
},
|
|
tryDeleteLocation(location) {
|
|
this.deleteStorageLocation(location).then(() => {
|
|
this.fetchLocationsForOwner()
|
|
})
|
|
},
|
|
locationRoute(location) {
|
|
return `/storage-locations/${encodeHandleForUrl(location.owner_group || location.owner)}/${location.id}`
|
|
},
|
|
shortIdLink(location) {
|
|
if (location.owner_group) {
|
|
const owner_group_id = this.groupIdByHandle[location.owner_group]
|
|
if (owner_group_id === undefined) return null
|
|
return shortenedRoute({
|
|
kind: 'group_storage_location', owner_group_id, storage_location_id: location.id
|
|
})
|
|
}
|
|
const owner_identity_id = this.identityIdByHandle[location.owner]
|
|
if (owner_identity_id === undefined) return null
|
|
return shortenedRoute({kind: 'storage_location', owner_identity_id, storage_location_id: location.id})
|
|
},
|
|
// Routes to Print.vue with this location's raw identity, same shape as Inventory.vue's
|
|
// printLinkFor. See docs/implementation.md#print-link-shape-for-storage-locations.
|
|
printLinkFor(location) {
|
|
const userHandle = location.owner_group || location.owner
|
|
return {path: '/print', query: {kind: 'storage-location', userHandle, location: location.id}}
|
|
},
|
|
},
|
|
watch: {
|
|
selectedOwner() {
|
|
this.fetchLocationsForOwner()
|
|
}
|
|
},
|
|
created() {
|
|
// Set before the first render so addLocationRoute/locations never see selectedOwner=null
|
|
// while user is already populated (which would misroute to the group branch).
|
|
this.selectedOwner = this.user
|
|
},
|
|
async mounted() {
|
|
await this.fetchIdMap()
|
|
await this.fetchGroups()
|
|
await this.fetchGroupMemberships()
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.card-text small {
|
|
font-size: 0.8rem;
|
|
}
|
|
|
|
.btn-group {
|
|
width: 100%;
|
|
}
|
|
|
|
.btn-group .btn {
|
|
flex: 1;
|
|
}
|
|
</style>
|