From 341a34f252bc78d28e2fd1dfbb2618d984dc9d56 Mon Sep 17 00:00:00 2001 From: Sehjot Singh Pannu Date: Mon, 22 Jun 2026 12:20:53 +0530 Subject: [PATCH 01/37] Enforced mandatory validation for "Reason for Refusal in section e in file TbCounsellingFormSeeder --- src/main/java/com/iemr/flw/seeder/TbCounsellingFormSeeder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/iemr/flw/seeder/TbCounsellingFormSeeder.java b/src/main/java/com/iemr/flw/seeder/TbCounsellingFormSeeder.java index d3a2c83e..6c0a248c 100644 --- a/src/main/java/com/iemr/flw/seeder/TbCounsellingFormSeeder.java +++ b/src/main/java/com/iemr/flw/seeder/TbCounsellingFormSeeder.java @@ -185,7 +185,7 @@ private FormSectionDTO buildSectionE() { qs.add(statusQ); // Reason for refusal — hidden by default, max 300 chars - qs.add(textQuestion("TB_E_REFUSAL", "Reason for refusal", 2, false, 300, false)); + qs.add(textQuestion("TB_E_REFUSAL", "Reason for refusal", 2, true, 300, false)); // Counsellor remarks — optional, max 500 chars qs.add(textQuestion("TB_E_REMARKS", "Counsellor remarks", 3, false, 500, true)); From 2908c20f8cbe7a0e3d59149a1ca5cc8746dedc08 Mon Sep 17 00:00:00 2001 From: Sehjot Singh Pannu Date: Tue, 23 Jun 2026 08:53:41 +0530 Subject: [PATCH 02/37] Added counselled flag to confirmed cases response, indicating whether a beneficiary has completed the TB Counselling form. Implemented using a single bulk query to avoid N+1 lookups against FormResponse. --- .../DynamicFormResponseController.java | 6 ++- .../flw/domain/iemr/TBConfirmedCaseDTO.java | 1 + .../iemr/flw/repo/iemr/FormResponseRepo.java | 8 ++++ .../service/DynamicFormResponseService.java | 3 +- .../impl/DynamicFormResponseServiceImpl.java | 40 ++++++++++++------- .../service/impl/FormResponseItemSaver.java | 20 +++++----- .../impl/TBConfirmedCaseServiceImpl.java | 33 ++++++++++++--- 7 files changed, 77 insertions(+), 34 deletions(-) diff --git a/src/main/java/com/iemr/flw/controller/DynamicFormResponseController.java b/src/main/java/com/iemr/flw/controller/DynamicFormResponseController.java index 636c6bcf..78a3dd83 100644 --- a/src/main/java/com/iemr/flw/controller/DynamicFormResponseController.java +++ b/src/main/java/com/iemr/flw/controller/DynamicFormResponseController.java @@ -25,6 +25,7 @@ import com.iemr.flw.dto.iemr.FormResponseRequest; import com.iemr.flw.service.DynamicFormResponseService; import com.iemr.flw.utils.ApiResponse; +import com.iemr.flw.utils.exception.IEMRException; import io.swagger.v3.oas.annotations.Operation; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @@ -64,8 +65,9 @@ public ResponseEntity submitForm( @RequestMapping(value = "/complete", method = RequestMethod.POST) public ResponseEntity completeForm( @RequestParam Long responseId, - @Valid @RequestBody FormResponseRequest request) { - FormResponseDTO dto = responseService.completeForm(responseId, request); + @Valid @RequestBody FormResponseRequest request, + @RequestHeader("JwtToken") String jwtToken) throws IEMRException { + FormResponseDTO dto = responseService.completeForm(responseId, request, jwtToken); return ResponseEntity.ok(new ApiResponse(true, "Form completed successfully", dto)); } diff --git a/src/main/java/com/iemr/flw/domain/iemr/TBConfirmedCaseDTO.java b/src/main/java/com/iemr/flw/domain/iemr/TBConfirmedCaseDTO.java index 07903035..7a521177 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/TBConfirmedCaseDTO.java +++ b/src/main/java/com/iemr/flw/domain/iemr/TBConfirmedCaseDTO.java @@ -29,5 +29,6 @@ public class TBConfirmedCaseDTO { private String placeOfDeath; private String reasonForDeath ; private String reasonForNotCompleting; + private Boolean counselled; } diff --git a/src/main/java/com/iemr/flw/repo/iemr/FormResponseRepo.java b/src/main/java/com/iemr/flw/repo/iemr/FormResponseRepo.java index b3c1d22f..830cad08 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/FormResponseRepo.java +++ b/src/main/java/com/iemr/flw/repo/iemr/FormResponseRepo.java @@ -42,6 +42,14 @@ public interface FormResponseRepo extends JpaRepository { List findByBeneficiaryId(Long beneficiaryId); + @Query("SELECT r.beneficiaryId FROM FormResponse r " + + "WHERE r.beneficiaryId IN :benIds " + + "AND r.formId = :formId " + + "AND r.status = :status") + List findCounselledBenIds(@Param("benIds") List benIds, + @Param("formId") Long formId, + @Param("status") String status); + @Query("SELECT r FROM FormResponse r " + "WHERE r.formId IN :formIds " + "AND r.status = 'SUBMITTED' " diff --git a/src/main/java/com/iemr/flw/service/DynamicFormResponseService.java b/src/main/java/com/iemr/flw/service/DynamicFormResponseService.java index 9e92f98e..7a07f23f 100644 --- a/src/main/java/com/iemr/flw/service/DynamicFormResponseService.java +++ b/src/main/java/com/iemr/flw/service/DynamicFormResponseService.java @@ -23,6 +23,7 @@ import com.iemr.flw.dto.iemr.FormResponseDTO; import com.iemr.flw.dto.iemr.FormResponseRequest; +import com.iemr.flw.utils.exception.IEMRException; import java.util.List; @@ -37,7 +38,7 @@ public interface DynamicFormResponseService { FormResponseDTO submitForm(FormResponseRequest request); /** Save POST_SUBMIT section answers and advance status to COMPLETE. */ - FormResponseDTO completeForm(Long responseId, FormResponseRequest request); + FormResponseDTO completeForm(Long responseId, FormResponseRequest request, String jwtToken) throws IEMRException; /** All responses for a beneficiary filtered by form UUID. */ List getResponsesByBeneficiary(Long beneficiaryId, String formUuid); diff --git a/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java index c3649990..4d1df037 100644 --- a/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java @@ -43,6 +43,8 @@ import com.iemr.flw.repo.iemr.SectionResponseRepo; import com.iemr.flw.masterEnum.QuestionType; import com.iemr.flw.service.DynamicFormResponseService; +import com.iemr.flw.utils.JwtUtil; +import com.iemr.flw.utils.exception.IEMRException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -83,6 +85,7 @@ public class DynamicFormResponseServiceImpl implements DynamicFormResponseServic private final SectionQuestionRepo sectionQuestionRepo; private final QuestionOptionRepo questionOptionRepo; private final FormResponseItemSaver itemSaver; + private final JwtUtil jwtUtil; @Override @Transactional @@ -107,18 +110,20 @@ public FormResponseDTO submitForm(FormResponseRequest request) { } else { formResponse = createFormResponse(request, version, STATUS_SUBMITTED, now); } - return processSections(formResponse, request, version); + return processSections(formResponse, request, version, null); } @Override @Transactional - public FormResponseDTO completeForm(Long responseId, FormResponseRequest request) { + public FormResponseDTO completeForm(Long responseId, FormResponseRequest request, String jwtToken) throws IEMRException { + String actor = jwtUtil.extractUserId(jwtToken).toString(); FormResponse formResponse = formResponseRepo.findById(responseId) .orElseThrow(() -> new NoSuchElementException("FormResponse not found: " + responseId)); if (!STATUS_SUBMITTED.equals(formResponse.getStatus())) { throw new IllegalStateException( "Cannot complete a response in status: " + formResponse.getStatus()); } + formResponse.setUpdatedBy(actor); formResponse.setStatus(STATUS_COMPLETE); formResponse.setCompletedAt(new Timestamp(System.currentTimeMillis())); formResponse = formResponseRepo.save(formResponse); @@ -127,7 +132,7 @@ public FormResponseDTO completeForm(Long responseId, FormResponseRequest request FormVersion version = formVersionRepo.findById(formResponse.getVersionId()) .orElseThrow(() -> new NoSuchElementException( "FormVersion not found: " + finalFormResponse.getVersionId())); - return processSections(formResponse, request, version); + return processSections(formResponse, request, version, actor); } @Override @@ -187,7 +192,8 @@ private FormResponse createFormResponse( private FormResponseDTO processSections( FormResponse formResponse, FormResponseRequest request, - FormVersion version) { + FormVersion version, + String actor) { List allSections = formSectionRepo.findByFormVersion_VersionIdOrderByDisplayOrderAsc(version.getVersionId()); @@ -231,7 +237,7 @@ private FormResponseDTO processSections( } SectionResponse sectionResponse = upsertSectionResponse( - formResponse.getResponseId(), section.getSectionId()); + formResponse.getResponseId(), section.getSectionId(), actor); if ("POST_SUBMIT".equals(section.getSectionPhase())) { formResponse.setLastFollowUpAt(sectionResponse.getSavedAt()); @@ -245,7 +251,8 @@ private FormResponseDTO processSections( sectionResponse.getSectionResponseId(), sectionReq.getAnswers(), questionMap, - optionsByQuestion); + optionsByQuestion, + actor); questionResponseRepo.saveAll(questionResponses); @@ -255,13 +262,14 @@ private FormResponseDTO processSections( return buildFormResponseDTO(formResponse, sectionDTOs); } - private SectionResponse upsertSectionResponse(Long responseId, Long sectionId) { + private SectionResponse upsertSectionResponse(Long responseId, Long sectionId, String actor) { Optional existing = sectionResponseRepo.findByResponseIdAndSectionId(responseId, sectionId); if (existing.isPresent()) { SectionResponse sr = existing.get(); sr.setStatus(SECTION_STATUS_DONE); sr.setSavedAt(new Timestamp(System.currentTimeMillis())); + sr.setUpdatedBy(actor); return sectionResponseRepo.save(sr); } SectionResponse sr = SectionResponse.builder() @@ -269,9 +277,8 @@ private SectionResponse upsertSectionResponse(Long responseId, Long sectionId) { .sectionId(sectionId) .status(SECTION_STATUS_DONE) .savedAt(new Timestamp(System.currentTimeMillis())) - // TODO: set audit fields when auth is re-enabled for /dynamicForm/response endpoints - // .createdBy(jwtUtil.extractUsername(authorization)) - // .updatedBy(jwtUtil.extractUsername(authorization)) + .createdBy(actor) + .updatedBy(actor) .build(); return sectionResponseRepo.save(sr); } @@ -280,7 +287,8 @@ private List processAnswers( Long sectionResponseId, List answers, Map questionMap, - Map> optionsByQuestion) { + Map> optionsByQuestion, + String actor) { List results = new ArrayList<>(); @@ -305,10 +313,6 @@ private List processAnswers( // Delete any existing answers for this question in this section (handles re-saves) questionResponseRepo.deleteByQuestionIdAndSectionResponseId(questionId, sectionResponseId); - // TODO: set .createdBy / .updatedBy on all QuestionResponse builders below - // when auth is re-enabled for /dynamicForm/response endpoints: - // .createdBy(jwtUtil.extractUsername(authorization)) - // .updatedBy(jwtUtil.extractUsername(authorization)) if (type == QuestionType.RADIO) { if (answer.getOptionValue() != null) { QuestionOption opt = resolveOption( @@ -317,6 +321,8 @@ private List processAnswers( .sectionResponseId(sectionResponseId) .questionId(questionId) .optionId(opt != null ? opt.getOptionId() : null) + .createdBy(actor) + .updatedBy(actor) .build()); } } else if (type == QuestionType.MCQ) { @@ -328,6 +334,8 @@ private List processAnswers( .sectionResponseId(sectionResponseId) .questionId(questionId) .optionId(opt != null ? opt.getOptionId() : null) + .createdBy(actor) + .updatedBy(actor) .build()); } } @@ -340,6 +348,8 @@ private List processAnswers( .sectionResponseId(sectionResponseId) .questionId(questionId) .answerText(value) + .createdBy(actor) + .updatedBy(actor) .build()); } } diff --git a/src/main/java/com/iemr/flw/service/impl/FormResponseItemSaver.java b/src/main/java/com/iemr/flw/service/impl/FormResponseItemSaver.java index 08cb34ae..0d844eb7 100644 --- a/src/main/java/com/iemr/flw/service/impl/FormResponseItemSaver.java +++ b/src/main/java/com/iemr/flw/service/impl/FormResponseItemSaver.java @@ -108,17 +108,7 @@ public FormResponseDTO saveForBulk(FormResponseRequest req, String jwtToken) thr formResponse = existing.get(0); Long responseId = formResponse.getResponseId(); - // Step 3: Delete old answers — children first, then parents - List oldSections = sectionResponseRepo.findByResponseId(responseId); - if (!oldSections.isEmpty()) { - Set oldSrIds = oldSections.stream() - .map(SectionResponse::getSectionResponseId) - .collect(Collectors.toSet()); - questionResponseRepo.deleteBySectionResponseIdIn(oldSrIds); - sectionResponseRepo.deleteByResponseId(responseId); - } - - // Step 4: Update FormResponse header + // Step 3: Update FormResponse header formResponse.setStatus(STATUS_SUBMITTED); formResponse.setSubmittedAt(now); formResponse.setUpdatedBy(actor); @@ -234,6 +224,14 @@ private FormResponseDTO processSections(FormResponse formResponse, FormResponseR "' not found in form '" + req.getFormUuid() + "'"); } + // Delete existing SectionResponse (and its QuestionResponses) for this section only + sectionResponseRepo.findByResponseIdAndSectionId( + formResponse.getResponseId(), section.getSectionId()) + .ifPresent(sr -> { + questionResponseRepo.deleteBySectionResponseIdIn(Set.of(sr.getSectionResponseId())); + sectionResponseRepo.deleteById(sr.getSectionResponseId()); + }); + SectionResponse sectionResponse = upsertSectionResponse( formResponse.getResponseId(), section.getSectionId(), actor); diff --git a/src/main/java/com/iemr/flw/service/impl/TBConfirmedCaseServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/TBConfirmedCaseServiceImpl.java index 0bf48e62..94784d29 100644 --- a/src/main/java/com/iemr/flw/service/impl/TBConfirmedCaseServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/TBConfirmedCaseServiceImpl.java @@ -2,16 +2,17 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import com.iemr.flw.domain.iemr.DynamicForm; import com.iemr.flw.domain.iemr.TBConfirmedCaseDTO; import com.iemr.flw.domain.iemr.TBConfirmedCase; import com.iemr.flw.domain.iemr.TBStopVisit; -import com.iemr.flw.dto.iemr.TBConfirmedCasesResponseDTO; +import com.iemr.flw.repo.iemr.DynamicFormRepo; +import com.iemr.flw.repo.iemr.FormResponseRepo; import com.iemr.flw.repo.iemr.TBConfirmedTreatmentRepository; +import com.iemr.flw.seeder.TbCounsellingFormSeeder; import com.iemr.flw.service.CampConfigService; import com.iemr.flw.service.TBConfirmedCaseService; import com.iemr.flw.service.TBStopVisitService; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; import com.iemr.flw.utils.JwtUtil; import com.iemr.flw.utils.LocalDateAdapter; import com.iemr.flw.utils.response.OutputResponse; @@ -21,9 +22,12 @@ import org.springframework.stereotype.Service; import java.time.LocalDate; -import java.util.HashMap; +import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; @Service @@ -32,6 +36,8 @@ public class TBConfirmedCaseServiceImpl implements TBConfirmedCaseService { private static final Logger logger = LoggerFactory.getLogger(TBConfirmedCaseServiceImpl.class); private final TBConfirmedTreatmentRepository repository; + private final FormResponseRepo formResponseRepo; + private final DynamicFormRepo dynamicFormRepo; @Autowired private JwtUtil jwtUtil; @@ -41,8 +47,13 @@ public class TBConfirmedCaseServiceImpl implements TBConfirmedCaseService { @Autowired private TBStopVisitService tbStopVisitService; - public TBConfirmedCaseServiceImpl(TBConfirmedTreatmentRepository repository) { + + public TBConfirmedCaseServiceImpl(TBConfirmedTreatmentRepository repository, + FormResponseRepo formResponseRepo, + DynamicFormRepo dynamicFormRepo) { this.repository = repository; + this.formResponseRepo = formResponseRepo; + this.dynamicFormRepo = dynamicFormRepo; } @Override @@ -135,6 +146,18 @@ public String getByUserId(String authorisation) throws Exception { Integer userId = jwtUtil.extractUserId(authorisation); List list = repository.findByUserId(userId); List dtoList = list.stream().map(this::toDTO).collect(Collectors.toList()); + + List benIds = dtoList.stream().map(TBConfirmedCaseDTO::getBenId).collect(Collectors.toList()); + Set counselledBenIds = Collections.emptySet(); + if (!benIds.isEmpty()) { + Optional counsellingForm = dynamicFormRepo.findByFormUuid(TbCounsellingFormSeeder.FORM_UUID); + if (counsellingForm.isPresent()) { + counselledBenIds = new HashSet<>(formResponseRepo.findCounselledBenIds(benIds, counsellingForm.get().getFormId(), "COMPLETE")); + } + } + final Set counselled = counselledBenIds; + dtoList.forEach(dto -> dto.setCounselled(counselled.contains(dto.getBenId()))); + Map response = new java.util.HashMap<>(); response.put("userId", userId); response.put("tbConfirmedCases", dtoList); From 939690554eea878e467dcabc7a7ca66e52d497f4 Mon Sep 17 00:00:00 2001 From: Sehjot Singh Pannu Date: Tue, 23 Jun 2026 11:07:19 +0530 Subject: [PATCH 03/37] Align `/complete` and `/submitBulk` form response editing behavior * Updated `/complete` to resolve `FormResponse` using `beneficiaryId` and `formUuid` from the request body, removing the dependency on `responseId` as a query parameter. * Updated `/submitBulk` section editing to match `/complete` behavior by performing in-place `SectionResponse` upserts and per-question delete-and-reinsert operations, instead of deleting and recreating the entire section. --- .../DynamicFormResponseController.java | 3 +-- .../service/DynamicFormResponseService.java | 2 +- .../impl/DynamicFormResponseServiceImpl.java | 23 +++++++++++-------- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/iemr/flw/controller/DynamicFormResponseController.java b/src/main/java/com/iemr/flw/controller/DynamicFormResponseController.java index 78a3dd83..3e190816 100644 --- a/src/main/java/com/iemr/flw/controller/DynamicFormResponseController.java +++ b/src/main/java/com/iemr/flw/controller/DynamicFormResponseController.java @@ -64,10 +64,9 @@ public ResponseEntity submitForm( @Operation(summary = "Save POST_SUBMIT section answers, status → COMPLETE") @RequestMapping(value = "/complete", method = RequestMethod.POST) public ResponseEntity completeForm( - @RequestParam Long responseId, @Valid @RequestBody FormResponseRequest request, @RequestHeader("JwtToken") String jwtToken) throws IEMRException { - FormResponseDTO dto = responseService.completeForm(responseId, request, jwtToken); + FormResponseDTO dto = responseService.completeForm(request, jwtToken); return ResponseEntity.ok(new ApiResponse(true, "Form completed successfully", dto)); } diff --git a/src/main/java/com/iemr/flw/service/DynamicFormResponseService.java b/src/main/java/com/iemr/flw/service/DynamicFormResponseService.java index 7a07f23f..6b27ade6 100644 --- a/src/main/java/com/iemr/flw/service/DynamicFormResponseService.java +++ b/src/main/java/com/iemr/flw/service/DynamicFormResponseService.java @@ -38,7 +38,7 @@ public interface DynamicFormResponseService { FormResponseDTO submitForm(FormResponseRequest request); /** Save POST_SUBMIT section answers and advance status to COMPLETE. */ - FormResponseDTO completeForm(Long responseId, FormResponseRequest request, String jwtToken) throws IEMRException; + FormResponseDTO completeForm(FormResponseRequest request, String jwtToken) throws IEMRException; /** All responses for a beneficiary filtered by form UUID. */ List getResponsesByBeneficiary(Long beneficiaryId, String formUuid); diff --git a/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java index 4d1df037..b819afc2 100644 --- a/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java @@ -115,23 +115,26 @@ public FormResponseDTO submitForm(FormResponseRequest request) { @Override @Transactional - public FormResponseDTO completeForm(Long responseId, FormResponseRequest request, String jwtToken) throws IEMRException { + public FormResponseDTO completeForm(FormResponseRequest request, String jwtToken) throws IEMRException { String actor = jwtUtil.extractUserId(jwtToken).toString(); - FormResponse formResponse = formResponseRepo.findById(responseId) - .orElseThrow(() -> new NoSuchElementException("FormResponse not found: " + responseId)); - if (!STATUS_SUBMITTED.equals(formResponse.getStatus())) { - throw new IllegalStateException( - "Cannot complete a response in status: " + formResponse.getStatus()); + + FormVersion version = resolveLatestVersion(request.getFormUuid()); + Long formId = version.getDynamicForm().getFormId(); + + List existing = + formResponseRepo.findByBeneficiaryIdAndFormId(request.getBeneficiaryId(), formId); + if (existing.isEmpty()) { + throw new NoSuchElementException( + "FormResponse not found for beneficiaryId=" + request.getBeneficiaryId() + + " formUuid=" + request.getFormUuid()); } + FormResponse formResponse = existing.get(0); + formResponse.setUpdatedBy(actor); formResponse.setStatus(STATUS_COMPLETE); formResponse.setCompletedAt(new Timestamp(System.currentTimeMillis())); formResponse = formResponseRepo.save(formResponse); - FormResponse finalFormResponse = formResponse; - FormVersion version = formVersionRepo.findById(formResponse.getVersionId()) - .orElseThrow(() -> new NoSuchElementException( - "FormVersion not found: " + finalFormResponse.getVersionId())); return processSections(formResponse, request, version, actor); } From 105ede6737c2c560136a42cad6a98a7ea23ecfd4 Mon Sep 17 00:00:00 2001 From: Sehjot Singh Pannu Date: Wed, 24 Jun 2026 14:47:52 +0530 Subject: [PATCH 04/37] Add van/sync tracking fields (vanID, parkingPlaceID, vanSerialNo, processed, syncedDate, syncedBy, syncFailureReason) to domain entities for offline mobile data collection support --- .../com/iemr/flw/domain/iemr/DynamicForm.java | 21 +++++++++++++++++ .../iemr/flw/domain/iemr/FormResponse.java | 21 +++++++++++++++++ .../com/iemr/flw/domain/iemr/FormSection.java | 22 ++++++++++++++++++ .../com/iemr/flw/domain/iemr/FormVersion.java | 21 +++++++++++++++++ .../iemr/flw/domain/iemr/OptionCondition.java | 23 +++++++++++++++++++ .../iemr/flw/domain/iemr/QuestionOption.java | 22 ++++++++++++++++++ .../flw/domain/iemr/QuestionResponse.java | 23 +++++++++++++++++++ .../flw/domain/iemr/QuestionValidation.java | 23 +++++++++++++++++++ .../iemr/flw/domain/iemr/SectionQuestion.java | 22 ++++++++++++++++++ .../iemr/flw/domain/iemr/SectionResponse.java | 21 +++++++++++++++++ 10 files changed, 219 insertions(+) diff --git a/src/main/java/com/iemr/flw/domain/iemr/DynamicForm.java b/src/main/java/com/iemr/flw/domain/iemr/DynamicForm.java index 5ff86faf..9d969b7d 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/DynamicForm.java +++ b/src/main/java/com/iemr/flw/domain/iemr/DynamicForm.java @@ -94,6 +94,27 @@ public class DynamicForm { @ToString.Exclude private List versions = new ArrayList<>(); + @Column(name = "vanID") + private Integer vanID; + + @Column(name = "parkingPlaceID") + private Integer parkingPlaceID; + + @Column(name = "processed") + private String processed = "N"; + + @Column(name = "vanSerialNo") + private Long vanSerialNo; + + @Column(name = "SyncedDate") + private Timestamp syncedDate; + + @Column(name = "Syncedby", length = 50) + private String syncedBy; + + @Column(name = "SyncFailureReason", length = 255) + private String syncFailureReason; + @PrePersist protected void onCreate() { createdAt = new Timestamp(System.currentTimeMillis()); diff --git a/src/main/java/com/iemr/flw/domain/iemr/FormResponse.java b/src/main/java/com/iemr/flw/domain/iemr/FormResponse.java index 7c0a09e0..14e0b891 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/FormResponse.java +++ b/src/main/java/com/iemr/flw/domain/iemr/FormResponse.java @@ -99,6 +99,27 @@ public class FormResponse { @Column(name = "updatedAt", nullable = false) private Timestamp updatedAt; + @Column(name = "vanID") + private Integer vanID; + + @Column(name = "parkingPlaceID") + private Integer parkingPlaceID; + + @Column(name = "processed") + private String processed = "N"; + + @Column(name = "vanSerialNo") + private Long vanSerialNo; + + @Column(name = "SyncedDate") + private Timestamp syncedDate; + + @Column(name = "Syncedby", length = 50) + private String syncedBy; + + @Column(name = "SyncFailureReason", length = 255) + private String syncFailureReason; + @PrePersist protected void onCreate() { createdAt = new Timestamp(System.currentTimeMillis()); diff --git a/src/main/java/com/iemr/flw/domain/iemr/FormSection.java b/src/main/java/com/iemr/flw/domain/iemr/FormSection.java index 97f02763..d7078c06 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/FormSection.java +++ b/src/main/java/com/iemr/flw/domain/iemr/FormSection.java @@ -37,6 +37,7 @@ import lombok.NoArgsConstructor; import lombok.ToString; +import java.sql.Timestamp; import java.util.ArrayList; import java.util.List; @@ -90,4 +91,25 @@ public class FormSection { @OneToMany(mappedBy = "formSection", cascade = CascadeType.ALL, fetch = FetchType.LAZY) @ToString.Exclude private List questions = new ArrayList<>(); + + @Column(name = "vanID") + private Integer vanID; + + @Column(name = "parkingPlaceID") + private Integer parkingPlaceID; + + @Column(name = "processed") + private String processed = "N"; + + @Column(name = "vanSerialNo") + private Long vanSerialNo; + + @Column(name = "SyncedDate") + private Timestamp syncedDate; + + @Column(name = "Syncedby", length = 50) + private String syncedBy; + + @Column(name = "SyncFailureReason", length = 255) + private String syncFailureReason; } diff --git a/src/main/java/com/iemr/flw/domain/iemr/FormVersion.java b/src/main/java/com/iemr/flw/domain/iemr/FormVersion.java index 0e7ec4ae..437a29bc 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/FormVersion.java +++ b/src/main/java/com/iemr/flw/domain/iemr/FormVersion.java @@ -87,6 +87,27 @@ public class FormVersion { @ToString.Exclude private List sections = new ArrayList<>(); + @Column(name = "vanID") + private Integer vanID; + + @Column(name = "parkingPlaceID") + private Integer parkingPlaceID; + + @Column(name = "processed") + private String processed = "N"; + + @Column(name = "vanSerialNo") + private Long vanSerialNo; + + @Column(name = "SyncedDate") + private Timestamp syncedDate; + + @Column(name = "Syncedby", length = 50) + private String syncedBy; + + @Column(name = "SyncFailureReason", length = 255) + private String syncFailureReason; + @PrePersist protected void onCreate() { createdAt = new Timestamp(System.currentTimeMillis()); diff --git a/src/main/java/com/iemr/flw/domain/iemr/OptionCondition.java b/src/main/java/com/iemr/flw/domain/iemr/OptionCondition.java index ae54b9ac..281a87d0 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/OptionCondition.java +++ b/src/main/java/com/iemr/flw/domain/iemr/OptionCondition.java @@ -35,6 +35,8 @@ import lombok.NoArgsConstructor; import lombok.ToString; +import java.sql.Timestamp; + /** * Conditional action triggered when a specific option is selected. * actionType: SHOW_QUESTION | DISABLE_SECTION_VALIDATION | LOCK_FORM @@ -77,4 +79,25 @@ public class OptionCondition { @Column(name = "updated_by", length = 100) private String updatedBy; + + @Column(name = "vanID") + private Integer vanID; + + @Column(name = "parkingPlaceID") + private Integer parkingPlaceID; + + @Column(name = "processed") + private String processed = "N"; + + @Column(name = "vanSerialNo") + private Long vanSerialNo; + + @Column(name = "SyncedDate") + private Timestamp syncedDate; + + @Column(name = "Syncedby", length = 50) + private String syncedBy; + + @Column(name = "SyncFailureReason", length = 255) + private String syncFailureReason; } diff --git a/src/main/java/com/iemr/flw/domain/iemr/QuestionOption.java b/src/main/java/com/iemr/flw/domain/iemr/QuestionOption.java index f0145523..4d6cbe19 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/QuestionOption.java +++ b/src/main/java/com/iemr/flw/domain/iemr/QuestionOption.java @@ -37,6 +37,7 @@ import lombok.NoArgsConstructor; import lombok.ToString; +import java.sql.Timestamp; import java.util.ArrayList; import java.util.List; @@ -78,4 +79,25 @@ public class QuestionOption { @OneToMany(mappedBy = "questionOption", cascade = CascadeType.ALL, fetch = FetchType.LAZY) @ToString.Exclude private List conditions = new ArrayList<>(); + + @Column(name = "vanID") + private Integer vanID; + + @Column(name = "parkingPlaceID") + private Integer parkingPlaceID; + + @Column(name = "processed") + private String processed = "N"; + + @Column(name = "vanSerialNo") + private Long vanSerialNo; + + @Column(name = "SyncedDate") + private Timestamp syncedDate; + + @Column(name = "Syncedby", length = 50) + private String syncedBy; + + @Column(name = "SyncFailureReason", length = 255) + private String syncFailureReason; } diff --git a/src/main/java/com/iemr/flw/domain/iemr/QuestionResponse.java b/src/main/java/com/iemr/flw/domain/iemr/QuestionResponse.java index 23c62ddc..871e31b5 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/QuestionResponse.java +++ b/src/main/java/com/iemr/flw/domain/iemr/QuestionResponse.java @@ -33,6 +33,8 @@ import lombok.Data; import lombok.NoArgsConstructor; +import java.sql.Timestamp; + /** * Stores one answer to a single question within a section response. * MCQ questions produce multiple rows (one per selected option) — no unique constraint on (sectionResponseId, questionId). @@ -76,4 +78,25 @@ public class QuestionResponse { @Column(name = "updated_by", length = 100) private String updatedBy; + + @Column(name = "vanID") + private Integer vanID; + + @Column(name = "parkingPlaceID") + private Integer parkingPlaceID; + + @Column(name = "processed") + private String processed = "N"; + + @Column(name = "vanSerialNo") + private Long vanSerialNo; + + @Column(name = "SyncedDate") + private Timestamp syncedDate; + + @Column(name = "Syncedby", length = 50) + private String syncedBy; + + @Column(name = "SyncFailureReason", length = 255) + private String syncFailureReason; } 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 eb533442..b3253526 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/QuestionValidation.java +++ b/src/main/java/com/iemr/flw/domain/iemr/QuestionValidation.java @@ -38,6 +38,8 @@ import lombok.NoArgsConstructor; import lombok.ToString; +import java.sql.Timestamp; + /** * Server-side validation rule for a question. * validationType: MAX_LENGTH | MIN_DATE | MAX_DATE | REGEX | MANDATORY_IF @@ -75,4 +77,25 @@ public class QuestionValidation { @Column(name = "updated_by", length = 100) private String updatedBy; + + @Column(name = "vanID") + private Integer vanID; + + @Column(name = "parkingPlaceID") + private Integer parkingPlaceID; + + @Column(name = "processed") + private String processed = "N"; + + @Column(name = "vanSerialNo") + private Long vanSerialNo; + + @Column(name = "SyncedDate") + private Timestamp syncedDate; + + @Column(name = "Syncedby", length = 50) + private String syncedBy; + + @Column(name = "SyncFailureReason", length = 255) + private String syncFailureReason; } 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 1e8581e7..0b571ea2 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/SectionQuestion.java +++ b/src/main/java/com/iemr/flw/domain/iemr/SectionQuestion.java @@ -41,6 +41,7 @@ import lombok.NoArgsConstructor; import lombok.ToString; +import java.sql.Timestamp; import java.util.ArrayList; import java.util.List; @@ -107,4 +108,25 @@ public class SectionQuestion { @OneToMany(mappedBy = "sectionQuestion", cascade = CascadeType.ALL, fetch = FetchType.LAZY) @ToString.Exclude private List validations = new ArrayList<>(); + + @Column(name = "vanID") + private Integer vanID; + + @Column(name = "parkingPlaceID") + private Integer parkingPlaceID; + + @Column(name = "processed") + private String processed = "N"; + + @Column(name = "vanSerialNo") + private Long vanSerialNo; + + @Column(name = "SyncedDate") + private Timestamp syncedDate; + + @Column(name = "Syncedby", length = 50) + private String syncedBy; + + @Column(name = "SyncFailureReason", length = 255) + private String syncFailureReason; } diff --git a/src/main/java/com/iemr/flw/domain/iemr/SectionResponse.java b/src/main/java/com/iemr/flw/domain/iemr/SectionResponse.java index 09fbae82..1fded4c8 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/SectionResponse.java +++ b/src/main/java/com/iemr/flw/domain/iemr/SectionResponse.java @@ -71,4 +71,25 @@ public class SectionResponse { @Column(name = "updated_by", length = 100) private String updatedBy; + + @Column(name = "vanID") + private Integer vanID; + + @Column(name = "parkingPlaceID") + private Integer parkingPlaceID; + + @Column(name = "processed") + private String processed = "N"; + + @Column(name = "vanSerialNo") + private Long vanSerialNo; + + @Column(name = "SyncedDate") + private Timestamp syncedDate; + + @Column(name = "Syncedby", length = 50) + private String syncedBy; + + @Column(name = "SyncFailureReason", length = 255) + private String syncFailureReason; } From 21b71877b48b4aa7870cee265e0c24613471111f Mon Sep 17 00:00:00 2001 From: Sehjot Singh Pannu Date: Thu, 25 Jun 2026 14:56:15 +0530 Subject: [PATCH 05/37] feat(dynamic-form): add Hindi localization fields and completed beneficiaries endpoint - Added `sectionName_hindi`, `questionText_hindi`, and `optionValue_hindi` fields to the FormSection, SectionQuestion, and QuestionOption entities to support Hindi localization. - Added `GET /dynamicForm/response/getCompletedBeneficiaries?formType=` endpoint to return beneficiary IDs with `COMPLETE` status for the specified form type. --- .../DynamicFormResponseController.java | 7 ++ .../com/iemr/flw/domain/iemr/FormSection.java | 3 + .../iemr/flw/domain/iemr/QuestionOption.java | 6 ++ .../iemr/flw/domain/iemr/SectionQuestion.java | 3 + .../com/iemr/flw/dto/iemr/FormSectionDTO.java | 2 + .../iemr/flw/dto/iemr/QuestionOptionDTO.java | 4 + .../iemr/flw/dto/iemr/SectionQuestionDTO.java | 2 + .../iemr/flw/repo/iemr/FormResponseRepo.java | 3 +- .../flw/seeder/TbCounsellingFormSeeder.java | 94 ++++++++++--------- .../service/DynamicFormResponseService.java | 4 + .../impl/DynamicFormResponseServiceImpl.java | 13 +++ 11 files changed, 96 insertions(+), 45 deletions(-) diff --git a/src/main/java/com/iemr/flw/controller/DynamicFormResponseController.java b/src/main/java/com/iemr/flw/controller/DynamicFormResponseController.java index 1261dcf2..65dd7036 100644 --- a/src/main/java/com/iemr/flw/controller/DynamicFormResponseController.java +++ b/src/main/java/com/iemr/flw/controller/DynamicFormResponseController.java @@ -23,6 +23,7 @@ import com.iemr.flw.dto.iemr.FormResponseDTO; import com.iemr.flw.dto.iemr.FormResponseRequest; +import com.iemr.flw.masterEnum.FormType; import com.iemr.flw.service.DynamicFormResponseService; import com.iemr.flw.utils.ApiResponse; import com.iemr.flw.utils.exception.IEMRException; @@ -97,4 +98,10 @@ public ResponseEntity getById(@RequestParam Long responseId) { FormResponseDTO dto = responseService.getResponseById(responseId); return ResponseEntity.ok(new ApiResponse(true, "Response fetched successfully", dto)); } + @Operation(summary = "Get beneficiary IDs that have a COMPLETE form response for the given form type") + @RequestMapping(value = "/getCompletedBeneficiaries", method = RequestMethod.GET) + public ResponseEntity getCompletedBeneficiaries(@RequestParam FormType formType) { + List beneficiaryIds = responseService.getCompletedBeneficiaries(formType); + return ResponseEntity.ok(new ApiResponse(true, "Completed beneficiaries fetched successfully", beneficiaryIds)); + } } diff --git a/src/main/java/com/iemr/flw/domain/iemr/FormSection.java b/src/main/java/com/iemr/flw/domain/iemr/FormSection.java index d7078c06..8b505ec8 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/FormSection.java +++ b/src/main/java/com/iemr/flw/domain/iemr/FormSection.java @@ -68,6 +68,9 @@ public class FormSection { @Column(name = "sectionName", nullable = false, length = 255) private String sectionName; + @Column(name = "sectionName_hindi", length = 255) + private String sectionNameHindi; + /** PRE_SUBMIT sections are shown before the form's main submit. POST_SUBMIT after. */ @Column(name = "sectionPhase", nullable = false, length = 20) private String sectionPhase; diff --git a/src/main/java/com/iemr/flw/domain/iemr/QuestionOption.java b/src/main/java/com/iemr/flw/domain/iemr/QuestionOption.java index 4d6cbe19..192af9ac 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/QuestionOption.java +++ b/src/main/java/com/iemr/flw/domain/iemr/QuestionOption.java @@ -64,9 +64,15 @@ public class QuestionOption { @Column(name = "optionLabel", nullable = false, length = 255) private String optionLabel; + @Column(name = "optionLabel_hindi", length = 100) + private String optionLabelHindi; + @Column(name = "optionValue", nullable = false, length = 100) private String optionValue; + @Column(name = "optionValue_hindi", length = 100) + private String optionValueHindi; + @Column(name = "displayOrder", nullable = false) private Integer displayOrder; 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 0b571ea2..5b13d733 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/SectionQuestion.java +++ b/src/main/java/com/iemr/flw/domain/iemr/SectionQuestion.java @@ -73,6 +73,9 @@ public class SectionQuestion { @Column(name = "questionText", nullable = false) private String questionText; + @Column(name = "questionText_hindi", columnDefinition = "TEXT") + private String questionTextHindi; + @Enumerated(EnumType.STRING) @Column(name = "questionType", nullable = false, length = 20) private QuestionType questionType; diff --git a/src/main/java/com/iemr/flw/dto/iemr/FormSectionDTO.java b/src/main/java/com/iemr/flw/dto/iemr/FormSectionDTO.java index 4c537106..2db1f8c7 100644 --- a/src/main/java/com/iemr/flw/dto/iemr/FormSectionDTO.java +++ b/src/main/java/com/iemr/flw/dto/iemr/FormSectionDTO.java @@ -47,6 +47,8 @@ public class FormSectionDTO { @NotBlank(message = "sectionName is required") private String sectionName; + private String sectionNameHindi; + @NotBlank(message = "sectionPhase is required (PRE_SUBMIT or POST_SUBMIT)") private String sectionPhase; diff --git a/src/main/java/com/iemr/flw/dto/iemr/QuestionOptionDTO.java b/src/main/java/com/iemr/flw/dto/iemr/QuestionOptionDTO.java index bd78c6b4..0328263c 100644 --- a/src/main/java/com/iemr/flw/dto/iemr/QuestionOptionDTO.java +++ b/src/main/java/com/iemr/flw/dto/iemr/QuestionOptionDTO.java @@ -44,9 +44,13 @@ public class QuestionOptionDTO { @NotBlank(message = "optionLabel is required") private String optionLabel; + private String optionLabelHindi; + @NotBlank(message = "optionValue is required") private String optionValue; + private String optionValueHindi; + @NotNull(message = "displayOrder is required") private Integer displayOrder; 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 e19bbeb0..11a81430 100644 --- a/src/main/java/com/iemr/flw/dto/iemr/SectionQuestionDTO.java +++ b/src/main/java/com/iemr/flw/dto/iemr/SectionQuestionDTO.java @@ -49,6 +49,8 @@ public class SectionQuestionDTO { @NotBlank(message = "questionText is required") private String questionText; + private String questionTextHindi; + @NotNull(message = "questionType is required") private QuestionType questionType; diff --git a/src/main/java/com/iemr/flw/repo/iemr/FormResponseRepo.java b/src/main/java/com/iemr/flw/repo/iemr/FormResponseRepo.java index 830cad08..02077c9f 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/FormResponseRepo.java +++ b/src/main/java/com/iemr/flw/repo/iemr/FormResponseRepo.java @@ -40,7 +40,8 @@ public interface FormResponseRepo extends JpaRepository { List findByBeneficiaryIdAndFormId(Long beneficiaryId, Long formId); - List findByBeneficiaryId(Long beneficiaryId); + @Query("SELECT r.beneficiaryId FROM FormResponse r WHERE r.formId = :formId AND r.status = :status") + List findBeneficiaryIdsByFormIdAndStatus(@Param("formId") Long formId, @Param("status") String status); @Query("SELECT r.beneficiaryId FROM FormResponse r " + "WHERE r.beneficiaryId IN :benIds " + diff --git a/src/main/java/com/iemr/flw/seeder/TbCounsellingFormSeeder.java b/src/main/java/com/iemr/flw/seeder/TbCounsellingFormSeeder.java index 6c0a248c..edb26b0f 100644 --- a/src/main/java/com/iemr/flw/seeder/TbCounsellingFormSeeder.java +++ b/src/main/java/com/iemr/flw/seeder/TbCounsellingFormSeeder.java @@ -33,6 +33,7 @@ import com.iemr.flw.repo.iemr.DynamicFormRepo; import com.iemr.flw.service.DynamicFormDefinitionService; import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; @@ -47,6 +48,7 @@ * @author Piramal Swasthya */ @Component +@RequiredArgsConstructor public class TbCounsellingFormSeeder { private static final Logger log = LoggerFactory.getLogger(TbCounsellingFormSeeder.class); @@ -56,12 +58,6 @@ public class TbCounsellingFormSeeder { private final DynamicFormDefinitionService formService; private final DynamicFormRepo formRepo; - public TbCounsellingFormSeeder(DynamicFormDefinitionService formService, - DynamicFormRepo formRepo) { - this.formService = formService; - this.formRepo = formRepo; - } - @PostConstruct public void seed() { if (formRepo.findByFormUuid(FORM_UUID).isPresent()) { @@ -97,13 +93,13 @@ private DynamicFormDTO buildFormDto() { // ── Section A: Disease Awareness ───────────────────────────────────────────── private FormSectionDTO buildSectionA() { - FormSectionDTO s = section("TB_SEC_A", "Disease Awareness", "PRE_SUBMIT", 1, true, false); + FormSectionDTO s = section("TB_SEC_A", "Disease Awareness", "बीमारी के बारे में जागरूकता", "PRE_SUBMIT", 1, true, false); List qs = new ArrayList<>(); - qs.add(yesNoRadio("TB_A_Q1", "TB disease explained to patient", 1, true)); - qs.add(yesNoRadio("TB_A_Q2", "Transmission route explained", 2, true)); - qs.add(yesNoRadio("TB_A_Q3", "Symptoms explained", 3, true)); - qs.add(yesNoRadio("TB_A_Q4", "Treatment duration explained", 4, true)); - qs.add(textQuestion("TB_A_REMARKS", "Disease awareness notes", 5, false, 500, true)); + qs.add(yesNoRadio("TB_A_Q1", "TB disease explained to patient", "मरीज़ को टीबी बीमारी के बारे में समझाया गया।", 1, true)); + qs.add(yesNoRadio("TB_A_Q2", "Transmission route explained", "ट्रांसमिशन के तरीके के बारे में जानकारी", 2, true)); + qs.add(yesNoRadio("TB_A_Q3", "Symptoms explained", "लक्षणों की जानकारी", 3, true)); + qs.add(yesNoRadio("TB_A_Q4", "Treatment duration explained", "इलाज की अवधि के बारे में जानकारी", 4, true)); + qs.add(textQuestion("TB_A_REMARKS", "Disease awareness notes", "बीमारी के बारे में जानकारी देने वाले नोट्स", 5, false, 500, true)); s.setQuestions(qs); return s; } @@ -111,15 +107,15 @@ private FormSectionDTO buildSectionA() { // ── Section B: Do's and Don'ts ─────────────────────────────────────────────── private FormSectionDTO buildSectionB() { - FormSectionDTO s = section("TB_SEC_B", "Do's and Don'ts", "PRE_SUBMIT", 2, true, false); + FormSectionDTO s = section("TB_SEC_B", "Do's and Don'ts", "करो और ना करो", "PRE_SUBMIT", 2, true, false); List qs = new ArrayList<>(); - qs.add(yesNoRadio("TB_B_Q1", "Cover mouth while coughing — advised", 1, true)); - qs.add(yesNoRadio("TB_B_Q2", "Complete full treatment course — advised", 2, true)); - qs.add(yesNoRadio("TB_B_Q3", "Regular follow-up attendance — advised", 3, true)); - qs.add(yesNoRadio("TB_B_Q4", "Nutritional guidance provided", 4, true)); - qs.add(yesNoRadio("TB_B_Q5", "No smoking / alcohol — advised", 5, true)); - qs.add(yesNoRadio("TB_B_Q6", "Isolation precautions explained", 6, true)); - qs.add(textQuestion("TB_B_REMARKS", "Do's & Don'ts notes", 7, false, 500, true)); + qs.add(yesNoRadio("TB_B_Q1", "Cover mouth while coughing — advised", "खांसते समय मुंह ढकने की सलाह दी जाती है।", 1, true)); + qs.add(yesNoRadio("TB_B_Q2", "Complete full treatment course — advised", "इलाज का पूरा कोर्स पूरा करने की सलाह दी जाती है।", 2, true)); + qs.add(yesNoRadio("TB_B_Q3", "Regular follow-up attendance — advised", "इलाज का पूरा कोर्स पूरा करने की सलाह दी जाती है।", 3, true)); + qs.add(yesNoRadio("TB_B_Q4", "Nutritional guidance provided", "पोषण संबंधी मार्गदर्शन प्रदान किया गया", 4, true)); + qs.add(yesNoRadio("TB_B_Q5", "No smoking / alcohol — advised", "धूम्रपान / शराब न लेने की सलाह दी जाती है।", 5, true)); + qs.add(yesNoRadio("TB_B_Q6", "Isolation precautions explained", "अलगाव सावधानियों के बारे में बताया गया", 6, true)); + qs.add(textQuestion("TB_B_REMARKS", "Do's & Don'ts notes", "क्या करें और क्या न करें - नोट्स", 7, false, 500, true)); s.setQuestions(qs); return s; } @@ -127,11 +123,11 @@ private FormSectionDTO buildSectionB() { // ── Section C: Government Schemes ──────────────────────────────────────────── private FormSectionDTO buildSectionC() { - FormSectionDTO s = section("TB_SEC_C", "Government Schemes", "PRE_SUBMIT", 3, false, false); + FormSectionDTO s = section("TB_SEC_C", "Government Schemes", "सरकारी योजनाएं", "PRE_SUBMIT", 3, false, false); List qs = new ArrayList<>(); - qs.add(yesNoRadio("TB_C_Q1", "Nikshay Poshan Yojana (NPY) eligibility explained", 1, false)); - qs.add(yesNoRadio("TB_C_Q2", "DOTS free treatment explained", 2, false)); - qs.add(textQuestion("TB_C_REMARKS", "Schemes notes", 3, false, 300, true)); + qs.add(yesNoRadio("TB_C_Q1", "Nikshay Poshan Yojana (NPY) eligibility explained", "निक्षय पोषण योजना (एनपीवाई) पात्रता के बारे में बताया गया", 1, false)); + qs.add(yesNoRadio("TB_C_Q2", "DOTS free treatment explained", "DOTS मुफ़्त इलाज के बारे में जानकारी", 2, false)); + qs.add(textQuestion("TB_C_REMARKS", "Schemes notes", "योजनाओं के नोट्स", 3, false, 300, true)); s.setQuestions(qs); return s; } @@ -139,13 +135,13 @@ private FormSectionDTO buildSectionC() { // ── Section D: Treatment Regimen ───────────────────────────────────────────── private FormSectionDTO buildSectionD() { - FormSectionDTO s = section("TB_SEC_D", "Treatment Regimen", "PRE_SUBMIT", 4, true, false); + FormSectionDTO s = section("TB_SEC_D", "Treatment Regimen", "इलाज का तरीका", "PRE_SUBMIT", 4, true, false); List qs = new ArrayList<>(); - qs.add(yesNoRadio("TB_D_Q1", "Regimen explained to patient", 1, true)); - qs.add(yesNoRadio("TB_D_Q2", "Medication names explained", 2, true)); - qs.add(yesNoRadio("TB_D_Q3", "Side effects explained", 3, true)); - qs.add(yesNoRadio("TB_D_Q4", "Importance of adherence explained", 4, true)); - qs.add(textQuestion("TB_D_REMARKS", "Treatment regimen notes", 5, false, 300, true)); + qs.add(yesNoRadio("TB_D_Q1", "Regimen explained to patient", "मरीज़ को इलाज का तरीका समझाया गया।", 1, true)); + qs.add(yesNoRadio("TB_D_Q2", "Medication names explained", "दवाओं के नामों की जानकारी", 2, true)); + qs.add(yesNoRadio("TB_D_Q3", "Side effects explained", "साइड इफ़ेक्ट्स के बारे में जानकारी", 3, true)); + qs.add(yesNoRadio("TB_D_Q4", "Importance of adherence explained", "निर्देशों का पालन करने का महत्व समझाया गया", 4, true)); + qs.add(textQuestion("TB_D_REMARKS", "Treatment regimen notes", "इलाज के तरीके से जुड़े नोट्स", 5, false, 300, true)); s.setQuestions(qs); return s; } @@ -153,20 +149,21 @@ private FormSectionDTO buildSectionD() { // ── Section E: Counselling Completion ──────────────────────────────────────── private FormSectionDTO buildSectionE() { - FormSectionDTO s = section("TB_SEC_E", "Counselling Completion", "PRE_SUBMIT", 5, true, true); + FormSectionDTO s = section("TB_SEC_E", "Counselling Completion", "काउंसलिंग पूरी होना", "PRE_SUBMIT", 5, true, true); List qs = new ArrayList<>(); // Counselling completion status — RADIO: Complete | Refused SectionQuestionDTO statusQ = new SectionQuestionDTO(); statusQ.setQuestionUuid("TB_E_Q1"); statusQ.setQuestionText("Counselling completion status"); + statusQ.setQuestionTextHindi("काउंसलिंग पूरी होने की स्थिति"); statusQ.setQuestionType(QuestionType.RADIO); statusQ.setIsMandatory(true); statusQ.setDisplayOrder(1); statusQ.setVisibleByDefault(true); // "Complete" option — no conditions - QuestionOptionDTO completeOpt = option("Complete", "COMPLETE", 1, List.of()); + QuestionOptionDTO completeOpt = option("Complete", "सम्पूर्ण","COMPLETE", "सम्पूर्ण", 1, List.of()); // "Refused" option — 5 conditions: disable sections A-D + show refusal text box List refusedConditions = new ArrayList<>(); @@ -175,7 +172,7 @@ private FormSectionDTO buildSectionE() { refusedConditions.add(disableSectionValidation("TB_SEC_C")); refusedConditions.add(disableSectionValidation("TB_SEC_D")); refusedConditions.add(showQuestion("TB_E_REFUSAL")); - QuestionOptionDTO refusedOpt = option("Refused", "REFUSED", 2, refusedConditions); + QuestionOptionDTO refusedOpt = option("Refused", "अस्वीकार करना", "REFUSED", "अस्वीकार करना", 2, refusedConditions); List statusOptions = new ArrayList<>(); statusOptions.add(completeOpt); @@ -185,10 +182,10 @@ private FormSectionDTO buildSectionE() { qs.add(statusQ); // Reason for refusal — hidden by default, max 300 chars - qs.add(textQuestion("TB_E_REFUSAL", "Reason for refusal", 2, true, 300, false)); + qs.add(textQuestion("TB_E_REFUSAL", "Reason for refusal", "इनकार का कारण",2, true, 300, false)); // Counsellor remarks — optional, max 500 chars - qs.add(textQuestion("TB_E_REMARKS", "Counsellor remarks", 3, false, 500, true)); + qs.add(textQuestion("TB_E_REMARKS", "Counsellor remarks", "काउंसलर की टिप्पणी", 3, false, 500, true)); s.setQuestions(qs); return s; @@ -197,23 +194,24 @@ private FormSectionDTO buildSectionE() { // ── Section F: Follow Up to TU ─────────────────────────────────────────────── private FormSectionDTO buildSectionF() { - FormSectionDTO s = section("TB_SEC_F", "Follow Up to TU", "POST_SUBMIT", 6, true, true); + FormSectionDTO s = section("TB_SEC_F", "Follow Up to TU", "TU के बाद की कार्रवाई", "POST_SUBMIT", 6, true, true); List qs = new ArrayList<>(); // "Has the patient started TB treatment?" — RADIO: Yes | No SectionQuestionDTO startedQ = new SectionQuestionDTO(); startedQ.setQuestionUuid("TB_F_Q1"); startedQ.setQuestionText("Has the patient started the prescribed TB treatment regimen?"); + startedQ.setQuestionTextHindi("क्या मरीज़ ने टीबी के लिए बताया गया इलाज शुरू कर दिया है?"); startedQ.setQuestionType(QuestionType.RADIO); startedQ.setIsMandatory(true); startedQ.setDisplayOrder(1); startedQ.setVisibleByDefault(true); - QuestionOptionDTO yesOpt = option("Yes", "YES", 1, List.of()); + QuestionOptionDTO yesOpt = option("Yes", "हाँ", "YES", "हाँ", 1, List.of()); List noConditions = new ArrayList<>(); noConditions.add(showQuestion("TB_F_NO_REASON")); - QuestionOptionDTO noOpt = option("No", "NO", 2, noConditions); + QuestionOptionDTO noOpt = option("No", "नहीं", "NO", "नहीं", 2, noConditions); List startedOptions = new ArrayList<>(); startedOptions.add(yesOpt); @@ -225,16 +223,19 @@ private FormSectionDTO buildSectionF() { // Reason for not starting — hidden by default, max 500 chars qs.add(textQuestion("TB_F_NO_REASON", "Reason for not starting the prescribed TB treatment regimen", + "टीबी के लिए तय इलाज का तरीका शुरू न करने का कारण", 2, false, 500, false)); // DOTS centre visit qs.add(yesNoRadio("TB_F_Q2", "Has the patient visited the DOTS centre / referred health facility for treatment collection?", + "क्या मरीज़ इलाज लेने के लिए DOTS सेंटर या रेफर किए गए स्वास्थ्य केंद्र गया है?", 3, true)); // Side effects reported qs.add(yesNoRadio("TB_F_Q3", "Has the patient reported side effects to the treating doctor or DOTS centre?", + "क्या मरीज़ ने इलाज करने वाले डॉक्टर या DOTS सेंटर को साइड इफ़ेक्ट्स के बारे में बताया है?", 4, true)); s.setQuestions(qs); @@ -243,11 +244,12 @@ private FormSectionDTO buildSectionF() { // ── Builder Helpers ─────────────────────────────────────────────────────────── - private FormSectionDTO section(String uuid, String name, String phase, + private FormSectionDTO section(String uuid, String name, String nameHindi, String phase, int order, boolean required, boolean hasSubmitButton) { FormSectionDTO s = new FormSectionDTO(); s.setSectionUuid(uuid); s.setSectionName(name); + s.setSectionNameHindi(nameHindi); s.setSectionPhase(phase); s.setDisplayOrder(order); s.setIsRequired(required); @@ -256,28 +258,30 @@ private FormSectionDTO section(String uuid, String name, String phase, return s; } - private SectionQuestionDTO yesNoRadio(String uuid, String text, int order, boolean mandatory) { + private SectionQuestionDTO yesNoRadio(String uuid, String text, String textHindi, int order, boolean mandatory) { SectionQuestionDTO q = new SectionQuestionDTO(); q.setQuestionUuid(uuid); q.setQuestionText(text); + q.setQuestionTextHindi(textHindi); q.setQuestionType(QuestionType.RADIO); q.setIsMandatory(mandatory); q.setDisplayOrder(order); q.setVisibleByDefault(true); List opts = new ArrayList<>(); - opts.add(option("Yes", "YES", 1, List.of())); - opts.add(option("No", "NO", 2, List.of())); + opts.add(option("Yes", "हाँ","YES","हाँ", 1, List.of())); + opts.add(option("No", "नहीं","NO", "नहीं", 2, List.of())); q.setOptions(opts); q.setValidations(List.of()); return q; } - private SectionQuestionDTO textQuestion(String uuid, String text, int order, + private SectionQuestionDTO textQuestion(String uuid, String text, String textHindi, int order, boolean mandatory, int maxLength, boolean visible) { SectionQuestionDTO q = new SectionQuestionDTO(); q.setQuestionUuid(uuid); q.setQuestionText(text); + q.setQuestionTextHindi(textHindi); q.setQuestionType(QuestionType.TEXT); q.setIsMandatory(mandatory); q.setDisplayOrder(order); @@ -293,11 +297,13 @@ private SectionQuestionDTO textQuestion(String uuid, String text, int order, return q; } - private QuestionOptionDTO option(String label, String value, int order, + private QuestionOptionDTO option(String label, String labelHindi, String value, String valueHindi, int order, List conditions) { QuestionOptionDTO o = new QuestionOptionDTO(); o.setOptionLabel(label); + o.setOptionLabelHindi(labelHindi); o.setOptionValue(value); + o.setOptionValueHindi(valueHindi); o.setDisplayOrder(order); o.setConditions(conditions); return o; diff --git a/src/main/java/com/iemr/flw/service/DynamicFormResponseService.java b/src/main/java/com/iemr/flw/service/DynamicFormResponseService.java index 6b27ade6..436cae75 100644 --- a/src/main/java/com/iemr/flw/service/DynamicFormResponseService.java +++ b/src/main/java/com/iemr/flw/service/DynamicFormResponseService.java @@ -23,6 +23,7 @@ import com.iemr.flw.dto.iemr.FormResponseDTO; import com.iemr.flw.dto.iemr.FormResponseRequest; +import com.iemr.flw.masterEnum.FormType; import com.iemr.flw.utils.exception.IEMRException; import java.util.List; @@ -59,4 +60,7 @@ public interface DynamicFormResponseService { * Used by the follow-up notification scheduler. */ List findPendingFollowUps(List formIds, int delayDays); + + /** Returns beneficiary IDs with COMPLETE status for the given form type. */ + List getCompletedBeneficiaries(FormType formType); } diff --git a/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java index a4d13079..8c8cc6ea 100644 --- a/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java @@ -34,6 +34,9 @@ import com.iemr.flw.dto.iemr.QuestionResponseDTO; import com.iemr.flw.dto.iemr.SectionAnswerRequest; import com.iemr.flw.dto.iemr.SectionResponseDTO; +import com.iemr.flw.domain.iemr.DynamicForm; +import com.iemr.flw.masterEnum.FormType; +import com.iemr.flw.repo.iemr.DynamicFormRepo; import com.iemr.flw.repo.iemr.FormResponseRepo; import com.iemr.flw.repo.iemr.FormSectionRepo; import com.iemr.flw.repo.iemr.FormVersionRepo; @@ -77,6 +80,7 @@ public class DynamicFormResponseServiceImpl implements DynamicFormResponseServic private static final String STATUS_COMPLETE = "COMPLETE"; private static final String SECTION_STATUS_DONE = "DONE"; + private final DynamicFormRepo dynamicFormRepo; private final FormResponseRepo formResponseRepo; private final SectionResponseRepo sectionResponseRepo; private final QuestionResponseRepo questionResponseRepo; @@ -462,4 +466,13 @@ public List findPendingFollowUps(List formIds, int delayD .map(r -> buildFormResponseDTO(r, List.of())) .collect(Collectors.toList()); } + + @Override + @Transactional(readOnly = true) + public List getCompletedBeneficiaries(FormType formType) { + DynamicForm form = dynamicFormRepo.findByFormTypeAndIsActive(formType, true) + .orElseThrow(() -> new NoSuchElementException( + "No active form found for type: " + formType)); + return formResponseRepo.findBeneficiaryIdsByFormIdAndStatus(form.getFormId(), STATUS_COMPLETE); + } } From fdb1a1f25352b19e05d731cf7d2afd52015b36c6 Mon Sep 17 00:00:00 2001 From: Sehjot Singh Pannu Date: Thu, 25 Jun 2026 14:56:15 +0530 Subject: [PATCH 06/37] feat(dynamic-form): add Hindi localization fields and completed beneficiaries endpoint - Added `sectionName_hindi`, `questionText_hindi`, and `optionValue_hindi` fields to the FormSection, SectionQuestion, and QuestionOption entities to support Hindi localization. - Added `GET /dynamicForm/response/getCompletedBeneficiaries?formType=` endpoint to return beneficiary IDs with `COMPLETE` status for the specified form type. --- src/main/java/com/iemr/flw/domain/iemr/DynamicForm.java | 2 +- src/main/java/com/iemr/flw/domain/iemr/FormResponse.java | 2 +- src/main/java/com/iemr/flw/domain/iemr/FormSection.java | 2 +- src/main/java/com/iemr/flw/domain/iemr/FormVersion.java | 2 +- src/main/java/com/iemr/flw/domain/iemr/QuestionOption.java | 2 +- src/main/java/com/iemr/flw/domain/iemr/QuestionResponse.java | 2 +- src/main/java/com/iemr/flw/domain/iemr/QuestionValidation.java | 2 +- src/main/java/com/iemr/flw/domain/iemr/SectionQuestion.java | 2 +- src/main/java/com/iemr/flw/domain/iemr/SectionResponse.java | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/iemr/flw/domain/iemr/DynamicForm.java b/src/main/java/com/iemr/flw/domain/iemr/DynamicForm.java index 9d969b7d..af183d66 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/DynamicForm.java +++ b/src/main/java/com/iemr/flw/domain/iemr/DynamicForm.java @@ -112,7 +112,7 @@ public class DynamicForm { @Column(name = "Syncedby", length = 50) private String syncedBy; - @Column(name = "SyncFailureReason", length = 255) + @Column(name = "SyncFailureReason") private String syncFailureReason; @PrePersist diff --git a/src/main/java/com/iemr/flw/domain/iemr/FormResponse.java b/src/main/java/com/iemr/flw/domain/iemr/FormResponse.java index 14e0b891..7462a561 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/FormResponse.java +++ b/src/main/java/com/iemr/flw/domain/iemr/FormResponse.java @@ -117,7 +117,7 @@ public class FormResponse { @Column(name = "Syncedby", length = 50) private String syncedBy; - @Column(name = "SyncFailureReason", length = 255) + @Column(name = "SyncFailureReason") private String syncFailureReason; @PrePersist diff --git a/src/main/java/com/iemr/flw/domain/iemr/FormSection.java b/src/main/java/com/iemr/flw/domain/iemr/FormSection.java index 8b505ec8..b8454716 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/FormSection.java +++ b/src/main/java/com/iemr/flw/domain/iemr/FormSection.java @@ -113,6 +113,6 @@ public class FormSection { @Column(name = "Syncedby", length = 50) private String syncedBy; - @Column(name = "SyncFailureReason", length = 255) + @Column(name = "SyncFailureReason") private String syncFailureReason; } diff --git a/src/main/java/com/iemr/flw/domain/iemr/FormVersion.java b/src/main/java/com/iemr/flw/domain/iemr/FormVersion.java index 437a29bc..5902aa21 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/FormVersion.java +++ b/src/main/java/com/iemr/flw/domain/iemr/FormVersion.java @@ -105,7 +105,7 @@ public class FormVersion { @Column(name = "Syncedby", length = 50) private String syncedBy; - @Column(name = "SyncFailureReason", length = 255) + @Column(name = "SyncFailureReason") private String syncFailureReason; @PrePersist diff --git a/src/main/java/com/iemr/flw/domain/iemr/QuestionOption.java b/src/main/java/com/iemr/flw/domain/iemr/QuestionOption.java index 192af9ac..2bc02cbd 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/QuestionOption.java +++ b/src/main/java/com/iemr/flw/domain/iemr/QuestionOption.java @@ -104,6 +104,6 @@ public class QuestionOption { @Column(name = "Syncedby", length = 50) private String syncedBy; - @Column(name = "SyncFailureReason", length = 255) + @Column(name = "SyncFailureReason") private String syncFailureReason; } diff --git a/src/main/java/com/iemr/flw/domain/iemr/QuestionResponse.java b/src/main/java/com/iemr/flw/domain/iemr/QuestionResponse.java index 871e31b5..c01fc715 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/QuestionResponse.java +++ b/src/main/java/com/iemr/flw/domain/iemr/QuestionResponse.java @@ -97,6 +97,6 @@ public class QuestionResponse { @Column(name = "Syncedby", length = 50) private String syncedBy; - @Column(name = "SyncFailureReason", length = 255) + @Column(name = "SyncFailureReason") private String syncFailureReason; } 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 b3253526..b7d41dc1 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/QuestionValidation.java +++ b/src/main/java/com/iemr/flw/domain/iemr/QuestionValidation.java @@ -96,6 +96,6 @@ public class QuestionValidation { @Column(name = "Syncedby", length = 50) private String syncedBy; - @Column(name = "SyncFailureReason", length = 255) + @Column(name = "SyncFailureReason") private String syncFailureReason; } 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 5b13d733..1bd7991b 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/SectionQuestion.java +++ b/src/main/java/com/iemr/flw/domain/iemr/SectionQuestion.java @@ -130,6 +130,6 @@ public class SectionQuestion { @Column(name = "Syncedby", length = 50) private String syncedBy; - @Column(name = "SyncFailureReason", length = 255) + @Column(name = "SyncFailureReason") private String syncFailureReason; } diff --git a/src/main/java/com/iemr/flw/domain/iemr/SectionResponse.java b/src/main/java/com/iemr/flw/domain/iemr/SectionResponse.java index 1fded4c8..36996028 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/SectionResponse.java +++ b/src/main/java/com/iemr/flw/domain/iemr/SectionResponse.java @@ -90,6 +90,6 @@ public class SectionResponse { @Column(name = "Syncedby", length = 50) private String syncedBy; - @Column(name = "SyncFailureReason", length = 255) + @Column(name = "SyncFailureReason") private String syncFailureReason; } From 5f05f6615d1fbec7046e92395dba6083de61f666 Mon Sep 17 00:00:00 2001 From: Sehjot Singh Pannu Date: Fri, 26 Jun 2026 13:20:35 +0530 Subject: [PATCH 07/37] feat(beneficiary): expose GPS coordinates in getBeneficiaryData response MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add gpsLatitude, gpsLongitude, digipin, gpsTimestamp, and isGpsUnavailable fields to RMNCHBeneficiaryDetailsRmnch and RMNCHHouseHoldDetails entities, mapping to the columns written by Identity-API's /syncDataToAmrit endpoint. Enable serializeNulls on the Gson builder in BeneficiaryServiceImpl so GPS keys are always present in householdDetails and beneficiaryDetails — null for web-registered records, real values for mobile-registered ones. Co-Authored-By: Claude Sonnet 4.6 --- .../RMNCHBeneficiaryDetailsRmnch.java | 18 +++++++++++++++++ .../identity/RMNCHHouseHoldDetails.java | 8 ++++++++ .../service/impl/BeneficiaryServiceImpl.java | 20 +++++++++++++++++++ 3 files changed, 46 insertions(+) diff --git a/src/main/java/com/iemr/flw/domain/identity/RMNCHBeneficiaryDetailsRmnch.java b/src/main/java/com/iemr/flw/domain/identity/RMNCHBeneficiaryDetailsRmnch.java index e4b634ff..84827388 100644 --- a/src/main/java/com/iemr/flw/domain/identity/RMNCHBeneficiaryDetailsRmnch.java +++ b/src/main/java/com/iemr/flw/domain/identity/RMNCHBeneficiaryDetailsRmnch.java @@ -102,6 +102,24 @@ public class RMNCHBeneficiaryDetailsRmnch { @Column(name = "longitude") private BigDecimal longitude; + @Column(name = "gpsLatitude") + private Double gpsLatitude; + + @Column(name = "gpsLongitude") + private Double gpsLongitude; + + @Expose + @Column(name = "digipin") + private String digipin; + + @Expose + @Column(name = "gpsTimestamp") + private Timestamp gpsTimestamp; + + @Expose + @Column(name = "isGpsUnavailable", nullable = false, columnDefinition = "TINYINT(1) DEFAULT 0") + private Boolean isGpsUnavailable = false; + @Expose @Column(name = "menstrualBFDId") private Integer menstrualBFDId; diff --git a/src/main/java/com/iemr/flw/domain/identity/RMNCHHouseHoldDetails.java b/src/main/java/com/iemr/flw/domain/identity/RMNCHHouseHoldDetails.java index 3a3ccec8..a0a0adf1 100644 --- a/src/main/java/com/iemr/flw/domain/identity/RMNCHHouseHoldDetails.java +++ b/src/main/java/com/iemr/flw/domain/identity/RMNCHHouseHoldDetails.java @@ -329,4 +329,12 @@ public class RMNCHHouseHoldDetails { @Column(name = "isDeactivate") private Boolean isDeactivate; + @Expose + @Column(name = "gpsLatitude") + private Double gpsLatitude; + + @Expose + @Column(name = "gpsLongitude") + private Double gpsLongitude; + } diff --git a/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java index bbcf59a1..dd1af6f5 100644 --- a/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java @@ -1,5 +1,6 @@ package com.iemr.flw.service.impl; +import java.math.BigDecimal; import java.math.BigInteger; import java.sql.Date; import java.text.SimpleDateFormat; @@ -324,6 +325,25 @@ private String getMappingsForAddressIDs(List addressLi if (benAddressOBJ.getPermAddrLine3() != null) benDetailsRMNCH_OBJ.setAddressLine3(benAddressOBJ.getPermAddrLine3()); + // GPS fallback: if not in RMNCH details (syncdatatoamrti not yet called), + // pull from i_beneficiaryaddress (saved during TM-API registration) + if (benDetailsRMNCH_OBJ.getGpsLatitude() == null && benAddressOBJ.getGpsLatitude() != null) + benDetailsRMNCH_OBJ.setGpsLatitude(benAddressOBJ.getGpsLatitude()); + if (benDetailsRMNCH_OBJ.getGpsLongitude() == null && benAddressOBJ.getGpsLongitude() != null) + benDetailsRMNCH_OBJ.setGpsLongitude(benAddressOBJ.getGpsLongitude()); + if (benDetailsRMNCH_OBJ.getDigipin() == null && benAddressOBJ.getDigipin() != null) + benDetailsRMNCH_OBJ.setDigipin(benAddressOBJ.getDigipin()); + if (benDetailsRMNCH_OBJ.getGpsTimestamp() == null && benAddressOBJ.getGpsTimestamp() != null) + benDetailsRMNCH_OBJ.setGpsTimestamp(benAddressOBJ.getGpsTimestamp()); + if (benDetailsRMNCH_OBJ.getIsGpsUnavailable() == null && benAddressOBJ.getIsGpsUnavailable() != null) + benDetailsRMNCH_OBJ.setIsGpsUnavailable(benAddressOBJ.getIsGpsUnavailable()); + + // Map GPS double fields to the exposed latitude/longitude BigDecimal fields for response + if (benDetailsRMNCH_OBJ.getGpsLatitude() != null) + benDetailsRMNCH_OBJ.setLatitude(BigDecimal.valueOf(benDetailsRMNCH_OBJ.getGpsLatitude())); + if (benDetailsRMNCH_OBJ.getGpsLongitude() != null) + benDetailsRMNCH_OBJ.setLongitude(BigDecimal.valueOf(benDetailsRMNCH_OBJ.getGpsLongitude())); + // ----------------------------------------------------------------------------- // related benids From 6ae9ed0d0e9d0771bf3f8be788893fbaa18cfd27 Mon Sep 17 00:00:00 2001 From: Sehjot Singh Pannu Date: Fri, 26 Jun 2026 13:21:21 +0530 Subject: [PATCH 08/37] =?UTF-8?q?Committed=2082b00ccf=20with=20only=20the?= =?UTF-8?q?=20three=20files:=20=20=20-=20BeneficiaryServiceImpl.java=20?= =?UTF-8?q?=E2=80=94=20serializeNulls()=20+=20GPS=20fallback=20from=20addr?= =?UTF-8?q?ess=20=20=20-=20RMNCHBeneficiaryDetailsRmnch.java=20=E2=80=94?= =?UTF-8?q?=20gpsLatitude,=20gpsLongitude,=20digipin,=20gpsTimestamp,=20is?= =?UTF-8?q?GpsUnavailable=20fields=20=20=20-=20RMNCHHouseHoldDetails.java?= =?UTF-8?q?=20=E2=80=94=20gpsLatitude,=20gpsLongitude=20fields?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/identity/RMNCHMBeneficiaryaddress.java | 15 +++++++++++++++ .../com/iemr/flw/domain/iemr/OptionCondition.java | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/iemr/flw/domain/identity/RMNCHMBeneficiaryaddress.java b/src/main/java/com/iemr/flw/domain/identity/RMNCHMBeneficiaryaddress.java index c9468688..ea58143d 100644 --- a/src/main/java/com/iemr/flw/domain/identity/RMNCHMBeneficiaryaddress.java +++ b/src/main/java/com/iemr/flw/domain/identity/RMNCHMBeneficiaryaddress.java @@ -247,4 +247,19 @@ public class RMNCHMBeneficiaryaddress implements Serializable { @Expose @Column(name = "SyncedDate") private Timestamp SyncedDate; + + @Column(name = "gpsLatitude") + private Double gpsLatitude; + + @Column(name = "gpsLongitude") + private Double gpsLongitude; + + @Column(name = "digipin") + private String digipin; + + @Column(name = "gpsTimestamp") + private Timestamp gpsTimestamp; + + @Column(name = "isGpsUnavailable", nullable = false, columnDefinition = "TINYINT(1) DEFAULT 0") + private Boolean isGpsUnavailable = false; } diff --git a/src/main/java/com/iemr/flw/domain/iemr/OptionCondition.java b/src/main/java/com/iemr/flw/domain/iemr/OptionCondition.java index 281a87d0..50af2b38 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/OptionCondition.java +++ b/src/main/java/com/iemr/flw/domain/iemr/OptionCondition.java @@ -98,6 +98,6 @@ public class OptionCondition { @Column(name = "Syncedby", length = 50) private String syncedBy; - @Column(name = "SyncFailureReason", length = 255) + @Column(name = "SyncFailureReason") private String syncFailureReason; } From 0332e9b9641db358a4100552e09002589d925792 Mon Sep 17 00:00:00 2001 From: Sehjot Singh Pannu Date: Fri, 26 Jun 2026 13:23:03 +0530 Subject: [PATCH 09/37] Revert "Committed 82b00ccf with only the three files:" This reverts commit 3040a08077d82d27dfd86f29f0fbdd4dc094ec56. --- .../domain/identity/RMNCHMBeneficiaryaddress.java | 15 --------------- .../com/iemr/flw/domain/iemr/OptionCondition.java | 2 +- 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/src/main/java/com/iemr/flw/domain/identity/RMNCHMBeneficiaryaddress.java b/src/main/java/com/iemr/flw/domain/identity/RMNCHMBeneficiaryaddress.java index ea58143d..c9468688 100644 --- a/src/main/java/com/iemr/flw/domain/identity/RMNCHMBeneficiaryaddress.java +++ b/src/main/java/com/iemr/flw/domain/identity/RMNCHMBeneficiaryaddress.java @@ -247,19 +247,4 @@ public class RMNCHMBeneficiaryaddress implements Serializable { @Expose @Column(name = "SyncedDate") private Timestamp SyncedDate; - - @Column(name = "gpsLatitude") - private Double gpsLatitude; - - @Column(name = "gpsLongitude") - private Double gpsLongitude; - - @Column(name = "digipin") - private String digipin; - - @Column(name = "gpsTimestamp") - private Timestamp gpsTimestamp; - - @Column(name = "isGpsUnavailable", nullable = false, columnDefinition = "TINYINT(1) DEFAULT 0") - private Boolean isGpsUnavailable = false; } diff --git a/src/main/java/com/iemr/flw/domain/iemr/OptionCondition.java b/src/main/java/com/iemr/flw/domain/iemr/OptionCondition.java index 50af2b38..281a87d0 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/OptionCondition.java +++ b/src/main/java/com/iemr/flw/domain/iemr/OptionCondition.java @@ -98,6 +98,6 @@ public class OptionCondition { @Column(name = "Syncedby", length = 50) private String syncedBy; - @Column(name = "SyncFailureReason") + @Column(name = "SyncFailureReason", length = 255) private String syncFailureReason; } From 412398551a30e444619521c0ffe82475a25d7844 Mon Sep 17 00:00:00 2001 From: Sehjot Singh Pannu Date: Fri, 26 Jun 2026 13:30:30 +0530 Subject: [PATCH 10/37] =?UTF-8?q?Committed=2082b00ccf=20with=20only=20the?= =?UTF-8?q?=20three=20files:=20=20=20-=20BeneficiaryServiceImpl.java=20?= =?UTF-8?q?=E2=80=94=20serializeNulls()=20+=20GPS=20fallback=20from=20addr?= =?UTF-8?q?ess=20=20=20-=20RMNCHBeneficiaryDetailsRmnch.java=20=E2=80=94?= =?UTF-8?q?=20gpsLatitude,=20gpsLongitude,=20digipin,=20gpsTimestamp,=20is?= =?UTF-8?q?GpsUnavailable=20fields=20=20=20-=20RMNCHHouseHoldDetails.java?= =?UTF-8?q?=20=E2=80=94=20gpsLatitude,=20gpsLongitude=20fields?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/identity/RMNCHMBeneficiaryaddress.java | 15 +++++++++++++++ .../com/iemr/flw/domain/iemr/OptionCondition.java | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/iemr/flw/domain/identity/RMNCHMBeneficiaryaddress.java b/src/main/java/com/iemr/flw/domain/identity/RMNCHMBeneficiaryaddress.java index c9468688..ea58143d 100644 --- a/src/main/java/com/iemr/flw/domain/identity/RMNCHMBeneficiaryaddress.java +++ b/src/main/java/com/iemr/flw/domain/identity/RMNCHMBeneficiaryaddress.java @@ -247,4 +247,19 @@ public class RMNCHMBeneficiaryaddress implements Serializable { @Expose @Column(name = "SyncedDate") private Timestamp SyncedDate; + + @Column(name = "gpsLatitude") + private Double gpsLatitude; + + @Column(name = "gpsLongitude") + private Double gpsLongitude; + + @Column(name = "digipin") + private String digipin; + + @Column(name = "gpsTimestamp") + private Timestamp gpsTimestamp; + + @Column(name = "isGpsUnavailable", nullable = false, columnDefinition = "TINYINT(1) DEFAULT 0") + private Boolean isGpsUnavailable = false; } diff --git a/src/main/java/com/iemr/flw/domain/iemr/OptionCondition.java b/src/main/java/com/iemr/flw/domain/iemr/OptionCondition.java index 281a87d0..50af2b38 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/OptionCondition.java +++ b/src/main/java/com/iemr/flw/domain/iemr/OptionCondition.java @@ -98,6 +98,6 @@ public class OptionCondition { @Column(name = "Syncedby", length = 50) private String syncedBy; - @Column(name = "SyncFailureReason", length = 255) + @Column(name = "SyncFailureReason") private String syncFailureReason; } From b7488fec8a87216dd9dc7c9ad0b261048a8c0fee Mon Sep 17 00:00:00 2001 From: Sehjot Singh Pannu Date: Fri, 26 Jun 2026 17:01:29 +0530 Subject: [PATCH 11/37] =?UTF-8?q?Committed=2082b00ccf=20with=20only=20the?= =?UTF-8?q?=20three=20files:=20=20=20-=20BeneficiaryServiceImpl.java=20?= =?UTF-8?q?=E2=80=94=20serializeNulls()=20+=20GPS=20fallback=20from=20addr?= =?UTF-8?q?ess=20=20=20-=20RMNCHBeneficiaryDetailsRmnch.java=20=E2=80=94?= =?UTF-8?q?=20gpsLatitude,=20gpsLongitude,=20digipin,=20gpsTimestamp,=20is?= =?UTF-8?q?GpsUnavailable=20fields=20=20=20-=20RMNCHHouseHoldDetails.java?= =?UTF-8?q?=20=E2=80=94=20gpsLatitude,=20gpsLongitude=20fields?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Conflicts: # src/main/java/com/iemr/flw/domain/identity/RMNCHMBeneficiaryaddress.java --- .../flw/domain/identity/RMNCHBeneficiaryDetailsRmnch.java | 4 ++++ .../iemr/flw/domain/identity/RMNCHMBeneficiaryaddress.java | 3 +++ .../com/iemr/flw/service/impl/BeneficiaryServiceImpl.java | 2 ++ 3 files changed, 9 insertions(+) diff --git a/src/main/java/com/iemr/flw/domain/identity/RMNCHBeneficiaryDetailsRmnch.java b/src/main/java/com/iemr/flw/domain/identity/RMNCHBeneficiaryDetailsRmnch.java index 84827388..dca4f5ee 100644 --- a/src/main/java/com/iemr/flw/domain/identity/RMNCHBeneficiaryDetailsRmnch.java +++ b/src/main/java/com/iemr/flw/domain/identity/RMNCHBeneficiaryDetailsRmnch.java @@ -120,6 +120,10 @@ public class RMNCHBeneficiaryDetailsRmnch { @Column(name = "isGpsUnavailable", nullable = false, columnDefinition = "TINYINT(1) DEFAULT 0") private Boolean isGpsUnavailable = false; + @Expose + @Column(name = "gpsUnavailableReason") + private String gpsUnavailableReason; + @Expose @Column(name = "menstrualBFDId") private Integer menstrualBFDId; diff --git a/src/main/java/com/iemr/flw/domain/identity/RMNCHMBeneficiaryaddress.java b/src/main/java/com/iemr/flw/domain/identity/RMNCHMBeneficiaryaddress.java index ea58143d..a589ec7a 100644 --- a/src/main/java/com/iemr/flw/domain/identity/RMNCHMBeneficiaryaddress.java +++ b/src/main/java/com/iemr/flw/domain/identity/RMNCHMBeneficiaryaddress.java @@ -262,4 +262,7 @@ public class RMNCHMBeneficiaryaddress implements Serializable { @Column(name = "isGpsUnavailable", nullable = false, columnDefinition = "TINYINT(1) DEFAULT 0") private Boolean isGpsUnavailable = false; + + @Column(name = "gpsUnavailableReason") + private String gpsUnavailableReason; } diff --git a/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java index dd1af6f5..bc146ca4 100644 --- a/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java @@ -337,6 +337,8 @@ private String getMappingsForAddressIDs(List addressLi benDetailsRMNCH_OBJ.setGpsTimestamp(benAddressOBJ.getGpsTimestamp()); if (benDetailsRMNCH_OBJ.getIsGpsUnavailable() == null && benAddressOBJ.getIsGpsUnavailable() != null) benDetailsRMNCH_OBJ.setIsGpsUnavailable(benAddressOBJ.getIsGpsUnavailable()); + if (benDetailsRMNCH_OBJ.getGpsUnavailableReason() == null && benAddressOBJ.getGpsUnavailableReason() != null) + benDetailsRMNCH_OBJ.setGpsUnavailableReason(benAddressOBJ.getGpsUnavailableReason()); // Map GPS double fields to the exposed latitude/longitude BigDecimal fields for response if (benDetailsRMNCH_OBJ.getGpsLatitude() != null) From b92cdeaf45f686e0676de0c5b65cb85fe8b4c9ff Mon Sep 17 00:00:00 2001 From: Sehjot Singh Pannu Date: Tue, 30 Jun 2026 11:02:14 +0530 Subject: [PATCH 12/37] Updated isMandatory field in Section C, so changed in TbCounsellingFormSeeder --- .../java/com/iemr/flw/seeder/TbCounsellingFormSeeder.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/iemr/flw/seeder/TbCounsellingFormSeeder.java b/src/main/java/com/iemr/flw/seeder/TbCounsellingFormSeeder.java index edb26b0f..0e956c9c 100644 --- a/src/main/java/com/iemr/flw/seeder/TbCounsellingFormSeeder.java +++ b/src/main/java/com/iemr/flw/seeder/TbCounsellingFormSeeder.java @@ -125,8 +125,8 @@ private FormSectionDTO buildSectionB() { private FormSectionDTO buildSectionC() { FormSectionDTO s = section("TB_SEC_C", "Government Schemes", "सरकारी योजनाएं", "PRE_SUBMIT", 3, false, false); List qs = new ArrayList<>(); - qs.add(yesNoRadio("TB_C_Q1", "Nikshay Poshan Yojana (NPY) eligibility explained", "निक्षय पोषण योजना (एनपीवाई) पात्रता के बारे में बताया गया", 1, false)); - qs.add(yesNoRadio("TB_C_Q2", "DOTS free treatment explained", "DOTS मुफ़्त इलाज के बारे में जानकारी", 2, false)); + qs.add(yesNoRadio("TB_C_Q1", "Nikshay Poshan Yojana (NPY) eligibility explained", "निक्षय पोषण योजना (एनपीवाई) पात्रता के बारे में बताया गया", 1, true)); + qs.add(yesNoRadio("TB_C_Q2", "DOTS free treatment explained", "DOTS मुफ़्त इलाज के बारे में जानकारी", 2, true)); qs.add(textQuestion("TB_C_REMARKS", "Schemes notes", "योजनाओं के नोट्स", 3, false, 300, true)); s.setQuestions(qs); return s; From 81e29d21b6ed3593c23dc4826a3c23ca8c6dd081 Mon Sep 17 00:00:00 2001 From: Sehjot Singh Pannu Date: Thu, 2 Jul 2026 19:16:41 +0530 Subject: [PATCH 13/37] Add optional villageId and providerServiceMapId filters to getCompletedBeneficiaries --- .../flw/controller/DynamicFormResponseController.java | 9 ++++++--- .../java/com/iemr/flw/repo/iemr/FormResponseRepo.java | 9 +++++++++ .../com/iemr/flw/service/DynamicFormResponseService.java | 4 ++-- .../flw/service/impl/DynamicFormResponseServiceImpl.java | 6 ++++-- 4 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/iemr/flw/controller/DynamicFormResponseController.java b/src/main/java/com/iemr/flw/controller/DynamicFormResponseController.java index 65dd7036..49d8e547 100644 --- a/src/main/java/com/iemr/flw/controller/DynamicFormResponseController.java +++ b/src/main/java/com/iemr/flw/controller/DynamicFormResponseController.java @@ -98,10 +98,13 @@ public ResponseEntity getById(@RequestParam Long responseId) { FormResponseDTO dto = responseService.getResponseById(responseId); return ResponseEntity.ok(new ApiResponse(true, "Response fetched successfully", dto)); } - @Operation(summary = "Get beneficiary IDs that have a COMPLETE form response for the given form type") + @Operation(summary = "Get beneficiary IDs that have a COMPLETE form response for the given form type, optionally filtered by village and/or provider service map") @RequestMapping(value = "/getCompletedBeneficiaries", method = RequestMethod.GET) - public ResponseEntity getCompletedBeneficiaries(@RequestParam FormType formType) { - List beneficiaryIds = responseService.getCompletedBeneficiaries(formType); + public ResponseEntity getCompletedBeneficiaries( + @RequestParam FormType formType, + @RequestParam(required = false) Integer villageId, + @RequestParam(required = false) Integer providerServiceMapId) { + List beneficiaryIds = responseService.getCompletedBeneficiaries(formType, villageId, providerServiceMapId); return ResponseEntity.ok(new ApiResponse(true, "Completed beneficiaries fetched successfully", beneficiaryIds)); } } diff --git a/src/main/java/com/iemr/flw/repo/iemr/FormResponseRepo.java b/src/main/java/com/iemr/flw/repo/iemr/FormResponseRepo.java index 02077c9f..66672c16 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/FormResponseRepo.java +++ b/src/main/java/com/iemr/flw/repo/iemr/FormResponseRepo.java @@ -43,6 +43,15 @@ public interface FormResponseRepo extends JpaRepository { @Query("SELECT r.beneficiaryId FROM FormResponse r WHERE r.formId = :formId AND r.status = :status") List findBeneficiaryIdsByFormIdAndStatus(@Param("formId") Long formId, @Param("status") String status); + @Query("SELECT r.beneficiaryId FROM FormResponse r WHERE r.formId = :formId AND r.status = :status " + + "AND r.beneficiaryId IN (SELECT b.beneficiaryID FROM BenFlowStatus b WHERE b.deleted = false " + + "AND (:villageId IS NULL OR b.villageID = :villageId) " + + "AND (:providerServiceMapId IS NULL OR b.providerServiceMapId = :providerServiceMapId))") + List findBeneficiaryIdsByFormIdAndStatusFiltered(@Param("formId") Long formId, + @Param("status") String status, + @Param("villageId") Integer villageId, + @Param("providerServiceMapId") Integer providerServiceMapId); + @Query("SELECT r.beneficiaryId FROM FormResponse r " + "WHERE r.beneficiaryId IN :benIds " + "AND r.formId = :formId " + diff --git a/src/main/java/com/iemr/flw/service/DynamicFormResponseService.java b/src/main/java/com/iemr/flw/service/DynamicFormResponseService.java index 436cae75..cca29e2e 100644 --- a/src/main/java/com/iemr/flw/service/DynamicFormResponseService.java +++ b/src/main/java/com/iemr/flw/service/DynamicFormResponseService.java @@ -61,6 +61,6 @@ public interface DynamicFormResponseService { */ List findPendingFollowUps(List formIds, int delayDays); - /** Returns beneficiary IDs with COMPLETE status for the given form type. */ - List getCompletedBeneficiaries(FormType formType); + /** Returns beneficiary IDs with COMPLETE status for the given form type, optionally filtered by village and/or provider service map. */ + List getCompletedBeneficiaries(FormType formType, Integer villageId, Integer providerServiceMapId); } diff --git a/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java index 8c8cc6ea..4553cc06 100644 --- a/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java @@ -469,10 +469,12 @@ public List findPendingFollowUps(List formIds, int delayD @Override @Transactional(readOnly = true) - public List getCompletedBeneficiaries(FormType formType) { + public List getCompletedBeneficiaries(FormType formType, Integer villageId, Integer providerServiceMapId) { DynamicForm form = dynamicFormRepo.findByFormTypeAndIsActive(formType, true) .orElseThrow(() -> new NoSuchElementException( "No active form found for type: " + formType)); - return formResponseRepo.findBeneficiaryIdsByFormIdAndStatus(form.getFormId(), STATUS_COMPLETE); + return (villageId != null || providerServiceMapId != null) + ? formResponseRepo.findBeneficiaryIdsByFormIdAndStatusFiltered(form.getFormId(), STATUS_COMPLETE, villageId, providerServiceMapId) + : formResponseRepo.findBeneficiaryIdsByFormIdAndStatus(form.getFormId(), STATUS_COMPLETE); } } From be0d0d69392f3f956b5965ee105c8778a2ea1038 Mon Sep 17 00:00:00 2001 From: Sehjot Singh Pannu Date: Thu, 2 Jul 2026 19:36:53 +0530 Subject: [PATCH 14/37] Removing section f from seeder --- src/main/java/com/iemr/flw/seeder/TbCounsellingFormSeeder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/iemr/flw/seeder/TbCounsellingFormSeeder.java b/src/main/java/com/iemr/flw/seeder/TbCounsellingFormSeeder.java index 0e956c9c..4fdd3a2d 100644 --- a/src/main/java/com/iemr/flw/seeder/TbCounsellingFormSeeder.java +++ b/src/main/java/com/iemr/flw/seeder/TbCounsellingFormSeeder.java @@ -85,7 +85,7 @@ private DynamicFormDTO buildFormDto() { sections.add(buildSectionC()); sections.add(buildSectionD()); sections.add(buildSectionE()); - sections.add(buildSectionF()); +// sections.add(buildSectionF()); dto.setSections(sections); return dto; } From f6aca3a8c92d18a7b392c62fd00cb7413b119906 Mon Sep 17 00:00:00 2001 From: Sehjot Singh Pannu Date: Thu, 2 Jul 2026 20:50:21 +0530 Subject: [PATCH 15/37] Removing already existing check in /complete api --- .../service/impl/DynamicFormResponseServiceImpl.java | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java index 4553cc06..d1673a0e 100644 --- a/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java @@ -124,19 +124,17 @@ public FormResponseDTO completeForm(FormResponseRequest request, String jwtToken FormVersion version = resolveLatestVersion(request.getFormUuid()); Long formId = version.getDynamicForm().getFormId(); + Timestamp now = new Timestamp(System.currentTimeMillis()); List existing = formResponseRepo.findByBeneficiaryIdAndFormId(request.getBeneficiaryId(), formId); - if (existing.isEmpty()) { - throw new NoSuchElementException( - "FormResponse not found for beneficiaryId=" + request.getBeneficiaryId() - + " formUuid=" + request.getFormUuid()); - } - FormResponse formResponse = existing.get(0); + FormResponse formResponse = existing.isEmpty() + ? createFormResponse(request, version, STATUS_SUBMITTED, now) + : existing.get(0); formResponse.setUpdatedBy(actor); formResponse.setStatus(STATUS_COMPLETE); - formResponse.setCompletedAt(new Timestamp(System.currentTimeMillis())); + formResponse.setCompletedAt(now); formResponse = formResponseRepo.save(formResponse); return processSections(formResponse, request, version, actor); } From 41550c28f51812789b1e6dbe1f174ed4be491f06 Mon Sep 17 00:00:00 2001 From: Sehjot Singh Pannu Date: Sun, 5 Jul 2026 22:44:09 +0530 Subject: [PATCH 16/37] Add a CHECKBOX question type and a TB_COUNSELLING_V2 seeder with a GENERAL_INFO consent gate ahead of the counselling sections. --- .../DynamicFormResponseController.java | 2 +- .../com/iemr/flw/domain/iemr/FormSection.java | 11 +- .../iemr/flw/domain/iemr/SectionQuestion.java | 2 +- .../com/iemr/flw/dto/iemr/FormSectionDTO.java | 7 +- .../iemr/flw/dto/iemr/SectionResponseDTO.java | 2 + .../com/iemr/flw/masterEnum/FormType.java | 3 +- .../com/iemr/flw/masterEnum/QuestionType.java | 3 + .../com/iemr/flw/masterEnum/SectionPhase.java | 37 +++ .../flw/seeder/TbCounsellingFormSeeder.java | 15 +- .../flw/seeder/TbCounsellingV2FormSeeder.java | 292 ++++++++++++++++++ .../service/DynamicFormResponseService.java | 2 +- .../impl/DynamicFormResponseServiceImpl.java | 41 ++- .../service/impl/FormResponseItemSaver.java | 30 +- 13 files changed, 410 insertions(+), 37 deletions(-) create mode 100644 src/main/java/com/iemr/flw/masterEnum/SectionPhase.java create mode 100644 src/main/java/com/iemr/flw/seeder/TbCounsellingV2FormSeeder.java diff --git a/src/main/java/com/iemr/flw/controller/DynamicFormResponseController.java b/src/main/java/com/iemr/flw/controller/DynamicFormResponseController.java index 49d8e547..99ebb9f7 100644 --- a/src/main/java/com/iemr/flw/controller/DynamicFormResponseController.java +++ b/src/main/java/com/iemr/flw/controller/DynamicFormResponseController.java @@ -53,7 +53,7 @@ public class DynamicFormResponseController { private final DynamicFormResponseService responseService; - @Operation(summary = "Submit PRE_SUBMIT section answers, status → SUBMITTED. " + + @Operation(summary = "Submit GENERAL_INFO and PRE_SUBMIT section answers, status → SUBMITTED. " + "Pass responseId in body to re-submit an existing SUBMITTED response.") @RequestMapping(value = "/submit", method = RequestMethod.POST) public ResponseEntity submitForm( diff --git a/src/main/java/com/iemr/flw/domain/iemr/FormSection.java b/src/main/java/com/iemr/flw/domain/iemr/FormSection.java index b8454716..28f50859 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/FormSection.java +++ b/src/main/java/com/iemr/flw/domain/iemr/FormSection.java @@ -21,9 +21,12 @@ */ package com.iemr.flw.domain.iemr; +import com.iemr.flw.masterEnum.SectionPhase; import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; @@ -71,9 +74,9 @@ public class FormSection { @Column(name = "sectionName_hindi", length = 255) private String sectionNameHindi; - /** PRE_SUBMIT sections are shown before the form's main submit. POST_SUBMIT after. */ + @Enumerated(EnumType.STRING) @Column(name = "sectionPhase", nullable = false, length = 20) - private String sectionPhase; + private SectionPhase sectionPhase; @Column(name = "isRequired", nullable = false) private Boolean isRequired = true; @@ -81,10 +84,12 @@ public class FormSection { @Column(name = "displayOrder", nullable = false) private Integer displayOrder; - /** True only on the last PRE_SUBMIT section — renders the Submit button. */ @Column(name = "hasSubmitButton", nullable = false) private Boolean hasSubmitButton = false; + @Column(name = "isEditable", nullable = false) + private Boolean isEditable = false; + @Column(name = "created_by", length = 100) private String createdBy; 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 1bd7991b..91cc3c43 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/SectionQuestion.java +++ b/src/main/java/com/iemr/flw/domain/iemr/SectionQuestion.java @@ -47,7 +47,7 @@ /** * A question within a form section. - * questionType: RADIO | MCQ | TEXT | DATE | DISPLAY | AUTO_FILL + * questionType: RADIO | MCQ | CHECKBOX | TEXT | DATE | DISPLAY | AUTO_FILL */ @Entity @Table(name = "t_section_question", schema = "db_iemr") diff --git a/src/main/java/com/iemr/flw/dto/iemr/FormSectionDTO.java b/src/main/java/com/iemr/flw/dto/iemr/FormSectionDTO.java index 2db1f8c7..6bb10795 100644 --- a/src/main/java/com/iemr/flw/dto/iemr/FormSectionDTO.java +++ b/src/main/java/com/iemr/flw/dto/iemr/FormSectionDTO.java @@ -21,6 +21,7 @@ */ package com.iemr.flw.dto.iemr; +import com.iemr.flw.masterEnum.SectionPhase; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; @@ -49,8 +50,8 @@ public class FormSectionDTO { private String sectionNameHindi; - @NotBlank(message = "sectionPhase is required (PRE_SUBMIT or POST_SUBMIT)") - private String sectionPhase; + @NotNull(message = "sectionPhase is required (GENERAL_INFO, PRE_SUBMIT, or POST_SUBMIT)") + private SectionPhase sectionPhase; private Boolean isRequired = true; @@ -59,6 +60,8 @@ public class FormSectionDTO { private Boolean hasSubmitButton = false; + private Boolean isEditable = false; + @Valid private List questions = new ArrayList<>(); } diff --git a/src/main/java/com/iemr/flw/dto/iemr/SectionResponseDTO.java b/src/main/java/com/iemr/flw/dto/iemr/SectionResponseDTO.java index dac289a7..78c918fe 100644 --- a/src/main/java/com/iemr/flw/dto/iemr/SectionResponseDTO.java +++ b/src/main/java/com/iemr/flw/dto/iemr/SectionResponseDTO.java @@ -42,6 +42,8 @@ public class SectionResponseDTO { private Long sectionResponseId; private Long sectionId; + private String sectionUuid; + private Boolean isEditable; private String status; private Timestamp savedAt; private List answers; diff --git a/src/main/java/com/iemr/flw/masterEnum/FormType.java b/src/main/java/com/iemr/flw/masterEnum/FormType.java index 779fce19..ccb0cfe2 100644 --- a/src/main/java/com/iemr/flw/masterEnum/FormType.java +++ b/src/main/java/com/iemr/flw/masterEnum/FormType.java @@ -30,5 +30,6 @@ public enum FormType { COUNSELLING_MODULE, SCREENING, COUNSELLING, - TB_COUNSELLING + TB_COUNSELLING, + TB_COUNSELLING_V2 } diff --git a/src/main/java/com/iemr/flw/masterEnum/QuestionType.java b/src/main/java/com/iemr/flw/masterEnum/QuestionType.java index ae4b91e1..0af07aa1 100644 --- a/src/main/java/com/iemr/flw/masterEnum/QuestionType.java +++ b/src/main/java/com/iemr/flw/masterEnum/QuestionType.java @@ -32,6 +32,9 @@ public enum QuestionType { RADIO, /** Multi-select from a predefined list of options. */ MCQ, + /** Single boolean checkbox — checked or unchecked. May carry options for display + * purposes; answers are still stored as free text. */ + CHECKBOX, /** Free-text input. */ TEXT, /** Date picker input. */ diff --git a/src/main/java/com/iemr/flw/masterEnum/SectionPhase.java b/src/main/java/com/iemr/flw/masterEnum/SectionPhase.java new file mode 100644 index 00000000..13faa60d --- /dev/null +++ b/src/main/java/com/iemr/flw/masterEnum/SectionPhase.java @@ -0,0 +1,37 @@ +/* + * 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.masterEnum; + +/** + * Exhaustive set of phases a form section can belong to. + * No other values may be stored in t_form_section.sectionPhase. + * + * @author Piramal Swasthya + */ +public enum SectionPhase { + /** Shown first, before PRE_SUBMIT sections. */ + GENERAL_INFO, + /** Shown before the form's main submit. Saved via /submit. */ + PRE_SUBMIT, + /** Shown after the main submit. Saved via /complete. */ + POST_SUBMIT +} \ No newline at end of file diff --git a/src/main/java/com/iemr/flw/seeder/TbCounsellingFormSeeder.java b/src/main/java/com/iemr/flw/seeder/TbCounsellingFormSeeder.java index 4fdd3a2d..ecddba20 100644 --- a/src/main/java/com/iemr/flw/seeder/TbCounsellingFormSeeder.java +++ b/src/main/java/com/iemr/flw/seeder/TbCounsellingFormSeeder.java @@ -29,6 +29,7 @@ import com.iemr.flw.dto.iemr.SectionQuestionDTO; import com.iemr.flw.masterEnum.FormType; import com.iemr.flw.masterEnum.QuestionType; +import com.iemr.flw.masterEnum.SectionPhase; import com.iemr.flw.masterEnum.ValidationType; import com.iemr.flw.repo.iemr.DynamicFormRepo; import com.iemr.flw.service.DynamicFormDefinitionService; @@ -93,7 +94,7 @@ private DynamicFormDTO buildFormDto() { // ── Section A: Disease Awareness ───────────────────────────────────────────── private FormSectionDTO buildSectionA() { - FormSectionDTO s = section("TB_SEC_A", "Disease Awareness", "बीमारी के बारे में जागरूकता", "PRE_SUBMIT", 1, true, false); + FormSectionDTO s = section("TB_SEC_A", "Disease Awareness", "बीमारी के बारे में जागरूकता", SectionPhase.PRE_SUBMIT, 1, true, false); List qs = new ArrayList<>(); qs.add(yesNoRadio("TB_A_Q1", "TB disease explained to patient", "मरीज़ को टीबी बीमारी के बारे में समझाया गया।", 1, true)); qs.add(yesNoRadio("TB_A_Q2", "Transmission route explained", "ट्रांसमिशन के तरीके के बारे में जानकारी", 2, true)); @@ -107,7 +108,7 @@ private FormSectionDTO buildSectionA() { // ── Section B: Do's and Don'ts ─────────────────────────────────────────────── private FormSectionDTO buildSectionB() { - FormSectionDTO s = section("TB_SEC_B", "Do's and Don'ts", "करो और ना करो", "PRE_SUBMIT", 2, true, false); + FormSectionDTO s = section("TB_SEC_B", "Do's and Don'ts", "करो और ना करो", SectionPhase.PRE_SUBMIT, 2, true, false); List qs = new ArrayList<>(); qs.add(yesNoRadio("TB_B_Q1", "Cover mouth while coughing — advised", "खांसते समय मुंह ढकने की सलाह दी जाती है।", 1, true)); qs.add(yesNoRadio("TB_B_Q2", "Complete full treatment course — advised", "इलाज का पूरा कोर्स पूरा करने की सलाह दी जाती है।", 2, true)); @@ -123,7 +124,7 @@ private FormSectionDTO buildSectionB() { // ── Section C: Government Schemes ──────────────────────────────────────────── private FormSectionDTO buildSectionC() { - FormSectionDTO s = section("TB_SEC_C", "Government Schemes", "सरकारी योजनाएं", "PRE_SUBMIT", 3, false, false); + FormSectionDTO s = section("TB_SEC_C", "Government Schemes", "सरकारी योजनाएं", SectionPhase.PRE_SUBMIT, 3, false, false); List qs = new ArrayList<>(); qs.add(yesNoRadio("TB_C_Q1", "Nikshay Poshan Yojana (NPY) eligibility explained", "निक्षय पोषण योजना (एनपीवाई) पात्रता के बारे में बताया गया", 1, true)); qs.add(yesNoRadio("TB_C_Q2", "DOTS free treatment explained", "DOTS मुफ़्त इलाज के बारे में जानकारी", 2, true)); @@ -135,7 +136,7 @@ private FormSectionDTO buildSectionC() { // ── Section D: Treatment Regimen ───────────────────────────────────────────── private FormSectionDTO buildSectionD() { - FormSectionDTO s = section("TB_SEC_D", "Treatment Regimen", "इलाज का तरीका", "PRE_SUBMIT", 4, true, false); + FormSectionDTO s = section("TB_SEC_D", "Treatment Regimen", "इलाज का तरीका", SectionPhase.PRE_SUBMIT, 4, true, false); List qs = new ArrayList<>(); qs.add(yesNoRadio("TB_D_Q1", "Regimen explained to patient", "मरीज़ को इलाज का तरीका समझाया गया।", 1, true)); qs.add(yesNoRadio("TB_D_Q2", "Medication names explained", "दवाओं के नामों की जानकारी", 2, true)); @@ -149,7 +150,7 @@ private FormSectionDTO buildSectionD() { // ── Section E: Counselling Completion ──────────────────────────────────────── private FormSectionDTO buildSectionE() { - FormSectionDTO s = section("TB_SEC_E", "Counselling Completion", "काउंसलिंग पूरी होना", "PRE_SUBMIT", 5, true, true); + FormSectionDTO s = section("TB_SEC_E", "Counselling Completion", "काउंसलिंग पूरी होना", SectionPhase.PRE_SUBMIT, 5, true, true); List qs = new ArrayList<>(); // Counselling completion status — RADIO: Complete | Refused @@ -194,7 +195,7 @@ private FormSectionDTO buildSectionE() { // ── Section F: Follow Up to TU ─────────────────────────────────────────────── private FormSectionDTO buildSectionF() { - FormSectionDTO s = section("TB_SEC_F", "Follow Up to TU", "TU के बाद की कार्रवाई", "POST_SUBMIT", 6, true, true); + FormSectionDTO s = section("TB_SEC_F", "Follow Up to TU", "TU के बाद की कार्रवाई", SectionPhase.POST_SUBMIT, 6, true, true); List qs = new ArrayList<>(); // "Has the patient started TB treatment?" — RADIO: Yes | No @@ -244,7 +245,7 @@ private FormSectionDTO buildSectionF() { // ── Builder Helpers ─────────────────────────────────────────────────────────── - private FormSectionDTO section(String uuid, String name, String nameHindi, String phase, + private FormSectionDTO section(String uuid, String name, String nameHindi, SectionPhase phase, int order, boolean required, boolean hasSubmitButton) { FormSectionDTO s = new FormSectionDTO(); s.setSectionUuid(uuid); diff --git a/src/main/java/com/iemr/flw/seeder/TbCounsellingV2FormSeeder.java b/src/main/java/com/iemr/flw/seeder/TbCounsellingV2FormSeeder.java new file mode 100644 index 00000000..5780eb76 --- /dev/null +++ b/src/main/java/com/iemr/flw/seeder/TbCounsellingV2FormSeeder.java @@ -0,0 +1,292 @@ +/* + * 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; + +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.masterEnum.FormType; +import com.iemr.flw.masterEnum.QuestionType; +import com.iemr.flw.masterEnum.SectionPhase; +import com.iemr.flw.masterEnum.ValidationType; +import com.iemr.flw.repo.iemr.DynamicFormRepo; +import com.iemr.flw.service.DynamicFormDefinitionService; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; + +/** + * Seeds the TB Counselling (v2) form definition on application startup if it does not already exist. + * Idempotent: skips creation if the form UUID is already present in the database. + * + * Adds an initial GENERAL_INFO consent gate ("Has the beneficiary agreed for counselling?") ahead + * of the PRE_SUBMIT sections, and uses CHECKBOX questions in place of Yes/No RADIO questions. + * + * @author Piramal Swasthya + */ +@Component +@RequiredArgsConstructor +public class TbCounsellingV2FormSeeder { + + private static final Logger log = LoggerFactory.getLogger(TbCounsellingV2FormSeeder.class); + + public static final String FORM_UUID = "TB_COUNSELLING_V2"; + + private final DynamicFormDefinitionService formService; + private final DynamicFormRepo formRepo; + + @PostConstruct + public void seed() { + if (formRepo.findByFormUuid(FORM_UUID).isPresent()) { + log.info("TbCounsellingV2FormSeeder: form '{}' already exists — skipping seed.", FORM_UUID); + return; + } + log.info("TbCounsellingV2FormSeeder: seeding TB Counselling (v2) form..."); + formService.createForm(buildFormDto()); + log.info("TbCounsellingV2FormSeeder: seed complete."); + } + + // ── Form Definition ────────────────────────────────────────────────────────── + + private DynamicFormDTO buildFormDto() { + DynamicFormDTO dto = new DynamicFormDTO(); + dto.setFormUuid(FORM_UUID); + dto.setFormName("TB Counselling"); + dto.setFormType(FormType.TB_COUNSELLING_V2); + dto.setIsActive(true); + dto.setFollowUpDelayDays(15); + + List sections = new ArrayList<>(); + sections.add(buildGeneralInfoSection()); + sections.add(buildSectionA()); + sections.add(buildSectionB()); + sections.add(buildSectionC()); + sections.add(buildSectionD()); + sections.add(buildSectionE()); + dto.setSections(sections); + return dto; + } + + // ── General Info: Counselling Consent ──────────────────────────────────────── + + private FormSectionDTO buildGeneralInfoSection() { + FormSectionDTO s = section("TB2_SEC_GENERAL_INFO", "General Information", "सामान्य जानकारी", + SectionPhase.GENERAL_INFO, 1, true, false, false); + List qs = new ArrayList<>(); + + // "Has the beneficiary agreed for counselling?" — RADIO: Yes | No + SectionQuestionDTO consentQ = new SectionQuestionDTO(); + consentQ.setQuestionUuid("TB2_GI_Q1"); + consentQ.setQuestionText("Has the beneficiary agreed for counselling?"); + consentQ.setQuestionTextHindi("क्या लाभार्थी काउंसलिंग के लिए सहमत हुआ है?"); + consentQ.setQuestionType(QuestionType.CHECKBOX); + consentQ.setIsMandatory(true); + consentQ.setDisplayOrder(1); + consentQ.setVisibleByDefault(true); + + // "Yes" option — no conditions, sections A-E remain enabled by default + QuestionOptionDTO yesOpt = option("Yes", "हाँ", "YES", "हाँ", 1, List.of()); + + // "No" option — disable validation for sections A-E + show refusal reason field + List noConditions = new ArrayList<>(); + noConditions.add(disableSectionValidation("TB2_SEC_A")); + noConditions.add(disableSectionValidation("TB2_SEC_B")); + noConditions.add(disableSectionValidation("TB2_SEC_C")); + noConditions.add(disableSectionValidation("TB2_SEC_D")); + noConditions.add(disableSectionValidation("TB2_SEC_E")); + noConditions.add(showQuestion("TB2_GI_REFUSAL")); + QuestionOptionDTO noOpt = option("No", "नहीं", "NO", "नहीं", 2, noConditions); + + List consentOptions = new ArrayList<>(); + consentOptions.add(yesOpt); + consentOptions.add(noOpt); + consentQ.setOptions(consentOptions); + consentQ.setValidations(List.of()); + qs.add(consentQ); + + // Reason for refusal — hidden by default, mandatory when shown, max 300 chars + qs.add(textQuestion("TB2_GI_REFUSAL", "Reason for refusal", "इनकार का कारण", 2, true, 300, false)); + + s.setQuestions(qs); + return s; + } + + // ── Section A: Disease Awareness ───────────────────────────────────────────── + + private FormSectionDTO buildSectionA() { + FormSectionDTO s = section("TB2_SEC_A", "Disease Awareness", "बीमारी के बारे में जागरूकता", SectionPhase.PRE_SUBMIT, 2, true, false, false); + List qs = new ArrayList<>(); + qs.add(checkboxQuestion("TB2_A_Q1", "TB disease explained to patient", "मरीज़ को टीबी बीमारी के बारे में समझाया गया।", 1, true)); + qs.add(checkboxQuestion("TB2_A_Q2", "Transmission route explained", "ट्रांसमिशन के तरीके के बारे में जानकारी", 2, true)); + qs.add(checkboxQuestion("TB2_A_Q3", "Symptoms explained", "लक्षणों की जानकारी", 3, true)); + qs.add(checkboxQuestion("TB2_A_Q4", "Treatment duration explained", "इलाज की अवधि के बारे में जानकारी", 4, true)); + qs.add(textQuestion("TB2_A_REMARKS", "Disease awareness notes", "बीमारी के बारे में जानकारी देने वाले नोट्स", 5, false, 500, true)); + s.setQuestions(qs); + return s; + } + + // ── Section B: Do's and Don'ts ─────────────────────────────────────────────── + + private FormSectionDTO buildSectionB() { + FormSectionDTO s = section("TB2_SEC_B", "Do's and Don'ts", "करो और ना करो", SectionPhase.PRE_SUBMIT, 3, true, false, false); + List qs = new ArrayList<>(); + qs.add(checkboxQuestion("TB2_B_Q1", "Cover mouth while coughing — advised", "खांसते समय मुंह ढकने की सलाह दी जाती है।", 1, true)); + qs.add(checkboxQuestion("TB2_B_Q2", "Complete full treatment course — advised", "इलाज का पूरा कोर्स पूरा करने की सलाह दी जाती है।", 2, true)); + qs.add(checkboxQuestion("TB2_B_Q3", "Regular follow-up attendance — advised", "इलाज का पूरा कोर्स पूरा करने की सलाह दी जाती है।", 3, true)); + qs.add(checkboxQuestion("TB2_B_Q4", "Nutritional guidance provided", "पोषण संबंधी मार्गदर्शन प्रदान किया गया", 4, true)); + qs.add(checkboxQuestion("TB2_B_Q5", "No smoking / alcohol — advised", "धूम्रपान / शराब न लेने की सलाह दी जाती है।", 5, true)); + qs.add(checkboxQuestion("TB2_B_Q6", "Isolation precautions explained", "अलगाव सावधानियों के बारे में बताया गया", 6, true)); + qs.add(textQuestion("TB2_B_REMARKS", "Do's & Don'ts notes", "क्या करें और क्या न करें - नोट्स", 7, false, 500, true)); + s.setQuestions(qs); + return s; + } + + // ── Section C: Government Schemes ──────────────────────────────────────────── + + private FormSectionDTO buildSectionC() { + FormSectionDTO s = section("TB2_SEC_C", "Government Schemes", "सरकारी योजनाएं", SectionPhase.PRE_SUBMIT, 4, true, false, false); + List qs = new ArrayList<>(); + qs.add(checkboxQuestion("TB2_C_Q1", "Nikshay Poshan Yojana (NPY) eligibility explained", "निक्षय पोषण योजना (एनपीवाई) पात्रता के बारे में बताया गया", 1, true)); + qs.add(checkboxQuestion("TB2_C_Q2", "DOTS free treatment explained", "DOTS मुफ़्त इलाज के बारे में जानकारी", 2, true)); + qs.add(textQuestion("TB2_C_REMARKS", "Schemes notes", "योजनाओं के नोट्स", 3, false, 300, true)); + s.setQuestions(qs); + return s; + } + + // ── Section D: Treatment Regimen ───────────────────────────────────────────── + + private FormSectionDTO buildSectionD() { + FormSectionDTO s = section("TB2_SEC_D", "Treatment Regimen", "इलाज का तरीका", SectionPhase.PRE_SUBMIT, 5, true, false, false); + List qs = new ArrayList<>(); + qs.add(checkboxQuestion("TB2_D_Q1", "Regimen explained to patient", "मरीज़ को इलाज का तरीका समझाया गया।", 1, true)); + qs.add(checkboxQuestion("TB2_D_Q2", "Medication names explained", "दवाओं के नामों की जानकारी", 2, true)); + qs.add(checkboxQuestion("TB2_D_Q3", "Side effects explained", "साइड इफ़ेक्ट्स के बारे में जानकारी", 3, true)); + qs.add(checkboxQuestion("TB2_D_Q4", "Importance of adherence explained", "निर्देशों का पालन करने का महत्व समझाया गया", 4, true)); + qs.add(textQuestion("TB2_D_REMARKS", "Treatment regimen notes", "इलाज के तरीके से जुड़े नोट्स", 5, false, 300, true)); + s.setQuestions(qs); + return s; + } + + // ── Section E: Impact Assessment ───────────────────────────────────────────── + + private FormSectionDTO buildSectionE() { + FormSectionDTO s = section("TB2_SEC_E", "Impact Assessment", "प्रभाव का आकलन", SectionPhase.PRE_SUBMIT, 6, true, true, true); + List qs = new ArrayList<>(); + qs.add(textQuestion("TB2_E_Q1", + "How has TB disease impacted the life of this beneficiary", + "टीबी बीमारी ने इस लाभार्थी के जीवन को कैसे प्रभावित किया है", + 1, false, 400, true)); + s.setQuestions(qs); + return s; + } + + // ── Builder Helpers ─────────────────────────────────────────────────────────── + + private FormSectionDTO section(String uuid, String name, String nameHindi, SectionPhase phase, + int order, boolean required, boolean hasSubmitButton, boolean isEditable) { + FormSectionDTO s = new FormSectionDTO(); + s.setSectionUuid(uuid); + s.setSectionName(name); + s.setSectionNameHindi(nameHindi); + s.setSectionPhase(phase); + s.setDisplayOrder(order); + s.setIsRequired(required); + s.setHasSubmitButton(hasSubmitButton); + s.setIsEditable(isEditable); + s.setQuestions(new ArrayList<>()); + return s; + } + + private SectionQuestionDTO checkboxQuestion(String uuid, String text, String textHindi, int order, boolean mandatory) { + SectionQuestionDTO q = new SectionQuestionDTO(); + q.setQuestionUuid(uuid); + q.setQuestionText(text); + q.setQuestionTextHindi(textHindi); + q.setQuestionType(QuestionType.CHECKBOX); + q.setIsMandatory(mandatory); + q.setDisplayOrder(order); + q.setVisibleByDefault(true); + + List opts = new ArrayList<>(); + opts.add(option("Yes", "हाँ", "YES", "हाँ", 1, List.of())); + opts.add(option("No", "नहीं", "NO", "नहीं", 2, List.of())); + q.setOptions(opts); + + q.setValidations(List.of()); + return q; + } + + private SectionQuestionDTO textQuestion(String uuid, String text, String textHindi, int order, + boolean mandatory, int maxLength, boolean visible) { + SectionQuestionDTO q = new SectionQuestionDTO(); + q.setQuestionUuid(uuid); + q.setQuestionText(text); + q.setQuestionTextHindi(textHindi); + q.setQuestionType(QuestionType.TEXT); + q.setIsMandatory(mandatory); + q.setDisplayOrder(order); + q.setMaxLength(maxLength); + q.setVisibleByDefault(visible); + q.setOptions(List.of()); + + QuestionValidationDTO v = new QuestionValidationDTO(); + v.setValidationType(ValidationType.MAX_LENGTH); + v.setValidationParam(String.valueOf(maxLength)); + v.setErrorMessage("Must be " + maxLength + " characters or fewer"); + q.setValidations(List.of(v)); + return q; + } + + private QuestionOptionDTO option(String label, String labelHindi, String value, String valueHindi, int order, + List conditions) { + QuestionOptionDTO o = new QuestionOptionDTO(); + o.setOptionLabel(label); + o.setOptionLabelHindi(labelHindi); + o.setOptionValue(value); + o.setOptionValueHindi(valueHindi); + o.setDisplayOrder(order); + o.setConditions(conditions); + return o; + } + + private OptionConditionDTO showQuestion(String targetQuestionUuid) { + OptionConditionDTO c = new OptionConditionDTO(); + c.setActionType("SHOW_QUESTION"); + c.setTargetQuestionUuid(targetQuestionUuid); + return c; + } + + private OptionConditionDTO disableSectionValidation(String targetSectionUuid) { + OptionConditionDTO c = new OptionConditionDTO(); + c.setActionType("DISABLE_SECTION_VALIDATION"); + c.setTargetSectionUuid(targetSectionUuid); + return c; + } +} \ No newline at end of file diff --git a/src/main/java/com/iemr/flw/service/DynamicFormResponseService.java b/src/main/java/com/iemr/flw/service/DynamicFormResponseService.java index cca29e2e..c6929e62 100644 --- a/src/main/java/com/iemr/flw/service/DynamicFormResponseService.java +++ b/src/main/java/com/iemr/flw/service/DynamicFormResponseService.java @@ -35,7 +35,7 @@ */ public interface DynamicFormResponseService { - /** Save PRE_SUBMIT section answers and advance status to SUBMITTED. */ + /** Save GENERAL_INFO and PRE_SUBMIT section answers and advance status to SUBMITTED. */ FormResponseDTO submitForm(FormResponseRequest request); /** Save POST_SUBMIT section answers and advance status to COMPLETE. */ diff --git a/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java index d1673a0e..0bcd49df 100644 --- a/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java @@ -45,6 +45,7 @@ import com.iemr.flw.repo.iemr.SectionQuestionRepo; import com.iemr.flw.repo.iemr.SectionResponseRepo; import com.iemr.flw.masterEnum.QuestionType; +import com.iemr.flw.masterEnum.SectionPhase; import com.iemr.flw.service.DynamicFormResponseService; import com.iemr.flw.utils.JwtUtil; import com.iemr.flw.utils.exception.IEMRException; @@ -240,10 +241,14 @@ private FormResponseDTO processSections( "' not found in form '" + request.getFormUuid() + "'"); } - SectionResponse sectionResponse = upsertSectionResponse( - formResponse.getResponseId(), section.getSectionId(), actor); + Optional sectionResponseOpt = upsertSectionResponse( + formResponse.getResponseId(), section, actor); + if (sectionResponseOpt.isEmpty()) { + continue; + } + SectionResponse sectionResponse = sectionResponseOpt.get(); - if ("POST_SUBMIT".equals(section.getSectionPhase())) { + if (section.getSectionPhase() == SectionPhase.POST_SUBMIT) { formResponse.setLastFollowUpAt(sectionResponse.getSavedAt()); formResponseRepo.save(formResponse); } @@ -260,31 +265,36 @@ private FormResponseDTO processSections( questionResponseRepo.saveAll(questionResponses); - sectionDTOs.add(buildSectionResponseDTO(sectionResponse, questionResponses)); + sectionDTOs.add(buildSectionResponseDTO(sectionResponse, section, questionResponses)); } return buildFormResponseDTO(formResponse, sectionDTOs); } - private SectionResponse upsertSectionResponse(Long responseId, Long sectionId, String actor) { + private Optional upsertSectionResponse(Long responseId, FormSection section, String actor) { Optional existing = - sectionResponseRepo.findByResponseIdAndSectionId(responseId, sectionId); + sectionResponseRepo.findByResponseIdAndSectionId(responseId, section.getSectionId()); if (existing.isPresent()) { + if (Boolean.FALSE.equals(section.getIsEditable())) { + log.info("Section '{}' is not editable — skipping update for responseId={}", + section.getSectionUuid(), responseId); + return Optional.empty(); + } SectionResponse sr = existing.get(); sr.setStatus(SECTION_STATUS_DONE); sr.setSavedAt(new Timestamp(System.currentTimeMillis())); sr.setUpdatedBy(actor); - return sectionResponseRepo.save(sr); + return Optional.of(sectionResponseRepo.save(sr)); } SectionResponse sr = SectionResponse.builder() .responseId(responseId) - .sectionId(sectionId) + .sectionId(section.getSectionId()) .status(SECTION_STATUS_DONE) .savedAt(new Timestamp(System.currentTimeMillis())) .createdBy(actor) .updatedBy(actor) .build(); - return sectionResponseRepo.save(sr); + return Optional.of(sectionResponseRepo.save(sr)); } private List processAnswers( @@ -344,7 +354,7 @@ private List processAnswers( } } } else { - // TEXT, DATE, AUTO_FILL — prefer answerText, then answerDate, then optionValue (legacy) + // TEXT, DATE, AUTO_FILL, CHECKBOX — prefer answerText, then answerDate, then optionValue (legacy) String value = answer.getAnswerText() != null ? answer.getAnswerText() : answer.getAnswerDate() != null ? answer.getAnswerDate() : answer.getOptionValue(); @@ -390,9 +400,16 @@ private List loadSectionResponseDTOs(Long responseId) { Map> answersBySectionResponse = allAnswers.stream() .collect(Collectors.groupingBy(QuestionResponse::getSectionResponseId)); + Set sectionIds = sections.stream() + .map(SectionResponse::getSectionId) + .collect(Collectors.toSet()); + Map sectionById = formSectionRepo.findAllById(sectionIds).stream() + .collect(Collectors.toMap(FormSection::getSectionId, Function.identity())); + return sections.stream() .map(sr -> buildSectionResponseDTO( sr, + sectionById.get(sr.getSectionId()), answersBySectionResponse.getOrDefault(sr.getSectionResponseId(), List.of()))) .collect(Collectors.toList()); } @@ -417,7 +434,7 @@ private FormResponseDTO buildFormResponseDTO(FormResponse r, List answers) { + SectionResponse sr, FormSection section, List answers) { List answerDTOs = answers.stream() .map(a -> QuestionResponseDTO.builder() .questionResponseId(a.getQuestionResponseId()) @@ -429,6 +446,8 @@ private SectionResponseDTO buildSectionResponseDTO( return SectionResponseDTO.builder() .sectionResponseId(sr.getSectionResponseId()) .sectionId(sr.getSectionId()) + .sectionUuid(section != null ? section.getSectionUuid() : null) + .isEditable(section != null ? section.getIsEditable() : null) .status(sr.getStatus()) .savedAt(sr.getSavedAt()) .answers(answerDTOs) diff --git a/src/main/java/com/iemr/flw/service/impl/FormResponseItemSaver.java b/src/main/java/com/iemr/flw/service/impl/FormResponseItemSaver.java index 0d844eb7..016681f3 100644 --- a/src/main/java/com/iemr/flw/service/impl/FormResponseItemSaver.java +++ b/src/main/java/com/iemr/flw/service/impl/FormResponseItemSaver.java @@ -42,6 +42,7 @@ import com.iemr.flw.repo.iemr.SectionQuestionRepo; import com.iemr.flw.repo.iemr.SectionResponseRepo; import com.iemr.flw.masterEnum.QuestionType; +import com.iemr.flw.masterEnum.SectionPhase; import com.iemr.flw.utils.JwtUtil; import com.iemr.flw.utils.exception.IEMRException; import lombok.RequiredArgsConstructor; @@ -224,18 +225,24 @@ private FormResponseDTO processSections(FormResponse formResponse, FormResponseR "' not found in form '" + req.getFormUuid() + "'"); } + Optional existingSr = sectionResponseRepo.findByResponseIdAndSectionId( + formResponse.getResponseId(), section.getSectionId()); + if (existingSr.isPresent() && Boolean.FALSE.equals(section.getIsEditable())) { + log.info("saveForBulk: section '{}' is not editable — skipping update for responseId={}", + section.getSectionUuid(), formResponse.getResponseId()); + continue; + } + // Delete existing SectionResponse (and its QuestionResponses) for this section only - sectionResponseRepo.findByResponseIdAndSectionId( - formResponse.getResponseId(), section.getSectionId()) - .ifPresent(sr -> { - questionResponseRepo.deleteBySectionResponseIdIn(Set.of(sr.getSectionResponseId())); - sectionResponseRepo.deleteById(sr.getSectionResponseId()); - }); + existingSr.ifPresent(sr -> { + questionResponseRepo.deleteBySectionResponseIdIn(Set.of(sr.getSectionResponseId())); + sectionResponseRepo.deleteById(sr.getSectionResponseId()); + }); SectionResponse sectionResponse = upsertSectionResponse( formResponse.getResponseId(), section.getSectionId(), actor); - if ("POST_SUBMIT".equals(section.getSectionPhase())) { + if (section.getSectionPhase() == SectionPhase.POST_SUBMIT) { formResponse.setLastFollowUpAt(sectionResponse.getSavedAt()); formResponseRepo.save(formResponse); } @@ -251,7 +258,7 @@ private FormResponseDTO processSections(FormResponse formResponse, FormResponseR actor); questionResponseRepo.saveAll(questionResponses); - sectionDTOs.add(buildSectionResponseDTO(sectionResponse, questionResponses)); + sectionDTOs.add(buildSectionResponseDTO(sectionResponse, section, questionResponses)); } return buildFormResponseDTO(formResponse, sectionDTOs); @@ -332,7 +339,7 @@ private List processAnswers( } } } else { - // TEXT, DATE, AUTO_FILL — prefer answerText, then answerDate, then optionValue (legacy) + // TEXT, DATE, AUTO_FILL, CHECKBOX — prefer answerText, then answerDate, then optionValue (legacy) String value = answer.getAnswerText() != null ? answer.getAnswerText() : answer.getAnswerDate() != null ? answer.getAnswerDate() : answer.getOptionValue(); @@ -378,7 +385,8 @@ private FormResponseDTO buildFormResponseDTO(FormResponse r, List answers) { + private SectionResponseDTO buildSectionResponseDTO( + SectionResponse sr, FormSection section, List answers) { List answerDTOs = answers.stream() .map(a -> QuestionResponseDTO.builder() .questionResponseId(a.getQuestionResponseId()) @@ -390,6 +398,8 @@ private SectionResponseDTO buildSectionResponseDTO(SectionResponse sr, List Date: Mon, 6 Jul 2026 16:12:47 +0530 Subject: [PATCH 17/37] Seed single-checkbox questions for Sections A-D and point the Counselled lookup at TB_COUNSELLING_V2 instead of the retired V1 form --- .../com/iemr/flw/seeder/TbCounsellingV2FormSeeder.java | 9 ++------- .../flw/service/impl/TBConfirmedCaseServiceImpl.java | 4 ++-- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/iemr/flw/seeder/TbCounsellingV2FormSeeder.java b/src/main/java/com/iemr/flw/seeder/TbCounsellingV2FormSeeder.java index 5780eb76..59690f5b 100644 --- a/src/main/java/com/iemr/flw/seeder/TbCounsellingV2FormSeeder.java +++ b/src/main/java/com/iemr/flw/seeder/TbCounsellingV2FormSeeder.java @@ -106,7 +106,7 @@ private FormSectionDTO buildGeneralInfoSection() { consentQ.setQuestionUuid("TB2_GI_Q1"); consentQ.setQuestionText("Has the beneficiary agreed for counselling?"); consentQ.setQuestionTextHindi("क्या लाभार्थी काउंसलिंग के लिए सहमत हुआ है?"); - consentQ.setQuestionType(QuestionType.CHECKBOX); + consentQ.setQuestionType(QuestionType.RADIO); consentQ.setIsMandatory(true); consentQ.setDisplayOrder(1); consentQ.setVisibleByDefault(true); @@ -233,12 +233,7 @@ private SectionQuestionDTO checkboxQuestion(String uuid, String text, String tex q.setIsMandatory(mandatory); q.setDisplayOrder(order); q.setVisibleByDefault(true); - - List opts = new ArrayList<>(); - opts.add(option("Yes", "हाँ", "YES", "हाँ", 1, List.of())); - opts.add(option("No", "नहीं", "NO", "नहीं", 2, List.of())); - q.setOptions(opts); - + q.setOptions(List.of(option("Checked", "चेक किया गया", "CHECKED", "चेक किया गया", 1, List.of()))); q.setValidations(List.of()); return q; } diff --git a/src/main/java/com/iemr/flw/service/impl/TBConfirmedCaseServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/TBConfirmedCaseServiceImpl.java index 94784d29..01ab464a 100644 --- a/src/main/java/com/iemr/flw/service/impl/TBConfirmedCaseServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/TBConfirmedCaseServiceImpl.java @@ -9,7 +9,7 @@ import com.iemr.flw.repo.iemr.DynamicFormRepo; import com.iemr.flw.repo.iemr.FormResponseRepo; import com.iemr.flw.repo.iemr.TBConfirmedTreatmentRepository; -import com.iemr.flw.seeder.TbCounsellingFormSeeder; +import com.iemr.flw.seeder.TbCounsellingV2FormSeeder; import com.iemr.flw.service.CampConfigService; import com.iemr.flw.service.TBConfirmedCaseService; import com.iemr.flw.service.TBStopVisitService; @@ -150,7 +150,7 @@ public String getByUserId(String authorisation) throws Exception { List benIds = dtoList.stream().map(TBConfirmedCaseDTO::getBenId).collect(Collectors.toList()); Set counselledBenIds = Collections.emptySet(); if (!benIds.isEmpty()) { - Optional counsellingForm = dynamicFormRepo.findByFormUuid(TbCounsellingFormSeeder.FORM_UUID); + Optional counsellingForm = dynamicFormRepo.findByFormUuid(TbCounsellingV2FormSeeder.FORM_UUID); if (counsellingForm.isPresent()) { counselledBenIds = new HashSet<>(formResponseRepo.findCounselledBenIds(benIds, counsellingForm.get().getFormId(), "COMPLETE")); } From 4aafc4b02d1555433cff61b08d209001068b4ab5 Mon Sep 17 00:00:00 2001 From: Sehjot Singh Pannu Date: Mon, 6 Jul 2026 19:27:45 +0530 Subject: [PATCH 18/37] Mark form/section status REFUSED when TB2_GI_Q1=NO on /complete, split getCompletedBeneficiaries into completed/refused lists, and extract FormResponseStatus enum --- .../DynamicFormResponseController.java | 8 +- .../dto/iemr/CompletedBeneficiariesDTO.java | 44 ++++++++++ .../flw/masterEnum/FormResponseStatus.java | 36 ++++++++ .../flw/seeder/TbCounsellingV2FormSeeder.java | 2 +- .../service/DynamicFormResponseService.java | 8 +- .../impl/DynamicFormResponseServiceImpl.java | 87 +++++++++++++++---- .../service/impl/FormResponseItemSaver.java | 6 +- 7 files changed, 164 insertions(+), 27 deletions(-) create mode 100644 src/main/java/com/iemr/flw/dto/iemr/CompletedBeneficiariesDTO.java create mode 100644 src/main/java/com/iemr/flw/masterEnum/FormResponseStatus.java diff --git a/src/main/java/com/iemr/flw/controller/DynamicFormResponseController.java b/src/main/java/com/iemr/flw/controller/DynamicFormResponseController.java index 99ebb9f7..33ac46b2 100644 --- a/src/main/java/com/iemr/flw/controller/DynamicFormResponseController.java +++ b/src/main/java/com/iemr/flw/controller/DynamicFormResponseController.java @@ -21,6 +21,7 @@ */ package com.iemr.flw.controller; +import com.iemr.flw.dto.iemr.CompletedBeneficiariesDTO; import com.iemr.flw.dto.iemr.FormResponseDTO; import com.iemr.flw.dto.iemr.FormResponseRequest; import com.iemr.flw.masterEnum.FormType; @@ -98,13 +99,14 @@ public ResponseEntity getById(@RequestParam Long responseId) { FormResponseDTO dto = responseService.getResponseById(responseId); return ResponseEntity.ok(new ApiResponse(true, "Response fetched successfully", dto)); } - @Operation(summary = "Get beneficiary IDs that have a COMPLETE form response for the given form type, optionally filtered by village and/or provider service map") + @Operation(summary = "Get beneficiary IDs that have a COMPLETE or REFUSED form response for the given form type, optionally filtered by village and/or provider service map") @RequestMapping(value = "/getCompletedBeneficiaries", method = RequestMethod.GET) public ResponseEntity getCompletedBeneficiaries( @RequestParam FormType formType, @RequestParam(required = false) Integer villageId, @RequestParam(required = false) Integer providerServiceMapId) { - List beneficiaryIds = responseService.getCompletedBeneficiaries(formType, villageId, providerServiceMapId); - return ResponseEntity.ok(new ApiResponse(true, "Completed beneficiaries fetched successfully", beneficiaryIds)); + CompletedBeneficiariesDTO result = + responseService.getCompletedBeneficiaries(formType, villageId, providerServiceMapId); + return ResponseEntity.ok(new ApiResponse(true, "Completed beneficiaries fetched successfully", result)); } } diff --git a/src/main/java/com/iemr/flw/dto/iemr/CompletedBeneficiariesDTO.java b/src/main/java/com/iemr/flw/dto/iemr/CompletedBeneficiariesDTO.java new file mode 100644 index 00000000..b391cff0 --- /dev/null +++ b/src/main/java/com/iemr/flw/dto/iemr/CompletedBeneficiariesDTO.java @@ -0,0 +1,44 @@ +/* + * 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.dto.iemr; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * Beneficiary IDs for a form type, split by outcome: those who completed the form and those + * who refused (TB2_GI_Q1 = NO on the General Info consent question). + * + * @author Piramal Swasthya + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class CompletedBeneficiariesDTO { + private List completed; + private List refused; +} \ No newline at end of file diff --git a/src/main/java/com/iemr/flw/masterEnum/FormResponseStatus.java b/src/main/java/com/iemr/flw/masterEnum/FormResponseStatus.java new file mode 100644 index 00000000..62fffd00 --- /dev/null +++ b/src/main/java/com/iemr/flw/masterEnum/FormResponseStatus.java @@ -0,0 +1,36 @@ +/* + * 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.masterEnum; + +/** + * Lifecycle status stored in FormResponse.status. + * + * @author Piramal Swasthya + */ +public enum FormResponseStatus { + /** Saved via /submit. */ + SUBMITTED, + /** Saved via /complete, consent question answered YES (or not applicable). */ + COMPLETE, + /** Saved via /complete, consent question TB2_GI_Q1 answered NO. */ + REFUSED +} \ No newline at end of file diff --git a/src/main/java/com/iemr/flw/seeder/TbCounsellingV2FormSeeder.java b/src/main/java/com/iemr/flw/seeder/TbCounsellingV2FormSeeder.java index 59690f5b..0173db59 100644 --- a/src/main/java/com/iemr/flw/seeder/TbCounsellingV2FormSeeder.java +++ b/src/main/java/com/iemr/flw/seeder/TbCounsellingV2FormSeeder.java @@ -98,7 +98,7 @@ private DynamicFormDTO buildFormDto() { private FormSectionDTO buildGeneralInfoSection() { FormSectionDTO s = section("TB2_SEC_GENERAL_INFO", "General Information", "सामान्य जानकारी", - SectionPhase.GENERAL_INFO, 1, true, false, false); + SectionPhase.GENERAL_INFO, 1, true, false, true); List qs = new ArrayList<>(); // "Has the beneficiary agreed for counselling?" — RADIO: Yes | No diff --git a/src/main/java/com/iemr/flw/service/DynamicFormResponseService.java b/src/main/java/com/iemr/flw/service/DynamicFormResponseService.java index c6929e62..61a3bce1 100644 --- a/src/main/java/com/iemr/flw/service/DynamicFormResponseService.java +++ b/src/main/java/com/iemr/flw/service/DynamicFormResponseService.java @@ -21,6 +21,7 @@ */ package com.iemr.flw.service; +import com.iemr.flw.dto.iemr.CompletedBeneficiariesDTO; import com.iemr.flw.dto.iemr.FormResponseDTO; import com.iemr.flw.dto.iemr.FormResponseRequest; import com.iemr.flw.masterEnum.FormType; @@ -61,6 +62,9 @@ public interface DynamicFormResponseService { */ List findPendingFollowUps(List formIds, int delayDays); - /** Returns beneficiary IDs with COMPLETE status for the given form type, optionally filtered by village and/or provider service map. */ - List getCompletedBeneficiaries(FormType formType, Integer villageId, Integer providerServiceMapId); + /** + * Returns beneficiary IDs for the given form type split by outcome — COMPLETE and REFUSED — + * optionally filtered by village and/or provider service map. + */ + CompletedBeneficiariesDTO getCompletedBeneficiaries(FormType formType, Integer villageId, Integer providerServiceMapId); } diff --git a/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java index 0bcd49df..4c1c3c4d 100644 --- a/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java @@ -28,6 +28,7 @@ import com.iemr.flw.domain.iemr.QuestionResponse; import com.iemr.flw.domain.iemr.SectionQuestion; import com.iemr.flw.domain.iemr.SectionResponse; +import com.iemr.flw.dto.iemr.CompletedBeneficiariesDTO; import com.iemr.flw.dto.iemr.FormResponseDTO; import com.iemr.flw.dto.iemr.FormResponseRequest; import com.iemr.flw.dto.iemr.QuestionAnswerRequest; @@ -35,6 +36,7 @@ import com.iemr.flw.dto.iemr.SectionAnswerRequest; import com.iemr.flw.dto.iemr.SectionResponseDTO; import com.iemr.flw.domain.iemr.DynamicForm; +import com.iemr.flw.masterEnum.FormResponseStatus; import com.iemr.flw.masterEnum.FormType; import com.iemr.flw.repo.iemr.DynamicFormRepo; import com.iemr.flw.repo.iemr.FormResponseRepo; @@ -76,10 +78,11 @@ @Service public class DynamicFormResponseServiceImpl implements DynamicFormResponseService { - private static final String STATUS_DRAFT = "DRAFT"; - private static final String STATUS_SUBMITTED = "SUBMITTED"; - private static final String STATUS_COMPLETE = "COMPLETE"; private static final String SECTION_STATUS_DONE = "DONE"; + private static final String SECTION_STATUS_REFUSED = "REFUSED"; + private static final String SECTION_UUID_GENERAL_INFO = "TB2_SEC_GENERAL_INFO"; + private static final String QUESTION_UUID_CONSENT = "TB2_GI_Q1"; + private static final String OPTION_VALUE_CONSENT_REFUSED = "NO"; private final DynamicFormRepo dynamicFormRepo; private final FormResponseRepo formResponseRepo; @@ -103,19 +106,19 @@ public FormResponseDTO submitForm(FormResponseRequest request) { formResponse = formResponseRepo.findById(request.getResponseId()) .orElseThrow(() -> new NoSuchElementException( "FormResponse not found: " + request.getResponseId())); - if (STATUS_COMPLETE.equals(formResponse.getStatus())) { + if (FormResponseStatus.COMPLETE.name().equals(formResponse.getStatus())) { throw new IllegalStateException( "Cannot update a COMPLETE response (responseId=" + request.getResponseId() + ")"); } // Bump submittedAt to reflect the re-submission time - formResponse.setStatus(STATUS_SUBMITTED); + formResponse.setStatus(FormResponseStatus.SUBMITTED.name()); formResponse.setSubmittedAt(now); formResponse.setLastFollowUpAt(now); formResponse = formResponseRepo.save(formResponse); } else { - formResponse = createFormResponse(request, version, STATUS_SUBMITTED, now); + formResponse = createFormResponse(request, version, FormResponseStatus.SUBMITTED.name(), now); } - return processSections(formResponse, request, version, null); + return processSections(formResponse, request, version, null, false); } @Override @@ -130,14 +133,16 @@ public FormResponseDTO completeForm(FormResponseRequest request, String jwtToken List existing = formResponseRepo.findByBeneficiaryIdAndFormId(request.getBeneficiaryId(), formId); FormResponse formResponse = existing.isEmpty() - ? createFormResponse(request, version, STATUS_SUBMITTED, now) + ? createFormResponse(request, version, FormResponseStatus.SUBMITTED.name(), now) : existing.get(0); formResponse.setUpdatedBy(actor); - formResponse.setStatus(STATUS_COMPLETE); + formResponse.setStatus(isConsentRefused(request) + ? FormResponseStatus.REFUSED.name() + : FormResponseStatus.COMPLETE.name()); formResponse.setCompletedAt(now); formResponse = formResponseRepo.save(formResponse); - return processSections(formResponse, request, version, actor); + return processSections(formResponse, request, version, actor, true); } @Override @@ -198,7 +203,8 @@ private FormResponseDTO processSections( FormResponse formResponse, FormResponseRequest request, FormVersion version, - String actor) { + String actor, + boolean applyRefusalRule) { List allSections = formSectionRepo.findByFormVersion_VersionIdOrderByDisplayOrderAsc(version.getVersionId()); @@ -241,8 +247,11 @@ private FormResponseDTO processSections( "' not found in form '" + request.getFormUuid() + "'"); } + String desiredStatus = applyRefusalRule + ? determineSectionStatus(section, sectionReq.getAnswers()) + : SECTION_STATUS_DONE; Optional sectionResponseOpt = upsertSectionResponse( - formResponse.getResponseId(), section, actor); + formResponse.getResponseId(), section, actor, desiredStatus); if (sectionResponseOpt.isEmpty()) { continue; } @@ -271,7 +280,8 @@ private FormResponseDTO processSections( return buildFormResponseDTO(formResponse, sectionDTOs); } - private Optional upsertSectionResponse(Long responseId, FormSection section, String actor) { + private Optional upsertSectionResponse( + Long responseId, FormSection section, String actor, String status) { Optional existing = sectionResponseRepo.findByResponseIdAndSectionId(responseId, section.getSectionId()); if (existing.isPresent()) { @@ -281,7 +291,7 @@ private Optional upsertSectionResponse(Long responseId, FormSec return Optional.empty(); } SectionResponse sr = existing.get(); - sr.setStatus(SECTION_STATUS_DONE); + sr.setStatus(status); sr.setSavedAt(new Timestamp(System.currentTimeMillis())); sr.setUpdatedBy(actor); return Optional.of(sectionResponseRepo.save(sr)); @@ -289,7 +299,7 @@ private Optional upsertSectionResponse(Long responseId, FormSec SectionResponse sr = SectionResponse.builder() .responseId(responseId) .sectionId(section.getSectionId()) - .status(SECTION_STATUS_DONE) + .status(status) .savedAt(new Timestamp(System.currentTimeMillis())) .createdBy(actor) .updatedBy(actor) @@ -297,6 +307,38 @@ private Optional upsertSectionResponse(Long responseId, FormSec return Optional.of(sectionResponseRepo.save(sr)); } + // TB2_SEC_GENERAL_INFO's consent question (TB2_GI_Q1) gates the rest of the TB Counselling + // form; a "NO" answer means the beneficiary refused counselling. Absence-tolerant by design: + // any other section, a missing answers list, or no answer for the consent question at all + // just falls through to the normal DONE status. + private String determineSectionStatus(FormSection section, List answers) { + if (!SECTION_UUID_GENERAL_INFO.equals(section.getSectionUuid()) || answers == null) { + return SECTION_STATUS_DONE; + } + boolean refused = answers.stream().anyMatch(this::isRefusalAnswer); + return refused ? SECTION_STATUS_REFUSED : SECTION_STATUS_DONE; + } + + // Same TB2_GI_Q1="NO" signal as determineSectionStatus, but scanned directly off the raw + // request (before any FormSection is resolved) so completeForm() can decide the top-level + // FormResponse status. Absence-tolerant: no TB2_SEC_GENERAL_INFO section, no answers, or no + // TB2_GI_Q1 answer at all just means "not refused". + private boolean isConsentRefused(FormResponseRequest request) { + if (request.getSections() == null) { + return false; + } + return request.getSections().stream() + .filter(s -> SECTION_UUID_GENERAL_INFO.equals(s.getSectionUuid())) + .filter(s -> s.getAnswers() != null) + .flatMap(s -> s.getAnswers().stream()) + .anyMatch(this::isRefusalAnswer); + } + + private boolean isRefusalAnswer(QuestionAnswerRequest answer) { + return QUESTION_UUID_CONSENT.equals(answer.getQuestionUuid()) + && OPTION_VALUE_CONSENT_REFUSED.equals(answer.getOptionValue()); + } + private List processAnswers( Long sectionResponseId, List answers, @@ -486,12 +528,21 @@ public List findPendingFollowUps(List formIds, int delayD @Override @Transactional(readOnly = true) - public List getCompletedBeneficiaries(FormType formType, Integer villageId, Integer providerServiceMapId) { + public CompletedBeneficiariesDTO getCompletedBeneficiaries( + FormType formType, Integer villageId, Integer providerServiceMapId) { DynamicForm form = dynamicFormRepo.findByFormTypeAndIsActive(formType, true) .orElseThrow(() -> new NoSuchElementException( "No active form found for type: " + formType)); + return CompletedBeneficiariesDTO.builder() + .completed(findBeneficiaryIdsByStatus(form.getFormId(), FormResponseStatus.COMPLETE.name(), villageId, providerServiceMapId)) + .refused(findBeneficiaryIdsByStatus(form.getFormId(), FormResponseStatus.REFUSED.name(), villageId, providerServiceMapId)) + .build(); + } + + private List findBeneficiaryIdsByStatus( + Long formId, String status, Integer villageId, Integer providerServiceMapId) { return (villageId != null || providerServiceMapId != null) - ? formResponseRepo.findBeneficiaryIdsByFormIdAndStatusFiltered(form.getFormId(), STATUS_COMPLETE, villageId, providerServiceMapId) - : formResponseRepo.findBeneficiaryIdsByFormIdAndStatus(form.getFormId(), STATUS_COMPLETE); + ? formResponseRepo.findBeneficiaryIdsByFormIdAndStatusFiltered(formId, status, villageId, providerServiceMapId) + : formResponseRepo.findBeneficiaryIdsByFormIdAndStatus(formId, status); } } diff --git a/src/main/java/com/iemr/flw/service/impl/FormResponseItemSaver.java b/src/main/java/com/iemr/flw/service/impl/FormResponseItemSaver.java index 016681f3..7b965223 100644 --- a/src/main/java/com/iemr/flw/service/impl/FormResponseItemSaver.java +++ b/src/main/java/com/iemr/flw/service/impl/FormResponseItemSaver.java @@ -41,6 +41,7 @@ import com.iemr.flw.repo.iemr.QuestionResponseRepo; import com.iemr.flw.repo.iemr.SectionQuestionRepo; import com.iemr.flw.repo.iemr.SectionResponseRepo; +import com.iemr.flw.masterEnum.FormResponseStatus; import com.iemr.flw.masterEnum.QuestionType; import com.iemr.flw.masterEnum.SectionPhase; import com.iemr.flw.utils.JwtUtil; @@ -73,7 +74,6 @@ @Component public class FormResponseItemSaver { - private static final String STATUS_SUBMITTED = "SUBMITTED"; private static final String SECTION_STATUS_DONE = "DONE"; private final FormResponseRepo formResponseRepo; @@ -110,7 +110,7 @@ public FormResponseDTO saveForBulk(FormResponseRequest req, String jwtToken) thr Long responseId = formResponse.getResponseId(); // Step 3: Update FormResponse header - formResponse.setStatus(STATUS_SUBMITTED); + formResponse.setStatus(FormResponseStatus.SUBMITTED.name()); formResponse.setSubmittedAt(now); formResponse.setUpdatedBy(actor); formResponse.setLastFollowUpAt(now); @@ -176,7 +176,7 @@ private FormResponse createFormResponse(FormResponseRequest req, FormVersion ver .formId(version.getDynamicForm().getFormId()) .versionId(version.getVersionId()) .officerId(req.getOfficerId()) - .status(STATUS_SUBMITTED) + .status(FormResponseStatus.SUBMITTED.name()) .createdBy(actor) .updatedBy(actor) .submittedAt(now) From b4bc4e07d1c006a6c96edb012742d7aa22934a86 Mon Sep 17 00:00:00 2001 From: Sehjot Singh Pannu Date: Tue, 7 Jul 2026 10:52:53 +0530 Subject: [PATCH 19/37] Resolved conflicts --- StopTB_Standard_Table_Integration.md | 294 ++++++++++++++++++ .../domain/iemr/BenAnthropometryDetail.java | 70 +++++ .../flw/domain/iemr/BenChiefComplaint.java | 73 +++++ .../domain/iemr/BenPhysicalVitalDetail.java | 76 +++++ .../domain/iemr/PhyGeneralExamination.java | 79 +++++ .../flw/domain/iemr/PrescribedDrugDetail.java | 82 +++++ .../flw/domain/iemr/PrescriptionDetail.java | 70 +++++ .../com/iemr/flw/domain/iemr/TBStopVisit.java | 77 ----- .../flw/repo/iemr/BenAnthropometryRepo.java | 9 + .../flw/repo/iemr/BenChiefComplaintRepo.java | 9 + .../flw/repo/iemr/BenPhysicalVitalRepo.java | 9 + .../flw/repo/iemr/BenVisitDetailsRepo.java | 18 +- .../repo/iemr/PhyGeneralExaminationRepo.java | 9 + .../repo/iemr/PrescribedDrugDetailRepo.java | 9 + .../flw/repo/iemr/PrescriptionDetailRepo.java | 9 + .../iemr/flw/repo/iemr/TBStopVisitRepo.java | 29 -- .../iemr/flw/service/TBStopVisitService.java | 12 +- .../impl/DynamicFormResponseServiceImpl.java | 46 ++- .../service/impl/FormResponseItemSaver.java | 38 ++- .../flw/service/impl/StopTBServiceImpl.java | 250 ++++++++++++++- .../impl/TBConfirmedCaseServiceImpl.java | 8 +- .../service/impl/TBStopVisitServiceImpl.java | 59 ++-- .../service/impl/TBSuspectedServiceImpl.java | 8 +- 23 files changed, 1165 insertions(+), 178 deletions(-) create mode 100644 StopTB_Standard_Table_Integration.md create mode 100644 src/main/java/com/iemr/flw/domain/iemr/BenAnthropometryDetail.java create mode 100644 src/main/java/com/iemr/flw/domain/iemr/BenChiefComplaint.java create mode 100644 src/main/java/com/iemr/flw/domain/iemr/BenPhysicalVitalDetail.java create mode 100644 src/main/java/com/iemr/flw/domain/iemr/PhyGeneralExamination.java create mode 100644 src/main/java/com/iemr/flw/domain/iemr/PrescribedDrugDetail.java create mode 100644 src/main/java/com/iemr/flw/domain/iemr/PrescriptionDetail.java delete mode 100644 src/main/java/com/iemr/flw/domain/iemr/TBStopVisit.java create mode 100644 src/main/java/com/iemr/flw/repo/iemr/BenAnthropometryRepo.java create mode 100644 src/main/java/com/iemr/flw/repo/iemr/BenChiefComplaintRepo.java create mode 100644 src/main/java/com/iemr/flw/repo/iemr/BenPhysicalVitalRepo.java create mode 100644 src/main/java/com/iemr/flw/repo/iemr/PhyGeneralExaminationRepo.java create mode 100644 src/main/java/com/iemr/flw/repo/iemr/PrescribedDrugDetailRepo.java create mode 100644 src/main/java/com/iemr/flw/repo/iemr/PrescriptionDetailRepo.java delete mode 100644 src/main/java/com/iemr/flw/repo/iemr/TBStopVisitRepo.java diff --git a/StopTB_Standard_Table_Integration.md b/StopTB_Standard_Table_Integration.md new file mode 100644 index 00000000..9df8b37d --- /dev/null +++ b/StopTB_Standard_Table_Integration.md @@ -0,0 +1,294 @@ +# Stop TB — Standard Table Integration + +**Branch:** `vb/counselling` +**Date:** 2026-06-30 +**Author:** vishwab1 + +--- + +## 1. Why this was done + +Stop TB's clinical data was siloed entirely inside custom `tb_stoptb_*` tables. No other AMRIT +service (HWC, MMU) could see a Stop TB beneficiary's record — referrals, vitals, prescriptions +were invisible across service lines. + +This integration dual-writes Stop TB clinical data into the same standard tables that MMU/HWC +already use, making beneficiary records portable across services without any change to the +mobile app. + +--- + +## 2. What was implemented + +### 2.1 Visit layer — `t_benvisitdetail` + +**Problem:** Stop TB used its own `tb_stoptb_visit` mirror table. `benVisitID` was null in all +clinical tables. HWC/MMU had no visit anchor to join on. + +**Fix:** `TBStopVisitServiceImpl` was completely rewritten to create rows in `t_benvisitdetail` +instead of `tb_stoptb_visit`. The old entity and repo were deleted. + +**Key behaviour (get-or-create-for-today):** +- First save of the day → creates a new `t_benvisitdetail` row +- Subsequent saves same day → finds and reuses the same row +- Next camp day → creates a new row (separate visit, history preserved) + +**VisitCode formula (same as MMU):** +``` +visitCode = "1" + vanID(zero-padded 5 digits) + benVisitId(zero-padded 8 digits) +Example: vanID=1, benVisitId=42 → 10000100000042 +``` + +**Files changed:** +- `TBStopVisitService.java` — interface return type changed to `BenVisitDetail` +- `TBStopVisitServiceImpl.java` — full rewrite, uses `BenVisitDetailsRepo` +- `BenVisitDetailsRepo.java` — added `findStopTBVisitForToday()` and `countStopTBVisits()` +- `TBStopVisit.java` — **DELETED** (orphaned) +- `TBStopVisitRepo.java` — **DELETED** (orphaned) + +--- + +### 2.2 Standard table dual-writes + +All dual-writes happen inside `StopTBServiceImpl`. Custom `tb_stoptb_*` tables continue to +be written unchanged. Standard table writes are **additive and insert-only** (no edit +capability exists in the mobile). + +Each write is wrapped in its own try/catch — a standard-table failure never breaks the +primary Stop TB save. + +#### `saveGeneralExamination()` writes to: + +| Standard Table | Data source | Fields written | +|---|---|---| +| `t_phy_anthropometry` | `i_beneficiarydetails.otherFields` (registration) | `Height_cm`, `Weight_Kg`, `BMI` | +| `t_phy_vitals` | otherFields + request | `Temperature` (from otherFields), `PulseRate`, `SystolicBP_1stReading`, `DiastolicBP_1stReading`, `BloodGlucose_Random` | +| `t_Phy_GeneralExam` | request | `Pallor`, `Jaundice` (←icterus), `Cyanosis`, `Clubbing`, `Lymphadenopathy`, `Edema` (←oedema) | +| `t_benreferdetails` | request | written only when `referralToHWCNeeded = true` | + +> **Note:** Height/weight/bmi/temperature come from registration `otherFields` because the +> mobile's `GeneralExaminationSaveRequest` does not include these fields. The mobile hardcodes +> them null in `VitalScreenFragment.saveVitals()`. They are captured at registration and synced +> to Identity-API via TM-API. + +#### `saveGeneralOpd()` writes to: + +| Standard Table | Fields written | +|---|---| +| `t_benchiefcomplaint` | `ChiefComplaint` (free text from request) | +| `t_prescription` | `Instruction` (from `notes` field) | +| `t_prescribeddrug` | `GenericDrugName` (medication), `Dose`, `Frequency`, `Duration`, linked to `PrescriptionID` | + +All 7 standard tables share the same `benVisitID` and `VisitCode` as the join key. + +--- + +### 2.3 New entities and repos created + +All in package `com.iemr.flw.domain.iemr` / `com.iemr.flw.repo.iemr`: + +| Entity | Table | Repo | +|---|---|---| +| `BenAnthropometryDetail` | `t_phy_anthropometry` | `BenAnthropometryRepo` | +| `BenPhysicalVitalDetail` | `t_phy_vitals` | `BenPhysicalVitalRepo` | +| `PhyGeneralExamination` | `t_Phy_GeneralExam` | `PhyGeneralExaminationRepo` | +| `BenChiefComplaint` | `t_benchiefcomplaint` | `BenChiefComplaintRepo` | +| `PrescriptionDetail` | `t_prescription` | `PrescriptionDetailRepo` | +| `PrescribedDrugDetail` | `t_prescribeddrug` | `PrescribedDrugDetailRepo` | + +All 6 standard tables already existed in `db_iemr`. No CREATE TABLE needed for these. +`BenReferDetails` and `BenReferDetailsRepo` already existed in FLW-API — just wired up. + +--- + +### 2.4 Service files updated + +All Stop TB service files that previously used `TBStopVisit` and `visit.getId()` were updated +to use `BenVisitDetail` and `visit.getVisitCode()`: + +| File | Change | +|---|---| +| `StopTBServiceImpl` | `saveGeneralExamination`, `saveNurseTBScreening`, `saveGeneralOpd` — all updated | +| `TBSuspectedServiceImpl` | `save()` — updated | +| `TBConfirmedCaseServiceImpl` | `save()` — updated | + +The `visitCode` column on `tb_stoptb_suspected` and `tb_stoptb_confirmed_cases` is still used +for per-visit keying. The value written is now the 14-digit standard visitCode instead of the +old `tb_stoptb_visit` PK. + +--- + +## 3. DB migration required + +Run these two statements on the server. The standard tables (`t_phy_anthropometry` etc.) +already exist in `db_iemr` — no CREATE needed for them. + +```sql +-- Required: visitCode column added to these two tables when per-visit history was introduced +ALTER TABLE tb_stoptb_suspected ADD COLUMN visitCode BIGINT; +ALTER TABLE tb_stoptb_confirmed_cases ADD COLUMN visitCode BIGINT; + +-- DO NOT run: tb_stoptb_visit is replaced by t_benvisitdetail +-- CREATE TABLE tb_stoptb_visit ← SKIP THIS +``` + +--- + +## 4. BenFlowStatus (`i_ben_flow_outreach`) — current state + +| Flag | Set when | Value | +|---|---|---| +| `nurseFlag` | Registration | `1` (pending for nurse) | +| `nurseFlag` | `submitNurseData()` | `9` (nurse done) | +| `doctorFlag` | `routeToStopTBCounsellor()` | `1` (pending for counsellor) | + +**Known gap:** Mobile does not call `flw-api/stoptb/nurse/submit` (app is live, cannot change). +So `nurseFlag` stays at `1` forever after registration. + +**Why this is not breaking:** The mobile also does not call the nurse worklist or registrar +worklist endpoints — both are commented out in `AmritApiService.kt`. The mobile uses +`flw-api/beneficiary/getBeneficiaryData` instead and manages its own local state. + +**Counsellor:** Finds confirmed TB cases via direct `tb_confirmed_cases` query, not via +`doctorFlag`. This works correctly today. + +**Future action (when web dashboard is built):** Wire up `nurseFlag` advancement by checking +all 3 mandatory nurse saves (generalExamination + tbScreening + generalOpd) exist for the +same `visitCode`, then auto-advance `nurseFlag = 9`. The query +`updateStopTBAfterNurseSubmit()` in `BenFlowStatusRepo` already exists for this. + +--- + +## 5. Complete mobile API list (live, as of 2026-06-30) + +### Auth +| Endpoint | +|---| +| `POST common-api/user/userAuthenticate` | +| `POST common-api/user/refreshToken` | +| `GET flw-api/user/getUserDetail` | +| `POST common-api/firebaseNotification/userToken` | + +### Beneficiary Registration +| Endpoint | +|---| +| `POST tm-api/registrar/registrarBeneficaryRegistrationNew` | +| `POST identity-api/rmnch/syncDataToAmrit` | +| `POST flw-api/beneficiary/getBeneficiaryData` | + +### Consent / OTP +| Endpoint | +|---| +| `POST common-api/beneficiaryConsent/sendConsent` | +| `POST common-api/beneficiaryConsent/resendConsent` | +| `POST common-api/beneficiaryConsent/validateConsent` | + +### Stop TB — Nurse +| Endpoint | +|---| +| `POST flw-api/stoptb/nurse/generalExamination/save` | +| `POST flw-api/stoptb/nurse/generalExamination/getAll` | +| `POST flw-api/stoptb/nurse/tbScreening/save` | +| `POST flw-api/stoptb/nurse/tbScreening/getAll` | +| `POST flw-api/stoptb/nurse/generalOpd/save` | +| `POST flw-api/stoptb/nurse/generalOpd/getAll` | +| `POST flw-api/stoptb/nurse/diagnostics/save` | +| `POST flw-api/stoptb/nurse/diagnostics/getAll` | + +### Stop TB — Suspected / Confirmed +| Endpoint | +|---| +| `POST flw-api/tb/suspected/saveAll` | +| `POST flw-api/tb/suspected/getAll` | +| `POST flw-api/tb/confirmed/save` | +| `GET flw-api/tb/confirmed/getAll` | + +### Counselling (Dynamic Form) +| Endpoint | +|---| +| `GET flw-api/dynamicForm/getAllForms` | +| `GET common-api/dynamicForm/form/{formId}/fields` | +| `POST flw-api/counselling/save` | +| `POST flw-api/dynamicForm/response/submitBulk` | +| `POST flw-api/dynamicForm/response/complete` | +| `GET flw-api/dynamicForm/response/getByBeneficiary` | +| `GET flw-api/dynamicForm/response/getCompletedBeneficiaries` | + +### CBAC / NCD +| Endpoint | +|---| +| `POST hwc-api/NCD/getByUserCbacDetails` | +| `POST hwc-api/NCD/save/nurseData` | +| `POST hwc-api/NCD/save/referDetails` | + +### ABHA / Health ID +| Endpoint | +|---| +| `POST fhir-api/healthIDWithUID/createHealthIDWithUID` | +| `POST fhir-api/healthID/getBenhealthID` | +| `POST fhir-api/healthIDRecord/mapHealthIDToBeneficiary` | +| `POST fhir-api/healthIDRecord/addHealthIdRecord` | +| `POST fhir-api/healthIDCard/generateOTP` | +| `POST fhir-api/healthIDCard/verifyOTPAndGenerateHealthCard` | + +### Other Disease Screens +| Endpoint | +|---| +| `POST flw-api/disease/kalaAzar/saveAll` | +| `POST flw-api/disease/malaria/saveAll` | +| `POST flw-api/disease/leprosy/saveAll` | +| `POST flw-api/disease/leprosy/followUp/saveAll` | +| `POST flw-api/disease/leprosy/followUp/getAll` | +| `POST flw-api/disease/aesJe/saveAll` | +| `POST flw-api/disease/filaria/saveAll` | +| `POST flw-api/disease/getAllDisease` | +| `POST flw-api/follow-up/save` | +| `POST flw-api/follow-up/get` | +| `POST flw-api/child-care/hbncVisit/saveAll` | +| `POST flw-api/disease/cdtfVisit/saveAll` | +| `POST flw-api/disease/cdtfVisit/getAll` | + +### Commented out / never called (documented for future reference) +| Endpoint | Reason | +|---|---| +| `POST flw-api/stoptb/nurse/worklist` | Mobile uses getBeneficiaryData instead | +| `POST flw-api/stoptb/registrar/worklist` | Mobile uses getBeneficiaryData instead | +| `POST flw-api/stoptb/nurse/submit` | Mobile is live — cannot add new call; nurseFlag stays 1 (acceptable, see Section 4) | +| `POST hwc-api/common/getBenReferDetailsByCreatedBy` | Referral pull not needed in Stop TB | +| `POST hwc-api/sync/generalOPDNurseFormDataToServer` | Abandoned earlier attempt at standard-table integration | +| `POST identity-api/id/getByBenId` | getBenHealthID used instead | + +--- + +## 6. What HWC can now see + +Once the DB migration SQL is run and FLW-API is deployed, any service reading from `db_iemr` +standard tables will find Stop TB beneficiary data joined by `benVisitID` / `VisitCode`: + +| Table | Content | +|---|---| +| `t_benvisitdetail` | One row per camp day, `visitCategory = 'Stop TB'` | +| `t_phy_anthropometry` | Height, weight, BMI (from registration) | +| `t_phy_vitals` | Temperature (from registration), pulse, BP, RBS (from nurse exam) | +| `t_Phy_GeneralExam` | Pallor, jaundice, cyanosis, clubbing, lymphadenopathy, edema | +| `t_benchiefcomplaint` | Chief complaint text | +| `t_prescription` | Notes/instructions | +| `t_prescribeddrug` | Medication, dose, frequency, duration | +| `t_benreferdetails` | HWC referral (only when `referralToHWCNeeded = true`) | + +> **Note:** HWC's referral-receiving worklist endpoints (`getBenPreviousReferralHistoryDetails`, +> `getBenReferDetailsByCreatedBy`) have zero callers on HWC-UI and HWC-Mobile-App as of today. +> Writing `t_benreferdetails` is correct for audit/reporting. Surfacing it as a live HWC +> worklist is a separate, future piece of work. + +--- + +## 7. What is NOT done (future scope) + +| Item | Detail | +|---|---| +| `nurseFlag` auto-advancement | Needs web dashboard to be useful. Logic: check all 3 saves (exam + screening + OPD) exist for same `visitCode` → set `nurseFlag=9`. Query already exists in `BenFlowStatusRepo.updateStopTBAfterNurseSubmit()` | +| HWC worklist for Stop TB referrals | HWC-API and HWC-UI changes needed to surface `t_benreferdetails` rows | +| Duplicate beneficiary detection | Requires new mobile screen — no server-side gap | +| Beneficiary edit/update | Requires new mobile screen — no edit endpoint exists anywhere in Stop TB | +| `height`/`weight`/`bmi`/`respiratoryRate`/`spo2` in nurse exam | Mobile captures these locally (`VitalCache`) but `GeneralExaminationSaveRequest.from()` hardcodes them null. Would require mobile change to send live | diff --git a/src/main/java/com/iemr/flw/domain/iemr/BenAnthropometryDetail.java b/src/main/java/com/iemr/flw/domain/iemr/BenAnthropometryDetail.java new file mode 100644 index 00000000..777e8011 --- /dev/null +++ b/src/main/java/com/iemr/flw/domain/iemr/BenAnthropometryDetail.java @@ -0,0 +1,70 @@ +package com.iemr.flw.domain.iemr; + +import jakarta.persistence.*; +import lombok.Data; +import java.sql.Timestamp; + +@Entity +@Table(name = "t_phy_anthropometry", schema = "db_iemr") +@Data +public class BenAnthropometryDetail { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "ID") + private Long id; + + @Column(name = "BeneficiaryRegID") + private Long beneficiaryRegID; + + @Column(name = "BenVisitID") + private Long benVisitID; + + @Column(name = "ProviderServiceMapID") + private Integer providerServiceMapID; + + @Column(name = "VisitCode") + private Long visitCode; + + @Column(name = "Weight_Kg") + private Double weightKg; + + @Column(name = "Height_cm") + private Double heightCm; + + @Column(name = "BMI") + private Double bmi; + + @Column(name = "Deleted", insertable = false, updatable = true) + private Boolean deleted; + + @Column(name = "Processed", insertable = false, updatable = true) + private String processed; + + @Column(name = "CreatedBy") + private String createdBy; + + @Column(name = "CreatedDate", insertable = false, updatable = false) + private Timestamp createdDate; + + @Column(name = "ModifiedBy") + private String modifiedBy; + + @Column(name = "LastModDate", insertable = false, updatable = false) + private Timestamp lastModDate; + + @Column(name = "VanSerialNo") + private Long vanSerialNo; + + @Column(name = "VanID") + private Integer vanID; + + @Column(name = "ParkingPlaceID") + private Integer parkingPlaceID; + + @Column(name = "SyncedBy") + private String syncedBy; + + @Column(name = "SyncedDate") + private Timestamp syncedDate; +} diff --git a/src/main/java/com/iemr/flw/domain/iemr/BenChiefComplaint.java b/src/main/java/com/iemr/flw/domain/iemr/BenChiefComplaint.java new file mode 100644 index 00000000..3b016c5f --- /dev/null +++ b/src/main/java/com/iemr/flw/domain/iemr/BenChiefComplaint.java @@ -0,0 +1,73 @@ +package com.iemr.flw.domain.iemr; + +import jakarta.persistence.*; +import lombok.Data; +import java.sql.Timestamp; + +@Entity +@Table(name = "t_benchiefcomplaint", schema = "db_iemr") +@Data +public class BenChiefComplaint { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "ID") + private Long id; + + @Column(name = "BeneficiaryRegID") + private Long beneficiaryRegID; + + @Column(name = "BenVisitID") + private Long benVisitID; + + @Column(name = "ProviderServiceMapID") + private Integer providerServiceMapID; + + @Column(name = "VisitCode") + private Long visitCode; + + @Column(name = "ChiefComplaint") + private String chiefComplaint; + + @Column(name = "Duration") + private Integer duration; + + @Column(name = "UnitOfDuration") + private String unitOfDuration; + + @Column(name = "Description") + private String description; + + @Column(name = "Deleted", insertable = false, updatable = true) + private Boolean deleted; + + @Column(name = "Processed", insertable = false, updatable = true) + private String processed; + + @Column(name = "CreatedBy") + private String createdBy; + + @Column(name = "CreatedDate", insertable = false, updatable = false) + private Timestamp createdDate; + + @Column(name = "ModifiedBy") + private String modifiedBy; + + @Column(name = "LastModDate", insertable = false, updatable = false) + private Timestamp lastModDate; + + @Column(name = "VanSerialNo") + private Long vanSerialNo; + + @Column(name = "VanID") + private Integer vanID; + + @Column(name = "ParkingPlaceID") + private Integer parkingPlaceID; + + @Column(name = "SyncedBy") + private String syncedBy; + + @Column(name = "SyncedDate") + private Timestamp syncedDate; +} diff --git a/src/main/java/com/iemr/flw/domain/iemr/BenPhysicalVitalDetail.java b/src/main/java/com/iemr/flw/domain/iemr/BenPhysicalVitalDetail.java new file mode 100644 index 00000000..15b03aee --- /dev/null +++ b/src/main/java/com/iemr/flw/domain/iemr/BenPhysicalVitalDetail.java @@ -0,0 +1,76 @@ +package com.iemr.flw.domain.iemr; + +import jakarta.persistence.*; +import lombok.Data; +import java.sql.Timestamp; + +@Entity +@Table(name = "t_phy_vitals", schema = "db_iemr") +@Data +public class BenPhysicalVitalDetail { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "ID") + private Long id; + + @Column(name = "BeneficiaryRegID") + private Long beneficiaryRegID; + + @Column(name = "BenVisitID") + private Long benVisitID; + + @Column(name = "ProviderServiceMapID") + private Integer providerServiceMapID; + + @Column(name = "VisitCode") + private Long visitCode; + + @Column(name = "Temperature") + private Double temperature; + + @Column(name = "PulseRate") + private Short pulseRate; + + @Column(name = "SystolicBP_1stReading") + private Short systolicBP; + + @Column(name = "DiastolicBP_1stReading") + private Short diastolicBP; + + @Column(name = "BloodGlucose_Random") + private Short bloodGlucoseRandom; + + @Column(name = "Deleted", insertable = false, updatable = true) + private Boolean deleted; + + @Column(name = "Processed", insertable = false, updatable = true) + private String processed; + + @Column(name = "CreatedBy") + private String createdBy; + + @Column(name = "CreatedDate", insertable = false, updatable = false) + private Timestamp createdDate; + + @Column(name = "ModifiedBy") + private String modifiedBy; + + @Column(name = "LastModDate", insertable = false, updatable = false) + private Timestamp lastModDate; + + @Column(name = "VanSerialNo") + private Long vanSerialNo; + + @Column(name = "VanID") + private Integer vanID; + + @Column(name = "ParkingPlaceID") + private Integer parkingPlaceID; + + @Column(name = "SyncedBy") + private String syncedBy; + + @Column(name = "SyncedDate") + private Timestamp syncedDate; +} diff --git a/src/main/java/com/iemr/flw/domain/iemr/PhyGeneralExamination.java b/src/main/java/com/iemr/flw/domain/iemr/PhyGeneralExamination.java new file mode 100644 index 00000000..b99fd804 --- /dev/null +++ b/src/main/java/com/iemr/flw/domain/iemr/PhyGeneralExamination.java @@ -0,0 +1,79 @@ +package com.iemr.flw.domain.iemr; + +import jakarta.persistence.*; +import lombok.Data; +import java.sql.Timestamp; + +@Entity +@Table(name = "t_Phy_GeneralExam", schema = "db_iemr") +@Data +public class PhyGeneralExamination { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "ID") + private Long id; + + @Column(name = "BeneficiaryRegID") + private Long beneficiaryRegID; + + @Column(name = "BenVisitID") + private Long benVisitID; + + @Column(name = "ProviderServiceMapID") + private Integer providerServiceMapID; + + @Column(name = "VisitCode") + private Long visitCode; + + @Column(name = "Pallor") + private String pallor; + + @Column(name = "Jaundice") + private String jaundice; + + @Column(name = "Cyanosis") + private String cyanosis; + + @Column(name = "Clubbing") + private String clubbing; + + @Column(name = "Lymphadenopathy") + private String lymphadenopathy; + + @Column(name = "Edema") + private String edema; + + @Column(name = "Deleted", insertable = false, updatable = true) + private Boolean deleted; + + @Column(name = "Processed", insertable = false, updatable = true) + private String processed; + + @Column(name = "CreatedBy") + private String createdBy; + + @Column(name = "CreatedDate", insertable = false, updatable = false) + private Timestamp createdDate; + + @Column(name = "ModifiedBy") + private String modifiedBy; + + @Column(name = "LastModDate", insertable = false, updatable = false) + private Timestamp lastModDate; + + @Column(name = "VanSerialNo") + private Long vanSerialNo; + + @Column(name = "vanID") + private Integer vanID; + + @Column(name = "ParkingPlaceID") + private Integer parkingPlaceID; + + @Column(name = "SyncedBy") + private String syncedBy; + + @Column(name = "SyncedDate") + private Timestamp syncedDate; +} diff --git a/src/main/java/com/iemr/flw/domain/iemr/PrescribedDrugDetail.java b/src/main/java/com/iemr/flw/domain/iemr/PrescribedDrugDetail.java new file mode 100644 index 00000000..9a0a1502 --- /dev/null +++ b/src/main/java/com/iemr/flw/domain/iemr/PrescribedDrugDetail.java @@ -0,0 +1,82 @@ +package com.iemr.flw.domain.iemr; + +import jakarta.persistence.*; +import lombok.Data; +import java.sql.Timestamp; + +@Entity +@Table(name = "t_prescribeddrug", schema = "db_iemr") +@Data +public class PrescribedDrugDetail { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "PrescribedDrugID", insertable = false, updatable = false) + private Long id; + + @Column(name = "BeneficiaryRegID") + private Long beneficiaryRegID; + + @Column(name = "BenVisitID") + private Long benVisitID; + + @Column(name = "ProviderServiceMapID") + private Integer providerServiceMapID; + + @Column(name = "VisitCode") + private Long visitCode; + + @Column(name = "PrescriptionID") + private Long prescriptionID; + + @Column(name = "GenericDrugName") + private String drugName; + + @Column(name = "Dose") + private String dose; + + @Column(name = "Frequency") + private String frequency; + + @Column(name = "Duration") + private String duration; + + @Column(name = "DuartionUnit") + private String durationUnit; + + @Column(name = "SpecialInstruction") + private String specialInstruction; + + @Column(name = "Deleted", insertable = false, updatable = true) + private Boolean deleted; + + @Column(name = "Processed", insertable = false, updatable = true) + private String processed; + + @Column(name = "CreatedBy") + private String createdBy; + + @Column(name = "CreatedDate", insertable = false, updatable = false) + private Timestamp createdDate; + + @Column(name = "ModifiedBy") + private String modifiedBy; + + @Column(name = "LastModDate", insertable = false, updatable = false) + private Timestamp lastModDate; + + @Column(name = "VanSerialNo") + private Long vanSerialNo; + + @Column(name = "VanID") + private Integer vanID; + + @Column(name = "ParkingPlaceID") + private Integer parkingPlaceID; + + @Column(name = "SyncedBy") + private String syncedBy; + + @Column(name = "SyncedDate") + private Timestamp syncedDate; +} diff --git a/src/main/java/com/iemr/flw/domain/iemr/PrescriptionDetail.java b/src/main/java/com/iemr/flw/domain/iemr/PrescriptionDetail.java new file mode 100644 index 00000000..5bc5295d --- /dev/null +++ b/src/main/java/com/iemr/flw/domain/iemr/PrescriptionDetail.java @@ -0,0 +1,70 @@ +package com.iemr.flw.domain.iemr; + +import jakarta.persistence.*; +import lombok.Data; +import java.sql.Timestamp; + +@Entity +@Table(name = "t_prescription", schema = "db_iemr") +@Data +public class PrescriptionDetail { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "PrescriptionID", insertable = false, updatable = false) + private Long prescriptionID; + + @Column(name = "BeneficiaryRegID") + private Long beneficiaryRegID; + + @Column(name = "BenVisitID") + private Long benVisitID; + + @Column(name = "ProviderServiceMapID") + private Integer providerServiceMapID; + + @Column(name = "VisitCode") + private Long visitCode; + + @Column(name = "DiagnosisProvided") + private String diagnosisProvided; + + @Column(name = "Instruction") + private String instruction; + + @Column(name = "Remarks") + private String remarks; + + @Column(name = "Deleted", insertable = false, updatable = true) + private Boolean deleted; + + @Column(name = "Processed", insertable = false, updatable = true) + private String processed; + + @Column(name = "CreatedBy") + private String createdBy; + + @Column(name = "CreatedDate", insertable = false, updatable = false) + private Timestamp createdDate; + + @Column(name = "ModifiedBy") + private String modifiedBy; + + @Column(name = "LastModDate", insertable = false, updatable = false) + private Timestamp lastModDate; + + @Column(name = "VanSerialNo") + private Long vanSerialNo; + + @Column(name = "VanID") + private Integer vanID; + + @Column(name = "ParkingPlaceID") + private Integer parkingPlaceID; + + @Column(name = "SyncedBy") + private String syncedBy; + + @Column(name = "SyncedDate") + private Timestamp syncedDate; +} diff --git a/src/main/java/com/iemr/flw/domain/iemr/TBStopVisit.java b/src/main/java/com/iemr/flw/domain/iemr/TBStopVisit.java deleted file mode 100644 index 8e7f0e20..00000000 --- a/src/main/java/com/iemr/flw/domain/iemr/TBStopVisit.java +++ /dev/null @@ -1,77 +0,0 @@ -package com.iemr.flw.domain.iemr; - -import lombok.Data; -import jakarta.persistence.*; -import org.hibernate.annotations.CreationTimestamp; - -import java.sql.Timestamp; - -/** - * One row per real-world Stop TB camp encounter (a beneficiary seen on a given calendar day). - * Mirrors MMU's t_benvisitdetail (visitNo, insert-only history) but scoped entirely to Stop TB's - * own tables — never written to or read from MMU/HWC's shared visit/outreach infrastructure. - */ -@Entity -@Table(name = "tb_stoptb_visit", schema = "db_iemr") -@Data -public class TBStopVisit { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(name = "beneficiary_reg_id") - private Long beneficiaryRegID; - - @Column(name = "visit_no") - private Integer visitNo; - - @CreationTimestamp - @Column(name = "visit_date", updatable = false) - private Timestamp visitDate; - - @Column(name = "provider_service_map_id") - private Integer providerServiceMapID; - - @Column(name = "created_by") - private String createdBy; - - // Role-handoff flags — only nurseCompletedFlag is populated today (Stop TB has no - // Doctor/Counsellor screens yet). Reserved so a future worklist can query against - // this table without a schema change later. - @Column(name = "nurse_completed_flag") - private String nurseCompletedFlag = "N"; - - @Column(name = "doctor_completed_flag") - private String doctorCompletedFlag = "N"; - - @Column(name = "counsellor_completed_flag") - private String counsellorCompletedFlag = "N"; - - // Sync fields — same vanID/vanSerialNo/processed discipline as every other Stop TB table, - // required so two camp laptops can each generate local visit rows without colliding at - // central (de-duplication key is (vanSerialNo, vanID), not the local auto-increment id). - @Column(name = "vanID") - private Integer vanID; - - @Column(name = "parkingPlaceID") - private Integer parkingPlaceID; - - @Column(name = "processed") - private String processed = "N"; - - @Column(name = "vanSerialNo") - private Long vanSerialNo; - - // Stamped by the generic local-to-central DataSyncRepository job (MMU/HWC/TM-API convention) - // when this row is actually pushed — required for tb_stoptb_visit to be syncable by that - // same generic mechanism without code changes there. - @Column(name = "SyncedDate") - private Timestamp syncedDate; - - @Column(name = "Syncedby") - private String syncedby; - - @Column(name = "SyncFailureReason") - private String syncFailureReason; -} diff --git a/src/main/java/com/iemr/flw/repo/iemr/BenAnthropometryRepo.java b/src/main/java/com/iemr/flw/repo/iemr/BenAnthropometryRepo.java new file mode 100644 index 00000000..b7c9efd3 --- /dev/null +++ b/src/main/java/com/iemr/flw/repo/iemr/BenAnthropometryRepo.java @@ -0,0 +1,9 @@ +package com.iemr.flw.repo.iemr; + +import com.iemr.flw.domain.iemr.BenAnthropometryDetail; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface BenAnthropometryRepo extends JpaRepository { +} diff --git a/src/main/java/com/iemr/flw/repo/iemr/BenChiefComplaintRepo.java b/src/main/java/com/iemr/flw/repo/iemr/BenChiefComplaintRepo.java new file mode 100644 index 00000000..47a73f98 --- /dev/null +++ b/src/main/java/com/iemr/flw/repo/iemr/BenChiefComplaintRepo.java @@ -0,0 +1,9 @@ +package com.iemr.flw.repo.iemr; + +import com.iemr.flw.domain.iemr.BenChiefComplaint; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface BenChiefComplaintRepo extends JpaRepository { +} diff --git a/src/main/java/com/iemr/flw/repo/iemr/BenPhysicalVitalRepo.java b/src/main/java/com/iemr/flw/repo/iemr/BenPhysicalVitalRepo.java new file mode 100644 index 00000000..cf1ed35b --- /dev/null +++ b/src/main/java/com/iemr/flw/repo/iemr/BenPhysicalVitalRepo.java @@ -0,0 +1,9 @@ +package com.iemr.flw.repo.iemr; + +import com.iemr.flw.domain.iemr.BenPhysicalVitalDetail; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface BenPhysicalVitalRepo extends JpaRepository { +} diff --git a/src/main/java/com/iemr/flw/repo/iemr/BenVisitDetailsRepo.java b/src/main/java/com/iemr/flw/repo/iemr/BenVisitDetailsRepo.java index dfd605ef..47aacb2b 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/BenVisitDetailsRepo.java +++ b/src/main/java/com/iemr/flw/repo/iemr/BenVisitDetailsRepo.java @@ -2,10 +2,26 @@ import com.iemr.flw.domain.iemr.BenVisitDetail; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import java.sql.Timestamp; import java.util.List; @Repository public interface BenVisitDetailsRepo extends JpaRepository { - public List findByBeneficiaryRegId(Long benRegID);} + + List findByBeneficiaryRegId(Long benRegID); + + @Query("SELECT v FROM BenVisitDetail v WHERE v.beneficiaryRegId = :benRegID " + + "AND v.visitCategory = 'Stop TB' " + + "AND v.visitDateTime >= :dayStart AND v.visitDateTime < :dayEnd") + BenVisitDetail findStopTBVisitForToday(@Param("benRegID") Long benRegID, + @Param("dayStart") Timestamp dayStart, + @Param("dayEnd") Timestamp dayEnd); + + @Query("SELECT COUNT(v) FROM BenVisitDetail v WHERE v.beneficiaryRegId = :benRegID " + + "AND v.visitCategory = 'Stop TB'") + Integer countStopTBVisits(@Param("benRegID") Long benRegID); +} diff --git a/src/main/java/com/iemr/flw/repo/iemr/PhyGeneralExaminationRepo.java b/src/main/java/com/iemr/flw/repo/iemr/PhyGeneralExaminationRepo.java new file mode 100644 index 00000000..abd73b0b --- /dev/null +++ b/src/main/java/com/iemr/flw/repo/iemr/PhyGeneralExaminationRepo.java @@ -0,0 +1,9 @@ +package com.iemr.flw.repo.iemr; + +import com.iemr.flw.domain.iemr.PhyGeneralExamination; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface PhyGeneralExaminationRepo extends JpaRepository { +} diff --git a/src/main/java/com/iemr/flw/repo/iemr/PrescribedDrugDetailRepo.java b/src/main/java/com/iemr/flw/repo/iemr/PrescribedDrugDetailRepo.java new file mode 100644 index 00000000..bcdd8ab5 --- /dev/null +++ b/src/main/java/com/iemr/flw/repo/iemr/PrescribedDrugDetailRepo.java @@ -0,0 +1,9 @@ +package com.iemr.flw.repo.iemr; + +import com.iemr.flw.domain.iemr.PrescribedDrugDetail; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface PrescribedDrugDetailRepo extends JpaRepository { +} diff --git a/src/main/java/com/iemr/flw/repo/iemr/PrescriptionDetailRepo.java b/src/main/java/com/iemr/flw/repo/iemr/PrescriptionDetailRepo.java new file mode 100644 index 00000000..c0d425ec --- /dev/null +++ b/src/main/java/com/iemr/flw/repo/iemr/PrescriptionDetailRepo.java @@ -0,0 +1,9 @@ +package com.iemr.flw.repo.iemr; + +import com.iemr.flw.domain.iemr.PrescriptionDetail; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface PrescriptionDetailRepo extends JpaRepository { +} diff --git a/src/main/java/com/iemr/flw/repo/iemr/TBStopVisitRepo.java b/src/main/java/com/iemr/flw/repo/iemr/TBStopVisitRepo.java deleted file mode 100644 index 51791436..00000000 --- a/src/main/java/com/iemr/flw/repo/iemr/TBStopVisitRepo.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.iemr.flw.repo.iemr; - -import com.iemr.flw.domain.iemr.TBStopVisit; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Modifying; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; -import org.springframework.stereotype.Repository; -import org.springframework.transaction.annotation.Transactional; - -import java.sql.Timestamp; - -@Repository -public interface TBStopVisitRepo extends JpaRepository { - - @Query("SELECT v FROM TBStopVisit v WHERE v.beneficiaryRegID = :beneficiaryRegID " + - "AND v.visitDate BETWEEN :start AND :end") - TBStopVisit findByBeneficiaryRegIDAndVisitDateBetween(@Param("beneficiaryRegID") Long beneficiaryRegID, - @Param("start") Timestamp start, - @Param("end") Timestamp end); - - @Query("SELECT COUNT(v) FROM TBStopVisit v WHERE v.beneficiaryRegID = :beneficiaryRegID") - Integer getVisitCountForBeneficiary(@Param("beneficiaryRegID") Long beneficiaryRegID); - - @Transactional - @Modifying - @Query("UPDATE TBStopVisit t SET t.vanSerialNo = t.id WHERE t.id = :id") - void updateVanSerialNo(@Param("id") Long id); -} diff --git a/src/main/java/com/iemr/flw/service/TBStopVisitService.java b/src/main/java/com/iemr/flw/service/TBStopVisitService.java index ec1d40e3..0e15fd9b 100644 --- a/src/main/java/com/iemr/flw/service/TBStopVisitService.java +++ b/src/main/java/com/iemr/flw/service/TBStopVisitService.java @@ -1,15 +1,9 @@ package com.iemr.flw.service; -import com.iemr.flw.domain.iemr.TBStopVisit; +import com.iemr.flw.domain.iemr.BenVisitDetail; public interface TBStopVisitService { - /** - * Returns today's visit row for this beneficiary, creating one (with visitNo = previous - * count + 1) if none exists yet for the current calendar day. No mobile change required — - * "new visit" is inferred purely from the save timestamp, same pattern MMU uses - * (count existing visits, increment, insert-only — never update an existing visit row). - */ - TBStopVisit getOrCreateVisitForToday(Long beneficiaryRegID, Integer providerServiceMapID, String createdBy, - Integer vanID, Integer parkingPlaceID); + BenVisitDetail getOrCreateVisitForToday(Long beneficiaryRegID, Integer providerServiceMapID, + String createdBy, Integer vanID, Integer parkingPlaceID); } diff --git a/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java index 4c1c3c4d..f6b26d4a 100644 --- a/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java @@ -37,6 +37,7 @@ import com.iemr.flw.dto.iemr.SectionResponseDTO; import com.iemr.flw.domain.iemr.DynamicForm; import com.iemr.flw.masterEnum.FormResponseStatus; +import com.iemr.flw.masterEnum.FormResponseStatus; import com.iemr.flw.masterEnum.FormType; import com.iemr.flw.repo.iemr.DynamicFormRepo; import com.iemr.flw.repo.iemr.FormResponseRepo; @@ -48,6 +49,7 @@ import com.iemr.flw.repo.iemr.SectionResponseRepo; import com.iemr.flw.masterEnum.QuestionType; import com.iemr.flw.masterEnum.SectionPhase; +import com.iemr.flw.service.CampConfigService; import com.iemr.flw.service.DynamicFormResponseService; import com.iemr.flw.utils.JwtUtil; import com.iemr.flw.utils.exception.IEMRException; @@ -94,10 +96,13 @@ public class DynamicFormResponseServiceImpl implements DynamicFormResponseServic private final QuestionOptionRepo questionOptionRepo; private final FormResponseItemSaver itemSaver; private final JwtUtil jwtUtil; + private final CampConfigService campConfigService; @Override @Transactional public FormResponseDTO submitForm(FormResponseRequest request) { + Integer vanID = campConfigService.getVanID(); + Integer parkingPlaceID = campConfigService.getParkingPlaceID(); FormVersion version = resolveLatestVersion(request.getFormUuid()); Timestamp now = new Timestamp(System.currentTimeMillis()); FormResponse formResponse; @@ -116,15 +121,17 @@ public FormResponseDTO submitForm(FormResponseRequest request) { formResponse.setLastFollowUpAt(now); formResponse = formResponseRepo.save(formResponse); } else { - formResponse = createFormResponse(request, version, FormResponseStatus.SUBMITTED.name(), now); + formResponse = createFormResponse(request, version, FormResponseStatus.SUBMITTED.name(), now, vanID, parkingPlaceID); } - return processSections(formResponse, request, version, null, false); + return processSections(formResponse, request, version, null, false, vanID, parkingPlaceID); } @Override @Transactional public FormResponseDTO completeForm(FormResponseRequest request, String jwtToken) throws IEMRException { String actor = jwtUtil.extractUserId(jwtToken).toString(); + Integer vanID = campConfigService.getVanID(); + Integer parkingPlaceID = campConfigService.getParkingPlaceID(); FormVersion version = resolveLatestVersion(request.getFormUuid()); Long formId = version.getDynamicForm().getFormId(); @@ -133,7 +140,7 @@ public FormResponseDTO completeForm(FormResponseRequest request, String jwtToken List existing = formResponseRepo.findByBeneficiaryIdAndFormId(request.getBeneficiaryId(), formId); FormResponse formResponse = existing.isEmpty() - ? createFormResponse(request, version, FormResponseStatus.SUBMITTED.name(), now) + ? createFormResponse(request, version, FormResponseStatus.SUBMITTED.name(), now, vanID, parkingPlaceID) : existing.get(0); formResponse.setUpdatedBy(actor); @@ -142,7 +149,7 @@ public FormResponseDTO completeForm(FormResponseRequest request, String jwtToken : FormResponseStatus.COMPLETE.name()); formResponse.setCompletedAt(now); formResponse = formResponseRepo.save(formResponse); - return processSections(formResponse, request, version, actor, true); + return processSections(formResponse, request, version, actor, true, vanID, parkingPlaceID); } @Override @@ -179,7 +186,9 @@ private FormResponse createFormResponse( FormResponseRequest request, FormVersion version, String status, - Timestamp submittedAt) { + Timestamp submittedAt, + Integer vanID, + Integer parkingPlaceID) { FormResponse formResponse = FormResponse.builder() .beneficiaryId(request.getBeneficiaryId()) @@ -192,6 +201,8 @@ private FormResponse createFormResponse( // TODO: set audit fields when auth is re-enabled for /dynamicForm/response endpoints // .createdBy(jwtUtil.extractUsername(authorization)) // .updatedBy(jwtUtil.extractUsername(authorization)) + .vanID(vanID) + .parkingPlaceID(parkingPlaceID) .build(); return formResponseRepo.save(formResponse); } @@ -204,7 +215,9 @@ private FormResponseDTO processSections( FormResponseRequest request, FormVersion version, String actor, - boolean applyRefusalRule) { + boolean applyRefusalRule, + Integer vanID, + Integer parkingPlaceID) { List allSections = formSectionRepo.findByFormVersion_VersionIdOrderByDisplayOrderAsc(version.getVersionId()); @@ -251,7 +264,7 @@ private FormResponseDTO processSections( ? determineSectionStatus(section, sectionReq.getAnswers()) : SECTION_STATUS_DONE; Optional sectionResponseOpt = upsertSectionResponse( - formResponse.getResponseId(), section, actor, desiredStatus); + formResponse.getResponseId(), section, actor, desiredStatus, vanID, parkingPlaceID); if (sectionResponseOpt.isEmpty()) { continue; } @@ -270,7 +283,9 @@ private FormResponseDTO processSections( sectionReq.getAnswers(), questionMap, optionsByQuestion, - actor); + actor, + vanID, + parkingPlaceID); questionResponseRepo.saveAll(questionResponses); @@ -281,7 +296,7 @@ private FormResponseDTO processSections( } private Optional upsertSectionResponse( - Long responseId, FormSection section, String actor, String status) { + Long responseId, FormSection section, String actor, String status, Integer vanID, Integer parkingPlaceID) { Optional existing = sectionResponseRepo.findByResponseIdAndSectionId(responseId, section.getSectionId()); if (existing.isPresent()) { @@ -294,6 +309,7 @@ private Optional upsertSectionResponse( sr.setStatus(status); sr.setSavedAt(new Timestamp(System.currentTimeMillis())); sr.setUpdatedBy(actor); + if (sr.getVanID() == null) { sr.setVanID(vanID); sr.setParkingPlaceID(parkingPlaceID); } return Optional.of(sectionResponseRepo.save(sr)); } SectionResponse sr = SectionResponse.builder() @@ -303,6 +319,8 @@ private Optional upsertSectionResponse( .savedAt(new Timestamp(System.currentTimeMillis())) .createdBy(actor) .updatedBy(actor) + .vanID(vanID) + .parkingPlaceID(parkingPlaceID) .build(); return Optional.of(sectionResponseRepo.save(sr)); } @@ -344,7 +362,9 @@ private List processAnswers( List answers, Map questionMap, Map> optionsByQuestion, - String actor) { + String actor, + Integer vanID, + Integer parkingPlaceID) { List results = new ArrayList<>(); @@ -379,6 +399,8 @@ private List processAnswers( .optionId(opt != null ? opt.getOptionId() : null) .createdBy(actor) .updatedBy(actor) + .vanID(vanID) + .parkingPlaceID(parkingPlaceID) .build()); } } else if (type == QuestionType.MCQ) { @@ -392,6 +414,8 @@ private List processAnswers( .optionId(opt != null ? opt.getOptionId() : null) .createdBy(actor) .updatedBy(actor) + .vanID(vanID) + .parkingPlaceID(parkingPlaceID) .build()); } } @@ -406,6 +430,8 @@ private List processAnswers( .answerText(value) .createdBy(actor) .updatedBy(actor) + .vanID(vanID) + .parkingPlaceID(parkingPlaceID) .build()); } } diff --git a/src/main/java/com/iemr/flw/service/impl/FormResponseItemSaver.java b/src/main/java/com/iemr/flw/service/impl/FormResponseItemSaver.java index 7b965223..4767f505 100644 --- a/src/main/java/com/iemr/flw/service/impl/FormResponseItemSaver.java +++ b/src/main/java/com/iemr/flw/service/impl/FormResponseItemSaver.java @@ -44,6 +44,7 @@ import com.iemr.flw.masterEnum.FormResponseStatus; import com.iemr.flw.masterEnum.QuestionType; import com.iemr.flw.masterEnum.SectionPhase; +import com.iemr.flw.service.CampConfigService; import com.iemr.flw.utils.JwtUtil; import com.iemr.flw.utils.exception.IEMRException; import lombok.RequiredArgsConstructor; @@ -84,6 +85,7 @@ public class FormResponseItemSaver { private final SectionQuestionRepo sectionQuestionRepo; private final QuestionOptionRepo questionOptionRepo; private final JwtUtil jwtUtil; + private final CampConfigService campConfigService; /** * Saves one form response in its own independent REQUIRES_NEW transaction. @@ -93,6 +95,8 @@ public class FormResponseItemSaver { @Transactional(propagation = Propagation.REQUIRES_NEW) public FormResponseDTO saveForBulk(FormResponseRequest req, String jwtToken) throws IEMRException { String actor = jwtUtil.extractUserId(jwtToken).toString(); + Integer vanID = campConfigService.getVanID(); + Integer parkingPlaceID = campConfigService.getParkingPlaceID(); FormVersion version = resolveLatestVersion(req.getFormUuid()); Timestamp now = new Timestamp(System.currentTimeMillis()); Long formId = version.getDynamicForm().getFormId(); @@ -118,11 +122,11 @@ public FormResponseDTO saveForBulk(FormResponseRequest req, String jwtToken) thr log.info("saveForBulk: overwriting responseId={} for beneficiaryId={}", formResponse.getResponseId(), req.getBeneficiaryId()); } else { - formResponse = createFormResponse(req, version, now, actor); + formResponse = createFormResponse(req, version, now, actor, vanID, parkingPlaceID); } // Step 5: Insert fresh sections and answers - return processSections(formResponse, req, version, actor); + return processSections(formResponse, req, version, actor, vanID, parkingPlaceID); } /** Pre-flight: validates every sectionUuid and questionUuid in the request exist in the form. */ @@ -170,7 +174,8 @@ private FormVersion resolveLatestVersion(String formUuid) { "No active FormVersion found for form: " + formUuid)); } - private FormResponse createFormResponse(FormResponseRequest req, FormVersion version, Timestamp now, String actor) { + private FormResponse createFormResponse(FormResponseRequest req, FormVersion version, Timestamp now, String actor, + Integer vanID, Integer parkingPlaceID) { FormResponse formResponse = FormResponse.builder() .beneficiaryId(req.getBeneficiaryId()) .formId(version.getDynamicForm().getFormId()) @@ -181,11 +186,14 @@ private FormResponse createFormResponse(FormResponseRequest req, FormVersion ver .updatedBy(actor) .submittedAt(now) .lastFollowUpAt(now) + .vanID(vanID) + .parkingPlaceID(parkingPlaceID) .build(); return formResponseRepo.save(formResponse); } - private FormResponseDTO processSections(FormResponse formResponse, FormResponseRequest req, FormVersion version, String actor) { + private FormResponseDTO processSections(FormResponse formResponse, FormResponseRequest req, FormVersion version, String actor, + Integer vanID, Integer parkingPlaceID) { List allSections = formSectionRepo.findByFormVersion_VersionIdOrderByDisplayOrderAsc(version.getVersionId()); Map sectionByUuid = allSections.stream() @@ -240,7 +248,7 @@ private FormResponseDTO processSections(FormResponse formResponse, FormResponseR }); SectionResponse sectionResponse = upsertSectionResponse( - formResponse.getResponseId(), section.getSectionId(), actor); + formResponse.getResponseId(), section.getSectionId(), actor, vanID, parkingPlaceID); if (section.getSectionPhase() == SectionPhase.POST_SUBMIT) { formResponse.setLastFollowUpAt(sectionResponse.getSavedAt()); @@ -255,7 +263,9 @@ private FormResponseDTO processSections(FormResponse formResponse, FormResponseR sectionReq.getAnswers(), questionMap, optionsByQuestion, - actor); + actor, + vanID, + parkingPlaceID); questionResponseRepo.saveAll(questionResponses); sectionDTOs.add(buildSectionResponseDTO(sectionResponse, section, questionResponses)); @@ -264,7 +274,8 @@ private FormResponseDTO processSections(FormResponse formResponse, FormResponseR return buildFormResponseDTO(formResponse, sectionDTOs); } - private SectionResponse upsertSectionResponse(Long responseId, Long sectionId, String actor) { + private SectionResponse upsertSectionResponse(Long responseId, Long sectionId, String actor, + Integer vanID, Integer parkingPlaceID) { Optional existing = sectionResponseRepo.findByResponseIdAndSectionId(responseId, sectionId); if (existing.isPresent()) { @@ -272,6 +283,7 @@ private SectionResponse upsertSectionResponse(Long responseId, Long sectionId, S sr.setStatus(SECTION_STATUS_DONE); sr.setSavedAt(new Timestamp(System.currentTimeMillis())); sr.setUpdatedBy(actor); + if (sr.getVanID() == null) { sr.setVanID(vanID); sr.setParkingPlaceID(parkingPlaceID); } return sectionResponseRepo.save(sr); } SectionResponse sr = SectionResponse.builder() @@ -281,6 +293,8 @@ private SectionResponse upsertSectionResponse(Long responseId, Long sectionId, S .savedAt(new Timestamp(System.currentTimeMillis())) .createdBy(actor) .updatedBy(actor) + .vanID(vanID) + .parkingPlaceID(parkingPlaceID) .build(); return sectionResponseRepo.save(sr); } @@ -290,7 +304,9 @@ private List processAnswers( List answers, Map questionMap, Map> optionsByQuestion, - String actor) { + String actor, + Integer vanID, + Integer parkingPlaceID) { List results = new ArrayList<>(); for (QuestionAnswerRequest answer : answers) { @@ -322,6 +338,8 @@ private List processAnswers( .optionId(opt != null ? opt.getOptionId() : null) .createdBy(actor) .updatedBy(actor) + .vanID(vanID) + .parkingPlaceID(parkingPlaceID) .build()); } } else if (type == QuestionType.MCQ) { @@ -335,6 +353,8 @@ private List processAnswers( .optionId(opt != null ? opt.getOptionId() : null) .createdBy(actor) .updatedBy(actor) + .vanID(vanID) + .parkingPlaceID(parkingPlaceID) .build()); } } @@ -349,6 +369,8 @@ private List processAnswers( .answerText(value) .createdBy(actor) .updatedBy(actor) + .vanID(vanID) + .parkingPlaceID(parkingPlaceID) .build()); } } diff --git a/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java index cb347bbf..2098599b 100644 --- a/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java @@ -5,15 +5,29 @@ import com.google.gson.JsonParser; import com.iemr.flw.domain.identity.RMNCHMBeneficiarydetail; import com.iemr.flw.domain.identity.RMNCHMBeneficiarymapping; +import com.iemr.flw.domain.iemr.BenAnthropometryDetail; +import com.iemr.flw.domain.iemr.BenChiefComplaint; import com.iemr.flw.domain.iemr.BenFlowStatus; +import com.iemr.flw.domain.iemr.BenPhysicalVitalDetail; +import com.iemr.flw.domain.iemr.BenReferDetails; +import com.iemr.flw.domain.iemr.BenVisitDetail; +import com.iemr.flw.domain.iemr.PhyGeneralExamination; +import com.iemr.flw.domain.iemr.PrescribedDrugDetail; +import com.iemr.flw.domain.iemr.PrescriptionDetail; import com.iemr.flw.domain.iemr.StopTBDiagnostics; import com.iemr.flw.domain.iemr.StopTBGeneralExamination; import com.iemr.flw.domain.iemr.StopTBGeneralOpd; import com.iemr.flw.domain.iemr.TBScreening; -import com.iemr.flw.domain.iemr.TBStopVisit; import com.iemr.flw.dto.iemr.StopTBRegistrationDto; import com.iemr.flw.repo.identity.BeneficiaryRepo; +import com.iemr.flw.repo.iemr.BenAnthropometryRepo; +import com.iemr.flw.repo.iemr.BenChiefComplaintRepo; import com.iemr.flw.repo.iemr.BenFlowStatusRepo; +import com.iemr.flw.repo.iemr.BenPhysicalVitalRepo; +import com.iemr.flw.repo.iemr.BenReferDetailsRepo; +import com.iemr.flw.repo.iemr.PhyGeneralExaminationRepo; +import com.iemr.flw.repo.iemr.PrescribedDrugDetailRepo; +import com.iemr.flw.repo.iemr.PrescriptionDetailRepo; import com.iemr.flw.repo.iemr.StopTBDiagnosticsRepo; import com.iemr.flw.repo.iemr.StopTBGeneralExaminationRepo; import com.iemr.flw.repo.iemr.StopTBGeneralOpdRepo; @@ -64,6 +78,27 @@ public class StopTBServiceImpl implements StopTBService { @Autowired private TBStopVisitService tbStopVisitService; + @Autowired + private BenAnthropometryRepo benAnthropometryRepo; + + @Autowired + private BenPhysicalVitalRepo benPhysicalVitalRepo; + + @Autowired + private PhyGeneralExaminationRepo phyGeneralExaminationRepo; + + @Autowired + private BenChiefComplaintRepo benChiefComplaintRepo; + + @Autowired + private PrescriptionDetailRepo prescriptionDetailRepo; + + @Autowired + private PrescribedDrugDetailRepo prescribedDrugDetailRepo; + + @Autowired + private BenReferDetailsRepo benReferDetailsRepo; + @Value("${tm-url}") private String tmUrl; @@ -299,15 +334,16 @@ public List> saveGeneralExamination(List Integer providerServiceMapID = getInt(data, "providerServiceMapID"); String createdBy = getString(data, "createdBy"); - TBStopVisit visit = tbStopVisitService.getOrCreateVisitForToday(beneficiaryRegID, providerServiceMapID, + BenVisitDetail visit = tbStopVisitService.getOrCreateVisitForToday(beneficiaryRegID, providerServiceMapID, createdBy, vanID, parkingPlaceID); - StopTBGeneralExamination exam = generalExaminationRepo.findByBeneficiaryRegIDAndVisitCode(beneficiaryRegID, visit.getId()); + StopTBGeneralExamination exam = generalExaminationRepo.findByBeneficiaryRegIDAndVisitCode(beneficiaryRegID, visit.getVisitCode()); if (exam == null) exam = new StopTBGeneralExamination(); boolean isNew = exam.getId() == null; exam.setBeneficiaryRegID(beneficiaryRegID); - exam.setVisitCode(visit.getId()); + exam.setVisitCode(visit.getVisitCode()); + exam.setBenVisitID(visit.getBenVisitId()); exam.setProviderServiceMapID(providerServiceMapID); exam.setPulseRate(getInt(data, "pulseRate")); exam.setSystolicBP(getInt(data, "systolicBP")); @@ -337,7 +373,10 @@ public List> saveGeneralExamination(List exam.setProcessed("N"); generalExaminationRepo.save(exam); - if (isNew) generalExaminationRepo.updateVanSerialNo(exam.getId()); + if (isNew) { + generalExaminationRepo.updateVanSerialNo(exam.getId()); + dualWriteExamToStandardTables(exam, beneficiaryRegID, visit, createdBy, vanID, parkingPlaceID); + } Map result = new HashMap<>(); result.put("beneficiaryRegID", beneficiaryRegID); @@ -422,15 +461,15 @@ public List> saveNurseTBScreening(List> Integer providerServiceMapID = getInt(data, "providerServiceMapID"); String createdBy = getString(data, "createdBy"); - TBStopVisit visit = tbStopVisitService.getOrCreateVisitForToday(beneficiaryRegID, providerServiceMapID, + BenVisitDetail visit = tbStopVisitService.getOrCreateVisitForToday(beneficiaryRegID, providerServiceMapID, createdBy, vanID, parkingPlaceID); - TBScreening screening = tbScreeningRepo.findByBenRegIDAndVisitCode(beneficiaryRegID, visit.getId()); + TBScreening screening = tbScreeningRepo.findByBenRegIDAndVisitCode(beneficiaryRegID, visit.getVisitCode()); if (screening == null) screening = new TBScreening(); boolean isNew = screening.getId() == null; screening.setBenRegID(beneficiaryRegID); - screening.setVisitCode(visit.getId()); + screening.setVisitCode(visit.getVisitCode()); screening.setProviderServiceMapID(providerServiceMapID); screening.setCoughMoreThan2Weeks(getBool(data, "coughMoreThan2Weeks")); screening.setBloodInSputum(getBool(data, "bloodInSputum")); @@ -550,15 +589,16 @@ public List> saveGeneralOpd(List> dataLi Integer providerServiceMapID = getInt(data, "providerServiceMapID"); String createdBy = getString(data, "createdBy"); - TBStopVisit visit = tbStopVisitService.getOrCreateVisitForToday(beneficiaryRegID, providerServiceMapID, + BenVisitDetail visit = tbStopVisitService.getOrCreateVisitForToday(beneficiaryRegID, providerServiceMapID, createdBy, vanID, parkingPlaceID); - StopTBGeneralOpd opd = generalOpdRepo.findByBenRegIDAndVisitCode(beneficiaryRegID, visit.getId()); + StopTBGeneralOpd opd = generalOpdRepo.findByBenRegIDAndVisitCode(beneficiaryRegID, visit.getVisitCode()); if (opd == null) opd = new StopTBGeneralOpd(); boolean isNew = opd.getId() == null; opd.setBenRegID(beneficiaryRegID); - opd.setVisitCode(visit.getId()); + opd.setVisitCode(visit.getVisitCode()); + opd.setBenVisitID(visit.getBenVisitId()); opd.setProviderServiceMapID(providerServiceMapID); opd.setChiefComplaint(toJsonString(data.get("chiefComplaint"))); opd.setMedication(getString(data, "medication")); @@ -573,7 +613,10 @@ public List> saveGeneralOpd(List> dataLi opd.setProcessed("N"); generalOpdRepo.save(opd); - if (isNew) generalOpdRepo.updateVanSerialNo(opd.getId()); + if (isNew) { + generalOpdRepo.updateVanSerialNo(opd.getId()); + dualWriteOpdToStandardTables(opd, beneficiaryRegID, visit, createdBy, vanID, parkingPlaceID); + } Map result = new HashMap<>(); result.put("beneficiaryRegID", beneficiaryRegID); @@ -754,6 +797,189 @@ public Map submitNurseData(Map data) throws Exce return result; } + // ── Standard table dual-writes ──────────────────────────────────────────── + + private Map getRegistrationExtras(Long beneficiaryRegID) { + try { + RMNCHMBeneficiarymapping mapping = beneficiaryRepo.getById(BigInteger.valueOf(beneficiaryRegID)); + RMNCHMBeneficiarydetail detail = (mapping != null && mapping.getBenDetailsId() != null) + ? beneficiaryRepo.getDetailsById(mapping.getBenDetailsId()) + : null; + if (detail != null && detail.getOtherFields() != null && !detail.getOtherFields().isEmpty()) { + return new ObjectMapper().readValue(detail.getOtherFields(), Map.class); + } + } catch (Exception e) { + logger.warn("Cannot read otherFields for benRegID: " + beneficiaryRegID); + } + return Collections.emptyMap(); + } + + private void dualWriteExamToStandardTables(StopTBGeneralExamination exam, Long beneficiaryRegID, + BenVisitDetail visit, String createdBy, Integer vanID, Integer parkingPlaceID) { + try { + Map extras = getRegistrationExtras(beneficiaryRegID); + writeAnthropometry(extras, beneficiaryRegID, visit, createdBy, vanID, parkingPlaceID); + writeVitals(exam, extras, beneficiaryRegID, visit, createdBy, vanID, parkingPlaceID); + writePhyGeneralExam(exam, beneficiaryRegID, visit, createdBy, vanID, parkingPlaceID); + if (Boolean.TRUE.equals(exam.getReferralToHWCNeeded())) { + writeReferral(beneficiaryRegID, visit, createdBy, vanID, parkingPlaceID); + } + } catch (Exception e) { + logger.error("Dual-write exam to standard tables failed for benRegID: " + beneficiaryRegID, e); + } + } + + private void writeAnthropometry(Map extras, Long beneficiaryRegID, + BenVisitDetail visit, String createdBy, Integer vanID, Integer parkingPlaceID) { + try { + BenAnthropometryDetail a = new BenAnthropometryDetail(); + a.setBeneficiaryRegID(beneficiaryRegID); + a.setBenVisitID(visit.getBenVisitId()); + a.setVisitCode(visit.getVisitCode()); + a.setProviderServiceMapID(visit.getProviderServiceMapID()); + a.setCreatedBy(createdBy); + a.setVanID(vanID); + a.setParkingPlaceID(parkingPlaceID); + Object h = extras.get("height"); + Object w = extras.get("weight"); + Object b = extras.get("bmi"); + if (h instanceof Number) a.setHeightCm(((Number) h).doubleValue()); + if (w instanceof Number) a.setWeightKg(((Number) w).doubleValue()); + if (b instanceof Number) a.setBmi(((Number) b).doubleValue()); + benAnthropometryRepo.save(a); + } catch (Exception e) { + logger.error("writeAnthropometry failed for benRegID: " + beneficiaryRegID, e); + } + } + + private void writeVitals(StopTBGeneralExamination exam, Map extras, + Long beneficiaryRegID, BenVisitDetail visit, String createdBy, Integer vanID, Integer parkingPlaceID) { + try { + BenPhysicalVitalDetail v = new BenPhysicalVitalDetail(); + v.setBeneficiaryRegID(beneficiaryRegID); + v.setBenVisitID(visit.getBenVisitId()); + v.setVisitCode(visit.getVisitCode()); + v.setProviderServiceMapID(visit.getProviderServiceMapID()); + v.setCreatedBy(createdBy); + v.setVanID(vanID); + v.setParkingPlaceID(parkingPlaceID); + Object t = extras.get("temperatureValue"); + if (t instanceof Number) v.setTemperature(((Number) t).doubleValue()); + if (exam.getPulseRate() != null) v.setPulseRate(exam.getPulseRate().shortValue()); + if (exam.getSystolicBP() != null) v.setSystolicBP(exam.getSystolicBP().shortValue()); + if (exam.getDiastolicBP() != null) v.setDiastolicBP(exam.getDiastolicBP().shortValue()); + if (exam.getRandomBloodSugar() != null) v.setBloodGlucoseRandom(exam.getRandomBloodSugar().shortValue()); + benPhysicalVitalRepo.save(v); + } catch (Exception e) { + logger.error("writeVitals failed for benRegID: " + beneficiaryRegID, e); + } + } + + private void writePhyGeneralExam(StopTBGeneralExamination exam, Long beneficiaryRegID, + BenVisitDetail visit, String createdBy, Integer vanID, Integer parkingPlaceID) { + try { + PhyGeneralExamination g = new PhyGeneralExamination(); + g.setBeneficiaryRegID(beneficiaryRegID); + g.setBenVisitID(visit.getBenVisitId()); + g.setVisitCode(visit.getVisitCode()); + g.setProviderServiceMapID(visit.getProviderServiceMapID()); + g.setCreatedBy(createdBy); + g.setVanID(vanID); + g.setParkingPlaceID(parkingPlaceID); + g.setPallor(exam.getPallor()); + g.setJaundice(exam.getIcterus()); + g.setCyanosis(exam.getCyanosis()); + g.setClubbing(exam.getClubbing()); + g.setLymphadenopathy(exam.getLymphadenopathy()); + g.setEdema(exam.getOedema()); + phyGeneralExaminationRepo.save(g); + } catch (Exception e) { + logger.error("writePhyGeneralExam failed for benRegID: " + beneficiaryRegID, e); + } + } + + private void writeReferral(Long beneficiaryRegID, BenVisitDetail visit, + String createdBy, Integer vanID, Integer parkingPlaceID) { + try { + BenReferDetails r = new BenReferDetails(); + r.setBeneficiaryRegID(beneficiaryRegID); + r.setBenVisitID(visit.getBenVisitId()); + r.setVisitCode(visit.getVisitCode()); + r.setProviderServiceMapID(visit.getProviderServiceMapID()); + r.setCreatedBy(createdBy); + r.setVanID(vanID); + r.setParkingPlaceID(parkingPlaceID); + r.setReferredToInstituteName("HWC"); + r.setReferralReason("Stop TB Referral"); + benReferDetailsRepo.save(r); + } catch (Exception e) { + logger.error("writeReferral failed for benRegID: " + beneficiaryRegID, e); + } + } + + private void dualWriteOpdToStandardTables(StopTBGeneralOpd opd, Long beneficiaryRegID, + BenVisitDetail visit, String createdBy, Integer vanID, Integer parkingPlaceID) { + try { + writeChiefComplaint(opd.getChiefComplaint(), beneficiaryRegID, visit, createdBy, vanID, parkingPlaceID); + writePrescription(opd, beneficiaryRegID, visit, createdBy, vanID, parkingPlaceID); + } catch (Exception e) { + logger.error("Dual-write OPD to standard tables failed for benRegID: " + beneficiaryRegID, e); + } + } + + private void writeChiefComplaint(String complaint, Long beneficiaryRegID, BenVisitDetail visit, + String createdBy, Integer vanID, Integer parkingPlaceID) { + if (complaint == null || complaint.isBlank()) return; + try { + BenChiefComplaint c = new BenChiefComplaint(); + c.setBeneficiaryRegID(beneficiaryRegID); + c.setBenVisitID(visit.getBenVisitId()); + c.setVisitCode(visit.getVisitCode()); + c.setProviderServiceMapID(visit.getProviderServiceMapID()); + c.setCreatedBy(createdBy); + c.setVanID(vanID); + c.setParkingPlaceID(parkingPlaceID); + c.setChiefComplaint(complaint); + benChiefComplaintRepo.save(c); + } catch (Exception e) { + logger.error("writeChiefComplaint failed for benRegID: " + beneficiaryRegID, e); + } + } + + private void writePrescription(StopTBGeneralOpd opd, Long beneficiaryRegID, BenVisitDetail visit, + String createdBy, Integer vanID, Integer parkingPlaceID) { + if (opd.getMedication() == null || opd.getMedication().isBlank()) return; + try { + PrescriptionDetail p = new PrescriptionDetail(); + p.setBeneficiaryRegID(beneficiaryRegID); + p.setBenVisitID(visit.getBenVisitId()); + p.setVisitCode(visit.getVisitCode()); + p.setProviderServiceMapID(visit.getProviderServiceMapID()); + p.setCreatedBy(createdBy); + p.setVanID(vanID); + p.setParkingPlaceID(parkingPlaceID); + p.setInstruction(opd.getNotes()); + p = prescriptionDetailRepo.save(p); + + PrescribedDrugDetail d = new PrescribedDrugDetail(); + d.setBeneficiaryRegID(beneficiaryRegID); + d.setBenVisitID(visit.getBenVisitId()); + d.setVisitCode(visit.getVisitCode()); + d.setProviderServiceMapID(visit.getProviderServiceMapID()); + d.setCreatedBy(createdBy); + d.setVanID(vanID); + d.setParkingPlaceID(parkingPlaceID); + d.setPrescriptionID(p.getPrescriptionID()); + d.setDrugName(opd.getMedication()); + d.setDose(opd.getDosage()); + d.setFrequency(opd.getFrequency()); + d.setDuration(opd.getDuration()); + prescribedDrugDetailRepo.save(d); + } catch (Exception e) { + logger.error("writePrescription failed for benRegID: " + beneficiaryRegID, e); + } + } + // ── Helpers ─────────────────────────────────────────────────────────────── private Timestamp parseDob(JsonObject obj) { diff --git a/src/main/java/com/iemr/flw/service/impl/TBConfirmedCaseServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/TBConfirmedCaseServiceImpl.java index 01ab464a..a38073d6 100644 --- a/src/main/java/com/iemr/flw/service/impl/TBConfirmedCaseServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/TBConfirmedCaseServiceImpl.java @@ -5,7 +5,7 @@ import com.iemr.flw.domain.iemr.DynamicForm; import com.iemr.flw.domain.iemr.TBConfirmedCaseDTO; import com.iemr.flw.domain.iemr.TBConfirmedCase; -import com.iemr.flw.domain.iemr.TBStopVisit; +import com.iemr.flw.domain.iemr.BenVisitDetail; import com.iemr.flw.repo.iemr.DynamicFormRepo; import com.iemr.flw.repo.iemr.FormResponseRepo; import com.iemr.flw.repo.iemr.TBConfirmedTreatmentRepository; @@ -68,14 +68,14 @@ public String save(List request, String authorisation) throw for(TBConfirmedCaseDTO dto:request){ String modifiedBy = jwtUtil.extractUsername(authorisation); - TBStopVisit visit = tbStopVisitService.getOrCreateVisitForToday(dto.getBenId(), null, + BenVisitDetail visit = tbStopVisitService.getOrCreateVisitForToday(dto.getBenId(), null, modifiedBy, vanID, parkingPlaceID); - List existing = repository.findByBenIdAndVisitCode(dto.getBenId(), visit.getId()); + List existing = repository.findByBenIdAndVisitCode(dto.getBenId(), visit.getVisitCode()); boolean isNew = existing.isEmpty(); TBConfirmedCase entity = isNew ? new TBConfirmedCase() : existing.get(0); entity.setBenId(dto.getBenId()); - entity.setVisitCode(visit.getId()); + entity.setVisitCode(visit.getVisitCode()); entity.setUserId(jwtUtil.extractUserId(authorisation)); entity.setModifiedBy(modifiedBy); entity.setRegimenType(dto.getRegimenType()); diff --git a/src/main/java/com/iemr/flw/service/impl/TBStopVisitServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/TBStopVisitServiceImpl.java index 5907b678..a54ebe4f 100644 --- a/src/main/java/com/iemr/flw/service/impl/TBStopVisitServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/TBStopVisitServiceImpl.java @@ -1,7 +1,7 @@ package com.iemr.flw.service.impl; -import com.iemr.flw.domain.iemr.TBStopVisit; -import com.iemr.flw.repo.iemr.TBStopVisitRepo; +import com.iemr.flw.domain.iemr.BenVisitDetail; +import com.iemr.flw.repo.iemr.BenVisitDetailsRepo; import com.iemr.flw.service.TBStopVisitService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -14,34 +14,45 @@ public class TBStopVisitServiceImpl implements TBStopVisitService { @Autowired - private TBStopVisitRepo tbStopVisitRepo; + private BenVisitDetailsRepo benVisitDetailsRepo; @Override @Transactional - public TBStopVisit getOrCreateVisitForToday(Long beneficiaryRegID, Integer providerServiceMapID, String createdBy, - Integer vanID, Integer parkingPlaceID) { + public BenVisitDetail getOrCreateVisitForToday(Long beneficiaryRegID, Integer providerServiceMapID, + String createdBy, Integer vanID, Integer parkingPlaceID) { LocalDate today = LocalDate.now(); - Timestamp todayStart = Timestamp.valueOf(today.atStartOfDay()); - Timestamp todayEnd = Timestamp.valueOf(today.plusDays(1).atStartOfDay()); - - TBStopVisit visit = tbStopVisitRepo.findByBeneficiaryRegIDAndVisitDateBetween(beneficiaryRegID, todayStart, todayEnd); - if (visit != null) { - return visit; - } - - Integer count = tbStopVisitRepo.getVisitCountForBeneficiary(beneficiaryRegID); - visit = new TBStopVisit(); - visit.setBeneficiaryRegID(beneficiaryRegID); - visit.setVisitNo(count != null ? count + 1 : 1); + Timestamp dayStart = Timestamp.valueOf(today.atStartOfDay()); + Timestamp dayEnd = Timestamp.valueOf(today.plusDays(1).atStartOfDay()); + + BenVisitDetail existing = benVisitDetailsRepo.findStopTBVisitForToday(beneficiaryRegID, dayStart, dayEnd); + if (existing != null) return existing; + + Integer visitCount = benVisitDetailsRepo.countStopTBVisits(beneficiaryRegID); + short visitNo = (short) ((visitCount != null ? visitCount : 0) + 1); + + BenVisitDetail visit = new BenVisitDetail(); + visit.setBeneficiaryRegId(beneficiaryRegID); + visit.setVisitNo(visitNo); + visit.setVisitCategory("Stop TB"); + visit.setVisitReason("Screening"); + visit.setVisitDateTime(new Timestamp(System.currentTimeMillis())); visit.setProviderServiceMapID(providerServiceMapID); visit.setCreatedBy(createdBy); - if (vanID != null) { - visit.setVanID(vanID); - visit.setParkingPlaceID(parkingPlaceID); - } + visit.setVanId(vanID); + visit.setParkingPlaceId(parkingPlaceID); + visit.setDeleted(false); visit.setProcessed("N"); - visit = tbStopVisitRepo.save(visit); - tbStopVisitRepo.updateVanSerialNo(visit.getId()); - return visit; + visit = benVisitDetailsRepo.save(visit); + + // VisitCode: sessionID(1) + vanID(5-digit) + benVisitId(8-digit) — MMU formula + long visitCode = generateVisitCode(visit.getBenVisitId(), vanID != null ? vanID : 1); + visit.setVisitCode(visitCode); + return benVisitDetailsRepo.save(visit); + } + + private long generateVisitCode(long visitId, int vanID) { + String vanStr = String.format("%05d", vanID); + String visitStr = String.format("%08d", visitId); + return Long.parseLong("1" + vanStr + visitStr); } } diff --git a/src/main/java/com/iemr/flw/service/impl/TBSuspectedServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/TBSuspectedServiceImpl.java index 28f05180..d675a01d 100644 --- a/src/main/java/com/iemr/flw/service/impl/TBSuspectedServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/TBSuspectedServiceImpl.java @@ -2,7 +2,7 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; -import com.iemr.flw.domain.iemr.TBStopVisit; +import com.iemr.flw.domain.iemr.BenVisitDetail; import com.iemr.flw.domain.iemr.TBSuspected; import com.iemr.flw.dto.identity.GetBenRequestHandler; import com.iemr.flw.dto.iemr.TBSuspectedDTO; @@ -42,11 +42,11 @@ public String save(TBSuspectedRequestDTO requestDTO) throws Exception { Integer vanID = campConfigService.getVanID(); Integer parkingPlaceID = campConfigService.getParkingPlaceID(); for (TBSuspectedDTO tbSuspectedDTO : requestDTO.getTbSuspectedList()) { - TBStopVisit visit = tbStopVisitService.getOrCreateVisitForToday(tbSuspectedDTO.getBenId(), null, + BenVisitDetail visit = tbStopVisitService.getOrCreateVisitForToday(tbSuspectedDTO.getBenId(), null, requestDTO.getUserId() != null ? requestDTO.getUserId().toString() : null, vanID, parkingPlaceID); TBSuspected tbSuspected = - tbSuspectedRepo.getByUserIdAndBenIdAndVisitCode(tbSuspectedDTO.getBenId(), requestDTO.getUserId(), visit.getId()); + tbSuspectedRepo.getByUserIdAndBenIdAndVisitCode(tbSuspectedDTO.getBenId(), requestDTO.getUserId(), visit.getVisitCode()); if (tbSuspected == null) { tbSuspected = new TBSuspected(); @@ -59,7 +59,7 @@ public String save(TBSuspectedRequestDTO requestDTO) throws Exception { } tbSuspected.setUserId(requestDTO.getUserId()); - tbSuspected.setVisitCode(visit.getId()); + tbSuspected.setVisitCode(visit.getVisitCode()); if (tbSuspected.getVanID() == null && vanID != null) { tbSuspected.setVanID(vanID); tbSuspected.setParkingPlaceID(parkingPlaceID); } tbSuspected.setProcessed("N"); tbSuspectedRepo.save(tbSuspected); From c53b9b546e5e5525e0184e94560cff621686ef8f Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Tue, 30 Jun 2026 14:23:03 +0530 Subject: [PATCH 20/37] fix(stoptb): getBeneficiaryData reads anthropometry/vitals from standard tables - getBeneficiaryData now reads height/weight/bmi from t_phy_anthropometry and temperature/pulse/BP/RBS from t_phy_vitals (latest row per beneficiary) - Fallback to otherFields if exam not yet saved for the beneficiary - Stop TB specific fields (personFrom, caseFindingType etc.) still from otherFields - Add findByBeneficiaryRegIDOrderByCreatedDateDesc and findByBenVisitID to BenAnthropometryRepo and BenPhysicalVitalRepo - Add StopTB_Future_Roadmap.md for future HWC integration and edit support Co-Authored-By: Claude Sonnet 4.6 --- StopTB_Future_Roadmap.md | 246 ++++++++++++++++++ .../flw/repo/iemr/BenAnthropometryRepo.java | 6 + .../flw/repo/iemr/BenPhysicalVitalRepo.java | 6 + .../service/impl/BeneficiaryServiceImpl.java | 58 ++++- 4 files changed, 309 insertions(+), 7 deletions(-) create mode 100644 StopTB_Future_Roadmap.md diff --git a/StopTB_Future_Roadmap.md b/StopTB_Future_Roadmap.md new file mode 100644 index 00000000..d6fea62b --- /dev/null +++ b/StopTB_Future_Roadmap.md @@ -0,0 +1,246 @@ +# Stop TB — Future Roadmap + +**Date:** 2026-06-30 +**Author:** vishwab1 +**Prerequisite:** Read `StopTB_Standard_Table_Integration.md` first for current implementation context. + +--- + +## Why this document exists + +The current dual-write implementation (FLW-API) writes Stop TB clinical data to both custom +`tb_stoptb_*` tables and standard AMRIT tables (`t_phy_vitals`, `t_Phy_GeneralExam` etc.). + +This document captures what still needs to be done in future phases — HWC integration, +edit support, worklist, and BenFlowStatus advancement. + +**None of these future changes require a mobile app change.** + +--- + +## 1. HWC-API — Read common fields from standard tables + +**Trigger:** When HWC doctor needs to see Stop TB beneficiary's vitals/examination history. + +**Problem:** HWC-API currently queries standard tables with a `providerServiceMapID` filter: +```sql +SELECT * FROM t_phy_vitals +WHERE beneficiaryRegID = ? AND providerServiceMapID = ? -- HWC's own PSMID +``` +Stop TB data was written with Stop TB's PSMID → HWC query misses it. + +**Fix — HWC-API only, remove PSMID filter:** +```sql +SELECT * FROM t_phy_vitals +WHERE beneficiaryRegID = ? -- no PSMID filter, shows all service lines +ORDER BY CreatedDate DESC +``` + +**Affected HWC-API endpoints:** +- `getBenPhysicalVitalDetail` +- `getBenAnthropometryDetail` +- `getBenPhyGeneralExamination` +- `getBenChiefComplaint` +- `getBenPrescriptionDetail` + +**Effort:** Small — query change only in HWC-API. No schema change. No FLW-API change. + +--- + +## 2. HWC-API — Read TB specific data from custom tables directly + +**Trigger:** When HWC doctor needs full TB clinical picture — screening questions, diagnostics, +Nikshay ID, treatment tracking. + +Standard tables only hold common clinical fields. TB specific data lives in custom tables. +HWC-API needs to query these directly by `benRegID`. + +**New queries needed in HWC-API:** + +```java +// TB Screening +tbScreeningRepo.findByBenRegIDOrderByCreatedDateDesc(benRegID); + +// Diagnostics (Nikshay ID, X-ray, Truenat, Sputum, Liquid Culture) +tbDiagnosticsRepo.findByBenRegIDOrderByCreatedDateDesc(benRegID); + +// Suspected cases +tbSuspectedRepo.findByBenIdOrderByCreatedDateDesc(benId); + +// Confirmed cases + treatment +tbConfirmedRepo.findByBenIdOrderByCreatedDateDesc(benId); +``` + +**Tables queried:** +| Table | Key data | +|---|---| +| `tb_screening` | Cough, fever, night sweats, weight loss, family history | +| `tb_stoptb_diagnostics` | Nikshay ID, X-ray result, Truenat result, Liquid Culture result | +| `tb_suspected` | Sputum result, chest X-ray, referral, TB/DRTB confirmed flag | +| `tb_confirmed_cases` | Regimen, treatment dates, adherence, outcome | + +**Effort:** Small — new repo methods + new HWC-API service calls. No schema change. + +--- + +## 3. FLW-API — Edit support for standard table dual-write + +**Trigger:** When mobile app adds edit/update capability for nurse data. + +**Current behaviour:** Dual-write only happens on first save (`isNew = true`). On re-save, +only `tb_stoptb_*` custom tables are updated. Standard tables keep the original values. + +**What breaks:** If mobile edits vitals → `tb_stoptb_general_examination` gets new values +but `t_phy_vitals` keeps old values → HWC sees stale data. + +### Changes in StopTBServiceImpl + +```java +// Change from: +if (isNew) { + generalExaminationRepo.updateVanSerialNo(exam.getId()); + dualWriteExamToStandardTables(exam, beneficiaryRegID, visit, createdBy, vanID, parkingPlaceID); +} + +// Change to: +if (isNew) generalExaminationRepo.updateVanSerialNo(exam.getId()); +dualWriteExamToStandardTables(exam, beneficiaryRegID, visit, createdBy, vanID, parkingPlaceID); +``` + +Same change for `saveGeneralOpd()`: +```java +// Remove isNew guard around dualWriteOpdToStandardTables +if (isNew) generalOpdRepo.updateVanSerialNo(opd.getId()); +dualWriteOpdToStandardTables(opd, beneficiaryRegID, visit, createdBy, vanID, parkingPlaceID); +``` + +### Changes in dual-write helpers — upsert instead of insert + +Each helper must find existing row first, update if found, insert if not: + +```java +private void writeVitals(StopTBGeneralExamination exam, ...) { + try { + // Find existing row for this visit + BenPhysicalVitalDetail existing = + benPhysicalVitalRepo.findByBenVisitID(visit.getBenVisitId()); + BenPhysicalVitalDetail v = existing != null ? existing : new BenPhysicalVitalDetail(); + + v.setBeneficiaryRegID(beneficiaryRegID); + v.setBenVisitID(visit.getBenVisitId()); + v.setVisitCode(visit.getVisitCode()); + // ... set all fields + benPhysicalVitalRepo.save(v); + } catch (Exception e) { + logger.error("writeVitals failed", e); + } +} +``` + +Same upsert pattern for: `writeAnthropometry()`, `writePhyGeneralExam()`, +`writeChiefComplaint()`, `writePrescription()`. + +### New repo methods needed (FLW-API) + +```java +// BenPhysicalVitalRepo +BenPhysicalVitalDetail findByBenVisitID(Long benVisitID); + +// BenAnthropometryRepo +BenAnthropometryDetail findByBenVisitID(Long benVisitID); + +// PhyGeneralExaminationRepo +PhyGeneralExamination findByBenVisitID(Long benVisitID); + +// BenChiefComplaintRepo +BenChiefComplaint findByBenVisitID(Long benVisitID); +``` + +> **Note:** `t_prescription` and `t_prescribeddrug` have a parent-child FK relationship. +> For prescription edit: find existing `PrescriptionDetail` by `benVisitID`, delete old +> `PrescribedDrugDetail` rows, re-insert fresh drug rows. + +**Effort:** Small — remove isNew guard, add findByBenVisitID to 4 repos, convert helpers to upsert. + +--- + +## 4. FLW-API — nurseFlag advancement + +**Trigger:** When a web dashboard / supervisor screen is built for Stop TB. + +**Current state:** `nurseFlag` in `i_ben_flow_outreach` is set to `1` at registration and +never advances because mobile never calls `flw-api/stoptb/nurse/submit`. + +**What to implement:** + +After each of the 3 mandatory nurse saves (generalExamination, tbScreening, generalOpd), +check if all 3 are complete for the same `visitCode`. If yes, auto-advance `nurseFlag = 9`. + +```java +private void tryAdvanceNurseFlag(Long benRegID, Long visitCode, Integer psmID) { + boolean examDone = generalExaminationRepo + .findByBeneficiaryRegIDAndVisitCode(benRegID, visitCode) != null; + boolean screeningDone = tbScreeningRepo + .findByBenRegIDAndVisitCode(benRegID, visitCode) != null; + boolean opdDone = generalOpdRepo + .findByBenRegIDAndVisitCode(benRegID, visitCode) != null; + + if (examDone && screeningDone && opdDone) { + benFlowStatusRepo.updateStopTBAfterNurseSubmit(benRegID, psmID); + // updateStopTBAfterNurseSubmit() already exists in BenFlowStatusRepo + } +} +``` + +Call `tryAdvanceNurseFlag()` at the end of each of the 3 save methods in `StopTBServiceImpl`. + +**Effort:** Small — new private method in StopTBServiceImpl, reuses existing repo query. + +--- + +## 5. HWC-API + HWC-UI — Stop TB referral worklist + +**Trigger:** When HWC wants to track Stop TB beneficiaries referred to it. + +**Current state:** When Stop TB nurse marks `referralToHWCNeeded = true`, FLW-API writes a +row to `t_benreferdetails` with `referredToInstituteName = 'HWC'` and +`referralReason = 'Stop TB Referral'`. HWC-API has no endpoint that surfaces these rows. + +**HWC-API change needed:** + +New query in `BenReferDetailsRepo`: +```java +@Query("SELECT r FROM BenReferDetails r WHERE r.beneficiaryRegID = :benRegID " + + "AND r.referralReason = 'Stop TB Referral' AND r.deleted = false " + + "ORDER BY r.createdDate DESC") +List findStopTBReferrals(@Param("benRegID") Long benRegID); +``` + +Or for worklist (all referred beneficiaries for a provider): +```java +@Query("SELECT r FROM BenReferDetails r WHERE r.referredToInstituteName = 'HWC' " + + "AND r.referralReason = 'Stop TB Referral' AND r.deleted = false") +List findAllStopTBReferrals(); +``` + +**HWC-UI change needed:** +- New worklist tab or filter: "Stop TB Referrals" +- Shows beneficiary name, referral date, FLW name + +**Effort:** Medium — HWC-API new endpoint + HWC-UI new screen. + +--- + +## Summary table + +| # | Change | Service | Effort | Blocks on | +|---|---|---|---|---| +| 1 | Read standard tables without PSMID filter | HWC-API | Small | HWC team decision | +| 2 | Read TB specific custom tables | HWC-API | Small | HWC team decision | +| 3 | Edit support — upsert in dual-write helpers | FLW-API | Small | Mobile adding edit | +| 4 | nurseFlag auto-advancement | FLW-API | Small | Web dashboard being built | +| 5 | Stop TB referral worklist | HWC-API + HWC-UI | Medium | HWC team + product decision | + +**No mobile change required for any of these.** +**No schema change required for items 1, 2, 3, 4.** +**Item 5 may need a column in `t_benreferdetails` for status tracking (pending/accepted/rejected) — TBD by HWC team.** diff --git a/src/main/java/com/iemr/flw/repo/iemr/BenAnthropometryRepo.java b/src/main/java/com/iemr/flw/repo/iemr/BenAnthropometryRepo.java index b7c9efd3..1856eb06 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/BenAnthropometryRepo.java +++ b/src/main/java/com/iemr/flw/repo/iemr/BenAnthropometryRepo.java @@ -4,6 +4,12 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import java.util.List; + @Repository public interface BenAnthropometryRepo extends JpaRepository { + + List findByBeneficiaryRegIDOrderByCreatedDateDesc(Long beneficiaryRegID); + + BenAnthropometryDetail findByBenVisitID(Long benVisitID); } diff --git a/src/main/java/com/iemr/flw/repo/iemr/BenPhysicalVitalRepo.java b/src/main/java/com/iemr/flw/repo/iemr/BenPhysicalVitalRepo.java index cf1ed35b..7e474935 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/BenPhysicalVitalRepo.java +++ b/src/main/java/com/iemr/flw/repo/iemr/BenPhysicalVitalRepo.java @@ -4,6 +4,12 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import java.util.List; + @Repository public interface BenPhysicalVitalRepo extends JpaRepository { + + List findByBeneficiaryRegIDOrderByCreatedDateDesc(Long beneficiaryRegID); + + BenPhysicalVitalDetail findByBenVisitID(Long benVisitID); } diff --git a/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java index bc146ca4..016576d5 100644 --- a/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java @@ -53,8 +53,12 @@ import com.iemr.flw.domain.identity.RMNCHMBeneficiarymapping; import com.iemr.flw.dto.identity.GetBenRequestHandler; import com.iemr.flw.mapper.InputMapper; +import com.iemr.flw.domain.iemr.BenAnthropometryDetail; +import com.iemr.flw.domain.iemr.BenPhysicalVitalDetail; import com.iemr.flw.repo.identity.BeneficiaryRepo; import com.iemr.flw.repo.identity.HouseHoldRepo; +import com.iemr.flw.repo.iemr.BenAnthropometryRepo; +import com.iemr.flw.repo.iemr.BenPhysicalVitalRepo; import com.iemr.flw.service.BeneficiaryService; import com.iemr.flw.utils.config.ConfigProperties; import com.iemr.flw.utils.http.HttpUtils; @@ -100,6 +104,12 @@ public class BeneficiaryServiceImpl implements BeneficiaryService { @Autowired private UserServiceRoleRepo userRepo; + @Autowired + private BenAnthropometryRepo benAnthropometryRepo; + + @Autowired + private BenPhysicalVitalRepo benPhysicalVitalRepo; + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("dd-MM-yyyy"); @@ -499,17 +509,51 @@ private String getMappingsForAddressIDs(List addressLi if (benDetailsOBJ != null && benDetailsOBJ.getOccupation() != null) benDetailsRMNCH_OBJ.setOccupation(benDetailsOBJ.getOccupation()); - // anthropometry and Stop TB fields from ExtraFields + // anthropometry from t_phy_anthropometry, vitals from t_phy_vitals + // fallback to otherFields if exam not yet saved for this beneficiary + Long benRegIdLong = m.getBenRegId() != null ? m.getBenRegId().longValue() : null; + if (benRegIdLong != null) { + List anthroList = + benAnthropometryRepo.findByBeneficiaryRegIDOrderByCreatedDateDesc(benRegIdLong); + List vitalList = + benPhysicalVitalRepo.findByBeneficiaryRegIDOrderByCreatedDateDesc(benRegIdLong); + + Map anthropometry = new HashMap<>(); + if (!anthroList.isEmpty()) { + BenAnthropometryDetail a = anthroList.get(0); + if (a.getHeightCm() != null) anthropometry.put("height", a.getHeightCm()); + if (a.getWeightKg() != null) anthropometry.put("weight", a.getWeightKg()); + if (a.getBmi() != null) anthropometry.put("bmi", a.getBmi()); + } + if (!vitalList.isEmpty()) { + BenPhysicalVitalDetail v = vitalList.get(0); + if (v.getTemperature() != null) anthropometry.put("temperatureValue", v.getTemperature()); + if (v.getPulseRate() != null) anthropometry.put("pulseRate", v.getPulseRate()); + if (v.getSystolicBP() != null) anthropometry.put("systolicBP", v.getSystolicBP()); + if (v.getDiastolicBP() != null) anthropometry.put("diastolicBP", v.getDiastolicBP()); + if (v.getBloodGlucoseRandom() != null) anthropometry.put("bloodGlucoseRandom", v.getBloodGlucoseRandom()); + } + + // fallback: if no exam saved yet, read from registration otherFields + if (anthropometry.isEmpty() && benDetailsOBJ != null && benDetailsOBJ.getOtherFields() != null) { + try { + Map extraFields = new Gson().fromJson(benDetailsOBJ.getOtherFields(), Map.class); + if (extraFields.containsKey("weight")) anthropometry.put("weight", extraFields.get("weight")); + if (extraFields.containsKey("height")) anthropometry.put("height", extraFields.get("height")); + if (extraFields.containsKey("bmi")) anthropometry.put("bmi", extraFields.get("bmi")); + if (extraFields.containsKey("temperatureValue")) anthropometry.put("temperatureValue", extraFields.get("temperatureValue")); + } catch (Exception ex) { + logger.warn("Could not parse otherFields for fallback anthropometry: " + benDetailsOBJ.getBeneficiaryDetailsId()); + } + } + if (!anthropometry.isEmpty()) resultMap.put("anthropometry", anthropometry); + } + + // Stop TB fields from ExtraFields Map stopTBDetails = new HashMap<>(); if (benDetailsOBJ != null && benDetailsOBJ.getOtherFields() != null) { try { Map extraFields = new Gson().fromJson(benDetailsOBJ.getOtherFields(), Map.class); - Map anthropometry = new HashMap<>(); - if (extraFields.containsKey("weight")) anthropometry.put("weight", extraFields.get("weight")); - if (extraFields.containsKey("height")) anthropometry.put("height", extraFields.get("height")); - if (extraFields.containsKey("bmi")) anthropometry.put("bmi", extraFields.get("bmi")); - if (extraFields.containsKey("temperatureValue")) anthropometry.put("temperatureValue", extraFields.get("temperatureValue")); - if (!anthropometry.isEmpty()) resultMap.put("anthropometry", anthropometry); if (extraFields.containsKey("personFrom")) stopTBDetails.put("personFrom", extraFields.get("personFrom")); if (extraFields.containsKey("caseFindingType")) stopTBDetails.put("caseFindingType", extraFields.get("caseFindingType")); From c7806cf929bb1db568d0dca3f24e5ab1c19e746f Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Tue, 30 Jun 2026 15:29:23 +0530 Subject: [PATCH 21/37] fix(compile): rename variable 'a' to 'anthro' to avoid conflict with outer loop variable Co-Authored-By: Claude Sonnet 4.6 --- .../service/impl/BeneficiaryServiceImpl.java | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java index 016576d5..a0c5d3b0 100644 --- a/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java @@ -520,18 +520,18 @@ private String getMappingsForAddressIDs(List addressLi Map anthropometry = new HashMap<>(); if (!anthroList.isEmpty()) { - BenAnthropometryDetail a = anthroList.get(0); - if (a.getHeightCm() != null) anthropometry.put("height", a.getHeightCm()); - if (a.getWeightKg() != null) anthropometry.put("weight", a.getWeightKg()); - if (a.getBmi() != null) anthropometry.put("bmi", a.getBmi()); + BenAnthropometryDetail anthro = anthroList.get(0); + if (anthro.getHeightCm() != null) anthropometry.put("height", anthro.getHeightCm()); + if (anthro.getWeightKg() != null) anthropometry.put("weight", anthro.getWeightKg()); + if (anthro.getBmi() != null) anthropometry.put("bmi", anthro.getBmi()); } if (!vitalList.isEmpty()) { - BenPhysicalVitalDetail v = vitalList.get(0); - if (v.getTemperature() != null) anthropometry.put("temperatureValue", v.getTemperature()); - if (v.getPulseRate() != null) anthropometry.put("pulseRate", v.getPulseRate()); - if (v.getSystolicBP() != null) anthropometry.put("systolicBP", v.getSystolicBP()); - if (v.getDiastolicBP() != null) anthropometry.put("diastolicBP", v.getDiastolicBP()); - if (v.getBloodGlucoseRandom() != null) anthropometry.put("bloodGlucoseRandom", v.getBloodGlucoseRandom()); + BenPhysicalVitalDetail vital = vitalList.get(0); + if (vital.getTemperature() != null) anthropometry.put("temperatureValue", vital.getTemperature()); + if (vital.getPulseRate() != null) anthropometry.put("pulseRate", vital.getPulseRate()); + if (vital.getSystolicBP() != null) anthropometry.put("systolicBP", vital.getSystolicBP()); + if (vital.getDiastolicBP() != null) anthropometry.put("diastolicBP", vital.getDiastolicBP()); + if (vital.getBloodGlucoseRandom() != null) anthropometry.put("bloodGlucoseRandom", vital.getBloodGlucoseRandom()); } // fallback: if no exam saved yet, read from registration otherFields From d8008c336f7fdd0aaf4bc902c48f629492fbde0e Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Tue, 30 Jun 2026 19:15:03 +0530 Subject: [PATCH 22/37] fix(stoptb): set CreatedDate on t_benvisitdetail insert to prevent NOT NULL constraint violation Co-Authored-By: Claude Sonnet 4.6 --- .../java/com/iemr/flw/service/impl/TBStopVisitServiceImpl.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/iemr/flw/service/impl/TBStopVisitServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/TBStopVisitServiceImpl.java index a54ebe4f..7a34bd34 100644 --- a/src/main/java/com/iemr/flw/service/impl/TBStopVisitServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/TBStopVisitServiceImpl.java @@ -38,6 +38,7 @@ public BenVisitDetail getOrCreateVisitForToday(Long beneficiaryRegID, Integer pr visit.setVisitDateTime(new Timestamp(System.currentTimeMillis())); visit.setProviderServiceMapID(providerServiceMapID); visit.setCreatedBy(createdBy); + visit.setCreatedDate(new Timestamp(System.currentTimeMillis())); visit.setVanId(vanID); visit.setParkingPlaceId(parkingPlaceID); visit.setDeleted(false); From 095f0aeb30983c0f5d820f80b1346fd933fd4537 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Tue, 30 Jun 2026 20:00:56 +0530 Subject: [PATCH 23/37] fix(anc-pnc): guard CreatedDate on BenVisitDetail insert for ANC and PNC flows If mobile does not send createdDate, ModelMapper leaves it null and MySQL rejects the insert with NOT NULL constraint. Set server timestamp as fallback. Co-Authored-By: Claude Sonnet 4.6 --- .../com/iemr/flw/service/impl/MaternalHealthServiceImpl.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/com/iemr/flw/service/impl/MaternalHealthServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/MaternalHealthServiceImpl.java index 52de9aa6..505aac1a 100644 --- a/src/main/java/com/iemr/flw/service/impl/MaternalHealthServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/MaternalHealthServiceImpl.java @@ -205,6 +205,8 @@ public String saveANCVisit(List ancVisitDTOs,Integer userId) { benVisitDetail.setModifiedBy(it.getUpdatedBy()); benVisitDetail.setLastModDate(it.getUpdatedDate()); benVisitDetail.setProviderServiceMapID(it.getProviderServiceMapID()); + if (benVisitDetail.getCreatedDate() == null) + benVisitDetail.setCreatedDate(new java.sql.Timestamp(System.currentTimeMillis())); logger.info("Saving BenVisitDetail"); @@ -406,6 +408,8 @@ public String savePNCVisit(List pncVisitDTOs) { benVisitDetail.setProcessed("N"); benVisitDetail.setModifiedBy(it.getUpdatedBy()); benVisitDetail.setLastModDate(it.getUpdatedDate()); + if (benVisitDetail.getCreatedDate() == null) + benVisitDetail.setCreatedDate(new java.sql.Timestamp(System.currentTimeMillis())); benVisitDetail = benVisitDetailsRepo.save(benVisitDetail); // Saving Data in AncCare table From 3b3a4c5130338d93511f60bd0256a08106651dec Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Wed, 1 Jul 2026 09:17:53 +0530 Subject: [PATCH 24/37] fix(docker): set TZ=Asia/Kolkata to prevent UTC/IST date mismatch Without explicit timezone, Docker defaults to UTC. This causes LocalDate.now() in getOrCreateVisitForToday() to use UTC date, so evening IST registrations appear as next-day July 1 in DB and visit lookups fail after UTC midnight. Co-Authored-By: Claude Sonnet 4.6 --- Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Dockerfile b/Dockerfile index f99697fa..417c00e1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,6 +14,8 @@ FROM eclipse-temurin:17-jre WORKDIR /app +ENV TZ=Asia/Kolkata + # Copy the built WAR file from the build stage COPY --from=build /app/target/*.war app.war From b1d28ae3ae926bf4a58f31b31efeff477cd93f20 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Wed, 1 Jul 2026 17:35:47 +0530 Subject: [PATCH 25/37] fix(stoptb): handle ISO 8601 Z suffix in parseDob to fix beneficiary age display Timestamp.valueOf() throws on "yyyy-MM-dd HH:mm:ss.SSSZ" format because the trailing Z is not a valid nanosecond value. Strip Z before parsing so mobile DOB like "1987-07-01T00:00:00.000Z" is correctly stored in i_ben_flow_outreach.ben_dob. Co-Authored-By: Claude Sonnet 4.6 --- src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java index 2098599b..b60b53c0 100644 --- a/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/StopTBServiceImpl.java @@ -985,7 +985,7 @@ private void writePrescription(StopTBGeneralOpd opd, Long beneficiaryRegID, BenV private Timestamp parseDob(JsonObject obj) { if (!obj.has("dob") || obj.get("dob").isJsonNull()) return null; try { return new Timestamp(obj.get("dob").getAsLong()); } catch (Exception ignored) {} - try { return Timestamp.valueOf(obj.get("dob").getAsString().replace("T", " ")); } catch (Exception ignored) {} + try { return Timestamp.valueOf(obj.get("dob").getAsString().replace("T", " ").replace("Z", "").trim()); } catch (Exception ignored) {} return null; } From 6d92f74920b663bce8e5fbd17e1153add0c92391 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Wed, 1 Jul 2026 17:56:06 +0530 Subject: [PATCH 26/37] fix(stoptb): fallback to i_ben_flow_outreach.ben_dob when i_beneficiarydetails.dob is null Identity-API mapper (@Mapping source=dto.dob) is commented out, causing dob to be null for all mobile registrations. Fall back to ben_dob from i_ben_flow_outreach so getBeneficiaryData returns correct DOB and mobile shows correct age. Co-Authored-By: Claude Sonnet 4.6 --- .../iemr/flw/service/impl/BeneficiaryServiceImpl.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java index a0c5d3b0..d1f9b354 100644 --- a/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java @@ -380,8 +380,15 @@ private String getMappingsForAddressIDs(List addressLi if (benContactOBJ.getPreferredPhoneNum() != null) benDetailsRMNCH_OBJ.setContact_number(benContactOBJ.getPreferredPhoneNum()); - if (benDetailsOBJ.getDob() != null) + if (benDetailsOBJ.getDob() != null) { benDetailsRMNCH_OBJ.setDob(benDetailsOBJ.getDob()); + } else { + // i_beneficiarydetails.dob is null for Stop TB mobile registrations + // (Identity-API mapper commented out) — fall back to i_ben_flow_outreach.ben_dob + List flows = benFlowStatusRepo.findByBeneficiaryRegID(m.getBenRegId().longValue()); + if (!flows.isEmpty() && flows.get(0).getDob() != null) + benDetailsRMNCH_OBJ.setDob(flows.get(0).getDob()); + } if (benDetailsOBJ.getFatherName() != null) benDetailsRMNCH_OBJ.setFatherName(benDetailsOBJ.getFatherName()); if (benDetailsOBJ.getFirstName() != null) From 7459c478d9c0c9a26dca812f33d1fb328b23198e Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Wed, 1 Jul 2026 18:51:19 +0530 Subject: [PATCH 27/37] fix: use saveAndFlush to prevent FK violation in t_benchiefcomplaint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit save() leaves VisitCode UPDATE pending in JPA session — t_benchiefcomplaint INSERT runs before the UPDATE reaches DB, causing FK constraint failure and full transaction rollback. saveAndFlush forces the UPDATE to DB immediately. Co-Authored-By: Claude Sonnet 4.6 --- .../com/iemr/flw/service/impl/TBStopVisitServiceImpl.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/iemr/flw/service/impl/TBStopVisitServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/TBStopVisitServiceImpl.java index 7a34bd34..4f36b8b4 100644 --- a/src/main/java/com/iemr/flw/service/impl/TBStopVisitServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/TBStopVisitServiceImpl.java @@ -48,7 +48,9 @@ public BenVisitDetail getOrCreateVisitForToday(Long beneficiaryRegID, Integer pr // VisitCode: sessionID(1) + vanID(5-digit) + benVisitId(8-digit) — MMU formula long visitCode = generateVisitCode(visit.getBenVisitId(), vanID != null ? vanID : 1); visit.setVisitCode(visitCode); - return benVisitDetailsRepo.save(visit); + // saveAndFlush ensures the VisitCode UPDATE reaches DB before child tables (e.g. t_benchiefcomplaint) + // insert with FK on VisitCode — same transaction, lazy flush otherwise causes FK violation + return benVisitDetailsRepo.saveAndFlush(visit); } private long generateVisitCode(long visitId, int vanID) { From 9fccf6f43a6959305341cbbb7804aa0f66ce1f47 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Fri, 3 Jul 2026 10:16:06 +0530 Subject: [PATCH 28/37] fix(stoptb): compute age from benDetailsRMNCH_OBJ.dob instead of benDetailsOBJ.dob benDetailsOBJ.dob (i_beneficiarydetails) is null for Stop TB mobile registrations, which is why c22b3fd2 added a fallback that fills benDetailsRMNCH_OBJ.dob from i_ben_flow_outreach. The age calculation block still read the original, still-null benDetailsOBJ.dob, so dob came back correct but age/ageFull stayed 0/"". Point it at the already-corrected benDetailsRMNCH_OBJ.dob instead. Co-Authored-By: Claude Sonnet 5 --- .../com/iemr/flw/service/impl/BeneficiaryServiceImpl.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java index d1f9b354..c4c1dc12 100644 --- a/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java @@ -431,9 +431,9 @@ private String getMappingsForAddressIDs(List addressLi String ageDetails = ""; int age_val = 0; String ageUnit = null; - if (benDetailsOBJ.getDob() != null) { + if (benDetailsRMNCH_OBJ.getDob() != null) { - Date date = new Date(benDetailsOBJ.getDob().getTime()); + Date date = new Date(benDetailsRMNCH_OBJ.getDob().getTime()); Calendar cal = Calendar.getInstance(); cal.setTime(date); From 7ddde0a9be3c5f7d872542933a36f0c9562e8ef3 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Fri, 3 Jul 2026 14:50:42 +0530 Subject: [PATCH 29/37] fix(stoptb): scope Suspected/Confirmed getAll by facility+village instead of userId Nurse and Counsellor are different logins hitting the same /tb/suspected/getAll and /tb/confirmed/getAll endpoints, but both queries filtered by the calling user's own userId. So a Counsellor pulling data after a Nurse creates a Suspected/Confirmed record saw none of it - only rows tied to their own login. Adds an optional providerServiceMapID(+villageID)-scoped query path on both endpoints, joined through the already-populated BenFlowStatus table (same pattern as StopTBDiagnosticsRepo), so any role at the same facility/village sees the same worklist. The old userId-based path is kept as the fallback when the new fields aren't sent, so already-deployed app builds are unaffected. --- .../java/com/iemr/flw/controller/TBController.java | 9 ++++++--- .../flw/repo/iemr/TBConfirmedTreatmentRepository.java | 4 ++++ .../java/com/iemr/flw/repo/iemr/TBSuspectedRepo.java | 6 ++++++ .../com/iemr/flw/service/TBConfirmedCaseService.java | 2 ++ .../flw/service/impl/TBConfirmedCaseServiceImpl.java | 10 ++++++++++ .../iemr/flw/service/impl/TBSuspectedServiceImpl.java | 5 ++++- 6 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/iemr/flw/controller/TBController.java b/src/main/java/com/iemr/flw/controller/TBController.java index e95c3ab4..becc387a 100644 --- a/src/main/java/com/iemr/flw/controller/TBController.java +++ b/src/main/java/com/iemr/flw/controller/TBController.java @@ -168,13 +168,16 @@ public String saveConfirmedCase( @Operation(summary = "get tb confirmed case by beneficiary id") @RequestMapping(value = { "/confirmed/getAll" }, method = { RequestMethod.GET }) public String getConfirmedByBenId( - @RequestHeader(value = "jwtToken") String token) { + @RequestHeader(value = "jwtToken") String token, + @RequestParam(required = false) Integer providerServiceMapID, + @RequestParam(required = false) Integer villageID) { OutputResponse response = new OutputResponse(); try { - String result = - tbConfirmedCaseService.getByUserId(token); + String result = providerServiceMapID != null + ? tbConfirmedCaseService.getByProviderServiceMapId(providerServiceMapID, villageID) + : tbConfirmedCaseService.getByUserId(token); if (result != null) response.setResponse(result); diff --git a/src/main/java/com/iemr/flw/repo/iemr/TBConfirmedTreatmentRepository.java b/src/main/java/com/iemr/flw/repo/iemr/TBConfirmedTreatmentRepository.java index 21c941ff..d333a913 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/TBConfirmedTreatmentRepository.java +++ b/src/main/java/com/iemr/flw/repo/iemr/TBConfirmedTreatmentRepository.java @@ -19,6 +19,10 @@ public interface TBConfirmedTreatmentRepository List findByUserId(Integer benId); List findByBenIdAndVisitCode(Long benId, Long visitCode); + @Query("SELECT tc FROM TBConfirmedCase tc WHERE tc.benId IN " + + "(SELECT b.beneficiaryRegID FROM BenFlowStatus b WHERE b.providerServiceMapId = :psmId AND b.villageID = :villageId)") + List getByProviderServiceMapIdAndVillageId(@Param("psmId") Integer psmId, @Param("villageId") Integer villageId); + @Transactional @Modifying @Query("UPDATE TBConfirmedCase t SET t.vanSerialNo = t.id WHERE t.id = :id") diff --git a/src/main/java/com/iemr/flw/repo/iemr/TBSuspectedRepo.java b/src/main/java/com/iemr/flw/repo/iemr/TBSuspectedRepo.java index ef3a2222..c7ca3066 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/TBSuspectedRepo.java +++ b/src/main/java/com/iemr/flw/repo/iemr/TBSuspectedRepo.java @@ -27,6 +27,12 @@ TBSuspected getByUserIdAndBenIdAndVisitCode(@Param("benId") Long benId, @Param(" @Query("SELECT tbs FROM TBSuspected tbs WHERE tbs.userId = :userId and tbs.visitDate >= :fromDate and tbs.visitDate <= :toDate") List getByUserId(@Param("userId") Integer userId, @Param("fromDate") Timestamp fromDate, @Param("toDate") Timestamp toDate); + @Query("SELECT tbs FROM TBSuspected tbs WHERE tbs.benId IN " + + "(SELECT b.beneficiaryRegID FROM BenFlowStatus b WHERE b.providerServiceMapId = :psmId AND b.villageID = :villageId) " + + "AND tbs.visitDate >= :fromDate and tbs.visitDate <= :toDate") + List getByProviderServiceMapIdAndVillageId(@Param("psmId") Integer psmId, @Param("villageId") Integer villageId, + @Param("fromDate") Timestamp fromDate, @Param("toDate") Timestamp toDate); + @Transactional @Modifying @Query("UPDATE TBSuspected t SET t.vanSerialNo = t.id WHERE t.id = :id") diff --git a/src/main/java/com/iemr/flw/service/TBConfirmedCaseService.java b/src/main/java/com/iemr/flw/service/TBConfirmedCaseService.java index 64505b79..6471f799 100644 --- a/src/main/java/com/iemr/flw/service/TBConfirmedCaseService.java +++ b/src/main/java/com/iemr/flw/service/TBConfirmedCaseService.java @@ -13,4 +13,6 @@ public interface TBConfirmedCaseService { String getByBenId(Long benId, String authorisation) throws Exception; String getByUserId(String authorisation) throws Exception; + + String getByProviderServiceMapId(Integer providerServiceMapID, Integer villageID) throws Exception; } diff --git a/src/main/java/com/iemr/flw/service/impl/TBConfirmedCaseServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/TBConfirmedCaseServiceImpl.java index a38073d6..a2785825 100644 --- a/src/main/java/com/iemr/flw/service/impl/TBConfirmedCaseServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/TBConfirmedCaseServiceImpl.java @@ -145,6 +145,16 @@ public String getByBenId(Long benId, String authorisation) throws Exception { public String getByUserId(String authorisation) throws Exception { Integer userId = jwtUtil.extractUserId(authorisation); List list = repository.findByUserId(userId); + return buildTbConfirmedCasesResponse(userId, list); + } + + @Override + public String getByProviderServiceMapId(Integer providerServiceMapID, Integer villageID) throws Exception { + List list = repository.getByProviderServiceMapIdAndVillageId(providerServiceMapID, villageID); + return buildTbConfirmedCasesResponse(null, list); + } + + private String buildTbConfirmedCasesResponse(Integer userId, List list) { List dtoList = list.stream().map(this::toDTO).collect(Collectors.toList()); List benIds = dtoList.stream().map(TBConfirmedCaseDTO::getBenId).collect(Collectors.toList()); diff --git a/src/main/java/com/iemr/flw/service/impl/TBSuspectedServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/TBSuspectedServiceImpl.java index d675a01d..24dd63fb 100644 --- a/src/main/java/com/iemr/flw/service/impl/TBSuspectedServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/TBSuspectedServiceImpl.java @@ -71,7 +71,10 @@ public String save(TBSuspectedRequestDTO requestDTO) throws Exception { @Override public String getByUserId(GetBenRequestHandler request) { List dtos = new ArrayList<>(); - List tbSuspectedList = tbSuspectedRepo.getByUserId(request.getAshaId(), request.getFromDate(), request.getToDate()); + List tbSuspectedList = request.getProviderServiceMapID() != null + ? tbSuspectedRepo.getByProviderServiceMapIdAndVillageId(request.getProviderServiceMapID(), request.getVillageID(), + request.getFromDate(), request.getToDate()) + : tbSuspectedRepo.getByUserId(request.getAshaId(), request.getFromDate(), request.getToDate()); for (TBSuspected tbSuspected : tbSuspectedList) { TBSuspectedDTO dto = modelMapper.map(tbSuspected, TBSuspectedDTO.class); dto.setUpdateDate(tbSuspected.getLastModDate()); From 33c4f4d590ebdf4f60ecc5083d8c7f143af66160 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Fri, 3 Jul 2026 15:07:04 +0530 Subject: [PATCH 30/37] fix(stoptb): drop date filter from facility-scoped Suspected query visitDate isn't the right dimension for a facility worklist - an unresolved Suspected case from months ago is still actionable, and filtering it out by date would hide it from the Counsellor, working against the visibility fix in the previous commit. Also matches StopTBDiagnosticsRepo's facility-scoped query, which never date-filters. Old ashaId-based fallback path is untouched and still date-filters as before. --- src/main/java/com/iemr/flw/repo/iemr/TBSuspectedRepo.java | 6 ++---- .../com/iemr/flw/service/impl/TBSuspectedServiceImpl.java | 3 +-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/iemr/flw/repo/iemr/TBSuspectedRepo.java b/src/main/java/com/iemr/flw/repo/iemr/TBSuspectedRepo.java index c7ca3066..c7b3ab35 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/TBSuspectedRepo.java +++ b/src/main/java/com/iemr/flw/repo/iemr/TBSuspectedRepo.java @@ -28,10 +28,8 @@ TBSuspected getByUserIdAndBenIdAndVisitCode(@Param("benId") Long benId, @Param(" List getByUserId(@Param("userId") Integer userId, @Param("fromDate") Timestamp fromDate, @Param("toDate") Timestamp toDate); @Query("SELECT tbs FROM TBSuspected tbs WHERE tbs.benId IN " + - "(SELECT b.beneficiaryRegID FROM BenFlowStatus b WHERE b.providerServiceMapId = :psmId AND b.villageID = :villageId) " + - "AND tbs.visitDate >= :fromDate and tbs.visitDate <= :toDate") - List getByProviderServiceMapIdAndVillageId(@Param("psmId") Integer psmId, @Param("villageId") Integer villageId, - @Param("fromDate") Timestamp fromDate, @Param("toDate") Timestamp toDate); + "(SELECT b.beneficiaryRegID FROM BenFlowStatus b WHERE b.providerServiceMapId = :psmId AND b.villageID = :villageId)") + List getByProviderServiceMapIdAndVillageId(@Param("psmId") Integer psmId, @Param("villageId") Integer villageId); @Transactional @Modifying diff --git a/src/main/java/com/iemr/flw/service/impl/TBSuspectedServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/TBSuspectedServiceImpl.java index 24dd63fb..1f5856a1 100644 --- a/src/main/java/com/iemr/flw/service/impl/TBSuspectedServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/TBSuspectedServiceImpl.java @@ -72,8 +72,7 @@ public String save(TBSuspectedRequestDTO requestDTO) throws Exception { public String getByUserId(GetBenRequestHandler request) { List dtos = new ArrayList<>(); List tbSuspectedList = request.getProviderServiceMapID() != null - ? tbSuspectedRepo.getByProviderServiceMapIdAndVillageId(request.getProviderServiceMapID(), request.getVillageID(), - request.getFromDate(), request.getToDate()) + ? tbSuspectedRepo.getByProviderServiceMapIdAndVillageId(request.getProviderServiceMapID(), request.getVillageID()) : tbSuspectedRepo.getByUserId(request.getAshaId(), request.getFromDate(), request.getToDate()); for (TBSuspected tbSuspected : tbSuspectedList) { TBSuspectedDTO dto = modelMapper.map(tbSuspected, TBSuspectedDTO.class); From 349e714496d228a50ecbdd74ecb787c1e90e4d19 Mon Sep 17 00:00:00 2001 From: vishwab1 Date: Fri, 3 Jul 2026 16:14:11 +0530 Subject: [PATCH 31/37] fix(stoptb): join facility-scoped query on beneficiaryID, not beneficiaryRegID TBSuspected.benId / TBConfirmedCase.benId store the person-level beneficiary_id (e.g. 907797933607), not beneficiaryRegID (e.g. 35137) - confirmed by querying i_ben_flow_outreach directly. The facility-scoped query added in the previous two commits joined against BenFlowStatus.beneficiaryRegID, so it never matched anything and always returned empty. Switched the join to BenFlowStatus.beneficiaryID, which is the correct corresponding column. --- .../com/iemr/flw/repo/iemr/TBConfirmedTreatmentRepository.java | 2 +- src/main/java/com/iemr/flw/repo/iemr/TBSuspectedRepo.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/iemr/flw/repo/iemr/TBConfirmedTreatmentRepository.java b/src/main/java/com/iemr/flw/repo/iemr/TBConfirmedTreatmentRepository.java index d333a913..122890e3 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/TBConfirmedTreatmentRepository.java +++ b/src/main/java/com/iemr/flw/repo/iemr/TBConfirmedTreatmentRepository.java @@ -20,7 +20,7 @@ public interface TBConfirmedTreatmentRepository List findByBenIdAndVisitCode(Long benId, Long visitCode); @Query("SELECT tc FROM TBConfirmedCase tc WHERE tc.benId IN " + - "(SELECT b.beneficiaryRegID FROM BenFlowStatus b WHERE b.providerServiceMapId = :psmId AND b.villageID = :villageId)") + "(SELECT b.beneficiaryID FROM BenFlowStatus b WHERE b.providerServiceMapId = :psmId AND b.villageID = :villageId)") List getByProviderServiceMapIdAndVillageId(@Param("psmId") Integer psmId, @Param("villageId") Integer villageId); @Transactional diff --git a/src/main/java/com/iemr/flw/repo/iemr/TBSuspectedRepo.java b/src/main/java/com/iemr/flw/repo/iemr/TBSuspectedRepo.java index c7b3ab35..e559bdfb 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/TBSuspectedRepo.java +++ b/src/main/java/com/iemr/flw/repo/iemr/TBSuspectedRepo.java @@ -28,7 +28,7 @@ TBSuspected getByUserIdAndBenIdAndVisitCode(@Param("benId") Long benId, @Param(" List getByUserId(@Param("userId") Integer userId, @Param("fromDate") Timestamp fromDate, @Param("toDate") Timestamp toDate); @Query("SELECT tbs FROM TBSuspected tbs WHERE tbs.benId IN " + - "(SELECT b.beneficiaryRegID FROM BenFlowStatus b WHERE b.providerServiceMapId = :psmId AND b.villageID = :villageId)") + "(SELECT b.beneficiaryID FROM BenFlowStatus b WHERE b.providerServiceMapId = :psmId AND b.villageID = :villageId)") List getByProviderServiceMapIdAndVillageId(@Param("psmId") Integer psmId, @Param("villageId") Integer villageId); @Transactional From 4d6606ea7df3bf206828eb1bd2ead2a869718a18 Mon Sep 17 00:00:00 2001 From: Sehjot Singh Pannu Date: Tue, 23 Jun 2026 08:53:41 +0530 Subject: [PATCH 32/37] Added counselled flag to confirmed cases response, indicating whether a beneficiary has completed the TB Counselling form. Implemented using a single bulk query to avoid N+1 lookups against FormResponse. --- .../java/com/iemr/flw/repo/iemr/FormResponseRepo.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/java/com/iemr/flw/repo/iemr/FormResponseRepo.java b/src/main/java/com/iemr/flw/repo/iemr/FormResponseRepo.java index 66672c16..e902ee94 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/FormResponseRepo.java +++ b/src/main/java/com/iemr/flw/repo/iemr/FormResponseRepo.java @@ -60,6 +60,14 @@ List findCounselledBenIds(@Param("benIds") List benIds, @Param("formId") Long formId, @Param("status") String status); + @Query("SELECT r.beneficiaryId FROM FormResponse r " + + "WHERE r.beneficiaryId IN :benIds " + + "AND r.formId = :formId " + + "AND r.status = :status") + List findCounselledBenIds(@Param("benIds") List benIds, + @Param("formId") Long formId, + @Param("status") String status); + @Query("SELECT r FROM FormResponse r " + "WHERE r.formId IN :formIds " + "AND r.status = 'SUBMITTED' " From 33219a156ae15a5327dd4b9a7cb06288e517416c Mon Sep 17 00:00:00 2001 From: Sehjot Singh Pannu Date: Fri, 26 Jun 2026 13:23:03 +0530 Subject: [PATCH 33/37] Revert "Committed 82b00ccf with only the three files:" This reverts commit 3040a08077d82d27dfd86f29f0fbdd4dc094ec56. --- src/main/java/com/iemr/flw/domain/iemr/OptionCondition.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/iemr/flw/domain/iemr/OptionCondition.java b/src/main/java/com/iemr/flw/domain/iemr/OptionCondition.java index 50af2b38..281a87d0 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/OptionCondition.java +++ b/src/main/java/com/iemr/flw/domain/iemr/OptionCondition.java @@ -98,6 +98,6 @@ public class OptionCondition { @Column(name = "Syncedby", length = 50) private String syncedBy; - @Column(name = "SyncFailureReason") + @Column(name = "SyncFailureReason", length = 255) private String syncFailureReason; } From 695122192b1cbd9901b692ece35f9fcded4d61f3 Mon Sep 17 00:00:00 2001 From: Sehjot Singh Pannu Date: Fri, 26 Jun 2026 13:30:30 +0530 Subject: [PATCH 34/37] =?UTF-8?q?Committed=2082b00ccf=20with=20only=20the?= =?UTF-8?q?=20three=20files:=20=20=20-=20BeneficiaryServiceImpl.java=20?= =?UTF-8?q?=E2=80=94=20serializeNulls()=20+=20GPS=20fallback=20from=20addr?= =?UTF-8?q?ess=20=20=20-=20RMNCHBeneficiaryDetailsRmnch.java=20=E2=80=94?= =?UTF-8?q?=20gpsLatitude,=20gpsLongitude,=20digipin,=20gpsTimestamp,=20is?= =?UTF-8?q?GpsUnavailable=20fields=20=20=20-=20RMNCHHouseHoldDetails.java?= =?UTF-8?q?=20=E2=80=94=20gpsLatitude,=20gpsLongitude=20fields?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/iemr/flw/domain/iemr/OptionCondition.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/iemr/flw/domain/iemr/OptionCondition.java b/src/main/java/com/iemr/flw/domain/iemr/OptionCondition.java index 281a87d0..50af2b38 100644 --- a/src/main/java/com/iemr/flw/domain/iemr/OptionCondition.java +++ b/src/main/java/com/iemr/flw/domain/iemr/OptionCondition.java @@ -98,6 +98,6 @@ public class OptionCondition { @Column(name = "Syncedby", length = 50) private String syncedBy; - @Column(name = "SyncFailureReason", length = 255) + @Column(name = "SyncFailureReason") private String syncFailureReason; } From e2cb73b77a1749992263de2e6e4530c6f21a54d5 Mon Sep 17 00:00:00 2001 From: Sehjot Singh Pannu Date: Fri, 26 Jun 2026 13:20:35 +0530 Subject: [PATCH 35/37] feat(beneficiary): expose GPS coordinates in getBeneficiaryData response MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add gpsLatitude, gpsLongitude, digipin, gpsTimestamp, and isGpsUnavailable fields to RMNCHBeneficiaryDetailsRmnch and RMNCHHouseHoldDetails entities, mapping to the columns written by Identity-API's /syncDataToAmrit endpoint. Enable serializeNulls on the Gson builder in BeneficiaryServiceImpl so GPS keys are always present in householdDetails and beneficiaryDetails — null for web-registered records, real values for mobile-registered ones. Co-Authored-By: Claude Sonnet 4.6 --- .../service/impl/BeneficiaryServiceImpl.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java index 829f9486..597abf5a 100644 --- a/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/BeneficiaryServiceImpl.java @@ -358,6 +358,25 @@ private String getMappingsForAddressIDs(List addressLi if (benDetailsRMNCH_OBJ.getGpsLongitude() != null) benDetailsRMNCH_OBJ.setLongitude(BigDecimal.valueOf(benDetailsRMNCH_OBJ.getGpsLongitude())); + // GPS fallback: if not in RMNCH details (syncdatatoamrti not yet called), + // pull from i_beneficiaryaddress (saved during TM-API registration) + if (benDetailsRMNCH_OBJ.getGpsLatitude() == null && benAddressOBJ.getGpsLatitude() != null) + benDetailsRMNCH_OBJ.setGpsLatitude(benAddressOBJ.getGpsLatitude()); + if (benDetailsRMNCH_OBJ.getGpsLongitude() == null && benAddressOBJ.getGpsLongitude() != null) + benDetailsRMNCH_OBJ.setGpsLongitude(benAddressOBJ.getGpsLongitude()); + if (benDetailsRMNCH_OBJ.getDigipin() == null && benAddressOBJ.getDigipin() != null) + benDetailsRMNCH_OBJ.setDigipin(benAddressOBJ.getDigipin()); + if (benDetailsRMNCH_OBJ.getGpsTimestamp() == null && benAddressOBJ.getGpsTimestamp() != null) + benDetailsRMNCH_OBJ.setGpsTimestamp(benAddressOBJ.getGpsTimestamp()); + if (benDetailsRMNCH_OBJ.getIsGpsUnavailable() == null && benAddressOBJ.getIsGpsUnavailable() != null) + benDetailsRMNCH_OBJ.setIsGpsUnavailable(benAddressOBJ.getIsGpsUnavailable()); + + // Map GPS double fields to the exposed latitude/longitude BigDecimal fields for response + if (benDetailsRMNCH_OBJ.getGpsLatitude() != null) + benDetailsRMNCH_OBJ.setLatitude(BigDecimal.valueOf(benDetailsRMNCH_OBJ.getGpsLatitude())); + if (benDetailsRMNCH_OBJ.getGpsLongitude() != null) + benDetailsRMNCH_OBJ.setLongitude(BigDecimal.valueOf(benDetailsRMNCH_OBJ.getGpsLongitude())); + // ----------------------------------------------------------------------------- // related benids From 043ab844c59f14e8d36bf9643c8fbd4419908608 Mon Sep 17 00:00:00 2001 From: Sehjot Singh Pannu Date: Tue, 7 Jul 2026 16:13:38 +0530 Subject: [PATCH 36/37] Changing working of checkbox as of radio button --- .../java/com/iemr/flw/dto/iemr/QuestionAnswerRequest.java | 1 + .../java/com/iemr/flw/dto/iemr/QuestionResponseDTO.java | 2 +- src/main/java/com/iemr/flw/masterEnum/QuestionType.java | 4 ++-- .../java/com/iemr/flw/repo/iemr/FormResponseRepo.java | 8 -------- .../flw/service/impl/DynamicFormResponseServiceImpl.java | 4 ++-- .../com/iemr/flw/service/impl/FormResponseItemSaver.java | 4 ++-- 6 files changed, 8 insertions(+), 15 deletions(-) diff --git a/src/main/java/com/iemr/flw/dto/iemr/QuestionAnswerRequest.java b/src/main/java/com/iemr/flw/dto/iemr/QuestionAnswerRequest.java index 8ee08c45..ad314ba5 100644 --- a/src/main/java/com/iemr/flw/dto/iemr/QuestionAnswerRequest.java +++ b/src/main/java/com/iemr/flw/dto/iemr/QuestionAnswerRequest.java @@ -33,6 +33,7 @@ * A single question's answer within a section answer request. * Exactly one of optionValue, optionValues, answerText, or answerDate should be set per question type: * RADIO → optionValue (single string) + * CHECKBOX → optionValue (single string, e.g. "CHECKED") * MCQ → optionValues (list of strings, one row saved per element) * TEXT/AUTO_FILL → answerText * DATE → answerDate (ISO date string) or answerText diff --git a/src/main/java/com/iemr/flw/dto/iemr/QuestionResponseDTO.java b/src/main/java/com/iemr/flw/dto/iemr/QuestionResponseDTO.java index f5f76d63..98557c10 100644 --- a/src/main/java/com/iemr/flw/dto/iemr/QuestionResponseDTO.java +++ b/src/main/java/com/iemr/flw/dto/iemr/QuestionResponseDTO.java @@ -40,7 +40,7 @@ public class QuestionResponseDTO { private Long questionResponseId; private Long questionId; - /** Populated for RADIO and MCQ answers. */ + /** Populated for RADIO, MCQ, and CHECKBOX answers. */ private Long optionId; /** Populated for TEXT, DATE, AUTO_FILL answers. */ private String answerText; diff --git a/src/main/java/com/iemr/flw/masterEnum/QuestionType.java b/src/main/java/com/iemr/flw/masterEnum/QuestionType.java index 0af07aa1..ea8ed677 100644 --- a/src/main/java/com/iemr/flw/masterEnum/QuestionType.java +++ b/src/main/java/com/iemr/flw/masterEnum/QuestionType.java @@ -32,8 +32,8 @@ public enum QuestionType { RADIO, /** Multi-select from a predefined list of options. */ MCQ, - /** Single boolean checkbox — checked or unchecked. May carry options for display - * purposes; answers are still stored as free text. */ + /** Single boolean checkbox — checked or unchecked. Resolved like RADIO: submit + * optionValue (e.g. "CHECKED") to select it, omit to leave unanswered. */ CHECKBOX, /** Free-text input. */ TEXT, diff --git a/src/main/java/com/iemr/flw/repo/iemr/FormResponseRepo.java b/src/main/java/com/iemr/flw/repo/iemr/FormResponseRepo.java index e902ee94..66672c16 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/FormResponseRepo.java +++ b/src/main/java/com/iemr/flw/repo/iemr/FormResponseRepo.java @@ -60,14 +60,6 @@ List findCounselledBenIds(@Param("benIds") List benIds, @Param("formId") Long formId, @Param("status") String status); - @Query("SELECT r.beneficiaryId FROM FormResponse r " + - "WHERE r.beneficiaryId IN :benIds " + - "AND r.formId = :formId " + - "AND r.status = :status") - List findCounselledBenIds(@Param("benIds") List benIds, - @Param("formId") Long formId, - @Param("status") String status); - @Query("SELECT r FROM FormResponse r " + "WHERE r.formId IN :formIds " + "AND r.status = 'SUBMITTED' " diff --git a/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java index f6b26d4a..6cf3fd3a 100644 --- a/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java @@ -389,7 +389,7 @@ private List processAnswers( // Delete any existing answers for this question in this section (handles re-saves) questionResponseRepo.deleteByQuestionIdAndSectionResponseId(questionId, sectionResponseId); - if (type == QuestionType.RADIO) { + if (type == QuestionType.RADIO || type == QuestionType.CHECKBOX) { if (answer.getOptionValue() != null) { QuestionOption opt = resolveOption( optionsByQuestion, questionId, answer.getOptionValue(), answer.getQuestionUuid()); @@ -420,7 +420,7 @@ private List processAnswers( } } } else { - // TEXT, DATE, AUTO_FILL, CHECKBOX — prefer answerText, then answerDate, then optionValue (legacy) + // TEXT, DATE, AUTO_FILL — prefer answerText, then answerDate, then optionValue (legacy) String value = answer.getAnswerText() != null ? answer.getAnswerText() : answer.getAnswerDate() != null ? answer.getAnswerDate() : answer.getOptionValue(); diff --git a/src/main/java/com/iemr/flw/service/impl/FormResponseItemSaver.java b/src/main/java/com/iemr/flw/service/impl/FormResponseItemSaver.java index 4767f505..fd3688c0 100644 --- a/src/main/java/com/iemr/flw/service/impl/FormResponseItemSaver.java +++ b/src/main/java/com/iemr/flw/service/impl/FormResponseItemSaver.java @@ -328,7 +328,7 @@ private List processAnswers( questionResponseRepo.deleteByQuestionIdAndSectionResponseId(questionId, sectionResponseId); - if (type == QuestionType.RADIO) { + if (type == QuestionType.RADIO || type == QuestionType.CHECKBOX) { if (answer.getOptionValue() != null) { QuestionOption opt = resolveOption(optionsByQuestion, questionId, answer.getOptionValue(), answer.getQuestionUuid()); @@ -359,7 +359,7 @@ private List processAnswers( } } } else { - // TEXT, DATE, AUTO_FILL, CHECKBOX — prefer answerText, then answerDate, then optionValue (legacy) + // TEXT, DATE, AUTO_FILL — prefer answerText, then answerDate, then optionValue (legacy) String value = answer.getAnswerText() != null ? answer.getAnswerText() : answer.getAnswerDate() != null ? answer.getAnswerDate() : answer.getOptionValue(); From 9a743b6006a9ae209b888e4b9e835ea09279b90b Mon Sep 17 00:00:00 2001 From: Sehjot Singh Pannu Date: Fri, 10 Jul 2026 17:28:30 +0530 Subject: [PATCH 37/37] fix(counselling): make /submitBulk update sections in place (respecting isEditable) and share the consent-refusal check with /complete via ConsentRefusalEvaluator feat(counselling): return per-beneficiary section-fill progress from getCompletedBeneficiaries instead of a flat beneficiary-ID split --- .../DynamicFormResponseController.java | 6 +- ...ava => CompletedBeneficiaryDetailDTO.java} | 16 ++-- .../iemr/flw/repo/iemr/FormResponseRepo.java | 14 ++-- .../iemr/flw/repo/iemr/FormSectionRepo.java | 11 +++ .../flw/repo/iemr/SectionResponseRepo.java | 10 +++ .../service/DynamicFormResponseService.java | 9 +- .../service/impl/ConsentRefusalEvaluator.java | 76 +++++++++++++++++ .../impl/DynamicFormResponseServiceImpl.java | 83 ++++++++----------- .../service/impl/FormResponseItemSaver.java | 43 +++++----- 9 files changed, 174 insertions(+), 94 deletions(-) rename src/main/java/com/iemr/flw/dto/iemr/{CompletedBeneficiariesDTO.java => CompletedBeneficiaryDetailDTO.java} (76%) create mode 100644 src/main/java/com/iemr/flw/service/impl/ConsentRefusalEvaluator.java diff --git a/src/main/java/com/iemr/flw/controller/DynamicFormResponseController.java b/src/main/java/com/iemr/flw/controller/DynamicFormResponseController.java index 33ac46b2..d5ec7597 100644 --- a/src/main/java/com/iemr/flw/controller/DynamicFormResponseController.java +++ b/src/main/java/com/iemr/flw/controller/DynamicFormResponseController.java @@ -21,7 +21,7 @@ */ package com.iemr.flw.controller; -import com.iemr.flw.dto.iemr.CompletedBeneficiariesDTO; +import com.iemr.flw.dto.iemr.CompletedBeneficiaryDetailDTO; import com.iemr.flw.dto.iemr.FormResponseDTO; import com.iemr.flw.dto.iemr.FormResponseRequest; import com.iemr.flw.masterEnum.FormType; @@ -99,13 +99,13 @@ public ResponseEntity getById(@RequestParam Long responseId) { FormResponseDTO dto = responseService.getResponseById(responseId); return ResponseEntity.ok(new ApiResponse(true, "Response fetched successfully", dto)); } - @Operation(summary = "Get beneficiary IDs that have a COMPLETE or REFUSED form response for the given form type, optionally filtered by village and/or provider service map") + @Operation(summary = "Get COMPLETE/REFUSED form-response details (beneficiaryId, refusal flag, sections filled vs total) for the given form type, optionally filtered by village and/or provider service map") @RequestMapping(value = "/getCompletedBeneficiaries", method = RequestMethod.GET) public ResponseEntity getCompletedBeneficiaries( @RequestParam FormType formType, @RequestParam(required = false) Integer villageId, @RequestParam(required = false) Integer providerServiceMapId) { - CompletedBeneficiariesDTO result = + List result = responseService.getCompletedBeneficiaries(formType, villageId, providerServiceMapId); return ResponseEntity.ok(new ApiResponse(true, "Completed beneficiaries fetched successfully", result)); } diff --git a/src/main/java/com/iemr/flw/dto/iemr/CompletedBeneficiariesDTO.java b/src/main/java/com/iemr/flw/dto/iemr/CompletedBeneficiaryDetailDTO.java similarity index 76% rename from src/main/java/com/iemr/flw/dto/iemr/CompletedBeneficiariesDTO.java rename to src/main/java/com/iemr/flw/dto/iemr/CompletedBeneficiaryDetailDTO.java index b391cff0..a505fc67 100644 --- a/src/main/java/com/iemr/flw/dto/iemr/CompletedBeneficiariesDTO.java +++ b/src/main/java/com/iemr/flw/dto/iemr/CompletedBeneficiaryDetailDTO.java @@ -26,11 +26,9 @@ import lombok.Data; import lombok.NoArgsConstructor; -import java.util.List; - /** - * Beneficiary IDs for a form type, split by outcome: those who completed the form and those - * who refused (TB2_GI_Q1 = NO on the General Info consent question). + * Per-beneficiary detail for a COMPLETE/REFUSED form response: outcome plus section-fill progress + * against the exact form version that response was submitted against. * * @author Piramal Swasthya */ @@ -38,7 +36,9 @@ @NoArgsConstructor @AllArgsConstructor @Builder -public class CompletedBeneficiariesDTO { - private List completed; - private List refused; -} \ No newline at end of file +public class CompletedBeneficiaryDetailDTO { + private Long beneficiaryId; + private boolean isRefused; + private int sectionsFilled; + private int totalSections; +} diff --git a/src/main/java/com/iemr/flw/repo/iemr/FormResponseRepo.java b/src/main/java/com/iemr/flw/repo/iemr/FormResponseRepo.java index 66672c16..6c30fdfb 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/FormResponseRepo.java +++ b/src/main/java/com/iemr/flw/repo/iemr/FormResponseRepo.java @@ -40,17 +40,17 @@ public interface FormResponseRepo extends JpaRepository { List findByBeneficiaryIdAndFormId(Long beneficiaryId, Long formId); - @Query("SELECT r.beneficiaryId FROM FormResponse r WHERE r.formId = :formId AND r.status = :status") - List findBeneficiaryIdsByFormIdAndStatus(@Param("formId") Long formId, @Param("status") String status); + @Query("SELECT r FROM FormResponse r WHERE r.formId = :formId AND r.status IN :statuses") + List findByFormIdAndStatusIn(@Param("formId") Long formId, @Param("statuses") List statuses); - @Query("SELECT r.beneficiaryId FROM FormResponse r WHERE r.formId = :formId AND r.status = :status " + + @Query("SELECT r FROM FormResponse r WHERE r.formId = :formId AND r.status IN :statuses " + "AND r.beneficiaryId IN (SELECT b.beneficiaryID FROM BenFlowStatus b WHERE b.deleted = false " + "AND (:villageId IS NULL OR b.villageID = :villageId) " + "AND (:providerServiceMapId IS NULL OR b.providerServiceMapId = :providerServiceMapId))") - List findBeneficiaryIdsByFormIdAndStatusFiltered(@Param("formId") Long formId, - @Param("status") String status, - @Param("villageId") Integer villageId, - @Param("providerServiceMapId") Integer providerServiceMapId); + List findByFormIdAndStatusInFiltered(@Param("formId") Long formId, + @Param("statuses") List statuses, + @Param("villageId") Integer villageId, + @Param("providerServiceMapId") Integer providerServiceMapId); @Query("SELECT r.beneficiaryId FROM FormResponse r " + "WHERE r.beneficiaryId IN :benIds " + diff --git a/src/main/java/com/iemr/flw/repo/iemr/FormSectionRepo.java b/src/main/java/com/iemr/flw/repo/iemr/FormSectionRepo.java index b4064395..9f135959 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/FormSectionRepo.java +++ b/src/main/java/com/iemr/flw/repo/iemr/FormSectionRepo.java @@ -22,9 +22,13 @@ package com.iemr.flw.repo.iemr; import com.iemr.flw.domain.iemr.FormSection; +import com.iemr.flw.masterEnum.SectionPhase; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import java.util.Collection; import java.util.List; /** @@ -35,4 +39,11 @@ public interface FormSectionRepo extends JpaRepository { List findByFormVersion_VersionIdOrderByDisplayOrderAsc(Long versionId); + + @Query("SELECT fs.formVersion.versionId, COUNT(fs) FROM FormSection fs " + + "WHERE fs.formVersion.versionId IN :versionIds " + + "AND fs.sectionPhase = :sectionPhase " + + "GROUP BY fs.formVersion.versionId") + List countByFormVersion_VersionIdInAndSectionPhase(@Param("versionIds") Collection versionIds, + @Param("sectionPhase") SectionPhase sectionPhase); } diff --git a/src/main/java/com/iemr/flw/repo/iemr/SectionResponseRepo.java b/src/main/java/com/iemr/flw/repo/iemr/SectionResponseRepo.java index b83ad8e9..2f485ad3 100644 --- a/src/main/java/com/iemr/flw/repo/iemr/SectionResponseRepo.java +++ b/src/main/java/com/iemr/flw/repo/iemr/SectionResponseRepo.java @@ -22,7 +22,10 @@ package com.iemr.flw.repo.iemr; import com.iemr.flw.domain.iemr.SectionResponse; +import com.iemr.flw.masterEnum.SectionPhase; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import java.util.Collection; @@ -44,4 +47,11 @@ public interface SectionResponseRepo extends JpaRepository findByResponseIdIn(Collection responseIds); void deleteByResponseId(Long responseId); + + @Query("SELECT sr.responseId, COUNT(sr) FROM SectionResponse sr, FormSection fs " + + "WHERE sr.sectionId = fs.sectionId " + + "AND fs.sectionPhase = :sectionPhase " + + "AND sr.responseId IN :responseIds GROUP BY sr.responseId") + List countByResponseIdInAndSectionPhase(@Param("responseIds") Collection responseIds, + @Param("sectionPhase") SectionPhase sectionPhase); } diff --git a/src/main/java/com/iemr/flw/service/DynamicFormResponseService.java b/src/main/java/com/iemr/flw/service/DynamicFormResponseService.java index 61a3bce1..5c4c1f1d 100644 --- a/src/main/java/com/iemr/flw/service/DynamicFormResponseService.java +++ b/src/main/java/com/iemr/flw/service/DynamicFormResponseService.java @@ -21,7 +21,7 @@ */ package com.iemr.flw.service; -import com.iemr.flw.dto.iemr.CompletedBeneficiariesDTO; +import com.iemr.flw.dto.iemr.CompletedBeneficiaryDetailDTO; import com.iemr.flw.dto.iemr.FormResponseDTO; import com.iemr.flw.dto.iemr.FormResponseRequest; import com.iemr.flw.masterEnum.FormType; @@ -63,8 +63,9 @@ public interface DynamicFormResponseService { List findPendingFollowUps(List formIds, int delayDays); /** - * Returns beneficiary IDs for the given form type split by outcome — COMPLETE and REFUSED — - * optionally filtered by village and/or provider service map. + * Returns per-beneficiary detail (outcome plus section-fill progress) for COMPLETE and REFUSED + * responses to the given form type, optionally filtered by village and/or provider service map. + * sectionsFilled/totalSections only count PRE_SUBMIT-phase sections. */ - CompletedBeneficiariesDTO getCompletedBeneficiaries(FormType formType, Integer villageId, Integer providerServiceMapId); + List getCompletedBeneficiaries(FormType formType, Integer villageId, Integer providerServiceMapId); } diff --git a/src/main/java/com/iemr/flw/service/impl/ConsentRefusalEvaluator.java b/src/main/java/com/iemr/flw/service/impl/ConsentRefusalEvaluator.java new file mode 100644 index 00000000..25794573 --- /dev/null +++ b/src/main/java/com/iemr/flw/service/impl/ConsentRefusalEvaluator.java @@ -0,0 +1,76 @@ +/* + * 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.service.impl; + +import com.iemr.flw.domain.iemr.FormSection; +import com.iemr.flw.dto.iemr.FormResponseRequest; +import com.iemr.flw.dto.iemr.QuestionAnswerRequest; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * TB2_SEC_GENERAL_INFO's consent question (TB2_GI_Q1) gates the rest of the TB Counselling + * form; a "NO" answer means the beneficiary refused counselling. Shared by /complete and + * /submitBulk so both apply the same refusal rule. + * + * @author Piramal Swasthya + */ +@Component +public class ConsentRefusalEvaluator { + + private static final String SECTION_UUID_GENERAL_INFO = "TB2_SEC_GENERAL_INFO"; + private static final String QUESTION_UUID_CONSENT = "TB2_GI_Q1"; + private static final String OPTION_VALUE_CONSENT_REFUSED = "NO"; + + public boolean isRefusalAnswer(QuestionAnswerRequest answer) { + return QUESTION_UUID_CONSENT.equals(answer.getQuestionUuid()) + && OPTION_VALUE_CONSENT_REFUSED.equals(answer.getOptionValue()); + } + + // Scanned directly off the raw request (before any FormSection is resolved) so the + // top-level FormResponse status can be decided. Absence-tolerant: no TB2_SEC_GENERAL_INFO + // section, no answers, or no TB2_GI_Q1 answer at all just means "not refused". + public boolean isConsentRefused(FormResponseRequest request) { + if (request.getSections() == null) { + return false; + } + return request.getSections().stream() + .filter(s -> SECTION_UUID_GENERAL_INFO.equals(s.getSectionUuid())) + .filter(s -> s.getAnswers() != null) + .flatMap(s -> s.getAnswers().stream()) + .anyMatch(this::isRefusalAnswer); + } + + // Same TB2_GI_Q1="NO" signal as isConsentRefused, but resolved per-section so callers can + // set a section's own status. Absence-tolerant by design: any other section, a missing + // answers list, or no answer for the consent question at all just falls through to + // doneStatus. + public String determineSectionStatus(FormSection section, List answers, + String doneStatus, String refusedStatus) { + if (!SECTION_UUID_GENERAL_INFO.equals(section.getSectionUuid()) || answers == null) { + return doneStatus; + } + boolean refused = answers.stream().anyMatch(this::isRefusalAnswer); + return refused ? refusedStatus : doneStatus; + } +} \ No newline at end of file diff --git a/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java b/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java index a36710a5..de940667 100644 --- a/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java +++ b/src/main/java/com/iemr/flw/service/impl/DynamicFormResponseServiceImpl.java @@ -28,7 +28,7 @@ import com.iemr.flw.domain.iemr.QuestionResponse; import com.iemr.flw.domain.iemr.SectionQuestion; import com.iemr.flw.domain.iemr.SectionResponse; -import com.iemr.flw.dto.iemr.CompletedBeneficiariesDTO; +import com.iemr.flw.dto.iemr.CompletedBeneficiaryDetailDTO; import com.iemr.flw.dto.iemr.FormResponseDTO; import com.iemr.flw.dto.iemr.FormResponseRequest; import com.iemr.flw.dto.iemr.QuestionAnswerRequest; @@ -37,7 +37,6 @@ import com.iemr.flw.dto.iemr.SectionResponseDTO; import com.iemr.flw.domain.iemr.DynamicForm; import com.iemr.flw.masterEnum.FormResponseStatus; -import com.iemr.flw.masterEnum.FormResponseStatus; import com.iemr.flw.masterEnum.FormType; import com.iemr.flw.repo.iemr.DynamicFormRepo; import com.iemr.flw.repo.iemr.FormResponseRepo; @@ -82,9 +81,6 @@ public class DynamicFormResponseServiceImpl implements DynamicFormResponseServic private static final String SECTION_STATUS_DONE = "DONE"; private static final String SECTION_STATUS_REFUSED = "REFUSED"; - private static final String SECTION_UUID_GENERAL_INFO = "TB2_SEC_GENERAL_INFO"; - private static final String QUESTION_UUID_CONSENT = "TB2_GI_Q1"; - private static final String OPTION_VALUE_CONSENT_REFUSED = "NO"; private final DynamicFormRepo dynamicFormRepo; private final FormResponseRepo formResponseRepo; @@ -97,6 +93,7 @@ public class DynamicFormResponseServiceImpl implements DynamicFormResponseServic private final FormResponseItemSaver itemSaver; private final JwtUtil jwtUtil; private final CampConfigService campConfigService; + private final ConsentRefusalEvaluator consentRefusalEvaluator; @Override @Transactional @@ -144,7 +141,7 @@ public FormResponseDTO completeForm(FormResponseRequest request, String jwtToken : existing.get(0); formResponse.setUpdatedBy(actor); - formResponse.setStatus(isConsentRefused(request) + formResponse.setStatus(consentRefusalEvaluator.isConsentRefused(request) ? FormResponseStatus.REFUSED.name() : FormResponseStatus.COMPLETE.name()); formResponse.setCompletedAt(now); @@ -261,7 +258,8 @@ private FormResponseDTO processSections( } String desiredStatus = applyRefusalRule - ? determineSectionStatus(section, sectionReq.getAnswers()) + ? consentRefusalEvaluator.determineSectionStatus( + section, sectionReq.getAnswers(), SECTION_STATUS_DONE, SECTION_STATUS_REFUSED) : SECTION_STATUS_DONE; Optional sectionResponseOpt = upsertSectionResponse( formResponse.getResponseId(), section, actor, desiredStatus, vanID, parkingPlaceID); @@ -325,38 +323,6 @@ private Optional upsertSectionResponse( return Optional.of(sectionResponseRepo.save(sr)); } - // TB2_SEC_GENERAL_INFO's consent question (TB2_GI_Q1) gates the rest of the TB Counselling - // form; a "NO" answer means the beneficiary refused counselling. Absence-tolerant by design: - // any other section, a missing answers list, or no answer for the consent question at all - // just falls through to the normal DONE status. - private String determineSectionStatus(FormSection section, List answers) { - if (!SECTION_UUID_GENERAL_INFO.equals(section.getSectionUuid()) || answers == null) { - return SECTION_STATUS_DONE; - } - boolean refused = answers.stream().anyMatch(this::isRefusalAnswer); - return refused ? SECTION_STATUS_REFUSED : SECTION_STATUS_DONE; - } - - // Same TB2_GI_Q1="NO" signal as determineSectionStatus, but scanned directly off the raw - // request (before any FormSection is resolved) so completeForm() can decide the top-level - // FormResponse status. Absence-tolerant: no TB2_SEC_GENERAL_INFO section, no answers, or no - // TB2_GI_Q1 answer at all just means "not refused". - private boolean isConsentRefused(FormResponseRequest request) { - if (request.getSections() == null) { - return false; - } - return request.getSections().stream() - .filter(s -> SECTION_UUID_GENERAL_INFO.equals(s.getSectionUuid())) - .filter(s -> s.getAnswers() != null) - .flatMap(s -> s.getAnswers().stream()) - .anyMatch(this::isRefusalAnswer); - } - - private boolean isRefusalAnswer(QuestionAnswerRequest answer) { - return QUESTION_UUID_CONSENT.equals(answer.getQuestionUuid()) - && OPTION_VALUE_CONSENT_REFUSED.equals(answer.getOptionValue()); - } - private List processAnswers( Long sectionResponseId, List answers, @@ -554,21 +520,38 @@ public List findPendingFollowUps(List formIds, int delayD @Override @Transactional(readOnly = true) - public CompletedBeneficiariesDTO getCompletedBeneficiaries( + public List getCompletedBeneficiaries( FormType formType, Integer villageId, Integer providerServiceMapId) { DynamicForm form = dynamicFormRepo.findByFormTypeAndIsActive(formType, true) .orElseThrow(() -> new NoSuchElementException( "No active form found for type: " + formType)); - return CompletedBeneficiariesDTO.builder() - .completed(findBeneficiaryIdsByStatus(form.getFormId(), FormResponseStatus.COMPLETE.name(), villageId, providerServiceMapId)) - .refused(findBeneficiaryIdsByStatus(form.getFormId(), FormResponseStatus.REFUSED.name(), villageId, providerServiceMapId)) - .build(); - } - private List findBeneficiaryIdsByStatus( - Long formId, String status, Integer villageId, Integer providerServiceMapId) { - return (villageId != null || providerServiceMapId != null) - ? formResponseRepo.findBeneficiaryIdsByFormIdAndStatusFiltered(formId, status, villageId, providerServiceMapId) - : formResponseRepo.findBeneficiaryIdsByFormIdAndStatus(formId, status); + List statuses = List.of(FormResponseStatus.COMPLETE.name(), FormResponseStatus.REFUSED.name(), FormResponseStatus.SUBMITTED.name()); + List responses = (villageId != null || providerServiceMapId != null) + ? formResponseRepo.findByFormIdAndStatusInFiltered(form.getFormId(), statuses, villageId, providerServiceMapId) + : formResponseRepo.findByFormIdAndStatusIn(form.getFormId(), statuses); + + if (responses.isEmpty()) { + return List.of(); + } + + List responseIds = responses.stream().map(FormResponse::getResponseId).collect(Collectors.toList()); + Map sectionsFilledByResponseId = sectionResponseRepo + .countByResponseIdInAndSectionPhase(responseIds, SectionPhase.PRE_SUBMIT).stream() + .collect(Collectors.toMap(row -> (Long) row[0], row -> ((Number) row[1]).intValue())); + + List versionIds = responses.stream().map(FormResponse::getVersionId).distinct().collect(Collectors.toList()); + Map totalSectionsByVersionId = formSectionRepo + .countByFormVersion_VersionIdInAndSectionPhase(versionIds, SectionPhase.PRE_SUBMIT).stream() + .collect(Collectors.toMap(row -> (Long) row[0], row -> ((Number) row[1]).intValue())); + + return responses.stream() + .map(r -> CompletedBeneficiaryDetailDTO.builder() + .beneficiaryId(r.getBeneficiaryId()) + .isRefused(FormResponseStatus.REFUSED.name().equals(r.getStatus())) + .sectionsFilled(sectionsFilledByResponseId.getOrDefault(r.getResponseId(), 0)) + .totalSections(totalSectionsByVersionId.getOrDefault(r.getVersionId(), 0)) + .build()) + .collect(Collectors.toList()); } } diff --git a/src/main/java/com/iemr/flw/service/impl/FormResponseItemSaver.java b/src/main/java/com/iemr/flw/service/impl/FormResponseItemSaver.java index 4ff8ca07..e0157f16 100644 --- a/src/main/java/com/iemr/flw/service/impl/FormResponseItemSaver.java +++ b/src/main/java/com/iemr/flw/service/impl/FormResponseItemSaver.java @@ -76,6 +76,7 @@ public class FormResponseItemSaver { private static final String SECTION_STATUS_DONE = "DONE"; + private static final String SECTION_STATUS_REFUSED = "REFUSED"; private final FormResponseRepo formResponseRepo; private final SectionResponseRepo sectionResponseRepo; @@ -86,6 +87,7 @@ public class FormResponseItemSaver { private final QuestionOptionRepo questionOptionRepo; private final JwtUtil jwtUtil; private final CampConfigService campConfigService; + private final ConsentRefusalEvaluator consentRefusalEvaluator; /** * Saves one form response in its own independent REQUIRES_NEW transaction. @@ -233,22 +235,14 @@ private FormResponseDTO processSections(FormResponse formResponse, FormResponseR "' not found in form '" + req.getFormUuid() + "'"); } - Optional existingSr = sectionResponseRepo.findByResponseIdAndSectionId( - formResponse.getResponseId(), section.getSectionId()); - if (existingSr.isPresent() && Boolean.FALSE.equals(section.getIsEditable())) { - log.info("saveForBulk: section '{}' is not editable — skipping update for responseId={}", - section.getSectionUuid(), formResponse.getResponseId()); + String desiredStatus = consentRefusalEvaluator.determineSectionStatus( + section, sectionReq.getAnswers(), SECTION_STATUS_DONE, SECTION_STATUS_REFUSED); + Optional sectionResponseOpt = upsertSectionResponse( + formResponse.getResponseId(), section, actor, desiredStatus, vanID, parkingPlaceID); + if (sectionResponseOpt.isEmpty()) { continue; } - - // Delete existing SectionResponse (and its QuestionResponses) for this section only - existingSr.ifPresent(sr -> { - questionResponseRepo.deleteBySectionResponseIdIn(Set.of(sr.getSectionResponseId())); - sectionResponseRepo.deleteById(sr.getSectionResponseId()); - }); - - SectionResponse sectionResponse = upsertSectionResponse( - formResponse.getResponseId(), section.getSectionId(), actor, vanID, parkingPlaceID); + SectionResponse sectionResponse = sectionResponseOpt.get(); if (section.getSectionPhase() == SectionPhase.POST_SUBMIT) { formResponse.setLastFollowUpAt(sectionResponse.getSavedAt()); @@ -274,29 +268,34 @@ private FormResponseDTO processSections(FormResponse formResponse, FormResponseR return buildFormResponseDTO(formResponse, sectionDTOs); } - private SectionResponse upsertSectionResponse(Long responseId, Long sectionId, String actor, - Integer vanID, Integer parkingPlaceID) { + private Optional upsertSectionResponse(Long responseId, FormSection section, String actor, + String status, Integer vanID, Integer parkingPlaceID) { Optional existing = - sectionResponseRepo.findByResponseIdAndSectionId(responseId, sectionId); + sectionResponseRepo.findByResponseIdAndSectionId(responseId, section.getSectionId()); if (existing.isPresent()) { + if (Boolean.FALSE.equals(section.getIsEditable())) { + log.info("saveForBulk: section '{}' is not editable — skipping update for responseId={}", + section.getSectionUuid(), responseId); + return Optional.empty(); + } SectionResponse sr = existing.get(); - sr.setStatus(SECTION_STATUS_DONE); + sr.setStatus(status); sr.setSavedAt(new Timestamp(System.currentTimeMillis())); sr.setUpdatedBy(actor); if (sr.getVanID() == null) { sr.setVanID(vanID); sr.setParkingPlaceID(parkingPlaceID); } - return sectionResponseRepo.save(sr); + return Optional.of(sectionResponseRepo.save(sr)); } SectionResponse sr = SectionResponse.builder() .responseId(responseId) - .sectionId(sectionId) - .status(SECTION_STATUS_DONE) + .sectionId(section.getSectionId()) + .status(status) .savedAt(new Timestamp(System.currentTimeMillis())) .createdBy(actor) .updatedBy(actor) .vanID(vanID) .parkingPlaceID(parkingPlaceID) .build(); - return sectionResponseRepo.save(sr); + return Optional.of(sectionResponseRepo.save(sr)); } private List processAnswers(