This commit is contained in:
j3d1 2026-08-19 16:45:20 +02:00
parent 6d2167ac66
commit 4787acd8eb
23 changed files with 1241 additions and 25 deletions

View file

@ -0,0 +1,207 @@
<template>
<BaseLayout>
<main class="content">
<div class="container-fluid p-0">
<h1 class="h3 mb-3">{{ group ? group.handle : '' }}</h1>
<div class="row" v-if="group">
<div class="col-12 col-xl-6">
<div class="card">
<div class="card-header">
<h5 class="card-title">Members</h5>
</div>
<table class="table table-striped">
<thead>
<tr>
<th>Name</th>
<th style="width: 16em">
<a @click="refresh" class="align-middle">
<b-icon-arrow-clockwise></b-icon-arrow-clockwise>
Refresh
</a>
<a @click="show_invite = true" class="align-middle">
<b-icon-plus></b-icon-plus>
Add member
</a>
</th>
</tr>
</thead>
<tbody>
<tr v-if="show_invite">
<td>
<input type="text" class="form-control" placeholder="user@domain"
v-model="invitee">
</td>
<td>
<button class="btn btn-primary" @click="tryInvite">Invite</button>
</td>
</tr>
<tr v-for="member in group.members" :key="member.id">
<td>
<user-name-tag :user="member"/>
</td>
<td class="table-action">
<a href="#" class="align-middle" @click.prevent="tryRemoveMember(member)">
<b-icon-trash></b-icon-trash>
Remove
</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="col-12 col-xl-6">
<div class="card">
<div class="card-header">
<h5 class="card-title">Group inventory</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%">Availability Policy</th>
<th class="d-none d-md-table-cell" style="width:25%">Amount</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="item in items" :key="item.id">
<td>
<router-link :to="`/inventory/${item.id}`">{{ item.name }}</router-link>
</td>
<td class="d-none d-md-table-cell">
<span class="badge bg-secondary text-white">{{ item.availability_policy }}</span>
</td>
<td class="d-none d-md-table-cell">{{ item.owned_quantity }}</td>
<td class="table-action">
<router-link :to="`/inventory/${item.id}/edit`">
<b-icon-pencil-square></b-icon-pencil-square>
</router-link>
<a :href="`/inventory/${item.id}/delete`" @click.prevent="tryDeleteItem(item)">
<b-icon-trash></b-icon-trash>
</a>
</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="item in items" :key="item.id">
<div class="card">
<div class="card-body">
<h5 class="card-title mb-0">
<router-link :to="`/inventory/${item.id}`">
{{ item.name }}
</router-link>
</h5>
<div class="card-text text-black-50">
<span class="badge bg-secondary text-white">{{ item.availability_policy }}</span>
<span class="float-right">{{ item.owned_quantity }}</span>
</div>
<div class="btn-group">
<button class="btn btn-danger btn-sm" @click="tryDeleteItem(item)">
Delete
</button>
<router-link :to="`/inventory/${item.id}/edit`"
class="btn btn-primary btn-sm">Edit
</router-link>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="card">
<button class="btn" @click="fetchItems">Refresh</button>
<router-link :to="`/inventory/new?group=${id}`" 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 UserNameTag from "@/components/UserNameTag.vue";
export default {
name: 'GroupDetail',
components: {
BaseLayout,
UserNameTag,
...BIcons
},
props: {
id: {
type: [String, Number],
required: true
}
},
data() {
return {
group: null,
layout: "grid",
show_invite: false,
invitee: ""
}
},
computed: {
...mapState(['user']),
items() {
return this.groupInventoryItems(this.id)
},
...mapGetters(['groupInventoryItems'])
},
methods: {
...mapActions(['fetchGroup', 'removeGroupMember', 'inviteToGroup', 'fetchGroupInventoryItems',
'deleteInventoryItem']),
refresh() {
this.fetchGroup({id: this.id}).then((group) => {
this.group = group
})
},
fetchItems() {
this.fetchGroupInventoryItems(this.id)
},
tryInvite() {
this.inviteToGroup({groupId: this.id, groupHandle: this.group.handle, invitee: this.invitee})
.then((ok) => {
if (ok) {
this.show_invite = false
this.invitee = ""
}
}).catch(() => {
})
},
tryRemoveMember(member) {
this.removeGroupMember({groupId: this.id, identityId: member.id}).then(() => {
this.refresh()
}).catch(() => {
})
},
tryDeleteItem(item) {
this.deleteInventoryItem(item).then(() => {
this.fetchItems()
})
}
},
mounted() {
this.refresh()
this.fetchItems()
}
}
</script>
<style scoped>
</style>

View file

@ -0,0 +1,52 @@
<template>
<BaseLayout>
<main class="content">
<div class="container-fluid p-0">
<h1 class="h3 mb-3">New group</h1>
<div class="row">
<div class="col-12 col-xl-6">
<div class="card">
<div class="card-body">
<div class="mb-3">
<label class="form-label">Name</label>
<input type="text" class="form-control" v-model="name">
</div>
<button type="submit" class="btn btn-primary" style="width: 100%"
@click="tryCreateGroup">Create</button>
</div>
</div>
</div>
</div>
</div>
</main>
</BaseLayout>
</template>
<script>
import {mapActions} from "vuex";
import BaseLayout from "@/components/BaseLayout.vue";
export default {
name: 'GroupNew',
components: {
BaseLayout
},
data() {
return {
name: ""
}
},
methods: {
...mapActions(['createGroup']),
tryCreateGroup() {
this.createGroup({name: this.name}).then((group) => {
this.$router.push(`/groups/${group.id}`)
}).catch(() => {
})
}
}
}
</script>
<style scoped>
</style>

