Compare commits

...

16 Commits

Author SHA1 Message Date
Ferdinand Thiessen dd33e77514 [events] AgendaView fixed styling on low res devices and fixed icons 2021-04-04 21:39:13 +02:00
Ferdinand Thiessen 494a555985 [events] Show if event needs service (spinner) 2021-04-04 21:39:13 +02:00
Ferdinand Thiessen 5780c73608 [events] Create empty models from factory function (fresh objects) 2021-04-04 21:39:13 +02:00
Ferdinand Thiessen 7d1993e3fa [events] Validate jobs before adding new 2021-04-04 21:39:13 +02:00
Ferdinand Thiessen 6a35f3c669 [events] Fixed minor issues 2021-04-04 21:39:13 +02:00
Ferdinand Thiessen 3530132e72 [events] Fixed AgendaView mobile layout in portrait mode 2021-04-04 21:39:13 +02:00
Ferdinand Thiessen 48aa6c724a [events] Can send invitations and transfers 2021-04-04 21:39:13 +02:00
Ferdinand Thiessen 43dcd0579e [events] First work on job transfer and job invite 2021-04-04 21:39:11 +02:00
Ferdinand Thiessen 1f71abcd4b [events] Fixed AgendaView breakpoint 2021-04-04 21:38:46 +02:00
Ferdinand Thiessen 8abcd44340 [events] Renamed to match backend name 2021-04-04 21:38:46 +02:00
Ferdinand Thiessen 4bc602fed2 [events] First version of ListView 2021-04-04 21:38:46 +02:00
Ferdinand Thiessen 285803a226 [events] Create event on specific date 2021-04-04 21:38:46 +02:00
Ferdinand Thiessen 9e570b9746 [events] Allow creating new events from AgendaView 2021-04-04 21:38:46 +02:00
Ferdinand Thiessen 5e19a437bd [events] EditEvent set date of templates to today 2021-04-04 21:38:46 +02:00
Ferdinand Thiessen a59f778851 [events] AgendaView button for current week / day 2021-04-04 21:38:46 +02:00
Ferdinand Thiessen f712bfd4f9 [events] Structural and minor optical cleanup 2021-04-04 21:38:46 +02:00
25 changed files with 906 additions and 517 deletions

@ -1 +1 @@
Subproject commit f245cb8b16c855c059d9170611797028c600696a Subproject commit 000c043ce6cb4a082d8bde4ff2b8c955eade10a4

View File

