Implemented transfer and invite to jobs and notifications
This commit is contained in:
parent
6c32aae7b4
commit
512e68f1ed
|
@ -13,11 +13,13 @@ declare namespace FG {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
interface Invite {
|
interface Invitation {
|
||||||
id: number;
|
id: number;
|
||||||
job_id: number;
|
job_id: number;
|
||||||
invitee_id: string;
|
invitee_id: string;
|
||||||
sender_id: string;
|
inviter_id: string;
|
||||||
|
transferee_id?: string;
|
||||||
|
transferee: User;
|
||||||
}
|
}
|
||||||
interface Job {
|
interface Job {
|
||||||
id: number;
|
id: number;
|
||||||
|
@ -25,9 +27,9 @@ declare namespace FG {
|
||||||
end?: Date;
|
end?: Date;
|
||||||
type: JobType | number;
|
type: JobType | number;
|
||||||
comment?: string;
|
comment?: string;
|
||||||
|
locked: boolean;
|
||||||
services: Array<Service>;
|
services: Array<Service>;
|
||||||
required_services: number;
|
required_services: number;
|
||||||
locked: boolean;
|
|
||||||
}
|
}
|
||||||
interface JobType {
|
interface JobType {
|
||||||
id: number;
|
id: number;
|
||||||
|
|
|
@ -32,7 +32,7 @@
|
||||||
flat
|
flat
|
||||||
color="primary"
|
color="primary"
|
||||||
label="Eintragen"
|
label="Eintragen"
|
||||||
@click="enrollForJob"
|
@click="assignJob()"
|
||||||
/>
|
/>
|
||||||
<q-btn v-if="isEnrolled && !modelValue.locked" flat color="secondary" label="Optionen">
|
<q-btn v-if="isEnrolled && !modelValue.locked" flat color="secondary" label="Optionen">
|
||||||
<q-menu auto-close>
|
<q-menu auto-close>
|
||||||
|
@ -44,7 +44,7 @@
|
||||||
<q-item-section>Tauschen</q-item-section>
|
<q-item-section>Tauschen</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
<q-separator />
|
<q-separator />
|
||||||
<q-item clickable @click="signOutFromJob">
|
<q-item clickable @click="assignJob(false)">
|
||||||
<q-item-section class="text-negative">Austragen</q-item-section>
|
<q-item-section class="text-negative">Austragen</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
</q-list>
|
</q-list>
|
||||||
|
@ -115,46 +115,21 @@ export default defineComponent({
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
async function enrollForJob() {
|
async function assignJob(assign = true) {
|
||||||
const newService: FG.Service = {
|
const newService: FG.Service = {
|
||||||
userid: mainStore.currentUser.userid,
|
userid: mainStore.currentUser.userid,
|
||||||
is_backup: false,
|
is_backup: false,
|
||||||
value: 1,
|
value: assign ? 1 : -1,
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
await store.assignToJob(props.modelValue.id, newService);
|
const job = await store.assignToJob(props.modelValue.id, newService);
|
||||||
const job = Object.assign({}, props.modelValue)
|
|
||||||
job.services.push(newService)
|
|
||||||
emit('update:modelValue', job);
|
emit('update:modelValue', job);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(error);
|
console.warn(error);
|
||||||
quasar.notify({
|
quasar.notify({
|
||||||
group: false,
|
group: false,
|
||||||
type: 'negative',
|
type: 'negative',
|
||||||
message: 'Fehler beim Eintragen als Dienst',
|
message: 'Fehler beim Ein- oder Austragen als Dienst',
|
||||||
timeout: 10000,
|
|
||||||
progress: true,
|
|
||||||
actions: [{ icon: 'mdi-close', color: 'white' }],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async function signOutFromJob() {
|
|
||||||
const newService: FG.Service = {
|
|
||||||
userid: mainStore.currentUser.userid,
|
|
||||||
is_backup: false,
|
|
||||||
value: -1,
|
|
||||||
};
|
|
||||||
try {
|
|
||||||
await store.assignToJob(props.modelValue.id, newService);
|
|
||||||
const job = Object.assign({}, props.modelValue)
|
|
||||||
job.services.push(newService)
|
|
||||||
emit('update:modelValue', job);
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(error);
|
|
||||||
quasar.notify({
|
|
||||||
group: false,
|
|
||||||
type: 'negative',
|
|
||||||
message: 'Fehler beim Austragen als Dienst',
|
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
progress: true,
|
progress: true,
|
||||||
actions: [{ icon: 'mdi-close', color: 'white' }],
|
actions: [{ icon: 'mdi-close', color: 'white' }],
|
||||||
|
@ -166,19 +141,18 @@ export default defineComponent({
|
||||||
quasar.dialog({
|
quasar.dialog({
|
||||||
component: TransferInviteDialog,
|
component: TransferInviteDialog,
|
||||||
componentProps: {
|
componentProps: {
|
||||||
invite: isInvite,
|
isInvite: isInvite,
|
||||||
job: props.modelValue,
|
job: props.modelValue,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
assignJob,
|
||||||
canInvite,
|
canInvite,
|
||||||
enrollForJob,
|
|
||||||
isEnrolled,
|
isEnrolled,
|
||||||
isFull,
|
isFull,
|
||||||
invite: () => invite(true),
|
invite: () => invite(true),
|
||||||
signOutFromJob,
|
|
||||||
transfer: () => invite(false),
|
transfer: () => invite(false),
|
||||||
typeName,
|
typeName,
|
||||||
userDisplay,
|
userDisplay,
|
||||||
|
|
|
@ -65,10 +65,9 @@ export default defineComponent({
|
||||||
);
|
);
|
||||||
|
|
||||||
function invite() {
|
function invite() {
|
||||||
store
|
void store
|
||||||
.sendInvite(props.job, invitees.value, !props.isInvite)
|
.invite(props.job, invitees.value, !props.isInvite ? mainStore.currentUser : undefined)
|
||||||
.then(() => onDialogOK())
|
.then(() => onDialogOK());
|
||||||
.catch(() => onDialogCancel());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
declare namespace FG {
|
import { FG_Plugin } from "@flaschengeist/types";
|
||||||
|
|
||||||
export interface RecurrenceRule {
|
export interface RecurrenceRule {
|
||||||
frequency: string;
|
frequency: string;
|
||||||
interval: number;
|
interval: number;
|
||||||
|
@ -6,4 +7,19 @@ declare namespace FG {
|
||||||
until?: Date;
|
until?: Date;
|
||||||
weekdays?: Array<number>;
|
weekdays?: Array<number>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface InvitationData {
|
||||||
|
invitation: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface InvitationResponseData {
|
||||||
|
event: number,
|
||||||
|
job: number,
|
||||||
|
invitee: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EventNotification extends FG_Plugin.Notification {
|
||||||
|
data: {
|
||||||
|
type: number
|
||||||
|
} & (InvitationData | InvitationResponseData);
|
||||||
}
|
}
|
||||||
|
|
34
src/index.ts
34
src/index.ts
|
@ -1,6 +1,39 @@
|
||||||
import { innerRoutes, privateRoutes } from './routes';
|
import { innerRoutes, privateRoutes } from './routes';
|
||||||
import { FG_Plugin } from '@flaschengeist/types';
|
import { FG_Plugin } from '@flaschengeist/types';
|
||||||
import { defineAsyncComponent } from 'vue';
|
import { defineAsyncComponent } from 'vue';
|
||||||
|
import { EventNotification, InvitationData, InvitationResponseData } from './events';
|
||||||
|
import { useEventStore } from './store';
|
||||||
|
|
||||||
|
const EventTypes = {
|
||||||
|
_mask_: 0xf0,
|
||||||
|
invitation: 0x00,
|
||||||
|
invite: 0x01,
|
||||||
|
transfer: 0x02,
|
||||||
|
invitation_response: 0x10,
|
||||||
|
invitation_accepted: 0x10,
|
||||||
|
invitation_rejected: 0x11,
|
||||||
|
};
|
||||||
|
|
||||||
|
function transpile(msg: FG_Plugin.Notification) {
|
||||||
|
const message = msg as EventNotification;
|
||||||
|
message.icon = 'mdi-calendar';
|
||||||
|
if ((message.data.type & EventTypes._mask_) === EventTypes.invitation) {
|
||||||
|
message.accept = () => {
|
||||||
|
const store = useEventStore();
|
||||||
|
return store.acceptInvitation((<InvitationData>message.data).invitation);
|
||||||
|
};
|
||||||
|
|
||||||
|
message.reject = () => {
|
||||||
|
const store = useEventStore();
|
||||||
|
return store.rejectInvitation((<InvitationData>message.data).invitation);
|
||||||
|
};
|
||||||
|
|
||||||
|
message.link = { name: 'events-requests' };
|
||||||
|
} else if ((message.data.type & EventTypes._mask_) === EventTypes.invitation_response) {
|
||||||
|
message.link = {name: 'events-single-view', params: {id: (<InvitationResponseData>message.data).event}}
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
const plugin: FG_Plugin.Plugin = {
|
const plugin: FG_Plugin.Plugin = {
|
||||||
id: 'dev.flaschengeist.events',
|
id: 'dev.flaschengeist.events',
|
||||||
|
@ -9,6 +42,7 @@ const plugin: FG_Plugin.Plugin = {
|
||||||
internalRoutes: privateRoutes,
|
internalRoutes: privateRoutes,
|
||||||
requiredModules: [['events']],
|
requiredModules: [['events']],
|
||||||
version: '0.0.1',
|
version: '0.0.1',
|
||||||
|
notification: transpile,
|
||||||
widgets: [
|
widgets: [
|
||||||
{
|
{
|
||||||
priority: 0,
|
priority: 0,
|
||||||
|
|
|
@ -1,25 +1,37 @@
|
||||||
<template>
|
<template>
|
||||||
<q-page padding class="fit row justify-center content-start items-start q-gutter-sm">
|
<q-page padding class="fit row justify-center content-start items-start q-gutter-sm">
|
||||||
<EditEvent v-model="event" />
|
<EditEvent :model-value="event" />
|
||||||
</q-page>
|
</q-page>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onBeforeMount, defineComponent, ref } from 'vue';
|
import { defineComponent, onMounted, ref, watch, PropType } from 'vue';
|
||||||
import EditEvent from '../components/management/EditEvent.vue';
|
import EditEvent from '../components/management/EditEvent.vue';
|
||||||
import { useEventStore } from '../store';
|
import { useEventStore } from '../store';
|
||||||
import { useRoute } from 'vue-router';
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: { EditEvent },
|
components: { EditEvent },
|
||||||
setup() {
|
props: {
|
||||||
const route = useRoute();
|
id: {
|
||||||
|
type: String as PropType<number | string>,
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
const store = useEventStore();
|
const store = useEventStore();
|
||||||
const event = ref<FG.Event | undefined>(undefined);
|
const event = ref<FG.Event>();
|
||||||
onBeforeMount(async () => {
|
|
||||||
if ('id' in route.params && typeof route.params.id === 'string')
|
function loadEvent(id?: string | number) {
|
||||||
event.value = await store.getEvent(parseInt(route.params.id));
|
if (id != event.value?.id)
|
||||||
});
|
if (id === undefined) event.value = undefined;
|
||||||
|
else
|
||||||
|
void store
|
||||||
|
.getEvent(typeof id === 'number' ? id : parseInt(id))
|
||||||
|
.then((e) => (event.value = e));
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(props, (v) => loadEvent(v.id));
|
||||||
|
onMounted(() => loadEvent(props.id));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
event,
|
event,
|
||||||
|
|
|
@ -7,8 +7,8 @@ export const innerRoutes: FG_Plugin.MenuRoute[] = [
|
||||||
icon: 'mdi-briefcase',
|
icon: 'mdi-briefcase',
|
||||||
permissions: ['user'],
|
permissions: ['user'],
|
||||||
route: {
|
route: {
|
||||||
path: 'schedule',
|
path: 'events',
|
||||||
name: 'schedule',
|
name: 'events',
|
||||||
redirect: { name: 'schedule-overview' },
|
redirect: { name: 'schedule-overview' },
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
|
@ -39,8 +39,8 @@ export const innerRoutes: FG_Plugin.MenuRoute[] = [
|
||||||
icon: 'mdi-account-switch',
|
icon: 'mdi-account-switch',
|
||||||
shortcut: false,
|
shortcut: false,
|
||||||
route: {
|
route: {
|
||||||
path: 'schedule-requests',
|
path: 'events-requests',
|
||||||
name: 'schedule-requests',
|
name: 'events-requests',
|
||||||
component: () => import('../pages/EventRequests.vue'),
|
component: () => import('../pages/EventRequests.vue'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -50,13 +50,14 @@ export const innerRoutes: FG_Plugin.MenuRoute[] = [
|
||||||
|
|
||||||
export const privateRoutes: FG_Plugin.NamedRouteRecordRaw[] = [
|
export const privateRoutes: FG_Plugin.NamedRouteRecordRaw[] = [
|
||||||
{
|
{
|
||||||
name: 'new-event',
|
name: 'events-single-view',
|
||||||
path: 'new-event',
|
path: 'events/:id',
|
||||||
redirect: { name: 'schedule-management' },
|
component: () => import('../pages/EventPage.vue'),
|
||||||
|
props: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'events-edit',
|
name: 'events-edit',
|
||||||
path: 'schedule/:id/edit',
|
path: 'events/:id/edit',
|
||||||
component: () => import('../pages/EventPage.vue'),
|
component: () => import('../pages/EventPage.vue'),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
@ -2,11 +2,18 @@ import { api, isAxiosError } from '@flaschengeist/api';
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { EditableEvent } from './models';
|
import { EditableEvent } from './models';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert JSON decoded Job to real job (fix Date object)
|
||||||
|
*/
|
||||||
function fixJob(job: FG.Job) {
|
function fixJob(job: FG.Job) {
|
||||||
job.start = new Date(job.start);
|
job.start = new Date(job.start);
|
||||||
if (job.end) job.end = new Date(job.end);
|
if (job.end) job.end = new Date(job.end);
|
||||||
|
return job;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert JSON decoded Event to real Event object (fix Date object)
|
||||||
|
*/
|
||||||
function fixEvent(event: FG.Event) {
|
function fixEvent(event: FG.Event) {
|
||||||
event.start = new Date(event.start);
|
event.start = new Date(event.start);
|
||||||
if (event.end) event.end = new Date(event.end);
|
if (event.end) event.end = new Date(event.end);
|
||||||
|
@ -77,18 +84,6 @@ export const useEventStore = defineStore({
|
||||||
.then(({ data }) => this.eventTypes.push(data));
|
.then(({ data }) => this.eventTypes.push(data));
|
||||||
},
|
},
|
||||||
|
|
||||||
async removeEvent(id: number) {
|
|
||||||
try {
|
|
||||||
await api.delete(`/events/${id}`);
|
|
||||||
const idx = this.templates.findIndex((v) => v.id === id);
|
|
||||||
if (idx !== -1) this.templates.splice(idx, 1);
|
|
||||||
} catch (e) {
|
|
||||||
if (isAxiosError(e, 404)) return false;
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
|
|
||||||
removeEventType(id: number) {
|
removeEventType(id: number) {
|
||||||
return api
|
return api
|
||||||
.delete(`/events/event-types/${id}`)
|
.delete(`/events/event-types/${id}`)
|
||||||
|
@ -135,8 +130,16 @@ export const useEventStore = defineStore({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async assignToJob(jobId: number, service: FG.Service) {
|
async removeEvent(id: number) {
|
||||||
return api.post<FG.Job>(`/events/jobs/${jobId}/assign`, service);
|
try {
|
||||||
|
await api.delete(`/events/${id}`);
|
||||||
|
const idx = this.templates.findIndex((v) => v.id === id);
|
||||||
|
if (idx !== -1) this.templates.splice(idx, 1);
|
||||||
|
} catch (e) {
|
||||||
|
if (isAxiosError(e, 404)) return false;
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
},
|
},
|
||||||
|
|
||||||
async addEvent(event: EditableEvent) {
|
async addEvent(event: EditableEvent) {
|
||||||
|
@ -157,11 +160,33 @@ export const useEventStore = defineStore({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async sendInvite(job: FG.Job, invitees: FG.User[], isInvite = true) {
|
async assignToJob(jobId: number, service: FG.Service) {
|
||||||
return api.post<FG.Event>('/events/transfer', {
|
return api
|
||||||
|
.post<FG.Job>(`/events/jobs/${jobId}/assign`, service)
|
||||||
|
.then(({ data }) => fixJob(data));
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send invite to job or transfer to other user
|
||||||
|
* @param job Job to invite to
|
||||||
|
* @param invitees Users to invite
|
||||||
|
* @param isTransfer set to True to transfer service instead of invite
|
||||||
|
*/
|
||||||
|
async invite(job: FG.Job, invitees: FG.User[], transferee: FG.User | undefined = undefined) {
|
||||||
|
return api.post<FG.Invitation[]>('/events/invitations', {
|
||||||
job: job.id,
|
job: job.id,
|
||||||
receiver: invitees,
|
invitees: invitees.map((v) => v.userid),
|
||||||
is_invite: isInvite,
|
transferee: transferee,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async rejectInvitation(invite: FG.Invitation | number) {
|
||||||
|
return api.delete(`/events/invitations/${typeof invite === 'number' ? invite : invite.id}`);
|
||||||
|
},
|
||||||
|
|
||||||
|
async acceptInvitation(invite: FG.Invitation | number) {
|
||||||
|
return api.put(`/events/invitations/${typeof invite === 'number' ? invite : invite.id}`, {
|
||||||
|
accept: true,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in New Issue