[schedule][events] Improved plugin
* Allow creating recurring events * Fixed AgendaView
This commit is contained in:
parent
17460a8543
commit
77bb463e5e
|
@ -38,11 +38,11 @@ declare namespace FG {
|
|||
id: number;
|
||||
start: Date;
|
||||
end?: Date;
|
||||
name?: string;
|
||||
description?: string;
|
||||
type: EventType | number;
|
||||
is_template: boolean;
|
||||
jobs: Array<Job>;
|
||||
recurrence_rule?: RecurrenceRule;
|
||||
template_id?: number;
|
||||
}
|
||||
interface EventType {
|
||||
id: number;
|
||||
|
@ -67,12 +67,6 @@ declare namespace FG {
|
|||
id: number;
|
||||
name: string;
|
||||
}
|
||||
interface RecurrenceRule {
|
||||
frequency: string;
|
||||
until?: Date;
|
||||
count?: number;
|
||||
interval: number;
|
||||
}
|
||||
interface Service {
|
||||
userid: string;
|
||||
value: number;
|
||||
|
|
|
@ -1,11 +1,29 @@
|
|||
<template>
|
||||
<q-page padding>
|
||||
<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">
|
||||
<div class="text-h6">Veranstaltung erstellen</div>
|
||||
</q-card-section>
|
||||
<div class="text-h6 col-xs-12 col-sm-6 q-pa-sm">Veranstaltung erstellen</div>
|
||||
<q-select
|
||||
: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
|
||||
v-model="event.type"
|
||||
filled
|
||||
|
@ -15,7 +33,8 @@
|
|||
class="col-xs-12 col-sm-6 q-pa-sm"
|
||||
:options="eventtypes"
|
||||
option-label="name"
|
||||
option-value="name"
|
||||
option-value="id"
|
||||
emit-value
|
||||
map-options
|
||||
clearable
|
||||
:rules="[notEmpty]"
|
||||
|
@ -24,22 +43,37 @@
|
|||
v-model="event.start"
|
||||
class="col-xs-12 col-sm-6 q-pa-sm"
|
||||
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
|
||||
v-model="event.description"
|
||||
class="col-xs-12 col-sm-6 q-pa-sm"
|
||||
class="col-12 q-pa-sm"
|
||||
label="Beschreibung"
|
||||
type="textarea"
|
||||
filled
|
||||
/>
|
||||
</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-btn color="primary" label="Schicht hinzufügen" @click="addJob()" />
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section v-for="(job, index) in event.jobs" :key="index">
|
||||
<q-card class="q-my-auto">
|
||||
<job
|
||||
|
@ -51,7 +85,8 @@
|
|||
</q-card-section>
|
||||
<q-card-actions align="right">
|
||||
<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-form>
|
||||
</q-card>
|
||||
|
@ -62,50 +97,97 @@
|
|||
import { defineComponent, ref, onBeforeMount, computed } from 'vue';
|
||||
import IsoDateInput from 'src/components/utils/IsoDateInput.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 { notEmpty } from 'src/utils/validators';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'CreateEvent',
|
||||
components: { IsoDateInput, Job },
|
||||
components: { IsoDateInput, Job, RecurrenceRule },
|
||||
setup() {
|
||||
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,
|
||||
start: new Date(),
|
||||
end: date.addToDate(new Date(), { hours: 1 }),
|
||||
services: [],
|
||||
required_services: 2,
|
||||
type: store.jobTypes[0],
|
||||
});
|
||||
};
|
||||
|
||||
const event = ref<FG.Event>({
|
||||
const emptyEvent = {
|
||||
id: NaN,
|
||||
start: new Date(),
|
||||
jobs: [Object.assign({}, newJob.value)],
|
||||
jobs: [Object.assign({}, emptyJob)],
|
||||
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(() => {
|
||||
void store.getEventTypes();
|
||||
void store.getJobTypes();
|
||||
void store.getTemplates();
|
||||
});
|
||||
|
||||
function addJob() {
|
||||
event.value.jobs.push(Object.assign({}, newJob.value));
|
||||
event.value.jobs.push(Object.assign({}, emptyJob));
|
||||
}
|
||||
|
||||
function removeJob(index: number) {
|
||||
event.value.jobs.splice(index, 1);
|
||||
}
|
||||
|
||||
async function save() {
|
||||
console.log('Event:', event);
|
||||
function fromTemplate(tpl: FG.Event) {
|
||||
template.value = tpl;
|
||||
event.value = Object.assign({}, tpl);
|
||||
}
|
||||
|
||||
async function save(template = false) {
|
||||
console.log(template);
|
||||
event.value.is_template = template;
|
||||
try {
|
||||
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();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
@ -113,34 +195,24 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
function reset() {
|
||||
event.value.id = NaN;
|
||||
event.value.start = new Date();
|
||||
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!';
|
||||
event.value = Object.assign({}, emptyEvent);
|
||||
template.value = undefined;
|
||||
}
|
||||
|
||||
function isAfterDate(val: Date) {
|
||||
return !!val;
|
||||
// return event.value.jobsstart.getTime() > val.getTime() || 'Ende muss hinter dem Start liegen';
|
||||
}
|
||||
return {
|
||||
jobDeleteDisabled,
|
||||
addJob,
|
||||
eventtypes,
|
||||
templates,
|
||||
removeJob,
|
||||
notEmpty,
|
||||
noValidDate,
|
||||
save,
|
||||
reset,
|
||||
recurrent,
|
||||
fromTemplate,
|
||||
template,
|
||||
recurrenceRule,
|
||||
event,
|
||||
isAfterDate,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
@ -6,17 +6,15 @@
|
|||
class="col-xs-12 col-sm-6 q-pa-sm"
|
||||
label="Beginn"
|
||||
type="datetime"
|
||||
:rules="[notEmpty, noValidDate]"
|
||||
:rules="[notEmpty]"
|
||||
/>
|
||||
<IsoDateInput
|
||||
v-model="job.end"
|
||||
class="col-xs-12 col-sm-6 q-pa-sm"
|
||||
label="Ende"
|
||||
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
|
||||
v-model="job.type"
|
||||
filled
|
||||
|
@ -39,11 +37,9 @@
|
|||
type="number"
|
||||
:rules="[notEmpty]"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-section class="fit row justify-start content-center items-center">
|
||||
<q-input
|
||||
v-model="job.comment"
|
||||
class="col-xs-12 col-sm-6 q-pa-sm"
|
||||
class="col-12 q-pa-sm"
|
||||
label="Beschreibung"
|
||||
type="textarea"
|
||||
filled
|
||||
|
@ -55,10 +51,10 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import IsoDateInput from 'src/components/utils/IsoDateInput.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 { date } from 'quasar';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Job',
|
||||
|
@ -88,7 +84,6 @@ export default defineComponent({
|
|||
}
|
||||
},
|
||||
set(obj, prop, value) {
|
||||
console.log('...', obj, prop, value);
|
||||
if (typeof prop === 'string') {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
emit('update:modelValue', Object.assign({}, props.modelValue, { [prop]: value }));
|
||||
|
@ -101,13 +96,6 @@ export default defineComponent({
|
|||
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) {
|
||||
return props.modelValue.start < new Date(val) || 'Ende muss hinter dem Start liegen';
|
||||
}
|
||||
|
@ -117,7 +105,6 @@ export default defineComponent({
|
|||
jobtypes,
|
||||
removeJob,
|
||||
notEmpty,
|
||||
noValidDate,
|
||||
isAfterDate,
|
||||
};
|
||||
},
|
||||
|
|
|
@ -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>
|
|
@ -55,8 +55,8 @@
|
|||
>
|
||||
<template #day="{ scope: { timestamp } }" style="min-height: 200px">
|
||||
<template v-if="!events[timestamp.weekday]" style="min-height: 200px"> </template>
|
||||
<template v-for="agenda in events[timestamp.weekday]" :key="agenda.id">
|
||||
<eventslot :event="agenda" />
|
||||
<template v-for="(agenda, index) in events[timestamp.weekday]" :key="agenda.id">
|
||||
<eventslot v-model="events[timestamp.weekday][index]" />
|
||||
</template>
|
||||
</template>
|
||||
</q-calendar-agenda>
|
||||
|
@ -104,6 +104,7 @@ export default defineComponent({
|
|||
|
||||
async function loadAgendas() {
|
||||
const selected = new Date(selectedDate.value);
|
||||
console.log(selected);
|
||||
const start = calendarRealView.value === 'day' ? selected : startOfWeek(selected);
|
||||
const end = date.addToDate(start, { days: calendarDays.value });
|
||||
|
||||
|
@ -120,12 +121,18 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
function calendarNext() {
|
||||
calendar.value?.next();
|
||||
selectedDate.value = date.formatDate(
|
||||
date.addToDate(selectedDate.value, { days: calendarDays.value }),
|
||||
'YYYY-MM-DD'
|
||||
);
|
||||
void loadAgendas();
|
||||
}
|
||||
|
||||
function calendarPrev() {
|
||||
calendar.value?.prev();
|
||||
selectedDate.value = date.formatDate(
|
||||
date.subtractFromDate(selectedDate.value, { days: calendarDays.value }),
|
||||
'YYYY-MM-DD'
|
||||
);
|
||||
void loadAgendas();
|
||||
}
|
||||
|
||||
|
|
|
@ -7,165 +7,39 @@
|
|||
<div class="col text-weight-bolder ellipsis">
|
||||
{{ event.type.name }}
|
||||
</div>
|
||||
|
||||
<div v-if="event.description" class="col text-weight-medium" style="font-size: 10px">
|
||||
Info
|
||||
{{ event.description }}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div v-for="(job, index) in event.jobs" :key="index">
|
||||
<q-separator style="justify-start content-center" />
|
||||
<div class="text-weight-medium q-px-xs">
|
||||
{{ 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>
|
||||
<JobSlot v-model="event.jobs[index]" :event-id="event.id" />
|
||||
</div>
|
||||
</q-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
import { defineComponent, ref, onBeforeMount, PropType } from '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';
|
||||
import { defineComponent, computed, PropType } from 'vue';
|
||||
import JobSlot from './JobSlot.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Eventslot',
|
||||
components: {},
|
||||
components: { JobSlot },
|
||||
props: {
|
||||
event: {
|
||||
modelValue: {
|
||||
required: true,
|
||||
type: Object as PropType<FG.Event>,
|
||||
},
|
||||
},
|
||||
|
||||
setup(props) {
|
||||
const store = useScheduleStore();
|
||||
const mainStore = useMainStore();
|
||||
const userStore = useUserStore();
|
||||
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' }],
|
||||
emits: { 'update:modelValue': (val: FG.Event) => !!val },
|
||||
setup(props, { emit }) {
|
||||
const event = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (v) => emit('update:modelValue', v),
|
||||
});
|
||||
}
|
||||
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 {
|
||||
refreshKey,
|
||||
availableUsers,
|
||||
enrollForJob,
|
||||
isUserEnrolled,
|
||||
signOutFromJob,
|
||||
jobFull,
|
||||
userDisplay,
|
||||
refresh,
|
||||
asHour,
|
||||
event,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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>
|
|
@ -0,0 +1,9 @@
|
|||
declare namespace FG {
|
||||
export interface RecurrenceRule {
|
||||
frequency: string;
|
||||
interval: number;
|
||||
count?: number;
|
||||
until?: Date;
|
||||
weekdays?: Array<number>;
|
||||
}
|
||||
}
|
|
@ -6,7 +6,7 @@ const plugin: FG_Plugin.Plugin = {
|
|||
name: 'Schedule',
|
||||
mainRoutes,
|
||||
requiredModules: ['User'],
|
||||
requiredBackendModules: ['schedule'],
|
||||
requiredBackendModules: ['events'],
|
||||
version: '0.0.1',
|
||||
widgets: [
|
||||
{
|
||||
|
|
|
@ -6,14 +6,16 @@ interface UserService {
|
|||
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) {
|
||||
event.start = new Date(event.start);
|
||||
if (event.end) event.end = new Date(event.end);
|
||||
|
||||
event.jobs.forEach((job) => {
|
||||
job.start = new Date(job.start);
|
||||
if (job.end) job.end = new Date(job.end);
|
||||
});
|
||||
event.jobs.forEach((job) => fixJob(job));
|
||||
}
|
||||
|
||||
export const useScheduleStore = defineStore({
|
||||
|
@ -22,6 +24,7 @@ export const useScheduleStore = defineStore({
|
|||
state: () => ({
|
||||
jobTypes: [] as FG.JobType[],
|
||||
eventTypes: [] as FG.EventType[],
|
||||
templates: [] as FG.Event[],
|
||||
}),
|
||||
|
||||
getters: {},
|
||||
|
@ -53,13 +56,15 @@ export const useScheduleStore = defineStore({
|
|||
// TODO handle rename
|
||||
},
|
||||
|
||||
async getEventTypes() {
|
||||
async getEventTypes(force = false) {
|
||||
if (force || this.eventTypes.length == 0)
|
||||
try {
|
||||
const { data } = await api.get<FG.EventType[]>('/schedule/event-types');
|
||||
this.eventTypes = data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
return this.eventTypes;
|
||||
},
|
||||
|
||||
/** 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) {
|
||||
try {
|
||||
const { data } = await api.get<FG.Event[]>('/schedule/events', { params: filter });
|
||||
|
@ -109,13 +123,13 @@ export const useScheduleStore = defineStore({
|
|||
throw error;
|
||||
}
|
||||
},
|
||||
async updateEvent(eventId: number, jobId: number, service: FG.Service | UserService) {
|
||||
async updateJob(eventId: number, jobId: number, service: FG.Service | UserService) {
|
||||
try {
|
||||
const { data } = await api.put<FG.Event>(
|
||||
const { data } = await api.put<FG.Job>(
|
||||
`/schedule/events/${eventId}/jobs/${jobId}`,
|
||||
service
|
||||
);
|
||||
fixEvent(data);
|
||||
fixJob(data);
|
||||
return data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
|
@ -124,8 +138,8 @@ export const useScheduleStore = defineStore({
|
|||
|
||||
async addEvent(event: FG.Event) {
|
||||
const { data } = await api.post<FG.Event>('/schedule/events', event);
|
||||
if (data.is_template) this.templates.push(data);
|
||||
return data;
|
||||
//TODO: Handle add event}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -10,20 +10,19 @@ export const useUserStore = defineStore({
|
|||
roles: [] as FG.Role[],
|
||||
users: [] as FG.User[],
|
||||
permissions: [] as FG.Permission[],
|
||||
_dirty_users: 0,
|
||||
_dirty_roles: 0,
|
||||
_dirty_users: true,
|
||||
_dirty_roles: true,
|
||||
}),
|
||||
|
||||
getters: {
|
||||
isDirty() {
|
||||
return new Date().getTime() - this._dirty_users > 60000;
|
||||
},
|
||||
},
|
||||
getters: {},
|
||||
|
||||
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);
|
||||
if (force || idx == -1 || this.isDirty) {
|
||||
if (force || this._dirty_users || idx === -1) {
|
||||
try {
|
||||
const { data } = await api.get<FG.User>(`/users/${userid}`);
|
||||
if (data.birthday) data.birthday = new Date(data.birthday);
|
||||
|
@ -39,17 +38,16 @@ export const useUserStore = defineStore({
|
|||
}
|
||||
},
|
||||
|
||||
async getUsers(force = true) {
|
||||
if (force || this.isDirty) {
|
||||
async getUsers(force = false) {
|
||||
if (force || this._dirty_users) {
|
||||
const { data } = await api.get<FG.User[]>('/users');
|
||||
data.forEach((user) => {
|
||||
if (user.birthday) user.birthday = new Date(user.birthday);
|
||||
});
|
||||
this.users = data;
|
||||
this._dirty_users = new Date().getTime();
|
||||
} else {
|
||||
return this.users;
|
||||
this._dirty_users = false;
|
||||
}
|
||||
return this.users;
|
||||
},
|
||||
|
||||
async updateUser(user: FG.User) {
|
||||
|
@ -57,7 +55,7 @@ export const useUserStore = defineStore({
|
|||
|
||||
const mainStore = useMainStore();
|
||||
if (user.userid === mainStore.user?.userid) mainStore.user = user;
|
||||
this._dirty_users = 0;
|
||||
this._dirty_users = true;
|
||||
},
|
||||
|
||||
async createUser(user: FG.User) {
|
||||
|
@ -85,10 +83,10 @@ export const useUserStore = defineStore({
|
|||
},
|
||||
|
||||
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');
|
||||
this.roles = data;
|
||||
this._dirty_roles = new Date().getTime();
|
||||
this._dirty_roles = false;
|
||||
}
|
||||
return this.roles;
|
||||
},
|
||||
|
@ -97,20 +95,18 @@ export const useUserStore = defineStore({
|
|||
await api.put(`/roles/${role.id}`, role);
|
||||
const idx = this.roles.findIndex((r) => r.id === role.id);
|
||||
if (idx != -1) this.roles[idx] = role;
|
||||
this._dirty_roles = 0;
|
||||
this._dirty_roles = true;
|
||||
},
|
||||
|
||||
async newRole(role: FG.Role) {
|
||||
const { data } = await api.post<FG.Role>('/roles', role);
|
||||
this.roles.push(data);
|
||||
this._dirty_roles = 0;
|
||||
return data;
|
||||
},
|
||||
|
||||
async deleteRole(role: FG.Role | number) {
|
||||
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._dirty_roles = 0;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue