diff --git a/projects/ngx-formentry/src/form-entry/form-entry.module.ts b/projects/ngx-formentry/src/form-entry/form-entry.module.ts index 258cbc8d..d8be0f3f 100755 --- a/projects/ngx-formentry/src/form-entry/form-entry.module.ts +++ b/projects/ngx-formentry/src/form-entry/form-entry.module.ts @@ -47,6 +47,7 @@ import { CustomControlWrapperModule } from '../components/custom-control-wrapper import { CustomComponentWrapperModule } from '../components/custom-component-wrapper/custom-component-wrapper..module'; import { TranslateModule } from '@ngx-translate/core'; import { PatientIdentifierAdapter } from './value-adapters/patient-identifier.adapter'; +import { AppointmentAdapter } from './value-adapters/appointment.adapter'; @NgModule({ schemas: [CUSTOM_ELEMENTS_SCHEMA], @@ -102,7 +103,8 @@ import { PatientIdentifierAdapter } from './value-adapters/patient-identifier.ad OrderValueAdapter, DiagnosisValueAdapter, DebugModeService, - PatientIdentifierAdapter + PatientIdentifierAdapter, + AppointmentAdapter ], exports: [ FormRendererComponent, diff --git a/projects/ngx-formentry/src/form-entry/form-factory/form.ts b/projects/ngx-formentry/src/form-entry/form-factory/form.ts index 22db87ec..0e318ac2 100644 --- a/projects/ngx-formentry/src/form-entry/form-factory/form.ts +++ b/projects/ngx-formentry/src/form-entry/form-factory/form.ts @@ -146,6 +146,14 @@ export class Form { } } + get value() { + return this.rootNode.control.value; + } + + get isDirty() { + return this.rootNode.control.dirty; + } + get valid() { return this.rootNode.control.valid; } diff --git a/projects/ngx-formentry/src/form-entry/form-factory/question.factory.ts b/projects/ngx-formentry/src/form-entry/form-factory/question.factory.ts index 9b5ec941..0a592259 100644 --- a/projects/ngx-formentry/src/form-entry/form-factory/question.factory.ts +++ b/projects/ngx-formentry/src/form-entry/form-factory/question.factory.ts @@ -162,10 +162,10 @@ export class QuestionFactory { const mappings: any = { label: 'label', required: 'required', - id: 'key' + id: 'key', + type: '' }; question.datePickerFormat = schemaQuestion.datePickerFormat ?? 'calendar'; - this.copyProperties(mappings, schemaQuestion, question); this.addDisableOrHideProperty(schemaQuestion, question); this.addAlertProperty(schemaQuestion, question); @@ -225,7 +225,8 @@ export class QuestionFactory { const mappings = { label: 'label', required: 'required', - id: 'key' + id: 'key', + type: '' }; question.componentConfigs = schemaQuestion.componentConfigs || []; this.copyProperties(mappings, schemaQuestion, question); @@ -681,6 +682,42 @@ export class QuestionFactory { return question; } + toRemoteSelect(schemaQuestion: any): UiSelectQuestion { + const question = new UiSelectQuestion({ + options: [], + type: '', + key: '', + searchFunction: function () {}, + resolveFunction: function () {} + }); + question.questionIndex = this.quetionIndex; + question.label = schemaQuestion.label; + question.prefix = schemaQuestion.prefix; + question.key = schemaQuestion.id; + question.renderingType = schemaQuestion.type; + question.renderingType = 'remote-select'; + question.validators = this.addValidators(schemaQuestion); + question.extras = schemaQuestion; + question.dataSource = schemaQuestion.questionOptions.dataSource; + + if (question.dataSource === undefined) { + console.error(`No data source provided for question ${question.label}`); + } + + const mappings: any = { + label: 'label', + required: 'required', + id: 'key' + }; + question.componentConfigs = schemaQuestion.componentConfigs || []; + this.copyProperties(mappings, schemaQuestion, question); + this.addDisableOrHideProperty(schemaQuestion, question); + this.addAlertProperty(schemaQuestion, question); + this.addHistoricalExpressions(schemaQuestion, question); + this.addCalculatorProperty(schemaQuestion, question); + return question; + } + toTestOrderQuestion(schemaQuestion: any): TestOrderQuestion { const question = new TestOrderQuestion({ type: '', @@ -938,6 +975,8 @@ export class QuestionFactory { return this.toFileUploadQuestion(schema); case 'workspace-launcher': return this.toWorkspaceLauncher(schema); + case 'remote-select': + return this.toRemoteSelect(schema); default: console.warn('New Schema Question Type found.........' + renderType); return this.toTextQuestion(schema); diff --git a/projects/ngx-formentry/src/form-entry/value-adapters/appointment-helper.ts b/projects/ngx-formentry/src/form-entry/value-adapters/appointment-helper.ts new file mode 100644 index 00000000..8d41f5cb --- /dev/null +++ b/projects/ngx-formentry/src/form-entry/value-adapters/appointment-helper.ts @@ -0,0 +1,71 @@ +export interface AppointmentResponsePayload { + uuid: string; + appointmentNumber: string; + dateCreated: number; + dateAppointmentScheduled: number; + patient: { + OpenMRSID: string; + identifier: string; + UniquePatientNumber: string; + gender: string; + name: string; + uuid: string; + age: number; + customAttributes: Record; + }; + service: { + appointmentServiceId: number; + name: string; + description: string | null; + speciality: Record; + startTime: string; + endTime: string; + maxAppointmentsLimit: number | null; + durationMins: number | null; + location: Record; + uuid: string; + color: string; + initialAppointmentStatus: string | null; + creatorName: string | null; + }; + serviceType: unknown | null; + provider: unknown | null; + location: { + name: string; + uuid: string; + }; + startDateTime: number; + endDateTime: number; + appointmentKind: string; + status: string; + comments: string; + additionalInfo: unknown | null; + teleconsultation: unknown | null; + providers: Array<{ + uuid: string; + comments: string | null; + response: string; + name: string; + }>; + voided: boolean; + extensions: { + patientEmailDefined: boolean; + }; + teleconsultationLink: string | null; + priority: unknown | null; + recurring: boolean; +} +/** + * Interface representing the structure of an appointment payload. + */ +export interface AppointmentPayload { + status: string; + appointmentKind: string; + locationUuid: string; + serviceUuid: string; + providers: { uuid: string }[]; + startDateTime: string; + endDateTime: string; + dateAppointmentIssued?: string; + uuid?: string; +} diff --git a/projects/ngx-formentry/src/form-entry/value-adapters/appointment.adapter.ts b/projects/ngx-formentry/src/form-entry/value-adapters/appointment.adapter.ts new file mode 100644 index 00000000..23cb6e29 --- /dev/null +++ b/projects/ngx-formentry/src/form-entry/value-adapters/appointment.adapter.ts @@ -0,0 +1,157 @@ +import { Injectable } from '@angular/core'; +import { ValueAdapter } from './value.adapter'; +import { Form } from '../form-factory'; +import { GroupNode, LeafNode } from '../form-factory/form-node'; +import moment from 'moment'; +import { + AppointmentPayload, + AppointmentResponsePayload +} from './appointment-helper'; + +@Injectable({ + providedIn: 'root' +}) +export class AppointmentAdapter implements ValueAdapter { + /** + * Generates the form payload for an appointment. + * @param {Form} form - The form object. + * @returns {AppointmentPayload} The generated appointment payload. + */ + public generateFormPayload(form: Form): AppointmentPayload { + const uuid = form?.valueProcessingInfo?.appointmentUuid; + const dateAppointmentIssued = + form?.valueProcessingInfo?.dateAppointmentIssued; + + const questionNodes = this.findAppointmentQuestionNodes(form.rootNode); + const payload = this.generateFormPayloadForQuestion(questionNodes); + + // If in edit mode, add the uuid to the payload + if (uuid) { + payload.uuid = uuid; + } + + // Add dateAppointmentIssued to the payload if it exists + if (dateAppointmentIssued) { + payload.dateAppointmentIssued = dateAppointmentIssued; + } + + return payload; + } + + public populateForm(form: Form, payload: AppointmentResponsePayload): void { + const questionNodes = this.findAppointmentQuestionNodes(form.rootNode); + this.populateFormForQuestion(questionNodes, payload); + } + + private populateFormForQuestion( + appointmentQuestionNodes: LeafNode[], + payload: AppointmentResponsePayload + ): void { + appointmentQuestionNodes.forEach((node) => { + const appointmentKey = + node.question.extras.questionOptions.appointmentKey; + const value = payload[appointmentKey]; + + if (appointmentKey) { + if (appointmentKey === 'duration') { + node.control.setValue(this.calculateDuration(payload)); + return; + } + + if (value instanceof Object) { + if (Array.isArray(value)) { + node.control.setValue(value[0].uuid); + return; + } + node.control.setValue(value.uuid); + return; + } + + node.control.setValue(value); + } + }); + } + + private calculateDuration(payload: AppointmentResponsePayload): string { + const duration = moment + .duration(moment(payload.endDateTime).diff(payload.startDateTime)) + .asMinutes(); + return duration.toString(); + } + + private findAppointmentQuestionNodes(formNode: GroupNode): LeafNode[] { + const appointmentNodes: LeafNode[] = []; + + const traverseNode = (node: GroupNode | LeafNode): void => { + if (node instanceof GroupNode) { + Object.values(node.children).forEach(traverseNode); + } else if ( + node instanceof LeafNode && + node.question.extras?.type === 'appointment' + ) { + appointmentNodes.push(node as LeafNode); + } + }; + + traverseNode(formNode); + + return appointmentNodes; + } + + /** + * Generates the form payload for appointment questions. + * @param {LeafNode[]} appointmentQuestionNodes - An array of leaf nodes representing appointment questions. + * @returns {AppointmentPayload} The generated appointment payload. + */ + private generateFormPayloadForQuestion( + appointmentQuestionNodes: LeafNode[] + ): AppointmentPayload { + const formPayload = appointmentQuestionNodes.reduce>( + (payload, node) => { + const { appointmentKey } = node.question.extras.questionOptions; + payload[appointmentKey] = node.control.value; + return payload; + }, + {} + ); + + const { + providers, + startDateTime, + duration, + service, + location, + status = 'Scheduled', + appointmentKind = 'Scheduled', + ...restOfPayload + } = formPayload; + + const endDateTime = duration + ? this.calculateEndDateTime(startDateTime, parseInt(duration, 10)) + : moment(startDateTime).endOf('day').toISOString(); + + return { + ...restOfPayload, + status, + appointmentKind, + locationUuid: location, + serviceUuid: service, + providers: [{ uuid: providers }], + startDateTime: moment(startDateTime).toISOString(), + endDateTime + }; + } + + /** + * Calculates the end date and time based on the start date and time and duration. + * @param {string} startDateTime - The start date and time in ISO format. + * @param {number} durationMinutes - The duration in minutes. + * @returns {string} The calculated end date and time in ISO format. + */ + private calculateEndDateTime( + startDateTime: string, + durationMinutes: number + ): string { + return moment(startDateTime).add(durationMinutes, 'minutes').toISOString(); + } +} diff --git a/projects/ngx-formentry/src/lib/index.ts b/projects/ngx-formentry/src/lib/index.ts index 395ff210..2934f6ec 100644 --- a/projects/ngx-formentry/src/lib/index.ts +++ b/projects/ngx-formentry/src/lib/index.ts @@ -60,3 +60,4 @@ export { DatePickerComponent } from '../components/date-time-picker'; export { TimePickerComponent } from '../components/date-time-picker'; export { MomentPipe } from '../components/date-time-picker'; export { PatientIdentifierAdapter } from '../form-entry/value-adapters/patient-identifier.adapter'; +export { AppointmentAdapter } from '../form-entry/value-adapters/appointment.adapter'; diff --git a/src/app/adult-1.6.json b/src/app/adult-1.6.json index f52d697a..d45d02d4 100644 --- a/src/app/adult-1.6.json +++ b/src/app/adult-1.6.json @@ -8176,6 +8176,131 @@ ] } ] + }, + { + "label": "Appointments", + "sections": [ + { + "label": "Appointments", + "isExpanded": "true", + "questions": [ + { + "type": "appointment", + "questionInfo": "Location of the facility where the appointment was scheduled", + "label": "Location", + "id": "appointmentLocation", + "required": "true", + "questionOptions": { + "rendering": "remote-select", + "appointmentKey": "location", + "dataSource": "location" + } + }, + { + "label": "Date appointment issued", + "id": "dateAppointmentIssued", + "questionOptions": { + "rendering": "date", + "appointmentKey": "dateAppointmentScheduled" + }, + "type": "appointment", + "validators": [] + }, + { + "label": "Service", + "id": "service", + "questionOptions": { + "rendering": "remote-select", + "appointmentKey": "service", + "dataSource": "services" + }, + "type": "appointment", + "validators": [] + }, + { + "label": "Appointment type", + "id": "appointmentType", + "questionOptions": { + "placeholder": "Select appointment status", + "rendering": "select", + "appointmentKey": "appointmentKind", + "answers": [ + { + "label": "Scheduled", + "concept": "Scheduled" + } + ] + }, + "type": "appointment", + "validators": [] + }, + { + "label": "Appointment date & time", + "id": "appointmentDatetime", + "datePickerFormat": "both", + "questionOptions": { + "placeholder": "Enter appointment date & time", + "rendering": "date", + "appointmentKey": "startDateTime", + "answers": [] + }, + "type": "appointment", + "validators": [] + }, + { + "label": "Duration", + "id": "appointmentDuration", + "questionOptions": { + "placeholder": "Enter appointment duration", + "rendering": "number", + "appointmentKey": "duration" + }, + "type": "appointment", + "validators": [] + }, + { + "label": "Appointment status", + "id": "appointmentStatus", + "questionOptions": { + "placeholder": "Select appointment status", + "rendering": "select", + "appointmentKey": "status", + "answers": [ + { + "label": "Scheduled", + "concept": "Scheduled" + } + ] + }, + "type": "appointment", + "validators": [] + }, + { + "label": "Provider", + "id": "provider", + "questionOptions": { + "rendering": "remote-select", + "appointmentKey": "providers", + "dataSource": "provider" + }, + "type": "appointment", + "validators": [] + }, + { + "label": "Notes", + "id": "appointmentNote", + "questionOptions": { + "placeholder": "Enter appointment notes", + "rendering": "textarea", + "appointmentKey": "comments", + "rows": 5 + }, + "type": "appointment", + "validators": [] + } + ] + } + ] } ] } diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 215e7ac9..af934577 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -19,6 +19,7 @@ import { import { MockObs } from './mock/mock-obs'; import { mockTranslationsData } from './mock/mock-translations'; import { PatientIdentifierAdapter } from 'projects/ngx-formentry/src/form-entry/value-adapters/patient-identifier.adapter'; +import { AppointmentAdapter } from 'projects/ngx-formentry/src/form-entry/value-adapters/appointment.adapter'; const adultReturnVisitForm = require('./adult-1.6.json'); const adultReturnVisitFormObs = require('./mock/obs.json'); @@ -50,7 +51,8 @@ export class AppComponent implements OnInit { private http: HttpClient, private translate: TranslateService, private personAttributeAdapter: PersonAttribuAdapter, - private patientIdenfierAdapter: PatientIdentifierAdapter + private patientIdenfierAdapter: PatientIdentifierAdapter, + private appointmentsAdapter: AppointmentAdapter ) { this.schema = adultReturnVisitForm; } @@ -80,6 +82,10 @@ export class AppComponent implements OnInit { searchOptions: this.sampleSearch, resolveSelectedValue: this.sampleResolve }); + this.dataSources.registerDataSource('services', { + searchOptions: this.sampleSearch, + resolveSelectedValue: this.sampleResolve + }); const ds = { dataSourceOptions: { concept: undefined }, @@ -169,6 +175,7 @@ export class AppComponent implements OnInit { this.form.showErrors = false; this.form.rootNode.control.markAsDirty(); } + // this.appointmentsAdapter.populateForm(this.form, appointmentPayload as any); // Alternative is to set individually for obs and orders as show below // // Set obs @@ -377,8 +384,10 @@ export class AppComponent implements OnInit { encounterUuid: 'encounterUuid', providerUuid: 'providerUuid', utcOffset: '+0300', - locationUuid: 'some-location-uuid' + locationUuid: 'some-location-uuid', + dateAppointmentIssued: new Date().toISOString() }; + if (this.form.valid) { this.form.showErrors = false; // const payload = this.encAdapter.generateFormPayload(this.form); @@ -392,6 +401,11 @@ export class AppComponent implements OnInit { // generate patient identifiers //const patientIdenfitiers = this.patientIdenfierAdapter.generateFormPayload(this.form,this.form.valueProcessingInfo['locationUuid']); + // generate appointment payload + // const appointmentPayload = this.appointmentsAdapter.generateFormPayload( + // this.form + // ); + // console.log('Appointment Payload', appointmentPayload); } else { this.form.showErrors = true; this.form.markInvalidControls(this.form.rootNode);