@ -10,7 +10,7 @@ const config: { [key: string]: Array<string> } = {
// Do not change required Modules !! // Do not change required Modules !!
requiredModules: ['User'], requiredModules: ['User'],
// here you can import plugins. // here you can import plugins.
loadModules: ['Balance', 'Schedule', 'Pricelist'], loadModules: ['Balance', 'events', 'Pricelist'],
}; };
/* Stop! /* Stop!

View File

@ -1,9 +1,9 @@
<template> <template>
<q-card> <q-card>
<q-form @submit="save()" @reset="reset"> <q-form ref="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">
<div class="text-h6 col-xs-12 col-sm-6 q-pa-sm"> <div class="text-h6 col-xs-12 col-sm-6 q-pa-sm">
Veranstaltung <template v-if="modelValue">bearbeiten</template Veranstaltung <template v-if="modelValue.id">bearbeiten</template
><template v-else>erstellen</template> ><template v-else>erstellen</template>
</div> </div>
<q-select <q-select
@ -74,21 +74,27 @@
</q-card-section> </q-card-section>
<q-separator /> <q-separator />
<q-card-section> <q-card-section>
<q-btn color="primary" label="Schicht hinzufügen" @click="addJob()" /> <q-card-section class="row justify-around" align="around">
</q-card-section> <div class="text-h6 text-center col-6">Schichten</div>
<q-card-section v-for="(job, index) in event.jobs" :key="index"> <div class="col-6 text-center">
<q-card class="q-my-auto"> <q-btn color="primary" label="Schicht hinzufügen" @click="addJob()" />
</div>
</q-card-section>
<template v-for="(job, index) in event.jobs" :key="index">
<job <job
:ref="active === index ? 'activeRef' : undefined"
v-model="event.jobs[index]" v-model="event.jobs[index]"
:job-can-delete="jobDeleteDisabled" :active="active === index"
class="q-mb-xs"
@remove-job="removeJob(index)" @remove-job="removeJob(index)"
@update:active="active = index"
/> />
</q-card> </template>
</q-card-section> </q-card-section>
<q-card-actions align="around"> <q-card-actions align="around">
<q-card-actions align="left"> <q-card-actions align="left">
<q-btn v-if="!template" color="secondary" label="Neue Vorlage" @click="save(true)" /> <q-btn v-if="template" color="negative" label="Vorlage löschen" @click="removeTemplate" />
<q-btn v-else color="negative" label="Vorlage löschen" @click="removeTemplate" /> <q-btn color="secondary" label="Vorlage speichern" @click="save(true)" />
</q-card-actions> </q-card-actions>
<q-card-actions align="right"> <q-card-actions align="right">
<q-btn label="Zurücksetzen" type="reset" /> <q-btn label="Zurücksetzen" type="reset" />
@ -101,8 +107,9 @@
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, PropType, ref, onBeforeMount } from 'vue'; import { computed, defineComponent, PropType, ref, onBeforeMount } from 'vue';
import { date, ModifyDateOptions } from 'quasar'; import { ModifyDateOptions, QForm, date as qdate } from 'quasar';
import { useScheduleStore } from '../../store'; import { useScheduleStore } from '../../store';
import { EditableEvent, emptyEvent, emptyJob } from '../../store/models';
import { notEmpty } from 'src/utils/validators'; import { notEmpty } from 'src/utils/validators';
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';
@ -113,9 +120,13 @@ export default defineComponent({
components: { IsoDateInput, Job, RecurrenceRule }, components: { IsoDateInput, Job, RecurrenceRule },
props: { props: {
modelValue: { modelValue: {
default: () => emptyEvent(),
type: Object as PropType<EditableEvent>,
},
date: {
required: false, required: false,
default: () => undefined, default: () => new Date(),
type: Object as PropType<FG.Event | undefined>, type: [Object, Number] as PropType<Date | number>,
}, },
}, },
emits: { emits: {
@ -123,31 +134,19 @@ export default defineComponent({
}, },
setup(props, { emit }) { setup(props, { emit }) {
const store = useScheduleStore(); const store = useScheduleStore();
const active = ref(0);
const emptyJob = { const activeRef = ref(Job);
id: NaN,
start: new Date(),
end: date.addToDate(new Date(), { hours: 1 }),
services: [],
required_services: 2,
type: store.jobTypes[0],
};
const emptyEvent = {
id: NaN,
start: new Date(),
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>(props.modelValue || Object.assign({}, emptyEvent));
const eventtypes = computed(() => store.eventTypes); const eventtypes = computed(() => store.eventTypes);
const jobDeleteDisabled = computed(() => event.value.jobs.length < 2); const form = ref<QForm>();
const recurrent = ref(false); const recurrent = ref(false);
const recurrenceRule = ref<FG.RecurrenceRule>({ frequency: 'daily', interval: 1 }); const recurrenceRule = ref<FG.RecurrenceRule>({ frequency: 'daily', interval: 1 });
const templates = computed(() => store.templates);
const template = ref<FG.Event>();
const event = ref<EditableEvent>(
props.modelValue.id
? props.modelValue
: Object.assign({}, props.modelValue, { start: new Date(props.date) })
);
onBeforeMount(() => { onBeforeMount(() => {
void store.getEventTypes(); void store.getEventTypes();
@ -155,23 +154,48 @@ export default defineComponent({
void store.getTemplates(); void store.getTemplates();
}); });
function addJob() { async function addJob() {
event.value.jobs.push(Object.assign({}, emptyJob)); if (event.value.jobs.length > 0) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
if (!(await activeRef.value?.validate())) return;
event.value.jobs.sort((a, b) => {
if (a.end && b.end)
if (a.end < b.end) return -1;
else if (a.end > b.end) return 1;
return a.start.getTime() - b.start.getTime();
});
}
event.value.jobs.push(
emptyJob(event.value.jobs[event.value.jobs.length - 1]?.end || new Date(props.date))
);
active.value = event.value.jobs.length - 1;
} }
function removeJob(index: number) { function removeJob(index: number) {
event.value.jobs.splice(index, 1); event.value.jobs.splice(index, 1);
active.value = index == active.value ? event.value.jobs.length - 1 : index;
} }
function fromTemplate(tpl: FG.Event) { function fromTemplate(tpl: FG.Event) {
template.value = tpl; template.value = tpl;
event.value = Object.assign({}, tpl); event.value = Object.assign({}, tpl);
event.value.start = qdate.buildDate({ hours: 0, minutes: 0, seconds: 0 });
const diff = event.value.start.getTime() - tpl.start.getTime();
if (event.value.end) event.value.end = new Date(event.value.end.getTime() + diff);
event.value.jobs.forEach((job) => {
job.start.setTime(job.start.getTime() + diff);
if (job.end) job.end.setTime(job.end.getTime() + diff);
});
} }
async function save(template = false) { async function save(template = false) {
if (!(await form.value?.validate())) return;
event.value.is_template = template; event.value.is_template = template;
try { try {
await store.addEvent(event.value); // Casting is save as .validate() ensures that type property is set!
await store.saveEvent(<FG.Event>event.value);
if (props.modelValue === undefined && recurrent.value && !event.value.is_template) { if (props.modelValue === undefined && recurrent.value && !event.value.is_template) {
let count = 0; let count = 0;
const options: ModifyDateOptions = {}; const options: ModifyDateOptions = {};
@ -187,11 +211,11 @@ export default defineComponent({
break; break;
} }
while (true) { while (true) {
event.value.start = date.addToDate(event.value.start, options); event.value.start = qdate.addToDate(event.value.start, options);
if (event.value.end) event.value.end = date.addToDate(event.value.end, options); if (event.value.end) event.value.end = qdate.addToDate(event.value.end, options);
event.value.jobs.forEach((job) => { event.value.jobs.forEach((job) => {
job.start = date.addToDate(job.start, options); job.start = qdate.addToDate(job.start, options);
if (job.end) job.end = date.addToDate(job.end, options); if (job.end) job.end = qdate.addToDate(job.end, options);
}); });
count++; count++;
if ( if (
@ -199,7 +223,7 @@ export default defineComponent({
(!recurrenceRule.value.count || count <= recurrenceRule.value.count) && (!recurrenceRule.value.count || count <= recurrenceRule.value.count) &&
(!recurrenceRule.value.until || event.value.start < recurrenceRule.value.until) (!recurrenceRule.value.until || event.value.start < recurrenceRule.value.until)
) )
await store.addEvent(event.value); void store.saveEvent(<FG.Event>event.value);
else break; else break;
} }
} }
@ -218,12 +242,13 @@ export default defineComponent({
} }
function reset() { function reset() {
event.value = Object.assign({}, props.modelValue || emptyEvent); event.value = Object.assign({}, props.modelValue || emptyEvent());
template.value = undefined; template.value = undefined;
} }
return { return {
jobDeleteDisabled, active,
activeRef,
addJob, addJob,
eventtypes, eventtypes,
templates, templates,
@ -231,6 +256,7 @@ export default defineComponent({
notEmpty, notEmpty,
save, save,
reset, reset,
form,
recurrent, recurrent,
fromTemplate, fromTemplate,
removeTemplate, removeTemplate,

View File

@ -1,6 +1,6 @@
<template> <template>
<q-card-section class="fit row justify-start content-center items-center"> <q-card class="fit row justify-start content-center items-center" @click="activate">
<q-card-section class="fit row justify-start content-center items-center"> <q-form v-if="active" ref="form" class="fit row justify-start content-center items-center">
<IsoDateInput <IsoDateInput
v-model="job.start" v-model="job.start"
class="col-xs-12 col-sm-6 q-pa-sm" class="col-xs-12 col-sm-6 q-pa-sm"
@ -40,42 +40,57 @@
<q-input <q-input
v-model="job.comment" v-model="job.comment"
class="col-12 q-pa-sm" class="col-12 q-pa-sm"
label="Beschreibung" label="Kommentar"
type="textarea" type="textarea"
filled filled
/> />
</q-form>
<q-card-section v-else class="fit row justify-start content-center items-center text-center">
<div class="text-h6 col-12">{{ formatStartEnd(job.start, job.end) }}</div>
<div class="text-subtitle1 col-12">
{{ job?.type?.name || 'Typ fehlt' }} ({{ job.required_services }})
</div>
<div class="text-body2 text-italic text-left col-12">{{ job.comment }}</div>
</q-card-section> </q-card-section>
<q-btn label="Schicht löschen" color="negative" :disabled="jobCanDelete" @click="removeJob" /> <q-card-actions class="fit row" align="right">
</q-card-section> <q-btn label="Schicht löschen" color="negative" @click="removeJob" />
</q-card-actions>
</q-card>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, computed, onBeforeMount, PropType } from 'vue'; import { defineComponent, computed, onBeforeMount, PropType, ref } from 'vue';
import IsoDateInput from 'src/components/utils/IsoDateInput.vue'; import IsoDateInput from 'src/components/utils/IsoDateInput.vue';
import { notEmpty } from 'src/utils/validators'; import { notEmpty } from 'src/utils/validators';
import { useScheduleStore } from '../../store'; import { useScheduleStore } from '../../store';
import { QForm } from 'quasar';
import { asDate, asHour } from 'src/utils/datetime';
export default defineComponent({ export default defineComponent({
name: 'Job', name: 'Job',
components: { IsoDateInput }, components: { IsoDateInput },
props: { props: {
active: {
default: () => true,
type: Boolean,
},
modelValue: { modelValue: {
required: true, required: true,
type: Object as PropType<FG.Job>, type: Object as PropType<FG.Job>,
}, },
jobCanDelete: Boolean,
}, },
emits: { emits: {
'remove-job': () => true, 'remove-job': () => true,
'update:active': (active: boolean) => typeof active === 'boolean',
'update:modelValue': (job: FG.Job) => !!job, 'update:modelValue': (job: FG.Job) => !!job,
}, },
setup(props, { emit }) { setup(props, { emit, expose }) {
const store = useScheduleStore(); const store = useScheduleStore();
onBeforeMount(() => store.getJobTypes()); onBeforeMount(() => store.getJobTypes());
const form = ref<QForm>();
const jobtypes = computed(() => store.jobTypes); const jobtypes = computed(() => store.jobTypes);
const job = new Proxy(props.modelValue, { const job = new Proxy(props.modelValue, {
get(target, prop) { get(target, prop) {
if (typeof prop === 'string') { if (typeof prop === 'string') {
@ -91,6 +106,15 @@ export default defineComponent({
}, },
}); });
function formatStartEnd(start: Date, end?: Date) {
const startDate = asDate(start);
const endDate = end ? asDate(end) : end;
return (
`${startDate}, ${asHour(start)}` +
(endDate ? ` - ${endDate !== startDate ? endDate + ', ' : ''}` + asHour(end) : '')
);
}
function removeJob() { function removeJob() {
emit('remove-job'); emit('remove-job');
} }
@ -99,7 +123,19 @@ export default defineComponent({
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';
} }
function activate() {
emit('update:active', true);
}
function validate() {
return form.value?.validate() || new Promise((r) => r(false));
}
expose({ validate });
return { return {
activate,
form,
formatStartEnd,
job, job,
jobtypes, jobtypes,
removeJob, removeJob,

View File

@ -0,0 +1,296 @@
<template>
<q-dialog
:model-value="editor !== undefined"
persistent
transition-show="scale"
transition-hide="scale"
>
<q-card>
<div class="column">
<div
class="col"
align="right"
style="position: sticky; top: 5px; padding-right: 5px; z-index: 999"
>
<q-btn round color="negative" icon="mdi-close" dense rounded @click="editDone(false)" />
</div>
<div class="col" style="margin: 0; padding: 0; margin-top: -2.4em">
<edit-event v-model="editor" :date="editor.start" @done="editDone" />
</div>
</div>
</q-card>
</q-dialog>
<q-card>
<q-toolbar class="bg-primary text-white q-my-md shadow-2 items-center row justify-center">
<q-btn flat dense class="absolute-left q-ml-sm"
>{{ asMonth(selectedDate) }} {{ asYear(selectedDate) }}
<q-popup-proxy transition-show="scale" transition-hide="scale" @before-show="updateProxy">
<q-date v-model="proxyDate">
<div class="row items-center justify-end q-gutter-sm">
<q-btn v-close-popup label="Cancel" color="primary" flat />
<q-btn
v-close-popup
label="OK"
color="primary"
flat
@click="saveSelectedDate(proxyDate)"
/>
</div>
</q-date>
</q-popup-proxy>
</q-btn>
<div
class="row"
:class="{ 'absolute-right': windowWidth < 600, 'q-mr-sm': windowWidth < 600 }"
>
<q-btn flat dense label="Zurück" @click="calendarPrev" />
<q-separator vertical />
<q-btn
flat
dense
:label="calendarDays == 1 ? 'Heute' : 'Diese Woche'"
@click="calendarNow"
/>
<q-separator vertical />
<q-btn flat dense label="Weiter" @click="calendarNext" />
</div>
<!-- <q-space /> -->
<q-btn-toggle
v-if="windowWidth >= 600"
v-model="calendarView"
class="row absolute-right"
flat
stretch
toggle-color=""
:options="[
{ label: 'Tag', value: 'day' },
{ label: 'Woche', value: 'week' },
]"
/>
</q-toolbar>
<q-calendar-agenda
v-model="selectedDate"
:view="calendarRealView"
:max-days="calendarDays"
:weekdays="[1, 2, 3, 4, 5, 6, 0]"
locale="de-de"
style="height: 100%; min-height: 400px"
>
<template #head-day-button="{ scope: { dayLabel, timestamp, activeDate } }">
<q-btn
round
dense
size="sm"
class="q-mb-xs"
:label="dayLabel"
:color="formatDayColor(timestamp.current, activeDate)"
>
<q-menu>
<q-list style="min-width: 100px">
<q-item clickable @click="showDay(timestamp.date)">
<q-item-section>Anzeigen</q-item-section>
</q-item>
<q-item clickable @click="newEvent(timestamp.date)">
<q-item-section>Neue Veranstaltung</q-item-section>
</q-item>
</q-list>
</q-menu>
</q-btn>
</template>
<template #day="{ scope: { timestamp } }">
<div itemref="" class="q-pb-sm" style="min-height: 200px">
<eventslot
v-for="(agenda, index) in events[timestamp.weekday]"
:key="index"
v-model="events[timestamp.weekday][index]"
@removeEvent="remove"
@editEvent="edit"
/>
</div>
</template>
</q-calendar-agenda>
</q-card>
</template>
<script lang="ts">
import { computed, defineComponent, onBeforeMount, ref } from 'vue';
import { startOfWeek } from 'src/utils/datetime';
import { useScheduleStore } from '../../store';
import { date } from 'quasar';
import Eventslot from './slots/EventSlot.vue';
import EditEvent from '../management/EditEvent.vue';
import { EditableEvent, emptyEvent } from '../../store/models';
export default defineComponent({
name: 'AgendaView',
components: { Eventslot, EditEvent },
setup() {
const store = useScheduleStore();
const windowWidth = ref(window.innerWidth);
const selectedDate = ref(date.formatDate(new Date(), 'YYYY-MM-DD'));
const proxyDate = ref('');
const calendarView = ref<'week' | 'day'>('week');
const calendarRealView = computed(() => (calendarDays.value != 7 ? 'day' : 'week'));
const calendarDays = computed(() =>
// 599px is breakpoint for xs, 1439px is the breakpoint for md
calendarView.value == 'day' || windowWidth.value < 600 ? 1 : windowWidth.value <= 1440 ? 3 : 7
);
const events = ref<Agendas>({});
const editor = ref<EditableEvent | undefined>(undefined);
interface Agendas {
[index: number]: FG.Event[];
}
onBeforeMount(async () => {
window.addEventListener('resize', () => {
windowWidth.value = window.innerWidth;
});
await loadAgendas();
});
async function edit(id: number) {
editor.value = await store.getEvent(id);
}
function editDone(changed: boolean) {
if (changed) void loadAgendas();
editor.value = undefined;
}
async function remove(id: number) {
if (await store.removeEvent(id)) {
// Successfull removed
for (const idx in events.value) {
const i = events.value[idx].findIndex((event) => event.id === id);
if (i !== -1) {
events.value[idx].splice(i, 1);
break;
}
}
} else {
// Not found, this means our eventa are outdated
await loadAgendas();
}
}
async function loadAgendas() {
const selected = new Date(selectedDate.value);
const start = calendarRealView.value === 'day' ? selected : startOfWeek(selected);
const end = date.addToDate(start, { days: calendarDays.value });
events.value = {};
const list = await store.getEvents({ from: start, to: end });
list.forEach((event) => {
const day = event.start.getDay();
if (!events.value[day]) {
events.value[day] = [];
}
events.value[day].push(event);
});
}
function calendarNext() {
selectedDate.value = date.formatDate(
date.addToDate(selectedDate.value, { days: calendarDays.value }),
'YYYY-MM-DD'
);
void loadAgendas();
}
function calendarPrev() {
selectedDate.value = date.formatDate(
date.subtractFromDate(selectedDate.value, { days: calendarDays.value }),
'YYYY-MM-DD'
);
void loadAgendas();
}
function calendarNow() {
const today = date.formatDate(new Date(), 'YYYY-MM-DD');
if (today !== selectedDate.value) {
selectedDate.value = today;
void loadAgendas();
}
}
function showDay(date: string) {
calendarView.value = 'day';
selectedDate.value = date;
}
function updateProxy() {
proxyDate.value = selectedDate.value;
}
function saveSelectedDate() {
proxyDate.value = date.formatDate(proxyDate.value, 'YYYY-MM-DD');
selectedDate.value = proxyDate.value;
}
function asMonth(value: string) {
if (value) {
return date.formatDate(new Date(value), 'MMMM', {
months: [
'Januar',
'Februar',
'März',
'April',
'Mai',
'Juni',
'Juli',
'August',
'September',
'Oktober',
'November',
'Dezember',
],
});
}
}
function asYear(value: string) {
if (value) {
return date.formatDate(new Date(value), 'YYYY');
}
}
function formatDayColor(today: boolean, selected: boolean) {
if (today) return 'primary';
if (selected) return 'secondary';
}
function newEvent(start: Date | string | number) {
if (typeof start === 'string') start = date.extractDate(start, 'YYYY-MM-DD');
editor.value = emptyEvent(new Date(start));
}
return {
asYear,
asMonth,
calendarDays,
calendarNext,
calendarNow,
calendarPrev,
calendarRealView,
calendarView,
edit,
editor,
editDone,
events,
formatDayColor,
newEvent,
proxyDate,
remove,
saveSelectedDate,
selectedDate,
showDay,
updateProxy,
windowWidth,
};
},
});
</script>
<style></style>

View File

@ -0,0 +1,92 @@
<template>
<q-dialog
:model-value="editor !== undefined"
persistent
transition-show="scale"
transition-hide="scale"
>
<q-card>
<div class="column">
<div class="col" align="right" style="position: sticky; top: 0; z-index: 999">
<q-btn round color="negative" icon="close" dense rounded @click="editDone(false)" />
</div>
<div class="col" style="margin: 0; padding: 0; margin-top: -2.4em">
<edit-event v-model="editor" @done="editDone" />
</div>
</div>
</q-card>
</q-dialog>
<div style="height: 700px">
<q-infinite-scroll ref="scroll" :offset="250" @load="loadEvents">
<div v-for="(event, index) in events" :key="index" class="caption">
<event-slot v-model="events[index]" size="md" />
</div>
<template #loading>
<div class="row justify-center q-my-md">
<q-spinner-dots color="primary" size="40px" />
</div>
</template>
</q-infinite-scroll>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
import { useScheduleStore } from '../../store';
import { date, QInfiniteScroll } from 'quasar';
import EventSlot from './slots/EventSlot.vue';
import EditEvent from '../management/EditEvent.vue';
export default defineComponent({
name: 'EventsListView',
components: { EditEvent, EventSlot },
setup() {
const store = useScheduleStore();
const events = ref<FG.Event[]>([]);
const editor = ref<FG.Event>();
const scroll = ref<QInfiniteScroll>();
async function edit(id: number) {
editor.value = await store.getEvent(id);
}
function editDone(changed: boolean) {
//if (changed) void loadEvents();
editor.value = undefined;
}
async function remove(id: number) {
if (await store.removeEvent(id)) {
const idx = events.value.findIndex((v) => v.id === id);
if (idx !== -1) events.value.splice(idx, 1);
} else {
events.value = [];
scroll.value?.reset();
}
}
async function loadEvents(index: number, done?: (stop: boolean) => void) {
console.log(index);
const today = new Date();
today.setHours(0, 0);
const from = date.addToDate(today, { days: (index - 1) * 3 });
const to = date.addToDate(today, { days: index * 3 });
const ev = await store.getEvents({ from, to });
if (ev.length > 0) events.value.push(...ev);
if (done !== undefined) done(ev.length == 0);
}
return {
scroll,
edit,
editor,
editDone,
events,
remove,
loadEvents,
};
},
});
</script>
<style></style>

View File

@ -1,16 +1,20 @@
<template> <template>
<q-card <q-card class="q-mx-xs q-mt-sm rounded-borders shadow-5" bordered>
class="q-mx-xs q-mt-sm justify-start content-center items-center rounded-borders shadow-5" <q-spinner-rings
bordered v-if="serviceNeeded"
> size="100%"
<q-card-section class="text-primary q-pa-xs"> style="max-height: min(3vw, 3em); max-width: min(3vw, 3em); position: absolute"
<div class="text-weight-bolder text-center" style="font-size: 1.5vw"> color="warning"
{{ event.type.name }} />
<template v-if="event.name" <q-card-section class="text-primary q-pa-xs" style="font-size: clamp(1em, 1.5vw, 1.6em)">
>: <span style="font-size: 1.2vw">{{ event.name }}</span> <div class="text-weight-bolder text-center">
</template> {{ event.type.name }}<template v-if="event.name">: </template
><span style="font-size: 0.9em">
{{ event.name }}
</span>
</div> </div>
<div v-if="event.description" class="text-weight-medium" style="font-size: 1vw">
<div v-if="event.description" class="text-weight-medium" style="font-size: 0.75em">
{{ event.description }} {{ event.description }}
</div> </div>
</q-card-section> </q-card-section>
@ -49,7 +53,7 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, computed, PropType } from 'vue'; import { defineComponent, computed, PropType } from 'vue';
import { hasPermission } from 'src/utils/permission'; import { hasPermission } from 'src/utils/permission';
import { PERMISSIONS } from 'src/plugins/schedule/permissions'; import { PERMISSIONS } from 'src/plugins/events/permissions';
import JobSlot from './JobSlot.vue'; import JobSlot from './JobSlot.vue';
export default defineComponent({ export default defineComponent({
@ -78,6 +82,14 @@ export default defineComponent({
set: (v) => emit('update:modelValue', v), set: (v) => emit('update:modelValue', v),
}); });
const serviceNeeded = computed(() =>
props.modelValue.jobs.some(
(job) =>
job.required_services >
job.services.reduce((p, c) => ((c.value += p.value) && c) || c, { value: 0 }).value
)
);
function remove() { function remove() {
emit('removeEvent', props.modelValue.id); emit('removeEvent', props.modelValue.id);
} }
@ -91,6 +103,7 @@ export default defineComponent({
edit, edit,
event, event,
remove, remove,
serviceNeeded,
}; };
}, },
}); });

View File

@ -0,0 +1,242 @@
<template>
<q-card bordered>
<q-dialog :model-value="dialog">
<q-card style="min-width: 320px">
<q-card-section class="row items-center q-pb-none">
<div class="text-h6">
{{ userDisplay(service.userid) }} {{ asDate(modelValue.start) }}
</div>
<q-space />
<q-btn icon="mdi-close" flat round dense @click="dialog = false" />
</q-card-section>
<q-card-section>
{{ modelValue.type.name }} {{ asHour(modelValue.start)
}}{{ modelValue.end ? ` - ${asHour(modelValue.end)}` : '' }}
Uhr
</q-card-section>
<q-card-actions align="around">
<q-btn style="width: 47.5%" color="primary" label="Eintragen" @click="enroll()" />
<q-toggle
v-model="service.is_backup"
style="width: 47.5%"
color="primary"
label="Als Backup"
/>
</q-card-actions>
<q-card-actions v-if="!enrolled(service.userid)" align="around">
<q-btn
v-if="isEnrolled"
style="width: 47.5%"
color="negative"
label="Übertragen"
@click="enroll(true)"
/>
<q-btn
v-if="!iam(service.userid)"
style="width: 47.5%"
color="secondary"
label="Einladen"
@click="enroll(false, true)"
/>
</q-card-actions>
</q-card>
</q-dialog>
<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"
:options="options"
:option-label="(v) => userDisplay(v.userid)"
filled
multiple
stack-label
label="Dienste"
class="col-auto q-px-xs"
style="font-size: 6px"
counter
:max-values="modelValue.required_services"
@add="enrollDialog"
>
<template #selected-item="scope">
<q-chip
:removable="canEdit(scope.opt.userid)"
dense
:tabindex="scope.tabindex"
color="white"
:text-color="scope.opt.is_backup ? 'primary' : 'secondary'"
class="q-ma-none"
@remove="remove(scope.opt, scope.removeAtIndex, scope.index)"
>
<q-avatar :color="scope.opt.is_backup ? 'primary' : 'secondary'" text-color="white">
<img
:src="userAvatar(scope.opt.userid)"
onerror="this.src=' data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==';"
/>
</q-avatar>
{{ userDisplay(scope.opt.userid) }}
</q-chip>
</template>
</q-select>
</div>
</q-card>
</template>
<script lang="ts">
import { defineComponent, onBeforeMount, computed, ref, PropType } from 'vue';
import { Notify } from 'quasar';
import { asHour, asDate } from 'src/utils/datetime';
import { useUserStore } from 'src/plugins/user/store';
import { useMainStore } from 'src/stores';
import { useScheduleStore } from 'src/plugins/events/store';
import { PERMISSIONS } from 'src/plugins/events/permissions';
import { hasPermission } from 'src/utils/permission';
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;
const dialog = ref(false);
const service = ref<FG.Service>();
onBeforeMount(async () => userStore.getUsers());
function userDisplay(uid: string) {
return userStore.findUser(uid)?.display_name || uid;
}
function userAvatar(uid: string) {
return userStore.findUser(uid)?.avatar_url;
}
function enrollDialog(details: { index: number; value: FG.User }) {
service.value = {
userid: details.value.userid,
is_backup: false,
value: 1,
};
dialog.value = true;
}
function iam(uid: string) {
return uid === mainStore.currentUser.userid;
}
async function enroll(transfer = false, invite = false) {
try {
dialog.value = false;
if (transfer)
service.value = Object.assign({}, service.value, {
replace: mainStore.currentUser.userid,
});
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
else if (invite) delete (<any>service.value).value;
const job = await store.updateJob(props.eventId, props.modelValue.id, {
user: <FG.Service>service.value,
});
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 remove(service: FG.Service, rem: (i: number) => void, index: number) {
if (
service.userid === mainStore.currentUser.userid ||
hasPermission(PERMISSIONS.ASSIGN_OTHER)
) {
rem(index);
try {
const job = await store.updateJob(props.eventId, props.modelValue.id, {
user: Object.assign({}, service, { value: -service.value }),
});
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' }],
});
}
}
}
const options = computed(() =>
hasPermission(PERMISSIONS.ASSIGN_OTHER)
? userStore.users.filter((v) => !enrolled(v.userid))
: !isEnrolled.value
? [mainStore.currentUser]
: []
);
function canEdit(uid: string) {
return uid === mainStore.currentUser.userid || hasPermission(PERMISSIONS.ASSIGN_OTHER);
}
function enrolled(userid: string) {
return props.modelValue.services.findIndex((service) => service.userid == userid) !== -1;
}
const isEnrolled = computed(() => enrolled(mainStore.currentUser.userid));
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;
});
return {
enroll,
availableUsers,
canEdit,
isEnrolled,
enrolled,
enrollDialog,
canEnroll,
remove,
userAvatar,
userDisplay,
options,
iam,
dialog,
service,
asHour,
asDate,
};
},
});
</script>
<style scoped></style>

View File

@ -1,5 +1,5 @@
<template> <template>
<div> <q-page padding class="fit row justify-center content-start items-start q-gutter-sm">
<q-tabs v-if="$q.screen.gt.sm" v-model="tab"> <q-tabs v-if="$q.screen.gt.sm" v-model="tab">
<q-tab <q-tab
v-for="(tabindex, index) in tabs" v-for="(tabindex, index) in tabs"
@ -24,25 +24,23 @@
</q-item> </q-item>
</q-list> </q-list>
</q-drawer> </q-drawer>
<q-page padding class="fit row justify-center content-start items-start q-gutter-sm"> <q-tab-panels
<q-tab-panels v-model="tab"
v-model="tab" style="background-color: transparent"
style="background-color: transparent" class="q-ma-none q-pa-none fit row justify-center content-start items-start"
class="q-ma-none q-pa-none fit row justify-center content-start items-start" animated
animated >
> <q-tab-panel name="create">
<q-tab-panel name="create"> <EditEvent />
<EditEvent /> </q-tab-panel>
</q-tab-panel> <q-tab-panel name="eventtypes">
<q-tab-panel name="eventtypes"> <EventTypes />
<EventTypes /> </q-tab-panel>
</q-tab-panel> <q-tab-panel name="jobtypes">
<q-tab-panel name="jobtypes"> <JobTypes v-if="canEditJobTypes" />
<JobTypes v-if="canEditJobTypes" /> </q-tab-panel>
</q-tab-panel> </q-tab-panels>
</q-tab-panels> </q-page>
</q-page>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">

View File

@ -1,5 +1,5 @@
<template> <template>
<div> <q-page padding class="fit row justify-center content-start items-start q-gutter-sm">
<q-tabs v-if="$q.screen.gt.sm" v-model="tab"> <q-tabs v-if="$q.screen.gt.sm" v-model="tab">
<q-tab <q-tab
v-for="(tabindex, index) in tabs" v-for="(tabindex, index) in tabs"
@ -24,34 +24,31 @@
</q-item> </q-item>
</q-list> </q-list>
</q-drawer> </q-drawer>
<q-page padding class="fit row justify-center content-start items-start q-gutter-sm"> <q-tab-panels
<q-tab-panels v-model="tab"
v-model="tab" style="background-color: transparent"
style="background-color: transparent" class="q-ma-none q-pa-none fit row justify-center content-start items-start"
class="q-ma-none q-pa-none fit row justify-center content-start items-start" animated
animated >
> <q-tab-panel name="agendaView">
<q-tab-panel name="agendaView"> <AgendaView />
<AgendaView /> </q-tab-panel>
</q-tab-panel> <q-tab-panel name="listView">
<q-tab-panel name="eventtypes"> <EventsListView />
<EventTypes /> </q-tab-panel>
</q-tab-panel> </q-tab-panels>
</q-tab-panels> </q-page>
</q-page>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, ref } from 'vue'; import { computed, defineComponent, ref } from 'vue';
import EventTypes from '../components/management/EventTypes.vue'; import EventsListView from '../components/overview/ListView.vue';
//import CreateEvent from '../components/management/CreateEvent.vue';
import AgendaView from '../components/overview/AgendaView.vue'; import AgendaView from '../components/overview/AgendaView.vue';
import { Screen } from 'quasar'; import { Screen } from 'quasar';
export default defineComponent({ export default defineComponent({
name: 'EventOverview', name: 'EventOverview',
components: { AgendaView, EventTypes }, components: { AgendaView, EventsListView },
setup() { setup() {
interface Tab { interface Tab {
name: string; name: string;
@ -60,8 +57,7 @@ export default defineComponent({
const tabs: Tab[] = [ const tabs: Tab[] = [
{ name: 'agendaView', label: 'Kalendar' }, { name: 'agendaView', label: 'Kalendar' },
// { name: 'eventtypes', label: 'Veranstaltungsarten' }, { name: 'listView', label: 'Liste' },
// { name: 'jobtypes', label: 'Dienstarten' }
]; ];
const drawer = ref<boolean>(false); const drawer = ref<boolean>(false);

View File

@ -0,0 +1,44 @@
import { defineAsyncComponent } from 'vue';
import { innerRoutes, privateRoutes } from './routes';
import { FG_Plugin } from 'src/plugins';
interface EventNotification extends FG_Plugin.Notification {
data: { type: NotificationType };
}
enum NotificationType {
REQUEST = 0x10,
ACCEPTED = 0x11,
REJECTED = 0x12,
}
function transpile(n: FG.Notification) {
const notification = <EventNotification>Object.assign({}, n);
if (notification.data.type === NotificationType.REQUEST)
notification.accept = () =>
new Promise((r) => {
console.log('REQUEST ACCEPTED');
r();
});
return notification;
}
const plugin: FG_Plugin.Plugin = {
name: 'events',
innerRoutes,
internalRoutes: privateRoutes,
requiredModules: ['User'],
requiredBackendModules: ['events'],
version: '0.0.2',
notification: transpile,
widgets: [
{
priority: 0,
name: 'stats',
permissions: [],
widget: defineAsyncComponent(() => import('./components/Widget.vue')),
},
],
};
export default plugin;

View File

@ -148,7 +148,7 @@ export const useScheduleStore = defineStore({
async updateJob(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.Job>(`/events/${eventId}/jobs/${jobId}`, service); const { data } = await api.put<FG.Job>(`/events/${eventId}/${jobId}`, service);
fixJob(data); fixJob(data);
return data; return data;
} catch (error) { } catch (error) {
@ -156,10 +156,26 @@ export const useScheduleStore = defineStore({
} }
}, },
async addEvent(event: FG.Event) { async createEvent(event: FG.Event) {
const { data } = await api.post<FG.Event>('/events', event); const { data } = await api.post<FG.Event>('/events', event);
if (data.is_template) this.templates.push(data); if (data.is_template) this.templates.push(data);
return data; return data;
}, },
async updateEvent(event: FG.Event) {
const { data } = await api.put<FG.Event>(`/events/${event.id}`, event);
if (data.is_template)
this.templates.splice(
this.templates.findIndex((t) => t.id === event.id),
1,
data
);
return data;
},
async saveEvent(event: FG.Event) {
if (event.id) return this.updateEvent(event);
else return this.createEvent(event);
},
}, },
}); });

View File

@ -0,0 +1,35 @@
import { date } from 'quasar';
/** An new event does not contain an id and the type might be unset */
export type EditableEvent = Omit<Omit<Omit<FG.Event, 'jobs'>, 'type'>, 'id'> & {
type?: FG.EventType | number;
id?: number;
jobs: EditableJob[];
};
/** A new job does not have an id or type assigned */
export type EditableJob = Omit<Omit<FG.Job, 'type'>, 'id'> & {
type?: FG.EventType | number;
id?: number;
};
export const emptyJob = (d: Date | number = new Date()): EditableJob =>
Object.assign(
{},
{
start: date.adjustDate(d, { minutes: 0, seconds: 0 }),
end: date.addToDate(date.adjustDate(d, { minutes: 0, seconds: 0 }), { hours: 1 }),
services: [],
required_services: 2,
}
);
export const emptyEvent = (d: Date | number = new Date()): EditableEvent =>
Object.assign(
{},
{
start: date.adjustDate(d, { hours: 0, minutes: 0, seconds: 0 }),
jobs: [],
is_template: false,
}
);

