├── .prettierignore ├── .browserslistrc ├── .dockerignore ├── babel.config.js ├── .prettierrc.yml ├── src ├── shims-vue.d.ts ├── assets │ ├── logo.png │ ├── pt-logo.png │ ├── vision.png │ ├── bss-logo.png │ ├── hih-logo.png │ ├── ehealthcomlogo.jpg │ ├── imis-workflow.png │ ├── Solution-Enabler.png │ ├── Tagesspiegel-Logo.jpg │ ├── sueddeutsche-logo.jpg │ ├── tech4germany-logo.png │ ├── tagesspiegel-background.jpeg │ ├── global.scss │ ├── wave-bg.svg │ └── imis-logo.svg ├── views │ ├── About.vue │ ├── TestList.vue │ ├── Dashboard.vue │ ├── AppRoot.vue │ ├── PublicStatistics.vue │ ├── Login.vue │ ├── RegisterTest.vue │ ├── SubmitTestResult.vue │ ├── RegisterPatient.vue │ ├── Account.vue │ └── RegisterInstitution.vue ├── config.ts ├── Root.vue ├── util │ ├── helper-functions.ts │ ├── country-service.ts │ ├── export-service.ts │ ├── index.ts │ ├── plz-service.ts │ ├── mapping.ts │ ├── typing.ts │ ├── permissions.ts │ └── search.ts ├── models │ ├── test-types.ts │ ├── test-materials.ts │ ├── risk-occupation.ts │ ├── symptoms.ts │ ├── pre-illnesses.ts │ ├── index.ts │ ├── exposures.ts │ └── event-types.ts ├── shims-tsx.d.ts ├── store │ ├── index.ts │ └── modules │ │ ├── patients.module.ts │ │ └── auth.module.ts ├── main.ts ├── components │ ├── inputs │ │ ├── TestInput.vue │ │ ├── DateInput.vue │ │ ├── LaboratoryInput.vue │ │ ├── PlzInput.vue │ │ ├── PatientInput.vue │ │ └── BarcodeScanner.vue │ ├── other │ │ ├── IndexPatientTableCell.vue │ │ ├── QuarantineHospitalizationCard.vue │ │ ├── TestIncidentsCard.vue │ │ ├── History.vue │ │ └── CaseData.vue │ ├── structural │ │ ├── Navigation.vue │ │ └── Header.vue │ ├── modals │ │ ├── ChangePatientStammdatenForm.vue │ │ ├── ChangePasswordForm.vue │ │ ├── ChangePatientFalldatenForm.vue │ │ ├── AddOrChangeUserForm.vue │ │ └── ChangeInstitutionForm.vue │ └── form-groups │ │ ├── ExposureForm.vue │ │ ├── PreIllnessesForm.vue │ │ ├── SymptomsForm.vue │ │ ├── IllnessStatusForm.vue │ │ └── LocationFormGroup.vue ├── registerServiceWorker.ts ├── api │ └── index.ts └── router │ └── index.ts ├── public ├── logo.png ├── favicon.ico ├── web-imis.png ├── health.html ├── unsupported.js ├── unsupported.html └── index.html ├── vue.config.js ├── .gitattributes ├── .editorconfig ├── Dockerfile.prod ├── Dockerfile ├── tests └── unit │ └── example.spec.ts ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ ├── deploy.yml │ └── ci.yml ├── tsconfig.json ├── .eslintrc.js ├── .gitignore ├── LICENSE ├── README.md └── package.json /.prettierignore: -------------------------------------------------------------------------------- 1 | ImisSwaggerApi.ts -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .gradle -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@vue/cli-plugin-babel/preset'], 3 | } 4 | -------------------------------------------------------------------------------- /.prettierrc.yml: -------------------------------------------------------------------------------- 1 | trailingComma: "es5" 2 | useTabs: false 3 | tabWidth: 2 4 | semi: false 5 | singleQuote: true -------------------------------------------------------------------------------- /src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import Vue from 'vue' 3 | export default Vue 4 | } 5 | -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImisDevelopers/1_011_a_infektionsfall_uebermittellung/HEAD/public/logo.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImisDevelopers/1_011_a_infektionsfall_uebermittellung/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/web-imis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImisDevelopers/1_011_a_infektionsfall_uebermittellung/HEAD/public/web-imis.png -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImisDevelopers/1_011_a_infektionsfall_uebermittellung/HEAD/src/assets/logo.png -------------------------------------------------------------------------------- /src/views/About.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | publicPath: process.env.NODE_ENV === 'production' 3 | ? '/' 4 | : '/' 5 | } 6 | -------------------------------------------------------------------------------- /src/assets/pt-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImisDevelopers/1_011_a_infektionsfall_uebermittellung/HEAD/src/assets/pt-logo.png -------------------------------------------------------------------------------- /src/assets/vision.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImisDevelopers/1_011_a_infektionsfall_uebermittellung/HEAD/src/assets/vision.png -------------------------------------------------------------------------------- /src/assets/bss-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImisDevelopers/1_011_a_infektionsfall_uebermittellung/HEAD/src/assets/bss-logo.png -------------------------------------------------------------------------------- /src/assets/hih-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImisDevelopers/1_011_a_infektionsfall_uebermittellung/HEAD/src/assets/hih-logo.png -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | export const config = { 2 | showAllViews: false, // when enabled all views are visible in the Navigation bar 3 | } 4 | -------------------------------------------------------------------------------- /src/assets/ehealthcomlogo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImisDevelopers/1_011_a_infektionsfall_uebermittellung/HEAD/src/assets/ehealthcomlogo.jpg -------------------------------------------------------------------------------- /src/assets/imis-workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImisDevelopers/1_011_a_infektionsfall_uebermittellung/HEAD/src/assets/imis-workflow.png -------------------------------------------------------------------------------- /src/assets/Solution-Enabler.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImisDevelopers/1_011_a_infektionsfall_uebermittellung/HEAD/src/assets/Solution-Enabler.png -------------------------------------------------------------------------------- /src/assets/Tagesspiegel-Logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImisDevelopers/1_011_a_infektionsfall_uebermittellung/HEAD/src/assets/Tagesspiegel-Logo.jpg -------------------------------------------------------------------------------- /src/assets/sueddeutsche-logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImisDevelopers/1_011_a_infektionsfall_uebermittellung/HEAD/src/assets/sueddeutsche-logo.jpg -------------------------------------------------------------------------------- /src/assets/tech4germany-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImisDevelopers/1_011_a_infektionsfall_uebermittellung/HEAD/src/assets/tech4germany-logo.png -------------------------------------------------------------------------------- /src/assets/tagesspiegel-background.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImisDevelopers/1_011_a_infektionsfall_uebermittellung/HEAD/src/assets/tagesspiegel-background.jpeg -------------------------------------------------------------------------------- /src/Root.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # 2 | # https://help.github.com/articles/dealing-with-line-endings/ 3 | # 4 | # These are explicitly windows files and should use crlf 5 | *.bat text eol=crlf 6 | 7 | -------------------------------------------------------------------------------- /public/health.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Health check 6 | 7 | 8 |

OK

