diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 6ff220b5196..0112fb2017a 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -39,7 +39,6 @@ jobs: run: ./gradlew check coverage - name: Upload coverage reports to Codecov - if: runner.os == 'Linux' uses: codecov/codecov-action@v3 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.gitignore b/.gitignore index 284c4ca7cd9..27abea02134 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,4 @@ src/test/data/sandbox/ # MacOS custom attributes files created by Finder .DS_Store docs/_site/ +.vscode/settings.json diff --git a/README.md b/README.md index 13f5c77403f..720f93e98de 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,23 @@ -[![CI Status](https://github.com/se-edu/addressbook-level3/workflows/Java%20CI/badge.svg)](https://github.com/se-edu/addressbook-level3/actions) +[![Java CI](https://github.com/AY2324S1-CS2103T-F12-1/tp/actions/workflows/gradle.yml/badge.svg)](https://github.com/AY2324S1-CS2103T-F12-1/tp/actions/workflows/gradle.yml) ![Ui](docs/images/Ui.png) -* This is **a sample project for Software Engineering (SE) students**.
- Example usages: - * as a starting point of a course project (as opposed to writing everything from scratch) - * as a case study -* The project simulates an ongoing software project for a desktop application (called _AddressBook_) used for managing contact details. - * It is **written in OOP fashion**. It provides a **reasonably well-written** code base **bigger** (around 6 KLoC) than what students usually write in beginner-level SE modules, without being overwhelmingly big. - * It comes with a **reasonable level of user and developer documentation**. -* It is named `AddressBook Level 3` (`AB3` for short) because it was initially created as a part of a series of `AddressBook` projects (`Level 1`, `Level 2`, `Level 3` ...). -* For the detailed documentation of this project, see the **[Address Book Product Website](https://se-education.org/addressbook-level3)**. -* This project is a **part of the se-education.org** initiative. If you would like to contribute code to this project, see [se-education.org](https://se-education.org#https://se-education.org/#contributing) for more info. +### About UNOFAS + +This project was created under the CS2103T Software Engineering module in NUS. + +UNOFAS is a desktop app for Financial Advisors (FA) to manage client’s contacts, optimized for use via a Command Line +Interface (CLI) while still having the benefits of a Graphical User Interface (GUI). The app also includes features +such as sorting, scheduling and other commands to query information quickly required by the FA. + +### Other Links +* UNOFAS [Main Website](https://ay2324s1-cs2103t-f12-1.github.io/tp/) +* UNOFAS [User Guide Website](https://ay2324s1-cs2103t-f12-1.github.io/tp/UserGuide.html) +* UNOFAS [Developer Guide Website](https://ay2324s1-cs2103t-f12-1.github.io/tp/DeveloperGuide.html) +* UNOFAS [About Us Website](https://ay2324s1-cs2103t-f12-1.github.io/tp/AboutUs.html) +* UNOFAS [Github Website](https://github.com/AY2324S1-CS2103T-F12-1/tp) + + +### Acknowledgements + +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..3f32367fbe0 100644 --- a/build.gradle +++ b/build.gradle @@ -66,7 +66,11 @@ dependencies { } shadowJar { - archiveFileName = 'addressbook.jar' + archiveFileName = 'unofas.jar' +} + +run { + enableAssertions = true; } defaultTasks 'clean', 'test' diff --git a/docs/AboutUs.md b/docs/AboutUs.md index 1c9514e966a..ebbbbffa965 100644 --- a/docs/AboutUs.md +++ b/docs/AboutUs.md @@ -9,51 +9,51 @@ You can reach us at the email `seer[at]comp.nus.edu.sg` ## Project team -### John Doe +### Aaron Tay Kai Boon - + -[[homepage](http://www.comp.nus.edu.sg/~damithch)] -[[github](https://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](https://github.com/Kb-tay)] +[[portfolio](team/kb-tay.md)] -* Role: Project Advisor +* Role: Developer +* Responsibilities: Testing + Debugging -### Jane Doe +### Nicholas Chia - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](http://github.com/nikele2001)] +[[portfolio](team/nikele2001.md)] -* Role: Team Lead -* Responsibilities: UI +* Role: Developer +* Responsibilities: Documentation + UI -### Johnny Doe +### Poon Yip Hang, Ryan - + -[[github](http://github.com/johndoe)] [[portfolio](team/johndoe.md)] +[[github](http://github.com/sopa301)] [[portfolio](team/sopa301.md)] -* Role: Developer -* Responsibilities: Data +* Role: Team Lead +* Responsibilities: Issue tracking + Documentation -### Jean Doe +### Low Jun Yu - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](http://github.com/jylow)] +[[portfolio](team/jylow.md)] * Role: Developer -* Responsibilities: Dev Ops + Threading +* Responsibilities: Quality Assurance + Testing -### James Doe +### Alyssa Png Kai Wen - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](https://github.com/AlyssaPng)] +[[portfolio](team/alyssapng.md)] * Role: Developer -* Responsibilities: UI +* Responsibilities: UI + Documentation diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 8a861859bfd..94c32f1bae7 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -9,7 +9,7 @@ title: Developer Guide ## **Acknowledgements** -* {list here sources of all reused/adapted ideas, code, documentation, and third-party libraries -- include links to the original source as well} +* This project is based on the AddressBook-Level3 project created by the [SE-EDU initiative](https://se-education.org). -------------------------------------------------------------------------------------------------------------------- @@ -23,7 +23,9 @@ Refer to the guide [_Setting up and getting started_](SettingUp.md).
-: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 +38,10 @@ 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-F12-1/tp/blob/master/src/main/java/seedu/address/Main.java) +and [`MainApp`](https://github.com/AY2324S1-CS2103T-F12-1/tp/blob/master/src/main/java/seedu/address/MainApp.java)) +is in charge of the app launch and shut down. * At app launch, it initializes the other components in the correct sequence, and connects them up with each other. * At shut down, it shuts down the other components and invokes cleanup methods where necessary. @@ -51,16 +56,21 @@ The bulk of the app's work is done by the following four components: **How the architecture components interact with each other** -The *Sequence Diagram* below shows how the components interact with each other for the scenario where the user issues the command `delete 1`. +The *Sequence Diagram* below shows how the components interact with each other for the scenario where the user issues +the command `delete 1`. Each of the four main components (also shown in the diagram above), * defines its *API* in an `interface` with the same name as the Component. -* implements its functionality using a concrete `{Component Name}Manager` class (which follows the corresponding API `interface` mentioned in the previous point. +* implements its functionality using a concrete `{Component Name}Manager` class which follows the corresponding API +`interface` mentioned in the previous point. -For example, the `Logic` component defines its API in the `Logic.java` interface and implements its functionality using the `LogicManager.java` class which follows the `Logic` interface. Other components interact with a given component through its interface rather than the concrete class (reason: to prevent outside component's being coupled to the implementation of a component), as illustrated in the (partial) class diagram below. +For example, the `Logic` component defines its API in the `Logic.java` interface and implements its functionality +using the `LogicManager.java` class which follows the `Logic` interface. Other components interact with a given +component through its interface rather than the concrete class (reason: to prevent outside component's being coupled +to the implementation of a component), as illustrated in the (partial) class diagram below. @@ -68,53 +78,71 @@ 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-F12-1/tp/blob/master/src/main/java/seedu/address/ui/Ui.java) ![Structure of the UI Component](images/UiClassDiagram.png) -The UI consists of a `MainWindow` that is made up of parts e.g.`CommandBox`, `ResultDisplay`, `PersonListPanel`, `StatusBarFooter` etc. All these, including the `MainWindow`, inherit from the abstract `UiPart` class which captures the commonalities between classes that represent parts of the visible GUI. +The UI consists of a `MainWindow` that is made up of parts e.g.`CommandBox`, `ResultDisplay`, `PersonListPanel`, +`StatusBarFooter` etc. All these, including the `MainWindow`, inherit from the abstract `UiPart` class which captures +the commonalities between classes that represent parts of the visible GUI. -The `UI` component uses the JavaFx UI framework. The layout of these UI parts are defined in matching `.fxml` files that are in the `src/main/resources/view` folder. For example, the layout of the [`MainWindow`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/ui/MainWindow.java) is specified in [`MainWindow.fxml`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/resources/view/MainWindow.fxml) +The `UI` component uses the JavaFx UI framework. The layout of these UI parts are defined in matching `.fxml` files +that are in the `src/main/resources/view` folder. For example, the layout of the +[`MainWindow`](https://github.com/AY2324S1-CS2103T-F12-1/tp/blob/master/src/main/java/seedu/address/ui/MainWindow.java) +is specified in +[`MainWindow.fxml`](https://github.com/AY2324S1-CS2103T-F12-1/tp/blob/master/src/main/resources/view/MainWindow.fxml). The `UI` component, * executes user commands using the `Logic` component. * listens for changes to `Model` data so that the UI can be updated with the modified data. * keeps a reference to the `Logic` component, because the `UI` relies on the `Logic` to execute commands. -* depends on some classes in the `Model` component, as it displays `Person` object residing in the `Model`. +* depends on some classes in the `Model` component, as it displays `Person` and `Appointment` objects residing in the +`Model`. ### Logic component -**API** : [`Logic.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/logic/Logic.java) +**API** : +[`Logic.java`](https://github.com/AY2324S1-CS2103T-F12-1/tp/blob/master/src/main/java/seedu/address/logic/Logic.java) Here's a (partial) class diagram of the `Logic` component: -The sequence diagram below illustrates the interactions within the `Logic` component, taking `execute("delete 1")` API call as an example. +The sequence diagram below illustrates the interactions within the `Logic` component, taking `execute("delete 1")` API +call as an example. ![Interactions Inside the Logic Component for the `delete 1` Command](images/DeleteSequenceDiagram.png) -
: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. +
: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: -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`. +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. +2. This results in a `Command` object (more precisely, an object of one of its subclasses e.g., `DeleteCommand`) which +is executed by the `LogicManager`. +3. The command can communicate with the `Model` when it is executed (e.g. to delete a person). +4. 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: -* 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. +* When called upon to parse a user command, the `AddressBookParser` class creates an `XYZCommandParser` (`XYZ` is a +placeholder for the specific command name e.g., `AddCommandParser`) which uses the other classes shown above to parse +the user command and create a `XYZCommand` object (e.g., `AddCommand`) which the `AddressBookParser` returns back as a +`Command` object. +* All `XYZCommandParser` classes (e.g., `AddCommandParser`, `DeleteCommandParser`, ...) inherit from the `Parser` +interface so that they can be treated similarly where possible e.g, during testing. ### Model component -**API** : [`Model.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/model/Model.java) +**API** : +[`Model.java`](https://github.com/AY2324S1-CS2103T-F12-1/tp/blob/master/src/main/java/seedu/address/model/Model.java) @@ -122,27 +150,32 @@ How the parsing works: The `Model` component, * stores the address book data i.e., all `Person` objects (which are contained in a `UniquePersonList` object). -* stores the currently 'selected' `Person` objects (e.g., results of a search query) as a separate _filtered_ list which is exposed to outsiders as an unmodifiable `ObservableList` that can be 'observed' e.g. the UI can be bound to this list so that the UI automatically updates when the data in the list change. -* 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.
- - - -
- +* stores the currently 'selected' `Person` objects (e.g., results of a search query) as a separate _filtered_ list +which is exposed to outsiders as an unmodifiable `ObservableList` that can be 'observed' e.g. the UI can be +bound to this list so that the UI automatically updates when the data in the list change. +* stores a `UserPref` object that represents the user’s preferences. This is exposed to the outside as a +`ReadOnlyUserPref` objects. +* stores an `observableAppointments` object that represents existing appointments in the address book, sorted in a +chronological order. +* stores a `sortedAppointments` object that represents existing appointments in the address book. +* `observableAppointments` and `sortedAppointments` depend on `filteredPersons`. Hence, appointments listed are for +`Person` objects in `filteredPersons`. +* 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). ### 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-F12-1/tp/blob/master/src/main/java/seedu/address/storage/Storage.java) The `Storage` component, * can save both address book data and user preference data in JSON format, and read them back into corresponding objects. -* inherits from both `AddressBookStorage` and `UserPrefStorage`, which means it can be treated as either one (if only the functionality of only one is needed). -* depends on some classes in the `Model` component (because the `Storage` component's job is to save/retrieve objects that belong to the `Model`) +* inherits from both `AddressBookStorage` and `UserPrefStorage`, which means it can be treated as either one (if only +the functionality of only one is needed). +* depends on some classes in the `Model` component (because the `Storage` component's job is to save/retrieve objects +that belong to the `Model`). ### Common classes @@ -154,90 +187,326 @@ Classes used by multiple components are in the `seedu.addressbook.commons` packa This section describes some noteworthy details on how certain features are implemented. -### \[Proposed\] Undo/redo feature +### Overview of How Commands Work -#### Proposed Implementation +The basic idea of what happens when a user types a command: +1. The LogicManager executes method is called and takes in the user's input. +2. The user's input is then parsed by `AddressBookParser`, which then creates the respective `XYZCommandParser`. +3. `XYZCommandParser` parses the additional arguments provided by the user and creates and `XYZCommand`. +4. `XYZCommand` then communicates with ModelManager to execute and returns a `CommandResult` which is displayed to the user. -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: -* `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. +The flow of how a `Command` is executed is illustrated with the `Schedule` Command below. +### Schedule Command -These operations are exposed in the `Model` interface as `Model#commitAddressBook()`, `Model#undoAddressBook()` and `Model#redoAddressBook()` respectively. +#### Implementation Overview +After the `AddressBookParser` identifies that the user's input has a schedule command word, it creates a +`ScheduleCommandParser`. The `ScheduleCommandParser` then parses the users input and creates a new `ScheduleCommand` +containing an `Appointment` and an `Index`. The `ScheduleCommand` is then executed by `Logic Manager`, which updates +the `Person` in `Model` to have the created `Appointment`. A `CommandResult` which stores the message of the outcome +of schedule command is then returned. The partial class diagram is shown below. -Given below is an example usage scenario and how the undo/redo mechanism behaves at each step. + -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. -![UndoRedoState0](images/UndoRedoState0.png) +In the event the `Person` already has an existing appointment, a different instance of `CommandResult` is instantiated. This +instance invokes the constructor that contains the `Person` as well as the new proposed `Appointment`. The returned +`CommandResult` instead results in the UI being notified that while the `ScheduleCommand` has been executed, the user +should be prompted to confirm this change on the `OverrideWindow` of the UI. Only after confirmation of this is the +overriding of the appointment completed using the appointment and person stored in the `CommandResult`. In the event of +cancelling the override, the program resumes its functionality, effectively discarding the execution of the rest of the +`scheduleCommand` -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. +The following activity diagram summarises what happens the user executes a schedule command. -![UndoRedoState1](images/UndoRedoState1.png) + -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`. +#### Design Considerations -![UndoRedoState2](images/UndoRedoState2.png) +**Aspect: How to implement appointment for Person** -
: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`. +Alternative 1 (Current Choice): Create an abstract class ScheduleItem and make it a compulsory field for Person. -
+The diagram below illustrates our current implementation. A `Person` has is associated with 1 `ScheduleItem`, which can be a `NullAppointment`(empty appointment) or `Appointment`. -Step 4. The user now decides that adding the person was a mistake, and decides to undo that action by executing the `undo` command. The `undo` command will call `Model#undoAddressBook()`, which will shift the `currentStatePointer` once to the left, pointing it to the previous address book state, and restores the address book to that state. + -![UndoRedoState3](images/UndoRedoState3.png) +- Pros: + * This ensures a 1-to-1 relationship between Person and Appointment, making implementation of other functions like + sort easier. This also prevents clutter of appointments in the UI. + * This makes use of a **facade** design pattern, where `NullAppointment` and `Appointment` will handle themselves + without the `Person` knowing. -
: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. +- Cons: + * This makes the scheduling of Appointments more inflexible, as the FA is unable to schedule multiple appointments + with the same person. -
+- Other considerations: + * `NullAppointment` is a Singleton class to prevent multiple instances of it being created, making it more efficient for memory. -The following sequence diagram shows how the undo operation works: +Alternative 2: Create a hashset of Appointments for each Person. +- Pros: + * More flexible, user can now schedule multiple appointment for a Person. -![UndoSequenceDiagram](images/UndoSequenceDiagram.png) +- Cons: + * Harder to implement operations such as editing of an appointment for a client. An additional step of finding the + specified appointment within the hashset is required, which may potentially introduce more bugs. + * Harder to implement default behaviours for when person has no appointment. -
: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. -
+**Aspect: How to implement override prompt** + +Alternative 1(current solution): Create a separate constructor in CommandResult to handle overriding. +- Pros: + * Easy to implement. + * This "freezes" functionality of the program to force user to acknowledge or cancel the execution of the command. +- Cons: + * This creates multiple different constructors within the CommandResult. + +Alternative 2: Abstract CommandResult to get a successfulExecutionResult and a PausedExecutionResult. +- Pros: + * Improves code readability and reduces coupling in code. +- Cons: + * Time-consuming to refactor code + * Improper implementation could result in breaking of coding principles. +- Note: + * With additional time, alternative 2 can be implemented by refactoring the code to create multiple subclasses. Be wary of the + Liskov Substitution Principle (LSP) when doing so. The earlier alternative 2 is implemented, the better to reduce amount of code + that needs to be refactored. + * The above implementation can be done in conjunction with the clear command prompt to reduce code coupling. + +### Complete Feature + +#### Implementation Overview + +The **Complete** feature is facilitated by the `CompleteCommand` and `CompleteCommandParser`. The +`CompleteCommandParser` creates a `CompleteByIndex` or `CompleteByDate` object depending on the user's input. Both `CompleteByIndex` and `CompleteByDate` extends `CompleteCommand` as illustrated in the class diagram below. -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. + -
: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. +The following sequence diagram illustrates how the complete operation is executed when date given. + + +
:information_source: The lifeline of the diagram should end at the destroyer mark (X) but reaches end of diagram due to limitation of plantUML.
-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. +The following activity diagram illustrates how the complete operation is executed. + + + +#### Design Considerations + +Alternative 1 (Previous Design): Use a `CompleteCommandDescriptor` that has a `Date` and `Index` field wrapped by Java `Optional`. + +* Pros: + * Allows for clean, readable code without having to check for null values regardless of whether user inputs a date or index. + +* Cons: + * Have to check for both fields for at every step of the command which is inefficient. + +Alternative 2 (Current Choice): Make `CompleteCommand` an abstract class with the subclass `CompleteByIndex` and `CompletebyDate`. + +* Pros: + * `LogicManager` can just execute `CompleteCommand` without needing to know if it is `CompleteByIndex` or `CompleteByDate`. + * Also eliminates the need to check for null fields, since each `CompleteCommand` subclass only has their required fields. + * This also increases the extensibility of the command, as a new subclass can just be added. + +* Cons: + * Increases the amount of code written and testing required. + +### Gather Emails Feature + +The **Gather Emails** feature in our software system is designed to efficiently collect email addresses. This feature is facilitated by the `GatherCommand` and `GatherCommandParser`. Below is the class diagram of the `gather emails` feature. + +![GatherClassDiagram](images/GatherClassDiagram.png) + +#### Implementation Overview + +The `GatherCommand` is initiated by the `GatherCommandParser`. The `GatherCommandParser` parses the arguments and creates either a `GatherEmailByFinancialPlan` or `GatherEmailByTag` object respectively. +Both `GatherEmailByFinancialPlan` or `GatherEmailByTag` implements the `GatherEmailPrompt` interface. + +The `GatherCommand` takes in the `GatherEmailPrompt` object and passes it into the current `Model`, subsequently interacting with the `AddressBook` class. +The `GatherCommand#execute()` executes the gather operation by calling `Model#gatherEmails(GatherEmailPrompt prompt)`. + +The following sequence diagram below shows how the gather operation works as described above: + +![GatherSequenceDiagram1](images/GatherSequenceDiagram1.png) + +The `ModelManager#gatherEmails(GatherEmailPrompt prompt)` calls the `AddressBook#gatherEmails(GatherEmailPrompt prompt)` method, which subsequently calls the `UniquePersonsList#gatherEmails(GatherEmailsPrompt prompt)` method. + +The `UniquePersonsList` class maintains a list of unique persons. The `UniquePersonsList` class implements the following operation: + +- `UniquePersonsList#gatherEmails(GatherEmailPrompt prompt)` —  Iterates through the persons list and calls `GatherEmailPrompt#gatherEmails(Person person)`, passing in each person. + +Depending on the type of `GatherEmailPrompt`, the method above triggers either: + +- `Person#gatherEmailsContainsTag(String prompt)` —  Checks if the given prompt is a substring of any `Tag` names in the `Set` of the current person. +- `Person#gatherEmailsContainsFinancialPlan(String prompt)` —  Checks if the given prompt is a substring of any `FinancialPlan` names in the `Set` of the current person. + +These methods internally utilize `Tag#containsSubstring(String substring)` and `FinancialPlan#containsSubstring(String substring)`, respectively. These substring comparisons are performed in a case-insensitive manner by converting both the prompt and the `FinancialPlan` or `Tag` names to lowercase before the check. +By allowing partial input, users can efficiently find email addresses associated with lengthy `Tag` or `FinancialPlan` names. The case-insensitive approach enhances user-friendliness, ensuring consistent and reliable results, regardless of input case. + +Currently, we only allow gathering emails by `FinancialPlan` and `Tag` fields as these are the more likely to be searched to gather emails by. However, additional classes implementing the `GatherEmailPrompt` interface can be added to enable the gathering of emails based on a broader range of fields. + +The following sequence diagram shows how the gather emails by `FinancialPlan` field operation works: + +![GatherSequenceDiagram2](images/GatherSequenceDiagram2.png) + +The following activity diagram illustrates how the complete operation is executed: + +![GatherClassActivityDiagram](images/GatherClassActivityDiagram.png) + +#### Design Considerations + +**Aspect: How many inputs to accept** +* **Alternative 1 (Current Choice):** User can only search by one `FinancialPlan` or `Tag`. + * **Pros:** Easy to implement. Limits the potential for bugs. + * **Cons:** Limited filtering options. + +* **Alternative 2:** User can search by multiple `FinancialPlan` and `Tag` fields. + * **Pros:** More flexible (e.g. gathering by a combination of `FinancialPlan` and `Tag`). + * **Cons:** Introduces more complexity and requires additional error handling. + + +### Expanded Find feature + +The enhanced find mechanism is facilitated by the `CombinedPredicate` and utilises the existing `FindCommand` structure. + +#### Implementation Overview + +Here's a sequence diagram that demonstrates how `FindCommand` works: + +![FindCommandSequenceDiagram](images/FindCommandSequenceDiagram.png) + +The `CombinedPredicate` class gives the `find` command the ability to search for multiple terms at once, implemented using an array +of `PersonContainsKeywordsPredicate`. Here's a partial class diagram of the `CombinedPredicate`. -![UndoRedoState4](images/UndoRedoState4.png) +![CombinedPredicateClassDiagram](images/CombinedPredicateClassDiagram.png) -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. +All `XYZContainsKeywordsPredicate` classes (e.g., `NameContainsKeywordsPredicate`, +`FinancialPlanContainsKeywordsPredicate`, ...) inherit from the `PersonContainsKeywordsPredicate` interface so that +they can be treated similarly in the `CombinedPredicate` class. -![UndoRedoState5](images/UndoRedoState5.png) +Note that only `NameContainsKeywordsPredicate` checks for whole words, because it is rare to search for people by +substrings e.g. `Marc` and `Marcus` should not show up in the same search. On the other hand, +`FinancialPlanContainsKeywordsPredicate` and `TagContainsKeywordsPredicate` allow matching for +substrings because there are certain cases where it is logical to search for substrings e.g. `Plan A` and +`Plan A Premium` are related, so they can show up in the same search. -The following activity diagram summarizes what happens when a user executes a new command: +The `find` command format also changes to resemble a format more similar to the `add` and `edit` commands, to allow for +searching for keywords in multiple fields at the same time. We also allow the use of duplicate prefixes so that we +can search for multiple terms belonging to the same field. - +For now, we only allow for searching for `Name`, `FinancialPlan` and `Tag` fields because they are the most commonly +searched fields, but extending the feature to search in other fields is possible by creating the appropriate +`Predicate` class and modifying the `FindCommandParser`. -#### Design considerations: +#### Design Considerations -**Aspect: How undo & redo executes:** +**Aspect: How to implement find for multiple fields** +* **Alternative 1 (current choice):** Use one unified command and format. + * Pros: Easy to implement (argument multimap is available), allows for more flexible usage. + * Cons: May get cluttered when there are many terms. -* **Alternative 1 (current choice):** Saves the entire address book. - * Pros: Easy to implement. - * Cons: May have performance issues in terms of memory usage. +* **Alternative 2:** Take an argument to decide which field to find by. + * Pros: More user-friendly and natural since there is no need to use prefixes. + * Cons: Less flexible, slightly more difficult to implement. -* **Alternative 2:** Individual command knows how to undo/redo 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. +**Aspect: How to implement `CombinedPredicate`** +* **Alternative 1 (current choice):** Use `varargs` and a common interface. + * Pros: More flexible in usage while still testable. + * Cons: More difficult to modify and the check for equality can be defeated with enough effort. -_{more aspects and alternatives to be added}_ +* **Alternative 2:** Compose it with the 3 component predicates. + * Pros: Easier to modify and test. + * Cons: Less flexible when trying to combine multiple predicates (that may be of the same type). -### \[Proposed\] Data archiving +* **Alternative 3:** Use a `Predicate` and use the `or()` method to chain predicates. + * Pros: More flexible in usage. + * Cons: More difficult to modify and test. -_{Explain here how the data archiving feature will be implemented}_ +### Sort Feature +The **Sort** feature in our software system is designed to sort the list of clients by name as well as appointment +time. This feature is facilitated through the `SortCommand` class. + +#### Implementation Overview + +The following diagram summarises what happens when the user executes a sort command: + + + +The `SortCommand` class is instantiated by the `SortCommandParser`, which parses user input commands. The +`SortCommandParser` class implements the following operations: + +- `SortCommandParser#parse(String args)` —  Checks the sort command keyword passed in by the user. + +The `SortCommand` takes in a `Comparator` object and passes it into the current `Model` model. The +`SortCommand` class implements the following operations: + +- `SortCommand#execute()` —  Executes the sort operation by calling `model.sortFilteredPersonList(comparator)`. + +The `Model` interface is implemented by the `ModelManager`, representing the in-memory model of the address book data. +It contains the following method: + +- ModelManager#sortFilteredPersonList(Comparator comparator)` —  Carries out the sorting operation by +setting the comparator on the list of clients wrapped in a SortedList wrapper. + +A `CommandResult` class is created and returned. + + + + +#### Design Considerations + +**Aspect: How Sort Executes** + +**Alternative 1(current choice):** User can sort by name and appointment at any time. As such, calling find on the sorted list will result +in the ordering of find to also be sorted. +- Pros: Improved usability of maintaining order of list throughout without the list having to be reordered after +each command. +- Cons: Limited sorting options as of now. + + +### Appointment List Feature + +#### Implementation Overview + +The appointment list is facilitated by `ModelManager`. It extends `Model` and stores an additional `SortedList` object that represents all existing appointments. +The `setAppointmentList()` method checks against `filteredPersons` to look for updates regarding existing `Appointment` objects. The `setAppointmentList()` method is called whenever there is a command that can potentially change the data stored, to ensure that the state of the appointment list is as updated as possible. + +The `getAppointmentList()` method is called once during the startup of the program by `getAppointmentList()` in `LogicManager`, which is in turn called by `MainWindow`. It returns the `sortedList` object within `modelManager`. + +Do note that appointments are inherently sorted by their date and time, with the earliest appointment showing up at the top. + +The following sequence diagram shows how the appointment list is updated. The `setPerson()` method is being called in the `ScheduleCommand#execute()` method. The `ModelManager#addToAppointmentIfPresent()` method adds an `Appointment` object into the `ObservableList` if the `Person` object in `filteredPersons` has a `scheduleItem` object that is an instance of `Appointment`. + + + +#### Design Considerations + +**Aspect: Where to create** `SortedList` +* **Alternative 1 (current choice):** Implement it within `modelManager` + - Pros: `SortedAppointments` object references `filteredPersons` which ensures that the appointment list + corresponds with `persons` from `addressBook`. In this implementation, `persons` acts as the single source of truth which provides all information to `modelManager`. + - Cons: Errors with respect to `addressBook` will affect the appointment list rendered. + +* **Alternative 2:** Implement it within `addressBook` + - Pros: `persons` and `appointmentList` are handled separately within. `addressBook` and hence the appointment + list is not dependent on `persons` in `addressBook`. + - Cons: `filteredPersons` and `sortedAppointments` might not correspond since `sortedAppointments` is no longer + dependent on `filteredPersons`. + +**Aspect: How to update appointment list** +* **Alternative 1 (current choice):** +Empty `appointmentList` and populate the list when there is a command that can potentially update it. + - Pros: Easy to implement. + - Cons: Time complexity might not be optimal if the application is dealing with large amounts of data. +* **Alternative 2:** +Based on specific index that user inputs for commands, update `appointmentList` accordingly. + - Pros: Time complexity will be optimal and will not deterioriate with increasing amounts of data. + - Cons: More difficult to test. -------------------------------------------------------------------------------------------------------------------- @@ -255,73 +524,292 @@ _{Explain here how the data archiving feature will be implemented}_ ### Product scope -**Target user profile**: - -* has a need to manage a significant number of contacts +**Target user profile**: Financial Advisors +* has a need to manage a significant number of client contacts. * prefer desktop apps over other types -* can type fast -* prefers typing to mouse interactions -* is reasonably comfortable using CLI apps +* 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**: +This tool functions as a digital address book suited to the needs of financial advisors. +It allows them to track, update, and manage their clients’ information efficiently. +This is facilitated through the use of a command line interface for efficient and effective querying and +modifying of clients’ data. ### 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 | +| Priority | As a …​ | I want to …​ | So that I can…​ | +|----------|-----------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------| +| `* * *` | financial advisor who often works with numerous clients | have a central repository for my clients’ contacts details | effectively manage the intricate details of each of my clients. | +| `* * *` | financial advisor | add clients' contacts to the contact book | accumulate contacts for future purposes. | +| `* * *` | financial advisor | remove clients contacts from the contact book | keep my contact book compact and relevant. | +| `* * *` | financial advisor | edit clients’ contacts in the contact book | keep my information updated. | +| `* *` | financial advisor | record appointments with my clients | keep track of when my next meeting with the client is. | +| `* *` | financial advisor | tag my clients by the plans they purchase | gather groups of clients based on the financial plan(s) they purchased. | +| `* *` | financial advisor | search for clients with specific financial plans | update those people about their plans more efficiently. | +| `* *` | financial advisor | sort my clients in certain orders including alphabetical order or appointment time in both ascending and descending order | view my clients in a more systematic manner. | +| `* *` | financial advisor | view my upcoming appointments I have with clients in chronological order | better plan my time. | +| `* *` | financial advisor | complete appointments | clean up the address book of completed appointments. | +| `* *` | financial advisor | gather emails of clients by their tags such as age group | collate and notify people with the same tags on any updates. | +| `* *` | financial advisor | search for clients with the same financial plan | efficiently provide targeted updates to individuals with the same plan. | +| `*` | busy financial advisor | streamline administrative tasks like tracking my clients contacts | focus most of my time on giving personalised financial advice and services to my clients. | +| `*` | financial advisor managing a substantial client portfolio | follow a standardised format to collect my clients’ information | manage data consistency among my clients. | +| `*` | financial advisor | search for specific client details | quickly contact my clients. | +| `*` | busy financial advisor | have a warning prompt to confirm clearing of contact book | prevent accidental clearing of contact book | +| `*` | financial advisor with many appointments | have a warning when scheduling multiple appointments for the same person | receive reminders of appointments before making a new one | -*{More to be added}* ### 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 `UNOFAS` and the **Actor** is the `financial advisor`, unless specified otherwise) -**Use case: Delete a person** + +**Use Case: UC01 - Show a list of all clients**\ +**Precondition:** NIL\ +**Guarantees**: A list of all clients' contact is shown. **MSS** +1. User requests to list all clients. +2. UNOFAS shows a list of all clients.\ + Use case ends. + +**Extensions** +* 2a. The list is empty.\ + Use case ends. -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 +**Use Case: UC02 - Add a client** \ +**Precondition:** NIL\ +**Guarantees**: A client contact is added into UNOFAS only if the data entered is correct. + +**MSS** +1. User request to add a client to the list via the `add` command. +2. UNOFAS checks the correctness of the request. +3. UNOFAS adds the client and displays updated client list. Use case ends. **Extensions** +* 2a. User did not specify all required fields. + * 2a1. UNOFAS shows an error message. -* 2a. The list is empty. + Use case resumes at step 1. - Use case ends. +**Use Case: UC03 - Edit a client's contacts** \ +**Precondition:** NIL\ +**Guarantees**: A client contact is edited in UNOFAS only if the data entered is correct. -* 3a. The given index is invalid. +**MSS** + +1. User requests to list clients (UC01). +2. User request to edit client’s contacts from the list via the `edit` command. +3. UNOFAS checks the correctness of the request. +4. UNOFAS changes the client’s contacts. - * 3a1. AddressBook shows an error message. + Use case ends. + +**Extensions** + +* 3a. User enters the wrong details. + + * 3a1. UNOFAS shows an error message. Use case resumes at step 2. +* 3b. There is a contact with the exact same name. + * 3b1. UNOFAS notifies user that contact exist. + + Use case resumes at step 2. + +**Use Case: UC04 - Delete a client** \ +**Precondition:** NIL\ +**Guarantees**: A client contact is deleted from UNOFAS only if the data entered is correct. -*{More to be added}* +**MSS** + +1. User requests to list clients (UC01). +2. User requests to delete a specific client in the list via the `delete` command. +3. UNOFAS checks the correctness of the request. +4. UNOFAS deletes the client. + + Use case ends. + +**Extensions** + +* 3a. User inputs an invalid index. + + * 3a1. UNOFAS shows an error message. + + Use case resumes at step 2. + +**Use Case: UC05 - Find a client** \ +**Precondition:** NIL\ +**Guarantees**: A list of clients that matches the query is displayed. + +**MSS** + +1. User requests to find client. +2. UNOFAS checks for the correctness of the request. +3. UNOFAS shows a list of clients which match search query + + Use case ends. + +**Extensions** +* 2a. User inputs invalid data for the command. + * 2a1. UNOFAS shows an error message. + Use case resumes at step 1. +* 3a. The list is empty. + + Use case ends. + +**Use Case: UC06 - Assign financial plan to a client** \ +**Precondition:** NIL\ +**Guarantees**: A financial plan is assigned to a client in UNOFAS only if the data entered is correct. + +**MSS** + +1. User requests to list clients (UC01). +2. UNOFAS shows a list of clients. +3. User request to add financial plan to client’s contacts (UC03). +4. UNOFAS checks for the correctness of the request. +5. UNOFAS changes the client’s contacts. + + Use case ends. + +**Extensions** + +* 4a. User enters the wrong details. + + * 4a1. UNOFAS shows an error message. + + Use case resumes at step 1. + +**Use Case: UC07 - Sort client's contacts** \ +**Precondition:** NIL\ +**Guarantees**: The contact list will be sorted in ascending order according to the sort function specified. + +**MSS** + +1. User requests to list clients (UC01) +2. UNOFAS shows a list of clients. +3. User requests to sort list of clients (by appointment time or name) via `sort` command. +4. UNOFAS checks for the correctness of the request. +5. UNOFAS updates ordering of clients' contacts. + + Use case ends. + +**Extensions** + +* 4a. User enters the wrong details. + * 4a1. UNOFAS shows an error message. + Use case resumes at step 1. + +**Use Case: UC08 - Schedule appointment for a client** \ +**Precondition:** Client must exist before scheduling appointment.\ +**Guarantees**: An appointment is scheduled for a client in UNOFAS only if the data entered is correct. + +**MSS** + +1. User requests to list clients (UC01) +2. UNOFAS shows a list of clients. +3. User request to schedule appointment for client via the `schedule` command. +4. UNOFAS checks the correctness of the request. +5. UNOFAS changes the client’s contacts. + + Use case ends. + +**Extensions** + +* 4a. User enters the wrong details. + * 4a1. System shows an error message. + Use case resumes at step 1. + +* 4b. Client's contact has an existing appointment scheduled. + * 4b1. UNOFAS shows a warning message. + * 4b2. User confirms to replace the existing appointment. + Use case resumes at step 5. + +**Use Case: UC09 - Complete appointment for a client** \ +**Precondition:** Appointment and client must exist before completing appointment.\ +**Guarantees**: An appointment is completed for a client in UNOFAS only if the data entered is correct. + +**MSS** + +1. User requests to list clients (UC01) +2. UNOFAS shows a list of clients. +3. User requests to complete appointment for client via the `complete` command. +4. UNOFAS checks for the correctness of the command. +5. UNOFAS removes appointment from appointment list and client's contact card + + Use case ends. + +**Extensions** + +* 4a. User enters the wrong details. + * 4a1. UNOFAS shows an error message. + Use case resumes at step 3. + +* 4b. Client's contact chosen does not have an existing appointment scheduled. + * 4b1. UNOFAS shows a warning message. + Use case ends. + +* 4c. No clients' appointments in contacts matches date input by user. + * 4c1. UNOFAS shows a warning message. + 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. -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. +3. A user with typing speed of above 80WPM 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. +4. A user should be able to have up to 2000 clients. +5. The product is offered as a free offline service. +6. The codebase should be well-documented to aid in future maintenance and updates. +7. Should continue working despite invalid commands and error messages should be shown to the user. +8. All features added to the code should be tested. +9. All commands should be able to be executed by a financial advisor with little technical knowledge. + +### Planned Enhancements +1. The current `schedule` command does not check if the given date is before the current date, so it is vulnerable to +user error. We plan to make the command check for the date and fail if the date is before the current date: `Date given + cannot be before the current date`. +2. The current `phone` and `next-of-kin phone` fields currently only accepts numbers. It cannot accept international +number formats. We plan to make the fields accept symbols so numbers such as `+6598765432` and `001-234-1-4610818` +will be accepted. This will involve changing the validity checker for both fields. +3. The current contact book does not check for duplicates beyond the exact matching of the person's `name`. +We plan to refuse adding/editing of a person's details if it results in two people sharing a `name` (case-insensitive) +or `phone` since two people are very unlikely to share those details. +4. The current `name` and `next-of-kin name` fields currently do not accept symbols. We plan to make the fields accept +symbols so that names like `Thaarshen s/o Thaarshen` and `O'Brien` are accepted. This will involve changing the +validity checker for both fields. +5. The current `gather` command does not allow the gathering of all emails in the contact book or by multiple fields +at once. To allow the gathering of all the persons emails using `gather all` command, we plan create another +`GatherEmailPrompt` class, with a method that will call the Person `getEmail()` method. To allow gathering emails by multiple fields, for example using the `fp/` and `t/` prefixes at once, we plan to use a similar approach +to `find` but return the person's email instead. +6. The `clear` command confirmation window can be manipulated using the arrow and 'Enter' keys. The window is +initialised with the focus on the `confirm` button. This makes it possible for a user to accidentally press 'Enter' +twice and wipe the contact book anyway, bypassing the defence mechanism entirely. We plan to make the command more +resistant to mistakes by having the user key in a specific phrase, or to initialise the window with the focus on the +`cancel` button instead. +7. The `schedule` and `complete` command current uses the same `d/` prefix but the format for the arguments is different for both. We plan to update add a new `dt/` prefix to represent date-time arguments for `schedule` command to prevent confusion for the users. -*{More to be added}* ### Glossary -* **Mainstream OS**: Windows, Linux, Unix, OS-X -* **Private contact detail**: A contact detail that is not meant to be shared with others +* **Mainstream OS**: Windows, Linux, UNIX, OS-X. +* **Private contact detail**: A contact detail that is not meant to be shared with others. +* **API**: Application Programming Interface that enables application to use capabilities or data from another application. +* **Appointment** : An arrangement to meet someone at a particular time, in this case, a client. +* **Financial Advisor**: A person who provides financial advice and sells financial plans to prospective clients. +* **Financial Products**: A product connected with the way a person manages or uses money (e.g. Insurance). +* **Client**: A person whose financial products are being managed by a financial advisor. +* **Portfolio value**: The intrinsic value of all financial products being held under a clients name. +* **Central Repository**: A centralised storage location for all user data. +* **Contact details**: Name, email, phone number, next-of-kin name, next-of-kin phone number, home address, financial plan(s), tag(s) and appointment(if any) of a client. +* **Lexicographical**: Generalisation of alphabetical order to include symbols or elements of a totally ordered set. -------------------------------------------------------------------------------------------------------------------- @@ -329,8 +817,8 @@ Priorities: High (must have) - `* * *`, Medium (nice to have) - `* *`, Low (unli Given below are instructions to test the app manually. -
:information_source: **Note:** These instructions only provide a starting point for testers to work on; -testers are expected to do more *exploratory* testing. +
:information_source: **Note:** These instructions only provide a starting +point for testers to work on; testers are expected to do more *exploratory* testing.
@@ -340,38 +828,195 @@ testers are expected to do more *exploratory* testing. 1. Download the jar file and copy into an empty folder - 1. Double-click the jar file Expected: Shows the GUI with a set of sample contacts. The window size may not be optimum. + 2. Double-click the jar file Expected: Shows the GUI with a set of sample contacts. The window size may not be optimum. -1. Saving window preferences +2. Saving window preferences 1. Resize the window to an optimum size. Move the window to a different location. Close the window. - 1. Re-launch the app by double-clicking the jar file.
+ 2. 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 person + +1. Adding a person. + + 1. Prerequisites: List all persons using the `list` command. + + 2. Test case: `add n/name p/987 a/address e/email@email.com nk/nokname nkp/654`
+ Expected: New contact with the above details is added to the bottom of the list. + + 3. Test case: `add n/invalidName! p/987 a/address e/email@email.com nk/nokname nkp/654`
+ Expected: No person is added. Error details shown in the status message. Status bar remains the same. + +### Editing a person + +1. Editing an existing person. + + 1. Prerequisites: List all persons using the `list` command. At least 1 person in the list. + + 2. Test case: `edit 1 n/New Name`
+ Expected: The first contact in the list has their name changed to `New Name`. + + 3. Test case: `edit 1 n/Invalid Name!`
+ Expected: The name of the first contact is unchanged. Error details shown in the status message. + Status bar remains the same. ### Deleting a person -1. Deleting a person while all persons are being shown +1. Deleting a person while all persons are being shown. 1. Prerequisites: List all persons using the `list` command. Multiple persons 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. + 2. Test case: `delete 1`
+ Expected: First contact is deleted from the list. Details of the deleted contact shown in the status message. + Timestamp in the status bar is updated. - 1. Test case: `delete 0`
+ 3. Test case: `delete 0`
Expected: No person is deleted. Error details shown in the status message. Status bar remains the same. - 1. Other incorrect delete commands to try: `delete`, `delete x`, `...` (where x is larger than the list size)
+ 4. Other incorrect delete commands to try: `delete`, `delete x` (where x is larger than the list size)
Expected: Similar to previous. -1. _{ more test cases …​ }_ +2. Deleting a person while not all persons are being shown + + 1. Prerequisites: List all persons using the `list` command, then filter the list using the `find` command. At least + 1 person in the remaining list and at least 1 person filtered out. + 2. Test case: `delete 1`
+ Expected: Same as with the full list. + 3. Test case: Delete a number that is equal to the number of people in the full contact book.
+ Expected: No person is deleted. Error details shown in the status message. Status bar remains the same. + +### Finding a person + +1. Finding a person by name. + + 1. Prerequisites: List all persons using the `list` command. At least 1 person in the contact book with the name + `Test Name`. + + 2. Test case: `find n/Name`
+ Expected: The person with the name `Test Name` appears in the filtered contact book. + + 3. Test case: `find n/Invalid Name!`
+ Expected: List remains unchanged. Error details shown in the status message. Status bar remains the same. + +2. Finding a person by tag or financial plan can be tested in a similar manner as above. + +### Gathering emails + +1. Gathering emails by tag. + + 1. Prerequisites: At least 1 person in the contact book with the tag `TestTag`. + + 2. Test case: `gather t/Tag`
+ Expected: The email of the person with the tag `TestTag` appears in the status message. + + 3. Test case: `gather t/WrongTag!`
+ Expected: Error details shown in the status message. Status bar remains the same. + +2. Gathering emails by financial plan can be tested in a similar manner as above. + +### Sorting the contact book + +1. Sorting by names. + + 1. Prerequisites: List all persons using the `list` command. At least 2 people in the contact book. + + 2. Test case: `sort name`
+ Expected: The contact book is sorted by names in alphabetical order. + + 3. Test case: `sort names`
+ Expected: List remains unchanged. Error details shown in the status message. Status bar remains the same. + +2. Sorting by appointments can be tested in a similar manner as above. + +### Scheduling an appointment + +1. Scheduling an appointment. + + 1. Prerequisites: List all persons using the `list` command. At least 1 person in the contact book. + + 2. Test case: `schedule 1 ap/Appointment Name d/11-11-2025 09:00`
+ Expected: The first person in the list is updated to contain the appointment details. The appointment list is + updated as well. + + 3. Test case: `schedule 1 ap/Appointment Name d/11-30-2025 09:00`
+ Expected: Error details shown in the status message. List, status bar and appointment list remains the same. + + 4. Test case: `schedule 1 ap/Appointment Name d/12-11-2025 09:00` on a person who already has an appointment
+ Expected: A prompt will appear that causes program functionality to temporarily stop. The prompt alerts the user + that the client already has an appointment arranged and the appointment will be overriden if the user wishes to + proceed. After proceeding, the old appointment is overridden and the app continues its notmal functionality. + +### Completing an appointment + +1. Completing by index. + + 1. Prerequisites: List all persons using the `list` command. At least 1 person in the contact book with a scheduled + appointment. + + 2. Test case: `complete 1`
+ Expected: Appointment details removed from the first person in the list. The appointment list is updated as well. + + 3. Test case: `complete 0`
+ Expected: Error details shown in the status message. List, status bar and appointment list remains the same. + +2. Completing by appointment date. + + 1. Prerequisites: List all persons using the `list` command. Exactly 2 people in the contact book with a scheduled + appointment on `11-11-2025`. + + 2. Test case: `complete d/11-11-2025`
+ Expected: Appointment details removed from the 2 people in the list. The appointment list is updated as well. + +### Clearing data + +1. User wishes to clear the addressbook. + 1. Test case: `clear`
+ Expected: A prompt will appear that causes program functionality to temporarily stop. The prompt alerts the user + that he intends to clear the address book and asks for confirmation of the clear. After proceeding, the entire address book will be cleared. ### Saving data -1. Dealing with missing/corrupted data files +1. Dealing with missing data files + 1. If there is no saved data, the application will open with a new data file loaded with sample data + 2. To do this: + 1. Go to the location of the saved data. The location of the saved data can be found at the bottom left of the UNOFAS app. + 2. Delete the file `addressbook.json`. + 3. Restart the application. + 3. A new file with sample contacts and appointments will be created. + +2. Dealing with corrupted data files + 1. If saved data is corrupted, the application will wipe the corrupted data and restart with no contacts and appointments. + 2. To simulate a corrupted file: + 1. Go to the location of the saved data. + 2. Open `addressbook.json` and corrupt the file in a way that makes it an invalid file to read (e.g. adding alphabets into a contact's phone number field) + 3. Restart the application. + 3. A new file will be created with no contacts and appointments. - 1. _{explain how to simulate a missing/corrupted file, and the expected behavior}_ +-------------------------------------------------------------------------------------------------------------------- -1. _{ more test cases …​ }_ +## **Appendix: Effort** +This project required a substantial effort to design and implement various features aimed at enhancing +the functionality of the software system. It was quite hard at the beginning because we were not well-versed with the +codebase. After understanding some pertinent classes to implement our enhancements, we also had to refactor and +add test cases to ensure the functionalities of our enhancements. + +In v1.2, we implemented the Sort command to allow users to sort his clients' contacts in alphabetical order with +respect to their names, or with respect to their appointment time in chronological order. This posed a significant +challenge as there was no clear documentation that provided aid as to how we should implement both the filtering function (via +the find command) and the sort command at the same time. We had to spend time understanding the inner workings of +JavaFX's `filteredList` and `sortedList` classes to finally come up with a solution to return a sorted and filtered +list of clients. + +In v1.3, we updated the GUI to include an appointment list to show upcoming appointments that clients have with the +financial advisor. We found out that the appointment list is not properly updated when there are changes made to the +data, and they are only updated upon restarting the application. We had to spend time understanding how the Observer +Pattern works so that changes to the appointment list are being reflected instantaneously. + +Moreover, we decided to implement safety features like the clear and override prompts that prevent accidental command executions +by the user. This required having to trace the entire code logic to understand how commands were executed and also how to +pause and split execution of commands to make sure that no bugs were introduced into the code. + +The project's difficulty level was notably high due to the complexity of implementing features such as scheduling +appointments, gathering emails, expanded find functionality, sorting, and the introduction of an appointment list. diff --git a/docs/UserGuide.md b/docs/UserGuide.md index 57437026c7b..5f7959f3299 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -1,55 +1,140 @@ --- layout: page -title: User Guide +title: UNOFAS User Guide --- -AddressBook Level 3 (AB3) is a **desktop app for managing contacts, optimized for use via a Command Line Interface** (CLI) while still having the benefits of a Graphical User Interface (GUI). If you can type fast, AB3 can get your contact management tasks done faster than traditional GUI apps. - -* Table of Contents -{:toc} +## Welcome to UNOFAS! +Welcome to the User Guide for UNOFAS, your comprehensive solution for efficient and organized client contact management. Ever found yourself spending countless hours sorting and managing your clients' contacts? We understand the importance of managing your life commitments alongside maintaining strong relationships with your clients as dedicated financial advisors. UNOFAS has been designed to streamline administrative hassles, providing you with the tools you need to manage clients' contacts efficiently. + +## What is UNOFAS? +UNOFAS (UNO: One FAS: Financial Advisors app) is a **desktop app for Financial Advisors to manage clients contacts and schedule appointments, +optimized for use via a Command Line Interface** (CLI) while still having the benefits of a +Graphical User Interface (GUI). For fast typists, UNOFAS can help you manage and retrieve client's information +better than traditional GUI apps. + +## Purpose of this guide +This guide provides you with comprehensive instructions on utilizing UNOFAS. It also serves as a mode of referral to +help learn the various commands required to effectively learn and be able to integrate the application to your daily +use as financial advisors. + +To help you make the most of this resource, we would recommend that you take some time to read through the entire guide. +You can then utilize the Table of Contents provided below and click on the relevant links to easily navigate through +the guide and access the information you might need or want to take another look at. We hope that this guide serves you well +to help make managing your clients a more pleasant experience. + +## Legend +:information_source: Important information to take note of.
+:bulb: Useful tips for improving usability of the application.
+:exclamation: Mistakes to be wary of. + + +## Table of Contents +* [Quick Start](#quick-start) +* [UI Components](#ui-components) + * [General UI Information](#general-ui-information) + * [Contact Card](#contact-card) + * [Appointment Card](#appointment-card) +* [Features](#features) + * [Help](#viewing-help--help) + * [Add](#adding-a-client-contact--add) + * [List](#listing-all-client-contacts--list) + * [Edit](#editing-a-client-contact--edit) + * [Find](#locating-clients-by-name-financial-plan-andor-tag--find) + * [Gather](#gathering-emails-of-matching-clients--gather) + * [Schedule](#scheduling-an-appointment--schedule) + * [Complete](#completing-an-appointment--complete) + * [Delete](#deleting-a-client--delete) + * [Clear](#clearing-all-entries--clear) + * [Sort](#sorting-of-data--sort) + * [Exit](#exiting-the-program--exit) +* [Saving the data](#saving-the-data) +* [Editing the data file](#editing-the-data-file) +* [FAQ](#faq) +* [Known Issues](#known-issues) +* [Command Summary](#command-summary) -------------------------------------------------------------------------------------------------------------------- -## Quick start +## Quick Start 1. Ensure you have Java `11` or above installed in your Computer. + For further information, you may refer to these guides on [how to check java version](https://www.java.com/en/download/help/version_manual.html) + and [how to install Java](https://docs.oracle.com/en/java/javase/11/install/overview-jdk-installation.html#GUID-8677A77F-231A-40F7-98B9-1FD0B48C346A) if needed. -1. Download the latest `addressbook.jar` from [here](https://github.com/se-edu/addressbook-level3/releases). +2. Download the latest `unofas.jar` from [here](https://github.com/AY2324S1-CS2103T-F12-1/tp/releases). -1. Copy the file to the folder you want to use as the _home folder_ for your AddressBook. +3. Copy the file to the folder you want to use as the _home folder_ for your UNOFAS. -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.
+4. Open a command terminal, `cd`(change directory) into the folder you put the jar file in. For example, if the file is in + your `Documents` folder, go to the terminal and type in `cd ~/Documents`.
+ Once you are in the selected folder, enter the `java -jar unofas.jar` command into the terminal + 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) -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.
+5. Type the command in the command box and press the Enter key 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 your client-contacts. + + * `add n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01 nk/David nkp/91234567` : Adds a contact named `John Doe` + to the Contact Book. + + * `delete 3` : Deletes the 3rd contact shown in the contact list. + + * `clear` : Deletes all client contacts. + + * `exit` : Exits the application. + +6. Refer to the [Features](#features) below for details of each command. + +-------------------------------------------------------------------------------------------------------------------- +## UI Components + +![generalUi](images/generalUi.png) + +### General UI information + +| Component | Purpose | +|----------------------|--------------------------------------------------------| +| **Navigation Bar** | Allows you to exit UNOFAS or view help | +| **Command Line** | Location to enter commands | +| **Result Line** | Displays the result after a command is entered | +| **Contact List** | Displays client contacts | +| **Appointment List** | Displays appointments | +| **Contact Card** | Displays detailed information about a client | +| **Appointment Card** | Displays detailed information about an appointment | +| **Save Location** | Displays the location where your UNOFAS data is stored | + +### Contact Card - * `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. +![contactCardUi](images/contactCardUi.png) - * `delete 3` : Deletes the 3rd contact shown in the current list. +* Shows the client's name, phone number, financial plan(s) (if any), address, email address, next-of-kin name, next-of-kin phone number, appointment (if any) and tag(s) (if any). - * `clear` : Deletes all contacts. +### Appointment Card - * `exit` : Exits the app. +![appointmentCardUi](images/appointmentCardUi.png) -1. Refer to the [Features](#features) below for details of each command. +* Shows the appointment name, client allocated to the appointment and the appointment date and time. +* The appointment cards displayed will only include appointments of clients displayed in the contact list. -------------------------------------------------------------------------------------------------------------------- -## Features +# Features
**:information_source: Notes about the command format:**
-* Words in `UPPER_CASE` are the parameters to be supplied by the user.
+* Words in `UPPER_CASE` are the parameters to be supplied by users.
e.g. in `add n/NAME`, `NAME` is a parameter which can be used as `add n/John Doe`. * Items in square brackets are optional.
- 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/friend` or as `n/John Doe`. + +* Unless explicitly allowed, blank inputs or inputting any number of spaces as an argument for a field is invalid. * 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. @@ -57,141 +142,402 @@ AddressBook Level 3 (AB3) is a **desktop app for managing contacts, optimized fo * 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.
+* Any input parameters that do not adhere to the accepted values will result in the command + **failing and not executing**. + +* Extraneous parameters for commands that do not take in parameters (such as `help`, `list`, `exit` and `clear`) will + be ignored.
e.g. if the command specifies `help 123`, it will be interpreted as `help`. -* If you are using a PDF version of this document, be careful when copying and pasting commands that span multiple lines as space characters surrounding line-breaks may be omitted when copied over to the application. +* 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. + +
+ +-------------------------------------------------------------------------------------------------------------------- +# Argument Summary + +Below is a table summarising common arguments used in `add`, `edit`, `find`, `schedule` etc. Refer to the table below +to view the arguments' prefix, and their acceptable values. Unless specified, having only space characters i.e. an empty +value, is not an acceptable value and will result in a warning. + +| Prefix | Argument | Acceptable Values | +|--------|-----------------------|--------------------------------------------------------------------------------------------------------------------------------| +| - | INDEX | Number (1 to current size of the contact book) | +| `n/` | NAME | Alphabets, numbers, and space characters only | +| `p/` | PHONE_NUMBER | Numbers only and at least 3 digits long | +| `e/` | EMAIL | Alphabets, numbers, and symbols only in a valid email format | +| `a/` | ADDRESS | Alphabets, numbers, space characters and symbols only | +| `nk/` | NEXT_KIN | Alphabets, numbers, and space characters only | +| `nkp/` | NEXT_KIN_PHONE | Numbers only and at least 3 digits long | +| `fp/` | FINANCIAL_PLAN | Alphabets, numbers, and space characters only. Empty value is accepted only when using [Edit](#editing-a-client-contact--edit) | +| `t/` | TAG | Alphabets and numbers only. Empty value is accepted only when using [Edit](#editing-a-client-contact--edit) | +| `ap/` | APPOINTMENT_NAME | Alphabets, numbers, and space characters only | +| `d/` | APPOINTMENT_DATE | Format: dd-MM-yyyy (e.g., 31-12-2023) | +| `d/` | APPOINTMENT_DATE_TIME | Format: dd-MM-yyyy HH:mm (e.g., 31-12-2023 14:30) | +| - | KEYWORD | `name` or `appointment` | + +
:information_source: +**Do note** If the ADDRESS includes any recognized prefixes (leading space + prefix), users should be careful. +For instance, if you input `a/Blk 285 n/Clementi` for the ADDRESS argument, it will trigger the identification of the prefix `n/` in the input and result in an error message.
+----------------------- ### Viewing help : `help` -Shows a message explaning how to access the help page. +Displays a message explaining how to access the help page, as well as a list of available keywords. ![help message](images/helpMessage.png) Format: `help` +--------------------------- +### Adding a client-contact : `add` -### Adding a person: `add` +Adds a client's contact (name, phone number, email, home address, next-of-kin name, next-of-kin phone +number) into contact book. Optionally you may add financial plans or tags to the client at the same time. -Adds a person to the address book. +Format: `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS nk/NEXT_KIN nkp/NEXT_KIN_PHONE [fp/FINANCIAL_PLAN]… [t/TAG]…​` -Format: `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]…​` +* Adding a new client-contact with the exact same name (case-sensitive) as a client currently in the contact book counts as a + duplicate and will cause the command to fail. Duplicate information in other fields does not count as a duplicate + client. +* To prevent accidentally adding duplicates, you can use [Find](#locating-clients-by-name-financial-plan-andor-tag--find) + to check if you have already added the client. +* After performing an add, the contact list will be reset to display all client contacts. +* You MAY NOT add an empty tag or financial plan eg.(`fp/` or `t/`) + +Acceptable Values: Refer to [Argument Summary](#argument-summary).
:bulb: **Tip:** -A person can have any number of tags (including 0) +A client-contact can have any number of Financial Plans (including 0). +A client-contact can have any number of tags (including 0).
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` +* `add n/John p/80101010 e/johndoe@gmail.com a/Punggol Central Blk 444 #15-32 820123 nk/Brennan nkp/82020202 t/80yo fp/Financial Plan C` + +Successful Output: `New person added: John; +Phone: 80101010; +Email: johndoe@gmail.com; +Address: Punggol Central Blk 444 #15-32 820123; +Next-of-kin Name: Brennan; +Next-of-kin Phone: 82020202; +Appointment: No Appointment made!; +Financial Plans: [Financial Plan C]; +Tags: [80yo]` + +![result for 'add n/John p/80101010 e/johndoe@gmail.com a/Punggol Central Blk 444 #15-32 820123 nk/Brennan nkp/82020202 t/80yo fp/Financial Plan C'](images/addUi.png) + +
:information_source: +**Do note** that it is possible to add a client's contact with multiple tags by duplicating the `t/` prefix. The same can be done for financial plans with the `fp/` prefix. +
-### Listing all persons : `list` +------------------ +### Listing all client-contacts : `list` -Shows a list of all persons in the address book. +Displays a list of all the clients and their contact details that are currently stored in the contact book. Format: `list` -### Editing a person : `edit` +Successful Output: `Listed all persons` -Edits an existing person in the address book. +![result for 'list'](images/ListUi.png) -Format: `edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAG]…​` +-------------------------------- +### Editing a client-contact : `edit` -* 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. +Edit clients contact fields using an index followed by the updated details. + +Format: `edit INDEX [n/NAME] [p/PHONE_NUMBER] [e/EMAIL] [a/ADDRESS] [nk/NEXT_KIN] [nkp/NEXT_KIN_PHONE] [fp/FINANCIAL_PLAN]…​ [t/TAG]…​` + +* Edits the client-contact at the specified `INDEX`. The index refers to the index number shown in the displayed contact 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. +* Editing the name of a client to be the exact same name as another client currently in the contact book +(case-sensitive) counts as a duplicate and will cause the command to fail. Duplicate information in other ways does +not count as a duplicate contact. +* When editing financial plans or tags, the existing financial plans or tags of the client will be removed i.e. adding +of tags is not cumulative. +* You can remove all the client's tags by typing `t/` without specifying any tags after it. +* You can remove all the client's financial plans by typing `fp/` without + specifying any financial plans after it. +* A client's appointment cannot be edited in this manner. Refer to [Schedule](#scheduling-an-appointment--schedule). +* After performing an edit, the contact list will be reset to display all contacts. + +Acceptable Values: Refer to [Argument Summary](#argument-summary). + +
:bulb: **Tip:** +As your list of clients might be long, you can consider using the [Find](#locating-clients-by-name-financial-plan-andor-tag--find) command first to filter the list before editing details of your client. +
+ +Examples: +* `edit 4 n/john doe a/23 woodlands ave 123` Edits the name and address of the 1st client to be `john doe` and `woodlands ave 123` respectively. + +Successful Output: +`Edited Person: john doe; +Phone: 80101010; +Email: johndoe@gmail.com; +Address: 23 woodlands ave 123; +Next-of-kin Name: Brennan; +Next-of-kin Phone: 82020202; +Appointment: No Appointment made!; +Financial Plans: ; +Tags:` + +![result for 'edit 4 n/john doe a/23 woodlands ave 123'](images/editUi.png) + +
:information_source: +**Do note** that it is possible to edit a client's contact with multiple tags by duplicating the `t/` prefix. The same can be done with for financial plans with the `fp/` prefix. +However, multiple fields of the same type with an empty tag or financial plan is not allowed. For example, `t/ t/` and `fp/ fp/plan` is not accepted. +
+ +--------------- +### Locating clients by name, financial plan, and/or tag : `find` + +Finds client-contact(s) whose names, tags or financial plans contain any of the specified keywords. + +Format: `find [n/NAME]…​ [fp/FINANCIAL_PLAN]…​ [t/TAG]…​` + +* At least one of the optional fields must be provided. +* This command will ignore other prefixes. Using them anyway can cause undefined behaviour. +* The search is case-insensitive. e.g `hans` will match `Hans`. +* For names, only full words will be matched e.g. `Han` will not match `Hans`. +* Calling this command on a sorted list will retain the sorted quality of the list. +* For financial plans and tags, any substring will be matched e.g. `Senior` will match `SuperSenior`. +* Contacts matching at least one keyword will be returned (i.e. `OR` search). + e.g. `n/Hans n/Bo` will return `Hans Gruber`, `Bo Yang`. +* Upon successful execution of the command, only the relevant contact details will be reflected in the **Contact list**. The relevant appointment details will also be updated in the **Appointment list**. + +Acceptable Values: Refer to [Argument Summary](#argument-summary). 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. +* `find n/john n/charlie` returns `Charlie`, `john doe`
+ + ![result for 'find john charlie'](images/findJohnCharlieResult.png) + +### Gathering emails of matching clients : `gather` -### Locating persons by name: `find` +Gathers all the emails of clients with a desired financial plan or tag. -Finds persons whose names contain any of the given keywords. +Format: `gather fp/FINANCIAL PLAN` or `gather t/TAG` -Format: `find KEYWORD [MORE_KEYWORDS]` +* Generates a list of emails separated by semicolons, making it convenient for copying and pasting into the recipient input of an email application. + This function currently known to be compatible with gmail and outlook but might not work for all email services. +* Either **Financial Plan or Tag** can be searched at once, but **not both**. +* The search is case-insensitive e.g. `financial` will match `FINANCIAL` or `Financial`. +* A client's email will be gathered if the prompt matches a substring of their financial plan or tag. + eg. `finan` will match `financial` or `financial plan`. -* 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` +Acceptable Values: Refer to [Argument Summary](#argument-summary). + +
:bulb: **Tip:** +You may find this command especially useful to quickly contact your clients when there is a financial policy change. +
Examples: -* `find John` returns `john` and `John Doe` -* `find alex david` returns `Alex Yeoh`, `David Li`
- ![result for 'find alex david'](images/findAlexDavidResult.png) +* `gather t/Elderly` +* `gather fp/Financial Plan A` -### Deleting a person : `delete` +Successful Output: +`davidmiller@gmail.com; bob@example.com;` -Deletes the specified person from the address book. +![result for`gather fp/Financial Plan A'](images/gatherUi.png) + +------------ +### Deleting a client : `delete` + +Deletes the client-contact from the contact book by their index. Format: `delete INDEX` -* Deletes the person at the specified `INDEX`. -* The index refers to the index number shown in the displayed person list. -* The index **must be a positive integer** 1, 2, 3, …​ +* Deletes the contact at the specified `INDEX`. +* The index refers to the index number shown in the displayed contact list. + +Acceptable Values: Refer to [Argument Summary](#argument-summary). 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 `delete 1` deletes the 1st contact in the contact book. + +Successful Output: +`Deleted Person: David; +Phone: 93234567; +Email: davidmiller@gmail.com; +Address: Bishan Blk 999 #08-15 569874; +Next-of-kin Name: Olivia; +Next-of-kin Phone: 56981234; +Appointment: Meeting, 15-12-2023 17:30; +Financial Plans: [Financial Plan A][Financial Plan B]; +Tags: ` + +---------- +### Scheduling an Appointment : `schedule` +Schedules an appointment for a client using an index followed by the appointment details. + +Format: `schedule INDEX ap/APPOINTMENT_NAME d/APPOINTMENT_DATE_TIME` + +- Schedules appointment with the client contact at the specified `INDEX`. The index refers to the index number shown in the displayed contact list. +- **Both appointment name and date-time** must be provided. +- Upon successful execution of the command, the scheduled appointment details will be updated in the **Contact list**. The appointment details will also be updated in the **Appointment list**. +The contact list displayed will be reset to display all clients in the Contact Book. + +
:information_source: +If there is an existing appointment with the client when the command is executed, you can replace it with a new appointment by **clicking confirm** or **pressing the enter key** when the prompt is given. +
+ + ![schedule prompt](images/schedulePrompt.png) + +Acceptable Values: Refer to [Argument Summary](#argument-summary). + +Example: +- `schedule 1 ap/Annual review of financial goals d/20-11-2023 15:00` + +Successful Output: \ +For overridden appointment: `Appointment updated!`\ +For new appointment: `New appointment added: David; Phone: 93234567; Email: davidmiller@gmail.com; Address: Bishan Blk 999 #08-15 569874; Next-of-kin Name: Olivia; Next-of-kin Phone: 56981234; Appointment: Annual review of financial goals, 20-11-2023 15:00; Financial Plans: [Financial Plan A][Financial Plan B]; Tags:` + +![result for`schedule 1 ap/Annual review of financial goals d/20-11-2023 15:00'](images/scheduleUi.png) + +
:information_source: +Upon triggering the overriding prompt, until confirmation or cancellation of command on the prompt, usage of the application +is temporarily halted (including trying to exit the program). +
+ +---------- + +### Completing an Appointment : `complete` + +Completes an appointment either with the contact at the specified `INDEX` or complete all appointments with matching `APPOINTMENT_DATE`. + +Format: `complete [INDEX] [d/APPOINTMENT_DATE]` + +- Either an **index or appointment date** must be provided for command to execute, but **not both**. +- If you input an `INDEX`, the command will complete appointment with the client at the specified index. +- The index refers to the index number shown in the displayed contact list. +- If you input an `APPOINTMENT_DATE`, the command will complete all appointments in contact book that have a date +matching the one input by user, **and is regardless of the currently displayed contact list**. +- After performing the complete, the contact list displayed will be reset to display all contacts in the contact book. + +
:information_source: +**Do note** an appointment's date is considered to be a match with user's input `APPOINTMENT_DATE` if the **year, month and day are the same**. Time of the appointment does not matter in this command. +
+ +Acceptable Values: Refer to [Argument Summary](#argument-summary). + +
:bulb: **Tip** +To save time, you may find it useful to `complete` by `APPOINTMENT_DATE` once at the end of the day. This will clear all your completed appointments for the entire day. +
+ +Examples: +- `complete 1` +- `complete d/01-05-2023` + +Successful Output: `Appointment(s) Completed!` + +![result for 'complete 1'](images/completeUi.png) + +---------- ### Clearing all entries : `clear` -Clears all entries from the address book. +Clears all client contacts from the contact book. UNOFAS will ask for confirmation first to ensure it is not a mistake. Click +the clear button to confirm. Format: `clear` +* If your computer runs windows, you may use the arrow keys to navigate between the clear and cancel buttons and the enter key to confirm your selection. + +Example: +* `clear` + +![confirm clear window](images/confirmClear.png) + +
:information_source: +Upon entering the `clear` command, until confirmation or cancellation of command on the prompt, usage of the application +is temporarily halted (including trying to exit the program). +
+ +---------------------------- +### Sorting of data : `sort` + +Sorts all the client contacts with predefined sorting functionalities. After sorting the list, the ordering of the contacts +will be changed. As a result, performing any operations that require indexing (such as delete, add or edit), will reference the new ordering that is currently displayed on the screen. + +**Here are the current predefined sorting functions that have been implemented** + +* `name` : sorts list by lexicographical ordering of client name (case-insensitive). +* `appointment`: sorts list by appointment timing in order of the earliest appointment first. If no appointment is found, the remaining clients without an appointment will be displayed in an arbitrary order, based on the reordering of the previous sorting functions applied. + +Format: `sort KEYWORD` + +* Calling this command after a `Find` command will preserve the results filtered by the `Find` command. + +Acceptable Values: Refer to [Argument Summary](#argument-summary). + +Example: `sort name` performs sorting by lexicographical ordering + +Successful Output: `4 persons listed!` + +![result for`sort name'](images/sortUi.png) + +------------ ### Exiting the program : `exit` Exits the program. 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. +UNOFAS data are saved in a `.json` file in the data folder found in the same folder the application is in. Data is written each time a command that alters the data in the list is executed. +There is no need to save manually. +------------ ### Editing the data file -AddressBook data are saved automatically as a JSON file `[JAR file location]/data/addressbook.json`. Advanced users are welcome to update data directly by editing that data file. +UNOFAS data are saved automatically as a JSON file `[JAR file location]/data/addressbook.json`. Advanced users are welcome to update data directly by editing that data file. -
:exclamation: **Caution:** -If your changes to the data file makes its format invalid, AddressBook will discard all data and start with an empty data file at the next run. Hence, it is recommended to take a backup of the file before editing it. +
:exclamation: **Caution:** +If your changes to the data file makes its format invalid, UNOFAS will discard all data and start with an empty data file at the next run. +Certain edits to the file may also cause unexpected behaviours. Please only edit the file if you are confident that it is correct, and it is recommended to take a backup of the file before editing it.
-### Archiving data files `[coming in v2.0]` - -_Details coming soon ..._ - -------------------------------------------------------------------------------------------------------------------- ## FAQ -**Q**: How do I transfer my data to another Computer?
-**A**: Install the app in the other computer and overwrite the empty data file it creates with the file that contains the data of your previous AddressBook home folder. +**Q**: How do I transfer my data to another computer?
+**A**: Install the app in the other computer and overwrite the empty data file it creates with the file that contains the data of your previous UNOFAS home folder. -------------------------------------------------------------------------------------------------------------------- ## Known issues 1. **When using multiple screens**, if you move the application to a secondary screen, and later switch to using only the primary screen, the GUI will open off-screen. The remedy is to delete the `preferences.json` file created by the application before running the application again. +2. **When sorting the list**, we have chosen to not implement returning sorted list to original ordering due to the lack of necessity. However, due to feedback, we intend to implement this in the next release to enable users to return list to original order should they wish to. +3. **It is possible to add appointments with dates** before the current date and time. +4. **On macOS Systems**, you have to use the cursor to manually click the confirm or cancel buttons for the overriding and clear prompts. In contrast, Windows users can choose to hit enter to confirm execution of command. +5. **On macOS Systems**, executing clear and appointment override might result in some slight UI bugs when application is used in full screen. Don't worry as the program should still work with intended functionality. +6. **No current method to de-conflict clashing appointments**. Users should be advised to check the appointment list to ensure appointments do not clash with each other. +7. **The appointment list** may display appointments with the same date and time in a different order after adding a new appointment and subsequently reopening the app. This will be concurrently resolved when fixing issue 6. +8. **Checking for duplicate clients** is done by checking their full name, case-sensitive. The future plan is to do this by checking of phone number as it is less likely 2 people share the same phone number than compared to name. -------------------------------------------------------------------------------------------------------------------- ## Command summary -Action | Format, Examples ---------|------------------ -**Add** | `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]…​`
e.g., `add n/James Ho p/22224444 e/jamesho@example.com a/123, Clementi Rd, 1234665 t/friend t/colleague` -**Clear** | `clear` -**Delete** | `delete INDEX`
e.g., `delete 3` -**Edit** | `edit INDEX [n/NAME] [p/PHONE_NUMBER] [e/EMAIL] [a/ADDRESS] [t/TAG]…​`
e.g.,`edit 2 n/James Lee e/jameslee@example.com` -**Find** | `find KEYWORD [MORE_KEYWORDS]`
e.g., `find James Jake` -**List** | `list` -**Help** | `help` +| Action | Format, Examples | +|--------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **Add** | `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS nk/NEXT_KIN nkp/NEXT_KIN_PHONE [fp/FINANCIAL_PLAN]…​ [t/TAG]…​`
e.g., `add n/John p/80101010 e/johndoe@gmail.com a/Punggol Central Blk 444 #15-32 820123 nk/Brennan nkp/82020202` | +| **Clear** | `clear` | +| **Delete** | `delete INDEX`
e.g., `delete 3` | +| **Edit** | `edit INDEX [n/NAME] [p/PHONE_NUMBER] [e/EMAIL] [a/ADDRESS] [nk/NEXT_KIN] [nkp/NEXT_KIN_PHONE] [fp/FINANCIAL_PLAN]…​ [t/TAG]…​`
e.g.,`edit 1 n/john doe a/23 woodlands ave 123` | +| **Find** | `find [n/NAME]…​ [fp/FINANCIAL_PLAN]…​ [t/TAG]…​`
e.g., `find n/James n/Jake` | +| **Gather** | `gather [fp/FINANCIAL PLAN]` or `gather [t/TAG]`
e.g., `gather fp/Basic Insurance Plan` | +| **Schedule** | `schedule INDEX ap/APPOINTMENT_NAME d/APPOINTMENT_DATE_TIME`
e.g. `schedule 1 ap/Annual review of financial goals d/20-11-2023 15:00` | +| **Complete** | `complete [INDEX] [d/APPOINTMENT_DATE]`
e.g `complete 1`
e.g `complete d/01-05-2023` | | +| **List** | `list` | +| **Help** | `help` | +| **Sort** | `sort KEYWORD`
e.g., `sort appointment` | + diff --git a/docs/_config.yml b/docs/_config.yml index 6bd245d8f4e..08a117def63 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -1,4 +1,4 @@ -title: "AB-3" +title: "UNOFAS" theme: minima header_pages: @@ -8,7 +8,7 @@ header_pages: markdown: kramdown -repository: "se-edu/addressbook-level3" +repository: "AY2324S1-CS2103T-F12-1/tp" github_icon: "images/github-icon.png" plugins: diff --git a/docs/_sass/minima/_base.scss b/docs/_sass/minima/_base.scss index 0d3f6e80ced..a02466e5a76 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: "UNOFAS"; font-size: 32px; } } diff --git a/docs/diagrams/AppointmentListSequenceDiagram.puml b/docs/diagrams/AppointmentListSequenceDiagram.puml new file mode 100644 index 00000000000..03ae491fa78 --- /dev/null +++ b/docs/diagrams/AppointmentListSequenceDiagram.puml @@ -0,0 +1,33 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box Model MODEL_COLOR_T1 +participant ":ModelManager" as ModelManager MODEL_COLOR +participant ":ObservableList" as observableAppointments MODEL_COLOR +end box + +[-> ModelManager: setPerson(personToEdit, editedPerson) +activate ModelManager + +ModelManager -> ModelManager: setAppointmentList() +activate ModelManager + +ModelManager -> observableAppointments: clear() +activate observableAppointments +observableAppointments --> ModelManager +deactivate observableAppointments + +loop through filteredPersons + ModelManager -> ModelManager: addToAppointmentListIfPresent(person) + activate ModelManager + ModelManager -> observableAppointments: add(appointment) + activate observableAppointments + deactivate observableAppointments + deactivate ModelManager +end loop + +deactivate ModelManager +[<--ModelManager +deactivate ModelManager +@enduml diff --git a/docs/diagrams/CombinedPredicateClassDiagram.puml b/docs/diagrams/CombinedPredicateClassDiagram.puml new file mode 100644 index 00000000000..c9125db19a3 --- /dev/null +++ b/docs/diagrams/CombinedPredicateClassDiagram.puml @@ -0,0 +1,18 @@ +@startuml +!include style.puml +skinparam arrowThickness 1.1 +skinparam arrowColor LOGIC_COLOR_T4 +skinparam classBackgroundColor LOGIC_COLOR + +Class "<>\nPredicate " as PredicatePerson +Class "<>\nPersonContainsKeywordsPredicate" as PersonContainsKeywordsPredicate +CombinedPredicate .up.|> PredicatePerson +PersonContainsKeywordsPredicate -up-|> PredicatePerson +CombinedPredicate---> "*"PersonContainsKeywordsPredicate +XYZContainsKeywordsPredicate .up.|> PersonContainsKeywordsPredicate + + +FindCommandParser ..> CombinedPredicate : creates > +FindCommandParser ..> FindCommand : creates > +FindCommand-->CombinedPredicate +@enduml diff --git a/docs/diagrams/CompleteActivityDiagram.puml b/docs/diagrams/CompleteActivityDiagram.puml new file mode 100644 index 00000000000..12eecdd152c --- /dev/null +++ b/docs/diagrams/CompleteActivityDiagram.puml @@ -0,0 +1,40 @@ +@startuml +skin rose +skinparam ActivityFontSize 15 +skinparam ArrowFontSize 12 +start +:User enters a complete command; + +:CompleteCommandParser parses the user input and checks validity; + + +'Since the beta syntax does not support placing the condition outside the +'diamond we place it as the true branch instead. + +switch () +case([the complete command is valid]) + : Creates a CompleteCommand which is executed by LogicManager; + switch () + case ([user inputs an index]) + : Checks for appointment of given person in FilterPersonList; + switch () + case([if person has existing appointment]) + : Clears appointment for the Person; + case([if person has no existing appointment]) + : Throws an error; + endswitch + case ([user inputs a date]) + : Checks for persons in FilterPersonList who appointment date matches input date; + switch() + case([have at least one match]) + : Clears all appointments with matching dates; + case([has no matches]) + : Throws an error; + endswitch + endswitch +case([else]) + : Throws an error; +endswitch +stop +@enduml + diff --git a/docs/diagrams/CompleteClassDiagram.puml b/docs/diagrams/CompleteClassDiagram.puml new file mode 100644 index 00000000000..9432bd61ced --- /dev/null +++ b/docs/diagrams/CompleteClassDiagram.puml @@ -0,0 +1,29 @@ +@startuml +hide empty methods +hide empty attributes +hide circle +skinparam class { + BackgroundColor #3333C4 + FontColor #FFFFFF + FontSize 15 +} + +abstract class CompleteCommand {} + +class CompleteByDate { + date : LocalDateTime +} + +class CompleteByIndex { + index : Index +} + +abstract class Command {} + +CompleteByDate -up-|> CompleteCommand +CompleteByIndex -up-|> CompleteCommand + +CompleteCommand -up-|> Command +CompleteByDate -[hidden]left-> CompleteByIndex + +@enduml diff --git a/docs/diagrams/CompleteSequenceDiagram.puml b/docs/diagrams/CompleteSequenceDiagram.puml new file mode 100644 index 00000000000..44dd6ede003 --- /dev/null +++ b/docs/diagrams/CompleteSequenceDiagram.puml @@ -0,0 +1,72 @@ +@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 ":CompleteCommandParser" as CompleteCommandParser LOGIC_COLOR +participant "c:CompleteByDate" as CompleteByDate 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("complete d/01-01-2023") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("complete d/01-01-2023")") +activate AddressBookParser + +create CompleteCommandParser +AddressBookParser -> CompleteCommandParser +activate CompleteCommandParser + +CompleteCommandParser --> AddressBookParser +deactivate CompleteCommandParser + +AddressBookParser -> CompleteCommandParser : parse("d/01-01-2023") +activate CompleteCommandParser + +create CompleteByDate +CompleteCommandParser -> CompleteByDate +activate CompleteByDate + +CompleteByDate --> CompleteCommandParser : c +deactivate CompleteByDate + +CompleteCommandParser --> AddressBookParser : c +deactivate CompleteCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +CompleteCommandParser -[hidden]-> AddressBookParser +destroy CompleteCommandParser + +AddressBookParser --> LogicManager : c +deactivate AddressBookParser + +LogicManager -> CompleteByDate : execute() +activate CompleteByDate + +CompleteByDate -> Model : clear appointments by date +activate Model + +Model --> CompleteByDate +deactivate Model + +create CommandResult +CompleteByDate -> CommandResult +activate CommandResult + +CommandResult --> CompleteByDate +deactivate CommandResult + +CompleteByDate --> LogicManager : result +deactivate CompleteByDate + +CompleteByDate -[hidden]-> CompleteCommandParser + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/FindCommandSequenceDiagram.puml b/docs/diagrams/FindCommandSequenceDiagram.puml new file mode 100644 index 00000000000..8d7bbe4517e --- /dev/null +++ b/docs/diagrams/FindCommandSequenceDiagram.puml @@ -0,0 +1,42 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box Logic LOGIC_COLOR_T1 +participant ":Logic" as Logic LOGIC_COLOR +participant "command:FindCommand" as FindCommand LOGIC_COLOR +participant "commandResult:CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant "model:Model" as Model MODEL_COLOR +end box + +[-> Logic : execute(command) +activate Logic + +Logic -> FindCommand : execute(model) +activate FindCommand + +FindCommand -> Model: updateFilteredPersonList(predicate) +activate Model + +Model --> FindCommand: +deactivate Model + +create CommandResult +FindCommand -> CommandResult +activate CommandResult + +CommandResult --> FindCommand +deactivate CommandResult + +FindCommand --> Logic: commandResult +deactivate FindCommand + +CommandResult -[hidden]-> Model +destroy FindCommand + +[<--Logic : commandResult +deactivate Logic +@enduml diff --git a/docs/diagrams/GatherClassActivityDiagram.puml b/docs/diagrams/GatherClassActivityDiagram.puml new file mode 100644 index 00000000000..590dad6ca20 --- /dev/null +++ b/docs/diagrams/GatherClassActivityDiagram.puml @@ -0,0 +1,24 @@ +@startuml +skin rose +skinparam ActivityFontSize 15 +skinparam ArrowFontSize 12 +start +:User executes gather command; + +:User input is parsed; + +'Since the beta syntax does not support placing the condition outside the +'diamond we place it as the true branch instead. + +switch () +case([prefix == "t/"]) + :Gathers all email addresses of + clients with given tag; +case([prefix == "fp/"]) + :Gathers all email addresses of + clients with given financial plan; +case([else]) + :Throws error message; +endswitch +stop +@enduml diff --git a/docs/diagrams/GatherClassDiagram.puml b/docs/diagrams/GatherClassDiagram.puml new file mode 100644 index 00000000000..d39479002e9 --- /dev/null +++ b/docs/diagrams/GatherClassDiagram.puml @@ -0,0 +1,18 @@ +@startuml +!include style.puml +skinparam arrowThickness 1.1 +skinparam arrowColor MODEL_COLOR +skinparam classBackgroundColor MODEL_COLOR + +Class GatherCommandParser +Class GatherCommand +Class "<>\nGatherEmailPrompt" as GatherEmailPrompt +Class GatherEmailByFinancialPlan +Class GatherEmailByTag + +GatherCommandParser .down.> GatherCommand : creates > +GatherCommandParser .down.> GatherEmailPrompt : creates > +GatherCommand -right-> "1" GatherEmailPrompt +GatherEmailByFinancialPlan .up.|> GatherEmailPrompt +GatherEmailByTag .up.|> GatherEmailPrompt +@enduml diff --git a/docs/diagrams/GatherSequenceDiagram1.puml b/docs/diagrams/GatherSequenceDiagram1.puml new file mode 100644 index 00000000000..ac8d6fd524e --- /dev/null +++ b/docs/diagrams/GatherSequenceDiagram1.puml @@ -0,0 +1,63 @@ +@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 ":GatherCommandParser" as GatherCommandParser LOGIC_COLOR +participant "command:GatherCommand" as GatherCommand 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("gather fp/Financial Plan A") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("gather fp/Financial Plan A") +activate AddressBookParser + +AddressBookParser -> GatherCommandParser : parse("fp/Financial Plan A") +activate GatherCommandParser + +create GatherCommand +GatherCommandParser -> GatherCommand : GatherCommand(prompt) +activate GatherCommand + +GatherCommand --> GatherCommandParser : command +deactivate GatherCommand + +GatherCommandParser --> AddressBookParser : command +deactivate GatherCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +GatherCommandParser -[hidden]-> AddressBookParser +destroy GatherCommandParser + +AddressBookParser --> LogicManager : command +deactivate AddressBookParser + +LogicManager -> GatherCommand : execute() +activate GatherCommand + +GatherCommand -> Model: gatherEmails(prompt) +activate Model + +Model --> GatherCommand: emails (or Empty String) +deactivate Model + +create CommandResult +GatherCommand -> CommandResult +activate CommandResult + +CommandResult --> GatherCommand +deactivate CommandResult + +GatherCommand --> LogicManager: result +deactivate GatherCommand + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/GatherSequenceDiagram2.puml b/docs/diagrams/GatherSequenceDiagram2.puml new file mode 100644 index 00000000000..3b7ebf5354a --- /dev/null +++ b/docs/diagrams/GatherSequenceDiagram2.puml @@ -0,0 +1,41 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box Model MODEL_COLOR_T1 +participant ":ModelManager" as ModelManager MODEL_COLOR +participant ":AddressBook" as AddressBook MODEL_COLOR +participant ":UniquePersonsList" as UniquePersonsList MODEL_COLOR +participant "prompt:GatherEmailByFinancialPlan" as GatherEmailByFinancialPlan MODEL_COLOR +participant ":Person" as Person MODEL_COLOR +end box + +[-> ModelManager: gatherEmails(prompt) +activate ModelManager + +ModelManager -> AddressBook: gatherEmails(prompt) +activate AddressBook + +AddressBook -> UniquePersonsList: gatherEmails(prompt) +activate UniquePersonsList + +loop through internalList + UniquePersonsList -> GatherEmailByFinancialPlan: gatherEmails(person) + activate GatherEmailByFinancialPlan + GatherEmailByFinancialPlan -> Person: gatherEmailsContainsFinancialPlan("Financial Plan A") + activate Person + Person --> GatherEmailByFinancialPlan: email (or Empty String) + deactivate Person + GatherEmailByFinancialPlan --> UniquePersonsList: email (or Empty String) + deactivate GatherEmailByFinancialPlan +end loop + +UniquePersonsList --> AddressBook: emails (or Empty String) +deactivate UniquePersonsList + +AddressBook --> ModelManager: emails (or Empty String) +deactivate AddressBook + +[<-- ModelManager : emails (or Empty String) +deactivate ModelManager +@enduml diff --git a/docs/diagrams/ModelClassDiagram.puml b/docs/diagrams/ModelClassDiagram.puml index 0de5673070d..5d6b0d69b49 100644 --- a/docs/diagrams/ModelClassDiagram.puml +++ b/docs/diagrams/ModelClassDiagram.puml @@ -19,7 +19,10 @@ Class Email Class Name Class Phone Class Tag - +Class FinancialPlan +abstract Class ScheduleItem +Class NextOfKinName +Class NextOfKinPhone Class I #FFFFFF } @@ -42,6 +45,10 @@ Person *--> Phone Person *--> Email Person *--> Address Person *--> "*" Tag +Person *--> "*" FinancialPlan +Person *--> ScheduleItem +Person *--> NextOfKinName +Person *--> NextOfKinPhone Person -[hidden]up--> I UniquePersonList -[hidden]right-> I diff --git a/docs/diagrams/ScheduleActivityDiagram.puml b/docs/diagrams/ScheduleActivityDiagram.puml new file mode 100644 index 00000000000..149e8baa80f --- /dev/null +++ b/docs/diagrams/ScheduleActivityDiagram.puml @@ -0,0 +1,31 @@ +@startuml +skin rose +skinparam ActivityFontSize 15 +skinparam ArrowFontSize 12 +start +:User enters a schedule command; + +:ScheduleCommandParser parses the user input and checks validity; + + +'Since the beta syntax does not support placing the condition outside the +'diamond we place it as the true branch instead. + +switch () +case([the schedule command is valid]) + : Creates a ScheduleCommand which is executed by Logic Manager; + switch() + case([person has an appointment]) + : Prompt user to decide if they wish to override; + if () then ([user continues]) + else ([user cancels]) + stop; + endif; + case([else]) + endswitch + : Updates Person in FilterPersonList to have the scheduled appointment; +case([else]) + : Throws an error; +endswitch +stop +@enduml diff --git a/docs/diagrams/ScheduleClassDiagram.puml b/docs/diagrams/ScheduleClassDiagram.puml new file mode 100644 index 00000000000..08fd3ff9758 --- /dev/null +++ b/docs/diagrams/ScheduleClassDiagram.puml @@ -0,0 +1,29 @@ +@startuml +!include style.puml +skinparam arrowThickness 1.1 +skinparam arrowColor LOGIC_COLOR_T4 +skinparam classBackgroundColor LOGIC_COLOR + +Package Schedule as SchedulePackage<>{ + +Class LogicManager +Class AddressBookParser +Class ScheduleCommandParser +Class ScheduleCommand +Class Index +Class CommandResult + + +LogicManager -down- "1" ScheduleCommand : executes > +LogicManager -right-> "1" AddressBookParser +AddressBookParser ..> ScheduleCommandParser : creates > +ScheduleCommand -right-> "1" Appointment +ScheduleCommandParser ..> ScheduleCommand : creates > +ScheduleCommand -right-> "1" Index +ScheduleCommand ..down> CommandResult : produces > + + +Appointment -[hidden]down-> Index +} + +@enduml diff --git a/docs/diagrams/ScheduleItemClassDiagram.puml b/docs/diagrams/ScheduleItemClassDiagram.puml new file mode 100644 index 00000000000..0432bf55b97 --- /dev/null +++ b/docs/diagrams/ScheduleItemClassDiagram.puml @@ -0,0 +1,31 @@ +@startuml +hide empty methods +hide empty attributes +hide circle +skinparam class { + BackgroundColor #9D0012 + FontColor #FFFFFF + FontSize 15 +} + +abstract class ScheduleItem {} +class Appointment { + value : String + date : LocalDateTime + person : Person +} + +class NullAppointment <> { + value : String +} +class Person {} + +Appointment -up-|> ScheduleItem +NullAppointment -up-|> ScheduleItem + +Person "1" *-down-> "1" ScheduleItem +Person -[hidden]down-> NullAppointment +Person -[hidden]down-> ScheduleItem +Appointment -[hidden]left-> NullAppointment + +@enduml diff --git a/docs/diagrams/SortClassActivityDiagram.puml b/docs/diagrams/SortClassActivityDiagram.puml new file mode 100644 index 00000000000..deffa43e8de --- /dev/null +++ b/docs/diagrams/SortClassActivityDiagram.puml @@ -0,0 +1,22 @@ +@startuml +skin rose +skinparam ActivityFontSize 15 +skinparam ArrowFontSize 12 +start +:User executes sort command; + +:User input is parsed; + +'Since the beta syntax does not support placing the condition outside the +'diamond we place it as the true branch instead. + +switch () +case([input == "name"]) + :Sorts the list by name lexicographically; +case([input == "appointment"]) + :Sorts the list by appointment; +case([else]) + :Throws error message; +endswitch +stop +@enduml diff --git a/docs/diagrams/SortClassSequenceDiagram.puml b/docs/diagrams/SortClassSequenceDiagram.puml new file mode 100644 index 00000000000..4c97a04455d --- /dev/null +++ b/docs/diagrams/SortClassSequenceDiagram.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 ":SortCommandParser" as SortCommandParser LOGIC_COLOR +participant "command:SortCommand" as SortCommand LOGIC_COLOR +participant ":CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +participant ":Person" as Person MODEL_COLOR + +end box + +[-> LogicManager : execute("sort appointment") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("sort appointment") +activate AddressBookParser + +AddressBookParser -> SortCommandParser: parse("appointment") +activate SortCommandParser + +create SortCommand +SortCommandParser -> SortCommand: SortCommand(new SortByAppointmentComparator) +activate SortCommand + +SortCommand --> SortCommandParser +deactivate SortCommand + +SortCommandParser --> AddressBookParser +deactivate SortCommandParser +SortCommandParser -[hidden]-> AddressBookParser +destroy SortCommandParser + +AddressBookParser --> LogicManager +deactivate AddressBookParser + +LogicManager -> SortCommand: execute() +activate SortCommand +SortCommand -> Model: model.sortFilteredPersonList(comparator); +activate Model + +Model -> Person : sortFilteredPersonList(SortByAppointmentComparator) +activate Person +deactivate Person + +Model --> SortCommand +deactivate Model + +create CommandResult +SortCommand -> CommandResult +activate CommandResult +CommandResult --> SortCommand +deactivate CommandResult +SortCommand --> LogicManager +deactivate SortCommand + +SortCommandParser -[hidden]-> AddressBookParser +destroy SortCommand + +[<-- LogicManager +deactivate LogicManager + + +@enduml diff --git a/docs/diagrams/StorageClassDiagram.puml b/docs/diagrams/StorageClassDiagram.puml index a821e06458c..8815ad22dd0 100644 --- a/docs/diagrams/StorageClassDiagram.puml +++ b/docs/diagrams/StorageClassDiagram.puml @@ -20,6 +20,8 @@ Class JsonAddressBookStorage Class JsonSerializableAddressBook Class JsonAdaptedPerson Class JsonAdaptedTag +Class JsonAdaptedFinancialPlan +Class JsonAdaptedAppointment } } @@ -39,5 +41,11 @@ JsonAddressBookStorage .up.|> AddressBookStorage JsonAddressBookStorage ..> JsonSerializableAddressBook JsonSerializableAddressBook --> "*" JsonAdaptedPerson JsonAdaptedPerson --> "*" JsonAdaptedTag +JsonAdaptedPerson --> "1" JsonAdaptedAppointment +JsonAdaptedPerson --> "*" JsonAdaptedFinancialPlan + + +JsonAdaptedFinancialPlan -[hidden]up-> JsonAdaptedTag +JsonAdaptedAppointment -[hidden]left-> JsonAdaptedFinancialPlan @enduml diff --git a/docs/diagrams/UiClassDiagram.puml b/docs/diagrams/UiClassDiagram.puml index 95473d5aa19..d2773893d57 100644 --- a/docs/diagrams/UiClassDiagram.puml +++ b/docs/diagrams/UiClassDiagram.puml @@ -13,8 +13,12 @@ Class HelpWindow Class ResultDisplay Class PersonListPanel Class PersonCard +Class AppointmentListPanel +Class AppointmentCard Class StatusBarFooter Class CommandBox +Class OverrideWindow +Class ClearWindow } package Model <> { @@ -33,10 +37,14 @@ UiManager -down-> "1" MainWindow MainWindow *-down-> "1" CommandBox MainWindow *-down-> "1" ResultDisplay MainWindow *-down-> "1" PersonListPanel +MainWindow *-down-> "1" AppointmentListPanel MainWindow *-down-> "1" StatusBarFooter MainWindow --> "0..1" HelpWindow +MainWindow --> "0..1" OverrideWindow +MainWindow --> "0..1" ClearWindow PersonListPanel -down-> "*" PersonCard +AppointmentListPanel -down-> "*" AppointmentCard MainWindow -left-|> UiPart @@ -44,10 +52,15 @@ ResultDisplay --|> UiPart CommandBox --|> UiPart PersonListPanel --|> UiPart PersonCard --|> UiPart +AppointmentListPanel --|> UiPart +AppointmentCard --|> UiPart StatusBarFooter --|> UiPart HelpWindow --|> UiPart +OverrideWindow --|> UiPart +ClearWindow --|> UiPart PersonCard ..> Model +AppointmentCard ..> Model UiManager -right-> Logic MainWindow -left-> Logic diff --git a/docs/diagrams/UndoRedoState0.puml b/docs/diagrams/UndoRedoState0.puml deleted file mode 100644 index 43a45903ac9..00000000000 --- a/docs/diagrams/UndoRedoState0.puml +++ /dev/null @@ -1,21 +0,0 @@ -@startuml -!include style.puml -skinparam ClassFontColor #000000 -skinparam ClassBorderColor #000000 -skinparam ClassBackgroundColor #FFFFAA - -title Initial state - -package States { - class State1 as "ab0:AddressBook" - class State2 as "ab1:AddressBook" - class State3 as "ab2:AddressBook" -} -State1 -[hidden]right-> State2 -State2 -[hidden]right-> State3 -hide State2 -hide State3 - -class Pointer as "Current State" #FFFFFF -Pointer -up-> State1 -@end diff --git a/docs/diagrams/UndoRedoState1.puml b/docs/diagrams/UndoRedoState1.puml deleted file mode 100644 index 5a41e9e1651..00000000000 --- a/docs/diagrams/UndoRedoState1.puml +++ /dev/null @@ -1,23 +0,0 @@ -@startuml -!include style.puml -skinparam ClassFontColor #000000 -skinparam ClassBorderColor #000000 -skinparam ClassBackgroundColor #FFFFAA - -title After command "delete 5" - -package States <> { - class State1 as "ab0:AddressBook" - class State2 as "ab1:AddressBook" - class State3 as "ab2:AddressBook" -} - -State1 -[hidden]right-> State2 -State2 -[hidden]right-> State3 - -hide State3 - -class Pointer as "Current State" #FFFFFF - -Pointer -up-> State2 -@end diff --git a/docs/diagrams/UndoRedoState2.puml b/docs/diagrams/UndoRedoState2.puml deleted file mode 100644 index ad32fce1b0b..00000000000 --- a/docs/diagrams/UndoRedoState2.puml +++ /dev/null @@ -1,21 +0,0 @@ -@startuml -!include style.puml -skinparam ClassFontColor #000000 -skinparam ClassBorderColor #000000 -skinparam ClassBackgroundColor #FFFFAA - -title After command "add n/David" - -package States <> { - class State1 as "ab0:AddressBook" - class State2 as "ab1:AddressBook" - class State3 as "ab2:AddressBook" -} - -State1 -[hidden]right-> State2 -State2 -[hidden]right-> State3 - -class Pointer as "Current State" #FFFFFF - -Pointer -up-> State3 -@end diff --git a/docs/diagrams/UndoRedoState3.puml b/docs/diagrams/UndoRedoState3.puml deleted file mode 100644 index 9187a690036..00000000000 --- a/docs/diagrams/UndoRedoState3.puml +++ /dev/null @@ -1,21 +0,0 @@ -@startuml -!include style.puml -skinparam ClassFontColor #000000 -skinparam ClassBorderColor #000000 -skinparam ClassBackgroundColor #FFFFAA - -title After command "undo" - -package States <> { - class State1 as "ab0:AddressBook" - class State2 as "ab1:AddressBook" - class State3 as "ab2:AddressBook" -} - -State1 -[hidden]right-> State2 -State2 -[hidden]right-> State3 - -class Pointer as "Current State" #FFFFFF - -Pointer -up-> State2 -@end diff --git a/docs/diagrams/UndoRedoState4.puml b/docs/diagrams/UndoRedoState4.puml deleted file mode 100644 index 2bc631ffcd0..00000000000 --- a/docs/diagrams/UndoRedoState4.puml +++ /dev/null @@ -1,21 +0,0 @@ -@startuml -!include style.puml -skinparam ClassFontColor #000000 -skinparam ClassBorderColor #000000 -skinparam ClassBackgroundColor #FFFFAA - -title After command "list" - -package States <> { - class State1 as "ab0:AddressBook" - class State2 as "ab1:AddressBook" - class State3 as "ab2:AddressBook" -} - -State1 -[hidden]right-> State2 -State2 -[hidden]right-> State3 - -class Pointer as "Current State" #FFFFFF - -Pointer -up-> State2 -@end diff --git a/docs/diagrams/UndoRedoState5.puml b/docs/diagrams/UndoRedoState5.puml deleted file mode 100644 index e77b04104aa..00000000000 --- a/docs/diagrams/UndoRedoState5.puml +++ /dev/null @@ -1,22 +0,0 @@ -@startuml -!include style.puml -skinparam ClassFontColor #000000 -skinparam ClassBorderColor #000000 -skinparam ClassBackgroundColor #FFFFAA - -title After command "clear" - -package States <> { - class State1 as "ab0:AddressBook" - class State2 as "ab1:AddressBook" - class State3 as "ab3:AddressBook" -} - -State1 -[hidden]right-> State2 -State2 -[hidden]right-> State3 - -class Pointer as "Current State" #FFFFFF - -Pointer -up-> State3 -note right on link: State ab2 deleted. -@end diff --git a/docs/diagrams/UndoSequenceDiagram.puml b/docs/diagrams/UndoSequenceDiagram.puml deleted file mode 100644 index 87ff3e9237e..00000000000 --- a/docs/diagrams/UndoSequenceDiagram.puml +++ /dev/null @@ -1,54 +0,0 @@ -@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 "u:UndoCommand" as UndoCommand LOGIC_COLOR -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 - -LogicManager -> AddressBookParser : parseCommand(undo) -activate AddressBookParser - -create UndoCommand -AddressBookParser -> UndoCommand -activate UndoCommand - -UndoCommand --> AddressBookParser -deactivate UndoCommand - -AddressBookParser --> LogicManager : u -deactivate AddressBookParser - -LogicManager -> UndoCommand : execute() -activate UndoCommand - -UndoCommand -> Model : undoAddressBook() -activate Model - -Model -> VersionedAddressBook : undo() -activate VersionedAddressBook - -VersionedAddressBook -> VersionedAddressBook :resetData(ReadOnlyAddressBook) -VersionedAddressBook --> Model : -deactivate VersionedAddressBook - -Model --> UndoCommand -deactivate Model - -UndoCommand --> LogicManager : result -deactivate UndoCommand -UndoCommand -[hidden]-> LogicManager : result -destroy UndoCommand - -[<--LogicManager -deactivate LogicManager -@enduml diff --git a/docs/images/AppointmentListSequenceDiagram.png b/docs/images/AppointmentListSequenceDiagram.png new file mode 100644 index 00000000000..dfacf9f2c19 Binary files /dev/null and b/docs/images/AppointmentListSequenceDiagram.png differ diff --git a/docs/images/CombinedPredicateClassDiagram.png b/docs/images/CombinedPredicateClassDiagram.png new file mode 100644 index 00000000000..cf8802b9349 Binary files /dev/null and b/docs/images/CombinedPredicateClassDiagram.png differ diff --git a/docs/images/CommitActivityDiagram.png b/docs/images/CommitActivityDiagram.png index 5b464126b35..f779297d006 100644 Binary files a/docs/images/CommitActivityDiagram.png and b/docs/images/CommitActivityDiagram.png differ diff --git a/docs/images/CompleteActivityDiagram.png b/docs/images/CompleteActivityDiagram.png new file mode 100644 index 00000000000..348e3a7a20c Binary files /dev/null and b/docs/images/CompleteActivityDiagram.png differ diff --git a/docs/images/CompleteClassDiagram.png b/docs/images/CompleteClassDiagram.png new file mode 100644 index 00000000000..52031a0cb77 Binary files /dev/null and b/docs/images/CompleteClassDiagram.png differ diff --git a/docs/images/CompleteSequenceDiagram.png b/docs/images/CompleteSequenceDiagram.png new file mode 100644 index 00000000000..cc8f6400c6f Binary files /dev/null and b/docs/images/CompleteSequenceDiagram.png differ diff --git a/docs/images/FindCommandSequenceDiagram.png b/docs/images/FindCommandSequenceDiagram.png new file mode 100644 index 00000000000..ef9404826a6 Binary files /dev/null and b/docs/images/FindCommandSequenceDiagram.png differ diff --git a/docs/images/GatherClassActivityDiagram.png b/docs/images/GatherClassActivityDiagram.png new file mode 100644 index 00000000000..2adfd6193e8 Binary files /dev/null and b/docs/images/GatherClassActivityDiagram.png differ diff --git a/docs/images/GatherClassDiagram.png b/docs/images/GatherClassDiagram.png new file mode 100644 index 00000000000..d9d0ffc23cb Binary files /dev/null and b/docs/images/GatherClassDiagram.png differ diff --git a/docs/images/GatherSequenceDiagram1.png b/docs/images/GatherSequenceDiagram1.png new file mode 100644 index 00000000000..7ca885cb970 Binary files /dev/null and b/docs/images/GatherSequenceDiagram1.png differ diff --git a/docs/images/GatherSequenceDiagram2.png b/docs/images/GatherSequenceDiagram2.png new file mode 100644 index 00000000000..ef37527bcad Binary files /dev/null and b/docs/images/GatherSequenceDiagram2.png differ diff --git a/docs/images/ListUi.png b/docs/images/ListUi.png new file mode 100644 index 00000000000..ec1c7f6a67e Binary files /dev/null and b/docs/images/ListUi.png differ diff --git a/docs/images/ModelClassDiagram.png b/docs/images/ModelClassDiagram.png index a19fb1b4ac8..1d144f80d39 100644 Binary files a/docs/images/ModelClassDiagram.png and b/docs/images/ModelClassDiagram.png differ diff --git a/docs/images/ScheduleActivityDiagram.png b/docs/images/ScheduleActivityDiagram.png new file mode 100644 index 00000000000..7f8097f3d23 Binary files /dev/null and b/docs/images/ScheduleActivityDiagram.png differ diff --git a/docs/images/ScheduleClassDiagram.png b/docs/images/ScheduleClassDiagram.png new file mode 100644 index 00000000000..0d81615aa6b Binary files /dev/null and b/docs/images/ScheduleClassDiagram.png differ diff --git a/docs/images/ScheduleItemClassDiagram.png b/docs/images/ScheduleItemClassDiagram.png new file mode 100644 index 00000000000..6fe172f985a Binary files /dev/null and b/docs/images/ScheduleItemClassDiagram.png differ diff --git a/docs/images/SortClassActivityDiagram.png b/docs/images/SortClassActivityDiagram.png new file mode 100644 index 00000000000..d395f25d995 Binary files /dev/null and b/docs/images/SortClassActivityDiagram.png differ diff --git a/docs/images/SortClassSequenceDiagram.png b/docs/images/SortClassSequenceDiagram.png new file mode 100644 index 00000000000..fb2a733681e Binary files /dev/null and b/docs/images/SortClassSequenceDiagram.png differ diff --git a/docs/images/StorageClassDiagram.png b/docs/images/StorageClassDiagram.png index 18fa4d0d51f..1b1de2f4edb 100644 Binary files a/docs/images/StorageClassDiagram.png and b/docs/images/StorageClassDiagram.png differ diff --git a/docs/images/Ui.png b/docs/images/Ui.png index 5bd77847aa2..94ecd868b14 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..b8c1c1f0387 100644 Binary files a/docs/images/UiClassDiagram.png and b/docs/images/UiClassDiagram.png differ diff --git a/docs/images/UndoRedoState0.png b/docs/images/UndoRedoState0.png deleted file mode 100644 index c5f91b58533..00000000000 Binary files a/docs/images/UndoRedoState0.png and /dev/null differ diff --git a/docs/images/UndoRedoState1.png b/docs/images/UndoRedoState1.png deleted file mode 100644 index 2d3ad09c047..00000000000 Binary files a/docs/images/UndoRedoState1.png and /dev/null differ diff --git a/docs/images/UndoRedoState2.png b/docs/images/UndoRedoState2.png deleted file mode 100644 index 20853694e03..00000000000 Binary files a/docs/images/UndoRedoState2.png and /dev/null differ diff --git a/docs/images/UndoRedoState3.png b/docs/images/UndoRedoState3.png deleted file mode 100644 index 1a9551b31be..00000000000 Binary files a/docs/images/UndoRedoState3.png and /dev/null differ diff --git a/docs/images/UndoRedoState4.png b/docs/images/UndoRedoState4.png deleted file mode 100644 index 46dfae78c94..00000000000 Binary files a/docs/images/UndoRedoState4.png and /dev/null differ diff --git a/docs/images/UndoRedoState5.png b/docs/images/UndoRedoState5.png deleted file mode 100644 index f45889b5fdf..00000000000 Binary files a/docs/images/UndoRedoState5.png and /dev/null differ diff --git a/docs/images/UndoSequenceDiagram.png b/docs/images/UndoSequenceDiagram.png deleted file mode 100644 index c7a7e637266..00000000000 Binary files a/docs/images/UndoSequenceDiagram.png and /dev/null differ diff --git a/docs/images/addUi.png b/docs/images/addUi.png new file mode 100644 index 00000000000..a658e10512e Binary files /dev/null and b/docs/images/addUi.png differ diff --git a/docs/images/alyssapng.png b/docs/images/alyssapng.png new file mode 100644 index 00000000000..09705a4cff8 Binary files /dev/null and b/docs/images/alyssapng.png differ diff --git a/docs/images/appointmentCardUi.png b/docs/images/appointmentCardUi.png new file mode 100644 index 00000000000..fba17c0ed4c Binary files /dev/null and b/docs/images/appointmentCardUi.png differ diff --git a/docs/images/completeUi.png b/docs/images/completeUi.png new file mode 100644 index 00000000000..b126e5ea65b Binary files /dev/null and b/docs/images/completeUi.png differ diff --git a/docs/images/confirmClear.png b/docs/images/confirmClear.png new file mode 100644 index 00000000000..bb641de05b9 Binary files /dev/null and b/docs/images/confirmClear.png differ diff --git a/docs/images/contactCardUi.png b/docs/images/contactCardUi.png new file mode 100644 index 00000000000..400000cd919 Binary files /dev/null and b/docs/images/contactCardUi.png differ diff --git a/docs/images/editUi.png b/docs/images/editUi.png new file mode 100644 index 00000000000..8038a8f05fb Binary files /dev/null and b/docs/images/editUi.png differ diff --git a/docs/images/findJohnCharlieResult.png b/docs/images/findJohnCharlieResult.png new file mode 100644 index 00000000000..ec1c7f6a67e Binary files /dev/null and b/docs/images/findJohnCharlieResult.png differ diff --git a/docs/images/gatherUi.png b/docs/images/gatherUi.png new file mode 100644 index 00000000000..861024b283c Binary files /dev/null and b/docs/images/gatherUi.png differ diff --git a/docs/images/generalUi.png b/docs/images/generalUi.png new file mode 100644 index 00000000000..513b333c035 Binary files /dev/null and b/docs/images/generalUi.png differ diff --git a/docs/images/helpMessage.png b/docs/images/helpMessage.png index b1f70470137..9350d1b971e 100644 Binary files a/docs/images/helpMessage.png and b/docs/images/helpMessage.png differ diff --git a/docs/images/jylow.png b/docs/images/jylow.png new file mode 100644 index 00000000000..8b23632244a Binary files /dev/null and b/docs/images/jylow.png differ diff --git a/docs/images/kb-tay.png b/docs/images/kb-tay.png new file mode 100644 index 00000000000..0131bee1aa1 Binary files /dev/null and b/docs/images/kb-tay.png differ diff --git a/docs/images/nikele2001.png b/docs/images/nikele2001.png new file mode 100644 index 00000000000..5637d524865 Binary files /dev/null and b/docs/images/nikele2001.png differ diff --git a/docs/images/schedulePrompt.png b/docs/images/schedulePrompt.png new file mode 100644 index 00000000000..f979ae69271 Binary files /dev/null and b/docs/images/schedulePrompt.png differ diff --git a/docs/images/scheduleUi.png b/docs/images/scheduleUi.png new file mode 100644 index 00000000000..ee79858ce3a Binary files /dev/null and b/docs/images/scheduleUi.png differ diff --git a/docs/images/sopa301.png b/docs/images/sopa301.png new file mode 100644 index 00000000000..a0c63796ab6 Binary files /dev/null and b/docs/images/sopa301.png differ diff --git a/docs/images/sortUi.png b/docs/images/sortUi.png new file mode 100644 index 00000000000..4bef45178f8 Binary files /dev/null and b/docs/images/sortUi.png differ diff --git a/docs/index.md b/docs/index.md index 7601dbaad0d..e9ceb161028 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,19 +1,20 @@ --- layout: page -title: AddressBook Level-3 +title: UNOFAS --- [![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) +[![codecov](https://codecov.io/gh/AY2324S1-CS2103T-F12-1/tp/branch/master/graph/badge.svg)](https://app.codecov.io/gh/AY2324S1-CS2103T-F12-1/tp) ![Ui](images/Ui.png) -**AddressBook is a desktop application for managing your contact details.** While it has a GUI, most of the user interactions happen using a CLI (Command Line Interface). +**UNOFAS for Financial Advisors to manage clients’ contacts and schedule appointments.** 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 UNOFAS, head over to the [_Quick Start_ section of the **User Guide**](UserGuide.html#quick-start). +* If you are interested about developing UNOFAS, the [**Developer Guide**](DeveloperGuide.html) is a good place to start. **Acknowledgements** +* This project is based on AddressBook-Level3 project created by [SE-EDU initiative](https://se-education.org). * Libraries used: [JavaFX](https://openjfx.io/), [Jackson](https://github.com/FasterXML/jackson), [JUnit5](https://github.com/junit-team/junit5) diff --git a/docs/team/alyssapng.md b/docs/team/alyssapng.md new file mode 100644 index 00000000000..0a73bc02b59 --- /dev/null +++ b/docs/team/alyssapng.md @@ -0,0 +1,61 @@ +--- +layout: page +title: Alyssa Png Kai Wen's Project Portfolio Page +--- + +### Overview + +UNOFAS is a desktop app for Financial Advisors (FA) to manage client’s contacts, optimized for use via a Command Line +Interface (CLI) while still having the benefits of a Graphical User Interface (GUI). The app also includes features +such as sorting, scheduling and other commands to query information quickly required by the FA. + +### Summary of Contributions + +Given below are my contributions to the project. + +* **New feature**: Added the ability to gather emails of clients by Financial plan or Tag. + + * What it does: It takes a prompt as input and retrieves emails of clients where the prompt matches a substring of their Financial Plan or Tag names. + * Justification: This feature significantly improves the product's efficiency by allowing financial advisors to consolidate emails effectively. It simplifies communication, enabling advisors to update multiple clients about changes in their financial plans or schedule crucial meetings promptly. + * Highlights: + * Provides a foundation for future email gathering implementations, paving the way for potential expansions into other fields. + * Required a deep understanding of interfaces and integration with existing codebase. + * Pull request [#72](https://github.com/AY2324S1-CS2103T-F12-1/tp/pull/72) + + +* **Code Contributed**: [RepoSense](https://nus-cs2103-ay2324s1.github.io/tp-dashboard/?search=alyssapng&breakdown=true) + + +* **Enhancements Implemented**: + * Gather command to be able to gather emails by Tag. + * Pull request [#109](https://github.com/AY2324S1-CS2103T-F12-1/tp/pull/109) + * Enhancing the UI design for UNOFAS. + * Justification: Provide financial advisors with a more intuitive, visually appealing interface, optimizing content visibility to reduce the likelihood of overlooking crucial details. + * Pull request [#151](https://github.com/AY2324S1-CS2103T-F12-1/tp/pull/151) + + +* **Contributions to the UG**: + * Updated Title and Introduction. + * Added Argument Summary. + * Added documentation for the features `gather`. + * Reading through UG and identifying any formatting or ambiguity concerns in the descriptions. + + +* **Contributions to the DG**: + * Added target user profile, value proposition, user stories and user cases. [#49](https://github.com/AY2324S1-CS2103T-F12-1/tp/pull/49) + * Added class diagram and sequence diagram for `gather` feature. + * Added implementation details for `gather` feature. + * Added planned enhancement for `gather` feature. + + +* **Community**: + * PRs reviewed (with non-trivial commments): + [#108](https://github.com/AY2324S1-CS2103T-F12-1/tp/pull/108), + [#90](https://github.com/AY2324S1-CS2103T-F12-1/tp/pull/90), + [#133](https://github.com/AY2324S1-CS2103T-F12-1/tp/pull/133) + + +* **Contributions to team-based tasks**: + * Release product for `v1.1`. + * Added screenshots into project notes document for `v1.2 demo`. + diff --git a/docs/team/jylow.md b/docs/team/jylow.md new file mode 100644 index 00000000000..0f4db45a992 --- /dev/null +++ b/docs/team/jylow.md @@ -0,0 +1,48 @@ +--- +layout: page +title: Low Jun Yu's Project Portfolio Page +--- + +### Overview + +UNOFAS is a desktop app for Financial Advisors (FA) to manage client’s contacts, optimized for use via a Command Line +Interface (CLI) while still having the benefits of a Graphical User Interface (GUI). The app also includes features +such as sorting, scheduling and other commands to query information quickly required by the FA. + +### Summary of Contributions + +Given below are my contributions to the project. + +* **New Feature**: Added the ability to sort list by appointment time, and name by lexicographical order. [\#73](https://github.com/AY2324S1-CS2103T-F12-1/tp/pull/73) [\#90](https://github.com/AY2324S1-CS2103T-F12-1/tp/pull/90) + * What it does: Allows the user to perform sorting of list by appointment time and lexicographical order of name. + * Justification: This feature improves the product significantly because a user can more efficiently find clients by name and the proximity of their appointments to view upcoming appointments. + * Credits: The feature was implemented by referencing JavaFX ObservableList documentation. +
+* **New Feature**: Added confirm override window for `schedule` command if person already has a current appointment. [\#123](https://github.com/AY2324S1-CS2103T-F12-1/tp/pull/123) + * What it does: Caution a user if he intends to schedule a new appointment when there is already one that is not yet complete. + * Justification: Most people only arrange one appointment at a time. So the design prevents multiple appointments and also serves as a reminder of a previously set appointment. + * Highlights: This feature causes the logic flow of the method to change if there is currently an appointment and results in breaking the execution into 2, getting a response from the user before deciding whether to continue the execution of the program. + * Credits: The feature was implemented by referencing the help function from AB3. However, the main logic of the function was done by myself. + +* **Code Contributed**: [RepoSense](https://nus-cs2103-ay2324s1.github.io/tp-dashboard/?search=jylow&breakdown=true) + +* **Enhancements Implemented**: + * Fixed bugs identified during manual testing. + +* **Contributions to the UG**: + * Added documentation for the feature `sort`. [\#81](https://github.com/AY2324S1-CS2103T-F12-1/tp/pull/81) + * Maintained known issues to be solved. + * Proofreading of UG and making changes such as standardising terms used throughout the UG. + +* **Contributions to the DG**: + * Added documentation for sort function and improved documentation for `schedule` command. + * Added sequence and activity diagram for sort function and update activity diagram for `schedule` command to include overriding. + * Update schedule and clear command documentation to include the warning prompts. + +* **Community**: + * PRs reviewed (with non-trivial review comments): [\#125](https://github.com/AY2324S1-CS2103T-F12-1/tp/pull/125) [\#133](https://github.com/AY2324S1-CS2103T-F12-1/tp/pull/133#pullrequestreview-1699166607) + * Contributed to 1 forum discussion. (examples: [1](https://github.com/nus-cs2103-AY2324S1/forum/issues/172#issuecomment-1730790631)) + * Reported bugs and suggestions for other teams in the class during PED. + +* **Contributions to team-based tasks**: + * Released v1.3 and v1.4 of the application. diff --git a/docs/team/kb-tay.md b/docs/team/kb-tay.md new file mode 100644 index 00000000000..4bae6f52517 --- /dev/null +++ b/docs/team/kb-tay.md @@ -0,0 +1,47 @@ +--- +layout: page +title: Aaron Tay's Project Portfolio Page +--- + +### Overview + +UNOFAS is a desktop app for **Financial Advisors (FA)** to manage client’s contacts, optimized for use via a **Command Line Interface (CLI)** while still having the benefits of a **Graphical User Interface (GUI)**. The app also includes features such as sorting, scheduling and other commands to query information quickly required by the FA. + +### Summary of Contributions + +Given below are my contributions to the project. + +* **New Feature**: Added the `ScheduleItem` class as a new field to Person. + * Justification: This feature provides users the ability to associate appointments with their clients. + * Pull Request [#70](https://github.com/AY2324S1-CS2103T-F12-1/tp/pull/70) + +* **New Feature**: Added the ability to `schedule` appointments. + * What it does: Allows the user to create an appointment associated with the specified client. + * Justification: This feature provides users the ability to schedule and keep track of appointments with their clients. + * Pull Request [#70](https://github.com/AY2324S1-CS2103T-F12-1/tp/pull/70) + +* **New Feature**: Added the ability to `complete` appointments. + * What it does: Allows users to clear appointments completed from the contact book. + * Justification: With this feature, user can keep track of appointments completed. + * Highlights: This command allows for more flexibility, giving users the choice to clear appointments by user's index or by a date. + * Pull Request [#133](https://github.com/AY2324S1-CS2103T-F12-1/tp/pull/133) +* **Code Contributed**: [RepoSense](https://nus-cs2103-ay2324s1.github.io/tp-dashboard/?search=kb-tay&breakdown=true) + + +* **Enhancements Implemented**: + * Added testing for implemented features + * Refactored `ScheduleItem` class to adhere to LSP. [#108](https://github.com/AY2324S1-CS2103T-F12-1/tp/pull/108) + * Refactored `CompleteCommand` to better adhere to the command pattern. [#217](https://github.com/AY2324S1-CS2103T-F12-1/tp/issues/217) +* **Contributions to the UG**: + * Added documentation for `schedule` command and `complete` under feature section. [#137](https://github.com/AY2324S1-CS2103T-F12-1/tp/pull/137) [#135](https://github.com/AY2324S1-CS2103T-F12-1/tp/pull/137) [#210](https://github.com/AY2324S1-CS2103T-F12-1/tp/pull/210) +* **Contributions to the DG**: + * Added documentation, class diagram and sequence diagram for `schedule` feature. [#137](https://github.com/AY2324S1-CS2103T-F12-1/tp/pull/137) + * Added documentation on design considerations for `Appointment` field. [#137](https://github.com/AY2324S1-CS2103T-F12-1/tp/pull/137) + * Added sequence and activity diagram for `complete` feature. [#209](https://github.com/AY2324S1-CS2103T-F12-1/tp/pull/209/files) + * Added documentation on implementation and design considerations for `complete` feature. [#328](https://github.com/AY2324S1-CS2103T-F12-1/tp/pull/328) +* **Community**: + * Review PRs of teammates. [#109](https://github.com/AY2324S1-CS2103T-F12-1/tp/pull/109) [#205](https://github.com/AY2324S1-CS2103T-F12-1/tp/pull/205) [#110](https://github.com/AY2324S1-CS2103T-F12-1/tp/pull/110) [#256](https://github.com/AY2324S1-CS2103T-F12-1/tp/pull/256) + * Review DG and suggested bug fixes for teammates. [#285](https://github.com/AY2324S1-CS2103T-F12-1/tp/issues/285) [#278](https://github.com/AY2324S1-CS2103T-F12-1/tp/issues/278) +* **Contributions to team-based tasks** + * Update Developer Guide - Update diagram for `Model` and `Storage`, checked for overall correctness. [#128](https://github.com/AY2324S1-CS2103T-F12-1/tp/pull/128) + * Handled PE-D bug triaging. diff --git a/docs/team/nikele2001.md b/docs/team/nikele2001.md new file mode 100644 index 00000000000..3679f14694b --- /dev/null +++ b/docs/team/nikele2001.md @@ -0,0 +1,55 @@ +--- +layout: page +title: Nicholas Chia Zhi Jie's Project Portfolio Page +--- + +### Overview + +UNOFAS is a desktop app for Financial Advisors (FA) to manage client’s contacts, optimized for use via a Command Line +Interface (CLI) while still having the benefits of a Graphical User Interface (GUI). The app also includes features +such as sorting, scheduling and other commands to query information quickly required by the FA. + +### Summary of Contributions + +Given below are my contributions to the project. + +* **New Feature**: Added appointment list that shows upcoming appointments of clients in chronological order and wrote tests. + * Justification: Financial advisors may want to view all upcoming appointments easily in chronological order so that it is easier for them to plan their timetables. + * Highlights: This enhancement creates another UI element to show various other appointment-specific details in the future. It requires an understanding of ObservableList interface and the way the list is being tracked by JavaFX. + * Pull request [#110](https://github.com/AY2324S1-CS2103T-F12-1/tp/pull/110) + + +* **Code Contributed**: [RepoSense](https://nus-cs2103-ay2324s1.github.io/tp-dashboard/?search=nikele2001&breakdown=true) + + +* **Enhancements Implemented**: + * Added `FinancialPlan` field and wrote tests. + * Justification: As a financial advisor, it would be convenient to have a person's current financial plans available for business purposes. + * Pull request [#69](https://github.com/AY2324S1-CS2103T-F12-1/tp/pull/69) + +* **Contributions to the UG**: + * Added introduction to UG. + * Added and updated all UI images. + * Added general UI information table. + + +* **Contributions to the DG**: + * Added use cases for `schedule` command, `sort` command, `complete` command and adding financial plans via the `fp/` prefix. + * Updated user stories. + * Updated UML class diagrams for `Model` and `UI` components. + * Added sequence diagram for the `setPerson()` method in the `ScheduleCommand#execute()` method to illustrate how the appointment list is being updated. + * Added activity diagram for the `gather` command. + * Contributed to `Instructions for manual testing` in DG. + * Added effort section. + + +* **Community**: + * PRs reviewed (with non-trivial commments): + [#72](https://github.com/AY2324S1-CS2103T-F12-1/tp/pull/72), + [#108](https://github.com/AY2324S1-CS2103T-F12-1/tp/pull/108) + + +* **Contributions to team-based tasks**: + * Set up team organisation. + * Set up TP repository. + * Set up Jekyll. diff --git a/docs/team/sopa301.md b/docs/team/sopa301.md new file mode 100644 index 00000000000..4794062f0be --- /dev/null +++ b/docs/team/sopa301.md @@ -0,0 +1,62 @@ +--- +layout: page +title: Poon Yip Hang, Ryan's Project Portfolio Page +--- + +### Overview + +UNOFAS is a desktop app for Financial Advisors (FA) to manage client’s contacts, optimized for use via a Command Line +Interface (CLI) while still having the benefits of a Graphical User Interface (GUI). The app also includes features +such as sorting, scheduling and other commands to query information quickly required by the FA. + +### Summary of Contributions + +Given below are my contributions to the project. + +* **Code Contributed**: [RepoSense link](https://nus-cs2103-ay2324s1.github.io/tp-dashboard/?search=sopa301&breakdown=true) + + +* **Enhancements Implemented**: + * Added `next-of-kin` and `next-of-kin phone` fields and wrote tests. + * Justification: As a financial advisor, it would be convenient to have a person's next-of-kin details available for + business purposes. + * Pull request [#43](https://github.com/AY2324S1-CS2103T-F12-1/tp/pull/43) + * Added confirm clear window for `clear` command. + * Justification: `clear` is a very powerful command that can delete the entirety of a user's work in an instant. To + safeguard against mistakes, we decided to add an extra confirmation requirement to ensure that the user actually wants + to wipe the contact book. + * Highlights: This feature causes the logic flow of the method to change if the clear command is about to be executed, getting a response from the user before deciding whether to continue the execution of the program. + * Pull request [#75](https://github.com/AY2324S1-CS2103T-F12-1/tp/pull/75) + * Added available keywords for `help` command. + * Pull request [#67](https://github.com/AY2324S1-CS2103T-F12-1/tp/pull/67) + * Enhanced `find` command to accept multiple names, tags and financial plans and wrote tests. + * Justification: Users may want to search by other fields, not just name. Also, searching across categories have + niche uses and can be easily supported. + * Pull request [#125](https://github.com/AY2324S1-CS2103T-F12-1/tp/pull/125) + + +* **Contributions to the UG**: + * Updated documentation for `add`, `clear`, `edit`, `find` and `help` commands. + * Updated the table of contents. + + +* **Contributions to the DG**: + * Updated references to code files in the `Design` section. + * Added implementation details for enhanced `find` command. + * Drafted `Planned Enhancements` section. + * Contributed to `Instructions for manual testing` in DG. + * Added sequence diagram for `find` command. + * Added class diagram for `CombinedPredicate` class. + + +* **Community**: + * PRs reviewed (with non-trivial comments): [#70](https://github.com/AY2324S1-CS2103T-F12-1/tp/pull/70), + [#256](https://github.com/AY2324S1-CS2103T-F12-1/tp/pull/256), + [#109](https://github.com/AY2324S1-CS2103T-F12-1/tp/pull/109), + [#110](https://github.com/AY2324S1-CS2103T-F12-1/tp/pull/110). + + +* **Contributions to team-based tasks**: + * Maintained issue tracker. + * Released product for `v1.2` and `v1.3.trial`. + * Refactored `AddCommandParser` and `EditCommandParser` for better SLAP. diff --git a/src/main/java/seedu/address/MainApp.java b/src/main/java/seedu/address/MainApp.java index 3d6bd06d5af..211eb5ab659 100644 --- a/src/main/java/seedu/address/MainApp.java +++ b/src/main/java/seedu/address/MainApp.java @@ -36,7 +36,7 @@ */ public class MainApp extends Application { - public static final Version VERSION = new Version(0, 2, 2, true); + public static final Version VERSION = new Version(1, 4, 0, false); private static final Logger logger = LogsCenter.getLogger(MainApp.class); @@ -88,11 +88,23 @@ private Model initModelManager(Storage storage, ReadOnlyUserPrefs userPrefs) { logger.warning("Data file at " + storage.getAddressBookFilePath() + " could not be loaded." + " Will be starting with an empty AddressBook."); initialData = new AddressBook(); + this.saveEmptyAddressBook(initialData); } return new ModelManager(initialData, userPrefs); } + /** + * Wipes corrupted data in addressbook.json when starting the application with corrupted data. + */ + private void saveEmptyAddressBook(ReadOnlyAddressBook initialData) { + try { + storage.saveAddressBook(initialData); + } catch (IOException e) { + logger.warning("Failed to locate filepath."); + } + } + private void initLogging(Config config) { LogsCenter.init(config); } diff --git a/src/main/java/seedu/address/commons/util/StringUtil.java b/src/main/java/seedu/address/commons/util/StringUtil.java index 61cc8c9a1cb..9932096f07d 100644 --- a/src/main/java/seedu/address/commons/util/StringUtil.java +++ b/src/main/java/seedu/address/commons/util/StringUtil.java @@ -6,6 +6,9 @@ import java.io.PrintWriter; import java.io.StringWriter; import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; /** * Helper functions for handling strings. @@ -38,6 +41,47 @@ public static boolean containsWordIgnoreCase(String sentence, String word) { .anyMatch(preppedWord::equalsIgnoreCase); } + /** + * Returns true if the {@code sentence} contains the {@code words}. + * Ignores case, but a full phrase match is required. + *
examples:
+     *       containsWordsIgnoreCase("ABc def", "abc def") == true
+     *       containsWordsIgnoreCase("ABc def", "DEF") == true
+     *       containsWordsIgnoreCase("ABc def", "ABc de") == false //not a full word match
+     *       
+ * @param sentence cannot be null + * @param words cannot be null, cannot be empty + */ + public static boolean containsWordsIgnoreCase(String sentence, String words) { + requireNonNull(sentence); + requireNonNull(words); + + String trimmedWords = words.trim(); + checkArgument(!trimmedWords.isEmpty(), "Word parameter cannot be empty"); + List preppedWords = prepareWords(trimmedWords); + + String trimmedSentence = sentence.trim(); + if (trimmedSentence.isEmpty()) { + return false; + } + List preppedSentence = prepareWords(sentence); + + return Collections.indexOfSubList(preppedSentence, preppedWords) != -1; + } + + /** + * Prepares the given words to be compared in the containsWordsIgnoreCase() method. + * + * @param words cannot be null + * @return the prepared list of strings + */ + private static List prepareWords(String words) { + requireNonNull(words); + return Arrays.stream(words.split("\\s+")) + .map(word -> word.toLowerCase()) + .collect(Collectors.toList()); + } + /** * Returns a detailed message of the t, including the stack trace. */ diff --git a/src/main/java/seedu/address/logic/Logic.java b/src/main/java/seedu/address/logic/Logic.java index 92cd8fa605a..0911ff4ed14 100644 --- a/src/main/java/seedu/address/logic/Logic.java +++ b/src/main/java/seedu/address/logic/Logic.java @@ -4,10 +4,12 @@ import javafx.collections.ObservableList; import seedu.address.commons.core.GuiSettings; +import seedu.address.logic.commands.Command; import seedu.address.logic.commands.CommandResult; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.appointment.Appointment; import seedu.address.model.person.Person; /** @@ -22,6 +24,13 @@ public interface Logic { * @throws ParseException If an error occurs during parsing. */ CommandResult execute(String commandText) throws CommandException, ParseException; + /** + * Executes the command and returns the result. + * @param command The command to be executed. + * @return the result of the command execution. + * @throws CommandException If an error occurs during command execution. + */ + CommandResult execute(Command command) throws CommandException; /** * Returns the AddressBook. @@ -33,6 +42,9 @@ public interface Logic { /** Returns an unmodifiable view of the filtered list of persons */ ObservableList getFilteredPersonList(); + /** Returns an unmodifiable view of the list of appointments */ + ObservableList getAppointmentList(); + /** * Returns the user prefs' address book file path. */ diff --git a/src/main/java/seedu/address/logic/LogicManager.java b/src/main/java/seedu/address/logic/LogicManager.java index 5aa3b91c7d0..6f17c245a4e 100644 --- a/src/main/java/seedu/address/logic/LogicManager.java +++ b/src/main/java/seedu/address/logic/LogicManager.java @@ -15,6 +15,7 @@ import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.Model; import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.appointment.Appointment; import seedu.address.model.person.Person; import seedu.address.storage.Storage; @@ -48,7 +49,13 @@ public CommandResult execute(String commandText) throws CommandException, ParseE CommandResult commandResult; Command command = addressBookParser.parseCommand(commandText); - commandResult = command.execute(model); + commandResult = execute(command); + + return commandResult; + } + @Override + public CommandResult execute(Command command) throws CommandException { + CommandResult commandResult = command.execute(model); try { storage.saveAddressBook(model.getAddressBook()); @@ -57,7 +64,6 @@ public CommandResult execute(String commandText) throws CommandException, ParseE } catch (IOException ioe) { throw new CommandException(String.format(FILE_OPS_ERROR_FORMAT, ioe.getMessage()), ioe); } - return commandResult; } @@ -71,6 +77,11 @@ public ObservableList getFilteredPersonList() { return model.getFilteredPersonList(); } + @Override + public ObservableList getAppointmentList() { + return model.getAppointmentList(); + } + @Override public Path getAddressBookFilePath() { return model.getAddressBookFilePath(); diff --git a/src/main/java/seedu/address/logic/Messages.java b/src/main/java/seedu/address/logic/Messages.java index ecd32c31b53..7abb62ab497 100644 --- a/src/main/java/seedu/address/logic/Messages.java +++ b/src/main/java/seedu/address/logic/Messages.java @@ -43,7 +43,15 @@ public static String format(Person person) { .append(person.getEmail()) .append("; Address: ") .append(person.getAddress()) - .append("; Tags: "); + .append("; Next-of-kin Name: ") + .append(person.getNextOfKinName()) + .append("; Next-of-kin Phone: ") + .append(person.getNextOfKinPhone()) + .append("; Appointment: ") + .append(person.getAppointment()) + .append("; Financial Plans: "); + person.getFinancialPlans().forEach(builder::append); + builder.append("; Tags: "); person.getTags().forEach(builder::append); return builder.toString(); } diff --git a/src/main/java/seedu/address/logic/commands/AddCommand.java b/src/main/java/seedu/address/logic/commands/AddCommand.java index 5d7185a9680..977c3cdd06d 100644 --- a/src/main/java/seedu/address/logic/commands/AddCommand.java +++ b/src/main/java/seedu/address/logic/commands/AddCommand.java @@ -3,7 +3,10 @@ import static java.util.Objects.requireNonNull; import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_FINANCIAL_PLAN; import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NEXT_OF_KIN_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NEXT_OF_KIN_PHONE; import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; @@ -26,12 +29,17 @@ public class AddCommand extends Command { + PREFIX_PHONE + "PHONE " + PREFIX_EMAIL + "EMAIL " + PREFIX_ADDRESS + "ADDRESS " + + PREFIX_NEXT_OF_KIN_NAME + "NEXT_OF_KIN_NAME " + + PREFIX_NEXT_OF_KIN_PHONE + "NEXT_OF_KIN_PHONE " + "[" + PREFIX_TAG + "TAG]...\n" + + "[" + PREFIX_FINANCIAL_PLAN + "FINANCIAL_PLAN]...\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_NEXT_OF_KIN_NAME + "Jim Doe " + + PREFIX_NEXT_OF_KIN_PHONE + "92345678 " + PREFIX_TAG + "friends " + PREFIX_TAG + "owesMoney"; diff --git a/src/main/java/seedu/address/logic/commands/ClearCommand.java b/src/main/java/seedu/address/logic/commands/ClearCommand.java index 9c86b1fa6e4..4d13af8a0d2 100644 --- a/src/main/java/seedu/address/logic/commands/ClearCommand.java +++ b/src/main/java/seedu/address/logic/commands/ClearCommand.java @@ -2,22 +2,20 @@ import static java.util.Objects.requireNonNull; -import seedu.address.model.AddressBook; import seedu.address.model.Model; /** - * Clears the address book. + * Attempts to clear the address book. */ public class ClearCommand extends Command { public static final String COMMAND_WORD = "clear"; - public static final String MESSAGE_SUCCESS = "Address book has been cleared!"; + public static final String CONFIRM_CLEAR_MESSAGE = "Please confirm the command."; @Override public CommandResult execute(Model model) { requireNonNull(model); - model.setAddressBook(new AddressBook()); - return new CommandResult(MESSAGE_SUCCESS); + return new CommandResult(CONFIRM_CLEAR_MESSAGE, false, false, true); } } diff --git a/src/main/java/seedu/address/logic/commands/CommandResult.java b/src/main/java/seedu/address/logic/commands/CommandResult.java index 249b6072d0d..6931a204dbd 100644 --- a/src/main/java/seedu/address/logic/commands/CommandResult.java +++ b/src/main/java/seedu/address/logic/commands/CommandResult.java @@ -5,6 +5,8 @@ import java.util.Objects; import seedu.address.commons.util.ToStringBuilder; +import seedu.address.model.appointment.Appointment; +import seedu.address.model.person.Person; /** * Represents the result of a command execution. @@ -18,14 +20,49 @@ public class CommandResult { /** The application should exit. */ private final boolean exit; + /** Confirmation for clear command should be requested. */ + private final boolean showClear; + /** Confirmation for overriding command should be requested. */ + private final boolean showOverride; + /** Appointment that will be replaced **/ + private Appointment appointment = null; + /** Person who's appointment will be replaced **/ + private Person personToEdit = null; /** - * Constructs a {@code CommandResult} with the specified fields. + * Constructs a {@code CommandResult} with the specified fields, with showClear set to false. */ public CommandResult(String feedbackToUser, boolean showHelp, boolean exit) { this.feedbackToUser = requireNonNull(feedbackToUser); this.showHelp = showHelp; this.exit = exit; + this.showClear = false; + this.showOverride = false; + } + + /** + * Constructs a {@code CommandResult} with the specified fields. + */ + public CommandResult(String feedbackToUser, boolean showHelp, boolean exit, boolean showClear) { + this.feedbackToUser = requireNonNull(feedbackToUser); + this.showHelp = showHelp; + this.exit = exit; + this.showClear = showClear; + this.showOverride = false; + } + + /** + * Constructs a {@code CommandResult} with the specified fields. + */ + public CommandResult(String feedbackToUser, boolean exit, boolean showOverride, + Person personToEdit, Appointment appointment) { + this.feedbackToUser = requireNonNull(feedbackToUser); + this.exit = exit; + this.showOverride = showOverride; + this.showClear = false; + this.showHelp = false; + this.personToEdit = requireNonNull(personToEdit); + this.appointment = requireNonNull(appointment); } /** @@ -47,7 +84,12 @@ public boolean isShowHelp() { public boolean isExit() { return exit; } - + public boolean isShowClear() { + return showClear; + } + public boolean isShowOverride() { + return showOverride; + } @Override public boolean equals(Object other) { if (other == this) { @@ -62,12 +104,15 @@ public boolean equals(Object other) { CommandResult otherCommandResult = (CommandResult) other; return feedbackToUser.equals(otherCommandResult.feedbackToUser) && showHelp == otherCommandResult.showHelp - && exit == otherCommandResult.exit; + && exit == otherCommandResult.exit + && showClear == otherCommandResult.showClear + && appointment == otherCommandResult.appointment + && personToEdit == otherCommandResult.personToEdit; } @Override public int hashCode() { - return Objects.hash(feedbackToUser, showHelp, exit); + return Objects.hash(feedbackToUser, showHelp, exit, showClear); } @Override @@ -76,7 +121,15 @@ public String toString() { .add("feedbackToUser", feedbackToUser) .add("showHelp", showHelp) .add("exit", exit) + .add("showClear", showClear) .toString(); } + public Appointment getAppointment() { + return this.appointment; + } + + public Person getPersonToEdit() { + return this.personToEdit; + } } diff --git a/src/main/java/seedu/address/logic/commands/CompleteByDate.java b/src/main/java/seedu/address/logic/commands/CompleteByDate.java new file mode 100644 index 00000000000..e2ad4268c71 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/CompleteByDate.java @@ -0,0 +1,53 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; + +import java.time.LocalDate; + +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; + +/** + * Completes and removes appointments from one or multiple person in address book by date input. + * All persons with appointments date matching user input will be cleared. + */ +public class CompleteByDate extends CompleteCommand { + private final LocalDate date; + + /** + * @param date that all matching appointment's date in address book will be cleared + */ + public CompleteByDate(LocalDate date) { + requireNonNull(date); + this.date = date; + } + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + if (!model.hasAppointmentWithDate(date)) { + throw new CommandException(MESSAGE_DATE_NO_APPOINTMENT); + } + + model.clearAppointments(date); + model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + + return new CommandResult(MESSAGE_COMPLETE_SUCCESS); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof CompleteByDate)) { + return false; + } + + CompleteByDate otherCommand = (CompleteByDate) other; + return this.date.equals(otherCommand.date); + } +} diff --git a/src/main/java/seedu/address/logic/commands/CompleteByIndex.java b/src/main/java/seedu/address/logic/commands/CompleteByIndex.java new file mode 100644 index 00000000000..41c14f4a6a1 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/CompleteByIndex.java @@ -0,0 +1,63 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +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; + +/** + * Completes and removes appointments from person in address book specified by index. + */ +public class CompleteByIndex extends CompleteCommand { + public final Index index; + + /** + * @param index of the person in the filtered person list to complete appointment with + */ + public CompleteByIndex(Index index) { + requireNonNull(index); + this.index = index; + } + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredPersonList(); + + if (index.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + Person personToEdit = lastShownList.get(index.getZeroBased()); + + if (personToEdit.hasNullAppointment()) { + throw new CommandException(MESSAGE_PERSON_NO_APPOINTMENT); + } + + Person personWithoutAppointment = personToEdit.clearAppointment(); + + model.setPerson(personToEdit, personWithoutAppointment); + model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + + return new CommandResult(MESSAGE_COMPLETE_SUCCESS); + } + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles null + if (!(other instanceof CompleteByIndex)) { + return false; + } + + CompleteByIndex otherCommand = (CompleteByIndex) other; + return this.index.equals(otherCommand.index); + } +} diff --git a/src/main/java/seedu/address/logic/commands/CompleteCommand.java b/src/main/java/seedu/address/logic/commands/CompleteCommand.java new file mode 100644 index 00000000000..ee69410bea4 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/CompleteCommand.java @@ -0,0 +1,31 @@ +package seedu.address.logic.commands; + +import static seedu.address.logic.parser.CliSyntax.PREFIX_APPOINTMENT_DATE; + +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; + +/** + * Completes and removes appointments from one or multiple person in address book. + */ +public abstract class CompleteCommand extends Command { + + public static final String COMMAND_WORD = "complete"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Completes an appointment identified " + + "by the index number used in the displayed person list or " + + "by a specified date\n" + + "Parameters: [INDEX(must be a positive integer)] " + + "[" + PREFIX_APPOINTMENT_DATE + "Appointment Date] *Only one of the 2 parameters may and " + + "must be specified\n" + + "Example: " + COMMAND_WORD + " 1 "; + public static final String MESSAGE_COMPLETE_SUCCESS = "Appointment(s) Completed!"; + public static final String MESSAGE_INVALID_DATE_FORMAT = "Input Date should be in format of dd-MM-yyyy"; + public static final String MESSAGE_INVALID_DATE = "Please input a valid Date"; + public static final String MESSAGE_PERSON_NO_APPOINTMENT = "No Appointment Found:" + + " Selected Person currently has no appointment scheduled"; + public static final String MESSAGE_DATE_NO_APPOINTMENT = "No Appointment Found:" + + " No Appointments found with the current date"; + @Override + public abstract CommandResult execute(Model model) throws CommandException; +} diff --git a/src/main/java/seedu/address/logic/commands/ConfirmClearCommand.java b/src/main/java/seedu/address/logic/commands/ConfirmClearCommand.java new file mode 100644 index 00000000000..5e7ab4a3083 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ConfirmClearCommand.java @@ -0,0 +1,19 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import seedu.address.model.AddressBook; +import seedu.address.model.Model; + +/** + * Clears the address book. + */ +public class ConfirmClearCommand extends Command { + public static final String MESSAGE_SUCCESS = "Contacts cleared!"; + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + model.setAddressBook(new AddressBook()); + return new CommandResult(MESSAGE_SUCCESS); + } +} diff --git a/src/main/java/seedu/address/logic/commands/ConfirmOverrideCommand.java b/src/main/java/seedu/address/logic/commands/ConfirmOverrideCommand.java new file mode 100644 index 00000000000..3915d89df58 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ConfirmOverrideCommand.java @@ -0,0 +1,82 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; + +import seedu.address.logic.Messages; +import seedu.address.model.Model; +import seedu.address.model.appointment.Appointment; +import seedu.address.model.person.Person; + +/** Overrides the current appointment and replaces it with the new appointment after confirmation **/ +public class ConfirmOverrideCommand extends Command { + public static final String MESSAGE_SUCCESS = "Appointment updated!"; + private Appointment appointment; + private Person personToEdit; + + /** + * Default constructor for a confirm override command + * @param appointment new appointment + * @param personToEdit person whose old appointment will be replaced by the new appointment + */ + public ConfirmOverrideCommand(Appointment appointment, Person personToEdit) { + requireNonNull(appointment); + requireNonNull(personToEdit); + this.appointment = appointment; + this.personToEdit = personToEdit; + } + + public Appointment getAppointment() { + return this.appointment; + } + + public Person getPersonToEdit() { + return this.personToEdit; + } + + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + requireNonNull(this.appointment); + requireNonNull(this.personToEdit); + + Person personWithApt = createPersonWithAppointment(this.personToEdit); + + assert personWithApt.getAppointment() instanceof Appointment + : "Schedule Command: person should have appointment"; + + this.appointment.setPerson(personWithApt); //sets person to appointment + + model.setPerson(this.personToEdit, personWithApt); + model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + return new CommandResult(String.format(MESSAGE_SUCCESS, Messages.format(personWithApt))); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof ConfirmOverrideCommand)) { + return false; + } + + ConfirmOverrideCommand otherOverrideCommand = (ConfirmOverrideCommand) other; + return this.appointment.equals(otherOverrideCommand.getAppointment()) + && this.personToEdit.equals(otherOverrideCommand.getPersonToEdit()); + } + + /** + * Returns the {@code Person} with the scheduled Appointment. + * + * @param personToEdit The Person the appointment is scheduled to. + * @return The Person with scheduled appointment. + */ + private Person createPersonWithAppointment(Person personToEdit) { + return new Person(personToEdit.getName(), personToEdit.getPhone(), personToEdit.getEmail(), + personToEdit.getAddress(), personToEdit.getNextOfKinName(), personToEdit.getNextOfKinPhone(), + personToEdit.getFinancialPlans(), personToEdit.getTags(), this.appointment); + } +} diff --git a/src/main/java/seedu/address/logic/commands/EditCommand.java b/src/main/java/seedu/address/logic/commands/EditCommand.java index 4b581c7331e..7fb84fa75b8 100644 --- a/src/main/java/seedu/address/logic/commands/EditCommand.java +++ b/src/main/java/seedu/address/logic/commands/EditCommand.java @@ -3,7 +3,10 @@ import static java.util.Objects.requireNonNull; import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_FINANCIAL_PLAN; import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NEXT_OF_KIN_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NEXT_OF_KIN_PHONE; import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; @@ -21,9 +24,13 @@ import seedu.address.logic.Messages; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.model.Model; +import seedu.address.model.appointment.ScheduleItem; +import seedu.address.model.financialplan.FinancialPlan; import seedu.address.model.person.Address; import seedu.address.model.person.Email; import seedu.address.model.person.Name; +import seedu.address.model.person.NextOfKinName; +import seedu.address.model.person.NextOfKinPhone; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; import seedu.address.model.tag.Tag; @@ -43,6 +50,9 @@ public class EditCommand extends Command { + "[" + PREFIX_PHONE + "PHONE] " + "[" + PREFIX_EMAIL + "EMAIL] " + "[" + PREFIX_ADDRESS + "ADDRESS] " + + "[" + PREFIX_NEXT_OF_KIN_NAME + "NOK_NAME] " + + "[" + PREFIX_NEXT_OF_KIN_PHONE + "NOK_PHONE] " + + "[" + PREFIX_FINANCIAL_PLAN + "FINANCIAL_PLAN]...\n" + "[" + PREFIX_TAG + "TAG]...\n" + "Example: " + COMMAND_WORD + " 1 " + PREFIX_PHONE + "91234567 " @@ -51,7 +61,6 @@ public class EditCommand extends Command { public static final String MESSAGE_EDIT_PERSON_SUCCESS = "Edited Person: %1$s"; public static final String MESSAGE_NOT_EDITED = "At least one field to edit must be provided."; public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book."; - private final Index index; private final EditPersonDescriptor editPersonDescriptor; @@ -99,9 +108,15 @@ 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()); + NextOfKinName updatedNokName = editPersonDescriptor.getNextOfKinName().orElse(personToEdit.getNextOfKinName()); + NextOfKinPhone updatedNokPhone = editPersonDescriptor.getNextOfKinPhone() + .orElse(personToEdit.getNextOfKinPhone()); + Set updatedFinancialPlans = editPersonDescriptor.getFinancialPlans() + .orElse(personToEdit.getFinancialPlans()); Set updatedTags = editPersonDescriptor.getTags().orElse(personToEdit.getTags()); - - return new Person(updatedName, updatedPhone, updatedEmail, updatedAddress, updatedTags); + ScheduleItem appointment = editPersonDescriptor.getAppointment().orElse(personToEdit.getAppointment()); + return new Person(updatedName, updatedPhone, updatedEmail, updatedAddress, updatedNokName, + updatedNokPhone, updatedFinancialPlans, updatedTags, appointment); } @Override @@ -137,7 +152,11 @@ public static class EditPersonDescriptor { private Phone phone; private Email email; private Address address; + private NextOfKinName nextOfKinName; + private NextOfKinPhone nextOfKinPhone; + private Set financialPlans; private Set tags; + private ScheduleItem appointment; public EditPersonDescriptor() {} @@ -150,6 +169,9 @@ public EditPersonDescriptor(EditPersonDescriptor toCopy) { setPhone(toCopy.phone); setEmail(toCopy.email); setAddress(toCopy.address); + setNextOfKinName(toCopy.nextOfKinName); + setNextOfKinPhone(toCopy.nextOfKinPhone); + setFinancialPlans(toCopy.financialPlans); setTags(toCopy.tags); } @@ -157,7 +179,8 @@ 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, nextOfKinName, + nextOfKinPhone, financialPlans, tags); } public void setName(Name name) { @@ -191,6 +214,47 @@ public void setAddress(Address address) { public Optional
getAddress() { return Optional.ofNullable(address); } + public void setNextOfKinName(NextOfKinName nokName) { + this.nextOfKinName = nokName; + } + + public Optional getNextOfKinName() { + return Optional.ofNullable(nextOfKinName); + } + + public void setNextOfKinPhone(NextOfKinPhone nokPhone) { + this.nextOfKinPhone = nokPhone; + } + + public Optional getNextOfKinPhone() { + return Optional.ofNullable(nextOfKinPhone); + } + + public void setAppointment(ScheduleItem appointment) { + this.appointment = appointment; + } + + public Optional getAppointment() { + return Optional.ofNullable(appointment); + } + + /** + * Sets {@code financialPlans} to this object's {@code financialPlans}. + * A defensive copy of {@code financialPlans} is used internally. + */ + public void setFinancialPlans(Set financialPlans) { + this.financialPlans = (financialPlans != null) ? new HashSet<>(financialPlans) : null; + } + + /** + * Returns an unmodifiable financial plan set, which throws {@code UnsupportedOperationException} + * if modification is attempted. + * Returns {@code Optional#empty()} if {@code financialPlans} is null. + */ + public Optional> getFinancialPlans() { + return (financialPlans != null) + ? Optional.of(Collections.unmodifiableSet(financialPlans)) : Optional.empty(); + } /** * Sets {@code tags} to this object's {@code tags}. @@ -225,6 +289,9 @@ public boolean equals(Object other) { && Objects.equals(phone, otherEditPersonDescriptor.phone) && Objects.equals(email, otherEditPersonDescriptor.email) && Objects.equals(address, otherEditPersonDescriptor.address) + && Objects.equals(nextOfKinName, otherEditPersonDescriptor.nextOfKinName) + && Objects.equals(nextOfKinPhone, otherEditPersonDescriptor.nextOfKinPhone) + && Objects.equals(financialPlans, otherEditPersonDescriptor.financialPlans) && Objects.equals(tags, otherEditPersonDescriptor.tags); } @@ -235,6 +302,9 @@ public String toString() { .add("phone", phone) .add("email", email) .add("address", address) + .add("nextOfKinName", nextOfKinName) + .add("nextOfKinPhone", nextOfKinPhone) + .add("financialPlans", financialPlans) .add("tags", tags) .toString(); } diff --git a/src/main/java/seedu/address/logic/commands/FindCommand.java b/src/main/java/seedu/address/logic/commands/FindCommand.java index 72b9eddd3a7..7ea5c24638d 100644 --- a/src/main/java/seedu/address/logic/commands/FindCommand.java +++ b/src/main/java/seedu/address/logic/commands/FindCommand.java @@ -1,28 +1,39 @@ package seedu.address.logic.commands; import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_FINANCIAL_PLAN; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; + +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. - * Keyword matching is case insensitive. + * Finds and lists all persons in address book whose name, tags or financial plans contains any of the argument + * keywords. Keyword matching is case-insensitive. */ public class FindCommand extends Command { public static final String COMMAND_WORD = "find"; - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all persons whose names contain any of " - + "the specified keywords (case-insensitive) and displays them as a list with index numbers.\n" - + "Parameters: KEYWORD [MORE_KEYWORDS]...\n" - + "Example: " + COMMAND_WORD + " alice bob charlie"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all persons whose names, tags or financial" + + " plans contain any of " + + "the specified keywords (case-insensitive) and displays them as a list with index numbers. " + + "At least one argument must be given.\n" + + "Parameters: " + + "[" + PREFIX_NAME + "NAME]... " + + "[" + PREFIX_FINANCIAL_PLAN + "FINANCIAL_PLAN]... " + + "[" + PREFIX_TAG + "TAG]...\n" + + "Example: " + COMMAND_WORD + " " + PREFIX_FINANCIAL_PLAN + "Financial Plan A " + + PREFIX_TAG + "TagA"; - 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/GatherCommand.java b/src/main/java/seedu/address/logic/commands/GatherCommand.java new file mode 100644 index 00000000000..8eec139ee36 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/GatherCommand.java @@ -0,0 +1,76 @@ +//@@author AlyssaPng +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_FINANCIAL_PLAN; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; + +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.model.Model; +import seedu.address.model.person.gatheremail.GatherEmailPrompt; + +/** + * Gathers and lists all persons in address book emails whose details contain a given Financial Plan. + * Keyword matching is case-insensitive. + */ +public class GatherCommand extends Command { + + public static final String COMMAND_WORD = "gather"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Gathers all emails of people whose details match " + + "the desired prompt.\n" + + "Emails can be gather by either Financial Plans or Tags, but not both.\n" + + "Parameters: " + + "[" + PREFIX_FINANCIAL_PLAN + "FINANCIAL_PLAN] or " + + "[" + PREFIX_TAG + "TAG]\n" + + "Example: " + COMMAND_WORD + " " + PREFIX_FINANCIAL_PLAN + "Financial Plan A"; + + public static final String MESSAGE_NO_PERSON_FOUND = "0 persons were found with "; + + private final GatherEmailPrompt prompt; + + /** + * Constructs a new GatherCommand object. + * @param prompt The user's prompt. + */ + public GatherCommand(GatherEmailPrompt prompt) { + this.prompt = prompt; + } + + /** + * Overrides Command execute method. + * @param model {@code Model} which the command should operate on. + * @return The CommandResult depending on the user input. + */ + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + String emails = model.gatherEmails(prompt); + if (emails.isEmpty()) { + return new CommandResult(MESSAGE_NO_PERSON_FOUND + prompt.toString()); + } + return new CommandResult(emails); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof GatherCommand)) { + return false; + } + + GatherCommand otherGatherCommand = (GatherCommand) other; + return prompt.equals(otherGatherCommand.prompt); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("prompt", prompt) + .toString(); + } +} diff --git a/src/main/java/seedu/address/logic/commands/HelpCommand.java b/src/main/java/seedu/address/logic/commands/HelpCommand.java index bf824f91bd0..7ce8c4a0df4 100644 --- a/src/main/java/seedu/address/logic/commands/HelpCommand.java +++ b/src/main/java/seedu/address/logic/commands/HelpCommand.java @@ -9,10 +9,23 @@ public class HelpCommand extends Command { public static final String COMMAND_WORD = "help"; - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Shows program usage instructions.\n" + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Shows program usage instructions " + + "and available keywords.\n" + "Example: " + COMMAND_WORD; - public static final String SHOWING_HELP_MESSAGE = "Opened help window."; + public static final String SHOWING_HELP_MESSAGE = "Opened help window. Available keywords:\n" + + AddCommand.COMMAND_WORD + ", " + + ClearCommand.COMMAND_WORD + ", " + + CompleteCommand.COMMAND_WORD + ", " + + DeleteCommand.COMMAND_WORD + ", " + + EditCommand.COMMAND_WORD + ", " + + ExitCommand.COMMAND_WORD + ", " + + FindCommand.COMMAND_WORD + ", " + + GatherCommand.COMMAND_WORD + ", " + + COMMAND_WORD + ", " + + ListCommand.COMMAND_WORD + ", " + + ScheduleCommand.COMMAND_WORD + ", " + + SortCommand.COMMAND_WORD; @Override public CommandResult execute(Model model) { diff --git a/src/main/java/seedu/address/logic/commands/ScheduleCommand.java b/src/main/java/seedu/address/logic/commands/ScheduleCommand.java new file mode 100644 index 00000000000..a5b257f8425 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ScheduleCommand.java @@ -0,0 +1,110 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_APPOINTMENT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_APPOINTMENT_DATE; +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.appointment.Appointment; +import seedu.address.model.appointment.NullAppointment; +import seedu.address.model.person.Person; + +/** + * Schedules an appointment for a person. + */ +public class ScheduleCommand extends Command { + + public static final String COMMAND_WORD = "schedule"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Schedules an appointment of the person identified " + + "by the index number used in the displayed person list. " + + "\nParameters: INDEX(must be a positive integer) " + + PREFIX_APPOINTMENT + "Appointment Name " + + PREFIX_APPOINTMENT_DATE + "Appointment Date " + + "\nExample: " + COMMAND_WORD + " 1 " + + PREFIX_APPOINTMENT + "Review Insurance " + + PREFIX_APPOINTMENT_DATE + "01-01-2023 20:00"; + public static final String MESSAGE_SCHEDULE_SUCCESS = "New appointment added: %1$s"; + public static final String CONFIRM_OVERRIDE_MESSAGE = "Previous appointment found."; + private final Index index; + private final Appointment toAdd; + + /** + * Creates a ScheduleCommand to schedule the specified {@code Appointment} to the indexed person. + * + * @param index The index of the person. + * @param appointment The Appointment to schedule. + */ + public ScheduleCommand(Index index, Appointment appointment) { + requireNonNull(index); + requireNonNull(appointment); + + this.index = index; + this.toAdd = appointment; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredPersonList(); + + if (index.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + Person personToEdit = lastShownList.get(index.getZeroBased()); + + if (!personToEdit.hasNullAppointment()) { + return new CommandResult(CONFIRM_OVERRIDE_MESSAGE, + false, true, personToEdit, toAdd); + } + + assert personToEdit.getAppointment() instanceof NullAppointment; + + Person personWithApt = createPersonWithAppointment(personToEdit); + + assert personWithApt.getAppointment() instanceof Appointment + : "Schedule Command: person should have appointment"; + + toAdd.setPerson(personWithApt); //sets person to appointment + + model.setPerson(personToEdit, personWithApt); + model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + return new CommandResult(String.format(MESSAGE_SCHEDULE_SUCCESS, Messages.format(personWithApt))); + + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof ScheduleCommand)) { + return false; + } + + ScheduleCommand otherScheduleCommand = (ScheduleCommand) other; + return index.equals(otherScheduleCommand.index) + && toAdd.equals(otherScheduleCommand.toAdd); + } + + /** + * Returns the {@code Person} with the scheduled Appointment. + * + * @param personToEdit The Person the appointment is scheduled to. + * @return The Person with scheduled appointment. + */ + private Person createPersonWithAppointment(Person personToEdit) { + return new Person(personToEdit.getName(), personToEdit.getPhone(), personToEdit.getEmail(), + personToEdit.getAddress(), personToEdit.getNextOfKinName(), personToEdit.getNextOfKinPhone(), + personToEdit.getFinancialPlans(), personToEdit.getTags(), toAdd); + } +} 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..af09c51d770 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/SortCommand.java @@ -0,0 +1,67 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.Comparator; + +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.Messages; +import seedu.address.model.Model; +import seedu.address.model.person.Person; + + +/** + * Sorts all persons in the list by lexicographical order barring capitalisation. + */ +public class SortCommand extends Command { + + public static final String COMMAND_WORD = "sort"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Sorts the current list based currently available " + + "sort functions.\n" + + "Currently available sorting variations: \n" + + "-name (lexicographical name sort)\n-appointment (earliest timing first)\n" + + "Parameters: sort [variation]...\n" + + "Example: sort name"; + + private final Comparator comparator; + + /** + * Default constructor for a SortCommand. + * @param comparator comparator to be used to sort the list. + */ + public SortCommand(Comparator comparator) { + requireNonNull(comparator); + this.comparator = comparator; + } + + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + model.sortFilteredPersonList(comparator); + return new CommandResult( + String.format(Messages.MESSAGE_PERSONS_LISTED_OVERVIEW, model.getFilteredPersonList().size())); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof SortCommand)) { + return false; + } + + SortCommand otherSortCommand = (SortCommand) other; + return comparator.equals(otherSortCommand.comparator); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("comparator", comparator) + .toString(); + } +} diff --git a/src/main/java/seedu/address/logic/parser/AddCommandParser.java b/src/main/java/seedu/address/logic/parser/AddCommandParser.java index 4ff1a97ed77..22928d85ca3 100644 --- a/src/main/java/seedu/address/logic/parser/AddCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/AddCommandParser.java @@ -3,7 +3,10 @@ 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_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_FINANCIAL_PLAN; import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NEXT_OF_KIN_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NEXT_OF_KIN_PHONE; import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; @@ -12,9 +15,14 @@ import seedu.address.logic.commands.AddCommand; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.appointment.NullAppointment; +import seedu.address.model.appointment.ScheduleItem; +import seedu.address.model.financialplan.FinancialPlan; import seedu.address.model.person.Address; import seedu.address.model.person.Email; import seedu.address.model.person.Name; +import seedu.address.model.person.NextOfKinName; +import seedu.address.model.person.NextOfKinPhone; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; import seedu.address.model.tag.Tag; @@ -30,23 +38,8 @@ 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); - - 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)); - - Person person = new Person(name, phone, email, address, tagList); - + ArgumentMultimap argMultimap = processRawCommand(args); + Person person = createPerson(argMultimap); return new AddCommand(person); } @@ -57,5 +50,55 @@ public AddCommand parse(String args) throws ParseException { private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); } + /** + * Processes the given {@code String} into an ArgumentMultimap in the context of an AddCommand. + * + * @param args Raw command string. + * @return ArgumentMultimap containing argument values to create a Person with. + * @throws ParseException if the string contains invalid arguments or duplicate arguments for Person fields that + * require exactly one argument. + */ + private ArgumentMultimap processRawCommand(String args) throws ParseException { + ArgumentMultimap argumentMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, + PREFIX_NEXT_OF_KIN_NAME, PREFIX_NEXT_OF_KIN_PHONE, PREFIX_FINANCIAL_PLAN, PREFIX_TAG); + + if (!arePrefixesPresent(argumentMultimap, PREFIX_NAME, PREFIX_ADDRESS, PREFIX_PHONE, PREFIX_EMAIL, + PREFIX_NEXT_OF_KIN_NAME, PREFIX_NEXT_OF_KIN_PHONE) + || !argumentMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE)); + } + + argumentMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, + PREFIX_NEXT_OF_KIN_NAME, PREFIX_NEXT_OF_KIN_PHONE); + + return argumentMultimap; + } + /** + * Creates a Person from the given ArgumentMultimap. This method ignores fields in the multimap that are not + * relevant to the Person class and will use the last value in the multimap if any prefixes are mapped to + * multiple values. + * + * @param argumentMultimap Multimap to draw values from. + * @return Person object. + * @throws ParseException if the multimap contains invalid values necessary to create a Person. + */ + private Person createPerson(ArgumentMultimap argumentMultimap) throws ParseException { + Name name = ParserUtil.parseName(argumentMultimap.getValue(PREFIX_NAME).get()); + Phone phone = ParserUtil.parsePhone(argumentMultimap.getValue(PREFIX_PHONE).get()); + Email email = ParserUtil.parseEmail(argumentMultimap.getValue(PREFIX_EMAIL).get()); + Address address = ParserUtil.parseAddress(argumentMultimap.getValue(PREFIX_ADDRESS).get()); + NextOfKinName nokName = ParserUtil.parseNextOfKinName(argumentMultimap.getValue(PREFIX_NEXT_OF_KIN_NAME).get()); + NextOfKinPhone nokPhone = ParserUtil + .parseNextOfKinPhone((argumentMultimap.getValue(PREFIX_NEXT_OF_KIN_PHONE)).get()); + Set financialPlanList = ParserUtil.parseFinancialPlans( + argumentMultimap.getAllValues(PREFIX_FINANCIAL_PLAN)); + Set tagList = ParserUtil.parseTags(argumentMultimap.getAllValues(PREFIX_TAG)); + ScheduleItem appointment = NullAppointment.getNullAppointment(); + + Person person = new Person(name, phone, email, address, nokName, nokPhone, + financialPlanList, tagList, appointment); + return person; + } } diff --git a/src/main/java/seedu/address/logic/parser/AddressBookParser.java b/src/main/java/seedu/address/logic/parser/AddressBookParser.java index 3149ee07e0b..97b44fe3a1e 100644 --- a/src/main/java/seedu/address/logic/parser/AddressBookParser.java +++ b/src/main/java/seedu/address/logic/parser/AddressBookParser.java @@ -11,12 +11,16 @@ import seedu.address.logic.commands.AddCommand; import seedu.address.logic.commands.ClearCommand; import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CompleteCommand; import seedu.address.logic.commands.DeleteCommand; import seedu.address.logic.commands.EditCommand; import seedu.address.logic.commands.ExitCommand; import seedu.address.logic.commands.FindCommand; +import seedu.address.logic.commands.GatherCommand; import seedu.address.logic.commands.HelpCommand; import seedu.address.logic.commands.ListCommand; +import seedu.address.logic.commands.ScheduleCommand; +import seedu.address.logic.commands.SortCommand; import seedu.address.logic.parser.exceptions.ParseException; /** @@ -57,6 +61,7 @@ public Command parseCommand(String userInput) throws ParseException { return new AddCommandParser().parse(arguments); case EditCommand.COMMAND_WORD: + return new EditCommandParser().parse(arguments); case DeleteCommand.COMMAND_WORD: @@ -77,6 +82,18 @@ public Command parseCommand(String userInput) throws ParseException { case HelpCommand.COMMAND_WORD: return new HelpCommand(); + case ScheduleCommand.COMMAND_WORD: + return new ScheduleCommandParser().parse(arguments); + + case SortCommand.COMMAND_WORD: + return new SortCommandParser().parse(arguments); + + case GatherCommand.COMMAND_WORD: + return new GatherCommandParser().parse(arguments); + + case CompleteCommand.COMMAND_WORD: + return new CompleteCommandParser().parse(arguments); + default: logger.finer("This user input caused a ParseException: " + userInput); throw new ParseException(MESSAGE_UNKNOWN_COMMAND); diff --git a/src/main/java/seedu/address/logic/parser/CliSyntax.java b/src/main/java/seedu/address/logic/parser/CliSyntax.java index 75b1a9bf119..e6cc736355b 100644 --- a/src/main/java/seedu/address/logic/parser/CliSyntax.java +++ b/src/main/java/seedu/address/logic/parser/CliSyntax.java @@ -10,6 +10,10 @@ public class CliSyntax { public static final Prefix PREFIX_PHONE = new Prefix("p/"); public static final Prefix PREFIX_EMAIL = new Prefix("e/"); public static final Prefix PREFIX_ADDRESS = new Prefix("a/"); + public static final Prefix PREFIX_FINANCIAL_PLAN = new Prefix("fp/"); public static final Prefix PREFIX_TAG = new Prefix("t/"); - + public static final Prefix PREFIX_NEXT_OF_KIN_NAME = new Prefix("nk/"); + public static final Prefix PREFIX_NEXT_OF_KIN_PHONE = new Prefix("nkp/"); + public static final Prefix PREFIX_APPOINTMENT = new Prefix("ap/"); + public static final Prefix PREFIX_APPOINTMENT_DATE = new Prefix("d/"); } diff --git a/src/main/java/seedu/address/logic/parser/CompleteCommandParser.java b/src/main/java/seedu/address/logic/parser/CompleteCommandParser.java new file mode 100644 index 00000000000..5eed96a0047 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/CompleteCommandParser.java @@ -0,0 +1,70 @@ +package seedu.address.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_APPOINTMENT_DATE; + +import java.time.LocalDate; +import java.util.Optional; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.CompleteByDate; +import seedu.address.logic.commands.CompleteByIndex; +import seedu.address.logic.commands.CompleteCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parse input arguments and creates a new CompleteCommand object + */ +public class CompleteCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the CompleteCommand + * and returns an CompleteCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + @Override + public CompleteCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_APPOINTMENT_DATE); + + String strIndex = argMultimap.getPreamble().trim(); + Optional strDate = argMultimap.getValue(PREFIX_APPOINTMENT_DATE); + + if (!isValidArguments(strIndex, strDate)) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + CompleteCommand.MESSAGE_USAGE)); + } + + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_APPOINTMENT_DATE); + + if (strDate.isPresent()) { + LocalDate date = ParserUtil.parseDate(argMultimap.getValue(PREFIX_APPOINTMENT_DATE).get()); + return new CompleteByDate(date); + } else { + try { + Index index = ParserUtil.parseIndex(strIndex); + return new CompleteByIndex(index); + } catch (ParseException pe) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + CompleteCommand.MESSAGE_USAGE), pe); + } + } + } + + /** + * Checks if either {@code strIndex} or {@code strDate} is present but not both. + */ + public static boolean isValidArguments(String strIndex, Optional strDate) { + //if no input + if (strIndex.isEmpty() && strDate.isEmpty()) { + return false; + } + //if both date and index given + if (!strIndex.isEmpty() && strDate.isPresent()) { + return false; + } + + return true; + } +} diff --git a/src/main/java/seedu/address/logic/parser/EditCommandParser.java b/src/main/java/seedu/address/logic/parser/EditCommandParser.java index 46b3309a78b..bfc86601e6f 100644 --- a/src/main/java/seedu/address/logic/parser/EditCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/EditCommandParser.java @@ -4,7 +4,10 @@ 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_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_FINANCIAL_PLAN; import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NEXT_OF_KIN_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NEXT_OF_KIN_PHONE; import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; @@ -17,6 +20,7 @@ import seedu.address.logic.commands.EditCommand; import seedu.address.logic.commands.EditCommand.EditPersonDescriptor; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.financialplan.FinancialPlan; import seedu.address.model.tag.Tag; /** @@ -31,40 +35,28 @@ 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); - - Index index; - - try { - index = ParserUtil.parseIndex(argMultimap.getPreamble()); - } catch (ParseException pe) { - throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditCommand.MESSAGE_USAGE), pe); - } - - argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS); - - EditPersonDescriptor editPersonDescriptor = new EditPersonDescriptor(); + ArgumentMultimap argMultimap = processRawCommand(args); + Index index = getIndex(argMultimap); + EditPersonDescriptor editPersonDescriptor = makeEditPersonDescriptor(argMultimap); + return new EditCommand(index, editPersonDescriptor); + } - if (argMultimap.getValue(PREFIX_NAME).isPresent()) { - editPersonDescriptor.setName(ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get())); - } - if (argMultimap.getValue(PREFIX_PHONE).isPresent()) { - editPersonDescriptor.setPhone(ParserUtil.parsePhone(argMultimap.getValue(PREFIX_PHONE).get())); - } - if (argMultimap.getValue(PREFIX_EMAIL).isPresent()) { - editPersonDescriptor.setEmail(ParserUtil.parseEmail(argMultimap.getValue(PREFIX_EMAIL).get())); - } - if (argMultimap.getValue(PREFIX_ADDRESS).isPresent()) { - editPersonDescriptor.setAddress(ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get())); - } - parseTagsForEdit(argMultimap.getAllValues(PREFIX_TAG)).ifPresent(editPersonDescriptor::setTags); + /** + * Parses {@code Collection financialPlans} into a {@code Set} + * if {@code financialPlans} is non-empty. + * If {@code financialPlans} contain only one element which is an empty string, it will be parsed into a + * {@code Set} containing zero financial plans. + */ + private Optional> parseFinancialPlansForEdit( + Collection financialPlans) throws ParseException { + assert financialPlans != null; - if (!editPersonDescriptor.isAnyFieldEdited()) { - throw new ParseException(EditCommand.MESSAGE_NOT_EDITED); + if (financialPlans.isEmpty()) { + return Optional.empty(); } - - return new EditCommand(index, editPersonDescriptor); + Collection financialPlanSet = financialPlans.size() == 1 && financialPlans.contains("") + ? Collections.emptySet() : financialPlans; + return Optional.of(ParserUtil.parseFinancialPlans(financialPlanSet)); } /** @@ -82,4 +74,77 @@ private Optional> parseTagsForEdit(Collection tags) throws Pars return Optional.of(ParserUtil.parseTags(tagSet)); } + /** + * Processes the given {@code String} into an ArgumentMultimap in the context of an EditCommand. + * + * @param args Raw command string. + * @return ArgumentMultimap containing argument values to edit a Person with. + * @throws ParseException if the string contains extra arguments for Person fields that require at most + * one argument. + */ + private ArgumentMultimap processRawCommand(String args) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, + PREFIX_NEXT_OF_KIN_NAME, PREFIX_NEXT_OF_KIN_PHONE, PREFIX_FINANCIAL_PLAN, PREFIX_TAG); + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, + PREFIX_NEXT_OF_KIN_NAME, PREFIX_NEXT_OF_KIN_PHONE); + return argMultimap; + } + + /** + * Gets the index from an ArgumentMultimap. + * + * @param argumentMultimap Multimap to extract index from. + * @return Index value. + * @throws ParseException If the map does not contain a valid index in its preamble. + */ + private Index getIndex(ArgumentMultimap argumentMultimap) throws ParseException { + try { + return ParserUtil.parseIndex(argumentMultimap.getPreamble()); + } catch (ParseException pe) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditCommand.MESSAGE_USAGE), pe); + } + } + + /** + * Creates an EditPersonDescriptor using the values from an ArgumentMultimap. Will ignore values not relevant to the + * EditPersonDescriptor class. + * + * @param argumentMultimap Multimap to draw values from. + * @return EditPersonDescriptor to pass to EditCommand. + * @throws ParseException if any relevant values are invalid or if the resulting EditPersonDescriptor has no values + * to edit. + */ + private EditPersonDescriptor makeEditPersonDescriptor(ArgumentMultimap argumentMultimap) throws ParseException { + EditPersonDescriptor editPersonDescriptor = new EditPersonDescriptor(); + if (argumentMultimap.getValue(PREFIX_NAME).isPresent()) { + editPersonDescriptor.setName(ParserUtil.parseName(argumentMultimap.getValue(PREFIX_NAME).get())); + } + if (argumentMultimap.getValue(PREFIX_PHONE).isPresent()) { + editPersonDescriptor.setPhone(ParserUtil.parsePhone(argumentMultimap.getValue(PREFIX_PHONE).get())); + } + if (argumentMultimap.getValue(PREFIX_EMAIL).isPresent()) { + editPersonDescriptor.setEmail(ParserUtil.parseEmail(argumentMultimap.getValue(PREFIX_EMAIL).get())); + } + if (argumentMultimap.getValue(PREFIX_ADDRESS).isPresent()) { + editPersonDescriptor.setAddress(ParserUtil.parseAddress(argumentMultimap.getValue(PREFIX_ADDRESS).get())); + } + if (argumentMultimap.getValue(PREFIX_NEXT_OF_KIN_NAME).isPresent()) { + editPersonDescriptor.setNextOfKinName(ParserUtil.parseNextOfKinName(argumentMultimap + .getValue(PREFIX_NEXT_OF_KIN_NAME) + .get())); + } + if (argumentMultimap.getValue(PREFIX_NEXT_OF_KIN_PHONE).isPresent()) { + editPersonDescriptor.setNextOfKinPhone(ParserUtil.parseNextOfKinPhone(argumentMultimap + .getValue(PREFIX_NEXT_OF_KIN_PHONE) + .get())); + } + parseFinancialPlansForEdit(argumentMultimap.getAllValues(PREFIX_FINANCIAL_PLAN)) + .ifPresent(editPersonDescriptor::setFinancialPlans); + parseTagsForEdit(argumentMultimap.getAllValues(PREFIX_TAG)).ifPresent(editPersonDescriptor::setTags); + if (!editPersonDescriptor.isAnyFieldEdited()) { + throw new ParseException(EditCommand.MESSAGE_NOT_EDITED); + } + return 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..80eb8b53cfd 100644 --- a/src/main/java/seedu/address/logic/parser/FindCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/FindCommandParser.java @@ -1,12 +1,19 @@ package seedu.address.logic.parser; +import static java.util.Objects.requireNonNull; import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_FINANCIAL_PLAN; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; -import java.util.Arrays; +import java.util.List; import seedu.address.logic.commands.FindCommand; import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.person.NameContainsKeywordsPredicate; +import seedu.address.model.person.predicates.CombinedPredicate; +import seedu.address.model.person.predicates.FinancialPlanContainsKeywordsPredicate; +import seedu.address.model.person.predicates.NameContainsKeywordsPredicate; +import seedu.address.model.person.predicates.TagContainsKeywordsPredicate; /** * Parses input arguments and creates a new FindCommand object @@ -19,15 +26,67 @@ public class FindCommandParser implements Parser { * @throws ParseException if the user input does not conform the expected format */ public FindCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = processRawCommand(args); + + List nameKeywords = getNameList(argMultimap); + List tagKeywords = getTagList(argMultimap); + List financialPlanKeywords = getFinancialPlanList(argMultimap); + + CombinedPredicate combinedPredicate = new CombinedPredicate( + new FinancialPlanContainsKeywordsPredicate(financialPlanKeywords), + new NameContainsKeywordsPredicate(nameKeywords), + new TagContainsKeywordsPredicate(tagKeywords) + ); + + return new FindCommand(combinedPredicate); + } + + /** + * Processes the raw command string into an ArgumentMultimap. + * + * @param args Raw command string. + * @return ArgumentMultimap containing argument values to find. + * @throws ParseException if the string contains no arguments or has a non-empty preamble. + */ + private ArgumentMultimap processRawCommand(String args) throws ParseException { + requireNonNull(args); String trimmedArgs = args.trim(); - if (trimmedArgs.isEmpty()) { + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_NAME, + PREFIX_FINANCIAL_PLAN, PREFIX_TAG); + if (trimmedArgs.isEmpty() || !argMultimap.getPreamble().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 argMultimap; + } + /** + * Gets the list of values mapped to the given prefix from the given multimap. + * + * @param argumentMultimap Multimap to draw values from. + * @param prefix Key value for the multimap. + * @param validator Validator for values drawn from multimap. + * @return List of String values from the multimap. + * @throws ParseException if any value in the list does not pass the validator's requirements. + */ + private List getFieldList(ArgumentMultimap argumentMultimap, + Prefix prefix, Validator validator) throws ParseException { + requireNonNull(argumentMultimap); + requireNonNull(prefix); + requireNonNull(validator); + List list = argumentMultimap.getAllValues(prefix); + list.replaceAll(String::trim); + for (String string : list) { + validator.validate(string); + } + return list; + } + private List getNameList(ArgumentMultimap argumentMultimap) throws ParseException { + return getFieldList(argumentMultimap, PREFIX_NAME, ParserUtil::validateName); + } + private List getFinancialPlanList(ArgumentMultimap argumentMultimap) throws ParseException { + return getFieldList(argumentMultimap, PREFIX_FINANCIAL_PLAN, ParserUtil::validateFinancialPlan); + } + private List getTagList(ArgumentMultimap argumentMultimap) throws ParseException { + return getFieldList(argumentMultimap, PREFIX_TAG, ParserUtil::validateTag); } - } diff --git a/src/main/java/seedu/address/logic/parser/GatherCommandParser.java b/src/main/java/seedu/address/logic/parser/GatherCommandParser.java new file mode 100644 index 00000000000..e8adf16b327 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/GatherCommandParser.java @@ -0,0 +1,78 @@ +//@@author AlyssaPng +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_FINANCIAL_PLAN; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import static seedu.address.logic.parser.ParserUtil.validateFinancialPlan; +import static seedu.address.logic.parser.ParserUtil.validateTag; +import static seedu.address.model.financialplan.FinancialPlan.isValidFinancialPlanName; +import static seedu.address.model.tag.Tag.isValidTagName; + +import seedu.address.logic.commands.GatherCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.gatheremail.GatherEmailByFinancialPlan; +import seedu.address.model.person.gatheremail.GatherEmailByTag; + +/** + * Parses input arguments and creates a new GatherCommand object + */ +public class GatherCommandParser implements Parser { + /** + * Parses the given {@code String} of arguments in the context of the GatherCommand. + * and returns a GatherCommand object for execution. + * @throws ParseException if the user input does not conform the expected format. + */ + public GatherCommand parse(String args) throws ParseException { + String trimmedArgs = args.trim(); + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_FINANCIAL_PLAN, PREFIX_TAG); + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_FINANCIAL_PLAN, PREFIX_TAG); + + if (argMultimap.getValue(PREFIX_FINANCIAL_PLAN).isPresent() && argMultimap.getValue(PREFIX_TAG).isPresent()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, GatherCommand.MESSAGE_USAGE)); + } + + if (argMultimap.getValue(PREFIX_FINANCIAL_PLAN).isPresent()) { + return createGatherByFinancialPlan(trimmedArgs); + } + + if (argMultimap.getValue(PREFIX_TAG).isPresent()) { + return createGatherByTag(trimmedArgs); + } + + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, GatherCommand.MESSAGE_USAGE)); + } + + /** + * Removes the {@code prefix} from the {@code currArg}. + */ + public String removePrefix(String currArg, Prefix prefix) { + String noPrefix = currArg.replace(prefix.getPrefix(), ""); + String trimmedArg = noPrefix.trim(); + return trimmedArg; + } + + /** + * Creates a GatherCommand object for Financial Plan from {@code argString} + */ + private GatherCommand createGatherByFinancialPlan(String argString) throws ParseException { + String financialPlanArg = removePrefix(argString, PREFIX_FINANCIAL_PLAN); + validateFinancialPlan(financialPlanArg); + assert isValidFinancialPlanName(financialPlanArg) : "Prompt has to meets valid FP requirements"; + GatherEmailByFinancialPlan prompt = new GatherEmailByFinancialPlan(financialPlanArg); + return new GatherCommand(prompt); + } + + /** + * Creates a GatherCommand object for Tag from {@code argString} + */ + private GatherCommand createGatherByTag(String argString) throws ParseException { + String tagArg = removePrefix(argString, PREFIX_TAG); + validateTag(tagArg); + assert isValidTagName(tagArg) : "Prompt has to meets valid Tag requirements"; + GatherEmailByTag prompt = new GatherEmailByTag(tagArg); + return new GatherCommand(prompt); + } +} diff --git a/src/main/java/seedu/address/logic/parser/ParserUtil.java b/src/main/java/seedu/address/logic/parser/ParserUtil.java index b117acb9c55..e8bed690ec5 100644 --- a/src/main/java/seedu/address/logic/parser/ParserUtil.java +++ b/src/main/java/seedu/address/logic/parser/ParserUtil.java @@ -1,25 +1,35 @@ package seedu.address.logic.parser; import static java.util.Objects.requireNonNull; +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.YearMonth; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; import java.util.Collection; import java.util.HashSet; import java.util.Set; import seedu.address.commons.core.index.Index; import seedu.address.commons.util.StringUtil; +import seedu.address.logic.commands.CompleteCommand; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.appointment.Appointment; +import seedu.address.model.financialplan.FinancialPlan; import seedu.address.model.person.Address; import seedu.address.model.person.Email; import seedu.address.model.person.Name; +import seedu.address.model.person.NextOfKinName; +import seedu.address.model.person.NextOfKinPhone; import seedu.address.model.person.Phone; import seedu.address.model.tag.Tag; /** - * Contains utility methods used for parsing strings in the various *Parser classes. + * Contains utility methods used for parsing and validating strings in the various *Parser classes. */ public class ParserUtil { - public static final String MESSAGE_INVALID_INDEX = "Index is not a non-zero unsigned integer."; /** @@ -94,6 +104,151 @@ public static Email parseEmail(String email) throws ParseException { } return new Email(trimmedEmail); } + /** + * Parses a {@code String nokName} into a {@code NextOfKinName}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code nokName} is invalid. + */ + public static NextOfKinName parseNextOfKinName(String nokName) throws ParseException { + requireNonNull(nokName); + String trimmedNokName = nokName.trim(); + if (!NextOfKinName.isValidName(trimmedNokName)) { + throw new ParseException(NextOfKinName.MESSAGE_CONSTRAINTS); + } + return new NextOfKinName(trimmedNokName); + } + /** + * Parses a {@code String nokPhone} into an {@code NextOfKinPhone}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code nokPhone} is invalid. + */ + public static NextOfKinPhone parseNextOfKinPhone(String nokPhone) throws ParseException { + requireNonNull(nokPhone); + String trimmedNokPhone = nokPhone.trim(); + if (!NextOfKinPhone.isValidPhone(trimmedNokPhone)) { + throw new ParseException(NextOfKinPhone.MESSAGE_CONSTRAINTS); + } + return new NextOfKinPhone(trimmedNokPhone); + } + + /** + * Parses a {@code String financialPlan} into a {@code FinancialPlan}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code tag} is invalid. + */ + public static FinancialPlan parseFinancialPlan(String financialPlan) throws ParseException { + requireNonNull(financialPlan); + String trimmedFinancialPlan = financialPlan.trim(); + if (!FinancialPlan.isValidFinancialPlanName(trimmedFinancialPlan)) { + throw new ParseException(FinancialPlan.MESSAGE_CONSTRAINTS); + } + return new FinancialPlan(trimmedFinancialPlan); + } + + /** + * Parses {@code Collection financialPlans} into a {@code Set}. + */ + public static Set parseFinancialPlans(Collection financialPlans) throws ParseException { + requireNonNull(financialPlans); + final Set financialPlanSet = new HashSet<>(); + for (String financialPlanName : financialPlans) { + financialPlanSet.add(parseFinancialPlan(financialPlanName)); + } + return financialPlanSet; + } + + /** + * Parses a {@code String aptName} and {@code String aptDateString} into an {@code Appointment}. + * + * @param appointmentName The name of the appointment. + * @param appointmentDateString The appointment date and time. + * @return The Appointment. + * @throws ParseException If the given {@code aptName} or {@code aptDateString}. + */ + public static Appointment parseAppointment(String appointmentName, String appointmentDateString) + throws ParseException { + requireNonNull(appointmentName); + requireNonNull(appointmentDateString); + + String trimmedAppointmentName = appointmentName.trim(); + + if (!Appointment.isValidDesc(appointmentName)) { + throw new ParseException(Appointment.MESSAGE_DESC_CONSTRAINTS); + } + + if (!Appointment.isValidDateFormat(appointmentDateString)) { + throw new ParseException(Appointment.MESSAGE_DATE_CONSTRAINTS); + } + + try { + LocalDateTime appointmentDate = parseAppointmentDate(appointmentDateString); + return new Appointment(trimmedAppointmentName, appointmentDate); + } catch (ParseException e) { + throw new ParseException(Appointment.MESSAGE_INVALID_DATE); + } + } + + /** + * Parses a {@code String appointmentDate} into a {@code LocalDateTime}. + * + * @throws ParseException if the given {@code date} is invalid. + */ + public static LocalDateTime parseAppointmentDate(String appointmentDateTime) throws ParseException { + String date = appointmentDateTime.split(" ")[0]; + + try { + YearMonth yearMonth = YearMonth.parse(date, Appointment.DATE_FORMATTER); + if (!isValidDay(yearMonth, date)) { + throw new ParseException(Appointment.MESSAGE_INVALID_DATE); + } + + return Appointment.parseAppointmentDate(appointmentDateTime); + } catch (DateTimeParseException e) { + throw new ParseException(Appointment.MESSAGE_INVALID_DATE); + } + + } + + /** + * Parses a {@code String date} into a {@code LocalDate}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code date} is invalid. + */ + public static LocalDate parseDate(String date) throws ParseException { + requireNonNull(date); + String trimmedDate = date.trim(); + + DateTimeFormatter dateFormatter = Appointment.DATE_FORMATTER; + + if (!trimmedDate.matches(Appointment.VALIDATION_DATE_REGEX)) { + throw new ParseException(CompleteCommand.MESSAGE_INVALID_DATE_FORMAT); + } + + try { + YearMonth yearMonth = YearMonth.parse(date, dateFormatter); + if (!isValidDay(yearMonth, date)) { + throw new ParseException(CompleteCommand.MESSAGE_INVALID_DATE); + } + return LocalDate.parse(date, dateFormatter); + } catch (DateTimeParseException e) { + throw new ParseException(CompleteCommand.MESSAGE_INVALID_DATE); + } + } + + /** + * Checks if the day of the month in the given user-input date is valid. + * + * @param date The String representation of the date in "dd-MM-yyyy" format. + * @return True if the day is within the valid range for the month. + */ + public static boolean isValidDay(YearMonth yearMonth, String date) { + int day = Integer.parseInt(date.split("-")[0]); + return day >= 1 && day <= yearMonth.lengthOfMonth(); + } /** * Parses a {@code String tag} into a {@code Tag}. @@ -121,4 +276,41 @@ public static Set parseTags(Collection tags) throws ParseException } return tagSet; } + /** + * Validates if a {@code String name} is a valid {@code Name}. + * + * @param input String to validate. + * @throws ParseException if the given string is invalid. + */ + public static void validateName(String input) throws ParseException { + if (!Name.isValidName(input)) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, Name.MESSAGE_CONSTRAINTS)); + } + } + /** + * Validates if a {@code String financial plan} is a valid name for a {@code FinancialPlan}. + * + * @param input String to validate. + * @throws ParseException if the given string is invalid. + */ + public static void validateFinancialPlan(String input) throws ParseException { + if (!FinancialPlan.isValidFinancialPlanName(input)) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, FinancialPlan.MESSAGE_CONSTRAINTS)); + } + } + + /** + * Validates if a {@code String tag} is a valid name for a {@code Tag}. + * + * @param input String to validate. + * @throws ParseException if the given string is invalid. + */ + public static void validateTag(String input) throws ParseException { + if (!Tag.isValidTagName(input)) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, Tag.MESSAGE_CONSTRAINTS)); + } + } } diff --git a/src/main/java/seedu/address/logic/parser/ScheduleCommandParser.java b/src/main/java/seedu/address/logic/parser/ScheduleCommandParser.java new file mode 100644 index 00000000000..715882f54ce --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/ScheduleCommandParser.java @@ -0,0 +1,57 @@ +package seedu.address.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_APPOINTMENT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_APPOINTMENT_DATE; +import static seedu.address.logic.parser.ParserUtil.parseAppointment; + +import java.util.stream.Stream; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.ScheduleCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.appointment.Appointment; + +/** + * Parses the input arguments and creates a new ScheduleCommand object + */ +public class ScheduleCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the ScheduleCommand + * and returns a ScheduleCommand object for execution. + * + * @throws ParseException if the user input does not conform to expected format. + */ + public ScheduleCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_APPOINTMENT, PREFIX_APPOINTMENT_DATE); + + if (!arePrefixesPresent(argMultimap, PREFIX_APPOINTMENT, PREFIX_APPOINTMENT_DATE) + || argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, ScheduleCommand.MESSAGE_USAGE)); + } + + Index index; + try { + index = ParserUtil.parseIndex(argMultimap.getPreamble()); + } catch (ParseException pe) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, ScheduleCommand.MESSAGE_USAGE), pe); + } + + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_APPOINTMENT, PREFIX_APPOINTMENT_DATE); + Appointment appointment = parseAppointment(argMultimap.getValue(PREFIX_APPOINTMENT).get(), + argMultimap.getValue(PREFIX_APPOINTMENT_DATE).get()); + + assert appointment.value != null; + assert appointment.date != null; + + return new ScheduleCommand(index, appointment); + } + + private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } +} diff --git a/src/main/java/seedu/address/logic/parser/SortCommandParser.java b/src/main/java/seedu/address/logic/parser/SortCommandParser.java new file mode 100644 index 00000000000..8d65269313e --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/SortCommandParser.java @@ -0,0 +1,32 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.logic.commands.SortCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.comparator.SortByAppointmentComparator; +import seedu.address.model.person.comparator.SortByNameComparator; + +/** + * Parses input arguments and creates a new SortCommand object + */ +public class SortCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the SortCommand + * and returns a SortCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public SortCommand parse(String args) throws ParseException { + String trimmedArgs = args.trim(); + switch (trimmedArgs) { + case "name": + return new SortCommand(new SortByNameComparator()); + case "appointment": + return new SortCommand(new SortByAppointmentComparator()); + default: + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, SortCommand.MESSAGE_USAGE)); + } + } +} diff --git a/src/main/java/seedu/address/logic/parser/Validator.java b/src/main/java/seedu/address/logic/parser/Validator.java new file mode 100644 index 00000000000..8255a56c9b6 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/Validator.java @@ -0,0 +1,17 @@ +package seedu.address.logic.parser; + +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Functional interface for validating input strings. + */ +@FunctionalInterface +public interface Validator { + /** + * Validates the given string. + * + * @param string value to validate. + * @throws ParseException if the string does not meet the requirements. + */ + void validate(String string) throws ParseException; +} diff --git a/src/main/java/seedu/address/model/AddressBook.java b/src/main/java/seedu/address/model/AddressBook.java index 73397161e84..ed780bd1a4b 100644 --- a/src/main/java/seedu/address/model/AddressBook.java +++ b/src/main/java/seedu/address/model/AddressBook.java @@ -2,19 +2,21 @@ import static java.util.Objects.requireNonNull; +import java.time.LocalDate; import java.util.List; import javafx.collections.ObservableList; import seedu.address.commons.util.ToStringBuilder; import seedu.address.model.person.Person; import seedu.address.model.person.UniquePersonList; +import seedu.address.model.person.gatheremail.GatherEmailPrompt; + /** * Wraps all data at the address-book level * Duplicates are not allowed (by .isSamePerson comparison) */ public class AddressBook implements ReadOnlyAddressBook { - private final UniquePersonList persons; /* @@ -82,7 +84,6 @@ public void addPerson(Person p) { */ public void setPerson(Person target, Person editedPerson) { requireNonNull(editedPerson); - persons.setPerson(target, editedPerson); } @@ -94,6 +95,21 @@ public void removePerson(Person key) { persons.remove(key); } + /** + * Gathers the {@code persons} emails with {@code prompt} from this {@code AddressBook}. + */ + public String gatherEmails(GatherEmailPrompt prompt) { + return persons.gatherEmails(prompt); + }; + + public void clearAppointments(LocalDate date) { + persons.clearAppointments(date); + } + + public boolean hasAppointmentWithDate(LocalDate date) { + return persons.hasAppointmentWithDate(date); + } + //// util methods @Override diff --git a/src/main/java/seedu/address/model/Model.java b/src/main/java/seedu/address/model/Model.java index d54df471c1f..868fc579492 100644 --- a/src/main/java/seedu/address/model/Model.java +++ b/src/main/java/seedu/address/model/Model.java @@ -1,11 +1,15 @@ package seedu.address.model; import java.nio.file.Path; +import java.time.LocalDate; +import java.util.Comparator; import java.util.function.Predicate; import javafx.collections.ObservableList; import seedu.address.commons.core.GuiSettings; +import seedu.address.model.appointment.Appointment; import seedu.address.model.person.Person; +import seedu.address.model.person.gatheremail.GatherEmailPrompt; /** * The API of the Model component. @@ -79,9 +83,30 @@ public interface Model { /** Returns an unmodifiable view of the filtered person list */ ObservableList getFilteredPersonList(); + /** Returns an unmodifiable view of the appointment list */ + ObservableList getAppointmentList(); + /** * Updates the filter of the filtered person list to filter by the given {@code predicate}. * @throws NullPointerException if {@code predicate} is null. */ void updateFilteredPersonList(Predicate predicate); + + /** + * Sorts the filtered person list to sort by the given {@code comparator}. + * @param comparator comparator to be sorted with. + */ + void sortFilteredPersonList(Comparator comparator); + + + /** + * Gathers the emails of person with the given prompt. + * @param prompt The user input for command. + * @return The String representation of all the gathered emails. + */ + String gatherEmails(GatherEmailPrompt prompt); + + void clearAppointments(LocalDate date); + + boolean hasAppointmentWithDate(LocalDate date); } diff --git a/src/main/java/seedu/address/model/ModelManager.java b/src/main/java/seedu/address/model/ModelManager.java index 57bc563fde6..d6906196848 100644 --- a/src/main/java/seedu/address/model/ModelManager.java +++ b/src/main/java/seedu/address/model/ModelManager.java @@ -4,24 +4,36 @@ import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; import java.nio.file.Path; +import java.time.LocalDate; +import java.util.Comparator; import java.util.function.Predicate; import java.util.logging.Logger; +import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.collections.transformation.FilteredList; +import javafx.collections.transformation.SortedList; import seedu.address.commons.core.GuiSettings; import seedu.address.commons.core.LogsCenter; +import seedu.address.model.appointment.Appointment; +import seedu.address.model.appointment.ScheduleItem; +import seedu.address.model.appointment.SortByAppointmentDateComparator; import seedu.address.model.person.Person; +import seedu.address.model.person.gatheremail.GatherEmailPrompt; /** * Represents the in-memory model of the address book data. */ public class ModelManager implements Model { private static final Logger logger = LogsCenter.getLogger(ModelManager.class); + private static final SortByAppointmentDateComparator appointmentComparator = new SortByAppointmentDateComparator(); private final AddressBook addressBook; private final UserPrefs userPrefs; private final FilteredList filteredPersons; + private SortedList sortedPersons; + private ObservableList observableAppointments; + private SortedList sortedAppointments; /** * Initializes a ModelManager with the given addressBook and userPrefs. @@ -33,7 +45,11 @@ public ModelManager(ReadOnlyAddressBook addressBook, ReadOnlyUserPrefs userPrefs this.addressBook = new AddressBook(addressBook); this.userPrefs = new UserPrefs(userPrefs); - filteredPersons = new FilteredList<>(this.addressBook.getPersonList()); + sortedPersons = new SortedList<>(this.addressBook.getPersonList()); + filteredPersons = new FilteredList<>(sortedPersons); + observableAppointments = FXCollections.observableArrayList(); + sortedAppointments = new SortedList<>(observableAppointments, + appointmentComparator); } public ModelManager() { @@ -80,6 +96,7 @@ public void setAddressBookFilePath(Path addressBookFilePath) { @Override public void setAddressBook(ReadOnlyAddressBook addressBook) { this.addressBook.resetData(addressBook); + setAppointmentList(); } @Override @@ -96,6 +113,7 @@ public boolean hasPerson(Person person) { @Override public void deletePerson(Person target) { addressBook.removePerson(target); + setAppointmentList(); } @Override @@ -107,8 +125,13 @@ public void addPerson(Person person) { @Override public void setPerson(Person target, Person editedPerson) { requireAllNonNull(target, editedPerson); - addressBook.setPerson(target, editedPerson); + setAppointmentList(); + } + + @Override + public String gatherEmails(GatherEmailPrompt prompt) { + return addressBook.gatherEmails(prompt); } //=========== Filtered Person List Accessors ============================================================= @@ -119,13 +142,66 @@ public void setPerson(Person target, Person editedPerson) { */ @Override public ObservableList getFilteredPersonList() { + setAppointmentList(); return filteredPersons; } + /** + * Returns an unmodifiable view of the list of {@code Person} backed by the internal list of + * {@code versionedAddressBook} + */ + @Override + public SortedList getAppointmentList() { + setAppointmentList(); + return sortedAppointments; + } + + /** + * Sets the appointment list. This method is called every time a command is being executed. + */ + public void setAppointmentList() { + observableAppointments.clear(); + filteredPersons.forEach(person -> addToAppointmentListIfPresent(person)); + } + + + /** + * Adds ScheduleItem object from person to the appointment list if ScheduleItem object + * is an instance of Appointment and not an instance of NullAppointment. + * @param person Person object that is being examined. + */ + public void addToAppointmentListIfPresent(Person person) { + ScheduleItem scheduleItem = person.getAppointment(); + if (scheduleItem instanceof Appointment) { + Appointment appointment = (Appointment) scheduleItem; + appointment.setPerson(person); + observableAppointments.add(appointment); + } + } + + @Override + public void clearAppointments(LocalDate date) { + addressBook.clearAppointments(date); + setAppointmentList(); + } + + @Override + public boolean hasAppointmentWithDate(LocalDate date) { + return addressBook.hasAppointmentWithDate(date); + } + @Override public void updateFilteredPersonList(Predicate predicate) { requireNonNull(predicate); filteredPersons.setPredicate(predicate); + setAppointmentList(); + } + + + @Override + public void sortFilteredPersonList(Comparator comparator) { + requireNonNull(comparator); + sortedPersons.setComparator(comparator); } @Override diff --git a/src/main/java/seedu/address/model/appointment/Appointment.java b/src/main/java/seedu/address/model/appointment/Appointment.java new file mode 100644 index 00000000000..3c5e5395816 --- /dev/null +++ b/src/main/java/seedu/address/model/appointment/Appointment.java @@ -0,0 +1,156 @@ +package seedu.address.model.appointment; + +import static java.util.Objects.requireNonNull; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.Objects; +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import seedu.address.model.person.Person; + +/** + * Represents an Appointment in the address book. + */ +public class Appointment extends ScheduleItem implements Comparable { + + public static final String MESSAGE_DATE_CONSTRAINTS = "Input Date should be in format of dd-MM-yyyy HH:mm"; + public static final String MESSAGE_INVALID_DATE = "Please ensure you input a valid date and time"; + public static final String MESSAGE_DESC_CONSTRAINTS = "Appointment description should only contain alphanumeric " + + "characters and spaces, and it should not be blank"; + public static final String MESSAGE_APT_CONSTRAINTS = "Invalid appointment string. " + + "Should be (description), (date) (time)"; + public static final String VALIDATION_DATE_TIME_REGEX = "\\d{2}-\\d{2}-\\d{4} \\d{2}:\\d{2}"; + public static final String VALIDATION_DATE_REGEX = "\\d{2}-\\d{2}-\\d{4}"; + public static final String VALIDATION_DESC_REGEX = "[\\p{Alnum}][\\p{Alnum} ]*"; + public static final String VALIDATION_APT_REGEX = "^([\\p{Alnum}][\\p{Alnum} ]*), " + + "(\\d{2}-\\d{2}-\\d{4} \\d{2}:\\d{2})$"; + public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("dd-MM-yyyy HH:mm"); + public static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("dd-MM-yyyy"); + + public final String value; + public final LocalDateTime date; + private Person person; + + /** + * Constructs a {@code Appointment} + * + * @param value A valid description of the Appointment. + * @param date A valid LocalDateTime object representing Appointment date. + */ + public Appointment(String value, LocalDateTime date) { + requireNonNull(value); + this.value = value; + this.date = date; + } + + public String getName() { + return this.value; + } + + public LocalDateTime getDateTime() { + return this.date; + } + public Person getPerson() { + return this.person; + } + @Override + public String toString() { + return value + ", " + date.format(DATE_TIME_FORMATTER); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof Appointment)) { + return false; + } + + Appointment otherAppointment = (Appointment) other; + return value.equals(otherAppointment.value) + && date.equals(otherAppointment.date); + } + @Override + public int hashCode() { + return Objects.hash(value, date); + } + + /** + * Returns true if given string is valid appointment description. + */ + public static boolean isValidDesc(String desc) { + requireNonNull(desc); + return desc.matches(VALIDATION_DESC_REGEX); + } + + /** + * Returns true if given string is valid date time. + */ + public static boolean isValidDateFormat(String date) { + requireNonNull(date); + return date.matches(VALIDATION_DATE_TIME_REGEX); + } + + /** + * Returns true if given string is valid appointment. + */ + public static boolean isValidAppointment(String appointment) { + requireNonNull(appointment); + return appointment.matches(VALIDATION_APT_REGEX); + } + + /** + * Returns the LocaleDateTime object representing the date and time of the Appointment. + * + * @param input The input date and time from user. + * @return The LocaleDateTime object representing Appointment date. + * @throws IllegalArgumentException If input date does not match specified format. + */ + public static LocalDateTime parseAppointmentDate(String input) throws DateTimeParseException { + return LocalDateTime.parse(input, DATE_TIME_FORMATTER); + } + + /** + * Returns an Appointment after parsing the string representation of the appointment. + * + * @param appointment The string representation of the appointment. + * @return The Appointment object. + */ + public static Appointment parseAppointmentDescription(String appointment) { + Logger logger = Logger.getLogger(Appointment.class.getName()); + Pattern pattern = Pattern.compile(VALIDATION_APT_REGEX); + Matcher matcher = pattern.matcher(appointment); + logger.info("Matches: " + matcher.matches()); + String valueField = matcher.group(1).trim(); + String dateField = matcher.group(2).trim(); + + return new Appointment(valueField, parseAppointmentDate(dateField)); + } + + public void setPerson(Person person) { + this.person = person; + } + + @Override + public int compareTo(ScheduleItem scheduleItem) { + if (scheduleItem instanceof NullAppointment) { + return -1; //person with appointment should be smaller to be sorted up on the list + } else { + Appointment appointment = (Appointment) scheduleItem; + return this.date.compareTo(appointment.date); + } + } + + @Override + public boolean isSameDate(LocalDate date) { + return this.date.toLocalDate().equals(date); + } +} diff --git a/src/main/java/seedu/address/model/appointment/NullAppointment.java b/src/main/java/seedu/address/model/appointment/NullAppointment.java new file mode 100644 index 00000000000..950e9e6138a --- /dev/null +++ b/src/main/java/seedu/address/model/appointment/NullAppointment.java @@ -0,0 +1,46 @@ +package seedu.address.model.appointment; + +import java.time.LocalDate; + +/** + * An empty Appointment Object to represent Appointments that have not been made. + */ +public class NullAppointment extends ScheduleItem implements Comparable { + public static final String MESSAGE_NULL_APT = "No Appointment made!"; + private static final NullAppointment nullappointment = new NullAppointment(); + private final String value; + private NullAppointment() { + this.value = MESSAGE_NULL_APT; + } + public static NullAppointment getNullAppointment() { + return nullappointment; + } + @Override + public String toString() { + return MESSAGE_NULL_APT; + } + + @Override + public boolean equals(Object other) { + return other == this; // Check if other is the same NullAppointment + } + + @Override + public int hashCode() { + return this.value.hashCode(); + } + + @Override + public int compareTo(ScheduleItem appointment) { + if (appointment == this) { + return 0; + } else { + return 1; //null appointment returns >0 so it will be sorted further down the list + } + } + + @Override + public boolean isSameDate(LocalDate date) { + return false; + } +} diff --git a/src/main/java/seedu/address/model/appointment/ScheduleItem.java b/src/main/java/seedu/address/model/appointment/ScheduleItem.java new file mode 100644 index 00000000000..66214ac2320 --- /dev/null +++ b/src/main/java/seedu/address/model/appointment/ScheduleItem.java @@ -0,0 +1,14 @@ +package seedu.address.model.appointment; + +import java.time.LocalDate; + +/** + * Abstract class representing schedule item. + * Subclasses should implement compareTo method to define how + * schedule items are compared. + */ +public abstract class ScheduleItem implements Comparable { + public abstract boolean isSameDate(LocalDate date); + @Override + public abstract int compareTo(ScheduleItem scheduleItem); +} diff --git a/src/main/java/seedu/address/model/appointment/SortByAppointmentDateComparator.java b/src/main/java/seedu/address/model/appointment/SortByAppointmentDateComparator.java new file mode 100644 index 00000000000..583126dc17d --- /dev/null +++ b/src/main/java/seedu/address/model/appointment/SortByAppointmentDateComparator.java @@ -0,0 +1,39 @@ +package seedu.address.model.appointment; + +import java.util.Comparator; + +import seedu.address.commons.util.ToStringBuilder; + +/** + * Compares an {@code Appointment} to another {@code Appointment} to determine + * appointment ordering. + */ +public class SortByAppointmentDateComparator implements Comparator { + + + @Override + public int compare(Appointment apt1, Appointment apt2) { + if (apt1.getDateTime().isBefore(apt2.getDateTime())) { + return -1; + } + return 1; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof SortByAppointmentDateComparator)) { + return false; + } + return true; + } + + @Override + public String toString() { + return new ToStringBuilder(this).toString(); + } +} diff --git a/src/main/java/seedu/address/model/financialplan/FinancialPlan.java b/src/main/java/seedu/address/model/financialplan/FinancialPlan.java new file mode 100644 index 00000000000..878434932ea --- /dev/null +++ b/src/main/java/seedu/address/model/financialplan/FinancialPlan.java @@ -0,0 +1,70 @@ +package seedu.address.model.financialplan; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +/** + * Represents a Financial Plan in the address book. + * Guarantees: immutable; name is valid as declared in {@link #isValidFinancialPlanName(String)} + */ +public class FinancialPlan { + + public static final String MESSAGE_CONSTRAINTS = "Financial plan names should be alphanumeric or space characters, " + + "and it should not be blank"; + public static final String VALIDATION_REGEX = "^[a-zA-Z0-9\\s]+"; + + public final String financialPlanName; + + /** + * Constructs a {@code financialPlan}. + * + * @param financialPlanName A valid financial plan name. + */ + public FinancialPlan(String financialPlanName) { + requireNonNull(financialPlanName); + checkArgument(isValidFinancialPlanName(financialPlanName), MESSAGE_CONSTRAINTS); + this.financialPlanName = financialPlanName; + } + + /** + * Returns true if a given string is a valid financial plan name. + */ + public static boolean isValidFinancialPlanName(String test) { + return test.matches(VALIDATION_REGEX); + } + + /** + * Returns true if the given string is a substring of {@code financialPlanName}. + */ + public boolean containsSubstring(String substring) { + return financialPlanName.toLowerCase().contains(substring.toLowerCase()); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof FinancialPlan)) { + return false; + } + + FinancialPlan otherFinancialPlan = (FinancialPlan) other; + return financialPlanName.equals(otherFinancialPlan.financialPlanName); + } + + @Override + public int hashCode() { + return financialPlanName.hashCode(); + } + + /** + * Formats state as text for viewing. + */ + public String toString() { + return '[' + financialPlanName + ']'; + } + +} diff --git a/src/main/java/seedu/address/model/person/Name.java b/src/main/java/seedu/address/model/person/Name.java index 173f15b9b00..b896d628f74 100644 --- a/src/main/java/seedu/address/model/person/Name.java +++ b/src/main/java/seedu/address/model/person/Name.java @@ -7,7 +7,7 @@ * Represents a Person's name in the address book. * Guarantees: immutable; is valid as declared in {@link #isValidName(String)} */ -public class Name { +public class Name implements Comparable { public static final String MESSAGE_CONSTRAINTS = "Names should only contain alphanumeric characters and spaces, and it should not be blank"; @@ -59,6 +59,12 @@ public boolean equals(Object other) { return fullName.equals(otherName.fullName); } + @Override + public int compareTo(Name a) { + return this.fullName.compareToIgnoreCase(a.fullName); + + } + @Override public int hashCode() { return fullName.hashCode(); diff --git a/src/main/java/seedu/address/model/person/NextOfKinName.java b/src/main/java/seedu/address/model/person/NextOfKinName.java new file mode 100644 index 00000000000..f78bb956717 --- /dev/null +++ b/src/main/java/seedu/address/model/person/NextOfKinName.java @@ -0,0 +1,17 @@ +package seedu.address.model.person; + +/** + * Represents a Person's next-of-kin name in the address book. + * Guarantees: immutable; is valid as declared in {@link #isValidName(String)} + */ +public class NextOfKinName extends Name { + public static final String MESSAGE_CONSTRAINTS = "Next of Kin's " + Name.MESSAGE_CONSTRAINTS; + /** + * Constructs a {@code NextOfKinName}. + * + * @param nextOfKinName A valid next-of-kin name. + */ + public NextOfKinName(String nextOfKinName) { + super(nextOfKinName); + } +} diff --git a/src/main/java/seedu/address/model/person/NextOfKinPhone.java b/src/main/java/seedu/address/model/person/NextOfKinPhone.java new file mode 100644 index 00000000000..5e9ac218f41 --- /dev/null +++ b/src/main/java/seedu/address/model/person/NextOfKinPhone.java @@ -0,0 +1,17 @@ +package seedu.address.model.person; + +/** + * Represents a Person's next-of-kin phone number in the address book. + * Guarantees: immutable; is valid as declared in {@link #isValidPhone(String)} + */ +public class NextOfKinPhone extends Phone { + public static final String MESSAGE_CONSTRAINTS = "Next of Kin's " + Phone.MESSAGE_CONSTRAINTS; + /** + * Constructs a {@code NextOfKinPhone}. + * + * @param nextOfKinPhone A valid phone number. + */ + public NextOfKinPhone(String nextOfKinPhone) { + super(nextOfKinPhone); + } +} diff --git a/src/main/java/seedu/address/model/person/Person.java b/src/main/java/seedu/address/model/person/Person.java index abe8c46b535..f7327263b93 100644 --- a/src/main/java/seedu/address/model/person/Person.java +++ b/src/main/java/seedu/address/model/person/Person.java @@ -2,12 +2,16 @@ import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; +import java.time.LocalDate; import java.util.Collections; import java.util.HashSet; import java.util.Objects; import java.util.Set; import seedu.address.commons.util.ToStringBuilder; +import seedu.address.model.appointment.NullAppointment; +import seedu.address.model.appointment.ScheduleItem; +import seedu.address.model.financialplan.FinancialPlan; import seedu.address.model.tag.Tag; /** @@ -20,21 +24,31 @@ public class Person { private final Name name; private final Phone phone; private final Email email; - - // Data fields private final Address address; + private final NextOfKinName nextOfKinName; + private final NextOfKinPhone nextOfKinPhone; + private final Set financialPlans = new HashSet<>(); private final Set tags = new HashSet<>(); - + private final ScheduleItem appointment; /** * Every field must be present and not null. */ - public Person(Name name, Phone phone, Email email, Address address, Set tags) { - requireAllNonNull(name, phone, email, address, tags); + public Person(Name name, Phone phone, Email email, Address address, NextOfKinName nextOfKinName, + NextOfKinPhone nextOfKinPhone, Set financialPlans, + Set tags, ScheduleItem appointment) { + + requireAllNonNull(name, phone, email, address, nextOfKinName, nextOfKinPhone, + financialPlans, tags, appointment); + this.name = name; this.phone = phone; this.email = email; this.address = address; + this.nextOfKinName = nextOfKinName; + this.nextOfKinPhone = nextOfKinPhone; + this.financialPlans.addAll(financialPlans); this.tags.addAll(tags); + this.appointment = appointment; } public Name getName() { @@ -52,6 +66,23 @@ public Email getEmail() { public Address getAddress() { return address; } + public NextOfKinName getNextOfKinName() { + return nextOfKinName; + } + public NextOfKinPhone getNextOfKinPhone() { + return nextOfKinPhone; + } + + public ScheduleItem getAppointment() { + return appointment; + } + /** + * Returns an immutable financial plan set, which throws {@code UnsupportedOperationException} + * if modification is attempted. + */ + public Set getFinancialPlans() { + return Collections.unmodifiableSet(financialPlans); + } /** * Returns an immutable tag set, which throws {@code UnsupportedOperationException} @@ -74,6 +105,62 @@ public boolean isSamePerson(Person otherPerson) { && otherPerson.getName().equals(getName()); } + /** + * Checks if the given {@code prompt} is a substring of {@code financialPlan} names in {@code financialPlans} + * and returns the email if true. + */ + public String gatherEmailsContainsFinancialPlan(String prompt) { + StringBuilder result = new StringBuilder(); + for (FinancialPlan financialPlan : financialPlans) { + // Check if the financialPlan contains the prompt as a substring + if (financialPlan.containsSubstring(prompt)) { + result.append(email); + break; // Should only add email to result once + } + assert result.length() == 0 : "Results string should be empty"; + } + return result.toString(); + } + + /** + * Checks if the given {@code prompt} is a substring of {@code tag} names in {@code Tags} + * and returns the email if true. + */ + public String gatherEmailsContainsTag(String prompt) { + StringBuilder result = new StringBuilder(); + + for (Tag tag : tags) { + // Check if the tag contains the prompt substring + if (tag.containsSubstring(prompt)) { + result.append(email); + break; // Should only add email to result once + } + assert result.length() == 0 : "Results string should be empty"; + } + + return result.toString(); + } + + public boolean isSameAppointmentDate(LocalDate date) { + return appointment.isSameDate(date); + } + + /** + * Creates the same {@code Person} with a NullAppointment object. + */ + public Person clearAppointment() { + return new Person(name, phone, email, address, nextOfKinName, + nextOfKinPhone, financialPlans, + tags, NullAppointment.getNullAppointment()); + } + + /** + * Checks if {@code Person} appoint is a NullAppointment object. + */ + public boolean hasNullAppointment() { + return appointment.equals(NullAppointment.getNullAppointment()); + } + /** * Returns true if both persons have the same identity and data fields. * This defines a stronger notion of equality between two persons. @@ -94,13 +181,18 @@ public boolean equals(Object other) { && phone.equals(otherPerson.phone) && email.equals(otherPerson.email) && address.equals(otherPerson.address) - && tags.equals(otherPerson.tags); + && nextOfKinName.equals(otherPerson.nextOfKinName) + && nextOfKinPhone.equals(otherPerson.nextOfKinPhone) + && tags.equals(otherPerson.tags) + && appointment.equals(otherPerson.appointment) + && financialPlans.equals(otherPerson.financialPlans); } @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, nextOfKinName, nextOfKinPhone, + financialPlans, tags, appointment); } @Override @@ -110,7 +202,11 @@ public String toString() { .add("phone", phone) .add("email", email) .add("address", address) + .add("nextOfKinName", nextOfKinName) + .add("nextOfKinPhone", nextOfKinPhone) + .add("financialPlans", financialPlans) .add("tags", tags) + .add("appointment", appointment) .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..0e5d0456194 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.time.LocalDate; import java.util.Iterator; import java.util.List; @@ -10,6 +11,7 @@ import javafx.collections.ObservableList; import seedu.address.model.person.exceptions.DuplicatePersonException; import seedu.address.model.person.exceptions.PersonNotFoundException; +import seedu.address.model.person.gatheremail.GatherEmailPrompt; /** * A list of persons that enforces uniqueness between its elements and does not allow nulls. @@ -97,6 +99,45 @@ public void setPersons(List persons) { internalList.setAll(persons); } + /** + * Gathers emails of persons with {@code prompt} from this {@code persons}. + */ + public String gatherEmails(GatherEmailPrompt prompt) { + StringBuilder emails = new StringBuilder(); + + for (Person person : internalList) { + String email = prompt.gatherEmails(person); + if (!email.isEmpty()) { + emails.append(email).append("; "); + } + } + + return emails.toString().trim(); + } + + /** + * Clears all appointments of {@code Person} in {@code persons} that match the given {@code LocalDate date}. + */ + public void clearAppointments(LocalDate date) { + for (Person person: internalList) { + if (person.isSameAppointmentDate(date)) { + setPerson(person, person.clearAppointment()); + } + } + } + + /** + * Returns if any Person {@code internalList} has an Appointment matching the {@code date}. + */ + public boolean hasAppointmentWithDate(LocalDate date) { + for (Person person : internalList) { + if (person.isSameAppointmentDate(date)) { + return true; + } + } + return false; + } + /** * Returns the backing list as an unmodifiable {@code ObservableList}. */ diff --git a/src/main/java/seedu/address/model/person/comparator/SortByAppointmentComparator.java b/src/main/java/seedu/address/model/person/comparator/SortByAppointmentComparator.java new file mode 100644 index 00000000000..b1e09809532 --- /dev/null +++ b/src/main/java/seedu/address/model/person/comparator/SortByAppointmentComparator.java @@ -0,0 +1,37 @@ +package seedu.address.model.person.comparator; + +import java.util.Comparator; + +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.model.person.Person; + +/** + * Compares a {@code Person}'s {@code Appointment} to another {@code Person}'s {@code Appointment} to determine + * appointment ordering. + */ +public class SortByAppointmentComparator implements Comparator { + + + @Override + public int compare(Person o1, Person o2) { + return o1.getAppointment().compareTo(o2.getAppointment()); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof SortByAppointmentComparator)) { + return false; + } + return true; + } + + @Override + public String toString() { + return new ToStringBuilder(this).toString(); + } +} diff --git a/src/main/java/seedu/address/model/person/comparator/SortByNameComparator.java b/src/main/java/seedu/address/model/person/comparator/SortByNameComparator.java new file mode 100644 index 00000000000..cbc0065754e --- /dev/null +++ b/src/main/java/seedu/address/model/person/comparator/SortByNameComparator.java @@ -0,0 +1,38 @@ +package seedu.address.model.person.comparator; + +import java.util.Comparator; + +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.model.person.Person; + +/** + * Compares a {@code Person}'s {@code Name} to another {@code Person}'s {@code Name} to determine + * lexicographical ordering of the names. + */ +public class SortByNameComparator implements Comparator { + + + @Override + public int compare(Person o1, Person o2) { + return o1.getName().compareTo(o2.getName()); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof SortByNameComparator)) { + return false; + } + return true; + } + + + @Override + public String toString() { + return new ToStringBuilder(this).toString(); + } +} diff --git a/src/main/java/seedu/address/model/person/gatheremail/GatherEmailByFinancialPlan.java b/src/main/java/seedu/address/model/person/gatheremail/GatherEmailByFinancialPlan.java new file mode 100644 index 00000000000..822235986e1 --- /dev/null +++ b/src/main/java/seedu/address/model/person/gatheremail/GatherEmailByFinancialPlan.java @@ -0,0 +1,50 @@ +//@@author AlyssaPng +package seedu.address.model.person.gatheremail; + +import seedu.address.model.person.Person; + +/** + * Gathers the email of {@code Person} if {@code Person}'s Financial Plans Names contains {@code promptFp} + * as a substring. + */ +public class GatherEmailByFinancialPlan implements GatherEmailPrompt { + private final String promptFp; + + /** + * Constructs a new GatherEmailByFinancial Object + */ + public GatherEmailByFinancialPlan(String promptFp) { + this.promptFp = promptFp; + } + + /** + * Gathers the email of {@code person} if prompt is a substring of any {@code person}'s financial plan names. + */ + @Override + public String gatherEmails(Person person) { + return person.gatherEmailsContainsFinancialPlan(promptFp); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof GatherEmailByFinancialPlan)) { + return false; + } + + GatherEmailByFinancialPlan otherGatherByFinancialPlan = (GatherEmailByFinancialPlan) other; + return promptFp.equals(otherGatherByFinancialPlan.promptFp); + } + + /** + * Returns the String representation of GatherEmailByFinancialPlan Object. + */ + @Override + public String toString() { + return "Financial Plan: " + promptFp; + } +} diff --git a/src/main/java/seedu/address/model/person/gatheremail/GatherEmailByTag.java b/src/main/java/seedu/address/model/person/gatheremail/GatherEmailByTag.java new file mode 100644 index 00000000000..be6f62f4b75 --- /dev/null +++ b/src/main/java/seedu/address/model/person/gatheremail/GatherEmailByTag.java @@ -0,0 +1,49 @@ +//@@author AlyssaPng +package seedu.address.model.person.gatheremail; + +import seedu.address.model.person.Person; + +/** + * Gathers the email of {@code Person} if {@code Person}'s Tags Names contains {@code promptTag} + * as a substring. + */ +public class GatherEmailByTag implements GatherEmailPrompt { + private final String promptTag; + + /** + * Constructs a new GatherEmailByTag Object + */ + public GatherEmailByTag(String promptTag) { + this.promptTag = promptTag; + } + + /** + * Gathers the email of {@code person} if prompt is a substring of any {@code person}'s tag names + */ + @Override + public String gatherEmails(Person person) { + return person.gatherEmailsContainsTag(promptTag); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof GatherEmailByTag)) { + return false; + } + GatherEmailByTag otherGatherCommand = (GatherEmailByTag) other; + return promptTag.equals(otherGatherCommand.promptTag); + } + + /** + * Returns the String representation of GatherEmailByTag Object. + */ + @Override + public String toString() { + return "Tag: " + promptTag; + } +} diff --git a/src/main/java/seedu/address/model/person/gatheremail/GatherEmailPrompt.java b/src/main/java/seedu/address/model/person/gatheremail/GatherEmailPrompt.java new file mode 100644 index 00000000000..042ed8c2a59 --- /dev/null +++ b/src/main/java/seedu/address/model/person/gatheremail/GatherEmailPrompt.java @@ -0,0 +1,14 @@ +//@@author AlyssaPng +package seedu.address.model.person.gatheremail; + +import seedu.address.model.person.Person; + +/** + * Gathers the email of {@code Person} if {@code Person}'s matches a specific prompt. + */ +public interface GatherEmailPrompt { + /** + * Gathers the email of {@code person} if prompt is a substring of any {@code person}'s financial plan or tag names + */ + public String gatherEmails(Person person); +} diff --git a/src/main/java/seedu/address/model/person/predicates/CombinedPredicate.java b/src/main/java/seedu/address/model/person/predicates/CombinedPredicate.java new file mode 100644 index 00000000000..27097eb7734 --- /dev/null +++ b/src/main/java/seedu/address/model/person/predicates/CombinedPredicate.java @@ -0,0 +1,64 @@ +package seedu.address.model.person.predicates; + +import java.util.Arrays; +import java.util.function.Predicate; + +import seedu.address.commons.util.CollectionUtil; +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.model.person.Person; + +/** + * Tests that at least one of a {@code Person}'s fields matches any of the keywords given. + */ +public class CombinedPredicate implements Predicate { + public final PersonContainsKeywordsPredicate[] predicates; + + /** + * Creates a combined "or" predicate from the given collection of predicates. Requires that none of the elements + * in the given collection is null. + * + * @param predicates keyword predicates to check a person against. + */ + public CombinedPredicate(PersonContainsKeywordsPredicate... predicates) { + CollectionUtil.requireAllNonNull((Object[]) predicates); + this.predicates = new PersonContainsKeywordsPredicate[predicates.length]; + for (int i = 0; i < predicates.length; i++) { + this.predicates[i] = predicates[i]; + } + } + + @Override + public boolean test(Person person) { + boolean result = false; + for (PersonContainsKeywordsPredicate predicate : this.predicates) { + result = result || predicate.test(person); + } + return result; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof CombinedPredicate)) { + return false; + } + + CombinedPredicate otherNameContainsKeywordsPredicate = + (CombinedPredicate) other; + return Arrays.equals(predicates, + otherNameContainsKeywordsPredicate.predicates); + } + + @Override + public String toString() { + ToStringBuilder toStringBuilder = new ToStringBuilder(this); + for (PersonContainsKeywordsPredicate predicate : this.predicates) { + toStringBuilder.add(predicate.getClass().getCanonicalName(), predicate.toString()); + } + return toStringBuilder.toString(); + } +} diff --git a/src/main/java/seedu/address/model/person/predicates/FinancialPlanContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/person/predicates/FinancialPlanContainsKeywordsPredicate.java new file mode 100644 index 00000000000..6659b9286c2 --- /dev/null +++ b/src/main/java/seedu/address/model/person/predicates/FinancialPlanContainsKeywordsPredicate.java @@ -0,0 +1,48 @@ +package seedu.address.model.person.predicates; + +import java.util.List; +import java.util.stream.Stream; + +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.model.financialplan.FinancialPlan; +import seedu.address.model.person.Person; + +/** + * Tests that at least one of a {@code Person}'s {@code FinancialPlan} matches any of the keywords given. + */ +public class FinancialPlanContainsKeywordsPredicate implements PersonContainsKeywordsPredicate { + private final List keywords; + + public FinancialPlanContainsKeywordsPredicate(List keywords) { + this.keywords = keywords; + } + + @Override + public boolean test(Person person) { + Stream financialPlanStream = person.getFinancialPlans().stream(); + // We check for each financial plan if it contains a keyword as a substring + return financialPlanStream.anyMatch(financialPlan -> keywords.stream() + .anyMatch(keyword -> financialPlan.containsSubstring(keyword))); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof FinancialPlanContainsKeywordsPredicate)) { + return false; + } + + FinancialPlanContainsKeywordsPredicate otherNameContainsKeywordsPredicate = + (FinancialPlanContainsKeywordsPredicate) other; + return keywords.equals(otherNameContainsKeywordsPredicate.keywords); + } + + @Override + public String toString() { + return new ToStringBuilder(this).add("keywords", keywords).toString(); + } +} diff --git a/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/person/predicates/NameContainsKeywordsPredicate.java similarity index 78% rename from src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java rename to src/main/java/seedu/address/model/person/predicates/NameContainsKeywordsPredicate.java index 62d19be2977..e515370166d 100644 --- a/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java +++ b/src/main/java/seedu/address/model/person/predicates/NameContainsKeywordsPredicate.java @@ -1,15 +1,15 @@ -package seedu.address.model.person; +package seedu.address.model.person.predicates; import java.util.List; -import java.util.function.Predicate; import seedu.address.commons.util.StringUtil; import seedu.address.commons.util.ToStringBuilder; +import seedu.address.model.person.Person; /** * Tests that a {@code Person}'s {@code Name} matches any of the keywords given. */ -public class NameContainsKeywordsPredicate implements Predicate { +public class NameContainsKeywordsPredicate implements PersonContainsKeywordsPredicate { private final List keywords; public NameContainsKeywordsPredicate(List keywords) { @@ -19,7 +19,7 @@ public NameContainsKeywordsPredicate(List keywords) { @Override public boolean test(Person person) { return keywords.stream() - .anyMatch(keyword -> StringUtil.containsWordIgnoreCase(person.getName().fullName, keyword)); + .anyMatch(keyword -> StringUtil.containsWordsIgnoreCase(person.getName().fullName, keyword)); } @Override diff --git a/src/main/java/seedu/address/model/person/predicates/PersonContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/person/predicates/PersonContainsKeywordsPredicate.java new file mode 100644 index 00000000000..1c0efe9dde8 --- /dev/null +++ b/src/main/java/seedu/address/model/person/predicates/PersonContainsKeywordsPredicate.java @@ -0,0 +1,21 @@ +package seedu.address.model.person.predicates; + +import java.util.function.Predicate; + +import seedu.address.model.person.Person; + +/** + * Tests if a {@code Person} contains certain keywords in a certain field. Note that the implementing predicates are not + * required to test the validity of the keywords. + */ +public interface PersonContainsKeywordsPredicate extends Predicate { + + @Override + boolean test(Person person); + + @Override + boolean equals(Object other); + + @Override + String toString(); +} diff --git a/src/main/java/seedu/address/model/person/predicates/TagContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/person/predicates/TagContainsKeywordsPredicate.java new file mode 100644 index 00000000000..fd9f96229de --- /dev/null +++ b/src/main/java/seedu/address/model/person/predicates/TagContainsKeywordsPredicate.java @@ -0,0 +1,47 @@ +package seedu.address.model.person.predicates; + +import java.util.List; +import java.util.stream.Stream; + +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.model.person.Person; +import seedu.address.model.tag.Tag; + +/** + * Tests that at least one of a {@code Person}'s {@code Tag} matches any of the keywords given. + */ +public class TagContainsKeywordsPredicate implements PersonContainsKeywordsPredicate { + private final List keywords; + + public TagContainsKeywordsPredicate(List keywords) { + this.keywords = keywords; + } + + @Override + public boolean test(Person person) { + Stream tagStream = person.getTags().stream(); + // We check for each tag if it contains a keyword as a substring + return tagStream.anyMatch(tag -> keywords.stream() + .anyMatch(keyword -> tag.containsSubstring(keyword))); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof TagContainsKeywordsPredicate)) { + return false; + } + + TagContainsKeywordsPredicate otherNameContainsKeywordsPredicate = (TagContainsKeywordsPredicate) other; + return keywords.equals(otherNameContainsKeywordsPredicate.keywords); + } + + @Override + public String toString() { + return new ToStringBuilder(this).add("keywords", keywords).toString(); + } +} diff --git a/src/main/java/seedu/address/model/tag/Tag.java b/src/main/java/seedu/address/model/tag/Tag.java index f1a0d4e233b..4272062ab05 100644 --- a/src/main/java/seedu/address/model/tag/Tag.java +++ b/src/main/java/seedu/address/model/tag/Tag.java @@ -9,7 +9,8 @@ */ public class Tag { - public static final String MESSAGE_CONSTRAINTS = "Tags names should be alphanumeric"; + public static final String MESSAGE_CONSTRAINTS = "Tags names should be alphanumeric only, " + + "and it should not be blank"; public static final String VALIDATION_REGEX = "\\p{Alnum}+"; public final String tagName; @@ -24,7 +25,6 @@ public Tag(String tagName) { checkArgument(isValidTagName(tagName), MESSAGE_CONSTRAINTS); this.tagName = tagName; } - /** * Returns true if a given string is a valid tag name. */ @@ -32,6 +32,13 @@ public static boolean isValidTagName(String test) { return test.matches(VALIDATION_REGEX); } + /** + * Returns true if the given string is a substring of {@code tagName}. + */ + public boolean containsSubstring(String substring) { + return tagName.toLowerCase().contains(substring.toLowerCase()); + } + @Override public boolean equals(Object other) { if (other == this) { @@ -53,7 +60,7 @@ public int hashCode() { } /** - * Format state as text for viewing. + * Formats state as text for viewing. */ public String toString() { 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..b58ccfed912 100644 --- a/src/main/java/seedu/address/model/util/SampleDataUtil.java +++ b/src/main/java/seedu/address/model/util/SampleDataUtil.java @@ -6,9 +6,14 @@ import seedu.address.model.AddressBook; import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.appointment.NullAppointment; +import seedu.address.model.appointment.ScheduleItem; +import seedu.address.model.financialplan.FinancialPlan; import seedu.address.model.person.Address; import seedu.address.model.person.Email; import seedu.address.model.person.Name; +import seedu.address.model.person.NextOfKinName; +import seedu.address.model.person.NextOfKinPhone; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; import seedu.address.model.tag.Tag; @@ -17,26 +22,34 @@ * Contains utility methods for populating {@code AddressBook} with sample data. */ public class SampleDataUtil { + + public static final ScheduleItem EMPTY_APPOINTMENT = NullAppointment.getNullAppointment(); 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 Address("Blk 30 Geylang Street 29, #06-40"), new NextOfKinName("Alex Dad"), + new NextOfKinPhone("999"), getFinancialPlanSet("voodoo financial plan A"), + getTagSet("friends"), EMPTY_APPOINTMENT), 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 Address("Blk 30 Lorong 3 Serangoon Gardens, #07-18"), new NextOfKinName("Bernice Mom"), + new NextOfKinPhone("888"), getFinancialPlanSet("voodoo financial plan B"), + getTagSet("colleagues", "friends"), EMPTY_APPOINTMENT), 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 Address("Blk 11 Ang Mo Kio Street 74, #11-04"), new NextOfKinName("Charlotte Bro"), + new NextOfKinPhone("777"), getFinancialPlanSet("voodoo financial plan C"), + getTagSet("neighbours"), EMPTY_APPOINTMENT), 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 Address("Blk 436 Serangoon Gardens Street 26, #16-43"), new NextOfKinName("David Sis"), + new NextOfKinPhone("666"), getFinancialPlanSet("voodoo financial plan D"), + getTagSet("family"), EMPTY_APPOINTMENT), 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 Address("Blk 47 Tampines Street 20, #17-35"), new NextOfKinName("Ifran Grandpa"), + new NextOfKinPhone("555"), getFinancialPlanSet("voodoo financial plan E"), + getTagSet("classmates"), EMPTY_APPOINTMENT), 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")) + new Address("Blk 45 Aljunied Street 85, #11-31"), new NextOfKinName("Roy Grandma"), + new NextOfKinPhone("444"), getFinancialPlanSet("voodoo financial plan F"), + getTagSet("colleagues"), EMPTY_APPOINTMENT) }; } @@ -48,6 +61,15 @@ public static ReadOnlyAddressBook getSampleAddressBook() { return sampleAb; } + /** + * Returns a financial plan set containing the list of strings given. + */ + public static Set getFinancialPlanSet(String... strings) { + return Arrays.stream(strings) + .map(FinancialPlan::new) + .collect(Collectors.toSet()); + } + /** * Returns a tag set containing the list of strings given. */ diff --git a/src/main/java/seedu/address/storage/JsonAdaptedAppointment.java b/src/main/java/seedu/address/storage/JsonAdaptedAppointment.java new file mode 100644 index 00000000000..fc77e529fc4 --- /dev/null +++ b/src/main/java/seedu/address/storage/JsonAdaptedAppointment.java @@ -0,0 +1,56 @@ +package seedu.address.storage; + +import static seedu.address.model.appointment.NullAppointment.MESSAGE_NULL_APT; + +import java.util.logging.Logger; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +import seedu.address.MainApp; +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.appointment.Appointment; +import seedu.address.model.appointment.NullAppointment; +import seedu.address.model.appointment.ScheduleItem; + +/** + * Jackson-friendly version of {@link Appointment}. + */ +class JsonAdaptedAppointment { + private static final Logger logger = LogsCenter.getLogger(MainApp.class); + private final String appointment; + + /** + * Constructs a {@code JsonAdaptedAppointment} with the given {@code Appointment}. + */ + @JsonCreator + public JsonAdaptedAppointment(String appointment) { + this.appointment = appointment; + } + + @JsonValue + public String getAptDescription() { + return appointment; + } + + /** + * Converts this Jackson-friendly adapted tag object into the model's {@code Tag} object. + * + * @throws IllegalValueException if there were any data constraints violated in the adapted tag. + */ + public ScheduleItem toModelType() throws IllegalValueException { + + if (appointment.equals(MESSAGE_NULL_APT)) { + return NullAppointment.getNullAppointment(); + } + + if (!Appointment.isValidAppointment(appointment)) { + logger.warning("Invalid Appointment Format: " + appointment); + throw new IllegalValueException(Appointment.MESSAGE_APT_CONSTRAINTS); + } + + return Appointment.parseAppointmentDescription(appointment); + } + +} diff --git a/src/main/java/seedu/address/storage/JsonAdaptedFinancialPlan.java b/src/main/java/seedu/address/storage/JsonAdaptedFinancialPlan.java new file mode 100644 index 00000000000..0ce8e0780ac --- /dev/null +++ b/src/main/java/seedu/address/storage/JsonAdaptedFinancialPlan.java @@ -0,0 +1,54 @@ +package seedu.address.storage; + +import java.util.logging.Logger; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +import seedu.address.MainApp; +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.financialplan.FinancialPlan; + + +/** + * Jackson-friendly version of {@link FinancialPlan}. + */ +public class JsonAdaptedFinancialPlan { + + private static final Logger logger = LogsCenter.getLogger(MainApp.class); + private final String financialPlanName; + + /** + * Constructs a {@code JsonAdaptedTag} with the given {@code tagName}. + */ + @JsonCreator + public JsonAdaptedFinancialPlan(String financialPlanName) { + this.financialPlanName = financialPlanName; + } + + /** + * Converts a given {@code Tag} into this class for Jackson use. + */ + public JsonAdaptedFinancialPlan(FinancialPlan source) { + financialPlanName = source.financialPlanName; + } + + @JsonValue + public String getFinancialPlanName() { + return financialPlanName; + } + + /** + * Converts this Jackson-friendly adapted tag object into the model's {@code Tag} object. + * + * @throws IllegalValueException if there were any data constraints violated in the adapted tag. + */ + public FinancialPlan toModelType() throws IllegalValueException { + if (!FinancialPlan.isValidFinancialPlanName(financialPlanName)) { + logger.warning("Invalid Financial Plan Name: " + financialPlanName); + throw new IllegalValueException(FinancialPlan.MESSAGE_CONSTRAINTS); + } + return new FinancialPlan(financialPlanName); + } +} diff --git a/src/main/java/seedu/address/storage/JsonAdaptedPerson.java b/src/main/java/seedu/address/storage/JsonAdaptedPerson.java index bd1ca0f56c8..6d659d98b02 100644 --- a/src/main/java/seedu/address/storage/JsonAdaptedPerson.java +++ b/src/main/java/seedu/address/storage/JsonAdaptedPerson.java @@ -4,15 +4,23 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.logging.Logger; import java.util.stream.Collectors; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import seedu.address.MainApp; +import seedu.address.commons.core.LogsCenter; import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.appointment.Appointment; +import seedu.address.model.appointment.ScheduleItem; +import seedu.address.model.financialplan.FinancialPlan; import seedu.address.model.person.Address; import seedu.address.model.person.Email; import seedu.address.model.person.Name; +import seedu.address.model.person.NextOfKinName; +import seedu.address.model.person.NextOfKinPhone; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; import seedu.address.model.tag.Tag; @@ -23,27 +31,42 @@ class JsonAdaptedPerson { public static final String MISSING_FIELD_MESSAGE_FORMAT = "Person's %s field is missing!"; - + private static final Logger logger = LogsCenter.getLogger(MainApp.class); private final String name; private final String phone; private final String email; private final String address; + private final String nextOfKinName; + private final String nextOfKinPhone; + + private final List financialPlans = new ArrayList<>(); private final List tags = new ArrayList<>(); + private final JsonAdaptedAppointment appointment; /** * Constructs a {@code JsonAdaptedPerson} with the given person details. */ @JsonCreator public JsonAdaptedPerson(@JsonProperty("name") String name, @JsonProperty("phone") String phone, - @JsonProperty("email") String email, @JsonProperty("address") String address, - @JsonProperty("tags") List tags) { + @JsonProperty("email") String email, @JsonProperty("address") String address, + @JsonProperty("nextOfKinName") String nextOfKinName, + @JsonProperty("nextOfKinPhone") String nextOfKinPhone, + @JsonProperty("financialPlans") List financialPlans, + @JsonProperty("tags") List tags, + @JsonProperty("appointment") JsonAdaptedAppointment appointment) { this.name = name; this.phone = phone; this.email = email; this.address = address; + this.nextOfKinName = nextOfKinName; + this.nextOfKinPhone = nextOfKinPhone; + if (financialPlans != null) { + this.financialPlans.addAll(financialPlans); + } if (tags != null) { this.tags.addAll(tags); } + this.appointment = appointment; } /** @@ -54,9 +77,15 @@ public JsonAdaptedPerson(Person source) { phone = source.getPhone().value; email = source.getEmail().value; address = source.getAddress().value; + nextOfKinName = source.getNextOfKinName().fullName; + nextOfKinPhone = source.getNextOfKinPhone().value; + financialPlans.addAll(source.getFinancialPlans().stream() + .map(JsonAdaptedFinancialPlan::new) + .collect(Collectors.toList())); tags.addAll(source.getTags().stream() .map(JsonAdaptedTag::new) .collect(Collectors.toList())); + appointment = new JsonAdaptedAppointment(source.getAppointment().toString()); } /** @@ -65,6 +94,10 @@ public JsonAdaptedPerson(Person source) { * @throws IllegalValueException if there were any data constraints violated in the adapted person. */ public Person toModelType() throws IllegalValueException { + final List personFinancialPlans = new ArrayList<>(); + for (JsonAdaptedFinancialPlan plan : financialPlans) { + personFinancialPlans.add(plan.toModelType()); + } final List personTags = new ArrayList<>(); for (JsonAdaptedTag tag : tags) { personTags.add(tag.toModelType()); @@ -74,6 +107,7 @@ public Person toModelType() throws IllegalValueException { throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Name.class.getSimpleName())); } if (!Name.isValidName(name)) { + logger.warning("Invalid Name: " + name); throw new IllegalValueException(Name.MESSAGE_CONSTRAINTS); } final Name modelName = new Name(name); @@ -82,6 +116,7 @@ public Person toModelType() throws IllegalValueException { throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Phone.class.getSimpleName())); } if (!Phone.isValidPhone(phone)) { + logger.warning("Invalid Phone: " + phone); throw new IllegalValueException(Phone.MESSAGE_CONSTRAINTS); } final Phone modelPhone = new Phone(phone); @@ -90,6 +125,7 @@ public Person toModelType() throws IllegalValueException { throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Email.class.getSimpleName())); } if (!Email.isValidEmail(email)) { + logger.warning("Invalid Email: " + email); throw new IllegalValueException(Email.MESSAGE_CONSTRAINTS); } final Email modelEmail = new Email(email); @@ -98,12 +134,44 @@ public Person toModelType() throws IllegalValueException { throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Address.class.getSimpleName())); } if (!Address.isValidAddress(address)) { + logger.warning("Invalid Address: " + address); throw new IllegalValueException(Address.MESSAGE_CONSTRAINTS); } final Address modelAddress = new Address(address); + if (nextOfKinName == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, + NextOfKinName.class.getSimpleName())); + } + if (!NextOfKinName.isValidName(nextOfKinName)) { + logger.warning("Invalid Next of Kin Name: " + nextOfKinName); + throw new IllegalValueException(NextOfKinName.MESSAGE_CONSTRAINTS); + } + final NextOfKinName modelNextOfKinName = new NextOfKinName(nextOfKinName); + + if (nextOfKinPhone == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, + NextOfKinPhone.class.getSimpleName())); + } + if (!NextOfKinPhone.isValidPhone(nextOfKinPhone)) { + logger.warning("Invalid Next of Kin Phone: " + nextOfKinPhone); + throw new IllegalValueException(NextOfKinPhone.MESSAGE_CONSTRAINTS); + } + final NextOfKinPhone modelNextOfKinPhone = new NextOfKinPhone(nextOfKinPhone); + final Set modelTags = new HashSet<>(personTags); - return new Person(modelName, modelPhone, modelEmail, modelAddress, modelTags); + final Set modelFinancialPlans = new HashSet<>(personFinancialPlans); + + if (appointment == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, + Appointment.class.getSimpleName())); + } + + final ScheduleItem modelAppointment = appointment.toModelType(); + + return new Person(modelName, modelPhone, modelEmail, modelAddress, modelNextOfKinName, modelNextOfKinPhone, + modelFinancialPlans, modelTags, modelAppointment); + } } diff --git a/src/main/java/seedu/address/storage/JsonAdaptedTag.java b/src/main/java/seedu/address/storage/JsonAdaptedTag.java index 0df22bdb754..24e323f18b8 100644 --- a/src/main/java/seedu/address/storage/JsonAdaptedTag.java +++ b/src/main/java/seedu/address/storage/JsonAdaptedTag.java @@ -1,8 +1,12 @@ package seedu.address.storage; +import java.util.logging.Logger; + import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue; +import seedu.address.MainApp; +import seedu.address.commons.core.LogsCenter; import seedu.address.commons.exceptions.IllegalValueException; import seedu.address.model.tag.Tag; @@ -11,6 +15,7 @@ */ class JsonAdaptedTag { + private static final Logger logger = LogsCenter.getLogger(MainApp.class); private final String tagName; /** @@ -40,6 +45,7 @@ public String getTagName() { */ public Tag toModelType() throws IllegalValueException { if (!Tag.isValidTagName(tagName)) { + logger.warning("Invalid Tag Name: " + tagName); throw new IllegalValueException(Tag.MESSAGE_CONSTRAINTS); } return new Tag(tagName); diff --git a/src/main/java/seedu/address/ui/AppointmentCard.java b/src/main/java/seedu/address/ui/AppointmentCard.java new file mode 100644 index 00000000000..59c65af77e5 --- /dev/null +++ b/src/main/java/seedu/address/ui/AppointmentCard.java @@ -0,0 +1,52 @@ +package seedu.address.ui; + +import java.time.format.DateTimeFormatter; + +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Region; +import seedu.address.model.appointment.Appointment; + +/** + * An UI component that displays information of a {@code Appointment}. + */ +public class AppointmentCard extends UiPart { + private static final String FXML = "AppointmentListCard.fxml"; + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("dd-MM-yyyy HH:mm"); + + /** + * Note: Certain keywords such as "location" and "resources" are reserved keywords in JavaFX. + * As a consequence, UI elements' variable names cannot be set to such keywords + * or an exception will be thrown by JavaFX during runtime. + * + * @see The issue on AddressBook level 4 + */ + + public final Appointment appt; + + @FXML + private HBox cardPane; + @FXML + private Label id; + @FXML + private Label name; + @FXML + private Label personName; + @FXML + private Label dateTime; + + + /** + * Creates a {@code AppointmentCode} with the given {@code Appointment} and index to display. + */ + public AppointmentCard(Appointment appt, int displayedIndex) { + super(FXML); + this.appt = appt; + + id.setText(displayedIndex + ". "); + name.setText(appt.getName()); + personName.setText(appt.getPerson().getName().toString()); + dateTime.setText(appt.getDateTime().format(DATE_FORMATTER)); + } +} diff --git a/src/main/java/seedu/address/ui/AppointmentListPanel.java b/src/main/java/seedu/address/ui/AppointmentListPanel.java new file mode 100644 index 00000000000..a0b066db56f --- /dev/null +++ b/src/main/java/seedu/address/ui/AppointmentListPanel.java @@ -0,0 +1,49 @@ +package seedu.address.ui; + +import java.util.logging.Logger; + +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.layout.Region; +import seedu.address.commons.core.LogsCenter; +import seedu.address.model.appointment.Appointment; + +/** + * Panel containing the list of persons. + */ +public class AppointmentListPanel extends UiPart { + private static final String FXML = "AppointmentListPanel.fxml"; + private final Logger logger = LogsCenter.getLogger(AppointmentListPanel.class); + + @FXML + private ListView appointmentListView; + + /** + * Creates a {@code AppointmentListPanel} with the given {@code ObservableList}. + */ + public AppointmentListPanel(ObservableList appointmentList) { + super(FXML); + appointmentListView.setItems(appointmentList); + appointmentListView.setCellFactory(listView -> new AppointmentListViewCell()); + } + + /** + * Custom {@code ListCell} that displays the graphics of a {@code Appointment} using a {@code AppointmentCard}. + */ + class AppointmentListViewCell extends ListCell { + @Override + protected void updateItem(Appointment appt, boolean empty) { + super.updateItem(appt, empty); + + if (empty || appt == null) { + setGraphic(null); + setText(null); + } else { + setGraphic(new AppointmentCard(appt, getIndex() + 1).getRoot()); + } + } + } + +} diff --git a/src/main/java/seedu/address/ui/ClearWindow.java b/src/main/java/seedu/address/ui/ClearWindow.java new file mode 100644 index 00000000000..ae64ed7311c --- /dev/null +++ b/src/main/java/seedu/address/ui/ClearWindow.java @@ -0,0 +1,131 @@ +package seedu.address.ui; + +import java.util.logging.Logger; + +import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.stage.Modality; +import javafx.stage.Stage; +import javafx.stage.StageStyle; +import seedu.address.commons.core.LogsCenter; +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.ConfirmClearCommand; +import seedu.address.logic.commands.exceptions.CommandException; + +/** + * Controller for the clear command confirmation page. + */ +public class ClearWindow extends UiPart { + + public static final String CONFIRM_CLEAR_MESSAGE = "Are you sure you want to clear all entries?"; + + private static final Logger logger = LogsCenter.getLogger(ClearWindow.class); + private static final String FXML = "ClearWindow.fxml"; + private CommandExecutor commandExecutor; + + @FXML + private Label clearMessage; + @FXML + private Button clearButton; + @FXML + private Button cancelButton; + + /** + * Creates a new ClearWindow. + * + * @param root Stage to use as the root of the ClearWindow. + */ + public ClearWindow(Stage root) { + super(FXML, root); + root.initStyle(StageStyle.UNDECORATED); + root.initModality(Modality.APPLICATION_MODAL); + clearMessage.setText(CONFIRM_CLEAR_MESSAGE); + } + + /** + * Creates a new ClearWindow. + */ + public ClearWindow(CommandExecutor commandExecutor) { + this(new Stage()); + this.commandExecutor = commandExecutor; + } + + /** + * Shows the clear window. + * @throws IllegalStateException + *
    + *
  • + * if this method is called on a thread other than the JavaFX Application Thread. + *
  • + *
  • + * if this method is called during animation or layout processing. + *
  • + *
  • + * if this method is called on the primary stage. + *
  • + *
  • + * if {@code dialogStage} is already showing. + *
  • + *
+ */ + public void show() { + logger.fine("Showing confirm clear window."); + getRoot().show(); + getRoot().centerOnScreen(); + } + + /** + * Returns true if the clear window is currently being shown. + */ + public boolean isShowing() { + return getRoot().isShowing(); + } + + /** + * Hides the clear window. + */ + public void hide() { + getRoot().hide(); + } + + /** + * Focuses on the clear window. + */ + public void focus() { + getRoot().requestFocus(); + } + /** + * Clears the address book. + */ + @FXML + private void confirmClear() { + Command clearCommand = new ConfirmClearCommand(); + try { + commandExecutor.execute(clearCommand); + } catch (CommandException e) { + // nothing to do in this window if there's an error. + } + hide(); + } + /** + * Cancels the clear command. + */ + @FXML + private void cancel() { + hide(); + } + /** + * Represents a function that can execute commands. + */ + @FunctionalInterface + public interface CommandExecutor { + /** + * Executes the command and returns the result. + * + * @see seedu.address.logic.Logic#execute(Command) + */ + CommandResult execute(Command command) throws CommandException; + } +} diff --git a/src/main/java/seedu/address/ui/CommandBox.java b/src/main/java/seedu/address/ui/CommandBox.java index 9e75478664b..29a934e1072 100644 --- a/src/main/java/seedu/address/ui/CommandBox.java +++ b/src/main/java/seedu/address/ui/CommandBox.java @@ -68,7 +68,6 @@ private void setStyleToIndicateCommandFailure() { styleClass.add(ERROR_STYLE_CLASS); } - /** * Represents a function that can execute commands. */ @@ -81,5 +80,4 @@ public interface CommandExecutor { */ CommandResult execute(String commandText) throws CommandException, ParseException; } - } diff --git a/src/main/java/seedu/address/ui/HelpWindow.java b/src/main/java/seedu/address/ui/HelpWindow.java index 3f16b2fcf26..d6f7b414ed0 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-f12-1.github.io/tp/UserGuide.html"; public static final String HELP_MESSAGE = "Refer to the user guide: " + USERGUIDE_URL; private static final Logger logger = LogsCenter.getLogger(HelpWindow.class); diff --git a/src/main/java/seedu/address/ui/MainWindow.java b/src/main/java/seedu/address/ui/MainWindow.java index 79e74ef37c0..566204615aa 100644 --- a/src/main/java/seedu/address/ui/MainWindow.java +++ b/src/main/java/seedu/address/ui/MainWindow.java @@ -13,9 +13,12 @@ import seedu.address.commons.core.GuiSettings; import seedu.address.commons.core.LogsCenter; import seedu.address.logic.Logic; +import seedu.address.logic.commands.Command; import seedu.address.logic.commands.CommandResult; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.appointment.Appointment; +import seedu.address.model.person.Person; /** * The Main Window. Provides the basic application layout containing @@ -32,8 +35,13 @@ public class MainWindow extends UiPart { // Independent Ui parts residing in this Ui container private PersonListPanel personListPanel; + private AppointmentListPanel appointmentListPanel; private ResultDisplay resultDisplay; private HelpWindow helpWindow; + private ClearWindow clearWindow; + private OverrideWindow overrideWindow; + private Appointment appointment; + private Person personToEdit; @FXML private StackPane commandBoxPlaceholder; @@ -44,6 +52,9 @@ public class MainWindow extends UiPart { @FXML private StackPane personListPanelPlaceholder; + @FXML + private StackPane appointmentListPanelPlaceholder; + @FXML private StackPane resultDisplayPlaceholder; @@ -62,10 +73,9 @@ public MainWindow(Stage primaryStage, Logic logic) { // Configure the UI setWindowDefaultSize(logic.getGuiSettings()); - setAccelerators(); - helpWindow = new HelpWindow(); + clearWindow = new ClearWindow(this::executeCommand); } public Stage getPrimaryStage() { @@ -113,6 +123,9 @@ void fillInnerParts() { personListPanel = new PersonListPanel(logic.getFilteredPersonList()); personListPanelPlaceholder.getChildren().add(personListPanel.getRoot()); + appointmentListPanel = new AppointmentListPanel(logic.getAppointmentList()); + appointmentListPanelPlaceholder.getChildren().add(appointmentListPanel.getRoot()); + resultDisplay = new ResultDisplay(); resultDisplayPlaceholder.getChildren().add(resultDisplay.getRoot()); @@ -147,6 +160,31 @@ public void handleHelp() { } } + /** + * Opens the clear window or focuses on it if it's already opened. + */ + @FXML + public void handleClear() { + if (!clearWindow.isShowing()) { + clearWindow.show(); + } else { + clearWindow.focus(); + } + } + + /** + * Opens the override window or focuses on it if it's already opened. + */ + @FXML + public void handleOverride(Appointment appointment, Person personToEdit) { + overrideWindow = new OverrideWindow(this::executeCommand, appointment, personToEdit); + if (!overrideWindow.isShowing()) { + overrideWindow.show(); + } else { + overrideWindow.focus(); + } + } + void show() { primaryStage.show(); } @@ -160,6 +198,7 @@ private void handleExit() { (int) primaryStage.getX(), (int) primaryStage.getY()); logic.setGuiSettings(guiSettings); helpWindow.hide(); + clearWindow.hide(); primaryStage.hide(); } @@ -175,17 +214,7 @@ public PersonListPanel getPersonListPanel() { private CommandResult executeCommand(String commandText) throws CommandException, ParseException { try { CommandResult commandResult = logic.execute(commandText); - logger.info("Result: " + commandResult.getFeedbackToUser()); - resultDisplay.setFeedbackToUser(commandResult.getFeedbackToUser()); - - if (commandResult.isShowHelp()) { - handleHelp(); - } - - if (commandResult.isExit()) { - handleExit(); - } - + handleCommandResult(commandResult); return commandResult; } catch (CommandException | ParseException e) { logger.info("An error occurred while executing command: " + commandText); @@ -193,4 +222,45 @@ private CommandResult executeCommand(String commandText) throws CommandException throw e; } } + /** + * Executes the command and returns the result. + * + * @see seedu.address.logic.Logic#execute(Command) + */ + private CommandResult executeCommand(Command command) throws CommandException { + try { + CommandResult commandResult = logic.execute(command); + handleCommandResult(commandResult); + return commandResult; + } catch (CommandException e) { + logger.info("An error occurred while executing internal command."); + resultDisplay.setFeedbackToUser(e.getMessage()); + throw e; + } + } + + /** + * Logs command result, sends feedback to user and shows windows depending on the command result. + * + * @param commandResult Command result to handle. + */ + private void handleCommandResult(CommandResult commandResult) { + logger.info("Result: " + commandResult.getFeedbackToUser()); + resultDisplay.setFeedbackToUser(commandResult.getFeedbackToUser()); + + if (commandResult.isShowHelp()) { + handleHelp(); + } + if (commandResult.isExit()) { + handleExit(); + } + if (commandResult.isShowClear()) { + handleClear(); + } + if (commandResult.isShowOverride()) { + this.appointment = commandResult.getAppointment(); + this.personToEdit = commandResult.getPersonToEdit(); + handleOverride(appointment, personToEdit); + } + } } diff --git a/src/main/java/seedu/address/ui/OverrideWindow.java b/src/main/java/seedu/address/ui/OverrideWindow.java new file mode 100644 index 00000000000..9a8f4eb86f0 --- /dev/null +++ b/src/main/java/seedu/address/ui/OverrideWindow.java @@ -0,0 +1,141 @@ +package seedu.address.ui; + +import java.util.logging.Logger; + +import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.stage.Modality; +import javafx.stage.Stage; +import javafx.stage.StageStyle; +import seedu.address.commons.core.LogsCenter; +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.ConfirmOverrideCommand; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.appointment.Appointment; +import seedu.address.model.person.Person; + +/** + * Controller for the override appointment command confirmation page. + */ +public class OverrideWindow extends UiPart { + + public static final String CONFIRM_OVERRIDE_MESSAGE = "There is an appointment found under this person's name. " + + "Are you sure you want to override this appointment? \n"; + + private static String message; + private static Appointment appointment = null; + private static Person personToEdit = null; + + private static final Logger logger = LogsCenter.getLogger(OverrideWindow.class); + private static final String FXML = "OverrideWindow.fxml"; + private CommandExecutor commandExecutor; + + @FXML + private Label overrideMessage; + @FXML + private Button overrideButton; + @FXML + private Button cancelButton; + + /** + * Creates a new OverrideWindow. + * + * @param root Stage to use as the root of the OverrideWindow. + */ + public OverrideWindow(Stage root) { + super(FXML, root); + root.initStyle(StageStyle.UNDECORATED); + root.initModality(Modality.APPLICATION_MODAL); + overrideMessage.setText(CONFIRM_OVERRIDE_MESSAGE); + } + + /** + * Creates a new OverrideWindow. + */ + public OverrideWindow(CommandExecutor commandExecutor, Appointment appointment, Person personToEdit) { + this(new Stage()); + this.commandExecutor = commandExecutor; + this.appointment = appointment; + this.personToEdit = personToEdit; + overrideMessage.setText(CONFIRM_OVERRIDE_MESSAGE + personToEdit.getAppointment().toString()); + } + + /** + * Shows the clear window. + * @throws IllegalStateException + *
    + *
  • + * if this method is called on a thread other than the JavaFX Application Thread. + *
  • + *
  • + * if this method is called during animation or layout processing. + *
  • + *
  • + * if this method is called on the primary stage. + *
  • + *
  • + * if {@code dialogStage} is already showing. + *
  • + *
+ */ + public void show() { + logger.fine("Showing confirm override window."); + getRoot().show(); + getRoot().centerOnScreen(); + } + + /** + * Returns true if the override window is currently being shown. + */ + public boolean isShowing() { + return getRoot().isShowing(); + } + + /** + * Hides the override window. + */ + public void hide() { + getRoot().hide(); + } + + /** + * Focuses on the override window. + */ + public void focus() { + getRoot().requestFocus(); + } + /** + * Executes changing of the appointment. + */ + @FXML + private void confirmOverride() { + Command overrideCommand = new ConfirmOverrideCommand(this.appointment, this.personToEdit); + try { + commandExecutor.execute(overrideCommand); + } catch (CommandException e) { + // nothing to do in this window if there's an error. + } + hide(); + } + /** + * Cancels the override command. + */ + @FXML + private void cancel() { + hide(); + } + /** + * Represents a function that can execute commands. + */ + @FunctionalInterface + public interface CommandExecutor { + /** + * Executes the command and returns the result. + * + * @see seedu.address.logic.Logic#execute(Command) + */ + CommandResult execute(Command command) throws CommandException; + } +} diff --git a/src/main/java/seedu/address/ui/PersonCard.java b/src/main/java/seedu/address/ui/PersonCard.java index 094c42cda82..10873982f84 100644 --- a/src/main/java/seedu/address/ui/PersonCard.java +++ b/src/main/java/seedu/address/ui/PersonCard.java @@ -10,7 +10,7 @@ import seedu.address.model.person.Person; /** - * An UI component that displays information of a {@code Person}. + * A UI component that displays information of a {@code Person}. */ public class PersonCard extends UiPart { @@ -39,8 +39,19 @@ public class PersonCard extends UiPart { @FXML private Label email; @FXML + private Label nextOfKinName; + @FXML + private Label nextOfKinPhone; + @FXML + private Label appointment; + @FXML + private FlowPane financialPlans; + @FXML + private Label tagsTitle; + @FXML private FlowPane tags; + /** * Creates a {@code PersonCode} with the given {@code Person} and index to display. */ @@ -52,8 +63,22 @@ public PersonCard(Person person, int displayedIndex) { phone.setText(person.getPhone().value); address.setText(person.getAddress().value); email.setText(person.getEmail().value); + String nextOfKinNameText = "Next-of-kin: " + person.getNextOfKinName().fullName; + nextOfKinName.setText(nextOfKinNameText); + String nextOfKinPhoneText = "Next-of-kin Phone: " + person.getNextOfKinPhone().value; + nextOfKinPhone.setText(nextOfKinPhoneText); + String appointmentText = "Appointment: " + person.getAppointment().toString(); + appointment.setText(appointmentText); + person.getFinancialPlans().stream() + .sorted(Comparator.comparing(financialPlan -> financialPlan.financialPlanName)) + .forEach(financialPlan -> financialPlans.getChildren() + .add(new Label(financialPlan.financialPlanName))); + financialPlans.getChildren().forEach(label -> ((Label) label).setMaxWidth(500)); + financialPlans.getChildren().forEach(label -> ((Label) label).setWrapText(true)); person.getTags().stream() .sorted(Comparator.comparing(tag -> tag.tagName)) .forEach(tag -> tags.getChildren().add(new Label(tag.tagName))); + tags.getChildren().forEach(label -> ((Label) label).setMaxWidth(500)); + tags.getChildren().forEach(label -> ((Label) label).setWrapText(true)); } } diff --git a/src/main/resources/view/AppointmentListCard.fxml b/src/main/resources/view/AppointmentListCard.fxml new file mode 100644 index 00000000000..316aba99eeb --- /dev/null +++ b/src/main/resources/view/AppointmentListCard.fxml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/AppointmentListPanel.fxml b/src/main/resources/view/AppointmentListPanel.fxml new file mode 100644 index 00000000000..cb897960409 --- /dev/null +++ b/src/main/resources/view/AppointmentListPanel.fxml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/main/resources/view/ClearWindow.css b/src/main/resources/view/ClearWindow.css new file mode 100644 index 00000000000..17e8a8722cd --- /dev/null +++ b/src/main/resources/view/ClearWindow.css @@ -0,0 +1,19 @@ +#copyButton, #helpMessage { + -fx-text-fill: white; +} + +#copyButton { + -fx-background-color: dimgray; +} + +#copyButton:hover { + -fx-background-color: gray; +} + +#copyButton:armed { + -fx-background-color: darkgray; +} + +#helpMessageContainer { + -fx-background-color: derive(#1d1d1d, 20%); +} diff --git a/src/main/resources/view/ClearWindow.fxml b/src/main/resources/view/ClearWindow.fxml new file mode 100644 index 00000000000..77bee79628a --- /dev/null +++ b/src/main/resources/view/ClearWindow.fxml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/DarkTheme.css b/src/main/resources/view/DarkTheme.css index 36e6b001cd8..06c3f1db94c 100644 --- a/src/main/resources/view/DarkTheme.css +++ b/src/main/resources/view/DarkTheme.css @@ -1,29 +1,37 @@ .background { - -fx-background-color: derive(#1d1d1d, 20%); - background-color: #383838; /* Used in the default.html file */ + -fx-background-color: #F5F7F8; + background-color: #F4CE14; } .label { -fx-font-size: 11pt; -fx-font-family: "Segoe UI Semibold"; - -fx-text-fill: #555555; + -fx-text-fill: #4F6F52; -fx-opacity: 0.9; } .label-bright { -fx-font-size: 11pt; -fx-font-family: "Segoe UI Semibold"; - -fx-text-fill: white; + -fx-text-fill: #ECE3CE; -fx-opacity: 1; } .label-header { -fx-font-size: 32pt; -fx-font-family: "Segoe UI Light"; - -fx-text-fill: white; + -fx-text-fill: #ECE3CE; -fx-opacity: 1; } +.title-pane { + -fx-font-size: 18px; + -fx-font-weight: bold; + -fx-padding: 10px; + -fx-text-fill: #ECE3CE; + -fx-alignment: center; +} + .text-field { -fx-font-size: 12pt; -fx-font-family: "Segoe UI Semibold"; @@ -40,9 +48,9 @@ } .table-view { - -fx-base: #1d1d1d; - -fx-control-inner-background: #1d1d1d; - -fx-background-color: #1d1d1d; + -fx-base: #9E7777; + -fx-control-inner-background: #45474B; + -fx-background-color: #45474B; -fx-table-cell-border-color: transparent; -fx-table-header-border-color: transparent; -fx-padding: 5; @@ -59,7 +67,7 @@ -fx-border-color: transparent transparent - derive(-fx-base, 80%) + derive(#739072, 80%) transparent; -fx-border-insets: 0 10 1 0; } @@ -77,42 +85,48 @@ } .split-pane:horizontal .split-pane-divider { - -fx-background-color: derive(#1d1d1d, 20%); - -fx-border-color: transparent transparent transparent #4d4d4d; + -fx-background-color: derive(#45474B, 20%); + -fx-border-color: transparent transparent transparent #4F6F52; } .split-pane { -fx-border-radius: 1; -fx-border-width: 1; - -fx-background-color: derive(#1d1d1d, 20%); + -fx-background-color: derive(#45474B, 20%); } .list-view { -fx-background-insets: 0; - -fx-padding: 0; - -fx-background-color: derive(#1d1d1d, 20%); + -fx-padding: 5; + -fx-background-color: #45474B; } .list-cell { -fx-label-padding: 0 0 0 0; -fx-graphic-text-gap : 0; -fx-padding: 0 0 0 0; + -fx-background-color: #45474B; } .list-cell:filled:even { - -fx-background-color: #3c3e3f; + -fx-background-color: #495E57; + -fx-background-radius: 10px; + -fx-margin-bottom: 10px; } .list-cell:filled:odd { - -fx-background-color: #515658; + -fx-background-color: #739072; + -fx-background-radius: 10px; } .list-cell:filled:selected { - -fx-background-color: #424d5f; + -fx-background-color: #3A4D39; + -fx-background-radius: 10px; } .list-cell:filled:selected #cardPane { - -fx-border-color: #3e7b91; + -fx-border-color: #4F6F52; + -fx-border-radius: 10px; -fx-border-width: 1; } @@ -123,27 +137,27 @@ .cell_big_label { -fx-font-family: "Segoe UI Semibold"; -fx-font-size: 16px; - -fx-text-fill: #010504; + -fx-text-fill: #3A4D39; } .cell_small_label { -fx-font-family: "Segoe UI"; -fx-font-size: 13px; - -fx-text-fill: #010504; + -fx-text-fill: #3A4D39; } .stack-pane { - -fx-background-color: derive(#1d1d1d, 20%); + -fx-background-color: derive(#45474B, 20%); } .pane-with-border { - -fx-background-color: derive(#1d1d1d, 20%); - -fx-border-color: derive(#1d1d1d, 10%); + -fx-background-color: derive(#45474B, 20%); + -fx-border-color: derive(#45474B, 10%); -fx-border-top-width: 1px; } .status-bar { - -fx-background-color: derive(#1d1d1d, 30%); + -fx-background-color: derive(#45474B, 30%); } .result-display { @@ -165,8 +179,8 @@ } .status-bar-with-border { - -fx-background-color: derive(#1d1d1d, 30%); - -fx-border-color: derive(#1d1d1d, 25%); + -fx-background-color: derive(#45474B, 30%); + -fx-border-color: derive(#45474B, 25%); -fx-border-width: 1px; } @@ -175,17 +189,17 @@ } .grid-pane { - -fx-background-color: derive(#1d1d1d, 30%); - -fx-border-color: derive(#1d1d1d, 30%); + -fx-background-color: derive(#45474B, 30%); + -fx-border-color: derive(#45474B, 30%); -fx-border-width: 1px; } .grid-pane .stack-pane { - -fx-background-color: derive(#1d1d1d, 30%); + -fx-background-color: derive(#45474B, 30%); } .context-menu { - -fx-background-color: derive(#1d1d1d, 50%); + -fx-background-color: derive(#45474B, 50%); } .context-menu .label { @@ -193,7 +207,7 @@ } .menu-bar { - -fx-background-color: derive(#1d1d1d, 20%); + -fx-background-color: derive(#45474B, 20%); } .menu-bar .label { @@ -217,7 +231,7 @@ -fx-border-color: #e2e2e2; -fx-border-width: 2; -fx-background-radius: 0; - -fx-background-color: #1d1d1d; + -fx-background-color: #45474B; -fx-font-family: "Segoe UI", Helvetica, Arial, sans-serif; -fx-font-size: 11pt; -fx-text-fill: #d8d8d8; @@ -230,7 +244,7 @@ .button:pressed, .button:default:hover:pressed { -fx-background-color: white; - -fx-text-fill: #1d1d1d; + -fx-text-fill: #45474B; } .button:focused { @@ -243,7 +257,7 @@ .button:disabled, .button:default:disabled { -fx-opacity: 0.4; - -fx-background-color: #1d1d1d; + -fx-background-color: #45474B; -fx-text-fill: white; } @@ -257,11 +271,11 @@ } .dialog-pane { - -fx-background-color: #1d1d1d; + -fx-background-color: #45474B; } .dialog-pane > *.button-bar > *.container { - -fx-background-color: #1d1d1d; + -fx-background-color: #45474B; } .dialog-pane > *.label.content { @@ -271,7 +285,7 @@ } .dialog-pane:header *.header-panel { - -fx-background-color: derive(#1d1d1d, 25%); + -fx-background-color: derive(#45474B, 25%); } .dialog-pane:header *.header-panel *.label { @@ -282,11 +296,11 @@ } .scroll-bar { - -fx-background-color: derive(#1d1d1d, 20%); + -fx-background-color: derive(#45474B, 20%); } .scroll-bar .thumb { - -fx-background-color: derive(#1d1d1d, 50%); + -fx-background-color: derive(#45474B, 50%); -fx-background-insets: 3; } @@ -318,9 +332,9 @@ } #commandTextField { - -fx-background-color: transparent #383838 transparent #383838; + -fx-background-color: transparent #45474B transparent #45474B; -fx-background-insets: 0; - -fx-border-color: #383838 #383838 #ffffff #383838; + -fx-border-color: #45474B #45474B #ECE3CE #45474B; -fx-border-insets: 0; -fx-border-width: 1; -fx-font-family: "Segoe UI Light"; @@ -333,11 +347,12 @@ } #resultDisplay .content { - -fx-background-color: transparent, #383838, transparent, #383838; + -fx-background-color: transparent, #45474B, transparent, #45474B; -fx-background-radius: 0; } #tags { + -fx-padding: 3 0 3 0; -fx-hgap: 7; -fx-vgap: 3; } @@ -350,3 +365,18 @@ -fx-background-radius: 2; -fx-font-size: 11; } + +#financialPlans { + -fx-padding: 3 0 3 0; + -fx-hgap: 7; + -fx-vgap: 3; +} + +#financialPlans .label { + -fx-text-fill: #0C356A; + -fx-background-color: #FFC436; + -fx-padding: 1 3 1 3; + -fx-border-radius: 2; + -fx-background-radius: 2; + -fx-font-size: 11; +} diff --git a/src/main/resources/view/MainWindow.fxml b/src/main/resources/view/MainWindow.fxml index 7778f666a0a..a197109ce93 100644 --- a/src/main/resources/view/MainWindow.fxml +++ b/src/main/resources/view/MainWindow.fxml @@ -8,11 +8,13 @@ + + + title="UNOFAS" minWidth="500" minHeight="600" onCloseRequest="#handleExit"> @@ -45,14 +47,22 @@ - - + + + - + + + diff --git a/src/main/resources/view/OverrideWindow.fxml b/src/main/resources/view/OverrideWindow.fxml new file mode 100644 index 00000000000..19945beef3d --- /dev/null +++ b/src/main/resources/view/OverrideWindow.fxml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/PersonListCard.fxml b/src/main/resources/view/PersonListCard.fxml index f5e812e25e6..5bcd3085d82 100644 --- a/src/main/resources/view/PersonListCard.fxml +++ b/src/main/resources/view/PersonListCard.fxml @@ -7,30 +7,39 @@ + - + - + - + - + - + + + + + + diff --git a/src/test/data/JsonAddressBookStorageTest/invalidAndValidPersonAddressBook.json b/src/test/data/JsonAddressBookStorageTest/invalidAndValidPersonAddressBook.json index 6a4d2b7181c..022ffbb5688 100644 --- a/src/test/data/JsonAddressBookStorageTest/invalidAndValidPersonAddressBook.json +++ b/src/test/data/JsonAddressBookStorageTest/invalidAndValidPersonAddressBook.json @@ -3,11 +3,15 @@ "name": "Valid Person", "phone": "9482424", "email": "hans@example.com", - "address": "4th street" + "address": "4th street", + "nextOfKinName": "Valid Dad", + "nextOfKinPhone": "9482111" }, { "name": "Person With Invalid Phone Field", "phone": "948asdf2424", "email": "hans@example.com", - "address": "4th street" + "address": "4th street", + "nextOfKinName": "Person Dad", + "nextOfKinPhone": "948111" } ] } diff --git a/src/test/data/JsonAddressBookStorageTest/invalidPersonAddressBook.json b/src/test/data/JsonAddressBookStorageTest/invalidPersonAddressBook.json index ccd21f7d1a9..7b84d4659bd 100644 --- a/src/test/data/JsonAddressBookStorageTest/invalidPersonAddressBook.json +++ b/src/test/data/JsonAddressBookStorageTest/invalidPersonAddressBook.json @@ -3,6 +3,9 @@ "name": "Person with invalid name field: Ha!ns Mu@ster", "phone": "9482424", "email": "hans@example.com", - "address": "4th street" + "address": "4th street", + "nextOfKinName": "Person Dad", + "nextOfKinPhone": "9482111", + "appointment": "No Appointment made!" } ] } diff --git a/src/test/data/JsonSerializableAddressBookTest/duplicatePersonAddressBook.json b/src/test/data/JsonSerializableAddressBookTest/duplicatePersonAddressBook.json index a7427fe7aa2..aa0fcaba390 100644 --- a/src/test/data/JsonSerializableAddressBookTest/duplicatePersonAddressBook.json +++ b/src/test/data/JsonSerializableAddressBookTest/duplicatePersonAddressBook.json @@ -4,11 +4,17 @@ "phone": "94351253", "email": "alice@example.com", "address": "123, Jurong West Ave 6, #08-111", - "tags": [ "friends" ] + "nextOfKinName": "Alice Dad", + "nextOfKinPhone": "9482111", + "tags": [ "friends" ], + "appointment": "No Appointment made!" }, { "name": "Alice Pauline", "phone": "94351253", "email": "pauline@example.com", - "address": "4th street" + "address": "4th street", + "nextOfKinName": "Alice Dad", + "nextOfKinPhone": "9482111", + "appointment": "No Appointment made!" } ] } diff --git a/src/test/data/JsonSerializableAddressBookTest/invalidPersonAddressBook.json b/src/test/data/JsonSerializableAddressBookTest/invalidPersonAddressBook.json index ad3f135ae42..38666483541 100644 --- a/src/test/data/JsonSerializableAddressBookTest/invalidPersonAddressBook.json +++ b/src/test/data/JsonSerializableAddressBookTest/invalidPersonAddressBook.json @@ -3,6 +3,8 @@ "name": "Hans Muster", "phone": "9482424", "email": "invalid@email!3e", - "address": "4th street" + "address": "4th street", + "nextOfKinName": "Hans Dad", + "nextOfKinPhone": "9482111" } ] } diff --git a/src/test/data/JsonSerializableAddressBookTest/typicalPersonsAddressBook.json b/src/test/data/JsonSerializableAddressBookTest/typicalPersonsAddressBook.json index 72262099d35..1200baf744d 100644 --- a/src/test/data/JsonSerializableAddressBookTest/typicalPersonsAddressBook.json +++ b/src/test/data/JsonSerializableAddressBookTest/typicalPersonsAddressBook.json @@ -5,42 +5,69 @@ "phone" : "94351253", "email" : "alice@example.com", "address" : "123, Jurong West Ave 6, #08-111", - "tags" : [ "friends" ] + "nextOfKinName": "Alice Dad", + "nextOfKinPhone": "94351111", + "financialPlans" : ["Sample Financial Plan 1", "Sample Financial Plan 2"], + "tags" : [ "friends" ], + "appointment" : "No Appointment made!" }, { "name" : "Benson Meier", "phone" : "98765432", "email" : "johnd@example.com", "address" : "311, Clementi Ave 2, #02-25", - "tags" : [ "owesMoney", "friends" ] + "nextOfKinName": "Benson Dad", + "nextOfKinPhone": "98761111", + "financialPlans" : ["Sample Financial Plan 1", "Sample Financial Plan 2"], + "tags" : [ "owesMoney", "friends" ], + "appointment" : "Review insurance, 01-05-2023 18:00" }, { "name" : "Carl Kurz", "phone" : "95352563", "email" : "heinz@example.com", "address" : "wall street", - "tags" : [ ] + "nextOfKinName": "Carl Dad", + "nextOfKinPhone": "95351111", + "financialPlans" : [ ], + "tags" : [ ], + "appointment" : "Review insurance, 01-05-2023 07:00" }, { "name" : "Daniel Meier", "phone" : "87652533", "email" : "cornelia@example.com", "address" : "10th street", - "tags" : [ "friends" ] + "nextOfKinName": "Daniel Dad", + "nextOfKinPhone": "87651111", + "tags" : [ "friends" ], + "appointment" : "No Appointment made!" }, { "name" : "Elle Meyer", "phone" : "9482224", "email" : "werner@example.com", "address" : "michegan ave", - "tags" : [ ] + "nextOfKinName": "Elle Dad", + "nextOfKinPhone": "94821113", + "financialPlans" : ["Sample Financial Plan 2"], + "tags" : [ ], + "appointment" : "No Appointment made!" }, { "name" : "Fiona Kunz", "phone" : "9482427", "email" : "lydia@example.com", "address" : "little tokyo", - "tags" : [ ] + "nextOfKinName": "Fiona Dad", + "nextOfKinPhone": "94821111", + "financialPlans" : [ ], + "tags" : [ ], + "appointment" : "No Appointment made!" }, { "name" : "George Best", "phone" : "9482442", "email" : "anna@example.com", "address" : "4th street", - "tags" : [ ] + "nextOfKinName": "George Dad", + "nextOfKinPhone": "94821112", + "financialPlans" : ["Sample Financial Plan 1", "Sample Financial Plan 2"], + "tags" : [ ], + "appointment" : "No Appointment made!" } ] } diff --git a/src/test/java/seedu/address/commons/util/StringUtilTest.java b/src/test/java/seedu/address/commons/util/StringUtilTest.java index c56d407bf3f..d693eace727 100644 --- a/src/test/java/seedu/address/commons/util/StringUtilTest.java +++ b/src/test/java/seedu/address/commons/util/StringUtilTest.java @@ -123,6 +123,46 @@ public void containsWordIgnoreCase_validInputs_correctResult() { assertTrue(StringUtil.containsWordIgnoreCase("AAA bBb ccc bbb", "bbB")); } + + //---------------- Tests for containsWordsIgnoreCase ------------------------- + @Test + public void containsWordsIgnoreCase_nullWord_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> StringUtil.containsWordsIgnoreCase("typical sentence", null)); + } + + @Test + public void containsWordsIgnoreCase_emptyWord_throwsIllegalArgumentException() { + assertThrows(IllegalArgumentException.class, "Word parameter cannot be empty", () + -> StringUtil.containsWordsIgnoreCase("typical sentence", " ")); + } + + @Test + public void containsWordsIgnoreCase_nullSentence_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> StringUtil.containsWordsIgnoreCase(null, "abc")); + } + + @Test + public void containsWordsIgnoreCase_validInputs_correctResult() { + + // Empty sentence + assertFalse(StringUtil.containsWordsIgnoreCase("", "abc")); // Boundary case + assertFalse(StringUtil.containsWordsIgnoreCase(" ", "123")); + + // Matches a partial word only + assertFalse(StringUtil.containsWordsIgnoreCase("aaa bbb ccc", "bbb c")); // Sentence word bigger than query word + assertFalse(StringUtil.containsWordsIgnoreCase("aaa bbb ccc", "bbbb")); // Query word bigger than sentence word + + // Matches word in the sentence, different upper/lower case letters + assertTrue(StringUtil.containsWordsIgnoreCase("aaa bBb ccc", "Bbb")); // First word (boundary case) + assertTrue(StringUtil.containsWordsIgnoreCase("aaa bBb ccc@1", "CCc@1")); // Last word (boundary case) + assertTrue(StringUtil.containsWordsIgnoreCase(" AAA bBb ccc ", "aaa bbb")); // Sentence has extra spaces + assertTrue(StringUtil.containsWordsIgnoreCase("Aaa", "aaa")); // Only one word in sentence (boundary case) + assertTrue(StringUtil.containsWordsIgnoreCase("aaa bbb ccc", " bbb ccc ")); // Leading/trailing spaces + + // Matches multiple words in sentence + assertTrue(StringUtil.containsWordsIgnoreCase("AAA bBb ccc bbb ccc", "bbB ccc")); + } + //---------------- Tests for getDetails -------------------------------------- /* diff --git a/src/test/java/seedu/address/logic/LogicManagerTest.java b/src/test/java/seedu/address/logic/LogicManagerTest.java index baf8ce336a2..2a15a3d963b 100644 --- a/src/test/java/seedu/address/logic/LogicManagerTest.java +++ b/src/test/java/seedu/address/logic/LogicManagerTest.java @@ -5,7 +5,10 @@ import static seedu.address.logic.Messages.MESSAGE_UNKNOWN_COMMAND; import static seedu.address.logic.commands.CommandTestUtil.ADDRESS_DESC_AMY; import static seedu.address.logic.commands.CommandTestUtil.EMAIL_DESC_AMY; +import static seedu.address.logic.commands.CommandTestUtil.FINANCIAL_PLAN_DESC_1; import static seedu.address.logic.commands.CommandTestUtil.NAME_DESC_AMY; +import static seedu.address.logic.commands.CommandTestUtil.NEXT_OF_KIN_NAME_DESC_AMY; +import static seedu.address.logic.commands.CommandTestUtil.NEXT_OF_KIN_PHONE_DESC_AMY; import static seedu.address.logic.commands.CommandTestUtil.PHONE_DESC_AMY; import static seedu.address.testutil.Assert.assertThrows; import static seedu.address.testutil.TypicalPersons.AMY; @@ -18,8 +21,11 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import seedu.address.commons.core.index.Index; import seedu.address.logic.commands.AddCommand; +import seedu.address.logic.commands.Command; import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.DeleteCommand; import seedu.address.logic.commands.ListCommand; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.logic.parser.exceptions.ParseException; @@ -53,23 +59,33 @@ public void setUp() { } @Test - public void execute_invalidCommandFormat_throwsParseException() { + public void execute_invalidCommandStringFormat_throwsParseException() { String invalidCommand = "uicfhmowqewca"; assertParseException(invalidCommand, MESSAGE_UNKNOWN_COMMAND); } @Test - public void execute_commandExecutionError_throwsCommandException() { + public void execute_commandStringExecutionError_throwsCommandException() { String deleteCommand = "delete 9"; assertCommandException(deleteCommand, MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); } @Test - public void execute_validCommand_success() throws Exception { + public void execute_validCommandString_success() throws Exception { String listCommand = ListCommand.COMMAND_WORD; assertCommandSuccess(listCommand, ListCommand.MESSAGE_SUCCESS, model); } + @Test + public void execute_commandExecutionError_throwsCommandException() { + Command deleteCommand = new DeleteCommand(Index.fromOneBased(9)); + assertCommandException(deleteCommand, MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + @Test + public void execute_validCommand_success() throws Exception { + Command listCommand = new ListCommand(); + assertCommandSuccess(listCommand, ListCommand.MESSAGE_SUCCESS, model); + } @Test public void execute_storageThrowsIoException_throwsCommandException() { assertCommandFailureForExceptionFromStorage(DUMMY_IO_EXCEPTION, String.format( @@ -98,9 +114,25 @@ private void assertCommandSuccess(String inputCommand, String expectedMessage, Model expectedModel) throws CommandException, ParseException { CommandResult result = logic.execute(inputCommand); assertEquals(expectedMessage, result.getFeedbackToUser()); + assertEquals(logic.getAppointmentList(), expectedModel.getAppointmentList()); + assertEquals(model.getAppointmentList(), expectedModel.getAppointmentList()); + assertEquals(expectedModel, model); + } + /** + * Executes the command and confirms that + * - no exceptions are thrown
+ * - the feedback message is equal to {@code expectedMessage}
+ * - the internal model manager state is the same as that in {@code expectedModel}
+ * @see #assertCommandFailure(Command, Class, String, Model) + */ + private void assertCommandSuccess(Command inputCommand, String expectedMessage, + Model expectedModel) throws CommandException { + CommandResult result = logic.execute(inputCommand); + assertEquals(logic.getAppointmentList(), expectedModel.getAppointmentList()); + assertEquals(model.getAppointmentList(), expectedModel.getAppointmentList()); + assertEquals(expectedMessage, result.getFeedbackToUser()); assertEquals(expectedModel, model); } - /** * Executes the command, confirms that a ParseException is thrown and that the result message is correct. * @see #assertCommandFailure(String, Class, String, Model) @@ -116,17 +148,31 @@ private void assertParseException(String inputCommand, String expectedMessage) { private void assertCommandException(String inputCommand, String expectedMessage) { assertCommandFailure(inputCommand, CommandException.class, expectedMessage); } - + /** + * Executes the command, confirms that a CommandException is thrown and that the result message is correct. + * @see #assertCommandFailure(Command, Class, String, Model) + */ + private void assertCommandException(Command inputCommand, String expectedMessage) { + assertCommandFailure(inputCommand, CommandException.class, expectedMessage); + } /** * Executes the command, confirms that the exception is thrown and that the result message is correct. * @see #assertCommandFailure(String, Class, String, Model) */ - private void assertCommandFailure(String inputCommand, Class expectedException, + private void assertCommandFailure(Command inputCommand, Class expectedException, String expectedMessage) { Model expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); assertCommandFailure(inputCommand, expectedException, expectedMessage, expectedModel); } - + /** + * Executes the command, confirms that the exception is thrown and that the result message is correct. + * @see #assertCommandFailure(Command, Class, String, Model) + */ + private void assertCommandFailure(String inputCommand, Class expectedException, + String expectedMessage) { + Model expectedModel = new ModelManager(model.getAddressBook(), new UserPrefs()); + assertCommandFailure(inputCommand, expectedException, expectedMessage, expectedModel); + } /** * Executes the command and confirms that * - the {@code expectedException} is thrown
@@ -139,7 +185,18 @@ private void assertCommandFailure(String inputCommand, Class logic.execute(inputCommand)); assertEquals(expectedModel, model); } - + /** + * Executes the command and confirms that + * - the {@code expectedException} is thrown
+ * - the resulting error message is equal to {@code expectedMessage}
+ * - the internal model manager state is the same as that in {@code expectedModel}
+ * @see #assertCommandSuccess(Command, String, Model) + */ + private void assertCommandFailure(Command command, Class expectedException, + String expectedMessage, Model expectedModel) { + assertThrows(expectedException, expectedMessage, () -> logic.execute(command)); + assertEquals(expectedModel, model); + } /** * Tests the Logic component's handling of an {@code IOException} thrown by the Storage component. * @@ -165,8 +222,8 @@ public void saveAddressBook(ReadOnlyAddressBook addressBook, Path filePath) logic = new LogicManager(model, storage); // Triggers the saveAddressBook method by executing an add command - String addCommand = AddCommand.COMMAND_WORD + NAME_DESC_AMY + PHONE_DESC_AMY - + EMAIL_DESC_AMY + ADDRESS_DESC_AMY; + String addCommand = AddCommand.COMMAND_WORD + NAME_DESC_AMY + PHONE_DESC_AMY + EMAIL_DESC_AMY + + ADDRESS_DESC_AMY + FINANCIAL_PLAN_DESC_1 + NEXT_OF_KIN_NAME_DESC_AMY + NEXT_OF_KIN_PHONE_DESC_AMY; Person expectedPerson = new PersonBuilder(AMY).withTags().build(); ModelManager expectedModel = new ModelManager(); expectedModel.addPerson(expectedPerson); diff --git a/src/test/java/seedu/address/logic/commands/AddCommandTest.java b/src/test/java/seedu/address/logic/commands/AddCommandTest.java index 90e8253f48e..0ea69c8cb4f 100644 --- a/src/test/java/seedu/address/logic/commands/AddCommandTest.java +++ b/src/test/java/seedu/address/logic/commands/AddCommandTest.java @@ -8,8 +8,10 @@ import static seedu.address.testutil.TypicalPersons.ALICE; import java.nio.file.Path; +import java.time.LocalDate; import java.util.ArrayList; import java.util.Arrays; +import java.util.Comparator; import java.util.function.Predicate; import org.junit.jupiter.api.Test; @@ -22,7 +24,9 @@ import seedu.address.model.Model; import seedu.address.model.ReadOnlyAddressBook; import seedu.address.model.ReadOnlyUserPrefs; +import seedu.address.model.appointment.Appointment; import seedu.address.model.person.Person; +import seedu.address.model.person.gatheremail.GatherEmailPrompt; import seedu.address.testutil.PersonBuilder; public class AddCommandTest { @@ -153,10 +157,36 @@ public ObservableList getFilteredPersonList() { throw new AssertionError("This method should not be called."); } + @Override + public ObservableList getAppointmentList() { + throw new AssertionError("This method should not be called."); + } + @Override public void updateFilteredPersonList(Predicate predicate) { throw new AssertionError("This method should not be called."); } + + + @Override + public void sortFilteredPersonList(Comparator comparator) { + throw new AssertionError("This method should not be called"); + } + + @Override + public String gatherEmails(GatherEmailPrompt prompt) { + throw new AssertionError("This method should not be called."); + } + + @Override + public void clearAppointments(LocalDate date) { + throw new AssertionError("This method should not be called."); + } + + @Override + public boolean hasAppointmentWithDate(LocalDate date) { + throw new AssertionError("This method should not be called."); + } } /** diff --git a/src/test/java/seedu/address/logic/commands/ClearCommandTest.java b/src/test/java/seedu/address/logic/commands/ClearCommandTest.java index 80d9110c03a..b333106d0c4 100644 --- a/src/test/java/seedu/address/logic/commands/ClearCommandTest.java +++ b/src/test/java/seedu/address/logic/commands/ClearCommandTest.java @@ -5,7 +5,6 @@ import org.junit.jupiter.api.Test; -import seedu.address.model.AddressBook; import seedu.address.model.Model; import seedu.address.model.ModelManager; import seedu.address.model.UserPrefs; @@ -17,16 +16,19 @@ public void execute_emptyAddressBook_success() { Model model = new ModelManager(); Model expectedModel = new ModelManager(); - assertCommandSuccess(new ClearCommand(), model, ClearCommand.MESSAGE_SUCCESS, expectedModel); + assertCommandSuccess(new ClearCommand(), model, + new CommandResult(ClearCommand.CONFIRM_CLEAR_MESSAGE, false, false, true), + expectedModel); } @Test public void execute_nonEmptyAddressBook_success() { Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); Model expectedModel = new ModelManager(getTypicalAddressBook(), new UserPrefs()); - expectedModel.setAddressBook(new AddressBook()); - assertCommandSuccess(new ClearCommand(), model, ClearCommand.MESSAGE_SUCCESS, expectedModel); + assertCommandSuccess(new ClearCommand(), model, + new CommandResult(ClearCommand.CONFIRM_CLEAR_MESSAGE, false, false, true), + expectedModel); } } diff --git a/src/test/java/seedu/address/logic/commands/CommandResultTest.java b/src/test/java/seedu/address/logic/commands/CommandResultTest.java index 7b8c7cd4546..75fabb89a45 100644 --- a/src/test/java/seedu/address/logic/commands/CommandResultTest.java +++ b/src/test/java/seedu/address/logic/commands/CommandResultTest.java @@ -4,9 +4,16 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; import org.junit.jupiter.api.Test; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.appointment.Appointment; +import seedu.address.model.person.Person; + public class CommandResultTest { @Test public void equals() { @@ -33,6 +40,8 @@ public void equals() { // different exit value -> returns false assertFalse(commandResult.equals(new CommandResult("feedback", false, true))); + // different showClear value -> returns false + assertFalse(commandResult.equals(new CommandResult("feedback", false, false, true))); } @Test @@ -50,6 +59,37 @@ public void hashcode() { // different exit value -> returns different hashcode assertNotEquals(commandResult.hashCode(), new CommandResult("feedback", false, true).hashCode()); + // different showClear value -> returns different hashcode + assertNotEquals(commandResult.hashCode(), new CommandResult("feedback", + false, false, true).hashCode()); + } + + @Test + public void commandResultWithParamsTest() { + Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + Model expectedModel = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + + Person person = model.getFilteredPersonList().get(1); + Person person2 = model.getFilteredPersonList().get(2); + Appointment appointment = Appointment.parseAppointmentDescription("Description, 01-01-2023 20:00"); + Appointment appointment2 = Appointment.parseAppointmentDescription("Description, 01-01-2023 00:00"); + + CommandResult commandResult = new CommandResult("feedback", false, + true, person, appointment); + + // same object -> returns true + assertTrue(commandResult.equals(commandResult)); + + // null -> returns false + assertFalse(commandResult.equals(null)); + + // different types -> returns false + assertFalse(commandResult.equals(0.5f)); + + //different constructors -> returns false + assertNotEquals(commandResult, new CommandResult("feedback", false, true)); + assertNotEquals(commandResult, new CommandResult("feedback", false, true, person, appointment2)); + assertNotEquals(commandResult, new CommandResult("feedback", false, true, person2, appointment)); } @Test @@ -57,7 +97,7 @@ public void toStringMethod() { CommandResult commandResult = new CommandResult("feedback"); String expected = CommandResult.class.getCanonicalName() + "{feedbackToUser=" + commandResult.getFeedbackToUser() + ", showHelp=" + commandResult.isShowHelp() - + ", exit=" + commandResult.isExit() + "}"; + + ", exit=" + commandResult.isExit() + ", showClear=" + commandResult.isShowClear() + "}"; assertEquals(expected, commandResult.toString()); } } diff --git a/src/test/java/seedu/address/logic/commands/CommandTestUtil.java b/src/test/java/seedu/address/logic/commands/CommandTestUtil.java index 643a1d08069..9ac6fa96b87 100644 --- a/src/test/java/seedu/address/logic/commands/CommandTestUtil.java +++ b/src/test/java/seedu/address/logic/commands/CommandTestUtil.java @@ -3,8 +3,13 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_APPOINTMENT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_APPOINTMENT_DATE; import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_FINANCIAL_PLAN; import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NEXT_OF_KIN_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NEXT_OF_KIN_PHONE; import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; import static seedu.address.testutil.Assert.assertThrows; @@ -17,8 +22,8 @@ import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.model.AddressBook; import seedu.address.model.Model; -import seedu.address.model.person.NameContainsKeywordsPredicate; import seedu.address.model.person.Person; +import seedu.address.model.person.predicates.NameContainsKeywordsPredicate; import seedu.address.testutil.EditPersonDescriptorBuilder; /** @@ -34,9 +39,19 @@ public class CommandTestUtil { public static final String VALID_EMAIL_BOB = "bob@example.com"; public static final String VALID_ADDRESS_AMY = "Block 312, Amy Street 1"; public static final String VALID_ADDRESS_BOB = "Block 123, Bobby Street 3"; + public static final String VALID_NEXT_OF_KIN_NAME_AMY = "Amy Dad"; + public static final String VALID_NEXT_OF_KIN_NAME_BOB = "Bob Dad"; + public static final String VALID_NEXT_OF_KIN_PHONE_AMY = "33333333"; + public static final String VALID_NEXT_OF_KIN_PHONE_BOB = "44444444"; + public static final String VALID_TAG_HUSBAND = "husband"; public static final String VALID_TAG_FRIEND = "friend"; - + public static final String VALID_FINANCIAL_PLAN_1 = "financial plan 1"; + public static final String VALID_FINANCIAL_PLAN_2 = "financial plan 2"; + public static final String VALID_APPOINTMENT_NAME = "Review Insurance"; + public static final String VALID_APPOINTMENT_DATE = "01-01-2023 20:00"; + public static final String VALID_APPOINTMENT = VALID_APPOINTMENT_NAME + ", " + VALID_APPOINTMENT_DATE; + public static final String VALID_COMPLETE_DATE = "05-01-2023"; public static final String NAME_DESC_AMY = " " + PREFIX_NAME + VALID_NAME_AMY; public static final String NAME_DESC_BOB = " " + PREFIX_NAME + VALID_NAME_BOB; public static final String PHONE_DESC_AMY = " " + PREFIX_PHONE + VALID_PHONE_AMY; @@ -45,15 +60,36 @@ public class CommandTestUtil { public static final String EMAIL_DESC_BOB = " " + PREFIX_EMAIL + VALID_EMAIL_BOB; public static final String ADDRESS_DESC_AMY = " " + PREFIX_ADDRESS + VALID_ADDRESS_AMY; public static final String ADDRESS_DESC_BOB = " " + PREFIX_ADDRESS + VALID_ADDRESS_BOB; + public static final String NEXT_OF_KIN_NAME_DESC_AMY = " " + PREFIX_NEXT_OF_KIN_NAME + VALID_NEXT_OF_KIN_NAME_AMY; + + public static final String NEXT_OF_KIN_NAME_DESC_BOB = " " + PREFIX_NEXT_OF_KIN_NAME + VALID_NEXT_OF_KIN_NAME_BOB; + + public static final String NEXT_OF_KIN_PHONE_DESC_AMY = " " + PREFIX_NEXT_OF_KIN_PHONE + + VALID_NEXT_OF_KIN_PHONE_AMY; + + public static final String NEXT_OF_KIN_PHONE_DESC_BOB = " " + PREFIX_NEXT_OF_KIN_PHONE + + VALID_NEXT_OF_KIN_PHONE_BOB; public static final String TAG_DESC_FRIEND = " " + PREFIX_TAG + VALID_TAG_FRIEND; public static final String TAG_DESC_HUSBAND = " " + PREFIX_TAG + VALID_TAG_HUSBAND; - + public static final String FINANCIAL_PLAN_DESC_1 = " " + PREFIX_FINANCIAL_PLAN + VALID_FINANCIAL_PLAN_1; + public static final String FINANCIAL_PLAN_DESC_2 = " " + PREFIX_FINANCIAL_PLAN + VALID_FINANCIAL_PLAN_2; + public static final String APPOINTMENT_NAME_DESC = " " + PREFIX_APPOINTMENT + VALID_APPOINTMENT_NAME; + public static final String APPOINTMENT_DATE_DESC = " " + PREFIX_APPOINTMENT_DATE + VALID_APPOINTMENT_DATE; public static final String INVALID_NAME_DESC = " " + PREFIX_NAME + "James&"; // '&' not allowed in names public static final String INVALID_PHONE_DESC = " " + PREFIX_PHONE + "911a"; // 'a' not allowed in phones public static final String INVALID_EMAIL_DESC = " " + PREFIX_EMAIL + "bob!yahoo"; // missing '@' symbol public static final String INVALID_ADDRESS_DESC = " " + PREFIX_ADDRESS; // empty string not allowed for addresses + public static final String INVALID_NEXT_OF_KIN_NAME_DESC = " " + PREFIX_NEXT_OF_KIN_NAME + + "James&"; // '&' not allowed in names + public static final String INVALID_NEXT_OF_KIN_PHONE_DESC = " " + PREFIX_NEXT_OF_KIN_PHONE + + "911a"; // 'a' not allowed in phones public static final String INVALID_TAG_DESC = " " + PREFIX_TAG + "hubby*"; // '*' not allowed in tags - + public static final String INVALID_FINANCIAL_PLAN_DESC = " " + PREFIX_FINANCIAL_PLAN + + "financial_plan"; // '_' not allowed in financial plan names + public static final String INVALID_APPOINTMENT_NAME_DESC = " " + PREFIX_APPOINTMENT + "Review *&Insurance"; + public static final String INVALID_APPOINTMENT_TIME_FORMAT = " " + PREFIX_APPOINTMENT_DATE + "01-01-2023 12pm"; + public static final String INVALID_APPOINTMENT_DATE_FORMAT = " " + PREFIX_APPOINTMENT_DATE + "1 Jan 2023 18:00"; + public static final String INVALID_APPOINTMENT_DATE = " " + PREFIX_APPOINTMENT_DATE + "10-13-2023 00:00"; public static final String PREAMBLE_WHITESPACE = "\t \r \n"; public static final String PREAMBLE_NON_EMPTY = "NonEmptyPreamble"; @@ -63,10 +99,13 @@ public class CommandTestUtil { static { DESC_AMY = new EditPersonDescriptorBuilder().withName(VALID_NAME_AMY) .withPhone(VALID_PHONE_AMY).withEmail(VALID_EMAIL_AMY).withAddress(VALID_ADDRESS_AMY) - .withTags(VALID_TAG_FRIEND).build(); + .withNextOfKinName(VALID_NEXT_OF_KIN_NAME_AMY).withNextOfKinPhone(VALID_NEXT_OF_KIN_PHONE_AMY) + .withTags(VALID_TAG_FRIEND).withFinancialPlans(VALID_FINANCIAL_PLAN_1).build(); DESC_BOB = new EditPersonDescriptorBuilder().withName(VALID_NAME_BOB) .withPhone(VALID_PHONE_BOB).withEmail(VALID_EMAIL_BOB).withAddress(VALID_ADDRESS_BOB) - .withTags(VALID_TAG_HUSBAND, VALID_TAG_FRIEND).build(); + .withNextOfKinName(VALID_NEXT_OF_KIN_NAME_BOB).withNextOfKinPhone(VALID_NEXT_OF_KIN_PHONE_BOB) + .withTags(VALID_TAG_HUSBAND, VALID_TAG_FRIEND) + .withFinancialPlans(VALID_FINANCIAL_PLAN_1, VALID_FINANCIAL_PLAN_2).build(); } /** diff --git a/src/test/java/seedu/address/logic/commands/CompleteByDateTest.java b/src/test/java/seedu/address/logic/commands/CompleteByDateTest.java new file mode 100644 index 00000000000..087ce848fba --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/CompleteByDateTest.java @@ -0,0 +1,100 @@ +package seedu.address.logic.commands; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandFailure; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.logic.commands.CommandTestUtil.showPersonAtIndex; +import static seedu.address.logic.commands.CompleteCommand.MESSAGE_COMPLETE_SUCCESS; +import static seedu.address.logic.commands.CompleteCommand.MESSAGE_DATE_NO_APPOINTMENT; +import static seedu.address.testutil.Assert.assertThrows; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; +import static seedu.address.testutil.TypicalPersons.BENSON; +import static seedu.address.testutil.TypicalPersons.CARL; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; + +import java.time.LocalDate; + +import org.junit.jupiter.api.Test; + +import seedu.address.model.AddressBook; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.person.Person; +import seedu.address.testutil.PersonBuilder; + +class CompleteByDateTest { + private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + @Test + public void constructor_nullDate_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> new CompleteByDate(null)); + } + @Test + public void execute_dateSpecified_success() { + Person editedPerson1 = new PersonBuilder(BENSON).withNullAppointment().build(); + Person editedPerson2 = new PersonBuilder(CARL).withNullAppointment().build(); + String expectedMessage = MESSAGE_COMPLETE_SUCCESS; + + + CompleteByDate completeCommand = new CompleteByDate(LocalDate.of(2023, 05, 01)); + + Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), new UserPrefs()); + expectedModel.setPerson(BENSON, editedPerson1); + expectedModel.setPerson(CARL, editedPerson2); + + assertCommandSuccess(completeCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_noMatchingDate_failure() { + Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), new UserPrefs()); + + //no matching appointment date + CompleteByDate completeCommand = new CompleteByDate(LocalDate.of(2023, 02, 10)); + + assertCommandFailure(completeCommand, expectedModel, MESSAGE_DATE_NO_APPOINTMENT); + } + + /** + * All entries having matching dates in addressBook should be cleared regardless of filtered list. + */ + @Test + public void execute_filteredList_success() { + showPersonAtIndex(model, INDEX_FIRST_PERSON); //model filtered to show only first person + Person editedPerson1 = new PersonBuilder(BENSON).withNullAppointment().build(); + Person editedPerson2 = new PersonBuilder(CARL).withNullAppointment().build(); + + CompleteByDate completeCommand = new CompleteByDate(LocalDate.of(2023, 05, 01)); + + Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), new UserPrefs()); + expectedModel.setPerson(BENSON, editedPerson1); + expectedModel.setPerson(CARL, editedPerson2); + + assertCommandSuccess(completeCommand, model, MESSAGE_COMPLETE_SUCCESS, expectedModel); + } + + @Test + public void equals() { + LocalDate date1 = LocalDate.of(2023, 02, 02); + LocalDate date2 = LocalDate.of(2023, 01, 01); + + CompleteByDate completeCommand1 = new CompleteByDate(date1); + CompleteByDate completeCommand2 = new CompleteByDate(date2); + + //same object + assertTrue(completeCommand1.equals(completeCommand1)); + + //same index + assertTrue(completeCommand1.equals(new CompleteByDate(date1))); + + // null -> returns false + assertFalse(completeCommand1.equals(null)); + + // different types -> returns false + assertFalse(completeCommand1.equals(new ClearCommand())); + + // diff index -> returns false + assertFalse(completeCommand1.equals(completeCommand2)); + } +} diff --git a/src/test/java/seedu/address/logic/commands/CompleteByIndexTest.java b/src/test/java/seedu/address/logic/commands/CompleteByIndexTest.java new file mode 100644 index 00000000000..c92c1e70c74 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/CompleteByIndexTest.java @@ -0,0 +1,95 @@ +package seedu.address.logic.commands; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandFailure; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.logic.commands.CommandTestUtil.showPersonAtIndex; +import static seedu.address.logic.commands.CompleteCommand.MESSAGE_COMPLETE_SUCCESS; +import static seedu.address.logic.commands.CompleteCommand.MESSAGE_PERSON_NO_APPOINTMENT; +import static seedu.address.testutil.Assert.assertThrows; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; +import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND_PERSON; +import static seedu.address.testutil.TypicalPersons.BENSON; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.Messages; +import seedu.address.model.AddressBook; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.person.Person; +import seedu.address.testutil.PersonBuilder; + +class CompleteByIndexTest { + private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + + @Test + public void constructor_nullIndex_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> new CompleteByIndex(null)); + } + + @Test + public void execute_indexSpecified_success() { + Person editedPerson = new PersonBuilder(BENSON).withNullAppointment().build(); + String expectedMessage = MESSAGE_COMPLETE_SUCCESS; + + Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), new UserPrefs()); + expectedModel.setPerson(model.getFilteredPersonList().get(1), editedPerson); + CompleteByIndex completeCommand = new CompleteByIndex(INDEX_SECOND_PERSON); + + assertCommandSuccess(completeCommand, model, expectedMessage, expectedModel); + } + + @Test + public void execute_personNoAppointment_failure() { + Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), new UserPrefs()); + CompleteByIndex completeCommand = new CompleteByIndex(INDEX_FIRST_PERSON); //person has no appointment + + assertCommandFailure(completeCommand, expectedModel, MESSAGE_PERSON_NO_APPOINTMENT); + } + + @Test + public void execute_invalidPersonIndexUnfilteredList_failure() { + Index outOfBoundIndex = Index.fromOneBased(model.getFilteredPersonList().size() + 1); + CompleteByIndex completeByIndex = new CompleteByIndex(outOfBoundIndex); + + assertCommandFailure(completeByIndex, model, Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + @Test + public void execute_invalidPersonIndexFilteredList_failure() { + showPersonAtIndex(model, INDEX_FIRST_PERSON); + Index outOfBoundIndex = INDEX_SECOND_PERSON; + // ensures that outOfBoundIndex is still in bounds of address book list + assertTrue(outOfBoundIndex.getZeroBased() < model.getAddressBook().getPersonList().size()); + + CompleteByIndex completeByIndex = new CompleteByIndex(outOfBoundIndex); + + assertCommandFailure(completeByIndex, model, Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + @Test + public void equals() { + CompleteByIndex completeCommand1 = new CompleteByIndex(INDEX_FIRST_PERSON); + CompleteByIndex completeCommand2 = new CompleteByIndex(INDEX_SECOND_PERSON); + + //same object + assertTrue(completeCommand1.equals(completeCommand1)); + + //same index + assertTrue(completeCommand1.equals(new CompleteByIndex(INDEX_FIRST_PERSON))); + + // null -> returns false + assertFalse(completeCommand1.equals(null)); + + // different types -> returns false + assertFalse(completeCommand1.equals(new ClearCommand())); + + // diff index -> returns false + assertFalse(completeCommand1.equals(completeCommand2)); + } +} diff --git a/src/test/java/seedu/address/logic/commands/CompleteCommandTest.java b/src/test/java/seedu/address/logic/commands/CompleteCommandTest.java new file mode 100644 index 00000000000..4a306b2cd29 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/CompleteCommandTest.java @@ -0,0 +1,75 @@ +package seedu.address.logic.commands; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.logic.commands.CompleteCommand.MESSAGE_COMPLETE_SUCCESS; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; +import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND_PERSON; +import static seedu.address.testutil.TypicalPersons.BENSON; +import static seedu.address.testutil.TypicalPersons.CARL; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; + +import java.time.LocalDate; + +import org.junit.jupiter.api.Test; + +import seedu.address.logic.parser.CompleteCommandParser; +import seedu.address.model.AddressBook; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.person.Person; +import seedu.address.testutil.PersonBuilder; + +class CompleteCommandTest { + private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + private CompleteCommandParser commandParser = new CompleteCommandParser(); + + @Test + public void testCompleteByIndexSubclass() { + //valid Index + Person editedPerson = new PersonBuilder(BENSON).withNullAppointment().build(); + String expectedMessage = MESSAGE_COMPLETE_SUCCESS; + + Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), new UserPrefs()); + expectedModel.setPerson(model.getFilteredPersonList().get(1), editedPerson); + + CompleteCommand completeCommand = new CompleteByIndex(INDEX_SECOND_PERSON); + + assertCommandSuccess(completeCommand, model, expectedMessage, expectedModel); + } + @Test + public void testCompleteByDateSubclass() { + Person editedPerson1 = new PersonBuilder(BENSON).withNullAppointment().build(); + Person editedPerson2 = new PersonBuilder(CARL).withNullAppointment().build(); + String expectedMessage = MESSAGE_COMPLETE_SUCCESS; + + LocalDate validDate = LocalDate.of(2023, 05, 01); + + Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), new UserPrefs()); + expectedModel.setPerson(model.getFilteredPersonList().get(1), editedPerson1); + expectedModel.setPerson(model.getFilteredPersonList().get(2), editedPerson2); + + CompleteCommand completeCommand = new CompleteByDate(validDate); + + assertCommandSuccess(completeCommand, model, expectedMessage, expectedModel); + } + @Test + public void equals() { + LocalDate validDate = LocalDate.of(2023, 05, 01); + + CompleteCommand completeCommandByIndex = new CompleteByIndex(INDEX_FIRST_PERSON); + CompleteCommand completeCommandByDate = new CompleteByDate(validDate); + + //different commands + assertFalse(completeCommandByIndex.equals(completeCommandByDate)); + + //same objects + assertTrue(completeCommandByIndex.equals(completeCommandByIndex)); + assertTrue(completeCommandByIndex.equals(new CompleteByIndex(INDEX_FIRST_PERSON))); + + assertTrue(completeCommandByDate.equals(completeCommandByDate)); + assertTrue(completeCommandByDate.equals(new CompleteByDate(validDate))); + } +} diff --git a/src/test/java/seedu/address/logic/commands/ConfirmClearCommandTest.java b/src/test/java/seedu/address/logic/commands/ConfirmClearCommandTest.java new file mode 100644 index 00000000000..c171aa93092 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/ConfirmClearCommandTest.java @@ -0,0 +1,31 @@ +package seedu.address.logic.commands; + +import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; + +import org.junit.jupiter.api.Test; + +import seedu.address.model.AddressBook; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; + +public class ConfirmClearCommandTest { + @Test + public void execute_emptyAddressBook_success() { + Model model = new ModelManager(); + Model expectedModel = new ModelManager(); + + assertCommandSuccess(new ConfirmClearCommand(), model, ConfirmClearCommand.MESSAGE_SUCCESS, expectedModel); + } + + @Test + public void execute_nonEmptyAddressBook_success() { + Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + Model expectedModel = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + expectedModel.setAddressBook(new AddressBook()); + + assertCommandSuccess(new ConfirmClearCommand(), model, ConfirmClearCommand.MESSAGE_SUCCESS, expectedModel); + } +} + diff --git a/src/test/java/seedu/address/logic/commands/ConfirmOverrideCommandTest.java b/src/test/java/seedu/address/logic/commands/ConfirmOverrideCommandTest.java new file mode 100644 index 00000000000..eb9d1166e6c --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/ConfirmOverrideCommandTest.java @@ -0,0 +1,84 @@ +package seedu.address.logic.commands; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; + +import org.junit.jupiter.api.Test; + +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.appointment.Appointment; +import seedu.address.model.person.Person; + +public class ConfirmOverrideCommandTest { + + private static final String APPOINTMENT_DESCRIPTION_STUB = "Review Insurance, 01-01-2023 20:00"; + + @Test + public void execute_overridingAppointment_success() { + Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + Model expectedModel = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + + Person updatedPerson = expectedModel.getFilteredPersonList().get(1); + Person personToEdit = model.getFilteredPersonList().get(1); + Appointment newAppointment = Appointment.parseAppointmentDescription(APPOINTMENT_DESCRIPTION_STUB); + + Person newPerson = new Person(updatedPerson.getName(), updatedPerson.getPhone(), updatedPerson.getEmail(), + updatedPerson.getAddress(), updatedPerson.getNextOfKinName(), updatedPerson.getNextOfKinPhone(), + updatedPerson.getFinancialPlans(), updatedPerson.getTags(), newAppointment); + + expectedModel.setPerson(updatedPerson, newPerson); + + assertCommandSuccess(new ConfirmOverrideCommand(newAppointment, personToEdit), model, + ConfirmOverrideCommand.MESSAGE_SUCCESS, expectedModel); + } + + @Test + public void getMethodTest() { + Appointment newAppointment = Appointment.parseAppointmentDescription(APPOINTMENT_DESCRIPTION_STUB); + Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + Person personToEdit = model.getFilteredPersonList().get(1); + ConfirmOverrideCommand command = new ConfirmOverrideCommand(newAppointment, personToEdit); + + assertEquals(command.getAppointment(), newAppointment); + assertEquals(command.getPersonToEdit(), personToEdit); + } + + @Test + public void equals() { + Appointment newAppointment = Appointment.parseAppointmentDescription(APPOINTMENT_DESCRIPTION_STUB); + Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + Person personToEdit = model.getFilteredPersonList().get(1); + Person personToEdit2 = model.getFilteredPersonList().get(2); + ConfirmOverrideCommand confirmOverrideCommand = new ConfirmOverrideCommand(newAppointment, personToEdit); + + // same values -> returns true + assertTrue(confirmOverrideCommand.equals(new ConfirmOverrideCommand(newAppointment, personToEdit))); + + // same object -> returns true + assertTrue(confirmOverrideCommand.equals(confirmOverrideCommand)); + + // null -> returns false + assertFalse(confirmOverrideCommand.equals(null)); + + // different types -> returns false + assertFalse(confirmOverrideCommand.equals(0.5f)); + + // different person value -> returns false + assertFalse(confirmOverrideCommand.equals(new ConfirmOverrideCommand(newAppointment, personToEdit2))); + + } + + @Test + public void hashcode() { + CommandResult commandResult = new CommandResult("feedback"); + + // same values -> returns same hashcode + assertEquals(commandResult.hashCode(), new CommandResult("feedback").hashCode()); + + } +} diff --git a/src/test/java/seedu/address/logic/commands/EditPersonDescriptorTest.java b/src/test/java/seedu/address/logic/commands/EditPersonDescriptorTest.java index b17c1f3d5c2..459ece32580 100644 --- a/src/test/java/seedu/address/logic/commands/EditPersonDescriptorTest.java +++ b/src/test/java/seedu/address/logic/commands/EditPersonDescriptorTest.java @@ -64,7 +64,10 @@ public void toStringMethod() { + editPersonDescriptor.getName().orElse(null) + ", phone=" + editPersonDescriptor.getPhone().orElse(null) + ", email=" + editPersonDescriptor.getEmail().orElse(null) + ", address=" - + editPersonDescriptor.getAddress().orElse(null) + ", tags=" + + editPersonDescriptor.getAddress().orElse(null) + ", nextOfKinName=" + + editPersonDescriptor.getNextOfKinName().orElse(null) + ", nextOfKinPhone=" + + editPersonDescriptor.getNextOfKinPhone().orElse(null) + ", financialPlans=" + + editPersonDescriptor.getFinancialPlans().orElse(null) + ", tags=" + editPersonDescriptor.getTags().orElse(null) + "}"; assertEquals(expected, editPersonDescriptor.toString()); } diff --git a/src/test/java/seedu/address/logic/commands/FindCommandTest.java b/src/test/java/seedu/address/logic/commands/FindCommandTest.java index b8b7dbba91a..a9b4652f2bd 100644 --- a/src/test/java/seedu/address/logic/commands/FindCommandTest.java +++ b/src/test/java/seedu/address/logic/commands/FindCommandTest.java @@ -12,13 +12,19 @@ import java.util.Arrays; import java.util.Collections; +import java.util.List; +import java.util.function.Predicate; import org.junit.jupiter.api.Test; import seedu.address.model.Model; import seedu.address.model.ModelManager; import seedu.address.model.UserPrefs; -import seedu.address.model.person.NameContainsKeywordsPredicate; +import seedu.address.model.person.Person; +import seedu.address.model.person.predicates.CombinedPredicate; +import seedu.address.model.person.predicates.FinancialPlanContainsKeywordsPredicate; +import seedu.address.model.person.predicates.NameContainsKeywordsPredicate; +import seedu.address.model.person.predicates.TagContainsKeywordsPredicate; /** * Contains integration tests (interaction with the Model) for {@code FindCommand}. @@ -29,13 +35,19 @@ public class FindCommandTest { @Test public void equals() { - NameContainsKeywordsPredicate firstPredicate = - new NameContainsKeywordsPredicate(Collections.singletonList("first")); - NameContainsKeywordsPredicate secondPredicate = - new NameContainsKeywordsPredicate(Collections.singletonList("second")); + Predicate firstPredicate = + new NameContainsKeywordsPredicate(Collections.singletonList("first")) + .or(new TagContainsKeywordsPredicate(Collections.singletonList("firstTag"))); + Predicate secondPredicate = + new NameContainsKeywordsPredicate(Collections.singletonList("second")) + .or(new TagContainsKeywordsPredicate(Collections.singletonList("firstTag"))); + Predicate firstPredicateAgain = + new TagContainsKeywordsPredicate(Collections.singletonList("firstTag")) + .or(new NameContainsKeywordsPredicate(Collections.singletonList("first"))); FindCommand findFirstCommand = new FindCommand(firstPredicate); FindCommand findSecondCommand = new FindCommand(secondPredicate); + FindCommand findFirstCommandAgain = new FindCommand(firstPredicateAgain); // same object -> returns true assertTrue(findFirstCommand.equals(findFirstCommand)); @@ -44,6 +56,9 @@ public void equals() { FindCommand findFirstCommandCopy = new FindCommand(firstPredicate); assertTrue(findFirstCommand.equals(findFirstCommandCopy)); + // same values, composed differently -> returns true + assertFalse(findFirstCommand.equals(findFirstCommandAgain)); + // different types -> returns false assertFalse(findFirstCommand.equals(1)); @@ -57,17 +72,21 @@ public void equals() { @Test public void execute_zeroKeywords_noPersonFound() { String expectedMessage = String.format(MESSAGE_PERSONS_LISTED_OVERVIEW, 0); - NameContainsKeywordsPredicate predicate = preparePredicate(" "); - FindCommand command = new FindCommand(predicate); - expectedModel.updateFilteredPersonList(predicate); + NameContainsKeywordsPredicate namePredicate = new NameContainsKeywordsPredicate(List.of()); + FinancialPlanContainsKeywordsPredicate financialPlanPredicate = + new FinancialPlanContainsKeywordsPredicate(List.of()); + TagContainsKeywordsPredicate tagPredicate = new TagContainsKeywordsPredicate(List.of()); + CombinedPredicate combinedPredicate = new CombinedPredicate(financialPlanPredicate, + namePredicate, tagPredicate); + FindCommand command = new FindCommand(combinedPredicate); + expectedModel.updateFilteredPersonList(combinedPredicate); assertCommandSuccess(command, model, expectedMessage, expectedModel); assertEquals(Collections.emptyList(), model.getFilteredPersonList()); } - @Test public void execute_multipleKeywords_multiplePersonsFound() { String expectedMessage = String.format(MESSAGE_PERSONS_LISTED_OVERVIEW, 3); - NameContainsKeywordsPredicate predicate = preparePredicate("Kurz Elle Kunz"); + NameContainsKeywordsPredicate predicate = prepareNamePredicate("Kurz Elle Kunz"); FindCommand command = new FindCommand(predicate); expectedModel.updateFilteredPersonList(predicate); assertCommandSuccess(command, model, expectedMessage, expectedModel); @@ -76,7 +95,8 @@ public void execute_multipleKeywords_multiplePersonsFound() { @Test public void toStringMethod() { - NameContainsKeywordsPredicate predicate = new NameContainsKeywordsPredicate(Arrays.asList("keyword")); + Predicate predicate = new NameContainsKeywordsPredicate(Arrays.asList("keyword")) + .or(new TagContainsKeywordsPredicate(Collections.singletonList("tag"))); FindCommand findCommand = new FindCommand(predicate); String expected = FindCommand.class.getCanonicalName() + "{predicate=" + predicate + "}"; assertEquals(expected, findCommand.toString()); @@ -85,7 +105,7 @@ public void toStringMethod() { /** * Parses {@code userInput} into a {@code NameContainsKeywordsPredicate}. */ - private NameContainsKeywordsPredicate preparePredicate(String userInput) { + private NameContainsKeywordsPredicate prepareNamePredicate(String userInput) { return new NameContainsKeywordsPredicate(Arrays.asList(userInput.split("\\s+"))); } } diff --git a/src/test/java/seedu/address/logic/commands/GatherCommandTest.java b/src/test/java/seedu/address/logic/commands/GatherCommandTest.java new file mode 100644 index 00000000000..1662daf9796 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/GatherCommandTest.java @@ -0,0 +1,94 @@ +package seedu.address.logic.commands; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.logic.commands.GatherCommand.MESSAGE_NO_PERSON_FOUND; +import static seedu.address.testutil.TypicalPersons.ALICE; +import static seedu.address.testutil.TypicalPersons.BENSON; +import static seedu.address.testutil.TypicalPersons.DANIEL; +import static seedu.address.testutil.TypicalPersons.GEORGE; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; + +import org.junit.jupiter.api.Test; + +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.person.gatheremail.GatherEmailByFinancialPlan; +import seedu.address.model.person.gatheremail.GatherEmailByTag; +import seedu.address.model.person.gatheremail.GatherEmailPrompt; + +/** + * Contains integration tests (interaction with the Model) for {@code GatherCommand}. + */ +public class GatherCommandTest { + private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + private Model expectedModel = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + + @Test + void execute_gatherEmailsByFinancialPlan_success() { + String message = ALICE.getEmail() + "; " + BENSON.getEmail() + "; " + GEORGE.getEmail() + ";"; + GatherEmailByFinancialPlan prompt = new GatherEmailByFinancialPlan("Sample Financial Plan 1"); + String expectedMessage = expectedModel.gatherEmails(prompt); + GatherCommand command = new GatherCommand(prompt); + assertCommandSuccess(command, model, expectedMessage, expectedModel); + assertCommandSuccess(command, model, message, expectedModel); + } + + @Test + void execute_gatherEmailsByTag_success() { + String message = ALICE.getEmail() + "; " + BENSON.getEmail() + "; " + DANIEL.getEmail() + ";"; + GatherEmailByTag prompt = new GatherEmailByTag("friends"); + String expectedMessage = expectedModel.gatherEmails(prompt); + GatherCommand command = new GatherCommand(prompt); + assertCommandSuccess(command, model, expectedMessage, expectedModel); + assertCommandSuccess(command, model, message, expectedModel); + } + + @Test + public void execute_noPersonFound() { + GatherEmailByFinancialPlan financialPlanPrompt = new GatherEmailByFinancialPlan("Sample Plan 3"); + GatherEmailByTag tagPrompt = new GatherEmailByTag("Sample Tag"); + String fpExpectedMsg = String.format(MESSAGE_NO_PERSON_FOUND + financialPlanPrompt); + String tagExpectedMsg = String.format(MESSAGE_NO_PERSON_FOUND + tagPrompt); + GatherCommand financialPlanCommand = new GatherCommand(financialPlanPrompt); + GatherCommand tagCommand = new GatherCommand(tagPrompt); + + assertCommandSuccess(financialPlanCommand, model, fpExpectedMsg, expectedModel); + assertCommandSuccess(tagCommand, model, tagExpectedMsg, expectedModel); + } + + @Test + void testEquals() { + GatherEmailPrompt first = new GatherEmailByFinancialPlan("first"); + GatherEmailPrompt second = new GatherEmailByFinancialPlan("second"); + GatherCommand gatherFirstCommand = new GatherCommand(first); + GatherCommand gatherSecondCommand = new GatherCommand(second); + + // same object -> returns true + assertTrue(gatherFirstCommand.equals(gatherFirstCommand)); + + // same values -> returns true + GatherCommand gatherFirstCommandCopy = new GatherCommand(first); + assertTrue(gatherFirstCommand.equals(gatherFirstCommandCopy)); + + // different types -> returns false + assertFalse(gatherFirstCommand.equals(1)); + + // null -> returns false + assertFalse(gatherFirstCommand.equals(null)); + + // different person -> returns false + assertFalse(gatherFirstCommand.equals(gatherSecondCommand)); + } + + @Test + void testToString() { + GatherEmailByFinancialPlan prompt = new GatherEmailByFinancialPlan("prompt"); + GatherCommand gatherCommand = new GatherCommand(prompt); + String expected = GatherCommand.class.getCanonicalName() + "{prompt=" + prompt + "}"; + assertEquals(expected, gatherCommand.toString()); + } +} diff --git a/src/test/java/seedu/address/logic/commands/ScheduleCommandTest.java b/src/test/java/seedu/address/logic/commands/ScheduleCommandTest.java new file mode 100644 index 00000000000..c23a4e92cbf --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/ScheduleCommandTest.java @@ -0,0 +1,110 @@ +package seedu.address.logic.commands; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandFailure; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.logic.commands.CommandTestUtil.showPersonAtIndex; +import static seedu.address.testutil.Assert.assertThrows; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; +import static seedu.address.testutil.TypicalIndexes.INDEX_SECOND_PERSON; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; + +import java.time.LocalDateTime; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.Messages; +import seedu.address.model.AddressBook; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.appointment.Appointment; +import seedu.address.model.person.Person; +import seedu.address.testutil.PersonBuilder; + +class ScheduleCommandTest { + private static final String APPOINTMENT_DESCRIPTION_STUB = "Review Insurance, 01-01-2023 20:00"; + private static final Appointment APPOINTMENT_STUB = Appointment.parseAppointmentDescription( + APPOINTMENT_DESCRIPTION_STUB); + private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + + @Test + public void constructor_nullParams_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> new ScheduleCommand(null, APPOINTMENT_STUB)); + assertThrows(NullPointerException.class, () -> new ScheduleCommand(INDEX_FIRST_PERSON, null)); + } + @Test + public void execute_scheduleAccepted_scheduleSuccessful() { + Person personWithoutSchedule = model.getFilteredPersonList().get(0); + + ScheduleCommand scheduleCommand = new ScheduleCommand(INDEX_FIRST_PERSON, APPOINTMENT_STUB); + Person personWithSchedule = new PersonBuilder(personWithoutSchedule) + .withAppointment(APPOINTMENT_DESCRIPTION_STUB).build(); + + String expectedMessage = String.format(ScheduleCommand.MESSAGE_SCHEDULE_SUCCESS, + Messages.format(personWithSchedule)); + + Model expectedModel = new ModelManager(new AddressBook(model.getAddressBook()), new UserPrefs()); + expectedModel.setPerson(model.getFilteredPersonList().get(0), personWithSchedule); + + assertCommandSuccess(scheduleCommand, model, expectedMessage, expectedModel); + + //checks if the person is associated with appointment + assertTrue(personWithSchedule.getAppointment().equals(APPOINTMENT_STUB)); + } + + @Test + public void execute_invalidPersonIndexUnfilteredList_failure() { + Index outOfBoundIndex = Index.fromOneBased(model.getFilteredPersonList().size() + 1); + ScheduleCommand scheduleCommand = new ScheduleCommand(outOfBoundIndex, APPOINTMENT_STUB); + + assertCommandFailure(scheduleCommand, model, Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + @Test + public void execute_invalidPersonIndexFilteredList_failure() { + showPersonAtIndex(model, INDEX_FIRST_PERSON); + Index outOfBoundIndex = INDEX_SECOND_PERSON; + // ensures that outOfBoundIndex is still in bounds of address book list + assertTrue(outOfBoundIndex.getZeroBased() < model.getAddressBook().getPersonList().size()); + + ScheduleCommand scheduleCommand = new ScheduleCommand(outOfBoundIndex, APPOINTMENT_STUB); + + assertCommandFailure(scheduleCommand, model, Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + @Test + public void equals() { + final ScheduleCommand standardCommand = new ScheduleCommand(INDEX_FIRST_PERSON, APPOINTMENT_STUB); + + // same values -> returns true + ScheduleCommand commandWithSameValues = new ScheduleCommand(INDEX_FIRST_PERSON, APPOINTMENT_STUB); + assertTrue(standardCommand.equals(commandWithSameValues)); + + // same object -> returns true + assertTrue(standardCommand.equals(standardCommand)); + + // null -> returns false + assertFalse(standardCommand.equals(null)); + + // different types -> returns false + assertFalse(standardCommand.equals(new ClearCommand())); + + // different index -> returns false + assertFalse(standardCommand.equals(new ScheduleCommand(INDEX_SECOND_PERSON, APPOINTMENT_STUB))); + + // different appointment -> returns false + ScheduleCommand commandWithDiffDateTime = new ScheduleCommand(INDEX_FIRST_PERSON, + new Appointment("Review Insurance", + LocalDateTime.of(2023, 01, 01, 20, 30))); + + ScheduleCommand commandWithDiffValue = new ScheduleCommand(INDEX_FIRST_PERSON, + new Appointment("Buy Insurance", + LocalDateTime.of(2023, 01, 01, 20, 0))); + + assertFalse(standardCommand.equals(commandWithDiffDateTime)); + assertFalse(standardCommand.equals(commandWithDiffValue)); + } +} diff --git a/src/test/java/seedu/address/logic/commands/SortCommandTest.java b/src/test/java/seedu/address/logic/commands/SortCommandTest.java new file mode 100644 index 00000000000..78203575196 --- /dev/null +++ b/src/test/java/seedu/address/logic/commands/SortCommandTest.java @@ -0,0 +1,85 @@ +package seedu.address.logic.commands; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.Messages.MESSAGE_PERSONS_LISTED_OVERVIEW; +import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; +import static seedu.address.testutil.TypicalPersons.ALICE; +import static seedu.address.testutil.TypicalPersons.BENSON; +import static seedu.address.testutil.TypicalPersons.CARL; +import static seedu.address.testutil.TypicalPersons.DANIEL; +import static seedu.address.testutil.TypicalPersons.ELLE; +import static seedu.address.testutil.TypicalPersons.FIONA; +import static seedu.address.testutil.TypicalPersons.GEORGE; +import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; + +import java.util.Arrays; + +import org.junit.jupiter.api.Test; + +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.UserPrefs; +import seedu.address.model.person.comparator.SortByAppointmentComparator; +import seedu.address.model.person.comparator.SortByNameComparator; + +public class SortCommandTest { + + private Model model = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + private Model expectedModel = new ModelManager(getTypicalAddressBook(), new UserPrefs()); + + @Test + public void execute_sortSortCommand() { + String expectedMessage = String.format(MESSAGE_PERSONS_LISTED_OVERVIEW, 7); + SortByNameComparator comparator = new SortByNameComparator(); + SortCommand command = new SortCommand(comparator); + expectedModel.sortFilteredPersonList(comparator); + assertCommandSuccess(command, model, expectedMessage, expectedModel); + assertEquals(model.getAppointmentList(), expectedModel.getAppointmentList()); + assertEquals(Arrays.asList(ALICE, BENSON, CARL, DANIEL, ELLE, FIONA, GEORGE), model.getFilteredPersonList()); + } + + @Test + public void equals() { + SortByNameComparator nameComparator = new SortByNameComparator(); + SortByAppointmentComparator appointmentComparator = new SortByAppointmentComparator(); + + SortCommand sortNameCommand = new SortCommand(nameComparator); + SortCommand appointmentSortCommand = new SortCommand(appointmentComparator); + + // same object -> returns true + assertTrue(sortNameCommand.equals(sortNameCommand)); + assertTrue(appointmentSortCommand.equals(appointmentSortCommand)); + + // different types -> returns false + assertFalse(sortNameCommand.equals(1)); + + // null -> returns false + assertFalse(sortNameCommand.equals(null)); + + // different command -> returns false + assertFalse(sortNameCommand.equals(appointmentSortCommand)); + assertFalse(appointmentSortCommand.equals(sortNameCommand)); + } + + @Test + public void toStringMethod() { + SortByNameComparator comparator = new SortByNameComparator(); + SortCommand sortCommand = new SortCommand(comparator); + String expected = SortCommand.class.getCanonicalName() + "{comparator=" + comparator + "}"; + assertEquals(expected, sortCommand.toString()); + } + + @Test + public void execute_sortAppointmentCommand() { + String expectedMessage = String.format(MESSAGE_PERSONS_LISTED_OVERVIEW, 7); + SortByAppointmentComparator comparator = new SortByAppointmentComparator(); + SortCommand command = new SortCommand(comparator); + expectedModel.sortFilteredPersonList(comparator); + assertCommandSuccess(command, model, expectedMessage, expectedModel); + assertEquals(model.getAppointmentList(), expectedModel.getAppointmentList()); + assertEquals(Arrays.asList(CARL, BENSON, ALICE, DANIEL, ELLE, FIONA, GEORGE), model.getFilteredPersonList()); + } + +} diff --git a/src/test/java/seedu/address/logic/parser/AddCommandParserTest.java b/src/test/java/seedu/address/logic/parser/AddCommandParserTest.java index 5bc11d3cdaa..e1bc5f33c8a 100644 --- a/src/test/java/seedu/address/logic/parser/AddCommandParserTest.java +++ b/src/test/java/seedu/address/logic/parser/AddCommandParserTest.java @@ -5,13 +5,21 @@ import static seedu.address.logic.commands.CommandTestUtil.ADDRESS_DESC_BOB; import static seedu.address.logic.commands.CommandTestUtil.EMAIL_DESC_AMY; import static seedu.address.logic.commands.CommandTestUtil.EMAIL_DESC_BOB; +import static seedu.address.logic.commands.CommandTestUtil.FINANCIAL_PLAN_DESC_1; +import static seedu.address.logic.commands.CommandTestUtil.FINANCIAL_PLAN_DESC_2; import static seedu.address.logic.commands.CommandTestUtil.INVALID_ADDRESS_DESC; import static seedu.address.logic.commands.CommandTestUtil.INVALID_EMAIL_DESC; import static seedu.address.logic.commands.CommandTestUtil.INVALID_NAME_DESC; +import static seedu.address.logic.commands.CommandTestUtil.INVALID_NEXT_OF_KIN_NAME_DESC; +import static seedu.address.logic.commands.CommandTestUtil.INVALID_NEXT_OF_KIN_PHONE_DESC; import static seedu.address.logic.commands.CommandTestUtil.INVALID_PHONE_DESC; import static seedu.address.logic.commands.CommandTestUtil.INVALID_TAG_DESC; import static seedu.address.logic.commands.CommandTestUtil.NAME_DESC_AMY; import static seedu.address.logic.commands.CommandTestUtil.NAME_DESC_BOB; +import static seedu.address.logic.commands.CommandTestUtil.NEXT_OF_KIN_NAME_DESC_AMY; +import static seedu.address.logic.commands.CommandTestUtil.NEXT_OF_KIN_NAME_DESC_BOB; +import static seedu.address.logic.commands.CommandTestUtil.NEXT_OF_KIN_PHONE_DESC_AMY; +import static seedu.address.logic.commands.CommandTestUtil.NEXT_OF_KIN_PHONE_DESC_BOB; import static seedu.address.logic.commands.CommandTestUtil.PHONE_DESC_AMY; import static seedu.address.logic.commands.CommandTestUtil.PHONE_DESC_BOB; import static seedu.address.logic.commands.CommandTestUtil.PREAMBLE_NON_EMPTY; @@ -20,13 +28,19 @@ import static seedu.address.logic.commands.CommandTestUtil.TAG_DESC_HUSBAND; import static seedu.address.logic.commands.CommandTestUtil.VALID_ADDRESS_BOB; import static seedu.address.logic.commands.CommandTestUtil.VALID_EMAIL_BOB; +import static seedu.address.logic.commands.CommandTestUtil.VALID_FINANCIAL_PLAN_1; +import static seedu.address.logic.commands.CommandTestUtil.VALID_FINANCIAL_PLAN_2; import static seedu.address.logic.commands.CommandTestUtil.VALID_NAME_BOB; +import static seedu.address.logic.commands.CommandTestUtil.VALID_NEXT_OF_KIN_NAME_BOB; +import static seedu.address.logic.commands.CommandTestUtil.VALID_NEXT_OF_KIN_PHONE_BOB; import static seedu.address.logic.commands.CommandTestUtil.VALID_PHONE_BOB; import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_FRIEND; import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_HUSBAND; import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NEXT_OF_KIN_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NEXT_OF_KIN_PHONE; import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; @@ -40,6 +54,8 @@ import seedu.address.model.person.Address; import seedu.address.model.person.Email; import seedu.address.model.person.Name; +import seedu.address.model.person.NextOfKinName; +import seedu.address.model.person.NextOfKinPhone; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; import seedu.address.model.tag.Tag; @@ -50,25 +66,30 @@ public class AddCommandParserTest { @Test public void parse_allFieldsPresent_success() { - Person expectedPerson = new PersonBuilder(BOB).withTags(VALID_TAG_FRIEND).build(); + Person expectedPerson = new PersonBuilder(BOB).withTags(VALID_TAG_FRIEND) + .withFinancialPlans(VALID_FINANCIAL_PLAN_1, VALID_FINANCIAL_PLAN_2).build(); // whitespace only preamble assertParseSuccess(parser, PREAMBLE_WHITESPACE + NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB - + ADDRESS_DESC_BOB + TAG_DESC_FRIEND, new AddCommand(expectedPerson)); + + ADDRESS_DESC_BOB + NEXT_OF_KIN_NAME_DESC_BOB + NEXT_OF_KIN_PHONE_DESC_BOB + FINANCIAL_PLAN_DESC_1 + + FINANCIAL_PLAN_DESC_2 + TAG_DESC_FRIEND, + new AddCommand(expectedPerson)); // multiple tags - all accepted Person expectedPersonMultipleTags = new PersonBuilder(BOB).withTags(VALID_TAG_FRIEND, VALID_TAG_HUSBAND) - .build(); + .withFinancialPlans(VALID_FINANCIAL_PLAN_1, VALID_FINANCIAL_PLAN_2).build(); assertParseSuccess(parser, - NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + ADDRESS_DESC_BOB + TAG_DESC_HUSBAND + TAG_DESC_FRIEND, + NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + ADDRESS_DESC_BOB + NEXT_OF_KIN_NAME_DESC_BOB + + NEXT_OF_KIN_PHONE_DESC_BOB + TAG_DESC_HUSBAND + FINANCIAL_PLAN_DESC_1 + FINANCIAL_PLAN_DESC_2 + + TAG_DESC_FRIEND, new AddCommand(expectedPersonMultipleTags)); } @Test public void parse_repeatedNonTagValue_failure() { String validExpectedPersonString = NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB - + ADDRESS_DESC_BOB + TAG_DESC_FRIEND; + + ADDRESS_DESC_BOB + NEXT_OF_KIN_NAME_DESC_BOB + NEXT_OF_KIN_PHONE_DESC_BOB + TAG_DESC_FRIEND; // multiple names assertParseFailure(parser, NAME_DESC_AMY + validExpectedPersonString, @@ -85,12 +106,20 @@ public void parse_repeatedNonTagValue_failure() { // multiple addresses assertParseFailure(parser, ADDRESS_DESC_AMY + validExpectedPersonString, Messages.getErrorMessageForDuplicatePrefixes(PREFIX_ADDRESS)); + // multiple nextOfKinNames + assertParseFailure(parser, NEXT_OF_KIN_NAME_DESC_AMY + validExpectedPersonString, + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_NEXT_OF_KIN_NAME)); + // multiple nextOfKinPhones + assertParseFailure(parser, NEXT_OF_KIN_PHONE_DESC_AMY + validExpectedPersonString, + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_NEXT_OF_KIN_PHONE)); // multiple fields repeated assertParseFailure(parser, validExpectedPersonString + PHONE_DESC_AMY + EMAIL_DESC_AMY + NAME_DESC_AMY + ADDRESS_DESC_AMY + + NEXT_OF_KIN_NAME_DESC_AMY + NEXT_OF_KIN_PHONE_DESC_AMY + validExpectedPersonString, - Messages.getErrorMessageForDuplicatePrefixes(PREFIX_NAME, PREFIX_ADDRESS, PREFIX_EMAIL, PREFIX_PHONE)); + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_NAME, PREFIX_ADDRESS, PREFIX_EMAIL, PREFIX_PHONE, + PREFIX_NEXT_OF_KIN_NAME, PREFIX_NEXT_OF_KIN_PHONE)); // invalid value followed by valid value @@ -109,7 +138,12 @@ public void parse_repeatedNonTagValue_failure() { // invalid address assertParseFailure(parser, INVALID_ADDRESS_DESC + validExpectedPersonString, Messages.getErrorMessageForDuplicatePrefixes(PREFIX_ADDRESS)); - + // invalid nextOfKinName + assertParseFailure(parser, INVALID_NEXT_OF_KIN_NAME_DESC + validExpectedPersonString, + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_NEXT_OF_KIN_NAME)); + // invalid nextOfKinPhone + assertParseFailure(parser, INVALID_NEXT_OF_KIN_PHONE_DESC + validExpectedPersonString, + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_NEXT_OF_KIN_PHONE)); // valid value followed by invalid value // invalid name @@ -127,13 +161,21 @@ public void parse_repeatedNonTagValue_failure() { // invalid address assertParseFailure(parser, validExpectedPersonString + INVALID_ADDRESS_DESC, Messages.getErrorMessageForDuplicatePrefixes(PREFIX_ADDRESS)); + // invalid nextOfKinName + assertParseFailure(parser, validExpectedPersonString + INVALID_NEXT_OF_KIN_NAME_DESC, + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_NEXT_OF_KIN_NAME)); + + // invalid nextOfKinPhone + assertParseFailure(parser, validExpectedPersonString + INVALID_NEXT_OF_KIN_PHONE_DESC, + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_NEXT_OF_KIN_PHONE)); } @Test public void parse_optionalFieldsMissing_success() { // zero tags Person expectedPerson = new PersonBuilder(AMY).withTags().build(); - assertParseSuccess(parser, NAME_DESC_AMY + PHONE_DESC_AMY + EMAIL_DESC_AMY + ADDRESS_DESC_AMY, + assertParseSuccess(parser, NAME_DESC_AMY + PHONE_DESC_AMY + EMAIL_DESC_AMY + ADDRESS_DESC_AMY + + NEXT_OF_KIN_NAME_DESC_AMY + FINANCIAL_PLAN_DESC_1 + NEXT_OF_KIN_PHONE_DESC_AMY, new AddCommand(expectedPerson)); } @@ -142,23 +184,37 @@ public void parse_compulsoryFieldMissing_failure() { String expectedMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE); // missing name prefix - assertParseFailure(parser, VALID_NAME_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + ADDRESS_DESC_BOB, + assertParseFailure(parser, VALID_NAME_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + ADDRESS_DESC_BOB + + NEXT_OF_KIN_NAME_DESC_BOB + NEXT_OF_KIN_PHONE_DESC_BOB, expectedMessage); // missing phone prefix - assertParseFailure(parser, NAME_DESC_BOB + VALID_PHONE_BOB + EMAIL_DESC_BOB + ADDRESS_DESC_BOB, + assertParseFailure(parser, NAME_DESC_BOB + VALID_PHONE_BOB + EMAIL_DESC_BOB + ADDRESS_DESC_BOB + + NEXT_OF_KIN_NAME_DESC_BOB + NEXT_OF_KIN_PHONE_DESC_BOB, expectedMessage); // missing email prefix - assertParseFailure(parser, NAME_DESC_BOB + PHONE_DESC_BOB + VALID_EMAIL_BOB + ADDRESS_DESC_BOB, + assertParseFailure(parser, NAME_DESC_BOB + PHONE_DESC_BOB + VALID_EMAIL_BOB + ADDRESS_DESC_BOB + + NEXT_OF_KIN_NAME_DESC_BOB + NEXT_OF_KIN_PHONE_DESC_BOB, expectedMessage); // missing address prefix - assertParseFailure(parser, NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + VALID_ADDRESS_BOB, + assertParseFailure(parser, NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + VALID_ADDRESS_BOB + + NEXT_OF_KIN_NAME_DESC_BOB + NEXT_OF_KIN_PHONE_DESC_BOB, + expectedMessage); + // missing nextOfKinName prefix + assertParseFailure(parser, NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + ADDRESS_DESC_BOB + + VALID_NEXT_OF_KIN_NAME_BOB + NEXT_OF_KIN_PHONE_DESC_BOB, + expectedMessage); + + // missing nextOfKinPhone prefix + assertParseFailure(parser, NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + ADDRESS_DESC_BOB + + NEXT_OF_KIN_NAME_DESC_BOB + VALID_NEXT_OF_KIN_PHONE_BOB, expectedMessage); // all prefixes missing - assertParseFailure(parser, VALID_NAME_BOB + VALID_PHONE_BOB + VALID_EMAIL_BOB + VALID_ADDRESS_BOB, + assertParseFailure(parser, VALID_NAME_BOB + VALID_PHONE_BOB + VALID_EMAIL_BOB + VALID_ADDRESS_BOB + + VALID_NEXT_OF_KIN_NAME_BOB + VALID_NEXT_OF_KIN_PHONE_BOB, expectedMessage); } @@ -166,31 +222,48 @@ public void parse_compulsoryFieldMissing_failure() { public void parse_invalidValue_failure() { // invalid name assertParseFailure(parser, INVALID_NAME_DESC + PHONE_DESC_BOB + EMAIL_DESC_BOB + ADDRESS_DESC_BOB + + NEXT_OF_KIN_NAME_DESC_BOB + NEXT_OF_KIN_PHONE_DESC_BOB + TAG_DESC_HUSBAND + TAG_DESC_FRIEND, Name.MESSAGE_CONSTRAINTS); // invalid phone assertParseFailure(parser, NAME_DESC_BOB + INVALID_PHONE_DESC + EMAIL_DESC_BOB + ADDRESS_DESC_BOB + + NEXT_OF_KIN_NAME_DESC_BOB + NEXT_OF_KIN_PHONE_DESC_BOB + TAG_DESC_HUSBAND + TAG_DESC_FRIEND, Phone.MESSAGE_CONSTRAINTS); // invalid email assertParseFailure(parser, NAME_DESC_BOB + PHONE_DESC_BOB + INVALID_EMAIL_DESC + ADDRESS_DESC_BOB + + NEXT_OF_KIN_NAME_DESC_BOB + NEXT_OF_KIN_PHONE_DESC_BOB + TAG_DESC_HUSBAND + TAG_DESC_FRIEND, Email.MESSAGE_CONSTRAINTS); // invalid address assertParseFailure(parser, NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + INVALID_ADDRESS_DESC + + NEXT_OF_KIN_NAME_DESC_BOB + NEXT_OF_KIN_PHONE_DESC_BOB + TAG_DESC_HUSBAND + TAG_DESC_FRIEND, Address.MESSAGE_CONSTRAINTS); + // invalid nextOfKinName + assertParseFailure(parser, NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + ADDRESS_DESC_BOB + + INVALID_NEXT_OF_KIN_NAME_DESC + NEXT_OF_KIN_PHONE_DESC_BOB + + TAG_DESC_HUSBAND + TAG_DESC_FRIEND, NextOfKinName.MESSAGE_CONSTRAINTS); + + // invalid nextOfKinPhone + assertParseFailure(parser, NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + ADDRESS_DESC_BOB + + NEXT_OF_KIN_NAME_DESC_BOB + INVALID_NEXT_OF_KIN_PHONE_DESC + + TAG_DESC_HUSBAND + TAG_DESC_FRIEND, NextOfKinPhone.MESSAGE_CONSTRAINTS); + // invalid tag assertParseFailure(parser, NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB + ADDRESS_DESC_BOB + + NEXT_OF_KIN_NAME_DESC_BOB + NEXT_OF_KIN_PHONE_DESC_BOB + FINANCIAL_PLAN_DESC_1 + INVALID_TAG_DESC + VALID_TAG_FRIEND, Tag.MESSAGE_CONSTRAINTS); // two invalid values, only first invalid value reported - assertParseFailure(parser, INVALID_NAME_DESC + PHONE_DESC_BOB + EMAIL_DESC_BOB + INVALID_ADDRESS_DESC, + assertParseFailure(parser, INVALID_NAME_DESC + PHONE_DESC_BOB + EMAIL_DESC_BOB + INVALID_ADDRESS_DESC + + NEXT_OF_KIN_NAME_DESC_BOB + NEXT_OF_KIN_PHONE_DESC_BOB, Name.MESSAGE_CONSTRAINTS); // non-empty preamble assertParseFailure(parser, PREAMBLE_NON_EMPTY + NAME_DESC_BOB + PHONE_DESC_BOB + EMAIL_DESC_BOB - + ADDRESS_DESC_BOB + TAG_DESC_HUSBAND + TAG_DESC_FRIEND, + + ADDRESS_DESC_BOB + NEXT_OF_KIN_NAME_DESC_BOB + NEXT_OF_KIN_PHONE_DESC_BOB + TAG_DESC_HUSBAND + + TAG_DESC_FRIEND, String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE)); } } diff --git a/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java b/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java index 5a1ab3dbc0c..cdcdec699a7 100644 --- a/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java +++ b/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java @@ -4,9 +4,20 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; import static seedu.address.logic.Messages.MESSAGE_UNKNOWN_COMMAND; +import static seedu.address.logic.commands.CommandTestUtil.APPOINTMENT_DATE_DESC; +import static seedu.address.logic.commands.CommandTestUtil.APPOINTMENT_NAME_DESC; +import static seedu.address.logic.commands.CommandTestUtil.FINANCIAL_PLAN_DESC_1; +import static seedu.address.logic.commands.CommandTestUtil.TAG_DESC_HUSBAND; +import static seedu.address.logic.commands.CommandTestUtil.VALID_APPOINTMENT_NAME; +import static seedu.address.logic.commands.CommandTestUtil.VALID_FINANCIAL_PLAN_1; +import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_HUSBAND; +import static seedu.address.logic.parser.CliSyntax.PREFIX_APPOINTMENT_DATE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; import static seedu.address.testutil.Assert.assertThrows; import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; +import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; @@ -15,16 +26,28 @@ import seedu.address.logic.commands.AddCommand; import seedu.address.logic.commands.ClearCommand; +import seedu.address.logic.commands.CompleteByDate; +import seedu.address.logic.commands.CompleteByIndex; +import seedu.address.logic.commands.CompleteCommand; import seedu.address.logic.commands.DeleteCommand; import seedu.address.logic.commands.EditCommand; import seedu.address.logic.commands.EditCommand.EditPersonDescriptor; import seedu.address.logic.commands.ExitCommand; import seedu.address.logic.commands.FindCommand; +import seedu.address.logic.commands.GatherCommand; import seedu.address.logic.commands.HelpCommand; import seedu.address.logic.commands.ListCommand; +import seedu.address.logic.commands.ScheduleCommand; +import seedu.address.logic.commands.SortCommand; import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.person.NameContainsKeywordsPredicate; +import seedu.address.model.appointment.Appointment; import seedu.address.model.person.Person; +import seedu.address.model.person.gatheremail.GatherEmailByFinancialPlan; +import seedu.address.model.person.gatheremail.GatherEmailByTag; +import seedu.address.model.person.predicates.CombinedPredicate; +import seedu.address.model.person.predicates.FinancialPlanContainsKeywordsPredicate; +import seedu.address.model.person.predicates.NameContainsKeywordsPredicate; +import seedu.address.model.person.predicates.TagContainsKeywordsPredicate; import seedu.address.testutil.EditPersonDescriptorBuilder; import seedu.address.testutil.PersonBuilder; import seedu.address.testutil.PersonUtil; @@ -57,6 +80,8 @@ public void parseCommand_delete() throws Exception { public void parseCommand_edit() throws Exception { Person person = new PersonBuilder().build(); EditPersonDescriptor descriptor = new EditPersonDescriptorBuilder(person).build(); + System.out.println(descriptor); + System.out.println(PersonUtil.getEditPersonDescriptorDetails(descriptor)); EditCommand command = (EditCommand) parser.parseCommand(EditCommand.COMMAND_WORD + " " + INDEX_FIRST_PERSON.getOneBased() + " " + PersonUtil.getEditPersonDescriptorDetails(descriptor)); assertEquals(new EditCommand(INDEX_FIRST_PERSON, descriptor), command); @@ -72,8 +97,57 @@ public void parseCommand_exit() throws Exception { public void parseCommand_find() throws Exception { List keywords = Arrays.asList("foo", "bar", "baz"); FindCommand command = (FindCommand) parser.parseCommand( - FindCommand.COMMAND_WORD + " " + keywords.stream().collect(Collectors.joining(" "))); - assertEquals(new FindCommand(new NameContainsKeywordsPredicate(keywords)), command); + FindCommand.COMMAND_WORD + " " + + keywords.stream().map(name -> PREFIX_NAME + name).collect(Collectors.joining(" "))); + assertEquals( + new FindCommand( + new CombinedPredicate( + new FinancialPlanContainsKeywordsPredicate(List.of()), + new NameContainsKeywordsPredicate(keywords), + new TagContainsKeywordsPredicate(List.of()))), + command); + } + + @Test + public void parseCommand_gather() throws Exception { + // financial plan + GatherEmailByFinancialPlan fpPrompt = new GatherEmailByFinancialPlan(VALID_FINANCIAL_PLAN_1); + GatherCommand fpCommand = (GatherCommand) parser.parseCommand( + GatherCommand.COMMAND_WORD + FINANCIAL_PLAN_DESC_1); + assertEquals(new GatherCommand(fpPrompt), fpCommand); + + // tag + GatherEmailByTag tagPrompt = new GatherEmailByTag(VALID_TAG_HUSBAND); + GatherCommand tagCommand = (GatherCommand) parser.parseCommand( + GatherCommand.COMMAND_WORD + TAG_DESC_HUSBAND); + assertEquals(new GatherCommand(tagPrompt), tagCommand); + } + + @Test + public void parseCommand_schedule() throws Exception { + ScheduleCommand command = (ScheduleCommand) parser.parseCommand( + ScheduleCommand.COMMAND_WORD + " " + INDEX_FIRST_PERSON.getOneBased() + + APPOINTMENT_NAME_DESC + APPOINTMENT_DATE_DESC); + assertEquals(new ScheduleCommand(INDEX_FIRST_PERSON, new Appointment(VALID_APPOINTMENT_NAME, + LocalDateTime.of(2023, 1, 1, 20, 0))), command); + } + + @Test + public void parseCommand_complete() throws Exception { + CompleteCommand commandIndex = (CompleteCommand) parser.parseCommand(CompleteCommand.COMMAND_WORD + + " " + INDEX_FIRST_PERSON.getOneBased()); + + CompleteCommand commandDate = (CompleteCommand) parser.parseCommand(CompleteCommand.COMMAND_WORD + + " " + PREFIX_APPOINTMENT_DATE + " " + "01-01-2023"); + + assertEquals(new CompleteByIndex(INDEX_FIRST_PERSON), commandIndex); + assertEquals(new CompleteByDate(LocalDate.of(2023, 1, 1)), commandDate); + } + + @Test + public void parseCommand_sort() throws Exception { + assertTrue(parser.parseCommand(SortCommand.COMMAND_WORD + " " + "name") instanceof SortCommand); + assertTrue(parser.parseCommand(SortCommand.COMMAND_WORD + " " + "appointment") instanceof SortCommand); } @Test diff --git a/src/test/java/seedu/address/logic/parser/CompleteCommandParserTest.java b/src/test/java/seedu/address/logic/parser/CompleteCommandParserTest.java new file mode 100644 index 00000000000..5ad08ee6b32 --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/CompleteCommandParserTest.java @@ -0,0 +1,118 @@ +package seedu.address.logic.parser; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.commands.CommandTestUtil.NAME_DESC_AMY; +import static seedu.address.logic.commands.CompleteCommand.MESSAGE_INVALID_DATE; +import static seedu.address.logic.commands.CompleteCommand.MESSAGE_INVALID_DATE_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_APPOINTMENT_DATE; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; + +import java.time.LocalDate; +import java.util.Optional; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.CompleteByDate; +import seedu.address.logic.commands.CompleteByIndex; +import seedu.address.logic.commands.CompleteCommand; + +class CompleteCommandParserTest { + public static final String DATE_DESC = " " + PREFIX_APPOINTMENT_DATE; + private static final String MESSAGE_INVALID_FORMAT = + String.format(MESSAGE_INVALID_COMMAND_FORMAT, CompleteCommand.MESSAGE_USAGE); + private CompleteCommandParser parser = new CompleteCommandParser(); + + @Test + public void parse_invalidPreamble_failure() { + // negative index + assertParseFailure(parser, "-5" + NAME_DESC_AMY, MESSAGE_INVALID_FORMAT); + + // multiple index + assertParseFailure(parser, "1 1" + NAME_DESC_AMY, MESSAGE_INVALID_FORMAT); + + // zero index + assertParseFailure(parser, "0" + NAME_DESC_AMY, MESSAGE_INVALID_FORMAT); + + // invalid arguments being parsed as preamble + assertParseFailure(parser, "1 some random string", MESSAGE_INVALID_FORMAT); + + // invalid prefix being parsed as preamble + assertParseFailure(parser, "1 i/ string", MESSAGE_INVALID_FORMAT); + + //max int + assertParseFailure(parser, "2147483648", MESSAGE_INVALID_FORMAT); + } + + @Test + public void parse_invalidDate_failure() { + //Invalid date format + assertParseFailure(parser, DATE_DESC + "01 May 2023", MESSAGE_INVALID_DATE_FORMAT); + assertParseFailure(parser, DATE_DESC + "01-05", MESSAGE_INVALID_DATE_FORMAT); + assertParseFailure(parser, DATE_DESC + "05-2023", MESSAGE_INVALID_DATE_FORMAT); + assertParseFailure(parser, DATE_DESC + "01-05-2023 20:00", MESSAGE_INVALID_DATE_FORMAT); + + //invalid date + assertParseFailure(parser, DATE_DESC + "01-13-2023", MESSAGE_INVALID_DATE); + assertParseFailure(parser, DATE_DESC + "29-02-2023", MESSAGE_INVALID_DATE); + assertParseFailure(parser, DATE_DESC + "31-04-2023", MESSAGE_INVALID_DATE); + } + + @Test + public void parse_invalidArgs_failure() { + //no index or date specified + assertParseFailure(parser, " ", MESSAGE_INVALID_FORMAT); + + //both index and date specified + assertParseFailure(parser, INDEX_FIRST_PERSON.getOneBased() + DATE_DESC + "01-05-2023", + MESSAGE_INVALID_FORMAT); + } + @Test + public void parse_duplicateFields_failure() { + String userInput = DATE_DESC + "01-05-2023" + DATE_DESC + "02-10-2023"; + assertParseFailure(parser, userInput, Messages.getErrorMessageForDuplicatePrefixes(PREFIX_APPOINTMENT_DATE)); + } + + @Test + public void parse_indexSpecified_success() { + Index targetIndex = INDEX_FIRST_PERSON; + String userInput = targetIndex.getOneBased() + ""; + + CompleteByIndex expectedCommand = new CompleteByIndex(targetIndex); + + assertParseSuccess(parser, userInput, expectedCommand); + } + + @Test + public void parse_dateSpecified_success() { + String userInput = DATE_DESC + "01-05-2023"; + CompleteByDate expectedCommand1 = new CompleteByDate(LocalDate.of(2023, 5, 1)); + + assertParseSuccess(parser, userInput, expectedCommand1); + + //leap year + String leapYear = DATE_DESC + "29-02-2024"; + CompleteByDate expectedCommand2 = new CompleteByDate(LocalDate.of(2024, 2, 29)); + + assertParseSuccess(parser, leapYear, expectedCommand2); + } + + @Test + public void isValidArguments() { + String nonEmptyIndex = "1"; + Optional nonEmptyDate = Optional.of("01-01-2022"); + String emptyIndex = ""; + Optional emptyDate = Optional.empty(); + + assertFalse(CompleteCommandParser.isValidArguments(nonEmptyIndex, nonEmptyDate)); + assertFalse(CompleteCommandParser.isValidArguments(emptyIndex, emptyDate)); + + assertTrue(CompleteCommandParser.isValidArguments(nonEmptyIndex, emptyDate)); + assertTrue(CompleteCommandParser.isValidArguments(emptyIndex, nonEmptyDate)); + } +} diff --git a/src/test/java/seedu/address/logic/parser/FindCommandParserTest.java b/src/test/java/seedu/address/logic/parser/FindCommandParserTest.java index d92e64d12f9..c5e26e455d0 100644 --- a/src/test/java/seedu/address/logic/parser/FindCommandParserTest.java +++ b/src/test/java/seedu/address/logic/parser/FindCommandParserTest.java @@ -1,15 +1,20 @@ package seedu.address.logic.parser; import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; import java.util.Arrays; +import java.util.List; import org.junit.jupiter.api.Test; import seedu.address.logic.commands.FindCommand; -import seedu.address.model.person.NameContainsKeywordsPredicate; +import seedu.address.model.person.predicates.CombinedPredicate; +import seedu.address.model.person.predicates.FinancialPlanContainsKeywordsPredicate; +import seedu.address.model.person.predicates.NameContainsKeywordsPredicate; +import seedu.address.model.person.predicates.TagContainsKeywordsPredicate; public class FindCommandParserTest { @@ -17,18 +22,36 @@ public class FindCommandParserTest { @Test public void parse_emptyArg_throwsParseException() { - assertParseFailure(parser, " ", String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindCommand.MESSAGE_USAGE)); + assertParseFailure(parser, " ", + String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindCommand.MESSAGE_USAGE)); + } + + @Test + public void parse_invalidArg_throwsParseException() { + assertParseFailure(parser, " doo doo ", + String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindCommand.MESSAGE_USAGE)); + } + + @Test + public void parse_nonEmptyPreamble_throwsParseException() { + String rawFindCommand = "doo doo" + PREFIX_NAME + "Alice " + PREFIX_NAME + "Bob"; + assertParseFailure(parser, rawFindCommand, + String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindCommand.MESSAGE_USAGE)); } @Test public void parse_validArgs_returnsFindCommand() { // no leading and trailing whitespaces FindCommand expectedFindCommand = - new FindCommand(new NameContainsKeywordsPredicate(Arrays.asList("Alice", "Bob"))); - assertParseSuccess(parser, "Alice Bob", expectedFindCommand); + new FindCommand(new CombinedPredicate( + new FinancialPlanContainsKeywordsPredicate(List.of()), + new NameContainsKeywordsPredicate(Arrays.asList("Alice", "Bob")), + new TagContainsKeywordsPredicate(List.of()))); + String rawFindCommand = " " + PREFIX_NAME + "Alice " + PREFIX_NAME + "Bob"; + assertParseSuccess(parser, rawFindCommand, expectedFindCommand); // multiple whitespaces between keywords - assertParseSuccess(parser, " \n Alice \n \t Bob \t", expectedFindCommand); + rawFindCommand = " " + PREFIX_NAME + " \n Alice \n " + PREFIX_NAME + " \t Bob \t"; + assertParseSuccess(parser, rawFindCommand, expectedFindCommand); } - } diff --git a/src/test/java/seedu/address/logic/parser/GatherCommandParserTest.java b/src/test/java/seedu/address/logic/parser/GatherCommandParserTest.java new file mode 100644 index 00000000000..c8eca113790 --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/GatherCommandParserTest.java @@ -0,0 +1,65 @@ +package seedu.address.logic.parser; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.commands.CommandTestUtil.FINANCIAL_PLAN_DESC_1; +import static seedu.address.logic.commands.CommandTestUtil.INVALID_FINANCIAL_PLAN_DESC; +import static seedu.address.logic.commands.CommandTestUtil.INVALID_TAG_DESC; +import static seedu.address.logic.commands.CommandTestUtil.TAG_DESC_HUSBAND; +import static seedu.address.logic.commands.CommandTestUtil.VALID_FINANCIAL_PLAN_1; +import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_HUSBAND; +import static seedu.address.logic.parser.CliSyntax.PREFIX_FINANCIAL_PLAN; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; + +import org.junit.jupiter.api.Test; + +import seedu.address.logic.commands.GatherCommand; +import seedu.address.model.financialplan.FinancialPlan; +import seedu.address.model.person.gatheremail.GatherEmailByFinancialPlan; +import seedu.address.model.person.gatheremail.GatherEmailByTag; +import seedu.address.model.tag.Tag; + +public class GatherCommandParserTest { + private GatherCommandParser parser = new GatherCommandParser(); + @Test + public void parse_emptyArg_throwsParseException() { + assertParseFailure(parser, " ", String.format(MESSAGE_INVALID_COMMAND_FORMAT, GatherCommand.MESSAGE_USAGE)); + } + + @Test + public void parse_invalidArgs_returnsException() { + // test for Financial Plan + String fpExceptionMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, FinancialPlan.MESSAGE_CONSTRAINTS); + assertParseFailure(parser, INVALID_FINANCIAL_PLAN_DESC, fpExceptionMessage); + + // test for Tag + String tagExceptionMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, Tag.MESSAGE_CONSTRAINTS); + assertParseFailure(parser, INVALID_TAG_DESC, tagExceptionMessage); + + // both prompts + String bothPrompts = FINANCIAL_PLAN_DESC_1 + TAG_DESC_HUSBAND; + String exceptionMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, GatherCommand.MESSAGE_USAGE); + assertParseFailure(parser, bothPrompts, exceptionMessage); + } + + @Test + public void parse_validArgs_returnsGatherCommand() { + GatherEmailByFinancialPlan financialPlanPrompt = new GatherEmailByFinancialPlan(VALID_FINANCIAL_PLAN_1); + GatherCommand fpExpectedGatherCommand = new GatherCommand(financialPlanPrompt); + assertParseSuccess(parser, FINANCIAL_PLAN_DESC_1, fpExpectedGatherCommand); + + GatherEmailByTag tagPrompt = new GatherEmailByTag(VALID_TAG_HUSBAND); + GatherCommand tagExpectedGatherCommand = new GatherCommand(tagPrompt); + assertParseSuccess(parser, TAG_DESC_HUSBAND, tagExpectedGatherCommand); + } + + @Test + public void removePrefix_success() { + // removes for Financial Plan + assertEquals(VALID_FINANCIAL_PLAN_1, parser.removePrefix(FINANCIAL_PLAN_DESC_1, PREFIX_FINANCIAL_PLAN)); + // removes for Tag + assertEquals(VALID_TAG_HUSBAND, parser.removePrefix(TAG_DESC_HUSBAND, PREFIX_TAG)); + } +} diff --git a/src/test/java/seedu/address/logic/parser/ParserUtilTest.java b/src/test/java/seedu/address/logic/parser/ParserUtilTest.java index 4256788b1a7..e06382fc02c 100644 --- a/src/test/java/seedu/address/logic/parser/ParserUtilTest.java +++ b/src/test/java/seedu/address/logic/parser/ParserUtilTest.java @@ -1,11 +1,15 @@ package seedu.address.logic.parser; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; import static seedu.address.logic.parser.ParserUtil.MESSAGE_INVALID_INDEX; import static seedu.address.testutil.Assert.assertThrows; import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; +import java.time.LocalDateTime; +import java.time.YearMonth; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; @@ -14,6 +18,7 @@ import org.junit.jupiter.api.Test; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.appointment.Appointment; import seedu.address.model.person.Address; import seedu.address.model.person.Email; import seedu.address.model.person.Name; @@ -26,16 +31,22 @@ public class ParserUtilTest { private static final String INVALID_ADDRESS = " "; private static final String INVALID_EMAIL = "example.com"; private static final String INVALID_TAG = "#friend"; - - private static final String VALID_NAME = "Rachel Walker"; + private static final String INVALID_FINANCIAL_PLAN = "Plan !!"; + private static final String VALID_NAME_1 = "Rachel Walker"; + private static final String VALID_NAME_2 = "Captain Kek"; private static final String VALID_PHONE = "123456"; private static final String VALID_ADDRESS = "123 Main Street #0505"; private static final String VALID_EMAIL = "rachel@example.com"; private static final String VALID_TAG_1 = "friend"; private static final String VALID_TAG_2 = "neighbour"; - + private static final String VALID_FINANCIAL_PLAN_1 = "Plan A"; + private static final String VALID_FINANCIAL_PLAN_2 = "Plan B"; + private static final String VALID_APPOINTMENT_DESC = "Review Insurance"; + private static final String VALID_APPOINTMENT_DATE = "01-01-2023 20:00"; private static final String WHITESPACE = " \t\r\n"; - + private static final String INVALID_APPOINTMENT_DESC = "#Review Insurance*"; + private static final String INVALID_APPOINTMENT_DATE_FORMAT = "1 May 2023 20:00"; + private static final String INVALID_APPOINTMENT_DATE = "01-13-2023 20:00"; @Test public void parseIndex_invalidInput_throwsParseException() { assertThrows(ParseException.class, () -> ParserUtil.parseIndex("10 a")); @@ -68,14 +79,14 @@ public void parseName_invalidValue_throwsParseException() { @Test public void parseName_validValueWithoutWhitespace_returnsName() throws Exception { - Name expectedName = new Name(VALID_NAME); - assertEquals(expectedName, ParserUtil.parseName(VALID_NAME)); + Name expectedName = new Name(VALID_NAME_1); + assertEquals(expectedName, ParserUtil.parseName(VALID_NAME_1)); } @Test public void parseName_validValueWithWhitespace_returnsTrimmedName() throws Exception { - String nameWithWhitespace = WHITESPACE + VALID_NAME + WHITESPACE; - Name expectedName = new Name(VALID_NAME); + String nameWithWhitespace = WHITESPACE + VALID_NAME_1 + WHITESPACE; + Name expectedName = new Name(VALID_NAME_1); assertEquals(expectedName, ParserUtil.parseName(nameWithWhitespace)); } @@ -193,4 +204,153 @@ public void parseTags_collectionWithValidTags_returnsTagSet() throws Exception { assertEquals(expectedTagSet, actualTagSet); } + + @Test + public void parseAppointment_validValueAndDate_returnsAppointment() throws Exception { + Appointment expectedAppointment = new Appointment("Review Insurance", + LocalDateTime.of(2023, 1, 1, 20, 0)); + assertEquals(expectedAppointment, ParserUtil.parseAppointment(VALID_APPOINTMENT_DESC, VALID_APPOINTMENT_DATE)); + } + + @Test + public void parseAppointment_null_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> ParserUtil.parseAppointment(null, + null)); + } + + @Test + public void parseAppointment_invalidInputs_throwsParseException() { + assertThrows(ParseException.class, () -> ParserUtil.parseAppointment(INVALID_APPOINTMENT_DESC, + VALID_APPOINTMENT_DATE)); + + assertThrows(ParseException.class, () -> ParserUtil.parseAppointment(VALID_APPOINTMENT_DESC, + INVALID_APPOINTMENT_DATE_FORMAT)); + + assertThrows(ParseException.class, () -> ParserUtil.parseAppointment(VALID_APPOINTMENT_DESC, + INVALID_APPOINTMENT_DATE)); + } + + @Test + public void validateName_validInput_success() { + try { + ParserUtil.validateName(VALID_NAME_1); + } catch (ParseException e) { + fail(); + } + } + @Test + public void validateName_invalidInput_throwsParseException() { + assertThrows(ParseException.class, () -> ParserUtil.validateName(INVALID_NAME)); + } + @Test + public void validateTag_validInput_success() { + try { + ParserUtil.validateTag(VALID_TAG_1); + } catch (ParseException e) { + fail(); + } + } + @Test + public void validateTag_invalidInput_throwsParseException() { + assertThrows(ParseException.class, () -> ParserUtil.validateTag(INVALID_TAG)); + + // with white space + assertThrows(ParseException.class, () -> ParserUtil.validateTag(VALID_TAG_1 + " " + VALID_TAG_2)); + } + @Test + public void validateFinancialPlan_validInput_success() { + try { + ParserUtil.validateFinancialPlan(VALID_FINANCIAL_PLAN_1); + } catch (ParseException e) { + fail(); + } + } + @Test + public void validateFinancialPlan_invalidInput_throwsParseException() { + assertThrows(ParseException.class, () -> ParserUtil.validateFinancialPlan(INVALID_FINANCIAL_PLAN)); + } + @Test + void isValidDay_validDate_shouldReturnTrue() { + // November has 30 days + YearMonth yearMonth = YearMonth.of(2022, 11); + String date = "15-11-2022"; + assertTrue(ParserUtil.isValidDay(yearMonth, date)); + } + @Test + void isValidDay_invalidDate_shouldReturnFalse() { + // February has 28 days + YearMonth yearMonth = YearMonth.of(2022, 2); + String date = "31-02-2022"; + assertFalse(ParserUtil.isValidDay(yearMonth, date)); + } + + @Test + void parseDate_validDate_shouldParseSuccessfully() throws ParseException { + String validDateString = "29-02-2024"; + try { + ParserUtil.parseDate(validDateString); + } catch (ParseException e) { + fail(); + } + } + @Test + void parseDate_invalidDate_shouldThrowParseException() { + // 29th Feb for non leap year + String invalidDateString1 = "29-02-2022"; + assertThrows(ParseException.class, () -> ParserUtil.parseDate(invalidDateString1)); + + // No 31st Nov + String invalidDateString2 = "31-04-2023"; + assertThrows(ParseException.class, () -> ParserUtil.parseDate(invalidDateString2)); + } + @Test + void parseAppointment_validInput_shouldParseSuccessfully() throws ParseException { + try { + ParserUtil.parseAppointment(VALID_APPOINTMENT_DESC, VALID_APPOINTMENT_DATE); + } catch (ParseException e) { + fail(); + } + } + @Test + void parseAppointment_invalidInput_shouldThrowParseException() { + // Invalid description + assertThrows(ParseException.class, () -> + ParserUtil.parseAppointment(INVALID_APPOINTMENT_DESC, VALID_APPOINTMENT_DATE)); + // 29th Feb for non leap year + String invalidDateString1 = "29-02-2022 14:30"; + assertThrows(ParseException.class, () -> + ParserUtil.parseAppointment(VALID_APPOINTMENT_DESC, invalidDateString1)); + // No 31st Nov + String invalidDateString2 = "31-11-2023 14:30"; + assertThrows(ParseException.class, () -> + ParserUtil.parseAppointment(VALID_APPOINTMENT_DESC, invalidDateString2)); + + // Wrong format + String invalidFormat = "1"; + assertThrows(ParseException.class, () -> + ParserUtil.parseAppointment(VALID_APPOINTMENT_DESC, invalidFormat)); + } + @Test + void parseAppointmentDate_validDate_shouldParseSuccessfully() throws ParseException { + String validDateTimeString = "29-02-2024 14:30"; + try { + ParserUtil.parseAppointmentDate(validDateTimeString); + } catch (ParseException e) { + fail(); + } + } + @Test + void parseAppointmentDate_invalidDate_shouldThrowParseException() { + // Wrong format + String invalidDateTime = "1"; + assertThrows(ParseException.class, () -> ParserUtil.parseAppointmentDate(invalidDateTime)); + + // 29th Feb for non leap year + String invalidDateTime1 = "29-02-2022 14:30"; + assertThrows(ParseException.class, () -> ParserUtil.parseAppointmentDate(invalidDateTime1)); + + // No 31st Nov + String invalidDateTime2 = "31-11-2023 14:30"; + assertThrows(ParseException.class, () -> ParserUtil.parseAppointmentDate(invalidDateTime2)); + } } diff --git a/src/test/java/seedu/address/logic/parser/ScheduleCommandParserTest.java b/src/test/java/seedu/address/logic/parser/ScheduleCommandParserTest.java new file mode 100644 index 00000000000..152904dde04 --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/ScheduleCommandParserTest.java @@ -0,0 +1,95 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.commands.CommandTestUtil.APPOINTMENT_DATE_DESC; +import static seedu.address.logic.commands.CommandTestUtil.APPOINTMENT_NAME_DESC; +import static seedu.address.logic.commands.CommandTestUtil.INVALID_APPOINTMENT_DATE; +import static seedu.address.logic.commands.CommandTestUtil.INVALID_APPOINTMENT_DATE_FORMAT; +import static seedu.address.logic.commands.CommandTestUtil.INVALID_APPOINTMENT_NAME_DESC; +import static seedu.address.logic.commands.CommandTestUtil.INVALID_APPOINTMENT_TIME_FORMAT; +import static seedu.address.logic.commands.CommandTestUtil.VALID_APPOINTMENT_DATE; +import static seedu.address.logic.commands.CommandTestUtil.VALID_APPOINTMENT_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_APPOINTMENT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_APPOINTMENT_DATE; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; +import static seedu.address.testutil.TypicalIndexes.INDEX_FIRST_PERSON; + +import java.time.LocalDateTime; + +import org.junit.jupiter.api.Test; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.ScheduleCommand; +import seedu.address.model.appointment.Appointment; + +class ScheduleCommandParserTest { + private ScheduleCommandParser parser = new ScheduleCommandParser(); + private Index targetIndex = INDEX_FIRST_PERSON; + + @Test + public void parse_validScheduleInput_success() { + String userInput = targetIndex.getOneBased() + APPOINTMENT_NAME_DESC + APPOINTMENT_DATE_DESC; + + LocalDateTime date = Appointment.parseAppointmentDate(VALID_APPOINTMENT_DATE); + Appointment appointment = new Appointment(VALID_APPOINTMENT_NAME, date); + + assertParseSuccess(parser, userInput, new ScheduleCommand(targetIndex, appointment)); + } + + @Test + public void parse_compulsoryFieldMissing_failure() { + String expectedMessage = String.format(MESSAGE_INVALID_COMMAND_FORMAT, ScheduleCommand.MESSAGE_USAGE); + + //missing appointment name prefix + assertParseFailure(parser, targetIndex.getOneBased() + " " + VALID_APPOINTMENT_NAME + + APPOINTMENT_DATE_DESC, expectedMessage); + + //missing appointment date prefix + assertParseFailure(parser, targetIndex.getOneBased() + APPOINTMENT_NAME_DESC + + " " + VALID_APPOINTMENT_DATE, expectedMessage); + + //missing index + assertParseFailure(parser, APPOINTMENT_NAME_DESC + APPOINTMENT_DATE_DESC, expectedMessage); + } + + @Test + public void parse_invalidValue_failure() { + + // invalid appointment name + String invalidNameString = targetIndex.getOneBased() + INVALID_APPOINTMENT_NAME_DESC + + APPOINTMENT_DATE_DESC; + assertParseFailure(parser, invalidNameString, Appointment.MESSAGE_DESC_CONSTRAINTS); + + // invalid time format + String invalidTimeFormatString = targetIndex.getOneBased() + APPOINTMENT_NAME_DESC + + INVALID_APPOINTMENT_TIME_FORMAT; + assertParseFailure(parser, invalidTimeFormatString, Appointment.MESSAGE_DATE_CONSTRAINTS); + + // invalid index + String invalidIndexString = "-1" + APPOINTMENT_NAME_DESC + APPOINTMENT_DATE_DESC; + assertParseFailure(parser, invalidIndexString, + String.format(MESSAGE_INVALID_COMMAND_FORMAT, ScheduleCommand.MESSAGE_USAGE)); + + // invalid date format + String invalidDateFormatString = targetIndex.getOneBased() + APPOINTMENT_NAME_DESC + + INVALID_APPOINTMENT_DATE_FORMAT; + assertParseFailure(parser, invalidDateFormatString, Appointment.MESSAGE_DATE_CONSTRAINTS); + + // invalid date + String invalidDateString = targetIndex.getOneBased() + APPOINTMENT_NAME_DESC + INVALID_APPOINTMENT_DATE; + assertParseFailure(parser, invalidDateString, Appointment.MESSAGE_INVALID_DATE); + + // duplicate name + String validExpectedSchedule = APPOINTMENT_NAME_DESC + APPOINTMENT_DATE_DESC; + assertParseFailure(parser, targetIndex.getOneBased() + " " + PREFIX_APPOINTMENT + + VALID_APPOINTMENT_NAME + validExpectedSchedule, + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_APPOINTMENT)); + + // duplicate date + assertParseFailure(parser, targetIndex.getOneBased() + validExpectedSchedule + " " + + PREFIX_APPOINTMENT_DATE + VALID_APPOINTMENT_DATE, + Messages.getErrorMessageForDuplicatePrefixes(PREFIX_APPOINTMENT_DATE)); + } +} diff --git a/src/test/java/seedu/address/logic/parser/SortCommandParserTest.java b/src/test/java/seedu/address/logic/parser/SortCommandParserTest.java new file mode 100644 index 00000000000..1ff7112331f --- /dev/null +++ b/src/test/java/seedu/address/logic/parser/SortCommandParserTest.java @@ -0,0 +1,43 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseFailure; +import static seedu.address.logic.parser.CommandParserTestUtil.assertParseSuccess; + +import org.junit.jupiter.api.Test; + +import seedu.address.logic.commands.SortCommand; +import seedu.address.model.person.comparator.SortByAppointmentComparator; +import seedu.address.model.person.comparator.SortByNameComparator; + +public class SortCommandParserTest { + + private SortCommandParser parser = new SortCommandParser(); + + @Test + public void parse_emptyArg_throwsParseException() { + assertParseFailure(parser, " ", String.format(MESSAGE_INVALID_COMMAND_FORMAT, SortCommand.MESSAGE_USAGE)); + } + + @Test + public void parse_validArgs_returnsSortByNameCommand() { + // no leading and trailing whitespaces + SortCommand expectedSortCommand = + new SortCommand(new SortByNameComparator()); + assertParseSuccess(parser, "name", expectedSortCommand); + } + + @Test + public void parse_validArgs_returnsSortByAppointmentCommand() { + // no leading and trailing whitespaces + SortCommand expectedSortCommand = + new SortCommand(new SortByAppointmentComparator()); + assertParseSuccess(parser, "appointment", expectedSortCommand); + } + + @Test + public void parse_invalidArg_throwsParseException() { + assertParseFailure(parser, "sorter", String.format(MESSAGE_INVALID_COMMAND_FORMAT, SortCommand.MESSAGE_USAGE)); + } + +} diff --git a/src/test/java/seedu/address/model/AddressBookTest.java b/src/test/java/seedu/address/model/AddressBookTest.java index 68c8c5ba4d5..bae666d0157 100644 --- a/src/test/java/seedu/address/model/AddressBookTest.java +++ b/src/test/java/seedu/address/model/AddressBookTest.java @@ -7,6 +7,8 @@ import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_HUSBAND; import static seedu.address.testutil.Assert.assertThrows; import static seedu.address.testutil.TypicalPersons.ALICE; +import static seedu.address.testutil.TypicalPersons.BOB; +import static seedu.address.testutil.TypicalPersons.ELLE; import static seedu.address.testutil.TypicalPersons.getTypicalAddressBook; import java.util.Arrays; @@ -18,8 +20,11 @@ import javafx.collections.FXCollections; import javafx.collections.ObservableList; +import seedu.address.model.financialplan.FinancialPlan; import seedu.address.model.person.Person; import seedu.address.model.person.exceptions.DuplicatePersonException; +import seedu.address.model.person.gatheremail.GatherEmailByFinancialPlan; +import seedu.address.model.person.gatheremail.GatherEmailByTag; import seedu.address.testutil.PersonBuilder; public class AddressBookTest { @@ -54,6 +59,36 @@ public void resetData_withDuplicatePersons_throwsDuplicatePersonException() { assertThrows(DuplicatePersonException.class, () -> addressBook.resetData(newData)); } + @Test + public void gatherByFinancialPlan_noPersonFound() { + addressBook.addPerson(ALICE); + GatherEmailByFinancialPlan prompt = new GatherEmailByFinancialPlan("Sample Financial Plan 3"); + assertEquals(new String(), addressBook.gatherEmails(prompt)); + } + + @Test + public void gatherByFinancialPlan_personFound() { + addressBook.addPerson(ELLE); + FinancialPlan elleFinancialPlan = ELLE.getFinancialPlans().iterator().next(); + String fpDescription = elleFinancialPlan.toString().replaceAll("[\\[\\]\\(\\)]", ""); + GatherEmailByFinancialPlan prompt = new GatherEmailByFinancialPlan(fpDescription); + assertEquals(ELLE.getEmail().toString() + ";", addressBook.gatherEmails(prompt)); + } + + @Test + public void gatherByTag_noPersonFound() { + addressBook.addPerson(ALICE); + GatherEmailByTag prompt = new GatherEmailByTag(VALID_TAG_HUSBAND); + assertEquals(new String(), addressBook.gatherEmails(prompt)); + } + + @Test + public void gatherByTag_personFound() { + addressBook.addPerson(BOB); + GatherEmailByTag prompt = new GatherEmailByTag(VALID_TAG_HUSBAND); + assertEquals(BOB.getEmail().toString() + ";", addressBook.gatherEmails(prompt)); + } + @Test public void hasPerson_nullPerson_throwsNullPointerException() { assertThrows(NullPointerException.class, () -> addressBook.hasPerson(null)); diff --git a/src/test/java/seedu/address/model/ModelManagerTest.java b/src/test/java/seedu/address/model/ModelManagerTest.java index 2cf1418d116..dc9010eb339 100644 --- a/src/test/java/seedu/address/model/ModelManagerTest.java +++ b/src/test/java/seedu/address/model/ModelManagerTest.java @@ -3,10 +3,13 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_HUSBAND; import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; import static seedu.address.testutil.Assert.assertThrows; import static seedu.address.testutil.TypicalPersons.ALICE; import static seedu.address.testutil.TypicalPersons.BENSON; +import static seedu.address.testutil.TypicalPersons.BOB; +import static seedu.address.testutil.TypicalPersons.ELLE; import java.nio.file.Path; import java.nio.file.Paths; @@ -15,7 +18,10 @@ import org.junit.jupiter.api.Test; import seedu.address.commons.core.GuiSettings; -import seedu.address.model.person.NameContainsKeywordsPredicate; +import seedu.address.model.financialplan.FinancialPlan; +import seedu.address.model.person.gatheremail.GatherEmailByFinancialPlan; +import seedu.address.model.person.gatheremail.GatherEmailByTag; +import seedu.address.model.person.predicates.NameContainsKeywordsPredicate; import seedu.address.testutil.AddressBookBuilder; public class ModelManagerTest { @@ -65,6 +71,36 @@ public void setAddressBookFilePath_nullPath_throwsNullPointerException() { assertThrows(NullPointerException.class, () -> modelManager.setAddressBookFilePath(null)); } + @Test + public void gatherEmailsByFinancialPlan_noPersonFound() { + modelManager.addPerson(ALICE); + GatherEmailByFinancialPlan prompt = new GatherEmailByFinancialPlan("Sample Financial Plan 3"); + assertEquals(new String(), modelManager.gatherEmails(prompt)); + } + + @Test + public void gatherEmailsByFinancialPlan_personFound() { + modelManager.addPerson(ELLE); + FinancialPlan elleFinancialPlan = ELLE.getFinancialPlans().iterator().next(); + String fpDescription = elleFinancialPlan.toString().replaceAll("[\\[\\]\\(\\)]", ""); + GatherEmailByFinancialPlan prompt = new GatherEmailByFinancialPlan(fpDescription); + assertEquals(ELLE.getEmail().toString() + ";", modelManager.gatherEmails(prompt)); + } + + @Test + public void gatherByTag_noPersonFound() { + modelManager.addPerson(ALICE); + GatherEmailByTag prompt = new GatherEmailByTag(VALID_TAG_HUSBAND); + assertEquals(new String(), modelManager.gatherEmails(prompt)); + } + + @Test + public void gatherByTag_personFound() { + modelManager.addPerson(BOB); + GatherEmailByTag prompt = new GatherEmailByTag(VALID_TAG_HUSBAND); + assertEquals(BOB.getEmail().toString() + ";", modelManager.gatherEmails(prompt)); + } + @Test public void setAddressBookFilePath_validPath_setsAddressBookFilePath() { Path path = Paths.get("address/book/file/path"); diff --git a/src/test/java/seedu/address/model/appointment/AppointmentTest.java b/src/test/java/seedu/address/model/appointment/AppointmentTest.java new file mode 100644 index 00000000000..a2e2ce92e43 --- /dev/null +++ b/src/test/java/seedu/address/model/appointment/AppointmentTest.java @@ -0,0 +1,154 @@ +package seedu.address.model.appointment; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.testutil.Assert.assertThrows; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeParseException; + +import org.junit.jupiter.api.Test; + +import seedu.address.model.person.Name; + +class AppointmentTest { + + public static final LocalDateTime VALID_DATE_TIME = LocalDateTime.of(2023, 1, + 1, 20, 0); + @Test + public void constructor_null_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> new Appointment(null, + LocalDateTime.of(2023, 1, 1, 1, 1))); + } + + @Test + public void isValidDesc() { + // null name + assertThrows(NullPointerException.class, () -> Appointment.isValidDesc(null)); + + // invalid Appointment name + assertFalse(Name.isValidName("")); // empty string + assertFalse(Name.isValidName(" ")); // spaces only + assertFalse(Name.isValidName("^")); // only non-alphanumeric characters + assertFalse(Name.isValidName("peter*")); // contains non-alphanumeric characters + + // valid Appointment name + assertTrue(Name.isValidName("insurance")); // alphabets only + assertTrue(Name.isValidName("12345")); // numbers only + assertTrue(Name.isValidName("review Insurance 1")); // alphanumeric characters + assertTrue(Name.isValidName("Review Insurance")); // with capital letters + assertTrue(Name.isValidName("Review Insurance policy 1")); // long names + } + + @Test + public void isValidDateFormat() { + // null date + assertThrows(NullPointerException.class, () -> Appointment.isValidDateFormat(null)); + + // valid date + assertTrue(Appointment.isValidDateFormat("01-01-2023 20:00")); + } + + @Test + public void isValidAppointment() { + // null appointment string representation + assertThrows(NullPointerException.class, () -> Appointment.isValidAppointment(null)); + + // invalid appointment string format + assertFalse(Appointment.isValidAppointment("Review Insurance 01-01-2023 20:00")); + assertFalse(Appointment.isValidAppointment("Review Insurance, ")); // missing date + assertFalse(Appointment.isValidAppointment("01-01-2023 20:00")); // missing name + assertFalse(Appointment.isValidAppointment("Review Insurance 01-01-2023 20:00")); // wrong format + assertFalse(Appointment.isValidAppointment("Review Insurance 1 May 2023 20:00")); // wrong date + assertFalse(Appointment.isValidAppointment("Review Insurance 01-01-2023 8pm")); // wrong time + // valid appointment string + assertTrue(Appointment.isValidAppointment("Review Insurance, 01-01-2023 20:00")); + } + + @Test + public void testToStringReturnsExpectedString() { + Appointment appointment = new Appointment("Review Insurance", + VALID_DATE_TIME); + + // correct string + assertEquals(appointment.toString(), "Review Insurance, 01-01-2023 20:00"); + } + + @Test + public void equals() { + Appointment appointment = new Appointment("Review Insurance", + LocalDateTime.of(2023, 1, 1, 20, 0)); + + // same values + assertTrue(appointment.equals(new Appointment("Review Insurance", + VALID_DATE_TIME))); + + // same object + assertTrue(appointment.equals(appointment)); + + // null -> returns false + assertFalse(appointment.equals(null)); + + // different types -> returns false + assertFalse(appointment.equals(5.0f)); + + // different name values -> returns false + assertFalse(appointment.equals(new Appointment("Insurance nomination", + LocalDateTime.of(2023, 1, 1, 20, 0)))); + + // different date obj -> returns false + assertFalse(appointment.equals(new Appointment("Review insurance", + LocalDateTime.of(2023, 5, 2, 18, 0)))); + } + @Test + public void parseAppointmentDate() { + LocalDateTime expectedDateTime = VALID_DATE_TIME; + + assertEquals(expectedDateTime, Appointment.parseAppointmentDate("01-01-2023 20:00")); + + // invalid day + assertThrows(DateTimeParseException.class, () -> Appointment.parseAppointmentDate("32-01-2023 20:00")); + // invalid month + assertThrows(DateTimeParseException.class, () -> Appointment.parseAppointmentDate("01-13-2023 20:00")); + // invalid year + assertThrows(DateTimeParseException.class, () -> Appointment.parseAppointmentDate("01-13-0100 20:00")); + // invalid hours + assertThrows(DateTimeParseException.class, () -> Appointment.parseAppointmentDate("01-01-2023 25:00")); + // invalid minutes + assertThrows(DateTimeParseException.class, () -> Appointment.parseAppointmentDate("01-01-2023 20:61")); + } + @Test + public void parseAppointmentDescription() { + Appointment expectedAppointment = new Appointment("Review Insurance", + VALID_DATE_TIME); + + Appointment testAppointment = Appointment.parseAppointmentDescription("Review Insurance, 01-01-2023 20:00"); + + assertEquals(expectedAppointment, testAppointment); + } + + @Test + public void compareTo() { + Appointment appointment1 = new Appointment("Review Insurance", VALID_DATE_TIME); + Appointment appointment2 = new Appointment("Buy Insurance", LocalDateTime.of(2023, 1, + 1, 8, 0)); + NullAppointment nullAppointment = NullAppointment.getNullAppointment(); + + assertTrue(appointment1.compareTo(nullAppointment) < 0); + assertTrue(appointment1.compareTo(appointment1) == 0); + assertTrue(appointment1.compareTo(appointment2) > 0); + } + + @Test + public void isSameDate() { + Appointment appointment1 = new Appointment("Review Insurance", VALID_DATE_TIME); + LocalDate sameDateTime = LocalDate.of(2023, 1, 1); + + assertTrue(appointment1.isSameDate(sameDateTime)); + + LocalDate differentDateTime = LocalDate.of(2023, 05, 05); + assertFalse(appointment1.isSameDate(differentDateTime)); + } +} diff --git a/src/test/java/seedu/address/model/appointment/NullAppointmentTest.java b/src/test/java/seedu/address/model/appointment/NullAppointmentTest.java new file mode 100644 index 00000000000..ee04a326a88 --- /dev/null +++ b/src/test/java/seedu/address/model/appointment/NullAppointmentTest.java @@ -0,0 +1,51 @@ +package seedu.address.model.appointment; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.time.LocalDateTime; + +import org.junit.jupiter.api.Test; + +class NullAppointmentTest { + + @Test + public void getNullAppointment() { + // Factory method should return same null appointment instance + NullAppointment nullAppointment1 = NullAppointment.getNullAppointment(); + NullAppointment nullAppointment2 = NullAppointment.getNullAppointment(); + + assertTrue(nullAppointment1 == nullAppointment2); + } + + @Test + public void equals() { + + NullAppointment nullAppointment1 = NullAppointment.getNullAppointment(); + Appointment appointment = new Appointment("Review Insurance", + LocalDateTime.of(2023, 1, 1, 20, 00)); + + assertFalse(nullAppointment1.equals(appointment)); + + NullAppointment nullAppointment2 = NullAppointment.getNullAppointment(); + assertTrue(nullAppointment1.equals(nullAppointment2)); + } + + @Test + public void testToStringReturnsExpectedString() { + NullAppointment nullAppointment = NullAppointment.getNullAppointment(); + assertTrue(nullAppointment.toString().equals("No Appointment made!")); + } + + @Test + public void compareTo() { + NullAppointment nullAppointment1 = NullAppointment.getNullAppointment(); + NullAppointment nullAppointment2 = NullAppointment.getNullAppointment(); + Appointment appointment = new Appointment("Review Insurance", + LocalDateTime.of(2023, 1, 1, 20, 0)); + + //if both null appointments + assertTrue(nullAppointment1.compareTo(nullAppointment2) == 0); + assertTrue(nullAppointment1.compareTo(appointment) >= 1); + } +} diff --git a/src/test/java/seedu/address/model/appointment/SortByAppointmentNameComparatorTest.java b/src/test/java/seedu/address/model/appointment/SortByAppointmentNameComparatorTest.java new file mode 100644 index 00000000000..48c0d2453c8 --- /dev/null +++ b/src/test/java/seedu/address/model/appointment/SortByAppointmentNameComparatorTest.java @@ -0,0 +1,34 @@ +package seedu.address.model.appointment; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +public class SortByAppointmentNameComparatorTest { + @Test + public void equals() { + SortByAppointmentDateComparator firstComparator = new SortByAppointmentDateComparator(); + SortByAppointmentDateComparator secondComparator = new SortByAppointmentDateComparator(); + + // same object -> returns true + assertTrue(firstComparator.equals(firstComparator)); + + // different types -> returns false + assertFalse(firstComparator.equals(1)); + + // null -> returns false + assertFalse(firstComparator.equals(null)); + + // different comparators -> returns true + assertTrue(firstComparator.equals(secondComparator)); + } + @Test + public void toStringMethod() { + SortByAppointmentDateComparator comparator = new SortByAppointmentDateComparator(); + + String expected = SortByAppointmentDateComparator.class.getCanonicalName() + "{}"; + assertEquals(expected, comparator.toString()); + } +} diff --git a/src/test/java/seedu/address/model/financialplan/FinancialPlanTest.java b/src/test/java/seedu/address/model/financialplan/FinancialPlanTest.java new file mode 100644 index 00000000000..b8dd4ed2faf --- /dev/null +++ b/src/test/java/seedu/address/model/financialplan/FinancialPlanTest.java @@ -0,0 +1,54 @@ +package seedu.address.model.financialplan; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.commands.CommandTestUtil.VALID_FINANCIAL_PLAN_1; +import static seedu.address.logic.commands.CommandTestUtil.VALID_FINANCIAL_PLAN_2; +import static seedu.address.testutil.Assert.assertThrows; + +import org.junit.jupiter.api.Test; + +public class FinancialPlanTest { + @Test + public void constructor_null_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> new FinancialPlan(null)); + } + + @Test + public void constructor_invalidTagName_throwsIllegalArgumentException() { + String invalidFinancialPlanName = "-_"; + assertThrows(IllegalArgumentException.class, () -> new FinancialPlan(invalidFinancialPlanName)); + } + + @Test + public void containsSubstring_failure() { + FinancialPlan fp = new FinancialPlan(VALID_FINANCIAL_PLAN_1); + String substring = "plan 2"; + assertFalse(fp.containsSubstring(substring)); + } + + @Test + public void containsSubstring_success() { + FinancialPlan fp = new FinancialPlan(VALID_FINANCIAL_PLAN_1); + String substring = "plan 1"; + assertTrue(fp.containsSubstring(substring)); + } + + @Test + public void isValidFinancialName() { + // null tag name + assertThrows(NullPointerException.class, () -> FinancialPlan.isValidFinancialPlanName(null)); + } + + @Test + public void equals() { + FinancialPlan fp1 = new FinancialPlan(VALID_FINANCIAL_PLAN_1); + FinancialPlan fp2 = new FinancialPlan(VALID_FINANCIAL_PLAN_1); + FinancialPlan fp3 = new FinancialPlan(VALID_FINANCIAL_PLAN_2); + + assertTrue(fp1.equals(fp1)); + assertTrue(fp1.equals(fp2)); + assertFalse(fp1.equals(fp3)); + assertFalse(fp2.equals(null)); + } +} diff --git a/src/test/java/seedu/address/model/person/NextOfKinNameTest.java b/src/test/java/seedu/address/model/person/NextOfKinNameTest.java new file mode 100644 index 00000000000..7f4f6d80361 --- /dev/null +++ b/src/test/java/seedu/address/model/person/NextOfKinNameTest.java @@ -0,0 +1,59 @@ +package seedu.address.model.person; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.testutil.Assert.assertThrows; + +import org.junit.jupiter.api.Test; + +public class NextOfKinNameTest { + @Test + public void constructor_null_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> new NextOfKinName(null)); + } + + @Test + public void constructor_invalidNextOfKinName_throwsIllegalArgumentException() { + String invalidName = ""; + assertThrows(IllegalArgumentException.class, () -> new NextOfKinName(invalidName)); + } + + @Test + public void isValidNextOfKinName() { + // null name + assertThrows(NullPointerException.class, () -> NextOfKinName.isValidName(null)); + + // invalid name + assertFalse(NextOfKinName.isValidName("")); // empty string + assertFalse(NextOfKinName.isValidName(" ")); // spaces only + assertFalse(NextOfKinName.isValidName("^")); // only non-alphanumeric characters + assertFalse(NextOfKinName.isValidName("peter*")); // contains non-alphanumeric characters + + // valid name + assertTrue(NextOfKinName.isValidName("peter jack")); // alphabets only + assertTrue(NextOfKinName.isValidName("12345")); // numbers only + assertTrue(NextOfKinName.isValidName("peter the 2nd")); // alphanumeric characters + assertTrue(NextOfKinName.isValidName("Capital Tan")); // with capital letters + assertTrue(NextOfKinName.isValidName("David Roger Jackson Ray Jr 2nd")); // long names + } + + @Test + public void equals() { + NextOfKinName name = new NextOfKinName("Valid NextOfKinName"); + + // same values -> returns true + assertTrue(name.equals(new NextOfKinName("Valid NextOfKinName"))); + + // same object -> returns true + assertTrue(name.equals(name)); + + // null -> returns false + assertFalse(name.equals(null)); + + // different types -> returns false + assertFalse(name.equals(5.0f)); + + // different values -> returns false + assertFalse(name.equals(new NextOfKinName("Other Valid NextOfKinName"))); + } +} diff --git a/src/test/java/seedu/address/model/person/NextOfKinPhoneTest.java b/src/test/java/seedu/address/model/person/NextOfKinPhoneTest.java new file mode 100644 index 00000000000..bcad3cec87c --- /dev/null +++ b/src/test/java/seedu/address/model/person/NextOfKinPhoneTest.java @@ -0,0 +1,59 @@ +package seedu.address.model.person; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.testutil.Assert.assertThrows; + +import org.junit.jupiter.api.Test; + +public class NextOfKinPhoneTest { + @Test + public void constructor_null_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> new NextOfKinPhone(null)); + } + + @Test + public void constructor_invalidNextOfKinPhone_throwsIllegalArgumentException() { + String invalidPhone = ""; + assertThrows(IllegalArgumentException.class, () -> new NextOfKinPhone(invalidPhone)); + } + + @Test + public void isValidNextOfKinPhone() { + // null phone number + assertThrows(NullPointerException.class, () -> NextOfKinPhone.isValidPhone(null)); + + // invalid phone numbers + assertFalse(NextOfKinPhone.isValidPhone("")); // empty string + assertFalse(NextOfKinPhone.isValidPhone(" ")); // spaces only + assertFalse(NextOfKinPhone.isValidPhone("91")); // less than 3 numbers + assertFalse(NextOfKinPhone.isValidPhone("phone")); // non-numeric + assertFalse(NextOfKinPhone.isValidPhone("9011p041")); // alphabets within digits + assertFalse(NextOfKinPhone.isValidPhone("9312 1534")); // spaces within digits + + // valid phone numbers + assertTrue(NextOfKinPhone.isValidPhone("911")); // exactly 3 numbers + assertTrue(NextOfKinPhone.isValidPhone("93121534")); + assertTrue(NextOfKinPhone.isValidPhone("124293842033123")); // long phone numbers + } + + @Test + public void equals() { + NextOfKinPhone phone = new NextOfKinPhone("999"); + + // same values -> returns true + assertTrue(phone.equals(new NextOfKinPhone("999"))); + + // same object -> returns true + assertTrue(phone.equals(phone)); + + // null -> returns false + assertFalse(phone.equals(null)); + + // different types -> returns false + assertFalse(phone.equals(5.0f)); + + // different values -> returns false + assertFalse(phone.equals(new NextOfKinPhone("995"))); + } +} diff --git a/src/test/java/seedu/address/model/person/PersonTest.java b/src/test/java/seedu/address/model/person/PersonTest.java index 31a10d156c9..8f48f3fb054 100644 --- a/src/test/java/seedu/address/model/person/PersonTest.java +++ b/src/test/java/seedu/address/model/person/PersonTest.java @@ -4,16 +4,26 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static seedu.address.logic.commands.CommandTestUtil.VALID_ADDRESS_BOB; +import static seedu.address.logic.commands.CommandTestUtil.VALID_APPOINTMENT_DATE; +import static seedu.address.logic.commands.CommandTestUtil.VALID_APPOINTMENT_NAME; import static seedu.address.logic.commands.CommandTestUtil.VALID_EMAIL_BOB; +import static seedu.address.logic.commands.CommandTestUtil.VALID_FINANCIAL_PLAN_1; import static seedu.address.logic.commands.CommandTestUtil.VALID_NAME_BOB; +import static seedu.address.logic.commands.CommandTestUtil.VALID_NEXT_OF_KIN_NAME_BOB; +import static seedu.address.logic.commands.CommandTestUtil.VALID_NEXT_OF_KIN_PHONE_BOB; import static seedu.address.logic.commands.CommandTestUtil.VALID_PHONE_BOB; import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_HUSBAND; import static seedu.address.testutil.Assert.assertThrows; import static seedu.address.testutil.TypicalPersons.ALICE; +import static seedu.address.testutil.TypicalPersons.BENSON; import static seedu.address.testutil.TypicalPersons.BOB; +import static seedu.address.testutil.TypicalPersons.ELLE; + +import java.time.LocalDate; import org.junit.jupiter.api.Test; +import seedu.address.model.financialplan.FinancialPlan; import seedu.address.testutil.PersonBuilder; public class PersonTest { @@ -34,7 +44,8 @@ public void isSamePerson() { // same name, all other attributes different -> returns true Person editedAlice = new PersonBuilder(ALICE).withPhone(VALID_PHONE_BOB).withEmail(VALID_EMAIL_BOB) - .withAddress(VALID_ADDRESS_BOB).withTags(VALID_TAG_HUSBAND).build(); + .withAddress(VALID_ADDRESS_BOB).withNextOfKinName(VALID_NEXT_OF_KIN_NAME_BOB) + .withNextOfKinPhone(VALID_NEXT_OF_KIN_PHONE_BOB).withTags(VALID_TAG_HUSBAND).build(); assertTrue(ALICE.isSamePerson(editedAlice)); // different name, all other attributes same -> returns false @@ -85,15 +96,126 @@ public void equals() { editedAlice = new PersonBuilder(ALICE).withAddress(VALID_ADDRESS_BOB).build(); assertFalse(ALICE.equals(editedAlice)); + // different nextOfKinName -> returns false + editedAlice = new PersonBuilder(ALICE).withNextOfKinName(VALID_NEXT_OF_KIN_NAME_BOB).build(); + assertFalse(ALICE.equals(editedAlice)); + + // different nextOfKinPhone -> returns false + editedAlice = new PersonBuilder(ALICE).withNextOfKinPhone(VALID_NEXT_OF_KIN_PHONE_BOB).build(); + assertFalse(ALICE.equals(editedAlice)); + // different tags -> returns false editedAlice = new PersonBuilder(ALICE).withTags(VALID_TAG_HUSBAND).build(); assertFalse(ALICE.equals(editedAlice)); + + // different Appointment -> returns false + Person editedBenson = new PersonBuilder(BENSON).withAppointment(VALID_APPOINTMENT_NAME + + ", " + VALID_APPOINTMENT_DATE).build(); + assertFalse(BENSON.equals(editedBenson)); + } + + @Test + public void gatherEmailsByFinancialPlan_noPersonFound() { + assertEquals(new String(), ELLE.gatherEmailsContainsFinancialPlan(VALID_FINANCIAL_PLAN_1)); + } + + @Test + public void gatherEmailsByFinancialPlan_personFound() { + // prompt is full plan name + FinancialPlan elleFinancialPlan = ELLE.getFinancialPlans().iterator().next(); + String prompt = elleFinancialPlan.toString().replaceAll("[\\[\\]\\(\\)]", ""); + String prompt2 = "Sample Financial Plan 2"; + assertEquals(ELLE.getEmail().toString(), ELLE.gatherEmailsContainsFinancialPlan(prompt)); + assertEquals(ELLE.getEmail().toString(), ELLE.gatherEmailsContainsFinancialPlan(prompt2)); + } + + @Test + public void gatherEmailsByFinancialPlan_substring_personFound() { + String substring = "Financial Plan 2"; + assertEquals(ELLE.getEmail().toString(), ELLE.gatherEmailsContainsFinancialPlan(substring)); + } + + @Test + public void gatherEmailsByFinancialPlan_noDuplicateEmail() { + // prompt is substring for both bobs Financial Plans: "financial plan 1" and "financial plan 2" + String substringBob = "financial plan"; + assertEquals(BOB.getEmail().toString(), BOB.gatherEmailsContainsFinancialPlan(substringBob)); + } + + @Test + public void gatherEmailsByFinancialPlan_caseInsensitive() { + String uppercaseBob = VALID_FINANCIAL_PLAN_1.toUpperCase(); + String lowercaseBob = VALID_FINANCIAL_PLAN_1.toLowerCase(); + assertEquals(BOB.getEmail().toString(), BOB.gatherEmailsContainsFinancialPlan(uppercaseBob)); + assertEquals(BOB.getEmail().toString(), BOB.gatherEmailsContainsFinancialPlan(lowercaseBob)); + } + + @Test + public void gatherByTag_noPersonFound() { + assertEquals(new String(), ALICE.gatherEmailsContainsTag(VALID_TAG_HUSBAND)); + } + + @Test + public void gatherByTag_personFound() { + // prompt full tag name + assertEquals(BOB.getEmail().toString(), BOB.gatherEmailsContainsTag(VALID_TAG_HUSBAND)); + } + + @Test + public void gatherByTag_substring_personFound() { + // prompt substring + String substring = "hus"; + assertEquals(BOB.getEmail().toString(), BOB.gatherEmailsContainsTag(substring)); + } + + @Test + public void gatherEmailsByTag_noDuplicateEmail() { + // prompt is substring for both bobs tags "friend" and "husband" + String substringBob = "nd"; + assertEquals(BOB.getEmail().toString(), BOB.gatherEmailsContainsTag(substringBob)); + } + + @Test + public void gatherEmailsByTag_caseInsensitive() { + String uppercaseBob = VALID_TAG_HUSBAND.toUpperCase(); + String lowercaseBob = VALID_TAG_HUSBAND.toLowerCase(); + assertEquals(BOB.getEmail().toString(), BOB.gatherEmailsContainsTag(uppercaseBob)); + assertEquals(BOB.getEmail().toString(), BOB.gatherEmailsContainsTag(lowercaseBob)); } @Test public void toStringMethod() { String expected = Person.class.getCanonicalName() + "{name=" + ALICE.getName() + ", phone=" + ALICE.getPhone() - + ", email=" + ALICE.getEmail() + ", address=" + ALICE.getAddress() + ", tags=" + ALICE.getTags() + "}"; + + ", email=" + ALICE.getEmail() + ", address=" + ALICE.getAddress() + ", nextOfKinName=" + + ALICE.getNextOfKinName() + ", nextOfKinPhone=" + ALICE.getNextOfKinPhone() + + ", financialPlans=" + ALICE.getFinancialPlans() + ", tags=" + ALICE.getTags() + + ", appointment=" + ALICE.getAppointment() + "}"; assertEquals(expected, ALICE.toString()); } + + @Test + public void isSameAppointmentDate() { + LocalDate matchingDate = LocalDate.of(2023, 05, 01); + LocalDate nonMatchingDate = LocalDate.of(2023, 02, 01); + + assertTrue(BENSON.isSameAppointmentDate(matchingDate)); + assertFalse(BENSON.isSameAppointmentDate(nonMatchingDate)); + } + + @Test + public void hasNullAppointment() { + assertFalse(BENSON.hasNullAppointment()); + assertTrue(ALICE.hasNullAppointment()); + } + + @Test + public void clearAppointment() { + Person editedBenson = new PersonBuilder().withName("Benson Meier") + .withAddress("311, Clementi Ave 2, #02-25").withEmail("johnd@example.com") + .withPhone("98765432").withNextOfKinName("Benson Dad").withNextOfKinPhone("98761111") + .withFinancialPlans("Sample Financial Plan 1", "Sample Financial Plan 2") + .withTags("owesMoney", "friends").withNullAppointment().build(); + + assertEquals(BENSON.clearAppointment(), editedBenson); + } } diff --git a/src/test/java/seedu/address/model/person/SortByAppointmentComparatorTest.java b/src/test/java/seedu/address/model/person/SortByAppointmentComparatorTest.java new file mode 100644 index 00000000000..57655f44dd0 --- /dev/null +++ b/src/test/java/seedu/address/model/person/SortByAppointmentComparatorTest.java @@ -0,0 +1,36 @@ +package seedu.address.model.person; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +import seedu.address.model.person.comparator.SortByAppointmentComparator; + +public class SortByAppointmentComparatorTest { + @Test + public void equals() { + SortByAppointmentComparator firstComparator = new SortByAppointmentComparator(); + SortByAppointmentComparator secondComparator = new SortByAppointmentComparator(); + + // same object -> returns true + assertTrue(firstComparator.equals(firstComparator)); + + // different types -> returns false + assertFalse(firstComparator.equals(1)); + + // null -> returns false + assertFalse(firstComparator.equals(null)); + + // different comparators -> returns true + assertTrue(firstComparator.equals(secondComparator)); + } + @Test + public void toStringMethod() { + SortByAppointmentComparator comparator = new SortByAppointmentComparator(); + + String expected = SortByAppointmentComparator.class.getCanonicalName() + "{}"; + assertEquals(expected, comparator.toString()); + } +} diff --git a/src/test/java/seedu/address/model/person/SortByNameComparatorTest.java b/src/test/java/seedu/address/model/person/SortByNameComparatorTest.java new file mode 100644 index 00000000000..b1400d38aaa --- /dev/null +++ b/src/test/java/seedu/address/model/person/SortByNameComparatorTest.java @@ -0,0 +1,36 @@ +package seedu.address.model.person; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +import seedu.address.model.person.comparator.SortByNameComparator; + +public class SortByNameComparatorTest { + @Test + public void equals() { + SortByNameComparator firstComparator = new SortByNameComparator(); + SortByNameComparator secondComparator = new SortByNameComparator(); + + // same object -> returns true + assertTrue(firstComparator.equals(firstComparator)); + + // different types -> returns false + assertFalse(firstComparator.equals(1)); + + // null -> returns false + assertFalse(firstComparator.equals(null)); + + // different comparators -> returns true + assertTrue(firstComparator.equals(secondComparator)); + } + @Test + public void toStringMethod() { + SortByNameComparator comparator = new SortByNameComparator(); + + String expected = SortByNameComparator.class.getCanonicalName() + "{}"; + assertEquals(expected, comparator.toString()); + } +} diff --git a/src/test/java/seedu/address/model/person/UniquePersonListTest.java b/src/test/java/seedu/address/model/person/UniquePersonListTest.java index 17ae501df08..afa60b2547e 100644 --- a/src/test/java/seedu/address/model/person/UniquePersonListTest.java +++ b/src/test/java/seedu/address/model/person/UniquePersonListTest.java @@ -8,6 +8,7 @@ import static seedu.address.testutil.Assert.assertThrows; import static seedu.address.testutil.TypicalPersons.ALICE; import static seedu.address.testutil.TypicalPersons.BOB; +import static seedu.address.testutil.TypicalPersons.ELLE; import java.util.Arrays; import java.util.Collections; @@ -15,8 +16,11 @@ import org.junit.jupiter.api.Test; +import seedu.address.model.financialplan.FinancialPlan; import seedu.address.model.person.exceptions.DuplicatePersonException; import seedu.address.model.person.exceptions.PersonNotFoundException; +import seedu.address.model.person.gatheremail.GatherEmailByFinancialPlan; +import seedu.address.model.person.gatheremail.GatherEmailByTag; import seedu.address.testutil.PersonBuilder; public class UniquePersonListTest { @@ -162,6 +166,36 @@ public void setPersons_listWithDuplicatePersons_throwsDuplicatePersonException() assertThrows(DuplicatePersonException.class, () -> uniquePersonList.setPersons(listWithDuplicatePersons)); } + @Test + public void gatherEmailsByFinancialPlan_noPersonFound() { + uniquePersonList.add(ALICE); + GatherEmailByFinancialPlan prompt = new GatherEmailByFinancialPlan("Sample Financial Plan 3"); + assertEquals(new String(), uniquePersonList.gatherEmails(prompt)); + } + + @Test + public void gatherEmailsByFinancialPlan_personFound() { + uniquePersonList.add(ELLE); + FinancialPlan elleFinancialPlan = ELLE.getFinancialPlans().iterator().next(); + String fpDescription = elleFinancialPlan.toString().replaceAll("[\\[\\]\\(\\)]", ""); + GatherEmailByFinancialPlan prompt = new GatherEmailByFinancialPlan(fpDescription); + assertEquals(ELLE.getEmail().toString() + ";", uniquePersonList.gatherEmails(prompt)); + } + + @Test + public void gatherByTag_noPersonFound() { + uniquePersonList.add(ALICE); + GatherEmailByTag prompt = new GatherEmailByTag(VALID_TAG_HUSBAND); + assertEquals(new String(), uniquePersonList.gatherEmails(prompt)); + } + + @Test + public void gatherByTag_personFound() { + uniquePersonList.add(BOB); + GatherEmailByTag prompt = new GatherEmailByTag(VALID_TAG_HUSBAND); + assertEquals(BOB.getEmail().toString() + ";", uniquePersonList.gatherEmails(prompt)); + } + @Test public void asUnmodifiableObservableList_modifyList_throwsUnsupportedOperationException() { assertThrows(UnsupportedOperationException.class, () @@ -172,4 +206,10 @@ public void asUnmodifiableObservableList_modifyList_throwsUnsupportedOperationEx public void toStringMethod() { assertEquals(uniquePersonList.asUnmodifiableObservableList().toString(), uniquePersonList.toString()); } + + @Test + public void equals() { + assertEquals(uniquePersonList, uniquePersonList); + assertFalse(uniquePersonList.equals(null)); + } } diff --git a/src/test/java/seedu/address/model/person/gatheremail/GatherEmailByFinancialPlanTest.java b/src/test/java/seedu/address/model/person/gatheremail/GatherEmailByFinancialPlanTest.java new file mode 100644 index 00000000000..a1b36f29ce7 --- /dev/null +++ b/src/test/java/seedu/address/model/person/gatheremail/GatherEmailByFinancialPlanTest.java @@ -0,0 +1,50 @@ +package seedu.address.model.person.gatheremail; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.commands.CommandTestUtil.VALID_FINANCIAL_PLAN_1; +import static seedu.address.testutil.TypicalPersons.BOB; +import static seedu.address.testutil.TypicalPersons.ELLE; + +import org.junit.jupiter.api.Test; + +public class GatherEmailByFinancialPlanTest { + + @Test + public void gatherEmails() { + GatherEmailByFinancialPlan fpPrompt = new GatherEmailByFinancialPlan(VALID_FINANCIAL_PLAN_1); + assertEquals(new String(), fpPrompt.gatherEmails(ELLE)); + assertEquals(BOB.getEmail().toString(), fpPrompt.gatherEmails(BOB)); + } + + @Test + public void testEquals() { + GatherEmailByFinancialPlan first = new GatherEmailByFinancialPlan("first"); + GatherEmailByFinancialPlan second = new GatherEmailByFinancialPlan("second"); + GatherEmailByFinancialPlan firstOther = new GatherEmailByFinancialPlan("first"); + + // same object -> returns true + assertTrue(first.equals(first)); + + // different types -> returns false + assertFalse(first.equals(1)); + + // null -> returns false + assertFalse(first.equals(null)); + + // different object -> returns false + assertFalse(first.equals(second)); + + // different object -> returns true + assertTrue(first.equals(firstOther)); + } + + @Test + public void testToString() { + String prompt = "prompt"; + String expected = "Financial Plan: " + prompt; + GatherEmailByFinancialPlan fpPrompt = new GatherEmailByFinancialPlan(prompt); + assertEquals(fpPrompt.toString(), expected); + } +} diff --git a/src/test/java/seedu/address/model/person/gatheremail/GatherEmailByTagTest.java b/src/test/java/seedu/address/model/person/gatheremail/GatherEmailByTagTest.java new file mode 100644 index 00000000000..4ff3728582a --- /dev/null +++ b/src/test/java/seedu/address/model/person/gatheremail/GatherEmailByTagTest.java @@ -0,0 +1,50 @@ +package seedu.address.model.person.gatheremail; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_HUSBAND; +import static seedu.address.testutil.TypicalPersons.ALICE; +import static seedu.address.testutil.TypicalPersons.BOB; + +import org.junit.jupiter.api.Test; + +public class GatherEmailByTagTest { + + @Test + public void gatherEmails() { + GatherEmailByTag tagPrompt = new GatherEmailByTag(VALID_TAG_HUSBAND); + assertEquals(new String(), tagPrompt.gatherEmails(ALICE)); + assertEquals(BOB.getEmail().toString(), tagPrompt.gatherEmails(BOB)); + } + + @Test + void testEquals() { + GatherEmailByTag first = new GatherEmailByTag("first"); + GatherEmailByTag second = new GatherEmailByTag("second"); + GatherEmailByTag firstOther = new GatherEmailByTag("first"); + + // same object -> returns true + assertTrue(first.equals(first)); + + // different types -> returns false + assertFalse(first.equals(1)); + + // null -> returns false + assertFalse(first.equals(null)); + + // different object -> returns false + assertFalse(first.equals(second)); + + // different object -> returns true + assertTrue(first.equals(firstOther)); + } + + @Test + public void testToString() { + String prompt = "prompt"; + String expected = "Tag: " + prompt; + GatherEmailByTag tagPrompt = new GatherEmailByTag(prompt); + assertEquals(tagPrompt.toString(), expected); + } +} diff --git a/src/test/java/seedu/address/model/person/predicates/CombinedPredicateTest.java b/src/test/java/seedu/address/model/person/predicates/CombinedPredicateTest.java new file mode 100644 index 00000000000..caa366e3b2f --- /dev/null +++ b/src/test/java/seedu/address/model/person/predicates/CombinedPredicateTest.java @@ -0,0 +1,115 @@ +package seedu.address.model.person.predicates; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.testutil.Assert.assertThrows; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import seedu.address.testutil.PersonBuilder; + +public class CombinedPredicateTest { + @Test + public void constructor_containsNull_throwsException() { + assertThrows(NullPointerException.class, () -> + new CombinedPredicate(new FinancialPlanContainsKeywordsPredicate(Arrays.asList("foo")), + null)); + } + @Test + public void equals() { + List firstPredicateKeywordList = Collections.singletonList("first"); + List secondPredicateKeywordList = Arrays.asList("first", "second"); + + CombinedPredicate firstPredicate = + new CombinedPredicate(new FinancialPlanContainsKeywordsPredicate(firstPredicateKeywordList)); + CombinedPredicate secondPredicate = + new CombinedPredicate(new FinancialPlanContainsKeywordsPredicate(secondPredicateKeywordList)); + + // same object -> returns true + assertTrue(firstPredicate.equals(firstPredicate)); + + // same values -> returns true + CombinedPredicate firstPredicateCopy = + new CombinedPredicate(new FinancialPlanContainsKeywordsPredicate(firstPredicateKeywordList)); + assertTrue(firstPredicate.equals(firstPredicateCopy)); + + // different types -> returns false + assertFalse(firstPredicate.equals(1)); + + // null -> returns false + assertFalse(firstPredicate.equals(null)); + + // different financial plan -> returns false + assertFalse(firstPredicate.equals(secondPredicate)); + } + + @Test + public void test_personContainsKeywords_returnsTrue() { + // One keyword + CombinedPredicate predicate = + new CombinedPredicate( + new FinancialPlanContainsKeywordsPredicate(Collections.singletonList("Senior"))); + assertTrue(predicate.test(new PersonBuilder().withFinancialPlans("Senior").build())); + + // Multiple keywords for same person + predicate = new CombinedPredicate(new NameContainsKeywordsPredicate(Arrays.asList("Alice", "Bob"))); + assertTrue(predicate.test(new PersonBuilder().withName("Alice Bob").build())); + + // Multiple keywords in different fields + predicate = new CombinedPredicate( + new FinancialPlanContainsKeywordsPredicate(Collections.singletonList("Senior")), + new NameContainsKeywordsPredicate(Collections.singletonList("Alice"))); + assertTrue(predicate.test(new PersonBuilder().withName("Alice").withFinancialPlans("Senior").build())); + + // Only one matching keyword + predicate = new CombinedPredicate( + new FinancialPlanContainsKeywordsPredicate(Collections.singletonList("Senior")), + new NameContainsKeywordsPredicate(Collections.singletonList("Alice"))); + assertTrue(predicate.test(new PersonBuilder().withFinancialPlans("Senior", "Koopa").build())); + + // Mixed-case keywords + predicate = new CombinedPredicate( + new FinancialPlanContainsKeywordsPredicate(Collections.singletonList("chILD"))); + assertTrue(predicate.test(new PersonBuilder().withName("Alice").withFinancialPlans("Child Premium").build())); + } + + @Test + public void test_personDoesNotContainKeywords_returnsFalse() { + // Zero keywords + CombinedPredicate predicate = new CombinedPredicate(); + assertFalse(predicate.test(new PersonBuilder().withFinancialPlans("Child").build())); + + // Non-matching keyword + predicate = new CombinedPredicate(new FinancialPlanContainsKeywordsPredicate(Arrays.asList("Senior"))); + assertFalse(predicate.test(new PersonBuilder().withFinancialPlans("Child Premium").build())); + + // Keywords match phone and address, but does not match financial plans + predicate = new CombinedPredicate(new FinancialPlanContainsKeywordsPredicate( + Arrays.asList("12345", "Main", "Street"))); + assertFalse(predicate.test(new PersonBuilder().withFinancialPlans("Child").withPhone("12345") + .withAddress("Main Street").build())); + } + + @Test + public void toStringMethod() { + List keywords = List.of("keyword1", "keyword2"); + NameContainsKeywordsPredicate nameContainsKeywordsPredicate = new NameContainsKeywordsPredicate(keywords); + FinancialPlanContainsKeywordsPredicate financialPlanContainsKeywordsPredicate = + new FinancialPlanContainsKeywordsPredicate(keywords); + CombinedPredicate predicate = new CombinedPredicate(nameContainsKeywordsPredicate, + financialPlanContainsKeywordsPredicate); + + String expected = CombinedPredicate.class.getCanonicalName() + "{" + + nameContainsKeywordsPredicate.getClass().getCanonicalName() + + "=" + nameContainsKeywordsPredicate + ", " + + financialPlanContainsKeywordsPredicate.getClass().getCanonicalName() + + "=" + financialPlanContainsKeywordsPredicate + + "}"; + assertEquals(expected, predicate.toString()); + } +} diff --git a/src/test/java/seedu/address/model/person/predicates/FinancialPlanContainsKeywordsPredicateTest.java b/src/test/java/seedu/address/model/person/predicates/FinancialPlanContainsKeywordsPredicateTest.java new file mode 100644 index 00000000000..191cd35f8f4 --- /dev/null +++ b/src/test/java/seedu/address/model/person/predicates/FinancialPlanContainsKeywordsPredicateTest.java @@ -0,0 +1,95 @@ +package seedu.address.model.person.predicates; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import seedu.address.testutil.PersonBuilder; + +public class FinancialPlanContainsKeywordsPredicateTest { + @Test + public void equals() { + List firstPredicateKeywordList = Collections.singletonList("first"); + List secondPredicateKeywordList = Arrays.asList("first", "second"); + + FinancialPlanContainsKeywordsPredicate firstPredicate = + new FinancialPlanContainsKeywordsPredicate(firstPredicateKeywordList); + FinancialPlanContainsKeywordsPredicate secondPredicate = + new FinancialPlanContainsKeywordsPredicate(secondPredicateKeywordList); + + // same object -> returns true + assertTrue(firstPredicate.equals(firstPredicate)); + + // same values -> returns true + FinancialPlanContainsKeywordsPredicate firstPredicateCopy = + new FinancialPlanContainsKeywordsPredicate(firstPredicateKeywordList); + assertTrue(firstPredicate.equals(firstPredicateCopy)); + + // different types -> returns false + assertFalse(firstPredicate.equals(1)); + + // null -> returns false + assertFalse(firstPredicate.equals(null)); + + // different financial plan -> returns false + assertFalse(firstPredicate.equals(secondPredicate)); + } + + @Test + public void test_financialPlansContainKeywords_returnsTrue() { + // One keyword + FinancialPlanContainsKeywordsPredicate predicate = + new FinancialPlanContainsKeywordsPredicate(Collections.singletonList("Senior")); + assertTrue(predicate.test(new PersonBuilder().withFinancialPlans("Senior").build())); + + // Multiple keywords in same financial plan + predicate = new FinancialPlanContainsKeywordsPredicate(Arrays.asList("Senior", "Premium")); + assertTrue(predicate.test(new PersonBuilder().withFinancialPlans("Senior Premium").build())); + + // Multiple keywords in different financial plans + predicate = new FinancialPlanContainsKeywordsPredicate(Arrays.asList("Senior", "Premium")); + assertTrue(predicate.test(new PersonBuilder().withFinancialPlans("Senior", "Premium").build())); + + // Only one matching keyword + predicate = new FinancialPlanContainsKeywordsPredicate(Arrays.asList("Child", "Premium")); + assertTrue(predicate.test(new PersonBuilder().withFinancialPlans("Child", "Koopa").build())); + + // Mixed-case keywords + predicate = new FinancialPlanContainsKeywordsPredicate(Arrays.asList("cHIld", "pREmium")); + assertTrue(predicate.test(new PersonBuilder().withFinancialPlans("Child Premium").build())); + } + + @Test + public void test_financialPlansDoNotContainKeywords_returnsFalse() { + // Zero keywords + FinancialPlanContainsKeywordsPredicate predicate = + new FinancialPlanContainsKeywordsPredicate(Collections.emptyList()); + assertFalse(predicate.test(new PersonBuilder().withFinancialPlans("Child").build())); + + // Non-matching keyword + predicate = new FinancialPlanContainsKeywordsPredicate(Arrays.asList("Senior")); + assertFalse(predicate.test(new PersonBuilder().withFinancialPlans("Child Premium").build())); + + // Keywords match phone and address, but does not match financial plans + predicate = new FinancialPlanContainsKeywordsPredicate( + Arrays.asList("12345", "Main", "Street")); + assertFalse(predicate.test(new PersonBuilder().withFinancialPlans("Child").withPhone("12345") + .withAddress("Main Street").build())); + } + + @Test + public void toStringMethod() { + List keywords = List.of("keyword1", "keyword2"); + FinancialPlanContainsKeywordsPredicate predicate = new FinancialPlanContainsKeywordsPredicate(keywords); + + String expected = FinancialPlanContainsKeywordsPredicate.class.getCanonicalName() + + "{keywords=" + keywords + "}"; + assertEquals(expected, predicate.toString()); + } +} diff --git a/src/test/java/seedu/address/model/person/NameContainsKeywordsPredicateTest.java b/src/test/java/seedu/address/model/person/predicates/NameContainsKeywordsPredicateTest.java similarity index 92% rename from src/test/java/seedu/address/model/person/NameContainsKeywordsPredicateTest.java rename to src/test/java/seedu/address/model/person/predicates/NameContainsKeywordsPredicateTest.java index 6b3fd90ade7..b2c3e12bc81 100644 --- a/src/test/java/seedu/address/model/person/NameContainsKeywordsPredicateTest.java +++ b/src/test/java/seedu/address/model/person/predicates/NameContainsKeywordsPredicateTest.java @@ -1,4 +1,4 @@ -package seedu.address.model.person; +package seedu.address.model.person.predicates; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -68,10 +68,10 @@ public void test_nameDoesNotContainKeywords_returnsFalse() { predicate = new NameContainsKeywordsPredicate(Arrays.asList("Carol")); assertFalse(predicate.test(new PersonBuilder().withName("Alice Bob").build())); - // Keywords match phone, email and address, but does not match name - predicate = new NameContainsKeywordsPredicate(Arrays.asList("12345", "alice@email.com", "Main", "Street")); - assertFalse(predicate.test(new PersonBuilder().withName("Alice").withPhone("12345") - .withEmail("alice@email.com").withAddress("Main Street").build())); + // Keywords match next-of-kin and address, but does not match name + predicate = new NameContainsKeywordsPredicate(Arrays.asList("Baller", "Main", "Street")); + assertFalse(predicate.test(new PersonBuilder().withName("Alice").withNextOfKinName("Baller") + .withAddress("Main Street").build())); } @Test diff --git a/src/test/java/seedu/address/model/person/predicates/TagContainsKeywordsPredicateTest.java b/src/test/java/seedu/address/model/person/predicates/TagContainsKeywordsPredicateTest.java new file mode 100644 index 00000000000..a26708241f4 --- /dev/null +++ b/src/test/java/seedu/address/model/person/predicates/TagContainsKeywordsPredicateTest.java @@ -0,0 +1,88 @@ +package seedu.address.model.person.predicates; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import seedu.address.testutil.PersonBuilder; + +public class TagContainsKeywordsPredicateTest { + @Test + public void equals() { + List firstPredicateKeywordList = Collections.singletonList("first"); + List secondPredicateKeywordList = Arrays.asList("first", "second"); + + TagContainsKeywordsPredicate firstPredicate = new TagContainsKeywordsPredicate(firstPredicateKeywordList); + TagContainsKeywordsPredicate secondPredicate = new TagContainsKeywordsPredicate(secondPredicateKeywordList); + + // same object -> returns true + assertTrue(firstPredicate.equals(firstPredicate)); + + // same values -> returns true + TagContainsKeywordsPredicate firstPredicateCopy = new TagContainsKeywordsPredicate(firstPredicateKeywordList); + assertTrue(firstPredicate.equals(firstPredicateCopy)); + + // different types -> returns false + assertFalse(firstPredicate.equals(1)); + + // null -> returns false + assertFalse(firstPredicate.equals(null)); + + // different tag -> returns false + assertFalse(firstPredicate.equals(secondPredicate)); + } + + @Test + public void test_tagsContainKeywords_returnsTrue() { + // One keyword + TagContainsKeywordsPredicate predicate = new TagContainsKeywordsPredicate(Collections.singletonList("Senior")); + assertTrue(predicate.test(new PersonBuilder().withTags("Senior").build())); + + // Multiple keywords in same tag + predicate = new TagContainsKeywordsPredicate(Arrays.asList("Senior", "Premium")); + assertTrue(predicate.test(new PersonBuilder().withTags("SeniorPremium").build())); + + // Multiple keywords in different tags + predicate = new TagContainsKeywordsPredicate(Arrays.asList("Senior", "Premium")); + assertTrue(predicate.test(new PersonBuilder().withTags("Senior", "Premium").build())); + + // Only one matching keyword + predicate = new TagContainsKeywordsPredicate(Arrays.asList("Child", "Premium")); + assertTrue(predicate.test(new PersonBuilder().withTags("Child", "Koopa").build())); + + // Mixed-case keywords + predicate = new TagContainsKeywordsPredicate(Arrays.asList("cHIld", "pREmium")); + assertTrue(predicate.test(new PersonBuilder().withTags("ChildPremium").build())); + } + + @Test + public void test_tagsDoNotContainKeywords_returnsFalse() { + // Zero keywords + TagContainsKeywordsPredicate predicate = new TagContainsKeywordsPredicate(Collections.emptyList()); + assertFalse(predicate.test(new PersonBuilder().withTags("Child").build())); + + // Non-matching keyword + predicate = new TagContainsKeywordsPredicate(Arrays.asList("Senior")); + assertFalse(predicate.test(new PersonBuilder().withTags("ChildPremium").build())); + + // Keywords match phone and address, but does not match tags + predicate = new TagContainsKeywordsPredicate(Arrays.asList("12345", "Main", "Street")); + assertFalse(predicate.test(new PersonBuilder().withTags("Child").withPhone("12345") + .withAddress("Main Street").build())); + } + + @Test + public void toStringMethod() { + List keywords = List.of("keyword1", "keyword2"); + TagContainsKeywordsPredicate predicate = new TagContainsKeywordsPredicate(keywords); + + String expected = TagContainsKeywordsPredicate.class.getCanonicalName() + "{keywords=" + keywords + "}"; + assertEquals(expected, predicate.toString()); + } +} diff --git a/src/test/java/seedu/address/model/tag/TagTest.java b/src/test/java/seedu/address/model/tag/TagTest.java index 64d07d79ee2..b7b49ff1781 100644 --- a/src/test/java/seedu/address/model/tag/TagTest.java +++ b/src/test/java/seedu/address/model/tag/TagTest.java @@ -1,5 +1,8 @@ package seedu.address.model.tag; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_HUSBAND; import static seedu.address.testutil.Assert.assertThrows; import org.junit.jupiter.api.Test; @@ -17,6 +20,20 @@ public void constructor_invalidTagName_throwsIllegalArgumentException() { assertThrows(IllegalArgumentException.class, () -> new Tag(invalidTagName)); } + @Test + public void containsSubstring_failure() { + Tag tag = new Tag(VALID_TAG_HUSBAND); + String substring = "friend"; + assertFalse(tag.containsSubstring(substring)); + } + + @Test + public void containsSubstring_success() { + Tag tag = new Tag(VALID_TAG_HUSBAND); + String substring = "hus"; + assertTrue(tag.containsSubstring(substring)); + } + @Test public void isValidTagName() { // null tag name diff --git a/src/test/java/seedu/address/storage/JsonAdaptedPersonTest.java b/src/test/java/seedu/address/storage/JsonAdaptedPersonTest.java index 83b11331cdb..496b63a419d 100644 --- a/src/test/java/seedu/address/storage/JsonAdaptedPersonTest.java +++ b/src/test/java/seedu/address/storage/JsonAdaptedPersonTest.java @@ -15,6 +15,8 @@ import seedu.address.model.person.Address; import seedu.address.model.person.Email; import seedu.address.model.person.Name; +import seedu.address.model.person.NextOfKinName; +import seedu.address.model.person.NextOfKinPhone; import seedu.address.model.person.Phone; public class JsonAdaptedPersonTest { @@ -22,12 +24,22 @@ public class JsonAdaptedPersonTest { private static final String INVALID_PHONE = "+651234"; private static final String INVALID_ADDRESS = " "; private static final String INVALID_EMAIL = "example.com"; + private static final String INVALID_NEXT_OF_KIN_NAME = "R@chel"; + private static final String INVALID_NEXT_OF_KIN_PHONE = "+651234"; private static final String INVALID_TAG = "#friend"; + private static final String INVALID_FINANCIAL_PLAN = "#plan"; private static final String VALID_NAME = BENSON.getName().toString(); private static final String VALID_PHONE = BENSON.getPhone().toString(); private static final String VALID_EMAIL = BENSON.getEmail().toString(); private static final String VALID_ADDRESS = BENSON.getAddress().toString(); + private static final String VALID_NEXT_OF_KIN_NAME = BENSON.getNextOfKinName().toString(); + private static final String VALID_NEXT_OF_KIN_PHONE = BENSON.getNextOfKinPhone().toString(); + private static final JsonAdaptedAppointment VALID_APPOINTMENT = new JsonAdaptedAppointment( + BENSON.getAppointment().toString()); + private static final List VALID_FINANCIAL_PLANS = BENSON.getFinancialPlans().stream() + .map(JsonAdaptedFinancialPlan::new) + .collect(Collectors.toList()); private static final List VALID_TAGS = BENSON.getTags().stream() .map(JsonAdaptedTag::new) .collect(Collectors.toList()); @@ -41,14 +53,19 @@ public void toModelType_validPersonDetails_returnsPerson() throws Exception { @Test public void toModelType_invalidName_throwsIllegalValueException() { JsonAdaptedPerson person = - new JsonAdaptedPerson(INVALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, VALID_TAGS); + new JsonAdaptedPerson(INVALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, + VALID_NEXT_OF_KIN_NAME, VALID_NEXT_OF_KIN_PHONE, + VALID_FINANCIAL_PLANS, VALID_TAGS, VALID_APPOINTMENT); + String expectedMessage = Name.MESSAGE_CONSTRAINTS; assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); } @Test public void toModelType_nullName_throwsIllegalValueException() { - JsonAdaptedPerson person = new JsonAdaptedPerson(null, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, VALID_TAGS); + JsonAdaptedPerson person = new JsonAdaptedPerson(null, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, + VALID_NEXT_OF_KIN_NAME, VALID_NEXT_OF_KIN_PHONE, VALID_FINANCIAL_PLANS, VALID_TAGS, VALID_APPOINTMENT); + String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, Name.class.getSimpleName()); assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); } @@ -56,14 +73,20 @@ public void toModelType_nullName_throwsIllegalValueException() { @Test public void toModelType_invalidPhone_throwsIllegalValueException() { JsonAdaptedPerson person = - new JsonAdaptedPerson(VALID_NAME, INVALID_PHONE, VALID_EMAIL, VALID_ADDRESS, VALID_TAGS); + new JsonAdaptedPerson(VALID_NAME, INVALID_PHONE, VALID_EMAIL, VALID_ADDRESS, + VALID_NEXT_OF_KIN_NAME, VALID_NEXT_OF_KIN_PHONE, + VALID_FINANCIAL_PLANS, VALID_TAGS, VALID_APPOINTMENT); + String expectedMessage = Phone.MESSAGE_CONSTRAINTS; assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); } @Test public void toModelType_nullPhone_throwsIllegalValueException() { - JsonAdaptedPerson person = new JsonAdaptedPerson(VALID_NAME, null, VALID_EMAIL, VALID_ADDRESS, VALID_TAGS); + JsonAdaptedPerson person = new JsonAdaptedPerson(VALID_NAME, null, VALID_EMAIL, VALID_ADDRESS, + VALID_NEXT_OF_KIN_NAME, VALID_NEXT_OF_KIN_PHONE, + VALID_FINANCIAL_PLANS, VALID_TAGS, VALID_APPOINTMENT); + String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, Phone.class.getSimpleName()); assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); } @@ -71,14 +94,19 @@ public void toModelType_nullPhone_throwsIllegalValueException() { @Test public void toModelType_invalidEmail_throwsIllegalValueException() { JsonAdaptedPerson person = - new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, INVALID_EMAIL, VALID_ADDRESS, VALID_TAGS); + new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, INVALID_EMAIL, VALID_ADDRESS, + VALID_NEXT_OF_KIN_NAME, VALID_NEXT_OF_KIN_PHONE, + VALID_FINANCIAL_PLANS, VALID_TAGS, VALID_APPOINTMENT); + String expectedMessage = Email.MESSAGE_CONSTRAINTS; assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); } @Test public void toModelType_nullEmail_throwsIllegalValueException() { - JsonAdaptedPerson person = new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, null, VALID_ADDRESS, VALID_TAGS); + JsonAdaptedPerson person = new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, null, VALID_ADDRESS, + VALID_NEXT_OF_KIN_NAME, VALID_NEXT_OF_KIN_PHONE, VALID_FINANCIAL_PLANS, VALID_TAGS, VALID_APPOINTMENT); + String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, Email.class.getSimpleName()); assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); } @@ -86,25 +114,103 @@ public void toModelType_nullEmail_throwsIllegalValueException() { @Test public void toModelType_invalidAddress_throwsIllegalValueException() { JsonAdaptedPerson person = - new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, INVALID_ADDRESS, VALID_TAGS); + new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, INVALID_ADDRESS, + VALID_NEXT_OF_KIN_NAME, VALID_NEXT_OF_KIN_PHONE, + VALID_FINANCIAL_PLANS, VALID_TAGS, VALID_APPOINTMENT); + String expectedMessage = Address.MESSAGE_CONSTRAINTS; assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); } @Test public void toModelType_nullAddress_throwsIllegalValueException() { - JsonAdaptedPerson person = new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, null, VALID_TAGS); + JsonAdaptedPerson person = new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, null, + VALID_NEXT_OF_KIN_NAME, VALID_NEXT_OF_KIN_PHONE, VALID_FINANCIAL_PLANS, VALID_TAGS, VALID_APPOINTMENT); + String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, Address.class.getSimpleName()); assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); } + @Test + public void toModelType_invalidNextOfKinName_throwsIllegalValueException() { + JsonAdaptedPerson person = + new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, + INVALID_NEXT_OF_KIN_NAME, VALID_NEXT_OF_KIN_PHONE, + VALID_FINANCIAL_PLANS, VALID_TAGS, VALID_APPOINTMENT); + + String expectedMessage = NextOfKinName.MESSAGE_CONSTRAINTS; + assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); + } + + @Test + public void toModelType_nullNextOfKinName_throwsIllegalValueException() { + JsonAdaptedPerson person = new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, + null, VALID_NEXT_OF_KIN_PHONE, VALID_FINANCIAL_PLANS, VALID_TAGS, VALID_APPOINTMENT); + + String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, NextOfKinName.class.getSimpleName()); + assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); + } + + @Test + public void toModelType_invalidNextOfKinPhone_throwsIllegalValueException() { + JsonAdaptedPerson person = + new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, + VALID_NEXT_OF_KIN_NAME, INVALID_NEXT_OF_KIN_PHONE, + VALID_FINANCIAL_PLANS, VALID_TAGS, VALID_APPOINTMENT); + + String expectedMessage = NextOfKinPhone.MESSAGE_CONSTRAINTS; + assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); + } + @Test + public void toModelType_nullNextOfKinPhone_throwsIllegalValueException() { + JsonAdaptedPerson person = new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, + VALID_NEXT_OF_KIN_NAME, null, + VALID_FINANCIAL_PLANS, VALID_TAGS, VALID_APPOINTMENT); + + String expectedMessage = String.format(MISSING_FIELD_MESSAGE_FORMAT, NextOfKinPhone.class.getSimpleName()); + assertThrows(IllegalValueException.class, expectedMessage, person::toModelType); + } @Test public void toModelType_invalidTags_throwsIllegalValueException() { List invalidTags = new ArrayList<>(VALID_TAGS); invalidTags.add(new JsonAdaptedTag(INVALID_TAG)); JsonAdaptedPerson person = - new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, invalidTags); + new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, + VALID_NEXT_OF_KIN_NAME, VALID_NEXT_OF_KIN_PHONE, + VALID_FINANCIAL_PLANS, invalidTags, VALID_APPOINTMENT); + assertThrows(IllegalValueException.class, person::toModelType); + } + + @Test + public void toModelType_invalidFinancialPlans_throwsIllegalValueException() { + List invalidFinancialPlans = new ArrayList<>(VALID_FINANCIAL_PLANS); + invalidFinancialPlans.add(new JsonAdaptedFinancialPlan(INVALID_FINANCIAL_PLAN)); + JsonAdaptedPerson person = + new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, + VALID_NEXT_OF_KIN_NAME, VALID_NEXT_OF_KIN_PHONE, + invalidFinancialPlans, VALID_TAGS, VALID_APPOINTMENT); assertThrows(IllegalValueException.class, person::toModelType); } + @Test + public void toModelType_invalidAppointment_throwsIllegalValueException() { + String invalidAppointmentName = "Review Insur@nce, 01-01-2023 10:00"; + String invalidAppointmentDateTime = "Review Insurance, 01-01-2020"; + + JsonAdaptedAppointment invalidAppointment1 = new JsonAdaptedAppointment(invalidAppointmentName); + JsonAdaptedAppointment invalidAppointment2 = new JsonAdaptedAppointment(invalidAppointmentDateTime); + + JsonAdaptedPerson person1 = + new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, + VALID_NEXT_OF_KIN_NAME, VALID_NEXT_OF_KIN_PHONE, + VALID_FINANCIAL_PLANS, VALID_TAGS, invalidAppointment1); + + JsonAdaptedPerson person2 = + new JsonAdaptedPerson(VALID_NAME, VALID_PHONE, VALID_EMAIL, VALID_ADDRESS, + VALID_NEXT_OF_KIN_NAME, VALID_NEXT_OF_KIN_PHONE, + VALID_FINANCIAL_PLANS, VALID_TAGS, invalidAppointment2); + + assertThrows(IllegalValueException.class, person1::toModelType); + assertThrows(IllegalValueException.class, person2::toModelType); + } } diff --git a/src/test/java/seedu/address/testutil/EditPersonDescriptorBuilder.java b/src/test/java/seedu/address/testutil/EditPersonDescriptorBuilder.java index 4584bd5044e..578654935a7 100644 --- a/src/test/java/seedu/address/testutil/EditPersonDescriptorBuilder.java +++ b/src/test/java/seedu/address/testutil/EditPersonDescriptorBuilder.java @@ -5,9 +5,13 @@ import java.util.stream.Stream; import seedu.address.logic.commands.EditCommand.EditPersonDescriptor; +import seedu.address.model.appointment.Appointment; +import seedu.address.model.financialplan.FinancialPlan; import seedu.address.model.person.Address; import seedu.address.model.person.Email; import seedu.address.model.person.Name; +import seedu.address.model.person.NextOfKinName; +import seedu.address.model.person.NextOfKinPhone; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; import seedu.address.model.tag.Tag; @@ -36,7 +40,11 @@ public EditPersonDescriptorBuilder(Person person) { descriptor.setPhone(person.getPhone()); descriptor.setEmail(person.getEmail()); descriptor.setAddress(person.getAddress()); + descriptor.setNextOfKinName(person.getNextOfKinName()); + descriptor.setNextOfKinPhone(person.getNextOfKinPhone()); + descriptor.setFinancialPlans(person.getFinancialPlans()); descriptor.setTags(person.getTags()); + descriptor.setAppointment(person.getAppointment()); } /** @@ -71,6 +79,33 @@ public EditPersonDescriptorBuilder withAddress(String address) { return this; } + /** + * Sets the {@code NextOfKinName} of the {@code EditPersonDescriptor} that we are building. + */ + public EditPersonDescriptorBuilder withNextOfKinName(String nokName) { + descriptor.setNextOfKinName(new NextOfKinName(nokName)); + return this; + } + + /** + * Sets the {@code NextOfKinPhone} of the {@code EditPersonDescriptor} that we are building. + */ + public EditPersonDescriptorBuilder withNextOfKinPhone(String nokPhone) { + descriptor.setNextOfKinPhone(new NextOfKinPhone(nokPhone)); + return this; + } + + /** + * Parses the {@code financialPlans} into a {@code Set} and set it to + * the {@code EditPersonDescriptor} that we are building. + */ + public EditPersonDescriptorBuilder withFinancialPlans(String... financialPlans) { + Set financialPlanSet = Stream.of(financialPlans) + .map(FinancialPlan::new).collect(Collectors.toSet()); + descriptor.setFinancialPlans(financialPlanSet); + return this; + } + /** * Parses the {@code tags} into a {@code Set} and set it to the {@code EditPersonDescriptor} * that we are building. @@ -81,6 +116,16 @@ public EditPersonDescriptorBuilder withTags(String... tags) { return this; } + /** + * Parses the {@code appointmentDesc} into a {@code Appointment} and set it to the {@code EditPersonDescriptor} + * that we are building. + */ + public EditPersonDescriptorBuilder withAppointment(String appointmentDesc) { + Appointment appointment = Appointment.parseAppointmentDescription(appointmentDesc); + descriptor.setAppointment(appointment); + return this; + } + public EditPersonDescriptor build() { return descriptor; } diff --git a/src/test/java/seedu/address/testutil/PersonBuilder.java b/src/test/java/seedu/address/testutil/PersonBuilder.java index 6be381d39ba..d7ff8f157da 100644 --- a/src/test/java/seedu/address/testutil/PersonBuilder.java +++ b/src/test/java/seedu/address/testutil/PersonBuilder.java @@ -3,9 +3,15 @@ import java.util.HashSet; import java.util.Set; +import seedu.address.model.appointment.Appointment; +import seedu.address.model.appointment.NullAppointment; +import seedu.address.model.appointment.ScheduleItem; +import seedu.address.model.financialplan.FinancialPlan; import seedu.address.model.person.Address; import seedu.address.model.person.Email; import seedu.address.model.person.Name; +import seedu.address.model.person.NextOfKinName; +import seedu.address.model.person.NextOfKinPhone; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; import seedu.address.model.tag.Tag; @@ -20,12 +26,17 @@ public class PersonBuilder { public static final String DEFAULT_PHONE = "85355255"; public static final String DEFAULT_EMAIL = "amy@gmail.com"; public static final String DEFAULT_ADDRESS = "123, Jurong West Ave 6, #08-111"; - + public static final String DEFAULT_NEXT_OF_KIN_NAME = "Adam Bee"; + public static final String DEFAULT_NEXT_OF_KIN_PHONE = "85555255"; private Name name; private Phone phone; private Email email; private Address address; + private NextOfKinName nextOfKinName; + private NextOfKinPhone nextOfKinPhone; + private Set financialPlans; private Set tags; + private ScheduleItem appointment; /** * Creates a {@code PersonBuilder} with the default details. @@ -35,7 +46,11 @@ public PersonBuilder() { phone = new Phone(DEFAULT_PHONE); email = new Email(DEFAULT_EMAIL); address = new Address(DEFAULT_ADDRESS); + nextOfKinName = new NextOfKinName(DEFAULT_NEXT_OF_KIN_NAME); + nextOfKinPhone = new NextOfKinPhone(DEFAULT_NEXT_OF_KIN_PHONE); + financialPlans = new HashSet<>(); tags = new HashSet<>(); + appointment = NullAppointment.getNullAppointment(); } /** @@ -46,7 +61,11 @@ public PersonBuilder(Person personToCopy) { phone = personToCopy.getPhone(); email = personToCopy.getEmail(); address = personToCopy.getAddress(); + nextOfKinName = personToCopy.getNextOfKinName(); + nextOfKinPhone = personToCopy.getNextOfKinPhone(); + financialPlans = new HashSet<>(personToCopy.getFinancialPlans()); tags = new HashSet<>(personToCopy.getTags()); + appointment = personToCopy.getAppointment(); } /** @@ -65,6 +84,14 @@ public PersonBuilder withTags(String ... tags) { return this; } + /** + * Parses the {@code tags} into a {@code Set} and set it to the {@code Person} that we are building. + */ + public PersonBuilder withFinancialPlans(String ... financialPlans) { + this.financialPlans = SampleDataUtil.getFinancialPlanSet(financialPlans); + return this; + } + /** * Sets the {@code Address} of the {@code Person} that we are building. */ @@ -88,9 +115,44 @@ public PersonBuilder withEmail(String email) { this.email = new Email(email); return this; } + /** + * Sets the {@code NextOfKinName} of the {@code Person} that we are building. + */ + public PersonBuilder withNextOfKinName(String nokName) { + this.nextOfKinName = new NextOfKinName(nokName); + return this; + } + /** + * Sets the {@code NextOfKinPhone} of the {@code Person} that we are building. + */ + public PersonBuilder withNextOfKinPhone(String nokPhone) { + this.nextOfKinPhone = new NextOfKinPhone(nokPhone); + return this; + } - public Person build() { - return new Person(name, phone, email, address, tags); + /** + * Sets the {@code Appointment} of the {@code Person} that we are building. + */ + public PersonBuilder withAppointment(String appointment) { + this.appointment = Appointment.parseAppointmentDescription(appointment); + return this; + } + + /** + * Sets the {@code NullAppointment} of the {@code Person} that we are building. + */ + public PersonBuilder withNullAppointment() { + this.appointment = NullAppointment.getNullAppointment(); + return this; } + /** + * Builds a Person + * + * @return The Person built. + */ + public Person build() { + return new Person(name, phone, email, address, nextOfKinName, nextOfKinPhone, + financialPlans, tags, appointment); + } } diff --git a/src/test/java/seedu/address/testutil/PersonUtil.java b/src/test/java/seedu/address/testutil/PersonUtil.java index 90849945183..6f96213a164 100644 --- a/src/test/java/seedu/address/testutil/PersonUtil.java +++ b/src/test/java/seedu/address/testutil/PersonUtil.java @@ -2,7 +2,10 @@ import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_FINANCIAL_PLAN; import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NEXT_OF_KIN_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NEXT_OF_KIN_PHONE; import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; @@ -10,6 +13,7 @@ import seedu.address.logic.commands.AddCommand; import seedu.address.logic.commands.EditCommand.EditPersonDescriptor; +import seedu.address.model.financialplan.FinancialPlan; import seedu.address.model.person.Person; import seedu.address.model.tag.Tag; @@ -34,6 +38,11 @@ public static String getPersonDetails(Person person) { sb.append(PREFIX_PHONE + person.getPhone().value + " "); sb.append(PREFIX_EMAIL + person.getEmail().value + " "); sb.append(PREFIX_ADDRESS + person.getAddress().value + " "); + sb.append(PREFIX_NEXT_OF_KIN_NAME + person.getNextOfKinName().fullName + " "); + sb.append(PREFIX_NEXT_OF_KIN_PHONE + person.getNextOfKinPhone().value + " "); + person.getFinancialPlans().stream().forEach( + s -> sb.append(PREFIX_FINANCIAL_PLAN + s.financialPlanName + " ") + ); person.getTags().stream().forEach( s -> sb.append(PREFIX_TAG + s.tagName + " ") ); @@ -49,10 +58,26 @@ public static String getEditPersonDescriptorDetails(EditPersonDescriptor descrip descriptor.getPhone().ifPresent(phone -> sb.append(PREFIX_PHONE).append(phone.value).append(" ")); descriptor.getEmail().ifPresent(email -> sb.append(PREFIX_EMAIL).append(email.value).append(" ")); descriptor.getAddress().ifPresent(address -> sb.append(PREFIX_ADDRESS).append(address.value).append(" ")); + descriptor.getNextOfKinName() + .ifPresent(nextOfKinName -> sb.append(PREFIX_NEXT_OF_KIN_NAME) + .append(nextOfKinName.fullName) + .append(" ")); + descriptor.getNextOfKinPhone() + .ifPresent(nextOfKinPhone -> sb.append(PREFIX_NEXT_OF_KIN_PHONE) + .append(nextOfKinPhone.value) + .append(" ")); + if (descriptor.getFinancialPlans().isPresent()) { + Set financialPlans = descriptor.getFinancialPlans().get(); + if (financialPlans.isEmpty()) { + sb.append(PREFIX_FINANCIAL_PLAN).append(" "); + } else { + financialPlans.forEach(s -> sb.append(PREFIX_FINANCIAL_PLAN).append(s.financialPlanName).append(" ")); + } + } if (descriptor.getTags().isPresent()) { Set tags = descriptor.getTags().get(); if (tags.isEmpty()) { - sb.append(PREFIX_TAG); + sb.append(PREFIX_TAG).append(" "); } else { tags.forEach(s -> sb.append(PREFIX_TAG).append(s.tagName).append(" ")); } diff --git a/src/test/java/seedu/address/testutil/TypicalPersons.java b/src/test/java/seedu/address/testutil/TypicalPersons.java index fec76fb7129..3d8ba85be27 100644 --- a/src/test/java/seedu/address/testutil/TypicalPersons.java +++ b/src/test/java/seedu/address/testutil/TypicalPersons.java @@ -4,8 +4,14 @@ import static seedu.address.logic.commands.CommandTestUtil.VALID_ADDRESS_BOB; import static seedu.address.logic.commands.CommandTestUtil.VALID_EMAIL_AMY; import static seedu.address.logic.commands.CommandTestUtil.VALID_EMAIL_BOB; +import static seedu.address.logic.commands.CommandTestUtil.VALID_FINANCIAL_PLAN_1; +import static seedu.address.logic.commands.CommandTestUtil.VALID_FINANCIAL_PLAN_2; import static seedu.address.logic.commands.CommandTestUtil.VALID_NAME_AMY; import static seedu.address.logic.commands.CommandTestUtil.VALID_NAME_BOB; +import static seedu.address.logic.commands.CommandTestUtil.VALID_NEXT_OF_KIN_NAME_AMY; +import static seedu.address.logic.commands.CommandTestUtil.VALID_NEXT_OF_KIN_NAME_BOB; +import static seedu.address.logic.commands.CommandTestUtil.VALID_NEXT_OF_KIN_PHONE_AMY; +import static seedu.address.logic.commands.CommandTestUtil.VALID_NEXT_OF_KIN_PHONE_BOB; import static seedu.address.logic.commands.CommandTestUtil.VALID_PHONE_AMY; import static seedu.address.logic.commands.CommandTestUtil.VALID_PHONE_BOB; import static seedu.address.logic.commands.CommandTestUtil.VALID_TAG_FRIEND; @@ -25,35 +31,53 @@ public class TypicalPersons { public static final Person ALICE = new PersonBuilder().withName("Alice Pauline") .withAddress("123, Jurong West Ave 6, #08-111").withEmail("alice@example.com") - .withPhone("94351253") + .withPhone("94351253").withNextOfKinName("Alice Dad").withNextOfKinPhone("94351111") + .withFinancialPlans("Sample Financial Plan 1", "Sample Financial Plan 2") .withTags("friends").build(); public static final Person BENSON = new PersonBuilder().withName("Benson Meier") - .withAddress("311, Clementi Ave 2, #02-25") - .withEmail("johnd@example.com").withPhone("98765432") - .withTags("owesMoney", "friends").build(); + .withAddress("311, Clementi Ave 2, #02-25").withEmail("johnd@example.com") + .withPhone("98765432").withNextOfKinName("Benson Dad").withNextOfKinPhone("98761111") + .withFinancialPlans("Sample Financial Plan 1", "Sample Financial Plan 2") + .withTags("owesMoney", "friends").withAppointment("Review insurance, 01-05-2023 18:00").build(); + public static final Person CARL = new PersonBuilder().withName("Carl Kurz").withPhone("95352563") - .withEmail("heinz@example.com").withAddress("wall street").build(); + .withEmail("heinz@example.com").withNextOfKinName("Carl Dad") + .withNextOfKinPhone("95351111").withAddress("wall street") + .withAppointment("Review insurance, 01-05-2023 07:00").build(); public static final Person DANIEL = new PersonBuilder().withName("Daniel Meier").withPhone("87652533") - .withEmail("cornelia@example.com").withAddress("10th street").withTags("friends").build(); + .withEmail("cornelia@example.com").withAddress("10th street").withNextOfKinName("Daniel Dad") + .withNextOfKinPhone("87651111").withTags("friends").build(); public static final Person ELLE = new PersonBuilder().withName("Elle Meyer").withPhone("9482224") - .withEmail("werner@example.com").withAddress("michegan ave").build(); + .withEmail("werner@example.com").withAddress("michegan ave").withNextOfKinName("Elle Dad") + .withNextOfKinPhone("94821113") + .withFinancialPlans("Sample Financial Plan 2").build(); public static final Person FIONA = new PersonBuilder().withName("Fiona Kunz").withPhone("9482427") - .withEmail("lydia@example.com").withAddress("little tokyo").build(); + .withEmail("lydia@example.com").withAddress("little tokyo").withNextOfKinName("Fiona Dad") + .withNextOfKinPhone("94821111").build(); public static final Person GEORGE = new PersonBuilder().withName("George Best").withPhone("9482442") - .withEmail("anna@example.com").withAddress("4th street").build(); + .withFinancialPlans("Sample Financial Plan 1", "Sample Financial Plan 2") + .withEmail("anna@example.com").withAddress("4th street").withNextOfKinName("George Dad") + .withNextOfKinPhone("94821112").build(); // Manually added public static final Person HOON = new PersonBuilder().withName("Hoon Meier").withPhone("8482424") - .withEmail("stefan@example.com").withAddress("little india").build(); + .withEmail("stefan@example.com").withAddress("little india").withNextOfKinName("Hoon Dad") + .withFinancialPlans("Sample Financial Plan 1", "Sample Financial Plan 2") + .withNextOfKinPhone("84822222").build(); public static final Person IDA = new PersonBuilder().withName("Ida Mueller").withPhone("8482131") - .withEmail("hans@example.com").withAddress("chicago ave").build(); + .withEmail("hans@example.com").withAddress("chicago ave").withNextOfKinName("Ida Dad") + .withNextOfKinPhone("84822221").build(); // Manually added - Person's details found in {@code CommandTestUtil} public static final Person AMY = new PersonBuilder().withName(VALID_NAME_AMY).withPhone(VALID_PHONE_AMY) - .withEmail(VALID_EMAIL_AMY).withAddress(VALID_ADDRESS_AMY).withTags(VALID_TAG_FRIEND).build(); + .withEmail(VALID_EMAIL_AMY).withAddress(VALID_ADDRESS_AMY).withNextOfKinName(VALID_NEXT_OF_KIN_NAME_AMY) + .withNextOfKinPhone(VALID_NEXT_OF_KIN_PHONE_AMY).withFinancialPlans(VALID_FINANCIAL_PLAN_1) + .withTags(VALID_TAG_FRIEND).build(); public static final Person BOB = new PersonBuilder().withName(VALID_NAME_BOB).withPhone(VALID_PHONE_BOB) - .withEmail(VALID_EMAIL_BOB).withAddress(VALID_ADDRESS_BOB).withTags(VALID_TAG_HUSBAND, VALID_TAG_FRIEND) - .build(); + .withEmail(VALID_EMAIL_BOB).withAddress(VALID_ADDRESS_BOB).withNextOfKinName(VALID_NEXT_OF_KIN_NAME_BOB) + .withNextOfKinPhone(VALID_NEXT_OF_KIN_PHONE_BOB) + .withFinancialPlans(VALID_FINANCIAL_PLAN_1, VALID_FINANCIAL_PLAN_2) + .withTags(VALID_TAG_HUSBAND, VALID_TAG_FRIEND).build(); public static final String KEYWORD_MATCHING_MEIER = "Meier"; // A keyword that matches MEIER