Skip to content
Merged
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<groupId>com.iemr.common.flw</groupId>
<artifactId>flw-api</artifactId>
<version>3.8.4</version>
<version>3.12.0</version>
<packaging>war</packaging>

<name>FLW-API</name>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* 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.domain.iemr;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;

import java.sql.Timestamp;

/**
* One row per applied {@code com.iemr.flw.seeder.migration.FormStructureMigration}, keyed by its
* stable migrationId β€” lets {@code DynamicFormMigrationRunner} skip a migration it has already run.
*/
@Entity
@Table(name = "t_dynamic_form_migration_log", schema = "db_iemr")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class DynamicFormMigrationLog {

@Id
@Column(name = "migrationId", length = 50)
private String migrationId;

@CreationTimestamp
@Column(name = "appliedAt", nullable = false, updatable = false)
private Timestamp appliedAt;
}
5 changes: 5 additions & 0 deletions src/main/java/com/iemr/flw/domain/iemr/FormSection.java
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,11 @@ public class FormSection {
@Column(name = "isEditable", nullable = false)
private Boolean isEditable = false;

/** Soft-delete flag: false means removed from its version. Never hard-deleted β€” historical
* section responses reference it. */
@Column(name = "isActive", nullable = false)
private Boolean isActive = true;

@Column(name = "created_by", length = 100)
private String createdBy;

Expand Down
5 changes: 5 additions & 0 deletions src/main/java/com/iemr/flw/domain/iemr/FormVersion.java
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ public class FormVersion {
@Column(name = "isLatest", nullable = false)
private Boolean isLatest = true;

/** Soft-delete flag: false means the version is retired. Never hard-deleted β€” historical
* form responses reference their version. */
@Column(name = "isActive", nullable = false)
private Boolean isActive = true;

@Column(name = "createdAt", nullable = false, updatable = false)
private Timestamp createdAt;

Expand Down
4 changes: 4 additions & 0 deletions src/main/java/com/iemr/flw/domain/iemr/OptionCondition.java
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ public class OptionCondition {
@ToString.Exclude
private FormSection targetSection;

/** Soft-delete flag: false means removed from its option. Never hard-deleted. */
@Column(name = "isActive", nullable = false)
private Boolean isActive = true;

@Column(name = "created_by", length = 100)
private String createdBy;

Expand Down
5 changes: 5 additions & 0 deletions src/main/java/com/iemr/flw/domain/iemr/QuestionOption.java
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,11 @@ public class QuestionOption {
@Column(name = "displayOrder", nullable = false)
private Integer displayOrder;

/** Soft-delete flag: false means removed from its question. Never hard-deleted β€” historical
* QuestionResponse rows reference optionId as a plain Long outside JPA's cascade graph. */
@Column(name = "isActive", nullable = false)
private Boolean isActive = true;

@Column(name = "created_by", length = 100)
private String createdBy;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ public class QuestionValidation {
@Column(name = "errorMessage", nullable = false, length = 500)
private String errorMessage;

/** Soft-delete flag: false means removed from its question. Never hard-deleted. */
@Column(name = "isActive", nullable = false)
private Boolean isActive = true;

@Column(name = "created_by", length = 100)
private String createdBy;

Expand Down
5 changes: 5 additions & 0 deletions src/main/java/com/iemr/flw/domain/iemr/SectionQuestion.java
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,11 @@ public class SectionQuestion {
@Column(name = "containsPii", nullable = false)
private Boolean containsPii = false;

/** Soft-delete flag: false means unlinked from its section. Never hard-deleted β€” historical
* QuestionResponse rows reference questionId as a plain Long outside JPA's cascade graph. */
@Column(name = "isActive", nullable = false)
private Boolean isActive = true;

@Column(name = "created_by", length = 100)
private String createdBy;

Expand Down
6 changes: 6 additions & 0 deletions src/main/java/com/iemr/flw/dto/iemr/DynamicFormDTO.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
*/
package com.iemr.flw.dto.iemr;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.iemr.flw.masterEnum.FormType;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
Expand Down Expand Up @@ -62,4 +63,9 @@ public class DynamicFormDTO {

@Valid
private List<FormSectionDTO> sections = new ArrayList<>();

/** Every version's full definition, oldest first β€” populated only by getAllForms (the top-level
* fields above stay the latest version); omitted from the JSON elsewhere and ignored on write. */
@JsonInclude(JsonInclude.Include.NON_NULL)
private List<DynamicFormDTO> versions;
}
1 change: 1 addition & 0 deletions src/main/java/com/iemr/flw/mapper/DynamicFormMapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public interface DynamicFormMapper {

@Mapping(target = "sections", ignore = true)
@Mapping(target = "versionNumber", ignore = true)
@Mapping(target = "versions", ignore = true)
DynamicFormDTO toDto(DynamicForm entity);

FormVersionDTO toDto(FormVersion entity);
Expand Down
5 changes: 4 additions & 1 deletion src/main/java/com/iemr/flw/masterEnum/ValidationType.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,8 @@ public enum ValidationType {
REGEX,
/** Field becomes mandatory when another field equals a specific value.
* validationParam format: "QUESTION_UUID=OPTION_VALUE" e.g. "Q-GENDER=FEMALE". */
MANDATORY_IF
MANDATORY_IF,
/** Display unit for a numeric answer. validationParam = unit label e.g. "sq. m.".
* Informational only β€” not a rule the answer is checked against. */
UNIT
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* 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.repo.iemr;

import com.iemr.flw.domain.iemr.DynamicFormMigrationLog;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

/**
* Repository for applied form-structure migration records.
* Id type is the migrationId String itself β€” existsById/save need no custom methods.
*/
@Repository
public interface DynamicFormMigrationLogRepo extends JpaRepository<DynamicFormMigrationLog, String> {
}
16 changes: 16 additions & 0 deletions src/main/java/com/iemr/flw/repo/iemr/FormSectionRepo.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,14 @@
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.Modifying;
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;
import java.util.Optional;

/**
* Repository for form sections.
Expand All @@ -40,6 +42,20 @@ public interface FormSectionRepo extends JpaRepository<FormSection, Long> {

List<FormSection> findByFormVersion_VersionIdOrderByDisplayOrderAsc(Long versionId);

/** Read-path variant excluding removed (isActive=false) sections. */
List<FormSection> findByFormVersion_VersionIdAndIsActiveTrueOrderByDisplayOrderAsc(Long versionId);

Optional<FormSection> findByFormVersion_VersionIdAndSectionUuid(Long versionId, String sectionUuid);

Optional<FormSection> findTopByFormVersion_VersionIdOrderByDisplayOrderDesc(Long versionId);

/** Shifts displayOrder by delta for every section in [from, to] within a version β€” used to make room for/close a gap around an inserted or moved sibling. */
@Modifying(flushAutomatically = true, clearAutomatically = true)
@Query("UPDATE FormSection s SET s.displayOrder = s.displayOrder + :delta " +
"WHERE s.formVersion.versionId = :versionId AND s.displayOrder BETWEEN :from AND :to")
void shiftDisplayOrder(@Param("versionId") Long versionId, @Param("from") int from,
@Param("to") int to, @Param("delta") int delta);

@Query("SELECT fs.formVersion.versionId, COUNT(fs) FROM FormSection fs " +
"WHERE fs.formVersion.versionId IN :versionIds " +
"AND fs.sectionPhase = :sectionPhase " +
Expand Down
12 changes: 11 additions & 1 deletion src/main/java/com/iemr/flw/repo/iemr/OptionConditionRepo.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@

import java.util.Collection;
import java.util.List;
import java.util.Optional;

/**
* Repository for option conditions.
Expand All @@ -38,6 +39,13 @@ public interface OptionConditionRepo extends JpaRepository<OptionCondition, Long

List<OptionCondition> findByQuestionOption_OptionId(Long optionId);

/** No surrogate natural key on conditions β€” matched by (actionType, target) within an option. */
Optional<OptionCondition> findByQuestionOption_OptionIdAndActionTypeAndTargetQuestion_QuestionUuid(
Long optionId, String actionType, String targetQuestionUuid);

Optional<OptionCondition> findByQuestionOption_OptionIdAndActionTypeAndTargetSection_SectionUuid(
Long optionId, String actionType, String targetSectionUuid);

@Query("SELECT oc.targetQuestion.questionId FROM OptionCondition oc "
+ "WHERE oc.questionOption.sectionQuestion.formSection.formVersion.versionId = :versionId "
+ "AND oc.targetQuestion IS NOT NULL")
Expand All @@ -47,9 +55,11 @@ public interface OptionConditionRepo extends JpaRepository<OptionCondition, Long
* Loads all conditions for a set of options in one query.
* JOIN FETCH ensures questionOption, targetQuestion, and targetSection are hydrated
* so callers can group and resolve references without extra queries.
* Excludes removed (isActive=false) conditions β€” used only by read paths; reconciliation matching
* uses the natural-key finders above, which must see inactive rows too.
*/
@Query("SELECT c FROM OptionCondition c JOIN FETCH c.questionOption "
+ "LEFT JOIN FETCH c.targetQuestion LEFT JOIN FETCH c.targetSection "
+ "WHERE c.questionOption.optionId IN :optionIds")
+ "WHERE c.questionOption.optionId IN :optionIds AND c.isActive = true")
List<OptionCondition> findByOptionIds(@Param("optionIds") Collection<Long> optionIds);
}
17 changes: 16 additions & 1 deletion src/main/java/com/iemr/flw/repo/iemr/QuestionOptionRepo.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,14 @@

import com.iemr.flw.domain.iemr.QuestionOption;
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 java.util.Collection;
import java.util.List;
import java.util.Optional;

/**
* Repository for question options.
Expand All @@ -38,11 +40,24 @@ public interface QuestionOptionRepo extends JpaRepository<QuestionOption, Long>

List<QuestionOption> findBySectionQuestion_QuestionIdOrderByDisplayOrderAsc(Long questionId);

Optional<QuestionOption> findBySectionQuestion_QuestionIdAndOptionValue(Long questionId, String optionValue);

Optional<QuestionOption> findTopBySectionQuestion_QuestionIdOrderByDisplayOrderDesc(Long questionId);

/** Shifts displayOrder by delta for every option in [from, to] within a question β€” used to make room for/close a gap around an inserted or moved sibling. */
@Modifying(flushAutomatically = true, clearAutomatically = true)
@Query("UPDATE QuestionOption o SET o.displayOrder = o.displayOrder + :delta " +
"WHERE o.sectionQuestion.questionId = :questionId AND o.displayOrder BETWEEN :from AND :to")
void shiftDisplayOrder(@Param("questionId") Long questionId, @Param("from") int from,
@Param("to") int to, @Param("delta") int delta);

/**
* Loads all options for a set of questions in one query.
* JOIN FETCH ensures sectionQuestion is hydrated so callers can group by questionId without extra queries.
* Excludes removed (isActive=false) options β€” used only by read paths; reconciliation matching
* uses the natural-key finder above, which must see inactive rows too.
*/
@Query("SELECT o FROM QuestionOption o JOIN FETCH o.sectionQuestion "
+ "WHERE o.sectionQuestion.questionId IN :questionIds ORDER BY o.displayOrder ASC")
+ "WHERE o.sectionQuestion.questionId IN :questionIds AND o.isActive = true ORDER BY o.displayOrder ASC")
List<QuestionOption> findByQuestionIdsOrderByDisplayOrderAsc(@Param("questionIds") Collection<Long> questionIds);
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,15 @@
package com.iemr.flw.repo.iemr;

import com.iemr.flw.domain.iemr.QuestionValidation;
import com.iemr.flw.masterEnum.ValidationType;
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;
import java.util.Optional;

/**
* Repository for question validation rules.
Expand All @@ -38,11 +40,17 @@ public interface QuestionValidationRepo extends JpaRepository<QuestionValidation

List<QuestionValidation> findBySectionQuestion_QuestionId(Long questionId);

/** No surrogate natural key on validations β€” matched by the (type, param) content tuple within a question. */
Optional<QuestionValidation> findBySectionQuestion_QuestionIdAndValidationTypeAndValidationParam(
Long questionId, ValidationType validationType, String validationParam);

/**
* Loads all validations for a set of questions in one query.
* JOIN FETCH ensures sectionQuestion is hydrated so callers can group by questionId without extra queries.
* Excludes removed (isActive=false) validations β€” used only by read paths; reconciliation matching
* uses the natural-key finder above, which must see inactive rows too.
*/
@Query("SELECT v FROM QuestionValidation v JOIN FETCH v.sectionQuestion "
+ "WHERE v.sectionQuestion.questionId IN :questionIds")
+ "WHERE v.sectionQuestion.questionId IN :questionIds AND v.isActive = true")
List<QuestionValidation> findByQuestionIds(@Param("questionIds") Collection<Long> questionIds);
}
20 changes: 19 additions & 1 deletion src/main/java/com/iemr/flw/repo/iemr/SectionQuestionRepo.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,14 @@

import com.iemr.flw.domain.iemr.SectionQuestion;
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 java.util.Collection;
import java.util.List;
import java.util.Optional;

/**
* Repository for section questions.
Expand All @@ -38,11 +40,27 @@ public interface SectionQuestionRepo extends JpaRepository<SectionQuestion, Long

List<SectionQuestion> findByFormSection_SectionIdOrderByDisplayOrderAsc(Long sectionId);

Optional<SectionQuestion> findByFormSection_SectionIdAndQuestionUuid(Long sectionId, String questionUuid);

/** Version-scoped lookup β€” a condition's target question may live in a different section of the same version. */
Optional<SectionQuestion> findByFormSection_FormVersion_VersionIdAndQuestionUuid(Long versionId, String questionUuid);

Optional<SectionQuestion> findTopByFormSection_SectionIdOrderByDisplayOrderDesc(Long sectionId);

/** Shifts displayOrder by delta for every question in [from, to] within a section β€” used to make room for/close a gap around an inserted or moved sibling. */
@Modifying(flushAutomatically = true, clearAutomatically = true)
@Query("UPDATE SectionQuestion q SET q.displayOrder = q.displayOrder + :delta " +
"WHERE q.formSection.sectionId = :sectionId AND q.displayOrder BETWEEN :from AND :to")
void shiftDisplayOrder(@Param("sectionId") Long sectionId, @Param("from") int from,
@Param("to") int to, @Param("delta") int delta);

/**
* Loads all questions for a set of sections in one query.
* JOIN FETCH ensures formSection is hydrated so callers can group by sectionId without extra queries.
* Excludes unlinked (isActive=false) questions β€” used only by read paths; reconciliation matching
* uses the natural-key finders above, which must see inactive rows too.
*/
@Query("SELECT q FROM SectionQuestion q JOIN FETCH q.formSection "
+ "WHERE q.formSection.sectionId IN :sectionIds ORDER BY q.displayOrder ASC")
+ "WHERE q.formSection.sectionId IN :sectionIds AND q.isActive = true ORDER BY q.displayOrder ASC")
List<SectionQuestion> findBySectionIdsOrderByDisplayOrderAsc(@Param("sectionIds") Collection<Long> sectionIds);
}
Loading
Loading