[core] Moved source from main flaschengeist tree
This commit is contained in:
parent
3a0b9a770e
commit
1a63f350a2
|
@ -0,0 +1,75 @@
|
||||||
|
const { resolve } = require('path');
|
||||||
|
module.exports = {
|
||||||
|
// https://eslint.org/docs/user-guide/configuring#configuration-cascading-and-hierarchy
|
||||||
|
// This option interrupts the configuration hierarchy at this file
|
||||||
|
// Remove this if you have an higher level ESLint config file (it usually happens into a monorepos)
|
||||||
|
root: true,
|
||||||
|
|
||||||
|
// https://eslint.vuejs.org/user-guide/#how-to-use-custom-parser
|
||||||
|
// Must use parserOptions instead of "parser" to allow vue-eslint-parser to keep working
|
||||||
|
// `parser: 'vue-eslint-parser'` is already included with any 'plugin:vue/**' config and should be omitted
|
||||||
|
parserOptions: {
|
||||||
|
// https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/parser#configuration
|
||||||
|
// https://github.com/TypeStrong/fork-ts-checker-webpack-plugin#eslint
|
||||||
|
// Needed to make the parser take into account 'vue' files
|
||||||
|
extraFileExtensions: ['.vue'],
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
project: resolve(__dirname, './tsconfig.json'),
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
|
ecmaVersion: 2019, // Allows for the parsing of modern ECMAScript features
|
||||||
|
sourceType: 'module' // Allows for the use of imports
|
||||||
|
},
|
||||||
|
|
||||||
|
env: {
|
||||||
|
browser: true
|
||||||
|
},
|
||||||
|
|
||||||
|
// Rules order is important, please avoid shuffling them
|
||||||
|
extends: [
|
||||||
|
// Base ESLint recommended rules
|
||||||
|
// 'eslint:recommended',
|
||||||
|
|
||||||
|
// https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/eslint-plugin#usage
|
||||||
|
// ESLint typescript rules
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
// consider disabling this class of rules if linting takes too long
|
||||||
|
'plugin:@typescript-eslint/recommended-requiring-type-checking',
|
||||||
|
|
||||||
|
// Uncomment any of the lines below to choose desired strictness,
|
||||||
|
// but leave only one uncommented!
|
||||||
|
// See https://eslint.vuejs.org/rules/#available-rules
|
||||||
|
// 'plugin:vue/vue3-essential', // Priority A: Essential (Error Prevention)
|
||||||
|
// 'plugin:vue/vue3-strongly-recommended', // Priority B: Strongly Recommended (Improving Readability)
|
||||||
|
'plugin:vue/vue3-recommended', // Priority C: Recommended (Minimizing Arbitrary Choices and Cognitive Overhead)
|
||||||
|
|
||||||
|
// https://github.com/prettier/eslint-config-prettier#installation
|
||||||
|
// usage with Prettier, provided by 'eslint-config-prettier'.
|
||||||
|
'prettier', //'plugin:prettier/recommended'
|
||||||
|
],
|
||||||
|
|
||||||
|
plugins: [
|
||||||
|
// required to apply rules which need type information
|
||||||
|
'@typescript-eslint',
|
||||||
|
|
||||||
|
// https://eslint.vuejs.org/user-guide/#why-doesn-t-it-work-on-vue-file
|
||||||
|
// required to lint *.vue files
|
||||||
|
'vue',
|
||||||
|
|
||||||
|
// https://github.com/typescript-eslint/typescript-eslint/issues/389#issuecomment-509292674
|
||||||
|
// Prettier has not been included as plugin to avoid performance impact
|
||||||
|
// add it as an extension for your IDE
|
||||||
|
],
|
||||||
|
|
||||||
|
// add your custom rules here
|
||||||
|
rules: {
|
||||||
|
'prefer-promise-reject-errors': 'off',
|
||||||
|
|
||||||
|
// TypeScript
|
||||||
|
quotes: ['warn', 'single', { avoidEscape: true }],
|
||||||
|
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||||
|
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||||
|
|
||||||
|
// allow debugger during development only
|
||||||
|
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
{
|
||||||
|
"private": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"version": "1.0.0-alpha.1",
|
||||||
|
"name": "@flaschengeist/schedule",
|
||||||
|
"author": "Ferdinand <rpm@fthiessen.de>",
|
||||||
|
"homepage": "https://flaschengeist.dev/Flaschengeist",
|
||||||
|
"description": "Flaschengeist schedule plugin",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://flaschengeist.dev/Flaschengeist/flaschengeist/issues"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://flaschengeist.dev/Flaschengeist/flaschengeist-schedule"
|
||||||
|
},
|
||||||
|
"main": "src/index.ts",
|
||||||
|
"scripts": {
|
||||||
|
"valid": "tsc --noEmit",
|
||||||
|
"pretty": "prettier --config ./package.json --write '{,!(node_modules)/**/}*.ts'",
|
||||||
|
"lint": "eslint --ext .js,.ts,.vue ./src"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@quasar/quasar-ui-qcalendar": "^4.0.0-alpha.8"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@flaschengeist/api": "file:../flaschengeist-frontend/api",
|
||||||
|
"@flaschengeist/types": "git+https://flaschengeist.dev/ferfissimo/flaschengeist-types.git#develop",
|
||||||
|
"@quasar/app": "^3.0.0-beta.26",
|
||||||
|
"quasar": "^2.0.0-beta.18",
|
||||||
|
"axios": "^0.21.1",
|
||||||
|
"prettier": "^2.3.0",
|
||||||
|
"typescript": "^4.2.4",
|
||||||
|
"pinia": "^2.0.0-alpha.18",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^4.24.0",
|
||||||
|
"@typescript-eslint/parser": "^4.24.0",
|
||||||
|
"eslint": "^7.26.0",
|
||||||
|
"eslint-config-prettier": "^8.3.0",
|
||||||
|
"eslint-plugin-vue": "^7.9.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@flaschengeist/api": "^1.0.0-alpha.1",
|
||||||
|
"@flaschengeist/users": "^1.0.0-alpha.1"
|
||||||
|
},
|
||||||
|
"prettier": {
|
||||||
|
"singleQuote": true,
|
||||||
|
"semi": true,
|
||||||
|
"printWidth": 100,
|
||||||
|
"arrowParens": "always"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
<template>
|
||||||
|
<q-card class="row justify-center content-center" style="text-align: center">
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6 col-12">Dienste diesen Monat: {{ jobs }}</div>
|
||||||
|
<!--TODO: Filters are deprecated! -->
|
||||||
|
<!--<div class="text-h6 col-12">Nächster Dienst: {{ nextJob | date }}</div>-->
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'DummyWidget',
|
||||||
|
setup() {
|
||||||
|
function randomNumber(start: number, end: number) {
|
||||||
|
return start + Math.floor(Math.random() * Math.floor(end));
|
||||||
|
}
|
||||||
|
const jobs = randomNumber(0, 5);
|
||||||
|
const nextJob = new Date(2021, randomNumber(1, 12), randomNumber(1, 31));
|
||||||
|
return { jobs, nextJob };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -0,0 +1,245 @@
|
||||||
|
<template>
|
||||||
|
<q-card>
|
||||||
|
<q-form @submit="save()" @reset="reset">
|
||||||
|
<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">
|
||||||
|
Veranstaltung <template v-if="modelValue">bearbeiten</template
|
||||||
|
><template v-else>erstellen</template>
|
||||||
|
</div>
|
||||||
|
<q-select
|
||||||
|
:model-value="template"
|
||||||
|
filled
|
||||||
|
label="Vorlage"
|
||||||
|
class="col-xs-12 col-sm-6 q-pa-sm"
|
||||||
|
:options="templates"
|
||||||
|
option-label="name"
|
||||||
|
map-options
|
||||||
|
clearable
|
||||||
|
:disable="templates.length == 0"
|
||||||
|
@update:modelValue="fromTemplate"
|
||||||
|
@clear="reset()"
|
||||||
|
/>
|
||||||
|
<q-input
|
||||||
|
v-model="event.name"
|
||||||
|
class="col-xs-12 col-sm-6 q-pa-sm"
|
||||||
|
label="Name"
|
||||||
|
type="text"
|
||||||
|
filled
|
||||||
|
/>
|
||||||
|
<q-select
|
||||||
|
v-model="event.type"
|
||||||
|
filled
|
||||||
|
use-input
|
||||||
|
label="Veranstaltungstyp"
|
||||||
|
input-debounce="0"
|
||||||
|
class="col-xs-12 col-sm-6 q-pa-sm"
|
||||||
|
:options="eventtypes"
|
||||||
|
option-label="name"
|
||||||
|
option-value="id"
|
||||||
|
emit-value
|
||||||
|
map-options
|
||||||
|
clearable
|
||||||
|
:rules="[notEmpty]"
|
||||||
|
/>
|
||||||
|
<IsoDateInput
|
||||||
|
v-model="event.start"
|
||||||
|
class="col-xs-12 col-sm-6 q-pa-sm"
|
||||||
|
label="Veranstaltungsbeginn"
|
||||||
|
:rules="[notEmpty]"
|
||||||
|
/>
|
||||||
|
<IsoDateInput
|
||||||
|
v-model="event.end"
|
||||||
|
class="col-xs-12 col-sm-6 q-pa-sm"
|
||||||
|
label="Veranstaltungsende"
|
||||||
|
/>
|
||||||
|
<q-input
|
||||||
|
v-model="event.description"
|
||||||
|
class="col-12 q-pa-sm"
|
||||||
|
label="Beschreibung"
|
||||||
|
type="textarea"
|
||||||
|
filled
|
||||||
|
/>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section v-if="event.template_id === undefined && modelValue === undefined">
|
||||||
|
<q-btn-toggle
|
||||||
|
v-model="recurrent"
|
||||||
|
spread
|
||||||
|
no-caps
|
||||||
|
:options="[
|
||||||
|
{ label: 'Einmalig', value: false },
|
||||||
|
{ label: 'Wiederkehrend', value: true },
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
<RecurrenceRule v-if="!!recurrent" v-model="recurrenceRule" />
|
||||||
|
</q-card-section>
|
||||||
|
<q-separator />
|
||||||
|
<q-card-section>
|
||||||
|
<q-btn color="primary" label="Schicht hinzufügen" @click="addJob()" />
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section v-for="(job, index) in event.jobs" :key="index">
|
||||||
|
<q-card class="q-my-auto">
|
||||||
|
<job
|
||||||
|
v-model="event.jobs[index]"
|
||||||
|
:job-can-delete="jobDeleteDisabled"
|
||||||
|
@remove-job="removeJob(index)"
|
||||||
|
/>
|
||||||
|
</q-card>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-actions align="around">
|
||||||
|
<q-card-actions align="left">
|
||||||
|
<q-btn v-if="!template" color="secondary" label="Neue Vorlage" @click="save(true)" />
|
||||||
|
<q-btn v-else color="negative" label="Vorlage löschen" @click="removeTemplate" />
|
||||||
|
</q-card-actions>
|
||||||
|
<q-card-actions align="right">
|
||||||
|
<q-btn label="Zurücksetzen" type="reset" />
|
||||||
|
<q-btn color="primary" type="submit" label="Speichern" />
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card-actions>
|
||||||
|
</q-form>
|
||||||
|
</q-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { computed, defineComponent, PropType, ref, onBeforeMount } from 'vue';
|
||||||
|
import { date, ModifyDateOptions } from 'quasar';
|
||||||
|
import { useScheduleStore } from '../../store';
|
||||||
|
import { notEmpty } from '@flaschengeist/api';
|
||||||
|
import IsoDateInput from 'src/components/utils/IsoDateInput.vue';
|
||||||
|
import Job from './Job.vue';
|
||||||
|
import RecurrenceRule from './RecurrenceRule.vue';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'EditEvent',
|
||||||
|
components: { IsoDateInput, Job, RecurrenceRule },
|
||||||
|
props: {
|
||||||
|
modelValue: {
|
||||||
|
required: false,
|
||||||
|
default: () => undefined,
|
||||||
|
type: Object as PropType<FG.Event | undefined>,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: {
|
||||||
|
done: (val: boolean) => typeof val === 'boolean',
|
||||||
|
},
|
||||||
|
setup(props, { emit }) {
|
||||||
|
const store = useScheduleStore();
|
||||||
|
|
||||||
|
const emptyJob = {
|
||||||
|
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 jobDeleteDisabled = computed(() => event.value.jobs.length < 2);
|
||||||
|
const recurrent = ref(false);
|
||||||
|
const recurrenceRule = ref<FG.RecurrenceRule>({ frequency: 'daily', interval: 1 });
|
||||||
|
|
||||||
|
onBeforeMount(() => {
|
||||||
|
void store.getEventTypes();
|
||||||
|
void store.getJobTypes();
|
||||||
|
void store.getTemplates();
|
||||||
|
});
|
||||||
|
|
||||||
|
function addJob() {
|
||||||
|
event.value.jobs.push(Object.assign({}, emptyJob));
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeJob(index: number) {
|
||||||
|
event.value.jobs.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fromTemplate(tpl: FG.Event) {
|
||||||
|
template.value = tpl;
|
||||||
|
event.value = Object.assign({}, tpl);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function save(template = false) {
|
||||||
|
event.value.is_template = template;
|
||||||
|
try {
|
||||||
|
await store.addEvent(event.value);
|
||||||
|
if (props.modelValue === undefined && recurrent.value && !event.value.is_template) {
|
||||||
|
let count = 0;
|
||||||
|
const options: ModifyDateOptions = {};
|
||||||
|
switch (recurrenceRule.value.frequency) {
|
||||||
|
case 'daily':
|
||||||
|
options['days'] = 1 * recurrenceRule.value.interval;
|
||||||
|
break;
|
||||||
|
case 'weekly':
|
||||||
|
options['days'] = 7 * recurrenceRule.value.interval;
|
||||||
|
break;
|
||||||
|
case 'monthly':
|
||||||
|
options['months'] = 1 * recurrenceRule.value.interval;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
while (true) {
|
||||||
|
event.value.start = date.addToDate(event.value.start, options);
|
||||||
|
if (event.value.end) event.value.end = date.addToDate(event.value.end, options);
|
||||||
|
event.value.jobs.forEach((job) => {
|
||||||
|
job.start = date.addToDate(job.start, options);
|
||||||
|
if (job.end) job.end = date.addToDate(job.end, options);
|
||||||
|
});
|
||||||
|
count++;
|
||||||
|
if (
|
||||||
|
count <= 120 &&
|
||||||
|
(!recurrenceRule.value.count || count <= recurrenceRule.value.count) &&
|
||||||
|
(!recurrenceRule.value.until || event.value.start < recurrenceRule.value.until)
|
||||||
|
)
|
||||||
|
await store.addEvent(event.value);
|
||||||
|
else break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reset();
|
||||||
|
emit('done', true);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeTemplate() {
|
||||||
|
if (template.value !== undefined) {
|
||||||
|
await store.removeEvent(template.value.id);
|
||||||
|
template.value = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
event.value = Object.assign({}, props.modelValue || emptyEvent);
|
||||||
|
template.value = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
jobDeleteDisabled,
|
||||||
|
addJob,
|
||||||
|
eventtypes,
|
||||||
|
templates,
|
||||||
|
removeJob,
|
||||||
|
notEmpty,
|
||||||
|
save,
|
||||||
|
reset,
|
||||||
|
recurrent,
|
||||||
|
fromTemplate,
|
||||||
|
removeTemplate,
|
||||||
|
template,
|
||||||
|
recurrenceRule,
|
||||||
|
event,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style></style>
|
|
@ -0,0 +1,126 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<q-dialog v-model="edittype">
|
||||||
|
<q-card>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">Editere Diensttyp {{ actualEvent.name }}</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<q-input v-model="newEventName" dense label="name" filled />
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-actions>
|
||||||
|
<q-btn flat color="danger" label="Abbrechen" @click="discardChanges()" />
|
||||||
|
<q-btn flat color="primary" label="Speichern" @click="saveChanges()" />
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
|
||||||
|
<q-card>
|
||||||
|
<q-card-section>
|
||||||
|
<q-table title="Veranstaltungstypen" :rows="rows" row-key="jobid" :columns="columns">
|
||||||
|
<template #top-right>
|
||||||
|
<q-input v-model="newEventType" dense placeholder="Neuer Typ" />
|
||||||
|
|
||||||
|
<div></div>
|
||||||
|
<q-btn color="primary" icon="mdi-plus" label="Hinzufügen" @click="addType" />
|
||||||
|
</template>
|
||||||
|
<template #body-cell-actions="props">
|
||||||
|
<!-- <q-btn :label="item"> -->
|
||||||
|
<!-- {{ item.row.name }} -->
|
||||||
|
<q-td :props="props" align="right" :auto-width="true">
|
||||||
|
<q-btn
|
||||||
|
round
|
||||||
|
icon="mdi-pencil"
|
||||||
|
@click="editType({ id: props.row.id, name: props.row.name })"
|
||||||
|
/>
|
||||||
|
<q-btn round icon="mdi-delete" @click="deleteType(props.row.id)" />
|
||||||
|
</q-td>
|
||||||
|
</template>
|
||||||
|
</q-table>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, ref, computed, onBeforeMount } from 'vue';
|
||||||
|
import { useScheduleStore } from '../../store';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'EventTypes',
|
||||||
|
components: {},
|
||||||
|
setup() {
|
||||||
|
const store = useScheduleStore();
|
||||||
|
const newEventType = ref('');
|
||||||
|
const edittype = ref(false);
|
||||||
|
const emptyEvent: FG.EventType = { id: -1, name: '' };
|
||||||
|
const actualEvent = ref(emptyEvent);
|
||||||
|
const newEventName = ref('');
|
||||||
|
|
||||||
|
onBeforeMount(async () => await store.getEventTypes());
|
||||||
|
|
||||||
|
const rows = computed(() => store.eventTypes);
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
label: 'Veranstaltungstyp',
|
||||||
|
field: 'name',
|
||||||
|
align: 'left',
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'actions',
|
||||||
|
label: 'Aktionen',
|
||||||
|
field: 'actions',
|
||||||
|
align: 'right',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
async function addType() {
|
||||||
|
await store.addEventType(newEventType.value);
|
||||||
|
// if null then conflict with name
|
||||||
|
newEventType.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function editType(event: FG.EventType) {
|
||||||
|
edittype.value = true;
|
||||||
|
actualEvent.value = event;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveChanges() {
|
||||||
|
try {
|
||||||
|
await store.renameEventType(actualEvent.value.id, newEventName.value);
|
||||||
|
} finally {
|
||||||
|
discardChanges();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function discardChanges() {
|
||||||
|
actualEvent.value = emptyEvent;
|
||||||
|
newEventName.value = '';
|
||||||
|
edittype.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteType(id: number) {
|
||||||
|
await store.removeEventType(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
columns,
|
||||||
|
rows,
|
||||||
|
addType,
|
||||||
|
newEventType,
|
||||||
|
deleteType,
|
||||||
|
edittype,
|
||||||
|
editType,
|
||||||
|
actualEvent,
|
||||||
|
newEventName,
|
||||||
|
discardChanges,
|
||||||
|
saveChanges,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
|
@ -0,0 +1,113 @@
|
||||||
|
<template>
|
||||||
|
<q-card-section class="fit row justify-start content-center items-center">
|
||||||
|
<q-card-section class="fit row justify-start content-center items-center">
|
||||||
|
<IsoDateInput
|
||||||
|
v-model="job.start"
|
||||||
|
class="col-xs-12 col-sm-6 q-pa-sm"
|
||||||
|
label="Beginn"
|
||||||
|
type="datetime"
|
||||||
|
:rules="[notEmpty]"
|
||||||
|
/>
|
||||||
|
<IsoDateInput
|
||||||
|
v-model="job.end"
|
||||||
|
class="col-xs-12 col-sm-6 q-pa-sm"
|
||||||
|
label="Ende"
|
||||||
|
type="datetime"
|
||||||
|
:rules="[notEmpty, isAfterDate]"
|
||||||
|
/>
|
||||||
|
<q-select
|
||||||
|
v-model="job.type"
|
||||||
|
filled
|
||||||
|
use-input
|
||||||
|
label="Dienstart"
|
||||||
|
input-debounce="0"
|
||||||
|
class="col-xs-12 col-sm-6 q-pa-sm"
|
||||||
|
:options="jobtypes"
|
||||||
|
option-label="name"
|
||||||
|
option-value="id"
|
||||||
|
map-options
|
||||||
|
clearable
|
||||||
|
:rules="[notEmpty]"
|
||||||
|
/>
|
||||||
|
<q-input
|
||||||
|
v-model="job.required_services"
|
||||||
|
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="Beschreibung"
|
||||||
|
type="textarea"
|
||||||
|
filled
|
||||||
|
/>
|
||||||
|
</q-card-section>
|
||||||
|
<q-btn label="Schicht löschen" color="negative" :disabled="jobCanDelete" @click="removeJob" />
|
||||||
|
</q-card-section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, computed, onBeforeMount, PropType } from 'vue';
|
||||||
|
import { IsoDateInput } from '@flaschengeist/api/components';
|
||||||
|
import { notEmpty } from '@flaschengeist/api';
|
||||||
|
import { useScheduleStore } from '../../store';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'Job',
|
||||||
|
components: { IsoDateInput },
|
||||||
|
props: {
|
||||||
|
modelValue: {
|
||||||
|
required: true,
|
||||||
|
type: Object as PropType<FG.Job>,
|
||||||
|
},
|
||||||
|
jobCanDelete: Boolean,
|
||||||
|
},
|
||||||
|
emits: {
|
||||||
|
'remove-job': () => true,
|
||||||
|
'update:modelValue': (job: FG.Job) => !!job,
|
||||||
|
},
|
||||||
|
setup(props, { emit }) {
|
||||||
|
const store = useScheduleStore();
|
||||||
|
|
||||||
|
onBeforeMount(() => store.getJobTypes());
|
||||||
|
|
||||||
|
const jobtypes = computed(() => store.jobTypes);
|
||||||
|
|
||||||
|
const job = new Proxy(props.modelValue, {
|
||||||
|
get(target, prop) {
|
||||||
|
if (typeof prop === 'string') {
|
||||||
|
return ((props.modelValue as unknown) as Record<string, unknown>)[prop];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
set(obj, prop, value) {
|
||||||
|
if (typeof prop === 'string') {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
|
emit('update:modelValue', Object.assign({}, props.modelValue, { [prop]: value }));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function removeJob() {
|
||||||
|
emit('remove-job');
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAfterDate(val: string) {
|
||||||
|
return props.modelValue.start < new Date(val) || 'Ende muss hinter dem Start liegen';
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
job,
|
||||||
|
jobtypes,
|
||||||
|
removeJob,
|
||||||
|
notEmpty,
|
||||||
|
isAfterDate,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style></style>
|
|
@ -0,0 +1,125 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<q-dialog v-model="edittype">
|
||||||
|
<q-card>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">Editere Diensttyp {{ actualJob.name }}</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<q-input v-model="newJobName" dense label="name" filled />
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-actions>
|
||||||
|
<q-btn flat color="danger" label="Abbrechen" @click="discardChanges()" />
|
||||||
|
<q-btn flat color="primary" label="Speichern" @click="saveChanges()" />
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
|
||||||
|
<q-card>
|
||||||
|
<q-card-section>
|
||||||
|
<q-table title="Diensttypen" :rows="rows" row-key="jobid" :columns="columns">
|
||||||
|
<template #top-right>
|
||||||
|
<q-input v-model="newJob" dense placeholder="Neuer Typ" />
|
||||||
|
|
||||||
|
<div></div>
|
||||||
|
<q-btn color="primary" icon="mdi-plus" label="Hinzufügen" @click="addType" />
|
||||||
|
</template>
|
||||||
|
<template #body-cell-actions="props">
|
||||||
|
<!-- <q-btn :label="item"> -->
|
||||||
|
<!-- {{ item.row.name }} -->
|
||||||
|
<q-td :props="props" align="right" :auto-width="true">
|
||||||
|
<q-btn
|
||||||
|
round
|
||||||
|
icon="mdi-pencil"
|
||||||
|
@click="editType({ id: props.row.id, name: props.row.name })"
|
||||||
|
/>
|
||||||
|
<q-btn round icon="mdi-delete" @click="deleteType(props.row.id)" />
|
||||||
|
</q-td>
|
||||||
|
</template>
|
||||||
|
</q-table>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, ref, computed, onBeforeMount } from 'vue';
|
||||||
|
import { useScheduleStore } from '../../store';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'JobTypes',
|
||||||
|
components: {},
|
||||||
|
setup() {
|
||||||
|
const store = useScheduleStore();
|
||||||
|
const newJob = ref('');
|
||||||
|
const edittype = ref(false);
|
||||||
|
const emptyJob: FG.JobType = { id: -1, name: '' };
|
||||||
|
const actualJob = ref(emptyJob);
|
||||||
|
const newJobName = ref('');
|
||||||
|
|
||||||
|
onBeforeMount(() => store.getJobTypes());
|
||||||
|
|
||||||
|
const rows = computed(() => store.jobTypes);
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
name: 'jobname',
|
||||||
|
label: 'Name',
|
||||||
|
field: 'name',
|
||||||
|
align: 'left',
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'actions',
|
||||||
|
label: 'Aktionen',
|
||||||
|
field: 'actions',
|
||||||
|
align: 'right',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
async function addType() {
|
||||||
|
await store.addJobType(newJob.value);
|
||||||
|
newJob.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function editType(job: FG.JobType) {
|
||||||
|
edittype.value = true;
|
||||||
|
actualJob.value = job;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveChanges() {
|
||||||
|
try {
|
||||||
|
await store.renameJobType(actualJob.value.id, newJobName.value);
|
||||||
|
} finally {
|
||||||
|
discardChanges();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function discardChanges() {
|
||||||
|
actualJob.value = emptyJob;
|
||||||
|
newJobName.value = '';
|
||||||
|
edittype.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteType(id: number) {
|
||||||
|
void store.removeJobType(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
columns,
|
||||||
|
rows,
|
||||||
|
addType,
|
||||||
|
newJob,
|
||||||
|
deleteType,
|
||||||
|
edittype,
|
||||||
|
editType,
|
||||||
|
actualJob,
|
||||||
|
newJobName,
|
||||||
|
discardChanges,
|
||||||
|
saveChanges,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
|
@ -0,0 +1,96 @@
|
||||||
|
<template>
|
||||||
|
<q-card class="fit row justify-start content-center items-center">
|
||||||
|
<q-input
|
||||||
|
v-model="rule.interval"
|
||||||
|
filled
|
||||||
|
class="col-xs-12 col-sm-6 q-pa-sm"
|
||||||
|
label="Interval"
|
||||||
|
type="number"
|
||||||
|
:rules="[notEmpty]"
|
||||||
|
/>
|
||||||
|
<q-select
|
||||||
|
v-model="rule.frequency"
|
||||||
|
filled
|
||||||
|
label="Wiederholung"
|
||||||
|
input-debounce="200"
|
||||||
|
class="col-xs-12 col-sm-6 q-pa-sm"
|
||||||
|
:options="freqTypes"
|
||||||
|
emit-value
|
||||||
|
map-options
|
||||||
|
/>
|
||||||
|
<q-input
|
||||||
|
v-model="rule.count"
|
||||||
|
filled
|
||||||
|
class="col-xs-12 col-sm-6 q-pa-sm"
|
||||||
|
label="Anzahl Wiederholungen"
|
||||||
|
type="number"
|
||||||
|
/>
|
||||||
|
<IsoDateInput
|
||||||
|
v-model="rule.until"
|
||||||
|
class="col-xs-12 col-sm-6 q-pa-sm"
|
||||||
|
label="Wiederholen bis"
|
||||||
|
type="date"
|
||||||
|
/>
|
||||||
|
</q-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import IsoDateInput from 'src/components/utils/IsoDateInput.vue';
|
||||||
|
import { defineComponent, PropType } from 'vue';
|
||||||
|
import { notEmpty } from '@flaschengeist/api';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'RecurrenceRule',
|
||||||
|
components: { IsoDateInput },
|
||||||
|
props: {
|
||||||
|
modelValue: {
|
||||||
|
required: true,
|
||||||
|
type: Object as PropType<FG.RecurrenceRule>,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: {
|
||||||
|
'update:modelValue': (rule: FG.RecurrenceRule) => !!rule,
|
||||||
|
},
|
||||||
|
setup(props, { emit }) {
|
||||||
|
const freqTypes = [
|
||||||
|
{ label: 'Täglich', value: 'daily' },
|
||||||
|
{ label: 'Wöchentlich', value: 'weekly' },
|
||||||
|
{ label: 'Monatlich', value: 'monthly' },
|
||||||
|
{ label: 'Jährlich', value: 'yearly' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const rule = new Proxy(props.modelValue, {
|
||||||
|
get(target, prop) {
|
||||||
|
if (typeof prop === 'string') {
|
||||||
|
return ((props.modelValue as unknown) as Record<string, unknown>)[prop];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
set(target, prop, value) {
|
||||||
|
if (typeof prop === 'string') {
|
||||||
|
const obj = Object.assign({}, props.modelValue);
|
||||||
|
if (prop == 'frequency' && typeof value === 'string') obj.frequency = value;
|
||||||
|
else if (prop == 'interval') {
|
||||||
|
obj.interval = typeof value === 'string' ? parseInt(value) : <number>value;
|
||||||
|
} else if (prop == 'count') {
|
||||||
|
obj.until = undefined;
|
||||||
|
obj.count = typeof value === 'string' ? parseInt(value) : <number>value;
|
||||||
|
} else if (prop == 'until' && (value instanceof Date || value === undefined)) {
|
||||||
|
obj.count = undefined;
|
||||||
|
obj.until = <Date | undefined>value;
|
||||||
|
} else return false;
|
||||||
|
emit('update:modelValue', obj);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
rule,
|
||||||
|
notEmpty,
|
||||||
|
freqTypes,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style></style>
|
|
@ -0,0 +1,241 @@
|
||||||
|
<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 '@flaschengeist/api';
|
||||||
|
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>
|
|
@ -0,0 +1,99 @@
|
||||||
|
<template>
|
||||||
|
<q-card
|
||||||
|
class="q-mx-xs q-mt-sm justify-start content-center items-center rounded-borders shadow-5"
|
||||||
|
bordered
|
||||||
|
>
|
||||||
|
<q-card-section class="text-primary q-pa-xs">
|
||||||
|
<div class="text-weight-bolder text-center" style="font-size: 1.5vw">
|
||||||
|
{{ event.type.name }}
|
||||||
|
<template v-if="event.name"
|
||||||
|
>: <span style="font-size: 1.2vw">{{ event.name }}</span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div v-if="event.description" class="text-weight-medium" style="font-size: 1vw">
|
||||||
|
{{ event.description }}
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-separator />
|
||||||
|
<q-card-section class="q-pa-xs">
|
||||||
|
<!-- Jobs -->
|
||||||
|
<JobSlot
|
||||||
|
v-for="(job, index) in event.jobs"
|
||||||
|
:key="index"
|
||||||
|
v-model="event.jobs[index]"
|
||||||
|
class="col q-my-xs"
|
||||||
|
:event-id="event.id"
|
||||||
|
/>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-actions v-if="canEdit || canDelete" vertical align="center">
|
||||||
|
<q-btn
|
||||||
|
v-if="canEdit"
|
||||||
|
color="secondary"
|
||||||
|
flat
|
||||||
|
label="Bearbeiten"
|
||||||
|
style="min-width: 95%"
|
||||||
|
@click="edit"
|
||||||
|
/>
|
||||||
|
<q-btn
|
||||||
|
v-if="canDelete"
|
||||||
|
color="negative"
|
||||||
|
flat
|
||||||
|
label="Löschen"
|
||||||
|
style="min-width: 95%"
|
||||||
|
@click="remove"
|
||||||
|
/>
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, computed, PropType } from 'vue';
|
||||||
|
import { hasPermission } from '@flaschengeist/api';
|
||||||
|
import { PERMISSIONS } from '../../../permissions';
|
||||||
|
import JobSlot from './JobSlot.vue';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'Eventslot',
|
||||||
|
components: { JobSlot },
|
||||||
|
props: {
|
||||||
|
modelValue: {
|
||||||
|
required: true,
|
||||||
|
type: Object as PropType<FG.Event>,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: {
|
||||||
|
'update:modelValue': (val: FG.Event) => !!val,
|
||||||
|
removeEvent: (val: number) => typeof val === 'number',
|
||||||
|
editEvent: (val: number) => !!val,
|
||||||
|
},
|
||||||
|
setup(props, { emit }) {
|
||||||
|
const canDelete = computed(() => hasPermission(PERMISSIONS.DELETE));
|
||||||
|
const canEdit = computed(
|
||||||
|
() =>
|
||||||
|
hasPermission(PERMISSIONS.EDIT) &&
|
||||||
|
(props.modelValue?.end || props.modelValue.start) > new Date()
|
||||||
|
);
|
||||||
|
const event = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (v) => emit('update:modelValue', v),
|
||||||
|
});
|
||||||
|
|
||||||
|
function remove() {
|
||||||
|
emit('removeEvent', props.modelValue.id);
|
||||||
|
}
|
||||||
|
function edit() {
|
||||||
|
emit('editEvent', props.modelValue.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
canDelete,
|
||||||
|
canEdit,
|
||||||
|
edit,
|
||||||
|
event,
|
||||||
|
remove,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
|
@ -0,0 +1,140 @@
|
||||||
|
<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, useMainStore, useUserStore } from '@flaschengeist/api';
|
||||||
|
import { useScheduleStore } from '../../../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>
|
|
@ -0,0 +1,9 @@
|
||||||
|
declare namespace FG {
|
||||||
|
export interface RecurrenceRule {
|
||||||
|
frequency: string;
|
||||||
|
interval: number;
|
||||||
|
count?: number;
|
||||||
|
until?: Date;
|
||||||
|
weekdays?: Array<number>;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { innerRoutes, privateRoutes } from './routes';
|
||||||
|
import { FG_Plugin } from '@flaschengeist/types';
|
||||||
|
import { defineAsyncComponent } from 'vue';
|
||||||
|
|
||||||
|
const plugin: FG_Plugin.Plugin = {
|
||||||
|
id: 'schedule',
|
||||||
|
name: 'Schedule',
|
||||||
|
innerRoutes,
|
||||||
|
internalRoutes: privateRoutes,
|
||||||
|
requiredModules: [['events']],
|
||||||
|
version: '0.0.1',
|
||||||
|
widgets: [
|
||||||
|
{
|
||||||
|
priority: 0,
|
||||||
|
name: 'stats',
|
||||||
|
permissions: [],
|
||||||
|
widget: defineAsyncComponent(() => import('./components/Widget.vue')),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default plugin;
|
|
@ -0,0 +1,29 @@
|
||||||
|
<template>
|
||||||
|
<q-page padding class="fit row justify-center content-start items-start q-gutter-sm">
|
||||||
|
<EditEvent v-model="event" />
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { onBeforeMount, defineComponent, ref } from 'vue';
|
||||||
|
import EditEvent from '../components/management/EditEvent.vue';
|
||||||
|
import { useScheduleStore } from '../store';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: { EditEvent },
|
||||||
|
setup() {
|
||||||
|
const route = useRoute();
|
||||||
|
const store = useScheduleStore();
|
||||||
|
const event = ref<FG.Event | undefined>(undefined);
|
||||||
|
onBeforeMount(async () => {
|
||||||
|
if ('id' in route.params && typeof route.params.id === 'string')
|
||||||
|
event.value = await store.getEvent(parseInt(route.params.id));
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
event,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -0,0 +1,95 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<q-tabs v-if="$q.screen.gt.sm" v-model="tab">
|
||||||
|
<q-tab
|
||||||
|
v-for="(tabindex, index) in tabs"
|
||||||
|
:key="'tab' + index"
|
||||||
|
:name="tabindex.name"
|
||||||
|
:label="tabindex.label"
|
||||||
|
/>
|
||||||
|
</q-tabs>
|
||||||
|
<div v-else class="fit row justify-end">
|
||||||
|
<q-btn flat round icon="mdi-menu" @click="showDrawer = !showDrawer" />
|
||||||
|
</div>
|
||||||
|
<q-drawer v-model="showDrawer" side="right" behavior="mobile" @click="showDrawer = !showDrawer">
|
||||||
|
<q-list v-model="tab">
|
||||||
|
<q-item
|
||||||
|
v-for="(tabindex, index) in tabs"
|
||||||
|
:key="'tab' + index"
|
||||||
|
:active="tab == tabindex.name"
|
||||||
|
clickable
|
||||||
|
@click="tab = tabindex.name"
|
||||||
|
>
|
||||||
|
<q-item-label>{{ tabindex.label }}</q-item-label>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</q-drawer>
|
||||||
|
<q-page padding class="fit row justify-center content-start items-start q-gutter-sm">
|
||||||
|
<q-tab-panels
|
||||||
|
v-model="tab"
|
||||||
|
style="background-color: transparent"
|
||||||
|
class="q-ma-none q-pa-none fit row justify-center content-start items-start"
|
||||||
|
animated
|
||||||
|
>
|
||||||
|
<q-tab-panel name="create">
|
||||||
|
<EditEvent />
|
||||||
|
</q-tab-panel>
|
||||||
|
<q-tab-panel name="eventtypes">
|
||||||
|
<EventTypes />
|
||||||
|
</q-tab-panel>
|
||||||
|
<q-tab-panel name="jobtypes">
|
||||||
|
<JobTypes v-if="canEditJobTypes" />
|
||||||
|
</q-tab-panel>
|
||||||
|
</q-tab-panels>
|
||||||
|
</q-page>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { computed, defineComponent, ref } from 'vue';
|
||||||
|
import EventTypes from '../components/management/EventTypes.vue';
|
||||||
|
import JobTypes from '../components/management/JobTypes.vue';
|
||||||
|
import EditEvent from '../components/management/EditEvent.vue';
|
||||||
|
import { hasPermission } from '@flaschengeist/api';
|
||||||
|
import { PERMISSIONS } from '../permissions';
|
||||||
|
import { Screen } from 'quasar';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'EventManagement',
|
||||||
|
components: { EditEvent, EventTypes, JobTypes },
|
||||||
|
setup() {
|
||||||
|
const canEditJobTypes = computed(() => hasPermission(PERMISSIONS.JOB_TYPE));
|
||||||
|
|
||||||
|
interface Tab {
|
||||||
|
name: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tabs: Tab[] = [
|
||||||
|
{ name: 'create', label: 'Veranstaltungen' },
|
||||||
|
{ name: 'eventtypes', label: 'Veranstaltungsarten' },
|
||||||
|
{ name: 'jobtypes', label: 'Dienstarten' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const drawer = ref<boolean>(false);
|
||||||
|
|
||||||
|
const showDrawer = computed({
|
||||||
|
get: () => {
|
||||||
|
return !Screen.gt.sm && drawer.value;
|
||||||
|
},
|
||||||
|
set: (val: boolean) => {
|
||||||
|
drawer.value = val;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const tab = ref<string>('create');
|
||||||
|
|
||||||
|
return {
|
||||||
|
canEditJobTypes,
|
||||||
|
showDrawer,
|
||||||
|
tab,
|
||||||
|
tabs,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -0,0 +1,87 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<q-tabs v-if="$q.screen.gt.sm" v-model="tab">
|
||||||
|
<q-tab
|
||||||
|
v-for="(tabindex, index) in tabs"
|
||||||
|
:key="'tab' + index"
|
||||||
|
:name="tabindex.name"
|
||||||
|
:label="tabindex.label"
|
||||||
|
/>
|
||||||
|
</q-tabs>
|
||||||
|
<div v-else class="fit row justify-end">
|
||||||
|
<q-btn flat round icon="mdi-menu" @click="showDrawer = !showDrawer" />
|
||||||
|
</div>
|
||||||
|
<q-drawer v-model="showDrawer" side="right" behavior="mobile" @click="showDrawer = !showDrawer">
|
||||||
|
<q-list v-model="tab">
|
||||||
|
<q-item
|
||||||
|
v-for="(tabindex, index) in tabs"
|
||||||
|
:key="'tab' + index"
|
||||||
|
:active="tab == tabindex.name"
|
||||||
|
clickable
|
||||||
|
@click="tab = tabindex.name"
|
||||||
|
>
|
||||||
|
<q-item-label>{{ tabindex.label }}</q-item-label>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</q-drawer>
|
||||||
|
<q-page padding class="fit row justify-center content-start items-start q-gutter-sm">
|
||||||
|
<q-tab-panels
|
||||||
|
v-model="tab"
|
||||||
|
style="background-color: transparent"
|
||||||
|
class="q-ma-none q-pa-none fit row justify-center content-start items-start"
|
||||||
|
animated
|
||||||
|
>
|
||||||
|
<q-tab-panel name="agendaView">
|
||||||
|
<AgendaView />
|
||||||
|
</q-tab-panel>
|
||||||
|
<q-tab-panel name="eventtypes">
|
||||||
|
<EventTypes />
|
||||||
|
</q-tab-panel>
|
||||||
|
</q-tab-panels>
|
||||||
|
</q-page>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { computed, defineComponent, ref } from 'vue';
|
||||||
|
import EventTypes from '../components/management/EventTypes.vue';
|
||||||
|
//import CreateEvent from '../components/management/CreateEvent.vue';
|
||||||
|
import AgendaView from '../components/overview/AgendaView.vue';
|
||||||
|
import { Screen } from 'quasar';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'EventOverview',
|
||||||
|
components: { AgendaView, EventTypes },
|
||||||
|
setup() {
|
||||||
|
interface Tab {
|
||||||
|
name: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tabs: Tab[] = [
|
||||||
|
{ name: 'agendaView', label: 'Kalendar' },
|
||||||
|
// { name: 'eventtypes', label: 'Veranstaltungsarten' },
|
||||||
|
// { name: 'jobtypes', label: 'Dienstarten' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const drawer = ref<boolean>(false);
|
||||||
|
|
||||||
|
const showDrawer = computed({
|
||||||
|
get: () => {
|
||||||
|
return !Screen.gt.sm && drawer.value;
|
||||||
|
},
|
||||||
|
set: (val: boolean) => {
|
||||||
|
drawer.value = val;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const tab = ref<string>('agendaView');
|
||||||
|
|
||||||
|
return {
|
||||||
|
showDrawer,
|
||||||
|
tab,
|
||||||
|
tabs,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -0,0 +1,8 @@
|
||||||
|
<template>
|
||||||
|
<q-page padding>
|
||||||
|
<q-card>
|
||||||
|
<q-card-section class="row"> </q-card-section>
|
||||||
|
<q-card-section> </q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</q-page>
|
||||||
|
</template>
|
|
@ -0,0 +1,16 @@
|
||||||
|
export const PERMISSIONS = {
|
||||||
|
// Can create events
|
||||||
|
CREATE: 'events_create',
|
||||||
|
// Can edit events
|
||||||
|
EDIT: 'events_edit',
|
||||||
|
// Can delete events
|
||||||
|
DELETE: 'events_delete',
|
||||||
|
// Can create and edit EventTypes
|
||||||
|
EVENT_TYPE: 'events_event_type',
|
||||||
|
// Can create and edit JobTypes
|
||||||
|
JOB_TYPE: 'events_job_type',
|
||||||
|
// Can self assign to jobs
|
||||||
|
ASSIGN: 'events_assign',
|
||||||
|
// Can assign other users to jobs
|
||||||
|
ASSIGN_OTHER: 'events_assign_other',
|
||||||
|
};
|
|
@ -0,0 +1,56 @@
|
||||||
|
import { FG_Plugin } from '@flaschengeist/types';
|
||||||
|
import { PERMISSIONS } from '../permissions';
|
||||||
|
|
||||||
|
export const innerRoutes: FG_Plugin.MenuRoute[] = [
|
||||||
|
{
|
||||||
|
title: 'Dienste',
|
||||||
|
icon: 'mdi-briefcase',
|
||||||
|
permissions: ['user'],
|
||||||
|
route: {
|
||||||
|
path: 'schedule',
|
||||||
|
name: 'schedule',
|
||||||
|
redirect: { name: 'schedule-overview' },
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
title: 'Dienstübersicht',
|
||||||
|
icon: 'mdi-account-group',
|
||||||
|
shortcut: true,
|
||||||
|
route: {
|
||||||
|
path: 'schedule-overview',
|
||||||
|
name: 'schedule-overview',
|
||||||
|
component: () => import('../pages/Overview.vue'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Dienstverwaltung',
|
||||||
|
icon: 'mdi-account-details',
|
||||||
|
shortcut: false,
|
||||||
|
permissions: [PERMISSIONS.CREATE],
|
||||||
|
route: {
|
||||||
|
path: 'schedule-management',
|
||||||
|
name: 'schedule-management',
|
||||||
|
component: () => import('../pages/Management.vue'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Dienstanfragen',
|
||||||
|
icon: 'mdi-account-switch',
|
||||||
|
shortcut: false,
|
||||||
|
route: {
|
||||||
|
path: 'schedule-requests',
|
||||||
|
name: 'schedule-requests',
|
||||||
|
component: () => import('../pages/Requests.vue'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const privateRoutes: FG_Plugin.NamedRouteRecordRaw[] = [
|
||||||
|
{
|
||||||
|
name: 'events-edit',
|
||||||
|
path: 'schedule/:id/edit',
|
||||||
|
component: () => import('../pages/Event.vue'),
|
||||||
|
},
|
||||||
|
];
|
|
@ -0,0 +1,6 @@
|
||||||
|
//https://github.com/vuejs/vue-next/issues/3130
|
||||||
|
declare module '*.vue' {
|
||||||
|
import { ComponentOptions } from 'vue';
|
||||||
|
const component: ComponentOptions;
|
||||||
|
export default component;
|
||||||
|
}
|
|
@ -0,0 +1,165 @@
|
||||||
|
import { api } from '@flaschengeist/api';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import { defineStore } from 'pinia';
|
||||||
|
|
||||||
|
interface UserService {
|
||||||
|
user: FG.Service;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fixJob(job: FG.Job) {
|
||||||
|
job.start = new Date(job.start);
|
||||||
|
if (job.end) job.end = new Date(job.end);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fixEvent(event: FG.Event) {
|
||||||
|
event.start = new Date(event.start);
|
||||||
|
if (event.end) event.end = new Date(event.end);
|
||||||
|
|
||||||
|
event.jobs.forEach((job) => fixJob(job));
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useScheduleStore = defineStore({
|
||||||
|
id: 'schedule',
|
||||||
|
|
||||||
|
state: () => ({
|
||||||
|
jobTypes: [] as FG.JobType[],
|
||||||
|
eventTypes: [] as FG.EventType[],
|
||||||
|
templates: [] as FG.Event[],
|
||||||
|
}),
|
||||||
|
|
||||||
|
getters: {},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
async getJobTypes(force = false) {
|
||||||
|
if (force || this.jobTypes.length == 0)
|
||||||
|
try {
|
||||||
|
const { data } = await api.get<FG.JobType[]>('/events/job-types');
|
||||||
|
this.jobTypes = data;
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return this.jobTypes;
|
||||||
|
},
|
||||||
|
|
||||||
|
async addJobType(name: string) {
|
||||||
|
await api.post<FG.JobType>('/events/job-types', { name: name });
|
||||||
|
//TODO: HAndle new JT
|
||||||
|
},
|
||||||
|
|
||||||
|
async removeJobType(id: number) {
|
||||||
|
await api.delete(`/events/job-types/${id}`);
|
||||||
|
//Todo Handle delete JT
|
||||||
|
},
|
||||||
|
|
||||||
|
async renameJobType(id: number, newName: string) {
|
||||||
|
await api.put(`/events/job-types/${id}`, { name: newName });
|
||||||
|
// TODO handle rename
|
||||||
|
},
|
||||||
|
|
||||||
|
async getEventTypes(force = false) {
|
||||||
|
if (force || this.eventTypes.length == 0)
|
||||||
|
try {
|
||||||
|
const { data } = await api.get<FG.EventType[]>('/events/event-types');
|
||||||
|
this.eventTypes = data;
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return this.eventTypes;
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Add new EventType
|
||||||
|
*
|
||||||
|
* @param name Name of new EventType
|
||||||
|
* @returns EventType object or null if name already exists
|
||||||
|
* @throws Exception if requests fails because of an other reason
|
||||||
|
*/
|
||||||
|
async addEventType(name: string) {
|
||||||
|
try {
|
||||||
|
const { data } = await api.post<FG.EventType>('/events/event-types', { name: name });
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
if ('response' in error) {
|
||||||
|
const ae = <AxiosError>error;
|
||||||
|
if (ae.response && ae.response.status == 409 /* CONFLICT */) return null;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async removeEvent(id: number) {
|
||||||
|
try {
|
||||||
|
await api.delete(`/events/${id}`);
|
||||||
|
const idx = this.templates.findIndex((v) => v.id === id);
|
||||||
|
if (idx !== -1) this.templates.splice(idx, 1);
|
||||||
|
} catch (e) {
|
||||||
|
const error = <AxiosError>e;
|
||||||
|
if (error.response && error.response.status === 404) return false;
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
async removeEventType(id: number) {
|
||||||
|
await api.delete(`/events/event-types/${id}`);
|
||||||
|
// TODO handle delete
|
||||||
|
},
|
||||||
|
|
||||||
|
async renameEventType(id: number, newName: string) {
|
||||||
|
try {
|
||||||
|
await api.put(`/events/event-types/${id}`, { name: newName });
|
||||||
|
// TODO handle rename
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
if ('response' in error) {
|
||||||
|
const ae = <AxiosError>error;
|
||||||
|
if (ae.response && ae.response.status == 409 /* CONFLICT */) return false;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async getTemplates(force = false) {
|
||||||
|
if (force || this.templates.length == 0) {
|
||||||
|
const { data } = await api.get<FG.Event[]>('/events/templates');
|
||||||
|
data.forEach((element) => fixEvent(element));
|
||||||
|
this.templates = data;
|
||||||
|
}
|
||||||
|
return this.templates;
|
||||||
|
},
|
||||||
|
|
||||||
|
async getEvents(filter: { from?: Date; to?: Date } | undefined = undefined) {
|
||||||
|
try {
|
||||||
|
const { data } = await api.get<FG.Event[]>('/events', { params: filter });
|
||||||
|
data.forEach((element) => fixEvent(element));
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async getEvent(id: number) {
|
||||||
|
try {
|
||||||
|
const { data } = await api.get<FG.Event>(`/events/${id}`);
|
||||||
|
fixEvent(data);
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateJob(eventId: number, jobId: number, service: FG.Service | UserService) {
|
||||||
|
try {
|
||||||
|
const { data } = await api.put<FG.Job>(`/events/${eventId}/jobs/${jobId}`, service);
|
||||||
|
fixJob(data);
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async addEvent(event: FG.Event) {
|
||||||
|
const { data } = await api.post<FG.Event>('/events', event);
|
||||||
|
if (data.is_template) this.templates.push(data);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
Loading…
Reference in New Issue