[ported] Validate jobs before adding new and minimize inactive jobs

Ported from flaschengeist-frontend @7d1993e3faecdca3af47bc19f444857c49c3f3c4
This commit is contained in:
Ferdinand Thiessen 2021-11-23 17:32:48 +01:00
parent c31b804102
commit ff15ceb7d0
5 changed files with 151 additions and 94 deletions

View File

@ -27,7 +27,7 @@
"quasar": "^2.3.3", "quasar": "^2.3.3",
"axios": "^0.24.0", "axios": "^0.24.0",
"prettier": "^2.4.1", "prettier": "^2.4.1",
"typescript": "^4.4.4", "typescript": "^4.5.2",
"pinia": "^2.0.4", "pinia": "^2.0.4",
"@typescript-eslint/eslint-plugin": "^5.4.0", "@typescript-eslint/eslint-plugin": "^5.4.0",
"@typescript-eslint/parser": "^5.4.0", "@typescript-eslint/parser": "^5.4.0",

View File

@ -82,7 +82,14 @@
</div> </div>
</div> </div>
<template v-for="(job, index) in event.jobs" :key="index"> <template v-for="(job, index) in event.jobs" :key="index">
<edit-job-slot v-model="event.jobs[index]" @remove-job="removeJob(index)" /> <edit-job-slot
:ref="active === index ? 'activeJob' : undefined"
v-model="event.jobs[index]"
:active="index === active"
class="q-mb-md"
@activate="activate(index)"
@remove-job="removeJob(index)"
/>
</template> </template>
</q-card-section> </q-card-section>
<q-card-actions align="around"> <q-card-actions align="around">
@ -143,6 +150,8 @@ export default defineComponent({
}); });
}); });
const active = ref(0);
const activeJob = ref<{ validate: () => Promise<boolean> }>();
const templates = computed(() => store.templates); const templates = computed(() => store.templates);
const template = ref<FG.Event>(); const template = ref<FG.Event>();
const event = ref<EditableEvent>(props.modelValue || emptyEvent(startDate.value)); const event = ref<EditableEvent>(props.modelValue || emptyEvent(startDate.value));
@ -157,34 +166,40 @@ export default defineComponent({
}); });
function addJob() { function addJob() {
event.value.jobs.push(emptyJob()); if (!activeJob.value) event.value.jobs.push(emptyJob());
else
void activeJob.value.validate().then((success) => {
if (success) {
event.value.jobs.push(emptyJob());
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);
if (active.value >= index) active.value--;
} }
function fromTemplate(tpl: FG.Event) { function fromTemplate(tpl: FG.Event) {
const today = new Date() const today = new Date();
template.value = tpl; template.value = tpl;
event.value = Object.assign({}, tpl, {id: undefined}); event.value = Object.assign({}, tpl, { id: undefined });
// Adjust the start to match today // Adjust the start to match today
event.value.start = date.adjustDate(event.value.start,{ event.value.start = date.adjustDate(event.value.start, {
date: today.getDate(), date: today.getDate(),
month: today.getMonth() + 1, // js inconsitency between getDate (1-31) and getMonth (0-11) month: today.getMonth() + 1, // js inconsitency between getDate (1-31) and getMonth (0-11)
year: today.getFullYear() year: today.getFullYear(),
}) });
// Use timestamp difference for faster adjustment // Use timestamp difference for faster adjustment
const diff = event.value.start.getTime() - tpl.start.getTime() const diff = event.value.start.getTime() - tpl.start.getTime();
// Adjust end of event and all jobs // Adjust end of event and all jobs
if (event.value.end) if (event.value.end) event.value.end.setTime(event.value.end.getTime() + diff);
event.value.end.setTime(event.value.end.getTime() + diff) event.value.jobs.forEach((job) => {
event.value.jobs.forEach(job => { job.start.setTime(job.start.getTime() + diff);
job.start.setTime(job.start.getTime() + diff) if (job.end) job.end.setTime(job.end.getTime() + diff);
if (job.end) });
job.end.setTime(job.end.getTime() + diff)
})
} }
async function save(template = false) { async function save(template = false) {
@ -242,14 +257,24 @@ export default defineComponent({
function reset() { function reset() {
event.value = Object.assign({}, props.modelValue || emptyEvent()); event.value = Object.assign({}, props.modelValue || emptyEvent());
active.value = 0;
template.value = undefined; template.value = undefined;
} }
const afterStart = (d: Date) => const afterStart = (d: Date) =>
!d || event.value.start <= d || 'Das Veranstaltungsende muss vor dem Beginn liegen'; !d || event.value.start <= d || 'Das Veranstaltungsende muss vor dem Beginn liegen';
function activate(idx: number) {
void activeJob.value?.validate().then((s) => {
if (s) active.value = idx;
});
}
return { return {
activate,
active,
addJob, addJob,
activeJob,
afterStart, afterStart,
event, event,
eventtypes, eventtypes,

View File

@ -1,66 +1,82 @@
<template> <template>
<q-card class="fit row justify-start content-center items-center"> <q-card class="fit">
<q-card-section class="fit row justify-start content-center items-center"> <q-card-section
<IsoDateInput v-if="!active"
v-model="job.start" class="fit row justify-start content-center items-center text-center"
class="col-xs-12 col-sm-6 q-pa-sm" @click="$emit('activate')"
label="Beginn" >
type="datetime" <div class="text-h6 col-12">{{ formatStartEnd(modelValue.start, modelValue.end) }}</div>
:rules="[notEmpty]" <div class="text-subtitle1 col-12">{{ typeName }} ({{ modelValue.required_services }})</div>
/> <div class="text-body2 text-italic text-left col-12">{{ modelValue.comment }}</div>
<IsoDateInput </q-card-section>
v-model="job.end" <q-card-section v-else>
class="col-xs-12 col-sm-6 q-pa-sm" <q-form ref="form" class="fit row justify-start content-center items-center">
label="Ende" <IsoDateInput
type="datetime" v-model="job.start"
:rules="[notEmpty, isAfterDate]" class="col-xs-12 col-sm-6 q-pa-sm"
/> label="Beginn"
<q-select type="datetime"
v-model="job.type" :rules="[notEmpty]"
filled />
use-input <IsoDateInput
label="Dienstart" v-model="job.end"
input-debounce="0" class="col-xs-12 col-sm-6 q-pa-sm"
class="col-xs-12 col-sm-6 q-pa-sm" label="Ende"
:options="jobtypes" type="datetime"
option-label="name" :rules="[notEmpty, isAfterDate]"
option-value="id" />
map-options <q-select
clearable v-model="job.type"
:rules="[notEmpty]" filled
/> use-input
<q-input label="Dienstart"
v-model="job.required_services" input-debounce="0"
filled class="col-xs-12 col-sm-6 q-pa-sm"
class="col-xs-12 col-sm-6 q-pa-sm" :options="jobtypes"
label="Dienstanzahl" option-label="name"
type="number" option-value="id"
:rules="[notEmpty]" map-options
/> clearable
<q-input :rules="[notEmpty]"
v-model="job.comment" />
class="col-12 q-pa-sm" <q-input
label="Beschreibung" v-model="job.required_services"
type="textarea" filled
filled class="col-xs-12 col-sm-6 q-pa-sm"
/> label="Dienstanzahl"
type="number"
:rules="[notEmpty]"
/>
<q-input
v-model="job.comment"
class="col-12 q-pa-sm"
label="Kommentar"
type="textarea"
filled
/>
</q-form>
</q-card-section> </q-card-section>
<q-card-actions> <q-card-actions>
<q-btn label="Schicht löschen" color="negative" :disabled="canDelete" @click="removeJob" /> <q-btn label="Schicht löschen" color="negative" :disabled="canDelete" @click="$emit('remove-job')" />
</q-card-actions> </q-card-actions>
</q-card> </q-card>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, computed, onBeforeMount, PropType } from 'vue'; import { defineComponent, computed, onBeforeMount, ref, PropType } from 'vue';
import { IsoDateInput } from '@flaschengeist/api/components'; import { IsoDateInput } from '@flaschengeist/api/components';
import { notEmpty } from '@flaschengeist/api'; import { formatStartEnd, notEmpty } from '@flaschengeist/api';
import { useEventStore } from '../../store'; import { useEventStore } from '../../store';
import { QForm } from 'quasar';
export default defineComponent({ export default defineComponent({
name: 'JobSlot', name: 'JobSlot',
components: { IsoDateInput }, components: { IsoDateInput },
props: { props: {
active: {
type: Boolean,
required: true,
},
modelValue: { modelValue: {
required: true, required: true,
type: Object as PropType<FG.Job>, type: Object as PropType<FG.Job>,
@ -71,16 +87,25 @@ export default defineComponent({
}, },
}, },
emits: { emits: {
activate: () => true,
'remove-job': () => true, 'remove-job': () => true,
'update:modelValue': (job: FG.Job) => !!job, 'update:modelValue': (job: FG.Job) => !!job,
}, },
setup(props, { emit }) { setup(props, { emit, expose }) {
const store = useEventStore(); const store = useEventStore();
onBeforeMount(() => store.getJobTypes()); onBeforeMount(() => store.getJobTypes());
const form = ref<QForm>();
const jobtypes = computed(() => store.jobTypes); const jobtypes = computed(() => store.jobTypes);
const typeName = computed(() =>
typeof props.modelValue.type === 'object'
? props.modelValue.type.name
: jobtypes.value.find((j) => j.id === props.modelValue.type)?.name || 'Kein Typ gesetzt!'
);
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') {
@ -96,23 +121,26 @@ export default defineComponent({
}, },
}); });
function removeJob() {
emit('remove-job');
}
function isAfterDate(val: Date) { function isAfterDate(val: Date) {
return props.modelValue.start < val || 'Ende muss hinter dem Start liegen'; return props.modelValue.start < val || 'Ende muss hinter dem Start liegen';
} }
expose({
validate: () => form.value?.validate() || Promise.resolve(true)
});
return { return {
form,
formatStartEnd,
isAfterDate,
job, job,
jobtypes, jobtypes,
removeJob,
notEmpty, notEmpty,
isAfterDate, typeName,
}; };
}, },
}); });
</script> </script>
<style></style> <style></style>

View File

@ -31,7 +31,7 @@ export const innerRoutes: FG_Plugin.MenuRoute[] = [
path: 'schedule-management', path: 'schedule-management',
name: 'schedule-management', name: 'schedule-management',
component: () => import('../pages/EventManagement.vue'), component: () => import('../pages/EventManagement.vue'),
props: (route) => ({date: route.query.date}), props: (route) => ({ date: route.query.date }),
}, },
}, },
{ {
@ -52,7 +52,7 @@ export const privateRoutes: FG_Plugin.NamedRouteRecordRaw[] = [
{ {
name: 'new-event', name: 'new-event',
path: 'new-event', path: 'new-event',
redirect: {name: 'schedule-management'} redirect: { name: 'schedule-management' },
}, },
{ {
name: 'events-edit', name: 'events-edit',

View File

@ -46,16 +46,14 @@ export const useEventStore = defineStore({
removeJobType(id: number) { removeJobType(id: number) {
return api return api
.delete(`/events/job-types/${id}`) .delete(`/events/job-types/${id}`)
.then(() => this.jobTypes = this.jobTypes.filter(v => v.id !== id)); .then(() => (this.jobTypes = this.jobTypes.filter((v) => v.id !== id)));
}, },
renameJobType(id: number, newName: string) { renameJobType(id: number, newName: string) {
return api return api.put(`/events/job-types/${id}`, { name: newName }).then(() => {
.put(`/events/job-types/${id}`, { name: newName }) const idx = this.jobTypes.findIndex((v) => v.id === id);
.then(() => { if (idx >= 0) this.jobTypes[idx].name = newName;
const idx = this.jobTypes.findIndex(v=>v.id===id); });
if (idx >= 0) this.jobTypes[idx].name = newName;
})
}, },
async getEventTypes(force = false) { async getEventTypes(force = false) {
@ -76,7 +74,7 @@ export const useEventStore = defineStore({
addEventType(name: string) { addEventType(name: string) {
return api return api
.post<FG.EventType>('/events/event-types', { name: name }) .post<FG.EventType>('/events/event-types', { name: name })
.then(({data}) => this.eventTypes.push(data)) .then(({ data }) => this.eventTypes.push(data));
}, },
async removeEvent(id: number) { async removeEvent(id: number) {
@ -85,7 +83,7 @@ export const useEventStore = defineStore({
const idx = this.templates.findIndex((v) => v.id === id); const idx = this.templates.findIndex((v) => v.id === id);
if (idx !== -1) this.templates.splice(idx, 1); if (idx !== -1) this.templates.splice(idx, 1);
} catch (e) { } catch (e) {
if (isAxiosError(e, 404)) return false if (isAxiosError(e, 404)) return false;
throw e; throw e;
} }
return true; return true;
@ -94,16 +92,14 @@ export const useEventStore = defineStore({
removeEventType(id: number) { removeEventType(id: number) {
return api return api
.delete(`/events/event-types/${id}`) .delete(`/events/event-types/${id}`)
.then(() => this.eventTypes = this.eventTypes.filter(v => v.id !== id)); .then(() => (this.eventTypes = this.eventTypes.filter((v) => v.id !== id)));
}, },
renameEventType(id: number, newName: string) { renameEventType(id: number, newName: string) {
return api return api.put(`/events/event-types/${id}`, { name: newName }).then(() => {
.put(`/events/event-types/${id}`, { name: newName }) const idx = this.eventTypes.findIndex((v) => v.id === id);
.then(() => { if (idx >= 0) this.eventTypes[idx].name = newName;
const idx = this.eventTypes.findIndex(v=>v.id===id); });
if (idx >= 0) this.eventTypes[idx].name = newName;
})
}, },
async getTemplates(force = false) { async getTemplates(force = false) {
@ -115,7 +111,11 @@ export const useEventStore = defineStore({
return this.templates; return this.templates;
}, },
async getEvents(filter: { from?: Date; to?: Date, limit?: number, offset?: number, descending?: boolean } | undefined = undefined) { async getEvents(
filter:
| { from?: Date; to?: Date; limit?: number; offset?: number; descending?: boolean }
| undefined = undefined
) {
try { try {
const { data } = await api.get<FG.Event[]>('/events', { params: filter }); const { data } = await api.get<FG.Event[]>('/events', { params: filter });
data.forEach((element) => fixEvent(element)); data.forEach((element) => fixEvent(element));
@ -140,6 +140,7 @@ export const useEventStore = defineStore({
}, },
async addEvent(event: EditableEvent) { async addEvent(event: EditableEvent) {
console.log('addEvent', event);
if (event?.id === undefined) { if (event?.id === undefined) {
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);
@ -147,7 +148,10 @@ export const useEventStore = defineStore({
} else { } else {
if (typeof event.type === 'object') event.type = event.type.id; if (typeof event.type === 'object') event.type = event.type.id;
const { data } = await api.put<FG.Event>(`/events/${event.id}`, Object.assign(event, {jobs: undefined})); const { data } = await api.put<FG.Event>(
`/events/${event.id}`,
Object.assign(event, { jobs: undefined })
);
if (data.is_template) this.templates.push(data); if (data.is_template) this.templates.push(data);
return data; return data;
} }
@ -157,8 +161,8 @@ export const useEventStore = defineStore({
return api.post<FG.Event>('/events/transfer', { return api.post<FG.Event>('/events/transfer', {
job: job.id, job: job.id,
receiver: invitees, receiver: invitees,
is_invite: isInvite is_invite: isInvite,
}); });
} },
}, },
}); });