diff --git a/.gitignore b/.gitignore index 284c4ca7cd9..a8ac58eacae 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,6 @@ src/test/data/sandbox/ # MacOS custom attributes files created by Finder .DS_Store docs/_site/ + +# Bin File +/bin/ diff --git a/README.md b/README.md index 16208adb9b6..271b29bf5d8 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,19 @@ -[![CI Status](https://github.com/se-edu/addressbook-level3/workflows/Java%20CI/badge.svg)](https://github.com/se-edu/addressbook-level3/actions) +# MedConnect + +[![CI Status](https://github.com/AY2425S1-CS2103T-T13-1/tp/workflows/Java%20CI/badge.svg)](https://github.com/AY2425S1-CS2103T-T13-1/tp/actions) +[![codecov](https://codecov.io/gh/AY2425S1-CS2103T-T13-1/tp/graph/badge.svg)](https://codecov.io/github/AY2425S1-CS2103T-T13-1/tp) ![Ui](docs/images/Ui.png) -* This is **a sample project for Software Engineering (SE) students**.
- Example usages: - * as a starting point of a course project (as opposed to writing everything from scratch) - * as a case study -* The project simulates an ongoing software project for a desktop application (called _AddressBook_) used for managing contact details. - * It is **written in OOP fashion**. It provides a **reasonably well-written** code base **bigger** (around 6 KLoC) than what students usually write in beginner-level SE modules, without being overwhelmingly big. - * It comes with a **reasonable level of user and developer documentation**. -* It is named `AddressBook Level 3` (`AB3` for short) because it was initially created as a part of a series of `AddressBook` projects (`Level 1`, `Level 2`, `Level 3` ...). -* For the detailed documentation of this project, see the **[Address Book Product Website](https://se-education.org/addressbook-level3)**. -* This project is a **part of the se-education.org** initiative. If you would like to contribute code to this project, see [se-education.org](https://se-education.org/#contributing-to-se-edu) for more info. +**MedConnect is a desktop application for healthcare administrators in old folks homes for dementia patients to consolidate contacts of patients and related information into a single database.** + +For the detailed documentation of this project, see the **[MedConnect Product Website](https://ay2425s1-cs2103t-t13-1.github.io/tp/)**. + +* If you are interested in using MedConnect, head over to the [_Quick Start_ section of the **User Guide**](UserGuide.html#quick-start). +* If you are interested about developing MedConnect, the [**Developer Guide**](DeveloperGuide.html) is a good place to start. + +**Acknowledgements** + +This project is based on the AddressBook-Level3 project created by the [SE-EDU initiative](https://se-education.org). + +* Libraries used: [JavaFX](https://openjfx.io/), [Jackson](https://github.com/FasterXML/jackson), [JUnit5](https://github.com/junit-team/junit5) diff --git a/build.gradle b/build.gradle index 0db3743584e..b88809023a1 100644 --- a/build.gradle +++ b/build.gradle @@ -6,11 +6,18 @@ plugins { id 'jacoco' } -mainClassName = 'seedu.address.Main' +application { + mainClass = 'seedu.address.Main' +} + sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 +jacoco { + toolVersion = "0.8.8" +} + repositories { mavenCentral() maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' } @@ -20,6 +27,10 @@ checkstyle { toolVersion = '10.2' } +run { + enableAssertions = true +} + test { useJUnitPlatform() finalizedBy jacocoTestReport @@ -66,7 +77,7 @@ dependencies { } shadowJar { - archiveFileName = 'addressbook.jar' + archiveFileName = 'medconnect.jar' } defaultTasks 'clean', 'test' diff --git a/docs/AboutUs.md b/docs/AboutUs.md index ff3f04abd02..709e469542b 100644 --- a/docs/AboutUs.md +++ b/docs/AboutUs.md @@ -9,51 +9,41 @@ You can reach us at the email `seer[at]comp.nus.edu.sg` ## Project team -### John Doe +### Alden Tan - + -[[homepage](http://www.comp.nus.edu.sg/~damithch)] -[[github](https://github.com/johndoe)] -[[portfolio](team/johndoe.md)] - -* Role: Project Advisor - -### Jane Doe - - - -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](https://github.com/aldentantan)] * Role: Team Lead -* Responsibilities: UI +* Responsibilities: Deliverables and Deadlines, Scheduling and Tracking -### Johnny Doe +### Kelly Wang Sze Qing - + -[[github](http://github.com/johndoe)] [[portfolio](team/johndoe.md)] +[[github](https://github.com/kellywsq03)] * Role: Developer -* Responsibilities: Data +* Responsibilities: Testing & Code Quality -### Jean Doe +### Saajid Shaik - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](http://github.com/saajidshaik02)] * Role: Developer -* Responsibilities: Dev Ops + Threading +* Responsibilities: Documentation, SourceView Expert -### James Doe +### Wong Zhian John - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] + +[[github](http://github.com/johnwz123)] + * Role: Developer -* Responsibilities: UI +* Responsibilities: Integration + Code Quality + diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 743c65a49d2..2e144e86744 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -7,9 +7,16 @@ title: Developer Guide -------------------------------------------------------------------------------------------------------------------- +
+ ## **Acknowledgements** -* {list here sources of all reused/adapted ideas, code, documentation, and third-party libraries -- include links to the original source as well} +* This project is based on the AddressBook-Level3 project created by the SE-EDU initiative. +* Libraries used: JavaFX, Jackson, JUnit5 +* The undo and redo features were inspired by the proposed implementation found in [AB3's Developer Guide](https://se-education.org/addressbook-level3/DeveloperGuide.html#proposed-undoredo-feature). +* The icons used were taken from [Flaticon](https://www.flaticon.com/). +* Use of GPT for auto-complete tool feature (A0272009L Saajid) +> Mutliple bugs and issues due to interaction between suggestion and autocompleting -> GPT generated code which broke down current code into multiple functions and structured for individual purposes. Thereafter, the code was modified to resolved more specific exepected outcomes to suit the needs of the app. (ChatGPT makes multiple serious mistakes, it CANNOT implement this feature AT ALL without substantial amount of human intervention) -------------------------------------------------------------------------------------------------------------------- @@ -19,11 +26,27 @@ Refer to the guide [_Setting up and getting started_](SettingUp.md). -------------------------------------------------------------------------------------------------------------------- +## **Documentation, logging, testing, configuration, dev-ops** + +* [Documentation guide](Documentation.md) +* [Testing guide](Testing.md) +* [Logging guide](Logging.md) +* [Configuration guide](Configuration.md) +* [DevOps guide](DevOps.md) + +-------------------------------------------------------------------------------------------------------------------- + +
+ ## **Design**
-:bulb: **Tip:** The `.puml` files used to create diagrams in this document `docs/diagrams` folder. Refer to the [_PlantUML Tutorial_ at se-edu/guides](https://se-education.org/guides/tutorials/plantUml.html) to learn how to create and edit diagrams. +:bulb: **Tip:** The `.puml` files used to create diagrams in this document can be found in the `docs/diagrams` folder. Refer to the [_PlantUML Tutorial_ at se-edu/guides](https://se-education.org/guides/tutorials/plantUml.html) to learn how to create and edit diagrams. +
+ +
+:information_source: **Note:** Due to a limitation of PlantUML, the destroy marker (X) for lifelines in sequence diagrams cannot be displayed at the correct position. As a workaround, the lifelines are extended to the end of the diagram.
### Architecture @@ -32,11 +55,13 @@ Refer to the guide [_Setting up and getting started_](SettingUp.md). The ***Architecture Diagram*** given above explains the high-level design of the App. +
+ Given below is a quick overview of main components and how they interact with each other. **Main components of the architecture** -**`Main`** (consisting of classes [`Main`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/Main.java) and [`MainApp`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/MainApp.java)) is in charge of the app launch and shut down. +**`Main`** (consisting of classes [`Main`](https://github.com/AY2425S1-CS2103T-T13-1/tp/blob/master/src/main/java/seedu/address/Main.java) and [`MainApp`](https://github.com/AY2425S1-CS2103T-T13-1/tp/blob/master/src/main/java/seedu/address/MainApp.java)) is in charge of the app launch and shut down. * At app launch, it initializes the other components in the correct sequence, and connects them up with each other. * At shut down, it shuts down the other components and invokes cleanup methods where necessary. @@ -58,7 +83,7 @@ The *Sequence Diagram* below shows how the components interact with each other f Each of the four main components (also shown in the diagram above), * defines its *API* in an `interface` with the same name as the Component. -* implements its functionality using a concrete `{Component Name}Manager` class (which follows the corresponding API `interface` mentioned in the previous point. +* implements its functionality using a concrete `{Component Name}Manager` class (which follows the corresponding API `interface` mentioned in the previous point). For example, the `Logic` component defines its API in the `Logic.java` interface and implements its functionality using the `LogicManager.java` class which follows the `Logic` interface. Other components interact with a given component through its interface rather than the concrete class (reason: to prevent outside component's being coupled to the implementation of a component), as illustrated in the (partial) class diagram below. @@ -66,31 +91,37 @@ For example, the `Logic` component defines its API in the `Logic.java` interface The sections below give more details of each component. +
+ ### UI component -The **API** of this component is specified in [`Ui.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/ui/Ui.java) +The **API** of this component is specified in [`Ui.java`](https://github.com/AY2425S1-CS2103T-T13-1/tp/blob/master/src/main/java/seedu/address/ui/Ui.java) ![Structure of the UI Component](images/UiClassDiagram.png) The UI consists of a `MainWindow` that is made up of parts e.g.`CommandBox`, `ResultDisplay`, `PersonListPanel`, `StatusBarFooter` etc. All these, including the `MainWindow`, inherit from the abstract `UiPart` class which captures the commonalities between classes that represent parts of the visible GUI. -The `UI` component uses the JavaFx UI framework. The layout of these UI parts are defined in matching `.fxml` files that are in the `src/main/resources/view` folder. For example, the layout of the [`MainWindow`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/ui/MainWindow.java) is specified in [`MainWindow.fxml`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/resources/view/MainWindow.fxml) +The `UI` component uses the JavaFx UI framework. The layout of these UI parts are defined in matching `.fxml` files that are in the `src/main/resources/view` folder. For example, the layout of the [`MainWindow`](https://github.com/AY2425S1-CS2103T-T13-1/tp/blob/master/src/main/java/seedu/address/ui/MainWindow.java) is specified in [`MainWindow.fxml`](https://github.com/AY2425S1-CS2103T-T13-1/tp/blob/master/src/main/resources/view/MainWindow.fxml) -The `UI` component, +The `UI` component: * executes user commands using the `Logic` component. * listens for changes to `Model` data so that the UI can be updated with the modified data. * keeps a reference to the `Logic` component, because the `UI` relies on the `Logic` to execute commands. * depends on some classes in the `Model` component, as it displays `Person` object residing in the `Model`. +
+ ### Logic component -**API** : [`Logic.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/logic/Logic.java) +**API** : [`Logic.java`](https://github.com/AY2425S1-CS2103T-T13-1/tp/blob/master/src/main/java/seedu/address/logic/Logic.java) Here's a (partial) class diagram of the `Logic` component: +
+ The sequence diagram below illustrates the interactions within the `Logic` component, taking `execute("delete 1")` API call as an example. ![Interactions Inside the Logic Component for the `delete 1` Command](images/DeleteSequenceDiagram.png) @@ -106,6 +137,8 @@ How the `Logic` component works: Note that although this is shown as a single step in the diagram above (for simplicity), in the code it can take several interactions (between the command object and the `Model`) to achieve. 1. The result of the command execution is encapsulated as a `CommandResult` object which is returned back from `Logic`. +
+ Here are the other classes in `Logic` (omitted from the class diagram above) that are used for parsing a user command: @@ -114,12 +147,13 @@ How the parsing works: * When called upon to parse a user command, the `AddressBookParser` class creates an `XYZCommandParser` (`XYZ` is a placeholder for the specific command name e.g., `AddCommandParser`) which uses the other classes shown above to parse the user command and create a `XYZCommand` object (e.g., `AddCommand`) which the `AddressBookParser` returns back as a `Command` object. * All `XYZCommandParser` classes (e.g., `AddCommandParser`, `DeleteCommandParser`, ...) inherit from the `Parser` interface so that they can be treated similarly where possible e.g, during testing. +
+ ### Model component -**API** : [`Model.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/model/Model.java) +**API** : [`Model.java`](https://github.com/AY2425S1-CS2103T-T13-1/tp/blob/master/src/main/java/seedu/address/model/Model.java) - The `Model` component, * stores the address book data i.e., all `Person` objects (which are contained in a `UniquePersonList` object). @@ -127,16 +161,19 @@ The `Model` component, * stores a `UserPref` object that represents the user’s preferences. This is exposed to the outside as a `ReadOnlyUserPref` objects. * does not depend on any of the other three components (as the `Model` represents data entities of the domain, they should make sense on their own without depending on other components) +
+
:information_source: **Note:** An alternative (arguably, a more OOP) model is given below. It has a `Tag` list in the `AddressBook`, which `Person` references. This allows `AddressBook` to only require one `Tag` object per unique tag, instead of each `Person` needing their own `Tag` objects.
+
### Storage component -**API** : [`Storage.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/storage/Storage.java) +**API** : [`Storage.java`](https://github.com/AY2425S1-CS2103T-T13-1/tp/blob/master/src/main/java/seedu/address/storage/Storage.java) @@ -151,13 +188,65 @@ Classes used by multiple components are in the `seedu.address.commons` package. -------------------------------------------------------------------------------------------------------------------- +
+ ## **Implementation** This section describes some noteworthy details on how certain features are implemented. -### \[Proposed\] Undo/redo feature +### Add feature + +#### Implementation + +The image below shows the class diagram of a Person object and its related class attributes. + +![Person Class Diagram](images/PersonClassDiagram.png) + +The Person object is made up of several attributes: +* `Name`: The name of the patient. +* `Phone`: The phone number of the patient. +* `Email`: The email of the patient. +* `Address`: The address of the patient. +* `Doctor`: The doctor assigned to the patient. +* `Emergency Contact`: A list of emergency contacts of the patient. +* `Tags`: Additional information about the patient. + +The Doctor object is also made up of attributes: +* `Doctor Name`: The name and title of the doctor. +* `Phone`: The phone number of the doctor. +* `Email`: The email of the doctor. + +The Emergency Contact object is also made up of attributes: +* `Name`: The name of the emergency contact. +* `Phone`: The phone number of the emergency contact. +* `Relationship`: The relationship of the emergency contact to the patient. + +
-#### Proposed Implementation +#### Feature details + +1. MedConnect will verify that the parameters supplied by the user follow a set of relevant restrictions for the respective parameters. +2. If any invalid parameter is provided, an error will be thrown, informing the user which parameter violates the restrictions. The format for the valid input for that parameter will be displayed to the user. +3. If all parameters are valid, a new `Person` entry will be created and stored in the `VersionedAddressBook`. + +#### Design Considerations: + +**Aspect: The required input of parameters:** + +* **Alternative 1 (current choice):** Make all parameters compulsory, except Tags. + * Pros: Will not have missing data when it is needed in an emergency. + * Cons: Add Command is lengthy to type out, might be hard to remember the syntax. +* **Alternative 2:** Make only a few specific parameters compulsory. + * Pros: Patient registration will be faster. + * Cons: If user forgets to update missing details, during an emergency there might not be an emergency contact to call. + +We opted for Alternative 1 to make almost all parameters compulsory as the autocomplete feature we implemented will aid users in typing out the Add Command. + +
+ +### Undo/redo feature + +#### Implementation The proposed undo/redo mechanism is facilitated by `VersionedAddressBook`. It extends `AddressBook` with an undo/redo history, stored internally as an `addressBookStateList` and `currentStatePointer`. Additionally, it implements the following operations: @@ -173,6 +262,8 @@ Step 1. The user launches the application for the first time. The `VersionedAddr ![UndoRedoState0](images/UndoRedoState0.png) +
+ Step 2. The user executes `delete 5` command to delete the 5th person in the address book. The `delete` command calls `Model#commitAddressBook()`, causing the modified state of the address book after the `delete 5` command executes to be saved in the `addressBookStateList`, and the `currentStatePointer` is shifted to the newly inserted address book state. ![UndoRedoState1](images/UndoRedoState1.png) @@ -185,6 +276,8 @@ Step 3. The user executes `add n/David …​` to add a new person. The `add` co +
+ Step 4. The user now decides that adding the person was a mistake, and decides to undo that action by executing the `undo` command. The `undo` command will call `Model#undoAddressBook()`, which will shift the `currentStatePointer` once to the left, pointing it to the previous address book state, and restores the address book to that state. ![UndoRedoState3](images/UndoRedoState3.png) @@ -194,6 +287,8 @@ than attempting to perform the undo. +
+ The following sequence diagram shows how an undo operation goes through the `Logic` component: ![UndoSequenceDiagram](images/UndoSequenceDiagram-Logic.png) @@ -220,6 +315,8 @@ Step 6. The user executes `clear`, which calls `Model#commitAddressBook()`. Sinc ![UndoRedoState5](images/UndoRedoState5.png) +
+ The following activity diagram summarizes what happens when a user executes a new command: @@ -237,98 +334,656 @@ The following activity diagram summarizes what happens when a user executes a ne * Pros: Will use less memory (e.g. for `delete`, just save the person being deleted). * Cons: We must ensure that the implementation of each individual command are correct. -_{more aspects and alternatives to be added}_ +
-### \[Proposed\] Data archiving +### Data archiving -_{Explain here how the data archiving feature will be implemented}_ +#### Implementation +The archive functionality in MedConnect is facilitated by the `ModelManager` class. It handles the archiving, listing, loading, and deleting of archived contact data. The `ModelManager` interacts with the `Filename` class and `FileUtil` components to manage the archive files in the archive directory. --------------------------------------------------------------------------------------------------------------------- +For example, the sequence diagram below illustrates the interactions within the `ModelManager` component when the `archive` command is executed. -## **Documentation, logging, testing, configuration, dev-ops** +![ArchiveSequenceDiagram.png](images%2FArchiveSequenceDiagram.png) -* [Documentation guide](Documentation.md) -* [Testing guide](Testing.md) -* [Logging guide](Logging.md) -* [Configuration guide](Configuration.md) -* [DevOps guide](DevOps.md) +1. The `ArchiveCommand` archives the current address book data by calling the `archiveAddressBook` method in the `ModelManager` component. +2. The `ModelManager` creates the archive directory if it does not exist. +3. The `ModelManager` saves the current address book data to a JSON file in the archive directory with the specified file name. -------------------------------------------------------------------------------------------------------------------- +
+ ## **Appendix: Requirements** -### Product scope +### Product Scope + +#### Target User Profile + +- **User Role:** Healthcare Administrator +- **Workplace:** Elderly care home for dementia patients +- **Responsibilities:** + - Manage and update contact details for patients, doctors, and next-of-kin. + - Respond quickly to emergency situations by accessing relevant contacts. + - Maintain communication efficiency with minimal manual processes. -**Target user profile**: +- **Key Characteristics:** + - Handles large volumes of contact data on a daily basis. + - Prefers using desktop applications over mobile or web-based alternatives. + - Skilled at typing and prefers keyboard shortcuts over mouse interactions for speed. + - Comfortable with using command-line interfaces (CLI) for fast data entry and retrieval. -* has a need to manage a significant number of contacts -* prefer desktop apps over other types -* can type fast -* prefers typing to mouse interactions -* is reasonably comfortable using CLI apps +#### Value Proposition -**Value proposition**: manage contacts faster than a typical mouse/GUI driven app +MedConnect offers a **streamlined contact management system** tailored for healthcare administrators. Its key features include: +- **Efficient Lookup and Update:** Quickly find and update contact information for patients, their emergency contacts, and healthcare staff. +- **Time-Sensitive Operations:** When every second counts, MedConnect ensures that administrators can contact the right person immediately. +- **Command-Line First:** Optimized for users who prefer CLI, allowing for rapid data entry and navigation without reliance on graphical interfaces. +- **Comprehensive Contact Database:** Centralizes all relevant contact details, reducing the need for multiple systems and improving response times in emergencies. + +--- + +
### User stories Priorities: High (must have) - `* * *`, Medium (nice to have) - `* *`, Low (unlikely to have) - `*` -| Priority | As a …​ | I want to …​ | So that I can…​ | -| -------- | ------------------------------------------ | ------------------------------ | ---------------------------------------------------------------------- | -| `* * *` | new user | see usage instructions | refer to instructions when I forget how to use the App | -| `* * *` | user | add a new person | | -| `* * *` | user | delete a person | remove entries that I no longer need | -| `* * *` | user | find a person by name | locate details of persons without having to go through the entire list | -| `* *` | user | hide private contact details | minimize chance of someone else seeing them by accident | -| `*` | user with many persons in the address book | sort persons by name | locate a person easily | +#### Beginner User Stories + +| Priority | As a …​ | I want to …​ | So that I can…​ | +|----------|--------------------------|-------------------------------------------------|--------------------------------------------------------------------------------| +| `* * *` | new user | have sample data to work with | understand how to use the application | +| `* * *` | healthcare administrator | add new doctors and patients | easily reach out to them when needed | +| `* * *` | healthcare administrator | update contact details | ensure all contact information is accurate and current | +| `* * *` | healthcare administrator | delete outdated patient contacts | ensure all information is relevant and current | +| `* * *` | healthcare administrator | view all contacts in the address book | have a comprehensive overview of all patients, doctors, and emergency contacts | +| `* * *` | healthcare administrator | view patient emergency contact details | notify next-of-kin during urgent medical events | +| `* * *` | healthcare administrator | add emergency contacts for patients | quickly reach out to next-of-kin during medical emergencies | +| `* * *` | healthcare administrator | assign doctors to patients | easily track which doctor is responsible for each patient | +| `* *` | healthcare administrator | search contacts by name or assigned doctor | quickly find and connect with the right person in high-pressure situations | +| `* *` | healthcare administrator | add multiple emergency contacts for each person | reach different emergency contacts when one is uncontactable | + + -*{More to be added}* +#### Intermediate User Stories + +| Priority | As a … | I want to … | So that I can… | +|----------|--------------------------|---------------------------------------|------------------------------------------------------------------------------| +| `* *` | healthcare administrator | filter contacts by their doctor | view a consolidated list of all the patients a doctor is responsible for | +| `* *` | healthcare administrator | sort patients by their admission time | provide appropriate care to longer-term patients | +| `* *` | healthcare administrator | tag important notes to patients | remember special considerations about certain patients | +| `* *` | healthcare administrator | archive outdated contacts | maintain a clean and relevant contact list without losing historical records | +| `* *` | healthcare administrator | load backup archived data | restore a backup copy in case of data corruption or user error | +| `* *` | healthcare administrator | delete archived data | free up storage space and remove unnecessary or outdated contact information | +| `* *` | healthcare administrator | view a list of all archived data | keep track of the archived data for reference or auditing purposes | +| `* *` | healthcare administrator | undo the last operation | recover from accidental deletions or modifications | +| `* *` | healthcare administrator | redo the last undone operation | reverse an undo operation if it was done in error | + +
+ +#### Advanced User Stories + +| Priority | As a … | I want to … | So that I can… | +|----------|--------------------------|-----------------------------------------------|----------------------------------------------------------------------| +| `* *` | healthcare administrator | import contact data in bulk | keep the database up-to-date without manual entry | +| `* *` | healthcare administrator | export contact information | provide it to others or have a backup in case of system failures | + +--- + +
### Use cases +Not all commands use cases are included as commands, such as `clear` and `exit` are self-explanatory. + (For all use cases below, the **System** is the `AddressBook` and the **Actor** is the `user`, unless specified otherwise) -**Use case: Delete a person** +#### Use Case: Add a New Contact + +**System:** MedConnect + +**Actor:** Healthcare Administrator + +**Main Success Scenario (MSS):** +1. User requests to add a new patient contact. +2. MedConnect prompts the user to enter patient details: + - Name + - Phone Number + - Address + - Email + - Doctor Name + - Doctor Phone Number + - Doctor Email + - Emergency Contact Name + - Emergency Contact Phone Number + - Emergency Contact Relationship to Patient + - Tag(s) [Optional] +3. User enters the required details. +4. MedConnect validates the provided information. +5. MedConnect successfully adds the new contact to the address book. +6. MedConnect confirms that the contact was added successfully. + + **Use case ends.** + +
+ +**Extensions:** + +**3a.** The entered details are invalid (e.g., phone number contains letters). +- **3a1.** MedConnect informs the user of the invalid details. +- **3a2.** User corrects the invalid details and resubmits. + + **Use case resumes from step 4.** + +**3b.** Some required fields are missing. +- **3b1.** MedConnect prompts the user to complete the missing fields. +- **3b2.** User provides the missing information. + + **Use case resumes from step 4.** + +**3c.** There are duplicate fields provided (e.g., 2 name fields). +- **3c1.** MedConnect informs the user of the duplicate field(s). +- **3c2.** User removes the duplicate parameters and resubmits. + + **Use case resumes from step 4.** + +**6a.** The contact already exists in the system (e.g., duplicate phone number). +- **6a1.** MedConnect notifies the user of the duplicate entry. + + **Use case ends.** + +--- + +
+ +#### Use Case: Edit a Contact + +**System:** MedConnect + +**Actor:** Healthcare Administrator + + +**Main Success Scenario (MSS):** +1. User requests to list all contacts. +2. MedConnect retrieves and shows a list of all contacts. +3. User requests to edit a patient's details. +4. MedConnect prompts the user to enter the following details: + - Index of the patient to be edited + - New data of the field(s) to be edited +5. MedConnect validates the provided information. +6. MedConnect successfully edits the contact with the new data. +7. MedConnect confirms that the contact was edited successfully. + + **Use case ends.** + +**Extensions:** + +**2a.** The patient list is empty. + + - **Use case ends.** + +**4a.** The given index is invalid (e.g., out of range). +- **4a1.** MedConnect informs the user of the invalid index. + + **Use case resumes from step 3.** + +**4b.** The entered details are invalid (e.g., phone number contains letters). +- **4b1.** MedConnect informs the user of the invalid details. +- **4b2.** User corrects the invalid details and resubmits. + + **Use case resumes from step 5.** + +--- + +
+ +#### Use Case: Delete a Contact + +**System:** MedConnect + +**Actor:** Healthcare Administrator + + +**Main Success Scenario (MSS):** +1. User requests to list all contacts. +2. MedConnect retrieves and shows a list of all contacts. +3. User requests to delete a specific contact by its index. +4. MedConnect removes the contact from the database. + + **Use case ends.** + +**Extensions:** + +**2a.** The contact list is empty. + + +- **Use case ends.** + + +**3a.** The given index is invalid (e.g., out of range). +- **3a1.** MedConnect informs the user of the invalid index. + + **Use case resumes from step 2.** + +--- + +
+ +#### Use Case: Add Emergency Contacts + +**System:** MedConnect + +**Actor:** Healthcare Administrator + +**Main Success Scenario (MSS):** +1. User requests to list all contacts. +2. MedConnect retrives and shows a list of all contacts. +3. User requests to add a new emergency contact to a patient. +4. MedConnect prompts the user to enter the following details: + - Index of the patient + - Emergency Contact Name + - Emergency Contact Phone Number + - Emergency Contact Relationship to Patient +5. User enters the required details. +6. MedConnect adds the emergency contact to the patient's details. + + **Use case ends.** + +**Extensions:** + +**2a.** The contact list is empty. + +- **Use case ends.** + +**5a.** The index given is out of range. +- **5a1.** MedConnect informs the user of the invalid index. + + **Use case resumes from step 3.** + +**5b.** The emergency contact already exists under the patient. (e.g., duplicate phone number). +- **5b1.** MedConnect notifies the user that the emergency contact already exists. + + **Use case ends.** + +**5c.** The emergency contact relationship provided is invalid. +- **5c1.** MedConnect informs the user of possible relationship types. +- **5c2.** User corrects the invalid relationship type and resubmits. + + **Use case resumes from step 6.** + +--- + +#### Use Case: Delete an Emergency Contact + +**System:** MedConnect + +**Actor:** Healthcare Administrator + + +**Main Success Scenario (MSS):** +1. User requests to list all contacts. +2. MedConnect retrieves and shows a list of all contacts. +3. User requests to delete a specific emergency contact of a patient by its index. +4. MedConnect removes the emergency contact from the database. + + **Use case ends.** + + +**Extensions:** + +**2a.** The contact list is empty. + +- **Use case ends.** + + +**3a.** The given index is invalid (e.g., out of range). +- **3a1.** MedConnect informs the user of the invalid index. + + **Use case resumes from step 2.** + +**3b.** The specified patient only has 1 emergency contact. +- **3b1.** MedConnect informs the user that they cannot delete a patient's only emergency contact. + + **Use case ends.** + +--- + +
+ +#### Use Case: Find Contacts By Patient Name + +**System:** MedConnect + +**Actor:** Healthcare Administrator + +**Main Success Scenario (MSS):** +1. User requests to find a patient by their name. +2. MedConnect prompts the user to provide a name to search for. +3. User provides a name. +4. MedConnect returns a list of patients who matches the provided name. + + **Use case ends.** -**MSS** +**Extensions:** -1. User requests to list persons -2. AddressBook shows a list of persons -3. User requests to delete a specific person in the list -4. AddressBook deletes the person +**3a.** User provides a blank name. +- **3a1.** MedConnect notifies the user to provide a name. - Use case ends. + **Use case resumes from step 3.** -**Extensions** +**4a.** There are no patients who match the provided name. +- **4a1.** MedConnect returns a list of 0 patients. -* 2a. The list is empty. + **Use case ends.** - Use case ends. +--- + +
+ +#### Use Case: Find Contacts By Doctor Name + +**System:** MedConnect + +**Actor:** Healthcare Administrator + +**Main Success Scenario (MSS):** +1. User requests to find a patient by their assigned doctor's name. +2. MedConnect prompts the user to provide a name to search for. +3. User provides a name. +4. MedConnect returns a list of patients whose assigned doctor matches the provided name. + + **Use case ends.** + +**Extensions:** + +**3a.** User provides a blank name. +- **3a1.** MedConnect notifies the user to provide a name. + + **Use case resumes from step 3.** + +**4a.** There are no patients whose doctor matches the provided name. +- **4a1.** MedConnect returns a list of 0 patients. + + **Use case ends.** + +--- + +
+ +#### Use Case: Archive Contacts + +**System:** MedConnect + +**Actor:** Healthcare Administrator + +**Main Success Scenario (MSS):** +1. User requests to archive the address book with a description. +2. MedConnect confirms that the contact data has been successfully archived. + + **Use case ends.** + +**Extensions:** + +**1a.** The given description is invalid. +- **1a1.** MedConnect informs the user of the invalid description. +- **1a2.** User corrects the invalid description and resubmits. + + **Use case resumes from step 2.** + +--- + +#### Use Case: List Archive Files + +**System:** MedConnect + +**Actor:** Healthcare Administrator + +**Main Success Scenario (MSS):** +1. User requests to list all archived data files in the archive folder. +2. MedConnect returns a list of all the archived data files in the archive folder. + + **Use case ends.** + +--- + +
+ +#### Use Case: Load Archived Contacts + +**System:** MedConnect + +**Actor:** Healthcare Administrator + +**Main Success Scenario (MSS):** +1. User requests to load an archive file. +2. MedConnect prompts the user to provide the file name of an archive file in the archives folder. +3. User enters a file name. +4. MedConnect validates the file name and checks for a matching file. +5. MedConnect loads the archived contacts from the file into the address book. + + **Use case ends.** + +**Extensions:** + +**3a.** The given file name is invalid. +- **3a1.** MedConnect notifies the user that the file name contains invalid characters. +- **3a2.** User corrects the file name and resubmits. + + **Use case resumes from step 4.** + +**3b.** The given file name does not match any existing file. +- **3b1.** MedConnect notifies the user that a file with the given file name is not found. +- **3b2.** User enters the file name of an existing file in the archives folder and resubmits. + + **Use case resumes from step 4.** + +--- + +
+ +#### Use Case: Delete Archive File + +**System:** MedConnect + +**Actor:** Healthcare Administrator + +**Main Success Scenario (MSS):** +1. User requests to delete an archive file. +2. MedConnect prompts the user to provide the file name of an archive file in the archives folder. +3. User enters a file name. +4. MedConnect validates the file name and checks for a matching file. +5. MedConnect deletes the archive file. + + **Use case ends.** + +**Extensions:** + +**3a.** The given file name is invalid. +- **3a1.** MedConnect notifies the user that the file name contains invalid characters. +- **3a2.** User corrects the file name and resubmits. + + **Use case resumes from step 4.** + +**4a.** The given file name does not match any existing file. +- **4a1.** MedConnect notifies the user that a file with the given file name is not found. +- **4a2.** User enters the file name of an existing file in the archives folder and resubmits. + + **Use case resumes from step 4.** + +--- + +
+ +#### Use Case: Undo Last Command + +**System:** MedConnect + +**Actor:** Healthcare Administrator + +**Main Success Scenario (MSS):** +1. User performs an command that modifies the address book (e.g., adds or deletes a contact). +2. User requests to undo the last command. +3. MedConnect reverts to the state before the last command. + + **Use case ends.** + +**Extensions:** + +**2a.** There is no previous command to undo. +- **2a1.** MedConnect informs the user that there are no actions to undo. + + **Use case ends.** + +--- + +
+ +#### Use Case: Redo Last Undone Command + +**System: MedConnect** + +**Actor: Healthcare Administrator** + +**Main Success Scenario (MSS):** +1. User performs an undo command. +2. User requests to redo the last undone action. +3. MedConnect restores the previously undone action. + + **Use case ends.** + +**Extensions:** + +**2a.** There is no undone command to redo. +- **2a1.** MedConnect informs the user that there are no actions to redo. -* 3a. The given index is invalid. + **Use case ends.** - * 3a1. AddressBook shows an error message. +**2b.** User makes a new change after an undo command. +- **2b1.** MedConnect restores the undo command that the user most recently executed. - Use case resumes at step 2. + **Use case ends.** -*{More to be added}* +
-### Non-Functional Requirements +### Non-Functional Requirements (NFRs) -1. Should work on any _mainstream OS_ as long as it has Java `17` or above installed. -2. Should be able to hold up to 1000 persons without a noticeable sluggishness in performance for typical usage. -3. A user with above average typing speed for regular English text (i.e. not code, not system admin commands) should be able to accomplish most of the tasks faster using commands than using the mouse. +#### 1. Performance Requirements: +- **Responsiveness:** + The system should respond to user commands (e.g., adding, updating, or viewing contacts) within **2 seconds** for typical operations under normal usage conditions (i.e., up to 1000 contacts in the database). -*{More to be added}* +- **Scalability:** + MedConnect should support operations with **up to 5000 contacts** without performance degradation. Basic operations such as retrieving or adding contacts should not exceed a response time of **3 seconds** under this load. + +#### 2. Reliability and Availability: +- **System Uptime:** + MedConnect must be available for use at least **99% of the time**, especially during hospital operating hours (24/7 access). Regular maintenance should be scheduled during off-peak times. + +- **Disaster Recovery and Backup:** + Contact data must be backed up **daily** to prevent data loss. The system should be able to recover from backup within **2 hours** of a failure. + +#### 3. Usability Requirements: +- **Typing Efficiency:** + The system should be optimized for keyboard-only interactions. A user familiar with the system should be able to complete key operations (e.g., adding a new contact, viewing emergency contacts) in less than **30 seconds** using only the keyboard. + +- **Error Handling and Feedback:** + The system must provide **immediate feedback** (within 1 second) when an error occurs, such as invalid input or missing fields. The user should be able to correct errors without restarting the operation. + +#### 4. Data and Storage Requirements: +- **Human-Editable File Format:** + Contact information should be stored in a **human-readable and editable format** (e.g., `.json` or `.csv`) so that administrators can manually access and modify data if needed. + +- **Data Integrity:** + The system must ensure that no data is lost or corrupted during common operations (e.g., adding, updating, or deleting contacts). **Transaction-like behavior** must be implemented to ensure all data operations either succeed fully or fail without partially corrupting data. + +#### 5. Compatibility and Portability: +- **Cross-Platform Support:** + MedConnect must be compatible with **mainstream operating systems** (Windows, macOS, Linux) and function seamlessly on systems with **Java 17 or higher** installed. + +#### 6. Maintainability and Extensibility: +- **Modular Design:** + The system must be designed with a modular structure, allowing future extensions such as additional data fields or user roles without requiring significant rework. + +- **Testability:** + MedConnect must be **easily testable**, with automated tests that can cover at least **70% of the codebase**. Each core feature (e.g., adding a contact, deleting outdated contacts) should have dedicated test cases. +--- + +
### Glossary -* **Mainstream OS**: Windows, Linux, Unix, MacOS -* **Private contact detail**: A contact detail that is not meant to be shared with others +- **Mainstream OS**: Windows, Linux, Unix, MacOS --------------------------------------------------------------------------------------------------------------------- +- **MedConnect**: A healthcare application designed to help healthcare administrators manage patient and staff contact information efficiently. + +- **Healthcare Administrator**: The primary user of MedConnect, responsible for managing patient and staff contact details in a healthcare environment. They ensure that the contact information is up-to-date for communication during critical situations. + +- **Emergency Contact**: The designated person or persons to be notified in case of a patient emergency, usually including details such as their name, phone number, and relationship to the patient. + +- **Primary Contact Method**: The main communication method for a person in the system, typically used for emergency situations. + +- **Command-Line Interface (CLI)**: A method of interacting with MedConnect through typed text commands, allowing fast input for users who prefer typing over graphical interfaces. + +- **Mainstream Operating Systems**: Common operating systems on which MedConnect can run, including Windows, macOS, and Linux. + +- **Encryption**: The process of encoding sensitive data, such as patient information, to protect it from unauthorized access. + +- **Java 17**: The version of Java required to run MedConnect, which ensures compatibility and performance across different operating systems. + +- **Human-Editable File**: A data file format (e.g., `.json` or `.csv`) that can be easily accessed and modified by healthcare administrators without needing special software. + +- **System Uptime**: The percentage of time that MedConnect is available and operational, measured as part of reliability goals. + +- **Backup**: The process of creating copies of MedConnect's data to ensure it can be restored in the event of data loss or system failure. + +- **Test Coverage**: The percentage of the system's code that is covered by automated tests, ensuring that key features and functionality are reliably tested. + +
+ +## **Appendix: Planned Enhancements** + +Team size: 4 + +The current version of MedConnect has its flaws so here are our plans for future enhancements to improve future versions of MedConnect. + +### Optimisations to handle large number of contacts + +Currently, depending on the limitations of the PC hardware that MedConnect is running on, a large number of contacts may cause an OutOfMemory error. Our planned enhancement is to optimise the data storage and retrieval process to handle a larger number of contacts without causing performance issues or memory errors, or implement a pagination feature to display contacts in smaller batches. We plan to also implement an import and export feature to allow users to import and export contacts in bulk to avoid the need to manually enter each contact. + +### Duplicate detection + +Currently, MedConnect's duplicate person detection only works within each class. There is no duplicate person detection between a patient and a doctor. In reality, there should likely not be a case where a patient is also a doctor. We plan to implement duplicate person detection across classes to prevent or display a warning message to prevent a person from being added as both a patient and a doctor, for instance, in the future. + +### Whitespaces in names + +Currently, MedConnect is able to remove leading and trailing whitespaces from names. However, the functionality to remove whitespaces in between words in a name is not yet implemented. We plan to implement this in the future to prevent users from entering names with excessive whitespaces between words in the future as it may reduce readability. + +
+ +### Handling long fields + +Currently, MedConnect does not handle long fields well. For example, if a user enters a long name, the name may be cut off in the GUI. Our planned enhancement is to implement a feature that allows users to view the full details of each field by hovering over the field in the GUI or clicking to expand the details for the field in the card. This will allow users to view the full field (e.g. full name) should the field be too long to be displayed in the GUI. + +### Multiple Language Support + +Currently, MedConnect is only available for usage in English. We recognise that our target users may not be able to read English proficiently or require non-English inputs (e.g. Chinese names). Our planned enhancement is to translate MedConnect into other languages, such as Chinese, Malay and Tamil to accommodate for healthcare administrators who are more fluent in these languages and require such languages to be supported in the application. + +### Emergency Contact UI + +Currently, clicking on a emergency contact card of a patient in the GUI, followed by clicking the same patient card results in the emergency contact card being unselected. This behavior is not ideal for users who select the card to focus on viewing the correct contact in the list. Our planned enhancement is to update the behaviour of selecting the patient card so that it will not refresh the user's selection upon clicking it. + +### Autocomplete field suggestion + +Currently, due to difficulties with parsing, the autocomplete feature does not suggest square brackets for optional fields, such as in the Edit command. Users would have to refer to the User Guide or error message to know which fields are optional. We plan to add the square brackets to clearly indicate optional parameters in the autocomplete feature in future iterations of MedConnect to minimise the need for users to continuously reference the User Guide. + +
+ +### Autocomplete dynamic parsing + +Currently, the autocomplete feature does not dynamically parse the user's input for each parameter as it is entered to provide the next suggestion. For example, the autocomplete feature would not function ideally if users enter a whitespace after command prefixes (e.g., `add n/ John` would continue to suggest `add n/ John (n/NAME)`). This also limits us to keep to the specified default order of parameters. We plan to implement dynamic parsing in the autocomplete feature to provide more accurate suggestions based on the user's input in future iterations of MedConnect, particularly for optional parameters. This would also improve the user experience by allowing us to carry out input validation for each parameter and provide feedback to the user in real-time without the need to submit the command. + +
## **Appendix: Instructions for manual testing** @@ -343,9 +998,11 @@ testers are expected to do more *exploratory* testing. 1. Initial launch - 1. Download the jar file and copy into an empty folder + 1. Download the latest jar file [here](https://github.com/AY2425S1-CS2103T-T13-1/tp/releases) and copy into an empty folder. + + 1. Open a terminal window and `cd` into the same folder. - 1. Double-click the jar file Expected: Shows the GUI with a set of sample contacts. The window size may not be optimum. + 1. Enter `java -jar medconnect.jar` into the terminal. Expected: Shows the GUI with a set of sample contacts. The window size may not be optimum but it is resizable. 1. Saving window preferences @@ -354,29 +1011,149 @@ testers are expected to do more *exploratory* testing. 1. Re-launch the app by double-clicking the jar file.
Expected: The most recent window size and location is retained. -1. _{ more test cases …​ }_ +1. Shutdown -### Deleting a person + There are multiple ways to exit the application: -1. Deleting a person while all persons are being shown + 1. Use the `exit` command. - 1. Prerequisites: List all persons using the `list` command. Multiple persons in the list. + 1. Click `File` in the top left corner, then `Exit` in the dropdown menu. - 1. Test case: `delete 1`
- Expected: First contact is deleted from the list. Details of the deleted contact shown in the status message. Timestamp in the status bar is updated. + 1. Click the red 'X' of the application window. - 1. Test case: `delete 0`
- Expected: No person is deleted. Error details shown in the status message. Status bar remains the same. + 1. Use the keyboard shortcut `Alt + F4`. - 1. Other incorrect delete commands to try: `delete`, `delete x`, `...` (where x is larger than the list size)
- Expected: Similar to previous. +
-1. _{ more test cases …​ }_ +### Adding a patient + +1. Adding a patient while any number of patients are being shown. +
+ **Note:** Due to the long length of valid `add` commands, the usage of `xx/PARAMETER...` will refer to all remaining compulsory parameters that have not been mentioned for that test case, with valid inputs for the respective parameters.
+ **Prerequisites:** List all patients using the `list` command. Patients are sorted by the time they were added to MedConnect. +
+ + | Test case input | Expected behaviour | Expected message | + |--------------------------------------------------|--------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------| + | `add n/Ryan p/98765432 xx/PARAMETER...` | A new patient Ryan is added to the bottom of the patient list. | New person added: [PERSON DETAILS] | + | `add n/Ryan n/Daniel p/98765432 xx/PARAMETER...` | Error message is shown. | Multiple values specified for the following single-valued field(s): n/ | + | `add n/Ryan` | Error message is shown. | Invalid command format! [CORRECT COMMAND FORMAT] | + | `add` | Error message is shown. | Invalid command format! [CORRECT COMMAND FORMAT] | + | `add n/` | Error message is shown | Invalid command format! [CORRECT COMMAND FORMAT] | + | `add p/???` | Error message is shown | Invalid command format! [CORRECT COMMAND FORMAT] | + | `add n/John+Doe xx/PARAMETER...` | Error message is shown | Names should not be blank and should only contain alphanumeric characters, spaces or the following special characters: - . ( ) @ / ' | + | `add p/98@1532 xx/PARAMETER...` | Error message is shown | Phone numbers should only contain numbers, and it should be at least 3 digits long | + | `add ecrs/knight xx/PARAMETER...` | Error message is shown | Relationship type should be Parent, Child, Sibling, Spouse, Grandparent or Relative or their gendered variants | + + + +
+ +### Editing a patient + +1. Editing a patient while any number of patients are being shown. +
+ **Prerequisites:** List all patients using the `list` command. Patients are sorted by the time they were added to MedConnect. +
+ + | Test case input | Expected behaviour | Expected message | + |--------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------| + | `edit 1 n/Ryan p/98765432 e/ryan@hotmail.com` | The name, phone and email of the first patient in the list is edited to the new values provided as arguments | Edited person: [PERSON DETAILS] | + | `edit 1` | Error message is shown. | At least one field to edit must be provided. | + | `edit 1 n/` | Error message is shown. | Names should not be blank and should only contain alphanumeric characters, spaces or the following special characters: - . ( ) @ / ' | + | `edit 1 n/John p/` | Error message is shown. | Phone numbers should only contain numbers, and it should be at least 3 digits long | + | `edit` | Error message is shown | Invalid command format! [CORRECT COMMAND FORMAT] | + | `edit 2 ecname/John Doe` | Error message is shown | At least one emergency contact index to edit must be provided. | + | `edit 2 ec/2` | Error message is shown | At least one emergency contact field to edit must be provided. | + | `edit 1 ec/x ecname/Heather ecphone/5137985 ecrs/Sibling`
(x > number of emergency contacts) | Error message is shown | Index is not a non-zero unsigned integer. | + | `edit x n/Heather`
(x > number of contacts) | Error message is shown | The person index provided is invalid | + +
+ +### Deleting a patient + +1. Deleting a patient while all patients are being shown
+ +
+ **Prerequisites:**
+ 1. List all patients using the `list` command.
+ 2. Multiple persons in the list. +
+ + | Test case input | Expected behaviour | Expected message | + |------------------------------------------------------|--------------------------------------------------------------------------|--------------------------------------------------| + | `delete 1` | First contact is deleted from the list. | Deleted Person: [PERSON DETAILS] | + | `delete 1 ec/1` | The first emergency contact of the first contact in the list is deleted. | Deleted emergency contact: [PERSON DETAILS] | + | `delete 0` | Error message is shown. | Invalid command format! [CORRECT COMMAND FORMAT] | + | `delete 2 ec/0` | Error message is shown. | Index is not a non-zero unsigned integer. | + | `delete ec/1` | Error message is shown | Invalid command format! [CORRECT COMMAND FORMAT] | + | `delete ec/x`
(x > number of emergency contacts) | Error message is shown | The emergency contact index provided is invalid | + | `delete x`
(x > number of contacts) | Error message is shown | The person index provided is invalid | + + +
+ +2. Deleting a patient while a filtered list is being shown + +
+ **Prerequisites:** The patient list is filtered using the `find` or `finddoc` command. +
+ + | Test case input | Expected behaviour | Expected message | + |------------------------------------------------------|--------------------------------------------------------------------------|--------------------------------------------------| + | `delete 1` | First contact is deleted from the list. | Deleted Person: [PERSON DETAILS] | + | `delete 1 ec/1` | The first emergency contact of the first contact in the list is deleted. | Deleted emergency contact: [PERSON DETAILS] | + | `delete 0` | Error message is shown. | Invalid command format! [CORRECT COMMAND FORMAT] | + | `delete 2 ec/0` | Error message is shown. | Index is not a non-zero unsigned integer. | + | `delete ec/1` | Error message is shown | Invalid command format! [CORRECT COMMAND FORMAT] | + | `delete ec/x`
(x > number of emergency contacts) | Error message is shown | The emergency contact index provided is invalid | + | `delete x`
(x > number of contacts) | Error message is shown | The person index provided is invalid | + +
+ +### Adding an emergency contact to a patient +1. Editing a patient while any number of patients are being shown. +
+ **Prerequisites:** List all patients using the `list` command. Patients are sorted by the time they were added to MedConnect. +
+ + | Test case input | Expected behaviour | Expected message | + |------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------| + | `addec 1 ecname/Sarah Lim ecphone/91234567 ecrs/Granddaughter` | The name, phone and email of the first patient in the list is edited to the new values provided as arguments | Added emergency contact: [PERSON DETAILS] | + | `addec 1` | Error message is shown. | Invalid command format! [CORRECT COMMAND FORMAT] | + | `addec ecname/Sarah Lim ecphone/91234567 ecrs/Granddaughter` | Error message is shown. | Invalid command format! [CORRECT COMMAND FORMAT] | + | `addec 1 ecname/Sarah Lim ecphone/91234567` | Error message is shown. | Invalid command format! [CORRECT COMMAND FORMAT] | + | `addec 1 ecname/Sarah Lim ecphone/91234567 ecrs/Neighbor` | Error message is shown | Relationship type should be Parent, Child, Sibling, Spouse, Grandparent or Relative or their gendered variants | + | `addec 2 ecname/D%#P! ecphone/91234567 ecrs/Son` | Error message is shown | Names should not be blank and should only contain alphanumeric characters, spaces or the following special characters: - . ( ) @ / ' | + | `addec x ecname/Heather ecphone/5137985 ecrs/Sibling`
(x > number of contacts) | Error message is shown | Invalid command format! [CORRECT COMMAND FORMAT] | ### Saving data -1. Dealing with missing/corrupted data files + 1. Dealing with missing/corrupted data files - 1. _{explain how to simulate a missing/corrupted file, and the expected behavior}_ + 1. _{explain how to simulate a missing/corrupted file, and the expected behavior}_ 1. _{ more test cases …​ }_ + +
+ +## **Appendix: Effort** + +Developing MedConnect as a brownfield project from the upgrading of AB3 was challenging for us as a team of relatively junior software engineers who did not have much experience in a software engineering project. +For some of our team members, the only prior software engineering experience we had was our Orbital project. + +Initially, we faced many challenges in managing the Git workflow of creating issues, creating branches, merging branches and pull requests. +This was because the process was new to most of the group and we carefully took the time to learn the proper workflow and avoid merge conflicts. + +Another challenge we faced was implementing the autocomplete feature. Since MedConnect was directed to be used by fast typists, we brainstormed the idea of having an autocomplete feature to greatly benefit them. +However, this idea was quite foreign to all of us and we took a great deal of time in figuring out how to tackle this problem. +In due time, we managed to figure out a solution as a team and it is now implemented in the current version of MedConnect. + +Finally, for the undo and redo feature, we adapted the proposed implementation provided in the developer guide of AB3. +This greatly reduced the effort required for these features as there are many ways to implement them. +A more complex solution would be for each command to have its own respective undo and redo implementation. +However, we followed the proposed implementation of saving the AddressBook in states and having a pointer that points to the current state. +The pointer would move between states upon execution of the undo and redo commands. + +Overall, we faced many challenges as a team that we had to overcome over a short runway. +We managed to stay afloat and tackle these challenges through constant communication and good teamwork between team members, helping each other out swiftly and decisively. diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock index bac5eb36d35..3b931188625 100644 --- a/docs/Gemfile.lock +++ b/docs/Gemfile.lock @@ -1,22 +1,36 @@ GEM remote: https://rubygems.org/ specs: - activesupport (7.0.7.2) + activesupport (7.1.5) + base64 + benchmark (>= 0.3) + bigdecimal concurrent-ruby (~> 1.0, >= 1.0.2) + connection_pool (>= 2.2.5) + drb i18n (>= 1.6, < 2) + logger (>= 1.4.2) minitest (>= 5.1) + mutex_m + securerandom (>= 0.3) tzinfo (~> 2.0) - addressable (2.8.4) - public_suffix (>= 2.0.2, < 6.0) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) + base64 (0.2.0) + benchmark (0.3.0) + bigdecimal (3.1.8) coffee-script (2.4.1) coffee-script-source execjs - coffee-script-source (1.11.1) + coffee-script-source (1.12.2) colorator (1.1.0) commonmarker (0.23.10) - concurrent-ruby (1.2.2) - dnsruby (1.70.0) + concurrent-ruby (1.3.4) + connection_pool (2.4.1) + csv (3.3.0) + dnsruby (1.72.2) simpleidn (~> 0.2.1) + drb (2.2.1) em-websocket (0.5.3) eventmachine (>= 0.12.9) http_parser.rb (~> 0) @@ -24,25 +38,27 @@ GEM ffi (>= 1.15.0) eventmachine (1.2.7) eventmachine (1.2.7-x64-mingw32) - execjs (2.8.1) - faraday (2.7.5) - faraday-net_http (>= 2.0, < 3.1) - ruby2_keywords (>= 0.0.4) - faraday-net_http (3.0.2) - ffi (1.15.5) - ffi (1.15.5-x64-mingw32) + execjs (2.10.0) + faraday (2.12.0) + faraday-net_http (>= 2.0, < 3.4) + json + logger + faraday-net_http (3.3.0) + net-http + ffi (1.17.0) + ffi (1.17.0-x64-mingw32) forwardable-extended (2.6.0) - gemoji (3.0.1) - github-pages (228) - github-pages-health-check (= 1.17.9) - jekyll (= 3.9.3) - jekyll-avatar (= 0.7.0) - jekyll-coffeescript (= 1.1.1) - jekyll-commonmark-ghpages (= 0.4.0) - jekyll-default-layout (= 0.1.4) - jekyll-feed (= 0.15.1) + gemoji (4.1.0) + github-pages (232) + github-pages-health-check (= 1.18.2) + jekyll (= 3.10.0) + jekyll-avatar (= 0.8.0) + jekyll-coffeescript (= 1.2.2) + jekyll-commonmark-ghpages (= 0.5.1) + jekyll-default-layout (= 0.1.5) + jekyll-feed (= 0.17.0) jekyll-gist (= 1.5.0) - jekyll-github-metadata (= 2.13.0) + jekyll-github-metadata (= 2.16.1) jekyll-include-cache (= 0.2.1) jekyll-mentions (= 1.6.0) jekyll-optional-front-matter (= 0.3.2) @@ -69,30 +85,32 @@ GEM jekyll-theme-tactile (= 0.2.0) jekyll-theme-time-machine (= 0.2.0) jekyll-titles-from-headings (= 0.5.3) - jemoji (= 0.12.0) - kramdown (= 2.3.2) + jemoji (= 0.13.0) + kramdown (= 2.4.0) kramdown-parser-gfm (= 1.1.0) liquid (= 4.0.4) mercenary (~> 0.3) minima (= 2.5.1) - nokogiri (>= 1.13.6, < 2.0) - rouge (= 3.26.0) + nokogiri (>= 1.16.2, < 2.0) + rouge (= 3.30.0) terminal-table (~> 1.4) - github-pages-health-check (1.17.9) + webrick (~> 1.8) + github-pages-health-check (1.18.2) addressable (~> 2.3) dnsruby (~> 1.60) - octokit (~> 4.0) - public_suffix (>= 3.0, < 5.0) + octokit (>= 4, < 8) + public_suffix (>= 3.0, < 6.0) typhoeus (~> 1.3) html-pipeline (2.14.3) activesupport (>= 2) nokogiri (>= 1.4) http_parser.rb (0.8.0) - i18n (1.14.1) + i18n (1.14.6) concurrent-ruby (~> 1.0) - jekyll (3.9.3) + jekyll (3.10.0) addressable (~> 2.4) colorator (~> 1.0) + csv (~> 3.0) em-websocket (~> 0.5) i18n (>= 0.7, < 2) jekyll-sass-converter (~> 1.0) @@ -103,27 +121,28 @@ GEM pathutil (~> 0.9) rouge (>= 1.7, < 4) safe_yaml (~> 1.0) - jekyll-avatar (0.7.0) + webrick (>= 1.0) + jekyll-avatar (0.8.0) jekyll (>= 3.0, < 5.0) - jekyll-coffeescript (1.1.1) + jekyll-coffeescript (1.2.2) coffee-script (~> 2.2) - coffee-script-source (~> 1.11.1) + coffee-script-source (~> 1.12) jekyll-commonmark (1.4.0) commonmarker (~> 0.22) - jekyll-commonmark-ghpages (0.4.0) - commonmarker (~> 0.23.7) - jekyll (~> 3.9.0) + jekyll-commonmark-ghpages (0.5.1) + commonmarker (>= 0.23.7, < 1.1.0) + jekyll (>= 3.9, < 4.0) jekyll-commonmark (~> 1.4.0) rouge (>= 2.0, < 5.0) - jekyll-default-layout (0.1.4) - jekyll (~> 3.0) - jekyll-feed (0.15.1) + jekyll-default-layout (0.1.5) + jekyll (>= 3.0, < 5.0) + jekyll-feed (0.17.0) jekyll (>= 3.7, < 5.0) jekyll-gist (1.5.0) octokit (~> 4.2) - jekyll-github-metadata (2.13.0) + jekyll-github-metadata (2.16.1) jekyll (>= 3.4, < 5.0) - octokit (~> 4.0, != 4.4.0) + octokit (>= 4, < 7, != 4.4.0) jekyll-include-cache (0.2.1) jekyll (>= 3.7, < 5.0) jekyll-mentions (1.6.0) @@ -194,42 +213,47 @@ GEM jekyll (>= 3.3, < 5.0) jekyll-watch (2.2.1) listen (~> 3.0) - jemoji (0.12.0) - gemoji (~> 3.0) + jemoji (0.13.0) + gemoji (>= 3, < 5) html-pipeline (~> 2.2) jekyll (>= 3.0, < 5.0) - kramdown (2.3.2) + json (2.7.6) + kramdown (2.4.0) rexml kramdown-parser-gfm (1.1.0) kramdown (~> 2.0) liquid (4.0.4) - listen (3.8.0) + listen (3.9.0) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) + logger (1.6.1) mercenary (0.3.6) - mini_portile2 (2.8.6) + mini_portile2 (2.8.7) minima (2.5.1) jekyll (>= 3.5, < 5.0) jekyll-feed (~> 0.9) jekyll-seo-tag (~> 2.1) - minitest (5.19.0) - nokogiri (1.16.5) + minitest (5.25.1) + mutex_m (0.2.0) + net-http (0.4.1) + uri + nokogiri (1.16.7) mini_portile2 (~> 2.8.2) racc (~> 1.4) + nokogiri (1.16.7-x64-mingw32) + racc (~> 1.4) octokit (4.25.1) faraday (>= 1, < 3) sawyer (~> 0.9) pathutil (0.16.2) forwardable-extended (~> 2.6) - public_suffix (4.0.7) - racc (1.7.3) + public_suffix (5.1.1) + racc (1.8.1) rb-fsevent (0.11.2) - rb-inotify (0.10.1) + rb-inotify (0.11.1) ffi (~> 1.0) - rexml (3.3.6) - strscan - rouge (3.26.0) - ruby2_keywords (0.0.5) + rexml (3.3.9) + rouge (3.30.0) rubyzip (2.3.2) safe_yaml (1.0.5) sass (3.7.4) @@ -240,20 +264,17 @@ GEM sawyer (0.9.2) addressable (>= 2.3.5) faraday (>= 0.17.3, < 3) - simpleidn (0.2.1) - unf (~> 0.1.4) - strscan (3.1.0) + securerandom (0.3.1) + simpleidn (0.2.3) terminal-table (1.8.0) unicode-display_width (~> 1.1, >= 1.1.1) - typhoeus (1.4.0) + typhoeus (1.4.1) ethon (>= 0.9.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - unf (0.1.4) - unf_ext - unf_ext (0.0.8.2) - unf_ext (0.0.8.2-x64-mingw32) unicode-display_width (1.8.0) + uri (0.13.1) + wdm (0.1.1) webrick (1.8.1) PLATFORMS @@ -263,7 +284,8 @@ PLATFORMS DEPENDENCIES github-pages jekyll + wdm (~> 0.1.0) webrick BUNDLED WITH - 2.1.4 + 2.2.33 diff --git a/docs/UserGuide.md b/docs/UserGuide.md index 84b4ddc4e40..da55a8fd8c3 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -3,41 +3,123 @@ layout: page title: User Guide --- -AddressBook Level 3 (AB3) is a **desktop app for managing contacts, optimized for use via a Command Line Interface** (CLI) while still having the benefits of a Graphical User Interface (GUI). If you can type fast, AB3 can get your contact management tasks done faster than traditional GUI apps. +
+ +**:warning: Disclaimer** + +The current version of MedConnect is only designed to support the **English language** and for use in a **single country and timezone**. + +Using MedConnect with other languages or across multiple countries and timezones may lead to unexpected behaviour. +
+
+MedConnect is a **desktop app designed for healthcare administrators in elderly care homes for dementia patients**. It consolidates crucial contact information into a single, accessible database, allowing administrative staff to manage patient and doctor contacts efficiently under high-pressure conditions. + +MedConnect combines the speed of a Command Line Interface ([CLI](#cli)) with the visual clarity of a Graphical User Interface ([GUI](#gui)), making it ideal for administrators who can type fast and need rapid access to information. MedConnect can get your contact management tasks done faster than traditional [GUI](#gui) apps. + +With MedConnect, connecting with on-call doctors, family members, or other essential contacts becomes seamless, helping you respond quickly when every second counts. + +
+ +## Table of Contents +{:.no_toc} * Table of Contents {:toc} --------------------------------------------------------------------------------------------------------------------- +
+ +## How to use this User Guide +This User Guide is designed to help you understand and use MedConnect effectively. Here are some tips on how to navigate and use this guide: +1. **[Table of Contents](#table-of-contents)**: At the beginning of the guide, you will find a Table of Contents. Use this to quickly jump to the section you are interested in. +1. **[Quick Start](#quick-start)**: If you are new to MedConnect, start with the Quick Start section. It provides step-by-step instructions on how to set up and start using the application. +1. **[Overview of GUI](#overview-of-gui)**: This section provides an overview of the graphical user interface (GUI) of MedConnect. Use this section to familiarize yourself with the different components of the application. +1. **[Features](#features)**: This section details all the commands available in MedConnect. Each command is explained with its format, parameters, and examples. Use this section to learn how to perform specific tasks. +1. **[Command Summary](#command-summary)**: At the end of the guide, there is a Command Summary table that provides a quick reference for all commands. Use this table to quickly look up the format of a command. +1. **[FAQ](#faq)**: The FAQ section addresses common questions and issues. Check this section if you encounter any problems or have questions about using MedConnect. +1. **[Known Issues](#known-issues)**: This section lists any known issues with the application and their workarounds. Refer to this section if you experience any unexpected behavior. +1. **[Glossary](#glossary)**: This section explains unfamiliar terms that we use in this User Guide. Check out the glossary if you're unsure what a certain word means. +1. **Notes and Tips**: Throughout the guide, you will find notes and tips highlighted in different styles. These provide additional information and helpful hints for using MedConnect effectively. + +By following these sections, you can quickly find the information you need and make the most out of MedConnect. + +[↑ Back to top](#table-of-contents) -## Quick start -1. Ensure you have Java `17` or above installed in your Computer. +
-1. Download the latest `.jar` file from [here](https://github.com/se-edu/addressbook-level3/releases). +## Quick Start -1. Copy the file to the folder you want to use as the _home folder_ for your AddressBook. +1. Ensure you have Java `17` or above installed on your computer. + * You can check your Java version by following the instructions [here](https://www.wikihow.com/Check-Your-Java-Version-in-the-Windows-Command-Line). + * If you do not have Java `17` or above installed in your computer, you can download Java from [here](https://www.oracle.com/java/technologies/downloads/#java17). -1. Open a command terminal, `cd` into the folder you put the jar file in, and use the `java -jar addressbook.jar` command to run the application.
- A GUI similar to the below should appear in a few seconds. Note how the app contains some sample data.
- ![Ui](images/Ui.png) +2. Download the latest release of the `medconnect.jar` file from [here](https://github.com/AY2425S1-CS2103T-T13-1/tp/releases). -1. Type the command in the command box and press Enter to execute it. e.g. typing **`help`** and pressing Enter will open the help window.
- Some example commands you can try: +3. Copy the file to the folder you want to use as the _home folder_. The _home folder_ will be where all the data files will be saved. - * `list` : Lists all contacts. +4. For *Windows:* Open the home folder and right-click anywhere in the red box, as shown in the image below. Click "Open in Terminal". A terminal window will pop up, then type in the command `java -jar medconnect.jar` to run the application. - * `add n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01` : Adds a contact named `John Doe` to the Address Book. + New terminal on Windows - * `delete 3` : Deletes the 3rd contact shown in the current list. +
- * `clear` : Deletes all contacts. + For *MacOS:* Right-click home folder. Hover over "Services". Select "New Terminal at folder". A terminal window will pop up, then type in the command `java -jar medconnect.jar` to run the application. - * `exit` : Exits the app. + New terminal on MacOS

+ +5. Type the command in the [command box](#command-box) and press `Enter` to execute it. Here are some example commands you can try: + + * `list` : [List](#listing-all-patients--list) all contacts. + + * `add n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01 ecname/Charlotte Lim ecphone/81243564 ecrs/Sibling dname/Ronald Lee dphone/99441234 demail/ronaldlee@gmail.com`
+ [Add](#adding-a-patient-add) a contact named `John Doe` to the Address Book with emergency contact `Charlotte Lim` and doctor `Ronald Lee`. + + * `delete 3` : [Delete](#deleting-a-patient--delete) the 3rd contact shown in the current list. + + * `clear` : [Clear](#clearing-all-entries--clear) the contacts list of all contacts. + + * `undo` : [Undo](#undoing-previous-command--undo) previous command. + + * `exit` : [Exit](#exiting-the-program--exit) the app. 1. Refer to the [Features](#features) below for details of each command. --------------------------------------------------------------------------------------------------------------------- +[↑ Back to top](#table-of-contents) + +
+ +## Overview of [GUI](#gui) + +![uitutorial](images/uitutorial.png) + +* The headers are colour-coded to match the GUI screenshot's boxes above. + +Menu Bar +* Clicking `File` will show the option to exit the application. +* Clicking `Help` will show `Help F1` which when clicked will link you to this User Guide. + +Command Box +* This is where you will be typing the commands for MedConnect. The full list of commands can be found under [Features](#features). + +Result Box +* This is where MedConnect will give you feedback after you type in a command. It will provide information on whether a command was successful or an invalid input was provided. For more information on the valid command inputs, head to [Features](#features). + +Patients List +* The list of patients will be shown here. +* You can scroll down the list to see more patients. + +Doctor Details +* Each patient has a doctor assigned to them. +* The name, phone number and email of the assigned doctor can be easily identified by the blue text colour. + +Emergency Contact Details +* Each patient will have at least one emergency contact listed. +* The name, phone number and relationship to the patient of each emergency contact is listed here. +* If a patient has multiple emergency contacts, this box will become scrollable to be able to view more contacts. + +[↑ Back to top](#table-of-contents) + +
## Features @@ -48,98 +130,330 @@ AddressBook Level 3 (AB3) is a **desktop app for managing contacts, optimized fo * Words in `UPPER_CASE` are the parameters to be supplied by the user.
e.g. in `add n/NAME`, `NAME` is a parameter which can be used as `add n/John Doe`. -* Items in square brackets are optional.
+* Items in square brackets `[ ]` are optional.
e.g `n/NAME [t/TAG]` can be used as `n/John Doe t/friend` or as `n/John Doe`. -* Items with `…`​ after them can be used multiple times including zero times.
- e.g. `[t/TAG]…​` can be used as ` ` (i.e. 0 times), `t/friend`, `t/friend t/family` etc. +* Items with `…` after them can be used multiple times.
+ e.g. `[t/TAG]…` can be used as ` ` (i.e. not included), `t/friend`, `t/friend t/family` etc. + +* Items without `…`​ after them should not be inputted multiple times in the command. -* Parameters can be in any order.
+* Parameters can be in any order unless otherwise stated.
e.g. if the command specifies `n/NAME p/PHONE_NUMBER`, `p/PHONE_NUMBER n/NAME` is also acceptable. -* Extraneous parameters for commands that do not take in parameters (such as `help`, `list`, `exit` and `clear`) will be ignored.
+* To avoid being too verbose, some error messages may be simplified for brevity. For a list of valid inputs for each parameter, please refer to the [Glossary](#glossary). + +* Extraneous parameters for commands that do not take in parameters (such as `help`, `exit` and `clear`) will be ignored.
e.g. if the command specifies `help 123`, it will be interpreted as `help`. * If you are using a PDF version of this document, be careful when copying and pasting commands that span multiple lines as space characters surrounding line-breaks may be omitted when copied over to the application. +[↑ Back to top](#table-of-contents) + +
+ +### Suggestions and Autocompletion + +The Command Box in MedConnect offers **suggestions** and **autocompletion** to assist users while typing commands. These features aim to enhance the command entry experience through reducing the need for memorising command formats and reducing the chance of errors. + +### Suggestions + +Suggestions provide visible hints about the expected command format as the command is entered. + +![suggestion](images/Suggestion.png) + +* If an incorrect entry is detected (e.g., type `adding` instead of `add`), the suggestion will be hidden to indicate a problem with the format. + +**Examples** (words in brackets "()", are suggested by the system.) +* Typing `add` will show the expected format for adding a patient: `add (n/NAME p/PHONE e/EMAIL a/ADDRESS ecname/EMERGENCY_CONTACT_NAME ...)` +* Upon typing the slash commands (e.g. `n/` or `p/`), the suggestion only shows the parameter that needs to be filled or the next command once done. e.g `add n/(NAME)`, `add n/Saajid Shaik (p/PHONE)` +* Inputs that are prefixes to more than 1 command, will result in full syntax suggestions for those commands to be displayed and separated with a `|`. e.g. `fi(nd KEYWORD MORE_KEYWORDS | finddoc KEYWORD MORE_KEYWORDS)` + +
+ +
+ +**:information_source: Note** + +While commands can be written in any order, the suggestion feature only follows the default order which puts related parameters together, analogous to how the contact details are displayed in the app. + +You may notice that suggestion and autocomplete features almost always work, even without default ordering, but this is currently a feature-in-progress for future enhancement. There are some edge cases that do not work. (e.g. 1st default parameter must always be in order `add n/` to work) + +Additionally, suggestions for parameters will never be duplicated unless users manually deviates from autocomplete or suggestion by the system. + +
+ +[↑ Back to top](#table-of-contents) + + +### Autocompletion + +Autocompletion helps to complete partially typed commands by pressing the `Control` key. + +![Autocomplete demonstration](images/autocomplete.gif) + +* If an incorrect entry is detected (e.g., type `adding` or `ade` instead of `add`), autocompletion will be disabled to indicate a problem with the format. + +**Examples**: +* Typing `ad` followed by pressing `Control` autocompletes the input to `add`. +* Typing `add` followed by pressing `Control` autocompletes the input to `add n/`. +* Typing `add n/Saajid Shaik` followed by pressing `Control` autocompletes the input to `add n/Saajid Shaik p/`. + +Since autocomplete can only work on fixed syntaxes, `INDEX`,`KEYWORD` and variable attribute (e.g. `NAME` in commands `n/NAME`) cannot be autocomplete. Should users try to autocomplete, the input text turns red, indicating users to fill in their information as needed. + +![red_error](images/red_autocomplete.png) + +[↑ Back to top](#table-of-contents) + +
+ + ### Viewing help : `help` -Shows a message explaning how to access the help page. +Shows a message explaining how to access the help page. -![help message](images/helpMessage.png) +![help message](images/helpWindow.png) Format: `help` +[↑ Back to top](#table-of-contents) + -### Adding a person: `add` +### Adding a patient: `add` -Adds a person to the address book. +Adds a patient to the address book. -Format: `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]…​` +Format: `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS ecname/EMERGENCY_CONTACT_NAME ecphone/EMERGENCY_CONTACT_PHONE +ecrs/EMERGENCY_CONTACT_RELATIONSHIP dname/DOCTOR_NAME dphone/DOCTOR_PHONE demail/DOCTOR_EMAIL [t/TAG]…​`
:bulb: **Tip:** -A person can have any number of tags (including 0) +A patient can have any number of tags (including 0). Tags can be used to add short descriptions or categories to patients.
-Examples: -* `add n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01` -* `add n/Betsy Crowe t/friend e/betsycrowe@example.com a/Newgate Prison p/1234567 t/criminal` +* You need not enter the prefix 'Dr' when typing `DOCTOR_NAME`. The app automatically adds the prefix `Dr` in front of the `DOCTOR_NAME` entered. +* Patients with the same `PHONE_NUMBER` will be flagged as duplicates and cannot be added to the address book. + +**Examples** +* `add n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01 ecname/Charlotte Lim ecphone/94681352 ecrs/daughter dname/Ronald Lee dphone/99441234 demail/ronaldlee@gmail.com`
+ Adds John Doe as a patient with his daughter Charlotte Lim as his emergency contact and Dr Ronald Lee as his assigned doctor. + +* `add n/Betsy Crowe t/friend e/betsycrowe@example.com a/Newgate Prison p/1234567 ecrs/son ecphone/94873631 ecname/Bob Builder demail/liampayne@gmail.com dphone/91231231 dname/Liam Payne t/criminal`
+ Adds Betsy Crowe as a patient with her son Bob Builder as her emergency contact and Dr Liam Payne as her assigned doctor. -### Listing all persons : `list` +[↑ Back to top](#table-of-contents) -Shows a list of all persons in the address book. -Format: `list` +### Listing all patients : `list` -### Editing a person : `edit` +Shows a list of every patient in the address book and sets the sort order of the list. -Edits an existing person in the address book. +Format: `list [SORT_ORDER]` -Format: `edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAG]…​` +
+ +Valid inputs for sort order parameter: `timeadded`, `timeadded asc`, `timeadded desc`, `name`, `name asc`, `name desc` + +
+ +* If `SORT_ORDER` is not provided, the patients listed will be sorted in the order they were added. The patient who was added the most recently will be at the bottom of the list. +* `timeadded`, `timeadded asc` and `timeadded desc` sets the patient list to be sorted according to the time they were added to Medconnect. + * `timeadded` and `timeadded asc` sorts the patient list from least to most recently added. + * `timeadded desc` sorts the patient list from most to least recently added. +* `name`, `name asc` and `name desc` sets the patient list to be sorted according to their name in alphabetical order. + * `name` and `name asc` sorts by the patients' names from uppercase A to Z, followed by lowercase a to z. + * `name desc` sorts by the patients' names from lowercase z to a, followed by uppercase Z to A. + +**Examples** +* `list name asc`
+ Sorts the patient list in ascending alphabetical order of their names. + +* `list timeadded desc`
+ Sorts the patient list in descending order of time added to MedConnect. + +[↑ Back to top](#table-of-contents) + +
+ +### Editing a patient : `edit` + +Edits an existing patient in the address book. -* Edits the person at the specified `INDEX`. The index refers to the index number shown in the displayed person list. The index **must be a positive integer** 1, 2, 3, …​ +Format: `edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [ec/EMERGENCY_CONTACT_INDEX] [ecname/EMERGENCY_CONTACT_NAME] [ecphone/EMERGENCY_CONTACT_PHONE] [ecrs/EMERGENCY_CONTACT_RELATIONSHIP] [dname/DOCTOR_NAME] [dphone/DOCTOR_PHONE] [demail/DOCTOR_EMAIL] [t/TAG]…` + +* Edits the patient at the specified `INDEX`. Existing values will be updated to the input values. +* `INDEX` should come before any of the optional fields. * At least one of the optional fields must be provided. -* Existing values will be updated to the input values. -* When editing tags, the existing tags of the person will be removed i.e adding of tags is not cumulative. -* You can remove all the person’s tags by typing `t/` without - specifying any tags after it. +* To edit the patient's emergency contact, provide the index of the emergency contact to edit under `ec/EMERGENCY_CONTACT_INDEX` and at least one of the emergency contact fields. +* When editing tags, all the existing tags of the person will be removed. You will have to re-enter pre-existing tags if you wish to preserve them. -Examples: -* `edit 1 p/91234567 e/johndoe@example.com` Edits the phone number and email address of the 1st person to be `91234567` and `johndoe@example.com` respectively. -* `edit 2 n/Betsy Crower t/` Edits the name of the 2nd person to be `Betsy Crower` and clears all existing tags. +
:bulb: **Tip:** +You can remove all of a person’s existing tags by typing `t/` without specifying any tags after it. +
+ +**Examples:** +* `edit 1 p/91234567 e/johndoe@example.com`
+Edits the phone number and email address of the 1st patient to be `91234567` and `johndoe@example.com` respectively. + +* `edit 2 n/Betsy Crower t/`
+Edits the name of the 2nd patient to be `Betsy Crower` and clears all existing tags. -### Locating persons by name: `find` +* `edit 2 n/Betsy Crower ec/1 ecname/Peter Tan`
+ Edits the name of the 2nd patient to be `Betsy Crower`. Edits the name of the first emergency contact of the 2nd patient to be `Peter Tan`. -Finds persons whose names contain any of the given keywords. +[↑ Back to top](#table-of-contents) + +
+ +### Locating patients by patient's name: `find` + +Finds patients whose names contain any of the given keywords. Format: `find KEYWORD [MORE_KEYWORDS]` -* The search is case-insensitive. e.g `hans` will match `Hans` -* The order of the keywords does not matter. e.g. `Hans Bo` will match `Bo Hans` -* Only the name is searched. -* Only full words will be matched e.g. `Han` will not match `Hans` -* Persons matching at least one keyword will be returned (i.e. `OR` search). - e.g. `Hans Bo` will return `Hans Gruber`, `Bo Yang` +* **Only the patient's name is searched.** +* The search is case-insensitive. (e.g `hans` will match `Hans`) +* The order of the keywords does not matter. (e.g. `Hans Bo` will match `Bo Hans`) +* Names will match if the keyword is found in any part of the name. (e.g. `Ha` will match `Hans`) +* All patients matching at least one keyword will be shown in the patient list. + (e.g. `Hans Bo` will return `Hans Gruber`, `Bo Yang`) +* The `find` command filters the list, which gets reset after entering an `add`, `addec`, `edit`, `list`, `undo` or `redo` command. Examples: -* `find John` returns `john` and `John Doe` -* `find alex david` returns `Alex Yeoh`, `David Li`
- ![result for 'find alex david'](images/findAlexDavidResult.png) +* `find Alex` displays`Alex Yeoh` and `Alexis Tan` +* `find dav Roy` displays `David Li` and `Roy Balakrishnan` -### Deleting a person : `delete` + ![result for 'find dav roy'](images/findDavRoyResult.png){: width="400"} -Deletes the specified person from the address book. +[↑ Back to top](#table-of-contents) -Format: `delete INDEX` +
-* Deletes the person at the specified `INDEX`. -* The index refers to the index number shown in the displayed person list. -* The index **must be a positive integer** 1, 2, 3, …​ +### Locating patients by doctor's name: `finddoc` + +Finds patients by checking if their doctor's names contain any of the provided keywords. + +Format: `finddoc KEYWORD [MORE_KEYWORDS]` + +* **Only the doctor's name is searched.** +* The search is case-insensitive. (e.g `hans` will match `Hans`) +* The order of the keywords does not matter. (e.g. `Hans Bo` will match `Bo Hans`) +* Names will match if the keyword is found in any part of the doctor's name. (e.g. `Ha` will match `Hans`) +* Persons matching at least one keyword in their doctor's name will be returned. (e.g. `Hans Bo` will return persons whose doctors are `Hans Gruber`, `Bo Yang`) +* * The `finddoc` command filters the list, which gets reset after entering an `add`, `addec`, `edit`, `list`, `undo` or `redo` command. + +**Examples:** +* `finddoc John` returns persons with doctors `john` and `John Doe` +* `finddoc tan ed` returns persons with doctors `Tan Wei Ming`, `Ed Sheeran` +* `findoc zhou` returns `Irfan Ibrahim` since `Dr Zhou Jie Lun` is his assigned doctor. + + ![result for 'finddoc zhou'](images/finddocZhouResult.png) + +[↑ Back to top](#table-of-contents) + +
+ +### Deleting a patient : `delete` + +Deletes the specified patient or emergency contact from the address book. + +Format: `delete INDEX [ec/EMERGENCY_CONTACT_INDEX]` + +* Deletes the patient at the specified `INDEX` **OR** deletes the emergency contact at the specified `EMERGENCY_CONTACT_INDEX` of the patient at the specified `INDEX`. +* `INDEX` refers to the index number shown in the displayed person list. +* The parameters **must** follow the order given above. +* Each patient must have at least one emergency contact. You cannot delete the final emergency contact in the list. + +**Examples:** +* `delete 2 ec/2` deletes the 2nd emergency contact of the 2nd patient in the address book. +* `list` followed by `delete 2` deletes the 2nd patient in the address book. +* `find Betsy` followed by `delete 1` deletes the 1st patient in the results of the `find` command. + +[↑ Back to top](#table-of-contents) + + +### Adding an emergency contact : `addec` + +Adds an emergency contact to a specified patient in the address book. + +Format: `addec INDEX ecname/EMERGENCY_CONTACT_NAME ecphone/EMERGENCY_CONTACT_PHONE ecrs/EMERGENCY_CONTACT_RELATIONSHIP` + +* `INDEX` should come before any of the optional fields. +* A patient cannot have more than one emergency contact with the same phone number. + +**Examples** +* `addec 1 ecname/Shannon Wong ecphone/84651325 ecrs/Daughter`
+Adds a new emergency contact Shannon Wong to the 1st patient in the address book. + +[↑ Back to top](#table-of-contents) + +
+ +### Archiving data files: `archive` + +Archives the current address book data to a timestamped data file with an optional description. + +Format: `archive [DESCRIPTION]` + +**Valid inputs** +* `DESCRIPTION` must be a valid file name (i.e., it cannot contain any of the following special characters: `\/:*?"<>|`). +* The archive data file will be saved as a [JSON file](#glossary) in the `[home folder]/data/archive/` folder. + +**Examples** +* `archive` Archives the current address book data to a timestamped data file. +* `archive before major update`
Archives the current address book data to a timestamped data file with the description "before major update". + +[↑ Back to top](#table-of-contents) + + +### Listing all archived data files: `listarchives` + +Lists the names of all the archived data files in the archive folder. + +Format: `listarchives` + +[↑ Back to top](#table-of-contents) + +
+ +### Loading data from an archived data file: `loadarchive` + +Loads the data from an archived data file into the address book. + +Format: `loadarchive FILE_NAME` + +* `FILE_NAME` should be the name of an archived data file in the archive folder. You can view a list of archived data files using the [`listarchives`](#listing-all-archived-data-files-listarchives) command. +* The data from the archived file will replace the current data in the address book. +* The data in the archived file will not be deleted. + +
:bulb: **Tip:** +Did you accidentally load an archive and want your old data back? Enter the 'undo' command! +
+ +**Examples** +* `loadarchive addressbook-2024-11-06T20-29-05.7609475-example.json`
Loads the data from the archived file named `addressbook-2024-11-06T20-29-05.7609475-example.json` into the address book. + +[↑ Back to top](#table-of-contents) + +
+ +### Deleting an archived data file: `deletearchive` + +
:exclamation: **Caution:** +Deleting an archive file is **permanent**. The `undo` command cannot restore a deleted archive file. +
+ +Deletes the data of an existing archived data file in the archive folder. + +Format: `deletearchive FILE_NAME` + +* `FILE_NAME` should be the name of an archived data file in the archive folder. You can view a list of archived data files using the [`listarchives`](#listing-all-archived-data-files-listarchives) command. + +**Examples** +* `deletearchive addressbook-2024-11-06T20-29-05.7609475-example.json` Deletes the archived file with the file name `addressbook-2024-11-06T20-29-05.7609475-example.json`. + +[↑ Back to top](#table-of-contents) -Examples: -* `list` followed by `delete 2` deletes the 2nd person in the address book. -* `find Betsy` followed by `delete 1` deletes the 1st person in the results of the `find` command. ### Clearing all entries : `clear` @@ -147,53 +461,153 @@ Clears all entries from the address book. Format: `clear` +[↑ Back to top](#table-of-contents) + +
+ +### Undoing previous command : `undo` +Restores the previous state of the address book after any change, such as an addition, edit, or deletion of a patient. + +Format: `undo` + +
:exclamation: **Warning:** +An action cannot be undone once you close the MedConnect application. +
+ +[↑ Back to top](#table-of-contents) + + +### Redoing previous command : `redo` + +Restores the state of the address book **after an undo operation has been executed**, effectively "redoing" the undone changes, such as an addition, edit, or deletion of a patient. + +Format: `redo` + +
:exclamation: **Warning:** +An action cannot be redone once you close the MedConnect application. +
+ +[↑ Back to top](#table-of-contents) + + ### Exiting the program : `exit` Exits the program. Format: `exit` +[↑ Back to top](#table-of-contents) + +
+ ### Saving the data AddressBook data are saved in the hard disk automatically after any command that changes the data. There is no need to save manually. +[↑ Back to top](#table-of-contents) + + ### Editing the data file AddressBook data are saved automatically as a JSON file `[JAR file location]/data/addressbook.json`. Advanced users are welcome to update data directly by editing that data file. -
:exclamation: **Caution:** -If your changes to the data file makes its format invalid, AddressBook will discard all data and start with an empty data file at the next run. Hence, it is recommended to take a backup of the file before editing it.
+
+:exclamation: **Caution:** +
+If your changes to the data file makes its format invalid, AddressBook will discard all data and start with an empty data file at the next run. Hence, it is recommended to take a backup of the file before editing it. +

Furthermore, certain edits can cause the AddressBook to behave in unexpected ways (e.g., if a value entered is outside of the acceptable range). Therefore, edit the data file only if you are confident that you can update it correctly.
-### Archiving data files `[coming in v2.0]` - -_Details coming soon ..._ +[↑ Back to top](#table-of-contents) --------------------------------------------------------------------------------------------------------------------- +
## FAQ -**Q**: How do I transfer my data to another Computer?
-**A**: Install the app in the other computer and overwrite the empty data file it creates with the file that contains the data of your previous AddressBook home folder. +**Q**: How do I transfer my data to another computer?
+**A**: Install the app in the other computer and overwrite the empty data file it creates with the file that contains the data of your previous MedConnect home folder. --------------------------------------------------------------------------------------------------------------------- +**Q**: How do I change the MedConnect home folder?
+**A**: The MedConnect home folder is set to the folder where the `medconnect.jar` file is located. If you want to change it, move the `medconnect.jar` file and all the files in the original home folder to the new folder. + +[↑ Back to top](#table-of-contents) ## Known issues -1. **When using multiple screens**, if you move the application to a secondary screen, and later switch to using only the primary screen, the GUI will open off-screen. The remedy is to delete the `preferences.json` file created by the application before running the application again. -2. **If you minimize the Help Window** and then run the `help` command (or use the `Help` menu, or the keyboard shortcut `F1`) again, the original Help Window will remain minimized, and no new Help Window will appear. The remedy is to manually restore the minimized Help Window. +1. When using multiple screens, if you move the application to a secondary screen, and later switch to using only the primary screen, the GUI will open off-screen. The remedy is to delete the `preferences.json` file created by the application before running the application again. + +2. If you minimize the Help Window and then run the `help` command (or use the `Help` menu, or the keyboard shortcut `F1`) again, the original Help Window will remain minimized, and no new Help Window will appear. The remedy is to manually restore the minimized Help Window. + +3. When typing commands in the CommandBox, inserting a space, e.g. `he lp` in between `he` and `lp`, will cause the suggestion and autocorrection to bug out and display incorrectly. + +4. MedConnect supports up to 2,147,483,647 patient contacts. Attempting to add or edit more than this number of contacts may result in unexpected behaviour. + +5. Adding a spaces after `/` for command identifiers such as `n/` or `p/` will cause suggestion to repeat. e.g. `add n/ hello world` (spacing between "/" and "h" will cause n/NAME to be suggested again) + +[↑ Back to top](#table-of-contents) + +
+ +## Glossary +### Terminology + +| Term | Details | Example | +|----------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **Alphanumeric** | Characters that are either numbers or letters. | 1, 2, 3, A, b, c are alphanumeric characters. | +| **Command** | Instructions that are given to MedConnect to execute. | [Features](#features) are commands that MedConnect can execute. [`add`](#adding-a-patient-add) is one such command. | +| **Command Line Interface (CLI)** | A Command Line Interface allows users to interact with an application by typing commands to execute actions. | The command line acts as a CLI in MedConnect. | +| **Graphical User Interface (GUI)** | A Graphical User Interface allows users to interact with an application through graphics like buttons or icons. | MedConnect acts as a GUI. | +| **JSON** | JSON (JavaScript Object Notation) is a lightweight data-interchange format that is easy for humans to read and write and easy for machines to parse and generate. It is based on a subset of the JavaScript Programming Language. | The data file used by MedConnect is in JSON format. | +| **Keyword** | The word you want to search for in a `find` or `finddoc` command. | Searching for a patient named Bernice Yu could be done by using keywords `Bern` or `Yu`. | +| **Parameter** | Information that you are required to provide to the MedConnect command. | `NAME` and `EMAIL` are examples of parameters you have to provide in an [`add`](#adding-a-patient-add) command.

`Paul` and `paul@gmail.com` are possible examples to provide to the respective parameters. | + +[↑ Back to top](#table-of-contents) + +
+ +### Valid Inputs for Patient parameters + +A person is uniquely identified by their `PHONE_NUMBER`. Persons with the same `PHONE_NUMBER` will be flagged as duplicates and cannot be added to the address book. + +An emergency contact is considered a duplicate if it all of its fields are the same as another emergency contact. You should not edit an emergency contact to have the same fields as another emergency contact of the same patient to prevent unexpected app behavior. If you edit an emergency contact to have the same name, phone and relationship as another emergency contact of the same patient, this is considered a duplicate emergency contact and will be automatically removed from the list. + +| Parameter | Details | Example | +|---------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **`name/` NAME** | This parameter accepts alphanumeric characters, the words `s/o`, `d/o`, spaces, and the following characters: `-`, `.`, `(`, `)`, `@`, `/`, `'` | `Connor T'Challa`, `Vika d/o Rajesh`, `Amir Fakri @ Ahmad` and `Buddy (Charles) Baxter` are examples of names you can provide in an [`add`](#adding-a-patient-add), [`edit`](#editing-a-patient--edit) or [`addec`](#adding-an-emergency-contact--addec) command `name/` paramter.

`Buddy/Charles` is an example of an invalid input to the `name/` parameter. | +| **`phone/` PHONE_NUMBER** | Phone numbers should only contain numbers and be at least 3 digits long. | `91234567` and `98765432` are examples of phone numbers you can provide in an [`add`](#adding-a-patient-add), [`edit`](#editing-a-patient--edit) or [`addec`](#adding-an-emergency-contact--addec) command `phone/` parameter. | +| **`email/` EMAIL** | MedConnect follows the valid email address format detailed [here](https://help.xmatters.com/ondemand/trial/valid_email_format.htm)

Emails should be of the format `local-part@domain` and adhere to the following constraints:
1. `local-part` should only contain alphanumeric characters and these special characters, excluding the parentheses, (+_.-). The local-part may not start or end with any special characters.
2. This is followed by a `@` and then a domain name for `domain`. The domain name is made up of domain labels separated by periods. The domain name must:
- end with a domain label at least 2 characters long
- have each domain label start and end with alphanumeric characters
- have each domain label consist of alphanumeric characters, separated only by hyphens, if any. | `johndoe@gmail.com` and `janedoe@hotmail.com` are examples of emails you can provide in an [`add`](#adding-a-patient-add), [`edit`](#editing-a-patient--edit) or [`addec`](#adding-an-emergency-contact--addec) command `email/` parameter. | +| **`address/` ADDRESS** | Addresses can be any value, but they cannot be blank. | `123, Clementi Rd, 123465` and `Block 123, Jurong West Street 6, #08-111` are examples of addresses you can provide in an [`add`](#adding-a-patient-add), [`edit`](#editing-a-patient--edit) or [`addec`](#adding-an-emergency-contact--addec) command `address/` parameter. | +| **`ecname/` EMERGENCY_CONTACT_
NAME** | Refer to `name/` above. | | +| **`ecphone/` EMERGENCY_CONTACT_
PHONE_NUMBER** | Refer to `phone/` above. | | +| **`ecrs/` EMERGENCY_CONTACT_
RELATIONSHIP** | This parameter accepts the following valid inputs:
`Parent, Mother, Father, Child, Son, Daughter, Sibling, Brother, Sister, Friend, Spouse, Husband, Wife, Partner, Cousin, Relative, Uncle, Aunt, Grandparent, Grandmother, Grandfather, Grandchild, Grandson, Granddaughter`.
It is case-insensitive. | `Spouse` and `GRANDcHILD` are examples of relationships you can provide in an [`add`](#adding-a-patient-add), [`edit`](#editing-a-patient--edit) or [`addec`](#adding-an-emergency-contact--addec) command `ecrs/` parameter. | +| **`dname/` DOCTOR_NAME** | Refer to `name/` above. | | +| **`demail/` DOCTOR_EMAIL** | Refer to `email/` above. | | +| **`dphone/` DOCTOR_PHONE** | Refer to `phone/` above. | | +| **`t/` TAG** | Tags should only contain alphanumeric characters, spaces, periods `.` or hyphens `-`. | `Mandarin-speaking`, `short-term patient` are examples of tags you can provide in an [`add`](#adding-a-patient-add), [`edit`](#editing-a-patient--edit) or [`addec`](#adding-an-emergency-contact--addec) command `t/` parameter. | + +[↑ Back to top](#table-of-contents) --------------------------------------------------------------------------------------------------------------------- +
## Command summary -Action | Format, Examples ---------|------------------ -**Add** | `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]…​`
e.g., `add n/James Ho p/22224444 e/jamesho@example.com a/123, Clementi Rd, 1234665 t/friend t/colleague` -**Clear** | `clear` -**Delete** | `delete INDEX`
e.g., `delete 3` -**Edit** | `edit INDEX [n/NAME] [p/PHONE_NUMBER] [e/EMAIL] [a/ADDRESS] [t/TAG]…​`
e.g.,`edit 2 n/James Lee e/jameslee@example.com` -**Find** | `find KEYWORD [MORE_KEYWORDS]`
e.g., `find James Jake` -**List** | `list` -**Help** | `help` +| Action | Format, Examples | +|---------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **Add** | `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS ecname/EMERGENCY_CONTACT_NAME ecphone/EMERGENCY_CONTACT_PHONE ecrs/EMERGENCY_CONTACT_RELATIONSHIP [t/TAG]…​`

e.g., `add n/James Ho p/81234567 e/jamesho@example.com a/123, Clementi Rd, 123465` `ecname/Lim Jun Wei ecphone/98765678 ecrs/Brother` `dname/Sam Lim dphone/9987766 demail/samlim@hotmail.com` `t/friend t/colleague` | +| **Add Emergency Contact** | `addec INDEX ecname/EMERGENCY_CONTACT_NAME ecphone/EMERGENCY_CONTACT_PHONE ecrs/EMERGENCY_CONTACT_RELATIONSHIP`
e.g., `addec 1 ecname/Shannon Wong ecphone/84651325 ecrs/Daughter` | +| **Archive** | `archive [DESCRIPTION]`
e.g., `archive before major update` | +| **Clear** | `clear` | +| **Delete** | `delete INDEX`
e.g., `delete 3` | +| **Delete Archive File** | `deletearchive FILE_NAME`
e.g., `deletearchive addressbook-2024-11-06T20-29-05.7609475-example.json` | +| **Edit** | `edit INDEX [n/NAME] [p/PHONE_NUMBER] [e/EMAIL] [a/ADDRESS] [dname/DOCTOR_NAME] [dphone/DOCTOR_PHONE] [demail/DOCTOR_EMAIL] [t/TAG]…​`
e.g.,`edit 2 n/James Lee e/jameslee@example.com` | +| **Find** | `find KEYWORD [MORE_KEYWORDS]`
e.g., `find James Jake` | +| **Find Doctor** | `finddoc KEYWORD [MORE_KEYWORDS]`
e.g., `find Tan Sheeran` | +| **Help** | `help` | +| **List** | `list [SORT_ORDER]`
e.g., `list timeadded desc` | +| **List Archive Files** | `listarchives` | +| **Load Archive File** | `loadarchive FILE_NAME`
e.g., `loadarchive addressbook-2024-11-06T20-29-05.7609475-example.json` | +| **Redo** | `redo` | +| **Undo** | `undo` | + +[↑ Back to top](#table-of-contents) diff --git a/docs/_config.yml b/docs/_config.yml index 6bd245d8f4e..39cb644c7e2 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -1,4 +1,4 @@ -title: "AB-3" +title: "MedConnect" theme: minima header_pages: @@ -8,7 +8,7 @@ header_pages: markdown: kramdown -repository: "se-edu/addressbook-level3" +repository: "ay2425s1-cs2103t-t13-1/tp" github_icon: "images/github-icon.png" plugins: diff --git a/docs/_sass/minima/_base.scss b/docs/_sass/minima/_base.scss index 0d3f6e80ced..2d2647b64c0 100644 --- a/docs/_sass/minima/_base.scss +++ b/docs/_sass/minima/_base.scss @@ -288,7 +288,7 @@ table { text-align: center; } .site-header:before { - content: "AB-3"; + content: "MedConnect"; font-size: 32px; } } diff --git a/docs/diagrams/ArchiveSequenceDiagram.puml b/docs/diagrams/ArchiveSequenceDiagram.puml new file mode 100644 index 00000000000..ff81f0a75b5 --- /dev/null +++ b/docs/diagrams/ArchiveSequenceDiagram.puml @@ -0,0 +1,20 @@ +@startuml +actor User +participant ArchiveCommand +participant ModelManager +participant FileUtil + +User -> ArchiveCommand: execute(Model) +activate ArchiveCommand + +ArchiveCommand -> ModelManager: archiveAddressBook(Filename) +activate ModelManager + +ModelManager -> FileUtil: Create archive directory if not exists +ModelManager -> FileUtil: Copy current address book to archive directory +ModelManager --> ArchiveCommand: +deactivate ModelManager + +ArchiveCommand --> User: CommandResult(MESSAGE_SUCCESS) +deactivate ArchiveCommand +@enduml diff --git a/docs/diagrams/BetterModelClassDiagram.puml b/docs/diagrams/BetterModelClassDiagram.puml index 598474a5c82..4cbfbc6ab07 100644 --- a/docs/diagrams/BetterModelClassDiagram.puml +++ b/docs/diagrams/BetterModelClassDiagram.puml @@ -14,8 +14,10 @@ UniquePersonList -right-> Person Person -up-> "*" Tag -Person *--> Name -Person *--> Phone -Person *--> Email -Person *--> Address +Person *--> "1"Name +Person *--> "1"Phone +Person *--> "1"Email +Person *--> "1"Address +Person --> "1..*"EmergencyContact +Person --> "1"Doctor @enduml diff --git a/docs/diagrams/DoctorClassDiagram.puml b/docs/diagrams/DoctorClassDiagram.puml new file mode 100644 index 00000000000..b2d43fd4dee --- /dev/null +++ b/docs/diagrams/DoctorClassDiagram.puml @@ -0,0 +1,11 @@ +@startuml +!include style.puml +skinparam arrowThickness 1.1 +skinparam arrowColor MODEL_COLOR +skinparam classBackgroundColor MODEL_COLOR + +Doctor *--> "1"DoctorName +Doctor *--> "1"Phone +Doctor *--> "1"Email + +@enduml diff --git a/docs/diagrams/EmergencyContactClassDiagram.puml b/docs/diagrams/EmergencyContactClassDiagram.puml new file mode 100644 index 00000000000..13003c29dca --- /dev/null +++ b/docs/diagrams/EmergencyContactClassDiagram.puml @@ -0,0 +1,11 @@ +@startuml +!include style.puml +skinparam arrowThickness 1.1 +skinparam arrowColor MODEL_COLOR +skinparam classBackgroundColor MODEL_COLOR + +EmergencyContact *--> "1"Name +EmergencyContact *--> "1"Phone +EmergencyContact *--> "1"Relationship + +@enduml diff --git a/docs/diagrams/FindSequenceDiagram.puml b/docs/diagrams/FindSequenceDiagram.puml new file mode 100644 index 00000000000..ca8158fe533 --- /dev/null +++ b/docs/diagrams/FindSequenceDiagram.puml @@ -0,0 +1,26 @@ +@startuml FindSequenceDiagram + +actor User +participant FindCommand +participant ModelManager + +User -> FindCommand: execute(Model) +activate FindCommand + +FindCommand -> ModelManager: updateFilteredPersonList(Predicate) +activate ModelManager + +ModelManager -> ModelManager: updateFilteredPersonList(Predicate) + +ModelManager -> ModelManager: setPredicate(Predicate) +ModelManager --> FindCommand: + +FindCommand -> ModelManager: getFilteredPersonList() +ModelManager -> ModelManager: size() +ModelManager --> FindCommand: ObservableList +deactivate ModelManager + +FindCommand --> User: CommandResult +deactivate FindCommand + +@enduml diff --git a/docs/diagrams/ModelClassDiagram.puml b/docs/diagrams/ModelClassDiagram.puml index 0de5673070d..01fb95715e6 100644 --- a/docs/diagrams/ModelClassDiagram.puml +++ b/docs/diagrams/ModelClassDiagram.puml @@ -19,6 +19,8 @@ Class Email Class Name Class Phone Class Tag +Class EmergencyContact +Class Doctor Class I #FFFFFF } @@ -37,11 +39,13 @@ UserPrefs .up.|> ReadOnlyUserPrefs AddressBook *--> "1" UniquePersonList UniquePersonList --> "~* all" Person -Person *--> Name -Person *--> Phone -Person *--> Email -Person *--> Address +Person *--> "1" Name +Person *--> "1" Phone +Person *--> "1" Email +Person *--> "1" Address +Person *--> "1..*" EmergencyContact Person *--> "*" Tag +Person *--> "1" Doctor Person -[hidden]up--> I UniquePersonList -[hidden]right-> I diff --git a/docs/diagrams/PersonClassDiagram.puml b/docs/diagrams/PersonClassDiagram.puml new file mode 100644 index 00000000000..05ded23e1c4 --- /dev/null +++ b/docs/diagrams/PersonClassDiagram.puml @@ -0,0 +1,16 @@ +@startuml +!include style.puml +skinparam arrowThickness 1.1 +skinparam arrowColor MODEL_COLOR +skinparam classBackgroundColor MODEL_COLOR + + +Person *--> "1"Name +Person *--> "1"Phone +Person *--> "1"Email +Person *--> "1..*"EmergencyContact +Person *--> "1"Doctor +Person *--> "1"Address +Person *--> "*" Tag + +@enduml diff --git a/docs/diagrams/StorageClassDiagram.puml b/docs/diagrams/StorageClassDiagram.puml index a821e06458c..2e8e582bdf8 100644 --- a/docs/diagrams/StorageClassDiagram.puml +++ b/docs/diagrams/StorageClassDiagram.puml @@ -20,6 +20,8 @@ Class JsonAddressBookStorage Class JsonSerializableAddressBook Class JsonAdaptedPerson Class JsonAdaptedTag +Class JsonAdaptedDoctor +Class JsonAdaptedEmergencyContact } } @@ -39,5 +41,7 @@ JsonAddressBookStorage .up.|> AddressBookStorage JsonAddressBookStorage ..> JsonSerializableAddressBook JsonSerializableAddressBook --> "*" JsonAdaptedPerson JsonAdaptedPerson --> "*" JsonAdaptedTag +JsonAdaptedPerson --> "*" JsonAdaptedEmergencyContact +JsonAdaptedPerson --> "1" JsonAdaptedDoctor @enduml diff --git a/docs/diagrams/UiClassDiagram.puml b/docs/diagrams/UiClassDiagram.puml index 95473d5aa19..497c0da171a 100644 --- a/docs/diagrams/UiClassDiagram.puml +++ b/docs/diagrams/UiClassDiagram.puml @@ -1,4 +1,4 @@ -@startuml + @startuml !include style.puml skinparam arrowThickness 1.1 skinparam arrowColor UI_COLOR_T4 @@ -15,6 +15,8 @@ Class PersonListPanel Class PersonCard Class StatusBarFooter Class CommandBox +Class EmergencyContactListPanel +Class EmergencyContactCard } package Model <> { @@ -43,14 +45,19 @@ MainWindow -left-|> UiPart ResultDisplay --|> UiPart CommandBox --|> UiPart PersonListPanel --|> UiPart -PersonCard --|> UiPart +PersonCard -left-|> UiPart +EmergencyContactListPanel --|> UiPart +EmergencyContactCard --|> UiPart StatusBarFooter --|> UiPart HelpWindow --|> UiPart PersonCard ..> Model +PersonCard *-down-> EmergencyContactListPanel +EmergencyContactListPanel -down-> "1..*" EmergencyContactCard UiManager -right-> Logic MainWindow -left-> Logic +'Unsure how to add EmergencyContact UI classes to this segment' PersonListPanel -[hidden]left- HelpWindow HelpWindow -[hidden]left- CommandBox CommandBox -[hidden]left- ResultDisplay diff --git a/docs/diagrams/UndoRedoState1.puml b/docs/diagrams/UndoRedoState1.puml index 5a41e9e1651..b746daf1785 100644 --- a/docs/diagrams/UndoRedoState1.puml +++ b/docs/diagrams/UndoRedoState1.puml @@ -12,6 +12,7 @@ package States <> { class State3 as "ab2:AddressBook" } + State1 -[hidden]right-> State2 State2 -[hidden]right-> State3 diff --git a/docs/diagrams/UndoRedoState2.puml b/docs/diagrams/UndoRedoState2.puml index ad32fce1b0b..acc1aea0eb7 100644 --- a/docs/diagrams/UndoRedoState2.puml +++ b/docs/diagrams/UndoRedoState2.puml @@ -12,6 +12,7 @@ package States <> { class State3 as "ab2:AddressBook" } + State1 -[hidden]right-> State2 State2 -[hidden]right-> State3 diff --git a/docs/diagrams/UndoRedoState3.puml b/docs/diagrams/UndoRedoState3.puml index 9187a690036..52785e39aa9 100644 --- a/docs/diagrams/UndoRedoState3.puml +++ b/docs/diagrams/UndoRedoState3.puml @@ -12,6 +12,7 @@ package States <> { class State3 as "ab2:AddressBook" } + State1 -[hidden]right-> State2 State2 -[hidden]right-> State3 diff --git a/docs/diagrams/UndoRedoState4.puml b/docs/diagrams/UndoRedoState4.puml index 2bc631ffcd0..a9b2b93907b 100644 --- a/docs/diagrams/UndoRedoState4.puml +++ b/docs/diagrams/UndoRedoState4.puml @@ -12,6 +12,7 @@ package States <> { class State3 as "ab2:AddressBook" } + State1 -[hidden]right-> State2 State2 -[hidden]right-> State3 diff --git a/docs/diagrams/UndoRedoState5.puml b/docs/diagrams/UndoRedoState5.puml index e77b04104aa..a76733d19ba 100644 --- a/docs/diagrams/UndoRedoState5.puml +++ b/docs/diagrams/UndoRedoState5.puml @@ -12,6 +12,7 @@ package States <> { class State3 as "ab3:AddressBook" } + State1 -[hidden]right-> State2 State2 -[hidden]right-> State3 diff --git a/docs/images/ArchiveSequenceDiagram.png b/docs/images/ArchiveSequenceDiagram.png new file mode 100644 index 00000000000..7a317bfe08f Binary files /dev/null and b/docs/images/ArchiveSequenceDiagram.png differ diff --git a/docs/images/BetterModelClassDiagram.png b/docs/images/BetterModelClassDiagram.png index 02a42e35e76..6b14adb38cd 100644 Binary files a/docs/images/BetterModelClassDiagram.png and b/docs/images/BetterModelClassDiagram.png differ diff --git a/docs/images/FindSequenceDiagram.png b/docs/images/FindSequenceDiagram.png new file mode 100644 index 00000000000..39c2e35fcf4 Binary files /dev/null and b/docs/images/FindSequenceDiagram.png differ diff --git a/docs/images/ModelClassDiagram.png b/docs/images/ModelClassDiagram.png index a19fb1b4ac8..d4322d26aef 100644 Binary files a/docs/images/ModelClassDiagram.png and b/docs/images/ModelClassDiagram.png differ diff --git a/docs/images/PersonClassDiagram.png b/docs/images/PersonClassDiagram.png new file mode 100644 index 00000000000..1fcc8253290 Binary files /dev/null and b/docs/images/PersonClassDiagram.png differ diff --git a/docs/images/Quickstart-new-terminal-MacOS.png b/docs/images/Quickstart-new-terminal-MacOS.png new file mode 100644 index 00000000000..a76cf7c0fb5 Binary files /dev/null and b/docs/images/Quickstart-new-terminal-MacOS.png differ diff --git a/docs/images/Quickstart-new-terminal.png b/docs/images/Quickstart-new-terminal.png new file mode 100644 index 00000000000..e2c36f0d353 Binary files /dev/null and b/docs/images/Quickstart-new-terminal.png differ diff --git a/docs/images/StorageClassDiagram.png b/docs/images/StorageClassDiagram.png index 18fa4d0d51f..13819f6a7e3 100644 Binary files a/docs/images/StorageClassDiagram.png and b/docs/images/StorageClassDiagram.png differ diff --git a/docs/images/Suggestion.png b/docs/images/Suggestion.png new file mode 100644 index 00000000000..9cef92177d6 Binary files /dev/null and b/docs/images/Suggestion.png differ diff --git a/docs/images/Ui.png b/docs/images/Ui.png index 5bd77847aa2..f1f41ac4e7a 100644 Binary files a/docs/images/Ui.png and b/docs/images/Ui.png differ diff --git a/docs/images/UiClassDiagram.png b/docs/images/UiClassDiagram.png index 11f06d68671..dee6dd1df6f 100644 Binary files a/docs/images/UiClassDiagram.png and b/docs/images/UiClassDiagram.png differ diff --git a/docs/images/aldentantan.png b/docs/images/aldentantan.png new file mode 100644 index 00000000000..61ee03fc725 Binary files /dev/null and b/docs/images/aldentantan.png differ diff --git a/docs/images/autocomplete.gif b/docs/images/autocomplete.gif new file mode 100644 index 00000000000..b575daf365c Binary files /dev/null and b/docs/images/autocomplete.gif differ diff --git a/docs/images/findAlexDavidResult.png b/docs/images/findAlexDavidResult.png deleted file mode 100644 index 235da1c273e..00000000000 Binary files a/docs/images/findAlexDavidResult.png and /dev/null differ diff --git a/docs/images/findDavRoyResult.png b/docs/images/findDavRoyResult.png new file mode 100644 index 00000000000..6c2821567ef Binary files /dev/null and b/docs/images/findDavRoyResult.png differ diff --git a/docs/images/finddocZhouResult.png b/docs/images/finddocZhouResult.png new file mode 100644 index 00000000000..3b315415b9a Binary files /dev/null and b/docs/images/finddocZhouResult.png differ diff --git a/docs/images/helpMessage.png b/docs/images/helpMessage.png deleted file mode 100644 index b1f70470137..00000000000 Binary files a/docs/images/helpMessage.png and /dev/null differ diff --git a/docs/images/helpWindow.png b/docs/images/helpWindow.png new file mode 100644 index 00000000000..65401169b67 Binary files /dev/null and b/docs/images/helpWindow.png differ diff --git a/docs/images/johnwz123.png b/docs/images/johnwz123.png new file mode 100644 index 00000000000..a4a20ee7696 Binary files /dev/null and b/docs/images/johnwz123.png differ diff --git a/docs/images/kellywsq03.png b/docs/images/kellywsq03.png new file mode 100644 index 00000000000..446278bf65f Binary files /dev/null and b/docs/images/kellywsq03.png differ diff --git a/docs/images/red_autocomplete.png b/docs/images/red_autocomplete.png new file mode 100644 index 00000000000..cd212f3c199 Binary files /dev/null and b/docs/images/red_autocomplete.png differ diff --git a/docs/images/saajidshaik02.png b/docs/images/saajidshaik02.png new file mode 100644 index 00000000000..5a59b39ee2b Binary files /dev/null and b/docs/images/saajidshaik02.png differ diff --git a/docs/images/sampleUiImage.png b/docs/images/sampleUiImage.png new file mode 100644 index 00000000000..a47da8a747f Binary files /dev/null and b/docs/images/sampleUiImage.png differ diff --git a/docs/images/similarprefix.png b/docs/images/similarprefix.png new file mode 100644 index 00000000000..da0e85ad045 Binary files /dev/null and b/docs/images/similarprefix.png differ diff --git a/docs/images/uitutorial.png b/docs/images/uitutorial.png new file mode 100644 index 00000000000..00b86d6b0c5 Binary files /dev/null and b/docs/images/uitutorial.png differ diff --git a/docs/index.md b/docs/index.md index 7601dbaad0d..6bd7abcad38 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,19 +1,25 @@ --- layout: page -title: AddressBook Level-3 +title: MedConnect --- -[![CI Status](https://github.com/se-edu/addressbook-level3/workflows/Java%20CI/badge.svg)](https://github.com/se-edu/addressbook-level3/actions) -[![codecov](https://codecov.io/gh/se-edu/addressbook-level3/branch/master/graph/badge.svg)](https://codecov.io/gh/se-edu/addressbook-level3) +[![CI Status](https://github.com/AY2425S1-CS2103T-T13-1/tp/workflows/Java%20CI/badge.svg)](https://github.com/AY2425S1-CS2103T-T13-1/tp/actions) +[![codecov](https://codecov.io/gh/AY2425S1-CS2103T-T13-1/tp/graph/badge.svg)](https://codecov.io/github/AY2425S1-CS2103T-T13-1/tp) ![Ui](images/Ui.png) -**AddressBook is a desktop application for managing your contact details.** While it has a GUI, most of the user interactions happen using a CLI (Command Line Interface). +**MedConnect is a desktop application for healthcare administrators in old folks homes for dementia patients to consolidate contacts of patients and related information into a single database.** -* If you are interested in using AddressBook, head over to the [_Quick Start_ section of the **User Guide**](UserGuide.html#quick-start). -* If you are interested about developing AddressBook, the [**Developer Guide**](DeveloperGuide.html) is a good place to start. +It enables speedy lookups and updates, ensuring that administrators can quickly connect with the right people, from patients' doctors to their families, when every second counts. + +While it has a GUI (Graphical User Interface), most of the user interactions happen using a CLI (Command Line Interface). + +* If you are interested in using MedConnect, head over to the [_Quick Start_ section of the **User Guide**](UserGuide.html#quick-start). +* If you are interested about developing MedConnect, the [**Developer Guide**](DeveloperGuide.html) is a good place to start. **Acknowledgements** +This project is based on the AddressBook-Level3 project created by the [SE-EDU initiative](https://se-education.org). + * Libraries used: [JavaFX](https://openjfx.io/), [Jackson](https://github.com/FasterXML/jackson), [JUnit5](https://github.com/junit-team/junit5) diff --git a/src/main/java/seedu/address/MainApp.java b/src/main/java/seedu/address/MainApp.java index 678ddc8c218..fcc9a8c6468 100644 --- a/src/main/java/seedu/address/MainApp.java +++ b/src/main/java/seedu/address/MainApp.java @@ -12,6 +12,7 @@ import seedu.address.commons.core.Version; import seedu.address.commons.exceptions.DataLoadingException; import seedu.address.commons.util.ConfigUtil; +import seedu.address.commons.util.FileUtil; import seedu.address.commons.util.StringUtil; import seedu.address.logic.Logic; import seedu.address.logic.LogicManager; @@ -36,7 +37,7 @@ */ public class MainApp extends Application { - public static final Version VERSION = new Version(0, 2, 2, true); + public static final Version VERSION = new Version(1, 6, 0, true); private static final Logger logger = LogsCenter.getLogger(MainApp.class); @@ -61,6 +62,9 @@ public void init() throws Exception { storage = new StorageManager(addressBookStorage, userPrefsStorage); model = initModelManager(storage, userPrefs); + if (!FileUtil.isFileExists(model.getAddressBookFilePath())) { + storage.saveAddressBook(model.getAddressBook()); + } logic = new LogicManager(model, storage); @@ -171,6 +175,7 @@ protected UserPrefs initPrefs(UserPrefsStorage storage) { @Override public void start(Stage primaryStage) { logger.info("Starting AddressBook " + MainApp.VERSION); + primaryStage.setMaximized(true); ui.start(primaryStage); } diff --git a/src/main/java/seedu/address/commons/core/filename/Filename.java b/src/main/java/seedu/address/commons/core/filename/Filename.java new file mode 100644 index 00000000000..e9a473e9465 --- /dev/null +++ b/src/main/java/seedu/address/commons/core/filename/Filename.java @@ -0,0 +1,65 @@ +package seedu.address.commons.core.filename; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +/** + * + */ +public class Filename { + public static final String MESSAGE_CONSTRAINTS_BLANK = "Filename should not be blank"; + public static final String MESSAGE_CONSTRAINTS = "Invalid filename. Filenames should not contain any of the " + + "following characters: < > : \" / \\ | ? *"; + + /* Regex pattern for valid Windows filenames: + * - Must not contain any of the following characters: < > : " / \ | ? * + */ + private static final String WINDOWS_VALIDATION_REGEX = "^[^<>:\"/\\\\|?*]*$"; + + + /* Regex pattern for valid Linux and MacOS filenames: + * - Must not contain a forward slash (/) + */ + private static final String LINUX_MAC_VALIDATION_REGEX = "^[^/]*$"; + + private String filename; + + /** + * Constructs a {@code Filename}. + * + * @param filename A valid filename. + */ + public Filename(String filename) { + requireNonNull(filename); + checkArgument(isValidFilename(filename), MESSAGE_CONSTRAINTS); + this.filename = filename; + } + + /** + * Returns true if a given string is a valid filename. + * + * @param test String to test. + */ + public static boolean isValidFilename(String test) { + return test.matches(WINDOWS_VALIDATION_REGEX) && test.matches(LINUX_MAC_VALIDATION_REGEX); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof Filename otherFilename)) { + return false; + } + + return filename.equals(otherFilename.filename); + } + + @Override + public String toString() { + return filename; + } +} diff --git a/src/main/java/seedu/address/commons/core/index/Index.java b/src/main/java/seedu/address/commons/core/index/Index.java index dd170d8b68d..a8b1e2e47e2 100644 --- a/src/main/java/seedu/address/commons/core/index/Index.java +++ b/src/main/java/seedu/address/commons/core/index/Index.java @@ -25,14 +25,6 @@ private Index(int zeroBasedIndex) { this.zeroBasedIndex = zeroBasedIndex; } - public int getZeroBased() { - return zeroBasedIndex; - } - - public int getOneBased() { - return zeroBasedIndex + 1; - } - /** * Creates a new {@code Index} using a zero-based index. */ @@ -47,6 +39,14 @@ public static Index fromOneBased(int oneBasedIndex) { return new Index(oneBasedIndex - 1); } + public int getZeroBased() { + return zeroBasedIndex; + } + + public int getOneBased() { + return zeroBasedIndex + 1; + } + @Override public boolean equals(Object other) { if (other == this) { @@ -66,4 +66,5 @@ public boolean equals(Object other) { public String toString() { return new ToStringBuilder(this).add("zeroBasedIndex", zeroBasedIndex).toString(); } + } diff --git a/src/main/java/seedu/address/commons/util/StringUtil.java b/src/main/java/seedu/address/commons/util/StringUtil.java index 61cc8c9a1cb..c5ed643f89e 100644 --- a/src/main/java/seedu/address/commons/util/StringUtil.java +++ b/src/main/java/seedu/address/commons/util/StringUtil.java @@ -14,11 +14,10 @@ public class StringUtil { /** * Returns true if the {@code sentence} contains the {@code word}. - * Ignores case, but a full word match is required. + * Ignores case and returns a match as long as the word matches a substring in the sentence. *
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 @@ -35,7 +34,7 @@ public static boolean containsWordIgnoreCase(String sentence, String word) { String[] wordsInPreppedSentence = preppedSentence.split("\\s+"); return Arrays.stream(wordsInPreppedSentence) - .anyMatch(preppedWord::equalsIgnoreCase); + .anyMatch(wordInSentence -> wordInSentence.toLowerCase().contains(preppedWord.toLowerCase())); } /** diff --git a/src/main/java/seedu/address/logic/Messages.java b/src/main/java/seedu/address/logic/Messages.java index ecd32c31b53..f026a156b6d 100644 --- a/src/main/java/seedu/address/logic/Messages.java +++ b/src/main/java/seedu/address/logic/Messages.java @@ -5,6 +5,7 @@ import java.util.stream.Stream; import seedu.address.logic.parser.Prefix; +import seedu.address.model.person.EmergencyContact; import seedu.address.model.person.Person; /** @@ -13,8 +14,12 @@ public class Messages { public static final String MESSAGE_UNKNOWN_COMMAND = "Unknown command"; - public static final String MESSAGE_INVALID_COMMAND_FORMAT = "Invalid command format! \n%1$s"; + public static final String MESSAGE_INVALID_COMMAND_FORMAT = "Invalid command format! \n\n%1$s"; public static final String MESSAGE_INVALID_PERSON_DISPLAYED_INDEX = "The person index provided is invalid"; + public static final String MESSAGE_INVALID_EMERGENCY_CONTACT_DISPLAYED_INDEX = + "The emergency contact index provided is invalid"; + public static final String MESSAGE_LAST_EMERGENCY_CONTACT_INDEX = + "The person must have at least one emergency contact. You cannot delete the last emergency contact."; public static final String MESSAGE_PERSONS_LISTED_OVERVIEW = "%1$d persons listed!"; public static final String MESSAGE_DUPLICATE_FIELDS = "Multiple values specified for the following single-valued field(s): "; @@ -31,19 +36,37 @@ public static String getErrorMessageForDuplicatePrefixes(Prefix... duplicatePref return MESSAGE_DUPLICATE_FIELDS + String.join(" ", duplicateFields); } + /** + * Formats the {@code person} for display to the user. + */ + public static String formatEmergencyContact(EmergencyContact emergencyContact) { + final StringBuilder builder = new StringBuilder(); + builder.append(emergencyContact.getName()) + .append("; Phone: ") + .append(emergencyContact.getPhone()) + .append("; Relationship: ") + .append(emergencyContact.getRelationship()); + return builder.toString(); + } + /** * Formats the {@code person} for display to the user. */ public static String format(Person person) { final StringBuilder builder = new StringBuilder(); builder.append(person.getName()) - .append("; Phone: ") + .append("\nPhone: ") .append(person.getPhone()) - .append("; Email: ") + .append("\nEmail: ") .append(person.getEmail()) - .append("; Address: ") + .append("\nAddress: ") .append(person.getAddress()) - .append("; Tags: "); + .append("\nEmergency Contact(s): "); + person.getEmergencyContacts().forEach(x -> { + builder.append(x); + builder.append("\n"); + }); + builder.append("\nTags: "); person.getTags().forEach(builder::append); return builder.toString(); } diff --git a/src/main/java/seedu/address/logic/commands/AddCommand.java b/src/main/java/seedu/address/logic/commands/AddCommand.java index 5d7185a9680..3e10e4eed58 100644 --- a/src/main/java/seedu/address/logic/commands/AddCommand.java +++ b/src/main/java/seedu/address/logic/commands/AddCommand.java @@ -2,7 +2,13 @@ import static java.util.Objects.requireNonNull; import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DOC_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DOC_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DOC_PHONE; import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EMERGENCY_CONTACT_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EMERGENCY_CONTACT_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EMERGENCY_CONTACT_RELATIONSHIP; import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; @@ -20,23 +26,36 @@ public class AddCommand extends Command { public static final String COMMAND_WORD = "add"; - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a person to the address book. " + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a person to the address book. \n\n" + "Parameters: " + PREFIX_NAME + "NAME " + PREFIX_PHONE + "PHONE " + PREFIX_EMAIL + "EMAIL " + PREFIX_ADDRESS + "ADDRESS " - + "[" + PREFIX_TAG + "TAG]...\n" + + PREFIX_EMERGENCY_CONTACT_NAME + "EMERGENCY CONTACT NAME " + + PREFIX_EMERGENCY_CONTACT_PHONE + "EMERGENCY CONTACT PHONE " + + PREFIX_EMERGENCY_CONTACT_RELATIONSHIP + "EMERGENCY CONTACT RELATIONSHIP " + + PREFIX_DOC_NAME + "DOCTOR NAME " + + PREFIX_DOC_PHONE + "DOCTOR PHONE " + + PREFIX_DOC_EMAIL + "DOCTOR EMAIL " + + "[" + PREFIX_TAG + "TAG]...\n\n" + "Example: " + COMMAND_WORD + " " + PREFIX_NAME + "John Doe " + PREFIX_PHONE + "98765432 " + PREFIX_EMAIL + "johnd@example.com " + PREFIX_ADDRESS + "311, Clementi Ave 2, #02-25 " - + PREFIX_TAG + "friends " - + PREFIX_TAG + "owesMoney"; + + PREFIX_EMERGENCY_CONTACT_NAME + "Beatrice Bean " + + PREFIX_EMERGENCY_CONTACT_PHONE + "91324856 " + + PREFIX_EMERGENCY_CONTACT_RELATIONSHIP + "Son " + + PREFIX_DOC_NAME + "Tan Wei Ming " + + PREFIX_DOC_PHONE + "62345678 " + + PREFIX_DOC_EMAIL + "tanweiming@gmail.com " + + PREFIX_TAG + "needs mobility support " + + PREFIX_TAG + "short-term stay"; public static final String MESSAGE_SUCCESS = "New person added: %1$s"; - public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book"; + public static final String MESSAGE_DUPLICATE_PERSON = + "A person with the same phone number already exists in the address book"; private final Person toAdd; diff --git a/src/main/java/seedu/address/logic/commands/AddEmergencyContactCommand.java b/src/main/java/seedu/address/logic/commands/AddEmergencyContactCommand.java new file mode 100644 index 00000000000..3b117b41660 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/AddEmergencyContactCommand.java @@ -0,0 +1,125 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EMERGENCY_CONTACT_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EMERGENCY_CONTACT_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EMERGENCY_CONTACT_RELATIONSHIP; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; + +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import seedu.address.commons.core.index.Index; +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.Address; +import seedu.address.model.person.Doctor; +import seedu.address.model.person.Email; +import seedu.address.model.person.EmergencyContact; +import seedu.address.model.person.Name; +import seedu.address.model.person.Person; +import seedu.address.model.person.Phone; +import seedu.address.model.tag.Tag; + +/** + * Adds an emergency contact to an existing person in the address book. + */ +public class AddEmergencyContactCommand extends Command { + + public static final String COMMAND_WORD = "addec"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a new emergency contact to the person " + + "identified by the index number used in the displayed person list.\n\n" + + "Parameters: INDEX (must be a positive integer) " + + PREFIX_EMERGENCY_CONTACT_NAME + "EMERGENCY CONTACT NAME " + + PREFIX_EMERGENCY_CONTACT_PHONE + "EMERGENCY CONTACT PHONE " + + PREFIX_EMERGENCY_CONTACT_RELATIONSHIP + "EMERGENCY CONTACT RELATIONSHIP\n\n" + + "Example: " + COMMAND_WORD + " 1 " + + PREFIX_EMERGENCY_CONTACT_NAME + "Sarah Lim " + + PREFIX_EMERGENCY_CONTACT_PHONE + "91234567 " + + PREFIX_EMERGENCY_CONTACT_RELATIONSHIP + "Granddaughter"; + + public static final String MESSAGE_SUCCESS = "Added emergency contact: %1$s"; + public static final String MESSAGE_DUPLICATE_EMERGENCY_CONTACT = "This person is already an emergency contact."; + + private final Index index; + private final EmergencyContact emergencyContactToAdd; + + /** + * @param index of the person in the filtered person list to add + * @param emergencyContactToAdd to + */ + public AddEmergencyContactCommand(Index index, EmergencyContact emergencyContactToAdd) { + requireNonNull(index); + this.index = index; + this.emergencyContactToAdd = emergencyContactToAdd; + } + + /** + * Creates and returns a {@code Person} with the details of {@code personToAddEmergencyContactTo} + * with an added {@code emergencyContactToAdd}. + */ + public static Person createEditedPerson(Person personToEdit, EmergencyContact emergencyContactToAdd) { + assert personToEdit != null; + + Set personToEditEmergencyContacts = personToEdit.getEmergencyContacts(); + Set updatedEmergencyContacts = new LinkedHashSet<>(personToEditEmergencyContacts); + updatedEmergencyContacts.add(emergencyContactToAdd); + + Name name = personToEdit.getName(); + Phone phone = personToEdit.getPhone(); + Email email = personToEdit.getEmail(); + Address address = personToEdit.getAddress(); + Doctor doctor = personToEdit.getDoctor(); + Set tags = personToEdit.getTags(); + + return new Person(name, phone, email, address, updatedEmergencyContacts, doctor, tags); + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredPersonList(); + + if (index.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + Person personToAddEmergencyContactTo = lastShownList.get(index.getZeroBased()); + + if (personToAddEmergencyContactTo.hasEmergencyContact(emergencyContactToAdd)) { + throw new CommandException(MESSAGE_DUPLICATE_EMERGENCY_CONTACT); + } + + Person editedPerson = createEditedPerson(personToAddEmergencyContactTo, emergencyContactToAdd); + + model.setPerson(personToAddEmergencyContactTo, editedPerson); + model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + return new CommandResult(String.format(MESSAGE_SUCCESS, Messages.format(editedPerson))); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof AddEmergencyContactCommand)) { + return false; + } + + AddEmergencyContactCommand otherAddEmergencyContactCommand = (AddEmergencyContactCommand) other; + return emergencyContactToAdd.equals(otherAddEmergencyContactCommand.emergencyContactToAdd); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("toAdd", emergencyContactToAdd) + .toString(); + } +} diff --git a/src/main/java/seedu/address/logic/commands/ArchiveCommand.java b/src/main/java/seedu/address/logic/commands/ArchiveCommand.java new file mode 100644 index 00000000000..2bf976a7f4a --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ArchiveCommand.java @@ -0,0 +1,60 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.io.IOException; + +import seedu.address.commons.core.filename.Filename; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; + +/** + * Archives the address book. + */ +public class ArchiveCommand extends Command { + + public static final String COMMAND_WORD = "archive"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Archives the address book.\n\n" + + "Example: " + COMMAND_WORD + " " + "1st Quarter 2021"; + public static final String MESSAGE_SUCCESS = "Address book has been archived successfully!"; + public static final String MESSAGE_FAILURE = "Address book failed to be archived. Please try again later."; + + private final Filename filename; + + public ArchiveCommand() { + this.filename = new Filename(""); + } + + public ArchiveCommand(Filename filename) { + this.filename = filename; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + try { + model.archiveAddressBook(filename); + } catch (IOException e) { + throw new CommandException(MESSAGE_FAILURE); + } + + return new CommandResult(MESSAGE_SUCCESS); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof ArchiveCommand)) { + return false; + } + + ArchiveCommand otherArchiveCommand = (ArchiveCommand) other; + return filename.equals(otherArchiveCommand.filename); + } +} diff --git a/src/main/java/seedu/address/logic/commands/DeleteArchiveCommand.java b/src/main/java/seedu/address/logic/commands/DeleteArchiveCommand.java new file mode 100644 index 00000000000..963777d9320 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/DeleteArchiveCommand.java @@ -0,0 +1,73 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.logging.Logger; + +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.core.filename.Filename; +import seedu.address.commons.util.FileUtil; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; + +/** + * Deletes an archive file. + */ +public class DeleteArchiveCommand extends Command { + public static final String COMMAND_WORD = "deletearchive"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Deletes an archive file.\n\n" + + "Parameters: FILENAME\n\n" + + "Example: " + COMMAND_WORD + " addressbook-2024-11-06T20-29-05.7609475-example.json"; + + public static final String MESSAGE_SUCCESS = "Deleted archive file: %1$s"; + public static final String MESSAGE_NOT_FOUND = "Archive file not found: %1$s"; + public static final String MESSAGE_FAILURE = "Failed to delete archive file: %1$s"; + + private static final Logger logger = LogsCenter.getLogger(DeleteArchiveCommand.class); + + private final Filename archiveFilename; + + public DeleteArchiveCommand(Filename archiveFilename) { + this.archiveFilename = archiveFilename; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + Path archiveFile = Paths.get(model.getArchiveDirectoryPath().toString(), archiveFilename.toString()); + if (!FileUtil.isFileExists(archiveFile)) { + logger.info("Archive file not found: " + archiveFilename); + throw new CommandException(String.format(MESSAGE_NOT_FOUND, archiveFilename)); + } + + try { + Files.deleteIfExists(archiveFile); + } catch (IOException e) { + logger.severe("Failed to delete archive file: " + e.getMessage()); + throw new CommandException(String.format(MESSAGE_FAILURE, archiveFilename)); + } + + logger.info("Deleted archive file: " + archiveFilename); + return new CommandResult(String.format(MESSAGE_SUCCESS, archiveFilename)); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof DeleteArchiveCommand)) { + return false; + } + + DeleteArchiveCommand otherDeleteCommand = (DeleteArchiveCommand) other; + return archiveFilename.equals(otherDeleteCommand.archiveFilename); + } +} diff --git a/src/main/java/seedu/address/logic/commands/DeleteCommand.java b/src/main/java/seedu/address/logic/commands/DeleteCommand.java index 1135ac19b74..ba18447dfa8 100644 --- a/src/main/java/seedu/address/logic/commands/DeleteCommand.java +++ b/src/main/java/seedu/address/logic/commands/DeleteCommand.java @@ -1,15 +1,26 @@ package seedu.address.logic.commands; import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EMERGENCY_CONTACT_TO_EDIT; import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; import seedu.address.commons.core.index.Index; import seedu.address.commons.util.ToStringBuilder; import seedu.address.logic.Messages; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.model.Model; +import seedu.address.model.person.Address; +import seedu.address.model.person.Doctor; +import seedu.address.model.person.Email; +import seedu.address.model.person.EmergencyContact; +import seedu.address.model.person.Name; import seedu.address.model.person.Person; +import seedu.address.model.person.Phone; +import seedu.address.model.tag.Tag; /** * Deletes a person identified using it's displayed index from the address book. @@ -19,16 +30,70 @@ public class DeleteCommand extends Command { public static final String COMMAND_WORD = "delete"; public static final String MESSAGE_USAGE = COMMAND_WORD - + ": Deletes the person identified by the index number used in the displayed person list.\n" - + "Parameters: INDEX (must be a positive integer)\n" - + "Example: " + COMMAND_WORD + " 1"; + + ": Deletes the person by the index number used in the displayed person list or emergency contact " + + "identified by the index number used in the displayed emergency contact list.\n\n" + + "Parameters: INDEX (must be a positive integer) [" + PREFIX_EMERGENCY_CONTACT_TO_EDIT + + "EMERGENCY CONTACT INDEX (must be a positive integer)]\n\n" + + "Example: " + COMMAND_WORD + " 1 " + PREFIX_EMERGENCY_CONTACT_TO_EDIT + "1"; public static final String MESSAGE_DELETE_PERSON_SUCCESS = "Deleted Person: %1$s"; + public static final String MESSAGE_DELETE_EMERGENCY_CONTACT_SUCCESS = "Deleted Emergency Contact: %1$s"; + private final Index targetIndex; + private final DeleteCommandDescriptor deleteCommandDescriptor; - public DeleteCommand(Index targetIndex) { + /** + * @param targetIndex of the person in the filtered person list to delete + * @param deleteCommandDescriptor carries emergency contact index, if present + */ + public DeleteCommand(Index targetIndex, DeleteCommandDescriptor deleteCommandDescriptor) { this.targetIndex = targetIndex; + this.deleteCommandDescriptor = deleteCommandDescriptor; + } + + /** + * Helper function which returns a new person with the updatedEmergencyContacts to abide by + * immutability of the Person class + * @param personToEdit person in the filtered person list to delete + * @param updatedEmergencyContacts updated emergency contacts list + */ + public static Person createEditedPerson(Person personToEdit, Set updatedEmergencyContacts) { + assert personToEdit != null; + + Name name = personToEdit.getName(); + Phone phone = personToEdit.getPhone(); + Email email = personToEdit.getEmail(); + Address address = personToEdit.getAddress(); + Doctor doctor = personToEdit.getDoctor(); + Set tagSet = personToEdit.getTags(); + + return new Person(name, phone, email, address, updatedEmergencyContacts, doctor, tagSet); + } + + private CommandResult executeDeleteEmergencyContact(Index emergencyContactIndex, + Person personToDelete, + Model model) throws CommandException { + if (emergencyContactIndex.getOneBased() > personToDelete.getEmergencyContacts().size()) { + throw new CommandException(Messages.MESSAGE_INVALID_EMERGENCY_CONTACT_DISPLAYED_INDEX); + } + + if (personToDelete.hasOnlyOneEmergencyContact()) { + throw new CommandException(Messages.MESSAGE_LAST_EMERGENCY_CONTACT_INDEX); + } + + EmergencyContact deletedEmergencyContact = + personToDelete.getEmergencyContact(emergencyContactIndex); + + Set updatedEmergencyContacts = + personToDelete.removeEmergencyContact(deletedEmergencyContact); + + Person updatedPerson = createEditedPerson(personToDelete, updatedEmergencyContacts); + + // Refreshes model + model.setPerson(personToDelete, updatedPerson); + return new CommandResult(String.format(MESSAGE_DELETE_EMERGENCY_CONTACT_SUCCESS, + Messages.formatEmergencyContact(deletedEmergencyContact))); } @Override @@ -41,6 +106,12 @@ public CommandResult execute(Model model) throws CommandException { } Person personToDelete = lastShownList.get(targetIndex.getZeroBased()); + + Optional emergencyContactIndex = deleteCommandDescriptor.getEmergencyContactIndex(); + if (emergencyContactIndex.isPresent()) { + return executeDeleteEmergencyContact(emergencyContactIndex.get(), personToDelete, model); + } + model.deletePerson(personToDelete); return new CommandResult(String.format(MESSAGE_DELETE_PERSON_SUCCESS, Messages.format(personToDelete))); } @@ -66,4 +137,53 @@ public String toString() { .add("targetIndex", targetIndex) .toString(); } + + /** + * Stores the details to edit the person with. Each non-empty field value will + * replace the + * corresponding field value of the person. + */ + public static class DeleteCommandDescriptor { + private Index emergencyContactIndex; + + public DeleteCommandDescriptor() { + } + + /** + * Copy constructor. + */ + public DeleteCommandDescriptor(DeleteCommandDescriptor toCopy) { + setEmergencyContactIndex(toCopy.emergencyContactIndex); + } + + public Optional getEmergencyContactIndex() { + return Optional.ofNullable(emergencyContactIndex); + } + + public void setEmergencyContactIndex(Index emergencyContactIndex) { + this.emergencyContactIndex = emergencyContactIndex; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof DeleteCommandDescriptor)) { + return false; + } + + DeleteCommandDescriptor otherDeleteCommandDescriptor = (DeleteCommandDescriptor) other; + return Objects.equals(emergencyContactIndex, otherDeleteCommandDescriptor.emergencyContactIndex); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("emergency contact index", emergencyContactIndex) + .toString(); + } + } } diff --git a/src/main/java/seedu/address/logic/commands/EditCommand.java b/src/main/java/seedu/address/logic/commands/EditCommand.java index 4b581c7331e..8bc877a6bfb 100644 --- a/src/main/java/seedu/address/logic/commands/EditCommand.java +++ b/src/main/java/seedu/address/logic/commands/EditCommand.java @@ -1,8 +1,16 @@ package seedu.address.logic.commands; import static java.util.Objects.requireNonNull; +import static seedu.address.logic.commands.AddEmergencyContactCommand.MESSAGE_DUPLICATE_EMERGENCY_CONTACT; import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DOC_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DOC_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DOC_PHONE; import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EMERGENCY_CONTACT_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EMERGENCY_CONTACT_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EMERGENCY_CONTACT_RELATIONSHIP; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EMERGENCY_CONTACT_TO_EDIT; import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; @@ -10,6 +18,7 @@ import java.util.Collections; import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -22,10 +31,14 @@ import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.model.Model; import seedu.address.model.person.Address; +import seedu.address.model.person.Doctor; +import seedu.address.model.person.DoctorName; import seedu.address.model.person.Email; +import seedu.address.model.person.EmergencyContact; import seedu.address.model.person.Name; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; +import seedu.address.model.person.Relationship; import seedu.address.model.tag.Tag; /** @@ -37,36 +50,135 @@ public class EditCommand extends Command { public static final String MESSAGE_USAGE = COMMAND_WORD + ": Edits the details of the person identified " + "by the index number used in the displayed person list. " - + "Existing values will be overwritten by the input values.\n" + + "Existing values will be overwritten by the input values.\n\n" + "Parameters: INDEX (must be a positive integer) " + "[" + PREFIX_NAME + "NAME] " + "[" + PREFIX_PHONE + "PHONE] " + "[" + PREFIX_EMAIL + "EMAIL] " + "[" + PREFIX_ADDRESS + "ADDRESS] " - + "[" + PREFIX_TAG + "TAG]...\n" + + "[" + PREFIX_EMERGENCY_CONTACT_TO_EDIT + "EMERGENCY_CONTACT_INDEX] " + + "[" + PREFIX_EMERGENCY_CONTACT_NAME + "EMERGENCY CONTACT NAME] " + + "[" + PREFIX_EMERGENCY_CONTACT_PHONE + "EMERGENCY CONTACT PHONE] " + + "[" + PREFIX_EMERGENCY_CONTACT_RELATIONSHIP + "EMERGENCY CONTACT RELATIONSHIP] " + + "[" + PREFIX_DOC_NAME + "DOCTOR NAME] " + + "[" + PREFIX_DOC_PHONE + "DOCTOR PHONE] " + + "[" + PREFIX_DOC_EMAIL + "DOCTOR EMAIL] " + + "[" + PREFIX_TAG + "TAG]...\n\n" + "Example: " + COMMAND_WORD + " 1 " + PREFIX_PHONE + "91234567 " - + PREFIX_EMAIL + "johndoe@example.com"; + + PREFIX_EMAIL + "johndoe@example.com " + + PREFIX_EMERGENCY_CONTACT_TO_EDIT + "1 " + + PREFIX_EMERGENCY_CONTACT_NAME + "John Kentucky"; public static final String MESSAGE_EDIT_PERSON_SUCCESS = "Edited Person: %1$s"; public static final String MESSAGE_NOT_EDITED = "At least one field to edit must be provided."; - public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book."; + public static final String MESSAGE_DUPLICATE_PERSON = + "A person with the same phone number already exists in the address book."; + public static final String MESSAGE_EMERGENCY_CONTACT_NOT_EDITED = "At least one emergency contact field to edit " + + "must be provided."; + public static final String MESSAGE_EMERGENCY_CONTACT_FIELDS_INVALID = "At least one emergency contact index to " + + "edit must be provided."; private final Index index; private final EditPersonDescriptor editPersonDescriptor; /** - * @param index of the person in the filtered person list to edit + * @param index of the person in the filtered person list to edit * @param editPersonDescriptor details to edit the person with */ public EditCommand(Index index, EditPersonDescriptor editPersonDescriptor) { requireNonNull(index); requireNonNull(editPersonDescriptor); - this.index = index; this.editPersonDescriptor = new EditPersonDescriptor(editPersonDescriptor); } + /** + * Creates and returns a {@code Person} with the details of {@code personToEdit} + * edited with {@code editPersonDescriptor}. + */ + private static Person createEditedPerson(Person personToEdit, EditPersonDescriptor editPersonDescriptor) + throws CommandException { + assert personToEdit != null; + + Name updatedName = editPersonDescriptor.getName().orElse(personToEdit.getName()); + Phone updatedPhone = editPersonDescriptor.getPhone().orElse(personToEdit.getPhone()); + Email updatedEmail = editPersonDescriptor.getEmail().orElse(personToEdit.getEmail()); + Address updatedAddress = editPersonDescriptor.getAddress().orElse(personToEdit.getAddress()); + Set updatedEmergencyContacts = new LinkedHashSet<>(); + + if (editPersonDescriptor.getIndexOfEmergencyContactToEdit().isPresent()) { + Index index = editPersonDescriptor.getIndexOfEmergencyContactToEdit().get(); + Set personEmergencyContacts = personToEdit.getEmergencyContacts(); + if (index.getZeroBased() >= personEmergencyContacts.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_EMERGENCY_CONTACT_DISPLAYED_INDEX); + } + + EmergencyContact emergencyContactToUpdate = personToEdit.getEmergencyContact(index); + EmergencyContact updatedEmergencyContact = + createEditedEmergencyContact(emergencyContactToUpdate, editPersonDescriptor); + + if (personToEdit.hasEmergencyContact(updatedEmergencyContact)) { + throw new CommandException(MESSAGE_DUPLICATE_EMERGENCY_CONTACT); + } + updatedEmergencyContacts = + updateEmergencyContacts(personEmergencyContacts, updatedEmergencyContact, index); + } else { + updatedEmergencyContacts = personToEdit.getEmergencyContacts(); + } + + Doctor updatedDoctor = createEditedDoctor(personToEdit.getDoctor(), editPersonDescriptor); + Set updatedTags = editPersonDescriptor.getTags().orElse(personToEdit.getTags()); + + return new Person(updatedName, updatedPhone, updatedEmail, updatedAddress, + updatedEmergencyContacts, updatedDoctor, updatedTags); + } + + private static Set updateEmergencyContacts(Set personEmergencyContacts, + EmergencyContact updatedEmergencyContact, + Index index) { + assert !personEmergencyContacts.isEmpty(); + + Set updatedEmergencyContacts = new LinkedHashSet<>(); + int i = index.getZeroBased(); + + for (EmergencyContact emergencyContact : personEmergencyContacts) { + if (i == 0) { + updatedEmergencyContacts.add(updatedEmergencyContact); + } else { + updatedEmergencyContacts.add(emergencyContact); + } + i = i - 1; + } + return updatedEmergencyContacts; + } + + private static EmergencyContact createEditedEmergencyContact(EmergencyContact emergencyContactToEdit, + EditPersonDescriptor editPersonDescriptor) { + assert emergencyContactToEdit != null; + + Name updatedName = editPersonDescriptor.getEmergencyContactName().orElse(emergencyContactToEdit.getName()); + Phone updatedPhone = editPersonDescriptor.getEmergencyContactPhone() + .orElse(emergencyContactToEdit.getPhone()); + Relationship updatedRelationship = editPersonDescriptor.getEmergencyContactRelationship() + .orElse(emergencyContactToEdit.getRelationship()); + + return new EmergencyContact(updatedName, updatedPhone, updatedRelationship); + } + + private static Doctor createEditedDoctor(Doctor doctorToEdit, + EditPersonDescriptor editPersonDescriptor) { + assert doctorToEdit != null; + + DoctorName updatedName = editPersonDescriptor.getDoctorName().orElse(doctorToEdit.getName()); + Phone updatedPhone = editPersonDescriptor.getDoctorPhone() + .orElse(doctorToEdit.getPhone()); + Email updatedEmail = editPersonDescriptor.getDoctorEmail() + .orElse(doctorToEdit.getEmail()); + + return new Doctor(updatedName, updatedPhone, updatedEmail); + } + @Override public CommandResult execute(Model model) throws CommandException { requireNonNull(model); @@ -88,22 +200,6 @@ public CommandResult execute(Model model) throws CommandException { return new CommandResult(String.format(MESSAGE_EDIT_PERSON_SUCCESS, Messages.format(editedPerson))); } - /** - * Creates and returns a {@code Person} with the details of {@code personToEdit} - * edited with {@code editPersonDescriptor}. - */ - private static Person createEditedPerson(Person personToEdit, EditPersonDescriptor editPersonDescriptor) { - assert personToEdit != null; - - Name updatedName = editPersonDescriptor.getName().orElse(personToEdit.getName()); - Phone updatedPhone = editPersonDescriptor.getPhone().orElse(personToEdit.getPhone()); - Email updatedEmail = editPersonDescriptor.getEmail().orElse(personToEdit.getEmail()); - Address updatedAddress = editPersonDescriptor.getAddress().orElse(personToEdit.getAddress()); - Set updatedTags = editPersonDescriptor.getTags().orElse(personToEdit.getTags()); - - return new Person(updatedName, updatedPhone, updatedEmail, updatedAddress, updatedTags); - } - @Override public boolean equals(Object other) { if (other == this) { @@ -129,7 +225,8 @@ public String toString() { } /** - * Stores the details to edit the person with. Each non-empty field value will replace the + * Stores the details to edit the person with. Each non-empty field value will + * replace the * corresponding field value of the person. */ public static class EditPersonDescriptor { @@ -137,9 +234,17 @@ public static class EditPersonDescriptor { private Phone phone; private Email email; private Address address; + private Index indexOfEmergencyContactToEdit; + private Name emergencyContactName; + private Phone emergencyContactPhone; + private Relationship emergencyContactRelationship; + private DoctorName doctorName; + private Phone doctorPhone; + private Email doctorEmail; private Set tags; - public EditPersonDescriptor() {} + public EditPersonDescriptor() { + } /** * Copy constructor. @@ -150,6 +255,13 @@ public EditPersonDescriptor(EditPersonDescriptor toCopy) { setPhone(toCopy.phone); setEmail(toCopy.email); setAddress(toCopy.address); + setIndexOfEmergencyContactToEdit(toCopy.indexOfEmergencyContactToEdit); + setEmergencyContactName(toCopy.emergencyContactName); + setEmergencyContactPhone(toCopy.emergencyContactPhone); + setEmergencyContactRelationship(toCopy.emergencyContactRelationship); + setDoctorName(toCopy.doctorName); + setDoctorPhone(toCopy.doctorPhone); + setDoctorEmail(toCopy.doctorEmail); setTags(toCopy.tags); } @@ -157,51 +269,102 @@ public EditPersonDescriptor(EditPersonDescriptor toCopy) { * Returns true if at least one field is edited. */ public boolean isAnyFieldEdited() { - return CollectionUtil.isAnyNonNull(name, phone, email, address, tags); + return CollectionUtil.isAnyNonNull(name, phone, email, address, indexOfEmergencyContactToEdit, + emergencyContactName, emergencyContactPhone, emergencyContactRelationship, + doctorName, doctorPhone, doctorEmail, tags); + } + + public Optional getName() { + return Optional.ofNullable(name); } public void setName(Name name) { this.name = name; } - public Optional getName() { - return Optional.ofNullable(name); + public Optional getPhone() { + return Optional.ofNullable(phone); } public void setPhone(Phone phone) { this.phone = phone; } - public Optional getPhone() { - return Optional.ofNullable(phone); + public Optional getEmail() { + return Optional.ofNullable(email); } public void setEmail(Email email) { this.email = email; } - public Optional getEmail() { - return Optional.ofNullable(email); + public Optional
getAddress() { + return Optional.ofNullable(address); } public void setAddress(Address address) { this.address = address; } - public Optional
getAddress() { - return Optional.ofNullable(address); + public Optional getEmergencyContactName() { + return Optional.ofNullable(emergencyContactName); } - /** - * Sets {@code tags} to this object's {@code tags}. - * A defensive copy of {@code tags} is used internally. - */ - public void setTags(Set tags) { - this.tags = (tags != null) ? new HashSet<>(tags) : null; + public void setEmergencyContactName(Name emergencyContactName) { + this.emergencyContactName = emergencyContactName; + } + + public Optional getEmergencyContactPhone() { + return Optional.ofNullable(emergencyContactPhone); + } + + public void setEmergencyContactPhone(Phone emergencyContactPhone) { + this.emergencyContactPhone = emergencyContactPhone; + } + + public Optional getEmergencyContactRelationship() { + return Optional.ofNullable(emergencyContactRelationship); + } + + public void setEmergencyContactRelationship(Relationship emergencyContactRelationship) { + this.emergencyContactRelationship = emergencyContactRelationship; + } + + public Optional getIndexOfEmergencyContactToEdit() { + return Optional.ofNullable(indexOfEmergencyContactToEdit); + } + + public void setIndexOfEmergencyContactToEdit(Index indexOfEmergencyContactToEdit) { + this.indexOfEmergencyContactToEdit = indexOfEmergencyContactToEdit; + } + + public Optional getDoctorName() { + return Optional.ofNullable(doctorName); + } + + public void setDoctorName(DoctorName doctorName) { + this.doctorName = doctorName; + } + + public Optional getDoctorPhone() { + return Optional.ofNullable(doctorPhone); + } + + public void setDoctorPhone(Phone doctorPhone) { + this.doctorPhone = doctorPhone; + } + + public Optional getDoctorEmail() { + return Optional.ofNullable(doctorEmail); + } + + public void setDoctorEmail(Email doctorEmail) { + this.doctorEmail = doctorEmail; } /** - * Returns an unmodifiable tag set, which throws {@code UnsupportedOperationException} + * Returns an unmodifiable tag set, which throws + * {@code UnsupportedOperationException} * if modification is attempted. * Returns {@code Optional#empty()} if {@code tags} is null. */ @@ -209,6 +372,14 @@ public Optional> getTags() { return (tags != null) ? Optional.of(Collections.unmodifiableSet(tags)) : Optional.empty(); } + /** + * Sets {@code tags} to this object's {@code tags}. + * A defensive copy of {@code tags} is used internally. + */ + public void setTags(Set tags) { + this.tags = (tags != null) ? new HashSet<>(tags) : null; + } + @Override public boolean equals(Object other) { if (other == this) { @@ -225,6 +396,15 @@ public boolean equals(Object other) { && Objects.equals(phone, otherEditPersonDescriptor.phone) && Objects.equals(email, otherEditPersonDescriptor.email) && Objects.equals(address, otherEditPersonDescriptor.address) + && Objects.equals(indexOfEmergencyContactToEdit, + otherEditPersonDescriptor.indexOfEmergencyContactToEdit) + && Objects.equals(emergencyContactName, otherEditPersonDescriptor.emergencyContactName) + && Objects.equals(emergencyContactPhone, otherEditPersonDescriptor.emergencyContactPhone) + && Objects.equals(emergencyContactRelationship, + otherEditPersonDescriptor.emergencyContactRelationship) + && Objects.equals(doctorName, otherEditPersonDescriptor.doctorName) + && Objects.equals(doctorPhone, otherEditPersonDescriptor.doctorPhone) + && Objects.equals(doctorEmail, otherEditPersonDescriptor.doctorEmail) && Objects.equals(tags, otherEditPersonDescriptor.tags); } @@ -235,6 +415,13 @@ public String toString() { .add("phone", phone) .add("email", email) .add("address", address) + .add("index of emergency contact to edit", indexOfEmergencyContactToEdit) + .add("emergency contact name", emergencyContactName) + .add("emergency contact phone", emergencyContactPhone) + .add("emergency contact relationship", emergencyContactRelationship) + .add("doctor name", doctorName) + .add("doctor phone", doctorPhone) + .add("doctor email", doctorEmail) .add("tags", tags) .toString(); } diff --git a/src/main/java/seedu/address/logic/commands/FindCommand.java b/src/main/java/seedu/address/logic/commands/FindCommand.java index 72b9eddd3a7..d16b235b1f2 100644 --- a/src/main/java/seedu/address/logic/commands/FindCommand.java +++ b/src/main/java/seedu/address/logic/commands/FindCommand.java @@ -16,8 +16,8 @@ public class FindCommand extends Command { public static final String COMMAND_WORD = "find"; public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all persons whose names contain any of " - + "the specified keywords (case-insensitive) and displays them as a list with index numbers.\n" - + "Parameters: KEYWORD [MORE_KEYWORDS]...\n" + + "the specified keywords (case-insensitive) and displays them as a list with index numbers.\n\n" + + "Parameters: KEYWORD [MORE_KEYWORDS]...\n\n" + "Example: " + COMMAND_WORD + " alice bob charlie"; private final NameContainsKeywordsPredicate predicate; diff --git a/src/main/java/seedu/address/logic/commands/FindDoctorCommand.java b/src/main/java/seedu/address/logic/commands/FindDoctorCommand.java new file mode 100644 index 00000000000..9cfc2aa1d9b --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/FindDoctorCommand.java @@ -0,0 +1,59 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.Messages; +import seedu.address.model.Model; +import seedu.address.model.person.DoctorNameContainsKeywordsPredicate; + +/** + * Finds and lists all persons with doctors whose names contains any of the argument keywords. + * Keyword matching is case insensitive. + */ +public class FindDoctorCommand extends Command { + + public static final String COMMAND_WORD = "finddoc"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all persons with assigned doctors whose names " + + "contain any of the specified keywords (case-insensitive) and displays them as a list with index " + + "numbers.\n\n" + + "Parameters: KEYWORD [MORE_KEYWORDS]...\n\n" + + "Example: " + COMMAND_WORD + " alice bob charlie"; + + private final DoctorNameContainsKeywordsPredicate predicate; + + public FindDoctorCommand(DoctorNameContainsKeywordsPredicate predicate) { + this.predicate = predicate; + } + + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + model.updateFilteredPersonList(predicate); + return new CommandResult( + String.format(Messages.MESSAGE_PERSONS_LISTED_OVERVIEW, model.getFilteredPersonList().size())); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof FindDoctorCommand)) { + return false; + } + + FindDoctorCommand otherFindDocCommand = (FindDoctorCommand) other; + return predicate.equals(otherFindDocCommand.predicate); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("predicate", predicate) + .toString(); + } +} diff --git a/src/main/java/seedu/address/logic/commands/HelpCommand.java b/src/main/java/seedu/address/logic/commands/HelpCommand.java index bf824f91bd0..881e8a9f7a8 100644 --- a/src/main/java/seedu/address/logic/commands/HelpCommand.java +++ b/src/main/java/seedu/address/logic/commands/HelpCommand.java @@ -9,7 +9,7 @@ public class HelpCommand extends Command { public static final String COMMAND_WORD = "help"; - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Shows program usage instructions.\n" + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Shows program usage instructions.\n\n" + "Example: " + COMMAND_WORD; public static final String SHOWING_HELP_MESSAGE = "Opened help window."; diff --git a/src/main/java/seedu/address/logic/commands/ListArchiveFilesCommand.java b/src/main/java/seedu/address/logic/commands/ListArchiveFilesCommand.java new file mode 100644 index 00000000000..7aa1b27edef --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ListArchiveFilesCommand.java @@ -0,0 +1,59 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.io.IOException; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Logger; + +import seedu.address.commons.core.LogsCenter; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; + +/** + * Lists all archive files in the archive folder. + */ +public class ListArchiveFilesCommand extends Command { + public static final String COMMAND_WORD = "listarchives"; + + public static final String MESSAGE_SUCCESS = "Listed all archive files."; + public static final String MESSAGE_NO_ARCHIVE = "No archive files found."; + public static final String MESSAGE_FAILURE = "Failed to find archive files. Please try again later."; + + private static final Logger logger = LogsCenter.getLogger(ListArchiveFilesCommand.class); + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + Path archiveDir = model.getArchiveDirectoryPath(); + if (!Files.exists(archiveDir)) { + logger.info("No archive directory found."); + throw new CommandException(MESSAGE_NO_ARCHIVE); + } + + List archiveFiles = new ArrayList<>(); + + try (DirectoryStream stream = Files.newDirectoryStream(archiveDir, "*.json")) { + for (Path entry : stream) { + archiveFiles.add(entry.getFileName().toString()); + } + } catch (IOException e) { + logger.severe("Failed to list archive files: " + e.getMessage()); + throw new CommandException(MESSAGE_FAILURE); + } + + if (archiveFiles.isEmpty()) { + throw new CommandException(MESSAGE_NO_ARCHIVE); + } + + String resultMessage = String.join("\n", archiveFiles); + + logger.info("Listed all archive files."); + return new CommandResult(MESSAGE_SUCCESS + "\n" + resultMessage); + } +} diff --git a/src/main/java/seedu/address/logic/commands/ListCommand.java b/src/main/java/seedu/address/logic/commands/ListCommand.java index 84be6ad2596..8c900f2728c 100644 --- a/src/main/java/seedu/address/logic/commands/ListCommand.java +++ b/src/main/java/seedu/address/logic/commands/ListCommand.java @@ -1,24 +1,52 @@ package seedu.address.logic.commands; import static java.util.Objects.requireNonNull; -import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; + +import java.util.Comparator; import seedu.address.model.Model; +import seedu.address.model.person.Person; +import seedu.address.model.person.PersonComparators; /** - * Lists all persons in the address book to the user. + * Lists all persons in the address book to the user in a user-specified sorted order. */ public class ListCommand extends Command { public static final String COMMAND_WORD = "list"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Lists all persons in the address book.\n" + + "Parameters: [SORT_ORDER]\n" + + "Sort order can be one of the following:\n" + + "timeAdded asc, timeAdded desc, name asc, name desc\n" + + "Note: If no sort order is specified, the default is by time added (timeAdded) and ascending (asc).\n" + + "Example: " + COMMAND_WORD + " name asc"; + public static final String MESSAGE_SUCCESS = "Listed all persons"; + private final Comparator comparator; + /** + * Creates a ListCommand with the default comparator. + */ + public ListCommand() { + // Default comparator is BY_ORDER_ADDED_REVERSED + this.comparator = PersonComparators.BY_ORDER_ADDED_REVERSED; + } + + /** + * Creates a ListCommand with the specified comparator. + * + * @param comparator Comparator to sort the list of persons. + */ + public ListCommand(Comparator comparator) { + this.comparator = comparator; + } @Override public CommandResult execute(Model model) { requireNonNull(model); - model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + model.updateFilteredPersonList(Model.PREDICATE_SHOW_ALL_PERSONS); + model.sortFilteredPersonList(comparator); return new CommandResult(MESSAGE_SUCCESS); } } diff --git a/src/main/java/seedu/address/logic/commands/LoadArchiveCommand.java b/src/main/java/seedu/address/logic/commands/LoadArchiveCommand.java new file mode 100644 index 00000000000..4c055919af9 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/LoadArchiveCommand.java @@ -0,0 +1,77 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.logging.Logger; + +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.core.filename.Filename; +import seedu.address.commons.exceptions.DataLoadingException; +import seedu.address.commons.util.FileUtil; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.storage.JsonAddressBookStorage; + +/** + * Loads an archive file and sets it as the current address book. + */ +public class LoadArchiveCommand extends Command { + public static final String COMMAND_WORD = "loadarchive"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Loads an archive file.\n\n" + + "Parameters: FILENAME\n\n" + + "Example: " + COMMAND_WORD + " addressbook-2024-11-06T20-29-05.7609475-example.json"; + + public static final String MESSAGE_SUCCESS = "Loaded archive file: %1$s"; + public static final String MESSAGE_NOT_FOUND = "Archive file not found: %1$s"; + public static final String MESSAGE_FAILURE = "Failed to load archive file: %1$s"; + + private static final Logger logger = LogsCenter.getLogger(LoadArchiveCommand.class); + + private final Filename archiveFilename; + + public LoadArchiveCommand(Filename archiveFilename) { + this.archiveFilename = archiveFilename; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + Path archiveFile = Paths.get(model.getArchiveDirectoryPath().toString(), archiveFilename.toString()); + if (!FileUtil.isFileExists(archiveFile)) { + logger.info("Archive file not found: " + archiveFilename); + throw new CommandException(String.format(MESSAGE_NOT_FOUND, archiveFilename)); + } + + try { + JsonAddressBookStorage storage = new JsonAddressBookStorage(archiveFile); + ReadOnlyAddressBook addressBook = storage.readAddressBook().orElseThrow(IOException::new); + model.setAddressBook(addressBook); + } catch (IOException | DataLoadingException e) { + logger.severe("Failed to load archive file: " + e.getMessage()); + throw new CommandException(String.format(MESSAGE_FAILURE, archiveFilename)); + } + + logger.info("Loaded archive file: " + archiveFilename); + return new CommandResult(String.format(MESSAGE_SUCCESS, archiveFilename)); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof LoadArchiveCommand)) { + return false; + } + + LoadArchiveCommand otherLoadArchiveCommand = (LoadArchiveCommand) other; + return archiveFilename.equals(otherLoadArchiveCommand.archiveFilename); + } +} diff --git a/src/main/java/seedu/address/logic/commands/RedoCommand.java b/src/main/java/seedu/address/logic/commands/RedoCommand.java new file mode 100644 index 00000000000..2f142d02268 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/RedoCommand.java @@ -0,0 +1,30 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; + + +/** + * Represents an Redo command that reverses the reversed last modification made to the address book. + * This command can be used to restore the previous state of the address book after an edit, + * addition, or deletion. + */ +public class RedoCommand extends Command { + + public static final String COMMAND_WORD = "redo"; + public static final String MESSAGE_SUCCESS = "Address book has redone previous command!"; + public static final String MESSAGE_FAILURE = "There is no previous command to be redone!"; + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + if (model.canRedoAddressBook()) { + model.redoAddressBook(); + return new CommandResult(MESSAGE_SUCCESS); + } else { + throw new CommandException(MESSAGE_FAILURE); + } + } +} diff --git a/src/main/java/seedu/address/logic/commands/UndoCommand.java b/src/main/java/seedu/address/logic/commands/UndoCommand.java new file mode 100644 index 00000000000..276c32321bd --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/UndoCommand.java @@ -0,0 +1,30 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; + + +/** + * Represents an Undo command that reverses the last modification made to the address book. + * This command can be used to restore the previous state of the address book after an edit, + * addition, or deletion. + */ +public class UndoCommand extends Command { + + public static final String COMMAND_WORD = "undo"; + public static final String MESSAGE_SUCCESS = "Address book has undone previous command!"; + public static final String MESSAGE_FAILURE = "There is no current command to be undone!"; + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + if (model.canUndoAddressBook()) { + model.undoAddressBook(); + return new CommandResult(MESSAGE_SUCCESS); + } else { + throw new CommandException(MESSAGE_FAILURE); + } + } +} diff --git a/src/main/java/seedu/address/logic/parser/AddCommandParser.java b/src/main/java/seedu/address/logic/parser/AddCommandParser.java index 4ff1a97ed77..7c63116d9d3 100644 --- a/src/main/java/seedu/address/logic/parser/AddCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/AddCommandParser.java @@ -2,21 +2,32 @@ import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DOC_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DOC_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DOC_PHONE; import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EMERGENCY_CONTACT_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EMERGENCY_CONTACT_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EMERGENCY_CONTACT_RELATIONSHIP; import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import java.util.LinkedHashSet; import java.util.Set; import java.util.stream.Stream; import seedu.address.logic.commands.AddCommand; import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.person.Address; +import seedu.address.model.person.Doctor; +import seedu.address.model.person.DoctorName; import seedu.address.model.person.Email; +import seedu.address.model.person.EmergencyContact; import seedu.address.model.person.Name; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; +import seedu.address.model.person.Relationship; import seedu.address.model.tag.Tag; /** @@ -24,38 +35,62 @@ */ public class AddCommandParser implements Parser { + /** + * Returns true if none of the prefixes contains empty {@code Optional} values + * in the given + * {@code ArgumentMultimap}. + */ + public static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } + /** * Parses the given {@code String} of arguments in the context of the AddCommand * and returns an AddCommand object for execution. + * * @throws ParseException if the user input does not conform the expected format */ public AddCommand parse(String args) throws ParseException { - ArgumentMultimap argMultimap = - ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_TAG); + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, + PREFIX_ADDRESS, PREFIX_EMERGENCY_CONTACT_NAME, + PREFIX_EMERGENCY_CONTACT_PHONE, PREFIX_EMERGENCY_CONTACT_RELATIONSHIP, PREFIX_DOC_NAME, + PREFIX_DOC_PHONE, PREFIX_DOC_EMAIL, PREFIX_TAG); - if (!arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_ADDRESS, PREFIX_PHONE, PREFIX_EMAIL) + // Compulsory fields + if (!arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, + PREFIX_EMERGENCY_CONTACT_NAME, PREFIX_EMERGENCY_CONTACT_PHONE, PREFIX_EMERGENCY_CONTACT_RELATIONSHIP, + PREFIX_DOC_NAME, PREFIX_DOC_PHONE, PREFIX_DOC_EMAIL) || !argMultimap.getPreamble().isEmpty()) { throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE)); } - argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS); + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, + PREFIX_EMERGENCY_CONTACT_NAME, PREFIX_EMERGENCY_CONTACT_PHONE, PREFIX_EMERGENCY_CONTACT_RELATIONSHIP, + PREFIX_DOC_NAME, PREFIX_DOC_PHONE, PREFIX_DOC_EMAIL); + Name name = ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get()); Phone phone = ParserUtil.parsePhone(argMultimap.getValue(PREFIX_PHONE).get()); Email email = ParserUtil.parseEmail(argMultimap.getValue(PREFIX_EMAIL).get()); Address address = ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get()); + + Name ecName = ParserUtil.parseName(argMultimap.getValue(PREFIX_EMERGENCY_CONTACT_NAME).get()); + Phone ecPhone = ParserUtil.parsePhone(argMultimap.getValue(PREFIX_EMERGENCY_CONTACT_PHONE).get()); + Relationship ecRelationship = ParserUtil.parseRelationship( + argMultimap.getValue(PREFIX_EMERGENCY_CONTACT_RELATIONSHIP).get()); + EmergencyContact emergencyContact = new EmergencyContact(ecName, ecPhone, ecRelationship); + Set emergencyContacts = new LinkedHashSet<>(); + emergencyContacts.add(emergencyContact); + + DoctorName doctorName = ParserUtil.parseDoctorName(argMultimap.getValue(PREFIX_DOC_NAME).get()); + Phone doctorPhone = ParserUtil.parsePhone(argMultimap.getValue(PREFIX_DOC_PHONE).get()); + Email doctorEmail = ParserUtil.parseEmail(argMultimap.getValue(PREFIX_DOC_EMAIL).get()); + Doctor doctor = new Doctor(doctorName, doctorPhone, doctorEmail); + Set tagList = ParserUtil.parseTags(argMultimap.getAllValues(PREFIX_TAG)); - Person person = new Person(name, phone, email, address, tagList); + Person person = new Person(name, phone, email, address, emergencyContacts, doctor, tagList); return new AddCommand(person); } - /** - * Returns true if none of the prefixes contains empty {@code Optional} values in the given - * {@code ArgumentMultimap}. - */ - private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { - return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); - } - } diff --git a/src/main/java/seedu/address/logic/parser/AddEmergencyContactCommandParser.java b/src/main/java/seedu/address/logic/parser/AddEmergencyContactCommandParser.java new file mode 100644 index 00000000000..eada0d8d7ea --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/AddEmergencyContactCommandParser.java @@ -0,0 +1,59 @@ +package seedu.address.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EMERGENCY_CONTACT_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EMERGENCY_CONTACT_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EMERGENCY_CONTACT_RELATIONSHIP; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.AddEmergencyContactCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.EmergencyContact; +import seedu.address.model.person.Name; +import seedu.address.model.person.Phone; +import seedu.address.model.person.Relationship; + +/** + * Parses input arguments and creates a new EditCommand object + */ +public class AddEmergencyContactCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the AddEmergencyContactCommand + * and returns an AddEmergencyContactCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public AddEmergencyContactCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_EMERGENCY_CONTACT_NAME, PREFIX_EMERGENCY_CONTACT_PHONE, + PREFIX_EMERGENCY_CONTACT_RELATIONSHIP); + + if (!AddCommandParser.arePrefixesPresent(argMultimap, PREFIX_EMERGENCY_CONTACT_NAME, + PREFIX_EMERGENCY_CONTACT_PHONE, PREFIX_EMERGENCY_CONTACT_RELATIONSHIP)) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + AddEmergencyContactCommand.MESSAGE_USAGE)); + } + + Index index; + + try { + index = ParserUtil.parseIndex(argMultimap.getPreamble()); + } catch (ParseException pe) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + AddEmergencyContactCommand.MESSAGE_USAGE), pe); + } + + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_EMERGENCY_CONTACT_NAME, PREFIX_EMERGENCY_CONTACT_PHONE, + PREFIX_EMERGENCY_CONTACT_RELATIONSHIP); + + Name name = ParserUtil.parseName(argMultimap.getValue(PREFIX_EMERGENCY_CONTACT_NAME).get()); + Phone phone = ParserUtil.parsePhone(argMultimap.getValue(PREFIX_EMERGENCY_CONTACT_PHONE).get()); + Relationship relationship = ParserUtil.parseRelationship( + argMultimap.getValue(PREFIX_EMERGENCY_CONTACT_RELATIONSHIP).get()); + EmergencyContact emergencyContact = new EmergencyContact(name, phone, relationship); + return new AddEmergencyContactCommand(index, emergencyContact); + } + +} diff --git a/src/main/java/seedu/address/logic/parser/AddressBookParser.java b/src/main/java/seedu/address/logic/parser/AddressBookParser.java index 3149ee07e0b..ca7eaec37ea 100644 --- a/src/main/java/seedu/address/logic/parser/AddressBookParser.java +++ b/src/main/java/seedu/address/logic/parser/AddressBookParser.java @@ -9,14 +9,22 @@ import seedu.address.commons.core.LogsCenter; import seedu.address.logic.commands.AddCommand; +import seedu.address.logic.commands.AddEmergencyContactCommand; +import seedu.address.logic.commands.ArchiveCommand; import seedu.address.logic.commands.ClearCommand; import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.DeleteArchiveCommand; import seedu.address.logic.commands.DeleteCommand; import seedu.address.logic.commands.EditCommand; import seedu.address.logic.commands.ExitCommand; import seedu.address.logic.commands.FindCommand; +import seedu.address.logic.commands.FindDoctorCommand; import seedu.address.logic.commands.HelpCommand; +import seedu.address.logic.commands.ListArchiveFilesCommand; import seedu.address.logic.commands.ListCommand; +import seedu.address.logic.commands.LoadArchiveCommand; +import seedu.address.logic.commands.RedoCommand; +import seedu.address.logic.commands.UndoCommand; import seedu.address.logic.parser.exceptions.ParseException; /** @@ -69,7 +77,28 @@ public Command parseCommand(String userInput) throws ParseException { return new FindCommandParser().parse(arguments); case ListCommand.COMMAND_WORD: - return new ListCommand(); + return new ListCommandParser().parse(arguments); + + case ArchiveCommand.COMMAND_WORD: + return new ArchiveCommandParser().parse(arguments); + + case ListArchiveFilesCommand.COMMAND_WORD: + return new ListArchiveFilesCommand(); + + case LoadArchiveCommand.COMMAND_WORD: + return new LoadArchiveCommandParser().parse(arguments); + + case DeleteArchiveCommand.COMMAND_WORD: + return new DeleteArchiveCommandParser().parse(arguments); + + case UndoCommand.COMMAND_WORD: + return new UndoCommand(); + + case RedoCommand.COMMAND_WORD: + return new RedoCommand(); + + case AddEmergencyContactCommand.COMMAND_WORD: + return new AddEmergencyContactCommandParser().parse(arguments); case ExitCommand.COMMAND_WORD: return new ExitCommand(); @@ -77,6 +106,9 @@ public Command parseCommand(String userInput) throws ParseException { case HelpCommand.COMMAND_WORD: return new HelpCommand(); + case FindDoctorCommand.COMMAND_WORD: + return new FindDoctorCommandParser().parse(arguments); + default: logger.finer("This user input caused a ParseException: " + userInput); throw new ParseException(MESSAGE_UNKNOWN_COMMAND); diff --git a/src/main/java/seedu/address/logic/parser/ArchiveCommandParser.java b/src/main/java/seedu/address/logic/parser/ArchiveCommandParser.java new file mode 100644 index 00000000000..e7012d0a3fa --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/ArchiveCommandParser.java @@ -0,0 +1,26 @@ +package seedu.address.logic.parser; + +import seedu.address.commons.core.filename.Filename; +import seedu.address.logic.commands.ArchiveCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new ArchiveCommand object + */ +public class ArchiveCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the ArchiveCommand + * and returns a ArchiveCommand object for execution. + * + * @throws ParseException if the user input does not conform the expected format + */ + public ArchiveCommand parse(String args) throws ParseException { + if (args.isBlank()) { + return new ArchiveCommand(); + } + + Filename filename = ParserUtil.parseFilename(args); + return new ArchiveCommand(filename); + } +} diff --git a/src/main/java/seedu/address/logic/parser/CliSyntax.java b/src/main/java/seedu/address/logic/parser/CliSyntax.java index 75b1a9bf119..d020df75927 100644 --- a/src/main/java/seedu/address/logic/parser/CliSyntax.java +++ b/src/main/java/seedu/address/logic/parser/CliSyntax.java @@ -11,5 +11,11 @@ public class CliSyntax { public static final Prefix PREFIX_EMAIL = new Prefix("e/"); public static final Prefix PREFIX_ADDRESS = new Prefix("a/"); public static final Prefix PREFIX_TAG = new Prefix("t/"); - + public static final Prefix PREFIX_EMERGENCY_CONTACT_TO_EDIT = new Prefix("ec/"); + public static final Prefix PREFIX_EMERGENCY_CONTACT_NAME = new Prefix("ecname/"); + public static final Prefix PREFIX_EMERGENCY_CONTACT_PHONE = new Prefix("ecphone/"); + public static final Prefix PREFIX_EMERGENCY_CONTACT_RELATIONSHIP = new Prefix("ecrs/"); + public static final Prefix PREFIX_DOC_NAME = new Prefix("dname/"); + public static final Prefix PREFIX_DOC_PHONE = new Prefix("dphone/"); + public static final Prefix PREFIX_DOC_EMAIL = new Prefix("demail/"); } diff --git a/src/main/java/seedu/address/logic/parser/DeleteArchiveCommandParser.java b/src/main/java/seedu/address/logic/parser/DeleteArchiveCommandParser.java new file mode 100644 index 00000000000..1dd79c739cc --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/DeleteArchiveCommandParser.java @@ -0,0 +1,29 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.commons.core.filename.Filename; +import seedu.address.logic.commands.DeleteArchiveCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new DeleteArchiveCommand object + */ +public class DeleteArchiveCommandParser implements Parser { + /** + * Parses the given {@code String} of arguments in the context of the DeleteArchiveCommand + * and returns a DeleteArchiveCommand object for execution. + * + * @throws ParseException if the user input does not conform the expected format + */ + public DeleteArchiveCommand parse(String args) throws ParseException { + Filename filename; + try { + filename = ParserUtil.parseFilename(args); + } catch (ParseException pe) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + pe.getMessage() + "\n\n" + DeleteArchiveCommand.MESSAGE_USAGE), pe); + } + return new DeleteArchiveCommand(filename); + } +} diff --git a/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java b/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java index 3527fe76a3e..4eada2a40f0 100644 --- a/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java @@ -1,9 +1,12 @@ package seedu.address.logic.parser; +import static java.util.Objects.requireNonNull; import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EMERGENCY_CONTACT_TO_EDIT; import seedu.address.commons.core.index.Index; import seedu.address.logic.commands.DeleteCommand; +import seedu.address.logic.commands.DeleteCommand.DeleteCommandDescriptor; import seedu.address.logic.parser.exceptions.ParseException; /** @@ -17,13 +20,34 @@ public class DeleteCommandParser implements Parser { * @throws ParseException if the user input does not conform the expected format */ public DeleteCommand parse(String args) throws ParseException { + requireNonNull(args); + + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_EMERGENCY_CONTACT_TO_EDIT); + + Index personIndex; try { - Index index = ParserUtil.parseIndex(args); - return new DeleteCommand(index); + personIndex = ParserUtil.parseIndex(argMultimap.getPreamble()); } catch (ParseException pe) { throw new ParseException( String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE), pe); } + + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_EMERGENCY_CONTACT_TO_EDIT); + DeleteCommandDescriptor deleteCommandDescriptor = new DeleteCommandDescriptor(); + + if (argMultimap.getValue(PREFIX_EMERGENCY_CONTACT_TO_EDIT).isPresent()) { + try { + Index emergencyContactIndex = ParserUtil.parseIndex(argMultimap.getValue( + PREFIX_EMERGENCY_CONTACT_TO_EDIT).get()); + deleteCommandDescriptor.setEmergencyContactIndex(emergencyContactIndex); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE), pe); + } + } + + return new DeleteCommand(personIndex, deleteCommandDescriptor); } } diff --git a/src/main/java/seedu/address/logic/parser/EditCommandParser.java b/src/main/java/seedu/address/logic/parser/EditCommandParser.java index 46b3309a78b..b56d800fa2f 100644 --- a/src/main/java/seedu/address/logic/parser/EditCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/EditCommandParser.java @@ -3,7 +3,14 @@ import static java.util.Objects.requireNonNull; import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DOC_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DOC_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DOC_PHONE; import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EMERGENCY_CONTACT_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EMERGENCY_CONTACT_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EMERGENCY_CONTACT_RELATIONSHIP; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EMERGENCY_CONTACT_TO_EDIT; import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; @@ -32,7 +39,11 @@ public class EditCommandParser implements Parser { public EditCommand parse(String args) throws ParseException { requireNonNull(args); ArgumentMultimap argMultimap = - ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_TAG); + ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, + PREFIX_ADDRESS, PREFIX_EMERGENCY_CONTACT_TO_EDIT, + PREFIX_EMERGENCY_CONTACT_NAME, PREFIX_EMERGENCY_CONTACT_PHONE, + PREFIX_EMERGENCY_CONTACT_RELATIONSHIP, PREFIX_DOC_NAME, PREFIX_DOC_PHONE, PREFIX_DOC_EMAIL, + PREFIX_TAG); Index index; @@ -42,10 +53,63 @@ public EditCommand parse(String args) throws ParseException { throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditCommand.MESSAGE_USAGE), pe); } - argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS); + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, + PREFIX_EMERGENCY_CONTACT_TO_EDIT, + PREFIX_EMERGENCY_CONTACT_NAME, PREFIX_EMERGENCY_CONTACT_PHONE, PREFIX_EMERGENCY_CONTACT_RELATIONSHIP, + PREFIX_DOC_NAME, PREFIX_DOC_PHONE, PREFIX_DOC_EMAIL); + + verifyEmergencyContactFields(args, argMultimap); EditPersonDescriptor editPersonDescriptor = new EditPersonDescriptor(); + setEditPersonDescriptorPersonFields(editPersonDescriptor, argMultimap); + setEditPersonDescriptorEmergencyContactFields(editPersonDescriptor, argMultimap); + setEditPersonDescriptorDoctorFields(editPersonDescriptor, argMultimap); + + if (!editPersonDescriptor.isAnyFieldEdited()) { + throw new ParseException(EditCommand.MESSAGE_NOT_EDITED); + } + + return new EditCommand(index, editPersonDescriptor); + } + private void setEditPersonDescriptorDoctorFields(EditPersonDescriptor editPersonDescriptor, + ArgumentMultimap argMultimap) throws ParseException { + if (argMultimap.getValue(PREFIX_DOC_NAME).isPresent()) { + editPersonDescriptor.setDoctorName(ParserUtil.parseDoctorName( + argMultimap.getValue(PREFIX_DOC_NAME).get())); + } + if (argMultimap.getValue(PREFIX_DOC_PHONE).isPresent()) { + editPersonDescriptor.setDoctorPhone(ParserUtil.parsePhone( + argMultimap.getValue(PREFIX_DOC_PHONE).get())); + } + if (argMultimap.getValue(PREFIX_DOC_EMAIL).isPresent()) { + editPersonDescriptor.setDoctorEmail(ParserUtil.parseEmail( + argMultimap.getValue(PREFIX_DOC_EMAIL).get())); + } + } + + private void setEditPersonDescriptorEmergencyContactFields(EditPersonDescriptor editPersonDescriptor, + ArgumentMultimap argMultimap) throws ParseException { + if (argMultimap.getValue(PREFIX_EMERGENCY_CONTACT_TO_EDIT).isPresent()) { + editPersonDescriptor.setIndexOfEmergencyContactToEdit( + ParserUtil.parseIndex(argMultimap.getValue(PREFIX_EMERGENCY_CONTACT_TO_EDIT).get())); + } + if (argMultimap.getValue(PREFIX_EMERGENCY_CONTACT_NAME).isPresent()) { + editPersonDescriptor.setEmergencyContactName( + ParserUtil.parseName(argMultimap.getValue(PREFIX_EMERGENCY_CONTACT_NAME).get())); + } + if (argMultimap.getValue(PREFIX_EMERGENCY_CONTACT_PHONE).isPresent()) { + editPersonDescriptor.setEmergencyContactPhone(ParserUtil.parsePhone( + argMultimap.getValue(PREFIX_EMERGENCY_CONTACT_PHONE).get())); + } + if (argMultimap.getValue(PREFIX_EMERGENCY_CONTACT_RELATIONSHIP).isPresent()) { + editPersonDescriptor.setEmergencyContactRelationship(ParserUtil.parseRelationship( + argMultimap.getValue(PREFIX_EMERGENCY_CONTACT_RELATIONSHIP).get())); + } + } + + private void setEditPersonDescriptorPersonFields(EditPersonDescriptor editPersonDescriptor, + ArgumentMultimap argMultimap) throws ParseException { if (argMultimap.getValue(PREFIX_NAME).isPresent()) { editPersonDescriptor.setName(ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get())); } @@ -59,12 +123,35 @@ public EditCommand parse(String args) throws ParseException { editPersonDescriptor.setAddress(ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get())); } parseTagsForEdit(argMultimap.getAllValues(PREFIX_TAG)).ifPresent(editPersonDescriptor::setTags); + } - if (!editPersonDescriptor.isAnyFieldEdited()) { - throw new ParseException(EditCommand.MESSAGE_NOT_EDITED); + private void verifyEmergencyContactFields(String args, ArgumentMultimap argMultimap) throws ParseException { + if (!isEmergencyContactIndexProvided(args, argMultimap)) { + throw new ParseException(EditCommand.MESSAGE_EMERGENCY_CONTACT_FIELDS_INVALID); } + if (!isEmergencyContactFieldsProvided(args, argMultimap)) { + throw new ParseException(EditCommand.MESSAGE_EMERGENCY_CONTACT_NOT_EDITED); + } + } - return new EditCommand(index, editPersonDescriptor); + private Boolean isEmergencyContactFieldsProvided(String args, ArgumentMultimap argMultimap) { + if (argMultimap.getValue(PREFIX_EMERGENCY_CONTACT_TO_EDIT).isPresent() + && !argMultimap.getValue(PREFIX_EMERGENCY_CONTACT_NAME).isPresent() + && !argMultimap.getValue(PREFIX_EMERGENCY_CONTACT_PHONE).isPresent() + && !argMultimap.getValue(PREFIX_EMERGENCY_CONTACT_RELATIONSHIP).isPresent()) { + return false; + } + return true; + } + + private Boolean isEmergencyContactIndexProvided(String args, ArgumentMultimap argMultimap) { + if (!argMultimap.getValue(PREFIX_EMERGENCY_CONTACT_TO_EDIT).isPresent() + && (argMultimap.getValue(PREFIX_EMERGENCY_CONTACT_NAME).isPresent() + || argMultimap.getValue(PREFIX_EMERGENCY_CONTACT_PHONE).isPresent() + || argMultimap.getValue(PREFIX_EMERGENCY_CONTACT_RELATIONSHIP).isPresent())) { + return false; + } + return true; } /** diff --git a/src/main/java/seedu/address/logic/parser/FindDoctorCommandParser.java b/src/main/java/seedu/address/logic/parser/FindDoctorCommandParser.java new file mode 100644 index 00000000000..9ca1d54cca4 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/FindDoctorCommandParser.java @@ -0,0 +1,33 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import java.util.Arrays; + +import seedu.address.logic.commands.FindDoctorCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.DoctorNameContainsKeywordsPredicate; + +/** + * Parses input arguments and creates a new FindDoctorCommand object + */ +public class FindDoctorCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the FindDoctorCommand + * and returns a FindDoctorCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public FindDoctorCommand parse(String args) throws ParseException { + String trimmedArgs = args.trim(); + if (trimmedArgs.isEmpty()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindDoctorCommand.MESSAGE_USAGE)); + } + + String[] nameKeywords = trimmedArgs.split("\\s+"); + + return new FindDoctorCommand(new DoctorNameContainsKeywordsPredicate(Arrays.asList(nameKeywords))); + } + +} diff --git a/src/main/java/seedu/address/logic/parser/ListCommandParser.java b/src/main/java/seedu/address/logic/parser/ListCommandParser.java new file mode 100644 index 00000000000..04cd629f72b --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/ListCommandParser.java @@ -0,0 +1,38 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.logic.commands.ListCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.PersonComparators; + +/** + * Parses input arguments and creates a new ListCommand object + */ +public class ListCommandParser implements Parser { + /** + * Parses the given {@code String} of arguments in the context of the ListCommand + * and returns a ListCommand object for execution. + * + * @throws ParseException if the user input does not conform the expected format + */ + public ListCommand parse(String args) throws ParseException { + String trimmedArgs = args.trim(); + + switch (trimmedArgs.toLowerCase()) { + case "": + // Default sort order is timeAdded asc + return new ListCommand(PersonComparators.BY_ORDER_ADDED_REVERSED); + case "timeadded", "timeadded asc": + return new ListCommand(PersonComparators.BY_ORDER_ADDED_REVERSED); + case "timeadded desc": + return new ListCommand(PersonComparators.BY_ORDER_ADDED); + case "name", "name asc": + return new ListCommand(PersonComparators.BY_NAME); + case "name desc": + return new ListCommand(PersonComparators.BY_NAME_REVERSED); + default: + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, ListCommand.MESSAGE_USAGE)); + } + } +} diff --git a/src/main/java/seedu/address/logic/parser/LoadArchiveCommandParser.java b/src/main/java/seedu/address/logic/parser/LoadArchiveCommandParser.java new file mode 100644 index 00000000000..4fd1aedfde1 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/LoadArchiveCommandParser.java @@ -0,0 +1,30 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.commons.core.filename.Filename; +import seedu.address.logic.commands.LoadArchiveCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new LoadArchiveCommand object + */ +public class LoadArchiveCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the LoadArchiveCommand + * and returns a LoadArchiveCommand object for execution. + * + * @throws ParseException if the user input does not conform the expected format + */ + public LoadArchiveCommand parse(String args) throws ParseException { + Filename filename; + try { + filename = ParserUtil.parseFilename(args); + } catch (ParseException pe) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + pe.getMessage() + "\n\n" + LoadArchiveCommand.MESSAGE_USAGE), pe); + } + return new LoadArchiveCommand(filename); + } +} diff --git a/src/main/java/seedu/address/logic/parser/ParserUtil.java b/src/main/java/seedu/address/logic/parser/ParserUtil.java index b117acb9c55..5e1719fa37a 100644 --- a/src/main/java/seedu/address/logic/parser/ParserUtil.java +++ b/src/main/java/seedu/address/logic/parser/ParserUtil.java @@ -6,13 +6,16 @@ import java.util.HashSet; import java.util.Set; +import seedu.address.commons.core.filename.Filename; import seedu.address.commons.core.index.Index; import seedu.address.commons.util.StringUtil; import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.person.Address; +import seedu.address.model.person.DoctorName; import seedu.address.model.person.Email; import seedu.address.model.person.Name; import seedu.address.model.person.Phone; +import seedu.address.model.person.Relationship; import seedu.address.model.tag.Tag; /** @@ -25,6 +28,7 @@ public class ParserUtil { /** * Parses {@code oneBasedIndex} into an {@code Index} and returns it. Leading and trailing whitespaces will be * trimmed. + * * @throws ParseException if the specified index is invalid (not non-zero unsigned integer). */ public static Index parseIndex(String oneBasedIndex) throws ParseException { @@ -35,6 +39,24 @@ public static Index parseIndex(String oneBasedIndex) throws ParseException { return Index.fromOneBased(Integer.parseInt(trimmedIndex)); } + /** + * Parses {@code String filename} into a {@code Filename}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the specified filename is invalid. + */ + public static Filename parseFilename(String filename) throws ParseException { + requireNonNull(filename); + String trimmedFilename = filename.trim(); + if (trimmedFilename.isBlank()) { + throw new ParseException(Filename.MESSAGE_CONSTRAINTS_BLANK); + } + if (!Filename.isValidFilename(trimmedFilename)) { + throw new ParseException(Filename.MESSAGE_CONSTRAINTS); + } + return new Filename(trimmedFilename); + } + /** * Parses a {@code String name} into a {@code Name}. * Leading and trailing whitespaces will be trimmed. @@ -50,6 +72,21 @@ public static Name parseName(String name) throws ParseException { return new Name(trimmedName); } + /** + * Parses a {@code String doctorName} into a {@code DoctorName}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code doctorName} is invalid. + */ + public static DoctorName parseDoctorName(String doctorName) throws ParseException { + requireNonNull(doctorName); + String trimmedName = doctorName.trim(); + if (!DoctorName.isValidName(trimmedName)) { + throw new ParseException(DoctorName.MESSAGE_CONSTRAINTS); + } + return new DoctorName(trimmedName); + } + /** * Parses a {@code String phone} into a {@code Phone}. * Leading and trailing whitespaces will be trimmed. @@ -95,6 +132,24 @@ public static Email parseEmail(String email) throws ParseException { return new Email(trimmedEmail); } + /** + * Parses a {@code String relationship} into a {@code Relationship}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code relationship} is invalid. + */ + public static Relationship parseRelationship(String relationship) throws ParseException { + requireNonNull(relationship); + String trimmedRelationship = relationship.trim(); + if (!Relationship.isAlphanumericRelationship(trimmedRelationship)) { + throw new ParseException(Relationship.ALPHANUMERIC_CONSTRAINTS); + } + if (!Relationship.isValidRelationship(trimmedRelationship)) { + throw new ParseException(Relationship.RELATIONSHIP_TYPE_CONSTRAINTS); + } + return new Relationship(trimmedRelationship); + } + /** * Parses a {@code String tag} into a {@code Tag}. * Leading and trailing whitespaces will be trimmed. diff --git a/src/main/java/seedu/address/model/Model.java b/src/main/java/seedu/address/model/Model.java index d54df471c1f..1fba7acf35f 100644 --- a/src/main/java/seedu/address/model/Model.java +++ b/src/main/java/seedu/address/model/Model.java @@ -1,17 +1,22 @@ package seedu.address.model; +import java.io.IOException; import java.nio.file.Path; +import java.util.Comparator; import java.util.function.Predicate; import javafx.collections.ObservableList; import seedu.address.commons.core.GuiSettings; +import seedu.address.commons.core.filename.Filename; import seedu.address.model.person.Person; /** * The API of the Model component. */ public interface Model { - /** {@code Predicate} that always evaluate to true */ + /** + * {@code Predicate} that always evaluate to true + */ Predicate PREDICATE_SHOW_ALL_PERSONS = unused -> true; /** @@ -49,9 +54,58 @@ public interface Model { */ void setAddressBook(ReadOnlyAddressBook addressBook); - /** Returns the AddressBook */ + /** + * Returns the AddressBook + */ ReadOnlyAddressBook getAddressBook(); + /** + * Returns the archive directory path. + */ + Path getArchiveDirectoryPath(); + + /** + * Archives the address book. + * + * @param filename the name of the file to archive the address book to. + * @throws IOException if there was an error writing to the file. + */ + void archiveAddressBook(Filename filename) throws IOException; + + /** + * Undoes the previous command that modified the state or storage of the address book. + */ + void undoAddressBook(); + + /** + * Returns true if there is a previous state in the address book that can be undone. + * + * @return true if undo can be performed, false otherwise. + */ + boolean canUndoAddressBook(); + + /** + * Saves the current state of the address book to history. + */ + void saveAddressBook(); + + /** + * Restores the next state of the address book (redo). + * This method reverts the address book to a state that was undone + * and is available in the redo history, if such a state exists. + * If there is no state available to redo, no changes will be made. + */ + void redoAddressBook(); + + /** + * Returns true if there is a future state available to redo. + * This method checks whether the redo history contains a state + * that can be restored, meaning if the user has undone a state before + * and can now move forward to that state again. + */ + boolean canRedoAddressBook(); + + /** * Returns true if a person with the same identity as {@code person} exists in the address book. */ @@ -76,12 +130,22 @@ public interface Model { */ void setPerson(Person target, Person editedPerson); - /** Returns an unmodifiable view of the filtered person list */ + /** + * Returns an unmodifiable view of the filtered person list + */ ObservableList getFilteredPersonList(); /** * Updates the filter of the filtered person list to filter by the given {@code predicate}. + * * @throws NullPointerException if {@code predicate} is null. */ void updateFilteredPersonList(Predicate predicate); + + /** + * Sorts the filtered person list using the given {@code comparator}. + * + * @throws NullPointerException if {@code comparator} is null. + */ + void sortFilteredPersonList(Comparator comparator); } diff --git a/src/main/java/seedu/address/model/ModelManager.java b/src/main/java/seedu/address/model/ModelManager.java index 57bc563fde6..57cb7ef92bc 100644 --- a/src/main/java/seedu/address/model/ModelManager.java +++ b/src/main/java/seedu/address/model/ModelManager.java @@ -1,27 +1,40 @@ package seedu.address.model; +import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; import static java.util.Objects.requireNonNull; import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; +import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Comparator; import java.util.function.Predicate; import java.util.logging.Logger; import javafx.collections.ObservableList; import javafx.collections.transformation.FilteredList; +import javafx.collections.transformation.SortedList; import seedu.address.commons.core.GuiSettings; import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.core.filename.Filename; +import seedu.address.commons.util.FileUtil; import seedu.address.model.person.Person; /** * Represents the in-memory model of the address book data. */ public class ModelManager implements Model { + public static final String ARCHIVE_DIRNAME = "archive"; private static final Logger logger = LogsCenter.getLogger(ModelManager.class); - private final AddressBook addressBook; + // Stack to store the history of address book states for undo functionality + private final VersionedAddressBook versionedAddressBook; private final UserPrefs userPrefs; private final FilteredList filteredPersons; + private final SortedList sortedPersons; /** * Initializes a ModelManager with the given addressBook and userPrefs. @@ -31,9 +44,10 @@ public ModelManager(ReadOnlyAddressBook addressBook, ReadOnlyUserPrefs userPrefs logger.fine("Initializing with address book: " + addressBook + " and user prefs " + userPrefs); - this.addressBook = new AddressBook(addressBook); + this.versionedAddressBook = new VersionedAddressBook(addressBook); this.userPrefs = new UserPrefs(userPrefs); - filteredPersons = new FilteredList<>(this.addressBook.getPersonList()); + filteredPersons = new FilteredList<>(this.versionedAddressBook.getPersonList()); + sortedPersons = new SortedList<>(filteredPersons); } public ModelManager() { @@ -79,36 +93,115 @@ public void setAddressBookFilePath(Path addressBookFilePath) { @Override public void setAddressBook(ReadOnlyAddressBook addressBook) { - this.addressBook.resetData(addressBook); + this.versionedAddressBook.resetData(addressBook); // Reset the versioned address book's data + saveAddressBook(); } @Override public ReadOnlyAddressBook getAddressBook() { - return addressBook; + return versionedAddressBook; + } + + @Override + public Path getArchiveDirectoryPath() { + Path source = this.getAddressBookFilePath(); + assert source != null : "Address book file path is null"; + + Path archiveDir = Paths.get(source.getParent().toString(), ARCHIVE_DIRNAME); + + if (!Files.exists(archiveDir)) { + logger.info("No archive directory found."); + } + + return archiveDir; + } + + @Override + public void archiveAddressBook(Filename filename) throws IOException { + Path source = this.getAddressBookFilePath(); + assert source != null : "Address book file path is null"; + + String timestamp = LocalDateTime.now() + .format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) + .replace(":", "-"); // : is not allowed in filenames in Windows + String archiveFilename = source.getFileName().toString().replace(".json", "") + "-" + + timestamp + (filename.toString().isEmpty() ? "" : "-" + filename) + ".json"; + + Path destination = Paths.get(this.getArchiveDirectoryPath().toString(), archiveFilename); + + FileUtil.createParentDirsOfFile(destination); + Files.copy(source, destination, REPLACE_EXISTING); + logger.info("Address book has been archived!"); } @Override public boolean hasPerson(Person person) { requireNonNull(person); - return addressBook.hasPerson(person); + return versionedAddressBook.hasPerson(person); } @Override public void deletePerson(Person target) { - addressBook.removePerson(target); + versionedAddressBook.removePerson(target); + // No need updateFilteredPList as FilteredList<> auto-updates from addressBook4 + saveAddressBook(); } @Override public void addPerson(Person person) { - addressBook.addPerson(person); + versionedAddressBook.addPerson(person); updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + // FilteredList<> auto-updates from addressBook, BUT may not show, if alr looking at filtered list + saveAddressBook(); } @Override public void setPerson(Person target, Person editedPerson) { requireAllNonNull(target, editedPerson); + versionedAddressBook.setPerson(target, editedPerson); + saveAddressBook(); + } - addressBook.setPerson(target, editedPerson); + // ============ Undo and Redo Methods ================================================================ + + /** + * Commits the current state of the address book to history. + */ + @Override + public void saveAddressBook() { + versionedAddressBook.save(); + } + + /** + * Restores the previous state of the address book (undo). + */ + @Override + public void undoAddressBook() { + if (canUndoAddressBook()) { + versionedAddressBook.undo(); + updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + } + } + + /** + * Returns true if there is a previous state to undo. + */ + @Override + public boolean canUndoAddressBook() { + return versionedAddressBook.canUndo(); + } + + @Override + public void redoAddressBook() { + if (canRedoAddressBook()) { + versionedAddressBook.redo(); + updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + } + } + + @Override + public boolean canRedoAddressBook() { + return versionedAddressBook.canRedo(); } //=========== Filtered Person List Accessors ============================================================= @@ -119,7 +212,7 @@ public void setPerson(Person target, Person editedPerson) { */ @Override public ObservableList getFilteredPersonList() { - return filteredPersons; + return sortedPersons; } @Override @@ -128,21 +221,25 @@ public void updateFilteredPersonList(Predicate predicate) { filteredPersons.setPredicate(predicate); } + @Override + public void sortFilteredPersonList(Comparator comparator) { + requireNonNull(comparator); + sortedPersons.setComparator(comparator); + } + @Override public boolean equals(Object other) { if (other == this) { return true; } - - // instanceof handles nulls if (!(other instanceof ModelManager)) { return false; } ModelManager otherModelManager = (ModelManager) other; - return addressBook.equals(otherModelManager.addressBook) + return versionedAddressBook.equals(otherModelManager.versionedAddressBook) && userPrefs.equals(otherModelManager.userPrefs) - && filteredPersons.equals(otherModelManager.filteredPersons); + && filteredPersons.equals(otherModelManager.filteredPersons) + && sortedPersons.equals(otherModelManager.sortedPersons); } - } diff --git a/src/main/java/seedu/address/model/VersionedAddressBook.java b/src/main/java/seedu/address/model/VersionedAddressBook.java new file mode 100644 index 00000000000..1e7dc26364f --- /dev/null +++ b/src/main/java/seedu/address/model/VersionedAddressBook.java @@ -0,0 +1,80 @@ +package seedu.address.model; + +import java.util.ArrayList; +import java.util.List; + +/** + * VersionedAddressBook is an extension of the AddressBook that supports undo/redo functionality. + * It stores a history of AddressBook states and allows for reverting to previous states or restoring + * undone states. + */ +public class VersionedAddressBook extends AddressBook { + private final List addressBookStateList; + private int currentStatePointer; + + /** + * Creates a VersionedAddressBook with the initial state. + * @param initialState The initial state of the AddressBook. + */ + public VersionedAddressBook(ReadOnlyAddressBook initialState) { + super(initialState); + addressBookStateList = new ArrayList<>(); + addressBookStateList.add(new AddressBook(initialState)); + currentStatePointer = 0; + } + + /** + * Saves the current address book state to the state list. + * It removes all future states in the history after the current state when a new change is saved. + * This ensures that any redoable history is discarded once a new change is made after an undo. + */ + public void save() { + if (currentStatePointer < addressBookStateList.size() - 1) { + addressBookStateList.subList(currentStatePointer + 1, addressBookStateList.size()).clear(); + } + + // Check if the current state is same as last saved state -> avoid saving a duplicate state + if (!addressBookStateList.get(currentStatePointer).equals(new AddressBook(this))) { + // Save the current state + addressBookStateList.add(new AddressBook(this)); + currentStatePointer++; + } + } + + /** + * Restores the previous state of the address book. + * Moves one step back in the history to undo the last change. + */ + public void undo() { + if (canUndo()) { + currentStatePointer--; + resetData(addressBookStateList.get(currentStatePointer)); + } + } + + /** + * Checks if there is a previous state to undo. + */ + public boolean canUndo() { + return currentStatePointer > 0; + } + + /** + * Restores the next state of the address book. + * Moves one step forward in the history to redo the last undone change. + */ + public void redo() { + if (canRedo()) { + currentStatePointer++; + resetData(addressBookStateList.get(currentStatePointer)); + } + } + + /** + * Checks if there is a next state to redo. + */ + public boolean canRedo() { + return currentStatePointer < addressBookStateList.size() - 1; + } + +} diff --git a/src/main/java/seedu/address/model/person/Doctor.java b/src/main/java/seedu/address/model/person/Doctor.java new file mode 100644 index 00000000000..084885a8731 --- /dev/null +++ b/src/main/java/seedu/address/model/person/Doctor.java @@ -0,0 +1,108 @@ +package seedu.address.model.person; + +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.Objects; + +import seedu.address.commons.util.ToStringBuilder; + +/** + * Represents a Person in the address book. + * Guarantees: details are present and not null, field values are validated, + * immutable. + */ +public class Doctor { + + // Identity fields + private final DoctorName name; + private final Phone phone; + private final Email email; + + /** + * Every field must be present and not null. + */ + public Doctor(DoctorName name, Phone phone, Email email) { + requireAllNonNull(name, phone, email); + this.name = name; + this.phone = phone; + this.email = email; + + } + + /** + * Returns the name of the doctor. + * + * @return Name of the doctor. + */ + public DoctorName getName() { + return this.name; + } + + /** + * Returns the phone number of the doctor. + * + * @return Phone number of the doctor. + */ + public Phone getPhone() { + return this.phone; + } + + /** + * Returns the email of the doctor. + * + * @return Email of the doctor. + */ + public Email getEmail() { + return this.email; + } + + /** + * Returns true if both doctors have the same name. + * This defines a weaker notion of equality between two doctors. + */ + public boolean isSamePerson(Doctor otherPerson) { + if (otherPerson == this) { + return true; + } + + return otherPerson != null + && otherPerson.getName().equals(getName()); + } + + /** + * Returns true if both doctors have the same identity and data fields. + * This defines a stronger notion of equality between two doctors. + */ + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof Doctor)) { + return false; + } + + Doctor otherDoctor = (Doctor) other; + return name.equals(otherDoctor.name) + && phone.equals(otherDoctor.phone) + && email.equals(otherDoctor.email); + } + + @Override + public int hashCode() { + // use this method for custom fields hashing instead of implementing your own + return Objects.hash(getName(), getPhone(), getEmail()); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("name", name) + .add("phone", phone) + .add("email", email) + .toString(); + } + +} diff --git a/src/main/java/seedu/address/model/person/DoctorName.java b/src/main/java/seedu/address/model/person/DoctorName.java new file mode 100644 index 00000000000..f4a0837577a --- /dev/null +++ b/src/main/java/seedu/address/model/person/DoctorName.java @@ -0,0 +1,69 @@ +package seedu.address.model.person; + +/** + * Represents a Doctor's name in the address book. + * Guarantees: immutable; is valid as declared in {@link #isValidName(String)} + */ +public class DoctorName extends Name { + + public static final String MESSAGE_CONSTRAINTS = + "Names should not be blank and should only contain alphanumeric characters, spaces, the words 'd/o' or " + + "'s/o' or the following special characters: - . ( ) @ '"; + + /* + * The first character of the address must not be a whitespace, + * otherwise " " (a blank string) becomes a valid input. + */ + public static final String VALIDATION_REGEX = "[\\p{Alnum}-.()@/'][\\p{Alnum}-.()@/' ]*"; + + public final String doctorName; + /** + * Constructs a {@code DoctorName}. + * + * @param name A valid name. + */ + public DoctorName(String name) { + super(name); + this.doctorName = "Dr " + name; + } + + /** + * Returns true if a given string is a valid name. + */ + public static boolean isValidName(String test) { + return test.matches(VALIDATION_REGEX); + } + + /* + * Returns the name of the doctor, with the "Dr " prefix. + */ + public String getDoctorName() { + return doctorName; + } + + @Override + public String toString() { + return fullName; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof DoctorName)) { + return false; + } + + DoctorName otherName = (DoctorName) other; + return fullName.equals(otherName.fullName); + } + + @Override + public int hashCode() { + return fullName.hashCode(); + } + +} diff --git a/src/main/java/seedu/address/model/person/DoctorNameContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/person/DoctorNameContainsKeywordsPredicate.java new file mode 100644 index 00000000000..bdc0766c7e1 --- /dev/null +++ b/src/main/java/seedu/address/model/person/DoctorNameContainsKeywordsPredicate.java @@ -0,0 +1,45 @@ +package seedu.address.model.person; + +import java.util.List; +import java.util.function.Predicate; + +import seedu.address.commons.util.StringUtil; +import seedu.address.commons.util.ToStringBuilder; + +/** + * Tests that a {@code Person}'s {@code Name} matches any of the keywords given. + */ +public class DoctorNameContainsKeywordsPredicate implements Predicate { + private final List keywords; + + public DoctorNameContainsKeywordsPredicate(List keywords) { + this.keywords = keywords; + } + + @Override + public boolean test(Person person) { + return keywords.stream() + .anyMatch(keyword -> StringUtil.containsWordIgnoreCase( + person.getDoctor().getName().fullName, keyword)); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof DoctorNameContainsKeywordsPredicate)) { + return false; + } + + DoctorNameContainsKeywordsPredicate otherPredicate = (DoctorNameContainsKeywordsPredicate) other; + return keywords.equals(otherPredicate.keywords); + } + + @Override + public String toString() { + return new ToStringBuilder(this).add("keywords", keywords).toString(); + } +} diff --git a/src/main/java/seedu/address/model/person/Email.java b/src/main/java/seedu/address/model/person/Email.java index c62e512bc29..85ab9294e97 100644 --- a/src/main/java/seedu/address/model/person/Email.java +++ b/src/main/java/seedu/address/model/person/Email.java @@ -14,7 +14,7 @@ public class Email { + "and adhere to the following constraints:\n" + "1. The local-part should only contain alphanumeric characters and these special characters, excluding " + "the parentheses, (" + SPECIAL_CHARACTERS + "). The local-part may not start or end with any special " - + "characters.\n" + + "characters or have more than 1 special character in a row.\n" + "2. This is followed by a '@' and then a domain name. The domain name is made up of domain labels " + "separated by periods.\n" + "The domain name must:\n" diff --git a/src/main/java/seedu/address/model/person/EmergencyContact.java b/src/main/java/seedu/address/model/person/EmergencyContact.java new file mode 100644 index 00000000000..4b79e8143ac --- /dev/null +++ b/src/main/java/seedu/address/model/person/EmergencyContact.java @@ -0,0 +1,102 @@ +package seedu.address.model.person; + +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.Objects; + +/** + * Represents an Emergency Contact in the address book. + * Guarantees: fields are present and not null, field values are validated. + */ +public class EmergencyContact { + // Identity fields + private final Name name; + private final Phone phone; + private final Relationship relationship; + + /** + * Constructs an {@code EmergencyContact}. + * + * @param name A valid name. + * @param phone A valid phone number. + * @param relationship A valid relationship. + */ + public EmergencyContact(Name name, Phone phone, Relationship relationship) { + requireAllNonNull(name, phone, relationship); + this.name = name; + this.phone = phone; + this.relationship = relationship; + } + + /** + * Returns the name of the emergency contact. + * + * @return Name of the emergency contact. + */ + public Name getName() { + return name; + } + + /** + * Returns the phone number of the emergency contact. + * + * @return Phone number of the emergency contact. + */ + public Phone getPhone() { + return phone; + } + + /** + * Returns the relationship with the emergency contact. + * + * @return Relationship with the emergency contact. + */ + public Relationship getRelationship() { + return relationship; + } + + /** + * Returns true if both persons have the same name. + * This defines a weaker notion of equality between two persons. + */ + public boolean isSamePerson(EmergencyContact otherPerson) { + if (otherPerson == this) { + return true; + } + + return otherPerson != null + && otherPerson.getPhone().equals(getPhone()); + } + + /** + * Returns true if both persons have the same identity and data fields. + * This defines a stronger notion of equality between two persons. + */ + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof EmergencyContact)) { + return false; + } + + EmergencyContact otherPerson = (EmergencyContact) other; + return name.equals(otherPerson.name) + && phone.equals(otherPerson.phone) + && relationship.equals(otherPerson.relationship); + } + + @Override + public int hashCode() { + // use this method for custom fields hashing instead of implementing your own + return Objects.hash(name, phone, relationship); + } + + @Override + public String toString() { + return "Name: " + name + "; Phone: " + phone + "; Relationship: " + relationship + ";"; + } +} diff --git a/src/main/java/seedu/address/model/person/Name.java b/src/main/java/seedu/address/model/person/Name.java index 173f15b9b00..5df0a947ec0 100644 --- a/src/main/java/seedu/address/model/person/Name.java +++ b/src/main/java/seedu/address/model/person/Name.java @@ -7,16 +7,17 @@ * Represents a Person's name in the address book. * Guarantees: immutable; is valid as declared in {@link #isValidName(String)} */ -public class Name { +public class Name implements Comparable { public static final String MESSAGE_CONSTRAINTS = - "Names should only contain alphanumeric characters and spaces, and it should not be blank"; + "Names should not be blank and should only contain alphanumeric characters, spaces, the words 'd/o' or " + + "'s/o' or the following special characters: - . ( ) @ '"; /* * The first character of the address must not be a whitespace, * otherwise " " (a blank string) becomes a valid input. */ - public static final String VALIDATION_REGEX = "[\\p{Alnum}][\\p{Alnum} ]*"; + public static final String VALIDATION_REGEX = "[\\p{Alnum}-.()@/'][\\p{Alnum}-.()@/' ]*"; public final String fullName; @@ -33,11 +34,17 @@ public Name(String name) { /** * Returns true if a given string is a valid name. + * + * @param test String to test. */ public static boolean isValidName(String test) { return test.matches(VALIDATION_REGEX); } + @Override + public int compareTo(Name otherName) { + return fullName.compareTo(otherName.fullName); + } @Override public String toString() { @@ -51,11 +58,10 @@ public boolean equals(Object other) { } // instanceof handles nulls - if (!(other instanceof Name)) { + if (!(other instanceof Name otherName)) { return false; } - Name otherName = (Name) other; return fullName.equals(otherName.fullName); } diff --git a/src/main/java/seedu/address/model/person/Person.java b/src/main/java/seedu/address/model/person/Person.java index abe8c46b535..769264d3199 100644 --- a/src/main/java/seedu/address/model/person/Person.java +++ b/src/main/java/seedu/address/model/person/Person.java @@ -2,17 +2,22 @@ import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; +import java.time.LocalDateTime; import java.util.Collections; import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.Objects; import java.util.Set; +import seedu.address.commons.core.index.Index; import seedu.address.commons.util.ToStringBuilder; +import seedu.address.model.person.exceptions.EmergencyContactNotFoundException; import seedu.address.model.tag.Tag; /** * Represents a Person in the address book. - * Guarantees: details are present and not null, field values are validated, immutable. + * Guarantees: details are present and not null, field values are validated, + * immutable. */ public class Person { @@ -23,18 +28,27 @@ public class Person { // Data fields private final Address address; + // LinkedHashSet preserves the order of emergency contacts such that + // the index of the emergency contact can be reliably access by the Delete command. + private final Set emergencyContacts = new LinkedHashSet<>(); + private final Doctor doctor; private final Set tags = new HashSet<>(); + private final LocalDateTime dateAdded; /** * Every field must be present and not null. */ - public Person(Name name, Phone phone, Email email, Address address, Set tags) { - requireAllNonNull(name, phone, email, address, tags); + public Person(Name name, Phone phone, Email email, Address address, + Set emergencyContacts, Doctor doctor, Set tags) { + requireAllNonNull(name, phone, email, address, emergencyContacts, tags); this.name = name; this.phone = phone; this.email = email; this.address = address; + this.emergencyContacts.addAll(emergencyContacts); + this.doctor = doctor; this.tags.addAll(tags); + this.dateAdded = LocalDateTime.now(); } public Name getName() { @@ -52,15 +66,80 @@ public Email getEmail() { public Address getAddress() { return address; } + public EmergencyContact getFirstEmergencyContact() { + return emergencyContacts.iterator().next(); + } + + public Set getEmergencyContacts() { + return Collections.unmodifiableSet(emergencyContacts); + } + + public EmergencyContact getEmergencyContact(Index oneBasedIndex) + throws EmergencyContactNotFoundException { + int i = oneBasedIndex.getZeroBased(); + for (EmergencyContact emergencyContact : emergencyContacts) { + if (i == 0) { + return emergencyContact; + } + i = i - 1; + } + throw new EmergencyContactNotFoundException(); + } + + /** + * Returns a copy of emergencyContacts with + * @param emergencyContactToRemove removed + * @return a copy of emergencyContacts with {@code emergencyContactToRemove} removed + */ + public Set removeEmergencyContact(EmergencyContact emergencyContactToRemove) { + Set updatedEmergencyContacts = new LinkedHashSet<>(); + for (EmergencyContact emergencyContact : emergencyContacts) { + if (!emergencyContact.equals(emergencyContactToRemove)) { + updatedEmergencyContacts.add(emergencyContact); + } + } + return updatedEmergencyContacts; + } + + public Boolean hasOnlyOneEmergencyContact() { + return emergencyContacts.size() == 1; + } + + /** + * Checks if the specified emergency contact exists in the list of emergency contacts. + * + * @param emergencyContactToCheck The emergency contact to check for. + * @return {@code true} if the emergency contact exists in the list, {@code false} otherwise. + */ + public Boolean hasEmergencyContact(EmergencyContact emergencyContactToCheck) { + for (EmergencyContact emergencyContact : emergencyContacts) { + if (emergencyContact.equals((emergencyContactToCheck))) { + return true; + } + } + return false; + } + + public Doctor getDoctor() { + return doctor; + } /** - * Returns an immutable tag set, which throws {@code UnsupportedOperationException} + * Returns an immutable tag set, which throws + * {@code UnsupportedOperationException} * if modification is attempted. */ public Set getTags() { return Collections.unmodifiableSet(tags); } + /** + * Returns the datetime the person was added to the address book. + */ + public LocalDateTime getDateAdded() { + return dateAdded; + } + /** * Returns true if both persons have the same name. * This defines a weaker notion of equality between two persons. @@ -71,7 +150,7 @@ public boolean isSamePerson(Person otherPerson) { } return otherPerson != null - && otherPerson.getName().equals(getName()); + && otherPerson.getPhone().equals(getPhone()); } /** @@ -94,13 +173,15 @@ public boolean equals(Object other) { && phone.equals(otherPerson.phone) && email.equals(otherPerson.email) && address.equals(otherPerson.address) + && emergencyContacts.equals(otherPerson.emergencyContacts) + && doctor.equals(otherPerson.doctor) && tags.equals(otherPerson.tags); } @Override public int hashCode() { // use this method for custom fields hashing instead of implementing your own - return Objects.hash(name, phone, email, address, tags); + return Objects.hash(name, phone, email, address, emergencyContacts, doctor, tags); } @Override @@ -110,6 +191,8 @@ public String toString() { .add("phone", phone) .add("email", email) .add("address", address) + .add("emergency contacts", emergencyContacts) + .add("doctor", doctor) .add("tags", tags) .toString(); } diff --git a/src/main/java/seedu/address/model/person/PersonComparators.java b/src/main/java/seedu/address/model/person/PersonComparators.java new file mode 100644 index 00000000000..89947ef02ff --- /dev/null +++ b/src/main/java/seedu/address/model/person/PersonComparators.java @@ -0,0 +1,13 @@ +package seedu.address.model.person; + +import java.util.Comparator; + +/** + * Contains a list of comparators for {@code Person}. + */ +public class PersonComparators { + public static final Comparator BY_ORDER_ADDED = Comparator.comparing(Person::getDateAdded).reversed(); + public static final Comparator BY_ORDER_ADDED_REVERSED = Comparator.comparing(Person::getDateAdded); + public static final Comparator BY_NAME = Comparator.comparing(Person::getName); + public static final Comparator BY_NAME_REVERSED = Comparator.comparing(Person::getName).reversed(); +} diff --git a/src/main/java/seedu/address/model/person/Relationship.java b/src/main/java/seedu/address/model/person/Relationship.java new file mode 100644 index 00000000000..85653d18cfb --- /dev/null +++ b/src/main/java/seedu/address/model/person/Relationship.java @@ -0,0 +1,117 @@ +package seedu.address.model.person; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +import java.util.Objects; + +/** + * Represents a Relationship between a Person and their EmergencyContact + * Guarantees: immutable; is valid as declared in {@link #isValidRelationship(String)} + * and {@link #isAlphanumericRelationship(String)}. + */ +public class Relationship { + public static final String RELATIONSHIP_TYPE_CONSTRAINTS = + "Relationship type should be Parent, Child, Sibling, Spouse, Friend, " + + "Grandparent or Relative or their gendered variants"; + public static final String ALPHANUMERIC_CONSTRAINTS = "Relationship name " + + "should only contain alphanumeric characters and spaces, and it should not be blank"; + public static final String VALIDATION_REGEX = "[\\p{Alnum}][\\p{Alnum} ]*"; + public final String relationship; + + /** + * Constructs a {@code Relationship}. + * + * @param relationship A valid relationship type. + */ + public Relationship(String relationship) { + requireNonNull(relationship); + checkArgument(isAlphanumericRelationship(relationship), ALPHANUMERIC_CONSTRAINTS); + checkArgument(isValidRelationship(relationship), RELATIONSHIP_TYPE_CONSTRAINTS); + this.relationship = getRelationshipString(relationship); + } + + private static String getRelationshipString(String relationship) { + assert isValidRelationship(relationship); + return relationship.substring(0, 1).toUpperCase() + relationship.substring(1).toLowerCase(); + } + + /** + * Returns true if a given string is a valid relationship name alphanumerically. + */ + public static boolean isAlphanumericRelationship(String test) { + return test.matches(VALIDATION_REGEX); + } + + /** + * Returns true if the given string is a valid relationship type. + * + * @param relationship The relationship string to test. + * @return True if the string is a valid relationship type, false otherwise. + */ + public static Boolean isValidRelationship(String relationship) { + for (RelationshipType relationshipType : RelationshipType.values()) { + if (relationshipType.name().equals(relationship.toUpperCase())) { + return true; + } + } + return false; + } + + @Override + public String toString() { + return relationship; + } + + @Override + public int hashCode() { + // use this method for custom fields hashing instead of implementing your own + return Objects.hash(relationship); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof Relationship)) { + return false; + } + + Relationship otherRelationship = (Relationship) other; + return relationship.equals(otherRelationship.relationship); + } + + /** + * Enum representing different types of relationships. + */ + private enum RelationshipType { + PARENT, + MOTHER, + FATHER, + CHILD, + SON, + DAUGHTER, + SIBLING, + BROTHER, + SISTER, + FRIEND, + SPOUSE, + HUSBAND, + WIFE, + PARTNER, + COUSIN, + RELATIVE, + UNCLE, + AUNT, + GRANDPARENT, + GRANDMOTHER, + GRANDFATHER, + GRANDCHILD, + GRANDSON, + GRANDDAUGHTER; + } + +} diff --git a/src/main/java/seedu/address/model/person/exceptions/EmergencyContactNotFoundException.java b/src/main/java/seedu/address/model/person/exceptions/EmergencyContactNotFoundException.java new file mode 100644 index 00000000000..71e9f54d2ae --- /dev/null +++ b/src/main/java/seedu/address/model/person/exceptions/EmergencyContactNotFoundException.java @@ -0,0 +1,7 @@ +package seedu.address.model.person.exceptions; + +/** + * Signals that the operation is unable to find the specified EmergencyContact. + */ +public class EmergencyContactNotFoundException extends RuntimeException{ +} diff --git a/src/main/java/seedu/address/model/tag/Tag.java b/src/main/java/seedu/address/model/tag/Tag.java index f1a0d4e233b..b2995d48aa0 100644 --- a/src/main/java/seedu/address/model/tag/Tag.java +++ b/src/main/java/seedu/address/model/tag/Tag.java @@ -10,7 +10,7 @@ public class Tag { public static final String MESSAGE_CONSTRAINTS = "Tags names should be alphanumeric"; - public static final String VALIDATION_REGEX = "\\p{Alnum}+"; + public static final String VALIDATION_REGEX = "[\\p{Alnum}-.][\\p{Alnum}- .]*"; public final String tagName; diff --git a/src/main/java/seedu/address/model/util/SampleDataUtil.java b/src/main/java/seedu/address/model/util/SampleDataUtil.java index 1806da4facf..3ea73a9212c 100644 --- a/src/main/java/seedu/address/model/util/SampleDataUtil.java +++ b/src/main/java/seedu/address/model/util/SampleDataUtil.java @@ -7,10 +7,14 @@ import seedu.address.model.AddressBook; import seedu.address.model.ReadOnlyAddressBook; import seedu.address.model.person.Address; +import seedu.address.model.person.Doctor; +import seedu.address.model.person.DoctorName; import seedu.address.model.person.Email; +import seedu.address.model.person.EmergencyContact; import seedu.address.model.person.Name; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; +import seedu.address.model.person.Relationship; import seedu.address.model.tag.Tag; /** @@ -19,24 +23,87 @@ public class SampleDataUtil { public static Person[] getSamplePersons() { return new Person[] { - new Person(new Name("Alex Yeoh"), new Phone("87438807"), new Email("alexyeoh@example.com"), + new Person(new Name("Alex Yeoh"), new Phone("87438807"), new Email("alexyeoh@gmail.com"), new Address("Blk 30 Geylang Street 29, #06-40"), - getTagSet("friends")), - new Person(new Name("Bernice Yu"), new Phone("99272758"), new Email("berniceyu@example.com"), + getEmergencyContactSet( + new EmergencyContact( + new Name("Sarah Lim"), + new Phone("98761234"), + new Relationship("Daughter")), + new EmergencyContact( + new Name("Peter Yeoh"), + new Phone("97645132"), + new Relationship("Son"))), + new Doctor(new DoctorName("Tan Wei Ming"), new Phone("99119919"), new Email("drtan@gmail.com")), + getTagSet("Mandarin-speaking", "hard of hearing")), + new Person(new Name("Bernice Yu"), new Phone("99272758"), new Email("berniceyu@yahoo.com"), new Address("Blk 30 Lorong 3 Serangoon Gardens, #07-18"), - getTagSet("colleagues", "friends")), - new Person(new Name("Charlotte Oliveiro"), new Phone("93210283"), new Email("charlotte@example.com"), + getEmergencyContactSet( + new EmergencyContact( + new Name("Kevin Goh"), + new Phone("98764123"), + new Relationship("Husband")), + new EmergencyContact( + new Name("Brad Goh"), + new Phone("98764142"), + new Relationship("Son") + )), + new Doctor(new DoctorName("Lim Heng Seng"), new Phone("80987123"), new Email("drlim@gmail.com")), + getTagSet("short-term patient")), + new Person(new Name("Charlotte Oliveiro"), new Phone("93210283"), new Email("charlotte@hotmail.com"), new Address("Blk 11 Ang Mo Kio Street 74, #11-04"), - getTagSet("neighbours")), - new Person(new Name("David Li"), new Phone("91031282"), new Email("lidavid@example.com"), + getEmergencyContactSet( + new EmergencyContact( + new Name("Haziq Bin Abudllah"), + new Phone("98763412"), + new Relationship("Brother")), + new EmergencyContact( + new Name("Ahmad Bin Ahman"), + new Phone("98763448"), + new Relationship("Grandson")) + ), + new Doctor(new DoctorName("Robert Lim"), new Phone("91919191"), new Email("robertlim@gmail.com")), + getTagSet("needs hearing aid")), + new Person(new Name("David Li"), new Phone("91031282"), new Email("lidavid@hotmail.com"), new Address("Blk 436 Serangoon Gardens Street 26, #16-43"), - getTagSet("family")), - new Person(new Name("Irfan Ibrahim"), new Phone("92492021"), new Email("irfan@example.com"), + getEmergencyContactSet( + new EmergencyContact(new Name("Amanda Lee"), new Phone("98762341"), new Relationship("Cousin")) + ), + new Doctor(new DoctorName("Jessica Loh"), new Phone("99119919"), new Email("jloh@gmail.com")), + getTagSet("short-term residential address")), + new Person(new Name("Irfan Ibrahim"), new Phone("92492021"), new Email("irfan@gmail.com"), new Address("Blk 47 Tampines Street 20, #17-35"), - getTagSet("classmates")), - new Person(new Name("Roy Balakrishnan"), new Phone("92624417"), new Email("royb@example.com"), + getEmergencyContactSet( + new EmergencyContact( + new Name("Nurul Ain"), + new Phone("98761243"), + new Relationship("Daughter")), + new EmergencyContact( + new Name("Izzudin Aiman"), + new Phone("94673215"), + new Relationship("Son") + ), + new EmergencyContact( + new Name("Khairul Anwar"), + new Phone("94673185"), + new Relationship("Relative") + ) + ), new Doctor(new DoctorName("Zhou Jie Lun"), new Phone("88888888"), new Email("zhoujl@hotmail.com")), + getTagSet("requires Malay translator")), + new Person(new Name("Roy Balakrishnan"), new Phone("92624417"), new Email("royb@yahoo.com"), new Address("Blk 45 Aljunied Street 85, #11-31"), - getTagSet("colleagues")) + getEmergencyContactSet( + new EmergencyContact( + new Name("Anjali Devi"), + new Phone("98763124"), + new Relationship("Daughter")), + new EmergencyContact( + new Name("Belle Choy"), + new Phone("98763187"), + new Relationship("Granddaughter")) + ), + new Doctor(new DoctorName("Ed Sheeran"), new Phone("95114320"), new Email("edsheeran@gmail.com")), + getTagSet("speech impaired")) }; } @@ -48,6 +115,13 @@ public static ReadOnlyAddressBook getSampleAddressBook() { return sampleAb; } + /** + * Returns a EmergencyContact set containing the list of emergency contacts given. + */ + public static Set getEmergencyContactSet(EmergencyContact... emergencyContacts) { + return Arrays.stream(emergencyContacts).collect(Collectors.toSet()); + } + /** * Returns a tag set containing the list of strings given. */ diff --git a/src/main/java/seedu/address/storage/JsonAdaptedDoctor.java b/src/main/java/seedu/address/storage/JsonAdaptedDoctor.java new file mode 100644 index 00000000000..f96a29392a0 --- /dev/null +++ b/src/main/java/seedu/address/storage/JsonAdaptedDoctor.java @@ -0,0 +1,78 @@ +package seedu.address.storage; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.person.Doctor; +import seedu.address.model.person.DoctorName; +import seedu.address.model.person.Email; +import seedu.address.model.person.Phone; + +/** + * Constructs a {@code JsonAdaptedDoctor} with the given person details. + */ +public class JsonAdaptedDoctor { + private final String doctorName; + private final String doctorPhone; + private final String doctorEmail; + + /** + * Constructs a {@code JsonAdaptedDoctor} with the given {@code doctorName}, + * {@code doctorPhone} and {@code doctorEmail}. + */ + @JsonCreator + public JsonAdaptedDoctor(@JsonProperty("doctorName") String doctorName, + @JsonProperty("doctorPhone") String doctorPhone, + @JsonProperty("doctorEmail") String doctorEmail) { + this.doctorName = doctorName; + this.doctorPhone = doctorPhone; + this.doctorEmail = doctorEmail; + } + + /** + * Converts a given {@code Doctor} into this class for Jackson use. + */ + public JsonAdaptedDoctor(Doctor source) { + doctorName = source.getName().fullName; + doctorPhone = source.getPhone().value; + doctorEmail = source.getEmail().value; + } + + /** + * Converts this Jackson-friendly adapted Doctor object into the model's {@code Doctor} object. + * + * @throws IllegalValueException if there were any data constraints violated in the adapted tag. + */ + public Doctor toModelType() throws IllegalValueException { + if (doctorName == null) { + throw new IllegalValueException(String.format(JsonAdaptedPerson.MISSING_FIELD_MESSAGE_FORMAT, + DoctorName.class.getSimpleName())); + } + if (!DoctorName.isValidName(doctorName)) { + throw new IllegalValueException(DoctorName.MESSAGE_CONSTRAINTS); + } + final DoctorName modelDoctorName = new DoctorName(doctorName); + + if (doctorPhone == null) { + throw new IllegalValueException(String.format(JsonAdaptedPerson.MISSING_FIELD_MESSAGE_FORMAT, + Phone.class.getSimpleName())); + } + if (!Phone.isValidPhone(doctorPhone)) { + throw new IllegalValueException(Phone.MESSAGE_CONSTRAINTS); + } + final Phone modelDoctorPhone = new Phone(doctorPhone); + + if (doctorEmail == null) { + throw new IllegalValueException(String.format(JsonAdaptedPerson.MISSING_FIELD_MESSAGE_FORMAT, + Email.class.getSimpleName())); + } + if (!Email.isValidEmail(doctorEmail)) { + throw new IllegalValueException(Email.MESSAGE_CONSTRAINTS); + } + + final Email modelDoctorEmail = new Email(doctorEmail); + + return new Doctor(modelDoctorName, modelDoctorPhone, modelDoctorEmail); + } +} diff --git a/src/main/java/seedu/address/storage/JsonAdaptedEmergencyContact.java b/src/main/java/seedu/address/storage/JsonAdaptedEmergencyContact.java new file mode 100644 index 00000000000..48b4a986b18 --- /dev/null +++ b/src/main/java/seedu/address/storage/JsonAdaptedEmergencyContact.java @@ -0,0 +1,79 @@ +package seedu.address.storage; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.person.EmergencyContact; +import seedu.address.model.person.Name; +import seedu.address.model.person.Phone; +import seedu.address.model.person.Relationship; + +/** + * Jackson-friendly version of {@link EmergencyContact}. + */ +public class JsonAdaptedEmergencyContact { + private final String ecName; + private final String ecPhone; + private final String ecRelationship; + /** + * Constructs a {@code JsonAdaptedEmergencyContact} with the given {@code ecName}, + * {@code ecPhone} and {@code ecRelationship}. + */ + @JsonCreator + public JsonAdaptedEmergencyContact(@JsonProperty("ecName") String ecName, + @JsonProperty("ecPhone") String ecPhone, + @JsonProperty("ecRelationship") String ecRelationship) { + this.ecName = ecName; + this.ecPhone = ecPhone; + this.ecRelationship = ecRelationship; + } + + /** + * Converts a given {@code Emergency Contact} into this class for Jackson use. + */ + public JsonAdaptedEmergencyContact(EmergencyContact source) { + ecName = source.getName().fullName; + ecPhone = source.getPhone().value; + ecRelationship = source.getRelationship().relationship; + } + + /** + * Converts this Jackson-friendly adapted Emergency Contact object into the model's {@code EmergencyContact} object. + * + * @throws IllegalValueException if there were any data constraints violated in the adapted tag. + */ + public EmergencyContact toModelType() throws IllegalValueException { + if (ecName == null) { + throw new IllegalValueException(String.format(JsonAdaptedPerson.MISSING_FIELD_MESSAGE_FORMAT, + Name.class.getSimpleName())); + } + if (!Name.isValidName(ecName)) { + throw new IllegalValueException(Name.MESSAGE_CONSTRAINTS); + } + final Name modelEcName = new Name(ecName); + + if (ecPhone == null) { + throw new IllegalValueException(String.format(JsonAdaptedPerson.MISSING_FIELD_MESSAGE_FORMAT, + Phone.class.getSimpleName())); + } + if (!Phone.isValidPhone(ecPhone)) { + throw new IllegalValueException(Phone.MESSAGE_CONSTRAINTS); + } + final Phone modelEcPhone = new Phone(ecPhone); + + if (ecRelationship == null) { + throw new IllegalValueException(String.format(JsonAdaptedPerson.MISSING_FIELD_MESSAGE_FORMAT, + Relationship.class.getSimpleName())); + } + if (!Relationship.isAlphanumericRelationship(ecRelationship)) { + throw new IllegalValueException(Relationship.ALPHANUMERIC_CONSTRAINTS); + } + if (!Relationship.isValidRelationship(ecRelationship)) { + throw new IllegalValueException(Relationship.RELATIONSHIP_TYPE_CONSTRAINTS); + } + final Relationship modelEcRelationship = new Relationship(ecRelationship); + + return new EmergencyContact(modelEcName, modelEcPhone, modelEcRelationship); + } +} diff --git a/src/main/java/seedu/address/storage/JsonAdaptedPerson.java b/src/main/java/seedu/address/storage/JsonAdaptedPerson.java index bd1ca0f56c8..fa629045678 100644 --- a/src/main/java/seedu/address/storage/JsonAdaptedPerson.java +++ b/src/main/java/seedu/address/storage/JsonAdaptedPerson.java @@ -2,6 +2,7 @@ import java.util.ArrayList; import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import java.util.stream.Collectors; @@ -11,7 +12,9 @@ import seedu.address.commons.exceptions.IllegalValueException; import seedu.address.model.person.Address; +import seedu.address.model.person.Doctor; import seedu.address.model.person.Email; +import seedu.address.model.person.EmergencyContact; import seedu.address.model.person.Name; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; @@ -28,6 +31,8 @@ class JsonAdaptedPerson { private final String phone; private final String email; private final String address; + private final List emergencyContacts = new ArrayList<>(); + private final JsonAdaptedDoctor doctor; private final List tags = new ArrayList<>(); /** @@ -36,11 +41,17 @@ class JsonAdaptedPerson { @JsonCreator public JsonAdaptedPerson(@JsonProperty("name") String name, @JsonProperty("phone") String phone, @JsonProperty("email") String email, @JsonProperty("address") String address, + @JsonProperty("emergencyContacts") List emergencyContacts, + @JsonProperty("doctor") JsonAdaptedDoctor doctor, @JsonProperty("tags") List tags) { this.name = name; this.phone = phone; this.email = email; this.address = address; + if (emergencyContacts != null) { + this.emergencyContacts.addAll(emergencyContacts); + } + this.doctor = doctor; if (tags != null) { this.tags.addAll(tags); } @@ -54,6 +65,10 @@ public JsonAdaptedPerson(Person source) { phone = source.getPhone().value; email = source.getEmail().value; address = source.getAddress().value; + emergencyContacts.addAll(source.getEmergencyContacts().stream() + .map(JsonAdaptedEmergencyContact::new) + .collect(Collectors.toList())); + doctor = new JsonAdaptedDoctor(source.getDoctor()); tags.addAll(source.getTags().stream() .map(JsonAdaptedTag::new) .collect(Collectors.toList())); @@ -65,6 +80,11 @@ public JsonAdaptedPerson(Person source) { * @throws IllegalValueException if there were any data constraints violated in the adapted person. */ public Person toModelType() throws IllegalValueException { + final List personEmergencyContacts = new ArrayList<>(); + for (JsonAdaptedEmergencyContact emergencyContact : emergencyContacts) { + personEmergencyContacts.add(emergencyContact.toModelType()); + } + final List personTags = new ArrayList<>(); for (JsonAdaptedTag tag : tags) { personTags.add(tag.toModelType()); @@ -102,8 +122,20 @@ public Person toModelType() throws IllegalValueException { } final Address modelAddress = new Address(address); + if (emergencyContacts.isEmpty()) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, + EmergencyContact.class.getSimpleName())); + } + final Set modelEmergencyContacts = new LinkedHashSet<>(personEmergencyContacts); + + if (doctor == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, "Doctor")); + } + final Doctor modelDoctor = doctor.toModelType(); + final Set modelTags = new HashSet<>(personTags); - return new Person(modelName, modelPhone, modelEmail, modelAddress, modelTags); + return new Person(modelName, modelPhone, modelEmail, modelAddress, modelEmergencyContacts, modelDoctor, + modelTags); } } diff --git a/src/main/java/seedu/address/ui/CommandBox.java b/src/main/java/seedu/address/ui/CommandBox.java index 9e75478664b..82a0b942413 100644 --- a/src/main/java/seedu/address/ui/CommandBox.java +++ b/src/main/java/seedu/address/ui/CommandBox.java @@ -1,85 +1,595 @@ package seedu.address.ui; -import javafx.collections.ObservableList; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + import javafx.fxml.FXML; +import javafx.scene.control.Label; import javafx.scene.control.TextField; +import javafx.scene.input.KeyCode; import javafx.scene.layout.Region; -import seedu.address.logic.commands.CommandResult; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.logic.parser.exceptions.ParseException; + /** - * The UI component that is responsible for receiving user command inputs. + * A CommandBox component that is part of the UI, allowing the user to input commands. + * This version provides simple word-by-word autocomplete suggestions based on the command syntax map. */ public class CommandBox extends UiPart { - public static final String ERROR_STYLE_CLASS = "error"; private static final String FXML = "CommandBox.fxml"; - - private final CommandExecutor commandExecutor; + private static final Map commandSyntaxMap = new HashMap<>(); + private static final Map> commandParameterOrder = new HashMap<>(); + private static final Map parameterMap = new HashMap<>(); + private static final String DEFAULT_STYLE = "-fx-font-family: 'Segoe UI'; -fx-font-size: 13pt;" + + " -fx-text-fill: white;"; + private static final String INPUT_NEEDED_STYLE = "-fx-font-family: 'Segoe UI'; -fx-font-size: 13pt;" + + " -fx-text-fill: #fb5252;"; @FXML private TextField commandTextField; + @FXML + private Label suggestionLabel; + + private final CommandExecutor commandExecutor; + + static { + // Initialize command syntax map + commandSyntaxMap.put("add", "add n/NAME p/PHONE e/EMAIL a/ADDRESS ecname/EMERGENCY_CONTACT_NAME " + + "ecphone/EMERGENCY_CONTACT_PHONE ecrs/EMERGENCY_CONTACT_RELATIONSHIP " + + "dname/DOCTOR_NAME dphone/DOCTOR_PHONE demail/DOCTOR_EMAIL t/TAG"); + commandSyntaxMap.put("addec", "addec INDEX ecname/EMERGENCY_CONTACT_NAME ecphone/EMERGENCY_CONTACT_PHONE " + + "ecrs/EMERGENCY_CONTACT_RELATIONSHIP"); + commandSyntaxMap.put("archive", "archive DESCRIPTION"); + commandSyntaxMap.put("clear", "clear"); + commandSyntaxMap.put("delete", "delete INDEX ec/EMERGENCY_CONTACT_INDEX"); + commandSyntaxMap.put("edit", "edit INDEX n/NAME p/PHONE e/EMAIL a/ADDRESS ec/EMERGENCY_CONTACT_INDEX " + + "ecname/EMERGENCY_CONTACT_NAME ecrs/EMERGENCY_CONTACT_RELATIONSHIP dname/DOCTOR_NAME " + + "dphone/DOCTOR_PHONE demail/DOCTOR_EMAIL t/TAG"); + commandSyntaxMap.put("find", "find KEYWORD MORE_KEYWORDS"); + commandSyntaxMap.put("finddoc", "finddoc KEYWORD MORE_KEYWORDS"); + commandSyntaxMap.put("help", "help"); + commandSyntaxMap.put("list", "list SORT_ORDER"); + commandSyntaxMap.put("listarchives", "listarchives"); + commandSyntaxMap.put("loadarchive", "loadarchive FILE_NAME"); + commandSyntaxMap.put("deletearchive", "deletearchive FILE_NAME"); + commandSyntaxMap.put("redo", "redo"); + commandSyntaxMap.put("undo", "undo"); + + // Initialize parameter mappings + parameterMap.put("n/", "n/NAME"); + parameterMap.put("p/", "p/PHONE"); + parameterMap.put("e/", "e/EMAIL"); + parameterMap.put("a/", "a/ADDRESS"); + parameterMap.put("ecname/", "ecname/EMERGENCY_CONTACT_NAME"); + parameterMap.put("ecphone/", "ecphone/EMERGENCY_CONTACT_PHONE"); + parameterMap.put("ecrs/", "ecrs/EMERGENCY_CONTACT_RELATIONSHIP"); + parameterMap.put("dname/", "dname/DOCTOR_NAME"); + parameterMap.put("dphone/", "dphone/DOCTOR_PHONE"); + parameterMap.put("demail/", "demail/DOCTOR_EMAIL"); + parameterMap.put("t/", "t/TAG"); + parameterMap.put("ec/", "ec/EMERGENCY_CONTACT_INDEX"); + + // Initialize command-specific parameter order for commands that have "/" in them + commandParameterOrder.put("add", Arrays.asList( + "n/", "p/", "e/", "a/", "ecname/", "ecphone/", "ecrs/", "dname/", "dphone/", "demail/", "t/" + )); + commandParameterOrder.put("addec", Arrays.asList( + "ecname/", "ecphone/", "ecrs/" + )); + commandParameterOrder.put("edit", Arrays.asList( + "n/", "p/", "e/", "a/", "ec/", "ecname/", "ecrs/", "dname/", "dphone/", "demail/", "t/" + )); + commandParameterOrder.put("delete", Arrays.asList( + "ec/" + )); + } /** - * Creates a {@code CommandBox} with the given {@code CommandExecutor}. + * Creates a CommandBox with the given CommandExecutor. + * This command box provides an command interface with suggestions based on command syntax. + * Suggestions are shown for: + * - Available commands when typing command keywords + * - Parameter syntax for commands with parameters + * - Next parameter in sequence after completing each parameter + * + * @param commandExecutor Lambda function that executes the entered command text */ public CommandBox(CommandExecutor commandExecutor) { super(FXML); this.commandExecutor = commandExecutor; - // calls #setStyleToDefault() whenever there is a change to the text of the command box. - commandTextField.textProperty().addListener((unused1, unused2, unused3) -> setStyleToDefault()); + // Listen for ANY text changes to reset ALL styles + commandTextField.textProperty().addListener((observable, oldValue, newValue) -> { + resetStyle(); + commandTextField.getStyleClass().remove(ERROR_STYLE_CLASS); + handleTextChanged(newValue); + }); + + // Consume control key to autocomplete + commandTextField.setOnKeyPressed(event -> { + if (event.getCode() == KeyCode.CONTROL) { + handleControlPressed(); + event.consume(); + } + }); + } + private void resetStyle() { + commandTextField.setStyle(DEFAULT_STYLE); + } + + + private String findShortestCommandMatch(String partial) { + if (partial.isEmpty()) { + return null; + } + String shortestMatch = null; + + for (String command : commandSyntaxMap.keySet()) { + if (command.startsWith(partial)) { + if (shortestMatch == null || command.length() < shortestMatch.length()) { + shortestMatch = command; + } + } + } + return shortestMatch; + } + + private void suggestNextParameter(String command, String input) { + String nextParam = getNextParameter(command, input.trim()); + if (nextParam != null) { + String nextPrefix = nextParam.substring(0, nextParam.indexOf('/') + 1); + commandTextField.setText(input.trim() + " " + nextPrefix); + commandTextField.positionCaret(commandTextField.getText().length()); + } } /** - * Handles the Enter button pressed event. + * Retrieves the next uncompleted parameter for the given command based on the current input. + * The method checks the sequence of parameters defined for the command and returns the + * next parameter that has not been provided yet. + * + * Example: + * If the command is "add" and the currentInput is "add n/John p/12345", + * and the parameter sequence is ["n/", "p/", "e/"], + * this method will return "e/" since it is the next uncompleted parameter. */ + private String getNextParameter(String command, String currentInput) { + List paramOrder = commandParameterOrder.get(command); + if (paramOrder == null) { + return null; + } + + // Create a set of completed prefixes + Set completedPrefixes = new HashSet<>(); + + // Split input into parts and process each part in order + String[] parts = currentInput.split("\\s+"); + for (int i = 1; i < parts.length; i++) { + String part = parts[i]; + // Find the longest matching prefix for this part + String matchedPrefix = null; + for (String prefix : paramOrder) { + if (part.startsWith(prefix) && (matchedPrefix == null || prefix.length() > matchedPrefix.length())) { + matchedPrefix = prefix; + } + } + + // If we found a prefix and there's a value after it, mark it as completed + if (matchedPrefix != null) { + String value = part.substring(matchedPrefix.length()); + if (!value.isEmpty()) { + completedPrefixes.add(matchedPrefix); + } + } + } + + // Find the first uncompleted parameter in the order + for (String prefix : paramOrder) { + if (!completedPrefixes.contains(prefix)) { + return parameterMap.get(prefix); + } + } + + return null; + } + + + private void handleControlPressed() { + /* Trim excess spaces but keep the input intact + "\\s+$" is the REGEX pattern being used here: + '\\s' matches any whitespace character (including spaces, tabs, etc.). + '+' means one or more occurrences of the previous pattern (\\s). + '$' matches the end of the string. + */ + String input = commandTextField.getText().replaceAll("\\s+$", ""); // Remove trailing spaces + + // check for single word commands e.g(add, INDEX, find etc) + if (!input.contains(" ")) { + String match = findShortestCommandMatch(input.trim()); + if (match != null) { + if (match.equals(input)) { + String syntax = commandSyntaxMap.get(match); + // these words need be replaced by 1-word inputs by users that cannot be autocompleted + if (syntax.contains("INDEX") || syntax.contains("KEYWORD")) { + commandTextField.setStyle(INPUT_NEEDED_STYLE); + return; + } + // If no INDEX needed, proceed with parameter autocomplete e.g.(proceed to n/ p/ cases) + suggestNextParameter(match, match); //input vs cur state same + } else { + // Just complete the command (delete, add, find) + commandTextField.setText(match); + commandTextField.positionCaret(match.length()); + } + } + return; + } + + // Check if input sequence is valid before proceeding with ANY autocomplete + if (!isValidPreSlashSequence(input)) { + return; + } + + String[] parts = input.split("\\s+"); + String command = parts[0]; + String lastPart = parts[parts.length - 1]; + + // If last part ends with a slash, turn red (user needs to input info) + if (lastPart.endsWith("/")) { + commandTextField.setStyle(INPUT_NEEDED_STYLE); + return; + } + + // Check if the last part contains a valid prefix and value + if (lastPart.contains("/")) { + String value = lastPart.substring(lastPart.indexOf('/') + 1).trim(); + + // If there's a value after the slash + if (!value.isEmpty()) { + suggestNextParameter(command, input); + return; + } + } + + if (!commandSyntaxMap.containsKey(command)) { + return; + } + suggestNextParameter(command, input); + } + + private boolean isValidPreSlashSequence(String input) { + String command = input.split("\\s+")[0]; + if (!commandSyntaxMap.containsKey(command)) { + return false; + } + + String fullSyntax = commandSyntaxMap.get(command); + + if (fullSyntax.contains("INDEX") || fullSyntax.contains("KEYWORD")) { + String[] inputParts = input.trim().split("\\s+"); + if (inputParts.length > 1) { + if (fullSyntax.contains("INDEX")) { + fullSyntax = fullSyntax.replace("INDEX", inputParts[1]); + } else { + fullSyntax = fullSyntax.replace("KEYWORD", inputParts[1]); + } + } + } + + // Find the longest matching prefix in the input + String longestPrefix = null; + int longestPrefixLength = 0; + for (String prefix : parameterMap.keySet()) { + if (input.contains(prefix) && prefix.length() > longestPrefixLength) { + longestPrefix = prefix; + longestPrefixLength = prefix.length(); + } + } + + // If no prefix found or input shorter than what we found so far + if (longestPrefix == null) { + return true; // Allow typing to continue + } + + int inputSlashPos = input.indexOf('/'); + int syntaxSlashPos = fullSyntax.indexOf('/'); + + String inputToCompare = (inputSlashPos == -1) ? input : input.substring(0, inputSlashPos); + String syntaxToCompare = fullSyntax.substring(0, syntaxSlashPos); + + if (inputToCompare.length() > syntaxToCompare.length()) { + return false; + } + + for (int i = 0; i < inputToCompare.length(); i++) { + if (inputToCompare.charAt(i) != syntaxToCompare.charAt(i)) { + return false; + } + } + + return true; + } + + private boolean hasInvalidParameterStructure(String input) { + String[] parts = input.split("\\s+"); + + // Create a set of valid prefixes for easier lookup + Set validPrefixes = new HashSet<>(parameterMap.keySet()); + + String currentPrefix = null; // Track current param prefix + + for (String part : parts) { + if (part.trim().isEmpty()) { + continue; + } + + int slashIndex = part.indexOf('/'); + if (slashIndex != -1) { + // Check for space before slash in this part + if (part.substring(0, slashIndex).contains(" ")) { + return true; // Invalid if there's space before slash + } + + // Find the longest valid prefix at the start of this part + String foundPrefix = null; + int longestLength = 0; + + for (String prefix : validPrefixes) { + if (part.startsWith(prefix) && prefix.length() > longestLength) { + foundPrefix = prefix; + longestLength = prefix.length(); + } + } + + // If we found a valid prefix at the start + if (foundPrefix != null) { + currentPrefix = foundPrefix; + // Don't check for more slashes in the value part + } else if (currentPrefix == null) { + // Only invalid if we're not in a parameter value + return true; + } + } + } + return false; + } + + private boolean hasProperSpaceBeforePrefix(String input, String prefix) { + int prefixPos = input.lastIndexOf(prefix); + if (prefixPos <= 0) { + return false; + } + + // Get the last space before this prefix + int lastSpacePos = input.lastIndexOf(' ', prefixPos); + if (lastSpacePos == -1) { + return false; // No space found before prefix at all + } + + // Get the text between the last space and this prefix + String textBetween = input.substring(lastSpacePos + 1, prefixPos); + + // Check if there are any valid prefixes in the text between + // This handles cases like "namep/" or "phonee/" + for (String validPrefix : parameterMap.keySet()) { + if (textBetween.contains(validPrefix)) { + return false; + } + } + + return true; + } + private boolean isValidCommandPrefix(String input) { + // Split the input by spaces to isolate the first word (the command keyword) + String[] parts = input.trim().split("\\s+"); + String commandPrefix = parts[0]; + if (parts.length == 1) { + // Show suggestions if commandPrefix exactly matches any command + // or is a valid beginning of a command in the syntax map + for (String command : commandSyntaxMap.keySet()) { + if (command.startsWith(commandPrefix)) { + return true; + } + } + } + + // Check if the commandPrefix exactly matches any valid command in the syntax map + return commandSyntaxMap.containsKey(commandPrefix); + } + + private void handleTextChanged(String input) { + + if (!isValidCommandPrefix(input)) { + suggestionLabel.setVisible(false); + return; + } + if (input.trim().isEmpty()) { + suggestionLabel.setVisible(false); + return; + } + + // Check if the last part of the input contains a slash + String[] parts = input.split("\\s+"); + String lastPart = parts[parts.length - 1]; + + // For just the command (before any parameters) + if (!input.contains(" ")) { + StringBuilder suggestions = new StringBuilder(); + boolean foundMatch = false; + + List commands = new ArrayList<>(commandSyntaxMap.keySet()); + Collections.sort(commands); + + for (String command : commands) { + if (command.startsWith(input.trim())) { + if (foundMatch) { + suggestions.append(" | "); + } + suggestions.append(commandSyntaxMap.get(command)); + foundMatch = true; + } + } + + if (foundMatch) { + suggestionLabel.setText(suggestions.toString()); + suggestionLabel.setVisible(true); + return; + } + } + + String command = parts[0]; + + // Special handling for commands without slash parameters + if (!commandSyntaxMap.get(command).contains("/")) { + // If just the command is typed + if (input.trim().equals(command)) { + suggestionLabel.setText(commandSyntaxMap.get(command)); + suggestionLabel.setVisible(true); + return; + } + + // For list command with optional sort order + if (command.equals("list") && parts.length == 1) { + suggestionLabel.setText(commandSyntaxMap.get(command)); + suggestionLabel.setVisible(true); + return; + } + + // Hide suggestions once parameters are being typed for non-slash commands + suggestionLabel.setVisible(false); + return; + } + + // Regular handling for slash-parameter commands + if (hasInvalidParameterStructure(input)) { + suggestionLabel.setVisible(false); + return; + } + + if (!isValidPreSlashSequence(input)) { + suggestionLabel.setVisible(false); + return; + } + + // Find the last parameter prefix in the input with longest match + String longestPrefix = null; + int lastPrefixPos = -1; + int longestPrefixLength = 0; + String restOfInput = ""; + + for (String prefix : parameterMap.keySet()) { + int pos = input.lastIndexOf(prefix); + if (pos > -1 && hasProperSpaceBeforePrefix(input, prefix) && prefix.length() > longestPrefixLength) { + lastPrefixPos = pos; + longestPrefix = prefix; + longestPrefixLength = prefix.length(); + restOfInput = input.substring(pos + prefix.length()); + } + } + + // We just typed the command + if (input.trim().equals(command)) { + suggestionLabel.setText(commandSyntaxMap.get(command)); + suggestionLabel.setVisible(true); + return; + } + + // If no valid prefix with proper spacing found, hide suggestion + if (longestPrefix == null) { + suggestionLabel.setVisible(false); + return; + } + + // If we just typed a properly spaced prefix OR if last part contains a slash + if (restOfInput.trim().isEmpty() || lastPart.contains("/")) { + String paramValue; + String prefix; + String beforePrefix; + + if (lastPart.contains("/")) { + // For parts that contain a slash + prefix = lastPart.substring(0, lastPart.indexOf('/') + 1); + String valueAfterSlash = lastPart.substring(lastPart.indexOf('/') + 1); + + // If typing after slash, hide suggestion unless followed by space + if (!valueAfterSlash.isEmpty()) { + if (input.endsWith(" ") && !valueAfterSlash.endsWith(" ")) { + // Show next parameter suggestion when space is hit + String nextParam = getNextParameter(command, input.trim()); + if (nextParam != null) { + suggestionLabel.setText(input.trim() + " " + nextParam); + suggestionLabel.setVisible(true); + return; + } + } + suggestionLabel.setVisible(false); + return; + } + + paramValue = parameterMap.get(prefix).substring(prefix.length()); + beforePrefix = input.substring(0, input.lastIndexOf(lastPart)); + } else { + // For just typed prefix + prefix = longestPrefix; + paramValue = parameterMap.get(longestPrefix).substring(longestPrefix.length()); + beforePrefix = input.substring(0, lastPrefixPos); + } + + suggestionLabel.setText(beforePrefix + prefix + paramValue); + suggestionLabel.setVisible(true); + return; + } + + // If we just pressed space after completing a value + if (input.endsWith(" ")) { + String nextParam = getNextParameter(command, input.trim()); + if (nextParam != null) { + suggestionLabel.setText(input.trim() + " " + nextParam); + suggestionLabel.setVisible(true); + return; + } + } + + suggestionLabel.setVisible(false); + } + @FXML private void handleCommandEntered() { String commandText = commandTextField.getText(); - if (commandText.equals("")) { + if (commandText.isEmpty()) { return; } try { commandExecutor.execute(commandText); commandTextField.setText(""); + suggestionLabel.setVisible(false); } catch (CommandException | ParseException e) { setStyleToIndicateCommandFailure(); } } - /** - * Sets the command box style to use the default style. - */ - private void setStyleToDefault() { - commandTextField.getStyleClass().remove(ERROR_STYLE_CLASS); - } - /** - * Sets the command box style to indicate a failed command. - */ private void setStyleToIndicateCommandFailure() { - ObservableList styleClass = commandTextField.getStyleClass(); - - if (styleClass.contains(ERROR_STYLE_CLASS)) { - return; + if (!commandTextField.getStyleClass().contains(ERROR_STYLE_CLASS)) { + commandTextField.getStyleClass().add(ERROR_STYLE_CLASS); } - - styleClass.add(ERROR_STYLE_CLASS); } /** - * Represents a function that can execute commands. + * Functional interface for command execution in the CommandBox. + * Implementations should handle parsing and execution of command text entered by users. + * Commands can throw CommandException for execution errors or ParseException for invalid syntax. */ @FunctionalInterface public interface CommandExecutor { - /** - * Executes the command and returns the result. - * - * @see seedu.address.logic.Logic#execute(String) - */ - CommandResult execute(String commandText) throws CommandException, ParseException; + void execute(String commandText) throws CommandException, ParseException; } - } diff --git a/src/main/java/seedu/address/ui/EmergencyContactCard.java b/src/main/java/seedu/address/ui/EmergencyContactCard.java new file mode 100644 index 00000000000..a674292be52 --- /dev/null +++ b/src/main/java/seedu/address/ui/EmergencyContactCard.java @@ -0,0 +1,50 @@ +package seedu.address.ui; + +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Region; +import seedu.address.model.person.EmergencyContact; + +/** + * An UI component that displays information of a {@code Person}. + */ +public class EmergencyContactCard extends UiPart { + + private static final String FXML = "EmergencyContactListCard.fxml"; + + /** + * Note: Certain keywords such as "location" and "resources" are reserved keywords in JavaFX. + * As a consequence, UI elements' variable names cannot be set to such keywords + * or an exception will be thrown by JavaFX during runtime. + * + * @see The issue on AddressBook level 4 + */ + + public final EmergencyContact emergencyContact; + + @FXML + private HBox cardPane; + @FXML + private Label emergencyContactName; + @FXML + private Label id; + @FXML + private Label phone; + @FXML + private Label emergencyContactPhone; + @FXML + private Label emergencyContactRelationship; + + /** + * Creates a {@code PersonCode} with the given {@code Person} and index to display. + */ + public EmergencyContactCard(EmergencyContact emergencyContact, int displayedIndex) { + super(FXML); + this.emergencyContact = emergencyContact; + id.setText(displayedIndex + ". "); + emergencyContactName.setText(emergencyContact.getName().fullName); + emergencyContactPhone.setText(emergencyContact.getPhone().value); + emergencyContactRelationship.setText(emergencyContact.getRelationship().relationship); + } +} diff --git a/src/main/java/seedu/address/ui/EmergencyContactListPanel.java b/src/main/java/seedu/address/ui/EmergencyContactListPanel.java new file mode 100644 index 00000000000..0c8f30b900b --- /dev/null +++ b/src/main/java/seedu/address/ui/EmergencyContactListPanel.java @@ -0,0 +1,53 @@ +package seedu.address.ui; + +import java.util.logging.Logger; + +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.layout.Region; +import seedu.address.commons.core.LogsCenter; +import seedu.address.model.person.EmergencyContact; + +/** + * Panel containing the list of emergency contacts. + */ +public class EmergencyContactListPanel extends UiPart { + private static final String FXML = "EmergencyContactListPanel.fxml"; + private final Logger logger = LogsCenter.getLogger(EmergencyContactListPanel.class); + + @FXML + private ListView emergencyContactListView; + + /** + * Creates a {@code EmergencyContactListPanel} with the given {@code ObservableList}. + */ + public EmergencyContactListPanel(ObservableList emergencyContacts) { + super(FXML); + emergencyContactListView.setItems(emergencyContacts); + emergencyContactListView.setCellFactory(listView -> new EmergencyContactListViewCell()); + } + + public ListView getEmergencyContactListView() { + return emergencyContactListView; + } + + /** + * Custom {@code ListCell} that displays the graphics of a {@code EmergencyContact} + * using a {@code EmergencyContactCard}. + */ + class EmergencyContactListViewCell extends ListCell { + @Override + protected void updateItem(EmergencyContact emergencyContact, boolean empty) { + super.updateItem(emergencyContact, empty); + + if (empty || emergencyContact == null) { + setGraphic(null); + setText(null); + } else { + setGraphic(new EmergencyContactCard(emergencyContact, getIndex() + 1).getRoot()); + } + } + } +} diff --git a/src/main/java/seedu/address/ui/EmergencyContactSelectionController.java b/src/main/java/seedu/address/ui/EmergencyContactSelectionController.java new file mode 100644 index 00000000000..b541a1af545 --- /dev/null +++ b/src/main/java/seedu/address/ui/EmergencyContactSelectionController.java @@ -0,0 +1,62 @@ +package seedu.address.ui; +import java.util.ArrayList; +import java.util.List; + +import javafx.scene.control.ListView; +import javafx.scene.control.SelectionMode; +import seedu.address.model.person.EmergencyContact; + +/** + * Manages selection behavior across multiple {@code ListView} instances + * to ensure only one {@code EmergencyContact} is selected at a time. + */ +public class EmergencyContactSelectionController { + private final List> emergencyContactListViews = new ArrayList<>(); + + /** + * Adds a {@code ListView} to the controller and sets up + * its selection listener to manage exclusive selection across all registered + * {@code ListView} instances. + * + * @param emergencyContactListView The {@code ListView} to add. + */ + public void addEmergencyContactListView(ListView emergencyContactListView) { + emergencyContactListViews.add(emergencyContactListView); + setupEmergencyContactSelectionListener(emergencyContactListView); + } + + /** + * Removes a {@code ListView} from the controller + * + * @param emergencyContactListView The {@code ListView} to remove. + */ + public void removeEmergencyContactListView(ListView emergencyContactListView) { + emergencyContactListViews.remove(emergencyContactListView); + } + + /** + * Sets up a selection listener on a given {@code ListView} to clear + * selections in other {@code ListView} instances when an item + * is selected in this list view. + * + * @param emergencyContactListView The {@code ListView} to set up the listener for. + */ + private void setupEmergencyContactSelectionListener(ListView emergencyContactListView) { + emergencyContactListView.getSelectionModel().setSelectionMode(SelectionMode.SINGLE); + + emergencyContactListView.getSelectionModel().selectedItemProperty().addListener((obs, oldVal, newVal) -> { + if (newVal != null) { + // Clear selection in other ListViews + clearListViewSelections(emergencyContactListView); + } + }); + } + + private void clearListViewSelections(ListView emergencyContactListView) { + for (ListView otherEmergencyContactListView : emergencyContactListViews) { + if (otherEmergencyContactListView != emergencyContactListView) { + otherEmergencyContactListView.getSelectionModel().clearSelection(); + } + } + } +} diff --git a/src/main/java/seedu/address/ui/HelpWindow.java b/src/main/java/seedu/address/ui/HelpWindow.java index 3f16b2fcf26..c0daf069405 100644 --- a/src/main/java/seedu/address/ui/HelpWindow.java +++ b/src/main/java/seedu/address/ui/HelpWindow.java @@ -15,7 +15,7 @@ */ public class HelpWindow extends UiPart { - public static final String USERGUIDE_URL = "https://se-education.org/addressbook-level3/UserGuide.html"; + public static final String USERGUIDE_URL = "https://ay2425s1-cs2103t-t13-1.github.io/tp/UserGuide.html"; public static final String HELP_MESSAGE = "Refer to the user guide: " + USERGUIDE_URL; private static final Logger logger = LogsCenter.getLogger(HelpWindow.class); diff --git a/src/main/java/seedu/address/ui/PersonCard.java b/src/main/java/seedu/address/ui/PersonCard.java index 094c42cda82..f71b6dbc71b 100644 --- a/src/main/java/seedu/address/ui/PersonCard.java +++ b/src/main/java/seedu/address/ui/PersonCard.java @@ -2,11 +2,19 @@ import java.util.Comparator; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; +import javafx.collections.FXCollections; import javafx.fxml.FXML; import javafx.scene.control.Label; +import javafx.scene.control.ListView; +import javafx.scene.control.MultipleSelectionModel; import javafx.scene.layout.FlowPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; +import seedu.address.model.person.EmergencyContact; import seedu.address.model.person.Person; /** @@ -15,7 +23,8 @@ public class PersonCard extends UiPart { private static final String FXML = "PersonListCard.fxml"; - + private static final double EMERGENCY_CONTACT_LIST_DEFAULT_CARD_HEIGHT = 90; + private static final double EMERGENCY_CONTACT_LIST_DEFAULT_BORDER_SIZE = 1.5; /** * Note: Certain keywords such as "location" and "resources" are reserved keywords in JavaFX. * As a consequence, UI elements' variable names cannot be set to such keywords @@ -25,9 +34,9 @@ public class PersonCard extends UiPart { */ public final Person person; - @FXML private HBox cardPane; + private EmergencyContactListPanel emergencyContactListPanel; @FXML private Label name; @FXML @@ -39,7 +48,19 @@ public class PersonCard extends UiPart { @FXML private Label email; @FXML + private StackPane emergencyContactListPanelPlaceholder; + @FXML + private Label doctorName; + @FXML + private Label doctorPhone; + @FXML + private Label doctorEmail; + @FXML private FlowPane tags; + @FXML + private VBox emergencyContactsBox; + @FXML + private VBox doctorBox; /** * Creates a {@code PersonCode} with the given {@code Person} and index to display. @@ -48,12 +69,87 @@ public PersonCard(Person person, int displayedIndex) { super(FXML); this.person = person; id.setText(displayedIndex + ". "); + setupPersonCardText(); + setupCardColours(displayedIndex); + + emergencyContactListPanel = new EmergencyContactListPanel( + FXCollections.observableArrayList(person.getEmergencyContacts())); + + ListView emergencyContactListView = emergencyContactListPanel.getEmergencyContactListView(); + ChangeListener emergencyContactListener = emergencyContactSelectionListener(); + MultipleSelectionModel emergencyContactSelectionModel = + emergencyContactListView.getSelectionModel(); + emergencyContactSelectionModel.selectedItemProperty().addListener(emergencyContactListener); + + emergencyContactListPanelPlaceholder.getChildren().add(emergencyContactListPanel.getRoot()); + setupEmergencyContactListPanelPlaceholder(); + } + + /** + * Populates the {@code PersonCard} with {@code Person} details. + */ + private void setupPersonCardText() { name.setText(person.getName().fullName); phone.setText(person.getPhone().value); address.setText(person.getAddress().value); email.setText(person.getEmail().value); + doctorName.setText(person.getDoctor().getName().getDoctorName()); + doctorPhone.setText(person.getDoctor().getPhone().value); + doctorEmail.setText(person.getDoctor().getEmail().value); person.getTags().stream() .sorted(Comparator.comparing(tag -> tag.tagName)) .forEach(tag -> tags.getChildren().add(new Label(tag.tagName))); } + + /** + * Sets the background colours of Doctor and Emergency Contact boxes to colours that alternate with + * {@code PersonCard} background colours. + */ + private void setupCardColours(int displayedIndex) { + // Follows syntax of personCard, odd here refers to a zero-indexed list + if (displayedIndex % 2 == 0) { + doctorBox.getStyleClass().add("alternateColourListView-odd"); + emergencyContactsBox.getStyleClass().add("alternateColourListView-odd"); + } else { + doctorBox.getStyleClass().add("alternateColourListView-even"); + emergencyContactsBox.getStyleClass().add("alternateColourListView-even"); + } + } + + /** + * Sets the height constraints of the {@code emergencyContactListPanelPlaceholder} with respect to the number of + * emergency contacts within the panel. + */ + private void setupEmergencyContactListPanelPlaceholder() { + int numEmergencyContacts = person.getEmergencyContacts().size(); + emergencyContactListPanelPlaceholder.setPrefHeight(EMERGENCY_CONTACT_LIST_DEFAULT_CARD_HEIGHT + * numEmergencyContacts); + emergencyContactListPanelPlaceholder.setMaxHeight(EMERGENCY_CONTACT_LIST_DEFAULT_CARD_HEIGHT * 2); + } + + /** + * Changes the height of the {@code emergencyContactListPanelPlaceholder} when the emergency contacts are selected, + * to accommodate increased {@code emergencyContactCard} heights, as a border is applied upon selection. + */ + private ChangeListener emergencyContactSelectionListener() { + return (ObservableValue observableValue, + EmergencyContact previousSelection, + EmergencyContact currentSelection) -> { + int updatedNumEmergencyContacts = person.getEmergencyContacts().size(); + if (currentSelection != null) { + emergencyContactListPanelPlaceholder.setPrefHeight(EMERGENCY_CONTACT_LIST_DEFAULT_CARD_HEIGHT + * updatedNumEmergencyContacts + EMERGENCY_CONTACT_LIST_DEFAULT_BORDER_SIZE * 2); + emergencyContactListPanelPlaceholder.setMaxHeight(EMERGENCY_CONTACT_LIST_DEFAULT_CARD_HEIGHT + * 2 + EMERGENCY_CONTACT_LIST_DEFAULT_BORDER_SIZE * 2); + } else { + emergencyContactListPanelPlaceholder.setPrefHeight(EMERGENCY_CONTACT_LIST_DEFAULT_CARD_HEIGHT + * updatedNumEmergencyContacts); + emergencyContactListPanelPlaceholder.setMaxHeight(EMERGENCY_CONTACT_LIST_DEFAULT_CARD_HEIGHT * 2); + } + }; + } + + public ListView getEmergencyContactListView() { + return emergencyContactListPanel.getEmergencyContactListView(); + } } diff --git a/src/main/java/seedu/address/ui/PersonListPanel.java b/src/main/java/seedu/address/ui/PersonListPanel.java index f4c501a897b..3aaff8150fb 100644 --- a/src/main/java/seedu/address/ui/PersonListPanel.java +++ b/src/main/java/seedu/address/ui/PersonListPanel.java @@ -16,7 +16,7 @@ public class PersonListPanel extends UiPart { private static final String FXML = "PersonListPanel.fxml"; private final Logger logger = LogsCenter.getLogger(PersonListPanel.class); - + private final EmergencyContactSelectionController emergencyContactSelectionController; @FXML private ListView personListView; @@ -25,6 +25,7 @@ public class PersonListPanel extends UiPart { */ public PersonListPanel(ObservableList personList) { super(FXML); + emergencyContactSelectionController = new EmergencyContactSelectionController(); personListView.setItems(personList); personListView.setCellFactory(listView -> new PersonListViewCell()); } @@ -33,15 +34,24 @@ public PersonListPanel(ObservableList personList) { * Custom {@code ListCell} that displays the graphics of a {@code Person} using a {@code PersonCard}. */ class PersonListViewCell extends ListCell { + private PersonCard personCard; @Override protected void updateItem(Person person, boolean empty) { super.updateItem(person, empty); if (empty || person == null) { + if (personCard != null) { + emergencyContactSelectionController.removeEmergencyContactListView( + personCard.getEmergencyContactListView()); + } setGraphic(null); setText(null); + personCard = null; } else { - setGraphic(new PersonCard(person, getIndex() + 1).getRoot()); + personCard = new PersonCard(person, getIndex() + 1); + setGraphic(personCard.getRoot()); + emergencyContactSelectionController.addEmergencyContactListView( + personCard.getEmergencyContactListView()); } } } diff --git a/src/main/java/seedu/address/ui/UiManager.java b/src/main/java/seedu/address/ui/UiManager.java index fdf024138bc..2c3b993ac19 100644 --- a/src/main/java/seedu/address/ui/UiManager.java +++ b/src/main/java/seedu/address/ui/UiManager.java @@ -20,7 +20,7 @@ public class UiManager implements Ui { public static final String ALERT_DIALOG_PANE_FIELD_ID = "alertDialogPane"; private static final Logger logger = LogsCenter.getLogger(UiManager.class); - private static final String ICON_APPLICATION = "/images/address_book_32.png"; + private static final String ICON_APPLICATION = "/images/health-report.png"; private Logic logic; private MainWindow mainWindow; @@ -32,6 +32,26 @@ public UiManager(Logic logic) { this.logic = logic; } + /** + * Shows an alert dialog on {@code owner} with the given parameters. + * This method only returns after the user has closed the alert dialog. + */ + private static void showAlertDialogAndWait(Stage owner, AlertType type, String title, String headerText, + String contentText) { + final Alert alert = new Alert(type); + alert.getDialogPane().getStylesheets().add("view/DarkTheme.css"); + alert.initOwner(owner); + alert.setTitle(title); + alert.setHeaderText(headerText); + alert.setContentText(contentText); + alert.getDialogPane().setId(ALERT_DIALOG_PANE_FIELD_ID); + alert.showAndWait(); + } + + void showAlertDialogAndWait(Alert.AlertType type, String title, String headerText, String contentText) { + showAlertDialogAndWait(mainWindow.getPrimaryStage(), type, title, headerText, contentText); + } + @Override public void start(Stage primaryStage) { logger.info("Starting UI..."); @@ -54,26 +74,6 @@ private Image getImage(String imagePath) { return new Image(MainApp.class.getResourceAsStream(imagePath)); } - void showAlertDialogAndWait(Alert.AlertType type, String title, String headerText, String contentText) { - showAlertDialogAndWait(mainWindow.getPrimaryStage(), type, title, headerText, contentText); - } - - /** - * Shows an alert dialog on {@code owner} with the given parameters. - * This method only returns after the user has closed the alert dialog. - */ - private static void showAlertDialogAndWait(Stage owner, AlertType type, String title, String headerText, - String contentText) { - final Alert alert = new Alert(type); - alert.getDialogPane().getStylesheets().add("view/DarkTheme.css"); - alert.initOwner(owner); - alert.setTitle(title); - alert.setHeaderText(headerText); - alert.setContentText(contentText); - alert.getDialogPane().setId(ALERT_DIALOG_PANE_FIELD_ID); - alert.showAndWait(); - } - /** * Shows an error alert dialog with {@code title} and error message, {@code e}, * and exits the application after the user has closed the alert dialog. diff --git a/src/main/resources/images/address.png b/src/main/resources/images/address.png new file mode 100644 index 00000000000..49aa15839db Binary files /dev/null and b/src/main/resources/images/address.png differ diff --git a/src/main/resources/images/doctor.png b/src/main/resources/images/doctor.png new file mode 100644 index 00000000000..1ad945e7839 Binary files /dev/null and b/src/main/resources/images/doctor.png differ diff --git a/src/main/resources/images/health-report.png b/src/main/resources/images/health-report.png new file mode 100644 index 00000000000..4b839aba17e Binary files /dev/null and b/src/main/resources/images/health-report.png differ diff --git a/src/main/resources/images/mail.png b/src/main/resources/images/mail.png new file mode 100644 index 00000000000..92814c10247 Binary files /dev/null and b/src/main/resources/images/mail.png differ diff --git a/src/main/resources/images/phone.png b/src/main/resources/images/phone.png new file mode 100644 index 00000000000..7dd9d60da22 Binary files /dev/null and b/src/main/resources/images/phone.png differ diff --git a/src/main/resources/images/relationship.png b/src/main/resources/images/relationship.png new file mode 100644 index 00000000000..ae84498658f Binary files /dev/null and b/src/main/resources/images/relationship.png differ diff --git a/src/main/resources/view/CommandBox.fxml b/src/main/resources/view/CommandBox.fxml index 124283a392e..9478b66e6cd 100644 --- a/src/main/resources/view/CommandBox.fxml +++ b/src/main/resources/view/CommandBox.fxml @@ -1,9 +1,17 @@ + - - + + + + diff --git a/src/main/resources/view/DarkTheme.css b/src/main/resources/view/DarkTheme.css index 36e6b001cd8..8332d5743ab 100644 --- a/src/main/resources/view/DarkTheme.css +++ b/src/main/resources/view/DarkTheme.css @@ -93,43 +93,86 @@ -fx-background-color: derive(#1d1d1d, 20%); } -.list-cell { +#personListPanel .list-cell { -fx-label-padding: 0 0 0 0; -fx-graphic-text-gap : 0; -fx-padding: 0 0 0 0; } -.list-cell:filled:even { +#personListView .list-cell:filled:even { -fx-background-color: #3c3e3f; } -.list-cell:filled:odd { +#personListView .list-cell:filled:odd { -fx-background-color: #515658; } -.list-cell:filled:selected { +#personListView .list-cell:filled:selected { -fx-background-color: #424d5f; } -.list-cell:filled:selected #cardPane { +#personListView .list-cell:filled:selected #cardPane { -fx-border-color: #3e7b91; -fx-border-width: 1; } -.list-cell .label { +.alternateColourListView-odd { + -fx-background-color: #3c3e3f; +} + +.alternateColourListView-even { + -fx-background-color: #515658; +} + +#emergencyContactListView .list-cell { + -fx-label-padding: 0 0 0 0; + -fx-graphic-text-gap : 0; + -fx-padding: 0 0 0 0; +} + +#emergencyContactListView .list-cell:filled:even { + -fx-background-color: #50879B; +} + +#emergencyContactListView .list-cell:filled:odd { + -fx-background-color: #6E9CAC; +} + +#emergencyContactListView .list-cell:filled:selected { + -fx-background-color: #92B4C1; +} + +#emergencyContactListView .list-cell:filled:selected #cardPane { + -fx-border-color: white; + -fx-border-width: 1.5; +} + +#emergencyContactListView .list-cell .label { -fx-text-fill: white; } .cell_big_label { -fx-font-family: "Segoe UI Semibold"; -fx-font-size: 16px; - -fx-text-fill: #010504; + -fx-text-fill: white; } .cell_small_label { -fx-font-family: "Segoe UI"; -fx-font-size: 13px; - -fx-text-fill: #010504; + -fx-text-fill: white; +} + +.emergencyContact { + -fx-font-family: "Segoe UI"; + -fx-font-size: 13px; + -fx-text-fill: #9999ff; +} + +.doctor { + -fx-font-family: "Segoe UI"; + -fx-font-size: 13px; + -fx-text-fill: #4ac6ff; } .stack-pane { diff --git a/src/main/resources/view/EmergencyContactListCard.fxml b/src/main/resources/view/EmergencyContactListCard.fxml new file mode 100644 index 00000000000..2cd2f6af17f --- /dev/null +++ b/src/main/resources/view/EmergencyContactListCard.fxml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/EmergencyContactListPanel.fxml b/src/main/resources/view/EmergencyContactListPanel.fxml new file mode 100644 index 00000000000..7aa1171bd85 --- /dev/null +++ b/src/main/resources/view/EmergencyContactListPanel.fxml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/main/resources/view/MainWindow.fxml b/src/main/resources/view/MainWindow.fxml index 7778f666a0a..05df3b0c184 100644 --- a/src/main/resources/view/MainWindow.fxml +++ b/src/main/resources/view/MainWindow.fxml @@ -11,10 +11,10 @@ - + - + @@ -39,19 +39,21 @@ - - - - - + + + + + + - - - - - - + + + + + + + diff --git a/src/main/resources/view/PersonListCard.fxml b/src/main/resources/view/PersonListCard.fxml index 84e09833a87..60d4758b61e 100644 --- a/src/main/resources/view/PersonListCard.fxml +++ b/src/main/resources/view/PersonListCard.fxml @@ -6,10 +6,13 @@ + + + - + @@ -28,9 +31,71 @@ - diff --git a/src/main/resources/view/ResultDisplay.fxml b/src/main/resources/view/ResultDisplay.fxml index 01b691792a9..6044fe878ee 100644 --- a/src/main/resources/view/ResultDisplay.fxml +++ b/src/main/resources/view/ResultDisplay.fxml @@ -5,5 +5,5 @@ -