Repository navigation
HTML-882: allow creating appointment in edit mode in ScheduleAppoinme… #345
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -57,7 +57,14 @@ public class ScheduleAppointmentElement implements HtmlGeneratorElement, FormSub | |
|
|
||
| private static final String DEFAULT_LOCATION_TAG = "Appointment Location"; | ||
|
|
||
| private boolean optional; | ||
| // True when the yes/no "schedule an appointment?" question should be rendered: whenever the form | ||
| // author asked for it via optional="true", and always in EDIT mode. | ||
| private boolean showScheduleChoice; | ||
|
|
||
| // Which side of that question starts selected (and whether the fields wrapper starts visible). | ||
| // EDIT mode defaults to "no" so that editing an encounter for any other reason does not force | ||
| // the user to book an appointment; ENTER mode keeps defaulting to "yes". | ||
| private boolean defaultToScheduling; | ||
|
|
||
| private String id; | ||
|
|
||
|
|
@@ -99,6 +106,10 @@ public class ScheduleAppointmentElement implements HtmlGeneratorElement, FormSub | |
| // Appointment fetched in VIEW/EDIT mode constructor | ||
| private Appointment existingAppointment; | ||
|
|
||
| // True when this element should render the scheduling form and handle submission: | ||
| // always in ENTER mode, and in EDIT mode when no appointment has been scheduled yet. | ||
| private boolean allowScheduling; | ||
|
|
||
| public ScheduleAppointmentElement(FormEntryContext context, Map<String, String> parameters) throws BadFormDesignException { | ||
| appointmentUuidConceptMapping = parameters.get("appointmentUuidConcept"); | ||
| tagControlId = parameters.get("controlId"); | ||
|
|
@@ -112,9 +123,16 @@ public ScheduleAppointmentElement(FormEntryContext context, Map<String, String> | |
| + appointmentUuidConceptMapping); | ||
| } | ||
|
|
||
| if (context.getMode() != Mode.ENTER) { | ||
| // VIEW/EDIT: claim the UUID obs so HFE doesn't warn about an unmatched obs, | ||
| // then fetch the appointment for display in generateHtml(). | ||
| if (context.getMode() == Mode.ENTER) { | ||
| allowScheduling = true; | ||
| } | ||
| else { | ||
| // VIEW/EDIT: look up the UUID obs written by a previous submission, so the appointment can | ||
| // be displayed in generateHtml(). Which lookup applies depends on whether this tag stamps a | ||
| // controlId into formFieldPath, mirroring ObsSubmissionElement.setExistingObs(): the | ||
| // controlId branch matches on the stamped path, removeExistingObs() only matches obs whose | ||
| // path is null. As elsewhere in HFE, that means adding or removing a controlId on a tag that | ||
| // already has data will stop the lookup from matching. | ||
| Obs uuidObs = StringUtils.isNotBlank(tagControlId) | ||
| ? context.getObsFromExistingObs(uuidConcept, tagControlId) | ||
| : context.removeExistingObs(uuidConcept, (Concept) null); | ||
|
|
@@ -127,10 +145,17 @@ public ScheduleAppointmentElement(FormEntryContext context, Map<String, String> | |
| // appointments module unavailable | ||
| } | ||
| } | ||
| // EDIT mode with no previously-scheduled appointment: let the user schedule one now. | ||
| if (context.getMode() == Mode.EDIT && uuidObs == null) { | ||
| allowScheduling = true; | ||
| } | ||
|
Comment on lines
+148
to
+151
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This makes an appointment mandatory in EDIT mode rather than merely possible. I ran the same api-tests probe against both branches. On master, editing an encounter that carries no appointment-uuid obs submits cleanly. On this branch the identical edit comes back with seven errors (Location, Service, Appointment type, Appointment date, Appointment time, Duration, Provider) and never reaches Merged as is, that means adding My suggestion is to make scheduling opt-in whenever the mode is EDIT: turn the yes/no choice on regardless of the
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. thoughts @chibongho , this does need to be fixed if it does work as Daniel says
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, this should be fixed. I'll make it so in EDIT mode, if no appointment has been scheduled yet, it defaults to "Don't schedule appointment" instead, regardless of the |
||
| } | ||
| else { | ||
| // ENTER mode: set up all widgets | ||
| optional = "true".equalsIgnoreCase(parameters.get("optional")); | ||
|
|
||
| if (allowScheduling) { | ||
| // set up all widgets | ||
| boolean optional = "true".equalsIgnoreCase(parameters.get("optional")); | ||
| showScheduleChoice = optional || context.getMode() == Mode.EDIT; | ||
| defaultToScheduling = context.getMode() != Mode.EDIT; | ||
| id = parameters.get("id"); | ||
| scheduleChoiceWidget = new TextFieldWidget(); | ||
| context.registerWidget(scheduleChoiceWidget); | ||
|
|
@@ -269,7 +294,7 @@ public ScheduleAppointmentElement(FormEntryContext context, Map<String, String> | |
|
|
||
| @Override | ||
| public String generateHtml(FormEntryContext context) { | ||
| if (context.getMode() != Mode.ENTER) { | ||
| if (!allowScheduling) { | ||
| return generateViewEditHtml(); | ||
| } | ||
|
|
||
|
|
@@ -286,19 +311,20 @@ public String generateHtml(FormEntryContext context) { | |
|
|
||
| sb.append("<div class=\"schedule-appointment\"").append(id != null ? " id=\"" + id + "\"" : "").append(">"); | ||
|
|
||
| if (optional) { | ||
| if (showScheduleChoice) { | ||
| String choiceFieldName = context.getFieldName(scheduleChoiceWidget); | ||
| sb.append("<p class=\"schedule-appointment-choice\">"); | ||
| sb.append("<label><input type=\"radio\" name=\"").append(choiceFieldName) | ||
| .append("\" value=\"yes\" checked") | ||
| .append("\" value=\"yes\"").append(defaultToScheduling ? " checked=\"true\"" : "") | ||
| .append(" onchange=\"this.closest('.schedule-appointment').querySelector('.schedule-appointment-fields').style.display='block'\"> ") | ||
| .append(msg("htmlformentry.scheduleAppointment.doSchedule")).append("</label> "); | ||
| sb.append("<label><input type=\"radio\" name=\"").append(choiceFieldName) | ||
| .append("\" value=\"no\"") | ||
| .append("\" value=\"no\"").append(defaultToScheduling ? "" : " checked=\"true\"") | ||
| .append(" onchange=\"this.closest('.schedule-appointment').querySelector('.schedule-appointment-fields').style.display='none'\"> ") | ||
| .append(msg("htmlformentry.scheduleAppointment.doNotSchedule")).append("</label>"); | ||
| sb.append("</p>"); | ||
| sb.append("<div class=\"schedule-appointment-fields\" style=\"display:block\">"); | ||
| sb.append("<div class=\"schedule-appointment-fields\" style=\"display:") | ||
| .append(defaultToScheduling ? "block" : "none").append("\">"); | ||
| } | ||
|
|
||
| // Location | ||
|
|
@@ -355,7 +381,7 @@ public String generateHtml(FormEntryContext context) { | |
| msg("htmlformentry.scheduleAppointment.note"), | ||
| noteWidget.generateHtml(context))); | ||
|
|
||
| if (optional) { | ||
| if (showScheduleChoice) { | ||
| sb.append("</div>"); | ||
| } | ||
|
|
||
|
|
@@ -433,11 +459,11 @@ private String generateViewEditHtml() { | |
|
|
||
| @Override | ||
| public Collection<FormSubmissionError> validateSubmission(FormEntryContext context, HttpServletRequest submission) { | ||
| if (context.getMode() != Mode.ENTER) { | ||
| if (!allowScheduling) { | ||
| return Collections.emptyList(); | ||
| } | ||
|
|
||
| if (optional && !"yes".equals(scheduleChoiceWidget.getValue(context, submission))) { | ||
| if (showScheduleChoice && !"yes".equals(scheduleChoiceWidget.getValue(context, submission))) { | ||
| return Collections.emptyList(); | ||
| } | ||
|
|
||
|
|
@@ -499,14 +525,15 @@ public Collection<FormSubmissionError> validateSubmission(FormEntryContext conte | |
|
|
||
| @Override | ||
| public void handleSubmission(FormEntrySession session, HttpServletRequest submission) { | ||
| // EDIT mode is intentionally a no-op: the element does not update existing appointments. | ||
| if (session.getContext().getMode() != Mode.ENTER) { | ||
| // Only schedule when the form was rendered for scheduling (ENTER, or EDIT with no existing | ||
| // appointment). Otherwise this is a no-op: the element never updates an existing appointment. | ||
| if (!allowScheduling) { | ||
| return; | ||
| } | ||
|
|
||
| FormEntryContext context = session.getContext(); | ||
|
|
||
| if (optional && !"yes".equals(scheduleChoiceWidget.getValue(context, submission))) { | ||
| if (showScheduleChoice && !"yes".equals(scheduleChoiceWidget.getValue(context, submission))) { | ||
| return; | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With a
controlIdset, this lookup only matches obs whoseformFieldPathcarries that control id, so an appointment recorded while the tag had nocontrolIdisn't found. Under the new rule,uuidObs == nullnow means "offer another one".I reproduced it in api-tests: seed encounter 101 with an appointment-uuid obs whose
formFieldPathis null (exactly whathandleSubmissionwrites when nocontrolIdis configured), then open it in EDIT withcontrolId="appt1"on the tag. Master renders the "No appointment scheduled" message, wrong but harmless. This branch renders the full scheduling form, and submitting it leaves the encounter with two appointment-uuid obs and the patient with two live appointments. The encounter then stops rendering at all, because two obs sharing a control id makegetObsFromExistingObsthrowIllegalStateException: Multiple obs are matching the control id 'appt1'in both VIEW and EDIT (that last part is framework behaviour, identical on master).Adding a
controlIdto an existing tag is a routine form edit, so I'd rather this didn't hinge on the two lookups agreeing. Before concluding that nothing was scheduled, check whether the encounter has any obs foruuidConceptat all (theexistingObsentry, read ahead of the remove) instead of trusting the controlId-scoped match. That also covers the mirror case, where acontrolIdgets dropped from a tag and the already-stampedformFieldPathmakesremoveExistingObsmiss.It takes a deliberate form change to trigger, so I don't think it has to hold up the merge if you'd rather split it out, but it's this PR that turns the mismatch from a cosmetic one into duplicate clinical data.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I understand correctly, this problem happens when we first record an appointment in a form with no
controlId, then change the form so that the tag has acontrolId, and then if we EDIT the form, it doesn't know that the prior appointment has already been created.Given that we expect users to version bump the form if they make edits to it, and that this is also how
controlIdin the<obs>tag works (if we change the controlId of<obs>, prior obs recordings will not be matched properly onto the right field), I think this is fine? Let me know if you disagree.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm fine with what @chibongho says above (the onus is on the implementer to upgrade the form version). Alternatively, since this is a new tag, we could just mandate that a controlId is used? (This is some that should be used for all data points, but it didn't exist back in the early days).