Compare commits
3 Commits
11a4f87005
...
e4d3ef2097
Author | SHA1 | Date |
---|---|---|
Ferdinand Thiessen | e4d3ef2097 | |
Ferdinand Thiessen | dd49b0eb9e | |
Ferdinand Thiessen | 58621d3da4 |
18
.eslintrc.js
18
.eslintrc.js
|
@ -17,11 +17,11 @@ module.exports = {
|
||||||
project: resolve(__dirname, './tsconfig.json'),
|
project: resolve(__dirname, './tsconfig.json'),
|
||||||
tsconfigRootDir: __dirname,
|
tsconfigRootDir: __dirname,
|
||||||
ecmaVersion: 2019, // Allows for the parsing of modern ECMAScript features
|
ecmaVersion: 2019, // Allows for the parsing of modern ECMAScript features
|
||||||
sourceType: 'module' // Allows for the use of imports
|
sourceType: 'module', // Allows for the use of imports
|
||||||
},
|
},
|
||||||
|
|
||||||
env: {
|
env: {
|
||||||
browser: true
|
browser: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Rules order is important, please avoid shuffling them
|
// Rules order is important, please avoid shuffling them
|
||||||
|
@ -44,7 +44,7 @@ module.exports = {
|
||||||
|
|
||||||
// https://github.com/prettier/eslint-config-prettier#installation
|
// https://github.com/prettier/eslint-config-prettier#installation
|
||||||
// usage with Prettier, provided by 'eslint-config-prettier'.
|
// usage with Prettier, provided by 'eslint-config-prettier'.
|
||||||
'prettier', //'plugin:prettier/recommended'
|
'plugin:prettier/recommended'
|
||||||
],
|
],
|
||||||
|
|
||||||
plugins: [
|
plugins: [
|
||||||
|
@ -54,10 +54,6 @@ module.exports = {
|
||||||
// https://eslint.vuejs.org/user-guide/#why-doesn-t-it-work-on-vue-file
|
// https://eslint.vuejs.org/user-guide/#why-doesn-t-it-work-on-vue-file
|
||||||
// required to lint *.vue files
|
// required to lint *.vue files
|
||||||
'vue',
|
'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
|
// add your custom rules here
|
||||||
|
@ -66,10 +62,8 @@ module.exports = {
|
||||||
|
|
||||||
// TypeScript
|
// TypeScript
|
||||||
quotes: ['warn', 'single', { avoidEscape: true }],
|
quotes: ['warn', 'single', { avoidEscape: true }],
|
||||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
|
||||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
|
||||||
|
|
||||||
// allow debugger during development only
|
// allow debugger during development only
|
||||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
|
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
|
|
27
package.json
27
package.json
|
@ -15,29 +15,30 @@
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"types": "src/api.d.ts",
|
"types": "src/api.d.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"pretty": "prettier --config ./package.json --write '{,!(node_modules)/**/}*.ts'",
|
"format": "prettier --config ./package.json --write '{,!(node_modules|backend)/**/}*.{js,ts,vue}'",
|
||||||
"lint": "eslint --ext .js,.ts,.vue ./src"
|
"lint": "eslint --ext .js,.ts,.vue ./src"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@quasar/quasar-ui-qcalendar": "^4.0.0-beta.10"
|
"@quasar/quasar-ui-qcalendar": "^4.0.0-beta.10"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@flaschengeist/api": "^1.0.0-alpha.6",
|
"@flaschengeist/api": "^1.0.0-alpha.7",
|
||||||
"@flaschengeist/types": "^1.0.0-alpha.9",
|
"@flaschengeist/types": "^1.0.0-alpha.10",
|
||||||
"@quasar/app": "^3.2.3",
|
"@quasar/app": "^3.2.4",
|
||||||
"quasar": "^2.3.3",
|
"@typescript-eslint/eslint-plugin": "^5.5.0",
|
||||||
|
"@typescript-eslint/parser": "^5.5.0",
|
||||||
"axios": "^0.24.0",
|
"axios": "^0.24.0",
|
||||||
"prettier": "^2.5.0",
|
"eslint": "^8.4.0",
|
||||||
"typescript": "^4.5.2",
|
|
||||||
"pinia": "^2.0.4",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^5.4.0",
|
|
||||||
"@typescript-eslint/parser": "^5.4.0",
|
|
||||||
"eslint": "^8.3.0",
|
|
||||||
"eslint-config-prettier": "^8.3.0",
|
"eslint-config-prettier": "^8.3.0",
|
||||||
"eslint-plugin-vue": "^8.1.1"
|
"eslint-plugin-prettier": "^4.0.0",
|
||||||
|
"eslint-plugin-vue": "^8.1.1",
|
||||||
|
"pinia": "^2.0.6",
|
||||||
|
"prettier": "^2.5.1",
|
||||||
|
"quasar": "^2.3.3",
|
||||||
|
"typescript": "^4.5.2"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@flaschengeist/api": "^1.0.0-alpha.6",
|
"@flaschengeist/api": "^1.0.0-alpha.7",
|
||||||
"@flaschengeist/users": "^1.0.0-alpha.2"
|
"@flaschengeist/users": "^1.0.0-alpha.2"
|
||||||
},
|
},
|
||||||
"prettier": {
|
"prettier": {
|
||||||
|
|
|
@ -2,9 +2,9 @@
|
||||||
<q-card style="text-align: center">
|
<q-card style="text-align: center">
|
||||||
<q-card-section class="row justify-center items-center content-center">
|
<q-card-section class="row justify-center items-center content-center">
|
||||||
<div class="col-5">
|
<div class="col-5">
|
||||||
<q-icon :name="jobs == 0 ? 'mdi-calendar-blank' : 'mdi-calendar-alert'" :size="divHeight" />
|
<q-icon :name="jobs == 0 ? 'mdi-calendar-blank' : 'mdi-calendar-alert'" :size="divHeight" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="(jobs || 0) > 0" ref="div" class="col-7">
|
<div v-if="(jobs || 0) > 0" ref="div" class="col-7">
|
||||||
<div class="text-h6">Anstehende Dienste</div>
|
<div class="text-h6">Anstehende Dienste</div>
|
||||||
<div class="text-body1">{{ jobs }}</div>
|
<div class="text-body1">{{ jobs }}</div>
|
||||||
<div class="text-h6">Nächster Dienst</div>
|
<div class="text-h6">Nächster Dienst</div>
|
||||||
|
|
|
@ -140,16 +140,6 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
setup(props, { emit }) {
|
setup(props, { emit }) {
|
||||||
const store = useEventStore();
|
const store = useEventStore();
|
||||||
const startDate = computed(() => {
|
|
||||||
const d = date.buildDate({ milliseconds: 0, seconds: 0, minutes: 0, hours: 0 });
|
|
||||||
if (!props.date || !date.isValid(props.date)) return d;
|
|
||||||
const split = props.date.split('-');
|
|
||||||
return date.adjustDate(d, {
|
|
||||||
year: parseInt(split[0]),
|
|
||||||
month: parseInt(split[1]),
|
|
||||||
date: parseInt(split[2]),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const active = ref(0);
|
const active = ref(0);
|
||||||
const activeJob = ref<{ validate: () => Promise<boolean> }>();
|
const activeJob = ref<{ validate: () => Promise<boolean> }>();
|
||||||
|
@ -166,9 +156,12 @@ export default defineComponent({
|
||||||
void store.getTemplates();
|
void store.getTemplates();
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(props, (n, o) => {
|
watch(
|
||||||
if (event.value?.id !== n.modelValue?.id) reset();
|
() => props.modelValue,
|
||||||
});
|
(newModelValue) => {
|
||||||
|
if (event.value?.id !== newModelValue?.id) reset();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
function addJob() {
|
function addJob() {
|
||||||
if (!activeJob.value) event.value.jobs.push(emptyJob());
|
if (!activeJob.value) event.value.jobs.push(emptyJob());
|
||||||
|
|
|
@ -57,7 +57,12 @@
|
||||||
</q-form>
|
</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="$emit('remove-job')" />
|
<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>
|
||||||
|
@ -126,7 +131,7 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
|
|
||||||
expose({
|
expose({
|
||||||
validate: () => form.value?.validate() || Promise.resolve(true)
|
validate: () => form.value?.validate() || Promise.resolve(true),
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -140,7 +145,6 @@ export default defineComponent({
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style></style>
|
<style></style>
|
||||||
|
|
|
@ -3,14 +3,27 @@
|
||||||
<q-dialog v-model="dialogOpen">
|
<q-dialog v-model="dialogOpen">
|
||||||
<q-card>
|
<q-card>
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<div class="text-h6">Editere {{title}} {{ actualType.name }}</div>
|
<div class="text-h6">Editere {{ title }} {{ actualType.name }}</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<q-input ref="dialogInput" v-model="actualType.name" :rules="rules" dense label="name" filled />
|
<q-input
|
||||||
|
ref="dialogInput"
|
||||||
|
v-model="actualType.name"
|
||||||
|
:rules="rules"
|
||||||
|
dense
|
||||||
|
label="name"
|
||||||
|
filled
|
||||||
|
/>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<q-card-actions>
|
<q-card-actions>
|
||||||
<q-btn flat color="danger" label="Abbrechen" @click="discardChanges()" />
|
<q-btn flat color="danger" label="Abbrechen" @click="discardChanges()" />
|
||||||
<q-btn flat color="primary" label="Speichern" :disable="!!dialogInput && !dialogInput.validate()" @click="saveChanges()" />
|
<q-btn
|
||||||
|
flat
|
||||||
|
color="primary"
|
||||||
|
label="Speichern"
|
||||||
|
:disable="!!dialogInput && !dialogInput.validate()"
|
||||||
|
@click="saveChanges()"
|
||||||
|
/>
|
||||||
</q-card-actions>
|
</q-card-actions>
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
|
@ -33,11 +46,7 @@
|
||||||
</template>
|
</template>
|
||||||
<template #body-cell-actions="props">
|
<template #body-cell-actions="props">
|
||||||
<q-td :props="props" align="right" :auto-width="true">
|
<q-td :props="props" align="right" :auto-width="true">
|
||||||
<q-btn
|
<q-btn round icon="mdi-pencil" @click="editType(props.row.id)" />
|
||||||
round
|
|
||||||
icon="mdi-pencil"
|
|
||||||
@click="editType(props.row.id)"
|
|
||||||
/>
|
|
||||||
<q-btn round icon="mdi-delete" @click="deleteType(props.row.id)" />
|
<q-btn round icon="mdi-delete" @click="deleteType(props.row.id)" />
|
||||||
</q-td>
|
</q-td>
|
||||||
</template>
|
</template>
|
||||||
|
@ -56,9 +65,9 @@ import { useQuasar, QInput } from 'quasar';
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'ManageTypes',
|
name: 'ManageTypes',
|
||||||
components: {},
|
components: {},
|
||||||
props:{
|
props: {
|
||||||
type: {type: String as PropType<'EventType' | 'JobType'>, required: true},
|
type: { type: String as PropType<'EventType' | 'JobType'>, required: true },
|
||||||
title: {type: String, required: true}
|
title: { type: String, required: true },
|
||||||
},
|
},
|
||||||
setup(props) {
|
setup(props) {
|
||||||
const store = useEventStore();
|
const store = useEventStore();
|
||||||
|
@ -69,17 +78,16 @@ export default defineComponent({
|
||||||
const actualType = ref(emptyType);
|
const actualType = ref(emptyType);
|
||||||
const input = ref<QInput>();
|
const input = ref<QInput>();
|
||||||
const dialogInput = ref<QInput>();
|
const dialogInput = ref<QInput>();
|
||||||
const storeName = computed(() => props.type == 'EventType' ? 'eventTypes' : 'jobTypes')
|
const storeName = computed(() => (props.type == 'EventType' ? 'eventTypes' : 'jobTypes'));
|
||||||
|
|
||||||
onBeforeMount(async () => await store[`get${props.type}s`]());
|
onBeforeMount(async () => await store[`get${props.type}s`]());
|
||||||
|
|
||||||
const rows = computed(() => <(FG.EventType|FG.JobType)[]>store[storeName.value]);
|
const rows = computed(() => <(FG.EventType | FG.JobType)[]>store[storeName.value]);
|
||||||
|
|
||||||
const rules = [
|
const rules = [
|
||||||
(s: unknown) => !!s || 'Darf nicht leer sein!',
|
(s: unknown) => !!s || 'Darf nicht leer sein!',
|
||||||
(s: string) =>
|
(s: string) =>
|
||||||
rows.value.find((e) => e.name === s) === undefined ||
|
rows.value.find((e) => e.name === s) === undefined || 'Der Name wird bereits verwendet',
|
||||||
'Der Name wird bereits verwendet',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
|
@ -100,8 +108,7 @@ export default defineComponent({
|
||||||
|
|
||||||
function addType() {
|
function addType() {
|
||||||
if (input.value === undefined || input.value.validate())
|
if (input.value === undefined || input.value.validate())
|
||||||
store
|
store[`add${props.type}`](actualType.value.name)
|
||||||
[`add${props.type}`](actualType.value.name)
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
actualType.value.name = '';
|
actualType.value.name = '';
|
||||||
})
|
})
|
||||||
|
@ -121,12 +128,17 @@ export default defineComponent({
|
||||||
|
|
||||||
function editType(id: number) {
|
function editType(id: number) {
|
||||||
dialogOpen.value = true;
|
dialogOpen.value = true;
|
||||||
actualType.value = Object.assign({}, rows.value.find((v) => v.id === id));
|
actualType.value = Object.assign(
|
||||||
|
{},
|
||||||
|
rows.value.find((v) => v.id === id)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveChanges() {
|
function saveChanges() {
|
||||||
if (dialogInput.value === undefined || dialogInput.value.validate())
|
if (dialogInput.value === undefined || dialogInput.value.validate())
|
||||||
void store[`rename${props.type}`](actualType.value.id, actualType.value.name).then(() => discardChanges());
|
void store[`rename${props.type}`](actualType.value.id, actualType.value.name).then(() =>
|
||||||
|
discardChanges()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function discardChanges() {
|
function discardChanges() {
|
||||||
|
|
|
@ -63,7 +63,7 @@ export default defineComponent({
|
||||||
const rule = new Proxy(props.modelValue, {
|
const rule = new Proxy(props.modelValue, {
|
||||||
get(target, prop) {
|
get(target, prop) {
|
||||||
if (typeof prop === 'string') {
|
if (typeof prop === 'string') {
|
||||||
return ((props.modelValue as unknown) as Record<string, unknown>)[prop];
|
return (props.modelValue as unknown as Record<string, unknown>)[prop];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
set(target, prop, value) {
|
set(target, prop, value) {
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
<div class="q-pa-md">
|
<div class="q-pa-md">
|
||||||
<q-card style="height: 70vh; max-width: 1800px" class="q-pa-md">
|
<q-card style="height: 70vh; max-width: 1800px" class="q-pa-md">
|
||||||
<div class="scroll" ref="scrollDiv" style="height: 100%">
|
<div ref="scrollDiv" class="scroll" style="height: 100%">
|
||||||
<q-infinite-scroll :offset="250" @load="load">
|
<q-infinite-scroll :offset="250" @load="load">
|
||||||
<q-list>
|
<q-list>
|
||||||
<q-item id="bbb">
|
<q-item id="bbb">
|
||||||
|
|
|
@ -13,18 +13,44 @@
|
||||||
<div>
|
<div>
|
||||||
<q-select
|
<q-select
|
||||||
:model-value="modelValue.services"
|
:model-value="modelValue.services"
|
||||||
|
:disable="!canAssignOther || modelValue.locked"
|
||||||
|
:options="options"
|
||||||
|
option-value="userid"
|
||||||
filled
|
filled
|
||||||
:option-label="(opt) => userDisplay(opt)"
|
|
||||||
multiple
|
multiple
|
||||||
disable
|
use-input
|
||||||
use-chips
|
|
||||||
stack-label
|
|
||||||
label="Dienste"
|
label="Dienste"
|
||||||
|
behavior="dialog"
|
||||||
class="col-auto q-px-xs"
|
class="col-auto q-px-xs"
|
||||||
style="font-size: 6px"
|
@filter="filterUsers"
|
||||||
counter
|
@add="({ value }) => assign(value)"
|
||||||
:max-values="modelValue.required_services"
|
@remove="({ value }) => unassign(value)"
|
||||||
>
|
>
|
||||||
|
<template #selected-item="{ opt, toggleOption }">
|
||||||
|
<service-user-chip :model-value="opt" removeable @remove="toggleOption" />
|
||||||
|
</template>
|
||||||
|
<template #option="{ opt, itemProps }">
|
||||||
|
<q-item v-bind="itemProps">
|
||||||
|
<q-item-section avatar>
|
||||||
|
<user-avatar :model-value="opt.userid" />
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>{{ userDisplay(opt.userid) }}</q-item-section>
|
||||||
|
<q-item-section style="max-width: 10em" side>
|
||||||
|
<q-input
|
||||||
|
v-model.number="opt.value"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
step="0.25"
|
||||||
|
dense
|
||||||
|
filled
|
||||||
|
@click.stop=""
|
||||||
|
/>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section side>
|
||||||
|
<q-toggle v-model="opt.is_backup" label="Backup" left-label />
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</template>
|
||||||
</q-select>
|
</q-select>
|
||||||
<div class="row col-12 justify-end">
|
<div class="row col-12 justify-end">
|
||||||
<q-btn
|
<q-btn
|
||||||
|
@ -32,7 +58,7 @@
|
||||||
flat
|
flat
|
||||||
color="primary"
|
color="primary"
|
||||||
label="Eintragen"
|
label="Eintragen"
|
||||||
@click="assignJob()"
|
@click="assign()"
|
||||||
/>
|
/>
|
||||||
<q-btn v-if="isEnrolled && !modelValue.locked" flat color="secondary" label="Optionen">
|
<q-btn v-if="isEnrolled && !modelValue.locked" flat color="secondary" label="Optionen">
|
||||||
<q-menu auto-close>
|
<q-menu auto-close>
|
||||||
|
@ -43,8 +69,18 @@
|
||||||
<q-item clickable @click="transfer">
|
<q-item clickable @click="transfer">
|
||||||
<q-item-section>Tauschen</q-item-section>
|
<q-item-section>Tauschen</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
|
<q-item v-if="isBackup" clickable @click="backup(false)">
|
||||||
|
<q-tooltip>Backup zu vollem Dienst machen</q-tooltip>
|
||||||
|
<q-item-section>Dienst</q-item-section>
|
||||||
|
<q-item-section side><q-icon name="mdi-eye" /></q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item v-else clickable @click="backup(true)">
|
||||||
|
<q-tooltip>Nur als Backup eintragen</q-tooltip>
|
||||||
|
<q-item-section>Backup</q-item-section>
|
||||||
|
<q-item-section side><q-icon name="mdi-eye-off" /></q-item-section>
|
||||||
|
</q-item>
|
||||||
<q-separator />
|
<q-separator />
|
||||||
<q-item clickable @click="assignJob(false)">
|
<q-item clickable @click="unassign()">
|
||||||
<q-item-section class="text-negative">Austragen</q-item-section>
|
<q-item-section class="text-negative">Austragen</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
</q-list>
|
</q-list>
|
||||||
|
@ -56,14 +92,19 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, onBeforeMount, computed, PropType } from 'vue';
|
|
||||||
import { date, useQuasar } from 'quasar';
|
import { date, useQuasar } from 'quasar';
|
||||||
import { asHour, useMainStore, useUserStore } from '@flaschengeist/api';
|
import { defineComponent, onBeforeMount, computed, ref, PropType } from 'vue';
|
||||||
|
import { asHour, hasPermission, useMainStore, useUserStore } from '@flaschengeist/api';
|
||||||
import { useEventStore } from '../../../store';
|
import { useEventStore } from '../../../store';
|
||||||
|
import { PERMISSIONS } from '../../../permissions';
|
||||||
|
|
||||||
import TransferInviteDialog from './TransferInviteDialog.vue';
|
import TransferInviteDialog from './TransferInviteDialog.vue';
|
||||||
|
import ServiceUserChip from './ServiceUserChip.vue';
|
||||||
|
import { UserAvatar } from '@flaschengeist/api/components';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'JobSlot',
|
name: 'JobSlot',
|
||||||
|
components: { ServiceUserChip, UserAvatar },
|
||||||
props: {
|
props: {
|
||||||
modelValue: {
|
modelValue: {
|
||||||
required: true,
|
required: true,
|
||||||
|
@ -81,31 +122,42 @@ export default defineComponent({
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const quasar = useQuasar();
|
const quasar = useQuasar();
|
||||||
|
|
||||||
onBeforeMount(async () => userStore.getUsers());
|
// Make sure users are loaded if we can assign them
|
||||||
|
onBeforeMount(async () => await userStore.getUsers());
|
||||||
|
|
||||||
function userDisplay(service: FG.Service) {
|
/* Stuff used for general display */
|
||||||
return userStore.findUser(service.userid)?.display_name || service.userid;
|
// Get displayname of user
|
||||||
|
function userDisplay(id: string) {
|
||||||
|
return userStore.findUser(id)?.display_name || id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The name of the current job
|
||||||
const typeName = computed(() =>
|
const typeName = computed(() =>
|
||||||
typeof props.modelValue.type === 'object'
|
typeof props.modelValue.type === 'object'
|
||||||
? props.modelValue.type.name
|
? props.modelValue.type.name
|
||||||
: store.jobTypes.find((j) => j.id === props.modelValue.type)?.name || 'Unbekannter Diensttyp'
|
: store.jobTypes.find((j) => j.id === props.modelValue.type)?.name ||
|
||||||
|
'Unbekannter Diensttyp'
|
||||||
);
|
);
|
||||||
|
|
||||||
const isEnrolled = computed(
|
// The service of the current user if self assigned to the job
|
||||||
() =>
|
const service = computed(() =>
|
||||||
props.modelValue.services.findIndex(
|
props.modelValue.services.find((service) => service.userid == mainStore.currentUser.userid)
|
||||||
(service) => service.userid == mainStore.currentUser.userid
|
|
||||||
) !== -1
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Weather the current user is assigned to the job
|
||||||
|
const isEnrolled = computed(() => service.value !== undefined);
|
||||||
|
|
||||||
|
// If the job has enough assigned services
|
||||||
const isFull = computed(
|
const isFull = computed(
|
||||||
() =>
|
() =>
|
||||||
props.modelValue.services.map((s) => s.value).reduce((p, c) => p + c, 0) >=
|
props.modelValue.services.map((s) => s.value).reduce((p, c) => p + c, 0) >=
|
||||||
props.modelValue.required_services
|
props.modelValue.required_services
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// If current user is only backup service
|
||||||
|
const isBackup = computed(() => service.value?.is_backup || false);
|
||||||
|
|
||||||
|
// If it is still possible to invite other users (= job is today or in the future)
|
||||||
const canInvite = computed(
|
const canInvite = computed(
|
||||||
() =>
|
() =>
|
||||||
(props.modelValue.end || props.modelValue.start) >
|
(props.modelValue.end || props.modelValue.start) >
|
||||||
|
@ -115,14 +167,15 @@ export default defineComponent({
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
async function assignJob(assign = true) {
|
// Assign user to a job
|
||||||
const newService: FG.Service = {
|
async function assign(service?: FG.Service) {
|
||||||
|
service = service || {
|
||||||
userid: mainStore.currentUser.userid,
|
userid: mainStore.currentUser.userid,
|
||||||
is_backup: false,
|
is_backup: false,
|
||||||
value: assign ? 1 : -1,
|
value: 1,
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
const job = await store.assignToJob(props.modelValue.id, newService);
|
const job = await store.assignToJob(props.modelValue.id, service);
|
||||||
emit('update:modelValue', job);
|
emit('update:modelValue', job);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(error);
|
console.warn(error);
|
||||||
|
@ -137,6 +190,7 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// open invite dialog (or transfer)
|
||||||
function invite(isInvite = true) {
|
function invite(isInvite = true) {
|
||||||
quasar.dialog({
|
quasar.dialog({
|
||||||
component: TransferInviteDialog,
|
component: TransferInviteDialog,
|
||||||
|
@ -147,15 +201,72 @@ export default defineComponent({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Stuff needed if we can assign other user */
|
||||||
|
// Current user can assign other users
|
||||||
|
const canAssignOther = computed(() => hasPermission(PERMISSIONS.ASSIGN_OTHER));
|
||||||
|
|
||||||
|
// options shown in the select
|
||||||
|
const options = ref([] as FG.Service[]);
|
||||||
|
|
||||||
|
// users which are available (e.g. not already assigned)
|
||||||
|
const freeUsers = computed(() =>
|
||||||
|
userStore.users.filter((u) => props.modelValue.services.every((s) => s.userid !== u.userid))
|
||||||
|
);
|
||||||
|
|
||||||
|
// used to filter options based on user input
|
||||||
|
function filterUsers(
|
||||||
|
input: string,
|
||||||
|
doneFn: (
|
||||||
|
callbackFn: () => void,
|
||||||
|
afterFn?: (ref: { [index: string]: unknown }) => void
|
||||||
|
) => void,
|
||||||
|
abortFn: () => void
|
||||||
|
) {
|
||||||
|
if (freeUsers.value.length == 0) return abortFn();
|
||||||
|
|
||||||
|
// Filter the options
|
||||||
|
doneFn(() => {
|
||||||
|
// Skip filter options if input is too short
|
||||||
|
if (!input || input.length < 2) {
|
||||||
|
options.value = freeUsers.value.map<FG.Service>((u) => ({
|
||||||
|
userid: u.userid,
|
||||||
|
value: 1,
|
||||||
|
is_backup: false,
|
||||||
|
}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Search matching string within all names
|
||||||
|
options.value = freeUsers.value
|
||||||
|
.filter((u) =>
|
||||||
|
input
|
||||||
|
.toLowerCase()
|
||||||
|
.split(' ')
|
||||||
|
.every(
|
||||||
|
(needle) =>
|
||||||
|
u.display_name.toLowerCase().indexOf(needle) > -1 ||
|
||||||
|
u.firstname.toLowerCase().indexOf(needle) > -1 ||
|
||||||
|
u.lastname.toLowerCase().indexOf(needle) > -1
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.map<FG.Service>((u) => ({ userid: u.userid, value: 1, is_backup: false }));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
assignJob,
|
assign,
|
||||||
|
unassign: (s?: FG.Service) => assign(Object.assign({}, s || service.value, { value: -1 })),
|
||||||
|
backup: (is_backup: boolean) => assign(Object.assign({}, service.value, { is_backup })),
|
||||||
|
canAssignOther,
|
||||||
canInvite,
|
canInvite,
|
||||||
|
filterUsers,
|
||||||
|
isBackup,
|
||||||
isEnrolled,
|
isEnrolled,
|
||||||
isFull,
|
isFull,
|
||||||
invite: () => invite(true),
|
invite: () => invite(true),
|
||||||
transfer: () => invite(false),
|
transfer: () => invite(false),
|
||||||
typeName,
|
typeName,
|
||||||
userDisplay,
|
userDisplay,
|
||||||
|
options,
|
||||||
asHour,
|
asHour,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
|
@ -0,0 +1,67 @@
|
||||||
|
<template>
|
||||||
|
<q-chip
|
||||||
|
:removable="removeable"
|
||||||
|
:color="modelValue.is_backup ? 'grey' : undefined"
|
||||||
|
@remove="remove"
|
||||||
|
>
|
||||||
|
<q-tooltip>{{ displayName }} ({{ serviceValue }}x)</q-tooltip>
|
||||||
|
<user-avatar :model-value="modelValue.userid">
|
||||||
|
<slot v-if="modelValue.is_backup">
|
||||||
|
<q-icon v-if="modelValue.is_backup" name="mdi-eye-off" />
|
||||||
|
</slot>
|
||||||
|
</user-avatar>
|
||||||
|
<div class="ellipsis">{{ displayName }}</div>
|
||||||
|
<q-badge v-if="modelValue.value !== 1" :label="serviceValue" style="margin-left: 0.25em" />
|
||||||
|
<slot />
|
||||||
|
</q-chip>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { useUserStore } from '@flaschengeist/api';
|
||||||
|
import { PropType, computed, defineComponent, onBeforeMount, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { UserAvatar } from '@flaschengeist/api/components';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'ServiceUserChip',
|
||||||
|
components: { UserAvatar },
|
||||||
|
props: {
|
||||||
|
modelValue: {
|
||||||
|
type: Object as PropType<FG.Service>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
removeable: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: ['remove'],
|
||||||
|
setup(props, { emit }) {
|
||||||
|
const userStore = useUserStore();
|
||||||
|
|
||||||
|
const user = ref<FG.User>();
|
||||||
|
|
||||||
|
onBeforeMount(async () => {
|
||||||
|
user.value = await userStore.getUser(props.modelValue.userid);
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
async () => (user.value = await userStore.getUser(props.modelValue.userid))
|
||||||
|
);
|
||||||
|
|
||||||
|
const displayName = computed(() => user.value?.display_name || '...');
|
||||||
|
const serviceValue = computed(() =>
|
||||||
|
props.modelValue.value.toFixed(Number.isInteger(props.modelValue.value) ? 0 : 1)
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
displayName,
|
||||||
|
remove: () => emit('remove', props.modelValue),
|
||||||
|
serviceValue,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style></style>
|
|
@ -81,4 +81,4 @@ export default defineComponent({
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -54,10 +54,10 @@ export default defineComponent({
|
||||||
setup() {
|
setup() {
|
||||||
const quasar = useQuasar();
|
const quasar = useQuasar();
|
||||||
|
|
||||||
const tabs = computed(() => ([
|
const tabs = computed(() => [
|
||||||
{ name: 'listView', label: 'Liste' },
|
{ name: 'listView', label: 'Liste' },
|
||||||
{ name: 'agendaView', label: 'Kalendar' }
|
{ name: 'agendaView', label: 'Kalendar' },
|
||||||
]));
|
]);
|
||||||
|
|
||||||
const drawer = ref<boolean>(false);
|
const drawer = ref<boolean>(false);
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue