[schedule][events] Improved plugin

* Allow creating recurring events
* Fixed AgendaView
This commit is contained in:
Ferdinand Thiessen 2021-03-21 00:58:31 +01:00
parent 17460a8543
commit 77bb463e5e
11 changed files with 433 additions and 244 deletions

View File

@ -38,11 +38,11 @@ declare namespace FG {
id: number; id: number;
start: Date; start: Date;
end?: Date; end?: Date;
name?: string;
description?: string; description?: string;
type: EventType | number; type: EventType | number;
is_template: boolean;
jobs: Array<Job>; jobs: Array<Job>;
recurrence_rule?: RecurrenceRule;
template_id?: number;
} }
interface EventType { interface EventType {
id: number; id: number;
@ -67,12 +67,6 @@ declare namespace FG {
id: number; id: number;
name: string; name: string;
} }
interface RecurrenceRule {
frequency: string;
until?: Date;
count?: number;
interval: number;
}
interface Service { interface Service {
userid: string; userid: string;
value: number; value: number;

View File

@ -1,11 +1,29 @@
<template> <template>
<q-page padding> <q-page padding>
<q-card> <q-card>
<q-form @submit="save" @reset="reset"> <q-form @submit="save()" @reset="reset">
<q-card-section class="fit row justify-start content-center items-center"> <q-card-section class="fit row justify-start content-center items-center">
<q-card-section class="fit"> <div class="text-h6 col-xs-12 col-sm-6 q-pa-sm">Veranstaltung erstellen</div>
<div class="text-h6">Veranstaltung erstellen</div> <q-select
</q-card-section> :model-value="template"
filled
label="Vorlage"
class="col-xs-12 col-sm-6 q-pa-sm"
:options="templates"
option-label="name"
map-options
clearable
:disable="templates.length == 0"
@update:modelValue="fromTemplate"
@clear="reset()"
/>
<q-input
v-model="event.name"
class="col-xs-12 col-sm-6 q-pa-sm"
label="Name"
type="text"
filled
/>
<q-select <q-select
v-model="event.type" v-model="event.type"
filled filled
@ -15,7 +33,8 @@
class="col-xs-12 col-sm-6 q-pa-sm" class="col-xs-12 col-sm-6 q-pa-sm"
:options="eventtypes" :options="eventtypes"
option-label="name" option-label="name"
option-value="name" option-value="id"
emit-value
map-options map-options
clearable clearable
:rules="[notEmpty]" :rules="[notEmpty]"
@ -24,22 +43,37 @@
v-model="event.start" v-model="event.start"
class="col-xs-12 col-sm-6 q-pa-sm" class="col-xs-12 col-sm-6 q-pa-sm"
label="Veranstaltungsbeginn" label="Veranstaltungsbeginn"
:rules="[noValidDate, notEmpty]" :rules="[notEmpty]"
/>
<IsoDateInput
v-model="event.end"
class="col-xs-12 col-sm-6 q-pa-sm"
label="Veranstaltungsende"
/> />
</q-card-section>
<q-card-section class="fit justify-start content-center items-center">
<q-input <q-input
v-model="event.description" v-model="event.description"
class="col-xs-12 col-sm-6 q-pa-sm" class="col-12 q-pa-sm"
label="Beschreibung" label="Beschreibung"
type="textarea" type="textarea"
filled filled
/> />
</q-card-section> </q-card-section>
<q-card-section v-if="event.template_id === undefined">
<q-btn-toggle
v-model="recurrent"
spread
no-caps
:options="[
{ label: 'Einmalig', value: false },
{ label: 'Wiederkehrend', value: true },
]"
/>
<RecurrenceRule v-if="!!recurrent" v-model="recurrenceRule" />
</q-card-section>
<q-separator />
<q-card-section> <q-card-section>
<q-btn color="primary" label="Schicht hinzufügen" @click="addJob()" /> <q-btn color="primary" label="Schicht hinzufügen" @click="addJob()" />
</q-card-section> </q-card-section>
<q-card-section v-for="(job, index) in event.jobs" :key="index"> <q-card-section v-for="(job, index) in event.jobs" :key="index">
<q-card class="q-my-auto"> <q-card class="q-my-auto">
<job <job
@ -51,7 +85,8 @@
</q-card-section> </q-card-section>
<q-card-actions align="right"> <q-card-actions align="right">
<q-btn label="Reset" type="reset" /> <q-btn label="Reset" type="reset" />
<q-btn color="primary" type="submit" label="Speichern" /> <q-btn color="secondary" label="Neue Vorlage" @click="save(true)" />
<q-btn color="primary" type="submit" label="Erstellen" />
</q-card-actions> </q-card-actions>
</q-form> </q-form>
</q-card> </q-card>
@ -62,50 +97,97 @@
import { defineComponent, ref, onBeforeMount, computed } from 'vue'; import { defineComponent, ref, onBeforeMount, computed } from 'vue';
import IsoDateInput from 'src/components/utils/IsoDateInput.vue'; import IsoDateInput from 'src/components/utils/IsoDateInput.vue';
import Job from './Job.vue'; import Job from './Job.vue';
import { date } from 'quasar'; import RecurrenceRule from './RecurrenceRule.vue';
import { date, ModifyDateOptions } from 'quasar';
import { useScheduleStore } from '../../store'; import { useScheduleStore } from '../../store';
import { notEmpty } from 'src/utils/validators';
export default defineComponent({ export default defineComponent({
name: 'CreateEvent', name: 'CreateEvent',
components: { IsoDateInput, Job }, components: { IsoDateInput, Job, RecurrenceRule },
setup() { setup() {
const store = useScheduleStore(); const store = useScheduleStore();
const eventtypes = computed(() => store.eventTypes);
const jobDeleteDisabled = computed(() => event.value.jobs.length < 2);
const newJob = ref<FG.Job>({ const emptyJob = {
id: NaN, id: NaN,
start: new Date(), start: new Date(),
end: date.addToDate(new Date(), { hours: 1 }), end: date.addToDate(new Date(), { hours: 1 }),
services: [], services: [],
required_services: 2, required_services: 2,
type: store.jobTypes[0], type: store.jobTypes[0],
}); };
const event = ref<FG.Event>({ const emptyEvent = {
id: NaN, id: NaN,
start: new Date(), start: new Date(),
jobs: [Object.assign({}, newJob.value)], jobs: [Object.assign({}, emptyJob)],
type: store.eventTypes[0], type: store.eventTypes[0],
}); is_template: false,
};
const templates = computed(() => store.templates);
const template = ref<FG.Event | undefined>(undefined);
const event = ref<FG.Event>(Object.assign({}, emptyEvent));
const eventtypes = computed(() => store.eventTypes);
const jobDeleteDisabled = computed(() => event.value.jobs.length < 2);
const recurrent = ref(false);
const recurrenceRule = ref<FG.RecurrenceRule>({ frequency: 'daily', interval: 1 });
onBeforeMount(() => { onBeforeMount(() => {
void store.getEventTypes(); void store.getEventTypes();
void store.getJobTypes(); void store.getJobTypes();
void store.getTemplates();
}); });
function addJob() { function addJob() {
event.value.jobs.push(Object.assign({}, newJob.value)); event.value.jobs.push(Object.assign({}, emptyJob));
} }
function removeJob(index: number) { function removeJob(index: number) {
event.value.jobs.splice(index, 1); event.value.jobs.splice(index, 1);
} }
async function save() { function fromTemplate(tpl: FG.Event) {
console.log('Event:', event); template.value = tpl;
event.value = Object.assign({}, tpl);
}
async function save(template = false) {
console.log(template);
event.value.is_template = template;
try { try {
await store.addEvent(event.value); await store.addEvent(event.value);
if (recurrent.value && !event.value.is_template) {
let count = 0;
const options: ModifyDateOptions = {};
switch (recurrenceRule.value.frequency) {
case 'daily':
options['days'] = 1 * recurrenceRule.value.interval;
break;
case 'weekly':
options['days'] = 7 * recurrenceRule.value.interval;
break;
case 'monthly':
options['months'] = 1 * recurrenceRule.value.interval;
break;
}
while (true) {
event.value.start = date.addToDate(event.value.start, options);
if (event.value.end) event.value.end = date.addToDate(event.value.end, options);
event.value.jobs.forEach((job) => {
job.start = date.addToDate(job.start, options);
if (job.end) job.end = date.addToDate(job.end, options);
});
count++;
if (
count <= 120 &&
(!recurrenceRule.value.count || count <= recurrenceRule.value.count) &&
(!recurrenceRule.value.until || event.value.start < recurrenceRule.value.until)
)
await store.addEvent(event.value);
else break;
}
}
reset(); reset();
} catch (error) { } catch (error) {
console.error(error); console.error(error);
@ -113,34 +195,24 @@ export default defineComponent({
} }
function reset() { function reset() {
event.value.id = NaN; event.value = Object.assign({}, emptyEvent);
event.value.start = new Date(); template.value = undefined;
event.value.description = '';
event.value.type = { id: -1, name: '' };
event.value.jobs = [Object.assign({}, newJob.value)];
}
function notEmpty(val: string) {
return !!val || 'Feld darf nicht leer sein!';
}
function noValidDate(val: string) {
return !!date.isValid(val) || 'Datum/Zeit muss gesetzt sein!';
} }
function isAfterDate(val: Date) {
return !!val;
// return event.value.jobsstart.getTime() > val.getTime() || 'Ende muss hinter dem Start liegen';
}
return { return {
jobDeleteDisabled, jobDeleteDisabled,
addJob, addJob,
eventtypes, eventtypes,
templates,
removeJob, removeJob,
notEmpty, notEmpty,
noValidDate,
save, save,
reset, reset,
recurrent,
fromTemplate,
template,
recurrenceRule,
event, event,
isAfterDate,
}; };
}, },
}); });

View File

@ -6,17 +6,15 @@
class="col-xs-12 col-sm-6 q-pa-sm" class="col-xs-12 col-sm-6 q-pa-sm"
label="Beginn" label="Beginn"
type="datetime" type="datetime"
:rules="[notEmpty, noValidDate]" :rules="[notEmpty]"
/> />
<IsoDateInput <IsoDateInput
v-model="job.end" v-model="job.end"
class="col-xs-12 col-sm-6 q-pa-sm" class="col-xs-12 col-sm-6 q-pa-sm"
label="Ende" label="Ende"
type="datetime" type="datetime"
:rules="[notEmpty, noValidDate, isAfterDate]" :rules="[notEmpty, isAfterDate]"
/> />
</q-card-section>
<q-card-section class="row fit justify-start content-center items-center">
<q-select <q-select
v-model="job.type" v-model="job.type"
filled filled
@ -39,11 +37,9 @@
type="number" type="number"
:rules="[notEmpty]" :rules="[notEmpty]"
/> />
</q-card-section>
<q-card-section class="fit row justify-start content-center items-center">
<q-input <q-input
v-model="job.comment" v-model="job.comment"
class="col-xs-12 col-sm-6 q-pa-sm" class="col-12 q-pa-sm"
label="Beschreibung" label="Beschreibung"
type="textarea" type="textarea"
filled filled
@ -55,10 +51,10 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import IsoDateInput from 'src/components/utils/IsoDateInput.vue';
import { defineComponent, computed, onBeforeMount, PropType } from 'vue'; import { defineComponent, computed, onBeforeMount, PropType } from 'vue';
import IsoDateInput from 'src/components/utils/IsoDateInput.vue';
import { notEmpty } from 'src/utils/validators';
import { useScheduleStore } from '../../store'; import { useScheduleStore } from '../../store';
import { date } from 'quasar';
export default defineComponent({ export default defineComponent({
name: 'Job', name: 'Job',
@ -88,7 +84,6 @@ export default defineComponent({
} }
}, },
set(obj, prop, value) { set(obj, prop, value) {
console.log('...', obj, prop, value);
if (typeof prop === 'string') { if (typeof prop === 'string') {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
emit('update:modelValue', Object.assign({}, props.modelValue, { [prop]: value })); emit('update:modelValue', Object.assign({}, props.modelValue, { [prop]: value }));
@ -101,13 +96,6 @@ export default defineComponent({
emit('remove-job'); emit('remove-job');
} }
function notEmpty(val: string) {
return !!val || 'Feld darf nicht leer sein!';
}
function noValidDate(val: string) {
return !!date.isValid(val) || 'Datum/Zeit muss gesetzt sein!';
}
function isAfterDate(val: string) { function isAfterDate(val: string) {
return props.modelValue.start < new Date(val) || 'Ende muss hinter dem Start liegen'; return props.modelValue.start < new Date(val) || 'Ende muss hinter dem Start liegen';
} }
@ -117,7 +105,6 @@ export default defineComponent({
jobtypes, jobtypes,
removeJob, removeJob,
notEmpty, notEmpty,
noValidDate,
isAfterDate, isAfterDate,
}; };
}, },

View File

@ -0,0 +1,96 @@
<template>
<q-card class="fit row justify-start content-center items-center">
<q-input
v-model="rule.interval"
filled
class="col-xs-12 col-sm-6 q-pa-sm"
label="Interval"
type="number"
:rules="[notEmpty]"
/>
<q-select
v-model="rule.frequency"
filled
label="Wiederholung"
input-debounce="200"
class="col-xs-12 col-sm-6 q-pa-sm"
:options="freqTypes"
emit-value
map-options
/>
<q-input
v-model="rule.count"
filled
class="col-xs-12 col-sm-6 q-pa-sm"
label="Anzahl Wiederholungen"
type="number"
/>
<IsoDateInput
v-model="rule.until"
class="col-xs-12 col-sm-6 q-pa-sm"
label="Wiederholen bis"
type="date"
/>
</q-card>
</template>
<script lang="ts">
import IsoDateInput from 'src/components/utils/IsoDateInput.vue';
import { defineComponent, PropType } from 'vue';
import { notEmpty } from 'src/utils/validators';
export default defineComponent({
name: 'RecurrenceRule',
components: { IsoDateInput },
props: {
modelValue: {
required: true,
type: Object as PropType<FG.RecurrenceRule>,
},
},
emits: {
'update:modelValue': (rule: FG.RecurrenceRule) => !!rule,
},
setup(props, { emit }) {
const freqTypes = [
{ label: 'Täglich', value: 'daily' },
{ label: 'Wöchentlich', value: 'weekly' },
{ label: 'Monatlich', value: 'monthly' },
{ label: 'Jährlich', value: 'yearly' },
];
const rule = new Proxy(props.modelValue, {
get(target, prop) {
if (typeof prop === 'string') {
return ((props.modelValue as unknown) as Record<string, unknown>)[prop];
}
},
set(target, prop, value) {
if (typeof prop === 'string') {
const obj = Object.assign({}, props.modelValue);
if (prop == 'frequency' && typeof value === 'string') obj.frequency = value;
else if (prop == 'interval') {
obj.interval = typeof value === 'string' ? parseInt(value) : <number>value;
} else if (prop == 'count') {
obj.until = undefined;
obj.count = typeof value === 'string' ? parseInt(value) : <number>value;
} else if (prop == 'until' && (value instanceof Date || value === undefined)) {
obj.count = undefined;
obj.until = <Date | undefined>value;
} else return false;
emit('update:modelValue', obj);
}
return true;
},
});
return {
rule,
notEmpty,
freqTypes,
};
},
});
</script>
<style></style>

View File

@ -55,8 +55,8 @@
> >
<template #day="{ scope: { timestamp } }" style="min-height: 200px"> <template #day="{ scope: { timestamp } }" style="min-height: 200px">
<template v-if="!events[timestamp.weekday]" style="min-height: 200px"> </template> <template v-if="!events[timestamp.weekday]" style="min-height: 200px"> </template>
<template v-for="agenda in events[timestamp.weekday]" :key="agenda.id"> <template v-for="(agenda, index) in events[timestamp.weekday]" :key="agenda.id">
<eventslot :event="agenda" /> <eventslot v-model="events[timestamp.weekday][index]" />
</template> </template>
</template> </template>
</q-calendar-agenda> </q-calendar-agenda>
@ -104,6 +104,7 @@ export default defineComponent({
async function loadAgendas() { async function loadAgendas() {
const selected = new Date(selectedDate.value); const selected = new Date(selectedDate.value);
console.log(selected);
const start = calendarRealView.value === 'day' ? selected : startOfWeek(selected); const start = calendarRealView.value === 'day' ? selected : startOfWeek(selected);
const end = date.addToDate(start, { days: calendarDays.value }); const end = date.addToDate(start, { days: calendarDays.value });
@ -120,12 +121,18 @@ export default defineComponent({
} }
function calendarNext() { function calendarNext() {
calendar.value?.next(); selectedDate.value = date.formatDate(
date.addToDate(selectedDate.value, { days: calendarDays.value }),
'YYYY-MM-DD'
);
void loadAgendas(); void loadAgendas();
} }
function calendarPrev() { function calendarPrev() {
calendar.value?.prev(); selectedDate.value = date.formatDate(
date.subtractFromDate(selectedDate.value, { days: calendarDays.value }),
'YYYY-MM-DD'
);
void loadAgendas(); void loadAgendas();
} }

View File

@ -7,165 +7,39 @@
<div class="col text-weight-bolder ellipsis"> <div class="col text-weight-bolder ellipsis">
{{ event.type.name }} {{ event.type.name }}
</div> </div>
<div v-if="event.description" class="col text-weight-medium" style="font-size: 10px"> <div v-if="event.description" class="col text-weight-medium" style="font-size: 10px">
Info Info
{{ event.description }} {{ event.description }}
</div> </div>
</header> </header>
<div v-for="(job, index) in event.jobs" :key="index"> <div v-for="(job, index) in event.jobs" :key="index">
<q-separator style="justify-start content-center" /> <q-separator style="justify-start content-center" />
<div class="text-weight-medium q-px-xs"> <JobSlot v-model="event.jobs[index]" :event-id="event.id" />
{{ asHour(job.start) }} <template v-if="job.end">- {{ asHour(job.end) }}</template>
</div>
<div class="q-px-xs">
{{ job.type.name }}
</div>
<div class="col-auto q-px-xs" style="font-size: 10px">
{{ job.comment }}
</div>
<div>
<q-select
:key="refreshKey"
v-model="job.services"
filled
:option-label="(opt) => userDisplay(opt)"
multiple
disable
use-chips
stack-label
label="Dienste"
class="col-auto q-px-xs"
style="font-size: 6px"
counter
:max-values="job.required_services"
>
</q-select>
<div class="row col-12 justify-end">
<q-btn v-if="false" />
<q-btn
v-if="!isUserEnrolled(job) && !jobFull(job)"
:key="refreshKey"
flat
color="primary"
label="Eintragen"
@click="enrollForJob(job)"
/>
<q-btn
v-if="isUserEnrolled(job)"
:key="refreshKey"
flat
color="negative"
label="Austragen"
@click="signOutFromJob(job)"
/>
</div>
</div>
</div> </div>
</q-card> </q-card>
</template> </template>
<script lang="ts"> <script lang="ts">
/* eslint-disable @typescript-eslint/no-unsafe-member-access */ import { defineComponent, computed, PropType } from 'vue';
import { defineComponent, ref, onBeforeMount, PropType } from 'vue'; import JobSlot from './JobSlot.vue';
import { useRouter } from 'vue-router';
import { Notify } from 'quasar';
import { asHour } from 'src/utils/datetime';
import { useUserStore } from 'src/plugins/user/store';
import { useMainStore } from 'src/store';
import { useScheduleStore } from 'src/plugins/schedule/store';
export default defineComponent({ export default defineComponent({
name: 'Eventslot', name: 'Eventslot',
components: {}, components: { JobSlot },
props: { props: {
event: { modelValue: {
required: true, required: true,
type: Object as PropType<FG.Event>, type: Object as PropType<FG.Event>,
}, },
}, },
emits: { 'update:modelValue': (val: FG.Event) => !!val },
setup(props) { setup(props, { emit }) {
const store = useScheduleStore(); const event = computed({
const mainStore = useMainStore(); get: () => props.modelValue,
const userStore = useUserStore(); set: (v) => emit('update:modelValue', v),
const router = useRouter();
const availableUsers = null;
const refreshKey = ref<number>(0);
onBeforeMount(async () => userStore.getUsers());
function refresh() {
router.go(0);
refreshKey.value += 1;
}
function isUserEnrolled(job: FG.Job) {
return (
job.services.findIndex((service) => service.userid == mainStore.currentUser.userid) >= 0
);
}
function jobFull(job: FG.Job) {
return job.services.length >= job.required_services;
}
function userDisplay(userid: string) {
return userStore.users.find((user) => (user.userid = userid))?.display_name;
}
async function enrollForJob(job: FG.Job) {
const newService: FG.Service = {
userid: mainStore.currentUser.userid,
value: 1,
};
try {
await store.updateEvent(props.event.id, job.id, { user: newService });
} catch (error) {
console.warn(error);
Notify.create({
group: false,
type: 'negative',
message: 'Fehler beim Eintragen als Dienst',
timeout: 10000,
progress: true,
actions: [{ icon: 'mdi-close', color: 'white' }],
}); });
}
refresh();
}
async function signOutFromJob(job: FG.Job) {
const newService: FG.Service = {
userid: mainStore.currentUser.userid,
value: -1,
};
try {
await store.updateEvent(props.event.id, job.id, { user: newService });
} catch (error) {
console.warn(error);
Notify.create({
group: false,
type: 'negative',
message: 'Fehler beim Austragen als Dienst',
timeout: 10000,
progress: true,
actions: [{ icon: 'mdi-close', color: 'white' }],
});
}
refresh();
}
return { return {
refreshKey, event,
availableUsers,
enrollForJob,
isUserEnrolled,
signOutFromJob,
jobFull,
userDisplay,
refresh,
asHour,
}; };
}, },
}); });

View File

@ -0,0 +1,140 @@
<template>
<q-card>
<div class="text-weight-medium q-px-xs">
{{ asHour(modelValue.start) }}
<template v-if="modelValue.end">- {{ asHour(modelValue.end) }}</template>
</div>
<div class="q-px-xs">
{{ modelValue.type.name }}
</div>
<div class="col-auto q-px-xs" style="font-size: 10px">
{{ modelValue.comment }}
</div>
<div>
<q-select
:model-value="modelValue.services"
filled
:option-label="(opt) => userDisplay(opt)"
multiple
disable
use-chips
stack-label
label="Dienste"
class="col-auto q-px-xs"
style="font-size: 6px"
counter
:max-values="modelValue.required_services"
>
</q-select>
<div class="row col-12 justify-end">
<q-btn v-if="canEnroll" flat color="primary" label="Eintragen" @click="enrollForJob" />
<q-btn v-if="isEnrolled" flat color="negative" label="Austragen" @click="signOutFromJob" />
</div>
</div>
</q-card>
</template>
<script lang="ts">
import { defineComponent, onBeforeMount, computed, PropType } from 'vue';
import { Notify } from 'quasar';
import { asHour } from 'src/utils/datetime';
import { useUserStore } from 'src/plugins/user/store';
import { useMainStore } from 'src/store';
import { useScheduleStore } from 'src/plugins/schedule/store';
export default defineComponent({
name: 'JobSlot',
props: {
modelValue: {
required: true,
type: Object as PropType<FG.Job>,
},
eventId: {
required: true,
type: Number,
},
},
emits: { 'update:modelValue': (v: FG.Job) => !!v },
setup(props, { emit }) {
const store = useScheduleStore();
const mainStore = useMainStore();
const userStore = useUserStore();
const availableUsers = null;
onBeforeMount(async () => userStore.getUsers());
function userDisplay(service: FG.Service) {
return userStore.findUser(service.userid)?.display_name || service.userid;
}
const isEnrolled = computed(
() =>
props.modelValue.services.findIndex(
(service) => service.userid == mainStore.currentUser.userid
) !== -1
);
const canEnroll = computed(() => {
const is = isEnrolled.value;
let sum = 0;
props.modelValue.services.forEach((s) => (sum += s.value));
return sum < props.modelValue.required_services && !is;
});
async function enrollForJob() {
const newService: FG.Service = {
userid: mainStore.currentUser.userid,
value: 1,
};
try {
const job = await store.updateJob(props.eventId, props.modelValue.id, { user: newService });
emit('update:modelValue', job);
} catch (error) {
console.warn(error);
Notify.create({
group: false,
type: 'negative',
message: 'Fehler beim Eintragen als Dienst',
timeout: 10000,
progress: true,
actions: [{ icon: 'mdi-close', color: 'white' }],
});
}
}
async function signOutFromJob() {
const newService: FG.Service = {
userid: mainStore.currentUser.userid,
value: -1,
};
try {
const job = await store.updateJob(props.eventId, props.modelValue.id, {
user: newService,
});
emit('update:modelValue', job);
} catch (error) {
console.warn(error);
Notify.create({
group: false,
type: 'negative',
message: 'Fehler beim Austragen als Dienst',
timeout: 10000,
progress: true,
actions: [{ icon: 'mdi-close', color: 'white' }],
});
}
}
return {
availableUsers,
enrollForJob,
isEnrolled,
signOutFromJob,
canEnroll,
userDisplay,
asHour,
};
},
});
</script>
<style scoped></style>

9
src/plugins/schedule/events.d.ts vendored Normal file
View File

@ -0,0 +1,9 @@
declare namespace FG {
export interface RecurrenceRule {
frequency: string;
interval: number;
count?: number;
until?: Date;
weekdays?: Array<number>;
}
}

View File

@ -6,7 +6,7 @@ const plugin: FG_Plugin.Plugin = {
name: 'Schedule', name: 'Schedule',
mainRoutes, mainRoutes,
requiredModules: ['User'], requiredModules: ['User'],
requiredBackendModules: ['schedule'], requiredBackendModules: ['events'],
version: '0.0.1', version: '0.0.1',
widgets: [ widgets: [
{ {

View File

@ -6,14 +6,16 @@ interface UserService {
user: FG.Service; user: FG.Service;
} }
function fixJob(job: FG.Job) {
job.start = new Date(job.start);
if (job.end) job.end = new Date(job.end);
}
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);
event.jobs.forEach((job) => { event.jobs.forEach((job) => fixJob(job));
job.start = new Date(job.start);
if (job.end) job.end = new Date(job.end);
});
} }
export const useScheduleStore = defineStore({ export const useScheduleStore = defineStore({
@ -22,6 +24,7 @@ export const useScheduleStore = defineStore({
state: () => ({ state: () => ({
jobTypes: [] as FG.JobType[], jobTypes: [] as FG.JobType[],
eventTypes: [] as FG.EventType[], eventTypes: [] as FG.EventType[],
templates: [] as FG.Event[],
}), }),
getters: {}, getters: {},
@ -53,13 +56,15 @@ export const useScheduleStore = defineStore({
// TODO handle rename // TODO handle rename
}, },
async getEventTypes() { async getEventTypes(force = false) {
if (force || this.eventTypes.length == 0)
try { try {
const { data } = await api.get<FG.EventType[]>('/schedule/event-types'); const { data } = await api.get<FG.EventType[]>('/schedule/event-types');
this.eventTypes = data; this.eventTypes = data;
} catch (error) { } catch (error) {
throw error; throw error;
} }
return this.eventTypes;
}, },
/** Add new EventType /** Add new EventType
@ -100,6 +105,15 @@ export const useScheduleStore = defineStore({
} }
}, },
async getTemplates(force = false) {
if (force || this.templates.length == 0) {
const { data } = await api.get<FG.Event[]>('/schedule/templates');
data.forEach((element) => fixEvent(element));
this.templates = data;
}
return this.templates;
},
async getEvents(filter: { from?: Date; to?: Date } | undefined = undefined) { async getEvents(filter: { from?: Date; to?: Date } | undefined = undefined) {
try { try {
const { data } = await api.get<FG.Event[]>('/schedule/events', { params: filter }); const { data } = await api.get<FG.Event[]>('/schedule/events', { params: filter });
@ -109,13 +123,13 @@ export const useScheduleStore = defineStore({
throw error; throw error;
} }
}, },
async updateEvent(eventId: number, jobId: number, service: FG.Service | UserService) { async updateJob(eventId: number, jobId: number, service: FG.Service | UserService) {
try { try {
const { data } = await api.put<FG.Event>( const { data } = await api.put<FG.Job>(
`/schedule/events/${eventId}/jobs/${jobId}`, `/schedule/events/${eventId}/jobs/${jobId}`,
service service
); );
fixEvent(data); fixJob(data);
return data; return data;
} catch (error) { } catch (error) {
throw error; throw error;
@ -124,8 +138,8 @@ export const useScheduleStore = defineStore({
async addEvent(event: FG.Event) { async addEvent(event: FG.Event) {
const { data } = await api.post<FG.Event>('/schedule/events', event); const { data } = await api.post<FG.Event>('/schedule/events', event);
if (data.is_template) this.templates.push(data);
return data; return data;
//TODO: Handle add event}
}, },
}, },
}); });

View File

@ -10,20 +10,19 @@ export const useUserStore = defineStore({
roles: [] as FG.Role[], roles: [] as FG.Role[],
users: [] as FG.User[], users: [] as FG.User[],
permissions: [] as FG.Permission[], permissions: [] as FG.Permission[],
_dirty_users: 0, _dirty_users: true,
_dirty_roles: 0, _dirty_roles: true,
}), }),
getters: { getters: {},
isDirty() {
return new Date().getTime() - this._dirty_users > 60000;
},
},
actions: { actions: {
async getUser(userid: string, force = true) { findUser(userid: string) {
return this.users.find((user) => user.userid === userid);
},
async getUser(userid: string, force = false) {
const idx = this.users.findIndex((user) => user.userid === userid); const idx = this.users.findIndex((user) => user.userid === userid);
if (force || idx == -1 || this.isDirty) { if (force || this._dirty_users || idx === -1) {
try { try {
const { data } = await api.get<FG.User>(`/users/${userid}`); const { data } = await api.get<FG.User>(`/users/${userid}`);
if (data.birthday) data.birthday = new Date(data.birthday); if (data.birthday) data.birthday = new Date(data.birthday);
@ -39,17 +38,16 @@ export const useUserStore = defineStore({
} }
}, },
async getUsers(force = true) { async getUsers(force = false) {
if (force || this.isDirty) { if (force || this._dirty_users) {
const { data } = await api.get<FG.User[]>('/users'); const { data } = await api.get<FG.User[]>('/users');
data.forEach((user) => { data.forEach((user) => {
if (user.birthday) user.birthday = new Date(user.birthday); if (user.birthday) user.birthday = new Date(user.birthday);
}); });
this.users = data; this.users = data;
this._dirty_users = new Date().getTime(); this._dirty_users = false;
} else {
return this.users;
} }
return this.users;
}, },
async updateUser(user: FG.User) { async updateUser(user: FG.User) {
@ -57,7 +55,7 @@ export const useUserStore = defineStore({
const mainStore = useMainStore(); const mainStore = useMainStore();
if (user.userid === mainStore.user?.userid) mainStore.user = user; if (user.userid === mainStore.user?.userid) mainStore.user = user;
this._dirty_users = 0; this._dirty_users = true;
}, },
async createUser(user: FG.User) { async createUser(user: FG.User) {
@ -85,10 +83,10 @@ export const useUserStore = defineStore({
}, },
async getRoles(force = false) { async getRoles(force = false) {
if (force || new Date().getTime() - this._dirty_roles > 60000) { if (force || this._dirty_roles) {
const { data } = await api.get<FG.Role[]>('/roles'); const { data } = await api.get<FG.Role[]>('/roles');
this.roles = data; this.roles = data;
this._dirty_roles = new Date().getTime(); this._dirty_roles = false;
} }
return this.roles; return this.roles;
}, },
@ -97,20 +95,18 @@ export const useUserStore = defineStore({
await api.put(`/roles/${role.id}`, role); await api.put(`/roles/${role.id}`, role);
const idx = this.roles.findIndex((r) => r.id === role.id); const idx = this.roles.findIndex((r) => r.id === role.id);
if (idx != -1) this.roles[idx] = role; if (idx != -1) this.roles[idx] = role;
this._dirty_roles = 0; this._dirty_roles = true;
}, },
async newRole(role: FG.Role) { async newRole(role: FG.Role) {
const { data } = await api.post<FG.Role>('/roles', role); const { data } = await api.post<FG.Role>('/roles', role);
this.roles.push(data); this.roles.push(data);
this._dirty_roles = 0;
return data; return data;
}, },
async deleteRole(role: FG.Role | number) { async deleteRole(role: FG.Role | number) {
await api.delete(`/roles/${typeof role === 'number' ? role : role.id}`); await api.delete(`/roles/${typeof role === 'number' ? role : role.id}`);
this.roles = this.roles.filter((r) => r.id !== (typeof role == 'number' ? role : role.id)); this.roles = this.roles.filter((r) => r.id !== (typeof role == 'number' ? role : role.id));
this._dirty_roles = 0;
}, },
}, },
}); });