Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ val appDependencies = Seq(
"com.h2database" % "h2" % "1.4.193",
"com.jcraft" % "jsch" % "0.1.54",
"ca.uhn.hapi.fhir" % "hapi-fhir-base" % "5.7.0",
"ca.uhn.hapi.fhir" % "hapi-fhir-structures-r5" % "5.7.0"
"ca.uhn.hapi.fhir" % "hapi-fhir-structures-r5" % "5.7.0",
"org.apache.commons" % "commons-csv" % "1.10.0"
)


Expand Down
9 changes: 5 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,12 @@ RUN --mount=type=cache,target=/root/.ivy2 \
WORKDIR $PROJECT_HOME/app/target/universal
RUN unzip femr-*.zip && rm femr-*.zip

FROM openjdk:8-jre-alpine
FROM eclipse-temurin:8-jre-alpine-3.23

RUN apk update
RUN apk add --no-cache bash python3 py3-pip gcc python3-dev musl-dev linux-headers mariadb-client mariadb-connector-c mariadb-connector-c-dev
RUN pip3 install psutil
#RUN apk add --no-cache bash python3 py3-pip gcc python3-dev musl -dev linux-headers mariadb-client mariadb-connector-c mariadb-connector-c-dev
#RUN pip3 install psutil
RUN apk add --no-cache bash python3 py3-pip py3-psutil gcc python3-dev musl-dev linux-headers mariadb-client mariadb-connector-c mariadb-connector-c-dev

#database variables
ARG APP_VERSION
Expand All @@ -86,4 +87,4 @@ EXPOSE 9000

# run fEMR using env variables
#ENTRYPOINT url=$DB_URL usr=$DB_USER pass=$DB_PASS sbt ~run
ENTRYPOINT ["/bin/bash", "-c", "/opt/bin/femr/bin/femr"]
ENTRYPOINT ["/bin/bash", "-c", "rm -f /opt/bin/femr/RUNNING_PID; exec /opt/bin/femr/bin/femr -Dpidfile.path=/dev/null"]
6 changes: 3 additions & 3 deletions app/femr/business/helpers/LogicDoer.java
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,9 @@ public static String getCsvFilePath() {
path += File.separator;
return path;
} catch (Exception ex) {
//If config doesn't exist, default to "photos"
path = "../Upload/Csv";
return path;
//If config doesn't exist, default to "../Upload/CSV"
return ".." + File.separator + "Upload" + File.separator + "CSV" + File.separator;

}

}
Expand Down
357 changes: 335 additions & 22 deletions app/femr/business/services/system/ResearchService.java

Large diffs are not rendered by default.