View File

@ -1,241 +0,0 @@
<template>
<q-dialog
:model-value="editor !== undefined"
persistent
transition-show="scale"
transition-hide="scale"
>
<q-card>
<div class="column">
<div class="col" align="right" style="position: sticky; top: 0; z-index: 999">
<q-btn round color="negative" icon="close" dense rounded @click="editDone(false)" />
</div>
<div class="col" style="margin: 0; padding: 0; margin-top: -2.4em">
<edit-event v-model="editor" @done="editDone" />
</div>
</div>
</q-card>
</q-dialog>
<q-page padding>
<q-card>
<div style="max-width: 1800px; width: 100%">
<q-toolbar class="bg-primary text-white q-my-md shadow-2 items-center row justify-center">
<div class="row justify-center items-center">
<q-btn flat dense label="Prev" @click="calendarPrev" />
<q-separator vertical />
<q-btn flat dense
>{{ asMonth(selectedDate) }} {{ asYear(selectedDate) }}
<q-popup-proxy
transition-show="scale"
transition-hide="scale"
@before-show="updateProxy"
>
<q-date v-model="proxyDate">
<div class="row items-center justify-end q-gutter-sm">
<q-btn v-close-popup label="Cancel" color="primary" flat />
<q-btn
v-close-popup
label="OK"
color="primary"
flat
@click="saveNewSelectedDate(proxyDate)"
/>
</div>
</q-date>
</q-popup-proxy>
</q-btn>
<q-separator vertical />
<q-btn flat dense label="Next" @click="calendarNext" />
</div>
<!-- <q-space /> -->
<q-btn-toggle
v-model="calendarView"
class="row absolute-right"
flat
stretch
toggle-color=""
:options="[
{ label: 'Tag', value: 'day' },
{ label: 'Woche', value: 'week' },
]"
/>
</q-toolbar>
<q-calendar-agenda
v-model="selectedDate"
:view="calendarRealView"
:max-days="calendarDays"
:weekdays="[1, 2, 3, 4, 5, 6, 0]"
locale="de-de"
style="height: 100%; min-height: 400px"
>
<template #day="{ scope: { timestamp } }">
<div itemref="" class="q-pb-sm" style="min-height: 200px">
<eventslot
v-for="(agenda, index) in events[timestamp.weekday]"
:key="index"
v-model="events[timestamp.weekday][index]"
@removeEvent="remove"
@editEvent="edit"
/>
</div>
</template>
</q-calendar-agenda>
</div>
</q-card>
</q-page>
</template>
<script lang="ts">
import { computed, defineComponent, onBeforeMount, ref } from 'vue';
import { useScheduleStore } from '../../store';
import Eventslot from './slots/EventSlot.vue';
import { date } from 'quasar';
import { startOfWeek } from 'src/utils/datetime';
import EditEvent from '../management/EditEvent.vue';
export default defineComponent({
name: 'AgendaView',
components: { Eventslot, EditEvent },
setup() {
const store = useScheduleStore();
const windowWidth = ref(window.innerWidth);
const selectedDate = ref(date.formatDate(new Date(), 'YYYY-MM-DD'));
const proxyDate = ref('');
const calendarView = ref('week');
const calendarRealView = computed(() => (calendarDays.value != 7 ? 'day' : 'week'));
const calendarDays = computed(() =>
// <= 1023 is the breakpoint for sm to md
calendarView.value == 'day' ? 1 : windowWidth.value <= 1023 ? 3 : 7
);
const events = ref<Agendas>({});
const editor = ref<FG.Event | undefined>(undefined);
interface Agendas {
[index: number]: FG.Event[];
}
onBeforeMount(async () => {
window.addEventListener('resize', () => {
windowWidth.value = window.innerWidth;
});
await loadAgendas();
});
async function edit(id: number) {
editor.value = await store.getEvent(id);
}
function editDone(changed: boolean) {
if (changed) void loadAgendas();
editor.value = undefined;
}
async function remove(id: number) {
if (await store.removeEvent(id)) {
// Successfull removed
for (const idx in events.value) {
const i = events.value[idx].findIndex((event) => event.id === id);
if (i !== -1) {
events.value[idx].splice(i, 1);
break;
}
}
} else {
// Not found, this means our eventa are outdated
await loadAgendas();
}
}
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 });
events.value = {};
const list = await store.getEvents({ from: start, to: end });
list.forEach((event) => {
const day = event.start.getDay();
if (!events.value[day]) {
events.value[day] = [];
}
events.value[day].push(event);
});
}
function calendarNext() {
selectedDate.value = date.formatDate(
date.addToDate(selectedDate.value, { days: calendarDays.value }),
'YYYY-MM-DD'
);
void loadAgendas();
}
function calendarPrev() {
selectedDate.value = date.formatDate(
date.subtractFromDate(selectedDate.value, { days: calendarDays.value }),
'YYYY-MM-DD'
);
void loadAgendas();
}
function updateProxy() {
proxyDate.value = selectedDate.value;
}
function saveNewSelectedDate() {
proxyDate.value = date.formatDate(proxyDate.value, 'YYYY-MM-DD');
selectedDate.value = proxyDate.value;
}
function asMonth(value: string) {
if (value) {
return date.formatDate(new Date(value), 'MMMM', {
months: [
'Januar',
'Februar',
'März',
'April',
'Mai',
'Juni',
'Juli',
'August',
'September',
'Oktober',
'November',
'Dezember',
],
});
}
}
function asYear(value: string) {
if (value) {
return date.formatDate(new Date(value), 'YYYY');
}
}
return {
asYear,
asMonth,
selectedDate,
edit,
editor,
editDone,
events,
calendarNext,
calendarPrev,
updateProxy,
saveNewSelectedDate,
proxyDate,
remove,
calendarDays,
calendarView,
calendarRealView,
};
},
});
</script>
<style></style>

View File

@ -1,142 +0,0 @@
<template>
<q-card bordered>
<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/stores';
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,
is_backup: false,
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,
is_backup: false,
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>

View File

@ -1,22 +0,0 @@
import { defineAsyncComponent } from 'vue';
import { innerRoutes, privateRoutes } from './routes';
import { FG_Plugin } from 'src/plugins';
const plugin: FG_Plugin.Plugin = {
name: 'Schedule',
innerRoutes,
internalRoutes: privateRoutes,
requiredModules: ['User'],
requiredBackendModules: ['events'],
version: '0.0.1',
widgets: [
{
priority: 0,
name: 'stats',
permissions: [],
widget: defineAsyncComponent(() => import('./components/Widget.vue')),
},
],
};
export default plugin;