From 1a63f350a23560baec6f43a067104e72f1828259 Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Tue, 25 May 2021 17:11:44 +0200 Subject: [PATCH] [core] Moved source from main flaschengeist tree --- .eslintrc.js | 75 ++++++ package.json | 50 ++++ src/components/Widget.vue | 25 ++ src/components/management/EditEvent.vue | 245 +++++++++++++++++++ src/components/management/EventTypes.vue | 126 ++++++++++ src/components/management/Job.vue | 113 +++++++++ src/components/management/JobTypes.vue | 125 ++++++++++ src/components/management/RecurrenceRule.vue | 96 ++++++++ src/components/overview/AgendaView.vue | 241 ++++++++++++++++++ src/components/overview/slots/EventSlot.vue | 99 ++++++++ src/components/overview/slots/JobSlot.vue | 140 +++++++++++ src/events.d.ts | 9 + src/index.ts | 22 ++ src/pages/Event.vue | 29 +++ src/pages/Management.vue | 95 +++++++ src/pages/Overview.vue | 87 +++++++ src/pages/Requests.vue | 8 + src/permissions.ts | 16 ++ src/routes/index.ts | 56 +++++ src/shims-vue.d.ts | 6 + src/store.ts | 165 +++++++++++++ 21 files changed, 1828 insertions(+) create mode 100644 .eslintrc.js create mode 100644 package.json create mode 100644 src/components/Widget.vue create mode 100644 src/components/management/EditEvent.vue create mode 100644 src/components/management/EventTypes.vue create mode 100644 src/components/management/Job.vue create mode 100644 src/components/management/JobTypes.vue create mode 100644 src/components/management/RecurrenceRule.vue create mode 100644 src/components/overview/AgendaView.vue create mode 100644 src/components/overview/slots/EventSlot.vue create mode 100644 src/components/overview/slots/JobSlot.vue create mode 100644 src/events.d.ts create mode 100644 src/index.ts create mode 100644 src/pages/Event.vue create mode 100644 src/pages/Management.vue create mode 100644 src/pages/Overview.vue create mode 100644 src/pages/Requests.vue create mode 100644 src/permissions.ts create mode 100644 src/routes/index.ts create mode 100644 src/shims-vue.d.ts create mode 100644 src/store.ts diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..ab9cd5e --- /dev/null +++ b/.eslintrc.js @@ -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' + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..ac786c5 --- /dev/null +++ b/package.json @@ -0,0 +1,50 @@ +{ + "private": true, + "license": "MIT", + "version": "1.0.0-alpha.1", + "name": "@flaschengeist/schedule", + "author": "Ferdinand ", + "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" + } +} diff --git a/src/components/Widget.vue b/src/components/Widget.vue new file mode 100644 index 0000000..84650c3 --- /dev/null +++ b/src/components/Widget.vue @@ -0,0 +1,25 @@ + + + diff --git a/src/components/management/EditEvent.vue b/src/components/management/EditEvent.vue new file mode 100644 index 0000000..d297f17 --- /dev/null +++ b/src/components/management/EditEvent.vue @@ -0,0 +1,245 @@ + + + + + diff --git a/src/components/management/EventTypes.vue b/src/components/management/EventTypes.vue new file mode 100644 index 0000000..a9dbf80 --- /dev/null +++ b/src/components/management/EventTypes.vue @@ -0,0 +1,126 @@ + + + + + diff --git a/src/components/management/Job.vue b/src/components/management/Job.vue new file mode 100644 index 0000000..58dc65d --- /dev/null +++ b/src/components/management/Job.vue @@ -0,0 +1,113 @@ + + + + + diff --git a/src/components/management/JobTypes.vue b/src/components/management/JobTypes.vue new file mode 100644 index 0000000..c3fc3f8 --- /dev/null +++ b/src/components/management/JobTypes.vue @@ -0,0 +1,125 @@ + + + + + diff --git a/src/components/management/RecurrenceRule.vue b/src/components/management/RecurrenceRule.vue new file mode 100644 index 0000000..03b020d --- /dev/null +++ b/src/components/management/RecurrenceRule.vue @@ -0,0 +1,96 @@ + + + + + diff --git a/src/components/overview/AgendaView.vue b/src/components/overview/AgendaView.vue new file mode 100644 index 0000000..0426042 --- /dev/null +++ b/src/components/overview/AgendaView.vue @@ -0,0 +1,241 @@ + + + + + diff --git a/src/components/overview/slots/EventSlot.vue b/src/components/overview/slots/EventSlot.vue new file mode 100644 index 0000000..3638682 --- /dev/null +++ b/src/components/overview/slots/EventSlot.vue @@ -0,0 +1,99 @@ + + + + + diff --git a/src/components/overview/slots/JobSlot.vue b/src/components/overview/slots/JobSlot.vue new file mode 100644 index 0000000..b891637 --- /dev/null +++ b/src/components/overview/slots/JobSlot.vue @@ -0,0 +1,140 @@ + + + + + diff --git a/src/events.d.ts b/src/events.d.ts new file mode 100644 index 0000000..ec9d101 --- /dev/null +++ b/src/events.d.ts @@ -0,0 +1,9 @@ +declare namespace FG { + export interface RecurrenceRule { + frequency: string; + interval: number; + count?: number; + until?: Date; + weekdays?: Array; + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..48f58d4 --- /dev/null +++ b/src/index.ts @@ -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; diff --git a/src/pages/Event.vue b/src/pages/Event.vue new file mode 100644 index 0000000..0e99801 --- /dev/null +++ b/src/pages/Event.vue @@ -0,0 +1,29 @@ + + + diff --git a/src/pages/Management.vue b/src/pages/Management.vue new file mode 100644 index 0000000..10fb7f6 --- /dev/null +++ b/src/pages/Management.vue @@ -0,0 +1,95 @@ + + + diff --git a/src/pages/Overview.vue b/src/pages/Overview.vue new file mode 100644 index 0000000..2a0f627 --- /dev/null +++ b/src/pages/Overview.vue @@ -0,0 +1,87 @@ + + + diff --git a/src/pages/Requests.vue b/src/pages/Requests.vue new file mode 100644 index 0000000..b64ab3f --- /dev/null +++ b/src/pages/Requests.vue @@ -0,0 +1,8 @@ + diff --git a/src/permissions.ts b/src/permissions.ts new file mode 100644 index 0000000..c1bc584 --- /dev/null +++ b/src/permissions.ts @@ -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', +}; diff --git a/src/routes/index.ts b/src/routes/index.ts new file mode 100644 index 0000000..23203af --- /dev/null +++ b/src/routes/index.ts @@ -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'), + }, +]; diff --git a/src/shims-vue.d.ts b/src/shims-vue.d.ts new file mode 100644 index 0000000..eab6529 --- /dev/null +++ b/src/shims-vue.d.ts @@ -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; +} diff --git a/src/store.ts b/src/store.ts new file mode 100644 index 0000000..1d74b50 --- /dev/null +++ b/src/store.ts @@ -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('/events/job-types'); + this.jobTypes = data; + } catch (error) { + throw error; + } + return this.jobTypes; + }, + + async addJobType(name: string) { + await api.post('/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('/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('/events/event-types', { name: name }); + return data; + } catch (error) { + if ('response' in error) { + const ae = 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 = 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 = 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('/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('/events', { params: filter }); + data.forEach((element) => fixEvent(element)); + return data; + } catch (error) { + throw error; + } + }, + + async getEvent(id: number) { + try { + const { data } = await api.get(`/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(`/events/${eventId}/jobs/${jobId}`, service); + fixJob(data); + return data; + } catch (error) { + throw error; + } + }, + + async addEvent(event: FG.Event) { + const { data } = await api.post('/events', event); + if (data.is_template) this.templates.push(data); + return data; + }, + }, +});