diff --git a/src/main/java/com/iemr/flw/domain/iemr/QuestionValidation.java b/src/main/java/com/iemr/flw/domain/iemr/QuestionValidation.java index ebfe9ac3..cf21b8d4 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/QuestionValidation.java +++ b/src/main/java/com/iemr/flw/domain/iemr/QuestionValidation.java @@ -42,7 +42,7 @@ /** * Server-side validation rule for a question. - * validationType: MAX_LENGTH | MIN_DATE | MAX_DATE | REGEX | MANDATORY_IF + * validationType: MAX_LENGTH | MIN_DATE | MAX_DATE | REGEX | MANDATORY_IF | UNIT | HELPER_TEXT_ENGLISH | HELPER_TEXT_HINDI | AUTO_FILL */ @Entity @Table(name = "t_question_validation", schema = "db_iemr") diff --git a/src/main/java/com/iemr/flw/domain/iemr/SectionQuestion.java b/src/main/java/com/iemr/flw/domain/iemr/SectionQuestion.java index a50418d0..a75634dd 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/SectionQuestion.java +++ b/src/main/java/com/iemr/flw/domain/iemr/SectionQuestion.java @@ -98,6 +98,15 @@ public class SectionQuestion { @Column(name = "containsPii", nullable = false) private Boolean containsPii = false; + /** If true, the client must make sure this question has a value at least once after a new form + * version is sent — e.g. a field added in that version that earlier responses never captured. */ + @Column(name = "requiresBackFill", nullable = false) + private Boolean requiresBackFill = false; + + /** If true, the client lets the user change this question's answer after the form is submitted. */ + @Column(name = "editableAfterSubmit", nullable = false) + private Boolean editableAfterSubmit = false; + /** Soft-delete flag: false means unlinked from its section. Never hard-deleted — historical * QuestionResponse rows reference questionId as a plain Long outside JPA's cascade graph. */ @Column(name = "isActive", nullable = false) diff --git a/src/main/java/com/iemr/flw/dto/iemr/SectionQuestionDTO.java b/src/main/java/com/iemr/flw/dto/iemr/SectionQuestionDTO.java index 11a81430..5f80c6ca 100644 --- a/src/main/java/com/iemr/flw/dto/iemr/SectionQuestionDTO.java +++ b/src/main/java/com/iemr/flw/dto/iemr/SectionQuestionDTO.java @@ -65,6 +65,12 @@ public class SectionQuestionDTO { private Boolean containsPii = false; + /** If true, the client must make sure this question has a value at least once after a new form version is sent. */ + private Boolean requiresBackFill = false; + + /** If true, the client lets the user change this question's answer after the form is submitted. */ + private Boolean editableAfterSubmit = false; + /** Computed at read time: false if this question is a target of any OptionCondition. */ private Boolean visibleByDefault = true; diff --git a/src/main/java/com/iemr/flw/masterEnum/ValidationType.java b/src/main/java/com/iemr/flw/masterEnum/ValidationType.java index ed789158..c9bf0ce9 100644 --- a/src/main/java/com/iemr/flw/masterEnum/ValidationType.java +++ b/src/main/java/com/iemr/flw/masterEnum/ValidationType.java @@ -41,5 +41,13 @@ public enum ValidationType { MANDATORY_IF, /** Display unit for a numeric answer. validationParam = unit label e.g. "sq. m.". * Informational only — not a rule the answer is checked against. */ - UNIT + UNIT, + /** One English helper-text bullet shown under the question. validationParam = the bullet text. + * A question may have several; display them in validationId order. Informational only. */ + HELPER_TEXT_ENGLISH, + /** Hindi counterpart of {@link #HELPER_TEXT_ENGLISH}, one bullet per row, in validationId order. */ + HELPER_TEXT_HINDI, + /** The client must fill this answer itself instead of asking the user. validationParam = the source, + * e.g. "TODAY" or "CAMP_XRAY_RESULT". Informational only — not a rule the answer is checked against. */ + AUTO_FILL } \ No newline at end of file diff --git a/src/main/java/com/iemr/flw/seeder/ContactFollowUpFormSeeder.java b/src/main/java/com/iemr/flw/seeder/ContactFollowUpFormSeeder.java index e8a7afaa..dcc0e6d0 100644 --- a/src/main/java/com/iemr/flw/seeder/ContactFollowUpFormSeeder.java +++ b/src/main/java/com/iemr/flw/seeder/ContactFollowUpFormSeeder.java @@ -116,10 +116,6 @@ private FormSectionDTO buildScreeningSection() { qs.add(yesNoQuestion("CFU_VISITED_FACILITY", "Have you visited the health facility", "क्या आपने स्वास्थ्य सुविधा का दौरा किया", 2)); - // Status of Clinical Screening — "TPT Eligible" opens the TPT Follow Up form. - // "Full Treatment"/"No Treatment" are meant to route this record elsewhere (confirmed-case - // listing / no action) — that record-routing is out of scope here, so no condition is wired - // for those two options. SectionQuestionDTO screeningStatus = new SectionQuestionDTO(); screeningStatus.setQuestionUuid("CFU_CLINICAL_SCREENING_STATUS"); screeningStatus.setQuestionText("Status of Clinical Screening"); diff --git a/src/main/java/com/iemr/flw/seeder/migration/FormVersionCloner.java b/src/main/java/com/iemr/flw/seeder/migration/FormVersionCloner.java new file mode 100644 index 00000000..5466afad --- /dev/null +++ b/src/main/java/com/iemr/flw/seeder/migration/FormVersionCloner.java @@ -0,0 +1,96 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +package com.iemr.flw.seeder.migration; + +import com.iemr.flw.domain.iemr.FormVersion; +import com.iemr.flw.dto.iemr.DynamicFormDTO; +import com.iemr.flw.dto.iemr.FormSectionDTO; +import com.iemr.flw.dto.iemr.OptionConditionDTO; +import com.iemr.flw.dto.iemr.QuestionOptionDTO; +import com.iemr.flw.dto.iemr.QuestionValidationDTO; +import com.iemr.flw.dto.iemr.SectionQuestionDTO; +import com.iemr.flw.service.DynamicFormDefinitionService; +import com.iemr.flw.service.DynamicFormReconciliationService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +/** + * Clones a form's latest version into a new one for migrations that must leave the earlier version + * untouched — same mechanism as {@link V001_1_CreateContactTracingVersion2}. Lets several migrations + * in one batch share a single new version: the first to call {@link #ensureVersion} for a form + * creates it, the rest find it already there. + * + * Uses {@link DynamicFormDefinitionService#updateForm}, which copies every active section, question, + * option, validation and condition into a new version and marks the current one isLatest=false. All + * ids are cleared from the copied DTO first: the mapper copies them onto the new entities, and saving + * an entity with an existing id would move that row into the new version instead of copying it. + * Conditions are re-wired by targetQuestionUuid/targetSectionUuid, which are kept. + */ +@Component +@RequiredArgsConstructor +public class FormVersionCloner { + + private final DynamicFormDefinitionService definitionService; + + /** + * Ensures {@code formUuid}'s latest version is {@code targetVersion}, cloning the previous one into + * it if needed, and returns it. Fails if the latest is neither the target nor the one just before it. + */ + public FormVersion ensureVersion(DynamicFormReconciliationService svc, String formUuid, int targetVersion) { + FormVersion latest = svc.latestVersion(formUuid) + .orElseThrow(() -> new IllegalStateException(formUuid + " not found — must run after its seeder")); + if (latest.getVersionNumber() == targetVersion) { + return latest; + } + if (latest.getVersionNumber() != targetVersion - 1) { + throw new IllegalStateException(formUuid + " is at version " + latest.getVersionNumber() + + ", expected " + (targetVersion - 1) + " or " + targetVersion); + } + + Long formId = latest.getDynamicForm().getFormId(); + DynamicFormDTO current = definitionService.getFormDefinition(formId); + clearIds(current); + definitionService.updateForm(formId, current); + return svc.latestVersion(formUuid) + .orElseThrow(() -> new IllegalStateException(formUuid + " version " + targetVersion + " not created")); + } + + private void clearIds(DynamicFormDTO form) { + for (FormSectionDTO section : form.getSections()) { + section.setSectionId(null); + for (SectionQuestionDTO question : section.getQuestions()) { + question.setQuestionId(null); + for (QuestionValidationDTO validation : question.getValidations()) { + validation.setValidationId(null); + } + for (QuestionOptionDTO option : question.getOptions()) { + option.setOptionId(null); + for (OptionConditionDTO condition : option.getConditions()) { + condition.setConditionId(null); + condition.setTargetQuestionId(null); + condition.setTargetSectionId(null); + } + } + } + } + } +} diff --git a/src/main/java/com/iemr/flw/seeder/migration/V010_RestructureContactFollowUpStatus.java b/src/main/java/com/iemr/flw/seeder/migration/V010_RestructureContactFollowUpStatus.java new file mode 100644 index 00000000..4ad2a9b3 --- /dev/null +++ b/src/main/java/com/iemr/flw/seeder/migration/V010_RestructureContactFollowUpStatus.java @@ -0,0 +1,379 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +package com.iemr.flw.seeder.migration; + +import com.iemr.flw.domain.iemr.FormSection; +import com.iemr.flw.domain.iemr.FormVersion; +import com.iemr.flw.domain.iemr.QuestionOption; +import com.iemr.flw.domain.iemr.SectionQuestion; +import com.iemr.flw.dto.iemr.OptionConditionDTO; +import com.iemr.flw.dto.iemr.QuestionOptionDTO; +import com.iemr.flw.dto.iemr.QuestionValidationDTO; +import com.iemr.flw.dto.iemr.SectionQuestionDTO; +import com.iemr.flw.masterEnum.QuestionType; +import com.iemr.flw.masterEnum.ValidationType; +import com.iemr.flw.repo.iemr.FormSectionRepo; +import com.iemr.flw.repo.iemr.QuestionOptionRepo; +import com.iemr.flw.repo.iemr.QuestionValidationRepo; +import com.iemr.flw.repo.iemr.SectionQuestionRepo; +import com.iemr.flw.service.DynamicFormReconciliationService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Set; + +/** + * Restructures the Contact Follow Up form's "Screening & Referral" section (CFU_SEC_1) per the + * revised Contact Follow-Up spec (PRD v203). The changes go into a new version 2 (via + * {@link FormVersionCloner}); version 1 keeps the seeded structure, so responses submitted against it + * keep their meaning. + * + * Final version-2 order (FINAL_QUESTION_ORDER): + * + * 1. Date (CFU_FOLLOW_UP_DATE, mandatory date, not in the future) — AUTO_FILL "TODAY". + * 2. "Chest X-ray Result" (CFU_CAMP_XRAY_RESULT, AUTO_FILL, optional) — AUTO_FILL "CAMP_XRAY_RESULT": + * the client fills it from the X-ray done in the camp, if any. + * 3. "NAAT Result" (CFU_CAMP_NAAT_RESULT, AUTO_FILL, optional) — AUTO_FILL "CAMP_NAAT_RESULT": the client + * fills it from the NAAT done in the camp, if any. + * 4. "Is beneficiary Referred to PHI?" (CFU_REFERRED_TO_PHI, mandatory Yes/No). + * 5. "Has beneficiary visited the PHI?" (CFU_VISITED_PHI, Yes/No) — shown when #4 = Yes; No → submit. + * 6. "Is Chest X-ray conducted?" (CFU_XRAY_CONDUCTED, Yes/No) — shown when #5 = Yes. + * 7. "Chest X-ray Result" (CFU_XRAY_RESULT: Normal, TB Presumptive, Abnormal but not TB Presumptive) + * — shown when #6 = Yes. + * 8. "Is NAAT conducted?" (CFU_NAAT_CONDUCTED, Yes/No) — shown when #5 = Yes. + * 9. "NAAT Result" (CFU_NAAT_RESULT: MTB Detected, MTB Not Detected, Indeterminate) — shown when #8 = Yes. + * 10. "Is Cy-TB Test conducted?" (CFU_CYTB_CONDUCTED, Yes/No) — shown when #5 = Yes. + * 11. "Cy-TB Test Result" (CFU_CYTB_RESULT: Positive, Negative) — shown when #10 = Yes. + * 12. "Treatment recommended by MO/ TU/ PHI" (CFU_CLINICAL_SCREENING_STATUS, radio: Recommend for TPT, + * Not recommended TPT, Recommend for ATT) — shown when #5 = Yes. Its option rows are the seeded + * ones renamed in place (TPT_ELIGIBLE, NO_TREATMENT, FULL_TREATMENT); optionUuid is deliberately left + * unchanged so clients keep mapping existing answers to the same option. + * 13. Remarks (CFU_REMARKS, optional free text, max 200 chars, all characters allowed). + * + * "Advised to visit health facility" (CFU_ADVISED_VISIT_FACILITY) and "Have you visited the health + * facility" (CFU_VISITED_FACILITY) are soft-removed — never hard-deleted, historical responses may + * reference them. Every question above is flagged requiresBackFill, so the client makes sure each has a + * value on beneficiaries whose earlier responses were captured on version 1 (for the camp results, by + * auto-filling them). #4, #5, #6, #8 and #10 are flagged editableAfterSubmit, + * and the section itself is set isEditable — the submit path skips every answer in a non-editable section, + * so the follow-up can be continued after submit. + * + * Deliberately not implemented: routing a record on "Recommend for TPT" (TPT Follow up) / "Recommend for + * ATT" (Confirmed TB Cases) — no option opens another form; those flows will be built separately. Handled + * statically by the client: hiding #2 when #6 = Yes and #3 when #8 = Yes, and hiding #6 / #8 when the camp + * result was auto-filled (the engine has no hide action); enabling "Recommend for ATT" only when the X-ray + * result is not Normal or NAAT = MTB Detected (the engine has no option-level conditions); and multiline + * rendering of Remarks (there is no multiline question type). + * + * Re-runnable: questions/options/conditions/validations go through ensureX or uuid/optionValue lookups, + * labels, option order, Remarks' limit and the final display order are set to the same values each run, + * and version 2 is reused once created. Re-running it also brings a database where an earlier revision of + * this migration already ran up to this one. + */ +@Component +@RequiredArgsConstructor +public class V010_RestructureContactFollowUpStatus implements FormStructureMigration { + + private static final String FORM_UUID = "CONTACT_FOLLOW_UP"; + private static final String SECTION_UUID = "CFU_SEC_1"; + private static final String DATE_UUID = "CFU_FOLLOW_UP_DATE"; + private static final String CAMP_XRAY_RESULT_UUID = "CFU_CAMP_XRAY_RESULT"; + private static final String CAMP_NAAT_RESULT_UUID = "CFU_CAMP_NAAT_RESULT"; + private static final String REFERRED_TO_PHI_UUID = "CFU_REFERRED_TO_PHI"; + private static final String VISITED_PHI_UUID = "CFU_VISITED_PHI"; + private static final String XRAY_CONDUCTED_UUID = "CFU_XRAY_CONDUCTED"; + private static final String XRAY_RESULT_UUID = "CFU_XRAY_RESULT"; + private static final String NAAT_CONDUCTED_UUID = "CFU_NAAT_CONDUCTED"; + private static final String NAAT_RESULT_UUID = "CFU_NAAT_RESULT"; + private static final String CYTB_CONDUCTED_UUID = "CFU_CYTB_CONDUCTED"; + private static final String CYTB_RESULT_UUID = "CFU_CYTB_RESULT"; + private static final String STATUS_QUESTION_UUID = "CFU_CLINICAL_SCREENING_STATUS"; + private static final String REMARKS_UUID = "CFU_REMARKS"; + private static final int REMARKS_MAX_LENGTH = 200; + private static final int TARGET_VERSION = 2; + + private static final List REMOVED_QUESTION_UUIDS = List.of( + "CFU_ADVISED_VISIT_FACILITY", + "CFU_VISITED_FACILITY"); + + /** Active questions top to bottom; all are new or changed in version 2. */ + private static final List FINAL_QUESTION_ORDER = List.of( + DATE_UUID, + CAMP_XRAY_RESULT_UUID, + CAMP_NAAT_RESULT_UUID, + REFERRED_TO_PHI_UUID, + VISITED_PHI_UUID, + XRAY_CONDUCTED_UUID, + XRAY_RESULT_UUID, + NAAT_CONDUCTED_UUID, + NAAT_RESULT_UUID, + CYTB_CONDUCTED_UUID, + CYTB_RESULT_UUID, + STATUS_QUESTION_UUID, + REMARKS_UUID); + + private static final Set EDITABLE_AFTER_SUBMIT_UUIDS = Set.of( + REFERRED_TO_PHI_UUID, + VISITED_PHI_UUID, + XRAY_CONDUCTED_UUID, + NAAT_CONDUCTED_UUID, + CYTB_CONDUCTED_UUID); + + private record Choice(String label, String labelHindi, String value) {} + + private record OptionRename(String oldValue, String newValue, String label, String labelHindi, int order) {} + + private static final List STATUS_OPTION_RENAMES = List.of( + new OptionRename("TPT_ELIGIBLE", "RECOMMEND_FOR_TPT", "Recommend for TPT", "टीपीटी के लिए सिफारिश", 1), + new OptionRename("NO_TREATMENT", "TPT_NOT_RECOMMENDED", "Not recommended TPT", "टीपीटी की सिफारिश नहीं", 2), + new OptionRename("FULL_TREATMENT", "RECOMMEND_FOR_ATT", "Recommend for ATT", "एटीटी के लिए सिफारिश", 3)); + + private static final List XRAY_RESULTS = List.of( + new Choice("Normal", "सामान्य", "NORMAL"), + new Choice("TB Presumptive", "टीबी संभावित", "TB_PRESUMPTIVE"), + new Choice("Abnormal but not TB Presumptive", "असामान्य लेकिन टीबी संभावित नहीं", "ABNORMAL_NOT_TB_PRESUMPTIVE")); + + private static final List NAAT_RESULTS = List.of( + new Choice("MTB Detected", "MTB पाया गया", "MTB_DETECTED"), + new Choice("MTB Not Detected", "MTB नहीं पाया गया", "MTB_NOT_DETECTED"), + new Choice("Indeterminate", "अनिर्धारित", "INDETERMINATE")); + + private static final List CYTB_RESULTS = List.of( + new Choice("Positive", "पॉजिटिव", "POSITIVE"), + new Choice("Negative", "नेगेटिव", "NEGATIVE")); + + private static final List YES_NO = List.of( + new Choice("Yes", "हाँ", "YES"), + new Choice("No", "नहीं", "NO")); + + private final QuestionOptionRepo optionRepo; + private final SectionQuestionRepo questionRepo; + private final QuestionValidationRepo validationRepo; + private final FormSectionRepo sectionRepo; + private final FormVersionCloner versionCloner; + + @Override + public String migrationId() { + return "V010"; + } + + @Override + public void apply(DynamicFormReconciliationService svc) { + FormVersion version = versionCloner.ensureVersion(svc, FORM_UUID, TARGET_VERSION); + FormSection section = svc.section(version, SECTION_UUID) + .orElseThrow(() -> new IllegalStateException("Section " + SECTION_UUID + " not found")); + + REMOVED_QUESTION_UUIDS.forEach(uuid -> svc.question(section, uuid) + .ifPresent(q -> svc.unlinkQuestion(q.getQuestionId()))); + + // New questions are appended (no displayOrder → no shifting); the final order is set at the end. + addDateQuestion(svc, section); + addAutoFillQuestion(svc, section, CAMP_XRAY_RESULT_UUID, "Chest X-ray Result", + "छाती के एक्स-रे का परिणाम", "CAMP_XRAY_RESULT", "Auto-filled from the X-ray done in the camp"); + addAutoFillQuestion(svc, section, CAMP_NAAT_RESULT_UUID, "NAAT Result", + "NAAT परिणाम", "CAMP_NAAT_RESULT", "Auto-filled from the NAAT done in the camp"); + addChoiceQuestion(svc, section, REFERRED_TO_PHI_UUID, "Is beneficiary Referred to PHI?", + "क्या लाभार्थी को पीएचआई रेफर किया गया है?", QuestionType.RADIO, YES_NO); + addChoiceQuestion(svc, section, VISITED_PHI_UUID, "Has beneficiary visited the PHI?", + "क्या लाभार्थी ने पीएचआई का दौरा किया है?", QuestionType.RADIO, YES_NO); + addChoiceQuestion(svc, section, XRAY_CONDUCTED_UUID, "Is Chest X-ray conducted?", + "क्या छाती का एक्स-रे किया गया?", QuestionType.RADIO, YES_NO); + addChoiceQuestion(svc, section, XRAY_RESULT_UUID, "Chest X-ray Result", + "छाती के एक्स-रे का परिणाम", QuestionType.RADIO, XRAY_RESULTS); + addChoiceQuestion(svc, section, NAAT_CONDUCTED_UUID, "Is NAAT conducted?", + "क्या NAAT जांच की गई?", QuestionType.RADIO, YES_NO); + addChoiceQuestion(svc, section, NAAT_RESULT_UUID, "NAAT Result", + "NAAT परिणाम", QuestionType.RADIO, NAAT_RESULTS); + addChoiceQuestion(svc, section, CYTB_CONDUCTED_UUID, "Is Cy-TB Test conducted?", + "क्या Cy-TB टेस्ट किया गया?", QuestionType.RADIO, YES_NO); + addChoiceQuestion(svc, section, CYTB_RESULT_UUID, "Cy-TB Test Result", + "Cy-TB टेस्ट परिणाम", QuestionType.RADIO, CYTB_RESULTS); + restructureStatusQuestion(svc, section); + addRemarksQuestion(svc, section); + + showWhenYes(svc, version, section, REFERRED_TO_PHI_UUID, VISITED_PHI_UUID); + showWhenYes(svc, version, section, VISITED_PHI_UUID, XRAY_CONDUCTED_UUID); + showWhenYes(svc, version, section, VISITED_PHI_UUID, NAAT_CONDUCTED_UUID); + showWhenYes(svc, version, section, VISITED_PHI_UUID, CYTB_CONDUCTED_UUID); + showWhenYes(svc, version, section, VISITED_PHI_UUID, STATUS_QUESTION_UUID); + showWhenYes(svc, version, section, XRAY_CONDUCTED_UUID, XRAY_RESULT_UUID); + showWhenYes(svc, version, section, NAAT_CONDUCTED_UUID, NAAT_RESULT_UUID); + showWhenYes(svc, version, section, CYTB_CONDUCTED_UUID, CYTB_RESULT_UUID); + + applyFinalOrderAndFlags(svc, section); + } + + private void addDateQuestion(DynamicFormReconciliationService svc, FormSection section) { + SectionQuestionDTO dto = new SectionQuestionDTO(); + dto.setQuestionUuid(DATE_UUID); + dto.setQuestionText("Date"); + dto.setQuestionTextHindi("तारीख"); + dto.setQuestionType(QuestionType.DATE); + dto.setIsMandatory(true); + SectionQuestion date = svc.ensureQuestion(section, dto); + + QuestionValidationDTO maxDate = new QuestionValidationDTO(); + maxDate.setValidationType(ValidationType.MAX_DATE); + maxDate.setValidationParam("TODAY"); + maxDate.setErrorMessage("Cannot be a future date"); + svc.ensureValidation(date, maxDate); + + svc.ensureValidation(date, autoFill("TODAY", "Auto-filled with today's date")); + } + + private void addAutoFillQuestion(DynamicFormReconciliationService svc, FormSection section, String uuid, + String text, String textHindi, String source, String message) { + SectionQuestionDTO dto = new SectionQuestionDTO(); + dto.setQuestionUuid(uuid); + dto.setQuestionText(text); + dto.setQuestionTextHindi(textHindi); + dto.setQuestionType(QuestionType.AUTO_FILL); + dto.setIsMandatory(false); + SectionQuestion question = svc.ensureQuestion(section, dto); + svc.ensureValidation(question, autoFill(source, message)); + } + + private QuestionValidationDTO autoFill(String source, String message) { + QuestionValidationDTO dto = new QuestionValidationDTO(); + dto.setValidationType(ValidationType.AUTO_FILL); + dto.setValidationParam(source); + dto.setErrorMessage(message); + return dto; + } + + // Labels are (re)applied on the entity too, so a question created by an earlier revision picks them up. + private void addChoiceQuestion(DynamicFormReconciliationService svc, FormSection section, String uuid, + String text, String textHindi, QuestionType type, List choices) { + SectionQuestionDTO dto = new SectionQuestionDTO(); + dto.setQuestionUuid(uuid); + dto.setQuestionText(text); + dto.setQuestionTextHindi(textHindi); + dto.setQuestionType(type); + dto.setIsMandatory(true); + SectionQuestion question = svc.ensureQuestion(section, dto); + question.setQuestionText(text); + question.setQuestionTextHindi(textHindi); + questionRepo.save(question); + + for (int i = 0; i < choices.size(); i++) { + Choice choice = choices.get(i); + QuestionOptionDTO option = new QuestionOptionDTO(); + option.setOptionLabel(choice.label()); + option.setOptionLabelHindi(choice.labelHindi()); + option.setOptionValue(choice.value()); + option.setOptionValueHindi(choice.labelHindi()); + option.setDisplayOrder(i + 1); + QuestionOption saved = svc.ensureOption(question, option); + saved.setOptionLabel(choice.label()); + saved.setOptionLabelHindi(choice.labelHindi()); + optionRepo.save(saved); + } + } + + // Options are matched by their seeded value or, after an earlier run, their renamed value. + private void restructureStatusQuestion(DynamicFormReconciliationService svc, FormSection section) { + SectionQuestion status = svc.question(section, STATUS_QUESTION_UUID) + .orElseThrow(() -> new IllegalStateException("Question " + STATUS_QUESTION_UUID + " not found")); + status.setQuestionText("Treatment recommended by MO/ TU/ PHI"); + status.setQuestionTextHindi("एमओ/ टीयू/ पीएचआई द्वारा अनुशंसित उपचार"); + status.setQuestionType(QuestionType.RADIO); + questionRepo.save(status); + + STATUS_OPTION_RENAMES.forEach(rename -> svc.option(status, rename.oldValue()) + .or(() -> svc.option(status, rename.newValue())) + .ifPresent(opt -> { + opt.setOptionLabel(rename.label()); + opt.setOptionLabelHindi(rename.labelHindi()); + opt.setOptionValue(rename.newValue()); + opt.setOptionValueHindi(rename.labelHindi()); + opt.setDisplayOrder(rename.order()); + optionRepo.save(opt); + })); + } + + // An earlier revision created Remarks with a 500-char limit; its MAX_LENGTH row is moved to 200 in place. + private void addRemarksQuestion(DynamicFormReconciliationService svc, FormSection section) { + SectionQuestionDTO dto = new SectionQuestionDTO(); + dto.setQuestionUuid(REMARKS_UUID); + dto.setQuestionText("Remarks"); + dto.setQuestionTextHindi("टिप्पणियाँ"); + dto.setQuestionType(QuestionType.TEXT); + dto.setIsMandatory(false); + dto.setMaxLength(REMARKS_MAX_LENGTH); + SectionQuestion remarks = svc.ensureQuestion(section, dto); + remarks.setIsMandatory(false); + remarks.setMaxLength(REMARKS_MAX_LENGTH); + questionRepo.save(remarks); + + String limit = String.valueOf(REMARKS_MAX_LENGTH); + String message = "Must be " + REMARKS_MAX_LENGTH + " characters or fewer"; + validationRepo.findBySectionQuestion_QuestionId(remarks.getQuestionId()).stream() + .filter(v -> v.getValidationType() == ValidationType.MAX_LENGTH && !limit.equals(v.getValidationParam())) + .forEach(v -> { + v.setValidationParam(limit); + v.setErrorMessage(message); + validationRepo.save(v); + }); + + QuestionValidationDTO maxLength = new QuestionValidationDTO(); + maxLength.setValidationType(ValidationType.MAX_LENGTH); + maxLength.setValidationParam(limit); + maxLength.setErrorMessage(message); + svc.ensureValidation(remarks, maxLength); + } + + private void showWhenYes(DynamicFormReconciliationService svc, FormVersion version, FormSection section, + String sourceQuestionUuid, String targetQuestionUuid) { + SectionQuestion source = svc.question(section, sourceQuestionUuid) + .orElseThrow(() -> new IllegalStateException("Question " + sourceQuestionUuid + " not found")); + QuestionOption yes = svc.option(source, "YES") + .orElseThrow(() -> new IllegalStateException("Option " + sourceQuestionUuid + "=YES not found")); + OptionConditionDTO show = new OptionConditionDTO(); + show.setActionType("SHOW_QUESTION"); + show.setTargetQuestionUuid(targetQuestionUuid); + svc.ensureCondition(yes, show, version); + } + + // Soft-removed questions go after the active ones so they never collide with a live display slot. + private void applyFinalOrderAndFlags(DynamicFormReconciliationService svc, FormSection section) { + section.setIsEditable(true); + sectionRepo.save(section); + + for (int i = 0; i < FINAL_QUESTION_ORDER.size(); i++) { + String uuid = FINAL_QUESTION_ORDER.get(i); + SectionQuestion question = svc.question(section, uuid) + .orElseThrow(() -> new IllegalStateException("Question " + uuid + " not found")); + question.setDisplayOrder(i + 1); + question.setRequiresBackFill(true); + question.setEditableAfterSubmit(EDITABLE_AFTER_SUBMIT_UUIDS.contains(uuid)); + questionRepo.save(question); + } + for (int i = 0; i < REMOVED_QUESTION_UUIDS.size(); i++) { + int order = FINAL_QUESTION_ORDER.size() + i + 1; + svc.question(section, REMOVED_QUESTION_UUIDS.get(i)).ifPresent(q -> { + q.setDisplayOrder(order); + questionRepo.save(q); + }); + } + } +} diff --git a/src/main/java/com/iemr/flw/seeder/migration/V011_UpdateTptFollowUpVisitQuestions.java b/src/main/java/com/iemr/flw/seeder/migration/V011_UpdateTptFollowUpVisitQuestions.java new file mode 100644 index 00000000..8ec9a25d --- /dev/null +++ b/src/main/java/com/iemr/flw/seeder/migration/V011_UpdateTptFollowUpVisitQuestions.java @@ -0,0 +1,263 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +package com.iemr.flw.seeder.migration; + +import com.iemr.flw.domain.iemr.FormSection; +import com.iemr.flw.domain.iemr.FormVersion; +import com.iemr.flw.domain.iemr.QuestionOption; +import com.iemr.flw.domain.iemr.SectionQuestion; +import com.iemr.flw.dto.iemr.QuestionOptionDTO; +import com.iemr.flw.dto.iemr.QuestionValidationDTO; +import com.iemr.flw.dto.iemr.SectionQuestionDTO; +import com.iemr.flw.masterEnum.QuestionType; +import com.iemr.flw.masterEnum.ValidationType; +import com.iemr.flw.repo.iemr.QuestionOptionRepo; +import com.iemr.flw.repo.iemr.SectionQuestionRepo; +import com.iemr.flw.service.DynamicFormReconciliationService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Updates the TPT Follow Up form per the revised TPT Follow-Up spec. The changes go into a new + * version 2 (via {@link FormVersionCloner}); version 1 keeps the seeded structure, so responses + * submitted against it keep their meaning. + * + * "Registration & Regimen Details" section (TFU_SEC_1): + * + * 0. Replaces all of "TPT Regimen advised" (TFU_REGIMEN_ADVISED)'s options with REGIMEN_OPTIONS, in + * that order: 6H, 3HP and 1HP are kept as they are; "3RH" is the existing 3HR option (same + * regimen — Rifampicin + Isoniazid daily for 3 months) with its value renamed 3HR → 3RH in place + * before the replacement; optionUuid is deliberately left unchanged (as V010 does) so clients keep + * mapping existing answers to the same option. 6Lfx and 4R are added; 6H (Modified dose) is removed. + * + * "Follow-up Visit" section (TFU_SEC_2): + * + * 1. Soft-removes "Adherence to Medicines" (TFU_ADHERENCE, Regular/Irregular radio) — never + * hard-deleted, the spec keeps the field for reference and historical responses may reference it. + * 2. Renames "Adverse Drug Reactions" (TFU_ADR) to "Side effects". Only the label text changes — + * questionUuid, type and conditions are untouched, so existing answers keep mapping to the same + * question. Edited directly on the entity and saved via its repo (as V010 does). + * 3. Replaces all of TFU_ADR's options with SIDE_EFFECT_OPTIONS, in that order, and clears its + * "UNKNOWN" default value — that option is no longer offered. + * 4. Replaces all of "TPT outcome status" (TFU_OUTCOME_STATUS)'s options with OUTCOME_STATUS_OPTIONS, + * in that order. "Treatment Completed" and "Died" are the existing COMPLETED and DIED_DURING_TPT + * options relabelled, so their SHOW_QUESTION conditions (outcome date; outcome date + cause of + * death) and the MANDATORY_IF validations on TFU_OUTCOME_STATUS=COMPLETED/DIED_DURING_TPT keep + * working unchanged. DEVELOPED_ACTIVE_TB and OTHER are removed, and so is "Other" (TFU_OUTCOME_OTHER), + * the question only the OTHER option revealed. + * 5. Adds "Remarks" (TFU_OUTCOME_REMARKS, optional free text, max 500 chars) in the removed + * TFU_OUTCOME_OTHER's place, always shown (no condition targets it). + * + * 6. Flags TFU_REGIMEN_ADVISED with requiresBackFill, so the client asks for it on beneficiaries whose + * earlier responses were captured on version 1. The changed Follow-up Visit questions — TFU_ADR, + * TFU_OUTCOME_STATUS and TFU_OUTCOME_REMARKS — are explicitly set requiresBackFill = false: TFU_SEC_2 is + * a POST_SUBMIT section, so every visit is a new entry and there is no earlier answer to back-fill. + * + * Option replacement (0, 3, 4): options not in the list are soft-removed (never hard-deleted — historical + * responses may reference them). Listed options reuse the row with the same optionValue where one + * exists (reactivated if a previous change removed it), so its optionUuid and existing answers stay + * stable; missing ones are created. + * + * Every step is idempotent (version 2 is reused once created, lookups by uuid/optionValue, same-value + * updates), so a re-run is a no-op. + */ +@Component +@RequiredArgsConstructor +public class V011_UpdateTptFollowUpVisitQuestions implements FormStructureMigration { + + private static final String FORM_UUID = "TPT_FOLLOW_UP"; + private static final String REGISTRATION_SECTION_UUID = "TFU_SEC_1"; + private static final String SECTION_UUID = "TFU_SEC_2"; + private static final String REGIMEN_QUESTION_UUID = "TFU_REGIMEN_ADVISED"; + private static final String ADHERENCE_QUESTION_UUID = "TFU_ADHERENCE"; + private static final String ADR_QUESTION_UUID = "TFU_ADR"; + private static final String ADR_NEW_TEXT = "Side effects"; + private static final String ADR_NEW_TEXT_HINDI = "दुष्प्रभाव"; + private static final String OUTCOME_STATUS_QUESTION_UUID = "TFU_OUTCOME_STATUS"; + private static final String OUTCOME_OTHER_QUESTION_UUID = "TFU_OUTCOME_OTHER"; + private static final String OUTCOME_REMARKS_QUESTION_UUID = "TFU_OUTCOME_REMARKS"; + private static final int OUTCOME_REMARKS_MAX_LENGTH = 500; + private static final int TARGET_VERSION = 2; + + private record OptionSpec(String label, String labelHindi, String value) {} + + private static final List REGIMEN_OPTIONS = List.of( + new OptionSpec("6H — Isoniazid daily for 6 months", + "6H — 6 महीने के लिए प्रतिदिन आइसोनियाज़िड", "6H"), + new OptionSpec("3RH — Rifampicin + Isoniazid daily for 3 months", + "3RH — 3 महीने के लिए प्रतिदिन रिफैम्पिसिन + आइसोनियाज़िड", "3RH"), + new OptionSpec("3HP — Isoniazid + Rifapentine weekly for 3 months", + "3HP — 3 महीने के लिए साप्ताहिक आइसोनियाज़िड + रिफापेंटाइन", "3HP"), + new OptionSpec("6Lfx — Levofloxacin daily for 6 months", + "6Lfx — 6 महीने के लिए प्रतिदिन लेवोफ़्लॉक्सासिन", "6LFX"), + new OptionSpec("4R — Rifampicin daily for 4 months", + "4R — 4 महीने के लिए प्रतिदिन रिफैम्पिसिन", "4R"), + new OptionSpec("1HP — Isoniazid + Rifapentine daily for 1 month", + "1HP — 1 महीने के लिए प्रतिदिन आइसोनियाज़िड + रिफापेंटाइन", "1HP")); + + // Hindi left empty, as seeded for this question. + private static final List SIDE_EFFECT_OPTIONS = List.of( + new OptionSpec("Nausea", "", "NAUSEA"), + new OptionSpec("Vomiting", "", "VOMITING"), + new OptionSpec("Abdominal Pain", "", "ABDOMINAL_PAIN"), + new OptionSpec("Flue like syndrome- Chills, Dry cough, Shortness of breath, loss of appetite, body ache and malaise, dizziness, headache", + "", "FLU_LIKE_SYNDROME_WITH_DIZZINESS_HEADACHE"), + new OptionSpec("Yellowish discoloration of skin and eyes", "", "YELLOWISH_DISCOLORATION_SKIN_EYES"), + new OptionSpec("Discoloration of body fluids", "", "DISCOLORATION_BODY_FLUIDS"), + new OptionSpec("Dark colour Urine", "", "DARK_COLOUR_URINE"), + new OptionSpec("Pale stool", "", "PALE_STOOL"), + new OptionSpec("Skin Rash", "", "SKIN_RASH"), + new OptionSpec("Itching", "", "ITCHING"), + new OptionSpec("Tingling/burning/numbness in hands and feet", "", "TINGLING_BURNING_NUMBNESS_HANDS_FEET"), + new OptionSpec("Flue like syndrome- Chills, Dry cough, Shortness of breath, loss of appetite, body ache and malaise", + "", "FLU_LIKE_SYNDROME"), + new OptionSpec("Sleepiness, lethargy", "", "SLEEPINESS_LETHARGY"), + new OptionSpec("Persistent episodes of unformed watery stools", "", "PERSISTENT_UNFORMED_WATERY_STOOLS"), + new OptionSpec("Mental changes and signs of bleeding", "", "MENTAL_CHANGES_SIGNS_BLEEDING"), + new OptionSpec("Convulsions", "", "CONVULSIONS"), + new OptionSpec("Anaemia", "", "ANAEMIA"), + new OptionSpec("Arthralgia", "", "ARTHRALGIA"), + new OptionSpec("Decreased appetite", "", "DECREASED_APPETITE"), + new OptionSpec("Hypotension / Syncope", "", "HYPOTENSION_SYNCOPE"), + new OptionSpec("Conjuctivities", "", "CONJUCTIVITIES"), + new OptionSpec("Shock", "", "SHOCK")); + + private static final List OUTCOME_STATUS_OPTIONS = List.of( + new OptionSpec("Treatment Completed", "उपचार पूर्ण", "COMPLETED"), + new OptionSpec("Died", "मृत्यु", "DIED_DURING_TPT"), + new OptionSpec("Treatment failure", "उपचार विफलता", "TREATMENT_FAILURE"), + new OptionSpec("Lost to follow-up", "फॉलो-अप में खोया", "LOST_TO_FOLLOW_UP"), + new OptionSpec("TPT discontinuation due to toxicity", "विषाक्तता के कारण टीपीटी बंद", "TPT_DISCONTINUED_TOXICITY"), + new OptionSpec("Not evaluated", "मूल्यांकन नहीं किया गया", "NOT_EVALUATED"), + new OptionSpec("Treatment regimen changed", "उपचार पद्धति बदली गई", "TREATMENT_REGIMEN_CHANGED")); + + private final SectionQuestionRepo questionRepo; + private final QuestionOptionRepo optionRepo; + private final FormVersionCloner versionCloner; + + @Override + public String migrationId() { + return "V011"; + } + + @Override + public void apply(DynamicFormReconciliationService svc) { + FormVersion version = versionCloner.ensureVersion(svc, FORM_UUID, TARGET_VERSION); + + FormSection registration = svc.section(version, REGISTRATION_SECTION_UUID) + .orElseThrow(() -> new IllegalStateException("Section " + REGISTRATION_SECTION_UUID + " not found")); + SectionQuestion regimen = requireQuestion(svc, registration, REGIMEN_QUESTION_UUID); + svc.option(regimen, "3HR").ifPresent(opt -> { + opt.setOptionValue("3RH"); + optionRepo.save(opt); + }); + replaceOptions(svc, regimen, REGIMEN_OPTIONS); + + FormSection section = svc.section(version, SECTION_UUID) + .orElseThrow(() -> new IllegalStateException("Section " + SECTION_UUID + " not found")); + + svc.question(section, ADHERENCE_QUESTION_UUID) + .ifPresent(q -> svc.unlinkQuestion(q.getQuestionId())); + + SectionQuestion adr = requireQuestion(svc, section, ADR_QUESTION_UUID); + adr.setQuestionText(ADR_NEW_TEXT); + adr.setQuestionTextHindi(ADR_NEW_TEXT_HINDI); + adr.setDefaultValue(null); + questionRepo.save(adr); + replaceOptions(svc, adr, SIDE_EFFECT_OPTIONS); + + replaceOptions(svc, requireQuestion(svc, section, OUTCOME_STATUS_QUESTION_UUID), OUTCOME_STATUS_OPTIONS); + svc.question(section, OUTCOME_OTHER_QUESTION_UUID).ifPresent(other -> { + svc.unlinkQuestion(other.getQuestionId()); + addOutcomeRemarksQuestion(svc, section, other.getDisplayOrder()); + }); + + setRequiresBackFill(svc, registration, REGIMEN_QUESTION_UUID, true); + setRequiresBackFill(svc, section, ADR_QUESTION_UUID, false); + setRequiresBackFill(svc, section, OUTCOME_STATUS_QUESTION_UUID, false); + setRequiresBackFill(svc, section, OUTCOME_REMARKS_QUESTION_UUID, false); + } + + private void setRequiresBackFill(DynamicFormReconciliationService svc, FormSection section, String uuid, + boolean requiresBackFill) { + SectionQuestion question = requireQuestion(svc, section, uuid); + question.setRequiresBackFill(requiresBackFill); + questionRepo.save(question); + } + + // Takes the removed Other question's displayOrder; ensureQuestion shifts it and everything after down by one. + private void addOutcomeRemarksQuestion(DynamicFormReconciliationService svc, FormSection section, int displayOrder) { + SectionQuestionDTO dto = new SectionQuestionDTO(); + dto.setQuestionUuid(OUTCOME_REMARKS_QUESTION_UUID); + dto.setQuestionText("Remarks"); + dto.setQuestionTextHindi("टिप्पणियाँ"); + dto.setQuestionType(QuestionType.TEXT); + dto.setIsMandatory(false); + dto.setMaxLength(OUTCOME_REMARKS_MAX_LENGTH); + dto.setDisplayOrder(displayOrder); + SectionQuestion remarks = svc.ensureQuestion(section, dto); + + QuestionValidationDTO maxLength = new QuestionValidationDTO(); + maxLength.setValidationType(ValidationType.MAX_LENGTH); + maxLength.setValidationParam(String.valueOf(OUTCOME_REMARKS_MAX_LENGTH)); + maxLength.setErrorMessage("Must be " + OUTCOME_REMARKS_MAX_LENGTH + " characters or fewer"); + svc.ensureValidation(remarks, maxLength); + } + + private SectionQuestion requireQuestion(DynamicFormReconciliationService svc, FormSection section, String uuid) { + return svc.question(section, uuid) + .orElseThrow(() -> new IllegalStateException("Question " + uuid + " not found")); + } + + private void replaceOptions(DynamicFormReconciliationService svc, SectionQuestion question, List specs) { + Set keptValues = specs.stream().map(OptionSpec::value).collect(Collectors.toSet()); + optionRepo.findBySectionQuestion_QuestionIdOrderByDisplayOrderAsc(question.getQuestionId()).stream() + .filter(opt -> !keptValues.contains(opt.getOptionValue())) + .forEach(opt -> svc.removeOption(opt.getOptionId())); + + // No displayOrder on create → appended without shifting; final order is set explicitly below. + for (int i = 0; i < specs.size(); i++) { + OptionSpec spec = specs.get(i); + QuestionOption option = svc.option(question, spec.value()) + .orElseGet(() -> svc.ensureOption(question, newOption(spec))); + option.setOptionLabel(spec.label()); + option.setOptionLabelHindi(spec.labelHindi()); + option.setOptionValueHindi(spec.labelHindi()); + option.setDisplayOrder(i + 1); + option.setIsActive(true); + optionRepo.save(option); + } + } + + private QuestionOptionDTO newOption(OptionSpec spec) { + QuestionOptionDTO dto = new QuestionOptionDTO(); + dto.setOptionLabel(spec.label()); + dto.setOptionLabelHindi(spec.labelHindi()); + dto.setOptionValue(spec.value()); + dto.setOptionValueHindi(spec.labelHindi()); + return dto; + } +} diff --git a/src/main/java/com/iemr/flw/seeder/migration/V012_AddTbCounsellingSchemeHelperText.java b/src/main/java/com/iemr/flw/seeder/migration/V012_AddTbCounsellingSchemeHelperText.java new file mode 100644 index 00000000..40f05eaf --- /dev/null +++ b/src/main/java/com/iemr/flw/seeder/migration/V012_AddTbCounsellingSchemeHelperText.java @@ -0,0 +1,141 @@ +/* + * AMRIT – Accessible Medical Records via Integrated Technology + * Integrated EHR (Electronic Health Records) Solution + * + * Copyright (C) "Piramal Swasthya Management and Research Institute" + * + * This file is part of AMRIT. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ +package com.iemr.flw.seeder.migration; + +import com.iemr.flw.domain.iemr.FormSection; +import com.iemr.flw.domain.iemr.FormVersion; +import com.iemr.flw.domain.iemr.SectionQuestion; +import com.iemr.flw.dto.iemr.QuestionValidationDTO; +import com.iemr.flw.masterEnum.ValidationType; +import com.iemr.flw.repo.iemr.SectionQuestionRepo; +import com.iemr.flw.service.DynamicFormReconciliationService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * Adds counsellor guidance to the TB Counselling V2 form's "Government Schemes" section (TB2_SEC_C), + * so the same points are explained to every patient. The changes go into a new version 2 (via + * {@link FormVersionCloner}); version 1 keeps the seeded structure, so responses submitted against it + * keep their meaning. + * + * 1. Adds helper text under "Nikshay Poshan Yojana (NPY) eligibility explained" (TB2_C_Q1) and + * "DOTS treatment" (TB2_C_Q2): one {@link ValidationType#HELPER_TEXT_ENGLISH} row per English bullet + * and one {@link ValidationType#HELPER_TEXT_HINDI} row per Hindi bullet, inserted in display order + * so the client shows them in validationId order. Each bullet fits validationParam's 255 chars, so + * no schema change is needed; errorMessage (NOT NULL) carries the same text, as UNIT rows do. + * 2. Renames TB2_C_Q2 from "DOTS free treatment explained" to "DOTS treatment", per the spec. + * 3. Flags both changed questions (TB2_C_Q1, TB2_C_Q2) with requiresBackFill, so the client asks for them + * on beneficiaries whose earlier responses were captured on version 1. + * + * Unchanged: Section C still opens only when "Has the beneficiary agreed for counselling" = Yes, keeps + * its single "Select All", and both checkboxes stay mandatory. + * + * Every step is idempotent (version 2 is reused once created, ensureValidation matches on type + param, + * same-value update), so a re-run is a no-op. + */ +@Component +@RequiredArgsConstructor +public class V012_AddTbCounsellingSchemeHelperText implements FormStructureMigration { + + private static final String FORM_UUID = "TB_COUNSELLING_V2"; + private static final String SECTION_UUID = "TB2_SEC_C"; + private static final String NPY_QUESTION_UUID = "TB2_C_Q1"; + private static final String DOTS_QUESTION_UUID = "TB2_C_Q2"; + private static final String DOTS_NEW_TEXT = "DOTS treatment"; + private static final String DOTS_NEW_TEXT_HINDI = "DOTS उपचार"; + private static final int TARGET_VERSION = 2; + + private static final List NPY_HELPER_ENGLISH = List.of( + "NPY: DBT of Rs. 1000/month for the duration of treatment.", + "Cash transfer (DBT) in the patient's Aadhaar-linked bank account, credited monthly for the full treatment duration.", + "Requirements: Aadhaar-linked bank account and correct patient details in Nikshay.", + "No separate application needed if enrolled in Nikshay."); + + private static final List NPY_HELPER_HINDI = List.of( + "NPY: उपचार की पूरी अवधि के दौरान ₹1000 प्रति माह का DBT।", + "नकद हस्तांतरण (DBT) मरीज़ के आधार से जुड़े बैंक खाते में, पूरी उपचार अवधि तक हर महीने जमा किया जाता है।", + "आवश्यकताएँ: आधार से जुड़ा बैंक खाता और निक्षय में मरीज़ का सही विवरण।", + "निक्षय में नामांकित होने पर अलग से आवेदन की आवश्यकता नहीं है।"); + + private static final List DOTS_HELPER_ENGLISH = List.of( + "A health worker, treatment supporter, or family member watches the patient take each dose, so adherence isn't left to memory or self-report.", + "Provision of free medicine.", + "Confirm the patient understands that treatment is free under NTEP."); + + private static final List DOTS_HELPER_HINDI = List.of( + "स्वास्थ्य कार्यकर्ता, उपचार सहायक या परिवार का कोई सदस्य मरीज़ को हर खुराक लेते हुए देखता है, ताकि दवा लेना याददाश्त या खुद की जानकारी पर निर्भर न रहे।", + "मुफ़्त दवा उपलब्ध कराई जाती है।", + "पुष्टि करें कि मरीज़ समझता है कि NTEP के तहत इलाज मुफ़्त है।"); + + private final SectionQuestionRepo questionRepo; + private final FormVersionCloner versionCloner; + + @Override + public String migrationId() { + return "V012"; + } + + @Override + public void apply(DynamicFormReconciliationService svc) { + FormVersion version = versionCloner.ensureVersion(svc, FORM_UUID, TARGET_VERSION); + FormSection section = svc.section(version, SECTION_UUID) + .orElseThrow(() -> new IllegalStateException("Section " + SECTION_UUID + " not found")); + + SectionQuestion npy = requireQuestion(svc, section, NPY_QUESTION_UUID); + addHelperText(svc, npy, NPY_HELPER_ENGLISH, NPY_HELPER_HINDI); + + SectionQuestion dots = requireQuestion(svc, section, DOTS_QUESTION_UUID); + dots.setQuestionText(DOTS_NEW_TEXT); + dots.setQuestionTextHindi(DOTS_NEW_TEXT_HINDI); + questionRepo.save(dots); + addHelperText(svc, dots, DOTS_HELPER_ENGLISH, DOTS_HELPER_HINDI); + + flagForBackFill(npy); + flagForBackFill(dots); + } + + private void flagForBackFill(SectionQuestion question) { + question.setRequiresBackFill(true); + questionRepo.save(question); + } + + private SectionQuestion requireQuestion(DynamicFormReconciliationService svc, FormSection section, String uuid) { + return svc.question(section, uuid) + .orElseThrow(() -> new IllegalStateException("Question " + uuid + " not found")); + } + + private void addHelperText(DynamicFormReconciliationService svc, SectionQuestion question, + List english, List hindi) { + english.forEach(text -> svc.ensureValidation(question, helperText(ValidationType.HELPER_TEXT_ENGLISH, text))); + hindi.forEach(text -> svc.ensureValidation(question, helperText(ValidationType.HELPER_TEXT_HINDI, text))); + } + + private QuestionValidationDTO helperText(ValidationType type, String text) { + QuestionValidationDTO dto = new QuestionValidationDTO(); + dto.setValidationType(type); + dto.setValidationParam(text); + dto.setErrorMessage(text); + return dto; + } +} diff --git a/src/main/java/com/iemr/flw/service/impl/DynamicFormReconciliationServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/DynamicFormReconciliationServiceImpl.java index 235093d4..94d253db 100644 --- a/src/main/java/com/iemr/flw/service/impl/DynamicFormReconciliationServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/DynamicFormReconciliationServiceImpl.java @@ -169,6 +169,12 @@ public SectionQuestion updateQuestion(Long questionId, SectionQuestionDTO dto) { question.setMaxLength(dto.getMaxLength()); question.setDefaultValue(dto.getDefaultValue()); question.setContainsPii(dto.getContainsPii()); + if (dto.getRequiresBackFill() != null) { + question.setRequiresBackFill(dto.getRequiresBackFill()); + } + if (dto.getEditableAfterSubmit() != null) { + question.setEditableAfterSubmit(dto.getEditableAfterSubmit()); + } if (dto.getDisplayOrder() != null && !dto.getDisplayOrder().equals(question.getDisplayOrder())) { moveQuestion(question, dto.getDisplayOrder()); }