├── .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 |
2 |
3 |
This is an about page
4 |
5 |
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 |
2 |
3 |
4 |
5 |
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 |
2 |
3 | TODO: Create Overview of tests
4 |
5 |
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 |
13 | We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled.
14 | Please enable it to continue.
15 |
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 |
2 |
3 |
4 |
5 | {{
6 | testId
7 | }}
8 |
9 |
10 |
11 |
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 |
2 |
3 |
7 | {{ indexPatient.lastName }}, {{ indexPatient.firstName }}
8 |
9 |
-
10 |
11 |
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 |
2 |
9 |
15 |
16 |
20 |
21 | {{ route.meta.navigationInfo.title }}
22 |
23 |
24 |
25 |
26 |
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 |
2 |
12 |
13 |
14 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/src/components/inputs/LaboratoryInput.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
14 |
15 | {{ laboratory.name }} ({{ laboratory.city }})
16 |
17 |
18 |
19 |
20 |
21 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/src/components/inputs/PlzInput.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 | {{ plz.fields.plz }} {{ plz.fields.note }}
11 |
12 |
13 |
14 |
15 |
16 |
17 |
67 |
68 |
69 |
--------------------------------------------------------------------------------
/src/views/Dashboard.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Willkommen in
5 |
6 |
Infektionsmelde- und Informationsystem
7 |
8 |
15 |
16 |
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 | [](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)   
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 |
2 |
3 |
4 | Keine Quarantäne.
5 |
6 |
7 | Quarantäne
8 |
9 |
10 | - Beginn:
11 | {{ getDate(item.eventDate) }}
12 |
13 |
14 | - Ende:
15 | {{ getDate(item.until) }}
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | Keine Hospitalisierung.
24 |
25 |
26 | Hospitalisierung
27 |
28 |
29 | - Beginn:
30 | {{ getDate(item.eventDate) }}
31 |
32 |
33 | - Ende:
34 | {{ getDate(item.releasedOn) }}
35 |
36 |
37 | - Intensiv:
38 | {{ item.intensiveCare ? 'Ja' : 'Nein' }}
39 |
40 |
41 |
42 |
43 |
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 |
2 | {
9 | $emit('cancel')
10 | }
11 | "
12 | @ok="save"
13 | width="650px"
14 | >
15 |
16 |
22 |
23 |
24 |
25 |
26 |
79 |
80 |
81 |
--------------------------------------------------------------------------------
/src/views/AppRoot.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
20 |
27 |
28 |
29 |
39 |
40 |
41 |
42 |
43 |
48 |
49 |
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 |
2 |
3 |
7 |
8 |
13 |
14 | {{ exposure.label }}
15 |
16 |
17 |
18 |
19 |
24 |
25 |
30 |
31 | {{ exposure.label }}
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
97 |
98 |
99 |
--------------------------------------------------------------------------------
/src/components/other/TestIncidentsCard.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
33 |
34 |
35 |
36 |
95 |
96 |
108 |
--------------------------------------------------------------------------------
/src/components/form-groups/PreIllnessesForm.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
12 |
13 | {{ preIllness.label }}
14 |
15 |
16 |
21 |
22 | {{ preIllness.label }}
23 |
24 |
25 |
26 |
27 |
28 |
36 | Andere:
37 |
38 |
39 |
46 |
47 |
48 |
49 |
50 |
51 |
93 |
94 |
95 |
--------------------------------------------------------------------------------
/src/components/other/History.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
14 | {{ formatDate(incident.eventDate) }},
15 | {{ eventTypes.find((type) => type.id === incident.eventType).label }}
16 |
17 | erfasst {{ formatTimestamp(incident.versionTimestamp) }} durch
18 | {{ incident.versionUser.institution.name }}
19 |
20 |
21 | erfasst {{ formatTimestamp(incident.versionTimestamp) }}
22 |
23 |
24 |
25 |
26 |
27 |
97 |
98 |
--------------------------------------------------------------------------------
/src/components/inputs/PatientInput.vue:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 | {{ entry.label }}
16 |
17 |
18 |
19 |
20 |
108 |
109 |
110 |
--------------------------------------------------------------------------------
/src/components/structural/Header.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
19 |
20 |
55 |
56 |
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 |
2 |
3 |
8 |
9 |
10 |
11 |
12 |
13 |
21 |
CSV exportieren
22 |
23 |
24 |
25 |
26 |
27 |
134 |
135 |
136 |
151 |
--------------------------------------------------------------------------------
/src/components/modals/ChangePasswordForm.vue:
--------------------------------------------------------------------------------
1 |
2 | {
8 | $emit('cancel')
9 | }
10 | "
11 | @ok="handleChangePassword"
12 | >
13 |
14 |
15 |
18 |
19 |
20 |
25 |
26 |
31 |
36 |
37 |
38 |
39 |
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 |
2 |
6 |
7 |
11 |
12 |
27 |
28 |
29 |
30 |
31 |
47 |
48 |
49 |
50 |
51 |
55 |
60 | Einloggen
61 |
62 |
63 |
64 |
65 |
66 |
Demo-Zugänge:
67 |
72 | Als Labor einloggen
73 |
74 |
75 |
76 | Kennung test_testing_site mit Passwort
77 | asdf
78 |
79 |
84 | Als Gesundheitsamt einloggen
85 |
86 |
87 | Kennung test_department_of_health mit Passwort
88 | asdf
89 |
90 |
92 |
93 |
94 |
95 |
96 |
128 |
129 |
136 |
--------------------------------------------------------------------------------
/src/components/inputs/BarcodeScanner.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
{{ result }}
11 |
17 |
18 |
33 |
34 |
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 |
2 |
3 |
4 |
8 |
9 |
14 |
15 | {{ symptom.label }}
16 |
17 |
18 |
19 |
20 |
21 |
30 | Andere:
31 |
32 |
35 |
39 |
40 |
41 |
42 |
48 |
52 |
53 | Plötzlich, innerhalb von einem Tag
54 |
55 |
56 | Langsam, innerhalb von mehreren Tagen
57 |
58 |
59 | Nicht bekannt
60 |
61 |
62 |
63 |
64 |
65 |
66 |
131 |
132 |
133 |
--------------------------------------------------------------------------------
/src/components/modals/ChangePatientFalldatenForm.vue:
--------------------------------------------------------------------------------
1 |
2 | {
9 | $emit('cancel')
10 | }
11 | "
12 | @ok="save"
13 | width="1000px"
14 | >
15 |
16 |
17 |
18 | Symptome
19 |
20 |
21 | Vorerkrankungen und Risikofaktoren
22 |
23 |
24 | Exposition
25 |
26 |
27 |
28 |
29 |
30 |
137 |
138 |
139 |
--------------------------------------------------------------------------------
/src/assets/imis-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | imis-logo
5 | Created with Sketch.
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/src/components/modals/AddOrChangeUserForm.vue:
--------------------------------------------------------------------------------
1 |
2 | {
9 | $emit('cancel')
10 | }
11 | "
12 | @ok="save"
13 | >
14 |
15 |
16 |
30 |
31 |
32 |
46 |
47 |
48 |
62 |
63 |
64 |
79 |
80 |
81 |
95 | Admin
96 | Regular
97 |
98 |
99 |
100 |
101 |
102 |
103 |
176 |
177 |
178 |
--------------------------------------------------------------------------------
/src/components/modals/ChangeInstitutionForm.vue:
--------------------------------------------------------------------------------
1 |
2 | {
9 | $emit('cancel')
10 | }
11 | "
12 | @ok="save"
13 | >
14 |
15 |
16 |
30 |
31 |
32 |
33 |
34 |
35 |
41 |
42 |
43 |
44 |
58 |
59 |
63 |
77 |
78 |
79 |
80 |
96 |
97 |
111 |
112 |
113 |
114 |
117 |
118 |
119 |
120 |
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 |
2 |
3 |
Erkrankung
4 |
5 |
6 |
7 |
22 | COVID-19
23 |
24 |
25 |
26 |
27 |
28 |
44 |
48 |
49 | {{ eventType.label }}
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
72 |
73 |
74 |
75 |
76 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
Hospitalisierung
97 |
98 |
99 |
100 |
104 | Patient/in ist hospitalisiert
105 |
106 |
107 |
108 |
109 |
110 |
127 |
128 |
129 |
133 | Auf der Intensivstation
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
204 |
205 |
206 |
--------------------------------------------------------------------------------
/src/components/other/CaseData.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
16 |
17 |
18 | Krankheit:
19 | {{ administrative.illness }}
20 |
21 |
22 | Erkrankungsdatum:
23 | {{ getDate(administrative.dateOfIllness) }}
24 |
25 |
26 | Meldedatum:
27 | {{ getDate(administrative.dateOfReporting) }}
28 |
29 |
30 | Behandelnder Artzt:
31 | {{ administrative.responsibleDoctor || 'keine Angabe' }}
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
45 | Kontakte mit Indexpatienten
47 | {{
48 | patientInfectionSources.length
49 | }}
50 | Keine
51 | bekannt
52 |
53 |
54 |
59 | Eigene Kontaktpersonen
61 | {{
62 | exposureContacts.length
63 | }}
64 | Keine
65 | angegeben
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
78 |
79 | Keine Symptome erfasst.
80 |
81 |
82 | Symptome:
83 |
84 |
88 | {{
89 | SYMPTOMS.find((symptomFind) => symptomFind.value === symptom)
90 | ? SYMPTOMS.find(
91 | (symptomFind) => symptomFind.value === symptom
92 | ).label
93 | : symptom
94 | }}
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 | Keine Prädispositionsinformationen.
103 |
104 |
105 | Prädisposition:
106 |
107 |
108 | {{ illness }}
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
122 |
123 |
124 |
125 |
126 |
208 |
209 |
229 |
--------------------------------------------------------------------------------
/src/views/RegisterTest.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
16 |
31 |
32 |
33 |
46 |
47 |
48 |
49 |
50 |
64 |
65 |
66 |
67 |
68 |
83 |
88 | {{ testTypeItem.label }}
89 |
90 |
91 |
92 |
93 |
94 |
109 |
114 | {{ testMaterialItem.label }}
115 |
116 |
117 |
118 |
119 |
120 |
135 |
136 |
137 |
138 |
139 |
144 |
145 |
146 |
147 |
148 |
149 |
150 | Speichern
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
243 |
244 |
245 |
--------------------------------------------------------------------------------
/src/views/SubmitTestResult.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
11 |
12 |
28 |
29 |
30 |
46 |
47 |
48 |
49 |
63 |
68 |
69 | {{ testResult.label }}
70 |
71 |
72 |
73 |
74 |
75 |
89 |
90 |
91 |
92 |
93 |
98 |
99 |
100 |
101 |
102 |
103 |
104 | Test Report hochladen
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 | Speichern
113 |
114 |
115 |
116 |
117 |
118 |
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 |
2 |
3 |
4 |
5 |
10 |
11 |
12 |
13 | Probe zuordnen
14 |
15 |
16 |
22 |
23 | Patienten/in einsehen
24 |
25 |
26 |
27 | Neuen Patienten registrieren
28 |
29 |
30 |
31 |
32 | {{ createdPatient.id }}
33 |
34 |
35 | {{ createdPatient.firstName }} {{ createdPatient.lastName }}
36 |
37 |
38 |
39 |
40 |
41 |
42 |
49 |
50 |
51 |
52 |
53 |
54 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
76 |
77 | Ja
78 | Nein
79 | Nicht bekannt
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
100 |
101 |
102 |
103 |
104 | Patient registrieren
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
234 |
235 |
260 |
--------------------------------------------------------------------------------
/src/components/form-groups/LocationFormGroup.vue:
--------------------------------------------------------------------------------
1 |
2 |
135 |
136 |
137 |
259 |
260 |
285 |
--------------------------------------------------------------------------------
/src/views/Account.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{
6 | showChangePasswordForm = false
7 | }
8 | "
9 | @create="
10 | () => {
11 | showChangePasswordForm = false
12 | }
13 | "
14 | :visible="showChangePasswordForm"
15 | />
16 | {
20 | showAddOrChangeUserForm = false
21 | }
22 | "
23 | @create="
24 | () => {
25 | showAddOrChangeUserForm = false
26 | }
27 | "
28 | :visible="showAddOrChangeUserForm"
29 | :user="addOrChangeUser"
30 | />
31 | {
35 | showChangeInstitutionForm = false
36 | }
37 | "
38 | @create="
39 | () => {
40 | showChangeInstitutionForm = false
41 | }
42 | "
43 | :visible="showChangeInstitutionForm"
44 | :institution="institution"
45 | />
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | Benutzername:
54 | {{ user.username }}
55 |
56 |
57 | Name:
58 | {{ user.firstName }} {{ user.lastName }}
59 |
60 |
61 | Rollen:
62 |
63 |
67 | {{ roleMapping[authority.authority] }}
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
83 |
84 |
85 | Name:
86 | {{ institution.name }}
87 |
88 |
89 | Typ:
90 | {{ typeMapping[institution.institutionType] }}
91 |
92 |
93 | Adresse:
94 |
95 | {{ institution.street }} {{ institution.houseNumber }} {{
96 | institution.zip
97 | }}
98 | {{ institution.city }}
99 |
100 |
101 |
102 | E-Mail:
103 | {{ institution.email }}
104 |
105 |
106 | Telefonnummer:
107 | {{ institution.phoneNumber }}
108 |
109 |
110 | Kommentar:
111 | {{ institution.comment }}
112 |
113 |
114 |
120 | Bearbeiten
121 |
122 |
123 |
124 |
125 |
126 |
131 |
134 |
140 | Hinzufügen
141 |
142 |
143 |
148 |
149 | {{ roleMapping[authorities[1].authority] }}
150 |
151 |
152 |
changeUser(user)"
156 | icon="edit"
157 | style="margin-right: 10px;"
158 | >
159 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
310 |
311 |
317 |
--------------------------------------------------------------------------------
/src/views/RegisterInstitution.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
11 |
12 |
19 |
20 |
21 |
22 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
35 |
36 |
37 |
38 |
39 |
40 |
46 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
77 | Labor
78 | Arztpraxis
81 | Klinik
82 | Teststelle
85 |
86 |
87 |
88 |
91 |
92 |
93 |
96 |
97 |
98 |
101 |
102 |
103 |
104 |
105 |
108 |
109 |
110 |
113 |
114 |
115 |
118 |
119 |
120 |
123 |
124 |
125 |
126 |
127 |
128 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 | Ich erkläre mich mit der Übermittlung dieser Daten zur
144 | weiteren Verarbeitung einverstanden.
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 | Registrieren
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
Die Institution wurde erfolgreich registriert.
167 |
168 |
Die Instituions ID lautet: {{ createdInstitution.id }}
169 |
170 |
171 |
172 |
173 |
256 |
257 |
274 |
--------------------------------------------------------------------------------