View file

@ -0,0 +1,122 @@
<template>
<BaseLayout>
<main class="content">
<div class="container-fluid p-0">
<h1 class="h3 mb-3">Groups</h1>
<div class="row">
<div class="col-12 col-xl-6">
<div class="card">
<div class="card-header">
<h5 class="card-title">My Groups</h5>
</div>
<table class="table table-striped">
<thead>
<tr>
<th>Name</th>
<th class="d-none d-md-table-cell" style="width:25%">Members</th>
<th style="width: 16em">
<a @click="fetchGroups" class="align-middle">
<b-icon-arrow-clockwise></b-icon-arrow-clockwise>
Refresh
</a>
<router-link to="/groups/new" class="align-middle">
<b-icon-plus></b-icon-plus>
New group
</router-link>
</th>
</tr>
</thead>
<tbody>
<tr v-for="group in groups" :key="group.id">
<td>
<router-link :to="`/groups/${group.id}`">{{ group.handle }}</router-link>
</td>
<td class="d-none d-md-table-cell">{{ group.members.length }}</td>
<td class="table-action"></td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="col-12 col-xl-6">
<div class="card">
<div class="card-header">
<h5 class="card-title">Open group invites</h5>
</div>
<table class="table table-striped">
<thead>
<tr>
<th>Group</th>
<th class="d-none d-md-table-cell" style="width:25%">Invited by</th>
<th style="width: 16em">
<a @click="fetchGroupInvites" class="align-middle">
<b-icon-arrow-clockwise></b-icon-arrow-clockwise>
Refresh
</a>
</th>
</tr>
</thead>
<tbody>
<tr v-for="invite in groupInvites" :key="invite.id">
<td>{{ invite.group }}</td>
<td class="d-none d-md-table-cell">{{ invite.inviter }}</td>
<td class="table-action">
<button class="btn btn-sm btn-success" @click="tryAcceptInvite(invite)">
<b-icon-check></b-icon-check>
Accept
</button> &nbsp;
<button class="btn btn-sm btn-danger" @click="tryDeclineInvite(invite)">
<b-icon-x></b-icon-x>
Decline
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</main>
</BaseLayout>
</template>
<script>
import {mapActions, mapState} from "vuex";
import * as BIcons from "bootstrap-icons-vue";
import BaseLayout from "@/components/BaseLayout.vue";
export default {
name: 'Groups',
components: {
BaseLayout,
...BIcons
},
computed: {
...mapState(['groups', 'groupInvites']),
},
methods: {
...mapActions(['fetchGroups', 'fetchGroupInvites', 'acceptGroupInvite', 'declineGroupInvite']),
tryAcceptInvite(invite) {
this.acceptGroupInvite(invite).then(() => {
this.fetchGroupInvites()
this.fetchGroups()
}).catch(() => {
})
},
tryDeclineInvite(invite) {
this.declineGroupInvite(invite).then(() => {
this.fetchGroupInvites()
}).catch(() => {
})
}
},
mounted() {
this.fetchGroups()
this.fetchGroupInvites()
}
}
</script>
<style scoped>
</style>

View file

@ -92,7 +92,7 @@ export default {
}
},
computed: {
...mapGetters(["inventory_items"]),
...mapGetters(["loaded_items"]),
...mapState(["availability_policies", "storage_locations"]),
item() {
return {
@ -104,14 +104,14 @@ export default {
image: "",
files: [],
storage_location: null,
...this.inventory_items.find(item => item.id === parseInt(this.id))
...this.loaded_items.find(item => item.id === parseInt(this.id))
}
}
},
methods: {
...mapActions(["fetchInventoryItems", "updateInventoryItem", "fetchInfo", "fetchStorageLocations"]),
changeFiles(files) {
this.inventory_items.find(item => item.id === parseInt(this.id)).files = files
this.loaded_items.find(item => item.id === parseInt(this.id)).files = files
},
},
async mounted() {

View file

@ -24,6 +24,15 @@
<label for="property" class="form-label">Property</label>
<property-field :value="item.properties"></property-field>
</div>
<div class="mb-3">
<label for="owner" class="form-label">Owner</label>
<select class="form-select" id="owner" name="owner" v-model="item.owner_group">
<option :value="null">Myself</option>
<option v-for="group in groups" :value="group.id" :key="group.id">
{{ group.handle }}
</option>
</select>
</div>
<div class="mb-3">
<label for="quantity" class="form-label">Quantity</label>
<input type="number" class="form-control" id="quantity" name="quantity"
@ -96,19 +105,24 @@ export default {
tags: [],
properties: [],
files: [],
storage_location: null
storage_location: null,
owner_group: null
}
}
},
methods: {
...mapActions(['createInventoryItem', 'fetchInfo', 'fetchStorageLocations'])
...mapActions(['createInventoryItem', 'fetchInfo', 'fetchStorageLocations', 'fetchGroups'])
},
computed: {
...mapState(["availability_policies", "storage_locations"]),
...mapState(["availability_policies", "storage_locations", "groups"]),
},
async mounted() {
await this.fetchInfo();
await this.fetchStorageLocations();
await this.fetchGroups();
if (this.$route.query.group) {
this.item.owner_group = parseInt(this.$route.query.group, 10)
}
}
}
</script>