16 changes: 2 additions & 14 deletions app/femr/common/models/ResearchExportItem.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,122 +32,110 @@ public class ResearchExportItem {
private Integer weeksPregnant;
private String dayOfVisit;
private Integer tripId;

private List<String> chiefComplaints;
private List<String> prescribedMedications;
private List<String> dispensedMedications;
private Map<String, Float> vitalMap;
private Map<String, String> tabFieldMap;

private String trip_team;
private String trip_country;

public UUID getPatientId() {
return patientId;
}

public void setPatientId(UUID patientId) {
this.patientId = patientId;
}

public String getGender() {
return gender;
}

public void setGender(String gender) {
this.gender = gender;
}

public Integer getAge() {
return age;
}

public void setAge(Integer age) {
this.age = age;
}

public Boolean getIsPregnant() {
return isPregnant;
}

public void setIsPregnant(Boolean isPregnant) {
this.isPregnant = isPregnant;
}

public Integer getWeeksPregnant() {
return weeksPregnant;
}

public void setWeeksPregnant(Integer weeksPregnant) {
this.weeksPregnant = weeksPregnant;
}

public List<String> getChiefComplaints() {
return chiefComplaints;
}

public void setChiefComplaints(List<String> chiefComplaints) {
this.chiefComplaints = chiefComplaints;
}

public List<String> getPrescribedMedications() {
return prescribedMedications;
}

public void setPrescribedMedications(List<String> prescribedMedications) {
this.prescribedMedications = prescribedMedications;
}

public List<String> getDispensedMedications() {
return dispensedMedications;
}

public void setDispensedMedications(List<String> dispensedMedications) {
this.dispensedMedications = dispensedMedications;
}

public Map<String, Float> getVitalMap() {
return vitalMap;
}

public void setVitalMap(Map<String, Float> vitalMap) {
this.vitalMap = vitalMap;
}

public Map<String, String> getTabFieldMap() {
return tabFieldMap;
}

public void setTabFieldMap(Map<String, String> tabFieldMap) {
this.tabFieldMap = tabFieldMap;
}

public String getDayOfVisit() {
return dayOfVisit;
}

public void setDayOfVisit(String day) {
this.dayOfVisit = day;
}

public Integer getTripId() {
return tripId;
}

public void setTripId(Integer tripId) {
this.tripId = tripId;
}

public String getTrip_team() {
return trip_team;
}

public void setTrip_team(String trip_team) {
this.trip_team = trip_team;
}

public String getTrip_country() {
return trip_country;
}

public void setTrip_country(String trip_country) {
this.trip_country = trip_country;
}
Expand Down
6 changes: 5 additions & 1 deletion app/femr/ui/controllers/ResearchController.java
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,11 @@ public Result indexGet() {
filterViewModel.setMissionTrips(missionItemServiceResponse.getResponseObject());

// Set Default Start (30 Days Ago) and End Date (Today)
// temporarily "-365 * 10" modified for development to default to last ten years
Calendar today = Calendar.getInstance();
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
filterViewModel.setEndDate(dateFormat.format(today.getTime()));
today.add(Calendar.DAY_OF_MONTH, -120);
today.add(Calendar.DAY_OF_MONTH, -365 * 10);
filterViewModel.setStartDate(dateFormat.format(today.getTime()));


Expand Down Expand Up @@ -130,6 +131,9 @@ public Result exportPost() {
// This does weird stuff and isn't reliable.
//ServiceResponse<File> exportServiceResponse = researchService.retrieveCsvExportFile(filterItem);
ServiceResponse<File> exportServiceResponse = researchService.exportPatientsByTrip(filterItem.getMissionTripId());
if (exportServiceResponse == null) {
return internalServerError("Export Failed");
}

File csvFile = exportServiceResponse.getResponseObject();

Expand Down
61 changes: 58 additions & 3 deletions app/femr/util/stringhelpers/StringUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Map;

/**
* This class contains utilities for manipulating strings. If you add something here, please clearly document the
Expand Down Expand Up @@ -240,14 +242,67 @@ public static String outputBloodPressureOrNA(String systolic, String diastolic)
* @return The user friendly trip title or null if parameters were null
*/
public static String generateMissionTripTitle(String teamName, String country, Date startDate, Date endDate){

if (StringUtils.isNullOrWhiteSpace(teamName) || StringUtils.isNullOrWhiteSpace(country) || startDate == null || endDate == null){

return null;
}

String tripTitle = teamName + "-" + country + "-(" + dateUtils.getFriendlyInternationalDate(startDate) + "-" + dateUtils.getFriendlyInternationalDate(endDate) + ")";
return tripTitle;
}

/**
* Converts a list of strings into a single semicolon-separated value.
* Example:
* ["headache", "nausea"] → "headache; nausea"
* @param list the list of strings to join; may be null or empty
* @return a semicolon-separated string, or an empty string if the list is null or empty
*/
public static String joinList(List<String> list) {
if (list == null || list.isEmpty()) return "";
return String.join("; ", list);
}

/**
* Converts a map of string keys to float values into a flattened
* semicolon-separated representation suitable for a single CSV column.
* Format:
* { "bp": 120.0, "hr": 80.0 }
* → "bp=120.0; hr=80.0"
* @param map the map of string keys to float values; may be null or empty
* @return a semicolon-delimited {@code key=value} string, or empty string for null/empty input
*/
public static String joinFloatMap(Map<String, Float> map) {
if (map == null || map.isEmpty()) return "";
StringBuilder sb = new StringBuilder();
boolean first = true;
for (Map.Entry<String, Float> e : map.entrySet()) {
if (!first) sb.append("; ");
sb.append(e.getKey()).append("=").append(e.getValue());
first = false;
}
return sb.toString();
}

/**
* Converts a map of string keys to string values into a flattened
* semicolon-separated representation for a single CSV cell.
* Format:
* { "field1": "value1", "field2": "value2" }
* → "field1=value1; field2=value2"
* @param map the map of string keys to string values; may be null or empty
* @return a semicolon-delimited {@code key=value} string, or empty string for null/empty input
*/
public static String joinStringMap(Map<String, String> map) {
if (map == null || map.isEmpty()) return "";
StringBuilder sb = new StringBuilder();
boolean first = true;
for (Map.Entry<String, String> e : map.entrySet()) {
if (!first) sb.append("; ");
sb.append(e.getKey()).append("=").append(e.getValue());
first = false;
}
return sb.toString();
}

}


54 changes: 52 additions & 2 deletions public/js/research/filter-menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -157,10 +157,60 @@ var filterMenuModule = (function () {
return false;
};

var exportData = function () {
var exportData = async function (e) {
if (e) {
e.preventDefault();
e.stopPropagation();
}

const btn = document.getElementById("export-button");
btn.disabled = true;
btn.value = "Exporting…";

// form is often a jQuery object in this codebase
var formEl = form && form.jquery ? form[0] : form;
if (!formEl) {
alert("Export failed (form not found).");
btn.disabled = false;
btn.value = "Export Data";
return false;
}

try {
const resp = await fetch(formEl.action, {
method: "POST",
body: new FormData(formEl),
credentials: "same-origin"
});

if (!resp.ok) {
alert("Export failed (" + resp.status + ")");
btn.disabled = false;
btn.value = "Export Data";
return false;
}

const blob = await resp.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;

const cd = resp.headers.get("content-disposition") || "";
const match = cd.match(/filename="?([^"]+)"?/i);
a.download = match ? match[1] : "research_export.csv";

document.body.appendChild(a);
a.click();
a.remove();
window.URL.revokeObjectURL(url);

$(form).submit();
alert("Research export completed!");
} catch (err) {
alert("Export failed.");
}

btn.disabled = false;
btn.value = "Export Data";
return false;
};

Expand Down