Compare commits
	
		
			3 Commits
		
	
	
		
			11a4f87005
			...
			e4d3ef2097
		
	
	| Author | SHA1 | Date | 
|---|---|---|
|  | e4d3ef2097 | |
|  | dd49b0eb9e | |
|  | 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