diff --git a/frontend/angular.json b/frontend/angular.json index 308e6c00..8c497794 100644 --- a/frontend/angular.json +++ b/frontend/angular.json @@ -35,6 +35,41 @@ "glob": "**/*", "input": "./node_modules/dwv/decoders/", "output": "/assets/dwv/decoders/" + }, + { + "glob": "**/*.js", + "input": "./node_modules/lforms/dist/lforms/webcomponent/", + "output": "/assets/js/lforms/" + }, + { + "glob": "**/*.js.map", + "input": "./node_modules/lforms/dist/lforms/webcomponent/", + "output": "/assets/js/lforms/" + }, + { + "glob": "**/*.js", + "input": "./node_modules/lforms/dist/lforms/fhir/", + "output": "/assets/js/lforms/fhir/" + }, + { + "glob": "**/*.js.map", + "input": "./node_modules/lforms/dist/lforms/fhir/", + "output": "/assets/js/lforms/fhir/" + }, + { + "glob": "**/*.js", + "input": "./node_modules/@webcomponents/webcomponentsjs", + "output": "/assets/js/webcomponents/" + }, + { + "glob": "**/*.css", + "input": "./node_modules/lforms/dist/lforms/webcomponent/", + "output": "/assets/css/lforms/" + }, + { + "glob": "**/*.png", + "input": "./node_modules/lforms/dist/lforms/webcomponent/", + "output": "/assets/css/lforms/" } ], "styles": [ diff --git a/frontend/package.json b/frontend/package.json index 59844632..7e65e596 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -36,6 +36,7 @@ "@panva/oauth4webapi": "1.2.0", "@swimlane/ngx-datatable": "^20.0.0", "@types/fhir": "^0.0.35", + "@webcomponents/webcomponentsjs": "^2.8.0", "asmcrypto.js": "^2.3.2", "bootstrap": "^4.4.1", "chart.js": "^4.0.1", @@ -45,6 +46,7 @@ "humanize-duration": "^3.27.3", "idb": "^7.1.0", "jose": "^4.10.4", + "lforms": "34.0.0", "moment": "^2.29.4", "ng2-charts": "^4.1.1", "ngx-dropzone": "^3.1.0", diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 1dec0827..df564a99 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -1,5 +1,5 @@ import { BrowserModule } from '@angular/platform-browser'; -import { NgModule } from '@angular/core'; +import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import {HttpClientModule, HTTP_INTERCEPTORS, HttpClient} from '@angular/common/http'; @@ -105,7 +105,8 @@ import {FhirDatatableModule} from './components/fhir-datatable/fhir-datatable.mo } ], exports: [], - bootstrap: [AppComponent] + bootstrap: [AppComponent], + schemas: [CUSTOM_ELEMENTS_SCHEMA] //required for lhncbc/lforms (webcomponent) }) export class AppModule { constructor(library: FaIconLibrary) { diff --git a/frontend/src/app/components/fhir-card/datatypes/dicom/dicom.component.html b/frontend/src/app/components/fhir-card/datatypes/dicom/dicom.component.html index c51cc142..44046dd5 100644 --- a/frontend/src/app/components/fhir-card/datatypes/dicom/dicom.component.html +++ b/frontend/src/app/components/fhir-card/datatypes/dicom/dicom.component.html @@ -68,6 +68,7 @@ class="mr-auto" [collectionSize]="metaData.length" [(page)]="tagsPage" + [maxSize]="15" [pageSize]="tagsPageSize" (pageChange)="refreshTags()" > diff --git a/frontend/src/app/components/fhir-card/resources/observation/observation.component.ts b/frontend/src/app/components/fhir-card/resources/observation/observation.component.ts index 6ee0c851..4521dab2 100644 --- a/frontend/src/app/components/fhir-card/resources/observation/observation.component.ts +++ b/frontend/src/app/components/fhir-card/resources/observation/observation.component.ts @@ -137,10 +137,13 @@ export class ObservationComponent implements OnInit { //populate chart data - - this.barChartLabels.push( - formatDate(this.displayModel?.effective_date, "mediumDate", "en-US", undefined) - ) + if(this.displayModel?.effective_date) { + this.barChartLabels.push( + formatDate(this.displayModel?.effective_date, "mediumDate", "en-US", undefined) + ) + } else { + this.barChartLabels.push("") + } this.barChartData[0].data = [[this.displayModel?.reference_range?.[0]?.low?.value, this.displayModel?.reference_range?.[0]?.high?.value]] this.barChartData[1].data = [[this.displayModel?.value_quantity_value as number, this.displayModel?.value_quantity_value as number]] diff --git a/frontend/src/app/components/medical-record-wizard-add-lab-results/medical-record-wizard-add-lab-results.component.html b/frontend/src/app/components/medical-record-wizard-add-lab-results/medical-record-wizard-add-lab-results.component.html new file mode 100644 index 00000000..e7926b59 --- /dev/null +++ b/frontend/src/app/components/medical-record-wizard-add-lab-results/medical-record-wizard-add-lab-results.component.html @@ -0,0 +1,38 @@ + + + diff --git a/frontend/src/app/components/medical-record-wizard-add-lab-results/medical-record-wizard-add-lab-results.component.scss b/frontend/src/app/components/medical-record-wizard-add-lab-results/medical-record-wizard-add-lab-results.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/frontend/src/app/components/medical-record-wizard-add-lab-results/medical-record-wizard-add-lab-results.component.spec.ts b/frontend/src/app/components/medical-record-wizard-add-lab-results/medical-record-wizard-add-lab-results.component.spec.ts new file mode 100644 index 00000000..f5390081 --- /dev/null +++ b/frontend/src/app/components/medical-record-wizard-add-lab-results/medical-record-wizard-add-lab-results.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { MedicalRecordWizardAddLabResultsComponent } from './medical-record-wizard-add-lab-results.component'; + +describe('MedicalRecordWizardAddLabResultsComponent', () => { + let component: MedicalRecordWizardAddLabResultsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ MedicalRecordWizardAddLabResultsComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(MedicalRecordWizardAddLabResultsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/components/medical-record-wizard-add-lab-results/medical-record-wizard-add-lab-results.component.ts b/frontend/src/app/components/medical-record-wizard-add-lab-results/medical-record-wizard-add-lab-results.component.ts new file mode 100644 index 00000000..dcaaa49e --- /dev/null +++ b/frontend/src/app/components/medical-record-wizard-add-lab-results/medical-record-wizard-add-lab-results.component.ts @@ -0,0 +1,99 @@ +import {Component, CUSTOM_ELEMENTS_SCHEMA, ElementRef, Input, OnInit, ViewChild} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators} from '@angular/forms'; +import {NlmTypeaheadComponent} from '../nlm-typeahead/nlm-typeahead.component'; +import {HighlightModule} from 'ngx-highlightjs'; +import {NgbActiveModal, NgbTooltipModule} from '@ng-bootstrap/ng-bootstrap'; +import {NlmClinicalTableSearchService} from '../../services/nlm-clinical-table-search.service'; +import {LabresultsQuestionnaire} from '../../models/fasten/labresults-questionnaire'; +import {fhirModelFactory} from '../../../lib/models/factory'; +import {LoadingSpinnerComponent} from '../loading-spinner/loading-spinner.component'; + +@Component({ + standalone: true, + imports: [ + CommonModule, + NlmTypeaheadComponent, + ReactiveFormsModule, + FormsModule, + LoadingSpinnerComponent + ], + selector: 'app-medical-record-wizard-add-lab-results', + templateUrl: './medical-record-wizard-add-lab-results.component.html', + styleUrls: ['./medical-record-wizard-add-lab-results.component.scss'], + schemas: [CUSTOM_ELEMENTS_SCHEMA] +}) +export class MedicalRecordWizardAddLabResultsComponent implements OnInit { + @Input() debugMode: boolean = false; + + @ViewChild('lhcForm', {read: ElementRef}) wcForm: ElementRef; + format: 'R4' | 'STU3' = 'R4'; + + newLabPanelTypeaheadForm: FormGroup + + options = { + displayScoreWithAnswerText: false + } + // questionnaire = {"lformsVersion":"29.0.0","PATH_DELIMITER":"/","code":"55399-0","codeList":[{"code":"55399-0","display":"Diabetes tracking panel","system":"http://loinc.org"}],"identifier":null,"codeSystem":"http://loinc.org","name":"Diabetes tracking panel","type":"LOINC","template":"table","copyrightNotice":null,"items":[{"questionCode":"41653-7","localQuestionCode":null,"dataType":"REAL","header":false,"units":[{"name":"mg/dL","code":"mg/dL","system":"http://unitsofmeasure.org","default":false}],"codingInstructions":null,"copyrightNotice":null,"question":"Glucose BldC Glucomtr-mCnc","answers":null,"skipLogic":null,"restrictions":null,"defaultAnswer":null,"formatting":null,"calculationMethod":null,"linkId":"/41653-7","questionCodeSystem":"http://loinc.org","codeList":[{"code":"41653-7","display":"Glucose BldC Glucomtr-mCnc","system":"http://loinc.org"}],"questionCardinality":{"min":"1","max":"1"},"answerCardinality":{"min":"0","max":"1"},"unit":{"name":"mg/dL","code":"mg/dL","system":"http://unitsofmeasure.org","default":false}},{"questionCode":"2345-7","localQuestionCode":null,"dataType":"REAL","header":false,"units":[{"name":"mg/dL","code":"mg/dL","system":"http://unitsofmeasure.org","default":false}],"codingInstructions":null,"copyrightNotice":null,"question":"Glucose - lab","answers":null,"skipLogic":null,"restrictions":null,"defaultAnswer":null,"formatting":null,"calculationMethod":null,"linkId":"/2345-7","questionCodeSystem":"http://loinc.org","codeList":[{"code":"2345-7","display":"Glucose - lab","system":"http://loinc.org"}],"questionCardinality":{"min":"1","max":"1"},"answerCardinality":{"min":"0","max":"1"},"unit":{"name":"mg/dL","code":"mg/dL","system":"http://unitsofmeasure.org","default":false}},{"questionCode":"55420-4","localQuestionCode":null,"dataType":"CNE","header":false,"units":[{"name":"h","code":"h","system":"http://unitsofmeasure.org","default":false}],"codingInstructions":null,"copyrightNotice":null,"question":"Hours p meal Time Patient","answers":[{"label":null,"code":"LA11828-3","text":"1 hour","other":null,"system":"http://loinc.org"},{"label":null,"code":"LA11829-1","text":"2 hours","other":null,"system":"http://loinc.org"},{"label":null,"code":"LA11830-9","text":"3 hours","other":null,"system":"http://loinc.org"},{"label":null,"code":"LA11831-7","text":"Fasting","other":null,"system":"http://loinc.org"}],"skipLogic":null,"restrictions":null,"defaultAnswer":null,"formatting":null,"calculationMethod":null,"linkId":"/55420-4","questionCodeSystem":"http://loinc.org","codeList":[{"code":"55420-4","display":"Hours p meal Time Patient","system":"http://loinc.org"}],"displayControl":{"answerLayout":{"type":"COMBO_BOX","columns":"0"}},"questionCardinality":{"min":"1","max":"1"},"answerCardinality":{"min":"0","max":"1"}},{"questionCode":"4548-4","localQuestionCode":null,"dataType":"REAL","header":false,"units":[{"name":"%","code":"%","system":"http://unitsofmeasure.org","default":false}],"codingInstructions":null,"copyrightNotice":null,"question":"HbA1c MFr Bld","answers":null,"skipLogic":null,"restrictions":null,"defaultAnswer":null,"formatting":null,"calculationMethod":null,"linkId":"/4548-4","questionCodeSystem":"http://loinc.org","codeList":[{"code":"4548-4","display":"HbA1c MFr Bld","system":"http://loinc.org"}],"questionCardinality":{"min":"1","max":"1"},"answerCardinality":{"min":"0","max":"1"},"unit":{"name":"%","code":"%","system":"http://unitsofmeasure.org","default":false}},{"questionCode":"27353-2","localQuestionCode":null,"dataType":"REAL","header":false,"units":[{"name":"mg/dL","code":"mg/dL","system":"http://unitsofmeasure.org","default":false}],"codingInstructions":null,"copyrightNotice":null,"question":"Est. average glucose Bld gHb Est-mCnc","answers":null,"skipLogic":null,"restrictions":null,"defaultAnswer":null,"formatting":null,"calculationMethod":null,"linkId":"/27353-2","questionCodeSystem":"http://loinc.org","codeList":[{"code":"27353-2","display":"Est. average glucose Bld gHb Est-mCnc","system":"http://loinc.org"}],"questionCardinality":{"min":"1","max":"1"},"answerCardinality":{"min":"0","max":"1"},"unit":{"name":"mg/dL","code":"mg/dL","system":"http://unitsofmeasure.org","default":false}},{"questionCode":"14957-5","localQuestionCode":null,"dataType":"REAL","header":false,"units":[{"name":"mg/L","code":null,"system":null,"default":false}],"codingInstructions":null,"copyrightNotice":null,"question":"Microalbumin Ur-mCnc","answers":null,"skipLogic":null,"restrictions":null,"defaultAnswer":null,"formatting":null,"calculationMethod":null,"linkId":"/14957-5","questionCodeSystem":"http://loinc.org","codeList":[{"code":"14957-5","display":"Microalbumin Ur-mCnc","system":"http://loinc.org"}],"questionCardinality":{"min":"1","max":"1"},"answerCardinality":{"min":"0","max":"1"},"unit":{"name":"mg/L","code":null,"system":null,"default":false}},{"questionCode":"2514-8","localQuestionCode":null,"dataType":"CNE","header":false,"units":null,"codingInstructions":"Beta-hydroxybutyrate+Acetoacetate+Acetone","copyrightNotice":null,"question":"Ketones Ur Strip","answers":[{"label":null,"code":"LA6577-6","text":"Negative","other":null,"system":"http://loinc.org"},{"label":null,"code":"LA11832-5","text":"Trace","other":null,"system":"http://loinc.org"},{"label":null,"code":"LA8983-4","text":"Small","other":null,"system":"http://loinc.org"},{"label":null,"code":"LA6751-7","text":"Moderate","other":null,"system":"http://loinc.org"},{"label":null,"code":"LA8981-8","text":"Large","other":null,"system":"http://loinc.org"}],"skipLogic":null,"restrictions":null,"defaultAnswer":null,"formatting":null,"calculationMethod":null,"linkId":"/2514-8","questionCodeSystem":"http://loinc.org","codeList":[{"code":"2514-8","display":"Ketones Ur Strip","system":"http://loinc.org"}],"displayControl":{"answerLayout":{"type":"COMBO_BOX","columns":"0"}},"questionCardinality":{"min":"1","max":"1"},"answerCardinality":{"min":"0","max":"1"}},{"questionCode":"5792-7","localQuestionCode":null,"dataType":"REAL","header":false,"units":[{"name":"mg/dL","code":"mg/dL","system":"http://unitsofmeasure.org","default":false}],"codingInstructions":null,"copyrightNotice":null,"question":"Glucose Ur Strip-mCnc","answers":null,"skipLogic":null,"restrictions":null,"defaultAnswer":null,"formatting":null,"calculationMethod":null,"linkId":"/5792-7","questionCodeSystem":"http://loinc.org","codeList":[{"code":"5792-7","display":"Glucose Ur Strip-mCnc","system":"http://loinc.org"}],"questionCardinality":{"min":"1","max":"1"},"answerCardinality":{"min":"0","max":"1"},"unit":{"name":"mg/dL","code":"mg/dL","system":"http://unitsofmeasure.org","default":false}},{"questionCode":"9057-1","localQuestionCode":null,"dataType":"REAL","header":false,"units":[{"name":"kcal/(24.h)","code":"kcal/(24.h)","system":"http://unitsofmeasure.org","default":false}],"codingInstructions":null,"copyrightNotice":null,"question":"Calorie intake total 24h","answers":null,"skipLogic":null,"restrictions":null,"defaultAnswer":null,"formatting":null,"calculationMethod":null,"linkId":"/9057-1","questionCodeSystem":"http://loinc.org","codeList":[{"code":"9057-1","display":"Calorie intake total 24h","system":"http://loinc.org"}],"questionCardinality":{"min":"1","max":"1"},"answerCardinality":{"min":"0","max":"1"},"unit":{"name":"kcal/(24.h)","code":"kcal/(24.h)","system":"http://unitsofmeasure.org","default":false}},{"questionCode":"55400-6","localQuestionCode":null,"dataType":"ST","header":false,"units":null,"codingInstructions":null,"copyrightNotice":null,"question":"Date of last eye exam","answers":null,"skipLogic":null,"restrictions":null,"defaultAnswer":null,"formatting":null,"calculationMethod":null,"linkId":"/55400-6","questionCodeSystem":"http://loinc.org","codeList":[{"code":"55400-6","display":"Date of last eye exam","system":"http://loinc.org"}],"questionCardinality":{"min":"1","max":"1"},"answerCardinality":{"min":"0","max":"1"}}],"templateOptions":{"showQuestionCode":false,"showCodingInstruction":false,"allowMultipleEmptyRepeatingItems":false,"allowHTMLInInstructions":false,"displayControl":{"questionLayout":"vertical"},"viewMode":"auto","defaultAnswerLayout":{"answerLayout":{"type":"COMBO_BOX","columns":"0"}},"hideTreeLine":false,"hideIndentation":false,"hideRepetitionNumber":false,"displayScoreWithAnswerText":true}} + questionnaire: LabresultsQuestionnaire = null + + loading: boolean = false + constructor(public activeModal: NgbActiveModal, public nlmClinicalTableSearchService: NlmClinicalTableSearchService) { } + + ngOnInit(): void { + + // const parent: HTMLElement = document.getElementById('lhcFormContainer'); + // console.log("Adding Form to page", this.questionnaire) + // LForms.Util.addFormToPage(this.questionnaire, parent); + this.resetLabPanelForm() + } + + submit() { + + if(LForms.Util.checkValidity(this.wcForm.nativeElement) != null){ + return + } + + let formData = LForms.Util.getFormFHIRData( + 'DiagnosticReport', + this.format, + null, + {bundleType: "transaction"} + ) + console.log(formData) + + this.activeModal.close({ + action: 'create', + data: formData + }); + } + onFormReady(e){ + console.log("onFormReady", e) + } + onError(e){ + console.log("onError", e) + } + + private resetLabPanelForm() { + this.newLabPanelTypeaheadForm = new FormGroup({ + data: new FormControl(null, Validators.required), + }) + this.newLabPanelTypeaheadForm.valueChanges.subscribe(form => { + console.log("CHANGE LabPanel IN MODAL", form) + let val = form.data + if(val && val.id){ + this.loading = true + this.nlmClinicalTableSearchService.searchLabPanelQuestionnaire(val.id) + .subscribe((res: LabresultsQuestionnaire) => { + this.loading = false + this.questionnaire = res + }, err => { + this.loading = false + console.error(err) + }) + } else { + this.questionnaire = null + } + }) + } +} diff --git a/frontend/src/app/components/medical-record-wizard-add-lab-results/medical-record-wizard-add-lab-results.stories.ts b/frontend/src/app/components/medical-record-wizard-add-lab-results/medical-record-wizard-add-lab-results.stories.ts new file mode 100644 index 00000000..126f24d2 --- /dev/null +++ b/frontend/src/app/components/medical-record-wizard-add-lab-results/medical-record-wizard-add-lab-results.stories.ts @@ -0,0 +1,46 @@ +import type { Meta, StoryObj } from '@storybook/angular'; +import {MedicalRecordWizardAddLabResultsComponent} from './medical-record-wizard-add-lab-results.component'; +import {applicationConfig} from '@storybook/angular'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {HTTP_CLIENT_TOKEN} from '../../dependency-injection'; +import {HttpClient} from '@angular/common/http'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {importProvidersFrom} from '@angular/core'; + +// More on how to set up stories at: https://storybook.js.org/docs/angular/writing-stories/introduction +const meta: Meta = { + title: 'Components/MedicalRecordWizardAddLabResults', + component: MedicalRecordWizardAddLabResultsComponent, + decorators: [ + applicationConfig({ + providers: [ + importProvidersFrom(HttpClientTestingModule), + NgbActiveModal, + { + provide: HttpClient, + useClass: HttpClient + }, + { + provide: HTTP_CLIENT_TOKEN, + useClass: HttpClient, + } + ] + }), + ], + tags: ['autodocs'], + render: (args: MedicalRecordWizardAddLabResultsComponent) => ({ + props: { + backgroundColor: null, + ...args, + }, + }), + argTypes: { + }, +}; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/angular/writing-stories/args +export const Primary: Story = {}; + diff --git a/frontend/src/app/components/medical-record-wizard/medical-record-wizard.component.html b/frontend/src/app/components/medical-record-wizard/medical-record-wizard.component.html index 2b3c37e0..a7b7c070 100644 --- a/frontend/src/app/components/medical-record-wizard/medical-record-wizard.component.html +++ b/frontend/src/app/components/medical-record-wizard/medical-record-wizard.component.html @@ -328,13 +328,29 @@

+ -