@openmrs/esm-form-engine-lib 4.2.1-pre.2435 → 4.2.1-pre.2439

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,6 +8,7 @@ import { init, teardown } from "./lifecycle.js";
8
8
  import { isEmpty, useFormJson } from "./index.js";
9
9
  import { formEngineAppName } from "./globals.js";
10
10
  import { reportError } from "./utils/error-utils.js";
11
+ import { getDateWithinVisitWindow } from "./utils/common-utils.js";
11
12
  import { useFormCollapse } from "./hooks/useFormCollapse.js";
12
13
  import { useFormWorkspaceSize } from "./hooks/useFormWorkspaceSize.js";
13
14
  import { usePageObserver } from "./components/sidebar/usePageObserver.js";
@@ -22,9 +23,15 @@ const FormEngine = ({ formJson, patientUUID, formUUID, encounterUUID, visit, for
22
23
  const { t } = useTranslation();
23
24
  const session = useSession();
24
25
  const ref = useRef(null);
26
+ const rawSessionDate = useRef(new Date());
27
+ // Recompute when the visit bounds arrive or change; the visit prop may not be
28
+ // fully loaded when the form mounts.
25
29
  const sessionDate = useMemo(()=>{
26
- return new Date();
27
- }, []);
30
+ return getDateWithinVisitWindow(rawSessionDate.current, visit);
31
+ }, [
32
+ visit?.startDatetime,
33
+ visit?.stopDatetime
34
+ ]);
28
35
  const workspaceSize = useFormWorkspaceSize(ref);
29
36
  const { patient, isLoadingPatient } = usePatientData(patientUUID);
30
37
  const [isLoadingDependencies, setIsLoadingDependencies] = useState(false);
@@ -32,7 +32,9 @@ export function prepareEncounter(context, encounterDate, encounterRole, encounte
32
32
  ];
33
33
  }
34
34
  // TODO: Question: Should we be editing the location, form and visit here?
35
- encounterForSubmission.encounterDatetime = encounterDate;
35
+ if (encounterDate) {
36
+ encounterForSubmission.encounterDatetime = encounterDate;
37
+ }
36
38
  encounterForSubmission.location = location;
