diff --git a/.editorconfig b/.editorconfig index 63f92aa4..de2782f7 100644 --- a/.editorconfig +++ b/.editorconfig @@ -5,5 +5,8 @@ indent_style = tab end_of_line = lf insert_final_newline = false +[*.{vue,js}] +indent_style = space + [*.{yml,yaml}] indent_style = space diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..432793cd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,33 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (optional):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..bbcbbe7d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 12fada13..078ae1cc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,6 +26,11 @@ jobs: with: java-version: 1.11 - uses: actions/checkout@v2 + - if: ${{ github.event_name == 'push' || ( github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository ) }} + uses: hallee/eslint-action@1.0.3 + with: + repo-token: "${{secrets.GITHUB_TOKEN}}" + source-root: "client" - run: | git fetch --unshallow - gradle build lint test \ No newline at end of file + gradle build lint test diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml new file mode 100644 index 00000000..d40b1be6 --- /dev/null +++ b/.github/workflows/pr.yml @@ -0,0 +1,113 @@ +name: Pull Request + +on: + pull_request: + branches: + - dev + - pilot + types: + - opened + - closed + - synchronize + +# Environment variables available to all jobs and steps in this workflow +env: + GKE_PROJECT_ID: ${{ secrets.GKE_PROJECT_ID }} + GKE_EMAIL: ${{ secrets.GKE_EMAIL }} + POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }} + GITHUB_SHA: ${{ github.sha }} + PR_NUMBER: ${{ github.event.pull_request.number }} + REGISTRY_HOSTNAME: eu.gcr.io + +jobs: + stop-pr: + if: github.event.pull_request.closed == true + name: Stop PR + runs-on: ubuntu-latest + steps: + - uses: GoogleCloudPlatform/github-actions/setup-gcloud@master + with: + version: '290.0.1' + service_account_email: ${{ secrets.GKE_EMAIL }} + service_account_key: ${{ secrets.GKE_KEY }} + project_id: ${{ secrets.GKE_PROJECT_ID }} + - name: Stop + run: | + gcloud components install beta + + gcloud run services delete imis-pr-${PR_NUMBER} + + deploy-pr: + if: github.event.pull_request.closed != true + name: Deploy PR + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - uses: actions/cache@v1 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }} + restore-keys: | + ${{ runner.os }}-gradle- + - name: Get yarn cache directory path + id: yarn-cache-dir-path + run: echo "::set-output name=dir::$(yarn cache dir)" + - uses: actions/cache@v1 + id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + + # Setup gcloud CLI + - uses: GoogleCloudPlatform/github-actions/setup-gcloud@master + with: + version: '290.0.1' + service_account_email: ${{ secrets.GKE_EMAIL }} + service_account_key: ${{ secrets.GKE_KEY }} + project_id: ${{ secrets.GKE_PROJECT_ID }} + - uses: actions/setup-java@v1.3.0 + with: + java-version: 1.11 + # Configure docker to use the gcloud command-line tool as a credential helper + - run: | + # Set up docker to authenticate + # via gcloud command-line tool. + gcloud auth configure-docker + + # Build and push image to Google Container Registry + - name: Build + run: |- + gradle server:test --tests "*GenerateSwagger" -x processResources + gradle client:generateClient + gradle jib --image "$REGISTRY_HOSTNAME/$GKE_PROJECT_ID/imis:$GITHUB_SHA" + + - name: Deploy + run: | + export IMAGE="$REGISTRY_HOSTNAME/$GKE_PROJECT_ID/imis:$GITHUB_SHA" + + gcloud components install beta + + gcloud beta run deploy imis-pr-${PR_NUMBER} --image "$IMAGE" \ + --platform managed \ + --allow-unauthenticated \ + --max-instances=3 \ + --memory=512Mi --cpu=1000m \ + --concurrency=80 \ + --set-env-vars="SPRING_PROFILES_ACTIVE=production,SPRING_DATASOURCE_PASSWORD=${POSTGRES_PASSWORD},CLOUD_SQL_INSTANCE=challenge-11" \ + --set-cloudsql-instances=onyx-yeti-271818:europe-west3:challenge-11 \ + --service-account=cloudsql-instance-service-acco@onyx-yeti-271818.iam.gserviceaccount.com \ + --region=europe-west1 + + PR_URL="$(gcloud beta run services describe imis-pr-417 --platform=managed --region=europe-west1 --format=yaml | grep -o 'url:.*' | uniq)" + echo "::set-env name=PR_COMMENT_MESSAGE::${PR_URL}" + + - name: comment PR + uses: unsplash/comment-on-pr@master + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + msg: "Live preview at ${{ env.PR_COMMENT_MESSAGE }}" + check_for_duplicate_msg: true \ No newline at end of file diff --git a/.gitignore b/.gitignore index 78816102..941d8007 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ .idea .gradle .DS_Store +client/.vscode .vscode **/.project **/.settings diff --git a/chart/templates/deployment.yaml b/chart/templates/deployment.yaml index 883ea1d0..15df5c9a 100644 --- a/chart/templates/deployment.yaml +++ b/chart/templates/deployment.yaml @@ -6,6 +6,7 @@ metadata: {{- include "imis.labels" . | nindent 4 }} spec: replicas: 1 + revisionHistoryLimit: 5 selector: matchLabels: {{- include "imis.selectorLabels" . | nindent 6 }} @@ -32,6 +33,10 @@ spec: value: jdbc:postgresql://localhost:5432/{{- .Values.database }} - name: SPRING_DATASOURCE_USERNAME value: postgres + - name: SPRING_CLOUD_GCP_SQL_ENABLED + value: "false" + - name: MANAGEMENT_SERVER_PORT + value: '8081' - name: SPRING_DATASOURCE_PASSWORD valueFrom: secretKeyRef: @@ -41,6 +46,9 @@ spec: - name: cloudstore-service-account mountPath: /usr/local/cloud-store readOnly: true + - name: lucene-index-volume + mountPath: /data + readOnly: false readinessProbe: httpGet: path: /actuator/health @@ -83,3 +91,6 @@ spec: - name: cloudstore-service-account secret: secretName: "{{ include "imis.fullname" . -}}-cloudstore" + - name: lucene-index-volume + emptyDir: {} + diff --git a/client/.eslintrc.js b/client/.eslintrc.js index 74ab81b6..51cb91a3 100644 --- a/client/.eslintrc.js +++ b/client/.eslintrc.js @@ -9,8 +9,8 @@ module.exports = { 'plugin:vue/essential', 'eslint:recommended', '@vue/typescript/recommended', - //'@vue/prettier', - //'@vue/prettier/@typescript-eslint', + '@vue/prettier', + '@vue/prettier/@typescript-eslint', ], parserOptions: { parser: '@typescript-eslint/parser', @@ -19,6 +19,8 @@ module.exports = { rules: { '@typescript-eslint/no-extra-semi': 'error', '@typescript-eslint/no-explicit-any': 'off', + // Does not work with delimiter "none" even though documentation says otherwise: + '@typescript-eslint/member-delimiter-style': 'off', }, overrides: [ { diff --git a/client/src/Root.vue b/client/src/Root.vue index defba808..bd2f642d 100644 --- a/client/src/Root.vue +++ b/client/src/Root.vue @@ -5,7 +5,5 @@ diff --git a/client/src/api/SwaggerApi.ts b/client/src/api/SwaggerApi.ts index 8467af4c..19034b26 100644 --- a/client/src/api/SwaggerApi.ts +++ b/client/src/api/SwaggerApi.ts @@ -94,7 +94,8 @@ export interface CreatePatientDTO { | "QUARANTINE_SELECTED" | "QUARANTINE_MANDATED" | "QUARANTINE_RELEASED" - | "QUARANTINE_PROFESSIONBAN_RELEASED"; + | "QUARANTINE_PROFESSIONBAN_RELEASED" + | "HOSPITALIZATION_MANDATED"; phoneNumber?: string; preIllnesses?: string[]; quarantineUntil?: string; @@ -183,7 +184,8 @@ export interface Incident { | "QUARANTINE_SELECTED" | "QUARANTINE_MANDATED" | "QUARANTINE_RELEASED" - | "QUARANTINE_PROFESSIONBAN_RELEASED"; + | "QUARANTINE_PROFESSIONBAN_RELEASED" + | "HOSPITALIZATION_MANDATED"; id?: string; patient?: Patient; versionTimestamp?: string; @@ -387,7 +389,8 @@ export interface Patient { | "QUARANTINE_SELECTED" | "QUARANTINE_MANDATED" | "QUARANTINE_RELEASED" - | "QUARANTINE_PROFESSIONBAN_RELEASED"; + | "QUARANTINE_PROFESSIONBAN_RELEASED" + | "HOSPITALIZATION_MANDATED"; phoneNumber?: string; preIllnesses?: string[]; quarantineUntil?: string; @@ -433,7 +436,8 @@ export interface PatientEvent { | "QUARANTINE_SELECTED" | "QUARANTINE_MANDATED" | "QUARANTINE_RELEASED" - | "QUARANTINE_PROFESSIONBAN_RELEASED"; + | "QUARANTINE_PROFESSIONBAN_RELEASED" + | "HOSPITALIZATION_MANDATED"; id?: string; illness?: "CORONA"; labTest?: LabTest; @@ -474,7 +478,8 @@ export interface PatientSearchParamsDTO { | "QUARANTINE_SELECTED" | "QUARANTINE_MANDATED" | "QUARANTINE_RELEASED" - | "QUARANTINE_PROFESSIONBAN_RELEASED"; + | "QUARANTINE_PROFESSIONBAN_RELEASED" + | "HOSPITALIZATION_MANDATED"; phoneNumber?: string; quarantineStatus?: Array< | "REGISTERED" @@ -493,6 +498,7 @@ export interface PatientSearchParamsDTO { | "QUARANTINE_MANDATED" | "QUARANTINE_RELEASED" | "QUARANTINE_PROFESSIONBAN_RELEASED" + | "HOSPITALIZATION_MANDATED" >; street?: string; zip?: string; @@ -526,7 +532,8 @@ export interface QuarantineIncident { | "QUARANTINE_SELECTED" | "QUARANTINE_MANDATED" | "QUARANTINE_RELEASED" - | "QUARANTINE_PROFESSIONBAN_RELEASED"; + | "QUARANTINE_PROFESSIONBAN_RELEASED" + | "HOSPITALIZATION_MANDATED"; id?: string; patient?: Patient; until?: string; @@ -568,7 +575,8 @@ export interface RequestQuarantineDTO { | "QUARANTINE_SELECTED" | "QUARANTINE_MANDATED" | "QUARANTINE_RELEASED" - | "QUARANTINE_PROFESSIONBAN_RELEASED"; + | "QUARANTINE_PROFESSIONBAN_RELEASED" + | "HOSPITALIZATION_MANDATED"; } export interface SendToQuarantineDTO { @@ -865,6 +873,22 @@ export class Api extends HttpClient { updateExposureContactUsingPut: (contact: ExposureContactToServer, params?: RequestParams) => this.request(`/api/exposure-contacts`, "PUT", params, contact, true), + /** + * @tags exposure-contact-controller + * @name getExposureSourceContactsForPatientsUsingPOST + * @summary getExposureSourceContactsForPatients + * @request POST:/api/exposure-contacts/by-contact/ + * @secure + */ + getExposureSourceContactsForPatientsUsingPost: (patientIds: string[], params?: RequestParams) => + this.request, any>( + `/api/exposure-contacts/by-contact/`, + "POST", + params, + patientIds, + true, + ), + /** * @tags exposure-contact-controller * @name getExposureSourceContactsForPatientUsingGET @@ -967,6 +991,20 @@ export class Api extends HttpClient { getLogUsingGet: (id: string, params?: RequestParams) => this.request(`/api/incidents/${id}/log`, "GET", params, null, true), + /** + * @tags incident-controller + * @name getPatientsCurrentByTypeUsingPOST + * @summary getPatientsCurrentByType + * @request POST:/api/incidents/{type}/patient + * @secure + */ + getPatientsCurrentByTypeUsingPost: ( + type: "test" | "quarantine" | "administrative", + patientIds: string[], + params?: RequestParams, + ) => + this.request, any>(`/api/incidents/${type}/patient`, "POST", params, patientIds, true), + /** * @tags incident-controller * @name getPatientCurrentByTypeUsingGET @@ -976,7 +1014,7 @@ export class Api extends HttpClient { */ getPatientCurrentByTypeUsingGet: ( id: string, - type: "test" | "quarantine" | "administrative", + type: "test" | "quarantine" | "administrative" | "hospitalization", params?: RequestParams, ) => this.request(`/api/incidents/${type}/patient/${id}`, "GET", params, null, true), @@ -987,8 +1025,11 @@ export class Api extends HttpClient { * @request GET:/api/incidents/{type}/patient/{id}/log * @secure */ - getPatientLogByTypeUsingGet: (id: string, type: "test" | "quarantine" | "administrative", params?: RequestParams) => - this.request(`/api/incidents/${type}/patient/${id}/log`, "GET", params, null, true), + getPatientLogByTypeUsingGet: ( + id: string, + type: "test" | "quarantine" | "administrative" | "hospitalization", + params?: RequestParams, + ) => this.request(`/api/incidents/${type}/patient/${id}/log`, "GET", params, null, true), /** * @tags institution-controller @@ -1270,70 +1311,72 @@ export class Api extends HttpClient { error = { /** * @tags basic-error-controller - * @name errorHtmlUsingGET - * @summary errorHtml + * @name errorUsingGET + * @summary error * @request GET:/error * @secure */ - errorHtmlUsingGet: (params?: RequestParams) => this.request(`/error`, "GET", params, null, true), + errorUsingGet: (params?: RequestParams) => + this.request, any>(`/error`, "GET", params, null, true), /** * @tags basic-error-controller - * @name errorHtmlUsingHEAD - * @summary errorHtml + * @name errorUsingHEAD + * @summary error * @request HEAD:/error * @secure */ - errorHtmlUsingHead: (params?: RequestParams) => - this.request(`/error`, "HEAD", params, null, true), + errorUsingHead: (params?: RequestParams) => + this.request, any>(`/error`, "HEAD", params, null, true), /** * @tags basic-error-controller - * @name errorHtmlUsingPOST - * @summary errorHtml + * @name errorUsingPOST + * @summary error * @request POST:/error * @secure */ - errorHtmlUsingPost: (params?: RequestParams) => - this.request(`/error`, "POST", params, null, true), + errorUsingPost: (params?: RequestParams) => + this.request, any>(`/error`, "POST", params, null, true), /** * @tags basic-error-controller - * @name errorHtmlUsingPUT - * @summary errorHtml + * @name errorUsingPUT + * @summary error * @request PUT:/error * @secure */ - errorHtmlUsingPut: (params?: RequestParams) => this.request(`/error`, "PUT", params, null, true), + errorUsingPut: (params?: RequestParams) => + this.request, any>(`/error`, "PUT", params, null, true), /** * @tags basic-error-controller - * @name errorHtmlUsingDELETE - * @summary errorHtml + * @name errorUsingDELETE + * @summary error * @request DELETE:/error * @secure */ - errorHtmlUsingDelete: (params?: RequestParams) => - this.request(`/error`, "DELETE", params, null, true), + errorUsingDelete: (params?: RequestParams) => + this.request, any>(`/error`, "DELETE", params, null, true), /** * @tags basic-error-controller - * @name errorHtmlUsingOPTIONS - * @summary errorHtml + * @name errorUsingOPTIONS + * @summary error * @request OPTIONS:/error * @secure */ - errorHtmlUsingOptions: (params?: RequestParams) => - this.request(`/error`, "OPTIONS", params, null, true), + errorUsingOptions: (params?: RequestParams) => + this.request, any>(`/error`, "OPTIONS", params, null, true), /** * @tags basic-error-controller - * @name errorHtmlUsingPATCH - * @summary errorHtml + * @name errorUsingPATCH + * @summary error * @request PATCH:/error * @secure */ - errorHtmlUsingPatch: (params?: RequestParams) => - this.request(`/error`, "PATCH", params, null, true), + errorUsingPatch: (params?: RequestParams) => + this.request, any>(`/error`, "PATCH", params, null, true), }; } diff --git a/client/src/api/index.ts b/client/src/api/index.ts index d8657864..4735d0a1 100644 --- a/client/src/api/index.ts +++ b/client/src/api/index.ts @@ -36,7 +36,8 @@ const apiWrapper = { }), } -function createApiProxy(foo: Api['api']): Api['api'] { // Proxy is compatible with Foo +function createApiProxy(foo: Api['api']): Api['api'] { + // Proxy is compatible with Foo const handler = { get: (target: Api['api'], prop: keyof Api['api'], receiver: any) => { if (apiWrapper.apiInstance.api[prop] !== null) { diff --git a/client/src/assets/global.scss b/client/src/assets/global.scss index 7216aa39..54b6f368 100644 --- a/client/src/assets/global.scss +++ b/client/src/assets/global.scss @@ -60,7 +60,8 @@ body { } h4 { - font-weight: bold, + font-weight: bold; + margin-bottom: 1em; } .fading-enter-active, diff --git a/client/src/components/AddOrChangeUserForm.vue b/client/src/components/AddOrChangeUserForm.vue index fd7a70fe..03dbc94c 100644 --- a/client/src/components/AddOrChangeUserForm.vue +++ b/client/src/components/AddOrChangeUserForm.vue @@ -2,55 +2,95 @@ { $emit('cancel') }" + @cancel=" + () => { + $emit('cancel') + } + " @ok="save" > - - + Admin Regular @@ -93,36 +133,40 @@ export default Vue.extend({ if (this.isNewUser) { this.registerUserForInstitution({ ...values, - }).then(() => { - this.$notification.success({ - message: 'Benutzer erfolgreich angelegt', - description: '', + }) + .then(() => { + this.$notification.success({ + message: 'Benutzer erfolgreich angelegt', + description: '', + }) + this.form.resetFields() + this.$emit('create') }) - this.form.resetFields() - this.$emit('create') - }).catch((error: Error) => { - this.$notification.error({ - message: 'Benutzer konnte nicht angelegt werden.', - description: error.message, + .catch((error: Error) => { + this.$notification.error({ + message: 'Benutzer konnte nicht angelegt werden.', + description: error.message, + }) }) - }) } else { this.updateUserForInstitution({ id: this.user.id, ...values, - }).then(() => { - this.$notification.success({ - message: 'Benutzer erfolgreich aktualisiert', - description: '', + }) + .then(() => { + this.$notification.success({ + message: 'Benutzer erfolgreich aktualisiert', + description: '', + }) + this.form.resetFields() + this.$emit('create') }) - this.form.resetFields() - this.$emit('create') - }).catch((error: Error) => { - this.$notification.error({ - message: 'Benutzer konnte nicht aktualisiert werden.', - description: error.message, + .catch((error: Error) => { + this.$notification.error({ + message: 'Benutzer konnte nicht aktualisiert werden.', + description: error.message, + }) }) - }) } }) }, @@ -130,5 +174,4 @@ export default Vue.extend({ }) - + diff --git a/client/src/components/BarcodeScanner.vue b/client/src/components/BarcodeScanner.vue index 3dccc220..afd4b6f1 100644 --- a/client/src/components/BarcodeScanner.vue +++ b/client/src/components/BarcodeScanner.vue @@ -3,25 +3,33 @@ - {{result}} + {{ result }} - Scan new result + Scan new result Use - - {{d.label}} + + {{ + d.label + }} Cancel - diff --git a/client/src/components/ChangeInstitutionForm.vue b/client/src/components/ChangeInstitutionForm.vue index ce985be2..3a94c56a 100644 --- a/client/src/components/ChangeInstitutionForm.vue +++ b/client/src/components/ChangeInstitutionForm.vue @@ -4,60 +4,109 @@ title="Institution ändern" okText="Speichern" cancelText="Abbrechen" - @cancel="() => { $emit('cancel') }" + @cancel=" + () => { + $emit('cancel') + } + " @ok="save" > - + - + - + - + @@ -107,24 +156,25 @@ export default Vue.extend({ ...values, institutionType: this.institution.institutionType, id: this.institution.id, - }).then(() => { - this.$notification.success({ - message: 'Institution erfolgreich geändert', - description: '', + }) + .then(() => { + this.$notification.success({ + message: 'Institution erfolgreich geändert', + description: '', + }) + this.form.resetFields() + this.$emit('create') }) - this.form.resetFields() - this.$emit('create') - }).catch((error: Error) => { - this.$notification.error({ - message: 'Institution konnte nicht geändert werden.', - description: error.message, + .catch((error: Error) => { + this.$notification.error({ + message: 'Institution konnte nicht geändert werden.', + description: error.message, + }) }) - }) }) }, }, }) - + diff --git a/client/src/components/ChangePasswordForm.vue b/client/src/components/ChangePasswordForm.vue index 12f091e7..be841480 100644 --- a/client/src/components/ChangePasswordForm.vue +++ b/client/src/components/ChangePasswordForm.vue @@ -1,15 +1,16 @@ { $emit('cancel') }" + title="Passwort ändern" + okText="Passwort ändern" + @cancel=" + () => { + $emit('cancel') + } + " @ok="handleChangePassword" > - + - + { - this.$notification.success({ - message: 'Passwort erfolgreich geändert', - description: '', + }) + .then(() => { + this.$notification.success({ + message: 'Passwort erfolgreich geändert', + description: '', + }) + this.changePasswordForm.resetFields() + this.$emit('create') }) - this.changePasswordForm.resetFields() - this.$emit('create') - }).catch((error: Error) => { - this.$notification.error({ - message: 'Passwort konnte nicht geändert werden.', - description: error.message, + .catch((error: Error) => { + this.$notification.error({ + message: 'Passwort konnte nicht geändert werden.', + description: error.message, + }) }) - }) }) }, handlePasswordRetryChangeNew(input: any) { - const newPasswordRetry = this.changePasswordForm.getFieldValue('newPasswordRetry') + const newPasswordRetry = this.changePasswordForm.getFieldValue( + 'newPasswordRetry' + ) const newPassword = input.target.value this.handlePasswordRetryChange(newPassword, newPasswordRetry) }, @@ -86,17 +95,19 @@ export default Vue.extend({ this.handlePasswordRetryChange(newPassword, newPasswordRetry) }, handlePasswordRetryChange(newPassword: string, newPasswordRetry: string) { - this.passwordRetry = newPassword === newPasswordRetry ? { - validateStatus: 'success', - errorMsg: null, - } : { - validateStatus: 'error', - errorMsg: 'Passwörter stimmen nicht überein', - } + this.passwordRetry = + newPassword === newPasswordRetry + ? { + validateStatus: 'success', + errorMsg: null, + } + : { + validateStatus: 'error', + errorMsg: 'Passwörter stimmen nicht überein', + } }, }, }) - + diff --git a/client/src/components/ChangePatientStammdatenForm.vue b/client/src/components/ChangePatientStammdatenForm.vue index 24057b3a..e35eaf7f 100644 --- a/client/src/components/ChangePatientStammdatenForm.vue +++ b/client/src/components/ChangePatientStammdatenForm.vue @@ -4,16 +4,21 @@ title="Patientenstammdaten ändern" okText="Speichern" cancelText="Abbrechen" - @cancel="() => { $emit('cancel') }" + @cancel=" + () => { + $emit('cancel') + } + " @ok="save" width="650px" > - - + + @@ -50,25 +55,26 @@ export default Vue.extend({ values.dateOfDeath = values.dateOfDeath.format('YYYY-MM-DD') } values = { ...this.patient, ...values } - Api.updatePatientUsingPut(values).then((updatedPatient) => { - this.setPatient(updatedPatient) - this.$notification.success({ - message: 'Patient erfolgreich aktualisiert', - description: '', + Api.updatePatientUsingPut(values) + .then((updatedPatient) => { + this.setPatient(updatedPatient) + this.$notification.success({ + message: 'Patient erfolgreich aktualisiert', + description: '', + }) + this.form.resetFields() + this.$emit('create') }) - this.form.resetFields() - this.$emit('create') - }).catch((error: Error) => { - this.$notification.error({ - message: 'Patient konnte nicht aktualisiert werden.', - description: error.message, + .catch((error: Error) => { + this.$notification.error({ + message: 'Patient konnte nicht aktualisiert werden.', + description: error.message, + }) }) - }) }) }, }, }) - + diff --git a/client/src/components/DateInput.vue b/client/src/components/DateInput.vue index 59adf393..1afd743d 100644 --- a/client/src/components/DateInput.vue +++ b/client/src/components/DateInput.vue @@ -13,10 +13,8 @@ - + diff --git a/client/src/components/EditExposureContact.vue b/client/src/components/EditExposureContact.vue index 5e3ad119..8e62e64f 100644 --- a/client/src/components/EditExposureContact.vue +++ b/client/src/components/EditExposureContact.vue @@ -1,75 +1,86 @@ - - + + + class="patient-input" + > + ], + initialValue: undefined, + }, + ]" + /> - - + Kontaktperson - - + + - - + + @keyup="fetchPropositions" + v-decorator="[ + formFieldName('contactLastName'), + { + rules: [], + initialValue: undefined, + }, + ]" + /> - + + @keyup="fetchPropositions" + v-decorator="[ + formFieldName('contactFirstName'), + { + rules: [], + initialValue: undefined, + }, + ]" + /> - + + @change="fetchPropositions" + :disabledDate="(date) => date.isAfter(moment())" + v-decorator="[ + formFieldName('contactDateOfBirth'), + { + rules: [], + }, + ]" + /> @@ -77,10 +88,18 @@ Männlich Weiblich @@ -88,49 +107,86 @@ - - Vorschläge - - - - - verwenden - + + + + verwenden + - {{moment(proposedPatient.dateOfBirth).format('DD.MM.YYYY')}} + {{ + moment(proposedPatient.dateOfBirth).format('DD.MM.YYYY') + }} - {{proposedPatient.street}} {{proposedPatient.houseNumber}}, {{proposedPatient.zip}} {{proposedPatient.city}} + {{ proposedPatient.street }} + {{ proposedPatient.houseNumber }}, + {{ proposedPatient.zip }} {{ proposedPatient.city }} - - - - + - - manuell eingeben - Patient bearbeiten - + + manuell eingeben + Patient bearbeiten + - {{moment(contact.dateOfBirth).format('DD.MM.YYYY')}} + {{ moment(contact.dateOfBirth).format('DD.MM.YYYY') }} - {{contact.street}} {{contact.houseNumber}}, {{contact.zip}} {{contact.city}} + {{ contact.street }} {{ contact.houseNumber }}, {{ contact.zip }} + {{ contact.city }} @@ -142,44 +198,60 @@ - + + :disabledDate="(date) => date.isAfter(moment())" + v-decorator="[ + formFieldName('dateOfContact'), + { + rules: [ + { + required: true, + message: 'Bitte ein gültiges Datum angeben', + }, + ], + initialValue: undefined, + }, + ]" + /> - + + v-decorator="[ + formFieldName('context'), + { + rules: [ + { + required: true, + message: 'Bitte den Umstand des Kontakts angeben', + }, + ], + initialValue: undefined, + }, + ]" + /> - + + v-decorator="[ + formFieldName('comment'), + { + rules: [], + initialValue: undefined, + }, + ]" + /> @@ -204,10 +276,10 @@ const exposureContexts = [ ] interface State { - contexts: string[]; - patientPropositions: Patient[]; - contact?: Patient; - lockContactEditing: boolean; + contexts: string[] + patientPropositions: Patient[] + contact?: Patient + lockContactEditing: boolean } export default mixins(FormGroupMixin).extend({ @@ -216,7 +288,18 @@ export default mixins(FormGroupMixin).extend({ DateInput, PatientInput, }, - fieldIdentifiers: ['id', 'source', 'contact', 'contactFirstName', 'contactLastName', 'contactDateOfBirth', 'contactGender', 'dateOfContact', 'context', 'comment'], + fieldIdentifiers: [ + 'id', + 'source', + 'contact', + 'contactFirstName', + 'contactLastName', + 'contactDateOfBirth', + 'contactGender', + 'dateOfContact', + 'context', + 'comment', + ], props: { showOriginatorPatient: { default: true }, disableOriginatorPatient: { default: false }, @@ -236,7 +319,7 @@ export default mixins(FormGroupMixin).extend({ (this as any).setData({ contact: c.id }) } }, - async contactPatient(contactPatient: {id: string}) { + async contactPatient(contactPatient: { id: string }) { if (contactPatient?.id) { this.contact = await Api.getPatientForIdUsingGet(contactPatient.id) this.lockContactEditing = true @@ -265,7 +348,13 @@ export default mixins(FormGroupMixin).extend({ return option.key !== this.withExts().getSingleValue('contact') }, async fetchPropositions() { - const query = [(this as any).getSingleValue('contactFirstName'), (this as any).getSingleValue('contactLastName')].filter(u => u).join(' ').trim() + const query = [ + (this as any).getSingleValue('contactFirstName'), + (this as any).getSingleValue('contactLastName'), + ] + .filter((u) => u) + .join(' ') + .trim() if (!query) { return } @@ -292,21 +381,21 @@ export default mixins(FormGroupMixin).extend({ diff --git a/client/src/components/Header.vue b/client/src/components/Header.vue index 8d6e1c60..71c239a4 100644 --- a/client/src/components/Header.vue +++ b/client/src/components/Header.vue @@ -1,60 +1,57 @@ - - + style=" + height: auto; + display: flex; + justify-content: space-between; + align-items: center; + padding: 0; + " + > + - IMIS + IMIS - - + + - - - - + + + Benutzerkonto - + - Abmelden - + Abmelden + - - + + + {{ username }} - {{username}} - - diff --git a/client/src/components/IndexPatientTableCell.vue b/client/src/components/IndexPatientTableCell.vue index 786f131a..e823ca15 100644 --- a/client/src/components/IndexPatientTableCell.vue +++ b/client/src/components/IndexPatientTableCell.vue @@ -1,7 +1,10 @@ - - {{indexPatient.lastName}}, {{indexPatient.firstName}} + + {{ indexPatient.lastName }}, {{ indexPatient.firstName }} - @@ -12,7 +15,7 @@ import { ExposureContactContactView } from '@/api/SwaggerApi' import Api from '@/api' interface State { - indexPatient: ExposureContactContactView | undefined; + indexPatient: ExposureContactContactView | undefined } export default Vue.extend({ @@ -24,18 +27,19 @@ export default Vue.extend({ } }, created() { - Api.getExposureSourceContactsForPatientUsingGet(this.patientId).then(infectionSources => { - if (infectionSources.length > 0) { - const contact = infectionSources[0].source - if (contact) { - this.indexPatient = contact - console.log('Has source: ' + this.patientId) + Api.getExposureSourceContactsForPatientUsingGet(this.patientId).then( + (infectionSources) => { + if (infectionSources.length > 0) { + const contact = infectionSources[0].source + if (contact) { + this.indexPatient = contact + console.log('Has source: ' + this.patientId) + } } } - }) + ) }, methods: {}, }) - + diff --git a/client/src/components/LaboratoryInput.vue b/client/src/components/LaboratoryInput.vue index c17b41f6..93b4f2c3 100644 --- a/client/src/components/LaboratoryInput.vue +++ b/client/src/components/LaboratoryInput.vue @@ -1,6 +1,7 @@ - - - {{laboratory.name}} ({{laboratory.city}}) - + > + + {{ laboratory.name }} ({{ laboratory.city }}) + - + diff --git a/client/src/components/LocationFormGroup.vue b/client/src/components/LocationFormGroup.vue index 4c5323d9..8985cbb6 100644 --- a/client/src/components/LocationFormGroup.vue +++ b/client/src/components/LocationFormGroup.vue @@ -2,7 +2,7 @@ - + @@ -11,13 +11,18 @@ class="custom-input" style="width: calc(100%);" placeholder="Straße und Hausnummer" - v-decorator="[formFieldName('street'), { - rules: [{ - required: $props.required!==false, - message: 'Bitte Straße und Hausnummer eingeben', - }], - initialValue: initialData('street'), - }]" + v-decorator="[ + formFieldName('street'), + { + rules: [ + { + required: $props.required !== false, + message: 'Bitte Straße und Hausnummer eingeben', + }, + ], + initialValue: initialData('street'), + }, + ]" /> @@ -28,50 +33,70 @@ handleZipSelection(val)" :dropdownMenuStyle="{ - width: 'max-content' + width: 'max-content', }" - v-decorator="[formFieldName('zip'), { - rules: [{ - required: $props.required!==false, - message: 'Bitte PLZ eingeben', - }], - initialValue: initialData('zip'), - }]" /> + v-decorator="[ + formFieldName('zip'), + { + rules: [ + { + required: $props.required !== false, + message: 'Bitte PLZ eingeben', + }, + ], + initialValue: initialData('zip'), + }, + ]" + /> + v-decorator="[ + formFieldName('city'), + { + rules: [ + { + required: $props.required !== false, + message: 'Bitte Ort eingeben', + }, + ], + initialValue: initialData('city'), + }, + ]" + /> - + + v-decorator="[ + formFieldName('country'), + { + rules: [ + { + required: $props.required !== false, + message: 'Bitte Land auswählen', + }, + ], + initialValue: initialCountry(), + }, + ]" + > Belgien Dänemark Deutschland @@ -105,23 +130,20 @@ import { getPlzs, Plz } from '@/util/plz-service' import { FormGroupMixin } from '@/util/forms' interface Input extends Vue { - value: string; - focus: () => void; + value: string + focus: () => void } interface ZipEntry { - text: string; - value: string; - zipData: Plz; + text: string + value: string + zipData: Plz } export default mixins(FormGroupMixin).extend({ name: 'LocationFormGroup', fieldIdentifiers: ['street', 'zip', 'city', 'country'], - props: [ - 'required', - 'data', - ], + props: ['required', 'data'], data() { return { zips: [] as ZipEntry[], @@ -141,7 +163,9 @@ export default mixins(FormGroupMixin).extend({ } }, initialData(fieldId: string) { - return this.$props.data ? this.$props.data[this.withExts().formFieldName(fieldId)] : null + return this.$props.data + ? this.$props.data[this.withExts().formFieldName(fieldId)] + : null }, updateByZipData(plzData: Plz) { this.withExts().setData({ @@ -165,8 +189,8 @@ export default mixins(FormGroupMixin).extend({ } if (result.length === 1) { setTimeout(() => { - this.updateByZipData(result[0]); - (this.$refs.city as Input).focus() + this.updateByZipData(result[0]) + ;(this.$refs.city as Input).focus() }, 0) this.zips = [] } else { @@ -181,7 +205,9 @@ export default mixins(FormGroupMixin).extend({ const ddRootId = document.evaluate( 'string(./descendant-or-self::*[@aria-controls and contains(@class, "ant-select-selection")]/@aria-controls)', (this.$refs.zip as Vue).$el, - null, XPathResult.STRING_TYPE).stringValue + null, + XPathResult.STRING_TYPE + ).stringValue let ddRoot = document.getElementById(ddRootId) if (ddRoot) { @@ -197,11 +223,13 @@ export default mixins(FormGroupMixin).extend({ } }, handleZipSelection(value: string) { - const zipEntry = this.zips.find((entry: ZipEntry) => entry.value === value) as ZipEntry + const zipEntry = this.zips.find( + (entry: ZipEntry) => entry.value === value + ) as ZipEntry if (zipEntry) { setTimeout(() => { - this.updateByZipData(zipEntry.zipData); - (this.$refs.city as Input).focus() + this.updateByZipData(zipEntry.zipData) + ;(this.$refs.city as Input).focus() }, 0) } }, @@ -210,14 +238,15 @@ export default mixins(FormGroupMixin).extend({ diff --git a/client/src/components/Navigation.vue b/client/src/components/Navigation.vue index 15e5bc82..66e4710c 100644 --- a/client/src/components/Navigation.vue +++ b/client/src/components/Navigation.vue @@ -12,10 +12,7 @@ theme="dark" :defaultSelectedKeys="[defaultPath]" > - + - + diff --git a/client/src/components/PatientInput.vue b/client/src/components/PatientInput.vue index 310ac0c5..79d9b376 100644 --- a/client/src/components/PatientInput.vue +++ b/client/src/components/PatientInput.vue @@ -1,14 +1,17 @@ - - + + {{ entry.label }} @@ -16,17 +19,15 @@ - + diff --git a/client/src/components/PatientStammdaten.vue b/client/src/components/PatientStammdaten.vue index cb338f4f..b5f54465 100644 --- a/client/src/components/PatientStammdaten.vue +++ b/client/src/components/PatientStammdaten.vue @@ -1,50 +1,89 @@ - - Allgemeine Angaben: - + Allgemeine Angaben + + - - + + - - - + + + + + - + - + Männlich Weiblich @@ -54,33 +93,42 @@ - - - - - - - - - - + - + - + Ja Nein @@ -88,41 +136,52 @@ - + - Wohnort: - + Wohnort + - Aufenthaltsort, falls von Wohnort abweichend: + Aufenthaltsort, falls von Wohnort abweichend + fieldNamePrefix="stay" + /> - Kommunikation und Sonstiges: - + Kommunikation und Beruf + @@ -130,58 +189,105 @@ - + - - - - {{riskOccupation.label}} - - + + + {{ riskOccupation.label }} + + + - + - - + + + + - + Krankenkasse + - - + + - + + v-decorator="[ + 'insuranceMembershipNumber', + { initialValue: patientInput.insuranceMembershipNumber }, + ]" + /> @@ -190,10 +296,12 @@ - + diff --git a/client/src/components/PlzInput.vue b/client/src/components/PlzInput.vue index 57f0814f..1def5857 100644 --- a/client/src/components/PlzInput.vue +++ b/client/src/components/PlzInput.vue @@ -1,9 +1,13 @@ - + - {{plz.fields.plz}} {{plz.fields.note}} + {{ plz.fields.plz }} {{ plz.fields.note }} @@ -11,13 +15,12 @@ - + diff --git a/client/src/components/TestInput.vue b/client/src/components/TestInput.vue index a590e436..bb837212 100644 --- a/client/src/components/TestInput.vue +++ b/client/src/components/TestInput.vue @@ -1,15 +1,20 @@ - + - {{testId}} + {{ + testId + }} - + diff --git a/client/src/components/UnprocessedCases.vue b/client/src/components/UnprocessedCases.vue index 6bfe94f4..6d52fa7a 100644 --- a/client/src/components/UnprocessedCases.vue +++ b/client/src/components/UnprocessedCases.vue @@ -5,21 +5,40 @@ :customRow="customRow" :dataSource="unprocessedCases" :pagination="{ pageSize: 500 }" - :scroll="{x: 0, y: 0}" + :scroll="{ x: 0, y: 0 }" @change="handleTableChange" class="imis-table-no-pagination" rowKey="id" > - - {{eventTypes.find(type => type.id === patientStatus).label}} + + {{ eventTypes.find((type) => type.id === patientStatus).label }} - - handlePatientClick(patient)" style="margin-right: 5px; cursor: pointer" type="search" /> + + handlePatientClick(patient)" + style="margin-right: 5px; cursor: pointer;" + type="search" + /> - - {{count}} Patienten + + {{ count }} Patienten [] = [ ] interface State { - unprocessedCases: PatientWithTimestamp[]; - currentPage: number; - pageSize: number; - columnsSchema: Partial[]; - order: string; - orderBy: string; - count: number; - eventTypes: EventTypeItem[]; + unprocessedCases: PatientWithTimestamp[] + currentPage: number + pageSize: number + columnsSchema: Partial[] + order: string + orderBy: string + count: number + eventTypes: EventTypeItem[] } interface PatientWithTimestamp extends Patient { - lastEventTimestamp: string; + lastEventTimestamp: string } export default Vue.extend({ @@ -138,28 +157,32 @@ export default Vue.extend({ const countPromise = Api.countQueryPatientsUsingPost(request) const queryPromise = Api.queryPatientsUsingPost(request) - countPromise.then(count => { + countPromise.then((count) => { this.count = count }) - queryPromise.then((result: Patient[]) => { - this.unprocessedCases = result.map(patient => { - let lastTimestamp: any - if (patient.events && patient.events.length > 0) { - lastTimestamp = moment(patient.events[patient.events.length - 1].eventTimestamp).format('DD.MM.YYYY') - } - return { - ...patient, - lastEventTimestamp: lastTimestamp, + queryPromise + .then((result: Patient[]) => { + this.unprocessedCases = result.map((patient) => { + let lastTimestamp: any + if (patient.events && patient.events.length > 0) { + lastTimestamp = moment( + patient.events[patient.events.length - 1].eventTimestamp + ).format('DD.MM.YYYY') + } + return { + ...patient, + lastEventTimestamp: lastTimestamp, + } + }) + }) + .catch((error) => { + console.error(error) + const notification = { + message: 'Fehler beim Laden der Patientendaten.', + description: error.message, } + this.$notification.error(notification) }) - }).catch(error => { - console.error(error) - const notification = { - message: 'Fehler beim Laden der Patientendaten.', - description: error.message, - } - this.$notification.error(notification) - }) }, handleTableChange(pagination: any, filters: any, sorter: any) { console.log('Table Changed to ' + this.currentPage) @@ -174,7 +197,10 @@ export default Vue.extend({ }, handlePatientClick(patient: Patient) { if (patient.id) { - this.$router.push({ name: 'patient-detail', params: { id: patient.id } }) + this.$router.push({ + name: 'patient-detail', + params: { id: patient.id }, + }) } }, customRow(record: Patient) { @@ -190,29 +216,28 @@ export default Vue.extend({ diff --git a/client/src/main.ts b/client/src/main.ts index 0bab4036..9432883f 100644 --- a/client/src/main.ts +++ b/client/src/main.ts @@ -17,5 +17,5 @@ authModule.context(store).actions.init() new Vue({ router, store, - render: h => h(Root), + render: (h) => h(Root), }).$mount('#app') diff --git a/client/src/models/event-types.ts b/client/src/models/event-types.ts index 0fa437e3..d85b645b 100644 --- a/client/src/models/event-types.ts +++ b/client/src/models/event-types.ts @@ -1,9 +1,9 @@ import { PatientStatus } from '@/models/index' export interface EventTypeItem { - id: PatientStatus; - label: string; - icon: string; + id: PatientStatus + label: string + icon: string } export const eventTypes: EventTypeItem[] = [ @@ -11,47 +11,58 @@ export const eventTypes: EventTypeItem[] = [ id: 'REGISTERED', label: 'Registriert', icon: 'login', - }, { + }, + { id: 'SUSPECTED', label: 'Verdachtsfall', icon: 'search', - }, { + }, + { id: 'ORDER_TEST', label: 'Test angefordert', icon: 'experiment', - }, { + }, + { id: 'SCHEDULED_FOR_TESTING', label: 'Wartet auf Test', icon: 'team', - }, { + }, + { id: 'TEST_SUBMITTED_IN_PROGRESS', label: 'Test eingereicht', icon: 'clock-circle', - }, { + }, + { id: 'TEST_FINISHED_POSITIVE', label: 'Test positiv', icon: 'check', - }, { + }, + { id: 'TEST_FINISHED_NEGATIVE', label: 'Test negativ', icon: 'stop', - }, { + }, + { id: 'TEST_FINISHED_INVALID', label: 'Test invalide', icon: 'warning', - }, { + }, + { id: 'TEST_FINISHED_RECOVERED', label: 'Getestet und erholt', icon: 'rollback', - }, { + }, + { id: 'TEST_FINISHED_NOT_RECOVERED', label: 'Getestet und nicht erholt', icon: 'logout', - }, { + }, + { id: 'PATIENT_DEAD', label: 'Verstorben', icon: 'cloud', - }, { + }, + { id: 'DOCTORS_VISIT', label: 'Arztbesuch', icon: 'reconciliation', @@ -76,31 +87,45 @@ export const eventTypes: EventTypeItem[] = [ label: 'Arbeitsverbot aufgehoben', icon: 'safety', }, + { + id: 'HOSPITALIZATION_MANDATED', + label: 'Hospitalisiert', + icon: 'safety', + }, ] export interface TestResultType { - id: 'TEST_SUBMITTED' | 'TEST_IN_PROGRESS' | 'TEST_POSITIVE' | 'TEST_NEGATIVE' | 'TEST_INVALID'; - label: string; - icon: string; + id: + | 'TEST_SUBMITTED' + | 'TEST_IN_PROGRESS' + | 'TEST_POSITIVE' + | 'TEST_NEGATIVE' + | 'TEST_INVALID' + label: string + icon: string } export const testResults: TestResultType[] = [ { id: 'TEST_SUBMITTED', label: 'Test eingereicht', icon: 'login', - }, { + }, + { id: 'TEST_IN_PROGRESS', label: 'Test läuft', icon: 'clock-circle', - }, { + }, + { id: 'TEST_POSITIVE', label: 'Test positiv', icon: 'check', - }, { + }, + { id: 'TEST_NEGATIVE', label: 'Test negativ', icon: 'stop', - }, { + }, + { id: 'TEST_INVALID', label: 'Test invalide', icon: 'warning', diff --git a/client/src/models/exposures.ts b/client/src/models/exposures.ts index 0bf5ace5..4e967899 100644 --- a/client/src/models/exposures.ts +++ b/client/src/models/exposures.ts @@ -12,22 +12,26 @@ const MEDICAL_LABORATORY = { } const STAY_IN_MEDICAL_FACILITY = { - label: 'Aufenthalt in medizinischer Einrichtung in den letzten 14 Tagen vor der Erkrankung', + label: + 'Aufenthalt in medizinischer Einrichtung in den letzten 14 Tagen vor der Erkrankung', value: 'STAY_IN_MEDICAL_FACILITY', } const CONTACT_WITH_CORONA_CASE = { - label: 'Enger Kontakt mit wahrscheinlichem oder bestätigtem Fall in den letzten 14 Tagen vor der Erkrankung', + label: + 'Enger Kontakt mit wahrscheinlichem oder bestätigtem Fall in den letzten 14 Tagen vor der Erkrankung', value: 'CONTACT_WITH_CORONA_CASE', } const COMMUNITY_FACILITY = { - label: 'Arbeit in Gemeinschaftseinrichtung (z.B. Schule, Kinderkrippe, Heim, sonst. Massenunterkünfte (§§34 und 36 Abs. 1 IfSG))', + label: + 'Arbeit in Gemeinschaftseinrichtung (z.B. Schule, Kinderkrippe, Heim, sonst. Massenunterkünfte (§§34 und 36 Abs. 1 IfSG))', value: 'COMMUNITY_FACILITY', } const COMMUNITY_FACILITY_MINORS = { - label: 'Betreuung in Gemeinschaftseinrichtung für Kinder oder Jugendliche, z.B.Schule, Kinderkrippe (§33 IfSG)', + label: + 'Betreuung in Gemeinschaftseinrichtung für Kinder oder Jugendliche, z.B.Schule, Kinderkrippe (§33 IfSG)', value: 'COMMUNITY_FACILITY_MINORS', } @@ -35,27 +39,36 @@ const COMMUNITY_FACILITY_MINORS = { // Registration By Doctor export const EXPOSURES_INTERNAL: Option[] = [ - MEDICAL_HEALTH_PROFESSION, MEDICAL_LABORATORY, STAY_IN_MEDICAL_FACILITY, - COMMUNITY_FACILITY, COMMUNITY_FACILITY_MINORS, + MEDICAL_HEALTH_PROFESSION, + MEDICAL_LABORATORY, + STAY_IN_MEDICAL_FACILITY, + COMMUNITY_FACILITY, + COMMUNITY_FACILITY_MINORS, CONTACT_WITH_CORONA_CASE, ] // Self-Registration export const EXPOSURES_PUBLIC: Option[] = [ - MEDICAL_HEALTH_PROFESSION, MEDICAL_LABORATORY, STAY_IN_MEDICAL_FACILITY, CONTACT_WITH_CORONA_CASE, + MEDICAL_HEALTH_PROFESSION, + MEDICAL_LABORATORY, + STAY_IN_MEDICAL_FACILITY, + CONTACT_WITH_CORONA_CASE, ] export const EXPOSURE_LOCATIONS: Option[] = [ { label: 'in einer medizinischen Einrichtung', value: 'MEDICAL_FACILITY', - }, { + }, + { label: 'im privaten Haushalt', value: 'PRIVATE', - }, { + }, + { label: 'am Arbeitsplatz', value: 'WORK', - }, { + }, + { label: 'andere / sonstige', value: 'OTHER', }, diff --git a/client/src/models/index.ts b/client/src/models/index.ts index 4e1a44ba..83690363 100644 --- a/client/src/models/index.ts +++ b/client/src/models/index.ts @@ -1,15 +1,19 @@ import { CreateInstitutionDTO, RegisterUserRequest } from '@/api/SwaggerApi' -export type InstitutionType = Exclude; +export type InstitutionType = Exclude< + CreateInstitutionDTO['institutionType'], + undefined +> export type InstitutionRole = - 'ROLE_TEST_SITE' + | 'ROLE_TEST_SITE' | 'ROLE_LABORATORY' | 'ROLE_DOCTORS_OFFICE' | 'ROLE_CLINIC' | 'ROLE_GOVERNMENT_AGENCY' | 'ROLE_DEPARTMENT_OF_HEALTH' -export type UserRole = Exclude; -export type PatientStatus = 'REGISTERED' +export type UserRole = Exclude +export type PatientStatus = + | 'REGISTERED' | 'SUSPECTED' | 'SCHEDULED_FOR_TESTING' | 'ORDER_TEST' @@ -24,9 +28,10 @@ export type PatientStatus = 'REGISTERED' | 'QUARANTINE_SELECTED' /* Vorgemerkt */ | 'QUARANTINE_MANDATED' | 'QUARANTINE_RELEASED' - | 'QUARANTINE_PROFESSIONBAN_RELEASED'; + | 'QUARANTINE_PROFESSIONBAN_RELEASED' + | 'HOSPITALIZATION_MANDATED' export type RiskOccupation = - 'NO_RISK_OCCUPATION' + | 'NO_RISK_OCCUPATION' | 'FIRE_FIGHTER_POLICE' | 'TEACHER' | 'PUBLIC_ADMINISTRATION' @@ -36,6 +41,6 @@ export type RiskOccupation = | 'NURSE' export interface Option { - label: string; - value: string; + label: string + value: string } diff --git a/client/src/models/pre-illnesses.ts b/client/src/models/pre-illnesses.ts index 6e57eb09..624cfd7d 100644 --- a/client/src/models/pre-illnesses.ts +++ b/client/src/models/pre-illnesses.ts @@ -4,7 +4,8 @@ export const ADDITIONAL_PRE_ILLNESSES: Option[] = [ { label: 'Akutes schweres Atemsyndrom (ARDS)', value: 'ARDS', - }, { + }, + { label: 'Beatmungspflichtige Atemwegserkrankung', value: 'RESPIRATORY_DISEASE', }, diff --git a/client/src/models/risk-occupation.ts b/client/src/models/risk-occupation.ts index 43fe05ab..4fd84231 100644 --- a/client/src/models/risk-occupation.ts +++ b/client/src/models/risk-occupation.ts @@ -1,17 +1,23 @@ import { RiskOccupation } from '@/models/index' export interface RiskOccupationOption { - label: string; - value: RiskOccupation; + label: string + value: RiskOccupation } export const RISK_OCCUPATIONS: RiskOccupationOption[] = [ { value: 'DOCTOR', label: 'Arzt/Ärztin' }, - { value: 'NURSE', label: 'Pflegepersonal' }, - { value: 'CAREGIVER', label: 'Altenpflege' }, - { value: 'FIRE_FIGHTER_POLICE', label: 'Gefahrenabwehr (Polizei, Feuerwehr usw.)' }, + { value: 'CAREGIVER', label: 'Altenpfleger-in' }, + { + value: 'FIRE_FIGHTER_POLICE', + label: 'Gefahrenabwehr (Polizei, Feuerwehr usw.)', + }, + { value: 'NURSE', label: 'Krankenpfleger-in' }, + { value: 'TEACHER', label: 'Lehrer-in/Kindergärtner-in' }, { value: 'PUBLIC_ADMINISTRATION', label: 'Öffentliche Verwaltung' }, { value: 'STUDENT', label: 'Schüler-in' }, - { value: 'TEACHER', label: 'Lehrer-in/Kindergärtner-in' }, - { value: 'NO_RISK_OCCUPATION', label: 'Keiner der genannten (bitte unten eingeben)' }, + { + value: 'NO_RISK_OCCUPATION', + label: 'Anderer', + }, ] diff --git a/client/src/models/test-materials.ts b/client/src/models/test-materials.ts index a9fc5297..3d3b8258 100644 --- a/client/src/models/test-materials.ts +++ b/client/src/models/test-materials.ts @@ -1,18 +1,20 @@ -export type TestMaterial = 'RACHENABSTRICH' | 'NASENABSTRICH' | 'VOLLBLUT'; +export type TestMaterial = 'RACHENABSTRICH' | 'NASENABSTRICH' | 'VOLLBLUT' export interface TestMaterialItem { - id: TestMaterial; - label: string; + id: TestMaterial + label: string } export const testMaterials: TestMaterialItem[] = [ { id: 'RACHENABSTRICH', label: 'Rachenabstrich', - }, { + }, + { id: 'NASENABSTRICH', label: 'Nasenabstrich', - }, { + }, + { id: 'VOLLBLUT', label: 'Vollblut', }, diff --git a/client/src/models/test-types.ts b/client/src/models/test-types.ts index 6c141709..a31bb8d1 100644 --- a/client/src/models/test-types.ts +++ b/client/src/models/test-types.ts @@ -1,15 +1,16 @@ -export type TestType = 'PCR' | 'ANTIBODY'; +export type TestType = 'PCR' | 'ANTIBODY' export interface TestTypeItem { - id: TestType; - label: string; + id: TestType + label: string } export const testTypes: TestTypeItem[] = [ { id: 'ANTIBODY', label: 'Antikörper', - }, { + }, + { id: 'PCR', label: 'PCR', }, diff --git a/client/src/registerServiceWorker.ts b/client/src/registerServiceWorker.ts index e20ae5e2..bb130a77 100644 --- a/client/src/registerServiceWorker.ts +++ b/client/src/registerServiceWorker.ts @@ -7,7 +7,7 @@ if (process.env.NODE_ENV === 'production') { ready() { console.log( 'App is being served from cache by a service worker.\n' + - 'For more details, visit https://goo.gl/AFskqB', + 'For more details, visit https://goo.gl/AFskqB' ) }, registered() { @@ -23,7 +23,9 @@ if (process.env.NODE_ENV === 'production') { console.log('New content is available; please refresh.') }, offline() { - console.log('No internet connection found. App is running in offline mode.') + console.log( + 'No internet connection found. App is running in offline mode.' + ) }, error(error) { console.error('Error during service worker registration:', error) diff --git a/client/src/router/index.ts b/client/src/router/index.ts index 37631adb..c71bf263 100644 --- a/client/src/router/index.ts +++ b/client/src/router/index.ts @@ -30,7 +30,11 @@ function isAuthenticated() { return window.localStorage.token } -const checkNotAuthenticatedBeforeEnter = (to: Route, from: Route, next: Function) => { +const checkNotAuthenticatedBeforeEnter = ( + to: Route, + from: Route, + next: Function +) => { if (isAuthenticated()) { next({ name: 'app' }) } else { @@ -49,12 +53,12 @@ const loginBeforeRouteLeave = (to: Route, from: Route, next: Function) => { export interface AppRoute extends RouteConfig { meta?: { navigationInfo?: { - icon: string; - title: string; - authorities: InstitutionRole[]; - showInSidenav: boolean; - }; - }; + icon: string + title: string + authorities: InstitutionRole[] + showInSidenav: boolean + } + } } const ALL_INSTITUTIONS: InstitutionRole[] = [ @@ -88,7 +92,12 @@ const appRoutes: AppRoute[] = [ navigationInfo: { icon: 'user-add', title: 'Patient Registrieren', - authorities: ['ROLE_DEPARTMENT_OF_HEALTH', 'ROLE_CLINIC', 'ROLE_DOCTORS_OFFICE', 'ROLE_TEST_SITE'], + authorities: [ + 'ROLE_DEPARTMENT_OF_HEALTH', + 'ROLE_CLINIC', + 'ROLE_DOCTORS_OFFICE', + 'ROLE_TEST_SITE', + ], showInSidenav: true, }, }, @@ -101,7 +110,12 @@ const appRoutes: AppRoute[] = [ navigationInfo: { icon: 'deployment-unit', title: 'Probe Zuordnen', - authorities: ['ROLE_DEPARTMENT_OF_HEALTH', 'ROLE_CLINIC', 'ROLE_DOCTORS_OFFICE', 'ROLE_TEST_SITE'], + authorities: [ + 'ROLE_DEPARTMENT_OF_HEALTH', + 'ROLE_CLINIC', + 'ROLE_DOCTORS_OFFICE', + 'ROLE_TEST_SITE', + ], showInSidenav: true, }, }, @@ -114,7 +128,11 @@ const appRoutes: AppRoute[] = [ navigationInfo: { icon: 'experiment', title: 'Testresultat Zuordnen', - authorities: ['ROLE_DEPARTMENT_OF_HEALTH', 'ROLE_LABORATORY', 'ROLE_TEST_SITE'], + authorities: [ + 'ROLE_DEPARTMENT_OF_HEALTH', + 'ROLE_LABORATORY', + 'ROLE_TEST_SITE', + ], showInSidenav: true, }, }, @@ -142,7 +160,12 @@ const appRoutes: AppRoute[] = [ navigationInfo: { icon: 'team', title: 'Alle Patienten', - authorities: ['ROLE_DEPARTMENT_OF_HEALTH', 'ROLE_CLINIC', 'ROLE_DOCTORS_OFFICE', 'ROLE_TEST_SITE'], + authorities: [ + 'ROLE_DEPARTMENT_OF_HEALTH', + 'ROLE_CLINIC', + 'ROLE_DOCTORS_OFFICE', + 'ROLE_TEST_SITE', + ], showInSidenav: true, }, }, @@ -250,8 +273,9 @@ const routes = [ }, ] -export const navigationRoutes = appRoutes - .filter(r => !r.path.includes('*') && r.meta?.navigationInfo?.showInSidenav) +export const navigationRoutes = appRoutes.filter( + (r) => !r.path.includes('*') && r.meta?.navigationInfo?.showInSidenav +) const router = new VueRouter({ mode: 'history', @@ -260,7 +284,7 @@ const router = new VueRouter({ }) router.beforeEach((to, from, next) => { - if (to.matched.some(record => record.meta.requiresAuth)) { + if (to.matched.some((record) => record.meta.requiresAuth)) { if (!isAuthenticated()) { next({ path: '/login', diff --git a/client/src/shims-tsx.d.ts b/client/src/shims-tsx.d.ts index cd313aa5..c656c68b 100644 --- a/client/src/shims-tsx.d.ts +++ b/client/src/shims-tsx.d.ts @@ -3,13 +3,11 @@ import Vue, { VNode } from 'vue' declare global { namespace JSX { // tslint:disable no-empty-interface - interface Element extends VNode { - } + interface Element extends VNode {} // tslint:disable no-empty-interface - interface ElementClass extends Vue { - } + interface ElementClass extends Vue {} interface IntrinsicElements { - [elem: string]: any; + [elem: string]: any } } } diff --git a/client/src/store/modules/auth.module.ts b/client/src/store/modules/auth.module.ts index d2f245cb..600bec42 100644 --- a/client/src/store/modules/auth.module.ts +++ b/client/src/store/modules/auth.module.ts @@ -1,15 +1,28 @@ import Api, { removeBearerToken, setBearerToken } from '@/api' -import { ChangePasswordDTO, Institution, InstitutionDTO, RegisterUserRequest, User, UserDTO } from '@/api/SwaggerApi' +import { + ChangePasswordDTO, + Institution, + InstitutionDTO, + RegisterUserRequest, + User, + UserDTO, +} from '@/api/SwaggerApi' import { config } from '@/config' import { InstitutionRole } from '@/models' import router, { AppRoute, navigationRoutes } from '@/router' import { parseJwt } from '@/util' -import { Actions, createMapper, Getters, Module, Mutations } from 'vuex-smart-module' +import { + Actions, + createMapper, + Getters, + Module, + Mutations, +} from 'vuex-smart-module' interface JwtData { - roles: InstitutionRole[]; - exp: number; - [key: string]: any; + roles: InstitutionRole[] + exp: number + [key: string]: any } class AuthState { @@ -34,9 +47,13 @@ class AuthGetters extends Getters { } routes(): AppRoute[] { - return navigationRoutes - .filter(r => (config.showAllViews || - this.getters.roles().some(a => r.meta?.navigationInfo?.authorities.includes(a)))) + return navigationRoutes.filter( + (r) => + config.showAllViews || + this.getters + .roles() + .some((a) => r.meta?.navigationInfo?.authorities.includes(a)) + ) } institutionUsers() { @@ -70,13 +87,20 @@ class AuthMutations extends Mutations { } } -class AuthActions extends Actions { +class AuthActions extends Actions< + AuthState, + AuthGetters, + AuthMutations, + AuthActions +> { async login(payload: { username: string; password: string }) { // # TODO loading animation, encrypt jwt - const token: string | undefined = (await Api.signInUserUsingPost({ - username: payload.username, - password: payload.password, - })).jwtToken + const token: string | undefined = ( + await Api.signInUserUsingPost({ + username: payload.username, + password: payload.password, + }) + ).jwtToken if (token) { this.commit('loginSuccess', token) this.dispatch('getAuthenticatedInstitution') diff --git a/client/src/store/modules/patients.module.ts b/client/src/store/modules/patients.module.ts index cca104c2..655becb2 100644 --- a/client/src/store/modules/patients.module.ts +++ b/client/src/store/modules/patients.module.ts @@ -1,7 +1,13 @@ import Api from '@/api' import { Patient } from '@/api/SwaggerApi' import { Vue } from 'vue/types/vue' -import { Actions, createMapper, Getters, Module, Mutations } from 'vuex-smart-module' +import { + Actions, + createMapper, + Getters, + Module, + Mutations, +} from 'vuex-smart-module' class PatientState { patient: Patient | undefined @@ -13,7 +19,7 @@ class PatientGetters extends Getters { if (this.state.patient && this.state.patient.id === id) { return this.state.patient } - return this.state.patients.find(patient => patient.id === id) + return this.state.patients.find((patient) => patient.id === id) } } @@ -31,7 +37,12 @@ class PatientMutations extends Mutations { } } -class PatientActions extends Actions { +class PatientActions extends Actions< + PatientState, + PatientGetters, + PatientMutations, + PatientActions +> { async fetchPatients(instance: Vue) { try { // this.commit('shared/startedLoading', 'fetchPatients', { root: true }) @@ -46,7 +57,10 @@ class PatientActions extends Actions { + async registerPatient(arg: { + patient: Patient + instance?: Vue + }): Promise { // commit('shared/startedLoading', 'registerPatient', { root: true }) try { const patientResponse = await Api.addPatientUsingPost(arg.patient) diff --git a/client/src/util/forms.ts b/client/src/util/forms.ts index 46539485..f12b155d 100644 --- a/client/src/util/forms.ts +++ b/client/src/util/forms.ts @@ -19,19 +19,22 @@ function prefixed(name: string, prefix?: string): string { interface MinimalFormContext { form: { - formItems: Record; - setFieldsValue(vals: Record): void; - getFieldsValue(names?: string[]): Record; - }; + formItems: Record< + string, + { + itemSelfUpdate: boolean + } + > + setFieldsValue(vals: Record): void + getFieldsValue(names?: string[]): Record + } } // >>>>> MIXIN DEFINITIONS >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> declare module 'vue/types/options' { interface ComponentOptions { - fieldIdentifiers?: string[]; + fieldIdentifiers?: string[] } } @@ -41,27 +44,27 @@ export interface FormGroupMixin { * Array of all the field identifiers used for the fields managed by * this form group component. */ - fieldIdentifiers?: string[]; - }; + fieldIdentifiers?: string[] + } $props: { /** * Optional prefix to be applied to all field names. */ - fieldNamePrefix?: string; + fieldNamePrefix?: string /** * Optional mapping of field identifiers to custom field names for some * or all fields. The names of fields not specified in this map are subject * to default field naming, respecting a provided field name prefix. */ - fieldNames?: Record; + fieldNames?: Record /** * Optional props to apply to some or all fields. The top level key * is the identifier of the form field with a map of the props to apply * to that field as the respective value. */ - controlProps?: Record>; - }; + controlProps?: Record> + } /** * Generates the name to be used for the field with the given identifier, @@ -69,27 +72,33 @@ export interface FormGroupMixin { * prefixing (see inputKeyPrefix prop). This function is typically called * to generate the appropriate field name for the v-decorator directive. */ - formFieldName(fieldId: string): string; + formFieldName(fieldId: string): string /** * Sets some or all fields of this form group. The second parameter specifies * whether the passed key-value mapping's keys specify field identifiers or * the actual field names as used in the form. */ - setData(data: Record, usesFormFieldNames?: boolean): void; + setData(data: Record, usesFormFieldNames?: boolean): void /** * Retrieves the values of some or all fields of this form group. The optional * second parameter specifies whether the given field name array specifies * field identifiers or the actual field names as used in the form. The result * mapping will map the values by the same names. */ - getData(fieldNames?: string[], usesFormFieldNames?: boolean): Record; + getData( + fieldNames?: string[], + usesFormFieldNames?: boolean + ): Record /** * Retrieves the value of one specific field of this form group. The optional * second parameter specifies whether the given field name array specifies * field identifiers or the actual field names as used in the form. */ - getSingleValue(fieldName: string, usesFormFieldNames?: boolean): any | undefined; + getSingleValue( + fieldName: string, + usesFormFieldNames?: boolean + ): any | undefined } /** @@ -127,7 +136,7 @@ export const FormGroupMixin = Vue.extend({ type: Object as PropType>>, default(): Record> { return Object.fromEntries( - this.$options.fieldIdentifiers!.map((key: string) => [key, {}]), + this.$options.fieldIdentifiers!.map((key: string) => [key, {}]) ) }, }, @@ -152,15 +161,18 @@ export const FormGroupMixin = Vue.extend({ // Make sure all form items use selfUpdate; this is crucial for form items // in the group to be re-rendered correctly when new values are set this.$options.fieldIdentifiers.forEach((key: string) => { - if (this.getFormContext().form.formItems[ - this.formFieldName(key)].itemSelfUpdate) { - console.error(`[ ${this.$options.name} ]: ` + - `\`itemSelfUpdate\` is not enabled for form item of \`${key}\`. ` + - 'This may lead to contents not being re-rendered when their ' + - 'value is modified by calling `setFieldsValue` on its ' + - 'containing form. To fix this, add `:selfUpdate="true"` to ' + - `the \`a-form-item\` component containing the \`${key}\` control ` + - 'or add `:selfUpdate="true"` to the root `a-form` element.', + if ( + this.getFormContext().form.formItems[this.formFieldName(key)] + .itemSelfUpdate + ) { + console.error( + `[ ${this.$options.name} ]: ` + + `\`itemSelfUpdate\` is not enabled for form item of \`${key}\`. ` + + 'This may lead to contents not being re-rendered when their ' + + 'value is modified by calling `setFieldsValue` on its ' + + 'containing form. To fix this, add `:selfUpdate="true"` to ' + + `the \`a-form-item\` component containing the \`${key}\` control ` + + 'or add `:selfUpdate="true"` to the root `a-form` element.' ) } }) @@ -168,9 +180,12 @@ export const FormGroupMixin = Vue.extend({ }, methods: { getFormContext() { - return typing.extended(this, typing.TypeArg<{ - FormContext: MinimalFormContext; - }>()).FormContext + return typing.extended( + this, + typing.TypeArg<{ + FormContext: MinimalFormContext + }>() + ).FormContext }, formFieldName(key: string): string { const propKeys = this.$props.fieldNames @@ -185,22 +200,30 @@ export const FormGroupMixin = Vue.extend({ // Check if conversion of identifiers to field names is required if (!usesFormFieldNames) { - data = Object.fromEntries(Object.entries(data).map( - (entry: [string, any]) => [this.formFieldName(entry[0]), entry[1]], - )) + data = Object.fromEntries( + Object.entries(data).map((entry: [string, any]) => [ + this.formFieldName(entry[0]), + entry[1], + ]) + ) } // Filter out any names that do not have a corresponding identifier // defined using the fieldIdentifiers option const fieldNames = this.$options.fieldIdentifiers.map(this.formFieldName) - data = Object.fromEntries(Object.entries(data).filter( - (value: [string, any]) => fieldNames.includes(value[0]), - )) + data = Object.fromEntries( + Object.entries(data).filter((value: [string, any]) => + fieldNames.includes(value[0]) + ) + ) // Set the values this.getFormContext().form.setFieldsValue(data) }, - getData(fieldNames?: string[], usesFormFieldNames?: boolean): Record { + getData( + fieldNames?: string[], + usesFormFieldNames?: boolean + ): Record { if (!this.$options.fieldIdentifiers) { throw new Error(`[ ${this.$options.name} ]: \`getData\` not supported`) } @@ -213,15 +236,17 @@ export const FormGroupMixin = Vue.extend({ }) } - const permittedFieldNames = this.$options.fieldIdentifiers.map(this.formFieldName) + const permittedFieldNames = this.$options.fieldIdentifiers.map( + this.formFieldName + ) if (fieldNames === undefined) { // No specific field names specified is equivalent to getting them all fieldNames = permittedFieldNames } else { // Filter out any names that do not have a corresponding identifier // defined using the fieldIdentifiers option - fieldNames = fieldNames.filter( - (fieldName: string) => permittedFieldNames.includes(fieldName), + fieldNames = fieldNames.filter((fieldName: string) => + permittedFieldNames.includes(fieldName) ) } @@ -229,14 +254,20 @@ export const FormGroupMixin = Vue.extend({ // as field identifiers, the result's keys will be translated back let result = this.getFormContext().form.getFieldsValue(fieldNames) if (!usesFormFieldNames) { - result = Object.fromEntries(Object.entries(result).map((entry: [string, any]) => - [this.formFieldNameBackTranslation[entry[0]], entry[1]], - )) + result = Object.fromEntries( + Object.entries(result).map((entry: [string, any]) => [ + this.formFieldNameBackTranslation[entry[0]], + entry[1], + ]) + ) } return result }, - getSingleValue(fieldName: string, usesFormFieldNames?: boolean): any | undefined { + getSingleValue( + fieldName: string, + usesFormFieldNames?: boolean + ): any | undefined { const key = usesFormFieldNames ? fieldName : this.formFieldName(fieldName) return this.getData([key], true)[key] }, @@ -245,7 +276,7 @@ export const FormGroupMixin = Vue.extend({ declare module 'vue/types/options' { interface ComponentOptions { - fieldValueConvert?(value: any): any; + fieldValueConvert?(value: any): any } } @@ -262,27 +293,27 @@ export interface FormControlMixin { * like the identity function for the native value format, since failure to do * so may lead to infinite recursion. */ - fieldValueConvert?(value: any): any; - }; + fieldValueConvert?(value: any): any + } /** * The name of this form control as specified by field decorator. */ - fieldName: string | null; + fieldName: string | null /** * Returns whether this form control is used in conjunction with a field decorator. */ - isDecoratedFormField(): boolean; + isDecoratedFormField(): boolean /** * Sets the value of this control. */ - setOwnValue(value: any): void; + setOwnValue(value: any): void /** * Retrieves the current value of this control. */ - getOwnValue(): any; + getOwnValue(): any } /** @@ -298,7 +329,7 @@ export const FormControlMixin = Vue.extend({ computed: { fieldName(): string | null { if (this.getFormContext() && this.$attrs['data-__field']) { - return (typing.cast<{ name: string }>(this.$attrs['data-__field'])).name + return typing.cast<{ name: string }>(this.$attrs['data-__field']).name } else { return null } @@ -311,9 +342,10 @@ export const FormControlMixin = Vue.extend({ }, created() { if (this.$options.fieldValueConvert) { - const valueProp = (this.$options.model && this.$options.model.prop) - ? this.$options.model.prop - : 'value' + const valueProp = + this.$options.model && this.$options.model.prop + ? this.$options.model.prop + : 'value' this.$watch(valueProp, (value: any) => { if (this.FormControlConvValue === value) { @@ -331,24 +363,29 @@ export const FormControlMixin = Vue.extend({ }, methods: { getFormContext(): MinimalFormContext { - return typing.extended(this, typing.TypeArg<{ - FormContext: MinimalFormContext; - }>()).FormContext + return typing.extended( + this, + typing.TypeArg<{ + FormContext: MinimalFormContext + }>() + ).FormContext }, isDecoratedFormField(): boolean { return !!(this.getFormContext() && this.$attrs['data-__field']) }, setOwnValue(value: any) { - const eventType = (this.$options.model && this.$options.model.event) - ? this.$options.model.event - : 'change' + const eventType = + this.$options.model && this.$options.model.event + ? this.$options.model.event + : 'change' this.$emit(eventType, value) }, getOwnValue() { - const valueProp = (this.$options.model && this.$options.model.prop) - ? this.$options.model.prop - : 'value' + const valueProp = + this.$options.model && this.$options.model.prop + ? this.$options.model.prop + : 'value' return (this as Record)[valueProp] }, }, diff --git a/client/src/util/index.ts b/client/src/util/index.ts index b0d88ba0..04b25635 100644 --- a/client/src/util/index.ts +++ b/client/src/util/index.ts @@ -1,14 +1,19 @@ export function parseJwt(token: string): any { const base64Url = token.split('.')[1] const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/') - const jsonPayload = decodeURIComponent(atob(base64).split('').map(function(c) { - return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2) - }).join('')) + const jsonPayload = decodeURIComponent( + atob(base64) + .split('') + .map(function (c) { + return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2) + }) + .join('') + ) return JSON.parse(jsonPayload) } export function anonymizeProperties(keys: any[], obj: any) { - keys.forEach(key => { + keys.forEach((key) => { if (typeof key === 'string' && obj[key]) { obj[key] = obj[key].substr(0, 1) + '**********' } else if (typeof key === 'object' && key.type === 'number' && obj[key]) { diff --git a/client/src/util/mapping.ts b/client/src/util/mapping.ts index 02422984..714fe506 100644 --- a/client/src/util/mapping.ts +++ b/client/src/util/mapping.ts @@ -1,13 +1,15 @@ export declare interface MapperDefEntry { - transform: (x: any) => any; + transform: (x: any) => any +} +export declare type MapperDef = { + [x: string]: (x: any) => any | Partial } -export declare type MapperDef = { [x: string]: ((x: any) => any | Partial) }; -export function map(object: {[x: string]: any}, mapperDef: MapperDef) { - return Object.fromEntries(Object.entries(object).map( - (entry: [any, any]) => { - const key = entry[0]; - let val = entry[1]; +export function map(object: { [x: string]: any }, mapperDef: MapperDef) { + return Object.fromEntries( + Object.entries(object).map((entry: [any, any]) => { + const key = entry[0] + let val = entry[1] let mapperEntry: any = mapperDef[key] @@ -24,6 +26,6 @@ export function map(object: {[x: string]: any}, mapperDef: MapperDef) { } return entry - }, - )) + }) + ) } diff --git a/client/src/util/permissions.ts b/client/src/util/permissions.ts index a9cb7b9a..750b23c9 100644 --- a/client/src/util/permissions.ts +++ b/client/src/util/permissions.ts @@ -2,10 +2,10 @@ import Api from '@/api' import { Api as ApiDefs } from '@/api/SwaggerApi' export interface ApiParams { - path: string; - method: string; + path: string + method: string } -export type ApiFunction = (...args: any[]) => any; +export type ApiFunction = (...args: any[]) => any // Retrieve the request method and path for the given Swagger API function export function queryApiParams(apiFunc: ApiFunction): ApiParams { @@ -18,7 +18,7 @@ export function queryApiParams(apiFunc: ApiFunction): ApiParams { */ // Step 1: Create API copy with surrogate `request` operation - let resultParams = undefined as (undefined | ApiParams) + let resultParams = undefined as undefined | ApiParams const myApiDefs = new ApiDefs() as any myApiDefs.request = (path: string, method: string) => { resultParams = { @@ -43,9 +43,14 @@ export function queryApiParams(apiFunc: ApiFunction): ApiParams { } } -export async function checkAllowed(funcs: ApiFunction): Promise; -export async function checkAllowed(funcs: ApiFunction[] | undefined): Promise; -export async function checkAllowed, R extends { [key in keyof T]: boolean }>(funcs: T): Promise; +export async function checkAllowed(funcs: ApiFunction): Promise +export async function checkAllowed( + funcs: ApiFunction[] | undefined +): Promise +export async function checkAllowed< + T extends Record, + R extends { [key in keyof T]: boolean } +>(funcs: T): Promise // export function checkAllowed(funcs: Record): Record; export async function checkAllowed(funcs: any): Promise { const resultLabels = [] as string[] @@ -69,8 +74,10 @@ export async function checkAllowed(funcs: any): Promise { // const params = funcs.map(queryApiParams) // Make the permission asking request - const apiResult = await Api.queryPermissionsUsingPost(Object.fromEntries( - funcs.map((func: ApiFunction) => [func.name, queryApiParams(func)]), + const apiResult = (await Api.queryPermissionsUsingPost( + Object.fromEntries( + funcs.map((func: ApiFunction) => [func.name, queryApiParams(func)]) + ) )) as Record const result = [] as boolean[] diff --git a/client/src/util/plz-service.ts b/client/src/util/plz-service.ts index 7df9733e..fb63ec74 100644 --- a/client/src/util/plz-service.ts +++ b/client/src/util/plz-service.ts @@ -5,18 +5,22 @@ const safeParseResponse = (response: Response): Promise => .catch((e) => response.text) export interface PlzFields { - plz: string; - note: string; // city + plz: string + note: string // city } export interface Plz { - fields: PlzFields; + fields: PlzFields } export async function getPlzs(plz: string): Promise { - return fetch('https://public.opendatasoft.com/api/records/1.0/search/?dataset=postleitzahlen-deutschland&facet=plz&q=' + plz, { - method: 'GET', - }).then(async(response) => { + return fetch( + 'https://public.opendatasoft.com/api/records/1.0/search/?dataset=postleitzahlen-deutschland&facet=plz&q=' + + plz, + { + method: 'GET', + } + ).then(async (response) => { const data = await safeParseResponse(response) if (!response.ok) throw data return data.records diff --git a/client/src/util/typing.ts b/client/src/util/typing.ts index 0eaaf3f8..223f3d28 100644 --- a/client/src/util/typing.ts +++ b/client/src/util/typing.ts @@ -3,7 +3,7 @@ */ /// Type representing a Type parameter to be passed to a type-inferring function. -export type TypeArg = T; +export type TypeArg = T /** * TypeArg generator. This function may be called for any TypeArg function @@ -13,13 +13,15 @@ export type TypeArg = T; * object is created. */ export function TypeArg(): TypeArg { - return null as unknown as TypeArg + return (null as unknown) as TypeArg } /** * Simple cast avoiding explicit conversion to unknown. */ -export function cast(arg: unknown, _?: TypeArg) { return arg as T } +export function cast(arg: unknown, _?: TypeArg) { + return arg as T +} /** * Identity function telling the Typescript compiler that the supplied argument @@ -30,6 +32,9 @@ export function cast(arg: unknown, _?: TypeArg) { return arg as T } * An example use case is the use of mixins or injections for Vue components, * which currently cannot be sufficiently inferred. */ -export function extended(obj: T, _: TypeArg): (T & ExtensionType) { - return obj as (T & ExtensionType) +export function extended( + obj: T, + _: TypeArg +): T & ExtensionType { + return obj as T & ExtensionType } diff --git a/client/src/views/Account.vue b/client/src/views/Account.vue index 6ade91dd..4a86746b 100644 --- a/client/src/views/Account.vue +++ b/client/src/views/Account.vue @@ -1,46 +1,70 @@ - + { showChangePasswordForm = false }" - @create="() => { showChangePasswordForm = false }" + @cancel=" + () => { + showChangePasswordForm = false + } + " + @create=" + () => { + showChangePasswordForm = false + } + " :visible="showChangePasswordForm" /> { showAddOrChangeUserForm = false }" - @create="() => { showAddOrChangeUserForm = false }" + @cancel=" + () => { + showAddOrChangeUserForm = false + } + " + @create=" + () => { + showAddOrChangeUserForm = false + } + " :visible="showAddOrChangeUserForm" :user="addOrChangeUser" /> { showChangeInstitutionForm = false }" - @create="() => { showChangeInstitutionForm = false }" + @cancel=" + () => { + showChangeInstitutionForm = false + } + " + @create=" + () => { + showChangeInstitutionForm = false + } + " :visible="showChangeInstitutionForm" :institution="institution" /> - + Benutzername: - {{user.username}} + {{ user.username }} Name: - {{user.firstName}} {{user.lastName}} + {{ user.firstName }} {{ user.lastName }} Rollen: - - {{roleMapping[authority.authority]}} + + {{ roleMapping[authority.authority] }} @@ -50,43 +74,69 @@ - + Name: - {{institution.name}} + {{ institution.name }} Typ: - {{typeMapping[institution.institutionType]}} + {{ typeMapping[institution.institutionType] }} Adresse: - {{institution.street}} {{institution.houseNumber}}{{institution.zip}} {{institution.city}} + + {{ institution.street }} {{ institution.houseNumber }}{{ + institution.zip + }} + {{ institution.city }} + E-Mail: - {{institution.email}} + {{ institution.email }} Telefonnummer: - {{institution.phoneNumber}} + {{ institution.phoneNumber }} Kommentar: - {{institution.comment}} + {{ institution.comment }} - + Bearbeiten - - - + + + Hinzufügen @@ -96,11 +146,16 @@ rowKey="email" > - {{ roleMapping[authorities[1].authority]}} + {{ roleMapping[authorities[1].authority] }} - changeUser(user)" icon="edit" - style="margin-right: 10px"> + changeUser(user)" + icon="edit" + style="margin-right: 10px;" + > diff --git a/client/src/views/LandingPage.vue b/client/src/views/LandingPage.vue index d5d84dec..3f2b46fc 100644 --- a/client/src/views/LandingPage.vue +++ b/client/src/views/LandingPage.vue @@ -4,76 +4,109 @@ - Schnell. Einfach. Zuverlässig. - + + Schnell. Einfach. Zuverlässig. + + - + Kontakt - + DevPost - + Twitter - + GitHub - - - + + + - Das Infektionsmelde- und Informationssystem + + Das Infektionsmelde- und Informationssystem + - + {{ paragraph1 }} - + {{ paragraph2 }} - + Zur Selbstregistrierung + >Zur Selbstregistrierung - + Zur Live-Demo + >Zur Live-Demo - - Der IMIS-Meldeprozess - Einfach, Schnell, Zentral + Gemeinsam gegen COVID-19 - IMIS ist aus dem #WirVsVirus-Hackathon - hervorgegangen und - wird nun von einem Team aus über 30 Leuten aktiv weiterentwickelt. + + IMIS ist aus dem + #WirVsVirus-Hackathon + hervorgegangen und wird nun von einem Team aus über 30 Leuten aktiv + weiterentwickelt. + - Als eines von 130 aus über 1500 Projekten ist IMIS in den - Solution Enabler - aufgenommen worden. - Dadurch erhalten wir Zugriff auf Coaching- und Unterstützungsangebote sowie Ressourcen und - Expertise. + + Als eines von 130 aus über 1500 Projekten ist IMIS in den + Solution Enabler + aufgenommen worden. + + + Dadurch erhalten wir Zugriff auf Coaching- und + Unterstützungsangebote sowie Ressourcen und Expertise. + Eine anspruchsvolle Aufgabe. - Und deshalb sind wir ein vielfältiges Team aus Organisatoren und Projektkoordinatoren, Frontend- und - Backend-Entwicklern, UI-Designern, Datenbankspezialisten und vielen mehr. + + Und deshalb sind wir ein vielfältiges Team aus Organisatoren und + Projektkoordinatoren, Frontend- und Backend-Entwicklern, + UI-Designern, Datenbankspezialisten und vielen mehr. + - + @@ -121,12 +172,12 @@ Kontakt: - + Andrey Eganov 0681 / 500 66 72 0 imis-team@gmx.de @@ -151,228 +202,221 @@ export default Vue.extend({ 'Das Sammeln und Verwalten der aktuellen Infektionszahlen erfordert extrem viel Zeit und Ressourcen. IMIS ist unsere Lösung für eine schnelle, sichere und einfache Infektionsfall-Erfassung für Ärzte, Gesundheitsämter und Labore. Damit ermöglichen wir, dass wichtige Ressourcen frei werden, die dann an anderer Stelle gewinnbringend eingesetzt werden können.', paragraph2: 'Die COVID-19-Pandemie stellt für das bestehende Infektionsmeldewesen in Deutschland eine immense Herausforderung dar. Ärzte und über 400 Gesundheitsämter benutzen gegenwärtig einen uneinheitlichen Mix aus Telefon, Fax, E-Mails und Excel-Tabellen, um Fälle zusammenzutragen. Eine starke Auslastung der Gesundheitsämter und verspätete Meldungen sind die Folge. So erhalten Entscheider häufig nur lückenhafte Statistiken. Diese Lage sorgt für Unsicherheit und macht es schwer, angemessen auf die Krise zu reagieren. ', - } }, }) diff --git a/client/src/views/Login.vue b/client/src/views/Login.vue index d7e07cd4..65452ed1 100644 --- a/client/src/views/Login.vue +++ b/client/src/views/Login.vue @@ -1,23 +1,28 @@ - + - - Login + + Login @@ -26,19 +31,32 @@ - - + + Einloggen @@ -49,8 +67,13 @@ test_lab mit Passwort asdf test_doctor mit Passwort asdf --> - test_testing_site mit Passwort asdf - test_department_of_health mit Passwort asdf + + test_testing_site mit Passwort asdf + + + test_department_of_health mit Passwort + asdf + @@ -58,7 +81,6 @@ + + diff --git a/client/src/views/PatientDetails.vue b/client/src/views/PatientDetails.vue index 32b85b23..71cf6ed6 100644 --- a/client/src/views/PatientDetails.vue +++ b/client/src/views/PatientDetails.vue @@ -1,13 +1,24 @@ - + { showChangePatientStammdatenForm = false }" - @create="() => { showChangePatientStammdatenForm = false; this.loadData() }" + @cancel=" + () => { + showChangePatientStammdatenForm = false + } + " + @create=" + () => { + showChangePatientStammdatenForm = false + this.loadData() + } + " :visible="showChangePatientStammdatenForm" :patient="patient" /> - - + - - - - - Neuen Test anordnen - - - - Quarantäne vormerken - - - Aktionen - - - + + + + + Neuen Test anordnen + + + + Quarantäne vormerken + + + + Aktionen + + + - - + + Daten ändern - - - - - - - Name: - {{patient.lastName}}, {{patient.firstName}} - - - Geburtsdatum: - {{dateOfBirth}} - - - Geschlecht: - {{gender}} - - - Staatsangehörigkeit: - {{patient.nationality}} - - - - - - - - - Straße/Hausnr.: - {{patient.street}} {{patient.houseNumber}} - - - PLZ/Ort: - {{patient.zip}} {{patient.city}} - - - Land: - {{patient.country}} - - - - - - - - - - - - - Telefonnummer: - {{patient.phoneNumber}} - - - Email: - {{patient.email}} - - - - - - - - - Versicherung: - {{patient.insuranceCompany}} - - - V-Nr: - {{patient.insuranceMembershipNumber}} - - - - - - - - - Beruf: - {{patient.occupation || 'Keine Angabe'}} - - - Arbeitgeber: - {{patient.employer || 'Keine Angabe'}} - - - - - + + + + + + + Name: + {{ patient.lastName }}, {{ patient.firstName }} + + + Geburtsdatum: + {{ dateOfBirth }} + + + Geschlecht: + {{ gender }} + + + Staatsangehörigkeit: + {{ patient.nationality }} + + + + + + + + + Straße/Hausnr.: + {{ patient.street }} {{ patient.houseNumber }} + + + PLZ/Ort: + {{ patient.zip }} {{ patient.city }} + + + Land: + {{ patient.country }} + + + + + + + + + + + + + Telefonnummer: + {{ patient.phoneNumber }} + + + Email: + + {{ patient.email }} + + + + + + + + + + Versicherung: + {{ patient.insuranceCompany }} + + + V-Nr: + {{ patient.insuranceMembershipNumber }} + + + + + + + + + Beruf: + {{ patient.occupation || 'Keine Angabe' }} + + + Arbeitgeber: + {{ patient.employer || 'Keine Angabe' }} + + + + + - - - - - - - - Fall-Status: {{(patientStatus ? patientStatus.label : 'Unbekannt') + (patient.quarantineUntil ? - (', Quarantäne angeordnet bis ' + patient.quarantineUntil) : '')}} - - Erkrankungsdatum: {{dateOfIllness}} - Meldedatum: {{dateOfReporting}} + + + + + + + + Fall-Status: + {{ + (patientStatus ? patientStatus.label : 'Unbekannt') + + (patient.quarantineUntil + ? ', Quarantäne angeordnet bis ' + + moment(patient.quarantineUntil).format('DD.MM.YYYY') + : '') + }} + + + Erkrankungsdatum: {{ dateOfIllness }} + + + Meldedatum: {{ dateOfReporting }} - - - {{getDate(lastUpdate)}} - - - - {{testResults.find(type => type.id === testStatus).label}} - - - - {{testTypes.find(type => type.id === testType).label}} - - - - - - - - - - - - Kontakte mit Indexpatienten - {{ patientInfectionSources.length }} - Keine - bekannt - - - Eigene Kontaktpersonen - {{ exposureContacts.length }} - Keine - angegeben - - - - - - - - {{illness}} - - - + {{ getDate(lastUpdate) }} + + + + {{ + testResults.find((type) => type.id === testStatus).label + }} + + + + {{ testTypes.find((type) => type.id === testType).label }} + + + + + + + + + + + + + + Kontakte mit Indexpatienten + {{ + patientInfectionSources.length + }} + Keine + bekannt + + + + Eigene Kontaktpersonen + {{ + exposureContacts.length + }} + Keine + angegeben + + + + + + + + + {{ illness }} + + + + + - - {{symptom}} - - - + + {{ symptom }} + + + + - + @@ -274,9 +292,13 @@ v-for="incident in this.incidents" > {{ formatDate(incident.eventDate) }}, - {{ eventTypes.find(type => type.id === incident.eventType).label }} - - erfasst {{ formatTimestamp(incident.versionTimestamp) }} durch {{ incident.versionUser.institution.name }} + {{ + eventTypes.find((type) => type.id === incident.eventType) + .label + }} + + erfasst {{ formatTimestamp(incident.versionTimestamp) }} durch + {{ incident.versionUser.institution.name }} erfasst {{ formatTimestamp(incident.versionTimestamp) }} @@ -285,32 +307,41 @@ - - + - - + + - Kontakte mit Indexpatienten ({{ patientInfectionSources.length }}) + Kontakte + mit Indexpatienten + ({{ patientInfectionSources.length }}) - - {{ contact.source.firstName }} {{ contact.source.lastName }}, + {{ contact.source.firstName }} + {{ contact.source.lastName }}, - am {{ moment(contact.dateOfContact).format('DD.MM.YYYY') }}, + am + {{ + moment(contact.dateOfContact).format('DD.MM.YYYY') + }}, @@ -318,8 +349,7 @@ Keine bekannt - + Keine bekannt @@ -337,7 +367,8 @@ + @click="addExposureContact" + > Hinzufügen @@ -347,24 +378,28 @@ :pagination="false" :dataSource="exposureContacts" class="imis-table-no-pagination" - :rowKey="contact => contact.contact.id" + :rowKey="(contact) => contact.contact.id" :loading="exposureContactsLoading" - :customRow="contact => ({ - on: { dblclick: () => showExposureContact(contact.id) } - })"> + :customRow=" + (contact) => ({ + on: { dblclick: () => showExposureContact(contact.id) }, + }) + " + > - + @click="showPatient(contact.contact.id)" + /> {{ moment(contact.dateOfContact).format('DD.MM.YYYY') }} - + Infiziert @@ -375,38 +410,58 @@ In Quarantäne - + Keine Quarantäne - - + + @click="removeExposureContact(contact.id)" + /> - - + @cancel="exposureContactInEditing = null" + > + + :contactPatient=" + exposureContactInEditing + ? exposureContactInEditing.contact + : null + " + /> @@ -420,10 +475,21 @@ import Vue from 'vue' import moment, { Moment } from 'moment' import Api from '@/api' import * as permissions from '@/util/permissions' -import { LabTest, Patient, Timestamp, ExposureContactFromServer, Incident } from '@/api/SwaggerApi' +import { + LabTest, + Patient, + Timestamp, + ExposureContactFromServer, + Incident, +} from '@/api/SwaggerApi' import { authMapper } from '@/store/modules/auth.module' import { patientMapper } from '@/store/modules/patients.module' -import { EventTypeItem, eventTypes, testResults, TestResultType } from '@/models/event-types' +import { + EventTypeItem, + eventTypes, + testResults, + TestResultType, +} from '@/models/event-types' import { SYMPTOMS } from '@/models/symptoms' import { PRE_ILLNESSES } from '@/models/pre-illnesses' import { Column } from 'ant-design-vue/types/table/column' @@ -438,28 +504,32 @@ const columnsTests: Partial[] = [ title: 'Test ID', dataIndex: 'testId', key: 'testId', - }, { + }, + { title: 'Test Typ', dataIndex: 'testType', key: 'testType', scopedSlots: { customRender: 'testType', }, - }, { + }, + { title: 'Test Status', dataIndex: 'testStatus', key: 'testStatus', scopedSlots: { customRender: 'testStatus', }, - }, { + }, + { title: 'Aktualisiert', dataIndex: 'lastUpdate', key: 'lastUpdate', scopedSlots: { customRender: 'lastUpdate', }, - }, { + }, + { title: 'Kommentar', dataIndex: 'comment', key: 'comment', @@ -546,31 +616,31 @@ const columnsIndexPatients = [ interface State { permissions: { - sendToQuarantine: boolean; - }; - patient: undefined | Patient; - patientInfectionSources: ExposureContactFromServer[]; - exposureContacts: ExposureContactFromServer[]; - exposureContactsLoading: boolean; - exposureContactForm: any; - exposureContactInEditing: any; - patientStatus: EventTypeItem | undefined; - eventTypes: any[]; - symptoms: string[]; - preIllnesses: string[]; - dateOfBirth: string; - showChangePatientStammdatenForm: boolean; - gender: string; - tests: LabTest[]; - columnsTests: Partial[]; - columnsExposureContacts: Partial[]; - columnsIndexPatients: Partial[]; - testResults: TestResultType[]; - testTypes: TestTypeItem[]; - dateOfReporting: string; - dateOfIllness: string; - dateFormat: string; - incidents: any[]; + sendToQuarantine: boolean + } + patient: undefined | Patient + patientInfectionSources: ExposureContactFromServer[] + exposureContacts: ExposureContactFromServer[] + exposureContactsLoading: boolean + exposureContactForm: any + exposureContactInEditing: any + patientStatus: EventTypeItem | undefined + eventTypes: any[] + symptoms: string[] + preIllnesses: string[] + dateOfBirth: string + showChangePatientStammdatenForm: boolean + gender: string + tests: LabTest[] + columnsTests: Partial[] + columnsExposureContacts: Partial[] + columnsIndexPatients: Partial[] + testResults: TestResultType[] + testTypes: TestTypeItem[] + dateOfReporting: string + dateOfIllness: string + dateFormat: string + incidents: any[] } export default Vue.extend({ @@ -591,9 +661,10 @@ export default Vue.extend({ async created() { this.loadData() this.exposureContactForm = this.$form.createForm( - this.$refs.exposureContactModal as Vue, { + this.$refs.exposureContactModal as Vue, + { name: 'exposure-contact', - }, + } ) }, @@ -659,39 +730,66 @@ export default Vue.extend({ this.incidents = await Api.getPatientLogUsingGet(patientId) this.incidents.sort((a: Incident, b: Incident) => { - return a.eventDate!.localeCompare(b.eventDate!) || a.versionTimestamp!.localeCompare(b.versionTimestamp!) + return ( + a.eventDate!.localeCompare(b.eventDate!) || + a.versionTimestamp!.localeCompare(b.versionTimestamp!) + ) }) if (this.patient.events) { - const event = this.patient.events.find(event => event.eventType === 'REGISTERED' || event.eventType === 'SUSPECTED') + const event = this.patient.events.find( + (event) => + event.eventType === 'REGISTERED' || event.eventType === 'SUSPECTED' + ) if (event) { - this.dateOfReporting = moment(event.eventTimestamp).format(this.dateFormat) + this.dateOfReporting = moment(event.eventTimestamp).format( + this.dateFormat + ) } } if (this.patient.dateOfIllness) { - this.dateOfIllness = moment(this.patient.dateOfIllness).format(this.dateFormat) + this.dateOfIllness = moment(this.patient.dateOfIllness).format( + this.dateFormat + ) } else { this.dateOfIllness = this.dateOfReporting } // Map patient attributes to their display representation - this.patientStatus = eventTypes.find(type => type.id === this.patient?.patientStatus) - this.symptoms = this.patient.symptoms?.map(symptom => { - const patientSymptom = SYMPTOMS.find(symptomFind => symptomFind.value === symptom) - return patientSymptom ? patientSymptom.label : symptom - }) || [] - this.preIllnesses = this.patient.preIllnesses?.map(preIllness => { - const patientIllness = PRE_ILLNESSES.find(illness => illness.value === preIllness) - return patientIllness ? patientIllness.label : preIllness - }) || [] - this.dateOfBirth = moment(this.patient.dateOfBirth).format(this.dateFormat) + this.patientStatus = eventTypes.find( + (type) => type.id === this.patient?.patientStatus + ) + this.symptoms = + this.patient.symptoms?.map((symptom) => { + const patientSymptom = SYMPTOMS.find( + (symptomFind) => symptomFind.value === symptom + ) + return patientSymptom ? patientSymptom.label : symptom + }) || [] + this.preIllnesses = + this.patient.preIllnesses?.map((preIllness) => { + const patientIllness = PRE_ILLNESSES.find( + (illness) => illness.value === preIllness + ) + return patientIllness ? patientIllness.label : preIllness + }) || [] + this.dateOfBirth = moment(this.patient.dateOfBirth).format( + this.dateFormat + ) const patientGender = this.patient.gender || '' - this.gender = patientGender === 'male' ? 'männlich' : patientGender === 'female' ? 'weiblich' : 'divers' + this.gender = + patientGender === 'male' + ? 'männlich' + : patientGender === 'female' + ? 'weiblich' + : 'divers' // Source of Infection try { - this.patientInfectionSources = await Api.getExposureSourceContactsForPatientUsingGet(patientId) + this.patientInfectionSources = await Api.getExposureSourceContactsForPatientUsingGet( + patientId + ) } catch (e) { this.patientInfectionSources = [] } @@ -700,7 +798,9 @@ export default Vue.extend({ this.tests = await Api.getLabTestForPatientUsingGet(patientId) // Retrieve exposure contacts data - this.exposureContacts = await Api.getExposureContactsForPatientUsingGet(patientId) + this.exposureContacts = await Api.getExposureContactsForPatientUsingGet( + patientId + ) this.exposureContactsLoading = false }, timelineColor(eventType: any) { @@ -753,18 +853,20 @@ export default Vue.extend({ if (this.patient) { Api.createOrderTestEventUsingPost({ patientId: this.patient.id, - }).then(() => { - this.$notification.success({ - message: 'Test angefordert', - description: '', + }) + .then(() => { + this.$notification.success({ + message: 'Test angefordert', + description: '', + }) + this.loadData() }) - this.loadData() - }).catch(() => { - this.$notification.error({ - message: 'Es ist ein Fehler aufgetreten', - description: '', + .catch(() => { + this.$notification.error({ + message: 'Es ist ein Fehler aufgetreten', + description: '', + }) }) - }) } }, addExposureContact() { @@ -778,52 +880,65 @@ export default Vue.extend({ }) }, showExposureContact(contactId: number) { - const contact = this.exposureContacts.find((contact: any) => contact.id === contactId) + const contact = this.exposureContacts.find( + (contact: any) => contact.id === contactId + ) this.exposureContactInEditing = contact Vue.nextTick(() => { - this.exposureContactForm.setFieldsValue(map(contact as {[x: string]: any}, { - // source: patient => patient.id, - // contact: patient => patient.id, - contact: contact => contact.id, - dateOfContact: moment, - })) + this.exposureContactForm.setFieldsValue( + map(contact as { [x: string]: any }, { + // source: patient => patient.id, + // contact: patient => patient.id, + contact: (contact) => contact.id, + dateOfContact: moment, + }) + ) }) }, persistExposureContact() { - const stringFromMoment = (value: Moment): string => value.format('YYYY-MM-DD') - - this.exposureContactForm.validateFields() - .then(async(values: any) => { - // Convert values to transport format - values = map(values, { - id: parseInt, - dateOfContact: stringFromMoment, - }) + const stringFromMoment = (value: Moment): string => + value.format('YYYY-MM-DD') + + this.exposureContactForm.validateFields().then(async (values: any) => { + // Convert values to transport format + values = map(values, { + id: parseInt, + dateOfContact: stringFromMoment, + }) - // send initial patient data in contact field as string - if (!values.contact) { - values.contact = JSON.stringify({ - firstName: values.contactFirstName, - lastName: values.contactLastName, - gender: values.contactGender, - dateOfBirth: values.contactDateOfBirth ? stringFromMoment(values.contactDateOfBirth) : undefined, - }) - } + // send initial patient data in contact field as string + if (!values.contact) { + values.contact = JSON.stringify({ + firstName: values.contactFirstName, + lastName: values.contactLastName, + gender: values.contactGender, + dateOfBirth: values.contactDateOfBirth + ? stringFromMoment(values.contactDateOfBirth) + : undefined, + }) + } - if (values.id) { - Object.assign(this.exposureContactInEditing, await Api.updateExposureContactUsingPut(values)) - } else { - this.exposureContacts.push(await Api.createExposureContactUsingPost(values)) - } + if (values.id) { + Object.assign( + this.exposureContactInEditing, + await Api.updateExposureContactUsingPut(values) + ) + } else { + this.exposureContacts.push( + await Api.createExposureContactUsingPost(values) + ) + } - this.exposureContactInEditing = null - }) + this.exposureContactInEditing = null + }) }, async removeExposureContact(contactId: number) { await Api.removeExposureContactUsingDelete(contactId) - this.exposureContacts = this.exposureContacts.filter(contact => contact.id !== contactId) + this.exposureContacts = this.exposureContacts.filter( + (contact) => contact.id !== contactId + ) }, showPatient(patientId: string) { (this.$refs.exposureContactModal as Modal).$emit('cancel') @@ -839,57 +954,57 @@ export default Vue.extend({ diff --git a/client/src/views/PatientList.vue b/client/src/views/PatientList.vue index bf0fdd58..9b942656 100644 --- a/client/src/views/PatientList.vue +++ b/client/src/views/PatientList.vue @@ -296,20 +296,20 @@ const columnsSchema: Partial[] = [ ] interface SimpleForm { - query: string; - order: string; - orderBy: string; - offsetPage: number; - pageSize: number; + query: string + order: string + orderBy: string + offsetPage: number + pageSize: number } interface State { - form: SimpleForm; - advancedForm: Partial; - quarantineSelection: string; - currentPatients: Patient[]; + form: SimpleForm + advancedForm: Partial + quarantineSelection: string + currentPatients: Patient[] - [key: string]: any; + [key: string]: any } export default Vue.extend({ @@ -451,14 +451,16 @@ export default Vue.extend({ queryPromise .then((result) => { const header = - 'ID;Vorname;Nachname;Geschlecht;Status;Geburtsdatum;Stadt;E-Mail;Telefonnummer;' + - 'Straße;Hausnummer;Stadt;Versicherung;Versichertennummer' + 'ID;Vorname;Nachname;Geschlecht;Status;Geburtsdatum;E-Mail;Telefonnummer;' + + 'Straße;Hausnummer;PLZ;Stadt;Versicherung;Versichertennummer' const patients = result .map( (patient: Patient) => - `${patient.id};${patient.firstName};${patient.lastName};${patient.gender};${patient.patientStatus};` + - `${patient.dateOfBirth};${patient.city};${patient.email};${patient.phoneNumber};${patient.street};` + - `${patient.houseNumber};${patient.city};${patient.insuranceCompany};${patient.insuranceMembershipNumber}` + `${patient.id};${patient.firstName};${patient.lastName};${patient.gender};` + + `${patient.patientStatus};${patient.dateOfBirth};${patient.email};` + + `${patient.phoneNumber};${patient.street};${patient.houseNumber};` + + `${patient.zip};${patient.city};${patient.insuranceCompany};` + + `${patient.insuranceMembershipNumber}` ) .join('\n') const filename = diff --git a/client/src/views/PublicRegister.vue b/client/src/views/PublicRegister.vue index b4b8495e..6b3ed5bc 100644 --- a/client/src/views/PublicRegister.vue +++ b/client/src/views/PublicRegister.vue @@ -2,99 +2,155 @@ - - Zur Startseite + + Zur Startseite - + - Selbstregistrierung + Selbstregistrierung - - this.current = current" - :current="current" - :direction="stepsDirection"> - - + + (this.current = current)" + :current="current" + :direction="stepsDirection" + > + - - + - Welche der folgenden Symptome hatten Sie in den letzten 24h? - - + + Welche der folgenden Symptome hatten Sie in den letzten 24h? + + - - + + - + Andere: - + - + Welche Formen der Exposition treffen auf Sie zu? - - - + + - + - - + + - + - Welche Vorerkrankungen und Risikofaktoren treffen auf Sie zu? - - + + Welche Vorerkrankungen und Risikofaktoren treffen auf Sie zu? + + - - + + - - Andere: + + Andere: - + - + - + Bitte erfassen Sie nun Ihre persönlichen Daten. @@ -102,45 +158,63 @@ - Sie haben es fast geschafft! + + + Abschließen und Daten übermitteln + + + - Ich erkläre mich mit der Übermittlung meiner Daten zur weiteren - Verarbeitung einverstanden. + Ich erkläre mich mit der Übermittlung meiner Daten zur + weiteren Verarbeitung einverstanden. - Bitte bestätigen + Bitte bestätigen - - - Daten übermitteln - Geschafft! - - + + Sie erhalten in Kürze eine Email zur Bestätigung. - Zur Startseite + Zur Startseite - - + + + + + + + Absenden + - - diff --git a/client/src/views/RegisterInstitution.vue b/client/src/views/RegisterInstitution.vue index a07cbb4e..f3bad365 100644 --- a/client/src/views/RegisterInstitution.vue +++ b/client/src/views/RegisterInstitution.vue @@ -1,12 +1,9 @@ - + - + Registrieren Sie hier eine neue Instutition in IMIS. @@ -16,31 +13,44 @@ :form="form" :layout="'horizontal'" :labelCol="{ span: 6 }" - :wrapperCol="{ span: 18}" + :wrapperCol="{ span: 18 }" @submit.prevent="handleSubmit" > - + - + - - + @@ -51,16 +61,27 @@ Labor - Arztpraxis + Arztpraxis Klinik - Teststelle + Teststelle @@ -106,8 +127,8 @@ @@ -118,10 +139,10 @@ - - Ich erkläre mich mit der Übermittlung dieser Daten zur weiteren - Verarbeitung einverstanden. - + + Ich erkläre mich mit der Übermittlung dieser Daten zur + weiteren Verarbeitung einverstanden. + @@ -184,7 +205,10 @@ export default Vue.extend({ // For Explanation of dynamic form validation // see https://www.antdv.com/components/form/#components-form-demo-handle-form-data-manually handlePasswordRepeatChange(event: any) { - this.passwordRepeat = validatePasswordRepeat(this.form.getFieldValue('password'), event.target.value) + this.passwordRepeat = validatePasswordRepeat( + this.form.getFieldValue('password'), + event.target.value + ) }, handleSubmit() { this.form.validateFields((err: any, values: any) => { @@ -204,24 +228,26 @@ export default Vue.extend({ userRole: 'USER_ROLE_ADMIN', }, } - Api.registerInstitutionUsingPost(values).then(() => { - this.form.resetFields() - const notification = { - message: 'Institution registriert.', - description: 'Die Institution wurde erfolgreich registriert.', - } - this.$notification.success(notification) - this.$store.dispatch('authModule/login', { - username: values.username, - password: values.password, + Api.registerInstitutionUsingPost(values) + .then(() => { + this.form.resetFields() + const notification = { + message: 'Institution registriert.', + description: 'Die Institution wurde erfolgreich registriert.', + } + this.$notification.success(notification) + this.$store.dispatch('authModule/login', { + username: values.username, + password: values.password, + }) + }) + .catch((err) => { + const notification = { + message: 'Fehler beim Registrieren der Institution.', + description: err.message, + } + this.$notification.error(notification) }) - }).catch(err => { - const notification = { - message: 'Fehler beim Registrieren der Institution.', - description: err.message, - } - this.$notification.error(notification) - }) }) }, }, @@ -229,19 +255,19 @@ export default Vue.extend({ diff --git a/client/src/views/RegisterPatient.vue b/client/src/views/RegisterPatient.vue index 2f57f946..d7ded767 100644 --- a/client/src/views/RegisterPatient.vue +++ b/client/src/views/RegisterPatient.vue @@ -1,66 +1,61 @@ - - - - - - - - - Patient/in wurde erfolgreich registriert. - - - - - - Name: - {{ createdPatient.firstName }} {{ createdPatient.lastName }} - - - Patienten-ID: - {{ createdPatient.id }} - - - - - - - Patienten/in einsehen - - - - - Probe zuordnen - - - - - - - - - - Registrieren Sie hier neue Patienten in IMIS. Bitte erfassen Sie die nachfolgenden Daten so vollständig wie - möglich. Pflichtangaben sind mit "*" markiert. - + + + + + Probe zuordnen + + + + + Patienten/in einsehen + + + + Neuen Patienten registrieren + + + + + {{ createdPatient.id }} + + + {{ createdPatient.firstName }} {{ createdPatient.lastName }} + + + - + + + + - - + @@ -70,21 +65,35 @@ :labelCol="{ div: 24 }" :wrapperCol="{ div: 24 }" > - + - + - {{exposure.label}} + {{ exposure.label }} - + - + - {{exposure.label}} + {{ exposure.label }} @@ -102,20 +111,36 @@ > - + - {{symptom.label}} + {{ symptom.label }} - - + + Andere: - - + + @@ -126,9 +151,7 @@ class="no-double-colon-form-field" label="Wie schnell sind die Beschwerden aufgetreten?" > - + Plötzlich, innerhalb von einem Tag @@ -161,24 +184,43 @@ > - + - {{preIllness.label}} + {{ preIllness.label }} - + - {{preIllness.label}} + {{ preIllness.label }} - - + + Andere: - - + + @@ -186,78 +228,149 @@ - - - + Erkrankung + + COVID-19 - + - - - - {{eventType.label}} + + + + {{ eventType.label }} - - - + + - + - + - + - - - - - - - Patient/in ist hospitalisiert - - - - - - Auf der Intensivstation - - - + + + + Hospitalisierung + + + + + Patient/in ist hospitalisiert + + + + + + + + + + Auf der Intensivstation + + - + @@ -286,19 +399,19 @@ import { EventTypeItem, eventTypes } from '@/models/event-types' import moment, { Moment } from 'moment' interface State { - form: any; - createdPatient: Patient | null; - SYMPTOMS: Option[]; - PRE_ILLNESSES: Option[]; - ADDITIONAL_PRE_ILLNESSES: Option[]; - EXPOSURES_INTERNAL: Option[]; - EXPOSURE_LOCATIONS: Option[]; - EVENT_TYPES: EventTypeItem[]; - showOtherSymptoms: boolean; - showOtherPreIllnesses: boolean; - disableExposureLocation: boolean; - disableHospitalization: boolean; - today: Moment; + form: any + createdPatient: Patient | null + SYMPTOMS: Option[] + PRE_ILLNESSES: Option[] + ADDITIONAL_PRE_ILLNESSES: Option[] + EXPOSURES_INTERNAL: Option[] + EXPOSURE_LOCATIONS: Option[] + EVENT_TYPES: EventTypeItem[] + showOtherSymptoms: boolean + showOtherPreIllnesses: boolean + disableExposureLocation: boolean + patientHospitalized: boolean + today: Moment } export default Vue.extend({ @@ -320,13 +433,13 @@ export default Vue.extend({ disableExposureLocation: true, showOtherSymptoms: false, showOtherPreIllnesses: false, - disableHospitalization: true, + patientHospitalized: false, today: moment(), } }, methods: { handleSubmit() { - this.form.validateFields(async(err: Error, values: any) => { + this.form.validateFields(async (err: Error, values: any) => { if (err) { return } @@ -359,8 +472,10 @@ export default Vue.extend({ request.dateOfDeath = request.dateOfDeath.format('YYYY-MM-DD') } - if (!this.disableHospitalization) { - request.dateOfHospitalization = values.dateOfHospitalization.format('YYYY-MM-DD') + if (this.patientHospitalized) { + request.dateOfHospitalization = values.dateOfHospitalization.format( + 'YYYY-MM-DD' + ) request.onIntensiveCareUnit = values.onIntensiveCareUnit } else { request.dateOfHospitalization = null @@ -378,37 +493,39 @@ export default Vue.extend({ } if (values.exposureLocation) { request.riskAreas = request.riskAreas.concat( - values.exposureLocation - .map((location: string) => 'CONTACT_WITH_CORONA_' + location), + values.exposureLocation.map( + (location: string) => 'CONTACT_WITH_CORONA_' + location + ) ) } - Api.addPatientUsingPost(request).then((patient: Patient) => { - this.form.resetFields() - this.createdPatient = patient as any - this.disableExposureLocation = true - this.disableHospitalization = true - this.showOtherSymptoms = false - this.form.setFieldsValue({ - symptomsOther: undefined, - symptomsOtherActivated: undefined, - preIllnessesOther: undefined, - preIllnessesActivated: undefined, + Api.addPatientUsingPost(request) + .then((patient: Patient) => { + this.form.resetFields() + this.createdPatient = patient as any + this.patientHospitalized = false + this.showOtherSymptoms = false + this.form.setFieldsValue({ + symptomsOther: undefined, + symptomsOtherActivated: undefined, + preIllnessesOther: undefined, + preIllnessesActivated: undefined, + }) + const notification = { + message: 'Patient/in registriert.', + description: 'Patient/in wurde erfolgreich registriert.', + } + this.$notification.success(notification) + window.scrollTo(0, 0) + }) + .catch((error: Error) => { + console.error(error) + const notification = { + message: 'Patient/in nicht registriert.', + description: 'Patient/in konnte nicht registriert werden.', + } + this.$notification.error(notification) }) - const notification = { - message: 'Patient/in registriert.', - description: 'Patient/in wurde erfolgreich registriert.', - } - this.$notification.success(notification) - window.scrollTo(0, 0) - }).catch((error: Error) => { - console.error(error) - const notification = { - message: 'Patient/in nicht registriert.', - description: 'Patient/in konnte nicht registriert werden.', - } - this.$notification.error(notification) - }) }) }, symptomsChanged(event: Event) { @@ -420,44 +537,36 @@ export default Vue.extend({ this.showOtherPreIllnesses = target.checked }, exposuresChanged(checkedValues: string[]) { - this.disableExposureLocation = !checkedValues.includes('CONTACT_WITH_CORONA_CASE') - }, - hospitalizationChanged(event: Event) { - const target = event.target as any - this.disableHospitalization = !target.checked + this.disableExposureLocation = !checkedValues.includes( + 'CONTACT_WITH_CORONA_CASE' + ) }, }, }) - - - diff --git a/client/src/views/RegisterTest.vue b/client/src/views/RegisterTest.vue index 9b128e5d..a38362ab 100644 --- a/client/src/views/RegisterTest.vue +++ b/client/src/views/RegisterTest.vue @@ -1,5 +1,5 @@ - + - - + - - {{testTypeItem.label}} + v-decorator="[ + 'testType', + { + rules: [ + { + required: true, + message: 'Bitte geben Sie den Typen des Tests an.', + }, + ], + }, + ]" + > + + {{ testTypeItem.label }} @@ -52,23 +87,42 @@ - - {{testMaterialItem.label}} + v-decorator="[ + 'testMaterial', + { + rules: [ + { + required: true, + message: 'Bitte geben Sie das Material des Tests an.', + }, + ], + }, + ]" + > + + {{ testMaterialItem.label }} @@ -105,10 +159,10 @@ import { TestMaterialItem, testMaterials } from '@/models/test-materials' import moment from 'moment' interface State { - form: any; - testTypes: TestTypeItem[]; - testMaterials: TestMaterialItem[]; - today: moment.Moment; + form: any + testTypes: TestTypeItem[] + testMaterials: TestMaterialItem[] + today: moment.Moment } export default Vue.extend({ @@ -136,27 +190,30 @@ export default Vue.extend({ ...values, } - Api.createTestForPatientUsingPost(request).then(labTest => { - const createdLabTest = labTest - const createdLabTestStatus = testResults - .find(testResult => testResult.id === labTest.testStatus) - ?.label || '' - this.form.resetFields() - const h = this.$createElement - this.$success({ - title: 'Der Test wurde erfolgreich angelegt.', - content: h('div', {}, [ - h('div', `Test ID: ${createdLabTest.testId}`), - h('div', `Test Status: ${createdLabTestStatus}`), - ]), + Api.createTestForPatientUsingPost(request) + .then((labTest) => { + const createdLabTest = labTest + const createdLabTestStatus = + testResults.find( + (testResult) => testResult.id === labTest.testStatus + )?.label || '' + this.form.resetFields() + const h = this.$createElement + this.$success({ + title: 'Der Test wurde erfolgreich angelegt.', + content: h('div', {}, [ + h('div', `Test ID: ${createdLabTest.testId}`), + h('div', `Test Status: ${createdLabTestStatus}`), + ]), + }) + }) + .catch((err) => { + const notification = { + message: 'Fehler beim Anlegen des Tests.', + description: err.message, + } + this.$notification.error(notification) }) - }).catch(err => { - const notification = { - message: 'Fehler beim Anlegen des Tests.', - description: err.message, - } - this.$notification.error(notification) - }) }) }, }, diff --git a/client/src/views/RequestQuarantine.vue b/client/src/views/RequestQuarantine.vue index 157d97f5..cef46384 100644 --- a/client/src/views/RequestQuarantine.vue +++ b/client/src/views/RequestQuarantine.vue @@ -1,46 +1,134 @@ - + - - - {{this.$route.params.patientFirstName}} {{this.$route.params.patientLastName}} - ({{this.$route.params.patientId}}) + > + + + {{ this.$route.params.patientFirstName }} + {{ this.$route.params.patientLastName }} ({{ + this.$route.params.patientId + }}) + + Quarantäne auch für alle Kontaktpersonen vormerken + + + + + Keine Kontaktpersonen hinterlegt + + + + + + {{ contact.contact.firstName }} {{ contact.contact.lastName }} + + Datum Kontakt: 21.04.2020 + + + + + Infiziert + + Infektionsstatus unbekannt + + + + In Quarantäne + + + Keine Quarantäne + + + + + + - - + diff --git a/client/src/views/SendToQuarantine.vue b/client/src/views/SendToQuarantine.vue index 961e1f58..44ee8ecc 100644 --- a/client/src/views/SendToQuarantine.vue +++ b/client/src/views/SendToQuarantine.vue @@ -1,44 +1,83 @@ - - Sollen die Quarantänen von {{quarantinesByZip.length}} Patienten in den Status 'Quarantäne angeordnet' - überführt werden? - - - - - + + + Sollen die Quarantänen von {{ quarantinesByZip.length }} Patienten in + den Status 'Quarantäne angeordnet' überführt werden? + + + + + + - - Alle Herunterladen - - - Quarantäne anordnen - - Es wurden {{quarantinesByZip.length}} Patienten für eine Quarantäne vorgemerkt. - - - + + Alle Herunterladen + + + Quarantäne anordnen + + + + Es wurden {{ quarantinesByZip.length }} Patienten für eine Quarantäne + vorgemerkt. + + + {{ moment(until).format('DD.MM.YYYY') }} - {{ patient.firstName }} {{ patient.lastName }} + {{ patient.firstName }} {{ patient.lastName }} {{ moment(timestamp).format('DD.MM.YYYY HH:mm') }} @@ -51,12 +90,17 @@ diff --git a/client/src/views/SubmitTestResult.vue b/client/src/views/SubmitTestResult.vue index 3b23db1c..8677f716 100644 --- a/client/src/views/SubmitTestResult.vue +++ b/client/src/views/SubmitTestResult.vue @@ -1,5 +1,5 @@ - + - @@ -34,24 +47,43 @@ -
{{result}}
{{ result }}
Allgemeine Angaben:
Wohnort:
Aufenthaltsort, falls von Wohnort abweichend:
Kommunikation und Sonstiges:
test_lab mit Passwort asdf
test_doctor mit Passwort asdf
test_testing_site mit Passwort asdf
test_department_of_health mit Passwort asdf
+ test_testing_site mit Passwort asdf +
+ test_department_of_health mit Passwort + asdf +
Sollen die Quarantänen von {{quarantinesByZip.length}} Patienten in den Status 'Quarantäne angeordnet' - überführt werden?
+ Sollen die Quarantänen von {{ quarantinesByZip.length }} Patienten in + den Status 'Quarantäne angeordnet' überführt werden? +