9 | 10 | 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | insert_final_newline = false 7 | 8 | [*.{vue,js}] 9 | indent_style = space 10 | 11 | [*.{yml,yaml}] 12 | indent_style = space 13 | -------------------------------------------------------------------------------- /src/views/TestList.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 14 | -------------------------------------------------------------------------------- /src/util/helper-functions.ts: -------------------------------------------------------------------------------- 1 | import moment from 'moment' 2 | import { Patient, InstitutionImpl } from '../api/ImisSwaggerApi' 3 | 4 | export function getDate(date: string) { 5 | if (date !== undefined && date !== null && date !== 'null') 6 | return moment(date).format('DD.MM.YYYY') 7 | else return 'Keine Angabe' 8 | } 9 | -------------------------------------------------------------------------------- /src/models/test-types.ts: -------------------------------------------------------------------------------- 1 | export type TestType = 'PCR' | 'ANTIBODY' 2 | 3 | export interface TestTypeItem { 4 | id: TestType 5 | label: string 6 | } 7 | 8 | export const testTypes: TestTypeItem[] = [ 9 | { 10 | id: 'ANTIBODY', 11 | label: 'Antikörper', 12 | }, 13 | { 14 | id: 'PCR', 15 | label: 'PCR', 16 | }, 17 | ] 18 | -------------------------------------------------------------------------------- /public/unsupported.js: -------------------------------------------------------------------------------- 1 | // Redirect to unsupported.html if the browser does not meet requirements 2 | // Actually only checking for IE (any version) 3 | const isIE10OrLower = (window.navigator.userAgent.indexOf('MSIE ') > 0) 4 | const isIE11 = (window.navigator.userAgent.indexOf('Trident/') > 0) 5 | if (isIE10OrLower || isIE11) { 6 | window.location.href = 'unsupported.html' 7 | } 8 | -------------------------------------------------------------------------------- /src/shims-tsx.d.ts: -------------------------------------------------------------------------------- 1 | import Vue, { VNode } from 'vue' 2 | 3 | declare global { 4 | namespace JSX { 5 | // tslint:disable no-empty-interface 6 | interface Element extends VNode {} 7 | // tslint:disable no-empty-interface 8 | interface ElementClass extends Vue {} 9 | interface IntrinsicElements { 10 | [elem: string]: any 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Dockerfile.prod: -------------------------------------------------------------------------------- 1 | # build stage 2 | FROM node:current-alpine as build-stage 3 | WORKDIR /app 4 | COPY package.json yarn.lock ./ 5 | RUN yarn install 6 | COPY . . 7 | RUN yarn run build 8 | 9 | # production stage 10 | FROM nginx:stable-alpine as production-stage 11 | RUN sed -i "s/80;/8080;/g" /etc/nginx/conf.d/*.conf 12 | COPY --from=build-stage /app/dist /usr/share/nginx/html 13 | EXPOSE 8080 14 | CMD ["nginx", "-g", "daemon off;"] 15 | 16 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:alpine 2 | 3 | # set working directory 4 | WORKDIR /app 5 | 6 | # add `/app/node_modules/.bin` to $PATH 7 | ENV PATH /app/node_modules/.bin:$PATH 8 | RUN yarn global add @vue/cli@3.7.0 9 | 10 | # install and cache app dependencies 11 | COPY package.json /app/package.json 12 | RUN yarn install 13 | COPY . . 14 | RUN yarn run lint 15 | RUN yarn run build 16 | 17 | 18 | # start app 19 | EXPOSE 8080 20 | CMD ["yarn", "serve"] -------------------------------------------------------------------------------- /src/util/country-service.ts: -------------------------------------------------------------------------------- 1 | import { CountryDto } from '@/api/ImisSwaggerApi' 2 | import Api from '@/api' 3 | 4 | let countries: CountryDto[] = [] 5 | 6 | export async function getCountries(): Promise { 7 | if (countries.length > 0) { 8 | return countries 9 | } else { 10 | const countriesBackend = await Api.getCountriesUsingGet() 11 | countries = countriesBackend 12 | return countriesBackend 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | 4 | import { createStore, Module } from 'vuex-smart-module' 5 | import { authModule } from './modules/auth.module' 6 | import { patientModule } from './modules/patients.module' 7 | 8 | Vue.use(Vuex) 9 | 10 | const root = new Module({ 11 | modules: { 12 | authModule, 13 | patientModule, 14 | }, 15 | }) 16 | 17 | const store = createStore(root) 18 | export default store 19 | -------------------------------------------------------------------------------- /tests/unit/example.spec.ts: -------------------------------------------------------------------------------- 1 | import Header from '@/components/structural/Header.vue' 2 | import { shallowMount } from '@vue/test-utils' 3 | import { expect } from 'chai' 4 | 5 | describe('Header.vue', () => { 6 | it('renders props.msg when passed', () => { 7 | const msg = 'IMIS' 8 | const wrapper = shallowMount(Header, { 9 | stubs: ['a-layout-header', 'a-icon'], 10 | }) 11 | expect(wrapper.text()).to.include(msg) 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /src/util/export-service.ts: -------------------------------------------------------------------------------- 1 | export function downloadCsv(csvString: string, filename: string | undefined) { 2 | const blob = new Blob([csvString], { type: 'text/csv;charset=utf-8;' }) 3 | const link = document.createElement('a') 4 | link.setAttribute('href', URL.createObjectURL(blob)) 5 | link.setAttribute('download', filename || 'data.csv') 6 | document.body.appendChild(link) // Required for FF 7 | link.click() 8 | document.body.removeChild(link) // Required for FF 9 | } 10 | -------------------------------------------------------------------------------- /src/models/test-materials.ts: -------------------------------------------------------------------------------- 1 | export type TestMaterial = 'RACHENABSTRICH' | 'NASENABSTRICH' | 'VOLLBLUT' 2 | 3 | export interface TestMaterialItem { 4 | id: TestMaterial 5 | label: string 6 | } 7 | 8 | export const testMaterials: TestMaterialItem[] = [ 9 | { 10 | id: 'RACHENABSTRICH', 11 | label: 'Rachenabstrich', 12 | }, 13 | { 14 | id: 'NASENABSTRICH', 15 | label: 'Nasenabstrich', 16 | }, 17 | { 18 | id: 'VOLLBLUT', 19 | label: 'Vollblut', 20 | }, 21 | ] 22 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import Antd from 'ant-design-vue' 2 | import 'ant-design-vue/dist/antd.css' 3 | import Vue from 'vue' 4 | import Root from './Root.vue' 5 | // important to import router before the store and authModule!! otherwise it breaks 6 | import router from './router' 7 | import { authModule } from '@/store/modules/auth.module' 8 | import store from './store' 9 | // import './registerServiceWorker' remove for now 10 | 11 | Vue.config.productionTip = false 12 | 13 | Vue.use(Antd) 14 | 15 | authModule.context(store).actions.init() 16 | 17 | new Vue({ 18 | router, 19 | store, 20 | render: (h) => h(Root), 21 | }).$mount('#app') 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /public/unsupported.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IMIS - Browser nicht unterstützt 6 | 7 | 8 |
9 | 10 |

Browser nicht unterstützt

11 |

Bitte verwenden Sie einen aktuellen Browser. Folgende Browser werden unterstützt:

12 | 18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /src/models/risk-occupation.ts: -------------------------------------------------------------------------------- 1 | import { RiskOccupation } from '@/models/index' 2 | 3 | export interface RiskOccupationOption { 4 | label: string 5 | value: RiskOccupation 6 | } 7 | 8 | export const RISK_OCCUPATIONS: RiskOccupationOption[] = [ 9 | { value: 'DOCTOR', label: 'Arzt/Ärztin' }, 10 | { value: 'CAREGIVER', label: 'Altenpfleger-in' }, 11 | { 12 | value: 'FIRE_FIGHTER_POLICE', 13 | label: 'Gefahrenabwehr (Polizei, Feuerwehr usw.)', 14 | }, 15 | { value: 'NURSE', label: 'Krankenpfleger-in' }, 16 | { value: 'TEACHER', label: 'Lehrer-in/Kindergärtner-in' }, 17 | { value: 'PUBLIC_ADMINISTRATION', label: 'Öffentliche Verwaltung' }, 18 | { value: 'STUDENT', label: 'Schüler-in' }, 19 | { 20 | value: 'NO_RISK_OCCUPATION', 21 | label: 'Anderer', 22 | }, 23 | ] 24 | -------------------------------------------------------------------------------- /src/util/index.ts: -------------------------------------------------------------------------------- 1 | export function parseJwt(token: string): any { 2 | const base64Url = token.split('.')[1] 3 | const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/') 4 | const jsonPayload = decodeURIComponent( 5 | atob(base64) 6 | .split('') 7 | .map(function (c) { 8 | return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2) 9 | }) 10 | .join('') 11 | ) 12 | return JSON.parse(jsonPayload) 13 | } 14 | 15 | export function anonymizeProperties(keys: any[], obj: any) { 16 | keys.forEach((key) => { 17 | if (typeof key === 'string' && obj[key]) { 18 | obj[key] = obj[key].substr(0, 1) + '**********' 19 | } else if (typeof key === 'object' && key.type === 'number' && obj[key]) { 20 | obj[key.key] = 11111 21 | } 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /src/util/plz-service.ts: -------------------------------------------------------------------------------- 1 | const safeParseResponse = (response: Response): Promise => 2 | response 3 | .json() 4 | .then((data) => data) 5 | .catch((e) => response.text) 6 | 7 | export interface PlzFields { 8 | plz: string 9 | note: string // city 10 | } 11 | 12 | export interface Plz { 13 | fields: PlzFields 14 | } 15 | 16 | export async function getPlzs(plz: string): Promise { 17 | return fetch( 18 | 'https://public.opendatasoft.com/api/records/1.0/search/?dataset=postleitzahlen-deutschland&facet=plz&q=' + 19 | plz, 20 | { 21 | method: 'GET', 22 | } 23 | ).then(async (response) => { 24 | const data = await safeParseResponse(response) 25 | if (!response.ok) throw data 26 | return data.records 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (optional):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | 32 | **Additional context** 33 | Add any other context about the problem here. 34 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | IMIS – Infektions Melde und Informations System 10 | 11 | 12 | 16 |
17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/util/mapping.ts: -------------------------------------------------------------------------------- 1 | export declare interface MapperDefEntry { 2 | transform: (x: any) => any 3 | } 4 | export declare type MapperDef = { 5 | [x: string]: (x: any) => any | Partial 6 | } 7 | 8 | export function map(object: { [x: string]: any }, mapperDef: MapperDef) { 9 | return Object.fromEntries( 10 | Object.entries(object).map((entry: [any, any]) => { 11 | const key = entry[0] 12 | let val = entry[1] 13 | 14 | let mapperEntry: any = mapperDef[key] 15 | 16 | if (mapperEntry) { 17 | if (typeof mapperEntry === 'function') { 18 | mapperEntry = { transform: mapperEntry as (value: any) => any } 19 | } 20 | 21 | if (Object.prototype.hasOwnProperty.call(mapperEntry, 'transform')) { 22 | val = mapperEntry.transform(val) 23 | } 24 | 25 | entry = [key, val] 26 | } 27 | 28 | return entry 29 | }) 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "noImplicitThis": true, 7 | "jsx": "preserve", 8 | "importHelpers": true, 9 | "moduleResolution": "node", 10 | "experimentalDecorators": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "sourceMap": true, 14 | "baseUrl": ".", 15 | "types": [ 16 | "webpack-env", 17 | "mocha", 18 | "chai" 19 | ], 20 | "paths": { 21 | "@/*": [ 22 | "src/*" 23 | ] 24 | }, 25 | "lib": [ 26 | "esnext", 27 | "dom", 28 | "dom.iterable", 29 | "scripthost" 30 | ] 31 | }, 32 | "include": [ 33 | "src/**/*.ts", 34 | "src/**/*.tsx", 35 | "src/**/*.vue", 36 | "tests/**/*.ts", 37 | "tests/**/*.tsx" 38 | ], 39 | "exclude": [ 40 | "node_modules" 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /src/components/inputs/TestInput.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: 'vue-eslint-parser', 4 | plugins: ['@typescript-eslint'], 5 | env: { 6 | node: true, 7 | }, 8 | extends: [ 9 | 'plugin:vue/essential', 10 | 'eslint:recommended', 11 | '@vue/typescript/recommended', 12 | '@vue/prettier', 13 | '@vue/prettier/@typescript-eslint', 14 | ], 15 | parserOptions: { 16 | parser: '@typescript-eslint/parser', 17 | ecmaVersion: 2020, 18 | }, 19 | rules: { 20 | '@typescript-eslint/no-extra-semi': 'error', 21 | '@typescript-eslint/no-explicit-any': 'off', 22 | // Does not work with delimiter "none" even though documentation says otherwise: 23 | '@typescript-eslint/member-delimiter-style': 'off', 24 | }, 25 | overrides: [ 26 | { 27 | files: [ 28 | 'src/**/*.{js,ts,vue}', 29 | '**/__tests__/*.{ts,js,vue}', 30 | '**/tests/unit/**/*.spec.{ts,js,vue}', 31 | ], 32 | env: { 33 | mocha: true, 34 | }, 35 | }, 36 | ], 37 | } 38 | -------------------------------------------------------------------------------- /src/registerServiceWorker.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | import { register } from 'register-service-worker' 4 | 5 | if (process.env.NODE_ENV === 'production') { 6 | register(`${process.env.BASE_URL}service-worker.js`, { 7 | ready() { 8 | console.log( 9 | 'App is being served from cache by a service worker.\n' + 10 | 'For more details, visit https://goo.gl/AFskqB' 11 | ) 12 | }, 13 | registered() { 14 | console.log('Service worker has been registered.') 15 | }, 16 | cached() { 17 | console.log('Content has been cached for offline use.') 18 | }, 19 | updatefound() { 20 | console.log('New content is downloading.') 21 | }, 22 | updated() { 23 | console.log('New content is available; please refresh.') 24 | }, 25 | offline() { 26 | console.log( 27 | 'No internet connection found. App is running in offline mode.' 28 | ) 29 | }, 30 | error(error) { 31 | console.error('Error during service worker registration:', error) 32 | }, 33 | }) 34 | } 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # User-specific stuff 2 | /.idea 3 | /.gradle 4 | .idea 5 | .gradle 6 | .DS_Store 7 | client/.vscode 8 | .vscode 9 | **/.project 10 | **/.settings 11 | 12 | # File-based project format 13 | *.iws 14 | 15 | # IntelliJ 16 | out/ 17 | 18 | # mpeltonen/sbt-idea plugin 19 | .idea_modules/ 20 | 21 | # JIRA plugin 22 | atlassian-ide-plugin.xml 23 | 24 | # Crashlytics plugin (for Android Studio and IntelliJ) 25 | com_crashlytics_export_strings.xml 26 | crashlytics.properties 27 | crashlytics-build.properties 28 | fabric.properties 29 | 30 | # Log file 31 | *.log 32 | 33 | # BlueJ files 34 | *.ctxt 35 | 36 | # Mobile Tools for Java (J2ME) 37 | .mtj.tmp/ 38 | 39 | # Package Files # 40 | *.jar 41 | *.war 42 | *.nar 43 | *.ear 44 | *.zip 45 | *.tar.gz 46 | *.rar 47 | 48 | # local env files 49 | .env.local 50 | .env.*.local 51 | 52 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 53 | hs_err_pid* 54 | /.project 55 | 56 | ### Node 57 | node_modules 58 | dist 59 | 60 | # Log files 61 | npm-debug.log* 62 | yarn-debug.log* 63 | yarn-error.log* 64 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Prod 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | setup-build-publish-deploy: 9 | name: Deploy 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | 14 | - name: Setup Node 15 | uses: actions/setup-node@v2 16 | with: 17 | node-version: '14' 18 | 19 | - name: Get yarn cache 20 | id: yarn-cache 21 | run: echo "::set-output name=dir::$(yarn cache dir)" 22 | 23 | - name: Cache dependencies 24 | uses: actions/cache@v2 25 | with: 26 | path: ${{ steps.yarn-cache.outputs.dir }} 27 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 28 | restore-keys: | 29 | ${{ runner.os }}-yarn- 30 | 31 | - run: yarn install --frozen-lockfile 32 | - run: yarn build 33 | 34 | - run: echo "imis-prototyp.de" > CNAME 35 | 36 | - name: Deploy to GH-Pages 37 | uses: peaceiris/actions-gh-pages@v3 38 | with: 39 | github_token: ${{ secrets.GITHUB_TOKEN }} 40 | publish_dir: ./dist 41 | -------------------------------------------------------------------------------- /src/models/symptoms.ts: -------------------------------------------------------------------------------- 1 | import { Option } from '@/models/index' 2 | 3 | export const SYMPTOMS: Option[] = [ 4 | { 5 | label: 'Appetitverlust', 6 | value: 'LOSS_OF_APPETITE', 7 | }, 8 | { 9 | label: 'Atembeschwerden', 10 | value: 'DIFFICULTY_BREATHING', 11 | }, 12 | { 13 | label: 'Atemnot', 14 | value: 'SHORTNESS_OF_BREATH', 15 | }, 16 | { 17 | label: 'Fieber', 18 | value: 'FEVER', 19 | }, 20 | { 21 | label: 'Gewichtsverlust', 22 | value: 'WEIGHT_LOSS', 23 | }, 24 | { 25 | label: 'Husten', 26 | value: 'COUGH', 27 | }, 28 | { 29 | label: 'Kopfschmerzen', 30 | value: 'HEADACHE', 31 | }, 32 | { 33 | label: 'Muskelschmerzen', 34 | value: 'MUSCLE_PAIN', 35 | }, 36 | { 37 | label: 'Rückenschmerzen', 38 | value: 'BACK_PAIN', 39 | }, 40 | { 41 | label: 'Schnupfen', 42 | value: 'COLD', 43 | }, 44 | { 45 | label: 'Übelkeit', 46 | value: 'NAUSEA', 47 | }, 48 | { 49 | label: 'Verlust des Geruchs- und/oder Geschmackssinnes', 50 | value: 'LOSS_OF_SENSE_OF_SMELL_TASTE', 51 | }, 52 | ] 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 ImisDevelopers 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - feature/* 7 | - master 8 | pull_request: 9 | branches: 10 | - master 11 | types: 12 | - opened 13 | - closed 14 | - synchronize 15 | 16 | jobs: 17 | setup-build-publish-deploy: 18 | name: Deploy 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v2 22 | 23 | - name: Setup Node 24 | uses: actions/setup-node@v2 25 | with: 26 | node-version: '14' 27 | 28 | - name: Get yarn cache 29 | id: yarn-cache 30 | run: echo "::set-output name=dir::$(yarn cache dir)" 31 | 32 | - name: Cache dependencies 33 | uses: actions/cache@v2 34 | with: 35 | path: ${{ steps.yarn-cache.outputs.dir }} 36 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 37 | restore-keys: | 38 | ${{ runner.os }}-yarn- 39 | 40 | - run: yarn install --frozen-lockfile 41 | - run: yarn build 42 | 43 | - run: echo "staging.imis-prototyp.de" > CNAME 44 | 45 | - name: Deploy to GH-Pages 46 | uses: peaceiris/actions-gh-pages@v3 47 | with: 48 | github_token: ${{ secrets.GITHUB_TOKEN }} 49 | publish_dir: ./dist 50 | -------------------------------------------------------------------------------- /src/components/other/IndexPatientTableCell.vue: -------------------------------------------------------------------------------- 1 | 12 | 45 | 46 | -------------------------------------------------------------------------------- /src/util/typing.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Typescript utility module. 3 | */ 4 | 5 | /// Type representing a Type parameter to be passed to a type-inferring function. 6 | export type TypeArg = T 7 | 8 | /** 9 | * TypeArg generator. This function may be called for any TypeArg function 10 | * argument to provide a type for type inferrence. 11 | * 12 | * Note that this function's sole purpose is type inferrence and no actual 13 | * object is created. 14 | */ 15 | export function TypeArg(): TypeArg { 16 | return (null as unknown) as TypeArg 17 | } 18 | 19 | /** 20 | * Simple cast avoiding explicit conversion to unknown. 21 | */ 22 | export function cast(arg: unknown, _?: TypeArg) { 23 | return arg as T 24 | } 25 | 26 | /** 27 | * Identity function telling the Typescript compiler that the supplied argument 28 | * is additionally supporting data and operations from the given extension type. 29 | * This function may be used in contexts where current type inferrence mechanisms 30 | * fail at resolving all of the functionality a type is actually capable of. 31 | * 32 | * An example use case is the use of mixins or injections for Vue components, 33 | * which currently cannot be sufficiently inferred. 34 | */ 35 | export function extended( 36 | obj: T, 37 | _: TypeArg 38 | ): T & ExtensionType { 39 | return obj as T & ExtensionType 40 | } 41 | -------------------------------------------------------------------------------- /src/models/pre-illnesses.ts: -------------------------------------------------------------------------------- 1 | import { Option } from '@/models/index' 2 | 3 | export const ADDITIONAL_PRE_ILLNESSES: Option[] = [ 4 | { 5 | label: 'Akutes schweres Atemsyndrom (ARDS)', 6 | value: 'ARDS', 7 | }, 8 | { 9 | label: 'Beatmungspflichtige Atemwegserkrankung', 10 | value: 'RESPIRATORY_DISEASE', 11 | }, 12 | ] 13 | 14 | export const PRE_ILLNESSES: Option[] = [ 15 | { 16 | label: 'Chronische Lungenerkrankung (z.B. COPD)', 17 | value: 'CHRONIC_LUNG_DISEASE', 18 | }, 19 | { 20 | label: 'Diabetes', 21 | value: 'DIABETES', 22 | }, 23 | { 24 | label: 'Fettleibigkeit', 25 | value: 'ADIPOSITAS', 26 | }, 27 | { 28 | label: 'Herz-Kreislauf (inkl. Bluthochdruck)', 29 | value: 'CARDIOVASCULAR_DISEASE', 30 | }, 31 | { 32 | label: 'Immundefizit (inkl. HIV)', 33 | value: 'IMMUNODEFICIENCY', 34 | }, 35 | { 36 | label: 'Krebserkrankung', 37 | value: 'CANCER', 38 | }, 39 | { 40 | label: 'Lebererkrankung', 41 | value: 'LIVER_DISEASE', 42 | }, 43 | { 44 | label: 'Neurologische / neuromuskuläre Erkrankung', 45 | value: 'NEUROLOGICAL_DISEASE', 46 | }, 47 | { 48 | label: 'Nierenerkrankung', 49 | value: 'KIDNEY_DISEASE', 50 | }, 51 | { 52 | label: 'Raucher', 53 | value: 'SMOKING', 54 | }, 55 | { 56 | label: 'Schwangerschaft', 57 | value: 'PREGNANCY', 58 | }, 59 | ] 60 | -------------------------------------------------------------------------------- /src/components/structural/Navigation.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/assets/global.scss: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300;0,400;0,700;1,300;1,400;1,700&display=swap"); 2 | 3 | body { 4 | font-family: "Open Sans", Helvetica, Arial, sans-serif; 5 | } 6 | 7 | .imis-table-no-pagination { 8 | .ant-table-pagination { 9 | display: none; 10 | } 11 | } 12 | 13 | #app { 14 | -webkit-font-smoothing: antialiased; 15 | -moz-osx-font-smoothing: grayscale; 16 | color: #2c3e50; 17 | } 18 | 19 | #nav { 20 | padding: 30px; 21 | 22 | a { 23 | font-weight: bold; 24 | color: #2c3e50; 25 | 26 | &.router-link-exact-active { 27 | color: #42b983; 28 | } 29 | } 30 | } 31 | 32 | // CSS classes used by multiple components 33 | .imis-radio-group { 34 | display: flex; 35 | flex-direction: row; 36 | align-items: normal; 37 | 38 | > label { 39 | padding: 10px 0 10px 15px; 40 | display: flex; 41 | align-items: center; 42 | } 43 | 44 | > label:hover { 45 | background: rgba(0, 0, 0, 0.1); 46 | } 47 | 48 | .ant-radio { 49 | margin-right: 10px; 50 | } 51 | 52 | i { 53 | margin-right: 10px; 54 | } 55 | } 56 | 57 | .ant-divider-horizontal { 58 | margin: 16px 0; 59 | } 60 | 61 | h4 { 62 | font-weight: bold; 63 | margin-bottom: 1em; 64 | } 65 | 66 | .fading-enter-active, 67 | .fading-leave-active { 68 | transition: opacity 0.2s; 69 | } 70 | .fading-enter, .fading-leave-to /* .fade-leave-active below version 2.1.8 */ { 71 | opacity: 0; 72 | } 73 | 74 | .ant-form-item { 75 | margin-bottom: 10px; 76 | } 77 | -------------------------------------------------------------------------------- /src/models/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CreateInstitutionDTO, 3 | RegisterUserRequest, 4 | TestIncident, 5 | HospitalizationIncident, 6 | QuarantineIncident, 7 | AdministrativeIncident, 8 | } from '@/api/ImisSwaggerApi' 9 | 10 | export type InstitutionType = Exclude< 11 | CreateInstitutionDTO['institutionType'], 12 | undefined 13 | > 14 | export type InstitutionRole = 15 | | 'ROLE_TEST_SITE' 16 | | 'ROLE_LABORATORY' 17 | | 'ROLE_DOCTORS_OFFICE' 18 | | 'ROLE_CLINIC' 19 | | 'ROLE_GOVERNMENT_AGENCY' 20 | | 'ROLE_DEPARTMENT_OF_HEALTH' 21 | export type UserRole = Exclude 22 | export type PatientStatus = 23 | | 'REGISTERED' 24 | | 'SUSPECTED' 25 | | 'ORDER_TEST' 26 | | 'SCHEDULED_FOR_TESTING' 27 | | 'TEST_SUBMITTED' 28 | | 'TEST_FINISHED_POSITIVE' 29 | | 'TEST_FINISHED_NEGATIVE' 30 | | 'PATIENT_DEAD' 31 | | 'DOCTORS_VISIT' 32 | | 'QUARANTINE_SELECTED' 33 | | 'QUARANTINE_MANDATED' 34 | | 'QUARANTINE_RELEASED' 35 | | 'QUARANTINE_PROFESSIONBAN_RELEASED' 36 | | 'HOSPITALIZATION_MANDATED' 37 | | 'HOSPITALIZATION_RELEASED' 38 | | 'CASE_DATA_UPDATED' 39 | export type RiskOccupation = 40 | | 'NO_RISK_OCCUPATION' 41 | | 'FIRE_FIGHTER_POLICE' 42 | | 'TEACHER' 43 | | 'PUBLIC_ADMINISTRATION' 44 | | 'STUDENT' 45 | | 'DOCTOR' 46 | | 'CAREGIVER' 47 | | 'NURSE' 48 | 49 | export type Incident = 50 | | TestIncident 51 | | HospitalizationIncident 52 | | QuarantineIncident 53 | | AdministrativeIncident 54 | 55 | export interface Option { 56 | label: string 57 | value: string 58 | } 59 | -------------------------------------------------------------------------------- /src/components/inputs/DateInput.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /src/components/inputs/LaboratoryInput.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /src/components/inputs/PlzInput.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /src/views/Dashboard.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 39 | 40 | 41 | 87 | -------------------------------------------------------------------------------- /src/api/index.ts: -------------------------------------------------------------------------------- 1 | import { Api, RequestParams } from '@/api/ImisSwaggerApi' 2 | 3 | let baseUrl: string = window.location.origin 4 | 5 | if ( 6 | location.host.includes('localhost') || 7 | location.host.includes('127.0.0.1') 8 | ) { 9 | baseUrl = 'http://localhost:80' 10 | // Alternative config to run the app locally without root; see proxy conf 11 | // baseUrl = 'http://localhost:8080/api' 12 | } 13 | /** 14 | * The npm package that creates the swagger client does not have a option 15 | * to change headers, but after sign in we have to set the jwt token 16 | * To to this we have to reinitialize the Api. 17 | * 18 | * To ensure all components always use the current api we use a proxy that 19 | * returns the correct Api object function 20 | * 21 | */ 22 | 23 | const baseApiParams: RequestParams = { 24 | credentials: 'same-origin', 25 | headers: { 26 | 'Content-Type': 'application/json', 27 | }, 28 | redirect: 'follow', 29 | referrerPolicy: 'no-referrer', 30 | } 31 | 32 | const apiWrapper = { 33 | apiInstance: new Api({ 34 | baseUrl: baseUrl, 35 | baseApiParams: baseApiParams, 36 | }), 37 | } 38 | 39 | function createApiProxy(foo: Api['api']): Api['api'] { 40 | // Proxy is compatible with Foo 41 | const handler = { 42 | get: (target: Api['api'], prop: keyof Api['api'], receiver: any) => { 43 | if (apiWrapper.apiInstance.api[prop] !== null) { 44 | return apiWrapper.apiInstance.api[prop] 45 | } 46 | 47 | return Reflect.get(target, prop, receiver) 48 | }, 49 | } 50 | return new Proxy(foo, handler) 51 | } 52 | 53 | export function setBearerToken(token: string) { 54 | apiWrapper.apiInstance = new Api({ 55 | baseUrl: baseUrl, 56 | baseApiParams: { 57 | ...baseApiParams, 58 | headers: { 59 | ...baseApiParams.headers, 60 | Authorization: 'Bearer ' + token, 61 | }, 62 | }, 63 | }) 64 | } 65 | 66 | export function removeBearerToken() { 67 | apiWrapper.apiInstance = new Api({ 68 | baseUrl: baseUrl, 69 | }) 70 | } 71 | 72 | export default createApiProxy(apiWrapper.apiInstance.api) 73 | -------------------------------------------------------------------------------- /src/models/exposures.ts: -------------------------------------------------------------------------------- 1 | import { Option } from '@/models/index' 2 | 3 | // All Exposure items: 4 | 5 | const MEDICAL_HEALTH_PROFESSION = { 6 | label: 'Medizinischer Heilberuf', 7 | value: 'MEDICAL_HEALTH_PROFESSION', 8 | } 9 | const MEDICAL_LABORATORY = { 10 | label: 'Arbeit in medizinischem Labor', 11 | value: 'MEDICAL_LABORATORY', 12 | } 13 | 14 | const STAY_IN_MEDICAL_FACILITY = { 15 | label: 16 | 'Aufenthalt in medizinischer Einrichtung in den letzten 14 Tagen vor der Erkrankung', 17 | value: 'STAY_IN_MEDICAL_FACILITY', 18 | } 19 | 20 | const CONTACT_WITH_CORONA_CASE = { 21 | label: 22 | 'Enger Kontakt mit wahrscheinlichem oder bestätigtem Fall in den letzten 14 Tagen vor der Erkrankung', 23 | value: 'CONTACT_WITH_CORONA_CASE', 24 | } 25 | 26 | const COMMUNITY_FACILITY = { 27 | label: 28 | 'Arbeit in Gemeinschaftseinrichtung (z.B. Schule, Kinderkrippe, Heim, sonst. Massenunterkünfte (§§34 und 36 Abs. 1 IfSG))', 29 | value: 'COMMUNITY_FACILITY', 30 | } 31 | 32 | const COMMUNITY_FACILITY_MINORS = { 33 | label: 34 | 'Betreuung in Gemeinschaftseinrichtung für Kinder oder Jugendliche, z.B.Schule, Kinderkrippe (§33 IfSG)', 35 | value: 'COMMUNITY_FACILITY_MINORS', 36 | } 37 | 38 | // We want to display different items depending on self-registration or registration by doctor 39 | 40 | // Registration By Doctor 41 | export const EXPOSURES_INTERNAL: Option[] = [ 42 | MEDICAL_HEALTH_PROFESSION, 43 | MEDICAL_LABORATORY, 44 | STAY_IN_MEDICAL_FACILITY, 45 | COMMUNITY_FACILITY, 46 | COMMUNITY_FACILITY_MINORS, 47 | CONTACT_WITH_CORONA_CASE, 48 | ] 49 | 50 | // Self-Registration 51 | export const EXPOSURES_PUBLIC: Option[] = [ 52 | MEDICAL_HEALTH_PROFESSION, 53 | MEDICAL_LABORATORY, 54 | STAY_IN_MEDICAL_FACILITY, 55 | CONTACT_WITH_CORONA_CASE, 56 | ] 57 | 58 | export const EXPOSURE_LOCATIONS: Option[] = [ 59 | { 60 | label: 'in einer medizinischen Einrichtung', 61 | value: 'MEDICAL_FACILITY', 62 | }, 63 | { 64 | label: 'im privaten Haushalt', 65 | value: 'PRIVATE', 66 | }, 67 | { 68 | label: 'am Arbeitsplatz', 69 | value: 'WORK', 70 | }, 71 | { 72 | label: 'andere / sonstige', 73 | value: 'OTHER', 74 | }, 75 | ] 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # This repository is deprecated! 2 | 3 | # IMIS 4 | ###### Infektions Melde und Informations System 5 | 6 | [![IMIS Cover](public/web-imis.png)](http://www.youtube.com/watch?v=XIIlMh3Lbsc "Pitch") 7 | 8 | * [Demo](https://imis-prototyp.de) 9 | * [Video pitch](https://www.youtube.com/watch?v=XIIlMh3Lbsc) 10 | 11 | Dieses Projekt entstand im Rahmen des [#WirvsVirus](https://wirvsvirushackathon.org/)-Hackathon. 12 | 13 | * [Organization - Google Docs](https://docs.google.com/document/d/1nEf7WGs6BJ9qcHcuUoVzV1i01kIPH0ENQihb6B7yiI4/edit?usp=sharing) 14 | * [DevPost submission](https://devpost.com/software/imis-infektions-melde-und-informations-system) 15 | * Mit freundlicher Unterstützung von [https://covidmeldeprozess.de/](https://covidmeldeprozess.de/) 16 | 17 | # Development 18 | ### [Prod](https://imis-prototyp.de) ![Test](https://github.com/ImisDevelopers/1_011_a_infektionsfall_uebermittellung/workflows/Build%20and%20Test/badge.svg?branch=master&event=push) ![Deploy](https://github.com/ImisDevelopers/1_011_a_infektionsfall_uebermittellung/workflows/Deploy/badge.svg) ![GitHub release (latest by date)](https://img.shields.io/github/v/release/imisDevelopers/1_011_a_infektionsfall_uebermittellung?color=green) 19 | 20 | ## General Guidelines 21 | 22 | Development happens in `master` using feature branches and PR. 23 | `master` branch is deployed at: 24 | 25 | * [Staging Deployment](https://staging.imis-prototyp.de) 26 | 27 | ## Tech Stack 28 | 29 | We are using: 30 | - Vue.js + Typescript 31 | - VueX + [vuex-smart-module](https://github.com/ktsn/vuex-smart-module) 32 | - Vue Router 33 | - [Ant Design](https://www.antdv.com/) 34 | - swagger-typescript-api 35 | - Deployment: Google Kubernetes Engine (GKE) 36 | 37 | ### Requirements 38 | 1. YARN 39 | - https://classic.yarnpkg.com/en/docs/install 40 | 41 | ### Frontend 42 | 1. Start local development server for vue.js development: 43 | ```yarn serve``` 44 | 45 | ## CI system 46 | All commits to `master`, `feature/*` and all PRs will be CI checked. 47 | 48 | New commit to `master` will result in new release to `staging.imis-prototyp.de`. 49 | 50 | A new release to `imis-prototyp.de` is not triggerd by commit on `master` but by a new release tag. 51 | -------------------------------------------------------------------------------- /src/components/other/QuarantineHospitalizationCard.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 67 | 68 | 88 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "IMIS", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "test:unit": "echo // Test gibts aktuell nicht - bitte fixen und dann wieder setzen: \"test:unit\": \"vue-cli-service test:unit\",", 9 | "lint": "vue-cli-service lint --fix", 10 | "generate:api-client": "npx swagger-typescript-api -p ../server/build/resources/swagger.json -o ./src/api -n SwaggerApi.ts", 11 | "generate:api-client-live": "npx swagger-typescript-api -p http://localhost:8642/v2/api-docs -o ./src/api -n SwaggerApi.ts", 12 | "generate:api-client-local:80": "npx swagger-typescript-api -p http://localhost/v2/api-docs -o ./src/api -n SwaggerApi.ts" 13 | }, 14 | "dependencies": { 15 | "@zxing/library": "^0.16.0", 16 | "ant-design-vue": "^1.5.0", 17 | "core-js": "^3.6.4", 18 | "moment": "^2.24.0", 19 | "register-service-worker": "^1.6.2", 20 | "vue": "^2.6.11", 21 | "vue-router": "^3.1.5", 22 | "vue-typed-mixins": "^0.2.0", 23 | "vuex": "^3.1.2", 24 | "vuex-smart-module": "^0.3.4" 25 | }, 26 | "devDependencies": { 27 | "@types/chai": "^4.2.8", 28 | "@types/mocha": "^5.2.4", 29 | "@typescript-eslint/eslint-plugin": "^2.18.0", 30 | "@typescript-eslint/parser": "^2.18.0", 31 | "@vue/cli-plugin-babel": "~4.2.0", 32 | "@vue/cli-plugin-eslint": "~4.2.0", 33 | "@vue/cli-plugin-pwa": "~4.2.0", 34 | "@vue/cli-plugin-router": "~4.2.0", 35 | "@vue/cli-plugin-typescript": "~4.2.0", 36 | "@vue/cli-plugin-unit-mocha": "~4.2.0", 37 | "@vue/cli-plugin-vuex": "~4.2.0", 38 | "@vue/cli-service": "~4.2.0", 39 | "@vue/eslint-config-prettier": "^6.0.0", 40 | "@vue/eslint-config-standard": "^5.1.0", 41 | "@vue/eslint-config-typescript": "^5.0.2", 42 | "@vue/test-utils": "1.0.0-beta.31", 43 | "chai": "^4.1.2", 44 | "eslint": "^6.7.2", 45 | "eslint-config-prettier": "^6.11.0", 46 | "eslint-plugin-import": "^2.20.1", 47 | "eslint-plugin-node": "^11.0.0", 48 | "eslint-plugin-prettier": "^3.1.3", 49 | "eslint-plugin-promise": "^4.2.1", 50 | "eslint-plugin-standard": "^4.0.0", 51 | "eslint-plugin-vue": "^6.1.2", 52 | "node-sass": "^4.14.1", 53 | "prettier": "^2.0.5", 54 | "sass-loader": "^8.0.2", 55 | "swagger-typescript-api": "1.8.2", 56 | "typescript": "~3.7.5", 57 | "vue-template-compiler": "^2.6.11" 58 | }, 59 | "engines": { 60 | "node": ">=14.3.0" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/components/modals/ChangePatientStammdatenForm.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /src/views/AppRoot.vue: -------------------------------------------------------------------------------- 1 | 50 | 79 | 80 | 85 | -------------------------------------------------------------------------------- /src/store/modules/patients.module.ts: -------------------------------------------------------------------------------- 1 | import Api from '@/api' 2 | import { Patient } from '@/api/ImisSwaggerApi' 3 | import { Vue } from 'vue/types/vue' 4 | import { 5 | Actions, 6 | createMapper, 7 | Getters, 8 | Module, 9 | Mutations, 10 | } from 'vuex-smart-module' 11 | 12 | class PatientState { 13 | patient: Patient | undefined 14 | patients: Patient[] = [] 15 | } 16 | 17 | class PatientGetters extends Getters { 18 | patientById(id: string): Patient | undefined { 19 | if (this.state.patient && this.state.patient.id === id) { 20 | return this.state.patient 21 | } 22 | return this.state.patients.find((patient) => patient.id === id) 23 | } 24 | } 25 | 26 | class PatientMutations extends Mutations { 27 | addPatients(patients: Patient[]) { 28 | this.state.patients.concat(patients) 29 | } 30 | 31 | setPatients(patients: Patient[]) { 32 | this.state.patients = patients 33 | } 34 | 35 | setPatient(patient: Patient) { 36 | this.state.patient = patient 37 | } 38 | } 39 | 40 | class PatientActions extends Actions< 41 | PatientState, 42 | PatientGetters, 43 | PatientMutations, 44 | PatientActions 45 | > { 46 | async fetchPatients(instance: Vue) { 47 | try { 48 | // this.commit('shared/startedLoading', 'fetchPatients', { root: true }) 49 | const patients = await Api.getAllPatientsUsingGet() 50 | this.commit('setPatients', patients) 51 | } catch (err) { 52 | instance.$notification.error({ 53 | message: 'Fehler', 54 | description: 'Patienten kontent nicht geladen werden', 55 | }) 56 | } 57 | // commit('shared/finishedLoading', 'fetchPatients', { root: true }) 58 | } 59 | 60 | async registerPatient(arg: { 61 | patient: Patient 62 | instance?: Vue 63 | }): Promise { 64 | // commit('shared/startedLoading', 'registerPatient', { root: true }) 65 | try { 66 | const patientResponse = await Api.addPatientUsingPost(arg.patient) 67 | this.commit('setPatient', patientResponse) 68 | return patientResponse 69 | } catch (err) { 70 | console.log(err) 71 | throw err 72 | } 73 | // commit('shared/finishedLoading', 'registerPatient', { root: true }) 74 | } 75 | } 76 | 77 | export const patientModule = new Module({ 78 | state: PatientState, 79 | getters: PatientGetters, 80 | mutations: PatientMutations, 81 | actions: PatientActions, 82 | }) 83 | 84 | export const patientMapper = createMapper(patientModule) 85 | -------------------------------------------------------------------------------- /src/models/event-types.ts: -------------------------------------------------------------------------------- 1 | import { PatientStatus } from '@/models/index' 2 | 3 | export interface EventTypeItem { 4 | id: PatientStatus 5 | label: string 6 | icon: string 7 | } 8 | 9 | export const eventTypes: EventTypeItem[] = [ 10 | { 11 | id: 'REGISTERED', 12 | label: 'Registriert', 13 | icon: 'login', 14 | }, 15 | { 16 | id: 'SUSPECTED', 17 | label: 'Verdachtsfall', 18 | icon: 'search', 19 | }, 20 | { 21 | id: 'ORDER_TEST', 22 | label: 'Test angefordert', 23 | icon: 'experiment', 24 | }, 25 | { 26 | id: 'SCHEDULED_FOR_TESTING', 27 | label: 'Wartet auf Test', 28 | icon: 'team', 29 | }, 30 | { 31 | id: 'TEST_SUBMITTED', 32 | label: 'Test eingereicht', 33 | icon: 'clock-circle', 34 | }, 35 | { 36 | id: 'TEST_FINISHED_POSITIVE', 37 | label: 'Test positiv', 38 | icon: 'check', 39 | }, 40 | { 41 | id: 'TEST_FINISHED_NEGATIVE', 42 | label: 'Test negativ', 43 | icon: 'stop', 44 | }, 45 | { 46 | id: 'PATIENT_DEAD', 47 | label: 'Verstorben', 48 | icon: 'cloud', 49 | }, 50 | { 51 | id: 'DOCTORS_VISIT', 52 | label: 'Arztbesuch', 53 | icon: 'reconciliation', 54 | }, 55 | { 56 | id: 'QUARANTINE_SELECTED', 57 | label: 'Quarantäne vorgemerkt', 58 | icon: 'safety', 59 | }, 60 | { 61 | id: 'QUARANTINE_MANDATED', 62 | label: 'Quarantäne angeordnet', 63 | icon: 'safety', 64 | }, 65 | { 66 | id: 'QUARANTINE_RELEASED', 67 | label: 'Quarantäne aufgehoben', 68 | icon: 'safety', 69 | }, 70 | { 71 | id: 'QUARANTINE_PROFESSIONBAN_RELEASED', 72 | label: 'Arbeitsverbot aufgehoben', 73 | icon: 'safety', 74 | }, 75 | { 76 | id: 'HOSPITALIZATION_MANDATED', 77 | label: 'Hospitalisiert', 78 | icon: 'safety', 79 | }, 80 | { 81 | id: 'HOSPITALIZATION_RELEASED', 82 | label: 'Hospitalisierung aufgehoben', 83 | icon: 'safety', 84 | }, 85 | { 86 | id: 'CASE_DATA_UPDATED', 87 | label: 'Symptome oder Erkrankungsdatum aktualisiert', 88 | icon: 'safety', 89 | }, 90 | ] 91 | 92 | export interface TestResultType { 93 | id: 'TEST_SUBMITTED' | 'TEST_POSITIVE' | 'TEST_NEGATIVE' 94 | label: string 95 | icon: string 96 | } 97 | export const testResults: TestResultType[] = [ 98 | { 99 | id: 'TEST_SUBMITTED', 100 | label: 'Test eingereicht', 101 | icon: 'login', 102 | }, 103 | { 104 | id: 'TEST_POSITIVE', 105 | label: 'Test positiv', 106 | icon: 'check', 107 | }, 108 | { 109 | id: 'TEST_NEGATIVE', 110 | label: 'Test negativ', 111 | icon: 'stop', 112 | }, 113 | ] 114 | -------------------------------------------------------------------------------- /src/components/form-groups/ExposureForm.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /src/components/other/TestIncidentsCard.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 95 | 96 | 108 | -------------------------------------------------------------------------------- /src/components/form-groups/PreIllnessesForm.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /src/components/other/History.vue: -------------------------------------------------------------------------------- 1 | 27 | 97 | 98 | -------------------------------------------------------------------------------- /src/components/inputs/PatientInput.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /src/components/structural/Header.vue: -------------------------------------------------------------------------------- 1 | 57 | 91 | 110 | -------------------------------------------------------------------------------- /src/util/permissions.ts: -------------------------------------------------------------------------------- 1 | import Api from '@/api' 2 | import { Api as ApiDefs } from '@/api/ImisSwaggerApi' 3 | 4 | export interface ApiParams { 5 | path: string 6 | method: string 7 | } 8 | export type ApiFunction = (...args: any[]) => any 9 | 10 | // Retrieve the request method and path for the given Swagger API function 11 | export function queryApiParams(apiFunc: ApiFunction): ApiParams { 12 | /* 13 | The following piece of code is a kind of ugly hack to find out the 14 | request method and path used in the given Swagger API. It operates 15 | by executing the exact same function on an own API copy with a 16 | surrogate `request` function that fetches path and method parameters 17 | to be returned as this function's result. 18 | */ 19 | 20 | // Step 1: Create API copy with surrogate `request` operation 21 | let resultParams = undefined as undefined | ApiParams 22 | const myApiDefs = new ApiDefs() as any 23 | myApiDefs.request = (path: string, method: string) => { 24 | resultParams = { 25 | path, 26 | method, 27 | } 28 | } 29 | 30 | // Step 2: Generate wildcard-parameters to be included in the function call 31 | const mockArgs = [] as string[] 32 | for (let i = 0; i < apiFunc.length - 1; i++) { 33 | mockArgs.push('*') 34 | } 35 | 36 | // Step 3: Do the function call on the own API copy 37 | myApiDefs.api[apiFunc.name](...mockArgs) 38 | 39 | if (!resultParams) { 40 | throw new Error('Could not extract request parameters from function') 41 | } else { 42 | return resultParams 43 | } 44 | } 45 | 46 | export async function checkAllowed(funcs: ApiFunction): Promise 47 | export async function checkAllowed( 48 | funcs: ApiFunction[] | undefined 49 | ): Promise 50 | export async function checkAllowed< 51 | T extends Record, 52 | R extends { [key in keyof T]: boolean } 53 | >(funcs: T): Promise 54 | // export function checkAllowed(funcs: Record): Record; 55 | export async function checkAllowed(funcs: any): Promise { 56 | const resultLabels = [] as string[] 57 | let singleResult = false 58 | if (!funcs) { 59 | funcs = Object.values((Api as any).api) 60 | } 61 | if (typeof funcs === 'object') { 62 | const funcsArr = [] as ApiFunction[] 63 | Object.entries(funcs).forEach((entry: [string, any]) => { 64 | resultLabels.push(entry[0]) 65 | funcsArr.push(entry[1]) 66 | }) 67 | 68 | funcs = funcsArr 69 | } else if (funcs && !Array.isArray(funcs)) { 70 | funcs = [funcs] 71 | singleResult = true 72 | } 73 | 74 | // const params = funcs.map(queryApiParams) 75 | 76 | // Make the permission asking request 77 | const apiResult = (await Api.queryPermissionsUsingPost( 78 | Object.fromEntries( 79 | funcs.map((func: ApiFunction) => [func.name, queryApiParams(func)]) 80 | ) 81 | )) as Record 82 | 83 | const result = [] as boolean[] 84 | for (let i = 0; i < funcs.length; i++) { 85 | result[i] = apiResult[funcs[i].name] 86 | } 87 | 88 | if (singleResult) { 89 | return result[0] 90 | } else { 91 | if (resultLabels) { 92 | const forReturn = {} as Record 93 | for (let i = 0; i < resultLabels.length; i++) { 94 | forReturn[resultLabels[i]] = result[i] 95 | } 96 | return forReturn 97 | } else { 98 | return result 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/views/PublicStatistics.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 134 | 135 | 136 | 151 | -------------------------------------------------------------------------------- /src/components/modals/ChangePasswordForm.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /src/util/search.ts: -------------------------------------------------------------------------------- 1 | const regexEscapables = /[.*+\-?^${}()|[\]\\]/g 2 | function escapeRegExp(val: string): string { 3 | return val.replace(regexEscapables, '\\$&') // $& means the whole matched string 4 | } 5 | 6 | /** 7 | * Options for TextMatcher. 8 | */ 9 | export interface TextMatcherOptions { 10 | /** 11 | * Whether only a single word is required for a match. Only effective if used 12 | * together with `separateWords` option. 13 | */ 14 | anyMatches: boolean 15 | /** 16 | * Whether to match ignoring casing. 17 | */ 18 | caseInsensitive: boolean 19 | /** 20 | * Whether spaces and punctuation separate words that are 21 | * matched separately. 22 | */ 23 | separateWords: boolean 24 | /** 25 | * Whether to calculate an additional score for matches; ideally, the higher 26 | * the score value, the better fitting the match. 27 | */ 28 | withScore: boolean 29 | } 30 | 31 | export interface TextMatcherResult { 32 | matches: boolean 33 | score: number 34 | } 35 | 36 | /** 37 | * Advanced search text matching facility. Allows separate matching of 38 | * words and a naive match score calculation. 39 | */ 40 | export class TextMatcher { 41 | // All regexps in this array need to match for an overall match 42 | searchRegex: RegExp[] 43 | options: TextMatcherOptions 44 | 45 | constructor(search: string, options?: Partial) { 46 | // Retrieve option set to use 47 | this.options = { 48 | anyMatches: false, 49 | caseInsensitive: true, 50 | separateWords: true, 51 | withScore: false, 52 | } 53 | 54 | if (options) { 55 | Object.assign(this.options, options) 56 | } 57 | 58 | let patterns = [] as string[][] 59 | if (!this.options.separateWords) { 60 | patterns = [[search]] 61 | } else { 62 | // Create a list of all search words to process 63 | const searchParts = [] as string[] 64 | 65 | search.split(/\s+|[-+,.]/g).forEach((part) => { 66 | if (part) searchParts.push(part) 67 | }) 68 | 69 | if (this.options.anyMatches) { 70 | // Single regexp, matching any word 71 | patterns.push(searchParts) 72 | } else { 73 | // One regexp for each word 74 | patterns = searchParts.map((part) => [part]) 75 | } 76 | } 77 | 78 | // Construct RegExps used for matching 79 | this.searchRegex = patterns.map((searchParts) => { 80 | let rawRegex = searchParts 81 | .map((entry) => `(${escapeRegExp(entry)})`) 82 | .join('|') 83 | 84 | // This regex structure allows repeated matches 85 | rawRegex = `${rawRegex}(?:.*?(?:${rawRegex}))*` 86 | 87 | return new RegExp( 88 | rawRegex, 89 | '' + (this.options.caseInsensitive ? 'i' : '') 90 | ) 91 | }) 92 | } 93 | 94 | /** 95 | * Tests the given text for a match with this matcher. If the option 96 | * `withScore` has been given during construction, the result will 97 | * also contain a naive score for the match. A higher score should in 98 | * general refer to a better match. 99 | */ 100 | match(text: string): TextMatcherResult { 101 | return this.searchRegex 102 | .map((re) => re.exec(text)) 103 | .map((matchResults) => { 104 | let score = 0 105 | if (matchResults && this.options.withScore) { 106 | // Count the number of groups that matched for score 107 | score = matchResults.reduce( 108 | (score, match) => score + (match ? 1 : 0), 109 | 0 110 | ) 111 | } 112 | 113 | return { 114 | matches: !!matchResults, 115 | score, 116 | } 117 | }) 118 | .reduce( 119 | (result, matchResult) => ({ 120 | matches: result.matches && matchResult.matches, 121 | score: result.score + matchResult.score, 122 | }), 123 | { matches: true, score: 0 } 124 | ) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/views/Login.vue: -------------------------------------------------------------------------------- 1 | 95 | 96 | 128 | 129 | 136 | -------------------------------------------------------------------------------- /src/components/inputs/BarcodeScanner.vue: -------------------------------------------------------------------------------- 1 | 35 | 105 | 149 | -------------------------------------------------------------------------------- /src/assets/wave-bg.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | wave-bg 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/components/form-groups/SymptomsForm.vue: -------------------------------------------------------------------------------- 1 | 65 | 66 | 131 | 132 | 133 | -------------------------------------------------------------------------------- /src/components/modals/ChangePatientFalldatenForm.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 137 | 138 | 139 | -------------------------------------------------------------------------------- /src/assets/imis-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | imis-logo 5 | Created with Sketch. 6 | 7 | 8 | 9 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/components/modals/AddOrChangeUserForm.vue: -------------------------------------------------------------------------------- 1 | 102 | 103 | 176 | 177 | 178 | -------------------------------------------------------------------------------- /src/components/modals/ChangeInstitutionForm.vue: -------------------------------------------------------------------------------- 1 | 121 | 122 | 179 | 180 | 181 | -------------------------------------------------------------------------------- /src/store/modules/auth.module.ts: -------------------------------------------------------------------------------- 1 | import Api, { removeBearerToken, setBearerToken } from '@/api' 2 | import { 3 | ChangePasswordDTO, 4 | Institution, 5 | InstitutionDTO, 6 | RegisterUserRequest, 7 | User, 8 | UserDTO, 9 | } from '@/api/ImisSwaggerApi' 10 | import { config } from '@/config' 11 | import { InstitutionRole } from '@/models' 12 | import router, { AppRoute, navigationRoutes } from '@/router' 13 | import { parseJwt } from '@/util' 14 | import { 15 | Actions, 16 | createMapper, 17 | Getters, 18 | Module, 19 | Mutations, 20 | } from 'vuex-smart-module' 21 | 22 | interface JwtData { 23 | roles: InstitutionRole[] 24 | exp: number 25 | [key: string]: any 26 | } 27 | 28 | class AuthState { 29 | jwtToken: string | undefined = undefined 30 | jwtData: JwtData | undefined = undefined 31 | user: User | undefined = undefined 32 | institution: Institution | undefined = undefined 33 | institutionUsers: UserDTO[] | undefined = undefined 34 | } 35 | 36 | class AuthGetters extends Getters { 37 | isAuthenticated(): boolean { 38 | return !!this.state.jwtToken // add is valid check expire date 39 | } 40 | 41 | institution(): Institution | undefined { 42 | return this.state.institution 43 | } 44 | 45 | roles() { 46 | return this.state.jwtData?.roles || [] 47 | } 48 | 49 | routes(): AppRoute[] { 50 | return navigationRoutes.filter( 51 | (r) => 52 | config.showAllViews || 53 | this.getters 54 | .roles() 55 | .some((a) => r.meta?.navigationInfo?.authorities.includes(a)) 56 | ) 57 | } 58 | 59 | institutionUsers() { 60 | return this.state.institutionUsers || [] 61 | } 62 | } 63 | 64 | class AuthMutations extends Mutations { 65 | loginSuccess(jwtToken: string) { 66 | this.state.jwtToken = jwtToken 67 | this.state.jwtData = parseJwt(jwtToken) 68 | setBearerToken(jwtToken) 69 | } 70 | 71 | logoutSuccess() { 72 | this.state.jwtToken = undefined 73 | this.state.jwtData = undefined 74 | removeBearerToken() 75 | } 76 | 77 | setAuthenticatedInstitution(institution: Institution) { 78 | this.state.institution = institution 79 | } 80 | 81 | setInstitutionUsers(users: UserDTO[]) { 82 | this.state.institutionUsers = users 83 | } 84 | 85 | setUser(user: User) { 86 | this.state.user = user 87 | } 88 | } 89 | 90 | class AuthActions extends Actions< 91 | AuthState, 92 | AuthGetters, 93 | AuthMutations, 94 | AuthActions 95 | > { 96 | async login(payload: { username: string; password: string }) { 97 | // # TODO loading animation, encrypt jwt 98 | const token: string | undefined = ( 99 | await Api.signInUserUsingPost({ 100 | username: payload.username, 101 | password: payload.password, 102 | }) 103 | ).jwtToken 104 | if (token) { 105 | this.commit('loginSuccess', token) 106 | this.dispatch('getAuthenticatedInstitution') 107 | this.dispatch('getAuthenticatedUser') 108 | window.localStorage.setItem('token', '' + token) 109 | router.push({ name: 'app' }) 110 | } 111 | } 112 | 113 | async logout() { 114 | // # TODO logout request 115 | this.commit('logoutSuccess') 116 | window.localStorage.clear() 117 | // # TODO empty state 118 | router.push({ name: 'login' }) 119 | } 120 | 121 | async init() { 122 | const jwtToken = window.localStorage.token 123 | if (jwtToken) { 124 | const decoded = parseJwt(jwtToken) 125 | const now = new Date() 126 | const tokenExpireDate = new Date(decoded.exp * 1000) 127 | if (tokenExpireDate > now) { 128 | this.commit('loginSuccess', jwtToken) 129 | this.dispatch('getAuthenticatedInstitution') 130 | this.dispatch('getAuthenticatedUser') 131 | } else { 132 | // this.commit('tokenExpired') 133 | window.localStorage.clear() 134 | } 135 | } 136 | } 137 | 138 | async getAuthenticatedInstitution() { 139 | const institution = await Api.getInstitutionUsingGet() 140 | this.commit('setAuthenticatedInstitution', institution) 141 | } 142 | 143 | async getInstitutionUsers() { 144 | const users = await Api.getInstitutionUsersUsingGet() 145 | this.commit('setInstitutionUsers', users) 146 | } 147 | 148 | async getAuthenticatedUser() { 149 | const user = await Api.currentUserUsingGet() 150 | this.commit('setUser', user) 151 | } 152 | 153 | async updateInstitution(institution: InstitutionDTO) { 154 | const updatedInstitution = await Api.updateInstitutionUsingPut(institution) 155 | this.commit('setAuthenticatedInstitution', updatedInstitution) 156 | } 157 | 158 | async registerUserForInstitution(user: RegisterUserRequest) { 159 | const res = await Api.registerUserUsingPost(user) 160 | this.dispatch('getInstitutionUsers') 161 | } 162 | 163 | async deleteUserForInstitution(userId: number) { 164 | const res = await Api.deleteInstitutionUserUsingDelete(userId) 165 | this.dispatch('getInstitutionUsers') 166 | } 167 | 168 | async updateUserForInstitution(user: UserDTO) { 169 | await Api.updateInstitutionUserUsingPut(user) 170 | this.dispatch('getInstitutionUsers') 171 | } 172 | 173 | changePassword(changePassword: ChangePasswordDTO): Promise { 174 | return Api.changePasswordUsingPost(changePassword) 175 | } 176 | } 177 | 178 | export const authModule = new Module({ 179 | state: AuthState, 180 | getters: AuthGetters, 181 | mutations: AuthMutations, 182 | actions: AuthActions, 183 | }) 184 | 185 | export const authMapper = createMapper(authModule) 186 | -------------------------------------------------------------------------------- /src/components/form-groups/IllnessStatusForm.vue: -------------------------------------------------------------------------------- 1 | 140 | 141 | 204 | 205 | 206 | -------------------------------------------------------------------------------- /src/components/other/CaseData.vue: -------------------------------------------------------------------------------- 1 | 126 | 208 | 209 | 229 | -------------------------------------------------------------------------------- /src/views/RegisterTest.vue: -------------------------------------------------------------------------------- 1 | 157 | 158 | 243 | 244 | 245 | -------------------------------------------------------------------------------- /src/views/SubmitTestResult.vue: -------------------------------------------------------------------------------- 1 | 119 | 120 | 265 | 266 | 267 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { InstitutionRole } from '@/models' 2 | import AccountView from '@/views/Account.vue' 3 | import AppRoot from '@/views/AppRoot.vue' 4 | import Dashboard from '@/views/Dashboard.vue' 5 | import LandingPage from '@/views/LandingPage.vue' 6 | import Impressum from '@/views/Impressum.vue' 7 | import Login from '@/views/Login.vue' 8 | import PatientDetails from '@/views/PatientDetails.vue' 9 | import PatientList from '@/views/PatientList.vue' 10 | import PublicRegister from '@/views/PublicRegister.vue' 11 | import PublicStatistics from '@/views/PublicStatistics.vue' 12 | import RegisterInstitution from '@/views/RegisterInstitution.vue' 13 | import RegisterPatient from '@/views/RegisterPatient.vue' 14 | import RegisterTest from '@/views/RegisterTest.vue' 15 | import RequestQuarantine from '@/views/RequestQuarantine.vue' 16 | import SendToQuarantine from '@/views/SendToQuarantine.vue' 17 | import SubmitTestResult from '@/views/SubmitTestResult.vue' 18 | import TestList from '@/views/TestList.vue' 19 | import Vue from 'vue' 20 | import VueRouter, { Route, RouteConfig } from 'vue-router' 21 | 22 | Vue.use(VueRouter) 23 | 24 | // not working, maybe because of circular dependency, when router is not imported in auth.modele, it works 25 | // const authGetters = authMapper.mapGetters({ 26 | // isAuthenticated: 'isAuthenticated', 27 | // }) 28 | 29 | function isAuthenticated() { 30 | // return authGetters.isAuthenticated() 31 | return window.localStorage.token 32 | } 33 | 34 | const checkNotAuthenticatedBeforeEnter = ( 35 | to: Route, 36 | from: Route, 37 | next: Function 38 | ) => { 39 | if (isAuthenticated()) { 40 | next({ name: 'app' }) 41 | } else { 42 | next() 43 | } 44 | } 45 | 46 | const loginBeforeRouteLeave = (to: Route, from: Route, next: Function) => { 47 | if (from.query.redirect) { 48 | next({ path: from.query.redirect }) 49 | } else { 50 | next() 51 | } 52 | } 53 | 54 | interface AppRouteExtensions { 55 | meta?: { 56 | navigationInfo?: { 57 | icon: string 58 | title: string 59 | authorities: InstitutionRole[] 60 | showInSidenav: boolean 61 | } 62 | } 63 | } 64 | 65 | export type AppRoute = RouteConfig & AppRouteExtensions 66 | 67 | const ALL_INSTITUTIONS: InstitutionRole[] = [ 68 | 'ROLE_TEST_SITE', 69 | 'ROLE_LABORATORY', 70 | 'ROLE_DOCTORS_OFFICE', 71 | 'ROLE_CLINIC', 72 | 'ROLE_GOVERNMENT_AGENCY', 73 | 'ROLE_DEPARTMENT_OF_HEALTH', 74 | ] 75 | 76 | const appRoutes: AppRoute[] = [ 77 | { 78 | name: 'dashboard', 79 | path: 'dashboard', 80 | component: Dashboard, 81 | meta: { 82 | navigationInfo: { 83 | icon: 'dashboard', 84 | title: 'Dashboard', 85 | authorities: ALL_INSTITUTIONS, 86 | showInSidenav: true, 87 | }, 88 | }, 89 | }, 90 | { 91 | name: 'register-patient', 92 | path: 'register-patient', 93 | component: RegisterPatient, 94 | meta: { 95 | navigationInfo: { 96 | icon: 'user-add', 97 | title: 'Patient Registrieren', 98 | authorities: [ 99 | 'ROLE_DEPARTMENT_OF_HEALTH', 100 | 'ROLE_CLINIC', 101 | 'ROLE_DOCTORS_OFFICE', 102 | 'ROLE_TEST_SITE', 103 | ] as InstitutionRole[], 104 | showInSidenav: true, 105 | }, 106 | }, 107 | }, 108 | { 109 | name: 'register-test', 110 | path: 'register-test', 111 | component: RegisterTest, 112 | meta: { 113 | navigationInfo: { 114 | icon: 'deployment-unit', 115 | title: 'Probe zuordnen', 116 | authorities: [ 117 | 'ROLE_DEPARTMENT_OF_HEALTH', 118 | 'ROLE_CLINIC', 119 | 'ROLE_DOCTORS_OFFICE', 120 | 'ROLE_TEST_SITE', 121 | ] as InstitutionRole[], 122 | showInSidenav: true, 123 | }, 124 | }, 125 | }, 126 | { 127 | name: 'submit-test-result', 128 | path: 'submit-test-result', 129 | component: SubmitTestResult, 130 | meta: { 131 | navigationInfo: { 132 | icon: 'experiment', 133 | title: 'Testresultat zuordnen', 134 | authorities: [ 135 | 'ROLE_DEPARTMENT_OF_HEALTH', 136 | 'ROLE_LABORATORY', 137 | 'ROLE_TEST_SITE', 138 | ] as InstitutionRole[], 139 | showInSidenav: true, 140 | }, 141 | }, 142 | }, 143 | /* 144 | { 145 | name: 'test-list', 146 | path: 'test-list', 147 | component: TestList, 148 | meta: { 149 | navigationInfo: { 150 | icon: 'unordered-list', 151 | title: 'Alle Tests', 152 | authorities: ['ROLE_DEPARTMENT_OF_HEALTH', 'ROLE_LABORATORY', 'ROLE_TEST_SITE'] as InstitutionRole[], 153 | showInSidenav: true, 154 | }, 155 | }, 156 | }, 157 | */ 158 | { 159 | name: 'patient-list', 160 | path: 'patient-list', 161 | component: PatientList, 162 | meta: { 163 | navigationInfo: { 164 | icon: 'team', 165 | title: 'Alle Patienten', 166 | authorities: [ 167 | 'ROLE_DEPARTMENT_OF_HEALTH', 168 | 'ROLE_CLINIC', 169 | 'ROLE_DOCTORS_OFFICE', 170 | 'ROLE_TEST_SITE', 171 | ] as InstitutionRole[], 172 | showInSidenav: true, 173 | }, 174 | }, 175 | }, 176 | { 177 | name: 'request-quarantine', 178 | path: 'request-quarantine', 179 | component: RequestQuarantine, 180 | meta: { 181 | navigationInfo: { 182 | icon: 'safety', 183 | title: 'Quarantäne vormerken', 184 | authorities: ['ROLE_DEPARTMENT_OF_HEALTH'] as InstitutionRole[], 185 | showInSidenav: true, 186 | }, 187 | }, 188 | }, 189 | { 190 | name: 'send-to-quarantine', 191 | path: 'send-to-quarantine', 192 | component: SendToQuarantine, 193 | meta: { 194 | navigationInfo: { 195 | icon: 'safety', 196 | title: 'In Quarantäne senden', 197 | authorities: ['ROLE_DEPARTMENT_OF_HEALTH'] as InstitutionRole[], 198 | showInSidenav: true, 199 | }, 200 | }, 201 | }, 202 | { 203 | name: 'public-statistics', 204 | path: 'public-statistics', 205 | component: PublicStatistics, 206 | meta: { 207 | navigationInfo: { 208 | icon: 'stock', 209 | title: 'Statistiken', 210 | authorities: ALL_INSTITUTIONS, 211 | showInSidenav: true, 212 | }, 213 | }, 214 | }, 215 | { 216 | name: 'patient-detail', 217 | path: 'patient/:id', 218 | component: PatientDetails, 219 | }, 220 | { 221 | name: 'account', 222 | path: 'account', 223 | component: AccountView, 224 | meta: { 225 | navigationInfo: { 226 | icon: 'user', 227 | title: 'Benutzerkonto', 228 | authorities: ALL_INSTITUTIONS, 229 | showInSidenav: false, 230 | }, 231 | }, 232 | }, 233 | { 234 | path: '*', 235 | redirect: { name: 'app' }, 236 | }, 237 | ] 238 | 239 | const routes = [ 240 | { 241 | name: 'landing-page', 242 | path: '/', 243 | component: LandingPage, 244 | }, 245 | { 246 | name: 'impressum', 247 | path: '/impressum', 248 | component: Impressum, 249 | }, 250 | { 251 | name: 'public-register', 252 | path: '/public-register', 253 | component: PublicRegister, 254 | }, 255 | { 256 | name: 'login', 257 | path: '/login', 258 | component: Login, 259 | beforeEnter: checkNotAuthenticatedBeforeEnter, 260 | beforeRouteLeave: loginBeforeRouteLeave, 261 | }, 262 | { 263 | name: 'register-institution', 264 | path: '/register-institution/:id', 265 | component: RegisterInstitution, 266 | beforeEnter: checkNotAuthenticatedBeforeEnter, 267 | }, 268 | { 269 | name: 'app', 270 | path: '/app', 271 | component: AppRoot, 272 | children: appRoutes, 273 | redirect: { name: 'dashboard' }, 274 | meta: { 275 | requiresAuth: true, 276 | }, 277 | }, 278 | { 279 | path: '*', 280 | redirect: '/', 281 | }, 282 | ] 283 | 284 | export const navigationRoutes = appRoutes.filter( 285 | (r) => !r.path.includes('*') && r.meta?.navigationInfo?.showInSidenav 286 | ) 287 | 288 | const router = new VueRouter({ 289 | mode: 'history', 290 | base: process.env.BASE_URL, 291 | routes, 292 | }) 293 | 294 | router.beforeEach((to, from, next) => { 295 | if (to.matched.some((record) => record.meta.requiresAuth)) { 296 | if (!isAuthenticated()) { 297 | next({ 298 | path: '/login', 299 | query: { redirect: to.fullPath }, 300 | }) 301 | } else { 302 | next() 303 | } 304 | } else { 305 | next() // make sure to always call next()! 306 | } 307 | }) 308 | 309 | export default router 310 | -------------------------------------------------------------------------------- /src/views/RegisterPatient.vue: -------------------------------------------------------------------------------- 1 | 113 | 114 | 234 | 235 | 260 | -------------------------------------------------------------------------------- /src/components/form-groups/LocationFormGroup.vue: -------------------------------------------------------------------------------- 1 | 136 | 137 | 259 | 260 | 285 | -------------------------------------------------------------------------------- /src/views/Account.vue: -------------------------------------------------------------------------------- 1 | 170 | 171 | 310 | 311 | 317 | -------------------------------------------------------------------------------- /src/views/RegisterInstitution.vue: -------------------------------------------------------------------------------- 1 | 172 | 173 | 256 | 257 | 274 | --------------------------------------------------------------------------------