From d128570761d4b8da3b275bef2a7d682bc1ea2cb5 Mon Sep 17 00:00:00 2001 From: Emmanuel Chinonye Nnajiofor Date: Tue, 30 Jun 2026 09:26:55 +0100 Subject: [PATCH 01/21] minor changes --- pom.xml | 2 +- .../lamisplus/modules/hts/domain/dto/HtsEncounterRequest.java | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/pom.xml b/pom.xml index 04dbc377..f9ba5cd9 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ org.lamisplus.modules hts - 2.5.0.2 + 2.5.0 jar Hts diff --git a/src/main/java/org/lamisplus/modules/hts/domain/dto/HtsEncounterRequest.java b/src/main/java/org/lamisplus/modules/hts/domain/dto/HtsEncounterRequest.java index c24f7802..9d280926 100644 --- a/src/main/java/org/lamisplus/modules/hts/domain/dto/HtsEncounterRequest.java +++ b/src/main/java/org/lamisplus/modules/hts/domain/dto/HtsEncounterRequest.java @@ -69,14 +69,12 @@ public class HtsEncounterRequest { private String complaintsScroralSwelling; private String complaintsGenitalSores; private String complaintsSwollenLymphNodes; - private String partnerNewlyDiagnosed; private String partnerPregnantOnArv; private String adolescentHivPositive; private String partnerNotRegularlyOnDrugs; private String partnerRecentlyReturnedToTreatment; private String hadSexWithHivPositivePartnerInRiskGroup; - private String typeOfHivTestDone; private String initialHivTest; private String suspectedAcuteInfection; @@ -85,7 +83,6 @@ public class HtsEncounterRequest { private String syphilisTestResult; private String recencyTest; private String hivEarlyDetectResult; - private String previouslyTestedThisYear; private String clientReceivedTestResult; private String hivTestKitsProvided; @@ -100,7 +97,6 @@ public class HtsEncounterRequest { private String completedBy; private String designation; private Boolean pmtctHts = false; - private String source ="Web"; private String longitude; private String latitude; From fa0072d69bc0e6324eb743b44f80a2697df54297 Mon Sep 17 00:00:00 2001 From: Emmanuel Chinonye Nnajiofor Date: Tue, 30 Jun 2026 11:06:16 +0100 Subject: [PATCH 02/21] resolved feedbacks around relaxing 6 mandatory fields in knowledge assessment section --- src/main/webapp/api.js | 2 +- .../NewToolForms/hooks/validationSchema.js | 107 ++++++++++-------- .../sections/PreTestCounsellingSection.jsx | 2 +- 3 files changed, 61 insertions(+), 50 deletions(-) diff --git a/src/main/webapp/api.js b/src/main/webapp/api.js index 0c47ea4d..c812c81b 100644 --- a/src/main/webapp/api.js +++ b/src/main/webapp/api.js @@ -4,7 +4,7 @@ export const url = : "/api/v1/"; export const token = process.env.NODE_ENV === "development" - ? "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJyZGUtdXNlciIsImF1dGgiOiJTdXBlciBBZG1pbixVc2VyLFJERSIsImV4cCI6MTc4MDUwMjcxNSwibmFtZSI6InJkZS11c2VyIGxhc3RuYW1lIn0.JmZiXX_viGAUfgIt4iSgHucV558YNJ7LQ63yAh2RA6uETvIUpPwCHaGCBneqL-9BBRAm8sVA7C9PEnJb1-h0LA" + ? "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJyZGUtdXNlciIsImF1dGgiOiJTdXBlciBBZG1pbixVc2VyLFJERSIsImV4cCI6MTc4MjgzNzI1MCwibmFtZSI6InJkZS11c2VyIGxhc3RuYW1lIn0.fvnONrUWlZhQDZslj5rrwFz-Mtx7lU2BFJZ3iBZ7njWZYVvOuRJMGodFXsocB-z3II0xZezBAI-qm42ZQ-9_UQ" : new URLSearchParams(window.location.search).get("jwt"); export const wsUrl = diff --git a/src/main/webapp/jsx/components/NewToolForms/hooks/validationSchema.js b/src/main/webapp/jsx/components/NewToolForms/hooks/validationSchema.js index 9e4846d9..79dd7e5d 100644 --- a/src/main/webapp/jsx/components/NewToolForms/hooks/validationSchema.js +++ b/src/main/webapp/jsx/components/NewToolForms/hooks/validationSchema.js @@ -232,14 +232,17 @@ export const buildValidationSchema = (isNewPatient) => { return !!value || this.createError({ message: "Index client code/ID is required" }); } ), - previouslyTestedNegative: yup.mixed().test( - "prev-tested-conditional", - "This field is required", - function (value) { - if (skipKnowledgeAndRisk(this)) return true; - return !!value || this.createError({ message: "This field is required" }); - } - ), + // previouslyTestedNegative: yup.mixed().test( + // "prev-tested-conditional", + // "This field is required", + // function (value) { + // if (skipKnowledgeAndRisk(this)) return true; + // return !!value || this.createError({ message: "This field is required" }); + // } + // ), + + previouslyTestedNegative: yup.string(), + timeOfLastNegativeTest: yup.mixed().test( "time-last-negative-conditional", "Time of last negative test is required", @@ -249,46 +252,54 @@ export const buildValidationSchema = (isNewPatient) => { return !!value || this.createError({ message: "Time of last negative test is required" }); } ), - clientInformedTransmissionRoutes: yup.mixed().test( - "transmission-routes-conditional", - "This field is required", - function (value) { - if (skipKnowledgeAndRisk(this)) return true; - return !!value || this.createError({ message: "This field is required" }); - } - ), - clientInformedRiskFactors: yup.mixed().test( - "risk-factors-conditional", - "This field is required", - function (value) { - if (skipKnowledgeAndRisk(this)) return true; - return !!value || this.createError({ message: "This field is required" }); - } - ), - clientInformedPreventionMethods: yup.mixed().test( - "prevention-methods-conditional", - "This field is required", - function (value) { - if (skipKnowledgeAndRisk(this)) return true; - return !!value || this.createError({ message: "This field is required" }); - } - ), - clientInformedPossibleResults: yup.mixed().test( - "possible-results-conditional", - "This field is required", - function (value) { - if (skipKnowledgeAndRisk(this)) return true; - return !!value || this.createError({ message: "This field is required" }); - } - ), - informedConsentGiven: yup.mixed().test( - "consent-conditional", - "This field is required", - function (value) { - if (skipKnowledgeAndRisk(this)) return true; - return !!value || this.createError({ message: "This field is required" }); - } - ), + // clientInformedTransmissionRoutes: yup.mixed().test( + // "transmission-routes-conditional", + // "This field is required", + // function (value) { + // if (skipKnowledgeAndRisk(this)) return true; + // return !!value || this.createError({ message: "This field is required" }); + // } + // ), + clientInformedTransmissionRoutes: yup.string(), + clientInformedRiskFactors: yup.string(), + clientInformedPreventionMethods: yup.string(), + clientInformedPossibleResults: yup.string(), + informedConsentGiven: yup.string(), + // clientInformedRiskFactors: yup.mixed().test( + // "risk-factors-conditional", + // "This field is required", + // function (value) { + // if (skipKnowledgeAndRisk(this)) return true; + // return !!value || this.createError({ message: "This field is required" }); + // } + // ), + + // clientInformedPreventionMethods: yup.mixed().test( + // "prevention-methods-conditional", + // "This field is required", + // function (value) { + // if (skipKnowledgeAndRisk(this)) return true; + // return !!value || this.createError({ message: "This field is required" }); + // } + // ), + + // clientInformedPossibleResults: yup.mixed().test( + // "possible-results-conditional", + // "This field is required", + // function (value) { + // if (skipKnowledgeAndRisk(this)) return true; + // return !!value || this.createError({ message: "This field is required" }); + // } + // ), + + // informedConsentGiven: yup.mixed().test( + // "consent-conditional", + // "This field is required", + // function (value) { + // if (skipKnowledgeAndRisk(this)) return true; + // return !!value || this.createError({ message: "This field is required" }); + // } + // ), everHadSexualIntercourse: yup.mixed().test( "ever-sex-conditional", "This field is required", diff --git a/src/main/webapp/jsx/components/NewToolForms/sections/PreTestCounsellingSection.jsx b/src/main/webapp/jsx/components/NewToolForms/sections/PreTestCounsellingSection.jsx index f20bfe2b..dc16ac9a 100644 --- a/src/main/webapp/jsx/components/NewToolForms/sections/PreTestCounsellingSection.jsx +++ b/src/main/webapp/jsx/components/NewToolForms/sections/PreTestCounsellingSection.jsx @@ -181,7 +181,7 @@ const PreTestCounsellingSection = ({ formik, readOnly }) => {
From 2f8eca3134a295948b6b9c066460525804add734 Mon Sep 17 00:00:00 2001 From: Emmanuel Chinonye Nnajiofor Date: Tue, 30 Jun 2026 11:20:20 +0100 Subject: [PATCH 03/21] minor changes --- .../sections/PreTestCounsellingSection.jsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/webapp/jsx/components/NewToolForms/sections/PreTestCounsellingSection.jsx b/src/main/webapp/jsx/components/NewToolForms/sections/PreTestCounsellingSection.jsx index dc16ac9a..4407ac47 100644 --- a/src/main/webapp/jsx/components/NewToolForms/sections/PreTestCounsellingSection.jsx +++ b/src/main/webapp/jsx/components/NewToolForms/sections/PreTestCounsellingSection.jsx @@ -200,35 +200,35 @@ const PreTestCounsellingSection = ({ formik, readOnly }) => {
From fd7a8799495994aa26839dc3646b531544eb572d Mon Sep 17 00:00:00 2001 From: Emmanuel Chinonye Nnajiofor Date: Wed, 1 Jul 2026 12:50:39 +0100 Subject: [PATCH 04/21] starting diagnostic testing --- src/main/webapp/api.js | 2 +- .../NewToolForms/hooks/validationSchema.js | 409 ++++++++++-------- .../sections/PostTestCounsellingSection.jsx | 4 +- .../sections/PreTestCounsellingSection.jsx | 38 +- 4 files changed, 252 insertions(+), 201 deletions(-) diff --git a/src/main/webapp/api.js b/src/main/webapp/api.js index c812c81b..5170a6b1 100644 --- a/src/main/webapp/api.js +++ b/src/main/webapp/api.js @@ -4,7 +4,7 @@ export const url = : "/api/v1/"; export const token = process.env.NODE_ENV === "development" - ? "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJyZGUtdXNlciIsImF1dGgiOiJTdXBlciBBZG1pbixVc2VyLFJERSIsImV4cCI6MTc4MjgzNzI1MCwibmFtZSI6InJkZS11c2VyIGxhc3RuYW1lIn0.fvnONrUWlZhQDZslj5rrwFz-Mtx7lU2BFJZ3iBZ7njWZYVvOuRJMGodFXsocB-z3II0xZezBAI-qm42ZQ-9_UQ" + ? "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJyZGUtdXNlciIsImF1dGgiOiJTdXBlciBBZG1pbixVc2VyLFJERSIsImV4cCI6MTc4MjkzNTEzOSwibmFtZSI6InJkZS11c2VyIGxhc3RuYW1lIn0.GATZtMNDJPz2yxjBs74aXt7lqfaqDdR4RFw5KexNTnbGGSf9RH6P4UZ8ahhSm_gGUSmNlIONKNYbcVGL8yCR0g" : new URLSearchParams(window.location.search).get("jwt"); export const wsUrl = diff --git a/src/main/webapp/jsx/components/NewToolForms/hooks/validationSchema.js b/src/main/webapp/jsx/components/NewToolForms/hooks/validationSchema.js index 79dd7e5d..7c8477fe 100644 --- a/src/main/webapp/jsx/components/NewToolForms/hooks/validationSchema.js +++ b/src/main/webapp/jsx/components/NewToolForms/hooks/validationSchema.js @@ -300,68 +300,86 @@ export const buildValidationSchema = (isNewPatient) => { // return !!value || this.createError({ message: "This field is required" }); // } // ), - everHadSexualIntercourse: yup.mixed().test( - "ever-sex-conditional", - "This field is required", - function (value) { - if (skipKnowledgeAndRisk(this)) return true; - return !!value || this.createError({ message: "This field is required" }); - } - ), - moreThanOneSexPartner: yup.mixed().test( - "multi-partner-conditional", - "This field is required", - function (value) { - if (skipKnowledgeAndRisk(this)) return true; - if (this.parent.everHadSexualIntercourse !== "YES_NO_YES") return true; - return !!value || this.createError({ message: "This field is required" }); - } - ), - unprotectedVaginalSex: yup.mixed().test( - "vaginal-sex-conditional", - "This field is required", - function (value) { - if (skipKnowledgeAndRisk(this)) return true; - if (this.parent.sex !== "SEX_FEMALE") return true; - if (this.parent.everHadSexualIntercourse !== "YES_NO_YES") return true; - return !!value || this.createError({ message: "This field is required" }); - } - ), - unprotectedAnalSex: yup.mixed().test( - "anal-sex-conditional", - "This field is required", - function (value) { - if (skipKnowledgeAndRisk(this)) return true; - if (this.parent.everHadSexualIntercourse !== "YES_NO_YES") return true; - return !!value || this.createError({ message: "This field is required" }); - } - ), - bloodTransfusionLast3Months: yup.mixed().test( - "blood-transfusion-conditional", - "This field is required", - function (value) { - if (skipKnowledgeAndRisk(this)) return true; - return !!value || this.createError({ message: "This field is required" }); - } - ), - sexUnderInfluence: yup.mixed().test( - "sex-influence-conditional", - "This field is required", - function (value) { - if (skipKnowledgeAndRisk(this)) return true; - if (this.parent.everHadSexualIntercourse !== "YES_NO_YES") return true; - return !!value || this.createError({ message: "This field is required" }); - } - ), - historyOfSTI: yup.mixed().test( - "sti-history-conditional", - "This field is required", - function (value) { - if (skipKnowledgeAndRisk(this)) return true; - if (this.parent.everHadSexualIntercourse !== "YES_NO_YES") return true; - return !!value || this.createError({ message: "This field is required" }); - } - ), + + everHadSexualIntercourse: yup.string(), + + // everHadSexualIntercourse: yup.mixed().test( + // "ever-sex-conditional", + // "This field is required", + // function (value) { + // if (skipKnowledgeAndRisk(this)) return true; + // return !!value || this.createError({ message: "This field is required" }); + // } + // ), + + moreThanOneSexPartner: yup.string(), + + // moreThanOneSexPartner: yup.mixed().test( + // "multi-partner-conditional", + // "This field is required", + // function (value) { + // if (skipKnowledgeAndRisk(this)) return true; + // if (this.parent.everHadSexualIntercourse !== "YES_NO_YES") return true; + // return !!value || this.createError({ message: "This field is required" }); + // } + // ), + unprotectedVaginalSex: yup.string(), + + // unprotectedVaginalSex: yup.mixed().test( + // "vaginal-sex-conditional", + // "This field is required", + // function (value) { + // if (skipKnowledgeAndRisk(this)) return true; + // if (this.parent.sex !== "SEX_FEMALE") return true; + // if (this.parent.everHadSexualIntercourse !== "YES_NO_YES") return true; + // return !!value || this.createError({ message: "This field is required" }); + // } + // ), + + unprotectedAnalSex:yup.string(), + + // unprotectedAnalSex: yup.mixed().test( + // "anal-sex-conditional", + // "This field is required", + // function (value) { + // if (skipKnowledgeAndRisk(this)) return true; + // if (this.parent.everHadSexualIntercourse !== "YES_NO_YES") return true; + // return !!value || this.createError({ message: "This field is required" }); + // } + // ), + + // bloodTransfusionLast3Months: yup.mixed().test( + // "blood-transfusion-conditional", + // "This field is required", + // function (value) { + // if (skipKnowledgeAndRisk(this)) return true; + // return !!value || this.createError({ message: "This field is required" }); + // } + // ), + + bloodTransfusionLast3Months: yup.string(), + sexUnderInfluence:yup.string(), + + // sexUnderInfluence: yup.mixed().test( + // "sex-influence-conditional", + // "This field is required", + // function (value) { + // if (skipKnowledgeAndRisk(this)) return true; + // if (this.parent.everHadSexualIntercourse !== "YES_NO_YES") return true; + // return !!value || this.createError({ message: "This field is required" }); + // } + // ), + historyOfSTI: yup.string(), + + // historyOfSTI: yup.mixed().test( + // "sti-history-conditional", + // "This field is required", + // function (value) { + // if (skipKnowledgeAndRisk(this)) return true; + // if (this.parent.everHadSexualIntercourse !== "YES_NO_YES") return true; + // return !!value || this.createError({ message: "This field is required" }); + // } + // ), currentCough: yup.mixed().test( "curent-cough-conditional", "This field is required", @@ -394,116 +412,142 @@ export const buildValidationSchema = (isNewPatient) => { return !!value || this.createError({ message: "This field is required" }); } ), - complaintsVaginalDischarge: yup.mixed().test( - "vaginal-discharge-conditional", - "This field is required", - function (value) { - if (skipKnowledgeAndRisk(this)) return true; - if (this.parent.sex !== "SEX_FEMALE") return true; - return !!value || this.createError({ message: "This field is required" }); - } - ), - complaintsLowerAbdominalPain: yup.mixed().test( - "lower-abdominal-conditional", - "This field is required", - function (value) { - if (skipKnowledgeAndRisk(this)) return true; - if (this.parent.sex !== "SEX_FEMALE") return true; - return !!value || this.createError({ message: "This field is required" }); - } - ), - complaintsUrethralDischarge: yup.mixed().test( - "urethral-discharge-conditional", - "This field is required", - function (value) { - if (skipKnowledgeAndRisk(this)) return true; - if (this.parent.sex !== "SEX_MALE") return true; - return !!value || this.createError({ message: "This field is required" }); - } - ), - complaintsScroralSwelling: yup.mixed().test( - "scrotal-swelling-conditional", - "This field is required", - function (value) { - if (skipKnowledgeAndRisk(this)) return true; - if (this.parent.sex !== "SEX_MALE") return true; - return !!value || this.createError({ message: "This field is required" }); - } - ), - complaintsGenitalSores: yup.mixed().test( - "complaint-genital-conditional", - "This field is required", - function (value) { - if (skipKnowledgeAndRisk(this)) return true; - return !!value || this.createError({ message: "This field is required" }); - } - ), - complaintsSwollenLymphNodes: yup.mixed().test( - "complaint-swollen-conditional", - "This field is required", - function (value) { - if (skipKnowledgeAndRisk(this)) return true; - return !!value || this.createError({ message: "This field is required" }); - } - ), - partnerNewlyDiagnosed: yup.mixed().test( - "partner-new-dx-conditional", - "This field is required", - function (value) { - if (skipKnowledgeAndRisk(this)) return true; - if (this.parent.everHadSexualIntercourse !== "YES_NO_YES") return true; - return !!value || this.createError({ message: "This field is required" }); - } - ), - partnerPregnantOnArv: yup.mixed().test( - "partner-pmtct-conditional", - "This field is required", - function (value) { - if (skipKnowledgeAndRisk(this)) return true; - if (this.parent.everHadSexualIntercourse !== "YES_NO_YES") return true; - return !!value || this.createError({ message: "This field is required" }); - } - ), - adolescentHivPositive: yup.mixed().test( - "adolescent-hiv-conditional", - "This field is required", - function (value) { - if (skipKnowledgeAndRisk(this)) return true; - if (this.parent.everHadSexualIntercourse !== "YES_NO_YES") return true; - // Use resolveAge so the check works whether age comes from the - // age field or is derived from dateOfBirth (avoids invisible required error) - const age = resolveAge(this.parent); - if (age === null || age < 10 || age > 19) return true; - return !!value || this.createError({ message: "This field is required" }); - } - ), - partnerNotRegularlyOnDrugs: yup.mixed().test( - "partner-not-drugs-conditional", - "This field is required", - function (value) { - if (skipKnowledgeAndRisk(this)) return true; - if (this.parent.everHadSexualIntercourse !== "YES_NO_YES") return true; - return !!value || this.createError({ message: "This field is required" }); - } - ), - partnerRecentlyReturnedToTreatment: yup.mixed().test( - "partner-ltfu-conditional", - "This field is required", - function (value) { - if (skipKnowledgeAndRisk(this)) return true; - if (this.parent.everHadSexualIntercourse !== "YES_NO_YES") return true; - return !!value || this.createError({ message: "This field is required" }); - } - ), - hadSexWithHivPositivePartnerInRiskGroup: yup.mixed().test( - "had-sex-with-hiv-conditional", - "This field is required", - function (value) { - if (skipKnowledgeAndRisk(this)) return true; - if (this.parent.everHadSexualIntercourse !== "YES_NO_YES") return true; - return !!value || this.createError({ message: "This field is required" }); - } - ), + complaintsVaginalDischarge: yup.string(), + + // complaintsVaginalDischarge: yup.mixed().test( + // "vaginal-discharge-conditional", + // "This field is required", + // function (value) { + // if (skipKnowledgeAndRisk(this)) return true; + // if (this.parent.sex !== "SEX_FEMALE") return true; + // return !!value || this.createError({ message: "This field is required" }); + // } + // ), + complaintsLowerAbdominalPain: yup.string(), + + // complaintsLowerAbdominalPain: yup.mixed().test( + // "lower-abdominal-conditional", + // "This field is required", + // function (value) { + // if (skipKnowledgeAndRisk(this)) return true; + // if (this.parent.sex !== "SEX_FEMALE") return true; + // return !!value || this.createError({ message: "This field is required" }); + // } + // ), + + // complaintsUrethralDischarge: yup.mixed().test( + // "urethral-discharge-conditional", + // "This field is required", + // function (value) { + // if (skipKnowledgeAndRisk(this)) return true; + // if (this.parent.sex !== "SEX_MALE") return true; + // return !!value || this.createError({ message: "This field is required" }); + // } + // ), + + complaintsUrethralDischarge: yup.string(), + complaintsScroralSwelling: yup.string(), + complaintsGenitalSores: yup.string(), + complaintsSwollenLymphNodes: yup.string(), + // complaintsScroralSwelling: yup.mixed().test( + // "scrotal-swelling-conditional", + // "This field is required", + // function (value) { + // if (skipKnowledgeAndRisk(this)) return true; + // if (this.parent.sex !== "SEX_MALE") return true; + // return !!value || this.createError({ message: "This field is required" }); + // } + // ), + // complaintsGenitalSores: yup.mixed().test( + // "complaint-genital-conditional", + // "This field is required", + // function (value) { + // if (skipKnowledgeAndRisk(this)) return true; + // return !!value || this.createError({ message: "This field is required" }); + // } + // ), + // complaintsSwollenLymphNodes: yup.mixed().test( + // "complaint-swollen-conditional", + // "This field is required", + // function (value) { + // if (skipKnowledgeAndRisk(this)) return true; + // return !!value || this.createError({ message: "This field is required" }); + // } + // ), + partnerNewlyDiagnosed: yup.string(), + + // partnerNewlyDiagnosed: yup.mixed().test( + // "partner-new-dx-conditional", + // "This field is required", + // function (value) { + // if (skipKnowledgeAndRisk(this)) return true; + // if (this.parent.everHadSexualIntercourse !== "YES_NO_YES") return true; + // return !!value || this.createError({ message: "This field is required" }); + // } + // ), + + partnerPregnantOnArv: yup.string(), + + // partnerPregnantOnArv: yup.mixed().test( + // "partner-pmtct-conditional", + // "This field is required", + // function (value) { + // if (skipKnowledgeAndRisk(this)) return true; + // if (this.parent.everHadSexualIntercourse !== "YES_NO_YES") return true; + // return !!value || this.createError({ message: "This field is required" }); + // } + // ), + + adolescentHivPositive: yup.string(), + + // adolescentHivPositive: yup.mixed().test( + // "adolescent-hiv-conditional", + // "This field is required", + // function (value) { + // if (skipKnowledgeAndRisk(this)) return true; + // if (this.parent.everHadSexualIntercourse !== "YES_NO_YES") return true; + // // Use resolveAge so the check works whether age comes from the + // // age field or is derived from dateOfBirth (avoids invisible required error) + // const age = resolveAge(this.parent); + // if (age === null || age < 10 || age > 19) return true; + // return !!value || this.createError({ message: "This field is required" }); + // } + // ), + + // partnerNotRegularlyOnDrugs: yup.mixed().test( + // "partner-not-drugs-conditional", + // "This field is required", + // function (value) { + // if (skipKnowledgeAndRisk(this)) return true; + // if (this.parent.everHadSexualIntercourse !== "YES_NO_YES") return true; + // return !!value || this.createError({ message: "This field is required" }); + // } + // ), + partnerNotRegularlyOnDrugs: yup.string(), + partnerRecentlyReturnedToTreatment: yup.string(), + + // partnerRecentlyReturnedToTreatment: yup.mixed().test( + // "partner-ltfu-conditional", + // "This field is required", + // function (value) { + // if (skipKnowledgeAndRisk(this)) return true; + // if (this.parent.everHadSexualIntercourse !== "YES_NO_YES") return true; + // return !!value || this.createError({ message: "This field is required" }); + // } + // ), + + hadSexWithHivPositivePartnerInRiskGroup: yup.string(), + + // hadSexWithHivPositivePartnerInRiskGroup: yup.mixed().test( + // "had-sex-with-hiv-conditional", + // "This field is required", + // function (value) { + // if (skipKnowledgeAndRisk(this)) return true; + // if (this.parent.everHadSexualIntercourse !== "YES_NO_YES") return true; + // return !!value || this.createError({ message: "This field is required" }); + // } + // ), + typeOfHivTestDone: yup.string().required("This field is required"), hivEarlyDetectResult: yup.mixed().test( "earlyDetectResult-conditional", @@ -551,8 +595,10 @@ export const buildValidationSchema = (isNewPatient) => { recencyTest: yup.string(), - previouslyTestedThisYear: yup.string().required("This field is required"), - clientReceivedTestResult: yup.string().required("This field is required"), + // previouslyTestedThisYear: yup.string().required("This field is required"), + previouslyTestedThisYear: yup.string(), + clientReceivedTestResult: yup.string(), + // clientReceivedTestResult: yup.string().required("This field is required"), hivTestKitsProvided: yup.string().required("This field is required"), categoryOfClients: yup.mixed().test( @@ -599,11 +645,16 @@ export const buildValidationSchema = (isNewPatient) => { return !!value || this.createError({ message: "Accepted Index testing is required when confirmatory HIV test is positive" }); } ), - providedFpInfo: yup.string().required("This field is required"), - clientPartnerUseFpMethods: yup.string().required("This field is required"), - clientPartnerUseCondoms: yup.string().required("This field is required"), - correctCondomUseDemonstrated: yup.string().required("This field is required"), - condomsProvided: yup.string().required("This field is required"), + providedFpInfo: yup.string(), + // providedFpInfo: yup.string().required("This field is required"), + clientPartnerUseFpMethods: yup.string(), + // clientPartnerUseFpMethods: yup.string().required("This field is required"), + clientPartnerUseCondoms: yup.string(), + // clientPartnerUseCondoms: yup.string().required("This field is required"), + correctCondomUseDemonstrated: yup.string(), + // correctCondomUseDemonstrated: yup.string().required("This field is required"), + condomsProvided: yup.string(), + // condomsProvided: yup.string().required("This field is required"), clientReferredToOtherServices: yup.string().required("This field is required"), completedBy: yup.string().required("This field is required"), designation: yup.string().required("This field is required"), diff --git a/src/main/webapp/jsx/components/NewToolForms/sections/PostTestCounsellingSection.jsx b/src/main/webapp/jsx/components/NewToolForms/sections/PostTestCounsellingSection.jsx index c219f6c5..46e15c13 100644 --- a/src/main/webapp/jsx/components/NewToolForms/sections/PostTestCounsellingSection.jsx +++ b/src/main/webapp/jsx/components/NewToolForms/sections/PostTestCounsellingSection.jsx @@ -70,14 +70,14 @@ const PostTestCounsellingSection = ({ formik, readOnly }) => {
diff --git a/src/main/webapp/jsx/components/NewToolForms/sections/PreTestCounsellingSection.jsx b/src/main/webapp/jsx/components/NewToolForms/sections/PreTestCounsellingSection.jsx index 4407ac47..17b74dd3 100644 --- a/src/main/webapp/jsx/components/NewToolForms/sections/PreTestCounsellingSection.jsx +++ b/src/main/webapp/jsx/components/NewToolForms/sections/PreTestCounsellingSection.jsx @@ -250,7 +250,7 @@ const PreTestCounsellingSection = ({ formik, readOnly }) => { label="Ever Had Sexual Intercourse" {...sp("everHadSexualIntercourse", transformOptions(codesets?.["YES_NO"]))} onChange={readOnly ? undefined : handleEverHadSexChange} - required + // required /> {showSexDependent && ( @@ -259,7 +259,7 @@ const PreTestCounsellingSection = ({ formik, readOnly }) => { {isFemale && ( @@ -267,7 +267,7 @@ const PreTestCounsellingSection = ({ formik, readOnly }) => { )} @@ -275,7 +275,7 @@ const PreTestCounsellingSection = ({ formik, readOnly }) => { @@ -284,7 +284,7 @@ const PreTestCounsellingSection = ({ formik, readOnly }) => { {showSexDependent && ( @@ -293,14 +293,14 @@ const PreTestCounsellingSection = ({ formik, readOnly }) => {
@@ -343,14 +343,14 @@ const PreTestCounsellingSection = ({ formik, readOnly }) => {
@@ -361,14 +361,14 @@ const PreTestCounsellingSection = ({ formik, readOnly }) => {
@@ -377,14 +377,14 @@ const PreTestCounsellingSection = ({ formik, readOnly }) => {
@@ -405,14 +405,14 @@ const PreTestCounsellingSection = ({ formik, readOnly }) => {
{Number(values?.age) >= 10 && Number(values?.age) <= 19 && ( @@ -420,7 +420,7 @@ const PreTestCounsellingSection = ({ formik, readOnly }) => { )} @@ -428,21 +428,21 @@ const PreTestCounsellingSection = ({ formik, readOnly }) => {
From db06a20e862e6f9d0dd382d63e56b9711d7dcbce Mon Sep 17 00:00:00 2001 From: Emmanuel Chinonye Nnajiofor Date: Wed, 1 Jul 2026 13:04:02 +0100 Subject: [PATCH 05/21] done with jira issues --- .../NewToolForms/hooks/validationSchema.js | 18 +++++++++++++++++- .../sections/DiagnosticTestingSection.jsx | 2 ++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/main/webapp/jsx/components/NewToolForms/hooks/validationSchema.js b/src/main/webapp/jsx/components/NewToolForms/hooks/validationSchema.js index 7c8477fe..5886823c 100644 --- a/src/main/webapp/jsx/components/NewToolForms/hooks/validationSchema.js +++ b/src/main/webapp/jsx/components/NewToolForms/hooks/validationSchema.js @@ -594,7 +594,23 @@ export const buildValidationSchema = (isNewPatient) => { ), - recencyTest: yup.string(), + // recencyTest: yup.string(), + + syphilisTestResult: yup.string().required("Syphilis Test Result is required"), + + recencyTest: yup.mixed().test( + "recencyTest-conditional", + "Recency test result is required", + function (value) { + // Mirrors DiagnosticTestingSection's `showRecency` flag: + // the field is only rendered — and therefore only required — + // when the initial HIV test result is Positive. + if (this.parent.initialHivTest !== "STI_HIV_RESULT_POSITIVE") return true; + return !!value || this.createError({ message: "Recency test result is required" }); + } + ), + + // previouslyTestedThisYear: yup.string().required("This field is required"), previouslyTestedThisYear: yup.string(), clientReceivedTestResult: yup.string(), diff --git a/src/main/webapp/jsx/components/NewToolForms/sections/DiagnosticTestingSection.jsx b/src/main/webapp/jsx/components/NewToolForms/sections/DiagnosticTestingSection.jsx index a115d396..4581b55c 100644 --- a/src/main/webapp/jsx/components/NewToolForms/sections/DiagnosticTestingSection.jsx +++ b/src/main/webapp/jsx/components/NewToolForms/sections/DiagnosticTestingSection.jsx @@ -240,6 +240,7 @@ const DiagnosticTestingSection = ({ formik, readOnly }) => { )} @@ -270,6 +271,7 @@ const DiagnosticTestingSection = ({ formik, readOnly }) => { From 4de0e3c458f87ab154986070a2a74c991a7511b8 Mon Sep 17 00:00:00 2001 From: Emmanuel Chinonye Nnajiofor Date: Mon, 6 Jul 2026 11:24:17 +0100 Subject: [PATCH 06/21] add a fi for completed by and desgniantion --- src/main/webapp/api.js | 2 +- .../components/History/HTSEncounterHistory.js | 15 ++++ .../sections/BasicInformationSection.jsx | 2 +- .../sections/PostTestCounsellingSection.jsx | 87 +++++++++++++++---- .../sections/PreTestCounsellingSection.jsx | 1 + .../jsx/services/getAllUsers.service.js | 13 +++ 6 files changed, 99 insertions(+), 21 deletions(-) create mode 100644 src/main/webapp/jsx/services/getAllUsers.service.js diff --git a/src/main/webapp/api.js b/src/main/webapp/api.js index 5170a6b1..b0bca0bf 100644 --- a/src/main/webapp/api.js +++ b/src/main/webapp/api.js @@ -4,7 +4,7 @@ export const url = : "/api/v1/"; export const token = process.env.NODE_ENV === "development" - ? "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJyZGUtdXNlciIsImF1dGgiOiJTdXBlciBBZG1pbixVc2VyLFJERSIsImV4cCI6MTc4MjkzNTEzOSwibmFtZSI6InJkZS11c2VyIGxhc3RuYW1lIn0.GATZtMNDJPz2yxjBs74aXt7lqfaqDdR4RFw5KexNTnbGGSf9RH6P4UZ8ahhSm_gGUSmNlIONKNYbcVGL8yCR0g" + ? "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJyZGUtdXNlciIsImF1dGgiOiJTdXBlciBBZG1pbixVc2VyLFJERSIsImV4cCI6MTc4MzM1Mjk0MywibmFtZSI6InJkZS11c2VyIGxhc3RuYW1lIn0.GM_4hREpICZZ7YTt6XzAkhwa93zYk1wzT9K3W-OZJqDsOksmcNhrsJqnoXu-C0QHBhFYeP2wYrCnVTBGL1L98A" : new URLSearchParams(window.location.search).get("jwt"); export const wsUrl = diff --git a/src/main/webapp/jsx/components/History/HTSEncounterHistory.js b/src/main/webapp/jsx/components/History/HTSEncounterHistory.js index 71db7b1e..7781a652 100644 --- a/src/main/webapp/jsx/components/History/HTSEncounterHistory.js +++ b/src/main/webapp/jsx/components/History/HTSEncounterHistory.js @@ -297,6 +297,14 @@ const formatHivResult = (rawCode) => { return ; }; +const formatPmtct = (rawCode) => { + if (rawCode?.pmtctHts === true) { + return TRUE + } + return FALSE; +}; + + const formatSyphilisResult = (rawCode) => { const { display, color } = mapResultCode(rawCode, SYPHILIS_RESULT_MAP); return ; @@ -411,6 +419,12 @@ const HTSEncounterHistory = (props) => { filtering: false, render: (rowData) => formatHivResult(rowData?.finalHivTestResult), }, + { + title: "PMTCT-HTS record", + field: "pmtctHts", + filtering: false, + render: (rowData) => formatPmtct(rowData), + }, { title: "Suspected Acute Infection", field: "suspectedAcuteInfection", @@ -472,6 +486,7 @@ const HTSEncounterHistory = (props) => { suspectedAcuteInfection: record.observation?.suspectedAcuteInfection ?? "", hivEarlyDetectResult: record.observation?.hivEarlyDetectResult ?? "", typeOfHivTestDone: record.observation?.typeOfHivTestDone ?? "", + pmtctHts: record?.pmtctHts ?? "", _raw: record, }))} options={{ diff --git a/src/main/webapp/jsx/components/NewToolForms/sections/BasicInformationSection.jsx b/src/main/webapp/jsx/components/NewToolForms/sections/BasicInformationSection.jsx index 41bc63e0..7e663638 100644 --- a/src/main/webapp/jsx/components/NewToolForms/sections/BasicInformationSection.jsx +++ b/src/main/webapp/jsx/components/NewToolForms/sections/BasicInformationSection.jsx @@ -469,7 +469,7 @@ const BasicInformationSection = ({ formik, isExistingPatient, readOnly }) => {
diff --git a/src/main/webapp/jsx/components/NewToolForms/sections/PostTestCounsellingSection.jsx b/src/main/webapp/jsx/components/NewToolForms/sections/PostTestCounsellingSection.jsx index 46e15c13..3e38f3c7 100644 --- a/src/main/webapp/jsx/components/NewToolForms/sections/PostTestCounsellingSection.jsx +++ b/src/main/webapp/jsx/components/NewToolForms/sections/PostTestCounsellingSection.jsx @@ -1,28 +1,73 @@ -import React, { useState } from "react"; +import React, { useState, useEffect, useMemo } from "react"; import { FormSelect, SectionSubheading } from "./FormFields"; import { PREVIOUSLY_TESTED_OPTIONS, } from "../constants"; import { useGetCodesets } from "../../../hooks/useGetCodesets.hook"; import { capitalizeFirstLetter } from "../../utils"; - -const COMPLETED_BY_OPTIONS = [ - { label: "Counsellor", value: "Counsellor" }, - { label: "Nurse", value: "Nurse" }, - { label: "Doctor", value: "Doctor" }, - { label: "Lab Technician", value: "Lab Technician" }, -]; - -const DESIGNATION_OPTIONS = [ - { label: "HTS Provider", value: "HTS Provider" }, - { label: "Nurse", value: "Nurse" }, - { label: "Doctor", value: "Doctor" }, - { label: "Community Health Worker", value: "Community Health Worker" }, -]; +import { getAllUsers } from "../../../services/getAllUsers.service"; const PostTestCounsellingSection = ({ formik, readOnly }) => { const { values, errors, touched, handleChange, handleBlur } = formik; const [codesets, setCodesets] = useState(null); + const [isLoadingAllUsers, setIsLoadingAllUsers] = useState(false); + const [allUsers, setAllUsers] = useState([]); + + useEffect(() => { + let isMounted = true; + + const fetchAllUsers = async () => { + setIsLoadingAllUsers(true); + try { + const data = await getAllUsers(); + if (isMounted) { + setAllUsers(Array.isArray(data) ? data : []); + } + } catch (error) { + console.error("Failed to fetch users:", error); + if (isMounted) { + setAllUsers([]); + } + } finally { + if (isMounted) { + setIsLoadingAllUsers(false); + } + } + }; + + fetchAllUsers(); + + return () => { + isMounted = false; + }; + }, []); + + // "Completed By" options: derived from firstname + lastname of each user + const completedByOptions = useMemo(() => { + if (!Array.isArray(allUsers)) return []; + return allUsers + .filter((user) => user?.firstName && user?.lastName) + .map((user) => { + const fullName = `${user.firstName} ${user.lastName}`; + return { label: fullName, value: fullName }; + }); + }, [allUsers]); + + // "Designation" options: unique, non-null designations across all users + const designationOptions = useMemo(() => { + if (!Array.isArray(allUsers)) return []; + const uniqueDesignations = [ + ...new Set( + allUsers + .map((user) => user?.designation) + .filter((designation) => !!designation) + ), + ]; + return uniqueDesignations.map((designation) => ({ + label: designation, + value: designation, + })); + }, [allUsers]); const fp = (name) => ({ name, @@ -60,6 +105,8 @@ const PostTestCounsellingSection = ({ formik, readOnly }) => { }); + + const showCategoryOfClient = values.hivTestKitsProvided === "YES_NO_YES"; const showAcceptedIndexTesting = values.confirmatoryHivTest?.toLowerCase() === "hiv_confirmatory_test_result_positive"; @@ -70,14 +117,14 @@ const PostTestCounsellingSection = ({ formik, readOnly }) => {
@@ -189,13 +236,15 @@ const PostTestCounsellingSection = ({ formik, readOnly }) => {
diff --git a/src/main/webapp/jsx/components/NewToolForms/sections/PreTestCounsellingSection.jsx b/src/main/webapp/jsx/components/NewToolForms/sections/PreTestCounsellingSection.jsx index 17b74dd3..20cfe26e 100644 --- a/src/main/webapp/jsx/components/NewToolForms/sections/PreTestCounsellingSection.jsx +++ b/src/main/webapp/jsx/components/NewToolForms/sections/PreTestCounsellingSection.jsx @@ -3,6 +3,7 @@ import React, { useEffect, useState } from "react"; import { FormSelect, SectionSubheading, ScoreDisplay } from "./FormFields"; import { useGetCodesets } from "../../../hooks/useGetCodesets.hook"; import { capitalizeFirstLetter } from "../../utils"; +import { url, token } from "../../../../api"; const skippedNoticeStyle = { padding: "12px 16px", diff --git a/src/main/webapp/jsx/services/getAllUsers.service.js b/src/main/webapp/jsx/services/getAllUsers.service.js new file mode 100644 index 00000000..3d5af38c --- /dev/null +++ b/src/main/webapp/jsx/services/getAllUsers.service.js @@ -0,0 +1,13 @@ +import axios from "axios"; +import { url as baseUrl, token } from "../../api"; + + + +export const getAllUsers = async () => { + const response = await axios + .get(`${baseUrl}users`, { + headers: { Authorization: `Bearer ${token}` }, + }) + + return response.data +} From b63e94b6af388683e4040a4f2b4a60eb8745bcdc Mon Sep 17 00:00:00 2001 From: Emmanuel Chinonye Nnajiofor Date: Mon, 6 Jul 2026 12:22:38 +0100 Subject: [PATCH 07/21] added ability to search by client code --- .../modules/hts/repository/HtsEncounterRepository.java | 2 ++ src/main/webapp/jsx/components/Patient/HtsEncounterList.jsx | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/src/main/java/org/lamisplus/modules/hts/repository/HtsEncounterRepository.java b/src/main/java/org/lamisplus/modules/hts/repository/HtsEncounterRepository.java index 9f94a881..e8a0d600 100644 --- a/src/main/java/org/lamisplus/modules/hts/repository/HtsEncounterRepository.java +++ b/src/main/java/org/lamisplus/modules/hts/repository/HtsEncounterRepository.java @@ -113,6 +113,7 @@ Page findPatientSummaries( " OR p.surname ILIKE CAST(:search AS text) \n" + " OR p.other_name ILIKE CAST(:search AS text) \n" + " OR p.hospital_number ILIKE CAST(:search AS text) \n" + + " OR e.client_code ILIKE CAST(:search AS text) \n" + " OR EXISTS (\n" + " SELECT 1 \n" + " FROM jsonb_array_elements(p.contact_point->'contactPoint') cp \n" + @@ -130,6 +131,7 @@ Page findPatientSummaries( " OR p.surname ILIKE CAST(:search AS text) \n" + " OR p.other_name ILIKE CAST(:search AS text) \n" + " OR p.hospital_number ILIKE CAST(:search AS text) \n" + + " OR e.client_code ILIKE CAST(:search AS text) \n" + " OR EXISTS (\n" + " SELECT 1 \n" + " FROM jsonb_array_elements(p.contact_point->'contactPoint') cp \n" + diff --git a/src/main/webapp/jsx/components/Patient/HtsEncounterList.jsx b/src/main/webapp/jsx/components/Patient/HtsEncounterList.jsx index 941880d4..6336ee82 100644 --- a/src/main/webapp/jsx/components/Patient/HtsEncounterList.jsx +++ b/src/main/webapp/jsx/components/Patient/HtsEncounterList.jsx @@ -128,6 +128,7 @@ const HTSEncounterList = () => { age: computeAge(p.dateOfBirth ?? row.observation?.dateOfBirth), htsCount: row.htsCount ?? 0, ictCount: row.ictCount ?? 0, + clientCode: row.clientCode ?? "", // ── Raw record for route state ───────────────────────── _raw, }; @@ -201,6 +202,11 @@ const HTSEncounterList = () => { ), }, + { + title: "Client Code", + field: "clientCode", + filtering: false, + }, { title: "Actions", field: "actions", From 09105122600d4a73cebdd926ab62012545f3999a Mon Sep 17 00:00:00 2001 From: gamalieldashuaDataFi <172114081+gamalieldashuaDataFi@users.noreply.github.com> Date: Mon, 6 Jul 2026 14:33:29 +0100 Subject: [PATCH 08/21] fix: optimized hts grids --- .../repository/HtsEncounterRepository.java | 79 +++++++++++++++++++ .../hts/service/HtsEncounterService.java | 2 +- src/main/resources/installers/hts/updates.xml | 17 ++++ 3 files changed, 97 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/lamisplus/modules/hts/repository/HtsEncounterRepository.java b/src/main/java/org/lamisplus/modules/hts/repository/HtsEncounterRepository.java index e8a0d600..96be4d6b 100644 --- a/src/main/java/org/lamisplus/modules/hts/repository/HtsEncounterRepository.java +++ b/src/main/java/org/lamisplus/modules/hts/repository/HtsEncounterRepository.java @@ -142,4 +142,83 @@ Page findHtsPatientSummaries( Long facilityId, @Param("search") String search, Pageable pageable); + + String HTS_SUMMARY_CTES = + "WITH hts_agg AS (\n" + + " SELECT DISTINCT ON (e.patient_uuid)\n" + + " e.id, e.uuid, e.patient_uuid, e.client_code, e.date_of_visit,\n" + + " e.setting, e.observation, e.facility_id,\n" + + " COUNT(*) OVER (PARTITION BY e.patient_uuid) AS hts_count\n" + + " FROM hts_encounter e\n" + + " WHERE e.archived = false\n" + + " AND e.facility_id = :facilityId\n" + + " ORDER BY e.patient_uuid, e.id DESC\n" + + "),\n" + + "ict_agg AS (\n" + + " SELECT patient_uuid, COUNT(*) AS ict_count\n" + + " FROM hts_ict_encounter\n" + + " WHERE archived = false\n" + + " AND facility_id = :facilityId\n" + + " GROUP BY patient_uuid\n" + + ")\n"; + + String HTS_SUMMARY_WHERE = + "WHERE p.archived = 0\n" + + " AND p.facility_id = :facilityId\n" + + " AND (:search IS NULL\n" + + " OR p.first_name ILIKE CAST(:search AS text)\n" + + " OR p.surname ILIKE CAST(:search AS text)\n" + + " OR p.other_name ILIKE CAST(:search AS text)\n" + + " OR p.hospital_number ILIKE CAST(:search AS text)\n" + + " OR EXISTS (\n" + + " SELECT 1\n" + + " FROM jsonb_array_elements(p.contact_point->'contactPoint') cp\n" + + " WHERE cp->>'value' ILIKE CAST(:search AS text)\n" + + " ))\n"; + + @Query(value = + HTS_SUMMARY_CTES + + "SELECT\n" + + " hts.id,\n" + + " CAST(hts.uuid AS text),\n" + + " p.id AS person_id,\n" + + " hts.client_code,\n" + + " hts.date_of_visit,\n" + + " hts.setting,\n" + + " CAST(hts.observation AS text),\n" + + " hts.facility_id,\n" + + " hts.hts_count,\n" + + " COALESCE(ict.ict_count, 0) AS ict_count\n" + + "FROM patient_person p\n" + + "INNER JOIN hts_agg hts ON hts.patient_uuid = p.uuid\n" + + "LEFT JOIN ict_agg ict ON ict.patient_uuid = p.uuid\n" + + HTS_SUMMARY_WHERE + + "ORDER BY hts.id DESC", + countQuery = + + "SELECT COUNT(*)\n" + + "FROM patient_person p\n" + + "WHERE p.archived = 0\n" + + " AND p.facility_id = :facilityId\n" + + " AND EXISTS (\n" + + " SELECT 1 FROM hts_encounter e\n" + + " WHERE e.patient_uuid = p.uuid\n" + + " AND e.archived = false\n" + + " AND e.facility_id = :facilityId\n" + + " )\n" + + " AND (:search IS NULL\n" + + " OR p.first_name ILIKE CAST(:search AS text)\n" + + " OR p.surname ILIKE CAST(:search AS text)\n" + + " OR p.other_name ILIKE CAST(:search AS text)\n" + + " OR p.hospital_number ILIKE CAST(:search AS text)\n" + + " OR EXISTS (\n" + + " SELECT 1\n" + + " FROM jsonb_array_elements(p.contact_point->'contactPoint') cp\n" + + " WHERE cp->>'value' ILIKE CAST(:search AS text)\n" + + " ))", + nativeQuery = true) + Page findHtsPatientSummariesOptimized( + @Param("facilityId") Long facilityId, + @Param("search") String search, + Pageable pageable); } \ No newline at end of file diff --git a/src/main/java/org/lamisplus/modules/hts/service/HtsEncounterService.java b/src/main/java/org/lamisplus/modules/hts/service/HtsEncounterService.java index fb87320d..e49fe9a1 100644 --- a/src/main/java/org/lamisplus/modules/hts/service/HtsEncounterService.java +++ b/src/main/java/org/lamisplus/modules/hts/service/HtsEncounterService.java @@ -168,7 +168,7 @@ public Page getHtsPatientSummaries(String search, Pageable ); Long facilityId = currentUserOrganizationService.getCurrentUserOrganization(); - Page raw = repository.findHtsPatientSummaries(facilityId, searchParam, pageableWithoutSort); + Page raw = repository.findHtsPatientSummariesOptimized(facilityId, searchParam, pageableWithoutSort); return raw.map(row -> { HtsPatientSummaryDto dto = new HtsPatientSummaryDto(); diff --git a/src/main/resources/installers/hts/updates.xml b/src/main/resources/installers/hts/updates.xml index 10980c74..111be8bb 100644 --- a/src/main/resources/installers/hts/updates.xml +++ b/src/main/resources/installers/hts/updates.xml @@ -291,4 +291,21 @@ + + + -- hts_encounter: DISTINCT ON latest + window COUNT driver + -- (filter on facility_id+archived, then order by patient_uuid, id DESC). + CREATE INDEX IF NOT EXISTS idx_hts_encounter_grid_latest + ON hts_encounter (facility_id, archived, patient_uuid, id DESC); + + -- hts_encounter: EXISTS semi-join in the lean count query. + CREATE INDEX IF NOT EXISTS idx_hts_encounter_grid_count + ON hts_encounter (patient_uuid, facility_id, archived); + + -- hts_ict_encounter: grouped ict_count CTE. + CREATE INDEX IF NOT EXISTS idx_hts_ict_encounter_grid_count + ON hts_ict_encounter (facility_id, archived, patient_uuid); + + + \ No newline at end of file From 9a51b09e01e600766ba5d8a55fb10a65625bee8d Mon Sep 17 00:00:00 2001 From: gamalieldashuaDataFi <172114081+gamalieldashuaDataFi@users.noreply.github.com> Date: Mon, 6 Jul 2026 15:54:03 +0100 Subject: [PATCH 09/21] fix: optimized hts grids --- .../hts/repository/HtsClientRepository.java | 225 +++++++++++------- .../repository/HtsEncounterRepository.java | 72 ------ .../modules/hts/service/HtsClientService.java | 15 +- src/main/resources/installers/hts/updates.xml | 91 +++++++ 4 files changed, 247 insertions(+), 156 deletions(-) diff --git a/src/main/java/org/lamisplus/modules/hts/repository/HtsClientRepository.java b/src/main/java/org/lamisplus/modules/hts/repository/HtsClientRepository.java index f56625cf..5907246d 100644 --- a/src/main/java/org/lamisplus/modules/hts/repository/HtsClientRepository.java +++ b/src/main/java/org/lamisplus/modules/hts/repository/HtsClientRepository.java @@ -34,30 +34,60 @@ public interface HtsClientRepository extends JpaRepository { Optional findFirstByRiskStratificationCode(String riskStratificationCode); - @Query(value = "SELECT \n" + - " p.hospital_number AS hospitalNumber,\n" + - " p.id AS personId,\n" + - " p.uuid AS personUuid,\n" + - " p.first_name AS firstName,\n" + - " p.surname AS surname,\n" + - " p.other_name AS otherName,\n" + - " CAST(EXTRACT(YEAR FROM AGE(NOW(), p.date_of_birth)) AS INTEGER) AS age,\n" + - " INITCAP(p.sex) AS gender\n" + - "FROM patient_person p\n" + - "WHERE p.archived = ?1\n" + - "AND (\n" + - " p.first_name ILIKE ?3\n" + - " OR p.surname ILIKE ?3\n" + - " OR p.other_name ILIKE ?3\n" + - " OR p.hospital_number ILIKE ?3\n" + - " OR p.contact_point#>>'{contactPoint,0,value}' ILIKE ?3\n" + - ")\n" + + @Query(value = "SELECT hc.client_code as clientCode, p.id as personId, p.first_name as firstName, p.surname as surname, p.other_name as otherName, " + + "p.hospital_number as hospitalNumber, CAST (EXTRACT(YEAR from AGE(NOW(), date_of_birth)) AS INTEGER) as age, " + + "INITCAP(p.sex) as gender, p.date_of_birth as dateOfBirth, CAST (COUNT(hc.person_uuid) AS INTEGER) as htsCount " + + "FROM patient_person p " + + "LEFT JOIN hts_client hc ON hc.person_uuid = p.uuid AND hc.archived = ?1 " + + "WHERE p.archived=?1 AND p.facility_id=?2 AND (p.first_name ILIKE ?3 " + + "OR p.surname ILIKE ?3 OR p.other_name ILIKE ?3 " + + "OR p.hospital_number ILIKE ?3 OR hc.client_code ILIKE ?3) " + + "GROUP BY hc.client_code, p.id, p.first_name, p.first_name, p.surname, p.other_name, p.hospital_number, p.date_of_birth", nativeQuery = true) + List findAllPersonHtsBySearchParam(Integer archived, Long facilityId, String search); + + + + // ───────────────────────────────────────────────────────────────────────── + // OPTIMIZED equivalent of findAllPersonHts / findAllPersonHtsBySearchParam + // ("Patients" tab). Same result set + projection, faster. + // + // The originals call is_pmtct_infant(p.hospital_number) once PER ROW in the + // WHERE clause — a scalar function that runs a sub-query per invocation, for + // every candidate at the facility, on BOTH the data and the count query. This + // version materialises the full set of PMTCT infant hospital numbers ONCE in a + // CTE (via pmtct_infant_hospital_numbers()) and excludes them with a single + // hash NOT EXISTS anti-join — O(n) instead of n per-row function calls. + // + // The two hts_encounter NOT EXISTS blocks are kept byte-for-byte (the planner + // already anti-joins them, and the perf indexes back them). Explicit count + // queries keep totals exact and avoid Spring's count-derivation on a leading + // WITH. NULL / missing-table / differing-shape behaviour is identical to + // is_pmtct_infant, so the row set is unchanged. + // + // Revert by pointing the service back at findAllPersonHts / ...BySearchParam. + // ───────────────────────────────────────────────────────────────────────── + String PATIENTS_INFANT_CTE = + "WITH pmtct_infants AS (\n" + + " SELECT DISTINCT hospital_number\n" + + " FROM pmtct_infant_hospital_numbers() AS t(hospital_number)\n" + + ")\n"; + + String PATIENTS_SELECT_COLS = + "SELECT p.hospital_number AS hospitalNumber, p.uuid AS personUuid, p.id AS personId,\n" + + " p.first_name AS firstName, p.surname AS surname, p.other_name AS otherName,\n" + + " CAST(EXTRACT(YEAR FROM AGE(NOW(), p.date_of_birth)) AS INTEGER) AS age,\n" + + " INITCAP(p.sex) AS gender\n" + + "FROM patient_person p\n"; + + // Two hts_encounter anti-joins — identical to findAllPersonHts — plus the + // set-based infant exclusion replacing is_pmtct_infant(p.hospital_number). + String PATIENTS_NOT_EXISTS = "AND NOT EXISTS (\n" + " SELECT 1\n" + " FROM hts_encounter hs\n" + " WHERE hs.client_code = p.hospital_number\n" + " AND hs.archived = false\n" + - " AND CAST(hs.patient_uuid AS TEXT) IS NOT NULL AND CAST(hs.patient_uuid AS TEXT) != ''\n" + + " AND CAST(hs.patient_uuid AS TEXT) is not null AND CAST(hs.patient_uuid AS TEXT) != ''\n" + " AND hs.facility_id = ?2\n" + ")\n" + "AND NOT EXISTS (\n" + @@ -67,72 +97,107 @@ public interface HtsClientRepository extends JpaRepository { " AND htse.facility_id = ?2\n" + " AND htse.archived = false\n" + ")\n" + - "AND is_pmtct_infant(p.hospital_number) = FALSE\n" + - "", nativeQuery = true) - Page findAllPersonHtsBySearchParam(Integer archived, Long facilityId, String search, Pageable pageable); - - - @Query(value = "SELECT DISTINCT hc.client_code AS clientCode, p.id as personId,p.uuid as personUuid, p.first_name as firstName, p.surname as surname, p.other_name as otherName, \n" + - "p.hospital_number as hospitalNumber, CAST (EXTRACT(YEAR from AGE(NOW(), date_of_birth)) AS INTEGER) as age, \n" + - "INITCAP(p.sex) as gender, p.date_of_birth as dateOfBirth,mini.person_uuid,mini.hts_count htsCount \n" + - "FROM patient_person p \n" + - "INNER JOIN hts_client hc ON hc.person_uuid = p.uuid AND hc.archived = ?1 \n" + - "INNER JOIN (SELECT person_uuid,MIN(date_created) first_hts_registration,COUNT(person_uuid) hts_count \n" + - "FROM hts_client \n" + - "WHERE person_uuid IS NOT NULL AND archived = ?1 AND facility_id=?2 " + - "GROUP BY 1) mini \n" + - "ON hc.person_uuid=mini.person_uuid AND hc.date_created=mini.first_hts_registration \n" + - "where p.first_name ILIKE ?3 OR p.surname ILIKE ?3 OR p.other_name ILIKE ?3 \n" + - "OR p.hospital_number ILIKE ?3 OR hc.client_code ILIKE ?3 ", nativeQuery = true) - Page findOnlyPersonHtsBySearchParam(Integer archived, Long facilityId, String search, Pageable pageable); + "AND NOT EXISTS (\n" + + " SELECT 1 FROM pmtct_infants pin WHERE pin.hospital_number = p.hospital_number\n" + + ")\n"; + @Query(value = + PATIENTS_INFANT_CTE + PATIENTS_SELECT_COLS + + "WHERE p.archived = ?1 AND p.facility_id = ?2\n" + + PATIENTS_NOT_EXISTS + + "ORDER BY p.created_date DESC", + countQuery = + PATIENTS_INFANT_CTE + + "SELECT COUNT(*) FROM patient_person p\n" + + "WHERE p.archived = ?1 AND p.facility_id = ?2\n" + + PATIENTS_NOT_EXISTS, + nativeQuery = true) + Page findAllPersonHtsOptimized(Integer archived, Long facilityId, Pageable pageable); - @Query(value = "SELECT hc.client_code as clientCode, p.id as personId, p.first_name as firstName, p.surname as surname, p.other_name as otherName, " + - "p.hospital_number as hospitalNumber, CAST (EXTRACT(YEAR from AGE(NOW(), date_of_birth)) AS INTEGER) as age, " + - "INITCAP(p.sex) as gender, p.date_of_birth as dateOfBirth, CAST (COUNT(hc.person_uuid) AS INTEGER) as htsCount " + - "FROM patient_person p " + - "LEFT JOIN hts_client hc ON hc.person_uuid = p.uuid AND hc.archived = ?1 " + - "WHERE p.archived=?1 AND p.facility_id=?2 AND (p.first_name ILIKE ?3 " + - "OR p.surname ILIKE ?3 OR p.other_name ILIKE ?3 " + - "OR p.hospital_number ILIKE ?3 OR hc.client_code ILIKE ?3) " + - "GROUP BY hc.client_code, p.id, p.first_name, p.first_name, p.surname, p.other_name, p.hospital_number, p.date_of_birth", nativeQuery = true) - List findAllPersonHtsBySearchParam(Integer archived, Long facilityId, String search); + @Query(value = + PATIENTS_INFANT_CTE + PATIENTS_SELECT_COLS + + "WHERE p.archived = ?1\n" + + "AND (\n" + + " p.first_name ILIKE ?3\n" + + " OR p.surname ILIKE ?3\n" + + " OR p.other_name ILIKE ?3\n" + + " OR p.hospital_number ILIKE ?3\n" + + " OR p.contact_point#>>'{contactPoint,0,value}' ILIKE ?3\n" + + ")\n" + + PATIENTS_NOT_EXISTS, + countQuery = + PATIENTS_INFANT_CTE + + "SELECT COUNT(*) FROM patient_person p\n" + + "WHERE p.archived = ?1\n" + + "AND (\n" + + " p.first_name ILIKE ?3\n" + + " OR p.surname ILIKE ?3\n" + + " OR p.other_name ILIKE ?3\n" + + " OR p.hospital_number ILIKE ?3\n" + + " OR p.contact_point#>>'{contactPoint,0,value}' ILIKE ?3\n" + + ")\n" + + PATIENTS_NOT_EXISTS, + nativeQuery = true) + Page findAllPersonHtsBySearchParamOptimized(Integer archived, Long facilityId, String search, Pageable pageable); + // ───────────────────────────────────────────────────────────────────────── + // OPTIMIZED equivalent of findOnlyPersonHts / findOnlyPersonHtsBySearchParam + // ("HTS Patients Archived" tab). Same result set + projection, faster. + // + // The originals scan hts_client TWICE — once for the MIN(date_created) + + // COUNT group-subquery ("mini"), then self-join hts_client back to it on + // (person_uuid, date_created) to fetch the first-registration row. This + // version scans hts_client ONCE: DISTINCT ON (person_uuid) ... ORDER BY + // person_uuid, date_created ASC, id ASC picks the earliest registration per + // person, and COUNT(*) OVER (PARTITION BY person_uuid) carries hts_count on + // the same pass. Explicit countQuery over the same CTE keeps totals exact + // (and avoids Spring's fragile count-derivation on a leading WITH). + // + // Revert by pointing the service back at findOnlyPersonHts / ...BySearchParam. + // ───────────────────────────────────────────────────────────────────────── + String ONLY_PERSON_HTS_CTE = + "WITH first_hc AS (\n" + + " SELECT DISTINCT ON (hc.person_uuid)\n" + + " hc.id, hc.client_code, hc.person_uuid, hc.date_created,\n" + + " COUNT(*) OVER (PARTITION BY hc.person_uuid) AS hts_count\n" + + " FROM hts_client hc\n" + + " WHERE hc.person_uuid IS NOT NULL\n" + + " AND hc.archived = ?1\n" + + " AND hc.facility_id = ?2\n" + + " ORDER BY hc.person_uuid, hc.date_created ASC, hc.id ASC\n" + + ")\n"; + String ONLY_PERSON_HTS_SELECT = + "SELECT fc.id, fc.client_code AS clientCode, p.id as personId, p.uuid as personUuid,\n" + + " p.first_name as firstName, p.surname as surname, p.other_name as otherName,\n" + + " p.hospital_number as hospitalNumber,\n" + + " CAST(EXTRACT(YEAR from AGE(NOW(), p.date_of_birth)) AS INTEGER) as age,\n" + + " INITCAP(p.sex) as gender, p.date_of_birth as dateOfBirth,\n" + + " fc.person_uuid, fc.hts_count htsCount\n" + + "FROM first_hc fc\n" + + "INNER JOIN patient_person p ON p.uuid = fc.person_uuid\n"; + @Query(value = + ONLY_PERSON_HTS_CTE + ONLY_PERSON_HTS_SELECT + "ORDER BY fc.id DESC", + countQuery = + ONLY_PERSON_HTS_CTE + + "SELECT COUNT(*) FROM first_hc fc " + + "INNER JOIN patient_person p ON p.uuid = fc.person_uuid", + nativeQuery = true) + Page findOnlyPersonHtsOptimized(Integer archived, Long facilityId, Pageable pageable); - @Query(value = "SELECT p.hospital_number AS hospitalNumber, p.uuid as personUuid, p.id AS personId, p.first_name AS firstName, p.surname AS surname, \n" + - " p.other_name AS otherName, CAST(EXTRACT(YEAR FROM AGE(NOW(), p.date_of_birth)) AS INTEGER) AS age, INITCAP(p.sex) AS gender \n" + - "\tFROM patient_person p \n" + - "\tWHERE p.archived = ?1 AND p.facility_id = ?2 \n" + - "\t\t\t\tAND NOT EXISTS (\n" + - "\t\t\t\t\tSELECT 1 \n" + - "\t\t\t\t\tFROM hts_encounter hs\n" + - "\t\t\t\t\tWHERE hs.client_code = p.hospital_number\n" + - "\t\t\t\t\tAND hs.archived = false \n" + - "\t\t\t\t\tAND CAST(hs.patient_uuid AS TEXT) is not null AND CAST(hs.patient_uuid AS TEXT) != ''\n" + - "\t\t\t\t\tAND hs.facility_id = ?2 \n" + - "\t\t\t\t)\n" + - " AND NOT EXISTS (\n" + - "\t\t\t\t SELECT 1\n" + - "\t\t\t\t FROM hts_encounter htse\n" + - "\t\t\t\t WHERE CAST(htse.patient_uuid AS TEXT) = p.uuid\n" + - "\t\t\t\t AND htse.facility_id = ?2\n" + - "\t\t\t\t AND htse.archived = false\n" + - " )\n" + - " AND is_pmtct_infant(p.hospital_number) = FALSE\n" + - "\t\t\tORDER BY p.created_date DESC" , nativeQuery = true) - Page findAllPersonHts(Integer archived, Long facilityId, Pageable pageable); - @Query(value = "SELECT DISTINCT hc.id, hc.client_code AS clientCode, p.id as personId,p.uuid as personUuid, p.first_name as firstName, p.surname as surname, p.other_name as otherName, \n" + - "p.hospital_number as hospitalNumber, CAST (EXTRACT(YEAR from AGE(NOW(), date_of_birth)) AS INTEGER) as age, \n" + - "INITCAP(p.sex) as gender, p.date_of_birth as dateOfBirth,mini.person_uuid,mini.hts_count htsCount\n" + - "FROM patient_person p\n" + - "INNER JOIN hts_client hc ON hc.person_uuid = p.uuid AND hc.archived = ?1 \n" + - "INNER JOIN (SELECT person_uuid,MIN(date_created) first_hts_registration,COUNT(person_uuid) hts_count\n" + - "FROM hts_client\n" + - "WHERE person_uuid IS NOT NULL AND archived = ?1 AND facility_id=?2 \n" + - "GROUP BY 1) mini\n" + - "ON hc.person_uuid=mini.person_uuid AND hc.date_created=mini.first_hts_registration ORDER BY hc.id DESC", nativeQuery = true) - Page findOnlyPersonHts(Integer archived, Long facilityId, Pageable pageable); + @Query(value = + ONLY_PERSON_HTS_CTE + ONLY_PERSON_HTS_SELECT + + "WHERE p.first_name ILIKE ?3 OR p.surname ILIKE ?3 OR p.other_name ILIKE ?3\n" + + " OR p.hospital_number ILIKE ?3 OR fc.client_code ILIKE ?3\n" + + "ORDER BY fc.id DESC", + countQuery = + ONLY_PERSON_HTS_CTE + + "SELECT COUNT(*) FROM first_hc fc " + + "INNER JOIN patient_person p ON p.uuid = fc.person_uuid " + + "WHERE p.first_name ILIKE ?3 OR p.surname ILIKE ?3 OR p.other_name ILIKE ?3 " + + " OR p.hospital_number ILIKE ?3 OR fc.client_code ILIKE ?3", + nativeQuery = true) + Page findOnlyPersonHtsBySearchParamOptimized(Integer archived, Long facilityId, String search, Pageable pageable); @Query(value = "WITH hts_patients AS ( \n" + " SELECT DISTINCT \n" + diff --git a/src/main/java/org/lamisplus/modules/hts/repository/HtsEncounterRepository.java b/src/main/java/org/lamisplus/modules/hts/repository/HtsEncounterRepository.java index 96be4d6b..bc4dde74 100644 --- a/src/main/java/org/lamisplus/modules/hts/repository/HtsEncounterRepository.java +++ b/src/main/java/org/lamisplus/modules/hts/repository/HtsEncounterRepository.java @@ -71,78 +71,6 @@ Page findPatientSummaries( @Param("search") String search, Pageable pageable); - @Query(value = - "SELECT \n" + - " e.id, \n" + - " CAST(e.uuid AS text), \n" + - " p.id AS person_id, \n" + - " e.client_code, \n" + - " e.date_of_visit, \n" + - " e.setting, \n" + - " CAST(e.observation AS text), \n" + - " e.facility_id, \n" + - " hts_agg.hts_count, \n" + - " ict_agg.ict_count \n" + - "FROM patient_person p \n" + - "INNER JOIN (\n" + - " SELECT patient_uuid, MAX(id) AS max_id \n" + - " FROM hts_encounter \n" + - " WHERE archived = false \n" + - "\tAND facility_id=:facilityId\n" + - " GROUP BY patient_uuid\n" + - ") latest ON latest.patient_uuid = p.uuid \n" + - "INNER JOIN hts_encounter e ON e.id = latest.max_id \n" + - "INNER JOIN (\n" + - " SELECT patient_uuid, COUNT(*) AS hts_count \n" + - " FROM hts_encounter \n" + - " WHERE archived = false \n" + - "\tAND facility_id=:facilityId\n" + - " GROUP BY patient_uuid\n" + - ") hts_agg ON hts_agg.patient_uuid = p.uuid \n" + - "LEFT JOIN (\n" + - " SELECT patient_uuid, COUNT(*) AS ict_count \n" + - " FROM hts_ict_encounter \n" + - " WHERE archived = false \n" + - "\tAND facility_id=:facilityId\n" + - " GROUP BY patient_uuid\n" + - ") ict_agg ON ict_agg.patient_uuid = p.uuid \n" + - "WHERE p.archived = 0 \n" + - "\tAND p.facility_id=:facilityId\n" + - " AND (:search IS NULL \n" + - " OR p.first_name ILIKE CAST(:search AS text) \n" + - " OR p.surname ILIKE CAST(:search AS text) \n" + - " OR p.other_name ILIKE CAST(:search AS text) \n" + - " OR p.hospital_number ILIKE CAST(:search AS text) \n" + - " OR e.client_code ILIKE CAST(:search AS text) \n" + - " OR EXISTS (\n" + - " SELECT 1 \n" + - " FROM jsonb_array_elements(p.contact_point->'contactPoint') cp \n" + - " WHERE cp->>'value' ILIKE CAST(:search AS text)\n" + - " )) \n" + - "ORDER BY e.id DESC", - countQuery = - "SELECT COUNT(DISTINCT p.id) \n" + - " FROM patient_person p \n" + - " INNER JOIN hts_encounter e ON e.patient_uuid = p.uuid AND e.archived = false \n" + - " WHERE p.archived = 0 \n" + - " AND p.facility_id=:facilityId\n" + - " AND (:search IS NULL \n" + - " OR p.first_name ILIKE CAST(:search AS text) \n" + - " OR p.surname ILIKE CAST(:search AS text) \n" + - " OR p.other_name ILIKE CAST(:search AS text) \n" + - " OR p.hospital_number ILIKE CAST(:search AS text) \n" + - " OR e.client_code ILIKE CAST(:search AS text) \n" + - " OR EXISTS (\n" + - " SELECT 1 \n" + - " FROM jsonb_array_elements(p.contact_point->'contactPoint') cp \n" + - " WHERE cp->>'value' ILIKE CAST(:search AS text)\n" + - " ))", - nativeQuery = true) - Page findHtsPatientSummaries( - Long facilityId, - @Param("search") String search, - Pageable pageable); - String HTS_SUMMARY_CTES = "WITH hts_agg AS (\n" + " SELECT DISTINCT ON (e.patient_uuid)\n" + diff --git a/src/main/java/org/lamisplus/modules/hts/service/HtsClientService.java b/src/main/java/org/lamisplus/modules/hts/service/HtsClientService.java index 6428173f..64211461 100644 --- a/src/main/java/org/lamisplus/modules/hts/service/HtsClientService.java +++ b/src/main/java/org/lamisplus/modules/hts/service/HtsClientService.java @@ -532,27 +532,34 @@ public Page getAllPersonHts(String search, int pageNo, int pageSize) Long facilityId = currentUserOrganizationService.getCurrentUserOrganization(); //List htsPeople = new ArrayList<>(); Pageable pageable = PageRequest.of(pageNo, pageSize); + // Optimized: PMTCT-infant exclusion is a single materialised NOT EXISTS + // anti-join (pmtct_infant_hospital_numbers() in a CTE) instead of a + // per-row is_pmtct_infant() call; two hts_encounter anti-joins unchanged. + // Revert by swapping back to findAllPersonHtsBySearchParam / findAllPersonHts. if(!String.valueOf(search).equals("null") && !search.equals("*")){ search = search.replaceAll("\\s", ""); String queryParam = "%"+search+"%"; return htsClientRepository - .findAllPersonHtsBySearchParam(UN_ARCHIVED, facilityId, queryParam, pageable); + .findAllPersonHtsBySearchParamOptimized(UN_ARCHIVED, facilityId, queryParam, pageable); } return htsClientRepository - .findAllPersonHts(UN_ARCHIVED, facilityId, pageable); + .findAllPersonHtsOptimized(UN_ARCHIVED, facilityId, pageable); } public Page getOnlyPersonHts(String search, int pageNo, int pageSize) { Long facilityId = currentUserOrganizationService.getCurrentUserOrganization(); Pageable pageable = PageRequest.of(pageNo, pageSize); + // Optimized: single hts_client scan (DISTINCT ON earliest registration + + // window COUNT) with an exact CTE-based count query. + // Revert by swapping back to findOnlyPersonHtsBySearchParam / findOnlyPersonHts. if(!String.valueOf(search).equals("null") && !search.equals("*")){ search = search.replaceAll("\\s", ""); String queryParam = "%"+search+"%"; return htsClientRepository - .findOnlyPersonHtsBySearchParam(UN_ARCHIVED, facilityId, queryParam, pageable); + .findOnlyPersonHtsBySearchParamOptimized(UN_ARCHIVED, facilityId, queryParam, pageable); } return htsClientRepository - .findOnlyPersonHts(UN_ARCHIVED, facilityId, pageable); + .findOnlyPersonHtsOptimized(UN_ARCHIVED, facilityId, pageable); } public HtsClientDtos getAllHtsClientDtos(Page page) { diff --git a/src/main/resources/installers/hts/updates.xml b/src/main/resources/installers/hts/updates.xml index 111be8bb..90680ba4 100644 --- a/src/main/resources/installers/hts/updates.xml +++ b/src/main/resources/installers/hts/updates.xml @@ -308,4 +308,95 @@ + + + + DROP FUNCTION IF EXISTS is_pmtct_infant(TEXT); + + + + + + + + -- patient_person driver + ORDER BY created_date DESC. + CREATE INDEX IF NOT EXISTS idx_patient_person_facility_arch_created + ON patient_person (facility_id, archived, created_date DESC); + + -- NOT EXISTS #1: hts_encounter.client_code = patient_person.hospital_number. + CREATE INDEX IF NOT EXISTS idx_hts_encounter_facility_arch_client_code + ON hts_encounter (facility_id, archived, client_code); + + -- NOT EXISTS #2: CAST(hts_encounter.patient_uuid AS TEXT) = patient_person.uuid. + CREATE INDEX IF NOT EXISTS idx_hts_encounter_facility_arch_uuid_text + ON hts_encounter (facility_id, archived, (CAST(patient_uuid AS text))); + + -- "HTS Patients Archived" tab (findOnlyPersonHts): DISTINCT ON first + -- (earliest) registration per person + per-person count. + CREATE INDEX IF NOT EXISTS idx_hts_client_facility_arch_person_created + ON hts_client (facility_id, archived, person_uuid, date_created); + + -- "HIVST Patients" tab (findAllPersonHivst): hivst ⋈ patient_person. + CREATE INDEX IF NOT EXISTS idx_hivst_patient_archived + ON hivst (patient_id, archived); + CREATE INDEX IF NOT EXISTS idx_patient_person_facility_arch_id + ON patient_person (facility_id, archived, id); + + + + + + + CREATE OR REPLACE FUNCTION pmtct_infant_hospital_numbers() + RETURNS SETOF text + LANGUAGE plpgsql + STABLE + AS $func$ + BEGIN + IF to_regclass('public.pmtct_infant_information') IS NULL THEN + RETURN; + END IF; + + RETURN QUERY EXECUTE ' + SELECT infant_hospital_number + FROM public.pmtct_infant_information + WHERE infant_hospital_number IS NOT NULL + '; + + EXCEPTION WHEN OTHERS THEN + -- Table exists but column/shape differs on this facility: stay safe. + RETURN; + END; + $func$; + + + DROP FUNCTION IF EXISTS pmtct_infant_hospital_numbers(); + + + \ No newline at end of file From 80bc11d0fa13662f6eeae8f1276c985d5c193ee3 Mon Sep 17 00:00:00 2001 From: gamalieldashuaDataFi Date: Mon, 6 Jul 2026 16:15:21 +0100 Subject: [PATCH 10/21] fix: added some cleanups --- .../modules/hts/installers/Updates.java | 2 +- .../hts/repository/HtsClientRepository.java | 36 +------------------ .../modules/hts/service/HtsClientService.java | 7 +--- src/main/resources/installers/hts/updates.xml | 28 --------------- 4 files changed, 3 insertions(+), 70 deletions(-) diff --git a/src/main/java/org/lamisplus/modules/hts/installers/Updates.java b/src/main/java/org/lamisplus/modules/hts/installers/Updates.java index bcc23d7a..2620163e 100644 --- a/src/main/java/org/lamisplus/modules/hts/installers/Updates.java +++ b/src/main/java/org/lamisplus/modules/hts/installers/Updates.java @@ -6,7 +6,7 @@ @Order(3) @Installer(name = "schema-installer-update", description = "Updates the required database tables data", - version = 15) + version = 16) public class Updates extends AcrossLiquibaseInstaller { public Updates() { super("classpath:installers/hts/updates.xml"); diff --git a/src/main/java/org/lamisplus/modules/hts/repository/HtsClientRepository.java b/src/main/java/org/lamisplus/modules/hts/repository/HtsClientRepository.java index 5907246d..808f5b35 100644 --- a/src/main/java/org/lamisplus/modules/hts/repository/HtsClientRepository.java +++ b/src/main/java/org/lamisplus/modules/hts/repository/HtsClientRepository.java @@ -46,26 +46,6 @@ public interface HtsClientRepository extends JpaRepository { List findAllPersonHtsBySearchParam(Integer archived, Long facilityId, String search); - - // ───────────────────────────────────────────────────────────────────────── - // OPTIMIZED equivalent of findAllPersonHts / findAllPersonHtsBySearchParam - // ("Patients" tab). Same result set + projection, faster. - // - // The originals call is_pmtct_infant(p.hospital_number) once PER ROW in the - // WHERE clause — a scalar function that runs a sub-query per invocation, for - // every candidate at the facility, on BOTH the data and the count query. This - // version materialises the full set of PMTCT infant hospital numbers ONCE in a - // CTE (via pmtct_infant_hospital_numbers()) and excludes them with a single - // hash NOT EXISTS anti-join — O(n) instead of n per-row function calls. - // - // The two hts_encounter NOT EXISTS blocks are kept byte-for-byte (the planner - // already anti-joins them, and the perf indexes back them). Explicit count - // queries keep totals exact and avoid Spring's count-derivation on a leading - // WITH. NULL / missing-table / differing-shape behaviour is identical to - // is_pmtct_infant, so the row set is unchanged. - // - // Revert by pointing the service back at findAllPersonHts / ...BySearchParam. - // ───────────────────────────────────────────────────────────────────────── String PATIENTS_INFANT_CTE = "WITH pmtct_infants AS (\n" + " SELECT DISTINCT hospital_number\n" + @@ -139,21 +119,7 @@ public interface HtsClientRepository extends JpaRepository { PATIENTS_NOT_EXISTS, nativeQuery = true) Page findAllPersonHtsBySearchParamOptimized(Integer archived, Long facilityId, String search, Pageable pageable); - // ───────────────────────────────────────────────────────────────────────── - // OPTIMIZED equivalent of findOnlyPersonHts / findOnlyPersonHtsBySearchParam - // ("HTS Patients Archived" tab). Same result set + projection, faster. - // - // The originals scan hts_client TWICE — once for the MIN(date_created) + - // COUNT group-subquery ("mini"), then self-join hts_client back to it on - // (person_uuid, date_created) to fetch the first-registration row. This - // version scans hts_client ONCE: DISTINCT ON (person_uuid) ... ORDER BY - // person_uuid, date_created ASC, id ASC picks the earliest registration per - // person, and COUNT(*) OVER (PARTITION BY person_uuid) carries hts_count on - // the same pass. Explicit countQuery over the same CTE keeps totals exact - // (and avoids Spring's fragile count-derivation on a leading WITH). - // - // Revert by pointing the service back at findOnlyPersonHts / ...BySearchParam. - // ───────────────────────────────────────────────────────────────────────── + String ONLY_PERSON_HTS_CTE = "WITH first_hc AS (\n" + " SELECT DISTINCT ON (hc.person_uuid)\n" + diff --git a/src/main/java/org/lamisplus/modules/hts/service/HtsClientService.java b/src/main/java/org/lamisplus/modules/hts/service/HtsClientService.java index 64211461..f06f657c 100644 --- a/src/main/java/org/lamisplus/modules/hts/service/HtsClientService.java +++ b/src/main/java/org/lamisplus/modules/hts/service/HtsClientService.java @@ -549,9 +549,7 @@ public Page getAllPersonHts(String search, int pageNo, int pageSize) public Page getOnlyPersonHts(String search, int pageNo, int pageSize) { Long facilityId = currentUserOrganizationService.getCurrentUserOrganization(); Pageable pageable = PageRequest.of(pageNo, pageSize); - // Optimized: single hts_client scan (DISTINCT ON earliest registration + - // window COUNT) with an exact CTE-based count query. - // Revert by swapping back to findOnlyPersonHtsBySearchParam / findOnlyPersonHts. + if(!String.valueOf(search).equals("null") && !search.equals("*")){ search = search.replaceAll("\\s", ""); String queryParam = "%"+search+"%"; @@ -681,9 +679,6 @@ public HtsClientDtos getRiskStratificationHtsClients(Long personId) { } public Boolean checkForClientCode(String clientCode) { - // if the repository finds that the client code exists, - // should return false to indicate that - // this client code doesn't pass the check, else true return !htsClientRepository.existsByClientCode(clientCode); } diff --git a/src/main/resources/installers/hts/updates.xml b/src/main/resources/installers/hts/updates.xml index 90680ba4..550edb84 100644 --- a/src/main/resources/installers/hts/updates.xml +++ b/src/main/resources/installers/hts/updates.xml @@ -308,13 +308,6 @@ - DROP FUNCTION IF EXISTS is_pmtct_infant(TEXT); @@ -322,16 +315,6 @@ - -- patient_person driver + ORDER BY created_date DESC. @@ -359,17 +342,6 @@ - CREATE OR REPLACE FUNCTION pmtct_infant_hospital_numbers() From 13329e310aff233e688bea2bf15be71ebaff2b32 Mon Sep 17 00:00:00 2001 From: Emmanuel Chinonye Nnajiofor Date: Mon, 6 Jul 2026 16:28:58 +0100 Subject: [PATCH 11/21] more issues solved --- pom.xml | 2 +- .../components/IctForm/NewPatientHtsForm.jsx | 2 + .../NewToolForms/ExistingPatientHtsForm.jsx | 3 ++ .../NewToolForms/NewEncounterHtsForm.jsx | 3 ++ .../NewToolForms/NewPatientHtsForm.jsx | 2 + .../sections/PostTestCounsellingSection.jsx | 40 +++++++++++++------ 6 files changed, 39 insertions(+), 13 deletions(-) diff --git a/pom.xml b/pom.xml index 1415fcd5..60077138 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ org.lamisplus.modules hts - 2.5.0 + 2.5.1 jar Hts diff --git a/src/main/webapp/jsx/components/IctForm/NewPatientHtsForm.jsx b/src/main/webapp/jsx/components/IctForm/NewPatientHtsForm.jsx index 28be62ce..3ae98e50 100644 --- a/src/main/webapp/jsx/components/IctForm/NewPatientHtsForm.jsx +++ b/src/main/webapp/jsx/components/IctForm/NewPatientHtsForm.jsx @@ -87,6 +87,8 @@ const NewPatientHtsForm = ({ onValuesChange, onSubmitSuccess, onBack } = {}) => }; const { formik } = useNewPatientFormik(onSubmit); + console.log(formik) + // Real-time value forwarding for the orchestrator eligibility watcher React.useEffect(() => { onValuesChange?.(formik.values); diff --git a/src/main/webapp/jsx/components/NewToolForms/ExistingPatientHtsForm.jsx b/src/main/webapp/jsx/components/NewToolForms/ExistingPatientHtsForm.jsx index 4bf22c51..ef1ef0af 100644 --- a/src/main/webapp/jsx/components/NewToolForms/ExistingPatientHtsForm.jsx +++ b/src/main/webapp/jsx/components/NewToolForms/ExistingPatientHtsForm.jsx @@ -303,6 +303,9 @@ const ExistingPatientHtsForm = ({ fullRecord, initialValues, readOnly = false, b const { formik } = useExistingPatientFormik(onSubmit, formInitialValues); + console.log(formik) + + const { errors, submitCount } = formik; const hasSubmitted = submitCount > 0; diff --git a/src/main/webapp/jsx/components/NewToolForms/NewEncounterHtsForm.jsx b/src/main/webapp/jsx/components/NewToolForms/NewEncounterHtsForm.jsx index 69ef43b1..180d374b 100644 --- a/src/main/webapp/jsx/components/NewToolForms/NewEncounterHtsForm.jsx +++ b/src/main/webapp/jsx/components/NewToolForms/NewEncounterHtsForm.jsx @@ -231,6 +231,9 @@ const NewEncounterHtsForm = ({ person, backButtonAction, onValuesChange, onSubmi onSubmit, }); + console.log(formik) + + useEffect(() => { onValuesChange?.(formik.values); }, [formik.values]); // eslint-disable-line react-hooks/exhaustive-deps diff --git a/src/main/webapp/jsx/components/NewToolForms/NewPatientHtsForm.jsx b/src/main/webapp/jsx/components/NewToolForms/NewPatientHtsForm.jsx index 0fcbfa60..a3634e55 100644 --- a/src/main/webapp/jsx/components/NewToolForms/NewPatientHtsForm.jsx +++ b/src/main/webapp/jsx/components/NewToolForms/NewPatientHtsForm.jsx @@ -64,6 +64,8 @@ const NewPatientHtsForm = ({ onValuesChange, onSubmitSuccess, onBack } = {}) => const { formik } = useNewPatientFormik(onSubmit); + console.log(formik) + const onSubmit = async (values) => { const payload = buildHtsEncounterPayload(values, true); diff --git a/src/main/webapp/jsx/components/NewToolForms/sections/PostTestCounsellingSection.jsx b/src/main/webapp/jsx/components/NewToolForms/sections/PostTestCounsellingSection.jsx index 3e38f3c7..ac45d68a 100644 --- a/src/main/webapp/jsx/components/NewToolForms/sections/PostTestCounsellingSection.jsx +++ b/src/main/webapp/jsx/components/NewToolForms/sections/PostTestCounsellingSection.jsx @@ -53,22 +53,38 @@ const PostTestCounsellingSection = ({ formik, readOnly }) => { }); }, [allUsers]); - // "Designation" options: unique, non-null designations across all users + const designationOptions = useMemo(() => { if (!Array.isArray(allUsers)) return []; - const uniqueDesignations = [ - ...new Set( - allUsers - .map((user) => user?.designation) - .filter((designation) => !!designation) - ), - ]; - return uniqueDesignations.map((designation) => ({ - label: designation, - value: designation, - })); + + const toTitleCase = (str) => + str + .toLowerCase() + .split(/\s+/) + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(" "); + + const seen = new Map(); // normalized key -> display label + + allUsers.forEach((user) => { + const raw = user?.designation; + if (!raw || typeof raw !== "string") return; + + const trimmed = raw.trim(); + if (!trimmed) return; + + const key = trimmed.toLowerCase(); + if (!seen.has(key)) { + seen.set(key, toTitleCase(trimmed)); + } + }); + + return Array.from(seen.values()) + .sort() + .map((designation) => ({ label: designation, value: designation })); }, [allUsers]); + const fp = (name) => ({ name, value: values[name], From 0e37a5a81c5b990a2d2efcedb00b9123ceb86cc9 Mon Sep 17 00:00:00 2001 From: Emmanuel Chinonye Nnajiofor Date: Tue, 7 Jul 2026 10:55:01 +0100 Subject: [PATCH 12/21] solved the update issue --- src/main/webapp/api.js | 2 +- .../NewToolForms/hooks/validationSchema.js | 64 +++++++++---------- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/src/main/webapp/api.js b/src/main/webapp/api.js index b0bca0bf..12bcbe44 100644 --- a/src/main/webapp/api.js +++ b/src/main/webapp/api.js @@ -4,7 +4,7 @@ export const url = : "/api/v1/"; export const token = process.env.NODE_ENV === "development" - ? "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJyZGUtdXNlciIsImF1dGgiOiJTdXBlciBBZG1pbixVc2VyLFJERSIsImV4cCI6MTc4MzM1Mjk0MywibmFtZSI6InJkZS11c2VyIGxhc3RuYW1lIn0.GM_4hREpICZZ7YTt6XzAkhwa93zYk1wzT9K3W-OZJqDsOksmcNhrsJqnoXu-C0QHBhFYeP2wYrCnVTBGL1L98A" + ? "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJyZGUtdXNlciIsImF1dGgiOiJTdXBlciBBZG1pbixVc2VyLFJERSIsImV4cCI6MTc4MzQ0MTYyMiwibmFtZSI6InJkZS11c2VyIGxhc3RuYW1lIn0.d83Md48yYYqSWH4Tr6RzXUF3dkPbXjiCxhUgoSIv33XDe7nzuTBYqlgyjJoLwkhMQ6rhVhMbS6ZfNn84W3fGeQ" : new URLSearchParams(window.location.search).get("jwt"); export const wsUrl = diff --git a/src/main/webapp/jsx/components/NewToolForms/hooks/validationSchema.js b/src/main/webapp/jsx/components/NewToolForms/hooks/validationSchema.js index 5886823c..1bd2de63 100644 --- a/src/main/webapp/jsx/components/NewToolForms/hooks/validationSchema.js +++ b/src/main/webapp/jsx/components/NewToolForms/hooks/validationSchema.js @@ -241,7 +241,7 @@ export const buildValidationSchema = (isNewPatient) => { // } // ), - previouslyTestedNegative: yup.string(), + previouslyTestedNegative: yup.string().nullable(), timeOfLastNegativeTest: yup.mixed().test( "time-last-negative-conditional", @@ -260,11 +260,11 @@ export const buildValidationSchema = (isNewPatient) => { // return !!value || this.createError({ message: "This field is required" }); // } // ), - clientInformedTransmissionRoutes: yup.string(), - clientInformedRiskFactors: yup.string(), - clientInformedPreventionMethods: yup.string(), - clientInformedPossibleResults: yup.string(), - informedConsentGiven: yup.string(), + clientInformedTransmissionRoutes: yup.string().nullable(), + clientInformedRiskFactors: yup.string().nullable(), + clientInformedPreventionMethods: yup.string().nullable(), + clientInformedPossibleResults: yup.string().nullable(), + informedConsentGiven: yup.string().nullable(), // clientInformedRiskFactors: yup.mixed().test( // "risk-factors-conditional", // "This field is required", @@ -301,7 +301,7 @@ export const buildValidationSchema = (isNewPatient) => { // } // ), - everHadSexualIntercourse: yup.string(), + everHadSexualIntercourse: yup.string().nullable(), // everHadSexualIntercourse: yup.mixed().test( // "ever-sex-conditional", @@ -312,7 +312,7 @@ export const buildValidationSchema = (isNewPatient) => { // } // ), - moreThanOneSexPartner: yup.string(), + moreThanOneSexPartner: yup.string().nullable(), // moreThanOneSexPartner: yup.mixed().test( // "multi-partner-conditional", @@ -323,7 +323,7 @@ export const buildValidationSchema = (isNewPatient) => { // return !!value || this.createError({ message: "This field is required" }); // } // ), - unprotectedVaginalSex: yup.string(), + unprotectedVaginalSex: yup.string().nullable(), // unprotectedVaginalSex: yup.mixed().test( // "vaginal-sex-conditional", @@ -336,7 +336,7 @@ export const buildValidationSchema = (isNewPatient) => { // } // ), - unprotectedAnalSex:yup.string(), + unprotectedAnalSex:yup.string().nullable(), // unprotectedAnalSex: yup.mixed().test( // "anal-sex-conditional", @@ -357,8 +357,8 @@ export const buildValidationSchema = (isNewPatient) => { // } // ), - bloodTransfusionLast3Months: yup.string(), - sexUnderInfluence:yup.string(), + bloodTransfusionLast3Months: yup.string().nullable(), + sexUnderInfluence:yup.string().nullable(), // sexUnderInfluence: yup.mixed().test( // "sex-influence-conditional", @@ -369,7 +369,7 @@ export const buildValidationSchema = (isNewPatient) => { // return !!value || this.createError({ message: "This field is required" }); // } // ), - historyOfSTI: yup.string(), + historyOfSTI: yup.string().nullable(), // historyOfSTI: yup.mixed().test( // "sti-history-conditional", @@ -412,7 +412,7 @@ export const buildValidationSchema = (isNewPatient) => { return !!value || this.createError({ message: "This field is required" }); } ), - complaintsVaginalDischarge: yup.string(), + complaintsVaginalDischarge: yup.string().nullable(), // complaintsVaginalDischarge: yup.mixed().test( // "vaginal-discharge-conditional", @@ -423,7 +423,7 @@ export const buildValidationSchema = (isNewPatient) => { // return !!value || this.createError({ message: "This field is required" }); // } // ), - complaintsLowerAbdominalPain: yup.string(), + complaintsLowerAbdominalPain: yup.string().nullable(), // complaintsLowerAbdominalPain: yup.mixed().test( // "lower-abdominal-conditional", @@ -445,10 +445,10 @@ export const buildValidationSchema = (isNewPatient) => { // } // ), - complaintsUrethralDischarge: yup.string(), - complaintsScroralSwelling: yup.string(), - complaintsGenitalSores: yup.string(), - complaintsSwollenLymphNodes: yup.string(), + complaintsUrethralDischarge: yup.string().nullable(), + complaintsScroralSwelling: yup.string().nullable(), + complaintsGenitalSores: yup.string().nullable(), + complaintsSwollenLymphNodes: yup.string().nullable(), // complaintsScroralSwelling: yup.mixed().test( // "scrotal-swelling-conditional", // "This field is required", @@ -474,7 +474,7 @@ export const buildValidationSchema = (isNewPatient) => { // return !!value || this.createError({ message: "This field is required" }); // } // ), - partnerNewlyDiagnosed: yup.string(), + partnerNewlyDiagnosed: yup.string().nullable(), // partnerNewlyDiagnosed: yup.mixed().test( // "partner-new-dx-conditional", @@ -486,7 +486,7 @@ export const buildValidationSchema = (isNewPatient) => { // } // ), - partnerPregnantOnArv: yup.string(), + partnerPregnantOnArv: yup.string().nullable(), // partnerPregnantOnArv: yup.mixed().test( // "partner-pmtct-conditional", @@ -498,7 +498,7 @@ export const buildValidationSchema = (isNewPatient) => { // } // ), - adolescentHivPositive: yup.string(), + adolescentHivPositive: yup.string().nullable(), // adolescentHivPositive: yup.mixed().test( // "adolescent-hiv-conditional", @@ -523,8 +523,8 @@ export const buildValidationSchema = (isNewPatient) => { // return !!value || this.createError({ message: "This field is required" }); // } // ), - partnerNotRegularlyOnDrugs: yup.string(), - partnerRecentlyReturnedToTreatment: yup.string(), + partnerNotRegularlyOnDrugs: yup.string().nullable(), + partnerRecentlyReturnedToTreatment: yup.string().nullable(), // partnerRecentlyReturnedToTreatment: yup.mixed().test( // "partner-ltfu-conditional", @@ -536,7 +536,7 @@ export const buildValidationSchema = (isNewPatient) => { // } // ), - hadSexWithHivPositivePartnerInRiskGroup: yup.string(), + hadSexWithHivPositivePartnerInRiskGroup: yup.string().nullable(), // hadSexWithHivPositivePartnerInRiskGroup: yup.mixed().test( // "had-sex-with-hiv-conditional", @@ -612,8 +612,8 @@ export const buildValidationSchema = (isNewPatient) => { // previouslyTestedThisYear: yup.string().required("This field is required"), - previouslyTestedThisYear: yup.string(), - clientReceivedTestResult: yup.string(), + previouslyTestedThisYear: yup.string().nullable(), + clientReceivedTestResult: yup.string().nullable(), // clientReceivedTestResult: yup.string().required("This field is required"), hivTestKitsProvided: yup.string().required("This field is required"), @@ -661,15 +661,15 @@ export const buildValidationSchema = (isNewPatient) => { return !!value || this.createError({ message: "Accepted Index testing is required when confirmatory HIV test is positive" }); } ), - providedFpInfo: yup.string(), + providedFpInfo: yup.string().nullable(), // providedFpInfo: yup.string().required("This field is required"), - clientPartnerUseFpMethods: yup.string(), + clientPartnerUseFpMethods: yup.string().nullable(), // clientPartnerUseFpMethods: yup.string().required("This field is required"), - clientPartnerUseCondoms: yup.string(), + clientPartnerUseCondoms: yup.string().nullable(), // clientPartnerUseCondoms: yup.string().required("This field is required"), - correctCondomUseDemonstrated: yup.string(), + correctCondomUseDemonstrated: yup.string().nullable(), // correctCondomUseDemonstrated: yup.string().required("This field is required"), - condomsProvided: yup.string(), + condomsProvided: yup.string().nullable(), // condomsProvided: yup.string().required("This field is required"), clientReferredToOtherServices: yup.string().required("This field is required"), completedBy: yup.string().required("This field is required"), From e5639af1282baacc9d8fd35aa5c5ebdcd42ad8e9 Mon Sep 17 00:00:00 2001 From: Emmanuel Chinonye Nnajiofor Date: Wed, 8 Jul 2026 16:30:03 +0100 Subject: [PATCH 13/21] worked up to pre realease --- pom.xml | 2 +- .../controller/HtsEncounterController.java | 5 +- .../hts/domain/dto/HtsEncounterResponse.java | 3 +- .../hts/domain/entity/HtsEncounter.java | 8 +- .../HarmoniseNewHtsTablesInstaller.java | 2 +- .../repository/HtsEncounterRepository.java | 121 +++++++++--------- .../hts/service/HtsEncounterService.java | 14 +- .../hts/harmonise_new_hts_tables.xml | 18 +++ 8 files changed, 97 insertions(+), 76 deletions(-) diff --git a/pom.xml b/pom.xml index 60077138..2c0c9f8a 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ org.lamisplus.modules hts - 2.5.1 + 2.5.1.1 jar Hts diff --git a/src/main/java/org/lamisplus/modules/hts/controller/HtsEncounterController.java b/src/main/java/org/lamisplus/modules/hts/controller/HtsEncounterController.java index d9da6dc8..e16c528c 100644 --- a/src/main/java/org/lamisplus/modules/hts/controller/HtsEncounterController.java +++ b/src/main/java/org/lamisplus/modules/hts/controller/HtsEncounterController.java @@ -20,7 +20,6 @@ import javax.validation.Valid; import java.time.LocalDate; import java.util.List; -import java.util.UUID; @RestController @RequestMapping("/api/v1/hts-encounter") @@ -86,7 +85,9 @@ public ResponseEntity getPatientSummaries( @PreAuthorize("hasAnyAuthority('hts_view', 'hts_encounter_view')") public ResponseEntity getForProphylaxis( @PathVariable("screening-date") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate screeningDate, - @PathVariable("patient-uuid") UUID patientUuid) { + // patientUuid taken as a raw String (not java.util.UUID) so legacy/migrated + // patient_uuid values that aren't well-formed UUIDs don't 400 at binding time. + @PathVariable("patient-uuid") String patientUuid) { return ResponseEntity.ok(service.getForProphylaxis(screeningDate, patientUuid)); } diff --git a/src/main/java/org/lamisplus/modules/hts/domain/dto/HtsEncounterResponse.java b/src/main/java/org/lamisplus/modules/hts/domain/dto/HtsEncounterResponse.java index 20e3e124..caeb060f 100644 --- a/src/main/java/org/lamisplus/modules/hts/domain/dto/HtsEncounterResponse.java +++ b/src/main/java/org/lamisplus/modules/hts/domain/dto/HtsEncounterResponse.java @@ -5,14 +5,13 @@ import org.lamisplus.modules.patient.domain.dto.PersonResponseDto; import java.time.LocalDate; -import java.util.UUID; @Data public class HtsEncounterResponse { private Long id; private String uuid; private Long patientId; - private UUID patientUuid; + private String patientUuid; private PersonResponseDto person; private String clientCode; private LocalDate dateOfVisit; diff --git a/src/main/java/org/lamisplus/modules/hts/domain/entity/HtsEncounter.java b/src/main/java/org/lamisplus/modules/hts/domain/entity/HtsEncounter.java index 8beffa4f..1aff63b4 100644 --- a/src/main/java/org/lamisplus/modules/hts/domain/entity/HtsEncounter.java +++ b/src/main/java/org/lamisplus/modules/hts/domain/entity/HtsEncounter.java @@ -15,7 +15,6 @@ import javax.persistence.*; import java.io.Serializable; import java.time.LocalDate; -import java.util.UUID; @Entity @Table(name = "hts_encounter") @@ -36,8 +35,9 @@ public class HtsEncounter extends Audit implements Serializable { @JoinColumn(name = "patient_id", nullable = false) private Person person; - @Column(name = "patient_uuid", columnDefinition = "uuid") - private UUID patientUuid; + + @Column(name = "patient_uuid", columnDefinition = "varchar(50)", length = 50) + private String patientUuid; @Column(name = "client_code", nullable = false, length = 50) private String clientCode; @@ -71,4 +71,4 @@ public class HtsEncounter extends Audit implements Serializable { @Column(nullable = false) private Boolean archived = false; -} +} \ No newline at end of file diff --git a/src/main/java/org/lamisplus/modules/hts/installers/HarmoniseNewHtsTablesInstaller.java b/src/main/java/org/lamisplus/modules/hts/installers/HarmoniseNewHtsTablesInstaller.java index 8139b271..4536d949 100644 --- a/src/main/java/org/lamisplus/modules/hts/installers/HarmoniseNewHtsTablesInstaller.java +++ b/src/main/java/org/lamisplus/modules/hts/installers/HarmoniseNewHtsTablesInstaller.java @@ -7,7 +7,7 @@ @Order(23) @Installer(name = "harmonize-new-hts-tables-new", description = "Resolves divergent HTS tables", - version = 15) + version = 16) public class HarmoniseNewHtsTablesInstaller extends AcrossLiquibaseInstaller { public HarmoniseNewHtsTablesInstaller() { super("classpath:installers/hts/harmonise_new_hts_tables.xml"); diff --git a/src/main/java/org/lamisplus/modules/hts/repository/HtsEncounterRepository.java b/src/main/java/org/lamisplus/modules/hts/repository/HtsEncounterRepository.java index bc4dde74..4679319c 100644 --- a/src/main/java/org/lamisplus/modules/hts/repository/HtsEncounterRepository.java +++ b/src/main/java/org/lamisplus/modules/hts/repository/HtsEncounterRepository.java @@ -11,7 +11,6 @@ import java.time.LocalDate; import java.util.List; import java.util.Optional; -import java.util.UUID; public interface HtsEncounterRepository extends JpaRepository { @@ -20,7 +19,7 @@ public interface HtsEncounterRepository extends JpaRepository findByPerson_IdAndArchivedOrderByDateOfVisitDesc(Long patientId, Boolean archived); Optional findFirstByPatientUuidAndDateOfVisitAndArchivedOrderByIdDesc( - UUID patientUuid, LocalDate dateOfVisit, Boolean archived); + String patientUuid, LocalDate dateOfVisit, Boolean archived); @Query("SELECT e FROM HtsEncounter e " + "JOIN e.person p " + @@ -73,67 +72,26 @@ Page findPatientSummaries( String HTS_SUMMARY_CTES = "WITH hts_agg AS (\n" + - " SELECT DISTINCT ON (e.patient_uuid)\n" + - " e.id, e.uuid, e.patient_uuid, e.client_code, e.date_of_visit,\n" + - " e.setting, e.observation, e.facility_id,\n" + - " COUNT(*) OVER (PARTITION BY e.patient_uuid) AS hts_count\n" + - " FROM hts_encounter e\n" + - " WHERE e.archived = false\n" + - " AND e.facility_id = :facilityId\n" + - " ORDER BY e.patient_uuid, e.id DESC\n" + - "),\n" + - "ict_agg AS (\n" + - " SELECT patient_uuid, COUNT(*) AS ict_count\n" + - " FROM hts_ict_encounter\n" + - " WHERE archived = false\n" + - " AND facility_id = :facilityId\n" + - " GROUP BY patient_uuid\n" + - ")\n"; + " SELECT DISTINCT ON (e.patient_uuid)\n" + + " e.id, e.uuid, e.patient_uuid, e.client_code, e.date_of_visit,\n" + + " e.setting, e.observation, e.facility_id,\n" + + " COUNT(*) OVER (PARTITION BY e.patient_uuid) AS hts_count\n" + + " FROM hts_encounter e\n" + + " WHERE e.archived = false\n" + + " AND e.facility_id = :facilityId\n" + + " ORDER BY e.patient_uuid, e.id DESC\n" + + "),\n" + + "ict_agg AS (\n" + + " SELECT patient_uuid, COUNT(*) AS ict_count\n" + + " FROM hts_ict_encounter\n" + + " WHERE archived = false\n" + + " AND facility_id = :facilityId\n" + + " GROUP BY patient_uuid\n" + + ")\n"; String HTS_SUMMARY_WHERE = "WHERE p.archived = 0\n" + - " AND p.facility_id = :facilityId\n" + - " AND (:search IS NULL\n" + - " OR p.first_name ILIKE CAST(:search AS text)\n" + - " OR p.surname ILIKE CAST(:search AS text)\n" + - " OR p.other_name ILIKE CAST(:search AS text)\n" + - " OR p.hospital_number ILIKE CAST(:search AS text)\n" + - " OR EXISTS (\n" + - " SELECT 1\n" + - " FROM jsonb_array_elements(p.contact_point->'contactPoint') cp\n" + - " WHERE cp->>'value' ILIKE CAST(:search AS text)\n" + - " ))\n"; - - @Query(value = - HTS_SUMMARY_CTES + - "SELECT\n" + - " hts.id,\n" + - " CAST(hts.uuid AS text),\n" + - " p.id AS person_id,\n" + - " hts.client_code,\n" + - " hts.date_of_visit,\n" + - " hts.setting,\n" + - " CAST(hts.observation AS text),\n" + - " hts.facility_id,\n" + - " hts.hts_count,\n" + - " COALESCE(ict.ict_count, 0) AS ict_count\n" + - "FROM patient_person p\n" + - "INNER JOIN hts_agg hts ON hts.patient_uuid = p.uuid\n" + - "LEFT JOIN ict_agg ict ON ict.patient_uuid = p.uuid\n" + - HTS_SUMMARY_WHERE + - "ORDER BY hts.id DESC", - countQuery = - - "SELECT COUNT(*)\n" + - "FROM patient_person p\n" + - "WHERE p.archived = 0\n" + " AND p.facility_id = :facilityId\n" + - " AND EXISTS (\n" + - " SELECT 1 FROM hts_encounter e\n" + - " WHERE e.patient_uuid = p.uuid\n" + - " AND e.archived = false\n" + - " AND e.facility_id = :facilityId\n" + - " )\n" + " AND (:search IS NULL\n" + " OR p.first_name ILIKE CAST(:search AS text)\n" + " OR p.surname ILIKE CAST(:search AS text)\n" + @@ -143,7 +101,50 @@ Page findPatientSummaries( " SELECT 1\n" + " FROM jsonb_array_elements(p.contact_point->'contactPoint') cp\n" + " WHERE cp->>'value' ILIKE CAST(:search AS text)\n" + - " ))", + " ))\n"; + + @Query(value = + HTS_SUMMARY_CTES + + "SELECT\n" + + " hts.id,\n" + + " CAST(hts.uuid AS text),\n" + + " p.id AS person_id,\n" + + " hts.client_code,\n" + + " hts.date_of_visit,\n" + + " hts.setting,\n" + + " CAST(hts.observation AS text),\n" + + " hts.facility_id,\n" + + " hts.hts_count,\n" + + " COALESCE(ict.ict_count, 0) AS ict_count\n" + + "FROM patient_person p\n" + + // patient_uuid is now varchar (legacy-tolerant); p.uuid / ict.patient_uuid + // are still native uuid columns, so cast explicitly on both sides. + "INNER JOIN hts_agg hts ON hts.patient_uuid = CAST(p.uuid AS text)\n" + + "LEFT JOIN ict_agg ict ON CAST(ict.patient_uuid AS text) = CAST(p.uuid AS text)\n" + + HTS_SUMMARY_WHERE + + "ORDER BY hts.id DESC", + countQuery = + + "SELECT COUNT(*)\n" + + "FROM patient_person p\n" + + "WHERE p.archived = 0\n" + + " AND p.facility_id = :facilityId\n" + + " AND EXISTS (\n" + + " SELECT 1 FROM hts_encounter e\n" + + " WHERE e.patient_uuid = CAST(p.uuid AS text)\n" + + " AND e.archived = false\n" + + " AND e.facility_id = :facilityId\n" + + " )\n" + + " AND (:search IS NULL\n" + + " OR p.first_name ILIKE CAST(:search AS text)\n" + + " OR p.surname ILIKE CAST(:search AS text)\n" + + " OR p.other_name ILIKE CAST(:search AS text)\n" + + " OR p.hospital_number ILIKE CAST(:search AS text)\n" + + " OR EXISTS (\n" + + " SELECT 1\n" + + " FROM jsonb_array_elements(p.contact_point->'contactPoint') cp\n" + + " WHERE cp->>'value' ILIKE CAST(:search AS text)\n" + + " ))", nativeQuery = true) Page findHtsPatientSummariesOptimized( @Param("facilityId") Long facilityId, diff --git a/src/main/java/org/lamisplus/modules/hts/service/HtsEncounterService.java b/src/main/java/org/lamisplus/modules/hts/service/HtsEncounterService.java index e49fe9a1..d4656a0b 100644 --- a/src/main/java/org/lamisplus/modules/hts/service/HtsEncounterService.java +++ b/src/main/java/org/lamisplus/modules/hts/service/HtsEncounterService.java @@ -26,7 +26,6 @@ import java.math.BigInteger; import java.time.LocalDate; import java.util.List; -import java.util.UUID; import java.util.stream.Collectors; @Service @@ -108,7 +107,7 @@ public Page search(Long facilityId, String search, Pageabl return repository.search(facilityId, searchParam, pageable).map(this::toResponse); } - public HtsEncounterResponse getForProphylaxis(LocalDate screeningDate, UUID patientUuid) { + public HtsEncounterResponse getForProphylaxis(LocalDate screeningDate, String patientUuid) { HtsEncounter encounter = repository .findFirstByPatientUuidAndDateOfVisitAndArchivedOrderByIdDesc(patientUuid, screeningDate, false) .orElseThrow(() -> new EntityNotFoundException( @@ -308,10 +307,13 @@ private void putStr(ObjectNode node, String key, String value) { if (value != null) node.put(key, value); } - private UUID resolveUuid(Object uuid) { - if (uuid instanceof UUID) return (UUID) uuid; - if (uuid instanceof String) return UUID.fromString((String) uuid); - return null; + // Relaxed: patient_uuid is stored as varchar to tolerate legacy/migrated data + // that isn't a strictly well-formed UUID (e.g. "787-KXoSesiSLeE-787"). We no + // longer call UUID.fromString() here — doing so throws IllegalArgumentException + // and rejects the whole save/update for records we still need to support. + private String resolveUuid(Object uuid) { + if (uuid == null) return null; + return uuid.toString(); } private long toLong(Object val) { diff --git a/src/main/resources/installers/hts/harmonise_new_hts_tables.xml b/src/main/resources/installers/hts/harmonise_new_hts_tables.xml index 51418009..e29e93f8 100644 --- a/src/main/resources/installers/hts/harmonise_new_hts_tables.xml +++ b/src/main/resources/installers/hts/harmonise_new_hts_tables.xml @@ -928,4 +928,22 @@ + + + UPDATE hts_encounter he + SET + setting = r.entry_point, + observation = jsonb_set( + he.observation, + '{facilitySetting}', + to_jsonb(r.testing_setting::text), + true + ) + FROM hts_client h + JOIN hts_risk_stratification r + ON r.code = h.risk_stratification_code + WHERE he.uuid = h.uuid; + + + From 2a2b2843392b2f8e37c4ee4673372ad04dace56a Mon Sep 17 00:00:00 2001 From: Emmanuel Chinonye Nnajiofor Date: Wed, 8 Jul 2026 16:30:36 +0100 Subject: [PATCH 14/21] worked up to pre realease --- .../modules/hts/installers/HarmoniseNewHtsTablesInstaller.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/org/lamisplus/modules/hts/installers/HarmoniseNewHtsTablesInstaller.java b/src/main/java/org/lamisplus/modules/hts/installers/HarmoniseNewHtsTablesInstaller.java index 4536d949..8d48d896 100644 --- a/src/main/java/org/lamisplus/modules/hts/installers/HarmoniseNewHtsTablesInstaller.java +++ b/src/main/java/org/lamisplus/modules/hts/installers/HarmoniseNewHtsTablesInstaller.java @@ -1,5 +1,4 @@ package org.lamisplus.modules.hts.installers; - import com.foreach.across.core.annotations.Installer; import com.foreach.across.core.installers.AcrossLiquibaseInstaller; import org.springframework.core.annotation.Order; From 8bf2f46520a868e7a7e9648876a48066af03ff72 Mon Sep 17 00:00:00 2001 From: gamalieldashuaDataFi Date: Wed, 8 Jul 2026 17:00:23 +0100 Subject: [PATCH 15/21] Update package.json --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 2b8b0cf8..0e4fea4d 100644 --- a/package.json +++ b/package.json @@ -129,9 +129,9 @@ "yup": "^0.32.8" }, "scripts": { - "start": "react-scripts start", + "start": "set NODE_OPTIONS=--openssl-legacy-provider && react-scripts start", "jsreport": "jsreport-start", - "build": "react-scripts build && xcopy /s /y build src\\main\\resources\\views\\static\\hts", + "build": "set NODE_OPTIONS=--openssl-legacy-provider && react-scripts build && xcopy /s /y build src\\main\\resources\\views\\static\\hts", "serve": "serve -s build", "run-build": "react-scripts build && serve", "test": "react-scripts test --env=jsdom", From 9f2485445c90125bc5bf0c44fe2cd239f4c8bb7e Mon Sep 17 00:00:00 2001 From: Emmanuel Chinonye Nnajiofor Date: Thu, 9 Jul 2026 11:05:03 +0100 Subject: [PATCH 16/21] minor issue to relax special characters for serialNumber --- .../components/NewToolForms/hooks/validationSchema.js | 10 +++++----- .../NewToolForms/sections/BasicInformationSection.jsx | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/webapp/jsx/components/NewToolForms/hooks/validationSchema.js b/src/main/webapp/jsx/components/NewToolForms/hooks/validationSchema.js index 1bd2de63..88e0bbe7 100644 --- a/src/main/webapp/jsx/components/NewToolForms/hooks/validationSchema.js +++ b/src/main/webapp/jsx/components/NewToolForms/hooks/validationSchema.js @@ -173,11 +173,11 @@ export const buildValidationSchema = (isNewPatient) => { serialNumber: yup .string() - .required("Serial number is required to generate a client code") - .matches( - /^[a-zA-Z0-9]+$/, - "Serial number must contain only letters and numbers — no special characters" - ), + .required("Serial number is required to generate a client code"), + // .matches( + // /^[a-zA-Z0-9]+$/, + // "Serial number must contain only letters and numbers — no special characters" + // ), setting: yup.string().required("Setting is required"), diff --git a/src/main/webapp/jsx/components/NewToolForms/sections/BasicInformationSection.jsx b/src/main/webapp/jsx/components/NewToolForms/sections/BasicInformationSection.jsx index 7e663638..54a3273f 100644 --- a/src/main/webapp/jsx/components/NewToolForms/sections/BasicInformationSection.jsx +++ b/src/main/webapp/jsx/components/NewToolForms/sections/BasicInformationSection.jsx @@ -539,9 +539,9 @@ const BasicInformationSection = ({ formik, isExistingPatient, readOnly }) => { {touched.serialNumber && errors.serialNumber && ( {errors.serialNumber} )} - + {/* Letters and numbers only — no special characters - + */} )} From a19dd72ce792dbcf4b8509e9174aec9ab905ca81 Mon Sep 17 00:00:00 2001 From: Emmanuel Chinonye Nnajiofor Date: Thu, 9 Jul 2026 11:08:12 +0100 Subject: [PATCH 17/21] minor change on package.json --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 0e4fea4d..2b8b0cf8 100644 --- a/package.json +++ b/package.json @@ -129,9 +129,9 @@ "yup": "^0.32.8" }, "scripts": { - "start": "set NODE_OPTIONS=--openssl-legacy-provider && react-scripts start", + "start": "react-scripts start", "jsreport": "jsreport-start", - "build": "set NODE_OPTIONS=--openssl-legacy-provider && react-scripts build && xcopy /s /y build src\\main\\resources\\views\\static\\hts", + "build": "react-scripts build && xcopy /s /y build src\\main\\resources\\views\\static\\hts", "serve": "serve -s build", "run-build": "react-scripts build && serve", "test": "react-scripts test --env=jsdom", From b50a26ebe25de32dce8a951ca1dfa02f13aee796 Mon Sep 17 00:00:00 2001 From: Emmanuel Chinonye Nnajiofor Date: Tue, 14 Jul 2026 11:40:46 +0100 Subject: [PATCH 18/21] added search hts patients by client code --- .../HarmoniseNewHtsTablesInstaller.java | 2 +- .../repository/HtsEncounterRepository.java | 8 ++ .../hts/harmonise_new_hts_tables.xml | 74 +++++++++++++++++++ src/main/webapp/api.js | 2 +- 4 files changed, 84 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/lamisplus/modules/hts/installers/HarmoniseNewHtsTablesInstaller.java b/src/main/java/org/lamisplus/modules/hts/installers/HarmoniseNewHtsTablesInstaller.java index 8d48d896..862d4ac6 100644 --- a/src/main/java/org/lamisplus/modules/hts/installers/HarmoniseNewHtsTablesInstaller.java +++ b/src/main/java/org/lamisplus/modules/hts/installers/HarmoniseNewHtsTablesInstaller.java @@ -6,7 +6,7 @@ @Order(23) @Installer(name = "harmonize-new-hts-tables-new", description = "Resolves divergent HTS tables", - version = 16) + version = 17) public class HarmoniseNewHtsTablesInstaller extends AcrossLiquibaseInstaller { public HarmoniseNewHtsTablesInstaller() { super("classpath:installers/hts/harmonise_new_hts_tables.xml"); diff --git a/src/main/java/org/lamisplus/modules/hts/repository/HtsEncounterRepository.java b/src/main/java/org/lamisplus/modules/hts/repository/HtsEncounterRepository.java index 4679319c..ee328724 100644 --- a/src/main/java/org/lamisplus/modules/hts/repository/HtsEncounterRepository.java +++ b/src/main/java/org/lamisplus/modules/hts/repository/HtsEncounterRepository.java @@ -97,6 +97,7 @@ Page findPatientSummaries( " OR p.surname ILIKE CAST(:search AS text)\n" + " OR p.other_name ILIKE CAST(:search AS text)\n" + " OR p.hospital_number ILIKE CAST(:search AS text)\n" + + " OR hts.client_code ILIKE CAST(:search AS text)\n" + " OR EXISTS (\n" + " SELECT 1\n" + " FROM jsonb_array_elements(p.contact_point->'contactPoint') cp\n" + @@ -141,6 +142,13 @@ Page findPatientSummaries( " OR p.other_name ILIKE CAST(:search AS text)\n" + " OR p.hospital_number ILIKE CAST(:search AS text)\n" + " OR EXISTS (\n" + + " SELECT 1 FROM hts_encounter e2\n" + + " WHERE CAST(e2.patient_uuid AS text) = CAST(p.uuid AS text)\n" + + " AND e2.archived = false\n" + + " AND e2.facility_id = :facilityId\n" + + " AND e2.client_code ILIKE CAST(:search AS text)\n" + + " )\n" + + " OR EXISTS (\n" + " SELECT 1\n" + " FROM jsonb_array_elements(p.contact_point->'contactPoint') cp\n" + " WHERE cp->>'value' ILIKE CAST(:search AS text)\n" + diff --git a/src/main/resources/installers/hts/harmonise_new_hts_tables.xml b/src/main/resources/installers/hts/harmonise_new_hts_tables.xml index e29e93f8..0c7dff75 100644 --- a/src/main/resources/installers/hts/harmonise_new_hts_tables.xml +++ b/src/main/resources/installers/hts/harmonise_new_hts_tables.xml @@ -946,4 +946,78 @@ + + + Backfills / corrects the derived "finalHivTestResult" field inside the + hts_encounter.observation JSONB column, using the same logic as + DiagnosticTestingSection.jsx: + + Path A (typeOfHivTestDone = TYPE_OF_HIV_TEST_HIV_EARLY_DETECT): + - hivEarlyDetectResult is Antigen Reactive or Antigen+Antibody Reactive + -> "Suspected Acute Infection" + - hivEarlyDetectResult is Antibody Reactive + -> confirmatoryHivTest positive -> "Positive" + -> confirmatoryHivTest negative -> "Negative" + + Path B (typeOfHivTestDone = TYPE_OF_HIV_TEST_RAPID_ANTIBODY): + - initialHivTest = STI_HIV_RESULT_NEGATIVE -> "Negative" + - initialHivTest = STI_HIV_RESULT_POSITIVE + -> confirmatoryHivTest positive -> "Positive" + -> confirmatoryHivTest negative -> "Negative" + + Any record for which none of the above branches resolve to a value + (incomplete data, unrecognised typeOfHivTestDone, etc.) is left + untouched — we only ever write a value we can actually derive. + + + WITH computed AS ( + SELECT + id, + CASE + WHEN observation->>'typeOfHivTestDone' = 'TYPE_OF_HIV_TEST_HIV_EARLY_DETECT' THEN + CASE + WHEN observation->>'hivEarlyDetectResult' IN ( + 'HIV_EARLY_DETECT_RESULT_ANTIGEN_REACTIVE', + 'HIV_EARLY_DETECT_RESULT_ANTIGEN_+_ANTIBODY_REACTIVE' + ) THEN 'Suspected Acute Infection' + WHEN observation->>'hivEarlyDetectResult' = 'HIV_EARLY_DETECT_RESULT_ANTIBODY_REACTIVE' THEN + CASE + WHEN lower(observation->>'confirmatoryHivTest') = 'hiv_confirmatory_test_result_positive' THEN 'Positive' + WHEN lower(observation->>'confirmatoryHivTest') = 'hiv_confirmatory_test_result_negative' THEN 'Negative' + ELSE NULL + END + ELSE NULL + END + WHEN observation->>'typeOfHivTestDone' = 'TYPE_OF_HIV_TEST_RAPID_ANTIBODY' THEN + CASE + WHEN observation->>'initialHivTest' = 'STI_HIV_RESULT_NEGATIVE' THEN 'Negative' + WHEN observation->>'initialHivTest' = 'STI_HIV_RESULT_POSITIVE' THEN + CASE + WHEN lower(observation->>'confirmatoryHivTest') = 'hiv_confirmatory_test_result_positive' THEN 'Positive' + WHEN lower(observation->>'confirmatoryHivTest') = 'hiv_confirmatory_test_result_negative' THEN 'Negative' + ELSE NULL + END + ELSE NULL + END + ELSE NULL + END AS computed_result + FROM public.hts_encounter + WHERE observation IS NOT NULL + ) + UPDATE public.hts_encounter e + SET observation = jsonb_set(e.observation, '{finalHivTestResult}', to_jsonb(c.computed_result::text), true) + FROM computed c + WHERE e.id = c.id + AND c.computed_result IS NOT NULL + AND (e.observation->>'finalHivTestResult') IS DISTINCT FROM c.computed_result; + + + + Not reversible: the prior values of finalHivTestResult (blank or + incorrect) are not preserved anywhere, so there's nothing safe to + roll back to. If you need reversibility, snapshot the affected + rows into a temp table before running this in production. + + + diff --git a/src/main/webapp/api.js b/src/main/webapp/api.js index 12bcbe44..de3973ee 100644 --- a/src/main/webapp/api.js +++ b/src/main/webapp/api.js @@ -4,7 +4,7 @@ export const url = : "/api/v1/"; export const token = process.env.NODE_ENV === "development" - ? "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJyZGUtdXNlciIsImF1dGgiOiJTdXBlciBBZG1pbixVc2VyLFJERSIsImV4cCI6MTc4MzQ0MTYyMiwibmFtZSI6InJkZS11c2VyIGxhc3RuYW1lIn0.d83Md48yYYqSWH4Tr6RzXUF3dkPbXjiCxhUgoSIv33XDe7nzuTBYqlgyjJoLwkhMQ6rhVhMbS6ZfNn84W3fGeQ" + ? "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJyZGUtdXNlciIsImF1dGgiOiJTdXBlciBBZG1pbixVc2VyLFJERSIsImV4cCI6MTc4NDA0OTExMCwibmFtZSI6InJkZS11c2VyIGxhc3RuYW1lIn0.4dtsqr_Qc_dOhQBkKNoSO7BYPRWxQ7H6ykcA-NBf8xm40u_ytZxXC1RpTJmewjAqSr-Hu-0i3J8UpppAnrrPFg" : new URLSearchParams(window.location.search).get("jwt"); export const wsUrl = From e406e2efe2a09602b24f3bd9441c44f7d7b1e55e Mon Sep 17 00:00:00 2001 From: Emmanuel Chinonye Nnajiofor Date: Sun, 19 Jul 2026 19:20:36 +0100 Subject: [PATCH 19/21] adjusted changeset to the proper version reslving a bug --- pom.xml | 2 +- .../HarmoniseNewHtsTablesInstaller.java | 4 +- .../HtsEncounterSchemaInstaller.java | 4 +- .../IctEncounterSchemaInstaller.java | 4 +- .../installers/hts/encounter_schema.xml | 2 +- .../hts/harmonise_new_hts_tables.xml | 114 +++++++++--------- src/main/webapp/api.js | 2 +- 7 files changed, 66 insertions(+), 66 deletions(-) diff --git a/pom.xml b/pom.xml index 2c0c9f8a..60077138 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ org.lamisplus.modules hts - 2.5.1.1 + 2.5.1 jar Hts diff --git a/src/main/java/org/lamisplus/modules/hts/installers/HarmoniseNewHtsTablesInstaller.java b/src/main/java/org/lamisplus/modules/hts/installers/HarmoniseNewHtsTablesInstaller.java index 862d4ac6..ad440821 100644 --- a/src/main/java/org/lamisplus/modules/hts/installers/HarmoniseNewHtsTablesInstaller.java +++ b/src/main/java/org/lamisplus/modules/hts/installers/HarmoniseNewHtsTablesInstaller.java @@ -4,9 +4,9 @@ import org.springframework.core.annotation.Order; @Order(23) -@Installer(name = "harmonize-new-hts-tables-new", +@Installer(name = "harmonize-new-hts-tables-new-bug-resolved", description = "Resolves divergent HTS tables", - version = 17) + version = 18) public class HarmoniseNewHtsTablesInstaller extends AcrossLiquibaseInstaller { public HarmoniseNewHtsTablesInstaller() { super("classpath:installers/hts/harmonise_new_hts_tables.xml"); diff --git a/src/main/java/org/lamisplus/modules/hts/installers/HtsEncounterSchemaInstaller.java b/src/main/java/org/lamisplus/modules/hts/installers/HtsEncounterSchemaInstaller.java index ff2cd8a4..7a5b3d73 100644 --- a/src/main/java/org/lamisplus/modules/hts/installers/HtsEncounterSchemaInstaller.java +++ b/src/main/java/org/lamisplus/modules/hts/installers/HtsEncounterSchemaInstaller.java @@ -5,9 +5,9 @@ import org.springframework.core.annotation.Order; @Order(20) -@Installer(name = "hts-encounter-schema-installer-new-again", +@Installer(name = "hts-encounter-schema-installer-new-again-bug-resolved", description = "Creates the hts_encounter table — V3: boolean archived, patient_id, patient_uuid, facility_uuid, observation column", - version = 43) + version = 44) public class HtsEncounterSchemaInstaller extends AcrossLiquibaseInstaller { public HtsEncounterSchemaInstaller() { super("classpath:installers/hts/encounter_schema.xml"); diff --git a/src/main/java/org/lamisplus/modules/hts/installers/IctEncounterSchemaInstaller.java b/src/main/java/org/lamisplus/modules/hts/installers/IctEncounterSchemaInstaller.java index 206d5101..fb9f8c39 100644 --- a/src/main/java/org/lamisplus/modules/hts/installers/IctEncounterSchemaInstaller.java +++ b/src/main/java/org/lamisplus/modules/hts/installers/IctEncounterSchemaInstaller.java @@ -5,9 +5,9 @@ import org.springframework.core.annotation.Order; @Order(21) -@Installer(name = "ict-encounter-schema-installer-new-again", +@Installer(name = "ict-encounter-schema-installer-new-again-bug-resolved", description = "Creates ict_encounter and ict_contact tables — V3: boolean archived, patient_id, patient_uuid, facility_uuid, contact renames, removed age columns", - version = 43) + version = 44) public class IctEncounterSchemaInstaller extends AcrossLiquibaseInstaller { public IctEncounterSchemaInstaller() { super("classpath:installers/hts/ict_encounter_schema.xml"); diff --git a/src/main/resources/installers/hts/encounter_schema.xml b/src/main/resources/installers/hts/encounter_schema.xml index de88db34..f764ff2e 100644 --- a/src/main/resources/installers/hts/encounter_schema.xml +++ b/src/main/resources/installers/hts/encounter_schema.xml @@ -1,7 +1,7 @@ + xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.4.xsd"> diff --git a/src/main/resources/installers/hts/harmonise_new_hts_tables.xml b/src/main/resources/installers/hts/harmonise_new_hts_tables.xml index 0c7dff75..3140421d 100644 --- a/src/main/resources/installers/hts/harmonise_new_hts_tables.xml +++ b/src/main/resources/installers/hts/harmonise_new_hts_tables.xml @@ -1,8 +1,8 @@ + xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.4.xsd" +> @@ -285,7 +285,7 @@ ALTER TABLE hts_encounter - ALTER COLUMN client_code TYPE VARCHAR(200) USING client_code::VARCHAR(200), + ALTER COLUMN client_code TYPE VARCHAR(200) USING client_code::VARCHAR(200), ALTER COLUMN setting TYPE VARCHAR(200) USING setting::VARCHAR(200), ALTER COLUMN source TYPE VARCHAR(200) USING source::VARCHAR(200), ALTER COLUMN latitude TYPE VARCHAR(200) USING latitude::VARCHAR(200), @@ -295,13 +295,13 @@ ALTER TABLE hts_encounter - ALTER COLUMN client_code TYPE VARCHAR(50), - ALTER COLUMN setting TYPE VARCHAR(50), - ALTER COLUMN source TYPE VARCHAR(50), - ALTER COLUMN latitude TYPE VARCHAR(50), - ALTER COLUMN longitude TYPE VARCHAR(50), - ALTER COLUMN created_by TYPE VARCHAR(50), - ALTER COLUMN modified_by TYPE VARCHAR(50); + ALTER COLUMN client_code TYPE VARCHAR(50), + ALTER COLUMN setting TYPE VARCHAR(50), + ALTER COLUMN source TYPE VARCHAR(50), + ALTER COLUMN latitude TYPE VARCHAR(50), + ALTER COLUMN longitude TYPE VARCHAR(50), + ALTER COLUMN created_by TYPE VARCHAR(50), + ALTER COLUMN modified_by TYPE VARCHAR(50); @@ -313,15 +313,15 @@ ALTER TABLE hts_ict_encounter - ALTER COLUMN setting TYPE VARCHAR(200) USING setting::VARCHAR(200), + ALTER COLUMN setting TYPE VARCHAR(200) USING setting::VARCHAR(200), ALTER COLUMN created_by TYPE VARCHAR(200) USING created_by::VARCHAR(200), ALTER COLUMN modified_by TYPE VARCHAR(200) USING modified_by::VARCHAR(200); ALTER TABLE hts_ict_encounter - ALTER COLUMN setting TYPE VARCHAR(100), - ALTER COLUMN created_by TYPE VARCHAR(255), - ALTER COLUMN modified_by TYPE VARCHAR(255); + ALTER COLUMN setting TYPE VARCHAR(100), + ALTER COLUMN created_by TYPE VARCHAR(255), + ALTER COLUMN modified_by TYPE VARCHAR(255); @@ -332,13 +332,13 @@ ALTER TABLE hts_ict_contact - ALTER COLUMN created_by TYPE VARCHAR(200) USING created_by::VARCHAR(200), + ALTER COLUMN created_by TYPE VARCHAR(200) USING created_by::VARCHAR(200), ALTER COLUMN modified_by TYPE VARCHAR(200) USING modified_by::VARCHAR(200); ALTER TABLE hts_ict_contact - ALTER COLUMN created_by TYPE VARCHAR(255), - ALTER COLUMN modified_by TYPE VARCHAR(255); + ALTER COLUMN created_by TYPE VARCHAR(255), + ALTER COLUMN modified_by TYPE VARCHAR(255); @@ -398,7 +398,7 @@ -- Retype the column ALTER TABLE hts_encounter - ALTER COLUMN uuid TYPE VARCHAR(50) USING uuid::text; + ALTER COLUMN uuid TYPE VARCHAR(50) USING uuid::text; -- Restore the default and unique constraint ALTER TABLE hts_encounter @@ -420,11 +420,11 @@ Convert hts_encounter.patient_uuid from UUID to VARCHAR(50). ALTER TABLE hts_encounter - ALTER COLUMN patient_uuid TYPE VARCHAR(50) USING patient_uuid::text; + ALTER COLUMN patient_uuid TYPE VARCHAR(50) USING patient_uuid::text; ALTER TABLE hts_encounter - ALTER COLUMN patient_uuid TYPE UUID USING patient_uuid::uuid; + ALTER COLUMN patient_uuid TYPE UUID USING patient_uuid::uuid; @@ -435,7 +435,7 @@ ALTER TABLE hts_ict_encounter DROP CONSTRAINT IF EXISTS hts_ict_encounter_uuid_key; ALTER TABLE hts_ict_encounter - ALTER COLUMN uuid TYPE VARCHAR(50) USING uuid::text; + ALTER COLUMN uuid TYPE VARCHAR(50) USING uuid::text; ALTER TABLE hts_ict_encounter ALTER COLUMN uuid SET DEFAULT gen_random_uuid()::text; @@ -456,11 +456,11 @@ Convert hts_ict_encounter.patient_uuid from UUID to VARCHAR(50). ALTER TABLE hts_ict_encounter - ALTER COLUMN patient_uuid TYPE VARCHAR(50) USING patient_uuid::text; + ALTER COLUMN patient_uuid TYPE VARCHAR(50) USING patient_uuid::text; ALTER TABLE hts_ict_encounter - ALTER COLUMN patient_uuid TYPE UUID USING patient_uuid::uuid; + ALTER COLUMN patient_uuid TYPE UUID USING patient_uuid::uuid; @@ -471,7 +471,7 @@ ALTER TABLE hts_ict_contact DROP CONSTRAINT IF EXISTS hts_ict_contact_uuid_key; ALTER TABLE hts_ict_contact - ALTER COLUMN uuid TYPE VARCHAR(50) USING uuid::text; + ALTER COLUMN uuid TYPE VARCHAR(50) USING uuid::text; ALTER TABLE hts_ict_contact ALTER COLUMN uuid SET DEFAULT gen_random_uuid()::text; @@ -521,12 +521,12 @@ ALTER TABLE hts_ict_encounter - DROP CONSTRAINT IF EXISTS fk_ict_encounter_hts; + DROP CONSTRAINT IF EXISTS fk_ict_encounter_hts; ALTER TABLE hts_ict_encounter - ADD CONSTRAINT fk_ict_encounter_hts - FOREIGN KEY (hts_encounter_id) REFERENCES hts_encounter(id); + ADD CONSTRAINT fk_ict_encounter_hts + FOREIGN KEY (hts_encounter_id) REFERENCES hts_encounter(id); @@ -542,7 +542,7 @@ ALTER TABLE hts_ict_encounter - DROP CONSTRAINT IF EXISTS fk_ict_encounter_hts_uuid; + DROP CONSTRAINT IF EXISTS fk_ict_encounter_hts_uuid; @@ -583,12 +583,12 @@ ALTER TABLE hts_ict_contact - DROP CONSTRAINT IF EXISTS fk_ict_contact_encounter; + DROP CONSTRAINT IF EXISTS fk_ict_contact_encounter; ALTER TABLE hts_ict_contact - ADD CONSTRAINT fk_ict_contact_encounter - FOREIGN KEY (ict_encounter_id) REFERENCES hts_ict_encounter(id); + ADD CONSTRAINT fk_ict_contact_encounter + FOREIGN KEY (ict_encounter_id) REFERENCES hts_ict_encounter(id); @@ -604,7 +604,7 @@ ALTER TABLE hts_ict_contact - DROP CONSTRAINT IF EXISTS fk_ict_contact_encounter_uuid; + DROP CONSTRAINT IF EXISTS fk_ict_contact_encounter_uuid; @@ -630,26 +630,26 @@ ═══════════════════════════════════════════════════════════════════════ --> - - - Convert pmtct_hts from jsonb to boolean where it may still be jsonb. - - - ALTER TABLE hts_encounter - ALTER COLUMN pmtct_hts TYPE boolean + + + Convert pmtct_hts from jsonb to boolean where it may still be jsonb. + + + ALTER TABLE hts_encounter + ALTER COLUMN pmtct_hts TYPE boolean USING COALESCE((pmtct_hts::text)::boolean, false); - ALTER TABLE hts_encounter - ALTER COLUMN pmtct_hts SET DEFAULT false; + ALTER TABLE hts_encounter + ALTER COLUMN pmtct_hts SET DEFAULT false; - ALTER TABLE hts_encounter - ALTER COLUMN pmtct_hts SET NOT NULL; - - - ALTER TABLE hts_encounter - ALTER COLUMN pmtct_hts TYPE jsonb USING to_jsonb(pmtct_hts); - - + ALTER TABLE hts_encounter + ALTER COLUMN pmtct_hts SET NOT NULL; + + + ALTER TABLE hts_encounter + ALTER COLUMN pmtct_hts TYPE jsonb USING to_jsonb(pmtct_hts); + + @@ -934,14 +934,14 @@ SET setting = r.entry_point, observation = jsonb_set( - he.observation, - '{facilitySetting}', - to_jsonb(r.testing_setting::text), - true - ) - FROM hts_client h + he.observation, + '{facilitySetting}', + to_jsonb(r.testing_setting::text), + true + ) + FROM hts_client h JOIN hts_risk_stratification r - ON r.code = h.risk_stratification_code + ON r.code = h.risk_stratification_code WHERE he.uuid = h.uuid; @@ -1020,4 +1020,4 @@ - + \ No newline at end of file diff --git a/src/main/webapp/api.js b/src/main/webapp/api.js index de3973ee..7bee1d59 100644 --- a/src/main/webapp/api.js +++ b/src/main/webapp/api.js @@ -4,7 +4,7 @@ export const url = : "/api/v1/"; export const token = process.env.NODE_ENV === "development" - ? "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJyZGUtdXNlciIsImF1dGgiOiJTdXBlciBBZG1pbixVc2VyLFJERSIsImV4cCI6MTc4NDA0OTExMCwibmFtZSI6InJkZS11c2VyIGxhc3RuYW1lIn0.4dtsqr_Qc_dOhQBkKNoSO7BYPRWxQ7H6ykcA-NBf8xm40u_ytZxXC1RpTJmewjAqSr-Hu-0i3J8UpppAnrrPFg" + ? "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJyZGUtdXNlciIsImF1dGgiOiJTdXBlciBBZG1pbixVc2VyLFJERSIsImV4cCI6MTc4NDUxMzM4MywibmFtZSI6InJkZS11c2VyIGxhc3RuYW1lIn0.mLGWEKGTmAVuxuWCw9dzx4ajFZbzLhwRSsG7Br7QDQ5HgJmVzzIDWtt6odLRHz_rgXrowEPZMO4EbqZA3U1f0Q" : new URLSearchParams(window.location.search).get("jwt"); export const wsUrl = From dca29cc1f7186c539c288986e5cf2f3c5d47c7cb Mon Sep 17 00:00:00 2001 From: Emmanuel Chinonye Nnajiofor Date: Wed, 22 Jul 2026 15:07:27 +0100 Subject: [PATCH 20/21] made minor changes to resolve a changeset issue --- .../lamisplus/modules/hts/installers/Updates.java | 2 +- src/main/resources/installers/hts/updates.xml | 4 +++- src/main/webapp/jsx/components/IctForm/IctForm.jsx | 12 ++++++------ 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/lamisplus/modules/hts/installers/Updates.java b/src/main/java/org/lamisplus/modules/hts/installers/Updates.java index 2620163e..4c9fe898 100644 --- a/src/main/java/org/lamisplus/modules/hts/installers/Updates.java +++ b/src/main/java/org/lamisplus/modules/hts/installers/Updates.java @@ -6,7 +6,7 @@ @Order(3) @Installer(name = "schema-installer-update", description = "Updates the required database tables data", - version = 16) + version = 17) public class Updates extends AcrossLiquibaseInstaller { public Updates() { super("classpath:installers/hts/updates.xml"); diff --git a/src/main/resources/installers/hts/updates.xml b/src/main/resources/installers/hts/updates.xml index 550edb84..089a1a21 100644 --- a/src/main/resources/installers/hts/updates.xml +++ b/src/main/resources/installers/hts/updates.xml @@ -91,10 +91,12 @@ + + + ALTER TABLE hts_risk_stratification ADD COLUMN spoke_facility varchar(255); - diff --git a/src/main/webapp/jsx/components/IctForm/IctForm.jsx b/src/main/webapp/jsx/components/IctForm/IctForm.jsx index 6593b2c8..fc386390 100644 --- a/src/main/webapp/jsx/components/IctForm/IctForm.jsx +++ b/src/main/webapp/jsx/components/IctForm/IctForm.jsx @@ -111,7 +111,7 @@ const mapIctResponseToFormValues = (response) => { indexFirstName: d.indexFirstName ?? "", indexMiddleName: d.indexMiddleName ?? "", indexSurname: d.indexSurname ?? "", - indexSex: d.indexSex ?? "", // codeset code — display resolved in Section A + indexSex: d.indexSex ?? "", // codeset code - display resolved in Section A indexDob: d.indexDob ?? "", indexAge: d.indexAge != null ? String(d.indexAge) : "", indexPhone: d.indexPhone ?? "", @@ -289,9 +289,9 @@ const IctForm = ({

{readOnly - ? "Viewing ICT record — no changes can be made" + ? "Viewing ICT record - no changes can be made" : isEditMode - ? "Editing existing ICT record — all fields are editable" + ? "Editing existing ICT record - all fields are editable" : "Complete Section A, then add all elicited contacts in Section B"}

@@ -309,7 +309,7 @@ const IctForm = ({ Date: Thu, 23 Jul 2026 08:18:44 +0100 Subject: [PATCH 21/21] finally slved the issue --- .../modules/hts/installers/Updates.java | 2 +- src/main/resources/installers/hts/updates.xml | 78 +++++++++++-------- src/main/webapp/api.js | 2 +- 3 files changed, 47 insertions(+), 35 deletions(-) diff --git a/src/main/java/org/lamisplus/modules/hts/installers/Updates.java b/src/main/java/org/lamisplus/modules/hts/installers/Updates.java index 4c9fe898..db7cd425 100644 --- a/src/main/java/org/lamisplus/modules/hts/installers/Updates.java +++ b/src/main/java/org/lamisplus/modules/hts/installers/Updates.java @@ -6,7 +6,7 @@ @Order(3) @Installer(name = "schema-installer-update", description = "Updates the required database tables data", - version = 17) + version = 18) public class Updates extends AcrossLiquibaseInstaller { public Updates() { super("classpath:installers/hts/updates.xml"); diff --git a/src/main/resources/installers/hts/updates.xml b/src/main/resources/installers/hts/updates.xml index 089a1a21..695d3535 100644 --- a/src/main/resources/installers/hts/updates.xml +++ b/src/main/resources/installers/hts/updates.xml @@ -31,31 +31,31 @@ CREATE TABLE hivst ( - id SERIAL PRIMARY KEY, - patient_id BIGINT, - date_of_visit DATE, - service_delivery_point VARCHAR(255), - user_type VARCHAR(255), - serial_number VARCHAR(255), - client_code VARCHAR(255), - previously_tested_within_12_months VARCHAR(255), - result_of_previously_tested_within_12_months VARCHAR(255), - consent_for_follow_up_calls VARCHAR(255), - type_of_hivst_kit_received VARCHAR(255), - number_of_hivst_kits_received BIGINT, - name_of_test_kit VARCHAR(255), - lot_number VARCHAR(255), - expiry_date DATE, - test_kit_users JSONB, - other_test_kit_user_info_available VARCHAR(255), - other_test_kit_user_details JSONB, - part_b JSONB, - referral_information JSONB, - archived INTEGER DEFAULT 0, - created_by VARCHAR(255) NOT NULL, - date_created TIMESTAMP NOT NULL, - modified_by VARCHAR(255), - date_modified TIMESTAMP + id SERIAL PRIMARY KEY, + patient_id BIGINT, + date_of_visit DATE, + service_delivery_point VARCHAR(255), + user_type VARCHAR(255), + serial_number VARCHAR(255), + client_code VARCHAR(255), + previously_tested_within_12_months VARCHAR(255), + result_of_previously_tested_within_12_months VARCHAR(255), + consent_for_follow_up_calls VARCHAR(255), + type_of_hivst_kit_received VARCHAR(255), + number_of_hivst_kits_received BIGINT, + name_of_test_kit VARCHAR(255), + lot_number VARCHAR(255), + expiry_date DATE, + test_kit_users JSONB, + other_test_kit_user_info_available VARCHAR(255), + other_test_kit_user_details JSONB, + part_b JSONB, + referral_information JSONB, + archived INTEGER DEFAULT 0, + created_by VARCHAR(255) NOT NULL, + date_created TIMESTAMP NOT NULL, + modified_by VARCHAR(255), + date_modified TIMESTAMP ); @@ -80,6 +80,7 @@ + @@ -97,12 +98,16 @@ ALTER TABLE hts_risk_stratification ADD COLUMN spoke_facility varchar(255); + + + + ALTER TABLE hts_risk_stratification ADD COLUMN health_facility varchar(255); @@ -133,6 +138,7 @@ + @@ -154,7 +160,7 @@ ALTER TABLE hts_family_index_testing_tracker - ALTER COLUMN attempts SET DATA TYPE character varying USING attempts::text; + ALTER COLUMN attempts SET DATA TYPE character varying USING attempts::text; @@ -163,16 +169,18 @@ ALTER TABLE hts_family_index_testing_tracker - RENAME COLUMN attempts TO attempt; + RENAME COLUMN attempts TO attempt; - + + + UPDATE hts_risk_stratification SET entry_point = 'HTS_ENTRY_POINT_FACILITY' - where entry_point = 'Facility'; + where entry_point = 'Facility'; UPDATE hts_risk_stratification SET entry_point = 'HTS_ENTRY_POINT_COMMUNITY' where entry_point = 'Community'; @@ -180,7 +188,9 @@ - + + + UPDATE hts_risk_stratification SET testing_setting = 'COMMUNITY_HTS_TEST_SETTING_DELIVERY_HOMES' WHERE testing_setting = 'COMMUNITY_HTS_TEST_SETTING_DELIVERY_HOMES'; UPDATE hts_risk_stratification SET testing_setting = 'FACILITY_HTS_TEST_SETTING_CT' WHERE testing_setting = 'TEST_SETTING_CT'; @@ -237,7 +247,9 @@ - + + + UPDATE hts_client SET testing_setting = 'COMMUNITY_HTS_TEST_SETTING_DELIVERY_HOMES' WHERE testing_setting = 'COMMUNITY_HTS_TEST_SETTING_DELIVERY_HOMES'; UPDATE hts_client SET testing_setting = 'FACILITY_HTS_TEST_SETTING_CT' WHERE testing_setting = 'TEST_SETTING_CT'; @@ -354,9 +366,9 @@ BEGIN IF to_regclass('public.pmtct_infant_information') IS NULL THEN RETURN; - END IF; + END IF; - RETURN QUERY EXECUTE ' + RETURN QUERY EXECUTE ' SELECT infant_hospital_number FROM public.pmtct_infant_information WHERE infant_hospital_number IS NOT NULL diff --git a/src/main/webapp/api.js b/src/main/webapp/api.js index 7bee1d59..d9c9aa9d 100644 --- a/src/main/webapp/api.js +++ b/src/main/webapp/api.js @@ -4,7 +4,7 @@ export const url = : "/api/v1/"; export const token = process.env.NODE_ENV === "development" - ? "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJyZGUtdXNlciIsImF1dGgiOiJTdXBlciBBZG1pbixVc2VyLFJERSIsImV4cCI6MTc4NDUxMzM4MywibmFtZSI6InJkZS11c2VyIGxhc3RuYW1lIn0.mLGWEKGTmAVuxuWCw9dzx4ajFZbzLhwRSsG7Br7QDQ5HgJmVzzIDWtt6odLRHz_rgXrowEPZMO4EbqZA3U1f0Q" + ? "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJyZGUtdXNlciIsImF1dGgiOiJTdXBlciBBZG1pbixVc2VyLFJERSIsImV4cCI6MTc4NDgxOTgwMywibmFtZSI6InJkZS11c2VyIGxhc3RuYW1lIn0.EQvz3OVEzcWLyjtgpYWca5no5qSP7amxZZLxC_0lAzWjTswwsgF5x_yZ8KbNM-3dsAm8oRyhxzp5t05LDCRVVg" : new URLSearchParams(window.location.search).get("jwt"); export const wsUrl =