feat(ui): Allow assigning other users (if you have the permission).
This commit is contained in:
parent
58621d3da4
commit
dd49b0eb9e
|
@ -22,9 +22,8 @@
|
|||
"@quasar/quasar-ui-qcalendar": "^4.0.0-beta.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@flaschengeist/api": "^1.0.0-alpha.6",
|
||||
"@flaschengeist/types": "^1.0.0-alpha.9",
|
||||
"@quasar/app": "^3.2.3",
|
||||
"@flaschengeist/api": "^1.0.0-alpha.7",
|
||||
"@flaschengeist/types": "^1.0.0-alpha.10",
|
||||
"quasar": "^2.3.3",
|
||||
"axios": "^0.24.0",
|
||||
"prettier": "^2.5.0",
|
||||
|
|
|
@ -13,18 +13,44 @@
|
|||
<div>
|
||||
<q-select
|
||||
:model-value="modelValue.services"
|
||||
:disable="!canAssignOther || modelValue.locked"
|
||||
:options="options"
|
||||
option-value="userid"
|
||||
filled
|
||||
:option-label="(opt) => userDisplay(opt)"
|
||||
multiple
|
||||
disable
|
||||
use-chips
|
||||
stack-label
|
||||
use-input
|
||||
label="Dienste"
|
||||
behavior="dialog"
|
||||
class="col-auto q-px-xs"
|
||||
style="font-size: 6px"
|
||||
counter
|
||||
:max-values="modelValue.required_services"
|
||||
@filter="filterUsers"
|
||||
@add="({ value }) => assign(value)"
|
||||
@remove="({ value }) => unassign(value)"
|
||||
>
|
||||
<template #selected-item="{ opt, toggleOption }">
|
||||
<service-user-chip :model-value="opt" removeable @remove="toggleOption" />
|
||||
</template>
|
||||
<template #option="{ opt, itemProps }">
|
||||
<q-item v-bind="itemProps">
|
||||
<q-item-section avatar>
|
||||
<user-avatar :model-value="opt.userid" />
|
||||
</q-item-section>
|
||||
<q-item-section>{{ userDisplay(opt.userid) }}</q-item-section>
|
||||
<q-item-section style="max-width: 10em" side>
|
||||
<q-input
|
||||
v-model.number="opt.value"
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.25"
|
||||
dense
|
||||
filled
|
||||
@click.stop=""
|
||||
/>
|
||||
</q-item-section>
|
||||
<q-item-section side>
|
||||
<q-toggle v-model="opt.is_backup" label="Backup" left-label />
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
</q-select>
|
||||
<div class="row col-12 justify-end">
|
||||
<q-btn
|
||||
|
@ -32,7 +58,7 @@
|
|||
flat
|
||||
color="primary"
|
||||
label="Eintragen"
|
||||
@click="assignJob()"
|
||||
@click="assign()"
|
||||
/>
|
||||
<q-btn v-if="isEnrolled && !modelValue.locked" flat color="secondary" label="Optionen">
|
||||
<q-menu auto-close>
|
||||
|
@ -43,8 +69,18 @@
|
|||
<q-item clickable @click="transfer">
|
||||
<q-item-section>Tauschen</q-item-section>
|
||||
</q-item>
|
||||
<q-item v-if="isBackup" clickable @click="backup(false)">
|
||||
<q-tooltip>Backup zu vollem Dienst machen</q-tooltip>
|
||||
<q-item-section>Dienst</q-item-section>
|
||||
<q-item-section side><q-icon name="mdi-eye" /></q-item-section>
|
||||
</q-item>
|
||||
<q-item v-else clickable @click="backup(true)">
|
||||
<q-tooltip>Nur als Backup eintragen</q-tooltip>
|
||||
<q-item-section>Backup</q-item-section>
|
||||
<q-item-section side><q-icon name="mdi-eye-off" /></q-item-section>
|
||||
</q-item>
|
||||
<q-separator />
|
||||
<q-item clickable @click="assignJob(false)">
|
||||
<q-item clickable @click="unassign()">
|
||||
<q-item-section class="text-negative">Austragen</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
|
@ -56,14 +92,19 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, onBeforeMount, computed, PropType } from 'vue';
|
||||
import { date, useQuasar } from 'quasar';
|
||||
import { asHour, useMainStore, useUserStore } from '@flaschengeist/api';
|
||||
import { defineComponent, onBeforeMount, computed, ref, PropType } from 'vue';
|
||||
import { asHour, hasPermission, useMainStore, useUserStore } from '@flaschengeist/api';
|
||||
import { useEventStore } from '../../../store';
|
||||
import { PERMISSIONS } from '../../../permissions';
|
||||
|
||||
import TransferInviteDialog from './TransferInviteDialog.vue';
|
||||
import ServiceUserChip from './ServiceUserChip.vue';
|
||||
import { UserAvatar } from '@flaschengeist/api/components';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'JobSlot',
|
||||
components: { ServiceUserChip, UserAvatar },
|
||||
props: {
|
||||
modelValue: {
|
||||
required: true,
|
||||
|
@ -81,31 +122,42 @@ export default defineComponent({
|
|||
const userStore = useUserStore();
|
||||
const quasar = useQuasar();
|
||||
|
||||
onBeforeMount(async () => userStore.getUsers());
|
||||
// Make sure users are loaded if we can assign them
|
||||
onBeforeMount(async () => await userStore.getUsers());
|
||||
|
||||
function userDisplay(service: FG.Service) {
|
||||
return userStore.findUser(service.userid)?.display_name || service.userid;
|
||||
/* Stuff used for general display */
|
||||
// Get displayname of user
|
||||
function userDisplay(id: string) {
|
||||
return userStore.findUser(id)?.display_name || id;
|
||||
}
|
||||
|
||||
// The name of the current job
|
||||
const typeName = computed(() =>
|
||||
typeof props.modelValue.type === 'object'
|
||||
? props.modelValue.type.name
|
||||
: store.jobTypes.find((j) => j.id === props.modelValue.type)?.name || 'Unbekannter Diensttyp'
|
||||
: store.jobTypes.find((j) => j.id === props.modelValue.type)?.name ||
|
||||
'Unbekannter Diensttyp'
|
||||
);
|
||||
|
||||
const isEnrolled = computed(
|
||||
() =>
|
||||
props.modelValue.services.findIndex(
|
||||
(service) => service.userid == mainStore.currentUser.userid
|
||||
) !== -1
|
||||
// The service of the current user if self assigned to the job
|
||||
const service = computed(() =>
|
||||
props.modelValue.services.find((service) => service.userid == mainStore.currentUser.userid)
|
||||
);
|
||||
|
||||
// Weather the current user is assigned to the job
|
||||
const isEnrolled = computed(() => service.value !== undefined);
|
||||
|
||||
// If the job has enough assigned services
|
||||
const isFull = computed(
|
||||
() =>
|
||||
props.modelValue.services.map((s) => s.value).reduce((p, c) => p + c, 0) >=
|
||||
props.modelValue.required_services
|
||||
);
|
||||
|
||||
// If current user is only backup service
|
||||
const isBackup = computed(() => service.value?.is_backup || false);
|
||||
|
||||
// If it is still possible to invite other users (= job is today or in the future)
|
||||
const canInvite = computed(
|
||||
() =>
|
||||
(props.modelValue.end || props.modelValue.start) >
|
||||
|
@ -115,14 +167,15 @@ export default defineComponent({
|
|||
)
|
||||
);
|
||||
|
||||
async function assignJob(assign = true) {
|
||||
const newService: FG.Service = {
|
||||
// Assign user to a job
|
||||
async function assign(service?: FG.Service) {
|
||||
service = service || {
|
||||
userid: mainStore.currentUser.userid,
|
||||
is_backup: false,
|
||||
value: assign ? 1 : -1,
|
||||
value: 1,
|
||||
};
|
||||
try {
|
||||
const job = await store.assignToJob(props.modelValue.id, newService);
|
||||
const job = await store.assignToJob(props.modelValue.id, service);
|
||||
emit('update:modelValue', job);
|
||||
} catch (error) {
|
||||
console.warn(error);
|
||||
|
@ -137,6 +190,7 @@ export default defineComponent({
|
|||
}
|
||||
}
|
||||
|
||||
// open invite dialog (or transfer)
|
||||
function invite(isInvite = true) {
|
||||
quasar.dialog({
|
||||
component: TransferInviteDialog,
|
||||
|
@ -147,15 +201,72 @@ export default defineComponent({
|
|||
});
|
||||
}
|
||||
|
||||
/* Stuff needed if we can assign other user */
|
||||
// Current user can assign other users
|
||||
const canAssignOther = computed(() => hasPermission(PERMISSIONS.ASSIGN_OTHER));
|
||||
|
||||
// options shown in the select
|
||||
const options = ref([] as FG.Service[]);
|
||||
|
||||
// users which are available (e.g. not already assigned)
|
||||
const freeUsers = computed(() =>
|
||||
userStore.users.filter((u) => props.modelValue.services.every((s) => s.userid !== u.userid))
|
||||
);
|
||||
|
||||
// used to filter options based on user input
|
||||
function filterUsers(
|
||||
input: string,
|
||||
doneFn: (
|
||||
callbackFn: () => void,
|
||||
afterFn?: (ref: { [index: string]: unknown }) => void
|
||||
) => void,
|
||||
abortFn: () => void
|
||||
) {
|
||||
if (freeUsers.value.length == 0) return abortFn();
|
||||
|
||||
// Filter the options
|
||||
doneFn(() => {
|
||||
// Skip filter options if input is too short
|
||||
if (!input || input.length < 2) {
|
||||
options.value = freeUsers.value.map<FG.Service>((u) => ({
|
||||
userid: u.userid,
|
||||
value: 1,
|
||||
is_backup: false,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
// Search matching string within all names
|
||||
options.value = freeUsers.value
|
||||
.filter((u) =>
|
||||
input
|
||||
.toLowerCase()
|
||||
.split(' ')
|
||||
.every(
|
||||
(needle) =>
|
||||
u.display_name.toLowerCase().indexOf(needle) > -1 ||
|
||||
u.firstname.toLowerCase().indexOf(needle) > -1 ||
|
||||
u.lastname.toLowerCase().indexOf(needle) > -1
|
||||
)
|
||||
)
|
||||
.map<FG.Service>((u) => ({ userid: u.userid, value: 1, is_backup: false }));
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
assignJob,
|
||||
assign,
|
||||
unassign: (s?: FG.Service) => assign(Object.assign({}, s || service.value, { value: -1 })),
|
||||
backup: (is_backup: boolean) => assign(Object.assign({}, service.value, { is_backup })),
|
||||
canAssignOther,
|
||||
canInvite,
|
||||
filterUsers,
|
||||
isBackup,
|
||||
isEnrolled,
|
||||
isFull,
|
||||
invite: () => invite(true),
|
||||
transfer: () => invite(false),
|
||||
typeName,
|
||||
userDisplay,
|
||||
options,
|
||||
asHour,
|
||||
};
|
||||
},
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
<template>
|
||||
<q-chip
|
||||
:removable="removeable"
|
||||
:color="modelValue.is_backup ? 'grey' : undefined"
|
||||
@remove="remove"
|
||||
>
|
||||
<q-tooltip>{{ displayName }} ({{ serviceValue }}x)</q-tooltip>
|
||||
<user-avatar :model-value="modelValue.userid">
|
||||
<slot v-if="modelValue.is_backup">
|
||||
<q-icon v-if="modelValue.is_backup" name="mdi-eye-off" />
|
||||
</slot>
|
||||
</user-avatar>
|
||||
<div class="ellipsis">{{ displayName }}</div>
|
||||
<q-badge v-if="modelValue.value !== 1" :label="serviceValue" style="margin-left: 0.25em" />
|
||||
<slot />
|
||||
</q-chip>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { useUserStore } from '@flaschengeist/api';
|
||||
import { PropType, computed, defineComponent, onBeforeMount, ref, watch } from 'vue';
|
||||
|
||||
import { UserAvatar } from '@flaschengeist/api/components';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ServiceUserChip',
|
||||
components: { UserAvatar },
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Object as PropType<FG.Service>,
|
||||
required: true,
|
||||
},
|
||||
removeable: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['remove'],
|
||||
setup(props, { emit }) {
|
||||
const userStore = useUserStore();
|
||||
|
||||
const user = ref<FG.User>();
|
||||
|
||||
onBeforeMount(async () => {
|
||||
user.value = await userStore.getUser(props.modelValue.userid);
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
async () => (user.value = await userStore.getUser(props.modelValue.userid))
|
||||
);
|
||||
|
||||
const displayName = computed(() => user.value?.display_name || '...');
|
||||
const serviceValue = computed(() =>
|
||||
props.modelValue.value.toFixed(Number.isInteger(props.modelValue.value) ? 0 : 1)
|
||||
);
|
||||
|
||||
return {
|
||||
displayName,
|
||||
remove: () => emit('remove', props.modelValue),
|
||||
serviceValue,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style></style>
|
Loading…
Reference in New Issue