diff --git a/Build.sbt b/Build.sbt index bd3fd915d..5dc1c62a8 100644 --- a/Build.sbt +++ b/Build.sbt @@ -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" ) diff --git a/Dockerfile b/Dockerfile index ca1ed757d..fd386e5b3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 @@ -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"] \ No newline at end of file +ENTRYPOINT ["/bin/bash", "-c", "rm -f /opt/bin/femr/RUNNING_PID; exec /opt/bin/femr/bin/femr -Dpidfile.path=/dev/null"] \ No newline at end of file diff --git a/app/femr/business/helpers/LogicDoer.java b/app/femr/business/helpers/LogicDoer.java index 0afdf2a3a..2322ad187 100644 --- a/app/femr/business/helpers/LogicDoer.java +++ b/app/femr/business/helpers/LogicDoer.java @@ -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; + } } diff --git a/app/femr/business/services/system/ResearchService.java b/app/femr/business/services/system/ResearchService.java index 76d990193..e8b430783 100644 --- a/app/femr/business/services/system/ResearchService.java +++ b/app/femr/business/services/system/ResearchService.java @@ -43,13 +43,20 @@ import femr.util.stringhelpers.CSVWriterGson; import femr.util.stringhelpers.GsonFlattener; import femr.util.stringhelpers.StringUtils; +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVPrinter; +import java.io.BufferedWriter; import java.io.File; import java.io.FileNotFoundException; +import java.io.FileWriter; import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.*; +import play.Logger; public class ResearchService implements IResearchService { @@ -198,14 +205,13 @@ private ResearchExportItem createResearchExportItem(IResearchEncounter encounter ResearchExportItem exportitem = new ResearchExportItem(); IPatient patient = encounter.getPatient(); - - // Patient Id - exportitem.setPatientId(patientId); - // Age Integer age = (int)Math.floor(dateUtils.getAgeAsOfDateFloat(patient.getAge(), encounter.getDateOfTriageVisit())); exportitem.setAge(age); + // Patient ID + exportitem.setPatientId(patientId); + // Gender String gender = StringUtils.outputGenderOrMissing(patient.getSex()); exportitem.setGender(gender); @@ -342,65 +348,351 @@ else if (filters.getPrimaryDataset().equals("prescribedMeds") || @Override public ServiceResponse exportPatientsByTrip(Integer tripId){ - + Logger.info("ResearchService:exportPatientsByTrip called with tripId={} at 0 seconds. ", tripId); + long startTimeNanos = System.nanoTime(); ServiceResponse response = new ServiceResponse<>(); - // Build Query based on Filters - Query researchEncounterQuery = QueryProvider.getResearchEncounterQuery(); + // + + String csvParentDirPath = LogicDoer.getCsvFilePath(); + File parentDir = new File(csvParentDirPath); + if (!parentDir.exists()) { + parentDir.mkdirs(); + } + + SimpleDateFormat dateformat = new SimpleDateFormat("MMddyy-HHmmss"); + String timestamp = dateformat.format(new Date()); + + File exportFile = null; + try { + exportFile = File.createTempFile("export" + timestamp+"-", ".csv"); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + +// +// exportFile.getParentFile().mkdirs(); // no-op if already exists + CSVFormat fileformat = CSVFormat.DEFAULT + .withHeader( + "age", + "alcohol", + "assessment", + "bloodPressureDiastolic", + "bloodPressureSystolic", + "chiefComplaints1", + "currentMedication", + "dayOfVisit", + "diabetic", + "dispensedMedications1", + "dispensedMedications2", + "dispensedMedications3", + "dispensedMedications4", + "familyHistory", + "gender", + "glucose", + "heartRate", + "heightFeet", + "heightInches", + "isPregnant", + "medicalSurgicalHistory", + "narrative", + "onset", + "oxygenSaturation", + "palliates", + "patientId", + "pharmacy_note", + "physicalExamination", + "prescribedMedications1", + "prescribedMedications2", + "prescribedMedications3", + "prescribedMedications4", + "prescribedMedications5", + "prescribedMedications6", + "problem", + "procedure_counseling", + "provokes", + "quality", + "radiation", + "respiratoryRate", + "severity", + "smoker", + "socialHistory", + "temperature", + "timeOfDay", + "tripId", + "trip_country", + "trip_team", + "weeksPregnant", + "weight" + ); + + CSVPrinter printer; + + assert exportFile != null; + Path p = exportFile.toPath(); // exportFile is your current string/path + System.out.println("Export absolute path: " + p.toAbsolutePath()); + System.out.println("CWD: " + System.getProperty("user.dir")); + System.out.println("Parent exists? " + (p.getParent() != null && Files.exists(p.getParent()))); + System.out.println("Parent writable? " + (p.getParent() != null && Files.isWritable(p.getParent()))); + + + BufferedWriter writer = null; + try { + writer = new BufferedWriter(new FileWriter(exportFile)); + // new FileWriter fails + } catch (IOException e) { + Logger.error("ResearchService:exportPatientsByTrip FileWriter to produce export could not be instantiated:"); + e.printStackTrace(); + return null; + } + + try { + printer = new CSVPrinter(writer, fileformat); + } catch (Exception e) { + Logger.error("ResearchService:exportPatientsByTrip CSVPrinter could not be instantiated:"); + Logger.info("user.dir=" + System.getProperty("user.dir")); + Logger.info("exportFile=" + exportFile.getAbsolutePath()); + e.printStackTrace(); + return null; + } + if (!exportFile.exists()){ + Logger.error("ResearchService:exportPatientsByTrip export file could not be created."); + return null; + } + // + // + Query researchEncounterQuery = QueryProvider.getResearchEncounterQuery(); researchEncounterQuery .fetch("patient") .fetch("patientPrescriptions") .fetch("patientPrescriptions.medication"); - ExpressionList researchEncounterExpressionList = researchEncounterQuery.where(); - // -1 is default from form if ( tripId != null && tripId != -1 ) { - researchEncounterExpressionList.eq("missionTrip.id",tripId); + } else { + Logger.error("ResearchService:exportPatientsByTrip called with trip id: {} where tripId=null and tripId=-1 are invalidated" + , tripId); } + // Need to add how to handle the case when no trips are selected instead of error? researchEncounterExpressionList.isNull("patient.isDeleted"); researchEncounterExpressionList.orderBy().desc("date_of_triage_visit"); - researchEncounterExpressionList.findList(); + // Do we really need to order the patients? + + researchEncounterExpressionList.findList(); //executes the query and returns into researchEncounterExpressionList List patientEncounters = researchEncounterRepository.find(researchEncounterExpressionList); + Logger.info("ResearchService:exportPatientsByTrip executed query and populated patientEncounters at {} seconds. ", + String.format("%.3f", (float) (System.nanoTime() - startTimeNanos) / 1_000_000_000)); - // As new patients are encountered, generate a UUID to represent them in the export file Map patientIdMap = new HashMap<>(); - // Format patient data for the csv file - List researchExportItemsForCSVExport = new ArrayList<>(); + long duration_iteration = System.nanoTime(); + long timestamp_sec1 = -1; + long timestamp_sec2 = -1; + long timestamp_sec3 = -1; for(IResearchEncounter patientEncounter : patientEncounters ){ UUID patient_uuid; - + // SECTION 1 // If UUID already generated for patient, use that if( patientIdMap.containsKey(patientEncounter.getPatient().getId()) ){ - patient_uuid = patientIdMap.get(patientEncounter.getPatient().getId()); } // otherwise generate and store for potential additional patient encounters else{ - patient_uuid = UUID.randomUUID(); patientIdMap.put(patientEncounter.getPatient().getId(), patient_uuid); } + if (timestamp_sec1 == -1) timestamp_sec1 = System.nanoTime() - duration_iteration; + // SECTION 2 ResearchExportItem item = createResearchExportItem(patientEncounter, patient_uuid); - researchExportItemsForCSVExport.add(item); + if (timestamp_sec2 == -1) timestamp_sec2 = System.nanoTime() - duration_iteration; + + //new SECTION 3 + try { + printer.printRecord( + item.getAge(), // age + item.getVitalMap().get("alcohol"), // alcohol + item.getTabFieldMap().get("assessment"), // assessment + item.getVitalMap().get("bloodPressureDiastolic"), // bloodPressureDiastolic + item.getVitalMap().get("bloodPressureSystolic"), // bloodPressureSystolic + item.getChiefComplaints().isEmpty() + ? null + : item.getChiefComplaints().get(0), // chiefComplaints1 + item.getTabFieldMap().get("currentMedication"), // currentMedication + item.getDayOfVisit(), // dayOfVisit + item.getVitalMap().get("diabetic"), // diabetic + item.getDispensedMedications().size() > 0 + ? item.getDispensedMedications().get(0) : null, // dispensedMedications1 + item.getDispensedMedications().size() > 1 + ? item.getDispensedMedications().get(1) : null, // dispensedMedications2 + item.getDispensedMedications().size() > 2 + ? item.getDispensedMedications().get(2) : null, // dispensedMedications3 + item.getDispensedMedications().size() > 3 + ? item.getDispensedMedications().get(3) : null, // dispensedMedications4 + + item.getTabFieldMap().get("familyHistory"), // familyHistory + item.getGender(), // gender + item.getVitalMap().get("glucose"), // glucose + item.getVitalMap().get("heartRate"), // heartRate + item.getVitalMap().get("heightFeet"), // heightFeet + item.getVitalMap().get("heightInches"), // heightInches + item.getIsPregnant(), // isPregnant + item.getTabFieldMap().get("medicalSurgicalHistory"), // medicalSurgicalHistory + item.getTabFieldMap().get("narrative"), // narrative + item.getTabFieldMap().get("onset"), // onset + item.getVitalMap().get("oxygenSaturation"), // oxygenSaturation + item.getTabFieldMap().get("palliates"), // palliates + item.getPatientId(), // patientId + item.getTabFieldMap().get("pharmacy_note"), // pharmacy_note + item.getTabFieldMap().get("physicalExamination"), // physicalExamination + + item.getPrescribedMedications().size() > 0 + ? item.getPrescribedMedications().get(0) : null, // prescribedMedications1 + item.getPrescribedMedications().size() > 1 + ? item.getPrescribedMedications().get(1) : null, // prescribedMedications2 + item.getPrescribedMedications().size() > 2 + ? item.getPrescribedMedications().get(2) : null, // prescribedMedications3 + item.getPrescribedMedications().size() > 3 + ? item.getPrescribedMedications().get(3) : null, // prescribedMedications4 + item.getPrescribedMedications().size() > 4 + ? item.getPrescribedMedications().get(4) : null, // prescribedMedications5 + item.getPrescribedMedications().size() > 5 + ? item.getPrescribedMedications().get(5) : null, // prescribedMedications6 + + item.getTabFieldMap().get("problem"), // problem + item.getTabFieldMap().get("procedure_counseling"),// procedure_counseling + item.getTabFieldMap().get("provokes"), // provokes + item.getTabFieldMap().get("quality"), // quality + item.getTabFieldMap().get("radiation"), // radiation + item.getVitalMap().get("respiratoryRate"), // respiratoryRate + item.getTabFieldMap().get("severity"), // severity + item.getVitalMap().get("smoker"), // smoker + item.getTabFieldMap().get("socialHistory"), // socialHistory + item.getVitalMap().get("temperature"), // temperature + item.getTabFieldMap().get("timeOfDay"), // timeOfDay + item.getTripId(), // tripId + item.getTrip_country(), // trip_country + item.getTrip_team(), // trip_team + item.getWeeksPregnant(), // weeksPregnant + item.getVitalMap().get("weight") // weight + ); + + printer.flush(); + } catch (Exception e) { + Logger.error("ResearchService:exportPatientsByTrip section3 failed to append row."); + return null; + } + if (timestamp_sec3 == -1) timestamp_sec3 = System.nanoTime() - duration_iteration; + + } + try { + printer.close(); + } catch (Exception e) { + Logger.error("ResearchService:exportPatientsByTrip exception occurred attempting to close printer."); + e.printStackTrace(); } - File eFile = createCsvFile(researchExportItemsForCSVExport); + response.setResponseObject(exportFile); + // + duration_iteration = System.nanoTime() - duration_iteration; + Logger.info("ResearchService:exportPatientsByTrip iterations finished in {} at {} seconds with an average of {} seconds per iteration. ", + String.format("%.3f", (float) (duration_iteration) / 1_000_000_000), + String.format("%.3f", (float) (System.nanoTime() - startTimeNanos) / 1_000_000_000), + String.format("%.3f", (float) (duration_iteration/patientEncounters.size()) / 1_000_000_000)); + Logger.info("ResearchService:exportPatientsByTrip first iteration section timestamps: {} {} {} milliseconds. ", + String.format("%.3f", (float) (timestamp_sec1) / 1_000_000), + String.format("%.3f", (float) (timestamp_sec2) / 1_000_000), + String.format("%.3f", (float) (timestamp_sec3) / 1_000_000)); + Logger.info("ResearchService:exportPatientsByTrip first iteration section durations : {} {} {} milliseconds. ", + String.format("%.3f", (float) (timestamp_sec1) / 1_000_000), + String.format("%.3f", (float) (timestamp_sec2 - timestamp_sec1) / 1_000_000), + String.format("%.3f", (float) (timestamp_sec3 - timestamp_sec2) / 1_000_000)); + long endTimeNanos = System.nanoTime(); + float executionTimeSeconds = (float) (endTimeNanos - startTimeNanos) / 1_000_000_000; + Logger.info("ResearchService:exportPatientsByTrip finished {} encounters in {} seconds. ", + patientEncounters.size(), String.format("%.3f", executionTimeSeconds)); + // - response.setResponseObject(eFile); + try { + writer.close(); + } catch (Exception e) + { + e.printStackTrace(); + return response; + } return response; } + // @Override + // public ServiceResponse exportPatientsByTrip(Integer tripId){ + + // ServiceResponse response = new ServiceResponse<>(); + + // // Build Query based on Filters + // Query researchEncounterQuery = QueryProvider.getResearchEncounterQuery(); + + // researchEncounterQuery + // .fetch("patient") + // .fetch("patientPrescriptions") + // .fetch("patientPrescriptions.medication"); + + // ExpressionList researchEncounterExpressionList = researchEncounterQuery.where(); + + // // -1 is default from form + // if ( tripId != null && tripId != -1 ) { + + // researchEncounterExpressionList.eq("missionTrip.id",tripId); + // } + + // researchEncounterExpressionList.isNull("patient.isDeleted"); + // researchEncounterExpressionList.orderBy().desc("date_of_triage_visit"); + // researchEncounterExpressionList.findList(); + + // List patientEncounters = researchEncounterRepository.find(researchEncounterExpressionList); + + + // // As new patients are encountered, generate a UUID to represent them in the export file + // Map patientIdMap = new HashMap<>(); + + // // Format patient data for the csv file + // List researchExportItemsForCSVExport = new ArrayList<>(); + + // for(IResearchEncounter patientEncounter : patientEncounters ){ + + // UUID patient_uuid; + + // // If UUID already generated for patient, use that + // if( patientIdMap.containsKey(patientEncounter.getPatient().getId()) ){ + + // patient_uuid = patientIdMap.get(patientEncounter.getPatient().getId()); + // } + // // otherwise generate and store for potential additional patient encounters + // else{ + + // patient_uuid = UUID.randomUUID(); + // patientIdMap.put(patientEncounter.getPatient().getId(), patient_uuid); + // } + + // ResearchExportItem item = createResearchExportItem(patientEncounter, patient_uuid); + // researchExportItemsForCSVExport.add(item); + // } + + // File eFile = createCsvFile(researchExportItemsForCSVExport); + + // response.setResponseObject(eFile); +// +// return response; +// } + /** * Creates a csv file from a list of ResearchExportItems * @@ -408,6 +700,9 @@ public ServiceResponse exportPatientsByTrip(Integer tripId){ * @return a csv formatted file of the encounters */ private File createCsvFile( List encounters ){ +// long startTimeNanos = System.nanoTime(); +// Logger.info("ResearchService:createCsvFile called with {} encounters. ", +// encounters.size()); // Make File and get path String csvFilePath = LogicDoer.getCsvFilePath(); @@ -428,7 +723,6 @@ private File createCsvFile( List encounters ){ fileCreated = eFile.createNewFile(); } catch( IOException e ){ - e.printStackTrace(); } } @@ -436,31 +730,50 @@ private File createCsvFile( List encounters ){ if( fileCreated ) { Gson gson = new Gson(); + // a Gson turns a Java object into a Json JsonParser gsonParser = new JsonParser(); String jsonString = gson.toJson(encounters); + // stores a BIG String called jsonString + // + the huge list of encounters in memory now GsonFlattener parser = new GsonFlattener(); CSVWriterGson writer = new CSVWriterGson(); + Logger.info("ResearchService-createCsvFile: jsonString: {} ", jsonString); + try { List> flatJson = parser.parse(gsonParser.parse(jsonString).getAsJsonArray()); + // converts the String into Array of JsonElements + // !!which is temporarily stored as a BIG array of JsonElements + + // parser.parse() goes and flattens each nested JsonElement in the JsonArray... + // ...turning them into key, value pairs + + //!!flatJson itself is BIG List of key-value pairs writer.writeAsCSV(flatJson, csvFileName); + //writeAsCSV internally also builds a BIG list } catch (FileNotFoundException e) { - e.printStackTrace(); } } +// Logger.info("ResearchService:createCsvFile created a csv file in {} seconds. ", +// String.format("%.3f", (float) (System.nanoTime() - startTimeNanos) / 1_000_000_000)); + + // work on a file with a + // rename the final file at the end return eFile; } + /** * take filters and make appropriate query, get list of matching patient encounters * @param filters an object that contains all possible filters for the data * @return a list of the encounters */ + private List queryPatientData(ResearchFilterItem filters){ String datasetName = filters.getPrimaryDataset(); diff --git a/app/femr/common/models/ResearchExportItem.java b/app/femr/common/models/ResearchExportItem.java index 17e56b519..0a7c295f4 100644 --- a/app/femr/common/models/ResearchExportItem.java +++ b/app/femr/common/models/ResearchExportItem.java @@ -32,18 +32,19 @@ public class ResearchExportItem { private Integer weeksPregnant; private String dayOfVisit; private Integer tripId; + private List chiefComplaints; private List prescribedMedications; private List dispensedMedications; private Map vitalMap; private Map tabFieldMap; + private String trip_team; private String trip_country; public UUID getPatientId() { return patientId; } - public void setPatientId(UUID patientId) { this.patientId = patientId; } @@ -51,7 +52,6 @@ public void setPatientId(UUID patientId) { public String getGender() { return gender; } - public void setGender(String gender) { this.gender = gender; } @@ -59,7 +59,6 @@ public void setGender(String gender) { public Integer getAge() { return age; } - public void setAge(Integer age) { this.age = age; } @@ -67,7 +66,6 @@ public void setAge(Integer age) { public Boolean getIsPregnant() { return isPregnant; } - public void setIsPregnant(Boolean isPregnant) { this.isPregnant = isPregnant; } @@ -75,7 +73,6 @@ public void setIsPregnant(Boolean isPregnant) { public Integer getWeeksPregnant() { return weeksPregnant; } - public void setWeeksPregnant(Integer weeksPregnant) { this.weeksPregnant = weeksPregnant; } @@ -83,7 +80,6 @@ public void setWeeksPregnant(Integer weeksPregnant) { public List getChiefComplaints() { return chiefComplaints; } - public void setChiefComplaints(List chiefComplaints) { this.chiefComplaints = chiefComplaints; } @@ -91,7 +87,6 @@ public void setChiefComplaints(List chiefComplaints) { public List getPrescribedMedications() { return prescribedMedications; } - public void setPrescribedMedications(List prescribedMedications) { this.prescribedMedications = prescribedMedications; } @@ -99,7 +94,6 @@ public void setPrescribedMedications(List prescribedMedications) { public List getDispensedMedications() { return dispensedMedications; } - public void setDispensedMedications(List dispensedMedications) { this.dispensedMedications = dispensedMedications; } @@ -107,7 +101,6 @@ public void setDispensedMedications(List dispensedMedications) { public Map getVitalMap() { return vitalMap; } - public void setVitalMap(Map vitalMap) { this.vitalMap = vitalMap; } @@ -115,7 +108,6 @@ public void setVitalMap(Map vitalMap) { public Map getTabFieldMap() { return tabFieldMap; } - public void setTabFieldMap(Map tabFieldMap) { this.tabFieldMap = tabFieldMap; } @@ -123,7 +115,6 @@ public void setTabFieldMap(Map tabFieldMap) { public String getDayOfVisit() { return dayOfVisit; } - public void setDayOfVisit(String day) { this.dayOfVisit = day; } @@ -131,7 +122,6 @@ public void setDayOfVisit(String day) { public Integer getTripId() { return tripId; } - public void setTripId(Integer tripId) { this.tripId = tripId; } @@ -139,7 +129,6 @@ public void setTripId(Integer tripId) { public String getTrip_team() { return trip_team; } - public void setTrip_team(String trip_team) { this.trip_team = trip_team; } @@ -147,7 +136,6 @@ public void setTrip_team(String trip_team) { public String getTrip_country() { return trip_country; } - public void setTrip_country(String trip_country) { this.trip_country = trip_country; } diff --git a/app/femr/ui/controllers/ResearchController.java b/app/femr/ui/controllers/ResearchController.java index 19d48d6a0..d497d1816 100644 --- a/app/femr/ui/controllers/ResearchController.java +++ b/app/femr/ui/controllers/ResearchController.java @@ -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())); @@ -130,6 +131,9 @@ public Result exportPost() { // This does weird stuff and isn't reliable. //ServiceResponse exportServiceResponse = researchService.retrieveCsvExportFile(filterItem); ServiceResponse exportServiceResponse = researchService.exportPatientsByTrip(filterItem.getMissionTripId()); + if (exportServiceResponse == null) { + return internalServerError("Export Failed"); + } File csvFile = exportServiceResponse.getResponseObject(); diff --git a/app/femr/util/stringhelpers/StringUtils.java b/app/femr/util/stringhelpers/StringUtils.java index 45027f563..37181b2eb 100644 --- a/app/femr/util/stringhelpers/StringUtils.java +++ b/app/femr/util/stringhelpers/StringUtils.java @@ -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 @@ -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 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 map) { + if (map == null || map.isEmpty()) return ""; + StringBuilder sb = new StringBuilder(); + boolean first = true; + for (Map.Entry 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 map) { + if (map == null || map.isEmpty()) return ""; + StringBuilder sb = new StringBuilder(); + boolean first = true; + for (Map.Entry e : map.entrySet()) { + if (!first) sb.append("; "); + sb.append(e.getKey()).append("=").append(e.getValue()); + first = false; + } + return sb.toString(); + } + } + + diff --git a/public/js/research/filter-menu.js b/public/js/research/filter-menu.js index 0be4cce01..ab921829e 100644 --- a/public/js/research/filter-menu.js +++ b/public/js/research/filter-menu.js @@ -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; };