Compare commits
16 Commits
develop
...
feature/ev
Author | SHA1 | Date |
---|---|---|
Ferdinand Thiessen | dd33e77514 | |
Ferdinand Thiessen | 494a555985 | |
Ferdinand Thiessen | 5780c73608 | |
Ferdinand Thiessen | 7d1993e3fa | |
Ferdinand Thiessen | 6a35f3c669 | |
Ferdinand Thiessen | 3530132e72 | |
Ferdinand Thiessen | 48aa6c724a | |
Ferdinand Thiessen | 43dcd0579e | |
Ferdinand Thiessen | 1f71abcd4b | |
Ferdinand Thiessen | 8abcd44340 | |
Ferdinand Thiessen | 4bc602fed2 | |
Ferdinand Thiessen | 285803a226 | |
Ferdinand Thiessen | 9e570b9746 | |
Ferdinand Thiessen | 5e19a437bd | |
Ferdinand Thiessen | a59f778851 | |
Ferdinand Thiessen | f712bfd4f9 |
|
@ -1 +1 @@
|
||||||
Subproject commit f245cb8b16c855c059d9170611797028c600696a
|
Subproject commit 000c043ce6cb4a082d8bde4ff2b8c955eade10a4
|
|
@ -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!
|
||||||
|
|
|
@ -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,
|
|
@ -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,
|
|
@ -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>
|
|
@ -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>
|
|
@ -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,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
|
@ -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>
|
|
@ -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">
|
|
@ -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);
|
|
@ -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;
|
|
@ -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);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
|
@ -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,
|
||||||
|
}
|
||||||
|
);
|
|
@ -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>
|
|
|
@ -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>
|
|
|
@ -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;
|
|
Loading…
Reference in New Issue