diff --git a/README.adoc b/README.adoc index 07b5ab51f4c..2ca9de160dd 100644 --- a/README.adoc +++ b/README.adoc @@ -44,8 +44,8 @@ ** Can't remember when your next appointment is? VISIT reminds you when your next consultation is. * *Alias / Macro command support* 🔤 ** No need to type the same commands over and over, simply add an alias from within the CLI! -* *Save patient information as a .pdf file* 📄 -** VISIT allows you to export and store a patient's profile as a .pdf in your own file system! +* *Save patient information as a text file* 📄 +** VISIT allows you to export and store a patient's profile in your own file system! * *Sleek and modern UI* 💼 * *Tagging and Search functionality* 🔎 * *Cross platform* 🖥️ diff --git a/src/main/java/unrealunity/visit/commons/util/FileUtil.java b/src/main/java/unrealunity/visit/commons/util/FileUtil.java index 1b64e31be83..4b8850ba91a 100644 --- a/src/main/java/unrealunity/visit/commons/util/FileUtil.java +++ b/src/main/java/unrealunity/visit/commons/util/FileUtil.java @@ -80,4 +80,13 @@ public static void writeToFile(Path file, String content) throws IOException { Files.write(file, content.getBytes(CHARSET)); } + /** + * Writes given string to a file. + * Will create the file if it does not exist yet. + */ + public static void writeToProtectedFile(Path file, String content) throws IOException { + Files.write(file, content.getBytes(CHARSET)); + file.toFile().setReadOnly(); + } + } diff --git a/src/main/java/unrealunity/visit/commons/util/ProfileUtil.java b/src/main/java/unrealunity/visit/commons/util/ProfileUtil.java index 85d300e3f08..db41ff2ec22 100644 --- a/src/main/java/unrealunity/visit/commons/util/ProfileUtil.java +++ b/src/main/java/unrealunity/visit/commons/util/ProfileUtil.java @@ -2,6 +2,8 @@ import static java.util.Objects.requireNonNull; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Set; @@ -14,28 +16,51 @@ import unrealunity.visit.model.tag.Tag; /** - * Helper functions for handling Person data for displaying in ProfileWindow. + * Helper functions for handling various {@code Person} data for displaying in {@code ProfileWindow}. */ public class ProfileUtil { + // Strings for formatting of .txt Profile generation + public static final String BREAKLINE = "=======================================================================\n"; + public static final String HEADER = BREAKLINE + "=========================== Patient Profile ==============" + + "=============\n" + BREAKLINE + "\n"; + public static final String FOOTER = BREAKLINE + BREAKLINE + BREAKLINE; + + // Header Strings for Patient Attributes + public static final String HEADER_NAME = "Name:"; + public static final String HEADER_TAG = "Tags:"; + public static final String HEADER_PHONE = "Contact Number:"; + public static final String HEADER_EMAIL = "Email:"; + public static final String HEADER_ADDRESS = "Address:"; + public static final String HEADER_VISIT = "Visits:"; + + // Header Strings for Visit Report + public static final String HEADER_DIAG = "*Diagnosis*"; + public static final String HEADER_MED = "*Medication*"; + public static final String HEADER_REMARK = "*Remarks*"; + + /** - * Returns the String representation of the Person's Name attribute. - * @param name a Name instance of a Person. Cannot be null - * @return a String representing the Person's Name attribute + * Returns the {@code String} representation of a {@code Name} instance. + * + * @param name a {@code Name} instance to be shown as a {@code String}. Cannot be null + * @return a {@code String} representing the {@code Name} instance */ public static String stringifyName(Name name) { - requireNonNull(name); + requireNonNull(name, "Name cannot be null."); return name.fullName; } /** - * Returns the String representation of the tags associated with a Person from their <SetTag> attribute. - * @param tagSet Set of Tag objects attributed to the Person instance called in setup. Cannot be null - * @return a String representing all Tags in the Person's Tag attribute + * Returns the {@code String} representation of {@code Set<Tag>} instance. + * Each {@code Tag} is separated with a ";" in the returned String representation. + * + * @param tagSet {@code Set} of {@code Tag} instances to be represented as a {@code String}. Cannot be null + * @return a {@code String} representing the {@code Set<Tag>} */ public static String stringifyTags(Set tagSet) { - requireNonNull(tagSet); + requireNonNull(tagSet, "Set cannot be null."); StringBuilder sb = new StringBuilder(); @@ -44,65 +69,72 @@ public static String stringifyTags(Set tagSet) { sb.append(tag.tagName); sb.append("; "); } + } else { + return "-"; } return sb.toString(); } /** - * Returns the String representation of the Phone instance associated with a Person. - * @param phone Phone attribute of the Person in called in setup. Cannot be null - * @return a String representing the Person's Phone attribute + * Returns the {@code String} representation of a {@code Phone} instance. + * + * @param phone {@code Phone} instance to be shown as a {@code String}. Cannot be null + * @return a {@code String} representing the {@code Phone} instance */ public static String stringifyPhone(Phone phone) { - requireNonNull(phone); + requireNonNull(phone, "Phone cannot be null."); return phone.value; } /** - * Returns the String representation of the Email instance associated with a Person. - * @param email Email attribute of the Person in called in setup. Cannot be null - * @return a String representing the Person's Email attribute + * Returns the {@code String} representation of a {@code Email} instance. Returns "-" if blank. + * + * @param email {@code Email} instance to be shown as a {@code String}. Cannot be null + * @return a {@code String} representing the {@code Email} instance */ public static String stringifyEmail(Email email) { - requireNonNull(email); + requireNonNull(email, "Email cannot be null."); return email.value; } /** - * Returns the String representation of the Address instance associated with a Person. - * @param address Phone attribute of the Person in called in setup. Cannot be null - * @return a String representing the Person's Phone attribute + * Returns the {@code String} representation of a {@code Address} instance. + * + * @param address {@code Address} instance to be shown as a {@code String}. Cannot be null + * @return a {@code String} representing the {@code Address} instance */ public static String stringifyAddress(Address address) { - requireNonNull(address); + requireNonNull(address, "Address cannot be null."); return address.value; } /** - * Returns the String representation of the VisitList instance associated with a Person. - * Visits are separated with a line of '=', each block detailing the full 'Diagnosis', - * 'Medication' and 'Remarks' fields from the VisitReport instance. Returns "-" if null. - * @param visitList VisitList attribute of a Person containing VisitReports - * @return a String representing the entire VisitList + * Returns the {@code String} representation of a {@code VisitList} instance. Each {@code VisitReport} in contained + * in the {@code VisitList} is represented in a {@code String} separated with a breakline of '='. + * Returns "-" if null. + * + * @param visitList {@code VisitList} instance to be shown as a {@code String} + * @return a {@code String} representing the {@code VisitList} instance */ public static String stringifyVisit(VisitList visitList) { if (visitList == null) { return "-"; } - String line = "==================================================================\n"; + // Get each all VisitReport instances ArrayList visits = visitList.getRecords(); if (visits.size() == 0) { return "-"; } + // Build String by iterating through all VisitReports StringBuilder output = new StringBuilder(); for (VisitReport visit : visits) { + output.append(BREAKLINE); output.append(stringifyVisitReport(visit)); - output.append(line); output.append("\n"); } @@ -110,13 +142,14 @@ public static String stringifyVisit(VisitList visitList) { } /** - * Returns the String representation of a VisitReport, detailing the full 'Diagnosis', - * 'Medication' and 'Remarks' fields from the VisitReport instance. - * @param report the VisitReport instance to be represented. Cannot be null - * @return a String representing the VisitReport + * Returns the {@code String} representation of a {@code VisitReport}, detailing the full 'Diagnosis', + * 'Medication' and 'Remarks' fields from the {@code VisitReport} instance. + * + * @param report {@code VisitReport} instance to be shown as a {@code String} + * @return a {@code String} representing the {@code VisitReport} instance */ public static String stringifyVisitReport(VisitReport report) { - requireNonNull(report); + requireNonNull(report, "VisitReport cannot be null."); String date = report.date; String diagnosis = report.getDiagnosis(); @@ -125,19 +158,90 @@ public static String stringifyVisitReport(VisitReport report) { StringBuilder output = new StringBuilder(); - // [Report on the XX/XX/2XXX] - output.append("[ Report on the " + date + "]\n\n"); + // Date Stamp for Visit + output.append("[ Report on the " + date + " ]\n\n"); + + // Appending all report details + output.append(paragraph(HEADER_DIAG, diagnosis)); + output.append(paragraph(HEADER_MED, medication)); + output.append(paragraph(HEADER_REMARK, remarks)); + + return output.toString(); + } - // *Diagnosis*: DIAGNOSIS - output.append("*Diagnosis*:\n" + diagnosis + "\n\n"); + /** + * Returns the {@code String} representation of a {@code LocalDateTime} instance in the "dd-MM-yyyy HH-mm-ss" + * format. The resulting {@code String} is file system friendly (No illegal characters for file naming). + * + * @param time {@code LocalDateTime} instance to be represented as a {@code String}. + * @return {@code String} representation of a {@code LocalDateTime} instance in the "dd-MM-yyyy HH-mm-ss" + * format. + */ + public static String stringifyDate(LocalDateTime time) { + requireNonNull(time, "LocalDateTime time cannot be null."); - // *Medication prescribed*: MEDICATION - output.append("*Medication prescribed*:\n" + medication + "\n\n"); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd-MM-yyyy HH-mm-ss"); // Windows Unix naming safe + return time.format(formatter); + } - // *Remarks*: REMARKS - output.append("*Remarks*:\n" + remarks + "\n\n"); + /** + * Builds content of a Profile Report given the following fields. The content is assembled as a {@code String}. + * + * @param name {@code String} representing the {@code Name} on the report. + * @param tags {@code String} representing the {@code Tag} instances on the report. + * @param phone {@code String} representing the {@code Phone} number on the report. + * @param email {@code String} representing the {@code Email} on the report. + * @param address {@code String} representing the {@code Address} on the report. + * @param visits {@code String} representing the {@code VisitList} on the report. + * @param time {@code String} representing the {@code LocalDateTime} in which the report is generated. + * @return {@code String} representing entire content of the report. + */ + public static String buildProfileReport(String name, String tags, String phone, String email, + String address, String visits, String time) { + assert StringUtil.allStringsNotNullOrBlank(name, tags, phone, email, + address, visits, time) : "All fields should not be blank or null"; + CollectionUtil.requireAllNonNull(name, tags, phone, email, address, visits, time); + + StringBuilder output = new StringBuilder(); + + output.append(HEADER); + output.append("[Profile generated at " + time + ".]\n\n"); + + output.append(paragraph(HEADER_NAME, name)); + output.append(paragraph(HEADER_TAG, tags)); + output.append(paragraph(HEADER_PHONE, phone)); + output.append(paragraph(HEADER_EMAIL, email)); + output.append(paragraph(HEADER_ADDRESS, address)); + + output.append("\n"); + + output.append(paragraph(HEADER_VISIT, visits)); + + output.append(FOOTER); return output.toString(); } + /** + * Arranges two {@code String} instances in a paragraph format. Where:
+ * Example:
+     *         header
+     *         content
+     *                  // Break line
+     *         
+ * + * @param header {@code String} representing the header or title of the section/paragraph + * @param content {@code String} representing the content of the section/paragraph + * @return {@code String} representing a complete paragraph composed of {@code header} and {@code content} + */ + public static String paragraph(String header, String content) { + assert StringUtil.allStringsNotNullOrBlank(header, content) : "Header or content should not be null/blank"; + StringBuilder paragraph = new StringBuilder(); + + paragraph.append(header + "\n"); + paragraph.append(content + "\n\n"); + + return paragraph.toString(); + } + } diff --git a/src/main/java/unrealunity/visit/commons/util/StringUtil.java b/src/main/java/unrealunity/visit/commons/util/StringUtil.java index 55f5983eabb..21595566a79 100644 --- a/src/main/java/unrealunity/visit/commons/util/StringUtil.java +++ b/src/main/java/unrealunity/visit/commons/util/StringUtil.java @@ -15,13 +15,14 @@ public class StringUtil { /** * Returns true if the {@code sentence} contains the {@code word}. * Ignores case, but a full word match is required. - *
examples:
+     *   
Examples:
      *       containsWordIgnoreCase("ABc def", "abc") == true
      *       containsWordIgnoreCase("ABc def", "DEF") == true
      *       containsWordIgnoreCase("ABc def", "AB") == false //not a full word match
      *       
* @param sentence cannot be null * @param word cannot be null, cannot be empty, must be a single word + * @return boolean for whether the sentence contains a match for the word */ public static boolean containsWordIgnoreCase(String sentence, String word) { requireNonNull(sentence); @@ -89,4 +90,19 @@ public static boolean isNonZeroUnsignedInteger(String s) { return false; } } + + /** + * Returns false if any Strings are null or blank. Utility function to check a variable number of Strings. + * + * @param strings variable number of Strings + * @return {@code boolean} representing if any Strings are null or blank + */ + public static boolean allStringsNotNullOrBlank (String ...strings) { + for (String str : strings) { + if (str == null || str.isBlank()) { + return false; + } + } + return true; + } } diff --git a/src/main/java/unrealunity/visit/logic/commands/GenerateProfileCommand.java b/src/main/java/unrealunity/visit/logic/commands/GenerateProfileCommand.java index a795e19b6fb..6a11f9a71f9 100644 --- a/src/main/java/unrealunity/visit/logic/commands/GenerateProfileCommand.java +++ b/src/main/java/unrealunity/visit/logic/commands/GenerateProfileCommand.java @@ -4,52 +4,54 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; import java.util.logging.Logger; import unrealunity.visit.commons.core.LogsCenter; import unrealunity.visit.commons.util.FileUtil; +import unrealunity.visit.commons.util.ProfileUtil; import unrealunity.visit.logic.commands.exceptions.CommandException; import unrealunity.visit.model.Model; -import unrealunity.visit.ui.HelpWindow; +import unrealunity.visit.model.person.Person; /** - * Saves new record to Visit List. + * Generates a .txt file detailing the ProfileWindow details. */ public class GenerateProfileCommand extends Command { public static final String MESSAGE_GENERATE_PROFILE_SUCCESS = "Profile successfully generated!"; - private static final Logger logger = LogsCenter.getLogger(HelpWindow.class); - private static final String line = "=======================================================================\n"; - private static final String header = line + "=========================== Patient Profile ==============" - + "=============\n" + line + "\n"; - - private String name; - private String tags; - private String phone; - private String email; - private String address; - private String visits; - - public GenerateProfileCommand(String name, String tags, String phone, String email, - String address, String visits) { - this.name = name; - this.tags = tags; - this.phone = phone; - this.email = email; - this.address = address; - this.visits = visits; + private static final Logger logger = LogsCenter.getLogger(GenerateProfileCommand.class); + + private final String name; + private final String tags; + private final String phone; + private final String email; + private final String address; + private final String visits; + private final String time; + + + public GenerateProfileCommand(Person person) { + // Stringify Person particulars + name = ProfileUtil.stringifyName(person.getName()); + tags = ProfileUtil.stringifyTags(person.getTags()); + phone = ProfileUtil.stringifyPhone(person.getPhone()); + email = ProfileUtil.stringifyEmail(person.getEmail()); + address = ProfileUtil.stringifyAddress(person.getAddress()); + + // Stringify Person Visits + visits = ProfileUtil.stringifyVisit(person.getVisitList()); + + // Stringify Current Time + time = ProfileUtil.stringifyDate(LocalDateTime.now()); } + /** + * Generates and writes a .txt file containing the Profile of the patient. + */ @Override public CommandResult execute(Model model) throws CommandException { - // Get date for profile generation - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd-MM-yyyy HH-mm-ss"); // Windows Unix naming safe - String now = LocalDateTime.now().format(formatter); - // File name formation : NAME_PHONE_dd-MM-yyyy HH-mm-ss.txt // E.g. Alex Yeoh_87438807_31-10-2019 11-41-02.txt - String filename = this.name + "_" + this.phone + "_" + now + ".txt"; - + String filename = name + "_" + phone + "_" + time + ".txt"; // Get the path for file to be created Path path = Paths.get("generated_profiles", filename); @@ -58,44 +60,17 @@ public CommandResult execute(Model model) throws CommandException { try { FileUtil.createIfMissing(path); } catch (IOException e) { - throw new CommandException("Error creating new file - Check permissions to folder: " + e.getMessage()); + throw new CommandException("Error creating new folder - Check permissions to directory -" + e.getMessage()); } // Form profile .txt content - StringBuilder output = new StringBuilder(); - - output.append(header); - - output.append("Name:\n"); - output.append(this.name); - output.append("\n\n"); - - output.append("Tags:\n"); - output.append(this.tags); - output.append("\n\n"); - - output.append("Phone:\n"); - output.append(this.phone); - output.append("\n\n"); - - output.append("Email:\n"); - output.append(this.email); - output.append("\n\n"); - - output.append("Address:\n"); - output.append(this.address); - output.append("\n\n"); - - output.append("Visits:\n\n"); - output.append("==================================================================\n"); - output.append(this.visits); - output.append("\n\n"); - - output.append("[Profile generated at " + now + ".]"); + String fileContent = ProfileUtil.buildProfileReport(name, tags, phone, email, + address, visits, time); + // Write to file content to file in path try { - FileUtil.writeToFile(path, output.toString()); - logger.info("User .pdf profile successfully exported to " + path); + FileUtil.writeToProtectedFile(path, fileContent); + logger.info("User .txt profile successfully exported to " + path); } catch (IOException e) { throw new CommandException("Error writing to filepath: " + e.getMessage()); } @@ -122,7 +97,8 @@ public boolean equals(Object other) { && phone.equals(e.phone) && email.equals(e.email) && address.equals(e.address) - && visits.equals(e.visits); + && visits.equals(e.visits) + && time.equals(e.time); } diff --git a/src/main/java/unrealunity/visit/logic/parser/ProfileCommandParser.java b/src/main/java/unrealunity/visit/logic/parser/ProfileCommandParser.java index 4b6eef11f94..5576c771853 100644 --- a/src/main/java/unrealunity/visit/logic/parser/ProfileCommandParser.java +++ b/src/main/java/unrealunity/visit/logic/parser/ProfileCommandParser.java @@ -9,12 +9,12 @@ import unrealunity.visit.logic.parser.exceptions.ParseException; /** - * Parses input arguments and creates a new {@code RemarkCommand} object + * Parses input arguments and creates a new {@code ProfileCommand} object */ public class ProfileCommandParser implements Parser { /** - * Parses the given {@code String} of arguments in the context of the {@code RemarkCommand} - * and returns a {@code RemarkCommand} object for execution. + * Parses the given {@code String} of arguments in the context of the {@code ProfileCommand} + * and returns a {@code ProfileCommand} object for execution. * @throws ParseException if the user input does not conform the expected format */ public ProfileCommand parse(String args) throws ParseException { diff --git a/src/main/java/unrealunity/visit/model/person/Person.java b/src/main/java/unrealunity/visit/model/person/Person.java index b4bd560c9c6..d7aa5c3a3a6 100644 --- a/src/main/java/unrealunity/visit/model/person/Person.java +++ b/src/main/java/unrealunity/visit/model/person/Person.java @@ -65,6 +65,13 @@ public Set getTags() { return Collections.unmodifiableSet(tags); } + /** + * Returns a defensive copy of Person. + */ + public Person getClone() { + return new Person(name, phone, email, address, visitList, tags); + } + /** * Returns true if both persons of the same name have at least one other identity field that is the same. * This defines a weaker notion of equality between two persons. diff --git a/src/main/java/unrealunity/visit/ui/MainWindow.java b/src/main/java/unrealunity/visit/ui/MainWindow.java index d72a6282121..0e7756c834f 100644 --- a/src/main/java/unrealunity/visit/ui/MainWindow.java +++ b/src/main/java/unrealunity/visit/ui/MainWindow.java @@ -70,8 +70,9 @@ public MainWindow(Stage primaryStage, Logic logic) { this.primaryStage = primaryStage; this.logic = logic; + // Load font - Font.loadFont(getClass().getResourceAsStream("/font/Gill-Sans-MT.TTF"), 10); + Font.loadFont(getClass().getResourceAsStream("/font/Gill-Sans-MT.ttf"), 10); // Configure the UI setWindowDefaultSize(logic.getGuiSettings()); @@ -318,7 +319,6 @@ private CommandResult executeCommand(String commandText) throws CommandException if (commandResult.isShowProfile()) { profilePanel.setup(commandResult.getProfilePerson(), logic); - profilePanel.populateVisitList(commandResult.getObservableVisitList()); handleProfilePanel(); } diff --git a/src/main/java/unrealunity/visit/ui/ProfileVisitCard.java b/src/main/java/unrealunity/visit/ui/ProfileVisitCard.java index 90dd3ff734f..5a55531d8d7 100644 --- a/src/main/java/unrealunity/visit/ui/ProfileVisitCard.java +++ b/src/main/java/unrealunity/visit/ui/ProfileVisitCard.java @@ -7,7 +7,7 @@ import unrealunity.visit.model.person.VisitReport; /** - * An UI component that displays information of a {@code VisitList}. + * An UI component that displays information of a {@code VisitReport} on the {@code ProfileWindow}. */ public class ProfileVisitCard extends UiPart { @@ -39,30 +39,31 @@ public ProfileVisitCard(VisitReport report) { this.remarks = report.getRemarks(); // Set date - profileVisitDate.setText("Visitation Report on [" + date + "]"); + profileVisitDate.setText("Visitation Report on [" + date + "]"); - // Set Diagnosis - if (diagnosis == null || diagnosis.isEmpty()) { - profileVisitDiagnosis.setText("-"); - diagnosis = "-"; - } else { - profileVisitDiagnosis.setText(report.getDiagnosis()); - } + // Set Diagnosis, Medication and Remark data + setVisitText(profileVisitDiagnosis, diagnosis); + setVisitText(profileVisitMedication, medication); + setVisitText(profileVisitRemarks, remarks); + } - // Set Medication - if (report.getMedication() == null || report.getMedication().isEmpty()) { - profileVisitMedication.setText("-"); - medication = "-"; - } else { - profileVisitMedication.setText(report.getMedication()); + /** + * Sets a label in the {@code ProfileVisitCard} with a specified {@code String} detailing + * the appropriate description. + * + * @param label {@code Label} instance to display the {@code String} content + * @param reportDetail {@code String} detailing the description for {@code Label} to display + */ + private void setVisitText(Label label, String reportDetail) { + // Guard function for null label + if (label == null) { + return; } - // Set Remarks - if (report.getRemarks() == null || report.getRemarks().isEmpty()) { - profileVisitRemarks.setText("-"); - remarks = "-"; + if (reportDetail == null || reportDetail.isBlank()) { + label.setText("-"); } else { - profileVisitRemarks.setText(report.getRemarks()); + label.setText(reportDetail); } } diff --git a/src/main/java/unrealunity/visit/ui/ProfileWindow.java b/src/main/java/unrealunity/visit/ui/ProfileWindow.java index 0795632a1fe..f989d8df7fb 100644 --- a/src/main/java/unrealunity/visit/ui/ProfileWindow.java +++ b/src/main/java/unrealunity/visit/ui/ProfileWindow.java @@ -26,8 +26,8 @@ /** - * Panel containing detailed information of the specified Person including - * the usual details on PersonCard, and also associated Visit information. + * Panel containing detailed information of the specified {@code Person} including + * the usual details on {@code PersonCard}, and also associated {@code VisitReport} information. */ public class ProfileWindow extends UiPart { private static final String FXML = "ProfileWindow.fxml"; @@ -64,7 +64,7 @@ public ProfileWindow(Stage root) { } /** - * Creates a new ProfilePanel. + * Creates a new {@code ProfilePanel}. */ public ProfileWindow() { this(new Stage()); @@ -76,31 +76,53 @@ public ProfileWindow() { */ this.getRoot().initModality(Modality.APPLICATION_MODAL); - /* - * Using default window instead. This removed the default control bar. - this.getRoot().initStyle(StageStyle.UTILITY); - */ + // Add handlers to ProfileWindow for user actions. + // (esc, q - Exit), (p - Generate .txt for user Profile) + getRoot().addEventHandler(KeyEvent.KEY_RELEASED, (KeyEvent event) -> { + if (KeyCode.ESCAPE == event.getCode()) { + logger.info("User pressed 'esc'. Closing Profile Panel.."); + getRoot().hide(); + logger.info("Profile Panel Closed."); + } else if (KeyCode.Q == event.getCode()) { + logger.info("User pressed 'q'. Closing Profile Panel."); + getRoot().hide(); + logger.info("Profile Panel Closed."); + } else if (KeyCode.P == event.getCode()) { + try { + generateProfilePressed(new ActionEvent()); + } catch (CommandException e) { + logger.warning("Exception when generating Profile. Error: " + e.getMessage()); + message.setText("Exception when generating Profile."); + } + } + } + ); } /** * Initializes the Profile Window with the particulars from the Person instance. + * * @param person Person instance to show in the Profile Window */ public void setup(Person person, Logic logic) { this.logic = logic; this.person = person; - // Set Person Particulars + // Set Person Particulars into relevant fields nameField.setText(ProfileUtil.stringifyName(person.getName())); tagField.setText(ProfileUtil.stringifyTags(person.getTags())); phoneField.setText(ProfileUtil.stringifyPhone(person.getPhone())); emailField.setText(ProfileUtil.stringifyEmail(person.getEmail())); addressField.setText(ProfileUtil.stringifyAddress(person.getAddress())); + + // Set Person Visits into ListView + populateVisitList(person.getVisitList().getObservableRecords()); } /** * Populates the ProfileWindow's ListView with the ProfileVisitListCells representing the VisitReport * instances contained within an ObservableList<VisitReport> instance. + * * @param visitList ObservableList<VisitReport> instance containing the VisitReports to be * visualized. */ @@ -111,7 +133,7 @@ public void populateVisitList(ObservableList visitList) { /** - * Custom {@code ListCell} that displays the graphics of a {@code Person} using a {@code PersonCard}. + * Custom {@code ListCell} that displays the graphics of a {@code VisitReport} using a {@code ProfileVisitCard}. */ class ProfileVisitListCell extends ListCell { @Override @@ -130,6 +152,7 @@ protected void updateItem(VisitReport report, boolean empty) { /** * Shows the Profile Panel. + * * @throws IllegalStateException *
    *
  • @@ -142,7 +165,7 @@ protected void updateItem(VisitReport report, boolean empty) { * if this method is called on the primary stage. *
  • *
  • - * if {@code dialogStage} is already showing. + * if {@code ProfileWindow} is already showing. *
  • *
*/ @@ -150,27 +173,7 @@ public void show() { logger.info("Showing Profile Panel"); getRoot().show(); getRoot().centerOnScreen(); - - getRoot().addEventHandler(KeyEvent.KEY_RELEASED, (KeyEvent event) -> { - if (KeyCode.ESCAPE == event.getCode()) { - logger.info("User pressed 'esc'. Closing Profile Panel.."); - getRoot().hide(); - logger.info("Profile Panel Closed."); - } else if (KeyCode.Q == event.getCode()) { - logger.info("User pressed 'q'. Closing Profile Panel."); - getRoot().hide(); - logger.info("Profile Panel Closed."); - } else if (KeyCode.P == event.getCode()) { - try { - logger.info("User pressed 'p'. Generating Profile .pdf.."); - generateProfilePressed(new ActionEvent()); - logger.info("Profile .pdf generation successful."); - } catch (CommandException e) { - logger.warning("Exception when generating Profile. Error: " + e.getMessage()); - } - } - } - ); + message.setText(""); } /** @@ -195,17 +198,20 @@ public void focus() { } /** - * @param event - * @throws CommandException + * Generates a {@code GenerateProfileCommand} instance with the details of the current for the current + * Profile Window to generate the patient profile. + * + * @param event {@code ActionEvent} instance triggering the profile generation + * @throws CommandException when the .txt file fails to write to the path */ @FXML void generateProfilePressed(ActionEvent event) throws CommandException { - GenerateProfileCommand generateProfile = new GenerateProfileCommand(nameField.getText(), tagField.getText(), - phoneField.getText(), emailField.getText(), addressField.getText(), - ProfileUtil.stringifyVisit(person.getVisitList())); + logger.info("Generating Profile .txt.."); + GenerateProfileCommand generateProfile = new GenerateProfileCommand(person); try { CommandResult commandResult = logic.execute(generateProfile); - message.setText("A log has been successfully created."); + message.setText("Profile .txt created in /generated_profiles/."); + logger.info("Profile .txt generation successful."); } catch (CommandException e) { throw e; } diff --git a/src/main/resources/view/ProfileWindow.fxml b/src/main/resources/view/ProfileWindow.fxml index dc193893e42..104b0f1c4c4 100644 --- a/src/main/resources/view/ProfileWindow.fxml +++ b/src/main/resources/view/ProfileWindow.fxml @@ -165,10 +165,13 @@ - + + + + diff --git a/src/test/java/unrealunity/visit/commons/util/ProfileUtilTest.java b/src/test/java/unrealunity/visit/commons/util/ProfileUtilTest.java new file mode 100644 index 00000000000..ebbec2fd4ae --- /dev/null +++ b/src/test/java/unrealunity/visit/commons/util/ProfileUtilTest.java @@ -0,0 +1,195 @@ +package unrealunity.visit.commons.util; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Set; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import unrealunity.visit.model.person.Address; +import unrealunity.visit.model.person.Email; +import unrealunity.visit.model.person.Name; +import unrealunity.visit.model.person.Phone; +import unrealunity.visit.model.person.VisitList; +import unrealunity.visit.model.person.VisitReport; +import unrealunity.visit.model.tag.Tag; +import unrealunity.visit.testutil.Assert; +import unrealunity.visit.testutil.TypicalVisits; + +/** + * Contains unit tests for ProfileUnitTest. + */ +public class ProfileUtilTest { + + /* + * Test for various stringify methods for the various Person attributes + * Invalid equivalence partition for null, and valid partitions for valid attributes + * (and empty if valid) are tested. + */ + + //---------------- Tests for stringifyName ----------------------------------------------- + + @Test + public void stringifyName_nullName_throwsNullPointerException() { + Name name = null; + Assert.assertThrows(NullPointerException.class, "Name cannot be null.", () + -> ProfileUtil.stringifyName(name)); + } + + @Test + public void stringifyName_validInput_correctResult() { + Name name = new Name("Test Name"); + Assertions.assertEquals("Test Name", ProfileUtil.stringifyName(name)); + } + + + //---------------- Tests for stringifyTags ----------------------------------------------- + + @Test + public void stringifyTags_nullTagSet_throwsNullPointerException() { + Set tagSet = null; + Assert.assertThrows(NullPointerException.class, "Set cannot be null.", () + -> ProfileUtil.stringifyTags(tagSet)); + } + + @Test + public void stringifyTags_validEmptyTagSet_correctResult() { + Set tagSet = new HashSet<>(); + Assertions.assertEquals("-", ProfileUtil.stringifyTags(tagSet)); + } + + @Test + public void stringifyTags_validFilledTagSet_correctResult() { + Set tagSet = new HashSet<>(); + tagSet.add(new Tag("flu")); + tagSet.add(new Tag("suspectedH1N1")); + Assertions.assertEquals("flu; suspectedH1N1; ", ProfileUtil.stringifyTags(tagSet)); + } + + //---------------- Tests for stringifyPhone ---------------------------------------------- + + @Test + public void stringifyPhone_nullPhone_throwsNullPointerException() { + Phone phone = null; + Assert.assertThrows(NullPointerException.class, "Phone cannot be null.", () + -> ProfileUtil.stringifyPhone(phone)); + } + + @Test + public void stringifyPhone_validInput_correctResult() { + Phone phone = new Phone("12345678"); + Assertions.assertEquals("12345678", ProfileUtil.stringifyPhone(phone)); + } + + //---------------- Tests for stringifyEmail ---------------------------------------------- + + @Test + public void stringifyEmail_nullEmail_throwsNullPointerException() { + Email email = null; + Assert.assertThrows(NullPointerException.class, "Email cannot be null.", () + -> ProfileUtil.stringifyEmail(email)); + } + + @Test + public void stringifyEmail_validInput_correctResult() { + Email email = new Email("test@gmail.com"); + Assertions.assertEquals("test@gmail.com", ProfileUtil.stringifyEmail(email)); + } + + //---------------- Tests for stringifyAddress -------------------------------------------- + + @Test + public void stringifyAddress_nullAddress_throwsNullPointerException() { + Address address = null; + Assert.assertThrows(NullPointerException.class, "Address cannot be null.", () + -> ProfileUtil.stringifyAddress(address)); + } + + @Test + public void stringifyAddress_validInput_correctResult() { + Address address = new Address("Address"); + Assertions.assertEquals("Address", ProfileUtil.stringifyAddress(address)); + } + + //---------------- Tests for stringifyDate ----------------------------------------------- + + @Test + public void stringifyDate_nullDate_throwsNullPointerException() { + LocalDateTime date = null; + Assert.assertThrows(NullPointerException.class, "LocalDateTime time cannot be null.", () + -> ProfileUtil.stringifyDate(date)); + } + + @Test + public void stringifyDate_validInput_correctResult() { + LocalDateTime date = LocalDateTime.of(2019, 1, 1, 0, 0, 0); + Assertions.assertEquals("01-01-2019 00-00-00", ProfileUtil.stringifyDate(date)); + } + + //---------------- Tests for stringifyVisit ---------------------------------------------- + + @Test + public void stringifyVisit_nullVisitList_correctResult() { + VisitList visitList = null; + Assertions.assertEquals("-", ProfileUtil.stringifyVisit(visitList)); + } + + @Test + public void stringifyVisit_validEmptyVisitList_correctResult() { + VisitList visitList = new VisitList(new ArrayList<>()); + Assertions.assertEquals("-", ProfileUtil.stringifyVisit(visitList)); + } + + @Test + public void stringifyVisit_validSingleVisitReportVisitList_correctResult() { + VisitList visitList = TypicalVisits.getShortTypicalVisitList("Test"); + + StringBuilder expectedOutput = new StringBuilder(); + for (VisitReport report : visitList.getRecords()) { + expectedOutput.append(ProfileUtil.BREAKLINE); + expectedOutput.append(ProfileUtil.stringifyVisitReport(report)); + expectedOutput.append("\n"); + } + + Assertions.assertEquals(expectedOutput.toString(), ProfileUtil.stringifyVisit(visitList)); + } + + @Test + public void stringifyVisit_validMultipleVisitReportVisitList_correctResult() { + VisitList visitList = TypicalVisits.getLongTypicalVisitList("Test"); + + StringBuilder expectedOutput = new StringBuilder(); + for (VisitReport report : visitList.getRecords()) { + expectedOutput.append(ProfileUtil.BREAKLINE); + expectedOutput.append(ProfileUtil.stringifyVisitReport(report)); + expectedOutput.append("\n"); + } + + Assertions.assertEquals(expectedOutput.toString(), ProfileUtil.stringifyVisit(visitList)); + } + + //---------------- Tests for stringifyVisitReport ---------------------------------------- + + @Test + public void stringifyVisitReport_nullVisitReport_correctResult() { + VisitReport visit = null; + Assert.assertThrows(NullPointerException.class, "VisitReport cannot be null.", () + -> ProfileUtil.stringifyVisitReport(visit)); + } + + @Test + public void stringifyVisitReport_validVisitReport_correctResult() { + VisitReport visit = TypicalVisits.REPORT_1; + + StringBuilder expectedOutput = new StringBuilder(); + expectedOutput.append("[ Report on the " + visit.date + " ]\n\n"); + expectedOutput.append(ProfileUtil.paragraph(ProfileUtil.HEADER_DIAG, visit.getDiagnosis())); + expectedOutput.append(ProfileUtil.paragraph(ProfileUtil.HEADER_MED, visit.getMedication())); + expectedOutput.append(ProfileUtil.paragraph(ProfileUtil.HEADER_REMARK, visit.getRemarks())); + + Assertions.assertEquals(expectedOutput.toString(), ProfileUtil.stringifyVisitReport(visit)); + } + +} diff --git a/src/test/java/unrealunity/visit/commons/util/StringUtilTest.java b/src/test/java/unrealunity/visit/commons/util/StringUtilTest.java index f61b2ab2833..0362a2cdad0 100644 --- a/src/test/java/unrealunity/visit/commons/util/StringUtilTest.java +++ b/src/test/java/unrealunity/visit/commons/util/StringUtilTest.java @@ -5,6 +5,7 @@ import java.io.FileNotFoundException; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import unrealunity.visit.testutil.Assert; @@ -142,4 +143,32 @@ public void getDetails_nullGiven_throwsNullPointerException() { Assert.assertThrows(NullPointerException.class, () -> StringUtil.getDetails(null)); } + //---------------- Tests for getDetails -------------------------------------- + + /* + * Equivalence Partitions: null, valid throwable object + */ + + @Test + public void allStringsNotNullOrBlank_nullString_correctOutput() { + String s = null; + Assertions.assertEquals(false , StringUtil.allStringsNotNullOrBlank(s)); + } + + @Test + public void allStringsNotNullOrBlank_blankString_correctOutput() { + Assertions.assertEquals(false , StringUtil.allStringsNotNullOrBlank(" ")); + Assertions.assertEquals(false , StringUtil.allStringsNotNullOrBlank("")); + } + + @Test + public void allStringsNotNullOrBlank_validString_correctOutput() { + Assertions.assertEquals(true , StringUtil.allStringsNotNullOrBlank("test")); + } + + @Test + public void allStringsNotNullOrBlank_multipleValidString_correctOutput() { + Assertions.assertEquals(true , StringUtil.allStringsNotNullOrBlank("test", "testing", "notABlank")); + } + } diff --git a/src/test/java/unrealunity/visit/logic/commands/GenerateProfileCommandTest.java b/src/test/java/unrealunity/visit/logic/commands/GenerateProfileCommandTest.java new file mode 100644 index 00000000000..8fba51d126d --- /dev/null +++ b/src/test/java/unrealunity/visit/logic/commands/GenerateProfileCommandTest.java @@ -0,0 +1,40 @@ +package unrealunity.visit.logic.commands; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +import unrealunity.visit.model.person.Person; +import unrealunity.visit.testutil.TypicalPersons; + +/** + * Contains unit tests for GenerateProfileCommand. + */ +public class GenerateProfileCommandTest { + + @Test + public void equals() { + Person firstPerson = TypicalPersons.ALICE.getClone(); + Person secondPerson = TypicalPersons.BENSON.getClone(); + final GenerateProfileCommand standardCommand = new GenerateProfileCommand(firstPerson); + + // same values -> returns true + GenerateProfileCommand commandWithSameValues = new GenerateProfileCommand(firstPerson); + assertTrue(standardCommand.equals(commandWithSameValues)); + + // same object -> returns true + assertTrue(standardCommand.equals(standardCommand)); + + // null -> returns false + assertFalse(standardCommand.equals(null)); + + // different types -> returns false + assertFalse(standardCommand.equals(new ClearCommand())); + + // different index -> returns false + GenerateProfileCommand otherCommand = new GenerateProfileCommand(secondPerson); + assertFalse(standardCommand.equals(otherCommand)); + } + +} diff --git a/src/test/java/unrealunity/visit/logic/commands/ProfileCommandTest.java b/src/test/java/unrealunity/visit/logic/commands/ProfileCommandTest.java new file mode 100644 index 00000000000..4f1e3e901f9 --- /dev/null +++ b/src/test/java/unrealunity/visit/logic/commands/ProfileCommandTest.java @@ -0,0 +1,67 @@ +package unrealunity.visit.logic.commands; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +import unrealunity.visit.commons.core.Messages; +import unrealunity.visit.commons.core.index.Index; +import unrealunity.visit.model.Model; +import unrealunity.visit.model.ModelManager; +import unrealunity.visit.model.UserPrefs; +import unrealunity.visit.testutil.TypicalIndexes; +import unrealunity.visit.testutil.TypicalPersons; + +/** + * Contains integration tests (interaction with the Person) and unit tests for GenerateProfileCommand. + */ +public class ProfileCommandTest { + + private Model model = new ModelManager(TypicalPersons.getTypicalAddressBook(), new UserPrefs()); + + @Test + public void execute_invalidPersonIndexUnfilteredList_failure() { + Index outOfBoundIndex = Index.fromOneBased(model.getFilteredPersonList().size() + 1); + ProfileCommand profileCommand = new ProfileCommand(outOfBoundIndex); + + CommandTestUtil.assertCommandFailure(profileCommand, model, Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + /** + * Edit filtered list where index is larger than size of filtered list, + * but smaller than size of address book + */ + @Test + public void execute_invalidPersonIndexFilteredList_failure() { + CommandTestUtil.showPersonAtIndex(model, TypicalIndexes.INDEX_FIRST_PERSON); + Index outOfBoundIndex = TypicalIndexes.INDEX_SECOND_PERSON; + // ensures that outOfBoundIndex is still in bounds of address book list + assertTrue(outOfBoundIndex.getZeroBased() < model.getAddressBook().getPersonList().size()); + + ProfileCommand profileCommand = new ProfileCommand(outOfBoundIndex); + CommandTestUtil.assertCommandFailure(profileCommand, model, Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + @Test + public void equals() { + final ProfileCommand standardCommand = new ProfileCommand(TypicalIndexes.INDEX_FIRST_PERSON); + + // same values -> returns true + ProfileCommand commandWithSameValues = new ProfileCommand(TypicalIndexes.INDEX_FIRST_PERSON); + assertTrue(standardCommand.equals(commandWithSameValues)); + + // same object -> returns true + assertTrue(standardCommand.equals(standardCommand)); + + // null -> returns false + assertFalse(standardCommand.equals(null)); + + // different types -> returns false + assertFalse(standardCommand.equals(new ClearCommand())); + + // different index -> returns false + assertFalse(standardCommand.equals(new ProfileCommand(TypicalIndexes.INDEX_SECOND_PERSON))); + } + +}