37
39
  encounterForSubmission.form = {
38
40
  uuid: formJson.uuid
@@ -46,7 +48,6 @@ export function prepareEncounter(context, encounterDate, encounterRole, encounte
46
48
  } else {
47
49
  encounterForSubmission = {
48
50
  patient: patient.id,
49
- encounterDatetime: encounterDate,
50
51
  location: location,
51
52
  encounterType: formJson.encounterType,
52
53
  encounterProviders: [
@@ -63,6 +64,9 @@ export function prepareEncounter(context, encounterDate, encounterRole, encounte
63
64
  orders: ordersForSubmission,
64
65
  diagnoses: diagnosesForSubmission
65
66
  };
67
+ if (encounterDate) {
68
+ encounterForSubmission.encounterDatetime = encounterDate;
69
+ }
66
70
  }
67
71
  return encounterForSubmission;
68
72
  }
@@ -134,16 +138,22 @@ export function saveAttachments(fields, encounter, abortController) {
134
138
  return Promise.all(allPromises);
135
139
  }
136
140
  export function getMutableSessionProps(context) {
137
- const { formFields, location, currentProvider, customDependencies, domainObjectValue: encounter } = context;
141
+ const { formFields, location, currentProvider, customDependencies, sessionDate, domainObjectValue: encounter, visit } = context;
138
142
  const { defaultEncounterRole } = customDependencies;
139
143
  const encounterRole = formFields.find((field)=>field.type === 'encounterRole')?.meta.submission?.newValue || defaultEncounterRole?.uuid;
140
144
  const encounterProvider = formFields.find((field)=>field.type === 'encounterProvider')?.meta.submission?.newValue || currentProvider.uuid;
141
- const encounterDate = formFields.find((field)=>field.type === 'encounterDatetime')?.meta.submission?.newValue;
145
+ const explicitEncounterDate = formFields.find((field)=>field.type === 'encounterDatetime')?.meta.submission?.newValue;
146
+ // Stopped visits need an explicit datetime because the backend defaults omitted
147
+ // encounter datetimes to server "now", which can be outside the visit window.
148
+ // Active visits should use the backend default so client clock skew does not
149
+ // submit future encounter datetimes.
150
+ const defaultEncounterDate = visit?.stopDatetime ? sessionDate : undefined;
151
+ const encounterDate = explicitEncounterDate ?? (encounter?.encounterDatetime ? new Date(encounter.encounterDatetime) : defaultEncounterDate);
142
152
  const encounterLocation = formFields.find((field)=>field.type === 'encounterLocation')?.meta.submission?.newValue || encounter?.location?.uuid || location.uuid;
143
153
  return {
144
154
  encounterRole: encounterRole,
145
155
  encounterProvider: encounterProvider,
146
- encounterDate: encounterDate,
156
+ encounterDate,
147
157
  encounterLocation: encounterLocation
148
158
  };
149
159
  }
@@ -1 +1 @@
1
- {"version":3,"file":"form-engine.component.d.ts","sourceRoot":"","sources":["../../src/form-engine.component.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA4D,MAAM,OAAO,CAAC;AAIjF,OAAO,EAAc,KAAK,KAAK,EAAE,MAAM,wBAAwB,CAAC;AAUhE,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAQtF,UAAU,eAAe;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,UAAU,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,GAAG,KAAK,IAAI,CAAC;IAC/B,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IACzB,6BAA6B,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,CAAC,SAAS,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACjF,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;IAC7C,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;CACzC;AA+KD,iBAAS,aAAa,CAAC,KAAK,EAAE,eAAe,qBAM5C;AAED,eAAe,aAAa,CAAC"}
1
+ {"version":3,"file":"form-engine.component.d.ts","sourceRoot":"","sources":["../../src/form-engine.component.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA4D,MAAM,OAAO,CAAC;AAIjF,OAAO,EAAc,KAAK,KAAK,EAAE,MAAM,wBAAwB,CAAC;AAWhE,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAQtF,UAAU,eAAe;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,UAAU,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,GAAG,KAAK,IAAI,CAAC;IAC/B,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IACzB,6BAA6B,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,CAAC,SAAS,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACjF,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;IAC7C,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;CACzC;AAkLD,iBAAS,aAAa,CAAC,KAAK,EAAE,eAAe,qBAM5C;AAED,eAAe,aAAa,CAAC"}
@@ -1,6 +1,12 @@
1
1
  import { type FormField, type FormProcessorContextProps, type OpenmrsEncounter, type PatientIdentifier, type PatientProgram, type PatientProgramPayload, type PersonAttribute } from '../../types';
2
2
  import { type FormContextProps } from '../../provider/form-provider';
3
- export declare function prepareEncounter(context: FormContextProps, encounterDate: Date, encounterRole: string, encounterProvider: string, location: string): OpenmrsEncounter;
3
+ type MutableSessionProps = {
4
+ encounterRole: string;
5
+ encounterProvider: string;
6
+ encounterDate?: Date;
7
+ encounterLocation: string;
8
+ };
9
+ export declare function prepareEncounter(context: FormContextProps, encounterDate: Date | undefined, encounterRole: string, encounterProvider: string, location: string): OpenmrsEncounter;
4
10
  export declare function preparePatientIdentifiers(fields: FormField[], encounterLocation: string): PatientIdentifier[];
5
11
  export declare function savePatientIdentifiers(patient: fhir.Patient, identifiers: PatientIdentifier[]): Promise<import("@openmrs/esm-api").FetchResponse<any>>[];
6
12
  export declare function preparePersonAttributes(fields: FormField[]): PersonAttribute[];
@@ -8,12 +14,8 @@ export declare function savePersonAttributes(patient: fhir.Patient, attributes:
8
14
  export declare function preparePatientPrograms(fields: FormField[], patient: fhir.Patient, currentPatientPrograms: Array<PatientProgram>): Array<PatientProgramPayload>;
9
15
  export declare function savePatientPrograms(patientPrograms: PatientProgramPayload[]): Promise<import("@openmrs/esm-api").FetchResponse<any>[]>;
10
16
  export declare function saveAttachments(fields: FormField[], encounter: OpenmrsEncounter, abortController: AbortController): any[] | Promise<import("@openmrs/esm-api").FetchResponse<any>[]>;
11
- export declare function getMutableSessionProps(context: FormContextProps): {
12
- encounterRole: string;
13
- encounterProvider: string;
14
- encounterDate: Date;
15
- encounterLocation: string;
16
- };
17
+ export declare function getMutableSessionProps(context: FormContextProps): MutableSessionProps;
17
18
  export declare function inferInitialValueFromDefaultFieldValue(field: FormField): any;
18
19
  export declare function hydrateRepeatField(field: FormField, encounter: OpenmrsEncounter, initialValues: Record<string, any>, context: FormProcessorContextProps): Promise<FormField[]>;
20
+ export {};
19
21
  //# sourceMappingURL=encounter-processor-helper.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"encounter-processor-helper.d.ts","sourceRoot":"","sources":["../../../../src/processors/encounter/encounter-processor-helper.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,SAAS,EACd,KAAK,yBAAyB,EAC9B,KAAK,gBAAgB,EAErB,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACnB,KAAK,qBAAqB,EAC1B,KAAK,eAAe,EACrB,MAAM,aAAa,CAAC;AAKrB,OAAO,EAAE,KAAK,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAQrE,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,gBAAgB,EACzB,aAAa,EAAE,IAAI,EACnB,aAAa,EAAE,MAAM,EACrB,iBAAiB,EAAE,MAAM,EACzB,QAAQ,EAAE,MAAM,oBA6DjB;AAED,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,iBAAiB,EAAE,MAAM,GAAG,iBAAiB,EAAE,CAI7G;AAED,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAE,4DAI7F;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,SAAS,EAAE,GAAG,eAAe,EAAE,CAI9E;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,4DAIxF;AAED,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,SAAS,EAAE,EACnB,OAAO,EAAE,IAAI,CAAC,OAAO,EACrB,sBAAsB,EAAE,KAAK,CAAC,cAAc,CAAC,GAC5C,KAAK,CAAC,qBAAqB,CAAC,CA+B9B;AAED,wBAAgB,mBAAmB,CAAC,eAAe,EAAE,qBAAqB,EAAE,4DAG3E;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,SAAS,EAAE,gBAAgB,EAAE,eAAe,EAAE,eAAe,oEAcjH;AAED,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,gBAAgB;mBAa5B,MAAM;uBACE,MAAM;mBACd,IAAI;uBACI,MAAM;EAEjD;AA4FD,wBAAgB,sCAAsC,CAAC,KAAK,EAAE,SAAS,OAetE;AAED,wBAAsB,kBAAkB,CACtC,KAAK,EAAE,SAAS,EAChB,SAAS,EAAE,gBAAgB,EAC3B,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAClC,OAAO,EAAE,yBAAyB,GACjC,OAAO,CAAC,SAAS,EAAE,CAAC,CAwEtB"}
1
+ {"version":3,"file":"encounter-processor-helper.d.ts","sourceRoot":"","sources":["../../../../src/processors/encounter/encounter-processor-helper.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,SAAS,EACd,KAAK,yBAAyB,EAC9B,KAAK,gBAAgB,EAErB,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACnB,KAAK,qBAAqB,EAC1B,KAAK,eAAe,EACrB,MAAM,aAAa,CAAC;AAKrB,OAAO,EAAE,KAAK,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAQrE,KAAK,mBAAmB,GAAG;IACzB,aAAa,EAAE,MAAM,CAAC;IACtB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,aAAa,CAAC,EAAE,IAAI,CAAC;IACrB,iBAAiB,EAAE,MAAM,CAAC;CAC3B,CAAC;AAEF,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,gBAAgB,EACzB,aAAa,EAAE,IAAI,GAAG,SAAS,EAC/B,aAAa,EAAE,MAAM,EACrB,iBAAiB,EAAE,MAAM,EACzB,QAAQ,EAAE,MAAM,oBAiEjB;AAED,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,iBAAiB,EAAE,MAAM,GAAG,iBAAiB,EAAE,CAI7G;AAED,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAE,4DAI7F;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,SAAS,EAAE,GAAG,eAAe,EAAE,CAI9E;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,4DAIxF;AAED,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,SAAS,EAAE,EACnB,OAAO,EAAE,IAAI,CAAC,OAAO,EACrB,sBAAsB,EAAE,KAAK,CAAC,cAAc,CAAC,GAC5C,KAAK,CAAC,qBAAqB,CAAC,CA+B9B;AAED,wBAAgB,mBAAmB,CAAC,eAAe,EAAE,qBAAqB,EAAE,4DAG3E;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,SAAS,EAAE,gBAAgB,EAAE,eAAe,EAAE,eAAe,oEAcjH;AAED,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,gBAAgB,GAAG,mBAAmB,CAmCrF;AA4FD,wBAAgB,sCAAsC,CAAC,KAAK,EAAE,SAAS,OAetE;AAED,wBAAsB,kBAAkB,CACtC,KAAK,EAAE,SAAS,EAChB,SAAS,EAAE,gBAAgB,EAC3B,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAClC,OAAO,EAAE,yBAAyB,GACjC,OAAO,CAAC,SAAS,EAAE,CAAC,CAwEtB"}
@@ -1,4 +1,5 @@
1
1
  import { type FormSchema, type FormField, type OpenmrsObs, type RenderType } from '../types';
2
+ import { type Visit } from '@openmrs/esm-framework';
2
3
  export declare function flattenObsList(obsList: OpenmrsObs[]): OpenmrsObs[];
3
4
  export declare function hasRendering(field: FormField, rendering: RenderType): boolean;
4
5
  export declare function clearSubmission(field: FormField): void;
@@ -6,6 +7,13 @@ export declare function gracefullySetSubmission(field: FormField, newValue: any,
6
7
  export declare function hasSubmission(field: FormField): boolean;
7
8
  export declare function isViewMode(sessionMode: string): sessionMode is "view" | "embedded-view";
8
9
  export declare function parseToLocalDateTime(dateString: string): Date;
10
+ /**
11
+ * Returns `date` if it falls within the visit's start/stop window, otherwise the
12
+ * visit's start datetime. This keeps default encounter datetimes valid when filling
13
+ * forms against a past (stopped) visit via retrospective data entry; the backend
14
+ * rejects encounters dated outside the visit window.
15
+ */
16
+ export declare function getDateWithinVisitWindow(date: Date, visit?: Visit): Date;
9
17
  export declare function formatDateAsDisplayString(field: FormField, date: Date): string;
10
18
  /**
11
19
  * Creates a new copy of `formJson` with updated references at the page and section levels.
@@ -1 +1 @@
1
- {"version":3,"file":"common-utils.d.ts","sourceRoot":"","sources":["../../../src/utils/common-utils.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,UAAU,EAAE,KAAK,SAAS,EAAE,KAAK,UAAU,EAAE,KAAK,UAAU,EAAE,MAAM,UAAU,CAAC;AAI7F,wBAAgB,cAAc,CAAC,OAAO,EAAE,UAAU,EAAE,GAAG,UAAU,EAAE,CAgBlE;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,UAAU,WAEnE;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,SAAS,QAS/C;AAED,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,EAAE,WAAW,EAAE,GAAG,OAWxF;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,SAAS,WAE7C;AAED,wBAAgB,UAAU,CAAC,WAAW,EAAE,MAAM,2CAE7C;AAED,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAgB7D;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,UAQrE;AAED;;;GAGG;AACH,wBAAgB,2BAA2B,CAAC,QAAQ,EAAE,UAAU;;;;;;;;;;;;;;;eA9E1D,CAAH;cAGD,CAAF;;;;;;;;gBAQmB,CAAC;4BAA0B,CAAC;;;;EAyE9C;AAED;;;;;GAKG;AACH,wBAAgB,OAAO,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAW,UAExD"}
1
+ {"version":3,"file":"common-utils.d.ts","sourceRoot":"","sources":["../../../src/utils/common-utils.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,UAAU,EAAE,KAAK,SAAS,EAAE,KAAK,UAAU,EAAE,KAAK,UAAU,EAAE,MAAM,UAAU,CAAC;AAE7F,OAAO,EAAsC,KAAK,KAAK,EAAE,MAAM,wBAAwB,CAAC;AAExF,wBAAgB,cAAc,CAAC,OAAO,EAAE,UAAU,EAAE,GAAG,UAAU,EAAE,CAgBlE;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,UAAU,WAEnE;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,SAAS,QAS/C;AAED,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,EAAE,WAAW,EAAE,GAAG,OAWxF;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,SAAS,WAE7C;AAED,wBAAgB,UAAU,CAAC,WAAW,EAAE,MAAM,2CAE7C;AAED,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAgB7D;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,CAAC,EAAE,KAAK,GAAG,IAAI,CAUxE;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,UAQrE;AAED;;;GAGG;AACH,wBAAgB,2BAA2B,CAAC,QAAQ,EAAE,UAAU;;;;;;;;;;;;;;;eAjG5C,CAAC;cACd,CAAC;;;;;;;;gBAWD,CAAC;4BAA0B,CAAC;;;;EA2FlC;AAED;;;;;GAKG;AACH,wBAAgB,OAAO,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAW,UAExD"}
@@ -69,6 +69,22 @@ export function parseToLocalDateTime(dateString) {
69
69
  }
70
70
  return dateObj;
71
71
  }
72
+ /**
73
+ * Returns `date` if it falls within the visit's start/stop window, otherwise the
74
+ * visit's start datetime. This keeps default encounter datetimes valid when filling
75
+ * forms against a past (stopped) visit via retrospective data entry; the backend
76
+ * rejects encounters dated outside the visit window.
77
+ */ export function getDateWithinVisitWindow(date, visit) {
78
+ if (!visit) {
79
+ return date;
80
+ }
81
+ const visitStart = visit.startDatetime ? new Date(visit.startDatetime) : null;
82
+ const visitStop = visit.stopDatetime ? new Date(visit.stopDatetime) : null;
83
+ if (visitStart && date < visitStart || visitStop && date > visitStop) {
84
+ return visitStart ?? visitStop;
85
+ }
86
+ return date;
87
+ }
72
88
  export function formatDateAsDisplayString(field, date) {
73
89
  const options = {
74
90
  noToday: true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openmrs/esm-form-engine-lib",
3
- "version": "4.2.1-pre.2435",
3
+ "version": "4.2.1-pre.2439",
4
4
  "description": "React Form Engine for O3",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
@@ -8,6 +8,7 @@ import { init, teardown } from './lifecycle';
8
8
  import { isEmpty, useFormJson } from '.';
9
9
  import { formEngineAppName } from './globals';
10
10
  import { reportError } from './utils/error-utils';
11
+ import { getDateWithinVisitWindow } from './utils/common-utils';
11
12
  import { useFormCollapse } from './hooks/useFormCollapse';
12
13
  import { useFormWorkspaceSize } from './hooks/useFormWorkspaceSize';
13
14
  import { usePageObserver } from './components/sidebar/usePageObserver';
@@ -58,9 +59,12 @@ const FormEngine = ({
58
59
  const { t } = useTranslation();
59
60
  const session = useSession();
60
61
  const ref = useRef(null);
62
+ const rawSessionDate = useRef(new Date());
63
+ // Recompute when the visit bounds arrive or change; the visit prop may not be
64
+ // fully loaded when the form mounts.
61
65
  const sessionDate = useMemo(() => {
62
- return new Date();
63
- }, []);
66
+ return getDateWithinVisitWindow(rawSessionDate.current, visit);
67
+ }, [visit?.startDatetime, visit?.stopDatetime]);
64
68
  const workspaceSize = useFormWorkspaceSize(ref);
65
69
  const { patient, isLoadingPatient } = usePatientData(patientUUID);
66
70
  const [isLoadingDependencies, setIsLoadingDependencies] = useState(false);
@@ -21,9 +21,16 @@ import { assignedOrderIds } from '../../adapters/orders-adapter';
21
21
  import { type OpenmrsResource } from '@openmrs/esm-framework';
22
22
  import { assignedDiagnosesIds } from '../../adapters/encounter-diagnosis-adapter';
23
23
 
24
+ type MutableSessionProps = {
25
+ encounterRole: string;
26
+ encounterProvider: string;
27
+ encounterDate?: Date;
28
+ encounterLocation: string;
29
+ };
30
+
24
31
  export function prepareEncounter(
25
32
  context: FormContextProps,
26
- encounterDate: Date,
33
+ encounterDate: Date | undefined,
27
34
  encounterRole: string,
28
35
  encounterProvider: string,
29
36
  location: string,
@@ -54,7 +61,9 @@ export function prepareEncounter(
54
61
  ];
55
62
  }
56
63
  // TODO: Question: Should we be editing the location, form and visit here?
57
- encounterForSubmission.encounterDatetime = encounterDate;
64
+ if (encounterDate) {
65
+ encounterForSubmission.encounterDatetime = encounterDate;
66
+ }
58
67
  encounterForSubmission.location = location;
59
68
  encounterForSubmission.form = {
60
69
  uuid: formJson.uuid,
@@ -68,7 +77,6 @@ export function prepareEncounter(
68
77
  } else {
69
78
  encounterForSubmission = {
70
79
  patient: patient.id,
71
- encounterDatetime: encounterDate,
72
80
  location: location,
73
81
  encounterType: formJson.encounterType,
74
82
  encounterProviders: [
@@ -85,6 +93,9 @@ export function prepareEncounter(
85
93
  orders: ordersForSubmission,
86
94
  diagnoses: diagnosesForSubmission,
87
95
  };
96
+ if (encounterDate) {
97
+ encounterForSubmission.encounterDatetime = encounterDate;
98
+ }
88
99
  }
89
100
  return encounterForSubmission;
90
101
  }
@@ -171,14 +182,31 @@ export function saveAttachments(fields: FormField[], encounter: OpenmrsEncounter
171
182
  return Promise.all(allPromises);
172
183
  }
173
184
 
174
- export function getMutableSessionProps(context: FormContextProps) {
175
- const { formFields, location, currentProvider, customDependencies, domainObjectValue: encounter } = context;
185
+ export function getMutableSessionProps(context: FormContextProps): MutableSessionProps {
186
+ const {
187
+ formFields,
188
+ location,
189
+ currentProvider,
190
+ customDependencies,
191
+ sessionDate,
192
+ domainObjectValue: encounter,
193
+ visit,
194
+ } = context;
176
195
  const { defaultEncounterRole } = customDependencies;
177
196
  const encounterRole =
178
197
  formFields.find((field) => field.type === 'encounterRole')?.meta.submission?.newValue || defaultEncounterRole?.uuid;
179
198
  const encounterProvider =
180
199
  formFields.find((field) => field.type === 'encounterProvider')?.meta.submission?.newValue || currentProvider.uuid;
181
- const encounterDate = formFields.find((field) => field.type === 'encounterDatetime')?.meta.submission?.newValue;
200
+ const explicitEncounterDate = formFields.find((field) => field.type === 'encounterDatetime')?.meta.submission
201
+ ?.newValue as Date | undefined;
202
+ // Stopped visits need an explicit datetime because the backend defaults omitted
203
+ // encounter datetimes to server "now", which can be outside the visit window.
204
+ // Active visits should use the backend default so client clock skew does not
205
+ // submit future encounter datetimes.
206
+ const defaultEncounterDate = visit?.stopDatetime ? sessionDate : undefined;
207
+ const encounterDate =
208
+ explicitEncounterDate ??
209
+ (encounter?.encounterDatetime ? new Date(encounter.encounterDatetime) : defaultEncounterDate);
182
210
  const encounterLocation =
183
211
  formFields.find((field) => field.type === 'encounterLocation')?.meta.submission?.newValue ||
184
212
  encounter?.location?.uuid ||
@@ -186,7 +214,7 @@ export function getMutableSessionProps(context: FormContextProps) {
186
214
  return {
187
215
  encounterRole: encounterRole as string,
188
216
  encounterProvider: encounterProvider as string,
189
- encounterDate: encounterDate as Date,
217
+ encounterDate,
190
218
  encounterLocation: encounterLocation as string,
191
219
  };
192
220
  }
@@ -1,7 +1,7 @@
1
1
  import dayjs from 'dayjs';
2
2
  import { type FormSchema, type FormField, type OpenmrsObs, type RenderType } from '../types';
3
3
  import { isEmpty } from '../validators/form-validator';
4
- import { formatDate, type FormatDateOptions } from '@openmrs/esm-framework';
4
+ import { formatDate, type FormatDateOptions, type Visit } from '@openmrs/esm-framework';
5
5
 
6
6
  export function flattenObsList(obsList: OpenmrsObs[]): OpenmrsObs[] {
7
7
  const flattenedList: OpenmrsObs[] = [];
@@ -75,6 +75,24 @@ export function parseToLocalDateTime(dateString: string): Date {
75
75
  return dateObj;
76
76
  }
77
77
 
78
+ /**
79
+ * Returns `date` if it falls within the visit's start/stop window, otherwise the
80
+ * visit's start datetime. This keeps default encounter datetimes valid when filling
81
+ * forms against a past (stopped) visit via retrospective data entry; the backend
82
+ * rejects encounters dated outside the visit window.
83
+ */
84
+ export function getDateWithinVisitWindow(date: Date, visit?: Visit): Date {
85
+ if (!visit) {
86
+ return date;
87
+ }
88
+ const visitStart = visit.startDatetime ? new Date(visit.startDatetime) : null;
89
+ const visitStop = visit.stopDatetime ? new Date(visit.stopDatetime) : null;
90
+ if ((visitStart && date < visitStart) || (visitStop && date > visitStop)) {
91
+ return visitStart ?? visitStop;
92
+ }
93
+ return date;
94
+ }
95
+
78
96
  export function formatDateAsDisplayString(field: FormField, date: Date) {
79
97
  const options: Partial<FormatDateOptions> = { noToday: true };
80
98
  if (field.datePickerFormat === 'calendar') {