diff --git a/README.md b/README.md index 13f5c77403f..937e3af8724 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,12 @@ -[![CI Status](https://github.com/se-edu/addressbook-level3/workflows/Java%20CI/badge.svg)](https://github.com/se-edu/addressbook-level3/actions) +[![CI Status](https://github.com/se-edu/addressbook-level3/workflows/Java%20CI/badge.svg)](https://github.com/AY2324S1-CS2103T-T13-4/tp/actions) ![Ui](docs/images/Ui.png) -* This is **a sample project for Software Engineering (SE) students**.
+* This is **an application for managing animal foster families in non-profit animal shelters** which currently do not have a good logistical workflow to do so.
+* Target user: Foster manager of the animal shelter +* 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#https://se-education.org/#contributing) for more info. + * as a streamlined workflow to **manage list of fosterers** + * provides an **intuitive** way to manage fosterers' details, such as their **availability** and **animals they can foster** +* For the detailed documentation of this project, see the **[Foster Family Product Website](https://ay2324s1-cs2103t-t13-4.github.io/tp/)**. +* This project is based on the AddressBook-Level3 project created by the [SE-EDU initiative](https://se-education.org). diff --git a/build.gradle b/build.gradle index a2951cc709e..2bd06be462c 100644 --- a/build.gradle +++ b/build.gradle @@ -20,6 +20,10 @@ checkstyle { toolVersion = '10.2' } +run { + enableAssertions = true +} + test { useJUnitPlatform() finalizedBy jacocoTestReport @@ -66,7 +70,7 @@ dependencies { } shadowJar { - archiveFileName = 'addressbook.jar' + archiveFileName = 'FosterFamily.jar' } defaultTasks 'clean', 'test' diff --git a/docs/AboutUs.md b/docs/AboutUs.md index 1c9514e966a..d501ffc3b62 100644 --- a/docs/AboutUs.md +++ b/docs/AboutUs.md @@ -9,51 +9,47 @@ You can reach us at the email `seer[at]comp.nus.edu.sg` ## Project team -### John Doe +### Nicole Chiong - + -[[homepage](http://www.comp.nus.edu.sg/~damithch)] -[[github](https://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](https://github.com/butteredyakiimo)] +[[portfolio](team/butteredyakiimo.md)] -* Role: Project Advisor +* Role: Deliverables and Deadlines, Code Quality -### Jane Doe +### Nanette Tan - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](https://github.com/nananakx-x)] +[[portfolio](team/nananakx-x.md)] -* Role: Team Lead -* Responsibilities: UI +* Role: Testing -### Johnny Doe +### Sia Zhi Hong - + -[[github](http://github.com/johndoe)] [[portfolio](team/johndoe.md)] +[[github](http://github.com/h1410101)] +[[portfolio](team/h1410101.md)] -* Role: Developer -* Responsibilities: Data +* Role: Integration -### Jean Doe +### Nam Dohyun (Brandon) - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](http://github.com/brandon-nam)] +[[portfolio](team/brandon-nam.md)] -* Role: Developer -* Responsibilities: Dev Ops + Threading +* Role: Scheduling and Tracking -### James Doe +### Tyrus Lye - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](https://github.com/TyrusLye)] +[[portfolio](team/tyruslye.md)] -* Role: Developer -* Responsibilities: UI +* Role: Documentation diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 8a861859bfd..86349a4739e 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -7,9 +7,12 @@ 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} +## **Acknowledgements** +* [File icon](https://www.flaticon.com/free-icon/document_2258853?term=file&page=1&position=6&origin=search&related_id=2258853) and [Help icon](https://www.flaticon.com/free-icon/question_471664?term=help&page=1&position=2&origin=search&related_id=471664) used in the main window are from Flaticon. +* Useful notations in the User Guide was inspired from a past project [TaskBook](https://ay2223s1-cs2103t-t13-4.github.io/tp/UserGuide.html#useful-notations). +* Technical Terms in the User Guide was inspired from a past project [Sellah](https://ay2122s1-cs2103t-t12-1.github.io/tp/UserGuide.html#321-technical-terms). -------------------------------------------------------------------------------------------------------------------- @@ -19,11 +22,14 @@ Refer to the guide [_Setting up and getting started_](SettingUp.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 `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.
### Architecture @@ -36,7 +42,7 @@ Given below is a quick overview of main components and how they interact with ea **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/AY2324S1-CS2103T-T13-4/tp/blob/master/src/main/java/seedu/address/Main.java) and [`MainApp`](https://github.com/AY2324S1-CS2103T-T13-4/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. @@ -68,13 +74,13 @@ 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/AY2324S1-CS2103T-T13-4/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 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. Notably, Profile is a separate set of classes that handle the UI of the `view` command, which will be covered later in this document. -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/AY2324S1-CS2103T-T13-4/tp/blob/master/src/main/java/seedu/address/ui/MainWindow.java) is specified in [`MainWindow.fxml`](https://github.com/AY2324S1-CS2103T-T13-4/tp/blob/master/src/main/resources/view/MainWindow.fxml) The `UI` component, @@ -85,11 +91,11 @@ The `UI` component, ### 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/AY2324S1-CS2103T-T13-4/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. @@ -98,26 +104,54 @@ The sequence diagram below illustrates the interactions within the `Logic` compo
:information_source: **Note:** The lifeline for `DeleteCommandParser` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram.
-How the `Logic` component works: +#### AddressBookParser and ViewModeParser Classes + +`LogicManager` class utilizes either `AddressBookParser` or `ViewModeParser` depending on whether the user is seeing the main window or a fosterer's profile. + +Given below is a sequence diagram that explains how `LogicManager` class chooses which parser class to use: + +![isInViewModeSequenceDiagram](images/IsInViewModeSequenceDiagram.png) + +As the diagram suggests, the `executeInView()` method is used when personListPanelPlaceHolder UI element - the placeholder that contains the normal fosterer list - is invisible, which means the user sees the profile page. This triggers the `ViewModeParser` instance in `LogicManager` class to be used to parse the command. If the placeholder is visible, it means the user is seeing the main window, in which case the `execute()` method is used and the commands the user enters are parsed by `AddressBookParser`. + +The reason for creating two separate parser classes is to provide mutual exclusion between the commands available in main window and in a profile page. For example, `SaveCommand` should only be executed in the context of editing a fosterer's detail in profile page, not in main window. + +
+ +**How the `Logic` component works** + +Here is a step-by-step explanation of how the `Logic` component works when it uses `AddressBookParser`: 1. When `Logic` is called upon to execute a command, it is passed to an `AddressBookParser` object which in turn creates a parser that matches the command (e.g., `DeleteCommandParser`) and uses it to parse the command. 1. This results in a `Command` object (more precisely, an object of one of its subclasses e.g., `DeleteCommand`) which is executed by the `LogicManager`. 1. The command can communicate with the `Model` when it is executed (e.g. to delete a person). +1. The result of the command execution is encapsulated as a `CommandResult` object which is returned back from `Logic` to `UI`. + +
+ +Here is a step-by-step explanation of how the `Logic` component works when it uses `ViewModeParser`: + +1. When `Logic` is called upon to execute a command, it is passed to an `ViewModeParser` object which in turn creates a `Command` object (e.g., `SaveCommand`). +1. The command is then executed by the `LogicManager`, communicating with the `Model` when it is executed (e.g. to save a person). 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: - + + +
+ +**How the parsing works in `AddressBookParser`** -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) +
- +### Model component +**API** : [`Model.java`](https://github.com/AY2324S1-CS2103T-T13-4/tp/blob/master/src/main/java/seedu/address/model/Model.java) + The `Model` component, @@ -126,16 +160,20 @@ 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.
+Multiplicities and other navigabilities are omitted from diagram for simplicity. +
### 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/AY2324S1-CS2103T-T13-4/tp/blob/master/src/main/java/seedu/address/storage/Storage.java) @@ -146,101 +184,698 @@ The `Storage` component, ### Common classes -Classes used by multiple components are in the `seedu.addressbook.commons` package. +Classes used by multiple components are in the [commons](https://github.com/AY2324S1-CS2103T-T13-4/tp/tree/master/src/main/java/seedu/address/commons) package. -------------------------------------------------------------------------------------------------------------------- -## **Implementation** +
+ +## **Selected Implementation** This section describes some noteworthy details on how certain features are implemented. -### \[Proposed\] Undo/redo feature +### View feature + +Called `PersonProfile` or simply "profile" internally. The profile follow this structure: + +![Structure of Profile Component](images/UiProfileDiagram.png) + +Except notes and tags, all other fosterer details are handled using dynamically created copies of `PersonProfileField`. + +- `PersonProfileHeader` serves as headers for the data, and cannot be edited by the user. +- `PersonProfileField` handles a field key and a field value, where the value can be edited. +- `PersonProfileNote` is similar to a field, except it contains a `TextArea` and supports multiline input. +- `PersonProfileTags` also supports multiline input, but when in read mode displays tags in a `FlowPane`. + +The following is a sequence diagram that shows two sequential processes: the user starting an edit in a field, +and the user successfully completing that edit. The alternative paths to this process are numerous, and covering them exhaustively +is likely not productive. Instead, this represents the "success path" of a successful edit, where nearly every step handles potential failure. + +Step 1. The user begins by entering "mail" in the command box of the main UI. -#### Proposed Implementation +Step 2. The `MainWindow` finds the correct field that corresponds to the user input, and tells `PersonProfile`. -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: +Step 3. `PersonProfile` locates the relevant `PersonProfileField`, and forwards the request for focus. -* `VersionedAddressBook#commit()` — Saves the current address book state in its history. -* `VersionedAddressBook#undo()` — Restores the previous address book state from its history. -* `VersionedAddressBook#redo()` — Restores a previously undone address book state from its history. +Step 4. The user's cursor now jumps to the relevant `PersonProfileField`, and thus their next action is handled directly from `PersonProfileField`. -These operations are exposed in the `Model` interface as `Model#commitAddressBook()`, `Model#undoAddressBook()` and `Model#redoAddressBook()` respectively. +Step 5. The user presses the `enter` key, which alongside the `esc` key, are special keys involved in the confirmation or cancellation of the edit. -Given below is an example usage scenario and how the undo/redo mechanism behaves at each step. +Step 6. `PersonProfileField` checks that the entered information is valid for that particular field. -Step 1. The user launches the application for the first time. The `VersionedAddressBook` will be initialized with the initial address book state, and the `currentStatePointer` pointing to that single address book state. +Step 7. `PersonProfileField` then updates the `PersonProfile` about the change. -![UndoRedoState0](images/UndoRedoState0.png) +Step 8. `PersonProfileField` triggers the event `AFTER_CONFIRM` because the user started a confirmation. -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. +Step 9. One of the event handlers listening to the `AFTER_CONFIRM` event is `handleFieldLockIn`, which initiates the next two steps. -![UndoRedoState1](images/UndoRedoState1.png) +Step 10. `handleFieldLockIn` checks that the new `Person` object described by the fields is valid, and creates one. -Step 3. The user executes `add n/David …​` to add a new person. The `add` command also calls `Model#commitAddressBook()`, causing another modified address book state to be saved into the `addressBookStateList`. +Step 11. `handleFieldLockIn` then asks the `MainWindow` to tell the user that the `Person` is created. -![UndoRedoState2](images/UndoRedoState2.png) -
:information_source: **Note:** If a command fails its execution, it will not call `Model#commitAddressBook()`, so the address book state will not be saved into the `addressBookStateList`. +Further details on the `MainWindow` side are omitted in this explanation and diagram. + +![Sequence of PersonProfile](images/ProfileEditSequenceDiagram.png) + + +### Add feature + +#### Implementation + +The add mechanism allows users to add new fosterers to the address book. This feature is facilitated by the `AddCommand` and `AddCommandParser` classes, to handle user input and create the appropriate `Person` object. Specifically, the feature is implemented through the following components and operations: + +* `AddCommand` — The core component responsible for executing the addition of a new fosterer to the address book. It handles the validation of input fields and ensures there are no duplicate persons in the address book. +* `Person` — Represents the structure of a person, including attributes such as name, phone, email, address, housing, availability, animal name, animal type, and associated tags. +* `ParserUtil` and `AddCommandParser` — Contains parsing methods for various input fields (e.g., name, phone, email, etc.) to ensure they are valid by meeting specific requirements and conditions. +* `ArgumentMultimap` — Tokenizes and manages command arguments. + +Given below is an example usage scenario and how the add mechanism behaves at each step. To make the sequence diagram for adding a fosterer more +readable, the following replacements for the lengthy add command format are used: + +1. `add n/Pete Tay p/98765411 e/pete@example.com a/Happy street block 5 housing/Condo availability/Available animal/nil animalType/able.Cat` +is replaced with `add command`. +2. `n/Pete Tay p/98765411 e/pete@example.com a/Happy street block 5 housing/Condo availability/Available animal/nil animalType/able.Cat` +is replaced with `arguments`. +3. `Pete Tay, 98765411, pete@example.com, Happy street block 5, Condo, Available, nil, able.Cat` is replaced with `attributes`. + +![Interactions Inside the Logic Component for the `add n/Pete Tay p/98765411 e/pete@example.com a/Happy street block 5 housing/Condo availability/Available animal/nil animalType/able.Cat` Command](images/AddSequenceDiagram.png) + +
:information_source: **Note:** The lifeline for `AddCommandParser` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram.
-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. +Step 1. The user enters the `add` command with relevant details for the new fosterer. The `AddCommandParser` is invoked to parse the user's input. + +Step 2. The `AddCommandParser` processes the user's input and verifies the presence of mandatory fields inputted in the correct format (omitted from diagram for simplicity).
+If this check fails, the system will generate a specific error message indicating which field format is invalid. +For example, if the email format is incorrect, the system will report that the email input is invalid. The error message will be displayed to the user, providing clear feedback about the issue and the specific constraints that are not met. + +Step 3. If all mandatory fields are present with the valid format, the new person is created using the `Person` class. The person's details, including their name, phone, email, address, housing, availability, animal name, animal type, and tags, are recorded, and the Person class also ensures that there is no conflicting data (omitted from diagram for simplicity).
+If this check fails, the system will generate a specific error message indicating which field is invalid, and how can it be resolved. + +Step 4. The `Person` is then passed to the new `AddCommand` created, which adds the person to the address book, ensuring that it is not a duplicate of an existing entry. This check is performed in the `execute` method of the `AddCommand`. + +Step 5. A success message is displayed to the user to confirm that the new fosterer has been added to the address book. + +The add feature ensures that user input is correctly parsed and validated, and it prevents duplicate entries in the address book. + +The following activity diagram summarizes what happens when a user executes an add command: + +![Add Command Activity Diagram](images/AddActivityDiagram.png) + +#### Design considerations: + +* **Data Validation** — The add feature performs thorough validation on input data, ensuring that it adheres to constraints for each field. +* **Duplicates** — The system checks for duplicate persons to prevent the addition of redundant data. + +**Aspect: Handling duplicate persons:** + +* **Alternative 1:** Checks for duplicates based on the person's name, which is case-sensitive. + * Pros: Easy to implement, and is simple and effective. + * Cons: May not catch duplicates with different names but similar attributes or similar names in different letter case. + +* **Alternative 2:** Implement a more comprehensive duplicate check considering multiple attributes. + * Pros: Provides better duplicate detection by comparing multiple attributes. + * Cons: May be more complex to implement. + +* **Alternative 3 (current choice):** Implement a more effective duplicate check considering the presence of multiple spaces between different words of the same name, and case-sensitivity of names. + * Pros: Provides better duplicate detection by identifying fosterers with the same name, but inputted in different formats. + * Cons: May be more complex to implement and such cases might be less likely to happen. + +**Aspect: How add executes:** + +* **Alternative 1 (current choice):** Add only one fosterer at a time. + * Pros: Easy to implement. + * Cons: May be time-inefficient and inconvenient if there is an influx of new fosterers to be added. + +* **Alternative 2:** Add multiple fosterers at once. + * Pros: Do not have to parse user input multiple times in order for the user to perform mass addition of new fosterers. + * Cons: The add command would be even more convoluted due to the increase in length with all the fields/arguments required, making the UI less desirable. + +### Editing and Saving the Changes in Profile Page Feature + +#### Handling UI Changes In Profile Page + +While the profile page is opened, `MainWindow` classes checks the `CommandType` Enum value that is carried by the `CommandResult` object which is returned from executing a `Command`. Depending on the types of the commands, `MainWindow` assigns handler methods to handle the corresponding UI changes. + +
+ +The sequence diagram given below illustrates the types of handlers `MainWindow` class deals with. + +![MainWindowCommandTypeSequenceDiagram.png](images/MainWindowCommandTypeSequenceDiagram.png) + +As the diagram suggests, depending on the different types of commands, `MainWindow` class executes handlers corresponding to them. + +
+ +#### Editing A Fosterer In Profile Page + +The mechanism allows the user to edit details of a fosterer in their profile page. This feature is facilitated by `ViewModeParser`, and `EditFieldCommand` classes, to handle user input in the profile page and edit the correct detail of a fosterer. This feature is implemented using the following components and operations: + +* `ViewModeParser` - Represents the parser that parses commands that are executed in a fosterer's profile. +* `EditFieldCommand` - The core component responsible for executing the edit of a fosterer in the address book. +* `MainWindow` - The UI component that handles navigating through fields. +* `CommandType` - The Enum class that represents the type of command which MainWindow checks to handle the UI change. + +Given below is an example usage scenario and how the mechanism behaves at each step, given that the user already opened person profile page: + +Step 1. The user enters the name of the field. e.g. "name". Since the fosterer list is invisible, "name" is passed to `executeInView()` method in `MainWindow` class (refer to the explanation in the section of [**Logic Component:AddressBookParser and ViewModeParser Classes**](#addressbookparser-and-viewmodeparser-classes)). + +![EditFieldSequenceDiagramStep1.png](images/EditFieldSequenceDiagramStep1.png) + +Step 2. With `executeInView()`, `ViewModeParser` is used to parse the command which returns `EditFieldCommand`. + +![EditFieldSequenceDiagramStep2.png](images/EditFieldSequenceDiagramStep2.png) + +Step 3. `EditFieldCommand` is executed, and with the `CommandType.EDIT_FIELD` carried by `CommandResult`, `MainWindow` calls `handleEditField()` method. The interaction between `MainWindow` and `PersonProfile` is covered in detail in **[View Feature](#view-feature)**. + +![EditFieldSequenceDiagram.png](images/EditFieldSequenceDiagram.png) + +
+
+ +#### Saving the Changes in Profile Page Feature + +The mechanism allows the user to save the edited details of a fosterer in their profile page. This feature is facilitated by `ViewModeParser`, and `SaveCommand` classes, to handle user input in the profile page and save the updated fosterer. This feature is implemented using the following components and operations: + +* `ViewModeParser` - Represents the parser that parses commands that are executed in a fosterer's profile. +* `SaveCommand` - The core component responsible for saving the changes made by the user. +* `MainWindow` - The UI component that handles navigating through fields. +* `CommandType` - The Enum class that represents the type of command which MainWindow checks to handle the UI change. + +Given below is an example usage scenario and how the save mechanism behaves at each step, given that the user already opened person profile page: + +Step 1. The user enters `save` command. Since the normal person list is invisible, the command text "save" is passed to `executeInView()` method in `MainWindow` class (refer to the explanation in the section of [**Logic Component:AddressBookParser and ViewModeParser Classes**](#addressbookparser-and-viewmodeparser-classes)). + +![SaveSequenceDiagramStep1.png](images/SaveSequenceDiagramStep1.png) + +Step 2. With `executeInView()`, `ViewModeParser` is used to parse the command text which returns `SaveCommand`. + +![SaveSequenceDiagramStep2.png](images/SaveSequenceDiagramStep2.png) + +
+ +Step 3. `EditFieldCommand` is executed, and `setPerson()` method from `Model` class is called with personToEdit and targetIndex obtained from `MainWindow` class. + +![SaveSequenceDiagram.png](images/SaveSequenceDiagram.png) + +Step 4. From `MainWindow`, `handleSave()` handler method is called which calls `resetValues()` in `PersonProfile` class that updates the field values to the currently saved fosterer's details and change the text color from red, if exists, back to black. + +![HandleSaveSequenceDiagram.png](images/HandleSaveSequenceDiagram.png) + +#### Ensuring the user saved the changes before exiting profile + +The mechanism ensures the user to save the edited details of a fosterer in their profile page by displaying warning message. This feature is facilitated by `ViewModeParser`, and `ViewExitCommand` classes, to handle user input in the profile page and save the updated fosterer. This feature is implemented using the following components and operations: + +* `ViewModeParser` - Represents the parser that parses commands that are executed in a fosterer's profile. +* `SaveCommand` - The core component responsible for saving the changes made by the user. +* `MainWindow` - The UI component that handles navigating through fields. +* `CommandBox` - The UI component that detects either Enter or Esc input to continue exiting or cancel exiting. +* `ResultDisplay` - The UI component that displays the warning message when the user attempts to exit without saving. +* `CommandType` - The Enum class that represents the type of command which MainWindow checks to handle the UI change. + +Given below is an example usage scenario and how this mechanism behaves at each step, given that the user already opened person profile page and did not save the edit: + +Step 1. The user enters `exit` command in the profile page. Since the fosterer list is invisible, the command text "exit" is passed to `executeInView()` method in `MainWindow` class (refer to the explanation in the section of [**Logic Component:AddressBookParser and ViewModeParser Classes**](#addressbookparser-and-viewmodeparser-classes)). + +![ViewExitSequenceDiagram1.png](images/ViewExitSequenceDiagram1.png) + +Step 2. With `executeInView()`, `ViewModeParser` is used to parse the command text which returns `ViewExitCommand`. + +![ViewExitSequenceDiagram2.png](images/ViewExitSequenceDiagram2.png) + +Step 3. `ViewExitCommand` is executed, and `getFilteredPersonList()` method in `Model` is used to compare the edited fosterer and the original fosterer to see if the details are saved. The `CommandResult` is returned to the `MainWindow`. + +![ViewExitSequenceDiagram3.png](images/ViewExitSequenceDiagram3.png) + +Step 4. From `MainWindow`, `handleViewExit()` handler method is called. The `getInConfirmationDialog()` method in `CommandBox` UI class is used to check if the user is seeing the confirmation message. If the user already saved the fosterer or is already in confirmation dialog, `exitProfilePage()` is called and user exits the profile. Otherwise, the user is shown the confirmation message alerting them that they did not save the details. + +![HandleViewExitSequenceDiagram.png](images/HandleViewExitSequenceDiagram.png) + +Step 5. If the user presses Enter in the `CommandBox`, `handleViewExit()` method is called again. This time, the screen is already showing the confirmation message, so isShowingConfirmationMessage is true. Thus, the user is exited out of the profile page. If the user presses Esc instead, `handleCancelViewExit()` method is called and lets the user stays in the profile page. + +![ViewExitSequenceDiagram.png](images/ViewExitSequenceDiagram.png) + +#### Design considerations: + +**Aspect 1: Different `SaveCommand` behaviors for adding and editing a fosterer in profile page:** + +Here are the justifications of why `SaveCommand` exits the profile page when adding a new fosterer but does not when editing a fosterer's details. + +* **Alternative 1 (current choice):** Executing `SaveCommand` exits directly when adding a new fosterer, but does not exit when editing a fosterer on their profile page. + * Pros: Less error-prone. Duplicate fosterers are checked with the names. If the user edits the person's name and enters save again, it would create another fosterer. + * Cons: May be cumbersome for users who want to edit the user's details on the same page. -![UndoRedoState3](images/UndoRedoState3.png) +* **Alternative 2:** Users are able to continue adding details when adding or editing a fosterer. + * Pros: Gives better user experience since user can continue working without exiting the profile page. + * Cons: Harder to implement without making it error prone. -
:information_source: **Note:** If the `currentStatePointer` is at index 0, pointing to the initial AddressBook state, then there are no previous AddressBook states to restore. The `undo` command uses `Model#canUndoAddressBook()` to check if this is the case. If so, it will return an error to the user rather -than attempting to perform the undo. +* **Alternative 3:** `SaveCommand` exits the profile page for both adding and editing a fosterer. + * Pros: Least error-prone and less confusing because of its consistent behavior. + * Cons: Users have to exit and re-enter profile page several times. +**Aspect 2: How displaying confirmation message is implemented:** + +* **Alternative 1 (current choice):** Accepts inputs (Enter or Esc) from `CommandBox` and call `MainWindow` handlers from it. + * Pros: Does not have to create separate command for confirming or cancelling exit. Since the user may type other commands instead of such confirming command, this prevents potential errors that may arise from attempting to execute commands while the message is shown. + * Cons: Increases coupling between `MainWindow` and `CommandBox`. + +* **Alternative 2:** Create another command for confirming or cancelling exit. + * Pros: Easy to implement. Utilizes the current architecture and does not add additional coupling between `MainWindow` and `CommandBox`. + * Cons: May potentially cause errors since users may type in different commands. + +### List feature + +#### Implementation + +The list feature allows users to filter through the address book. Note that internally, `find` and `list` are considered the same command. +For class naming purposes, `ListCommand` displays all individuals, while `FindCommand` parses a search expression. +This naming oddity is due to historical reasons; currently, the user triggers `ListCommand` with `find` (no search expression), and triggers `FindCommand` with `list Tom`. + +In this section, we focus only on `FindCommand`, which is strictly the more complex of the two. Particularly important classes are as follows: + +* `FindCommand` — The core component responsible for applying the filter to the address book. +* `Model` — An object representing all the fosterers in the address book. Required for `FindCommand` to pass a `Predicate` to, to begin filtering. +* `SearchPredicate` — `Predicate` wrapper around other search objects, to be passed to the `Model`. +* `FindCommandParser` and `FindCommandArgumentParser` — Parse user arguments after the `find` keyword. `FindCommandParser` adheres to the same method convention as other `Parsers` for other commands, while calling `FindCommandArgumentParser` to turn the user argument into a `SearchPredicate`. + +The following class diagram shows all classes involved: + +![FindCommandClassDiagram.png](images/FindCommandClassDiagram.png) + +To illustrate how everything works together, we trace the flow of execution as the user searches for `Tom / Sam`. Details involving `MainWindow` and `LogicManager` are ignored, especially since they are common across all commands. + +Step 1. The user enters `list Tom/Sam`. `MainWindow` calls `LogicManager`, which calls `AddressBookParser`, and in turn `FindCommandParser`, which handles the resulting String. + +Step 2. `FindCommandParser` calls `FindCommandArgumentParser`, to turn the string into a `SearchPredicate` containing a `SearchMatcher`. + +Step 3. `FindCommandParser` uses the `SearchPredicate` to initialize a `FindCommand`, and returns it to `AddressBookParser`, which in turn returns to `LogicManager`. + +Step 4. `LogicManager` calls the `execute` function of the command it received. In this case, it received `FindCommand`. It passes a `Model` to the execute function as well. + +Step 5. `FindCommand` calls `updateFilteredPersonList` of the `Model`, which refreshes the currently displayed list of fosterers. + +Step 6. The `list` command stops here, and execution is returned to the `LogicManager`, and then `MainWindow`. The `MainWindow` shows text feedback, together with the updated list of fosterers. + + +The following diagram ignores details for `SearchPredicate`, which will be covered shortly: + +
:information_source: **Note:** Lifelines in the following sequence diagrams should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram. +
+ +![FindCommandSequenceDiagram.png](images/FindCommandSequenceDiagram.png) + + +Let's dive into greater detail for `SearchPredicate`. We look at how the `SearchPredicate` handles a `Person` object: + +![FindSearchPredicateSequenceDiagram.png](images/FindSearchPredicateSequenceDiagram.png) + +To illustrate how `SearchMatcher` works, we consider the object diagram for the command `list John & Doe / Mae`. +These objects are created by `FindCommandArgumentParser` through `parse("John & Doe / Mae)`: + +![FindPredicateExampleObjectDiagram.png](images/FindPredicateExampleObjectDiagram.png) + +When the Search Predicate is applied to a specific Person, the resulting flow of calls is as depicted by the following sequence diagram: + +![FindPredicateExampleSequenceDiagram.png](images/FindPredicateExampleSequenceDiagram.png) + + +#### Design Considerations + +* Expressiveness — The search is required to be sufficiently expressive to handle common everyday tasks with efficient, singular commands. This includes brackets. +* Typing Flow — Where possible, search expressions should support being written from left to right, without having to use the cursor, arrow keys or backspace key to backtrack or rewrite. This is the primary reason the search has two `AND` symbols of different precedences. +* Intuitiveness — Search expressions use symbols `&` and `/`, which are intuitive for the target user. +* Conciseness — Search expressions are as short as possible. This is the primary reason for not including field-specific searches, as that would increase the amount of text users have to type, often for minimal benefit. +* Flexibility — Where possible, search expressions are allowed to be flexible. This includes automatically closing brackets and allowing any number of whitespaces (including none) between expression terms. + +Note: The `Range` return value from `SearchMatcher` is kept for potential future work, such as matches that avoid overlaps, or matches that must adhere to a certain order. Current functionality does not take advantage of `Range`. + +### Delete feature + +#### Implementation + +The delete mechanism allows users to delete fosterers in the address book. This feature is facilitated by the `DeleteCommand`, `DeleteCommandParser` and `Indices` classes, to handle user input and delete the correct fosterers. This feature is implemented using the following components and operations: + +* `DeleteCommand` — The core component responsible for executing the deletion of fosterers in the address book. +* `Indices` — Represents the indices that the user inputs, each index corresponding to a fosterer in the last seen list of fosterers. This class encapsulates one or more `Index` objects. +* `ParserUtil` and `DeleteCommandParser` — Contains the parsing methods for string input of the user. They ensure that the indices are valid by meeting specific requirements. + +Given below is an example usage scenario and how the delete mechanism behaves at each step. The sequence diagram for deleting multiple fosterers is similar to the example in [Logic](#logic-component). + +![Interactions Inside the Logic Component for the `delete 1 2 3` Command](images/DeleteMultipleSequenceDiagram.png) + +
:information_source: **Note:** The lifeline for `DeleteCommandParser` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram.
-The following sequence diagram shows how the undo operation works: +Step 1. The user enters the delete command with at least one index. In this example, the user inputs indices `1 2 3`, which means that they want to delete the first, second and third fosterers in the currently displayed list. The `DeleteCommandParser` is invoked to parse the user's input. + +Step 2. `DeleteCommandParser` will then invoke the `ParserUtil` to check for the validity of indices (invocation of methods in `ParserUtil` and `Indices` classes are omitted from diagram for simplicity). If indices are invalid, the system will generate an error message. The error message will be displayed to the user, providing feedback about the issue and the specific constraints that are not met. + +Step 3. For each valid index, the `DeleteCommand` will execute the deletion by obtaining that fosterer from the list of unique persons in the address book, and then updating the model to remove the fosterer. This is done in the `execute` method of `DeleteCommand`. + +Step 4. A success message is displayed to the user to confirm that the selected fosterers have been deleted from the address book. + +Therefore, by ensuring that the user input indices are correctly parsed and validated, this delete feature allows the user to delete multiple fosterers at once. + +#### Design considerations: + +**Aspect: How delete executes:** + +* **Alternative 1 (current choice):** Delete multiple fosterers at once. + * Pros: Do not have to parse user input multiple times in order for the user to perform mass deletion. + * Cons: Slightly harder to implement. + +* **Alternative 2:** Delete only one fosterer at a time. + * Pros: Easy to implement. + * Cons: Overhead associated with the chain of delete commands should the user choose to perform multiple deletions. + +### Sort feature +#### Implementation + +The Sort feature allows the user to sort the list of fosterers alphabetically by name, to make the address book more organised. +This is facilitated by the `SortCommand`, `Model`, `AddressBook`, and `UniquePersonList` classes: + +* `SortCommand` — This class represents the command to sort the list of persons by name. It calls the `sortByName` method in the `Model` class. +* `Model` and `ModelManager` — Declares and implements the `sortByName` method, specifying a comparator based on the person's name. It calls the `sortNames` method in the `AddressBook` class. +* `AddressBook` — Implements the `sortNames` method which uses `sort` on the `persons` UniquePersonList. +* `UniquePersonList` — Contains the `sort` method to perform the sorting operation by using the method on its `internalList`. -![UndoSequenceDiagram](images/UndoSequenceDiagram.png) +Given below is an example usage scenario and how the sort feature behaves at each step. -
:information_source: **Note:** The lifeline for `UndoCommand` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram. +![Interactions Inside the Logic Component for SortCommand](images/SortSequenceDiagram.png) +Step 1. The user enters the `sort` command. The `SortCommand` is invoked, and it calls the `sortByName` method in the `Model` interface, which is implemented in the `ModelManager` class. + +Step 2. The `sortByName` method creates a comparator based on the name of a person. +It then calls the `sortNames` method in the `AddressBook` class (omitted from diagram for simplicity). + +Step 3. The `sortNames` method initiates the sorting process and sorts the list of persons using the provided comparator. +It calls the `sort` method on the `persons` UniquePersonList (omitted from diagram for simplicity). + +Step 4. The `sort` method in the `UniquePersonList` class performs the actual sorting operation on its `internalList`. + +Step 5. A success message is displayed to the user to confirm that the address book has been sorted alphabetically by the names of the fosterers. + +The sort feature offers the user a choice to ensure that the list of fosterers in the address book is collated neatly and systematically. + +#### Design considerations: + +**Aspect: How sort executes:** + +* **Alternative 1 (current choice):** Sort alphabetically only based on the fosterers' names. + * Pros: Easier to implement. + * Cons: Users may have different preferences for sorting (e.g., sorting by first name, last name, or a combination).. + +* **Alternative 2:** Customisable sorting - Allow the user to choose to sort the list alphabetically based on either the fosterers' names or the names of the animals fostered. + * Pros: Offers greater flexibility for the users to choose which sorting criteria he/she prefers. + * Cons: If two animals have the same name or if both are `nil`, the sorting operation may change their relative order. + +**Aspect: Reverting the sort operation:** + +* **Alternative 1 (current choice):** Implement an `undo` command to revert the list back to its original state (sorted chronologically based on when the fosterer is added). + * Pros: Maintains consistency with undo mechanisms used in other features of Foster Family. + * Cons: If `sort` is executed multiple times consecutively followed by `undo`, the list will not revert back to its original state with the current implementation of `undo`, which only allows the users to undo the last valid command. + +* **Alternative 2:** Implement another `sort` command like `sort time` to revert the list back to its original state (sorted chronologically based on when the fosterer is added). + * Pros: Offers an explicit and clear command for reverting to the original sorting of the address book list. + * Cons: Introduces additional commands, potentially leading to increased cognitive load for the users. + +### Statistics feature +#### Implementation + +The Statistics feature provides a summary of selected address book data to the user. The data we can provide insights to are those of available fosterers, current fosterers and housing type of fosterers. Statistics include _number_ and _percentage_ information.
+This is facilitated by the `StatsCommand`, `StatsAvailCommand`, `StatsCurrentCommand` and `StatsHousingCommand` classes, as well as the `StatsCommandParser` class. The relationship between the Stats command classes are shown below. +![StatsClassDiagram](images/StatsClassDiagram.png) + +* `StatsCommand` — This is an abstract class that contains utility methods used by its subclasses for percentage calculation. +* `StatsAvailCommand` — Contains methods to calculate statistics of available fosterers and the animals they can foster. +* `StatsCurrentCommand` — Contains methods to calculate statistics of current fosterers and the animals they are currently fostering. +* `StatsHousingCommand` — Contains methods to calculate statistics of the different housing types of fosterers. +* `StatsCommandParser` — Contains the parsing methods for string input of the user. It is in charge of parsing the type of statistics requested by the user, and creating the corresponding `StatsCommand`.
+ +Given below is an example usage scenario and how the statistics feature behaves at each step. It shows the execution of a `stats avail` command, which requests for statistics about available fosterers. + +![Interactions Inside the Logic Component for the StatsAvailCommand](images/StatsAvailSequenceDiagram.png) + +
:information_source: **Note:** The lifeline for `StatsCommandParser` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram.
-The `redo` command does the opposite — it calls `Model#redoAddressBook()`, which shifts the `currentStatePointer` once to the right, pointing to the previously undone state, and restores the address book to that state. +Step 1. The user enters the `stats avail` command. + +Step 2. `StatsCommandParser` is then invoked to parse the argument inputted with the `stats` command, in this case it is `avail`. If the argument is not `avail`, `current` or `housing`, the `StatsCommandParser` will generate an error message to the user, indicating that the requested statistic is not available. -
:information_source: **Note:** If the `currentStatePointer` is at index `addressBookStateList.size() - 1`, pointing to the latest address book state, then there are no undone AddressBook states to restore. The `redo` command uses `Model#canRedoAddressBook()` to check if this is the case. If so, it will return an error to the user rather than attempting to perform the redo. +Step 3. The `StatsAvailCommand` will then call relevant methods to obtain the needed numbers and percentages, done in the `execute` command. +![Self Invocation StatsAvail](images/StatsAvailSelfInvDiagram.png) + +
:information_source: **Note:** The activation bar of the execute method is omitted for simplicity in the sd frame.
-Step 5. The user then decides to execute the command `list`. Commands that do not modify the address book, such as `list`, will usually not call `Model#commitAddressBook()`, `Model#undoAddressBook()` or `Model#redoAddressBook()`. Thus, the `addressBookStateList` remains unchanged. +Step 4. A success message with the statistics will then be displayed to the user. + +The other commands `stats current` and `stats housing` have a similar execution path, replacing `StatsAvailCommand` with `StatsCurrentCommand` and `StatsHousingCommand` respectively, and obtaining their own relevant statistics. + +#### Design considerations: + +**Aspect: How to display the statistics:** + +* **Alternative 1 (current choice):** Show the availability and current statistics separately. + * Pros: Displayed statistics are specific to the user's query, showing only the relevant data. + * Cons: User may need to query multiple times to get all desired statistics. -![UndoRedoState4](images/UndoRedoState4.png) +* **Alternative 2:** Show availability and current statistics together. + * Pros: Easier to implement. More convenient for user, only one query needed to get all statistics. + * Cons: Result message displayed will be very long, making the UI less desirable. -Step 6. The user executes `clear`, which calls `Model#commitAddressBook()`. Since the `currentStatePointer` is not pointing at the end of the `addressBookStateList`, all address book states after the `currentStatePointer` will be purged. Reason: It no longer makes sense to redo the `add n/David …​` command. This is the behavior that most modern desktop applications follow. +**Aspect: Which list should the statistics be calculated from:** -![UndoRedoState5](images/UndoRedoState5.png) +* **Alternative 1 (current choice):** Calculated from the currently displayed list. + * Pros: The resulting statistic corresponds to what the user sees, allowing for easy verification. + * Cons: The user may first need to perform a `find` or `list` command to query the list of interest before entering the stats command. + +* **Alternative 2:** Calculated from the main list of fosterers. + * Pros: The resulting statistic corresponds to the whole address book, which may cause less confusion for the user. + * Cons: Less flexibility for the user. + + +### Undo feature + +#### Implementation + +The undo feature allows users to revert the last executed command in the address book. This feature is facilitated by the `UndoCommand`, `AddressBookParser`, and `LogicManager` classes. The undo feature is implemented using the following components and operations: + +* `UndoCommand` — The core component responsible for executing the undo operation in the address book. +* `AddressBookParser` — Handles the parsing of the user's undo command. +* `LogicManager` — Represents the application's data and business logic, including the functionality to undo the last command. + +Given below is an example usage scenario and how the undo mechanism behaves at each step. The sequence diagram illustrates the interactions inside the Logic component for the undo command. The diagrams illustrate the flow of the execution of a successful undo command. In the diagrams below, the instance of `backupModel` is a separate instance from the current model and only serves as a container to save the data from the current model. + +![Interactions Inside the Logic Component for the UndoCommand](images/UndoSequenceDiagram.png) + +Step 1. The user launches the application for the first time. The `backupModel` will be initialized with the initial address book state. + +Step 2. The user executes a command. The `LogicManager` checks if the command is an instance of the `undo` command. + +Step 3. If the command is an instance of the `add`, `delete`, `edit`, `sort` or a successful execution of the `reset` command, the `backupmodel` data will be replaced with a copy of the current model data before the execution of the entered command. + +Step 4. If the command is an instance of the `undo` command and the current model data is not the same as the data from the `backupModel`, the current model data will be replaced with the data from the `backupModel`. Else an error message will be displayed. + +Step 5. A success message is displayed to the user to confirm that the last command has been undone. + +The following activity diagram summarizes what happens when a user executes an undo command: + +![Undo Command Activity Diagram](images/UndoActivityDiagram.png) + +
:information_source: **Note:** If a command fails its execution, no backup of the model will be mode, so the address book state will not be saved into the `addressBookStateList`. + +
-The following activity diagram summarizes what happens when a user executes a new command: - #### Design considerations: -**Aspect: How undo & redo executes:** +**Aspect: How undo executes:** -* **Alternative 1 (current choice):** Saves the entire address book. - * Pros: Easy to implement. - * Cons: May have performance issues in terms of memory usage. +* **Alternative 1 (current choice):** Saves a backup of the entire address book. + * Pros: Easy to implement and not too memory intensive. + * Cons: Can only save one previous instance of the address book. -* **Alternative 2:** Individual command knows how to undo/redo by - itself. +* **Alternative 2:** Individual commands knows how to undo by itself. * 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}_ +* **Alternative 3:** Saves multiple backups of the entire address book. + * Pros: Easy to implement. + * Cons: Memory intensive and may have performance issues in terms of memory usage. + +### Reset feature + +#### Implementation + +The reset feature allows users to erase the contents of the address book. This feature is facilitated by the `ClearCommand`, `ClearCommandParser`, `AddressBookParser`, and `LogicManager` classes. The undo feature is implemented using the following components and operations: + +* `ClearCommand` — The core component responsible for erasing the entire address book. +* `ClearCommandParser` — Handles the parsing of the arguments for the reset command. +* `AddressBookParser` — Handles the parsing of the user's reset command. +* `LogicManager` — Represents the application's data and business logic, including the functionality to check if the user has properly gone through the appropriate steps to execute a successful address book reset. + +Given below is an example usage scenario and how the reset mechanism behaves at each step. The sequence diagram illustrates the interactions inside the Logic component for the first execution of the `reset` command. The diagrams illustrate the flow of the execution of a successful `reset` command. + +Step 1. The user enters the `reset` command. And is given a warning of the functionality of the `reset` command and is prompted to enter `reset confirm` to continue executing the command. -### \[Proposed\] Data archiving +![Interactions Inside the Logic Component for the ClearCommand](images/ResetSequenceDiagram.png) -_{Explain here how the data archiving feature will be implemented}_ +Step 2. The user enters the `reset confirm` command. + +![Interactions Inside the Logic Component for the ClearConfirmCommand](images/ResetConfirmSequenceDiagram.png) + +Step 3. A success message is displayed to the user to confirm that the entire address book has been erased. + + +The following activity diagram summarizes what happens when a user executes a reset command: + +![Reset Command Activity Diagram](images/ResetActivityDiagram.png) + + +#### Design considerations: + +**Aspect: How reset prompts confirmation:** + +* **Alternative 1 (current choice):** Require a `reset` command followed by `reset confirm`. + * Pros: Intuitive and prevents accidental resets. + * Cons: Troublesome to execute. + +* **Alternative 2:** Make `reset` throw an error and have `reset confirm` be the actual command. + * Pros: Easy to implement. + * Cons: Accidental resets can still happen. + +
+ + +## **Planned Enhancements** + +### Shorter Command Formats +Currently, the default add command may be too long for an average typer to key in quickly. A future enhancement we are planning would be to allow the addition of a fosterer with just the basic details, such as their name, phone number, email, housing type and availability. The rest of the fields will be set to `nil` by default. +Only when the fosterer is ready to foster, then other details such as animal name, animal type and address need to be filled in via the `edit` command. + +We are also planning to shorten some input parameters when adding or editing a fosterer: + +| Current | Enhancement | +|-----------------------------|---------------| +| `availability/Available` | `avail/true` | +| `availability/NotAvailable` | `avail/false` | +| `availability/nil` | `avail/nil` | +| `animalType/ ` | `type/ ` | + +With this, the command `add n/Jerry Tan p/98765412 e/jerry123@example.com housing/HDB avail/true` will be a valid add +command. + +### Reduce Coupling Between Availability and Animal Type +Currently, the `animalType` field also contains information about the availability of a fosterer.
+e.g. if the `animalType` field of a fosterer is `able.Dog`, it suggests that the fosterer is available, and is +able to foster a dog. However, the user will still need to enter the `availability` field as `available`.
+ +Building on the enhancement in [Shorter Command Formats](#shorter-command-formats), we will be revising the +`AVAILABILITY` and `ANIMAL_TYPE` parameters: + +| Parameter | About | Values | +|-----------------------|-------------------------------------------------------------------|------------------------| +| `AVAILABILITY` | Indicates availability of fosterer | `false`, `true`, `nil` | +| `ABLE_ANIMAL_TYPE` | Indicates the type of animals the fosterer can foster | `dog`, `cat`, `nil` | +| `CURRENT_ANIMAL_TYPE` | Indicates the type of animals the fosterer is currently fostering | `dog`, `cat`, `nil` | + + +### Support More Animal Types + +Building on the enhancement in [Reduce Coupling Between Availability and Animal Type](#reduce-coupling-between-availability-and-animal-type), we can support more animal types by allowing the addition of animals other than cats and dogs under `ABLE_ANIMAL_TYPE` and `CURRENT_ANIMAL_TYPE`. Some examples include +`hamster` and `rabbit`. This will make our product available to more animal shelters. + +### Allow Fosterers To Foster More Than One Animal At A Time + +Currently, we only allow the assignment of one fosterer to one animal. To improve this, we can make the `ANIMAL_NAME`, +`ABLE_ANIMAL_TYPE` and `CURRENT_ANIMAL_TYPE` fields to be maintained as lists instead. This will allow one fosterer to be associated with more than one animal. + +### Case-sensitivity of Inputs +Currently, the fields and parameters for housing, availability and animal type are case-sensitive. An enhancement to this would be to make these fields and their parameters case-insensitive to improve the user experience. Moreover, the aforementioned enhancements will also be case-insensitive. + +### Allow Symbols in Name +Currently, names in Foster Family must be alphanumeric. However, this excludes certain legal names that have other +characters such as `/`. For example, we currently do not allow `s/o` in a person's name as the `/` is used as a command +delimiter. Hence, one possible improvement is to enforce that the name inputted by the user must be enclosed in quotation marks for parsing, and to allow symbols such as `/`, `'`, `-` etc. using regex. Additionally, we will disallow the use of numeric values in names, to prevent the case where a number is inputted as a name. + +e.g. `n/"Henry Tan"` and `n/"Nagaratnam s/o Suppiah"` are now valid name parameters. + + +### Phone Number Input + +Currently, phone numbers in Foster Family accept more than 8 digits as a valid input. This might cause invalid phone numbers to +be recorded without error messages to warn the user of such mistakes, especially in the situation where local phone numbers are used. +Hence, one possible improvement is to enforce that the phone number inputted by the user must be restricted to a maximum of 15 +digits (according to the international phone numbering plan). + +e.g. `p/90876534567890234567` is now an invalid phone number parameter. + +### Guide Users on How To Rectify / Preventing the Corruption of Data File + +Currently, the Foster Family data is saved automatically as a JSON file, and in the case where the data file is updated directly +and made invalid, Foster Family would either discard all data and start with an empty data file. Even though it is advised in the +user guide that user should not make changes to the data file directly, one possible improvement to be made to prevent such +incidents would be to either use a database with username and password authentication or implement encryption. An alternative +solution would be to account for all the cases and error messages to handle an invalid fosterer entry in the data file. We can do so +by editing the classes in `storage` such that the user would be informed, through the message, of the steps to take to rectify the +errors made. + +### Notes Feature as a Separate Command + +Currently, the notes feature is only available in the profile page, and its content, which can only be recorded via the +`save` command, is inputted through the text box visible next to the `Notes` field. However, it could potentially be +inconvenient for the user to have to use a combination of different commands (an example would be `view`, followed by `notes` and +`save`) in order to record additional crucial information with regard to a particular fosterer. This breeds inefficiency in some +scenarios, especially in the situation where an animal with existing medical conditions is fostered and its health status has to +be frequently updated. Hence, an enhancement to this feature would be to make the notes feature a separate command, such that the +user would be able to add important notes in the main window, without having to navigate to the profile page and using the +additional `save` command to add the notes. + +e.g. `notes 1 n/require an urgent visit to the vet` would add the notes "require an urgent visit to the vet" under the fosterer +listed at index 1. + +### Specificity of Error Messages + +Currently, when the user attempts to add a fosterer with an invalid combination of availability, animal name and animal type, +the same error message with multiple details of how to rectify different errors is sometimes shown, which can be confusing for the user. +In particular, in the following three cases, the same error message is displayed: + +Cases: +1. `availability/nil` but `animalType/` is not `nil`. +2. `availability/Available` with `animalType/` values set to other values which are NOT `able.Cat`, `able.Dog` or `nil`. +3. `availability/NotAvailable` with `animalType/` values set to other values which are NOT `current.Cat`, `current.Dog` or `nil`. + +Error message:
+"If fosterer is available, animal type should be 'able.Dog' / 'able.Cat'. +If animal type information is not available, it should be inputted as 'nil'.
+If fosterer is NOT available and is currently fostering, animal type should be 'current.Dog' / 'current.Cat'.
+If fosterer is currently unable to foster, animal type should be inputted as 'nil'.
+ +
+ +If availability is 'nil', animal type should be 'nil' too." + +Hence, an enhancement would be to split the error message up to only show when each specific case occur, instead of grouping them +all together into a single message. This would reduce confusion for the user and provide more convenience as the user is no longer +required to read long error messages with details that might be irrelevant to the specific error made. + +| Cases | Enhanced error message | +|-------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `availability/nil` but `animalType/` is not `nil` | If availability is 'nil', animal type should be 'nil' too. | +| `availability/Available` with `animalType/` values set to other values which are NOT `able.Cat`, `able.Dog` or `nil` | If fosterer is available, animal type should be 'able.Dog' / 'able.Cat'. If animal type information is not available, it should be inputted as 'nil'. | +| `availability/NotAvailable` with `animalType/` values set to other values which are NOT `current.Cat`, `current.Dog` or `nil` | If fosterer is NOT available and is currently fostering, animal type should be 'current.Dog' / 'current.Cat'. If fosterer is currently unable to foster, animal type should be inputted as 'nil'. | -------------------------------------------------------------------------------------------------------------------- +
+ ## **Documentation, logging, testing, configuration, dev-ops** * [Documentation guide](Documentation.md) @@ -251,80 +886,330 @@ _{Explain here how the data archiving feature will be implemented}_ -------------------------------------------------------------------------------------------------------------------- +
+ ## **Appendix: Requirements** ### Product scope **Target user profile**: +* Foster manager of non-profit animal shelters for cats and dogs, who currently do not have a good logistical workflow to keep track of their fosterers +* Prefer desktop apps over other types +* Can type fast +* Prefers typing to mouse interactions +* Is reasonably comfortable using CLI apps -* 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**: manage contacts faster than a typical mouse/GUI driven app - +**Value proposition**: Low budget and efficient system that manages fosterers ### 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 | - -*{More to be added}* +| Priority | As a … | I want to … | So that I can… | +|----------|---------------------------|-----------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `* * *` | foster manager | delete a fosterer from the list when they want to stop fostering with us | update the list to see the fosterers who are currently in our program | +| `* * *` | foster manager | delete multiple fosterers at once | perform mass deletions quickly | +| `* * *` | foster manager | add each fosterer's details efficiently | know how to contact the fosterer should I have animals that require fostering | +| `* * *` | foster manager | assign an animal to a fosterer | provide the animal with a temporary home, and make a record of who is taking care of the animal | +| `* * *` | foster manager | provide details of the fostered animal of concern to the fosterer | ensure that the animal is well taken care by informing the fosterer of existing health conditions to prepare for | +| `* * *` | foster manager | update a fosterer's details | keep track of fosterer's most up-to-date information, including information about the animal fostered | +| `* * *` | foster manager | search for a specific animal / fosterer’s detail instead of browsing through the entire list | be more productive when searching for suitable fosterers for the animal that needs fostering | +| `* * *` | foster manager | to be aware of the address of the fosterer | conduct checks on the fosterer to ensure the animal is well taken care of | +| `* * *` | foster manager | retrieve information about the foster family | provide the necessary information to the Nparks authorities for audit | +| `* * *` | foster manager | note down the foster period of the animal | keep track of when the fosterer is supposed to return the animal to the shelter | +| `* * *` | foster manager | sort the list of fosterers alphabetically | have a neater, and more organised view of all the fosterers | +| `* * * ` | foster manager | know the distribution of the different housing types among fosterers | correctly allocate the animals to foster homes that are able to accommodate them | +| `* * *` | foster manager | obtain statistics about available fosterers | better estimate shelter capacity, since having more animals under the care of fosterers will allow me to have more temporary space in the shelter for more animals | +| `* * ` | foster manager | have the fosterer’s important information collated neatly | get all the information I need with one glance | +| `* * ` | foster manager | have an easily accessible and visible help button | get help when I am unsure of what command to use | +| `* * ` | foster manager | undo my previous command | quickly resolve errors caused by the erroneous command | +| `* *` | new foster manager | have my initial data file pre-populated with sample data | work with this sample data as an introduction to the app | +| `* *` | new foster manager | purge all current data from address book | remove all sample/ experimental data I used to explore the app | +| `* *` | careless foster manager | be asked to confirm my decision before purging all fosterer data | prevent myself from accidentally deleting all fosterer records | +| `* *` | foster manager | keep track of animals who are eligible for fostering | know which animal is ready to be assigned to a fosterer | +| `* *` | foster manager | assign a volunteer to the fosterer | have volunteers check up on the foster animal, to verify its well being | +| `* *` | foster manager | view which volunteer is assigned to which animal | properly balance workload and ensure that no volunteer is overburdened | +| `*` | foster manager | retrieve documentation of all species of animals under the purview of the shelter | ensure compliance with regulations for wildlife, protected or endangered species (Nparks audit) | +| `*` | foster manager | retrieve history of whereabouts of an animal | ensure accountability for pets’ accommodations, and verify that pets are monitored daily for signs of illness, injury or disease (Nparks audit) | +| `*` | foster manager | assign more than one animal to the fosterer | allow more animals to benefit from living under the care of a fosterer | ### Use cases -(For all use cases below, the **System** is the `AddressBook` and the **Actor** is the `user`, unless specified otherwise) +(For all use cases below, the **System** is the `Foster Family`, and the **Actor** is the `Foster Manager`, unless specified otherwise) + +**Use case: UC1 - Add Fosterer** -**Use case: Delete a person** +**Preconditions**: Foster Manager has collected fosterer details. **MSS** -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 +1. Foster Manager requests to add fosterer to the System. +2. System adds the specified fosterer. + + Use case ends. + +**Extensions** + +* 1a. System detects a format error in the entered command. + * 1a1. System indicates the error and requests for data to be inputted in the correct format. + * 1a2. Foster Manager enters new command. + + Steps 1a1 - 1a2 are repeated until the command entered is in the correct format. + + Use case resumes from step 2. + +* 1b. System detects a conflicting add that will create an invalid fosterer. + * 1b1. System indicates the error and requests for Foster Manager to input a valid fosterer. + * 1b2. Foster Manager enters new command. + + Steps 1b1 - 1b2 are repeated until the fosterer is valid. + + Use case resumes from step 2. + +**Use case: UC2 - List / Find Fosterers** + +**MSS** + +1. Foster Manager requests to list fosterers. +2. System displays list of fosterers that matches the Foster Manager's query. + + Use case ends. + +**Extensions** + +* 1a. System detects a format error in the entered command. + * 1a1. System indicates the error and requests for data to be inputted in the correct format. + * 1a2. Foster Manager enters new command. + + Steps 1a1 - 1a2 are repeated until the command entered is in the correct format. + + Use case resumes from step 2. + +**Use case: UC3 - Edit Fosterer Using Main Window** + +**MSS** + +1. Foster Manager lists fosterers (UC2). +2. Foster Manager requests to edit a fosterer referenced by their index shown in the list, specifying details to edit. +3. System updates fosterer details. +4. System displays updated fosterer. Use case ends. **Extensions** +* 2a. System detects an invalid index. + * 2a1. System indicates the error and requests for a valid index. + * 2a2. Foster Manager enters new command. + + Steps 2a1 - 2a2 are repeated until the entered index is valid. + + Use case resumes from step 3. + +* 2b. System detects a format error in the entered command. + * 2b1. System indicates the error and requests for data to be inputted in the correct format. + * 2b2. Foster Manager enters new command. + + Steps 2b1 - 2b2 are repeated until the command entered is in the correct format. + + Use case resumes from step 3. + +* 2c. System detects a conflicting edit that will create an invalid fosterer. + * 2c1. System indicates the error and requests for Foster Manager to input a valid edit. + * 2c2. Foster Manager enters new command. + + Steps 2c1 - 2c2 are repeated until the command entered is valid. + + Use case resumes from step 3. + +**Use case: UC4 - Edit Fosterer Using Profile** + +**MSS** + +1. Foster Manager lists fosterers (UC2). +2. Foster Manager requests to edit a fosterer referenced by their index shown in the list. +3. System displays details of the selected fosterer. +4. Foster Manager enters the desired changes. +5. Foster Manager requests to save the changes. +6. System displays the updated list of fosterers. + + Use case ends. + +**Extensions** +* 1a. System detects an invalid index. + * 1a1. System indicates the error and requests for a valid index. + * 1a2. Foster Manager enters new command. + + Steps 1a1 - 1a2 are repeated until the entered index is valid. + + Use case resumes from step 2. -* 2a. The list is empty. +* 4a. System detects that the Foster Manager did not fill up compulsory fields. + * 4a1. System indicates missing field error. + * 4a2. Foster Manager fills up the missing compulsory fields. + * 4a3. Foster Manager requests to save the changes. - Use case ends. + Steps 4a1-4a3 are repeated until every compulsory field is filled. -* 3a. The given index is invalid. + Use case resumes from step 5. - * 3a1. AddressBook shows an error message. +* 4b. System detects a conflicting edit that will create an invalid fosterer. + * 4b1. System indicates the error and requests for Foster Manager to input a valid edit. + * 4b2. Foster Manager enters new command. - Use case resumes at step 2. + Steps 4b1 - 4b2 are repeated until the command entered is valid. -*{More to be added}* + Use case resumes from step 5. + +**Use case: UC5 - Delete Fosterers** + +**MSS** + +1. Foster Manager lists fosterers (UC2). +2. Foster Manager requests to delete fosterers referenced by their index shown in the list. +3. System deletes selected fosterers. +4. System displays the updated list of fosterers. + + Use case ends. + +**Extensions** + +* 2a. System detects invalid indices. + * 2a1. System indicates the error and requests for valid indices. + * 2a2. Foster Manager enters new command. + + Steps 2a1 - 2a2 are repeated until all entered indices are valid. + + Use case resumes from step 3. + + +**Use case: UC6 - Sort List Of Fosterers** + +**MSS** + +1. Foster Manager lists fosterers (UC2). +2. Foster Manager requests to sort the list of fosterers. +3. System displays the updated list of fosterers, sorted alphabetically by name, where uppercase letters come before lowercase letters. + + Use case ends. + +**Extensions** + +* 1a. System detects an error in the entered command. + * 1a1. System indicates the error. + * 1a2. Foster Manager requests for command 'help' (UC8). + * 1a3. Foster Manager enters new command. + + Steps 1a1 - 1a3 are repeated until the command entered is correct. + + Use case resumes from step 2. + + +**Use case: UC7 - View Statistics** + +**MSS** + +1. Foster Manager lists fosterers (UC2). +2. Foster Manager requests to view a certain statistic of the displayed fosterers. +3. System displays the relevant statistic. + + Use case ends. + +**Extensions** + +* 2a. The displayed list is empty. + * 2a1. System indicates error. + + Use case ends. + + +* 2b. Requested statistic is invalid. + * 2b1. System indicates error, and prompts Foster Manager to request for a valid statistic. + * 2b2. Foster Manager enters new command. + + Steps 2b1 - 2b2 are repeated until a valid statistic is requested. + + Use case resumes from step 3. + +**Use case: UC8 - Undo Previous Command** + +**MSS** + +1. Foster Manager requests to undo the previous command. +2. System undoes the previous command. + + Use case ends. + +**Extensions** + +* 1a. No more undo history is found. + * 1a1. System indicates error. + + Use case ends. + +**Use case: UC9 - Request For Command Help** + +**MSS** + +1. Foster Manager requests for help. +2. System displays the help page link. + + Use case ends. + +**Use case: UC10 - Reset System** + +**MSS** + +1. Foster Manager requests to reset the System. +2. System requests for confirmation. +3. Foster Manager confirms. +4. System purges all fosterer data from the system. + + Use case ends. + +**Extensions** + +* 2a. Foster Manager chooses to cancel reset. + + Use case ends. + + +**Use case: UC11 - Exit** + +**MSS** + +1. Foster Manager requests to exit the program. +2. System closes program. + + Use case ends. ### Non-Functional Requirements 1. Should work on any _mainstream OS_ as long as it has Java `11` or above installed. -2. Should be able to hold up to 1000 persons without a noticeable sluggishness in performance for typical usage. +2. Should be able to hold up to 500 fosterers 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. - -*{More to be added}* +4. The system should be usable by a novice foster manager who has never tried out keeping track of fosterers through this new system. +5. The system should respond within two seconds. (i.e. each time a valid command is entered or there is any use action) +6. The user interface should be intuitive enough for users who are not IT-savvy. +7. The user guide should be easily understood by target users. (foster managers of animal shelters, assuming they have no relevant technical knowledge) +8. The system should be able to detect invalidity of command within 2 seconds. +9. The system must be able to scale up or down as needed (in the case where there is a sudden fosterers). +10. The system should not contain the fosterer's private information in the case that the fosterer did not give consent. +11. The system must comply with all applicable laws and regulations like the Personal Data Protection Act (PDPA). +12. The system should be reliable, easy to maintain, and accessible 24/7. +13. The product should be able to run on mainstream OS. ### Glossary * **Mainstream OS**: Windows, Linux, Unix, OS-X -* **Private contact detail**: A contact detail that is not meant to be shared with others +* **Fosterer**: A person who temporarily cares for an animal in their own home +* **Housing type**: Categorised into HDB, Condo, Landed +* **Types of animal fostered**: Categorised into solely cats and/or dogs (current.Dog/Cat or able.Dog/Cat depends on availability of fosterer) +* **Current list**: The currently displayed list obtained from a list command or its variants -------------------------------------------------------------------------------------------------------------------- +
+ ## **Appendix: Instructions for manual testing** Given below are instructions to test the app manually. @@ -349,29 +1234,181 @@ 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 …​ }_ +### Adding a fosterer + +1. Adding a fosterer after collecting all the mandatory details to be inputted + + 1. Prerequisites: Foster Manager has collected fosterer details such as name, phone number, email, address, housing type, availability, name of animal fostered + (if currently fostering) and type of animal fostered (if currently fostering). + + 1. Test case (valid): `add n/Anne Lim p/98765422 e/anne123@example.com a/Baker street, block 6, #27-01 housing/Landed availability/NotAvailable animal/Bucky animalType/current.Dog`
+ Expected: Fosterer named Anne Lim is added to the list. Details of the added fosterer are shown in the status message. + + 1. Test case (non-exhaustive list of invalid add commands):
-### Deleting a person +| Scenario | Example command | Expected Error Message | +|---------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Without any of the following: `n/`, `p/`,`e/`, `a/`, `housing/`, `availability/`, `animal/`, `animalType/` | `add n/Jerry Tan p/98765412 e/jerry123@example.com a/Baker street, block 5, #27-01 housing/HDB animal/Dexter animalType/current.Cat t/Urgent` | `Invalid command format! add: Adds a person to the address book. Parameters: n/NAME p/PHONE e/EMAIL a/ADDRESS housing/HOUSING availability/AVAILABILITY animal/ANIMAL_NAME animalType/ANIMAL_TYPE [t/TAG]... Note: If information for that field is not available, put 'nil'. Example: add n/John Doe p/98765432 e/johnd@example.com a/311, Clementi Ave 2, #02-25 housing/HDB availability/NotAvailable animal/Dexter animalType/current.Dog t/Urgent t/goodWithDogs` | +| `availability/nil` but `animal/` is not 'nil' | `add n/Jerry Tan p/98765412 e/jerry123@example.com a/Baker street, block 5, #27-01 housing/HDB availability/nil animal/Dexter animalType/nil t/Urgent` | `When an animal name is provided, availability should not be 'Available' or 'nil'.` | +| `availability/Available` but `animal/` is not 'nil' | `add n/Jerry Tan p/98765412 e/jerry123@example.com a/Baker street, block 5, #27-01 housing/HDB availability/Available animal/Dexter animalType/nil t/Urgent` | `When an animal name is provided, availability should not be 'Available' or 'nil'.` | +| `housing/` with values other than ‘HDB’, ‘Condo’, ‘Landed’, ‘nil’ | `add n/Jerry Tan p/98765412 e/jerry123@example.com a/Baker street, block 5, #27-01 housing/hdb availability/NotAvailable animal/Dexter animalType/current.Cat t/Urgent` | `Housing type should be either 'HDB', 'Condo', 'Landed' or 'nil'` | +| `availability/` with values other than ‘Available’, ‘NotAvailable’, ‘nil’ | `add n/Jerry Tan p/98765412 e/jerry123@example.com a/Baker street, block 5, #27-01 housing/HDB availability/available animal/Dexter animalType/current.Cat t/Urgent` | `Availability should be either 'Available', 'NotAvailable' or 'nil'` | +| `availability/nil` but `animalType/` is not 'nil' | `add n/Jerry Tan p/98765412 e/jerry123@example.com a/Baker street, block 5, #27-01 housing/HDB availability/nil animal/nil animalType/able.Cat t/Urgent` | `If fosterer is available, animal type should be 'able.Dog' / 'able.Cat'. If animal type information is not available, it should be inputted as 'nil'. If fosterer is NOT available and is currently fostering, animal type should be 'current.Dog' / 'current.Cat'. If fosterer is currently unable to foster, animal type should be inputted as 'nil'. If availability is 'nil', animal type should be 'nil' too.` | +| `availability/Available` with `animalType/` values set to other values which are NOT ‘able.Cat’ or ‘able.Dog’ or 'nil' | `add n/Jerry Tan p/98765412 e/jerry123@example.com a/Baker street, block 5, #27-01 housing/HDB availability/Available animal/nil animalType/current.Cat t/Urgent` | `If fosterer is available, animal type should be 'able.Dog' / 'able.Cat'. If animal type information is not available, it should be inputted as 'nil'. If fosterer is NOT available and is currently fostering, animal type should be 'current.Dog' / 'current.Cat'. If fosterer is currently unable to foster, animal type should be inputted as 'nil'. If availability is 'nil', animal type should be 'nil' too.` | +| `availability/NotAvailable` with `animalType/` values set to other values which are NOT ‘current.Cat’ or ‘current.Dog' or 'nil' | `add n/Jerry Tan p/98765412 e/jerry123@example.com a/Baker street, block 5, #27-01 housing/HDB availability/NotAvailable animal/Dexter animalType/able.Cat t/Urgent` | `If fosterer is available, animal type should be 'able.Dog' / 'able.Cat'. If animal type information is not available, it should be inputted as 'nil'. If fosterer is NOT available and is currently fostering, animal type should be 'current.Dog' / 'current.Cat'. If fosterer is currently unable to foster, animal type should be inputted as 'nil'. If availability is 'nil', animal type should be 'nil' too.` | +| `availability/NotAvailable` with `animalType/` values set to 'nil' but `animal/` values NOT 'nil' | `add n/Jerry Tan p/98765412 e/jerry123@example.com a/Baker street, block 5, #27-01 housing/HDB availability/NotAvailable animal/Dexter animalType/nil t/Urgent` | `When availability is 'NotAvailable', animal name and type have to either be both 'nil' or both not 'nil'.` | +| `availability/NotAvailable` with `animal/` values set to 'nil' but `animalType/` values NOT 'nil' | `add n/Jerry Tan p/98765412 e/jerry123@example.com a/Baker street, block 5, #27-01 housing/HDB availability/NotAvailable animal/nil animalType/current.Cat t/Urgent` | `When availability is 'NotAvailable', animal name and type have to either be both 'nil' or both not 'nil'.` | -1. Deleting a person while all persons are being shown - 1. Prerequisites: List all persons using the `list` command. Multiple persons in the list. +### Deleting a fosterer + +1. Deleting a fosterer while all fosterers are being shown + + 1. Prerequisites: List all fosterers using the `list` or `find` command. At least 3 fosterers in the list. 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. + Expected: First fosterer is deleted from the list. Details of the deleted fosterer are shown in the status message. + + 1. Test case: `delete 1 2 3`
+ Expected: First, second and third fosterers are deleted from the list. Number of deleted fosterers, and their details are shown in the status message. 1. Test case: `delete 0`
- Expected: No person is deleted. Error details shown in the status message. Status bar remains the same. + Expected: No person is deleted. Error details are shown in the status message. + + 1. Other incorrect delete commands to try: `delete`, `delete x`, (where x is larger than the list size)
+ Expected: Similar to previous. + +2. Deleting fosterers while only some fosterers are shown follows similar test cases. + +### Listing a fosterer + +1. Find through an exact, quoted match with `"`. + + 1. Test case: `list "Pete"`
+ Expected: Shown fosterers include "Pete" but not "Peter". + +2. Verify operator precedence and parentheses. + + 1. Test case: `list Doe & Sam / John`
+ Expected: Equivalent to `list Doe & (Sam / John)`. Shown fosterers include "John Doe" but not "John Snow". + + 2. Test case: `list (Doe & Sam) / John`
+ Expected: Shown fosterers include "Sam Doe" and "John Snow". + +### Saving changes in details of fosterer when add is done in the profile page + +1. Saving the new fosterer added through the profile page + + 1. Test case: `save`
+ Expected: Exits the profile page. Fosterer will be successfully added to the address book. + +### Saving changes in details of fosterer when edit is done in the profile page + +1. Saving the edits made to a fosterer in the profile page + + 1. Test case: `save`
+ Expected: Fosterer's details will be successfully edited and saved if they are valid. Command success message is shown. + +### Sorting the list of fosterers - 1. Other incorrect delete commands to try: `delete`, `delete x`, `...` (where x is larger than the list size)
+1. Sorting the list of fosterers in the main window alphabetically by name, where uppercase letters come before lowercase letters + + 1. Test case: `sort`
+ Expected: The whole list of fosterers will be sorted alphabetically. Command success message is shown. + + 1. Test case: `sort 12345`
+ Expected: The whole list of fosterers will be sorted alphabetically. Command success message is shown. + +### Undoing the previous command + +1. Undoing the previous command successfully executed + + 1. Prerequisites: The previous command successfully executed is either `add`, `delete`, `edit`, `sort` or a successful execution of the `reset` command. + + 1. Test case: `undo`
+ Expected: Previous command will be undone. Command success message is shown. + + 1. Test case: `undo 12345`
+ Expected: Previous command will be undone. Command success message is shown. + +### Viewing statistics of fosterers +1. Viewing statistics of available fosterers + 1. Prerequisites: List all fosterers using the list or find command. At least 1 fosterer in the list. + + 1. Test case: `stats avail`
+ Expected: Availability statistics shown. + + 1. Test case: `stats avail`
Expected: Similar to previous. -1. _{ more test cases …​ }_ + 1. Test case: `stats availl avail`
+ Expected: Invalid command, error details shown in status message. + + 1. Test case: `stats avail list`
+ Expected: Availability statistics shown. `stats` commands ignore extraneous parameters after the valid command is detected. + + 1. Test case: `stats avail current`
+ Expected: Only availability statistics shown. `stats` commands will only display the first valid statistic field detected (ie. either `avail`, `current` or `housing`). + +2. Viewing statistics of current fosterers and housing types work similarly, replacing `avail` with `current` and `housing` respectively. + + +### Editing details of a fosterer + +1. Editing through the main window + 1. Prerequisites: At least one fosterer in the list, and the first fosterer has at least 2 tags.
+ 1. Test case: `edit 1 p/99887776`
+ Expected: Fosterer 1's phone number successfully edited.
+ + 1. Test case: `edit 1 t/new`
+ Expected: Fosterer 1's existing tags are overwritten. The only tag is the `new` tag.
+ + 1. Test case: `edit 1 n/Ben Yeo e/benyeo123@gmail.com`
+ Expected: Fosterer 1's name and email successfully edited.
+ + 1. Invalid edit commands to try: `edit n/Ben Yeo`, `edit x n/Ben Yeo` (where x is larger than the list size).
+ +2. Opening the profile view for editing + 1. Prerequisites: At least one fosterer in the list, and the first fosterer has at least 2 tags.
+ + 1. Test case: `edit 1` or `view 1`
+ Expected: Profile view of fosterer 1 opens for editing.
+ + 1. Test case: `edit 1 list` or `edit` or `view`
+ Expected: Profile view does not open, error message shown.
+ +3. Field jumping in profile view + 1. Prerequisites: The profile view of a fosterer is currently open.
+ + 1. Test case: `ph`
+ Expected: Cursor jumps to the Phone field for editing.
+ + 1. Test case: `type`
+ Expected: Cursor jumps to the Animal Type field for editing.
+ +4. Editing details in profile view + 1. Availability = `NotAvailable`, Animal Name = `nil` Animal Type = `nil`
+ Expected on Enter: Valid fosterer.
+ + 1. Availability = `Available`, Animal Name = `nil` Animal Type = `nil`
+ Expected on Enter: Valid fosterer.
+ + 1. Availability = `NotAvailable`, Animal Name = `mew` Animal Type = `able.Cat`
+ Expected on Enter: Invalid fosterer, and error message shown.
+ +### Resetting the address book +1. Resetting the Address Book + 1. Prerequisites: `reset` command must be entered first, before entering `reset confirm`.
-### Saving data + 1. Test case: `reset`
+ Expected: Warning displayed of the function of the `reset` command and a prompt of entering `reset confirm` is displayed to continue executing the command.
-1. Dealing with missing/corrupted data files + 1. Test case: `reset confirm`
+ Expected: A prompt to enter `reset` first followed by `reset confirm` to execute the command.
- 1. _{explain how to simulate a missing/corrupted file, and the expected behavior}_ + 1. Test case: `reset randomArg`
+ Expected: Warning displayed of the function of the `reset` command and a prompt of entering `reset confirm` is displayed to continue executing the command.
-1. _{ more test cases …​ }_ + 1. Test case: `reset` followed by `reset confirm`
+ Expected: The Address Book is erased and a command success massage is displayed.
+ diff --git a/docs/UserGuide.md b/docs/UserGuide.md index 57437026c7b..160c42b7567 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -2,196 +2,961 @@ layout: page title: User Guide --- +**Foster Family** is a desktop application built for **foster managers of cat and dog shelters** to facilitate the **administrative management** of **foster families**. -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. +Here are some tasks which Foster Family can help you with: +* **Store and update** the important details of animal fosterers (people who temporarily care for an animal in their own homes). +* **Search** for a fosterer using _any_ detail you can remember of them. +* **Gain insights** on the overall status of managed fosterers. +Foster Family is optimised for use via a **Command Line Interface (CLI)**. This means that you primarily interact with it by typing commands. It also retains the benefits of a Graphical User Interface (GUI), allowing you to interact with the application (app) through graphical components. If you can type fast, Foster Family can get things done faster than traditional GUI apps. + +This document is a comprehensive guide to all the commands available to you, along with **step-by-step explanations** and **examples** to help you master the features Foster Family has to offer. If you are a _new user_, we recommend beginning your journey with [Quick Start](#quick-start). For those who are _already acquainted_, you can refer to the [Table of Contents](#table-of-contents) below to navigate to a section that interests you. + +
+ +## **Table of Contents** * Table of Contents {:toc} -------------------------------------------------------------------------------------------------------------------- -## Quick start +
+ + +## **Quick Start** + +Step 1. Ensure you have Java `11` or above installed in your computer. You can download it from the [Oracle website](https://www.oracle.com/sg/java/technologies/javase/jdk11-archive-downloads.html). + +Step 2. Download the latest `FosterFamily.jar` from [our GitHub Page](https://github.com/AY2324S1-CS2103T-T13-4/tp/releases). + +Step 3. Copy the file to the folder you want to use as the _home folder_ for Foster Family. + +Step 4. Open a command terminal. + +Step 5. Navigate to the home folder you put the jar file in using the command
+`cd `, replacing `` with your file path. + +Step 6. Use the `java -jar FosterFamily.jar` command to run the app.
-1. Ensure you have Java `11` or above installed in your Computer. + The Foster Family GUI, similar to the image below, should appear on your screen. Note that the app contains some sample data the first time you launch it.
+ ![Ui](images/Ui.png) -1. Download the latest `addressbook.jar` from [here](https://github.com/se-edu/addressbook-level3/releases). +
-1. Copy the file to the folder you want to use as the _home folder_ for your AddressBook. -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) +Step 7. Type a command in the command box and press Enter to execute it. e.g. typing `help` and pressing Enter will open the help window.
-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: - * `list` : Lists all contacts. +* `list` : Lists all fosterers. - * `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. +* `add n/Jerry Tan p/98765412 e/jerry123@example.com a/61 Baker Street housing/nil availability/nil animal/nil animalType/nil` : Adds a fosterer named `"Jerry Tan"` to Foster Family. - * `delete 3` : Deletes the 3rd contact shown in the current list. +* `delete 3` : Deletes the 3rd fosterer shown in the current list. - * `clear` : Deletes all contacts. +* `reset`, followed by `reset confirm` : Deletes all fosterers. - * `exit` : Exits the app. +* `exit` : Exits Foster Family.
-1. Refer to the [Features](#features) below for details of each command. +Please refer to the [Features](#features) section for details of each command. -------------------------------------------------------------------------------------------------------------------- -## Features +
+ +## **Useful Notations** +These symbols highlight information you may find important. + +| Symbol | Meaning | +|----------------------|-----------------------------------------------------------------------------------------------| +| :information_source: | General notes about the command | +| :exclamation: | Important notes about the command | +| :warning: | Warnings about the command, especially when data loss or misinterpretation is likely to occur | +| :bulb: | Tips to optimise the use of Foster Family | + +-------------------------------------------------------------------------------------------------------------------- + +## **Technical Terms** +These are some technical terms you may come across in this user guide. + +| Term | Definition | +|----------------------|-------------------------------------------------------------------------------------------| +| Command | Features of Foster Family, as well as to keywords that trigger those features | +| Parameter / Argument | Information to be passed to commands as inputs | +| Index | The number to the left of each fosterer's name in the list shown in the main page | +| Field | Attributes associated with a fosterer entry in Foster Family, such as name and email, etc | + +-------------------------------------------------------------------------------------------------------------------- + +
+ +## **User Interface (UI)** + +There are two different screens you will interact with in Foster Family. + +### The main page + +![Ui](images/Ui.png) + +This is the main view that welcomes you when you first start up Foster Family. + +
+ +### The profile page +![ProfileExample](images/screenshots/EditExample.png) + +This is the profile view that you can use to add a fosterer, or to edit the details of an existing fosterer. + +#### Opening the profile page +There are two ways you can use to navigate to the profile page. +1. Enter add to view an _empty_ profile page to [add](#adding-a-fosterer-through-the-profile-page-add) a fosterer. +2. Enter either edit INDEX or view INDEX to [edit](#editing-a-fosterers-details-through-the-profile-page-edit) or [view](#viewing-a-fosterers-detail-view) the fosterer at index INDEX in Foster Family. + +
+ +Suppose you want to open the profile page of a fosterer named Alex Yeoh who is currently at index 1. + +
+ +
+ +
+ +To do so, enter view 1 as shown in the image above. You will be directed to his profile page as shown in the image below. + +
+ +
+ +
+ +
+ +#### Navigating through fields + +Entering the name, fully or partially, of the field you want to edit brings your cursor to the corresponding textbox. + +
+ +
+ +
+ +In the example above, entering name, or a part of `name` like nam, brings the focus to the name field. + +
+ +
+ +
+ +After you are done editing, _press the Enter key_ on your keyboard to **confirm your changes**, and bring your cursor back to the command box. + +
+ +If you **wish to discard changes** while editing in the textbox, _press the Esc key_ to undo the changes and direct your cursor back to the command box. + +
+ + + +
+ +In the example above, after changing name from "Yeoh" to "Yeo", the Enter key was pressed. + +The same process can be applied to other fields. + +
+ +#### Saving changes + +Entering save saves the changes you have made into storage. + +Suppose you want to save your changes after changing the name of the fosterer. + +
+ +
+ +
+ +Key in save and press Enter to save the changes. + +
+ +
+ +
+ +
+ +
+ +The effect of a `save` depends on the command you used to enter the profile page. +* If this was an `edit` command, your changes to the fosterer will be saved.
+* If this was an `add` command, the new fosterer with the inputted details will be added, and you will be redirected back to the main page. + +
+ +#### Exiting the profile page + +Entering exit closes the profile page and directs you back to the main page. Foster Family will warn you if you attempt to exit without saving your changes. + +
+ +**1. Changes are saved** + +Suppose you have already saved your changes. + +![Exit command saved after](images/screenshots/ExitCommandSavedBefore.png) + +
+ +
+ +Key in exit and press Enter to close the profile page. + +![Exit command saved after](images/screenshots/ExitCommandSavedAfter.png) + +
+ +**2. Changes are not saved** + +Suppose you entered exit without saving your latest changes. + +![Exit command not saved warning](images/screenshots/ExitCommandNotSavedWarning.png) + +
+ +
+ +If you _press the Enter key_ again, you will **discard your changes** and be redirected back to the main page. + +
+ +If you _press the Esc key_, the **exit is cancelled**, and you can continue working on your changes in the profile page. + +![Exit command not saved cancel](images/screenshots/ExitCommandNotSavedCancel.png) + +The image above is the result of pressing the Esc key after the warning. + +To learn more about **adding a new fosterer through the profile page**, refer to the section [Adding a fosterer through the profile page: add](#adding-a-fosterer-through-the-profile-page-add).
+To learn more about **editing a fosterer through the profile page**, refer to the section [Editing a fosterer's detail through the profile page: edit](#editing-a-fosterers-details-through-the-profile-page-edit). + +
+ +-------------------------------------------------------------------------------------------------------------------- + +
+ +## **Features**
**:information_source: Notes about the command format:**
-* 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`. +* Words in `UPPER_CASE` are parameters to be supplied by you.
+ e.g. in `n/NAME`, `NAME` is a parameter which can be used as `add n/John Doe`. * 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`. + e.g `n/NAME [t/TAG]` can be used as `n/John Doe t/urgent` or `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, _**including zero times**_.
+ e.g. `[t/TAG]…​` can be used as ` ` (i.e. zero times), `t/urgent`, `t/urgent t/experienced` etc. * Parameters can be in any order.
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.
+* Extraneous parameters for commands that do not take in parameters (such as `help`, `exit`, `undo`, `sort` and `reset`) 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. +* 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 app. + +* If the command you entered has an invalid format, our app will provide specific error messages to guide you to rectify the issue. +
-### Viewing help : `help` +
-Shows a message explaning how to access the help page. +### Viewing help for commands : `help` -![help message](images/helpMessage.png) +Opens a pop-up window, providing you with the link to our User Guide for help. Format: `help` +![Help](images/screenshots/HelpWindow.png) + +
+ +
+ +### Adding a fosterer through the main page: `add` + +Adds a fosterer to your address book, done through the main page. + +Format: `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS housing/HOUSING_TYPE availability/AVAILABILITY animal/ANIMAL_NAME animalType/TYPE_OF_ANIMAL [t/TAG]…` + +Parameters: + +| Parameter | About | Example | +|------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------| +| `NAME` | Name of the fosterer | `Alice Tan`, `Harry Yeo` | +| `PHONE_NUMBER` | Phone number of the fosterer | `93456778`, `89067547` | +| `EMAIL` | Email of the fosterer | `thomas718@gmail.com`, `kate@yahoo.com.sg` | +| `ADDRESS` | Address of the fosterer | `Orchard road, Blk 8, #13-04` | +| `HOUSING_TYPE` | - Housing type of the fosterer
- Case-sensitive
- Can only take the values shown in the example column | `HDB`, `Condo`, `Landed`, `nil` | +| `AVAILABILITY` | - Availability of the fosterer
- Case-sensitive
- Can only take the values shown in the example column | `NotAvailable`, `Available`, `nil` | +| `ANIMAL_NAME` | Name of the animal fostered | `Fluffball`, `nil` | +| `TYPE_OF_ANIMAL` | - Type of animal which the fosterer is currently fostering, or prefer to foster
- Case-sensitive
- Can only take the values shown in the example column | `current.Dog`, `current.Cat`, `able.Dog`, `able.Cat`, `nil` | +| `TAG` | Tag to be associated with the fosterer | `experienced`, `urgent` | + +
+ +**:bulb: Tip:**
+ +* A person can have any number of tags (including 0). +* `nil` can be indicated for `HOUSING_TYPE`, `AVAILABILITY`, `ANIMAL_NAME` and `TYPE_OF_ANIMAL` if that specific information is not currently available. + +
+ +
+ +**Valid cases**: + +| No. | Scenario | `AVAILABILITY` | `TYPE_OF_ANIMAL` | `ANIMAL_NAME` | +|-----|-------------------------------------------------------------|-------------|-------------------|---------------| +| 1 | Not fostering, insufficient info collected | `nil` | `nil` | `nil` | +| 2 | Not fostering, insufficient info collected | `Available` | `nil` | `nil` | +| 3 | Not fostering, preference indicated | `Available` | `able.Dog/Cat` | `nil` | +| 4 | Not fostering (e.g. overseas, currently not able to foster) | `NotAvailable` | `nil` | `nil` | +| 5 | Fostering: ALL information must be present | `NotAvailable` | `current.Dog/Cat` | NOT `nil` | + +For **invalid cases**, error messages will be shown when you enter the invalid commands. For example: +![Add](images/screenshots/AddErrorMessage.png) + +
+ +
+ +**:exclamation: Important:**
+ +You cannot add duplicate fosterers. This is detected using the fosterer's name.
+e.g. "Anne Tay" is the same person as "anne tay" and "anne (multiple spaces) tay". + +
+ +Examples: +* `add n/Jerry Tan p/98765412 e/jerry123@example.com a/Baker street, block 5, #27-01 housing/HDB availability/NotAvailable animal/Dexter animalType/current.Cat t/Urgent` + * adds a fosterer named Jerry Tan with the following details: +

+ ![Add](images/screenshots/AddJerry.png) +

+ +* `add n/Pete Tay p/98765411 e/pete@example.com a/Happy street, block 5, #27-01 housing/Condo availability/Available animal/nil animalType/able.Cat` + * adds a fosterer named Pete Tay with the following details: +

+ ![Add](images/screenshots/AddPete.png) + +
+ +In the case where duplicate field descriptions or values for `HOUSING_TYPE`, `AVAILABILITY`, `ANIMAL_NAME` and `TYPE_OF_ANIMAL` are given, the last one will be chosen: +* `add n/Jerry Tan p/98765412 e/jerry123@example.com a/Baker street, block 5, #27-01 housing/HDB housing/Condo availability/Available availability/NotAvailable animal/Dexter animal/Happy animalType/able.Dog animalType/current.Cat t/Urgent` + * adds a fosterer named Jerry Tan, who lives in a Condo and is fostering a cat named Happy. + ![Add](images/screenshots/AddDuplicate.png) + +
+ +### Adding a fosterer through the profile page: `add` + +Redirects you to an empty profile page with all the fields set to `nil`. In the profile page, you can key in the fosterer's details and save the +information to add the fosterer to your address book. + +Format: `add` + +
+ +**:information_source: Notes about the command:**
+ +* The restrictions imposed on what makes a valid fosterer, as explained in the section [Adding a fosterer through the main page: add](#adding-a-fosterer-through-the-main-page-add), still applies in this alternative way of adding a fosterer. +
+ +Here is the profile page you will see after entering add: + +
+ +
+ +
+ +To learn more about the profile page, please refer to the section [User Interface: The profile page](#the-profile-page). + +
+ +
+ +### Listing fosterers: `list` (Alias: `find`) + +Lists fosterers in your address book that match a particular description or search, or all fosterers if the search is blank. + +Format: `list *KEYWORDS` + +Alias: `find` + +
+ +**:information_source: Notes about the command:**
+ +* The keywords are case-insensitive. + +* The order of the keywords does not matter. e.g. `Hans Bo` will match `Bo Hans`. + +* All fields are searched (including tags). + +* Your keywords can overlap. e.g. `samm my` will match `Sammy`. + +* Fosterers must match all keywords (i.e. `AND` search).
+ e.g. `Hans Bo` will return `Hansbo Grahm`, but not `Hans Duo`. + +* You can use double quotes `"` for exact, case-sensitive, word-level match.
+ e.g. `"Tom"` matches "Tom", but not "Tommy". + +* Symbols between keywords or sections will combine them according to the function of the symbol.
+ +| Symbol / Operator | Description | Precedence | +|-------------------|--------------------------|------------| +| `&` | Logical AND | lowest | +| `/` | Logical OR | low | +| '` `' (space) | Logical AND | high | +| `(` and `)` | Parentheses for grouping | highest | + +e.g. `a & b / c d` is the same as `a & (b / (c & d))`. + +
+ +
+ +
+ +**:exclamation: Important:**
+ +For most fields, your **keywords can match as parts of words**.
+e.g. `john` will match `Johnny`.
+ +However, for the `Housing` and `Availability` fields, as well as for tags, your **keywords must match the entire field**.
+e.g. `available` will not match `NotAvailable`. + +
+ +Examples: +* `list` + * lists all fosterers in your address book. + +
+ +* `list john doe` + * lists fosterers that match "John Doe", "Doe John", "Johnny Doe", and "Mary" who lives on "John Doe Street". + +
+ +* `find john john doe` + * is redundant and gives the same result as `find john doe`. + +
+ +* `list "John" / zam & doe` + * lists fosterers which match "John Doe" and "Doe Shazam", but not "John Grahm". + +
-### Adding a person: `add` +
-Adds a person to the address book. +### Viewing a fosterer's detail: `view` -Format: `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]…​` +Redirects you to the profile page of the fosterer at the specified index of your currently displayed list. -
:bulb: **Tip:** -A person can have any number of tags (including 0) +Format: `view INDEX` + +Parameters: + +| Parameter | About | Example | +|-----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------| +| `INDEX` | - Index of a fosterer displayed in the list obtained from a `list`/`find` command
- Index must be a positive integer | `1`, `2`, `3` | + +
+ +**:exclamation: Important:**
+ +Only the save and exit commands are available to you in the profile page.
+ +To learn more about profile page, please refer to the section [User Interface: The profile page](#the-profile-page). + +
+ +Examples: +* `list` followed by `view 2` + * views the profile of the 2nd fosterer in your address book. + +
+ +
+ +### Editing a fosterer's details through the main page: `edit` + +Edits the details of the fosterer at the specified index in your currently displayed list, done through the main page. + +Format: `edit INDEX [n/NAME] [p/PHONE_NUMBER] [e/EMAIL] [a/ADDRESS] [housing/HOUSING_TYPE] [availability/AVAILABILITY] [animal/ANIMAL_NAME] [animalType/TYPE_OF_ANIMAL] [t/TAG…]` + +Parameters: + +| Parameter | About | Example | +|------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------| +| `INDEX` | - The index of a fosterer displayed in the list obtained from a `list`/`find` command
- Index must be a positive integer | `1`, `2`, `3` | +| `NAME` | Updated name of the fosterer | `Alice Tan`, `Harry Yeo` | +| `PHONE_NUMBER` | Updated Phone number of the fosterer | `93456778`, `89067547` | +| `EMAIL` | Updated email of the fosterer | `thomas718@gmail.com`, `kate@yahoo.com.sg` | +| `ADDRESS` | Updated address of the fosterer | `Orchard road, Blk 8, #13-04` | +| `HOUSING_TYPE` | - Updated housing type of the fosterer
- Case-sensitive
- Can only take the values shown in the example column | `HDB`, `Condo`, `Landed`, `nil` | +| `AVAILABILITY` | - Updated availability of the fosterer
- Case-sensitive
- Can only take the values shown in the example column | `NotAvailable`, `Available`, `nil` | +| `ANIMAL_NAME` | Updated name of animal fostered | `Fluffball`, `nil` | +| `TYPE_OF_ANIMAL` | - Updated type of animal which the fosterer is currently fostering, or prefer to foster
- Case-sensitive
- Can only take the values shown in the example column | `current.Dog`, `current.Cat`, `able.Dog`, `able.Cat`, `nil` | +| `TAG` | Tag to be associated with the fosterer | `experienced`, `urgent` | + +
+ +**:bulb: Tip:**
+ +* The index of the fosterer has to be provided. However, the number of parameters to be edited can vary from zero to all fields. + +
+ +
+ +**:exclamation: Important:**
+ +* If the parameters are not provided, edit INDEX operates the same way as view INDEX, leading you to the profile page of the person at index INDEX in the address book. +* If you run the **same** `edit` **command multiple times consecutively** (resulting in no visible change after the first run), the undo command _will not_ be able to revert the data back to the original state.
+ This is because `undo` can only undo the last command, _even if the command made no visible changes_.
+
+ 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` +* `find` or `list` followed by `edit 3 n/John` + * edits the name of the 3rd fosterer in your address book to "John". + +
+ +* `find` or `list` followed by `edit 1 p/12345678 animal/Bobby` + * edits the phone number and animal name of the 1st fosterer in your address book to "12345678" and "Bobby" respectively. + ![edit 1 example](images/screenshots/EditExample2.png)
+ +
+ +* `find` or `list` followed by `edit 2` + * opens the profile page of the 2nd fosterer in your address book, since parameters are not provided. + +
+ +**:warning: Caution:**
+ +**Edit for tags** in the main page **overwrites existing tags**.
+e.g. If the first fosterer has 2 tags : `experienced` and `reliable`, entering `edit 1 t/goodWithCats` will mean fosterer 1 will lose the `experienced` and `reliable` tags.
+ +This is not a problem when editing using the [profile page](#editing-a-fosterers-details-through-the-profile-page-edit).
-### Listing all persons : `list` +
+ +
+ +
-Shows a list of all persons in the address book. +### Editing a fosterer's details through the profile page: `edit` -Format: `list` +Edits the details of the fosterer at the specified index in your address book, by redirecting you to the profile page. -### Editing a person : `edit` +Format: `edit INDEX` -Edits an existing person in the address book. +Parameters: -Format: `edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAG]…​` +| Parameter | About | Example | +|-----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------| +| `INDEX` | - The index of a fosterer displayed in the list obtained from a `list`/`find` command
- Index must be a positive integer | `1`, `2`, `3` | + + +
-* 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, …​ -* 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. +**:information_source: Notes about the command:**
+ +* Apart from the details added by the add command, we also provide an optional **notes feature** in the profile page for +more flexibility. + +
+ + ![notes example](images/screenshots/Notes.png) + +* You can use this to include _additional details_ such as: + * Health condition of the animal + * Foster period of the animal + * Identifiable physical traits of the animal + +
+ +
+ +
+ +If you have at least one fosterer in your address book, here is an example of a profile page you will see after entering `view 1` or `edit 1`: + +
+ +
+ +
+ +To learn more about the profile page, please refer to the section [User Interface: The profile page](#the-profile-page). + +
+ +
+ +### Saving changes in a fosterer's details: `save` + +Saves changes to details of the fosterer which you have made in the profile page. + +
+ +**:exclamation: Important:**
+ +This command only works in the profile page, which you can navigate to by executing the `view` command. + +
+ +Format: `save` + +
+ +**:information_source: Notes about the command:**
+ +* Entering save in [the profile of a new fosterer](#adding-a-fosterer-through-the-profile-page-add) saves the new fosterer and exits the profile page. +* Entering save in [the profile of an already existing fosterer](#editing-a-fosterers-details-through-the-profile-page-edit) saves the changes but does not exit the profile page, allowing you to edit more details if needed. +
+ +
+ +
+ +### Deleting a fosterer : `delete` + +Deletes the fosterer at the specified index of your currently displayed list. + +Format: `delete INDEX [INDEX...]` + +
+ +**:warning: Caution:**
+ +The **index** of a fosterer **is not fixed**. It is relative to the current list of fosterers you are handling. + +
+ +Parameters: + +| Parameter | About | Example | +|------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------| +| `INDEX` | - The index of a fosterer displayed in the list obtained from a `list`/`find` command
- At least one index must be provided
- Index must be a positive integer | `1`, `2`, `3` | + +
+ +**:bulb: Tip:**
+ +You can **delete multiple fosterers at once**.
+ +* Each index needs to be separated by a white space.
+* Any duplicates and extra white spaces will be ignored. + +
+ +
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. +* `list` followed by `delete 2` + * deletes the 2nd fosterer in your address book. + +
+ +* `find Jerry` or `list Jerry`, followed by `delete 1` + * deletes the 1st fosterer in the result list of your `find` / `list` query -### Locating persons by name: `find` +
-Finds persons whose names contain any of the given keywords. +* `list` followed by `delete 1 3 7` + * deletes the 1st, 3rd and 7th fosterers in your address book. +
-Format: `find KEYWORD [MORE_KEYWORDS]` + ![Delete](images/screenshots/Delete.png) +In the example above, Alex, Bernice and Charlotte are the fosterers deleted. + +
-* 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` +* `list` followed by `delete 3 3 3 3` + * deletes the 3rd fosterer in your address book. + +
+ +
+ +### Sorting fosterers: `sort` + +Sorts your list of fosterers alphabetically by name, where uppercase letters come before lowercase letters +(i.e. `"annie tan"` will be sorted behind `"Jerry Tan"`). + +Format: `sort` + +![Sort](images/screenshots/Sort.png) + +
+ +
+ +**:exclamation: Important:**
+ +Repeated `sort` commands will not result in additional changes to the address book, and `undo` will not be able to revert the data back to its original state.
+This is because `undo` can only revert the last command, _even if the command made no changes to the address book_. + +
+ +
+ +### Viewing statistics of available fosterers : `stats avail` + +Helps you calculate statistics about fosterers who are available to foster, and the animals they can foster. Percentages are calculated to 2 decimal places. + +Format: `stats avail` + +
+ +**:information_source: Notes about the command:**
+ +* All statistic commands are **calculated based on the list currently displayed** in your address book.
+In the example below, `find available` was first entered, resulting in a list of 2 available fosterers.
+`stats avail` was then entered, and we see the resulting statistic reporting all listed fosterers as available.
+ +![Stats](images/screenshots/StatsAllAvail.png)
+ +Therefore, remember to use the `find` or `list` commands to get the list you want your statistics to be calculated from first, before using the statistic commands.
+ +
+ +
Examples: -* `find John` returns `john` and `John Doe` -* `find alex david` returns `Alex Yeoh`, `David Li`
- ![result for 'find alex david'](images/findAlexDavidResult.png) +* `list` followed by `stats avail` + * calculates statistics of available fosterers, taking into account all fosterers in your address book. + +
+ + ![Stats](images/screenshots/StatsAvail.png) + + In the example above, you have 6 fosterers in your address book, and 3 of them are available to foster. + +
+
+ + +* `find cat` followed by `stats avail` + * calculates statistics of available fosterers, only taking into account fosterers who are either currently fostering a cat or are able to foster a cat. -### Deleting a person : `delete` +
-Deletes the specified person from the address book. +
-Format: `delete INDEX` +### Viewing statistics of current fosterers : `stats current` +Helps you calculate statistics about fosterers who are currently fostering, and the type of animals they are fostering. Percentages are calculated to 2 decimal places. -* 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, …​ +Format: `stats current` 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. +* `list` followed by `stats current` + * calculates statistics of current fosterers, taking into account all fosterers in your address book. + +
+ + ![Stats](images/screenshots/StatsCurrent.png) + + In the example above, you have 6 fosterers in your address book, and 2 of them are currently fostering. + +
+
+ + +* `find dog` followed by `stats current` + * calculates statistics of current fosterers, only taking into account fosterers who are either currently fostering a dog, or are able to foster a dog. + +
+ +
+ +### Viewing statistics of housing types: `stats housing` +Helps you calculate statistics about the various housing types of fosterers. Percentages are calculated to 2 decimal places. + +Format: `stats housing` + +Examples: +* `list` followed by `stats housing` + * calculates housing statistics, taking into account all fosterers in your address book. + +
+ + ![Stats](images/screenshots/StatsHousing.png) + + In the example above, out of the 6 fosterers in your address book, 3 live in HDBs, 1 live in a Condo, and 2 live in Landed properties. + +
+
+ + +* `find available` followed by `stats housing` + * calculates housing statistics, only taking into account fosterers who are available. -### Clearing all entries : `clear` +
-Clears all entries from the address book. +
-Format: `clear` +### Undoing the previous command : `undo` -### Exiting the program : `exit` +Undoes your previous command, given that the previous command successfully executed is either `add`, `delete`, `edit`, `sort` or a successful execution of the `reset` command. -Exits the program. + +Format: `undo` + + +
+ +**:information_source: Notes about the command:**
+ +* The `undo` command can only be executed **once** at a time, and it will undo your last successful command. When the `undo` command is executed consecutively more than once, an error message will be shown: +![Undo](images/screenshots/UndoError.png) + +
+ +
+ +
+ +### Clearing all entries : `reset`, followed by `reset confirm` + + +Clears all your fosterer entries from the address book. + +Format: `reset`, followed by `reset confirm` + +
+ +**:information_source: Notes about the command:**
+ +* Upon entering `reset`, a confirmation message will be shown for the user to verify if he/she really wants to clear all the fosterer entries. +![Reset](images/screenshots/Reset.png) + + * User is prompted to enter `reset confirm` to confirm and execute the deletion of all fosterers. +![Reset](images/screenshots/ResetConfirm.png) + + * In the case where the user wishes to **cancel the reset**, he/she just has to proceed to type any other **valid command** in the command box. + + +
+ +
+ +
+ +### Exiting Foster Family : `exit` + +Exits the app. Format: `exit` -### 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. +**:information_source: Notes about the command:**
-### Editing the data file +On the profile page, entering exit leads you back to the main page, instead of exiting the app. -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. +Please refer to [User Interface: The profile page: Exiting the profile page](#exiting-the-profile-page) for more information. -
: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.
+ + +### Saving data + +In the **main page**, your Foster Family data is **saved in the hard disk automatically** after any command that changes the data, so no manual saving is needed. However, **edits made in the profile page have to be saved** via the `save` command. Else, changes will be discarded once you exit out of that fosterer's profile page. + +
+ +### Editing data file + +* Your Foster Family data is saved automatically as a JSON file with the file path
+`[JAR file location]/data/addressbook.json`.
-### Archiving data files `[coming in v2.0]` +* A JSON file stores fosterer details as key value pairs, making it more readable than a regular text file.
-_Details coming soon ..._ + + + In the example above, the "name" key is paired with a value "Alex Yeoh" for the first fosterer in your address book.
+ + +* We strongly advise you to update the data file directly **only if you are an advanced user**.
+ +* Otherwise, we highly recommend you to perform edits using our user-friendly interface instead.
+ +
+ +
+ +**:warning: Caution:**
+ +If your changes to the data file makes its **format invalid**, Foster Family will **discard all data** and start with an empty data file at the next run. Hence, it is recommended to make a **backup** of the file before editing it.
+ +
-------------------------------------------------------------------------------------------------------------------- -## FAQ +## **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 Foster Family 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 AddressBook home folder. +**Q**: How do I know which version of Java I am using / have installed on my computer?
+**A**: Open a command terminal, type `java -version` and press Enter. The Java version in use will be displayed as a response message. -------------------------------------------------------------------------------------------------------------------- -## Known issues +## **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. +- If you are **using multiple screens** and you move the app to a secondary screen, a part of the GUI may appear "off-screen" if you later choose to switch back to your primary screen.
+ To resolve this, you can delete the `preferences.json` file that was created, before running the app again. This file is located in the same home folder as your jar file. -------------------------------------------------------------------------------------------------------------------- -## 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` +
+ +## **Command Summary** + +| Action | Format | Examples | +|----------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **Help** | `help` | - | +| **Add** from main page | `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS housing/HOUSING_TYPE availability/AVAILABILITY animal/ANIMAL_NAME animalType/TYPE_OF_ANIMAL [t/TAG]…` | `add n/Jerry Tan p/98765412 e/jerry123@example.com a/Baker street, block 5, #27-01 housing/HDB availability/NotAvailable animal/Dexter animalType/current.Cat t/Urgent` | +| **Add** from profile page | `add` | - | +| **List** or **Find** | `list`, `find` | `list`, `find`, `list available`, `find available` | +| **View Profile** | `view INDEX` | `view 1` | +| **Save updated fosterer details** | `save` | - | +| **Edit** from main page | `edit INDEX [n/NAME] [p/PHONE_NUMBER] [e/EMAIL] [a/ADDRESS] [housing/HOUSING_TYPE] [availability/AVAILABILITY] [animal/ANIMAL_NAME] [animalType/TYPE_OF_ANIMAL] [t/TAG…]` | `edit 2 n/James Lee e/jameslee@example.com` | +| **Edit** from profile page | `edit INDEX` | `edit 1` | +| **Delete** | `delete INDEX [INDEX...]` | `delete 1 2 3` | +| **Sort** | `sort` | - | +| **View Available Fosterer Statistics** | `stats avail` | - | +| **View Current Fosterer Statistics** | `stats current` | - | +| **View Housing Statistics** | `stats housing` | - | +| **Undo** | `undo` | - | +| **Clear all data entries** | `reset`, followed by `reset confirm` | - | +| **Exit** from app / profile page | `exit` | - | diff --git a/docs/_config.yml b/docs/_config.yml index 6bd245d8f4e..3216d113cbf 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -1,4 +1,4 @@ -title: "AB-3" +title: "Foster Family" theme: minima header_pages: @@ -8,7 +8,7 @@ header_pages: markdown: kramdown -repository: "se-edu/addressbook-level3" +repository: "AY2324S1-CS2103T-T13-4/tp" github_icon: "images/github-icon.png" plugins: diff --git a/docs/_sass/minima/_base.scss b/docs/_sass/minima/_base.scss index 0d3f6e80ced..3a27e47add9 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: "Foster Family"; font-size: 32px; } } diff --git a/docs/diagrams/AddActivityDiagram.puml b/docs/diagrams/AddActivityDiagram.puml new file mode 100644 index 00000000000..ac5524f98d6 --- /dev/null +++ b/docs/diagrams/AddActivityDiagram.puml @@ -0,0 +1,28 @@ +@startuml +skin rose +skinparam ActivityFontSize 15 +skinparam ArrowFontSize 12 +start +:User executes add command; +:AddressBookParser parses the command; +:AddCommandParser parses the arguments provided in the add command; + +if () then ([Command is valid]) + :A new Person object with the specified attributes is created; + + if() then ([There exists invalid input/s or combination of inputs]) + :Display error message; + else ([else]) + :A new AddCommand object is created; + if() then ([Fosterer already exists in the address book]) + :Display error message; + else ([else]) + :Fosterer is added to the address book; + endif + endif + +else ([else]) + :Display error message; +endif +stop +@enduml diff --git a/docs/diagrams/AddSequenceDiagram.puml b/docs/diagrams/AddSequenceDiagram.puml new file mode 100644 index 00000000000..3954d4aacbd --- /dev/null +++ b/docs/diagrams/AddSequenceDiagram.puml @@ -0,0 +1,75 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant ":AddCommandParser" as AddCommandParser LOGIC_COLOR +participant "a:AddCommand" as AddCommand LOGIC_COLOR +participant ":CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ": Model" as Model MODEL_COLOR +participant "p:Person" as Person MODEL_COLOR +end box + +[-> LogicManager : execute(add command) +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand(add command) +activate AddressBookParser + +create AddCommandParser +AddressBookParser -> AddCommandParser +activate AddCommandParser + +AddCommandParser --> AddressBookParser +deactivate AddCommandParser + +AddressBookParser -> AddCommandParser : parse(arguments) +activate AddCommandParser + +create Person +AddCommandParser -> Person : new Person(attributes) +activate Person +return p + +create AddCommand +AddCommandParser -> AddCommand : new AddCommand(p) +activate AddCommand + +AddCommand --> AddCommandParser : a +deactivate AddCommand + +AddCommandParser --> AddressBookParser : a +deactivate AddCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +AddCommandParser -[hidden]-> AddressBookParser +destroy AddCommandParser + +AddressBookParser --> LogicManager : a +deactivate AddressBookParser + +LogicManager -> AddCommand : execute() +activate AddCommand + +AddCommand -> Model : addPerson(p) +activate Model +Model --> AddCommand +deactivate Model + +create CommandResult +AddCommand -> CommandResult +activate CommandResult + +CommandResult --> AddCommand +deactivate CommandResult + +AddCommand --> LogicManager : result +deactivate AddCommand + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/BetterModelClassDiagram.puml b/docs/diagrams/BetterModelClassDiagram.puml index 598474a5c82..abf505a0fa3 100644 --- a/docs/diagrams/BetterModelClassDiagram.puml +++ b/docs/diagrams/BetterModelClassDiagram.puml @@ -18,4 +18,7 @@ Person *--> Name Person *--> Phone Person *--> Email Person *--> Address +Person *--> Housing +Person *--> Availability +Person *--> AnimalType @enduml diff --git a/docs/diagrams/DeleteMultipleSequenceDiagram.puml b/docs/diagrams/DeleteMultipleSequenceDiagram.puml new file mode 100644 index 00000000000..de9a9038387 --- /dev/null +++ b/docs/diagrams/DeleteMultipleSequenceDiagram.puml @@ -0,0 +1,71 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant ":DeleteCommandParser" as DeleteCommandParser LOGIC_COLOR +participant "d:DeleteCommand" as DeleteCommand LOGIC_COLOR +participant ":CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +end box + +[-> LogicManager : execute("delete 1 2 3") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("delete 1 2 3") +activate AddressBookParser + +create DeleteCommandParser +AddressBookParser -> DeleteCommandParser +activate DeleteCommandParser + +DeleteCommandParser --> AddressBookParser +deactivate DeleteCommandParser + +AddressBookParser -> DeleteCommandParser : parse("1 2 3") +activate DeleteCommandParser + +create DeleteCommand +DeleteCommandParser -> DeleteCommand +activate DeleteCommand + +DeleteCommand --> DeleteCommandParser : d +deactivate DeleteCommand + +DeleteCommandParser --> AddressBookParser : d +deactivate DeleteCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +DeleteCommandParser -[hidden]-> AddressBookParser +destroy DeleteCommandParser + +AddressBookParser --> LogicManager : d +deactivate AddressBookParser + +LogicManager -> DeleteCommand : execute() +activate DeleteCommand + +loop until all selected persons are deleted + DeleteCommand -> Model : deletePerson(person) + activate Model + Model --> DeleteCommand + deactivate Model +end + +create CommandResult +DeleteCommand -> CommandResult +activate CommandResult + +CommandResult --> DeleteCommand +deactivate CommandResult + +DeleteCommand --> LogicManager : result +deactivate DeleteCommand + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/EditFieldSequenceDiagram.puml b/docs/diagrams/EditFieldSequenceDiagram.puml new file mode 100644 index 00000000000..abf1c8c70fb --- /dev/null +++ b/docs/diagrams/EditFieldSequenceDiagram.puml @@ -0,0 +1,74 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box UI UI_COLOR_T1 +participant ":MainWindow" as MainWindow UI_COLOR +participant ":PersonProfile" as PersonProfile UI_COLOR +end box + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":ViewModeParser" as ViewModeParser LOGIC_COLOR +participant "e:EditFieldCommand" as EditFieldCommand LOGIC_COLOR +participant "commandResult:CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +end box + +[-> MainWindow : executeCommand("name") +activate MainWindow + +MainWindow -> PersonProfile : getPerson() +activate PersonProfile + +PersonProfile --> MainWindow : get personToEdit +deactivate PersonProfile + +MainWindow -> LogicManager : executeInView("name", personToEdit, targetIndex) +activate LogicManager + +LogicManager -> ViewModeParser : parseCommand("name", personToEdit, targetIndex) +activate ViewModeParser + +create EditFieldCommand +ViewModeParser -> EditFieldCommand +activate EditFieldCommand + +EditFieldCommand --> ViewModeParser : e +deactivate EditFieldCommand + +ViewModeParser --> LogicManager : e +deactivate ViewModeParser + +LogicManager -> EditFieldCommand : execute() +activate EditFieldCommand + +create CommandResult +EditFieldCommand -> CommandResult +activate CommandResult + +CommandResult --> EditFieldCommand : +deactivate CommandResult + + +EditFieldCommand --> LogicManager : commandResult +deactivate EditFieldCommand + +LogicManager --> MainWindow : commandResult +deactivate LogicManager + +MainWindow -> MainWindow : handleEditField() +activate MainWindow + +group ref [Profile Edit Sequence Diagram] + MainWindow -[hidden]-> PersonProfile +end + +deactivate MainWindow + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/EditFieldSequenceDiagramStep1.puml b/docs/diagrams/EditFieldSequenceDiagramStep1.puml new file mode 100644 index 00000000000..84afd2d621b --- /dev/null +++ b/docs/diagrams/EditFieldSequenceDiagramStep1.puml @@ -0,0 +1,23 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box UI UI_COLOR_T1 +participant ":MainWindow" as MainWindow UI_COLOR +participant ":PersonProfile" as PersonProfile UI_COLOR +end box + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +end box + + +[-> MainWindow : executeCommand("name") + +MainWindow -> PersonProfile : getPerson() + +PersonProfile --> MainWindow : get personToEdit + +MainWindow -> LogicManager : executeInView("name", personToEdit, targetIndex) + +@enduml diff --git a/docs/diagrams/EditFieldSequenceDiagramStep2.puml b/docs/diagrams/EditFieldSequenceDiagramStep2.puml new file mode 100644 index 00000000000..c09ba7f157f --- /dev/null +++ b/docs/diagrams/EditFieldSequenceDiagramStep2.puml @@ -0,0 +1,37 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box UI UI_COLOR_T1 +participant ":MainWindow" as MainWindow UI_COLOR +participant ":PersonProfile" as PersonProfile UI_COLOR +end box + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":ViewModeParser" as ViewModeParser LOGIC_COLOR +participant "e:EditFieldCommand" as EditFieldCommand LOGIC_COLOR +end box + + +[-> MainWindow : executeCommand("name") + +MainWindow -> PersonProfile : getPerson() + +PersonProfile --> MainWindow : get personToEdit + +MainWindow -> LogicManager : executeInView("name", personToEdit, targetIndex) + +LogicManager -> ViewModeParser : parseCommand("name", personToEdit, targetIndex) + +create EditFieldCommand +ViewModeParser -> EditFieldCommand +activate EditFieldCommand + +EditFieldCommand --> ViewModeParser : e +deactivate EditFieldCommand + +ViewModeParser --> LogicManager : e +deactivate ViewModeParser + +@enduml diff --git a/docs/diagrams/FindCommandClassDiagram.puml b/docs/diagrams/FindCommandClassDiagram.puml new file mode 100644 index 00000000000..033a3de8922 --- /dev/null +++ b/docs/diagrams/FindCommandClassDiagram.puml @@ -0,0 +1,44 @@ +@startuml +!include style.puml +skinparam arrowThickness 1.1 +skinparam classBackgroundColor UI_COLOR + +package search <> { + Class FindCommandArgumentParser + Class SearchPredicate + Class "{abstract}\nSearchMatcher" as SearchMatcher + Class "{abstract}\nBinarySearchMatcher" as BinarySearchMatcher + Class AndSearchMatcher + Class OrSearchMatcher + Class NotSearchMatcher + Class SingleTextSearchMatcher + Class FieldRanges + Class Range +} + +package parser <> { + Class FindCommandParser +} + +package commands <> { + Class FindCommand +} + +FindCommandParser .> FindCommandArgumentParser +FindCommandParser .> FindCommand +FindCommandArgumentParser .> SearchPredicate + +FindCommand *--> SearchPredicate +SearchPredicate *--> SearchMatcher + +FieldRanges *-right-> Range + +SearchMatcher .> FieldRanges +SearchMatcher <|-- BinarySearchMatcher +SearchMatcher <|-- SingleTextSearchMatcher +SearchMatcher <|-- NotSearchMatcher + +BinarySearchMatcher <|-- AndSearchMatcher +BinarySearchMatcher <|-- OrSearchMatcher + +@enduml diff --git a/docs/diagrams/FindCommandSequenceDiagram.puml b/docs/diagrams/FindCommandSequenceDiagram.puml new file mode 100644 index 00000000000..1e42339454f --- /dev/null +++ b/docs/diagrams/FindCommandSequenceDiagram.puml @@ -0,0 +1,69 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +Actor User as user USER_COLOR +Participant ":MainWindow" as mw UI_COLOR +Participant ":LogicManager" as lm LOGIC_COLOR +Participant ":AddressBookParser" as abp LOGIC_COLOR_T2 +Participant ":FindCommandParser" as fcp LOGIC_COLOR_T3 +Participant ":FindCommandArgumentParser" as argp LOGIC_COLOR_T4 +Participant "cmd:FindCommand" as cmd MODEL_COLOR +Participant ":Model" as model STORAGE_COLOR + +user -[USER_COLOR]> mw : "list Tom/Sam" +activate mw UI_COLOR + +mw -[UI_COLOR]> lm : execute +activate lm LOGIC_COLOR + +lm -[LOGIC_COLOR]> abp : parseCommand("list Tom/Sam") +activate abp LOGIC_COLOR_T2 + +create fcp +abp -[LOGIC_COLOR_T2]> fcp + +abp -[LOGIC_COLOR_T2]> fcp : parse("Tom/Sam") +activate fcp LOGIC_COLOR_T3 + +create argp +fcp -[LOGIC_COLOR_T3]> argp + +fcp -[LOGIC_COLOR_T3]> argp : parse("Tom/Sam") +activate argp LOGIC_COLOR_T4 + +argp --[LOGIC_COLOR_T4]-> fcp : SearchPredicate +deactivate argp LOGIC_COLOR_T4 +argp -[hidden]-> fcp +destroy argp + +create cmd +fcp -[LOGIC_COLOR_T3]> cmd + +fcp --[LOGIC_COLOR_T3]-> abp : cmd +deactivate fcp LOGIC_COLOR_T3 +fcp -[hidden]-> abp +destroy fcp + +abp --[LOGIC_COLOR_T2]-> lm : cmd +deactivate abp LOGIC_COLOR_T2 + +lm -[LOGIC_COLOR]> cmd : execute(Model) +activate cmd MODEL_COLOR + +cmd -[MODEL_COLOR]> model : updateFilteredPersonList(SearchPredicate) + +cmd --[MODEL_COLOR]-> lm : CommandResult +deactivate cmd MODEL_COLOR + +cmd -[hidden]-> lm +destroy cmd + +lm --[LOGIC_COLOR]-> mw +deactivate lm LOGIC_COLOR + +mw --[UI_COLOR]-> user +deactivate mw + + +@enduml diff --git a/docs/diagrams/FindPredicateExampleObjectDiagram.puml b/docs/diagrams/FindPredicateExampleObjectDiagram.puml new file mode 100644 index 00000000000..6f3c80bc424 --- /dev/null +++ b/docs/diagrams/FindPredicateExampleObjectDiagram.puml @@ -0,0 +1,22 @@ +@startuml + +object SearchPredicate +object "and:AndSearchMatcher" as and +object "john:SingleTextSearchMatcher" as john { + textToFind = "John" +} +object "or:OrSearchMatcher" as or +object "doe:SingleTextSearchMatcher" as doe { + textToFind = "Doe" +} +object "mae:SingleTextSearchMatcher" as mae { + textToFind = "Mae" +} + +SearchPredicate --> and +and -down-> john : a +and -down-> or : b +or -down-> doe : a +or -down-> mae : b + +@enduml diff --git a/docs/diagrams/FindPredicateExampleSequenceDiagram.puml b/docs/diagrams/FindPredicateExampleSequenceDiagram.puml new file mode 100644 index 00000000000..daebede67c1 --- /dev/null +++ b/docs/diagrams/FindPredicateExampleSequenceDiagram.puml @@ -0,0 +1,54 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +Participant "SearchPredicate" as sp LOGIC_COLOR +Participant "Person" as person MODEL_COLOR +Participant "and:AndSearchMatcher" as and LOGIC_COLOR_T2 +Participant "john:SingleTextSearchMatcher" as john LOGIC_COLOR_T3 +Participant "or:OrSearchMatcher" as or LOGIC_COLOR_T3 +Participant "doe:SingleTextSearchMatcher" as doe LOGIC_COLOR_T4 +Participant "mae:SingleTextSearchMatcher" as mae LOGIC_COLOR_T4 + +create sp +[-[USER_COLOR]> sp : new(SearchMatcher) + +[-[USER_COLOR]> sp : test(Person) +activate sp LOGIC_COLOR + +sp -[LOGIC_COLOR]> person : getFieldsAndAttributes() +activate person MODEL_COLOR + +person --[MODEL_COLOR]-> sp : Map +deactivate person MODEL_COLOR + +sp -[LOGIC_COLOR]> and : test(Map) +activate and LOGIC_COLOR_T2 + +and -[LOGIC_COLOR_T2]> john : test(Map) +activate john LOGIC_COLOR_T3 +john --[LOGIC_COLOR_T3]-> and : FieldRanges +deactivate john LOGIC_COLOR_T3 + +and -[LOGIC_COLOR_T2]> or : test(Map) +activate or LOGIC_COLOR_T3 + +or -[LOGIC_COLOR_T3]> doe : test(Map) +activate doe LOGIC_COLOR_T4 +doe --[LOGIC_COLOR_T4]-> or : FieldRanges +deactivate doe LOGIC_COLOR_T4 + +or -[LOGIC_COLOR_T3]> mae : test(Map) +activate mae LOGIC_COLOR_T4 +mae --[LOGIC_COLOR_T4]-> or : FieldRanges +deactivate mae LOGIC_COLOR_T4 + +or --[LOGIC_COLOR_T3]-> and : FieldRanges +deactivate or LOGIC_COLOR_T3 + +and --[LOGIC_COLOR_T2]-> sp : FieldRanges +deactivate and LOGIC_COLOR_T2 + +[<-[USER_COLOR]- sp : boolean + +@enduml diff --git a/docs/diagrams/FindSearchPredicateSequenceDiagram.puml b/docs/diagrams/FindSearchPredicateSequenceDiagram.puml new file mode 100644 index 00000000000..52dfd12f786 --- /dev/null +++ b/docs/diagrams/FindSearchPredicateSequenceDiagram.puml @@ -0,0 +1,29 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +Participant "SearchPredicate" as sp LOGIC_COLOR +Participant "Person" as person MODEL_COLOR +Participant "SearchMatcher" as sm LOGIC_COLOR_T2 + +create sp +[-[USER_COLOR]> sp : new(SearchMatcher) + +[-[USER_COLOR]> sp : test(Person) +activate sp LOGIC_COLOR + +sp -[LOGIC_COLOR]> person : getFieldsAndAttributes() +activate person MODEL_COLOR + +person --[MODEL_COLOR]-> sp : Map +deactivate person MODEL_COLOR + +sp -[LOGIC_COLOR]> sm : test(Map) +activate sm LOGIC_COLOR_T2 + +sm --[LOGIC_COLOR_T2]-> sp : FieldRanges +deactivate sm LOGIC_COLOR_T2 + +[<-[USER_COLOR]- sp : boolean + +@enduml diff --git a/docs/diagrams/HandleSaveSequenceDiagram.puml b/docs/diagrams/HandleSaveSequenceDiagram.puml new file mode 100644 index 00000000000..420e7da9351 --- /dev/null +++ b/docs/diagrams/HandleSaveSequenceDiagram.puml @@ -0,0 +1,18 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +mainframe **sd** Handle Save +participant "MainWindow" as MainWindow UI_COLOR +participant "PersonProfile" as PersonProfile UI_COLOR + +MainWindow -> MainWindow : handleSave() +activate MainWindow +MainWindow -> MainWindow : resetValues() +activate MainWindow +MainWindow -> PersonProfile : resetValues() +deactivate MainWindow +[<--MainWindow +deactivate MainWindow + +@enduml diff --git a/docs/diagrams/HandleViewExitSequenceDiagram.puml b/docs/diagrams/HandleViewExitSequenceDiagram.puml new file mode 100644 index 00000000000..924d071c7ab --- /dev/null +++ b/docs/diagrams/HandleViewExitSequenceDiagram.puml @@ -0,0 +1,32 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +mainframe **sd** Handle View Exit +participant "MainWindow" as MainWindow UI_COLOR +participant "PersonProfile" as PersonProfile UI_COLOR +participant "CommandBox" as CommandBox UI_COLOR + +MainWindow -> MainWindow : handleViewExit() +activate MainWindow + +MainWindow -> CommandBox : getInConfirmationDialog() +activate CommandBox +CommandBox --> MainWindow: isShowingConfirmationMessage +deactivate CommandBox + +alt isSaved || isShowingConfirmationMessage + MainWindow -> MainWindow : exitProfilePage() + activate MainWindow + MainWindow --> MainWindow + deactivate MainWindow +else + MainWindow -> MainWindow : displayConfirmationMessage() + activate MainWindow + MainWindow --> MainWindow + deactivate MainWindow + MainWindow -[hidden]->] + deactivate MainWindow +end + +@enduml diff --git a/docs/diagrams/IsInViewModeSequenceDiagram.puml b/docs/diagrams/IsInViewModeSequenceDiagram.puml new file mode 100644 index 00000000000..865052164db --- /dev/null +++ b/docs/diagrams/IsInViewModeSequenceDiagram.puml @@ -0,0 +1,33 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box UI UI_COLOR_T1 +participant ":MainWindow" as MainWindow UI_COLOR +participant ":PersonProfile" as PersonProfile UI_COLOR +end box + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":ViewModeParser" as ViewModeParser LOGIC_COLOR +participant ":AddressbookParser" as AddressbookParser LOGIC_COLOR +end box + +[-> MainWindow : executeCommand(commandText) + +alt personListPanelPlaceholder is visible + MainWindow -> LogicManager : execute(commandText) + LogicManager -> AddressbookParser : parseCommand(commandText) +else else + MainWindow -> PersonProfile : getPerson() + PersonProfile --> MainWindow : get personToEdit + MainWindow -> LogicManager : executeInView(commandText, personToEdit, targetIndex) + LogicManager -> ViewModeParser : parseCommand(commandText, personToEdit, targetIndex) +end + + + + +deactivate MainWindow + +@enduml diff --git a/docs/diagrams/LogicClassDiagram.puml b/docs/diagrams/LogicClassDiagram.puml index a57720890ee..f15032595dd 100644 --- a/docs/diagrams/LogicClassDiagram.puml +++ b/docs/diagrams/LogicClassDiagram.puml @@ -7,10 +7,11 @@ skinparam classBackgroundColor LOGIC_COLOR package Logic as LogicPackage { Class AddressBookParser +Class ViewModeParser Class XYZCommand Class CommandResult Class "{abstract}\nCommand" as Command - +Class "<>\nCommandType" as CommandType Class "<>\nLogic" as Logic Class LogicManager @@ -27,11 +28,16 @@ Class HiddenOutside #FFFFFF HiddenOutside ..> Logic LogicManager .right.|> Logic -LogicManager -right->"1" AddressBookParser -AddressBookParser ..> XYZCommand : creates > +LogicManager -down->"1" AddressBookParser +LogicManager -down->"1" ViewModeParser +LogicManager .down.> CommandType + +AddressBookParser .down..> XYZCommand : creates > +ViewModeParser .down..> XYZCommand : creates > +ViewModeParser .right.> Model XYZCommand -up-|> Command -LogicManager .left.> Command : executes > +LogicManager .down..> Command : executes > LogicManager --> Model LogicManager --> Storage @@ -41,6 +47,7 @@ Command .right.> Model note right of XYZCommand: XYZCommand = AddCommand, \nFindCommand, etc Logic ..> CommandResult -LogicManager .down.> CommandResult +LogicManager .down..> CommandResult Command .up.> CommandResult : produces > +CommandResult .right.>"0..1" CommandType @enduml diff --git a/docs/diagrams/MainWindowCommandTypeSequenceDiagram.puml b/docs/diagrams/MainWindowCommandTypeSequenceDiagram.puml new file mode 100644 index 00000000000..539c226b4f3 --- /dev/null +++ b/docs/diagrams/MainWindowCommandTypeSequenceDiagram.puml @@ -0,0 +1,31 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box UI UI_COLOR_T1 +participant ":MainWindow" as MainWindow UI_COLOR +participant ":PersonProfile" as PersonProfile UI_COLOR +end box + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +end box + +LogicManager --> MainWindow : commandResult +deactivate LogicManager + +alt commandResult.getCommandType == CommandType.HELP + MainWindow -> MainWindow : handleHelp() +else commandResult.getCommandType == CommandType.EXIT + MainWindow -> MainWindow : handleExit() +else commandResult.getCommandType == CommandType.VIEW + MainWindow -> MainWindow : handleView() +else commandResult.getCommandType == CommandType.VIEW_EXIT + MainWindow -> MainWindow : handleViewExit() +else commandResult.getCommandType == CommandType.EDIT_FIELD + MainWindow -> MainWindow : handleEditField() +else commandResult.getCommandType == CommandType.SAVE + MainWindow -> MainWindow : handleSave() +end + +@enduml diff --git a/docs/diagrams/ModelClassDiagram.puml b/docs/diagrams/ModelClassDiagram.puml index 0de5673070d..c209533228d 100644 --- a/docs/diagrams/ModelClassDiagram.puml +++ b/docs/diagrams/ModelClassDiagram.puml @@ -19,6 +19,9 @@ Class Email Class Name Class Phone Class Tag +Class Availability +Class Housing +Class AnimalType Class I #FFFFFF } @@ -36,12 +39,15 @@ ModelManager -right-> "1" UserPrefs UserPrefs .up.|> ReadOnlyUserPrefs AddressBook *--> "1" UniquePersonList -UniquePersonList --> "~* all" Person -Person *--> Name -Person *--> Phone -Person *--> Email -Person *--> Address +Person *--> "1..2"Name +Person *--> "1"Phone +Person *--> "1"Email +Person *--> "1"Address Person *--> "*" Tag +Person *--> "1 " Availability +Person *--> "1" Housing +Person *--> "1" AnimalType +AnimalType --> "1" Availability Person -[hidden]up--> I UniquePersonList -[hidden]right-> I @@ -50,5 +56,8 @@ Name -[hidden]right-> Phone Phone -[hidden]right-> Address Address -[hidden]right-> Email -ModelManager --> "~* filtered" Person +ModelManager --> "~* Filtered"Person +UniquePersonList -left-> "~*all " Person + + @enduml diff --git a/docs/diagrams/ParserClasses.puml b/docs/diagrams/ParserClasses.puml index 0c7424de6e0..d77b65cd8ed 100644 --- a/docs/diagrams/ParserClasses.puml +++ b/docs/diagrams/ParserClasses.puml @@ -4,12 +4,11 @@ skinparam arrowThickness 1.1 skinparam arrowColor LOGIC_COLOR_T4 skinparam classBackgroundColor LOGIC_COLOR -Class "{abstract}\nCommand" as Command -Class XYZCommand - +package "Logic" { package "Parser classes"{ Class "<>\nParser" as Parser Class AddressBookParser +Class ViewModeParser Class XYZCommandParser Class CliSyntax Class ParserUtil @@ -18,13 +17,31 @@ Class ArgumentTokenizer Class Prefix } +Class "{abstract}\nCommand" as Command +Class XYZCommand +} + + +package "Model" { +Class Person +} + + +package "Commons" { +Class Index +} + Class HiddenOutside #FFFFFF HiddenOutside ..> AddressBookParser +HiddenOutside ..> ViewModeParser -AddressBookParser .down.> XYZCommandParser: creates > +AddressBookParser ..> XYZCommandParser: creates > +ViewModeParser ..> Index +ViewModeParser ..> Person XYZCommandParser ..> XYZCommand : creates > AddressBookParser ..> Command : returns > +ViewModeParser ..> Command : returns > XYZCommandParser .up.|> Parser XYZCommandParser ..> ArgumentMultimap XYZCommandParser ..> ArgumentTokenizer diff --git a/docs/diagrams/ProfileEditSequenceDiagram.puml b/docs/diagrams/ProfileEditSequenceDiagram.puml new file mode 100644 index 00000000000..78b80011040 --- /dev/null +++ b/docs/diagrams/ProfileEditSequenceDiagram.puml @@ -0,0 +1,80 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +Actor User as user USER_COLOR +Participant ":MainWindow" as mw UI_COLOR +Participant ":PersonProfile" as profile LOGIC_COLOR +Participant "email:PersonProfileField" as field MODEL_COLOR + + + +user -[USER_COLOR]> mw : "mail" +activate mw UI_COLOR + +mw -[UI_COLOR]> profile : setFocus(Field.EMAIL) +activate profile LOGIC_COLOR + +profile -[LOGIC_COLOR]> field : setFocus() +activate field MODEL_COLOR + +field -[MODEL_COLOR]> field : updateState +activate field MODEL_COLOR_T1 +field --[MODEL_COLOR]-> field +deactivate field MODEL_COLOR_T1 + +field --[MODEL_COLOR]-> profile +deactivate field MODEL_COLOR + +profile --[LOGIC_COLOR]-> mw +deactivate profile + +mw --[UI_COLOR]-> user +deactivate mw + + + +user -[USER_COLOR]> field : "" +activate field MODEL_COLOR + +field -[MODEL_COLOR]> field : confirmIfValid +activate field MODEL_COLOR_T1 + +field -[MODEL_COLOR_T1]> profile : updateField +activate profile LOGIC_COLOR +profile --[LOGIC_COLOR]-> field +deactivate profile + +field -[MODEL_COLOR_T1]> profile : triggerEvent(AFTER_CONFIRM) +activate profile LOGIC_COLOR + +profile -[LOGIC_COLOR]> profile : handleFieldLockIn +activate profile LOGIC_COLOR_T1 + +profile -[LOGIC_COLOR_T1]> profile : createAndUpdatePerson +activate profile LOGIC_COLOR_T2 +profile --[LOGIC_COLOR_T2]-> profile +deactivate profile LOGIC_COLOR_T2 + +profile -[LOGIC_COLOR_T1]> profile : sendPersonCreated +activate profile LOGIC_COLOR_T2 + + +profile -[LOGIC_COLOR_T2]> mw : sendFeedback + +profile --[LOGIC_COLOR_T2]-> profile +deactivate profile LOGIC_COLOR_T2 + +profile --[LOGIC_COLOR_T1]-> profile +deactivate profile + +profile --[LOGIC_COLOR]-> field +deactivate profile + +field --[MODEL_COLOR_T1]-> field +deactivate field MODEL_COLOR_T1 + +field --[MODEL_COLOR]-> user +deactivate field + +@enduml diff --git a/docs/diagrams/ResetActivityDiagram.puml b/docs/diagrams/ResetActivityDiagram.puml new file mode 100644 index 00000000000..ef51149ba49 --- /dev/null +++ b/docs/diagrams/ResetActivityDiagram.puml @@ -0,0 +1,26 @@ +@startuml +skin rose +skinparam ActivityFontSize 15 +skinparam ArrowFontSize 12 +start +:User executes a command; +:AddressBookParser parses the command; + +if () then ([Command is "reset"]) + + :Set finalConfirmation to TRUE; + +else ([else]) + if() then ([Command is "reset confirm"]) + if() then ([finalConfirmation == TRUE]) + :Reset the entire address book; + else([else]) + :Display message to user to enter "reset" first; + endif + else ([else]) + :Display error message; + :Set finalConfirmation to FALSE; + endif +endif +stop +@enduml diff --git a/docs/diagrams/ResetConfirmSequenceDiagram.puml b/docs/diagrams/ResetConfirmSequenceDiagram.puml new file mode 100644 index 00000000000..f5a23c11166 --- /dev/null +++ b/docs/diagrams/ResetConfirmSequenceDiagram.puml @@ -0,0 +1,70 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant ":ClearCommandParser" as ClearCommandParser LOGIC_COLOR +participant "d:ClearCommand" as ClearCommand LOGIC_COLOR +participant ":CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +end box + +[-> LogicManager : execute("reset confirm") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("reset confirm") +activate AddressBookParser + +create ClearCommandParser +AddressBookParser -> ClearCommandParser +activate ClearCommandParser + +ClearCommandParser --> AddressBookParser +deactivate ClearCommandParser + +AddressBookParser -> ClearCommandParser : parse("confirm") +activate ClearCommandParser + +create ClearCommand +ClearCommandParser -> ClearCommand +activate ClearCommand + +ClearCommand --> ClearCommandParser : d +deactivate ClearCommand + +ClearCommandParser --> AddressBookParser : d +deactivate ClearCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +ClearCommandParser -[hidden]-> AddressBookParser +destroy ClearCommandParser + +AddressBookParser --> LogicManager : d +deactivate AddressBookParser + +LogicManager -> ClearCommand : execute() +activate ClearCommand + +ClearCommand -> Model : setAddressBook(new AddressBook()) +activate Model + +Model --> ClearCommand +deactivate Model + +create CommandResult +ClearCommand -> CommandResult +activate CommandResult + +CommandResult --> ClearCommand +deactivate CommandResult + +ClearCommand --> LogicManager : result +deactivate ClearCommand + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/ResetSequenceDiagram.puml b/docs/diagrams/ResetSequenceDiagram.puml new file mode 100644 index 00000000000..ac347d63770 --- /dev/null +++ b/docs/diagrams/ResetSequenceDiagram.puml @@ -0,0 +1,64 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant ":ClearCommandParser" as ClearCommandParser LOGIC_COLOR +participant "d:ClearCommand" as ClearCommand LOGIC_COLOR +participant ":CommandResult" as CommandResult LOGIC_COLOR +end box + + +[-> LogicManager : execute("reset") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("reset") +activate AddressBookParser + +create ClearCommandParser +AddressBookParser -> ClearCommandParser +activate ClearCommandParser + +ClearCommandParser --> AddressBookParser +deactivate ClearCommandParser + +AddressBookParser -> ClearCommandParser : parse("") +activate ClearCommandParser + +create ClearCommand +ClearCommandParser -> ClearCommand +activate ClearCommand + +ClearCommand --> ClearCommandParser : d +deactivate ClearCommand + +ClearCommandParser --> AddressBookParser : d +deactivate ClearCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +ClearCommandParser -[hidden]-> AddressBookParser +destroy ClearCommandParser + +AddressBookParser --> LogicManager : d +deactivate AddressBookParser + +LogicManager -> ClearCommand : execute() +activate ClearCommand + + + +create CommandResult +ClearCommand -> CommandResult +activate CommandResult + +CommandResult --> ClearCommand +deactivate CommandResult + +ClearCommand --> LogicManager : result +deactivate ClearCommand + +[<--LogicManager +deactivate LogicManager +@enduml + diff --git a/docs/diagrams/SaveSequenceDiagram.puml b/docs/diagrams/SaveSequenceDiagram.puml new file mode 100644 index 00000000000..2b2957b6d07 --- /dev/null +++ b/docs/diagrams/SaveSequenceDiagram.puml @@ -0,0 +1,86 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box UI UI_COLOR_T1 +participant ":MainWindow" as MainWindow UI_COLOR +participant ":PersonProfile" as PersonProfile UI_COLOR +end box + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":ViewModeParser" as ViewModeParser LOGIC_COLOR +participant "s:SaveCommand" as SaveCommand LOGIC_COLOR +participant "commandResult:CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +end box + +[-> MainWindow : executeCommand("save") +activate MainWindow + +MainWindow -> PersonProfile : getPerson() +activate PersonProfile + +PersonProfile --> MainWindow : get personToEdit +deactivate PersonProfile + +MainWindow -> LogicManager : executeInView("save", personToEdit, targetIndex) +activate LogicManager + +LogicManager -> ViewModeParser : parseCommand("save", personToEdit, targetIndex) +activate ViewModeParser + +create SaveCommand +ViewModeParser -> SaveCommand +activate SaveCommand + +SaveCommand --> ViewModeParser : s +deactivate SaveCommand + +ViewModeParser --> LogicManager : s +deactivate ViewModeParser + +LogicManager -> SaveCommand : execute() +activate SaveCommand + +SaveCommand -> Model : setPerson(personToEdit, targetIndex) +activate Model + +SaveCommand -> Model : updateFilteredPersonList(); + +Model --> SaveCommand +deactivate Model + +create CommandResult +SaveCommand -> CommandResult +activate CommandResult + +CommandResult --> SaveCommand : +deactivate CommandResult + + +SaveCommand --> LogicManager : commandResult +deactivate SaveCommand + +LogicManager --> MainWindow : commandResult +deactivate LogicManager + +MainWindow -> CommandResult : getIsFostererEdited() +activate CommandResult + +CommandResult --> MainWindow : isSaved +deactivate CommandResult + +group ref [Handle Save] + MainWindow -[hidden]-> PersonProfile +end + +deactivate MainWindow + +[<--MainWindow +deactivate MainWindow + +@enduml diff --git a/docs/diagrams/SaveSequenceDiagramStep1.puml b/docs/diagrams/SaveSequenceDiagramStep1.puml new file mode 100644 index 00000000000..8e6d1a8e8bf --- /dev/null +++ b/docs/diagrams/SaveSequenceDiagramStep1.puml @@ -0,0 +1,21 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box UI UI_COLOR_T1 +participant ":MainWindow" as MainWindow UI_COLOR +participant ":PersonProfile" as PersonProfile UI_COLOR +end box + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +end box + +[-> MainWindow : executeCommand("save") + +MainWindow -> PersonProfile : getPerson() + +PersonProfile --> MainWindow : get personToEdit + +MainWindow -> LogicManager : executeInView("name", personToEdit, targetIndex) +@enduml diff --git a/docs/diagrams/SaveSequenceDiagramStep2.puml b/docs/diagrams/SaveSequenceDiagramStep2.puml new file mode 100644 index 00000000000..6f4bc90d716 --- /dev/null +++ b/docs/diagrams/SaveSequenceDiagramStep2.puml @@ -0,0 +1,32 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box UI UI_COLOR_T1 +participant ":MainWindow" as MainWindow UI_COLOR +participant ":PersonProfile" as PersonProfile UI_COLOR +end box + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":ViewModeParser" as ViewModeParser LOGIC_COLOR +participant "s:SaveCommand" as SaveCommand LOGIC_COLOR +end box + +[-> MainWindow : executeCommand("save") + +MainWindow -> PersonProfile : getPerson() + +PersonProfile --> MainWindow : get personToEdit + +MainWindow -> LogicManager : executeInView("name", personToEdit, targetIndex) + +LogicManager -> ViewModeParser : parseCommand("name", personToEdit, targetIndex) + +create SaveCommand +ViewModeParser -> SaveCommand + +SaveCommand --> ViewModeParser : s + +ViewModeParser --> LogicManager : s +@enduml diff --git a/docs/diagrams/SortSequenceDiagram.puml b/docs/diagrams/SortSequenceDiagram.puml new file mode 100644 index 00000000000..632a7effc6b --- /dev/null +++ b/docs/diagrams/SortSequenceDiagram.puml @@ -0,0 +1,44 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant "s:SortCommand" as SortCommand LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +end box + +[-> LogicManager : execute(sort) +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand(sort) +activate AddressBookParser + +create SortCommand +AddressBookParser -> SortCommand + +SortCommand --> AddressBookParser +deactivate SortCommand + +AddressBookParser --> LogicManager : s +deactivate AddressBookParser + +LogicManager -> SortCommand : execute() +activate SortCommand + +SortCommand -> Model : sortByName() +activate Model + +Model --> SortCommand +deactivate Model + +SortCommand --> LogicManager : result +deactivate SortCommand + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/StatsAvailSelfInvDiagram.puml b/docs/diagrams/StatsAvailSelfInvDiagram.puml new file mode 100644 index 00000000000..70789e3b6fe --- /dev/null +++ b/docs/diagrams/StatsAvailSelfInvDiagram.puml @@ -0,0 +1,41 @@ +@startuml +!include style.puml + +skinparam ArrowFontStyle plain + +mainframe **sd** get availability statistics + +participant "a:StatsAvailCommand" as StatsAvailCommand LOGIC_COLOR + +StatsAvailCommand -> StatsAvailCommand : getAvailableFosterers(list) +activate StatsAvailCommand +StatsAvailCommand --> StatsAvailCommand : availableFosterers +deactivate StatsAvailCommand + +StatsAvailCommand -> StatsAvailCommand : calculatePercentage(availableCount, total) +activate StatsAvailCommand +StatsAvailCommand --> StatsAvailCommand +deactivate StatsAvailCommand + +StatsAvailCommand -> StatsAvailCommand : getAbleDogCount(availableFosterers) +activate StatsAvailCommand +StatsAvailCommand --> StatsAvailCommand : ableDogCount +deactivate StatsAvailCommand + +StatsAvailCommand -> StatsAvailCommand : getAbleCatCount(availableFosterers) +activate StatsAvailCommand +StatsAvailCommand --> StatsAvailCommand : ableCatCount +deactivate StatsAvailCommand + +StatsAvailCommand -> StatsAvailCommand : calculatePercentage(ableDogCount, availableCount) +activate StatsAvailCommand +StatsAvailCommand --> StatsAvailCommand +deactivate StatsAvailCommand + +StatsAvailCommand -> StatsAvailCommand : calculatePercentage(ableCatCount, availableCount) +activate StatsAvailCommand +StatsAvailCommand --> StatsAvailCommand +deactivate StatsAvailCommand +||| + +@enduml diff --git a/docs/diagrams/StatsAvailSequenceDiagram.puml b/docs/diagrams/StatsAvailSequenceDiagram.puml new file mode 100644 index 00000000000..193b89e09dc --- /dev/null +++ b/docs/diagrams/StatsAvailSequenceDiagram.puml @@ -0,0 +1,74 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant ":StatsCommandParser" as StatsCommandParser LOGIC_COLOR +participant "a:StatsAvailCommand" as StatsAvailCommand LOGIC_COLOR +participant ":CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +end box + +[-> LogicManager : execute("stats avail") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("stats avail") +activate AddressBookParser + +create StatsCommandParser +AddressBookParser -> StatsCommandParser +activate StatsCommandParser + +StatsCommandParser --> AddressBookParser +deactivate StatsCommandParser + +AddressBookParser -> StatsCommandParser : parse("avail") +activate StatsCommandParser + +create StatsAvailCommand +StatsCommandParser -> StatsAvailCommand +activate StatsAvailCommand + +StatsAvailCommand --> StatsCommandParser : a +deactivate StatsAvailCommand + +StatsCommandParser --> AddressBookParser : a +deactivate StatsCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +StatsCommandParser -[hidden]-> AddressBookParser +destroy StatsCommandParser + +AddressBookParser --> LogicManager : a +deactivate AddressBookParser + +LogicManager -> StatsAvailCommand : execute() +activate StatsAvailCommand + +StatsAvailCommand -> Model : getFilteredPersonList() +activate Model + +Model --> StatsAvailCommand : list +deactivate Model + +ref over StatsAvailCommand +get availability statistics +end ref + +create CommandResult +StatsAvailCommand -> CommandResult +activate CommandResult + +CommandResult --> StatsAvailCommand +deactivate CommandResult + +StatsAvailCommand --> LogicManager : result +deactivate StatsAvailCommand + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/StatsClassDiagram.puml b/docs/diagrams/StatsClassDiagram.puml new file mode 100644 index 00000000000..0280d1c77a0 --- /dev/null +++ b/docs/diagrams/StatsClassDiagram.puml @@ -0,0 +1,15 @@ +@startuml +!include style.puml +skinparam arrowThickness 1.1 +skinparam arrowColor LOGIC_COLOR +skinparam classBackgroundColor LOGIC_COLOR + +Class StatsCommand +Class "{abstract}\nCommand" as Command + +StatsCommand -up-|> Command +StatsAvailCommand -up-|> StatsCommand +StatsCurrentCommand -up-|> StatsCommand +StatsHousingCommand -up-|> StatsCommand + +@enduml diff --git a/docs/diagrams/UiClassDiagram.puml b/docs/diagrams/UiClassDiagram.puml index 95473d5aa19..ba50b7a172a 100644 --- a/docs/diagrams/UiClassDiagram.puml +++ b/docs/diagrams/UiClassDiagram.puml @@ -5,16 +5,19 @@ skinparam arrowColor UI_COLOR_T4 skinparam classBackgroundColor UI_COLOR package UI <>{ -Class "<>\nUi" as Ui -Class "{abstract}\nUiPart" as UiPart -Class UiManager -Class MainWindow -Class HelpWindow -Class ResultDisplay -Class PersonListPanel -Class PersonCard -Class StatusBarFooter -Class CommandBox + Class "<>\nUi" as Ui + Class "{abstract}\nUiPart" as UiPart + Class UiManager + Class MainWindow + Class HelpWindow + Class ResultDisplay + Class PersonListPanel + Class PersonCard + Class StatusBarFooter + Class CommandBox + package Profile <> { + Class HiddenProfile #FFFFFF + } } package Model <> { @@ -35,10 +38,11 @@ MainWindow *-down-> "1" ResultDisplay MainWindow *-down-> "1" PersonListPanel MainWindow *-down-> "1" StatusBarFooter MainWindow --> "0..1" HelpWindow +MainWindow *--> Profile PersonListPanel -down-> "*" PersonCard -MainWindow -left-|> UiPart +MainWindow -down-|> UiPart ResultDisplay --|> UiPart CommandBox --|> UiPart @@ -46,6 +50,7 @@ PersonListPanel --|> UiPart PersonCard --|> UiPart StatusBarFooter --|> UiPart HelpWindow --|> UiPart +Profile ---|> UiPart PersonCard ..> Model UiManager -right-> Logic diff --git a/docs/diagrams/UiProfileDiagram.puml b/docs/diagrams/UiProfileDiagram.puml new file mode 100644 index 00000000000..48a1cc65711 --- /dev/null +++ b/docs/diagrams/UiProfileDiagram.puml @@ -0,0 +1,33 @@ +@startuml +!include style.puml +skinparam arrowThickness 1.1 +skinparam arrowColor UI_COLOR_T4 +skinparam classBackgroundColor UI_COLOR + +package UI <>{ + Class "{abstract}\nUiPart" as UiPart + Class MainWindow + package Profile <> { + Class PersonProfile + Class PersonProfileField + Class PersonProfileHeader + Class PersonProfileNote + Class PersonProfileTags + } +} + +MainWindow *-> "1" PersonProfile + + +PersonProfile *--> "*" PersonProfileField +PersonProfile *--> "1" PersonProfileHeader +PersonProfile *--> "1" PersonProfileNote +PersonProfile *--> "1" PersonProfileTags + +MainWindow ---|> UiPart +PersonProfile ---|> UiPart +PersonProfileField ---|> UiPart +PersonProfileHeader ---|> UiPart +PersonProfileNote ---|> UiPart +PersonProfileTags ---|> UiPart +@enduml diff --git a/docs/diagrams/UndoActivityDiagram.puml b/docs/diagrams/UndoActivityDiagram.puml new file mode 100644 index 00000000000..6c006046945 --- /dev/null +++ b/docs/diagrams/UndoActivityDiagram.puml @@ -0,0 +1,27 @@ +@startuml +skin rose +skinparam ActivityFontSize 15 +skinparam ArrowFontSize 12 +start +:User executes a command; +:AddressBookParser parses the command; + +if () then ([Command is the undo command]) + + if() then ([The existing model equal to the backup model]) + :Display error message; + else ([else]) + :Overwrite the current model data with the backup model data; + + endif + +else ([Command is not the undo command]) + if() then ([Command is valid]) + :Overwrite the backup model data with the current model data; + :Execute the command; + else ([else]) + :Display error message; + endif +endif +stop +@enduml diff --git a/docs/diagrams/UndoSequenceDiagram.puml b/docs/diagrams/UndoSequenceDiagram.puml index 87ff3e9237e..cda8a409651 100644 --- a/docs/diagrams/UndoSequenceDiagram.puml +++ b/docs/diagrams/UndoSequenceDiagram.puml @@ -10,8 +10,8 @@ end box box Model MODEL_COLOR_T1 participant ":Model" as Model MODEL_COLOR -participant ":VersionedAddressBook" as VersionedAddressBook MODEL_COLOR end box + [-> LogicManager : execute(undo) activate LogicManager @@ -28,26 +28,18 @@ deactivate UndoCommand AddressBookParser --> LogicManager : u deactivate AddressBookParser -LogicManager -> UndoCommand : execute() -activate UndoCommand +LogicManager -> Model : setAddressBook(backupModel.getAddressBook()) -UndoCommand -> Model : undoAddressBook() -activate Model -Model -> VersionedAddressBook : undo() -activate VersionedAddressBook +activate Model -VersionedAddressBook -> VersionedAddressBook :resetData(ReadOnlyAddressBook) -VersionedAddressBook --> Model : -deactivate VersionedAddressBook -Model --> UndoCommand +Model --> LogicManager deactivate Model -UndoCommand --> LogicManager : result deactivate UndoCommand UndoCommand -[hidden]-> LogicManager : result -destroy UndoCommand + [<--LogicManager deactivate LogicManager diff --git a/docs/diagrams/ViewExitSequenceDiagram.puml b/docs/diagrams/ViewExitSequenceDiagram.puml new file mode 100644 index 00000000000..5aa55620816 --- /dev/null +++ b/docs/diagrams/ViewExitSequenceDiagram.puml @@ -0,0 +1,96 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box UI UI_COLOR_T1 +participant ":MainWindow" as MainWindow UI_COLOR +participant ":PersonProfile" as PersonProfile UI_COLOR +participant ":CommandBox" as CommandBox UI_COLOR +end box + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":ViewModeParser" as ViewModeParser LOGIC_COLOR +participant "e:ViewExitCommand" as ViewExitCommand LOGIC_COLOR +participant "commandResult:CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +end box + +[-> MainWindow : executeCommand("exit") +activate MainWindow + +MainWindow -> PersonProfile : getPerson() +activate PersonProfile + +PersonProfile --> MainWindow : get personToEdit +deactivate PersonProfile + +MainWindow -> LogicManager : executeInView("exit", personToEdit, targetIndex) +activate LogicManager + +LogicManager -> ViewModeParser : parseCommand("exit", personToEdit, targetIndex) +activate ViewModeParser + +create ViewExitCommand +ViewModeParser -> ViewExitCommand +activate ViewExitCommand + +ViewExitCommand --> ViewModeParser : s +deactivate ViewExitCommand + +ViewModeParser --> LogicManager : s +deactivate ViewModeParser + +LogicManager -> ViewExitCommand : execute() +activate ViewExitCommand + +ViewExitCommand -> Model : getFilteredPersonList() +activate Model + +Model --> ViewExitCommand +deactivate Model + +create CommandResult +ViewExitCommand -> CommandResult +activate CommandResult + +CommandResult --> ViewExitCommand : +deactivate CommandResult + + +ViewExitCommand --> LogicManager : commandResult +deactivate ViewExitCommand + +LogicManager --> MainWindow : commandResult +deactivate LogicManager + +group ref [Handle View Exit] + MainWindow -[hidden]-> CommandBox +end + +alt User types Enter + + [->CommandBox : "Enter" + CommandBox -> MainWindow : handleViewExit() + group ref [Handle View Exit] + MainWindow -[hidden]-> CommandBox + end +else User types ESC + [->CommandBox : "Esc" + CommandBox -> MainWindow : handleCancelViewExit() + activate MainWindow + MainWindow -> CommandBox : setInConfirmationDialog(false) + MainWindow -> PersonProfile : setIsInConfirmationDialog(false) + PersonProfile -[hidden]-> MainWindow + deactivate MainWindow +end + +MainWindow --> CommandBox + CommandBox --> MainWindow +[<--MainWindow +deactivate MainWindow + +@enduml diff --git a/docs/diagrams/ViewExitSequenceDiagram1.puml b/docs/diagrams/ViewExitSequenceDiagram1.puml new file mode 100644 index 00000000000..63265ca5c70 --- /dev/null +++ b/docs/diagrams/ViewExitSequenceDiagram1.puml @@ -0,0 +1,29 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box UI UI_COLOR_T1 +participant ":MainWindow" as MainWindow UI_COLOR +participant ":PersonProfile" as PersonProfile UI_COLOR +participant ":CommandBox" as CommandBox UI_COLOR +end box + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant "e:ViewExitCommand" as ViewExitCommand LOGIC_COLOR +participant "commandResult:CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +end box + +[-> MainWindow : executeCommand("exit") + +MainWindow -> PersonProfile : getPerson() + +PersonProfile --> MainWindow : get personToEdit + +MainWindow -> LogicManager : executeInView("exit", personToEdit, targetIndex) + +@enduml diff --git a/docs/diagrams/ViewExitSequenceDiagram2.puml b/docs/diagrams/ViewExitSequenceDiagram2.puml new file mode 100644 index 00000000000..e916834e431 --- /dev/null +++ b/docs/diagrams/ViewExitSequenceDiagram2.puml @@ -0,0 +1,42 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box UI UI_COLOR_T1 +participant ":MainWindow" as MainWindow UI_COLOR +participant ":PersonProfile" as PersonProfile UI_COLOR +participant ":CommandBox" as CommandBox UI_COLOR +end box + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":ViewModeParser" as ViewModeParser LOGIC_COLOR +participant "e:ViewExitCommand" as ViewExitCommand LOGIC_COLOR +participant "commandResult:CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +end box + +[-> MainWindow : executeCommand("exit") + +MainWindow -> PersonProfile : getPerson() + +PersonProfile --> MainWindow : get personToEdit + +MainWindow -> LogicManager : executeInView("exit", personToEdit, targetIndex) + +LogicManager -> ViewModeParser : parseCommand("exit", personToEdit, targetIndex) + +create ViewExitCommand +ViewModeParser -> ViewExitCommand +activate ViewExitCommand + +ViewExitCommand --> ViewModeParser : s +deactivate ViewExitCommand + +ViewModeParser --> LogicManager : s +deactivate ViewModeParser + +@enduml diff --git a/docs/diagrams/ViewExitSequenceDiagram3.puml b/docs/diagrams/ViewExitSequenceDiagram3.puml new file mode 100644 index 00000000000..dcd78d31c2a --- /dev/null +++ b/docs/diagrams/ViewExitSequenceDiagram3.puml @@ -0,0 +1,74 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box UI UI_COLOR_T1 +participant ":MainWindow" as MainWindow UI_COLOR +participant ":PersonProfile" as PersonProfile UI_COLOR +participant ":CommandBox" as CommandBox UI_COLOR +end box + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":ViewModeParser" as ViewModeParser LOGIC_COLOR +participant "e:ViewExitCommand" as ViewExitCommand LOGIC_COLOR +participant "commandResult:CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +end box + +[-> MainWindow : executeCommand("exit") +activate MainWindow + +MainWindow -> PersonProfile : getPerson() +activate PersonProfile + +PersonProfile --> MainWindow : get personToEdit +deactivate PersonProfile + +MainWindow -> LogicManager : executeInView("exit", personToEdit, targetIndex) +activate LogicManager + +LogicManager -> ViewModeParser : parseCommand("exit", personToEdit, targetIndex) +activate ViewModeParser + +create ViewExitCommand +ViewModeParser -> ViewExitCommand +activate ViewExitCommand + +ViewExitCommand --> ViewModeParser : s +deactivate ViewExitCommand + +ViewModeParser --> LogicManager : s +deactivate ViewModeParser + +LogicManager -> ViewExitCommand : execute() +activate ViewExitCommand + +ViewExitCommand -> Model : getFilteredPersonList() +activate Model + +Model --> ViewExitCommand +deactivate Model + +create CommandResult +ViewExitCommand -> CommandResult +activate CommandResult + +CommandResult --> ViewExitCommand : +deactivate CommandResult + + +ViewExitCommand --> LogicManager : commandResult +deactivate ViewExitCommand + +LogicManager --> MainWindow : commandResult +deactivate LogicManager + +group ref [Handle View Exit] + MainWindow -[hidden]-> CommandBox +end + +@enduml diff --git a/docs/images/AddActivityDiagram.png b/docs/images/AddActivityDiagram.png new file mode 100644 index 00000000000..dde89f685af Binary files /dev/null and b/docs/images/AddActivityDiagram.png differ diff --git a/docs/images/AddSequenceDiagram.png b/docs/images/AddSequenceDiagram.png new file mode 100644 index 00000000000..8de1828dd74 Binary files /dev/null and b/docs/images/AddSequenceDiagram.png differ diff --git a/docs/images/BetterModelClassDiagram.png b/docs/images/BetterModelClassDiagram.png index 02a42e35e76..83ed8ccc935 100644 Binary files a/docs/images/BetterModelClassDiagram.png and b/docs/images/BetterModelClassDiagram.png differ diff --git a/docs/images/DeleteMultipleSequenceDiagram.png b/docs/images/DeleteMultipleSequenceDiagram.png new file mode 100644 index 00000000000..a29f8c02fb7 Binary files /dev/null and b/docs/images/DeleteMultipleSequenceDiagram.png differ diff --git a/docs/images/EditFieldSequenceDiagram.png b/docs/images/EditFieldSequenceDiagram.png new file mode 100644 index 00000000000..0dcf86951ee Binary files /dev/null and b/docs/images/EditFieldSequenceDiagram.png differ diff --git a/docs/images/EditFieldSequenceDiagramStep1.png b/docs/images/EditFieldSequenceDiagramStep1.png new file mode 100644 index 00000000000..1ee7d9fb7be Binary files /dev/null and b/docs/images/EditFieldSequenceDiagramStep1.png differ diff --git a/docs/images/EditFieldSequenceDiagramStep2.png b/docs/images/EditFieldSequenceDiagramStep2.png new file mode 100644 index 00000000000..62fde8d56a7 Binary files /dev/null and b/docs/images/EditFieldSequenceDiagramStep2.png differ diff --git a/docs/images/FindCommandClassDiagram.png b/docs/images/FindCommandClassDiagram.png new file mode 100644 index 00000000000..5a429e6b1a0 Binary files /dev/null and b/docs/images/FindCommandClassDiagram.png differ diff --git a/docs/images/FindCommandSequenceDiagram.png b/docs/images/FindCommandSequenceDiagram.png new file mode 100644 index 00000000000..7fe6d369ffc Binary files /dev/null and b/docs/images/FindCommandSequenceDiagram.png differ diff --git a/docs/images/FindPredicateExampleObjectDiagram.png b/docs/images/FindPredicateExampleObjectDiagram.png new file mode 100644 index 00000000000..eabf4779e24 Binary files /dev/null and b/docs/images/FindPredicateExampleObjectDiagram.png differ diff --git a/docs/images/FindPredicateExampleSequenceDiagram.png b/docs/images/FindPredicateExampleSequenceDiagram.png new file mode 100644 index 00000000000..1fe5f507cd1 Binary files /dev/null and b/docs/images/FindPredicateExampleSequenceDiagram.png differ diff --git a/docs/images/FindSearchPredicateSequenceDiagram.png b/docs/images/FindSearchPredicateSequenceDiagram.png new file mode 100644 index 00000000000..17412146be6 Binary files /dev/null and b/docs/images/FindSearchPredicateSequenceDiagram.png differ diff --git a/docs/images/HandleSaveSequenceDiagram.png b/docs/images/HandleSaveSequenceDiagram.png new file mode 100644 index 00000000000..f6a6c6e743f Binary files /dev/null and b/docs/images/HandleSaveSequenceDiagram.png differ diff --git a/docs/images/HandleViewExitSequenceDiagram.png b/docs/images/HandleViewExitSequenceDiagram.png new file mode 100644 index 00000000000..3ddeaa50be3 Binary files /dev/null and b/docs/images/HandleViewExitSequenceDiagram.png differ diff --git a/docs/images/IsInViewModeSequenceDiagram.png b/docs/images/IsInViewModeSequenceDiagram.png new file mode 100644 index 00000000000..f60c270ab33 Binary files /dev/null and b/docs/images/IsInViewModeSequenceDiagram.png differ diff --git a/docs/images/LogicClassDiagram.png b/docs/images/LogicClassDiagram.png index e3b784310fe..710649e4688 100644 Binary files a/docs/images/LogicClassDiagram.png and b/docs/images/LogicClassDiagram.png differ diff --git a/docs/images/MainWindowCommandTypeSequenceDiagram.png b/docs/images/MainWindowCommandTypeSequenceDiagram.png new file mode 100644 index 00000000000..d5924b9198b Binary files /dev/null and b/docs/images/MainWindowCommandTypeSequenceDiagram.png differ diff --git a/docs/images/ModelClassDiagram.png b/docs/images/ModelClassDiagram.png index a19fb1b4ac8..b201a1cb585 100644 Binary files a/docs/images/ModelClassDiagram.png and b/docs/images/ModelClassDiagram.png differ diff --git a/docs/images/ParserClasses.png b/docs/images/ParserClasses.png index edfd1ff7897..76c140a8bcf 100644 Binary files a/docs/images/ParserClasses.png and b/docs/images/ParserClasses.png differ diff --git a/docs/images/ProfileEditSequenceDiagram.png b/docs/images/ProfileEditSequenceDiagram.png new file mode 100644 index 00000000000..fb1710a0d49 Binary files /dev/null and b/docs/images/ProfileEditSequenceDiagram.png differ diff --git a/docs/images/ResetActivityDiagram.png b/docs/images/ResetActivityDiagram.png new file mode 100644 index 00000000000..678390a541d Binary files /dev/null and b/docs/images/ResetActivityDiagram.png differ diff --git a/docs/images/ResetConfirmSequenceDiagram.png b/docs/images/ResetConfirmSequenceDiagram.png new file mode 100644 index 00000000000..710733c478f Binary files /dev/null and b/docs/images/ResetConfirmSequenceDiagram.png differ diff --git a/docs/images/ResetSequenceDiagram.png b/docs/images/ResetSequenceDiagram.png new file mode 100644 index 00000000000..769d0c6bba6 Binary files /dev/null and b/docs/images/ResetSequenceDiagram.png differ diff --git a/docs/images/SaveSequenceDiagram.png b/docs/images/SaveSequenceDiagram.png new file mode 100644 index 00000000000..62455137047 Binary files /dev/null and b/docs/images/SaveSequenceDiagram.png differ diff --git a/docs/images/SaveSequenceDiagramStep1.png b/docs/images/SaveSequenceDiagramStep1.png new file mode 100644 index 00000000000..ba86b105896 Binary files /dev/null and b/docs/images/SaveSequenceDiagramStep1.png differ diff --git a/docs/images/SaveSequenceDiagramStep2.png b/docs/images/SaveSequenceDiagramStep2.png new file mode 100644 index 00000000000..aea7af30c85 Binary files /dev/null and b/docs/images/SaveSequenceDiagramStep2.png differ diff --git a/docs/images/SortSequenceDiagram.png b/docs/images/SortSequenceDiagram.png new file mode 100644 index 00000000000..189c52ee7e9 Binary files /dev/null and b/docs/images/SortSequenceDiagram.png differ diff --git a/docs/images/StatsAvailSelfInvDiagram.png b/docs/images/StatsAvailSelfInvDiagram.png new file mode 100644 index 00000000000..eb01d7f1939 Binary files /dev/null and b/docs/images/StatsAvailSelfInvDiagram.png differ diff --git a/docs/images/StatsAvailSequenceDiagram.png b/docs/images/StatsAvailSequenceDiagram.png new file mode 100644 index 00000000000..3de96438480 Binary files /dev/null and b/docs/images/StatsAvailSequenceDiagram.png differ diff --git a/docs/images/StatsClassDiagram.png b/docs/images/StatsClassDiagram.png new file mode 100644 index 00000000000..1c284c85239 Binary files /dev/null and b/docs/images/StatsClassDiagram.png differ diff --git a/docs/images/Ui.png b/docs/images/Ui.png index 5bd77847aa2..d2ef07631af 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..8918857420e 100644 Binary files a/docs/images/UiClassDiagram.png and b/docs/images/UiClassDiagram.png differ diff --git a/docs/images/UiProfileDiagram.png b/docs/images/UiProfileDiagram.png new file mode 100644 index 00000000000..f6754832f97 Binary files /dev/null and b/docs/images/UiProfileDiagram.png differ diff --git a/docs/images/UndoActivityDiagram.png b/docs/images/UndoActivityDiagram.png new file mode 100644 index 00000000000..e6cac2a5792 Binary files /dev/null and b/docs/images/UndoActivityDiagram.png differ diff --git a/docs/images/UndoSequenceDiagram.png b/docs/images/UndoSequenceDiagram.png index c7a7e637266..b8191ece39c 100644 Binary files a/docs/images/UndoSequenceDiagram.png and b/docs/images/UndoSequenceDiagram.png differ diff --git a/docs/images/ViewExitSequenceDiagram.png b/docs/images/ViewExitSequenceDiagram.png new file mode 100644 index 00000000000..ce1b9fcdf2d Binary files /dev/null and b/docs/images/ViewExitSequenceDiagram.png differ diff --git a/docs/images/ViewExitSequenceDiagram1.png b/docs/images/ViewExitSequenceDiagram1.png new file mode 100644 index 00000000000..b62b09aec29 Binary files /dev/null and b/docs/images/ViewExitSequenceDiagram1.png differ diff --git a/docs/images/ViewExitSequenceDiagram2.png b/docs/images/ViewExitSequenceDiagram2.png new file mode 100644 index 00000000000..b8a5760f371 Binary files /dev/null and b/docs/images/ViewExitSequenceDiagram2.png differ diff --git a/docs/images/ViewExitSequenceDiagram3.png b/docs/images/ViewExitSequenceDiagram3.png new file mode 100644 index 00000000000..e727b3c27ee Binary files /dev/null and b/docs/images/ViewExitSequenceDiagram3.png differ diff --git a/docs/images/brandon-nam.png b/docs/images/brandon-nam.png new file mode 100644 index 00000000000..5b08292bf04 Binary files /dev/null and b/docs/images/brandon-nam.png differ diff --git a/docs/images/butteredyakiimo.png b/docs/images/butteredyakiimo.png new file mode 100644 index 00000000000..84637603a24 Binary files /dev/null and b/docs/images/butteredyakiimo.png differ diff --git a/docs/images/h1410101.png b/docs/images/h1410101.png new file mode 100644 index 00000000000..c5a4a44dd8d Binary files /dev/null and b/docs/images/h1410101.png differ diff --git a/docs/images/nananakx-x.png b/docs/images/nananakx-x.png new file mode 100644 index 00000000000..f67bff10744 Binary files /dev/null and b/docs/images/nananakx-x.png differ diff --git a/docs/images/screenshots/AddDuplicate.png b/docs/images/screenshots/AddDuplicate.png new file mode 100644 index 00000000000..39e3fab7ae7 Binary files /dev/null and b/docs/images/screenshots/AddDuplicate.png differ diff --git a/docs/images/screenshots/AddErrorMessage.png b/docs/images/screenshots/AddErrorMessage.png new file mode 100644 index 00000000000..f887a05bc91 Binary files /dev/null and b/docs/images/screenshots/AddErrorMessage.png differ diff --git a/docs/images/screenshots/AddJerry.png b/docs/images/screenshots/AddJerry.png new file mode 100644 index 00000000000..1db60a10cba Binary files /dev/null and b/docs/images/screenshots/AddJerry.png differ diff --git a/docs/images/screenshots/AddPete.png b/docs/images/screenshots/AddPete.png new file mode 100644 index 00000000000..9c07d96e952 Binary files /dev/null and b/docs/images/screenshots/AddPete.png differ diff --git a/docs/images/screenshots/BeforeEnteringName.png b/docs/images/screenshots/BeforeEnteringName.png new file mode 100644 index 00000000000..b1a4f57ac94 Binary files /dev/null and b/docs/images/screenshots/BeforeEnteringName.png differ diff --git a/docs/images/screenshots/BobYeoh.png b/docs/images/screenshots/BobYeoh.png new file mode 100644 index 00000000000..5f8ae285aec Binary files /dev/null and b/docs/images/screenshots/BobYeoh.png differ diff --git a/docs/images/screenshots/ChangeNameToBobYeoh.png b/docs/images/screenshots/ChangeNameToBobYeoh.png new file mode 100644 index 00000000000..0c0b7a1ed7b Binary files /dev/null and b/docs/images/screenshots/ChangeNameToBobYeoh.png differ diff --git a/docs/images/screenshots/CursorBackToCommandbox.png b/docs/images/screenshots/CursorBackToCommandbox.png new file mode 100644 index 00000000000..20391826696 Binary files /dev/null and b/docs/images/screenshots/CursorBackToCommandbox.png differ diff --git a/docs/images/screenshots/Delete.png b/docs/images/screenshots/Delete.png new file mode 100644 index 00000000000..0ee91f8ae82 Binary files /dev/null and b/docs/images/screenshots/Delete.png differ diff --git a/docs/images/screenshots/EditExample.png b/docs/images/screenshots/EditExample.png new file mode 100644 index 00000000000..fd1fcbb0dfd Binary files /dev/null and b/docs/images/screenshots/EditExample.png differ diff --git a/docs/images/screenshots/EditExample2.png b/docs/images/screenshots/EditExample2.png new file mode 100644 index 00000000000..c718cfb41fb Binary files /dev/null and b/docs/images/screenshots/EditExample2.png differ diff --git a/docs/images/screenshots/EnterName.png b/docs/images/screenshots/EnterName.png new file mode 100644 index 00000000000..5c46ecf09b6 Binary files /dev/null and b/docs/images/screenshots/EnterName.png differ diff --git a/docs/images/screenshots/ExitCommandNotSavedCancel.png b/docs/images/screenshots/ExitCommandNotSavedCancel.png new file mode 100644 index 00000000000..862a81d0b78 Binary files /dev/null and b/docs/images/screenshots/ExitCommandNotSavedCancel.png differ diff --git a/docs/images/screenshots/ExitCommandNotSavedWarning.png b/docs/images/screenshots/ExitCommandNotSavedWarning.png new file mode 100644 index 00000000000..1ca6972b6db Binary files /dev/null and b/docs/images/screenshots/ExitCommandNotSavedWarning.png differ diff --git a/docs/images/screenshots/ExitCommandSavedAfter.png b/docs/images/screenshots/ExitCommandSavedAfter.png new file mode 100644 index 00000000000..b276ee72e35 Binary files /dev/null and b/docs/images/screenshots/ExitCommandSavedAfter.png differ diff --git a/docs/images/screenshots/ExitCommandSavedBefore.png b/docs/images/screenshots/ExitCommandSavedBefore.png new file mode 100644 index 00000000000..aca8e69dbf2 Binary files /dev/null and b/docs/images/screenshots/ExitCommandSavedBefore.png differ diff --git a/docs/images/screenshots/ExitProfilePageWithoutDetails.png b/docs/images/screenshots/ExitProfilePageWithoutDetails.png new file mode 100644 index 00000000000..70ee8d8091f Binary files /dev/null and b/docs/images/screenshots/ExitProfilePageWithoutDetails.png differ diff --git a/docs/images/screenshots/ExitWithoutSave.png b/docs/images/screenshots/ExitWithoutSave.png new file mode 100644 index 00000000000..f1358422b79 Binary files /dev/null and b/docs/images/screenshots/ExitWithoutSave.png differ diff --git a/docs/images/screenshots/HelpWindow.png b/docs/images/screenshots/HelpWindow.png new file mode 100644 index 00000000000..07313f80009 Binary files /dev/null and b/docs/images/screenshots/HelpWindow.png differ diff --git a/docs/images/screenshots/JsonExample.png b/docs/images/screenshots/JsonExample.png new file mode 100644 index 00000000000..fff2f41837a Binary files /dev/null and b/docs/images/screenshots/JsonExample.png differ diff --git a/docs/images/screenshots/NavigatingFields.png b/docs/images/screenshots/NavigatingFields.png new file mode 100644 index 00000000000..274a713034b Binary files /dev/null and b/docs/images/screenshots/NavigatingFields.png differ diff --git a/docs/images/screenshots/Notes.png b/docs/images/screenshots/Notes.png new file mode 100644 index 00000000000..8d1c4b738a2 Binary files /dev/null and b/docs/images/screenshots/Notes.png differ diff --git a/docs/images/screenshots/ProfilePage.png b/docs/images/screenshots/ProfilePage.png new file mode 100644 index 00000000000..c0f82f907ee Binary files /dev/null and b/docs/images/screenshots/ProfilePage.png differ diff --git a/docs/images/screenshots/Reset.png b/docs/images/screenshots/Reset.png new file mode 100644 index 00000000000..6dee917ecf0 Binary files /dev/null and b/docs/images/screenshots/Reset.png differ diff --git a/docs/images/screenshots/ResetConfirm.png b/docs/images/screenshots/ResetConfirm.png new file mode 100644 index 00000000000..24fc27cddda Binary files /dev/null and b/docs/images/screenshots/ResetConfirm.png differ diff --git a/docs/images/screenshots/ResetPrompt.png b/docs/images/screenshots/ResetPrompt.png new file mode 100644 index 00000000000..90855bf04f0 Binary files /dev/null and b/docs/images/screenshots/ResetPrompt.png differ diff --git a/docs/images/screenshots/SaveBobYeoh.png b/docs/images/screenshots/SaveBobYeoh.png new file mode 100644 index 00000000000..98fd8ebc31d Binary files /dev/null and b/docs/images/screenshots/SaveBobYeoh.png differ diff --git a/docs/images/screenshots/SaveCommandAfter.png b/docs/images/screenshots/SaveCommandAfter.png new file mode 100644 index 00000000000..549476a8642 Binary files /dev/null and b/docs/images/screenshots/SaveCommandAfter.png differ diff --git a/docs/images/screenshots/SaveCommandBefore.png b/docs/images/screenshots/SaveCommandBefore.png new file mode 100644 index 00000000000..d9f8e27cee7 Binary files /dev/null and b/docs/images/screenshots/SaveCommandBefore.png differ diff --git a/docs/images/screenshots/SaveExistingFosterer.png b/docs/images/screenshots/SaveExistingFosterer.png new file mode 100644 index 00000000000..cfde8112e07 Binary files /dev/null and b/docs/images/screenshots/SaveExistingFosterer.png differ diff --git a/docs/images/screenshots/SaveNewFosterer.png b/docs/images/screenshots/SaveNewFosterer.png new file mode 100644 index 00000000000..3e0d512aba0 Binary files /dev/null and b/docs/images/screenshots/SaveNewFosterer.png differ diff --git a/docs/images/screenshots/Sort.png b/docs/images/screenshots/Sort.png new file mode 100644 index 00000000000..6d111f57dec Binary files /dev/null and b/docs/images/screenshots/Sort.png differ diff --git a/docs/images/screenshots/StatsAllAvail.png b/docs/images/screenshots/StatsAllAvail.png new file mode 100644 index 00000000000..3bfe24efe69 Binary files /dev/null and b/docs/images/screenshots/StatsAllAvail.png differ diff --git a/docs/images/screenshots/StatsAvail.png b/docs/images/screenshots/StatsAvail.png new file mode 100644 index 00000000000..eaebddec820 Binary files /dev/null and b/docs/images/screenshots/StatsAvail.png differ diff --git a/docs/images/screenshots/StatsCurrent.png b/docs/images/screenshots/StatsCurrent.png new file mode 100644 index 00000000000..f7bf3aec31e Binary files /dev/null and b/docs/images/screenshots/StatsCurrent.png differ diff --git a/docs/images/screenshots/StatsHousing.png b/docs/images/screenshots/StatsHousing.png new file mode 100644 index 00000000000..7072b8ba273 Binary files /dev/null and b/docs/images/screenshots/StatsHousing.png differ diff --git a/docs/images/screenshots/StatsTotalPercent.png b/docs/images/screenshots/StatsTotalPercent.png new file mode 100644 index 00000000000..96a3f64fece Binary files /dev/null and b/docs/images/screenshots/StatsTotalPercent.png differ diff --git a/docs/images/screenshots/Undo.png b/docs/images/screenshots/Undo.png new file mode 100644 index 00000000000..3fd038df1df Binary files /dev/null and b/docs/images/screenshots/Undo.png differ diff --git a/docs/images/screenshots/UndoError.png b/docs/images/screenshots/UndoError.png new file mode 100644 index 00000000000..c0aca2d3b6d Binary files /dev/null and b/docs/images/screenshots/UndoError.png differ diff --git a/docs/images/screenshots/View1.png b/docs/images/screenshots/View1.png new file mode 100644 index 00000000000..7e461a94216 Binary files /dev/null and b/docs/images/screenshots/View1.png differ diff --git a/docs/images/screenshots/ViewCommandExample.png b/docs/images/screenshots/ViewCommandExample.png new file mode 100644 index 00000000000..c68908a29bc Binary files /dev/null and b/docs/images/screenshots/ViewCommandExample.png differ diff --git a/docs/images/tyruslye.png b/docs/images/tyruslye.png new file mode 100644 index 00000000000..4002a53894a Binary files /dev/null and b/docs/images/tyruslye.png differ diff --git a/docs/index.md b/docs/index.md index 7601dbaad0d..defb948f4fb 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,17 +1,17 @@ --- layout: page -title: AddressBook Level-3 +title: Foster Family --- -[![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/se-edu/addressbook-level3/workflows/Java%20CI/badge.svg)](https://github.com/AY2324S1-CS2103T-T13-4/tp/actions) +[![codecov](https://codecov.io/gh/se-edu/addressbook-level3/branch/master/graph/badge.svg)](https://app.codecov.io/gh/AY2324S1-CS2103T-T13-4/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). +**Foster Family is a desktop application for managing animal foster families.** While it has a GUI, most of the user interactions happen using a CLI (Command Line Interface). -* 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. +* If you are interested in using Foster Family, head over to the [_Quick Start_ section of the **User Guide**](https://ay2324s1-cs2103t-t13-4.github.io/tp/UserGuide.html#quick-start). +* If you are interested about developing Foster Family, the [**Developer Guide**](https://ay2324s1-cs2103t-t13-4.github.io/tp/DeveloperGuide.html) is a good place to start. **Acknowledgements** diff --git a/docs/team/brandon-nam.md b/docs/team/brandon-nam.md new file mode 100644 index 00000000000..f17bb21cc7a --- /dev/null +++ b/docs/team/brandon-nam.md @@ -0,0 +1,43 @@ +--- +layout: page +title: Brandon's Project Portfolio Page +--- + +### Project: Foster Family + +This is a desktop application for **managing animal foster families**. Our target audience are the foster managers of animal shelters who currently do not have a good logistical workflow to keep track of their fosterers. The user interacts with it using a CLI, and it has a GUI created with JavaFX. It is written in Java, and has about 10 kLoC. + +Given below are my contributions to the project. + +* **New Feature**: + + 1. Added the ability to view a fosterer's profile page + * What it does: Displays the chosen fosterer's details in full screen. + * Justification: There are many attributes added to a fosterer, such as AnimalName or Availability. Lengthy attributes can disrupt user experience, as users may forget the attribute names or waste time figuring out mistakes. Adding a profile page can show users which parameters they have forgotten, and easily spot input errors. + * Highlights: Implementing the GUI changing required a certain level of understanding of JavaFX. The feature took inspiration from how `HelpCommand` is implemented from the original addressbook. + * Pull requests: [#58](https://github.com/AY2324S1-CS2103T-T13-4/tp/pull/58) + 2. Added the ability of adding a new fosterer and edit a fosterer's details from a fosterer's profile page and save the changes. + * What it does: Using the profile UI and handlers implemented by my teammate Zhi Hong, user can edit a fosterer's detail, save the changes, and exit from the profile page. The users are also reminded of saving the changes if they attempt to exit without saving. + * Justification: Saving is essential part of using our profile UI. After the user is done editing or filling in the details of the new fosterer, only after entering save will the changes be reflected in the storage. + * Highlights: Implementing adding a new fosterer and saving changes required a thorough understanding of the overall structure as well as how other features are being implemented. For example, preventing adding an invalid fosterer detail required the understanding of checking validity of a fosterer's details, adding and editing a fosterer. + * Pull requests: [#88](https://github.com/AY2324S1-CS2103T-T13-4/tp/pull/88) +* **Documentation**: + * User Guide: + * Added documentation for the feature `edit`. + * Added documentation for the feature `view`. + * Added documentation for the feature `save`. + * Added a documentation for the feature `add` without any parameter. + * Added a note for the feature `exit` which behaves differently in the two different windows. + * Added a section of User Interface that explains the different windows we have. + * Developer Guide: + * Updated the class diagram for `Logic` class. + * Updated the class diagram for `Parser` classes. + * Added explanation of parsing commands in profile page. + * Added explanation of assigning UI change handler methods in `MainWindow` class. + * Added implementation details of editing in a profile page. + * Added implementation details of saving changes made in the profile page. + * Added implementation details of the exiting the profile page. +* **Project Mangement**: + * PR reviewed: [#59](https://github.com/AY2324S1-CS2103T-T13-4/tp/pull/59) + +* **Code contributed**: [RepoSense link](https://nus-cs2103-ay2324s1.github.io/tp-dashboard/?search=brandon-nam&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code&since=2023-09-22) diff --git a/docs/team/butteredyakiimo.md b/docs/team/butteredyakiimo.md new file mode 100644 index 00000000000..ee7873090ba --- /dev/null +++ b/docs/team/butteredyakiimo.md @@ -0,0 +1,72 @@ +--- +layout: page +title: Nicole's Project Portfolio Page +--- + +### Project: Foster Family +This is a desktop application for **managing animal foster families**. Our target audience are the foster managers of animal shelters who currently do not have a good logistical workflow to keep track of their fosterers. The user interacts with it using a CLI, and it has a GUI created with JavaFX. It is written in Java, and has about 10 kLoC. + +Given below are my contributions to the project. + +* **Code contributed**: [RepoSense link](https://nus-cs2103-ay2324s1.github.io/tp-dashboard/?search=butteredyakiimo&breakdown=false&sort=groupTitle%20dsc&sortWithin=title&since=2023-09-22&timeframe=commit&mergegroup=&groupSelect=groupByRepos) + + +* **New Feature**: Added the ability to view various statistics about fosterers. + * What it does: Performs calculations and summarises different types of information for the foster manager. The information available are about the available fosterers(those available to foster an animal), current fosterers (those currently fostering) and the different housing types of fosterers. + * Justification: This feature improves the product significantly, because the foster manager may have a large number of fosterers in the address book, and will require a quick and effective way to gain insights on the different types of fosterer information. + * Highlights: This features requires access to the different fields of a fosterer, which may have resulted in slightly tedious code to adhere to the SLAP principle. The implementation was also challenging, as it was built on top of new fields, requiring changes to existing classes. However, this feature can be easily extended in the future to account for new information that the foster manager wants to know about. + * Pull requests: [#86](https://github.com/AY2324S1-CS2103T-T13-4/tp/pull/86), [#96](https://github.com/AY2324S1-CS2103T-T13-4/tp/pull/96) + +
+ +* **Enhancement to existing feature**: Allow for mass deletions + * What it does: Allows the foster manager to enter more than one fosterer to delete. Extra blank spaces and duplicate indices will be ignored. + * Justification: In the previous implementation, the foster manager can either delete one fosterer at a time, or completely clear the address book of all data. Hence, implementing a multiple delete feature will allow the foster manager to delete more than one fosterer using just a single command, making deletes faster. + * Highlights: This feature requires tweaks to the existing `DeleteCommand`, and also a new class to encapsulate the multiple indices inputted. + * Pull requests: [#59](https://github.com/AY2324S1-CS2103T-T13-4/tp/pull/59), [#80](https://github.com/AY2324S1-CS2103T-T13-4/tp/pull/80) + +
+ +* **Other enhancements to existing features**: + * Designed the draft UI + * Updated main window GUI to follow draft UI, except tags (Pull requests: [#66](https://github.com/AY2324S1-CS2103T-T13-4/tp/pull/66)) + * Updated sample persons data for testing (Pull requests: [#86](https://github.com/AY2324S1-CS2103T-T13-4/tp/pull/86)) + +
+ +* **Documentation**: + * User Guide: + * Added documentation for the feature `delete` + * Added documentation for the feature `stats` + * Added useful notations for better readability + * Modified quick-start section to fit current project context and for better clarity + * Did cosmetic tweaks to existing documentation, for conversion to pdf + + * Developer Guide: + * Added implementation details of the `delete` feature + * Added sequence diagram for multiple `delete` feature + * Added implementation details of the `stats` feature + * Added `StatsCommand` class diagram + * Added sequence diagram for `stats` feature + * Updated `Model` class diagram + * Added user stories and use cases related to `stats` and `delete` + * Added planned enhancements for: + * Shorter command formats + * Reducing coupling between availability and animal type + * Supporting more animal types + * Allowing one fosterer to care for more than one animal + * Case-sensitivity of inputs + * Allowing symbols in name + * Pull requests: [#24](https://github.com/AY2324S1-CS2103T-T13-4/tp/pull/24), [#51](https://github.com/AY2324S1-CS2103T-T13-4/tp/pull/51), [#80](https://github.com/AY2324S1-CS2103T-T13-4/tp/pull/80), [#139](https://github.com/AY2324S1-CS2103T-T13-4/tp/pull/139), [#148](https://github.com/AY2324S1-CS2103T-T13-4/tp/pull/148) + +
+ +* **Project management**: + * Kept track of deliverables and deadlines + * Helped to maintain the issue tracker for milestones + * Managed releases `v1.2.1` and `v1.3.1` on GitHub + +
+ +* **Community**: + * PRs reviewed (with non-trivial review comments): [#63](https://github.com/AY2324S1-CS2103T-T13-4/tp/pull/63), [#73](https://github.com/AY2324S1-CS2103T-T13-4/tp/pull/73), [#82](https://github.com/AY2324S1-CS2103T-T13-4/tp/pull/82), diff --git a/docs/team/h1410101.md b/docs/team/h1410101.md new file mode 100644 index 00000000000..2d034cc2486 --- /dev/null +++ b/docs/team/h1410101.md @@ -0,0 +1,68 @@ +--- +layout: page +title: Zhi Hong's Project Portfolio Page +--- + +### Project: Foster Family +This is a desktop application for **managing animal foster families**. Our target audience are the foster managers of animal shelters who currently do not have a good logistical workflow to keep track of their fosterers. The user interacts with it using a CLI, and it has a GUI created with JavaFX. It is written in Java, and has about 10 kLoC. + +Given below are my contributions to the project. + +* **Code contributed**: [RepoSense link](https://nus-cs2103-ay2324s1.github.io/tp-dashboard/?search=h1410101&breakdown=false&sort=groupTitle%20dsc&sortWithin=title&since=2023-09-22&timeframe=commit&mergegroup=&groupSelect=groupByRepos) + +* **New Feature**: Designed and created a new Profile UI, to be used for `view`, `edit` and `add` commands. + * What it does: Displays all information regarding a Person, and allows the user to edit them. + * Checks user input, and does not accept invalid inputs. + * Checks the overall `Person` object based on all fields provided, and does not accept invalid combinations. + * If there are errors, gives descriptive textual feedback as well as highlighting the problematic field(s). + * In editing, allows the user to either confirm their changes or cancel and revert them. + * Justification: A far more natural interface than long one-liners otherwise required of the `edit` and `add` commands. + * Highlights: + * This involves a lot of JavaFX, and UI responsiveness in JavaFX is quite awkward. + * Design needed to be agreed upon in advance, since the UI is invoked by other commands written by teammates. + * Uses a simple Event Listener system, which is also used internally. + * Uses an Enum to refer to text fields of the Person. Rows in the UI are dynamically generated from these Enums, and focus can be requested of any specific field through a `setFocus` method by passing in an enum value. + * Dynamically handles any number of tags, and allows newlines for tags and notes. + * Pull requests: [#89](https://github.com/AY2324S1-CS2103T-T13-4/tp/pull/89), [#93](https://github.com/AY2324S1-CS2103T-T13-4/tp/pull/93) + +
+ +* **Small modification:** Added text notes to each `Person`, integrated with Profile UI. + * Bundled with Profile UI feature. + +
+ +* **Enhanced feature**: `find` command overhauled to include expressive search. + * What it does: Allows users to write keywords joined by `&` and `/`. + * Parses parentheses correctly, and implicitly closes them if possible. + * Searches every field of each `Person`. + * Allows double-quotes `"` for exact string match. + * Justification: Makes the `find` command more practical. + * Highlights: + * Includes an expression parser. + * Dynamically handles any fields and values reported by `Person`, including tags. + * Includes thorough testing with a lot of edge cases. + * Pull requests: [#68](https://github.com/AY2324S1-CS2103T-T13-4/tp/pull/68), [#83](https://github.com/AY2324S1-CS2103T-T13-4/tp/pull/83), [#90](https://github.com/AY2324S1-CS2103T-T13-4/tp/pull/90) + +
+ +* **Reviews** (with non-trivial review comments): [#31](https://github.com/AY2324S1-CS2103T-T13-4/tp/pull/31), [#58](https://github.com/AY2324S1-CS2103T-T13-4/tp/pull/58), [#59](https://github.com/AY2324S1-CS2103T-T13-4/tp/pull/59), [#92](https://github.com/AY2324S1-CS2103T-T13-4/tp/pull/92) + +
+ +* **Documentation**: + * User Guide: + * Added documentation for the feature `list`. + * Developer Guide: + * Added user story for `list` feature. + * Modified class diagram for `UI` to include Profile UI. + * Added implementation details of the `view` feature (Profile UI). + * Added class diagram for `view` feature. + * Added sequence diagram for `view` feature. + * Added implementation details of the `list` feature. + * Added class diagram for `list` feature. + * Added sequence diagram for `list` feature. + * Added sequence diagram for lower level details of `list` feature. + * Added object diagram for example of `list` feature. + * Added sequence diagram for example of `list` feature. + * Pull requests: [#28](https://github.com/AY2324S1-CS2103T-T13-4/tp/pull/28), [#83](https://github.com/AY2324S1-CS2103T-T13-4/tp/pull/83), [#140](https://github.com/AY2324S1-CS2103T-T13-4/tp/pull/140), [#151](https://github.com/AY2324S1-CS2103T-T13-4/tp/pull/151) diff --git a/docs/team/nananakx-x.md b/docs/team/nananakx-x.md new file mode 100644 index 00000000000..5cd900f7311 --- /dev/null +++ b/docs/team/nananakx-x.md @@ -0,0 +1,91 @@ +--- +layout: page +title: Nanette's Project Portfolio Page +--- + +### Project: Foster Family + +This is a desktop application for **managing animal foster families**. Our target audience are the foster managers of animal shelters who currently do not have a good +logistical workflow to keep track of their fosterers. The user interacts with it using a CLI, and it has a GUI created with JavaFX. It is written in Java, and has about 10 +kLoC. + +Given below are my contributions to the project. + +* **Code contributed**: [RepoSense link](https://nus-cs2103-ay2324s1.github.io/tp-dashboard/?search=nananakx-x&breakdown=false&sort=groupTitle%20dsc&sortWithin=title&since=2023-09-22&timeframe=commit&mergegroup=&groupSelect=groupByRepos) + +* **New Feature**: Added the ability to sort the fosterers list in the main window. + * What it does: Sorts the list of fosterers alphabetically by name (uppercase letters come before lowercase letters). + * Justification: This feature improves the product because it allows the foster manager to retrieve information efficiently at a glance, without having to type + the `find` command if the name of the fosterer is known. + * Highlights: This feature requires the use of a comparator, specified based on the person's name. It can be easily extended in the future to account for new sorting criteria. + * Pull requests: [#73](https://github.com/AY2324S1-CS2103T-T13-4/tp/pull/73) + + +* **Enhancements to existing features**: Allow for new fields like `housing`, `availability`, `animalName` and `animalType` to be added with a fosterer entry. + * What it does: Allows the foster manager to enter more essential attributes related to a fosterer when the `add` command is executed. + * Justification: In the previous implementation, the foster manager can only add name, email, phone number and address which is not well-suited for the + needs of the target user since crucial information like housing type should also be taken into account, given the context of managing fosterers for cat and dog shelters. + * Highlights: This feature requires tweaks to the existing `Person` class, and also new classes to encapsulate the new attributes. Furthermore, the implementation was also + challenging when coming up with methods to check if the combination of data / data inputted is valid are implemented since there were multiples cases to consider. Constant + changes were made to improve on the checks and usefulness of error messages. + * Pull requests: [#63](https://github.com/AY2324S1-CS2103T-T13-4/tp/pull/63), [#73](https://github.com/AY2324S1-CS2103T-T13-4/tp/pull/73), + [#82](https://github.com/AY2324S1-CS2103T-T13-4/tp/pull/82) + + +* **Other enhancements to existing features**: + * Updated the UI for the person card such that the new fields are displayed according to the draft UI. (Pull requests: + [#63](https://github.com/AY2324S1-CS2103T-T13-4/tp/pull/63)) + * Implemented checks to ensure that the fosterer entries are valid before they can be added. (Pull requests: + [#63](https://github.com/AY2324S1-CS2103T-T13-4/tp/pull/63), [#73](https://github.com/AY2324S1-CS2103T-T13-4/tp/pull/73), + [#82](https://github.com/AY2324S1-CS2103T-T13-4/tp/pull/82)) + * Implemented error messages to guide the users on how to rectify the errors which resulted in an invalid fosterer entry. + (Pull requests: [#63](https://github.com/AY2324S1-CS2103T-T13-4/tp/pull/63), [#73](https://github.com/AY2324S1-CS2103T-T13-4/tp/pull/73), + [#82](https://github.com/AY2324S1-CS2103T-T13-4/tp/pull/82)) + * Improved on duplicate persons check such that it is now case-insensitive and multiple spaces between words will be ignored. + (Pull requests: [#91](https://github.com/AY2324S1-CS2103T-T13-4/tp/pull/91)) + + +* **Documentation**: + * User Guide: + * Added documentation for the feature `add`, through both the main window and profile page + * Added documentation for the feature `sort` + * Added documentation for the feature `undo`, `reset` and `reset confirm` + * Added technical terms section for better readability + * Modified the `edit` section by separating it into two separate features - editing through the main window and editing through the profile page, for better + readability + * Updated and maintained the command summary + * Did cosmetic tweaks to existing documentation + + * Developer Guide: + * Added implementation details of the `add` feature. + * Added sequence diagram for `add` feature + * Added activity diagram for `add` feature + * Added implementation details of the `sort` feature + * Added sequence diagram for `sort` feature + * Added user stories and use cases related to `add`, `sort` and `undo` + * Added non-functional requirements and manual testing of `add` and `sort` + * Added planned enhancements for: + * Phone number input + * Guide Users on How To Rectify / Preventing the Corruption of Data File + * Notes feature as a separate command + * Specificity of error messages + + * Pull requests: [#30](https://github.com/AY2324S1-CS2103T-T13-4/tp/pull/30), [#48](https://github.com/AY2324S1-CS2103T-T13-4/tp/pull/48), + [#65](https://github.com/AY2324S1-CS2103T-T13-4/tp/pull/65), [#73](https://github.com/AY2324S1-CS2103T-T13-4/tp/pull/73), + [#82](https://github.com/AY2324S1-CS2103T-T13-4/tp/pull/82), [#91](https://github.com/AY2324S1-CS2103T-T13-4/tp/pull/91), + [#98](https://github.com/AY2324S1-CS2103T-T13-4/tp/pull/98), [#144](https://github.com/AY2324S1-CS2103T-T13-4/tp/pull/144), + [#147](https://github.com/AY2324S1-CS2103T-T13-4/tp/pull/147), [#149](https://github.com/AY2324S1-CS2103T-T13-4/tp/pull/149), + [#161](https://github.com/AY2324S1-CS2103T-T13-4/tp/pull/161) + + +* **Project management**: + * Helped to set up the GitHub team org/repo, codecov and the project website + * Kept track of deliverables and deadlines + * Helped to maintain the issue tracker for milestones + * Helped with demo video + + +* **Community**: + * PRs reviewed (with non-trivial review comments): [#67](https://github.com/AY2324S1-CS2103T-T13-4/tp/pull/67), + [#80](https://github.com/AY2324S1-CS2103T-T13-4/tp/pull/80), [#84](https://github.com/AY2324S1-CS2103T-T13-4/tp/pull/84) + diff --git a/docs/team/tyruslye.md b/docs/team/tyruslye.md new file mode 100644 index 00000000000..33c427f16bd --- /dev/null +++ b/docs/team/tyruslye.md @@ -0,0 +1,49 @@ +--- +layout: page +title: Tyrus's Project Portfolio Page +--- + +### Project: Foster Family + +This is a desktop application for **managing animal foster families**. Our target audience are the foster managers of animal shelters who currently do not have a good logistical workflow to keep track of their fosterers. The user interacts with it using a CLI, and it has a GUI created with JavaFX. It is written in Java, and has about 10 kLoC. + +Given below are my contributions to the project. + +* **Code contributed**: [RepoSense link](https://nus-cs2103-ay2324s1.github.io/tp-dashboard/?search=tyruslye&breakdown=false&sort=groupTitle%20dsc&sortWithin=title&since=2023-09-22&timeframe=commit&mergegroup=&groupSelect=groupByRepos) + + +* **New Feature**: Added the ability to undo the last command that was executed + * What it does: Undoes the last command that was executed. + * Justification: Accidental edits and deletes can happen and having a means of reversing any unwanted edits allows users more leniency when making changes. + * Highlights: Through experimenting with many methods of implementing an undo function, and due to the nature of the features of the application, the simplest method of state saving was used. + * Pull requests: [#94](https://github.com/AY2324S1-CS2103T-T13-4/tp/pull/94) + + +* **Enhancements to existing feature**: Made the `reset` command require confirmation before execution + * What it does: Instead of entering reset to erase the entire contents of the Address Book, the user is now required to enter `reset` followed by `reset confirm` to execute the command. + * Justification: Due to its highly catastrophic consequence in the event of a accidental execution of this command, it was decided that the user needed to be prompted to confirm their decision before the command is executed to prevent accidental deletion of the entire Addressbook. + * Highlights: This feature high-jacks and overrides an existing method to ensure state consistency between different command execution instances. + * Pull requests: [#143](https://github.com/AY2324S1-CS2103T-T13-4/tp/pull/143) + + +* **Other enhancements to existing features**: + * Changed the `clear` command word to `reset`. + * Made the commands `list` and `find` synonymous. + + +* **Documentation**: + * User Guide: + * Added documentation for the feature `reset`. + * Added documentation for the feature `help`. + * Added documentation for the feature `exit`. + * Added documentation for the feature `undo`. + * Developer Guide: + * Added implementation details of the `reset` feature. + * Updated the class diagram for `reset` feature. + * Added the activity diagram for `reset` feature. + * Added implementation details of the `help` feature. + * Added implementation details of the `exit` feature. + * Added implementation details of the `undo` feature. + * Updated the class diagram for `undo` feature. + * Added the activity diagram for `undo` feature. + 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..2ce695ad86c 100644 --- a/src/main/java/seedu/address/commons/core/index/Index.java +++ b/src/main/java/seedu/address/commons/core/index/Index.java @@ -4,14 +4,14 @@ /** * Represents a zero-based or one-based index. - * + *

* {@code Index} should be used right from the start (when parsing in a new user input), so that if the current * component wants to communicate with another component, it can send an {@code Index} to avoid having to know what * base the other component is using for its index. However, after receiving the {@code Index}, that component can * convert it back to an int if the index will not be passed to a different component again. */ -public class Index { - private int zeroBasedIndex; +public class Index implements Comparable { + private final int zeroBasedIndex; /** * Index can only be created by calling {@link Index#fromZeroBased(int)} or @@ -47,6 +47,11 @@ public static Index fromOneBased(int oneBasedIndex) { return new Index(oneBasedIndex - 1); } + @Override + public int compareTo(Index other) { + return this.zeroBasedIndex - other.zeroBasedIndex; + } + @Override public boolean equals(Object other) { if (other == this) { diff --git a/src/main/java/seedu/address/commons/core/index/Indices.java b/src/main/java/seedu/address/commons/core/index/Indices.java new file mode 100644 index 00000000000..80b2abbcbe1 --- /dev/null +++ b/src/main/java/seedu/address/commons/core/index/Indices.java @@ -0,0 +1,141 @@ +package seedu.address.commons.core.index; + +import java.util.Collections; +import java.util.SortedSet; +import java.util.TreeSet; + +import seedu.address.commons.util.ToStringBuilder; + +/** + * Represents one or more sorted, unique Index objects. + *

+ * All Index objects in Indices have to be either zero-based or one-based. Similar to {@code Index}, + * it should be used right from the start when parsing new user input that allows for more than + * one index provided. + */ +public class Indices { + private final SortedSet zeroBasedIndices; + private int size; + + /** + * Indices can only be created by calling {@link Indices#fromZeroBased} or + * {@link Indices#fromOneBased(int[])}. + */ + private Indices(SortedSet zeroBasedIndices) { + this.zeroBasedIndices = zeroBasedIndices; + this.size = zeroBasedIndices.size(); + } + + /** + * Creates a new {@code Indices} using zero-based indices. + */ + public static Indices fromZeroBased(int[] zeroBasedIndices) { + assert (zeroBasedIndices != null); + SortedSet result = new TreeSet<>(); + + for (int index : zeroBasedIndices) { + Index zeroBasedIndex = Index.fromZeroBased(index); + result.add(zeroBasedIndex); + } + return new Indices(result); + } + + /** + * Creates a new {@code Indices} using one-based indices. + */ + public static Indices fromOneBased(int[] oneBasedIndices) { + assert (oneBasedIndices != null); + SortedSet result = new TreeSet<>(); + + for (int index : oneBasedIndices) { + Index oneBasedIndex = Index.fromOneBased(index); + result.add(oneBasedIndex); + } + return new Indices(result); + } + + public int[] getZeroBased() { + return zeroBasedIndices.stream() + .mapToInt(Index::getZeroBased) + .toArray(); + } + + public int[] getOneBased() { + return zeroBasedIndices.stream() + .mapToInt(Index::getOneBased) + .toArray(); + } + + /** + * Returns a white space separated string of zero-based indices. + */ + public String getZeroBasedString() { + int[] zeroBased = this.getZeroBased(); + + StringBuilder result = new StringBuilder(); + for (int i = 0; i < size - 1; i++) { + result.append(zeroBased[i]) + .append(" "); + } + result.append(zeroBased[size - 1]); + return result.toString(); + } + + /** + * Returns a white space separated string of one-based indices. + */ + public String getOneBasedString() { + int[] oneBased = this.getOneBased(); + + StringBuilder result = new StringBuilder(); + for (int i = 0; i < size - 1; i++) { + result.append(oneBased[i]) + .append(" "); + } + result.append(oneBased[size - 1]); + return result.toString(); + } + + public int getSize() { + return this.size; + } + + /** + * Returns the smallest zero-based index in indices. + */ + public int getZeroBasedMin() { + return Collections.min(this.zeroBasedIndices).getZeroBased(); + } + + /** + * Returns the largest zero-based index in indices. + */ + public int getZeroBasedMax() { + return Collections.max(this.zeroBasedIndices).getZeroBased(); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof Indices)) { + return false; + } + + Indices otherIndices = (Indices) other; + + if (this.size != otherIndices.size) { + return false; + } + return this.zeroBasedIndices.equals(otherIndices.zeroBasedIndices); + } + + @Override + public String toString() { + return new ToStringBuilder(this).add("zeroBasedIndices", + this.getZeroBasedString()).toString(); + } +} diff --git a/src/main/java/seedu/address/commons/exceptions/DataLoadingException.java b/src/main/java/seedu/address/commons/exceptions/DataLoadingException.java index 9904ba47afe..af120ba8fc8 100644 --- a/src/main/java/seedu/address/commons/exceptions/DataLoadingException.java +++ b/src/main/java/seedu/address/commons/exceptions/DataLoadingException.java @@ -8,4 +8,5 @@ public DataLoadingException(Exception cause) { super(cause); } + } diff --git a/src/main/java/seedu/address/logic/Logic.java b/src/main/java/seedu/address/logic/Logic.java index 92cd8fa605a..eaf1d3629c8 100644 --- a/src/main/java/seedu/address/logic/Logic.java +++ b/src/main/java/seedu/address/logic/Logic.java @@ -1,9 +1,12 @@ package seedu.address.logic; +import java.io.IOException; import java.nio.file.Path; import javafx.collections.ObservableList; import seedu.address.commons.core.GuiSettings; +import seedu.address.commons.core.index.Index; +import seedu.address.commons.exceptions.DataLoadingException; import seedu.address.logic.commands.CommandResult; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.logic.parser.exceptions.ParseException; @@ -21,7 +24,8 @@ public interface Logic { * @throws CommandException If an error occurs during command execution. * @throws ParseException If an error occurs during parsing. */ - CommandResult execute(String commandText) throws CommandException, ParseException; + CommandResult execute(String commandText) throws CommandException, ParseException, IOException, + DataLoadingException; /** * Returns the AddressBook. @@ -47,4 +51,9 @@ public interface Logic { * Set the user prefs' GUI settings. */ void setGuiSettings(GuiSettings guiSettings); + + CommandResult executeInView(String commandText, Person person, Index targetIndex) + throws CommandException, ParseException; + + boolean getFinalConfirmation(); } diff --git a/src/main/java/seedu/address/logic/LogicManager.java b/src/main/java/seedu/address/logic/LogicManager.java index 5aa3b91c7d0..62fa65df718 100644 --- a/src/main/java/seedu/address/logic/LogicManager.java +++ b/src/main/java/seedu/address/logic/LogicManager.java @@ -8,13 +8,20 @@ import javafx.collections.ObservableList; import seedu.address.commons.core.GuiSettings; import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.core.index.Index; +import seedu.address.commons.exceptions.DataLoadingException; import seedu.address.logic.commands.Command; import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.CommandType; +import seedu.address.logic.commands.UndoCommand; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.logic.parser.AddressBookParser; +import seedu.address.logic.parser.ViewModeParser; import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.Model; +import seedu.address.model.ModelManager; import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.UserPrefs; import seedu.address.model.person.Person; import seedu.address.storage.Storage; @@ -26,12 +33,14 @@ public class LogicManager implements Logic { public static final String FILE_OPS_PERMISSION_ERROR_FORMAT = "Could not save data to file %s due to insufficient permissions to write to the file or the folder."; - private final Logger logger = LogsCenter.getLogger(LogicManager.class); private final Model model; private final Storage storage; private final AddressBookParser addressBookParser; + private final ViewModeParser viewModeParser; + private final Model backupModel; + private boolean finalConfirmation = false; /** * Constructs a {@code LogicManager} with the given {@code Model} and {@code Storage}. @@ -40,14 +49,62 @@ public LogicManager(Model model, Storage storage) { this.model = model; this.storage = storage; addressBookParser = new AddressBookParser(); + viewModeParser = new ViewModeParser(); + this.backupModel = new ModelManager(model.getAddressBook(), new UserPrefs()); + Path mainPath = Path.of("data\\addressbookbackup.json"); + backupModel.setAddressBookFilePath(mainPath); } @Override - public CommandResult execute(String commandText) throws CommandException, ParseException { + public CommandResult execute(String commandText) throws CommandException, ParseException, IOException, + DataLoadingException { logger.info("----------------[USER COMMAND][" + commandText + "]"); + Command command; + CommandResult commandResult; + command = addressBookParser.parseCommand(commandText); + //checking for undo command + if (command instanceof UndoCommand) { + if (backupModel.getAddressBook().equals(model.getAddressBook())) { + commandResult = command.execute(null); + return commandResult; + } else { + model.setAddressBook(backupModel.getAddressBook()); + } + } else { + backupModel.setAddressBook(model.getAddressBook()); + } + + if (this.finalConfirmation) { + command.toString(); + } + + commandResult = command.execute(model); + if (commandResult.getCommandType() == CommandType.CLEAR) { + this.finalConfirmation = true; + } else { + this.finalConfirmation = false; + } + + try { + storage.saveAddressBook(model.getAddressBook()); + } catch (AccessDeniedException e) { + throw new CommandException(String.format(FILE_OPS_PERMISSION_ERROR_FORMAT, e.getMessage()), e); + } catch (IOException ioe) { + throw new CommandException(String.format(FILE_OPS_ERROR_FORMAT, ioe.getMessage()), ioe); + } + + return commandResult; + } + + @Override + public CommandResult executeInView(String commandText, Person newPerson, Index targetIndex) + throws CommandException, ParseException { + logger.info("----------------[USER COMMAND][" + commandText + "]"); + Command command; CommandResult commandResult; - Command command = addressBookParser.parseCommand(commandText); + + command = viewModeParser.parseCommand(commandText, newPerson, targetIndex); commandResult = command.execute(model); try { @@ -85,4 +142,8 @@ public GuiSettings getGuiSettings() { public void setGuiSettings(GuiSettings guiSettings) { model.setGuiSettings(guiSettings); } + + public boolean getFinalConfirmation() { + return this.finalConfirmation; + } } diff --git a/src/main/java/seedu/address/logic/Messages.java b/src/main/java/seedu/address/logic/Messages.java index ecd32c31b53..082cab31ce5 100644 --- a/src/main/java/seedu/address/logic/Messages.java +++ b/src/main/java/seedu/address/logic/Messages.java @@ -13,11 +13,13 @@ public class Messages { public static final String MESSAGE_UNKNOWN_COMMAND = "Unknown command"; + public static final String MESSAGE_UNAVAILABLE_COMMAND_IN_VIEW_MODE = + "The command is unavailable in view profile mode."; public static final String MESSAGE_INVALID_COMMAND_FORMAT = "Invalid command format! \n%1$s"; - public static final String MESSAGE_INVALID_PERSON_DISPLAYED_INDEX = "The person index provided is invalid"; - public static final String MESSAGE_PERSONS_LISTED_OVERVIEW = "%1$d persons listed!"; + public static final String MESSAGE_INVALID_PERSON_DISPLAYED_INDEX = "The fosterer index provided is invalid."; + public static final String MESSAGE_PERSONS_LISTED_OVERVIEW = "%1$d fosterers listed!"; public static final String MESSAGE_DUPLICATE_FIELDS = - "Multiple values specified for the following single-valued field(s): "; + "Multiple values specified for the following single-valued field(s): "; /** * Returns an error message indicating the duplicate prefixes. @@ -43,8 +45,34 @@ public static String format(Person person) { .append(person.getEmail()) .append("; Address: ") .append(person.getAddress()) - .append("; Tags: "); + .append("; Housing: ") + .append(person.getHousing()) + .append("; Availability: ") + .append(person.getAvailability()) + .append("; Animal name: ") + .append(person.getAnimalName()) + .append("; Animal type: ") + .append(person.getAnimalType()); + + builder.append("; Tags: "); person.getTags().forEach(builder::append); + + return builder.toString(); + } + + + /** + * Formats the {@code people} for display to the user. + */ + public static String format(Person[] people) { + int size = people.length; + final StringBuilder builder = new StringBuilder(); + + for (int i = 0; i < size - 1; i++) { + builder.append(format(people[i])) + .append(", \n"); + } + builder.append(format(people[size - 1])); 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..3a45c3f1ab3 100644 --- a/src/main/java/seedu/address/logic/commands/AddCommand.java +++ b/src/main/java/seedu/address/logic/commands/AddCommand.java @@ -2,7 +2,11 @@ import static java.util.Objects.requireNonNull; import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ANIMAL_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ANIMAL_TYPE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_AVAILABILITY; import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_HOUSING; 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; @@ -26,16 +30,25 @@ public class AddCommand extends Command { + PREFIX_PHONE + "PHONE " + PREFIX_EMAIL + "EMAIL " + PREFIX_ADDRESS + "ADDRESS " + + PREFIX_HOUSING + "HOUSING " + + PREFIX_AVAILABILITY + "AVAILABILITY " + + PREFIX_ANIMAL_NAME + "ANIMAL_NAME " + + PREFIX_ANIMAL_TYPE + "ANIMAL_TYPE " + "[" + PREFIX_TAG + "TAG]...\n" + + "Note: " + "If information for that field is not available, put 'nil'.\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_HOUSING + "HDB " + + PREFIX_AVAILABILITY + "NotAvailable " + + PREFIX_ANIMAL_NAME + "Dexter " + + PREFIX_ANIMAL_TYPE + "current.Dog " + + PREFIX_TAG + "Urgent " + + PREFIX_TAG + "goodWithDogs"; - public static final String MESSAGE_SUCCESS = "New person added: %1$s"; + public static final String MESSAGE_SUCCESS = "New fosterer added: %1$s"; public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book"; private final Person toAdd; diff --git a/src/main/java/seedu/address/logic/commands/ClearCommand.java b/src/main/java/seedu/address/logic/commands/ClearCommand.java index 9c86b1fa6e4..4c16ade61cd 100644 --- a/src/main/java/seedu/address/logic/commands/ClearCommand.java +++ b/src/main/java/seedu/address/logic/commands/ClearCommand.java @@ -2,6 +2,8 @@ import static java.util.Objects.requireNonNull; +import java.util.Objects; + import seedu.address.model.AddressBook; import seedu.address.model.Model; @@ -9,15 +11,65 @@ * Clears the address book. */ public class ClearCommand extends Command { - - public static final String COMMAND_WORD = "clear"; + public static final String COMMAND_WORD = "reset"; public static final String MESSAGE_SUCCESS = "Address book has been cleared!"; + public static final String MESSAGE_USAGE = "The 'reset' command will erase the entire Address Book.\n" + + "Are you sure you want to do this? " + + "If you are sure, Enter 'reset confirm'."; + public static final String MESSAGE_PROMPT = "To reset the entire Address Book, " + + "enter 'reset' followed by 'reset confirm'."; + + private final String confirmation; + private boolean finalConfirmation = false; + + /** + * Constructs a ClearCommand with the provided confirmation. + * + * @param confirmation The user's confirmation. Must be "confirm" to execute the clear command. + * @throws NullPointerException if the confirmation is null. + */ + public ClearCommand(String confirmation) { + if (confirmation == null) { + throw new NullPointerException("Confirmation cannot be null."); + } + this.confirmation = confirmation; + } @Override public CommandResult execute(Model model) { - requireNonNull(model); - model.setAddressBook(new AddressBook()); - return new CommandResult(MESSAGE_SUCCESS); + if (Objects.equals(this.confirmation, "confirm") && this.finalConfirmation) { + requireNonNull(model); + model.setAddressBook(new AddressBook()); + return new CommandResult(MESSAGE_SUCCESS); + } else if (Objects.equals(this.confirmation, "confirm")) { + return new CommandResult(MESSAGE_PROMPT); + } else { + return new CommandResult(MESSAGE_USAGE, CommandType.CLEAR); + } + + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + ClearCommand other = (ClearCommand) obj; + return Objects.equals(this.confirmation, other.confirmation); + } + + @Override + public int hashCode() { + return Objects.hash(confirmation); + } + + @Override + public String toString() { + this.finalConfirmation = true; + return this.confirmation; } } diff --git a/src/main/java/seedu/address/logic/commands/CommandResult.java b/src/main/java/seedu/address/logic/commands/CommandResult.java index 249b6072d0d..734e5f7d1e9 100644 --- a/src/main/java/seedu/address/logic/commands/CommandResult.java +++ b/src/main/java/seedu/address/logic/commands/CommandResult.java @@ -4,7 +4,9 @@ import java.util.Objects; +import seedu.address.commons.core.index.Index; import seedu.address.commons.util.ToStringBuilder; +import seedu.address.model.person.Person; /** * Represents the result of a command execution. @@ -13,19 +15,45 @@ public class CommandResult { private final String feedbackToUser; - /** Help information should be shown to the user. */ - private final boolean showHelp; + /** + * Help information should be shown to the user. + */ + + private final Person personToView; + + private final Index targetIndex; + + private final boolean isFostererEdited; - /** The application should exit. */ - private final boolean exit; + private final CommandType commandType; /** * Constructs a {@code CommandResult} with the specified fields. */ - public CommandResult(String feedbackToUser, boolean showHelp, boolean exit) { + public CommandResult( + String feedbackToUser, + Person personToView, + Index targetIndex, + CommandType commandType, + boolean isFostererEdited + ) { this.feedbackToUser = requireNonNull(feedbackToUser); - this.showHelp = showHelp; - this.exit = exit; + this.personToView = personToView; + this.targetIndex = targetIndex; + this.commandType = commandType; + this.isFostererEdited = isFostererEdited; + } + + /** + * Constructs a {@code CommandResult} with the specified {@code feedbackToUser}, + * and other fields set to their default value. + */ + public CommandResult(String feedbackToUser, CommandType commandType) { + this.feedbackToUser = requireNonNull(feedbackToUser); + this.personToView = null; + this.targetIndex = null; + this.commandType = commandType; + this.isFostererEdited = false; } /** @@ -33,19 +61,27 @@ public CommandResult(String feedbackToUser, boolean showHelp, boolean exit) { * and other fields set to their default value. */ public CommandResult(String feedbackToUser) { - this(feedbackToUser, false, false); + this(feedbackToUser, null); } public String getFeedbackToUser() { return feedbackToUser; } - public boolean isShowHelp() { - return showHelp; + public Person getPersonToView() { + return personToView; + } + + public Index getTargetIndex() { + return targetIndex; + } + + public CommandType getCommandType() { + return commandType; } - public boolean isExit() { - return exit; + public boolean getIsFostererEdited() { + return isFostererEdited; } @Override @@ -61,22 +97,27 @@ public boolean equals(Object other) { CommandResult otherCommandResult = (CommandResult) other; return feedbackToUser.equals(otherCommandResult.feedbackToUser) - && showHelp == otherCommandResult.showHelp - && exit == otherCommandResult.exit; + && Objects.equals(personToView, otherCommandResult.personToView) + && Objects.equals(targetIndex, otherCommandResult.targetIndex) + && commandType == otherCommandResult.commandType + && isFostererEdited == otherCommandResult.isFostererEdited; } @Override public int hashCode() { - return Objects.hash(feedbackToUser, showHelp, exit); + return Objects.hash(feedbackToUser, personToView, targetIndex, commandType, isFostererEdited); } @Override public String toString() { - return new ToStringBuilder(this) + ToStringBuilder t = new ToStringBuilder(this) .add("feedbackToUser", feedbackToUser) - .add("showHelp", showHelp) - .add("exit", exit) - .toString(); + .add("person", personToView) + .add("targetIndex", targetIndex) + .add("commandType", commandType) + .add("isFostererEdited", isFostererEdited); + return t.toString(); } } + diff --git a/src/main/java/seedu/address/logic/commands/CommandType.java b/src/main/java/seedu/address/logic/commands/CommandType.java new file mode 100644 index 00000000000..e8cea57f4c2 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/CommandType.java @@ -0,0 +1,34 @@ +package seedu.address.logic.commands; + + + +/** + * Represents the types of commands, which are used to detect them and set appropriate handlers for teach type in + * MainWindow. + */ +public enum CommandType { + ADD("Add Command"), + CLEAR("Clear Command"), + DELETE("Delete Command"), + EDIT("Edit Command"), + EDIT_FIELD("Edit Field Command"), + EXIT("Exit Command"), + FIND("Find Command"), + HELP("Help Command"), + LIST("List Command"), + SAVE("Save Command"), + SORT("Sort Command"), + VIEW("View Command"), + VIEW_EXIT("View Exit Command"); + + private String commandName; + + CommandType(String commandName) { + this.commandName = commandName; + } + + @Override + public String toString() { + return commandName; + } +} diff --git a/src/main/java/seedu/address/logic/commands/DeleteCommand.java b/src/main/java/seedu/address/logic/commands/DeleteCommand.java index 1135ac19b74..82be6a2ee7d 100644 --- a/src/main/java/seedu/address/logic/commands/DeleteCommand.java +++ b/src/main/java/seedu/address/logic/commands/DeleteCommand.java @@ -3,8 +3,11 @@ import static java.util.Objects.requireNonNull; import java.util.List; +import java.util.logging.Logger; -import seedu.address.commons.core.index.Index; +import seedu.address.MainApp; +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.core.index.Indices; import seedu.address.commons.util.ToStringBuilder; import seedu.address.logic.Messages; import seedu.address.logic.commands.exceptions.CommandException; @@ -12,37 +15,80 @@ import seedu.address.model.person.Person; /** - * Deletes a person identified using it's displayed index from the address book. + * Deletes one or more fosterers identified using their displayed index from the address book. */ 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 fosterers identified by the index number used in the displayed person list.\n" + + "Parameters: INDEX [INDEX...] (must be a positive integer)\n" + + "Example: " + COMMAND_WORD + " 1 2 3"; - public static final String MESSAGE_DELETE_PERSON_SUCCESS = "Deleted Person: %1$s"; + public static final String MESSAGE_DELETE_PERSON_SUCCESS = "1 fosterer deleted:\n%1$s"; - private final Index targetIndex; + public static final String MESSAGE_DELETE_PEOPLE_SUCCESS = "%1$d fosterers deleted:\n%2$s"; - public DeleteCommand(Index targetIndex) { - this.targetIndex = targetIndex; + private static final Logger logger = LogsCenter.getLogger(MainApp.class); + + private final Indices targetIndices; + + public DeleteCommand(Indices targetIndices) { + this.targetIndices = targetIndices; + } + + /** + * Checks if zero-based target index is valid. + */ + public static boolean isValidIndex(int targetIndex, int listSize) throws CommandException { + if (targetIndex >= listSize || targetIndex < 0) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + return true; + } + + /** + * Returns the people to delete, given the last shown list of Persons. + * + * @throws CommandException when target index is out of bounds. + */ + public Person[] getPeopleToDelete(List lastShownList) throws CommandException { + logger.info("Retrieving people to delete from last shown list."); + int[] zeroBasedIndices = this.targetIndices.getZeroBased(); + int numberOfDeletions = this.targetIndices.getSize(); + Person[] peopleToDelete = new Person[numberOfDeletions]; + + for (int i = 0; i < numberOfDeletions; i++) { + int targetIndex = zeroBasedIndices[i]; + + if (isValidIndex(targetIndex, lastShownList.size())) { + Person personToDelete = lastShownList.get(targetIndex); + peopleToDelete[i] = personToDelete; + } + } + return peopleToDelete; + } + + public String getDeleteResultMessage(Person[] peopleToDelete) { + int numberDeleted = peopleToDelete.length; + if (numberDeleted == 1) { + return String.format(MESSAGE_DELETE_PERSON_SUCCESS, Messages.format(peopleToDelete)); + } + return String.format(MESSAGE_DELETE_PEOPLE_SUCCESS, numberDeleted, Messages.format(peopleToDelete)); } @Override public CommandResult execute(Model model) throws CommandException { requireNonNull(model); List lastShownList = model.getFilteredPersonList(); + Person[] peopleToDelete = getPeopleToDelete(lastShownList); - if (targetIndex.getZeroBased() >= lastShownList.size()) { - throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + for (Person personToDelete : peopleToDelete) { + model.deletePerson(personToDelete); } - - Person personToDelete = lastShownList.get(targetIndex.getZeroBased()); - model.deletePerson(personToDelete); - return new CommandResult(String.format(MESSAGE_DELETE_PERSON_SUCCESS, Messages.format(personToDelete))); + String message = getDeleteResultMessage(peopleToDelete); + return new CommandResult(message); } @Override @@ -57,13 +103,13 @@ public boolean equals(Object other) { } DeleteCommand otherDeleteCommand = (DeleteCommand) other; - return targetIndex.equals(otherDeleteCommand.targetIndex); + return this.targetIndices.equals(otherDeleteCommand.targetIndices); } @Override public String toString() { return new ToStringBuilder(this) - .add("targetIndex", targetIndex) + .add("targetIndices", targetIndices) .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..f233999c57b 100644 --- a/src/main/java/seedu/address/logic/commands/EditCommand.java +++ b/src/main/java/seedu/address/logic/commands/EditCommand.java @@ -22,12 +22,14 @@ import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.model.Model; import seedu.address.model.person.Address; +import seedu.address.model.person.AnimalType; +import seedu.address.model.person.Availability; import seedu.address.model.person.Email; +import seedu.address.model.person.Housing; import seedu.address.model.person.Name; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; import seedu.address.model.tag.Tag; - /** * Edits the details of an existing person in the address book. */ @@ -49,13 +51,15 @@ public class EditCommand extends Command { + PREFIX_EMAIL + "johndoe@example.com"; 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 VIEWING_PROFILE_SUCCESS = "Viewing Person: %1$s"; public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book."; private final Index index; private final EditPersonDescriptor editPersonDescriptor; /** + * Represents a constructor of an EditCommand with an index of the person in the list to edit + * and the model person data to be edited. * @param index of the person in the filtered person list to edit * @param editPersonDescriptor details to edit the person with */ @@ -67,6 +71,18 @@ public EditCommand(Index index, EditPersonDescriptor editPersonDescriptor) { this.editPersonDescriptor = new EditPersonDescriptor(editPersonDescriptor); } + /** + * Represents an EditCommand that behaves the same as the ViewCommand which views the fosterer at the given index. + * + * @param index is the index of the fosterer in the list whose details will be edited. + */ + public EditCommand(Index index) { + requireNonNull(index); + + this.index = index; + this.editPersonDescriptor = null; + } + @Override public CommandResult execute(Model model) throws CommandException { requireNonNull(model); @@ -77,15 +93,29 @@ public CommandResult execute(Model model) throws CommandException { } Person personToEdit = lastShownList.get(index.getZeroBased()); - Person editedPerson = createEditedPerson(personToEdit, editPersonDescriptor); - - if (!personToEdit.isSamePerson(editedPerson) && model.hasPerson(editedPerson)) { - throw new CommandException(MESSAGE_DUPLICATE_PERSON); + if (editPersonDescriptor == null) { + return new CommandResult( + String.format(VIEWING_PROFILE_SUCCESS, Messages.format(personToEdit)), + personToEdit, + index, + CommandType.VIEW, + false + ); } - model.setPerson(personToEdit, editedPerson); - model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); - return new CommandResult(String.format(MESSAGE_EDIT_PERSON_SUCCESS, Messages.format(editedPerson))); + try { + Person editedPerson = createEditedPerson(personToEdit, editPersonDescriptor); + if (!personToEdit.isSamePerson(editedPerson) && model.hasPerson(editedPerson)) { + throw new CommandException(MESSAGE_DUPLICATE_PERSON); + } + + model.setPerson(personToEdit, editedPerson); + model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + return new CommandResult(String.format(MESSAGE_EDIT_PERSON_SUCCESS, Messages.format(editedPerson))); + + } catch (IllegalArgumentException e) { + throw new CommandException(e.getMessage()); + } } /** @@ -99,9 +129,21 @@ private static Person createEditedPerson(Person personToEdit, EditPersonDescript Phone updatedPhone = editPersonDescriptor.getPhone().orElse(personToEdit.getPhone()); Email updatedEmail = editPersonDescriptor.getEmail().orElse(personToEdit.getEmail()); Address updatedAddress = editPersonDescriptor.getAddress().orElse(personToEdit.getAddress()); + Housing updatedHousing = editPersonDescriptor.getHousing().orElse(personToEdit.getHousing()); + Availability updatedAvailability = + editPersonDescriptor.getAvailability().orElse(personToEdit.getAvailability()); + Name updatedAnimalName = editPersonDescriptor.getAnimalName().orElse(personToEdit.getAnimalName()); + AnimalType updatedAnimalType; + if (editPersonDescriptor.getAnimalType().isPresent()) { + updatedAnimalType = new AnimalType(editPersonDescriptor.getAnimalType().get(), updatedAvailability); + } else { + updatedAnimalType = personToEdit.getAnimalType(); + } Set updatedTags = editPersonDescriptor.getTags().orElse(personToEdit.getTags()); - return new Person(updatedName, updatedPhone, updatedEmail, updatedAddress, updatedTags); + return new Person(updatedName, updatedPhone, updatedEmail, updatedAddress, updatedHousing, + updatedAvailability, updatedAnimalName, updatedAnimalType, + updatedTags); } @Override @@ -137,6 +179,10 @@ public static class EditPersonDescriptor { private Phone phone; private Email email; private Address address; + private Housing housing; + private Availability availability; + private Name animalName; + private String animalType; private Set tags; public EditPersonDescriptor() {} @@ -150,6 +196,10 @@ public EditPersonDescriptor(EditPersonDescriptor toCopy) { setPhone(toCopy.phone); setEmail(toCopy.email); setAddress(toCopy.address); + setHousing(toCopy.housing); + setAvailability(toCopy.availability); + setAnimalName(toCopy.animalName); + setAnimalType(toCopy.animalType); setTags(toCopy.tags); } @@ -157,7 +207,17 @@ 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, + housing, + availability, + animalName, + animalType, + tags + ); } public void setName(Name name) { @@ -192,6 +252,35 @@ public Optional

getAddress() { return Optional.ofNullable(address); } + public void setHousing(Housing housing) { + this.housing = housing; + } + public Optional getHousing() { + return Optional.ofNullable(housing); + } + public void setAvailability(Availability availability) { + this.availability = availability; + } + public Optional getAvailability() { + return Optional.ofNullable(availability); + } + + public void setAnimalName(Name animalName) { + this.animalName = animalName; + } + + public Optional getAnimalName() { + return Optional.ofNullable(animalName); + } + + public void setAnimalType(String animalType) { + this.animalType = animalType; + } + + public Optional getAnimalType() { + return Optional.ofNullable(animalType); + } + /** * Sets {@code tags} to this object's {@code tags}. * A defensive copy of {@code tags} is used internally. @@ -225,6 +314,10 @@ public boolean equals(Object other) { && Objects.equals(phone, otherEditPersonDescriptor.phone) && Objects.equals(email, otherEditPersonDescriptor.email) && Objects.equals(address, otherEditPersonDescriptor.address) + && Objects.equals(housing, otherEditPersonDescriptor.housing) + && Objects.equals(availability, otherEditPersonDescriptor.availability) + && Objects.equals(animalName, otherEditPersonDescriptor.animalName) + && Objects.equals(animalType, otherEditPersonDescriptor.animalType) && Objects.equals(tags, otherEditPersonDescriptor.tags); } @@ -235,8 +328,15 @@ public String toString() { .add("phone", phone) .add("email", email) .add("address", address) + .add("housing", housing) + .add("availability", availability) + .add("animalName", animalName) + .add("animalType", animalType) .add("tags", tags) .toString(); } + + + } } diff --git a/src/main/java/seedu/address/logic/commands/EditFieldCommand.java b/src/main/java/seedu/address/logic/commands/EditFieldCommand.java new file mode 100644 index 00000000000..a06702a0eb9 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/EditFieldCommand.java @@ -0,0 +1,22 @@ +package seedu.address.logic.commands; + +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; + +/** + * Represents and EditFieldCommand in the profile view page which is executed + * when the name of the field is entered into the command box. + */ +public class EditFieldCommand extends Command { + + public static final String MESSAGE_SUCCESS = "Editing fosterer field"; + + public EditFieldCommand() { + super(); + } + + @Override + public CommandResult execute(Model model) throws CommandException { + return new CommandResult(MESSAGE_SUCCESS, CommandType.EDIT_FIELD); + } +} diff --git a/src/main/java/seedu/address/logic/commands/ExitCommand.java b/src/main/java/seedu/address/logic/commands/ExitCommand.java index 3dd85a8ba90..bb4e7239b35 100644 --- a/src/main/java/seedu/address/logic/commands/ExitCommand.java +++ b/src/main/java/seedu/address/logic/commands/ExitCommand.java @@ -13,7 +13,7 @@ public class ExitCommand extends Command { @Override public CommandResult execute(Model model) { - return new CommandResult(MESSAGE_EXIT_ACKNOWLEDGEMENT, false, true); + return new CommandResult(MESSAGE_EXIT_ACKNOWLEDGEMENT, CommandType.EXIT); } } diff --git a/src/main/java/seedu/address/logic/commands/FindCommand.java b/src/main/java/seedu/address/logic/commands/FindCommand.java index 72b9eddd3a7..0bd840a9d64 100644 --- a/src/main/java/seedu/address/logic/commands/FindCommand.java +++ b/src/main/java/seedu/address/logic/commands/FindCommand.java @@ -2,10 +2,12 @@ import static java.util.Objects.requireNonNull; +import java.util.function.Predicate; + import seedu.address.commons.util.ToStringBuilder; import seedu.address.logic.Messages; import seedu.address.model.Model; -import seedu.address.model.person.NameContainsKeywordsPredicate; +import seedu.address.model.person.Person; /** * Finds and lists all persons in address book whose name contains any of the argument keywords. @@ -18,11 +20,11 @@ public class FindCommand extends Command { 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" - + "Example: " + COMMAND_WORD + " alice bob charlie"; + + "Example: " + COMMAND_WORD + " alice bob charlie"; //todo update usage message - private final NameContainsKeywordsPredicate predicate; + private final Predicate predicate; - public FindCommand(NameContainsKeywordsPredicate predicate) { + public FindCommand(Predicate predicate) { this.predicate = predicate; } diff --git a/src/main/java/seedu/address/logic/commands/HelpCommand.java b/src/main/java/seedu/address/logic/commands/HelpCommand.java index bf824f91bd0..0fd4f7386b2 100644 --- a/src/main/java/seedu/address/logic/commands/HelpCommand.java +++ b/src/main/java/seedu/address/logic/commands/HelpCommand.java @@ -16,6 +16,7 @@ public class HelpCommand extends Command { @Override public CommandResult execute(Model model) { - return new CommandResult(SHOWING_HELP_MESSAGE, true, false); + + return new CommandResult(SHOWING_HELP_MESSAGE, CommandType.HELP); } } diff --git a/src/main/java/seedu/address/logic/commands/SaveCommand.java b/src/main/java/seedu/address/logic/commands/SaveCommand.java new file mode 100644 index 00000000000..c49fcd46b56 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/SaveCommand.java @@ -0,0 +1,98 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.commands.EditCommand.MESSAGE_DUPLICATE_PERSON; +import static seedu.address.logic.commands.EditCommand.MESSAGE_EDIT_PERSON_SUCCESS; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; + +import java.util.List; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.Person; + +/** + * Represents a SaveCommand that saves the edited fosterer data and update the fosterer information in the application. + */ +public class SaveCommand extends Command { + public static final String SAVE_COMMAND_WORD = "save"; + public static final String MESSAGE_ADD_SUCCESS = "New fosterer added: %1$s"; + public static final String MESSAGE_DETAILS_NOT_FILLED = "Fosterer details must be filled out."; + public static final String MESSAGE_FOSTERER_NOT_EDITED = "No details are edited."; + + private Person newFosterer; + private Index index; + + /** + * Represents a SaveCommand constructor that takes in the index of the fosterer to edit + * and the new fosterer that replaces the already existing fosterer in the index. + * @param index is the index of the fosterer stored in the list of fosterers. + * @param newFosterer is the fosterer details to be updated to the already existing fosterer. + */ + public SaveCommand(Index index, Person newFosterer) { + super(); + this.newFosterer = newFosterer; + this.index = index; + } + + /** + * Represents a SaveCommand constructor used when adding a fosterer from PersonProfile. + * @param newFosterer is the new fosterer to be added in the program. + */ + public SaveCommand(Person newFosterer) { + super(); + this.newFosterer = newFosterer; + this.index = null; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredPersonList(); + + + // While on add ProfilePage. + if (index == null) { + // If the details are not all filled, throw an exception + if (newFosterer == null) { + throw new CommandException(MESSAGE_DETAILS_NOT_FILLED); + } + + // If the model already has the newly adding fosterer, throw an exception + if (model.hasPerson(newFosterer)) { + throw new CommandException(MESSAGE_DUPLICATE_PERSON); + } + + model.addPerson(newFosterer); + return new CommandResult( + String.format(MESSAGE_ADD_SUCCESS, Messages.format(newFosterer)), + null, + null, + CommandType.VIEW_EXIT, + true + ); + } + + Person personToEdit = lastShownList.get(index.getZeroBased()); + Person editedPerson = newFosterer; + + // Edits a name but the person with the name already exists, then throw an exception + if (!personToEdit.isSamePerson(editedPerson) && model.hasPerson(editedPerson)) { + throw new CommandException(MESSAGE_DUPLICATE_PERSON); + } + + // None of the details have changed, then throw an exception + if (personToEdit.equals(editedPerson)) { + throw new CommandException(MESSAGE_FOSTERER_NOT_EDITED); + } + + model.setPerson(personToEdit, editedPerson); + model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + return new CommandResult( + String.format(MESSAGE_EDIT_PERSON_SUCCESS, Messages.format(editedPerson)), + CommandType.SAVE + ); + } +} diff --git a/src/main/java/seedu/address/logic/commands/SortCommand.java b/src/main/java/seedu/address/logic/commands/SortCommand.java new file mode 100644 index 00000000000..221e0e70976 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/SortCommand.java @@ -0,0 +1,23 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import seedu.address.model.Model; + +/** + * Lists all persons in the address book to the user, sorted by + * alphabetical order of names. + */ +public class SortCommand extends Command { + + public static final String COMMAND_WORD = "sort"; + + public static final String MESSAGE_SUCCESS = "List sorted in alphabetical order of names"; + + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + model.sortByName(); + return new CommandResult(MESSAGE_SUCCESS); + } +} diff --git a/src/main/java/seedu/address/logic/commands/StatsAvailCommand.java b/src/main/java/seedu/address/logic/commands/StatsAvailCommand.java new file mode 100644 index 00000000000..1fb969dc11a --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/StatsAvailCommand.java @@ -0,0 +1,102 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.List; +import java.util.stream.Collectors; + +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.AnimalType; +import seedu.address.model.person.Person; + +/** + * Calculates the statistics of available fosterers from the currently displayed list. + */ +public class StatsAvailCommand extends StatsCommand { + public static final String COMMAND_WORD = "avail"; + public static final String MESSAGE_AVAIL_SUMMARY = "%1$d out of %2$d listed are available (%3$.2f%%)!"; + + public static final String MESSAGE_AVAIL_DETAILS = "Out of those available, \n" + + "- %1$d can foster dogs (%2$.2f%%)\n" + + "- %3$d can foster cats (%4$.2f%%)\n" + + "- %5$d unknown (%6$.2f%%)"; + + /** + * Returns a list of available fosterers. + */ + protected List getAvailableFosterers(List fosterers) { + return fosterers.stream() + .filter(Person::isAvailableFosterer) + .collect(Collectors.toList()); + } + + /** + * Returns the number of fosterers from the given list who can foster dogs. + */ + protected int getAbleDogCount(List fosterers) { + return (int) fosterers.stream() + .filter(fosterer -> + fosterer.getAnimalType() + .equals(AnimalType.ABLE_DOG)) + .count(); + } + + /** + * Returns the number of fosterers from the given list who can foster cats. + */ + protected int getAbleCatCount(List fosterers) { + return (int) fosterers.stream() + .filter(fosterer -> + fosterer.getAnimalType() + .equals(AnimalType.ABLE_CAT)) + .count(); + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredPersonList(); + int total = lastShownList.size(); + + if (total == 0) { + throw new CommandException(StatsCommand.MESSAGE_NO_FOSTERERS); + } + + List availableFosterers = getAvailableFosterers(lastShownList); + int availableCount = availableFosterers.size(); + double availPercent = calculatePercentage(availableCount, total); + String resultSummary = String.format(MESSAGE_AVAIL_SUMMARY, availableCount, total, availPercent); + + if (availableCount == 0) { + return new CommandResult(resultSummary); + } + + int canFosterDogsCount = getAbleDogCount(availableFosterers); + int canFosterCatsCount = getAbleCatCount(availableFosterers); + int unknownCount = availableCount - canFosterDogsCount - canFosterCatsCount; + + double canFosterDogsPercent = calculatePercentage(canFosterDogsCount, availableCount); + double canFosterCatsPercent = calculatePercentage(canFosterCatsCount, availableCount); + double unknownPercent = 100.0 - canFosterCatsPercent - canFosterDogsPercent; + + String resultDetails = String.format(MESSAGE_AVAIL_DETAILS, canFosterDogsCount, canFosterDogsPercent, + canFosterCatsCount, canFosterCatsPercent, unknownCount, unknownPercent); + + return new CommandResult(resultSummary + "\n" + resultDetails); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof StatsAvailCommand)) { + return false; + } + + return true; + } +} diff --git a/src/main/java/seedu/address/logic/commands/StatsCommand.java b/src/main/java/seedu/address/logic/commands/StatsCommand.java new file mode 100644 index 00000000000..05480cf6d57 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/StatsCommand.java @@ -0,0 +1,35 @@ +package seedu.address.logic.commands; + +import seedu.address.commons.util.ToStringBuilder; + +/** + * Calculates the requested statistic about the currently displayed list. + */ +public abstract class StatsCommand extends Command { + public static final String COMMAND_WORD = "stats"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Displays the requested statistic about the currently displayed list.\n" + + "Examples: \n" + + COMMAND_WORD + " " + StatsAvailCommand.COMMAND_WORD + " \n" + + COMMAND_WORD + " " + StatsCurrentCommand.COMMAND_WORD + " \n" + + COMMAND_WORD + " " + StatsHousingCommand.COMMAND_WORD; + + public static final String MESSAGE_NO_FOSTERERS = "No fosterers to generate statistics from. " + + "Please add some fosterers!"; + + /** + * Calculates percentage using a numerator and denominator. + * Denominator must be a non-zero value. + */ + protected static double calculatePercentage(int num, int denom) { + assert(denom != 0); + return num / (double) denom * 100; + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .toString(); + } +} diff --git a/src/main/java/seedu/address/logic/commands/StatsCurrentCommand.java b/src/main/java/seedu/address/logic/commands/StatsCurrentCommand.java new file mode 100644 index 00000000000..92e47c04afa --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/StatsCurrentCommand.java @@ -0,0 +1,99 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.List; +import java.util.stream.Collectors; + +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.AnimalType; +import seedu.address.model.person.Person; + +/** + * Calculates the statistics of fosterers who are currently fostering from the currently displayed list. + */ +public class StatsCurrentCommand extends StatsCommand { + public static final String COMMAND_WORD = "current"; + public static final String MESSAGE_CURRENT_SUMMARY = "%1$d out of %2$d listed are " + + "currently fostering (%3$.2f%%)!"; + + public static final String MESSAGE_CURRENT_DETAILS = "- %1$d fostering dogs (%2$.2f%%)\n" + + "- %3$d fostering cats (%4$.2f%%)"; + + /** + * Returns a list of fosterers who are currently fostering. + */ + protected List getCurrentFosterers(List fosterers) { + return fosterers.stream() + .filter(Person::isCurrentFosterer) + .collect(Collectors.toList()); + } + + /** + * Returns the number of fosterers from the given list who are fostering dogs. + */ + protected int getCurrentDogCount(List fosterers) { + return (int) fosterers.stream() + .filter(fosterer -> + fosterer.getAnimalType() + .equals(AnimalType.CURRENT_DOG)) + .count(); + } + + /** + * Returns the number of fosterers from the given list who are fostering cats. + */ + protected int getCurrentCatCount(List fosterers) { + return (int) fosterers.stream() + .filter(fosterer -> + fosterer.getAnimalType() + .equals(AnimalType.CURRENT_CAT)) + .count(); + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredPersonList(); + int total = lastShownList.size(); + + if (total == 0) { + throw new CommandException(StatsCommand.MESSAGE_NO_FOSTERERS); + } + + List currentFosterers = getCurrentFosterers(lastShownList); + int currentCount = currentFosterers.size(); + double currentPercent = calculatePercentage(currentCount, total); + String resultSummary = String.format(MESSAGE_CURRENT_SUMMARY, currentCount, total, currentPercent); + + if (currentCount == 0) { + return new CommandResult(resultSummary); + } + + int fosteringDogsCount = getCurrentDogCount(currentFosterers); + int fosteringCatsCount = getCurrentCatCount(currentFosterers); + + double fosteringDogsPercent = calculatePercentage(fosteringDogsCount, currentCount); + double fosteringCatsPercent = 100.0 - fosteringDogsPercent; + + String resultDetails = String.format(MESSAGE_CURRENT_DETAILS, fosteringDogsCount, + fosteringDogsPercent, fosteringCatsCount, fosteringCatsPercent); + + return new CommandResult(resultSummary + "\n" + resultDetails); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof StatsCurrentCommand)) { + return false; + } + + return true; + } +} diff --git a/src/main/java/seedu/address/logic/commands/StatsHousingCommand.java b/src/main/java/seedu/address/logic/commands/StatsHousingCommand.java new file mode 100644 index 00000000000..840d441c6ff --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/StatsHousingCommand.java @@ -0,0 +1,95 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.List; + +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.Housing; +import seedu.address.model.person.Person; + +/** + * Calculates the fosterers' housing statistics from the currently displayed list. + */ +public class StatsHousingCommand extends StatsCommand { + public static final String COMMAND_WORD = "housing"; + public static final String MESSAGE_HOUSING_SUCCESS = "Out of %1$d listed,\n" + + "- %2$d live in HDB (%3$.2f%%)\n" + + "- %4$d live in Condo (%5$.2f%%)\n" + + "- %6$d live in Landed (%7$.2f%%)\n" + + "- %8$d unknown (%9$.2f%%)"; + + /** + * Returns the number of fosterers who stay in HDB. + */ + protected int getHdbCount(List fosterers) { + return (int) fosterers.stream() + .filter(fosterer -> + fosterer.getHousing() + .equals(Housing.HDB)) + .count(); + } + + /** + * Returns the number of fosterers who stay in Condos. + */ + protected int getCondoCount(List fosterers) { + return (int) fosterers.stream() + .filter(fosterer -> + fosterer.getHousing() + .equals(Housing.CONDO)) + .count(); + } + + /** + * Returns the number of fosterers who stay in Landed. + */ + protected int getLandedCount(List fosterers) { + return (int) fosterers.stream() + .filter(fosterer -> + fosterer.getHousing() + .equals(Housing.LANDED)) + .count(); + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredPersonList(); + int total = lastShownList.size(); + + if (total == 0) { + throw new CommandException(StatsCommand.MESSAGE_NO_FOSTERERS); + } + + int hdbCount = getHdbCount(lastShownList); + int condoCount = getCondoCount(lastShownList); + int landedCount = getLandedCount(lastShownList); + int unknownCount = total - hdbCount - condoCount - landedCount; + + double hdbPercent = calculatePercentage(hdbCount, total); + double condoPercent = calculatePercentage(condoCount, total); + double landedPercent = calculatePercentage(landedCount, total); + double unknownPercent = 100.0 - hdbPercent - condoPercent - landedPercent; + + String result = String.format(MESSAGE_HOUSING_SUCCESS, total, hdbCount, hdbPercent, + condoCount, condoPercent, landedCount, landedPercent, unknownCount, unknownPercent); + + return new CommandResult(result); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof StatsHousingCommand)) { + return false; + } + + return true; + } +} 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..0c69d5c6191 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/UndoCommand.java @@ -0,0 +1,22 @@ +package seedu.address.logic.commands; + +import seedu.address.model.Model; + +/** + * Undoes the last command + */ +public class UndoCommand extends Command { + public static final String COMMAND_WORD = "undo"; + public static final String MESSAGE_SUCCESS = "Undo completed"; + public static final String MESSAGE_USAGE = "This command will undo the last command."; + public static final String MESSAGE_UNDO_DONE = "No more undo history found!"; + + + @Override + public CommandResult execute(Model model) { + if (model == null) { + return new CommandResult(MESSAGE_UNDO_DONE); + } + return new CommandResult(MESSAGE_SUCCESS); + } +} diff --git a/src/main/java/seedu/address/logic/commands/ViewCommand.java b/src/main/java/seedu/address/logic/commands/ViewCommand.java new file mode 100644 index 00000000000..961d69aed7b --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ViewCommand.java @@ -0,0 +1,79 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.List; + +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.Person; + +/** + * Views the profile of the fosterer using their displayed index in the addressbook. + */ +public class ViewCommand extends Command { + public static final String COMMAND_WORD = "view"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Shows the profile of the user.\n" + + "Example: " + COMMAND_WORD + " 1"; + public static final String VIEWING_PROFILE_SUCCESS = "Viewing Person: %1$s"; + public static final String VIEWING_NEW_PROFILE_SUCCESS = "Viewing New Fosterer Profile"; + private final Index indexOfTheFostererToView; + + public ViewCommand(Index index) { + this.indexOfTheFostererToView = index; + } + public ViewCommand() { + this.indexOfTheFostererToView = null; + } + @Override + public CommandResult execute(Model model) throws CommandException { + // Viewing empty profile for adding new fosterer + if (indexOfTheFostererToView == null) { + return new CommandResult( + VIEWING_NEW_PROFILE_SUCCESS, + CommandType.VIEW + ); + } + + requireNonNull(model); + List lastShownList = model.getFilteredPersonList(); + + if (indexOfTheFostererToView.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + Person personToView = lastShownList.get(indexOfTheFostererToView.getZeroBased()); + return new CommandResult( + String.format(VIEWING_PROFILE_SUCCESS, Messages.format(personToView)), + personToView, + indexOfTheFostererToView, + CommandType.VIEW, + false + ); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof ViewCommand)) { + return false; + } + + ViewCommand e = (ViewCommand) other; + return indexOfTheFostererToView.equals(e.indexOfTheFostererToView); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("indexOfTheFostererToView", indexOfTheFostererToView) + .toString(); + } +} diff --git a/src/main/java/seedu/address/logic/commands/ViewExitCommand.java b/src/main/java/seedu/address/logic/commands/ViewExitCommand.java new file mode 100644 index 00000000000..28772334b70 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ViewExitCommand.java @@ -0,0 +1,110 @@ +package seedu.address.logic.commands; + +import java.util.List; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.Person; + +/** + * Exits the view mode and prompts to the original fosterer list view. + */ +public class ViewExitCommand extends Command { + public static final String COMMAND_WORD = "exit"; + + public static final String MESSAGE_EXIT_ACKNOWLEDGEMENT = "Exiting view mode as requested ..."; + public static final String MESSAGE_CONFIRM_EXIT = "You did not save your changes. Are you sure you want to exit?\n" + + "Yes: [Enter] No: [Esc]"; + public static final String MESSAGE_CONFIRM_EXIT_WITHOUT_DETAILS = "You did not fill in every detail." + + " Are you sure you want to exit?\n" + + "Yes: [Enter] No: [Esc]"; + + private Index index; + private Person newFosterer; + + /** + * Represents a ViewExitCommand constructor that takes in only a Person object that is used to exit + * a profile view page when adding a new fosterer. + * + * @param newFosterer to be added to the storage. + */ + public ViewExitCommand(Person newFosterer) { + super(); + this.index = null; + this.newFosterer = newFosterer; + } + + /** + * Represents a ViewExitCommand constructor that takes in both the index of the fosterer to be edited and + * the fosterer object that includes the edited details on the currently opened PersonProfile. + * + * @param index of the fosterer to be edited. + * @param newFosterer to be compared with the original fosterer to see if there is any change in the details. + */ + public ViewExitCommand(Index index, Person newFosterer) { + super(); + this.index = index; + this.newFosterer = newFosterer; + } + + /** + * Returns different CommandResult objects for different cases. + * If a new fosterer is being created from PersonProfile, the command checks if the person can be created with + * the details currently on the page. If can, the CommandResult asks the user if they want to save before exit. + * Otherwise, the user will be asked if they want to continue adding details before exit. + * If a fosterer's details were edited but not saved, the CommandResult asks the user + * if they want to save before exit. + * If a fosterer's details were edited and saved, the CommandResult would not ask the user to save. + * + * @param model {@code Model} which the command should operate on. + * @return CommandResult that carries the necessary details used to check + * if the person is saved or needs to be saved. + * @throws CommandException when the index of the fosterer is invalid. + */ + @Override + public CommandResult execute(Model model) throws CommandException { + List lastShownList = model.getFilteredPersonList(); + + // for exiting from empty profile page with not every field filled out + if (index == null && newFosterer == null) { + return new CommandResult( + MESSAGE_CONFIRM_EXIT_WITHOUT_DETAILS, + null, + null, + CommandType.VIEW_EXIT, + false); + } + + // for exiting from profile page with every field filled out + if (index == null && newFosterer != null) { + return new CommandResult( + MESSAGE_CONFIRM_EXIT, + newFosterer, + null, + CommandType.VIEW_EXIT, + false); + } + + Person personToCompare = lastShownList.get(index.getZeroBased()); + Person editedPerson = newFosterer; + + // fosterer is edited and saved, exit without any message. + if (editedPerson.equals(personToCompare)) { + return new CommandResult( + MESSAGE_EXIT_ACKNOWLEDGEMENT, + newFosterer, + null, + CommandType.VIEW_EXIT, + true); + } else { + // fosterer is edited but not saved, exit with message. + return new CommandResult( + MESSAGE_CONFIRM_EXIT, + newFosterer, + null, + CommandType.VIEW_EXIT, + false); + } + } +} diff --git a/src/main/java/seedu/address/logic/parser/AddCommandParser.java b/src/main/java/seedu/address/logic/parser/AddCommandParser.java index 4ff1a97ed77..202569b54e3 100644 --- a/src/main/java/seedu/address/logic/parser/AddCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/AddCommandParser.java @@ -2,7 +2,11 @@ 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_ANIMAL_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ANIMAL_TYPE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_AVAILABILITY; import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_HOUSING; 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; @@ -13,16 +17,20 @@ import seedu.address.logic.commands.AddCommand; import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.person.Address; +import seedu.address.model.person.AnimalType; +import seedu.address.model.person.Availability; import seedu.address.model.person.Email; +import seedu.address.model.person.Housing; import seedu.address.model.person.Name; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; import seedu.address.model.tag.Tag; + /** * Parses input arguments and creates a new AddCommand object */ -public class AddCommandParser implements Parser { +public class AddCommandParser implements CommandParser { /** * Parses the given {@code String} of arguments in the context of the AddCommand @@ -30,24 +38,40 @@ public class AddCommandParser implements Parser { * @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); + try { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, + PREFIX_ADDRESS, PREFIX_TAG, PREFIX_ANIMAL_NAME, + PREFIX_AVAILABILITY, PREFIX_ANIMAL_TYPE, PREFIX_HOUSING); - if (!arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_ADDRESS, PREFIX_PHONE, PREFIX_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); - 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()); - Set tagList = ParserUtil.parseTags(argMultimap.getAllValues(PREFIX_TAG)); + if (!arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_ADDRESS, PREFIX_PHONE, PREFIX_EMAIL, + PREFIX_ANIMAL_NAME, PREFIX_AVAILABILITY, PREFIX_ANIMAL_TYPE, PREFIX_HOUSING) + || !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); + 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()); + Set tagList = ParserUtil.parseTags(argMultimap.getAllValues(PREFIX_TAG)); - Person person = new Person(name, phone, email, address, tagList); + Name animalName = ParserUtil.parseName(argMultimap.getValue(PREFIX_ANIMAL_NAME).get()); + Availability availability = ParserUtil.parseAvailability(argMultimap + .getValue(PREFIX_AVAILABILITY).get()); + AnimalType animalType = ParserUtil.parseAnimalType(argMultimap + .getValue(PREFIX_ANIMAL_TYPE).get(), availability); + Housing housing = ParserUtil.parseHousing(argMultimap.getValue(PREFIX_HOUSING).get()); - return new AddCommand(person); + Person person = new Person(name, phone, email, address, + housing, availability, animalName, animalType, tagList); + + return new AddCommand(person); + } catch (IllegalArgumentException e) { + throw new ParseException(e.getMessage()); + } } /** diff --git a/src/main/java/seedu/address/logic/parser/AddressBookParser.java b/src/main/java/seedu/address/logic/parser/AddressBookParser.java index 3149ee07e0b..5e8da980189 100644 --- a/src/main/java/seedu/address/logic/parser/AddressBookParser.java +++ b/src/main/java/seedu/address/logic/parser/AddressBookParser.java @@ -17,6 +17,10 @@ import seedu.address.logic.commands.FindCommand; import seedu.address.logic.commands.HelpCommand; import seedu.address.logic.commands.ListCommand; +import seedu.address.logic.commands.SortCommand; +import seedu.address.logic.commands.StatsCommand; +import seedu.address.logic.commands.UndoCommand; +import seedu.address.logic.commands.ViewCommand; import seedu.address.logic.parser.exceptions.ParseException; /** @@ -54,6 +58,10 @@ public Command parseCommand(String userInput) throws ParseException { switch (commandWord) { case AddCommand.COMMAND_WORD: + if (arguments.isEmpty()) { + return new ViewCommand(); + } + return new AddCommandParser().parse(arguments); case EditCommand.COMMAND_WORD: @@ -62,14 +70,22 @@ public Command parseCommand(String userInput) throws ParseException { case DeleteCommand.COMMAND_WORD: return new DeleteCommandParser().parse(arguments); + case StatsCommand.COMMAND_WORD: + return new StatsCommandParser().parse(arguments); + case ClearCommand.COMMAND_WORD: - return new ClearCommand(); + return new ClearCommandParser().parse(arguments); case FindCommand.COMMAND_WORD: - return new FindCommandParser().parse(arguments); - case ListCommand.COMMAND_WORD: - return new ListCommand(); + if (arguments.isEmpty()) { + return new ListCommand(); + } else { + return new FindCommandParser().parse(arguments); + } + + case ViewCommand.COMMAND_WORD: + return new ViewCommandParser().parse(arguments); case ExitCommand.COMMAND_WORD: return new ExitCommand(); @@ -77,6 +93,12 @@ public Command parseCommand(String userInput) throws ParseException { case HelpCommand.COMMAND_WORD: return new HelpCommand(); + case SortCommand.COMMAND_WORD: + return new SortCommand(); + + case UndoCommand.COMMAND_WORD: + return new UndoCommand(); + 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/ClearCommandParser.java b/src/main/java/seedu/address/logic/parser/ClearCommandParser.java new file mode 100644 index 00000000000..fa80fa89c70 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/ClearCommandParser.java @@ -0,0 +1,33 @@ +package seedu.address.logic.parser; + +import seedu.address.logic.commands.ClearCommand; +import seedu.address.logic.parser.exceptions.ParseException; + + +/** + * Parses input arguments and creates a new DeleteCommand object + */ +public class ClearCommandParser { + /** + * Parses the given {@code String} of arguments in the context of the ClearCommand + * and returns a ClearCommand object for execution. + * + * @throws ParseException if the user input does not conform the expected format + */ + public ClearCommand parse(String args) throws ParseException { + if (args.trim().isEmpty()) { + return new ClearCommand("reset prompt"); + } + try { + String confirmation = ParserUtil.parseSimpleString(args); + if (!confirmation.equals("confirm")) { + throw new ParseException( + String.format(ClearCommand.MESSAGE_USAGE)); + } + return new ClearCommand(confirmation); + } catch (ParseException pe) { + throw new ParseException( + String.format(ClearCommand.MESSAGE_USAGE), pe); + } + } +} diff --git a/src/main/java/seedu/address/logic/parser/CliSyntax.java b/src/main/java/seedu/address/logic/parser/CliSyntax.java index 75b1a9bf119..fa8da6a23b6 100644 --- a/src/main/java/seedu/address/logic/parser/CliSyntax.java +++ b/src/main/java/seedu/address/logic/parser/CliSyntax.java @@ -11,5 +11,8 @@ 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_ANIMAL_NAME = new Prefix("animal/"); + public static final Prefix PREFIX_AVAILABILITY = new Prefix("availability/"); + public static final Prefix PREFIX_ANIMAL_TYPE = new Prefix("animalType/"); + public static final Prefix PREFIX_HOUSING = new Prefix("housing/"); } diff --git a/src/main/java/seedu/address/logic/parser/Parser.java b/src/main/java/seedu/address/logic/parser/CommandParser.java similarity index 90% rename from src/main/java/seedu/address/logic/parser/Parser.java rename to src/main/java/seedu/address/logic/parser/CommandParser.java index d6551ad8e3f..15057aa21c6 100644 --- a/src/main/java/seedu/address/logic/parser/Parser.java +++ b/src/main/java/seedu/address/logic/parser/CommandParser.java @@ -6,7 +6,7 @@ /** * Represents a Parser that is able to parse user input into a {@code Command} of type {@code T}. */ -public interface Parser { +public interface CommandParser { /** * Parses {@code userInput} into a command and returns it. diff --git a/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java b/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java index 3527fe76a3e..98e371f652e 100644 --- a/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java @@ -2,28 +2,28 @@ import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; -import seedu.address.commons.core.index.Index; +import seedu.address.commons.core.index.Indices; import seedu.address.logic.commands.DeleteCommand; import seedu.address.logic.parser.exceptions.ParseException; /** * Parses input arguments and creates a new DeleteCommand object */ -public class DeleteCommandParser implements Parser { +public class DeleteCommandParser implements CommandParser { /** * Parses the given {@code String} of arguments in the context of the DeleteCommand * and returns a DeleteCommand object for execution. + * * @throws ParseException if the user input does not conform the expected format */ public DeleteCommand parse(String args) throws ParseException { try { - Index index = ParserUtil.parseIndex(args); - return new DeleteCommand(index); + Indices indices = ParserUtil.parseIndices(args); + return new DeleteCommand(indices); } catch (ParseException pe) { throw new ParseException( String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE), pe); } } - } diff --git a/src/main/java/seedu/address/logic/parser/EditCommandParser.java b/src/main/java/seedu/address/logic/parser/EditCommandParser.java index 46b3309a78b..1829750daf7 100644 --- a/src/main/java/seedu/address/logic/parser/EditCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/EditCommandParser.java @@ -3,7 +3,11 @@ 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_ANIMAL_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ANIMAL_TYPE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_AVAILABILITY; import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_HOUSING; 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; @@ -22,7 +26,7 @@ /** * Parses input arguments and creates a new EditCommand object */ -public class EditCommandParser implements Parser { +public class EditCommandParser implements CommandParser { /** * Parses the given {@code String} of arguments in the context of the EditCommand @@ -32,7 +36,18 @@ 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_HOUSING, + PREFIX_AVAILABILITY, + PREFIX_ANIMAL_NAME, + PREFIX_ANIMAL_TYPE, + PREFIX_TAG + ); Index index; @@ -58,10 +73,25 @@ public EditCommand parse(String args) throws ParseException { if (argMultimap.getValue(PREFIX_ADDRESS).isPresent()) { editPersonDescriptor.setAddress(ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get())); } + if (argMultimap.getValue(PREFIX_HOUSING).isPresent()) { + editPersonDescriptor.setHousing(ParserUtil.parseHousing(argMultimap.getValue(PREFIX_HOUSING).get())); + } + if (argMultimap.getValue(PREFIX_AVAILABILITY).isPresent()) { + editPersonDescriptor.setAvailability(ParserUtil.parseAvailability( + argMultimap.getValue(PREFIX_AVAILABILITY).get()) + ); + } + if (argMultimap.getValue(PREFIX_ANIMAL_NAME).isPresent()) { + editPersonDescriptor.setAnimalName(ParserUtil.parseName(argMultimap.getValue(PREFIX_ANIMAL_NAME).get())); + } + if (argMultimap.getValue(PREFIX_ANIMAL_TYPE).isPresent()) { + editPersonDescriptor.setAnimalType(argMultimap.getValue(PREFIX_ANIMAL_TYPE).get()); + } + parseTagsForEdit(argMultimap.getAllValues(PREFIX_TAG)).ifPresent(editPersonDescriptor::setTags); if (!editPersonDescriptor.isAnyFieldEdited()) { - throw new ParseException(EditCommand.MESSAGE_NOT_EDITED); + return new EditCommand(index); } return new EditCommand(index, editPersonDescriptor); diff --git a/src/main/java/seedu/address/logic/parser/FindCommandParser.java b/src/main/java/seedu/address/logic/parser/FindCommandParser.java index 2867bde857b..35e9946bdfe 100644 --- a/src/main/java/seedu/address/logic/parser/FindCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/FindCommandParser.java @@ -1,17 +1,13 @@ package seedu.address.logic.parser; -import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; - -import java.util.Arrays; - import seedu.address.logic.commands.FindCommand; import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.person.NameContainsKeywordsPredicate; +import seedu.address.logic.search.FindCommandArgumentParser; /** * Parses input arguments and creates a new FindCommand object */ -public class FindCommandParser implements Parser { +public class FindCommandParser implements CommandParser { /** * Parses the given {@code String} of arguments in the context of the FindCommand @@ -19,15 +15,7 @@ public class FindCommandParser implements Parser { * @throws ParseException if the user input does not conform the expected format */ public FindCommand parse(String args) throws ParseException { - String trimmedArgs = args.trim(); - if (trimmedArgs.isEmpty()) { - throw new ParseException( - String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindCommand.MESSAGE_USAGE)); - } - - String[] nameKeywords = trimmedArgs.split("\\s+"); - - return new FindCommand(new NameContainsKeywordsPredicate(Arrays.asList(nameKeywords))); + return new FindCommand(new FindCommandArgumentParser().parse(args)); } } diff --git a/src/main/java/seedu/address/logic/parser/ParserUtil.java b/src/main/java/seedu/address/logic/parser/ParserUtil.java index b117acb9c55..55be36b21c3 100644 --- a/src/main/java/seedu/address/logic/parser/ParserUtil.java +++ b/src/main/java/seedu/address/logic/parser/ParserUtil.java @@ -7,11 +7,16 @@ import java.util.Set; import seedu.address.commons.core.index.Index; +import seedu.address.commons.core.index.Indices; import seedu.address.commons.util.StringUtil; import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.person.Address; +import seedu.address.model.person.AnimalType; +import seedu.address.model.person.Availability; import seedu.address.model.person.Email; +import seedu.address.model.person.Housing; import seedu.address.model.person.Name; +import seedu.address.model.person.Person; import seedu.address.model.person.Phone; import seedu.address.model.tag.Tag; @@ -25,6 +30,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 +41,26 @@ public static Index parseIndex(String oneBasedIndex) throws ParseException { return Index.fromOneBased(Integer.parseInt(trimmedIndex)); } + /** + * Parses a string of {@code oneBasedIndices} into {@code Indices} and returns it. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if at least one specified index is invalid (not non-zero unsigned integer). + */ + public static Indices parseIndices(String oneBasedIndices) throws ParseException { + String[] indices = oneBasedIndices.trim().split("\\s+"); + int size = indices.length; + int[] trimmedIndices = new int[size]; + + for (int i = 0; i < size; i++) { + if (!StringUtil.isNonZeroUnsignedInteger(indices[i])) { + throw new ParseException(MESSAGE_INVALID_INDEX); + } + trimmedIndices[i] = Integer.parseInt(indices[i]); + } + return Indices.fromOneBased(trimmedIndices); + } + /** * Parses a {@code String name} into a {@code Name}. * Leading and trailing whitespaces will be trimmed. @@ -121,4 +147,73 @@ public static Set parseTags(Collection tags) throws ParseException } return tagSet; } + + /** + * Parses a {@code String availability} into an {@code Availability}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code availability} is invalid. + */ + public static Availability parseAvailability(String availability) throws ParseException { + requireNonNull(availability); + String trimmedAvailability = availability.trim(); + if (!Availability.isValidAvailability(trimmedAvailability)) { + throw new ParseException(Availability.MESSAGE_CONSTRAINTS); + } + return new Availability(trimmedAvailability); + } + + /** + * Parses a {@code String animalType} into an {@code AnimalType}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code animalType} is invalid. + */ + public static AnimalType parseAnimalType(String animalType, Availability availability) throws ParseException { + requireNonNull(animalType); + requireNonNull(availability); + + String trimmedAnimalType = animalType.trim(); + boolean isValidType = false; + + switch (availability.value) { + case "Available": + isValidType = AnimalType.isValidAnimalType(trimmedAnimalType, AnimalType.VALIDATION_REGEX_AVAILABLE); + break; + case "NotAvailable": + isValidType = AnimalType.isValidAnimalType(trimmedAnimalType, AnimalType.VALIDATION_REGEX_NOT_AVAILABLE); + break; + case Person.NIL_WORD: + isValidType = AnimalType.isValidAnimalType(trimmedAnimalType, AnimalType.VALIDATION_REGEX_NIL); + break; + default: + // no specific action is needed. + break; + } + + if (!isValidType) { + throw new ParseException(AnimalType.MESSAGE_CONSTRAINTS); + } + + return new AnimalType(trimmedAnimalType, availability); + } + + /** + * Parses a {@code String housing} into an {@code Housing}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code housing} is invalid. + */ + public static Housing parseHousing(String housing) throws ParseException { + requireNonNull(housing); + String trimmedHousing = housing.trim(); + if (!Housing.isValidHousing(trimmedHousing)) { + throw new ParseException(Housing.MESSAGE_CONSTRAINTS); + } + return new Housing(trimmedHousing); + } + + public static String parseSimpleString(String confirmation) throws ParseException { + return confirmation.trim(); + } } diff --git a/src/main/java/seedu/address/logic/parser/StatsCommandParser.java b/src/main/java/seedu/address/logic/parser/StatsCommandParser.java new file mode 100644 index 00000000000..2de4b2b9fdd --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/StatsCommandParser.java @@ -0,0 +1,37 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.logic.commands.StatsAvailCommand; +import seedu.address.logic.commands.StatsCommand; +import seedu.address.logic.commands.StatsCurrentCommand; +import seedu.address.logic.commands.StatsHousingCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new StatsCommand object + */ +public class StatsCommandParser implements CommandParser { + + /** + * Parses the given {@code String} of arguments in the context of the StatsCommand + * and returns either a StatsAvailCommand, StatsCurrentCommand + * or StatsHousingCommand object for execution. + * + * @throws ParseException if the user input does not conform the expected format + */ + public StatsCommand parse(String args) throws ParseException { + String field = args.trim().split(" ")[0]; + + switch (field) { + case StatsAvailCommand.COMMAND_WORD: + return new StatsAvailCommand(); + case StatsCurrentCommand.COMMAND_WORD: + return new StatsCurrentCommand(); + case StatsHousingCommand.COMMAND_WORD: + return new StatsHousingCommand(); + default: + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, StatsCommand.MESSAGE_USAGE)); + } + } +} diff --git a/src/main/java/seedu/address/logic/parser/ViewCommandParser.java b/src/main/java/seedu/address/logic/parser/ViewCommandParser.java new file mode 100644 index 00000000000..c5dba454b21 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/ViewCommandParser.java @@ -0,0 +1,27 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.ViewCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input index and creates a new ViewCommand object + */ +public class ViewCommandParser implements CommandParser { + /** + * Parses the given {@code String} of arguments in the context of the ViewCommand + * and returns an ViewCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public ViewCommand parse(String args) throws ParseException { + try { + Index index = ParserUtil.parseIndex(args); + return new ViewCommand(index); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, ViewCommand.MESSAGE_USAGE), pe); + } + } +} diff --git a/src/main/java/seedu/address/logic/parser/ViewModeParser.java b/src/main/java/seedu/address/logic/parser/ViewModeParser.java new file mode 100644 index 00000000000..c5e1a694c92 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/ViewModeParser.java @@ -0,0 +1,72 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.EditFieldCommand; +import seedu.address.logic.commands.HelpCommand; +import seedu.address.logic.commands.SaveCommand; +import seedu.address.logic.commands.ViewExitCommand; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.Person; + +/** + * Parses user input while in the fosterer profile view page. + */ +public class ViewModeParser { + /** + * Used for initial separation of command word and args. + */ + private static final Pattern BASIC_COMMAND_FORMAT = Pattern.compile("(?.*)"); + private static final Logger logger = LogsCenter.getLogger(seedu.address.logic.parser.AddressBookParser.class); + + /** + * Parses user input into command for execution. + * + * @param userInput full user input string + * @return the command based on the user input + * @throws ParseException if the user input does not conform the expected format + */ + public Command parseCommand(String userInput, Person newPerson, Index targetIndex) + throws ParseException, CommandException { + final Matcher matcher = BASIC_COMMAND_FORMAT.matcher(userInput.trim()); + if (!matcher.matches()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, HelpCommand.MESSAGE_USAGE)); + } + + final String commandWord = matcher.group("commandWord").toLowerCase(); + + // Note to developers: Change the log level in config.json to enable lower level (i.e., FINE, FINER and lower) + // log messages such as the one below. + // Lower level log messages are used sparingly to minimize noise in the code. + logger.fine("Command word: " + commandWord); + + switch (commandWord) { + + case SaveCommand.SAVE_COMMAND_WORD: + if (targetIndex != null) { + return new SaveCommand(targetIndex, newPerson); + } else { + return new SaveCommand(newPerson); + } + case ViewExitCommand.COMMAND_WORD: + if (targetIndex != null) { + return new ViewExitCommand(targetIndex, newPerson); + } else { + return new ViewExitCommand(newPerson); + } + + default: + return new EditFieldCommand(); + } + + + } +} diff --git a/src/main/java/seedu/address/logic/search/AndSearchMatcher.java b/src/main/java/seedu/address/logic/search/AndSearchMatcher.java new file mode 100644 index 00000000000..d2268833512 --- /dev/null +++ b/src/main/java/seedu/address/logic/search/AndSearchMatcher.java @@ -0,0 +1,29 @@ +package seedu.address.logic.search; + +import java.util.Map; + +/** + * Represents the combination of two other SearchMatchers in a logical "and" manner. + * Evaluates lazily. + */ +public class AndSearchMatcher extends BinarySearchMatcher { + + private static final char SYMBOL = '&'; + + AndSearchMatcher(SearchMatcher a, SearchMatcher b) { + super(a, b, SYMBOL); + } + + @Override + FieldRanges test(Map p) { + FieldRanges rangeA = a.test(p); + if (!FieldRanges.isMatch(rangeA)) { + return null; + } + FieldRanges rangeB = b.test(p); + if (!FieldRanges.isMatch(rangeB)) { + return null; + } + return FieldRanges.union(rangeA, rangeB); + } +} diff --git a/src/main/java/seedu/address/logic/search/BinarySearchMatcher.java b/src/main/java/seedu/address/logic/search/BinarySearchMatcher.java new file mode 100644 index 00000000000..7b4f46ba470 --- /dev/null +++ b/src/main/java/seedu/address/logic/search/BinarySearchMatcher.java @@ -0,0 +1,27 @@ +package seedu.address.logic.search; + +abstract class BinarySearchMatcher extends SearchMatcher { + + protected SearchMatcher a; + protected SearchMatcher b; + protected char operation; + + BinarySearchMatcher(SearchMatcher a, SearchMatcher b, char operation) { + this.a = a; + this.b = b; + this.operation = operation; + } + + + @Override + void setFlag(SearchMatcher.Flag flag, boolean isFlagApplied) { + super.setFlag(flag, isFlagApplied); + a.setFlag(flag, isFlagApplied); + b.setFlag(flag, isFlagApplied); + } + + @Override + public String toString() { + return "(" + a + operation + b + ")"; + } +} diff --git a/src/main/java/seedu/address/logic/search/FieldRanges.java b/src/main/java/seedu/address/logic/search/FieldRanges.java new file mode 100644 index 00000000000..74fafbc857d --- /dev/null +++ b/src/main/java/seedu/address/logic/search/FieldRanges.java @@ -0,0 +1,78 @@ +package seedu.address.logic.search; + + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; + + +/** + * Used to represent a SearchMatcher match, + * determines the fields and corresponding sections of the fields where a match occurs. + */ +class FieldRanges extends HashMap { + + private boolean isMatch = false; + + @Override + public Range put(String key, Range value) { + if (value == null) { + return null; + } + isMatch = true; + return super.put(key, value); + } + + public void setIsMatch(boolean isMatch) { + this.isMatch = isMatch; + } + + public static FieldRanges union(FieldRanges a, FieldRanges b) { + if (a == null && b == null) { + return null; + } else if (a == null) { + return (FieldRanges) b.clone(); + } else if (b == null) { + return (FieldRanges) a.clone(); + } + FieldRanges ranges = new FieldRanges(); + Set keys = new HashSet<>(a.keySet()); + keys.addAll(b.keySet()); + for (String key : keys) { + ranges.put(key, Range.union(a.get(key), b.get(key))); + } + ranges.isMatch = a.isMatch | b.isMatch; + return ranges; + } + + public static boolean isMatch(FieldRanges ranges) { + if (ranges == null) { + return false; + } + return ranges.isMatch; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) { + return false; + } + + FieldRanges that = (FieldRanges) o; + + return isMatch == that.isMatch; + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 3 * result + (isMatch ? 1 : 0); + return result; + } +} diff --git a/src/main/java/seedu/address/logic/search/FindCommandArgumentParser.java b/src/main/java/seedu/address/logic/search/FindCommandArgumentParser.java new file mode 100644 index 00000000000..012a6f25cd9 --- /dev/null +++ b/src/main/java/seedu/address/logic/search/FindCommandArgumentParser.java @@ -0,0 +1,311 @@ +package seedu.address.logic.search; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import java.util.HashMap; +import java.util.Map; +import java.util.Stack; +import java.util.function.BiFunction; + +import seedu.address.logic.commands.FindCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Utility class that parses search strings accepted by the Find/List command. + */ +public class FindCommandArgumentParser { + + private String search; + private int index; + + private enum Joiner implements BiFunction { + IMPLICIT_AND(' ', 2, SearchMatcher::and), + EXPLICIT_OR('/', 1, SearchMatcher::or), + EXPLICIT_AND('&', 0, SearchMatcher::and); + + private static final Map set = new HashMap<>(); + + private final char symbol; + private final int precedence; + private final BiFunction join; + + Joiner(char symbol, int precedence, BiFunction join) { + this.symbol = symbol; + this.precedence = precedence; + this.join = join; + } + + static Joiner get(Character c) { + return set.get(c); + } + + boolean isLeqPrecedenceThan(Joiner other) { + return this.precedence <= other.precedence; + } + + @Override + public SearchMatcher apply(SearchMatcher a, SearchMatcher b) { + return join.apply(a, b); + } + + static { + for (Joiner j : Joiner.values()) { + set.put(j.symbol, j); + } + } + } + + private static class ParserDualStack { + + private final Stack predicates; + private final Stack joiners; + private NextInput nextInput; + + ParserDualStack() { + predicates = new Stack<>(); + joiners = new Stack<>(); + nextInput = NextInput.PREDICATE; + } + + void sanityCheckElseThrow() throws UnexpectedTokenException { + if ( + predicates.size() + (nextInput == NextInput.PREDICATE ? 1 : 0) + != joiners.size() + 1 + ) { + throw new UnexpectedTokenException(nextInput.getOther(), nextInput); + } + } + + SearchMatcher collapse() throws UnexpectedTokenException { + sanityCheckElseThrow(); + if (nextInput == NextInput.PREDICATE) { + throw new UnexpectedTokenException(NextInput.JOINER, nextInput); + } + if (joiners.isEmpty() && predicates.isEmpty()) { + return null; + } + while (!joiners.isEmpty()) { + Joiner joiner = joiners.pop(); + SearchMatcher newPredicate = joiner.apply( + predicates.pop(), + predicates.pop() + ); + predicates.push(newPredicate); + } + assert predicates.size() == 1; + return predicates.pop(); + } + + enum NextInput { + PREDICATE, JOINER; + + NextInput getOther() { + return NextInput.getOther(this); + } + + private static NextInput getOther(NextInput input) { + switch (input) { + case PREDICATE: + return JOINER; + case JOINER: + return PREDICATE; + default: + assert false : "Unexpected NextInput value: " + input; + return input; + } + } + } + + static class UnexpectedTokenException extends Exception { + UnexpectedTokenException(NextInput ignoredExpected, NextInput ignoredActual) {} + } + + void append(SearchMatcher predicate) throws UnexpectedTokenException { + throwIfUnexpectedNextInput(NextInput.PREDICATE); + predicates.push(predicate); + nextInput = NextInput.JOINER; + } + + void append(Joiner joiner) throws UnexpectedTokenException { + throwIfUnexpectedNextInput(NextInput.JOINER); + resolveOperationsBeforeAppending(joiner); + joiners.push(joiner); + nextInput = NextInput.PREDICATE; + } + + void throwIfUnexpectedNextInput(NextInput actualInput) throws UnexpectedTokenException { + if (nextInput != actualInput) { + throw new UnexpectedTokenException(nextInput, NextInput.JOINER); + } + } + + void resolveOperationsBeforeAppending(Joiner joiner) { + while (!joiners.isEmpty() && joiner.isLeqPrecedenceThan(joiners.peek())) { + predicates.push(joiners.pop().apply(predicates.pop(), predicates.pop())); + } + } + } + + private class RecursiveParseHelper { + + private final ParserDualStack dualStack = new ParserDualStack(); + + RecursiveParseHelper() { + } + + RecursiveParseHelper(SearchMatcher predicate) throws ParserDualStack.UnexpectedTokenException { + dualStack.append(predicate); + } + + SearchMatcher parse() throws ParserDualStack.UnexpectedTokenException { + while (hasChar()) { + if (getChar() == '(') { + incrementCharIndex(); + dualStack.append(new RecursiveParseHelper().parse()); + dualStack.sanityCheckElseThrow(); + } else if (getChar() == ')') { + incrementCharIndex(); //consume ')' + break; + } + getAndAppendNextToken(); + } + return collapse(); + } + + SearchMatcher collapse() throws ParserDualStack.UnexpectedTokenException { + return dualStack.collapse(); + } + + void getAndAppendNextToken() throws ParserDualStack.UnexpectedTokenException { + if (dualStack.nextInput == ParserDualStack.NextInput.JOINER) { + findAndAppendNextJoiner(); + } else if (dualStack.nextInput == ParserDualStack.NextInput.PREDICATE) { + findAndAppendNextPredicate(); + } else { + assert false; //error has occurred + } + } + + void findAndAppendNextJoiner() throws ParserDualStack.UnexpectedTokenException { + incrementIndexWhileSpace(); + if (!hasChar()) { + return; + } + if (isCharJoiner()) { + dualStack.append(Joiner.get(getChar())); + incrementCharIndex(); + } else { + dualStack.append(Joiner.get(' ')); + } + incrementIndexWhileSpace(); + } + + void findAndAppendNextPredicate() throws ParserDualStack.UnexpectedTokenException { + incrementIndexWhileSpace(); + if (!hasChar()) { + return; + } + if (getChar() == '\'' || getChar() == '"') { + findAndAppendQuotedPredicate(); + return; + } + int startIndexOfPredicate = index; + incrementIndexWhileNotReservedChar(); + dualStack.append(new SingleTextSearchMatcher(search.substring(startIndexOfPredicate, index))); + } + + private void findAndAppendQuotedPredicate() throws ParserDualStack.UnexpectedTokenException { + assert hasChar() && isCharQuote(); + incrementCharIndex(); + int predicateStartIndex = index; + while (hasChar() && !isCharQuote()) { + incrementCharIndex(); + } + dualStack.append(SingleTextSearchMatcher.getQuotedMatch( + search.substring(predicateStartIndex, index) + )); + if (hasChar() && isCharQuote()) { + incrementCharIndex(); + } + } + + } + + private boolean isCharJoiner() { + return Joiner.set.containsKey(getChar()); + } + + private boolean isCharQuote() { + return getChar() == '"' || getChar() == '\''; + } + + /** + * Returns a SearchPredicate representing the provided expression. + * Meant for use with {@link FindCommand}. + * + * @param query expression to parse into a predicate for Persons. + * @return SearchPredicate that returns true if applied to a Person matching the expression provided. + * @throws ParseException if expression is not supported or is invalid. + */ + public SearchPredicate parse(String query) throws ParseException { + if (query == null) { + return new SearchPredicate(null); + } + try { + search = query.trim(); + SearchMatcher predicate = parse(); + return new SearchPredicate(predicate); + } catch (ParserDualStack.UnexpectedTokenException e) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindCommand.MESSAGE_USAGE)); + } + } + + private SearchMatcher parse() throws ParserDualStack.UnexpectedTokenException { + index = 0; + SearchMatcher predicate = new RecursiveParseHelper().parse(); + while (hasChar()) { + incrementIndexWhileSpace(); + predicate = new RecursiveParseHelper(predicate).parse(); + } + return predicate; + } + + private void incrementIndexWhileSpace() { + while (hasChar() && Character.isSpaceChar(getChar())) { + incrementCharIndex(); + } + } + + private void incrementIndexWhileNotReservedChar() { + while (hasChar()) { + char c = getChar(); + if (Character.isWhitespace(c) || isCharJoiner()) { + break; + } + switch (c) { + case '(': + case ')': + case '"': + case '\'': + return; + default: + incrementCharIndex(); + } + } + } + + private void incrementCharIndex() { + if (hasChar()) { + index++; + } + } + + private boolean hasChar() { + return index < search.length(); + } + + private char getChar() { + return search.charAt(index); + } + +} diff --git a/src/main/java/seedu/address/logic/search/NotSearchMatcher.java b/src/main/java/seedu/address/logic/search/NotSearchMatcher.java new file mode 100644 index 00000000000..8fd8bfeba1c --- /dev/null +++ b/src/main/java/seedu/address/logic/search/NotSearchMatcher.java @@ -0,0 +1,27 @@ +package seedu.address.logic.search; + +import java.util.Map; + +/** + * Represents a negation (flip) of another SearchMatcher. + */ +public class NotSearchMatcher extends SearchMatcher { + + private final SearchMatcher prev; + + NotSearchMatcher(SearchMatcher prev) { + this.prev = prev; + } + + @Override + FieldRanges test(Map p) { + FieldRanges ranges = prev.test(p); + if (!FieldRanges.isMatch(ranges)) { + ranges = new FieldRanges(); + ranges.setIsMatch(true); + } else { + ranges = null; + } + return ranges; + } +} diff --git a/src/main/java/seedu/address/logic/search/OrSearchMatcher.java b/src/main/java/seedu/address/logic/search/OrSearchMatcher.java new file mode 100644 index 00000000000..f60b6d13c9a --- /dev/null +++ b/src/main/java/seedu/address/logic/search/OrSearchMatcher.java @@ -0,0 +1,21 @@ +package seedu.address.logic.search; + +import java.util.Map; + +/** + * Represents the combination of two other SearchMatchers in a logical "or" manner. + * Does not evaluate lazily. + */ +public class OrSearchMatcher extends BinarySearchMatcher { + + private static final char SYMBOL = '/'; + + OrSearchMatcher(SearchMatcher a, SearchMatcher b) { + super(a, b, SYMBOL); + } + + @Override + FieldRanges test(Map p) { + return FieldRanges.union(a.test(p), b.test(p)); + } +} diff --git a/src/main/java/seedu/address/logic/search/Range.java b/src/main/java/seedu/address/logic/search/Range.java new file mode 100644 index 00000000000..6365ef3aaee --- /dev/null +++ b/src/main/java/seedu/address/logic/search/Range.java @@ -0,0 +1,86 @@ +package seedu.address.logic.search; + +import static java.lang.Math.max; +import static java.lang.Math.min; + +/** + * Attached to a String representation of the value of a field, + * represents the start and end of a SearchMatcher match on that field. + * Intended for use by {@link SearchMatcher#test}, to enable certain features such as order and non-overlap. + * or enabling/disabling overlaps. + */ +class Range { + private final int start; + private final int end; + + Range(int start, int end) { + if (start >= end) { //swap + start = start ^ end; + end = start ^ end; + start = start ^ end; + } + this.start = start; + this.end = end; + } + + static boolean isOverlap(Range a, Range b) { + if (a == null || b == null) { + return false; + } + return !(isBefore(a, b) || isAfter(a, b)); + } + + static boolean isBefore(Range a, Range b) { + if (a == null || b == null) { + return true; + } + return a.end < b.start; + } + + static boolean isAfter(Range a, Range b) { + if (a == null || b == null) { + return true; + } + return a.start > b.end; + } + + static Range union(Range a, Range b) { + if (a == null) { + return b; + } else if (b == null) { + return a; + } + return new Range(min(a.start, b.start), max(a.end, b.end)); + } + + public String getSubstring(String string) { + if (string.length() < this.end + 1) { + return null; + } + return string.substring(this.start, this.end + 1); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + Range range = (Range) o; + + if (start != range.start) { + return false; + } + return end == range.end; + } + + @Override + public int hashCode() { //IntelliJ generated + int result = start; + result = 31 * result + end; + return result; + } +} diff --git a/src/main/java/seedu/address/logic/search/SearchMatcher.java b/src/main/java/seedu/address/logic/search/SearchMatcher.java new file mode 100644 index 00000000000..4fffd60aa3d --- /dev/null +++ b/src/main/java/seedu/address/logic/search/SearchMatcher.java @@ -0,0 +1,104 @@ +package seedu.address.logic.search; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +/** + * Represents a predicate (boolean-valued function) of a Map representing a person, + * meant for use in filtering a list of persons. + * Capable of dealing with arbitrary fields. + * + *

Adapted from {@link java.util.function.Predicate}, which could not be used because + * it does not support returning information other than a boolean. + */ +abstract class SearchMatcher { + + enum Flag { + CASE_SENSITIVITY, + FULL_WORD_MATCHING_ONLY + } + + private Map flags; + + SearchMatcher() { + initFlags(); + } + + /** + * Evaluates this predicate on the given argument, + * which is expected to be a Map representation of a {@link seedu.address.model.person.Person}. + * + * @param p the person's attributes, as a Map. + * @return {@link FieldRanges} of all {@code null} values if no match, + * otherwise contains at least one {@link Range} that specifies the location(s) of the match. + */ + abstract FieldRanges test(Map p); + + /** + * Returns a composed predicate that represents a short-circuiting logical + * AND of this predicate and another. When evaluating the composed + * predicate, if this predicate is {@code false}, then the {@code other} + * predicate is not evaluated. + * + *

Any exceptions thrown during evaluation of either predicate are relayed + * to the caller; if evaluation of this predicate throws an exception, the + * {@code other} predicate will not be evaluated. + * + * @param other a predicate that will be logically-ANDed with this + * predicate + * @return a composed predicate that represents the short-circuiting logical + * AND of this predicate and the {@code other} predicate + * @throws NullPointerException if other is null + */ + SearchMatcher and(SearchMatcher other) { + Objects.requireNonNull(other); + return new AndSearchMatcher(this, other); + } + + /** + * Returns a predicate that represents the logical negation of this + * predicate. + * + * @return a predicate that represents the logical negation of this + * predicate. + */ + SearchMatcher negate() { + return new NotSearchMatcher(this); + } + + /** + * Returns a composed predicate that represents a logical OR of this predicate and another. + * When evaluating the composed predicate, no short-circuiting occurs; + * this is due to needing an accurate representation of the matching range. + * + *

Any exceptions thrown during evaluation of either predicate are relayed + * to the caller; if evaluation of this predicate throws an exception, the + * {@code other} predicate will not be evaluated. + * + * @param other a predicate that will be logically-ORed with this + * predicate + * @return a composed predicate that represents the logical + * OR of this predicate and the {@code other} predicate + * @throws NullPointerException if other is null + */ + SearchMatcher or(SearchMatcher other) { + Objects.requireNonNull(other); + return new OrSearchMatcher(this, other); + } + + protected void initFlags() { + flags = new HashMap<>(); + Arrays.stream(Flag.values()).forEach(flag -> flags.put(flag, false)); + } + + void setFlag(Flag flag, boolean isFlagApplied) { + flags.put(flag, isFlagApplied); + } + + boolean isFlagApplied(Flag flag) { + return flags.get(flag); + } + +} diff --git a/src/main/java/seedu/address/logic/search/SearchPredicate.java b/src/main/java/seedu/address/logic/search/SearchPredicate.java new file mode 100644 index 00000000000..8afc05ab8d9 --- /dev/null +++ b/src/main/java/seedu/address/logic/search/SearchPredicate.java @@ -0,0 +1,35 @@ +package seedu.address.logic.search; + +import java.util.Map; +import java.util.function.Predicate; + +import seedu.address.model.person.Person; + +/** + * A layer of abstraction around {@link SearchMatcher} that gives it {@link Predicate} behaviour. + */ +public class SearchPredicate implements Predicate { + + private final SearchMatcher predicate; + + SearchPredicate(SearchMatcher predicate) { + this.predicate = predicate; + } + + /** + * Applies the predicate to the provided person. + * + * @param p person to apply a predicate to. + * @return {@code true} if the person matches the predicate, + * {@code false} otherwise. + */ + @Override + public boolean test(Person p) { + if (predicate == null) { + return true; + } + Map map = p.getFieldsAndAttributes(); + return FieldRanges.isMatch(this.predicate.test(map)); + } + +} diff --git a/src/main/java/seedu/address/logic/search/SingleTextSearchMatcher.java b/src/main/java/seedu/address/logic/search/SingleTextSearchMatcher.java new file mode 100644 index 00000000000..4d142c5e0dc --- /dev/null +++ b/src/main/java/seedu/address/logic/search/SingleTextSearchMatcher.java @@ -0,0 +1,94 @@ +package seedu.address.logic.search; + +import java.util.AbstractMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Searches for a single string of text. + * Applies full word matching on tags regardless of the flag's presence. + */ +class SingleTextSearchMatcher extends SearchMatcher { + private static final Set fullMatches; + + static { + fullMatches = new HashSet<>(); + fullMatches.add("housing"); + fullMatches.add("availability"); + } + + private final String textToFind; + + public SingleTextSearchMatcher(String search) { + textToFind = search; + } + + public static SingleTextSearchMatcher getQuotedMatch(String text) { + SingleTextSearchMatcher matcher = new SingleTextSearchMatcher(text); + matcher.setFlag(Flag.CASE_SENSITIVITY, true); + matcher.setFlag(Flag.FULL_WORD_MATCHING_ONLY, true); + return matcher; + } + + @Override + public FieldRanges test(Map p) { + + Optional> optionalMatch = p.entrySet().stream() + .map(this::getFieldRangeEntryElseNull).filter(Objects::nonNull).findAny(); + + FieldRanges fieldRanges = new FieldRanges(); + optionalMatch.ifPresent(entry -> fieldRanges.put(entry.getKey(), entry.getValue())); + return fieldRanges; + } + + private Map.Entry getFieldRangeEntryElseNull(Map.Entry entry) { + Range range; + if (entry.getValue() == null) { + range = getRangeIfMatchElseNull(entry.getKey(), false); + } else if (fullMatches.contains(entry.getKey())) { + range = getRangeIfMatchElseNull(entry.getValue(), true); + } else { + range = getRangeIfMatchElseNull(entry.getValue(), isFlagApplied(Flag.FULL_WORD_MATCHING_ONLY)); + } + if (range == null) { + return null; + } + return new AbstractMap.SimpleEntry<>(entry.getKey(), range); + } + + private Range getRangeIfMatchElseNull(String target, boolean isFullWord) { + int index = index(target, isFullWord); + if (index == -1) { + return null; + } + return new Range(index, index + textToFind.length() - 1); + } + + + + public int index(String toSearch, boolean isFullWord) { + String regex = isFullWord + ? "\\b" + Pattern.quote(textToFind) + "\\b" + : Pattern.quote(textToFind); + Pattern pattern = isFlagApplied(Flag.CASE_SENSITIVITY) + ? Pattern.compile(regex) + : Pattern.compile(regex, Pattern.CASE_INSENSITIVE); + Matcher matcher = pattern.matcher(toSearch); + + if (matcher.find()) { + return matcher.start(); + } else { + return -1; + } + } + + @Override + public String toString() { + return textToFind; + } +} diff --git a/src/main/java/seedu/address/model/AddressBook.java b/src/main/java/seedu/address/model/AddressBook.java index 73397161e84..031c2fcb932 100644 --- a/src/main/java/seedu/address/model/AddressBook.java +++ b/src/main/java/seedu/address/model/AddressBook.java @@ -2,6 +2,7 @@ import static java.util.Objects.requireNonNull; +import java.util.Comparator; import java.util.List; import javafx.collections.ObservableList; @@ -108,6 +109,15 @@ public ObservableList getPersonList() { return persons.asUnmodifiableObservableList(); } + /** + * Sorts the list of persons using the provided comparator. + * + * @param comparator The comparator to use for sorting. + */ + public void sortNames(Comparator comparator) { + persons.sort(comparator); + } + @Override public boolean equals(Object other) { if (other == this) { diff --git a/src/main/java/seedu/address/model/Model.java b/src/main/java/seedu/address/model/Model.java index d54df471c1f..e15f0556a53 100644 --- a/src/main/java/seedu/address/model/Model.java +++ b/src/main/java/seedu/address/model/Model.java @@ -84,4 +84,9 @@ public interface Model { * @throws NullPointerException if {@code predicate} is null. */ void updateFilteredPersonList(Predicate predicate); + + /** + * Sorts the list of persons in alphabetical order of names. + */ + void sortByName(); } diff --git a/src/main/java/seedu/address/model/ModelManager.java b/src/main/java/seedu/address/model/ModelManager.java index 57bc563fde6..95ed8cfd47c 100644 --- a/src/main/java/seedu/address/model/ModelManager.java +++ b/src/main/java/seedu/address/model/ModelManager.java @@ -4,6 +4,7 @@ import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; import java.nio.file.Path; +import java.util.Comparator; import java.util.function.Predicate; import java.util.logging.Logger; @@ -128,6 +129,16 @@ public void updateFilteredPersonList(Predicate predicate) { filteredPersons.setPredicate(predicate); } + /** + * Sorts the list of fosterers/persons in the address book by name in alphabetical order. + * Uses the natural order of names as the sorting criteria and updates the state of the address book. + */ + @Override + public void sortByName() { + Comparator nameComp = Comparator.comparing(person -> person.getName().toString()); + addressBook.sortNames(nameComp); + } + @Override public boolean equals(Object other) { if (other == this) { diff --git a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java b/src/main/java/seedu/address/model/ReadOnlyAddressBook.java index 6ddc2cd9a29..d7be082d497 100644 --- a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java +++ b/src/main/java/seedu/address/model/ReadOnlyAddressBook.java @@ -1,5 +1,7 @@ package seedu.address.model; +import java.util.Comparator; + import javafx.collections.ObservableList; import seedu.address.model.person.Person; @@ -14,4 +16,6 @@ public interface ReadOnlyAddressBook { */ ObservableList getPersonList(); + void sortNames(Comparator comparator); + } diff --git a/src/main/java/seedu/address/model/person/Address.java b/src/main/java/seedu/address/model/person/Address.java index 469a2cc9a1e..df802a90597 100644 --- a/src/main/java/seedu/address/model/person/Address.java +++ b/src/main/java/seedu/address/model/person/Address.java @@ -31,7 +31,7 @@ public Address(String address) { } /** - * Returns true if a given string is a valid email. + * Returns true if a given string is a valid address. */ public static boolean isValidAddress(String test) { return test.matches(VALIDATION_REGEX); diff --git a/src/main/java/seedu/address/model/person/AnimalType.java b/src/main/java/seedu/address/model/person/AnimalType.java new file mode 100644 index 00000000000..67aa487f1b8 --- /dev/null +++ b/src/main/java/seedu/address/model/person/AnimalType.java @@ -0,0 +1,96 @@ +package seedu.address.model.person; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +/** + * Represents a Person's type of animal fostered, if any, in the address book. + */ +public class AnimalType { + public static final String MESSAGE_CONSTRAINTS = "If fosterer is available, animal type should be " + + "'able.Dog' / 'able.Cat'.\n" + + "If animal type information is not available, it should be inputted as 'nil'.\n" + + "If fosterer is NOT available and is currently fostering, animal type should be " + + "'current.Dog' / 'current.Cat'.\n" + + "If fosterer is currently unable to foster, " + + "animal type should be inputted as 'nil'.\n" + + "If availability is 'nil', animal type should be 'nil' too. "; + + public static final String VALIDATION_REGEX_AVAILABLE = "^(able\\.Dog|able\\.Cat|nil)$"; + public static final String VALIDATION_REGEX_NOT_AVAILABLE = "^(current\\.Dog|current\\.Cat|nil)$"; + + public static final String ABLE_CAT_WORD = "able.Cat"; + public static final String ABLE_DOG_WORD = "able.Dog"; + public static final String CURRENT_CAT_WORD = "current.Cat"; + public static final String CURRENT_DOG_WORD = "current.Dog"; + public static final AnimalType ABLE_CAT = new AnimalType(ABLE_CAT_WORD, Availability.AVAILABLE); + public static final AnimalType ABLE_DOG = new AnimalType(ABLE_DOG_WORD, Availability.AVAILABLE); + public static final AnimalType CURRENT_CAT = new AnimalType(CURRENT_CAT_WORD, Availability.NOT_AVAILABLE); + public static final AnimalType CURRENT_DOG = new AnimalType(CURRENT_DOG_WORD, Availability.NOT_AVAILABLE); + + public static final String VALIDATION_REGEX_NIL = "^(nil)$"; + + + public final Availability availability; + public final String value; + + /** + * Constructs an {@code AnimalType}. + * + * @param value A valid animal type. + * @param availability The availability of the fosterer to determine if the animal type is valid. + */ + public AnimalType(String value, Availability availability) { + requireNonNull(availability); + requireNonNull(value); + + if (availability.equals(Availability.AVAILABLE)) { + checkArgument(isValidAnimalType(value, VALIDATION_REGEX_AVAILABLE), MESSAGE_CONSTRAINTS); + } else if (availability.equals(Availability.NOT_AVAILABLE)) { + checkArgument(isValidAnimalType(value, VALIDATION_REGEX_NOT_AVAILABLE), MESSAGE_CONSTRAINTS); + } else if (availability.equals(Availability.NIL_AVAILABILITY)) { + checkArgument(isValidAnimalType(value, VALIDATION_REGEX_NIL), MESSAGE_CONSTRAINTS); + } + + this.value = value; + this.availability = availability; + } + + /** + * Returns true if a given string is a valid animal type. + */ + public static boolean isValidAnimalType(String test, String validationRegex) { + return test.matches(validationRegex); + } + + /** + * Checks if the given string can be a valid Animal Type under any situation. + * + * @return true if the animal type matches any possible validation. + */ + public static boolean isValidAnimalType(String test) { + return test.matches(VALIDATION_REGEX_AVAILABLE) || test.matches(VALIDATION_REGEX_NOT_AVAILABLE); + } + + @Override + public String toString() { + return value; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + AnimalType otherAnimalType = (AnimalType) other; + return value.equals(otherAnimalType.value) && availability.equals(otherAnimalType.availability); + } + + @Override + public int hashCode() { + return value.hashCode(); + } +} diff --git a/src/main/java/seedu/address/model/person/Availability.java b/src/main/java/seedu/address/model/person/Availability.java new file mode 100644 index 00000000000..c89565eeff8 --- /dev/null +++ b/src/main/java/seedu/address/model/person/Availability.java @@ -0,0 +1,62 @@ +package seedu.address.model.person; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +/** + * Represents a Person's availability to foster an animal in the address book. + */ +public class Availability { + public static final String MESSAGE_CONSTRAINTS = "Availability should be either 'Available', " + + "'NotAvailable' or 'nil'"; + + public static final String VALIDATION_REGEX = "^(Available|NotAvailable|nil)$"; + + public static final String AVAILABLE_WORD = "Available"; + public static final String NOT_AVAILABLE_WORD = "NotAvailable"; + public static final Availability AVAILABLE = new Availability(AVAILABLE_WORD); + public static final Availability NOT_AVAILABLE = new Availability(NOT_AVAILABLE_WORD); + public static final Availability NIL_AVAILABILITY = new Availability(Person.NIL_WORD); + + public final String value; + + /** + * Constructs an {@code Availability}. + * + * @param value A valid availability. + */ + public Availability(String value) { + requireNonNull(value); + checkArgument(isValidAvailability(value), MESSAGE_CONSTRAINTS); + this.value = value; + } + + /** + * Returns true if a given string is a valid availability. + */ + public static boolean isValidAvailability(String test) { + return test.matches(VALIDATION_REGEX); + } + + @Override + public String toString() { + return value; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + Availability otherAvailability = (Availability) other; + return value.equals(otherAvailability.value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } +} diff --git a/src/main/java/seedu/address/model/person/Email.java b/src/main/java/seedu/address/model/person/Email.java index c62e512bc29..82ae9313c84 100644 --- a/src/main/java/seedu/address/model/person/Email.java +++ b/src/main/java/seedu/address/model/person/Email.java @@ -45,7 +45,7 @@ public Email(String email) { } /** - * Returns if a given string is a valid email. + * Returns true if a given string is a valid email. */ public static boolean isValidEmail(String test) { return test.matches(VALIDATION_REGEX); diff --git a/src/main/java/seedu/address/model/person/Housing.java b/src/main/java/seedu/address/model/person/Housing.java new file mode 100644 index 00000000000..3a5489a031d --- /dev/null +++ b/src/main/java/seedu/address/model/person/Housing.java @@ -0,0 +1,60 @@ +package seedu.address.model.person; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +/** + * Represents a Person's housing type in the address book. + */ +public class Housing { + public static final String MESSAGE_CONSTRAINTS = "Housing type should be either 'HDB', 'Condo', 'Landed' or 'nil'"; + public static final String VALIDATION_REGEX = "^(HDB|Condo|Landed|nil)$"; + public static final String HDB_WORD = "HDB"; + public static final String CONDO_WORD = "Condo"; + public static final String LANDED_WORD = "Landed"; + public static final Housing HDB = new Housing(HDB_WORD); + public static final Housing CONDO = new Housing(CONDO_WORD); + public static final Housing LANDED = new Housing(LANDED_WORD); + + public final String value; + + /** + * Constructs an {@code Housing}. + * + * @param value A valid housing type. + */ + public Housing(String value) { + requireNonNull(value); + checkArgument(isValidHousing(value), MESSAGE_CONSTRAINTS); + this.value = value; + } + + /** + * Returns true if a given string is a valid housing type. + */ + public static boolean isValidHousing(String test) { + return test.matches(VALIDATION_REGEX); + } + + @Override + public String toString() { + return value; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + Housing otherHousing = (Housing) other; + return value.equals(otherHousing.value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } +} diff --git a/src/main/java/seedu/address/model/person/Name.java b/src/main/java/seedu/address/model/person/Name.java index 173f15b9b00..d141b35680f 100644 --- a/src/main/java/seedu/address/model/person/Name.java +++ b/src/main/java/seedu/address/model/person/Name.java @@ -44,6 +44,10 @@ public String toString() { return fullName; } + /** + * Returns true when the two case-insensitive names are the same, ignoring the presence of + * multiple spaces between words. + */ @Override public boolean equals(Object other) { if (other == this) { @@ -56,7 +60,11 @@ public boolean equals(Object other) { } Name otherName = (Name) other; - return fullName.equals(otherName.fullName); + // replaceAll("\\s+", " ") replaces 1 or more spaces with a single space + String nameRemovedSpaces = fullName.replaceAll("\\s+", " "); + String otherNameRemovedSpaces = otherName.fullName.replaceAll("\\s+", " "); + // equalsIgnoreCase() method compares two strings, ignoring lower case and upper case differences + return nameRemovedSpaces.equalsIgnoreCase(otherNameRemovedSpaces); } @Override diff --git a/src/main/java/seedu/address/model/person/Person.java b/src/main/java/seedu/address/model/person/Person.java index abe8c46b535..4930c48879b 100644 --- a/src/main/java/seedu/address/model/person/Person.java +++ b/src/main/java/seedu/address/model/person/Person.java @@ -3,7 +3,9 @@ import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; +import java.util.Map; import java.util.Objects; import java.util.Set; @@ -16,6 +18,14 @@ */ public class Person { + // Error Messages + public static final String AVAILABLE_WHILE_ANIMAL_NAMED_MESSAGE = + "When an animal name is provided, availability should not be 'Available' or 'nil'."; + public static final String ANIMAL_NAME_TYPE_MISMATCH_WHEN_UNAVAILABLE_MESSAGE = + "When availability is 'NotAvailable', animal name and type have to either be both 'nil' or both not 'nil'."; + public static final String NIL_WORD = "nil"; + private static final String NAME_CANNOT_BE_NIL_MESSAGE = "Name of fosterer cannot be 'nil'!"; + // Identity fields private final Name name; private final Phone phone; @@ -25,16 +35,93 @@ public class Person { private final Address address; private final Set tags = new HashSet<>(); + private final Name animalName; + private final Availability availability; + private final Housing housing; + private final AnimalType animalType; + private String note; + /** - * Every field must be present and not null. + * Constructor for Person object. Ensures that required fields are 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, Housing housing, + Availability availability, Name animalName, AnimalType animalType, + Set tags) { + requireAllNonNull(name, phone, email, address, housing, availability, animalName, animalType, tags); this.name = name; this.phone = phone; this.email = email; this.address = address; + this.housing = housing; + this.availability = availability; + this.animalName = animalName; + this.animalType = animalType; this.tags.addAll(tags); + + if (Objects.equals(name.fullName, Person.NIL_WORD)) { + throw new IllegalArgumentException(NAME_CANNOT_BE_NIL_MESSAGE); + } + + if (!isAvailabilityValidWhenAnimalNameNotNil()) { + throw new IllegalArgumentException(AVAILABLE_WHILE_ANIMAL_NAMED_MESSAGE); + } + + if (!isAnimalNameTypeValidWhenNotAvailable()) { + throw new IllegalArgumentException(ANIMAL_NAME_TYPE_MISMATCH_WHEN_UNAVAILABLE_MESSAGE); + } + } + + /** + * Minimal constructor that fills non-required fields with placeholder values (nil). + */ + public Person(Name name, Phone phone, Email email, Address address, Set tags) { + this(name, phone, email, address, new Housing(NIL_WORD), new Availability(NIL_WORD), + new Name(NIL_WORD), new AnimalType(NIL_WORD, new Availability(NIL_WORD)), tags); + } + + /** + * Returns boolean value to check if availability is valid based on presence of animal name. + * + * @return a boolean value which represents if availability is valid. + */ + public boolean isAvailabilityValidWhenAnimalNameNotNil() { + String avail = availability.value; + if (!animalName.fullName.equals(Person.NIL_WORD)) { + return !(avail.equals("Available") || avail.equals(Person.NIL_WORD)); + } + return true; + } + + /** + * Returns boolean value to check if animal name and type are valid when NotAvailable. + * + * @return a boolean value which represents if animal name and type are valid. + */ + public boolean isAnimalNameTypeValidWhenNotAvailable() { + String avail = availability.value; + if (avail.equals("NotAvailable")) { + String type = animalType.value; + String name = animalName.fullName; + return (name.equals("nil") && type.equals("nil")) + || (!name.equals("nil") && !type.equals("nil")); + } + return true; + } + + public Name getAnimalName() { + return animalName; + } + + public Availability getAvailability() { + return availability; + } + + public Housing getHousing() { + return housing; + } + + public AnimalType getAnimalType() { + return animalType; } public Name getName() { @@ -53,6 +140,43 @@ public Address getAddress() { return address; } + public String getNote() { + return Objects.requireNonNullElse(note, ""); + } + + public void setNote(String note) { + this.note = note; + } + + /** + * Returns a map of fields and their existing attributes as strings. + * Intended for use with predicates generated through the find command. + * + * @return a Map; keys include all publicly gettable fields as well as all tags, + * and values are values of the respective fields, or {@code null} for tags. + */ + public Map getFieldsAndAttributes() { + HashMap map = new HashMap<>(); + tryPut(map, "name", getName()); + tryPut(map, "phone", getPhone()); + tryPut(map, "email", getEmail()); + tryPut(map, "address", getAddress()); + tryPut(map, "housing", getHousing()); + tryPut(map, "availability", getAvailability()); + tryPut(map, "animal name", getAnimalName()); + tryPut(map, "animal type", getAnimalType()); + getTags().forEach(tag -> map.put(tag.tagName, null)); + tryPut(map, "note", getNote()); + return map; + } + + protected void tryPut(Map map, String key, Object value) { + if (Objects.isNull(value)) { + return; + } + map.put(key, value.toString()); + } + /** * Returns an immutable tag set, which throws {@code UnsupportedOperationException} * if modification is attempted. @@ -61,6 +185,28 @@ public Set getTags() { return Collections.unmodifiableSet(tags); } + + /** + * Returns true if person is an available fosterer. + */ + public boolean isAvailableFosterer() { + return availability.equals(Availability.AVAILABLE); + } + + /** + * Returns true if person is a current fosterer. + * Person is a current fosterer if and only if they are not available, and is + * currently fostering an animal. + */ + public boolean isCurrentFosterer() { + boolean isNotAvailable = this.availability.equals(Availability.NOT_AVAILABLE); + boolean isAnimalNameNil = this.animalName.fullName.equals(Person.NIL_WORD); + boolean isAnimalCurrentCatOrDog = this.animalType.equals(AnimalType.CURRENT_DOG) + || this.animalType.equals(AnimalType.CURRENT_CAT); + + return isNotAvailable && !isAnimalNameNil && isAnimalCurrentCatOrDog; + } + /** * Returns true if both persons have the same name. * This defines a weaker notion of equality between two persons. @@ -94,13 +240,18 @@ public boolean equals(Object other) { && phone.equals(otherPerson.phone) && email.equals(otherPerson.email) && address.equals(otherPerson.address) - && tags.equals(otherPerson.tags); + && tags.equals(otherPerson.tags) + && animalName.equals(otherPerson.animalName) + && availability.equals(otherPerson.availability) + && animalType.equals(otherPerson.animalType) + && housing.equals(otherPerson.housing) + && this.getNote().equals(otherPerson.getNote()); } @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, housing, availability, animalName, animalType, tags); } @Override @@ -111,7 +262,10 @@ public String toString() { .add("email", email) .add("address", address) .add("tags", tags) + .add("animalName", animalName) + .add("availability", availability) + .add("animalType", animalType) + .add("housing", housing) .toString(); } - } diff --git a/src/main/java/seedu/address/model/person/UniquePersonList.java b/src/main/java/seedu/address/model/person/UniquePersonList.java index cc0a68d79f9..fc577b63c3b 100644 --- a/src/main/java/seedu/address/model/person/UniquePersonList.java +++ b/src/main/java/seedu/address/model/person/UniquePersonList.java @@ -3,6 +3,7 @@ import static java.util.Objects.requireNonNull; import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; +import java.util.Comparator; import java.util.Iterator; import java.util.List; @@ -109,6 +110,16 @@ public Iterator iterator() { return internalList.iterator(); } + /** + * Sorts the list of persons using the provided comparator. + * This method allows for sorting of the list of fosterers based on a specified comparator. + * + * @param comparator The comparator to use for sorting the list of fosterers. + */ + public void sort(Comparator comparator) { + internalList.sort(comparator); + } + @Override public boolean equals(Object other) { if (other == this) { diff --git a/src/main/java/seedu/address/model/tag/Tag.java b/src/main/java/seedu/address/model/tag/Tag.java index f1a0d4e233b..0d5bcd982b3 100644 --- a/src/main/java/seedu/address/model/tag/Tag.java +++ b/src/main/java/seedu/address/model/tag/Tag.java @@ -59,4 +59,7 @@ public String toString() { return '[' + tagName + ']'; } + public String getTagName() { + return 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..eba6914b20c 100644 --- a/src/main/java/seedu/address/model/util/SampleDataUtil.java +++ b/src/main/java/seedu/address/model/util/SampleDataUtil.java @@ -7,7 +7,10 @@ import seedu.address.model.AddressBook; import seedu.address.model.ReadOnlyAddressBook; import seedu.address.model.person.Address; +import seedu.address.model.person.AnimalType; +import seedu.address.model.person.Availability; import seedu.address.model.person.Email; +import seedu.address.model.person.Housing; import seedu.address.model.person.Name; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; @@ -17,27 +20,52 @@ * Contains utility methods for populating {@code AddressBook} with sample data. */ public class SampleDataUtil { + public static final Availability AVAILABLE = new Availability("Available"); + public static final Availability NOT_AVAILABLE = new Availability("NotAvailable"); + public static final Availability NIL_AVAILABILITY = new Availability("nil"); + public static Person[] getSamplePersons() { - return new Person[] { - new Person(new Name("Alex Yeoh"), new Phone("87438807"), new Email("alexyeoh@example.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"), - 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"), - 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"), - 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"), - 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"), - new Address("Blk 45 Aljunied Street 85, #11-31"), - getTagSet("colleagues")) - }; + Person personOne = new Person(new Name("Alex Yeoh"), new Phone("87438807"), + new Email("alexyeoh@example.com"), new Address("Blk 30 Geylang Street 29, #06-40"), + new Housing("HDB"), AVAILABLE, new Name("nil"), + new AnimalType("able.Cat", AVAILABLE), + getTagSet("new")); + + Person personTwo = new Person(new Name("Bernice Yu"), new Phone("99272758"), + new Email("berniceyu@example.com"), + new Address("Blk 30 Lorong 3 Serangoon Gardens, #07-18"), new Housing("Landed"), + NOT_AVAILABLE, new Name("Bobby"), + new AnimalType("current.Dog", NOT_AVAILABLE), + getTagSet("reliable", "experienced")); + + Person personThree = new Person(new Name("Charlotte Oliveiro"), new Phone("93210283"), + new Email("charlotte@example.com"), new Address("Blk 11 Ang Mo Kio Street 74, #11-04"), + new Housing("HDB"), AVAILABLE, new Name("nil"), + new AnimalType("able.Dog", AVAILABLE), + getTagSet("new")); + + Person personFour = new Person(new Name("David Li"), new Phone("91031282"), + new Email("lidavid@example.com"), + new Address("Blk 436 Serangoon Gardens Street 26, #16-43"), new Housing("Condo"), + NOT_AVAILABLE, new Name("MeowMeow"), + new AnimalType("current.Cat", NOT_AVAILABLE), + getTagSet("new")); + + Person personFive = new Person(new Name("Irfan Ibrahim"), new Phone("92492021"), + new Email("irfan@example.com"), + new Address("Blk 47 Tampines Street 20, #17-35"), new Housing("HDB"), + NIL_AVAILABILITY, new Name("nil"), + new AnimalType("nil", NIL_AVAILABILITY), + getTagSet("experienced")); + + Person personSix = new Person(new Name("Roy Balakrishnan"), new Phone("92624417"), + new Email("royb@example.com"), + new Address("Blk 45 Aljunied Street 85, #11-31"), new Housing("Landed"), + AVAILABLE, new Name("nil"), + new AnimalType("able.Cat", AVAILABLE), + getTagSet("new")); + + return new Person[]{personOne, personTwo, personThree, personFour, personFive, personSix}; } public static ReadOnlyAddressBook getSampleAddressBook() { diff --git a/src/main/java/seedu/address/storage/JsonAdaptedPerson.java b/src/main/java/seedu/address/storage/JsonAdaptedPerson.java index bd1ca0f56c8..d3c41ada037 100644 --- a/src/main/java/seedu/address/storage/JsonAdaptedPerson.java +++ b/src/main/java/seedu/address/storage/JsonAdaptedPerson.java @@ -11,7 +11,10 @@ import seedu.address.commons.exceptions.IllegalValueException; import seedu.address.model.person.Address; +import seedu.address.model.person.AnimalType; +import seedu.address.model.person.Availability; import seedu.address.model.person.Email; +import seedu.address.model.person.Housing; import seedu.address.model.person.Name; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; @@ -29,6 +32,10 @@ class JsonAdaptedPerson { private final String email; private final String address; private final List tags = new ArrayList<>(); + private final String animalName; + private final String availability; + private final String animalType; + private final String housing; /** * Constructs a {@code JsonAdaptedPerson} with the given person details. @@ -36,7 +43,10 @@ class JsonAdaptedPerson { @JsonCreator public JsonAdaptedPerson(@JsonProperty("name") String name, @JsonProperty("phone") String phone, @JsonProperty("email") String email, @JsonProperty("address") String address, - @JsonProperty("tags") List tags) { + @JsonProperty("tags") List tags, @JsonProperty("animalName") String animalName, + @JsonProperty("availability") String availability, + @JsonProperty("animalType") String animalType, + @JsonProperty("housing") String housing) { this.name = name; this.phone = phone; this.email = email; @@ -44,6 +54,10 @@ public JsonAdaptedPerson(@JsonProperty("name") String name, @JsonProperty("phone if (tags != null) { this.tags.addAll(tags); } + this.animalName = animalName; + this.availability = availability; + this.animalType = animalType; + this.housing = housing; } /** @@ -57,6 +71,10 @@ public JsonAdaptedPerson(Person source) { tags.addAll(source.getTags().stream() .map(JsonAdaptedTag::new) .collect(Collectors.toList())); + animalName = source.getAnimalName().fullName; + availability = source.getAvailability().value; + animalType = source.getAnimalType().value; + housing = source.getHousing().value; } /** @@ -65,45 +83,92 @@ public JsonAdaptedPerson(Person source) { * @throws IllegalValueException if there were any data constraints violated in the adapted person. */ public Person toModelType() throws IllegalValueException { - final List personTags = new ArrayList<>(); - for (JsonAdaptedTag tag : tags) { - personTags.add(tag.toModelType()); - } + try { + final List personTags = new ArrayList<>(); + for (JsonAdaptedTag tag : tags) { + personTags.add(tag.toModelType()); + } - if (name == null) { - throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Name.class.getSimpleName())); - } - if (!Name.isValidName(name)) { - throw new IllegalValueException(Name.MESSAGE_CONSTRAINTS); - } - final Name modelName = new Name(name); + if (name == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, + Name.class.getSimpleName())); + } + if (!Name.isValidName(name)) { + throw new IllegalValueException(Name.MESSAGE_CONSTRAINTS); + } + final Name modelName = new Name(name); - if (phone == null) { - throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Phone.class.getSimpleName())); - } - if (!Phone.isValidPhone(phone)) { - throw new IllegalValueException(Phone.MESSAGE_CONSTRAINTS); - } - final Phone modelPhone = new Phone(phone); + if (phone == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, + Phone.class.getSimpleName())); + } + if (!Phone.isValidPhone(phone)) { + throw new IllegalValueException(Phone.MESSAGE_CONSTRAINTS); + } + final Phone modelPhone = new Phone(phone); - if (email == null) { - throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Email.class.getSimpleName())); - } - if (!Email.isValidEmail(email)) { - throw new IllegalValueException(Email.MESSAGE_CONSTRAINTS); - } - final Email modelEmail = new Email(email); + if (email == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, + Email.class.getSimpleName())); + } + if (!Email.isValidEmail(email)) { + throw new IllegalValueException(Email.MESSAGE_CONSTRAINTS); + } + final Email modelEmail = new Email(email); - if (address == null) { - throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Address.class.getSimpleName())); - } - if (!Address.isValidAddress(address)) { - throw new IllegalValueException(Address.MESSAGE_CONSTRAINTS); - } - final Address modelAddress = new Address(address); + if (address == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, + Address.class.getSimpleName())); + } + if (!Address.isValidAddress(address)) { + throw new IllegalValueException(Address.MESSAGE_CONSTRAINTS); + } + final Address modelAddress = new Address(address); + + final Set modelTags = new HashSet<>(personTags); + + if (animalName == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, + Name.class.getSimpleName())); + } + if (!Name.isValidName(animalName)) { + throw new IllegalValueException(Name.MESSAGE_CONSTRAINTS); + } + final Name modelAnimalName = new Name(animalName); + + if (availability == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, + Availability.class.getSimpleName())); + } + if (!Availability.isValidAvailability(availability)) { + throw new IllegalValueException(Availability.MESSAGE_CONSTRAINTS); + } + final Availability modelAvailability = new Availability(availability); - final Set modelTags = new HashSet<>(personTags); - return new Person(modelName, modelPhone, modelEmail, modelAddress, modelTags); + if (animalType == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, + AnimalType.class.getSimpleName())); + } + if (!AnimalType.isValidAnimalType(animalType)) { + throw new IllegalValueException(AnimalType.MESSAGE_CONSTRAINTS); + } + final AnimalType modelAnimaltype = new AnimalType(animalType, modelAvailability); + + if (housing == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, + Housing.class.getSimpleName())); + } + if (!Housing.isValidHousing(housing)) { + throw new IllegalValueException(Housing.MESSAGE_CONSTRAINTS); + } + final Housing modelHousing = new Housing(housing); + + return new Person(modelName, modelPhone, modelEmail, modelAddress, modelHousing, + modelAvailability, modelAnimalName, modelAnimaltype, modelTags); + + } catch (IllegalArgumentException e) { + throw new IllegalValueException(e.getMessage()); + } } } diff --git a/src/main/java/seedu/address/storage/StorageManager.java b/src/main/java/seedu/address/storage/StorageManager.java index 8b84a9024d5..f28c66ed92a 100644 --- a/src/main/java/seedu/address/storage/StorageManager.java +++ b/src/main/java/seedu/address/storage/StorageManager.java @@ -7,6 +7,7 @@ import seedu.address.commons.core.LogsCenter; import seedu.address.commons.exceptions.DataLoadingException; +import seedu.address.model.AddressBook; import seedu.address.model.ReadOnlyAddressBook; import seedu.address.model.ReadOnlyUserPrefs; import seedu.address.model.UserPrefs; @@ -20,6 +21,8 @@ public class StorageManager implements Storage { private AddressBookStorage addressBookStorage; private UserPrefsStorage userPrefsStorage; + private AddressBook originalAddressBook; + /** * Creates a {@code StorageManager} with the given {@code AddressBookStorage} and {@code UserPrefStorage}. */ diff --git a/src/main/java/seedu/address/ui/CommandBox.java b/src/main/java/seedu/address/ui/CommandBox.java index 9e75478664b..6450b7fda0e 100644 --- a/src/main/java/seedu/address/ui/CommandBox.java +++ b/src/main/java/seedu/address/ui/CommandBox.java @@ -3,6 +3,8 @@ import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.scene.control.TextField; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; import javafx.scene.layout.Region; import seedu.address.logic.commands.CommandResult; import seedu.address.logic.commands.exceptions.CommandException; @@ -15,17 +17,19 @@ public class CommandBox extends UiPart { public static final String ERROR_STYLE_CLASS = "error"; private static final String FXML = "CommandBox.fxml"; - + private MainWindow mainWindow; private final CommandExecutor commandExecutor; @FXML private TextField commandTextField; + private boolean inConfirmationDialog; /** * Creates a {@code CommandBox} with the given {@code CommandExecutor}. */ - public CommandBox(CommandExecutor commandExecutor) { + public CommandBox(MainWindow mainWindow, CommandExecutor commandExecutor) { super(FXML); + this.mainWindow = mainWindow; this.commandExecutor = commandExecutor; // calls #setStyleToDefault() whenever there is a change to the text of the command box. commandTextField.textProperty().addListener((unused1, unused2, unused3) -> setStyleToDefault()); @@ -35,17 +39,41 @@ public CommandBox(CommandExecutor commandExecutor) { * Handles the Enter button pressed event. */ @FXML - private void handleCommandEntered() { - String commandText = commandTextField.getText(); - if (commandText.equals("")) { + private void handleKey(KeyEvent keyEvent) { + if (!inConfirmationDialog && keyEvent.getCode() != KeyCode.ENTER) { + return; + } + + if (inConfirmationDialog) { + commandTextField.setEditable(false); + } + + if (inConfirmationDialog && keyEvent.getCode() == KeyCode.ENTER) { + mainWindow.handleViewExit(); + commandTextField.setText(""); + commandTextField.setEditable(true); return; } - try { - commandExecutor.execute(commandText); + if (inConfirmationDialog && keyEvent.getCode() == KeyCode.ESCAPE) { commandTextField.setText(""); - } catch (CommandException | ParseException e) { - setStyleToIndicateCommandFailure(); + mainWindow.handleCancelViewExit(); + commandTextField.setEditable(true); + } + + if (!inConfirmationDialog && keyEvent.getCode() == KeyCode.ENTER) { + String commandText = commandTextField.getText(); + + if (commandText.equals("")) { + return; + } + + try { + commandExecutor.execute(commandText); + commandTextField.setText(""); + } catch (CommandException | ParseException e) { + setStyleToIndicateCommandFailure(); + } } } @@ -69,6 +97,21 @@ private void setStyleToIndicateCommandFailure() { styleClass.add(ERROR_STYLE_CLASS); } + /** + * Sets the focus of the user's cursor back to the TextField in the CommandBox. + */ + public void setFocus() { + commandTextField.requestFocus(); + } + + public void setIsInConfirmationDialog(boolean inConfirmationDialog) { + this.inConfirmationDialog = inConfirmationDialog; + } + + public boolean getInConfirmationDialog() { + return inConfirmationDialog; + } + /** * Represents a function that can execute commands. */ diff --git a/src/main/java/seedu/address/ui/HelpWindow.java b/src/main/java/seedu/address/ui/HelpWindow.java index 3f16b2fcf26..1ffb8d85f98 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://ay2324s1-cs2103t-t13-4.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/MainWindow.java b/src/main/java/seedu/address/ui/MainWindow.java index 79e74ef37c0..9fd2daeb136 100644 --- a/src/main/java/seedu/address/ui/MainWindow.java +++ b/src/main/java/seedu/address/ui/MainWindow.java @@ -1,5 +1,9 @@ package seedu.address.ui; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Optional; import java.util.logging.Logger; import javafx.event.ActionEvent; @@ -12,10 +16,13 @@ import javafx.stage.Stage; import seedu.address.commons.core.GuiSettings; import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.core.index.Index; +import seedu.address.commons.exceptions.DataLoadingException; import seedu.address.logic.Logic; import seedu.address.logic.commands.CommandResult; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.Person; /** * The Main Window. Provides the basic application layout containing @@ -24,31 +31,34 @@ public class MainWindow extends UiPart { private static final String FXML = "MainWindow.fxml"; - private final Logger logger = LogsCenter.getLogger(getClass()); - - private Stage primaryStage; - private Logic logic; + private final Stage primaryStage; + private final Logic logic; // Independent Ui parts residing in this Ui container private PersonListPanel personListPanel; + private PersonProfile personProfile; private ResultDisplay resultDisplay; private HelpWindow helpWindow; + private Index targetIndex; + private boolean isSaved; + private boolean isShowingConfirmationMessage; + @FXML private StackPane commandBoxPlaceholder; - @FXML private MenuItem helpMenuItem; - @FXML private StackPane personListPanelPlaceholder; - + @FXML + private StackPane personProfilePlaceholder; @FXML private StackPane resultDisplayPlaceholder; - @FXML private StackPane statusbarPlaceholder; + @FXML + private CommandBox commandBox; /** * Creates a {@code MainWindow} with the given {@code Stage} and {@code Logic}. @@ -66,6 +76,7 @@ public MainWindow(Stage primaryStage, Logic logic) { setAccelerators(); helpWindow = new HelpWindow(); + } public Stage getPrimaryStage() { @@ -78,6 +89,7 @@ private void setAccelerators() { /** * Sets the accelerator of a MenuItem. + * * @param keyCombination the KeyCombination value of the accelerator */ private void setAccelerator(MenuItem menuItem, KeyCombination keyCombination) { @@ -119,7 +131,8 @@ void fillInnerParts() { StatusBarFooter statusBarFooter = new StatusBarFooter(logic.getAddressBookFilePath()); statusbarPlaceholder.getChildren().add(statusBarFooter.getRoot()); - CommandBox commandBox = new CommandBox(this::executeCommand); + CommandBox commandBox = new CommandBox(this, this::executeCommand); + this.commandBox = commandBox; commandBoxPlaceholder.getChildren().add(commandBox.getRoot()); } @@ -135,11 +148,19 @@ private void setWindowDefaultSize(GuiSettings guiSettings) { } } + void show() { + primaryStage.show(); + } + + void sendFeedback(String feedback) { + resultDisplay.setFeedbackToUser(feedback); + } + /** * Opens the help window or focuses on it if it's already opened. */ @FXML - public void handleHelp() { + private void handleHelp() { if (!helpWindow.isShowing()) { helpWindow.show(); } else { @@ -147,10 +168,6 @@ public void handleHelp() { } } - void show() { - primaryStage.show(); - } - /** * Closes the application. */ @@ -163,8 +180,17 @@ private void handleExit() { primaryStage.hide(); } - public PersonListPanel getPersonListPanel() { - return personListPanel; + @FXML + private void handleFocusField(PersonProfile.Field field) { + if (personProfilePlaceholder.isVisible() && !personListPanelPlaceholder.isVisible()) { + personProfile.setFocus(field); + } + } + + private void resetValues() { + if (personProfilePlaceholder.isVisible() && !personListPanelPlaceholder.isVisible()) { + personProfile.resetValues(); + } } /** @@ -174,23 +200,202 @@ public PersonListPanel getPersonListPanel() { */ private CommandResult executeCommand(String commandText) throws CommandException, ParseException { try { - CommandResult commandResult = logic.execute(commandText); + CommandResult commandResult; + if (personListPanelPlaceholder.isVisible()) { + commandResult = logic.execute(commandText); + } else { + commandResult = logic.executeInView(commandText, personProfile.getPerson(), targetIndex); + } + logger.info("Result: " + commandResult.getFeedbackToUser()); resultDisplay.setFeedbackToUser(commandResult.getFeedbackToUser()); - if (commandResult.isShowHelp()) { - handleHelp(); + if (commandResult.getCommandType() == null) { + return commandResult; } - if (commandResult.isExit()) { + switch (commandResult.getCommandType()) { + + case HELP: + handleHelp(); + break; + + case EXIT: handleExit(); + break; + + case VIEW: + targetIndex = commandResult.getTargetIndex(); + handleView(commandResult.getPersonToView()); + break; + + case VIEW_EXIT: + isSaved = commandResult.getIsFostererEdited(); + handleViewExit(); + break; + + case EDIT_FIELD: + handleEditField(commandText); + break; + + case SAVE: + handleSave(); + break; + + default: + break; } return commandResult; + } catch (CommandException | ParseException e) { logger.info("An error occurred while executing command: " + commandText); resultDisplay.setFeedbackToUser(e.getMessage()); + resetValues(); throw e; + } catch (IOException e) { + throw new RuntimeException(e); + } catch (DataLoadingException e) { + throw new RuntimeException(e); + } + } + + void handleViewExit() { + isShowingConfirmationMessage = commandBox.getInConfirmationDialog(); + // if the fosterer is already saved or the confirmation message is already displayed, exit the profile page. + if (isSaved || isShowingConfirmationMessage) { + exitProfilePage(); + } else { + // if fosterer is not saved or message is not displayed before, display the confirmation message + displayConfirmationMessage(); + } + } + + void handleCancelViewExit() { + sendFeedback("Cancelled exit."); + commandBox.setIsInConfirmationDialog(false); + personProfile.setIsInConfirmationDialog(false); + } + + private void exitProfilePage() { + commandBox.setIsInConfirmationDialog(false); + personProfile.setIsInConfirmationDialog(false); + personListPanelPlaceholder.setVisible(true); + personProfilePlaceholder.getChildren().remove(personProfile.getRoot()); + personProfilePlaceholder.setVisible(false); + sendFeedback("Exiting view as requested."); + } + + private void displayConfirmationMessage() { + commandBox.setIsInConfirmationDialog(true); + personProfile.setIsInConfirmationDialog(true); + } + + /** + * Opens a fosterer's profile. + * + * @param personToView is a person to view their profile. + */ + private void handleView(Person personToView) { + + // open a profile of a fosterer in the list + if (personListPanelPlaceholder.isVisible() && personToView != null) { + personProfile = new PersonProfile(personToView, this); + } + + // open an empty profile to add a new fosterer + if (personListPanelPlaceholder.isVisible() && personToView == null) { + personProfile = new PersonProfile(this); + } + + personProfilePlaceholder.getChildren().add(personProfile.getRoot()); + personProfilePlaceholder.setVisible(true); + personListPanelPlaceholder.setVisible(false); + } + + private void handleEditField(String commandText) { + String[] tagAndNote = new String[]{"tags", "notes"}; + Optional field; + Optional tagOrNote = null; + + // check if the command text matches any one of the prefix of the fields + field = findPrefixMatchField(commandText); + + // if no match, look for the prefix match from either tags or notes + if (!field.isPresent()) { + tagOrNote = findPrefixMatchString(commandText, tagAndNote); + } + + // if still no match, look for a field name that contains the command text + if (!field.isPresent() && !tagOrNote.isPresent()) { + field = findContainsMatchField(commandText); + } + + // if still no match, look for a contain match from either tags or notes + if (!field.isPresent() && !tagOrNote.isPresent()) { + tagOrNote = findContainsMatchString(commandText, tagAndNote); } + + // try setting focus + setFocus(field, tagOrNote); + + // if no match is found, then display no such field is found + if (!field.isPresent() && !tagOrNote.isPresent()) { + sendFeedback("No such field found"); + } + } + + private Optional findPrefixMatchField(String commandText) { + Optional field = Arrays.stream(PersonProfile.Field.values()) + .filter(f -> f.getDisplayName().toLowerCase().startsWith(commandText.toLowerCase().trim())) + .findFirst(); + + return field; + } + + private Optional findPrefixMatchString(String commandText, String[] tagAndNote) { + Optional field = Arrays.stream(tagAndNote) + .filter(f -> f.startsWith(commandText.toLowerCase().trim())) + .findFirst(); + + return field; + } + + private Optional findContainsMatchField(String commandText) { + Optional field = Arrays.stream(PersonProfile.Field.values()) + .filter(f -> f.getDisplayName().toLowerCase().contains(commandText.toLowerCase().trim())) + .findFirst(); + + return field; + } + + private Optional findContainsMatchString(String commandText, String[] tagAndNote) { + Optional field = Arrays.stream(tagAndNote) + .filter(f -> f.contains(commandText.toLowerCase().trim())) + .findFirst(); + + return field; + } + + private void setFocus(Optional field, Optional tagOrNote) { + // if found a match from fields that are neither tags nor notes, then call setFocus + if (field.isPresent()) { + field.ifPresent(personProfile::setFocus); + } + + // if the fields are either tag or note, then call setFocus accordingly + if (!field.isPresent() && tagOrNote.isPresent()) { + tagOrNote.ifPresent(f -> { + if (f.equals("tags")) { + personProfile.setFocusTags(); + } else { + personProfile.setFocusNotes(); + } + }); + } + } + + private void handleSave() { + resetValues(); } } diff --git a/src/main/java/seedu/address/ui/PersonCard.java b/src/main/java/seedu/address/ui/PersonCard.java index 094c42cda82..0b139397ee8 100644 --- a/src/main/java/seedu/address/ui/PersonCard.java +++ b/src/main/java/seedu/address/ui/PersonCard.java @@ -39,6 +39,8 @@ public class PersonCard extends UiPart { @FXML private Label email; @FXML + private Label animalName; + @FXML private FlowPane tags; /** @@ -52,8 +54,35 @@ public PersonCard(Person person, int displayedIndex) { phone.setText(person.getPhone().value); address.setText(person.getAddress().value); email.setText(person.getEmail().value); + + if (!person.getHousing().equals(null) && !"nil".equals(person.getHousing().value)) { + Label housingLabel = new Label(person.getHousing().value); + housingLabel.setStyle("-fx-background-color: #784c87;"); + tags.getChildren().add(housingLabel); + } + + if (!person.getAvailability().equals(null) && !"nil".equals(person.getAvailability().value)) { + Label availabilityLabel = new Label(person.getAvailability().value); + if ("Available".equals(person.getAvailability().value)) { + availabilityLabel.setStyle("-fx-background-color: #55874c;"); + } + if ("NotAvailable".equals(person.getAvailability().value)) { + availabilityLabel.setStyle("-fx-background-color: #874c53;"); + } + tags.getChildren().add(availabilityLabel); + } + + if (!person.getAnimalType().equals(null) && !"nil".equals(person.getAnimalType().value)) { + Label animalTypeLabel = new Label(person.getAnimalType().value); + animalTypeLabel.setStyle("-fx-background-color: #87854c"); + tags.getChildren().add(animalTypeLabel); + } + person.getTags().stream() .sorted(Comparator.comparing(tag -> tag.tagName)) .forEach(tag -> tags.getChildren().add(new Label(tag.tagName))); + + animalName.setText(!person.getAnimalName().equals(null) ? "Fostering: " + person.getAnimalName().fullName + : "Fostering: nil"); } } diff --git a/src/main/java/seedu/address/ui/PersonListPanel.java b/src/main/java/seedu/address/ui/PersonListPanel.java index f4c501a897b..61c5952be9c 100644 --- a/src/main/java/seedu/address/ui/PersonListPanel.java +++ b/src/main/java/seedu/address/ui/PersonListPanel.java @@ -27,6 +27,7 @@ public PersonListPanel(ObservableList personList) { super(FXML); personListView.setItems(personList); personListView.setCellFactory(listView -> new PersonListViewCell()); + personListView.setStyle("-fx-background-color: #f8f8f8ff"); } /** diff --git a/src/main/java/seedu/address/ui/PersonProfile.java b/src/main/java/seedu/address/ui/PersonProfile.java new file mode 100644 index 00000000000..dc4ef67da66 --- /dev/null +++ b/src/main/java/seedu/address/ui/PersonProfile.java @@ -0,0 +1,524 @@ +package seedu.address.ui; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.EnumMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.Predicate; + +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.Node; +import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; +import seedu.address.model.person.Address; +import seedu.address.model.person.AnimalType; +import seedu.address.model.person.Availability; +import seedu.address.model.person.Email; +import seedu.address.model.person.Housing; +import seedu.address.model.person.Name; +import seedu.address.model.person.Person; +import seedu.address.model.person.Phone; +import seedu.address.model.tag.Tag; + +/** + * An interactive UI component that contains all the fields associated with a {@link Person}, and their values. + * Can be instructed to start editing any field. Handles editing logic, including confirmation or cancellation actions + * by the user. At any point, a Person object can be retrieved. + */ +public class PersonProfile extends UiPart { + + // region Super Constants + private static final String FXML = "PersonProfile.fxml"; + // endregion + + // region String Constants + private static final String VALID_FOSTERER = "Valid fosterer."; + private static final String UNEXPECTED_ERROR = "Internal Error: fosterer was unexpectedly unable to be created."; + private static final String EDITING_IN_PROGRESS = "Some fields are still under edit. \n" + + "Please confirm or cancel them with enter or escape keys before proceeding."; + + private static final String INVALID_VALUE = "Invalid value for field: "; + private static final String FIELDS_ARE_INCOMPATIBLE = "Some fields are incompatible!"; + private static final String AVAILABLE_NIL_TYPE_MUST_NIL = "For 'nil' Availability, Animal Type must also be 'nil'."; + private static final String AVAILABLE_NIL_NAME_MUST_NIL = "For 'nil' Availability, Animal Name must also be 'nil'."; + private static final String AVAILABLE_NAME_MUST_NIL = "If fosterer is 'Available', Animal Name must be 'nil'."; + private static final String AVAILABLE_TYPE_MUST_ABLE_OR_NIL = + "If fosterer is 'Available', Animal Type must either be 'nil', or start with 'able'."; + private static final String NOT_AVAILABLE_NAME_TYPE_BOTH_SAME = + "If fosterer is 'NotAvailable', Animal Name and Type must either both be 'nil', or both not be 'nil'."; + private static final String FIELD_IS_MISSING = "Field is required to be not 'nil': "; + private static final String NOT_AVAILABLE_TYPE_NOT_CURRENT = + "If fosterer is 'NotAvailable', Animal Type must either be 'nil' or begin with 'current'."; + + // endregion + + // region FXML + @FXML private VBox vbox; + + // endregion + + // region Enums + + /** + * Represents possible times in UI execution flow where handlers/listeners can be added. + * @see #setEventHandler(Event, Runnable) + */ + public enum Event { + AFTER_CONFIRM, CANCEL, BEFORE_START_EDIT + } + + /** + * Represents a field of a Person as displayed in the UI. + */ + public enum Field { + NAME("Name", Name::isValidName, Name.MESSAGE_CONSTRAINTS), + PHONE("Phone", Phone::isValidPhone, Phone.MESSAGE_CONSTRAINTS), + EMAIL("Email", Email::isValidEmail, Email.MESSAGE_CONSTRAINTS), + ADDRESS("Address", Address::isValidAddress, Address.MESSAGE_CONSTRAINTS), + HOUSING("Housing", Housing::isValidHousing, Housing.MESSAGE_CONSTRAINTS), + AVAILABILITY("Availability", Availability::isValidAvailability, Availability.MESSAGE_CONSTRAINTS), + ANIMAL_NAME("Animal Name", Name::isValidName, Name.MESSAGE_CONSTRAINTS), + ANIMAL_TYPE("Animal Type", AnimalType::isValidAnimalType, AnimalType.MESSAGE_CONSTRAINTS); + + private final String name; + private final Predicate isValid; + private final String hint; + + Field(String name, Predicate isValid, String hint) { + this.name = name; + this.isValid = isValid; + this.hint = hint; + } + + public String getDisplayName() { + return name; + } + public boolean isValid(String string) { + return isValid.test(string); + } + public String getHint() { + return hint; + } + } + + // endregion + + // region Fields + + // region Final + private final MainWindow mainWindow; + private Person person; + private final Map fields = new EnumMap<>(Field.class); + private Set tags; + private String note; + + private final Map uiElements = new EnumMap<>(Field.class); + private PersonProfileTags tagUI; + private PersonProfileNote noteUI; + private final Map> eventHandlers = new EnumMap<>(Event.class); + // endregion + + // endregion + + // region Constructor + + /** + * Creates a UI showcasing details of the given {@link Person}. + */ + public PersonProfile(Person person, MainWindow mainWindow) { + super(FXML); + this.mainWindow = mainWindow; + this.person = person; + fields.put(Field.NAME, person.getName().toString()); + fields.put(Field.PHONE, person.getPhone().toString()); + fields.put(Field.EMAIL, person.getEmail().toString()); + fields.put(Field.ADDRESS, person.getAddress().toString()); + fields.put(Field.HOUSING, person.getHousing().toString()); + fields.put(Field.AVAILABILITY, person.getAvailability().toString()); + fields.put(Field.ANIMAL_NAME, person.getAnimalName().toString()); + fields.put(Field.ANIMAL_TYPE, person.getAnimalType().toString()); + tags = new HashSet<>(); + tags.addAll(person.getTags()); + note = person.getNote(); + initialize(); + } + + /** + * Creates a UI showcasing details of a new {@link Person}, starting from blanks. + */ + public PersonProfile(MainWindow mainWindow) { + super(FXML); + this.mainWindow = mainWindow; + this.person = null; + + Arrays.stream(Field.values()).forEach(field -> fields.put(field, null)); + tags = new HashSet<>(); + initialize(); + } + + private void initialize() { + //clear + ObservableList vboxChildren = vbox.getChildren(); + vboxChildren.clear(); + + //header + PersonProfileHeader header = new PersonProfileHeader(); + vboxChildren.add(header.getRoot()); + + //event handlers + Arrays.stream(Event.values()).forEach(event -> eventHandlers.put(event, new ArrayList<>())); + setEventHandler(Event.AFTER_CONFIRM, this::handleFieldLockIn); + setEventHandler(Event.CANCEL, this::handleFieldLockIn); + + //ui + Arrays.stream(Field.values()).forEach(field -> + uiElements.put(field, new PersonProfileField(this, field)) + ); + uiElements.values().stream() + .map(UiPart::getRoot) + .forEach(vboxChildren::add); + tagUI = new PersonProfileTags(this); + vboxChildren.add(tagUI.getRoot()); + noteUI = new PersonProfileNote(this); + vboxChildren.add(noteUI.getRoot()); + } + + // endregion + + // region Internal Event Handlers + + private void handleFieldLockIn() { + if (handleIfRequiredFieldsNil()) { + return; + } + + if (handleAvailabilityGroupInvalid()) { + return; + } + + if (editingInProgress()) { + sendEditingInProgress(); + return; + } + + try { + createAndUpdatePerson(); + sendPersonCreated(); + } catch (IllegalArgumentException ignored) { + sendUnexpectedError(); + } + } + + // endregion + + // region Internal Helpers + + private void createAndUpdatePerson() { + this.person = null; + + Name name = new Name(getNonNullOrNil(Field.NAME)); + Phone phone = new Phone(getNonNullOrNil(Field.PHONE)); + Email email = new Email(getNonNullOrNil(Field.EMAIL)); + Address address = new Address(getNonNullOrNil(Field.ADDRESS)); + Housing housing = new Housing(getNonNullOrNil(Field.HOUSING)); + Availability availability = new Availability(getNonNullOrNil(Field.AVAILABILITY)); + Name animalName = new Name(getNonNullOrNil(Field.ANIMAL_NAME)); + AnimalType animalType = new AnimalType(getNonNullOrNil(Field.ANIMAL_TYPE), availability); + + this.person = new Person(name, phone, email, address, housing, availability, animalName, animalType, this.tags); + person.setNote(note); + } + + private boolean handleAvailabilityGroupInvalid() { + Availability availability; + Name animalName; + + try { + availability = new Availability(getNonNullOrNil(Field.AVAILABILITY)); + animalName = new Name(getNonNullOrNil(Field.ANIMAL_NAME)); + } catch (Exception ignored) { + sendUnexpectedError(); + return true; + } + + if (!checkAvailabilityGroupValidElseFeedback(availability, animalName, getNonNullOrNil(Field.ANIMAL_TYPE))) { + return true; + } + + try { + new AnimalType(getNonNullOrNil(Field.ANIMAL_TYPE), availability); + } catch (Exception ignored) { + sendUnexpectedError(); + return true; + } + + return false; + } + + private boolean editingInProgress() { + return uiElements.values().stream().anyMatch(PersonProfileField::isEditing) + || tagUI.isEditing() || noteUI.isEditing(); + } + + private void sendFeedback(String string) { + mainWindow.sendFeedback(string); + } + + /** + * Checks if the availability, animal name, and animal type objects provided follow validity rules + * used in the Person constructor. + */ + private boolean checkAvailabilityGroupValidElseFeedback( + Availability availability, Name animalName, String animalType + ) throws IllegalArgumentException { + String avail = availability.value; + boolean isNameNil = Objects.equals(animalName.fullName, "nil"); + boolean isTypeNil = Objects.equals(animalType, "nil"); + switch (avail) { + case "nil": + if (!isTypeNil) { + sendConflict(AVAILABLE_NIL_TYPE_MUST_NIL, Field.AVAILABILITY, Field.ANIMAL_TYPE); + return false; + } else if (!isNameNil) { + sendConflict(AVAILABLE_NIL_NAME_MUST_NIL, Field.AVAILABILITY, Field.ANIMAL_NAME); + return false; + } else { + return true; + } + case "Available": + boolean isTypeAbleOrNil = isTypeNil || animalType.startsWith("able."); + if (!isNameNil) { + sendConflict(AVAILABLE_NAME_MUST_NIL, Field.AVAILABILITY, Field.ANIMAL_NAME); + return false; + } else if (!isTypeAbleOrNil) { + sendConflict(AVAILABLE_TYPE_MUST_ABLE_OR_NIL, Field.AVAILABILITY, Field.ANIMAL_TYPE); + return false; + } else { + return true; + } + case "NotAvailable": + boolean isTypeCurrent = animalType.startsWith("current."); + if (isNameNil != isTypeNil) { + sendConflict( + NOT_AVAILABLE_NAME_TYPE_BOTH_SAME, Field.AVAILABILITY, Field.ANIMAL_TYPE, Field.ANIMAL_NAME + ); + return false; + } else if (!isTypeNil && !isTypeCurrent) { + sendConflict( + NOT_AVAILABLE_TYPE_NOT_CURRENT, Field.AVAILABILITY, Field.ANIMAL_TYPE + ); + return false; + } else { + return true; + } + default: + sendUnexpectedError(); + return false; + } + } + + private boolean handleIfRequiredFieldsNil() { + Field[] requiredFields = {Field.NAME, Field.PHONE, Field.EMAIL, Field.ADDRESS}; + Optional nullOrNilField = Arrays.stream(requiredFields) + .filter(field -> isNullOrNil(getNonNullOrNil(field))) + .findFirst(); + if (nullOrNilField.isPresent()) { + sendMissing(nullOrNilField.get()); + return true; + } + return false; + } + + private boolean isNullOrNil(String string) { + return string == null || string.equals("nil"); + } + + private String getNonNullOrNil(Field field) { + String value = fields.get(field); + if (value == null) { + return "nil"; + } + return value; + } + + // endregion + + // region User Feedback + + private void sendEditingInProgress() { + sendFeedback(EDITING_IN_PROGRESS); + } + + private void sendPersonCreated() { + sendFeedback(VALID_FOSTERER); + } + + private void sendUnexpectedError() { + sendFeedback(UNEXPECTED_ERROR); + } + + private void sendConflict(String conflictMessage, Field... fields) { + sendFeedback(FIELDS_ARE_INCOMPATIBLE + "\n" + conflictMessage); + Objects.requireNonNull(fields); + Arrays.stream(fields).map(uiElements::get).forEach(PersonProfileField::indicateIsError); + } + + private void sendMissing(Field field) { + sendFeedback(FIELD_IS_MISSING + field.getDisplayName()); + uiElements.get(field).indicateIsError(); + } + + // endregion + + // region Package + + void updateField(Field field, String value) { + fields.put(field, value); + } + + void updateTags(Set tags) { + this.tags = tags; + } + + void updateNote(String note) { + this.note = note; + } + + void triggerEvent(Event event) { + List runnables = eventHandlers.get(event); + runnables.forEach(Runnable::run); + } + + void sendHint(Field field) { + sendFeedback(field.getHint()); + } + + void sendHint(String hint) { + sendFeedback(hint); + } + + String getValueOfField(Field field) { + return getNonNullOrNil(field); + } + + Set getTags() { + return tags; + } + + String getNote() { + return note; + } + + void sendInvalidInput(Field field) { + sendFeedback(INVALID_VALUE + field.getDisplayName() + "\n" + field.getHint()); + } + + void sendInvalidInput(String fieldName, String message) { + sendFeedback(INVALID_VALUE + fieldName + "\n" + message); + } + + // endregion + + // region External + + /** + * Sets focus to the UI element responsible for editing the provided field. + * + * @param field the field of Person that should currently be under edit. + */ + public void setFocus(Field field) { + uiElements.get(field).setFocus(); + } + + /** + * Sets focus to the UI element responsible for editing tags. + */ + public void setFocusTags() { + tagUI.setFocus(); + } + + /** + * Sets focus to the UI element responsible for editing notes. + */ + public void setFocusNotes() { + noteUI.setFocus(); + } + + /** + * Gets the last valid Person object. + * + * @return valid Person object, or null if unavailable. + */ + public Person getPerson() { + return person; + } + + /** + * Sets a Runnable to run when a specific {@link Event} occurs. + * + * @param event trigger to run the handler. + * @param handler Runnable to run when the event occurs. + */ + public void setEventHandler(Event event, Runnable handler) { + eventHandlers.get(event).add(handler); + } + + public boolean isStillEditing() { + return editingInProgress() || person == null; + } + + /** + * Sets a Boolean that represents whether the profile page is in save confirmation dialog + * by mapping through each PersonProfileField and setting their inConfirmationDialog boolean values. + * @param isInConfirmationDialog is a boolean the tells whether the current window is is showing confirmation dialog + */ + public void setIsInConfirmationDialog(boolean isInConfirmationDialog) { + uiElements.values().stream() + .forEach(field -> field.setIsInConfirmationDialog(isInConfirmationDialog)); + tagUI.setIsInConfirmationDialog(isInConfirmationDialog); + noteUI.setIsInConfirmationDialog(isInConfirmationDialog); + } + + /** + * Resets the field values back to the most recently edited valid details + * and change the red colored text, if there is any, back to black colored text. + */ + public void resetValues() { + if (person == null) { + return; + } + + fields.keySet().forEach(field -> { + if (field == Field.NAME) { + updateField(field, person.getName().toString()); + } else if (field == Field.PHONE) { + updateField(field, person.getPhone().toString()); + } else if (field == Field.EMAIL) { + updateField(field, person.getEmail().toString()); + } else if (field == Field.ADDRESS) { + updateField(field, person.getAddress().toString()); + } else if (field == Field.HOUSING) { + updateField(field, person.getHousing().toString()); + } else if (field == Field.AVAILABILITY) { + updateField(field, person.getAvailability().toString()); + } else if (field == Field.ANIMAL_NAME) { + updateField(field, person.getAnimalName().toString()); + } else if (field == Field.ANIMAL_TYPE) { + updateField(field, person.getAnimalType().toString()); + } + }); + + uiElements.values().stream() + .forEach(field -> { + field.refresh(); + }); + } + + // endregion +} diff --git a/src/main/java/seedu/address/ui/PersonProfileField.java b/src/main/java/seedu/address/ui/PersonProfileField.java new file mode 100644 index 00000000000..5b297a4aaf8 --- /dev/null +++ b/src/main/java/seedu/address/ui/PersonProfileField.java @@ -0,0 +1,225 @@ +package seedu.address.ui; + +import javafx.application.Platform; +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.control.SplitPane; +import javafx.scene.control.TextField; +import javafx.scene.input.KeyEvent; +import seedu.address.model.person.Person; + +/** + * A row of the PersonProfile UI, representing one field of the Person displayed. + */ +public class PersonProfileField extends UiPart { + + // region Super + private static final String FXML = "PersonProfileField.fxml"; + // endregion + + // region Constants + private static final String errorTextCss = "-fx-text-fill: red"; + // endregion + + // region FXML + @FXML private Label valueLabel; + @FXML private TextField valueField; + @FXML private Label keyLabel; + // endregion + + // region Fields + private final PersonProfile personProfile; + private final PersonProfile.Field field; + + private String value; + private State state; + + private Boolean isInConfirmationDialog = false; + + // endregion + + // region Enums + + /** + * Represents that the Field is either under edit, or not. + */ + enum State { + READ_ONLY, EDITING, LOCKED + } + + // endregion + + // region Constructor + + /** + * Creates a PersonProfileField from the provided UI parent, as well as a Field value. + * + * @param personProfile UI parent, serving as a container for this object. + * @param field field of a Person to display and allow editing of. + */ + public PersonProfileField(PersonProfile personProfile, PersonProfile.Field field) { + super(FXML); + this.personProfile = personProfile; + this.field = field; + initializeAndRefresh(); + } + + private void initializeAndRefresh() { + keyLabel.setText(field.getDisplayName()); + valueField.focusedProperty().addListener(((observable, oldValue, newValue) -> { + if (oldValue) { + handleLoseFocus(); + } + })); + state = State.READ_ONLY; + personProfile.setEventHandler(PersonProfile.Event.AFTER_CONFIRM, this::refresh); + refresh(); + } + // endregion + + // region Internal Actions + private void updateState(State state) { + this.state = state; + updateState(); + } + + private void updateState() { + switch (state) { + case READ_ONLY: + valueLabel.setText(value); + valueField.setText(value); + valueLabel.setVisible(true); + valueField.setVisible(false); + break; + case EDITING: + valueLabel.setVisible(false); + valueField.setVisible(true); + valueField.requestFocus(); + personProfile.sendHint(field); + break; + default: + initializeAndRefresh(); + } + } + + private boolean confirmIfValid() { + assert state == State.EDITING; + if (isValueValid()) { + this.value = getTextOrNil(); + updateProfile(); + updateState(State.READ_ONLY); + personProfile.triggerEvent(PersonProfile.Event.AFTER_CONFIRM); + return true; + } else { + sendValueInvalid(); + return false; + } + } + + private boolean isValueValid() { + return field.isValid(getTextOrNil()); + } + + private void cancel() { + updateState(State.READ_ONLY); + updateProfile(); + personProfile.triggerEvent(PersonProfile.Event.CANCEL); + } + + private String getTextOrNil() { + String newValue = valueField.getText(); + if (newValue == null || newValue.isBlank()) { + newValue = Person.NIL_WORD; + } else { + newValue = newValue.trim(); + } + return newValue; + } + + private void updateProfile() { + personProfile.updateField(field, value); + } + + private void resetTextPaint() { + keyLabel.setStyle(""); + valueLabel.setStyle(""); + } + + private void setErrorTextPaint() { + Platform.runLater(() -> { + keyLabel.setStyle(errorTextCss); + valueLabel.setStyle(errorTextCss); + }); + } + + private void sendValueInvalid() { + personProfile.sendInvalidInput(field); + } + + // endregion + + // region Event Handlers + + @FXML + private void handleKey(KeyEvent keyEvent) { + if (!isEditing()) { + return; + } + switch (keyEvent.getCode()) { + case ENTER: + if (confirmIfValid()) { + return; + } + break; + case ESCAPE: + cancel(); + break; + default: + //noinspection UnnecessarySemicolon + ; + } + } + + private void handleLoseFocus() { + if (!isEditing()) { + return; + } + if (confirmIfValid()) { + return; + } + cancel(); + } + + // endregion + + // region External + + @FXML + void setFocus() { + if (!isInConfirmationDialog) { + personProfile.triggerEvent(PersonProfile.Event.BEFORE_START_EDIT); + updateState(State.EDITING); + } + } + + boolean isEditing() { + return state == State.EDITING; + } + + void indicateIsError() { + setErrorTextPaint(); + } + + void refresh() { + value = personProfile.getValueOfField(field); + resetTextPaint(); + updateState(); + } + + public void setIsInConfirmationDialog(boolean isInConfirmationDialog) { + this.isInConfirmationDialog = isInConfirmationDialog; + } + + // endregion + +} diff --git a/src/main/java/seedu/address/ui/PersonProfileHeader.java b/src/main/java/seedu/address/ui/PersonProfileHeader.java new file mode 100644 index 00000000000..a44129ecdd0 --- /dev/null +++ b/src/main/java/seedu/address/ui/PersonProfileHeader.java @@ -0,0 +1,52 @@ +package seedu.address.ui; + +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.control.SplitPane; +import javafx.scene.control.TextField; + +/** + * The top row of the PersonProfile UI, representing the header of the UI. + */ +public class PersonProfileHeader extends UiPart { + + // region Constants + private static final String FXML = "PersonProfileField.fxml"; + private static final String KEY_HEADER = "Field"; + private static final String VALUE_HEADER = "Value"; + private static final String CSS_FONT_SIZE = "-fx-font-size: 20px;"; + // endregion + + @FXML private Label valueLabel; + @FXML private TextField valueField; + @FXML private Label keyLabel; + + /** + * Constructor for PersonProfileHeader. + */ + public PersonProfileHeader() { + super(FXML); + initialize(); + } + + private void initialize() { + keyLabel.setText(KEY_HEADER); + valueLabel.setText(VALUE_HEADER); + valueField.setVisible(false); + + keyLabel.setStyle(CSS_FONT_SIZE); + valueLabel.setStyle(CSS_FONT_SIZE); + } + + @FXML + private void handleKey() { + //do nothing + } + + @FXML + private void setFocus() { + //do nothing + } + + +} diff --git a/src/main/java/seedu/address/ui/PersonProfileNote.java b/src/main/java/seedu/address/ui/PersonProfileNote.java new file mode 100644 index 00000000000..4511b00e33d --- /dev/null +++ b/src/main/java/seedu/address/ui/PersonProfileNote.java @@ -0,0 +1,205 @@ +package seedu.address.ui; + +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.control.SplitPane; +import javafx.scene.control.TextArea; +import javafx.scene.input.KeyEvent; + +/** + * A row of the PersonProfile UI, representing one field of the Person displayed. + */ +public class PersonProfileNote extends UiPart { + + // region Super + private static final String FXML = "PersonProfileNote.fxml"; + // endregion + + // region Constants + private static final String KEY_NAME = "Notes"; + private static final String NOTES_HINT = "Notes can be any text."; + // endregion + + // region FXML + @FXML private Label valueLabel; + @FXML private TextArea valueField; + @FXML private Label keyLabel; + // endregion + + // region Fields + private final PersonProfile personProfile; + private String value; + private State state; + private boolean isInConfirmationDialog; + + // endregion + + // region Enums + + /** + * Represents that the Field is either under edit, or not. + */ + enum State { + READ_ONLY, EDITING + } + + // endregion + + // region Constructor + + /** + * Creates a PersonProfileField from the provided UI parent, as well as a Field value. + * + * @param personProfile UI parent, serving as a container for this object. + */ + public PersonProfileNote(PersonProfile personProfile) { + super(FXML); + this.personProfile = personProfile; + initializeAndRefresh(); + } + + private void initializeAndRefresh() { + keyLabel.setText(KEY_NAME); + valueField.focusedProperty().addListener(((observable, oldValue, newValue) -> { + if (oldValue) { + handleLoseFocus(); + } + })); + valueField.addEventFilter(KeyEvent.KEY_PRESSED, this::handleKey); + state = State.READ_ONLY; + personProfile.setEventHandler(PersonProfile.Event.AFTER_CONFIRM, this::refresh); + refresh(); + } + // endregion + + // region Internal Actions + private void updateState(State state) { + this.state = state; + updateState(); + } + + private void updateState() { + switch (state) { + case READ_ONLY: + valueLabel.setText(value); + valueField.setText(value); + valueLabel.setVisible(true); + valueField.setVisible(false); + break; + case EDITING: + valueLabel.setVisible(false); + valueField.setVisible(true); + valueField.requestFocus(); + sendHint(); + break; + default: + initializeAndRefresh(); + } + } + + private boolean confirm() { + assert state == State.EDITING; + retrieveValue(); + updateProfile(); + updateState(State.READ_ONLY); + personProfile.triggerEvent(PersonProfile.Event.AFTER_CONFIRM); + return true; + } + + private void cancel() { + updateState(State.READ_ONLY); + updateProfile(); + personProfile.triggerEvent(PersonProfile.Event.CANCEL); + } + + private String getNonNull() { + String text = valueField.getText(); + if (text == null) { + text = ""; + } + return text; + } + + private void updateProfile() { + personProfile.updateNote(value); + } + + private void refresh() { + value = personProfile.getNote(); + updateState(); + } + + private void sendHint() { + personProfile.sendHint(NOTES_HINT); + } + + private void retrieveValue() { + this.value = getNonNull().trim(); + } + + // endregion + + // region Event Handlers + + private void handleKey(KeyEvent keyEvent) { + if (!isEditing()) { + return; + } + switch (keyEvent.getCode()) { + case ENTER: + keyEvent.consume(); + if (keyEvent.isShiftDown()) { + int cursor = valueField.getCaretPosition(); + String text = getNonNull(); + String before = text.substring(0, cursor); + String after = text.substring(cursor); + valueField.setText(before + "\n" + after); + valueField.positionCaret(cursor + 1); + return; + } + if (confirm()) { + return; + } + break; + case ESCAPE: + cancel(); + break; + default: + //noinspection UnnecessarySemicolon + ; + } + } + + private void handleLoseFocus() { + if (!isEditing()) { + return; + } + if (confirm()) { + return; + } + cancel(); + } + + // endregion + + // region External + + @FXML + void setFocus() { + if (!isInConfirmationDialog) { + personProfile.triggerEvent(PersonProfile.Event.BEFORE_START_EDIT); + updateState(State.EDITING); + } + } + + boolean isEditing() { + return state == State.EDITING; + } + + public void setIsInConfirmationDialog(boolean isInConfirmationDialog) { + this.isInConfirmationDialog = isInConfirmationDialog; + } + + // endregion + +} diff --git a/src/main/java/seedu/address/ui/PersonProfileTags.java b/src/main/java/seedu/address/ui/PersonProfileTags.java new file mode 100644 index 00000000000..f7c06da4908 --- /dev/null +++ b/src/main/java/seedu/address/ui/PersonProfileTags.java @@ -0,0 +1,248 @@ +package seedu.address.ui; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.control.SplitPane; +import javafx.scene.control.TextArea; +import javafx.scene.input.KeyEvent; +import javafx.scene.layout.FlowPane; +import seedu.address.model.tag.Tag; + +/** + * A row of the PersonProfile UI, representing one field of the Person displayed. + */ +public class PersonProfileTags extends UiPart { + + // region Super + private static final String FXML = "PersonProfileTags.fxml"; + // endregion + + // region Constants + private static final String KEY_NAME = "Tags"; + private static final String TAG_SEPARATOR_REGEX = "[ ,;\n]"; + private static final String DEFAULT_TAG_SEPARATOR = "\n"; + private static final String TAGS_HINT = Tag.MESSAGE_CONSTRAINTS + "\n" + + "Tag names can be separated by spaces, commas, semicolons or new lines."; + private static final String TAG_INVALID_MESSAGE = "is not a valid tag."; + // endregion + + // region FXML + @FXML private FlowPane valueFlowPane; + @FXML private TextArea valueField; + @FXML private Label keyLabel; + // endregion + + // region Fields + private final PersonProfile personProfile; + private Set tags; + private State state; + private boolean isInConfirmationDialog; + // endregion + + // region Enums + + /** + * Represents that the Field is either under edit, or not. + */ + enum State { + READ_ONLY, EDITING + } + + // endregion + + // region Constructor + + /** + * Creates a PersonProfileField from the provided UI parent, as well as a Field value. + * + * @param personProfile UI parent, serving as a container for this object. + */ + public PersonProfileTags(PersonProfile personProfile) { + super(FXML); + this.personProfile = personProfile; + initializeAndRefresh(); + } + + private void initializeAndRefresh() { + keyLabel.setText(KEY_NAME); + valueField.focusedProperty().addListener(((observable, oldValue, newValue) -> { + if (oldValue) { + handleLoseFocus(); + } + })); + valueField.addEventFilter(KeyEvent.KEY_PRESSED, this::handleKey); + state = State.READ_ONLY; + personProfile.setEventHandler(PersonProfile.Event.AFTER_CONFIRM, this::refresh); + refresh(); + } + // endregion + + // region Internal Actions + private void updateState(State state) { + this.state = state; + updateState(); + } + + private void updateState() { + switch (state) { + case READ_ONLY: + generateAndSetLabels(); + valueFlowPane.setVisible(true); + valueField.setVisible(false); + break; + case EDITING: + valueFlowPane.setVisible(false); + valueField.setVisible(true); + valueField.requestFocus(); + sendHint(); + break; + default: + initializeAndRefresh(); + } + } + + private void generateAndSetLabels() { + List

+ + + + + + + + + + - + + + + + + + + - + - + - + - + - + - + + - + - + diff --git a/src/main/resources/view/PersonListCard.fxml b/src/main/resources/view/PersonListCard.fxml index f5e812e25e6..8dac22407e4 100644 --- a/src/main/resources/view/PersonListCard.fxml +++ b/src/main/resources/view/PersonListCard.fxml @@ -28,6 +28,7 @@