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/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..82308544 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 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 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/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 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 64f405b9..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; @@ -81,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; @@ -96,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 @@ -143,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); @@ -260,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); @@ -324,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, @@ -388,7 +355,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()); @@ -553,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 4767f505..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( @@ -328,7 +327,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());